Compare commits

...
62 Commits
Author SHA1 Message Date
sriram veeraghantaandGitHub e7468292c7 Merge pull request #3175 from makeplane/preview
release: moving changes from preview to master
2023-12-18 19:29:43 +05:30
Anmol Singh BhatiaandGitHub 7bff8d2ec5 chore: cycle and module sidebar mutation fix (#3174)
* chore: issue action cycle sidebar mutation fix

* chore: issue action module sidebar mutation fix
2023-12-18 19:01:10 +05:30
sriram veeraghantaandGitHub 6f2cce081f Merge pull request #3171 from makeplane/develop
Promote: develop change to preview
2023-12-18 15:37:53 +05:30
sriram veeraghantaandGitHub a86dafc11c Merge pull request #3170 from makeplane/preview
sync: moving contributions from preview to develop
2023-12-18 15:25:34 +05:30
Aaryan KhandelwalandGitHub 81256d6373 chore: add tooltips to issue properties with no value (#3169)
* chore: add tolltips to properties with no value

* chore: update property types
2023-12-18 15:17:38 +05:30
Lakhan BahetiandGitHub c9792da4a1 fix: bug fixes & improvements (#3159)
* fix: on issue update updates_on not updating

* fix: on project change cycle update

* fix: update issue
2023-12-18 14:59:25 +05:30
Prateek ShouryaandGitHub 37df0bcdd8 fix: issue with peek view properties not editable and options not being shown in all workspace issues. (#3100)
* fix: issue with peek view properties not editable and options not being shown in `all workspace issues`..

* refactor: use projectId from props instead of router query.
fix: issue in add to module/ cycle not working properly.
2023-12-18 14:47:40 +05:30
Prateek ShouryaandGitHub 18c86bd8cc chore: add email from address in Instance Admin Email Settings. (#3155) 2023-12-18 13:36:09 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
0ee6c20272 chore(deps): bump cryptography in /apiserver/requirements (#3166)
Bumps [cryptography](https://github.com/pyca/cryptography) from 41.0.5 to 41.0.6.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/41.0.5...41.0.6)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-18 13:26:21 +05:30
Prateek ShouryaandGitHub 184db0156c User role validation across workspace and projects. (#3167)
* chore: remove `add link` button for guests & viewer in modules sidebar.

* chore: remove `+` (add view) icon for guests & viewer in `All Issues`.

* chore: remove `Start Project` button from Dashboard & Projects empty state for guests & viewers.

* chore: project level user role validation for empty states.
2023-12-18 13:25:03 +05:30
NikhilandGitHub b7a0f3c693 fix: project cover list endpoint (#3168) 2023-12-18 13:19:04 +05:30
Ikko Eltociear AshimineandGitHub e40f38e2e1 fix: typo in install.sh (#3136)
availabe -> available
2023-12-18 12:17:15 +05:30
Lakhan BahetiandGitHub 05e7afab8d fix: issue peek-overview delete functionality (#3134)
* fix: peek-overview delete issue

* refactor: removed unused variables
2023-12-18 12:14:57 +05:30
969a51f425 chore: issue click & peek overview improvement (#3157)
* improve issue popover to detect outside click

* chore: stopPropagation event added to prevent peekoverview triggering in action menu & issue properties

* chore: stopPropagation event added to prevent peekoverview triggering in issue properties

* chore: enable entire issue card clickability in list and kanban layout, introduce control-click functionality
to open issues in new tabs

* chore: build error fix and unused variable removed

* chore: build error fix

---------

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
2023-12-18 12:11:14 +05:30
sriram veeraghantaandGitHub 849bc92aea Merge pull request #3121 from notpushkin/patch-1
Fix .vertical-lr in Firefox
2023-12-18 01:11:31 +05:30
Lakhan BahetiandGitHub a37dec45d9 chore: made project-identifier rule consistent (#3148) 2023-12-17 00:39:23 +05:30
Lakhan BahetiandGitHub e1793dda74 chore: role restriction to issue detail & peek-overview (#3146)
* chore: disabled interaction with title & description in peekview & issue detail

* chore: restriction to attachments delete for lower roles

* chore: kanban & calendar block drag restriction for lower roles

* fix: module/issue sidebar links access for lower roles

* fix: issue detail role validation

* fix: user role validation condition
2023-12-17 00:38:05 +05:30
Anmol Singh BhatiaandGitHub 31fdaf2659 fix: project members fetch fix (#3145)
* chore: all issues assignee fetch fix

* chore: assignee fetch logic updated
2023-12-17 00:36:56 +05:30
Aaryan KhandelwalandGitHub 8a1a6c6f62 chore: disable API calls for un-authorized users (#3144)
* fix: disable api calls if user doesn't have access

* chore: remove permissions from component level

* chore: rmeove console log and organize import statements
2023-12-17 00:35:53 +05:30
Prateek ShouryaandGitHub 3d83101f69 fix: issue with cycle and module sidebar filter implementation. (#3142) 2023-12-17 00:34:42 +05:30
Lakhan BahetiandGitHub d473ba9d0d fix: create issue project ID (#3131) 2023-12-17 00:32:00 +05:30
Aaryan KhandelwalandGitHub e5902152ab fix: overflow issues in the profile settings layout (#3163) 2023-12-17 00:21:03 +05:30
sriram veeraghantaandGitHub f03a9a6de8 fix: sync changes (#3156)
* fix: sync changes

* fix: sync changes

* fix: sync changes updated
2023-12-15 18:12:16 +05:30
Anmol Singh BhatiaandGitHub b7e2f1e57a chore: resolve priority sorting order (#3141) 2023-12-15 17:37:37 +05:30
sriram veeraghantaandGitHub 885de6f679 fix: sync changes (#3154) 2023-12-15 17:36:26 +05:30
sriram veeraghantaandGitHub ce9714ff12 fix: sync pr changes (#3153) 2023-12-15 17:31:14 +05:30
sriram veeraghantaandGitHub 74b141eea2 fix: sync changes (#3152) 2023-12-15 17:24:24 +05:30
sriram veeraghantaandGitHub 08425c9614 fix: sync changes (#3151) 2023-12-15 17:21:06 +05:30
sriram veeraghantaandGitHub ecfcc03ef0 fix: removing unneccessary checks (#3150) 2023-12-15 17:12:57 +05:30
sriram veeraghantaandGitHub 829c08f0ee fix: making changes to sync job (#3149) 2023-12-15 17:08:37 +05:30
Anmol Singh BhatiaandGitHub 2edd2d947e chore: implement validation for accepting 'http://' and 'https://' url in link modal (#3147) 2023-12-15 16:39:32 +05:30
Aaryan KhandelwalandGitHub 5b67f27345 fix: remove get requests from the catch block (#3135)
* fix: refetching project filters on error

* fix: get request in the catch block
2023-12-14 19:55:45 +05:30
Anmol Singh BhatiaandGitHub 7684a2c091 chore: sidebar quick action improvement (#3133) 2023-12-14 17:24:42 +05:30
Lakhan BahetiandGitHub 4e2bf24e8d chore: filter edit operation in views is disabled for lower roles (#3113)
* chore: edit/delete view options hidden for lower roles

* chore: project -> views access restriction for lower roles

* refactor: allowance condition
2023-12-14 16:49:36 +05:30
Anmol Singh BhatiaandGitHub aafac9ed1d chore: state sequence ordering (#3130) 2023-12-14 16:45:39 +05:30
Prateek ShouryaandGitHub 3adf48e429 chore: add new or existing issues validation for guest & viewers in cycles and modules empty state. (#3126) 2023-12-14 16:31:57 +05:30
Lakhan BahetiandGitHub 1c546e3cc5 fix: cycle & module sidebar date range picker (#3127)
* fix: cycle sidebar date range picker

* chore: cycle sidebar hover pointer

* fix: module sidebar date range picker

* fix: module sidebar state select popover pointer
2023-12-14 16:31:39 +05:30
5c7382d894 fix: environment file missing for space (#3105)
* fix: environment file missing for space

* dev: remove container name from the docker-compose local

* dockerfile.dev modified for volumes

* local dev fixes

---------

Co-authored-by: Manish Gupta <manish@mgupta.me>
2023-12-14 16:18:02 +05:30
Bavisetti NarayanandGitHub 1f8ae3a5ad chore: project member list (#3087) 2023-12-14 16:14:50 +05:30
Bavisetti NarayanandGitHub f1ed0c979c chore: workspace project reinvite (#3112)
* chore: user can rejoin a project by invite

* chore: workspace member leave
2023-12-14 16:14:12 +05:30
Bavisetti NarayanandGitHub 78b29eb81b chore: resolved the use tls error (#3114) 2023-12-14 16:11:46 +05:30
Anmol Singh BhatiaandGitHub 8d3a0a2eec fix: build error (#3128) 2023-12-14 16:07:53 +05:30
sriram veeraghanta 910d1a1de3 Merge branch 'preview' of github.com:makeplane/plane into develop 2023-12-13 23:18:09 +05:30
Ehtesham SiddiquiandGitHub 6004f29bbc Fix: update self-host docs link in README (#3109)
* Fix: update self-host docs link

* fix: update readme.md to link to self-hosting docker compose
2023-12-13 23:11:48 +05:30
Lakhan BahetiandGitHub 9d0056cfee fix: spreadsheet layout sub-issues property update (#3110)
* fix: spreadsheet subissues property update

* fix: hover effect for sub-issues

* refactor: mutate sub-issues
2023-12-13 23:07:26 +05:30
Lakhan BahetiandGitHub 4bb99d5fbf fix: cycle delete & active cycle loading (#3088) 2023-12-13 23:06:38 +05:30
Anmol Singh BhatiaandGitHub 6c61fbd102 chore: guest user profile access-related problem (#3089)
* fix: resolve guest user profile access-related problem

* chore: header and app sidebar permission validation added
2023-12-13 23:05:50 +05:30
Anmol Singh BhatiaandGitHub 2605b938f0 chore: empty state action button validation (#3094) 2023-12-13 23:05:23 +05:30
Anmol Singh BhatiaandGitHub b4f51cb5af chore: cycle and module sidebar permission validation (#3095) 2023-12-13 23:05:02 +05:30
Anmol Singh BhatiaandGitHub fe80ca3e1c chore: issue sidebar and project view improvement and validation (#3098)
* chore: project view header button validation

* chore: copy view link option added in project view list item and role action validation added

* chore: issue sidebar improvement
2023-12-13 23:04:33 +05:30
Anmol Singh BhatiaandGitHub b78e83d81b chore: image file update, icon consistency, bug fixes and code refactor (#3107)
* chore: workspace view icon consistency

* chore: icon added in breadcrumb dropdown

* chore: svg image replaced with webp

* fix: build fix

* chore: unused variables removed

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

Some files were not shown because too many files have changed in this diff Show More