mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
Merge branch 'develop' of github.com:makeplane/plane into develop
This commit is contained in:
commit
67a33c1352
91
.github/workflows/build-aio-base.yml
vendored
Normal file
91
.github/workflows/build-aio-base.yml
vendored
Normal file
@ -0,0 +1,91 @@
|
||||
name: Build AIO Base Image
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
TARGET_BRANCH: ${{ github.ref_name }}
|
||||
|
||||
jobs:
|
||||
base_build_setup:
|
||||
name: Build Preparation
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
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_base: ${{ steps.changed_files.outputs.base_any_changed }}
|
||||
|
||||
steps:
|
||||
- id: set_env_variables
|
||||
name: Set Environment Variables
|
||||
run: |
|
||||
echo "BUILDX_DRIVER=cloud" >> $GITHUB_OUTPUT
|
||||
echo "BUILDX_VERSION=lab:latest" >> $GITHUB_OUTPUT
|
||||
echo "BUILDX_PLATFORMS=linux/amd64,linux/arm64" >> $GITHUB_OUTPUT
|
||||
echo "BUILDX_ENDPOINT=makeplane/plane-dev" >> $GITHUB_OUTPUT
|
||||
echo "TARGET_BRANCH=${{ env.TARGET_BRANCH }}" >> $GITHUB_OUTPUT
|
||||
|
||||
- id: checkout_files
|
||||
name: Checkout Files
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Get changed files
|
||||
id: changed_files
|
||||
uses: tj-actions/changed-files@v42
|
||||
with:
|
||||
files_yaml: |
|
||||
base:
|
||||
- aio/Dockerfile.base
|
||||
|
||||
base_build_push:
|
||||
if: ${{ needs.base_build_setup.outputs.build_base == 'true' || github.event_name == 'workflow_dispatch' || needs.base_build_setup.outputs.gh_branch_name == 'master' }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: [base_build_setup]
|
||||
env:
|
||||
BASE_IMG_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-aio-base:${{ needs.base_build_setup.outputs.gh_branch_name }}
|
||||
TARGET_BRANCH: ${{ needs.base_build_setup.outputs.gh_branch_name }}
|
||||
BUILDX_DRIVER: ${{ needs.base_build_setup.outputs.gh_buildx_driver }}
|
||||
BUILDX_VERSION: ${{ needs.base_build_setup.outputs.gh_buildx_version }}
|
||||
BUILDX_PLATFORMS: ${{ needs.base_build_setup.outputs.gh_buildx_platforms }}
|
||||
BUILDX_ENDPOINT: ${{ needs.base_build_setup.outputs.gh_buildx_endpoint }}
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set Docker Tag
|
||||
run: |
|
||||
if [ "${{ env.TARGET_BRANCH }}" == "master" ]; then
|
||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-aio-base:latest
|
||||
else
|
||||
TAG=${{ env.BASE_IMG_TAG }}
|
||||
fi
|
||||
echo "BASE_IMG_TAG=${TAG}" >> $GITHUB_ENV
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
driver: ${{ env.BUILDX_DRIVER }}
|
||||
version: ${{ env.BUILDX_VERSION }}
|
||||
endpoint: ${{ env.BUILDX_ENDPOINT }}
|
||||
|
||||
- name: Build and Push to Docker Hub
|
||||
uses: docker/build-push-action@v5.1.0
|
||||
with:
|
||||
context: ./aio
|
||||
file: ./aio/Dockerfile.base
|
||||
platforms: ${{ env.BUILDX_PLATFORMS }}
|
||||
tags: ${{ env.BASE_IMG_TAG }}
|
||||
push: true
|
||||
env:
|
||||
DOCKER_BUILDKIT: 1
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
|
124
Dockerfile
124
Dockerfile
@ -1,124 +0,0 @@
|
||||
FROM node:18-alpine AS builder
|
||||
RUN apk add --no-cache libc6-compat
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
ENV NEXT_PUBLIC_API_BASE_URL=http://NEXT_PUBLIC_API_BASE_URL_PLACEHOLDER
|
||||
|
||||
RUN yarn global add turbo
|
||||
RUN apk add tree
|
||||
COPY . .
|
||||
|
||||
RUN turbo prune --scope=app --scope=plane-deploy --docker
|
||||
CMD tree -I node_modules/
|
||||
|
||||
# Add lockfile and package.json's of isolated subworkspace
|
||||
FROM node:18-alpine AS installer
|
||||
|
||||
RUN apk add --no-cache libc6-compat
|
||||
WORKDIR /app
|
||||
ARG NEXT_PUBLIC_API_BASE_URL=http://localhost:8000
|
||||
# First install the dependencies (as they change less often)
|
||||
COPY .gitignore .gitignore
|
||||
COPY --from=builder /app/out/json/ .
|
||||
COPY --from=builder /app/out/yarn.lock ./yarn.lock
|
||||
RUN yarn install
|
||||
|
||||
# # Build the project
|
||||
COPY --from=builder /app/out/full/ .
|
||||
COPY turbo.json turbo.json
|
||||
COPY replace-env-vars.sh /usr/local/bin/
|
||||
|
||||
RUN chmod +x /usr/local/bin/replace-env-vars.sh
|
||||
|
||||
RUN yarn turbo run build
|
||||
|
||||
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \
|
||||
BUILT_NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
|
||||
|
||||
RUN /usr/local/bin/replace-env-vars.sh http://NEXT_PUBLIC_WEBAPP_URL_PLACEHOLDER ${NEXT_PUBLIC_API_BASE_URL}
|
||||
|
||||
FROM python:3.11.1-alpine3.17 AS backend
|
||||
|
||||
# set environment variables
|
||||
ENV PYTHONDONTWRITEBYTECODE 1
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
|
||||
|
||||
WORKDIR /code
|
||||
|
||||
RUN apk --no-cache add \
|
||||
"libpq~=15" \
|
||||
"libxslt~=1.1" \
|
||||
"nodejs-current~=19" \
|
||||
"xmlsec~=1.2" \
|
||||
"nginx" \
|
||||
"nodejs" \
|
||||
"npm" \
|
||||
"supervisor"
|
||||
|
||||
COPY apiserver/requirements.txt ./
|
||||
COPY apiserver/requirements ./requirements
|
||||
RUN apk add --no-cache libffi-dev
|
||||
RUN apk add --no-cache --virtual .build-deps \
|
||||
"bash~=5.2" \
|
||||
"g++~=12.2" \
|
||||
"gcc~=12.2" \
|
||||
"cargo~=1.64" \
|
||||
"git~=2" \
|
||||
"make~=4.3" \
|
||||
"postgresql13-dev~=13" \
|
||||
"libc-dev" \
|
||||
"linux-headers" \
|
||||
&& \
|
||||
pip install -r requirements.txt --compile --no-cache-dir \
|
||||
&& \
|
||||
apk del .build-deps
|
||||
|
||||
# Add in Django deps and generate Django's static files
|
||||
COPY apiserver/manage.py manage.py
|
||||
COPY apiserver/plane plane/
|
||||
COPY apiserver/templates templates/
|
||||
|
||||
RUN apk --no-cache add "bash~=5.2"
|
||||
COPY apiserver/bin ./bin/
|
||||
|
||||
RUN chmod +x ./bin/*
|
||||
RUN chmod -R 777 /code
|
||||
|
||||
# Expose container port and run entry point script
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=installer /app/apps/app/next.config.js .
|
||||
COPY --from=installer /app/apps/app/package.json .
|
||||
COPY --from=installer /app/apps/space/next.config.js .
|
||||
COPY --from=installer /app/apps/space/package.json .
|
||||
|
||||
COPY --from=installer /app/apps/app/.next/standalone ./
|
||||
|
||||
COPY --from=installer /app/apps/app/.next/static ./apps/app/.next/static
|
||||
|
||||
COPY --from=installer /app/apps/space/.next/standalone ./
|
||||
COPY --from=installer /app/apps/space/.next ./apps/space/.next
|
||||
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
|
||||
# RUN rm /etc/nginx/conf.d/default.conf
|
||||
#######################################################################
|
||||
COPY nginx/nginx-single-docker-image.conf /etc/nginx/http.d/default.conf
|
||||
#######################################################################
|
||||
|
||||
COPY nginx/supervisor.conf /code/supervisor.conf
|
||||
|
||||
ARG NEXT_PUBLIC_API_BASE_URL=http://localhost:8000
|
||||
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \
|
||||
BUILT_NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
|
||||
|
||||
COPY replace-env-vars.sh /usr/local/bin/
|
||||
COPY start.sh /usr/local/bin/
|
||||
RUN chmod +x /usr/local/bin/replace-env-vars.sh
|
||||
RUN chmod +x /usr/local/bin/start.sh
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
CMD ["supervisord","-c","/code/supervisor.conf"]
|
@ -1,4 +1,5 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
|
||||
const nextConfig = {
|
||||
trailingSlash: true,
|
||||
reactStrictMode: false,
|
||||
|
149
aio/Dockerfile
Normal file
149
aio/Dockerfile
Normal file
@ -0,0 +1,149 @@
|
||||
# *****************************************************************************
|
||||
# STAGE 1: Build the project
|
||||
# *****************************************************************************
|
||||
FROM node:18-alpine AS builder
|
||||
RUN apk add --no-cache libc6-compat
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
ENV NEXT_PUBLIC_API_BASE_URL=http://NEXT_PUBLIC_API_BASE_URL_PLACEHOLDER
|
||||
|
||||
RUN yarn global add turbo
|
||||
COPY . .
|
||||
|
||||
RUN turbo prune --scope=web --scope=space --scope=admin --docker
|
||||
|
||||
# *****************************************************************************
|
||||
# STAGE 2: Install dependencies & build the project
|
||||
# *****************************************************************************
|
||||
# Add lockfile and package.json's of isolated subworkspace
|
||||
FROM node:18-alpine AS installer
|
||||
|
||||
RUN apk add --no-cache libc6-compat
|
||||
WORKDIR /app
|
||||
|
||||
# First install the dependencies (as they change less often)
|
||||
COPY .gitignore .gitignore
|
||||
COPY --from=builder /app/out/json/ .
|
||||
COPY --from=builder /app/out/yarn.lock ./yarn.lock
|
||||
RUN yarn install
|
||||
|
||||
# # Build the project
|
||||
COPY --from=builder /app/out/full/ .
|
||||
COPY turbo.json turbo.json
|
||||
|
||||
ARG NEXT_PUBLIC_API_BASE_URL=""
|
||||
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
|
||||
|
||||
ARG NEXT_PUBLIC_ADMIN_BASE_URL=""
|
||||
ENV NEXT_PUBLIC_ADMIN_BASE_URL=$NEXT_PUBLIC_ADMIN_BASE_URL
|
||||
|
||||
ARG NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode"
|
||||
ENV NEXT_PUBLIC_ADMIN_BASE_PATH=$NEXT_PUBLIC_ADMIN_BASE_PATH
|
||||
|
||||
ARG NEXT_PUBLIC_SPACE_BASE_URL=""
|
||||
ENV NEXT_PUBLIC_SPACE_BASE_URL=$NEXT_PUBLIC_SPACE_BASE_URL
|
||||
|
||||
ARG NEXT_PUBLIC_SPACE_BASE_PATH="/spaces"
|
||||
ENV NEXT_PUBLIC_SPACE_BASE_PATH=$NEXT_PUBLIC_SPACE_BASE_PATH
|
||||
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
ENV TURBO_TELEMETRY_DISABLED 1
|
||||
|
||||
RUN yarn turbo run build
|
||||
|
||||
# *****************************************************************************
|
||||
# STAGE 3: Copy the project and start it
|
||||
# *****************************************************************************
|
||||
# FROM makeplane/plane-aio-base AS runner
|
||||
FROM makeplane/plane-aio-base:develop AS runner
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
SHELL [ "/bin/bash", "-c" ]
|
||||
|
||||
# PYTHON APPLICATION SETUP
|
||||
|
||||
ENV PYTHONDONTWRITEBYTECODE 1
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
|
||||
|
||||
COPY apiserver/requirements.txt ./api/
|
||||
COPY apiserver/requirements ./api/requirements
|
||||
|
||||
RUN python3.12 -m venv /app/venv && \
|
||||
source /app/venv/bin/activate && \
|
||||
/app/venv/bin/pip install --upgrade pip && \
|
||||
/app/venv/bin/pip install -r ./api/requirements.txt --compile --no-cache-dir
|
||||
|
||||
# Add in Django deps and generate Django's static files
|
||||
COPY apiserver/manage.py ./api/manage.py
|
||||
COPY apiserver/plane ./api/plane/
|
||||
COPY apiserver/templates ./api/templates/
|
||||
COPY package.json ./api/package.json
|
||||
|
||||
COPY apiserver/bin ./api/bin/
|
||||
|
||||
RUN chmod +x ./api/bin/*
|
||||
RUN chmod -R 777 ./api/
|
||||
|
||||
# NEXTJS BUILDS
|
||||
|
||||
COPY --from=installer /app/web/next.config.js ./web/
|
||||
COPY --from=installer /app/web/package.json ./web/
|
||||
COPY --from=installer /app/web/.next/standalone ./web
|
||||
COPY --from=installer /app/web/.next/static ./web/web/.next/static
|
||||
COPY --from=installer /app/web/public ./web/web/public
|
||||
|
||||
COPY --from=installer /app/space/next.config.js ./space/
|
||||
COPY --from=installer /app/space/package.json ./space/
|
||||
COPY --from=installer /app/space/.next/standalone ./space
|
||||
COPY --from=installer /app/space/.next/static ./space/space/.next/static
|
||||
COPY --from=installer /app/space/public ./space/space/public
|
||||
|
||||
COPY --from=installer /app/admin/next.config.js ./admin/
|
||||
COPY --from=installer /app/admin/package.json ./admin/
|
||||
COPY --from=installer /app/admin/.next/standalone ./admin
|
||||
COPY --from=installer /app/admin/.next/static ./admin/admin/.next/static
|
||||
COPY --from=installer /app/admin/public ./admin/admin/public
|
||||
|
||||
ARG NEXT_PUBLIC_API_BASE_URL=""
|
||||
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
|
||||
|
||||
ARG NEXT_PUBLIC_ADMIN_BASE_URL=""
|
||||
ENV NEXT_PUBLIC_ADMIN_BASE_URL=$NEXT_PUBLIC_ADMIN_BASE_URL
|
||||
|
||||
ARG NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode"
|
||||
ENV NEXT_PUBLIC_ADMIN_BASE_PATH=$NEXT_PUBLIC_ADMIN_BASE_PATH
|
||||
|
||||
ARG NEXT_PUBLIC_SPACE_BASE_URL=""
|
||||
ENV NEXT_PUBLIC_SPACE_BASE_URL=$NEXT_PUBLIC_SPACE_BASE_URL
|
||||
|
||||
ARG NEXT_PUBLIC_SPACE_BASE_PATH="/spaces"
|
||||
ENV NEXT_PUBLIC_SPACE_BASE_PATH=$NEXT_PUBLIC_SPACE_BASE_PATH
|
||||
|
||||
ARG NEXT_PUBLIC_WEB_BASE_URL=""
|
||||
ENV NEXT_PUBLIC_WEB_BASE_URL=$NEXT_PUBLIC_WEB_BASE_URL
|
||||
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
ENV TURBO_TELEMETRY_DISABLED 1
|
||||
|
||||
COPY aio/supervisord.conf /app/supervisord.conf
|
||||
|
||||
COPY aio/aio.sh /app/aio.sh
|
||||
RUN chmod +x /app/aio.sh
|
||||
|
||||
COPY aio/pg-setup.sh /app/pg-setup.sh
|
||||
RUN chmod +x /app/pg-setup.sh
|
||||
|
||||
COPY deploy/selfhost/variables.env /app/plane.env
|
||||
|
||||
# NGINX Conf Copy
|
||||
COPY ./aio/nginx.conf.aio /etc/nginx/nginx.conf.template
|
||||
COPY ./nginx/env.sh /app/nginx-start.sh
|
||||
RUN chmod +x /app/nginx-start.sh
|
||||
|
||||
RUN ./pg-setup.sh
|
||||
|
||||
VOLUME [ "/app/data/minio/uploads", "/var/lib/postgresql/data" ]
|
||||
|
||||
CMD ["/usr/bin/supervisord", "-c", "/app/supervisord.conf"]
|
92
aio/Dockerfile.base
Normal file
92
aio/Dockerfile.base
Normal file
@ -0,0 +1,92 @@
|
||||
FROM --platform=$BUILDPLATFORM tonistiigi/binfmt as binfmt
|
||||
|
||||
FROM debian:12-slim
|
||||
|
||||
# Set environment variables to non-interactive for apt
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
SHELL [ "/bin/bash", "-c" ]
|
||||
|
||||
# Update the package list and install prerequisites
|
||||
RUN apt-get update && \
|
||||
apt-get install -y \
|
||||
gnupg2 curl ca-certificates lsb-release software-properties-common \
|
||||
build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev \
|
||||
libsqlite3-dev wget llvm libncurses5-dev libncursesw5-dev xz-utils \
|
||||
tk-dev libffi-dev liblzma-dev supervisor nginx nano vim ncdu
|
||||
|
||||
# Install Redis 7.2
|
||||
RUN echo "deb http://deb.debian.org/debian $(lsb_release -cs)-backports main" > /etc/apt/sources.list.d/backports.list && \
|
||||
curl -fsSL https://packages.redis.io/gpg | gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpg && \
|
||||
echo "deb [signed-by=/usr/share/keyrings/redis-archive-keyring.gpg] https://packages.redis.io/deb $(lsb_release -cs) main" > /etc/apt/sources.list.d/redis.list && \
|
||||
apt-get update && \
|
||||
apt-get install -y redis-server
|
||||
|
||||
# Install PostgreSQL 15
|
||||
ENV POSTGRES_VERSION 15
|
||||
RUN curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor -o /usr/share/keyrings/pgdg-archive-keyring.gpg && \
|
||||
echo "deb [signed-by=/usr/share/keyrings/pgdg-archive-keyring.gpg] http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list && \
|
||||
apt-get update && \
|
||||
apt-get install -y postgresql-$POSTGRES_VERSION postgresql-client-$POSTGRES_VERSION && \
|
||||
mkdir -p /var/lib/postgresql/data && \
|
||||
chown -R postgres:postgres /var/lib/postgresql
|
||||
|
||||
# Install MinIO
|
||||
ARG TARGETARCH
|
||||
RUN if [ "$TARGETARCH" = "amd64" ]; then \
|
||||
curl -fSl https://dl.min.io/server/minio/release/linux-amd64/minio -o /usr/local/bin/minio; \
|
||||
elif [ "$TARGETARCH" = "arm64" ]; then \
|
||||
curl -fSl https://dl.min.io/server/minio/release/linux-arm64/minio -o /usr/local/bin/minio; \
|
||||
else \
|
||||
echo "Unsupported architecture: $TARGETARCH"; exit 1; \
|
||||
fi && \
|
||||
chmod +x /usr/local/bin/minio
|
||||
|
||||
|
||||
# Install Node.js 18
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_18.x | bash - && \
|
||||
apt-get install -y nodejs
|
||||
|
||||
# Install Python 3.12 from source
|
||||
RUN cd /usr/src && \
|
||||
wget https://www.python.org/ftp/python/3.12.0/Python-3.12.0.tgz && \
|
||||
tar xzf Python-3.12.0.tgz && \
|
||||
cd Python-3.12.0 && \
|
||||
./configure --enable-optimizations && \
|
||||
make altinstall && \
|
||||
rm -f /usr/src/Python-3.12.0.tgz
|
||||
|
||||
RUN python3.12 -m pip install --upgrade pip
|
||||
|
||||
RUN echo "alias python=/usr/local/bin/python3.12" >> ~/.bashrc && \
|
||||
echo "alias pip=/usr/local/bin/pip3.12" >> ~/.bashrc
|
||||
|
||||
# Clean up
|
||||
RUN apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/* /usr/src/Python-3.12.0
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN mkdir -p /app/{data,logs} && \
|
||||
mkdir -p /app/data/{redis,pg,minio,nginx} && \
|
||||
mkdir -p /app/logs/{access,error} && \
|
||||
mkdir -p /etc/supervisor/conf.d
|
||||
|
||||
# Create Supervisor configuration file
|
||||
COPY supervisord.base /app/supervisord.conf
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y sudo lsof net-tools libpq-dev procps gettext && \
|
||||
apt-get clean
|
||||
|
||||
RUN sudo -u postgres /usr/lib/postgresql/$POSTGRES_VERSION/bin/initdb -D /var/lib/postgresql/data
|
||||
COPY postgresql.conf /etc/postgresql/postgresql.conf
|
||||
|
||||
RUN echo "alias python=/usr/local/bin/python3.12" >> ~/.bashrc && \
|
||||
echo "alias pip=/usr/local/bin/pip3.12" >> ~/.bashrc
|
||||
|
||||
# Expose ports for Redis, PostgreSQL, and MinIO
|
||||
EXPOSE 6379 5432 9000 80
|
||||
|
||||
# Start Supervisor
|
||||
CMD ["/usr/bin/supervisord", "-c", "/app/supervisord.conf"]
|
30
aio/aio.sh
Normal file
30
aio/aio.sh
Normal file
@ -0,0 +1,30 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
|
||||
if [ "$1" = 'api' ]; then
|
||||
source /app/venv/bin/activate
|
||||
cd /app/api
|
||||
exec ./bin/docker-entrypoint-api.sh
|
||||
elif [ "$1" = 'worker' ]; then
|
||||
source /app/venv/bin/activate
|
||||
cd /app/api
|
||||
exec ./bin/docker-entrypoint-worker.sh
|
||||
elif [ "$1" = 'beat' ]; then
|
||||
source /app/venv/bin/activate
|
||||
cd /app/api
|
||||
exec ./bin/docker-entrypoint-beat.sh
|
||||
elif [ "$1" = 'migrator' ]; then
|
||||
source /app/venv/bin/activate
|
||||
cd /app/api
|
||||
exec ./bin/docker-entrypoint-migrator.sh
|
||||
elif [ "$1" = 'web' ]; then
|
||||
node /app/web/web/server.js
|
||||
elif [ "$1" = 'space' ]; then
|
||||
node /app/space/space/server.js
|
||||
elif [ "$1" = 'admin' ]; then
|
||||
node /app/admin/admin/server.js
|
||||
else
|
||||
echo "Command not found"
|
||||
exit 1
|
||||
fi
|
73
aio/nginx.conf.aio
Normal file
73
aio/nginx.conf.aio
Normal file
@ -0,0 +1,73 @@
|
||||
events {
|
||||
}
|
||||
|
||||
http {
|
||||
sendfile on;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
root /www/data/;
|
||||
access_log /var/log/nginx/access.log;
|
||||
|
||||
client_max_body_size ${FILE_SIZE_LIMIT};
|
||||
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header Referrer-Policy "no-referrer-when-downgrade" always;
|
||||
add_header Permissions-Policy "interest-cohort=()" always;
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
add_header X-Forwarded-Proto "${dollar}scheme";
|
||||
add_header X-Forwarded-Host "${dollar}host";
|
||||
add_header X-Forwarded-For "${dollar}proxy_add_x_forwarded_for";
|
||||
add_header X-Real-IP "${dollar}remote_addr";
|
||||
|
||||
location / {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade ${dollar}http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host ${dollar}http_host;
|
||||
proxy_pass http://localhost:3001/;
|
||||
}
|
||||
|
||||
location /spaces/ {
|
||||
rewrite ^/spaces/?$ /spaces/login break;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade ${dollar}http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host ${dollar}http_host;
|
||||
proxy_pass http://localhost:3002/spaces/;
|
||||
}
|
||||
|
||||
|
||||
location /god-mode/ {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade ${dollar}http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host ${dollar}http_host;
|
||||
proxy_pass http://localhost:3003/god-mode/;
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade ${dollar}http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host ${dollar}http_host;
|
||||
proxy_pass http://localhost:8000/api/;
|
||||
}
|
||||
|
||||
location /auth/ {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade ${dollar}http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host ${dollar}http_host;
|
||||
proxy_pass http://localhost:8000/auth/;
|
||||
}
|
||||
|
||||
location /${BUCKET_NAME}/ {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade ${dollar}http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host ${dollar}http_host;
|
||||
proxy_pass http://localhost:9000/uploads/;
|
||||
}
|
||||
}
|
||||
}
|
14
aio/pg-setup.sh
Normal file
14
aio/pg-setup.sh
Normal file
@ -0,0 +1,14 @@
|
||||
#!/bin/bash
|
||||
|
||||
|
||||
# Variables
|
||||
set -o allexport
|
||||
source plane.env set
|
||||
set +o allexport
|
||||
|
||||
export PGHOST=localhost
|
||||
|
||||
sudo -u postgres "/usr/lib/postgresql/${POSTGRES_VERSION}/bin/pg_ctl" -D /var/lib/postgresql/data start
|
||||
sudo -u postgres "/usr/lib/postgresql/${POSTGRES_VERSION}/bin/psql" --command "CREATE USER $POSTGRES_USER WITH SUPERUSER PASSWORD '$POSTGRES_PASSWORD';" && \
|
||||
sudo -u postgres "/usr/lib/postgresql/${POSTGRES_VERSION}/bin/createdb" -O "$POSTGRES_USER" "$POSTGRES_DB" && \
|
||||
sudo -u postgres "/usr/lib/postgresql/${POSTGRES_VERSION}/bin/pg_ctl" -D /var/lib/postgresql/data stop
|
12
aio/postgresql.conf
Normal file
12
aio/postgresql.conf
Normal file
@ -0,0 +1,12 @@
|
||||
# PostgreSQL configuration file
|
||||
|
||||
# Allow connections from any IP address
|
||||
listen_addresses = '*'
|
||||
|
||||
# Set the maximum number of connections
|
||||
max_connections = 100
|
||||
|
||||
# Set the shared buffers size
|
||||
shared_buffers = 128MB
|
||||
|
||||
# Other custom configurations can be added here
|
37
aio/supervisord.base
Normal file
37
aio/supervisord.base
Normal file
@ -0,0 +1,37 @@
|
||||
[supervisord]
|
||||
user=root
|
||||
nodaemon=true
|
||||
stderr_logfile=/app/logs/error/supervisor.err.log
|
||||
stdout_logfile=/app/logs/access/supervisor.out.log
|
||||
|
||||
[program:redis]
|
||||
directory=/app/data/redis
|
||||
command=redis-server
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stderr_logfile=/app/logs/error/redis.err.log
|
||||
stdout_logfile=/app/logs/access/redis.out.log
|
||||
|
||||
[program:postgresql]
|
||||
user=postgres
|
||||
command=/usr/lib/postgresql/15/bin/postgres --config-file=/etc/postgresql/15/main/postgresql.conf
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stderr_logfile=/app/logs/error/postgresql.err.log
|
||||
stdout_logfile=/app/logs/access/postgresql.out.log
|
||||
|
||||
[program:minio]
|
||||
directory=/app/data/minio
|
||||
command=minio server /app/data/minio
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stderr_logfile=/app/logs/error/minio.err.log
|
||||
stdout_logfile=/app/logs/access/minio.out.log
|
||||
|
||||
[program:nginx]
|
||||
directory=/app/data/nginx
|
||||
command=/usr/sbin/nginx -g 'daemon off;'
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stderr_logfile=/app/logs/error/nginx.err.log
|
||||
stdout_logfile=/app/logs/access/nginx.out.log
|
115
aio/supervisord.conf
Normal file
115
aio/supervisord.conf
Normal file
@ -0,0 +1,115 @@
|
||||
[supervisord]
|
||||
user=root
|
||||
nodaemon=true
|
||||
priority=1
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stdout
|
||||
stderr_logfile_maxbytes=0
|
||||
|
||||
[program:redis]
|
||||
directory=/app/data/redis
|
||||
command=redis-server
|
||||
autostart=true
|
||||
autorestart=true
|
||||
priority=1
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stdout
|
||||
stderr_logfile_maxbytes=0
|
||||
|
||||
[program:postgresql]
|
||||
user=postgres
|
||||
command=/usr/lib/postgresql/15/bin/postgres -D /var/lib/postgresql/data --config-file=/etc/postgresql/postgresql.conf
|
||||
autostart=true
|
||||
autorestart=true
|
||||
priority=1
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stdout
|
||||
stderr_logfile_maxbytes=0
|
||||
|
||||
[program:minio]
|
||||
directory=/app/data/minio
|
||||
command=minio server /app/data/minio
|
||||
autostart=true
|
||||
autorestart=true
|
||||
priority=1
|
||||
stdout_logfile=/app/logs/access/minio.log
|
||||
stderr_logfile=/app/logs/error/minio.err.log
|
||||
|
||||
[program:nginx]
|
||||
command=/app/nginx-start.sh
|
||||
autostart=true
|
||||
autorestart=true
|
||||
priority=1
|
||||
stdout_logfile=/app/logs/access/nginx.log
|
||||
stderr_logfile=/app/logs/error/nginx.err.log
|
||||
|
||||
|
||||
[program:web]
|
||||
command=/app/aio.sh web
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stdout
|
||||
stderr_logfile_maxbytes=0
|
||||
environment=PORT=3001,HOSTNAME=0.0.0.0
|
||||
|
||||
[program:space]
|
||||
command=/app/aio.sh space
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stdout
|
||||
stderr_logfile_maxbytes=0
|
||||
environment=PORT=3002,HOSTNAME=0.0.0.0
|
||||
|
||||
[program:admin]
|
||||
command=/app/aio.sh admin
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stdout
|
||||
stderr_logfile_maxbytes=0
|
||||
environment=PORT=3003,HOSTNAME=0.0.0.0
|
||||
|
||||
[program:migrator]
|
||||
command=/app/aio.sh migrator
|
||||
autostart=true
|
||||
autorestart=false
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stdout
|
||||
stderr_logfile_maxbytes=0
|
||||
|
||||
[program:api]
|
||||
command=/app/aio.sh api
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stdout
|
||||
stderr_logfile_maxbytes=0
|
||||
|
||||
[program:worker]
|
||||
command=/app/aio.sh worker
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stdout
|
||||
stderr_logfile_maxbytes=0
|
||||
|
||||
[program:beat]
|
||||
command=/app/aio.sh beat
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stdout
|
||||
stderr_logfile_maxbytes=0
|
||||
|
@ -1,5 +1,5 @@
|
||||
# Python imports
|
||||
# import uuid
|
||||
import uuid
|
||||
|
||||
# Django imports
|
||||
from django.db.models import Case, Count, IntegerField, Q, When
|
||||
@ -183,8 +183,8 @@ class UserEndpoint(BaseViewSet):
|
||||
profile.save()
|
||||
|
||||
# Reset password
|
||||
# user.is_password_autoset = True
|
||||
# user.set_password(uuid.uuid4().hex)
|
||||
user.is_password_autoset = True
|
||||
user.set_password(uuid.uuid4().hex)
|
||||
|
||||
# Deactivate the user
|
||||
user.is_active = False
|
||||
|
@ -8,6 +8,10 @@ from django.utils import timezone
|
||||
from plane.db.models import Account
|
||||
|
||||
from .base import Adapter
|
||||
from plane.authentication.adapter.error import (
|
||||
AuthenticationException,
|
||||
AUTHENTICATION_ERROR_CODES,
|
||||
)
|
||||
|
||||
|
||||
class OauthAdapter(Adapter):
|
||||
@ -50,20 +54,42 @@ class OauthAdapter(Adapter):
|
||||
return self.complete_login_or_signup()
|
||||
|
||||
def get_user_token(self, data, headers=None):
|
||||
headers = headers or {}
|
||||
response = requests.post(
|
||||
self.get_token_url(), data=data, headers=headers
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
try:
|
||||
headers = headers or {}
|
||||
response = requests.post(
|
||||
self.get_token_url(), data=data, headers=headers
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except requests.RequestException:
|
||||
code = (
|
||||
"GOOGLE_OAUTH_PROVIDER_ERROR"
|
||||
if self.provider == "google"
|
||||
else "GITHUB_OAUTH_PROVIDER_ERROR"
|
||||
)
|
||||
raise AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES[code],
|
||||
error_message=str(code),
|
||||
)
|
||||
|
||||
def get_user_response(self):
|
||||
headers = {
|
||||
"Authorization": f"Bearer {self.token_data.get('access_token')}"
|
||||
}
|
||||
response = requests.get(self.get_user_info_url(), headers=headers)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
try:
|
||||
headers = {
|
||||
"Authorization": f"Bearer {self.token_data.get('access_token')}"
|
||||
}
|
||||
response = requests.get(self.get_user_info_url(), headers=headers)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except requests.RequestException:
|
||||
code = (
|
||||
"GOOGLE_OAUTH_PROVIDER_ERROR"
|
||||
if self.provider == "google"
|
||||
else "GITHUB_OAUTH_PROVIDER_ERROR"
|
||||
)
|
||||
raise AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES[code],
|
||||
error_message=str(code),
|
||||
)
|
||||
|
||||
def set_user_data(self, data):
|
||||
self.user_data = data
|
||||
|
@ -105,14 +105,26 @@ class GitHubOAuthProvider(OauthAdapter):
|
||||
)
|
||||
|
||||
def __get_email(self, headers):
|
||||
# Github does not provide email in user response
|
||||
emails_url = "https://api.github.com/user/emails"
|
||||
emails_response = requests.get(emails_url, headers=headers).json()
|
||||
email = next(
|
||||
(email["email"] for email in emails_response if email["primary"]),
|
||||
None,
|
||||
)
|
||||
return email
|
||||
try:
|
||||
# Github does not provide email in user response
|
||||
emails_url = "https://api.github.com/user/emails"
|
||||
emails_response = requests.get(emails_url, headers=headers).json()
|
||||
email = next(
|
||||
(
|
||||
email["email"]
|
||||
for email in emails_response
|
||||
if email["primary"]
|
||||
),
|
||||
None,
|
||||
)
|
||||
return email
|
||||
except requests.RequestException:
|
||||
raise AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES[
|
||||
"GITHUB_OAUTH_PROVIDER_ERROR"
|
||||
],
|
||||
error_message="GITHUB_OAUTH_PROVIDER_ERROR",
|
||||
)
|
||||
|
||||
def set_user_data(self):
|
||||
user_info_response = self.get_user_response()
|
||||
|
@ -128,7 +128,7 @@ services:
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-stable}
|
||||
platform: ${DOCKER_PLATFORM:-}
|
||||
pull_policy: ${PULL_POLICY:-always}
|
||||
restart: no
|
||||
restart: "no"
|
||||
command: ./bin/docker-entrypoint-migrator.sh
|
||||
volumes:
|
||||
- logs_migrator:/code/plane/logs
|
||||
|
@ -1,32 +0,0 @@
|
||||
[supervisord] ## This is the main process for the Supervisor
|
||||
nodaemon=true
|
||||
|
||||
[program:node]
|
||||
command=sh /usr/local/bin/start.sh
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stderr_logfile=/var/log/node.err.log
|
||||
stdout_logfile=/var/log/node.out.log
|
||||
|
||||
[program:python]
|
||||
directory=/code
|
||||
command=sh bin/docker-entrypoint-api.sh
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stderr_logfile=/var/log/python.err.log
|
||||
stdout_logfile=/var/log/python.out.log
|
||||
|
||||
[program:nginx]
|
||||
command=nginx -g "daemon off;"
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stderr_logfile=/var/log/nginx.err.log
|
||||
stdout_logfile=/var/log/nginx.out.log
|
||||
|
||||
[program:worker]
|
||||
directory=/code
|
||||
command=sh bin/worker
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stderr_logfile=/var/log/worker.err.log
|
||||
stdout_logfile=/var/log/worker.out.log
|
@ -0,0 +1,25 @@
|
||||
import { Extension } from "@tiptap/core";
|
||||
|
||||
export const EnterKeyExtension = (onEnterKeyPress?: () => void) =>
|
||||
Extension.create({
|
||||
name: "enterKey",
|
||||
|
||||
addKeyboardShortcuts(this) {
|
||||
return {
|
||||
Enter: () => {
|
||||
if (onEnterKeyPress) {
|
||||
onEnterKeyPress();
|
||||
}
|
||||
return true;
|
||||
},
|
||||
"Shift-Enter": ({ editor }) =>
|
||||
editor.commands.first(({ commands }) => [
|
||||
() => commands.newlineInCode(),
|
||||
() => commands.splitListItem("listItem"),
|
||||
() => commands.createParagraphNear(),
|
||||
() => commands.liftEmptyBlock(),
|
||||
() => commands.splitBlock(),
|
||||
]),
|
||||
};
|
||||
},
|
||||
});
|
@ -1,13 +1,21 @@
|
||||
import { UploadImage } from "@plane/editor-core";
|
||||
import { DragAndDrop, SlashCommand } from "@plane/editor-extensions";
|
||||
import { EnterKeyExtension } from "./enter-key-extension";
|
||||
|
||||
type TArguments = {
|
||||
uploadFile: UploadImage;
|
||||
dragDropEnabled?: boolean;
|
||||
setHideDragHandle?: (hideDragHandlerFromDragDrop: () => void) => void;
|
||||
onEnterKeyPress?: () => void;
|
||||
};
|
||||
|
||||
export const RichTextEditorExtensions = ({ uploadFile, dragDropEnabled, setHideDragHandle }: TArguments) => [
|
||||
export const RichTextEditorExtensions = ({
|
||||
uploadFile,
|
||||
dragDropEnabled,
|
||||
setHideDragHandle,
|
||||
onEnterKeyPress,
|
||||
}: TArguments) => [
|
||||
SlashCommand(uploadFile),
|
||||
dragDropEnabled === true && DragAndDrop(setHideDragHandle),
|
||||
EnterKeyExtension(onEnterKeyPress),
|
||||
];
|
||||
|
@ -33,6 +33,7 @@ export type IRichTextEditor = {
|
||||
};
|
||||
placeholder?: string | ((isFocused: boolean, value: string) => string);
|
||||
tabIndex?: number;
|
||||
onEnterKeyPress?: (e?: any) => void;
|
||||
};
|
||||
|
||||
const RichTextEditor = (props: IRichTextEditor) => {
|
||||
@ -50,6 +51,7 @@ const RichTextEditor = (props: IRichTextEditor) => {
|
||||
placeholder,
|
||||
tabIndex,
|
||||
mentionHandler,
|
||||
onEnterKeyPress,
|
||||
} = props;
|
||||
|
||||
const [hideDragHandleOnMouseLeave, setHideDragHandleOnMouseLeave] = React.useState<() => void>(() => {});
|
||||
@ -73,6 +75,7 @@ const RichTextEditor = (props: IRichTextEditor) => {
|
||||
uploadFile: fileHandler.upload,
|
||||
dragDropEnabled,
|
||||
setHideDragHandle: setHideDragHandleFunction,
|
||||
onEnterKeyPress,
|
||||
}),
|
||||
tabIndex,
|
||||
mentionHandler,
|
||||
|
@ -19,4 +19,3 @@ export * from "./priority-icon";
|
||||
export * from "./related-icon";
|
||||
export * from "./side-panel-icon";
|
||||
export * from "./transfer-icon";
|
||||
export * from "./user-group-icon";
|
||||
|
@ -1,35 +0,0 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { ISvgIcons } from "./type";
|
||||
|
||||
export const UserGroupIcon: React.FC<ISvgIcons> = ({ className = "text-current", ...rest }) => (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
className={`${className} stroke-2`}
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...rest}
|
||||
>
|
||||
<path
|
||||
d="M18 19C18 17.4087 17.3679 15.8826 16.2426 14.7574C15.1174 13.6321 13.5913 13 12 13C10.4087 13 8.88258 13.6321 7.75736 14.7574C6.63214 15.8826 6 17.4087 6 19"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M12 13C14.2091 13 16 11.2091 16 9C16 6.79086 14.2091 5 12 5C9.79086 5 8 6.79086 8 9C8 11.2091 9.79086 13 12 13Z"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M23 18C23 16.636 22.4732 15.3279 21.5355 14.3635C20.5979 13.399 19.3261 12.8571 18 12.8571C18.8841 12.8571 19.7319 12.4959 20.357 11.8529C20.9821 11.21 21.3333 10.3379 21.3333 9.42857C21.3333 8.51926 20.9821 7.64719 20.357 7.00421C19.7319 6.36122 18.8841 6 18 6"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M1 18C1 16.636 1.52678 15.3279 2.46447 14.3635C3.40215 13.399 4.67392 12.8571 6 12.8571C5.11595 12.8571 4.2681 12.4959 3.64298 11.8529C3.01786 11.21 2.66667 10.3379 2.66667 9.42857C2.66667 8.51926 3.01786 7.64719 3.64298 7.00421C4.2681 6.36122 5.11595 6 6 6"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
3
space/.gitignore
vendored
3
space/.gitignore
vendored
@ -37,3 +37,6 @@ next-env.d.ts
|
||||
|
||||
# env
|
||||
.env
|
||||
|
||||
# Sentry Config File
|
||||
.env.sentry-build-plugin
|
||||
|
9
space/instrumentation.ts
Normal file
9
space/instrumentation.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export async function register() {
|
||||
if (process.env.NEXT_RUNTIME === 'nodejs') {
|
||||
await import('./sentry.server.config');
|
||||
}
|
||||
|
||||
if (process.env.NEXT_RUNTIME === 'edge') {
|
||||
await import('./sentry.edge.config');
|
||||
}
|
||||
}
|
@ -28,12 +28,46 @@ const nextConfig = {
|
||||
},
|
||||
};
|
||||
|
||||
if (parseInt(process.env.NEXT_PUBLIC_ENABLE_SENTRY || "0", 10)) {
|
||||
module.exports = withSentryConfig(
|
||||
nextConfig,
|
||||
{ silent: true, authToken: process.env.SENTRY_AUTH_TOKEN },
|
||||
{ hideSourceMaps: true }
|
||||
);
|
||||
|
||||
const sentryConfig = {
|
||||
// For all available options, see:
|
||||
// https://github.com/getsentry/sentry-webpack-plugin#options
|
||||
|
||||
org: process.env.SENTRY_ORG_ID || "plane-hq",
|
||||
project: process.env.SENTRY_PROJECT_ID || "plane-space",
|
||||
authToken: process.env.SENTRY_AUTH_TOKEN,
|
||||
// Only print logs for uploading source maps in CI
|
||||
silent: true,
|
||||
|
||||
// For all available options, see:
|
||||
// https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/
|
||||
|
||||
// Upload a larger set of source maps for prettier stack traces (increases build time)
|
||||
widenClientFileUpload: true,
|
||||
|
||||
// Route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers.
|
||||
// This can increase your server load as well as your hosting bill.
|
||||
// Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client-
|
||||
// side errors will fail.
|
||||
tunnelRoute: "/monitoring",
|
||||
|
||||
// Hides source maps from generated client bundles
|
||||
hideSourceMaps: true,
|
||||
|
||||
// Automatically tree-shake Sentry logger statements to reduce bundle size
|
||||
disableLogger: true,
|
||||
|
||||
// Enables automatic instrumentation of Vercel Cron Monitors. (Does not yet work with App Router route handlers.)
|
||||
// See the following for more information:
|
||||
// https://docs.sentry.io/product/crons/
|
||||
// https://vercel.com/docs/cron-jobs
|
||||
automaticVercelMonitors: true,
|
||||
}
|
||||
|
||||
|
||||
if (parseInt(process.env.SENTRY_MONITORING_ENABLED || "0", 10)) {
|
||||
module.exports = withSentryConfig(nextConfig, sentryConfig);
|
||||
} else {
|
||||
module.exports = nextConfig;
|
||||
}
|
||||
|
||||
|
@ -23,7 +23,7 @@
|
||||
"@plane/rich-text-editor": "*",
|
||||
"@plane/types": "*",
|
||||
"@plane/ui": "*",
|
||||
"@sentry/nextjs": "^7.108.0",
|
||||
"@sentry/nextjs": "^8",
|
||||
"axios": "^1.3.4",
|
||||
"clsx": "^2.0.0",
|
||||
"dompurify": "^3.0.11",
|
||||
|
@ -1,18 +0,0 @@
|
||||
// This file configures the initialization of Sentry on the browser.
|
||||
// The config you add here will be used whenever a page is visited.
|
||||
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
|
||||
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
|
||||
const SENTRY_DSN = process.env.NEXT_PUBLIC_SENTRY_DSN;
|
||||
|
||||
Sentry.init({
|
||||
dsn: SENTRY_DSN,
|
||||
environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT || "development",
|
||||
// Adjust this value in production, or use tracesSampler for greater control
|
||||
tracesSampleRate: 1.0,
|
||||
// ...
|
||||
// Note: if you want to override the automatic release value, do not set a
|
||||
// `release` value here - use the environment variable `SENTRY_RELEASE`, so
|
||||
// that it will also get attached to your source maps
|
||||
});
|
31
space/sentry.client.config.ts
Normal file
31
space/sentry.client.config.ts
Normal file
@ -0,0 +1,31 @@
|
||||
// This file configures the initialization of Sentry on the client.
|
||||
// The config you add here will be used whenever a users loads a page in their browser.
|
||||
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
|
||||
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
|
||||
Sentry.init({
|
||||
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||
environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT || "development",
|
||||
|
||||
// Adjust this value in production, or use tracesSampler for greater control
|
||||
tracesSampleRate: 1,
|
||||
|
||||
// Setting this option to true will print useful information to the console while you're setting up Sentry.
|
||||
debug: false,
|
||||
|
||||
replaysOnErrorSampleRate: 1.0,
|
||||
|
||||
// This sets the sample rate to be 10%. You may want this to be 100% while
|
||||
// in development and sample at a lower rate in production
|
||||
replaysSessionSampleRate: 0.1,
|
||||
|
||||
// You can remove this option if you're not planning to use the Sentry Session Replay feature:
|
||||
integrations: [
|
||||
Sentry.replayIntegration({
|
||||
// Additional Replay configuration goes in here, for example:
|
||||
maskAllText: true,
|
||||
blockAllMedia: true,
|
||||
}),
|
||||
],
|
||||
});
|
@ -1,18 +0,0 @@
|
||||
// This file configures the initialization of Sentry on the server.
|
||||
// The config you add here will be used whenever middleware or an Edge route handles a request.
|
||||
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
|
||||
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
|
||||
const SENTRY_DSN = process.env.NEXT_PUBLIC_SENTRY_DSN;
|
||||
|
||||
Sentry.init({
|
||||
dsn: SENTRY_DSN,
|
||||
environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT || "development",
|
||||
// Adjust this value in production, or use tracesSampler for greater control
|
||||
tracesSampleRate: 1.0,
|
||||
// ...
|
||||
// Note: if you want to override the automatic release value, do not set a
|
||||
// `release` value here - use the environment variable `SENTRY_RELEASE`, so
|
||||
// that it will also get attached to your source maps
|
||||
});
|
17
space/sentry.edge.config.ts
Normal file
17
space/sentry.edge.config.ts
Normal file
@ -0,0 +1,17 @@
|
||||
// This file configures the initialization of Sentry for edge features (middleware, edge routes, and so on).
|
||||
// The config you add here will be used whenever one of the edge features is loaded.
|
||||
// Note that this config is unrelated to the Vercel Edge Runtime and is also required when running locally.
|
||||
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
|
||||
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
|
||||
Sentry.init({
|
||||
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||
environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT || "development",
|
||||
|
||||
// Adjust this value in production, or use tracesSampler for greater control
|
||||
tracesSampleRate: 1,
|
||||
|
||||
// Setting this option to true will print useful information to the console while you're setting up Sentry.
|
||||
debug: false,
|
||||
});
|
@ -4,15 +4,16 @@
|
||||
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
|
||||
const SENTRY_DSN = process.env.NEXT_PUBLIC_SENTRY_DSN;
|
||||
|
||||
Sentry.init({
|
||||
dsn: SENTRY_DSN,
|
||||
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||
environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT || "development",
|
||||
|
||||
// Adjust this value in production, or use tracesSampler for greater control
|
||||
tracesSampleRate: 1.0,
|
||||
// ...
|
||||
// Note: if you want to override the automatic release value, do not set a
|
||||
// `release` value here - use the environment variable `SENTRY_RELEASE`, so
|
||||
// that it will also get attached to your source maps
|
||||
tracesSampleRate: 1,
|
||||
|
||||
// Setting this option to true will print useful information to the console while you're setting up Sentry.
|
||||
debug: false,
|
||||
|
||||
// Uncomment the line below to enable Spotlight (https://spotlightjs.com)
|
||||
// spotlight: process.env.NODE_ENV === 'development',
|
||||
});
|
11
turbo.json
11
turbo.json
@ -8,10 +8,6 @@
|
||||
"NEXT_PUBLIC_SPACE_BASE_URL",
|
||||
"NEXT_PUBLIC_SPACE_BASE_PATH",
|
||||
"NEXT_PUBLIC_WEB_BASE_URL",
|
||||
"NEXT_PUBLIC_SENTRY_DSN",
|
||||
"NEXT_PUBLIC_SENTRY_ENVIRONMENT",
|
||||
"NEXT_PUBLIC_ENABLE_SENTRY",
|
||||
"NEXT_PUBLIC_TRACK_EVENTS",
|
||||
"NEXT_PUBLIC_PLAUSIBLE_DOMAIN",
|
||||
"NEXT_PUBLIC_CRISP_ID",
|
||||
"NEXT_PUBLIC_ENABLE_SESSION_RECORDER",
|
||||
@ -21,7 +17,12 @@
|
||||
"NEXT_PUBLIC_POSTHOG_HOST",
|
||||
"NEXT_PUBLIC_POSTHOG_DEBUG",
|
||||
"NEXT_PUBLIC_SUPPORT_EMAIL",
|
||||
"SENTRY_AUTH_TOKEN"
|
||||
"SENTRY_AUTH_TOKEN",
|
||||
"SENTRY_ORG_ID",
|
||||
"SENTRY_PROJECT_ID",
|
||||
"NEXT_PUBLIC_SENTRY_ENVIRONMENT",
|
||||
"NEXT_PUBLIC_SENTRY_DSN",
|
||||
"SENTRY_MONITORING_ENABLED"
|
||||
],
|
||||
"pipeline": {
|
||||
"build": {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React from "react";
|
||||
import React, { Fragment } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Tab } from "@headlessui/react";
|
||||
import { ICycle, IModule, IProject } from "@plane/types";
|
||||
@ -20,20 +20,21 @@ export const ProjectAnalyticsModalMainContent: React.FC<Props> = observer((props
|
||||
|
||||
return (
|
||||
<Tab.Group as={React.Fragment}>
|
||||
<Tab.List as="div" className="flex space-x-2 border-b border-custom-border-200 px-0 md:px-5 py-0 md:py-3">
|
||||
<Tab.List as="div" className="flex space-x-2 border-b h-[50px] border-custom-border-200 px-0 md:px-3">
|
||||
{ANALYTICS_TABS.map((tab) => (
|
||||
<Tab
|
||||
key={tab.key}
|
||||
className={({ selected }) =>
|
||||
`rounded-0 w-full md:w-max md:rounded-3xl border-b md:border border-custom-border-200 focus:outline-none px-0 md:px-4 py-2 text-xs hover:bg-custom-background-80 ${
|
||||
selected
|
||||
? "border-custom-primary-100 text-custom-primary-100 md:bg-custom-background-80 md:text-custom-text-200 md:border-custom-border-200"
|
||||
: "border-transparent"
|
||||
}`
|
||||
}
|
||||
onClick={() => {}}
|
||||
>
|
||||
{tab.title}
|
||||
<Tab key={tab.key} as={Fragment}>
|
||||
{({ selected }) => (
|
||||
<button
|
||||
className={`text-sm group relative flex items-center gap-1 h-[50px] px-3 cursor-pointer transition-all font-medium outline-none ${
|
||||
selected ? "text-custom-primary-100 " : "hover:text-custom-text-200"
|
||||
}`}
|
||||
>
|
||||
{tab.title}
|
||||
<div
|
||||
className={`border absolute bottom-0 right-0 left-0 rounded-t-md ${selected ? "border-custom-primary-100" : "border-transparent group-hover:border-custom-border-200"}`}
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
</Tab>
|
||||
))}
|
||||
</Tab.List>
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { Command } from "cmdk";
|
||||
import { observer } from "mobx-react";
|
||||
import { useRouter } from "next/router";
|
||||
import { LinkIcon, Signal, Trash2, UserMinus2, UserPlus2 } from "lucide-react";
|
||||
import { LinkIcon, Signal, Trash2, UserMinus2, UserPlus2, Users } from "lucide-react";
|
||||
import { TIssue } from "@plane/types";
|
||||
// hooks
|
||||
import { DoubleCircleIcon, UserGroupIcon, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { DoubleCircleIcon, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// constants
|
||||
import { EIssuesStoreType } from "@/constants/issue";
|
||||
// helpers
|
||||
@ -115,7 +115,7 @@ export const CommandPaletteIssueActions: React.FC<Props> = observer((props) => {
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<UserGroupIcon className="h-3.5 w-3.5" />
|
||||
<Users className="h-3.5 w-3.5" />
|
||||
Assign to...
|
||||
</div>
|
||||
</Command.Item>
|
||||
|
@ -502,7 +502,7 @@ const activityDetails: {
|
||||
name: {
|
||||
message: (activity, showIssue) => (
|
||||
<>
|
||||
set the name to <span className="break-all">{activity.new_value}</span>
|
||||
set the title to <span className="break-all">{activity.new_value}</span>
|
||||
{showIssue && (
|
||||
<>
|
||||
{" "}
|
||||
|
@ -2,6 +2,8 @@ import React, { FC } from "react";
|
||||
import Link from "next/link";
|
||||
// ui
|
||||
import { Tooltip } from "@plane/ui";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
|
||||
interface IListItemProps {
|
||||
title: string;
|
||||
@ -12,6 +14,7 @@ interface IListItemProps {
|
||||
actionableItems?: JSX.Element;
|
||||
isMobile?: boolean;
|
||||
parentRef: React.RefObject<HTMLDivElement>;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const ListItem: FC<IListItemProps> = (props) => {
|
||||
@ -24,12 +27,18 @@ export const ListItem: FC<IListItemProps> = (props) => {
|
||||
onItemClick,
|
||||
isMobile = false,
|
||||
parentRef,
|
||||
className = "",
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div ref={parentRef} className="relative">
|
||||
<Link href={itemLink} onClick={onItemClick}>
|
||||
<div className="group h-24 sm:h-[52px] flex w-full flex-col items-center justify-between gap-3 sm:gap-5 px-6 py-4 sm:py-0 text-sm border-b border-custom-border-200 bg-custom-background-100 hover:bg-custom-background-90 sm:flex-row">
|
||||
<div
|
||||
className={cn(
|
||||
"group h-24 sm:h-[52px] flex w-full flex-col items-center justify-between gap-3 sm:gap-5 px-6 py-4 sm:py-0 text-sm border-b border-custom-border-200 bg-custom-background-100 hover:bg-custom-background-90 sm:flex-row",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="relative flex w-full items-center justify-between gap-3 overflow-hidden">
|
||||
<div className="relative flex w-full items-center gap-3 overflow-hidden">
|
||||
<div className="flex items-center gap-4 truncate">
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
import Image from "next/image";
|
||||
// headless ui
|
||||
import { Tab } from "@headlessui/react";
|
||||
@ -15,6 +15,7 @@ import {
|
||||
// hooks
|
||||
import { Avatar, StateGroupIcon } from "@plane/ui";
|
||||
import { SingleProgressStats } from "@/components/core";
|
||||
import { useProjectState } from "@/hooks/store";
|
||||
import useLocalStorage from "@/hooks/use-local-storage";
|
||||
// images
|
||||
import emptyLabel from "public/empty-state/empty_label.svg";
|
||||
@ -44,20 +45,23 @@ type Props = {
|
||||
handleFiltersUpdate: (key: keyof IIssueFilterOptions, value: string | string[]) => void;
|
||||
};
|
||||
|
||||
export const SidebarProgressStats: React.FC<Props> = ({
|
||||
distribution,
|
||||
groupedIssues,
|
||||
totalIssues,
|
||||
module,
|
||||
roundedTab,
|
||||
noBackground,
|
||||
isPeekView = false,
|
||||
isCompleted = false,
|
||||
filters,
|
||||
handleFiltersUpdate,
|
||||
}) => {
|
||||
export const SidebarProgressStats: React.FC<Props> = observer((props) => {
|
||||
const {
|
||||
distribution,
|
||||
groupedIssues,
|
||||
totalIssues,
|
||||
module,
|
||||
roundedTab,
|
||||
noBackground,
|
||||
isPeekView = false,
|
||||
isCompleted = false,
|
||||
filters,
|
||||
handleFiltersUpdate,
|
||||
} = props;
|
||||
const { storedValue: tab, setValue: setTab } = useLocalStorage("tab", "Assignees");
|
||||
|
||||
const { groupedProjectStates } = useProjectState();
|
||||
|
||||
const currentValue = (tab: string | null) => {
|
||||
switch (tab) {
|
||||
case "Assignees":
|
||||
@ -71,6 +75,12 @@ export const SidebarProgressStats: React.FC<Props> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const getStateGroupState = (stateGroup: string) => {
|
||||
const stateGroupStates = groupedProjectStates?.[stateGroup];
|
||||
const stateGroupStatesId = stateGroupStates?.map((state) => state.id);
|
||||
return stateGroupStatesId;
|
||||
};
|
||||
|
||||
return (
|
||||
<Tab.Group
|
||||
defaultIndex={currentValue(tab)}
|
||||
@ -261,10 +271,14 @@ export const SidebarProgressStats: React.FC<Props> = ({
|
||||
}
|
||||
completed={groupedIssues[group]}
|
||||
total={totalIssues}
|
||||
{...(!isPeekView &&
|
||||
!isCompleted && {
|
||||
onClick: () => handleFiltersUpdate("state", getStateGroupState(group) ?? []),
|
||||
})}
|
||||
/>
|
||||
))}
|
||||
</Tab.Panel>
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { FC } from "react";
|
||||
import Link from "next/link";
|
||||
// types
|
||||
import { ICycle } from "@plane/types";
|
||||
// components
|
||||
@ -8,14 +9,19 @@ import { EmptyState } from "@/components/empty-state";
|
||||
import { EmptyStateType } from "@/constants/empty-state";
|
||||
|
||||
export type ActiveCycleProductivityProps = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
cycle: ICycle;
|
||||
};
|
||||
|
||||
export const ActiveCycleProductivity: FC<ActiveCycleProductivityProps> = (props) => {
|
||||
const { cycle } = props;
|
||||
const { workspaceSlug, projectId, cycle } = props;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col justify-center min-h-[17rem] gap-5 py-4 px-3.5 bg-custom-background-100 border border-custom-border-200 rounded-lg">
|
||||
<Link
|
||||
href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycle?.id}`}
|
||||
className="flex flex-col justify-center min-h-[17rem] gap-5 py-4 px-3.5 bg-custom-background-100 border border-custom-border-200 rounded-lg"
|
||||
>
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<h3 className="text-base text-custom-text-300 font-semibold">Issue burndown</h3>
|
||||
</div>
|
||||
@ -53,6 +59,6 @@ export const ActiveCycleProductivity: FC<ActiveCycleProductivityProps> = (props)
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { FC } from "react";
|
||||
import Link from "next/link";
|
||||
// types
|
||||
import { ICycle } from "@plane/types";
|
||||
// ui
|
||||
@ -10,11 +11,13 @@ import { CYCLE_STATE_GROUPS_DETAILS } from "@/constants/cycle";
|
||||
import { EmptyStateType } from "@/constants/empty-state";
|
||||
|
||||
export type ActiveCycleProgressProps = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
cycle: ICycle;
|
||||
};
|
||||
|
||||
export const ActiveCycleProgress: FC<ActiveCycleProgressProps> = (props) => {
|
||||
const { cycle } = props;
|
||||
const { workspaceSlug, projectId, cycle } = props;
|
||||
|
||||
const progressIndicatorData = CYCLE_STATE_GROUPS_DETAILS.map((group, index) => ({
|
||||
id: index,
|
||||
@ -31,7 +34,10 @@ export const ActiveCycleProgress: FC<ActiveCycleProgressProps> = (props) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col min-h-[17rem] gap-5 py-4 px-3.5 bg-custom-background-100 border border-custom-border-200 rounded-lg">
|
||||
<Link
|
||||
href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycle?.id}`}
|
||||
className="flex flex-col min-h-[17rem] gap-5 py-4 px-3.5 bg-custom-background-100 border border-custom-border-200 rounded-lg"
|
||||
>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<h3 className="text-base text-custom-text-300 font-semibold">Progress</h3>
|
||||
@ -85,6 +91,6 @@ export const ActiveCycleProgress: FC<ActiveCycleProgressProps> = (props) => {
|
||||
<EmptyState type={EmptyStateType.ACTIVE_CYCLE_PROGRESS_EMPTY_STATE} layout="screen-simple" size="sm" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
@ -62,13 +62,18 @@ export const ActiveCycleRoot: React.FC<IActiveCycleDetails> = observer((props) =
|
||||
cycleId={currentProjectActiveCycleId}
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
className="!border-b-transparent"
|
||||
/>
|
||||
)}
|
||||
<div className="bg-custom-background-90 py-6 px-8">
|
||||
<div className="grid grid-cols-1 bg-custom-background-90 gap-3 lg:grid-cols-2 xl:grid-cols-3">
|
||||
<ActiveCycleProgress cycle={activeCycle} />
|
||||
<ActiveCycleProductivity cycle={activeCycle} />
|
||||
<ActiveCycleStats cycle={activeCycle} workspaceSlug={workspaceSlug} projectId={projectId} />
|
||||
<div className="bg-custom-background-100 pt-3 pb-6 px-6">
|
||||
<div className="grid grid-cols-1 bg-custom-background-100 gap-3 lg:grid-cols-2 xl:grid-cols-3">
|
||||
<ActiveCycleProgress workspaceSlug={workspaceSlug} projectId={projectId} cycle={activeCycle} />
|
||||
<ActiveCycleProductivity
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
cycle={activeCycle}
|
||||
/>
|
||||
<ActiveCycleStats workspaceSlug={workspaceSlug} projectId={projectId} cycle={activeCycle} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -2,7 +2,7 @@ import { useRef } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { User2 } from "lucide-react";
|
||||
import { Users } from "lucide-react";
|
||||
// ui
|
||||
import { Avatar, AvatarGroup, setPromiseToast } from "@plane/ui";
|
||||
// components
|
||||
@ -112,9 +112,7 @@ export const UpcomingCycleListItem: React.FC<Props> = observer((props) => {
|
||||
})}
|
||||
</AvatarGroup>
|
||||
) : (
|
||||
<span className="flex h-5 w-5 items-end justify-center rounded-full border border-dashed border-custom-text-400 bg-custom-background-80">
|
||||
<User2 className="h-4 w-4 text-custom-text-400" />
|
||||
</span>
|
||||
<Users className="h-4 w-4 text-custom-text-300" />
|
||||
)}
|
||||
|
||||
<FavoriteStar
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { FC, MouseEvent } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { CalendarCheck2, CalendarClock, MoveRight, User2 } from "lucide-react";
|
||||
import { CalendarCheck2, CalendarClock, MoveRight, Users } from "lucide-react";
|
||||
// types
|
||||
import { ICycle, TCycleGroups } from "@plane/types";
|
||||
// ui
|
||||
@ -146,9 +146,7 @@ export const CycleListItemAction: FC<Props> = observer((props) => {
|
||||
})}
|
||||
</AvatarGroup>
|
||||
) : (
|
||||
<span className="flex h-5 w-5 items-end justify-center rounded-full border border-dashed border-custom-text-400 bg-custom-background-80">
|
||||
<User2 className="h-4 w-4 text-custom-text-400" />
|
||||
</span>
|
||||
<Users className="h-4 w-4 text-custom-text-300" />
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
@ -22,10 +22,11 @@ type TCyclesListItem = {
|
||||
handleRemoveFromFavorites?: () => void;
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const CyclesListItem: FC<TCyclesListItem> = observer((props) => {
|
||||
const { cycleId, workspaceSlug, projectId } = props;
|
||||
const { cycleId, workspaceSlug, projectId, className = "" } = props;
|
||||
// refs
|
||||
const parentRef = useRef(null);
|
||||
// router
|
||||
@ -83,6 +84,7 @@ export const CyclesListItem: FC<TCyclesListItem> = observer((props) => {
|
||||
onItemClick={(e) => {
|
||||
if (cycleDetails.archived_at) openCycleOverview(e);
|
||||
}}
|
||||
className={className}
|
||||
prependTitleElement={
|
||||
<CircularProgressIndicator size={30} percentage={progress} strokeWidth={3}>
|
||||
{isCompleted ? (
|
||||
|
@ -1,5 +1,6 @@
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import isEmpty from "lodash/isEmpty";
|
||||
import isEqual from "lodash/isEqual";
|
||||
import { observer } from "mobx-react";
|
||||
import { useRouter } from "next/router";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
@ -9,10 +10,10 @@ import {
|
||||
ChevronDown,
|
||||
LinkIcon,
|
||||
Trash2,
|
||||
UserCircle2,
|
||||
AlertCircle,
|
||||
ChevronRight,
|
||||
CalendarClock,
|
||||
SquareUser,
|
||||
} from "lucide-react";
|
||||
import { Disclosure, Transition } from "@headlessui/react";
|
||||
// types
|
||||
@ -199,14 +200,18 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
const handleFiltersUpdate = useCallback(
|
||||
(key: keyof IIssueFilterOptions, value: string | string[]) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
const newValues = issueFilters?.filters?.[key] ?? [];
|
||||
let 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);
|
||||
});
|
||||
if (key === "state") {
|
||||
if (isEqual(newValues, value)) newValues = [];
|
||||
else newValues = value;
|
||||
} else {
|
||||
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);
|
||||
@ -427,7 +432,7 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
|
||||
<div className="flex items-center justify-start gap-1">
|
||||
<div className="flex w-2/5 items-center justify-start gap-2 text-custom-text-300">
|
||||
<UserCircle2 className="h-4 w-4" />
|
||||
<SquareUser className="h-4 w-4" />
|
||||
<span className="text-base">Lead</span>
|
||||
</div>
|
||||
<div className="flex w-3/5 items-center rounded-sm">
|
||||
|
@ -1,16 +1,19 @@
|
||||
import { observer } from "mobx-react";
|
||||
// hooks
|
||||
import { Avatar, AvatarGroup, UserGroupIcon } from "@plane/ui";
|
||||
import { useMember } from "@/hooks/store";
|
||||
// icons
|
||||
import { LucideIcon, Users } from "lucide-react";
|
||||
// ui
|
||||
import { Avatar, AvatarGroup } from "@plane/ui";
|
||||
// hooks
|
||||
import { useMember } from "@/hooks/store";
|
||||
|
||||
type AvatarProps = {
|
||||
showTooltip: boolean;
|
||||
userIds: string | string[] | null;
|
||||
icon?: LucideIcon;
|
||||
};
|
||||
|
||||
export const ButtonAvatars: React.FC<AvatarProps> = observer((props) => {
|
||||
const { showTooltip, userIds } = props;
|
||||
const { showTooltip, userIds, icon: Icon } = props;
|
||||
// store hooks
|
||||
const { getUserDetails } = useMember();
|
||||
|
||||
@ -33,5 +36,9 @@ export const ButtonAvatars: React.FC<AvatarProps> = observer((props) => {
|
||||
}
|
||||
}
|
||||
|
||||
return <UserGroupIcon className="h-3 w-3 flex-shrink-0" />;
|
||||
return Icon ? (
|
||||
<Icon className="h-3 w-3 flex-shrink-0" />
|
||||
) : (
|
||||
<Users className="h-3 w-3 flex-shrink-0" />
|
||||
);
|
||||
});
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Fragment, useRef, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import { ChevronDown, LucideIcon } from "lucide-react";
|
||||
// headless ui
|
||||
import { Combobox } from "@headlessui/react";
|
||||
// helpers
|
||||
@ -19,6 +19,7 @@ import { MemberDropdownProps } from "./types";
|
||||
|
||||
type Props = {
|
||||
projectId?: string;
|
||||
icon?: LucideIcon;
|
||||
onClose?: () => void;
|
||||
} & MemberDropdownProps;
|
||||
|
||||
@ -43,6 +44,7 @@ export const MemberDropdown: React.FC<Props> = observer((props) => {
|
||||
showTooltip = false,
|
||||
tabIndex,
|
||||
value,
|
||||
icon,
|
||||
} = props;
|
||||
// states
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
@ -115,7 +117,7 @@ export const MemberDropdown: React.FC<Props> = observer((props) => {
|
||||
showTooltip={showTooltip}
|
||||
variant={buttonVariant}
|
||||
>
|
||||
{!hideIcon && <ButtonAvatars showTooltip={showTooltip} userIds={value} />}
|
||||
{!hideIcon && <ButtonAvatars showTooltip={showTooltip} userIds={value} icon={icon} />}
|
||||
{BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && (
|
||||
<span className="flex-grow truncate text-xs leading-5">
|
||||
{Array.isArray(value) && value.length > 0
|
||||
|
@ -1,9 +1,9 @@
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useRouter } from "next/router";
|
||||
import { CalendarCheck2, CopyPlus, Signal, Tag } from "lucide-react";
|
||||
import { CalendarCheck2, CopyPlus, Signal, Tag, Users } from "lucide-react";
|
||||
import { TInboxDuplicateIssueDetails, TIssue } from "@plane/types";
|
||||
import { ControlLink, DoubleCircleIcon, Tooltip, UserGroupIcon } from "@plane/ui";
|
||||
import { ControlLink, DoubleCircleIcon, Tooltip } from "@plane/ui";
|
||||
// components
|
||||
import { DateDropdown, PriorityDropdown, MemberDropdown, StateDropdown } from "@/components/dropdowns";
|
||||
import { IssueLabel, TIssueOperations } from "@/components/issues";
|
||||
@ -64,7 +64,7 @@ export const InboxIssueContentProperties: React.FC<Props> = observer((props) =>
|
||||
{/* Assignee */}
|
||||
<div className="flex h-8 items-center gap-2">
|
||||
<div className="flex w-2/5 flex-shrink-0 items-center gap-1 text-sm text-custom-text-300">
|
||||
<UserGroupIcon className="h-4 w-4 flex-shrink-0" />
|
||||
<Users className="h-4 w-4 flex-shrink-0" />
|
||||
<span>Assignees</span>
|
||||
</div>
|
||||
<MemberDropdown
|
||||
|
@ -42,6 +42,7 @@ export const InboxIssueCreateRoot: FC<TInboxIssueCreateRoot> = observer((props)
|
||||
const router = useRouter();
|
||||
// refs
|
||||
const descriptionEditorRef = useRef<EditorRefApi>(null);
|
||||
const submitBtnRef = useRef<HTMLButtonElement | null>(null);
|
||||
// hooks
|
||||
const { captureIssueEvent } = useEventTracker();
|
||||
const { createInboxIssue } = useProjectInbox();
|
||||
@ -139,6 +140,7 @@ export const InboxIssueCreateRoot: FC<TInboxIssueCreateRoot> = observer((props)
|
||||
handleData={handleFormData}
|
||||
editorRef={descriptionEditorRef}
|
||||
containerClassName="border-[0.5px] border-custom-border-200 py-3 min-h-[150px]"
|
||||
onEnterKeyPress={() => submitBtnRef?.current?.click()}
|
||||
/>
|
||||
<InboxIssueProperties projectId={projectId} data={formData} handleData={handleFormData} />
|
||||
</div>
|
||||
@ -158,6 +160,7 @@ export const InboxIssueCreateRoot: FC<TInboxIssueCreateRoot> = observer((props)
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
ref={submitBtnRef}
|
||||
size="sm"
|
||||
type="submit"
|
||||
loading={formSubmitting}
|
||||
|
@ -34,6 +34,7 @@ export const InboxIssueEditRoot: FC<TInboxIssueEditRoot> = observer((props) => {
|
||||
const router = useRouter();
|
||||
// refs
|
||||
const descriptionEditorRef = useRef<EditorRefApi>(null);
|
||||
const submitBtnRef = useRef<HTMLButtonElement | null>(null);
|
||||
// store hooks
|
||||
const { captureIssueEvent } = useEventTracker();
|
||||
const { currentProjectDetails } = useProject();
|
||||
@ -148,6 +149,7 @@ export const InboxIssueEditRoot: FC<TInboxIssueEditRoot> = observer((props) => {
|
||||
handleData={handleFormData}
|
||||
editorRef={descriptionEditorRef}
|
||||
containerClassName="border-[0.5px] border-custom-border-200 py-3 min-h-[150px]"
|
||||
onEnterKeyPress={() => submitBtnRef?.current?.click()}
|
||||
/>
|
||||
<InboxIssueProperties projectId={projectId} data={formData} handleData={handleFormData} isVisible />
|
||||
</div>
|
||||
@ -160,6 +162,7 @@ export const InboxIssueEditRoot: FC<TInboxIssueEditRoot> = observer((props) => {
|
||||
variant="primary"
|
||||
size="sm"
|
||||
type="button"
|
||||
ref={submitBtnRef}
|
||||
loading={formSubmitting}
|
||||
disabled={isTitleLengthMoreThan255Character}
|
||||
onClick={handleFormSubmit}
|
||||
|
@ -18,11 +18,13 @@ type TInboxIssueDescription = {
|
||||
data: Partial<TIssue>;
|
||||
handleData: (issueKey: keyof Partial<TIssue>, issueValue: Partial<TIssue>[keyof Partial<TIssue>]) => void;
|
||||
editorRef: RefObject<EditorRefApi>;
|
||||
onEnterKeyPress?: (e?: any) => void;
|
||||
};
|
||||
|
||||
// TODO: have to implement GPT Assistance
|
||||
export const InboxIssueDescription: FC<TInboxIssueDescription> = observer((props) => {
|
||||
const { containerClassName, workspaceSlug, projectId, workspaceId, data, handleData, editorRef } = props;
|
||||
const { containerClassName, workspaceSlug, projectId, workspaceId, data, handleData, editorRef, onEnterKeyPress } =
|
||||
props;
|
||||
// hooks
|
||||
const { loader } = useProjectInbox();
|
||||
|
||||
@ -44,6 +46,7 @@ export const InboxIssueDescription: FC<TInboxIssueDescription> = observer((props
|
||||
onChange={(_description: object, description_html: string) => handleData("description_html", description_html)}
|
||||
placeholder={getDescriptionPlaceholder}
|
||||
containerClassName={containerClassName}
|
||||
onEnterKeyPress={onEnterKeyPress}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
@ -10,9 +10,9 @@ import useSWR, { mutate } from "swr";
|
||||
// react-hook-form
|
||||
// services
|
||||
// components
|
||||
import { ArrowLeft, Check, List, Settings, UploadCloud } from "lucide-react";
|
||||
import { ArrowLeft, Check, List, Settings, UploadCloud, Users } from "lucide-react";
|
||||
import { IGithubRepoCollaborator, IGithubServiceImportFormData } from "@plane/types";
|
||||
import { UserGroupIcon, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import {
|
||||
GithubImportConfigure,
|
||||
GithubImportData,
|
||||
@ -72,7 +72,7 @@ const integrationWorkflowData = [
|
||||
{
|
||||
title: "Users",
|
||||
key: "import-users",
|
||||
icon: UserGroupIcon,
|
||||
icon: Users,
|
||||
},
|
||||
{
|
||||
title: "Confirm",
|
||||
|
@ -5,12 +5,12 @@ import { useRouter } from "next/router";
|
||||
import { FormProvider, useForm } from "react-hook-form";
|
||||
import { mutate } from "swr";
|
||||
// icons
|
||||
import { ArrowLeft, Check, List, Settings } from "lucide-react";
|
||||
import { ArrowLeft, Check, List, Settings, Users } from "lucide-react";
|
||||
import { IJiraImporterForm } from "@plane/types";
|
||||
// services
|
||||
// fetch keys
|
||||
// components
|
||||
import { Button, UserGroupIcon } from "@plane/ui";
|
||||
import { Button } from "@plane/ui";
|
||||
import { IMPORTER_SERVICES_LIST } from "@/constants/fetch-keys";
|
||||
// assets
|
||||
import { JiraImporterService } from "@/services/integrations";
|
||||
@ -44,7 +44,7 @@ const integrationWorkflowData: Array<{
|
||||
{
|
||||
title: "Users",
|
||||
key: "import-users",
|
||||
icon: UserGroupIcon,
|
||||
icon: Users,
|
||||
},
|
||||
{
|
||||
title: "Confirm",
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// hooks
|
||||
import { UserGroupIcon } from "@plane/ui";
|
||||
// icons
|
||||
import { Users } from "lucide-react";
|
||||
// hooks;
|
||||
import { useIssueDetail } from "@/hooks/store";
|
||||
// components
|
||||
import { IssueActivityBlockComponent, IssueLink } from "./";
|
||||
// icons
|
||||
|
||||
type TIssueAssigneeActivity = { activityId: string; showIssue?: boolean; ends: "top" | "bottom" | undefined };
|
||||
|
||||
@ -21,7 +21,7 @@ export const IssueAssigneeActivity: FC<TIssueAssigneeActivity> = observer((props
|
||||
if (!activity) return <></>;
|
||||
return (
|
||||
<IssueActivityBlockComponent
|
||||
icon={<UserGroupIcon className="h-4 w-4 flex-shrink-0" />}
|
||||
icon={<Users className="h-3 w-3 flex-shrink-0" />}
|
||||
activityId={activityId}
|
||||
ends={ends}
|
||||
>
|
||||
|
@ -56,7 +56,7 @@ export const IssueParentDetail: FC<TIssueParentDetail> = observer((props) => {
|
||||
Sibling issues
|
||||
</div>
|
||||
|
||||
<IssueParentSiblings currentIssue={issue} parentIssue={parentIssue} />
|
||||
<IssueParentSiblings workspaceSlug={workspaceSlug} currentIssue={issue} parentIssue={parentIssue} />
|
||||
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => issueOperations.update(workspaceSlug, projectId, issueId, { parent_id: null })}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
// ui
|
||||
import { CustomMenu, LayersIcon } from "@plane/ui";
|
||||
@ -6,15 +7,15 @@ import { CustomMenu, LayersIcon } from "@plane/ui";
|
||||
import { useIssueDetail, useProject } from "@/hooks/store";
|
||||
|
||||
type TIssueParentSiblingItem = {
|
||||
workspaceSlug: string;
|
||||
issueId: string;
|
||||
};
|
||||
|
||||
export const IssueParentSiblingItem: FC<TIssueParentSiblingItem> = (props) => {
|
||||
const { issueId } = props;
|
||||
export const IssueParentSiblingItem: FC<TIssueParentSiblingItem> = observer((props) => {
|
||||
const { workspaceSlug, issueId } = props;
|
||||
// hooks
|
||||
const { getProjectById } = useProject();
|
||||
const {
|
||||
peekIssue,
|
||||
issue: { getIssueById },
|
||||
} = useIssueDetail();
|
||||
|
||||
@ -27,7 +28,7 @@ export const IssueParentSiblingItem: FC<TIssueParentSiblingItem> = (props) => {
|
||||
<>
|
||||
<CustomMenu.MenuItem key={issueDetail.id}>
|
||||
<Link
|
||||
href={`/${peekIssue?.workspaceSlug}/projects/${issueDetail?.project_id as string}/issues/${issueDetail.id}`}
|
||||
href={`/${workspaceSlug}/projects/${issueDetail?.project_id as string}/issues/${issueDetail.id}`}
|
||||
className="flex items-center gap-2 py-2"
|
||||
>
|
||||
<LayersIcon className="h-4 w-4" />
|
||||
@ -36,4 +37,4 @@ export const IssueParentSiblingItem: FC<TIssueParentSiblingItem> = (props) => {
|
||||
</CustomMenu.MenuItem>
|
||||
</>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import useSWR from "swr";
|
||||
import { TIssue } from "@plane/types";
|
||||
// components
|
||||
@ -8,25 +9,25 @@ import { useIssueDetail } from "@/hooks/store";
|
||||
import { IssueParentSiblingItem } from "./sibling-item";
|
||||
|
||||
export type TIssueParentSiblings = {
|
||||
workspaceSlug: string;
|
||||
currentIssue: TIssue;
|
||||
parentIssue: TIssue;
|
||||
};
|
||||
|
||||
export const IssueParentSiblings: FC<TIssueParentSiblings> = (props) => {
|
||||
const { currentIssue, parentIssue } = props;
|
||||
export const IssueParentSiblings: FC<TIssueParentSiblings> = observer((props) => {
|
||||
const { workspaceSlug, currentIssue, parentIssue } = props;
|
||||
// hooks
|
||||
const {
|
||||
peekIssue,
|
||||
fetchSubIssues,
|
||||
subIssues: { subIssuesByIssueId },
|
||||
} = useIssueDetail();
|
||||
|
||||
const { isLoading } = useSWR(
|
||||
peekIssue && parentIssue && parentIssue.project_id
|
||||
? `ISSUE_PARENT_CHILD_ISSUES_${peekIssue?.workspaceSlug}_${parentIssue.project_id}_${parentIssue.id}`
|
||||
parentIssue && parentIssue.project_id
|
||||
? `ISSUE_PARENT_CHILD_ISSUES_${workspaceSlug}_${parentIssue.project_id}_${parentIssue.id}`
|
||||
: null,
|
||||
peekIssue && parentIssue && parentIssue.project_id
|
||||
? () => fetchSubIssues(peekIssue?.workspaceSlug, parentIssue.project_id, parentIssue.id)
|
||||
parentIssue && parentIssue.project_id
|
||||
? () => fetchSubIssues(workspaceSlug, parentIssue.project_id, parentIssue.id)
|
||||
: null
|
||||
);
|
||||
|
||||
@ -40,7 +41,10 @@ export const IssueParentSiblings: FC<TIssueParentSiblings> = (props) => {
|
||||
</div>
|
||||
) : subIssueIds && subIssueIds.length > 0 ? (
|
||||
subIssueIds.map(
|
||||
(issueId) => currentIssue.id != issueId && <IssueParentSiblingItem key={issueId} issueId={issueId} />
|
||||
(issueId) =>
|
||||
currentIssue.id != issueId && (
|
||||
<IssueParentSiblingItem key={issueId} workspaceSlug={workspaceSlug} issueId={issueId} />
|
||||
)
|
||||
)
|
||||
) : (
|
||||
<div className="flex items-center gap-2 whitespace-nowrap px-1 py-1 text-left text-xs text-custom-text-200">
|
||||
@ -49,4 +53,4 @@ export const IssueParentSiblings: FC<TIssueParentSiblings> = (props) => {
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -12,6 +12,7 @@ import {
|
||||
Tag,
|
||||
Trash2,
|
||||
Triangle,
|
||||
Users,
|
||||
XCircle,
|
||||
} from "lucide-react";
|
||||
// hooks
|
||||
@ -24,7 +25,6 @@ import {
|
||||
RelatedIcon,
|
||||
TOAST_TYPE,
|
||||
Tooltip,
|
||||
UserGroupIcon,
|
||||
setToast,
|
||||
} from "@plane/ui";
|
||||
import {
|
||||
@ -219,7 +219,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
|
||||
<div className="flex h-8 items-center gap-2">
|
||||
<div className="flex w-2/5 flex-shrink-0 items-center gap-1 text-sm text-custom-text-300">
|
||||
<UserGroupIcon className="h-4 w-4 flex-shrink-0" />
|
||||
<Users className="h-4 w-4 flex-shrink-0" />
|
||||
<span>Assignees</span>
|
||||
</div>
|
||||
<MemberDropdown
|
||||
|
@ -68,7 +68,6 @@ export const CycleEmptyState: React.FC<Props> = observer((props) => {
|
||||
? EmptyStateType.PROJECT_EMPTY_FILTER
|
||||
: EmptyStateType.PROJECT_CYCLE_NO_ISSUES;
|
||||
const additionalPath = isCompletedAndEmpty ? undefined : activeLayout ?? "list";
|
||||
const emptyStateSize = isEmptyFilters ? "lg" : "sm";
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -84,7 +83,6 @@ export const CycleEmptyState: React.FC<Props> = observer((props) => {
|
||||
<EmptyState
|
||||
type={emptyStateType}
|
||||
additionalPath={additionalPath}
|
||||
size={emptyStateSize}
|
||||
primaryButtonOnClick={
|
||||
!isCompletedAndEmpty && !isEmptyFilters
|
||||
? () => {
|
||||
|
@ -41,14 +41,12 @@ export const ProjectDraftEmptyState: React.FC = observer(() => {
|
||||
const emptyStateType =
|
||||
issueFilterCount > 0 ? EmptyStateType.PROJECT_DRAFT_EMPTY_FILTER : EmptyStateType.PROJECT_DRAFT_NO_ISSUES;
|
||||
const additionalPath = issueFilterCount > 0 ? activeLayout ?? "list" : undefined;
|
||||
const emptyStateSize = issueFilterCount > 0 ? "lg" : "sm";
|
||||
|
||||
return (
|
||||
<div className="relative h-full w-full overflow-y-auto">
|
||||
<EmptyState
|
||||
type={emptyStateType}
|
||||
additionalPath={additionalPath}
|
||||
size={emptyStateSize}
|
||||
secondaryButtonOnClick={issueFilterCount > 0 ? handleClearAllFilters : undefined}
|
||||
/>
|
||||
</div>
|
||||
|
@ -43,14 +43,12 @@ export const ProjectEmptyState: React.FC = observer(() => {
|
||||
|
||||
const emptyStateType = issueFilterCount > 0 ? EmptyStateType.PROJECT_EMPTY_FILTER : EmptyStateType.PROJECT_NO_ISSUES;
|
||||
const additionalPath = issueFilterCount > 0 ? activeLayout ?? "list" : undefined;
|
||||
const emptyStateSize = issueFilterCount > 0 ? "lg" : "sm";
|
||||
|
||||
return (
|
||||
<div className="relative h-full w-full overflow-y-auto">
|
||||
<EmptyState
|
||||
type={emptyStateType}
|
||||
additionalPath={additionalPath}
|
||||
size={emptyStateSize}
|
||||
primaryButtonOnClick={
|
||||
issueFilterCount > 0
|
||||
? undefined
|
||||
|
@ -182,14 +182,14 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
|
||||
};
|
||||
|
||||
const handleKanbanFilters = (toggle: "group_by" | "sub_group_by", value: string) => {
|
||||
if (workspaceSlug && projectId) {
|
||||
if (workspaceSlug) {
|
||||
let kanbanFilters = issuesFilter?.issueFilters?.kanbanFilters?.[toggle] || [];
|
||||
if (kanbanFilters.includes(value)) {
|
||||
kanbanFilters = kanbanFilters.filter((_value) => _value != value);
|
||||
} else {
|
||||
kanbanFilters.push(value);
|
||||
}
|
||||
updateFilters(projectId.toString(), EIssueFilterType.KANBAN_FILTERS, {
|
||||
updateFilters(projectId?.toString() ?? "", EIssueFilterType.KANBAN_FILTERS, {
|
||||
[toggle]: kanbanFilters,
|
||||
});
|
||||
}
|
||||
|
@ -154,12 +154,11 @@ export const AllIssueLayoutRoot: React.FC = observer(() => {
|
||||
|
||||
return (
|
||||
<div className="relative flex h-full w-full flex-col overflow-hidden">
|
||||
<div className="relative flex h-full w-full flex-col">
|
||||
<div className="relative flex h-full w-full flex-col overflow-auto">
|
||||
<GlobalViewsAppliedFiltersRoot globalViewId={globalViewId} />
|
||||
{issueIds.length === 0 ? (
|
||||
<EmptyState
|
||||
type={emptyStateType as keyof typeof EMPTY_STATE_DETAILS}
|
||||
size="sm"
|
||||
primaryButtonOnClick={
|
||||
(workspaceProjectIds ?? []).length > 0
|
||||
? currentView !== "custom-view" && currentView !== "subscribed"
|
||||
|
@ -109,6 +109,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
||||
const [iAmFeelingLucky, setIAmFeelingLucky] = useState(false);
|
||||
// refs
|
||||
const editorRef = useRef<EditorRefApi>(null);
|
||||
const submitBtnRef = useRef<HTMLButtonElement | null>(null);
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
@ -470,6 +471,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
||||
onChange(description_html);
|
||||
handleFormChange();
|
||||
}}
|
||||
onEnterKeyPress={() => submitBtnRef?.current?.click()}
|
||||
ref={editorRef}
|
||||
tabIndex={getTabIndex("description_html")}
|
||||
placeholder={getDescriptionPlaceholder}
|
||||
@ -770,6 +772,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
||||
variant="primary"
|
||||
type="submit"
|
||||
size="sm"
|
||||
ref={submitBtnRef}
|
||||
loading={isSubmitting}
|
||||
tabIndex={isDraft ? getTabIndex("submit_button") : getTabIndex("draft_button")}
|
||||
>
|
||||
|
@ -10,10 +10,11 @@ import {
|
||||
XCircle,
|
||||
CalendarClock,
|
||||
CalendarCheck2,
|
||||
Users,
|
||||
} from "lucide-react";
|
||||
// hooks
|
||||
// ui icons
|
||||
import { DiceIcon, DoubleCircleIcon, UserGroupIcon, ContrastIcon, RelatedIcon } from "@plane/ui";
|
||||
import { DiceIcon, DoubleCircleIcon, ContrastIcon, RelatedIcon } from "@plane/ui";
|
||||
// components
|
||||
import {
|
||||
DateDropdown,
|
||||
@ -94,7 +95,7 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
|
||||
{/* assignee */}
|
||||
<div className="flex w-full items-center gap-3 h-8">
|
||||
<div className="flex items-center gap-1 w-1/4 flex-shrink-0 text-sm text-custom-text-300">
|
||||
<UserGroupIcon className="h-4 w-4 flex-shrink-0" />
|
||||
<Users className="h-4 w-4 flex-shrink-0" />
|
||||
<span>Assignees</span>
|
||||
</div>
|
||||
<MemberDropdown
|
||||
|
@ -2,7 +2,7 @@ import React, { useRef } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { CalendarCheck2, CalendarClock, Info, MoveRight, User2 } from "lucide-react";
|
||||
import { CalendarCheck2, CalendarClock, Info, MoveRight, SquareUser } from "lucide-react";
|
||||
// ui
|
||||
import { LayersIcon, Tooltip, setPromiseToast } from "@plane/ui";
|
||||
// components
|
||||
@ -188,9 +188,7 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
|
||||
</span>
|
||||
) : (
|
||||
<Tooltip tooltipContent="No lead">
|
||||
<span className="cursor-default flex h-5 w-5 items-end justify-center rounded-full border border-dashed border-custom-text-400 bg-custom-background-80">
|
||||
<User2 className="h-4 w-4 text-custom-text-400" />
|
||||
</span>
|
||||
<SquareUser className="h-4 w-4 mx-1 text-custom-text-300 " />
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
|
@ -2,7 +2,7 @@ import React, { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useRouter } from "next/router";
|
||||
// icons
|
||||
import { CalendarCheck2, CalendarClock, MoveRight, User2 } from "lucide-react";
|
||||
import { CalendarCheck2, CalendarClock, MoveRight, SquareUser } from "lucide-react";
|
||||
// types
|
||||
import { IModule } from "@plane/types";
|
||||
// ui
|
||||
@ -140,9 +140,7 @@ export const ModuleListItemAction: FC<Props> = observer((props) => {
|
||||
</span>
|
||||
) : (
|
||||
<Tooltip tooltipContent="No lead">
|
||||
<span className="cursor-default flex h-5 w-5 items-end justify-center rounded-full border border-dashed border-custom-text-400 bg-custom-background-80">
|
||||
<User2 className="h-4 w-4 text-custom-text-400" />
|
||||
</span>
|
||||
<SquareUser className="h-4 w-4 text-custom-text-300" />
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import isEqual from "lodash/isEqual";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useRouter } from "next/router";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
@ -11,8 +12,9 @@ import {
|
||||
Info,
|
||||
LinkIcon,
|
||||
Plus,
|
||||
SquareUser,
|
||||
Trash2,
|
||||
UserCircle2,
|
||||
Users,
|
||||
} from "lucide-react";
|
||||
import { Disclosure, Transition } from "@headlessui/react";
|
||||
import { IIssueFilterOptions, ILinkDetails, IModule, ModuleLink } from "@plane/types";
|
||||
@ -23,7 +25,6 @@ import {
|
||||
LayersIcon,
|
||||
CustomSelect,
|
||||
ModuleStatusIcon,
|
||||
UserGroupIcon,
|
||||
TOAST_TYPE,
|
||||
setToast,
|
||||
ArchiveIcon,
|
||||
@ -252,14 +253,18 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
const handleFiltersUpdate = useCallback(
|
||||
(key: keyof IIssueFilterOptions, value: string | string[]) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
const newValues = issueFilters?.filters?.[key] ?? [];
|
||||
let 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);
|
||||
});
|
||||
if (key === "state") {
|
||||
if (isEqual(newValues, value)) newValues = [];
|
||||
else newValues = value;
|
||||
} else {
|
||||
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);
|
||||
@ -493,7 +498,7 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
<div className="flex flex-col gap-5 pb-6 pt-2.5">
|
||||
<div className="flex items-center justify-start gap-1">
|
||||
<div className="flex w-2/5 items-center justify-start gap-2 text-custom-text-300">
|
||||
<UserCircle2 className="h-4 w-4" />
|
||||
<SquareUser className="h-4 w-4" />
|
||||
<span className="text-base">Lead</span>
|
||||
</div>
|
||||
<Controller
|
||||
@ -511,6 +516,7 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
buttonVariant="background-with-text"
|
||||
placeholder="Lead"
|
||||
disabled={!isEditingAllowed || isArchived}
|
||||
icon={SquareUser}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@ -518,7 +524,7 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
</div>
|
||||
<div className="flex items-center justify-start gap-1">
|
||||
<div className="flex w-2/5 items-center justify-start gap-2 text-custom-text-300">
|
||||
<UserGroupIcon className="h-4 w-4" />
|
||||
<Users className="h-4 w-4" />
|
||||
<span className="text-base">Members</span>
|
||||
</div>
|
||||
<Controller
|
||||
|
@ -16,9 +16,11 @@ export const ProjectCardList = observer(() => {
|
||||
// store hooks
|
||||
const { toggleCreateProjectModal } = useCommandPalette();
|
||||
const { setTrackElement } = useEventTracker();
|
||||
const { workspaceProjectIds, filteredProjectIds, getProjectById } = useProject();
|
||||
const { workspaceProjectIds, filteredProjectIds, getProjectById, loader } = useProject();
|
||||
const { searchQuery, currentWorkspaceDisplayFilters } = useProjectFilter();
|
||||
|
||||
if (!filteredProjectIds || !workspaceProjectIds || loader) return <ProjectsLoader />;
|
||||
|
||||
if (workspaceProjectIds?.length === 0 && !currentWorkspaceDisplayFilters?.archived_projects)
|
||||
return (
|
||||
<EmptyState
|
||||
@ -30,8 +32,6 @@ export const ProjectCardList = observer(() => {
|
||||
/>
|
||||
);
|
||||
|
||||
if (!filteredProjectIds) return <ProjectsLoader />;
|
||||
|
||||
if (filteredProjectIds.length === 0)
|
||||
return (
|
||||
<div className="grid h-full w-full place-items-center">
|
||||
|
@ -1,8 +1,13 @@
|
||||
import { useRef, useState } from "react";
|
||||
import { DraggableProvided, DraggableStateSnapshot } from "@hello-pangea/dnd";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
|
||||
import { draggable, dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
|
||||
import { pointerOutsideOfPreview } from "@atlaskit/pragmatic-drag-and-drop/element/pointer-outside-of-preview";
|
||||
import { setCustomNativeDragPreview } from "@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview";
|
||||
import { attachInstruction, extractInstruction } from "@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import {
|
||||
MoreVertical,
|
||||
PenSquare,
|
||||
@ -28,6 +33,7 @@ import {
|
||||
ContrastIcon,
|
||||
LayersIcon,
|
||||
setPromiseToast,
|
||||
DropIndicator,
|
||||
} from "@plane/ui";
|
||||
import { LeaveProjectModal, ProjectLogo, PublishProjectModal } from "@/components/project";
|
||||
import { EUserProjectRoles } from "@/constants/project";
|
||||
@ -36,17 +42,23 @@ import { cn } from "@/helpers/common.helper";
|
||||
import { useAppTheme, useEventTracker, useProject } from "@/hooks/store";
|
||||
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
import { HIGHLIGHT_CLASS, highlightIssueOnDrop } from "../issues/issue-layouts/utils";
|
||||
// helpers
|
||||
|
||||
// components
|
||||
|
||||
type Props = {
|
||||
projectId: string;
|
||||
provided?: DraggableProvided;
|
||||
snapshot?: DraggableStateSnapshot;
|
||||
handleCopyText: () => void;
|
||||
shortContextMenu?: boolean;
|
||||
handleOnProjectDrop?: (
|
||||
sourceId: string | undefined,
|
||||
destinationId: string | undefined,
|
||||
shouldDropAtEnd: boolean
|
||||
) => void;
|
||||
projectListType: "JOINED" | "FAVORITES";
|
||||
disableDrag?: boolean;
|
||||
disableDrop?: boolean;
|
||||
isLastChild: boolean;
|
||||
};
|
||||
|
||||
const navigation = (workspaceSlug: string, projectId: string) => [
|
||||
@ -89,7 +101,8 @@ const navigation = (workspaceSlug: string, projectId: string) => [
|
||||
|
||||
export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { projectId, provided, snapshot, handleCopyText, shortContextMenu = false, disableDrag } = props;
|
||||
const { projectId, handleCopyText, disableDrag, disableDrop, isLastChild, handleOnProjectDrop, projectListType } =
|
||||
props;
|
||||
// store hooks
|
||||
const { sidebarCollapsed: isCollapsed, toggleSidebar } = useAppTheme();
|
||||
const { setTrackElement } = useEventTracker();
|
||||
@ -99,8 +112,12 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
|
||||
const [leaveProjectModalOpen, setLeaveProjectModal] = useState(false);
|
||||
const [publishModalOpen, setPublishModal] = useState(false);
|
||||
const [isMenuActive, setIsMenuActive] = useState(false);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [instruction, setInstruction] = useState<"DRAG_OVER" | "DRAG_BELOW" | undefined>(undefined);
|
||||
// refs
|
||||
const actionSectionRef = useRef<HTMLDivElement | null>(null);
|
||||
const projectRef = useRef<HTMLDivElement | null>(null);
|
||||
const dragHandleRef = useRef<HTMLButtonElement | null>(null);
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId: URLProjectId } = router.query;
|
||||
@ -160,7 +177,97 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const element = projectRef.current;
|
||||
const dragHandleElement = dragHandleRef.current;
|
||||
|
||||
if (!element) return;
|
||||
|
||||
return combine(
|
||||
draggable({
|
||||
element,
|
||||
canDrag: () => !disableDrag,
|
||||
dragHandle: dragHandleElement ?? undefined,
|
||||
getInitialData: () => ({ id: projectId, dragInstanceId: "PROJECTS" }),
|
||||
onDragStart: () => {
|
||||
setIsDragging(true);
|
||||
},
|
||||
onDrop: () => {
|
||||
setIsDragging(false);
|
||||
},
|
||||
onGenerateDragPreview: ({ nativeSetDragImage }) => {
|
||||
// Add a custom drag image
|
||||
setCustomNativeDragPreview({
|
||||
getOffset: pointerOutsideOfPreview({ x: "0px", y: "0px" }),
|
||||
render: ({ container }) => {
|
||||
const root = createRoot(container);
|
||||
root.render(
|
||||
<div className="rounded flex items-center bg-custom-background-100 text-sm p-1 pr-2">
|
||||
<div className="flex items-center h-7 w-5 grid place-items-center flex-shrink-0">
|
||||
{project && <ProjectLogo logo={project?.logo_props} />}
|
||||
</div>
|
||||
<p className="truncate text-custom-sidebar-text-200">{project?.name}</p>
|
||||
</div>
|
||||
);
|
||||
return () => root.unmount();
|
||||
},
|
||||
nativeSetDragImage,
|
||||
});
|
||||
},
|
||||
}),
|
||||
dropTargetForElements({
|
||||
element,
|
||||
canDrop: ({ source }) =>
|
||||
!disableDrop && source?.data?.id !== projectId && source?.data?.dragInstanceId === "PROJECTS",
|
||||
getData: ({ input, element }) => {
|
||||
const data = { id: projectId };
|
||||
|
||||
// attach instruction for last in list
|
||||
return attachInstruction(data, {
|
||||
input,
|
||||
element,
|
||||
currentLevel: 0,
|
||||
indentPerLevel: 0,
|
||||
mode: isLastChild ? "last-in-group" : "standard",
|
||||
});
|
||||
},
|
||||
onDrag: ({ self }) => {
|
||||
const extractedInstruction = extractInstruction(self?.data)?.type;
|
||||
// check if the highlight is to be shown above or below
|
||||
setInstruction(
|
||||
extractedInstruction
|
||||
? extractedInstruction === "reorder-below" && isLastChild
|
||||
? "DRAG_BELOW"
|
||||
: "DRAG_OVER"
|
||||
: undefined
|
||||
);
|
||||
},
|
||||
onDragLeave: () => {
|
||||
setInstruction(undefined);
|
||||
},
|
||||
onDrop: ({ self, source }) => {
|
||||
setInstruction(undefined);
|
||||
const extractedInstruction = extractInstruction(self?.data)?.type;
|
||||
const currentInstruction = extractedInstruction
|
||||
? extractedInstruction === "reorder-below" && isLastChild
|
||||
? "DRAG_BELOW"
|
||||
: "DRAG_OVER"
|
||||
: undefined;
|
||||
if (!currentInstruction) return;
|
||||
|
||||
const sourceId = source?.data?.id as string | undefined;
|
||||
const destinationId = self?.data?.id as string | undefined;
|
||||
|
||||
handleOnProjectDrop && handleOnProjectDrop(sourceId, destinationId, currentInstruction === "DRAG_BELOW");
|
||||
|
||||
highlightIssueOnDrop(`sidebar-${sourceId}-${projectListType}`);
|
||||
},
|
||||
})
|
||||
);
|
||||
}, [projectRef?.current, dragHandleRef?.current, projectId, isLastChild, projectListType, handleOnProjectDrop]);
|
||||
|
||||
useOutsideClickDetector(actionSectionRef, () => setIsMenuActive(false));
|
||||
useOutsideClickDetector(projectRef, () => projectRef?.current?.classList?.remove(HIGHLIGHT_CLASS));
|
||||
|
||||
if (!project) return null;
|
||||
|
||||
@ -168,48 +275,47 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
|
||||
<>
|
||||
<PublishProjectModal isOpen={publishModalOpen} project={project} onClose={() => setPublishModal(false)} />
|
||||
<LeaveProjectModal project={project} isOpen={leaveProjectModalOpen} onClose={handleLeaveProjectModalClose} />
|
||||
<Disclosure key={`${project.id} ${URLProjectId}`} defaultOpen={URLProjectId === project.id}>
|
||||
<Disclosure key={`${project.id}_${URLProjectId}`} ref={projectRef} defaultOpen={URLProjectId === project.id}>
|
||||
{({ open }) => (
|
||||
<>
|
||||
<div
|
||||
id={`sidebar-${projectId}-${projectListType}`}
|
||||
className={cn("rounded relative", { "bg-custom-sidebar-background-80 opacity-60": isDragging })}
|
||||
>
|
||||
<DropIndicator classNames="absolute top-0" isVisible={instruction === "DRAG_OVER"} />
|
||||
<div
|
||||
className={cn(
|
||||
"group relative flex w-full items-center rounded-md px-2 py-1 text-custom-sidebar-text-100 hover:bg-custom-sidebar-background-80",
|
||||
"group relative flex w-full items-center rounded-md py-1 text-custom-sidebar-text-100 hover:bg-custom-sidebar-background-80",
|
||||
{
|
||||
"opacity-60": snapshot?.isDragging,
|
||||
"bg-custom-sidebar-background-80": isMenuActive,
|
||||
"pl-8": disableDrag,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{provided && !disableDrag && (
|
||||
{!disableDrag && (
|
||||
<Tooltip
|
||||
isMobile={isMobile}
|
||||
tooltipContent={project.sort_order === null ? "Join the project to rearrange" : "Drag to rearrange"}
|
||||
position="top-right"
|
||||
disabled={isDragging}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"absolute -left-2.5 top-1/2 hidden -translate-y-1/2 rounded p-0.5 text-custom-sidebar-text-400",
|
||||
"flex opacity-0 rounded text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-80 ml-2",
|
||||
{
|
||||
"group-hover:flex": !isCollapsed,
|
||||
"group-hover:opacity-100": !isCollapsed,
|
||||
"cursor-not-allowed opacity-60": project.sort_order === null,
|
||||
flex: isMenuActive,
|
||||
}
|
||||
)}
|
||||
{...provided?.dragHandleProps}
|
||||
ref={dragHandleRef}
|
||||
>
|
||||
<MoreVertical className="h-3.5" />
|
||||
<MoreVertical className="-ml-3 h-3.5" />
|
||||
<MoreVertical className="-ml-5 h-3.5" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip
|
||||
tooltipContent={`${project.name}`}
|
||||
position="right"
|
||||
className="ml-2"
|
||||
disabled={!isCollapsed}
|
||||
isMobile={isMobile}
|
||||
>
|
||||
<Tooltip tooltipContent={`${project.name}`} position="right" disabled={!isCollapsed} isMobile={isMobile}>
|
||||
<Disclosure.Button
|
||||
as="div"
|
||||
className={cn(
|
||||
@ -220,11 +326,11 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn("flex w-full flex-grow items-center gap-1 truncate", {
|
||||
className={cn("flex w-full flex-grow items-center gap-1 truncate -ml-1", {
|
||||
"justify-center": isCollapsed,
|
||||
})}
|
||||
>
|
||||
<div className="h-7 w-7 grid place-items-center flex-shrink-0">
|
||||
<div className="h-7 w-5 grid place-items-center flex-shrink-0">
|
||||
<ProjectLogo logo={project.logo_props} />
|
||||
</div>
|
||||
{!isCollapsed && <p className="truncate text-custom-sidebar-text-200">{project.name}</p>}
|
||||
@ -380,7 +486,8 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
|
||||
})}
|
||||
</Disclosure.Panel>
|
||||
</Transition>
|
||||
</>
|
||||
{isLastChild && <DropIndicator isVisible={instruction === "DRAG_BELOW"} />}
|
||||
</div>
|
||||
)}
|
||||
</Disclosure>
|
||||
</>
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { useState, FC, useRef, useEffect } from "react";
|
||||
import { DragDropContext, Draggable, DropResult, Droppable } from "@hello-pangea/dnd";
|
||||
import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
|
||||
import { autoScrollForElements } from "@atlaskit/pragmatic-drag-and-drop-auto-scroll/element";
|
||||
import { observer } from "mobx-react";
|
||||
import { useRouter } from "next/router";
|
||||
import { ChevronDown, ChevronRight, Plus } from "lucide-react";
|
||||
@ -54,21 +55,28 @@ export const ProjectSidebarList: FC = observer(() => {
|
||||
});
|
||||
};
|
||||
|
||||
const onDragEnd = (result: DropResult) => {
|
||||
const { source, destination, draggableId } = result;
|
||||
if (!destination || !workspaceSlug) return;
|
||||
if (source.index === destination.index) return;
|
||||
const handleOnProjectDrop = (
|
||||
sourceId: string | undefined,
|
||||
destinationId: string | undefined,
|
||||
shouldDropAtEnd: boolean
|
||||
) => {
|
||||
if (!sourceId || !destinationId || !workspaceSlug) return;
|
||||
if (sourceId === destinationId) return;
|
||||
|
||||
const joinedProjectsList: IProject[] = [];
|
||||
joinedProjects.map((projectId) => {
|
||||
const projectDetails = getProjectById(projectId);
|
||||
if (projectDetails) joinedProjectsList.push(projectDetails);
|
||||
});
|
||||
|
||||
const sourceIndex = joinedProjects.indexOf(sourceId);
|
||||
const destinationIndex = shouldDropAtEnd ? joinedProjects.length : joinedProjects.indexOf(destinationId);
|
||||
|
||||
if (joinedProjectsList.length <= 0) return;
|
||||
|
||||
const updatedSortOrder = orderJoinedProjects(source.index, destination.index, draggableId, joinedProjectsList);
|
||||
const updatedSortOrder = orderJoinedProjects(sourceIndex, destinationIndex, sourceId, joinedProjectsList);
|
||||
if (updatedSortOrder != undefined)
|
||||
updateProjectView(workspaceSlug.toString(), draggableId, { sort_order: updatedSortOrder }).catch(() => {
|
||||
updateProjectView(workspaceSlug.toString(), sourceId, { sort_order: updatedSortOrder }).catch(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
@ -98,7 +106,21 @@ export const ProjectSidebarList: FC = observer(() => {
|
||||
currentContainerRef.removeEventListener("scroll", handleScroll);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
}, [containerRef]);
|
||||
|
||||
useEffect(() => {
|
||||
const element = containerRef.current;
|
||||
|
||||
if (!element) return;
|
||||
|
||||
return combine(
|
||||
autoScrollForElements({
|
||||
element,
|
||||
canScroll: ({ source }) => source?.data?.dragInstanceId === "PROJECTS",
|
||||
getAllowedAxis: () => "vertical",
|
||||
})
|
||||
);
|
||||
}, [containerRef]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -123,156 +145,117 @@ export const ProjectSidebarList: FC = observer(() => {
|
||||
}
|
||||
)}
|
||||
>
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<Droppable droppableId="favorite-projects">
|
||||
{(provided) => (
|
||||
<div ref={provided.innerRef} {...provided.droppableProps}>
|
||||
{favoriteProjects && favoriteProjects.length > 0 && (
|
||||
<Disclosure as="div" className="flex flex-col" defaultOpen>
|
||||
{({ open }) => (
|
||||
<>
|
||||
{!isCollapsed && (
|
||||
<div className="group flex w-full items-center justify-between rounded p-1.5 text-xs text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-80">
|
||||
<Disclosure.Button
|
||||
as="button"
|
||||
type="button"
|
||||
className="group flex w-full items-center gap-1 whitespace-nowrap rounded px-1.5 text-left text-sm font-semibold text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-80"
|
||||
>
|
||||
Favorites
|
||||
{open ? (
|
||||
<ChevronDown className="h-3.5 w-3.5" />
|
||||
) : (
|
||||
<ChevronRight className="h-3.5 w-3.5" />
|
||||
)}
|
||||
</Disclosure.Button>
|
||||
{isAuthorizedUser && (
|
||||
<button
|
||||
className="opacity-0 group-hover:opacity-100"
|
||||
onClick={() => {
|
||||
setTrackElement("APP_SIDEBAR_FAVORITES_BLOCK");
|
||||
setIsFavoriteProjectCreate(true);
|
||||
setIsProjectModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<Plus className="h-3 w-3" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<Transition
|
||||
enter="transition duration-100 ease-out"
|
||||
enterFrom="transform scale-95 opacity-0"
|
||||
enterTo="transform scale-100 opacity-100"
|
||||
leave="transition duration-75 ease-out"
|
||||
leaveFrom="transform scale-100 opacity-100"
|
||||
leaveTo="transform scale-95 opacity-0"
|
||||
<div>
|
||||
{favoriteProjects && favoriteProjects.length > 0 && (
|
||||
<Disclosure as="div" className="flex flex-col" defaultOpen>
|
||||
{({ open }) => (
|
||||
<>
|
||||
{!isCollapsed && (
|
||||
<div className="group flex w-full items-center justify-between rounded p-1.5 text-xs text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-80">
|
||||
<Disclosure.Button
|
||||
as="button"
|
||||
type="button"
|
||||
className="group flex w-full items-center gap-1 whitespace-nowrap rounded px-1.5 text-left text-sm font-semibold text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-80"
|
||||
>
|
||||
Favorites
|
||||
{open ? <ChevronDown className="h-3.5 w-3.5" /> : <ChevronRight className="h-3.5 w-3.5" />}
|
||||
</Disclosure.Button>
|
||||
{isAuthorizedUser && (
|
||||
<button
|
||||
className="opacity-0 group-hover:opacity-100"
|
||||
onClick={() => {
|
||||
setTrackElement("APP_SIDEBAR_FAVORITES_BLOCK");
|
||||
setIsFavoriteProjectCreate(true);
|
||||
setIsProjectModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<Disclosure.Panel as="div" className="space-y-2">
|
||||
{favoriteProjects.map((projectId, index) => (
|
||||
<Draggable
|
||||
key={projectId}
|
||||
draggableId={projectId}
|
||||
index={index}
|
||||
// FIXME refactor the Draggable to a different component
|
||||
//isDragDisabled={!project.is_member}
|
||||
>
|
||||
{(provided, snapshot) => (
|
||||
<div ref={provided.innerRef} {...provided.draggableProps}>
|
||||
<ProjectSidebarListItem
|
||||
key={projectId}
|
||||
projectId={projectId}
|
||||
provided={provided}
|
||||
snapshot={snapshot}
|
||||
handleCopyText={() => handleCopyText(projectId)}
|
||||
shortContextMenu
|
||||
disableDrag
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
</Disclosure.Panel>
|
||||
</Transition>
|
||||
{provided.placeholder}
|
||||
</>
|
||||
)}
|
||||
</Disclosure>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<Droppable droppableId="joined-projects">
|
||||
{(provided) => (
|
||||
<div ref={provided.innerRef} {...provided.droppableProps}>
|
||||
{joinedProjects && joinedProjects.length > 0 && (
|
||||
<Disclosure as="div" className="flex flex-col" defaultOpen>
|
||||
{({ open }) => (
|
||||
<>
|
||||
{!isCollapsed && (
|
||||
<div className="group flex w-full items-center justify-between rounded p-1.5 text-xs text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-80">
|
||||
<Disclosure.Button
|
||||
as="button"
|
||||
type="button"
|
||||
className="group flex w-full items-center gap-1 whitespace-nowrap rounded px-1.5 text-left text-sm font-semibold text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-80"
|
||||
>
|
||||
Your projects
|
||||
{open ? (
|
||||
<ChevronDown className="h-3.5 w-3.5" />
|
||||
) : (
|
||||
<ChevronRight className="h-3.5 w-3.5" />
|
||||
)}
|
||||
</Disclosure.Button>
|
||||
{isAuthorizedUser && (
|
||||
<button
|
||||
className="opacity-0 group-hover:opacity-100"
|
||||
onClick={() => {
|
||||
setTrackElement("Sidebar");
|
||||
setIsFavoriteProjectCreate(false);
|
||||
setIsProjectModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<Plus className="h-3 w-3" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<Transition
|
||||
enter="transition duration-100 ease-out"
|
||||
enterFrom="transform scale-95 opacity-0"
|
||||
enterTo="transform scale-100 opacity-100"
|
||||
leave="transition duration-75 ease-out"
|
||||
leaveFrom="transform scale-100 opacity-100"
|
||||
leaveTo="transform scale-95 opacity-0"
|
||||
<Plus className="h-3 w-3" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<Transition
|
||||
enter="transition duration-100 ease-out"
|
||||
enterFrom="transform scale-95 opacity-0"
|
||||
enterTo="transform scale-100 opacity-100"
|
||||
leave="transition duration-75 ease-out"
|
||||
leaveFrom="transform scale-100 opacity-100"
|
||||
leaveTo="transform scale-95 opacity-0"
|
||||
>
|
||||
<Disclosure.Panel as="div" className="space-y-2">
|
||||
{favoriteProjects.map((projectId, index) => (
|
||||
<ProjectSidebarListItem
|
||||
key={projectId}
|
||||
projectId={projectId}
|
||||
handleCopyText={() => handleCopyText(projectId)}
|
||||
projectListType="FAVORITES"
|
||||
disableDrag
|
||||
disableDrop
|
||||
isLastChild={index === favoriteProjects.length - 1}
|
||||
/>
|
||||
))}
|
||||
</Disclosure.Panel>
|
||||
</Transition>
|
||||
</>
|
||||
)}
|
||||
</Disclosure>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
{joinedProjects && joinedProjects.length > 0 && (
|
||||
<Disclosure as="div" className="flex flex-col" defaultOpen>
|
||||
{({ open }) => (
|
||||
<>
|
||||
{!isCollapsed && (
|
||||
<div className="group flex w-full items-center justify-between rounded p-1.5 text-xs text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-80">
|
||||
<Disclosure.Button
|
||||
as="button"
|
||||
type="button"
|
||||
className="group flex w-full items-center gap-1 whitespace-nowrap rounded px-1.5 text-left text-sm font-semibold text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-80"
|
||||
>
|
||||
Your projects
|
||||
{open ? <ChevronDown className="h-3.5 w-3.5" /> : <ChevronRight className="h-3.5 w-3.5" />}
|
||||
</Disclosure.Button>
|
||||
{isAuthorizedUser && (
|
||||
<button
|
||||
className="opacity-0 group-hover:opacity-100"
|
||||
onClick={() => {
|
||||
setTrackElement("Sidebar");
|
||||
setIsFavoriteProjectCreate(false);
|
||||
setIsProjectModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<Disclosure.Panel as="div" className="space-y-2">
|
||||
{joinedProjects.map((projectId, index) => (
|
||||
<Draggable key={projectId} draggableId={projectId} index={index}>
|
||||
{(provided, snapshot) => (
|
||||
<div ref={provided.innerRef} {...provided.draggableProps}>
|
||||
<ProjectSidebarListItem
|
||||
key={projectId}
|
||||
projectId={projectId}
|
||||
provided={provided}
|
||||
snapshot={snapshot}
|
||||
handleCopyText={() => handleCopyText(projectId)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
</Disclosure.Panel>
|
||||
</Transition>
|
||||
{provided.placeholder}
|
||||
</>
|
||||
)}
|
||||
</Disclosure>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
<Plus className="h-3 w-3" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<Transition
|
||||
enter="transition duration-100 ease-out"
|
||||
enterFrom="transform scale-95 opacity-0"
|
||||
enterTo="transform scale-100 opacity-100"
|
||||
leave="transition duration-75 ease-out"
|
||||
leaveFrom="transform scale-100 opacity-100"
|
||||
leaveTo="transform scale-95 opacity-0"
|
||||
>
|
||||
<Disclosure.Panel as="div">
|
||||
{joinedProjects.map((projectId, index) => (
|
||||
<ProjectSidebarListItem
|
||||
key={projectId}
|
||||
projectId={projectId}
|
||||
projectListType="JOINED"
|
||||
handleCopyText={() => handleCopyText(projectId)}
|
||||
isLastChild={index === joinedProjects.length - 1}
|
||||
handleOnProjectDrop={handleOnProjectDrop}
|
||||
/>
|
||||
))}
|
||||
</Disclosure.Panel>
|
||||
</Transition>
|
||||
</>
|
||||
)}
|
||||
</Disclosure>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isAuthorizedUser && joinedProjects && joinedProjects.length === 0 && (
|
||||
<button
|
||||
|
@ -4,7 +4,7 @@ import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { usePopper } from "react-popper";
|
||||
// icons
|
||||
import { Check, ChevronDown, CircleUserRound, LogOut, Mails, PlusSquare, Settings, UserCircle2 } from "lucide-react";
|
||||
import { Activity, Check, ChevronDown, LogOut, Mails, PlusSquare, Settings } from "lucide-react";
|
||||
// ui
|
||||
import { Menu, Transition } from "@headlessui/react";
|
||||
// types
|
||||
@ -27,7 +27,7 @@ const userLinks = (workspaceSlug: string, userId: string) => [
|
||||
key: "my_activity",
|
||||
name: "My activity",
|
||||
href: `/${workspaceSlug}/profile/${userId}`,
|
||||
icon: CircleUserRound,
|
||||
icon: Activity,
|
||||
},
|
||||
{
|
||||
key: "settings",
|
||||
@ -39,7 +39,7 @@ const userLinks = (workspaceSlug: string, userId: string) => [
|
||||
const profileLinks = (workspaceSlug: string, userId: string) => [
|
||||
{
|
||||
name: "My activity",
|
||||
icon: UserCircle2,
|
||||
icon: Activity,
|
||||
link: `/${workspaceSlug}/profile/${userId}`,
|
||||
},
|
||||
{
|
||||
|
@ -1,7 +1,12 @@
|
||||
import { FC } from "react";
|
||||
// icons
|
||||
import { CalendarDays, Link2, Signal, Tag, Triangle, Paperclip, CalendarCheck2, CalendarClock, Users } from "lucide-react";
|
||||
// types
|
||||
import { IIssueDisplayProperties, TIssue, TIssueOrderByOptions } from "@plane/types";
|
||||
// ui
|
||||
import { LayersIcon, DoubleCircleIcon, DiceIcon, ContrastIcon } from "@plane/ui";
|
||||
import { ISvgIcons } from "@plane/ui/src/icons/type";
|
||||
import { CalendarDays, Link2, Signal, Tag, Triangle, Paperclip, CalendarCheck2, CalendarClock } from "lucide-react";
|
||||
import { LayersIcon, DoubleCircleIcon, UserGroupIcon, DiceIcon, ContrastIcon } from "@plane/ui";
|
||||
// components
|
||||
import {
|
||||
SpreadsheetAssigneeColumn,
|
||||
SpreadsheetAttachmentColumn,
|
||||
@ -18,7 +23,6 @@ import {
|
||||
SpreadsheetSubIssueColumn,
|
||||
SpreadsheetUpdatedOnColumn,
|
||||
} from "@/components/issues/issue-layouts/spreadsheet";
|
||||
import { IIssueDisplayProperties, TIssue, TIssueOrderByOptions } from "@plane/types";
|
||||
|
||||
export const SPREADSHEET_PROPERTY_DETAILS: {
|
||||
[key: string]: {
|
||||
@ -42,7 +46,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: {
|
||||
ascendingOrderTitle: "A",
|
||||
descendingOrderKey: "-assignees__first_name",
|
||||
descendingOrderTitle: "Z",
|
||||
icon: UserGroupIcon,
|
||||
icon: Users,
|
||||
Column: SpreadsheetAssigneeColumn,
|
||||
},
|
||||
created_on: {
|
||||
|
@ -29,23 +29,16 @@ export const orderJoinedProjects = (
|
||||
// updating project at the top of the project
|
||||
const currentSortOrder = joinedProjects[destinationIndex].sort_order || 0;
|
||||
updatedSortOrder = currentSortOrder - sortOrderDefaultValue;
|
||||
} else if (destinationIndex === joinedProjects.length - 1) {
|
||||
} else if (destinationIndex === joinedProjects.length) {
|
||||
// updating project at the bottom of the project
|
||||
const currentSortOrder = joinedProjects[destinationIndex - 1].sort_order || 0;
|
||||
updatedSortOrder = currentSortOrder + sortOrderDefaultValue;
|
||||
} else {
|
||||
// updating project in the middle of the project
|
||||
if (sourceIndex > destinationIndex) {
|
||||
const destinationTopProjectSortOrder = joinedProjects[destinationIndex - 1].sort_order || 0;
|
||||
const destinationBottomProjectSortOrder = joinedProjects[destinationIndex].sort_order || 0;
|
||||
const updatedValue = (destinationTopProjectSortOrder + destinationBottomProjectSortOrder) / 2;
|
||||
updatedSortOrder = updatedValue;
|
||||
} else {
|
||||
const destinationTopProjectSortOrder = joinedProjects[destinationIndex].sort_order || 0;
|
||||
const destinationBottomProjectSortOrder = joinedProjects[destinationIndex + 1].sort_order || 0;
|
||||
const updatedValue = (destinationTopProjectSortOrder + destinationBottomProjectSortOrder) / 2;
|
||||
updatedSortOrder = updatedValue;
|
||||
}
|
||||
const destinationTopProjectSortOrder = joinedProjects[destinationIndex - 1].sort_order || 0;
|
||||
const destinationBottomProjectSortOrder = joinedProjects[destinationIndex].sort_order || 0;
|
||||
const updatedValue = (destinationTopProjectSortOrder + destinationBottomProjectSortOrder) / 2;
|
||||
updatedSortOrder = updatedValue;
|
||||
}
|
||||
|
||||
return updatedSortOrder;
|
||||
|
9
web/instrumentation.ts
Normal file
9
web/instrumentation.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export async function register() {
|
||||
if (process.env.NEXT_RUNTIME === 'nodejs') {
|
||||
await import('./sentry.server.config');
|
||||
}
|
||||
|
||||
if (process.env.NEXT_RUNTIME === 'edge') {
|
||||
await import('./sentry.edge.config');
|
||||
}
|
||||
}
|
@ -20,7 +20,7 @@ export interface IPosthogWrapper {
|
||||
}
|
||||
|
||||
const PostHogProvider: FC<IPosthogWrapper> = (props) => {
|
||||
const { children, user, workspaceRole, currentWorkspaceId, projectRole, posthogAPIKey, posthogHost } = props;
|
||||
const { children, user, workspaceRole, currentWorkspaceId, projectRole } = props;
|
||||
// states
|
||||
const [lastWorkspaceId, setLastWorkspaceId] = useState(currentWorkspaceId);
|
||||
// router
|
||||
@ -42,15 +42,16 @@ const PostHogProvider: FC<IPosthogWrapper> = (props) => {
|
||||
}, [user, workspaceRole, projectRole]);
|
||||
|
||||
useEffect(() => {
|
||||
if (posthogAPIKey && (process.env.NEXT_PUBLIC_POSTHOG_HOST || posthogHost)) {
|
||||
posthog.init(posthogAPIKey, {
|
||||
api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST || posthogHost || "https://app.posthog.com",
|
||||
if (process.env.NEXT_PUBLIC_POSTHOG_KEY && process.env.NEXT_PUBLIC_POSTHOG_HOST) {
|
||||
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY, {
|
||||
api_host: "/ingest",
|
||||
ui_host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
|
||||
debug: process.env.NEXT_PUBLIC_POSTHOG_DEBUG === "1", // Debug mode based on the environment variable
|
||||
autocapture: false,
|
||||
capture_pageview: false, // Disable automatic pageview capture, as we capture manually
|
||||
});
|
||||
}
|
||||
}, [posthogAPIKey, posthogHost]);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Join workspace group on workspace change
|
||||
@ -74,7 +75,9 @@ const PostHogProvider: FC<IPosthogWrapper> = (props) => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
if (posthogAPIKey) return <PHProvider client={posthog}>{children}</PHProvider>;
|
||||
if (process.env.NEXT_PUBLIC_POSTHOG_KEY && process.env.NEXT_PUBLIC_POSTHOG_HOST)
|
||||
return <PHProvider client={posthog}>{children}</PHProvider>;
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
|
@ -55,14 +55,15 @@ const nextConfig = {
|
||||
]
|
||||
},
|
||||
async rewrites() {
|
||||
const posthogHost = process.env.NEXT_PUBLIC_POSTHOG_HOST || "https://app.posthog.com"
|
||||
const rewrites = [
|
||||
{
|
||||
source: "/ingest/static/:path*",
|
||||
destination: "https://us-assets.i.posthog.com/static/:path*",
|
||||
destination: `${posthogHost}/static/:path*`,
|
||||
},
|
||||
{
|
||||
source: "/ingest/:path*",
|
||||
destination: "https://us.i.posthog.com/:path*",
|
||||
destination: `${posthogHost}/:path*`,
|
||||
},
|
||||
];
|
||||
if (process.env.NEXT_PUBLIC_ADMIN_BASE_URL || process.env.NEXT_PUBLIC_ADMIN_BASE_PATH) {
|
||||
@ -82,12 +83,40 @@ const nextConfig = {
|
||||
},
|
||||
};
|
||||
|
||||
if (parseInt(process.env.NEXT_PUBLIC_ENABLE_SENTRY || "0", 10)) {
|
||||
module.exports = withSentryConfig(
|
||||
nextConfig,
|
||||
{ silent: true, authToken: process.env.SENTRY_AUTH_TOKEN },
|
||||
{ hideSourceMaps: true }
|
||||
);
|
||||
const sentryConfig = {
|
||||
// For all available options, see:
|
||||
// https://github.com/getsentry/sentry-webpack-plugin#options
|
||||
org: process.env.SENTRY_ORG_ID || "plane-hq",
|
||||
project: process.env.SENTRY_PROJECT_ID || "plane-web",
|
||||
authToken: process.env.SENTRY_AUTH_TOKEN,
|
||||
// Only print logs for uploading source maps in CI
|
||||
silent: true,
|
||||
|
||||
// Upload a larger set of source maps for prettier stack traces (increases build time)
|
||||
widenClientFileUpload: true,
|
||||
|
||||
// Route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers.
|
||||
// This can increase your server load as well as your hosting bill.
|
||||
// Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client-
|
||||
// side errors will fail.
|
||||
tunnelRoute: "/monitoring",
|
||||
|
||||
// Hides source maps from generated client bundles
|
||||
hideSourceMaps: true,
|
||||
|
||||
// Automatically tree-shake Sentry logger statements to reduce bundle size
|
||||
disableLogger: true,
|
||||
|
||||
// Enables automatic instrumentation of Vercel Cron Monitors. (Does not yet work with App Router route handlers.)
|
||||
// See the following for more information:
|
||||
// https://docs.sentry.io/product/crons/
|
||||
// https://vercel.com/docs/cron-jobs
|
||||
automaticVercelMonitors: true,
|
||||
}
|
||||
|
||||
|
||||
if (parseInt(process.env.SENTRY_MONITORING_ENABLED || "0", 10)) {
|
||||
module.exports = withSentryConfig(nextConfig, sentryConfig);
|
||||
} else {
|
||||
module.exports = nextConfig;
|
||||
}
|
||||
|
@ -17,7 +17,6 @@
|
||||
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3",
|
||||
"@blueprintjs/popover2": "^1.13.3",
|
||||
"@headlessui/react": "^1.7.3",
|
||||
"@hello-pangea/dnd": "^16.3.0",
|
||||
"@nivo/bar": "0.80.0",
|
||||
"@nivo/calendar": "0.80.0",
|
||||
"@nivo/core": "0.80.0",
|
||||
@ -25,14 +24,14 @@
|
||||
"@nivo/line": "0.80.0",
|
||||
"@nivo/pie": "0.80.0",
|
||||
"@nivo/scatterplot": "0.80.0",
|
||||
"@plane/constants": "*",
|
||||
"@plane/document-editor": "*",
|
||||
"@plane/lite-text-editor": "*",
|
||||
"@plane/rich-text-editor": "*",
|
||||
"@plane/types": "*",
|
||||
"@plane/ui": "*",
|
||||
"@plane/constants": "*",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@sentry/nextjs": "^7.108.0",
|
||||
"@sentry/nextjs": "^8",
|
||||
"axios": "^1.1.3",
|
||||
"clsx": "^2.0.0",
|
||||
"cmdk": "^1.0.0",
|
||||
|
@ -34,23 +34,21 @@ const AnalyticsPage: NextPageWithLayout = observer(() => {
|
||||
{workspaceProjectIds && workspaceProjectIds.length > 0 ? (
|
||||
<div className="flex h-full flex-col overflow-hidden bg-custom-background-100">
|
||||
<Tab.Group as={Fragment} defaultIndex={analytics_tab === "custom" ? 1 : 0}>
|
||||
<Tab.List as="div" className="flex space-x-2 border-b border-custom-border-200 px-0 py-0 md:px-5 md:py-3">
|
||||
<Tab.List as="div" className="flex space-x-2 border-b h-[50px] border-custom-border-200 px-0 md:px-5">
|
||||
{ANALYTICS_TABS.map((tab) => (
|
||||
<Tab
|
||||
key={tab.key}
|
||||
className={({ selected }) =>
|
||||
`rounded-0 w-full border-b border-custom-border-200 px-0 py-2 text-xs hover:bg-custom-background-80 focus:outline-none md:w-max md:rounded-3xl md:border md:px-4 ${
|
||||
selected
|
||||
? "border-custom-primary-100 text-custom-primary-100 md:border-custom-border-200 md:bg-custom-background-80 md:text-custom-text-200"
|
||||
: "border-transparent"
|
||||
}`
|
||||
}
|
||||
onClick={() => {
|
||||
router.query.analytics_tab = tab.key;
|
||||
router.push(router);
|
||||
}}
|
||||
>
|
||||
{tab.title}
|
||||
<Tab key={tab.key} as={Fragment}>
|
||||
{({ selected }) => (
|
||||
<button
|
||||
className={`text-sm group relative flex items-center gap-1 h-[50px] px-3 cursor-pointer transition-all font-medium outline-none ${
|
||||
selected ? "text-custom-primary-100 " : "hover:text-custom-text-200"
|
||||
}`}
|
||||
>
|
||||
{tab.title}
|
||||
<div
|
||||
className={`border absolute bottom-0 right-0 left-0 rounded-t-md ${selected ? "border-custom-primary-100" : "border-transparent group-hover:border-custom-border-200"}`}
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
</Tab>
|
||||
))}
|
||||
</Tab.List>
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { useEffect, useState, ReactElement } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { ChevronDown, User2 } from "lucide-react";
|
||||
import { ChevronDown, CircleUserRound } from "lucide-react";
|
||||
import { Disclosure, Transition } from "@headlessui/react";
|
||||
// services
|
||||
// hooks
|
||||
@ -172,7 +172,7 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => {
|
||||
<button type="button" onClick={() => setIsImageUploadModalOpen(true)}>
|
||||
{!watch("avatar") || watch("avatar") === "" ? (
|
||||
<div className="h-16 w-16 rounded-md bg-custom-background-80 p-2">
|
||||
<User2 className="h-full w-full text-custom-text-200" />
|
||||
<CircleUserRound className="h-full w-full text-custom-text-200" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="relative h-16 w-16 overflow-hidden">
|
||||
|
@ -1,18 +0,0 @@
|
||||
// This file configures the initialization of Sentry on the browser.
|
||||
// The config you add here will be used whenever a page is visited.
|
||||
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
|
||||
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
|
||||
const SENTRY_DSN = process.env.NEXT_PUBLIC_SENTRY_DSN;
|
||||
|
||||
Sentry.init({
|
||||
dsn: SENTRY_DSN,
|
||||
environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT || "development",
|
||||
// Adjust this value in production, or use tracesSampler for greater control
|
||||
tracesSampleRate: 1.0,
|
||||
// ...
|
||||
// Note: if you want to override the automatic release value, do not set a
|
||||
// `release` value here - use the environment variable `SENTRY_RELEASE`, so
|
||||
// that it will also get attached to your source maps
|
||||
});
|
31
web/sentry.client.config.ts
Normal file
31
web/sentry.client.config.ts
Normal file
@ -0,0 +1,31 @@
|
||||
// This file configures the initialization of Sentry on the client.
|
||||
// The config you add here will be used whenever a users loads a page in their browser.
|
||||
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
|
||||
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
|
||||
Sentry.init({
|
||||
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||
environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT || "development",
|
||||
|
||||
// Adjust this value in production, or use tracesSampler for greater control
|
||||
tracesSampleRate: 1,
|
||||
|
||||
// Setting this option to true will print useful information to the console while you're setting up Sentry.
|
||||
debug: false,
|
||||
|
||||
replaysOnErrorSampleRate: 1.0,
|
||||
|
||||
// This sets the sample rate to be 10%. You may want this to be 100% while
|
||||
// in development and sample at a lower rate in production
|
||||
replaysSessionSampleRate: 0.1,
|
||||
|
||||
// You can remove this option if you're not planning to use the Sentry Session Replay feature:
|
||||
integrations: [
|
||||
Sentry.replayIntegration({
|
||||
// Additional Replay configuration goes in here, for example:
|
||||
maskAllText: true,
|
||||
blockAllMedia: true,
|
||||
}),
|
||||
],
|
||||
});
|
@ -1,18 +0,0 @@
|
||||
// This file configures the initialization of Sentry on the server.
|
||||
// The config you add here will be used whenever middleware or an Edge route handles a request.
|
||||
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
|
||||
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
|
||||
const SENTRY_DSN = process.env.NEXT_PUBLIC_SENTRY_DSN;
|
||||
|
||||
Sentry.init({
|
||||
dsn: SENTRY_DSN,
|
||||
environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT || "development",
|
||||
// Adjust this value in production, or use tracesSampler for greater control
|
||||
tracesSampleRate: 1.0,
|
||||
// ...
|
||||
// Note: if you want to override the automatic release value, do not set a
|
||||
// `release` value here - use the environment variable `SENTRY_RELEASE`, so
|
||||
// that it will also get attached to your source maps
|
||||
});
|
17
web/sentry.edge.config.ts
Normal file
17
web/sentry.edge.config.ts
Normal file
@ -0,0 +1,17 @@
|
||||
// This file configures the initialization of Sentry for edge features (middleware, edge routes, and so on).
|
||||
// The config you add here will be used whenever one of the edge features is loaded.
|
||||
// Note that this config is unrelated to the Vercel Edge Runtime and is also required when running locally.
|
||||
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
|
||||
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
|
||||
Sentry.init({
|
||||
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||
environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT || "development",
|
||||
|
||||
// Adjust this value in production, or use tracesSampler for greater control
|
||||
tracesSampleRate: 1,
|
||||
|
||||
// Setting this option to true will print useful information to the console while you're setting up Sentry.
|
||||
debug: false,
|
||||
});
|
@ -4,15 +4,16 @@
|
||||
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
|
||||
const SENTRY_DSN = process.env.NEXT_PUBLIC_SENTRY_DSN;
|
||||
|
||||
Sentry.init({
|
||||
dsn: SENTRY_DSN,
|
||||
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||
environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT || "development",
|
||||
|
||||
// Adjust this value in production, or use tracesSampler for greater control
|
||||
tracesSampleRate: 1.0,
|
||||
// ...
|
||||
// Note: if you want to override the automatic release value, do not set a
|
||||
// `release` value here - use the environment variable `SENTRY_RELEASE`, so
|
||||
// that it will also get attached to your source maps
|
||||
tracesSampleRate: 1,
|
||||
|
||||
// Setting this option to true will print useful information to the console while you're setting up Sentry.
|
||||
debug: false,
|
||||
|
||||
// Uncomment the line below to enable Spotlight (https://spotlightjs.com)
|
||||
// spotlight: process.env.NODE_ENV === 'development',
|
||||
});
|
@ -14,6 +14,7 @@ import { RootStore } from "../root.store";
|
||||
|
||||
export interface IProjectStore {
|
||||
// observables
|
||||
loader: boolean;
|
||||
projectMap: {
|
||||
[projectId: string]: IProject; // projectId: project Info
|
||||
};
|
||||
@ -47,6 +48,7 @@ export interface IProjectStore {
|
||||
|
||||
export class ProjectStore implements IProjectStore {
|
||||
// observables
|
||||
loader: boolean = false;
|
||||
projectMap: {
|
||||
[projectId: string]: IProject; // projectId: project Info
|
||||
} = {};
|
||||
@ -62,6 +64,7 @@ export class ProjectStore implements IProjectStore {
|
||||
constructor(_rootStore: RootStore) {
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
loader: observable.ref,
|
||||
projectMap: observable,
|
||||
// computed
|
||||
filteredProjectIds: computed,
|
||||
@ -208,15 +211,18 @@ export class ProjectStore implements IProjectStore {
|
||||
*/
|
||||
fetchProjects = async (workspaceSlug: string) => {
|
||||
try {
|
||||
this.loader = true;
|
||||
const projectsResponse = await this.projectService.getProjects(workspaceSlug);
|
||||
runInAction(() => {
|
||||
projectsResponse.forEach((project) => {
|
||||
set(this.projectMap, [project.id], project);
|
||||
});
|
||||
this.loader = false;
|
||||
});
|
||||
return projectsResponse;
|
||||
} catch (error) {
|
||||
console.log("Failed to fetch project from workspace store");
|
||||
this.loader = false;
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user