forked from github/plane
Merge pull request #3171 from makeplane/develop
Promote: develop change to preview
This commit is contained in:
commit
6f2cce081f
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?
|
||||||
|
|
||||||
|
@ -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,6 +1034,16 @@ 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"
|
||||||
|
if (
|
||||||
|
hasattr(settings, "AWS_S3_CUSTOM_DOMAIN")
|
||||||
|
and settings.AWS_S3_CUSTOM_DOMAIN
|
||||||
|
and hasattr(settings, "AWS_S3_URL_PROTOCOL")
|
||||||
|
and settings.AWS_S3_URL_PROTOCOL
|
||||||
|
):
|
||||||
|
files.append(
|
||||||
|
f"{settings.AWS_S3_URL_PROTOCOL}//{settings.AWS_S3_CUSTOM_DOMAIN}/{content['Key']}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
files.append(
|
files.append(
|
||||||
f"https://{settings.AWS_STORAGE_BUCKET_NAME}.s3.{settings.AWS_REGION}.amazonaws.com/{content['Key']}"
|
f"https://{settings.AWS_STORAGE_BUCKET_NAME}.s3.{settings.AWS_REGION}.amazonaws.com/{content['Key']}"
|
||||||
)
|
)
|
||||||
|
@ -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(
|
||||||
|
@ -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,6 +63,7 @@ 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
|
<a
|
||||||
href={link.url}
|
href={link.url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@ -71,6 +72,7 @@ export const LinksList: React.FC<Props> = ({ links, handleDeleteLink, handleEdit
|
|||||||
>
|
>
|
||||||
<ExternalLinkIcon className="h-3 w-3 stroke-[1.5] text-custom-text-200" />
|
<ExternalLinkIcon className="h-3 w-3 stroke-[1.5] text-custom-text-200" />
|
||||||
</a>
|
</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,9 +84,9 @@ 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">
|
||||||
Added {timeAgo(link.created_at)}
|
Added {timeAgo(link.created_at)}
|
||||||
|
@ -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,19 +147,10 @@ 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 ?? ""),
|
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@ -200,16 +198,10 @@ 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" />
|
||||||
|
@ -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}`)}
|
||||||
>
|
>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<ContrastIcon className="h-3 w-3" />
|
||||||
{truncateText(cycle.name, 40)}
|
{truncateText(cycle.name, 40)}
|
||||||
|
</div>
|
||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
))}
|
))}
|
||||||
</CustomMenu>
|
</CustomMenu>
|
||||||
@ -192,10 +195,12 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
|||||||
handleDisplayPropertiesUpdate={handleDisplayProperties}
|
handleDisplayPropertiesUpdate={handleDisplayProperties}
|
||||||
/>
|
/>
|
||||||
</FiltersDropdown>
|
</FiltersDropdown>
|
||||||
|
|
||||||
|
{canUserCreateIssue && (
|
||||||
|
<>
|
||||||
<Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
|
<Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
|
||||||
Analytics
|
Analytics
|
||||||
</Button>
|
</Button>
|
||||||
{canUserCreateIssue && (
|
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setTrackElement("CYCLE_PAGE_HEADER");
|
setTrackElement("CYCLE_PAGE_HEADER");
|
||||||
@ -206,6 +211,7 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
|||||||
>
|
>
|
||||||
Add Issue
|
Add Issue
|
||||||
</Button>
|
</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}`)}
|
||||||
>
|
>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<DiceIcon className="h-3 w-3" />
|
||||||
{truncateText(module.name, 40)}
|
{truncateText(module.name, 40)}
|
||||||
|
</div>
|
||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
))}
|
))}
|
||||||
</CustomMenu>
|
</CustomMenu>
|
||||||
@ -193,10 +196,12 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
|||||||
handleDisplayPropertiesUpdate={handleDisplayProperties}
|
handleDisplayPropertiesUpdate={handleDisplayProperties}
|
||||||
/>
|
/>
|
||||||
</FiltersDropdown>
|
</FiltersDropdown>
|
||||||
|
|
||||||
|
{canUserCreateIssue && (
|
||||||
|
<>
|
||||||
<Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
|
<Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
|
||||||
Analytics
|
Analytics
|
||||||
</Button>
|
</Button>
|
||||||
{canUserCreateIssue && (
|
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setTrackElement("MODULE_PAGE_HEADER");
|
setTrackElement("MODULE_PAGE_HEADER");
|
||||||
@ -207,6 +212,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
|||||||
>
|
>
|
||||||
Add Issue
|
Add Issue
|
||||||
</Button>
|
</Button>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -202,10 +202,12 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
|
|||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{canUserCreateIssue && (
|
||||||
|
<>
|
||||||
<Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
|
<Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
|
||||||
Analytics
|
Analytics
|
||||||
</Button>
|
</Button>
|
||||||
{canUserCreateIssue && (
|
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setTrackElement("PROJECT_PAGE_HEADER");
|
setTrackElement("PROJECT_PAGE_HEADER");
|
||||||
@ -216,6 +218,7 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
|
|||||||
>
|
>
|
||||||
Add Issue
|
Add Issue
|
||||||
</Button>
|
</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}`)}
|
||||||
>
|
>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<PhotoFilterIcon height={12} width={12} />
|
||||||
{truncateText(view.name, 40)}
|
{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,6 +59,7 @@ export const ProjectViewsHeader: React.FC = observer(() => {
|
|||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{canUserCreateIssue && (
|
||||||
<div className="flex flex-shrink-0 items-center gap-2">
|
<div className="flex flex-shrink-0 items-center gap-2">
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
@ -62,6 +72,7 @@ export const ProjectViewsHeader: React.FC = observer(() => {
|
|||||||
</Button>
|
</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,7 +49,7 @@ export const ProjectsHeader = observer(() => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{isAuthorizedUser && (
|
||||||
<Button
|
<Button
|
||||||
prependIcon={<Plus />}
|
prependIcon={<Plus />}
|
||||||
size="sm"
|
size="sm"
|
||||||
@ -55,6 +60,7 @@ export const ProjectsHeader = observer(() => {
|
|||||||
>
|
>
|
||||||
Add Project
|
Add Project
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -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,6 +93,7 @@ export const IssueAttachments = () => {
|
|||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
|
{editable && (
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setDeleteAttachment(file);
|
setDeleteAttachment(file);
|
||||||
@ -94,6 +102,7 @@ export const IssueAttachments = () => {
|
|||||||
>
|
>
|
||||||
<X className="h-4 w-4 text-custom-text-200 hover:text-custom-text-100" />
|
<X className="h-4 w-4 text-custom-text-200 hover:text-custom-text-100" />
|
||||||
</button>
|
</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) {
|
||||||
|
const issueUrl = `/${issue.workspace_detail.slug}/projects/${issue.project_detail.id}/issues/${issue?.id}`;
|
||||||
|
window.open(issueUrl, "_blank"); // Open link in a new tab
|
||||||
|
} else {
|
||||||
router.push({
|
router.push({
|
||||||
pathname: router.pathname,
|
pathname: router.pathname,
|
||||||
query: { ...query, peekIssueId: issue?.id, peekProjectId: issue?.project },
|
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>
|
||||||
|
@ -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={
|
||||||
|
isEditingAllowed
|
||||||
|
? {
|
||||||
text: "Create your first issue",
|
text: "Create your first issue",
|
||||||
icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />,
|
icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
setTrackElement("PROJECT_EMPTY_STATE");
|
setTrackElement("PROJECT_EMPTY_STATE");
|
||||||
commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.PROJECT);
|
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,11 +101,13 @@ 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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{isEditingAllowed && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
|
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
|
||||||
@ -98,10 +115,12 @@ export const AppliedFiltersList: React.FC<Props> = observer((props) => {
|
|||||||
>
|
>
|
||||||
<X size={12} strokeWidth={2} />
|
<X size={12} strokeWidth={2} />
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
{isEditingAllowed && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleClearAllFilters}
|
onClick={handleClearAllFilters}
|
||||||
@ -110,6 +129,7 @@ export const AppliedFiltersList: React.FC<Props> = observer((props) => {
|
|||||||
Clear all
|
Clear all
|
||||||
<X size={12} strokeWidth={2} />
|
<X size={12} strokeWidth={2} />
|
||||||
</button>
|
</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,6 +31,7 @@ export const AppliedLabelsFilters: React.FC<Props> = observer((props) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<span className="normal-case">{labelDetails.name}</span>
|
<span className="normal-case">{labelDetails.name}</span>
|
||||||
|
{editable && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
|
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
|
||||||
@ -37,6 +39,7 @@ export const AppliedLabelsFilters: React.FC<Props> = observer((props) => {
|
|||||||
>
|
>
|
||||||
<X size={10} strokeWidth={2} />
|
<X size={10} strokeWidth={2} />
|
||||||
</button>
|
</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,6 +26,7 @@ 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>
|
||||||
|
{editable && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
|
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
|
||||||
@ -32,6 +34,7 @@ export const AppliedMembersFilters: React.FC<Props> = observer((props) => {
|
|||||||
>
|
>
|
||||||
<X size={10} strokeWidth={2} />
|
<X size={10} strokeWidth={2} />
|
||||||
</button>
|
</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,6 +21,7 @@ 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}
|
||||||
|
{editable && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
|
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
|
||||||
@ -27,6 +29,7 @@ export const AppliedPriorityFilters: React.FC<Props> = observer((props) => {
|
|||||||
>
|
>
|
||||||
<X size={10} strokeWidth={2} />
|
<X size={10} strokeWidth={2} />
|
||||||
</button>
|
</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,6 +35,7 @@ export const AppliedProjectFilters: React.FC<Props> = observer((props) => {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<span className="normal-case">{projectDetails.name}</span>
|
<span className="normal-case">{projectDetails.name}</span>
|
||||||
|
{editable && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
|
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
|
||||||
@ -41,6 +43,7 @@ export const AppliedProjectFilters: React.FC<Props> = observer((props) => {
|
|||||||
>
|
>
|
||||||
<X size={10} strokeWidth={2} />
|
<X size={10} strokeWidth={2} />
|
||||||
</button>
|
</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,6 +27,7 @@ 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}
|
||||||
|
{editable && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
|
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
|
||||||
@ -33,6 +35,7 @@ export const AppliedStateFilters: React.FC<Props> = observer((props) => {
|
|||||||
>
|
>
|
||||||
<X size={10} strokeWidth={2} />
|
<X size={10} strokeWidth={2} />
|
||||||
</button>
|
</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) {
|
||||||
|
const issueUrl = `/${data?.workspace_detail.slug}/projects/${data?.project_detail.id}/issues/${data?.id}`;
|
||||||
|
window.open(issueUrl, "_blank"); // Open link in a new tab
|
||||||
|
} else {
|
||||||
router.push({
|
router.push({
|
||||||
pathname: router.pathname,
|
pathname: router.pathname,
|
||||||
query: { ...query, peekIssueId: data?.id, peekProjectId: data?.project },
|
query: { ...query, peekIssueId: data?.id, peekProjectId: data?.project },
|
||||||
});
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -4,15 +4,43 @@ 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 } = useMobxStore();
|
||||||
|
|
||||||
|
const issueActions = {
|
||||||
|
[EIssueActions.UPDATE]: async (issue: IIssue) => {
|
||||||
|
if (!workspaceSlug || !cycleId) return;
|
||||||
|
|
||||||
|
await cycleIssueStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue, cycleId.toString());
|
||||||
|
},
|
||||||
|
[EIssueActions.DELETE]: async (issue: IIssue) => {
|
||||||
|
if (!workspaceSlug || !cycleId) return;
|
||||||
|
|
||||||
|
await cycleIssueStore.removeIssue(workspaceSlug.toString(), issue.project, issue.id, 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
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseGanttRoot
|
<BaseGanttRoot
|
||||||
|
issueActions={issueActions}
|
||||||
issueFiltersStore={cycleIssueFilterStore}
|
issueFiltersStore={cycleIssueFilterStore}
|
||||||
issueStore={cycleIssueStore}
|
issueStore={cycleIssueStore}
|
||||||
viewId={cycleId?.toString()}
|
viewId={cycleId?.toString()}
|
||||||
|
@ -4,15 +4,43 @@ 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 } = useMobxStore();
|
||||||
|
|
||||||
|
const issueActions = {
|
||||||
|
[EIssueActions.UPDATE]: async (issue: IIssue) => {
|
||||||
|
if (!workspaceSlug || !moduleId) return;
|
||||||
|
|
||||||
|
await moduleIssueStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue, moduleId.toString());
|
||||||
|
},
|
||||||
|
[EIssueActions.DELETE]: async (issue: IIssue) => {
|
||||||
|
if (!workspaceSlug || !moduleId) return;
|
||||||
|
|
||||||
|
await moduleIssueStore.removeIssue(workspaceSlug.toString(), issue.project, issue.id, 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
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
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) {
|
||||||
|
const issueUrl = `/${issue.workspace_detail.slug}/projects/${issue.project_detail.id}/issues/${issue?.id}`;
|
||||||
|
window.open(issueUrl, "_blank"); // Open link in a new tab
|
||||||
|
} else {
|
||||||
router.push({
|
router.push({
|
||||||
pathname: router.pathname,
|
pathname: router.pathname,
|
||||||
query: { ...query, peekIssueId: issue?.id, peekProjectId: issue?.project },
|
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,11 +151,6 @@ 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
|
|
||||||
className={`space-y-2 rounded border-[0.5px] border-custom-border-200 bg-custom-background-100 px-3 py-2 text-sm shadow-custom-shadow-2xs transition-all ${
|
|
||||||
isDragDisabled ? "" : "hover:cursor-grab"
|
|
||||||
} ${snapshot.isDragging ? `border-custom-primary-100` : `border-transparent`}`}
|
|
||||||
>
|
|
||||||
<KanbanIssueMemoBlock
|
<KanbanIssueMemoBlock
|
||||||
sub_group_id={sub_group_id}
|
sub_group_id={sub_group_id}
|
||||||
columnId={columnId}
|
columnId={columnId}
|
||||||
@ -146,9 +160,10 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = (props) => {
|
|||||||
quickActions={quickActions}
|
quickActions={quickActions}
|
||||||
displayProperties={displayProperties}
|
displayProperties={displayProperties}
|
||||||
isReadOnly={!canEditIssueProperties}
|
isReadOnly={!canEditIssueProperties}
|
||||||
|
snapshot={snapshot}
|
||||||
|
isDragDisabled={isDragDisabled}
|
||||||
/>
|
/>
|
||||||
</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"
|
||||||
/>
|
/>
|
||||||
|
@ -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) {
|
||||||
|
const issueUrl = `/${issue.workspace_detail.slug}/projects/${issue.project_detail.id}/issues/${issue?.id}`;
|
||||||
|
window.open(issueUrl, "_blank"); // Open link in a new tab
|
||||||
|
} else {
|
||||||
router.push({
|
router.push({
|
||||||
pathname: router.pathname,
|
pathname: router.pathname,
|
||||||
query: { ...query, peekIssueId: issue?.id, peekProjectId: issue?.project },
|
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"
|
||||||
/>
|
/>
|
||||||
|
@ -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,7 +56,17 @@ export const IssuePropertyDate: React.FC<IIssuePropertyDate> = observer((props)
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Popover.Button
|
<Popover.Button
|
||||||
|
as="button"
|
||||||
|
type="button"
|
||||||
ref={dropdownBtn}
|
ref={dropdownBtn}
|
||||||
|
className="border-none outline-none"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<Tooltip
|
||||||
|
tooltipHeading={dateOptionDetails.placeholder}
|
||||||
|
tooltipContent={value ? renderFormattedDate(value) : "None"}
|
||||||
|
>
|
||||||
|
<div
|
||||||
className={`flex h-5 w-full items-center rounded border-[0.5px] border-custom-border-300 px-2.5 py-1 outline-none duration-300 ${
|
className={`flex h-5 w-full items-center rounded border-[0.5px] border-custom-border-300 px-2.5 py-1 outline-none duration-300 ${
|
||||||
disabled
|
disabled
|
||||||
? "pointer-events-none cursor-not-allowed text-custom-text-200"
|
? "pointer-events-none cursor-not-allowed text-custom-text-200"
|
||||||
@ -67,10 +77,7 @@ export const IssuePropertyDate: React.FC<IIssuePropertyDate> = observer((props)
|
|||||||
<dateOptionDetails.icon className="h-3 w-3" strokeWidth={2} />
|
<dateOptionDetails.icon className="h-3 w-3" strokeWidth={2} />
|
||||||
{value && (
|
{value && (
|
||||||
<>
|
<>
|
||||||
<Tooltip tooltipHeading={dateOptionDetails.placeholder} tooltipContent={value ?? "None"}>
|
|
||||||
<div className="text-xs">{value}</div>
|
<div className="text-xs">{value}</div>
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="flex flex-shrink-0 items-center justify-center"
|
className="flex flex-shrink-0 items-center justify-center"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -82,6 +89,8 @@ export const IssuePropertyDate: React.FC<IIssuePropertyDate> = observer((props)
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</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,6 +145,7 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
|
<Tooltip position="top" tooltipHeading="Labels" tooltipContent="None">
|
||||||
<div
|
<div
|
||||||
className={`h-full flex items-center justify-center gap-2 rounded px-2.5 py-1 text-xs hover:bg-custom-background-80 ${
|
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"
|
noLabelBorder ? "" : "border-[0.5px] border-custom-border-300"
|
||||||
@ -153,6 +154,7 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
|
|||||||
<Tags className="h-3.5 w-3.5" strokeWidth={2} />
|
<Tags className="h-3.5 w-3.5" strokeWidth={2} />
|
||||||
{placeholderText}
|
{placeholderText}
|
||||||
</div>
|
</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) {
|
||||||
|
const issueUrl = `/${issue.workspace_detail.slug}/projects/${issue.project_detail.id}/issues/${issue?.id}`;
|
||||||
|
window.open(issueUrl, "_blank"); // Open link in a new tab
|
||||||
|
} else {
|
||||||
router.push({
|
router.push({
|
||||||
pathname: router.pathname,
|
pathname: router.pathname,
|
||||||
query: { ...query, peekIssueId: issue?.id, peekProjectId: issue?.project },
|
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
|
||||||
|
@ -18,7 +18,7 @@ export const SpreadsheetSubIssueColumn: 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.sub_issues_count} {issue.sub_issues_count === 1 ? "sub-issue" : "sub-issues"}
|
{issue.sub_issues_count} {issue.sub_issues_count === 1 ? "sub-issue" : "sub-issues"}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ export const SpreadsheetUpdatedOnColumn: React.FC<Props> = (props) => {
|
|||||||
|
|
||||||
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.updated_at)}
|
{renderLongDetailDateFormat(issue.updated_at)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -163,18 +163,13 @@ export const SpreadsheetColumn: React.FC<Props> = (props) => {
|
|||||||
{issues?.map((issue) => {
|
{issues?.map((issue) => {
|
||||||
const disableUserActions = !canEditProperties(issue.project);
|
const disableUserActions = !canEditProperties(issue.project);
|
||||||
return (
|
return (
|
||||||
<div
|
<div key={`${property}-${issue.id}`} className={`h-fit ${disableUserActions ? "" : "cursor-pointer"}`}>
|
||||||
key={`${property}-${issue.id}`}
|
|
||||||
className={`h-fit ${
|
|
||||||
disableUserActions ? "" : "cursor-pointer hover:bg-custom-background-80"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{property === "state" ? (
|
{property === "state" ? (
|
||||||
<SpreadsheetStateColumn
|
<SpreadsheetStateColumn
|
||||||
disabled={disableUserActions}
|
disabled={disableUserActions}
|
||||||
expandedIssues={expandedIssues}
|
expandedIssues={expandedIssues}
|
||||||
issue={issue}
|
issue={issue}
|
||||||
onChange={(data: Partial<IIssue>) => handleUpdateIssue(issue, data)}
|
onChange={(issue: IIssue, data: Partial<IIssue>) => handleUpdateIssue(issue, data)}
|
||||||
states={states}
|
states={states}
|
||||||
/>
|
/>
|
||||||
) : property === "priority" ? (
|
) : property === "priority" ? (
|
||||||
@ -182,14 +177,14 @@ export const SpreadsheetColumn: React.FC<Props> = (props) => {
|
|||||||
disabled={disableUserActions}
|
disabled={disableUserActions}
|
||||||
expandedIssues={expandedIssues}
|
expandedIssues={expandedIssues}
|
||||||
issue={issue}
|
issue={issue}
|
||||||
onChange={(data: Partial<IIssue>) => handleUpdateIssue(issue, data)}
|
onChange={(issue: IIssue, data: Partial<IIssue>) => handleUpdateIssue(issue, data)}
|
||||||
/>
|
/>
|
||||||
) : property === "estimate" ? (
|
) : property === "estimate" ? (
|
||||||
<SpreadsheetEstimateColumn
|
<SpreadsheetEstimateColumn
|
||||||
disabled={disableUserActions}
|
disabled={disableUserActions}
|
||||||
expandedIssues={expandedIssues}
|
expandedIssues={expandedIssues}
|
||||||
issue={issue}
|
issue={issue}
|
||||||
onChange={(data: Partial<IIssue>) => handleUpdateIssue(issue, data)}
|
onChange={(issue: IIssue, data: Partial<IIssue>) => handleUpdateIssue(issue, data)}
|
||||||
/>
|
/>
|
||||||
) : property === "assignee" ? (
|
) : property === "assignee" ? (
|
||||||
<SpreadsheetAssigneeColumn
|
<SpreadsheetAssigneeColumn
|
||||||
@ -197,7 +192,7 @@ export const SpreadsheetColumn: React.FC<Props> = (props) => {
|
|||||||
expandedIssues={expandedIssues}
|
expandedIssues={expandedIssues}
|
||||||
issue={issue}
|
issue={issue}
|
||||||
members={members}
|
members={members}
|
||||||
onChange={(data: Partial<IIssue>) => handleUpdateIssue(issue, data)}
|
onChange={(issue: IIssue, data: Partial<IIssue>) => handleUpdateIssue(issue, data)}
|
||||||
/>
|
/>
|
||||||
) : property === "labels" ? (
|
) : property === "labels" ? (
|
||||||
<SpreadsheetLabelColumn
|
<SpreadsheetLabelColumn
|
||||||
@ -205,21 +200,21 @@ export const SpreadsheetColumn: React.FC<Props> = (props) => {
|
|||||||
expandedIssues={expandedIssues}
|
expandedIssues={expandedIssues}
|
||||||
issue={issue}
|
issue={issue}
|
||||||
labels={labels}
|
labels={labels}
|
||||||
onChange={(data: Partial<IIssue>) => handleUpdateIssue(issue, data)}
|
onChange={(issue: IIssue, data: Partial<IIssue>) => handleUpdateIssue(issue, data)}
|
||||||
/>
|
/>
|
||||||
) : property === "start_date" ? (
|
) : property === "start_date" ? (
|
||||||
<SpreadsheetStartDateColumn
|
<SpreadsheetStartDateColumn
|
||||||
disabled={disableUserActions}
|
disabled={disableUserActions}
|
||||||
expandedIssues={expandedIssues}
|
expandedIssues={expandedIssues}
|
||||||
issue={issue}
|
issue={issue}
|
||||||
onChange={(data: Partial<IIssue>) => handleUpdateIssue(issue, data)}
|
onChange={(issue: IIssue, data: Partial<IIssue>) => handleUpdateIssue(issue, data)}
|
||||||
/>
|
/>
|
||||||
) : property === "due_date" ? (
|
) : property === "due_date" ? (
|
||||||
<SpreadsheetDueDateColumn
|
<SpreadsheetDueDateColumn
|
||||||
disabled={disableUserActions}
|
disabled={disableUserActions}
|
||||||
expandedIssues={expandedIssues}
|
expandedIssues={expandedIssues}
|
||||||
issue={issue}
|
issue={issue}
|
||||||
onChange={(data: Partial<IIssue>) => handleUpdateIssue(issue, data)}
|
onChange={(issue: IIssue, data: Partial<IIssue>) => handleUpdateIssue(issue, data)}
|
||||||
/>
|
/>
|
||||||
) : property === "created_on" ? (
|
) : property === "created_on" ? (
|
||||||
<SpreadsheetCreatedOnColumn expandedIssues={expandedIssues} issue={issue} />
|
<SpreadsheetCreatedOnColumn expandedIssues={expandedIssues} issue={issue} />
|
||||||
|
@ -194,7 +194,7 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
|
|||||||
workspaceSlug={workspaceSlug.toString()}
|
workspaceSlug={workspaceSlug.toString()}
|
||||||
projectId={peekProjectId.toString()}
|
projectId={peekProjectId.toString()}
|
||||||
issueId={peekIssueId.toString()}
|
issueId={peekIssueId.toString()}
|
||||||
handleIssue={async (issueToUpdate: any) => await handleIssues(issueToUpdate, EIssueActions.UPDATE)}
|
handleIssue={async (issueToUpdate: any, action: EIssueActions) => await handleIssues(issueToUpdate, action)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -41,7 +41,7 @@ const issueService = new IssueService();
|
|||||||
const issueCommentService = new IssueCommentService();
|
const issueCommentService = new IssueCommentService();
|
||||||
|
|
||||||
export const IssueMainContent: React.FC<Props> = observer((props) => {
|
export const IssueMainContent: React.FC<Props> = observer((props) => {
|
||||||
const { issueDetails, submitChanges, uneditable = false } = props;
|
const { issueDetails, submitChanges, uneditable } = props;
|
||||||
// states
|
// states
|
||||||
const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved");
|
const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved");
|
||||||
// router
|
// router
|
||||||
@ -152,7 +152,9 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const isAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
|
const isAllowed =
|
||||||
|
(!!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER) ||
|
||||||
|
(uneditable !== undefined && !uneditable);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -216,7 +218,7 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
|
|||||||
</CustomMenu>
|
</CustomMenu>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<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"
|
||||||
@ -232,7 +234,7 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
|
|||||||
workspaceSlug={workspaceSlug as string}
|
workspaceSlug={workspaceSlug as string}
|
||||||
issue={issueDetails}
|
issue={issueDetails}
|
||||||
handleFormSubmit={submitChanges}
|
handleFormSubmit={submitChanges}
|
||||||
isAllowed={isAllowed || !uneditable}
|
isAllowed={isAllowed}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{workspaceSlug && projectId && (
|
{workspaceSlug && projectId && (
|
||||||
@ -250,8 +252,8 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
|
|||||||
<div className="flex flex-col gap-3 py-3">
|
<div className="flex flex-col gap-3 py-3">
|
||||||
<h3 className="text-lg">Attachments</h3>
|
<h3 className="text-lg">Attachments</h3>
|
||||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
|
||||||
<IssueAttachmentUpload disabled={uneditable} />
|
<IssueAttachmentUpload disabled={!isAllowed} />
|
||||||
<IssueAttachments />
|
<IssueAttachments editable={isAllowed} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-5 pt-3">
|
<div className="space-y-5 pt-3">
|
||||||
@ -264,7 +266,7 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
|
|||||||
/>
|
/>
|
||||||
<AddComment
|
<AddComment
|
||||||
onSubmit={handleAddComment}
|
onSubmit={handleAddComment}
|
||||||
disabled={uneditable}
|
disabled={!isAllowed}
|
||||||
showAccessSpecifier={projectDetails && projectDetails.is_deployed}
|
showAccessSpecifier={projectDetails && projectDetails.is_deployed}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -153,10 +153,12 @@ export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = (props) =
|
|||||||
debouncedFormSave();
|
debouncedFormSave();
|
||||||
}}
|
}}
|
||||||
required={true}
|
required={true}
|
||||||
className="min-h-10 block w-full resize-none overflow-hidden rounded border-none bg-transparent !p-0 text-xl outline-none ring-0 focus:!px-3 focus:!py-2 focus:ring-1 focus:ring-custom-primary"
|
className={`min-h-10 block w-full resize-none overflow-hidden rounded border-none bg-transparent !p-0 text-xl outline-none ring-0 focus:!px-3 focus:!py-2 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={!true}
|
disabled={!isAllowed}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@ -188,7 +190,9 @@ export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = (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);
|
||||||
|
@ -47,7 +47,7 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
|
|||||||
} = useMobxStore();
|
} = useMobxStore();
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, peekProjectId: projectId } = router.query;
|
||||||
|
|
||||||
const handleState = (_state: string) => {
|
const handleState = (_state: string) => {
|
||||||
issueUpdate({ ...issue, state: _state });
|
issueUpdate({ ...issue, state: _state });
|
||||||
@ -116,7 +116,12 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
|
|||||||
<p>State</p>
|
<p>State</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<SidebarStateSelect value={issue?.state || ""} onChange={handleState} disabled={disableUserActions} />
|
<SidebarStateSelect
|
||||||
|
value={issue?.state || ""}
|
||||||
|
projectId={projectId as string}
|
||||||
|
onChange={handleState}
|
||||||
|
disabled={disableUserActions}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -129,6 +134,7 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
|
|||||||
<div>
|
<div>
|
||||||
<SidebarAssigneeSelect
|
<SidebarAssigneeSelect
|
||||||
value={issue.assignees || []}
|
value={issue.assignees || []}
|
||||||
|
projectId={projectId as string}
|
||||||
onChange={handleAssignee}
|
onChange={handleAssignee}
|
||||||
disabled={disableUserActions}
|
disabled={disableUserActions}
|
||||||
/>
|
/>
|
||||||
@ -210,7 +216,12 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
|
|||||||
<p>Parent</p>
|
<p>Parent</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<SidebarParentSelect onChange={handleParent} issueDetails={issue} disabled={disableUserActions} />
|
<SidebarParentSelect
|
||||||
|
onChange={handleParent}
|
||||||
|
issueDetails={issue}
|
||||||
|
projectId={projectId as string}
|
||||||
|
disabled={disableUserActions}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -226,6 +237,7 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
|
|||||||
<div>
|
<div>
|
||||||
<SidebarCycleSelect
|
<SidebarCycleSelect
|
||||||
issueDetail={issue}
|
issueDetail={issue}
|
||||||
|
projectId={projectId as string}
|
||||||
disabled={disableUserActions}
|
disabled={disableUserActions}
|
||||||
handleIssueUpdate={handleCycleOrModuleChange}
|
handleIssueUpdate={handleCycleOrModuleChange}
|
||||||
/>
|
/>
|
||||||
@ -240,6 +252,7 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
|
|||||||
<div>
|
<div>
|
||||||
<SidebarModuleSelect
|
<SidebarModuleSelect
|
||||||
issueDetail={issue}
|
issueDetail={issue}
|
||||||
|
projectId={projectId as string}
|
||||||
disabled={disableUserActions}
|
disabled={disableUserActions}
|
||||||
handleIssueUpdate={handleCycleOrModuleChange}
|
handleIssueUpdate={handleCycleOrModuleChange}
|
||||||
/>
|
/>
|
||||||
@ -253,6 +266,7 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
|
|||||||
<div className="flex w-full flex-col gap-3">
|
<div className="flex w-full flex-col gap-3">
|
||||||
<SidebarLabelSelect
|
<SidebarLabelSelect
|
||||||
issueDetails={issue}
|
issueDetails={issue}
|
||||||
|
projectId={projectId as string}
|
||||||
labelList={issue.labels}
|
labelList={issue.labels}
|
||||||
submitChanges={handleLabels}
|
submitChanges={handleLabels}
|
||||||
isNotAllowed={disableUserActions}
|
isNotAllowed={disableUserActions}
|
||||||
|
@ -11,6 +11,7 @@ import { IssueView } from "components/issues";
|
|||||||
import { copyUrlToClipboard } from "helpers/string.helper";
|
import { copyUrlToClipboard } from "helpers/string.helper";
|
||||||
// types
|
// types
|
||||||
import { IIssue, IIssueLink } from "types";
|
import { IIssue, IIssueLink } from "types";
|
||||||
|
import { EIssueActions } from "../issue-layouts/types";
|
||||||
// constants
|
// constants
|
||||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||||
|
|
||||||
@ -18,7 +19,7 @@ interface IIssuePeekOverview {
|
|||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
issueId: string;
|
issueId: string;
|
||||||
handleIssue: (issue: Partial<IIssue>) => void;
|
handleIssue: (issue: Partial<IIssue>, action: EIssueActions) => Promise<void>;
|
||||||
isArchived?: boolean;
|
isArchived?: boolean;
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
}
|
}
|
||||||
@ -30,8 +31,6 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
|||||||
const { peekIssueId } = router.query;
|
const { peekIssueId } = router.query;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
user: { currentProjectRole },
|
|
||||||
issue: { removeIssueFromStructure },
|
|
||||||
issueDetail: {
|
issueDetail: {
|
||||||
createIssueComment,
|
createIssueComment,
|
||||||
updateIssueComment,
|
updateIssueComment,
|
||||||
@ -58,6 +57,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
|||||||
},
|
},
|
||||||
archivedIssues: { deleteArchivedIssue },
|
archivedIssues: { deleteArchivedIssue },
|
||||||
project: { currentProjectDetails },
|
project: { currentProjectDetails },
|
||||||
|
workspaceMember: { currentWorkspaceUserProjectsRole },
|
||||||
} = useMobxStore();
|
} = useMobxStore();
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
@ -98,7 +98,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
|||||||
|
|
||||||
const issueUpdate = async (_data: Partial<IIssue>) => {
|
const issueUpdate = async (_data: Partial<IIssue>) => {
|
||||||
if (handleIssue) {
|
if (handleIssue) {
|
||||||
await handleIssue(_data);
|
await handleIssue(_data, EIssueActions.UPDATE);
|
||||||
fetchIssueActivity(workspaceSlug, projectId, issueId);
|
fetchIssueActivity(workspaceSlug, projectId, issueId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -133,7 +133,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
|||||||
|
|
||||||
const handleDeleteIssue = async () => {
|
const handleDeleteIssue = async () => {
|
||||||
if (isArchived) await deleteArchivedIssue(workspaceSlug, projectId, issue!);
|
if (isArchived) await deleteArchivedIssue(workspaceSlug, projectId, issue!);
|
||||||
else removeIssueFromStructure(workspaceSlug, projectId, issue!);
|
else await handleIssue(issue!, EIssueActions.DELETE);
|
||||||
const { query } = router;
|
const { query } = router;
|
||||||
if (query.peekIssueId) {
|
if (query.peekIssueId) {
|
||||||
setPeekId(null);
|
setPeekId(null);
|
||||||
@ -146,7 +146,8 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const userRole = currentProjectRole ?? EUserWorkspaceRoles.GUEST;
|
const userRole =
|
||||||
|
(currentWorkspaceUserProjectsRole && currentWorkspaceUserProjectsRole[projectId]) ?? EUserWorkspaceRoles.GUEST;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { FC, ReactNode, useState } from "react";
|
import { FC, ReactNode, useRef, 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 useSWR from "swr";
|
import useSWR from "swr";
|
||||||
@ -14,6 +14,8 @@ import {
|
|||||||
PeekOverviewIssueDetails,
|
PeekOverviewIssueDetails,
|
||||||
PeekOverviewProperties,
|
PeekOverviewProperties,
|
||||||
} from "components/issues";
|
} from "components/issues";
|
||||||
|
// hooks
|
||||||
|
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||||
// ui
|
// ui
|
||||||
import { Button, CenterPanelIcon, CustomSelect, FullScreenPanelIcon, SidePanelIcon, Spinner } from "@plane/ui";
|
import { Button, CenterPanelIcon, CustomSelect, FullScreenPanelIcon, SidePanelIcon, Spinner } from "@plane/ui";
|
||||||
// types
|
// types
|
||||||
@ -107,6 +109,8 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
|||||||
const [peekMode, setPeekMode] = useState<TPeekModes>("side-peek");
|
const [peekMode, setPeekMode] = useState<TPeekModes>("side-peek");
|
||||||
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
|
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
|
||||||
const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved");
|
const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved");
|
||||||
|
// ref
|
||||||
|
const issuePeekOverviewRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const updateRoutePeekId = () => {
|
const updateRoutePeekId = () => {
|
||||||
if (issueId != peekIssueId) {
|
if (issueId != peekIssueId) {
|
||||||
@ -151,6 +155,8 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
|||||||
|
|
||||||
const currentMode = PEEK_OPTIONS.find((m) => m.key === peekMode);
|
const currentMode = PEEK_OPTIONS.find((m) => m.key === peekMode);
|
||||||
|
|
||||||
|
useOutsideClickDetector(issuePeekOverviewRef, () => removeRoutePeekId());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{issue && !isArchived && (
|
{issue && !isArchived && (
|
||||||
@ -178,6 +184,7 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
|||||||
|
|
||||||
{issueId === peekIssueId && (
|
{issueId === peekIssueId && (
|
||||||
<div
|
<div
|
||||||
|
ref={issuePeekOverviewRef}
|
||||||
className={`fixed z-20 flex flex-col overflow-hidden rounded border border-custom-border-200 bg-custom-background-100 transition-all duration-300
|
className={`fixed z-20 flex flex-col overflow-hidden rounded border border-custom-border-200 bg-custom-background-100 transition-all duration-300
|
||||||
${peekMode === "side-peek" ? `bottom-0 right-0 top-0 w-full md:w-[50%]` : ``}
|
${peekMode === "side-peek" ? `bottom-0 right-0 top-0 w-full md:w-[50%]` : ``}
|
||||||
${peekMode === "modal" ? `left-[50%] top-[50%] h-5/6 w-5/6 -translate-x-[50%] -translate-y-[50%]` : ``}
|
${peekMode === "modal" ? `left-[50%] top-[50%] h-5/6 w-5/6 -translate-x-[50%] -translate-y-[50%]` : ``}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user