Compare commits

..
274 Commits
Author SHA1 Message Date
Vamsi KuramaandGitHub 2e1c113fdd Merge pull request #402 from makeplane/develop
dev: promote to staging
2023-03-08 20:30:28 +05:30
vamsi 981a246db1 Merge branch 'develop' of https://github.com/makeplane/plane into develop 2023-03-08 20:29:42 +05:30
vamsi 8641e35a61 Merge branch 'stage-release' into develop 2023-03-08 20:28:36 +05:30
+4 20aa3ce318 dev: promote the develop to stage-release (#399) (#403)
* chore: new link endpoints

* chore: added created by info for link

* chore: cannot have empty state group

* feat: filtering for cycle and module issue and updated grouper function for grouping in modules and cycles (#342)

* docs: github integration (#346)

* fix: add pagination for github repositories endpoint (#345)

* fix: remove bot accounts from list api (#344)

* refactor: create new endpoints for date checking getting current upcoming and past cycles (#343)

* refactor: create new endpoints for date checking getting current upcoming and past cycles

* refactor: rename endpoint to match consistency

* fix: remove project slug (#340)

* refactor: update links to different endpoints (#338)

* chore: cycle validation services and constants added

* style: kanban board

* chore:  cycle type and services updated

* chore: completed cycle dynamic importing and refactor

* feat: cycle modal date validation

* fix: build fix

* style: redesigned sidebar, added new icons and spacing changes

* style: changed app header color to white

* feat: cover image selector for project create

* style/projects_page

* style: added dragging state design

* fix: cycle form date

* chore: draft cycle services and types

* feat: draft tab and cycle sidebar update

* style: projects list page

* fix: image aspect ratio

* style: assignee drop down label

* style: new primary button design

* style: assignee dropdown

* style: assignee dropdown stlye fix

* style: state dropdown redesign

* style: dropdown ui consisteny

* style: priority dropdown redesign

* style: label dropdown redesign

* style: issue dropdown re-order

* style: state Icon

* style: date dropdown redesign

* fix: dropdown issue label

* style: transsition

* style: color fixed

* chore: labels list file and function rename

* style: redesigned create project modal

style: changed image picker to pop-over instread of modal

* fix: upload button on workspace settings page not working, UX of workspace settings image upload

* feat: date range status function added

* style: project settings pages

* fix: merge conflicts

* fix: mutation fix and date range helper fn added

* style: workspace settings pages

* style: dropdowns, feat: favorite projects in sidebar

* feat: global component for combobox with new design

* feat: custom context menu for issues in kanban board

* refactor: global context menu component

* chore: updated context menu component

* chore: updated sidebar selects

* style: kanban horizontal scrollbar added (#372)

* style: new cycle list (#374)

* feat: short date helper function

* feat: linear progress indicator added

* style: new cyce list and cycle card design

* feat: short date function improve

* feat: linear progress indicator improvement

* style: cycle card and progress indicator

* fix: helper date function and progress indicator fix

* fix: build error

---------



* chore: updated project favorites endpoints (#375)

* feat: favorite cycle and style: style improvements (#376)

* style: consistent btn

* style: caret direction for disclosure

* fix: progress tooltip value rounded

* chore: favorite cycle serivces

* chore: favorite cycle type and constant

* feat: favorite cycle feat added

* refactor: favorite services and type

* fix: build fix

* refactor: sidebar projects menu (#377)

* feat: add endpoint for draft cycles and add validation for creating draft cycles (#355)

* feat: add endpoint for draft cycles and add validation for creating draft cycles

* fix: key error in cycle create endpoint

* feat: delete file assets from storage (#373)

* chore: rename past cycle to completed cycle (#347)

* fix: workspace member listing endpoint (#348)

* fix: module issue viewset typo (#349)

* feat: add project to favourites (#352)

* feat: add project to favourites

* feat: add project is_favourite attribute to list endpoints

* refactor: updated destroy endpoint to send project_id

* chore: nomenclature update

* feat: add cover image to project (#353)

* fix: cycle date filtering for current and upcoming cycle (#357)

* fix: update filtering for completed cycles

* fix: filter updated for upcoming cycles

* fix: cycle and module issue filtering (#363)

* feat: already exisiting  url validation (#368)

* feat: cycle favourites for user (#369)

* feat: cycle favourites for user

* chore: update nomenclature

* chore: update on nomenclature

* feat: add favorites for completed and current cycle endpoints

* feat: module favourites for user (#370)

* feat: added floating toolbar on text selection (#378)

style: re-designed create-issue modal

* dev: migrations added for ProjectFavorite, ModuleFavorite, CycleFavorite including a bunch of other attribs

* chore: cycles loading, fix: cycles favorite mutation (#379)

* style: cycle sidebar, fix: cycle card bug fix   (#383)

* style: new cycle sidebar

* style: other information section

* style: progress bar bg fix

* fix: cycle card bug fix

* style: progress chart

* style: chart tooltip

* style : module sidebar (#385)

* style: new cycle sidebar

* style: other information section

* style: progress bar bg fix

* fix: cycle card bug fix

* style: progress chart

* style: chart tooltip

* style: module link tab added in sidebar stats

* style: lead and member select

* fix: text selection moving when typing in between (#384)

* feat: added floating toolbar on text selection (#386)

style: re-designed create-issue modal

* style :module list (#387)

* chore: module favorite type and services

* style: module list

* style: module list and card

* fix: link fix

* style: truncate (#388)

* style: truncate

* fix: truncate text added to cycle and module card

* fix: custom menu link item (#390)

* fix: ui fixes (#392)

* fix: ui fixes

* chore: kanban issue title length

* style: ui fix (#393)

* style: truncate

* fix: truncate text added to cycle and module card

* fix: progress percentage

* feat: cycle card tooltip

* fix: sidebar fix

* fix: edit module mutation error (#394)

* fix: issue details mutation (#389)

* fix: ui improvement (#395)

* fix: current cycle date updation

* fix: sidebar overflow fix , date helper fn added

* chore: update module dropdowns (#396)

* fix: project member filter for bot accounts (#391)

* fix: make api token only view once (#382)

* dev: add back migration for project cover images (#381)

* fix: rename db host name for docker setup (#380)

* dev: promote to staging (#397)













* Revert "dev: promote to staging (#397)" (#398)

This reverts commit f7405ba1d6.

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
Co-authored-by: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com>
Co-authored-by: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com>
Co-authored-by: Anmol Singh Bhatia <anmolsinghbhatia1001@gmail.com>
Co-authored-by: vamsi <vamsi.kurama@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Dakshesh Jain <65905942+dakshesh14@users.noreply.github.com>
Co-authored-by: Narayana <narayana.vadapalli1996@gmail.com>
Co-authored-by: sphynxux <122926002+sphynxux@users.noreply.github.com>
Co-authored-by: Anmol Singh Bhatia <anmolsinghbhatia@caravel.tech>
Co-authored-by: Dakshesh Jain <dakshesh.jain14@gmail.com>
2023-03-08 19:58:36 +05:30
Vamsi KuramaandGitHub aa240b90b0 Merge pull request #401 from makeplane/fix/order_by_text
fix: order by text
2023-03-08 19:22:57 +05:30
Aaryan Khandelwal 674ecd33ef fix: order by text 2023-03-08 19:15:28 +05:30
Vamsi KuramaandGitHub 523308a768 Merge pull request #400 from makeplane/fix/kanban_overlay
fix: drag overlay z-index, sidebar dropdowns
2023-03-08 19:12:32 +05:30
Aaryan Khandelwal 223a204a97 fix: drag overlay z-index, sidebar dropdowns 2023-03-08 19:08:57 +05:30
+5 c9252c9713 dev: promote the develop to stage-release (#399)
* chore: new link endpoints

* chore: added created by info for link

* chore: cannot have empty state group

* feat: filtering for cycle and module issue and updated grouper function for grouping in modules and cycles (#342)

* docs: github integration (#346)

* fix: add pagination for github repositories endpoint (#345)

* fix: remove bot accounts from list api (#344)

* refactor: create new endpoints for date checking getting current upcoming and past cycles (#343)

* refactor: create new endpoints for date checking getting current upcoming and past cycles

* refactor: rename endpoint to match consistency

* fix: remove project slug (#340)

* refactor: update links to different endpoints (#338)

* chore: cycle validation services and constants added

* style: kanban board

* chore:  cycle type and services updated

* chore: completed cycle dynamic importing and refactor

* feat: cycle modal date validation

* fix: build fix

* style: redesigned sidebar, added new icons and spacing changes

* style: changed app header color to white

* feat: cover image selector for project create

* style/projects_page

* style: added dragging state design

* fix: cycle form date

* chore: draft cycle services and types

* feat: draft tab and cycle sidebar update

* style: projects list page

* fix: image aspect ratio

* style: assignee drop down label

* style: new primary button design

* style: assignee dropdown

* style: assignee dropdown stlye fix

* style: state dropdown redesign

* style: dropdown ui consisteny

* style: priority dropdown redesign

* style: label dropdown redesign

* style: issue dropdown re-order

* style: state Icon

* style: date dropdown redesign

* fix: dropdown issue label

* style: transsition

* style: color fixed

* chore: labels list file and function rename

* style: redesigned create project modal

style: changed image picker to pop-over instread of modal

* fix: upload button on workspace settings page not working, UX of workspace settings image upload

* feat: date range status function added

* style: project settings pages

* fix: merge conflicts

* fix: mutation fix and date range helper fn added

* style: workspace settings pages

* style: dropdowns, feat: favorite projects in sidebar

* feat: global component for combobox with new design

* feat: custom context menu for issues in kanban board

* refactor: global context menu component

* chore: updated context menu component

* chore: updated sidebar selects

* style: kanban horizontal scrollbar added (#372)

* style: new cycle list (#374)

* feat: short date helper function

* feat: linear progress indicator added

* style: new cyce list and cycle card design

* feat: short date function improve

* feat: linear progress indicator improvement

* style: cycle card and progress indicator

* fix: helper date function and progress indicator fix

* fix: build error

---------

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

* chore: updated project favorites endpoints (#375)

* feat: favorite cycle and style: style improvements (#376)

* style: consistent btn

* style: caret direction for disclosure

* fix: progress tooltip value rounded

* chore: favorite cycle serivces

* chore: favorite cycle type and constant

* feat: favorite cycle feat added

* refactor: favorite services and type

* fix: build fix

* refactor: sidebar projects menu (#377)

* feat: add endpoint for draft cycles and add validation for creating draft cycles (#355)

* feat: add endpoint for draft cycles and add validation for creating draft cycles

* fix: key error in cycle create endpoint

* feat: delete file assets from storage (#373)

* chore: rename past cycle to completed cycle (#347)

* fix: workspace member listing endpoint (#348)

* fix: module issue viewset typo (#349)

* feat: add project to favourites (#352)

* feat: add project to favourites

* feat: add project is_favourite attribute to list endpoints

* refactor: updated destroy endpoint to send project_id

* chore: nomenclature update

* feat: add cover image to project (#353)

* fix: cycle date filtering for current and upcoming cycle (#357)

* fix: update filtering for completed cycles

* fix: filter updated for upcoming cycles

* fix: cycle and module issue filtering (#363)

* feat: already exisiting  url validation (#368)

* feat: cycle favourites for user (#369)

* feat: cycle favourites for user

* chore: update nomenclature

* chore: update on nomenclature

* feat: add favorites for completed and current cycle endpoints

* feat: module favourites for user (#370)

* feat: added floating toolbar on text selection (#378)

style: re-designed create-issue modal

* dev: migrations added for ProjectFavorite, ModuleFavorite, CycleFavorite including a bunch of other attribs

* chore: cycles loading, fix: cycles favorite mutation (#379)

* style: cycle sidebar, fix: cycle card bug fix   (#383)

* style: new cycle sidebar

* style: other information section

* style: progress bar bg fix

* fix: cycle card bug fix

* style: progress chart

* style: chart tooltip

* style : module sidebar (#385)

* style: new cycle sidebar

* style: other information section

* style: progress bar bg fix

* fix: cycle card bug fix

* style: progress chart

* style: chart tooltip

* style: module link tab added in sidebar stats

* style: lead and member select

* fix: text selection moving when typing in between (#384)

* feat: added floating toolbar on text selection (#386)

style: re-designed create-issue modal

* style :module list (#387)

* chore: module favorite type and services

* style: module list

* style: module list and card

* fix: link fix

* style: truncate (#388)

* style: truncate

* fix: truncate text added to cycle and module card

* fix: custom menu link item (#390)

* fix: ui fixes (#392)

* fix: ui fixes

* chore: kanban issue title length

* style: ui fix (#393)

* style: truncate

* fix: truncate text added to cycle and module card

* fix: progress percentage

* feat: cycle card tooltip

* fix: sidebar fix

* fix: edit module mutation error (#394)

* fix: issue details mutation (#389)

* fix: ui improvement (#395)

* fix: current cycle date updation

* fix: sidebar overflow fix , date helper fn added

* chore: update module dropdowns (#396)

* fix: project member filter for bot accounts (#391)

* fix: make api token only view once (#382)

* dev: add back migration for project cover images (#381)

* fix: rename db host name for docker setup (#380)

* dev: promote to staging (#397)

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
Co-authored-by: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com>
Co-authored-by: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com>
Co-authored-by: Anmol Singh Bhatia <anmolsinghbhatia1001@gmail.com>
Co-authored-by: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Dakshesh Jain <65905942+dakshesh14@users.noreply.github.com>
Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
Co-authored-by: Narayana <narayana.vadapalli1996@gmail.com>

* Revert "dev: promote to staging (#397)" (#398)

This reverts commit f7405ba1d6.

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
Co-authored-by: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com>
Co-authored-by: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com>
Co-authored-by: sphynxux <122926002+sphynxux@users.noreply.github.com>
Co-authored-by: Anmol Singh Bhatia <anmolsinghbhatia@caravel.tech>
Co-authored-by: Dakshesh Jain <dakshesh.jain14@gmail.com>
Co-authored-by: Dakshesh Jain <65905942+dakshesh14@users.noreply.github.com>
Co-authored-by: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com>
Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
Co-authored-by: Anmol Singh Bhatia <anmolsinghbhatia1001@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Narayana <narayana.vadapalli1996@gmail.com>
2023-03-08 01:07:00 +05:30
Vamsi KuramaandGitHub 303f266fc7 Revert "dev: promote to staging (#397)" (#398)
This reverts commit f7405ba1d6.
2023-03-08 01:05:38 +05:30
+2 f7405ba1d6 dev: promote to staging (#397)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
Co-authored-by: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com>
Co-authored-by: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com>
Co-authored-by: Anmol Singh Bhatia <anmolsinghbhatia1001@gmail.com>
Co-authored-by: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Dakshesh Jain <65905942+dakshesh14@users.noreply.github.com>
Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
Co-authored-by: Narayana <narayana.vadapalli1996@gmail.com>
2023-03-08 01:03:11 +05:30
pablohashescobarandGitHub cf72a187fe fix: rename db host name for docker setup (#380) 2023-03-08 01:00:52 +05:30
pablohashescobarandGitHub 2967fb1bee dev: add back migration for project cover images (#381) 2023-03-08 01:00:34 +05:30
pablohashescobarandGitHub 1a04eda613 fix: make api token only view once (#382) 2023-03-08 01:00:10 +05:30
pablohashescobarandGitHub 0bd3e8ae95 fix: project member filter for bot accounts (#391) 2023-03-08 00:59:48 +05:30
Aaryan KhandelwalandGitHub cc498096f3 chore: update module dropdowns (#396) 2023-03-07 22:56:22 +05:30
Anmol Singh BhatiaandGitHub afe2b029c0 fix: ui improvement (#395)
* fix: current cycle date updation

* fix: sidebar overflow fix , date helper fn added
2023-03-07 22:38:49 +05:30
Dakshesh JainandGitHub 30a91a6b91 fix: issue details mutation (#389) 2023-03-07 22:22:06 +05:30
Aaryan KhandelwalandGitHub f725ea5b15 fix: edit module mutation error (#394) 2023-03-07 22:21:50 +05:30
Anmol Singh BhatiaandGitHub cf94b92be2 style: ui fix (#393)
* style: truncate

* fix: truncate text added to cycle and module card

* fix: progress percentage

* feat: cycle card tooltip

* fix: sidebar fix
2023-03-07 20:48:19 +05:30
Aaryan KhandelwalandGitHub 88fca3c67c fix: ui fixes (#392)
* fix: ui fixes

* chore: kanban issue title length
2023-03-07 20:27:16 +05:30
Aaryan KhandelwalandGitHub cadb67a018 fix: custom menu link item (#390) 2023-03-07 20:02:03 +05:30
Anmol Singh BhatiaandGitHub d18765a613 style: truncate (#388)
* style: truncate

* fix: truncate text added to cycle and module card
2023-03-07 19:32:29 +05:30
Anmol Singh BhatiaandGitHub 61102952d0 style :module list (#387)
* chore: module favorite type and services

* style: module list

* style: module list and card

* fix: link fix
2023-03-07 18:46:56 +05:30
Dakshesh JainandGitHub d8bf9b4c2a feat: added floating toolbar on text selection (#386)
style: re-designed create-issue modal
2023-03-07 16:09:37 +05:30
Dakshesh JainandGitHub 388d5b054a fix: text selection moving when typing in between (#384) 2023-03-07 16:09:17 +05:30
Anmol Singh BhatiaandGitHub 09eab9e6bf style : module sidebar (#385)
* style: new cycle sidebar

* style: other information section

* style: progress bar bg fix

* fix: cycle card bug fix

* style: progress chart

* style: chart tooltip

* style: module link tab added in sidebar stats

* style: lead and member select
2023-03-07 15:04:02 +05:30
Anmol Singh BhatiaandGitHub 0246e0585b style: cycle sidebar, fix: cycle card bug fix (#383)
* style: new cycle sidebar

* style: other information section

* style: progress bar bg fix

* fix: cycle card bug fix

* style: progress chart

* style: chart tooltip
2023-03-07 13:43:09 +05:30
Aaryan KhandelwalandGitHub 64978969a0 chore: cycles loading, fix: cycles favorite mutation (#379) 2023-03-07 11:04:51 +05:30
vamsi b54a1f221f dev: migrations added for ProjectFavorite, ModuleFavorite, CycleFavorite including a bunch of other attribs 2023-03-07 03:09:04 +05:30
Dakshesh JainandGitHub 82f8b6d387 feat: added floating toolbar on text selection (#378)
style: re-designed create-issue modal
2023-03-06 22:49:06 +05:30
pablohashescobarandGitHub d28fe930a6 feat: module favourites for user (#370) 2023-03-06 19:00:00 +05:30
pablohashescobarandGitHub cb8b6b43dc feat: cycle favourites for user (#369)
* feat: cycle favourites for user

* chore: update nomenclature

* chore: update on nomenclature

* feat: add favorites for completed and current cycle endpoints
2023-03-06 18:59:47 +05:30
pablohashescobarandGitHub 79d7b6fec3 feat: already exisiting url validation (#368) 2023-03-06 18:58:10 +05:30
pablohashescobarandGitHub 39f54d8265 fix: cycle and module issue filtering (#363) 2023-03-06 18:57:58 +05:30
pablohashescobarandGitHub 3d57edfcf8 fix: cycle date filtering for current and upcoming cycle (#357)
* fix: update filtering for completed cycles

* fix: filter updated for upcoming cycles
2023-03-06 18:57:46 +05:30
pablohashescobarandGitHub ae64b53cf3 feat: add cover image to project (#353) 2023-03-06 18:57:20 +05:30
pablohashescobarandGitHub 689eaad0f0 feat: add project to favourites (#352)
* feat: add project to favourites

* feat: add project is_favourite attribute to list endpoints

* refactor: updated destroy endpoint to send project_id

* chore: nomenclature update
2023-03-06 18:57:07 +05:30
pablohashescobarandGitHub 697e7f13b5 fix: module issue viewset typo (#349) 2023-03-06 18:56:53 +05:30
pablohashescobarandGitHub cee8a6a8cd fix: workspace member listing endpoint (#348) 2023-03-06 18:56:41 +05:30
pablohashescobarandGitHub f5e96b8078 chore: rename past cycle to completed cycle (#347) 2023-03-06 18:56:21 +05:30
pablohashescobarandGitHub cecd025a78 feat: delete file assets from storage (#373) 2023-03-06 18:56:05 +05:30
pablohashescobarandGitHub 3a81a6c186 feat: add endpoint for draft cycles and add validation for creating draft cycles (#355)
* feat: add endpoint for draft cycles and add validation for creating draft cycles

* fix: key error in cycle create endpoint
2023-03-06 18:45:20 +05:30
Aaryan KhandelwalandGitHub 27653907f9 refactor: sidebar projects menu (#377) 2023-03-06 18:38:01 +05:30
Anmol Singh BhatiaandGitHub 626aae696f feat: favorite cycle and style: style improvements (#376)
* style: consistent btn

* style: caret direction for disclosure

* fix: progress tooltip value rounded

* chore: favorite cycle serivces

* chore: favorite cycle type and constant

* feat: favorite cycle feat added

* refactor: favorite services and type

* fix: build fix
2023-03-06 16:36:22 +05:30
Aaryan KhandelwalandGitHub d6badcd9b8 chore: updated project favorites endpoints (#375) 2023-03-06 11:37:18 +05:30
786816ed41 style: new cycle list (#374)
* feat: short date helper function

* feat: linear progress indicator added

* style: new cyce list and cycle card design

* feat: short date function improve

* feat: linear progress indicator improvement

* style: cycle card and progress indicator

* fix: helper date function and progress indicator fix

* fix: build error

---------

Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
2023-03-06 11:36:48 +05:30
Anmol Singh BhatiaandGitHub fef72ccc70 style: kanban horizontal scrollbar added (#372) 2023-03-06 11:13:08 +05:30
Aaryan KhandelwalandGitHub a4dc4d1f15 Merge pull request #371 from makeplane/style/dropdowns
style: consistent dropdowns, feat: custom context menu
2023-03-06 10:39:50 +05:30
Aaryan Khandelwal 4f4f3ebbde chore: updated sidebar selects 2023-03-05 23:24:50 +05:30
Aaryan Khandelwal 6d99557de5 chore: updated context menu component 2023-03-05 20:22:01 +05:30
Aaryan Khandelwal a4da4bf889 refactor: global context menu component 2023-03-05 03:24:24 +05:30
Aaryan Khandelwal 0a681937fd feat: custom context menu for issues in kanban board 2023-03-04 19:33:24 +05:30
Aaryan Khandelwal a875c608d4 feat: global component for combobox with new design 2023-03-04 19:10:35 +05:30
Aaryan Khandelwal 4d598fd6b6 style: dropdowns, feat: favorite projects in sidebar 2023-03-04 17:47:03 +05:30
Dakshesh JainandGitHub 067859b4bd Merge pull request #367 from makeplane/style/workspace_settings
style: workspace settings pages
2023-03-03 18:16:53 +05:30
Aaryan Khandelwal 1d8b4296fb style: workspace settings pages 2023-03-03 14:30:21 +05:30
Aaryan KhandelwalandGitHub 433276c833 Merge pull request #359 from makeplane/style/projects_page
style: projects page
2023-03-03 14:06:59 +05:30
Aaryan KhandelwalandGitHub 03a8ad336e Merge pull request #366 from makeplane/style/project_settings
style: project settings pages
2023-03-03 14:04:04 +05:30
Aaryan Khandelwal e281feddf5 fix: merge conflicts 2023-03-03 13:55:18 +05:30
Aaryan KhandelwalandGitHub f290a417bc Merge pull request #358 from makeplane/feat/cycle_validations
feat: cycle validations
2023-03-03 13:53:09 +05:30
Anmol Singh Bhatia f965734f3b fix: mutation fix and date range helper fn added 2023-03-03 13:49:19 +05:30
Aaryan Khandelwal 530edbe149 fix: merge conflicts 2023-03-03 13:38:40 +05:30
Aaryan Khandelwal ad7b691b2b Merge branch 'develop' of https://github.com/makeplane/plane into style/project_settings 2023-03-03 13:32:42 +05:30
Aaryan KhandelwalandGitHub 76b615d964 Merge pull request #365 from makeplane/fix/workspace_settings_logo
fix: upload button on workspace settings page not working
2023-03-03 13:32:25 +05:30
Aaryan KhandelwalandGitHub 2b01ae6500 Merge pull request #364 from makeplane/style/create_project
style: redesigned create project modal
2023-03-03 13:31:45 +05:30
Aaryan Khandelwal db547cc51a style: project settings pages 2023-03-03 13:29:36 +05:30
Anmol Singh Bhatia 02e4e58f19 feat: date range status function added 2023-03-03 11:32:00 +05:30
Dakshesh Jain 6bf26617a3 fix: upload button on workspace settings page not working, UX of workspace settings image upload 2023-03-02 18:39:51 +05:30
Dakshesh Jain 63d0a0dea7 Merge branch 'develop' of https://github.com/makeplane/plane into style/create_project 2023-03-02 17:09:28 +05:30
Dakshesh Jain e96a755a2e style: redesigned create project modal
style: changed image picker to pop-over instread of modal
2023-03-02 17:06:21 +05:30
Aaryan KhandelwalandGitHub aeadf0ebbf Merge pull request #362 from makeplane/style/issue_dropdown
style: new dropdown
2023-03-02 17:01:19 +05:30
Anmol Singh Bhatia c6d54a0ad2 chore: labels list file and function rename 2023-03-02 15:43:50 +05:30
Anmol Singh Bhatia 1fe1596f9d style: color fixed 2023-03-02 14:23:38 +05:30
Anmol Singh Bhatia 6bf608a37a style: transsition 2023-03-02 13:36:20 +05:30
Anmol Singh Bhatia 5ae1f63985 fix: dropdown issue label 2023-03-02 13:29:55 +05:30
Aaryan KhandelwalandGitHub f5db3dc07b Merge pull request #351 from makeplane/style/app_header
style: changed app header color to white
2023-03-02 13:16:23 +05:30
Aaryan KhandelwalandGitHub 2e6cd2638d Merge pull request #350 from makeplane/style/app_sidebar
style: redesigned sidebar, added new icons and spacing changes
2023-03-02 13:15:29 +05:30
Anmol Singh Bhatia 1f1fa19432 style: date dropdown redesign 2023-03-02 12:33:12 +05:30
Aaryan KhandelwalandGitHub ea509211a0 Merge pull request #354 from makeplane/feat/unsplash_modal
feat: cover image selector for project create
2023-03-02 12:00:16 +05:30
Aaryan KhandelwalandGitHub 032f39d9ec Merge pull request #360 from makeplane/style/button
style: new primary button design
2023-03-02 11:59:20 +05:30
Anmol Singh Bhatia 9a88803a3f style: state Icon 2023-03-02 11:32:18 +05:30
Anmol Singh Bhatia 216c565afc Merge branch 'develop' of github.com:makeplane/plane into style/issue_dropdown 2023-03-02 11:02:01 +05:30
Anmol Singh Bhatia 700769665f style: issue dropdown re-order 2023-03-02 10:37:09 +05:30
Anmol Singh Bhatia 1476896005 style: label dropdown redesign 2023-03-02 10:36:19 +05:30
Anmol Singh Bhatia cb2f0633f7 style: priority dropdown redesign 2023-03-02 09:38:54 +05:30
Anmol Singh Bhatia fdbad4ff1a style: dropdown ui consisteny 2023-03-02 09:38:13 +05:30
Anmol Singh Bhatia d8e1710a9b style: state dropdown redesign 2023-03-02 09:01:47 +05:30
Anmol Singh Bhatia 9a52031d59 style: assignee dropdown stlye fix 2023-03-02 09:01:16 +05:30
Anmol Singh Bhatia 217d6ea51c style: assignee dropdown 2023-03-02 08:03:41 +05:30
Dakshesh Jain c5d7d4f751 style: new primary button design 2023-03-01 18:43:24 +05:30
Anmol Singh Bhatia c897f04926 style: assignee drop down label 2023-03-01 16:10:48 +05:30
Aaryan KhandelwalandGitHub 99cf2d4e8d Merge pull request #356 from makeplane/style/kanban_board
style: kanban board
2023-03-01 15:07:21 +05:30
Aaryan Khandelwal 35af45ddd7 fix: image aspect ratio 2023-03-01 14:18:02 +05:30
Aaryan Khandelwal 8f0ef7bf13 fix: merge conflicts 2023-03-01 14:14:24 +05:30
Aaryan Khandelwal 6afcf1f0e3 style: projects list page 2023-03-01 14:11:27 +05:30
Anmol Singh Bhatia 8a941d0d14 feat: draft tab and cycle sidebar update 2023-03-01 11:58:30 +05:30
Anmol Singh Bhatia 7ab6eb7b48 chore: draft cycle services and types 2023-03-01 11:56:14 +05:30
Anmol Singh Bhatia a840cea9e9 fix: cycle form date 2023-03-01 11:55:04 +05:30
Aaryan Khandelwal 8589ce777f style: added dragging state design 2023-03-01 11:30:49 +05:30
Aaryan Khandelwal f7e0e257a4 Merge branch 'develop' of https://github.com/makeplane/plane into style/kanban_board 2023-03-01 11:30:17 +05:30
Aaryan KhandelwalandGitHub 2c0b27d838 Merge pull request #341 from makeplane/chore/delete_state_options
chore: cannot have empty state group
2023-03-01 10:48:40 +05:30
Aaryan Khandelwal 76b8b9eaef style/projects_page 2023-02-28 23:50:21 +05:30
Dakshesh Jain b660b1d814 feat: cover image selector for project create 2023-02-28 20:54:55 +05:30
Dakshesh Jain dea21cd660 style: changed app header color to white 2023-02-28 17:09:49 +05:30
Dakshesh Jain 73567dc7fc style: redesigned sidebar, added new icons and spacing changes 2023-02-28 16:48:02 +05:30
Anmol Singh Bhatia 7d42262e72 fix: build fix 2023-02-28 15:17:14 +05:30
Anmol Singh Bhatia 19e9f510bc feat: cycle modal date validation 2023-02-28 14:55:19 +05:30
Anmol Singh Bhatia 17e09d70e2 chore: completed cycle dynamic importing and refactor 2023-02-28 14:53:10 +05:30
Anmol Singh Bhatia 443c9300dd chore: cycle type and services updated 2023-02-28 14:47:32 +05:30
Aaryan Khandelwal 0cd3bb5956 style: kanban board 2023-02-28 14:42:46 +05:30
Anmol Singh Bhatia d480325829 chore: cycle validation services and constants added 2023-02-28 10:31:52 +05:30
pablohashescobarandGitHub 1b369feb6a refactor: update links to different endpoints (#338) 2023-02-28 02:09:22 +05:30
pablohashescobarandGitHub 7b4d7f12f5 fix: remove project slug (#340) 2023-02-28 02:09:09 +05:30
pablohashescobarandGitHub 1255552ebe refactor: create new endpoints for date checking getting current upcoming and past cycles (#343)
* refactor: create new endpoints for date checking getting current upcoming and past cycles

* refactor: rename endpoint to match consistency
2023-02-28 02:08:55 +05:30
pablohashescobarandGitHub 1ff0970ed6 fix: remove bot accounts from list api (#344) 2023-02-28 02:08:34 +05:30
pablohashescobarandGitHub 90b8d66946 fix: add pagination for github repositories endpoint (#345) 2023-02-28 02:08:17 +05:30
sphynxuxandGitHub 07295ac314 docs: github integration (#346) 2023-02-28 02:07:12 +05:30
pablohashescobarandGitHub ec4332ea6b feat: filtering for cycle and module issue and updated grouper function for grouping in modules and cycles (#342) 2023-02-27 15:32:15 +05:30
Aaryan KhandelwalandGitHub 3af3bb0fb5 Merge pull request #339 from makeplane/chore/new_link_endpoint
chore: new link endpoints
2023-02-27 15:20:00 +05:30
Aaryan Khandelwal 522952fa59 chore: cannot have empty state group 2023-02-24 16:22:49 +05:30
Aaryan Khandelwal 9dd5b15cd3 chore: added created by info for link 2023-02-24 15:53:25 +05:30
Aaryan Khandelwal df836d55d5 chore: new link endpoints 2023-02-24 15:50:15 +05:30
Vamsi KuramaandGitHub 397a3cec4f Merge pull request #336 from makeplane/develop
release: bug-fixes and ui/ux improvements for 23 Feb 2023
2023-02-24 00:03:37 +05:30
Vamsi KuramaandGitHub 3c6752807d Merge pull request #335 from makeplane/style/ui_consistency
style: kanban dropdowns
2023-02-23 23:58:06 +05:30
pablohashescobarandGitHub 517600ac89 fix: add filter for workspace integrations (#325)
* fix: add filter for workspace integrations

* fix: update url for delete

* fix: remove github installation when deleted

* fix: delete old repos

* fix: add filter on repository endpoints
2023-02-23 23:56:48 +05:30
Aaryan Khandelwal 90c913ce03 fix: merge conflicts 2023-02-23 22:36:20 +05:30
Aaryan Khandelwal b4c4271f66 style: kanban dropdowns, github integration loaders 2023-02-23 22:34:36 +05:30
Dakshesh JainandGitHub b53b0bc3f0 fix: create issue modal close on escape click (#333) 2023-02-23 19:56:18 +05:30
Anmol Singh BhatiaandGitHub 443c187cde feat: sidebar select option truncate (#334) 2023-02-23 19:54:28 +05:30
Anmol Singh BhatiaandGitHub 946dddb6b2 fix: kanban assignees tooltip (#332) 2023-02-23 19:04:08 +05:30
Anmol Singh BhatiaandGitHub 69e8b504de fix: ui fix (#331)
* fix: project card id removed

* feat: my issue page copy issue option
2023-02-23 18:12:43 +05:30
36a733cd06 style: github integration ui (#329)
* fix: ellipsis added to issue title

* feat: toolttip added

* feat: assignees tooltip added

* fix: build fix

* fix: build fix

* fix: build error

* fix: minor bugs and ux improvements

* style: github integration ui

* chore: updated .env.example file

---------

Co-authored-by: Anmol Singh Bhatia <anmolsinghbhatia@caravel.tech>
2023-02-23 18:12:07 +05:30
sriram veeraghantaandGitHub 2b3cb839ad environmental example variables fixes (#330) 2023-02-23 18:05:32 +05:30
Dakshesh JainandGitHub 98d4473501 fix: issue view not updating order_by value (#324) 2023-02-23 17:50:47 +05:30
Dakshesh JainandGitHub a550f4b161 refractor: added proper types to getServerSideProps context (#321)
* fix: redirection after login

* refractor: added proper types to getServerSideProps context
2023-02-23 17:50:37 +05:30
Dakshesh JainandGitHub 1e63c5b1b3 style: added direction for multi-level drop-down (#328)
* feat: made new multi-level select listbox

* refractor: changeds Multi-level-select component and added direction props

* style: added direction for multi-level drop-down
2023-02-23 17:02:26 +05:30
Anmol Singh BhatiaandGitHub 4caa4e33b1 fix: ui improvements (#327)
* fix: kanban board header scroll fix

* style: enable scrollbar style added

* fix: emoji picker overflow

* fix: delete project modal text overflow

* fix: cycle card ellipsis

* fix: tooltip position updated and custom class added

* fix: assignees tooltip overflow

* fix: module card

* fix: my issue page  tooltip and responsive title  added

* fix: home page tooltip and responsiveness
2023-02-23 16:46:52 +05:30
Dakshesh JainandGitHub 6a10faca68 feat: made new multi-level select listbox (#326) 2023-02-23 15:36:46 +05:30
sriram veeraghantaandGitHub ad5a8be0e2 Merge pull request #323 from makeplane/develop
release: Stage Release 23rd Feb 2023
2023-02-23 10:58:57 +05:30
92f717962c fix: minor bugs and ux improvements (#322)
* fix: ellipsis added to issue title

* feat: toolttip added

* feat: assignees tooltip added

* fix: build fix

* fix: build fix

* fix: build error

* fix: minor bugs and ux improvements

---------

Co-authored-by: Anmol Singh Bhatia <anmolsinghbhatia@caravel.tech>
2023-02-23 10:54:54 +05:30
vamsi 702cfeb4ee dev: added new migrations 2023-02-23 01:04:44 +05:30
vamsi 649748f801 Merge branch 'develop' of https://github.com/makeplane/plane into develop 2023-02-23 01:02:34 +05:30
vamsi 2fffad130e Merge branch 'master' of https://github.com/makeplane/plane into develop 2023-02-23 01:01:52 +05:30
pablohashescobarandGitHub 937222fdd4 feat: assignee and label details in cycle and module issues (#319) 2023-02-23 01:01:09 +05:30
a9802f816e feat: github integration (#315)
* feat: initiate integrations

* feat: initiate github integration create models for the same

* feat: github integration views

* fix: update workspace integration view to create bot users

* refactor: rename repository model

* refactor: update github repo sync endpoint to create repo and sync in one go

* refactor: update issue activities to post the updates to segway hook

* refactor: update endpoints to get project id and add actor as a member of project in repo sync

* fix: make is bot as a read only field

* fix: remove github repo imports

* fix: url mapping

* feat: repo views

* refactor: update webhook request endpoint

* refactor: rename repositories table to github_repositories

* fix: workpace integration actor

* feat: label for github integration

* refactor: issue activity on create issue

* refactor: repo create endpoint and add db constraints for repo sync and issues

* feat: create api token on workpsace integration and avatar_url for integrations

* refactor: add uuid primary key for Audit model

* refactor: remove id from auditfield to maintain integrity and make avatar blank if none supplied

* feat: track comments on an issue

* feat: comment syncing from plane to github

* fix: prevent activities created by bot to be sent to webhook

* feat: github app installation id retrieve

* feat: github app installation id saved into db

* feat: installation_id for the github integragation and unique provider and project base integration for repo

* refactor: remove actor logic from activity task

* feat: saving github metadata using installation id in workspace integration table

* feat: github repositories endpoint

* feat: github and project repos synchronisation

* feat: delete issue and delete comment activity

* refactor: remove print logs

* FIX: reading env names for github app while installation

* refactor: update bot user firstname with title

* fix: add is_bot value in field

---------

Co-authored-by: venplane <venkatesh@plane.so>
2023-02-22 19:40:57 +05:30
Dakshesh JainandGitHub c1a78cc230 fix: redirection after login (#320) 2023-02-22 17:54:27 +05:30
d29f34566c fix : tooltip fix (#318)
* fix: ellipsis added to issue title

* feat: toolttip added

* feat: assignees tooltip added

* fix: build fix

* fix: build fix

* fix: build error

---------

Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
2023-02-22 13:24:59 +05:30
Anmol Singh BhatiaandGitHub d8c10b6bc0 feat: issues tooltip , fix: ui improvement (#317)
* fix: ellipsis added to issue title

* feat: toolttip added

* feat: assignees tooltip added

* fix: build fix
2023-02-22 11:42:17 +05:30
sriram veeraghantaandGitHub 2cadb3784b env fixes (#316) 2023-02-22 00:53:22 +05:30
pablohashescobarandGitHub f17ecd597b Merge pull request #278 from makeplane/feat/multiple_sub_issues
feat: assign multiple sub issues
2023-02-21 23:52:33 +05:30
pablohashescobarandGitHub 33ed2f6c35 Merge pull request #287 from makeplane/fix/state_ordering
fix: state ordering in group
2023-02-21 23:52:21 +05:30
pablohashescobarandGitHub a904c4a7de Merge pull request #302 from makeplane/feat/issue_sorting_grouping
feat: updated issue grouping and filtering
2023-02-21 23:52:08 +05:30
pablohashescobarandGitHub 71f9ae41f3 feat: created_by details for links (#313) 2023-02-21 23:50:54 +05:30
+2 1b94c7b640 Merge pull request #314 from makeplane/develop
* chore: update all backend dependencies to the latest version

* feat: record issue completed at date when the issues are moved to fompleted group (#262)

* feat: cycle status (#265)

* feat: cycle status and dates added in sidebar

* feat: update status added

* chore: update python runtime

* feat: label grouping in dropdowns, default state in project settings (#266)

* feat: label grouping in dropdowns, default state in project settings

* feat: label disclosure default open

* refactor: label setting page

* chore: tooltip component updated

* chore: tooltip component updated

* feat/state_sequence_change

* fix: remirror buttons (#267)

* feat: burndown chart (#268)

* chore: recharts dependencie added

* chore: tpye added for issue completed at

* feat: date range helper fn added

* feat: progress chart added

* feat: ideal task line added in progress chart

* feat: chart legends added

* fix: state reordering (#269)

* fix: state reordering

* refactor: remove unnecessary argument

* refactor: mutation after setting default

* feat: drag and drop an issue to delete (#270)

* feat: drag and drop an issue to delete

* style: repositioned trash box

* feat : cycle sidebar revamp (#271)

* feat: range date picker added

* feat: cycle status ui improved

* feat : sidebar progress improvement (#272)

* feat: progress chart render validation

* fix: sidebar stats tab

* feat: sidebar active tab context

* chore: removed minor bugs (#273)

* fix: ui bug (#274)

* fix: shortcut search fix
shortcut modal ui fixes
shortcut search fix
email us label change
* fix: email us label updated

* feat: default state for project (#264)

* build: add channels requirement for the asgi configuration (#225)

* refactor: combine sign in and sign up endpoint to a single endpoint (#263)

* feat: state grouping and ordering list (#253)

* feat: state grouping and ordering list

* fix: state grouping in state list endpoint

* dev: added migrations for new models schema changes

* fix: mac text copy fix (#277)

* feat: state description in settings (#275)

* chore: removed minor bugs

* feat: state description in settings

* feat: group by assignee

* refactor: update django admin panel heading (#276)

* feat: create label option in create issue modal (#281)

* refactor: issue details page (#282)

* fix: shortcut search  (#283)

* fix: search case innsensitive

* style: email icon updated

* feat: module sidebar date and status updated (#285)

* feat: bulk assign sub-issues (#284)

* fix: consistent dropdowns, refactor: ui components (#286)

* build(deps): bump django in /apiserver/requirements (#289)

Bumps [django](https://github.com/django/django) from 3.2.17 to 3.2.18.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/3.2.17...3.2.18)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* fix: workspace name and breadcrumb title , refactor: command palette (#290)

* refactor: command pallette

* fix: workspace name trim

* fix: breadcrumb title responsiveness added

* feat: copy link option (#292)

* feat: copy issue link added in issue card

* feat: copy cycle link added

* feat: ellipsis added in module card

* fix: origin path and handlecopytext added

* fix: remirror image not updating (#294)

* feat: resend login magic code  (#291)

* feat: resend login code on signing page after 30 seconds

* feat: handling error on code send

* refractor: isResendDisabled varible for resend button

* dev: timer count-down hook

* refractor: using new timer hook in sign in page

* feat: issue links (#288)

* feat: links for issues

* fix: add issue link in serilaizer

* feat: links can be added to issues

---------

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

* fix: default label color (#295)

* fix: colors of old labels can now be changed

* fix: black color for labels with no color

* fix: ui changes (#297)

* fix: module card height and invalid date

* fix: issue details page title resizing fix

* refractor: use local storage hook (#293)

* feat: resend login code on signing page after 30 seconds

* refractor: use local storage hook

* refractor: properly using new local storage hook on modules sidebar

* fix: assignee and labels field while editing an issue (#296)

* fix: assignee and labels field while editing an issue

* chore: remove unused declarations

* fix: issue title resizing fix (#300)

* fix: issue title resizing fix

* fix: header ui fix and invalid date label updated

* fix: try/catch for invalid values stored in local storage (#301)

* fix: create issue modal closing on clicking on Grammarly recommendation (#299)

fixed it by not closing modal on outside click

* style: not showing pointer & theme color on resend code button disabled (#298)

* fix: new project issues response (#303)

* refactor/cycles_folder_structure (#304)

* fix: ui changes (#306)

* fix: sidebar date range

* fix: renamed key with id in filters

* fix: replace progress bar

* chore: react progress bar package removed

* fix: progress chart legends position

* fix: progress chart legends alignment fix

* feat: manual ordering of issues (#305)

* feat: global component for links list (#307)

* Feat: Dockerizing using nginx reverse proxy (#280)

* minor docker fixes

* eslint config changes

* dockerfile changes to backend and frontend

* oauth enabled env flag

* sentry enabled env flag

* build: get alternatives for environment variables and static file storage

* build: automatically generate random secret key if not provided

* build: update docker compose for next url env add channels to requirements for asgi server and save files in local machine for docker environment

* build: update nginx conf for backend base url update backend dockerfile to make way for static file uploads

* feat: create a default user with given values else default values

* chore: update docker python version and other dependency version in docker

* build: update local settings file to run it in docker

* fix: update script to run in default production setting

* fix: env variable changes and env setup shell script added

* Added Single Dockerfile to run the Entire plane application

* docs build fixes

---------

Co-authored-by: Narayana <narayana.vadapalli1996@gmail.com>
Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>

* feat: edit module (#309)

* feat: edit module

* fix: build fix

* refactor: dnd function (#308)

* refactor: manual ordering bugs (#312)

* refactor: create issue modal input fields (#310)

* style: showing user sign-in progress on sign-in with code (#311)

* style: not showing pointer & theme color on resend code button disabled

* style: showing user sign-in progress on sign-in with code

* style: showing error from server on sign-in with code fail

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
Co-authored-by: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com>
Co-authored-by: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com>
Co-authored-by: Anmol Singh Bhatia <anmolsinghbhatia1001@gmail.com>
Co-authored-by: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com>
Co-authored-by: vamsi <vamsi.kurama@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Dakshesh Jain <65905942+dakshesh14@users.noreply.github.com>
Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
Co-authored-by: Narayana <narayana.vadapalli1996@gmail.com>
2023-02-21 19:17:32 +05:30
Dakshesh JainandGitHub 5cbb3ecd4d style: showing user sign-in progress on sign-in with code (#311)
* style: not showing pointer & theme color on resend code button disabled

* style: showing user sign-in progress on sign-in with code

* style: showing error from server on sign-in with code fail
2023-02-21 19:14:12 +05:30
Aaryan KhandelwalandGitHub c0263acb8a refactor: create issue modal input fields (#310) 2023-02-21 19:12:38 +05:30
Aaryan KhandelwalandGitHub 8d6a357a7f refactor: manual ordering bugs (#312) 2023-02-21 19:11:54 +05:30
Aaryan KhandelwalandGitHub 8c15a1519f refactor: dnd function (#308) 2023-02-21 14:56:32 +05:30
Anmol Singh BhatiaandGitHub 3d28cde91d feat: edit module (#309)
* feat: edit module

* fix: build fix
2023-02-21 12:45:04 +05:30
bdca84bd09 Feat: Dockerizing using nginx reverse proxy (#280)
* minor docker fixes

* eslint config changes

* dockerfile changes to backend and frontend

* oauth enabled env flag

* sentry enabled env flag

* build: get alternatives for environment variables and static file storage

* build: automatically generate random secret key if not provided

* build: update docker compose for next url env add channels to requirements for asgi server and save files in local machine for docker environment

* build: update nginx conf for backend base url update backend dockerfile to make way for static file uploads

* feat: create a default user with given values else default values

* chore: update docker python version and other dependency version in docker

* build: update local settings file to run it in docker

* fix: update script to run in default production setting

* fix: env variable changes and env setup shell script added

* Added Single Dockerfile to run the Entire plane application

* docs build fixes

---------

Co-authored-by: Narayana <narayana.vadapalli1996@gmail.com>
Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
2023-02-21 11:31:43 +05:30
Aaryan KhandelwalandGitHub 33e2986062 feat: global component for links list (#307) 2023-02-21 11:23:50 +05:30
Aaryan KhandelwalandGitHub 818fe3ecf7 feat: manual ordering of issues (#305) 2023-02-20 19:19:46 +05:30
Anmol Singh BhatiaandGitHub 202096500e fix: ui changes (#306)
* fix: sidebar date range

* fix: renamed key with id in filters

* fix: replace progress bar

* chore: react progress bar package removed

* fix: progress chart legends position

* fix: progress chart legends alignment fix
2023-02-20 19:00:40 +05:30
Aaryan KhandelwalandGitHub e5934e0b07 refactor/cycles_folder_structure (#304) 2023-02-20 11:23:04 +05:30
Aaryan KhandelwalandGitHub 77c319c748 fix: new project issues response (#303) 2023-02-18 21:19:04 +05:30
pablohashescobar d50cc14972 fix: typo in model aggregation key 2023-02-18 18:21:42 +05:30
pablohashescobar 495ac0ca00 feat: improved grouper with grouping function 2023-02-18 15:43:47 +05:30
pablohashescobar 236c660cc7 feat: sort order during create 2023-02-18 12:35:42 +05:30
pablohashescobar eba0f02aeb feat: back migration script to populate random sort_order values 2023-02-18 12:29:46 +05:30
pablohashescobar 2505417dbd feat: updated issue grouping and filtering 2023-02-18 12:22:17 +05:30
Dakshesh JainandGitHub 393638c700 style: not showing pointer & theme color on resend code button disabled (#298) 2023-02-17 20:10:44 +05:30
Dakshesh JainandGitHub 11a36b4398 fix: create issue modal closing on clicking on Grammarly recommendation (#299)
fixed it by not closing modal on outside click
2023-02-17 20:10:21 +05:30
Dakshesh JainandGitHub d71cf567e9 fix: try/catch for invalid values stored in local storage (#301) 2023-02-17 20:10:02 +05:30
Anmol Singh BhatiaandGitHub fcb932dc5d fix: issue title resizing fix (#300)
* fix: issue title resizing fix

* fix: header ui fix and invalid date label updated
2023-02-17 19:58:27 +05:30
Aaryan KhandelwalandGitHub c979599e53 fix: assignee and labels field while editing an issue (#296)
* fix: assignee and labels field while editing an issue

* chore: remove unused declarations
2023-02-17 19:07:36 +05:30
Dakshesh JainandGitHub a0d176c952 refractor: use local storage hook (#293)
* feat: resend login code on signing page after 30 seconds

* refractor: use local storage hook

* refractor: properly using new local storage hook on modules sidebar
2023-02-17 18:52:16 +05:30
Anmol Singh BhatiaandGitHub 8c39717068 fix: ui changes (#297)
* fix: module card height and invalid date

* fix: issue details page title resizing fix
2023-02-17 18:51:45 +05:30
Aaryan KhandelwalandGitHub 45319d81db fix: default label color (#295)
* fix: colors of old labels can now be changed

* fix: black color for labels with no color
2023-02-17 17:06:30 +05:30
7c1f357bed feat: issue links (#288)
* feat: links for issues

* fix: add issue link in serilaizer

* feat: links can be added to issues

---------

Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
2023-02-17 17:04:12 +05:30
Dakshesh JainandGitHub a66b2fd73d feat: resend login magic code (#291)
* feat: resend login code on signing page after 30 seconds

* feat: handling error on code send

* refractor: isResendDisabled varible for resend button

* dev: timer count-down hook

* refractor: using new timer hook in sign in page
2023-02-17 16:58:00 +05:30
Aaryan KhandelwalandGitHub 4b068398bd fix: remirror image not updating (#294) 2023-02-17 16:57:31 +05:30
Anmol Singh BhatiaandGitHub 1665863bd9 feat: copy link option (#292)
* feat: copy issue link added in issue card

* feat: copy cycle link added

* feat: ellipsis added in module card

* fix: origin path and handlecopytext added
2023-02-17 14:04:34 +05:30
Anmol Singh BhatiaandGitHub a28be95002 fix: workspace name and breadcrumb title , refactor: command palette (#290)
* refactor: command pallette

* fix: workspace name trim

* fix: breadcrumb title responsiveness added
2023-02-16 18:01:14 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
6ed5c05164 build(deps): bump django in /apiserver/requirements (#289)
Bumps [django](https://github.com/django/django) from 3.2.17 to 3.2.18.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/3.2.17...3.2.18)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-16 16:04:26 +05:30
Aaryan KhandelwalandGitHub 667dafbda4 fix: consistent dropdowns, refactor: ui components (#286) 2023-02-16 12:03:52 +05:30
pablohashescobar 2d2751c58d fix: state ordering in group 2023-02-15 17:53:37 +05:30
Aaryan KhandelwalandGitHub ec37bb9d23 feat: bulk assign sub-issues (#284) 2023-02-15 14:45:28 +05:30
Anmol Singh BhatiaandGitHub f21135d955 feat: module sidebar date and status updated (#285) 2023-02-15 14:44:19 +05:30
Anmol Singh BhatiaandGitHub 9b51a918cf fix: shortcut search (#283)
* fix: search case innsensitive

* style: email icon updated
2023-02-15 12:28:22 +05:30
Aaryan KhandelwalandGitHub ab0ce2f649 refactor: issue details page (#282) 2023-02-15 12:21:41 +05:30
pablohashescobar 9a5d7b1049 Merge branch 'develop' of github.com:makeplane/plane into feat/multiple_sub_issues 2023-02-14 20:08:39 +05:30
pablohashescobar b8c1305883 fix: error validation for empty length 2023-02-14 20:08:04 +05:30
Aaryan KhandelwalandGitHub 6f0539f01d feat: create label option in create issue modal (#281) 2023-02-14 20:05:32 +05:30
pablohashescobar c9dce08842 feat: assign multiple sub issues 2023-02-14 15:33:53 +05:30
pablohashescobarandGitHub fcba332589 refactor: update django admin panel heading (#276) 2023-02-14 14:50:16 +05:30
Aaryan KhandelwalandGitHub e53ff4c02e feat: state description in settings (#275)
* chore: removed minor bugs

* feat: state description in settings

* feat: group by assignee
2023-02-14 14:46:48 +05:30
Anmol Singh BhatiaandGitHub 9c8c7f1dda fix: mac text copy fix (#277) 2023-02-14 14:35:14 +05:30
vamsi 7950f191e7 dev: added migrations for new models schema changes 2023-02-14 01:19:59 +05:30
pablohashescobarandGitHub 92d5749997 feat: state grouping and ordering list (#253)
* feat: state grouping and ordering list

* fix: state grouping in state list endpoint
2023-02-14 01:16:35 +05:30
pablohashescobarandGitHub af1d49bbf5 refactor: combine sign in and sign up endpoint to a single endpoint (#263) 2023-02-14 01:14:56 +05:30
pablohashescobarandGitHub 0477db69a0 build: add channels requirement for the asgi configuration (#225) 2023-02-14 01:14:24 +05:30
pablohashescobarandGitHub 7a3c00aba4 Merge pull request #226 from makeplane/chore/backend_dependencies
chore: update all backend dependencies to the latest version
2023-02-14 01:14:05 +05:30
pablohashescobarandGitHub 97ffdc8124 feat: default state for project (#264) 2023-02-14 01:12:32 +05:30
Anmol Singh BhatiaandGitHub c6f0990605 fix: ui bug (#274)
* fix: shortcut search fix
shortcut modal ui fixes
shortcut search fix
email us label change
* fix: email us label updated
2023-02-13 20:19:46 +05:30
Aaryan KhandelwalandGitHub 214e860e67 chore: removed minor bugs (#273) 2023-02-13 19:38:58 +05:30
Anmol Singh BhatiaandGitHub 8fb34fe1e3 feat : sidebar progress improvement (#272)
* feat: progress chart render validation

* fix: sidebar stats tab

* feat: sidebar active tab context
2023-02-13 13:14:23 +05:30
Anmol Singh BhatiaandGitHub ebf294af55 feat : cycle sidebar revamp (#271)
* feat: range date picker added

* feat: cycle status ui improved
2023-02-13 10:33:02 +05:30
Aaryan KhandelwalandGitHub d0afa486c7 feat: drag and drop an issue to delete (#270)
* feat: drag and drop an issue to delete

* style: repositioned trash box
2023-02-13 10:32:02 +05:30
Aaryan KhandelwalandGitHub 0a88b3ed84 fix: state reordering (#269)
* fix: state reordering

* refactor: remove unnecessary argument

* refactor: mutation after setting default
2023-02-13 10:30:44 +05:30
Anmol Singh BhatiaandGitHub bb4ffec7e8 feat: burndown chart (#268)
* chore: recharts dependencie added

* chore: tpye added for issue completed at

* feat: date range helper fn added

* feat: progress chart added

* feat: ideal task line added in progress chart

* feat: chart legends added
2023-02-10 18:40:02 +05:30
Aaryan KhandelwalandGitHub af22dc9c58 fix: remirror buttons (#267) 2023-02-10 18:39:23 +05:30
Aaryan KhandelwalandGitHub a403c0c346 feat: label grouping in dropdowns, default state in project settings (#266)
* feat: label grouping in dropdowns, default state in project settings

* feat: label disclosure default open

* refactor: label setting page

* chore: tooltip component updated

* chore: tooltip component updated

* feat/state_sequence_change
2023-02-10 18:02:18 +05:30
pablohashescobar 37c28b251d chore: update python runtime 2023-02-09 19:07:08 +05:30
7c06be19fc feat: cycle status (#265)
* feat: cycle status and dates added in sidebar

* feat: update status added

---------

Co-authored-by: Anmol Singh Bhatia <anmolsinghbhatia1001@gmail.com>
2023-02-09 17:54:47 +05:30
pablohashescobarandGitHub 9e9a6f4cce feat: record issue completed at date when the issues are moved to fompleted group (#262) 2023-02-09 10:41:43 +05:30
sriram veeraghantaandGitHub 605ab26bb0 Merge pull request #261 from makeplane/develop
Merge pull request #260 from makeplane/stage-release
2023-02-09 00:21:51 +05:30
sriram veeraghantaandGitHub a94a3e2726 Merge pull request #260 from makeplane/stage-release
Branch Sync
2023-02-09 00:21:12 +05:30
sriram veeraghantaandGitHub 394c73885d Merge pull request #259 from makeplane/stage-release-develop
release: Stage Release
2023-02-09 00:19:42 +05:30
sriramveeraghanta 7f406ceb39 fix: merge conflicts resolved 2023-02-09 00:12:59 +05:30
56030b1c2c fix: github auth login (#250)
* fix: added PROJECT_ISSUES_LIST on the imports (#221)

* fix: github signin by parsing email

* refactor: changed variable names

---------

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
Co-authored-by: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com>
Co-authored-by: Vamsi Kurama <vamsi.kurama@gmail.com>
2023-02-09 00:01:45 +05:30
Aaryan KhandelwalandGitHub 4ffa31fd02 fix: create issue modal bugs (#257) 2023-02-08 23:58:53 +05:30
Aaryan KhandelwalandGitHub 8e3541b947 fix: mutation of states (#256)
* feat: label grouping, fix: new states response

* fix: mutation of states
2023-02-08 23:58:17 +05:30
sriram veeraghantaandGitHub bd399d6d1a sentry changes (#255) 2023-02-08 20:44:35 +05:30
Aaryan KhandelwalandGitHub 166520dfda feat: label grouping, fix: new states response (#254) 2023-02-08 18:51:03 +05:30
c978632938 feat: sidebar progress (#252)
* feat: cycle assignees and labels progress added

* fix: build fix

* feat: sidebar progress stats added and refactor

* refactor: progress stats and cycle sidebar

* feat: module sidebar progress added

* feat: sidebar progress no assignee added

* feat: states tab added

---------

Co-authored-by: Anmol Singh Bhatia <anmolsinghbhatia1001@gmail.com>
2023-02-08 18:50:08 +05:30
d3b73dc32f release: Stage Release (#251)
* feat: manual ordering for issues in kanban

* refactor: issues folder structure

* refactor: modules and states folder structure

* refactor: datepicker code

* fix: create issue modal bug

* feat: custom progress bar added

* refactor: created global component for kanban board

* refactor: update cycle and module issue create

* refactor: return modules created

* refactor: integrated global kanban view everywhere

* refactor: integrated global list view everywhere

* refactor: removed unnecessary api calls

* refactor: update nomenclature for consistency

* refactor: global select component for issue view

* refactor: track cycles and modules for issue

* fix: tracking new cycles and modules in activities

* feat: segregate api token workspace

* fix: workpsace id during token creation

* refactor: update model association to cascade on delete

* feat: sentry integrated (#235)

* feat: sentry integrated

* fix: removed unnecessary env variable

* fix: update remirror description to save empty string and empty paragraph (#237)

* Update README.md

* fix: description and comment_json default value to remove warnings

* feat: link option in remirror (#240)

* feat: link option in remirror

* fix: removed link import from remirror toolbar

* feat: module and cycle settings under project

* fix:  module issue assignment

* fix: module issue updation and activity logging

* fix: typo while creating module issues

* fix: string comparison for update operation

* fix: ui fixes (#246)

* style: shortcut command label bg color change

* sidebar shortcut ui fix

---------

Co-authored-by: Anmol Singh Bhatia <anmolsinghbhatia1001@gmail.com>

* fix: update empty passwords to hashed string and add hashing for magic sign in

* refactor: remove print logs from back migrations

* build(deps): bump django in /apiserver/requirements

Bumps [django](https://github.com/django/django) from 3.2.16 to 3.2.17.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/3.2.16...3.2.17)

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

Signed-off-by: dependabot[bot] <support@github.com>

* feat: cycles and modules toggle in settings, refactor: folder structure (#247)

* feat: link option in remirror

* fix: removed link import from remirror toolbar

* refactor: constants folder

* refactor: layouts folder structure

* fix: issue view context

* feat: cycles and modules toggle in settings

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
Co-authored-by: Anmol Singh Bhatia <anmolsinghbhatia1001@gmail.com>
Co-authored-by: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com>
Co-authored-by: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com>
Co-authored-by: sphynxux <122926002+sphynxux@users.noreply.github.com>
Co-authored-by: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-08 10:15:18 +05:30
Aaryan KhandelwalandGitHub 76cc634a46 feat: cycles and modules toggle in settings, refactor: folder structure (#247)
* feat: link option in remirror

* fix: removed link import from remirror toolbar

* refactor: constants folder

* refactor: layouts folder structure

* fix: issue view context

* feat: cycles and modules toggle in settings
2023-02-08 10:13:07 +05:30
pablohashescobarandGitHub 4e27e93739 Merge pull request #243 from makeplane/fix/description_comment_migration_warnings
fix: description and comment_json default value to remove warnings
2023-02-08 01:47:46 +05:30
pablohashescobarandGitHub 8e1ba90a34 Merge pull request #244 from makeplane/feat/module_cycle_settings
feat: module and cycle settings under project
2023-02-08 01:47:23 +05:30
pablohashescobarandGitHub a67690186a Merge pull request #245 from makeplane/fix/module_issue
fix:  module issue assignment
2023-02-08 01:46:41 +05:30
pablohashescobarandGitHub 5785ab9e96 Merge pull request #248 from makeplane/fix/password_empty
fix: update empty passwords to hashed string and add hashing for magic sign in
2023-02-08 01:46:13 +05:30
pablohashescobarandGitHub e77defc622 chore: update django version to 3.2.17
chore: update django version to 3.2.17
2023-02-08 01:03:04 +05:30
dependabot[bot]andGitHub 27849ee079 build(deps): bump django in /apiserver/requirements
Bumps [django](https://github.com/django/django) from 3.2.16 to 3.2.17.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/3.2.16...3.2.17)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-07 19:30:23 +00:00
pablohashescobar d36e8af571 refactor: remove print logs from back migrations 2023-02-08 00:58:51 +05:30
pablohashescobar 343718cd2a fix: update empty passwords to hashed string and add hashing for magic sign in 2023-02-08 00:45:56 +05:30
d5bf1f7a91 fix: ui fixes (#246)
* style: shortcut command label bg color change

* sidebar shortcut ui fix

---------

Co-authored-by: Anmol Singh Bhatia <anmolsinghbhatia1001@gmail.com>
2023-02-07 19:28:09 +05:30
pablohashescobar 1d44071a77 fix: string comparison for update operation 2023-02-07 14:26:28 +05:30
pablohashescobar 51b3815b72 fix: typo while creating module issues 2023-02-07 14:10:06 +05:30
pablohashescobar 8801a291af fix: module issue updation and activity logging 2023-02-07 14:05:09 +05:30
pablohashescobar f09c9b67f6 fix: module issue assignment 2023-02-07 13:25:06 +05:30
pablohashescobar 8872f3f627 feat: module and cycle settings under project 2023-02-07 13:20:15 +05:30
Aaryan KhandelwalandGitHub 859fef24f4 feat: link option in remirror (#240)
* feat: link option in remirror

* fix: removed link import from remirror toolbar
2023-02-07 11:20:41 +05:30
pablohashescobar 32d83ac2c9 fix: description and comment_json default value to remove warnings 2023-02-07 02:29:36 +05:30
sphynxuxandGitHub f308fe2ce1 Merge pull request #242 from makeplane/sphynxux-patch-1
content: updated the discord link on Readme
2023-02-07 01:32:08 +05:30
sphynxuxandGitHub 240be69c7f Update README.md 2023-02-07 01:31:15 +05:30
pablohashescobarandGitHub 2c4dbc84a2 Merge pull request #231 from makeplane/feat/issue_manual_ordering
feat: manual ordering for issues in kanban
2023-02-07 01:22:02 +05:30
pablohashescobarandGitHub c7ad9f3da1 Merge pull request #236 from makeplane/refactor/cycle_modules
refactor: update cycle and module create operation
2023-02-07 01:20:13 +05:30
pablohashescobarandGitHub b93abb4b37 fix: update remirror description to save empty string and empty paragraph (#237) 2023-02-07 01:19:42 +05:30
pablohashescobarandGitHub aaaf75c5da Merge pull request #239 from makeplane/refactor/color_nomenclature
refactor: update nomenclature for consistency
2023-02-07 01:15:14 +05:30
pablohashescobarandGitHub d256472776 Merge pull request #241 from makeplane/feat/api_token_workspace
feat: segregate api token workspace
2023-02-07 01:13:55 +05:30
Aaryan KhandelwalandGitHub 21e042c852 feat: sentry integrated (#235)
* feat: sentry integrated

* fix: removed unnecessary env variable
2023-02-07 01:07:01 +05:30
pablohashescobar cd41a05022 refactor: update model association to cascade on delete 2023-02-06 23:20:21 +05:30
pablohashescobar f69b76c77d fix: workpsace id during token creation 2023-02-06 22:52:20 +05:30
pablohashescobar 0b7f0640c9 feat: segregate api token workspace 2023-02-06 20:22:08 +05:30
pablohashescobar a576a2ab59 fix: tracking new cycles and modules in activities 2023-02-06 19:25:11 +05:30
sriram veeraghantaandGitHub e1b7e8d139 Merge pull request #233 from makeplane/feat/progress_bar
feat: custom progress bar added
2023-02-06 19:15:43 +05:30
sriram veeraghantaandGitHub c7e006d2c1 Merge pull request #232 from makeplane/refactor/folder_structure
refactor: folder structure
2023-02-06 19:14:59 +05:30
pablohashescobar 67a1052b7b refactor: track cycles and modules for issue 2023-02-06 15:31:05 +05:30
Aaryan Khandelwal adbe16f8ae refactor: global select component for issue view 2023-02-06 15:18:57 +05:30
pablohashescobar eec82eca2f refactor: update nomenclature for consistency 2023-02-06 13:56:02 +05:30
Aaryan Khandelwal 0e07c1e19f refactor: removed unnecessary api calls 2023-02-05 22:01:23 +05:30
Aaryan Khandelwal d673aedf48 refactor: integrated global list view everywhere 2023-02-05 16:57:37 +05:30
Aaryan Khandelwal 85b7f39ed3 refactor: integrated global kanban view everywhere 2023-02-04 20:08:13 +05:30
pablohashescobar 7207d92d62 refactor: return modules created 2023-02-03 19:03:58 +05:30
pablohashescobar a4f095fb59 refactor: update cycle and module issue create 2023-02-03 19:03:27 +05:30
Aaryan Khandelwal 58eda658c8 refactor: created global component for kanban board 2023-02-03 16:03:27 +05:30
Anmol Singh Bhatia aa805b2b16 feat: custom progress bar added 2023-02-03 02:35:25 +05:30
Aaryan Khandelwal 4f85773a48 fix: create issue modal bug 2023-02-02 19:03:54 +05:30
Aaryan Khandelwal 563921d0cf refactor: datepicker code 2023-02-02 18:33:46 +05:30
Aaryan Khandelwal b2eab805e9 refactor: modules and states folder structure 2023-02-02 18:04:13 +05:30
Aaryan Khandelwal 8b1bf53831 refactor: issues folder structure 2023-02-02 15:00:35 +05:30
pablohashescobar c4fff45429 feat: manual ordering for issues in kanban 2023-02-02 13:29:17 +05:30
pablohashescobar c60e771e9c chore: update all backend dependencies to the latest version 2023-02-01 15:08:52 +05:30
184 changed files with 8170 additions and 5308 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
SECRET_KEY="<-- django secret -->"
DJANGO_SETTINGS_MODULE="plane.settings.production"
# Database
DATABASE_URL=postgres://plane:plane@plane-db-1:5432/plane
DATABASE_URL=postgres://plane:plane@db:5432/plane
# Cache
REDIS_URL=redis://redis:6379/
# SMPT
+39 -1
View File
@@ -3,7 +3,7 @@ import uuid
import random
from django.contrib.auth.hashers import make_password
from plane.db.models import ProjectIdentifier
from plane.db.models import Issue, IssueComment, User
from plane.db.models import Issue, IssueComment, User, Project
# Update description and description html values for old descriptions
@@ -96,3 +96,41 @@ def updated_issue_sort_order():
except Exception as e:
print(e)
print("Failed")
def update_project_cover_images():
try:
project_cover_images = [
"https://images.unsplash.com/photo-1677432658720-3d84f9d657b4?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80",
"https://images.unsplash.com/photo-1661107564401-57497d8fe86f?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1332&q=80",
"https://images.unsplash.com/photo-1677352241429-dc90cfc7a623?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1332&q=80",
"https://images.unsplash.com/photo-1677196728306-eeafea692454?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1331&q=80",
"https://images.unsplash.com/photo-1660902179734-c94c944f7830?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1255&q=80",
"https://images.unsplash.com/photo-1672243775941-10d763d9adef?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80",
"https://images.unsplash.com/photo-1677040628614-53936ff66632?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80",
"https://images.unsplash.com/photo-1676920410907-8d5f8dd4b5ba?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1332&q=80",
"https://images.unsplash.com/photo-1676846328604-ce831c481346?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1155&q=80",
"https://images.unsplash.com/photo-1676744843212-09b7e64c3a05?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80",
"https://images.unsplash.com/photo-1676798531090-1608bedeac7b?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80",
"https://images.unsplash.com/photo-1597088758740-56fd7ec8a3f0?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1169&q=80",
"https://images.unsplash.com/photo-1676638392418-80aad7c87b96?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=774&q=80",
"https://images.unsplash.com/photo-1649639194967-2fec0b4ea7bc?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80",
"https://images.unsplash.com/photo-1675883086902-b453b3f8146e?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=774&q=80",
"https://images.unsplash.com/photo-1675887057159-40fca28fdc5d?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1173&q=80",
"https://images.unsplash.com/photo-1675373980203-f84c5a672aa5?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80",
"https://images.unsplash.com/photo-1675191475318-d2bf6bad1200?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1332&q=80",
"https://images.unsplash.com/photo-1675456230532-2194d0c4bcc0?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80",
"https://images.unsplash.com/photo-1675371788315-60fa0ef48267?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1332&q=80",
]
projects = Project.objects.all()
updated_projects = []
for project in projects:
project.cover_image = project_cover_images[random.randint(0, 19)]
updated_projects.append(project)
Project.objects.bulk_update(updated_projects, ["cover_image"], batch_size=100)
print("Success")
except Exception as e:
print(e)
print("Failed")
+10 -2
View File
@@ -17,11 +17,12 @@ from .project import (
ProjectMemberSerializer,
ProjectMemberInviteSerializer,
ProjectIdentifierSerializer,
ProjectFavoriteSerializer,
)
from .state import StateSerializer
from .shortcut import ShortCutSerializer
from .view import ViewSerializer
from .cycle import CycleSerializer, CycleIssueSerializer
from .cycle import CycleSerializer, CycleIssueSerializer, CycleFavoriteSerializer
from .asset import FileAssetSerializer
from .issue import (
IssueCreateSerializer,
@@ -36,9 +37,16 @@ from .issue import (
IssueSerializer,
IssueFlatSerializer,
IssueStateSerializer,
IssueLinkSerializer,
)
from .module import ModuleWriteSerializer, ModuleSerializer, ModuleIssueSerializer
from .module import (
ModuleWriteSerializer,
ModuleSerializer,
ModuleIssueSerializer,
ModuleLinkSerializer,
ModuleFavoriteSerializer,
)
from .api_token import APITokenSerializer
+7 -1
View File
@@ -5,4 +5,10 @@ from plane.db.models import APIToken
class APITokenSerializer(BaseSerializer):
class Meta:
model = APIToken
fields = "__all__"
fields = [
"label",
"user",
"user_type",
"workspace",
"created_at",
]
+15 -3
View File
@@ -5,12 +5,12 @@ from rest_framework import serializers
from .base import BaseSerializer
from .user import UserLiteSerializer
from .issue import IssueStateSerializer
from plane.db.models import Cycle, CycleIssue
from plane.db.models import Cycle, CycleIssue, CycleFavorite
class CycleSerializer(BaseSerializer):
owned_by = UserLiteSerializer(read_only=True)
is_favorite = serializers.BooleanField(read_only=True)
class Meta:
model = Cycle
@@ -23,7 +23,6 @@ class CycleSerializer(BaseSerializer):
class CycleIssueSerializer(BaseSerializer):
issue_detail = IssueStateSerializer(read_only=True, source="issue")
sub_issues_count = serializers.IntegerField(read_only=True)
@@ -35,3 +34,16 @@ class CycleIssueSerializer(BaseSerializer):
"project",
"cycle",
]
class CycleFavoriteSerializer(BaseSerializer):
cycle_detail = CycleSerializer(source="cycle", read_only=True)
class Meta:
model = CycleFavorite
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"user",
]
+19 -49
View File
@@ -28,11 +28,6 @@ from plane.db.models import (
)
class IssueLinkCreateSerializer(serializers.Serializer):
url = serializers.CharField(required=True)
title = serializers.CharField(required=False)
class IssueFlatSerializer(BaseSerializer):
## Contain only flat fields
@@ -82,11 +77,6 @@ class IssueCreateSerializer(BaseSerializer):
write_only=True,
required=False,
)
links_list = serializers.ListField(
child=IssueLinkCreateSerializer(),
write_only=True,
required=False,
)
class Meta:
model = Issue
@@ -105,7 +95,6 @@ class IssueCreateSerializer(BaseSerializer):
assignees = validated_data.pop("assignees_list", None)
labels = validated_data.pop("labels_list", None)
blocks = validated_data.pop("blocks_list", None)
links = validated_data.pop("links_list", None)
project = self.context["project"]
issue = Issue.objects.create(**validated_data, project=project)
@@ -174,24 +163,6 @@ class IssueCreateSerializer(BaseSerializer):
batch_size=10,
)
if links is not None:
IssueLink.objects.bulk_create(
[
IssueLink(
issue=issue,
project=project,
workspace=project.workspace,
created_by=issue.created_by,
updated_by=issue.updated_by,
title=link.get("title", None),
url=link.get("url", None),
)
for link in links
],
batch_size=10,
ignore_conflicts=True,
)
return issue
def update(self, instance, validated_data):
@@ -199,7 +170,6 @@ class IssueCreateSerializer(BaseSerializer):
assignees = validated_data.pop("assignees_list", None)
labels = validated_data.pop("labels_list", None)
blocks = validated_data.pop("blocks_list", None)
links = validated_data.pop("links_list", None)
if blockers is not None:
IssueBlocker.objects.filter(block=instance).delete()
@@ -269,25 +239,6 @@ class IssueCreateSerializer(BaseSerializer):
batch_size=10,
)
if links is not None:
IssueLink.objects.filter(issue=instance).delete()
IssueLink.objects.bulk_create(
[
IssueLink(
issue=instance,
project=instance.project,
workspace=instance.project.workspace,
created_by=instance.created_by,
updated_by=instance.updated_by,
title=link.get("title", None),
url=link.get("url", None),
)
for link in links
],
batch_size=10,
ignore_conflicts=True,
)
return super().update(instance, validated_data)
@@ -456,6 +407,25 @@ class IssueLinkSerializer(BaseSerializer):
class Meta:
model = IssueLink
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"created_by",
"updated_by",
"created_at",
"updated_at",
"issue",
]
# Validation if url already exists
def create(self, validated_data):
if IssueLink.objects.filter(
url=validated_data.get("url"), issue_id=validated_data.get("issue_id")
).exists():
raise serializers.ValidationError(
{"error": "URL already exists for this Issue"}
)
return IssueLink.objects.create(**validated_data)
# Issue Serializer with state details
+15 -57
View File
@@ -7,27 +7,15 @@ from .user import UserLiteSerializer
from .project import ProjectSerializer
from .issue import IssueStateSerializer
from plane.db.models import User, Module, ModuleMember, ModuleIssue, ModuleLink
class LinkCreateSerializer(serializers.Serializer):
url = serializers.CharField(required=True)
title = serializers.CharField(required=False)
from plane.db.models import User, Module, ModuleMember, ModuleIssue, ModuleLink, ModuleFavorite
class ModuleWriteSerializer(BaseSerializer):
members_list = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()),
write_only=True,
required=False,
)
links_list = serializers.ListField(
child=LinkCreateSerializer(),
write_only=True,
required=False,
)
class Meta:
model = Module
@@ -42,9 +30,7 @@ class ModuleWriteSerializer(BaseSerializer):
]
def create(self, validated_data):
members = validated_data.pop("members_list", None)
links = validated_data.pop("links_list", None)
project = self.context["project"]
@@ -67,30 +53,10 @@ class ModuleWriteSerializer(BaseSerializer):
ignore_conflicts=True,
)
if links is not None:
ModuleLink.objects.bulk_create(
[
ModuleLink(
module=module,
project=project,
workspace=project.workspace,
created_by=module.created_by,
updated_by=module.updated_by,
title=link.get("title", None),
url=link.get("url", None),
)
for link in links
],
batch_size=10,
ignore_conflicts=True,
)
return module
def update(self, instance, validated_data):
members = validated_data.pop("members_list", None)
links = validated_data.pop("links_list", None)
if members is not None:
ModuleMember.objects.filter(module=instance).delete()
@@ -110,25 +76,6 @@ class ModuleWriteSerializer(BaseSerializer):
ignore_conflicts=True,
)
if links is not None:
ModuleLink.objects.filter(module=instance).delete()
ModuleLink.objects.bulk_create(
[
ModuleLink(
module=instance,
project=instance.project,
workspace=instance.project.workspace,
created_by=instance.created_by,
updated_by=instance.updated_by,
title=link.get("title", None),
url=link.get("url", None),
)
for link in links
],
batch_size=10,
ignore_conflicts=True,
)
return super().update(instance, validated_data)
@@ -147,7 +94,6 @@ class ModuleFlatSerializer(BaseSerializer):
class ModuleIssueSerializer(BaseSerializer):
module_detail = ModuleFlatSerializer(read_only=True, source="module")
issue_detail = IssueStateSerializer(read_only=True, source="issue")
sub_issues_count = serializers.IntegerField(read_only=True)
@@ -167,7 +113,6 @@ class ModuleIssueSerializer(BaseSerializer):
class ModuleLinkSerializer(BaseSerializer):
created_by_detail = UserLiteSerializer(read_only=True, source="created_by")
class Meta:
@@ -180,16 +125,17 @@ class ModuleLinkSerializer(BaseSerializer):
"updated_by",
"created_at",
"updated_at",
"module",
]
class ModuleSerializer(BaseSerializer):
project_detail = ProjectSerializer(read_only=True, source="project")
lead_detail = UserLiteSerializer(read_only=True, source="lead")
members_detail = UserLiteSerializer(read_only=True, many=True, source="members")
issue_module = ModuleIssueSerializer(read_only=True, many=True)
link_module = ModuleLinkSerializer(read_only=True, many=True)
is_favorite = serializers.BooleanField(read_only=True)
class Meta:
model = Module
@@ -202,3 +148,15 @@ class ModuleSerializer(BaseSerializer):
"created_at",
"updated_at",
]
class ModuleFavoriteSerializer(BaseSerializer):
module_detail = ModuleFlatSerializer(source="module", read_only=True)
class Meta:
model = ModuleFavorite
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"user",
]
+14 -4
View File
@@ -13,6 +13,7 @@ from plane.db.models import (
ProjectMember,
ProjectMemberInvite,
ProjectIdentifier,
ProjectFavorite,
)
@@ -44,7 +45,6 @@ class ProjectSerializer(BaseSerializer):
return project
def update(self, instance, validated_data):
identifier = validated_data.get("identifier", "").strip().upper()
# If identifier is not passed update the project and return
@@ -73,10 +73,10 @@ class ProjectSerializer(BaseSerializer):
class ProjectDetailSerializer(BaseSerializer):
workspace = WorkSpaceSerializer(read_only=True)
default_assignee = UserLiteSerializer(read_only=True)
project_lead = UserLiteSerializer(read_only=True)
is_favorite = serializers.BooleanField(read_only=True)
class Meta:
model = Project
@@ -84,7 +84,6 @@ class ProjectDetailSerializer(BaseSerializer):
class ProjectMemberSerializer(BaseSerializer):
workspace = WorkSpaceSerializer(read_only=True)
project = ProjectSerializer(read_only=True)
member = UserLiteSerializer(read_only=True)
@@ -95,7 +94,6 @@ class ProjectMemberSerializer(BaseSerializer):
class ProjectMemberInviteSerializer(BaseSerializer):
project = ProjectSerializer(read_only=True)
workspace = WorkSpaceSerializer(read_only=True)
@@ -108,3 +106,15 @@ class ProjectIdentifierSerializer(BaseSerializer):
class Meta:
model = ProjectIdentifier
fields = "__all__"
class ProjectFavoriteSerializer(BaseSerializer):
project_detail = ProjectSerializer(source="project", read_only=True)
class Meta:
model = ProjectFavorite
fields = "__all__"
read_only_fields = [
"workspace",
"user",
]
+135
View File
@@ -52,6 +52,7 @@ from plane.api.views import (
ProjectJoinEndpoint,
UserProjectInvitationsViewset,
ProjectIdentifierEndpoint,
ProjectFavoritesViewSet,
## End Projects
# Issues
IssueViewSet,
@@ -65,6 +66,8 @@ from plane.api.views import (
IssuePropertyViewSet,
LabelViewSet,
SubIssuesEndpoint,
IssueLinkViewSet,
ModuleLinkViewSet,
## End Issues
# States
StateViewSet,
@@ -78,10 +81,16 @@ from plane.api.views import (
# Cycles
CycleViewSet,
CycleIssueViewSet,
CycleDateCheckEndpoint,
CurrentUpcomingCyclesEndpoint,
CompletedCyclesEndpoint,
CycleFavoriteViewSet,
DraftCyclesEndpoint,
## End Cycles
# Modules
ModuleViewSet,
ModuleIssueViewSet,
ModuleFavoriteViewSet,
## End Modules
# Api Tokens
ApiTokenEndpoint,
@@ -372,6 +381,25 @@ urlpatterns = [
ProjectMemberUserEndpoint.as_view(),
name="project-view",
),
path(
"workspaces/<str:slug>/user-favorite-projects/",
ProjectFavoritesViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project",
),
path(
"workspaces/<str:slug>/user-favorite-projects/<uuid:project_id>/",
ProjectFavoritesViewSet.as_view(
{
"delete": "destroy",
}
),
name="project",
),
# End Projects
# States
path(
@@ -490,6 +518,45 @@ urlpatterns = [
),
name="project-cycle",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/date-check/",
CycleDateCheckEndpoint.as_view(),
name="project-cycle",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/current-upcoming-cycles/",
CurrentUpcomingCyclesEndpoint.as_view(),
name="project-cycle-upcoming",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/completed-cycles/",
CompletedCyclesEndpoint.as_view(),
name="project-cycle-completed",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/draft-cycles/",
DraftCyclesEndpoint.as_view(),
name="project-cycle-draft",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-cycles/",
CycleFavoriteViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="user-favorite-cycle",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-cycles/<uuid:cycle_id>/",
CycleFavoriteViewSet.as_view(
{
"delete": "destroy",
}
),
name="user-favorite-cycle",
),
## End Cycles
# Issue
path(
@@ -555,6 +622,28 @@ urlpatterns = [
SubIssuesEndpoint.as_view(),
name="sub-issues",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-links/",
IssueLinkViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-issue-links",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-links/<uuid:pk>/",
IssueLinkViewSet.as_view(
{
"get": "retrieve",
"put": "update",
"patch": "partial_update",
"delete": "destroy",
}
),
name="project-issue-links",
),
## End Issues
## Issue Activity
path(
@@ -641,6 +730,11 @@ urlpatterns = [
FileAssetEndpoint.as_view(),
name="File Assets",
),
path(
"workspaces/<str:slug>/file-assets/<uuid:pk>/",
FileAssetEndpoint.as_view(),
name="File Assets",
),
## End File Assets
## Modules
path(
@@ -687,6 +781,47 @@ urlpatterns = [
),
name="project-module-issues",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/module-links/",
ModuleLinkViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-issue-module-links",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/module-links/<uuid:pk>/",
ModuleLinkViewSet.as_view(
{
"get": "retrieve",
"put": "update",
"patch": "partial_update",
"delete": "destroy",
}
),
name="project-issue-module-links",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-modules/",
ModuleFavoriteViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="user-favorite-module",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-modules/<uuid:module_id>/",
ModuleFavoriteViewSet.as_view(
{
"delete": "destroy",
}
),
name="user-favorite-module",
),
## End Modules
# API Tokens
path("api-tokens/", ApiTokenEndpoint.as_view(), name="api-tokens"),
+17 -2
View File
@@ -11,6 +11,7 @@ from .project import (
ProjectJoinEndpoint,
ProjectUserViewsEndpoint,
ProjectMemberUserEndpoint,
ProjectFavoritesViewSet,
)
from .people import (
UserEndpoint,
@@ -39,7 +40,15 @@ from .workspace import (
from .state import StateViewSet
from .shortcut import ShortCutViewSet
from .view import ViewViewSet
from .cycle import CycleViewSet, CycleIssueViewSet
from .cycle import (
CycleViewSet,
CycleIssueViewSet,
CycleDateCheckEndpoint,
CurrentUpcomingCyclesEndpoint,
CompletedCyclesEndpoint,
CycleFavoriteViewSet,
DraftCyclesEndpoint,
)
from .asset import FileAssetEndpoint
from .issue import (
IssueViewSet,
@@ -52,6 +61,7 @@ from .issue import (
BulkDeleteIssuesEndpoint,
UserWorkSpaceIssues,
SubIssuesEndpoint,
IssueLinkViewSet,
)
from .auth_extended import (
@@ -70,7 +80,12 @@ from .authentication import (
MagicSignInGenerateEndpoint,
)
from .module import ModuleViewSet, ModuleIssueViewSet
from .module import (
ModuleViewSet,
ModuleIssueViewSet,
ModuleLinkViewSet,
ModuleFavoriteViewSet,
)
from .api_token import ApiTokenEndpoint
+5 -1
View File
@@ -28,7 +28,11 @@ class ApiTokenEndpoint(BaseAPIView):
)
serializer = APITokenSerializer(api_token)
return Response(serializer.data, status=status.HTTP_201_CREATED)
# Token will be only vissible while creating
return Response(
{"api_token": serializer.data, "token": api_token.token},
status=status.HTTP_201_CREATED,
)
except Exception as e:
capture_exception(e)
+19 -2
View File
@@ -11,7 +11,6 @@ from plane.api.serializers import FileAssetSerializer
class FileAssetEndpoint(BaseAPIView):
parser_classes = (MultiPartParser, FormParser)
"""
@@ -27,7 +26,6 @@ class FileAssetEndpoint(BaseAPIView):
try:
serializer = FileAssetSerializer(data=request.data)
if serializer.is_valid():
if request.user.last_workspace_id is None:
return Response(
{"error": "Workspace id is required"},
@@ -43,3 +41,22 @@ class FileAssetEndpoint(BaseAPIView):
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
def delete(self, request, slug, pk):
try:
file_asset = FileAsset.objects.get(pk=pk, workspace__slug=slug)
# Delete the file from storage
file_asset.asset.delete(save=False)
# Delete the file object
file_asset.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
except FileAsset.DoesNotExist:
return Response(
{"error": "File Asset doesn't exist"}, status=status.HTTP_404_NOT_FOUND
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
+274 -4
View File
@@ -2,8 +2,10 @@
import json
# Django imports
from django.db.models import OuterRef, Func, F
from django.db import IntegrityError
from django.db.models import OuterRef, Func, F, Q, Exists, OuterRef
from django.core import serializers
from django.utils import timezone
# Third party imports
from rest_framework.response import Response
@@ -11,11 +13,16 @@ from rest_framework import status
from sentry_sdk import capture_exception
# Module imports
from . import BaseViewSet
from plane.api.serializers import CycleSerializer, CycleIssueSerializer
from . import BaseViewSet, BaseAPIView
from plane.api.serializers import (
CycleSerializer,
CycleIssueSerializer,
CycleFavoriteSerializer,
)
from plane.api.permissions import ProjectEntityPermission
from plane.db.models import Cycle, CycleIssue, Issue
from plane.db.models import Cycle, CycleIssue, Issue, CycleFavorite
from plane.bgtasks.issue_activites_task import issue_activity
from plane.utils.grouper import group_results
class CycleViewSet(BaseViewSet):
@@ -43,6 +50,54 @@ class CycleViewSet(BaseViewSet):
.distinct()
)
def list(self, request, slug, project_id):
try:
subquery = CycleFavorite.objects.filter(
user=self.request.user,
cycle_id=OuterRef("pk"),
project_id=project_id,
workspace__slug=slug,
)
cycles = self.get_queryset().annotate(is_favorite=Exists(subquery))
return Response(CycleSerializer(cycles, many=True).data)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
def create(self, request, slug, project_id):
try:
if (
request.data.get("start_date", None) is None
and request.data.get("end_date", None) is None
) or (
request.data.get("start_date", None) is not None
and request.data.get("end_date", None) is not None
):
serializer = CycleSerializer(data=request.data)
if serializer.is_valid():
serializer.save(
project_id=project_id,
owned_by=request.user,
)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
else:
return Response(
{
"error": "Both start date and end date are either required or are to be null"
},
status=status.HTTP_400_BAD_REQUEST,
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class CycleIssueViewSet(BaseViewSet):
serializer_class = CycleIssueSerializer
@@ -52,6 +107,11 @@ class CycleIssueViewSet(BaseViewSet):
ProjectEntityPermission,
]
filterset_fields = [
"issue__labels__id",
"issue__assignees__id",
]
def perform_create(self, serializer):
serializer.save(
project_id=self.kwargs.get("project_id"),
@@ -80,6 +140,31 @@ class CycleIssueViewSet(BaseViewSet):
.distinct()
)
def list(self, request, slug, project_id, cycle_id):
try:
order_by = request.GET.get("order_by", "created_at")
queryset = self.get_queryset().order_by(f"issue__{order_by}")
group_by = request.GET.get("group_by", False)
cycle_issues = CycleIssueSerializer(queryset, many=True).data
if group_by:
return Response(
group_results(cycle_issues, f"issue_detail.{group_by}"),
status=status.HTTP_200_OK,
)
return Response(
cycle_issues,
status=status.HTTP_200_OK,
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
def create(self, request, slug, project_id, cycle_id):
try:
issues = request.data.get("issues", [])
@@ -175,3 +260,188 @@ class CycleIssueViewSet(BaseViewSet):
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class CycleDateCheckEndpoint(BaseAPIView):
def post(self, request, slug, project_id):
try:
start_date = request.data.get("start_date")
end_date = request.data.get("end_date")
cycles = Cycle.objects.filter(
Q(start_date__lte=start_date, end_date__gte=start_date)
| Q(start_date__gte=end_date, end_date__lte=end_date),
workspace__slug=slug,
project_id=project_id,
)
if cycles.exists():
return Response(
{
"error": "You have a cycle already on the given dates, if you want to create your draft cycle you can do that by removing dates",
"cycles": CycleSerializer(cycles, many=True).data,
"status": False,
}
)
else:
return Response({"status": True}, status=status.HTTP_200_OK)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class CurrentUpcomingCyclesEndpoint(BaseAPIView):
def get(self, request, slug, project_id):
try:
subquery = CycleFavorite.objects.filter(
user=self.request.user,
cycle_id=OuterRef("pk"),
project_id=project_id,
workspace__slug=slug,
)
current_cycle = Cycle.objects.filter(
workspace__slug=slug,
project_id=project_id,
start_date__lte=timezone.now(),
end_date__gte=timezone.now(),
).annotate(is_favorite=Exists(subquery))
upcoming_cycle = Cycle.objects.filter(
workspace__slug=slug,
project_id=project_id,
start_date__gt=timezone.now(),
).annotate(is_favorite=Exists(subquery))
return Response(
{
"current_cycle": CycleSerializer(current_cycle, many=True).data,
"upcoming_cycle": CycleSerializer(upcoming_cycle, many=True).data,
},
status=status.HTTP_200_OK,
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class CompletedCyclesEndpoint(BaseAPIView):
def get(self, request, slug, project_id):
try:
subquery = CycleFavorite.objects.filter(
user=self.request.user,
cycle_id=OuterRef("pk"),
project_id=project_id,
workspace__slug=slug,
)
completed_cycles = Cycle.objects.filter(
workspace__slug=slug,
project_id=project_id,
end_date__lt=timezone.now(),
).annotate(is_favorite=Exists(subquery))
return Response(
{
"completed_cycles": CycleSerializer(
completed_cycles, many=True
).data,
},
status=status.HTTP_200_OK,
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class DraftCyclesEndpoint(BaseAPIView):
def get(self, request, slug, project_id):
try:
draft_cycles = Cycle.objects.filter(
workspace__slug=slug,
project_id=project_id,
end_date=None,
start_date=None,
)
return Response(
{"draft_cycles": CycleSerializer(draft_cycles, many=True).data},
status=status.HTTP_200_OK,
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class CycleFavoriteViewSet(BaseViewSet):
serializer_class = CycleFavoriteSerializer
model = CycleFavorite
def get_queryset(self):
return self.filter_queryset(
super()
.get_queryset()
.filter(workspace__slug=self.kwargs.get("slug"))
.filter(user=self.request.user)
.select_related("cycle", "cycle__owned_by")
)
def create(self, request, slug, project_id):
try:
serializer = CycleFavoriteSerializer(data=request.data)
if serializer.is_valid():
serializer.save(user=request.user, project_id=project_id)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except IntegrityError as e:
if "already exists" in str(e):
return Response(
{"error": "The cycle is already added to favorites"},
status=status.HTTP_410_GONE,
)
else:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
def destroy(self, request, slug, project_id, cycle_id):
try:
cycle_favorite = CycleFavorite.objects.get(
project=project_id,
user=request.user,
workspace__slug=slug,
cycle_id=cycle_id,
)
cycle_favorite.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
except CycleFavorite.DoesNotExist:
return Response(
{"error": "Cycle is not in favorites"},
status=status.HTTP_400_BAD_REQUEST,
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
@@ -25,11 +25,15 @@ from plane.utils.integrations.github import get_github_repos
class GithubRepositoriesEndpoint(BaseAPIView):
def get(self, request, slug, workspace_integration_id):
try:
page = request.GET.get("page", 1)
workspace_integration = WorkspaceIntegration.objects.get(
workspace__slug=slug, pk=workspace_integration_id
)
access_tokens_url = workspace_integration.metadata["access_tokens_url"]
repositories_url = workspace_integration.metadata["repositories_url"]
repositories_url = (
workspace_integration.metadata["repositories_url"]
+ f"?per_page=100&page={page}"
)
repositories = get_github_repos(access_tokens_url, repositories_url)
return Response(repositories, status=status.HTTP_200_OK)
except WorkspaceIntegration.DoesNotExist:
+28 -1
View File
@@ -23,6 +23,7 @@ from plane.api.serializers import (
IssueSerializer,
LabelSerializer,
IssueFlatSerializer,
IssueLinkSerializer,
)
from plane.api.permissions import (
ProjectEntityPermission,
@@ -185,7 +186,7 @@ class IssueViewSet(BaseViewSet):
)
issues = IssueSerializer(issue_queryset, many=True).data
## Grouping the results
group_by = request.GET.get("group_by", False)
if group_by:
@@ -690,3 +691,29 @@ class SubIssuesEndpoint(BaseAPIView):
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class IssueLinkViewSet(BaseViewSet):
permission_classes = [
ProjectEntityPermission,
]
model = IssueLink
serializer_class = IssueLinkSerializer
def perform_create(self, serializer):
serializer.save(
project_id=self.kwargs.get("project_id"),
issue_id=self.kwargs.get("issue_id"),
)
def get_queryset(self):
return (
super()
.get_queryset()
.filter(workspace__slug=self.kwargs.get("slug"))
.filter(project_id=self.kwargs.get("project_id"))
.filter(issue_id=self.kwargs.get("issue_id"))
.filter(project__project_projectmember__member=self.request.user)
.distinct()
)
+137 -3
View File
@@ -3,7 +3,7 @@ import json
# Django Imports
from django.db import IntegrityError
from django.db.models import Prefetch, F, OuterRef, Func
from django.db.models import Prefetch, F, OuterRef, Func, Exists
from django.core import serializers
# Third party imports
@@ -17,6 +17,8 @@ from plane.api.serializers import (
ModuleWriteSerializer,
ModuleSerializer,
ModuleIssueSerializer,
ModuleLinkSerializer,
ModuleFavoriteSerializer,
)
from plane.api.permissions import ProjectEntityPermission
from plane.db.models import (
@@ -25,8 +27,10 @@ from plane.db.models import (
Project,
Issue,
ModuleLink,
ModuleFavorite,
)
from plane.bgtasks.issue_activites_task import issue_activity
from plane.utils.grouper import group_results
class ModuleViewSet(BaseViewSet):
@@ -97,14 +101,31 @@ class ModuleViewSet(BaseViewSet):
status=status.HTTP_400_BAD_REQUEST,
)
def list(self, request, slug, project_id):
try:
subquery = ModuleFavorite.objects.filter(
user=self.request.user,
module_id=OuterRef("pk"),
project_id=project_id,
workspace__slug=slug,
)
modules = self.get_queryset().annotate(is_favorite=Exists(subquery))
return Response(ModuleSerializer(modules, many=True).data)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class ModuleIssueViewSet(BaseViewSet):
serializer_class = ModuleIssueSerializer
model = ModuleIssue
filterset_fields = [
"issue__id",
"workspace__id",
"issue__labels__id",
"issue__assignees__id",
]
permission_classes = [
@@ -140,6 +161,31 @@ class ModuleIssueViewSet(BaseViewSet):
.distinct()
)
def list(self, request, slug, project_id, module_id):
try:
order_by = request.GET.get("order_by", "issue__created_at")
queryset = self.get_queryset().order_by(f"issue__{order_by}")
group_by = request.GET.get("group_by", False)
module_issues = ModuleIssueSerializer(queryset, many=True).data
if group_by:
return Response(
group_results(module_issues, f"issue_detail.{group_by}"),
status=status.HTTP_200_OK,
)
return Response(
module_issues,
status=status.HTTP_200_OK,
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
def create(self, request, slug, project_id, module_id):
try:
issues = request.data.get("issues", [])
@@ -232,3 +278,91 @@ class ModuleIssueViewSet(BaseViewSet):
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class ModuleLinkViewSet(BaseViewSet):
permission_classes = [
ProjectEntityPermission,
]
model = ModuleLink
serializer_class = ModuleLinkSerializer
def perform_create(self, serializer):
serializer.save(
project_id=self.kwargs.get("project_id"),
module_id=self.kwargs.get("module_id"),
)
def get_queryset(self):
return (
super()
.get_queryset()
.filter(workspace__slug=self.kwargs.get("slug"))
.filter(project_id=self.kwargs.get("project_id"))
.filter(module_id=self.kwargs.get("module_id"))
.filter(project__project_projectmember__member=self.request.user)
.distinct()
)
class ModuleFavoriteViewSet(BaseViewSet):
serializer_class = ModuleFavoriteSerializer
model = ModuleFavorite
def get_queryset(self):
return self.filter_queryset(
super()
.get_queryset()
.filter(workspace__slug=self.kwargs.get("slug"))
.filter(user=self.request.user)
.select_related("module")
)
def create(self, request, slug, project_id):
try:
serializer = ModuleFavoriteSerializer(data=request.data)
if serializer.is_valid():
serializer.save(user=request.user, project_id=project_id)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except IntegrityError as e:
if "already exists" in str(e):
return Response(
{"error": "The module is already added to favorites"},
status=status.HTTP_410_GONE,
)
else:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
def destroy(self, request, slug, project_id, module_id):
try:
module_favorite = ModuleFavorite.objects.get(
project=project_id,
user=request.user,
workspace__slug=slug,
module_id=module_id,
)
module_favorite.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
except ModuleFavorite.DoesNotExist:
return Response(
{"error": "Module is not in favorites"},
status=status.HTTP_400_BAD_REQUEST,
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
+86 -1
View File
@@ -5,7 +5,7 @@ from datetime import datetime
# Django imports
from django.core.exceptions import ValidationError
from django.db import IntegrityError
from django.db.models import Q
from django.db.models import Q, Exists, OuterRef
from django.core.validators import validate_email
from django.conf import settings
@@ -22,6 +22,7 @@ from plane.api.serializers import (
ProjectMemberSerializer,
ProjectDetailSerializer,
ProjectMemberInviteSerializer,
ProjectFavoriteSerializer,
)
from plane.api.permissions import ProjectBasePermission
@@ -35,6 +36,7 @@ from plane.db.models import (
WorkspaceMember,
State,
TeamMember,
ProjectFavorite,
)
from plane.db.models import (
@@ -73,6 +75,22 @@ class ProjectViewSet(BaseViewSet):
.distinct()
)
def list(self, request, slug):
try:
subquery = ProjectFavorite.objects.filter(
user=self.request.user,
project_id=OuterRef("pk"),
workspace__slug=self.kwargs.get("slug"),
)
projects = self.get_queryset().annotate(is_favorite=Exists(subquery))
return Response(ProjectDetailSerializer(projects, many=True).data)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
def create(self, request, slug):
try:
workspace = Workspace.objects.get(slug=slug)
@@ -345,6 +363,7 @@ class ProjectMemberViewSet(BaseViewSet):
.get_queryset()
.filter(workspace__slug=self.kwargs.get("slug"))
.filter(project_id=self.kwargs.get("project_id"))
.filter(member__is_bot=False)
.select_related("project")
.select_related("member")
.select_related("workspace", "workspace__owner")
@@ -659,3 +678,69 @@ class ProjectMemberUserEndpoint(BaseAPIView):
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class ProjectFavoritesViewSet(BaseViewSet):
serializer_class = ProjectFavoriteSerializer
model = ProjectFavorite
def get_queryset(self):
return self.filter_queryset(
super()
.get_queryset()
.filter(workspace__slug=self.kwargs.get("slug"))
.filter(user=self.request.user)
.select_related(
"project", "project__project_lead", "project__default_assignee"
)
.select_related("workspace", "workspace__owner")
)
def perform_create(self, serializer):
serializer.save(user=self.request.user)
def create(self, request, slug):
try:
serializer = ProjectFavoriteSerializer(data=request.data)
if serializer.is_valid():
serializer.save(user=request.user)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except IntegrityError as e:
print(str(e))
if "already exists" in str(e):
return Response(
{"error": "The project is already added to favorites"},
status=status.HTTP_410_GONE,
)
else:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_410_GONE,
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
def destroy(self, request, slug, project_id):
try:
project_favorite = ProjectFavorite.objects.get(
project=project_id, user=request.user, workspace__slug=slug
)
project_favorite.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
except ProjectFavorite.DoesNotExist:
return Response(
{"error": "Project is not in favorites"},
status=status.HTTP_400_BAD_REQUEST,
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
+1 -19
View File
@@ -43,7 +43,6 @@ from plane.bgtasks.workspace_invitation_task import workspace_invitation
class WorkSpaceViewSet(BaseViewSet):
model = Workspace
serializer_class = WorkSpaceSerializer
permission_classes = [
@@ -101,7 +100,6 @@ class WorkSpaceViewSet(BaseViewSet):
class UserWorkSpacesEndpoint(BaseAPIView):
search_fields = [
"name",
]
@@ -111,7 +109,6 @@ class UserWorkSpacesEndpoint(BaseAPIView):
def get(self, request):
try:
member_count = (
WorkspaceMember.objects.filter(workspace=OuterRef("id"))
.order_by()
@@ -163,14 +160,12 @@ class WorkSpaceAvailabilityCheckEndpoint(BaseAPIView):
class InviteWorkspaceEndpoint(BaseAPIView):
permission_classes = [
WorkSpaceAdminPermission,
]
def post(self, request, slug):
try:
emails = request.data.get("emails", False)
# Check if email is provided
if not emails or not len(emails):
@@ -267,7 +262,6 @@ class JoinWorkspaceEndpoint(BaseAPIView):
def post(self, request, slug, pk):
try:
workspace_invite = WorkspaceMemberInvite.objects.get(
pk=pk, workspace__slug=slug
)
@@ -286,7 +280,6 @@ class JoinWorkspaceEndpoint(BaseAPIView):
workspace_invite.save()
if workspace_invite.accepted:
# Check if the user created account after invitation
user = User.objects.filter(email=email).first()
@@ -334,7 +327,6 @@ class JoinWorkspaceEndpoint(BaseAPIView):
class WorkspaceInvitationsViewset(BaseViewSet):
serializer_class = WorkSpaceMemberInviteSerializer
model = WorkspaceMemberInvite
@@ -352,7 +344,6 @@ class WorkspaceInvitationsViewset(BaseViewSet):
class UserWorkspaceInvitationsEndpoint(BaseViewSet):
serializer_class = WorkSpaceMemberInviteSerializer
model = WorkspaceMemberInvite
@@ -366,7 +357,6 @@ class UserWorkspaceInvitationsEndpoint(BaseViewSet):
def create(self, request):
try:
invitations = request.data.get("invitations")
workspace_invitations = WorkspaceMemberInvite.objects.filter(
pk__in=invitations
@@ -397,7 +387,6 @@ class UserWorkspaceInvitationsEndpoint(BaseViewSet):
class WorkSpaceMemberViewSet(BaseViewSet):
serializer_class = WorkSpaceMemberSerializer
model = WorkspaceMember
@@ -414,14 +403,13 @@ class WorkSpaceMemberViewSet(BaseViewSet):
return self.filter_queryset(
super()
.get_queryset()
.filter(workspace__slug=self.kwargs.get("slug"))
.filter(workspace__slug=self.kwargs.get("slug"), member__is_bot=False)
.select_related("workspace", "workspace__owner")
.select_related("member")
)
class TeamMemberViewSet(BaseViewSet):
serializer_class = TeamSerializer
model = Team
permission_classes = [
@@ -443,9 +431,7 @@ class TeamMemberViewSet(BaseViewSet):
)
def create(self, request, slug):
try:
members = list(
WorkspaceMember.objects.filter(
workspace__slug=slug, member__id__in=request.data.get("members", [])
@@ -456,7 +442,6 @@ class TeamMemberViewSet(BaseViewSet):
)
if len(members) != len(request.data.get("members", [])):
users = list(set(request.data.get("members", [])).difference(members))
users = User.objects.filter(pk__in=users)
@@ -493,7 +478,6 @@ class TeamMemberViewSet(BaseViewSet):
class UserWorkspaceInvitationEndpoint(BaseViewSet):
model = WorkspaceMemberInvite
serializer_class = WorkSpaceMemberInviteSerializer
@@ -513,7 +497,6 @@ class UserWorkspaceInvitationEndpoint(BaseViewSet):
class UserLastProjectWithWorkspaceEndpoint(BaseAPIView):
def get(self, request):
try:
user = User.objects.get(pk=request.user.id)
last_workspace_id = user.last_workspace_id
@@ -577,7 +560,6 @@ class WorkspaceMemberUserEndpoint(BaseAPIView):
class WorkspaceMemberUserViewsEndpoint(BaseAPIView):
def post(self, request, slug):
try:
workspace_member = WorkspaceMember.objects.get(
workspace__slug=slug, member=request.user
)
@@ -0,0 +1,101 @@
# Generated by Django 3.2.16 on 2023-03-06 21:34
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
dependencies = [
('db', '0021_auto_20230223_0104'),
]
operations = [
migrations.RemoveField(
model_name='cycle',
name='status',
),
migrations.RemoveField(
model_name='project',
name='slug',
),
migrations.AddField(
model_name='issuelink',
name='metadata',
field=models.JSONField(default=dict),
),
migrations.AddField(
model_name='modulelink',
name='metadata',
field=models.JSONField(default=dict),
),
migrations.AddField(
model_name='project',
name='cover_image',
field=models.URLField(blank=True, null=True),
),
migrations.CreateModel(
name='ProjectFavorite',
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='projectfavorite_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_projectfavorite', to='db.project')),
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='projectfavorite_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_favorites', to=settings.AUTH_USER_MODEL)),
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_projectfavorite', to='db.workspace')),
],
options={
'verbose_name': 'Project Favorite',
'verbose_name_plural': 'Project Favorites',
'db_table': 'project_favorites',
'ordering': ('-created_at',),
'unique_together': {('project', 'user')},
},
),
migrations.CreateModel(
name='ModuleFavorite',
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='modulefavorite_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
('module', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='module_favorites', to='db.module')),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_modulefavorite', to='db.project')),
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='modulefavorite_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='module_favorites', to=settings.AUTH_USER_MODEL)),
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_modulefavorite', to='db.workspace')),
],
options={
'verbose_name': 'Module Favorite',
'verbose_name_plural': 'Module Favorites',
'db_table': 'module_favorites',
'ordering': ('-created_at',),
'unique_together': {('module', 'user')},
},
),
migrations.CreateModel(
name='CycleFavorite',
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='cyclefavorite_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
('cycle', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='cycle_favorites', to='db.cycle')),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_cyclefavorite', to='db.project')),
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='cyclefavorite_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='cycle_favorites', to=settings.AUTH_USER_MODEL)),
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_cyclefavorite', to='db.workspace')),
],
options={
'verbose_name': 'Cycle Favorite',
'verbose_name_plural': 'Cycle Favorites',
'db_table': 'cycle_favorites',
'ordering': ('-created_at',),
'unique_together': {('cycle', 'user')},
},
),
]
+3 -2
View File
@@ -16,6 +16,7 @@ from .project import (
ProjectBaseModel,
ProjectMemberInvite,
ProjectIdentifier,
ProjectFavorite,
)
from .issue import (
@@ -38,13 +39,13 @@ from .social_connection import SocialLoginConnection
from .state import State
from .cycle import Cycle, CycleIssue
from .cycle import Cycle, CycleIssue, CycleFavorite
from .shortcut import Shortcut
from .view import View
from .module import Module, ModuleMember, ModuleIssue, ModuleLink
from .module import Module, ModuleMember, ModuleIssue, ModuleLink, ModuleFavorite
from .api_token import APIToken
+26 -11
View File
@@ -7,11 +7,6 @@ from . import ProjectBaseModel
class Cycle(ProjectBaseModel):
STATUS_CHOICES = (
("draft", "Draft"),
("started", "Started"),
("completed", "Completed"),
)
name = models.CharField(max_length=255, verbose_name="Cycle Name")
description = models.TextField(verbose_name="Cycle Description", blank=True)
start_date = models.DateField(verbose_name="Start Date", blank=True, null=True)
@@ -21,12 +16,6 @@ class Cycle(ProjectBaseModel):
on_delete=models.CASCADE,
related_name="owned_by_cycle",
)
status = models.CharField(
max_length=255,
verbose_name="Cycle Status",
choices=STATUS_CHOICES,
default="draft",
)
class Meta:
verbose_name = "Cycle"
@@ -59,3 +48,29 @@ class CycleIssue(ProjectBaseModel):
def __str__(self):
return f"{self.cycle}"
class CycleFavorite(ProjectBaseModel):
"""_summary_
CycleFavorite (model): To store all the cycle favorite of the user
"""
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="cycle_favorites",
)
cycle = models.ForeignKey(
"db.Cycle", on_delete=models.CASCADE, related_name="cycle_favorites"
)
class Meta:
unique_together = ["cycle", "user"]
verbose_name = "Cycle Favorite"
verbose_name_plural = "Cycle Favorites"
db_table = "cycle_favorites"
ordering = ("-created_at",)
def __str__(self):
"""Return user and the cycle"""
return f"{self.user.email} <{self.cycle.name}>"
+1
View File
@@ -174,6 +174,7 @@ class IssueLink(ProjectBaseModel):
issue = models.ForeignKey(
"db.Issue", on_delete=models.CASCADE, related_name="issue_link"
)
metadata = models.JSONField(default=dict)
class Meta:
verbose_name = "Issue Link"
+31 -7
View File
@@ -7,7 +7,6 @@ from . import ProjectBaseModel
class Module(ProjectBaseModel):
name = models.CharField(max_length=255, verbose_name="Module Name")
description = models.TextField(verbose_name="Module Description", blank=True)
description_text = models.JSONField(
@@ -41,7 +40,6 @@ class Module(ProjectBaseModel):
through_fields=("module", "member"),
)
class Meta:
unique_together = ["name", "project"]
verbose_name = "Module"
@@ -54,7 +52,6 @@ class Module(ProjectBaseModel):
class ModuleMember(ProjectBaseModel):
module = models.ForeignKey("db.Module", on_delete=models.CASCADE)
member = models.ForeignKey("db.User", on_delete=models.CASCADE)
@@ -70,7 +67,6 @@ class ModuleMember(ProjectBaseModel):
class ModuleIssue(ProjectBaseModel):
module = models.ForeignKey(
"db.Module", on_delete=models.CASCADE, related_name="issue_module"
)
@@ -89,10 +85,12 @@ class ModuleIssue(ProjectBaseModel):
class ModuleLink(ProjectBaseModel):
title = models.CharField(max_length=255, null=True)
url = models.URLField()
module = models.ForeignKey(Module, on_delete=models.CASCADE, related_name="link_module")
module = models.ForeignKey(
Module, on_delete=models.CASCADE, related_name="link_module"
)
metadata = models.JSONField(default=dict)
class Meta:
verbose_name = "Module Link"
@@ -101,4 +99,30 @@ class ModuleLink(ProjectBaseModel):
ordering = ("-created_at",)
def __str__(self):
return f"{self.module.name} {self.url}"
return f"{self.module.name} {self.url}"
class ModuleFavorite(ProjectBaseModel):
"""_summary_
ModuleFavorite (model): To store all the module favorite of the user
"""
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="module_favorites",
)
module = models.ForeignKey(
"db.Module", on_delete=models.CASCADE, related_name="module_favorites"
)
class Meta:
unique_together = ["module", "user"]
verbose_name = "Module Favorite"
verbose_name_plural = "Module Favorites"
db_table = "module_favorites"
ordering = ("-created_at",)
def __str__(self):
"""Return user and the module"""
return f"{self.user.email} <{self.module.name}>"
+20 -2
View File
@@ -46,7 +46,6 @@ class Project(BaseModel):
max_length=5,
verbose_name="Project Identifier",
)
slug = models.SlugField(max_length=100, blank=True)
default_assignee = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
@@ -64,6 +63,7 @@ class Project(BaseModel):
icon = models.CharField(max_length=255, null=True, blank=True)
module_view = models.BooleanField(default=True)
cycle_view = models.BooleanField(default=True)
cover_image = models.URLField(blank=True, null=True)
def __str__(self):
"""Return name of the project"""
@@ -77,7 +77,6 @@ class Project(BaseModel):
ordering = ("-created_at",)
def save(self, *args, **kwargs):
self.slug = slugify(self.name)
self.identifier = self.identifier.strip().upper()
return super().save(*args, **kwargs)
@@ -157,3 +156,22 @@ class ProjectIdentifier(AuditModel):
verbose_name_plural = "Project Identifiers"
db_table = "project_identifiers"
ordering = ("-created_at",)
class ProjectFavorite(ProjectBaseModel):
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="project_favorites",
)
class Meta:
unique_together = ["project", "user"]
verbose_name = "Project Favorite"
verbose_name_plural = "Project Favorites"
db_table = "project_favorites"
ordering = ("-created_at",)
def __str__(self):
"""Return user of the project"""
return f"{self.user.email} <{self.project.name}>"
+27 -5
View File
@@ -1,12 +1,34 @@
def group_results(results_data, group_by):
def resolve_keys(group_keys, value):
"""resolve keys to a key which will be used for
grouping
Args:
group_keys (string): key which will be used for grouping
value (obj): data value
Returns:
string: the key which will be used for
"""
Utility function to group data into a given attribute.
Function can group attributes of string and list type.
keys = group_keys.split(".")
for key in keys:
value = value.get(key, None)
return value
def group_results(results_data, group_by):
"""group results data into certain group_by
Args:
results_data (obj): complete results data
group_by (key): string
Returns:
obj: grouped results
"""
response_dict = dict()
for value in results_data:
group_attribute = value.get(group_by, None)
group_attribute = resolve_keys(group_by, value)
if isinstance(group_attribute, list):
if len(group_attribute):
for attrib in group_attribute:
@@ -28,4 +50,4 @@ def group_results(results_data, group_by):
response_dict[str(group_attribute)] = []
response_dict[str(group_attribute)].append(value)
return response_dict
return response_dict
@@ -204,7 +204,7 @@ export const CommandPalette: React.FC = () => {
<div className="fixed inset-0 bg-gray-500 bg-opacity-25 transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-20 overflow-y-auto p-4 sm:p-6 md:p-20">
<div className="fixed inset-0 z-20 p-4 sm:p-6 md:p-20">
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
@@ -214,7 +214,7 @@ export const CommandPalette: React.FC = () => {
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<Dialog.Panel className="relative mx-auto max-w-2xl transform divide-y divide-gray-500 divide-opacity-10 rounded-xl bg-white bg-opacity-80 shadow-2xl ring-1 ring-black ring-opacity-5 backdrop-blur backdrop-filter transition-all">
<Dialog.Panel className="relative mx-auto max-w-2xl transform divide-y divide-gray-500 divide-opacity-10 rounded-xl bg-white shadow-2xl ring-1 ring-black ring-opacity-5 transition-all">
<Combobox
onChange={(value: any) => {
if (value?.url) router.push(value.url);
@@ -11,6 +11,7 @@ type Props = {
states: IState[] | undefined;
members: IProjectMember[] | undefined;
addIssueToState: (groupTitle: string, stateId: string | null) => void;
makeIssueCopy: (issue: IIssue) => void;
handleEditIssue: (issue: IIssue) => void;
openIssuesListModal?: (() => void) | null;
handleDeleteIssue: (issue: IIssue) => void;
@@ -25,6 +26,7 @@ export const AllBoards: React.FC<Props> = ({
states,
members,
addIssueToState,
makeIssueCopy,
handleEditIssue,
openIssuesListModal,
handleDeleteIssue,
@@ -37,43 +39,46 @@ export const AllBoards: React.FC<Props> = ({
return (
<>
{groupedByIssues ? (
<div className="h-[calc(100vh-157px)] lg:h-[calc(100vh-115px)] w-full">
<div className="h-full w-full overflow-hidden">
<div className="h-full w-full">
<div className="flex h-full gap-x-4 overflow-x-auto overflow-y-hidden">
{Object.keys(groupedByIssues).map((singleGroup, index) => {
const stateId =
selectedGroup === "state_detail.name"
? states?.find((s) => s.name === singleGroup)?.id ?? null
: null;
<div className="h-[calc(100vh-157px)] w-full lg:h-[calc(100vh-115px)]">
<div className="horizontal-scroll-enable flex h-full gap-x-4 overflow-x-auto overflow-y-hidden">
{Object.keys(groupedByIssues).map((singleGroup, index) => {
const currentState =
selectedGroup === "state_detail.name"
? states?.find((s) => s.name === singleGroup)
: null;
const bgColor =
selectedGroup === "state_detail.name"
? states?.find((s) => s.name === singleGroup)?.color
: "#000000";
const stateId =
selectedGroup === "state_detail.name"
? states?.find((s) => s.name === singleGroup)?.id ?? null
: null;
return (
<SingleBoard
key={index}
type={type}
bgColor={bgColor}
groupTitle={singleGroup}
groupedByIssues={groupedByIssues}
selectedGroup={selectedGroup}
members={members}
handleEditIssue={handleEditIssue}
addIssueToState={() => addIssueToState(singleGroup, stateId)}
handleDeleteIssue={handleDeleteIssue}
openIssuesListModal={openIssuesListModal ?? null}
orderBy={orderBy}
handleTrashBox={handleTrashBox}
removeIssue={removeIssue}
userAuth={userAuth}
/>
);
})}
</div>
</div>
const bgColor =
selectedGroup === "state_detail.name"
? states?.find((s) => s.name === singleGroup)?.color
: "#000000";
return (
<SingleBoard
key={index}
type={type}
currentState={currentState}
bgColor={bgColor}
groupTitle={singleGroup}
groupedByIssues={groupedByIssues}
selectedGroup={selectedGroup}
members={members}
handleEditIssue={handleEditIssue}
makeIssueCopy={makeIssueCopy}
addIssueToState={() => addIssueToState(singleGroup, stateId)}
handleDeleteIssue={handleDeleteIssue}
openIssuesListModal={openIssuesListModal ?? null}
orderBy={orderBy}
handleTrashBox={handleTrashBox}
removeIssue={removeIssue}
userAuth={userAuth}
/>
);
})}
</div>
</div>
) : (
@@ -1,22 +1,17 @@
import React from "react";
// react-beautiful-dnd
import { DraggableProvided } from "react-beautiful-dnd";
// icons
import {
ArrowsPointingInIcon,
ArrowsPointingOutIcon,
EllipsisHorizontalIcon,
PlusIcon,
} from "@heroicons/react/24/outline";
import { ArrowsPointingInIcon, ArrowsPointingOutIcon, PlusIcon } from "@heroicons/react/24/outline";
// helpers
import { addSpaceIfCamelCase } from "helpers/string.helper";
// types
import { IIssue, IProjectMember, NestedKeyOf } from "types";
import { IIssue, IProjectMember, IState, NestedKeyOf } from "types";
import { getStateGroupIcon } from "components/icons";
type Props = {
groupedByIssues: {
[key: string]: IIssue[];
};
currentState?: IState | null;
selectedGroup: NestedKeyOf<IIssue> | null;
groupTitle: string;
bgColor?: string;
@@ -28,6 +23,7 @@ type Props = {
export const BoardHeader: React.FC<Props> = ({
groupedByIssues,
currentState,
selectedGroup,
groupTitle,
bgColor,
@@ -54,22 +50,19 @@ export const BoardHeader: React.FC<Props> = ({
return (
<div
className={`flex justify-between p-3 pb-0 ${
className={`flex justify-between px-1 ${
!isCollapsed ? "flex-col rounded-md border bg-gray-50" : ""
}`}
>
<div className={`flex items-center ${!isCollapsed ? "flex-col gap-2" : "gap-1"}`}>
<div
className={`flex cursor-pointer items-center gap-x-1 rounded-md bg-slate-900 px-2 ${
className={`flex cursor-pointer items-center gap-x-3.5 ${
!isCollapsed ? "mb-2 flex-col gap-y-2 py-2" : ""
}`}
style={{
border: `2px solid ${bgColor}`,
backgroundColor: `${bgColor}20`,
}}
>
{currentState && getStateGroupIcon(currentState.group, "20", "20", bgColor)}
<h2
className={`text-[0.9rem] font-medium capitalize`}
className={`text-xl font-semibold capitalize`}
style={{
writingMode: !isCollapsed ? "vertical-rl" : "horizontal-tb",
}}
@@ -80,14 +73,16 @@ export const BoardHeader: React.FC<Props> = ({
? assignees
: addSpaceIfCamelCase(groupTitle)}
</h2>
<span className="ml-0.5 text-sm text-gray-500">{groupedByIssues[groupTitle].length}</span>
<span className="ml-0.5 text-sm bg-gray-100 py-1 px-3 rounded-full">
{groupedByIssues[groupTitle].length}
</span>
</div>
</div>
<div className={`flex items-center ${!isCollapsed ? "flex-col pb-2" : ""}`}>
<button
type="button"
className="grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-gray-200"
className="grid h-7 w-7 place-items-center rounded p-1 text-gray-700 outline-none duration-300 hover:bg-gray-100"
onClick={() => {
setIsCollapsed((prevData) => !prevData);
}}
@@ -100,7 +95,7 @@ export const BoardHeader: React.FC<Props> = ({
</button>
<button
type="button"
className="grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-gray-200"
className="grid h-7 w-7 place-items-center rounded p-1 text-gray-700 outline-none duration-300 hover:bg-gray-100"
onClick={addIssueToState}
>
<PlusIcon className="h-4 w-4" />
@@ -13,11 +13,14 @@ import { BoardHeader, SingleBoardIssue } from "components/core";
import { CustomMenu } from "components/ui";
// icons
import { PlusIcon } from "@heroicons/react/24/outline";
// helpers
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
// types
import { IIssue, IProjectMember, NestedKeyOf, UserAuth } from "types";
import { IIssue, IProjectMember, IState, NestedKeyOf, UserAuth } from "types";
type Props = {
type?: "issue" | "cycle" | "module";
currentState?: IState | null;
bgColor?: string;
groupTitle: string;
groupedByIssues: {
@@ -26,6 +29,7 @@ type Props = {
selectedGroup: NestedKeyOf<IIssue> | null;
members: IProjectMember[] | undefined;
handleEditIssue: (issue: IIssue) => void;
makeIssueCopy: (issue: IIssue) => void;
addIssueToState: () => void;
handleDeleteIssue: (issue: IIssue) => void;
openIssuesListModal?: (() => void) | null;
@@ -37,12 +41,14 @@ type Props = {
export const SingleBoard: React.FC<Props> = ({
type,
currentState,
bgColor,
groupTitle,
groupedByIssues,
selectedGroup,
members,
handleEditIssue,
makeIssueCopy,
addIssueToState,
handleDeleteIssue,
openIssuesListModal,
@@ -71,10 +77,11 @@ export const SingleBoard: React.FC<Props> = ({
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
return (
<div className={`h-full flex-shrink-0 rounded ${!isCollapsed ? "" : "w-80 border bg-gray-50"}`}>
<div className={`h-full flex-shrink-0 rounded ${!isCollapsed ? "" : "w-96 bg-gray-50"}`}>
<div className={`${!isCollapsed ? "" : "flex h-full flex-col space-y-3"}`}>
<BoardHeader
addIssueToState={addIssueToState}
currentState={currentState}
bgColor={bgColor}
selectedGroup={selectedGroup}
groupTitle={groupTitle}
@@ -86,7 +93,7 @@ export const SingleBoard: React.FC<Props> = ({
<StrictModeDroppable key={groupTitle} droppableId={groupTitle}>
{(provided, snapshot) => (
<div
className={`relative mt-3 h-full px-3 pb-3 overflow-y-auto ${
className={`relative h-full overflow-y-auto p-1 ${
snapshot.isDraggingOver ? "bg-indigo-50 bg-opacity-50" : ""
} ${!isCollapsed ? "hidden" : "block"}`}
ref={provided.innerRef}
@@ -97,14 +104,14 @@ export const SingleBoard: React.FC<Props> = ({
<div
className={`absolute ${
snapshot.isDraggingOver ? "block" : "hidden"
} top-0 left-0 h-full w-full bg-indigo-200 opacity-50 pointer-events-none z-[99999998]`}
} pointer-events-none top-0 left-0 z-[99] h-full w-full bg-gray-100 opacity-50`}
/>
<div
className={`absolute ${
snapshot.isDraggingOver ? "block" : "hidden"
} top-1/2 left-1/2 -translate-y-1/2 -translate-x-1/2 text-xs whitespace-nowrap bg-white p-2 rounded pointer-events-none z-[99999999]`}
} pointer-events-none top-1/2 left-1/2 z-[99] -translate-y-1/2 -translate-x-1/2 whitespace-nowrap rounded bg-white p-2 text-xs`}
>
This board is ordered by {orderBy}
This board is ordered by {replaceUnderscoreIfSnakeCase(orderBy ?? "created_at")}
</div>
</>
)}
@@ -127,6 +134,7 @@ export const SingleBoard: React.FC<Props> = ({
selectedGroup={selectedGroup}
properties={properties}
editIssue={() => handleEditIssue(issue)}
makeIssueCopy={() => makeIssueCopy(issue)}
handleDeleteIssue={handleDeleteIssue}
orderBy={orderBy}
handleTrashBox={handleTrashBox}
@@ -148,21 +156,23 @@ export const SingleBoard: React.FC<Props> = ({
{type === "issue" ? (
<button
type="button"
className="flex items-center rounded p-2 text-xs font-medium outline-none duration-300 hover:bg-gray-100"
className="flex items-center gap-2 font-medium text-theme outline-none"
onClick={addIssueToState}
>
<PlusIcon className="mr-1 h-3 w-3" />
Create
<PlusIcon className="h-4 w-4" />
Add Issue
</button>
) : (
<CustomMenu
label={
<span className="flex items-center gap-1">
<PlusIcon className="h-3 w-3" />
Add issue
</span>
customButton={
<button
type="button"
className="flex items-center gap-2 font-medium text-theme outline-none"
>
<PlusIcon className="h-4 w-4" />
Add Issue
</button>
}
className="mt-1"
optionsPosition="left"
noBorder
>
@@ -1,4 +1,4 @@
import React, { useCallback, useEffect } from "react";
import React, { useCallback, useEffect, useState } from "react";
import Link from "next/link";
import { useRouter } from "next/router";
@@ -24,9 +24,16 @@ import {
ViewStateSelect,
} from "components/issues/view-select";
// ui
import { CustomMenu } from "components/ui";
import { ContextMenu, CustomMenu, Tooltip } from "components/ui";
// icons
import {
ClipboardDocumentCheckIcon,
LinkIcon,
PencilIcon,
TrashIcon,
} from "@heroicons/react/24/outline";
// helpers
import { copyTextToClipboard } from "helpers/string.helper";
import { copyTextToClipboard, truncateText } from "helpers/string.helper";
// types
import {
CycleIssueResponse,
@@ -47,6 +54,7 @@ type Props = {
selectedGroup: NestedKeyOf<IIssue> | null;
properties: Properties;
editIssue: () => void;
makeIssueCopy: () => void;
removeIssue?: (() => void) | null;
handleDeleteIssue: (issue: IIssue) => void;
orderBy: NestedKeyOf<IIssue> | null;
@@ -62,12 +70,17 @@ export const SingleBoardIssue: React.FC<Props> = ({
selectedGroup,
properties,
editIssue,
makeIssueCopy,
removeIssue,
handleDeleteIssue,
orderBy,
handleTrashBox,
userAuth,
}) => {
// context menu
const [contextMenu, setContextMenu] = useState(false);
const [contextMenuPosition, setContextMenuPosition] = useState({ x: 0, y: 0 });
const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
@@ -88,6 +101,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
issue_detail: {
...p.issue_detail,
...formData,
assignees: formData.assignees_list ?? p.issue_detail.assignees_list,
},
};
}
@@ -109,6 +123,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
issue_detail: {
...p.issue_detail,
...formData,
assignees: formData.assignees_list ?? p.issue_detail.assignees_list,
},
};
}
@@ -123,7 +138,8 @@ export const SingleBoardIssue: React.FC<Props> = ({
PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string),
(prevData) =>
(prevData ?? []).map((p) => {
if (p.id === issue.id) return { ...p, ...formData };
if (p.id === issue.id)
return { ...p, ...formData, assignees: formData.assignees_list ?? p.assignees_list };
return p;
}),
@@ -146,10 +162,10 @@ export const SingleBoardIssue: React.FC<Props> = ({
[workspaceSlug, projectId, cycleId, moduleId, issue]
);
function getStyle(
const getStyle = (
style: DraggingStyle | NotDraggingStyle | undefined,
snapshot: DraggableStateSnapshot
) {
) => {
if (orderBy === "sort_order") return style;
if (!snapshot.isDragging) return {};
if (!snapshot.isDropAnimating) {
@@ -160,7 +176,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
...style,
transitionDuration: `0.001s`,
};
}
};
const handleCopyText = () => {
const originURL =
@@ -183,107 +199,135 @@ export const SingleBoardIssue: React.FC<Props> = ({
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
return (
<div
className={`rounded border bg-white shadow-sm mb-3 ${
snapshot.isDragging ? "border-theme bg-indigo-50 shadow-lg" : ""
}`}
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={getStyle(provided.draggableProps.style, snapshot)}
>
<div className="group/card relative select-none p-2">
{!isNotAllowed && (
<div className="absolute top-1.5 right-1.5 z-10 opacity-0 group-hover/card:opacity-100">
{type && !isNotAllowed && (
<CustomMenu width="auto" ellipsis>
<CustomMenu.MenuItem onClick={editIssue}>Edit issue</CustomMenu.MenuItem>
{type !== "issue" && removeIssue && (
<CustomMenu.MenuItem onClick={removeIssue}>
<>Remove from {type}</>
<>
<ContextMenu
position={contextMenuPosition}
title="Quick actions"
isOpen={contextMenu}
setIsOpen={setContextMenu}
>
<ContextMenu.Item Icon={PencilIcon} onClick={editIssue}>
Edit issue
</ContextMenu.Item>
<ContextMenu.Item Icon={ClipboardDocumentCheckIcon} onClick={makeIssueCopy}>
Make a copy...
</ContextMenu.Item>
<ContextMenu.Item Icon={TrashIcon} onClick={() => handleDeleteIssue(issue)}>
Delete issue
</ContextMenu.Item>
<ContextMenu.Item Icon={LinkIcon} onClick={handleCopyText}>
Copy issue link
</ContextMenu.Item>
</ContextMenu>
<div
className={`mb-3 rounded bg-white shadow ${
snapshot.isDragging ? "border-2 border-theme shadow-lg" : ""
}`}
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={getStyle(provided.draggableProps.style, snapshot)}
onContextMenu={(e) => {
e.preventDefault();
setContextMenu(true);
setContextMenuPosition({ x: e.pageX, y: e.pageY });
}}
>
<div className="group/card relative select-none p-4">
{!isNotAllowed && (
<div className="absolute top-1.5 right-1.5 z-10 opacity-0 group-hover/card:opacity-100">
{type && !isNotAllowed && (
<CustomMenu width="auto" ellipsis>
<CustomMenu.MenuItem onClick={editIssue}>Edit issue</CustomMenu.MenuItem>
{type !== "issue" && removeIssue && (
<CustomMenu.MenuItem onClick={removeIssue}>
<>Remove from {type}</>
</CustomMenu.MenuItem>
)}
<CustomMenu.MenuItem onClick={() => handleDeleteIssue(issue)}>
Delete issue
</CustomMenu.MenuItem>
)}
<CustomMenu.MenuItem onClick={() => handleDeleteIssue(issue)}>
Delete issue
</CustomMenu.MenuItem>
<CustomMenu.MenuItem onClick={handleCopyText}>Copy issue link</CustomMenu.MenuItem>
</CustomMenu>
<CustomMenu.MenuItem onClick={handleCopyText}>
Copy issue link
</CustomMenu.MenuItem>
</CustomMenu>
)}
</div>
)}
<Link href={`/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`}>
<a>
{properties.key && (
<div className="mb-2.5 text-xs font-medium text-gray-700">
{issue.project_detail.identifier}-{issue.sequence_id}
</div>
)}
<h5
className="break-all text-sm group-hover:text-theme"
style={{ lineClamp: 3, WebkitLineClamp: 3 }}
>
{truncateText(issue.name, 100)}
</h5>
</a>
</Link>
<div className="relative mt-2.5 flex flex-wrap items-center gap-2 text-xs">
{properties.priority && (
<ViewPrioritySelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
isNotAllowed={isNotAllowed}
selfPositioned
/>
)}
</div>
)}
<Link href={`/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`}>
<a>
{properties.key && (
<div className="mb-2 text-xs font-medium text-gray-500">
{issue.project_detail.identifier}-{issue.sequence_id}
{properties.state && (
<ViewStateSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
isNotAllowed={isNotAllowed}
selfPositioned
/>
)}
{properties.due_date && (
<ViewDueDateSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
isNotAllowed={isNotAllowed}
/>
)}
{properties.sub_issue_count && (
<div className="flex flex-shrink-0 items-center gap-1 rounded-md border px-3 py-1.5 text-xs shadow-sm">
{issue.sub_issues_count} {issue.sub_issues_count === 1 ? "sub-issue" : "sub-issues"}
</div>
)}
<h5
className="mb-3 text-sm group-hover:text-theme"
style={{ lineClamp: 3, WebkitLineClamp: 3 }}
>
{issue.name}
</h5>
</a>
</Link>
<div className="relative flex flex-wrap items-center gap-x-1 gap-y-2 text-xs">
{properties.priority && selectedGroup !== "priority" && (
<ViewPrioritySelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
isNotAllowed={isNotAllowed}
selfPositioned
/>
)}
{properties.state && selectedGroup !== "state_detail.name" && (
<ViewStateSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
isNotAllowed={isNotAllowed}
selfPositioned
/>
)}
{properties.due_date && (
<ViewDueDateSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
isNotAllowed={isNotAllowed}
/>
)}
{properties.sub_issue_count && (
<div className="flex flex-shrink-0 items-center gap-1 rounded border px-2 py-1 text-xs shadow-sm duration-300 hover:bg-gray-100 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500">
{issue.sub_issues_count} {issue.sub_issues_count === 1 ? "sub-issue" : "sub-issues"}
</div>
)}
{properties.labels && (
<div className="flex flex-wrap gap-1">
{issue.label_details.map((label) => (
<span
key={label.id}
className="group flex items-center gap-1 rounded-2xl border px-2 py-0.5 text-xs"
>
{properties.labels && issue.label_details.length > 0 && (
<div className="flex flex-wrap gap-1">
{issue.label_details.map((label) => (
<span
className="h-1.5 w-1.5 flex-shrink-0 rounded-full"
style={{
backgroundColor: label?.color && label.color !== "" ? label.color : "#000",
}}
/>
{label.name}
</span>
))}
</div>
)}
{properties.assignee && (
<ViewAssigneeSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
isNotAllowed={isNotAllowed}
tooltipPosition="left"
selfPositioned
/>
)}
key={label.id}
className="group flex items-center gap-1 rounded-2xl border px-2 py-0.5 text-xs"
>
<span
className="h-1.5 w-1.5 flex-shrink-0 rounded-full"
style={{
backgroundColor: label?.color && label.color !== "" ? label.color : "#000",
}}
/>
{label.name}
</span>
))}
</div>
)}
{properties.assignee && (
<ViewAssigneeSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
isNotAllowed={isNotAllowed}
tooltipPosition="left"
selfPositioned
/>
)}
</div>
</div>
</div>
</div>
</>
);
};
@@ -129,7 +129,7 @@ export const ExistingIssuesListModal: React.FC<Props> = ({
{filteredIssues.length > 0 ? (
<li className="p-2">
{query === "" && (
<h2 className="mt-4 mb-2 px-3 text-xs font-semibold text-gray-900">
<h2 className="mb-2 px-3 text-xs font-semibold text-gray-900">
Select issues to add
</h2>
)}
@@ -175,18 +175,6 @@ export const ExistingIssuesListModal: React.FC<Props> = ({
</div>
)}
</Combobox.Options>
{query !== "" && filteredIssues.length === 0 && (
<div className="py-14 px-6 text-center sm:px-14">
<RectangleStackIcon
className="mx-auto h-6 w-6 text-gray-900 text-opacity-40"
aria-hidden="true"
/>
<p className="mt-4 text-sm text-gray-900">
We couldn{"'"}t find any issue with that term. Please try again.
</p>
</div>
)}
</Combobox>
)}
/>
@@ -0,0 +1,152 @@
import React, { useEffect, useState, useRef } from "react";
// next
import Image from "next/image";
// swr
import useSWR from "swr";
// headless ui
import { Tab, Transition, Popover } from "@headlessui/react";
// services
import fileService from "services/file.service";
// components
import { Input, Spinner } from "components/ui";
import { PrimaryButton } from "components/ui/button/primary-button";
// hooks
import useOutsideClickDetector from "hooks/use-outside-click-detector";
const tabOptions = [
{
key: "unsplash",
title: "Unsplash",
},
{
key: "upload",
title: "Upload",
},
];
type Props = {
label: string | React.ReactNode;
value: string | null;
onChange: (data: string) => void;
};
export const ImagePickerPopover: React.FC<Props> = ({ label, value, onChange }) => {
const ref = useRef<HTMLDivElement>(null);
const [isOpen, setIsOpen] = useState(false);
const [searchParams, setSearchParams] = useState("");
const [formData, setFormData] = useState({
search: "",
});
const { data: images } = useSWR(`UNSPLASH_IMAGES_${searchParams}`, () =>
fileService.getUnsplashImages(1, searchParams)
);
useOutsideClickDetector(ref, () => {
setIsOpen(false);
});
useEffect(() => {
if (!images || value !== null) return;
onChange(images[0].urls.regular);
}, [value, onChange, images]);
return (
<Popover className="relative z-[2]" ref={ref}>
<Popover.Button
className="rounded-md border border-gray-500 bg-white px-2 py-1 text-xs text-gray-700"
onClick={() => setIsOpen((prev) => !prev)}
>
{label}
</Popover.Button>
<Transition
show={isOpen}
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"
>
<Popover.Panel className="absolute right-0 z-10 mt-2 rounded-md bg-white shadow-lg">
<div className="h-96 w-80 overflow-auto rounded border bg-white p-5 shadow-2xl sm:max-w-2xl md:w-96 lg:w-[40rem]">
<Tab.Group>
<Tab.List as="span" className="inline-block rounded bg-gray-200 p-1">
{tabOptions.map((tab) => (
<Tab
key={tab.key}
className={({ selected }) =>
`rounded py-1 px-4 text-center text-sm outline-none transition-colors ${
selected ? "bg-theme text-white" : "text-black"
}`
}
>
{tab.title}
</Tab>
))}
</Tab.List>
<Tab.Panels className="h-full w-full flex-1 overflow-y-auto overflow-x-hidden">
<Tab.Panel className="h-full w-full space-y-4">
<form
onSubmit={(e) => {
e.preventDefault();
setSearchParams(formData.search);
}}
className="flex gap-x-2 pt-7"
>
<Input
name="search"
className="text-sm"
id="search"
value={formData.search}
onChange={(e) => setFormData({ ...formData, search: e.target.value })}
placeholder="Search for images"
/>
<PrimaryButton className="bg-indigo-600" size="sm">
Search
</PrimaryButton>
</form>
{images ? (
<div className="grid grid-cols-4 gap-4">
{images.map((image) => (
<div
key={image.id}
className="relative col-span-2 aspect-video md:col-span-1"
>
<Image
src={image.urls.small}
alt={image.alt_description}
layout="fill"
objectFit="cover"
className="cursor-pointer rounded"
onClick={() => {
setIsOpen(false);
onChange(image.urls.regular);
}}
/>
</div>
))}
</div>
) : (
<div className="flex justify-center pt-20">
<Spinner />
</div>
)}
</Tab.Panel>
<Tab.Panel className="flex h-full w-full flex-col items-center justify-center">
<p>Coming Soon...</p>
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
</div>
</Popover.Panel>
</Transition>
</Popover>
);
};
+1
View File
@@ -9,3 +9,4 @@ export * from "./issues-view";
export * from "./link-modal";
export * from "./not-authorized-view";
export * from "./multi-level-select";
export * from "./image-picker-popover";
+14 -3
View File
@@ -269,6 +269,15 @@ export const IssuesView: React.FC<Props> = ({
[setCreateIssueModal, setPreloadedData, selectedGroup]
);
const makeIssueCopy = useCallback(
(issue: IIssue) => {
setCreateIssueModal(true);
setPreloadedData({ ...issue, name: `${issue.name} (Copy)`, actionType: "createIssue" });
},
[setCreateIssueModal, setPreloadedData]
);
const handleEditIssue = useCallback(
(issue: IIssue) => {
setEditIssueModal(true);
@@ -370,14 +379,14 @@ export const IssuesView: React.FC<Props> = ({
{(provided, snapshot) => (
<div
className={`${
trashBox ? "opacity-100 pointer-events-auto" : "opacity-0 pointer-events-none"
} fixed z-20 top-12 left-1/2 -translate-x-1/2 flex items-center gap-2 bg-red-100 border-2 border-red-500 p-3 text-xs rounded ${
trashBox ? "pointer-events-auto opacity-100" : "pointer-events-none opacity-0"
} fixed top-9 right-9 z-20 flex h-28 w-96 items-center justify-center gap-2 rounded border-2 border-red-500 bg-red-100 p-3 text-xs font-medium italic text-red-500 ${
snapshot.isDraggingOver ? "bg-red-500 text-white" : ""
} duration-200`}
ref={provided.innerRef}
{...provided.droppableProps}
>
<TrashIcon className="h-3 w-3" />
<TrashIcon className="h-4 w-4" />
Drop issue here to delete
</div>
)}
@@ -389,6 +398,7 @@ export const IssuesView: React.FC<Props> = ({
states={states}
members={members}
addIssueToState={addIssueToState}
makeIssueCopy={makeIssueCopy}
handleEditIssue={handleEditIssue}
handleDeleteIssue={handleDeleteIssue}
openIssuesListModal={type !== "issue" ? openIssuesListModal : null}
@@ -408,6 +418,7 @@ export const IssuesView: React.FC<Props> = ({
states={states}
members={members}
addIssueToState={addIssueToState}
makeIssueCopy={makeIssueCopy}
handleEditIssue={handleEditIssue}
openIssuesListModal={type !== "issue" ? openIssuesListModal : null}
handleDeleteIssue={handleDeleteIssue}
+1 -1
View File
@@ -16,7 +16,7 @@ import type { IIssueLink, ModuleLink } from "types";
type Props = {
isOpen: boolean;
handleClose: () => void;
onFormSubmit: (formData: IIssueLink | ModuleLink) => void;
onFormSubmit: (formData: IIssueLink | ModuleLink) => Promise<void>;
};
const defaultValues: ModuleLink = {
@@ -12,6 +12,7 @@ type Props = {
states: IState[] | undefined;
members: IProjectMember[] | undefined;
addIssueToState: (groupTitle: string, stateId: string | null) => void;
makeIssueCopy: (issue: IIssue) => void;
handleEditIssue: (issue: IIssue) => void;
handleDeleteIssue: (issue: IIssue) => void;
openIssuesListModal?: (() => void) | null;
@@ -25,6 +26,7 @@ export const AllLists: React.FC<Props> = ({
states,
members,
addIssueToState,
makeIssueCopy,
openIssuesListModal,
handleEditIssue,
handleDeleteIssue,
@@ -50,6 +52,7 @@ export const AllLists: React.FC<Props> = ({
selectedGroup={selectedGroup}
members={members}
addIssueToState={() => addIssueToState(singleGroup, stateId)}
makeIssueCopy={makeIssueCopy}
handleEditIssue={handleEditIssue}
handleDeleteIssue={handleDeleteIssue}
openIssuesListModal={type !== "issue" ? openIssuesListModal : null}
@@ -1,4 +1,4 @@
import React, { useCallback } from "react";
import React, { useCallback, useState } from "react";
import Link from "next/link";
import { useRouter } from "next/router";
@@ -18,7 +18,14 @@ import {
} from "components/issues/view-select";
// ui
import { Tooltip, CustomMenu } from "components/ui";
import { Tooltip, CustomMenu, ContextMenu } from "components/ui";
// icons
import {
ClipboardDocumentCheckIcon,
LinkIcon,
PencilIcon,
TrashIcon,
} from "@heroicons/react/24/outline";
// helpers
import { copyTextToClipboard } from "helpers/string.helper";
// types
@@ -31,6 +38,7 @@ type Props = {
issue: IIssue;
properties: Properties;
editIssue: () => void;
makeIssueCopy: () => void;
removeIssue?: (() => void) | null;
handleDeleteIssue: (issue: IIssue) => void;
userAuth: UserAuth;
@@ -41,13 +49,20 @@ export const SingleListIssue: React.FC<Props> = ({
issue,
properties,
editIssue,
makeIssueCopy,
removeIssue,
handleDeleteIssue,
userAuth,
}) => {
// context menu
const [contextMenu, setContextMenu] = useState(false);
const [contextMenuPosition, setContextMenuPosition] = useState({ x: 0, y: 0 });
const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
const { setToastAlert } = useToast();
const partialUpdateIssue = useCallback(
(formData: Partial<IIssue>) => {
if (!workspaceSlug || !projectId) return;
@@ -63,6 +78,7 @@ export const SingleListIssue: React.FC<Props> = ({
issue_detail: {
...p.issue_detail,
...formData,
assignees: formData.assignees_list ?? p.issue_detail.assignees_list,
},
};
}
@@ -84,6 +100,7 @@ export const SingleListIssue: React.FC<Props> = ({
issue_detail: {
...p.issue_detail,
...formData,
assignees: formData.assignees_list ?? p.issue_detail.assignees_list,
},
};
}
@@ -98,7 +115,8 @@ export const SingleListIssue: React.FC<Props> = ({
PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string),
(prevData) =>
(prevData ?? []).map((p) => {
if (p.id === issue.id) return { ...p, ...formData };
if (p.id === issue.id)
return { ...p, ...formData, assignees: formData.assignees_list ?? p.assignees_list };
return p;
}),
@@ -134,104 +152,136 @@ export const SingleListIssue: React.FC<Props> = ({
});
});
};
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
return (
<div className="flex items-center justify-between gap-2 px-4 py-3 text-sm">
<div className="flex items-center gap-2">
<span
className="block h-1.5 w-1.5 flex-shrink-0 rounded-full"
style={{
backgroundColor: issue.state_detail.color,
}}
/>
<Link href={`/${workspaceSlug}/projects/${issue?.project_detail?.id}/issues/${issue.id}`}>
<a className="group relative flex items-center gap-2">
{properties.key && (
<Tooltip
tooltipHeading="ID"
tooltipContent={`${issue.project_detail?.identifier}-${issue.sequence_id}`}
>
<span className="flex-shrink-0 text-xs text-gray-500">
{issue.project_detail?.identifier}-{issue.sequence_id}
<>
<ContextMenu
position={contextMenuPosition}
title="Quick actions"
isOpen={contextMenu}
setIsOpen={setContextMenu}
>
<ContextMenu.Item Icon={PencilIcon} onClick={editIssue}>
Edit issue
</ContextMenu.Item>
<ContextMenu.Item Icon={ClipboardDocumentCheckIcon} onClick={makeIssueCopy}>
Make a copy...
</ContextMenu.Item>
<ContextMenu.Item Icon={TrashIcon} onClick={() => handleDeleteIssue(issue)}>
Delete issue
</ContextMenu.Item>
<ContextMenu.Item Icon={LinkIcon} onClick={handleCopyText}>
Copy issue link
</ContextMenu.Item>
</ContextMenu>
<div
className="flex items-center justify-between gap-2 px-4 py-3 text-sm"
onContextMenu={(e) => {
e.preventDefault();
setContextMenu(true);
setContextMenuPosition({ x: e.pageX, y: e.pageY });
}}
>
<div className="flex items-center gap-2">
<span
className="block h-1.5 w-1.5 flex-shrink-0 rounded-full"
style={{
backgroundColor: issue.state_detail.color,
}}
/>
<Link href={`/${workspaceSlug}/projects/${issue?.project_detail?.id}/issues/${issue.id}`}>
<a className="group relative flex items-center gap-2">
{properties.key && (
<Tooltip
tooltipHeading="ID"
tooltipContent={`${issue.project_detail?.identifier}-${issue.sequence_id}`}
>
<span className="flex-shrink-0 text-xs text-gray-500">
{issue.project_detail?.identifier}-{issue.sequence_id}
</span>
</Tooltip>
)}
<Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
<span className="w-auto max-w-lg overflow-hidden text-ellipsis whitespace-nowrap">
{issue.name}
</span>
</Tooltip>
)}
<Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
<span className="w-auto max-w-lg text-ellipsis overflow-hidden whitespace-nowrap">
{issue.name}
</span>
</Tooltip>
</a>
</Link>
</div>
<div className="flex flex-shrink-0 flex-wrap items-center gap-x-1 gap-y-2 text-xs">
{properties.priority && (
<ViewPrioritySelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
isNotAllowed={isNotAllowed}
/>
)}
{properties.state && (
<ViewStateSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
isNotAllowed={isNotAllowed}
/>
)}
{properties.due_date && (
<ViewDueDateSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
isNotAllowed={isNotAllowed}
/>
)}
{properties.sub_issue_count && (
<div className="flex flex-shrink-0 items-center gap-1 rounded border px-2 py-1 text-xs shadow-sm">
{issue.sub_issues_count} {issue.sub_issues_count === 1 ? "sub-issue" : "sub-issues"}
</div>
)}
{properties.labels && (
<div className="flex flex-wrap gap-1">
{issue.label_details.map((label) => (
<span
key={label.id}
className="group flex items-center gap-1 rounded-2xl border px-2 py-0.5 text-xs"
>
</a>
</Link>
</div>
<div className="flex flex-shrink-0 flex-wrap items-center gap-x-1 gap-y-2 text-xs">
{properties.priority && (
<ViewPrioritySelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
position="right"
isNotAllowed={isNotAllowed}
/>
)}
{properties.state && (
<ViewStateSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
position="right"
isNotAllowed={isNotAllowed}
/>
)}
{properties.due_date && (
<ViewDueDateSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
isNotAllowed={isNotAllowed}
/>
)}
{properties.sub_issue_count && (
<div className="flex flex-shrink-0 items-center gap-1 rounded-md border px-3 py-1.5 text-xs shadow-sm">
{issue.sub_issues_count} {issue.sub_issues_count === 1 ? "sub-issue" : "sub-issues"}
</div>
)}
{properties.labels && (
<div className="flex flex-wrap gap-1">
{issue.label_details.map((label) => (
<span
className="h-1.5 w-1.5 flex-shrink-0 rounded-full"
style={{
backgroundColor: label?.color && label.color !== "" ? label.color : "#000",
}}
/>
{label.name}
</span>
))}
</div>
)}
{properties.assignee && (
<ViewAssigneeSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
isNotAllowed={isNotAllowed}
/>
)}
{type && !isNotAllowed && (
<CustomMenu width="auto" ellipsis>
<CustomMenu.MenuItem onClick={editIssue}>Edit issue</CustomMenu.MenuItem>
{type !== "issue" && removeIssue && (
<CustomMenu.MenuItem onClick={removeIssue}>
<>Remove from {type}</>
key={label.id}
className="group flex items-center gap-1 rounded-2xl border px-2 py-0.5 text-xs"
>
<span
className="h-1.5 w-1.5 flex-shrink-0 rounded-full"
style={{
backgroundColor: label?.color && label.color !== "" ? label.color : "#000",
}}
/>
{label.name}
</span>
))}
</div>
)}
{properties.assignee && (
<ViewAssigneeSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
position="right"
isNotAllowed={isNotAllowed}
/>
)}
{type && !isNotAllowed && (
<CustomMenu width="auto" ellipsis>
<CustomMenu.MenuItem onClick={editIssue}>Edit issue</CustomMenu.MenuItem>
{type !== "issue" && removeIssue && (
<CustomMenu.MenuItem onClick={removeIssue}>
<>Remove from {type}</>
</CustomMenu.MenuItem>
)}
<CustomMenu.MenuItem onClick={() => handleDeleteIssue(issue)}>
Delete issue
</CustomMenu.MenuItem>
)}
<CustomMenu.MenuItem onClick={() => handleDeleteIssue(issue)}>
Delete issue
</CustomMenu.MenuItem>
<CustomMenu.MenuItem onClick={handleCopyText}>Copy issue link</CustomMenu.MenuItem>
</CustomMenu>
)}
<CustomMenu.MenuItem onClick={handleCopyText}>Copy issue link</CustomMenu.MenuItem>
</CustomMenu>
)}
</div>
</div>
</div>
</>
);
};
@@ -23,6 +23,7 @@ type Props = {
selectedGroup: NestedKeyOf<IIssue> | null;
members: IProjectMember[] | undefined;
addIssueToState: () => void;
makeIssueCopy: (issue: IIssue) => void;
handleEditIssue: (issue: IIssue) => void;
handleDeleteIssue: (issue: IIssue) => void;
openIssuesListModal?: (() => void) | null;
@@ -37,6 +38,7 @@ export const SingleList: React.FC<Props> = ({
selectedGroup,
members,
addIssueToState,
makeIssueCopy,
handleEditIssue,
handleDeleteIssue,
openIssuesListModal,
@@ -113,6 +115,7 @@ export const SingleList: React.FC<Props> = ({
issue={issue}
properties={properties}
editIssue={() => handleEditIssue(issue)}
makeIssueCopy={() => makeIssueCopy(issue)}
handleDeleteIssue={handleDeleteIssue}
removeIssue={() => {
removeIssue && removeIssue(issue.bridge);
@@ -83,10 +83,10 @@ export const MultiLevelSelect: React.FC<TMultipleSelectProps> = (props) => {
<>
{openChildFor?.id === option.id && (
<div
className={`w-72 h-auto max-h-72 bg-white border border-gray-200 absolute rounded-lg ${
className={`absolute h-auto max-h-72 w-72 rounded-lg border bg-white ${
direction === "right"
? "rounded-tl-none shadow-md left-full translate-x-2"
: "rounded-tr-none shadow-md right-full -translate-x-2"
? "left-full translate-x-2 rounded-tl-none shadow-md"
: "right-full -translate-x-2 rounded-tr-none shadow-md"
}`}
>
{option.children?.map((child) => (
@@ -118,7 +118,7 @@ export const MultiLevelSelect: React.FC<TMultipleSelectProps> = (props) => {
))}
<div
className={`w-0 h-0 absolute border-t-8 border-gray-300 ${
className={`absolute h-0 w-0 border-t-8 border-gray-300 ${
direction === "right"
? "top-0 left-0 -translate-x-2 border-r-8 border-b-8 border-b-transparent border-t-transparent border-l-transparent"
: "top-0 right-0 translate-x-2 border-l-8 border-b-8 border-b-transparent border-t-transparent border-r-transparent"
@@ -14,6 +14,7 @@ type Props = {
created_at: Date;
created_by: string;
created_by_detail: IUserLite;
metadata: any;
title: string;
url: string;
}[];
@@ -56,8 +57,8 @@ export const LinksList: React.FC<Props> = ({ links, handleDeleteLink, userAuth }
<h5 className="w-4/5">{link.title}</h5>
<p className="mt-0.5 text-gray-500">
Added {timeAgo(link.created_at)}
{/* <br />
by {link.created_by_detail.email} */}
<br />
by {link.created_by_detail.email}
</p>
</div>
</a>
@@ -1,17 +1,10 @@
import React from "react";
import {
XAxis,
YAxis,
Tooltip,
ResponsiveContainer,
AreaChart,
Area,
ReferenceLine,
} from "recharts";
import { XAxis, YAxis, Tooltip, AreaChart, Area, ReferenceLine, TooltipProps} from "recharts";
//types
import { IIssue } from "types";
import { NameType, ValueType } from "recharts/types/component/DefaultTooltipContent";
// helper
import { getDatesInRange, renderShortNumericDateFormat } from "helpers/date-time.helper";
@@ -43,53 +36,69 @@ const ProgressChart: React.FC<Props> = ({ issues, start, end }) => {
});
return dateWiseData;
};
const CustomTooltip = ({ active, payload }: TooltipProps<ValueType, NameType>) => {
if (active && payload && payload.length) {
console.log(payload[0].payload.currentDate);
return (
<div className="rounded-sm bg-gray-300 p-1 text-xs text-gray-800">
<p>{payload[0].payload.currentDate}</p>
</div>
);
}
return null;
};
const ChartData = getChartData();
return (
<div className="relative h-[200px] w-full ">
<div className="flex justify-start items-start gap-4 text-xs">
<div className="flex justify-center items-center gap-1">
<span className="h-2 w-2 bg-green-600 rounded-full" />
<span>Ideal</span>
</div>
<div className="flex justify-center items-center gap-1">
<span className="h-2 w-2 bg-[#8884d8] rounded-full" />
<span>Current</span>
</div>
</div>
<div className="flex items-center justify-center h-full w-full absolute -left-8 py-3 text-xs">
<ResponsiveContainer width="100%" height="100%">
<AreaChart
width={300}
height={200}
data={ChartData}
margin={{
top: 0,
right: 0,
left: 0,
bottom: 0,
}}
>
<XAxis dataKey="currentDate" />
<YAxis />
<Tooltip />
<Area
type="monotone"
dataKey="pending"
stroke="#8884d8"
fill="#98d1fb"
activeDot={{ r: 8 }}
/>
<ReferenceLine
stroke="#16a34a"
strokeDasharray="3 3"
segment={[
{ x: `${renderShortNumericDateFormat(endDate)}`, y: 0 },
{ x: `${renderShortNumericDateFormat(startDate)}`, y: issues.length },
]}
/>
</AreaChart>
</ResponsiveContainer>
</div>
<div className="absolute -left-4 flex h-full w-full items-center justify-center text-xs">
<AreaChart
width={360}
height={160}
data={ChartData}
margin={{
top: 12,
right: 12,
left: 0,
bottom: 12,
}}
>
<defs>
<linearGradient id="linearblue" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#3F76FF" stopOpacity={0.7} />
<stop offset="50%" stopColor="#3F76FF" stopOpacity={0.1} />
<stop offset="100%" stopColor="#3F76FF" stopOpacity={0} />
</linearGradient>
</defs>
<XAxis
dataKey="currentDate"
stroke="#9ca3af"
tick={{ fontSize: "12px", fill: "#1f2937" }}
tickSize={10}
minTickGap={10}
/>
<YAxis
stroke="#9ca3af"
tick={{ fontSize: "12px", fill: "#1f2937" }}
tickSize={10}
minTickGap={10}
/>
<Tooltip content={<CustomTooltip />} />
<Area
type="monotone"
dataKey="pending"
stroke="#8884d8"
fill="url(#linearblue)"
activeDot={{ r: 8 }}
/>
<ReferenceLine
stroke="#16a34a"
strokeDasharray="3 3"
segment={[
{ x: `${renderShortNumericDateFormat(endDate)}`, y: 0 },
{ x: `${renderShortNumericDateFormat(startDate)}`, y: issues.length },
]}
/>
</AreaChart>
</div>
);
};
@@ -13,19 +13,24 @@ import projectService from "services/project.service";
// hooks
import useLocalStorage from "hooks/use-local-storage";
// components
import { SingleProgressStats } from "components/core";
import { LinksList, SingleProgressStats } from "components/core";
// ui
import { Avatar } from "components/ui";
// icons
import User from "public/user.png";
import { PlusIcon } from "@heroicons/react/24/outline";
// types
import { IIssue, IIssueLabels } from "types";
import { IIssue, IIssueLabels, IModule, UserAuth } from "types";
// fetch-keys
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS } from "constants/fetch-keys";
// types
type Props = {
groupedIssues: any;
issues: IIssue[];
module?: IModule;
setModuleLinkModal?: any;
handleDeleteLink?: any;
userAuth?: UserAuth;
};
const stateGroupColours: {
@@ -38,7 +43,14 @@ const stateGroupColours: {
completed: "#096e8d",
};
export const SidebarProgressStats: React.FC<Props> = ({ groupedIssues, issues }) => {
export const SidebarProgressStats: React.FC<Props> = ({
groupedIssues,
issues,
module,
setModuleLinkModal,
handleDeleteLink,
userAuth,
}) => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
@@ -60,14 +72,17 @@ export const SidebarProgressStats: React.FC<Props> = ({ groupedIssues, issues })
const currentValue = (tab: string | null) => {
switch (tab) {
case "Links":
return 0;
case "Assignees":
return 0;
case "Labels":
return 1;
case "States":
case "Labels":
return 2;
case "States":
return 3;
default:
return 0;
return 3;
}
};
return (
@@ -76,45 +91,91 @@ export const SidebarProgressStats: React.FC<Props> = ({ groupedIssues, issues })
onChange={(i) => {
switch (i) {
case 0:
return setTab("Assignees");
return setTab("Links");
case 1:
return setTab("Labels");
return setTab("Assignees");
case 2:
return setTab("Labels");
case 3:
return setTab("States");
default:
return setTab("Assignees");
return setTab("States");
}
}}
>
<Tab.List
as="div"
className="flex items-center justify-between w-full rounded bg-gray-100 text-xs"
className={`flex w-full items-center justify-between rounded-md bg-gray-100 px-1 py-1.5
${module ? "text-xs" : "text-sm"} `}
>
{module ? (
<Tab
className={({ selected }) =>
`w-full rounded px-3 py-1 text-gray-900 ${
selected ? " bg-theme text-white" : " hover:bg-hover-gray"
}`
}
>
Links
</Tab>
) : (
""
)}
<Tab
className={({ selected }) =>
`w-1/2 rounded py-1 ${selected ? "bg-gray-300" : "hover:bg-gray-200"}`
`w-full rounded px-3 py-1 text-gray-900 ${
selected ? " bg-theme text-white" : " hover:bg-hover-gray"
}`
}
>
Assignees
</Tab>
<Tab
className={({ selected }) =>
`w-1/2 rounded py-1 ${selected ? "bg-gray-300 font-semibold" : "hover:bg-gray-200 "}`
`w-full rounded px-3 py-1 text-gray-900 ${
selected ? " bg-theme text-white" : " hover:bg-hover-gray"
}`
}
>
Labels
</Tab>
<Tab
className={({ selected }) =>
`w-1/2 rounded py-1 ${selected ? "bg-gray-300 font-semibold" : "hover:bg-gray-200 "}`
`w-full rounded px-3 py-1 text-gray-900 ${
selected ? " bg-theme text-white" : " hover:bg-hover-gray"
}`
}
>
States
</Tab>
</Tab.List>
<Tab.Panels className="flex items-center justify-between w-full">
<Tab.Panel as="div" className="w-full flex flex-col ">
<Tab.Panels className="flex w-full items-center justify-between p-1">
{module ? (
<Tab.Panel as="div" className="flex w-full flex-col text-xs ">
<button
type="button"
className="flex w-full items-center justify-start gap-2 rounded px-4 py-2 hover:bg-theme/5"
onClick={() => setModuleLinkModal(true)}
>
<PlusIcon className="h-4 w-4" /> <span>Add Link</span>
</button>
<div className="mt-2 space-y-2 hover:bg-theme/5">
{userAuth && module.link_module && module.link_module.length > 0 ? (
<LinksList
links={module.link_module}
handleDeleteLink={handleDeleteLink}
userAuth={userAuth}
/>
) : null}
</div>
</Tab.Panel>
) : (
""
)}
<Tab.Panel as="div" className="flex w-full flex-col text-xs ">
{members?.map((member, index) => {
const totalArray = issues?.filter((i) => i.assignees?.includes(member.member.id));
const completeArray = totalArray?.filter((i) => i.state_detail.group === "completed");
@@ -161,7 +222,7 @@ export const SidebarProgressStats: React.FC<Props> = ({ groupedIssues, issues })
""
)}
</Tab.Panel>
<Tab.Panel as="div" className="w-full flex flex-col ">
<Tab.Panel as="div" className="flex w-full flex-col ">
{issueLabels?.map((issue, index) => {
const totalArray = issues?.filter((i) => i.labels?.includes(issue.id));
const completeArray = totalArray?.filter((i) => i.state_detail.group === "completed");
@@ -170,15 +231,15 @@ export const SidebarProgressStats: React.FC<Props> = ({ groupedIssues, issues })
<SingleProgressStats
key={index}
title={
<>
<div className="flex items-center gap-2">
<span
className="block h-2 w-2 rounded-full "
className="block h-3 w-3 rounded-full "
style={{
backgroundColor: issue.color,
}}
/>
<span className="text-xs capitalize">{issue.name}</span>
</>
</div>
}
completed={completeArray.length}
total={totalArray.length}
@@ -187,20 +248,20 @@ export const SidebarProgressStats: React.FC<Props> = ({ groupedIssues, issues })
}
})}
</Tab.Panel>
<Tab.Panel as="div" className="w-full flex flex-col ">
<Tab.Panel as="div" className="flex w-full flex-col ">
{Object.keys(groupedIssues).map((group, index) => (
<SingleProgressStats
key={index}
title={
<>
<div className="flex items-center gap-2">
<span
className="block h-2 w-2 rounded-full "
className="block h-3 w-3 rounded-full "
style={{
backgroundColor: stateGroupColours[group],
}}
/>
<span className="text-xs capitalize">{group}</span>
</>
</div>
}
completed={groupedIssues[group].length}
total={issues.length}
@@ -13,10 +13,10 @@ export const SingleProgressStats: React.FC<TSingleProgressStatsProps> = ({
completed,
total,
}) => (
<div className="flex items-center justify-between w-full py-3 text-xs border-b-[1px] border-gray-200">
<div className="flex items-center justify-start w-1/2 gap-2">{title}</div>
<div className="flex items-center justify-end w-1/2 gap-1 px-2">
<div className="flex h-5 justify-center items-center gap-1 ">
<div className="flex w-full items-center justify-between py-3 text-xs">
<div className="flex w-1/2 items-center justify-start gap-2">{title}</div>
<div className="flex w-1/2 items-center justify-end gap-1 px-2">
<div className="flex h-5 items-center justify-center gap-1 ">
<span className="h-4 w-4 ">
<ProgressBar value={completed} maxValue={total} />
</span>
@@ -0,0 +1,92 @@
import { useState } from "react";
import { useRouter } from "next/router";
import useSWR from "swr";
// services
import cyclesService from "services/cycles.service";
// components
import { DeleteCycleModal, SingleCycleCard } from "components/cycles";
// icons
import { CompletedCycleIcon } from "components/icons";
// types
import { ICycle, SelectCycleType } from "types";
// fetch-keys
import { CYCLE_COMPLETE_LIST } from "constants/fetch-keys";
import { Loader } from "components/ui";
export interface CompletedCyclesListProps {
setCreateUpdateCycleModal: React.Dispatch<React.SetStateAction<boolean>>;
setSelectedCycle: React.Dispatch<React.SetStateAction<SelectCycleType>>;
}
export const CompletedCyclesList: React.FC<CompletedCyclesListProps> = ({
setCreateUpdateCycleModal,
setSelectedCycle,
}) => {
const [cycleDeleteModal, setCycleDeleteModal] = useState(false);
const [selectedCycleForDelete, setSelectedCycleForDelete] = useState<SelectCycleType>();
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { data: completedCycles } = useSWR(
workspaceSlug && projectId ? CYCLE_COMPLETE_LIST(projectId as string) : null,
workspaceSlug && projectId
? () => cyclesService.getCompletedCycles(workspaceSlug as string, projectId as string)
: null
);
const handleDeleteCycle = (cycle: ICycle) => {
setSelectedCycleForDelete({ ...cycle, actionType: "delete" });
setCycleDeleteModal(true);
};
const handleEditCycle = (cycle: ICycle) => {
setSelectedCycle({ ...cycle, actionType: "edit" });
setCreateUpdateCycleModal(true);
};
return (
<>
<DeleteCycleModal
isOpen={
cycleDeleteModal &&
!!selectedCycleForDelete &&
selectedCycleForDelete.actionType === "delete"
}
setIsOpen={setCycleDeleteModal}
data={selectedCycleForDelete}
/>
{completedCycles ? (
completedCycles.completed_cycles.length > 0 ? (
<div className="grid grid-cols-1 gap-9 md:grid-cols-2 lg:grid-cols-3">
{completedCycles.completed_cycles.map((cycle) => (
<SingleCycleCard
key={cycle.id}
cycle={cycle}
handleDeleteCycle={() => handleDeleteCycle(cycle)}
handleEditCycle={() => handleEditCycle(cycle)}
/>
))}
</div>
) : (
<div className="flex flex-col items-center justify-center gap-4 text-center">
<CompletedCycleIcon height="56" width="56" />
<h3 className="text-gray-500">
No completed cycles yet. Create with{" "}
<pre className="inline rounded bg-gray-200 px-2 py-1">Q</pre>.
</h3>
</div>
)
) : (
<Loader className="grid grid-cols-1 gap-9 md:grid-cols-2 lg:grid-cols-3">
<Loader.Item height="200px" />
<Loader.Item height="200px" />
<Loader.Item height="200px" />
</Loader>
)}
</>
);
};
@@ -1,72 +0,0 @@
// react
import { useState } from "react";
// components
import { DeleteCycleModal, SingleCycleCard } from "components/cycles";
// types
import { ICycle, SelectCycleType } from "types";
import { CompletedCycleIcon, CurrentCycleIcon, UpcomingCycleIcon } from "components/icons";
type TCycleStatsViewProps = {
cycles: ICycle[];
setCreateUpdateCycleModal: React.Dispatch<React.SetStateAction<boolean>>;
setSelectedCycle: React.Dispatch<React.SetStateAction<SelectCycleType>>;
type: "current" | "upcoming" | "completed";
};
export const CyclesListView: React.FC<TCycleStatsViewProps> = ({
cycles,
setCreateUpdateCycleModal,
setSelectedCycle,
type,
}) => {
const [cycleDeleteModal, setCycleDeleteModal] = useState(false);
const [selectedCycleForDelete, setSelectedCycleForDelete] = useState<SelectCycleType>();
const handleDeleteCycle = (cycle: ICycle) => {
setSelectedCycleForDelete({ ...cycle, actionType: "delete" });
setCycleDeleteModal(true);
};
const handleEditCycle = (cycle: ICycle) => {
setSelectedCycle({ ...cycle, actionType: "edit" });
setCreateUpdateCycleModal(true);
};
return (
<>
<DeleteCycleModal
isOpen={
cycleDeleteModal &&
!!selectedCycleForDelete &&
selectedCycleForDelete.actionType === "delete"
}
setIsOpen={setCycleDeleteModal}
data={selectedCycleForDelete}
/>
{cycles.length > 0 ? (
cycles.map((cycle) => (
<SingleCycleCard
key={cycle.id}
cycle={cycle}
handleDeleteCycle={() => handleDeleteCycle(cycle)}
handleEditCycle={() => handleEditCycle(cycle)}
/>
))
) : (
<div className="flex flex-col items-center justify-center gap-4 text-center">
{type === "upcoming" ? (
<UpcomingCycleIcon height="56" width="56" />
) : type === "completed" ? (
<CompletedCycleIcon height="56" width="56" />
) : (
<CurrentCycleIcon height="56" width="56" />
)}
<h3 className="text-gray-500">
No {type} {type === "current" ? "cycle" : "cycles"} yet. Create with{" "}
<pre className="inline rounded bg-gray-200 px-2 py-1">Q</pre>.
</h3>
</div>
)}
</>
);
};
@@ -0,0 +1,82 @@
import { useState } from "react";
// components
import { DeleteCycleModal, SingleCycleCard } from "components/cycles";
// icons
import { CompletedCycleIcon, CurrentCycleIcon, UpcomingCycleIcon } from "components/icons";
// types
import { ICycle, SelectCycleType } from "types";
import { Loader } from "components/ui";
type TCycleStatsViewProps = {
cycles: ICycle[] | undefined;
setCreateUpdateCycleModal: React.Dispatch<React.SetStateAction<boolean>>;
setSelectedCycle: React.Dispatch<React.SetStateAction<SelectCycleType>>;
type: "current" | "upcoming" | "draft";
};
export const CyclesList: React.FC<TCycleStatsViewProps> = ({
cycles,
setCreateUpdateCycleModal,
setSelectedCycle,
type,
}) => {
const [cycleDeleteModal, setCycleDeleteModal] = useState(false);
const [selectedCycleForDelete, setSelectedCycleForDelete] = useState<SelectCycleType>();
const handleDeleteCycle = (cycle: ICycle) => {
setSelectedCycleForDelete({ ...cycle, actionType: "delete" });
setCycleDeleteModal(true);
};
const handleEditCycle = (cycle: ICycle) => {
setSelectedCycle({ ...cycle, actionType: "edit" });
setCreateUpdateCycleModal(true);
};
return (
<>
<DeleteCycleModal
isOpen={
cycleDeleteModal &&
!!selectedCycleForDelete &&
selectedCycleForDelete.actionType === "delete"
}
setIsOpen={setCycleDeleteModal}
data={selectedCycleForDelete}
/>
{cycles ? (
cycles.length > 0 ? (
<div className="grid grid-cols-1 gap-9 md:grid-cols-2 lg:grid-cols-3">
{cycles.map((cycle) => (
<SingleCycleCard
key={cycle.id}
cycle={cycle}
handleDeleteCycle={() => handleDeleteCycle(cycle)}
handleEditCycle={() => handleEditCycle(cycle)}
/>
))}
</div>
) : (
<div className="flex flex-col items-center justify-center gap-4 text-center">
{type === "upcoming" ? (
<UpcomingCycleIcon height="56" width="56" />
) : type === "draft" ? (
<CompletedCycleIcon height="56" width="56" />
) : (
<CurrentCycleIcon height="56" width="56" />
)}
<h3 className="text-gray-500">
No {type} {type === "current" ? "cycle" : "cycles"} yet. Create with{" "}
<pre className="inline rounded bg-gray-200 px-2 py-1">Q</pre>.
</h3>
</div>
)
) : (
<Loader className="grid grid-cols-1 gap-9 md:grid-cols-2 lg:grid-cols-3">
<Loader.Item height="300px" />
</Loader>
)}
</>
);
};
+78 -33
View File
@@ -1,11 +1,18 @@
import { useEffect } from "react";
import { useEffect, useState } from "react";
import { useRouter } from "next/router";
// toast
import useToast from "hooks/use-toast";
// react-hook-form
import { Controller, useForm } from "react-hook-form";
// ui
import { Button, CustomDatePicker, CustomSelect, Input, TextArea } from "components/ui";
// types
import { ICycle } from "types";
// services
import cyclesService from "services/cycles.service";
// helper
import { getDateRangeStatus } from "helpers/date-time.helper";
type Props = {
handleFormSubmit: (values: Partial<ICycle>) => Promise<void>;
@@ -17,17 +24,24 @@ type Props = {
const defaultValues: Partial<ICycle> = {
name: "",
description: "",
status: "draft",
start_date: "",
end_date: "",
start_date: null,
end_date: null,
};
export const CycleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, status, data }) => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { setToastAlert } = useToast();
const [isDateValid, setIsDateValid] = useState(true);
const {
register,
formState: { errors, isSubmitting },
handleSubmit,
control,
watch,
reset,
} = useForm<ICycle>({
defaultValues,
@@ -41,6 +55,35 @@ export const CycleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, stat
});
};
const cycleStatus =
data?.start_date && data?.end_date
? getDateRangeStatus(data?.start_date, data?.end_date) : "";
const dateChecker = async (payload: any) => {
await cyclesService
.cycleDateCheck(workspaceSlug as string, projectId as string, payload)
.then((res) => {
if (res.status) {
setIsDateValid(true);
} else {
setIsDateValid(false);
setToastAlert({
type: "error",
title: "Error!",
message:
"You have a cycle already on the given dates, if you want to create your draft cycle you can do that by removing dates",
});
}
})
.catch((err) => {
console.log(err);
});
};
const checkEmptyDate =
(watch("start_date") === "" && watch("end_date") === "") ||
(!watch("start_date") && !watch("end_date"));
useEffect(() => {
reset({
...defaultValues,
@@ -84,30 +127,7 @@ export const CycleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, stat
register={register}
/>
</div>
<div>
<h6 className="text-gray-500">Status</h6>
<Controller
name="status"
control={control}
render={({ field }) => (
<CustomSelect
{...field}
label={<span className="capitalize">{field.value ?? "Select Status"}</span>}
input
>
{[
{ label: "Draft", value: "draft" },
{ label: "Started", value: "started" },
{ label: "Completed", value: "completed" },
].map((item) => (
<CustomSelect.Option key={item.value} value={item.value}>
{item.label}
</CustomSelect.Option>
))}
</CustomSelect>
)}
/>
</div>
<div className="flex gap-x-2">
<div className="w-full">
<h6 className="text-gray-500">Start Date</h6>
@@ -115,12 +135,19 @@ export const CycleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, stat
<Controller
control={control}
name="start_date"
rules={{ required: "Start date is required" }}
render={({ field: { value, onChange } }) => (
<CustomDatePicker
renderAs="input"
value={value}
onChange={onChange}
onChange={(val) => {
onChange(val);
watch("end_date") && cycleStatus != "current"
? dateChecker({
start_date: val,
end_date: watch("end_date"),
})
: "";
}}
error={errors.start_date ? true : false}
/>
)}
@@ -136,12 +163,19 @@ export const CycleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, stat
<Controller
control={control}
name="end_date"
rules={{ required: "End date is required" }}
render={({ field: { value, onChange } }) => (
<CustomDatePicker
renderAs="input"
value={value}
onChange={onChange}
onChange={(val) => {
onChange(val);
watch("start_date") && cycleStatus != "current"
? dateChecker({
start_date: watch("start_date"),
end_date: val,
})
: "";
}}
error={errors.end_date ? true : false}
/>
)}
@@ -158,7 +192,18 @@ export const CycleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, stat
<Button theme="secondary" onClick={handleClose}>
Cancel
</Button>
<Button type="submit" disabled={isSubmitting}>
<Button
type="submit"
className={
checkEmptyDate
? "cursor-pointer"
: isDateValid
? "cursor-pointer"
: "cursor-not-allowed"
}
disabled={isSubmitting || checkEmptyDate ? false : isDateValid ? false : true}
>
{status
? isSubmitting
? "Updating Cycle..."
+2 -1
View File
@@ -1,4 +1,5 @@
export * from "./cycles-list-view";
export * from "./completed-cycles-list";
export * from "./cycles-list";
export * from "./delete-cycle-modal";
export * from "./form";
export * from "./modal";
+34 -4
View File
@@ -12,10 +12,16 @@ import cycleService from "services/cycles.service";
import useToast from "hooks/use-toast";
// components
import { CycleForm } from "components/cycles";
// helper
import { getDateRangeStatus } from "helpers/date-time.helper";
// types
import type { ICycle } from "types";
// fetch keys
import { CYCLE_LIST } from "constants/fetch-keys";
import {
CYCLE_COMPLETE_LIST,
CYCLE_CURRENT_AND_UPCOMING_LIST,
CYCLE_DRAFT_LIST,
} from "constants/fetch-keys";
type CycleModalProps = {
isOpen: boolean;
@@ -37,7 +43,19 @@ export const CreateUpdateCycleModal: React.FC<CycleModalProps> = ({
await cycleService
.createCycle(workspaceSlug as string, projectId as string, payload)
.then((res) => {
mutate(CYCLE_LIST(projectId as string));
switch (getDateRangeStatus(res.start_date, res.end_date)) {
case "completed":
mutate(CYCLE_COMPLETE_LIST(projectId as string));
break;
case "current":
mutate(CYCLE_CURRENT_AND_UPCOMING_LIST(projectId as string));
break;
case "upcoming":
mutate(CYCLE_CURRENT_AND_UPCOMING_LIST(projectId as string));
break;
default:
mutate(CYCLE_DRAFT_LIST(projectId as string));
}
handleClose();
setToastAlert({
@@ -59,7 +77,19 @@ export const CreateUpdateCycleModal: React.FC<CycleModalProps> = ({
await cycleService
.updateCycle(workspaceSlug as string, projectId as string, cycleId, payload)
.then((res) => {
mutate(CYCLE_LIST(projectId as string));
switch (getDateRangeStatus(res.start_date, res.end_date)) {
case "completed":
mutate(CYCLE_COMPLETE_LIST(projectId as string));
break;
case "current":
mutate(CYCLE_CURRENT_AND_UPCOMING_LIST(projectId as string));
break;
case "upcoming":
mutate(CYCLE_CURRENT_AND_UPCOMING_LIST(projectId as string));
break;
default:
mutate(CYCLE_DRAFT_LIST(projectId as string));
}
handleClose();
setToastAlert({
@@ -113,7 +143,7 @@ export const CreateUpdateCycleModal: React.FC<CycleModalProps> = ({
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-white px-5 py-8 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6">
<Dialog.Panel className="relative transform rounded-lg bg-white px-5 py-8 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6">
<CycleForm
handleFormSubmit={handleFormSubmit}
handleClose={handleClose}
+303 -224
View File
@@ -6,20 +6,22 @@ import Image from "next/image";
import { mutate } from "swr";
// react-hook-form
import { Controller, useForm } from "react-hook-form";
import { Popover, Transition } from "@headlessui/react";
import { useForm } from "react-hook-form";
import { Disclosure, Popover, Transition } from "@headlessui/react";
import DatePicker from "react-datepicker";
// icons
import {
CalendarDaysIcon,
ChartPieIcon,
LinkIcon,
Squares2X2Icon,
ArrowLongRightIcon,
TrashIcon,
UserIcon,
DocumentDuplicateIcon,
UserCircleIcon,
ChevronDownIcon,
DocumentIcon,
} from "@heroicons/react/24/outline";
// ui
import { CustomSelect, Loader, ProgressBar } from "components/ui";
import { CustomMenu, Loader, ProgressBar } from "components/ui";
// hooks
import useToast from "hooks/use-toast";
// services
@@ -29,38 +31,41 @@ import { SidebarProgressStats } from "components/core";
import ProgressChart from "components/core/sidebar/progress-chart";
import { DeleteCycleModal } from "components/cycles";
// helpers
import { copyTextToClipboard } from "helpers/string.helper";
import { capitalizeFirstLetter, copyTextToClipboard } from "helpers/string.helper";
import { groupBy } from "helpers/array.helper";
import { renderDateFormat, renderShortNumericDateFormat } from "helpers/date-time.helper";
import { renderDateFormat, renderShortDate } from "helpers/date-time.helper";
// types
import { CycleIssueResponse, ICycle, IIssue } from "types";
// fetch-keys
import { CYCLE_DETAILS } from "constants/fetch-keys";
// constants
import { CYCLE_STATUS } from "constants/cycle";
type Props = {
issues: IIssue[];
cycle: ICycle | undefined;
isOpen: boolean;
cycleIssues: CycleIssueResponse[];
cycleStatus: string;
};
export const CycleDetailsSidebar: React.FC<Props> = ({ issues, cycle, isOpen, cycleIssues }) => {
export const CycleDetailsSidebar: React.FC<Props> = ({
issues,
cycle,
isOpen,
cycleIssues,
cycleStatus,
}) => {
const [cycleDeleteModal, setCycleDeleteModal] = useState(false);
const [startDateRange, setStartDateRange] = useState<Date | null>(new Date());
const [endDateRange, setEndDateRange] = useState<Date | null>(null);
const router = useRouter();
const { workspaceSlug, projectId, cycleId } = router.query;
const [startDateRange, setStartDateRange] = useState<Date | null>(new Date());
const [endDateRange, setEndDateRange] = useState<Date | null>(null);
const { setToastAlert } = useToast();
const defaultValues: Partial<ICycle> = {
start_date: new Date().toString(),
end_date: new Date().toString(),
status: cycle?.status,
};
const groupedIssues = {
@@ -72,7 +77,7 @@ export const CycleDetailsSidebar: React.FC<Props> = ({ issues, cycle, isOpen, cy
...groupBy(cycleIssues ?? [], "issue_detail.state_detail.group"),
};
const { reset, watch, control } = useForm({
const { reset } = useForm({
defaultValues,
});
@@ -96,6 +101,25 @@ export const CycleDetailsSidebar: React.FC<Props> = ({ issues, cycle, isOpen, cy
});
};
const handleCopyText = () => {
const originURL =
typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/cycles/${cycle?.id}`)
.then(() => {
setToastAlert({
type: "success",
title: "Cycle link copied to clipboard",
});
})
.catch(() => {
setToastAlert({
type: "error",
title: "Some error occurred",
});
});
};
useEffect(() => {
if (cycle)
reset({
@@ -106,236 +130,291 @@ export const CycleDetailsSidebar: React.FC<Props> = ({ issues, cycle, isOpen, cy
const isStartValid = new Date(`${cycle?.start_date}`) <= new Date();
const isEndValid = new Date(`${cycle?.end_date}`) >= new Date(`${cycle?.start_date}`);
const progressPercentage = cycleIssues
? Math.round((groupedIssues.completed.length / cycleIssues?.length) * 100)
: null;
return (
<>
<DeleteCycleModal isOpen={cycleDeleteModal} setIsOpen={setCycleDeleteModal} data={cycle} />
<div
className={`fixed top-0 ${
isOpen ? "right-0" : "-right-[24rem]"
} z-20 h-full w-[24rem] overflow-y-auto border-l bg-gray-50 p-5 duration-300`}
} z-20 h-full w-[24rem] overflow-y-auto border-l bg-gray-50 py-5 duration-300`}
>
{cycle ? (
<>
<div className="flex gap-1 text-sm my-2">
<div className="flex items-center ">
<Controller
control={control}
name="status"
render={({ field: { value } }) => (
<CustomSelect
label={
<span
className={`flex items-center gap-1 text-left capitalize p-1 text-xs h-full w-full text-gray-900`}
>
<Squares2X2Icon className="h-4 w-4 flex-shrink-0" />
{watch("status")}
</span>
}
value={value}
onChange={(value: any) => {
submitChanges({ status: value });
}}
>
{CYCLE_STATUS.map((option) => (
<CustomSelect.Option key={option.value} value={option.value}>
<span className="text-xs">{option.label}</span>
</CustomSelect.Option>
))}
</CustomSelect>
)}
/>
</div>
<div className="flex justify-center items-center gap-2 rounded-md border bg-transparent h-full p-2 px-4 text-xs font-medium text-gray-900 hover:bg-gray-100 hover:text-gray-900 focus:outline-none">
<Popover className="flex justify-center items-center relative rounded-lg">
{({ open }) => (
<>
<Popover.Button
className={`group flex items-center ${open ? "bg-gray-100" : ""}`}
>
<CalendarDaysIcon className="h-4 w-4 flex-shrink-0 mr-2" />
<span>
{renderShortNumericDateFormat(`${cycle.start_date}`)
? renderShortNumericDateFormat(`${cycle.start_date}`)
: "N/A"}
</span>
</Popover.Button>
<Transition
as={React.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 top-10 -left-10 z-20 transform overflow-hidden">
<DatePicker
selected={startDateRange}
onChange={(date) => {
submitChanges({
start_date: renderDateFormat(date),
});
setStartDateRange(date);
}}
selectsStart
startDate={startDateRange}
endDate={endDateRange}
inline
/>
</Popover.Panel>
</Transition>
</>
)}
</Popover>
<Popover className="flex justify-center items-center relative rounded-lg">
{({ open }) => (
<>
<Popover.Button
className={`group flex items-center ${open ? "bg-gray-100" : ""}`}
>
<span>
-{" "}
{renderShortNumericDateFormat(`${cycle.end_date}`)
? renderShortNumericDateFormat(`${cycle.end_date}`)
: "N/A"}
</span>
</Popover.Button>
<Transition
as={React.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 top-10 -right-20 z-20 transform overflow-hidden">
<DatePicker
selected={endDateRange}
onChange={(date) => {
submitChanges({
end_date: renderDateFormat(date),
});
setEndDateRange(date);
}}
selectsEnd
startDate={startDateRange}
endDate={endDateRange}
minDate={startDateRange}
inline
/>
</Popover.Panel>
</Transition>
</>
)}
</Popover>
</div>
</div>
<div className="flex items-center justify-between pb-3">
<h4 className="text-sm font-medium">{cycle.name}</h4>
<div className="flex flex-wrap items-center gap-2">
<button
type="button"
className="rounded-md border p-2 shadow-sm duration-300 hover:bg-gray-100 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500"
onClick={() =>
copyTextToClipboard(
`https://app.plane.so/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`
)
.then(() => {
setToastAlert({
type: "success",
title: "Cycle link copied to clipboard",
});
})
.catch(() => {
setToastAlert({
type: "error",
title: "Some error occurred",
});
})
}
>
<LinkIcon className="h-3.5 w-3.5" />
</button>
<button
type="button"
className="rounded-md border border-red-500 p-2 text-red-500 shadow-sm duration-300 hover:bg-red-50 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500"
onClick={() => setCycleDeleteModal(true)}
>
<TrashIcon className="h-3.5 w-3.5" />
</button>
</div>
</div>
<div className="divide-y-2 divide-gray-100 text-xs">
<div className="py-1">
<div className="flex flex-wrap items-center py-2">
<div className="flex items-center gap-x-2 text-sm sm:basis-1/2">
<UserIcon className="h-4 w-4 flex-shrink-0" />
<p>Owned by</p>
</div>
<div className="sm:basis-1/2 flex items-center gap-1">
{cycle.owned_by &&
(cycle.owned_by.avatar && cycle.owned_by.avatar !== "" ? (
<div className="h-5 w-5 rounded-full border-2 border-transparent">
<Image
src={cycle.owned_by.avatar}
height="100%"
width="100%"
className="rounded-full"
alt={cycle.owned_by?.first_name}
/>
</div>
) : (
<div className="grid h-5 w-5 place-items-center rounded-full border-2 border-white bg-gray-700 capitalize text-white">
{cycle.owned_by?.first_name && cycle.owned_by.first_name !== ""
? cycle.owned_by.first_name.charAt(0)
: cycle.owned_by?.email.charAt(0)}
</div>
))}
{cycle.owned_by.first_name !== ""
? cycle.owned_by.first_name
: cycle.owned_by.email}
</div>
<div className="flex flex-col items-start justify-center">
<div className="flex gap-2.5 px-7 text-sm">
<div className="flex items-center ">
<span
className={`flex items-center rounded border-[0.5px] border-gray-200 bg-gray-100 px-2.5 py-1.5 text-center text-sm capitalize text-gray-800 `}
>
{capitalizeFirstLetter(cycleStatus)}
</span>
</div>
<div className="flex flex-wrap items-center py-2">
<div className="flex items-center gap-x-2 text-sm sm:basis-1/2">
<ChartPieIcon className="h-4 w-4 flex-shrink-0" />
<p>Progress</p>
<div className="relative flex h-full w-52 items-center justify-center gap-2 text-sm text-gray-800">
<Popover className="flex h-full items-center justify-center rounded-lg">
{({ open }) => (
<>
<Popover.Button
className={`group flex h-full items-center gap-1 rounded border-[0.5px] border-gray-200 bg-gray-100 px-2.5 py-1.5 text-gray-800 ${
open ? "bg-gray-100" : ""
}`}
>
<CalendarDaysIcon className="h-3 w-3" />
<span>{renderShortDate(new Date(`${cycle?.start_date}`))}</span>
</Popover.Button>
<Transition
as={React.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 top-10 -right-5 z-20 transform overflow-hidden">
<DatePicker
selected={startDateRange}
onChange={(date) => {
submitChanges({
start_date: renderDateFormat(date),
});
setStartDateRange(date);
}}
selectsStart
startDate={startDateRange}
endDate={endDateRange}
maxDate={endDateRange}
shouldCloseOnSelect
inline
/>
</Popover.Panel>
</Transition>
</>
)}
</Popover>
<span>
<ArrowLongRightIcon className="h-3 w-3" />
</span>
<Popover className="flex h-full items-center justify-center rounded-lg">
{({ open }) => (
<>
<Popover.Button
className={`group flex items-center gap-1 rounded border-[0.5px] border-gray-200 bg-gray-100 px-2.5 py-1.5 text-gray-800 ${
open ? "bg-gray-100" : ""
}`}
>
<CalendarDaysIcon className="h-3 w-3 " />
<span>{renderShortDate(new Date(`${cycle?.end_date}`))}</span>
</Popover.Button>
<Transition
as={React.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 top-10 -right-5 z-20 transform overflow-hidden">
<DatePicker
selected={endDateRange}
onChange={(date) => {
submitChanges({
end_date: renderDateFormat(date),
});
setEndDateRange(date);
}}
selectsEnd
startDate={startDateRange}
endDate={endDateRange}
// minDate={startDateRange}
inline
/>
</Popover.Panel>
</Transition>
</>
)}
</Popover>
</div>
</div>
<div className="flex flex-col gap-6 px-7 py-6">
<div className="flex flex-col items-start justify-start gap-2 ">
<div className="flex items-center justify-start gap-2 ">
<h4 className="text-xl font-semibold text-gray-900">{cycle.name}</h4>
<CustomMenu width="lg" ellipsis>
<CustomMenu.MenuItem onClick={handleCopyText}>
<span className="flex items-center justify-start gap-2 text-gray-800">
<DocumentDuplicateIcon className="h-4 w-4" />
<span>Copy Link</span>
</span>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem onClick={() => setCycleDeleteModal(true)}>
<span className="flex items-center justify-start gap-2 text-gray-800">
<TrashIcon className="h-4 w-4" />
<span>Delete</span>
</span>
</CustomMenu.MenuItem>
</CustomMenu>
</div>
<div className="flex items-center gap-2 sm:basis-1/2">
<div className="grid flex-shrink-0 place-items-center">
<span className="whitespace-normal text-sm leading-5 text-black">
{cycle.description}
</span>
</div>
<div className="flex flex-col gap-4 text-sm">
<div className="flex items-center justify-start gap-1">
<div className="flex w-40 items-center justify-start gap-2">
<UserCircleIcon className="h-5 w-5 text-gray-400" />
<span>Lead</span>
</div>
<div className="flex items-center gap-2.5">
{cycle.owned_by.avatar && cycle.owned_by.avatar !== "" ? (
<Image
src={cycle.owned_by.avatar}
height={12}
width={12}
className="rounded-full"
alt={cycle.owned_by.first_name}
/>
) : (
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-gray-800 capitalize text-white">
{cycle.owned_by.first_name.charAt(0)}
</span>
)}
<span className="text-gray-900">{cycle.owned_by.first_name}</span>
</div>
</div>
<div className="flex items-center justify-start gap-1">
<div className="flex w-40 items-center justify-start gap-2">
<ChartPieIcon className="h-5 w-5 text-gray-400" />
<span>Progress</span>
</div>
<div className="flex items-center gap-2.5 text-gray-800">
<span className="h-4 w-4">
<ProgressBar
value={groupedIssues.completed.length}
maxValue={cycleIssues?.length}
/>
</span>
{groupedIssues.completed.length}/{cycleIssues?.length}
</div>
{groupedIssues.completed.length}/{cycleIssues?.length}
</div>
</div>
</div>
<div className="py-1" />
</div>
<div className="flex flex-col items-center justify-center w-full gap-2 ">
{isStartValid && isEndValid ? (
<div className="relative h-[200px] w-full ">
<ProgressChart
issues={issues}
start={cycle?.start_date ?? ""}
end={cycle?.end_date ?? ""}
/>
</div>
) : (
""
)}
{issues.length > 0 ? (
<SidebarProgressStats issues={issues} groupedIssues={groupedIssues} />
) : (
""
)}
<div className="flex w-full flex-col items-center justify-start gap-2 border-t border-gray-300 px-7 py-6 ">
<Disclosure>
{({ open }) => (
<div
className={`relative flex h-full w-full flex-col ${open ? "" : "flex-row"}`}
>
<div className="flex w-full items-center justify-between gap-2 ">
<div className="flex items-center justify-start gap-2 text-sm">
<span className="font-medium text-gray-500">Progress</span>
{!open && cycleIssues && progressPercentage ? (
<span className="rounded bg-[#09A953]/10 px-1.5 py-0.5 text-xs text-[#09A953]">
{progressPercentage ? `${progressPercentage}%` : ""}
</span>
) : (
""
)}
</div>
<Disclosure.Button>
<ChevronDownIcon
className={`h-3 w-3 ${open ? "rotate-180 transform" : ""}`}
aria-hidden="true"
/>
</Disclosure.Button>
</div>
<Transition show={open}>
<Disclosure.Panel>
{isStartValid && isEndValid ? (
<div className=" h-full w-full py-4">
<div className="flex items-start justify-between gap-4 py-2 text-xs">
<div className="flex items-center gap-1">
<span>
<DocumentIcon className="h-3 w-3 text-gray-500" />
</span>
<span>
Pending Issues -{" "}
{cycleIssues?.length - groupedIssues.completed.length}{" "}
</span>
</div>
<div className="flex items-center gap-3 text-gray-900">
<div className="flex items-center justify-center gap-1">
<span className="h-2.5 w-2.5 rounded-full bg-[#A9BBD0]" />
<span>Ideal</span>
</div>
<div className="flex items-center justify-center gap-1">
<span className="h-2.5 w-2.5 rounded-full bg-[#4C8FFF]" />
<span>Current</span>
</div>
</div>
</div>
<div className="relative h-40 w-80">
<ProgressChart
issues={issues}
start={cycle?.start_date ?? ""}
end={cycle?.end_date ?? ""}
/>
</div>
</div>
) : (
""
)}
</Disclosure.Panel>
</Transition>
</div>
)}
</Disclosure>
</div>
<div className="flex w-full flex-col items-center justify-start gap-2 border-t border-gray-300 px-7 py-6 ">
<Disclosure>
{({ open }) => (
<div
className={`relative flex h-full w-full flex-col ${open ? "" : "flex-row"}`}
>
<div className="flex w-full items-center justify-between gap-2 ">
<div className="flex items-center justify-start gap-2 text-sm">
<span className="font-medium text-gray-500">Other Information</span>
</div>
<Disclosure.Button>
<ChevronDownIcon
className={`h-3 w-3 ${open ? "rotate-180 transform" : ""}`}
aria-hidden="true"
/>
</Disclosure.Button>
</div>
<Transition show={open}>
<Disclosure.Panel>
{issues.length > 0 ? (
<div className=" h-full w-full py-4">
<SidebarProgressStats issues={issues} groupedIssues={groupedIssues} />
</div>
) : (
""
)}
</Disclosure.Panel>
</Transition>
</div>
)}
</Disclosure>
</div>
</>
) : (
+283 -99
View File
@@ -4,26 +4,38 @@ import Link from "next/link";
import Image from "next/image";
import { useRouter } from "next/router";
// swr
import useSWR from "swr";
import useSWR, { mutate } from "swr";
// services
import cyclesService from "services/cycles.service";
// hooks
import useToast from "hooks/use-toast";
// ui
import { Button, CustomMenu } from "components/ui";
import { CustomMenu, LinearProgressIndicator, Tooltip } from "components/ui";
import { Disclosure, Transition } from "@headlessui/react";
// icons
import { CalendarDaysIcon } from "@heroicons/react/20/solid";
import { UserIcon } from "@heroicons/react/24/outline";
import { CyclesIcon } from "components/icons";
import { ChevronDownIcon, PencilIcon, StarIcon } from "@heroicons/react/24/outline";
// helpers
import { renderShortNumericDateFormat } from "helpers/date-time.helper";
import { getDateRangeStatus, renderShortDateWithYearFormat } from "helpers/date-time.helper";
import { groupBy } from "helpers/array.helper";
import { copyTextToClipboard } from "helpers/string.helper";
import { capitalizeFirstLetter, copyTextToClipboard, truncateText } from "helpers/string.helper";
// types
import { CycleIssueResponse, ICycle } from "types";
import {
CompletedCyclesResponse,
CurrentAndUpcomingCyclesResponse,
CycleIssueResponse,
DraftCyclesResponse,
ICycle,
} from "types";
// fetch-keys
import { CYCLE_ISSUES } from "constants/fetch-keys";
import {
CYCLE_COMPLETE_LIST,
CYCLE_CURRENT_AND_UPCOMING_LIST,
CYCLE_DRAFT_LIST,
CYCLE_ISSUES,
CYCLE_LIST,
} from "constants/fetch-keys";
type TSingleStatProps = {
cycle: ICycle;
@@ -34,11 +46,11 @@ type TSingleStatProps = {
const stateGroupColours: {
[key: string]: string;
} = {
backlog: "#3f76ff",
unstarted: "#ff9e9e",
started: "#d687ff",
cancelled: "#ff5353",
completed: "#096e8d",
backlog: "#DEE2E6",
unstarted: "#26B5CE",
started: "#F7AE59",
cancelled: "#D687FF",
completed: "#09A953",
};
export const SingleCycleCard: React.FC<TSingleStatProps> = (props) => {
@@ -67,6 +79,130 @@ export const SingleCycleCard: React.FC<TSingleStatProps> = (props) => {
...groupBy(cycleIssues ?? [], "issue_detail.state_detail.group"),
};
const handleAddToFavorites = () => {
if (!workspaceSlug && !projectId && !cycle) return;
cyclesService
.addCycleToFavorites(workspaceSlug as string, projectId as string, {
cycle: cycle.id,
})
.then(() => {
const cycleStatus = getDateRangeStatus(cycle.start_date, cycle.end_date);
if (cycleStatus === "current" || cycleStatus === "upcoming")
mutate<CurrentAndUpcomingCyclesResponse>(
CYCLE_CURRENT_AND_UPCOMING_LIST(projectId as string),
(prevData) => ({
current_cycle: (prevData?.current_cycle ?? []).map((c) => ({
...c,
is_favorite: c.id === cycle.id ? true : c.is_favorite,
})),
upcoming_cycle: (prevData?.upcoming_cycle ?? []).map((c) => ({
...c,
is_favorite: c.id === cycle.id ? true : c.is_favorite,
})),
}),
false
);
else if (cycleStatus === "completed")
mutate<CompletedCyclesResponse>(
CYCLE_COMPLETE_LIST(projectId as string),
(prevData) => ({
completed_cycles: (prevData?.completed_cycles ?? []).map((c) => ({
...c,
is_favorite: c.id === cycle.id ? true : c.is_favorite,
})),
}),
false
);
else
mutate<DraftCyclesResponse>(
CYCLE_DRAFT_LIST(projectId as string),
(prevData) => ({
draft_cycles: (prevData?.draft_cycles ?? []).map((c) => ({
...c,
is_favorite: c.id === cycle.id ? true : c.is_favorite,
})),
}),
false
);
setToastAlert({
type: "success",
title: "Success!",
message: "Successfully added the cycle to favorites.",
});
})
.catch(() => {
setToastAlert({
type: "error",
title: "Error!",
message: "Couldn't add the cycle to favorites. Please try again.",
});
});
};
const handleRemoveFromFavorites = () => {
if (!workspaceSlug || !cycle) return;
cyclesService
.removeCycleFromFavorites(workspaceSlug as string, projectId as string, cycle.id)
.then(() => {
const cycleStatus = getDateRangeStatus(cycle.start_date, cycle.end_date);
if (cycleStatus === "current" || cycleStatus === "upcoming")
mutate<CurrentAndUpcomingCyclesResponse>(
CYCLE_CURRENT_AND_UPCOMING_LIST(projectId as string),
(prevData) => ({
current_cycle: (prevData?.current_cycle ?? []).map((c) => ({
...c,
is_favorite: c.id === cycle.id ? false : c.is_favorite,
})),
upcoming_cycle: (prevData?.upcoming_cycle ?? []).map((c) => ({
...c,
is_favorite: c.id === cycle.id ? false : c.is_favorite,
})),
}),
false
);
else if (cycleStatus === "completed")
mutate<CompletedCyclesResponse>(
CYCLE_COMPLETE_LIST(projectId as string),
(prevData) => ({
completed_cycles: (prevData?.completed_cycles ?? []).map((c) => ({
...c,
is_favorite: c.id === cycle.id ? false : c.is_favorite,
})),
}),
false
);
else
mutate<DraftCyclesResponse>(
CYCLE_DRAFT_LIST(projectId as string),
(prevData) => ({
draft_cycles: (prevData?.draft_cycles ?? []).map((c) => ({
...c,
is_favorite: c.id === cycle.id ? false : c.is_favorite,
})),
}),
false
);
setToastAlert({
type: "success",
title: "Success!",
message: "Successfully removed the cycle from favorites.",
});
})
.catch(() => {
setToastAlert({
type: "error",
title: "Error!",
message: "Couldn't remove the cycle from favorites. Please try again.",
});
});
};
const handleCopyText = () => {
const originURL =
typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
@@ -82,100 +218,148 @@ export const SingleCycleCard: React.FC<TSingleStatProps> = (props) => {
});
};
const progressIndicatorData = Object.keys(groupedIssues).map((group, index) => ({
id: index,
name: capitalizeFirstLetter(group),
value:
cycleIssues && cycleIssues.length > 0
? (groupedIssues[group].length / cycleIssues.length) * 100
: 0,
color: stateGroupColours[group],
}));
return (
<>
<div className="rounded-md border bg-white p-3">
<div className="grid grid-cols-9 gap-2 divide-x">
<div className="col-span-3 flex flex-col space-y-3">
<div className="flex items-start justify-between gap-2">
<Link href={`/${workspaceSlug}/projects/${projectId as string}/cycles/${cycle.id}`}>
<a>
<h2 className="font-medium w-full max-w-[175px] lg:max-w-[225px] xl:max-w-[300px] text-ellipsis overflow-hidden">
{cycle.name}
</h2>
</a>
</Link>
<CustomMenu width="auto" ellipsis>
<CustomMenu.MenuItem onClick={handleEditCycle}>Edit cycle</CustomMenu.MenuItem>
<div className="h-full w-full">
<div className="flex flex-col rounded-[10px] bg-white text-xs shadow">
<div className="flex h-full flex-col gap-4 rounded-b-[10px] px-5 py-5">
<div className="flex items-center justify-between gap-1">
<Link href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`}>
<a className="w-full">
<Tooltip tooltipContent={cycle.name} position="top-left">
<h3 className="text-xl font-semibold leading-5 ">
{truncateText(cycle.name, 75)}
</h3>
</Tooltip>
</a>
</Link>
{cycle.is_favorite ? (
<button onClick={handleRemoveFromFavorites}>
<StarIcon className="h-4 w-4 text-orange-400" fill="#f6ad55" />
</button>
) : (
<button onClick={handleAddToFavorites}>
<StarIcon className="h-4 w-4 " color="#858E96" />
</button>
)}
</div>
<div className="flex items-center justify-start gap-5">
<div className="flex items-start gap-1 ">
<CalendarDaysIcon className="h-4 w-4 text-gray-900" />
<span className="text-gray-400">Start :</span>
<span>{renderShortDateWithYearFormat(startDate)}</span>
</div>
<div className="flex items-start gap-1 ">
<CalendarDaysIcon className="h-4 w-4 text-gray-900" />
<span className="text-gray-400">End :</span>
<span>{renderShortDateWithYearFormat(endDate)}</span>
</div>
</div>
</div>
<div className="flex h-full flex-col rounded-b-[10px]">
<div className="flex items-center justify-between px-5 py-4">
<div className="flex items-center gap-2.5">
{cycle.owned_by.avatar && cycle.owned_by.avatar !== "" ? (
<Image
src={cycle.owned_by.avatar}
height={16}
width={16}
className="rounded-full"
alt={cycle.owned_by.first_name}
/>
) : (
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-gray-800 capitalize text-white">
{cycle.owned_by.first_name.charAt(0)}
</span>
)}
<span className="text-gray-900">{cycle.owned_by.first_name}</span>
</div>
<div className="flex items-center ">
<button
onClick={handleEditCycle}
className="flex cursor-pointer items-center rounded p-1 duration-300 hover:bg-gray-100"
>
<span>
<PencilIcon className="h-4 w-4" />
</span>
</button>
<CustomMenu width="auto" verticalEllipsis>
<CustomMenu.MenuItem onClick={handleDeleteCycle}>Delete cycle</CustomMenu.MenuItem>
<CustomMenu.MenuItem onClick={handleCopyText}>Copy cycle link</CustomMenu.MenuItem>
</CustomMenu>
</div>
<div className="grid grid-cols-3 gap-x-2 gap-y-3 text-xs">
<div className="flex items-center gap-2 text-gray-500">
<CalendarDaysIcon className="h-4 w-4" />
Cycle dates
</div>
<div className="col-span-2">
{renderShortNumericDateFormat(startDate)} - {renderShortNumericDateFormat(endDate)}
</div>
<div className="flex items-center gap-2 text-gray-500">
<UserIcon className="h-4 w-4" />
Created by
</div>
<div className="col-span-2 flex items-center gap-2">
{cycle.owned_by.avatar && cycle.owned_by.avatar !== "" ? (
<Image
src={cycle.owned_by.avatar}
height={16}
width={16}
className="rounded-full"
alt={cycle.owned_by.first_name}
/>
) : (
<span className="grid h-5 w-5 place-items-center rounded-full bg-gray-700 capitalize text-white">
{cycle.owned_by.first_name.charAt(0)}
</span>
)}
{cycle.owned_by.first_name}
</div>
</div>
<div className="flex h-full items-end">
<Button
theme="secondary"
className="flex items-center gap-2"
onClick={() =>
router.push(`/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`)
}
</div>
<Disclosure>
{({ open }) => (
<div
className={`flex h-full w-full flex-col border-t border-gray-200 bg-gray-100 ${
open ? "" : "flex-row"
}`}
>
<CyclesIcon className="h-3 w-3" />
Open Cycle
</Button>
</div>
</div>
<div className="col-span-2 space-y-3 px-5">
<h4 className="text-sm tracking-widest">PROGRESS</h4>
<div className="space-y-3 text-xs">
{Object.keys(groupedIssues).map((group) => (
<div key={group} className="flex items-center gap-2">
<div className="flex basis-2/3 items-center gap-2">
<span
className="block h-2 w-2 rounded-full"
style={{
backgroundColor: stateGroupColours[group],
}}
<div className="flex w-full items-center gap-2 px-5 py-4 ">
<span> Progress </span>
<LinearProgressIndicator data={progressIndicatorData} />
<Disclosure.Button>
<ChevronDownIcon
className={`h-3 w-3 ${open ? "rotate-180 transform" : ""}`}
aria-hidden="true"
/>
<h6 className="text-xs capitalize">{group}</h6>
</div>
<div>
<span>
{groupedIssues[group].length}{" "}
<span className="text-gray-500">
-{" "}
{cycleIssues && cycleIssues.length > 0
? `${Math.round(
(groupedIssues[group].length / cycleIssues.length) * 100
)}%`
: "0%"}
</span>
</span>
</div>
</Disclosure.Button>
</div>
))}
</div>
</div>
<Transition show={open}>
<Disclosure.Panel>
<div className="overflow-hidden rounded-b-md bg-white p-3 shadow">
<div className="col-span-2 space-y-3 px-5">
<div className="space-y-3 text-xs">
{Object.keys(groupedIssues).map((group) => (
<div key={group} className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2">
<span
className="block h-2 w-2 rounded-full"
style={{
backgroundColor: stateGroupColours[group],
}}
/>
<h6 className="text-xs capitalize">{group}</h6>
</div>
<div>
<span>
{groupedIssues[group].length}{" "}
<span className="text-gray-500">
-{" "}
{cycleIssues && cycleIssues.length > 0
? `${Math.round(
(groupedIssues[group].length / cycleIssues.length) * 100
)}%`
: "0%"}
</span>
</span>
</div>
</div>
))}
</div>
</div>
</div>
</Disclosure.Panel>
</Transition>
</div>
)}
</Disclosure>
</div>
</div>
</>
</div>
);
};
+11 -11
View File
@@ -44,7 +44,7 @@ const EmojiIconPicker: React.FC<Props> = ({ label, value, onChange }) => {
return (
<Popover className="relative z-[1]" ref={ref}>
<Popover.Button
className="rounded-md border border-gray-300 p-2 outline-none sm:text-sm"
className="rounded-full bg-gray-100 p-2 outline-none sm:text-sm"
onClick={() => setIsOpen((prev) => !prev)}
>
{label}
@@ -58,10 +58,10 @@ const EmojiIconPicker: React.FC<Props> = ({ label, value, onChange }) => {
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Popover.Panel className="absolute z-10 mt-2 w-80 rounded-md bg-white shadow-lg">
<Popover.Panel className="absolute z-10 mt-2 w-80 rounded-lg bg-white shadow-lg">
<div className="h-72 w-80 overflow-auto rounded border bg-white p-2 shadow-2xl">
<Tab.Group as="div" className="flex h-full w-full flex-col">
<Tab.List className="flex-0 -mx-2 flex justify-around gap-1 rounded border-b p-1">
<Tab.List className="flex-0 -mx-2 flex justify-around gap-1 border-b p-1">
{tabOptions.map((tab) => (
<Tab
key={tab.key}
@@ -75,16 +75,16 @@ const EmojiIconPicker: React.FC<Props> = ({ label, value, onChange }) => {
</Tab>
))}
</Tab.List>
<Tab.Panels className="h-full w-full flex-1 overflow-y-auto overflow-x-hidden">
<Tab.Panel className="h-full w-full">
<Tab.Panels className="flex-1 overflow-y-auto">
<Tab.Panel>
{recentEmojis.length > 0 && (
<div className="w-full py-2">
<h3 className="mb-2 text-lg">Recent Emojis</h3>
<div className="py-2">
<h3 className="mb-2">Recent Emojis</h3>
<div className="grid grid-cols-9 gap-2">
{recentEmojis.map((emoji) => (
<button
type="button"
className="select-none text-xl"
className="select-none text-lg hover:bg-hover-gray"
key={emoji}
onClick={() => {
onChange(emoji);
@@ -97,13 +97,13 @@ const EmojiIconPicker: React.FC<Props> = ({ label, value, onChange }) => {
</div>
</div>
)}
<div className="py-3">
<h3 className="mb-2 text-lg">All Emojis</h3>
<div>
<h3 className="mb-2">All Emojis</h3>
<div className="grid grid-cols-9 gap-2">
{emojis.map((emoji) => (
<button
type="button"
className="select-none text-xl"
className="select-none text-lg hover:bg-hover-gray"
key={emoji}
onClick={() => {
onChange(emoji);
@@ -0,0 +1,20 @@
import React from "react";
import type { Props } from "./types";
export const AssignmentClipboardIcon: React.FC<Props> = ({
width = "24",
height = "24",
className,
}) => (
<svg
width={width}
height={height}
className={className}
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M2.125 19.25C1.74306 19.25 1.4184 19.1163 1.15104 18.8489C0.883681 18.5816 0.75 18.2569 0.75 17.875V4.12499C0.75 3.74305 0.883681 3.41839 1.15104 3.15103C1.4184 2.88367 1.74306 2.74999 2.125 2.74999H6.82292C6.89931 2.21527 7.14375 1.77603 7.55625 1.43228C7.96875 1.08853 8.45 0.916656 9 0.916656C9.55 0.916656 10.0312 1.08853 10.4438 1.43228C10.8562 1.77603 11.1007 2.21527 11.1771 2.74999H15.875C16.2569 2.74999 16.5816 2.88367 16.849 3.15103C17.1163 3.41839 17.25 3.74305 17.25 4.12499V17.875C17.25 18.2569 17.1163 18.5816 16.849 18.8489C16.5816 19.1163 16.2569 19.25 15.875 19.25H2.125ZM2.125 17.875H15.875V4.12499H2.125V17.875ZM4.41667 15.5833H10.6729V14.2083H4.41667V15.5833ZM4.41667 11.6875H13.5833V10.3125H4.41667V11.6875ZM4.41667 7.79166H13.5833V6.41666H4.41667V7.79166ZM9 3.73541C9.21389 3.73541 9.40104 3.6552 9.56146 3.49478C9.72188 3.33436 9.80208 3.14721 9.80208 2.93332C9.80208 2.71943 9.72188 2.53228 9.56146 2.37186C9.40104 2.21145 9.21389 2.13124 9 2.13124C8.78611 2.13124 8.59896 2.21145 8.43854 2.37186C8.27812 2.53228 8.19792 2.71943 8.19792 2.93332C8.19792 3.14721 8.27812 3.33436 8.43854 3.49478C8.59896 3.6552 8.78611 3.73541 9 3.73541ZM2.125 17.875V4.12499V17.875Z" />
</svg>
);
@@ -0,0 +1,21 @@
import React from "react";
import type { Props } from "./types";
export const BacklogStateIcon: React.FC<Props> = ({
width = "20",
height = "20",
className,
color = "#858e96",
}) => (
<svg
width={width}
height={height}
className={className}
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle cx="10" cy="10" r="9" stroke={color} strokeLinecap="round" strokeDasharray="4 4" />
</svg>
);
@@ -0,0 +1,78 @@
import React from "react";
import type { Props } from "./types";
export const CancelledStateIcon: React.FC<Props> = ({
width = "20",
height = "20",
className,
color = "#f2655a",
}) => (
<svg
width={width}
height={height}
className={className}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 84.36 84.36"
>
<g id="Layer_2" data-name="Layer 2">
<g id="Layer_1-2" data-name="Layer 1">
<path
className="cls-1"
fill="none"
strokeWidth={3}
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
d="M20.45,7.69a39.74,39.74,0,0,1,43.43.54"
/>
<path
className="cls-1"
fill="none"
strokeWidth={3}
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
d="M76.67,20.45a39.76,39.76,0,0,1-.53,43.43"
/>
<path
className="cls-1"
fill="none"
strokeWidth={3}
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
d="M63.92,76.67a39.78,39.78,0,0,1-43.44-.53"
/>
<path
className="cls-1"
fill="none"
strokeWidth={3}
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
d="M7.69,63.92a39.75,39.75,0,0,1,.54-43.44"
/>
<circle className="cls-2" fill={color} cx="42.18" cy="42.18" r="31.04" />
<path
className="cls-3"
fill="none"
strokeWidth={3}
stroke="#ffffff"
strokeLinecap="square"
strokeMiterlimit={10}
d="M32.64,32.44q9.54,9.75,19.09,19.48"
/>
<path
className="cls-3"
fill="none"
strokeWidth={3}
stroke="#ffffff"
strokeLinecap="square"
strokeMiterlimit={10}
d="M32.64,51.92,51.73,32.44"
/>
</g>
</g>
</svg>
);
@@ -0,0 +1,69 @@
import React from "react";
import type { Props } from "./types";
export const CompletedStateIcon: React.FC<Props> = ({
width = "20",
height = "20",
className,
color = "#438af3",
}) => (
<svg
width={width}
height={height}
className={className}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 84.36 84.36"
>
<g id="Layer_2" data-name="Layer 2">
<g id="Layer_1-2" data-name="Layer 1">
<path
className="cls-1"
fill="none"
strokeWidth={3}
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
d="M20.45,7.69a39.74,39.74,0,0,1,43.43.54"
/>
<path
className="cls-1"
fill="none"
strokeWidth={3}
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
d="M76.67,20.45a39.76,39.76,0,0,1-.53,43.43"
/>
<path
className="cls-1"
fill="none"
strokeWidth={3}
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
d="M63.92,76.67a39.78,39.78,0,0,1-43.44-.53"
/>
<path
className="cls-1"
fill="none"
strokeWidth={3}
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
d="M7.69,63.92a39.75,39.75,0,0,1,.54-43.44"
/>
<circle className="cls-2" fill={color} cx="42.18" cy="42.18" r="31.04" />
<path
className="cls-3"
fill="none"
strokeWidth={3}
stroke="#ffffff"
strokeLinecap="square"
strokeMiterlimit={10}
d="M30.45,43.75l6.61,6.61L53.92,34"
/>
</g>
</g>
</svg>
);
@@ -0,0 +1,25 @@
import React from "react";
import type { Props } from "./types";
export const ContrastIcon: React.FC<Props> = ({
width = "24",
height = "24",
color = "#858E96",
className,
}) => (
<svg
width={width}
height={height}
className={className}
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle cx="9" cy="9" r="5.4375" stroke={color} strokeLinecap="round" />
<path
fill={color}
d="M9 5.81247C9 5.39825 9.33876 5.05526 9.74548 5.13368C10.0057 5.18385 10.2608 5.26029 10.5068 5.36219C10.9845 5.56007 11.4186 5.8501 11.7842 6.21574C12.1499 6.58137 12.4399 7.01543 12.6378 7.49315C12.8357 7.97087 12.9375 8.48289 12.9375 8.99997C12.9375 9.51705 12.8357 10.0291 12.6378 10.5068C12.4399 10.9845 12.1499 11.4186 11.7842 11.7842C11.4186 12.1498 10.9845 12.4399 10.5068 12.6377C10.2608 12.7396 10.0057 12.8161 9.74548 12.8663C9.33876 12.9447 9 12.6017 9 12.1875L9 5.81247Z"
/>
</svg>
);
@@ -0,0 +1,24 @@
import React from "react";
import type { Props } from "./types";
export const GridViewIcon: React.FC<Props> = ({
width = "24",
height = "24",
color = "#858E96",
className,
}) => (
<svg
width={width}
height={height}
className={className}
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill={color}
d="M2.125 8.3125C1.74688 8.3125 1.42318 8.17786 1.15391 7.90859C0.884635 7.63932 0.75 7.31563 0.75 6.9375V2.125C0.75 1.74688 0.884635 1.42318 1.15391 1.15391C1.42318 0.884635 1.74688 0.75 2.125 0.75H6.9375C7.31563 0.75 7.63932 0.884635 7.90859 1.15391C8.17786 1.42318 8.3125 1.74688 8.3125 2.125V6.9375C8.3125 7.31563 8.17786 7.63932 7.90859 7.90859C7.63932 8.17786 7.31563 8.3125 6.9375 8.3125H2.125ZM2.125 17.25C1.74688 17.25 1.42318 17.1154 1.15391 16.8461C0.884635 16.5768 0.75 16.2531 0.75 15.875V11.0625C0.75 10.6844 0.884635 10.3607 1.15391 10.0914C1.42318 9.82214 1.74688 9.6875 2.125 9.6875H6.9375C7.31563 9.6875 7.63932 9.82214 7.90859 10.0914C8.17786 10.3607 8.3125 10.6844 8.3125 11.0625V15.875C8.3125 16.2531 8.17786 16.5768 7.90859 16.8461C7.63932 17.1154 7.31563 17.25 6.9375 17.25H2.125ZM11.0625 8.3125C10.6844 8.3125 10.3607 8.17786 10.0914 7.90859C9.82214 7.63932 9.6875 7.31563 9.6875 6.9375V2.125C9.6875 1.74688 9.82214 1.42318 10.0914 1.15391C10.3607 0.884635 10.6844 0.75 11.0625 0.75H15.875C16.2531 0.75 16.5768 0.884635 16.8461 1.15391C17.1154 1.42318 17.25 1.74688 17.25 2.125V6.9375C17.25 7.31563 17.1154 7.63932 16.8461 7.90859C16.5768 8.17786 16.2531 8.3125 15.875 8.3125H11.0625ZM11.0625 17.25C10.6844 17.25 10.3607 17.1154 10.0914 16.8461C9.82214 16.5768 9.6875 16.2531 9.6875 15.875V11.0625C9.6875 10.6844 9.82214 10.3607 10.0914 10.0914C10.3607 9.82214 10.6844 9.6875 11.0625 9.6875H15.875C16.2531 9.6875 16.5768 9.82214 16.8461 10.0914C17.1154 10.3607 17.25 10.6844 17.25 11.0625V15.875C17.25 16.2531 17.1154 16.5768 16.8461 16.8461C16.5768 17.1154 16.2531 17.25 15.875 17.25H11.0625ZM2.125 6.9375H6.9375V2.125H2.125V6.9375ZM11.0625 6.9375H15.875V2.125H11.0625V6.9375ZM11.0625 15.875H15.875V11.0625H11.0625V15.875ZM2.125 15.875H6.9375V11.0625H2.125V15.875Z"
/>
</svg>
);
+23 -18
View File
@@ -2,21 +2,26 @@ import React from "react";
import type { Props } from "./types";
export const HeartbeatIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => (
<svg
width={width}
height={height}
className={className}
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M3 12H7.5L9 6L13 18L15 9L16.5 12H21"
stroke="black"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
export const HeartbeatIcon: React.FC<Props> = ({
width = "24",
height = "24",
color = "#858E96",
className,
}) => (
<svg
width={width}
height={height}
className={className}
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M2 8H5L6 4L8.66667 12L10 6L11 8H14"
stroke={color}
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
+12
View File
@@ -1,12 +1,15 @@
export * from "./attachment-icon";
export * from "./backlog-state-icon";
export * from "./blocked-icon";
export * from "./blocker-icon";
export * from "./bolt-icon";
export * from "./calendar-month-icon";
export * from "./cancel-icon";
export * from "./cancelled-state-icon";
export * from "./clipboard-icon";
export * from "./comment-icon";
export * from "./completed-cycle-icon";
export * from "./completed-state-icon";
export * from "./current-cycle-icon";
export * from "./cycle-icon";
export * from "./discord-icon";
@@ -16,6 +19,7 @@ export * from "./ellipsis-horizontal-icon";
export * from "./external-link-icon";
export * from "./github-icon";
export * from "./heartbeat-icon";
export * from "./started-state-icon";
export * from "./layer-diagonal-icon";
export * from "./lock-icon";
export * from "./menu-icon";
@@ -23,9 +27,17 @@ export * from "./plus-icon";
export * from "./question-mark-circle-icon";
export * from "./setting-icon";
export * from "./signal-cellular-icon";
export * from "./started-state-icon";
export * from "./state-group-icon";
export * from "./tag-icon";
export * from "./tune-icon";
export * from "./unstarted-state-icon";
export * from "./upcoming-cycle-icon";
export * from "./user-group-icon";
export * from "./user-icon-circle";
export * from "./user-icon";
export * from "./grid-view-icons";
export * from "./assignment-clipboard-icon";
export * from "./tick-mark-icon";
export * from "./contrast-icon";
export * from "./people-group-icon";
@@ -6,19 +6,21 @@ export const LayerDiagonalIcon: React.FC<Props> = ({
width = "24",
height = "24",
className,
color = "black",
color = "#858E96",
}) => (
<svg
width={width}
height={height}
className={className}
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12.6004 4.20111C12.6005 4.10308 12.5766 4.00653 12.5307 3.91989C12.4849 3.83325 12.4185 3.75916 12.3374 3.7041C12.2563 3.64905 12.1629 3.6147 12.0655 3.60407C11.968 3.59343 11.8695 3.60684 11.7784 3.64311L4.73799 6.43191C4.4024 6.56473 4.11449 6.79536 3.91163 7.09387C3.70877 7.39239 3.60033 7.74499 3.60039 8.10591V14.9963C3.60037 15.0942 3.6243 15.1906 3.67009 15.2771C3.71587 15.3636 3.78212 15.4376 3.86306 15.4926C3.94401 15.5476 4.03718 15.582 4.13446 15.5928C4.23175 15.6035 4.33018 15.5903 4.42119 15.5543L4.80039 15.4043V16.6943C4.52882 16.7903 4.23817 16.8198 3.95286 16.7802C3.66755 16.7405 3.39592 16.633 3.16077 16.4667C2.92563 16.3003 2.73385 16.0799 2.60153 15.8241C2.46922 15.5682 2.40024 15.2844 2.40039 14.9963V8.10591C2.40029 7.50438 2.58101 6.91671 2.91912 6.41919C3.25722 5.92166 3.73707 5.53727 4.29639 5.31591L11.3368 2.52711C11.6011 2.42232 11.8864 2.38163 12.1695 2.40837C12.4526 2.43511 12.7252 2.52852 12.9652 2.68095C13.2052 2.83337 13.4057 3.04049 13.5503 3.28532C13.6948 3.53015 13.7793 3.80574 13.7968 4.08951L12.5992 4.56231V4.20111H12.6004ZM16.2004 6.60111C16.2003 6.50318 16.1762 6.40677 16.1303 6.32029C16.0844 6.23381 16.018 6.15988 15.9369 6.10496C15.8558 6.05005 15.7625 6.01581 15.6652 6.00523C15.5678 5.99466 15.4694 6.00808 15.3784 6.04431L7.95879 8.98311C7.73506 9.07165 7.54312 9.22541 7.40788 9.42442C7.27264 9.62343 7.20035 9.8585 7.20039 10.0991V17.3975C7.20037 17.4954 7.2243 17.5918 7.27009 17.6783C7.31587 17.7648 7.38212 17.8388 7.46306 17.8938C7.54401 17.9488 7.63718 17.9832 7.73446 17.994C7.83175 18.0047 7.93018 17.9915 8.02119 17.9555L9.60039 17.3279V18.6191L8.46399 19.0691C8.1911 19.1773 7.89587 19.2172 7.60405 19.1852C7.31223 19.1531 7.03267 19.0502 6.78974 18.8854C6.54681 18.7206 6.34788 18.4988 6.2103 18.2395C6.07272 17.9801 6.00065 17.6911 6.00039 17.3975V10.0979C6.00031 9.61668 6.14489 9.14655 6.41537 8.74853C6.68585 8.35051 7.06974 8.043 7.51719 7.86591L14.9368 4.92831C15.2096 4.82033 15.5047 4.78068 15.7964 4.81283C16.088 4.84497 16.3674 4.94793 16.6102 5.11273C16.8529 5.27753 17.0517 5.49919 17.1893 5.75839C17.3268 6.01759 17.3989 6.30648 17.3992 6.59991V6.72351L16.2004 7.19991V6.59991V6.60111ZM19.5796 8.44311C19.6705 8.40713 19.7688 8.39391 19.866 8.4046C19.9632 8.4153 20.0563 8.44958 20.1372 8.50447C20.2181 8.55936 20.2844 8.63319 20.3303 8.71954C20.3761 8.80589 20.4002 8.90213 20.4004 8.99991V16.9487C20.4002 17.0688 20.3639 17.1861 20.2963 17.2853C20.2287 17.3846 20.1329 17.4613 20.0212 17.5055L12.8212 20.3579C12.7302 20.3939 12.6317 20.4071 12.5345 20.3964C12.4372 20.3856 12.344 20.3512 12.2631 20.2962C12.1821 20.2412 12.1159 20.1672 12.0701 20.0807C12.0243 19.9942 12.0004 19.8978 12.0004 19.7999V11.8523C12.0004 11.732 12.0365 11.6145 12.1041 11.515C12.1718 11.4155 12.2677 11.3386 12.3796 11.2943L19.5796 8.44311ZM21.6004 8.99991C21.6002 8.70638 21.5283 8.41735 21.3909 8.15799C21.2535 7.89863 21.0547 7.67681 20.8119 7.51187C20.5691 7.34693 20.2896 7.24386 19.9979 7.21166C19.7061 7.17946 19.4109 7.21909 19.138 7.32711L11.938 10.1783C11.6024 10.3111 11.3145 10.5418 11.1116 10.8403C10.9088 11.1388 10.8003 11.4914 10.8004 11.8523V19.7999C10.8003 20.0935 10.8721 20.3827 11.0095 20.6422C11.1468 20.9018 11.3456 21.1237 11.5884 21.2888C11.8312 21.4539 12.1108 21.5571 12.4026 21.5893C12.6945 21.6216 12.9898 21.582 13.2628 21.4739L20.4628 18.6215C20.7982 18.4888 21.086 18.2583 21.2888 17.96C21.4917 17.6618 21.6002 17.3094 21.6004 16.9487V8.99991Z"
fill={color}
/>
</svg>
);
<svg
width={width}
height={height}
className={className}
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8.40034 2.80076C8.40041 2.73541 8.38446 2.67104 8.35389 2.61328C8.32332 2.55552 8.27907 2.50613 8.225 2.46942C8.17093 2.43272 8.1087 2.40982 8.04373 2.40273C7.97877 2.39564 7.91306 2.40458 7.85234 2.42876L3.15874 4.28796C2.93501 4.3765 2.74307 4.53026 2.60783 4.72927C2.47259 4.92828 2.4003 5.16335 2.40034 5.40396V9.99756C2.40033 10.0628 2.41628 10.1271 2.44681 10.1847C2.47733 10.2424 2.5215 10.2917 2.57546 10.3284C2.62942 10.3651 2.69154 10.388 2.75639 10.3952C2.82125 10.4024 2.88687 10.3936 2.94754 10.3696L3.20034 10.2696V11.1296C3.01929 11.1936 2.82553 11.2132 2.63532 11.1868C2.44512 11.1604 2.26403 11.0887 2.10726 10.9778C1.9505 10.8669 1.82265 10.72 1.73444 10.5494C1.64623 10.3788 1.60024 10.1896 1.60034 9.99756V5.40396C1.60027 5.00294 1.72076 4.61116 1.94616 4.27948C2.17156 3.9478 2.49146 3.69153 2.86434 3.54396L7.55794 1.68476C7.73414 1.6149 7.92438 1.58777 8.11308 1.6056C8.30178 1.62343 8.48358 1.6857 8.64358 1.78732C8.80358 1.88894 8.93723 2.02701 9.03359 2.19023C9.12994 2.35345 9.18627 2.53718 9.19794 2.72636L8.39954 3.04156V2.80076H8.40034ZM10.8003 4.40076C10.8003 4.33548 10.7842 4.2712 10.7536 4.21355C10.723 4.15589 10.6787 4.10661 10.6247 4.07C10.5706 4.03339 10.5084 4.01056 10.4435 4.00351C10.3786 3.99646 10.313 4.0054 10.2523 4.02956L5.30594 5.98876C5.15679 6.04779 5.02883 6.15029 4.93867 6.28297C4.84851 6.41564 4.80031 6.57235 4.80034 6.73276V11.5984C4.80033 11.6636 4.81628 11.7279 4.84681 11.7855C4.87733 11.8432 4.9215 11.8925 4.97546 11.9292C5.02942 11.9659 5.09154 11.9888 5.15639 11.996C5.22125 12.0032 5.28687 11.9944 5.34754 11.9704L6.40034 11.552V12.4128L5.64274 12.7128C5.46081 12.7849 5.264 12.8115 5.06945 12.7901C4.8749 12.7688 4.68853 12.7002 4.52658 12.5903C4.36462 12.4804 4.232 12.3326 4.14028 12.1597C4.04856 11.9868 4.00052 11.7941 4.00034 11.5984V6.73196C4.00029 6.41114 4.09668 6.09772 4.277 5.83237C4.45732 5.56703 4.71324 5.36202 5.01154 5.24396L9.95794 3.28556C10.1398 3.21357 10.3366 3.18714 10.531 3.20857C10.7254 3.23 10.9117 3.29864 11.0735 3.40851C11.2354 3.51838 11.3679 3.66615 11.4596 3.83895C11.5513 4.01175 11.5993 4.20434 11.5995 4.39996V4.48236L10.8003 4.79996V4.39996V4.40076ZM13.0531 5.62876C13.1138 5.60477 13.1793 5.59596 13.2441 5.60309C13.3089 5.61022 13.371 5.63307 13.4249 5.66967C13.4788 5.70626 13.523 5.75548 13.5536 5.81305C13.5842 5.87061 13.6002 5.93478 13.6003 5.99996V11.2992C13.6002 11.3792 13.576 11.4574 13.531 11.5236C13.4859 11.5898 13.422 11.6409 13.3475 11.6704L8.54754 13.572C8.48687 13.596 8.42125 13.6048 8.35639 13.5976C8.29154 13.5904 8.22942 13.5675 8.17546 13.5308C8.1215 13.4941 8.07733 13.4448 8.04681 13.3871C8.01628 13.3295 8.00033 13.2652 8.00034 13.2V7.90156C8.00033 7.82136 8.02443 7.743 8.06951 7.67666C8.11459 7.61033 8.17857 7.55907 8.25314 7.52956L13.0531 5.62876V5.62876ZM14.4003 5.99996C14.4002 5.80428 14.3523 5.61159 14.2607 5.43868C14.1691 5.26577 14.0365 5.11789 13.8747 5.00793C13.7128 4.89797 13.5265 4.82926 13.332 4.80779C13.1375 4.78633 12.9407 4.81275 12.7587 4.88476L7.95874 6.78556C7.73502 6.8741 7.54307 7.02786 7.40783 7.22687C7.27259 7.42588 7.2003 7.66095 7.20034 7.90156V13.2C7.20031 13.3957 7.24816 13.5885 7.33973 13.7615C7.4313 13.9345 7.56381 14.0825 7.72569 14.1926C7.88758 14.3026 8.07393 14.3714 8.26849 14.3929C8.46306 14.4144 8.65993 14.388 8.84194 14.316L13.6419 12.4144C13.8655 12.3259 14.0574 12.1722 14.1926 11.9734C14.3279 11.7745 14.4002 11.5396 14.4003 11.2992V5.99996Z"
fill={color}
stroke={color}
strokeWidth="0.25"
/>
</svg>
);
@@ -0,0 +1,24 @@
import React from "react";
import type { Props } from "./types";
export const PeopleGroupIcon: React.FC<Props> = ({
width = "24",
height = "16",
color = "#858E96",
className,
}) => (
<svg
width={width}
height={height}
className={className}
viewBox="0 0 20 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill={color}
d="M12.2951 6.66667C13.1001 6.66667 13.7534 7.18933 13.7534 7.83333V10.9993C13.7534 11.7952 13.3582 12.5584 12.6548 13.1211C11.9514 13.6839 10.9974 14 10.0026 14C9.0078 14 8.05376 13.6839 7.35034 13.1211C6.64692 12.5584 6.25175 11.7952 6.25175 10.9993V7.83333C6.25175 7.18933 6.90425 6.66667 7.71008 6.66667H12.2951ZM12.2951 7.66667H7.71008C7.65483 7.66667 7.60184 7.68423 7.56277 7.71548C7.5237 7.74674 7.50175 7.78913 7.50175 7.83333V10.9993C7.50175 11.5299 7.76523 12.0388 8.23422 12.414C8.70322 12.7892 9.33932 13 10.0026 13C10.6658 13 11.3019 12.7892 11.7709 12.414C12.2399 12.0388 12.5034 11.5299 12.5034 10.9993V7.83333C12.5034 7.78913 12.4815 7.74674 12.4424 7.71548C12.4033 7.68423 12.3503 7.66667 12.2951 7.66667ZM3.12508 6.66667H5.94258C5.64858 6.95073 5.46903 7.29937 5.42758 7.66667H3.12508C3.06983 7.66667 3.01684 7.68423 2.97777 7.71548C2.9387 7.74674 2.91675 7.78913 2.91675 7.83333V9.99933C2.91669 10.2513 2.988 10.4999 3.12531 10.7266C3.26262 10.9533 3.46236 11.1522 3.70952 11.3083C3.95669 11.4644 4.24486 11.5737 4.55239 11.6279C4.85991 11.6821 5.17879 11.6799 5.48508 11.6213C5.55591 11.9573 5.68508 12.278 5.86258 12.576C5.36865 12.6817 4.85094 12.6951 4.34949 12.6152C3.84804 12.5353 3.37629 12.3642 2.9707 12.1151C2.56512 11.866 2.23657 11.5457 2.01047 11.1788C1.78436 10.8119 1.66676 10.4084 1.66675 9.99933V7.83333C1.66675 7.18933 2.32008 6.66667 3.12508 6.66667ZM14.0626 6.66667H16.8751C17.6801 6.66667 18.3334 7.18933 18.3334 7.83333V10C18.3335 10.4088 18.2162 10.8121 17.9904 11.1788C17.7646 11.5455 17.4365 11.8658 17.0314 12.1149C16.6262 12.364 16.155 12.5353 15.6539 12.6155C15.1529 12.6956 14.6355 12.6826 14.1417 12.5773C14.3201 12.2787 14.4492 11.958 14.5209 11.622C14.8268 11.6798 15.1451 11.6815 15.452 11.627C15.7588 11.5725 16.0463 11.4631 16.2928 11.307C16.5393 11.151 16.7384 10.9524 16.8754 10.726C17.0123 10.4997 17.0834 10.2515 17.0834 10V7.83333C17.0834 7.78913 17.0615 7.74674 17.0224 7.71548C16.9833 7.68423 16.9303 7.66667 16.8751 7.66667H14.5776C14.5361 7.29937 14.3566 6.95073 14.0626 6.66667ZM10.0001 2C10.6631 2 11.299 2.21071 11.7678 2.58579C12.2367 2.96086 12.5001 3.46957 12.5001 4C12.5001 4.53043 12.2367 5.03914 11.7678 5.41421C11.299 5.78929 10.6631 6 10.0001 6C9.33704 6 8.70115 5.78929 8.23231 5.41421C7.76347 5.03914 7.50008 4.53043 7.50008 4C7.50008 3.46957 7.76347 2.96086 8.23231 2.58579C8.70115 2.21071 9.33704 2 10.0001 2ZM15.4167 2.66667C15.9693 2.66667 16.4992 2.84226 16.8899 3.15482C17.2806 3.46738 17.5001 3.89131 17.5001 4.33333C17.5001 4.77536 17.2806 5.19928 16.8899 5.51184C16.4992 5.82441 15.9693 6 15.4167 6C14.8642 6 14.3343 5.82441 13.9436 5.51184C13.5529 5.19928 13.3334 4.77536 13.3334 4.33333C13.3334 3.89131 13.5529 3.46738 13.9436 3.15482C14.3343 2.84226 14.8642 2.66667 15.4167 2.66667ZM4.58341 2.66667C5.13595 2.66667 5.66585 2.84226 6.05655 3.15482C6.44725 3.46738 6.66675 3.89131 6.66675 4.33333C6.66675 4.77536 6.44725 5.19928 6.05655 5.51184C5.66585 5.82441 5.13595 6 4.58341 6C4.03088 6 3.50098 5.82441 3.11028 5.51184C2.71957 5.19928 2.50008 4.77536 2.50008 4.33333C2.50008 3.89131 2.71957 3.46738 3.11028 3.15482C3.50098 2.84226 4.03088 2.66667 4.58341 2.66667ZM10.0001 3C9.66856 3 9.35062 3.10536 9.1162 3.29289C8.88178 3.48043 8.75008 3.73478 8.75008 4C8.75008 4.26522 8.88178 4.51957 9.1162 4.70711C9.35062 4.89464 9.66856 5 10.0001 5C10.3316 5 10.6495 4.89464 10.884 4.70711C11.1184 4.51957 11.2501 4.26522 11.2501 4C11.2501 3.73478 11.1184 3.48043 10.884 3.29289C10.6495 3.10536 10.3316 3 10.0001 3ZM15.4167 3.66667C15.1957 3.66667 14.9838 3.7369 14.8275 3.86193C14.6712 3.98695 14.5834 4.15652 14.5834 4.33333C14.5834 4.51014 14.6712 4.67971 14.8275 4.80474C14.9838 4.92976 15.1957 5 15.4167 5C15.6378 5 15.8497 4.92976 16.006 4.80474C16.1623 4.67971 16.2501 4.51014 16.2501 4.33333C16.2501 4.15652 16.1623 3.98695 16.006 3.86193C15.8497 3.7369 15.6378 3.66667 15.4167 3.66667ZM4.58341 3.66667C4.3624 3.66667 4.15044 3.7369 3.99416 3.86193C3.83788 3.98695 3.75008 4.15652 3.75008 4.33333C3.75008 4.51014 3.83788 4.67971 3.99416 4.80474C4.15044 4.92976 4.3624 5 4.58341 5C4.80443 5 5.01639 4.92976 5.17267 4.80474C5.32895 4.67971 5.41675 4.51014 5.41675 4.33333C5.41675 4.15652 5.32895 3.98695 5.17267 3.86193C5.01639 3.7369 4.80443 3.66667 4.58341 3.66667Z"
/>
</svg>
);
+20 -15
View File
@@ -2,18 +2,23 @@ import React from "react";
import type { Props } from "./types";
export const SettingIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => (
<svg
width={width}
height={height}
className={className}
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M14.0794 1.5C14.2382 1.50001 14.3929 1.55041 14.5212 1.64394C14.6495 1.73748 14.7448 1.86933 14.7934 2.0205L15.6184 4.584C15.9649 4.7535 16.2964 4.944 16.6129 5.1585L19.2469 4.5915C19.4022 4.55834 19.564 4.57534 19.7091 4.64003C19.8541 4.70473 19.9748 4.81379 20.0539 4.9515L22.1329 8.55C22.2123 8.68763 22.2459 8.84693 22.2289 9.0049C22.212 9.16288 22.1452 9.31139 22.0384 9.429L20.2309 11.424C20.2572 11.8065 20.2572 12.1905 20.2309 12.573L22.0384 14.571C22.1452 14.6886 22.212 14.8371 22.2289 14.9951C22.2459 15.1531 22.2123 15.3124 22.1329 15.45L20.0539 19.05C19.9746 19.1874 19.8538 19.2962 19.7088 19.3606C19.5638 19.425 19.4021 19.4418 19.2469 19.4085L16.6129 18.8415C16.2979 19.0545 15.9649 19.2465 15.6199 19.416L14.7934 21.9795C14.7448 22.1307 14.6495 22.2625 14.5212 22.3561C14.3929 22.4496 14.2382 22.5 14.0794 22.5H9.92141C9.76262 22.5 9.60793 22.4496 9.47962 22.3561C9.35131 22.2625 9.256 22.1307 9.20741 21.9795L8.38391 19.4175C8.03834 19.2485 7.70502 19.0555 7.38641 18.84L4.75391 19.4085C4.5986 19.4417 4.43678 19.4247 4.29176 19.36C4.14673 19.2953 4.02598 19.1862 3.94691 19.0485L1.86791 15.45C1.78852 15.3124 1.75489 15.1531 1.77188 14.9951C1.78886 14.8371 1.85558 14.6886 1.96241 14.571L3.76991 12.573C3.74372 12.1914 3.74372 11.8086 3.76991 11.427L1.96241 9.429C1.85558 9.31139 1.78886 9.16288 1.77188 9.0049C1.75489 8.84693 1.78852 8.68763 1.86791 8.55L3.94691 4.95C4.0262 4.81256 4.14704 4.70381 4.29205 4.63939C4.43706 4.57497 4.59876 4.55821 4.75391 4.5915L7.38641 5.16C7.70441 4.9455 8.03741 4.752 8.38391 4.5825L9.20891 2.0205C9.25734 1.86982 9.3522 1.73832 9.47991 1.64482C9.60762 1.55133 9.76163 1.50064 9.91991 1.5H14.0779H14.0794ZM13.5304 3H10.4704L9.61841 5.6505L9.04391 5.931C8.76148 6.06921 8.48885 6.22657 8.22791 6.402L7.69691 6.762L4.97291 6.174L3.44291 8.826L5.31041 10.893L5.26541 11.529C5.24385 11.8426 5.24385 12.1574 5.26541 12.471L5.31041 13.107L3.43991 15.174L4.97141 17.826L7.69541 17.2395L8.22641 17.598C8.48735 17.7734 8.75998 17.9308 9.04241 18.069L9.61691 18.3495L10.4704 21H13.5334L14.3884 18.348L14.9614 18.069C15.2435 17.9311 15.5157 17.7737 15.7759 17.598L16.3054 17.2395L19.0309 17.826L20.5609 15.174L18.6919 13.107L18.7369 12.471C18.7585 12.1569 18.7585 11.8416 18.7369 11.5275L18.6919 10.8915L20.5624 8.826L19.0309 6.174L16.3054 6.759L15.7759 6.402C15.5157 6.22622 15.2435 6.06884 14.9614 5.931L14.3884 5.652L13.5319 3H13.5304ZM12.0004 7.5C13.1939 7.5 14.3385 7.97411 15.1824 8.81802C16.0263 9.66193 16.5004 10.8065 16.5004 12C16.5004 13.1935 16.0263 14.3381 15.1824 15.182C14.3385 16.0259 13.1939 16.5 12.0004 16.5C10.8069 16.5 9.66234 16.0259 8.81843 15.182C7.97451 14.3381 7.50041 13.1935 7.50041 12C7.50041 10.8065 7.97451 9.66193 8.81843 8.81802C9.66234 7.97411 10.8069 7.5 12.0004 7.5ZM12.0004 9C11.2048 9 10.4417 9.31607 9.87909 9.87868C9.31648 10.4413 9.00041 11.2044 9.00041 12C9.00041 12.7956 9.31648 13.5587 9.87909 14.1213C10.4417 14.6839 11.2048 15 12.0004 15C12.7961 15 13.5591 14.6839 14.1217 14.1213C14.6843 13.5587 15.0004 12.7956 15.0004 12C15.0004 11.2044 14.6843 10.4413 14.1217 9.87868C13.5591 9.31607 12.7961 9 12.0004 9Z"
fill="black"
/>
</svg>
);
export const SettingIcon: React.FC<Props> = ({
width = "24",
height = "24",
color = "black",
className,
}) => (
<svg
width={width}
height={height}
className={className}
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill={color}
d="M12.9062 1.375C13.0518 1.375 13.1936 1.42121 13.3112 1.50695C13.4288 1.59269 13.5162 1.71355 13.5607 1.85212L14.317 4.202C14.6346 4.35737 14.9385 4.532 15.2286 4.72862L17.6431 4.20888C17.7854 4.17848 17.9338 4.19406 18.0667 4.25336C18.1997 4.31267 18.3103 4.41264 18.3828 4.53888L20.2886 7.8375C20.3614 7.96366 20.3922 8.10968 20.3766 8.2545C20.361 8.39931 20.2999 8.53544 20.202 8.64325L18.5451 10.472C18.5692 10.8227 18.5692 11.1746 18.5451 11.5252L20.202 13.3567C20.2999 13.4646 20.361 13.6007 20.3766 13.7455C20.3922 13.8903 20.3614 14.0363 20.2886 14.1625L18.3828 17.4625C18.3101 17.5885 18.1994 17.6882 18.0664 17.7472C17.9335 17.8063 17.7853 17.8216 17.6431 17.7911L15.2286 17.2714C14.9398 17.4666 14.6346 17.6426 14.3183 17.798L13.5607 20.1479C13.5162 20.2864 13.4288 20.4073 13.3112 20.4931C13.1936 20.5788 13.0518 20.625 12.9062 20.625H9.0947C8.94915 20.625 8.80735 20.5788 8.68973 20.4931C8.57211 20.4073 8.48474 20.2864 8.4402 20.1479L7.68533 17.7994C7.36856 17.6445 7.06302 17.4676 6.77095 17.27L4.35783 17.7911C4.21547 17.8215 4.06713 17.8059 3.93419 17.7466C3.80125 17.6873 3.69057 17.5874 3.61808 17.4611L1.71233 14.1625C1.63956 14.0363 1.60873 13.8903 1.6243 13.7455C1.63987 13.6007 1.70103 13.4646 1.79895 13.3567L3.45583 11.5252C3.43183 11.1755 3.43183 10.8245 3.45583 10.4748L1.79895 8.64325C1.70103 8.53544 1.63987 8.39931 1.6243 8.2545C1.60873 8.10968 1.63956 7.96366 1.71233 7.8375L3.61808 4.5375C3.69077 4.41151 3.80154 4.31183 3.93446 4.25278C4.06739 4.19373 4.21562 4.17836 4.35783 4.20888L6.77095 4.73C7.06245 4.53338 7.3677 4.356 7.68533 4.20063L8.44158 1.85212C8.48597 1.714 8.57293 1.59346 8.69 1.50776C8.80706 1.42205 8.94824 1.37559 9.09333 1.375H12.9048H12.9062ZM12.403 2.75H9.59795L8.81695 5.17963L8.29033 5.43675C8.03144 5.56344 7.78152 5.70769 7.54233 5.8685L7.05558 6.1985L4.55858 5.6595L3.15608 8.0905L4.86795 9.98525L4.8267 10.5682C4.80695 10.8557 4.80695 11.1443 4.8267 11.4318L4.86795 12.0147L3.15333 13.9095L4.5572 16.3405L7.0542 15.8029L7.54095 16.1315C7.78015 16.2923 8.03007 16.4366 8.28895 16.5632L8.81558 16.8204L9.59795 19.25H12.4057L13.1895 16.819L13.7147 16.5632C13.9733 16.4369 14.2228 16.2926 14.4613 16.1315L14.9467 15.8029L17.4451 16.3405L18.8476 13.9095L17.1343 12.0147L17.1756 11.4318C17.1954 11.1438 17.1954 10.8548 17.1756 10.5669L17.1343 9.98388L18.849 8.0905L17.4451 5.6595L14.9467 6.19575L14.4613 5.8685C14.2228 5.70737 13.9733 5.5631 13.7147 5.43675L13.1895 5.181L12.4043 2.75H12.403ZM11.0005 6.875C12.0945 6.875 13.1437 7.3096 13.9173 8.08318C14.6909 8.85677 15.1255 9.90598 15.1255 11C15.1255 12.094 14.6909 13.1432 13.9173 13.9168C13.1437 14.6904 12.0945 15.125 11.0005 15.125C9.90644 15.125 8.85723 14.6904 8.08364 13.9168C7.31005 13.1432 6.87545 12.094 6.87545 11C6.87545 9.90598 7.31005 8.85677 8.08364 8.08318C8.85723 7.3096 9.90644 6.875 11.0005 6.875ZM11.0005 8.25C10.2711 8.25 9.57164 8.53973 9.05591 9.05546C8.54018 9.57118 8.25045 10.2707 8.25045 11C8.25045 11.7293 8.54018 12.4288 9.05591 12.9445C9.57164 13.4603 10.2711 13.75 11.0005 13.75C11.7298 13.75 12.4293 13.4603 12.945 12.9445C13.4607 12.4288 13.7505 11.7293 13.7505 11C13.7505 10.2707 13.4607 9.57118 12.945 9.05546C12.4293 8.53973 11.7298 8.25 11.0005 8.25Z"
/>
</svg>
);
@@ -0,0 +1,77 @@
import React from "react";
import type { Props } from "./types";
export const StartedStateIcon: React.FC<Props> = ({
width = "20",
height = "20",
className,
color = "#fbb040",
}) => (
<svg
width={width}
height={height}
className={className}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 83.36 83.36"
>
<g id="Layer_2" data-name="Layer 2">
<g id="Layer_1-2" data-name="Layer 1">
<path
className="cls-1"
fill="none"
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={3}
d="M20,7.19a39.74,39.74,0,0,1,43.43.54"
/>
<path
className="cls-1"
fill="none"
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={3}
d="M76.17,20a39.76,39.76,0,0,1-.53,43.43"
/>
<path
className="cls-1"
fill="none"
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={3}
d="M63.42,76.17A39.78,39.78,0,0,1,20,75.64"
/>
<path
className="cls-1"
fill="none"
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={3}
d="M7.19,63.42A39.75,39.75,0,0,1,7.73,20"
/>
<path
className="cls-2"
fill={color}
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={3}
d="M42.32,41.21q9.57-14.45,19.13-28.9a35.8,35.8,0,0,0-39.09,0Z"
/>
<path
className="cls-2"
fill={color}
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={3}
d="M42.32,41.7,61.45,70.6a35.75,35.75,0,0,1-39.09,0Z"
/>
</g>
</g>
</svg>
);
@@ -0,0 +1,29 @@
import {
BacklogStateIcon,
CancelledStateIcon,
CompletedStateIcon,
StartedStateIcon,
UnstartedStateIcon,
} from "components/icons";
export const getStateGroupIcon = (
stateGroup: "backlog" | "unstarted" | "started" | "completed" | "cancelled",
width = "20",
height = "20",
color?: string
) => {
switch (stateGroup) {
case "backlog":
return <BacklogStateIcon width={width} height={height} color={color} />;
case "unstarted":
return <UnstartedStateIcon width={width} height={height} color={color} />;
case "started":
return <StartedStateIcon width={width} height={height} color={color} />;
case "completed":
return <CompletedStateIcon width={width} height={height} color={color} />;
case "cancelled":
return <CancelledStateIcon width={width} height={height} color={color} />;
default:
return <></>;
}
};
@@ -0,0 +1,24 @@
import React from "react";
import type { Props } from "./types";
export const TickMarkIcon: React.FC<Props> = ({
width = "24",
height = "24",
color = "#858E96",
className,
}) => (
<svg
width={width}
height={height}
className={className}
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill={color}
d="M19.0653 7.06348C19.2051 7.52373 19.3125 8.00065 19.3875 8.49423C19.4625 8.98783 19.5 9.48976 19.5 10C19.5 11.3333 19.2544 12.5779 18.7634 13.7337C18.2724 14.8894 17.5977 15.8964 16.7394 16.7548C15.881 17.6131 14.874 18.2852 13.7182 18.7711C12.5625 19.257 11.323 19.5 9.99998 19.5C8.66664 19.5 7.42209 19.257 6.26633 18.7711C5.11058 18.2852 4.10353 17.6131 3.2452 16.7548C2.38687 15.8964 1.71475 14.8894 1.22885 13.7337C0.74295 12.5779 0.5 11.3333 0.5 10C0.5 8.67694 0.74295 7.43752 1.22885 6.28176C1.71475 5.12599 2.38687 4.11894 3.2452 3.26061C4.10353 2.40227 5.11058 1.7276 6.26633 1.23658C7.42209 0.745547 8.66664 0.500031 9.99998 0.500031C11.0577 0.500031 12.0529 0.656764 12.9856 0.970231C13.9182 1.2837 14.7852 1.7103 15.5865 2.25003C15.7186 2.34106 15.7939 2.46254 15.8125 2.61446C15.8311 2.76637 15.7865 2.90643 15.6788 3.03463C15.5775 3.15643 15.4477 3.22598 15.2894 3.24328C15.1311 3.2606 14.9827 3.22374 14.8442 3.13271C14.1506 2.67374 13.3965 2.30931 12.5817 2.03943C11.767 1.76956 10.9064 1.63463 9.99998 1.63463C7.64101 1.63463 5.65703 2.43911 4.04805 4.04808C2.43908 5.65706 1.6346 7.64104 1.6346 10C1.6346 12.359 2.43908 14.3429 4.04805 15.9519C5.65703 17.5609 7.64101 18.3654 9.99998 18.3654C12.3589 18.3654 14.3429 17.5609 15.9519 15.9519C17.5609 14.3429 18.3654 12.359 18.3654 10C18.3654 9.56411 18.3371 9.14487 18.2807 8.74231C18.2243 8.33974 18.1397 7.94038 18.0269 7.54423C17.9897 7.40833 17.9942 7.26603 18.0404 7.11733C18.0865 6.96861 18.1724 6.86221 18.298 6.79811C18.4429 6.70836 18.5919 6.6856 18.7451 6.72983C18.8983 6.77405 19.0051 6.88526 19.0653 7.06348ZM8.0192 13.7077L5.12308 10.7962C5.01153 10.6846 4.95736 10.5462 4.96058 10.3808C4.96378 10.2154 5.02628 10.0718 5.14808 9.95001C5.26601 9.83207 5.40607 9.77311 5.56825 9.77311C5.73042 9.77311 5.87561 9.83207 6.00383 9.95001L8.52498 12.4962L18.2384 2.78273C18.35 2.67118 18.4868 2.61445 18.649 2.61253C18.8112 2.61061 18.9564 2.67055 19.0846 2.79233C19.2128 2.92053 19.2769 3.06572 19.2769 3.22791C19.2769 3.39007 19.2128 3.53526 19.0846 3.66348L9.03075 13.7077C8.88715 13.8577 8.71599 13.9327 8.51728 13.9327C8.31856 13.9327 8.15253 13.8577 8.0192 13.7077Z"
/>
</svg>
);
@@ -0,0 +1,59 @@
import React from "react";
import type { Props } from "./types";
export const UnstartedStateIcon: React.FC<Props> = ({
width = "20",
height = "20",
className,
color = "#858e96",
}) => (
<svg
width={width}
height={height}
className={className}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 84.36 84.36"
>
<g id="Layer_2" data-name="Layer 2">
<g id="Layer_1-2" data-name="Layer 1">
<path
className="cls-1"
fill="none"
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={3}
d="M20.45,7.69a39.74,39.74,0,0,1,43.43.54"
/>
<path
className="cls-1"
fill="none"
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={3}
d="M76.67,20.45a39.76,39.76,0,0,1-.53,43.43"
/>
<path
className="cls-1"
fill="none"
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={3}
d="M63.92,76.67a39.78,39.78,0,0,1-43.44-.53"
/>
<path
className="cls-1"
fill="none"
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={3}
d="M7.69,63.92a39.75,39.75,0,0,1,.54-43.44"
/>
</g>
</g>
</svg>
);
@@ -96,7 +96,7 @@ export const AddComment: React.FC = () => {
setValue("comment_json", jsonValue);
setValue("comment_html", htmlValue);
}}
placeholder="Enter Your comment..."
// placeholder="Enter Your comment..."
/>
)}
/>
+44 -25
View File
@@ -1,11 +1,9 @@
import { FC, useCallback, useEffect, useMemo, useState } from "react";
import { FC, useCallback, useEffect, useState } from "react";
import dynamic from "next/dynamic";
// react-hook-form
import { useForm } from "react-hook-form";
// lodash
import debounce from "lodash.debounce";
// components
import { Loader, TextArea } from "components/ui";
const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor"), {
@@ -27,7 +25,7 @@ export interface IssueDescriptionFormValues {
export interface IssueDetailsProps {
issue: IIssue;
handleFormSubmit: (value: IssueDescriptionFormValues) => void;
handleFormSubmit: (value: IssueDescriptionFormValues) => Promise<void>;
userAuth: UserAuth;
}
@@ -36,6 +34,7 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
handleFormSubmit,
userAuth,
}) => {
const [isSubmitting, setIsSubmitting] = useState(false);
const [characterLimit, setCharacterLimit] = useState(false);
const {
@@ -53,10 +52,10 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
});
const handleDescriptionFormSubmit = useCallback(
(formData: Partial<IIssue>) => {
async (formData: Partial<IIssue>) => {
if (!formData.name || formData.name.length === 0 || formData.name.length > 255) return;
handleFormSubmit({
await handleFormSubmit({
name: formData.name ?? "",
description: formData.description ?? "",
description_html: formData.description_html ?? "<p></p>",
@@ -65,17 +64,19 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
[handleFormSubmit]
);
const debounceHandler = useMemo(
() => debounce(handleSubmit(handleDescriptionFormSubmit), 2000),
[handleSubmit, handleDescriptionFormSubmit]
);
useEffect(() => {
const alertUser = (e: BeforeUnloadEvent) => {
console.log("beforeunload");
e.preventDefault();
e.returnValue = "";
return "Are you sure you want to leave?";
};
useEffect(
() => () => {
debounceHandler.cancel();
},
[debounceHandler]
);
window.addEventListener("beforeunload", alertUser);
return () => {
window.removeEventListener("beforeunload", alertUser);
};
}, [isSubmitting]);
// reset form values
useEffect(() => {
@@ -95,19 +96,29 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
placeholder="Enter issue name"
value={watch("name")}
onFocus={() => setCharacterLimit(true)}
onBlur={() => setCharacterLimit(false)}
onBlur={() => {
setCharacterLimit(false);
setIsSubmitting(true);
handleSubmit(handleDescriptionFormSubmit)()
.then(() => {
setIsSubmitting(false);
})
.catch(() => {
setIsSubmitting(false);
});
}}
onChange={(e) => {
setValue("name", e.target.value);
debounceHandler();
}}
required={true}
className="block px-3 py-2 text-xl
w-full overflow-hidden resize-none min-h-10
rounded border-none bg-transparent ring-0 focus:ring-1 focus:ring-theme outline-none"
className="min-h-10 block w-full resize-none
overflow-hidden rounded border-none bg-transparent
px-3 py-2 text-xl outline-none ring-0 focus:ring-1 focus:ring-theme"
role="textbox"
/>
{characterLimit && (
<div className="absolute bottom-0 right-0 text-xs bg-white p-1 rounded pointer-events-none z-[2]">
<div className="pointer-events-none absolute bottom-0 right-0 z-[2] rounded bg-white p-1 text-xs">
<span
className={`${
watch("name").length === 0 || watch("name").length > 255 ? "text-red-500" : ""
@@ -123,13 +134,21 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
<RemirrorRichTextEditor
value={watch("description")}
placeholder="Describe the issue..."
onJSONChange={(json) => {
setValue("description", json);
debounceHandler();
onBlur={() => {
setIsSubmitting(true);
handleSubmit(handleDescriptionFormSubmit)()
.then(() => {
setIsSubmitting(false);
})
.catch(() => {
setIsSubmitting(false);
});
}}
onJSONChange={(json) => setValue("description", json)}
onHTMLChange={(html) => setValue("description_html", html)}
editable={!isNotAllowed}
/>
<div className="text-right text-sm text-gray-500">{isSubmitting && "Saving..."}</div>
</div>
);
};
+19 -33
View File
@@ -14,13 +14,14 @@ import {
IssuePrioritySelect,
IssueProjectSelect,
IssueStateSelect,
IssueDateSelect,
} from "components/issues/select";
import { CycleSelect as IssueCycleSelect } from "components/cycles/select";
import { CreateStateModal } from "components/states";
import { CreateUpdateCycleModal } from "components/cycles";
import { CreateLabelModal } from "components/labels";
// ui
import { Button, CustomDatePicker, CustomMenu, Input, Loader } from "components/ui";
import { Button, CustomMenu, Input, Loader } from "components/ui";
import { PrimaryButton } from "components/ui/button/primary-button";
// icons
import { XMarkIcon } from "@heroicons/react/24/outline";
// helpers
@@ -95,7 +96,6 @@ export const IssueForm: FC<IssueFormProps> = ({
setFocus,
} = useForm<IIssue>({
defaultValues,
mode: "all",
reValidateMode: "onChange",
});
@@ -157,7 +157,7 @@ export const IssueForm: FC<IssueFormProps> = ({
/>
)}
/>
<h3 className="text-lg font-medium leading-6 text-gray-900">
<h3 className="text-xl font-semibold leading-6 text-gray-900">
{status ? "Update" : "Create"} Issue
</h3>
</div>
@@ -190,11 +190,11 @@ export const IssueForm: FC<IssueFormProps> = ({
<div>
<Input
id="name"
label="Title"
name="name"
onChange={handleTitleChange}
className="resize-none"
placeholder="Enter title"
className="resize-none text-xl"
placeholder="Title"
mode="transparent"
autoComplete="off"
error={errors.name}
register={register}
@@ -220,7 +220,7 @@ export const IssueForm: FC<IssueFormProps> = ({
</span>
?
</a>
</Link>{" "}
</Link>
</p>
<button
type="button"
@@ -235,9 +235,6 @@ export const IssueForm: FC<IssueFormProps> = ({
)}
</div>
<div>
<label htmlFor={"description"} className="mb-2 text-gray-500">
Description
</label>
<Controller
name="description"
control={control}
@@ -246,7 +243,7 @@ export const IssueForm: FC<IssueFormProps> = ({
value={value}
onJSONChange={(jsonValue) => setValue("description", jsonValue)}
onHTMLChange={(htmlValue) => setValue("description_html", htmlValue)}
placeholder="Enter Your Text..."
placeholder="Description"
/>
)}
/>
@@ -266,16 +263,16 @@ export const IssueForm: FC<IssueFormProps> = ({
/>
<Controller
control={control}
name="cycle"
name="priority"
render={({ field: { value, onChange } }) => (
<IssueCycleSelect projectId={projectId} value={value} onChange={onChange} />
<IssuePrioritySelect value={value} onChange={onChange} />
)}
/>
<Controller
control={control}
name="priority"
name="assignees"
render={({ field: { value, onChange } }) => (
<IssuePrioritySelect value={value} onChange={onChange} />
<IssueAssigneeSelect projectId={projectId} value={value} onChange={onChange} />
)}
/>
<Controller
@@ -295,21 +292,10 @@ export const IssueForm: FC<IssueFormProps> = ({
control={control}
name="target_date"
render={({ field: { value, onChange } }) => (
<CustomDatePicker
value={value}
onChange={onChange}
className="max-w-[7rem]"
/>
<IssueDateSelect value={value} onChange={onChange} />
)}
/>
</div>
<Controller
control={control}
name="assignees"
render={({ field: { value, onChange } }) => (
<IssueAssigneeSelect projectId={projectId} value={value} onChange={onChange} />
)}
/>
<IssueParentSelect
control={control}
isOpen={parentIssueListModalOpen}
@@ -345,7 +331,7 @@ export const IssueForm: FC<IssueFormProps> = ({
</div>
</div>
</div>
<div className="mt-5 flex items-center justify-between gap-2">
<div className="-mx-5 mt-5 flex items-center justify-between gap-2 border-t px-5 pt-5">
<div
className="flex cursor-pointer items-center gap-1"
onClick={() => setCreateMore((prevData) => !prevData)}
@@ -372,15 +358,15 @@ export const IssueForm: FC<IssueFormProps> = ({
<Button type="button" theme="secondary" onClick={handleClose}>
Discard
</Button>
<Button type="submit" disabled={isSubmitting}>
<PrimaryButton type="submit" size="sm" loading={isSubmitting}>
{status
? isSubmitting
? "Updating Issue..."
: "Update Issue"
: isSubmitting
? "Creating Issue..."
: "Create Issue"}
</Button>
? "Adding Issue..."
: "Add Issue"}
</PrimaryButton>
</div>
</div>
</form>
+2 -2
View File
@@ -86,7 +86,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, []);
}, [handleClose]);
const addIssueToCycle = async (issueId: string, cycleId: string) => {
if (!workspaceSlug || !projectId) return;
@@ -231,7 +231,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
</Transition.Child>
<div className="fixed inset-0 z-10 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center sm:p-0">
<div className="mt-10 flex min-h-full items-start justify-center p-4 text-center sm:p-0 md:mt-20">
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
@@ -105,7 +105,7 @@ export const MyIssuesListItem: React.FC<Props> = ({
</Tooltip>
)}
<Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
<span className="w-auto max-w-lg text-ellipsis overflow-hidden whitespace-nowrap">
<span className="w-auto max-w-lg overflow-hidden text-ellipsis whitespace-nowrap">
{issue.name}
</span>
</Tooltip>
@@ -135,7 +135,7 @@ export const MyIssuesListItem: React.FC<Props> = ({
/>
)}
{properties.sub_issue_count && (
<div className="flex flex-shrink-0 items-center gap-1 rounded border px-2 py-1 text-xs shadow-sm duration-300 hover:bg-gray-100 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500">
<div className="flex flex-shrink-0 items-center gap-1 rounded-md border px-3 py-1.5 text-xs shadow-sm">
{issue?.sub_issues_count} {issue?.sub_issues_count === 1 ? "sub-issue" : "sub-issues"}
</div>
)}
+45 -92
View File
@@ -1,122 +1,75 @@
import { useState, FC, Fragment } from "react";
import { useRouter } from "next/router";
import useSWR from "swr";
// headless ui
import { Transition, Combobox } from "@headlessui/react";
// services
import projectServices from "services/project.service";
// ui
import { AssigneesList, Avatar } from "components/ui";
// fetch keys
import { AssigneesList, Avatar, CustomSearchSelect } from "components/ui";
// icons
import { UserGroupIcon } from "@heroicons/react/24/outline";
// fetch-keys
import { PROJECT_MEMBERS } from "constants/fetch-keys";
export type IssueAssigneeSelectProps = {
export type Props = {
projectId: string;
value: string[];
onChange: (value: string[]) => void;
};
export const IssueAssigneeSelect: FC<IssueAssigneeSelectProps> = ({
projectId,
value = [],
onChange,
}) => {
// states
const [query, setQuery] = useState("");
export const IssueAssigneeSelect: React.FC<Props> = ({ projectId, value = [], onChange }) => {
const router = useRouter();
const { workspaceSlug } = router.query;
// fetching project members
const { data: people } = useSWR(
const { data: members } = useSWR(
workspaceSlug && projectId ? PROJECT_MEMBERS(projectId as string) : null,
workspaceSlug && projectId
? () => projectServices.projectMembers(workspaceSlug as string, projectId as string)
: null
);
const options = people?.map((person) => ({
value: person.member.id,
display:
person.member.first_name && person.member.first_name !== ""
? person.member.first_name
: person.member.email,
}));
const filteredOptions =
query === ""
? options
: options?.filter((option) => option.display.toLowerCase().includes(query.toLowerCase()));
const options =
members?.map((member) => ({
value: member.member.id,
query:
(member.member.first_name && member.member.first_name !== ""
? member.member.first_name
: member.member.email) +
" " +
member.member.last_name ?? "",
content: (
<div className="flex items-center gap-2">
<Avatar user={member.member} />
{member.member.first_name && member.member.first_name !== ""
? member.member.first_name
: member.member.email}
</div>
),
})) ?? [];
return (
<Combobox
as="div"
<CustomSearchSelect
value={value}
onChange={(val) => onChange(val)}
className="relative flex-shrink-0"
multiple
>
{({ open }: any) => (
<>
<Combobox.Button className="flex items-center cursor-pointer gap-1 rounded-md">
<div className="flex items-center gap-1 text-xs">
{value && Array.isArray(value) ? <AssigneesList userIds={value} length={10} /> : null}
onChange={onChange}
options={options}
label={
<div className="flex items-center gap-2 text-gray-500">
{value && value.length > 0 && Array.isArray(value) ? (
<div className="flex items-center justify-center gap-2">
<AssigneesList userIds={value} length={3} showLength={false} />
<span className="text-gray-500">{value.length} Assignees</span>
</div>
</Combobox.Button>
<Transition
show={open}
as={Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Combobox.Options
className={`absolute z-10 mt-1 max-h-32 min-w-[8rem] overflow-auto rounded-md bg-white py-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none text-xs`}
>
<Combobox.Input
className="w-full border-b bg-transparent p-2 text-xs focus:outline-none"
onChange={(event) => setQuery(event.target.value)}
placeholder="Search"
displayValue={(assigned: any) => assigned?.name}
/>
<div className="py-1">
{filteredOptions ? (
filteredOptions.length > 0 ? (
filteredOptions.map((option) => (
<Combobox.Option
key={option.value}
className={({ active, selected }) =>
`${active ? "bg-indigo-50" : ""} ${
selected ? "bg-indigo-50 font-medium" : ""
} flex cursor-pointer select-none items-center gap-2 truncate px-2 py-1 text-gray-900`
}
value={option.value}
>
{people && (
<>
<Avatar
user={people?.find((p) => p.member.id === option.value)?.member}
/>
{option.display}
</>
)}
</Combobox.Option>
))
) : (
<p className="text-xs text-gray-500 px-2">No assignees found</p>
)
) : (
<p className="text-xs text-gray-500 px-2">Loading...</p>
)}
</div>
</Combobox.Options>
</Transition>
</>
)}
</Combobox>
) : (
<div className="flex items-center justify-center gap-2">
<UserGroupIcon className="h-4 w-4 text-gray-500" />
<span className="text-gray-500">Assignee</span>
</div>
)}
</div>
}
multiple
noChevron
/>
);
};
@@ -0,0 +1,70 @@
import React from "react";
import { Popover, Transition } from "@headlessui/react";
import { CalendarDaysIcon, XMarkIcon } from "@heroicons/react/24/outline";
// react-datepicker
import DatePicker from "react-datepicker";
// import "react-datepicker/dist/react-datepicker.css";
import { renderDateFormat } from "helpers/date-time.helper";
type Props = {
value: string | null;
onChange: (val: string | null) => void;
};
export const IssueDateSelect: React.FC<Props> = ({ value, onChange }) => (
<Popover className="relative flex items-center justify-center rounded-lg">
{({ open }) => (
<>
<Popover.Button
className={({ open }) =>
`flex cursor-pointer items-center rounded-md border text-xs shadow-sm duration-200
${
open
? "border-theme bg-theme/5 outline-none ring-1 ring-theme "
: "hover:bg-theme/5 "
}`
}
>
<span className="flex items-center justify-center gap-2 px-3 py-1.5 text-xs">
{value ? (
<>
<span className="text-gray-600">{value}</span>
<button onClick={() => onChange(null)}>
<XMarkIcon className="h-3 w-3 text-gray-600" />
</button>
</>
) : (
<>
<CalendarDaysIcon className="h-4 w-4 flex-shrink-0 " />
<span className="text-gray-500">Due Date</span>
</>
)}
</span>
</Popover.Button>
<Transition
as={React.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 top-10 -left-10 z-20 transform overflow-hidden">
<DatePicker
selected={value ? new Date(value) : null}
onChange={(val) => {
if (!val) onChange("");
else onChange(renderDateFormat(val));
}}
dateFormat="dd-MM-yyyy"
inline
/>
</Popover.Panel>
</Transition>
</>
)}
</Popover>
);
@@ -4,3 +4,4 @@ export * from "./parent";
export * from "./priority";
export * from "./project";
export * from "./state";
export * from "./date";
+109 -51
View File
@@ -7,13 +7,20 @@ import useSWR from "swr";
// headless ui
import { Combobox, Transition } from "@headlessui/react";
// icons
import { PlusIcon, RectangleGroupIcon, TagIcon } from "@heroicons/react/24/outline";
import {
CheckIcon,
MagnifyingGlassIcon,
PlusIcon,
RectangleGroupIcon,
TagIcon,
} from "@heroicons/react/24/outline";
// services
import issuesServices from "services/issues.service";
// types
import type { IIssueLabels } from "types";
// fetch-keys
import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys";
import { IssueLabelsList } from "components/ui";
type Props = {
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
@@ -52,36 +59,57 @@ export const IssueLabelSelect: React.FC<Props> = ({ setIsOpen, value, onChange,
>
{({ open }: any) => (
<>
<Combobox.Label className="sr-only">Labels</Combobox.Label>
<Combobox.Button
className={`flex cursor-pointer items-center gap-1 rounded-md border px-2 py-1 text-xs shadow-sm duration-300 hover:bg-gray-100 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500`}
className={({ open }) =>
`flex cursor-pointer items-center rounded-md border text-xs shadow-sm duration-200
${
open
? "border-theme bg-theme/5 outline-none ring-1 ring-theme "
: "hover:bg-theme/5 "
}`
}
>
<TagIcon className="h-3 w-3 text-gray-500" />
<span className={`flex items-center gap-2 ${!value ? "" : "text-gray-900"}`}>
{Array.isArray(value)
? value.map((v) => issueLabels?.find((l) => l.id === v)?.name).join(", ") ||
"Labels"
: issueLabels?.find((l) => l.id === value)?.name || "Labels"}
</span>
{value && value.length > 0 ? (
<span className="flex items-center justify-center gap-2 px-3 py-1 text-xs">
<IssueLabelsList
labels={value.map((v) => issueLabels?.find((l) => l.id === v)?.color) ?? []}
length={3}
showLength
/>
<span className=" text-gray-600">{value.length} Labels</span>
</span>
) : (
<span className="flex items-center justify-center gap-2 px-3 py-1.5 text-xs">
<TagIcon className="h-3 w-3 text-gray-500" />
<span className=" text-gray-500">Label</span>
</span>
)}
</Combobox.Button>
<Transition
show={open}
as={React.Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
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"
>
<Combobox.Options
className={`absolute z-10 mt-1 max-h-32 min-w-[8rem] overflow-auto rounded-md bg-white py-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none text-xs`}
className={`absolute z-10 mt-1 max-h-52 min-w-[8rem] overflow-auto rounded-md border-none
bg-white px-2 py-2 text-xs shadow-md focus:outline-none`}
>
<Combobox.Input
className="w-full border-b bg-transparent p-2 text-xs focus:outline-none"
onChange={(event) => setQuery(event.target.value)}
placeholder="Search"
displayValue={(assigned: any) => assigned?.name}
/>
<div className="py-1">
<div className="flex w-full items-center justify-start rounded-sm border-[0.6px] bg-gray-100 px-2">
<MagnifyingGlassIcon className="h-3 w-3 text-gray-500" />
<Combobox.Input
className="w-full bg-transparent py-1 px-2 text-xs text-gray-500 focus:outline-none"
onChange={(event) => setQuery(event.target.value)}
placeholder="Search for label..."
displayValue={(assigned: any) => assigned?.name}
/>
</div>
<div className="py-1.5">
{issueLabels && filteredOptions ? (
filteredOptions.length > 0 ? (
filteredOptions.map((label) => {
@@ -92,47 +120,75 @@ export const IssueLabelSelect: React.FC<Props> = ({ setIsOpen, value, onChange,
return (
<Combobox.Option
key={label.id}
className={({ active, selected }) =>
`${active ? "bg-indigo-50" : ""} ${
selected ? "bg-indigo-50 font-medium" : ""
} flex cursor-pointer select-none items-center gap-2 truncate p-2 text-gray-900`
className={({ active }) =>
`${
active ? "bg-gray-200" : ""
} group flex min-w-[14rem] cursor-pointer select-none items-center gap-2 truncate rounded px-1 py-1.5 text-gray-600`
}
value={label.id}
>
<span
className="h-1.5 w-1.5 flex-shrink-0 rounded-full"
style={{
backgroundColor:
label.color && label.color !== "" ? label.color : "#000",
}}
/>
{label.name}
{({ selected }) => (
<div className="flex w-full justify-between gap-2 rounded">
<div className="flex items-center justify-start gap-2">
<span
className="h-3 w-3 flex-shrink-0 rounded-full"
style={{
backgroundColor:
label.color && label.color !== ""
? label.color
: "#000",
}}
/>
<span>{label.name}</span>
</div>
<div className="flex items-center justify-center rounded p-1">
<CheckIcon
className={`h-3 w-3 ${
selected ? "opacity-100" : "opacity-0"
}`}
/>
</div>
</div>
)}
</Combobox.Option>
);
} else
return (
<div className="bg-gray-50 border-y border-gray-400">
<div className="flex select-none font-medium items-center gap-2 truncate p-2 text-gray-900">
<div className="border-y border-gray-400 bg-gray-50">
<div className="flex select-none items-center gap-2 truncate p-2 font-medium text-gray-900">
<RectangleGroupIcon className="h-3 w-3" /> {label.name}
</div>
<div>
{children.map((child) => (
<Combobox.Option
key={child.id}
className={({ active, selected }) =>
`${active ? "bg-indigo-50" : ""} ${
selected ? "bg-indigo-50 font-medium" : ""
} flex cursor-pointer select-none items-center gap-2 truncate p-2 text-gray-900`
className={({ active }) =>
`${
active ? "bg-gray-200" : ""
} group flex min-w-[14rem] cursor-pointer select-none items-center gap-2 truncate rounded px-1 py-1.5 text-gray-600`
}
value={child.id}
>
<span
className="h-2 w-2 flex-shrink-0 rounded-full"
style={{
backgroundColor: child?.color ?? "black",
}}
/>
{child.name}
{({ selected }) => (
<div className="flex w-full justify-between gap-2 rounded">
<div className="flex items-center justify-start gap-2">
<span
className="h-3 w-3 flex-shrink-0 rounded-full"
style={{
backgroundColor: child?.color ?? "black",
}}
/>
<span>{child.name}</span>
</div>
<div className="flex items-center justify-center rounded p-1">
<CheckIcon
className={`h-3 w-3 ${
selected ? "opacity-100" : "opacity-0"
}`}
/>
</div>
</div>
)}
</Combobox.Option>
))}
</div>
@@ -140,18 +196,20 @@ export const IssueLabelSelect: React.FC<Props> = ({ setIsOpen, value, onChange,
);
})
) : (
<p className="text-xs text-gray-500 px-2">No labels found</p>
<p className="px-2 text-xs text-gray-500">No labels found</p>
)
) : (
<p className="text-xs text-gray-500 px-2">Loading...</p>
<p className="px-2 text-xs text-gray-500">Loading...</p>
)}
<button
type="button"
className="flex select-none w-full items-center gap-2 p-2 text-gray-400 outline-none hover:bg-indigo-50 hover:text-gray-900"
className="flex w-full select-none items-center rounded py-2 px-1 hover:bg-gray-200"
onClick={() => setIsOpen(true)}
>
<PlusIcon className="h-3 w-3 text-gray-400" aria-hidden="true" />
<span className="text-xs whitespace-nowrap">Create label</span>
<span className="flex items-center justify-start gap-1">
<PlusIcon className="h-4 w-4 text-gray-600" aria-hidden="true" />
<span className="text-gray-600">Create New Label</span>
</span>
</button>
</div>
</Combobox.Options>
+28 -41
View File
@@ -1,7 +1,7 @@
import React from "react";
// headless ui
import { Listbox, Transition } from "@headlessui/react";
// ui
import { CustomSelect } from "components/ui";
// icons
import { getPriorityIcon } from "components/icons/priority-icon";
// constants
@@ -13,43 +13,30 @@ type Props = {
};
export const IssuePrioritySelect: React.FC<Props> = ({ value, onChange }) => (
<Listbox as="div" className="relative" value={value} onChange={onChange}>
{({ open }) => (
<>
<Listbox.Button className="flex cursor-pointer items-center gap-1 rounded-md border px-2 py-1 text-xs shadow-sm duration-300 hover:bg-gray-100 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500">
<span className="text-gray-500 grid place-items-center">{getPriorityIcon(value)}</span>
<div className="flex items-center gap-2 capitalize">{value ?? "Priority"}</div>
</Listbox.Button>
<Transition
show={open}
as={React.Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options className="absolute mt-1 max-h-32 min-w-[8rem] overflow-y-auto whitespace-nowrap bg-white shadow-lg text-xs z-10 rounded-md py-1 ring-1 ring-black ring-opacity-5 focus:outline-none">
<div className="py-1">
{PRIORITIES.map((priority) => (
<Listbox.Option
key={priority}
className={({ selected, active }) =>
`${selected ? "bg-indigo-50 font-medium" : ""} ${
active ? "bg-indigo-50" : ""
} relative cursor-pointer select-none p-2 text-gray-900`
}
value={priority}
>
<span className="flex items-center gap-2 capitalize">
{getPriorityIcon(priority)}
{priority ?? "None"}
</span>
</Listbox.Option>
))}
</div>
</Listbox.Options>
</Transition>
</>
)}
</Listbox>
<CustomSelect
value={value}
label={
<div className="flex items-center justify-center gap-2 text-xs">
<span className="flex items-center">
{getPriorityIcon(value, `${value ? "text-xs" : "text-xs text-gray-500"}`)}
</span>
<span className={`${value ? "text-gray-600" : "text-gray-500"} capitalize`}>
{value ?? "Priority"}
</span>
</div>
}
onChange={onChange}
noChevron
>
{PRIORITIES.map((priority) => (
<CustomSelect.Option key={priority} value={priority}>
<div className="flex w-full justify-between gap-2 rounded">
<div className="flex items-center justify-start gap-2">
<span>{getPriorityIcon(priority)}</span>
<span className="capitalize">{priority ?? "None"}</span>
</div>
</div>
</CustomSelect.Option>
))}
</CustomSelect>
);
+33 -71
View File
@@ -1,11 +1,9 @@
import { FC, Fragment } from "react";
import { useRouter } from "next/router";
import useSWR from "swr";
// headless ui
import { Listbox, Transition } from "@headlessui/react";
// ui
import { CustomSelect } from "components/ui";
// icons
import { ClipboardDocumentListIcon } from "@heroicons/react/24/outline";
// services
@@ -19,7 +17,7 @@ export interface IssueProjectSelectProps {
setActiveProject: React.Dispatch<React.SetStateAction<string | null>>;
}
export const IssueProjectSelect: FC<IssueProjectSelectProps> = ({
export const IssueProjectSelect: React.FC<IssueProjectSelectProps> = ({
value,
onChange,
setActiveProject,
@@ -34,71 +32,35 @@ export const IssueProjectSelect: FC<IssueProjectSelectProps> = ({
);
return (
<>
<Listbox
value={value}
onChange={(val) => {
onChange(val);
setActiveProject(val);
}}
>
{({ open }) => (
<>
<div className="relative">
<Listbox.Button className="relative flex cursor-pointer items-center gap-1 rounded-md border bg-white px-2 py-1 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm">
<ClipboardDocumentListIcon className="h-3 w-3" />
<span className="block truncate">
{projects?.find((i) => i.id === value)?.identifier ?? "Project"}
</span>
</Listbox.Button>
<Transition
show={open}
as={Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options className="absolute z-10 mt-1 max-h-28 overflow-auto rounded-md bg-white py-1 text-xs shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
<div className="py-1">
{projects ? (
projects.length > 0 ? (
projects.map((project) => (
<Listbox.Option
key={project.id}
className={({ active, selected }) =>
`${active ? "bg-indigo-50" : ""} ${
selected ? "bg-indigo-50 font-medium" : ""
} cursor-pointer select-none p-2 text-gray-900`
}
value={project.id}
>
{({ selected }) => (
<>
<span
className={`${
selected ? "font-medium" : "font-normal"
} block truncate`}
>
{project.name}
</span>
</>
)}
</Listbox.Option>
))
) : (
<p className="text-gray-400">No projects found!</p>
)
) : (
<div className="text-sm text-gray-500 px-2">Loading...</div>
)}
</div>
</Listbox.Options>
</Transition>
</div>
</>
)}
</Listbox>
</>
<CustomSelect
value={value}
label={
<>
<ClipboardDocumentListIcon className="h-3 w-3" />
<span className="block truncate">
{projects?.find((i) => i.id === value)?.identifier ?? "Project"}
</span>
</>
}
onChange={(val: string) => {
onChange(val);
setActiveProject(val);
}}
noChevron
>
{projects ? (
projects.length > 0 ? (
projects.map((project) => (
<CustomSelect.Option key={project.id} value={project.id}>
<>{project.name}</>
</CustomSelect.Option>
))
) : (
<p className="text-gray-400">No projects found!</p>
)
) : (
<div className="px-2 text-sm text-gray-500">Loading...</div>
)}
</CustomSelect>
);
};
+36 -99
View File
@@ -1,4 +1,4 @@
import React, { useState } from "react";
import React from "react";
import { useRouter } from "next/router";
@@ -6,10 +6,11 @@ import useSWR from "swr";
// services
import stateService from "services/state.service";
// headless ui
import { Squares2X2Icon, PlusIcon } from "@heroicons/react/24/outline";
// ui
import { CustomSearchSelect } from "components/ui";
// icons
import { Combobox, Transition } from "@headlessui/react";
import { PlusIcon, Squares2X2Icon } from "@heroicons/react/24/outline";
import { getStateGroupIcon } from "components/icons";
// helpers
import { getStatesList } from "helpers/state.helper";
// fetch keys
@@ -24,8 +25,6 @@ type Props = {
export const IssueStateSelect: React.FC<Props> = ({ setIsOpen, value, onChange, projectId }) => {
// states
const [query, setQuery] = useState("");
const router = useRouter();
const { workspaceSlug } = router.query;
@@ -39,103 +38,41 @@ export const IssueStateSelect: React.FC<Props> = ({ setIsOpen, value, onChange,
const options = states?.map((state) => ({
value: state.id,
display: state.name,
color: state.color,
query: state.name,
content: (
<div className="flex items-center gap-2">
{getStateGroupIcon(state.group, "16", "16", state.color)}
{state.name}
</div>
),
}));
const filteredOptions =
query === ""
? options
: options?.filter((option) => option.display.toLowerCase().includes(query.toLowerCase()));
const selectedOption = states?.find((s) => s.id === value);
return (
<Combobox
as="div"
<CustomSearchSelect
value={value}
onChange={(val: any) => onChange(val)}
className="relative flex-shrink-0"
>
{({ open }: any) => (
<>
<Combobox.Label className="sr-only">State</Combobox.Label>
<Combobox.Button
className={`flex cursor-pointer items-center gap-1 rounded-md border px-2 py-1 text-xs shadow-sm duration-300 hover:bg-gray-100 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500`}
>
<Squares2X2Icon className="h-3 w-3 text-gray-500" />
<span className={`flex items-center gap-2 ${!value ? "" : "text-gray-900"}`}>
{value && value !== "" ? (
<span
className="h-1.5 w-1.5 flex-shrink-0 rounded-full"
style={{
backgroundColor: options?.find((option) => option.value === value)?.color,
}}
/>
) : null}
{options?.find((option) => option.value === value)?.display || "State"}
</span>
</Combobox.Button>
<Transition
show={open}
as={React.Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Combobox.Options
className={`absolute z-10 mt-1 max-h-32 min-w-[8rem] overflow-auto rounded-md bg-white py-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none text-xs`}
>
<Combobox.Input
className="w-full border-b bg-transparent p-2 text-xs focus:outline-none"
onChange={(event) => setQuery(event.target.value)}
placeholder="Search"
displayValue={(assigned: any) => assigned?.name}
/>
<div className="py-1">
{filteredOptions ? (
filteredOptions.length > 0 ? (
filteredOptions.map((option) => (
<Combobox.Option
key={option.value}
className={({ active, selected }) =>
`${active ? "bg-indigo-50" : ""} ${
selected ? "bg-indigo-50 font-medium" : ""
} flex cursor-pointer select-none items-center gap-2 truncate p-2 text-gray-900`
}
value={option.value}
>
{states && (
<>
<span
className="h-1.5 w-1.5 flex-shrink-0 rounded-full"
style={{
backgroundColor: option.color,
}}
/>
{option.display}
</>
)}
</Combobox.Option>
))
) : (
<p className="text-xs text-gray-500 px-2">No states found</p>
)
) : (
<p className="text-xs text-gray-500 px-2">Loading...</p>
)}
<button
type="button"
className="flex select-none w-full items-center gap-2 p-2 text-gray-400 hover:bg-indigo-50 hover:text-gray-900"
onClick={() => setIsOpen(true)}
>
<PlusIcon className="h-3 w-3 text-gray-400" aria-hidden="true" />
<span className="text-xs whitespace-nowrap">Create state</span>
</button>
</div>
</Combobox.Options>
</Transition>
</>
)}
</Combobox>
onChange={onChange}
options={options}
label={
<div className="flex items-center gap-2 text-gray-500">
<Squares2X2Icon className="h-4 w-4" />
{selectedOption &&
getStateGroupIcon(selectedOption.group, "16", "16", selectedOption.color)}
{selectedOption?.name ?? "State"}
</div>
}
footerOption={
<button
type="button"
className="flex w-full select-none items-center gap-2 rounded px-1 py-1.5 text-xs text-gray-500 hover:bg-hover-gray"
onClick={() => setIsOpen(true)}
>
<PlusIcon className="h-4 w-4" aria-hidden="true" />
Create New State
</button>
}
noChevron
/>
);
};
@@ -1,41 +1,57 @@
import React from "react";
import Image from "next/image";
import { useRouter } from "next/router";
import useSWR from "swr";
// react-hook-form
import { Control, Controller } from "react-hook-form";
// headless ui
import { Listbox, Transition } from "@headlessui/react";
// services
import { UserGroupIcon } from "@heroicons/react/24/outline";
import workspaceService from "services/workspace.service";
// hooks
import projectService from "services/project.service";
// ui
import { AssigneesList } from "components/ui/avatar";
import { Spinner } from "components/ui";
import { CustomSearchSelect } from "components/ui";
import { AssigneesList, Avatar } from "components/ui/avatar";
// icons
import { UserGroupIcon } from "@heroicons/react/24/outline";
// types
import { IIssue, UserAuth } from "types";
// constants
import { WORKSPACE_MEMBERS } from "constants/fetch-keys";
import { UserAuth } from "types";
// fetch-keys
import { PROJECT_MEMBERS } from "constants/fetch-keys";
type Props = {
control: Control<IIssue, any>;
submitChanges: (formData: Partial<IIssue>) => void;
value: string[];
onChange: (val: string[]) => void;
userAuth: UserAuth;
};
export const SidebarAssigneeSelect: React.FC<Props> = ({ control, submitChanges, userAuth }) => {
export const SidebarAssigneeSelect: React.FC<Props> = ({ value, onChange, userAuth }) => {
const router = useRouter();
const { workspaceSlug } = router.query;
const { workspaceSlug, projectId } = router.query;
const { data: people } = useSWR(
workspaceSlug ? WORKSPACE_MEMBERS(workspaceSlug as string) : null,
workspaceSlug ? () => workspaceService.workspaceMembers(workspaceSlug as string) : null
const { data: members } = useSWR(
workspaceSlug && projectId ? PROJECT_MEMBERS(projectId as string) : null,
workspaceSlug && projectId
? () => projectService.projectMembers(workspaceSlug as string, projectId as string)
: null
);
const options =
members?.map((member) => ({
value: member.member.id,
query:
(member.member.first_name && member.member.first_name !== ""
? member.member.first_name
: member.member.email) +
" " +
member.member.last_name ?? "",
content: (
<div className="flex items-center gap-2">
<Avatar user={member.member} />
{member.member.first_name && member.member.first_name !== ""
? member.member.first_name
: member.member.email}
</div>
),
})) ?? [];
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
return (
@@ -45,93 +61,25 @@ export const SidebarAssigneeSelect: React.FC<Props> = ({ control, submitChanges,
<p>Assignees</p>
</div>
<div className="sm:basis-1/2">
<Controller
control={control}
name="assignees_list"
render={({ field: { value } }) => (
<Listbox
as="div"
value={value}
multiple={true}
onChange={(value: any) => {
submitChanges({ assignees_list: value });
}}
className="flex-shrink-0"
disabled={isNotAllowed}
>
{({ open }) => (
<div className="relative">
<Listbox.Button
className={`flex w-full ${
isNotAllowed ? "cursor-not-allowed" : "cursor-pointer"
} items-center gap-1 text-xs`}
>
<div className="flex items-center gap-1 text-xs">
{value && Array.isArray(value) ? (
<AssigneesList userIds={value} length={10} />
) : null}
</div>
</Listbox.Button>
<Transition
show={open}
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"
>
<Listbox.Options className="absolute left-0 z-10 mt-1 max-h-48 w-full overflow-auto rounded-md bg-white py-1 text-xs shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
<div className="py-1">
{people ? (
people.length > 0 ? (
people.map((option) => (
<Listbox.Option
key={option.member.id}
className={({ active, selected }) =>
`${active || selected ? "bg-indigo-50" : ""} ${
selected ? "font-medium" : ""
} flex cursor-pointer select-none items-center gap-2 truncate p-2 text-gray-900`
}
value={option.member.id}
>
{option.member.avatar && option.member.avatar !== "" ? (
<div className="relative h-4 w-4">
<Image
src={option.member.avatar}
alt="avatar"
className="rounded-full"
layout="fill"
objectFit="cover"
/>
</div>
) : (
<div className="grid h-4 w-4 flex-shrink-0 place-items-center rounded-full bg-gray-700 capitalize text-white">
{option.member.first_name && option.member.first_name !== ""
? option.member.first_name.charAt(0)
: option.member.email.charAt(0)}
</div>
)}
{option.member.first_name && option.member.first_name !== ""
? option.member.first_name
: option.member.email}
</Listbox.Option>
))
) : (
<div className="text-center">No assignees found</div>
)
) : (
<Spinner />
)}
</div>
</Listbox.Options>
</Transition>
<CustomSearchSelect
value={value}
label={
<div className="flex items-center gap-2 text-gray-500">
{value && value.length > 0 && Array.isArray(value) ? (
<div className="flex items-center justify-center gap-2">
<AssigneesList userIds={value} length={3} showLength={false} />
<span className="text-gray-500">{value.length} Assignees</span>
</div>
) : (
"No assignees"
)}
</Listbox>
)}
</div>
}
options={options}
onChange={onChange}
position="right"
multiple
disabled={isNotAllowed}
/>
</div>
</div>
@@ -65,26 +65,22 @@ export const SidebarCycleSelect: React.FC<Props> = ({
<div className="space-y-1 sm:basis-1/2">
<CustomSelect
label={
<Tooltip
position="top-right"
tooltipHeading="Cycle"
tooltipContent={issueCycle ? issueCycle.cycle_detail.name : "None"}
<span
className={`w-full max-w-[125px] truncate text-left sm:block ${
issueCycle ? "" : "text-gray-900"
}`}
>
<span
className={` w-full max-w-[125px] truncate text-left sm:block ${
issueCycle ? "" : "text-gray-900"
}`}
>
{issueCycle ? issueCycle.cycle_detail.name : "None"}
</span>
</Tooltip>
{issueCycle ? issueCycle.cycle_detail.name : "None"}
</span>
}
value={issueCycle?.cycle_detail.id}
onChange={(value: any) => {
value === null
!value
? removeIssueFromCycle(issueCycle?.id ?? "", issueCycle?.cycle ?? "")
: handleCycleChange(cycles?.find((c) => c.id === value) as ICycle);
}}
width="w-full"
position="right"
disabled={isNotAllowed}
>
{cycles ? (
@@ -97,11 +93,7 @@ export const SidebarCycleSelect: React.FC<Props> = ({
</Tooltip>
</CustomSelect.Option>
))}
<CustomSelect.Option value={null} className="capitalize">
<Tooltip position="left-bottom" tooltipContent="None">
<span className="w-full max-w-[125px] truncate">None</span>
</Tooltip>
</CustomSelect.Option>
<CustomSelect.Option value={null}>None</CustomSelect.Option>
</>
) : (
<div className="text-center">No cycles found</div>
@@ -64,26 +64,22 @@ export const SidebarModuleSelect: React.FC<Props> = ({
<div className="space-y-1 sm:basis-1/2">
<CustomSelect
label={
<Tooltip
position="top-right"
tooltipHeading="Module"
tooltipContent={modules?.find((m) => m.id === issueModule?.module)?.name ?? "None"}
<span
className={`w-full max-w-[125px] truncate text-left sm:block ${
issueModule ? "" : "text-gray-900"
}`}
>
<span
className={`w-full max-w-[125px] truncate text-left sm:block ${
issueModule ? "" : "text-gray-900"
}`}
>
{modules?.find((m) => m.id === issueModule?.module)?.name ?? "None"}
</span>
</Tooltip>
{modules?.find((m) => m.id === issueModule?.module)?.name ?? "None"}
</span>
}
value={issueModule?.module_detail?.id}
onChange={(value: any) => {
value === null
!value
? removeIssueFromModule(issueModule?.id ?? "", issueModule?.module ?? "")
: handleModuleChange(modules?.find((m) => m.id === value) as IModule);
}}
width="w-full"
position="right"
disabled={isNotAllowed}
>
{modules ? (
@@ -96,11 +92,7 @@ export const SidebarModuleSelect: React.FC<Props> = ({
</Tooltip>
</CustomSelect.Option>
))}
<CustomSelect.Option value={null} className="capitalize">
<Tooltip position="left-bottom" tooltipContent="None">
<span className="w-full max-w-[125px] truncate"> None </span>
</Tooltip>
</CustomSelect.Option>
<CustomSelect.Option value={null}>None</CustomSelect.Option>
</>
) : (
<div className="text-center">No modules found</div>
@@ -1,24 +1,22 @@
import React from "react";
// react-hook-form
import { Control, Controller } from "react-hook-form";
// ui
import { CustomSelect } from "components/ui";
// icons
import { ChartBarIcon } from "@heroicons/react/24/outline";
import { getPriorityIcon } from "components/icons/priority-icon";
// types
import { IIssue, UserAuth } from "types";
import { UserAuth } from "types";
// constants
import { PRIORITIES } from "constants/project";
type Props = {
control: Control<IIssue, any>;
submitChanges: (formData: Partial<IIssue>) => void;
value: string | null;
onChange: (val: string) => void;
userAuth: UserAuth;
};
export const SidebarPrioritySelect: React.FC<Props> = ({ control, submitChanges, userAuth }) => {
export const SidebarPrioritySelect: React.FC<Props> = ({ value, onChange, userAuth }) => {
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
return (
@@ -28,38 +26,32 @@ export const SidebarPrioritySelect: React.FC<Props> = ({ control, submitChanges,
<p>Priority</p>
</div>
<div className="sm:basis-1/2">
<Controller
control={control}
name="priority"
render={({ field: { value } }) => (
<CustomSelect
label={
<span
className={`flex items-center gap-2 text-left capitalize ${
value ? "" : "text-gray-900"
}`}
>
{getPriorityIcon(value && value !== "" ? value ?? "" : "None", "text-sm")}
{value && value !== "" ? value : "None"}
</span>
}
value={value}
onChange={(value: any) => {
submitChanges({ priority: value });
}}
disabled={isNotAllowed}
<CustomSelect
label={
<span
className={`flex items-center gap-2 text-left capitalize ${
value ? "" : "text-gray-900"
}`}
>
{PRIORITIES.map((option) => (
<CustomSelect.Option key={option} value={option} className="capitalize">
<>
{getPriorityIcon(option, "text-sm")}
{option ?? "None"}
</>
</CustomSelect.Option>
))}
</CustomSelect>
)}
/>
{getPriorityIcon(value && value !== "" ? value ?? "" : "None", "text-sm")}
{value && value !== "" ? value : "None"}
</span>
}
value={value}
onChange={onChange}
width="w-full"
position="right"
disabled={isNotAllowed}
>
{PRIORITIES.map((option) => (
<CustomSelect.Option key={option} value={option} className="capitalize">
<>
{getPriorityIcon(option, "text-sm")}
{option ?? "None"}
</>
</CustomSelect.Option>
))}
</CustomSelect>
</div>
</div>
);
@@ -4,28 +4,28 @@ import { useRouter } from "next/router";
import useSWR from "swr";
// react-hook-form
import { Control, Controller } from "react-hook-form";
// services
import stateService from "services/state.service";
// ui
import { Spinner, CustomSelect } from "components/ui";
// icons
import { Squares2X2Icon } from "@heroicons/react/24/outline";
import { getStateGroupIcon } from "components/icons";
// helpers
import { getStatesList } from "helpers/state.helper";
import { addSpaceIfCamelCase } from "helpers/string.helper";
// types
import { IIssue, UserAuth } from "types";
import { UserAuth } from "types";
// constants
import { STATE_LIST } from "constants/fetch-keys";
type Props = {
control: Control<IIssue, any>;
submitChanges: (formData: Partial<IIssue>) => void;
value: string;
onChange: (val: string) => void;
userAuth: UserAuth;
};
export const SidebarStateSelect: React.FC<Props> = ({ control, submitChanges, userAuth }) => {
export const SidebarStateSelect: React.FC<Props> = ({ value, onChange, userAuth }) => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
@@ -37,6 +37,8 @@ export const SidebarStateSelect: React.FC<Props> = ({ control, submitChanges, us
);
const states = getStatesList(stateGroups ?? {});
const selectedState = states?.find((s) => s.id === value);
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
return (
@@ -46,60 +48,41 @@ export const SidebarStateSelect: React.FC<Props> = ({ control, submitChanges, us
<p>State</p>
</div>
<div className="sm:basis-1/2">
<Controller
control={control}
name="state"
render={({ field: { value } }) => (
<CustomSelect
label={
<span
className={`flex items-center gap-2 text-left ${value ? "" : "text-gray-900"}`}
>
{value ? (
<>
<span
className="h-2 w-2 flex-shrink-0 rounded-full"
style={{
backgroundColor: states?.find((option) => option.id === value)?.color,
}}
/>
{states?.find((option) => option.id === value)?.name}
</>
) : (
"None"
)}
</span>
}
value={value}
onChange={(value: any) => {
submitChanges({ state: value });
}}
disabled={isNotAllowed}
>
{states ? (
states.length > 0 ? (
states.map((option) => (
<CustomSelect.Option key={option.id} value={option.id}>
<>
{option.color && (
<span
className="h-2 w-2 flex-shrink-0 rounded-full"
style={{ backgroundColor: option.color }}
/>
)}
{option.name}
</>
</CustomSelect.Option>
))
) : (
<div className="text-center">No states found</div>
)
) : (
<Spinner />
<CustomSelect
label={
<div className={`flex items-center gap-2 text-left ${value ? "" : "text-gray-900"}`}>
{getStateGroupIcon(
selectedState?.group ?? "backlog",
"16",
"16",
selectedState?.color ?? ""
)}
</CustomSelect>
{addSpaceIfCamelCase(selectedState?.name ?? "")}
</div>
}
value={value}
onChange={onChange}
width="w-full"
position="right"
disabled={isNotAllowed}
>
{states ? (
states.length > 0 ? (
states.map((state) => (
<CustomSelect.Option key={state.id} value={state.id}>
<>
{getStateGroupIcon(state.group, "16", "16", state.color)}
{state.name}
</>
</CustomSelect.Option>
))
) : (
<div className="text-center">No states found</div>
)
) : (
<Spinner />
)}
/>
</CustomSelect>
</div>
</div>
);
+36 -24
View File
@@ -149,16 +149,12 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
const handleCreateLink = async (formData: IIssueLink) => {
if (!workspaceSlug || !projectId || !issueDetail) return;
const previousLinks = issueDetail?.issue_link.map((l) => ({ title: l.title, url: l.url }));
const payload: Partial<IIssue> = {
links_list: [...(previousLinks ?? []), formData],
};
const payload = { metadata: {}, ...formData };
await issuesService
.patchIssue(workspaceSlug as string, projectId as string, issueDetail.id, payload)
.createIssueLink(workspaceSlug as string, projectId as string, issueDetail.id, payload)
.then((res) => {
mutate(ISSUE_DETAILS(issueDetail.id as string));
mutate(ISSUE_DETAILS(issueDetail.id));
})
.catch((err) => {
console.log(err);
@@ -171,17 +167,15 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
const updatedLinks = issueDetail.issue_link.filter((l) => l.id !== linkId);
mutate<IIssue>(
ISSUE_DETAILS(issueDetail.id as string),
ISSUE_DETAILS(issueDetail.id),
(prevData) => ({ ...(prevData as IIssue), issue_link: updatedLinks }),
false
);
await issuesService
.patchIssue(workspaceSlug as string, projectId as string, issueDetail.id, {
links_list: updatedLinks,
})
.deleteIssueLink(workspaceSlug as string, projectId as string, issueDetail.id, linkId)
.then((res) => {
mutate(ISSUE_DETAILS(issueDetail.id as string));
mutate(ISSUE_DETAILS(issueDetail.id));
})
.catch((err) => {
console.log(err);
@@ -223,7 +217,7 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
isOpen={deleteIssueModal}
data={issueDetail ?? null}
/>
<div className="w-full divide-y-2 divide-gray-100 sticky top-5">
<div className="sticky top-5 w-full divide-y-2 divide-gray-100">
<div className="flex items-center justify-between pb-3">
<h4 className="text-sm font-medium">
{issueDetail?.project_detail?.identifier}-{issueDetail?.sequence_id}
@@ -249,20 +243,38 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
</div>
<div className="divide-y-2 divide-gray-100">
<div className="py-1">
<SidebarStateSelect
<Controller
control={control}
submitChanges={submitChanges}
userAuth={userAuth}
name="state"
render={({ field: { value } }) => (
<SidebarStateSelect
value={value}
onChange={(val: string) => submitChanges({ state: val })}
userAuth={userAuth}
/>
)}
/>
<SidebarAssigneeSelect
<Controller
control={control}
submitChanges={submitChanges}
userAuth={userAuth}
name="assignees_list"
render={({ field: { value } }) => (
<SidebarAssigneeSelect
value={value}
onChange={(val: string[]) => submitChanges({ assignees_list: val })}
userAuth={userAuth}
/>
)}
/>
<SidebarPrioritySelect
<Controller
control={control}
submitChanges={submitChanges}
userAuth={userAuth}
name="priority"
render={({ field: { value } }) => (
<SidebarPrioritySelect
value={value}
onChange={(val: string) => submitChanges({ priority: val })}
userAuth={userAuth}
/>
)}
/>
</div>
<div className="py-1">
@@ -448,8 +460,8 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
);
} else
return (
<div className="bg-gray-50 border-y border-gray-400">
<div className="flex select-none font-medium items-center gap-2 truncate p-2 text-gray-900">
<div className="border-y border-gray-400 bg-gray-50">
<div className="flex select-none items-center gap-2 truncate p-2 font-medium text-gray-900">
<RectangleGroupIcon className="h-3 w-3" />{" "}
{label.name}
</div>
@@ -4,12 +4,12 @@ import { useRouter } from "next/router";
import useSWR from "swr";
// headless ui
import { Listbox, Transition } from "@headlessui/react";
// services
import projectService from "services/project.service";
// ui
import { AssigneesList, Avatar, Tooltip } from "components/ui";
import { AssigneesList, Avatar, CustomSearchSelect, Tooltip } from "components/ui";
// icons
import { UserGroupIcon } from "@heroicons/react/24/outline";
// types
import { IIssue } from "types";
// fetch-keys
@@ -18,6 +18,7 @@ import { PROJECT_MEMBERS } from "constants/fetch-keys";
type Props = {
issue: IIssue;
partialUpdateIssue: (formData: Partial<IIssue>) => void;
position?: "left" | "right";
selfPositioned?: boolean;
tooltipPosition?: "left" | "right";
isNotAllowed: boolean;
@@ -26,6 +27,7 @@ type Props = {
export const ViewAssigneeSelect: React.FC<Props> = ({
issue,
partialUpdateIssue,
position = "left",
selfPositioned = false,
tooltipPosition = "right",
isNotAllowed,
@@ -40,9 +42,27 @@ export const ViewAssigneeSelect: React.FC<Props> = ({
: null
);
const options =
members?.map((member) => ({
value: member.member.id,
query:
(member.member.first_name && member.member.first_name !== ""
? member.member.first_name
: member.member.email) +
" " +
member.member.last_name ?? "",
content: (
<div className="flex items-center gap-2">
<Avatar user={member.member} />
{member.member.first_name && member.member.first_name !== ""
? member.member.first_name
: member.member.email}
</div>
),
})) ?? [];
return (
<Listbox
as="div"
<CustomSearchSelect
value={issue.assignees}
onChange={(data: any) => {
const newData = issue.assignees ?? [];
@@ -50,69 +70,46 @@ export const ViewAssigneeSelect: React.FC<Props> = ({
if (newData.includes(data)) newData.splice(newData.indexOf(data), 1);
else newData.push(data);
partialUpdateIssue({ assignees_list: newData });
partialUpdateIssue({ assignees_list: data });
}}
className={`group ${!selfPositioned ? "relative" : ""} flex-shrink-0`}
disabled={isNotAllowed}
>
{({ open }) => (
<div>
<Listbox.Button>
<Tooltip
position={`top-${tooltipPosition}`}
tooltipHeading="Assignees"
tooltipContent={
issue.assignee_details.length > 0
? issue.assignee_details
.map((assignee) =>
assignee?.first_name !== "" ? assignee?.first_name : assignee?.email
)
.join(", ")
: "No Assignee"
}
>
<div
className={`flex ${
isNotAllowed ? "cursor-not-allowed" : "cursor-pointer"
} items-center gap-1 text-xs`}
>
<AssigneesList userIds={issue.assignees ?? []} />
</div>
</Tooltip>
</Listbox.Button>
<Transition
show={open}
as={React.Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
options={options}
label={
<Tooltip
position={`top-${tooltipPosition}`}
tooltipHeading="Assignees"
tooltipContent={
issue.assignee_details.length > 0
? issue.assignee_details
.map((assignee) =>
assignee?.first_name !== "" ? assignee?.first_name : assignee?.email
)
.join(", ")
: "No Assignee"
}
>
<div
className={`flex ${
isNotAllowed ? "cursor-not-allowed" : "cursor-pointer"
} items-center gap-2 text-gray-500`}
>
<Listbox.Options className="absolute right-0 z-10 mt-1 max-h-48 overflow-auto rounded-md bg-white py-1 text-xs shadow-lg min-w-full ring-1 ring-black ring-opacity-5 focus:outline-none">
{members?.map((member) => (
<Listbox.Option
key={member.member.id}
className={({ active, selected }) =>
`flex items-center gap-x-1 cursor-pointer select-none p-2 whitespace-nowrap ${
active ? "bg-indigo-50" : ""
} ${
selected || issue.assignees?.includes(member.member.id)
? "bg-indigo-50 font-medium"
: "font-normal"
}`
}
value={member.member.id}
>
<Avatar user={member.member} />
{member.member.first_name && member.member.first_name !== ""
? member.member.first_name
: member.member.email}
</Listbox.Option>
))}
</Listbox.Options>
</Transition>
</div>
)}
</Listbox>
{issue.assignees && issue.assignees.length > 0 && Array.isArray(issue.assignees) ? (
<div className="flex items-center justify-center gap-2">
<AssigneesList userIds={issue.assignees} length={3} showLength={false} />
<span className="text-gray-500">{issue.assignees.length} Assignees</span>
</div>
) : (
<div className="flex items-center justify-center gap-2">
<UserGroupIcon className="h-4 w-4 text-gray-500" />
<span className="text-gray-500">Assignee</span>
</div>
)}
</div>
</Tooltip>
}
multiple
noChevron
position={position}
disabled={isNotAllowed}
/>
);
};
@@ -12,6 +12,7 @@ import { PRIORITIES } from "constants/project";
type Props = {
issue: IIssue;
partialUpdateIssue: (formData: Partial<IIssue>) => void;
position?: "left" | "right";
selfPositioned?: boolean;
isNotAllowed: boolean;
};
@@ -19,40 +20,44 @@ type Props = {
export const ViewPrioritySelect: React.FC<Props> = ({
issue,
partialUpdateIssue,
position = "left",
selfPositioned = false,
isNotAllowed,
}) => (
<CustomSelect
label={
<Tooltip tooltipHeading="Priority" tooltipContent={issue.priority ?? "None"}>
<span>
{getPriorityIcon(
issue.priority && issue.priority !== "" ? issue.priority ?? "" : "None",
"text-sm"
)}
</span>
</Tooltip>
}
value={issue.state}
onChange={(data: string) => {
partialUpdateIssue({ priority: data });
}}
onChange={(data: string) => partialUpdateIssue({ priority: data })}
maxHeight="md"
buttonClassName={`flex ${
isNotAllowed ? "cursor-not-allowed" : "cursor-pointer"
} items-center gap-x-2 rounded px-2 py-0.5 capitalize shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 ${
issue.priority === "urgent"
? "bg-red-100 text-red-600 hover:bg-red-100"
: issue.priority === "high"
? "bg-orange-100 text-orange-500 hover:bg-orange-100"
: issue.priority === "medium"
? "bg-yellow-100 text-yellow-500 hover:bg-yellow-100"
: issue.priority === "low"
? "bg-green-100 text-green-500 hover:bg-green-100"
: "bg-gray-100"
} border-none`}
customButton={
<button
type="button"
className={`grid h-6 w-6 place-items-center rounded ${
isNotAllowed ? "cursor-not-allowed" : "cursor-pointer"
} items-center shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 ${
issue.priority === "urgent"
? "bg-red-100 text-red-600 hover:bg-red-100"
: issue.priority === "high"
? "bg-orange-100 text-orange-500 hover:bg-orange-100"
: issue.priority === "medium"
? "bg-yellow-100 text-yellow-500 hover:bg-yellow-100"
: issue.priority === "low"
? "bg-green-100 text-green-500 hover:bg-green-100"
: "bg-gray-100"
} border-none`}
>
<Tooltip tooltipHeading="Priority" tooltipContent={issue.priority ?? "None"}>
<span>
{getPriorityIcon(
issue.priority && issue.priority !== "" ? issue.priority ?? "" : "None",
"text-sm"
)}
</span>
</Tooltip>
</button>
}
noChevron
disabled={isNotAllowed}
position={position}
selfPositioned={selfPositioned}
>
{PRIORITIES?.map((priority) => (
@@ -5,7 +5,9 @@ import useSWR from "swr";
// services
import stateService from "services/state.service";
// ui
import { CustomSelect, Tooltip } from "components/ui";
import { CustomSearchSelect, Tooltip } from "components/ui";
// icons
import { getStateGroupIcon } from "components/icons";
// helpers
import { addSpaceIfCamelCase } from "helpers/string.helper";
import { getStatesList } from "helpers/state.helper";
@@ -17,6 +19,7 @@ import { STATE_LIST } from "constants/fetch-keys";
type Props = {
issue: IIssue;
partialUpdateIssue: (formData: Partial<IIssue>) => void;
position?: "left" | "right";
selfPositioned?: boolean;
isNotAllowed: boolean;
};
@@ -24,6 +27,7 @@ type Props = {
export const ViewStateSelect: React.FC<Props> = ({
issue,
partialUpdateIssue,
position = "left",
selfPositioned = false,
isNotAllowed,
}) => {
@@ -38,50 +42,39 @@ export const ViewStateSelect: React.FC<Props> = ({
);
const states = getStatesList(stateGroups ?? {});
const options = states?.map((state) => ({
value: state.id,
query: state.name,
content: (
<div className="flex items-center gap-2">
{getStateGroupIcon(state.group, "16", "16", state.color)}
{state.name}
</div>
),
}));
const selectedOption = states?.find((s) => s.id === issue.state);
return (
<CustomSelect
label={
<>
<span
className="h-1.5 w-1.5 flex-shrink-0 rounded-full"
style={{
backgroundColor: states?.find((s) => s.id === issue.state)?.color,
}}
/>
<Tooltip
tooltipHeading="State"
tooltipContent={addSpaceIfCamelCase(
states?.find((s) => s.id === issue.state)?.name ?? ""
)}
>
<span>
{addSpaceIfCamelCase(states?.find((s) => s.id === issue.state)?.name ?? "")}
</span>
</Tooltip>
</>
}
<CustomSearchSelect
value={issue.state}
onChange={(data: string) => {
partialUpdateIssue({ state: data });
}}
maxHeight="md"
noChevron
onChange={(data: string) => partialUpdateIssue({ state: data })}
options={options}
label={
<Tooltip
tooltipHeading="State"
tooltipContent={addSpaceIfCamelCase(selectedOption?.name ?? "")}
>
<div className="flex items-center gap-2 text-gray-500">
{selectedOption &&
getStateGroupIcon(selectedOption.group, "16", "16", selectedOption.color)}
{selectedOption?.name ?? "State"}
</div>
</Tooltip>
}
position={position}
disabled={isNotAllowed}
selfPositioned={selfPositioned}
>
{states?.map((state) => (
<CustomSelect.Option key={state.id} value={state.id}>
<>
<span
className="h-1.5 w-1.5 flex-shrink-0 rounded-full"
style={{
backgroundColor: state.color,
}}
/>
{addSpaceIfCamelCase(state.name)}
</>
</CustomSelect.Option>
))}
</CustomSelect>
noChevron
/>
);
};
@@ -109,7 +109,7 @@ export const CreateUpdateLabelInline: React.FC<Props> = ({
return (
<div
className={`flex items-center gap-2 rounded-md border p-3 md:w-2/3 ${
className={`flex items-center gap-2 rounded-[10px] border bg-white p-5 ${
labelForm ? "" : "hidden"
}`}
>
@@ -92,7 +92,7 @@ export const LabelsListModal: React.FC<Props> = ({ isOpen, handleClose, parent }
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<Dialog.Panel className="relative mx-auto max-w-2xl transform divide-y divide-gray-500 divide-opacity-10 rounded-xl bg-white bg-opacity-80 shadow-2xl ring-1 ring-black ring-opacity-5 backdrop-blur backdrop-filter transition-all">
<Dialog.Panel className="relative mx-auto max-w-2xl transform divide-y divide-gray-500 divide-opacity-10 rounded-xl bg-white shadow-2xl ring-1 ring-black ring-opacity-5 transition-all">
<Combobox>
<div className="relative m-1">
<MagnifyingGlassIcon
@@ -144,7 +144,7 @@ export const LabelsListModal: React.FC<Props> = ({ isOpen, handleClose, parent }
}}
>
<span
className="block flex-shrink-0 h-1.5 w-1.5 rounded-full"
className="block h-1.5 w-1.5 flex-shrink-0 rounded-full"
style={{
backgroundColor: label.color,
}}
@@ -59,32 +59,34 @@ export const SingleLabelGroup: React.FC<Props> = ({
};
return (
<Disclosure as="div" className="rounded-md border p-3 text-gray-900 md:w-2/3" defaultOpen>
<Disclosure as="div" className="rounded-[10px] border bg-white p-5 text-gray-900" defaultOpen>
{({ open }) => (
<>
<div className="flex items-center justify-between gap-2 cursor-pointer">
<Disclosure.Button>
<div className="flex items-center gap-2">
<div className="flex cursor-pointer items-center justify-between gap-2">
<div className="flex items-center gap-2">
<span>
<RectangleGroupIcon className="h-4 w-4" />
</span>
<h6 className="font-medium text-gray-600">{label.name}</h6>
</div>
<div className="flex items-center gap-2">
<CustomMenu ellipsis>
<CustomMenu.MenuItem onClick={() => addLabelToGroup(label)}>
Add more labels
</CustomMenu.MenuItem>
<CustomMenu.MenuItem onClick={() => editLabel(label)}>Edit</CustomMenu.MenuItem>
<CustomMenu.MenuItem onClick={() => handleLabelDelete(label.id)}>
Delete
</CustomMenu.MenuItem>
</CustomMenu>
<Disclosure.Button>
<span>
<ChevronDownIcon
className={`h-4 w-4 text-gray-500 ${!open ? "-rotate-90 transform" : ""}`}
className={`h-4 w-4 text-gray-500 ${!open ? "rotate-90 transform" : ""}`}
/>
</span>
<span>
<RectangleGroupIcon className="h-4 w-4" />
</span>
<h6 className="text-sm">{label.name}</h6>
</div>
</Disclosure.Button>
<CustomMenu ellipsis>
<CustomMenu.MenuItem onClick={() => addLabelToGroup(label)}>
Add more labels
</CustomMenu.MenuItem>
<CustomMenu.MenuItem onClick={() => editLabel(label)}>Edit</CustomMenu.MenuItem>
<CustomMenu.MenuItem onClick={() => handleLabelDelete(label.id)}>
Delete
</CustomMenu.MenuItem>
</CustomMenu>
</Disclosure.Button>
</div>
</div>
<Transition
show={open}
@@ -96,22 +98,22 @@ export const SingleLabelGroup: React.FC<Props> = ({
leaveTo="transform opacity-0"
>
<Disclosure.Panel>
<div className="mt-2 ml-4">
<div className="mt-3 ml-6 space-y-3">
{labelChildren.map((child) => (
<div
key={child.id}
className="group pl-4 py-1 flex items-center justify-between rounded text-sm hover:bg-gray-100"
className="group flex items-center justify-between rounded-md border p-2 text-sm"
>
<h5 className="flex items-center gap-2">
<h5 className="flex items-center gap-3 text-gray-600">
<span
className="h-2 w-2 flex-shrink-0 rounded-full"
className="h-2.5 w-2.5 flex-shrink-0 rounded-full"
style={{
backgroundColor: child.color,
}}
/>
{child.name}
</h5>
<div className="opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto">
<div className="pointer-events-none opacity-0 group-hover:pointer-events-auto group-hover:opacity-100">
<CustomMenu ellipsis>
<CustomMenu.MenuItem onClick={() => removeFromGroup(child)}>
Remove from group
+4 -4
View File
@@ -18,16 +18,16 @@ export const SingleLabel: React.FC<Props> = ({
editLabel,
handleLabelDelete,
}) => (
<div className="gap-2 space-y-3 divide-y rounded-md border p-3 md:w-2/3">
<div className="gap-2 space-y-3 divide-y rounded-[10px] border bg-white p-5">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="flex items-center gap-3">
<span
className="h-3 w-3 flex-shrink-0 rounded-full"
className="h-3.5 w-3.5 flex-shrink-0 rounded-full"
style={{
backgroundColor: label.color && label.color !== "" ? label.color : "#000",
}}
/>
<h6 className="text-sm">{label.name}</h6>
<h6 className="font-medium text-gray-600">{label.name}</h6>
</div>
<CustomMenu ellipsis>
<CustomMenu.MenuItem onClick={() => addLabelToGroup(label)}>
+14 -2
View File
@@ -114,8 +114,20 @@ export const ModuleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, sta
</div>
<div className="flex flex-wrap items-center gap-2">
<ModuleStatusSelect control={control} error={errors.status} />
<ModuleLeadSelect control={control} />
<ModuleMembersSelect control={control} />
<Controller
control={control}
name="lead"
render={({ field: { value, onChange } }) => (
<ModuleLeadSelect value={value} onChange={onChange} />
)}
/>
<Controller
control={control}
name="members"
render={({ field: { value, onChange } }) => (
<ModuleMembersSelect value={value} onChange={onChange} />
)}
/>
</div>
</div>
</div>
+7 -9
View File
@@ -76,15 +76,12 @@ export const CreateUpdateModuleModal: React.FC<Props> = ({ isOpen, setIsOpen, da
.then((res) => {
mutate<IModule[]>(
MODULE_LIST(projectId as string),
(prevData) => {
const newData = prevData?.map((item) => {
if (item.id === res.id) {
return res;
}
return item;
});
return newData;
},
(prevData) =>
prevData?.map((p) => {
if (p.id === res.id) return { ...p, ...payload };
return p;
}),
false
);
handleClose();
@@ -109,6 +106,7 @@ export const CreateUpdateModuleModal: React.FC<Props> = ({ isOpen, setIsOpen, da
const payload: Partial<IModule> = {
...formData,
members_list: formData.members,
};
if (!data) await createModule(payload);
@@ -1,57 +1,78 @@
import React from "react";
import { useRouter } from "next/router";
import Image from "next/image";
import useSWR from "swr";
// react-hook-form
import { Controller, Control } from "react-hook-form";
// services
import projectServices from "services/project.service";
// ui
import SearchListbox from "components/search-listbox";
import { Avatar, CustomSearchSelect } from "components/ui";
// icons
import { UserIcon } from "@heroicons/react/24/outline";
// types
import type { IModule } from "types";
import User from "public/user.png";
// fetch-keys
import { PROJECT_MEMBERS } from "constants/fetch-keys";
type Props = {
control: Control<IModule, any>;
value: string | null;
onChange: () => void;
};
export const ModuleLeadSelect: React.FC<Props> = ({ control }) => {
export const ModuleLeadSelect: React.FC<Props> = ({ value, onChange }) => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { data: people } = useSWR(
const { data: members } = useSWR(
workspaceSlug && projectId ? PROJECT_MEMBERS(projectId as string) : null,
workspaceSlug && projectId
? () => projectServices.projectMembers(workspaceSlug as string, projectId as string)
: null
);
const options =
members?.map((member) => ({
value: member.member.id,
query:
(member.member.first_name && member.member.first_name !== ""
? member.member.first_name
: member.member.email) +
" " +
member.member.last_name ?? "",
content: (
<div className="flex items-center gap-2">
<Avatar user={member.member} />
{member.member.first_name && member.member.first_name !== ""
? member.member.first_name
: member.member.email}
</div>
),
})) ?? [];
const selectedOption = members?.find((m) => m.member.id === value)?.member;
return (
<Controller
control={control}
name="lead"
render={({ field: { value, onChange } }) => (
<SearchListbox
title="Lead"
optionsFontsize="sm"
options={people?.map((person) => ({
value: person.member.id,
display:
person.member.first_name && person.member.first_name !== ""
? person.member.first_name
: person.member.email,
}))}
value={value}
onChange={onChange}
icon={<UserIcon className="h-3 w-3 text-gray-500" />}
/>
)}
<CustomSearchSelect
options={options}
value={value}
label={
<div className="flex items-center gap-2 text-gray-500">
{selectedOption ? (
<Avatar user={selectedOption} />
) : (
<div className="h-4 w-4 rounded-full bg-white">
<Image src={User} height="100%" width="100%" className="rounded-full" alt="No user" />
</div>
)}
{selectedOption
? selectedOption?.first_name && selectedOption.first_name !== ""
? selectedOption?.first_name
: selectedOption?.email
: "N/A"}
</div>
}
onChange={onChange}
noChevron
/>
);
};
@@ -4,55 +4,72 @@ import { useRouter } from "next/router";
import useSWR from "swr";
// react-hook-form
import { Controller, Control } from "react-hook-form";
// services
import projectServices from "services/project.service";
// ui
import SearchListbox from "components/search-listbox";
import { AssigneesList, Avatar, CustomSearchSelect } from "components/ui";
// icons
import { UserIcon } from "@heroicons/react/24/outline";
// types
import type { IModule } from "types";
import { UserGroupIcon } from "@heroicons/react/24/outline";
// fetch-keys
import { PROJECT_MEMBERS } from "constants/fetch-keys";
type Props = {
control: Control<IModule, any>;
value: string[];
onChange: () => void;
};
export const ModuleMembersSelect: React.FC<Props> = ({ control }) => {
export const ModuleMembersSelect: React.FC<Props> = ({ value, onChange }) => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { data: people } = useSWR(
const { data: members } = useSWR(
workspaceSlug && projectId ? PROJECT_MEMBERS(projectId as string) : null,
workspaceSlug && projectId
? () => projectServices.projectMembers(workspaceSlug as string, projectId as string)
: null
);
const options =
members?.map((member) => ({
value: member.member.id,
query:
(member.member.first_name && member.member.first_name !== ""
? member.member.first_name
: member.member.email) +
" " +
member.member.last_name ?? "",
content: (
<div className="flex items-center gap-2">
<Avatar user={member.member} />
{member.member.first_name && member.member.first_name !== ""
? member.member.first_name
: member.member.email}
</div>
),
})) ?? [];
return (
<Controller
control={control}
name="members_list"
render={({ field: { value, onChange } }) => (
<SearchListbox
title="Members"
optionsFontsize="sm"
options={people?.map((person) => ({
value: person.member.id,
display:
person.member.first_name && person.member.first_name !== ""
? person.member.first_name
: person.member.email,
}))}
multiple={true}
value={value}
onChange={onChange}
icon={<UserIcon className="h-3 w-3 text-gray-500" />}
/>
)}
<CustomSearchSelect
value={value}
label={
<div className="flex items-center gap-2 text-gray-500">
{value && value.length > 0 && Array.isArray(value) ? (
<div className="flex items-center justify-center gap-2">
<AssigneesList userIds={value} length={3} showLength={false} />
<span className="text-gray-500">{value.length} Assignees</span>
</div>
) : (
<div className="flex items-center justify-center gap-2">
<UserGroupIcon className="h-4 w-4 text-gray-500" />
<span className="text-gray-500">Assignee</span>
</div>
)}
</div>
}
options={options}
onChange={onChange}
height="md"
multiple
noChevron
/>
);
};
@@ -3,7 +3,7 @@ import React from "react";
// react hook form
import { Controller, FieldError, Control } from "react-hook-form";
// ui
import { CustomListbox } from "components/ui";
import { CustomSelect } from "components/ui";
// icons
import { Squares2X2Icon } from "@heroicons/react/24/outline";
// types
@@ -22,26 +22,43 @@ export const ModuleStatusSelect: React.FC<Props> = ({ control, error }) => (
rules={{ required: true }}
name="status"
render={({ field: { value, onChange } }) => (
<div>
<CustomListbox
className={`${
error
? "border-red-500 bg-red-100 hover:bg-red-100 focus:outline-none focus:ring-red-500"
: ""
}`}
title="Status"
options={MODULE_STATUS.map((status) => ({
value: status.value,
display: status.label,
color: status.color,
}))}
value={value}
optionsFontsize="sm"
onChange={onChange}
icon={<Squares2X2Icon className={`h-3 w-3 ${error ? "text-black" : "text-gray-400"}`} />}
/>
{error && <p className="mt-1 text-sm text-red-600">{error.message}</p>}
</div>
<CustomSelect
value={value}
label={
<div
className={`flex items-center justify-center gap-2 text-xs ${
error ? "text-red-500" : ""
}`}
>
<Squares2X2Icon className={`h-3 w-3 ${error ? "text-red-500" : "text-gray-400"}`} />
{value && (
<span
className="h-1.5 w-1.5 flex-shrink-0 rounded-full"
style={{
backgroundColor: MODULE_STATUS.find((s) => s.value === value)?.color,
}}
/>
)}
{MODULE_STATUS.find((s) => s.value === value)?.label ?? "Status"}
</div>
}
onChange={onChange}
noChevron
>
{MODULE_STATUS.map((status) => (
<CustomSelect.Option key={status.value} value={status.value}>
<div className="flex items-center gap-2">
<span
className="h-1.5 w-1.5 flex-shrink-0 rounded-full"
style={{
backgroundColor: status.color,
}}
/>
{status.label}
</div>
</CustomSelect.Option>
))}
</CustomSelect>
)}
/>
);
@@ -5,160 +5,88 @@ import { useRouter } from "next/router";
import useSWR from "swr";
// react-hook-form
import { Control, Controller } from "react-hook-form";
// headless ui
import { Listbox, Transition } from "@headlessui/react";
// services
import workspaceService from "services/workspace.service";
import projectService from "services/project.service";
// ui
import { Avatar, CustomSearchSelect } from "components/ui";
// icons
import { UserIcon } from "@heroicons/react/24/outline";
import { UserCircleIcon } from "@heroicons/react/24/outline";
import User from "public/user.png";
// types
import { IModule, IUserLite } from "types";
// fetch-keys
import { WORKSPACE_MEMBERS } from "constants/fetch-keys";
import { PROJECT_MEMBERS } from "constants/fetch-keys";
type Props = {
control: Control<Partial<IModule>, any>;
submitChanges: (formData: Partial<IModule>) => void;
lead: IUserLite | null;
value: string | null | undefined;
onChange: (val: string) => void;
};
export const SidebarLeadSelect: React.FC<Props> = ({ control, submitChanges, lead }) => {
export const SidebarLeadSelect: React.FC<Props> = ({ value, onChange }) => {
const router = useRouter();
const { workspaceSlug } = router.query;
const { workspaceSlug, projectId } = router.query;
const { data: people } = useSWR(
workspaceSlug ? WORKSPACE_MEMBERS(workspaceSlug as string) : null,
workspaceSlug ? () => workspaceService.workspaceMembers(workspaceSlug as string) : null
const { data: members } = useSWR(
workspaceSlug && projectId ? PROJECT_MEMBERS(projectId as string) : null,
workspaceSlug && projectId
? () => projectService.projectMembers(workspaceSlug as string, projectId as string)
: null
);
const options =
members?.map((member) => ({
value: member.member.id,
query:
(member.member.first_name && member.member.first_name !== ""
? member.member.first_name
: member.member.email) +
" " +
member.member.last_name ?? "",
content: (
<div className="flex items-center gap-2">
<Avatar user={member.member} />
{member.member.first_name && member.member.first_name !== ""
? member.member.first_name
: member.member.email}
</div>
),
})) ?? [];
const selectedOption = members?.find((m) => m.member.id === value)?.member;
return (
<div className="flex flex-wrap items-center py-2">
<div className="flex items-center gap-x-2 text-sm sm:basis-1/2">
<UserIcon className="h-4 w-4 flex-shrink-0" />
<p>Lead</p>
<div className="flex items-center justify-start gap-1">
<div className="flex w-40 items-center justify-start gap-2">
<UserCircleIcon className="h-5 w-5 text-gray-400" />
<span>Lead</span>
</div>
<div className="sm:basis-1/2">
<Controller
control={control}
name="lead"
render={({ field: { value } }) => (
<Listbox
as="div"
value={value}
onChange={(value: any) => {
submitChanges({ lead: value });
}}
className="flex-shrink-0"
>
{({ open }) => (
<div className="relative">
<Listbox.Button className="flex w-full cursor-pointer items-center gap-1 text-xs">
<span
className={`hidden truncate text-left sm:block ${
value ? "" : "text-gray-900"
}`}
>
<div className="flex items-center gap-1 text-xs">
{lead ? (
lead.avatar && lead.avatar !== "" ? (
<div className="h-5 w-5 rounded-full border-2 border-transparent">
<Image
src={lead.avatar}
height="100%"
width="100%"
className="rounded-full"
alt={lead?.first_name}
/>
</div>
) : (
<div className="grid h-5 w-5 place-items-center rounded-full border-2 border-white bg-gray-700 capitalize text-white">
{lead?.first_name && lead.first_name !== ""
? lead.first_name.charAt(0)
: lead?.email.charAt(0)}
</div>
)
) : (
<div className="h-5 w-5 rounded-full border-2 border-white bg-white">
<Image
src={User}
height="100%"
width="100%"
className="rounded-full"
alt="No user"
/>
</div>
)}
{lead
? lead?.first_name && lead.first_name !== ""
? lead?.first_name
: lead?.email
: "N/A"}
</div>
</span>
</Listbox.Button>
<Transition
show={open}
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"
>
<Listbox.Options className="absolute right-0 z-10 mt-1 max-h-48 w-full overflow-auto rounded-md bg-white py-1 text-xs shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
<div className="py-1">
{people ? (
people.length > 0 ? (
people.map((option) => (
<Listbox.Option
key={option.member.id}
className={({ active, selected }) =>
`${
active || selected ? "bg-indigo-50" : ""
} flex cursor-pointer select-none items-center gap-2 truncate p-2 text-gray-900`
}
value={option.member.id}
>
{option.member.avatar && option.member.avatar !== "" ? (
<div className="relative h-4 w-4">
<Image
src={option.member.avatar}
alt="avatar"
className="rounded-full"
layout="fill"
objectFit="cover"
/>
</div>
) : (
<div className="grid h-4 w-4 flex-shrink-0 place-items-center rounded-full bg-gray-700 capitalize text-white">
{option.member.first_name && option.member.first_name !== ""
? option.member.first_name.charAt(0)
: option.member.email.charAt(0)}
</div>
)}
{option.member.first_name && option.member.first_name !== ""
? option.member.first_name
: option.member.email}
</Listbox.Option>
))
) : (
<div className="text-center">No members found</div>
)
) : (
<p className="text-xs text-gray-500 px-2">Loading...</p>
)}
</div>
</Listbox.Options>
</Transition>
<CustomSearchSelect
value={value}
label={
<div className="flex items-center gap-2 text-gray-500">
{selectedOption ? (
<Avatar user={selectedOption} />
) : (
<div className="h-5 w-5 rounded-full border-2 border-transparent bg-white">
<Image
src={User}
height="100%"
width="100%"
className="rounded-full"
alt="No user"
/>
</div>
)}
</Listbox>
)}
{selectedOption
? selectedOption?.first_name && selectedOption.first_name !== ""
? selectedOption?.first_name
: selectedOption?.email
: "N/A"}
</div>
}
options={options}
height="md"
position="right"
onChange={onChange}
/>
</div>
</div>
@@ -1,132 +1,79 @@
import React from "react";
import Image from "next/image";
import { useRouter } from "next/router";
import useSWR from "swr";
import { Control, Controller } from "react-hook-form";
// services
import { Listbox, Transition } from "@headlessui/react";
import { UserGroupIcon } from "@heroicons/react/24/outline";
import workspaceService from "services/workspace.service";
// headless ui
import projectService from "services/project.service";
// ui
import { AssigneesList } from "components/ui";
// types
import { IModule } from "types";
// constants
import { WORKSPACE_MEMBERS } from "constants/fetch-keys";
import { AssigneesList, Avatar, CustomSearchSelect } from "components/ui";
// icons
import { UserGroupIcon } from "@heroicons/react/24/outline";
// fetch-keys
import { PROJECT_MEMBERS } from "constants/fetch-keys";
type Props = {
control: Control<Partial<IModule>, any>;
submitChanges: (formData: Partial<IModule>) => void;
value: string[] | undefined;
onChange: (val: string[]) => void;
};
export const SidebarMembersSelect: React.FC<Props> = ({ control, submitChanges }) => {
export const SidebarMembersSelect: React.FC<Props> = ({ value, onChange }) => {
const router = useRouter();
const { workspaceSlug } = router.query;
const { workspaceSlug, projectId } = router.query;
const { data: people } = useSWR(
workspaceSlug ? WORKSPACE_MEMBERS(workspaceSlug as string) : null,
workspaceSlug ? () => workspaceService.workspaceMembers(workspaceSlug as string) : null
const { data: members } = useSWR(
workspaceSlug && projectId ? PROJECT_MEMBERS(projectId as string) : null,
workspaceSlug && projectId
? () => projectService.projectMembers(workspaceSlug as string, projectId as string)
: null
);
const options =
members?.map((member) => ({
value: member.member.id,
query:
(member.member.first_name && member.member.first_name !== ""
? member.member.first_name
: member.member.email) +
" " +
member.member.last_name ?? "",
content: (
<div className="flex items-center gap-2">
<Avatar user={member.member} />
{member.member.first_name && member.member.first_name !== ""
? member.member.first_name
: member.member.email}
</div>
),
})) ?? [];
return (
<div className="flex flex-wrap items-center py-2">
<div className="flex items-center gap-x-2 text-sm sm:basis-1/2">
<UserGroupIcon className="h-4 w-4 flex-shrink-0" />
<p>Members</p>
<div className="flex items-center justify-start gap-1">
<div className="flex w-40 items-center justify-start gap-2">
<UserGroupIcon className="h-5 w-5 text-gray-400" />
<span>Members</span>
</div>
<div className="sm:basis-1/2">
<Controller
control={control}
name="members_list"
render={({ field: { value } }) => (
<Listbox
as="div"
value={value}
multiple={true}
onChange={(value: any) => {
submitChanges({ members_list: value });
}}
className="flex-shrink-0"
>
{({ open }) => (
<div className="relative">
<Listbox.Button className="flex w-full cursor-pointer items-center gap-1 text-xs">
<span
className={`hidden truncate text-left sm:block ${
value ? "" : "text-gray-900"
}`}
>
<div className="flex cursor-pointer items-center gap-1 text-xs">
{value && Array.isArray(value) ? (
<AssigneesList userIds={value} length={10} />
) : null}
</div>
</span>
</Listbox.Button>
<Transition
show={open}
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"
>
<Listbox.Options className="absolute left-0 z-10 mt-1 max-h-48 overflow-auto rounded-md bg-white py-1 text-xs shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none w-full">
<div className="py-1">
{people ? (
people.length > 0 ? (
people.map((option) => (
<Listbox.Option
key={option.member.id}
className={({ active, selected }) =>
`${
active || selected ? "bg-indigo-50" : ""
} flex cursor-pointer select-none items-center gap-2 truncate p-2 text-gray-900`
}
value={option.member.id}
>
{option.member.avatar && option.member.avatar !== "" ? (
<div className="relative h-4 w-4">
<Image
src={option.member.avatar}
alt="avatar"
className="rounded-full"
layout="fill"
objectFit="cover"
/>
</div>
) : (
<div className="grid h-4 w-4 flex-shrink-0 place-items-center rounded-full bg-gray-700 capitalize text-white">
{option.member.first_name && option.member.first_name !== ""
? option.member.first_name.charAt(0)
: option.member.email.charAt(0)}
</div>
)}
{option.member.first_name && option.member.first_name !== ""
? option.member.first_name
: option.member.email}
</Listbox.Option>
))
) : (
<div className="text-center">No members found</div>
)
) : (
<p className="text-xs text-gray-500 px-2">Loading...</p>
)}
</div>
</Listbox.Options>
</Transition>
<CustomSearchSelect
value={value}
label={
<div className="flex items-center gap-2 text-gray-500">
{value && value.length > 0 && Array.isArray(value) ? (
<div className="flex items-center justify-center gap-2">
<AssigneesList userIds={value} length={3} showLength={false} />
<span className="text-gray-500">{value.length} Assignees</span>
</div>
) : (
"No members"
)}
</Listbox>
)}
</div>
}
options={options}
onChange={onChange}
height="md"
position="right"
multiple
/>
</div>
</div>
+338 -219
View File
@@ -1,6 +1,5 @@
import React, { useEffect, useState } from "react";
import Link from "next/link";
import { useRouter } from "next/router";
import { mutate } from "swr";
@@ -9,32 +8,32 @@ import { mutate } from "swr";
import { Controller, useForm } from "react-hook-form";
// icons
import {
ArrowLongRightIcon,
CalendarDaysIcon,
ChartPieIcon,
LinkIcon,
PlusIcon,
Squares2X2Icon,
ChevronDownIcon,
DocumentDuplicateIcon,
DocumentIcon,
TrashIcon,
} from "@heroicons/react/24/outline";
import { Popover, Transition } from "@headlessui/react";
import { Disclosure, Popover, Transition } from "@headlessui/react";
import DatePicker from "react-datepicker";
// services
import modulesService from "services/modules.service";
// hooks
import useToast from "hooks/use-toast";
// components
import { LinkModal, LinksList, SidebarProgressStats } from "components/core";
import { LinkModal, SidebarProgressStats } from "components/core";
import { DeleteModuleModal, SidebarLeadSelect, SidebarMembersSelect } from "components/modules";
import ProgressChart from "components/core/sidebar/progress-chart";
// components
// ui
import { CustomSelect, Loader, ProgressBar } from "components/ui";
import { CustomMenu, CustomSelect, Loader, ProgressBar } from "components/ui";
// helpers
import { renderDateFormat, renderShortNumericDateFormat, timeAgo } from "helpers/date-time.helper";
import { copyTextToClipboard } from "helpers/string.helper";
import { renderDateFormat, renderShortDate, timeAgo } from "helpers/date-time.helper";
import { capitalizeFirstLetter, copyTextToClipboard } from "helpers/string.helper";
import { groupBy } from "helpers/array.helper";
// types
import { IIssue, IModule, ModuleIssueResponse, ModuleLink, UserAuth } from "types";
@@ -115,14 +114,10 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({
const handleCreateLink = async (formData: ModuleLink) => {
if (!workspaceSlug || !projectId || !moduleId) return;
const previousLinks = module?.link_module.map((l) => ({ title: l.title, url: l.url }));
const payload: Partial<IModule> = {
links_list: [...(previousLinks ?? []), formData],
};
const payload = { metadata: {}, ...formData };
await modulesService
.patchModule(workspaceSlug as string, projectId as string, moduleId as string, payload)
.createModuleLink(workspaceSlug as string, projectId as string, moduleId as string, payload)
.then((res) => {
mutate(MODULE_DETAILS(moduleId as string));
})
@@ -135,11 +130,44 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({
});
};
const handleDeleteLink = (linkId: string) => {
if (!module) return;
const handleDeleteLink = async (linkId: string) => {
if (!workspaceSlug || !projectId || !module) return;
const updatedLinks = module.link_module.filter((l) => l.id !== linkId);
submitChanges({ links_list: updatedLinks });
mutate<IModule>(
MODULE_DETAILS(module.id),
(prevData) => ({ ...(prevData as IModule), link_module: updatedLinks }),
false
);
await modulesService
.deleteModuleLink(workspaceSlug as string, projectId as string, module.id, linkId)
.then((res) => {
mutate(MODULE_DETAILS(module.id));
})
.catch((err) => {
console.log(err);
});
};
const handleCopyText = () => {
const originURL =
typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
copyTextToClipboard(`${workspaceSlug}/projects/${projectId}/modules/${module?.id}`)
.then(() => {
setToastAlert({
type: "success",
title: "Module link copied to clipboard",
});
})
.catch(() => {
setToastAlert({
type: "error",
title: "Some error occurred",
});
});
};
useEffect(() => {
@@ -153,6 +181,10 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({
const isStartValid = new Date(`${module?.start_date}`) <= new Date();
const isEndValid = new Date(`${module?.target_date}`) >= new Date(`${module?.start_date}`);
const progressPercentage = moduleIssues
? Math.round((groupedIssues.completed.length / moduleIssues?.length) * 100)
: null;
return (
<>
<LinkModal
@@ -168,225 +200,312 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({
<div
className={`fixed top-0 ${
isOpen ? "right-0" : "-right-[24rem]"
} z-20 h-full w-[24rem] overflow-y-auto border-l bg-gray-50 p-5 duration-300`}
} z-20 h-full w-[24rem] overflow-y-auto border-l bg-gray-50 py-5 duration-300`}
>
{module ? (
<>
<div className="flex gap-1 text-sm my-2">
<div className="flex items-center ">
<Controller
control={control}
name="status"
render={({ field: { value } }) => (
<CustomSelect
label={
<span
className={`flex items-center gap-1 text-left capitalize p-1 text-xs h-full w-full text-gray-900`}
<div className="flex flex-col items-start justify-center">
<div className="flex gap-2.5 px-7 text-sm">
<div className="flex items-center ">
<Controller
control={control}
name="status"
render={({ field: { value } }) => (
<CustomSelect
customButton={
<span
className={`flex cursor-pointer items-center rounded border-[0.5px] border-gray-200 bg-gray-100 px-2.5 py-1.5 text-center text-sm capitalize text-gray-800 `}
>
{capitalizeFirstLetter(`${watch("status")}`)}
</span>
}
value={value}
onChange={(value: any) => {
submitChanges({ status: value });
}}
>
{MODULE_STATUS.map((option) => (
<CustomSelect.Option key={option.value} value={option.value}>
<span className="text-xs">{option.label}</span>
</CustomSelect.Option>
))}
</CustomSelect>
)}
/>
</div>
<div className="relative flex h-full w-52 items-center justify-center gap-2 text-sm text-gray-800">
<Popover className="flex h-full items-center justify-center rounded-lg">
{({ open }) => (
<>
<Popover.Button
className={`group flex h-full items-center gap-1 rounded border-[0.5px] border-gray-200 bg-gray-100 px-2.5 py-1.5 text-gray-800 ${
open ? "bg-gray-100" : ""
}`}
>
<Squares2X2Icon className="h-4 w-4 flex-shrink-0" />
{watch("status")}
</span>
}
value={value}
onChange={(value: any) => {
submitChanges({ status: value });
}}
>
{MODULE_STATUS.map((option) => (
<CustomSelect.Option key={option.value} value={option.value}>
<span className="text-xs">{option.label}</span>
</CustomSelect.Option>
))}
</CustomSelect>
)}
/>
</div>
<div className="flex justify-center items-center gap-2 rounded-md border bg-transparent h-full p-2 px-4 text-xs font-medium text-gray-900 hover:bg-gray-100 hover:text-gray-900 focus:outline-none">
<Popover className="flex justify-center items-center relative rounded-lg">
{({ open }) => (
<>
<Popover.Button
className={`group flex items-center ${open ? "bg-gray-100" : ""}`}
>
<CalendarDaysIcon className="h-4 w-4 flex-shrink-0 mr-2" />
<span>
{renderShortNumericDateFormat(`${module?.start_date}`)
? renderShortNumericDateFormat(`${module?.start_date}`)
: "N/A"}
</span>
</Popover.Button>
<CalendarDaysIcon className="h-3 w-3" />
<span>{renderShortDate(new Date(`${module.start_date}`))}</span>
</Popover.Button>
<Transition
as={React.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 top-10 -left-10 z-20 transform overflow-hidden">
<DatePicker
selected={startDateRange}
onChange={(date) => {
submitChanges({
start_date: renderDateFormat(date),
});
setStartDateRange(date);
}}
selectsStart
startDate={startDateRange}
endDate={endDateRange}
inline
/>
</Popover.Panel>
</Transition>
</>
)}
</Popover>
<Popover className="flex justify-center items-center relative rounded-lg">
{({ open }) => (
<>
<Popover.Button
className={`group flex items-center ${open ? "bg-gray-100" : ""}`}
>
<span>
-{" "}
{renderShortNumericDateFormat(`${module?.target_date}`)
? renderShortNumericDateFormat(`${module?.target_date}`)
: "N/A"}
</span>
</Popover.Button>
<Transition
as={React.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 top-10 -right-5 z-20 transform overflow-hidden">
<DatePicker
selected={startDateRange}
onChange={(date) => {
submitChanges({
start_date: renderDateFormat(date),
});
setStartDateRange(date);
}}
selectsStart
startDate={startDateRange}
endDate={endDateRange}
maxDate={endDateRange}
shouldCloseOnSelect
inline
/>
</Popover.Panel>
</Transition>
</>
)}
</Popover>
<span>
<ArrowLongRightIcon className="h-3 w-3" />
</span>
<Popover className="flex h-full items-center justify-center rounded-lg">
{({ open }) => (
<>
<Popover.Button
className={`group flex items-center gap-1 rounded border-[0.5px] border-gray-200 bg-gray-100 px-2.5 py-1.5 text-gray-800 ${
open ? "bg-gray-100" : ""
}`}
>
<CalendarDaysIcon className="h-3 w-3 " />
<Transition
as={React.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 top-10 -right-20 z-20 transform overflow-hidden">
<DatePicker
selected={endDateRange}
onChange={(date) => {
submitChanges({
target_date: renderDateFormat(date),
});
setEndDateRange(date);
}}
selectsEnd
startDate={startDateRange}
endDate={endDateRange}
minDate={startDateRange}
inline
/>
</Popover.Panel>
</Transition>
</>
)}
</Popover>
<span>{renderShortDate(new Date(`${module?.target_date}`))}</span>
</Popover.Button>
<Transition
as={React.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 top-10 -right-5 z-20 transform overflow-hidden">
<DatePicker
selected={endDateRange}
onChange={(date) => {
submitChanges({
target_date: renderDateFormat(date),
});
setEndDateRange(date);
}}
selectsEnd
startDate={startDateRange}
endDate={endDateRange}
// minDate={startDateRange}
inline
/>
</Popover.Panel>
</Transition>
</>
)}
</Popover>
</div>
</div>
</div>
<div className="flex items-center justify-between pb-3">
<h4 className="text-sm font-medium">{module.name}</h4>
<div className="flex flex-wrap items-center gap-2">
<button
type="button"
className="rounded-md border p-2 shadow-sm duration-300 hover:bg-gray-100 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500"
onClick={() =>
copyTextToClipboard(
`https://app.plane.so/${workspaceSlug}/projects/${projectId}/modules/${module.id}`
)
.then(() => {
setToastAlert({
type: "success",
title: "Module link copied to clipboard",
});
})
.catch(() => {
setToastAlert({
type: "error",
title: "Some error occurred",
});
})
}
>
<LinkIcon className="h-3.5 w-3.5" />
</button>
<button
type="button"
className="rounded-md border border-red-500 p-2 text-red-500 shadow-sm duration-300 hover:bg-red-50 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500"
onClick={() => setModuleDeleteModal(true)}
>
<TrashIcon className="h-3.5 w-3.5" />
</button>
</div>
</div>
<div className="divide-y-2 divide-gray-100 text-xs">
<div className="py-1">
<SidebarLeadSelect
control={control}
submitChanges={submitChanges}
lead={module.lead_detail}
/>
<SidebarMembersSelect control={control} submitChanges={submitChanges} />
<div className="flex flex-wrap items-center py-2">
<div className="flex items-center gap-x-2 text-sm sm:basis-1/2">
<ChartPieIcon className="h-4 w-4 flex-shrink-0" />
<p>Progress</p>
<div className="flex flex-col gap-6 px-7 py-6">
<div className="flex flex-col items-start justify-start gap-2 ">
<div className="flex items-center justify-start gap-2 ">
<h4 className="text-xl font-semibold text-gray-900">{module.name}</h4>
<CustomMenu width="lg" ellipsis>
<CustomMenu.MenuItem onClick={handleCopyText}>
<span className="flex items-center justify-start gap-2 text-gray-800">
<DocumentDuplicateIcon className="h-4 w-4" />
<span>Copy Link</span>
</span>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem onClick={() => setModuleDeleteModal(true)}>
<span className="flex items-center justify-start gap-2 text-gray-800">
<TrashIcon className="h-4 w-4" />
<span>Delete</span>
</span>
</CustomMenu.MenuItem>
</CustomMenu>
</div>
<div className="flex items-center gap-2 sm:basis-1/2">
<div className="grid flex-shrink-0 place-items-center">
<span className="whitespace-normal text-sm leading-5 text-black">
{module.description}
</span>
</div>
<div className="flex flex-col gap-4 text-sm">
<Controller
control={control}
name="lead"
render={({ field: { value } }) => (
<SidebarLeadSelect
value={value}
onChange={(val: string) => {
submitChanges({ lead: value });
}}
/>
)}
/>
<Controller
control={control}
name="members_list"
render={({ field: { value } }) => (
<SidebarMembersSelect
value={value}
onChange={(val: string[]) => {
submitChanges({ members_list: val });
}}
/>
)}
/>
<div className="flex items-center justify-start gap-1">
<div className="flex w-40 items-center justify-start gap-2">
<ChartPieIcon className="h-5 w-5 text-gray-400" />
<span>Progress</span>
</div>
<div className="flex items-center gap-2.5 text-gray-800">
<span className="h-4 w-4">
<ProgressBar
value={groupedIssues.completed.length}
maxValue={moduleIssues?.length}
/>
</span>
{groupedIssues.completed.length}/{moduleIssues?.length}
</div>
{groupedIssues.completed.length}/{moduleIssues?.length}
</div>
</div>
</div>
<div className="py-1">
<div className="flex items-center justify-between gap-2">
<h4>Links</h4>
<button
type="button"
className="grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-gray-100"
onClick={() => setModuleLinkModal(true)}
>
<PlusIcon className="h-4 w-4" />
</button>
</div>
<div className="mt-2 space-y-2">
{module.link_module && module.link_module.length > 0 ? (
<LinksList
links={module.link_module}
handleDeleteLink={handleDeleteLink}
userAuth={userAuth}
/>
) : null}
</div>
</div>
</div>
<div className="flex flex-col items-center justify-center w-full gap-2">
{isStartValid && isEndValid ? (
<ProgressChart
issues={issues}
start={module?.start_date ?? ""}
end={module?.target_date ?? ""}
/>
) : (
""
)}
{issues.length > 0 ? (
<SidebarProgressStats issues={issues} groupedIssues={groupedIssues} />
) : (
""
)}
<div className="flex w-full flex-col items-center justify-start gap-2 border-t border-gray-300 px-7 py-6 ">
<Disclosure>
{({ open }) => (
<div
className={`relative flex h-full w-full flex-col ${open ? "" : "flex-row"}`}
>
<div className="flex w-full items-center justify-between gap-2 ">
<div className="flex items-center justify-start gap-2 text-sm">
<span className="font-medium text-gray-500">Progress</span>
{!open && moduleIssues && progressPercentage ? (
<span className="rounded bg-[#09A953]/10 px-1.5 py-0.5 text-xs text-[#09A953]">
{progressPercentage ? `${progressPercentage}%` : ""}
</span>
) : (
""
)}
</div>
<Disclosure.Button>
<ChevronDownIcon
className={`h-3 w-3 ${open ? "rotate-180 transform" : ""}`}
aria-hidden="true"
/>
</Disclosure.Button>
</div>
<Transition show={open}>
<Disclosure.Panel>
{isStartValid && isEndValid && moduleIssues ? (
<div className=" h-full w-full py-4">
<div className="flex items-start justify-between gap-4 py-2 text-xs">
<div className="flex items-center gap-1">
<span>
<DocumentIcon className="h-3 w-3 text-gray-500" />
</span>
<span>
Pending Issues -{" "}
{moduleIssues?.length - groupedIssues.completed.length}{" "}
</span>
</div>
<div className="flex items-center gap-3 text-gray-900">
<div className="flex items-center justify-center gap-1">
<span className="h-2.5 w-2.5 rounded-full bg-[#A9BBD0]" />
<span>Ideal</span>
</div>
<div className="flex items-center justify-center gap-1">
<span className="h-2.5 w-2.5 rounded-full bg-[#4C8FFF]" />
<span>Current</span>
</div>
</div>
</div>
<div className="relative h-40 w-80">
<ProgressChart
issues={issues}
start={module?.start_date ?? ""}
end={module?.target_date ?? ""}
/>
</div>
</div>
) : (
""
)}
</Disclosure.Panel>
</Transition>
</div>
)}
</Disclosure>
</div>
<div className="flex w-full flex-col items-center justify-start gap-2 border-t border-gray-300 px-7 py-6 ">
<Disclosure>
{({ open }) => (
<div
className={`relative flex h-full w-full flex-col ${open ? "" : "flex-row"}`}
>
<div className="flex w-full items-center justify-between gap-2 ">
<div className="flex items-center justify-start gap-2 text-sm">
<span className="font-medium text-gray-500">Other Information</span>
</div>
<Disclosure.Button>
<ChevronDownIcon
className={`h-3 w-3 ${open ? "rotate-180 transform" : ""}`}
aria-hidden="true"
/>
</Disclosure.Button>
</div>
<Transition show={open}>
<Disclosure.Panel>
{issues.length > 0 ? (
<>
<div className=" h-full w-full py-4">
<SidebarProgressStats
issues={issues}
groupedIssues={groupedIssues}
setModuleLinkModal={setModuleLinkModal}
handleDeleteLink={handleDeleteLink}
userAuth={userAuth}
module={module}
/>
</div>
</>
) : (
""
)}
</Disclosure.Panel>
</Transition>
</div>
)}
</Disclosure>
</div>
</>
) : (

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