Compare commits

...
Author SHA1 Message Date
Vamsi KuramaandGitHub 11faf3f810 Merge pull request #1600 from makeplane/stage-release
promote: stage-release to master
2023-07-20 19:36:43 +05:30
Aaryan KhandelwalandGitHub 30ea1adf61 Merge pull request #1597 from makeplane/develop
promote: develop to stage-release
2023-07-20 19:32:04 +05:30
Dakshesh JainandGitHub 546aa40aa3 fix: un-read count not mutating (#1598)
* refactor: height of popover & api fetch call

* fix: notification subscribe endpoint

* fix: un-read count not mutating
2023-07-20 19:15:13 +05:30
NikhilandGitHub 78669363b1 fix: restrict notifications for created by when the actor is same (#1596) 2023-07-20 18:53:02 +05:30
guru_sainathandGitHub bca749986a Merge pull request #1595 from makeplane/develop
promote: develop to stage-release
2023-07-20 18:11:18 +05:30
gurusainath 229938114d Merge branch 'develop' of gurusainath:makeplane/plane into develop 2023-07-20 17:25:31 +05:30
gurusainath c5e418ab47 Merge branch 'stage-release' of gurusainath:makeplane/plane into develop 2023-07-20 17:25:00 +05:30
ecdd1f1d03 promote: develop to stage-release (#1594)
* fix: onboarding invitations overflow (#1575)

* fix: onboarding invitations overflow

* fix: user avatar in the notification card

* style: update graph grid color

* fix: no 'Create by me' label coming up (#1573)

* feat: added new issue subscriber table

* dev: notification model

* feat: added CRUD operation for issue subscriber

* Revert "feat: added CRUD operation for issue subscriber"

This reverts commit b22e062576.

* feat: added CRUD operation for issue subscriber

* dev: notification models and operations

* dev: remove delete endpoint response data

* dev: notification endpoints and fix bg worker for saving notifications

* feat: added list and unsubscribe function in issue subscriber

* dev: filter by snoozed and response update for list and permissions

* dev: update issue notifications

* dev: notification  segregation

* dev: update notifications

* dev: notification filtering

* dev: add issue name in notifications

* dev: notification new endpoints

* fix: pushing local settings

* feat: notification workflow setup and made basic UI

* style: improved UX with toast alerts and other interactions

refactor: changed classnames according to new theme structure, changed all icons to material icons

* feat: showing un-read notification count

* feat: not showing 'subscribe' button on issue created by user & assigned to user

not showing 'Create by you' for view & guest of the workspace

* fix: 'read' -> 'unread' heading, my issue wrong filter

* feat: made snooze dropdown & modal

feat: switched to calendar

* fix: minor ui fixes

* feat: snooze modal date/time select

* fix: params for read/un-read notification

* style: snooze notification modal

* fix: no label for 'Create by me'

* fix: no label for 'Create by me'

* fix: removed console log

* fix: tooltip going behind popover

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>

* style: tooltip on notification header actions (#1577)

* style: tooltip on notification header

* chore: update tooltip content

---------

Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>

* fix: user migrations for back population (#1578)

* fix: total notifications count (#1579)

* fix: notification card (#1583)

* feat: add new icons package (#1586)

* feat: add material icons package

* chore: replace issue view icons

* chore: notification ordering (#1584)

* fix: uuid error when cycle and module updates (#1585)

* refactor: height of popover & api fetch call (#1587)

* fix: snooze dropdown overflow (#1588)

* fix: notification subscribe endpoint (#1593)

* refactor: height of popover & api fetch call

* fix: notification subscribe endpoint

* chore: notification empty state overflow (#1592)

* chore: notification empty state overflow

* fix: white logo

* fix: custom theme default values

* fix: custom theme default values

* fix: issues count to remove archived issues (#1591)

* dev: background migration for user custom themes (#1590)

---------

Co-authored-by: Dakshesh Jain <65905942+dakshesh14@users.noreply.github.com>
Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
Co-authored-by: Nikhil <118773738+pablohashescobar@users.noreply.github.com>
Co-authored-by: gurusainath <gurusainath007@gmail.com>
2023-07-20 17:24:17 +05:30
NikhilandGitHub e687cd6f6a dev: background migration for user custom themes (#1590) 2023-07-20 16:59:09 +05:30
NikhilandGitHub 9b6721790f fix: issues count to remove archived issues (#1591) 2023-07-20 16:58:34 +05:30
gurusainath 05df65577a Merge branch 'stage-release' of gurusainath:makeplane/plane into develop 2023-07-20 16:54:40 +05:30
Aaryan KhandelwalandGitHub 52d21b9dda chore: notification empty state overflow (#1592)
* chore: notification empty state overflow

* fix: white logo

* fix: custom theme default values

* fix: custom theme default values
2023-07-20 16:51:00 +05:30
Dakshesh JainandGitHub 51f10d5f36 fix: notification subscribe endpoint (#1593)
* refactor: height of popover & api fetch call

* fix: notification subscribe endpoint
2023-07-20 16:35:18 +05:30
9275e6f373 promote: develop to stage-release (#1589)
* fix: onboarding invitations overflow (#1575)

* fix: onboarding invitations overflow

* fix: user avatar in the notification card

* style: update graph grid color

* fix: no 'Create by me' label coming up (#1573)

* feat: added new issue subscriber table

* dev: notification model

* feat: added CRUD operation for issue subscriber

* Revert "feat: added CRUD operation for issue subscriber"

This reverts commit b22e062576.

* feat: added CRUD operation for issue subscriber

* dev: notification models and operations

* dev: remove delete endpoint response data

* dev: notification endpoints and fix bg worker for saving notifications

* feat: added list and unsubscribe function in issue subscriber

* dev: filter by snoozed and response update for list and permissions

* dev: update issue notifications

* dev: notification  segregation

* dev: update notifications

* dev: notification filtering

* dev: add issue name in notifications

* dev: notification new endpoints

* fix: pushing local settings

* feat: notification workflow setup and made basic UI

* style: improved UX with toast alerts and other interactions

refactor: changed classnames according to new theme structure, changed all icons to material icons

* feat: showing un-read notification count

* feat: not showing 'subscribe' button on issue created by user & assigned to user

not showing 'Create by you' for view & guest of the workspace

* fix: 'read' -> 'unread' heading, my issue wrong filter

* feat: made snooze dropdown & modal

feat: switched to calendar

* fix: minor ui fixes

* feat: snooze modal date/time select

* fix: params for read/un-read notification

* style: snooze notification modal

* fix: no label for 'Create by me'

* fix: no label for 'Create by me'

* fix: removed console log

* fix: tooltip going behind popover

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>

* style: tooltip on notification header actions (#1577)

* style: tooltip on notification header

* chore: update tooltip content

---------

Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>

* fix: user migrations for back population (#1578)

* fix: total notifications count (#1579)

* fix: notification card (#1583)

* feat: add new icons package (#1586)

* feat: add material icons package

* chore: replace issue view icons

* chore: notification ordering (#1584)

* fix: uuid error when cycle and module updates (#1585)

* refactor: height of popover & api fetch call (#1587)

* fix: snooze dropdown overflow (#1588)

---------

Co-authored-by: Dakshesh Jain <65905942+dakshesh14@users.noreply.github.com>
Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
Co-authored-by: Nikhil <118773738+pablohashescobar@users.noreply.github.com>
2023-07-20 15:14:57 +05:30
Aaryan KhandelwalandGitHub 4aef8c2242 fix: snooze dropdown overflow (#1588) 2023-07-20 14:49:30 +05:30
Dakshesh JainandGitHub 780573dadd refactor: height of popover & api fetch call (#1587) 2023-07-20 14:33:24 +05:30
NikhilandGitHub 5e625ab132 fix: uuid error when cycle and module updates (#1585) 2023-07-20 14:18:28 +05:30
NikhilandGitHub 34123681cf chore: notification ordering (#1584) 2023-07-20 14:18:06 +05:30
Aaryan KhandelwalandGitHub c72ff782ac feat: add new icons package (#1586)
* feat: add material icons package

* chore: replace issue view icons
2023-07-20 14:17:21 +05:30
Aaryan KhandelwalandGitHub 6eb72507a5 fix: notification card (#1583) 2023-07-20 10:52:52 +05:30
Aaryan KhandelwalandGitHub 26b18b431b fix: total notifications count (#1579) 2023-07-19 20:37:40 +05:30
NikhilandGitHub 1bae9289f5 fix: user migrations for back population (#1578) 2023-07-19 20:06:12 +05:30
5c5bcb33e3 style: tooltip on notification header actions (#1577)
* style: tooltip on notification header

* chore: update tooltip content

---------

Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
2023-07-19 18:42:32 +05:30
bed5f76082 fix: no 'Create by me' label coming up (#1573)
* feat: added new issue subscriber table

* dev: notification model

* feat: added CRUD operation for issue subscriber

* Revert "feat: added CRUD operation for issue subscriber"

This reverts commit b22e062576.

* feat: added CRUD operation for issue subscriber

* dev: notification models and operations

* dev: remove delete endpoint response data

* dev: notification endpoints and fix bg worker for saving notifications

* feat: added list and unsubscribe function in issue subscriber

* dev: filter by snoozed and response update for list and permissions

* dev: update issue notifications

* dev: notification  segregation

* dev: update notifications

* dev: notification filtering

* dev: add issue name in notifications

* dev: notification new endpoints

* fix: pushing local settings

* feat: notification workflow setup and made basic UI

* style: improved UX with toast alerts and other interactions

refactor: changed classnames according to new theme structure, changed all icons to material icons

* feat: showing un-read notification count

* feat: not showing 'subscribe' button on issue created by user & assigned to user

not showing 'Create by you' for view & guest of the workspace

* fix: 'read' -> 'unread' heading, my issue wrong filter

* feat: made snooze dropdown & modal

feat: switched to calendar

* fix: minor ui fixes

* feat: snooze modal date/time select

* fix: params for read/un-read notification

* style: snooze notification modal

* fix: no label for 'Create by me'

* fix: no label for 'Create by me'

* fix: removed console log

* fix: tooltip going behind popover

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
2023-07-19 17:40:40 +05:30
Aaryan KhandelwalandGitHub 124c2f772e fix: onboarding invitations overflow (#1575)
* fix: onboarding invitations overflow

* fix: user avatar in the notification card

* style: update graph grid color
2023-07-19 17:09:53 +05:30
Vihar KuramaandGitHub b38898753f Merge pull request #1570 from makeplane/develop
promote: develop to stage-release
2023-07-19 16:07:43 +05:30
guru_sainathandGitHub 86a120f11e Merge branch 'stage-release' into develop 2023-07-19 16:05:13 +05:30
Rhea JainandGitHub da603dc3f8 Update README.md (#1543) 2023-07-19 15:28:01 +05:30
NikhilandGitHub 2f3970f641 feat: beat worker for configuration (#1571) 2023-07-19 15:25:33 +05:30
NikhilandGitHub d759438ebd fix: issue automation (#1569) 2023-07-19 15:08:41 +05:30
NikhilandGitHub 0102f1d693 fix: psycopg errors (#1568) 2023-07-19 15:07:54 +05:30
53e443d816 feat: notifications (#1566)
* feat: added new issue subscriber table

* dev: notification model

* feat: added CRUD operation for issue subscriber

* Revert "feat: added CRUD operation for issue subscriber"

This reverts commit b22e062576.

* feat: added CRUD operation for issue subscriber

* dev: notification models and operations

* dev: remove delete endpoint response data

* dev: notification endpoints and fix bg worker for saving notifications

* feat: added list and unsubscribe function in issue subscriber

* dev: filter by snoozed and response update for list and permissions

* dev: update issue notifications

* dev: notification  segregation

* dev: update notifications

* dev: notification filtering

* dev: add issue name in notifications

* dev: notification new endpoints

* fix: pushing local settings

* feat: notification workflow setup and made basic UI

* style: improved UX with toast alerts and other interactions

refactor: changed classnames according to new theme structure, changed all icons to material icons

* feat: showing un-read notification count

* feat: not showing 'subscribe' button on issue created by user & assigned to user

not showing 'Create by you' for view & guest of the workspace

* fix: 'read' -> 'unread' heading, my issue wrong filter

* feat: made snooze dropdown & modal

feat: switched to calendar

* fix: minor ui fixes

* feat: snooze modal date/time select

* fix: params for read/un-read notification

* style: snooze notification modal

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
2023-07-19 14:44:04 +05:30
NikhilandGitHub 98b9957753 fix: issue created notification (#1553)
* fix: issue created notification

* chore: my issues assigned notifications

* dev: fix read at notification
2023-07-19 14:31:45 +05:30
NikhilandGitHub 509af4662d dev: migrations (#1561) 2023-07-19 14:31:07 +05:30
Aaryan KhandelwalandGitHub d1f2a819f5 fix: update border colors on theme switch (#1565) 2023-07-19 13:53:22 +05:30
Aaryan KhandelwalandGitHub a42bff675b fix: archived issue details page overflow, restore button loading (#1564) 2023-07-19 13:29:08 +05:30
Aaryan KhandelwalandGitHub c71a2137e6 fix: scope and demand line graph y axis values (#1563) 2023-07-19 13:13:23 +05:30
Rhea JainandGitHub 5e1f0a2604 fixes as per consistency (#1562) 2023-07-19 13:00:11 +05:30
Aaryan KhandelwalandGitHub ccab382d7d chore: add dashboard navbar (#1560) 2023-07-19 12:16:39 +05:30
Aaryan KhandelwalandGitHub a7aa78a349 chore: high contrast theming (#1559)
* fix: archived issues empty state

* chore: update high contrast theme colors
2023-07-19 11:47:41 +05:30
Aaryan KhandelwalandGitHub 5ae963c451 fix: tour sidebar icons, auth pages scroll (#1555)
* fix: tour sidebar icons, menu button word wrap

* chore: change theme on sign out

* fix: auth pages scroll y-padding
2023-07-18 19:02:33 +05:30
Dakshesh JainandGitHub 07c097c9ad style: different style for archive & project issues (#1552)
* fix: same empty state coming for all issue list related UI

* style: different style for archive & project  issues
2023-07-18 18:58:36 +05:30
Aaryan KhandelwalandGitHub fc92d7d1a0 chore: hide issue properties if there are none (#1554) 2023-07-18 18:47:28 +05:30
NikhilandGitHub 9ba8f5c21f fix: cycle module notifications (#1536)
* chore: add triggered by details for notifications

* dev: update issue activity json to include extra fields

* dev: improve issue fetch
2023-07-18 15:59:58 +05:30
Aaryan KhandelwalandGitHub 059b8c793a refactor: parent issue select (#1546)
* refactor: parent issue select

* fix: sibling issues list
2023-07-18 15:36:03 +05:30
Dakshesh JainandGitHub 93da220c4a fix: same empty state coming for all issue list related UI (#1547) 2023-07-18 15:28:12 +05:30
Aaryan KhandelwalandGitHub 0feab162ff style: onboarding screens (#1539)
* fix: onboarding screen styling

* chore: minor styling fixes

* chore: disable buttons if form is invalid
2023-07-18 15:20:05 +05:30
NikhilandGitHub 55a1291b1d fix: inbox issue archival (#1538)
* chore: add triggered by details for notifications

* dev: update issue activity json to include extra fields

* dev: fix inboc issue archival

* dev: update the filter for closing issues
2023-07-18 15:10:20 +05:30
NikhilandGitHub b12a00cf4a chore: default project state colors (#1540)
* chore: add triggered by details for notifications

* dev: update issue activity json to include extra fields

* chore: project create default state colors
2023-07-18 15:10:01 +05:30
NikhilandGitHub 52ee8c5615 chore: remove logger from gpt assistant (#1542)
* chore: add triggered by details for notifications

* dev: update issue activity json to include extra fields

* chore: remove logger from gpt assistant
2023-07-18 15:09:44 +05:30
NikhilandGitHub 68108c9fe9 fix: favorite permissions (#1549)
* chore: add triggered by details for notifications

* dev: update issue activity json to include extra fields

* dev: fix permissions for cycle and views
2023-07-18 15:09:18 +05:30
Aaryan KhandelwalandGitHub 88fbe85087 fix: button and input heights (#1548) 2023-07-18 15:00:06 +05:30
Dakshesh JainandGitHub 9b423cea4b fix: create workspace valid (#1535)
show alert on-change rather then on-submit
2023-07-18 12:08:22 +05:30
Dakshesh JainandGitHub 9d891ecce1 fix: selecting label once create using modal (#1537) 2023-07-18 12:08:09 +05:30
16a7bd3bda feat: user issue notifications (#1523)
* feat: added new issue subscriber table

* dev: notification model

* feat: added CRUD operation for issue subscriber

* Revert "feat: added CRUD operation for issue subscriber"

This reverts commit b22e062576.

* feat: added CRUD operation for issue subscriber

* dev: notification models and operations

* dev: remove delete endpoint response data

* dev: notification endpoints and fix bg worker for saving notifications

* feat: added list and unsubscribe function in issue subscriber

* dev: filter by snoozed and response update for list and permissions

* dev: update issue notifications

* dev: notification  segregation

* dev: update notifications

* dev: notification filtering

* dev: add issue name in notifications

* dev: notification new endpoints

* fix: pushing local settings

* feat: notification workflow setup and made basic UI

* style: improved UX with toast alerts and other interactions

refactor: changed classnames according to new theme structure, changed all icons to material icons

* feat: showing un-read notification count

* feat: not showing 'subscribe' button on issue created by user & assigned to user

not showing 'Create by you' for view & guest of the workspace

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
2023-07-18 12:07:55 +05:30
Dakshesh JainandGitHub 6e9f3971a5 style: empty state for archive issue (#1541) 2023-07-18 12:07:35 +05:30
pablohashescobarandGitHub fd3f6ab7a4 Merge pull request #1478 from makeplane/stage-release
promote: stage-release to master
2023-07-05 21:24:39 +05:30
pablohashescobarandGitHub d05d814efe chore: workspace invitation for self-hosted versions (#1477) 2023-07-05 21:21:31 +05:30
Vamsi KuramaandGitHub 6394f517a2 Merge pull request #1413 from makeplane/stage-release
promote: stage-release to master
2023-06-28 19:19:59 +05:30
379d258375 fix: charts design and mutation (#1426) (#1427)
* fix: pie chart overlap issue

* fix: burndown chart mutation

* fix: burndown chart mutation

Co-authored-by: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com>
2023-06-28 19:09:44 +05:30
pablohashescobarandGitHub 6a27827c16 Merge pull request #1425 from makeplane/develop
promote: develop to stage-release
2023-06-28 17:48:53 +05:30
pablohashescobarandGitHub 206ca98307 Merge pull request #1418 from makeplane/develop
promote: develop to stage-release
2023-06-28 11:14:41 +05:30
pablohashescobarandGitHub 473c32d3d4 fix: github importer issue (#1414) (#1415) 2023-06-27 22:56:06 +05:30
guru_sainathandGitHub 124d1c30aa Merge pull request #1410 from makeplane/develop
promote: develop to stage-release
2023-06-27 19:01:26 +05:30
guru_sainathandGitHub fd7274ba1c Merge pull request #1406 from makeplane/develop
promote: develop to stage-release
2023-06-27 16:48:34 +05:30
pablohashescobarandGitHub 46d93525be Merge pull request #1386 from makeplane/develop
promote: develop to stage-release
2023-06-24 01:34:34 +05:30
Vamsi KuramaandGitHub dbbce439ff Merge pull request #1311 from makeplane/stage-release
promote: staging to master
2023-06-16 21:34:03 +05:30
pablohashescobarandGitHub 4c0857233e fix: workspace member invite to avoid lower permission user to invite higher permission member (#1309) 2023-06-16 19:52:24 +05:30
111 changed files with 6116 additions and 1352 deletions
+8 -8
View File
@@ -19,14 +19,14 @@
<p>
<a href="https://app.plane.so/#gh-light-mode-only" target="_blank">
<img
src="https://ik.imagekit.io/killbluedog/Plane_Screen.png?updatedAt=1684942001069"
src="https://plane-marketing.s3.ap-south-1.amazonaws.com/Plane+Screen.png"
alt="Plane Screens"
width="100%"
/>
</a>
<a href="https://app.plane.so/#gh-dark-mode-only" target="_blank">
<img
src="https://ik.imagekit.io/killbluedog/Plane_Screens_Dark_Mode.png?updatedAt=1684942388044"
src="https://plane-marketing.s3.ap-south-1.amazonaws.com/Plane+Screens+Dark+Mode.png"
alt="Plane Screens"
width="100%"
/>
@@ -86,7 +86,7 @@ docker compose up -d
<p>
<a href="https://plane.so" target="_blank">
<img
src="https://ik.imagekit.io/killbluedog/Plane_Views_Dark_Mode.png?updatedAt=1684943050275"
src="https://plane-marketing.s3.ap-south-1.amazonaws.com/Plane+Views+Dark+Mode.png"
alt="Plane Views"
width="100%"
/>
@@ -95,7 +95,7 @@ docker compose up -d
<p>
<a href="https://plane.so" target="_blank">
<img
src="https://ik.imagekit.io/killbluedog/Plane_Issue_Detail_Dark_Mode.png?updatedAt=1684943050202"
src="https://plane-marketing.s3.ap-south-1.amazonaws.com/Plane+Issue+Detail+Dark+Mode.png"
alt="Plane Issue Details"
width="100%"
/>
@@ -104,7 +104,7 @@ docker compose up -d
<p>
<a href="https://plane.so" target="_blank">
<img
src="https://ik.imagekit.io/killbluedog/Plane_Cycles___Modules_Dark_Mode.png?updatedAt=1684943050281"
src="https://plane-marketing.s3.ap-south-1.amazonaws.com/Plane+Cycles+%26+Modules+Dark+Mode.png"
alt="Plane Cycles and Modules"
width="100%"
/>
@@ -113,7 +113,7 @@ docker compose up -d
<p>
<a href="https://plane.so" target="_blank">
<img
src="https://ik.imagekit.io/killbluedog/Plane_Analytics_Dark_Mode.png?updatedAt=1684944596824"
src="https://plane-marketing.s3.ap-south-1.amazonaws.com/Plane+Analytics+Dark+Mode.png"
alt="Plane Analytics"
width="100%"
/>
@@ -122,7 +122,7 @@ docker compose up -d
<p>
<a href="https://plane.so" target="_blank">
<img
src="https://ik.imagekit.io/killbluedog/Plane_Pages_Dark_Mode.png?updatedAt=1684943050202"
src="https://plane-marketing.s3.ap-south-1.amazonaws.com/Plane+Pages+Dark+Mode.png"
alt="Plane Pages"
width="100%"
/>
@@ -132,7 +132,7 @@ docker compose up -d
<p>
<a href="https://plane.so" target="_blank">
<img
src="https://ik.imagekit.io/killbluedog/Plane_Commad_K_Dark_Mode.png?updatedAt=1684943050312"
src="https://plane-marketing.s3.ap-south-1.amazonaws.com/Plane+Commad+K+Dark+Mode.png"
alt="Plane Command Menu"
width="100%"
/>
+1 -1
View File
@@ -49,7 +49,7 @@ USER root
RUN apk --no-cache add "bash~=5.2"
COPY ./bin ./bin/
RUN chmod +x ./bin/takeoff ./bin/worker
RUN chmod +x ./bin/takeoff ./bin/worker ./bin/beat
RUN chmod -R 777 /code
USER captain
+5
View File
@@ -0,0 +1,5 @@
#!/bin/bash
set -e
python manage.py wait_for_db
celery -A plane beat -l info
+1 -1
View File
@@ -6,4 +6,4 @@ python manage.py migrate
# Create a Default User
python bin/user_script.py
exec gunicorn -w 8 -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:8000 --config gunicorn.config.py --max-requests 1200 --max-requests-jitter 1000 --access-logfile -
exec gunicorn -w 8 -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:8000 --max-requests 1200 --max-requests-jitter 1000 --access-logfile -
-3
View File
@@ -706,9 +706,6 @@ class CycleDateCheckEndpoint(BaseAPIView):
class CycleFavoriteViewSet(BaseViewSet):
permission_classes = [
ProjectEntityPermission,
]
serializer_class = CycleFavoriteSerializer
model = CycleFavorite
-26
View File
@@ -30,31 +30,6 @@ class GPTIntegrationEndpoint(BaseAPIView):
status=status.HTTP_400_BAD_REQUEST,
)
count = 0
# If logger is enabled check for request limit
if settings.LOGGER_BASE_URL:
try:
headers = {
"Content-Type": "application/json",
}
response = requests.post(
settings.LOGGER_BASE_URL,
json={"user_id": str(request.user.id)},
headers=headers,
)
count = response.json().get("count", 0)
if not response.json().get("success", False):
return Response(
{
"error": "You have surpassed the monthly limit for AI assistance"
},
status=status.HTTP_429_TOO_MANY_REQUESTS,
)
except Exception as e:
capture_exception(e)
prompt = request.data.get("prompt", False)
task = request.data.get("task", False)
@@ -82,7 +57,6 @@ class GPTIntegrationEndpoint(BaseAPIView):
{
"response": text,
"response_html": text_html,
"count": count,
"project_detail": ProjectLiteSerializer(project).data,
"workspace_detail": WorkspaceLiteSerializer(workspace).data,
},
-3
View File
@@ -480,9 +480,6 @@ class ModuleLinkViewSet(BaseViewSet):
class ModuleFavoriteViewSet(BaseViewSet):
permission_classes = [
ProjectEntityPermission,
]
serializer_class = ModuleFavoriteSerializer
model = ModuleFavorite
+7 -7
View File
@@ -30,17 +30,16 @@ class NotificationViewSet(BaseViewSet):
def list(self, request, slug):
try:
order_by = request.GET.get("order_by", "-created_at")
snoozed = request.GET.get("snoozed", "false")
archived = request.GET.get("archived", "false")
read = request.GET.get("read", "false")
read = request.GET.get("read", "true")
# Filter type
type = request.GET.get("type", "all")
notifications = Notification.objects.filter(
workspace__slug=slug, receiver_id=request.user.id
).order_by(order_by)
).order_by("snoozed_till", "-created_at")
# Filter for snoozed notifications
if snoozed == "false":
@@ -53,8 +52,6 @@ class NotificationViewSet(BaseViewSet):
Q(snoozed_till__lt=timezone.now()) | Q(snoozed_till__isnull=False)
)
if read == "true":
notifications = notifications.filter(read_at__isnull=False)
if read == "false":
notifications = notifications.filter(read_at__isnull=True)
@@ -213,10 +210,11 @@ class UnreadNotificationEndpoint(BaseAPIView):
def get(self, request, slug):
try:
# Watching Issues Count
watching_notification_count = Notification.objects.filter(
watching_issues_count = Notification.objects.filter(
workspace__slug=slug,
receiver_id=request.user.id,
read_at__isnull=True,
archived_at__isnull=True,
entity_identifier__in=IssueSubscriber.objects.filter(
workspace__slug=slug, subscriber_id=request.user.id
).values_list("issue_id", flat=True),
@@ -227,6 +225,7 @@ class UnreadNotificationEndpoint(BaseAPIView):
workspace__slug=slug,
receiver_id=request.user.id,
read_at__isnull=True,
archived_at__isnull=True,
entity_identifier__in=IssueAssignee.objects.filter(
workspace__slug=slug, assignee_id=request.user.id
).values_list("issue_id", flat=True),
@@ -237,6 +236,7 @@ class UnreadNotificationEndpoint(BaseAPIView):
workspace__slug=slug,
receiver_id=request.user.id,
read_at__isnull=True,
archived_at__isnull=True,
entity_identifier__in=Issue.objects.filter(
workspace__slug=slug, created_by=request.user
).values_list("pk", flat=True),
@@ -244,7 +244,7 @@ class UnreadNotificationEndpoint(BaseAPIView):
return Response(
{
"watching_notifications": watching_notification_count,
"watching_issues": watching_issues_count,
"my_issues": my_issues_count,
"created_issues": created_issues_count,
},
+5 -5
View File
@@ -160,32 +160,32 @@ class ProjectViewSet(BaseViewSet):
states = [
{
"name": "Backlog",
"color": "#5e6ad2",
"color": "#A3A3A3",
"sequence": 15000,
"group": "backlog",
"default": True,
},
{
"name": "Todo",
"color": "#eb5757",
"color": "#3A3A3A",
"sequence": 25000,
"group": "unstarted",
},
{
"name": "In Progress",
"color": "#26b5ce",
"color": "#F59E0B",
"sequence": 35000,
"group": "started",
},
{
"name": "Done",
"color": "#f2c94c",
"color": "#16A34A",
"sequence": 45000,
"group": "completed",
},
{
"name": "Cancelled",
"color": "#4cb782",
"color": "#EF4444",
"sequence": 55000,
"group": "cancelled",
},
+84 -67
View File
@@ -1028,23 +1028,26 @@ def issue_activity(
actor = User.objects.get(pk=actor_id)
project = Project.objects.get(pk=project_id)
if type not in [
"cycle.activity.created",
"cycle.activity.deleted",
"module.activity.created",
"module.activity.deleted",
]:
issue = Issue.objects.filter(pk=issue_id, project_id=project_id).first()
try:
issue = Issue.objects.get(pk=issue_id)
issue.updated_at = timezone.now()
issue.save(update_fields=["updated_at"])
except Exception as e:
pass
if issue is not None:
issue.updated_at = timezone.now()
issue.save(update_fields=["updated_at"])
if subscriber:
# add the user to issue subscriber
try:
_ = IssueSubscriber.objects.get_or_create(
issue_id=issue_id, subscriber=actor
)
except Exception as e:
pass
if subscriber:
# add the user to issue subscriber
try:
_ = IssueSubscriber.objects.get_or_create(
issue_id=issue_id, subscriber=actor
)
except Exception as e:
pass
ACTIVITY_MAPPER = {
"issue.activity.created": create_issue_activity,
@@ -1096,66 +1099,80 @@ def issue_activity(
except Exception as e:
capture_exception(e)
# Create Notifications
bulk_notifications = []
if type not in [
"cycle.activity.created",
"cycle.activity.deleted",
"module.activity.created",
"module.activity.deleted",
]:
# Create Notifications
bulk_notifications = []
issue_subscribers = list(
IssueSubscriber.objects.filter(project=project, issue_id=issue_id)
.exclude(subscriber_id=actor_id)
.values_list("subscriber", flat=True)
)
issue_subscribers = list(
IssueSubscriber.objects.filter(project=project, issue_id=issue_id)
.exclude(subscriber_id=actor_id)
.values_list("subscriber", flat=True)
)
issue_assignees = list(
IssueAssignee.objects.filter(project=project, issue_id=issue_id)
.exclude(assignee_id=actor_id)
.values_list("assignee", flat=True)
)
issue_assignees = list(
IssueAssignee.objects.filter(project=project, issue_id=issue_id)
.exclude(assignee_id=actor_id)
.values_list("assignee", flat=True)
)
issue_subscribers = issue_subscribers + issue_assignees
issue_subscribers = issue_subscribers + issue_assignees
# Add bot filtering
if issue.created_by_id is not None and not issue.created_by.is_bot:
issue_subscribers = issue_subscribers + [issue.created_by_id]
issue = Issue.objects.filter(pk=issue_id, project_id=project_id).first()
issue = Issue.objects.get(project=project, pk=issue_id)
for subscriber in issue_subscribers:
for issue_activity in issue_activities_created:
bulk_notifications.append(
Notification(
workspace=project.workspace,
sender="in_app:issue_activities",
triggered_by_id=actor_id,
receiver_id=subscriber,
entity_identifier=issue_id,
entity_name="issue",
project=project,
title=issue_activity.comment,
data={
"issue": {
"id": str(issue_id),
"name": str(issue.name),
"identifier": str(project.identifier),
"sequence_id": issue.sequence_id,
"state_name": issue.state.name,
"state_group": issue.state.group,
# Add bot filtering
if (
issue is not None
and issue.created_by_id is not None
and not issue.created_by.is_bot
and str(issue.created_by_id) != str(actor_id)
):
issue_subscribers = issue_subscribers + [issue.created_by_id]
for subscriber in issue_subscribers:
for issue_activity in issue_activities_created:
bulk_notifications.append(
Notification(
workspace=project.workspace,
sender="in_app:issue_activities",
triggered_by_id=actor_id,
receiver_id=subscriber,
entity_identifier=issue_id,
entity_name="issue",
project=project,
title=issue_activity.comment,
data={
"issue": {
"id": str(issue_id),
"name": str(issue.name),
"identifier": str(project.identifier),
"sequence_id": issue.sequence_id,
"state_name": issue.state.name,
"state_group": issue.state.group,
},
"issue_activity": {
"id": str(issue_activity.id),
"verb": str(issue_activity.verb),
"field": str(issue_activity.field),
"actor": str(issue_activity.actor_id),
"new_value": str(issue_activity.new_value),
"old_value": str(issue_activity.old_value),
"issue_comment": str(
issue_activity.issue_comment.comment_stripped
if issue_activity.issue_comment is not None
else ""
),
},
},
"issue_activity": {
"id": str(issue_activity.id),
"verb": str(issue_activity.verb),
"field": str(issue_activity.field),
"actor": str(issue_activity.actor_id),
"new_value": str(issue_activity.new_value),
"old_value": str(issue_activity.old_value),
"issue_comment": str(
issue_activity.issue_comment.comment_stripped if issue_activity.issue_comment is not None else ""
),
},
},
)
)
)
# Bulk create notifications
Notification.objects.bulk_create(bulk_notifications, batch_size=100)
# Bulk create notifications
Notification.objects.bulk_create(bulk_notifications, batch_size=100)
return
except Exception as e:
@@ -49,6 +49,11 @@ def archive_old_issues():
Q(issue_module__module__target_date__lt=timezone.now().date())
& Q(issue_module__isnull=False)
),
).filter(
Q(issue_inbox__status=1)
| Q(issue_inbox__status=-1)
| Q(issue_inbox__status=2)
| Q(issue_inbox__isnull=True)
)
# Check if Issues
@@ -65,7 +70,7 @@ def archive_old_issues():
[
issue_activity.delay(
type="issue.activity.updated",
requested_data=json.dumps({"archived_at": issue.archived_at}),
requested_data=json.dumps({"archived_at": str(issue.archived_at)}),
actor_id=str(project.created_by_id),
issue_id=issue.id,
project_id=project_id,
@@ -111,6 +116,11 @@ def close_old_issues():
Q(issue_module__module__target_date__lt=timezone.now().date())
& Q(issue_module__isnull=False)
),
).filter(
Q(issue_inbox__status=1)
| Q(issue_inbox__status=-1)
| Q(issue_inbox__status=2)
| Q(issue_inbox__isnull=True)
)
# Check if Issues
@@ -130,7 +140,7 @@ def close_old_issues():
[
issue_activity.delay(
type="issue.activity.updated",
requested_data=json.dumps({"closed_to": issue.state_id}),
requested_data=json.dumps({"closed_to": str(issue.state_id)}),
actor_id=str(project.created_by_id),
issue_id=issue.id,
project_id=project_id,
@@ -0,0 +1,266 @@
# Generated by Django 4.2.3 on 2023-07-19 06:52
from django.conf import settings
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import plane.db.models.user
import uuid
def onboarding_default_steps(apps, schema_editor):
default_onboarding_schema = {
"workspace_join": True,
"profile_complete": True,
"workspace_create": True,
"workspace_invite": True,
}
Model = apps.get_model("db", "User")
updated_user = []
for obj in Model.objects.filter(is_onboarded=True):
obj.onboarding_step = default_onboarding_schema
obj.is_tour_completed = True
updated_user.append(obj)
Model.objects.bulk_update(updated_user, ["onboarding_step", "is_tour_completed"], batch_size=100)
class Migration(migrations.Migration):
dependencies = [
("db", "0036_alter_workspace_organization_size"),
]
operations = [
migrations.AddField(
model_name="issue",
name="archived_at",
field=models.DateField(null=True),
),
migrations.AddField(
model_name="project",
name="archive_in",
field=models.IntegerField(
default=0,
validators=[
django.core.validators.MinValueValidator(0),
django.core.validators.MaxValueValidator(12),
],
),
),
migrations.AddField(
model_name="project",
name="close_in",
field=models.IntegerField(
default=0,
validators=[
django.core.validators.MinValueValidator(0),
django.core.validators.MaxValueValidator(12),
],
),
),
migrations.AddField(
model_name="project",
name="default_state",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="default_state",
to="db.state",
),
),
migrations.AddField(
model_name="user",
name="is_tour_completed",
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name="user",
name="onboarding_step",
field=models.JSONField(default=plane.db.models.user.get_default_onboarding),
),
migrations.RunPython(onboarding_default_steps),
migrations.CreateModel(
name="Notification",
fields=[
(
"created_at",
models.DateTimeField(auto_now_add=True, verbose_name="Created At"),
),
(
"updated_at",
models.DateTimeField(
auto_now=True, verbose_name="Last Modified At"
),
),
(
"id",
models.UUIDField(
db_index=True,
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
unique=True,
),
),
("data", models.JSONField(null=True)),
("entity_identifier", models.UUIDField(null=True)),
("entity_name", models.CharField(max_length=255)),
("title", models.TextField()),
("message", models.JSONField(null=True)),
("message_html", models.TextField(blank=True, default="<p></p>")),
("message_stripped", models.TextField(blank=True, null=True)),
("sender", models.CharField(max_length=255)),
("read_at", models.DateTimeField(null=True)),
("snoozed_till", models.DateTimeField(null=True)),
("archived_at", models.DateTimeField(null=True)),
(
"created_by",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="%(class)s_created_by",
to=settings.AUTH_USER_MODEL,
verbose_name="Created By",
),
),
(
"project",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="notifications",
to="db.project",
),
),
(
"receiver",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="received_notifications",
to=settings.AUTH_USER_MODEL,
),
),
(
"triggered_by",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="triggered_notifications",
to=settings.AUTH_USER_MODEL,
),
),
(
"updated_by",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="%(class)s_updated_by",
to=settings.AUTH_USER_MODEL,
verbose_name="Last Modified By",
),
),
(
"workspace",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="notifications",
to="db.workspace",
),
),
],
options={
"verbose_name": "Notification",
"verbose_name_plural": "Notifications",
"db_table": "notifications",
"ordering": ("-created_at",),
},
),
migrations.CreateModel(
name="IssueSubscriber",
fields=[
(
"created_at",
models.DateTimeField(auto_now_add=True, verbose_name="Created At"),
),
(
"updated_at",
models.DateTimeField(
auto_now=True, verbose_name="Last Modified At"
),
),
(
"id",
models.UUIDField(
db_index=True,
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
unique=True,
),
),
(
"created_by",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="%(class)s_created_by",
to=settings.AUTH_USER_MODEL,
verbose_name="Created By",
),
),
(
"issue",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="issue_subscribers",
to="db.issue",
),
),
(
"project",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="project_%(class)s",
to="db.project",
),
),
(
"subscriber",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="issue_subscribers",
to=settings.AUTH_USER_MODEL,
),
),
(
"updated_by",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="%(class)s_updated_by",
to=settings.AUTH_USER_MODEL,
verbose_name="Last Modified By",
),
),
(
"workspace",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="workspace_%(class)s",
to="db.workspace",
),
),
],
options={
"verbose_name": "Issue Subscriber",
"verbose_name_plural": "Issue Subscribers",
"db_table": "issue_subscribers",
"ordering": ("-created_at",),
"unique_together": {("issue", "subscriber")},
},
),
]
@@ -0,0 +1,35 @@
# Generated by Django 4.2.3 on 2023-07-20 09:35
from django.db import migrations, models
def restructure_theming(apps, schema_editor):
Model = apps.get_model("db", "User")
updated_user = []
for obj in Model.objects.exclude(theme={}).all():
current_theme = obj.theme
updated_theme = {
"primary": current_theme.get("accent", ""),
"background": current_theme.get("bgBase", ""),
"sidebarBackground": current_theme.get("sidebar", ""),
"text": current_theme.get("textBase", ""),
"sidebarText": current_theme.get("textBase", ""),
"palette": f"""{current_theme.get("bgBase","")},{current_theme.get("textBase", "")},{current_theme.get("accent", "")},{current_theme.get("sidebar","")},{current_theme.get("textBase", "")}""",
"darkPalette": current_theme.get("darkPalette", "")
}
obj.theme = updated_theme
updated_user.append(obj)
Model.objects.bulk_update(
updated_user, ["theme"], batch_size=100
)
class Migration(migrations.Migration):
dependencies = [
("db", "0037_issue_archived_at_project_archive_in_and_more"),
]
operations = [
migrations.RunPython(restructure_theming)
]
+1 -1
View File
@@ -17,7 +17,7 @@ EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql_psycopg2",
"ENGINE": "django.db.backends.postgresql",
"NAME": os.environ.get("PGUSER", "plane"),
"USER": "",
"PASSWORD": "",
+1 -1
View File
@@ -17,7 +17,7 @@ DEBUG = int(os.environ.get("DEBUG", 0)) == 1
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql_psycopg2",
"ENGINE": "django.db.backends.postgresql",
"NAME": "plane",
"USER": os.environ.get("PGUSER", ""),
"PASSWORD": os.environ.get("PGPASSWORD", ""),
+1 -1
View File
@@ -16,7 +16,7 @@ from .common import * # noqa
DEBUG = int(os.environ.get("DEBUG", 1)) == 1
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql_psycopg2",
"ENGINE": "django.db.backends.postgresql",
"NAME": os.environ.get("PGUSER", "plane"),
"USER": "",
"PASSWORD": "",
+2
View File
@@ -30,3 +30,5 @@ openai==0.27.8
slack-sdk==3.21.3
celery==5.3.1
django_celery_beat==2.5.0
psycopg-binary==3.1.9
psycopg-c==3.1.9
+1 -1
View File
@@ -7,5 +7,5 @@ django-storages==1.13.2
boto3==1.27.0
django-anymail==10.0
django-debug-toolbar==4.1.0
gevent==22.10.2
gevent==23.7.0
psycogreen==1.0.2
@@ -120,7 +120,7 @@ export const EmailCodeForm = ({ handleSignIn }: any) => {
Please check your inbox at <span className="font-medium">{watch("email")}</span>
</p>
)}
<form className="space-y-4 mt-10 w-full sm:w-[360px] mx-auto">
<form className="space-y-4 mt-10 sm:w-[360px] mx-auto">
<div className="space-y-1">
<Input
id="email"
@@ -136,7 +136,7 @@ export const EmailCodeForm = ({ handleSignIn }: any) => {
}}
error={errors.email}
placeholder="Enter your email address..."
className="border-custom-border-300"
className="border-custom-border-300 h-[46px]"
/>
</div>
@@ -152,7 +152,7 @@ export const EmailCodeForm = ({ handleSignIn }: any) => {
}}
error={errors.token}
placeholder="Enter code..."
className="border-custom-border-300"
className="border-custom-border-300 h-[46px]"
/>
<button
type="button"
@@ -178,7 +178,7 @@ export const EmailCodeForm = ({ handleSignIn }: any) => {
) : errorResendingCode ? (
"Please try again later"
) : (
"Resend code"
<span className="font-medium">Resend code</span>
)}
</button>
</>
@@ -186,7 +186,7 @@ export const EmailCodeForm = ({ handleSignIn }: any) => {
{codeSent ? (
<PrimaryButton
type="submit"
className="w-full text-center"
className="w-full text-center h-[46px]"
size="md"
onClick={handleSubmit(handleSignin)}
disabled={!isValid && isDirty}
@@ -196,7 +196,7 @@ export const EmailCodeForm = ({ handleSignIn }: any) => {
</PrimaryButton>
) : (
<PrimaryButton
className="w-full text-center"
className="w-full text-center h-[46px]"
size="md"
onClick={() => {
handleSubmit(onSubmit)().then(() => {
@@ -71,7 +71,7 @@ export const EmailPasswordForm: React.FC<Props> = ({ onSubmit }) => {
}}
error={errors.email}
placeholder="Enter your email address..."
className="border-custom-border-300"
className="border-custom-border-300 h-[46px]"
/>
</div>
<div className="space-y-1">
@@ -85,7 +85,7 @@ export const EmailPasswordForm: React.FC<Props> = ({ onSubmit }) => {
}}
error={errors.password}
placeholder="Enter your password..."
className="border-custom-border-300"
className="border-custom-border-300 h-[46px]"
/>
</div>
<div className="text-right text-xs">
@@ -108,7 +108,7 @@ export const EmailPasswordForm: React.FC<Props> = ({ onSubmit }) => {
<div>
<PrimaryButton
type="submit"
className="w-full text-center"
className="w-full text-center h-[46px]"
disabled={!isValid && isDirty}
loading={isSubmitting}
>
@@ -78,17 +78,17 @@ export const EmailResetPasswordForm: React.FC<Props> = ({ setIsResettingPassword
}}
error={errors.email}
placeholder="Enter registered email address.."
className="border-custom-border-300"
className="border-custom-border-300 h-[46px]"
/>
</div>
<div className="mt-5 flex flex-col-reverse sm:flex-row items-center gap-2">
<SecondaryButton
className="w-full text-center"
className="w-full text-center h-[46px]"
onClick={() => setIsResettingPassword(false)}
>
Go Back
</SecondaryButton>
<PrimaryButton type="submit" className="w-full text-center" loading={isSubmitting}>
<PrimaryButton type="submit" className="w-full text-center h-[46px]" loading={isSubmitting}>
{isSubmitting ? "Sending link..." : "Send reset link"}
</PrimaryButton>
</div>
@@ -44,9 +44,9 @@ export const GithubLoginButton: FC<GithubLoginButtonProps> = ({ handleSignIn })
<Link
href={`https://github.com/login/oauth/authorize?client_id=${NEXT_PUBLIC_GITHUB_ID}&redirect_uri=${loginCallBackURL}&scope=read:user,user:email`}
>
<button className="flex w-full items-center justify-center gap-2 rounded border border-custom-border-300 p-2 text-sm font-medium text-custom-text-100 duration-300 hover:bg-custom-background-80">
<button className="flex w-full items-center justify-center gap-2 rounded border border-custom-border-300 p-2 text-sm font-medium text-custom-text-100 duration-300 hover:bg-custom-background-80 h-[46px]">
<Image
src={theme === "light" ? githubBlackImage : githubWhiteImage}
src={theme === "dark" ? githubWhiteImage : githubBlackImage}
height={20}
width={20}
alt="GitHub Logo"
+7 -5
View File
@@ -1,5 +1,5 @@
import { FC, CSSProperties, useEffect, useRef, useCallback, useState } from "react";
// next
import Script from "next/script";
export interface IGoogleLoginButton {
@@ -8,18 +8,18 @@ export interface IGoogleLoginButton {
styles?: CSSProperties;
}
export const GoogleLoginButton: FC<IGoogleLoginButton> = (props) => {
const { handleSignIn } = props;
export const GoogleLoginButton: FC<IGoogleLoginButton> = ({ handleSignIn }) => {
const googleSignInButton = useRef<HTMLDivElement>(null);
const [gsiScriptLoaded, setGsiScriptLoaded] = useState(false);
const loadScript = useCallback(() => {
if (!googleSignInButton.current || gsiScriptLoaded) return;
window?.google?.accounts.id.initialize({
client_id: process.env.NEXT_PUBLIC_GOOGLE_CLIENTID || "",
callback: handleSignIn,
});
window?.google?.accounts.id.renderButton(
googleSignInButton.current,
{
@@ -28,10 +28,12 @@ export const GoogleLoginButton: FC<IGoogleLoginButton> = (props) => {
size: "large",
logo_alignment: "center",
width: "360",
text: "continue_with",
text: "signin_with",
} as GsiButtonConfiguration // customization attributes
);
window?.google?.accounts.id.prompt(); // also display the One Tap dialog
setGsiScriptLoaded(true);
}, [handleSignIn, gsiScriptLoaded]);
@@ -9,54 +9,46 @@ type Props = {
defaultAnalytics: IDefaultAnalyticsResponse;
};
export const AnalyticsYearWiseIssues: React.FC<Props> = ({ defaultAnalytics }) => {
const currentMonth = new Date().getMonth();
const startMonth = Math.floor(currentMonth / 3) * 3 + 1;
const quarterMonthsList = [startMonth, startMonth + 1, startMonth + 2];
return (
<div className="py-3 border border-custom-border-200 rounded-[10px]">
<h1 className="px-3 text-base font-medium">Issues closed in a year</h1>
{defaultAnalytics.issue_completed_month_wise.length > 0 ? (
<LineGraph
data={[
{
id: "issues_closed",
color: "rgb(var(--color-primary-100))",
data: MONTHS_LIST.map((month) => ({
x: month.label.substring(0, 3),
y:
defaultAnalytics.issue_completed_month_wise.find(
(data) => data.month === month.value
)?.count || 0,
})),
},
]}
customYAxisTickValues={defaultAnalytics.issue_completed_month_wise.map((data) => {
if (quarterMonthsList.includes(data.month)) return data.count;
return 0;
})}
height="300px"
colors={(datum) => datum.color}
curve="monotoneX"
margin={{ top: 20 }}
enableSlices="x"
sliceTooltip={(datum) => (
<div className="rounded-md border border-custom-border-200 bg-custom-background-80 p-2 text-xs">
{datum.slice.points[0].data.yFormatted}
<span className="text-custom-text-200"> issues closed in </span>
{datum.slice.points[0].data.xFormatted}
</div>
)}
theme={{
background: "rgb(var(--color-background-100))",
}}
enableArea
/>
) : (
<div className="text-custom-text-200 text-center text-sm py-8">No matching data found.</div>
)}
</div>
);
};
export const AnalyticsYearWiseIssues: React.FC<Props> = ({ defaultAnalytics }) => (
<div className="py-3 border border-custom-border-200 rounded-[10px]">
<h1 className="px-3 text-base font-medium">Issues closed in a year</h1>
{defaultAnalytics.issue_completed_month_wise.length > 0 ? (
<LineGraph
data={[
{
id: "issues_closed",
color: "rgb(var(--color-primary-100))",
data: MONTHS_LIST.map((month) => ({
x: month.label.substring(0, 3),
y:
defaultAnalytics.issue_completed_month_wise.find(
(data) => data.month === month.value
)?.count || 0,
})),
},
]}
customYAxisTickValues={defaultAnalytics.issue_completed_month_wise.map(
(data) => data.count
)}
height="300px"
colors={(datum) => datum.color}
curve="monotoneX"
margin={{ top: 20 }}
enableSlices="x"
sliceTooltip={(datum) => (
<div className="rounded-md border border-custom-border-200 bg-custom-background-80 p-2 text-xs">
{datum.slice.points[0].data.yFormatted}
<span className="text-custom-text-200"> issues closed in </span>
{datum.slice.points[0].data.xFormatted}
</div>
)}
theme={{
background: "rgb(var(--color-background-100))",
}}
enableArea
/>
) : (
<div className="text-custom-text-200 text-center text-sm py-8">No matching data found.</div>
)}
</div>
);
@@ -380,7 +380,6 @@ export const CommandPalette: React.FC = () => {
user={user}
/>
)}
<CreateUpdateIssueModal
isOpen={isIssueModalOpen}
handleClose={() => setIsIssueModalOpen(false)}
@@ -356,7 +356,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
selfPositioned
/>
)}
{properties.due_date && (
{properties.due_date && issue.target_date && (
<ViewDueDateSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
@@ -364,7 +364,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
isNotAllowed={isNotAllowed}
/>
)}
{properties.labels && (
{properties.labels && issue.labels.length > 0 && (
<ViewLabelSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
@@ -382,7 +382,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
selfPositioned
/>
)}
{properties.estimate && (
{properties.estimate && issue.estimate_point !== null && (
<ViewEstimateSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
@@ -391,7 +391,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
selfPositioned
/>
)}
{properties.sub_issue_count && (
{properties.sub_issue_count && issue.sub_issues_count > 0 && (
<div className="flex cursor-default items-center rounded-md border border-custom-border-200 px-2.5 py-1 text-xs shadow-sm">
<Tooltip tooltipHeading="Sub-issue" tooltipContent={`${issue.sub_issues_count}`}>
<div className="flex items-center gap-1 text-custom-text-200">
@@ -401,7 +401,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
</Tooltip>
</div>
)}
{properties.link && (
{properties.link && issue.link_count > 0 && (
<div className="flex cursor-default items-center rounded-md border border-custom-border-200 px-2.5 py-1 text-xs shadow-sm">
<Tooltip tooltipHeading="Link" tooltipContent={`${issue.link_count}`}>
<div className="flex items-center gap-1 text-custom-text-200">
@@ -411,7 +411,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
</Tooltip>
</div>
)}
{properties.attachment_count && (
{properties.attachment_count && issue.attachment_count > 0 && (
<div className="flex cursor-default items-center rounded-md border border-custom-border-200 px-2.5 py-1 text-xs shadow-sm">
<Tooltip tooltipHeading="Attachment" tooltipContent={`${issue.attachment_count}`}>
<div className="flex items-center gap-1 text-custom-text-200">
@@ -231,7 +231,7 @@ export const SingleCalendarIssue: React.FC<Props> = ({
/>
)}
{properties.due_date && (
{properties.due_date && issue.target_date && (
<ViewDueDateSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
@@ -239,7 +239,7 @@ export const SingleCalendarIssue: React.FC<Props> = ({
isNotAllowed={isNotAllowed}
/>
)}
{properties.labels && (
{properties.labels && issue.labels.length > 0 && (
<ViewLabelSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
@@ -257,7 +257,7 @@ export const SingleCalendarIssue: React.FC<Props> = ({
isNotAllowed={isNotAllowed}
/>
)}
{properties.estimate && (
{properties.estimate && issue.estimate_point !== null && (
<ViewEstimateSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
@@ -266,7 +266,7 @@ export const SingleCalendarIssue: React.FC<Props> = ({
isNotAllowed={isNotAllowed}
/>
)}
{properties.sub_issue_count && (
{properties.sub_issue_count && issue.sub_issues_count > 0 && (
<div className="flex cursor-default items-center rounded-md border border-custom-border-200 px-2.5 py-1 text-xs shadow-sm">
<Tooltip tooltipHeading="Sub-issue" tooltipContent={`${issue.sub_issues_count}`}>
<div className="flex items-center gap-1 text-custom-text-200">
@@ -276,7 +276,7 @@ export const SingleCalendarIssue: React.FC<Props> = ({
</Tooltip>
</div>
)}
{properties.link && (
{properties.link && issue.link_count > 0 && (
<div className="flex cursor-default items-center rounded-md border border-custom-border-200 px-2.5 py-1 text-xs shadow-sm">
<Tooltip tooltipHeading="Links" tooltipContent={`${issue.link_count}`}>
<div className="flex items-center gap-1 text-custom-text-200">
@@ -286,7 +286,7 @@ export const SingleCalendarIssue: React.FC<Props> = ({
</Tooltip>
</div>
)}
{properties.attachment_count && (
{properties.attachment_count && issue.attachment_count > 0 && (
<div className="flex cursor-default items-center rounded-md border border-custom-border-200 px-2.5 py-1 text-xs shadow-sm">
<Tooltip tooltipHeading="Attachments" tooltipContent={`${issue.attachment_count}`}>
<div className="flex items-center gap-1 text-custom-text-200">
@@ -11,14 +11,16 @@ import useEstimateOption from "hooks/use-estimate-option";
// components
import { SelectFilters } from "components/views";
// ui
import { CustomMenu, Icon, ToggleSwitch, Tooltip } from "components/ui";
import { CustomMenu, ToggleSwitch, Tooltip } from "components/ui";
// icons
import { ChevronDownIcon } from "@heroicons/react/24/outline";
import {
ChevronDownIcon,
ListBulletIcon,
Squares2X2Icon,
CalendarDaysIcon,
} from "@heroicons/react/24/outline";
CalendarMonthOutlined,
FormatListBulletedOutlined,
GridViewOutlined,
TableChartOutlined,
WaterfallChartOutlined,
} from "@mui/icons-material";
// helpers
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
import { checkIfArraysHaveSameElements } from "helpers/array.helper";
@@ -27,26 +29,26 @@ import { Properties, TIssueViewOptions } from "types";
// constants
import { GROUP_BY_OPTIONS, ORDER_BY_OPTIONS, FILTER_ISSUE_OPTIONS } from "constants/issue";
const issueViewOptions: { type: TIssueViewOptions; icon: any }[] = [
const issueViewOptions: { type: TIssueViewOptions; Icon: any }[] = [
{
type: "list",
icon: <ListBulletIcon className="h-4 w-4" />,
Icon: FormatListBulletedOutlined,
},
{
type: "kanban",
icon: <Squares2X2Icon className="h-4 w-4" />,
Icon: GridViewOutlined,
},
{
type: "calendar",
icon: <CalendarDaysIcon className="h-4 w-4" />,
Icon: CalendarMonthOutlined,
},
{
type: "spreadsheet",
icon: <Icon iconName="table_chart" />,
Icon: TableChartOutlined,
},
{
type: "gantt_chart",
icon: <Icon iconName="waterfall_chart" className="rotate-90" />,
Icon: WaterfallChartOutlined,
},
];
@@ -98,7 +100,12 @@ export const IssuesFilterView: React.FC = () => {
}`}
onClick={() => setIssueView(option.type)}
>
{option.icon}
<option.Icon
sx={{
fontSize: 16,
}}
className={option.type === "gantt_chart" ? "rotate-90" : ""}
/>
</button>
</Tooltip>
))}
@@ -177,7 +184,6 @@ export const IssuesFilterView: React.FC = () => {
GROUP_BY_OPTIONS.find((option) => option.key === groupByProperty)
?.name ?? "Select"
}
width="lg"
>
{GROUP_BY_OPTIONS.map((option) =>
issueView === "kanban" && option.key === null ? null : (
@@ -198,7 +204,6 @@ export const IssuesFilterView: React.FC = () => {
ORDER_BY_OPTIONS.find((option) => option.key === orderBy)?.name ??
"Select"
}
width="lg"
>
{ORDER_BY_OPTIONS.map((option) =>
groupByProperty === "priority" && option.key === "priority" ? null : (
@@ -223,7 +228,6 @@ export const IssuesFilterView: React.FC = () => {
FILTER_ISSUE_OPTIONS.find((option) => option.key === filters.type)
?.name ?? "Select"
}
width="lg"
>
{FILTER_ISSUE_OPTIONS.map((option) => (
<CustomMenu.MenuItem
+19 -2
View File
@@ -31,11 +31,12 @@ import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
import { CreateUpdateViewModal } from "components/views";
import { TransferIssues, TransferIssuesModal } from "components/cycles";
// ui
import { EmptyState, PrimaryButton, Spinner } from "components/ui";
import { EmptyState, PrimaryButton, Spinner, Icon } from "components/ui";
// icons
import { PlusIcon, TrashIcon } from "@heroicons/react/24/outline";
// images
import emptyIssue from "public/empty-state/issue.svg";
import emptyIssueArchive from "public/empty-state/issue-archive.svg";
// helpers
import { getStatesList } from "helpers/state.helper";
import { orderArrayBy } from "helpers/array.helper";
@@ -577,9 +578,25 @@ export const IssuesView: React.FC<Props> = ({
issueView === "gantt_chart" && <GanttChartView />
)}
</>
) : router.pathname.includes("archived-issues") ? (
<EmptyState
title="Archived Issues will be shown here"
description="All the issues that have been in the completed or canceled groups for the configured period of time can be viewed here."
image={emptyIssueArchive}
buttonText="Go to Automation Settings"
onClick={() => {
router.push(`/${workspaceSlug}/projects/${projectId}/settings/automations`);
}}
/>
) : (
<EmptyState
title="Project issues will appear here"
title={
cycleId
? "Cycle issues will appear here"
: moduleId
? "Module issues will appear here"
: "Project issues will appear here"
}
description="Issues help you track individual pieces of work. With Issues, keep track of what's going on, who is working on it, and what's done."
image={emptyIssue}
buttonText="New Issue"
@@ -271,7 +271,7 @@ export const SingleListIssue: React.FC<Props> = ({
isNotAllowed={isNotAllowed}
/>
)}
{properties.due_date && (
{properties.due_date && issue.target_date && (
<ViewDueDateSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
@@ -279,7 +279,7 @@ export const SingleListIssue: React.FC<Props> = ({
isNotAllowed={isNotAllowed}
/>
)}
{properties.labels && (
{properties.labels && issue.labels.length > 0 && (
<ViewLabelSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
@@ -297,7 +297,7 @@ export const SingleListIssue: React.FC<Props> = ({
isNotAllowed={isNotAllowed}
/>
)}
{properties.estimate && (
{properties.estimate && issue.estimate_point !== null && (
<ViewEstimateSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
@@ -306,7 +306,7 @@ export const SingleListIssue: React.FC<Props> = ({
isNotAllowed={isNotAllowed}
/>
)}
{properties.sub_issue_count && (
{properties.sub_issue_count && issue.sub_issues_count > 0 && (
<div className="flex cursor-default items-center rounded-md border border-custom-border-200 px-2.5 py-1 text-xs shadow-sm">
<Tooltip tooltipHeading="Sub-issue" tooltipContent={`${issue.sub_issues_count}`}>
<div className="flex items-center gap-1 text-custom-text-200">
@@ -316,7 +316,7 @@ export const SingleListIssue: React.FC<Props> = ({
</Tooltip>
</div>
)}
{properties.link && (
{properties.link && issue.link_count > 0 && (
<div className="flex cursor-default items-center rounded-md border border-custom-border-200 px-2.5 py-1 text-xs shadow-sm">
<Tooltip tooltipHeading="Links" tooltipContent={`${issue.link_count}`}>
<div className="flex items-center gap-1 text-custom-text-200">
@@ -326,7 +326,7 @@ export const SingleListIssue: React.FC<Props> = ({
</Tooltip>
</div>
)}
{properties.attachment_count && (
{properties.attachment_count && issue.attachment_count > 0 && (
<div className="flex cursor-default items-center rounded-md border border-custom-border-200 px-2.5 py-1 text-xs shadow-sm">
<Tooltip tooltipHeading="Attachments" tooltipContent={`${issue.attachment_count}`}>
<div className="flex items-center gap-1 text-custom-text-200">
@@ -21,11 +21,11 @@ type Props = {
};
const defaultValues: ICustomTheme = {
background: "#292d3e",
text: "#ffffff",
primary: "#7d57c1",
sidebarBackground: "#292d3e",
sidebarText: "#ffffff",
background: "#0d101b",
text: "#c5c5c5",
primary: "#3f76ff",
sidebarBackground: "#0d101b",
sidebarText: "#c5c5c5",
darkPalette: false,
palette: "",
};
@@ -69,8 +69,8 @@ export const CustomThemeSelector: React.FC<Props> = ({ preLoadedData }) => {
return { ...prevData, ...res };
}, false);
applyTheme(payload.palette, darkPalette);
setTheme("custom");
applyTheme(payload.palette, darkPalette);
})
.catch((err) => console.log(err));
};
@@ -71,7 +71,21 @@ export const ThemeSwitch: React.FC<Props> = ({
}
onChange={({ value, type }: { value: string; type: string }) => {
if (value === "custom") {
if (user?.theme.palette) setPreLoadedData(user.theme);
if (user?.theme.palette) {
setPreLoadedData({
background: user.theme.background !== "" ? user.theme.background : "#0d101b",
text: user.theme.text !== "" ? user.theme.text : "#c5c5c5",
primary: user.theme.primary !== "" ? user.theme.primary : "#3f76ff",
sidebarBackground:
user.theme.sidebarBackground !== "" ? user.theme.sidebarBackground : "#0d101b",
sidebarText: user.theme.sidebarText !== "" ? user.theme.sidebarText : "#c5c5c5",
darkPalette: false,
palette:
user.theme.palette !== ",,,,"
? user.theme.palette
: "#0d101b,#c5c5c5,#3f76ff,#0d101b,#c5c5c5",
});
}
if (!customThemeSelectorOptions) setCustomThemeSelectorOptions(true);
} else {
@@ -80,11 +94,14 @@ export const ThemeSwitch: React.FC<Props> = ({
for (let i = 10; i <= 900; i >= 100 ? (i += 100) : (i += 10)) {
document.documentElement.style.removeProperty(`--color-background-${i}`);
document.documentElement.style.removeProperty(`--color-text-${i}`);
document.documentElement.style.removeProperty(`--color-border-${i}`);
document.documentElement.style.removeProperty(`--color-primary-${i}`);
document.documentElement.style.removeProperty(`--color-sidebar-background-${i}`);
document.documentElement.style.removeProperty(`--color-sidebar-text-${i}`);
document.documentElement.style.removeProperty(`--color-sidebar-border-${i}`);
}
}
setTheme(value);
document.documentElement.style.setProperty("color-scheme", type);
}}
@@ -0,0 +1,19 @@
import React from "react";
import type { Props } from "./types";
export const ArchiveIcon: React.FC<Props> = ({ width = "24", height = "24", className, color }) => (
<svg
width={width}
height={height}
className={className}
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M5.75 19.5C5.41667 19.5 5.125 19.375 4.875 19.125C4.625 18.875 4.5 18.5833 4.5 18.25V7.35417C4.5 7.14583 4.52083 6.96875 4.5625 6.82292C4.60417 6.67708 4.68056 6.54167 4.79167 6.41667L5.95833 4.83333C6.06944 4.70833 6.19792 4.62153 6.34375 4.57292C6.48958 4.52431 6.6624 4.5 6.86221 4.5H17.1378C17.3376 4.5 17.5069 4.52431 17.6458 4.57292C17.7847 4.62153 17.9097 4.70833 18.0208 4.83333L19.2083 6.41667C19.3194 6.54167 19.3958 6.67708 19.4375 6.82292C19.4792 6.96875 19.5 7.14583 19.5 7.35417V18.25C19.5 18.5833 19.375 18.875 19.125 19.125C18.875 19.375 18.5833 19.5 18.25 19.5H5.75ZM6.10417 6.70833H17.875L17.1165 5.75H6.85417L6.10417 6.70833ZM5.75 7.95833V18.25H18.25V7.95833H5.75ZM12 16.375L15.25 13.125L14.4167 12.2917L12.625 14.0833V9.89583H11.375V14.0833L9.58333 12.2917L8.75 13.125L12 16.375Z"
fill={color ? color : "currentColor"}
/>
</svg>
);
+24
View File
@@ -0,0 +1,24 @@
import React from "react";
import type { Props } from "./types";
export const BellNotificationIcon: React.FC<Props> = ({
width = "24",
height = "24",
color = "rgb(var(--color-text-200))",
className,
}) => (
<svg
width={width}
height={height}
className={className}
viewBox="0 0 22 22"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M4.35425 17.4154C4.15946 17.4154 3.99618 17.3491 3.8644 17.2166C3.73263 17.084 3.66675 16.9198 3.66675 16.7239C3.66675 16.5279 3.73263 16.365 3.8644 16.2352C3.99618 16.1053 4.15946 16.0404 4.35425 16.0404H5.59175V9.02786C5.59175 7.77509 5.96987 6.64071 6.72612 5.62474C7.48237 4.60877 8.47925 3.97092 9.71675 3.7112V3.04661C9.71675 2.69523 9.84099 2.40495 10.0895 2.17578C10.338 1.94661 10.6397 1.83203 10.9947 1.83203C11.3497 1.83203 11.6532 1.94661 11.9053 2.17578C12.1574 2.40495 12.2834 2.69523 12.2834 3.04661V3.7112C13.5209 3.97092 14.5216 4.60877 15.2855 5.62474C16.0494 6.64071 16.4313 7.77509 16.4313 9.02786V16.0404H17.6459C17.8407 16.0404 18.004 16.1066 18.1358 16.2392C18.2675 16.3717 18.3334 16.536 18.3334 16.7319C18.3334 16.9278 18.2675 17.0907 18.1358 17.2206C18.004 17.3504 17.8407 17.4154 17.6459 17.4154H4.35425ZM11.0001 20.1654C10.5112 20.1654 10.0834 19.9859 9.71675 19.6268C9.35008 19.2678 9.16675 18.8362 9.16675 18.332H12.8334C12.8334 18.8362 12.6539 19.2678 12.2949 19.6268C11.9358 19.9859 11.5042 20.1654 11.0001 20.1654ZM6.96675 16.0404H15.0563V9.02786C15.0563 7.88203 14.6706 6.91571 13.899 6.12891C13.1275 5.3421 12.1727 4.9487 11.0345 4.9487C9.89626 4.9487 8.93376 5.3421 8.14696 6.12891C7.36015 6.91571 6.96675 7.88203 6.96675 9.02786V16.0404Z"
fill={color}
/>
</svg>
);
+19
View File
@@ -0,0 +1,19 @@
import React from "react";
import type { Props } from "./types";
export const ClockIcon: React.FC<Props> = ({ width = "24", height = "24", className, color }) => (
<svg
width={width}
height={height}
className={className}
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12.6876 11.7513V8.1888C12.6876 8.00825 12.6286 7.85894 12.5105 7.74088C12.3924 7.62283 12.2431 7.5638 12.0626 7.5638C11.882 7.5638 11.7327 7.62283 11.6147 7.74088C11.4966 7.85894 11.4376 8.00825 11.4376 8.1888V12.0013C11.4376 12.0846 11.4515 12.161 11.4792 12.2305C11.507 12.2999 11.5487 12.3694 11.6042 12.4388L14.6042 15.543C14.7292 15.6819 14.8855 15.7478 15.073 15.7409C15.2605 15.7339 15.4167 15.668 15.5417 15.543C15.6667 15.418 15.7292 15.2652 15.7292 15.0846C15.7292 14.9041 15.6667 14.7513 15.5417 14.6263L12.6876 11.7513ZM12.0001 20.3346C10.8612 20.3346 9.7848 20.1159 8.77091 19.6784C7.75703 19.2409 6.87161 18.6437 6.11466 17.8867C5.35772 17.1298 4.7605 16.2444 4.323 15.2305C3.8855 14.2166 3.66675 13.1402 3.66675 12.0013C3.66675 10.8624 3.8855 9.78602 4.323 8.77213C4.7605 7.75825 5.35772 6.87283 6.11466 6.11589C6.87161 5.35894 7.75703 4.76172 8.77091 4.32422C9.7848 3.88672 10.8612 3.66797 12.0001 3.66797C13.139 3.66797 14.2154 3.88672 15.2292 4.32422C16.2431 4.76172 17.1286 5.35894 17.8855 6.11589C18.6424 6.87283 19.2397 7.75825 19.6772 8.77213C20.1147 9.78602 20.3334 10.8624 20.3334 12.0013C20.3334 13.1402 20.1147 14.2166 19.6772 15.2305C19.2397 16.2444 18.6424 17.1298 17.8855 17.8867C17.1286 18.6437 16.2431 19.2409 15.2292 19.6784C14.2154 20.1159 13.139 20.3346 12.0001 20.3346ZM12.0001 19.0846C13.9445 19.0846 15.6112 18.3902 17.0001 17.0013C18.389 15.6124 19.0834 13.9457 19.0834 12.0013C19.0834 10.0569 18.389 8.39019 17.0001 7.0013C15.6112 5.61241 13.9445 4.91797 12.0001 4.91797C10.0556 4.91797 8.38897 5.61241 7.00008 7.0013C5.61119 8.39019 4.91675 10.0569 4.91675 12.0013C4.91675 13.9457 5.61119 15.6124 7.00008 17.0013C8.38897 18.3902 10.0556 19.0846 12.0001 19.0846Z"
fill={color ? color : "currentColor"}
/>
</svg>
);
+6
View File
@@ -82,3 +82,9 @@ export * from "./command-icon";
export * from "./color-picker-icon";
export * from "./inbox-icon";
export * from "./stacked-layers-horizontal-icon";
export * from "./sort-icon";
export * from "./x-mark-icon";
export * from "./archive-icon";
export * from "./clock-icon";
export * from "./bell-icon";
export * from "./single-comment-icon";
@@ -0,0 +1,24 @@
import React from "react";
import type { Props } from "./types";
export const SingleCommentCard: React.FC<Props> = ({
width = "24",
height = "24",
className,
color,
}) => (
<svg
width={width}
height={height}
className={className}
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M3.66663 20.3346V4.91797C3.66663 4.58464 3.79163 4.29297 4.04163 4.04297C4.29163 3.79297 4.58329 3.66797 4.91663 3.66797H19.0833C19.4166 3.66797 19.7083 3.79297 19.9583 4.04297C20.2083 4.29297 20.3333 4.58464 20.3333 4.91797V15.7513C20.3333 16.0846 20.2083 16.3763 19.9583 16.6263C19.7083 16.8763 19.4166 17.0013 19.0833 17.0013H6.99996L3.66663 20.3346ZM6.45829 15.7513H19.0833V4.91797H4.91663V17.418L6.45829 15.7513Z"
fill={color ? color : "currentColor"}
/>
</svg>
);
+19
View File
@@ -0,0 +1,19 @@
import React from "react";
import type { Props } from "./types";
export const SortIcon: React.FC<Props> = ({ width = "24", height = "24", className, color }) => (
<svg
width={width}
height={height}
className={className}
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10.9583 17C10.7778 17 10.6285 16.941 10.5104 16.8229C10.3924 16.7049 10.3333 16.5556 10.3333 16.375C10.3333 16.1944 10.3924 16.0451 10.5104 15.9271C10.6285 15.809 10.7778 15.75 10.9583 15.75H13.0417C13.2222 15.75 13.3715 15.809 13.4896 15.9271C13.6076 16.0451 13.6667 16.1944 13.6667 16.375C13.6667 16.5556 13.6076 16.7049 13.4896 16.8229C13.3715 16.941 13.2222 17 13.0417 17H10.9583ZM5.125 8.25C4.94444 8.25 4.79514 8.19097 4.67708 8.07292C4.55903 7.95486 4.5 7.80556 4.5 7.625C4.5 7.44444 4.55903 7.29514 4.67708 7.17708C4.79514 7.05903 4.94444 7 5.125 7H18.875C19.0556 7 19.2049 7.05903 19.3229 7.17708C19.441 7.29514 19.5 7.44444 19.5 7.625C19.5 7.80556 19.441 7.95486 19.3229 8.07292C19.2049 8.19097 19.0556 8.25 18.875 8.25H5.125ZM7.625 12.625C7.44444 12.625 7.29514 12.566 7.17708 12.4479C7.05903 12.3299 7 12.1806 7 12C7 11.8194 7.05903 11.6701 7.17708 11.5521C7.29514 11.434 7.44444 11.375 7.625 11.375H16.375C16.5556 11.375 16.7049 11.434 16.8229 11.5521C16.941 11.6701 17 11.8194 17 12C17 12.1806 16.941 12.3299 16.8229 12.4479C16.7049 12.566 16.5556 12.625 16.375 12.625H7.625Z"
fill={color ? color : "currentColor"}
/>
</svg>
);
+19
View File
@@ -0,0 +1,19 @@
import React from "react";
import type { Props } from "./types";
export const XMarkIcon: React.FC<Props> = ({ width = "24", height = "24", className, color }) => (
<svg
width={width}
height={height}
className={className}
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 12.875L7.625 17.25C7.5 17.375 7.35417 17.4375 7.1875 17.4375C7.02083 17.4375 6.875 17.375 6.75 17.25C6.625 17.125 6.5625 16.9792 6.5625 16.8125C6.5625 16.6458 6.625 16.5 6.75 16.375L11.125 12L6.75 7.625C6.625 7.5 6.5625 7.35417 6.5625 7.1875C6.5625 7.02083 6.625 6.875 6.75 6.75C6.875 6.625 7.02083 6.5625 7.1875 6.5625C7.35417 6.5625 7.5 6.625 7.625 6.75L12 11.125L16.375 6.75C16.5 6.625 16.6458 6.5625 16.8125 6.5625C16.9792 6.5625 17.125 6.625 17.25 6.75C17.375 6.875 17.4375 7.02083 17.4375 7.1875C17.4375 7.35417 17.375 7.5 17.25 7.625L12.875 12L17.25 16.375C17.375 16.5 17.4375 16.6458 17.4375 16.8125C17.4375 16.9792 17.375 17.125 17.25 17.25C17.125 17.375 16.9792 17.4375 16.8125 17.4375C16.6458 17.4375 16.5 17.375 16.375 17.25L12 12.875Z"
fill={color ? color : "currentColor"}
/>
</svg>
);
+32 -56
View File
@@ -1,6 +1,5 @@
import React, { ChangeEvent, FC, useState, useEffect, useRef } from "react";
import React, { FC, useState, useEffect, useRef } from "react";
import Link from "next/link";
import dynamic from "next/dynamic";
import { useRouter } from "next/router";
@@ -12,12 +11,12 @@ import aiService from "services/ai.service";
import useToast from "hooks/use-toast";
// components
import { GptAssistantModal } from "components/core";
import { ParentIssuesListModal } from "components/issues";
import {
IssueAssigneeSelect,
IssueDateSelect,
IssueEstimateSelect,
IssueLabelSelect,
IssueParentSelect,
IssuePrioritySelect,
IssueProjectSelect,
IssueStateSelect,
@@ -35,10 +34,8 @@ import {
} from "components/ui";
// icons
import { SparklesIcon, XMarkIcon } from "@heroicons/react/24/outline";
// helpers
import { cosineSimilarity } from "helpers/string.helper";
// types
import type { ICurrentUserResponse, IIssue } from "types";
import type { ICurrentUserResponse, IIssue, ISearchIssueResponse } from "types";
// rich-text-editor
const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor"), {
ssr: false,
@@ -72,6 +69,7 @@ const defaultValues: Partial<IIssue> = {
description_html: "<p></p>",
estimate_point: null,
state: "",
parent: null,
priority: null,
assignees: [],
assignees_list: [],
@@ -82,7 +80,6 @@ const defaultValues: Partial<IIssue> = {
export interface IssueFormProps {
handleFormSubmit: (values: Partial<IIssue>) => Promise<void>;
initialData?: Partial<IIssue>;
issues: IIssue[];
projectId: string;
setActiveProject: React.Dispatch<React.SetStateAction<string | null>>;
createMore: boolean;
@@ -108,7 +105,6 @@ export interface IssueFormProps {
export const IssueForm: FC<IssueFormProps> = ({
handleFormSubmit,
initialData,
issues = [],
projectId,
setActiveProject,
createMore,
@@ -118,11 +114,10 @@ export const IssueForm: FC<IssueFormProps> = ({
user,
fieldsToShow,
}) => {
// states
const [mostSimilarIssue, setMostSimilarIssue] = useState<IIssue | undefined>();
const [stateModal, setStateModal] = useState(false);
const [labelModal, setLabelModal] = useState(false);
const [parentIssueListModalOpen, setParentIssueListModalOpen] = useState(false);
const [selectedParentIssue, setSelectedParentIssue] = useState<ISearchIssueResponse | null>(null);
const [gptAssistantModal, setGptAssistantModal] = useState(false);
const [iAmFeelingLucky, setIAmFeelingLucky] = useState(false);
@@ -151,12 +146,6 @@ export const IssueForm: FC<IssueFormProps> = ({
const issueName = watch("name");
const handleTitleChange = (e: ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
const similarIssue = issues?.find((i: IIssue) => cosineSimilarity(i.name, value) > 0.7);
setMostSimilarIssue(similarIssue);
};
const handleCreateUpdateIssue = async (formData: Partial<IIssue>) => {
await handleFormSubmit(formData);
@@ -254,6 +243,10 @@ export const IssueForm: FC<IssueFormProps> = ({
handleClose={() => setLabelModal(false)}
projectId={projectId}
user={user}
onSuccess={(response) => {
setValue("labels", [...watch("labels"), response.id]);
setValue("labels_list", [...watch("labels_list"), response.id]);
}}
/>
</>
)}
@@ -279,26 +272,28 @@ export const IssueForm: FC<IssueFormProps> = ({
</div>
{watch("parent") &&
watch("parent") !== "" &&
(fieldsToShow.includes("all") || fieldsToShow.includes("parent")) && (
(fieldsToShow.includes("all") || fieldsToShow.includes("parent")) &&
selectedParentIssue && (
<div className="flex w-min items-center gap-2 whitespace-nowrap rounded bg-custom-background-80 p-2 text-xs">
<div className="flex items-center gap-2">
<span
className="block h-1.5 w-1.5 rounded-full"
style={{
backgroundColor: issues.find((i) => i.id === watch("parent"))?.state_detail
.color,
backgroundColor: selectedParentIssue.state__color,
}}
/>
<span className="flex-shrink-0 text-custom-text-200">
{/* {projects?.find((p) => p.id === projectId)?.identifier}- */}
{issues.find((i) => i.id === watch("parent"))?.sequence_id}
{selectedParentIssue.project__identifier}-{selectedParentIssue.sequence_id}
</span>
<span className="truncate font-medium">
{issues.find((i) => i.id === watch("parent"))?.name.substring(0, 50)}
{selectedParentIssue.name.substring(0, 50)}
</span>
<XMarkIcon
className="h-3 w-3 cursor-pointer"
onClick={() => setValue("parent", null)}
onClick={() => {
setValue("parent", null);
setSelectedParentIssue(null);
}}
/>
</div>
</div>
@@ -310,7 +305,6 @@ export const IssueForm: FC<IssueFormProps> = ({
<Input
id="name"
name="name"
onChange={handleTitleChange}
className="resize-none text-xl"
placeholder="Title"
autoComplete="off"
@@ -324,38 +318,11 @@ export const IssueForm: FC<IssueFormProps> = ({
},
}}
/>
{mostSimilarIssue && (
<div className="flex items-center gap-x-2">
<p className="text-sm text-custom-text-200">
<Link
href={`/${workspaceSlug}/projects/${projectId}/issues/${mostSimilarIssue.id}`}
>
<a target="_blank" type="button" className="inline text-left">
<span>Did you mean </span>
<span className="italic">
{mostSimilarIssue.project_detail.identifier}-
{mostSimilarIssue.sequence_id}: {mostSimilarIssue.name}{" "}
</span>
?
</a>
</Link>
</p>
<button
type="button"
className="text-sm text-custom-primary"
onClick={() => {
setMostSimilarIssue(undefined);
}}
>
No
</button>
</div>
)}
</div>
)}
{(fieldsToShow.includes("all") || fieldsToShow.includes("description")) && (
<div className="relative">
<div className="-mb-2 flex justify-end">
<div className="flex justify-end">
{issueName && issueName !== "" && (
<button
type="button"
@@ -491,11 +458,20 @@ export const IssueForm: FC<IssueFormProps> = ({
</div>
)}
{(fieldsToShow.includes("all") || fieldsToShow.includes("parent")) && (
<IssueParentSelect
<Controller
control={control}
isOpen={parentIssueListModalOpen}
setIsOpen={setParentIssueListModalOpen}
issues={issues ?? []}
name="parent"
render={({ field: { onChange } }) => (
<ParentIssuesListModal
isOpen={parentIssueListModalOpen}
handleClose={() => setParentIssueListModalOpen(false)}
onChange={(issue) => {
onChange(issue.id);
setSelectedParentIssue(issue);
}}
projectId={projectId}
/>
)}
/>
)}
{(fieldsToShow.includes("all") || fieldsToShow.includes("parent")) && (
+29 -17
View File
@@ -58,7 +58,7 @@ export const IssueMainContent: React.FC<Props> = ({
<>
<div className="rounded-lg">
{issueDetails?.parent && issueDetails.parent !== "" ? (
<div className="mb-5 flex w-min items-center gap-2 whitespace-nowrap rounded bg-custom-background-80 p-2 text-xs">
<div className="mb-5 flex w-min items-center gap-2 whitespace-nowrap rounded bg-custom-background-90 p-2 text-xs">
<Link href={`/${workspaceSlug}/projects/${projectId}/issues/${issueDetails.parent}`}>
<a className="flex items-center gap-2 text-custom-text-200">
<span
@@ -76,24 +76,36 @@ export const IssueMainContent: React.FC<Props> = ({
</a>
</Link>
<CustomMenu ellipsis position="left">
{siblingIssues && siblingIssues.length > 0 ? (
siblingIssues.map((issue: IIssue) => (
<CustomMenu.MenuItem key={issue.id}>
<Link
href={`/${workspaceSlug}/projects/${projectId as string}/issues/${issue.id}`}
>
<a>
{issueDetails.project_detail.identifier}-{issue.sequence_id}
</a>
</Link>
</CustomMenu.MenuItem>
))
<CustomMenu position="left" ellipsis>
{siblingIssues && siblingIssues.sub_issues.length > 0 ? (
<>
<h2 className="text-custom-text-200 px-1 mb-2">Sibling issues</h2>
{siblingIssues.sub_issues.map((issue) => {
if (issue.id !== issueDetails.id)
return (
<CustomMenu.MenuItem
key={issue.id}
renderAs="a"
href={`/${workspaceSlug}/projects/${projectId as string}/issues/${
issue.id
}`}
>
{issueDetails.project_detail.identifier}-{issue.sequence_id}
</CustomMenu.MenuItem>
);
})}
</>
) : (
<CustomMenu.MenuItem className="flex items-center gap-2 whitespace-nowrap p-2 text-left text-xs text-custom-text-200">
No other sibling issues
</CustomMenu.MenuItem>
<p className="flex items-center gap-2 whitespace-nowrap px-1 text-left text-xs text-custom-text-200 py-1">
No sibling issues
</p>
)}
<CustomMenu.MenuItem
renderAs="button"
onClick={() => submitChanges({ parent: null })}
>
Remove parent issue
</CustomMenu.MenuItem>
</CustomMenu>
</div>
) : null}
+4 -12
View File
@@ -2,7 +2,7 @@ import React, { useEffect, useState } from "react";
import { useRouter } from "next/router";
import useSWR, { mutate } from "swr";
import { mutate } from "swr";
// headless ui
import { Dialog, Transition } from "@headlessui/react";
@@ -94,15 +94,6 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
assignees: [...(prePopulateData?.assignees ?? []), user?.id ?? ""],
};
const { data: issues } = useSWR(
workspaceSlug && activeProject
? PROJECT_ISSUES_LIST(workspaceSlug as string, activeProject ?? "")
: null,
workspaceSlug && activeProject
? () => issuesService.getIssues(workspaceSlug as string, activeProject ?? "")
: null
);
useEffect(() => {
if (projects && projects.length > 0)
setActiveProject(projects?.find((p) => p.id === projectId)?.id ?? projects?.[0].id ?? null);
@@ -271,7 +262,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
const updateIssue = async (payload: Partial<IIssue>) => {
await issuesService
.updateIssue(workspaceSlug as string, activeProject ?? "", data?.id ?? "", payload, user)
.patchIssue(workspaceSlug as string, activeProject ?? "", data?.id ?? "", payload, user)
.then((res) => {
if (isUpdatingSingleIssue) {
mutate<IIssue>(PROJECT_ISSUES_DETAILS, (prevData) => ({ ...prevData, ...res }), false);
@@ -317,6 +308,8 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
else await updateIssue(payload);
};
if (!projects || projects.length === 0) return null;
return (
<Transition.Root show={isOpen} as={React.Fragment}>
<Dialog as="div" className="relative z-20" onClose={() => handleClose()}>
@@ -345,7 +338,6 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
>
<Dialog.Panel className="relative transform rounded-lg border border-custom-border-200 bg-custom-background-100 p-5 text-left shadow-xl transition-all sm:w-full sm:max-w-2xl">
<IssueForm
issues={issues ?? []}
handleFormSubmit={handleFormSubmit}
initialData={data ?? prePopulateData}
createMore={createMore}
@@ -126,7 +126,7 @@ export const MyIssuesListItem: React.FC<Props> = ({ issue, properties, projectId
isNotAllowed={isNotAllowed}
/>
)}
{properties.due_date && (
{properties.due_date && issue.target_date && (
<ViewDueDateSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
@@ -134,7 +134,7 @@ export const MyIssuesListItem: React.FC<Props> = ({ issue, properties, projectId
isNotAllowed={isNotAllowed}
/>
)}
{properties.labels && issue.label_details.length > 0 ? (
{properties.labels && issue.labels.length > 0 && (
<div className="flex flex-wrap gap-1">
{issue.label_details.map((label) => (
<span
@@ -151,8 +151,6 @@ export const MyIssuesListItem: React.FC<Props> = ({ issue, properties, projectId
</span>
))}
</div>
) : (
""
)}
{properties.assignee && (
<div className="flex cursor-default items-center rounded-md border border-custom-border-200 px-2 py-1 text-xs shadow-sm">
@@ -175,7 +173,7 @@ export const MyIssuesListItem: React.FC<Props> = ({ issue, properties, projectId
</Tooltip>
</div>
)}
{properties.sub_issue_count && (
{properties.sub_issue_count && issue.sub_issues_count > 0 && (
<div className="flex cursor-default items-center rounded-md border border-custom-border-200 px-2.5 py-1 text-xs shadow-sm">
<Tooltip tooltipHeading="Sub-issue" tooltipContent={`${issue.sub_issues_count}`}>
<div className="flex items-center gap-1 text-custom-text-200">
@@ -185,7 +183,7 @@ export const MyIssuesListItem: React.FC<Props> = ({ issue, properties, projectId
</Tooltip>
</div>
)}
{properties.link && (
{properties.link && issue.link_count > 0 && (
<div className="flex cursor-default items-center rounded-md border border-custom-border-200 px-2 py-1 text-xs shadow-sm">
<Tooltip tooltipHeading="Link" tooltipContent={`${issue.link_count}`}>
<div className="flex items-center gap-1 text-custom-text-200">
@@ -195,7 +193,7 @@ export const MyIssuesListItem: React.FC<Props> = ({ issue, properties, projectId
</Tooltip>
</div>
)}
{properties.attachment_count && (
{properties.attachment_count && issue.attachment_count > 0 && (
<div className="flex cursor-default items-center rounded-md border border-custom-border-200 px-2 py-1 text-xs shadow-sm">
<Tooltip tooltipHeading="Attachment" tooltipContent={`${issue.attachment_count}`}>
<div className="flex items-center gap-1 text-custom-text-200">
@@ -21,7 +21,8 @@ type Props = {
isOpen: boolean;
handleClose: () => void;
value?: any;
onChange: (...event: any[]) => void;
onChange: (issue: ISearchIssueResponse) => void;
projectId: string;
issueId?: string;
customDisplay?: JSX.Element;
};
@@ -31,6 +32,7 @@ export const ParentIssuesListModal: React.FC<Props> = ({
handleClose: onClose,
value,
onChange,
projectId,
issueId,
customDisplay,
}) => {
@@ -42,7 +44,7 @@ export const ParentIssuesListModal: React.FC<Props> = ({
const debouncedSearchTerm: string = useDebounce(searchTerm, 500);
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { workspaceSlug } = router.query;
const handleClose = () => {
onClose();
@@ -109,7 +111,13 @@ export const ParentIssuesListModal: React.FC<Props> = ({
leaveTo="opacity-0 scale-95"
>
<Dialog.Panel className="relative mx-auto max-w-2xl transform rounded-xl border border-custom-border-200 bg-custom-background-100 shadow-2xl transition-all">
<Combobox value={value} onChange={onChange}>
<Combobox
value={value}
onChange={(val) => {
onChange(val);
handleClose();
}}
>
<div className="relative m-1">
<MagnifyingGlassIcon
className="pointer-events-none absolute top-3.5 left-4 h-5 w-5 text-custom-text-100 text-opacity-40"
@@ -165,13 +173,12 @@ export const ParentIssuesListModal: React.FC<Props> = ({
{issues.map((issue) => (
<Combobox.Option
key={issue.id}
value={issue.id}
value={issue}
className={({ active, selected }) =>
`flex cursor-pointer select-none items-center gap-2 rounded-md px-3 py-2 text-custom-text-200 ${
active ? "bg-custom-background-80 text-custom-text-100" : ""
} ${selected ? "text-custom-text-100" : ""}`
}
onClick={handleClose}
>
<>
<span
+1 -2
View File
@@ -1,8 +1,7 @@
export * from "./assignee";
export * from "./date";
export * from "./estimate"
export * from "./estimate";
export * from "./label";
export * from "./parent";
export * from "./priority";
export * from "./project";
export * from "./state";
@@ -1,27 +0,0 @@
import React from "react";
import { Controller, Control } from "react-hook-form";
// components
import { ParentIssuesListModal } from "components/issues";
// types
import type { IIssue } from "types";
type Props = {
control: Control<IIssue, any>;
isOpen: boolean;
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
issues: IIssue[];
};
export const IssueParentSelect: React.FC<Props> = ({ control, isOpen, setIsOpen, issues }) => (
<Controller
control={control}
name="parent"
render={({ field: { onChange } }) => (
<ParentIssuesListModal
isOpen={isOpen}
handleClose={() => setIsOpen(false)}
onChange={onChange}
/>
)}
/>
);
@@ -2,51 +2,31 @@ import React, { useState } from "react";
import { useRouter } from "next/router";
import useSWR from "swr";
import { Control, Controller, UseFormWatch } from "react-hook-form";
// fetch keys
// icons
import { UserIcon } from "@heroicons/react/24/outline";
// services
import issuesServices from "services/issues.service";
// components
import { ParentIssuesListModal } from "components/issues";
// icons
// types
import { IIssue, UserAuth } from "types";
// fetch-keys
import { PROJECT_ISSUES_LIST } from "constants/fetch-keys";
import { IIssue, ISearchIssueResponse, UserAuth } from "types";
type Props = {
control: Control<IIssue, any>;
submitChanges: (formData: Partial<IIssue>) => void;
customDisplay: JSX.Element;
watch: UseFormWatch<IIssue>;
onChange: (value: string) => void;
issueDetails: IIssue | undefined;
userAuth: UserAuth;
disabled?: boolean;
};
export const SidebarParentSelect: React.FC<Props> = ({
control,
submitChanges,
customDisplay,
watch,
onChange,
issueDetails,
userAuth,
disabled = false,
}) => {
const [isParentModalOpen, setIsParentModalOpen] = useState(false);
const [selectedParentIssue, setSelectedParentIssue] = useState<ISearchIssueResponse | null>(null);
const router = useRouter();
const { workspaceSlug, projectId, issueId } = router.query;
const { data: issues } = useSWR(
workspaceSlug && projectId
? PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string)
: null,
workspaceSlug && projectId
? () => issuesServices.getIssues(workspaceSlug as string, projectId as string)
: null
);
const { projectId, issueId } = router.query;
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disabled;
@@ -57,22 +37,15 @@ export const SidebarParentSelect: React.FC<Props> = ({
<p>Parent</p>
</div>
<div className="sm:basis-1/2">
<Controller
control={control}
name="parent"
render={({ field: { value, onChange } }) => (
<ParentIssuesListModal
isOpen={isParentModalOpen}
handleClose={() => setIsParentModalOpen(false)}
onChange={(val) => {
submitChanges({ parent: val });
onChange(val);
}}
issueId={issueId as string}
value={value}
customDisplay={customDisplay}
/>
)}
<ParentIssuesListModal
isOpen={isParentModalOpen}
handleClose={() => setIsParentModalOpen(false)}
onChange={(issue) => {
onChange(issue.id);
setSelectedParentIssue(issue);
}}
issueId={issueId as string}
projectId={projectId as string}
/>
<button
type="button"
@@ -82,10 +55,10 @@ export const SidebarParentSelect: React.FC<Props> = ({
onClick={() => setIsParentModalOpen(true)}
disabled={isNotAllowed}
>
{watch("parent") && watch("parent") !== "" ? (
`${issues?.find((i) => i.id === watch("parent"))?.project_detail?.identifier}-${
issues?.find((i) => i.id === watch("parent"))?.sequence_id
}`
{selectedParentIssue ? (
`${selectedParentIssue.project__identifier}-${selectedParentIssue.sequence_id}`
) : issueDetails?.parent ? (
`${issueDetails.project_detail.identifier}-${issueDetails.parent_detail?.sequence_id}`
) : (
<span className="text-custom-text-200">Select issue</span>
)}
+36 -30
View File
@@ -9,6 +9,7 @@ import { Controller, UseFormWatch } from "react-hook-form";
// hooks
import useToast from "hooks/use-toast";
import useUserAuth from "hooks/use-user-auth";
import useUserIssueNotificationSubscription from "hooks/use-issue-notification-subscription";
// services
import issuesService from "services/issues.service";
import modulesService from "services/modules.service";
@@ -30,15 +31,9 @@ import {
SidebarLabelSelect,
} from "components/issues";
// ui
import { CustomDatePicker } from "components/ui";
import { CustomDatePicker, Icon } from "components/ui";
// icons
import {
LinkIcon,
CalendarDaysIcon,
TrashIcon,
PlusIcon,
XMarkIcon,
} from "@heroicons/react/24/outline";
import { LinkIcon, CalendarDaysIcon, TrashIcon, PlusIcon } from "@heroicons/react/24/outline";
// helpers
import { copyTextToClipboard } from "helpers/string.helper";
// types
@@ -66,6 +61,7 @@ type Props = {
| "link"
| "delete"
| "all"
| "subscribe"
)[];
uneditable?: boolean;
};
@@ -86,6 +82,9 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
const { user } = useUserAuth();
const { loading, handleSubscribe, handleUnsubscribe, subscribed } =
useUserIssueNotificationSubscription(workspaceSlug, projectId, issueId);
const { memberRole } = useProjectMyMembership();
const { setToastAlert } = useToast();
@@ -232,6 +231,22 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
{issueDetail?.project_detail?.identifier}-{issueDetail?.sequence_id}
</h4>
<div className="flex flex-wrap items-center gap-2">
{issueDetail?.created_by !== user?.id &&
!issueDetail?.assignees.includes(user?.id ?? "") &&
!router.pathname.includes("[archivedIssueId]") &&
(fieldsToShow.includes("all") || fieldsToShow.includes("subscribe")) && (
<button
type="button"
className="rounded-md flex items-center gap-2 border border-custom-primary-100 px-2 py-1 text-xs text-custom-primary-100 shadow-sm duration-300 focus:outline-none"
onClick={() => {
if (subscribed) handleUnsubscribe();
else handleSubscribe();
}}
>
<Icon iconName="notifications" />
{loading ? "Loading..." : subscribed ? "Unsubscribe" : "Subscribe"}
</button>
)}
{(fieldsToShow.includes("all") || fieldsToShow.includes("link")) && (
<button
type="button"
@@ -318,29 +333,20 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
{showSecondSection && (
<div className="py-1">
{(fieldsToShow.includes("all") || fieldsToShow.includes("parent")) && (
<SidebarParentSelect
<Controller
control={control}
submitChanges={submitChanges}
customDisplay={
issueDetail?.parent_detail ? (
<button
type="button"
className="flex items-center gap-2 rounded bg-custom-background-80 px-3 py-2 text-xs"
onClick={() => submitChanges({ parent: null })}
>
<span className="text-custom-text-200">Selected:</span>{" "}
{issueDetail.parent_detail?.name}
<XMarkIcon className="h-3 w-3" />
</button>
) : (
<div className="inline-block rounded bg-custom-background-90 px-3 py-2 text-xs text-custom-text-200">
No parent selected
</div>
)
}
watch={watchIssue}
userAuth={memberRole}
disabled={uneditable}
name="parent"
render={({ field: { onChange } }) => (
<SidebarParentSelect
onChange={(val: string) => {
submitChanges({ parent: val });
onChange(val);
}}
issueDetails={issueDetail}
userAuth={memberRole}
disabled={uneditable}
/>
)}
/>
)}
{(fieldsToShow.includes("all") || fieldsToShow.includes("blocker")) && (
@@ -272,7 +272,7 @@ export const SubIssuesList: FC<Props> = ({ parentIssue, user, disabled = false }
key={issue.id}
href={`/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`}
>
<a className="group flex items-center justify-between gap-2 rounded p-2 hover:bg-custom-background-100">
<a className="group flex items-center justify-between gap-2 rounded p-2 hover:bg-custom-background-90">
<div className="flex items-center gap-2 rounded text-xs">
<span
className="block h-1.5 w-1.5 flex-shrink-0 rounded-full"
@@ -316,6 +316,7 @@ export const SubIssuesList: FC<Props> = ({ parentIssue, user, disabled = false }
Add sub-issue
</>
}
buttonClassName="whitespace-nowrap"
position="left"
noBorder
noChevron
@@ -26,6 +26,7 @@ type Props = {
isOpen: boolean;
projectId: string;
handleClose: () => void;
onSuccess?: (response: IIssueLabels) => void;
user: ICurrentUserResponse | undefined;
};
@@ -34,7 +35,13 @@ const defaultValues: Partial<IState> = {
color: "rgb(var(--color-text-200))",
};
export const CreateLabelModal: React.FC<Props> = ({ isOpen, projectId, handleClose, user }) => {
export const CreateLabelModal: React.FC<Props> = ({
isOpen,
projectId,
handleClose,
user,
onSuccess,
}) => {
const router = useRouter();
const { workspaceSlug } = router.query;
@@ -66,6 +73,7 @@ export const CreateLabelModal: React.FC<Props> = ({ isOpen, projectId, handleClo
false
);
onClose();
if (onSuccess) onSuccess(res);
})
.catch((error) => {
console.log(error);
@@ -0,0 +1,3 @@
export * from "./notification-card";
export * from "./notification-popover";
export * from "./select-snooze-till-modal";
@@ -0,0 +1,269 @@
import React from "react";
import Image from "next/image";
import { useRouter } from "next/router";
// hooks
import useToast from "hooks/use-toast";
// icons
import { CustomMenu, Icon, Tooltip } from "components/ui";
// helper
import { stripHTML, replaceUnderscoreIfSnakeCase, truncateText } from "helpers/string.helper";
import {
formatDateDistance,
render12HourFormatTime,
renderLongDateFormat,
renderShortDate,
renderShortDateWithYearFormat,
} from "helpers/date-time.helper";
// type
import type { IUserNotification } from "types";
type NotificationCardProps = {
notification: IUserNotification;
markNotificationReadStatus: (notificationId: string) => Promise<void>;
markNotificationArchivedStatus: (notificationId: string) => Promise<void>;
setSelectedNotificationForSnooze: (notificationId: string) => void;
markSnoozeNotification: (notificationId: string, dateTime?: Date | undefined) => Promise<void>;
};
const snoozeOptions = [
{
label: "1 day",
value: new Date(new Date().getTime() + 24 * 60 * 60 * 1000),
},
{
label: "3 days",
value: new Date(new Date().getTime() + 3 * 24 * 60 * 60 * 1000),
},
{
label: "5 days",
value: new Date(new Date().getTime() + 5 * 24 * 60 * 60 * 1000),
},
{
label: "1 week",
value: new Date(new Date().getTime() + 7 * 24 * 60 * 60 * 1000),
},
{
label: "2 weeks",
value: new Date(new Date().getTime() + 14 * 24 * 60 * 60 * 1000),
},
{
label: "Custom",
value: null,
},
];
export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
const {
notification,
markNotificationReadStatus,
markNotificationArchivedStatus,
setSelectedNotificationForSnooze,
markSnoozeNotification,
} = props;
const router = useRouter();
const { workspaceSlug } = router.query;
const { setToastAlert } = useToast();
return (
<div
onClick={() => {
markNotificationReadStatus(notification.id);
router.push(
`/${workspaceSlug}/projects/${notification.project}/issues/${notification.data.issue.id}`
);
}}
className={`group w-full flex items-center gap-4 p-3 pl-6 relative cursor-pointer ${
notification.read_at === null ? "bg-custom-primary-70/5" : "hover:bg-custom-background-200"
}`}
>
{notification.read_at === null && (
<span className="absolute top-1/2 left-2 -translate-y-1/2 w-1.5 h-1.5 bg-custom-primary-100 rounded-full" />
)}
<div className="relative w-12 h-12 rounded-full">
{notification.triggered_by_details.avatar &&
notification.triggered_by_details.avatar !== "" ? (
<div className="h-12 w-12 rounded-full">
<Image
src={notification.triggered_by_details.avatar}
alt="Profile Image"
layout="fill"
objectFit="cover"
className="rounded-full"
/>
</div>
) : (
<div className="w-12 h-12 bg-custom-background-80 rounded-full flex justify-center items-center">
<span className="text-custom-text-100 font-medium text-lg">
{notification.triggered_by_details.first_name[0].toUpperCase()}
</span>
</div>
)}
</div>
<div className="space-y-2.5 w-full overflow-hidden">
<div className="text-sm w-full break-words">
<span className="font-semibold">
{notification.triggered_by_details.first_name}{" "}
{notification.triggered_by_details.last_name}{" "}
</span>
{notification.data.issue_activity.field !== "comment" &&
notification.data.issue_activity.verb}{" "}
{notification.data.issue_activity.field === "comment"
? "commented"
: notification.data.issue_activity.field === "None"
? null
: replaceUnderscoreIfSnakeCase(notification.data.issue_activity.field)}{" "}
{notification.data.issue_activity.field !== "comment" &&
notification.data.issue_activity.field !== "None"
? "to"
: ""}
<span className="font-semibold">
{" "}
{notification.data.issue_activity.field !== "None" ? (
notification.data.issue_activity.field !== "comment" ? (
notification.data.issue_activity.field === "target_date" ? (
renderShortDateWithYearFormat(notification.data.issue_activity.new_value)
) : notification.data.issue_activity.field === "attachment" ? (
"the issue"
) : stripHTML(notification.data.issue_activity.new_value).length > 55 ? (
stripHTML(notification.data.issue_activity.new_value).slice(0, 50) + "..."
) : (
stripHTML(notification.data.issue_activity.new_value)
)
) : (
<span>
{`"`}
{notification.data.issue_activity.new_value.length > 55
? notification?.data?.issue_activity?.issue_comment?.slice(0, 50) + "..."
: notification.data.issue_activity.issue_comment}
{`"`}
</span>
)
) : (
"the issue and assigned it to you."
)}
</span>
</div>
<div className="flex justify-between gap-2 text-xs">
<p className="text-custom-text-300">
{truncateText(
`${notification.data.issue.identifier}-${notification.data.issue.sequence_id} ${notification.data.issue.name}`,
50
)}
</p>
{notification.snoozed_till ? (
<p className="text-custom-text-300 flex items-center justify-end gap-x-1 flex-shrink-0">
<Icon iconName="schedule" className="!text-base -my-0.5" />
<span>
Till {renderShortDate(notification.snoozed_till)},{" "}
{render12HourFormatTime(notification.snoozed_till)}
</span>
</p>
) : (
<p className="text-custom-text-300 flex-shrink-0">
{formatDateDistance(notification.created_at)}
</p>
)}
</div>
</div>
<div className="absolute py-1 gap-x-3 right-3 top-3 hidden group-hover:flex">
{[
{
id: 1,
name: notification.read_at ? "Mark as unread" : "Mark as read",
icon: "chat_bubble",
onClick: () => {
markNotificationReadStatus(notification.id).then(() => {
setToastAlert({
title: notification.read_at
? "Notification marked as unread"
: "Notification marked as read",
type: "success",
});
});
},
},
{
id: 2,
name: notification.archived_at ? "Unarchive" : "Archive",
icon: notification.archived_at ? "unarchive" : "archive",
onClick: () => {
markNotificationArchivedStatus(notification.id).then(() => {
setToastAlert({
title: notification.archived_at
? "Notification un-archived"
: "Notification archived",
type: "success",
});
});
},
},
].map((item) => (
<Tooltip tooltipContent={item.name}>
<button
type="button"
onClick={(e) => {
e.stopPropagation();
item.onClick();
}}
key={item.id}
className="text-sm flex w-full items-center gap-x-2 bg-custom-background-80 hover:bg-custom-background-100 p-0.5 rounded outline-none"
>
<Icon iconName={item.icon} className="h-5 w-5 text-custom-text-300" />
</button>
</Tooltip>
))}
<Tooltip tooltipContent="Snooze">
<div>
<CustomMenu
menuButtonOnClick={(e) => {
e.stopPropagation();
}}
customButton={
<button
type="button"
className="text-sm flex w-full items-center gap-x-2 bg-custom-background-80 hover:bg-custom-background-100 p-0.5 rounded"
>
<Icon iconName="schedule" className="h-5 w-5 text-custom-text-300" />
</button>
}
optionsClassName="!z-20"
>
{snoozeOptions.map((item) => (
<CustomMenu.MenuItem
key={item.label}
renderAs="button"
onClick={(e) => {
e.stopPropagation();
if (!item.value) {
setSelectedNotificationForSnooze(notification.id);
return;
}
markSnoozeNotification(notification.id, item.value).then(() => {
setToastAlert({
title: `Notification snoozed till ${renderLongDateFormat(item.value)}`,
type: "success",
});
});
}}
>
{item.label}
</CustomMenu.MenuItem>
))}
</CustomMenu>
</div>
</Tooltip>
</div>
</div>
);
};
@@ -0,0 +1,302 @@
import React, { Fragment } from "react";
import { useRouter } from "next/router";
// hooks
import useTheme from "hooks/use-theme";
import { Popover, Transition } from "@headlessui/react";
// hooks
import useWorkspaceMembers from "hooks/use-workspace-members";
import useUserNotification from "hooks/use-user-notifications";
// components
import { Icon, Loader, EmptyState, Tooltip } from "components/ui";
import { SnoozeNotificationModal, NotificationCard } from "components/notifications";
// icons
import { NotificationsOutlined } from "@mui/icons-material";
// images
import emptyNotification from "public/empty-state/notification.svg";
// helpers
import { getNumberCount } from "helpers/string.helper";
// type
import type { NotificationType } from "types";
export const NotificationPopover = () => {
const {
notifications,
archived,
readNotification,
selectedNotificationForSnooze,
selectedTab,
setArchived,
setReadNotification,
setSelectedNotificationForSnooze,
setSelectedTab,
setSnoozed,
snoozed,
notificationsMutate,
markNotificationArchivedStatus,
markNotificationReadStatus,
markSnoozeNotification,
notificationCount,
totalNotificationCount,
} = useUserNotification();
const router = useRouter();
const { workspaceSlug } = router.query;
const { isOwner, isMember } = useWorkspaceMembers(workspaceSlug?.toString() ?? "");
// theme context
const { collapsed: sidebarCollapse } = useTheme();
const notificationTabs: Array<{
label: string;
value: NotificationType;
unreadCount?: number;
}> = [
{
label: "My Issues",
value: "assigned",
unreadCount: notificationCount?.my_issues,
},
{
label: "Created by me",
value: "created",
unreadCount: notificationCount?.created_issues,
},
{
label: "Subscribed",
value: "watching",
unreadCount: notificationCount?.watching_issues,
},
];
return (
<>
<SnoozeNotificationModal
isOpen={selectedNotificationForSnooze !== null}
onClose={() => setSelectedNotificationForSnooze(null)}
onSubmit={markSnoozeNotification}
notification={
notifications?.find(
(notification) => notification.id === selectedNotificationForSnooze
) || null
}
onSuccess={() => {
notificationsMutate();
setSelectedNotificationForSnooze(null);
}}
/>
<Popover className="relative w-full">
{({ open: isActive, close: closePopover }) => (
<>
<Popover.Button
className={`group flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-sm font-medium outline-none ${
isActive
? "bg-custom-primary-100/10 text-custom-primary-100"
: "text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80"
} ${sidebarCollapse ? "justify-center" : ""}`}
>
<NotificationsOutlined fontSize="small" />
{sidebarCollapse ? null : <span>Notifications</span>}
{totalNotificationCount && totalNotificationCount > 0 ? (
<span className="ml-auto bg-custom-primary-300 rounded-full text-xs text-white px-1.5">
{getNumberCount(totalNotificationCount)}
</span>
) : null}
</Popover.Button>
<Transition
as={Fragment}
enter="transition ease-out duration-200"
enterFrom="opacity-0 translate-y-1"
enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-150"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<Popover.Panel className="absolute bg-custom-background-100 flex flex-col left-0 md:left-full ml-8 z-10 top-0 md:w-[36rem] w-[20rem] h-[50vh] border border-custom-border-300 shadow-lg rounded-xl">
<div className="flex items-center justify-between px-5 pt-5">
<h2 className="text-xl font-semibold mb-2">Notifications</h2>
<div className="flex gap-x-4 justify-center items-center text-custom-text-200">
<Tooltip tooltipContent="Refresh">
<button
type="button"
onClick={(e) => {
notificationsMutate();
const target = e.target as HTMLButtonElement;
target?.classList.add("animate-spin");
setTimeout(() => {
target?.classList.remove("animate-spin");
}, 1000);
}}
>
<Icon iconName="refresh" />
</button>
</Tooltip>
<Tooltip tooltipContent="Unread notifications">
<button
type="button"
onClick={() => {
setSnoozed(false);
setArchived(false);
setReadNotification((prev) => !prev);
}}
>
<Icon iconName="filter_list" />
</button>
</Tooltip>
<Tooltip tooltipContent="Snoozed notifications">
<button
type="button"
onClick={() => {
setArchived(false);
setReadNotification(false);
setSnoozed((prev) => !prev);
}}
>
<Icon iconName="schedule" />
</button>
</Tooltip>
<Tooltip tooltipContent="Archived notifications">
<button
type="button"
onClick={() => {
setSnoozed(false);
setReadNotification(false);
setArchived((prev) => !prev);
}}
>
<Icon iconName="archive" />
</button>
</Tooltip>
<button type="button" onClick={() => closePopover()}>
<Icon iconName="close" />
</button>
</div>
</div>
<div className="border-b border-custom-border-300 w-full px-5 mt-5">
{snoozed || archived || readNotification ? (
<button
type="button"
onClick={() => {
setSnoozed(false);
setArchived(false);
setReadNotification(false);
}}
>
<h4 className="flex items-center gap-2 pb-4">
<Icon iconName="arrow_back" />
<span className="ml-2 font-medium">
{snoozed
? "Snoozed Notifications"
: readNotification
? "Unread Notifications"
: "Archived Notifications"}
</span>
</h4>
</button>
) : (
<nav className="flex space-x-5 overflow-x-auto" aria-label="Tabs">
{notificationTabs.map((tab) =>
tab.value === "created" ? (
isMember || isOwner ? (
<button
type="button"
key={tab.value}
onClick={() => setSelectedTab(tab.value)}
className={`whitespace-nowrap border-b-2 pb-4 px-1 text-sm font-medium outline-none ${
tab.value === selectedTab
? "border-custom-primary-100 text-custom-primary-100"
: "border-transparent text-custom-text-200"
}`}
>
{tab.label}
{tab.unreadCount && tab.unreadCount > 0 ? (
<span
className={`ml-2 rounded-full text-xs px-2 py-0.5 ${
tab.value === selectedTab
? "bg-custom-primary-100 text-white"
: "bg-custom-background-80 text-custom-text-200"
}`}
>
{getNumberCount(tab.unreadCount)}
</span>
) : null}
</button>
) : null
) : (
<button
type="button"
key={tab.value}
onClick={() => setSelectedTab(tab.value)}
className={`whitespace-nowrap border-b-2 pb-4 px-1 text-sm font-medium ${
tab.value === selectedTab
? "border-custom-primary-100 text-custom-primary-100"
: "border-transparent text-custom-text-200"
}`}
>
{tab.label}
{tab.unreadCount && tab.unreadCount > 0 ? (
<span
className={`ml-2 rounded-full text-xs px-2 py-0.5 ${
tab.value === selectedTab
? "bg-custom-primary-100 text-white"
: "bg-custom-background-80 text-custom-text-200"
}`}
>
{getNumberCount(tab.unreadCount)}
</span>
) : null}
</button>
)
)}
</nav>
)}
</div>
{notifications ? (
notifications.length > 0 ? (
<div className="divide-y divide-custom-border-100 overflow-y-auto h-full">
{notifications.map((notification) => (
<NotificationCard
key={notification.id}
notification={notification}
markNotificationArchivedStatus={markNotificationArchivedStatus}
markNotificationReadStatus={markNotificationReadStatus}
setSelectedNotificationForSnooze={setSelectedNotificationForSnooze}
markSnoozeNotification={markSnoozeNotification}
/>
))}
</div>
) : (
<div className="grid h-full w-full place-items-center overflow-hidden scale-75">
<EmptyState
title="You're updated with all the notifications"
description="You have read all the notifications."
image={emptyNotification}
isFullScreen={false}
/>
</div>
)
) : (
<Loader className="p-5 space-y-4 overflow-y-auto">
<Loader.Item height="50px" />
<Loader.Item height="50px" />
<Loader.Item height="50px" />
<Loader.Item height="50px" />
<Loader.Item height="50px" />
</Loader>
)}
</Popover.Panel>
</Transition>
</>
)}
</Popover>
</>
);
};
@@ -0,0 +1,288 @@
import React, { Fragment } from "react";
// next
import { useRouter } from "next/router";
// react hook form
import { useForm, Controller } from "react-hook-form";
import { Transition, Dialog, Listbox } from "@headlessui/react";
// date helper
import { getAllTimeIn30MinutesInterval } from "helpers/date-time.helper";
// hooks
import useToast from "hooks/use-toast";
// components
import {
PrimaryButton,
SecondaryButton,
Icon,
CustomDatePicker,
CustomSelect,
} from "components/ui";
// types
import type { IUserNotification } from "types";
type SnoozeModalProps = {
isOpen: boolean;
onClose: () => void;
onSuccess: () => void;
notification: IUserNotification | null;
onSubmit: (notificationId: string, dateTime?: Date | undefined) => Promise<void>;
};
type FormValues = {
time: string | null;
date: Date | null;
period: "AM" | "PM";
};
const defaultValues: FormValues = {
time: null,
date: null,
period: "AM",
};
const timeStamps = getAllTimeIn30MinutesInterval();
export const SnoozeNotificationModal: React.FC<SnoozeModalProps> = (props) => {
const { isOpen, onClose, notification, onSuccess, onSubmit: handleSubmitSnooze } = props;
const router = useRouter();
const { workspaceSlug } = router.query;
const { setToastAlert } = useToast();
const {
formState: { isSubmitting },
reset,
handleSubmit,
control,
watch,
setValue,
} = useForm({
defaultValues,
});
const getTimeStamp = () => {
const today = new Date();
const formDataDate = watch("date");
if (!formDataDate) return timeStamps;
const isToday = today.toDateString() === new Date(formDataDate).toDateString();
if (!isToday) return timeStamps;
const hours = today.getHours();
const minutes = today.getMinutes();
return timeStamps.filter((optionTime) => {
let optionHours = parseInt(optionTime.value.split(":")[0]);
const optionMinutes = parseInt(optionTime.value.split(":")[1]);
const period = watch("period");
if (period === "PM" && optionHours !== 12) optionHours += 12;
if (optionHours < hours) return false;
if (optionHours === hours && optionMinutes < minutes) return false;
return true;
});
};
const onSubmit = async (formData: FormValues) => {
if (!workspaceSlug || !notification || !formData.date || !formData.time) return;
const period = formData.period;
const time = formData.time.split(":");
const hours = parseInt(
`${period === "AM" ? time[0] : parseInt(time[0]) + 12 === 24 ? "00" : parseInt(time[0]) + 12}`
);
const minutes = parseInt(time[1]);
const dateTime = new Date(formData.date);
dateTime.setHours(hours);
dateTime.setMinutes(minutes);
await handleSubmitSnooze(notification.id, dateTime).then(() => {
handleClose();
onSuccess();
setToastAlert({
title: "Notification snoozed",
message: "Notification snoozed successfully",
type: "success",
});
});
};
const handleClose = () => {
onClose();
const timeout = setTimeout(() => {
reset({ ...defaultValues });
clearTimeout(timeout);
}, 500);
};
return (
<Transition.Root show={isOpen} as={React.Fragment}>
<Dialog as="div" className="relative z-20" onClose={handleClose}>
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-custom-backdrop bg-opacity-50 transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-20 overflow-y-auto">
<div className="flex items-center justify-center min-h-full p-4 text-center">
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative transform rounded-lg border border-custom-border-100 bg-custom-background-100 p-5 text-left shadow-xl transition-all sm:w-full sm:max-w-2xl">
<form onSubmit={handleSubmit(onSubmit)}>
<div className="flex justify-between items-center">
<Dialog.Title
as="h3"
className="text-lg font-medium leading-6 text-custom-text-100"
>
Customize Snooze Time
</Dialog.Title>
<div>
<button type="button" onClick={handleClose}>
<Icon iconName="close" className="w-5 h-5 text-custom-text-100" />
</button>
</div>
</div>
<div className="mt-5 flex items-center gap-3">
<div className="flex-1">
<h6 className="block text-sm font-medium text-custom-text-400 mb-2">
Pick a date
</h6>
<Controller
name="date"
control={control}
rules={{ required: "Please select a date" }}
render={({ field: { value, onChange } }) => (
<CustomDatePicker
placeholder="Select date"
value={value}
onChange={(val) => {
setValue("time", null);
onChange(val);
}}
className="px-3 py-2 w-full rounded-md border border-custom-border-300 bg-custom-background-100 text-custom-text-100 focus:outline-none !text-sm"
noBorder
minDate={new Date()}
/>
)}
/>
</div>
<div className="flex-1">
<h6 className="block text-sm font-medium text-custom-text-400 mb-2">
Pick a time
</h6>
<Controller
control={control}
name="time"
rules={{ required: "Please select a time" }}
render={({ field: { value, onChange } }) => (
<CustomSelect
value={value}
onChange={onChange}
label={
<div className="truncate">
{value ? (
<span>
{value} {watch("period").toLowerCase()}
</span>
) : (
<span className="text-custom-text-400 text-sm">
Select a time
</span>
)}
</div>
}
width="w-full"
input
>
<div className="w-full rounded overflow-hidden h-9 mb-2 flex">
<div
onClick={() => {
setValue("period", "AM");
}}
className={`w-1/2 h-full cursor-pointer flex justify-center items-center text-center ${
watch("period") === "AM"
? "bg-custom-primary-100/90 text-custom-primary-0"
: "bg-custom-background-80"
}`}
>
AM
</div>
<div
onClick={() => {
setValue("period", "PM");
}}
className={`w-1/2 h-full cursor-pointer flex justify-center items-center text-center ${
watch("period") === "PM"
? "bg-custom-primary-100/90 text-custom-primary-0"
: "bg-custom-background-80"
}`}
>
PM
</div>
</div>
{getTimeStamp().length > 0 ? (
getTimeStamp().map((time, index) => (
<CustomSelect.Option key={`${time}-${index}`} value={time.value}>
<div className="flex items-center">
<span className="ml-3 block truncate">{time.label}</span>
</div>
</CustomSelect.Option>
))
) : (
<p className="text-custom-text-200 text-center p-3">
No available time for this date.
</p>
)}
</CustomSelect>
)}
/>
</div>
</div>
<div className="mt-5 flex items-center justify-between gap-2">
<div className="w-full flex items-center gap-2 justify-end">
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
<PrimaryButton type="submit" loading={isSubmitting}>
{isSubmitting ? "Submitting..." : "Submit"}
</PrimaryButton>
</div>
</div>
</form>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
);
};
@@ -41,7 +41,7 @@ export const InviteMembers: React.FC<Props> = ({ workspace, user, stepChange })
const {
control,
handleSubmit,
formState: { isSubmitting, errors },
formState: { isSubmitting, errors, isValid },
} = useForm<FormValues>();
const { fields, append, remove } = useFieldArray({
@@ -204,10 +204,10 @@ export const InviteMembers: React.FC<Props> = ({ workspace, user, stepChange })
</button>
</div>
<div className="flex items-center gap-4">
<PrimaryButton type="submit" loading={isSubmitting} size="md">
<PrimaryButton type="submit" disabled={!isValid} loading={isSubmitting} size="md">
{isSubmitting ? "Sending..." : "Send Invite"}
</PrimaryButton>
<SecondaryButton size="md" onClick={nextStep} outline>
<SecondaryButton className="border border-none bg-transparent" size="md" onClick={nextStep}>
Skip this step
</SecondaryButton>
</div>
@@ -88,7 +88,7 @@ export const JoinWorkspaces: React.FC<Props> = ({ stepChange }) => {
<div className="w-full space-y-7 sm:space-y-10">
<h5 className="sm:text-lg">We see that someone has invited you to</h5>
<h4 className="text-xl sm:text-2xl font-semibold">Join a workspace</h4>
<div className="md:w-3/5 space-y-4">
<div className="max-h-[37vh] overflow-y-auto md:w-3/5 space-y-4">
{invitations &&
invitations.map((invitation) => {
const isSelected = invitationsRespond.includes(invitation.id);
@@ -146,7 +146,11 @@ export const JoinWorkspaces: React.FC<Props> = ({ stepChange }) => {
>
Accept & Join
</PrimaryButton>
<SecondaryButton size="md" onClick={finishOnboarding} outline>
<SecondaryButton
className="border border-none bg-transparent"
size="md"
onClick={finishOnboarding}
>
Skip for now
</SecondaryButton>
</div>
+3 -3
View File
@@ -53,7 +53,7 @@ const TOUR_STEPS: {
key: "modules",
title: "Break into modules",
description:
"Modules break your big think into Projects or Features, to help you organize better.",
"Modules break your big thing into Projects or Features, to help you organize better.",
image: ModulesTour,
prevStep: "cycles",
nextStep: "views",
@@ -93,7 +93,7 @@ export const TourRoot: React.FC<Props> = ({ onComplete }) => {
<Image src={PlaneWhiteLogo} alt="Plane White Logo" />
</div>
<div className="h-2/5 overflow-y-auto p-6">
<h3 className="font-medium text-lg">
<h3 className="font-semibold sm:text-xl">
Welcome to Plane, {user?.first_name} {user?.last_name}
</h3>
<p className="text-custom-text-200 text-sm mt-3">
@@ -132,7 +132,7 @@ export const TourRoot: React.FC<Props> = ({ onComplete }) => {
<Image src={currentStep?.image} alt={currentStep?.title} />
</div>
<div className="flex flex-col h-1/2 sm:h-2/5 p-4 overflow-y-auto">
<h3 className="font-medium text-lg">{currentStep?.title}</h3>
<h3 className="font-semibold sm:text-xl">{currentStep?.title}</h3>
<p className="text-custom-text-200 text-sm mt-3">{currentStep?.description}</p>
<div className="h-full flex items-end justify-between gap-4 mt-3">
<div className="flex items-center gap-4">
+17 -15
View File
@@ -1,32 +1,37 @@
// icons
import { ContrastIcon, LayerDiagonalIcon, PeopleGroupIcon, ViewListIcon } from "components/icons";
import { DocumentTextIcon } from "@heroicons/react/24/outline";
import {
ArticleOutlined,
ContrastOutlined,
DatasetOutlined,
FilterNoneOutlined,
PhotoFilterOutlined,
} from "@mui/icons-material";
// types
import { TTourSteps } from "./root";
const sidebarOptions: {
key: TTourSteps;
icon: any;
Icon: any;
}[] = [
{
key: "issues",
icon: LayerDiagonalIcon,
Icon: FilterNoneOutlined,
},
{
key: "cycles",
icon: ContrastIcon,
Icon: ContrastOutlined,
},
{
key: "modules",
icon: PeopleGroupIcon,
Icon: DatasetOutlined,
},
{
key: "views",
icon: ViewListIcon,
Icon: PhotoFilterOutlined,
},
{
key: "pages",
icon: DocumentTextIcon,
Icon: ArticleOutlined,
},
];
@@ -53,13 +58,10 @@ export const TourSidebar: React.FC<Props> = ({ step, setStep }) => (
}`}
onClick={() => setStep(option.key)}
>
<option.icon
className={`h-5 w-5 flex-shrink-0 ${
step === option.key ? "text-custom-primary-100" : "text-custom-text-200"
}`}
color={`${
step === option.key ? "rgb(var(--color-primary-100))" : "rgb(var(--color-text-200))"
}`}
<option.Icon
sx={{
fontSize: 18,
}}
aria-hidden="true"
/>
{option.key}
@@ -35,7 +35,7 @@ export const UserDetails: React.FC<Props> = ({ user }) => {
handleSubmit,
control,
reset,
formState: { errors, isSubmitting },
formState: { errors, isSubmitting, isValid },
} = useForm<IUser>({
defaultValues,
});
@@ -164,7 +164,7 @@ export const UserDetails: React.FC<Props> = ({ user }) => {
</div>
</div>
<PrimaryButton type="submit" size="md" disabled={isSubmitting}>
<PrimaryButton type="submit" size="md" disabled={!isValid} loading={isSubmitting}>
{isSubmitting ? "Updating..." : "Continue"}
</PrimaryButton>
</form>
@@ -38,6 +38,10 @@ export const Workspace: React.FC<Props> = ({ user, updateLastWorkspace, stepChan
defaultValues={defaultValues}
setDefaultValues={setDefaultValues}
user={user}
primaryButtonText={{
loading: "Creating...",
default: "Continue",
}}
secondaryButton={
<SecondaryButton onClick={() => stepChange({ profile_complete: false })}>
Back
@@ -175,7 +175,11 @@ export const DeleteProjectModal: React.FC<TConfirmProjectDeletionProps> = ({
</div>
<div className="flex justify-end gap-2">
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
<DangerButton onClick={handleDeletion} loading={isDeleteLoading || !canDelete}>
<DangerButton
onClick={handleDeletion}
disabled={!canDelete}
loading={isDeleteLoading}
>
{isDeleteLoading ? "Deleting..." : "Delete Project"}
</DangerButton>
</div>
@@ -10,9 +10,19 @@ import projectService from "services/project.service";
// hooks
import useToast from "hooks/use-toast";
// ui
import { CustomMenu, Icon, Tooltip } from "components/ui";
import { CustomMenu, Tooltip } from "components/ui";
// icons
import { LinkIcon, StarIcon, TrashIcon } from "@heroicons/react/24/outline";
import {
ArchiveOutlined,
ArticleOutlined,
ContrastOutlined,
DatasetOutlined,
ExpandMoreOutlined,
FilterNoneOutlined,
PhotoFilterOutlined,
SettingsOutlined,
} from "@mui/icons-material";
// helpers
import { truncateText } from "helpers/string.helper";
import { renderEmoji } from "helpers/emoji.helper";
@@ -33,32 +43,32 @@ const navigation = (workspaceSlug: string, projectId: string) => [
{
name: "Issues",
href: `/${workspaceSlug}/projects/${projectId}/issues`,
icon: "stack",
Icon: FilterNoneOutlined,
},
{
name: "Cycles",
href: `/${workspaceSlug}/projects/${projectId}/cycles`,
icon: "contrast",
Icon: ContrastOutlined,
},
{
name: "Modules",
href: `/${workspaceSlug}/projects/${projectId}/modules`,
icon: "dataset",
Icon: DatasetOutlined,
},
{
name: "Views",
href: `/${workspaceSlug}/projects/${projectId}/views`,
icon: "photo_filter",
Icon: PhotoFilterOutlined,
},
{
name: "Pages",
href: `/${workspaceSlug}/projects/${projectId}/pages`,
icon: "article",
Icon: ArticleOutlined,
},
{
name: "Settings",
href: `/${workspaceSlug}/projects/${projectId}/settings`,
icon: "settings",
Icon: SettingsOutlined,
},
];
@@ -164,8 +174,8 @@ export const SingleSidebarProject: React.FC<Props> = ({
)}
</div>
{!sidebarCollapse && (
<Icon
iconName="expand_more"
<ExpandMoreOutlined
fontSize="small"
className={`${open ? "rotate-180" : ""} text-custom-text-200 duration-300`}
/>
)}
@@ -211,7 +221,7 @@ export const SingleSidebarProject: React.FC<Props> = ({
}
>
<div className="flex items-center justify-start gap-2">
<Icon iconName="archive" className="h-4 w-4" />
<ArchiveOutlined fontSize="small" />
<span>Archived Issues</span>
</div>
</CustomMenu.MenuItem>
@@ -248,13 +258,17 @@ export const SingleSidebarProject: React.FC<Props> = ({
disabled={!sidebarCollapse}
>
<div
className={`group flex items-center rounded-md px-2 py-1.5 gap-2 text-xs font-medium outline-none ${
className={`group flex items-center rounded-md px-2 py-1.5 gap-2.5 text-xs font-medium outline-none ${
router.asPath.includes(item.href)
? "bg-custom-primary-100/10 text-custom-primary-100"
: "text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80 focus:bg-custom-sidebar-background-80"
} ${sidebarCollapse ? "justify-center" : ""}`}
>
<Icon iconName={item.icon} />
<item.Icon
sx={{
fontSize: 18,
}}
/>
{!sidebarCollapse && item.name}
</div>
</Tooltip>
@@ -19,11 +19,7 @@ export const PrimaryButton: React.FC<ButtonProps> = ({
: size === "md"
? "rounded-md px-3.5 py-2 text-sm"
: "rounded-lg px-4 py-2 text-base"
} ${
disabled
? "cursor-not-allowed bg-opacity-70 border-opacity-70 hover:bg-opacity-70 hover:border-opacity-70"
: ""
} ${
} ${disabled ? "cursor-not-allowed opacity-70 hover:opacity-70" : ""} ${
outline
? "bg-transparent text-custom-primary hover:bg-custom-primary hover:text-white"
: "text-white bg-custom-primary hover:border-opacity-90 hover:bg-opacity-90"
-187
View File
@@ -1,187 +0,0 @@
import React from "react";
// next
import Link from "next/link";
// headless ui
import { Menu, Transition } from "@headlessui/react";
// icons
import { ChevronDownIcon } from "@heroicons/react/24/outline";
import { Icon } from "./icon";
type Props = {
children: React.ReactNode;
label?: string | JSX.Element;
className?: string;
ellipsis?: boolean;
verticalEllipsis?: boolean;
height?: "sm" | "md" | "rg" | "lg";
width?: "sm" | "md" | "lg" | "xl" | "auto";
textAlignment?: "left" | "center" | "right";
noBorder?: boolean;
noChevron?: boolean;
position?: "left" | "right";
verticalPosition?: "top" | "bottom";
menuItemsClassName?: string;
customButton?: JSX.Element;
menuItemsWhiteBg?: boolean;
};
type MenuItemProps = {
children: JSX.Element | string;
renderAs?: "button" | "a";
href?: string;
onClick?: (args?: any) => void;
className?: string;
};
const CustomMenu = ({
children,
label,
className = "",
ellipsis = false,
verticalEllipsis = false,
height = "md",
width = "auto",
textAlignment,
noBorder = false,
noChevron = false,
position = "right",
verticalPosition = "bottom",
menuItemsClassName = "",
customButton,
menuItemsWhiteBg = false,
}: Props) => (
<Menu as="div" className={`relative w-min whitespace-nowrap text-left ${className}`}>
{({ open }) => (
<>
{customButton ? (
<Menu.Button as="div">{customButton}</Menu.Button>
) : (
<div>
{ellipsis || verticalEllipsis ? (
<Menu.Button
type="button"
className="relative grid place-items-center rounded p-1 text-custom-text-200 hover:bg-custom-background-80 outline-none"
>
<Icon
iconName="more_horiz"
className={`${verticalEllipsis ? "rotate-90" : ""} text-brand-secondary`}
/>
</Menu.Button>
) : (
<Menu.Button
type="button"
className={`flex cursor-pointer items-center justify-between gap-1 px-2.5 py-1 text-xs duration-300 hover:bg-custom-background-80 ${
open ? "bg-custom-background-90 text-custom-text-100" : "text-custom-text-200"
} ${
textAlignment === "right"
? "text-right"
: textAlignment === "center"
? "text-center"
: "text-left"
} ${
noBorder
? "rounded-md"
: "rounded-md border border-custom-border-200 shadow-sm focus:outline-none"
} ${
width === "sm"
? "w-10"
: width === "md"
? "w-20"
: width === "lg"
? "w-32"
: width === "xl"
? "w-48"
: "w-full"
}`}
>
{label}
{!noChevron && <ChevronDownIcon className="h-3 w-3" aria-hidden="true" />}
</Menu.Button>
)}
</div>
)}
<Transition
as={React.Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items
className={`absolute z-20 overflow-y-scroll whitespace-nowrap rounded-md border p-1 text-xs shadow-lg focus:outline-none ${
position === "left" ? "left-0 origin-top-left" : "right-0 origin-top-right"
} ${verticalPosition === "top" ? "bottom-full mb-1" : "mt-1"} ${
height === "sm"
? "max-h-28"
: height === "md"
? "max-h-44"
: height === "rg"
? "max-h-56"
: height === "lg"
? "max-h-80"
: ""
} ${
width === "sm"
? "w-10"
: width === "md"
? "w-20"
: width === "lg"
? "w-32"
: width === "xl"
? "w-48"
: "min-w-full"
} ${
menuItemsWhiteBg
? "border-custom-border-200 bg-custom-background-100"
: "border-custom-border-200 bg-custom-background-90"
} ${menuItemsClassName}`}
>
<div className="py-1">{children}</div>
</Menu.Items>
</Transition>
</>
)}
</Menu>
);
const MenuItem: React.FC<MenuItemProps> = ({
children,
renderAs,
href,
onClick,
className = "",
}) => (
<Menu.Item as="div">
{({ active, close }) =>
renderAs === "a" ? (
<Link href={href ?? ""}>
<a
className={`${className} ${
active ? "bg-custom-background-80" : ""
} hover:text-custom-text-200 inline-block w-full select-none gap-2 truncate rounded px-1 py-1.5 text-left text-custom-text-200 hover:bg-custom-background-80`}
onClick={close}
>
{children}
</a>
</Link>
) : (
<button
type="button"
className={`${className} ${
active ? "bg-custom-background-80" : ""
} hover:text-custom-text-200 w-full select-none gap-2 truncate rounded px-1 py-1.5 text-left text-custom-text-200 hover:bg-custom-background-80`}
onClick={onClick}
>
{children}
</button>
)
}
</Menu.Item>
);
CustomMenu.MenuItem = MenuItem;
export { CustomMenu };
+3
View File
@@ -15,6 +15,7 @@ type Props = {
className?: string;
isClearable?: boolean;
disabled?: boolean;
minDate?: Date;
};
export const CustomDatePicker: React.FC<Props> = ({
@@ -28,6 +29,7 @@ export const CustomDatePicker: React.FC<Props> = ({
className = "",
isClearable = true,
disabled = false,
minDate,
}) => (
<DatePicker
placeholderText={placeholder}
@@ -52,5 +54,6 @@ export const CustomDatePicker: React.FC<Props> = ({
dateFormat="MMM dd, yyyy"
isClearable={isClearable}
disabled={disabled}
minDate={minDate}
/>
);
@@ -4,15 +4,17 @@ import Link from "next/link";
// headless ui
import { Menu, Transition } from "@headlessui/react";
// ui
import { DropdownProps } from "components/ui";
// icons
import { DropdownProps, Icon } from "components/ui";
import { ChevronDownIcon } from "@heroicons/react/24/outline";
import { ExpandMoreOutlined, MoreHorizOutlined } from "@mui/icons-material";
export type CustomMenuProps = DropdownProps & {
children: React.ReactNode;
ellipsis?: boolean;
noBorder?: boolean;
verticalEllipsis?: boolean;
menuButtonOnClick?: (...args: any) => void;
};
const CustomMenu = ({
@@ -32,34 +34,38 @@ const CustomMenu = ({
verticalEllipsis = false,
verticalPosition = "bottom",
width = "auto",
menuButtonOnClick,
}: CustomMenuProps) => (
<Menu as="div" className={`${selfPositioned ? "" : "relative"} w-min text-left ${className}`}>
{({ open }) => (
<>
{customButton ? (
<Menu.Button as="div">{customButton}</Menu.Button>
<Menu.Button as="button" onClick={menuButtonOnClick}>
{customButton}
</Menu.Button>
) : (
<>
{ellipsis || verticalEllipsis ? (
<Menu.Button
type="button"
onClick={menuButtonOnClick}
disabled={disabled}
className={`relative grid place-items-center rounded p-1 text-custom-text-200 outline-none ${
disabled ? "cursor-not-allowed" : "cursor-pointer hover:bg-custom-background-80"
} ${buttonClassName}`}
>
<Icon
iconName="more_horiz"
<MoreHorizOutlined
fontSize="small"
className={`${verticalEllipsis ? "rotate-90" : ""} text-custom-text-200`}
/>
</Menu.Button>
) : (
<Menu.Button
type="button"
className={`flex items-center justify-between gap-1 rounded-md px-2.5 py-1 text-xs duration-300 ${
className={`flex items-center justify-between gap-1 rounded-md px-2.5 py-1 text-xs whitespace-nowrap duration-300 ${
open ? "bg-custom-background-90 text-custom-text-100" : "text-custom-text-200"
} ${
noBorder ? "" : "border border-custom-border-100 shadow-sm focus:outline-none"
noBorder ? "" : "border border-custom-border-300 shadow-sm focus:outline-none"
} ${
disabled
? "cursor-not-allowed text-custom-text-200"
@@ -67,7 +73,14 @@ const CustomMenu = ({
} ${buttonClassName}`}
>
{label}
{!noChevron && <ChevronDownIcon className="h-3 w-3" aria-hidden="true" />}
{!noChevron && (
<ExpandMoreOutlined
sx={{
fontSize: 14,
}}
aria-hidden="true"
/>
)}
</Menu.Button>
)}
</>
@@ -83,7 +96,7 @@ const CustomMenu = ({
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items
className={`absolute z-10 overflow-y-scroll whitespace-nowrap rounded-md border border-custom-border-100 p-1 text-xs shadow-lg focus:outline-none bg-custom-background-90 ${
className={`absolute z-10 overflow-y-scroll whitespace-nowrap rounded-md border border-custom-border-300 p-1 text-xs shadow-lg focus:outline-none bg-custom-background-90 ${
position === "left" ? "left-0 origin-top-left" : "right-0 origin-top-right"
} ${verticalPosition === "top" ? "bottom-full mb-1" : "mt-1"} ${
maxHeight === "lg"
@@ -106,7 +119,7 @@ const CustomMenu = ({
);
type MenuItemProps = {
children: JSX.Element | string;
children: React.ReactNode;
renderAs?: "button" | "a";
href?: string;
onClick?: (args?: any) => void;
@@ -69,7 +69,7 @@ export const CustomSearchSelect = ({
<Combobox.Button as="div">{customButton}</Combobox.Button>
) : (
<Combobox.Button
className={`flex items-center justify-between gap-1 w-full rounded-md shadow-sm border border-custom-border-100 duration-300 focus:outline-none ${
className={`flex items-center justify-between gap-1 w-full rounded-md shadow-sm border border-custom-border-300 duration-300 focus:outline-none ${
input ? "px-3 py-2 text-sm" : "px-2.5 py-1 text-xs"
} ${
disabled
@@ -94,7 +94,7 @@ export const CustomSearchSelect = ({
leaveTo="opacity-0 translate-y-1"
>
<Combobox.Options
className={`absolute z-10 min-w-[10rem] border border-custom-border-100 p-2 rounded-md bg-custom-background-90 text-xs shadow-lg focus:outline-none ${
className={`absolute z-10 min-w-[10rem] border border-custom-border-300 p-2 rounded-md bg-custom-background-90 text-xs shadow-lg focus:outline-none ${
position === "left" ? "left-0 origin-top-left" : "right-0 origin-top-right"
} ${verticalPosition === "top" ? "bottom-full mb-1" : "mt-1"} ${
width === "auto" ? "min-w-[8rem] whitespace-nowrap" : width
@@ -44,7 +44,7 @@ const CustomSelect = ({
<Listbox.Button as="div">{customButton}</Listbox.Button>
) : (
<Listbox.Button
className={`flex items-center justify-between gap-1 w-full rounded-md border border-custom-border-100 shadow-sm duration-300 focus:outline-none ${
className={`flex items-center justify-between gap-1 w-full rounded-md border border-custom-border-300 shadow-sm duration-300 focus:outline-none ${
input ? "px-3 py-2 text-sm" : "px-2.5 py-1 text-xs"
} ${
disabled
@@ -68,7 +68,7 @@ const CustomSelect = ({
leaveTo="transform opacity-0 scale-95"
>
<Listbox.Options
className={`absolute z-10 border border-custom-border-100 mt-1 origin-top-right overflow-y-auto rounded-md bg-custom-background-90 text-xs shadow-lg focus:outline-none ${
className={`absolute z-10 border border-custom-border-300 mt-1 origin-top-right overflow-y-auto rounded-md bg-custom-background-90 text-xs shadow-lg focus:outline-none ${
position === "left" ? "left-0 origin-top-left" : "right-0 origin-top-right"
} ${verticalPosition === "top" ? "bottom-full mb-1" : "mt-1"} ${
maxHeight === "lg"
+7 -5
View File
@@ -9,7 +9,7 @@ type Props = {
title: string;
description: React.ReactNode | string;
image: any;
buttonText: string;
buttonText?: string;
buttonIcon?: any;
onClick?: () => void;
isFullScreen?: boolean;
@@ -33,10 +33,12 @@ export const EmptyState: React.FC<Props> = ({
<Image src={image} className="w-52 sm:w-60" alt={buttonText} />
<h6 className="text-xl font-semibold mt-6 sm:mt-8 mb-3">{title}</h6>
<p className="text-custom-text-300 mb-7 sm:mb-8">{description}</p>
<PrimaryButton className="flex items-center gap-1.5" onClick={onClick}>
{buttonIcon}
{buttonText}
</PrimaryButton>
{buttonText && (
<PrimaryButton className="flex items-center gap-1.5" onClick={onClick}>
{buttonIcon}
{buttonText}
</PrimaryButton>
)}
</div>
</div>
);
File diff suppressed because it is too large Load Diff
@@ -35,7 +35,7 @@ export const ProductUpdatesModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
</Transition.Child>
<div className="fixed inset-0 z-20 overflow-y-auto">
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
<div className="grid place-items-center min-h-full text-center p-4">
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
@@ -45,8 +45,8 @@ export const ProductUpdatesModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-custom-background-80 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg">
<div className="max-h-[600px] overflow-y-auto bg-custom-background-80 p-5">
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-custom-background-100 border border-custom-border-100 text-left shadow-xl transition-all grid place-items-center sm:w-full sm:max-w-2xl">
<div className="max-h-[90vh] overflow-y-auto p-5">
<div className="sm:flex sm:items-start">
<div className="flex w-full flex-col gap-y-4 text-center sm:text-left">
<Dialog.Title
@@ -9,7 +9,7 @@ import workspaceService from "services/workspace.service";
// hooks
import useToast from "hooks/use-toast";
// ui
import { CustomSelect, Input, PrimaryButton, SecondaryButton } from "components/ui";
import { CustomSelect, Input, PrimaryButton } from "components/ui";
// types
import { ICurrentUserResponse, IWorkspace } from "types";
// fetch-keys
@@ -27,6 +27,10 @@ type Props = {
setDefaultValues: Dispatch<SetStateAction<any>>;
user: ICurrentUserResponse | undefined;
secondaryButton?: React.ReactNode;
primaryButtonText?: {
loading: string;
default: string;
};
};
const restrictedUrls = [
@@ -49,6 +53,10 @@ export const CreateWorkspaceForm: React.FC<Props> = ({
setDefaultValues,
user,
secondaryButton,
primaryButtonText = {
loading: "Creating...",
default: "Create Workspace",
},
}) => {
const [slugError, setSlugError] = useState(false);
const [invalidSlug, setInvalidSlug] = useState(false);
@@ -61,8 +69,8 @@ export const CreateWorkspaceForm: React.FC<Props> = ({
control,
setValue,
getValues,
formState: { errors, isSubmitting },
} = useForm<IWorkspace>({ defaultValues });
formState: { errors, isSubmitting, isValid },
} = useForm<IWorkspace>({ defaultValues, mode: "onChange" });
const handleCreateWorkspace = async (formData: IWorkspace) => {
await workspaceService
@@ -202,8 +210,8 @@ export const CreateWorkspaceForm: React.FC<Props> = ({
<div className="flex items-center gap-4">
{secondaryButton}
<PrimaryButton type="submit" size="md" disabled={isSubmitting}>
{isSubmitting ? "Creating..." : "Create Workspace"}
<PrimaryButton type="submit" size="md" disabled={!isValid} loading={isSubmitting}>
{isSubmitting ? primaryButtonText.loading : primaryButtonText.default}
</PrimaryButton>
</div>
</form>
+53 -147
View File
@@ -1,34 +1,16 @@
import React, { useRef, useState } from "react";
import { useRouter } from "next/router";
import Link from "next/link";
import useSWR from "swr";
// headless ui
import { Transition } from "@headlessui/react";
// services
import workspaceService from "services/workspace.service";
// hooks
import useTheme from "hooks/use-theme";
import useUser from "hooks/use-user";
import useOutsideClickDetector from "hooks/use-outside-click-detector";
// components
import UpgradeToProModal from "./upgrade-to-pro-modal";
// ui
import { CircularProgress } from "components/ui";
// icons
import {
ArrowLongLeftIcon,
ChatBubbleOvalLeftEllipsisIcon,
RocketLaunchIcon,
ArrowUpCircleIcon,
XMarkIcon,
} from "@heroicons/react/24/outline";
import { QuestionMarkCircleIcon, DocumentIcon, DiscordIcon, GithubIcon } from "components/icons";
// fetch-keys
import { WORKSPACE_DETAILS } from "constants/fetch-keys";
import { Bolt, HelpOutlineOutlined, WestOutlined } from "@mui/icons-material";
import { ChatBubbleOvalLeftEllipsisIcon } from "@heroicons/react/24/outline";
import { DocumentIcon, DiscordIcon, GithubIcon } from "components/icons";
const helpOptions = [
{
@@ -59,150 +41,74 @@ export interface WorkspaceHelpSectionProps {
}
export const WorkspaceHelpSection: React.FC<WorkspaceHelpSectionProps> = ({ setSidebarActive }) => {
const [alert, setAlert] = useState(false);
const [isNeedHelpOpen, setIsNeedHelpOpen] = useState(false);
const helpOptionsRef = useRef<HTMLDivElement | null>(null);
const router = useRouter();
const { workspaceSlug } = router.query;
const { collapsed: sidebarCollapse, toggleCollapsed } = useTheme();
useOutsideClickDetector(helpOptionsRef, () => setIsNeedHelpOpen(false));
const { user } = useUser();
const [upgradeModal, setUpgradeModal] = useState(false);
const { data: workspaceDetails } = useSWR(
workspaceSlug ? WORKSPACE_DETAILS(workspaceSlug as string) : null,
workspaceSlug ? () => workspaceService.getWorkspace(workspaceSlug as string) : null
);
const issueNumber = workspaceDetails?.total_issues || 0;
return (
<>
<UpgradeToProModal
isOpen={upgradeModal}
onClose={() => setUpgradeModal(false)}
user={user}
issueNumber={issueNumber}
/>
{!sidebarCollapse && (alert || issueNumber >= 750) && (
<div
className={`border-t border-custom-sidebar-border-200 p-4 ${
issueNumber >= 750
? "bg-red-500/10 text-red-600"
: issueNumber >= 500
? "bg-yellow-500/10 text-yellow-600"
: "text-green-600"
}`}
>
<div className="flex items-center gap-2">
<CircularProgress progress={(issueNumber / 1024) * 100} />
<div>Free Plan</div>
{issueNumber < 750 && (
<div
className="ml-auto text-custom-text-200 cursor-pointer"
onClick={() => setAlert(false)}
>
<XMarkIcon className="h-4 w-4" />
</div>
)}
</div>
<div className="text-custom-text-200 text-xs mt-2">
This workspace has used {issueNumber} of its 1024 issues creation limit (
{((issueNumber / 1024) * 100).toFixed(0)}
%).
</div>
</div>
)}
<div
className={`flex w-full items-center justify-between gap-1 self-baseline border-t border-custom-border-200 bg-custom-sidebar-background-100 px-4 py-2 ${
className={`flex w-full items-center justify-between gap-1 self-baseline border-t border-custom-border-200 bg-custom-sidebar-background-100 py-2 px-4 ${
sidebarCollapse ? "flex-col" : ""
}`}
>
{alert || issueNumber >= 750 ? (
<button
type="button"
className={`flex items-center gap-2 rounded-md px-2.5 py-1.5 font-medium outline-none text-sm ${
issueNumber >= 750
? "bg-red-500/10 text-red-500"
: "bg-blue-500/10 text-custom-primary-100"
} ${sidebarCollapse ? "w-full justify-center" : ""}`}
title="Shortcuts"
onClick={() => setUpgradeModal(true)}
>
<ArrowUpCircleIcon className="h-4 w-4" />
{!sidebarCollapse && <span>Learn more</span>}
</button>
) : (
<button
type="button"
className={`flex items-center gap-2 rounded-md px-2.5 py-1.5 font-medium outline-none text-sm ${
issueNumber >= 750
? "bg-red-500/10 text-red-600"
: issueNumber >= 500
? "bg-yellow-500/10 text-yellow-600"
: "bg-green-500/10 text-green-600"
}
${sidebarCollapse ? "w-full justify-center" : ""}`}
title="Shortcuts"
onClick={() => setAlert(true)}
>
<CircularProgress
progress={(issueNumber / 1024) * 100 > 100 ? 100 : (issueNumber / 1024) * 100}
/>
{!sidebarCollapse && <span>Free Plan</span>}
</button>
{!sidebarCollapse && (
<div className="w-1/2 text-center rounded-md px-2.5 py-1.5 font-medium outline-none text-sm bg-green-500/10 text-green-500">
Free Plan
</div>
)}
<button
type="button"
className={`flex items-center gap-x-1 rounded-md px-2 py-2 text-xs font-medium text-custom-text-200 outline-none hover:bg-custom-background-90 hover:text-custom-text-100 ${
sidebarCollapse ? "w-full justify-center" : ""
<div
className={`flex items-center gap-1 ${
sidebarCollapse ? "flex-col justify-center" : "justify-evenly w-1/2"
}`}
onClick={() => {
const e = new KeyboardEvent("keydown", {
key: "h",
});
document.dispatchEvent(e);
}}
title="Shortcuts"
>
<RocketLaunchIcon className="h-4 w-4 text-custom-text-200" />
</button>
<button
type="button"
className={`flex items-center gap-x-1 rounded-md px-2 py-2 text-xs font-medium text-custom-text-200 outline-none hover:bg-custom-background-90 hover:text-custom-text-100 ${
sidebarCollapse ? "w-full justify-center" : ""
}`}
onClick={() => setIsNeedHelpOpen((prev) => !prev)}
title="Help"
>
<QuestionMarkCircleIcon className="h-4 w-4 text-custom-text-200" />
</button>
<button
type="button"
className="flex items-center gap-3 rounded-md px-2 py-2 text-xs font-medium text-custom-text-200 outline-none hover:bg-custom-background-90 hover:text-custom-text-100 md:hidden"
onClick={() => setSidebarActive(false)}
>
<ArrowLongLeftIcon className="h-4 w-4 flex-shrink-0 text-custom-text-200 group-hover:text-custom-text-100" />
</button>
<button
type="button"
className={`hidden items-center gap-3 rounded-md px-2 py-2 text-xs font-medium text-custom-text-200 outline-none hover:bg-custom-background-90 hover:text-custom-text-100 md:flex ${
sidebarCollapse ? "w-full justify-center" : ""
}`}
onClick={() => toggleCollapsed()}
>
<ArrowLongLeftIcon
className={`h-4 w-4 flex-shrink-0 text-custom-text-200 duration-300 group-hover:text-custom-text-100 ${
sidebarCollapse ? "rotate-180" : ""
<button
type="button"
className={`rounded-md p-1.5 text-custom-text-200 hover:text-custom-text-100 hover:bg-custom-background-90 outline-none ${
sidebarCollapse ? "w-full" : ""
}`}
/>
</button>
onClick={() => {
const e = new KeyboardEvent("keydown", {
key: "h",
});
document.dispatchEvent(e);
}}
>
<Bolt fontSize="small" />
</button>
<button
type="button"
className={`rounded-md p-1.5 text-custom-text-200 hover:text-custom-text-100 hover:bg-custom-background-90 outline-none ${
sidebarCollapse ? "w-full" : ""
}`}
onClick={() => setIsNeedHelpOpen((prev) => !prev)}
>
<HelpOutlineOutlined fontSize="small" />
</button>
<button
type="button"
className="rounded-md p-1.5 text-custom-text-200 hover:text-custom-text-100 hover:bg-custom-background-90 outline-none md:hidden"
onClick={() => setSidebarActive(false)}
>
<WestOutlined fontSize="small" />
</button>
<button
type="button"
className={`hidden md:flex rounded-md p-1.5 text-custom-text-200 hover:text-custom-text-100 hover:bg-custom-background-90 outline-none ${
sidebarCollapse ? "w-full" : ""
}`}
onClick={() => toggleCollapsed()}
>
<WestOutlined
fontSize="small"
className={`duration-300 ${sidebarCollapse ? "rotate-180" : ""}`}
/>
</button>
</div>
<div className="relative">
<Transition
@@ -36,7 +36,7 @@ export const IssuesPieChart: React.FC<Props> = ({ groupedIssues }) => (
activeInnerRadiusOffset={5}
colors={(datum) => datum.data.color}
tooltip={(datum) => (
<div className="flex items-center gap-2 rounded-md border border-custom-border-200 bg-custom-background-80 p-2 text-xs">
<div className="flex items-center gap-2 rounded-md border border-custom-border-200 bg-custom-background-90 p-2 text-xs">
<span className="text-custom-text-200 capitalize">{datum.datum.label} issues:</span>{" "}
{datum.datum.value}
</div>
@@ -59,7 +59,7 @@ export const IssuesPieChart: React.FC<Props> = ({ groupedIssues }) => (
className="h-2 w-2"
style={{ backgroundColor: STATE_GROUP_COLORS[cell.state_group] }}
/>
<div className="capitalize text-custom-text-200 text-xs">
<div className="capitalize text-custom-text-200 text-xs whitespace-nowrap">
{cell.state_group}- {cell.state_count}
</div>
</div>
@@ -3,11 +3,13 @@ import { Fragment } from "react";
import { useRouter } from "next/router";
import Link from "next/link";
// next-themes
import { useTheme } from "next-themes";
// headless ui
import { Menu, Transition } from "@headlessui/react";
// hooks
import useUser from "hooks/use-user";
import useTheme from "hooks/use-theme";
import useThemeHook from "hooks/use-theme";
import useWorkspaces from "hooks/use-workspaces";
import useToast from "hooks/use-toast";
// services
@@ -41,14 +43,15 @@ const userLinks = (workspaceSlug: string) => [
export const WorkspaceSidebarDropdown = () => {
const router = useRouter();
const { workspaceSlug } = router.query;
// fetching user details
const { user, mutateUser } = useUser();
// fetching theme context
const { collapsed: sidebarCollapse } = useTheme();
const { collapsed: sidebarCollapse } = useThemeHook();
const { setTheme } = useTheme();
const { setToastAlert } = useToast();
// fetching workspaces
const { activeWorkspace, workspaces } = useWorkspaces();
const handleWorkspaceNavigation = (workspace: IWorkspace) => {
@@ -75,6 +78,7 @@ export const WorkspaceSidebarDropdown = () => {
.then(() => {
mutateUser(undefined);
router.push("/");
setTheme("dark");
})
.catch(() =>
setToastAlert({
@@ -135,8 +139,8 @@ export const WorkspaceSidebarDropdown = () => {
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items
className="fixed left-2 z-20 mt-1 flex w-full max-w-[17rem] origin-top-left flex-col rounded-md
border border-custom-sidebar-border-200 bg-custom-sidebar-background-90 shadow-lg focus:outline-none"
className="fixed left-4 z-20 mt-1 flex flex-col w-full max-w-[17rem] origin-top-left rounded-md
border border-custom-sidebar-border-200 bg-custom-sidebar-background-90 shadow-lg outline-none"
>
<div className="flex flex-col items-start justify-start gap-3 p-3">
<div className="text-sm text-custom-sidebar-text-200">{user?.email}</div>
+37 -32
View File
@@ -1,48 +1,51 @@
import React from "react";
import { useRouter } from "next/router";
import Link from "next/link";
import { useRouter } from "next/router";
// hooks
import useTheme from "hooks/use-theme";
// components
import { Icon, Tooltip } from "components/ui";
import { NotificationPopover } from "components/notifications";
// ui
import { Tooltip } from "components/ui";
// icons
import {
BarChartRounded,
GridViewOutlined,
TaskAltOutlined,
WorkOutlineOutlined,
} from "@mui/icons-material";
const workspaceLinks = (workspaceSlug: string) => [
{
Icon: GridViewOutlined,
name: "Dashboard",
href: `/${workspaceSlug}`,
},
{
Icon: BarChartRounded,
name: "Analytics",
href: `/${workspaceSlug}/analytics`,
},
{
Icon: WorkOutlineOutlined,
name: "Projects",
href: `/${workspaceSlug}/projects`,
},
{
Icon: TaskAltOutlined,
name: "My Issues",
href: `/${workspaceSlug}/me/my-issues`,
},
];
export const WorkspaceSidebarMenu = () => {
const router = useRouter();
const { workspaceSlug } = router.query;
// theme context
const { collapsed: sidebarCollapse } = useTheme();
const workspaceLinks = (workspaceSlug: string) => [
{
icon: "grid_view",
name: "Dashboard",
href: `/${workspaceSlug}`,
},
{
icon: "bar_chart",
name: "Analytics",
href: `/${workspaceSlug}/analytics`,
},
{
icon: "work",
name: "Projects",
href: `/${workspaceSlug}/projects`,
},
{
icon: "task_alt",
name: "My Issues",
href: `/${workspaceSlug}/me/my-issues`,
},
{
icon: "settings",
name: "Settings",
href: `/${workspaceSlug}/settings`,
},
];
return (
<div className="w-full cursor-pointer space-y-2 px-4 mt-5">
{workspaceLinks(workspaceSlug as string).map((link, index) => {
@@ -67,7 +70,7 @@ export const WorkspaceSidebarMenu = () => {
: "text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80 focus:bg-custom-sidebar-background-80"
} ${sidebarCollapse ? "justify-center" : ""}`}
>
<Icon iconName={`${link.icon}`} />
{<link.Icon fontSize="small" />}
{!sidebarCollapse && link.name}
</div>
</Tooltip>
@@ -75,6 +78,8 @@ export const WorkspaceSidebarMenu = () => {
</Link>
);
})}
<NotificationPopover />
</div>
);
};
@@ -1,248 +0,0 @@
import React, { useState, useEffect } from "react";
// headless ui
import { Dialog, Transition } from "@headlessui/react";
// icons
import { XCircleIcon, RocketLaunchIcon } from "@heroicons/react/24/outline";
import { CheckCircleIcon } from "@heroicons/react/24/solid";
// ui
import { CircularProgress } from "components/ui";
// types
import type { ICurrentUserResponse, IWorkspace } from "types";
declare global {
interface Window {
supabase: any;
}
}
type Props = {
isOpen: boolean;
onClose: () => void;
user: ICurrentUserResponse | undefined;
issueNumber: number;
};
const UpgradeToProModal: React.FC<Props> = ({ isOpen, onClose, user, issueNumber }) => {
const [supabaseClient, setSupabaseClient] = useState<any>(null);
useEffect(() => {
// Create a Supabase client
if (process.env.NEXT_PUBLIC_SUPABASE_URL && process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY) {
const { createClient } = window.supabase;
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
{
auth: {
autoRefreshToken: false,
persistSession: false,
},
}
);
if (supabase) {
setSupabaseClient(supabase);
}
}
}, []);
const [isLoading, setIsLoading] = useState(false);
const handleClose = () => {
onClose();
setIsLoading(false);
};
const proFeatures = [
"Everything in free",
"Unlimited users",
"Unlimited file uploads",
"Priority Support",
"Custom Theming",
"Access to Roadmap",
"Plane AI (GPT unlimited)",
];
const [errorMessage, setErrorMessage] = useState<null | { status: String; message: string }>(
null
);
const [loader, setLoader] = useState(false);
const submitEmail = async () => {
setLoader(true);
const payload = { email: user?.email || "" };
if (supabaseClient) {
if (payload?.email) {
const emailExists = await supabaseClient
.from("web-waitlist")
.select("id,email,count")
.eq("email", payload?.email);
if (emailExists.data.length === 0) {
const emailCreation = await supabaseClient
.from("web-waitlist")
.insert([{ email: payload?.email, count: 1, last_visited: new Date() }])
.select("id,email,count");
if (emailCreation.status === 201)
setErrorMessage({ status: "success", message: "Successfully registered." });
else setErrorMessage({ status: "insert_error", message: "Insertion Error." });
} else {
const emailCountUpdate = await supabaseClient
.from("web-waitlist")
.upsert({
id: emailExists.data[0]?.id,
count: emailExists.data[0]?.count + 1,
last_visited: new Date(),
})
.select("id,email,count");
if (emailCountUpdate.status === 201)
setErrorMessage({
status: "email_already_exists",
message: "Email already exists.",
});
else setErrorMessage({ status: "update_error", message: "Update Error." });
}
} else setErrorMessage({ status: "email_required", message: "Please provide email." });
} else
setErrorMessage({
status: "supabase_error",
message: "Network error. Please try again later.",
});
setLoader(false);
};
return (
<Transition.Root show={isOpen} as={React.Fragment}>
<Dialog as="div" className="relative z-20" onClose={handleClose}>
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-custom-backdrop bg-opacity-50 transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-20 overflow-y-auto">
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative transform overflow-hidden rounded-lg border border-custom-border-200 bg-custom-background-100 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-4xl">
<div className="flex flex-wrap">
<div className="w-full md:w-3/5 p-6 flex flex-col gap-y-6">
<div className="flex gap-2">
<div
className={`font-semibold outline-none text-sm mt-1.5 ${
issueNumber >= 750
? "text-red-600"
: issueNumber >= 500
? "text-yellow-600"
: "text-green-600"
}`}
title="Shortcuts"
>
<CircularProgress
progress={
(issueNumber / 1024) * 100 > 100 ? 100 : (issueNumber / 1024) * 100
}
/>
</div>
<div className="">
<div className="font-semibold text-lg">Upgrade to pro</div>
<div className="text-custom-text-200 text-sm">
This workspace has used {issueNumber} of its 1024 issues creation limit (
{((issueNumber / 1024) * 100).toFixed(2)}%).
</div>
</div>
<div
onClick={handleClose}
className="w-5 h-5 text-custom-text-200 cursor-pointer mt-1.5 md:hidden block ml-auto"
>
<XCircleIcon />
</div>
</div>
<div className="flex gap-2 pt-6">
<div
className={`font-semibold outline-none text-sm mt-1.5 w-5 h-5 text-[#892FFF] flex-shrink-0`}
title="Shortcuts"
>
<RocketLaunchIcon />
</div>
<div className="">
<div className="font-semibold text-lg">Order summary</div>
<div className="text-custom-text-200 text-sm">
Priority support, file uploads, and access to premium features.
</div>
<div className="flex flex-wrap my-4">
{proFeatures.map((feature, index) => (
<div key={index} className="w-1/2 py-2 flex gap-2 my-1.5">
<div className="w-5 h-5 mt-0.5 text-green-600">
<CheckCircleIcon />
</div>
<div>{feature}</div>
</div>
))}
</div>
</div>
</div>
</div>
<div className="w-full md:w-2/5 bg-custom-background-90 p-6 flex flex-col">
<div className="flex justify-between items-center">
<div className="font-semibold text-lg">Summary</div>
<div
onClick={handleClose}
className="w-5 h-5 text-custom-text-200 cursor-pointer mt-1.5 hidden md:block"
>
<XCircleIcon />
</div>
</div>
<div className="text-custom-text-200 text-sm mt-4">
Plane application is currently in dev-mode. We will soon introduce Pro plans
once general availability has been established. Stay tuned for more updates.
In the meantime, Plane remains free and unrestricted.
<br /> <br />
We{"'"}ll ensure a smooth transition from the community version to the Pro
plan for you.
</div>
<button
disabled={loader}
onClick={() => submitEmail()}
type="button"
className="mt-5 md:mt-auto whitespace-nowrap max-w-min items-center gap-x-1 rounded-md px-3 py-2 font-medium outline-none text-sm bg-custom-primary-100 text-white"
>
{loader ? "Loading.." : " Join waitlist"}
</button>
{errorMessage && (
<div
className={`mt-1 text-sm ${
errorMessage && errorMessage?.status === "success"
? "text-green-500"
: " text-red-500"
}`}
>
{errorMessage?.message}
</div>
)}
</div>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
);
};
export default UpgradeToProModal;
+22 -1
View File
@@ -1,4 +1,4 @@
import { IAnalyticsParams, IJiraMetadata } from "types";
import { IAnalyticsParams, IJiraMetadata, INotificationParams } from "types";
const paramsToKey = (params: any) => {
const { state, priority, assignees, created_by, labels, target_date, sub_issue } = params;
@@ -223,3 +223,24 @@ export const DEFAULT_ANALYTICS = (workspaceSlug: string, params?: Partial<IAnaly
`DEFAULT_ANALYTICS_${workspaceSlug.toUpperCase()}_${params?.project?.toString()}_${
params?.cycle
}_${params?.module}`;
// notifications
export const USER_WORKSPACE_NOTIFICATIONS = (
workspaceSlug: string,
params: INotificationParams
) => {
const { type, snoozed, archived, read } = params;
return `USER_WORKSPACE_NOTIFICATIONS_${workspaceSlug.toUpperCase()}_TYPE_${(
type ?? "assigned"
).toUpperCase()}_SNOOZED_${snoozed}_ARCHIVED_${archived}_READ_${read}`;
};
export const USER_WORKSPACE_NOTIFICATIONS_DETAILS = (
workspaceSlug: string,
notificationId: string
) =>
`USER_WORKSPACE_NOTIFICATIONS_DETAILS_${workspaceSlug.toUpperCase()}_${notificationId.toUpperCase()}`;
export const UNREAD_NOTIFICATIONS_COUNT = (workspaceSlug: string) =>
`UNREAD_NOTIFICATIONS_COUNT_${workspaceSlug.toUpperCase()}`;
+2 -2
View File
@@ -17,12 +17,12 @@ export const CHARTS_THEME: Theme = {
background: "rgb(var(--color-background-80))",
color: "rgb(var(--color-text-200))",
fontSize: "0.8rem",
border: "1px solid rgb(var(--color-background-80))",
border: "1px solid rgb(var(--color-border-300))",
},
},
grid: {
line: {
stroke: "rgb(var(--color-background-80))",
stroke: "rgb(var(--color-border-100))",
},
},
};
+6 -1
View File
@@ -94,7 +94,12 @@ export const ThemeContextProvider: React.FC<{ children: React.ReactNode }> = ({
const theme = localStorage.getItem("theme");
if (theme && theme === "custom") {
if (user && user.theme.palette) {
applyTheme(user.theme.palette, user.theme.darkPalette);
applyTheme(
user.theme.palette !== ",,,,"
? user.theme.palette
: "#0d101b,#c5c5c5,#3f76ff,#0d101b,#c5c5c5",
user.theme.darkPalette
);
}
}
}, [user]);
+130 -3
View File
@@ -92,7 +92,7 @@ export const timeAgo = (time: any) => {
list_choice = 2;
}
var i = 0,
format;
format: any[];
while ((format = time_formats[i++]))
if (seconds < format[0]) {
if (typeof format[2] == "string") return format[list_choice];
@@ -101,6 +101,35 @@ export const timeAgo = (time: any) => {
return time;
};
export const formatDateDistance = (date: string | Date) => {
const today = new Date();
const eventDate = new Date(date);
const timeDiff = Math.abs(eventDate.getTime() - today.getTime());
const days = Math.ceil(timeDiff / (1000 * 3600 * 24));
if (days < 1) {
const hours = Math.ceil(timeDiff / (1000 * 3600));
if (hours < 1) {
const minutes = Math.ceil(timeDiff / (1000 * 60));
if (minutes < 1) {
return "Just now";
} else {
return `${minutes}m`;
}
} else {
return `${hours}h`;
}
} else if (days < 7) {
return `${days}d`;
} else if (days < 30) {
return `${Math.floor(days / 7)}w`;
} else if (days < 365) {
return `${Math.floor(days / 30)}m`;
} else {
return `${Math.floor(days / 365)}y`;
}
};
export const getDateRangeStatus = (
startDate: string | null | undefined,
endDate: string | null | undefined
@@ -188,7 +217,7 @@ export const render12HourFormatTime = (date: string | Date): string => {
if (hours > 12) hours -= 12;
}
return hours + ":" + minutes + " " + period;
return hours + ":" + (minutes < 10 ? `0${minutes}` : minutes) + " " + period;
};
export const render24HourFormatTime = (date: string | Date): string => {
@@ -214,7 +243,7 @@ export const isDateGreaterThanToday = (dateStr: string) => {
return date > today;
};
export const renderLongDateFormat = (dateString: string) => {
export const renderLongDateFormat = (dateString: string | Date) => {
const date = new Date(dateString);
const day = date.getDate();
const year = date.getFullYear();
@@ -247,3 +276,101 @@ export const renderLongDateFormat = (dateString: string) => {
}
return `${day}${suffix} ${monthName} ${year}`;
};
/**
*
* @returns {Array} Array of time objects with label and value as keys
*/
export const getTimestampAfterCurrentTime = (): Array<{
label: string;
value: Date;
}> => {
const current = new Date();
const time = [];
for (let i = 0; i < 24; i++) {
const newTime = new Date(current.getTime() + i * 60 * 60 * 1000);
time.push({
label: newTime.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }),
value: newTime,
});
}
return time;
};
/**
* @returns {Array} Array of date objects with label and value as keys
* @description Returns an array of date objects starting from current date to 7 days after
*/
export const getDatesAfterCurrentDate = (): Array<{
label: string;
value: Date;
}> => {
const current = new Date();
const date = [];
for (let i = 0; i < 7; i++) {
const newDate = new Date(Math.round(current.getTime() / (30 * 60 * 1000)) * 30 * 60 * 1000);
date.push({
label: newDate.toLocaleDateString([], {
day: "numeric",
month: "long",
year: "numeric",
}),
value: newDate,
});
}
return date;
};
/**
* @returns {boolean} true if date is valid
* @description Returns true if date is valid
* @param {string} date
* @example checkIfStringIsDate("2021-01-01") // true
* @example checkIfStringIsDate("2021-01-32") // false
*/
export const checkIfStringIsDate = (date: string): boolean =>
new Date(date).toString() !== "Invalid Date";
// return an array of dates starting from 12:00 to 23:30 with 30 minutes interval as dates
export const getDatesWith30MinutesInterval = (): Array<Date> => {
const dates = [];
const current = new Date();
for (let i = 0; i < 24; i++) {
const newDate = new Date(current.getTime() + i * 60 * 60 * 1000);
dates.push(newDate);
}
return dates;
};
export const getAllTimeIn30MinutesInterval = (): Array<{
label: string;
value: string;
}> => [
{ label: "12:00", value: "12:00" },
{ label: "12:30", value: "12:30" },
{ label: "01:00", value: "01:00" },
{ label: "01:30", value: "01:30" },
{ label: "02:00", value: "02:00" },
{ label: "02:30", value: "02:30" },
{ label: "03:00", value: "03:00" },
{ label: "03:30", value: "03:30" },
{ label: "04:00", value: "04:00" },
{ label: "04:30", value: "04:30" },
{ label: "05:00", value: "05:00" },
{ label: "05:30", value: "05:30" },
{ label: "06:00", value: "06:00" },
{ label: "06:30", value: "06:30" },
{ label: "07:00", value: "07:00" },
{ label: "07:30", value: "07:30" },
{ label: "08:00", value: "08:00" },
{ label: "08:30", value: "08:30" },
{ label: "09:00", value: "09:00" },
{ label: "09:30", value: "09:30" },
{ label: "10:00", value: "10:00" },
{ label: "10:30", value: "10:30" },
{ label: "11:00", value: "11:00" },
{ label: "11:30", value: "11:30" },
];
+33 -39
View File
@@ -49,45 +49,6 @@ export const copyTextToClipboard = async (text: string) => {
await navigator.clipboard.writeText(text);
};
const wordsVector = (str: string) => {
const words = str.split(" ");
const vector: any = {};
for (let i = 0; i < words.length; i++) {
const word = words[i];
if (vector[word]) {
vector[word] += 1;
} else {
vector[word] = 1;
}
}
return vector;
};
export const cosineSimilarity = (a: string, b: string) => {
const vectorA = wordsVector(a.trim());
const vectorB = wordsVector(b.trim());
const vectorAKeys = Object.keys(vectorA);
const vectorBKeys = Object.keys(vectorB);
const union = vectorAKeys.concat(vectorBKeys);
let dotProduct = 0;
let magnitudeA = 0;
let magnitudeB = 0;
for (let i = 0; i < union.length; i++) {
const key = union[i];
const valueA = vectorA[key] || 0;
const valueB = vectorB[key] || 0;
dotProduct += valueA * valueB;
magnitudeA += valueA * valueA;
magnitudeB += valueB * valueB;
}
return dotProduct / Math.sqrt(magnitudeA * magnitudeB);
};
export const generateRandomColor = (string: string): string => {
if (!string) return "rgb(var(--color-primary-100))";
@@ -118,3 +79,36 @@ export const getFirstCharacters = (str: string) => {
return words[0].charAt(0) + words[1].charAt(0);
}
};
/**
* @description: This function will remove all the HTML tags from the string
* @param {string} html
* @return {string}
* @example:
* const html = "<p>Some text</p>";
* const text = stripHTML(html);
* console.log(text); // Some text
*/
export const stripHTML = (html: string) => {
const tmp = document.createElement("DIV");
tmp.innerHTML = html;
return tmp.textContent || tmp.innerText || "";
};
/**
* @description: This function return number count in string if number is more than 100 then it will return 99+
* @param {number} number
* @return {string}
* @example:
* const number = 100;
* const text = getNumberCount(number);
* console.log(text); // 99+
*/
export const getNumberCount = (number: number): string => {
if (number > 99) {
return "99+";
}
return number.toString();
};
+1 -1
View File
@@ -96,7 +96,7 @@ export const applyTheme = (palette: string, isDarkPalette: boolean) => {
?.style.setProperty(`--color-sidebar-text-${shade}`, sidebarTextRgbValues);
if (i >= 100 && i <= 400) {
const borderShade = (shade + 100) as keyof TShades;
const borderShade = i === 100 ? 70 : i === 200 ? 80 : i === 300 ? 90 : 100;
document
.querySelector<HTMLElement>("[data-theme='custom']")
@@ -0,0 +1,85 @@
import { useCallback } from "react";
import useSWR from "swr";
// hooks
import useUserAuth from "hooks/use-user-auth";
// services
import userNotificationServices from "services/notifications.service";
const useUserIssueNotificationSubscription = (
workspaceSlug?: string | string[] | null,
projectId?: string | string[] | null,
issueId?: string | string[] | null
) => {
const { user } = useUserAuth();
const { data, error, mutate } = useSWR(
workspaceSlug && projectId && issueId
? `SUBSCRIPTION_STATUE_${workspaceSlug}_${projectId}_${issueId}`
: null,
workspaceSlug && projectId && issueId
? () =>
userNotificationServices.getIssueNotificationSubscriptionStatus(
workspaceSlug.toString(),
projectId.toString(),
issueId.toString()
)
: null
);
const handleUnsubscribe = useCallback(() => {
if (!workspaceSlug || !projectId || !issueId) return;
mutate(
{
subscribed: false,
},
false
);
userNotificationServices
.unsubscribeFromIssueNotifications(
workspaceSlug.toString(),
projectId.toString(),
issueId.toString()
)
.then(() => {
mutate({
subscribed: false,
});
});
}, [workspaceSlug, projectId, issueId, mutate]);
const handleSubscribe = useCallback(() => {
if (!workspaceSlug || !projectId || !issueId || !user) return;
mutate(
{
subscribed: true,
},
false
);
userNotificationServices
.subscribeToIssueNotifications(
workspaceSlug.toString(),
projectId.toString(),
issueId.toString()
)
.then(() => {
mutate({
subscribed: true,
});
});
}, [workspaceSlug, projectId, issueId, mutate, user]);
return {
loading: !data && !error,
subscribed: data?.subscribed,
handleSubscribe,
handleUnsubscribe,
} as const;
};
export default useUserIssueNotificationSubscription;
+199
View File
@@ -0,0 +1,199 @@
import { useState } from "react";
import { useRouter } from "next/router";
// swr
import useSWR from "swr";
// services
import userNotificationServices from "services/notifications.service";
// fetch-keys
import { UNREAD_NOTIFICATIONS_COUNT, USER_WORKSPACE_NOTIFICATIONS } from "constants/fetch-keys";
// type
import type { NotificationType } from "types";
const useUserNotification = () => {
const router = useRouter();
const { workspaceSlug } = router.query;
const [snoozed, setSnoozed] = useState<boolean>(false);
const [archived, setArchived] = useState<boolean>(false);
const [readNotification, setReadNotification] = useState<boolean>(false);
const [selectedNotificationForSnooze, setSelectedNotificationForSnooze] = useState<string | null>(
null
);
const [selectedTab, setSelectedTab] = useState<NotificationType>("assigned");
const params = {
type: snoozed || archived || readNotification ? undefined : selectedTab,
snoozed,
archived,
read: !readNotification ? undefined : false,
};
const { data: notifications, mutate: notificationsMutate } = useSWR(
workspaceSlug ? USER_WORKSPACE_NOTIFICATIONS(workspaceSlug.toString(), params) : null,
workspaceSlug
? () => userNotificationServices.getUserNotifications(workspaceSlug.toString(), params)
: null
);
const { data: notificationCount, mutate: mutateNotificationCount } = useSWR(
workspaceSlug ? UNREAD_NOTIFICATIONS_COUNT(workspaceSlug.toString()) : null,
() =>
workspaceSlug
? userNotificationServices.getUnreadNotificationsCount(workspaceSlug.toString())
: null
);
const markNotificationReadStatus = async (notificationId: string) => {
if (!workspaceSlug) return;
const isRead =
notifications?.find((notification) => notification.id === notificationId)?.read_at !== null;
if (isRead) {
notificationsMutate(
(prev) =>
prev?.map((prevNotification) => {
if (prevNotification.id === notificationId) {
return {
...prevNotification,
read_at: null,
};
}
return prevNotification;
}),
false
);
await userNotificationServices
.markUserNotificationAsUnread(workspaceSlug.toString(), notificationId)
.catch(() => {
throw new Error("Something went wrong");
})
.finally(() => {
notificationsMutate();
mutateNotificationCount();
});
} else {
notificationsMutate(
(prev) =>
prev?.map((prevNotification) => {
if (prevNotification.id === notificationId) {
return {
...prevNotification,
read_at: new Date(),
};
}
return prevNotification;
}),
false
);
await userNotificationServices
.markUserNotificationAsRead(workspaceSlug.toString(), notificationId)
.catch(() => {
throw new Error("Something went wrong");
})
.finally(() => {
notificationsMutate();
mutateNotificationCount();
});
}
};
const markNotificationArchivedStatus = async (notificationId: string) => {
if (!workspaceSlug) return;
const isArchived =
notifications?.find((notification) => notification.id === notificationId)?.archived_at !==
null;
if (isArchived) {
await userNotificationServices
.markUserNotificationAsUnarchived(workspaceSlug.toString(), notificationId)
.catch(() => {
throw new Error("Something went wrong");
})
.finally(() => {
notificationsMutate();
mutateNotificationCount();
});
} else {
notificationsMutate(
(prev) => prev?.filter((prevNotification) => prevNotification.id !== notificationId),
false
);
await userNotificationServices
.markUserNotificationAsArchived(workspaceSlug.toString(), notificationId)
.catch(() => {
throw new Error("Something went wrong");
})
.finally(() => {
notificationsMutate();
mutateNotificationCount();
});
}
};
const markSnoozeNotification = async (notificationId: string, dateTime?: Date) => {
if (!workspaceSlug) return;
const isSnoozed =
notifications?.find((notification) => notification.id === notificationId)?.snoozed_till !==
null;
if (isSnoozed) {
await userNotificationServices
.patchUserNotification(workspaceSlug.toString(), notificationId, {
snoozed_till: null,
})
.finally(() => {
notificationsMutate();
mutateNotificationCount();
});
} else {
notificationsMutate(
(prevData) => prevData?.filter((prev) => prev.id !== notificationId) || [],
false
);
await userNotificationServices
.patchUserNotification(workspaceSlug.toString(), notificationId, {
snoozed_till: dateTime,
})
.catch(() => {
new Error("Something went wrong");
})
.finally(() => {
notificationsMutate();
mutateNotificationCount();
});
}
};
return {
notifications,
notificationsMutate,
markNotificationReadStatus,
markNotificationArchivedStatus,
markSnoozeNotification,
snoozed,
setSnoozed,
archived,
setArchived,
readNotification,
setReadNotification,
selectedNotificationForSnooze,
setSelectedNotificationForSnooze,
selectedTab,
setSelectedTab,
totalNotificationCount: notificationCount
? notificationCount.created_issues +
notificationCount.watching_issues +
notificationCount.my_issues
: null,
notificationCount,
mutateNotificationCount,
};
};
export default useUserNotification;
+2
View File
@@ -14,6 +14,8 @@
"@headlessui/react": "^1.7.3",
"@heroicons/react": "^2.0.12",
"@jitsu/nextjs": "^3.1.5",
"@mui/icons-material": "^5.14.1",
"@mui/material": "^5.14.1",
"@nivo/bar": "0.80.0",
"@nivo/calendar": "0.80.0",
"@nivo/core": "0.80.0",
+39 -22
View File
@@ -2,9 +2,12 @@ import React, { useEffect, useState } from "react";
import { useRouter } from "next/router";
import Image from "next/image";
import Link from "next/link";
import useSWR, { mutate } from "swr";
// next-themes
import { useTheme } from "next-themes";
// layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
// services
@@ -22,8 +25,12 @@ import {
import { TourRoot } from "components/onboarding";
// ui
import { PrimaryButton, ProductUpdatesModal } from "components/ui";
// icons
import { BoltOutlined, GridViewOutlined } from "@mui/icons-material";
// images
import emptyDashboard from "public/empty-state/dashboard.svg";
import githubBlackImage from "/public/logos/github-black.png";
import githubWhiteImage from "/public/logos/github-white.png";
// helpers
import { render12HourFormatTime, renderShortDate } from "helpers/date-time.helper";
// types
@@ -41,6 +48,8 @@ const WorkspacePage: NextPage = () => {
const router = useRouter();
const { workspaceSlug } = router.query;
const { theme } = useTheme();
const { user } = useUser();
const { projects } = useProjects();
@@ -60,7 +69,36 @@ const WorkspacePage: NextPage = () => {
}, [month, workspaceSlug]);
return (
<WorkspaceAuthorizationLayout noHeader>
<WorkspaceAuthorizationLayout
left={
<div className="flex items-center gap-2 pl-3">
<GridViewOutlined fontSize="small" />
Dashboard
</div>
}
right={
<div className="flex items-center gap-3 px-3">
<button
onClick={() => setIsProductUpdatesModalOpen(true)}
className="flex items-center gap-1.5 bg-custom-background-80 text-xs font-medium py-1.5 px-3 rounded"
>
<BoltOutlined fontSize="small" className="-my-1" />
What{"'"}s New?
</button>
<Link href="https://github.com/makeplane/plane" target="_blank" rel="noopener noreferrer">
<a className="flex items-center gap-1.5 bg-custom-background-80 text-xs font-medium py-1.5 px-3 rounded">
<Image
src={theme === "dark" ? githubWhiteImage : githubBlackImage}
height={16}
width={16}
alt="GitHub Logo"
/>
Star us on GitHub
</a>
</Link>
</div>
}
>
{isProductUpdatesModalOpen && (
<ProductUpdatesModal
isOpen={isProductUpdatesModalOpen}
@@ -93,27 +131,6 @@ const WorkspacePage: NextPage = () => {
projects.length > 0 ? (
<div className="p-8">
<div className="flex flex-col gap-8">
<div className="text-custom-text-200 flex flex-col justify-between gap-x-2 gap-y-6 rounded-lg border border-custom-border-200 bg-custom-background-100 px-4 py-6 md:flex-row md:items-center md:py-3">
<p className="font-medium text-custom-text-100">
Plane is open source, support us by starring us on GitHub.
</p>
<div className="flex items-center gap-2">
<button
onClick={() => setIsProductUpdatesModalOpen(true)}
className="rounded-md border-2 border-custom-border-200 px-3 py-1.5 text-sm font-medium hover:text-custom-text-100 duration-300"
>
What{"'"}s New?
</button>
<a
href="https://github.com/makeplane/plane"
target="_blank"
className="rounded-md border-2 border-custom-border-200 px-3 py-1.5 text-sm font-medium hover:text-custom-text-100 duration-300"
rel="noopener noreferrer"
>
Star us on GitHub
</a>
</div>
</div>
<IssuesStats data={workspaceDashboardData} />
<div className="grid grid-cols-1 gap-8 lg:grid-cols-2">
<IssuesList issues={workspaceDashboardData?.overdue_issues} type="overdue" />
@@ -72,7 +72,8 @@ const MyIssuesPage: NextPage = () => {
<h4 className="text-sm text-custom-text-200">Properties</h4>
<div className="flex flex-wrap items-center gap-2">
{Object.keys(properties).map((key) => {
if (key === "estimate") return null;
if (key === "estimate" || key === "created_on" || key === "updated_on")
return null;
return (
<button
@@ -22,7 +22,22 @@ const ProfilePreferences = () => {
useEffect(() => {
if (theme === "custom") {
if (myProfile?.theme.palette) setPreLoadedData(myProfile.theme);
if (myProfile?.theme.palette)
setPreLoadedData({
background: myProfile.theme.background !== "" ? myProfile.theme.background : "#0d101b",
text: myProfile.theme.text !== "" ? myProfile.theme.text : "#c5c5c5",
primary: myProfile.theme.primary !== "" ? myProfile.theme.primary : "#3f76ff",
sidebarBackground:
myProfile.theme.sidebarBackground !== ""
? myProfile.theme.sidebarBackground
: "#0d101b",
sidebarText: myProfile.theme.sidebarText !== "" ? myProfile.theme.sidebarText : "#c5c5c5",
darkPalette: false,
palette:
myProfile.theme.palette !== ",,,,"
? myProfile.theme.palette
: "#0d101b,#c5c5c5,#3f76ff,#0d101b,#c5c5c5",
});
if (!customThemeSelectorOptions) setCustomThemeSelectorOptions(true);
}
}, [myProfile, theme, customThemeSelectorOptions]);
@@ -1,4 +1,4 @@
import React, { useCallback, useEffect } from "react";
import React, { useCallback, useEffect, useState } from "react";
import { useRouter } from "next/router";
@@ -39,6 +39,8 @@ const defaultValues = {
};
const ArchivedIssueDetailsPage: NextPage = () => {
const [isRestoring, setIsRestoring] = useState(false);
const router = useRouter();
const { workspaceSlug, projectId, archivedIssueId } = router.query;
@@ -117,6 +119,8 @@ const ArchivedIssueDetailsPage: NextPage = () => {
const handleUnArchive = async () => {
if (!workspaceSlug || !projectId || !archivedIssueId) return;
setIsRestoring(true);
await issuesService
.unarchiveIssue(workspaceSlug as string, projectId as string, archivedIssueId as string)
.then(() => {
@@ -133,7 +137,8 @@ const ArchivedIssueDetailsPage: NextPage = () => {
title: "Error!",
message: "Something went wrong. Please try again.",
});
});
})
.finally(() => setIsRestoring(false));
};
return (
@@ -153,8 +158,8 @@ const ArchivedIssueDetailsPage: NextPage = () => {
}
>
{issueDetails && projectId ? (
<div className="flex h-full">
<div className="w-2/3 space-y-2 p-5">
<div className="flex h-full overflow-hidden">
<div className="w-2/3 h-full overflow-y-auto space-y-2 divide-y-2 divide-custom-border-300 p-5">
{issueDetails.archived_at && (
<div className="flex items-center justify-between gap-2 px-2.5 py-2 text-sm border rounded-md text-custom-text-200 border-custom-border-200 bg-custom-background-90">
<div className="flex gap-2 items-center">
@@ -164,13 +169,14 @@ const ArchivedIssueDetailsPage: NextPage = () => {
<button
className="flex items-center gap-2 p-1.5 text-sm rounded-md border border-custom-border-200"
onClick={handleUnArchive}
disabled={isRestoring}
>
<Icon iconName="history" />
<p>Restore Issue</p>
<span>{isRestoring ? "Restoring..." : "Restore Issue"}</span>
</button>
</div>
)}
<div className="space-y-5 divide-y-2 divide-custom-border-200 opacity-60">
<div className="space-y-5 divide-y-2 divide-custom-border-200 opacity-60 pointer-events-none">
<IssueMainContent
issueDetails={issueDetails}
submitChanges={submitChanges}
@@ -178,7 +184,7 @@ const ArchivedIssueDetailsPage: NextPage = () => {
/>
</div>
</div>
<div className="w-1/3 space-y-5 border-l border-custom-border-200 p-5">
<div className="w-1/3 h-full space-y-5 border-l border-custom-border-300 p-5 overflow-hidden">
<IssueDetailsSidebar
control={control}
issueDetail={issueDetails}
@@ -120,10 +120,10 @@ const IssueDetailsPage: NextPage = () => {
>
{issueDetails && projectId ? (
<div className="flex h-full overflow-hidden">
<div className="w-2/3 h-full overflow-y-auto space-y-5 divide-y-2 divide-custom-border-200 p-5">
<div className="w-2/3 h-full overflow-y-auto space-y-5 divide-y-2 divide-custom-border-300 p-5">
<IssueMainContent issueDetails={issueDetails} submitChanges={submitChanges} />
</div>
<div className="w-1/3 h-full space-y-5 border-l border-custom-border-200 p-5 overflow-hidden">
<div className="w-1/3 h-full space-y-5 border-l border-custom-border-300 p-5 overflow-hidden">
<IssueDetailsSidebar
control={control}
issueDetail={issueDetails}
@@ -553,6 +553,11 @@ const SinglePage: NextPage = () => {
handleClose={() => setLabelModal(false)}
projectId={projectId}
user={user}
onSuccess={(response) => {
partialUpdatePage({
labels_list: [...(pageDetails.labels ?? []), response.id],
});
}}
/>
)}
</>
@@ -44,28 +44,18 @@ const BillingSettings: NextPage = () => {
<section className="space-y-8">
<div>
<h3 className="text-2xl font-semibold leading-6">Billing & Plans</h3>
<p className="mt-4 text-sm text-custom-text-200">[Free launch preview] plan Pro</p>
<p className="mt-4 text-sm text-custom-text-200">Free launch preview</p>
</div>
<div className="space-y-8 md:w-2/3">
<div>
<div className="w-80 rounded-md border border-custom-border-200 bg-custom-background-100 p-4 text-center">
<h4 className="text-md mb-1 leading-6">Payment due</h4>
<h2 className="text-3xl font-extrabold">--</h2>
</div>
</div>
<div>
<h4 className="text-md mb-1 leading-6">Current plan</h4>
<p className="mb-3 text-sm text-custom-text-200">
You are currently using the free plan
</p>
<a href="https://plane.so/pricing" target="_blank" rel="noreferrer">
<SecondaryButton outline>View Plans and Upgrade</SecondaryButton>
<SecondaryButton outline>View Plans</SecondaryButton>
</a>
</div>
<div>
<h4 className="text-md mb-1 leading-6">Billing history</h4>
<p className="mb-3 text-sm text-custom-text-200">There are no invoices to display</p>
</div>
</div>
</section>
</div>
@@ -207,7 +207,10 @@ const WorkspaceSettings: NextPage = () => {
{isImageUploading ? "Uploading..." : "Upload"}
</SecondaryButton>
{activeWorkspace.logo && activeWorkspace.logo !== "" && (
<DangerButton onClick={() => handleDelete(activeWorkspace.logo)}>
<DangerButton
onClick={() => handleDelete(activeWorkspace.logo)}
loading={isImageRemoving}
>
{isImageRemoving ? "Removing..." : "Remove"}
</DangerButton>
)}
@@ -276,8 +279,8 @@ const WorkspaceSettings: NextPage = () => {
</div>
<div className="grid grid-cols-12 gap-4 sm:gap-16">
<div className="col-span-12 sm:col-span-6">
<h4 className="text-lg font-semibold">Company Size</h4>
<p className="text-sm text-custom-text-200">How big is your company?</p>
<h4 className="text-lg font-semibold">Organization Size</h4>
<p className="text-sm text-custom-text-200">What size is your organization?</p>
</div>
<div className="col-span-12 sm:col-span-6">
<Controller
@@ -287,7 +290,10 @@ const WorkspaceSettings: NextPage = () => {
<CustomSelect
value={value}
onChange={onChange}
label={ORGANIZATION_SIZE.find((c) => c === value) ?? "Select company size"}
label={
ORGANIZATION_SIZE.find((c) => c === value) ?? "Select organization size"
}
width="w-full"
input
>
{ORGANIZATION_SIZE?.map((item) => (
+1 -1
View File
@@ -45,7 +45,7 @@ Router.events.on("routeChangeComplete", NProgress.done);
function MyApp({ Component, pageProps }: AppProps) {
return (
// <UserProvider>
<ThemeProvider themes={THEMES} defaultTheme="light">
<ThemeProvider themes={THEMES} defaultTheme="dark">
<ToastContextProvider>
<ThemeContextProvider>
<CrispWithNoSSR />
+3 -3
View File
@@ -138,18 +138,18 @@ const HomePage: NextPage = () => {
</div>
</div>
</>
<div className="grid place-items-center h-full overflow-y-auto px-7">
<div className="grid place-items-center h-full overflow-y-auto py-5 px-7">
<div>
{parseInt(process.env.NEXT_PUBLIC_ENABLE_OAUTH || "0") ? (
<>
<h1 className="text-center text-2xl sm:text-2.5xl font-semibold text-custom-text-100">
Sign in to Plane
</h1>
<div className="divide-y divide-custom-border-200">
<div className="flex flex-col divide-y divide-custom-border-200">
<div className="pb-7">
<EmailCodeForm handleSignIn={handleEmailCodeSignIn} />
</div>
<div className="space-y-4 pt-7 w-full sm:w-[360px] mx-auto">
<div className="flex flex-col items-center justify-center gap-4 pt-7 sm:w-[360px] mx-auto overflow-hidden">
<GoogleLoginButton handleSignIn={handleGoogleSignIn} />
<GithubLoginButton handleSignIn={handleGitHubSignIn} />
</div>
+82 -80
View File
@@ -105,91 +105,93 @@ const OnBoard: NextPage = () => {
{user?.email}
</div>
</div>
{invitations && invitations.length > 0 ? (
<div className="relative flex justify-center sm:justify-start sm:items-center h-full px-8 pb-8 sm:p-0 sm:pr-[8.33%] sm:w-10/12 md:w-9/12 lg:w-4/5">
<div className="w-full space-y-10">
<h5 className="text-lg">We see that someone has invited you to</h5>
<h4 className="text-2xl font-semibold">Join a workspace</h4>
<div className="md:w-3/5 space-y-4">
{invitations.map((invitation) => {
const isSelected = invitationsRespond.includes(invitation.id);
{invitations ? (
invitations.length > 0 ? (
<div className="relative flex justify-center sm:justify-start sm:items-center h-full px-8 pb-8 sm:p-0 sm:pr-[8.33%] sm:w-10/12 md:w-9/12 lg:w-4/5">
<div className="w-full space-y-10">
<h5 className="text-lg">We see that someone has invited you to</h5>
<h4 className="text-2xl font-semibold">Join a workspace</h4>
<div className="max-h-[37vh] md:w-3/5 space-y-4 overflow-y-auto">
{invitations.map((invitation) => {
const isSelected = invitationsRespond.includes(invitation.id);
return (
<div
key={invitation.id}
className={`flex cursor-pointer items-center gap-2 border py-5 px-3.5 rounded ${
isSelected
? "border-custom-primary-100"
: "border-custom-border-200 hover:bg-custom-background-80"
}`}
onClick={() =>
handleInvitation(invitation, isSelected ? "withdraw" : "accepted")
}
>
<div className="flex-shrink-0">
<div className="grid place-items-center h-9 w-9 rounded">
{invitation.workspace.logo && invitation.workspace.logo !== "" ? (
<img
src={invitation.workspace.logo}
height="100%"
width="100%"
className="rounded"
alt={invitation.workspace.name}
/>
) : (
<span className="grid place-items-center h-9 w-9 py-1.5 px-3 rounded bg-gray-700 uppercase text-white">
{invitation.workspace.name[0]}
</span>
)}
</div>
</div>
<div className="min-w-0 flex-1">
<div className="text-sm font-medium">
{truncateText(invitation.workspace.name, 30)}
</div>
<p className="text-xs text-custom-text-200">{ROLE[invitation.role]}</p>
</div>
<span
className={`flex-shrink-0 ${
isSelected ? "text-custom-primary-100" : "text-custom-text-200"
return (
<div
key={invitation.id}
className={`flex cursor-pointer items-center gap-2 border py-5 px-3.5 rounded ${
isSelected
? "border-custom-primary-100"
: "border-custom-border-200 hover:bg-custom-background-80"
}`}
onClick={() =>
handleInvitation(invitation, isSelected ? "withdraw" : "accepted")
}
>
<CheckCircleIcon className="h-5 w-5" />
</span>
</div>
);
})}
</div>
<div className="flex items-center gap-3">
<PrimaryButton
type="submit"
size="md"
onClick={submitInvitations}
disabled={isJoiningWorkspaces || invitationsRespond.length === 0}
>
Accept & Join
</PrimaryButton>
<Link href="/">
<a>
<SecondaryButton size="md" outline>
Go Home
</SecondaryButton>
</a>
</Link>
<div className="flex-shrink-0">
<div className="grid place-items-center h-9 w-9 rounded">
{invitation.workspace.logo && invitation.workspace.logo !== "" ? (
<img
src={invitation.workspace.logo}
height="100%"
width="100%"
className="rounded"
alt={invitation.workspace.name}
/>
) : (
<span className="grid place-items-center h-9 w-9 py-1.5 px-3 rounded bg-gray-700 uppercase text-white">
{invitation.workspace.name[0]}
</span>
)}
</div>
</div>
<div className="min-w-0 flex-1">
<div className="text-sm font-medium">
{truncateText(invitation.workspace.name, 30)}
</div>
<p className="text-xs text-custom-text-200">{ROLE[invitation.role]}</p>
</div>
<span
className={`flex-shrink-0 ${
isSelected ? "text-custom-primary-100" : "text-custom-text-200"
}`}
>
<CheckCircleIcon className="h-5 w-5" />
</span>
</div>
);
})}
</div>
<div className="flex items-center gap-3">
<PrimaryButton
type="submit"
size="md"
onClick={submitInvitations}
disabled={isJoiningWorkspaces || invitationsRespond.length === 0}
>
Accept & Join
</PrimaryButton>
<Link href="/">
<a>
<SecondaryButton size="md" outline>
Go Home
</SecondaryButton>
</a>
</Link>
</div>
</div>
</div>
</div>
) : (
<div className="fixed top-0 left-0 h-full w-full grid place-items-center">
<EmptyState
title="No pending invites"
description="You can see here if someone invites you to a workspace."
image={emptyInvitation}
buttonText="Back to Dashboard"
onClick={() => router.push("/")}
/>
</div>
)}
) : (
<div className="fixed top-0 left-0 h-full w-full grid place-items-center">
<EmptyState
title="No pending invites"
description="You can see here if someone invites you to a workspace."
image={emptyInvitation}
buttonText="Back to Dashboard"
onClick={() => router.push("/")}
/>
</div>
)
) : null}
</div>
</DefaultLayout>
</UserAuthorizationLayout>
+8 -4
View File
@@ -100,7 +100,7 @@ const ResetPasswordPage: NextPage = () => {
</div>
</div>
</>
<div className="grid place-items-center h-full w-full overflow-y-auto px-7">
<div className="grid place-items-center h-full w-full overflow-y-auto py-5 px-7">
<div className="w-full">
<h1 className="text-center text-2xl sm:text-2.5xl font-semibold text-custom-text-100">
Reset your password
@@ -121,7 +121,7 @@ const ResetPasswordPage: NextPage = () => {
}}
error={errors.password}
placeholder="Enter new password..."
className="border-custom-border-300"
className="border-custom-border-300 h-[46px]"
/>
</div>
<div className="space-y-1">
@@ -135,10 +135,14 @@ const ResetPasswordPage: NextPage = () => {
}}
error={errors.confirmPassword}
placeholder="Confirm new password..."
className="border-custom-border-300"
className="border-custom-border-300 h-[46px]"
/>
</div>
<PrimaryButton type="submit" className="w-full text-center" loading={isSubmitting}>
<PrimaryButton
type="submit"
className="w-full text-center h-[46px]"
loading={isSubmitting}
>
{isSubmitting ? "Resetting..." : "Reset"}
</PrimaryButton>
</form>
+1 -1
View File
@@ -86,7 +86,7 @@ const SignUp: NextPage = () => {
</div>
</div>
</>
<div className="grid place-items-center h-full w-full overflow-y-auto px-7">
<div className="grid place-items-center h-full w-full overflow-y-auto py-5 px-7">
<div>
<EmailPasswordForm onSubmit={handleSignUp} />
</div>
@@ -0,0 +1,3 @@
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18.75 87.5C17.0833 87.5 15.625 86.875 14.375 85.625C13.125 84.375 12.5 82.9167 12.5 81.25V26.7708C12.5 25.7292 12.6042 24.8438 12.8125 24.1146C13.0208 23.3854 13.4028 22.7083 13.9583 22.0833L19.7917 14.1667C20.3472 13.5417 20.9896 13.1076 21.7188 12.8646C22.4479 12.6215 23.312 12.5 24.311 12.5H75.689C76.688 12.5 77.5347 12.6215 78.2292 12.8646C78.9236 13.1076 79.5486 13.5417 80.1042 14.1667L86.0417 22.0833C86.5972 22.7083 86.9792 23.3854 87.1875 24.1146C87.3958 24.8438 87.5 25.7292 87.5 26.7708V81.25C87.5 82.9167 86.875 84.375 85.625 85.625C84.375 86.875 82.9167 87.5 81.25 87.5H18.75ZM20.5208 23.5417H79.375L75.5824 18.75H24.2708L20.5208 23.5417ZM18.75 29.7917V81.25H81.25V29.7917H18.75ZM50 71.875L66.25 55.625L62.0833 51.4583L53.125 60.4167V39.4792H46.875V60.4167L37.9167 51.4583L33.75 55.625L50 71.875Z" fill="#ACB5BD"/>
</svg>

After

Width:  |  Height:  |  Size: 947 B

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