Compare commits

...
Author SHA1 Message Date
gurusainath 9a60cfd44e chore: updated view create and edit flow 2024-02-17 22:14:01 +05:30
gurusainath 4783e791ec Merge branch 'develop' of gurusainath:makeplane/plane into feat-global-views 2024-02-16 20:09:27 +05:30
Anmol Singh BhatiaandGitHub 41e812a811 fix: quick action copy link action (#3686) 2024-02-16 20:07:38 +05:30
a94c607031 fix: updated issue title and description components (#3687)
* fix: issue description fixes

* chore: description html in the archive issue

* chore: changed retrieve viewset

* chore: implemented new issue title description components in inbox, issue-detail and fixed issue in archived store

* chore: removed consoles and empty description update in issue detail

* fix: draft issue empty state image

---------

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
Co-authored-by: Anmol Singh Bhatia <anmolsinghbhatia@plane.so>
2024-02-16 20:07:04 +05:30
Anmol Singh BhatiaandGitHub 665a07f15a chore: dropdown and peek overview improvement (#3682)
* chore: dropdown focus state improvement

* fix: peek overview dropdown placement fix
2024-02-16 20:01:58 +05:30
gurusainath 61f184a2ef chore: view ui and new store workflow 2024-02-16 18:28:21 +05:30
rahulrameshaandGitHub 85a8af5125 fix: spreadsheet views sorting (#3683)
* fix sorting in spreadsheet all issues

* removing focus border since it is being handled globally
2024-02-16 18:20:44 +05:30
gurusainath 5661ae730a Merge branch 'develop' of gurusainath:makeplane/plane into feat-global-views 2024-02-15 23:45:06 +05:30
7628419a26 fix: inbox description and mutation (#3677)
* fix: inbox status mutation fix

* fix: inbox description fix

* chore: added description in inbox issue

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
2024-02-15 19:31:38 +05:30
Manish GuptaandGitHub 2cd16c61ba arm build for release tag (#3676) 2024-02-15 14:52:18 +05:30
sriram veeraghanta 8f7b05b73f Merge branch 'preview' of github.com:makeplane/plane into develop 2024-02-15 13:48:13 +05:30
Manish GuptaandGitHub 98d545cfb9 github action modified (#3673) 2024-02-15 13:47:26 +05:30
2548a9f062 fix: peek overview issue description initial load bug (#3670)
* fix: peek overview issue description initial load bug

* fix: peekoverview issue description infinite loading for certain data sets

---------

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
2024-02-15 03:17:16 +05:30
gurusainath 77c89427e6 Merge branch 'develop' of gurusainath:makeplane/plane into feat-global-views 2024-02-14 20:07:10 +05:30
gurusainath e55d7175dc chore: view rootstore update 2024-02-14 20:06:59 +05:30
gurusainath 430d9de722 chore: updated empty states 2024-02-14 19:14:18 +05:30
Bavisetti NarayanandGitHub d901277a20 chore: parent can be added to issue (#3665) 2024-02-14 18:16:27 +05:30
rahulrameshaandGitHub 489555f788 fix: horizontal scroll in gantt chart while dragging (#3664)
* fix horizontal scroll in gantt chart while dragging

* add aline indicator for quick add

* add border color for line above quick add in gantt to make it look better in dark mode
2024-02-14 18:15:13 +05:30
Anmol Singh BhatiaandGitHub b827a1af27 fix: issue details page parent issue redirection bug (#3662) 2024-02-14 18:13:29 +05:30
gurusainath c35d650de0 chore: implemented extra options in the display filter properties dropdown and hanlded the arrow functions with useCallback 2024-02-14 16:13:37 +05:30
gurusainath 6bde956166 Merge branch 'develop' of gurusainath:makeplane/plane into feat-global-views 2024-02-14 16:11:58 +05:30
gurusainath 178ad3dd73 Merge branch 'feat-global-views' of gurusainath:makeplane/plane into feat-global-views 2024-02-14 16:02:11 +05:30
Ramesh Kumar ChandraandGitHub 1d2e331cec fix: mobile responsive sidebar close on item tap (#3661) 2024-02-14 14:55:33 +05:30
Anmol Singh BhatiaandGitHub e51e4761b9 fix: cycle favorite mutation (#3659)
* fix: cycle favorite mutation

* fix: issue details sidebar overflow fix
2024-02-14 14:17:46 +05:30
Anmol Singh BhatiaandGitHub 9299478539 chore: module & cycle empty state improvement (#3658) 2024-02-14 13:03:37 +05:30
gurusainath 01f5a0a4e2 Merge branch 'develop' of gurusainath:makeplane/plane into feat-global-views 2024-02-14 12:52:34 +05:30
guru_sainathandGitHub fb4cffdd1c fix: infinity loader in the issue description in issue peek overview and issue detail page (#3656) 2024-02-14 12:46:52 +05:30
Anmol Singh BhatiaandGitHub 83bf28bb83 chore: loading state improvement (#3657) 2024-02-14 12:40:03 +05:30
NarayanBavisetti 9d39096ed2 Merge branch 'feat-global-views' of github.com:makeplane/plane into feat-global-views 2024-02-14 12:13:39 +05:30
NarayanBavisetti 06f6a21077 chore: global issues listing 2024-02-14 12:13:09 +05:30
gurusainath 8c5f42874c conflicts:merge conflicts resolved 2024-02-13 21:42:20 +05:30
guru_sainathandGitHub 25a2816a76 chore: empty state loaders and filter cancel state update for modules and cycles (#3653) 2024-02-13 20:58:44 +05:30
rahulrameshaandGitHub 571b89632c fix: issue spillover on switching workspace (#3652)
* fix global issues spillover to other workspace

* fix the same for profile issues as well
2024-02-13 20:50:42 +05:30
sriram veeraghanta 5b5698ad97 Merge branch 'preview' of github.com:makeplane/plane into develop 2024-02-13 19:13:07 +05:30
Anmol Singh BhatiaandGitHub b1989bae1b feat: loading states update (#3639)
* dev: implement layout skeleton loader and helper function

* chore: implemented layout loader

* chore: settings loader added

* chore: cycle, module, view, pages, notification and projects loader added

* chore: kanban loader improvement

* chore: loader utils updated
2024-02-13 19:12:10 +05:30
rahulrameshaandGitHub 83139989c2 fix: sort order in the same list for Kanban (#3651)
* fixing kanban dnd by stooping the modification of the original array by spreading to change the array reference

* fix sort order in the same list

* minor change in condition
2024-02-13 19:11:20 +05:30
Anmol Singh BhatiaandGitHub 1bf06821bb fix: add existing issue modal fix (#3649) 2024-02-13 18:57:56 +05:30
gurusainath a5fd6f0e8a chore: store changes on the filters and display filters 2024-02-13 18:41:34 +05:30
gurusainath 02380c730d Merge branch 'develop' of gurusainath:makeplane/plane into feat-global-views 2024-02-13 18:13:18 +05:30
Anmol Singh BhatiaandGitHub eea3b4fa54 chore: new empty state (#3640)
* chore: empty state asset updated

* chore: empty state config updated

* chore: cycle and module issues layout empty state added

* chore: workspace and project settings empty state added
2024-02-13 16:35:20 +05:30
rahulrameshaandGitHub f64284f6a0 fixing kanban dnd by stooping the modification of the original array by spreading to change the array reference (#3646) 2024-02-13 16:33:19 +05:30
Anmol Singh BhatiaandGitHub 06496ff0f0 chore: app sidebar state (#3644)
* chore: app sidebar state fix

* chore: app sidebar state fix
2024-02-13 16:33:01 +05:30
gurusainath 68e8c449c8 Merge branch 'develop' of gurusainath:makeplane/plane into feat-global-views 2024-02-13 15:46:24 +05:30
Aaryan KhandelwalandGitHub 247937d93a fix: scroll sync (#3645) 2024-02-13 14:33:24 +05:30
AbId KhAnandGitHub e0a18578f5 fix local development setup issue due to docker compose local file makeplane/plane#3641 (#3642) 2024-02-13 12:39:58 +05:30
gurusainath 608eea67f7 chore: stabilising the views 2024-02-13 12:28:05 +05:30
gurusainath badb190c25 Merge branch 'develop' of gurusainath:makeplane/plane into feat/mobx-global-views 2024-02-13 01:47:12 +05:30
sriram veeraghantaandGitHub e93bbec4cd fix: pages tabs responsive fixes (#3635) 2024-02-12 22:14:11 +05:30
rahulrameshaandGitHub d90aaba842 chore: change border of placeholder for spreadsheet (#3631)
* change border to place holder for spreadsheet

* add completed cylces exception fix

* remove window idle callback for making issues visible
2024-02-12 20:25:37 +05:30
sriram veeraghanta a303e52039 fix: package version upgrade 2024-02-12 20:23:39 +05:30
sriram veeraghanta 73d12cf1bb fix: package version upgrade 2024-02-12 20:23:12 +05:30
sriram veeraghanta 7651640e29 Merge branch 'preview' of github.com:makeplane/plane into develop 2024-02-12 19:12:26 +05:30
Ramesh Kumar ChandraandGitHub 41a9dc3603 fix: pages table of content drop down close on click and app sidebar expansion by default on big screens (#3629) 2024-02-12 19:11:56 +05:30
Prateek ShouryaandGitHub afda3ee6ca fix: priority sort order in display filters. (#3627) 2024-02-12 19:10:41 +05:30
Aaryan KhandelwalandGitHub fc8edab59b fix: sidebar height, authorization (#3626) 2024-02-12 19:10:07 +05:30
gurusainath 7e1b90c97a Merge branch 'develop' of gurusainath:makeplane/plane into feat/mobx-global-views 2024-02-12 17:18:39 +05:30
sriram veeraghanta 9cf5348019 fix: merge conflicts resolved 2024-02-12 17:09:22 +05:30
Lakhan BahetiandGitHub 042ed04a03 fix: project-pages responsiveness (#3624)
* fix: pages responsiveness

* fix: build errors
2024-02-12 17:01:58 +05:30
Ramesh Kumar ChandraandGitHub e29edfc02b style: issue detail responsiveness (#3625) 2024-02-12 17:01:24 +05:30
eb0af8de59 fix: inbox issues icon in project header render in big screen (#3622)
Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
2024-02-12 15:58:22 +05:30
0fb43c6fc5 chore: Removing 'description_html' from Issue List (#3623)
* chore: removed issue description from issue list

* fix: issue description handling on peekoverview

---------

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
2024-02-12 15:26:02 +05:30
963d26ccda refactor: Gantt chart layout (#3585)
* chore: gantt sidebar and main content scroll sync

* chore: add arrow navigation position logic

* refactor: scroll position update logic

* refactor: gantt chart components

* refactor: gantt sidebar

* fix: vertical scroll issue

* fix: move to the hidden block button flickering

* refactor: gantt sidebar components

* chore: move timeline header outside

* fix gantt scroll issue

* fix: sticky position issues

* fix: infinite timeline scroll logic

* chore: removed unnecessary import statements

---------

Co-authored-by: rahulramesha <rahulramesham@gmail.com>
2024-02-12 15:08:17 +05:30
sriram veeraghanta 3eb819c4ae fix: merge conflicts resolved 2024-02-12 13:14:00 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
4b67a41b41 chore(deps): bump django from 4.2.7 to 4.2.10 in /apiserver/requirements (#3606)
Bumps [django](https://github.com/django/django) from 4.2.7 to 4.2.10.
- [Commits](https://github.com/django/django/compare/4.2.7...4.2.10)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-12 12:44:16 +05:30
Aaryan KhandelwalandGitHub 0a36850d03 chore: show project tour modal before project creation (#3611) 2024-02-12 12:43:45 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
5bce2014c5 chore(deps): bump cryptography in /apiserver/requirements (#3619)
Bumps [cryptography](https://github.com/pyca/cryptography) from 41.0.6 to 42.0.0.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/41.0.6...42.0.0)

---
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>
2024-02-12 12:42:52 +05:30
Ramesh Kumar ChandraandGitHub 3f7f91e120 fix: breadcrumbs responsiveness fix, modules list mobile responsive layout component added, inbox button fix for responsiveness, board view card responsiveness in cycles and modules (#3620) 2024-02-12 12:34:15 +05:30
gurusainath 46a6a599a1 chore: updated filters and filter components 2024-02-12 12:30:59 +05:30
NarayanBavisetti 8ae1e3cff2 chore: visibility and lock 2024-02-12 12:23:03 +05:30
NarayanBavisetti ffdd515cf8 chore: migrations and visiblity of views 2024-02-12 00:49:37 +05:30
gurusainath 5d9393cfa6 chore: filter saving store and ui updates 2024-02-10 14:15:57 +05:30
gurusainath 0fb531e4b7 chore: ui and filter store updates 2024-02-09 22:16:17 +05:30
gurusainath cf10e3445d conflict: merge conflicts resolved 2024-02-09 22:14:46 +05:30
gurusainath f90595ca31 chore: updated ui and store 2024-02-09 14:33:22 +05:30
gurusainath 1ce7f20c2d chore: ui fixes on filter and implemented default data 2024-02-08 23:41:30 +05:30
gurusainath 84d3d34e14 conflict: merge conlficts resolved 2024-02-08 23:40:49 +05:30
gurusainath 76e55bee95 Merge branch 'develop' of gurusainath:makeplane/plane into feat/mobx-global-views 2024-02-08 12:04:51 +05:30
gurusainath f05b8de91d chore: implemented view create and edit operations and updated the store 2024-02-07 11:30:06 +05:30
gurusainath bc694bb742 Merge branch 'develop' of gurusainath:makeplane/plane into feat/mobx-global-views 2024-02-06 22:52:28 +05:30
gurusainath 65d2a2546d fix: SWR config 2024-02-05 20:09:59 +05:30
gurusainath cd81ec1002 fix: view compoennt and store updates 2024-02-05 20:09:17 +05:30
gurusainath 6325f97c8e Merge branch 'develop' of gurusainath:makeplane/plane into feat/mobx-global-views 2024-02-05 14:33:30 +05:30
gurusainath fe505e6b31 Merge branch 'develop' of gurusainath:makeplane/plane into feat/mobx-global-views 2024-02-03 21:23:56 +05:30
gurusainath d57d91e530 fix: store updates 2024-02-02 13:52:38 +05:30
gurusainath 984f7ed6b8 Merge branch 'feat/mobx-global-views' of gurusainath:makeplane/plane into feat/mobx-global-views 2024-02-02 12:11:21 +05:30
gurusainath 90bcdeccf4 Merge branch 'develop' of gurusainath:makeplane/plane into feat/mobx-global-views 2024-02-02 12:11:01 +05:30
NarayanBavisetti 1d88de472e chore: list of all views in workspace 2024-02-01 14:04:12 +05:30
NarayanBavisetti 4af5fef210 Merge branch 'feat/mobx-global-views' of github.com:makeplane/plane into feat/mobx-global-views 2024-01-31 16:13:33 +05:30
NarayanBavisetti f1ded8540e chore: lock and favorite 2024-01-31 16:12:51 +05:30
gurusainath b38a352c98 Merge branch 'develop' of gurusainath:makeplane/plane into feat/mobx-global-views 2024-01-31 15:42:40 +05:30
NarayanBavisetti a77839a942 fix: user and workspace views 2024-01-31 11:50:40 +05:30
NarayanBavisetti f63a04c1ab Merge branch 'develop' of github.com:makeplane/plane into feat/mobx-global-views 2024-01-31 11:49:50 +05:30
gurusainath dfb6b2b247 fix: updated store and types in packages 2024-01-31 11:37:02 +05:30
gurusainath 088cc8c659 fix: updated store and added favorite 2024-01-30 16:45:14 +05:30
gurusainath 05e4311e06 fix: build errors in the inbox store and updated the types for filters, display_filters and display_properties in the global views 2024-01-30 14:58:35 +05:30
gurusainath 5674acd985 Merge branch 'develop' of gurusainath:makeplane/plane into feat/mobx-global-views 2024-01-30 14:39:33 +05:30
gurusainath 8e590f6f60 init: stort init for global view store 2024-01-30 14:26:59 +05:30
486 changed files with 11312 additions and 3228 deletions
+5 -5
View File
@@ -28,7 +28,7 @@ jobs:
- id: set_env_variables
name: Set Environment Variables
run: |
if [ "${{ env.TARGET_BRANCH }}" == "master" ]; then
if [ "${{ env.TARGET_BRANCH }}" == "master" ] || [ "${{ github.event_name }}" == "release" ]; then
echo "BUILDX_DRIVER=cloud" >> $GITHUB_OUTPUT
echo "BUILDX_VERSION=lab:latest" >> $GITHUB_OUTPUT
echo "BUILDX_PLATFORMS=linux/amd64,linux/arm64" >> $GITHUB_OUTPUT
@@ -54,7 +54,7 @@ jobs:
steps:
- name: Set Frontend Docker Tag
run: |
if [ "${{ env.TARGET_BRANCH }}" == "master" ] && [ "${{ github.event_name }}" == "release" ]; then
if [ "${{ github.event_name }}" == "release" ]; then
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend:latest,${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend:${{ github.event.release.tag_name }}
elif [ "${{ env.TARGET_BRANCH }}" == "master" ]; then
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend:stable
@@ -105,7 +105,7 @@ jobs:
steps:
- name: Set Space Docker Tag
run: |
if [ "${{ env.TARGET_BRANCH }}" == "master" ] && [ "${{ github.event_name }}" == "release" ]; then
if [ "${{ github.event_name }}" == "release" ]; then
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-space:latest,${{ secrets.DOCKERHUB_USERNAME }}/plane-space:${{ github.event.release.tag_name }}
elif [ "${{ env.TARGET_BRANCH }}" == "master" ]; then
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-space:stable
@@ -156,7 +156,7 @@ jobs:
steps:
- name: Set Backend Docker Tag
run: |
if [ "${{ env.TARGET_BRANCH }}" == "master" ] && [ "${{ github.event_name }}" == "release" ]; then
if [ "${{ github.event_name }}" == "release" ]; then
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-backend:latest,${{ secrets.DOCKERHUB_USERNAME }}/plane-backend:${{ github.event.release.tag_name }}
elif [ "${{ env.TARGET_BRANCH }}" == "master" ]; then
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-backend:stable
@@ -208,7 +208,7 @@ jobs:
steps:
- name: Set Proxy Docker Tag
run: |
if [ "${{ env.TARGET_BRANCH }}" == "master" ] && [ "${{ github.event_name }}" == "release" ]; then
if [ "${{ github.event_name }}" == "release" ]; then
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy:latest,${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy:${{ github.event.release.tag_name }}
elif [ "${{ env.TARGET_BRANCH }}" == "master" ]; then
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy:stable
-238
View File
@@ -1,238 +0,0 @@
# All the python scripts that are used for back migrations
import uuid
import random
from django.contrib.auth.hashers import make_password
from plane.db.models import ProjectIdentifier
from plane.db.models import (
Issue,
IssueComment,
User,
Project,
ProjectMember,
Label,
Integration,
)
# Update description and description html values for old descriptions
def update_description():
try:
issues = Issue.objects.all()
updated_issues = []
for issue in issues:
issue.description_html = f"<p>{issue.description}</p>"
issue.description_stripped = issue.description
updated_issues.append(issue)
Issue.objects.bulk_update(
updated_issues,
["description_html", "description_stripped"],
batch_size=100,
)
print("Success")
except Exception as e:
print(e)
print("Failed")
def update_comments():
try:
issue_comments = IssueComment.objects.all()
updated_issue_comments = []
for issue_comment in issue_comments:
issue_comment.comment_html = (
f"<p>{issue_comment.comment_stripped}</p>"
)
updated_issue_comments.append(issue_comment)
IssueComment.objects.bulk_update(
updated_issue_comments, ["comment_html"], batch_size=100
)
print("Success")
except Exception as e:
print(e)
print("Failed")
def update_project_identifiers():
try:
project_identifiers = ProjectIdentifier.objects.filter(
workspace_id=None
).select_related("project", "project__workspace")
updated_identifiers = []
for identifier in project_identifiers:
identifier.workspace_id = identifier.project.workspace_id
updated_identifiers.append(identifier)
ProjectIdentifier.objects.bulk_update(
updated_identifiers, ["workspace_id"], batch_size=50
)
print("Success")
except Exception as e:
print(e)
print("Failed")
def update_user_empty_password():
try:
users = User.objects.filter(password="")
updated_users = []
for user in users:
user.password = make_password(uuid.uuid4().hex)
user.is_password_autoset = True
updated_users.append(user)
User.objects.bulk_update(updated_users, ["password"], batch_size=50)
print("Success")
except Exception as e:
print(e)
print("Failed")
def updated_issue_sort_order():
try:
issues = Issue.objects.all()
updated_issues = []
for issue in issues:
issue.sort_order = issue.sequence_id * random.randint(100, 500)
updated_issues.append(issue)
Issue.objects.bulk_update(
updated_issues, ["sort_order"], batch_size=100
)
print("Success")
except Exception as e:
print(e)
print("Failed")
def update_project_cover_images():
try:
project_cover_images = [
"https://images.unsplash.com/photo-1677432658720-3d84f9d657b4?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80",
"https://images.unsplash.com/photo-1661107564401-57497d8fe86f?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1332&q=80",
"https://images.unsplash.com/photo-1677352241429-dc90cfc7a623?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1332&q=80",
"https://images.unsplash.com/photo-1677196728306-eeafea692454?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1331&q=80",
"https://images.unsplash.com/photo-1660902179734-c94c944f7830?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1255&q=80",
"https://images.unsplash.com/photo-1672243775941-10d763d9adef?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80",
"https://images.unsplash.com/photo-1677040628614-53936ff66632?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80",
"https://images.unsplash.com/photo-1676920410907-8d5f8dd4b5ba?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1332&q=80",
"https://images.unsplash.com/photo-1676846328604-ce831c481346?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1155&q=80",
"https://images.unsplash.com/photo-1676744843212-09b7e64c3a05?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80",
"https://images.unsplash.com/photo-1676798531090-1608bedeac7b?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80",
"https://images.unsplash.com/photo-1597088758740-56fd7ec8a3f0?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1169&q=80",
"https://images.unsplash.com/photo-1676638392418-80aad7c87b96?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=774&q=80",
"https://images.unsplash.com/photo-1649639194967-2fec0b4ea7bc?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80",
"https://images.unsplash.com/photo-1675883086902-b453b3f8146e?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=774&q=80",
"https://images.unsplash.com/photo-1675887057159-40fca28fdc5d?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1173&q=80",
"https://images.unsplash.com/photo-1675373980203-f84c5a672aa5?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80",
"https://images.unsplash.com/photo-1675191475318-d2bf6bad1200?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1332&q=80",
"https://images.unsplash.com/photo-1675456230532-2194d0c4bcc0?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80",
"https://images.unsplash.com/photo-1675371788315-60fa0ef48267?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1332&q=80",
]
projects = Project.objects.all()
updated_projects = []
for project in projects:
project.cover_image = project_cover_images[random.randint(0, 19)]
updated_projects.append(project)
Project.objects.bulk_update(
updated_projects, ["cover_image"], batch_size=100
)
print("Success")
except Exception as e:
print(e)
print("Failed")
def update_user_view_property():
try:
project_members = ProjectMember.objects.all()
updated_project_members = []
for project_member in project_members:
project_member.default_props = {
"filters": {"type": None},
"orderBy": "-created_at",
"collapsed": True,
"issueView": "list",
"filterIssue": None,
"groupByProperty": None,
"showEmptyGroups": True,
}
updated_project_members.append(project_member)
ProjectMember.objects.bulk_update(
updated_project_members, ["default_props"], batch_size=100
)
print("Success")
except Exception as e:
print(e)
print("Failed")
def update_label_color():
try:
labels = Label.objects.filter(color="")
updated_labels = []
for label in labels:
label.color = "#" + "%06x" % random.randint(0, 0xFFFFFF)
updated_labels.append(label)
Label.objects.bulk_update(updated_labels, ["color"], batch_size=100)
print("Success")
except Exception as e:
print(e)
print("Failed")
def create_slack_integration():
try:
_ = Integration.objects.create(
provider="slack", network=2, title="Slack"
)
print("Success")
except Exception as e:
print(e)
print("Failed")
def update_integration_verified():
try:
integrations = Integration.objects.all()
updated_integrations = []
for integration in integrations:
integration.verified = True
updated_integrations.append(integration)
Integration.objects.bulk_update(
updated_integrations, ["verified"], batch_size=10
)
print("Success")
except Exception as e:
print(e)
print("Failed")
def update_start_date():
try:
issues = Issue.objects.filter(
state__group__in=["started", "completed"]
)
updated_issues = []
for issue in issues:
issue.start_date = issue.created_at.date()
updated_issues.append(issue)
Issue.objects.bulk_update(
updated_issues, ["start_date"], batch_size=500
)
print("Success")
except Exception as e:
print(e)
print("Failed")
+1 -1
View File
@@ -1,4 +1,4 @@
{
"name": "plane-api",
"version": "0.15.0"
"version": "0.15.1"
}
+3 -3
View File
@@ -36,9 +36,8 @@ from .project import (
)
from .state import StateSerializer, StateLiteSerializer
from .view import (
GlobalViewSerializer,
IssueViewSerializer,
IssueViewFavoriteSerializer,
ViewSerializer,
ViewFavoriteSerializer,
)
from .cycle import (
CycleSerializer,
@@ -68,6 +67,7 @@ from .issue import (
IssueRelationSerializer,
RelatedIssueSerializer,
IssuePublicSerializer,
IssueDetailSerializer,
)
from .module import (
+7 -1
View File
@@ -586,7 +586,6 @@ class IssueSerializer(DynamicBaseSerializer):
"id",
"name",
"state_id",
"description_html",
"sort_order",
"completed_at",
"estimate_point",
@@ -618,6 +617,13 @@ class IssueSerializer(DynamicBaseSerializer):
return [module for module in obj.issue_module.values_list("module_id", flat=True)]
class IssueDetailSerializer(IssueSerializer):
description_html = serializers.CharField()
class Meta(IssueSerializer.Meta):
fields = IssueSerializer.Meta.fields + ['description_html']
class IssueLiteSerializer(DynamicBaseSerializer):
workspace_detail = WorkspaceLiteSerializer(
read_only=True, source="workspace"
+9 -46
View File
@@ -3,69 +3,33 @@ from rest_framework import serializers
# Module imports
from .base import BaseSerializer, DynamicBaseSerializer
from .workspace import WorkspaceLiteSerializer
from .project import ProjectLiteSerializer
from plane.db.models import GlobalView, IssueView, IssueViewFavorite
from plane.db.models import View, ViewFavorite
from plane.utils.issue_filters import issue_filters
class GlobalViewSerializer(BaseSerializer):
workspace_detail = WorkspaceLiteSerializer(
source="workspace", read_only=True
)
class Meta:
model = GlobalView
fields = "__all__"
read_only_fields = [
"workspace",
"query",
]
def create(self, validated_data):
query_params = validated_data.get("query_data", {})
if bool(query_params):
validated_data["query"] = issue_filters(query_params, "POST")
else:
validated_data["query"] = dict()
return GlobalView.objects.create(**validated_data)
def update(self, instance, validated_data):
query_params = validated_data.get("query_data", {})
if bool(query_params):
validated_data["query"] = issue_filters(query_params, "POST")
else:
validated_data["query"] = dict()
validated_data["query"] = issue_filters(query_params, "PATCH")
return super().update(instance, validated_data)
class IssueViewSerializer(DynamicBaseSerializer):
class ViewSerializer(DynamicBaseSerializer):
is_favorite = serializers.BooleanField(read_only=True)
project_detail = ProjectLiteSerializer(source="project", read_only=True)
workspace_detail = WorkspaceLiteSerializer(
source="workspace", read_only=True
)
class Meta:
model = IssueView
model = View
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"query",
"access",
]
def create(self, validated_data):
query_params = validated_data.get("query_data", {})
query_params = validated_data.get("filters", {})
if bool(query_params):
validated_data["query"] = issue_filters(query_params, "POST")
else:
validated_data["query"] = {}
return IssueView.objects.create(**validated_data)
return View.objects.create(**validated_data)
def update(self, instance, validated_data):
query_params = validated_data.get("query_data", {})
query_params = validated_data.get("filters", {})
if bool(query_params):
validated_data["query"] = issue_filters(query_params, "POST")
else:
@@ -74,11 +38,10 @@ class IssueViewSerializer(DynamicBaseSerializer):
return super().update(instance, validated_data)
class IssueViewFavoriteSerializer(BaseSerializer):
view_detail = IssueViewSerializer(source="issue_view", read_only=True)
class ViewFavoriteSerializer(BaseSerializer):
class Meta:
model = IssueViewFavorite
model = ViewFavorite
fields = "__all__"
read_only_fields = [
"workspace",
+82 -1
View File
@@ -14,6 +14,8 @@ from plane.app.views import (
UserActivityGraphEndpoint,
UserIssueCompletedGraphEndpoint,
UserWorkspaceDashboardEndpoint,
UserWorkspaceViewViewSet,
UserProjectViewViewSet,
## End Workspaces
)
@@ -85,6 +87,7 @@ urlpatterns = [
UserIssueCompletedGraphEndpoint.as_view(),
name="completed-graph",
),
## End User Graph
path(
"users/me/workspaces/<str:slug>/dashboard/",
UserWorkspaceDashboardEndpoint.as_view(),
@@ -95,5 +98,83 @@ urlpatterns = [
SetUserPasswordEndpoint.as_view(),
name="set-password",
),
## End User Graph
path(
"users/me/workspaces/<str:slug>/views/",
UserWorkspaceViewViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="user-workspace-views",
),
path(
"users/me/workspaces/<str:slug>/views/<uuid:pk>/",
UserWorkspaceViewViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="user-workspace-views",
),
path(
"users/me/workspaces/<str:slug>/views/<uuid:pk>/duplicate/",
UserWorkspaceViewViewSet.as_view(
{
"post": "duplicate",
}
),
name="user-workspace-views",
),
path(
"users/me/workspaces/<str:slug>/views/<uuid:pk>/lock/",
UserWorkspaceViewViewSet.as_view(
{
"post": "toggle_lock",
}
),
name="user-workspace-views-lock",
),
path(
"users/me/workspaces/<str:slug>/projects/<uuid:project_id>/views/",
UserProjectViewViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="user-project-views",
),
path(
"users/me/workspaces/<str:slug>/projects/<uuid:project_id>/views/<uuid:pk>/",
UserProjectViewViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="user-project-views",
),
path(
"users/me/workspaces/<str:slug>/projects/<uuid:project_id>/views/<uuid:pk>/lock/",
UserProjectViewViewSet.as_view(
{
"post": "toggle_lock",
}
),
name="user-project-lock-views",
),
path(
"users/me/workspaces/<str:slug>/projects/<uuid:project_id>/views/<uuid:pk>/duplicate/",
UserWorkspaceViewViewSet.as_view(
{
"post": "duplicate",
}
),
name="user-project-duplicate-views",
),
]
+105 -48
View File
@@ -2,17 +2,86 @@ from django.urls import path
from plane.app.views import (
IssueViewViewSet,
GlobalViewViewSet,
GlobalViewIssuesViewSet,
IssueViewFavoriteViewSet,
ProjectViewViewSet,
WorkspaceViewViewSet,
WorkspaceViewFavoriteViewSet,
ProjectViewFavoriteViewSet,
WorkspaceViewIssuesViewSet,
)
urlpatterns = [
path(
"workspaces/<str:slug>/issues/",
WorkspaceViewIssuesViewSet.as_view(
{
"get": "list",
}
),
name="workspace-view-issues",
),
path(
"workspaces/<str:slug>/views/",
WorkspaceViewViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="workspace-view",
),
path(
"workspaces/<str:slug>/views/<uuid:pk>/",
WorkspaceViewViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="workspace-view",
),
path(
"workspaces/<str:slug>/views/<uuid:view_id>/duplicate/",
WorkspaceViewFavoriteViewSet.as_view(
{
"post": "duplicate",
}
),
name="workspace-duplicate-view",
),
path(
"workspaces/<str:slug>/views/<uuid:view_id>/favorite/",
WorkspaceViewFavoriteViewSet.as_view(
{
"get": "list",
"post": "create",
"delete": "destroy",
}
),
name="workspace-favorite-view",
),
path(
"workspaces/<str:slug>/views/<uuid:pk>/visibility/",
WorkspaceViewViewSet.as_view(
{
"post": "visibility",
}
),
name="workspace-duplicate-view",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/views/<uuid:pk>/lock/",
WorkspaceViewViewSet.as_view(
{
"post": "toggle_lock",
}
),
name="project-lock-views",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/views/",
IssueViewViewSet.as_view(
ProjectViewViewSet.as_view(
{
"get": "list",
"post": "create",
@@ -22,7 +91,7 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/views/<uuid:pk>/",
IssueViewViewSet.as_view(
ProjectViewViewSet.as_view(
{
"get": "retrieve",
"put": "update",
@@ -33,53 +102,41 @@ urlpatterns = [
name="project-view",
),
path(
"workspaces/<str:slug>/views/",
GlobalViewViewSet.as_view(
"workspaces/<str:slug>/projects/<uuid:project_id>/views/<uuid:pk>/duplicate/",
ProjectViewViewSet.as_view(
{
"post": "duplicate",
}
),
name="project-duplicate-view",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/views/<uuid:pk>/visibility/",
ProjectViewViewSet.as_view(
{
"post": "visibility",
}
),
name="project-duplicate-view",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/views/<uuid:pk>/lock/",
ProjectViewViewSet.as_view(
{
"post": "toggle_lock",
}
),
name="project-lock-views",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/views/<uuid:view_id>/favorite/",
ProjectViewFavoriteViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="global-view",
),
path(
"workspaces/<str:slug>/views/<uuid:pk>/",
GlobalViewViewSet.as_view(
{
"get": "retrieve",
"put": "update",
"patch": "partial_update",
"delete": "destroy",
}
),
name="global-view",
),
path(
"workspaces/<str:slug>/issues/",
GlobalViewIssuesViewSet.as_view(
{
"get": "list",
}
),
name="global-view-issues",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-views/",
IssueViewFavoriteViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="user-favorite-view",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-views/<uuid:view_id>/",
IssueViewFavoriteViewSet.as_view(
{
"delete": "destroy",
}
),
name="user-favorite-view",
name="project-favorite-view",
),
]
+8 -4
View File
@@ -52,10 +52,14 @@ from .workspace import (
)
from .state import StateViewSet
from .view import (
GlobalViewViewSet,
GlobalViewIssuesViewSet,
IssueViewViewSet,
IssueViewFavoriteViewSet,
WorkspaceViewViewSet,
ProjectViewViewSet,
WorkspaceViewFavoriteViewSet,
ProjectViewFavoriteViewSet,
UserWorkspaceViewViewSet,
UserProjectViewViewSet,
ProjectViewViewSet,
WorkspaceViewIssuesViewSet,
)
from .cycle import (
CycleViewSet,
+2 -2
View File
@@ -27,7 +27,7 @@ from plane.app.serializers import (
InboxSerializer,
InboxIssueSerializer,
IssueCreateSerializer,
IssueStateInboxSerializer,
IssueDetailSerializer,
)
from plane.utils.issue_filters import issue_filters
from plane.bgtasks.issue_activites_task import issue_activity
@@ -333,7 +333,7 @@ class InboxIssueViewSet(BaseViewSet):
def retrieve(self, request, slug, project_id, inbox_id, issue_id):
issue = self.get_queryset().filter(pk=issue_id).first()
serializer = IssueSerializer(issue, expand=self.expand,)
serializer = IssueDetailSerializer(issue, expand=self.expand,)
return Response(serializer.data, status=status.HTTP_200_OK)
def destroy(self, request, slug, project_id, inbox_id, issue_id):
+8 -7
View File
@@ -50,6 +50,7 @@ from plane.app.serializers import (
CommentReactionSerializer,
IssueRelationSerializer,
RelatedIssueSerializer,
IssueDetailSerializer,
)
from plane.app.permissions import (
ProjectEntityPermission,
@@ -267,7 +268,7 @@ class IssueViewSet(WebhookMixin, BaseViewSet):
def retrieve(self, request, slug, project_id, pk=None):
issue = self.get_queryset().filter(pk=pk).first()
return Response(
IssueSerializer(
IssueDetailSerializer(
issue, fields=self.fields, expand=self.expand
).data,
status=status.HTTP_200_OK,
@@ -1208,13 +1209,13 @@ class IssueArchiveViewSet(BaseViewSet):
return Response(issues, status=status.HTTP_200_OK)
def retrieve(self, request, slug, project_id, pk=None):
issue = Issue.objects.get(
workspace__slug=slug,
project_id=project_id,
archived_at__isnull=False,
pk=pk,
issue = self.get_queryset().filter(pk=pk).first()
return Response(
IssueDetailSerializer(
issue, fields=self.fields, expand=self.expand
).data,
status=status.HTTP_200_OK,
)
return Response(IssueSerializer(issue).data, status=status.HTTP_200_OK)
def unarchive(self, request, slug, project_id, pk=None):
issue = Issue.objects.get(
+3 -8
View File
@@ -17,7 +17,7 @@ from plane.db.models import (
Cycle,
Module,
Page,
IssueView,
View,
)
from plane.utils.issue_search import search_issues
@@ -161,7 +161,7 @@ class GlobalSearchEndpoint(BaseAPIView):
for field in fields:
q |= Q(**{f"{field}__icontains": query})
issue_views = IssueView.objects.filter(
issue_views = View.objects.filter(
q,
project__project_projectmember__member=self.request.user,
workspace__slug=slug,
@@ -247,12 +247,7 @@ class IssueSearchEndpoint(BaseAPIView):
if parent == "true" and issue_id:
issue = Issue.issue_objects.get(pk=issue_id)
issues = issues.filter(
~Q(pk=issue_id), ~Q(pk=issue.parent_id), parent__isnull=True
).exclude(
pk__in=Issue.issue_objects.filter(
parent__isnull=False
).values_list("parent_id", flat=True)
)
~Q(pk=issue_id), ~Q(pk=issue.parent_id), ~Q(parent_id=issue_id))
if issue_relation == "true" and issue_id:
issue = Issue.issue_objects.get(pk=issue_id)
issues = issues.filter(
+388 -106
View File
@@ -1,6 +1,5 @@
# Django imports
from django.db.models import (
Prefetch,
OuterRef,
Func,
F,
@@ -10,68 +9,370 @@ from django.db.models import (
When,
Exists,
Max,
Q,
)
from django.utils.decorators import method_decorator
from django.views.decorators.gzip import gzip_page
from django.db.models import Prefetch, OuterRef, Exists
# Third party imports
from rest_framework.response import Response
from rest_framework import status
# Module imports
from . import BaseViewSet, BaseAPIView
from . import BaseViewSet
from plane.app.serializers import (
GlobalViewSerializer,
IssueViewSerializer,
ViewSerializer,
IssueSerializer,
IssueViewFavoriteSerializer,
ViewFavoriteSerializer,
)
from plane.app.permissions import (
WorkspaceEntityPermission,
ProjectEntityPermission,
WorkspaceViewerPermission,
ProjectLitePermission,
)
from plane.db.models import (
Workspace,
GlobalView,
IssueView,
View,
Issue,
IssueViewFavorite,
IssueReaction,
ViewFavorite,
IssueLink,
IssueAttachment,
IssueSubscriber,
)
from plane.utils.issue_filters import issue_filters
from plane.utils.grouper import group_results
class GlobalViewViewSet(BaseViewSet):
serializer_class = IssueViewSerializer
model = IssueView
class UserWorkspaceViewViewSet(BaseViewSet):
serializer_class = ViewSerializer
model = View
permission_classes = [
WorkspaceEntityPermission,
]
def perform_create(self, serializer):
workspace = Workspace.objects.get(slug=self.kwargs.get("slug"))
serializer.save(workspace_id=workspace.id)
serializer.save(
workspace_id=workspace.id, access=0, owned_by=self.request.user
)
def get_queryset(self):
subquery = ViewFavorite.objects.filter(
user=self.request.user,
view_id=OuterRef("pk"),
workspace__slug=self.kwargs.get("slug"),
)
return self.filter_queryset(
super()
.get_queryset()
.filter(workspace__slug=self.kwargs.get("slug"))
# .filter(project__isnull=True)
.filter(Q(owned_by=self.request.user) & Q(access=0))
.select_related("workspace")
.annotate(is_favorite=Exists(subquery))
.order_by(self.request.GET.get("order_by", "-is_pinned"))
.order_by("-is_pinned", "-created_at")
.distinct()
)
def partial_update(self, request, slug, pk):
view = View.objects.get(pk=pk, workspace__slug=slug)
if view.owned_by == self.request.user and not view.is_locked:
serializer = ViewSerializer(view, data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(
serializer.errors, status=status.HTTP_400_BAD_REQUEST
)
return Response(
{"error": "You cannot update the view"},
status=status.HTTP_403_FORBIDDEN,
)
def list(self, request, slug):
type = request.GET.get("type", None)
views = self.get_queryset()
if type == "workspace":
views = views.filter(project__isnull=True)
if type == "project":
views = views.filter(project__isnull=False)
serializer = ViewSerializer(views, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
def toggle_lock(self, request, slug, pk):
view = View.objects.get(pk=pk, workspace__slug=slug)
lock = request.data.get("lock", view.is_locked)
if view.owned_by != self.request.user:
return Response(
{"error": "You cannot lock the view"},
status=status.HTTP_403_FORBIDDEN,
)
view.is_locked = lock
view.save(update_fields=["is_locked"])
return Response(ViewSerializer(view).data, status=status.HTTP_200_OK)
def duplicate(self, request, slug, pk):
view = View.objects.get(workspace__slug=slug, pk=pk)
# Create a shallow copy of the original view object
new_view = view
# Set the primary key of the new view to None to ensure it gets a new primary key
new_view.pk = None
# Modify the name of the new view to indicate that it's a copy
new_view.name = f"{view.name} (Copy)"
new_view.save(owned_by=request.user)
return Response(ViewSerializer(new_view).data, status=status.HTTP_201_CREATED)
def destroy(self, request, slug, pk):
view = View.objects.get(workspace__slug=slug, pk=pk)
if view.owned_by != self.request.user:
return Response(
{"error": "You cannot delete the view"},
status=status.HTTP_403_FORBIDDEN,
)
view.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
class WorkspaceViewViewSet(BaseViewSet):
serializer_class = ViewSerializer
model = View
permission_classes = [
WorkspaceEntityPermission,
]
def perform_create(self, serializer):
workspace = Workspace.objects.get(slug=self.kwargs.get("slug"))
serializer.save(workspace_id=workspace.id, owned_by=self.request.user)
def get_queryset(self):
subquery = ViewFavorite.objects.filter(
user=self.request.user,
view_id=OuterRef("pk"),
workspace__slug=self.kwargs.get("slug"),
)
return self.filter_queryset(
super()
.get_queryset()
.filter(workspace__slug=self.kwargs.get("slug"))
.filter(project__isnull=True)
.filter(Q(access=1))
.select_related("workspace")
.annotate(is_favorite=Exists(subquery))
.order_by(self.request.GET.get("order_by", "-created_at"))
.distinct()
)
def toggle_lock(self, request, slug, pk):
view = View.objects.get(pk=pk, workspace__slug=slug)
lock = request.data.get("lock", view.is_locked)
view.is_locked = lock
view.save(update_fields=["is_locked"])
return Response(ViewSerializer(view).data, status=status.HTTP_200_OK)
def duplicate(self, request, slug, pk):
view = View.objects.get(workspace__slug=slug, pk=pk)
# Create a shallow copy of the original view object
new_view = view
# Set the primary key of the new view to None to ensure it gets a new primary key
new_view.pk = None
# Modify the name of the new view to indicate that it's a copy
new_view.name = f"{view.name} (Copy)"
new_view.save(owned_by=request.user)
return Response(ViewSerializer(new_view).data, status=status.HTTP_201_CREATED)
class GlobalViewIssuesViewSet(BaseViewSet):
def visibility(self, request, slug, pk):
view = (
self.get_queryset()
.filter(pk=pk, workspace__slug=slug)
.first()
)
if view.owned_by != self.request.user:
return Response(
{"error": "You cannot update the view"},
status=status.HTTP_403_FORBIDDEN,
)
view.access = request.data.get("access", view.access)
view.save(update_fields=["access"])
return Response(ViewSerializer(view).data, status=status.HTTP_200_OK)
class UserProjectViewViewSet(BaseViewSet):
serializer_class = ViewSerializer
model = View
permission_classes = [
ProjectEntityPermission,
]
def perform_create(self, serializer):
workspace = Workspace.objects.get(slug=self.kwargs.get("slug"))
serializer.save(
workspace_id=workspace.id,
project_id=self.kwargs.get("project_id"),
access=0,
owned_by=self.request.user,
)
def get_queryset(self):
subquery = ViewFavorite.objects.filter(
user=self.request.user,
view_id=OuterRef("pk"),
project_id=self.kwargs.get("project_id"),
workspace__slug=self.kwargs.get("slug"),
)
return self.filter_queryset(
super()
.get_queryset()
.filter(workspace__slug=self.kwargs.get("slug"))
.filter(project_id=self.kwargs.get("project_id"))
.filter(project__project_projectmember__member=self.request.user)
.filter(Q(owned_by=self.request.user) & Q(access=0))
.select_related("workspace")
.annotate(is_favorite=Exists(subquery))
.order_by(self.request.GET.get("order_by", "-created_at"))
.distinct()
)
def partial_update(self, request, slug, project_id, pk):
view = View.objects.get(
pk=pk, project_id=project_id, workspace__slug=slug
)
if view.owned_by == self.request.user and not view.is_locked:
serializer = ViewSerializer(view, data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(
serializer.errors, status=status.HTTP_400_BAD_REQUEST
)
return Response(
{"error": "You cannot update the view"},
status=status.HTTP_403_FORBIDDEN,
)
def toggle_lock(self, request, slug, project_id, pk):
view = View.objects.get(
pk=pk, project_id=project_id, workspace__slug=slug
)
lock = request.data.get("lock", view.is_locked)
if view.owned_by != self.request.user:
return Response(
{"error": "You cannot lock the view"},
status=status.HTTP_403_FORBIDDEN,
)
view.is_locked = lock
view.save(update_fields=["is_locked"])
return Response(ViewSerializer(view).data, status=status.HTTP_200_OK)
def duplicate(self, request, slug, project_id, pk):
view = View.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
# Create a shallow copy of the original view object
new_view = view
# Set the primary key of the new view to None to ensure it gets a new primary key
new_view.pk = None
# Modify the name of the new view to indicate that it's a copy
new_view.name = f"{view.name} (Copy)"
new_view.save(owned_by=request.user)
return Response(ViewSerializer(new_view).data, status=status.HTTP_201_CREATED)
def destroy(self, request, slug, project_id, pk):
view = View.objects.get(
workspace__slug=slug, project_id=project_id, pk=pk
)
if view.owned_by != self.request.user:
return Response(
{"error": "You cannot delete the view"},
status=status.HTTP_403_FORBIDDEN,
)
view.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
class ProjectViewViewSet(BaseViewSet):
serializer_class = ViewSerializer
model = View
permission_classes = [
ProjectEntityPermission,
]
def perform_create(self, serializer):
workspace = Workspace.objects.get(slug=self.kwargs.get("slug"))
serializer.save(
workspace_id=workspace.id,
project_id=self.kwargs.get("project_id"),
owned_by=self.request.user,
)
def get_queryset(self):
subquery = ViewFavorite.objects.filter(
user=self.request.user,
view_id=OuterRef("pk"),
project_id=self.kwargs.get("project_id"),
workspace__slug=self.kwargs.get("slug"),
)
return self.filter_queryset(
super()
.get_queryset()
.filter(workspace__slug=self.kwargs.get("slug"))
.filter(project_id=self.kwargs.get("project_id"))
.filter(project__project_projectmember__member=self.request.user)
.filter(Q(access=1))
.select_related("workspace")
.annotate(is_favorite=Exists(subquery))
.order_by(self.request.GET.get("order_by", "-created_at"))
.distinct()
)
def toggle_lock(self, request, slug, project_id, pk):
view = (
self.get_queryset()
.filter(pk=pk, project_id=project_id, workspace__slug=slug)
.first()
)
lock = request.data.get("lock", view.is_locked)
view.is_locked = lock
view.save(update_fields=["is_locked"])
return Response(ViewSerializer(view).data, status=status.HTTP_200_OK)
def duplicate(self, request, slug, project_id, pk):
view = View.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
# Create a shallow copy of the original view object
new_view = view
# Set the primary key of the new view to None to ensure it gets a new primary key
new_view.pk = None
# Modify the name of the new view to indicate that it's a copy
new_view.name = f"{view.name} (Copy)"
new_view.save(owned_by=request.user)
return Response(ViewSerializer(new_view).data, status=status.HTTP_201_CREATED)
def visibility(self, request, slug, project_id, pk):
view = (
self.get_queryset()
.filter(pk=pk, project_id=project_id, workspace__slug=slug)
.first()
)
if view.owned_by != self.request.user:
return Response(
{"error": "You cannot update the view"},
status=status.HTTP_403_FORBIDDEN,
)
view.access = request.data.get("access", view.access)
view.save(update_fields=["access"])
return Response(ViewSerializer(view).data, status=status.HTTP_200_OK)
class WorkspaceViewIssuesViewSet(BaseViewSet):
permission_classes = [
WorkspaceEntityPermission,
]
@@ -87,41 +388,9 @@ class GlobalViewIssuesViewSet(BaseViewSet):
.values("count")
)
.filter(workspace__slug=self.kwargs.get("slug"))
.filter(project__project_projectmember__member=self.request.user)
.select_related("workspace", "project", "state", "parent")
.prefetch_related("assignees", "labels", "issue_module__module")
.prefetch_related(
Prefetch(
"issue_reactions",
queryset=IssueReaction.objects.select_related("actor"),
)
)
)
@method_decorator(gzip_page)
def list(self, request, slug):
filters = issue_filters(request.query_params, "GET")
fields = [
field
for field in request.GET.get("fields", "").split(",")
if field
]
# Custom ordering for priority and state
priority_order = ["urgent", "high", "medium", "low", "none"]
state_order = [
"backlog",
"unstarted",
"started",
"completed",
"cancelled",
]
order_by_param = request.GET.get("order_by", "-created_at")
issue_queryset = (
self.get_queryset()
.filter(**filters)
.filter(project__project_projectmember__member=self.request.user)
.annotate(cycle_id=F("issue_cycle__cycle_id"))
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
@@ -147,6 +416,29 @@ class GlobalViewIssuesViewSet(BaseViewSet):
)
)
@method_decorator(gzip_page)
def list(self, request, slug):
filters = issue_filters(request.query_params, "GET")
fields = [
field
for field in request.GET.get("fields", "").split(",")
if field
]
# Custom ordering for priority and state
priority_order = ["urgent", "high", "medium", "low", "none"]
state_order = [
"backlog",
"unstarted",
"started",
"completed",
"cancelled",
]
order_by_param = request.GET.get("order_by", "-created_at")
issue_queryset = self.get_queryset().filter(**filters)
# Priority Ordering
if order_by_param == "priority" or order_by_param == "-priority":
priority_order = (
@@ -213,52 +505,9 @@ class GlobalViewIssuesViewSet(BaseViewSet):
return Response(serializer.data, status=status.HTTP_200_OK)
class IssueViewViewSet(BaseViewSet):
serializer_class = IssueViewSerializer
model = IssueView
permission_classes = [
ProjectEntityPermission,
]
def perform_create(self, serializer):
serializer.save(project_id=self.kwargs.get("project_id"))
def get_queryset(self):
subquery = IssueViewFavorite.objects.filter(
user=self.request.user,
view_id=OuterRef("pk"),
project_id=self.kwargs.get("project_id"),
workspace__slug=self.kwargs.get("slug"),
)
return self.filter_queryset(
super()
.get_queryset()
.filter(workspace__slug=self.kwargs.get("slug"))
.filter(project_id=self.kwargs.get("project_id"))
.filter(project__project_projectmember__member=self.request.user)
.select_related("project")
.select_related("workspace")
.annotate(is_favorite=Exists(subquery))
.order_by("-is_favorite", "name")
.distinct()
)
def list(self, request, slug, project_id):
queryset = self.get_queryset()
fields = [
field
for field in request.GET.get("fields", "").split(",")
if field
]
views = IssueViewSerializer(
queryset, many=True, fields=fields if fields else None
).data
return Response(views, status=status.HTTP_200_OK)
class IssueViewFavoriteViewSet(BaseViewSet):
serializer_class = IssueViewFavoriteSerializer
model = IssueViewFavorite
class WorkspaceViewFavoriteViewSet(BaseViewSet):
serializer_class = ViewFavoriteSerializer
model = ViewFavorite
def get_queryset(self):
return self.filter_queryset(
@@ -269,19 +518,52 @@ class IssueViewFavoriteViewSet(BaseViewSet):
.select_related("view")
)
def create(self, request, slug, project_id):
serializer = IssueViewFavoriteSerializer(data=request.data)
if serializer.is_valid():
serializer.save(user=request.user, project_id=project_id)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def create(self, request, slug, view_id):
workspace = Workspace.objects.get(slug=slug)
view = ViewFavorite.objects.create(
view_id=view_id, user=request.user, workspace_id=workspace.id
)
return Response(
ViewFavoriteSerializer(view).data, status=status.HTTP_201_CREATED
)
def destroy(self, request, slug, view_id):
view_favorite = ViewFavorite.objects.get(
user=request.user,
workspace__slug=slug,
view_id=view_id,
)
view_favorite.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
class ProjectViewFavoriteViewSet(BaseViewSet):
serializer_class = ViewFavoriteSerializer
model = ViewFavorite
def get_queryset(self):
return self.filter_queryset(
super()
.get_queryset()
.filter(workspace__slug=self.kwargs.get("slug"))
.filter(user=self.request.user)
.select_related("view")
)
def create(self, request, slug, project_id, view_id):
view = ViewFavorite.objects.create(
view_id=view_id, user=request.user, project_id=project_id
)
return Response(
ViewFavoriteSerializer(view).data, status=status.HTTP_201_CREATED
)
def destroy(self, request, slug, project_id, view_id):
view_favourite = IssueViewFavorite.objects.get(
view_favorite = ViewFavorite.objects.get(
project=project_id,
user=request.user,
workspace__slug=slug,
view_id=view_id,
)
view_favourite.delete()
view_favorite.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
+3 -5
View File
@@ -1516,11 +1516,9 @@ class WorkspaceUserPropertiesEndpoint(BaseAPIView):
return Response(serializer.data, status=status.HTTP_201_CREATED)
def get(self, request, slug):
(
workspace_properties,
_,
) = WorkspaceUserProperties.objects.get_or_create(
user=request.user, workspace__slug=slug
workspace = Workspace.objects.get(slug=slug)
workspace_properties, _ = WorkspaceUserProperties.objects.get_or_create(
user=request.user, workspace=workspace
)
serializer = WorkspaceUserPropertiesSerializer(workspace_properties)
return Response(serializer.data, status=status.HTTP_200_OK)
@@ -1,11 +1,12 @@
# Generated by Django 4.2.7 on 2024-01-02 13:15
from plane.db.models import WorkspaceUserProperties, ProjectMember, IssueView
from plane.db.models import ProjectMember
from django.db import migrations
def workspace_user_properties(apps, schema_editor):
WorkspaceMember = apps.get_model("db", "WorkspaceMember")
WorkspaceUserProperties = apps.get_model("db", "WorkspaceUserProperties")
updated_workspace_user_properties = []
for workspace_members in WorkspaceMember.objects.all():
updated_workspace_user_properties.append(
@@ -49,6 +50,7 @@ def project_user_properties(apps, schema_editor):
def issue_view(apps, schema_editor):
GlobalView = apps.get_model("db", "GlobalView")
IssueView = apps.get_model("db", "IssueView")
updated_issue_views = []
for global_view in GlobalView.objects.all():
@@ -0,0 +1,70 @@
# Generated by Django 4.2.7 on 2024-01-30 07:49
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
from django.db.models import F
def views_owned_by(apps, schema_editor):
View = apps.get_model("db", "View")
View.objects.update(owned_by=F('created_by'))
class Migration(migrations.Migration):
dependencies = [
('db', '0060_cycle_progress_snapshot'),
]
operations = [
migrations.RenameModel(
old_name='IssueView',
new_name='View',
),
migrations.AlterModelTable(
name='view',
table='views',
),
migrations.RenameModel(
old_name='IssueViewFavorite',
new_name='ViewFavorite',
),
migrations.AlterField(
model_name='viewfavorite',
name='project',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterModelTable(
name='workspaceuserproperties',
table='workspace_user_properties',
),
migrations.AlterModelOptions(
name='view',
options={'ordering': ('-created_at',), 'verbose_name': 'View', 'verbose_name_plural': 'Views'},
),
migrations.AddField(
model_name='view',
name='is_locked',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='view',
name='is_pinned',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='view',
name='owned_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='views', to=settings.AUTH_USER_MODEL),
),
migrations.AlterField(
model_name='view',
name='access',
field=models.PositiveSmallIntegerField(choices=[(0, 'Private'), (1, 'Public'), (2, 'Shared')], default=1),
),
migrations.AlterField(
model_name='viewfavorite',
name='view',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='view_favorites', to='db.view'),
),
migrations.RunPython(views_owned_by)
]
+1 -1
View File
@@ -52,7 +52,7 @@ from .state import State
from .cycle import Cycle, CycleIssue, CycleFavorite, CycleUserProperties
from .view import GlobalView, IssueView, IssueViewFavorite
from .view import View, ViewFavorite
from .module import (
Module,
+23 -8
View File
@@ -3,7 +3,7 @@ from django.db import models
from django.conf import settings
# Module import
from . import ProjectBaseModel, BaseModel, WorkspaceBaseModel
from . import BaseModel, WorkspaceBaseModel
def get_default_filters():
@@ -84,7 +84,7 @@ class GlobalView(BaseModel):
return f"{self.name} <{self.workspace.name}>"
class IssueView(WorkspaceBaseModel):
class View(WorkspaceBaseModel):
name = models.CharField(max_length=255, verbose_name="View Name")
description = models.TextField(verbose_name="View Description", blank=True)
query = models.JSONField(verbose_name="View Query")
@@ -94,29 +94,44 @@ class IssueView(WorkspaceBaseModel):
default=get_default_display_properties
)
access = models.PositiveSmallIntegerField(
default=1, choices=((0, "Private"), (1, "Public"))
default=1, choices=((0, "Private"), (1, "Public"), (2, "Shared"))
)
owned_by = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="views", null=True, blank=True
)
sort_order = models.FloatField(default=65535)
is_locked = models.BooleanField(default=False)
is_pinned = models.BooleanField(default=False)
class Meta:
verbose_name = "Issue View"
verbose_name_plural = "Issue Views"
db_table = "issue_views"
verbose_name = "View"
verbose_name_plural = "Views"
db_table = "views"
ordering = ("-created_at",)
def save(self, *args, **kwargs):
if self._state.adding:
largest_sort_order = View.objects.filter(
workspace=self.workspace
).aggregate(largest=models.Max("sort_order"))["largest"]
if largest_sort_order is not None:
self.sort_order = largest_sort_order + 10000
super(View, self).save(*args, **kwargs)
def __str__(self):
"""Return name of the View"""
return f"{self.name} <{self.project.name}>"
class IssueViewFavorite(ProjectBaseModel):
class ViewFavorite(WorkspaceBaseModel):
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="user_view_favorites",
)
view = models.ForeignKey(
"db.IssueView", on_delete=models.CASCADE, related_name="view_favorites"
"db.View", on_delete=models.CASCADE, related_name="view_favorites"
)
class Meta:
+1 -1
View File
@@ -326,7 +326,7 @@ class WorkspaceUserProperties(BaseModel):
unique_together = ["workspace", "user"]
verbose_name = "Workspace User Property"
verbose_name_plural = "Workspace User Property"
db_table = "Workspace_user_properties"
db_table = "workspace_user_properties"
ordering = ("-created_at",)
def __str__(self):
+2 -2
View File
@@ -1,6 +1,6 @@
# base requirements
Django==4.2.7
Django==4.2.10
psycopg==3.1.12
djangorestframework==3.14.0
redis==4.6.0
@@ -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.6
cryptography==42.0.0
lxml==4.9.3
boto3==1.28.40
+1 -1
View File
@@ -137,7 +137,7 @@ services:
dockerfile: Dockerfile.dev
args:
DOCKER_BUILDKIT: 1
restart: no
restart: "no"
networks:
- dev_env
volumes:
+2 -2
View File
@@ -1,6 +1,6 @@
{
"repository": "https://github.com/makeplane/plane.git",
"version": "0.15.0",
"version": "0.15.1",
"license": "AGPL-3.0",
"private": true,
"workspaces": [
@@ -34,4 +34,4 @@
"@types/react": "18.2.42"
},
"packageManager": "yarn@1.22.19"
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/editor-core",
"version": "0.15.0",
"version": "0.15.1",
"description": "Core Editor that powers Plane",
"private": true,
"main": "./dist/index.mjs",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/document-editor",
"version": "0.15.0",
"version": "0.15.1",
"description": "Package that powers Plane's Pages Editor",
"main": "./dist/index.mjs",
"module": "./dist/index.mjs",
@@ -6,10 +6,16 @@ import { scrollSummary } from "src/utils/editor-summary-utils";
interface ContentBrowserProps {
editor: Editor;
markings: IMarking[];
setSidePeekVisible?: (sidePeekState: boolean) => void;
}
export const ContentBrowser = (props: ContentBrowserProps) => {
const { editor, markings } = props;
const { editor, markings, setSidePeekVisible } = props;
const handleOnClick = (marking: IMarking) => {
scrollSummary(editor, marking);
if (setSidePeekVisible) setSidePeekVisible(false);
}
return (
<div className="flex h-full flex-col overflow-hidden">
@@ -18,11 +24,11 @@ export const ContentBrowser = (props: ContentBrowserProps) => {
{markings.length !== 0 ? (
markings.map((marking) =>
marking.level === 1 ? (
<HeadingComp onClick={() => scrollSummary(editor, marking)} heading={marking.text} />
<HeadingComp onClick={() => handleOnClick(marking)} heading={marking.text} />
) : marking.level === 2 ? (
<SubheadingComp onClick={() => scrollSummary(editor, marking)} subHeading={marking.text} />
<SubheadingComp onClick={() => handleOnClick(marking)} subHeading={marking.text} />
) : (
<HeadingThreeComp heading={marking.text} onClick={() => scrollSummary(editor, marking)} />
<HeadingThreeComp heading={marking.text} onClick={() => handleOnClick(marking)} />
)
)
) : (
@@ -42,8 +42,8 @@ export const EditorHeader = (props: IEditorHeader) => {
} = props;
return (
<div className="flex items-center border-b border-custom-border-200 px-5 py-2">
<div className="w-56 flex-shrink-0 lg:w-72">
<div className="flex items-center border-b border-custom-border-200 md:px-5 px-3 py-2">
<div className="md:w-56 flex-shrink-0 lg:w-72 w-fit">
<SummaryPopover
editor={editor}
markings={markings}
@@ -52,7 +52,7 @@ export const EditorHeader = (props: IEditorHeader) => {
/>
</div>
<div className="flex-shrink-0">
<div className="flex-shrink-0 hidden md:flex">
{!readonly && uploadFile && (
<FixedMenu editor={editor} uploadFile={uploadFile} setIsSubmitting={setIsSubmitting} />
)}
@@ -152,7 +152,7 @@ export const PageRenderer = (props: IPageRenderer) => {
);
return (
<div className="w-full pb-64 pl-7 pt-5 page-renderer">
<div className="w-full pb-64 md:pl-7 pl-3 pt-5 page-renderer">
{!readonly ? (
<input
onChange={(e) => handlePageTitleChange(e.target.value)}
@@ -33,23 +33,36 @@ export const SummaryPopover: React.FC<Props> = (props) => {
<button
type="button"
ref={setReferenceElement}
className={`grid h-7 w-7 place-items-center rounded ${
sidePeekVisible ? "bg-custom-primary-100/20 text-custom-primary-100" : "text-custom-text-300"
}`}
className={`grid h-7 w-7 place-items-center rounded ${sidePeekVisible ? "bg-custom-primary-100/20 text-custom-primary-100" : "text-custom-text-300"
}`}
onClick={() => setSidePeekVisible(!sidePeekVisible)}
>
<List className="h-4 w-4" />
</button>
{!sidePeekVisible && (
<div
className="z-10 hidden max-h-80 w-64 overflow-y-auto rounded border-[0.5px] border-custom-border-200 bg-custom-background-100 p-3 shadow-custom-shadow-rg group-hover/summary-popover:block"
ref={setPopperElement}
style={summaryPopoverStyles.popper}
{...summaryPopoverAttributes.popper}
>
<ContentBrowser editor={editor} markings={markings} />
</div>
)}
<div className="md:hidden block">
{sidePeekVisible && (
<div
className="z-10 max-h-80 w-64 overflow-y-auto rounded border-[0.5px] border-custom-border-200 bg-custom-background-100 p-3 shadow-custom-shadow-rg"
ref={setPopperElement}
style={summaryPopoverStyles.popper}
{...summaryPopoverAttributes.popper}
>
<ContentBrowser setSidePeekVisible={setSidePeekVisible} editor={editor} markings={markings} />
</div>
)}
</div>
<div className="hidden md:block">
{!sidePeekVisible && (
<div
className="z-10 hidden max-h-80 w-64 overflow-y-auto rounded border-[0.5px] border-custom-border-200 bg-custom-background-100 p-3 shadow-custom-shadow-rg group-hover/summary-popover:block"
ref={setPopperElement}
style={summaryPopoverStyles.popper}
{...summaryPopoverAttributes.popper}
>
<ContentBrowser editor={editor} markings={markings} />
</div>
)}
</div>
</div>
);
};
@@ -10,6 +10,7 @@ import { DocumentDetails } from "src/types/editor-types";
import { PageRenderer } from "src/ui/components/page-renderer";
import { getMenuOptions } from "src/utils/menu-options";
import { useRouter } from "next/router";
import { FixedMenu } from "src";
interface IDocumentEditor {
// document info
@@ -149,11 +150,14 @@ const DocumentEditor = ({
documentDetails={documentDetails}
isSubmitting={isSubmitting}
/>
<div className="flex-shrink-0 md:hidden border-b border-custom-border-200 pl-3 py-2">
{uploadFile && <FixedMenu editor={editor} uploadFile={uploadFile} setIsSubmitting={setIsSubmitting} />}
</div>
<div className="flex h-full w-full overflow-y-auto frame-renderer">
<div className="sticky top-0 h-full w-56 flex-shrink-0 lg:w-72">
<div className="sticky top-0 h-full w-56 flex-shrink-0 lg:w-72 hidden md:block">
<SummarySideBar editor={editor} markings={markings} sidePeekVisible={sidePeekVisible} />
</div>
<div className="h-full w-[calc(100%-14rem)] lg:w-[calc(100%-18rem-18rem)] page-renderer">
<div className="h-full w-full md:w-[calc(100%-14rem)] lg:w-[calc(100%-18rem-18rem)] page-renderer">
<PageRenderer
onActionCompleteHandler={onActionCompleteHandler}
hideDragHandle={hideDragHandleOnMouseLeave}
@@ -77,7 +77,7 @@ export const FixedMenu = (props: EditorBubbleMenuProps) => {
}
return (
<div className="flex items-center divide-x divide-custom-border-200">
<div className="flex flex-wrap items-center divide-x divide-custom-border-200">
<div className="flex items-center gap-0.5 pr-2">
{basicMarkItems.map((item) => (
<button
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/editor-extensions",
"version": "0.15.0",
"version": "0.15.1",
"description": "Package that powers Plane's Editor with extensions",
"private": true,
"main": "./dist/index.mjs",
@@ -1,6 +1,6 @@
{
"name": "@plane/lite-text-editor",
"version": "0.15.0",
"version": "0.15.1",
"description": "Package that powers Plane's Comment Editor",
"private": true,
"main": "./dist/index.mjs",
@@ -1,6 +1,6 @@
{
"name": "@plane/rich-text-editor",
"version": "0.15.0",
"version": "0.15.1",
"description": "Rich Text Editor that powers Plane",
"private": true,
"main": "./dist/index.mjs",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "eslint-config-custom",
"private": true,
"version": "0.15.0",
"version": "0.15.1",
"main": "index.js",
"license": "MIT",
"dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "tailwind-config-custom",
"version": "0.15.0",
"version": "0.15.1",
"description": "common tailwind configuration across monorepo",
"main": "index.js",
"private": true,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "tsconfig",
"version": "0.15.0",
"version": "0.15.1",
"private": true,
"files": [
"base.json",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/types",
"version": "0.15.0",
"version": "0.15.1",
"private": true,
"main": "./src/index.d.ts"
}
-72
View File
@@ -1,72 +0,0 @@
import { TIssue } from "./issues/base";
import type { IProjectLite } from "./projects";
export type TInboxIssueExtended = {
completed_at: string | null;
start_date: string | null;
target_date: string | null;
};
export interface IInboxIssue extends TIssue, TInboxIssueExtended {
issue_inbox: {
duplicate_to: string | null;
id: string;
snoozed_till: Date | null;
source: string;
status: -2 | -1 | 0 | 1 | 2;
}[];
}
export interface IInbox {
id: string;
project_detail: IProjectLite;
pending_issue_count: number;
created_at: Date;
updated_at: Date;
name: string;
description: string;
is_default: boolean;
created_by: string;
updated_by: string;
project: string;
view_props: { filters: IInboxFilterOptions };
workspace: string;
}
interface StatePending {
readonly status: -2;
}
interface StatusReject {
status: -1;
}
interface StatusSnoozed {
status: 0;
snoozed_till: Date;
}
interface StatusAccepted {
status: 1;
}
interface StatusDuplicate {
status: 2;
duplicate_to: string;
}
export type TInboxStatus =
| StatusReject
| StatusSnoozed
| StatusAccepted
| StatusDuplicate
| StatePending;
export interface IInboxFilterOptions {
priority?: string[] | null;
inbox_status?: number[] | null;
}
export interface IInboxQueryParams {
priority: string | null;
inbox_status: string | null;
}
+1 -4
View File
@@ -13,11 +13,7 @@ export * from "./pages";
export * from "./ai";
export * from "./estimate";
export * from "./importer";
// FIXME: Remove this after development and the refactor/mobx-store-issue branch is stable
export * from "./inbox";
export * from "./inbox/root";
export * from "./analytics";
export * from "./calendar";
export * from "./notifications";
@@ -31,6 +27,7 @@ export * from "./auth";
export * from "./api_token";
export * from "./instance";
export * from "./app";
export * from "./view/root";
export type NestedKeyOf<ObjectType extends object> = {
[Key in keyof ObjectType & (string | number)]: ObjectType[Key] extends object
+8 -2
View File
@@ -1,5 +1,11 @@
import { EUserProjectRoles } from "constants/project";
import type { IUser, IUserLite, IWorkspace, IWorkspaceLite, TStateGroups } from ".";
import type {
IUser,
IUserLite,
IWorkspace,
IWorkspaceLite,
TStateGroups,
} from ".";
export interface IProject {
archive_in: number;
@@ -117,7 +123,7 @@ export type TProjectIssuesSearchParams = {
parent?: boolean;
issue_relation?: boolean;
cycle?: boolean;
module?: string[];
module?: string;
sub_issue?: boolean;
issue_id?: string;
workspace_search: boolean;
+54
View File
@@ -0,0 +1,54 @@
import {
TViewFilters,
TViewDisplayFilters,
TViewDisplayProperties,
} from "./filter";
export type TViewTypes =
| "WORKSPACE_PRIVATE_VIEWS"
| "WORKSPACE_PUBLIC_VIEWS"
| "PROJECT_PRIVATE_VIEWS"
| "PROJECT_PUBLIC_VIEWS";
declare enum EViewAccess {
"public" = 0,
"private" = 1,
"shared" = 2,
}
export type TViewAccess =
| EViewAccess.public
| EViewAccess.private
| EViewAccess.shared;
export type TView = {
id: string | undefined;
workspace: string | undefined;
project: string | undefined;
name: string | undefined;
description: string | undefined;
query: string | undefined;
filters: TViewFilters;
display_filters: TViewDisplayFilters;
display_properties: TViewDisplayProperties;
access: TViewAccess | undefined;
owned_by: string | undefined;
sort_order: number | undefined;
is_locked: boolean;
is_pinned: boolean;
is_favorite: boolean;
created_by: string | undefined;
updated_by: string | undefined;
created_at: Date | undefined;
updated_at: Date | undefined;
// local view variables
is_local_view: boolean;
};
export type TUpdateView = {
name: string | undefined;
description: string | undefined;
filters: TViewFilters;
display_filters: TViewDisplayFilters;
display_properties: TViewDisplayProperties;
};
+134
View File
@@ -0,0 +1,134 @@
declare enum EViewLayouts {
LIST = "list",
KANBAN = "kanban",
CALENDAR = "calendar",
SPREADSHEET = "spreadsheet",
GANTT = "gantt",
}
export type TViewLayouts =
| EViewLayouts.LIST
| EViewLayouts.KANBAN
| EViewLayouts.CALENDAR
| EViewLayouts.SPREADSHEET
| EViewLayouts.GANTT;
export type TViewDisplayFiltersGrouped =
| "project"
| "state_detail.group"
| "state"
| "priority"
| "labels"
| "created_by"
| "assignees"
| "mentions"
| "modules"
| "cycles";
export type TViewDisplayFiltersOrderBy =
| "sort_order"
| "created_at"
| "-created_at"
| "updated_at"
| "-updated_at"
| "start_date"
| "-start_date"
| "target_date"
| "-target_date"
| "state__name"
| "-state__name"
| "priority"
| "-priority"
| "labels__name"
| "-labels__name"
| "assignees__first_name"
| "-assignees__first_name"
| "estimate_point"
| "-estimate_point"
| "link_count"
| "-link_count"
| "attachment_count"
| "-attachment_count"
| "sub_issues_count"
| "-sub_issues_count";
export type TViewDisplayFiltersExtraOptions = "sub_issue" | "show_empty_groups";
export type TViewDisplayFiltersType = "active" | "backlog";
export type TViewCalendarLayouts = "month" | "week";
export type TViewFilters = {
project: string[];
module: string[];
cycle: string[];
priority: string[];
state: string[];
state_group: string[];
assignees: string[];
mentions: string[];
subscriber: string[];
created_by: string[];
labels: string[];
start_date: string[];
target_date: string[];
};
export type TViewDisplayFilters = {
layout: TViewLayouts;
group_by: TViewDisplayFiltersGrouped | undefined;
sub_group_by: TViewDisplayFiltersGrouped | undefined;
order_by: TViewDisplayFiltersOrderBy;
type: TViewDisplayFiltersType | undefined;
sub_issue: boolean;
show_empty_groups: boolean;
calendar: {
show_weekends: boolean;
layout: TViewCalendarLayouts;
};
};
export type TViewDisplayProperties = {
assignee: boolean;
start_date: boolean;
due_date: boolean;
labels: boolean;
key: boolean;
priority: boolean;
state: boolean;
sub_issue_count: boolean;
link: boolean;
attachment_count: boolean;
estimate: boolean;
created_on: boolean;
updated_on: boolean;
};
export type TViewFilterProps = {
filters: TViewFilters;
display_filters: TViewDisplayFilters;
display_properties: TViewDisplayProperties;
};
export type TViewFilterPartialProps = {
filters: Partial<TViewFilters>;
display_filters: Partial<TViewDisplayFilters>;
display_properties: Partial<TViewDisplayProperties>;
};
export type TViewFilterQueryParams =
| "project"
| "module"
| "cycle"
| "priority"
| "state"
| "state_group"
| "assignees"
| "mentions"
| "subscriber"
| "created_by"
| "labels"
| "start_date"
| "target_date"
| "type"
| "sub_issue";
+3
View File
@@ -0,0 +1,3 @@
export * from "./filter";
export * from "./base";
export * from "./user-base";
+18
View File
@@ -0,0 +1,18 @@
import {
TViewFilters,
TViewDisplayFilters,
TViewDisplayProperties,
} from "./filter";
export type TUserView = {
id: string | undefined;
workspace: string | undefined;
user: string | undefined;
filters: TViewFilters;
display_filters: TViewDisplayFilters;
display_properties: TViewDisplayProperties;
created_by: string | undefined;
updated_by: string | undefined;
created_at: Date | undefined;
updated_at: Date | undefined;
};
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "@plane/ui",
"description": "UI components shared across multiple apps internally",
"private": true,
"version": "0.15.0",
"version": "0.15.1",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
+55 -17
View File
@@ -1,35 +1,73 @@
import * as React from "react";
// icons
import { ChevronRight } from "lucide-react";
type BreadcrumbsProps = {
children: any;
children: React.ReactNode;
onBack?: () => void;
};
const Breadcrumbs = ({ children }: BreadcrumbsProps) => (
<div className="flex items-center space-x-2">
{React.Children.map(children, (child, index) => (
<div key={index} className="flex items-center gap-2.5">
{child}
{index !== React.Children.count(children) - 1 && (
<ChevronRight className="h-3.5 w-3.5 flex-shrink-0 text-custom-text-400" aria-hidden="true" />
)}
</div>
))}
</div>
);
const Breadcrumbs = ({ children, onBack }: BreadcrumbsProps) => {
const [isSmallScreen, setIsSmallScreen] = React.useState(false);
React.useEffect(() => {
const handleResize = () => {
setIsSmallScreen(window.innerWidth <= 640); // Adjust this value as per your requirement
};
window.addEventListener("resize", handleResize);
handleResize(); // Call it initially to set the correct state
return () => window.removeEventListener("resize", handleResize);
}, []);
const childrenArray = React.Children.toArray(children);
return (
<div className="flex items-center space-x-2 overflow-hidden">
{!isSmallScreen && (
<>
{childrenArray.map((child, index) => (
<React.Fragment key={index}>
{index > 0 && !isSmallScreen && (
<div className="flex items-center gap-2.5">
<ChevronRight
className="h-3.5 w-3.5 flex-shrink-0 text-custom-text-400"
aria-hidden="true"
/>
</div>
)}
<div className={`flex items-center gap-2.5 ${isSmallScreen && index > 0 ? 'hidden sm:flex' : 'flex'}`}>
{child}
</div>
</React.Fragment>
))}
</>
)}
{isSmallScreen && childrenArray.length > 1 && (
<>
<div className="flex items-center gap-2.5">
{onBack && <span onClick={onBack} className="text-custom-text-200">...</span>}
<ChevronRight className="h-3.5 w-3.5 flex-shrink-0 text-custom-text-400" aria-hidden="true" />
</div>
<div className="flex items-center gap-2.5">{childrenArray[childrenArray.length - 1]}</div>
</>
)}
{isSmallScreen && childrenArray.length === 1 && childrenArray}
</div>
);
};
type Props = {
type?: "text" | "component";
component?: React.ReactNode;
link?: JSX.Element;
};
const BreadcrumbItem: React.FC<Props> = (props) => {
const { type = "text", component, link } = props;
return <>{type != "text" ? <div className="flex items-center space-x-2">{component}</div> : link}</>;
return <>{type !== "text" ? <div className="flex items-center space-x-2">{component}</div> : link}</>;
};
Breadcrumbs.BreadcrumbItem = BreadcrumbItem;
export { Breadcrumbs, BreadcrumbItem };
export { Breadcrumbs, BreadcrumbItem };
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "space",
"version": "0.15.0",
"version": "0.15.1",
"private": true,
"scripts": {
"dev": "turbo run develop",
@@ -1,4 +1,4 @@
import { useEffect, } from "react";
import { useEffect } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import { mutate } from "swr";
@@ -138,14 +138,18 @@ export const CustomAnalyticsSidebar: React.FC<Props> = observer((props) => {
const selectedProjects = params.project && params.project.length > 0 ? params.project : workspaceProjectIds;
return (
<div className={cn("relative h-full flex w-full gap-2 justify-between items-start px-5 py-4 bg-custom-sidebar-background-100", !isProjectLevel ? "flex-col" : "")}
<div
className={cn(
"relative h-full flex w-full gap-2 justify-between items-start px-5 py-4 bg-custom-sidebar-background-100",
!isProjectLevel ? "flex-col" : ""
)}
>
<div className="flex flex-wrap items-center gap-2">
<div className="flex items-center gap-1 rounded-md bg-custom-background-80 px-3 py-1 text-xs text-custom-text-200">
<LayersIcon height={14} width={14} />
{analytics ? analytics.total : "..."} <div className={cn(isProjectLevel ? "hidden md:block" : "")}>Issues</div>
{analytics ? analytics.total : "..."}{" "}
<div className={cn(isProjectLevel ? "hidden md:block" : "")}>Issues</div>
</div>
{isProjectLevel && (
<div className="flex items-center gap-1 rounded-md bg-custom-background-80 px-3 py-1 text-xs text-custom-text-200">
@@ -154,8 +158,8 @@ export const CustomAnalyticsSidebar: React.FC<Props> = observer((props) => {
(cycleId
? cycleDetails?.created_at
: moduleId
? moduleDetails?.created_at
: projectDetails?.created_at) ?? ""
? moduleDetails?.created_at
: projectDetails?.created_at) ?? ""
)}
</div>
)}
@@ -78,7 +78,6 @@ export const ExistingIssuesListModal: React.FC<Props> = (props) => {
useEffect(() => {
if (!isOpen || !workspaceSlug || !projectId) return;
if (issues.length <= 0) setIsSearching(true);
projectService
.projectIssuesSearch(workspaceSlug as string, projectId as string, {
@@ -88,16 +87,7 @@ export const ExistingIssuesListModal: React.FC<Props> = (props) => {
})
.then((res) => setIssues(res))
.finally(() => setIsSearching(false));
}, [issues, debouncedSearchTerm, isOpen, isWorkspaceLevel, projectId, searchParams, workspaceSlug]);
useEffect(() => {
setSearchTerm("");
setIssues([]);
setSelectedIssues([]);
setIsSearching(false);
setIsSubmitting(false);
setIsWorkspaceLevel(false);
}, [isOpen]);
}, [debouncedSearchTerm, isOpen, isWorkspaceLevel, projectId, searchParams, workspaceSlug]);
return (
<>
@@ -40,13 +40,15 @@ const RenderIfVisible: React.FC<Props> = (props) => {
if (intersectionRef.current) {
const observer = new IntersectionObserver(
(entries) => {
if (typeof window !== undefined && window.requestIdleCallback) {
window.requestIdleCallback(() => setShouldVisible(entries[0].isIntersecting), {
timeout: 300,
});
} else {
setShouldVisible(entries[0].isIntersecting);
}
//DO no remove comments for future
// if (typeof window !== undefined && window.requestIdleCallback) {
// window.requestIdleCallback(() => setShouldVisible(entries[0].isIntersecting), {
// timeout: 300,
// });
// } else {
// setShouldVisible(entries[0].isIntersecting);
// }
setShouldVisible(entries[0].isIntersecting);
},
{
root: root?.current,
@@ -3,12 +3,20 @@ import { Menu } from "lucide-react";
import { useApplication } from "hooks/store";
import { observer } from "mobx-react";
export const SidebarHamburgerToggle: FC = observer(() => {
const { theme: themStore } = useApplication();
type Props = {
onClick?: () => void;
}
export const SidebarHamburgerToggle: FC<Props> = observer((props) => {
const { onClick } = props
const { theme: themeStore } = useApplication();
return (
<div
className="w-7 h-7 rounded flex justify-center items-center bg-custom-background-80 transition-all hover:bg-custom-background-90 cursor-pointer group md:hidden"
onClick={() => themStore.toggleSidebar()}
className="w-7 h-7 flex-shrink-0 rounded flex justify-center items-center bg-custom-background-80 transition-all hover:bg-custom-background-90 cursor-pointer group md:hidden"
onClick={() => {
if (onClick) onClick()
else themeStore.toggleMobileSidebar()
}}
>
<Menu size={14} className="text-custom-text-200 group-hover:text-custom-text-100 transition-all" />
</div>
@@ -34,7 +34,8 @@ import { ICycle, TCycleGroups } from "@plane/types";
// constants
import { EIssuesStoreType } from "constants/issue";
import { CYCLE_ISSUES_WITH_PARAMS } from "constants/fetch-keys";
import { CYCLE_EMPTY_STATE_DETAILS, CYCLE_STATE_GROUPS_DETAILS } from "constants/cycle";
import { CYCLE_STATE_GROUPS_DETAILS } from "constants/cycle";
import { CYCLE_EMPTY_STATE_DETAILS } from "constants/empty-state";
interface IActiveCycleDetails {
workspaceSlug: string;
@@ -38,7 +38,7 @@ export const CyclePeekOverview: React.FC<Props> = observer(({ projectId, workspa
{peekCycle && (
<div
ref={ref}
className="flex h-full w-[24rem] flex-shrink-0 flex-col gap-3.5 overflow-y-auto border-l border-custom-border-100 bg-custom-sidebar-background-100 px-6 py-3.5 duration-300"
className="flex h-full w-full max-w-[24rem] flex-shrink-0 flex-col gap-3.5 overflow-y-auto border-l border-custom-border-100 bg-custom-sidebar-background-100 px-6 py-3.5 duration-300 fixed md:relative right-0 z-[9]"
style={{
boxShadow:
"0px 1px 4px 0px rgba(0, 0, 0, 0.06), 0px 2px 4px 0px rgba(16, 24, 40, 0.06), 0px 1px 8px -1px rgba(16, 24, 40, 0.06)",
+5 -4
View File
@@ -1,6 +1,7 @@
import { FC, MouseEvent, useState } from "react";
import { useRouter } from "next/router";
import Link from "next/link";
import { observer } from "mobx-react";
// hooks
import { useEventTracker, useCycle, useUser } from "hooks/store";
import useToast from "hooks/use-toast";
@@ -26,7 +27,7 @@ export interface ICyclesBoardCard {
cycleId: string;
}
export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
export const CyclesBoardCard: FC<ICyclesBoardCard> = observer((props) => {
const { cycleId, workspaceSlug, projectId } = props;
// states
const [updateModal, setUpdateModal] = useState(false);
@@ -175,7 +176,7 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
/>
<Link href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycleDetails.id}`}>
<div className="flex h-44 w-full min-w-[250px] flex-col justify-between rounded border border-custom-border-100 bg-custom-background-100 p-4 text-sm hover:shadow-md">
<div className="flex h-44 w-full flex-col justify-between rounded border border-custom-border-100 bg-custom-background-100 p-4 text-sm hover:shadow-md">
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-3 truncate">
<span className="flex-shrink-0">
@@ -253,7 +254,7 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
) : (
<span className="text-xs text-custom-text-400">No due date</span>
)}
<div className="z-10 flex items-center gap-1.5">
<div className="z-[5] flex items-center gap-1.5">
{isEditingAllowed &&
(cycleDetails.is_favorite ? (
<button type="button" onClick={handleRemoveFromFavorites}>
@@ -295,4 +296,4 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
</Link>
</div>
);
};
});
+1 -1
View File
@@ -7,7 +7,7 @@ import { useUser } from "hooks/store";
import { CyclePeekOverview, CyclesBoardCard } from "components/cycles";
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
// constants
import { CYCLE_EMPTY_STATE_DETAILS } from "constants/cycle";
import { CYCLE_EMPTY_STATE_DETAILS } from "constants/empty-state";
export interface ICyclesBoard {
cycleIds: string[];
+4 -3
View File
@@ -1,6 +1,7 @@
import { FC, MouseEvent, useState } from "react";
import Link from "next/link";
import { useRouter } from "next/router";
import { observer } from "mobx-react";
// hooks
import { useEventTracker, useCycle, useUser } from "hooks/store";
import useToast from "hooks/use-toast";
@@ -30,7 +31,7 @@ type TCyclesListItem = {
projectId: string;
};
export const CyclesListItem: FC<TCyclesListItem> = (props) => {
export const CyclesListItem: FC<TCyclesListItem> = observer((props) => {
const { cycleId, workspaceSlug, projectId } = props;
// states
const [updateModal, setUpdateModal] = useState(false);
@@ -204,7 +205,7 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
</Tooltip>
</div>
<button onClick={openCycleOverview} className="invisible z-10 flex-shrink-0 group-hover:visible">
<button onClick={openCycleOverview} className="flex-shrink-0 z-[5] invisible group-hover:visible">
<Info className="h-4 w-4 text-custom-text-400" />
</button>
</div>
@@ -289,4 +290,4 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
</Link>
</>
);
};
});
+1 -1
View File
@@ -9,7 +9,7 @@ import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
// ui
import { Loader } from "@plane/ui";
// constants
import { CYCLE_EMPTY_STATE_DETAILS } from "constants/cycle";
import { CYCLE_EMPTY_STATE_DETAILS } from "constants/empty-state";
export interface ICyclesList {
cycleIds: string[];
+20 -42
View File
@@ -5,7 +5,7 @@ import { useCycle } from "hooks/store";
// components
import { CyclesBoard, CyclesList, CyclesListGanttChartView } from "components/cycles";
// ui components
import { Loader } from "@plane/ui";
import { CycleModuleBoardLayout, CycleModuleListLayout, GanttLayoutLoader } from "components/ui";
// types
import { TCycleLayout, TCycleView } from "@plane/types";
@@ -25,6 +25,7 @@ export const CyclesView: FC<ICyclesView> = observer((props) => {
currentProjectDraftCycleIds,
currentProjectUpcomingCycleIds,
currentProjectCycleIds,
loader,
} = useCycle();
const cyclesList =
@@ -36,55 +37,32 @@ export const CyclesView: FC<ICyclesView> = observer((props) => {
? currentProjectUpcomingCycleIds
: currentProjectCycleIds;
if (loader || !cyclesList)
return (
<>
{layout === "list" && <CycleModuleListLayout />}
{layout === "board" && <CycleModuleBoardLayout />}
{layout === "gantt" && <GanttLayoutLoader />}
</>
);
return (
<>
{layout === "list" && (
<>
{cyclesList ? (
<CyclesList cycleIds={cyclesList} filter={filter} workspaceSlug={workspaceSlug} projectId={projectId} />
) : (
<Loader className="space-y-4 p-8">
<Loader.Item height="50px" />
<Loader.Item height="50px" />
<Loader.Item height="50px" />
</Loader>
)}
</>
<CyclesList cycleIds={cyclesList} filter={filter} workspaceSlug={workspaceSlug} projectId={projectId} />
)}
{layout === "board" && (
<>
{cyclesList ? (
<CyclesBoard
cycleIds={cyclesList}
filter={filter}
workspaceSlug={workspaceSlug}
projectId={projectId}
peekCycle={peekCycle}
/>
) : (
<Loader className="grid grid-cols-1 gap-9 p-8 md:grid-cols-2 lg:grid-cols-3">
<Loader.Item height="200px" />
<Loader.Item height="200px" />
<Loader.Item height="200px" />
</Loader>
)}
</>
<CyclesBoard
cycleIds={cyclesList}
filter={filter}
workspaceSlug={workspaceSlug}
projectId={projectId}
peekCycle={peekCycle}
/>
)}
{layout === "gantt" && (
<>
{cyclesList ? (
<CyclesListGanttChartView cycleIds={cyclesList} workspaceSlug={workspaceSlug} />
) : (
<Loader className="space-y-4">
<Loader.Item height="50px" />
<Loader.Item height="50px" />
<Loader.Item height="50px" />
</Loader>
)}
</>
)}
{layout === "gantt" && <CyclesListGanttChartView cycleIds={cyclesList} workspaceSlug={workspaceSlug} />}
</>
);
});
+39 -16
View File
@@ -1,16 +1,30 @@
import { useRouter } from "next/router";
import { observer } from "mobx-react";
// hooks
import { useApplication, useCycle } from "hooks/store";
// ui
import { Tooltip, ContrastIcon } from "@plane/ui";
// helpers
import { renderFormattedDate } from "helpers/date-time.helper";
// types
import { ICycle } from "@plane/types";
export const CycleGanttBlock = ({ data }: { data: ICycle }) => {
type Props = {
cycleId: string;
};
export const CycleGanttBlock: React.FC<Props> = observer((props) => {
const { cycleId } = props;
// router
const router = useRouter();
const { workspaceSlug } = router.query;
// store hooks
const {
router: { workspaceSlug },
} = useApplication();
const { getCycleById } = useCycle();
// derived values
const cycleDetails = getCycleById(cycleId);
const cycleStatus = cycleDetails?.status.toLocaleLowerCase();
const cycleStatus = data.status.toLocaleLowerCase();
return (
<div
className="relative flex h-full w-full items-center rounded"
@@ -26,36 +40,45 @@ export const CycleGanttBlock = ({ data }: { data: ICycle }) => {
? "rgb(var(--color-text-200))"
: "",
}}
onClick={() => router.push(`/${workspaceSlug}/projects/${data?.project}/cycles/${data?.id}`)}
onClick={() => router.push(`/${workspaceSlug}/projects/${cycleDetails?.project}/cycles/${cycleDetails?.id}`)}
>
<div className="absolute left-0 top-0 h-full w-full bg-custom-background-100/50" />
<Tooltip
tooltipContent={
<div className="space-y-1">
<h5>{data?.name}</h5>
<h5>{cycleDetails?.name}</h5>
<div>
{renderFormattedDate(data?.start_date ?? "")} to {renderFormattedDate(data?.end_date ?? "")}
{renderFormattedDate(cycleDetails?.start_date ?? "")} to{" "}
{renderFormattedDate(cycleDetails?.end_date ?? "")}
</div>
</div>
}
position="top-left"
>
<div className="relative w-full truncate px-2.5 py-1 text-sm text-custom-text-100">{data?.name}</div>
<div className="relative w-full truncate px-2.5 py-1 text-sm text-custom-text-100">{cycleDetails?.name}</div>
</Tooltip>
</div>
);
};
});
export const CycleGanttSidebarBlock = ({ data }: { data: ICycle }) => {
export const CycleGanttSidebarBlock: React.FC<Props> = observer((props) => {
const { cycleId } = props;
// router
const router = useRouter();
const { workspaceSlug } = router.query;
// store hooks
const {
router: { workspaceSlug },
} = useApplication();
const { getCycleById } = useCycle();
// derived values
const cycleDetails = getCycleById(cycleId);
const cycleStatus = data.status.toLocaleLowerCase();
const cycleStatus = cycleDetails?.status.toLocaleLowerCase();
return (
<div
className="relative flex h-full w-full items-center gap-2"
onClick={() => router.push(`/${workspaceSlug}/projects/${data?.project}/cycles/${data?.id}`)}
onClick={() => router.push(`/${workspaceSlug}/projects/${cycleDetails?.project}/cycles/${cycleDetails?.id}`)}
>
<ContrastIcon
className="h-5 w-5 flex-shrink-0"
@@ -71,7 +94,7 @@ export const CycleGanttSidebarBlock = ({ data }: { data: ICycle }) => {
: ""
}`}
/>
<h6 className="flex-grow truncate text-sm font-medium">{data?.name}</h6>
<h6 className="flex-grow truncate text-sm font-medium">{cycleDetails?.name}</h6>
</div>
);
};
});
@@ -63,7 +63,7 @@ export const CyclesListGanttChartView: FC<Props> = observer((props) => {
blocks={cycleIds ? blockFormat(cycleIds.map((c) => getCycleById(c))) : null}
blockUpdateHandler={(block, payload) => handleCycleUpdate(block, payload)}
sidebarToRender={(props) => <CycleGanttSidebar {...props} />}
blockToRender={(data: ICycle) => <CycleGanttBlock data={data} />}
blockToRender={(data: ICycle) => <CycleGanttBlock cycleId={data.id} />}
enableBlockLeftResize={false}
enableBlockRightResize={false}
enableBlockMove={false}
+2 -2
View File
@@ -100,7 +100,7 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
});
})
.catch((_) => {
.catch(() => {
captureCycleEvent({
eventName: CYCLE_UPDATED,
payload: {
@@ -329,7 +329,7 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
const isEndValid = new Date(`${cycleDetails?.end_date}`) >= new Date(`${cycleDetails?.start_date}`);
const progressPercentage = cycleDetails
? isCompleted
? isCompleted && cycleDetails?.progress_snapshot
? Math.round(
(cycleDetails.progress_snapshot.completed_issues / cycleDetails.progress_snapshot.total_issues) * 100
)
+10 -9
View File
@@ -61,6 +61,7 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
const [isOpen, setIsOpen] = useState(false);
// refs
const dropdownRef = useRef<HTMLDivElement | null>(null);
const inputRef = useRef<HTMLInputElement | null>(null);
// popper-js refs
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
@@ -111,23 +112,15 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
const filteredOptions =
query === "" ? options : options?.filter((o) => o.query.toLowerCase().includes(query.toLowerCase()));
// fetch cycles of the project if not already present in the store
useEffect(() => {
if (!workspaceSlug) return;
if (!cycleIds) fetchAllCycles(workspaceSlug, projectId);
}, [cycleIds, fetchAllCycles, projectId, workspaceSlug]);
const selectedCycle = value ? getCycleById(value) : null;
const onOpen = () => {
if (referenceElement) referenceElement.focus();
if (workspaceSlug && !cycleIds) fetchAllCycles(workspaceSlug, projectId);
};
const handleClose = () => {
if (!isOpen) return;
setIsOpen(false);
if (referenceElement) referenceElement.blur();
onClose && onClose();
};
@@ -151,6 +144,12 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
useOutsideClickDetector(dropdownRef, handleClose);
useEffect(() => {
if (isOpen && inputRef.current) {
inputRef.current.focus();
}
}, [isOpen]);
return (
<Combobox
as="div"
@@ -216,6 +215,8 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
<div className="flex items-center gap-1.5 rounded border border-custom-border-100 bg-custom-background-90 px-2">
<Search className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={1.5} />
<Combobox.Input
as="input"
ref={inputRef}
className="w-full bg-transparent py-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
value={query}
onChange={(e) => setQuery(e.target.value)}
+10 -3
View File
@@ -1,4 +1,4 @@
import { Fragment, ReactNode, useRef, useState } from "react";
import { Fragment, ReactNode, useEffect, useRef, useState } from "react";
import { observer } from "mobx-react-lite";
import { Combobox } from "@headlessui/react";
import { usePopper } from "react-popper";
@@ -60,6 +60,7 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
const [isOpen, setIsOpen] = useState(false);
// refs
const dropdownRef = useRef<HTMLDivElement | null>(null);
const inputRef = useRef<HTMLInputElement | null>(null);
// popper-js refs
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
@@ -110,13 +111,11 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
const onOpen = () => {
if (!activeEstimate && workspaceSlug) fetchProjectEstimates(workspaceSlug, projectId);
if (referenceElement) referenceElement.focus();
};
const handleClose = () => {
if (!isOpen) return;
setIsOpen(false);
if (referenceElement) referenceElement.blur();
onClose && onClose();
};
@@ -140,6 +139,12 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
useOutsideClickDetector(dropdownRef, handleClose);
useEffect(() => {
if (isOpen && inputRef.current) {
inputRef.current.focus();
}
}, [isOpen]);
return (
<Combobox
as="div"
@@ -205,6 +210,8 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
<div className="flex items-center gap-1.5 rounded border border-custom-border-100 bg-custom-background-90 px-2">
<Search className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={1.5} />
<Combobox.Input
as="input"
ref={inputRef}
className="w-full bg-transparent py-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
value={query}
onChange={(e) => setQuery(e.target.value)}
@@ -1,4 +1,4 @@
import { Fragment, useRef, useState } from "react";
import { Fragment, useEffect, useRef, useState } from "react";
import { observer } from "mobx-react-lite";
import { Combobox } from "@headlessui/react";
import { usePopper } from "react-popper";
@@ -50,6 +50,7 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
const [isOpen, setIsOpen] = useState(false);
// refs
const dropdownRef = useRef<HTMLDivElement | null>(null);
const inputRef = useRef<HTMLInputElement | null>(null);
// popper-js refs
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
@@ -103,13 +104,11 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
const onOpen = () => {
if (!projectMemberIds && workspaceSlug) fetchProjectMembers(workspaceSlug, projectId);
if (referenceElement) referenceElement.focus();
};
const handleClose = () => {
if (!isOpen) return;
setIsOpen(false);
if (referenceElement) referenceElement.blur();
onClose && onClose();
};
@@ -133,6 +132,12 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
useOutsideClickDetector(dropdownRef, handleClose);
useEffect(() => {
if (isOpen && inputRef.current) {
inputRef.current.focus();
}
}, [isOpen]);
return (
<Combobox
as="div"
@@ -203,6 +208,8 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
<div className="flex items-center gap-1.5 rounded border border-custom-border-100 bg-custom-background-90 px-2">
<Search className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={1.5} />
<Combobox.Input
as="input"
ref={inputRef}
className="w-full bg-transparent py-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
value={query}
onChange={(e) => setQuery(e.target.value)}
@@ -1,4 +1,4 @@
import { Fragment, useRef, useState } from "react";
import { Fragment, useEffect, useRef, useState } from "react";
import { observer } from "mobx-react-lite";
import { Combobox } from "@headlessui/react";
import { usePopper } from "react-popper";
@@ -44,6 +44,7 @@ export const WorkspaceMemberDropdown: React.FC<MemberDropdownProps> = observer((
const [isOpen, setIsOpen] = useState<boolean>(false);
// refs
const dropdownRef = useRef<HTMLDivElement | null>(null);
const inputRef = useRef<HTMLInputElement | null>(null);
// popper-js refs
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
@@ -91,19 +92,13 @@ export const WorkspaceMemberDropdown: React.FC<MemberDropdownProps> = observer((
};
if (multiple) comboboxProps.multiple = true;
const onOpen = () => {
if (referenceElement) referenceElement.focus();
};
const handleClose = () => {
if (!isOpen) return;
setIsOpen(false);
if (referenceElement) referenceElement.blur();
onClose && onClose();
};
const toggleDropdown = () => {
if (!isOpen) onOpen();
setIsOpen((prevIsOpen) => !prevIsOpen);
};
@@ -122,6 +117,12 @@ export const WorkspaceMemberDropdown: React.FC<MemberDropdownProps> = observer((
useOutsideClickDetector(dropdownRef, handleClose);
useEffect(() => {
if (isOpen && inputRef.current) {
inputRef.current.focus();
}
}, [isOpen]);
return (
<Combobox
as="div"
@@ -192,6 +193,8 @@ export const WorkspaceMemberDropdown: React.FC<MemberDropdownProps> = observer((
<div className="flex items-center gap-1.5 rounded border border-custom-border-100 bg-custom-background-90 px-2">
<Search className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={1.5} />
<Combobox.Input
as="input"
ref={inputRef}
className="w-full bg-transparent py-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
value={query}
onChange={(e) => setQuery(e.target.value)}
+10 -9
View File
@@ -166,6 +166,7 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
const [isOpen, setIsOpen] = useState(false);
// refs
const dropdownRef = useRef<HTMLDivElement | null>(null);
const inputRef = useRef<HTMLInputElement | null>(null);
// popper-js refs
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
@@ -216,21 +217,13 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
const filteredOptions =
query === "" ? options : options?.filter((o) => o.query.toLowerCase().includes(query.toLowerCase()));
// fetch modules of the project if not already present in the store
useEffect(() => {
if (!workspaceSlug) return;
if (!moduleIds) fetchModules(workspaceSlug, projectId);
}, [moduleIds, fetchModules, projectId, workspaceSlug]);
const onOpen = () => {
if (referenceElement) referenceElement.focus();
if (!moduleIds && workspaceSlug) fetchModules(workspaceSlug, projectId);
};
const handleClose = () => {
if (!isOpen) return;
setIsOpen(false);
if (referenceElement) referenceElement.blur();
onClose && onClose();
};
@@ -261,6 +254,12 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
};
if (multiple) comboboxProps.multiple = true;
useEffect(() => {
if (isOpen && inputRef.current) {
inputRef.current.focus();
}
}, [isOpen]);
return (
<Combobox
as="div"
@@ -331,6 +330,8 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
<div className="flex items-center gap-1.5 rounded border border-custom-border-100 bg-custom-background-90 px-2">
<Search className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={1.5} />
<Combobox.Input
as="input"
ref={inputRef}
className="w-full bg-transparent py-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
value={query}
onChange={(e) => setQuery(e.target.value)}
+10 -7
View File
@@ -1,4 +1,4 @@
import { Fragment, ReactNode, useRef, useState } from "react";
import { Fragment, ReactNode, useEffect, useRef, useState } from "react";
import { Combobox } from "@headlessui/react";
import { usePopper } from "react-popper";
import { Check, ChevronDown, Search } from "lucide-react";
@@ -272,6 +272,7 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
const [isOpen, setIsOpen] = useState(false);
// refs
const dropdownRef = useRef<HTMLDivElement | null>(null);
const inputRef = useRef<HTMLInputElement | null>(null);
// popper-js refs
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
@@ -305,19 +306,13 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
const filteredOptions =
query === "" ? options : options.filter((o) => o.query.toLowerCase().includes(query.toLowerCase()));
const onOpen = () => {
if (referenceElement) referenceElement.focus();
};
const handleClose = () => {
if (!isOpen) return;
setIsOpen(false);
if (referenceElement) referenceElement.blur();
onClose && onClose();
};
const toggleDropdown = () => {
if (!isOpen) onOpen();
setIsOpen((prevIsOpen) => !prevIsOpen);
};
@@ -342,6 +337,12 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
? BackgroundButton
: TransparentButton;
useEffect(() => {
if (isOpen && inputRef.current) {
inputRef.current.focus();
}
}, [isOpen]);
return (
<Combobox
as="div"
@@ -409,6 +410,8 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
<div className="flex items-center gap-1.5 rounded border border-custom-border-100 bg-custom-background-90 px-2">
<Search className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={1.5} />
<Combobox.Input
as="input"
ref={inputRef}
className="w-full bg-transparent py-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
value={query}
onChange={(e) => setQuery(e.target.value)}
+10 -7
View File
@@ -1,4 +1,4 @@
import { Fragment, ReactNode, useRef, useState } from "react";
import { Fragment, ReactNode, useEffect, useRef, useState } from "react";
import { observer } from "mobx-react-lite";
import { Combobox } from "@headlessui/react";
import { usePopper } from "react-popper";
@@ -50,6 +50,7 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
const [isOpen, setIsOpen] = useState(false);
// refs
const dropdownRef = useRef<HTMLDivElement | null>(null);
const inputRef = useRef<HTMLInputElement | null>(null);
// popper-js refs
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
@@ -94,19 +95,13 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
const selectedProject = value ? getProjectById(value) : null;
const onOpen = () => {
if (referenceElement) referenceElement.focus();
};
const handleClose = () => {
if (!isOpen) return;
setIsOpen(false);
onClose && onClose();
if (referenceElement) referenceElement.blur();
};
const toggleDropdown = () => {
if (!isOpen) onOpen();
setIsOpen((prevIsOpen) => !prevIsOpen);
};
@@ -125,6 +120,12 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
useOutsideClickDetector(dropdownRef, handleClose);
useEffect(() => {
if (isOpen && inputRef.current) {
inputRef.current.focus();
}
}, [isOpen]);
return (
<Combobox
as="div"
@@ -198,6 +199,8 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
<div className="flex items-center gap-1.5 rounded border border-custom-border-100 bg-custom-background-90 px-2">
<Search className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={1.5} />
<Combobox.Input
as="input"
ref={inputRef}
className="w-full bg-transparent py-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
value={query}
onChange={(e) => setQuery(e.target.value)}
+10 -3
View File
@@ -1,4 +1,4 @@
import { Fragment, ReactNode, useRef, useState } from "react";
import { Fragment, ReactNode, useEffect, useRef, useState } from "react";
import { observer } from "mobx-react-lite";
import { Combobox } from "@headlessui/react";
import { usePopper } from "react-popper";
@@ -52,6 +52,7 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
const [isOpen, setIsOpen] = useState(false);
// refs
const dropdownRef = useRef<HTMLDivElement | null>(null);
const inputRef = useRef<HTMLInputElement | null>(null);
// popper-js refs
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
@@ -92,14 +93,12 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
const onOpen = () => {
if (!statesList && workspaceSlug) fetchProjectStates(workspaceSlug, projectId);
if (referenceElement) referenceElement.focus();
};
const handleClose = () => {
if (!isOpen) return;
setIsOpen(false);
onClose && onClose();
if (referenceElement) referenceElement.blur();
};
const toggleDropdown = () => {
@@ -122,6 +121,12 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
useOutsideClickDetector(dropdownRef, handleClose);
useEffect(() => {
if (isOpen && inputRef.current) {
inputRef.current.focus();
}
}, [isOpen]);
return (
<Combobox
as="div"
@@ -193,6 +198,8 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
<div className="flex items-center gap-1.5 rounded border border-custom-border-100 bg-custom-background-90 px-2">
<Search className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={1.5} />
<Combobox.Input
as="input"
ref={inputRef}
className="w-full bg-transparent py-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
value={query}
onChange={(e) => setQuery(e.target.value)}
+4 -4
View File
@@ -65,14 +65,14 @@ export const EmptyState: React.FC<Props> = ({
);
return (
<div className="flex items-center justify-center min-h-full min-w-full overflow-y-auto py-10 px-20">
<div className="flex items-center justify-center min-h-full min-w-full overflow-y-auto py-10 md:px-20 px-5">
<div
className={cn("flex flex-col gap-5", {
"min-w-[24rem] max-w-[45rem]": size === "sm",
"min-w-[30rem] max-w-[60rem]": size === "lg",
"md:min-w-[24rem] max-w-[45rem]": size === "sm",
"md:min-w-[30rem] max-w-[60rem]": size === "lg",
})}
>
<div className="flex flex-col gap-1.5 flex-shrink-0">{emptyStateHeader}</div>
<div className="flex flex-col gap-1.5 flex-shrink">{emptyStateHeader}</div>
<Image
src={image}
+17 -17
View File
@@ -1,21 +1,21 @@
import React, { useState } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import { Plus } from "lucide-react";
import { useTheme } from "next-themes";
// store hooks
import { useEstimate, useProject } from "hooks/store";
import { useEstimate, useProject, useUser } from "hooks/store";
import useToast from "hooks/use-toast";
// components
import { CreateUpdateEstimateModal, DeleteEstimateModal, EstimateListItem } from "components/estimates";
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
// ui
import { Button, Loader } from "@plane/ui";
import { EmptyState } from "components/common";
// images
import emptyEstimate from "public/empty-state/estimate.svg";
// types
import { IEstimate } from "@plane/types";
// helpers
import { orderArrayBy } from "helpers/array.helper";
// constants
import { PROJECT_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state";
export const EstimatesList: React.FC = observer(() => {
// states
@@ -25,9 +25,12 @@ export const EstimatesList: React.FC = observer(() => {
// router
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
// theme
const { resolvedTheme } = useTheme();
// store hooks
const { updateProject, currentProjectDetails } = useProject();
const { projectEstimates, getProjectEstimateById } = useEstimate();
const { currentUser } = useUser();
// toast alert
const { setToastAlert } = useToast();
@@ -55,6 +58,10 @@ export const EstimatesList: React.FC = observer(() => {
});
};
const emptyStateDetail = PROJECT_SETTINGS_EMPTY_STATE_DETAILS["estimate"];
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
const emptyStateImage = getEmptyStateImagePath("project-settings", "estimates", isLightMode);
return (
<>
<CreateUpdateEstimateModal
@@ -108,19 +115,12 @@ export const EstimatesList: React.FC = observer(() => {
))}
</section>
) : (
<div className="w-full py-8">
<div className="h-full w-full py-8">
<EmptyState
title="No estimates yet"
description="Estimates help you communicate the complexity of an issue."
image={emptyEstimate}
primaryButton={{
icon: <Plus className="h-4 w-4" />,
text: "Add Estimate",
onClick: () => {
setEstimateFormOpen(true);
setEstimateToUpdate(undefined);
},
}}
title={emptyStateDetail.title}
description={emptyStateDetail.description}
image={emptyStateImage}
size="lg"
/>
</div>
)
+23 -12
View File
@@ -3,25 +3,28 @@ import { useState } from "react";
import Link from "next/link";
import Image from "next/image";
import { useRouter } from "next/router";
import { useTheme } from "next-themes";
import useSWR, { mutate } from "swr";
import { observer } from "mobx-react-lite";
// hooks
import { useUser } from "hooks/store";
import useUserAuth from "hooks/use-user-auth";
// services
import { IntegrationService } from "services/integrations";
// components
import { Exporter, SingleExport } from "components/exporter";
import { EmptyState, getEmptyStateImagePath } from "components/empty-state";
// ui
import { Button, Loader } from "@plane/ui";
import { Button } from "@plane/ui";
import { ImportExportSettingsLoader } from "components/ui";
// icons
import { MoveLeft, MoveRight, RefreshCw } from "lucide-react";
// fetch-keys
import { EXPORT_SERVICES_LIST } from "constants/fetch-keys";
// constants
import { EXPORTERS_LIST } from "constants/workspace";
import { observer } from "mobx-react-lite";
import { useUser } from "hooks/store";
import { WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS } from "constants/empty-state";
// services
const integrationService = new IntegrationService();
@@ -34,6 +37,8 @@ const IntegrationGuide = observer(() => {
// router
const router = useRouter();
const { workspaceSlug, provider } = router.query;
// theme
const { resolvedTheme } = useTheme();
// store hooks
const { currentUser, currentUserLoader } = useUser();
// custom hooks
@@ -46,6 +51,10 @@ const IntegrationGuide = observer(() => {
: null
);
const emptyStateDetail = WORKSPACE_SETTINGS_EMPTY_STATE_DETAILS["export"];
const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light";
const emptyStateImage = getEmptyStateImagePath("workspace-settings", "exports", isLightMode);
const handleCsvClose = () => {
router.replace(`/${workspaceSlug?.toString()}/settings/exports`);
};
@@ -140,15 +149,17 @@ const IntegrationGuide = observer(() => {
</div>
</div>
) : (
<p className="px-4 py-6 text-sm text-custom-text-200">No previous export available.</p>
<div className="h-full w-full flex items-center justify-center">
<EmptyState
title={emptyStateDetail.title}
description={emptyStateDetail.description}
image={emptyStateImage}
size="sm"
/>
</div>
)
) : (
<Loader className="mt-6 grid grid-cols-1 gap-3">
<Loader.Item height="40px" width="100%" />
<Loader.Item height="40px" width="100%" />
<Loader.Item height="40px" width="100%" />
<Loader.Item height="40px" width="100%" />
</Loader>
<ImportExportSettingsLoader />
)}
</div>
</div>
@@ -1,3 +1,4 @@
import { observer } from "mobx-react";
import { FC } from "react";
// hooks
import { useIssueDetail } from "hooks/store";
@@ -8,6 +9,8 @@ import { renderFormattedPayloadDate } from "helpers/date-time.helper";
import { cn } from "helpers/common.helper";
// types
import { IBlockUpdateData, IGanttBlock } from "../types";
// constants
import { BLOCK_HEIGHT, HEADER_HEIGHT } from "../constants";
export type GanttChartBlocksProps = {
itemsContainerWidth: number;
@@ -17,10 +20,11 @@ export type GanttChartBlocksProps = {
enableBlockLeftResize: boolean;
enableBlockRightResize: boolean;
enableBlockMove: boolean;
enableAddBlock: boolean;
showAllBlocks: boolean;
};
export const GanttChartBlocks: FC<GanttChartBlocksProps> = (props) => {
export const GanttChartBlocksList: FC<GanttChartBlocksProps> = observer((props) => {
const {
itemsContainerWidth,
blocks,
@@ -29,11 +33,13 @@ export const GanttChartBlocks: FC<GanttChartBlocksProps> = (props) => {
enableBlockLeftResize,
enableBlockRightResize,
enableBlockMove,
enableAddBlock,
showAllBlocks,
} = props;
const { activeBlock, dispatch } = useChart();
// store hooks
const { peekIssue } = useIssueDetail();
// chart hook
const { activeBlock, dispatch } = useChart();
// update the active block on hover
const updateActiveBlock = (block: IGanttBlock | null) => {
@@ -77,43 +83,51 @@ export const GanttChartBlocks: FC<GanttChartBlocksProps> = (props) => {
return (
<div
className="relative z-[5] mt-[72px] h-full overflow-hidden overflow-y-auto"
style={{ width: `${itemsContainerWidth}px` }}
className="h-full"
style={{
width: `${itemsContainerWidth}px`,
transform: `translateY(${HEADER_HEIGHT}px)`,
}}
>
{blocks &&
blocks.length > 0 &&
blocks.map((block) => {
// hide the block if it doesn't have start and target dates and showAllBlocks is false
if (!showAllBlocks && !(block.start_date && block.target_date)) return;
{blocks?.map((block) => {
// hide the block if it doesn't have start and target dates and showAllBlocks is false
if (!showAllBlocks && !(block.start_date && block.target_date)) return;
const isBlockVisibleOnChart = block.start_date && block.target_date;
const isBlockVisibleOnChart = block.start_date && block.target_date;
return (
return (
<div
key={`block-${block.id}`}
className="relative min-w-full w-max"
style={{
height: `${BLOCK_HEIGHT}px`,
}}
>
<div
key={`block-${block.id}`}
className={cn(
"h-11",
{ "rounded bg-custom-background-80": activeBlock?.id === block.id },
{
"rounded-l border border-r-0 border-custom-primary-70 hover:border-custom-primary-70":
peekIssue?.issueId === block.data.id,
}
)}
className={cn("relative h-full", {
"bg-custom-background-80": activeBlock?.id === block.id,
"rounded-l border border-r-0 border-custom-primary-70 hover:border-custom-primary-70":
peekIssue?.issueId === block.data.id,
})}
onMouseEnter={() => updateActiveBlock(block)}
onMouseLeave={() => updateActiveBlock(null)}
>
{!isBlockVisibleOnChart && <ChartAddBlock block={block} blockUpdateHandler={blockUpdateHandler} />}
<ChartDraggable
block={block}
blockToRender={blockToRender}
handleBlock={(...args) => handleChartBlockPosition(block, ...args)}
enableBlockLeftResize={enableBlockLeftResize}
enableBlockRightResize={enableBlockRightResize}
enableBlockMove={enableBlockMove}
/>
{isBlockVisibleOnChart ? (
<ChartDraggable
block={block}
blockToRender={blockToRender}
handleBlock={(...args) => handleChartBlockPosition(block, ...args)}
enableBlockLeftResize={enableBlockLeftResize}
enableBlockRightResize={enableBlockRightResize}
enableBlockMove={enableBlockMove}
/>
) : (
enableAddBlock && <ChartAddBlock block={block} blockUpdateHandler={blockUpdateHandler} />
)}
</div>
);
})}
</div>
);
})}
</div>
);
};
});
+1 -1
View File
@@ -1 +1 @@
export * from "./blocks-display";
export * from "./blocks-list";
@@ -0,0 +1,59 @@
import { Expand, Shrink } from "lucide-react";
// hooks
import { useChart } from "../hooks";
// helpers
import { cn } from "helpers/common.helper";
// types
import { IGanttBlock, TGanttViews } from "../types";
type Props = {
blocks: IGanttBlock[] | null;
fullScreenMode: boolean;
handleChartView: (view: TGanttViews) => void;
handleToday: () => void;
loaderTitle: string;
title: string;
toggleFullScreenMode: () => void;
};
export const GanttChartHeader: React.FC<Props> = (props) => {
const { blocks, fullScreenMode, handleChartView, handleToday, loaderTitle, title, toggleFullScreenMode } = props;
// chart hook
const { currentView, allViews } = useChart();
return (
<div className="relative flex w-full flex-shrink-0 flex-wrap items-center gap-2 whitespace-nowrap px-2.5 py-2 z-10">
<div className="flex items-center gap-2 text-lg font-medium">{title}</div>
<div className="ml-auto">
<div className="ml-auto text-sm font-medium">{blocks ? `${blocks.length} ${loaderTitle}` : "Loading..."}</div>
</div>
<div className="flex flex-wrap items-center gap-2">
{allViews?.map((chartView: any) => (
<div
key={chartView?.key}
className={cn("cursor-pointer rounded-sm p-1 px-2 text-xs", {
"bg-custom-background-80": currentView === chartView?.key,
"hover:bg-custom-background-90": currentView !== chartView?.key,
})}
onClick={() => handleChartView(chartView?.key)}
>
{chartView?.title}
</div>
))}
</div>
<button type="button" className="rounded-sm p-1 px-2 text-xs hover:bg-custom-background-80" onClick={handleToday}>
Today
</button>
<button
type="button"
className="flex items-center justify-center rounded-sm border border-custom-border-200 p-1 transition-all hover:bg-custom-background-80"
onClick={toggleFullScreenMode}
>
{fullScreenMode ? <Shrink className="h-4 w-4" /> : <Expand className="h-4 w-4" />}
</button>
</div>
);
};
@@ -0,0 +1,4 @@
export * from "./views";
export * from "./header";
export * from "./main-content";
export * from "./root";
-324
View File
@@ -1,324 +0,0 @@
import { FC, useEffect, useState } from "react";
// icons
// components
import { GanttChartBlocks } from "components/gantt-chart";
// import { GanttSidebar } from "../sidebar";
// import { HourChartView } from "./hours";
// import { DayChartView } from "./day";
// import { WeekChartView } from "./week";
// import { BiWeekChartView } from "./bi-week";
import { MonthChartView } from "./month";
// import { QuarterChartView } from "./quarter";
// import { YearChartView } from "./year";
// icons
import { Expand, Shrink } from "lucide-react";
// views
import {
// generateHourChart,
// generateDayChart,
// generateWeekChart,
// generateBiWeekChart,
generateMonthChart,
// generateQuarterChart,
// generateYearChart,
getNumberOfDaysBetweenTwoDatesInMonth,
// getNumberOfDaysBetweenTwoDatesInQuarter,
// getNumberOfDaysBetweenTwoDatesInYear,
getMonthChartItemPositionWidthInMonth,
} from "../views";
// types
import { ChartDataType, IBlockUpdateData, IGanttBlock, TGanttViews } from "../types";
// data
import { currentViewDataWithView } from "../data";
// context
import { useChart } from "../hooks";
type ChartViewRootProps = {
border: boolean;
title: string;
loaderTitle: string;
blocks: IGanttBlock[] | null;
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
blockToRender: (data: any) => React.ReactNode;
sidebarToRender: (props: any) => React.ReactNode;
enableBlockLeftResize: boolean;
enableBlockRightResize: boolean;
enableBlockMove: boolean;
enableReorder: boolean;
bottomSpacing: boolean;
showAllBlocks: boolean;
};
export const ChartViewRoot: FC<ChartViewRootProps> = (props) => {
const {
border,
title,
blocks = null,
loaderTitle,
blockUpdateHandler,
sidebarToRender,
blockToRender,
enableBlockLeftResize,
enableBlockRightResize,
enableBlockMove,
enableReorder,
bottomSpacing,
showAllBlocks,
} = props;
// states
const [itemsContainerWidth, setItemsContainerWidth] = useState<number>(0);
const [fullScreenMode, setFullScreenMode] = useState<boolean>(false);
const [chartBlocks, setChartBlocks] = useState<IGanttBlock[] | null>(null); // blocks state management starts
// hooks
const { currentView, currentViewData, renderView, dispatch, allViews, updateScrollLeft } = useChart();
const renderBlockStructure = (view: any, blocks: IGanttBlock[] | null) =>
blocks && blocks.length > 0
? blocks.map((block: any) => ({
...block,
position: getMonthChartItemPositionWidthInMonth(view, block),
}))
: [];
useEffect(() => {
if (currentViewData && blocks) setChartBlocks(() => renderBlockStructure(currentViewData, blocks));
}, [currentViewData, blocks]);
// blocks state management ends
const handleChartView = (key: TGanttViews) => updateCurrentViewRenderPayload(null, key);
const updateCurrentViewRenderPayload = (side: null | "left" | "right", view: TGanttViews) => {
const selectedCurrentView: TGanttViews = view;
const selectedCurrentViewData: ChartDataType | undefined =
selectedCurrentView && selectedCurrentView === currentViewData?.key
? currentViewData
: currentViewDataWithView(view);
if (selectedCurrentViewData === undefined) return;
let currentRender: any;
// if (view === "hours") currentRender = generateHourChart(selectedCurrentViewData, side);
// if (view === "day") currentRender = generateDayChart(selectedCurrentViewData, side);
// if (view === "week") currentRender = generateWeekChart(selectedCurrentViewData, side);
// if (view === "bi_week") currentRender = generateBiWeekChart(selectedCurrentViewData, side);
if (selectedCurrentView === "month") currentRender = generateMonthChart(selectedCurrentViewData, side);
// if (view === "quarter") currentRender = generateQuarterChart(selectedCurrentViewData, side);
// if (selectedCurrentView === "year")
// currentRender = generateYearChart(selectedCurrentViewData, side);
// updating the prevData, currentData and nextData
if (currentRender.payload.length > 0) {
if (side === "left") {
dispatch({
type: "PARTIAL_UPDATE",
payload: {
currentView: selectedCurrentView,
currentViewData: currentRender.state,
renderView: [...currentRender.payload, ...renderView],
},
});
updatingCurrentLeftScrollPosition(currentRender.scrollWidth);
setItemsContainerWidth(itemsContainerWidth + currentRender.scrollWidth);
} else if (side === "right") {
dispatch({
type: "PARTIAL_UPDATE",
payload: {
currentView: view,
currentViewData: currentRender.state,
renderView: [...renderView, ...currentRender.payload],
},
});
setItemsContainerWidth(itemsContainerWidth + currentRender.scrollWidth);
} else {
dispatch({
type: "PARTIAL_UPDATE",
payload: {
currentView: view,
currentViewData: currentRender.state,
renderView: [...currentRender.payload],
},
});
setItemsContainerWidth(currentRender.scrollWidth);
setTimeout(() => {
handleScrollToCurrentSelectedDate(currentRender.state, currentRender.state.data.currentDate);
}, 50);
}
}
};
const handleToday = () => updateCurrentViewRenderPayload(null, currentView);
// handling the scroll positioning from left and right
useEffect(() => {
handleToday();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const updatingCurrentLeftScrollPosition = (width: number) => {
const scrollContainer = document.getElementById("scroll-container") as HTMLElement;
if (!scrollContainer) return;
scrollContainer.scrollLeft = width + scrollContainer?.scrollLeft;
setItemsContainerWidth(width + scrollContainer?.scrollLeft);
};
const handleScrollToCurrentSelectedDate = (currentState: ChartDataType, date: Date) => {
const scrollContainer = document.getElementById("scroll-container") as HTMLElement;
if (!scrollContainer) return;
const clientVisibleWidth: number = scrollContainer?.clientWidth;
let scrollWidth: number = 0;
let daysDifference: number = 0;
// if (currentView === "hours")
// daysDifference = getNumberOfDaysBetweenTwoDatesInMonth(currentState.data.startDate, date);
// if (currentView === "day")
// daysDifference = getNumberOfDaysBetweenTwoDatesInMonth(currentState.data.startDate, date);
// if (currentView === "week")
// daysDifference = getNumberOfDaysBetweenTwoDatesInMonth(currentState.data.startDate, date);
// if (currentView === "bi_week")
// daysDifference = getNumberOfDaysBetweenTwoDatesInMonth(currentState.data.startDate, date);
if (currentView === "month")
daysDifference = getNumberOfDaysBetweenTwoDatesInMonth(currentState.data.startDate, date);
// if (currentView === "quarter")
// daysDifference = getNumberOfDaysBetweenTwoDatesInQuarter(currentState.data.startDate, date);
// if (currentView === "year")
// daysDifference = getNumberOfDaysBetweenTwoDatesInYear(currentState.data.startDate, date);
scrollWidth = daysDifference * currentState.data.width - (clientVisibleWidth / 2 - currentState.data.width);
scrollContainer.scrollLeft = scrollWidth;
};
// handling scroll functionality
const onScroll = () => {
const scrollContainer = document.getElementById("scroll-container") as HTMLElement;
if (!scrollContainer) return;
const scrollWidth: number = scrollContainer?.scrollWidth;
const clientVisibleWidth: number = scrollContainer?.clientWidth;
const currentScrollPosition: number = scrollContainer?.scrollLeft;
updateScrollLeft(currentScrollPosition);
const approxRangeLeft: number = scrollWidth >= clientVisibleWidth + 1000 ? 1000 : scrollWidth - clientVisibleWidth;
const approxRangeRight: number = scrollWidth - (approxRangeLeft + clientVisibleWidth);
if (currentScrollPosition >= approxRangeRight) updateCurrentViewRenderPayload("right", currentView);
if (currentScrollPosition <= approxRangeLeft) updateCurrentViewRenderPayload("left", currentView);
};
return (
<div
className={`${
fullScreenMode ? `fixed bottom-0 left-0 right-0 top-0 z-[999999] bg-custom-background-100` : `relative`
} ${
border ? `border border-custom-border-200` : ``
} flex h-full select-none flex-col rounded-sm bg-custom-background-100 shadow`}
>
{/* chart header */}
<div className="flex w-full flex-shrink-0 flex-wrap items-center gap-2 whitespace-nowrap px-2.5 py-2">
{title && (
<div className="flex items-center gap-2 text-lg font-medium">
<div>{title}</div>
{/* <div className="text-xs rounded-full px-2 py-1 font-bold border border-custom-primary/75 bg-custom-primary/5 text-custom-text-100">
Gantt View Beta
</div> */}
</div>
)}
<div className="ml-auto">
{blocks === null ? (
<div className="ml-auto text-sm font-medium">Loading...</div>
) : (
<div className="ml-auto text-sm font-medium">
{blocks.length} {loaderTitle}
</div>
)}
</div>
<div className="flex flex-wrap items-center gap-2">
{allViews &&
allViews.length > 0 &&
// eslint-disable-next-line @typescript-eslint/no-unused-vars
allViews.map((_chatView: any, _idx: any) => (
<div
key={_chatView?.key}
className={`cursor-pointer rounded-sm p-1 px-2 text-xs ${
currentView === _chatView?.key ? `bg-custom-background-80` : `hover:bg-custom-background-90`
}`}
onClick={() => handleChartView(_chatView?.key)}
>
{_chatView?.title}
</div>
))}
</div>
<div className="flex items-center gap-1">
<div
className="cursor-pointer rounded-sm p-1 px-2 text-xs hover:bg-custom-background-80"
onClick={handleToday}
>
Today
</div>
</div>
<div
className="flex cursor-pointer items-center justify-center rounded-sm border border-custom-border-200 p-1 transition-all hover:bg-custom-background-80"
onClick={() => setFullScreenMode((prevData) => !prevData)}
>
{fullScreenMode ? <Shrink className="h-4 w-4" /> : <Expand className="h-4 w-4" />}
</div>
</div>
{/* content */}
<div
id="gantt-container"
className={`relative flex h-full w-full flex-1 overflow-hidden border-t border-custom-border-200 ${
bottomSpacing ? "mb-8" : ""
}`}
>
<div id="gantt-sidebar" className="flex h-full w-1/4 flex-col border-r border-custom-border-200">
<div className="box-border flex h-[60px] flex-shrink-0 items-end justify-between gap-2 border-b border-custom-border-200 pb-2 pl-10 pr-4 text-sm font-medium text-custom-text-300">
<h6>{title}</h6>
<h6>Duration</h6>
</div>
{sidebarToRender && sidebarToRender({ title, blockUpdateHandler, blocks, enableReorder })}
</div>
<div
className="horizontal-scroll-enable relative flex h-full w-full flex-1 flex-col overflow-hidden overflow-x-auto"
id="scroll-container"
onScroll={onScroll}
>
{/* {currentView && currentView === "hours" && <HourChartView />} */}
{/* {currentView && currentView === "day" && <DayChartView />} */}
{/* {currentView && currentView === "week" && <WeekChartView />} */}
{/* {currentView && currentView === "bi_week" && <BiWeekChartView />} */}
{currentView && currentView === "month" && <MonthChartView />}
{/* {currentView && currentView === "quarter" && <QuarterChartView />} */}
{/* {currentView && currentView === "year" && <YearChartView />} */}
{/* blocks */}
{currentView && currentViewData && (
<GanttChartBlocks
itemsContainerWidth={itemsContainerWidth}
blocks={chartBlocks}
blockToRender={blockToRender}
blockUpdateHandler={blockUpdateHandler}
enableBlockLeftResize={enableBlockLeftResize}
enableBlockRightResize={enableBlockRightResize}
enableBlockMove={enableBlockMove}
showAllBlocks={showAllBlocks}
/>
)}
</div>
</div>
</div>
);
};
@@ -0,0 +1,126 @@
// components
import {
BiWeekChartView,
DayChartView,
GanttChartBlocksList,
GanttChartSidebar,
HourChartView,
IBlockUpdateData,
IGanttBlock,
MonthChartView,
QuarterChartView,
TGanttViews,
WeekChartView,
YearChartView,
useChart,
} from "components/gantt-chart";
// helpers
import { cn } from "helpers/common.helper";
type Props = {
blocks: IGanttBlock[] | null;
blockToRender: (data: any) => React.ReactNode;
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
bottomSpacing: boolean;
chartBlocks: IGanttBlock[] | null;
enableBlockLeftResize: boolean;
enableBlockMove: boolean;
enableBlockRightResize: boolean;
enableReorder: boolean;
enableAddBlock: boolean;
itemsContainerWidth: number;
showAllBlocks: boolean;
sidebarToRender: (props: any) => React.ReactNode;
title: string;
updateCurrentViewRenderPayload: (direction: "left" | "right", currentView: TGanttViews) => void;
quickAdd?: React.JSX.Element | undefined;
};
export const GanttChartMainContent: React.FC<Props> = (props) => {
const {
blocks,
blockToRender,
blockUpdateHandler,
bottomSpacing,
chartBlocks,
enableBlockLeftResize,
enableBlockMove,
enableBlockRightResize,
enableReorder,
enableAddBlock,
itemsContainerWidth,
showAllBlocks,
sidebarToRender,
title,
updateCurrentViewRenderPayload,
quickAdd,
} = props;
// chart hook
const { currentView, currentViewData, updateScrollLeft } = useChart();
// handling scroll functionality
const onScroll = (e: React.UIEvent<HTMLDivElement, UIEvent>) => {
const { clientWidth, scrollLeft, scrollWidth } = e.currentTarget;
updateScrollLeft(scrollLeft);
const approxRangeLeft = scrollLeft >= clientWidth + 1000 ? 1000 : scrollLeft - clientWidth;
const approxRangeRight = scrollWidth - (scrollLeft + clientWidth);
if (approxRangeRight < 1000) updateCurrentViewRenderPayload("right", currentView);
if (approxRangeLeft < 1000) updateCurrentViewRenderPayload("left", currentView);
};
const CHART_VIEW_COMPONENTS: {
[key in TGanttViews]: React.FC;
} = {
hours: HourChartView,
day: DayChartView,
week: WeekChartView,
bi_week: BiWeekChartView,
month: MonthChartView,
quarter: QuarterChartView,
year: YearChartView,
};
if (!currentView) return null;
const ActiveChartView = CHART_VIEW_COMPONENTS[currentView];
return (
<div
// DO NOT REMOVE THE ID
id="gantt-container"
className={cn(
"h-full w-full overflow-auto horizontal-scroll-enable flex border-t-[0.5px] border-custom-border-200",
{
"mb-8": bottomSpacing,
}
)}
onScroll={onScroll}
>
<GanttChartSidebar
blocks={blocks}
blockUpdateHandler={blockUpdateHandler}
enableReorder={enableReorder}
sidebarToRender={sidebarToRender}
title={title}
quickAdd={quickAdd}
/>
<div className="relative min-h-full h-max flex-shrink-0 flex-grow">
<ActiveChartView />
{currentViewData && (
<GanttChartBlocksList
itemsContainerWidth={itemsContainerWidth}
blocks={chartBlocks}
blockToRender={blockToRender}
blockUpdateHandler={blockUpdateHandler}
enableBlockLeftResize={enableBlockLeftResize}
enableBlockRightResize={enableBlockRightResize}
enableBlockMove={enableBlockMove}
enableAddBlock={enableAddBlock}
showAllBlocks={showAllBlocks}
/>
)}
</div>
</div>
);
};
@@ -1,72 +0,0 @@
import { FC } from "react";
// hooks
import { useChart } from "../hooks";
// types
import { IMonthBlock } from "../views";
export const MonthChartView: FC<any> = () => {
const { currentViewData, renderView } = useChart();
const monthBlocks: IMonthBlock[] = renderView;
return (
<>
<div className="absolute flex h-full flex-grow divide-x divide-custom-border-100/50">
{monthBlocks &&
monthBlocks.length > 0 &&
monthBlocks.map((block, _idxRoot) => (
<div key={`month-${block?.month}-${block?.year}`} className="relative flex flex-col">
<div className="h-[60px] w-full">
<div className="relative h-[30px]">
<div className="sticky left-0 inline-flex whitespace-nowrap px-3 py-2 text-xs font-medium capitalize">
{block?.title}
</div>
</div>
<div className="flex h-[30px] w-full">
{block?.children &&
block?.children.length > 0 &&
block?.children.map((monthDay, _idx) => (
<div
key={`sub-title-${_idxRoot}-${_idx}`}
className="flex-shrink-0 border-b border-custom-border-200 py-1 text-center capitalize"
style={{ width: `${currentViewData?.data.width}px` }}
>
<div className="space-x-1 text-xs">
<span className="text-custom-text-200">{monthDay.dayData.shortTitle[0]}</span>{" "}
<span className={monthDay.today ? "rounded-full bg-custom-primary-100 px-1 text-white" : ""}>
{monthDay.day}
</span>
</div>
</div>
))}
</div>
</div>
<div className="flex h-full w-full divide-x divide-custom-border-100/50">
{block?.children &&
block?.children.length > 0 &&
block?.children.map((monthDay, _idx) => (
<div
key={`column-${_idxRoot}-${_idx}`}
className="relative flex h-full flex-col overflow-hidden whitespace-nowrap"
style={{ width: `${currentViewData?.data.width}px` }}
>
<div
className={`relative flex h-full w-full flex-1 justify-center ${
["sat", "sun"].includes(monthDay?.dayData?.shortTitle || "") ? `bg-custom-background-90` : ``
}`}
>
{/* {monthDay?.today && (
<div className="absolute top-0 bottom-0 w-[1px] bg-red-500" />
)} */}
</div>
</div>
))}
</div>
</div>
))}
</div>
</>
);
};
+209
View File
@@ -0,0 +1,209 @@
import { FC, useEffect, useState } from "react";
// components
import { GanttChartHeader, useChart, GanttChartMainContent } from "components/gantt-chart";
// views
import {
generateMonthChart,
getNumberOfDaysBetweenTwoDatesInMonth,
getMonthChartItemPositionWidthInMonth,
} from "../views";
// helpers
import { cn } from "helpers/common.helper";
// types
import { ChartDataType, IBlockUpdateData, IGanttBlock, TGanttViews } from "../types";
// data
import { currentViewDataWithView } from "../data";
// constants
import { SIDEBAR_WIDTH } from "../constants";
type ChartViewRootProps = {
border: boolean;
title: string;
loaderTitle: string;
blocks: IGanttBlock[] | null;
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
blockToRender: (data: any) => React.ReactNode;
sidebarToRender: (props: any) => React.ReactNode;
enableBlockLeftResize: boolean;
enableBlockRightResize: boolean;
enableBlockMove: boolean;
enableReorder: boolean;
enableAddBlock: boolean;
bottomSpacing: boolean;
showAllBlocks: boolean;
quickAdd?: React.JSX.Element | undefined;
};
export const ChartViewRoot: FC<ChartViewRootProps> = (props) => {
const {
border,
title,
blocks = null,
loaderTitle,
blockUpdateHandler,
sidebarToRender,
blockToRender,
enableBlockLeftResize,
enableBlockRightResize,
enableBlockMove,
enableReorder,
enableAddBlock,
bottomSpacing,
showAllBlocks,
quickAdd,
} = props;
// states
const [itemsContainerWidth, setItemsContainerWidth] = useState(0);
const [fullScreenMode, setFullScreenMode] = useState(false);
const [chartBlocks, setChartBlocks] = useState<IGanttBlock[] | null>(null);
// hooks
const { currentView, currentViewData, renderView, dispatch } = useChart();
// rendering the block structure
const renderBlockStructure = (view: any, blocks: IGanttBlock[] | null) =>
blocks
? blocks.map((block: any) => ({
...block,
position: getMonthChartItemPositionWidthInMonth(view, block),
}))
: [];
useEffect(() => {
if (!currentViewData || !blocks) return;
setChartBlocks(() => renderBlockStructure(currentViewData, blocks));
}, [currentViewData, blocks]);
const updateCurrentViewRenderPayload = (side: null | "left" | "right", view: TGanttViews) => {
const selectedCurrentView: TGanttViews = view;
const selectedCurrentViewData: ChartDataType | undefined =
selectedCurrentView && selectedCurrentView === currentViewData?.key
? currentViewData
: currentViewDataWithView(view);
if (selectedCurrentViewData === undefined) return;
let currentRender: any;
if (selectedCurrentView === "month") currentRender = generateMonthChart(selectedCurrentViewData, side);
// updating the prevData, currentData and nextData
if (currentRender.payload.length > 0) {
if (side === "left") {
dispatch({
type: "PARTIAL_UPDATE",
payload: {
currentView: selectedCurrentView,
currentViewData: currentRender.state,
renderView: [...currentRender.payload, ...renderView],
},
});
updatingCurrentLeftScrollPosition(currentRender.scrollWidth);
setItemsContainerWidth(itemsContainerWidth + currentRender.scrollWidth);
} else if (side === "right") {
dispatch({
type: "PARTIAL_UPDATE",
payload: {
currentView: view,
currentViewData: currentRender.state,
renderView: [...renderView, ...currentRender.payload],
},
});
setItemsContainerWidth(itemsContainerWidth + currentRender.scrollWidth);
} else {
dispatch({
type: "PARTIAL_UPDATE",
payload: {
currentView: view,
currentViewData: currentRender.state,
renderView: [...currentRender.payload],
},
});
setItemsContainerWidth(currentRender.scrollWidth);
setTimeout(() => {
handleScrollToCurrentSelectedDate(currentRender.state, currentRender.state.data.currentDate);
}, 50);
}
}
};
const handleToday = () => updateCurrentViewRenderPayload(null, currentView);
// handling the scroll positioning from left and right
useEffect(() => {
handleToday();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const updatingCurrentLeftScrollPosition = (width: number) => {
const scrollContainer = document.querySelector("#gantt-container") as HTMLDivElement;
if (!scrollContainer) return;
scrollContainer.scrollLeft = width + scrollContainer?.scrollLeft;
setItemsContainerWidth(width + scrollContainer?.scrollLeft);
};
const handleScrollToCurrentSelectedDate = (currentState: ChartDataType, date: Date) => {
const scrollContainer = document.querySelector("#gantt-container") as HTMLDivElement;
if (!scrollContainer) return;
const clientVisibleWidth: number = scrollContainer?.clientWidth;
let scrollWidth: number = 0;
let daysDifference: number = 0;
// if (currentView === "hours")
// daysDifference = getNumberOfDaysBetweenTwoDatesInMonth(currentState.data.startDate, date);
// if (currentView === "day")
// daysDifference = getNumberOfDaysBetweenTwoDatesInMonth(currentState.data.startDate, date);
// if (currentView === "week")
// daysDifference = getNumberOfDaysBetweenTwoDatesInMonth(currentState.data.startDate, date);
// if (currentView === "bi_week")
// daysDifference = getNumberOfDaysBetweenTwoDatesInMonth(currentState.data.startDate, date);
if (currentView === "month")
daysDifference = getNumberOfDaysBetweenTwoDatesInMonth(currentState.data.startDate, date);
// if (currentView === "quarter")
// daysDifference = getNumberOfDaysBetweenTwoDatesInQuarter(currentState.data.startDate, date);
// if (currentView === "year")
// daysDifference = getNumberOfDaysBetweenTwoDatesInYear(currentState.data.startDate, date);
scrollWidth =
daysDifference * currentState.data.width - (clientVisibleWidth / 2 - currentState.data.width) + SIDEBAR_WIDTH / 2;
scrollContainer.scrollLeft = scrollWidth;
};
return (
<div
className={cn("relative flex flex-col h-full select-none rounded-sm bg-custom-background-100 shadow", {
"fixed inset-0 z-[999999] bg-custom-background-100": fullScreenMode,
"border-[0.5px] border-custom-border-200": border,
})}
>
<GanttChartHeader
blocks={blocks}
fullScreenMode={fullScreenMode}
toggleFullScreenMode={() => setFullScreenMode((prevData) => !prevData)}
handleChartView={(key) => updateCurrentViewRenderPayload(null, key)}
handleToday={handleToday}
loaderTitle={loaderTitle}
title={title}
/>
<GanttChartMainContent
blocks={blocks}
blockToRender={blockToRender}
blockUpdateHandler={blockUpdateHandler}
bottomSpacing={bottomSpacing}
chartBlocks={chartBlocks}
enableBlockLeftResize={enableBlockLeftResize}
enableBlockMove={enableBlockMove}
enableBlockRightResize={enableBlockRightResize}
enableReorder={enableReorder}
enableAddBlock={enableAddBlock}
itemsContainerWidth={itemsContainerWidth}
showAllBlocks={showAllBlocks}
sidebarToRender={sidebarToRender}
title={title}
updateCurrentViewRenderPayload={updateCurrentViewRenderPayload}
quickAdd={quickAdd}
/>
</div>
);
};
@@ -1,6 +1,6 @@
import { FC } from "react";
// context
import { useChart } from "../hooks";
import { useChart } from "components/gantt-chart";
export const BiWeekChartView: FC<any> = () => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -1,6 +1,6 @@
import { FC } from "react";
// context
import { useChart } from "../hooks";
import { useChart } from "../../hooks";
export const DayChartView: FC<any> = () => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -1,6 +1,6 @@
import { FC } from "react";
// context
import { useChart } from "../hooks";
import { useChart } from "components/gantt-chart";
export const HourChartView: FC<any> = () => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -0,0 +1,7 @@
export * from "./bi-week";
export * from "./day";
export * from "./hours";
export * from "./month";
export * from "./quarter";
export * from "./week";
export * from "./year";
@@ -0,0 +1,74 @@
import { FC } from "react";
// hooks
import { useChart } from "components/gantt-chart";
// helpers
import { cn } from "helpers/common.helper";
// types
import { IMonthBlock } from "../../views";
// constants
import { HEADER_HEIGHT, SIDEBAR_WIDTH } from "components/gantt-chart/constants";
export const MonthChartView: FC<any> = () => {
// chart hook
const { currentViewData, renderView } = useChart();
const monthBlocks: IMonthBlock[] = renderView;
return (
<div className="absolute top-0 left-0 h-full w-max flex divide-x divide-custom-border-100/50">
{monthBlocks?.map((block, rootIndex) => (
<div key={`month-${block?.month}-${block?.year}`} className="relative">
<div
className="w-full sticky top-0 z-[5] bg-custom-background-100"
style={{
height: `${HEADER_HEIGHT}px`,
}}
>
<div className="h-1/2">
<div
className="sticky inline-flex whitespace-nowrap px-3 py-2 text-xs font-medium capitalize"
style={{
left: `${SIDEBAR_WIDTH}px`,
}}
>
{block?.title}
</div>
</div>
<div className="h-1/2 w-full flex">
{block?.children?.map((monthDay, index) => (
<div
key={`sub-title-${rootIndex}-${index}`}
className="flex-shrink-0 border-b-[0.5px] border-custom-border-200 py-1 text-center capitalize"
style={{ width: `${currentViewData?.data.width}px` }}
>
<div className="space-x-1 text-xs">
<span className="text-custom-text-200">{monthDay.dayData.shortTitle[0]}</span>{" "}
<span
className={cn({
"rounded-full bg-custom-primary-100 px-1 text-white": monthDay.today,
})}
>
{monthDay.day}
</span>
</div>
</div>
))}
</div>
</div>
<div className="h-full w-full flex divide-x divide-custom-border-100/50">
{block?.children?.map((monthDay, index) => (
<div
key={`column-${rootIndex}-${index}`}
className="h-full overflow-hidden"
style={{ width: `${currentViewData?.data.width}px` }}
>
{["sat", "sun"].includes(monthDay?.dayData?.shortTitle) && (
<div className="h-full bg-custom-background-90" />
)}
</div>
))}
</div>
</div>
))}
</div>
);
};
@@ -1,6 +1,6 @@
import { FC } from "react";
// context
import { useChart } from "../hooks";
import { useChart } from "../../hooks";
export const QuarterChartView: FC<any> = () => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -1,6 +1,6 @@
import { FC } from "react";
// context
import { useChart } from "../hooks";
import { useChart } from "../../hooks";
export const WeekChartView: FC<any> = () => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -1,6 +1,6 @@
import { FC } from "react";
// context
import { useChart } from "../hooks";
import { useChart } from "../../hooks";
export const YearChartView: FC<any> = () => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
+5
View File
@@ -0,0 +1,5 @@
export const BLOCK_HEIGHT = 44;
export const HEADER_HEIGHT = 60;
export const SIDEBAR_WIDTH = 360;
+10 -7
View File
@@ -24,6 +24,7 @@ const chartReducer = (state: ChartContextData, action: ChartContextActionPayload
const initialView = "month";
export const ChartContextProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
// states;
const [state, dispatch] = useState<ChartContextData>({
currentView: initialView,
currentViewData: currentViewDataWithView(initialView),
@@ -31,23 +32,25 @@ export const ChartContextProvider: React.FC<{ children: React.ReactNode }> = ({
allViews: allViewsWithData,
activeBlock: null,
});
const [scrollLeft, setScrollLeft] = useState(0);
const handleDispatch = (action: ChartContextActionPayload): ChartContextData => {
const newState = chartReducer(state, action);
dispatch(() => newState);
return newState;
};
const updateScrollLeft = (scrollLeft: number) => {
setScrollLeft(scrollLeft);
};
const updateScrollLeft = (scrollLeft: number) => setScrollLeft(scrollLeft);
return (
<ChartContext.Provider value={{ ...state, scrollLeft, updateScrollLeft, dispatch: handleDispatch }}>
<ChartContext.Provider
value={{
...state,
scrollLeft,
updateScrollLeft,
dispatch: handleDispatch,
}}
>
{children}
</ChartContext.Provider>
);
@@ -1,9 +1,11 @@
import React, { useEffect, useRef, useState } from "react";
import { ArrowLeft, ArrowRight } from "lucide-react";
import { ArrowRight } from "lucide-react";
// hooks
import { useChart } from "../hooks";
// types
import { IGanttBlock } from "../types";
import { IGanttBlock, useChart } from "components/gantt-chart";
// helpers
import { cn } from "helpers/common.helper";
// constants
import { SIDEBAR_WIDTH } from "../constants";
type Props = {
block: IGanttBlock;
@@ -20,7 +22,7 @@ export const ChartDraggable: React.FC<Props> = (props) => {
const [isLeftResizing, setIsLeftResizing] = useState(false);
const [isRightResizing, setIsRightResizing] = useState(false);
const [isMoving, setIsMoving] = useState(false);
const [posFromLeft, setPosFromLeft] = useState<number | null>(null);
const [isHidden, setIsHidden] = useState(true);
// refs
const resizableRef = useRef<HTMLDivElement>(null);
// chart hook
@@ -31,12 +33,10 @@ export const ChartDraggable: React.FC<Props> = (props) => {
let delWidth = 0;
const ganttContainer = document.querySelector("#gantt-container") as HTMLElement;
const ganttSidebar = document.querySelector("#gantt-sidebar") as HTMLElement;
const ganttContainer = document.querySelector("#gantt-container") as HTMLDivElement;
const ganttSidebar = document.querySelector("#gantt-sidebar") as HTMLDivElement;
const scrollContainer = document.querySelector("#scroll-container") as HTMLElement;
if (!ganttContainer || !ganttSidebar || !scrollContainer) return 0;
if (!ganttContainer || !ganttSidebar) return 0;
const posFromLeft = e.clientX;
// manually scroll to left if reached the left end while dragging
@@ -45,7 +45,7 @@ export const ChartDraggable: React.FC<Props> = (props) => {
delWidth = -5;
scrollContainer.scrollBy(delWidth, 0);
ganttContainer.scrollBy(delWidth, 0);
} else delWidth = e.movementX;
// manually scroll to right if reached the right end while dragging
@@ -55,7 +55,7 @@ export const ChartDraggable: React.FC<Props> = (props) => {
delWidth = 5;
scrollContainer.scrollBy(delWidth, 0);
ganttContainer.scrollBy(delWidth, 0);
} else delWidth = e.movementX;
return delWidth;
@@ -201,50 +201,61 @@ export const ChartDraggable: React.FC<Props> = (props) => {
};
// scroll to a hidden block
const handleScrollToBlock = () => {
const scrollContainer = document.querySelector("#scroll-container") as HTMLElement;
const scrollContainer = document.querySelector("#gantt-container") as HTMLDivElement;
if (!scrollContainer || !block.position) return;
// update container's scroll position to the block's position
scrollContainer.scrollLeft = block.position.marginLeft - 4;
};
// update block position from viewport's left end on scroll
useEffect(() => {
const block = resizableRef.current;
if (!block) return;
setPosFromLeft(block.getBoundingClientRect().left);
}, [scrollLeft]);
// check if block is hidden on either side
const isBlockHiddenOnLeft =
block.position?.marginLeft &&
block.position?.width &&
scrollLeft > block.position.marginLeft + block.position.width;
const isBlockHiddenOnRight = posFromLeft && window && posFromLeft > window.innerWidth;
useEffect(() => {
const intersectionRoot = document.querySelector("#gantt-container") as HTMLDivElement;
const resizableBlock = resizableRef.current;
if (!resizableBlock || !intersectionRoot) return;
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
setIsHidden(!entry.isIntersecting);
});
},
{
root: intersectionRoot,
rootMargin: `0px 0px 0px -${SIDEBAR_WIDTH}px`,
}
);
observer.observe(resizableBlock);
return () => {
observer.unobserve(resizableBlock);
};
}, [block.data.name]);
return (
<>
{/* move to left side hidden block button */}
{isBlockHiddenOnLeft && (
<div
className="fixed z-[1] ml-1 mt-1.5 grid h-8 w-8 cursor-pointer place-items-center rounded border border-custom-border-300 bg-custom-background-80 text-custom-text-200 hover:text-custom-text-100"
{/* move to the hidden block */}
{isHidden && (
<button
type="button"
className="sticky z-[1] grid h-8 w-8 translate-y-1.5 cursor-pointer place-items-center rounded border border-custom-border-300 bg-custom-background-80 text-custom-text-200 hover:text-custom-text-100"
style={{
left: `${SIDEBAR_WIDTH + 4}px`,
}}
onClick={handleScrollToBlock}
>
<ArrowLeft className="h-3.5 w-3.5" />
</div>
)}
{/* move to right side hidden block button */}
{isBlockHiddenOnRight && (
<div
className="fixed right-1 z-[1] mt-1.5 grid h-8 w-8 cursor-pointer place-items-center rounded border border-custom-border-300 bg-custom-background-80 text-custom-text-200 hover:text-custom-text-100"
onClick={handleScrollToBlock}
>
<ArrowRight className="h-3.5 w-3.5" />
</div>
<ArrowRight
className={cn("h-3.5 w-3.5", {
"rotate-180": isBlockHiddenOnLeft,
})}
/>
</button>
)}
<div
id={`block-${block.id}`}
ref={resizableRef}
className="group relative inline-flex h-full cursor-pointer items-center font-medium transition-all"
style={{
@@ -259,17 +270,22 @@ export const ChartDraggable: React.FC<Props> = (props) => {
onMouseDown={handleBlockLeftResize}
onMouseEnter={() => setIsLeftResizing(true)}
onMouseLeave={() => setIsLeftResizing(false)}
className="absolute -left-2.5 top-1/2 z-[3] h-full w-6 -translate-y-1/2 cursor-col-resize rounded-md"
className="absolute -left-2.5 top-1/2 -translate-y-1/2 z-[3] h-full w-6 cursor-col-resize rounded-md"
/>
<div
className={`absolute top-1/2 h-7 w-1 -translate-y-1/2 rounded-sm bg-custom-background-100 transition-all duration-300 ${
isLeftResizing ? "-left-2.5" : "left-1"
}`}
className={cn(
"absolute left-1 top-1/2 -translate-y-1/2 h-7 w-1 rounded-sm bg-custom-background-100 transition-all duration-300",
{
"-left-2.5": isLeftResizing,
}
)}
/>
</>
)}
<div
className={`relative z-[2] flex h-8 w-full items-center rounded ${isMoving ? "pointer-events-none" : ""}`}
className={cn("relative z-[2] flex h-8 w-full items-center rounded", {
"pointer-events-none": isMoving,
})}
onMouseDown={handleBlockMove}
>
{blockToRender(block.data)}
@@ -281,12 +297,15 @@ export const ChartDraggable: React.FC<Props> = (props) => {
onMouseDown={handleBlockRightResize}
onMouseEnter={() => setIsRightResizing(true)}
onMouseLeave={() => setIsRightResizing(false)}
className="absolute -right-2.5 top-1/2 z-[2] h-full w-6 -translate-y-1/2 cursor-col-resize rounded-md"
className="absolute -right-2.5 top-1/2 -translate-y-1/2 z-[2] h-full w-6 cursor-col-resize rounded-md"
/>
<div
className={`absolute top-1/2 h-7 w-1 -translate-y-1/2 rounded-sm bg-custom-background-100 transition-all duration-300 ${
isRightResizing ? "-right-2.5" : "right-1"
}`}
className={cn(
"absolute right-1 top-1/2 -translate-y-1/2 h-7 w-1 rounded-sm bg-custom-background-100 transition-all duration-300",
{
"-right-2.5": isRightResizing,
}
)}
/>
</>
)}
+1
View File
@@ -1,4 +1,5 @@
export * from "./blocks";
export * from "./chart";
export * from "./helpers";
export * from "./hooks";
export * from "./root";
+11 -7
View File
@@ -1,10 +1,8 @@
import { FC } from "react";
// components
import { ChartViewRoot } from "./chart";
import { ChartViewRoot, IBlockUpdateData, IGanttBlock } from "components/gantt-chart";
// context
import { ChartContextProvider } from "./contexts";
// types
import { IBlockUpdateData, IGanttBlock } from "./types";
type GanttChartRootProps = {
border?: boolean;
@@ -14,10 +12,12 @@ type GanttChartRootProps = {
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
blockToRender: (data: any) => React.ReactNode;
sidebarToRender: (props: any) => React.ReactNode;
quickAdd?: React.JSX.Element | undefined;
enableBlockLeftResize?: boolean;
enableBlockRightResize?: boolean;
enableBlockMove?: boolean;
enableReorder?: boolean;
enableAddBlock?: boolean;
bottomSpacing?: boolean;
showAllBlocks?: boolean;
};
@@ -31,12 +31,14 @@ export const GanttChartRoot: FC<GanttChartRootProps> = (props) => {
blockUpdateHandler,
sidebarToRender,
blockToRender,
enableBlockLeftResize = true,
enableBlockRightResize = true,
enableBlockMove = true,
enableReorder = true,
enableBlockLeftResize = false,
enableBlockRightResize = false,
enableBlockMove = false,
enableReorder = false,
enableAddBlock = false,
bottomSpacing = false,
showAllBlocks = false,
quickAdd,
} = props;
return (
@@ -53,8 +55,10 @@ export const GanttChartRoot: FC<GanttChartRootProps> = (props) => {
enableBlockRightResize={enableBlockRightResize}
enableBlockMove={enableBlockMove}
enableReorder={enableReorder}
enableAddBlock={enableAddBlock}
bottomSpacing={bottomSpacing}
showAllBlocks={showAllBlocks}
quickAdd={quickAdd}
/>
</ChartContextProvider>
);
@@ -1,4 +1,3 @@
import { useRouter } from "next/router";
import { DragDropContext, Draggable, DropResult, Droppable } from "@hello-pangea/dnd";
import { MoreVertical } from "lucide-react";
// hooks
@@ -9,8 +8,11 @@ import { Loader } from "@plane/ui";
import { CycleGanttSidebarBlock } from "components/cycles";
// helpers
import { findTotalDaysInRange } from "helpers/date-time.helper";
import { cn } from "helpers/common.helper";
// types
import { IBlockUpdateData, IGanttBlock } from "components/gantt-chart/types";
// constants
import { BLOCK_HEIGHT } from "../constants";
type Props = {
title: string;
@@ -20,12 +22,8 @@ type Props = {
};
export const CycleGanttSidebar: React.FC<Props> = (props) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { title, blockUpdateHandler, blocks, enableReorder } = props;
const router = useRouter();
const { cycleId } = router.query;
const { blockUpdateHandler, blocks, enableReorder } = props;
// chart hook
const { activeBlock, dispatch } = useChart();
// update the active block on hover
@@ -84,12 +82,7 @@ export const CycleGanttSidebar: React.FC<Props> = (props) => {
<DragDropContext onDragEnd={handleOrderChange}>
<Droppable droppableId="gantt-sidebar">
{(droppableProvided) => (
<div
id={`gantt-sidebar-${cycleId}`}
className="mt-3 max-h-full overflow-y-auto pl-2.5"
ref={droppableProvided.innerRef}
{...droppableProvided.droppableProps}
>
<div className="h-full" ref={droppableProvided.innerRef} {...droppableProvided.droppableProps}>
<>
{blocks ? (
blocks.map((block, index) => {
@@ -104,7 +97,9 @@ export const CycleGanttSidebar: React.FC<Props> = (props) => {
>
{(provided, snapshot) => (
<div
className={`h-11 ${snapshot.isDragging ? "rounded bg-custom-background-80" : ""}`}
className={cn({
"rounded bg-custom-background-80": snapshot.isDragging,
})}
onMouseEnter={() => updateActiveBlock(block)}
onMouseLeave={() => updateActiveBlock(null)}
ref={provided.innerRef}
@@ -112,9 +107,12 @@ export const CycleGanttSidebar: React.FC<Props> = (props) => {
>
<div
id={`sidebar-block-${block.id}`}
className={`group flex h-full w-full items-center gap-2 rounded-l px-2 pr-4 ${
activeBlock?.id === block.id ? "bg-custom-background-80" : ""
}`}
className={cn("group w-full flex items-center gap-2 pl-2 pr-4", {
"bg-custom-background-80": activeBlock?.id === block.id,
})}
style={{
height: `${BLOCK_HEIGHT}px`,
}}
>
{enableReorder && (
<button
@@ -128,9 +126,9 @@ export const CycleGanttSidebar: React.FC<Props> = (props) => {
)}
<div className="flex h-full flex-grow items-center justify-between gap-2 truncate">
<div className="flex-grow truncate">
<CycleGanttSidebarBlock data={block.data} />
<CycleGanttSidebarBlock cycleId={block.data.id} />
</div>
{duration !== undefined && (
{duration && (
<div className="flex-shrink-0 text-sm text-custom-text-200">
{duration} day{duration > 1 ? "s" : ""}
</div>
+5 -4
View File
@@ -1,4 +1,5 @@
export * from "./cycle-sidebar";
export * from "./module-sidebar";
export * from "./sidebar";
export * from "./project-view-sidebar";
export * from "./cycles";
export * from "./issues";
export * from "./modules";
export * from "./project-views";
export * from "./root";
@@ -0,0 +1,173 @@
import { observer } from "mobx-react";
import { DragDropContext, Draggable, Droppable, DropResult } from "@hello-pangea/dnd";
import { MoreVertical } from "lucide-react";
// hooks
import { useChart } from "components/gantt-chart/hooks";
import { useIssueDetail } from "hooks/store";
// ui
import { Loader } from "@plane/ui";
// components
import { IssueGanttSidebarBlock } from "components/issues";
// helpers
import { findTotalDaysInRange } from "helpers/date-time.helper";
import { cn } from "helpers/common.helper";
// types
import { IGanttBlock, IBlockUpdateData } from "components/gantt-chart/types";
import { BLOCK_HEIGHT } from "../constants";
type Props = {
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
blocks: IGanttBlock[] | null;
enableReorder: boolean;
showAllBlocks?: boolean;
};
export const IssueGanttSidebar: React.FC<Props> = observer((props: Props) => {
const { blockUpdateHandler, blocks, enableReorder, showAllBlocks = false } = props;
const { activeBlock, dispatch } = useChart();
const { peekIssue } = useIssueDetail();
// update the active block on hover
const updateActiveBlock = (block: IGanttBlock | null) => {
dispatch({
type: "PARTIAL_UPDATE",
payload: {
activeBlock: block,
},
});
};
const handleOrderChange = (result: DropResult) => {
if (!blocks) return;
const { source, destination } = result;
// return if dropped outside the list
if (!destination) return;
// return if dropped on the same index
if (source.index === destination.index) return;
let updatedSortOrder = blocks[source.index].sort_order;
// update the sort order to the lowest if dropped at the top
if (destination.index === 0) updatedSortOrder = blocks[0].sort_order - 1000;
// update the sort order to the highest if dropped at the bottom
else if (destination.index === blocks.length - 1) updatedSortOrder = blocks[blocks.length - 1].sort_order + 1000;
// update the sort order to the average of the two adjacent blocks if dropped in between
else {
const destinationSortingOrder = blocks[destination.index].sort_order;
const relativeDestinationSortingOrder =
source.index < destination.index
? blocks[destination.index + 1].sort_order
: blocks[destination.index - 1].sort_order;
updatedSortOrder = (destinationSortingOrder + relativeDestinationSortingOrder) / 2;
}
// extract the element from the source index and insert it at the destination index without updating the entire array
const removedElement = blocks.splice(source.index, 1)[0];
blocks.splice(destination.index, 0, removedElement);
// call the block update handler with the updated sort order, new and old index
blockUpdateHandler(removedElement.data, {
sort_order: {
destinationIndex: destination.index,
newSortOrder: updatedSortOrder,
sourceIndex: source.index,
},
});
};
return (
<>
<DragDropContext onDragEnd={handleOrderChange}>
<Droppable droppableId="gantt-sidebar">
{(droppableProvided) => (
<div ref={droppableProvided.innerRef} {...droppableProvided.droppableProps}>
<>
{blocks ? (
blocks.map((block, index) => {
const isBlockVisibleOnSidebar = block.start_date && block.target_date;
// hide the block if it doesn't have start and target dates and showAllBlocks is false
if (!showAllBlocks && !isBlockVisibleOnSidebar) return;
const duration =
!block.start_date || !block.target_date
? null
: findTotalDaysInRange(block.start_date, block.target_date);
return (
<Draggable
key={`sidebar-block-${block.id}`}
draggableId={`sidebar-block-${block.id}`}
index={index}
isDragDisabled={!enableReorder}
>
{(provided, snapshot) => (
<div
className={cn({
"rounded bg-custom-background-80": snapshot.isDragging,
"rounded-l border border-r-0 border-custom-primary-70 hover:border-custom-primary-70":
peekIssue?.issueId === block.data.id,
})}
onMouseEnter={() => updateActiveBlock(block)}
onMouseLeave={() => updateActiveBlock(null)}
ref={provided.innerRef}
{...provided.draggableProps}
>
<div
className={cn("group w-full flex items-center gap-2 pl-2 pr-4", {
"bg-custom-background-80": activeBlock?.id === block.id,
})}
style={{
height: `${BLOCK_HEIGHT}px`,
}}
>
{enableReorder && (
<button
type="button"
className="flex flex-shrink-0 rounded p-0.5 text-custom-sidebar-text-200 opacity-0 group-hover:opacity-100"
{...provided.dragHandleProps}
>
<MoreVertical className="h-3.5 w-3.5" />
<MoreVertical className="-ml-5 h-3.5 w-3.5" />
</button>
)}
<div className="flex h-full flex-grow items-center justify-between gap-2 truncate">
<div className="flex-grow truncate">
<IssueGanttSidebarBlock issueId={block.data.id} />
</div>
{duration && (
<div className="flex-shrink-0 text-sm text-custom-text-200">
<span>
{duration} day{duration > 1 ? "s" : ""}
</span>
</div>
)}
</div>
</div>
</div>
)}
</Draggable>
);
})
) : (
<Loader className="space-y-3 pr-2">
<Loader.Item height="34px" />
<Loader.Item height="34px" />
<Loader.Item height="34px" />
<Loader.Item height="34px" />
</Loader>
)}
{droppableProvided.placeholder}
</>
</div>
)}
</Droppable>
</DragDropContext>
</>
);
});
@@ -1,4 +1,3 @@
import { useRouter } from "next/router";
import { DragDropContext, Draggable, Droppable, DropResult } from "@hello-pangea/dnd";
import { MoreVertical } from "lucide-react";
// hooks
@@ -9,8 +8,11 @@ import { Loader } from "@plane/ui";
import { ModuleGanttSidebarBlock } from "components/modules";
// helpers
import { findTotalDaysInRange } from "helpers/date-time.helper";
import { cn } from "helpers/common.helper";
// types
import { IBlockUpdateData, IGanttBlock } from "components/gantt-chart";
// constants
import { BLOCK_HEIGHT } from "../constants";
type Props = {
title: string;
@@ -20,12 +22,8 @@ type Props = {
};
export const ModuleGanttSidebar: React.FC<Props> = (props) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { title, blockUpdateHandler, blocks, enableReorder } = props;
const router = useRouter();
const { cycleId } = router.query;
const { blockUpdateHandler, blocks, enableReorder } = props;
// chart hook
const { activeBlock, dispatch } = useChart();
// update the active block on hover
@@ -84,12 +82,7 @@ export const ModuleGanttSidebar: React.FC<Props> = (props) => {
<DragDropContext onDragEnd={handleOrderChange}>
<Droppable droppableId="gantt-sidebar">
{(droppableProvided) => (
<div
id={`gantt-sidebar-${cycleId}`}
className="mt-3 max-h-full overflow-y-auto pl-2.5"
ref={droppableProvided.innerRef}
{...droppableProvided.droppableProps}
>
<div className="h-full" ref={droppableProvided.innerRef} {...droppableProvided.droppableProps}>
<>
{blocks ? (
blocks.map((block, index) => {
@@ -104,7 +97,9 @@ export const ModuleGanttSidebar: React.FC<Props> = (props) => {
>
{(provided, snapshot) => (
<div
className={`h-11 ${snapshot.isDragging ? "rounded bg-custom-background-80" : ""}`}
className={cn({
"rounded bg-custom-background-80": snapshot.isDragging,
})}
onMouseEnter={() => updateActiveBlock(block)}
onMouseLeave={() => updateActiveBlock(null)}
ref={provided.innerRef}
@@ -112,9 +107,12 @@ export const ModuleGanttSidebar: React.FC<Props> = (props) => {
>
<div
id={`sidebar-block-${block.id}`}
className={`group flex h-full w-full items-center gap-2 rounded-l px-2 pr-4 ${
activeBlock?.id === block.id ? "bg-custom-background-80" : ""
}`}
className={cn("group w-full flex items-center gap-2 pl-2 pr-4", {
"bg-custom-background-80": activeBlock?.id === block.id,
})}
style={{
height: `${BLOCK_HEIGHT}px`,
}}
>
{enableReorder && (
<button
@@ -128,7 +126,7 @@ export const ModuleGanttSidebar: React.FC<Props> = (props) => {
)}
<div className="flex h-full flex-grow items-center justify-between gap-2 truncate">
<div className="flex-grow truncate">
<ModuleGanttSidebarBlock data={block.data} />
<ModuleGanttSidebarBlock moduleId={block.data.id} />
</div>
{duration !== undefined && (
<div className="flex-shrink-0 text-sm text-custom-text-200">
@@ -1,4 +1,3 @@
import { useRouter } from "next/router";
import { DragDropContext, Draggable, Droppable, DropResult } from "@hello-pangea/dnd";
import { MoreVertical } from "lucide-react";
// hooks
@@ -11,6 +10,8 @@ import { IssueGanttSidebarBlock } from "components/issues";
import { findTotalDaysInRange } from "helpers/date-time.helper";
// types
import { IBlockUpdateData, IGanttBlock } from "components/gantt-chart/types";
// constants
import { BLOCK_HEIGHT } from "../constants";
type Props = {
title: string;
@@ -21,12 +22,8 @@ type Props = {
};
export const ProjectViewGanttSidebar: React.FC<Props> = (props) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { title, blockUpdateHandler, blocks, enableReorder } = props;
const router = useRouter();
const { cycleId } = router.query;
const { blockUpdateHandler, blocks, enableReorder } = props;
// chart hook
const { activeBlock, dispatch } = useChart();
// update the active block on hover
@@ -86,7 +83,6 @@ export const ProjectViewGanttSidebar: React.FC<Props> = (props) => {
<Droppable droppableId="gantt-sidebar">
{(droppableProvided) => (
<div
id={`gantt-sidebar-${cycleId}`}
className="mt-3 max-h-full overflow-y-auto pl-2.5"
ref={droppableProvided.innerRef}
{...droppableProvided.droppableProps}
@@ -105,7 +101,10 @@ export const ProjectViewGanttSidebar: React.FC<Props> = (props) => {
>
{(provided, snapshot) => (
<div
className={`h-11 ${snapshot.isDragging ? "rounded bg-custom-background-80" : ""}`}
className={`${snapshot.isDragging ? "rounded bg-custom-background-80" : ""}`}
style={{
height: `${BLOCK_HEIGHT}px`,
}}
onMouseEnter={() => updateActiveBlock(block)}
onMouseLeave={() => updateActiveBlock(null)}
ref={provided.innerRef}
@@ -129,7 +128,7 @@ export const ProjectViewGanttSidebar: React.FC<Props> = (props) => {
)}
<div className="flex h-full flex-grow items-center justify-between gap-2 truncate">
<div className="flex-grow truncate">
<IssueGanttSidebarBlock data={block.data} />
<IssueGanttSidebarBlock issueId={block.data.id} />
</div>
{duration !== undefined && (
<div className="flex-shrink-0 text-sm text-custom-text-200">
@@ -0,0 +1,43 @@
// components
import { IBlockUpdateData, IGanttBlock } from "components/gantt-chart";
// constants
import { HEADER_HEIGHT, SIDEBAR_WIDTH } from "../constants";
type Props = {
blocks: IGanttBlock[] | null;
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
enableReorder: boolean;
sidebarToRender: (props: any) => React.ReactNode;
title: string;
quickAdd?: React.JSX.Element | undefined;
};
export const GanttChartSidebar: React.FC<Props> = (props) => {
const { blocks, blockUpdateHandler, enableReorder, sidebarToRender, title, quickAdd } = props;
return (
<div
// DO NOT REMOVE THE ID
id="gantt-sidebar"
className="sticky left-0 z-10 min-h-full h-max flex-shrink-0 border-r-[0.5px] border-custom-border-200 bg-custom-background-100"
style={{
width: `${SIDEBAR_WIDTH}px`,
}}
>
<div
className="box-border flex-shrink-0 flex items-end justify-between gap-2 border-b-[0.5px] border-custom-border-200 pb-2 pl-8 pr-4 text-sm font-medium text-custom-text-300 sticky top-0 z-10 bg-custom-background-100"
style={{
height: `${HEADER_HEIGHT}px`,
}}
>
<h6>{title}</h6>
<h6>Duration</h6>
</div>
<div className="min-h-full h-max bg-custom-background-100 overflow-x-hidden overflow-y-auto">
{sidebarToRender && sidebarToRender({ title, blockUpdateHandler, blocks, enableReorder })}
</div>
{quickAdd ? quickAdd : null}
</div>
);
};

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