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