forked from github/plane
Merge pull request #3175 from makeplane/preview
release: moving changes from preview to master
This commit is contained in:
commit
e7468292c7
51
.github/workflows/create-sync-pr.yml
vendored
51
.github/workflows/create-sync-pr.yml
vendored
@ -1,11 +1,13 @@
|
||||
name: Create PR in Plane EE Repository to sync the changes
|
||||
name: Create Sync Action
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- preview
|
||||
types:
|
||||
- closed
|
||||
env:
|
||||
SOURCE_BRANCH_NAME: ${{github.event.pull_request.base.ref}}
|
||||
|
||||
jobs:
|
||||
create_pr:
|
||||
@ -16,27 +18,13 @@ jobs:
|
||||
pull-requests: write
|
||||
contents: read
|
||||
steps:
|
||||
- name: Check SOURCE_REPO
|
||||
id: check_repo
|
||||
env:
|
||||
SOURCE_REPO: ${{ secrets.SOURCE_REPO_NAME }}
|
||||
run: |
|
||||
echo "::set-output name=is_correct_repo::$(if [[ "$SOURCE_REPO" == "makeplane/plane" ]]; then echo 'true'; else echo 'false'; fi)"
|
||||
|
||||
- name: Checkout Code
|
||||
if: steps.check_repo.outputs.is_correct_repo == 'true'
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Branch Name
|
||||
if: steps.check_repo.outputs.is_correct_repo == 'true'
|
||||
run: |
|
||||
echo "SOURCE_BRANCH_NAME=${{ github.head_ref }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup GH CLI
|
||||
if: steps.check_repo.outputs.is_correct_repo == 'true'
|
||||
run: |
|
||||
type -p curl >/dev/null || (sudo apt update && sudo apt install curl -y)
|
||||
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg
|
||||
@ -45,35 +33,14 @@ jobs:
|
||||
sudo apt update
|
||||
sudo apt install gh -y
|
||||
|
||||
- name: Create Pull Request
|
||||
if: steps.check_repo.outputs.is_correct_repo == 'true'
|
||||
- name: Push Changes to Target Repo
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
||||
run: |
|
||||
TARGET_REPO="${{ secrets.TARGET_REPO_NAME }}"
|
||||
TARGET_BRANCH="${{ secrets.TARGET_REPO_BRANCH }}"
|
||||
TARGET_REPO="${{ secrets.SYNC_TARGET_REPO_NAME }}"
|
||||
TARGET_BRANCH="${{ secrets.SYNC_TARGET_BRANCH_NAME }}"
|
||||
SOURCE_BRANCH="${{ env.SOURCE_BRANCH_NAME }}"
|
||||
|
||||
git checkout $SOURCE_BRANCH
|
||||
git remote add target "https://$GH_TOKEN@github.com/$TARGET_REPO.git"
|
||||
git push target $SOURCE_BRANCH:$SOURCE_BRANCH
|
||||
|
||||
PR_TITLE="${{ github.event.pull_request.title }}"
|
||||
PR_BODY="${{ github.event.pull_request.body }}"
|
||||
|
||||
# Remove double quotes
|
||||
PR_TITLE_CLEANED="${PR_TITLE//\"/}"
|
||||
PR_BODY_CLEANED="${PR_BODY//\"/}"
|
||||
|
||||
# Construct PR_BODY_CONTENT using a here-document
|
||||
PR_BODY_CONTENT=$(cat <<EOF
|
||||
$PR_BODY_CLEANED
|
||||
EOF
|
||||
)
|
||||
|
||||
gh pr create \
|
||||
--base $TARGET_BRANCH \
|
||||
--head $SOURCE_BRANCH \
|
||||
--title "[SYNC] $PR_TITLE_CLEANED" \
|
||||
--body "$PR_BODY_CONTENT" \
|
||||
--repo $TARGET_REPO
|
||||
git remote add target-origin "https://$GH_TOKEN@github.com/$TARGET_REPO.git"
|
||||
git push target-origin $SOURCE_BRANCH:$TARGET_BRANCH
|
@ -33,8 +33,8 @@ The backend is a django project which is kept inside apiserver
|
||||
1. Clone the repo
|
||||
|
||||
```bash
|
||||
git clone https://github.com/makeplane/plane
|
||||
cd plane
|
||||
git clone https://github.com/makeplane/plane.git [folder-name]
|
||||
cd [folder-name]
|
||||
chmod +x setup.sh
|
||||
```
|
||||
|
||||
@ -44,33 +44,12 @@ chmod +x setup.sh
|
||||
./setup.sh
|
||||
```
|
||||
|
||||
3. Define `NEXT_PUBLIC_API_BASE_URL=http://localhost` in **web/.env** and **space/.env** file
|
||||
3. Start the containers
|
||||
|
||||
```bash
|
||||
echo "\nNEXT_PUBLIC_API_BASE_URL=http://localhost\n" >> ./web/.env
|
||||
docker compose -f docker-compose-local.yml up
|
||||
```
|
||||
|
||||
```bash
|
||||
echo "\nNEXT_PUBLIC_API_BASE_URL=http://localhost\n" >> ./space/.env
|
||||
```
|
||||
|
||||
4. Run Docker compose up
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
5. Install dependencies
|
||||
|
||||
```bash
|
||||
yarn install
|
||||
```
|
||||
|
||||
6. Run the web app in development mode
|
||||
|
||||
```bash
|
||||
yarn dev
|
||||
```
|
||||
|
||||
## Missing a Feature?
|
||||
|
||||
|
@ -37,7 +37,7 @@ Meet [Plane](https://plane.so). An open-source software development tool to mana
|
||||
|
||||
> Plane is still in its early days, not everything will be perfect yet, and hiccups may happen. Please let us know of any suggestions, ideas, or bugs that you encounter on our [Discord](https://discord.com/invite/A92xrEGCge) or GitHub issues, and we will use your feedback to improve on our upcoming releases.
|
||||
|
||||
The easiest way to get started with Plane is by creating a [Plane Cloud](https://app.plane.so) account. Plane Cloud offers a hosted solution for Plane. If you prefer to self-host Plane, please refer to our [deployment documentation](https://docs.plane.so/self-hosting).
|
||||
The easiest way to get started with Plane is by creating a [Plane Cloud](https://app.plane.so) account. Plane Cloud offers a hosted solution for Plane. If you prefer to self-host Plane, please refer to our [deployment documentation](https://docs.plane.so/self-hosting/docker-compose).
|
||||
|
||||
## ⚡️ Contributors Quick Start
|
||||
|
||||
@ -63,7 +63,7 @@ Thats it!
|
||||
|
||||
## 🍙 Self Hosting
|
||||
|
||||
For self hosting environment setup, visit the [Self Hosting](https://docs.plane.so/self-hosting) documentation page
|
||||
For self hosting environment setup, visit the [Self Hosting](https://docs.plane.so/self-hosting/docker-compose) documentation page
|
||||
|
||||
## 🚀 Features
|
||||
|
||||
|
@ -49,5 +49,5 @@ USER captain
|
||||
# Expose container port and run entry point script
|
||||
EXPOSE 8000
|
||||
|
||||
# CMD [ "./bin/takeoff" ]
|
||||
CMD [ "./bin/takeoff.local" ]
|
||||
|
||||
|
31
apiserver/bin/takeoff.local
Executable file
31
apiserver/bin/takeoff.local
Executable file
@ -0,0 +1,31 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
python manage.py wait_for_db
|
||||
python manage.py migrate
|
||||
|
||||
# Create the default bucket
|
||||
#!/bin/bash
|
||||
|
||||
# Collect system information
|
||||
HOSTNAME=$(hostname)
|
||||
MAC_ADDRESS=$(ip link show | awk '/ether/ {print $2}' | head -n 1)
|
||||
CPU_INFO=$(cat /proc/cpuinfo)
|
||||
MEMORY_INFO=$(free -h)
|
||||
DISK_INFO=$(df -h)
|
||||
|
||||
# Concatenate information and compute SHA-256 hash
|
||||
SIGNATURE=$(echo "$HOSTNAME$MAC_ADDRESS$CPU_INFO$MEMORY_INFO$DISK_INFO" | sha256sum | awk '{print $1}')
|
||||
|
||||
# Export the variables
|
||||
export MACHINE_SIGNATURE=$SIGNATURE
|
||||
|
||||
# Register instance
|
||||
python manage.py register_instance $MACHINE_SIGNATURE
|
||||
# Load the configuration variable
|
||||
python manage.py configure_instance
|
||||
|
||||
# Create the default bucket
|
||||
python manage.py create_bucket
|
||||
|
||||
python manage.py runserver 0.0.0.0:8000 --settings=plane.settings.local
|
||||
|
@ -145,6 +145,16 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
|
||||
)
|
||||
)
|
||||
)
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"project_projectmember",
|
||||
queryset=ProjectMember.objects.filter(
|
||||
workspace__slug=self.kwargs.get("slug"),
|
||||
is_active=True,
|
||||
).select_related("member"),
|
||||
to_attr="members_list",
|
||||
)
|
||||
)
|
||||
.distinct()
|
||||
)
|
||||
|
||||
@ -160,16 +170,6 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
|
||||
projects = (
|
||||
self.get_queryset()
|
||||
.annotate(sort_order=Subquery(sort_order_query))
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"project_projectmember",
|
||||
queryset=ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
is_active=True,
|
||||
).select_related("member"),
|
||||
to_attr="members_list",
|
||||
)
|
||||
)
|
||||
.order_by("sort_order", "name")
|
||||
)
|
||||
if request.GET.get("per_page", False) and request.GET.get("cursor", False):
|
||||
@ -679,6 +679,25 @@ class ProjectMemberViewSet(BaseViewSet):
|
||||
)
|
||||
)
|
||||
|
||||
# Check if the user is already a member of the project and is inactive
|
||||
if ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
member_id=member.get("member_id"),
|
||||
is_active=False,
|
||||
).exists():
|
||||
member_detail = ProjectMember.objects.get(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
member_id=member.get("member_id"),
|
||||
is_active=False,
|
||||
)
|
||||
# Check if the user has not deactivated the account
|
||||
user = User.objects.filter(pk=member.get("member_id")).first()
|
||||
if user.is_active:
|
||||
member_detail.is_active = True
|
||||
member_detail.save(update_fields=["is_active"])
|
||||
|
||||
project_members = ProjectMember.objects.bulk_create(
|
||||
bulk_project_members,
|
||||
batch_size=10,
|
||||
@ -991,11 +1010,18 @@ class ProjectPublicCoverImagesEndpoint(BaseAPIView):
|
||||
|
||||
def get(self, request):
|
||||
files = []
|
||||
s3 = boto3.client(
|
||||
"s3",
|
||||
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
|
||||
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
|
||||
)
|
||||
s3_client_params = {
|
||||
"service_name": "s3",
|
||||
"aws_access_key_id": settings.AWS_ACCESS_KEY_ID,
|
||||
"aws_secret_access_key": settings.AWS_SECRET_ACCESS_KEY,
|
||||
}
|
||||
|
||||
# Use AWS_S3_ENDPOINT_URL if it is present in the settings
|
||||
if hasattr(settings, "AWS_S3_ENDPOINT_URL") and settings.AWS_S3_ENDPOINT_URL:
|
||||
s3_client_params["endpoint_url"] = settings.AWS_S3_ENDPOINT_URL
|
||||
|
||||
s3 = boto3.client(**s3_client_params)
|
||||
|
||||
params = {
|
||||
"Bucket": settings.AWS_STORAGE_BUCKET_NAME,
|
||||
"Prefix": "static/project-cover/",
|
||||
@ -1008,9 +1034,19 @@ class ProjectPublicCoverImagesEndpoint(BaseAPIView):
|
||||
if not content["Key"].endswith(
|
||||
"/"
|
||||
): # This line ensures we're only getting files, not "sub-folders"
|
||||
files.append(
|
||||
f"https://{settings.AWS_STORAGE_BUCKET_NAME}.s3.{settings.AWS_REGION}.amazonaws.com/{content['Key']}"
|
||||
)
|
||||
if (
|
||||
hasattr(settings, "AWS_S3_CUSTOM_DOMAIN")
|
||||
and settings.AWS_S3_CUSTOM_DOMAIN
|
||||
and hasattr(settings, "AWS_S3_URL_PROTOCOL")
|
||||
and settings.AWS_S3_URL_PROTOCOL
|
||||
):
|
||||
files.append(
|
||||
f"{settings.AWS_S3_URL_PROTOCOL}//{settings.AWS_S3_CUSTOM_DOMAIN}/{content['Key']}"
|
||||
)
|
||||
else:
|
||||
files.append(
|
||||
f"https://{settings.AWS_STORAGE_BUCKET_NAME}.s3.{settings.AWS_REGION}.amazonaws.com/{content['Key']}"
|
||||
)
|
||||
|
||||
return Response(files, status=status.HTTP_200_OK)
|
||||
|
||||
|
@ -70,6 +70,7 @@ from plane.app.permissions import (
|
||||
WorkSpaceAdminPermission,
|
||||
WorkspaceEntityPermission,
|
||||
WorkspaceViewerPermission,
|
||||
WorkspaceUserPermission,
|
||||
)
|
||||
from plane.bgtasks.workspace_invitation_task import workspace_invitation
|
||||
from plane.utils.issue_filters import issue_filters
|
||||
@ -495,6 +496,18 @@ class WorkSpaceMemberViewSet(BaseViewSet):
|
||||
WorkspaceEntityPermission,
|
||||
]
|
||||
|
||||
def get_permissions(self):
|
||||
if self.action == "leave":
|
||||
self.permission_classes = [
|
||||
WorkspaceUserPermission,
|
||||
]
|
||||
else:
|
||||
self.permission_classes = [
|
||||
WorkspaceEntityPermission,
|
||||
]
|
||||
|
||||
return super(WorkSpaceMemberViewSet, self).get_permissions()
|
||||
|
||||
search_fields = [
|
||||
"member__display_name",
|
||||
"member__first_name",
|
||||
|
@ -65,7 +65,7 @@ def send_export_email(email, slug, csv_buffer, rows):
|
||||
port=int(EMAIL_PORT),
|
||||
username=EMAIL_HOST_USER,
|
||||
password=EMAIL_HOST_PASSWORD,
|
||||
use_tls=bool(EMAIL_USE_TLS),
|
||||
use_tls=EMAIL_USE_TLS == "1",
|
||||
)
|
||||
|
||||
msg = EmailMultiAlternatives(
|
||||
|
@ -51,7 +51,7 @@ def forgot_password(first_name, email, uidb64, token, current_site):
|
||||
port=int(EMAIL_PORT),
|
||||
username=EMAIL_HOST_USER,
|
||||
password=EMAIL_HOST_PASSWORD,
|
||||
use_tls=bool(EMAIL_USE_TLS),
|
||||
use_tls=EMAIL_USE_TLS == "1",
|
||||
)
|
||||
|
||||
msg = EmailMultiAlternatives(
|
||||
|
@ -41,7 +41,7 @@ def magic_link(email, key, token, current_site):
|
||||
port=int(EMAIL_PORT),
|
||||
username=EMAIL_HOST_USER,
|
||||
password=EMAIL_HOST_PASSWORD,
|
||||
use_tls=bool(EMAIL_USE_TLS),
|
||||
use_tls=EMAIL_USE_TLS == "1",
|
||||
)
|
||||
|
||||
msg = EmailMultiAlternatives(
|
||||
|
@ -60,7 +60,7 @@ def project_invitation(email, project_id, token, current_site, invitor):
|
||||
port=int(EMAIL_PORT),
|
||||
username=EMAIL_HOST_USER,
|
||||
password=EMAIL_HOST_PASSWORD,
|
||||
use_tls=bool(EMAIL_USE_TLS),
|
||||
use_tls=EMAIL_USE_TLS == "1",
|
||||
)
|
||||
|
||||
msg = EmailMultiAlternatives(
|
||||
|
@ -70,7 +70,7 @@ def workspace_invitation(email, workspace_id, token, current_site, invitor):
|
||||
port=int(EMAIL_PORT),
|
||||
username=EMAIL_HOST_USER,
|
||||
password=EMAIL_HOST_PASSWORD,
|
||||
use_tls=bool(EMAIL_USE_TLS),
|
||||
use_tls=EMAIL_USE_TLS == "1",
|
||||
)
|
||||
|
||||
msg = EmailMultiAlternatives(
|
||||
|
@ -30,7 +30,7 @@ openpyxl==3.1.2
|
||||
beautifulsoup4==4.12.2
|
||||
dj-database-url==2.1.0
|
||||
posthog==3.0.2
|
||||
cryptography==41.0.5
|
||||
cryptography==41.0.6
|
||||
lxml==4.9.3
|
||||
boto3==1.28.40
|
||||
|
||||
|
@ -39,7 +39,7 @@ function download(){
|
||||
echo ""
|
||||
echo "Latest version is now available for you to use"
|
||||
echo ""
|
||||
echo "In case of Upgrade, your new setting file is availabe as 'variables-upgrade.env'. Please compare and set the required values in '.env 'file."
|
||||
echo "In case of Upgrade, your new setting file is available as 'variables-upgrade.env'. Please compare and set the required values in '.env 'file."
|
||||
echo ""
|
||||
|
||||
}
|
||||
|
@ -12,7 +12,6 @@ volumes:
|
||||
|
||||
services:
|
||||
plane-redis:
|
||||
container_name: plane-redis
|
||||
image: redis:6.2.7-alpine
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
@ -21,7 +20,6 @@ services:
|
||||
- redisdata:/data
|
||||
|
||||
plane-minio:
|
||||
container_name: plane-minio
|
||||
image: minio/minio
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
@ -36,7 +34,6 @@ services:
|
||||
MINIO_ROOT_PASSWORD: ${AWS_SECRET_ACCESS_KEY}
|
||||
|
||||
plane-db:
|
||||
container_name: plane-db
|
||||
image: postgres:15.2-alpine
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
@ -53,7 +50,6 @@ services:
|
||||
PGDATA: /var/lib/postgresql/data
|
||||
|
||||
web:
|
||||
container_name: web
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./web/Dockerfile.dev
|
||||
@ -61,8 +57,7 @@ services:
|
||||
networks:
|
||||
- dev_env
|
||||
volumes:
|
||||
- .:/app
|
||||
command: yarn dev --filter=web
|
||||
- ./web:/app/web
|
||||
env_file:
|
||||
- ./web/.env
|
||||
depends_on:
|
||||
@ -73,22 +68,17 @@ services:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./space/Dockerfile.dev
|
||||
container_name: space
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- dev_env
|
||||
volumes:
|
||||
- .:/app
|
||||
command: yarn dev --filter=space
|
||||
env_file:
|
||||
- ./space/.env
|
||||
- ./space:/app/space
|
||||
depends_on:
|
||||
- api
|
||||
- worker
|
||||
- web
|
||||
|
||||
api:
|
||||
container_name: api
|
||||
build:
|
||||
context: ./apiserver
|
||||
dockerfile: Dockerfile.dev
|
||||
@ -99,7 +89,7 @@ services:
|
||||
- dev_env
|
||||
volumes:
|
||||
- ./apiserver:/code
|
||||
command: /bin/sh -c "python manage.py migrate && python manage.py runserver 0.0.0.0:8000 --settings=plane.settings.local"
|
||||
# command: /bin/sh -c "python manage.py migrate && python manage.py runserver 0.0.0.0:8000 --settings=plane.settings.local"
|
||||
env_file:
|
||||
- ./apiserver/.env
|
||||
depends_on:
|
||||
@ -107,7 +97,6 @@ services:
|
||||
- plane-redis
|
||||
|
||||
worker:
|
||||
container_name: bgworker
|
||||
build:
|
||||
context: ./apiserver
|
||||
dockerfile: Dockerfile.dev
|
||||
@ -127,7 +116,6 @@ services:
|
||||
- plane-redis
|
||||
|
||||
beat-worker:
|
||||
container_name: beatworker
|
||||
build:
|
||||
context: ./apiserver
|
||||
dockerfile: Dockerfile.dev
|
||||
@ -147,10 +135,9 @@ services:
|
||||
- plane-redis
|
||||
|
||||
proxy:
|
||||
container_name: proxy
|
||||
build:
|
||||
context: ./nginx
|
||||
dockerfile: Dockerfile
|
||||
dockerfile: Dockerfile.dev
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- dev_env
|
||||
|
10
nginx/Dockerfile.dev
Normal file
10
nginx/Dockerfile.dev
Normal file
@ -0,0 +1,10 @@
|
||||
FROM nginx:1.25.0-alpine
|
||||
|
||||
RUN rm /etc/nginx/conf.d/default.conf
|
||||
COPY nginx.conf.dev /etc/nginx/nginx.conf.template
|
||||
|
||||
COPY ./env.sh /docker-entrypoint.sh
|
||||
|
||||
RUN chmod +x /docker-entrypoint.sh
|
||||
# Update all environment variables
|
||||
CMD ["/docker-entrypoint.sh"]
|
@ -18,7 +18,7 @@ server {
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
location /space/ {
|
||||
location /spaces/ {
|
||||
proxy_pass http://localhost:4000/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
|
36
nginx/nginx.conf.dev
Normal file
36
nginx/nginx.conf.dev
Normal file
@ -0,0 +1,36 @@
|
||||
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;
|
||||
|
||||
location / {
|
||||
proxy_pass http://web:3000/;
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://api:8000/api/;
|
||||
}
|
||||
|
||||
location /spaces/ {
|
||||
rewrite ^/spaces/?$ /spaces/login break;
|
||||
proxy_pass http://space:4000/spaces/;
|
||||
}
|
||||
|
||||
location /${BUCKET_NAME}/ {
|
||||
proxy_pass http://plane-minio:9000/uploads/;
|
||||
}
|
||||
}
|
||||
}
|
1
setup.sh
1
setup.sh
@ -6,7 +6,6 @@ export LC_ALL=C
|
||||
export LC_CTYPE=C
|
||||
|
||||
cp ./web/.env.example ./web/.env
|
||||
cp ./space/.env.example ./space/.env
|
||||
cp ./apiserver/.env.example ./apiserver/.env
|
||||
|
||||
# Generate the SECRET_KEY that will be used by django
|
||||
|
@ -7,5 +7,8 @@ WORKDIR /app
|
||||
COPY . .
|
||||
RUN yarn global add turbo
|
||||
RUN yarn install
|
||||
EXPOSE 3000
|
||||
EXPOSE 4000
|
||||
ENV NEXT_PUBLIC_DEPLOY_WITH_NGINX=1
|
||||
|
||||
VOLUME [ "/app/node_modules", "/app/space/node_modules"]
|
||||
CMD ["yarn","dev", "--filter=space"]
|
||||
|
@ -8,4 +8,5 @@ COPY . .
|
||||
RUN yarn global add turbo
|
||||
RUN yarn install
|
||||
EXPOSE 3000
|
||||
VOLUME [ "/app/node_modules", "/app/web/node_modules" ]
|
||||
CMD ["yarn", "dev", "--filter=web"]
|
||||
|
@ -19,7 +19,7 @@ type Props = {
|
||||
icon?: any;
|
||||
text: string;
|
||||
onClick: () => void;
|
||||
};
|
||||
} | null;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
|
@ -1,14 +1,14 @@
|
||||
import React, { useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import useSWR from "swr";
|
||||
// react hook form
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { SubmitHandler, useForm } from "react-hook-form";
|
||||
// headless ui
|
||||
import { Combobox, Dialog, Transition } from "@headlessui/react";
|
||||
import useSWR from "swr";
|
||||
// hooks
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import useToast from "hooks/use-toast";
|
||||
// services
|
||||
import { IssueService } from "services/issue";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// ui
|
||||
import { Button, LayersIcon } from "@plane/ui";
|
||||
// icons
|
||||
@ -30,17 +30,25 @@ type Props = {
|
||||
|
||||
const issueService = new IssueService();
|
||||
|
||||
export const BulkDeleteIssuesModal: React.FC<Props> = (props) => {
|
||||
export const BulkDeleteIssuesModal: React.FC<Props> = observer((props) => {
|
||||
const { isOpen, onClose } = props;
|
||||
// states
|
||||
const [query, setQuery] = useState("");
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
// states
|
||||
const [query, setQuery] = useState("");
|
||||
// store hooks
|
||||
const {
|
||||
user: { hasPermissionToCurrentProject },
|
||||
} = useMobxStore();
|
||||
// fetching project issues.
|
||||
const { data: issues } = useSWR(
|
||||
workspaceSlug && projectId ? PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string) : null,
|
||||
workspaceSlug && projectId ? () => issueService.getIssues(workspaceSlug as string, projectId as string) : null
|
||||
workspaceSlug && projectId && hasPermissionToCurrentProject
|
||||
? PROJECT_ISSUES_LIST(workspaceSlug.toString(), projectId.toString())
|
||||
: null,
|
||||
workspaceSlug && projectId && hasPermissionToCurrentProject
|
||||
? () => issueService.getIssues(workspaceSlug.toString(), projectId.toString())
|
||||
: null
|
||||
);
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
@ -222,4 +230,4 @@ export const BulkDeleteIssuesModal: React.FC<Props> = (props) => {
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -118,6 +118,7 @@ export const LinkModal: FC<Props> = (props) => {
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.url)}
|
||||
placeholder="https://..."
|
||||
pattern="^(https?://).*"
|
||||
className="w-full"
|
||||
/>
|
||||
)}
|
||||
|
@ -50,8 +50,8 @@ export const LinksList: React.FC<Props> = ({ links, handleDeleteLink, handleEdit
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
{!isNotAllowed && (
|
||||
<div className="z-[1] flex flex-shrink-0 items-center gap-2">
|
||||
<div className="z-[1] flex flex-shrink-0 items-center gap-2">
|
||||
{!isNotAllowed && (
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center justify-center p-1 hover:bg-custom-background-80"
|
||||
@ -63,14 +63,16 @@ export const LinksList: React.FC<Props> = ({ links, handleDeleteLink, handleEdit
|
||||
>
|
||||
<Pencil className="h-3 w-3 stroke-[1.5] text-custom-text-200" />
|
||||
</button>
|
||||
<a
|
||||
href={link.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center p-1 hover:bg-custom-background-80"
|
||||
>
|
||||
<ExternalLinkIcon className="h-3 w-3 stroke-[1.5] text-custom-text-200" />
|
||||
</a>
|
||||
)}
|
||||
<a
|
||||
href={link.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center p-1 hover:bg-custom-background-80"
|
||||
>
|
||||
<ExternalLinkIcon className="h-3 w-3 stroke-[1.5] text-custom-text-200" />
|
||||
</a>
|
||||
{!isNotAllowed && (
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center justify-center p-1 hover:bg-custom-background-80"
|
||||
@ -82,8 +84,8 @@ export const LinksList: React.FC<Props> = ({ links, handleDeleteLink, handleEdit
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-5">
|
||||
<p className="mt-0.5 stroke-[1.5] text-xs text-custom-text-300">
|
||||
|
@ -14,6 +14,7 @@ import { SingleProgressStats } from "components/core";
|
||||
import { Avatar, StateGroupIcon } from "@plane/ui";
|
||||
// types
|
||||
import {
|
||||
IIssueFilterOptions,
|
||||
IModule,
|
||||
TAssigneesDistribution,
|
||||
TCompletionChartDistribution,
|
||||
@ -35,6 +36,9 @@ type Props = {
|
||||
roundedTab?: boolean;
|
||||
noBackground?: boolean;
|
||||
isPeekView?: boolean;
|
||||
isCompleted?: boolean;
|
||||
filters?: IIssueFilterOptions;
|
||||
handleFiltersUpdate: (key: keyof IIssueFilterOptions, value: string | string[]) => void;
|
||||
};
|
||||
|
||||
export const SidebarProgressStats: React.FC<Props> = ({
|
||||
@ -44,7 +48,10 @@ export const SidebarProgressStats: React.FC<Props> = ({
|
||||
module,
|
||||
roundedTab,
|
||||
noBackground,
|
||||
isCompleted = false,
|
||||
isPeekView = false,
|
||||
filters,
|
||||
handleFiltersUpdate,
|
||||
}) => {
|
||||
const { storedValue: tab, setValue: setTab } = useLocalStorage("tab", "Assignees");
|
||||
|
||||
@ -140,20 +147,11 @@ export const SidebarProgressStats: React.FC<Props> = ({
|
||||
}
|
||||
completed={assignee.completed_issues}
|
||||
total={assignee.total_issues}
|
||||
{...(!isPeekView && {
|
||||
onClick: () => {
|
||||
// TODO: set filters here
|
||||
// if (filters?.assignees?.includes(assignee.assignee_id ?? ""))
|
||||
// setFilters({
|
||||
// assignees: filters?.assignees?.filter((a) => a !== assignee.assignee_id),
|
||||
// });
|
||||
// else
|
||||
// setFilters({
|
||||
// assignees: [...(filters?.assignees ?? []), assignee.assignee_id ?? ""],
|
||||
// });
|
||||
},
|
||||
// selected: filters?.assignees?.includes(assignee.assignee_id ?? ""),
|
||||
})}
|
||||
{...(!isPeekView &&
|
||||
!isCompleted && {
|
||||
onClick: () => handleFiltersUpdate("assignees", assignee.assignee_id ?? ""),
|
||||
selected: filters?.assignees?.includes(assignee.assignee_id ?? ""),
|
||||
})}
|
||||
/>
|
||||
);
|
||||
else
|
||||
@ -200,17 +198,11 @@ export const SidebarProgressStats: React.FC<Props> = ({
|
||||
}
|
||||
completed={label.completed_issues}
|
||||
total={label.total_issues}
|
||||
{...(!isPeekView && {
|
||||
// TODO: set filters here
|
||||
onClick: () => {
|
||||
// if (filters.labels?.includes(label.label_id ?? ""))
|
||||
// setFilters({
|
||||
// labels: filters?.labels?.filter((l) => l !== label.label_id),
|
||||
// });
|
||||
// else setFilters({ labels: [...(filters?.labels ?? []), label.label_id ?? ""] });
|
||||
},
|
||||
// selected: filters?.labels?.includes(label.label_id ?? ""),
|
||||
})}
|
||||
{...(!isPeekView &&
|
||||
!isCompleted && {
|
||||
onClick: () => handleFiltersUpdate("labels", label.label_id ?? ""),
|
||||
selected: filters?.labels?.includes(label.label_id ?? `no-label-${index}`),
|
||||
})}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
|
@ -75,7 +75,7 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
useSWR(
|
||||
const { isLoading } = useSWR(
|
||||
workspaceSlug && projectId ? `ACTIVE_CYCLE_ISSUE_${projectId}_CURRENT` : null,
|
||||
workspaceSlug && projectId ? () => cycleStore.fetchCycles(workspaceSlug, projectId, "current") : null
|
||||
);
|
||||
@ -94,7 +94,7 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
|
||||
// : null
|
||||
// ) as { data: IIssue[] | undefined };
|
||||
|
||||
if (!cycle)
|
||||
if (!cycle && isLoading)
|
||||
return (
|
||||
<Loader>
|
||||
<Loader.Item height="250px" />
|
||||
@ -187,12 +187,12 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
|
||||
cycleStatus === "current"
|
||||
? "#09A953"
|
||||
: cycleStatus === "upcoming"
|
||||
? "#F7AE59"
|
||||
: cycleStatus === "completed"
|
||||
? "#3F76FF"
|
||||
: cycleStatus === "draft"
|
||||
? "rgb(var(--color-text-200))"
|
||||
: ""
|
||||
? "#F7AE59"
|
||||
: cycleStatus === "completed"
|
||||
? "#3F76FF"
|
||||
: cycleStatus === "draft"
|
||||
? "rgb(var(--color-text-200))"
|
||||
: ""
|
||||
}`}
|
||||
/>
|
||||
</span>
|
||||
@ -207,12 +207,12 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
|
||||
cycleStatus === "current"
|
||||
? "bg-green-600/5 text-green-600"
|
||||
: cycleStatus === "upcoming"
|
||||
? "bg-orange-300/5 text-orange-300"
|
||||
: cycleStatus === "completed"
|
||||
? "bg-blue-500/5 text-blue-500"
|
||||
: cycleStatus === "draft"
|
||||
? "bg-neutral-400/5 text-neutral-400"
|
||||
: ""
|
||||
? "bg-orange-300/5 text-orange-300"
|
||||
: cycleStatus === "completed"
|
||||
? "bg-blue-500/5 text-blue-500"
|
||||
: cycleStatus === "draft"
|
||||
? "bg-neutral-400/5 text-neutral-400"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{cycleStatus === "current" ? (
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useForm } from "react-hook-form";
|
||||
@ -29,7 +29,10 @@ import {
|
||||
renderShortMonthDate,
|
||||
} from "helpers/date-time.helper";
|
||||
// types
|
||||
import { ICycle } from "types";
|
||||
import { ICycle, IIssueFilterOptions } from "types";
|
||||
import { EFilterType } from "store/issues/types";
|
||||
// constants
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
// fetch-keys
|
||||
import { CYCLE_STATUS } from "constants/cycle";
|
||||
|
||||
@ -52,7 +55,9 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
|
||||
const {
|
||||
cycle: cycleDetailsStore,
|
||||
cycleIssuesFilter: { issueFilters, updateFilters },
|
||||
trackEvent: { setTrackElement },
|
||||
user: { currentProjectRole },
|
||||
} = useMobxStore();
|
||||
|
||||
const cycleDetails = cycleDetailsStore.cycle_details[cycleId] ?? undefined;
|
||||
@ -242,6 +247,25 @@ 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] ?? [];
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((val) => {
|
||||
if (!newValues.includes(val)) newValues.push(val);
|
||||
});
|
||||
} else {
|
||||
if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
|
||||
else newValues.push(value);
|
||||
}
|
||||
|
||||
updateFilters(workspaceSlug.toString(), projectId.toString(), EFilterType.FILTERS, { [key]: newValues }, cycleId);
|
||||
},
|
||||
[workspaceSlug, projectId, cycleId, issueFilters, updateFilters]
|
||||
);
|
||||
|
||||
const cycleStatus =
|
||||
cycleDetails?.start_date && cycleDetails?.end_date
|
||||
? getDateRangeStatus(cycleDetails?.start_date, cycleDetails?.end_date)
|
||||
@ -270,10 +294,11 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
</Loader>
|
||||
);
|
||||
|
||||
const endDate = new Date(cycleDetails.end_date ?? "");
|
||||
const startDate = new Date(cycleDetails.start_date ?? "");
|
||||
const endDate = new Date(watch("end_date") ?? cycleDetails.end_date ?? "");
|
||||
const startDate = new Date(watch("start_date") ?? cycleDetails.start_date ?? "");
|
||||
|
||||
const areYearsEqual = startDate.getFullYear() === endDate.getFullYear();
|
||||
const areYearsEqual =
|
||||
startDate.getFullYear() === endDate.getFullYear() || isNaN(startDate.getFullYear()) || isNaN(endDate.getFullYear());
|
||||
|
||||
const currentCycle = CYCLE_STATUS.find((status) => status.value === cycleStatus);
|
||||
|
||||
@ -286,6 +311,8 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
: `${cycleDetails.total_issues}`
|
||||
: `${cycleDetails.completed_issues}/${cycleDetails.total_issues}`;
|
||||
|
||||
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
|
||||
|
||||
return (
|
||||
<>
|
||||
{cycleDetails && workspaceSlug && projectId && (
|
||||
@ -312,7 +339,7 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
<button onClick={handleCopyText}>
|
||||
<LinkIcon className="h-3 w-3 text-custom-text-300" />
|
||||
</button>
|
||||
{!isCompleted && (
|
||||
{!isCompleted && isEditingAllowed && (
|
||||
<CustomMenu width="lg" placement="bottom-end" ellipsis>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
@ -349,8 +376,10 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
<div className="relative flex h-full w-52 items-center gap-2.5">
|
||||
<Popover className="flex h-full items-center justify-center rounded-lg">
|
||||
<Popover.Button
|
||||
disabled={isCompleted ?? false}
|
||||
className="cursor-default text-sm font-medium text-custom-text-300"
|
||||
className={`text-sm font-medium text-custom-text-300 ${
|
||||
isEditingAllowed ? "cursor-pointer" : "cursor-not-allowed"
|
||||
}`}
|
||||
disabled={isCompleted || !isEditingAllowed}
|
||||
>
|
||||
{areYearsEqual ? renderShortDate(startDate, "_ _") : renderShortMonthDate(startDate, "_ _")}
|
||||
</Popover.Button>
|
||||
@ -373,10 +402,10 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
handleStartDateChange(val);
|
||||
}
|
||||
}}
|
||||
startDate={watch("start_date") ? `${watch("start_date")}` : null}
|
||||
endDate={watch("end_date") ? `${watch("end_date")}` : null}
|
||||
startDate={watch("start_date") ?? watch("end_date") ?? null}
|
||||
endDate={watch("end_date") ?? watch("start_date") ?? null}
|
||||
maxDate={new Date(`${watch("end_date")}`)}
|
||||
selectsStart
|
||||
selectsStart={watch("end_date") ? true : false}
|
||||
/>
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
@ -385,8 +414,10 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
<Popover className="flex h-full items-center justify-center rounded-lg">
|
||||
<>
|
||||
<Popover.Button
|
||||
disabled={isCompleted ?? false}
|
||||
className="cursor-default text-sm font-medium text-custom-text-300"
|
||||
className={`text-sm font-medium text-custom-text-300 ${
|
||||
isEditingAllowed ? "cursor-pointer" : "cursor-not-allowed"
|
||||
}`}
|
||||
disabled={isCompleted || !isEditingAllowed}
|
||||
>
|
||||
{areYearsEqual ? renderShortDate(endDate, "_ _") : renderShortMonthDate(endDate, "_ _")}
|
||||
</Popover.Button>
|
||||
@ -409,10 +440,10 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
handleEndDateChange(val);
|
||||
}
|
||||
}}
|
||||
startDate={watch("start_date") ? `${watch("start_date")}` : null}
|
||||
endDate={watch("end_date") ? `${watch("end_date")}` : null}
|
||||
startDate={watch("start_date") ?? watch("end_date") ?? null}
|
||||
endDate={watch("end_date") ?? watch("start_date") ?? null}
|
||||
minDate={new Date(`${watch("start_date")}`)}
|
||||
selectsEnd
|
||||
selectsEnd={watch("start_date") ? true : false}
|
||||
/>
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
@ -528,6 +559,9 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
}}
|
||||
totalIssues={cycleDetails.total_issues}
|
||||
isPeekView={Boolean(peekCycle)}
|
||||
isCompleted={isCompleted}
|
||||
filters={issueFilters?.filters}
|
||||
handleFiltersUpdate={handleFiltersUpdate}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
@ -15,7 +15,6 @@ import { IGanttBlock, IBlockUpdateData } from "components/gantt-chart/types";
|
||||
import { IIssue } from "types";
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
|
||||
blocks: IGanttBlock[] | null;
|
||||
enableReorder: boolean;
|
||||
@ -33,7 +32,6 @@ type Props = {
|
||||
export const IssueGanttSidebar: React.FC<Props> = (props) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const {
|
||||
title,
|
||||
blockUpdateHandler,
|
||||
blocks,
|
||||
enableReorder,
|
||||
|
@ -155,7 +155,10 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
||||
key={cycle.id}
|
||||
onClick={() => router.push(`/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`)}
|
||||
>
|
||||
{truncateText(cycle.name, 40)}
|
||||
<div className="flex items-center gap-1.5">
|
||||
<ContrastIcon className="h-3 w-3" />
|
||||
{truncateText(cycle.name, 40)}
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
))}
|
||||
</CustomMenu>
|
||||
@ -192,20 +195,23 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
||||
handleDisplayPropertiesUpdate={handleDisplayProperties}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
<Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
|
||||
Analytics
|
||||
</Button>
|
||||
|
||||
{canUserCreateIssue && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
setTrackElement("CYCLE_PAGE_HEADER");
|
||||
commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.CYCLE);
|
||||
}}
|
||||
size="sm"
|
||||
prependIcon={<Plus />}
|
||||
>
|
||||
Add Issue
|
||||
</Button>
|
||||
<>
|
||||
<Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
|
||||
Analytics
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setTrackElement("CYCLE_PAGE_HEADER");
|
||||
commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.CYCLE);
|
||||
}}
|
||||
size="sm"
|
||||
prependIcon={<Plus />}
|
||||
>
|
||||
Add Issue
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
|
@ -16,6 +16,7 @@ import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOption
|
||||
// constants
|
||||
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
|
||||
import { EFilterType } from "store/issues/types";
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
|
||||
const GLOBAL_VIEW_LAYOUTS = [
|
||||
{ key: "list", title: "List", link: "/workspace-views", icon: List },
|
||||
@ -38,7 +39,7 @@ export const GlobalIssuesHeader: React.FC<Props> = observer((props) => {
|
||||
workspace: { workspaceLabels },
|
||||
workspaceMember: { workspaceMembers },
|
||||
project: { workspaceProjects },
|
||||
|
||||
user: { currentWorkspaceRole },
|
||||
workspaceGlobalIssuesFilter: { issueFilters, updateFilters },
|
||||
} = useMobxStore();
|
||||
|
||||
@ -77,6 +78,8 @@ export const GlobalIssuesHeader: React.FC<Props> = observer((props) => {
|
||||
[workspaceSlug, updateFilters]
|
||||
);
|
||||
|
||||
const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
|
||||
|
||||
return (
|
||||
<>
|
||||
<CreateUpdateWorkspaceViewModal isOpen={createViewModal} onClose={() => setCreateViewModal(false)} />
|
||||
@ -142,10 +145,11 @@ export const GlobalIssuesHeader: React.FC<Props> = observer((props) => {
|
||||
</FiltersDropdown>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Button variant="primary" size="sm" prependIcon={<PlusIcon />} onClick={() => setCreateViewModal(true)}>
|
||||
New View
|
||||
</Button>
|
||||
{isAuthorizedUser && (
|
||||
<Button variant="primary" size="sm" prependIcon={<PlusIcon />} onClick={() => setCreateViewModal(true)}>
|
||||
New View
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
@ -11,7 +11,7 @@ import { ProjectAnalyticsModal } from "components/analytics";
|
||||
// ui
|
||||
import { Breadcrumbs, Button, CustomMenu, DiceIcon } from "@plane/ui";
|
||||
// icons
|
||||
import { ArrowRight, ContrastIcon, Plus } from "lucide-react";
|
||||
import { ArrowRight, Plus } from "lucide-react";
|
||||
// helpers
|
||||
import { truncateText } from "helpers/string.helper";
|
||||
import { renderEmoji } from "helpers/emoji.helper";
|
||||
@ -143,7 +143,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
||||
<CustomMenu
|
||||
label={
|
||||
<>
|
||||
<ContrastIcon className="h-3 w-3" />
|
||||
<DiceIcon className="h-3 w-3" />
|
||||
{moduleDetails?.name && truncateText(moduleDetails.name, 40)}
|
||||
</>
|
||||
}
|
||||
@ -156,7 +156,10 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
||||
key={module.id}
|
||||
onClick={() => router.push(`/${workspaceSlug}/projects/${projectId}/modules/${module.id}`)}
|
||||
>
|
||||
{truncateText(module.name, 40)}
|
||||
<div className="flex items-center gap-1.5">
|
||||
<DiceIcon className="h-3 w-3" />
|
||||
{truncateText(module.name, 40)}
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
))}
|
||||
</CustomMenu>
|
||||
@ -193,20 +196,23 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
||||
handleDisplayPropertiesUpdate={handleDisplayProperties}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
<Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
|
||||
Analytics
|
||||
</Button>
|
||||
|
||||
{canUserCreateIssue && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
setTrackElement("MODULE_PAGE_HEADER");
|
||||
commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.MODULE);
|
||||
}}
|
||||
size="sm"
|
||||
prependIcon={<Plus />}
|
||||
>
|
||||
Add Issue
|
||||
</Button>
|
||||
<>
|
||||
<Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
|
||||
Analytics
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setTrackElement("MODULE_PAGE_HEADER");
|
||||
commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.MODULE);
|
||||
}}
|
||||
size="sm"
|
||||
prependIcon={<Plus />}
|
||||
>
|
||||
Add Issue
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
|
@ -202,20 +202,23 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
|
||||
</span>
|
||||
</Link>
|
||||
)}
|
||||
<Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
|
||||
Analytics
|
||||
</Button>
|
||||
|
||||
{canUserCreateIssue && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
setTrackElement("PROJECT_PAGE_HEADER");
|
||||
commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.PROJECT);
|
||||
}}
|
||||
size="sm"
|
||||
prependIcon={<Plus />}
|
||||
>
|
||||
Add Issue
|
||||
</Button>
|
||||
<>
|
||||
<Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
|
||||
Analytics
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setTrackElement("PROJECT_PAGE_HEADER");
|
||||
commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.PROJECT);
|
||||
}}
|
||||
size="sm"
|
||||
prependIcon={<Plus />}
|
||||
>
|
||||
Add Issue
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -138,7 +138,10 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
||||
key={view.id}
|
||||
onClick={() => router.push(`/${workspaceSlug}/projects/${projectId}/views/${view.id}`)}
|
||||
>
|
||||
{truncateText(view.name, 40)}
|
||||
<div className="flex items-center gap-1.5">
|
||||
<PhotoFilterIcon height={12} width={12} />
|
||||
{truncateText(view.name, 40)}
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
))}
|
||||
</CustomMenu>
|
||||
@ -152,7 +155,8 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
||||
onChange={(layout) => handleLayoutChange(layout)}
|
||||
selectedLayout={activeLayout}
|
||||
/>
|
||||
<FiltersDropdown title="Filters" placement="bottom-end">
|
||||
|
||||
<FiltersDropdown title="Filters" placement="bottom-end" disabled={!canUserCreateIssue}>
|
||||
<FilterSelection
|
||||
filters={issueFilters?.filters ?? {}}
|
||||
handleFiltersUpdate={handleFiltersUpdate}
|
||||
@ -175,7 +179,7 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
||||
handleDisplayPropertiesUpdate={handleDisplayProperties}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
{
|
||||
{canUserCreateIssue && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
setTrackElement("PROJECT_VIEW_PAGE_HEADER");
|
||||
@ -186,7 +190,7 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
||||
>
|
||||
Add Issue
|
||||
</Button>
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -7,15 +7,24 @@ import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import { Breadcrumbs, PhotoFilterIcon, Button } from "@plane/ui";
|
||||
// helpers
|
||||
import { renderEmoji } from "helpers/emoji.helper";
|
||||
// constants
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
|
||||
export const ProjectViewsHeader: React.FC = observer(() => {
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const { project: projectStore, commandPalette } = useMobxStore();
|
||||
const {
|
||||
project: projectStore,
|
||||
commandPalette,
|
||||
user: { currentProjectRole },
|
||||
} = useMobxStore();
|
||||
const { currentProjectDetails } = projectStore;
|
||||
|
||||
const canUserCreateIssue =
|
||||
currentProjectRole && [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentProjectRole);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
|
||||
@ -50,18 +59,20 @@ export const ProjectViewsHeader: React.FC = observer(() => {
|
||||
</Breadcrumbs>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-shrink-0 items-center gap-2">
|
||||
<div>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
prependIcon={<Plus className="h-3.5 w-3.5 stroke-2" />}
|
||||
onClick={() => commandPalette.toggleCreateViewModal(true)}
|
||||
>
|
||||
Create View
|
||||
</Button>
|
||||
{canUserCreateIssue && (
|
||||
<div className="flex flex-shrink-0 items-center gap-2">
|
||||
<div>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
prependIcon={<Plus className="h-3.5 w-3.5 stroke-2" />}
|
||||
onClick={() => commandPalette.toggleCreateViewModal(true)}
|
||||
>
|
||||
Create View
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
@ -5,6 +5,8 @@ import { Breadcrumbs, Button } from "@plane/ui";
|
||||
// hooks
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// constants
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
|
||||
export const ProjectsHeader = observer(() => {
|
||||
const router = useRouter();
|
||||
@ -15,10 +17,13 @@ export const ProjectsHeader = observer(() => {
|
||||
project: projectStore,
|
||||
commandPalette: commandPaletteStore,
|
||||
trackEvent: { setTrackElement },
|
||||
user: { currentWorkspaceRole },
|
||||
} = useMobxStore();
|
||||
|
||||
const projectsList = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : [];
|
||||
|
||||
const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
|
||||
|
||||
return (
|
||||
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
|
||||
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
||||
@ -44,17 +49,18 @@ export const ProjectsHeader = observer(() => {
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
prependIcon={<Plus />}
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setTrackElement("PROJECTS_PAGE_HEADER");
|
||||
commandPaletteStore.toggleCreateProjectModal(true);
|
||||
}}
|
||||
>
|
||||
Add Project
|
||||
</Button>
|
||||
{isAuthorizedUser && (
|
||||
<Button
|
||||
prependIcon={<Plus />}
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setTrackElement("PROJECTS_PAGE_HEADER");
|
||||
commandPaletteStore.toggleCreateProjectModal(true);
|
||||
}}
|
||||
>
|
||||
Add Project
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -165,16 +165,16 @@ export const InboxMainContent: React.FC = observer(() => {
|
||||
issueStatus === -2
|
||||
? "border-yellow-500 bg-yellow-500/10 text-yellow-500"
|
||||
: issueStatus === -1
|
||||
? "border-red-500 bg-red-500/10 text-red-500"
|
||||
: issueStatus === 0
|
||||
? new Date(issueDetails.issue_inbox[0].snoozed_till ?? "") < new Date()
|
||||
? "border-red-500 bg-red-500/10 text-red-500"
|
||||
: issueStatus === 0
|
||||
? new Date(issueDetails.issue_inbox[0].snoozed_till ?? "") < new Date()
|
||||
? "border-red-500 bg-red-500/10 text-red-500"
|
||||
: "border-gray-500 bg-gray-500/10 text-custom-text-200"
|
||||
: issueStatus === 1
|
||||
? "border-green-500 bg-green-500/10 text-green-500"
|
||||
: issueStatus === 2
|
||||
? "border-gray-500 bg-gray-500/10 text-custom-text-200"
|
||||
: ""
|
||||
: "border-gray-500 bg-gray-500/10 text-custom-text-200"
|
||||
: issueStatus === 1
|
||||
? "border-green-500 bg-green-500/10 text-green-500"
|
||||
: issueStatus === 2
|
||||
? "border-gray-500 bg-gray-500/10 text-custom-text-200"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{issueStatus === -2 ? (
|
||||
@ -225,7 +225,7 @@ export const InboxMainContent: React.FC = observer(() => {
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mb-5 flex items-center">
|
||||
<div className="mb-2.5 flex items-center">
|
||||
{currentIssueState && (
|
||||
<StateGroupIcon
|
||||
className="mr-3 h-4 w-4"
|
||||
|
@ -21,6 +21,7 @@ export interface EmailFormValues {
|
||||
EMAIL_HOST_PASSWORD: string;
|
||||
EMAIL_USE_TLS: string;
|
||||
// EMAIL_USE_SSL: string;
|
||||
EMAIL_FROM: string;
|
||||
}
|
||||
|
||||
export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
|
||||
@ -45,6 +46,7 @@ export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
|
||||
EMAIL_HOST_PASSWORD: config["EMAIL_HOST_PASSWORD"],
|
||||
EMAIL_USE_TLS: config["EMAIL_USE_TLS"],
|
||||
// EMAIL_USE_SSL: config["EMAIL_USE_SSL"],
|
||||
EMAIL_FROM: config["EMAIL_FROM"],
|
||||
},
|
||||
});
|
||||
|
||||
@ -168,6 +170,31 @@ export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid-col grid w-full max-w-4xl grid-cols-1 items-center justify-between gap-x-16 gap-y-8 lg:grid-cols-2">
|
||||
<div className="flex flex-col gap-1">
|
||||
<h4 className="text-sm">From address</h4>
|
||||
<Controller
|
||||
control={control}
|
||||
name="EMAIL_FROM"
|
||||
render={({ field: { value, onChange, ref } }) => (
|
||||
<Input
|
||||
id="EMAIL_FROM"
|
||||
name="EMAIL_FROM"
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.EMAIL_FROM)}
|
||||
placeholder="no-reply@projectplane.so"
|
||||
className="w-full rounded-md font-medium"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<p className="text-xs text-custom-text-400">
|
||||
You will have to verify your email address to being sending emails.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex w-full max-w-md flex-col gap-y-8 px-1">
|
||||
<div className="mr-8 flex items-center gap-10 pt-4">
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import React, { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import useSWR from "swr";
|
||||
@ -24,7 +24,14 @@ import { IIssueAttachment } from "types";
|
||||
const issueAttachmentService = new IssueAttachmentService();
|
||||
const projectMemberService = new ProjectMemberService();
|
||||
|
||||
export const IssueAttachments = () => {
|
||||
type Props = {
|
||||
editable: boolean;
|
||||
};
|
||||
|
||||
export const IssueAttachments: React.FC<Props> = (props) => {
|
||||
const { editable } = props;
|
||||
|
||||
// states
|
||||
const [deleteAttachment, setDeleteAttachment] = useState<IIssueAttachment | null>(null);
|
||||
const [attachmentDeleteModal, setAttachmentDeleteModal] = useState<boolean>(false);
|
||||
|
||||
@ -86,14 +93,16 @@ export const IssueAttachments = () => {
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
setDeleteAttachment(file);
|
||||
setAttachmentDeleteModal(true);
|
||||
}}
|
||||
>
|
||||
<X className="h-4 w-4 text-custom-text-200 hover:text-custom-text-100" />
|
||||
</button>
|
||||
{editable && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setDeleteAttachment(file);
|
||||
setAttachmentDeleteModal(true);
|
||||
}}
|
||||
>
|
||||
<X className="h-4 w-4 text-custom-text-200 hover:text-custom-text-100" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
|
@ -135,7 +135,9 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = (props) => {
|
||||
debouncedFormSave();
|
||||
}}
|
||||
required
|
||||
className="min-h-10 block w-full resize-none overflow-hidden rounded border-none bg-transparent px-3 py-2 text-xl outline-none ring-0 focus:ring-1 focus:ring-custom-primary"
|
||||
className={`min-h-min block w-full resize-none overflow-hidden rounded border-none bg-transparent px-3 py-2 text-2xl font-medium outline-none ring-0 focus:ring-1 focus:ring-custom-primary ${
|
||||
!isAllowed ? "hover:cursor-not-allowed" : ""
|
||||
}`}
|
||||
hasError={Boolean(errors?.description)}
|
||||
role="textbox"
|
||||
disabled={!isAllowed}
|
||||
@ -170,7 +172,9 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = (props) => {
|
||||
setShouldShowAlert={setShowAlert}
|
||||
setIsSubmitting={setIsSubmitting}
|
||||
dragDropEnabled
|
||||
customClassName={isAllowed ? "min-h-[150px] shadow-sm" : "!p-0 !pt-2 text-custom-text-200"}
|
||||
customClassName={
|
||||
isAllowed ? "min-h-[150px] shadow-sm" : "!p-0 !pt-2 text-custom-text-200 pointer-events-none"
|
||||
}
|
||||
noBorder={!isAllowed}
|
||||
onChange={(description: Object, description_html: string) => {
|
||||
setShowAlert(true);
|
||||
|
@ -227,6 +227,7 @@ export const IssueForm: FC<IssueFormProps> = observer((props) => {
|
||||
reset({
|
||||
...defaultValues,
|
||||
...initialData,
|
||||
project: projectId,
|
||||
});
|
||||
}, [setFocus, initialData, reset]);
|
||||
|
||||
|
@ -120,8 +120,8 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => {
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={peekProjectId.toString()}
|
||||
issueId={peekIssueId.toString()}
|
||||
handleIssue={async (issueToUpdate) =>
|
||||
await handleIssues(issueToUpdate.target_date ?? "", issueToUpdate as IIssue, EIssueActions.UPDATE)
|
||||
handleIssue={async (issueToUpdate, action: EIssueActions) =>
|
||||
await handleIssues(issueToUpdate.target_date ?? "", issueToUpdate as IIssue, action)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Droppable } from "@hello-pangea/dnd";
|
||||
// components
|
||||
@ -48,11 +49,12 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
|
||||
quickAddCallback,
|
||||
viewId,
|
||||
} = props;
|
||||
|
||||
const [showAllIssues, setShowAllIssues] = useState(false);
|
||||
const calendarLayout = issuesFilterStore?.issueFilters?.displayFilters?.calendar?.layout ?? "month";
|
||||
|
||||
const issueIdList = groupedIssueIds ? groupedIssueIds[renderDateFormat(date.date)] : null;
|
||||
|
||||
const totalIssues = issueIdList?.length ?? 0;
|
||||
return (
|
||||
<>
|
||||
<div className="group relative flex h-full w-full flex-col bg-custom-background-90">
|
||||
@ -87,7 +89,13 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
|
||||
{...provided.droppableProps}
|
||||
ref={provided.innerRef}
|
||||
>
|
||||
<CalendarIssueBlocks issues={issues} issueIdList={issueIdList} quickActions={quickActions} />
|
||||
<CalendarIssueBlocks
|
||||
issues={issues}
|
||||
issueIdList={issueIdList}
|
||||
quickActions={quickActions}
|
||||
showAllIssues={showAllIssues}
|
||||
/>
|
||||
|
||||
{enableQuickIssueCreate && !disableIssueCreation && (
|
||||
<div className="px-2 py-1">
|
||||
<CalendarQuickAddIssueForm
|
||||
@ -98,9 +106,23 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
|
||||
}}
|
||||
quickAddCallback={quickAddCallback}
|
||||
viewId={viewId}
|
||||
onOpen={() => setShowAllIssues(true)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{totalIssues > 4 && (
|
||||
<div className="flex items-center px-2.5 py-1">
|
||||
<button
|
||||
type="button"
|
||||
className="w-min whitespace-nowrap rounded text-xs px-1.5 py-1 text-custom-text-400 font-medium hover:bg-custom-background-80 hover:text-custom-text-300"
|
||||
onClick={() => setShowAllIssues((prevData) => !prevData)}
|
||||
>
|
||||
{showAllIssues ? "Hide" : totalIssues - 4 + " more"}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
)}
|
||||
|
@ -10,30 +10,43 @@ import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
import { IIssueResponse } from "store/issues/types";
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// constants
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
|
||||
type Props = {
|
||||
issues: IIssueResponse | undefined;
|
||||
issueIdList: string[] | null;
|
||||
quickActions: (issue: IIssue, customActionButton?: React.ReactElement) => React.ReactNode;
|
||||
showAllIssues?: boolean;
|
||||
};
|
||||
|
||||
export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
|
||||
const { issues, issueIdList, quickActions } = props;
|
||||
const { issues, issueIdList, quickActions, showAllIssues = false } = props;
|
||||
// router
|
||||
const router = useRouter();
|
||||
|
||||
// states
|
||||
const [isMenuActive, setIsMenuActive] = useState(false);
|
||||
|
||||
// mobx store
|
||||
const {
|
||||
user: { currentProjectRole },
|
||||
} = useMobxStore();
|
||||
|
||||
const menuActionRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const handleIssuePeekOverview = (issue: IIssue) => {
|
||||
const handleIssuePeekOverview = (issue: IIssue, event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
||||
const { query } = router;
|
||||
|
||||
router.push({
|
||||
pathname: router.pathname,
|
||||
query: { ...query, peekIssueId: issue?.id, peekProjectId: issue?.project },
|
||||
});
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
const issueUrl = `/${issue.workspace_detail.slug}/projects/${issue.project_detail.id}/issues/${issue?.id}`;
|
||||
window.open(issueUrl, "_blank"); // Open link in a new tab
|
||||
} else {
|
||||
router.push({
|
||||
pathname: router.pathname,
|
||||
query: { ...query, peekIssueId: issue?.id, peekProjectId: issue?.project },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useOutsideClickDetector(menuActionRef, () => setIsMenuActive(false));
|
||||
@ -50,21 +63,23 @@ export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
|
||||
</div>
|
||||
);
|
||||
|
||||
const isEditable = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
|
||||
|
||||
return (
|
||||
<>
|
||||
{issueIdList?.map((issueId, index) => {
|
||||
{issueIdList?.slice(0, showAllIssues ? issueIdList.length : 4).map((issueId, index) => {
|
||||
if (!issues?.[issueId]) return null;
|
||||
|
||||
const issue = issues?.[issueId];
|
||||
return (
|
||||
<Draggable key={issue.id} draggableId={issue.id} index={index}>
|
||||
<Draggable key={issue.id} draggableId={issue.id} index={index} isDragDisabled={!isEditable}>
|
||||
{(provided, snapshot) => (
|
||||
<div
|
||||
className="relative cursor-pointer p-1 px-2"
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
ref={provided.innerRef}
|
||||
onClick={() => handleIssuePeekOverview(issue)}
|
||||
onClick={(e) => handleIssuePeekOverview(issue, e)}
|
||||
>
|
||||
{issue?.tempId !== undefined && (
|
||||
<div className="absolute left-0 top-0 z-[99999] h-full w-full animate-pulse bg-custom-background-100/20" />
|
||||
|
@ -27,6 +27,7 @@ type Props = {
|
||||
viewId?: string
|
||||
) => Promise<IIssue | undefined>;
|
||||
viewId?: string;
|
||||
onOpen?: () => void;
|
||||
};
|
||||
|
||||
const defaultValues: Partial<IIssue> = {
|
||||
@ -57,7 +58,7 @@ const Inputs = (props: any) => {
|
||||
};
|
||||
|
||||
export const CalendarQuickAddIssueForm: React.FC<Props> = observer((props) => {
|
||||
const { formKey, groupId, prePopulatedData, quickAddCallback, viewId } = props;
|
||||
const { formKey, groupId, prePopulatedData, quickAddCallback, viewId, onOpen } = props;
|
||||
|
||||
// router
|
||||
const router = useRouter();
|
||||
@ -146,6 +147,11 @@ export const CalendarQuickAddIssueForm: React.FC<Props> = observer((props) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpen = () => {
|
||||
setIsOpen(true);
|
||||
if (onOpen) onOpen();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{isOpen && (
|
||||
@ -169,7 +175,7 @@ export const CalendarQuickAddIssueForm: React.FC<Props> = observer((props) => {
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full items-center gap-x-[6px] rounded-md px-2 py-1.5 text-custom-primary-100"
|
||||
onClick={() => setIsOpen(true)}
|
||||
onClick={handleOpen}
|
||||
>
|
||||
<PlusIcon className="h-3.5 w-3.5 stroke-2" />
|
||||
<span className="text-sm font-medium text-custom-primary-100">New Issue</span>
|
||||
|
@ -14,6 +14,7 @@ export const CycleCalendarLayout: React.FC = observer(() => {
|
||||
cycleIssues: cycleIssueStore,
|
||||
cycleIssuesFilter: cycleIssueFilterStore,
|
||||
calendarHelpers: { handleDragDrop: handleCalenderDragDrop },
|
||||
cycle: { fetchCycleWithId },
|
||||
} = useMobxStore();
|
||||
|
||||
const router = useRouter();
|
||||
@ -24,10 +25,12 @@ export const CycleCalendarLayout: React.FC = observer(() => {
|
||||
if (!workspaceSlug || !cycleId) return;
|
||||
|
||||
await cycleIssueStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue, cycleId.toString());
|
||||
fetchCycleWithId(workspaceSlug.toString(), issue.project, cycleId.toString());
|
||||
},
|
||||
[EIssueActions.DELETE]: async (issue: IIssue) => {
|
||||
if (!workspaceSlug || !cycleId) return;
|
||||
await cycleIssueStore.removeIssue(workspaceSlug.toString(), issue.project, issue.id, cycleId.toString());
|
||||
fetchCycleWithId(workspaceSlug.toString(), issue.project, cycleId.toString());
|
||||
},
|
||||
[EIssueActions.REMOVE]: async (issue: IIssue) => {
|
||||
if (!workspaceSlug || !cycleId || !projectId || !issue.bridge_id) return;
|
||||
@ -38,6 +41,7 @@ export const CycleCalendarLayout: React.FC = observer(() => {
|
||||
issue.id,
|
||||
issue.bridge_id
|
||||
);
|
||||
fetchCycleWithId(workspaceSlug.toString(), issue.project, cycleId.toString());
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -14,6 +14,7 @@ export const ModuleCalendarLayout: React.FC = observer(() => {
|
||||
moduleIssues: moduleIssueStore,
|
||||
moduleIssuesFilter: moduleIssueFilterStore,
|
||||
calendarHelpers: { handleDragDrop: handleCalenderDragDrop },
|
||||
module: { fetchModuleDetails },
|
||||
} = useMobxStore();
|
||||
|
||||
const router = useRouter();
|
||||
@ -27,14 +28,17 @@ export const ModuleCalendarLayout: React.FC = observer(() => {
|
||||
[EIssueActions.UPDATE]: async (issue: IIssue) => {
|
||||
if (!workspaceSlug || !moduleId) return;
|
||||
await moduleIssueStore.updateIssue(workspaceSlug, issue.project, issue.id, issue, moduleId);
|
||||
fetchModuleDetails(workspaceSlug, issue.project, moduleId);
|
||||
},
|
||||
[EIssueActions.DELETE]: async (issue: IIssue) => {
|
||||
if (!workspaceSlug || !moduleId) return;
|
||||
await moduleIssueStore.removeIssue(workspaceSlug, issue.project, issue.id, moduleId);
|
||||
fetchModuleDetails(workspaceSlug, issue.project, moduleId);
|
||||
},
|
||||
[EIssueActions.REMOVE]: async (issue: IIssue) => {
|
||||
if (!workspaceSlug || !moduleId || !issue.bridge_id) return;
|
||||
await moduleIssueStore.removeIssueFromModule(workspaceSlug, issue.project, moduleId, issue.id, issue.bridge_id);
|
||||
fetchModuleDetails(workspaceSlug, issue.project, moduleId);
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -15,6 +15,8 @@ import emptyIssue from "public/empty-state/issue.svg";
|
||||
// types
|
||||
import { ISearchIssueResponse } from "types";
|
||||
import { EProjectStore } from "store/command-palette.store";
|
||||
// constants
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
|
||||
type Props = {
|
||||
workspaceSlug: string | undefined;
|
||||
@ -31,6 +33,7 @@ export const CycleEmptyState: React.FC<Props> = observer((props) => {
|
||||
cycleIssues: cycleIssueStore,
|
||||
commandPalette: commandPaletteStore,
|
||||
trackEvent: { setTrackElement },
|
||||
user: { currentProjectRole: userRole },
|
||||
} = useMobxStore();
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
@ -49,6 +52,8 @@ export const CycleEmptyState: React.FC<Props> = observer((props) => {
|
||||
});
|
||||
};
|
||||
|
||||
const isEditingAllowed = !!userRole && userRole >= EUserWorkspaceRoles.MEMBER;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ExistingIssuesListModal
|
||||
@ -75,10 +80,12 @@ export const CycleEmptyState: React.FC<Props> = observer((props) => {
|
||||
variant="neutral-primary"
|
||||
prependIcon={<PlusIcon className="h-3 w-3" strokeWidth={2} />}
|
||||
onClick={() => setCycleIssuesListModal(true)}
|
||||
disabled={!isEditingAllowed}
|
||||
>
|
||||
Add an existing issue
|
||||
</Button>
|
||||
}
|
||||
disabled={!isEditingAllowed}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
@ -10,6 +10,8 @@ import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import { ISearchIssueResponse } from "types";
|
||||
import useToast from "hooks/use-toast";
|
||||
import { useState } from "react";
|
||||
// constants
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
|
||||
type Props = {
|
||||
workspaceSlug: string | undefined;
|
||||
@ -26,6 +28,7 @@ export const ModuleEmptyState: React.FC<Props> = observer((props) => {
|
||||
moduleIssues: moduleIssueStore,
|
||||
commandPalette: commandPaletteStore,
|
||||
trackEvent: { setTrackElement },
|
||||
user: { currentProjectRole: userRole },
|
||||
} = useMobxStore();
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
@ -44,6 +47,8 @@ export const ModuleEmptyState: React.FC<Props> = observer((props) => {
|
||||
);
|
||||
};
|
||||
|
||||
const isEditingAllowed = !!userRole && userRole >= EUserWorkspaceRoles.MEMBER;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ExistingIssuesListModal
|
||||
@ -70,10 +75,12 @@ export const ModuleEmptyState: React.FC<Props> = observer((props) => {
|
||||
variant="neutral-primary"
|
||||
prependIcon={<PlusIcon className="h-3 w-3" strokeWidth={2} />}
|
||||
onClick={() => setModuleIssuesListModal(true)}
|
||||
disabled={!isEditingAllowed}
|
||||
>
|
||||
Add an existing issue
|
||||
</Button>
|
||||
}
|
||||
disabled={!isEditingAllowed}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
@ -4,6 +4,8 @@ import { PlusIcon } from "lucide-react";
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
import { NewEmptyState } from "components/common/new-empty-state";
|
||||
// constants
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
// assets
|
||||
import emptyIssue from "public/empty-state/empty_issues.webp";
|
||||
import { EProjectStore } from "store/command-palette.store";
|
||||
@ -12,8 +14,11 @@ export const ProjectEmptyState: React.FC = observer(() => {
|
||||
const {
|
||||
commandPalette: commandPaletteStore,
|
||||
trackEvent: { setTrackElement },
|
||||
user: { currentProjectRole },
|
||||
} = useMobxStore();
|
||||
|
||||
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
|
||||
|
||||
return (
|
||||
<div className="grid h-full w-full place-items-center">
|
||||
<NewEmptyState
|
||||
@ -26,14 +31,19 @@ export const ProjectEmptyState: React.FC = observer(() => {
|
||||
description:
|
||||
"Redesign the Plane UI, Rebrand the company, or Launch the new fuel injection system are examples of issues that likely have sub-issues.",
|
||||
}}
|
||||
primaryButton={{
|
||||
text: "Create your first issue",
|
||||
icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />,
|
||||
onClick: () => {
|
||||
setTrackElement("PROJECT_EMPTY_STATE");
|
||||
commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.PROJECT);
|
||||
},
|
||||
}}
|
||||
primaryButton={
|
||||
isEditingAllowed
|
||||
? {
|
||||
text: "Create your first issue",
|
||||
icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />,
|
||||
onClick: () => {
|
||||
setTrackElement("PROJECT_EMPTY_STATE");
|
||||
commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.PROJECT);
|
||||
},
|
||||
}
|
||||
: null
|
||||
}
|
||||
disabled={!isEditingAllowed}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
import {
|
||||
AppliedDateFilters,
|
||||
@ -16,6 +16,8 @@ import { X } from "lucide-react";
|
||||
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
||||
// types
|
||||
import { IIssueFilterOptions, IIssueLabel, IProject, IState, IUserLite } from "types";
|
||||
// constants
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
|
||||
type Props = {
|
||||
appliedFilters: IIssueFilterOptions;
|
||||
@ -33,10 +35,16 @@ const dateFilters = ["start_date", "target_date"];
|
||||
export const AppliedFiltersList: React.FC<Props> = observer((props) => {
|
||||
const { appliedFilters, handleClearAllFilters, handleRemoveFilter, labels, members, projects, states } = props;
|
||||
|
||||
const {
|
||||
user: { currentProjectRole },
|
||||
} = useMobxStore();
|
||||
|
||||
if (!appliedFilters) return null;
|
||||
|
||||
if (Object.keys(appliedFilters).length === 0) return null;
|
||||
|
||||
const isEditingAllowed = currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-stretch gap-2 bg-custom-background-100">
|
||||
{Object.entries(appliedFilters).map(([key, value]) => {
|
||||
@ -53,6 +61,7 @@ export const AppliedFiltersList: React.FC<Props> = observer((props) => {
|
||||
<div className="flex flex-wrap items-center gap-1">
|
||||
{membersFilters.includes(filterKey) && (
|
||||
<AppliedMembersFilters
|
||||
editable={isEditingAllowed}
|
||||
handleRemove={(val) => handleRemoveFilter(filterKey, val)}
|
||||
members={members}
|
||||
values={value}
|
||||
@ -63,16 +72,22 @@ export const AppliedFiltersList: React.FC<Props> = observer((props) => {
|
||||
)}
|
||||
{filterKey === "labels" && (
|
||||
<AppliedLabelsFilters
|
||||
editable={isEditingAllowed}
|
||||
handleRemove={(val) => handleRemoveFilter("labels", val)}
|
||||
labels={labels}
|
||||
values={value}
|
||||
/>
|
||||
)}
|
||||
{filterKey === "priority" && (
|
||||
<AppliedPriorityFilters handleRemove={(val) => handleRemoveFilter("priority", val)} values={value} />
|
||||
<AppliedPriorityFilters
|
||||
editable={isEditingAllowed}
|
||||
handleRemove={(val) => handleRemoveFilter("priority", val)}
|
||||
values={value}
|
||||
/>
|
||||
)}
|
||||
{filterKey === "state" && states && (
|
||||
<AppliedStateFilters
|
||||
editable={isEditingAllowed}
|
||||
handleRemove={(val) => handleRemoveFilter("state", val)}
|
||||
states={states}
|
||||
values={value}
|
||||
@ -86,30 +101,35 @@ export const AppliedFiltersList: React.FC<Props> = observer((props) => {
|
||||
)}
|
||||
{filterKey === "project" && (
|
||||
<AppliedProjectFilters
|
||||
editable={isEditingAllowed}
|
||||
handleRemove={(val) => handleRemoveFilter("project", val)}
|
||||
projects={projects}
|
||||
values={value}
|
||||
/>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
|
||||
onClick={() => handleRemoveFilter(filterKey, null)}
|
||||
>
|
||||
<X size={12} strokeWidth={2} />
|
||||
</button>
|
||||
{isEditingAllowed && (
|
||||
<button
|
||||
type="button"
|
||||
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
|
||||
onClick={() => handleRemoveFilter(filterKey, null)}
|
||||
>
|
||||
<X size={12} strokeWidth={2} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClearAllFilters}
|
||||
className="flex items-center gap-2 rounded-md border border-custom-border-200 px-2 py-1 text-xs text-custom-text-300 hover:text-custom-text-200"
|
||||
>
|
||||
Clear all
|
||||
<X size={12} strokeWidth={2} />
|
||||
</button>
|
||||
{isEditingAllowed && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClearAllFilters}
|
||||
className="flex items-center gap-2 rounded-md border border-custom-border-200 px-2 py-1 text-xs text-custom-text-300 hover:text-custom-text-200"
|
||||
>
|
||||
Clear all
|
||||
<X size={12} strokeWidth={2} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
@ -9,10 +9,11 @@ type Props = {
|
||||
handleRemove: (val: string) => void;
|
||||
labels: IIssueLabel[] | undefined;
|
||||
values: string[];
|
||||
editable: boolean | undefined;
|
||||
};
|
||||
|
||||
export const AppliedLabelsFilters: React.FC<Props> = observer((props) => {
|
||||
const { handleRemove, labels, values } = props;
|
||||
const { handleRemove, labels, values, editable } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -30,13 +31,15 @@ export const AppliedLabelsFilters: React.FC<Props> = observer((props) => {
|
||||
}}
|
||||
/>
|
||||
<span className="normal-case">{labelDetails.name}</span>
|
||||
<button
|
||||
type="button"
|
||||
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
|
||||
onClick={() => handleRemove(labelId)}
|
||||
>
|
||||
<X size={10} strokeWidth={2} />
|
||||
</button>
|
||||
{editable && (
|
||||
<button
|
||||
type="button"
|
||||
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
|
||||
onClick={() => handleRemove(labelId)}
|
||||
>
|
||||
<X size={10} strokeWidth={2} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
@ -9,10 +9,11 @@ type Props = {
|
||||
handleRemove: (val: string) => void;
|
||||
members: IUserLite[] | undefined;
|
||||
values: string[];
|
||||
editable: boolean | undefined;
|
||||
};
|
||||
|
||||
export const AppliedMembersFilters: React.FC<Props> = observer((props) => {
|
||||
const { handleRemove, members, values } = props;
|
||||
const { handleRemove, members, values, editable } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -25,13 +26,15 @@ export const AppliedMembersFilters: React.FC<Props> = observer((props) => {
|
||||
<div key={memberId} className="flex items-center gap-1 rounded bg-custom-background-80 p-1 text-xs">
|
||||
<Avatar name={memberDetails.display_name} src={memberDetails.avatar} showTooltip={false} />
|
||||
<span className="normal-case">{memberDetails.display_name}</span>
|
||||
<button
|
||||
type="button"
|
||||
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
|
||||
onClick={() => handleRemove(memberId)}
|
||||
>
|
||||
<X size={10} strokeWidth={2} />
|
||||
</button>
|
||||
{editable && (
|
||||
<button
|
||||
type="button"
|
||||
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
|
||||
onClick={() => handleRemove(memberId)}
|
||||
>
|
||||
<X size={10} strokeWidth={2} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
@ -9,10 +9,11 @@ import { TIssuePriorities } from "types";
|
||||
type Props = {
|
||||
handleRemove: (val: string) => void;
|
||||
values: string[];
|
||||
editable: boolean | undefined;
|
||||
};
|
||||
|
||||
export const AppliedPriorityFilters: React.FC<Props> = observer((props) => {
|
||||
const { handleRemove, values } = props;
|
||||
const { handleRemove, values, editable } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -20,13 +21,15 @@ export const AppliedPriorityFilters: React.FC<Props> = observer((props) => {
|
||||
<div key={priority} className="flex items-center gap-1 rounded bg-custom-background-80 p-1 text-xs">
|
||||
<PriorityIcon priority={priority as TIssuePriorities} className={`h-3 w-3`} />
|
||||
{priority}
|
||||
<button
|
||||
type="button"
|
||||
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
|
||||
onClick={() => handleRemove(priority)}
|
||||
>
|
||||
<X size={10} strokeWidth={2} />
|
||||
</button>
|
||||
{editable && (
|
||||
<button
|
||||
type="button"
|
||||
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
|
||||
onClick={() => handleRemove(priority)}
|
||||
>
|
||||
<X size={10} strokeWidth={2} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
|
@ -10,10 +10,11 @@ type Props = {
|
||||
handleRemove: (val: string) => void;
|
||||
projects: IProject[] | undefined;
|
||||
values: string[];
|
||||
editable: boolean | undefined;
|
||||
};
|
||||
|
||||
export const AppliedProjectFilters: React.FC<Props> = observer((props) => {
|
||||
const { handleRemove, projects, values } = props;
|
||||
const { handleRemove, projects, values, editable } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -34,13 +35,15 @@ export const AppliedProjectFilters: React.FC<Props> = observer((props) => {
|
||||
</span>
|
||||
)}
|
||||
<span className="normal-case">{projectDetails.name}</span>
|
||||
<button
|
||||
type="button"
|
||||
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
|
||||
onClick={() => handleRemove(projectId)}
|
||||
>
|
||||
<X size={10} strokeWidth={2} />
|
||||
</button>
|
||||
{editable && (
|
||||
<button
|
||||
type="button"
|
||||
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
|
||||
onClick={() => handleRemove(projectId)}
|
||||
>
|
||||
<X size={10} strokeWidth={2} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
@ -10,10 +10,11 @@ type Props = {
|
||||
handleRemove: (val: string) => void;
|
||||
states: IState[];
|
||||
values: string[];
|
||||
editable: boolean | undefined;
|
||||
};
|
||||
|
||||
export const AppliedStateFilters: React.FC<Props> = observer((props) => {
|
||||
const { handleRemove, states, values } = props;
|
||||
const { handleRemove, states, values, editable } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -26,13 +27,15 @@ export const AppliedStateFilters: React.FC<Props> = observer((props) => {
|
||||
<div key={stateId} className="flex items-center gap-1 rounded bg-custom-background-80 p-1 text-xs">
|
||||
<StateGroupIcon color={stateDetails.color} stateGroup={stateDetails.group} height="12px" width="12px" />
|
||||
{stateDetails.name}
|
||||
<button
|
||||
type="button"
|
||||
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
|
||||
onClick={() => handleRemove(stateId)}
|
||||
>
|
||||
<X size={10} strokeWidth={2} />
|
||||
</button>
|
||||
{editable && (
|
||||
<button
|
||||
type="button"
|
||||
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
|
||||
onClick={() => handleRemove(stateId)}
|
||||
>
|
||||
<X size={10} strokeWidth={2} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
@ -11,10 +11,11 @@ type Props = {
|
||||
children: React.ReactNode;
|
||||
title?: string;
|
||||
placement?: Placement;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const FiltersDropdown: React.FC<Props> = (props) => {
|
||||
const { children, title = "Dropdown", placement } = props;
|
||||
const { children, title = "Dropdown", placement, disabled = false } = props;
|
||||
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||
@ -32,6 +33,7 @@ export const FiltersDropdown: React.FC<Props> = (props) => {
|
||||
<>
|
||||
<Popover.Button as={React.Fragment}>
|
||||
<Button
|
||||
disabled={disabled}
|
||||
ref={setReferenceElement}
|
||||
variant="neutral-primary"
|
||||
size="sm"
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React from "react";
|
||||
import React, { useCallback } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// mobx store
|
||||
@ -25,6 +25,8 @@ import {
|
||||
IViewIssuesStore,
|
||||
} from "store/issues";
|
||||
import { TUnGroupedIssues } from "store/issues/types";
|
||||
import { EIssueActions } from "../types";
|
||||
// constants
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
|
||||
interface IBaseGanttRoot {
|
||||
@ -35,10 +37,15 @@ interface IBaseGanttRoot {
|
||||
| IViewIssuesFilterStore;
|
||||
issueStore: IProjectIssuesStore | IModuleIssuesStore | ICycleIssuesStore | IViewIssuesStore;
|
||||
viewId?: string;
|
||||
issueActions: {
|
||||
[EIssueActions.DELETE]: (issue: IIssue) => Promise<void>;
|
||||
[EIssueActions.UPDATE]?: (issue: IIssue) => Promise<void>;
|
||||
[EIssueActions.REMOVE]?: (issue: IIssue) => Promise<void>;
|
||||
};
|
||||
}
|
||||
|
||||
export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGanttRoot) => {
|
||||
const { issueFiltersStore, issueStore, viewId } = props;
|
||||
const { issueFiltersStore, issueStore, viewId, issueActions } = props;
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, peekIssueId, peekProjectId } = router.query;
|
||||
@ -64,11 +71,14 @@ export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGan
|
||||
await issueStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, payload, viewId);
|
||||
};
|
||||
|
||||
const updateIssue = async (projectId: string, issueId: string, payload: Partial<IIssue>) => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
await issueStore.updateIssue(workspaceSlug.toString(), projectId, issueId, payload, viewId);
|
||||
};
|
||||
const handleIssues = useCallback(
|
||||
async (issue: IIssue, action: EIssueActions) => {
|
||||
if (issueActions[action]) {
|
||||
await issueActions[action]!(issue);
|
||||
}
|
||||
},
|
||||
[issueActions]
|
||||
);
|
||||
|
||||
const isAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
|
||||
|
||||
@ -102,8 +112,8 @@ export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGan
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={peekProjectId.toString()}
|
||||
issueId={peekIssueId.toString()}
|
||||
handleIssue={async (issueToUpdate) => {
|
||||
await updateIssue(peekProjectId.toString(), peekIssueId.toString(), issueToUpdate);
|
||||
handleIssue={async (issueToUpdate, action) => {
|
||||
await handleIssues(issueToUpdate as IIssue, action);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
@ -9,13 +9,17 @@ import { IIssue } from "types";
|
||||
export const IssueGanttBlock = ({ data }: { data: IIssue }) => {
|
||||
const router = useRouter();
|
||||
|
||||
const handleIssuePeekOverview = () => {
|
||||
const handleIssuePeekOverview = (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
||||
const { query } = router;
|
||||
|
||||
router.push({
|
||||
pathname: router.pathname,
|
||||
query: { ...query, peekIssueId: data?.id, peekProjectId: data?.project },
|
||||
});
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
const issueUrl = `/${data?.workspace_detail.slug}/projects/${data?.project_detail.id}/issues/${data?.id}`;
|
||||
window.open(issueUrl, "_blank"); // Open link in a new tab
|
||||
} else {
|
||||
router.push({
|
||||
pathname: router.pathname,
|
||||
query: { ...query, peekIssueId: data?.id, peekProjectId: data?.project },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -4,15 +4,50 @@ import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
import { BaseGanttRoot } from "./base-gantt-root";
|
||||
import { useRouter } from "next/router";
|
||||
// types
|
||||
import { EIssueActions } from "../types";
|
||||
import { IIssue } from "types";
|
||||
|
||||
export const CycleGanttLayout: React.FC = observer(() => {
|
||||
const router = useRouter();
|
||||
const { cycleId } = router.query;
|
||||
const { cycleId, workspaceSlug } = router.query;
|
||||
|
||||
const { cycleIssues: cycleIssueStore, cycleIssuesFilter: cycleIssueFilterStore } = useMobxStore();
|
||||
const {
|
||||
cycleIssues: cycleIssueStore,
|
||||
cycleIssuesFilter: cycleIssueFilterStore,
|
||||
cycle: { fetchCycleWithId },
|
||||
} = useMobxStore();
|
||||
|
||||
const issueActions = {
|
||||
[EIssueActions.UPDATE]: async (issue: IIssue) => {
|
||||
if (!workspaceSlug || !cycleId) return;
|
||||
|
||||
await cycleIssueStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue, cycleId.toString());
|
||||
fetchCycleWithId(workspaceSlug.toString(), issue.project, cycleId.toString());
|
||||
},
|
||||
[EIssueActions.DELETE]: async (issue: IIssue) => {
|
||||
if (!workspaceSlug || !cycleId) return;
|
||||
|
||||
await cycleIssueStore.removeIssue(workspaceSlug.toString(), issue.project, issue.id, cycleId.toString());
|
||||
fetchCycleWithId(workspaceSlug.toString(), issue.project, cycleId.toString());
|
||||
},
|
||||
[EIssueActions.REMOVE]: async (issue: IIssue) => {
|
||||
if (!workspaceSlug || !cycleId || !issue.bridge_id) return;
|
||||
|
||||
await cycleIssueStore.removeIssueFromCycle(
|
||||
workspaceSlug.toString(),
|
||||
issue.project,
|
||||
cycleId.toString(),
|
||||
issue.id,
|
||||
issue.bridge_id
|
||||
);
|
||||
fetchCycleWithId(workspaceSlug.toString(), issue.project, cycleId.toString());
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseGanttRoot
|
||||
issueActions={issueActions}
|
||||
issueFiltersStore={cycleIssueFilterStore}
|
||||
issueStore={cycleIssueStore}
|
||||
viewId={cycleId?.toString()}
|
||||
|
@ -4,15 +4,50 @@ import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
import { BaseGanttRoot } from "./base-gantt-root";
|
||||
import { useRouter } from "next/router";
|
||||
// types
|
||||
import { EIssueActions } from "../types";
|
||||
import { IIssue } from "types";
|
||||
|
||||
export const ModuleGanttLayout: React.FC = observer(() => {
|
||||
const router = useRouter();
|
||||
const { moduleId } = router.query;
|
||||
const { moduleId, workspaceSlug } = router.query;
|
||||
|
||||
const { moduleIssues: moduleIssueStore, moduleIssuesFilter: moduleIssueFilterStore } = useMobxStore();
|
||||
const {
|
||||
moduleIssues: moduleIssueStore,
|
||||
moduleIssuesFilter: moduleIssueFilterStore,
|
||||
module: { fetchModuleDetails },
|
||||
} = useMobxStore();
|
||||
|
||||
const issueActions = {
|
||||
[EIssueActions.UPDATE]: async (issue: IIssue) => {
|
||||
if (!workspaceSlug || !moduleId) return;
|
||||
|
||||
await moduleIssueStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue, moduleId.toString());
|
||||
fetchModuleDetails(workspaceSlug.toString(), issue.project, moduleId.toString());
|
||||
},
|
||||
[EIssueActions.DELETE]: async (issue: IIssue) => {
|
||||
if (!workspaceSlug || !moduleId) return;
|
||||
|
||||
await moduleIssueStore.removeIssue(workspaceSlug.toString(), issue.project, issue.id, moduleId.toString());
|
||||
fetchModuleDetails(workspaceSlug.toString(), issue.project, moduleId.toString());
|
||||
},
|
||||
[EIssueActions.REMOVE]: async (issue: IIssue) => {
|
||||
if (!workspaceSlug || !moduleId || !issue.bridge_id) return;
|
||||
|
||||
await moduleIssueStore.removeIssueFromModule(
|
||||
workspaceSlug.toString(),
|
||||
issue.project,
|
||||
moduleId.toString(),
|
||||
issue.id,
|
||||
issue.bridge_id
|
||||
);
|
||||
fetchModuleDetails(workspaceSlug.toString(), issue.project, moduleId.toString());
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseGanttRoot
|
||||
issueActions={issueActions}
|
||||
issueFiltersStore={moduleIssueFilterStore}
|
||||
issueStore={moduleIssueStore}
|
||||
viewId={moduleId?.toString()}
|
||||
|
@ -1,12 +1,36 @@
|
||||
import React from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// hooks
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
import { BaseGanttRoot } from "./base-gantt-root";
|
||||
// types
|
||||
import { EIssueActions } from "../types";
|
||||
import { IIssue } from "types";
|
||||
|
||||
export const GanttLayout: React.FC = observer(() => {
|
||||
const { projectIssues: projectIssuesStore, projectIssuesFilter: projectIssueFiltersStore } = useMobxStore();
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
return <BaseGanttRoot issueFiltersStore={projectIssueFiltersStore} issueStore={projectIssuesStore} />;
|
||||
const issueActions = {
|
||||
[EIssueActions.UPDATE]: async (issue: IIssue) => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
await projectIssuesStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue);
|
||||
},
|
||||
[EIssueActions.DELETE]: async (issue: IIssue) => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
await projectIssuesStore.removeIssue(workspaceSlug.toString(), issue.project, issue.id);
|
||||
},
|
||||
};
|
||||
return (
|
||||
<BaseGanttRoot
|
||||
issueActions={issueActions}
|
||||
issueFiltersStore={projectIssueFiltersStore}
|
||||
issueStore={projectIssuesStore}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
@ -1,11 +1,35 @@
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useRouter } from "next/router";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
import { BaseGanttRoot } from "./base-gantt-root";
|
||||
// types
|
||||
import { EIssueActions } from "../types";
|
||||
import { IIssue } from "types";
|
||||
|
||||
export const ProjectViewGanttLayout: React.FC = observer(() => {
|
||||
const { viewIssues: projectIssueViewStore, viewIssuesFilter: projectIssueViewFiltersStore } = useMobxStore();
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
return <BaseGanttRoot issueFiltersStore={projectIssueViewFiltersStore} issueStore={projectIssueViewStore} />;
|
||||
const issueActions = {
|
||||
[EIssueActions.UPDATE]: async (issue: IIssue) => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
await projectIssueViewStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue);
|
||||
},
|
||||
[EIssueActions.DELETE]: async (issue: IIssue) => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
await projectIssueViewStore.removeIssue(workspaceSlug.toString(), issue.project, issue.id);
|
||||
},
|
||||
};
|
||||
return (
|
||||
<BaseGanttRoot
|
||||
issueActions={issueActions}
|
||||
issueFiltersStore={projectIssueViewFiltersStore}
|
||||
issueStore={projectIssueViewStore}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
@ -346,8 +346,8 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={peekProjectId.toString()}
|
||||
issueId={peekIssueId.toString()}
|
||||
handleIssue={async (issueToUpdate) =>
|
||||
await handleIssues(sub_group_by, group_by, issueToUpdate as IIssue, EIssueActions.UPDATE)
|
||||
handleIssue={async (issueToUpdate, action: EIssueActions) =>
|
||||
await handleIssues(sub_group_by, group_by, issueToUpdate as IIssue, action)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { memo } from "react";
|
||||
import { Draggable } from "@hello-pangea/dnd";
|
||||
import { Draggable, DraggableStateSnapshot } from "@hello-pangea/dnd";
|
||||
import isEqual from "lodash/isEqual";
|
||||
// components
|
||||
import { KanBanProperties } from "./properties";
|
||||
@ -32,11 +32,23 @@ interface IssueDetailsBlockProps {
|
||||
quickActions: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => React.ReactNode;
|
||||
displayProperties: IIssueDisplayProperties | null;
|
||||
isReadOnly: boolean;
|
||||
snapshot: DraggableStateSnapshot;
|
||||
isDragDisabled: boolean;
|
||||
}
|
||||
|
||||
const KanbanIssueDetailsBlock: React.FC<IssueDetailsBlockProps> = (props) => {
|
||||
const { sub_group_id, columnId, issue, showEmptyGroup, handleIssues, quickActions, displayProperties, isReadOnly } =
|
||||
props;
|
||||
const {
|
||||
sub_group_id,
|
||||
columnId,
|
||||
issue,
|
||||
showEmptyGroup,
|
||||
handleIssues,
|
||||
quickActions,
|
||||
displayProperties,
|
||||
isReadOnly,
|
||||
snapshot,
|
||||
isDragDisabled,
|
||||
} = props;
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
@ -44,20 +56,29 @@ const KanbanIssueDetailsBlock: React.FC<IssueDetailsBlockProps> = (props) => {
|
||||
if (issueToUpdate) handleIssues(sub_group_by, group_by, issueToUpdate, EIssueActions.UPDATE);
|
||||
};
|
||||
|
||||
const handleIssuePeekOverview = () => {
|
||||
const handleIssuePeekOverview = (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
||||
const { query } = router;
|
||||
|
||||
router.push({
|
||||
pathname: router.pathname,
|
||||
query: { ...query, peekIssueId: issue?.id, peekProjectId: issue?.project },
|
||||
});
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
const issueUrl = `/${issue.workspace_detail.slug}/projects/${issue.project_detail.id}/issues/${issue?.id}`;
|
||||
window.open(issueUrl, "_blank"); // Open link in a new tab
|
||||
} else {
|
||||
router.push({
|
||||
pathname: router.pathname,
|
||||
query: { ...query, peekIssueId: issue?.id, peekProjectId: issue?.project },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={`flex flex-col space-y-2 cursor-pointer rounded border-[0.5px] border-custom-border-200 bg-custom-background-100 px-3 py-2 text-sm shadow-custom-shadow-2xs transition-all w-full ${
|
||||
isDragDisabled ? "" : "hover:cursor-grab"
|
||||
} ${snapshot.isDragging ? `border-custom-primary-100` : `border-transparent`}`}
|
||||
onClick={handleIssuePeekOverview}
|
||||
>
|
||||
{displayProperties && displayProperties?.key && (
|
||||
<div className="relative">
|
||||
<div className="line-clamp-1 text-xs text-custom-text-300">
|
||||
<div className="relative w-full ">
|
||||
<div className="line-clamp-1 text-xs text-left text-custom-text-300">
|
||||
{issue.project_detail.identifier}-{issue.sequence_id}
|
||||
</div>
|
||||
<div className="absolute -top-1 right-0 hidden group-hover/kanban-block:block">
|
||||
@ -70,9 +91,7 @@ const KanbanIssueDetailsBlock: React.FC<IssueDetailsBlockProps> = (props) => {
|
||||
</div>
|
||||
)}
|
||||
<Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
|
||||
<div className="line-clamp-2 text-sm font-medium text-custom-text-100" onClick={handleIssuePeekOverview}>
|
||||
{issue.name}
|
||||
</div>
|
||||
<div className="line-clamp-2 text-sm font-medium text-custom-text-100">{issue.name}</div>
|
||||
</Tooltip>
|
||||
<div>
|
||||
<KanBanProperties
|
||||
@ -85,7 +104,7 @@ const KanbanIssueDetailsBlock: React.FC<IssueDetailsBlockProps> = (props) => {
|
||||
isReadOnly={isReadOnly}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -121,10 +140,10 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = (props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Draggable draggableId={draggableId} index={index}>
|
||||
<Draggable draggableId={draggableId} index={index} isDragDisabled={!canEditIssueProperties}>
|
||||
{(provided, snapshot) => (
|
||||
<div
|
||||
className="group/kanban-block relative p-1.5 hover:cursor-default"
|
||||
className="group/kanban-block relative p-1.5"
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
ref={provided.innerRef}
|
||||
@ -132,22 +151,18 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = (props) => {
|
||||
{issue.tempId !== undefined && (
|
||||
<div className="absolute left-0 top-0 z-[99999] h-full w-full animate-pulse bg-custom-background-100/20" />
|
||||
)}
|
||||
<div
|
||||
className={`space-y-2 rounded border-[0.5px] border-custom-border-200 bg-custom-background-100 px-3 py-2 text-sm shadow-custom-shadow-2xs transition-all ${
|
||||
isDragDisabled ? "" : "hover:cursor-grab"
|
||||
} ${snapshot.isDragging ? `border-custom-primary-100` : `border-transparent`}`}
|
||||
>
|
||||
<KanbanIssueMemoBlock
|
||||
sub_group_id={sub_group_id}
|
||||
columnId={columnId}
|
||||
issue={issue}
|
||||
showEmptyGroup={showEmptyGroup}
|
||||
handleIssues={handleIssues}
|
||||
quickActions={quickActions}
|
||||
displayProperties={displayProperties}
|
||||
isReadOnly={!canEditIssueProperties}
|
||||
/>
|
||||
</div>
|
||||
<KanbanIssueMemoBlock
|
||||
sub_group_id={sub_group_id}
|
||||
columnId={columnId}
|
||||
issue={issue}
|
||||
showEmptyGroup={showEmptyGroup}
|
||||
handleIssues={handleIssues}
|
||||
quickActions={quickActions}
|
||||
displayProperties={displayProperties}
|
||||
isReadOnly={!canEditIssueProperties}
|
||||
snapshot={snapshot}
|
||||
isDragDisabled={isDragDisabled}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
|
@ -57,7 +57,7 @@ export const KanBanProperties: React.FC<IKanBanProperties> = observer((props) =>
|
||||
);
|
||||
};
|
||||
|
||||
const handleStartDate = (date: string) => {
|
||||
const handleStartDate = (date: string | null) => {
|
||||
handleIssues(
|
||||
!sub_group_id && sub_group_id === "null" ? null : sub_group_id,
|
||||
!group_id && group_id === "null" ? null : group_id,
|
||||
@ -65,7 +65,7 @@ export const KanBanProperties: React.FC<IKanBanProperties> = observer((props) =>
|
||||
);
|
||||
};
|
||||
|
||||
const handleTargetDate = (date: string) => {
|
||||
const handleTargetDate = (date: string | null) => {
|
||||
handleIssues(
|
||||
!sub_group_id && sub_group_id === "null" ? null : sub_group_id,
|
||||
!group_id && group_id === "null" ? null : group_id,
|
||||
@ -122,7 +122,7 @@ export const KanBanProperties: React.FC<IKanBanProperties> = observer((props) =>
|
||||
{displayProperties && displayProperties?.start_date && (
|
||||
<IssuePropertyDate
|
||||
value={issue?.start_date || null}
|
||||
onChange={(date: string) => handleStartDate(date)}
|
||||
onChange={(date) => handleStartDate(date)}
|
||||
disabled={isReadOnly}
|
||||
type="start_date"
|
||||
/>
|
||||
@ -132,7 +132,7 @@ export const KanBanProperties: React.FC<IKanBanProperties> = observer((props) =>
|
||||
{displayProperties && displayProperties?.due_date && (
|
||||
<IssuePropertyDate
|
||||
value={issue?.target_date || null}
|
||||
onChange={(date: string) => handleTargetDate(date)}
|
||||
onChange={(date) => handleTargetDate(date)}
|
||||
disabled={isReadOnly}
|
||||
type="target_date"
|
||||
/>
|
||||
|
@ -25,6 +25,7 @@ export const CycleKanBanLayout: React.FC = observer(() => {
|
||||
cycleIssuesFilter: cycleIssueFilterStore,
|
||||
cycleIssueKanBanView: cycleIssueKanBanViewStore,
|
||||
kanBanHelpers: kanBanHelperStore,
|
||||
cycle: { fetchCycleWithId },
|
||||
} = useMobxStore();
|
||||
|
||||
const issueActions = {
|
||||
@ -32,11 +33,13 @@ export const CycleKanBanLayout: React.FC = observer(() => {
|
||||
if (!workspaceSlug || !cycleId) return;
|
||||
|
||||
await cycleIssueStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue, cycleId.toString());
|
||||
fetchCycleWithId(workspaceSlug.toString(), issue.project, cycleId.toString());
|
||||
},
|
||||
[EIssueActions.DELETE]: async (issue: IIssue) => {
|
||||
if (!workspaceSlug || !cycleId) return;
|
||||
|
||||
await cycleIssueStore.removeIssue(workspaceSlug.toString(), issue.project, issue.id, cycleId.toString());
|
||||
fetchCycleWithId(workspaceSlug.toString(), issue.project, cycleId.toString());
|
||||
},
|
||||
[EIssueActions.REMOVE]: async (issue: IIssue) => {
|
||||
if (!workspaceSlug || !cycleId || !issue.bridge_id) return;
|
||||
@ -48,6 +51,7 @@ export const CycleKanBanLayout: React.FC = observer(() => {
|
||||
issue.id,
|
||||
issue.bridge_id
|
||||
);
|
||||
fetchCycleWithId(workspaceSlug.toString(), issue.project, cycleId.toString());
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -25,6 +25,7 @@ export const ModuleKanBanLayout: React.FC = observer(() => {
|
||||
moduleIssuesFilter: moduleIssueFilterStore,
|
||||
moduleIssueKanBanView: moduleIssueKanBanViewStore,
|
||||
kanBanHelpers: kanBanHelperStore,
|
||||
module: { fetchModuleDetails },
|
||||
} = useMobxStore();
|
||||
|
||||
const issueActions = {
|
||||
@ -32,11 +33,13 @@ export const ModuleKanBanLayout: React.FC = observer(() => {
|
||||
if (!workspaceSlug || !moduleId) return;
|
||||
|
||||
await moduleIssueStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue, moduleId.toString());
|
||||
fetchModuleDetails(workspaceSlug.toString(), issue.project, moduleId.toString());
|
||||
},
|
||||
[EIssueActions.DELETE]: async (issue: IIssue) => {
|
||||
if (!workspaceSlug || !moduleId) return;
|
||||
|
||||
await moduleIssueStore.removeIssue(workspaceSlug.toString(), issue.project, issue.id, moduleId.toString());
|
||||
fetchModuleDetails(workspaceSlug.toString(), issue.project, moduleId.toString());
|
||||
},
|
||||
[EIssueActions.REMOVE]: async (issue: IIssue) => {
|
||||
if (!workspaceSlug || !moduleId || !issue.bridge_id) return;
|
||||
@ -48,6 +51,7 @@ export const ModuleKanBanLayout: React.FC = observer(() => {
|
||||
issue.id,
|
||||
issue.bridge_id
|
||||
);
|
||||
fetchModuleDetails(workspaceSlug.toString(), issue.project, moduleId.toString());
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -168,7 +168,9 @@ export const BaseListRoot = observer((props: IBaseListRoot) => {
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={peekProjectId.toString()}
|
||||
issueId={peekIssueId.toString()}
|
||||
handleIssue={async (issueToUpdate) => await handleIssues(issueToUpdate as IIssue, EIssueActions.UPDATE)}
|
||||
handleIssue={async (issueToUpdate, action: EIssueActions) =>
|
||||
await handleIssues(issueToUpdate as IIssue, action)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
@ -25,20 +25,27 @@ export const IssueBlock: React.FC<IssueBlockProps> = (props) => {
|
||||
handleIssues(issueToUpdate, EIssueActions.UPDATE);
|
||||
};
|
||||
|
||||
const handleIssuePeekOverview = () => {
|
||||
const handleIssuePeekOverview = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
const { query } = router;
|
||||
|
||||
router.push({
|
||||
pathname: router.pathname,
|
||||
query: { ...query, peekIssueId: issue?.id, peekProjectId: issue?.project },
|
||||
});
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
const issueUrl = `/${issue.workspace_detail.slug}/projects/${issue.project_detail.id}/issues/${issue?.id}`;
|
||||
window.open(issueUrl, "_blank"); // Open link in a new tab
|
||||
} else {
|
||||
router.push({
|
||||
pathname: router.pathname,
|
||||
query: { ...query, peekIssueId: issue?.id, peekProjectId: issue?.project },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const canEditIssueProperties = canEditProperties(issue.project);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative flex items-center gap-3 bg-custom-background-100 p-3 text-sm">
|
||||
<button
|
||||
className="relative flex items-center gap-3 bg-custom-background-100 p-3 text-sm w-full"
|
||||
onClick={handleIssuePeekOverview}
|
||||
>
|
||||
{displayProperties && displayProperties?.key && (
|
||||
<div className="flex-shrink-0 text-xs font-medium text-custom-text-300">
|
||||
{issue?.project_detail?.identifier}-{issue.sequence_id}
|
||||
@ -49,10 +56,7 @@ export const IssueBlock: React.FC<IssueBlockProps> = (props) => {
|
||||
<div className="absolute left-0 top-0 z-[99999] h-full w-full animate-pulse bg-custom-background-100/20" />
|
||||
)}
|
||||
<Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
|
||||
<div
|
||||
className="line-clamp-1 w-full cursor-pointer text-sm font-medium text-custom-text-100"
|
||||
onClick={handleIssuePeekOverview}
|
||||
>
|
||||
<div className="line-clamp-1 w-full cursor-pointer text-sm font-medium text-custom-text-100 text-left">
|
||||
{issue.name}
|
||||
</div>
|
||||
</Tooltip>
|
||||
@ -75,7 +79,7 @@ export const IssueBlock: React.FC<IssueBlockProps> = (props) => {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -40,11 +40,11 @@ export const ListProperties: FC<IListProperties> = observer((props) => {
|
||||
handleIssues(!group_id && group_id === "null" ? null : group_id, { ...issue, assignees: ids });
|
||||
};
|
||||
|
||||
const handleStartDate = (date: string) => {
|
||||
const handleStartDate = (date: string | null) => {
|
||||
handleIssues(!group_id && group_id === "null" ? null : group_id, { ...issue, start_date: date });
|
||||
};
|
||||
|
||||
const handleTargetDate = (date: string) => {
|
||||
const handleTargetDate = (date: string | null) => {
|
||||
handleIssues(!group_id && group_id === "null" ? null : group_id, { ...issue, target_date: date });
|
||||
};
|
||||
|
||||
@ -106,7 +106,7 @@ export const ListProperties: FC<IListProperties> = observer((props) => {
|
||||
{displayProperties && displayProperties?.start_date && (
|
||||
<IssuePropertyDate
|
||||
value={issue?.start_date || null}
|
||||
onChange={(date: string) => handleStartDate(date)}
|
||||
onChange={(date) => handleStartDate(date)}
|
||||
disabled={isReadonly}
|
||||
type="start_date"
|
||||
/>
|
||||
@ -116,7 +116,7 @@ export const ListProperties: FC<IListProperties> = observer((props) => {
|
||||
{displayProperties && displayProperties?.due_date && (
|
||||
<IssuePropertyDate
|
||||
value={issue?.target_date || null}
|
||||
onChange={(date: string) => handleTargetDate(date)}
|
||||
onChange={(date) => handleTargetDate(date)}
|
||||
disabled={isReadonly}
|
||||
type="target_date"
|
||||
/>
|
||||
|
@ -19,23 +19,30 @@ export const CycleListLayout: React.FC = observer(() => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, cycleId } = router.query as { workspaceSlug: string; cycleId: string };
|
||||
// store
|
||||
const { cycleIssues: cycleIssueStore, cycleIssuesFilter: cycleIssueFilterStore } = useMobxStore();
|
||||
const {
|
||||
cycleIssues: cycleIssueStore,
|
||||
cycleIssuesFilter: cycleIssueFilterStore,
|
||||
cycle: { fetchCycleWithId },
|
||||
} = useMobxStore();
|
||||
|
||||
const issueActions = {
|
||||
[EIssueActions.UPDATE]: async (group_by: string | null, issue: IIssue) => {
|
||||
if (!workspaceSlug || !cycleId) return;
|
||||
|
||||
await cycleIssueStore.updateIssue(workspaceSlug, issue.project, issue.id, issue, cycleId);
|
||||
fetchCycleWithId(workspaceSlug, issue.project, cycleId);
|
||||
},
|
||||
[EIssueActions.DELETE]: async (group_by: string | null, issue: IIssue) => {
|
||||
if (!workspaceSlug || !cycleId) return;
|
||||
|
||||
await cycleIssueStore.removeIssue(workspaceSlug, issue.project, issue.id, cycleId);
|
||||
fetchCycleWithId(workspaceSlug, issue.project, cycleId);
|
||||
},
|
||||
[EIssueActions.REMOVE]: async (group_by: string | null, issue: IIssue) => {
|
||||
if (!workspaceSlug || !cycleId || !issue.bridge_id) return;
|
||||
|
||||
await cycleIssueStore.removeIssueFromCycle(workspaceSlug, issue.project, cycleId, issue.id, issue.bridge_id);
|
||||
fetchCycleWithId(workspaceSlug, issue.project, cycleId);
|
||||
},
|
||||
};
|
||||
const getProjects = (projectStore: IProjectStore) => {
|
||||
|
@ -19,23 +19,30 @@ export const ModuleListLayout: React.FC = observer(() => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, moduleId } = router.query as { workspaceSlug: string; moduleId: string };
|
||||
|
||||
const { moduleIssues: moduleIssueStore, moduleIssuesFilter: moduleIssueFilterStore } = useMobxStore();
|
||||
const {
|
||||
moduleIssues: moduleIssueStore,
|
||||
moduleIssuesFilter: moduleIssueFilterStore,
|
||||
module: { fetchModuleDetails },
|
||||
} = useMobxStore();
|
||||
|
||||
const issueActions = {
|
||||
[EIssueActions.UPDATE]: async (group_by: string | null, issue: IIssue) => {
|
||||
if (!workspaceSlug || !moduleId) return;
|
||||
|
||||
await moduleIssueStore.updateIssue(workspaceSlug, issue.project, issue.id, issue, moduleId);
|
||||
fetchModuleDetails(workspaceSlug, issue.project, moduleId);
|
||||
},
|
||||
[EIssueActions.DELETE]: async (group_by: string | null, issue: IIssue) => {
|
||||
if (!workspaceSlug || !moduleId) return;
|
||||
|
||||
await moduleIssueStore.removeIssue(workspaceSlug, issue.project, issue.id, moduleId);
|
||||
fetchModuleDetails(workspaceSlug, issue.project, moduleId);
|
||||
},
|
||||
[EIssueActions.REMOVE]: async (group_by: string | null, issue: IIssue) => {
|
||||
if (!workspaceSlug || !moduleId || !issue.bridge_id) return;
|
||||
|
||||
await moduleIssueStore.removeIssueFromModule(workspaceSlug, issue.project, moduleId, issue.id, issue.bridge_id);
|
||||
fetchModuleDetails(workspaceSlug, issue.project, moduleId);
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -42,7 +42,7 @@ export const IssuePropertyAssignee: React.FC<IIssuePropertyAssignee> = observer(
|
||||
// store
|
||||
const {
|
||||
workspace: workspaceStore,
|
||||
projectMember: { projectMembers: _projectMembers, fetchProjectMembers },
|
||||
projectMember: { members: _members, fetchProjectMembers },
|
||||
} = useMobxStore();
|
||||
const workspaceSlug = workspaceStore?.workspaceSlug;
|
||||
// states
|
||||
@ -51,14 +51,14 @@ export const IssuePropertyAssignee: React.FC<IIssuePropertyAssignee> = observer(
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||
const [isLoading, setIsLoading] = useState<Boolean>(false);
|
||||
|
||||
const getWorkspaceMembers = () => {
|
||||
const getProjectMembers = () => {
|
||||
setIsLoading(true);
|
||||
if (workspaceSlug && projectId) fetchProjectMembers(workspaceSlug, projectId).then(() => setIsLoading(false));
|
||||
};
|
||||
|
||||
const updatedDefaultOptions: IProjectMember[] =
|
||||
defaultOptions.map((member: any) => ({ member: { ...member } })) ?? [];
|
||||
const projectMembers = _projectMembers ?? updatedDefaultOptions;
|
||||
const projectMembers = projectId && _members[projectId] ? _members[projectId] : updatedDefaultOptions;
|
||||
|
||||
const options = projectMembers?.map((member) => ({
|
||||
value: member.member.id,
|
||||
@ -100,7 +100,7 @@ export const IssuePropertyAssignee: React.FC<IIssuePropertyAssignee> = observer(
|
||||
|
||||
const label = (
|
||||
<Tooltip tooltipHeading="Assignee" tooltipContent={getTooltipContent()} position="top">
|
||||
<div className="flex h-full w-full cursor-pointer items-center gap-2 text-custom-text-200">
|
||||
<div className="flex h-full w-full items-center gap-2 text-custom-text-200">
|
||||
{value && value.length > 0 && Array.isArray(value) ? (
|
||||
<AvatarGroup showTooltip={false}>
|
||||
{value.map((assigneeId) => {
|
||||
@ -142,7 +142,10 @@ export const IssuePropertyAssignee: React.FC<IIssuePropertyAssignee> = observer(
|
||||
className={`flex w-full items-center justify-between gap-1 text-xs ${
|
||||
disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer"
|
||||
} ${buttonClassName}`}
|
||||
onClick={() => !projectMembers && getWorkspaceMembers()}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
(!projectId || !_members[projectId]) && getProjectMembers();
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
{!hideDropdownArrow && !disabled && <ChevronDown className="h-3 w-3" aria-hidden="true" />}
|
||||
@ -168,7 +171,7 @@ export const IssuePropertyAssignee: React.FC<IIssuePropertyAssignee> = observer(
|
||||
<div className={`mt-2 max-h-48 space-y-1 overflow-y-scroll`}>
|
||||
{isLoading ? (
|
||||
<p className="text-center text-custom-text-200">Loading...</p>
|
||||
) : filteredOptions.length > 0 ? (
|
||||
) : filteredOptions && filteredOptions.length > 0 ? (
|
||||
filteredOptions.map((option) => (
|
||||
<Combobox.Option
|
||||
key={option.value}
|
||||
@ -178,6 +181,7 @@ export const IssuePropertyAssignee: React.FC<IIssuePropertyAssignee> = observer(
|
||||
active && !selected ? "bg-custom-background-80" : ""
|
||||
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
|
||||
}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<>
|
||||
|
@ -12,11 +12,11 @@ import { Tooltip } from "@plane/ui";
|
||||
// hooks
|
||||
import useDynamicDropdownPosition from "hooks/use-dynamic-dropdown";
|
||||
// helpers
|
||||
import { renderDateFormat } from "helpers/date-time.helper";
|
||||
import { renderDateFormat, renderFormattedDate } from "helpers/date-time.helper";
|
||||
|
||||
export interface IIssuePropertyDate {
|
||||
value: any;
|
||||
onChange: (date: any) => void;
|
||||
value: string | null;
|
||||
onChange: (date: string | null) => void;
|
||||
disabled?: boolean;
|
||||
type: "start_date" | "target_date";
|
||||
}
|
||||
@ -56,32 +56,41 @@ export const IssuePropertyDate: React.FC<IIssuePropertyDate> = observer((props)
|
||||
return (
|
||||
<>
|
||||
<Popover.Button
|
||||
as="button"
|
||||
type="button"
|
||||
ref={dropdownBtn}
|
||||
className={`flex h-5 w-full items-center rounded border-[0.5px] border-custom-border-300 px-2.5 py-1 outline-none duration-300 ${
|
||||
disabled
|
||||
? "pointer-events-none cursor-not-allowed text-custom-text-200"
|
||||
: "cursor-pointer hover:bg-custom-background-80"
|
||||
}`}
|
||||
className="border-none outline-none"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex items-center justify-center gap-2 overflow-hidden">
|
||||
<dateOptionDetails.icon className="h-3 w-3" strokeWidth={2} />
|
||||
{value && (
|
||||
<>
|
||||
<Tooltip tooltipHeading={dateOptionDetails.placeholder} tooltipContent={value ?? "None"}>
|
||||
<div className="text-xs">{value}</div>
|
||||
</Tooltip>
|
||||
|
||||
<div
|
||||
className="flex flex-shrink-0 items-center justify-center"
|
||||
onClick={() => {
|
||||
if (onChange) onChange(null);
|
||||
}}
|
||||
>
|
||||
<X className="h-2.5 w-2.5" strokeWidth={2} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<Tooltip
|
||||
tooltipHeading={dateOptionDetails.placeholder}
|
||||
tooltipContent={value ? renderFormattedDate(value) : "None"}
|
||||
>
|
||||
<div
|
||||
className={`flex h-5 w-full items-center rounded border-[0.5px] border-custom-border-300 px-2.5 py-1 outline-none duration-300 ${
|
||||
disabled
|
||||
? "pointer-events-none cursor-not-allowed text-custom-text-200"
|
||||
: "cursor-pointer hover:bg-custom-background-80"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-center gap-2 overflow-hidden">
|
||||
<dateOptionDetails.icon className="h-3 w-3" strokeWidth={2} />
|
||||
{value && (
|
||||
<>
|
||||
<div className="text-xs">{value}</div>
|
||||
<div
|
||||
className="flex flex-shrink-0 items-center justify-center"
|
||||
onClick={() => {
|
||||
if (onChange) onChange(null);
|
||||
}}
|
||||
>
|
||||
<X className="h-2.5 w-2.5" strokeWidth={2} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Popover.Button>
|
||||
|
||||
<div className={`${open ? "fixed left-0 top-0 z-20 h-full w-full cursor-auto" : ""}`}>
|
||||
@ -92,7 +101,8 @@ export const IssuePropertyDate: React.FC<IIssuePropertyDate> = observer((props)
|
||||
{({ close }) => (
|
||||
<DatePicker
|
||||
selected={value ? new Date(value) : new Date()}
|
||||
onChange={(val: any) => {
|
||||
onChange={(val, e) => {
|
||||
e?.stopPropagation();
|
||||
if (onChange && val) {
|
||||
onChange(renderDateFormat(val));
|
||||
close();
|
||||
|
@ -116,6 +116,7 @@ export const IssuePropertyEstimates: React.FC<IIssuePropertyEstimates> = observe
|
||||
className={`flex h-5 w-full items-center justify-between gap-1 rounded border-[0.5px] border-custom-border-300 px-2.5 py-1 text-xs ${
|
||||
disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer hover:bg-custom-background-80"
|
||||
} ${buttonClassName}`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{label}
|
||||
{!hideDropdownArrow && !disabled && <ChevronDown className="h-3 w-3" aria-hidden="true" />}
|
||||
@ -150,6 +151,7 @@ export const IssuePropertyEstimates: React.FC<IIssuePropertyEstimates> = observe
|
||||
active ? "bg-custom-background-80" : ""
|
||||
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
|
||||
}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<>
|
||||
|
@ -107,7 +107,7 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
|
||||
{projectLabels
|
||||
?.filter((l) => value.includes(l.id))
|
||||
.map((label) => (
|
||||
<Tooltip position="top" tooltipHeading="Labels" tooltipContent={label.name ?? ""}>
|
||||
<Tooltip position="top" tooltipHeading="Label" tooltipContent={label.name ?? ""}>
|
||||
<div
|
||||
key={label.id}
|
||||
className={`flex overflow-hidden hover:bg-custom-background-80 ${
|
||||
@ -145,14 +145,16 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div
|
||||
className={`h-full flex items-center justify-center gap-2 rounded px-2.5 py-1 text-xs hover:bg-custom-background-80 ${
|
||||
noLabelBorder ? "" : "border-[0.5px] border-custom-border-300"
|
||||
}`}
|
||||
>
|
||||
<Tags className="h-3.5 w-3.5" strokeWidth={2} />
|
||||
{placeholderText}
|
||||
</div>
|
||||
<Tooltip position="top" tooltipHeading="Labels" tooltipContent="None">
|
||||
<div
|
||||
className={`h-full flex items-center justify-center gap-2 rounded px-2.5 py-1 text-xs hover:bg-custom-background-80 ${
|
||||
noLabelBorder ? "" : "border-[0.5px] border-custom-border-300"
|
||||
}`}
|
||||
>
|
||||
<Tags className="h-3.5 w-3.5" strokeWidth={2} />
|
||||
{placeholderText}
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@ -177,7 +179,10 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
|
||||
? "cursor-pointer"
|
||||
: "cursor-pointer hover:bg-custom-background-80"
|
||||
} ${buttonClassName}`}
|
||||
onClick={() => !storeLabels && fetchLabels()}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
!storeLabels && fetchLabels();
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
{!hideDropdownArrow && !disabled && <ChevronDown className="h-3 w-3" aria-hidden="true" />}
|
||||
@ -214,6 +219,7 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
|
||||
selected ? "text-custom-text-100" : "text-custom-text-200"
|
||||
}`
|
||||
}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<>
|
||||
|
@ -121,7 +121,10 @@ export const IssuePropertyState: React.FC<IIssuePropertyState> = observer((props
|
||||
className={`flex h-5 w-full items-center justify-between gap-1 rounded border-[0.5px] border-custom-border-300 px-2.5 py-1 text-xs ${
|
||||
disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer hover:bg-custom-background-80"
|
||||
} ${buttonClassName}`}
|
||||
onClick={() => !storeStates && fetchProjectStates()}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
!storeStates && fetchProjectStates();
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
{!hideDropdownArrow && !disabled && <ChevronDown className="h-3 w-3" aria-hidden="true" />}
|
||||
@ -157,6 +160,7 @@ export const IssuePropertyState: React.FC<IIssuePropertyState> = observer((props
|
||||
active ? "bg-custom-background-80" : ""
|
||||
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
|
||||
}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<>
|
||||
|
@ -58,7 +58,12 @@ export const AllIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
}}
|
||||
currentStore={EProjectStore.PROJECT}
|
||||
/>
|
||||
<CustomMenu placement="bottom-start" customButton={customActionButton} ellipsis>
|
||||
<CustomMenu
|
||||
placement="bottom-start"
|
||||
customButton={customActionButton}
|
||||
ellipsis
|
||||
menuButtonOnClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
|
@ -40,7 +40,12 @@ export const ArchivedIssueQuickActions: React.FC<IQuickActionProps> = (props) =>
|
||||
handleClose={() => setDeleteIssueModal(false)}
|
||||
onSubmit={handleDelete}
|
||||
/>
|
||||
<CustomMenu placement="bottom-start" customButton={customActionButton} ellipsis>
|
||||
<CustomMenu
|
||||
placement="bottom-start"
|
||||
customButton={customActionButton}
|
||||
ellipsis
|
||||
menuButtonOnClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
|
@ -58,7 +58,12 @@ export const CycleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
}}
|
||||
currentStore={EProjectStore.CYCLE}
|
||||
/>
|
||||
<CustomMenu placement="bottom-start" customButton={customActionButton} ellipsis>
|
||||
<CustomMenu
|
||||
placement="bottom-start"
|
||||
customButton={customActionButton}
|
||||
ellipsis
|
||||
menuButtonOnClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
|
@ -58,7 +58,13 @@ export const ModuleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
}}
|
||||
currentStore={EProjectStore.MODULE}
|
||||
/>
|
||||
<CustomMenu placement="bottom-start" customButton={customActionButton} ellipsis>
|
||||
|
||||
<CustomMenu
|
||||
placement="bottom-start"
|
||||
customButton={customActionButton}
|
||||
ellipsis
|
||||
menuButtonOnClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
|
@ -68,7 +68,12 @@ export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = (props) =>
|
||||
}}
|
||||
currentStore={EProjectStore.PROJECT}
|
||||
/>
|
||||
<CustomMenu placement="bottom-start" customButton={customActionButton} ellipsis>
|
||||
<CustomMenu
|
||||
placement="bottom-start"
|
||||
customButton={customActionButton}
|
||||
ellipsis
|
||||
menuButtonOnClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
|
@ -9,7 +9,7 @@ import { ArchivedIssueListLayout, ArchivedIssueAppliedFiltersRoot } from "compon
|
||||
|
||||
export const ArchivedIssueLayoutRoot: React.FC = observer(() => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string };
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const {
|
||||
projectArchivedIssues: { getIssues, fetchIssues },
|
||||
@ -18,8 +18,8 @@ export const ArchivedIssueLayoutRoot: React.FC = observer(() => {
|
||||
|
||||
useSWR(workspaceSlug && projectId ? `ARCHIVED_FILTERS_AND_ISSUES_${projectId.toString()}` : null, async () => {
|
||||
if (workspaceSlug && projectId) {
|
||||
await fetchFilters(workspaceSlug, projectId);
|
||||
await fetchIssues(workspaceSlug, projectId, getIssues ? "mutation" : "init-loader");
|
||||
await fetchFilters(workspaceSlug.toString(), projectId.toString());
|
||||
await fetchIssues(workspaceSlug.toString(), projectId.toString(), getIssues ? "mutation" : "init-loader");
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -24,11 +24,7 @@ export const CycleLayoutRoot: React.FC = observer(() => {
|
||||
const [transferIssuesModal, setTransferIssuesModal] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, cycleId } = router.query as {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
cycleId: string;
|
||||
};
|
||||
const { workspaceSlug, projectId, cycleId } = router.query;
|
||||
|
||||
const {
|
||||
cycle: cycleStore,
|
||||
@ -40,8 +36,13 @@ export const CycleLayoutRoot: React.FC = observer(() => {
|
||||
workspaceSlug && projectId && cycleId ? `CYCLE_ISSUES_V3_${workspaceSlug}_${projectId}_${cycleId}` : null,
|
||||
async () => {
|
||||
if (workspaceSlug && projectId && cycleId) {
|
||||
await fetchFilters(workspaceSlug, projectId, cycleId);
|
||||
await fetchIssues(workspaceSlug, projectId, getIssues ? "mutation" : "init-loader", cycleId);
|
||||
await fetchFilters(workspaceSlug.toString(), projectId.toString(), cycleId.toString());
|
||||
await fetchIssues(
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString(),
|
||||
getIssues ? "mutation" : "init-loader",
|
||||
cycleId.toString()
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
@ -69,7 +70,11 @@ export const CycleLayoutRoot: React.FC = observer(() => {
|
||||
) : (
|
||||
<>
|
||||
{Object.keys(getIssues ?? {}).length == 0 ? (
|
||||
<CycleEmptyState workspaceSlug={workspaceSlug} projectId={projectId} cycleId={cycleId} />
|
||||
<CycleEmptyState
|
||||
workspaceSlug={workspaceSlug?.toString()}
|
||||
projectId={projectId?.toString()}
|
||||
cycleId={cycleId?.toString()}
|
||||
/>
|
||||
) : (
|
||||
<div className="h-full w-full overflow-auto">
|
||||
{activeLayout === "list" ? (
|
||||
|
@ -11,7 +11,7 @@ import { DraftKanBanLayout } from "../kanban/roots/draft-issue-root";
|
||||
|
||||
export const DraftIssueLayoutRoot: React.FC = observer(() => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string };
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const {
|
||||
projectDraftIssuesFilter: { issueFilters, fetchFilters },
|
||||
@ -20,8 +20,8 @@ export const DraftIssueLayoutRoot: React.FC = observer(() => {
|
||||
|
||||
useSWR(workspaceSlug && projectId ? `DRAFT_FILTERS_AND_ISSUES_${projectId.toString()}` : null, async () => {
|
||||
if (workspaceSlug && projectId) {
|
||||
await fetchFilters(workspaceSlug, projectId);
|
||||
await fetchIssues(workspaceSlug, projectId, getIssues ? "mutation" : "init-loader");
|
||||
await fetchFilters(workspaceSlug.toString(), projectId.toString());
|
||||
await fetchIssues(workspaceSlug.toString(), projectId.toString(), getIssues ? "mutation" : "init-loader");
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -20,11 +20,7 @@ import { Spinner } from "@plane/ui";
|
||||
|
||||
export const ModuleLayoutRoot: React.FC = observer(() => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, moduleId } = router.query as {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
moduleId: string;
|
||||
};
|
||||
const { workspaceSlug, projectId, moduleId } = router.query;
|
||||
|
||||
const {
|
||||
moduleIssues: { loader, getIssues, fetchIssues },
|
||||
@ -35,8 +31,13 @@ export const ModuleLayoutRoot: React.FC = observer(() => {
|
||||
workspaceSlug && projectId && moduleId ? `MODULE_ISSUES_V3_${workspaceSlug}_${projectId}_${moduleId}` : null,
|
||||
async () => {
|
||||
if (workspaceSlug && projectId && moduleId) {
|
||||
await fetchFilters(workspaceSlug, projectId, moduleId);
|
||||
await fetchIssues(workspaceSlug, projectId, getIssues ? "mutation" : "init-loader", moduleId);
|
||||
await fetchFilters(workspaceSlug.toString(), projectId.toString(), moduleId.toString());
|
||||
await fetchIssues(
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString(),
|
||||
getIssues ? "mutation" : "init-loader",
|
||||
moduleId.toString()
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
@ -54,7 +55,11 @@ export const ModuleLayoutRoot: React.FC = observer(() => {
|
||||
) : (
|
||||
<>
|
||||
{Object.keys(getIssues ?? {}).length == 0 ? (
|
||||
<ModuleEmptyState workspaceSlug={workspaceSlug} projectId={projectId} moduleId={moduleId} />
|
||||
<ModuleEmptyState
|
||||
workspaceSlug={workspaceSlug?.toString()}
|
||||
projectId={projectId?.toString()}
|
||||
moduleId={moduleId?.toString()}
|
||||
/>
|
||||
) : (
|
||||
<div className="h-full w-full overflow-auto">
|
||||
{activeLayout === "list" ? (
|
||||
|
@ -19,7 +19,7 @@ import { Spinner } from "@plane/ui";
|
||||
export const ProjectLayoutRoot: React.FC = observer(() => {
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string };
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const {
|
||||
projectIssues: { loader, getIssues, fetchIssues },
|
||||
@ -28,8 +28,8 @@ export const ProjectLayoutRoot: React.FC = observer(() => {
|
||||
|
||||
useSWR(workspaceSlug && projectId ? `PROJECT_ISSUES_V3_${workspaceSlug}_${projectId}` : null, async () => {
|
||||
if (workspaceSlug && projectId) {
|
||||
await fetchFilters(workspaceSlug, projectId);
|
||||
await fetchIssues(workspaceSlug, projectId, getIssues ? "mutation" : "init-loader");
|
||||
await fetchFilters(workspaceSlug.toString(), projectId.toString());
|
||||
await fetchIssues(workspaceSlug.toString(), projectId.toString(), getIssues ? "mutation" : "init-loader");
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -18,11 +18,7 @@ import { Spinner } from "@plane/ui";
|
||||
|
||||
export const ProjectViewLayoutRoot: React.FC = observer(() => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, viewId } = router.query as {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
viewId?: string;
|
||||
};
|
||||
const { workspaceSlug, projectId, viewId } = router.query;
|
||||
|
||||
const {
|
||||
viewIssues: { loader, getIssues, fetchIssues },
|
||||
@ -31,8 +27,8 @@ export const ProjectViewLayoutRoot: React.FC = observer(() => {
|
||||
|
||||
useSWR(workspaceSlug && projectId && viewId ? `PROJECT_ISSUES_V3_${workspaceSlug}_${projectId}` : null, async () => {
|
||||
if (workspaceSlug && projectId && viewId) {
|
||||
await fetchFilters(workspaceSlug, projectId, viewId);
|
||||
await fetchIssues(workspaceSlug, projectId, getIssues ? "mutation" : "init-loader");
|
||||
await fetchFilters(workspaceSlug.toString(), projectId.toString(), viewId.toString());
|
||||
await fetchIssues(workspaceSlug.toString(), projectId.toString(), getIssues ? "mutation" : "init-loader");
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -10,7 +10,7 @@ import { IIssue, IUserLite } from "types";
|
||||
type Props = {
|
||||
issue: IIssue;
|
||||
members: IUserLite[] | undefined;
|
||||
onChange: (data: Partial<IIssue>) => void;
|
||||
onChange: (issue: IIssue, data: Partial<IIssue>) => void;
|
||||
expandedIssues: string[];
|
||||
disabled: boolean;
|
||||
};
|
||||
@ -18,7 +18,7 @@ type Props = {
|
||||
export const SpreadsheetAssigneeColumn: React.FC<Props> = ({ issue, members, onChange, expandedIssues, disabled }) => {
|
||||
const isExpanded = expandedIssues.indexOf(issue.id) > -1;
|
||||
|
||||
const { subIssues, isLoading } = useSubIssue(issue.project_detail?.id, issue.id, isExpanded);
|
||||
const { subIssues, isLoading, mutateSubIssues } = useSubIssue(issue.project_detail?.id, issue.id, isExpanded);
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -26,8 +26,13 @@ export const SpreadsheetAssigneeColumn: React.FC<Props> = ({ issue, members, onC
|
||||
projectId={issue.project_detail?.id ?? null}
|
||||
value={issue.assignees}
|
||||
defaultOptions={issue?.assignee_details ? issue.assignee_details : []}
|
||||
onChange={(data) => onChange({ assignees: data })}
|
||||
className="h-11 w-full border-b-[0.5px] border-custom-border-200"
|
||||
onChange={(data) => {
|
||||
onChange(issue, { assignees: data });
|
||||
if (issue.parent) {
|
||||
mutateSubIssues(issue, { assignees: data });
|
||||
}
|
||||
}}
|
||||
className="h-11 w-full border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80"
|
||||
buttonClassName="!shadow-none !border-0 h-full w-full px-2.5 py-1 "
|
||||
noLabelBorder
|
||||
hideDropdownArrow
|
||||
|
@ -18,7 +18,7 @@ export const SpreadsheetAttachmentColumn: React.FC<Props> = (props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex h-11 w-full items-center px-2.5 py-1 text-xs border-b-[0.5px] border-custom-border-200">
|
||||
<div className="flex h-11 w-full items-center px-2.5 py-1 text-xs border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80">
|
||||
{issue.attachment_count} {issue.attachment_count === 1 ? "attachment" : "attachments"}
|
||||
</div>
|
||||
|
||||
|
@ -19,7 +19,7 @@ export const SpreadsheetCreatedOnColumn: React.FC<Props> = ({ issue, expandedIss
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex h-11 w-full items-center justify-center text-xs border-b-[0.5px] border-custom-border-200">
|
||||
<div className="flex h-11 w-full items-center justify-center text-xs border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80">
|
||||
{renderLongDetailDateFormat(issue.created_at)}
|
||||
</div>
|
||||
|
||||
|
@ -9,7 +9,7 @@ import { IIssue } from "types";
|
||||
|
||||
type Props = {
|
||||
issue: IIssue;
|
||||
onChange: (data: Partial<IIssue>) => void;
|
||||
onChange: (issue: IIssue, data: Partial<IIssue>) => void;
|
||||
expandedIssues: string[];
|
||||
disabled: boolean;
|
||||
};
|
||||
@ -17,14 +17,19 @@ type Props = {
|
||||
export const SpreadsheetDueDateColumn: React.FC<Props> = ({ issue, onChange, expandedIssues, disabled }) => {
|
||||
const isExpanded = expandedIssues.indexOf(issue.id) > -1;
|
||||
|
||||
const { subIssues, isLoading } = useSubIssue(issue.project_detail?.id, issue.id, isExpanded);
|
||||
const { subIssues, isLoading, mutateSubIssues } = useSubIssue(issue.project_detail?.id, issue.id, isExpanded);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ViewDueDateSelect
|
||||
issue={issue}
|
||||
onChange={(val) => onChange({ target_date: val })}
|
||||
className="flex !h-11 !w-full max-w-full items-center px-2.5 py-1 border-b-[0.5px] border-custom-border-200"
|
||||
onChange={(val) => {
|
||||
onChange(issue, { target_date: val });
|
||||
if (issue.parent) {
|
||||
mutateSubIssues(issue, { target_date: val });
|
||||
}
|
||||
}}
|
||||
className="flex !h-11 !w-full max-w-full items-center px-2.5 py-1 border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80"
|
||||
noBorder
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
@ -7,7 +7,7 @@ import { IIssue } from "types";
|
||||
|
||||
type Props = {
|
||||
issue: IIssue;
|
||||
onChange: (formData: Partial<IIssue>) => void;
|
||||
onChange: (issue: IIssue, formData: Partial<IIssue>) => void;
|
||||
expandedIssues: string[];
|
||||
disabled: boolean;
|
||||
};
|
||||
@ -17,15 +17,20 @@ export const SpreadsheetEstimateColumn: React.FC<Props> = (props) => {
|
||||
|
||||
const isExpanded = expandedIssues.indexOf(issue.id) > -1;
|
||||
|
||||
const { subIssues, isLoading } = useSubIssue(issue.project_detail?.id, issue.id, isExpanded);
|
||||
const { subIssues, isLoading, mutateSubIssues } = useSubIssue(issue.project_detail?.id, issue.id, isExpanded);
|
||||
|
||||
return (
|
||||
<>
|
||||
<IssuePropertyEstimates
|
||||
projectId={issue.project_detail?.id ?? null}
|
||||
value={issue.estimate_point}
|
||||
onChange={(data) => onChange({ estimate_point: data })}
|
||||
className="h-11 w-full border-b-[0.5px] border-custom-border-200"
|
||||
onChange={(data) => {
|
||||
onChange(issue, { estimate_point: data });
|
||||
if (issue.parent) {
|
||||
mutateSubIssues(issue, { estimate_point: data });
|
||||
}
|
||||
}}
|
||||
className="h-11 w-full border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80"
|
||||
buttonClassName="h-full w-full px-2.5 py-1 !shadow-none !border-0"
|
||||
hideDropdownArrow
|
||||
disabled={disabled}
|
||||
|
@ -34,13 +34,17 @@ export const IssueColumn: React.FC<Props> = ({
|
||||
|
||||
const menuActionRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const handleIssuePeekOverview = (issue: IIssue) => {
|
||||
const handleIssuePeekOverview = (issue: IIssue, event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
||||
const { query } = router;
|
||||
|
||||
router.push({
|
||||
pathname: router.pathname,
|
||||
query: { ...query, peekIssueId: issue?.id, peekProjectId: issue?.project },
|
||||
});
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
const issueUrl = `/${issue.workspace_detail.slug}/projects/${issue.project_detail.id}/issues/${issue?.id}`;
|
||||
window.open(issueUrl, "_blank"); // Open link in a new tab
|
||||
} else {
|
||||
router.push({
|
||||
pathname: router.pathname,
|
||||
query: { ...query, peekIssueId: issue?.id, peekProjectId: issue?.project },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const paddingLeft = `${nestingLevel * 54}px`;
|
||||
@ -99,7 +103,7 @@ export const IssueColumn: React.FC<Props> = ({
|
||||
<Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
|
||||
<div
|
||||
className="h-full w-full cursor-pointer truncate px-4 py-2.5 text-left text-[0.825rem] text-custom-text-100"
|
||||
onClick={() => handleIssuePeekOverview(issue)}
|
||||
onClick={(e) => handleIssuePeekOverview(issue, e)}
|
||||
>
|
||||
{issue.name}
|
||||
</div>
|
||||
|
@ -9,7 +9,7 @@ import { IIssue, IIssueLabel } from "types";
|
||||
|
||||
type Props = {
|
||||
issue: IIssue;
|
||||
onChange: (formData: Partial<IIssue>) => void;
|
||||
onChange: (issue: IIssue, formData: Partial<IIssue>) => void;
|
||||
labels: IIssueLabel[] | undefined;
|
||||
expandedIssues: string[];
|
||||
disabled: boolean;
|
||||
@ -20,7 +20,7 @@ export const SpreadsheetLabelColumn: React.FC<Props> = (props) => {
|
||||
|
||||
const isExpanded = expandedIssues.indexOf(issue.id) > -1;
|
||||
|
||||
const { subIssues, isLoading } = useSubIssue(issue.project_detail?.id, issue.id, isExpanded);
|
||||
const { subIssues, isLoading, mutateSubIssues } = useSubIssue(issue.project_detail?.id, issue.id, isExpanded);
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -28,8 +28,13 @@ export const SpreadsheetLabelColumn: React.FC<Props> = (props) => {
|
||||
projectId={issue.project_detail?.id ?? null}
|
||||
value={issue.labels}
|
||||
defaultOptions={issue?.label_details ? issue.label_details : []}
|
||||
onChange={(data) => onChange({ labels: data })}
|
||||
className="h-11 w-full border-b-[0.5px] border-custom-border-200"
|
||||
onChange={(data) => {
|
||||
onChange(issue, { labels: data });
|
||||
if (issue.parent) {
|
||||
mutateSubIssues(issue, { assignees: data });
|
||||
}
|
||||
}}
|
||||
className="h-11 w-full border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80"
|
||||
buttonClassName="px-2.5 h-full"
|
||||
hideDropdownArrow
|
||||
maxRender={1}
|
||||
|
@ -18,7 +18,7 @@ export const SpreadsheetLinkColumn: React.FC<Props> = (props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex h-11 w-full items-center px-2.5 py-1 text-xs border-b-[0.5px] border-custom-border-200">
|
||||
<div className="flex h-11 w-full items-center px-2.5 py-1 text-xs border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80">
|
||||
{issue.link_count} {issue.link_count === 1 ? "link" : "links"}
|
||||
</div>
|
||||
|
||||
|
@ -9,7 +9,7 @@ import { IIssue } from "types";
|
||||
|
||||
type Props = {
|
||||
issue: IIssue;
|
||||
onChange: (data: Partial<IIssue>) => void;
|
||||
onChange: (issue: IIssue, data: Partial<IIssue>) => void;
|
||||
expandedIssues: string[];
|
||||
disabled: boolean;
|
||||
};
|
||||
@ -17,14 +17,19 @@ type Props = {
|
||||
export const SpreadsheetPriorityColumn: React.FC<Props> = ({ issue, onChange, expandedIssues, disabled }) => {
|
||||
const isExpanded = expandedIssues.indexOf(issue.id) > -1;
|
||||
|
||||
const { subIssues, isLoading } = useSubIssue(issue.project_detail?.id, issue.id, isExpanded);
|
||||
const { subIssues, isLoading, mutateSubIssues } = useSubIssue(issue.project_detail?.id, issue.id, isExpanded);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PrioritySelect
|
||||
value={issue.priority}
|
||||
onChange={(data) => onChange({ priority: data })}
|
||||
className="h-11 w-full border-b-[0.5px] border-custom-border-200"
|
||||
onChange={(data) => {
|
||||
onChange(issue, { priority: data });
|
||||
if (issue.parent) {
|
||||
mutateSubIssues(issue, { priority: data });
|
||||
}
|
||||
}}
|
||||
className="h-11 w-full border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80"
|
||||
buttonClassName="!shadow-none !border-0 h-full w-full px-2.5 py-1"
|
||||
showTitle
|
||||
highlightUrgentPriority={false}
|
||||
|
@ -9,7 +9,7 @@ import { IIssue } from "types";
|
||||
|
||||
type Props = {
|
||||
issue: IIssue;
|
||||
onChange: (formData: Partial<IIssue>) => void;
|
||||
onChange: (issue: IIssue, formData: Partial<IIssue>) => void;
|
||||
expandedIssues: string[];
|
||||
disabled: boolean;
|
||||
};
|
||||
@ -17,14 +17,19 @@ type Props = {
|
||||
export const SpreadsheetStartDateColumn: React.FC<Props> = ({ issue, onChange, expandedIssues, disabled }) => {
|
||||
const isExpanded = expandedIssues.indexOf(issue.id) > -1;
|
||||
|
||||
const { subIssues, isLoading } = useSubIssue(issue.project_detail?.id, issue.id, isExpanded);
|
||||
const { subIssues, isLoading, mutateSubIssues } = useSubIssue(issue.project_detail?.id, issue.id, isExpanded);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ViewStartDateSelect
|
||||
issue={issue}
|
||||
onChange={(val) => onChange({ start_date: val })}
|
||||
className="flex !h-11 !w-full max-w-full items-center px-2.5 py-1 border-b-[0.5px] border-custom-border-200"
|
||||
onChange={(val) => {
|
||||
onChange(issue, { start_date: val });
|
||||
if (issue.parent) {
|
||||
mutateSubIssues(issue, { start_date: val });
|
||||
}
|
||||
}}
|
||||
className="flex !h-11 !w-full max-w-full items-center px-2.5 py-1 border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80"
|
||||
noBorder
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
@ -9,7 +9,7 @@ import { IIssue, IState } from "types";
|
||||
|
||||
type Props = {
|
||||
issue: IIssue;
|
||||
onChange: (data: Partial<IIssue>) => void;
|
||||
onChange: (issue: IIssue, data: Partial<IIssue>) => void;
|
||||
states: IState[] | undefined;
|
||||
expandedIssues: string[];
|
||||
disabled: boolean;
|
||||
@ -20,7 +20,7 @@ export const SpreadsheetStateColumn: React.FC<Props> = (props) => {
|
||||
|
||||
const isExpanded = expandedIssues.indexOf(issue.id) > -1;
|
||||
|
||||
const { subIssues, isLoading } = useSubIssue(issue.project_detail?.id, issue.id, isExpanded);
|
||||
const { subIssues, isLoading, mutateSubIssues } = useSubIssue(issue.project_detail?.id, issue.id, isExpanded);
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -28,7 +28,12 @@ export const SpreadsheetStateColumn: React.FC<Props> = (props) => {
|
||||
projectId={issue.project_detail?.id ?? null}
|
||||
value={issue.state}
|
||||
defaultOptions={issue?.state_detail ? [issue.state_detail] : []}
|
||||
onChange={(data) => onChange({ state: data.id, state_detail: data })}
|
||||
onChange={(data) => {
|
||||
onChange(issue, { state: data.id, state_detail: data });
|
||||
if (issue.parent) {
|
||||
mutateSubIssues(issue, { state: data.id, state_detail: data });
|
||||
}
|
||||
}}
|
||||
className="w-full !h-11 border-b-[0.5px] border-custom-border-200"
|
||||
buttonClassName="!shadow-none !border-0 h-full w-full"
|
||||
hideDropdownArrow
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user