mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
fix: Implementing mobx, refactoring service layer and rewriting components (#2441)
* chore: kanban refactoring * chore: Implemented new kanaban board UX and implemented draggable using react beautiful dnd * chore: updated yarn lock * chore: updated the store for issues and issue filters * chore: resolved build error * chore: created filters and updated the issue filters, display_filter and display_properties in mobx and components * chore: implemented filters for issues * chore: UI theming updates * chore: handled single and multi select in filter cards * chore: implemented filters and views in kanaban * chore: updating filters, display_filter and display properties * chore: filter, layout, display filters, extra filters and display properties render validation * chore: clean up and resolved import warnings * chore: type check * chore: renamed gantt key to gantt_chart * chore: filter render UI and Functionality implementation * chore: filter empty state handling in issue filter selection * Implementing list view * chore: kanban drag drop logic * filtering * chore: store setup * chore: handled build issues * chore: store setup * user filter * chore: store setup * chore: store fixes and static data setup * chore: store setup for build fixes * fix: merge conflicts (#2231) * chore: dynamic position dropdown (#2138) * chore: dynamic position state dropdown for issue view * style: state select dropdown styling * fix: state icon attribute names * chore: state select dynamic dropdown * chore: member select dynamic dropdown * chore: label select dynamic dropdown * chore: priority select dynamic dropdown * chore: label select dropdown improvement * refactor: state dropdown location * chore: dropdown improvement and code refactor * chore: dynamic dropdown hook type added --------- Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com> * fix: fields not getting selected in the create issue form (#2212) * fix: hydration error and draft issue workflow * fix: build error * fix: properties getting de-selected after create, module & cycle not getting auto-select on the form * fix: display layout, props being updated directly * chore: sub issues count in individual issue (#2221) * fix: service imports * chore: rename csv service file --------- Co-authored-by: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Co-authored-by: Dakshesh Jain <65905942+dakshesh14@users.noreply.github.com> Co-authored-by: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com> * chore: store fixes * chore: update issue detail store to handle peek overview (#2237) * chore: dynamic position dropdown (#2138) * chore: dynamic position state dropdown for issue view * style: state select dropdown styling * fix: state icon attribute names * chore: state select dynamic dropdown * chore: member select dynamic dropdown * chore: label select dynamic dropdown * chore: priority select dynamic dropdown * chore: label select dropdown improvement * refactor: state dropdown location * chore: dropdown improvement and code refactor * chore: dynamic dropdown hook type added --------- Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com> * fix: fields not getting selected in the create issue form (#2212) * fix: hydration error and draft issue workflow * fix: build error * fix: properties getting de-selected after create, module & cycle not getting auto-select on the form * fix: display layout, props being updated directly * chore: sub issues count in individual issue (#2221) * Implemented nested issues in the sub issues section in issue detail page (#2233) * feat: subissues infinte level * feat: updated UI for sub issues * feat: subissues new ui and nested sub issues in issue detail * chore: removed repeated code * refactor: product updates modal layout (#2225) * fix: handle no issues in custom analytics (#2226) * fix: activity label color (#2227) * fix: profile issues layout switch (#2228) * chore: update service imports * chore: update issue detail store to handle peek overview --------- Co-authored-by: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Co-authored-by: Dakshesh Jain <65905942+dakshesh14@users.noreply.github.com> Co-authored-by: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com> Co-authored-by: guru_sainath <gurusainath007@gmail.com> * chore: minor fixes * workspace project fixes * feat: project issues topbar (#2256) * chore: project issues topbar * style: theming and minor UI fixes * refactor: file structure * chore: layout wise authorization added * style: filter dropdowns * chore: add fetch keys * chore: minor fixes * chore: filters dropdown (#2260) * chore: project issues topbar * style: theming and minor UI fixes * refactor: file structure * chore: layout wise authorization added * style: filter dropdowns * chore: add fetch keys * feat: search option for filters * fix: sticky headers * chore: sub_group_by section added * fix: leave project fixes * refactor: project card component refactor * Implemented swimlanes and kanban view (#2262) * chore: issue store for kanban and calendar * chore: updated ui for kanba and swimlanes * chore: yarn.lock updated * fix: computed filters logic * chore: added sub_group_by in params and handled sub-group-by render error in display filter's * fix: ui package setup and project update form refactor * fix: ui package setup * fix: minor ui fixes * dev: calendar view layout revamp (#2293) * dev: calendar view init * chore: new render logic * chore: implement calendar view * chore: calendar view * refactor: calendar payload * chore: remove active month logic from backend * chore: setup new store for calendar * refactor: issues fetching structure * chore: months dropdown * chore: modify request query params for calendar layout * refactor: remove console logs and add comments * chore: removed demo m-store routes * cycles changes * chore: issues grouped kanban and swimlanes UI and functionality (#2294) * chore: updated the all the group_by and sub_group_by UI and functionality render in kanban * chore: kanban sorting in mobx and ui updates * chore: ui changes and drag and drop functionality changes in kanban * chore: issues count render in kanban default and swimlanes * chore: Added icons to the group_by and sub_group_by in kanban and swimlanes * refactor: filter components, constants and helper functions (#2297) * refactor: filters and display filters to accept handlers as props * refactor: filters and display filters folder structure * refactor: change issue layout options constant structure * chore: display filters validations * chore: view less filters functionality * fix: display filters validation * refactor: wrap functions around useCallback * chore: start and target date filter options added * refactor: query params generator function * fix: query params generator function * dev: gantt chart implementation using MobX (#2302) * dev: fetch project gantt issues using mobx * chore: handle group by options in the kanban layout * dev: spreadsheet layout implementation using MobX (#2306) * dev: implement spreadsheet view using mobx * refactor: remove console logs and props * chore: refactoring cycles list * feat: adding additional ui components * dev: applied filters list implementation using MobX (#2325) * dev: applied filters list UI * fix: filter item height * chore: remove unnecessary classes * fix: params generator * fix: cycles views list and board * fix: cycles list rendering fixes * fix: layout fixes * refactor: filter components (#2359) * fix: calendar layout dividers * refactor: filter selection components * fix: dropdown closing after selection * refactor: filters components * chore: issue properties for list and kanban layouts and implemented estimates in project store (#2363) * chore: issue properties for state, priorit, labels and members * feat: implemented assignee, labels properties * fix: implemented estimates in project store and issue properties * chore: staer_date and due_date and validation properties in kanban * chore: filters import conflict * dev: setup module and module filter store (#2364) * dev: implement module issues using mobx store * dev: module filter store setup * chore: module store crud operations * chore: issue list layout (#2367) * chore: merge develop (#2388) * fix: build erros * chore: cycles, modules store integration, list and kanban layouts and updated kanban logic (#2399) * chore: cycle, cycle-issue, cycle-filters, cycle-kanban, cycle layout setup * chore: cycles kanban and list view store * chore: cycles, modules kanban and list, kanban view store * refactor: change naming convention (#2383) * fix:auth layer revamp * chore: Implemented list and kanban views in project modules (#2402) * chore: updated kanban logic in project cycles and modules * chore: updated list and kanban in module * dev: implement global views using MobX (#2404) * fix: selfhosted fixes (#2154) * fix: selfhosted fixes * fix: updated env example * chore: dynamic position dropdown (#2138) * chore: dynamic position state dropdown for issue view * style: state select dropdown styling * fix: state icon attribute names * chore: state select dynamic dropdown * chore: member select dynamic dropdown * chore: label select dynamic dropdown * chore: priority select dynamic dropdown * chore: label select dropdown improvement * refactor: state dropdown location * chore: dropdown improvement and code refactor * chore: dynamic dropdown hook type added --------- Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com> * fix: fields not getting selected in the create issue form (#2212) * fix: hydration error and draft issue workflow * fix: build error * fix: properties getting de-selected after create, module & cycle not getting auto-select on the form * fix: display layout, props being updated directly * chore: sub issues count in individual issue (#2221) * Implemented nested issues in the sub issues section in issue detail page (#2233) * feat: subissues infinte level * feat: updated UI for sub issues * feat: subissues new ui and nested sub issues in issue detail * chore: removed repeated code * refactor: product updates modal layout (#2225) * fix: handle no issues in custom analytics (#2226) * fix: activity label color (#2227) * fix: profile issues layout switch (#2228) * fix: issues resolved in sub issues (#2238) * fix: aws region name (#2234) * chore: updated docker naming conventions (#2239) * naming convention changes * dev: update docker-compose-hub in consistent with docker-compose * dev: updated docker container name --------- Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com> * chore: added state and priority order in workspace user profile (#2241) * fix: changed priority from None to none (#2229) * fix: cycle and module stats when issues are archived (#2185) * fix: cycle and module stats when issues are archived * fix: added draft filter --------- Co-authored-by: NarayanBavisetti <narayan3119@gmail.com> * feat: quick add (#2240) * feat: quick add * style: made text color muted * chore: added epoch in draft (#2244) * chore: added epoch in draft * chore: removed extra spaces * fix: resolved pending issue graph in analytics, user wishes in dashboard, and typo in projects list (#2247) * style: settings page improvement (#2211) * style: settings page improvement * style: toggle switch styling --------- Co-authored-by: Anmol Singh Bhatia <asb@Anmols-MacBook-Pro.local> * chore: changed priority props in workspace and project (#2253) * fix: bug fix related to fetching dropdown options for the profile issue (#2246) * fix: sub issue state and member select build error (#2254) * rename view to layout (#2255) Co-authored-by: Your Name <you@example.com> * fix: bug fixes and ui improvement (#2250) * dev: remove auto filter endpoint * feat: quick-add placement in spreadsheet and gantt (#2259) * feat: sticking quick-add at the bottom of the screen fix: opening create issue modal instead of quick-add in draft-issues, my-issue and profile page * fix: build error due to dynamic import * fix: draft issue delete not working (#2249) * fix: draft issue not deleting, project can't be changed in draft issue modal * fix: removed mutation for view where draft issues are not shown * fix: inline create issue for draft issue * fix: clearing data from localstorage on discard click * feat: Add peek overview in sub issues and updated UI for empty states. (#2263) * chore: add tooltip to show full time on activity logs (#2235) * fix: issue automation iterable error (#2208) * fix: n+1 queries for cycle list and project member endpoints (#2257) * [fix] nginx continuously rewriting and reloading on index page of spaces app (#2236) * chore: shifted index page to /home route * chore: added rewrite logic, to rewrite index to /home * chore: routed home to login route as login page * chore: updated nginx config to route to login * chore: updated path for home * dev: migration for 0.13 (#2266) * dev: updated migrations * dev: migration for 0.13 * dev: re-split migrations into two different files (#2268) * dev: split issue activity migration separate files * dev: resplit migrations into two different files * dev: changed the batch size * chore: udpate date filters to support dynamic options * fix: bugs in quick-add and draft issues (#2269) * fix: 'Last Drafted Issue' making sidebar look weird on collapsed * feat: scroll to the bottom when issue is created * fix: 'Add Issue' button overlapping issue card in spreadsheet view * fix: wrong placement of quick-add in calender layout * fix: spacing for issue card in spreadsheet view * chore: add instructions to contributing guide (#2270) * chore: add instructions to contributing guide * dev: update contributing.md to use the new configuration --------- Co-authored-by: Nikhil <118773738+pablohashescobar@users.noreply.github.com> * fix: user dashboard greeting timezone (#2267) * chore: user greeting timezone * fix: group by labels not working on workspace level * feat: workspace global view, style: spreadsheet view revamp (#2273) * chore: workspace view types, services and hooks added * style: spreadsheet view revamp and code refactor * feat: workspace view * fix: build fix * chore: sidebar workspace issues redirection updated * style: gantt layout quick-add padding (#2272) * fix: 'Last Drafted Issue' making sidebar look weird on collapsed * feat: scroll to the bottom when issue is created * fix: 'Add Issue' button overlapping issue card in spreadsheet view * fix: wrong placement of quick-add in calender layout * fix: spacing for issue card in spreadsheet view * style: gantt layout quick-add padding style: removed 'State group' from draft issue * style: decrese shadow, quick-add position on calender layout, and 'add issue' sticky * style: button color * fix: block click happening while moving (#2275) * dev: refactor date filters to a single function * chore: handle calendar date range in frontend (#2277) * chore: gantt chart empty state (#2279) * chore: gantt empty state * chore: Add heading to the gantt sidebar * style: calender quick-add same width as single date (#2280) * style: calender quick-add same width as single date * style: margin bottom in quick-add in spreadsheet view * fix: quick add opening in list-layout * style: reduced margin left * chore: updated created at in draft issue (#2278) * chore: make target dates inclusive when filtering (#2276) * chore: sort order and issue props for global views (#2283) * chore: removed project filter (#2284) * fix: inbox issue deletes (#2290) * chore: views (#2288) * chore: global views order by * chore: update permissions for global views --------- Co-authored-by: NarayanBavisetti <narayan3119@gmail.com> * chore: fetch issues from previous and next month in the calendar view (#2282) * fix: issue activity estimate value bug fix (#2281) * fix: issue activity estimate value bug fix * fix: activity typo fix * fix: ui and bugs (#2289) * fix: 24 character limit on first & last name in onboarding page * fix: no option: 'Add Issue' in archive issue page * fix: in archive issue directly sending to issue detail page * fix: issue type showing in archive issue * fix: custom menu overflowing * fix: changing subscriber in filters has no effect * style: border in quick-add * fix: on onboarding member role overflowing * fix: inconsistent icons in issue detail * style: spacing, borders and shadows in quick-add * fix: custom menu truncate * fix: notifications for created by me and assigned to me (#2292) * chore: workspace view display filters and properties , code refactor (#2295) * chore: spreadsheet view context * chore: spreadsheet context provider * chore: spreadsheet view context * chore: display filters and properties added in workspace view and code refactor * fix: build error fix * chore: set sub-issue display option to false for global views --------- Co-authored-by: gurusainath <gurusainath007@gmail.com> * chore: label create error (#2299) * chore: global issues ui improvement and bug fixes (#2300) * chore: workspace view mutation fix ,bug fixes and code refactor (#2301) * chore: workspace view mutation fix ,bug fixes and code refactor * chore: update workspace view toast alert added * chore: workspace view order by removed (#2303) * dev: updated migrations for 0.13-dev (#2305) * chore: epoch migration batch size changed * chore: reoredered the migration files * dev: updated migrations for 0.13-dev * chore: added epoch field * dev: merged the migration files * fix: workspace view filters count fix (#2307) * fix: unsplash api fix (#2310) * fix: workspace view redirection fix, style: spreadsheet view shadow scroll fix (#2314) * fix: workspace view redirection fix * style: spreadsheet view scroll shadow fix * fix: update build workflow for the deploy app (#2315) * fix: workspace view add issue mutation fix (#2317) * dev: create action to sync PR changes to the repo (#2333) * fix: ui package readme added (#2334) * fix: variable name for token (#2336) * dev: update add permissions to the action (#2337) * dev: rename token variables (#2338) * fix: updated readme fixes (#2339) * dev: update sync workflow to run only when the source repo is configured (#2346) * dev: update sync workflow to run only when the source repo is configured * fix: naming convention changes --------- Co-authored-by: sriramveeraghanta <veeraghanta.sriram@gmail.com> * fix: issue relation mutation and draft issue (#2340) * fix: issue relation mutation and draft issue * fix: 'New Issue' in gantt view fix: emoji select going under * fix: profile page typo * fix: sync workflow fixes (#2365) * fix: sync job pr description escaped values fix (#2366) * Update index.tsx (#2343) Fixes #2342 * dev: update apiserver configuration files (#2348) * dev: update apiserver configuration files * dev: add email and minio redirection urls * fix: themening validation in store init. (#2350) * chore: member can change role (#2371) * chore: removed the issue draft log from my profile (#2368) * adding sync info in pr title (#2373) * chore: layout access validation and switch in plane deploy issues route (#2351) * chore: handled route validation and layout access validation in plane deploy issues * chore: impoved validation condition * show current version in the help section dropdown (#2353) * fix: table menu positioning (#2354) * fix: handle cross project issues in the sub-issues. (#2357) * fix: login process validation based on api config (#2361) * dev: configuration endpoint for frontend client (#2355) * dev: configuration endpoint for frontend clients * dev: configuration enable magic and email/password signup * dev: update unsplash keys * dev: add unsplash API and add env for magic login * fix: 404 when redirecting user clicks on Sign In button (#2349) * fix: 404 when redirecting user to login page * fix: next_path redirection not working * fix: authentication workflow update in plane deploy --------- Co-authored-by: gurusainath <gurusainath007@gmail.com> * fix: project setting member role validation (#2369) * fix: project setting member role validation * chore: opacity removed from member setting page * chore: member setting page validation * chore: project covers endpoint (#2370) * chore: project covers endpoint * dev: remove print logs * dev: formatting --------- Co-authored-by: sriramveeraghanta <veeraghanta.sriram@gmail.com> * feat: default project cover images tab on the change cover popover (#2375) * feat: default project cover images tab * chore: remove unnecessary env vars from turbo.json * chore: remove unnecessary OAuth envs (#2378) * chore: remove unnecessary oauth envs * merge conflicts resolved * fix: adding new service --------- Co-authored-by: sriramveeraghanta <veeraghanta.sriram@gmail.com> * fix: added user store variables in mobx store observable (#2380) * fix: state group icons (#2381) * fix: removed default theme setting in the index page (#2382) * fix: removed default theme setting in the index page * fix: empty space * dev: global views and workspace filters store implemented * sync CE Master to EE Develop * refactor: create update view modal * chore: static issue global views * refactor: remove old code * refactor: filters select dropdown * chore: fix calendar layout * chore: mobx store for new applied filters * chore: dded search functionality --------- Co-authored-by: Vamsi Kurama <vamsi.kurama@gmail.com> Co-authored-by: Nikhil <118773738+pablohashescobar@users.noreply.github.com> Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com> Co-authored-by: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Co-authored-by: Dakshesh Jain <65905942+dakshesh14@users.noreply.github.com> Co-authored-by: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com> Co-authored-by: guru_sainath <gurusainath007@gmail.com> Co-authored-by: NarayanBavisetti <narayan3119@gmail.com> Co-authored-by: Anmol Singh Bhatia <asb@Anmols-MacBook-Pro.local> Co-authored-by: Rhea Jain <65884341+rhea0110@users.noreply.github.com> Co-authored-by: Your Name <you@example.com> Co-authored-by: pablohashescobar <nikhilschacko@gmail.com> Co-authored-by: Henit Chobisa <chobisa.henit@gmail.com> Co-authored-by: Thomas <git@thomasync.dev> Co-authored-by: Luis Cruz <55716036+luis-cruzt@users.noreply.github.com> Co-authored-by: Manish Gupta <manish@mgupta.me> * fix: Auth fixes and Layout fixes (#2408) * fix: auth fixes and layout improvements * fix: layout fixes * fix: analytics page fixes * dev: implemented project views using MobX (#2410) * dev: implemented project views list using mobx * style: views list UI * dev: implemented view issues page using mobx * refactor: project view issues fetching * chore: plane ui library component and code refactor (#2406) * chore: swap input component with plane/ui package * chore: swap textarea component with plane/ui package * chore: swap button component with plane/ui package * chore: button component revamp * fix: button type fix * chore: secondary button revamp * chore: button props updated * chore: swap loader component with plane/ui package * fix: build error fix * chore: button component refactor * chore: code refactor * chore: swap toggle switch component with plane/ui package * chore: swap spinner component with plane/ui package * chore: swap progress bar componenet with plan/ui package * chore: code refactor * fix: gitignore fixes * fix: project card fixes * chore: ui component revamp (#2415) * chore: swap tooltip component with plane ui package * chore: swap linear progress component with plane ui package * fix: login button fix * chore: implement new worksapace wrapper for global views (#2412) * chore: implement new worksapace wrapper for global views pages * fix: merge conflicts * fix: merge conflicts * dev: add remaining layouts to cycle (#2413) * fix: workspace auth wrapper changes * chore: project card revamp and refactor (#2416) * removing dist from ui * refactor: analytics (#2419) * refactor: helper functions * chore: updated all the page headers * refactor: custom analytics * refactor: project analytics modal * refactor: folder structure, remove junk code (#2423) * refactor: folder structure * chore: ad order by target date option * refactor: remove old layout components * refactor: inbox folder structure * fix: services fixes * fix: store imports changes * fix: services export fixes * fix: services implementation fixes * fix: build issue fixes * fix: react library fixes * refactor: MobX store folder structure (#2435) * refactor: store folder structure * chore: update import statements * fix: service import errors (#2436) * fix: service imports * chore: update service imports in store * chore: fix remianing service imports * build fixes * editor ts config fixes * fix: turbo and build fixes * fix: Auth screen loading implementation * fix: build issues * fix: turbo settings for ui package --------- Co-authored-by: gurusainath <gurusainath007@gmail.com> Co-authored-by: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Co-authored-by: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Co-authored-by: Dakshesh Jain <65905942+dakshesh14@users.noreply.github.com> Co-authored-by: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com> Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com> Co-authored-by: Vamsi Kurama <vamsi.kurama@gmail.com> Co-authored-by: Nikhil <118773738+pablohashescobar@users.noreply.github.com> Co-authored-by: NarayanBavisetti <narayan3119@gmail.com> Co-authored-by: Anmol Singh Bhatia <asb@Anmols-MacBook-Pro.local> Co-authored-by: Rhea Jain <65884341+rhea0110@users.noreply.github.com> Co-authored-by: Your Name <you@example.com> Co-authored-by: pablohashescobar <nikhilschacko@gmail.com> Co-authored-by: Henit Chobisa <chobisa.henit@gmail.com> Co-authored-by: Thomas <git@thomasync.dev> Co-authored-by: Luis Cruz <55716036+luis-cruzt@users.noreply.github.com> Co-authored-by: Manish Gupta <manish@mgupta.me>
This commit is contained in:
parent
c6e021d41f
commit
d80a593520
2
.github/workflows/create-sync-pr.yml
vendored
2
.github/workflows/create-sync-pr.yml
vendored
@ -2,7 +2,7 @@ name: Create PR in Plane EE Repository to sync the changes
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
types:
|
types:
|
||||||
- closed
|
- closed
|
||||||
|
16
README.md
16
README.md
@ -39,29 +39,33 @@ Meet [Plane](https://plane.so). An open-source software development tool to mana
|
|||||||
|
|
||||||
The easiest way to get started with Plane is by creating a [Plane Cloud](https://app.plane.so) account. Plane Cloud offers a hosted solution for Plane. If you prefer to self-host Plane, please refer to our [deployment documentation](https://docs.plane.so/self-hosting).
|
The easiest way to get started with Plane is by creating a [Plane Cloud](https://app.plane.so) account. Plane Cloud offers a hosted solution for Plane. If you prefer to self-host Plane, please refer to our [deployment documentation](https://docs.plane.so/self-hosting).
|
||||||
|
|
||||||
## ⚡️ Contributors Quick Start
|
## ⚡️ Contributors Quick Start
|
||||||
|
|
||||||
### Prerequisite
|
### Prerequisite
|
||||||
|
|
||||||
Development system must have docker engine installed and running.
|
Development system must have docker engine installed and running.
|
||||||
|
|
||||||
|
### Steps
|
||||||
|
|
||||||
### Steps
|
|
||||||
Setting up local environment is extremely easy and straight forward. Follow the below step and you will be ready to contribute
|
Setting up local environment is extremely easy and straight forward. Follow the below step and you will be ready to contribute
|
||||||
|
|
||||||
1. Clone the code locally using `git clone https://github.com/makeplane/plane.git`
|
1. Clone the code locally using `git clone https://github.com/makeplane/plane.git`
|
||||||
1. Switch to the code folder `cd plane`
|
1. Switch to the code folder `cd plane`
|
||||||
1. Create your feature or fix branch you plan to work on using `git checkout -b <feature-branch-name>`
|
1. Create your feature or fix branch you plan to work on using `git checkout -b <feature-branch-name>`
|
||||||
1. Open terminal and run `./setup.sh`
|
1. Open terminal and run `./setup.sh`
|
||||||
1. Open the code on VSCode or similar equivalent IDE
|
1. Open the code on VSCode or similar equivalent IDE
|
||||||
1. Review the `.env` files available in various folders. Visit [Environment Setup](./ENV_SETUP.md) to know about various environment variables used in system
|
1. Review the `.env` files available in various folders. Visit [Environment Setup](./ENV_SETUP.md) to know about various environment variables used in system
|
||||||
1. Run the docker command to initiate various services `docker compose -f docker-compose-local.yml up -d`
|
1. Run the docker command to initiate various services `docker compose -f docker-compose-local.yml up -d`
|
||||||
|
|
||||||
You are ready to make changes to the code. Do not forget to refresh the browser (in case id does not auto-reload)
|
```bash
|
||||||
|
./setup.sh
|
||||||
|
```
|
||||||
|
|
||||||
Thats it!
|
You are ready to make changes to the code. Do not forget to refresh the browser (in case id does not auto-reload)
|
||||||
|
|
||||||
|
Thats it!
|
||||||
|
|
||||||
## 🍙 Self Hosting
|
## 🍙 Self Hosting
|
||||||
|
|
||||||
For self hosting environment setup, visit the [Self Hosting](https://docs.plane.so/self-hosting) documentation page
|
For self hosting environment setup, visit the [Self Hosting](https://docs.plane.so/self-hosting) documentation page
|
||||||
|
|
||||||
|
@ -187,7 +187,6 @@ def filter_target_date(params, filter, method):
|
|||||||
if method == "GET":
|
if method == "GET":
|
||||||
target_dates = params.get("target_date").split(",")
|
target_dates = params.get("target_date").split(",")
|
||||||
if len(target_dates) and "" not in target_dates:
|
if len(target_dates) and "" not in target_dates:
|
||||||
|
|
||||||
date_filter(filter=filter, date_term="target_date", queries=target_dates)
|
date_filter(filter=filter, date_term="target_date", queries=target_dates)
|
||||||
else:
|
else:
|
||||||
if params.get("target_date", None) and len(params.get("target_date")):
|
if params.get("target_date", None) and len(params.get("target_date")):
|
||||||
|
@ -27,7 +27,7 @@
|
|||||||
"prettier": "latest",
|
"prettier": "latest",
|
||||||
"prettier-plugin-tailwindcss": "^0.5.4",
|
"prettier-plugin-tailwindcss": "^0.5.4",
|
||||||
"tailwindcss": "^3.3.3",
|
"tailwindcss": "^3.3.3",
|
||||||
"turbo": "latest"
|
"turbo": "^1.10.14"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"@types/react": "18.2.0"
|
"@types/react": "18.2.0"
|
||||||
|
@ -1,12 +1,5 @@
|
|||||||
{
|
{
|
||||||
"extends": "tsconfig/react.json",
|
"extends": "tsconfig/react-library.json",
|
||||||
"include": [
|
"include": ["src/**/*", "index.d.ts"],
|
||||||
"src/**/*",
|
"exclude": ["dist", "build", "node_modules"]
|
||||||
"index.d.ts"
|
|
||||||
],
|
|
||||||
"exclude": [
|
|
||||||
"dist",
|
|
||||||
"build",
|
|
||||||
"node_modules"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,5 @@
|
|||||||
{
|
{
|
||||||
"extends": "tsconfig/react.json",
|
"extends": "tsconfig/react-library.json",
|
||||||
"include": [
|
"include": ["src/**/*", "index.d.ts"],
|
||||||
"src/**/*",
|
"exclude": ["dist", "build", "node_modules"]
|
||||||
"index.d.ts"
|
|
||||||
],
|
|
||||||
"exclude": [
|
|
||||||
"dist",
|
|
||||||
"build",
|
|
||||||
"node_modules"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,5 @@
|
|||||||
{
|
{
|
||||||
"extends": "tsconfig/react.json",
|
"extends": "tsconfig/react-library.json",
|
||||||
"include": [
|
"include": ["src/**/*", "index.d.ts"],
|
||||||
"src/**/*",
|
"exclude": ["dist", "build", "node_modules"]
|
||||||
"index.d.ts"
|
|
||||||
],
|
|
||||||
"exclude": [
|
|
||||||
"dist",
|
|
||||||
"build",
|
|
||||||
"node_modules"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
@ -11,8 +11,9 @@ module.exports = {
|
|||||||
"./layouts/**/*.tsx",
|
"./layouts/**/*.tsx",
|
||||||
"./pages/**/*.tsx",
|
"./pages/**/*.tsx",
|
||||||
"./ui/**/*.tsx",
|
"./ui/**/*.tsx",
|
||||||
"../packages/editor/**/*.{js,ts,jsx,tsx}"
|
"../packages/ui/**/*.{js,ts,jsx,tsx}",
|
||||||
]
|
"../packages/editor/**/*.{js,ts,jsx,tsx}",
|
||||||
|
],
|
||||||
},
|
},
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
"lib": ["ES2015", "DOM"],
|
"lib": ["ES2015", "DOM"],
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"target": "es6"
|
"target": "es6",
|
||||||
|
"sourceMap": true
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,3 +0,0 @@
|
|||||||
export const Button = () => {
|
|
||||||
return <button>button</button>;
|
|
||||||
};
|
|
@ -1,17 +0,0 @@
|
|||||||
// import * as React from "react";
|
|
||||||
// components
|
|
||||||
// export * from "./breadcrumbs";
|
|
||||||
// export * from "./button";
|
|
||||||
// export * from "./custom-listbox";
|
|
||||||
// export * from "./custom-menu";
|
|
||||||
// export * from "./custom-select";
|
|
||||||
// export * from "./empty-space";
|
|
||||||
// export * from "./header-button";
|
|
||||||
// export * from "./input";
|
|
||||||
// export * from "./loader";
|
|
||||||
// export * from "./outline-button";
|
|
||||||
// export * from "./select";
|
|
||||||
// export * from "./spinner";
|
|
||||||
// export * from "./text-area";
|
|
||||||
// export * from "./tooltip";
|
|
||||||
export * from "./button";
|
|
@ -1,23 +1,40 @@
|
|||||||
{
|
{
|
||||||
"name": "ui",
|
"name": "@plane/ui",
|
||||||
"version": "0.13.2",
|
"version": "0.0.1",
|
||||||
"main": "./index.tsx",
|
"main": "./dist/index.js",
|
||||||
"types": "./index.tsx",
|
"module": "./dist/index.mjs",
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"sideEffects": false,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"files": [
|
||||||
|
"dist/**"
|
||||||
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "eslint *.ts*"
|
"build": "tsup src/index.tsx --format esm,cjs --dts --external react",
|
||||||
|
"dev": "tsup src/index.tsx --format esm,cjs --watch --dts --external react",
|
||||||
|
"lint": "eslint src/",
|
||||||
|
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^18.0.17",
|
"@types/node": "^20.5.2",
|
||||||
"@types/react-dom": "^18.0.6",
|
"@types/react": "18.2.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.51.0",
|
"@types/react-dom": "18.2.0",
|
||||||
"classnames": "^2.3.2",
|
"classnames": "^2.3.2",
|
||||||
"eslint": "^7.32.0",
|
|
||||||
"eslint-config-custom": "*",
|
"eslint-config-custom": "*",
|
||||||
"next": "12.3.2",
|
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"tsconfig": "*",
|
"tsconfig": "*",
|
||||||
"tailwind-config-custom": "*",
|
"tailwind-config-custom": "*",
|
||||||
|
"tsup": "^5.10.1",
|
||||||
"typescript": "4.7.4"
|
"typescript": "4.7.4"
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@blueprintjs/core": "^4.16.3",
|
||||||
|
"@blueprintjs/popover2": "^1.13.3",
|
||||||
|
"@headlessui/react": "^1.7.17",
|
||||||
|
"clsx": "^2.0.0",
|
||||||
|
"next-themes": "^0.2.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
66
packages/ui/src/button/button.tsx
Normal file
66
packages/ui/src/button/button.tsx
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import {
|
||||||
|
getIconStyling,
|
||||||
|
getButtonStyling,
|
||||||
|
TButtonVariant,
|
||||||
|
TButtonSizes,
|
||||||
|
} from "./helper";
|
||||||
|
|
||||||
|
export interface ButtonProps
|
||||||
|
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
|
variant?: TButtonVariant;
|
||||||
|
size?: TButtonSizes;
|
||||||
|
className?: string;
|
||||||
|
loading?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
appendIcon?: any;
|
||||||
|
prependIcon?: any;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
|
(props, ref) => {
|
||||||
|
const {
|
||||||
|
variant = "primary",
|
||||||
|
size = "md",
|
||||||
|
className = "",
|
||||||
|
type = "button",
|
||||||
|
loading = false,
|
||||||
|
disabled = false,
|
||||||
|
prependIcon = null,
|
||||||
|
appendIcon = null,
|
||||||
|
children,
|
||||||
|
...rest
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const buttonStyle = getButtonStyling(variant, size, disabled || loading);
|
||||||
|
const buttonIconStyle = getIconStyling(size);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
ref={ref}
|
||||||
|
type={type}
|
||||||
|
className={`${buttonStyle} ${className}`}
|
||||||
|
disabled={disabled || loading}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{prependIcon && (
|
||||||
|
<div className={buttonIconStyle}>
|
||||||
|
{React.cloneElement(prependIcon, { "stroke-width": 2 })}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{children}
|
||||||
|
{appendIcon && (
|
||||||
|
<div className={buttonIconStyle}>
|
||||||
|
{React.cloneElement(appendIcon, { "stroke-width": 2 })}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
Button.displayName = "plane-ui-button";
|
||||||
|
|
||||||
|
export { Button };
|
123
packages/ui/src/button/helper.tsx
Normal file
123
packages/ui/src/button/helper.tsx
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
export type TButtonVariant =
|
||||||
|
| "primary"
|
||||||
|
| "accent-primary"
|
||||||
|
| "outline-primary"
|
||||||
|
| "neutral-primary"
|
||||||
|
| "link-primary"
|
||||||
|
| "danger"
|
||||||
|
| "accent-danger"
|
||||||
|
| "outline-danger"
|
||||||
|
| "link-danger"
|
||||||
|
| "tertiary-danger";
|
||||||
|
|
||||||
|
export type TButtonSizes = "sm" | "md" | "lg" | "xl";
|
||||||
|
|
||||||
|
export interface IButtonStyling {
|
||||||
|
[key: string]: {
|
||||||
|
default: string;
|
||||||
|
hover: string;
|
||||||
|
pressed: string;
|
||||||
|
disabled: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
enum buttonSizeStyling {
|
||||||
|
sm = `px-3 py-1.5 font-medium text-xs rounded flex items-center gap-1.5 whitespace-nowrap transition-all justify-center inline`,
|
||||||
|
md = `px-4 py-1.5 font-medium text-sm rounded flex items-center gap-1.5 whitespace-nowrap transition-all justify-center inline`,
|
||||||
|
lg = `px-5 py-2 font-medium text-sm rounded flex items-center gap-1.5 whitespace-nowrap transition-all justify-center inline`,
|
||||||
|
xl = `px-5 py-3.5 font-medium text-sm rounded flex items-center gap-1.5 whitespace-nowrap transition-all justify-center inline`,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum buttonIconStyling {
|
||||||
|
sm = "h-3 w-3 flex justify-center items-center overflow-hidden my-0.5 flex-shrink-0",
|
||||||
|
md = "h-3.5 w-3.5 flex justify-center items-center overflow-hidden my-0.5 flex-shrink-0",
|
||||||
|
lg = "h-4 w-4 flex justify-center items-center overflow-hidden my-0.5 flex-shrink-0",
|
||||||
|
xl = "h-4 w-4 flex justify-center items-center overflow-hidden my-0.5 flex-shrink-0",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const buttonStyling: IButtonStyling = {
|
||||||
|
primary: {
|
||||||
|
default: `text-white bg-custom-primary-100`,
|
||||||
|
hover: `hover:bg-custom-primary-200`,
|
||||||
|
pressed: `focus:text-custom-brand-40 focus:bg-custom-primary-200`,
|
||||||
|
disabled: `cursor-not-allowed !bg-custom-primary-60 hover:bg-custom-primary-60`,
|
||||||
|
},
|
||||||
|
"accent-primary": {
|
||||||
|
default: `bg-custom-primary-10 text-custom-primary-100`,
|
||||||
|
hover: `hover:bg-custom-primary-20 hover:text-custom-primary-200`,
|
||||||
|
pressed: `focus:bg-custom-primary-20`,
|
||||||
|
disabled: `cursor-not-allowed !text-custom-primary-60`,
|
||||||
|
},
|
||||||
|
"outline-primary": {
|
||||||
|
default: `text-custom-primary-100 bg-custom-background-100 border border-custom-primary-100`,
|
||||||
|
hover: `hover:border-custom-primary-80 hover:bg-custom-primary-10`,
|
||||||
|
pressed: `focus:text-custom-primary-80 focus:bg-custom-primary-10 focus:border-custom-primary-80`,
|
||||||
|
disabled: `cursor-not-allowed !text-custom-primary-60 !border-custom-primary-60 `,
|
||||||
|
},
|
||||||
|
"neutral-primary": {
|
||||||
|
default: `text-custom-text-200 bg-custom-background-100 border border-custom-border-200`,
|
||||||
|
hover: `hover:bg-custom-background-90`,
|
||||||
|
pressed: `focus:text-custom-text-300 focus:bg-custom-background-90`,
|
||||||
|
disabled: `cursor-not-allowed !text-custom-text-400`,
|
||||||
|
},
|
||||||
|
"link-primary": {
|
||||||
|
default: `text-custom-primary-100 bg-custom-background-100`,
|
||||||
|
hover: `hover:text-custom-primary-200`,
|
||||||
|
pressed: `focus:text-custom-primary-80 `,
|
||||||
|
disabled: `cursor-not-allowed !text-custom-primary-60`,
|
||||||
|
},
|
||||||
|
|
||||||
|
danger: {
|
||||||
|
default: `text-white bg-red-500`,
|
||||||
|
hover: ` hover:bg-red-600`,
|
||||||
|
pressed: `focus:text-red-200 focus:bg-red-600`,
|
||||||
|
disabled: `cursor-not-allowed !bg-red-300`,
|
||||||
|
},
|
||||||
|
"accent-danger": {
|
||||||
|
default: `text-red-500 bg-red-50`,
|
||||||
|
hover: `hover:text-red-600 hover:bg-red-100`,
|
||||||
|
pressed: `focus:text-red-500 focus:bg-red-100`,
|
||||||
|
disabled: `cursor-not-allowed !text-red-300`,
|
||||||
|
},
|
||||||
|
"outline-danger": {
|
||||||
|
default: `text-red-500 bg-custom-background-100 border border-red-500`,
|
||||||
|
hover: `hover:text-red-400 hover:border-red-400`,
|
||||||
|
pressed: `focus:text-red-400 focus:border-red-400`,
|
||||||
|
disabled: `cursor-not-allowed !text-red-300 !border-red-300`,
|
||||||
|
},
|
||||||
|
"link-danger": {
|
||||||
|
default: `text-red-500 bg-custom-background-100`,
|
||||||
|
hover: `hover:text-red-400`,
|
||||||
|
pressed: `focus:text-red-400`,
|
||||||
|
disabled: `cursor-not-allowed !text-red-300`,
|
||||||
|
},
|
||||||
|
"tertiary-danger": {
|
||||||
|
default: `text-red-500 bg-custom-background-100 border border-red-200`,
|
||||||
|
hover: `hover:bg-red-50 hover:border-red-300`,
|
||||||
|
pressed: `focus:text-red-400`,
|
||||||
|
disabled: `cursor-not-allowed !text-red-300`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getButtonStyling = (
|
||||||
|
variant: TButtonVariant,
|
||||||
|
size: TButtonSizes,
|
||||||
|
disabled: boolean = false
|
||||||
|
): string => {
|
||||||
|
let _variant: string = ``;
|
||||||
|
const currentVariant = buttonStyling[variant];
|
||||||
|
|
||||||
|
_variant = `${currentVariant.default} ${
|
||||||
|
disabled ? currentVariant.disabled : currentVariant.hover
|
||||||
|
} ${currentVariant.pressed}`;
|
||||||
|
|
||||||
|
let _size: string = ``;
|
||||||
|
if (size) _size = buttonSizeStyling[size];
|
||||||
|
return `${_variant} ${_size}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getIconStyling = (size: TButtonSizes): string => {
|
||||||
|
let icon: string = ``;
|
||||||
|
if (size) icon = buttonIconStyling[size];
|
||||||
|
return icon;
|
||||||
|
};
|
2
packages/ui/src/button/index.tsx
Normal file
2
packages/ui/src/button/index.tsx
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./button";
|
||||||
|
export * from "./toggle-switch";
|
49
packages/ui/src/button/toggle-switch.tsx
Normal file
49
packages/ui/src/button/toggle-switch.tsx
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { Switch } from "@headlessui/react";
|
||||||
|
|
||||||
|
interface IToggleSwitchProps {
|
||||||
|
value: boolean;
|
||||||
|
onChange: (value: boolean) => void;
|
||||||
|
label?: string;
|
||||||
|
size?: "sm" | "md" | "lg";
|
||||||
|
disabled?: boolean;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ToggleSwitch: React.FC<IToggleSwitchProps> = (props) => {
|
||||||
|
const { value, onChange, label, size = "sm", disabled, className } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Switch
|
||||||
|
checked={value}
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={onChange}
|
||||||
|
className={`relative flex-shrink-0 inline-flex ${
|
||||||
|
size === "sm" ? "h-4 w-6" : size === "md" ? "h-5 w-8" : "h-6 w-10"
|
||||||
|
} flex-shrink-0 cursor-pointer rounded-full border border-custom-border-200 transition-colors duration-200 ease-in-out focus:outline-none ${
|
||||||
|
value ? "bg-custom-primary-100" : "bg-gray-700"
|
||||||
|
} ${className || ""} ${disabled ? "cursor-not-allowed" : ""}`}
|
||||||
|
>
|
||||||
|
<span className="sr-only">{label}</span>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className={`self-center inline-block ${
|
||||||
|
size === "sm" ? "h-2 w-2" : size === "md" ? "h-3 w-3" : "h-4 w-4"
|
||||||
|
} transform rounded-full shadow ring-0 transition duration-200 ease-in-out ${
|
||||||
|
value
|
||||||
|
? (size === "sm"
|
||||||
|
? "translate-x-3"
|
||||||
|
: size === "md"
|
||||||
|
? "translate-x-4"
|
||||||
|
: "translate-x-5") + " bg-white"
|
||||||
|
: "translate-x-0.5 bg-custom-background-90"
|
||||||
|
} ${disabled ? "cursor-not-allowed" : ""}`}
|
||||||
|
/>
|
||||||
|
</Switch>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ToggleSwitch.displayName = "plane-ui-toggle-switch";
|
||||||
|
|
||||||
|
export { ToggleSwitch };
|
2
packages/ui/src/form-fields/index.tsx
Normal file
2
packages/ui/src/form-fields/index.tsx
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./input";
|
||||||
|
export * from "./textarea";
|
49
packages/ui/src/form-fields/input.tsx
Normal file
49
packages/ui/src/form-fields/input.tsx
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
export interface InputProps
|
||||||
|
extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||||
|
mode?: "primary" | "transparent" | "true-transparent";
|
||||||
|
inputSize?: "sm" | "md";
|
||||||
|
hasError?: boolean;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Input = React.forwardRef<HTMLInputElement, InputProps>((props, ref) => {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
type,
|
||||||
|
name,
|
||||||
|
mode = "primary",
|
||||||
|
inputSize = "sm",
|
||||||
|
hasError = false,
|
||||||
|
className = "",
|
||||||
|
...rest
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
id={id}
|
||||||
|
ref={ref}
|
||||||
|
type={type}
|
||||||
|
name={name}
|
||||||
|
className={`block rounded-md bg-transparent text-sm focus:outline-none placeholder-custom-text-400 ${
|
||||||
|
mode === "primary"
|
||||||
|
? "rounded-md border border-custom-border-200"
|
||||||
|
: mode === "transparent"
|
||||||
|
? "rounded border-none bg-transparent ring-0 transition-all focus:ring-1 focus:ring-custom-primary"
|
||||||
|
: mode === "true-transparent"
|
||||||
|
? "rounded border-none bg-transparent ring-0"
|
||||||
|
: ""
|
||||||
|
} ${hasError ? "border-red-500" : ""} ${
|
||||||
|
hasError && mode === "primary" ? "bg-red-500/20" : ""
|
||||||
|
} ${
|
||||||
|
inputSize === "sm" ? "px-3 py-2" : inputSize === "md" ? "p-3" : ""
|
||||||
|
} ${className}`}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
Input.displayName = "form-input-field";
|
||||||
|
|
||||||
|
export { Input };
|
69
packages/ui/src/form-fields/textarea.tsx
Normal file
69
packages/ui/src/form-fields/textarea.tsx
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
export interface TextAreaProps
|
||||||
|
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
|
||||||
|
mode?: "primary" | "transparent";
|
||||||
|
hasError?: boolean;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Updates the height of a <textarea> when the value changes.
|
||||||
|
const useAutoSizeTextArea = (
|
||||||
|
textAreaRef: HTMLTextAreaElement | null,
|
||||||
|
value: any
|
||||||
|
) => {
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (textAreaRef) {
|
||||||
|
// We need to reset the height momentarily to get the correct scrollHeight for the textarea
|
||||||
|
textAreaRef.style.height = "0px";
|
||||||
|
const scrollHeight = textAreaRef.scrollHeight;
|
||||||
|
|
||||||
|
// We then set the height directly, outside of the render loop
|
||||||
|
// Trying to set this with state or a ref will product an incorrect value.
|
||||||
|
textAreaRef.style.height = scrollHeight + "px";
|
||||||
|
}
|
||||||
|
}, [textAreaRef, value]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const TextArea = React.forwardRef<HTMLTextAreaElement, TextAreaProps>(
|
||||||
|
(props, ref) => {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
value = "",
|
||||||
|
rows = 1,
|
||||||
|
cols = 1,
|
||||||
|
mode = "primary",
|
||||||
|
hasError = false,
|
||||||
|
className = "",
|
||||||
|
...rest
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const textAreaRef = React.useRef<any>(ref);
|
||||||
|
|
||||||
|
ref && useAutoSizeTextArea(textAreaRef?.current, value);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<textarea
|
||||||
|
id={id}
|
||||||
|
name={name}
|
||||||
|
ref={textAreaRef}
|
||||||
|
value={value}
|
||||||
|
rows={rows}
|
||||||
|
cols={cols}
|
||||||
|
className={`no-scrollbar w-full bg-transparent placeholder-custom-text-400 px-3 py-2 outline-none ${
|
||||||
|
mode === "primary"
|
||||||
|
? "rounded-md border border-custom-border-200"
|
||||||
|
: mode === "transparent"
|
||||||
|
? "rounded border-none bg-transparent ring-0 transition-all focus:ring-1 focus:ring-theme"
|
||||||
|
: ""
|
||||||
|
} ${hasError ? "border-red-500" : ""} ${
|
||||||
|
hasError && mode === "primary" ? "bg-red-100" : ""
|
||||||
|
} ${className}`}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export { TextArea };
|
6
packages/ui/src/index.tsx
Normal file
6
packages/ui/src/index.tsx
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export * from "./button";
|
||||||
|
export * from "./form-fields";
|
||||||
|
export * from "./progress";
|
||||||
|
export * from "./spinners";
|
||||||
|
export * from "./loader";
|
||||||
|
export * from "./tooltip";
|
30
packages/ui/src/loader.tsx
Normal file
30
packages/ui/src/loader.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Loader = ({ children, className = "" }: Props) => (
|
||||||
|
<div className={`${className} animate-pulse`} role="status">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
type ItemProps = {
|
||||||
|
height?: string;
|
||||||
|
width?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Item: React.FC<ItemProps> = ({ height = "auto", width = "auto" }) => (
|
||||||
|
<div
|
||||||
|
className="rounded-md bg-custom-background-80"
|
||||||
|
style={{ height: height, width: width }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
Loader.Item = Item;
|
||||||
|
|
||||||
|
Loader.displayName = "plane-ui-loader";
|
||||||
|
|
||||||
|
export { Loader };
|
3
packages/ui/src/progress/index.tsx
Normal file
3
packages/ui/src/progress/index.tsx
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from "./radial-progress";
|
||||||
|
export * from "./progress-bar";
|
||||||
|
export * from "./linear-progress-indicator";
|
44
packages/ui/src/progress/linear-progress-indicator.tsx
Normal file
44
packages/ui/src/progress/linear-progress-indicator.tsx
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Tooltip } from "../tooltip";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
data: any;
|
||||||
|
noTooltip?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LinearProgressIndicator: React.FC<Props> = ({
|
||||||
|
data,
|
||||||
|
noTooltip = false,
|
||||||
|
}) => {
|
||||||
|
const total = data.reduce((acc: any, cur: any) => acc + cur.value, 0);
|
||||||
|
let progress = 0;
|
||||||
|
|
||||||
|
const bars = data.map((item: any) => {
|
||||||
|
const width = `${(item.value / total) * 100}%`;
|
||||||
|
const style = {
|
||||||
|
width,
|
||||||
|
backgroundColor: item.color,
|
||||||
|
};
|
||||||
|
progress += item.value;
|
||||||
|
if (noTooltip) return <div style={style} />;
|
||||||
|
else
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
key={item.id}
|
||||||
|
tooltipContent={`${item.name} ${Math.round(item.value)}%`}
|
||||||
|
>
|
||||||
|
<div style={style} />
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-1 w-full items-center justify-between gap-1">
|
||||||
|
{total === 0 ? (
|
||||||
|
<div className="flex h-full w-full gap-1 bg-neutral-500">{bars}</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex h-full w-full gap-1">{bars}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
77
packages/ui/src/progress/progress-bar.tsx
Normal file
77
packages/ui/src/progress/progress-bar.tsx
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
maxValue?: number;
|
||||||
|
value?: number;
|
||||||
|
radius?: number;
|
||||||
|
strokeWidth?: number;
|
||||||
|
activeStrokeColor?: string;
|
||||||
|
inactiveStrokeColor?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ProgressBar: React.FC<Props> = ({
|
||||||
|
maxValue = 0,
|
||||||
|
value = 0,
|
||||||
|
radius = 8,
|
||||||
|
strokeWidth = 2,
|
||||||
|
activeStrokeColor = "#3e98c7",
|
||||||
|
inactiveStrokeColor = "#ddd",
|
||||||
|
}) => {
|
||||||
|
// PIE Calc Fn
|
||||||
|
const generatePie = (value: any) => {
|
||||||
|
const x = radius - Math.cos((2 * Math.PI) / (100 / value)) * radius;
|
||||||
|
const y = radius + Math.sin((2 * Math.PI) / (100 / value)) * radius;
|
||||||
|
const long = value <= 50 ? 0 : 1;
|
||||||
|
const d = `M${radius} ${radius} L${radius} ${0} A${radius} ${radius} 0 ${long} 1 ${y} ${x} Z`;
|
||||||
|
|
||||||
|
return d;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- PIE Area Calc --------
|
||||||
|
const calculatePieValue = (numberOfBars: any) => {
|
||||||
|
const angle = 360 / numberOfBars;
|
||||||
|
const pieValue = Math.floor(angle / 4);
|
||||||
|
return pieValue < 1 ? 1 : Math.floor(angle / 4);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- PIE Render Fn --------
|
||||||
|
const renderPie = (i: any) => {
|
||||||
|
const DIRECTION = -1;
|
||||||
|
// Rotation Calc
|
||||||
|
const primaryRotationAngle = (maxValue - 1) * (360 / maxValue);
|
||||||
|
const rotationAngle =
|
||||||
|
-1 * DIRECTION * primaryRotationAngle +
|
||||||
|
i * DIRECTION * primaryRotationAngle;
|
||||||
|
const rotationTransformation = `rotate(${rotationAngle}, ${radius}, ${radius})`;
|
||||||
|
const pieValue = calculatePieValue(maxValue);
|
||||||
|
const dValue = generatePie(pieValue);
|
||||||
|
const fillColor =
|
||||||
|
value > 0 && i <= value ? activeStrokeColor : inactiveStrokeColor;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<path
|
||||||
|
style={{ opacity: i === 0 ? 0 : 1 }}
|
||||||
|
key={i}
|
||||||
|
d={dValue}
|
||||||
|
fill={fillColor}
|
||||||
|
transform={rotationTransformation}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// combining the Pies
|
||||||
|
const renderOuterCircle = () =>
|
||||||
|
[...Array(maxValue + 1)].map((e, i) => renderPie(i));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg width={radius * 2} height={radius * 2}>
|
||||||
|
{renderOuterCircle()}
|
||||||
|
<circle
|
||||||
|
r={radius - strokeWidth}
|
||||||
|
cx={radius}
|
||||||
|
cy={radius}
|
||||||
|
className="progress-bar"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
45
packages/ui/src/progress/radial-progress.tsx
Normal file
45
packages/ui/src/progress/radial-progress.tsx
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import React, { useState, useEffect, FC } from "react";
|
||||||
|
|
||||||
|
interface IRadialProgressBar {
|
||||||
|
progress: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RadialProgressBar: FC<IRadialProgressBar> = (props) => {
|
||||||
|
const { progress } = props;
|
||||||
|
const [circumference, setCircumference] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const radius = 40;
|
||||||
|
const circumference = 2 * Math.PI * radius;
|
||||||
|
setCircumference(circumference);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const progressOffset = ((100 - progress) / 100) * circumference;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative h-4 w-4">
|
||||||
|
<svg className="absolute top-0 left-0" viewBox="0 0 100 100">
|
||||||
|
<circle
|
||||||
|
className={"stroke-current opacity-10"}
|
||||||
|
cx="50"
|
||||||
|
cy="50"
|
||||||
|
r="40"
|
||||||
|
strokeWidth="12"
|
||||||
|
fill="none"
|
||||||
|
strokeDasharray={`${circumference} ${circumference}`}
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
className={`stroke-current`}
|
||||||
|
cx="50"
|
||||||
|
cy="50"
|
||||||
|
r="40"
|
||||||
|
strokeWidth="12"
|
||||||
|
fill="none"
|
||||||
|
strokeDasharray={`${circumference} ${circumference}`}
|
||||||
|
strokeDashoffset={progressOffset}
|
||||||
|
transform="rotate(-90 50 50)"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
23
packages/ui/src/spinners/circular-spinner.tsx
Normal file
23
packages/ui/src/spinners/circular-spinner.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
export const Spinner: React.FC = () => (
|
||||||
|
<div role="status">
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
className="mr-2 h-8 w-8 animate-spin fill-blue-600 text-custom-text-200"
|
||||||
|
viewBox="0 0 100 101"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
|
||||||
|
fill="currentFill"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span className="sr-only">Loading...</span>
|
||||||
|
</div>
|
||||||
|
);
|
1
packages/ui/src/spinners/index.ts
Normal file
1
packages/ui/src/spinners/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./circular-spinner";
|
1
packages/ui/src/tooltip/index.tsx
Normal file
1
packages/ui/src/tooltip/index.tsx
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./tooltip";
|
86
packages/ui/src/tooltip/tooltip.tsx
Normal file
86
packages/ui/src/tooltip/tooltip.tsx
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
// next-themes
|
||||||
|
import { useTheme } from "next-themes";
|
||||||
|
import { Tooltip2 } from "@blueprintjs/popover2";
|
||||||
|
|
||||||
|
export type TPosition =
|
||||||
|
| "top"
|
||||||
|
| "right"
|
||||||
|
| "bottom"
|
||||||
|
| "left"
|
||||||
|
| "auto"
|
||||||
|
| "auto-end"
|
||||||
|
| "auto-start"
|
||||||
|
| "bottom-left"
|
||||||
|
| "bottom-right"
|
||||||
|
| "left-bottom"
|
||||||
|
| "left-top"
|
||||||
|
| "right-bottom"
|
||||||
|
| "right-top"
|
||||||
|
| "top-left"
|
||||||
|
| "top-right";
|
||||||
|
|
||||||
|
interface ITooltipProps {
|
||||||
|
tooltipHeading?: string;
|
||||||
|
tooltipContent: string | React.ReactNode;
|
||||||
|
position?: TPosition;
|
||||||
|
children: JSX.Element;
|
||||||
|
disabled?: boolean;
|
||||||
|
className?: string;
|
||||||
|
openDelay?: number;
|
||||||
|
closeDelay?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Tooltip: React.FC<ITooltipProps> = ({
|
||||||
|
tooltipHeading,
|
||||||
|
tooltipContent,
|
||||||
|
position = "top",
|
||||||
|
children,
|
||||||
|
disabled = false,
|
||||||
|
className = "",
|
||||||
|
openDelay = 200,
|
||||||
|
closeDelay,
|
||||||
|
}) => {
|
||||||
|
const { theme } = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip2
|
||||||
|
disabled={disabled}
|
||||||
|
hoverOpenDelay={openDelay}
|
||||||
|
hoverCloseDelay={closeDelay}
|
||||||
|
content={
|
||||||
|
<div
|
||||||
|
className={`relative z-50 max-w-xs gap-1 rounded-md p-2 text-xs shadow-md ${
|
||||||
|
theme === "custom"
|
||||||
|
? "bg-custom-background-100 text-custom-text-200"
|
||||||
|
: "bg-black text-gray-400"
|
||||||
|
} break-words overflow-hidden ${className}`}
|
||||||
|
>
|
||||||
|
{tooltipHeading && (
|
||||||
|
<h5
|
||||||
|
className={`font-medium ${
|
||||||
|
theme === "custom" ? "text-custom-text-100" : "text-white"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{tooltipHeading}
|
||||||
|
</h5>
|
||||||
|
)}
|
||||||
|
{tooltipContent}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
position={position}
|
||||||
|
renderTarget={({
|
||||||
|
isOpen: isTooltipOpen,
|
||||||
|
ref: eleReference,
|
||||||
|
...tooltipProps
|
||||||
|
}) =>
|
||||||
|
React.cloneElement(children, {
|
||||||
|
ref: eleReference,
|
||||||
|
...tooltipProps,
|
||||||
|
...children.props,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -1,5 +1,8 @@
|
|||||||
{
|
{
|
||||||
"extends": "tsconfig/react-library.json",
|
"extends": "tsconfig/react-library.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"jsx": "react"
|
||||||
|
},
|
||||||
"include": ["."],
|
"include": ["."],
|
||||||
"exclude": ["dist", "build", "node_modules"]
|
"exclude": ["dist", "build", "node_modules"]
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,8 @@ import userService from "services/user.service";
|
|||||||
// hooks
|
// hooks
|
||||||
// import useToast from "hooks/use-toast";
|
// import useToast from "hooks/use-toast";
|
||||||
// ui
|
// ui
|
||||||
import { Input, PrimaryButton, SecondaryButton } from "components/ui";
|
import { Input } from "components/ui";
|
||||||
|
import { Button } from "@plane/ui";
|
||||||
// types
|
// types
|
||||||
type Props = {
|
type Props = {
|
||||||
setIsResettingPassword: React.Dispatch<React.SetStateAction<boolean>>;
|
setIsResettingPassword: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
@ -77,12 +78,12 @@ export const EmailResetPasswordForm: React.FC<Props> = ({ setIsResettingPassword
|
|||||||
{errors.email && <div className="text-sm text-red-500">{errors.email.message}</div>}
|
{errors.email && <div className="text-sm text-red-500">{errors.email.message}</div>}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-5 flex flex-col-reverse sm:flex-row items-center gap-2">
|
<div className="mt-5 flex flex-col-reverse sm:flex-row items-center gap-2">
|
||||||
<SecondaryButton className="w-full text-center h-[46px]" onClick={() => setIsResettingPassword(false)}>
|
<Button variant="neutral-primary" className="w-full" onClick={() => setIsResettingPassword(false)}>
|
||||||
Go Back
|
Go Back
|
||||||
</SecondaryButton>
|
</Button>
|
||||||
<PrimaryButton type="submit" className="w-full text-center h-[46px]" loading={isSubmitting}>
|
<Button variant="primary" className="w-full" type="submit" loading={isSubmitting}>
|
||||||
{isSubmitting ? "Sending link..." : "Send reset link"}
|
{isSubmitting ? "Sending link..." : "Send reset link"}
|
||||||
</PrimaryButton>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
35
turbo.json
35
turbo.json
@ -3,9 +3,7 @@
|
|||||||
"globalEnv": [
|
"globalEnv": [
|
||||||
"NEXT_PUBLIC_API_BASE_URL",
|
"NEXT_PUBLIC_API_BASE_URL",
|
||||||
"NEXT_PUBLIC_DEPLOY_URL",
|
"NEXT_PUBLIC_DEPLOY_URL",
|
||||||
"API_BASE_URL",
|
|
||||||
"NEXT_PUBLIC_SENTRY_DSN",
|
"NEXT_PUBLIC_SENTRY_DSN",
|
||||||
"SENTRY_AUTH_TOKEN",
|
|
||||||
"NEXT_PUBLIC_SENTRY_ENVIRONMENT",
|
"NEXT_PUBLIC_SENTRY_ENVIRONMENT",
|
||||||
"NEXT_PUBLIC_GITHUB_APP_NAME",
|
"NEXT_PUBLIC_GITHUB_APP_NAME",
|
||||||
"NEXT_PUBLIC_ENABLE_SENTRY",
|
"NEXT_PUBLIC_ENABLE_SENTRY",
|
||||||
@ -28,20 +26,16 @@
|
|||||||
],
|
],
|
||||||
"pipeline": {
|
"pipeline": {
|
||||||
"build": {
|
"build": {
|
||||||
"dependsOn": [
|
"dependsOn": ["^build"],
|
||||||
"^build"
|
"outputs": [".next/**", "dist/**"]
|
||||||
],
|
|
||||||
"outputs": [
|
|
||||||
".next/**",
|
|
||||||
"dist/**"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"web#develop": {
|
"web#develop": {
|
||||||
"cache": false,
|
"cache": false,
|
||||||
"persistent": true,
|
"persistent": true,
|
||||||
"dependsOn": [
|
"dependsOn": [
|
||||||
"@plane/lite-text-editor#build",
|
"@plane/lite-text-editor#build",
|
||||||
"@plane/rich-text-editor#build"
|
"@plane/rich-text-editor#build",
|
||||||
|
"@plane/ui#build"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"space#develop": {
|
"space#develop": {
|
||||||
@ -49,39 +43,36 @@
|
|||||||
"persistent": true,
|
"persistent": true,
|
||||||
"dependsOn": [
|
"dependsOn": [
|
||||||
"@plane/lite-text-editor#build",
|
"@plane/lite-text-editor#build",
|
||||||
"@plane/rich-text-editor#build"
|
"@plane/rich-text-editor#build",
|
||||||
|
"@plane/ui#build"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"web#build": {
|
"web#build": {
|
||||||
"cache": true,
|
"cache": true,
|
||||||
"dependsOn": [
|
"dependsOn": [
|
||||||
"@plane/lite-text-editor#build",
|
"@plane/lite-text-editor#build",
|
||||||
"@plane/rich-text-editor#build"
|
"@plane/rich-text-editor#build",
|
||||||
|
"@plane/ui#build"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"space#build": {
|
"space#build": {
|
||||||
"cache": true,
|
"cache": true,
|
||||||
"dependsOn": [
|
"dependsOn": [
|
||||||
"@plane/lite-text-editor#build",
|
"@plane/lite-text-editor#build",
|
||||||
"@plane/rich-text-editor#build"
|
"@plane/rich-text-editor#build",
|
||||||
|
"@plane/ui#build"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@plane/lite-text-editor#build": {
|
"@plane/lite-text-editor#build": {
|
||||||
"cache": true,
|
"cache": true,
|
||||||
"dependsOn": [
|
"dependsOn": ["@plane/editor-core#build"]
|
||||||
"@plane/editor-core#build"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"@plane/rich-text-editor#build": {
|
"@plane/rich-text-editor#build": {
|
||||||
"cache": true,
|
"cache": true,
|
||||||
"dependsOn": [
|
"dependsOn": ["@plane/editor-core#build"]
|
||||||
"@plane/editor-core#build"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"test": {
|
"test": {
|
||||||
"dependsOn": [
|
"dependsOn": ["^build"],
|
||||||
"^build"
|
|
||||||
],
|
|
||||||
"outputs": []
|
"outputs": []
|
||||||
},
|
},
|
||||||
"lint": {
|
"lint": {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"printWidth": 100,
|
"printWidth": 120,
|
||||||
"tabWidth": 2,
|
"tabWidth": 2,
|
||||||
"trailingComma": "es5"
|
"trailingComma": "es5"
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
// ui
|
// ui
|
||||||
import { CheckCircleIcon } from "@heroicons/react/20/solid";
|
import { Button, Input } from "@plane/ui";
|
||||||
import { Input, PrimaryButton, SecondaryButton } from "components/ui";
|
|
||||||
// services
|
// services
|
||||||
import authenticationService from "services/authentication.service";
|
import { AuthService } from "services/auth.service";
|
||||||
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
import useTimer from "hooks/use-timer";
|
import useTimer from "hooks/use-timer";
|
||||||
// icons
|
|
||||||
|
|
||||||
// types
|
// types
|
||||||
type EmailCodeFormValues = {
|
type EmailCodeFormValues = {
|
||||||
@ -16,6 +15,8 @@ type EmailCodeFormValues = {
|
|||||||
token?: string;
|
token?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const authService = new AuthService();
|
||||||
|
|
||||||
export const EmailCodeForm = ({ handleSignIn }: any) => {
|
export const EmailCodeForm = ({ handleSignIn }: any) => {
|
||||||
const [codeSent, setCodeSent] = useState(false);
|
const [codeSent, setCodeSent] = useState(false);
|
||||||
const [codeResent, setCodeResent] = useState(false);
|
const [codeResent, setCodeResent] = useState(false);
|
||||||
@ -27,8 +28,8 @@ export const EmailCodeForm = ({ handleSignIn }: any) => {
|
|||||||
const { timer: resendCodeTimer, setTimer: setResendCodeTimer } = useTimer();
|
const { timer: resendCodeTimer, setTimer: setResendCodeTimer } = useTimer();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
|
control,
|
||||||
setError,
|
setError,
|
||||||
setValue,
|
setValue,
|
||||||
getValues,
|
getValues,
|
||||||
@ -44,12 +45,11 @@ export const EmailCodeForm = ({ handleSignIn }: any) => {
|
|||||||
reValidateMode: "onChange",
|
reValidateMode: "onChange",
|
||||||
});
|
});
|
||||||
|
|
||||||
const isResendDisabled =
|
const isResendDisabled = resendCodeTimer > 0 || isCodeResending || isSubmitting || errorResendingCode;
|
||||||
resendCodeTimer > 0 || isCodeResending || isSubmitting || errorResendingCode;
|
|
||||||
|
|
||||||
const onSubmit = async ({ email }: EmailCodeFormValues) => {
|
const onSubmit = async ({ email }: EmailCodeFormValues) => {
|
||||||
setErrorResendingCode(false);
|
setErrorResendingCode(false);
|
||||||
await authenticationService
|
await authService
|
||||||
.emailCode({ email })
|
.emailCode({ email })
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
setValue("key", res.key);
|
setValue("key", res.key);
|
||||||
@ -67,7 +67,7 @@ export const EmailCodeForm = ({ handleSignIn }: any) => {
|
|||||||
|
|
||||||
const handleSignin = async (formData: EmailCodeFormValues) => {
|
const handleSignin = async (formData: EmailCodeFormValues) => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
await authenticationService
|
await authService
|
||||||
.magicSignIn(formData)
|
.magicSignIn(formData)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
handleSignIn(response);
|
handleSignIn(response);
|
||||||
@ -109,6 +109,7 @@ export const EmailCodeForm = ({ handleSignIn }: any) => {
|
|||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener("keydown", submitForm);
|
window.removeEventListener("keydown", submitForm);
|
||||||
};
|
};
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [handleSubmit, codeSent]);
|
}, [handleSubmit, codeSent]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -122,44 +123,58 @@ export const EmailCodeForm = ({ handleSignIn }: any) => {
|
|||||||
)}
|
)}
|
||||||
<form className="space-y-4 mt-10 sm:w-[360px] mx-auto">
|
<form className="space-y-4 mt-10 sm:w-[360px] mx-auto">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Input
|
<Controller
|
||||||
id="email"
|
control={control}
|
||||||
type="email"
|
|
||||||
name="email"
|
name="email"
|
||||||
register={register}
|
rules={{
|
||||||
validations={{
|
|
||||||
required: "Email address is required",
|
required: "Email address is required",
|
||||||
validate: (value) =>
|
validate: (value) =>
|
||||||
/^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test(
|
/^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test(
|
||||||
value
|
value
|
||||||
) || "Email address is not valid",
|
) || "Email address is not valid",
|
||||||
}}
|
}}
|
||||||
error={errors.email}
|
render={({ field: { value, onChange, ref } }) => (
|
||||||
placeholder="Enter your email address..."
|
<Input
|
||||||
className="border-custom-border-300 h-[46px]"
|
id="email"
|
||||||
|
name="email"
|
||||||
|
type="email"
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
ref={ref}
|
||||||
|
hasError={Boolean(errors.email)}
|
||||||
|
placeholder="Enter your email address..."
|
||||||
|
className="border-custom-border-300 h-[46px] w-full"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{codeSent && (
|
{codeSent && (
|
||||||
<>
|
<>
|
||||||
<Input
|
<Controller
|
||||||
id="token"
|
control={control}
|
||||||
type="token"
|
|
||||||
name="token"
|
name="token"
|
||||||
register={register}
|
rules={{
|
||||||
validations={{
|
|
||||||
required: "Code is required",
|
required: "Code is required",
|
||||||
}}
|
}}
|
||||||
error={errors.token}
|
render={({ field: { value, onChange, ref } }) => (
|
||||||
placeholder="Enter code..."
|
<Input
|
||||||
className="border-custom-border-300 h-[46px]"
|
id="token"
|
||||||
|
name="token"
|
||||||
|
type="token"
|
||||||
|
value={value ?? ""}
|
||||||
|
onChange={onChange}
|
||||||
|
ref={ref}
|
||||||
|
hasError={Boolean(errors.token)}
|
||||||
|
placeholder="Enter code..."
|
||||||
|
className="border-custom-border-300 h-[46px] w-full"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`flex w-full justify-end text-xs outline-none ${
|
className={`flex w-full justify-end text-xs outline-none ${
|
||||||
isResendDisabled
|
isResendDisabled ? "cursor-default text-custom-text-200" : "cursor-pointer text-custom-primary-100"
|
||||||
? "cursor-default text-custom-text-200"
|
|
||||||
: "cursor-pointer text-custom-primary-100"
|
|
||||||
} `}
|
} `}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsCodeResending(true);
|
setIsCodeResending(true);
|
||||||
@ -184,20 +199,22 @@ export const EmailCodeForm = ({ handleSignIn }: any) => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{codeSent ? (
|
{codeSent ? (
|
||||||
<PrimaryButton
|
<Button
|
||||||
|
variant="primary"
|
||||||
type="submit"
|
type="submit"
|
||||||
className="w-full text-center h-[46px]"
|
className="w-full"
|
||||||
size="md"
|
size="md"
|
||||||
onClick={handleSubmit(handleSignin)}
|
onClick={handleSubmit(handleSignin)}
|
||||||
disabled={!isValid && isDirty}
|
disabled={!isValid && isDirty}
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
>
|
>
|
||||||
{isLoading ? "Signing in..." : "Sign in"}
|
{isLoading ? "Signing in..." : "Sign in"}
|
||||||
</PrimaryButton>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<PrimaryButton
|
<Button
|
||||||
className="w-full text-center h-[46px]"
|
variant="primary"
|
||||||
size="md"
|
className="w-full"
|
||||||
|
size="xl"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleSubmit(onSubmit)().then(() => {
|
handleSubmit(onSubmit)().then(() => {
|
||||||
setResendCodeTimer(30);
|
setResendCodeTimer(30);
|
||||||
@ -207,7 +224,7 @@ export const EmailCodeForm = ({ handleSignIn }: any) => {
|
|||||||
loading={isSubmitting}
|
loading={isSubmitting}
|
||||||
>
|
>
|
||||||
{isSubmitting ? "Sending code..." : "Send sign in code"}
|
{isSubmitting ? "Sending code..." : "Send sign in code"}
|
||||||
</PrimaryButton>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</form>
|
</form>
|
||||||
</>
|
</>
|
||||||
|
69
web/components/account/email-forgot-password-form.tsx
Normal file
69
web/components/account/email-forgot-password-form.tsx
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import { FC } from "react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { useForm, Controller } from "react-hook-form";
|
||||||
|
// ui
|
||||||
|
import { Input, Button } from "@plane/ui";
|
||||||
|
|
||||||
|
export interface EmailForgotPasswordFormValues {
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IEmailForgotPasswordForm {
|
||||||
|
onSubmit: (formValues: any) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EmailForgotPasswordForm: FC<IEmailForgotPasswordForm> = (props) => {
|
||||||
|
const { onSubmit } = props;
|
||||||
|
// router
|
||||||
|
const router = useRouter();
|
||||||
|
// form data
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
formState: { errors, isSubmitting },
|
||||||
|
} = useForm<EmailForgotPasswordFormValues>({
|
||||||
|
defaultValues: {
|
||||||
|
email: "",
|
||||||
|
},
|
||||||
|
mode: "onChange",
|
||||||
|
reValidateMode: "onChange",
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form className="space-y-4 mt-10 w-full sm:w-[360px] mx-auto" onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="email"
|
||||||
|
rules={{
|
||||||
|
required: "Email address is required",
|
||||||
|
validate: (value) =>
|
||||||
|
/^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test(
|
||||||
|
value
|
||||||
|
) || "Email address is not valid",
|
||||||
|
}}
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<Input
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
name="email"
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
hasError={Boolean(errors.email)}
|
||||||
|
placeholder="Enter registered email address.."
|
||||||
|
className="border-custom-border-300 h-[46px]"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mt-5 flex flex-col-reverse sm:flex-row items-center gap-2">
|
||||||
|
<Button className="w-full text-center h-[46px]" onClick={() => router.push("/")}>
|
||||||
|
Go Back
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" className="w-full text-center h-[46px]" loading={isSubmitting}>
|
||||||
|
{isSubmitting ? "Sending link..." : "Send reset link"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
@ -1,24 +1,26 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm, Controller } from "react-hook-form";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
// ui
|
// ui
|
||||||
import { Input, PrimaryButton } from "components/ui";
|
import { Input, Button } from "@plane/ui";
|
||||||
// types
|
|
||||||
type EmailPasswordFormValues = {
|
export interface EmailPasswordFormValues {
|
||||||
email: string;
|
email: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
medium?: string;
|
medium?: string;
|
||||||
};
|
}
|
||||||
|
|
||||||
type Props = {
|
export interface IEmailPasswordForm {
|
||||||
onSubmit: (formData: EmailPasswordFormValues) => Promise<void>;
|
onSubmit: (formData: EmailPasswordFormValues) => Promise<void>;
|
||||||
setIsResettingPassword: (value: boolean) => void;
|
}
|
||||||
};
|
|
||||||
|
|
||||||
export const EmailPasswordForm: React.FC<Props> = (props) => {
|
export const EmailPasswordForm: React.FC<IEmailPasswordForm> = (props) => {
|
||||||
const { onSubmit, setIsResettingPassword } = props;
|
const { onSubmit } = props;
|
||||||
|
// router
|
||||||
|
const router = useRouter();
|
||||||
// form info
|
// form info
|
||||||
const {
|
const {
|
||||||
register,
|
control,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
formState: { errors, isSubmitting, isValid, isDirty },
|
formState: { errors, isSubmitting, isValid, isDirty },
|
||||||
} = useForm<EmailPasswordFormValues>({
|
} = useForm<EmailPasswordFormValues>({
|
||||||
@ -33,60 +35,80 @@ export const EmailPasswordForm: React.FC<Props> = (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<form
|
<form className="space-y-4 mt-10 w-full sm:w-[360px] mx-auto" onSubmit={handleSubmit(onSubmit)}>
|
||||||
className="space-y-4 mt-10 w-full sm:w-[360px] mx-auto"
|
|
||||||
onSubmit={handleSubmit(onSubmit)}
|
|
||||||
>
|
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Input
|
<Controller
|
||||||
id="email"
|
control={control}
|
||||||
type="email"
|
|
||||||
name="email"
|
name="email"
|
||||||
register={register}
|
rules={{
|
||||||
validations={{
|
|
||||||
required: "Email address is required",
|
required: "Email address is required",
|
||||||
validate: (value) =>
|
validate: (value) =>
|
||||||
/^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test(
|
/^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test(
|
||||||
value
|
value
|
||||||
) || "Email address is not valid",
|
) || "Email address is not valid",
|
||||||
}}
|
}}
|
||||||
error={errors.email}
|
render={({ field: { value, onChange } }) => (
|
||||||
placeholder="Enter your email address..."
|
<Input
|
||||||
className="border-custom-border-300 h-[46px]"
|
id="email"
|
||||||
|
type="email"
|
||||||
|
name="email"
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
hasError={Boolean(errors.email)}
|
||||||
|
placeholder="Enter your email address..."
|
||||||
|
className="border-custom-border-300 h-[46px]"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Input
|
<Controller
|
||||||
id="password"
|
control={control}
|
||||||
type="password"
|
|
||||||
name="password"
|
name="password"
|
||||||
register={register}
|
rules={{
|
||||||
validations={{
|
|
||||||
required: "Password is required",
|
required: "Password is required",
|
||||||
}}
|
}}
|
||||||
error={errors.password}
|
render={({ field: { value, onChange } }) => (
|
||||||
placeholder="Enter your password..."
|
<Input
|
||||||
className="border-custom-border-300 h-[46px]"
|
id="password"
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
hasError={Boolean(errors.password)}
|
||||||
|
placeholder="Enter your password..."
|
||||||
|
className="border-custom-border-300 h-[46px]"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right text-xs">
|
<div className="text-right text-xs">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setIsResettingPassword(true)}
|
onClick={() => router.push("/accounts/forgot-password")}
|
||||||
className="text-custom-text-200 hover:text-custom-primary-100"
|
className="text-custom-text-200 hover:text-custom-primary-100"
|
||||||
>
|
>
|
||||||
Forgot your password?
|
Forgot your password?
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<PrimaryButton
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="w-full text-center h-[46px]"
|
className="w-full text-center h-[46px]"
|
||||||
disabled={!isValid && isDirty}
|
disabled={!isValid && isDirty}
|
||||||
loading={isSubmitting}
|
loading={isSubmitting}
|
||||||
>
|
>
|
||||||
{isSubmitting ? "Signing in..." : "Sign in"}
|
{isSubmitting ? "Signing in..." : "Sign in"}
|
||||||
</PrimaryButton>
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => router.push("/accounts/sign-up")}
|
||||||
|
className="text-custom-text-200 hover:text-custom-primary-100"
|
||||||
|
>
|
||||||
|
{"Don't have an account? Sign Up"}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</>
|
</>
|
||||||
|
@ -1,97 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
|
|
||||||
// react hook form
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
// services
|
|
||||||
import userService from "services/user.service";
|
|
||||||
// hooks
|
|
||||||
import useToast from "hooks/use-toast";
|
|
||||||
// ui
|
|
||||||
import { Input, PrimaryButton, SecondaryButton } from "components/ui";
|
|
||||||
// types
|
|
||||||
type Props = {
|
|
||||||
setIsResettingPassword: React.Dispatch<React.SetStateAction<boolean>>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const EmailResetPasswordForm: React.FC<Props> = ({ setIsResettingPassword }) => {
|
|
||||||
const { setToastAlert } = useToast();
|
|
||||||
|
|
||||||
const {
|
|
||||||
register,
|
|
||||||
handleSubmit,
|
|
||||||
formState: { errors, isSubmitting },
|
|
||||||
} = useForm({
|
|
||||||
defaultValues: {
|
|
||||||
email: "",
|
|
||||||
},
|
|
||||||
mode: "onChange",
|
|
||||||
reValidateMode: "onChange",
|
|
||||||
});
|
|
||||||
|
|
||||||
const forgotPassword = async (formData: any) => {
|
|
||||||
const payload = {
|
|
||||||
email: formData.email,
|
|
||||||
};
|
|
||||||
|
|
||||||
await userService
|
|
||||||
.forgotPassword(payload)
|
|
||||||
.then(() =>
|
|
||||||
setToastAlert({
|
|
||||||
type: "success",
|
|
||||||
title: "Success!",
|
|
||||||
message: "Password reset link has been sent to your email address.",
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.catch((err) => {
|
|
||||||
if (err.status === 400)
|
|
||||||
setToastAlert({
|
|
||||||
type: "error",
|
|
||||||
title: "Error!",
|
|
||||||
message: "Please check the Email ID entered.",
|
|
||||||
});
|
|
||||||
else
|
|
||||||
setToastAlert({
|
|
||||||
type: "error",
|
|
||||||
title: "Error!",
|
|
||||||
message: "Something went wrong. Please try again.",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form
|
|
||||||
className="space-y-4 mt-10 w-full sm:w-[360px] mx-auto"
|
|
||||||
onSubmit={handleSubmit(forgotPassword)}
|
|
||||||
>
|
|
||||||
<div className="space-y-1">
|
|
||||||
<Input
|
|
||||||
id="email"
|
|
||||||
type="email"
|
|
||||||
name="email"
|
|
||||||
register={register}
|
|
||||||
validations={{
|
|
||||||
required: "Email address is required",
|
|
||||||
validate: (value) =>
|
|
||||||
/^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test(
|
|
||||||
value
|
|
||||||
) || "Email address is not valid",
|
|
||||||
}}
|
|
||||||
error={errors.email}
|
|
||||||
placeholder="Enter registered email address.."
|
|
||||||
className="border-custom-border-300 h-[46px]"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="mt-5 flex flex-col-reverse sm:flex-row items-center gap-2">
|
|
||||||
<SecondaryButton
|
|
||||||
className="w-full text-center h-[46px]"
|
|
||||||
onClick={() => setIsResettingPassword(false)}
|
|
||||||
>
|
|
||||||
Go Back
|
|
||||||
</SecondaryButton>
|
|
||||||
<PrimaryButton type="submit" className="w-full text-center h-[46px]" loading={isSubmitting}>
|
|
||||||
{isSubmitting ? "Sending link..." : "Send reset link"}
|
|
||||||
</PrimaryButton>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,8 +1,8 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
// ui
|
// ui
|
||||||
import { Input, PrimaryButton } from "components/ui";
|
import { Button, Input } from "@plane/ui";
|
||||||
// types
|
// types
|
||||||
type EmailPasswordFormValues = {
|
type EmailPasswordFormValues = {
|
||||||
email: string;
|
email: string;
|
||||||
@ -19,8 +19,8 @@ export const EmailSignUpForm: React.FC<Props> = (props) => {
|
|||||||
const { onSubmit } = props;
|
const { onSubmit } = props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
|
control,
|
||||||
watch,
|
watch,
|
||||||
formState: { errors, isSubmitting, isValid, isDirty },
|
formState: { errors, isSubmitting, isValid, isDirty },
|
||||||
} = useForm<EmailPasswordFormValues>({
|
} = useForm<EmailPasswordFormValues>({
|
||||||
@ -36,49 +36,60 @@ export const EmailSignUpForm: React.FC<Props> = (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<form
|
<form className="space-y-4 mt-10 w-full sm:w-[360px] mx-auto" onSubmit={handleSubmit(onSubmit)}>
|
||||||
className="space-y-4 mt-10 w-full sm:w-[360px] mx-auto"
|
|
||||||
onSubmit={handleSubmit(onSubmit)}
|
|
||||||
>
|
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Input
|
<Controller
|
||||||
id="email"
|
control={control}
|
||||||
type="email"
|
|
||||||
name="email"
|
name="email"
|
||||||
register={register}
|
rules={{
|
||||||
validations={{
|
|
||||||
required: "Email address is required",
|
required: "Email address is required",
|
||||||
validate: (value) =>
|
validate: (value) =>
|
||||||
/^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test(
|
/^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test(
|
||||||
value
|
value
|
||||||
) || "Email address is not valid",
|
) || "Email address is not valid",
|
||||||
}}
|
}}
|
||||||
error={errors.email}
|
render={({ field: { value, onChange, ref } }) => (
|
||||||
placeholder="Enter your email address..."
|
<Input
|
||||||
className="border-custom-border-300 h-[46px]"
|
id="email"
|
||||||
|
name="email"
|
||||||
|
type="email"
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
ref={ref}
|
||||||
|
hasError={Boolean(errors.email)}
|
||||||
|
placeholder="Enter your email address..."
|
||||||
|
className="border-custom-border-300 h-[46px] w-full"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Input
|
<Controller
|
||||||
id="password"
|
control={control}
|
||||||
type="password"
|
|
||||||
name="password"
|
name="password"
|
||||||
register={register}
|
rules={{
|
||||||
validations={{
|
|
||||||
required: "Password is required",
|
required: "Password is required",
|
||||||
}}
|
}}
|
||||||
error={errors.password}
|
render={({ field: { value, onChange, ref } }) => (
|
||||||
placeholder="Enter your password..."
|
<Input
|
||||||
className="border-custom-border-300 h-[46px]"
|
id="password"
|
||||||
|
name="password"
|
||||||
|
type="password"
|
||||||
|
value={value ?? ""}
|
||||||
|
onChange={onChange}
|
||||||
|
ref={ref}
|
||||||
|
hasError={Boolean(errors.password)}
|
||||||
|
placeholder="Enter your password..."
|
||||||
|
className="border-custom-border-300 h-[46px] w-full"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Input
|
<Controller
|
||||||
id="confirm_password"
|
control={control}
|
||||||
type="password"
|
|
||||||
name="confirm_password"
|
name="confirm_password"
|
||||||
register={register}
|
rules={{
|
||||||
validations={{
|
|
||||||
required: "Password is required",
|
required: "Password is required",
|
||||||
validate: (val: string) => {
|
validate: (val: string) => {
|
||||||
if (watch("password") != val) {
|
if (watch("password") != val) {
|
||||||
@ -86,27 +97,36 @@ export const EmailSignUpForm: React.FC<Props> = (props) => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
error={errors.confirm_password}
|
render={({ field: { value, onChange, ref } }) => (
|
||||||
placeholder="Confirm your password..."
|
<Input
|
||||||
className="border-custom-border-300 h-[46px]"
|
id="confirm_password"
|
||||||
|
name="confirm_password"
|
||||||
|
type="password"
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
ref={ref}
|
||||||
|
hasError={Boolean(errors.confirm_password)}
|
||||||
|
placeholder="Confirm your password..."
|
||||||
|
className="border-custom-border-300 h-[46px] w-full"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right text-xs">
|
<div className="text-right text-xs">
|
||||||
<Link href="/">
|
<Link href="/">
|
||||||
<a className="text-custom-text-200 hover:text-custom-primary-100">
|
<a className="text-custom-text-200 hover:text-custom-primary-100">Already have an account? Sign in.</a>
|
||||||
Already have an account? Sign in.
|
|
||||||
</a>
|
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<PrimaryButton
|
<Button
|
||||||
|
variant="primary"
|
||||||
type="submit"
|
type="submit"
|
||||||
className="w-full text-center h-[46px]"
|
className="w-full"
|
||||||
disabled={!isValid && isDirty}
|
disabled={!isValid && isDirty}
|
||||||
loading={isSubmitting}
|
loading={isSubmitting}
|
||||||
>
|
>
|
||||||
{isSubmitting ? "Signing up..." : "Sign up"}
|
{isSubmitting ? "Signing up..." : "Sign up"}
|
||||||
</PrimaryButton>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</>
|
</>
|
||||||
|
@ -54,11 +54,7 @@ export const GoogleLoginButton: FC<IGoogleLoginButton> = (props) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Script src="https://accounts.google.com/gsi/client" async defer onLoad={loadScript} />
|
<Script src="https://accounts.google.com/gsi/client" async defer onLoad={loadScript} />
|
||||||
<div
|
<div className="overflow-hidden rounded w-full" id="googleSignInButton" ref={googleSignInButton} />
|
||||||
className="overflow-hidden rounded w-full"
|
|
||||||
id="googleSignInButton"
|
|
||||||
ref={googleSignInButton}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
export * from "./email-code-form";
|
export * from "./email-code-form";
|
||||||
export * from "./email-password-form";
|
export * from "./email-password-form";
|
||||||
export * from "./email-reset-password-form";
|
export * from "./email-forgot-password-form";
|
||||||
export * from "./github-login-button";
|
export * from "./github-login-button";
|
||||||
export * from "./google-login";
|
export * from "./google-login";
|
||||||
export * from "./email-signup-form";
|
export * from "./email-signup-form";
|
||||||
|
@ -1,161 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
|
|
||||||
// react-hook-form
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
// headless ui
|
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
|
||||||
// services
|
|
||||||
import analyticsService from "services/analytics.service";
|
|
||||||
// hooks
|
|
||||||
import useToast from "hooks/use-toast";
|
|
||||||
// ui
|
|
||||||
import { Input, PrimaryButton, SecondaryButton, TextArea } from "components/ui";
|
|
||||||
// types
|
|
||||||
import { IAnalyticsParams, ISaveAnalyticsFormData } from "types";
|
|
||||||
|
|
||||||
// types
|
|
||||||
type Props = {
|
|
||||||
isOpen: boolean;
|
|
||||||
handleClose: () => void;
|
|
||||||
params?: IAnalyticsParams;
|
|
||||||
};
|
|
||||||
|
|
||||||
type FormValues = {
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const defaultValues: FormValues = {
|
|
||||||
name: "",
|
|
||||||
description: "",
|
|
||||||
};
|
|
||||||
|
|
||||||
export const CreateUpdateAnalyticsModal: React.FC<Props> = ({ isOpen, handleClose, params }) => {
|
|
||||||
const router = useRouter();
|
|
||||||
const { workspaceSlug } = router.query;
|
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
|
||||||
|
|
||||||
const {
|
|
||||||
register,
|
|
||||||
formState: { errors, isSubmitting },
|
|
||||||
handleSubmit,
|
|
||||||
reset,
|
|
||||||
} = useForm<FormValues>({
|
|
||||||
defaultValues,
|
|
||||||
});
|
|
||||||
|
|
||||||
const onClose = () => {
|
|
||||||
handleClose();
|
|
||||||
reset(defaultValues);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSubmit = async (formData: FormValues) => {
|
|
||||||
if (!workspaceSlug) return;
|
|
||||||
|
|
||||||
const payload: ISaveAnalyticsFormData = {
|
|
||||||
name: formData.name,
|
|
||||||
description: formData.description,
|
|
||||||
query_dict: {
|
|
||||||
x_axis: "priority",
|
|
||||||
y_axis: "issue_count",
|
|
||||||
...params,
|
|
||||||
project: params?.project ?? [],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
await analyticsService
|
|
||||||
.saveAnalytics(workspaceSlug.toString(), payload)
|
|
||||||
.then(() => {
|
|
||||||
setToastAlert({
|
|
||||||
type: "success",
|
|
||||||
title: "Success!",
|
|
||||||
message: "Analytics saved successfully.",
|
|
||||||
});
|
|
||||||
onClose();
|
|
||||||
})
|
|
||||||
.catch(() =>
|
|
||||||
setToastAlert({
|
|
||||||
type: "error",
|
|
||||||
title: "Error!",
|
|
||||||
message: "Analytics could not be saved. Please try again.",
|
|
||||||
})
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Transition.Root show={isOpen} as={React.Fragment}>
|
|
||||||
<Dialog as="div" className="relative z-30" onClose={onClose}>
|
|
||||||
<Transition.Child
|
|
||||||
as={React.Fragment}
|
|
||||||
enter="ease-out duration-300"
|
|
||||||
enterFrom="opacity-0"
|
|
||||||
enterTo="opacity-100"
|
|
||||||
leave="ease-in duration-200"
|
|
||||||
leaveFrom="opacity-100"
|
|
||||||
leaveTo="opacity-0"
|
|
||||||
>
|
|
||||||
<div className="fixed inset-0 bg-custom-backdrop bg-opacity-50 transition-opacity" />
|
|
||||||
</Transition.Child>
|
|
||||||
|
|
||||||
<div className="fixed inset-0 z-10 overflow-y-auto">
|
|
||||||
<div className="flex min-h-full items-center justify-center p-4 text-center sm:p-0">
|
|
||||||
<Transition.Child
|
|
||||||
as={React.Fragment}
|
|
||||||
enter="ease-out duration-300"
|
|
||||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
|
||||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
|
||||||
leave="ease-in duration-200"
|
|
||||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
|
||||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
|
||||||
>
|
|
||||||
<Dialog.Panel className="relative transform rounded-lg border border-custom-border-200 bg-custom-background-100 px-4 pt-5 pb-4 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6">
|
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
|
||||||
<div>
|
|
||||||
<Dialog.Title
|
|
||||||
as="h3"
|
|
||||||
className="text-lg font-medium leading-6 text-custom-text-100"
|
|
||||||
>
|
|
||||||
Save Analytics
|
|
||||||
</Dialog.Title>
|
|
||||||
<div className="mt-5">
|
|
||||||
<Input
|
|
||||||
type="text"
|
|
||||||
id="name"
|
|
||||||
name="name"
|
|
||||||
placeholder="Title"
|
|
||||||
autoComplete="off"
|
|
||||||
error={errors.name}
|
|
||||||
register={register}
|
|
||||||
width="full"
|
|
||||||
validations={{
|
|
||||||
required: "Title is required",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<TextArea
|
|
||||||
id="description"
|
|
||||||
name="description"
|
|
||||||
placeholder="Description"
|
|
||||||
className="mt-3 h-32 resize-none text-sm"
|
|
||||||
error={errors.description}
|
|
||||||
register={register}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mt-5 flex justify-end gap-2">
|
|
||||||
<SecondaryButton onClick={onClose}>Cancel</SecondaryButton>
|
|
||||||
<PrimaryButton type="submit" loading={isSubmitting}>
|
|
||||||
{isSubmitting ? "Saving..." : "Save Analytics"}
|
|
||||||
</PrimaryButton>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</Dialog.Panel>
|
|
||||||
</Transition.Child>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Dialog>
|
|
||||||
</Transition.Root>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,133 +1,76 @@
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import useSWR from "swr";
|
||||||
import { mutate } from "swr";
|
import { useForm } from "react-hook-form";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
// react-hook-form
|
// services
|
||||||
import { Control, UseFormSetValue } from "react-hook-form";
|
import { AnalyticsService } from "services/analytics.service";
|
||||||
// hooks
|
|
||||||
import useProjects from "hooks/use-projects";
|
|
||||||
// components
|
// components
|
||||||
import {
|
import { CustomAnalyticsSelectBar, CustomAnalyticsMainContent, CustomAnalyticsSidebar } from "components/analytics";
|
||||||
AnalyticsGraph,
|
|
||||||
AnalyticsSelectBar,
|
|
||||||
AnalyticsSidebar,
|
|
||||||
AnalyticsTable,
|
|
||||||
} from "components/analytics";
|
|
||||||
// ui
|
|
||||||
import { Loader, PrimaryButton } from "components/ui";
|
|
||||||
// helpers
|
|
||||||
import { convertResponseToBarGraphData } from "helpers/analytics.helper";
|
|
||||||
// types
|
// types
|
||||||
import { IAnalyticsParams, IAnalyticsResponse, ICurrentUserResponse } from "types";
|
import { IAnalyticsParams } from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { ANALYTICS } from "constants/fetch-keys";
|
import { ANALYTICS } from "constants/fetch-keys";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
analytics: IAnalyticsResponse | undefined;
|
additionalParams?: Partial<IAnalyticsParams>;
|
||||||
analyticsError: any;
|
|
||||||
params: IAnalyticsParams;
|
|
||||||
control: Control<IAnalyticsParams, any>;
|
|
||||||
setValue: UseFormSetValue<IAnalyticsParams>;
|
|
||||||
fullScreen: boolean;
|
fullScreen: boolean;
|
||||||
user: ICurrentUserResponse | undefined;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CustomAnalytics: React.FC<Props> = ({
|
const defaultValues: IAnalyticsParams = {
|
||||||
analytics,
|
x_axis: "priority",
|
||||||
analyticsError,
|
y_axis: "issue_count",
|
||||||
params,
|
segment: null,
|
||||||
control,
|
project: null,
|
||||||
setValue,
|
};
|
||||||
fullScreen,
|
|
||||||
user,
|
const analyticsService = new AnalyticsService();
|
||||||
}) => {
|
|
||||||
|
export const CustomAnalytics: React.FC<Props> = observer((props) => {
|
||||||
|
const { additionalParams, fullScreen } = props;
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
|
const { control, watch, setValue } = useForm({ defaultValues });
|
||||||
|
|
||||||
|
const params: IAnalyticsParams = {
|
||||||
|
x_axis: watch("x_axis"),
|
||||||
|
y_axis: watch("y_axis"),
|
||||||
|
segment: watch("segment"),
|
||||||
|
project: projectId ? [projectId.toString()] : watch("project"),
|
||||||
|
...additionalParams,
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data: analytics, error: analyticsError } = useSWR(
|
||||||
|
workspaceSlug ? ANALYTICS(workspaceSlug.toString(), params) : null,
|
||||||
|
workspaceSlug ? () => analyticsService.getAnalytics(workspaceSlug.toString(), params) : null
|
||||||
|
);
|
||||||
|
|
||||||
const isProjectLevel = projectId ? true : false;
|
const isProjectLevel = projectId ? true : false;
|
||||||
|
|
||||||
const yAxisKey = params.y_axis === "issue_count" ? "count" : "estimate";
|
|
||||||
const barGraphData = convertResponseToBarGraphData(analytics?.distribution, params);
|
|
||||||
|
|
||||||
const { projects } = useProjects();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={`overflow-hidden flex flex-col-reverse ${fullScreen ? "md:grid md:grid-cols-4 md:h-full" : ""}`}>
|
||||||
className={`overflow-hidden flex flex-col-reverse ${
|
|
||||||
fullScreen ? "md:grid md:grid-cols-4 md:h-full" : ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="col-span-3 flex flex-col h-full overflow-hidden">
|
<div className="col-span-3 flex flex-col h-full overflow-hidden">
|
||||||
<AnalyticsSelectBar
|
<CustomAnalyticsSelectBar
|
||||||
control={control}
|
control={control}
|
||||||
setValue={setValue}
|
setValue={setValue}
|
||||||
projects={projects ?? []}
|
|
||||||
params={params}
|
params={params}
|
||||||
fullScreen={fullScreen}
|
fullScreen={fullScreen}
|
||||||
isProjectLevel={isProjectLevel}
|
isProjectLevel={isProjectLevel}
|
||||||
/>
|
/>
|
||||||
{!analyticsError ? (
|
<CustomAnalyticsMainContent
|
||||||
analytics ? (
|
analytics={analytics}
|
||||||
analytics.total > 0 ? (
|
error={analyticsError}
|
||||||
<div className="h-full overflow-y-auto">
|
fullScreen={fullScreen}
|
||||||
<AnalyticsGraph
|
params={params}
|
||||||
analytics={analytics}
|
/>
|
||||||
barGraphData={barGraphData}
|
|
||||||
params={params}
|
|
||||||
yAxisKey={yAxisKey}
|
|
||||||
fullScreen={fullScreen}
|
|
||||||
/>
|
|
||||||
<AnalyticsTable
|
|
||||||
analytics={analytics}
|
|
||||||
barGraphData={barGraphData}
|
|
||||||
params={params}
|
|
||||||
yAxisKey={yAxisKey}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="grid h-full place-items-center p-5">
|
|
||||||
<div className="space-y-4 text-custom-text-200">
|
|
||||||
<p className="text-sm">No matching issues found. Try changing the parameters.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<Loader className="space-y-6 p-5">
|
|
||||||
<Loader.Item height="300px" />
|
|
||||||
<Loader className="space-y-4">
|
|
||||||
<Loader.Item height="30px" />
|
|
||||||
<Loader.Item height="30px" />
|
|
||||||
<Loader.Item height="30px" />
|
|
||||||
<Loader.Item height="30px" />
|
|
||||||
</Loader>
|
|
||||||
</Loader>
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<div className="grid h-full place-items-center p-5">
|
|
||||||
<div className="space-y-4 text-custom-text-200">
|
|
||||||
<p className="text-sm">There was some error in fetching the data.</p>
|
|
||||||
<div className="flex items-center justify-center gap-2">
|
|
||||||
<PrimaryButton
|
|
||||||
onClick={() => {
|
|
||||||
if (!workspaceSlug) return;
|
|
||||||
|
|
||||||
mutate(ANALYTICS(workspaceSlug.toString(), params));
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Refresh
|
|
||||||
</PrimaryButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<AnalyticsSidebar
|
<CustomAnalyticsSidebar
|
||||||
analytics={analytics}
|
analytics={analytics}
|
||||||
params={params}
|
params={params}
|
||||||
fullScreen={fullScreen}
|
fullScreen={fullScreen}
|
||||||
isProjectLevel={isProjectLevel}
|
isProjectLevel={isProjectLevel}
|
||||||
user={user}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -6,7 +6,7 @@ import { CustomTooltip } from "./custom-tooltip";
|
|||||||
import { BarGraph } from "components/ui";
|
import { BarGraph } from "components/ui";
|
||||||
// helpers
|
// helpers
|
||||||
import { findStringWithMostCharacters } from "helpers/array.helper";
|
import { findStringWithMostCharacters } from "helpers/array.helper";
|
||||||
import { generateBarColor } from "helpers/analytics.helper";
|
import { generateBarColor, generateDisplayName } from "helpers/analytics.helper";
|
||||||
// types
|
// types
|
||||||
import { IAnalyticsParams, IAnalyticsResponse } from "types";
|
import { IAnalyticsParams, IAnalyticsResponse } from "types";
|
||||||
|
|
||||||
@ -21,21 +21,7 @@ type Props = {
|
|||||||
fullScreen: boolean;
|
fullScreen: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AnalyticsGraph: React.FC<Props> = ({
|
export const AnalyticsGraph: React.FC<Props> = ({ analytics, barGraphData, params, yAxisKey, fullScreen }) => {
|
||||||
analytics,
|
|
||||||
barGraphData,
|
|
||||||
params,
|
|
||||||
yAxisKey,
|
|
||||||
fullScreen,
|
|
||||||
}) => {
|
|
||||||
const renderAssigneeName = (assigneeId: string): string => {
|
|
||||||
const assignee = analytics.extras.assignee_details.find((a) => a.assignees__id === assigneeId);
|
|
||||||
|
|
||||||
if (!assignee) return "?";
|
|
||||||
|
|
||||||
return assignee.assignees__display_name || "?";
|
|
||||||
};
|
|
||||||
|
|
||||||
const generateYAxisTickValues = () => {
|
const generateYAxisTickValues = () => {
|
||||||
if (!analytics) return [];
|
if (!analytics) return [];
|
||||||
|
|
||||||
@ -110,7 +96,7 @@ export const AnalyticsGraph: React.FC<Props> = ({
|
|||||||
<text x={0} y={21} textAnchor="middle" fontSize={9} fill="#ffffff">
|
<text x={0} y={21} textAnchor="middle" fontSize={9} fill="#ffffff">
|
||||||
{params.x_axis === "assignees__id"
|
{params.x_axis === "assignees__id"
|
||||||
? datum.value && datum.value !== "None"
|
? datum.value && datum.value !== "None"
|
||||||
? renderAssigneeName(datum.value)[0].toUpperCase()
|
? generateDisplayName(datum.value, analytics, params, "x_axis")[0].toUpperCase()
|
||||||
: "?"
|
: "?"
|
||||||
: datum.value && datum.value !== "None"
|
: datum.value && datum.value !== "None"
|
||||||
? `${datum.value}`.toUpperCase()[0]
|
? `${datum.value}`.toUpperCase()[0]
|
||||||
@ -119,7 +105,13 @@ export const AnalyticsGraph: React.FC<Props> = ({
|
|||||||
</g>
|
</g>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
: undefined,
|
: (datum) => (
|
||||||
|
<g transform={`translate(${datum.x},${datum.y})`}>
|
||||||
|
<text x={0} y={21} textAnchor="middle" fontSize={10}>
|
||||||
|
{generateDisplayName(datum.value, analytics, params, "x_axis")}
|
||||||
|
</text>
|
||||||
|
</g>
|
||||||
|
),
|
||||||
}}
|
}}
|
||||||
theme={{
|
theme={{
|
||||||
axis: {},
|
axis: {},
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
export * from "./graph";
|
export * from "./graph";
|
||||||
export * from "./create-update-analytics-modal";
|
export * from "./select";
|
||||||
export * from "./custom-analytics";
|
export * from "./custom-analytics";
|
||||||
|
export * from "./main-content";
|
||||||
export * from "./select-bar";
|
export * from "./select-bar";
|
||||||
export * from "./sidebar";
|
export * from "./sidebar";
|
||||||
export * from "./table";
|
export * from "./table";
|
||||||
|
85
web/components/analytics/custom-analytics/main-content.tsx
Normal file
85
web/components/analytics/custom-analytics/main-content.tsx
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { mutate } from "swr";
|
||||||
|
|
||||||
|
// components
|
||||||
|
import { AnalyticsGraph, AnalyticsTable } from "components/analytics";
|
||||||
|
// ui
|
||||||
|
import { Button, Loader } from "@plane/ui";
|
||||||
|
// helpers
|
||||||
|
import { convertResponseToBarGraphData } from "helpers/analytics.helper";
|
||||||
|
// types
|
||||||
|
import { IAnalyticsParams, IAnalyticsResponse } from "types";
|
||||||
|
// fetch-keys
|
||||||
|
import { ANALYTICS } from "constants/fetch-keys";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
analytics: IAnalyticsResponse | undefined;
|
||||||
|
error: any;
|
||||||
|
fullScreen: boolean;
|
||||||
|
params: IAnalyticsParams;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CustomAnalyticsMainContent: React.FC<Props> = (props) => {
|
||||||
|
const { analytics, error, fullScreen, params } = props;
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug } = router.query;
|
||||||
|
|
||||||
|
const yAxisKey = params.y_axis === "issue_count" ? "count" : "estimate";
|
||||||
|
const barGraphData = convertResponseToBarGraphData(analytics?.distribution, params);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{!error ? (
|
||||||
|
analytics ? (
|
||||||
|
analytics.total > 0 ? (
|
||||||
|
<div className="h-full overflow-y-auto">
|
||||||
|
<AnalyticsGraph
|
||||||
|
analytics={analytics}
|
||||||
|
barGraphData={barGraphData}
|
||||||
|
params={params}
|
||||||
|
yAxisKey={yAxisKey}
|
||||||
|
fullScreen={fullScreen}
|
||||||
|
/>
|
||||||
|
<AnalyticsTable analytics={analytics} barGraphData={barGraphData} params={params} yAxisKey={yAxisKey} />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid h-full place-items-center p-5">
|
||||||
|
<div className="space-y-4 text-custom-text-200">
|
||||||
|
<p className="text-sm">No matching issues found. Try changing the parameters.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<Loader className="space-y-6 p-5">
|
||||||
|
<Loader.Item height="300px" />
|
||||||
|
<Loader className="space-y-4">
|
||||||
|
<Loader.Item height="30px" />
|
||||||
|
<Loader.Item height="30px" />
|
||||||
|
<Loader.Item height="30px" />
|
||||||
|
<Loader.Item height="30px" />
|
||||||
|
</Loader>
|
||||||
|
</Loader>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<div className="grid h-full place-items-center p-5">
|
||||||
|
<div className="space-y-4 text-custom-text-200">
|
||||||
|
<p className="text-sm">There was some error in fetching the data.</p>
|
||||||
|
<div className="flex items-center justify-center gap-2">
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
onClick={() => {
|
||||||
|
if (!workspaceSlug) return;
|
||||||
|
|
||||||
|
mutate(ANALYTICS(workspaceSlug.toString(), params));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -1,80 +1,85 @@
|
|||||||
// react-hook-form
|
import { useRouter } from "next/router";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
import { Control, Controller, UseFormSetValue } from "react-hook-form";
|
import { Control, Controller, UseFormSetValue } from "react-hook-form";
|
||||||
|
|
||||||
|
// mobx store
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// components
|
// components
|
||||||
import { SelectProject, SelectSegment, SelectXAxis, SelectYAxis } from "components/analytics";
|
import { SelectProject, SelectSegment, SelectXAxis, SelectYAxis } from "components/analytics";
|
||||||
// types
|
// types
|
||||||
import { IAnalyticsParams, IProject } from "types";
|
import { IAnalyticsParams } from "types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
control: Control<IAnalyticsParams, any>;
|
control: Control<IAnalyticsParams, any>;
|
||||||
setValue: UseFormSetValue<IAnalyticsParams>;
|
setValue: UseFormSetValue<IAnalyticsParams>;
|
||||||
projects: IProject[];
|
|
||||||
params: IAnalyticsParams;
|
params: IAnalyticsParams;
|
||||||
fullScreen: boolean;
|
fullScreen: boolean;
|
||||||
isProjectLevel: boolean;
|
isProjectLevel: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AnalyticsSelectBar: React.FC<Props> = ({
|
export const CustomAnalyticsSelectBar: React.FC<Props> = observer((props) => {
|
||||||
control,
|
const { control, setValue, params, fullScreen, isProjectLevel } = props;
|
||||||
setValue,
|
|
||||||
projects,
|
const router = useRouter();
|
||||||
params,
|
const { workspaceSlug } = router.query;
|
||||||
fullScreen,
|
|
||||||
isProjectLevel,
|
const { project: projectStore } = useMobxStore();
|
||||||
}) => (
|
|
||||||
<div
|
const projectsList = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : null;
|
||||||
className={`grid items-center gap-4 px-5 py-2.5 ${
|
|
||||||
isProjectLevel ? "grid-cols-3" : "grid-cols-2"
|
return (
|
||||||
} ${fullScreen ? "lg:grid-cols-4 md:py-5" : ""}`}
|
<div
|
||||||
>
|
className={`grid items-center gap-4 px-5 py-2.5 ${isProjectLevel ? "grid-cols-3" : "grid-cols-2"} ${
|
||||||
{!isProjectLevel && (
|
fullScreen ? "lg:grid-cols-4 md:py-5" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{!isProjectLevel && (
|
||||||
|
<div>
|
||||||
|
<h6 className="text-xs text-custom-text-200">Project</h6>
|
||||||
|
<Controller
|
||||||
|
name="project"
|
||||||
|
control={control}
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<SelectProject value={value ?? undefined} onChange={onChange} projects={projectsList ?? undefined} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div>
|
<div>
|
||||||
<h6 className="text-xs text-custom-text-200">Project</h6>
|
<h6 className="text-xs text-custom-text-200">Measure (y-axis)</h6>
|
||||||
<Controller
|
<Controller
|
||||||
name="project"
|
name="y_axis"
|
||||||
|
control={control}
|
||||||
|
render={({ field: { value, onChange } }) => <SelectYAxis value={value} onChange={onChange} />}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h6 className="text-xs text-custom-text-200">Dimension (x-axis)</h6>
|
||||||
|
<Controller
|
||||||
|
name="x_axis"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<SelectProject value={value} onChange={onChange} projects={projects} />
|
<SelectXAxis
|
||||||
|
value={value}
|
||||||
|
onChange={(val: string) => {
|
||||||
|
if (params.segment === val) setValue("segment", null);
|
||||||
|
|
||||||
|
onChange(val);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h6 className="text-xs text-custom-text-200">Group</h6>
|
||||||
|
<Controller
|
||||||
|
name="segment"
|
||||||
|
control={control}
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<SelectSegment value={value} onChange={onChange} params={params} />
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
<div>
|
|
||||||
<h6 className="text-xs text-custom-text-200">Measure (y-axis)</h6>
|
|
||||||
<Controller
|
|
||||||
name="y_axis"
|
|
||||||
control={control}
|
|
||||||
render={({ field: { value, onChange } }) => (
|
|
||||||
<SelectYAxis value={value} onChange={onChange} />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
);
|
||||||
<h6 className="text-xs text-custom-text-200">Dimension (x-axis)</h6>
|
});
|
||||||
<Controller
|
|
||||||
name="x_axis"
|
|
||||||
control={control}
|
|
||||||
render={({ field: { value, onChange } }) => (
|
|
||||||
<SelectXAxis
|
|
||||||
value={value}
|
|
||||||
onChange={(val: string) => {
|
|
||||||
if (params.segment === val) setValue("segment", null);
|
|
||||||
|
|
||||||
onChange(val);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h6 className="text-xs text-custom-text-200">Group</h6>
|
|
||||||
<Controller
|
|
||||||
name="segment"
|
|
||||||
control={control}
|
|
||||||
render={({ field: { value, onChange } }) => (
|
|
||||||
<SelectSegment value={value} onChange={onChange} params={params} />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
@ -4,9 +4,9 @@ import { CustomSearchSelect } from "components/ui";
|
|||||||
import { IProject } from "types";
|
import { IProject } from "types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
value: string[] | null | undefined;
|
value: string[] | undefined;
|
||||||
onChange: (val: string[] | null) => void;
|
onChange: (val: string[] | null) => void;
|
||||||
projects: IProject[];
|
projects: IProject[] | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SelectProject: React.FC<Props> = ({ value, onChange, projects }) => {
|
export const SelectProject: React.FC<Props> = ({ value, onChange, projects }) => {
|
||||||
@ -29,7 +29,7 @@ export const SelectProject: React.FC<Props> = ({ value, onChange, projects }) =>
|
|||||||
label={
|
label={
|
||||||
value && value.length > 0
|
value && value.length > 0
|
||||||
? projects
|
? projects
|
||||||
.filter((p) => value.includes(p.id))
|
?.filter((p) => value.includes(p.id))
|
||||||
.map((p) => p.identifier)
|
.map((p) => p.identifier)
|
||||||
.join(", ")
|
.join(", ")
|
||||||
: "All projects"
|
: "All projects"
|
@ -34,8 +34,8 @@ export const SelectSegment: React.FC<Props> = ({ value, onChange, params }) => {
|
|||||||
<CustomSelect.Option value={null}>No value</CustomSelect.Option>
|
<CustomSelect.Option value={null}>No value</CustomSelect.Option>
|
||||||
{ANALYTICS_X_AXIS_VALUES.map((item) => {
|
{ANALYTICS_X_AXIS_VALUES.map((item) => {
|
||||||
if (params.x_axis === item.value) return null;
|
if (params.x_axis === item.value) return null;
|
||||||
if (cycleId && item.value === "issue_cycle__cycle__name") return null;
|
if (cycleId && item.value === "issue_cycle__cycle_id") return null;
|
||||||
if (moduleId && item.value === "issue_module__module__name") return null;
|
if (moduleId && item.value === "issue_module__module_id") return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CustomSelect.Option key={item.value} value={item.value}>
|
<CustomSelect.Option key={item.value} value={item.value}>
|
@ -25,8 +25,8 @@ export const SelectXAxis: React.FC<Props> = ({ value, onChange }) => {
|
|||||||
maxHeight="lg"
|
maxHeight="lg"
|
||||||
>
|
>
|
||||||
{ANALYTICS_X_AXIS_VALUES.map((item) => {
|
{ANALYTICS_X_AXIS_VALUES.map((item) => {
|
||||||
if (cycleId && item.value === "issue_cycle__cycle__name") return null;
|
if (cycleId && item.value === "issue_cycle__cycle_id") return null;
|
||||||
if (moduleId && item.value === "issue_module__module__name") return null;
|
if (moduleId && item.value === "issue_module__module_id") return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CustomSelect.Option key={item.value} value={item.value}>
|
<CustomSelect.Option key={item.value} value={item.value}>
|
@ -1,381 +0,0 @@
|
|||||||
import { useRouter } from "next/router";
|
|
||||||
|
|
||||||
import useSWR, { mutate } from "swr";
|
|
||||||
|
|
||||||
// services
|
|
||||||
import analyticsService from "services/analytics.service";
|
|
||||||
import projectService from "services/project.service";
|
|
||||||
import cyclesService from "services/cycles.service";
|
|
||||||
import modulesService from "services/modules.service";
|
|
||||||
import trackEventServices from "services/track-event.service";
|
|
||||||
// hooks
|
|
||||||
import useProjects from "hooks/use-projects";
|
|
||||||
import useToast from "hooks/use-toast";
|
|
||||||
// ui
|
|
||||||
import { PrimaryButton, SecondaryButton } from "components/ui";
|
|
||||||
// icons
|
|
||||||
import {
|
|
||||||
ArrowDownTrayIcon,
|
|
||||||
ArrowPathIcon,
|
|
||||||
CalendarDaysIcon,
|
|
||||||
UserGroupIcon,
|
|
||||||
} from "@heroicons/react/24/outline";
|
|
||||||
import { ContrastIcon, LayerDiagonalIcon } from "components/icons";
|
|
||||||
// helpers
|
|
||||||
import { renderShortDate } from "helpers/date-time.helper";
|
|
||||||
import { renderEmoji } from "helpers/emoji.helper";
|
|
||||||
import { truncateText } from "helpers/string.helper";
|
|
||||||
// types
|
|
||||||
import {
|
|
||||||
IAnalyticsParams,
|
|
||||||
IAnalyticsResponse,
|
|
||||||
ICurrentUserResponse,
|
|
||||||
IExportAnalyticsFormData,
|
|
||||||
IWorkspace,
|
|
||||||
} from "types";
|
|
||||||
// fetch-keys
|
|
||||||
import { ANALYTICS, CYCLE_DETAILS, MODULE_DETAILS, PROJECT_DETAILS } from "constants/fetch-keys";
|
|
||||||
// constants
|
|
||||||
import { NETWORK_CHOICES } from "constants/project";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
analytics: IAnalyticsResponse | undefined;
|
|
||||||
params: IAnalyticsParams;
|
|
||||||
fullScreen: boolean;
|
|
||||||
isProjectLevel: boolean;
|
|
||||||
user: ICurrentUserResponse | undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const AnalyticsSidebar: React.FC<Props> = ({
|
|
||||||
analytics,
|
|
||||||
params,
|
|
||||||
fullScreen,
|
|
||||||
isProjectLevel = false,
|
|
||||||
user,
|
|
||||||
}) => {
|
|
||||||
const router = useRouter();
|
|
||||||
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
|
||||||
|
|
||||||
const { projects } = useProjects();
|
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
|
||||||
|
|
||||||
const { data: projectDetails } = useSWR(
|
|
||||||
workspaceSlug && projectId && !(cycleId || moduleId)
|
|
||||||
? PROJECT_DETAILS(projectId.toString())
|
|
||||||
: null,
|
|
||||||
workspaceSlug && projectId && !(cycleId || moduleId)
|
|
||||||
? () => projectService.getProject(workspaceSlug.toString(), projectId.toString())
|
|
||||||
: null
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data: cycleDetails } = useSWR(
|
|
||||||
workspaceSlug && projectId && cycleId ? CYCLE_DETAILS(cycleId.toString()) : null,
|
|
||||||
workspaceSlug && projectId && cycleId
|
|
||||||
? () =>
|
|
||||||
cyclesService.getCycleDetails(
|
|
||||||
workspaceSlug.toString(),
|
|
||||||
projectId.toString(),
|
|
||||||
cycleId.toString()
|
|
||||||
)
|
|
||||||
: null
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data: moduleDetails } = useSWR(
|
|
||||||
workspaceSlug && projectId && moduleId ? MODULE_DETAILS(moduleId.toString()) : null,
|
|
||||||
workspaceSlug && projectId && moduleId
|
|
||||||
? () =>
|
|
||||||
modulesService.getModuleDetails(
|
|
||||||
workspaceSlug.toString(),
|
|
||||||
projectId.toString(),
|
|
||||||
moduleId.toString()
|
|
||||||
)
|
|
||||||
: null
|
|
||||||
);
|
|
||||||
|
|
||||||
const trackExportAnalytics = () => {
|
|
||||||
const eventPayload: any = {
|
|
||||||
workspaceSlug: workspaceSlug?.toString(),
|
|
||||||
params: {
|
|
||||||
x_axis: params.x_axis,
|
|
||||||
y_axis: params.y_axis,
|
|
||||||
group: params.segment,
|
|
||||||
project: params.project,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (projectDetails) {
|
|
||||||
const workspaceDetails = projectDetails.workspace as IWorkspace;
|
|
||||||
|
|
||||||
eventPayload.workspaceId = workspaceDetails.id;
|
|
||||||
eventPayload.workspaceName = workspaceDetails.name;
|
|
||||||
eventPayload.projectId = projectDetails.id;
|
|
||||||
eventPayload.projectIdentifier = projectDetails.identifier;
|
|
||||||
eventPayload.projectName = projectDetails.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cycleDetails || moduleDetails) {
|
|
||||||
const details = cycleDetails || moduleDetails;
|
|
||||||
|
|
||||||
eventPayload.workspaceId = details?.workspace_detail?.id;
|
|
||||||
eventPayload.workspaceName = details?.workspace_detail?.name;
|
|
||||||
eventPayload.projectId = details?.project_detail.id;
|
|
||||||
eventPayload.projectIdentifier = details?.project_detail.identifier;
|
|
||||||
eventPayload.projectName = details?.project_detail.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cycleDetails) {
|
|
||||||
eventPayload.cycleId = cycleDetails.id;
|
|
||||||
eventPayload.cycleName = cycleDetails.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (moduleDetails) {
|
|
||||||
eventPayload.moduleId = moduleDetails.id;
|
|
||||||
eventPayload.moduleName = moduleDetails.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
trackEventServices.trackAnalyticsEvent(
|
|
||||||
eventPayload,
|
|
||||||
cycleId
|
|
||||||
? "CYCLE_ANALYTICS_EXPORT"
|
|
||||||
: moduleId
|
|
||||||
? "MODULE_ANALYTICS_EXPORT"
|
|
||||||
: projectId
|
|
||||||
? "PROJECT_ANALYTICS_EXPORT"
|
|
||||||
: "WORKSPACE_ANALYTICS_EXPORT",
|
|
||||||
user
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const exportAnalytics = () => {
|
|
||||||
if (!workspaceSlug) return;
|
|
||||||
|
|
||||||
const data: IExportAnalyticsFormData = {
|
|
||||||
x_axis: params.x_axis,
|
|
||||||
y_axis: params.y_axis,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (params.segment) data.segment = params.segment;
|
|
||||||
if (params.project) data.project = params.project;
|
|
||||||
|
|
||||||
analyticsService
|
|
||||||
.exportAnalytics(workspaceSlug.toString(), data)
|
|
||||||
.then((res) => {
|
|
||||||
setToastAlert({
|
|
||||||
type: "success",
|
|
||||||
title: "Success!",
|
|
||||||
message: res.message,
|
|
||||||
});
|
|
||||||
|
|
||||||
trackExportAnalytics();
|
|
||||||
})
|
|
||||||
.catch(() =>
|
|
||||||
setToastAlert({
|
|
||||||
type: "error",
|
|
||||||
title: "Error!",
|
|
||||||
message: "There was some error in exporting the analytics. Please try again.",
|
|
||||||
})
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const selectedProjects =
|
|
||||||
params.project && params.project.length > 0 ? params.project : projects?.map((p) => p.id);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`px-5 py-2.5 flex items-center justify-between space-y-2 ${
|
|
||||||
fullScreen
|
|
||||||
? "border-l border-custom-border-200 md:h-full md:border-l md:border-custom-border-200 md:space-y-4 overflow-hidden md:flex-col md:items-start md:py-5"
|
|
||||||
: ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
|
||||||
<div className="flex items-center gap-1 bg-custom-background-80 rounded-md px-3 py-1 text-custom-text-200 text-xs">
|
|
||||||
<LayerDiagonalIcon height={14} width={14} />
|
|
||||||
{analytics ? analytics.total : "..."} Issues
|
|
||||||
</div>
|
|
||||||
{isProjectLevel && (
|
|
||||||
<div className="flex items-center gap-1 bg-custom-background-80 rounded-md px-3 py-1 text-custom-text-200 text-xs">
|
|
||||||
<CalendarDaysIcon className="h-3.5 w-3.5" />
|
|
||||||
{renderShortDate(
|
|
||||||
(cycleId
|
|
||||||
? cycleDetails?.created_at
|
|
||||||
: moduleId
|
|
||||||
? moduleDetails?.created_at
|
|
||||||
: projectDetails?.created_at) ?? ""
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="h-full w-full overflow-hidden">
|
|
||||||
{fullScreen ? (
|
|
||||||
<>
|
|
||||||
{!isProjectLevel && selectedProjects && selectedProjects.length > 0 && (
|
|
||||||
<div className="hidden h-full overflow-hidden md:flex md:flex-col">
|
|
||||||
<h4 className="font-medium">Selected Projects</h4>
|
|
||||||
<div className="space-y-6 mt-4 h-full overflow-y-auto">
|
|
||||||
{selectedProjects.map((projectId) => {
|
|
||||||
const project = projects?.find((p) => p.id === projectId);
|
|
||||||
|
|
||||||
if (project)
|
|
||||||
return (
|
|
||||||
<div key={project.id} className="w-full">
|
|
||||||
<div className="text-sm flex items-center gap-1">
|
|
||||||
{project.emoji ? (
|
|
||||||
<span className="grid h-6 w-6 flex-shrink-0 place-items-center">
|
|
||||||
{renderEmoji(project.emoji)}
|
|
||||||
</span>
|
|
||||||
) : project.icon_prop ? (
|
|
||||||
<div className="h-6 w-6 grid place-items-center flex-shrink-0">
|
|
||||||
{renderEmoji(project.icon_prop)}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<span className="grid h-6 w-6 mr-1 flex-shrink-0 place-items-center rounded bg-gray-700 uppercase text-white">
|
|
||||||
{project?.name.charAt(0)}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<h5 className="flex items-center gap-1">
|
|
||||||
<p className="break-words">{truncateText(project.name, 20)}</p>
|
|
||||||
<span className="text-custom-text-200 text-xs ml-1">
|
|
||||||
({project.identifier})
|
|
||||||
</span>
|
|
||||||
</h5>
|
|
||||||
</div>
|
|
||||||
<div className="mt-4 space-y-3 pl-2 w-full">
|
|
||||||
<div className="flex items-center justify-between gap-2 text-xs">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<UserGroupIcon className="h-4 w-4 text-custom-text-200" />
|
|
||||||
<h6>Total members</h6>
|
|
||||||
</div>
|
|
||||||
<span className="text-custom-text-200">{project.total_members}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between gap-2 text-xs">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<ContrastIcon height={16} width={16} />
|
|
||||||
<h6>Total cycles</h6>
|
|
||||||
</div>
|
|
||||||
<span className="text-custom-text-200">{project.total_cycles}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between gap-2 text-xs">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<UserGroupIcon className="h-4 w-4 text-custom-text-200" />
|
|
||||||
<h6>Total modules</h6>
|
|
||||||
</div>
|
|
||||||
<span className="text-custom-text-200">{project.total_modules}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{projectId ? (
|
|
||||||
cycleId && cycleDetails ? (
|
|
||||||
<div className="hidden md:block h-full overflow-y-auto">
|
|
||||||
<h4 className="font-medium break-words">Analytics for {cycleDetails.name}</h4>
|
|
||||||
<div className="space-y-4 mt-4">
|
|
||||||
<div className="flex items-center gap-2 text-xs">
|
|
||||||
<h6 className="text-custom-text-200">Lead</h6>
|
|
||||||
<span>{cycleDetails.owned_by?.display_name}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 text-xs">
|
|
||||||
<h6 className="text-custom-text-200">Start Date</h6>
|
|
||||||
<span>
|
|
||||||
{cycleDetails.start_date && cycleDetails.start_date !== ""
|
|
||||||
? renderShortDate(cycleDetails.start_date)
|
|
||||||
: "No start date"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 text-xs">
|
|
||||||
<h6 className="text-custom-text-200">Target Date</h6>
|
|
||||||
<span>
|
|
||||||
{cycleDetails.end_date && cycleDetails.end_date !== ""
|
|
||||||
? renderShortDate(cycleDetails.end_date)
|
|
||||||
: "No end date"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : moduleId && moduleDetails ? (
|
|
||||||
<div className="hidden md:block h-full overflow-y-auto">
|
|
||||||
<h4 className="font-medium break-words">Analytics for {moduleDetails.name}</h4>
|
|
||||||
<div className="space-y-4 mt-4">
|
|
||||||
<div className="flex items-center gap-2 text-xs">
|
|
||||||
<h6 className="text-custom-text-200">Lead</h6>
|
|
||||||
<span>{moduleDetails.lead_detail?.display_name}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 text-xs">
|
|
||||||
<h6 className="text-custom-text-200">Start Date</h6>
|
|
||||||
<span>
|
|
||||||
{moduleDetails.start_date && moduleDetails.start_date !== ""
|
|
||||||
? renderShortDate(moduleDetails.start_date)
|
|
||||||
: "No start date"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 text-xs">
|
|
||||||
<h6 className="text-custom-text-200">Target Date</h6>
|
|
||||||
<span>
|
|
||||||
{moduleDetails.target_date && moduleDetails.target_date !== ""
|
|
||||||
? renderShortDate(moduleDetails.target_date)
|
|
||||||
: "No end date"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="hidden md:flex md:flex-col h-full overflow-y-auto">
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
{projectDetails?.emoji ? (
|
|
||||||
<div className="grid h-6 w-6 flex-shrink-0 place-items-center">
|
|
||||||
{renderEmoji(projectDetails.emoji)}
|
|
||||||
</div>
|
|
||||||
) : projectDetails?.icon_prop ? (
|
|
||||||
<div className="h-6 w-6 grid place-items-center flex-shrink-0">
|
|
||||||
{renderEmoji(projectDetails.icon_prop)}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<span className="grid h-6 w-6 mr-1 flex-shrink-0 place-items-center rounded bg-gray-700 uppercase text-white">
|
|
||||||
{projectDetails?.name.charAt(0)}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<h4 className="font-medium break-words">{projectDetails?.name}</h4>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-4 mt-4">
|
|
||||||
<div className="flex items-center gap-2 text-xs">
|
|
||||||
<h6 className="text-custom-text-200">Network</h6>
|
|
||||||
<span>
|
|
||||||
{NETWORK_CHOICES.find((n) => n.key === projectDetails?.network)?.label ??
|
|
||||||
""}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
) : null}
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 flex-wrap justify-self-end">
|
|
||||||
<SecondaryButton
|
|
||||||
onClick={() => {
|
|
||||||
if (!workspaceSlug) return;
|
|
||||||
|
|
||||||
mutate(ANALYTICS(workspaceSlug.toString(), params));
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2 -my-1">
|
|
||||||
<ArrowPathIcon className="h-3.5 w-3.5" />
|
|
||||||
Refresh
|
|
||||||
</div>
|
|
||||||
</SecondaryButton>
|
|
||||||
<PrimaryButton onClick={exportAnalytics}>
|
|
||||||
<div className="flex items-center gap-2 -my-1">
|
|
||||||
<ArrowDownTrayIcon className="h-3.5 w-3.5" />
|
|
||||||
Export as CSV
|
|
||||||
</div>
|
|
||||||
</PrimaryButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
@ -0,0 +1,3 @@
|
|||||||
|
export * from "./projects-list";
|
||||||
|
export * from "./sidebar-header";
|
||||||
|
export * from "./sidebar";
|
@ -0,0 +1,65 @@
|
|||||||
|
// icons
|
||||||
|
import { Contrast, LayoutGrid, Users } from "lucide-react";
|
||||||
|
// helpers
|
||||||
|
import { renderEmoji } from "helpers/emoji.helper";
|
||||||
|
import { truncateText } from "helpers/string.helper";
|
||||||
|
// types
|
||||||
|
import { IProject } from "types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
projects: IProject[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CustomAnalyticsSidebarProjectsList: React.FC<Props> = (props) => {
|
||||||
|
const { projects } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="hidden h-full overflow-hidden md:flex md:flex-col">
|
||||||
|
<h4 className="font-medium">Selected Projects</h4>
|
||||||
|
<div className="space-y-6 mt-4 h-full overflow-y-auto">
|
||||||
|
{projects.map((project) => (
|
||||||
|
<div key={project.id} className="w-full">
|
||||||
|
<div className="text-sm flex items-center gap-1">
|
||||||
|
{project.emoji ? (
|
||||||
|
<span className="grid h-6 w-6 flex-shrink-0 place-items-center">{renderEmoji(project.emoji)}</span>
|
||||||
|
) : project.icon_prop ? (
|
||||||
|
<div className="h-6 w-6 grid place-items-center flex-shrink-0">{renderEmoji(project.icon_prop)}</div>
|
||||||
|
) : (
|
||||||
|
<span className="grid h-6 w-6 mr-1 flex-shrink-0 place-items-center rounded bg-gray-700 uppercase text-white">
|
||||||
|
{project?.name.charAt(0)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<h5 className="flex items-center gap-1">
|
||||||
|
<p className="break-words">{truncateText(project.name, 20)}</p>
|
||||||
|
<span className="text-custom-text-200 text-xs ml-1">({project.identifier})</span>
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 space-y-3 pl-2 w-full">
|
||||||
|
<div className="flex items-center justify-between gap-2 text-xs">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Users className="text-custom-text-200" size={14} strokeWidth={2} />
|
||||||
|
<h6>Total members</h6>
|
||||||
|
</div>
|
||||||
|
<span className="text-custom-text-200">{project.total_members}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between gap-2 text-xs">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Contrast className="text-custom-text-200" size={14} strokeWidth={2} />
|
||||||
|
<h6>Total cycles</h6>
|
||||||
|
</div>
|
||||||
|
<span className="text-custom-text-200">{project.total_cycles}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between gap-2 text-xs">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<LayoutGrid className="text-custom-text-200" size={14} strokeWidth={2} />
|
||||||
|
<h6>Total modules</h6>
|
||||||
|
</div>
|
||||||
|
<span className="text-custom-text-200">{project.total_modules}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,107 @@
|
|||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
|
||||||
|
// mobx store
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
|
// helpers
|
||||||
|
import { renderEmoji } from "helpers/emoji.helper";
|
||||||
|
import { renderShortDate } from "helpers/date-time.helper";
|
||||||
|
// constants
|
||||||
|
import { NETWORK_CHOICES } from "constants/project";
|
||||||
|
|
||||||
|
export const CustomAnalyticsSidebarHeader = observer(() => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
||||||
|
|
||||||
|
const { cycle: cycleStore, module: moduleStore, project: projectStore } = useMobxStore();
|
||||||
|
|
||||||
|
const cycleDetails = cycleId ? cycleStore.getCycleById(cycleId.toString()) : undefined;
|
||||||
|
const moduleDetails = moduleId ? moduleStore.getModuleById(moduleId.toString()) : undefined;
|
||||||
|
const projectDetails =
|
||||||
|
workspaceSlug && projectId
|
||||||
|
? projectStore.getProjectById(workspaceSlug.toString(), projectId.toString())
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{projectId ? (
|
||||||
|
cycleDetails ? (
|
||||||
|
<div className="hidden md:block h-full overflow-y-auto">
|
||||||
|
<h4 className="font-medium break-words">Analytics for {cycleDetails.name}</h4>
|
||||||
|
<div className="space-y-4 mt-4">
|
||||||
|
<div className="flex items-center gap-2 text-xs">
|
||||||
|
<h6 className="text-custom-text-200">Lead</h6>
|
||||||
|
<span>{cycleDetails.owned_by?.display_name}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-xs">
|
||||||
|
<h6 className="text-custom-text-200">Start Date</h6>
|
||||||
|
<span>
|
||||||
|
{cycleDetails.start_date && cycleDetails.start_date !== ""
|
||||||
|
? renderShortDate(cycleDetails.start_date)
|
||||||
|
: "No start date"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-xs">
|
||||||
|
<h6 className="text-custom-text-200">Target Date</h6>
|
||||||
|
<span>
|
||||||
|
{cycleDetails.end_date && cycleDetails.end_date !== ""
|
||||||
|
? renderShortDate(cycleDetails.end_date)
|
||||||
|
: "No end date"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : moduleDetails ? (
|
||||||
|
<div className="hidden md:block h-full overflow-y-auto">
|
||||||
|
<h4 className="font-medium break-words">Analytics for {moduleDetails.name}</h4>
|
||||||
|
<div className="space-y-4 mt-4">
|
||||||
|
<div className="flex items-center gap-2 text-xs">
|
||||||
|
<h6 className="text-custom-text-200">Lead</h6>
|
||||||
|
<span>{moduleDetails.lead_detail?.display_name}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-xs">
|
||||||
|
<h6 className="text-custom-text-200">Start Date</h6>
|
||||||
|
<span>
|
||||||
|
{moduleDetails.start_date && moduleDetails.start_date !== ""
|
||||||
|
? renderShortDate(moduleDetails.start_date)
|
||||||
|
: "No start date"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-xs">
|
||||||
|
<h6 className="text-custom-text-200">Target Date</h6>
|
||||||
|
<span>
|
||||||
|
{moduleDetails.target_date && moduleDetails.target_date !== ""
|
||||||
|
? renderShortDate(moduleDetails.target_date)
|
||||||
|
: "No end date"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="hidden md:flex md:flex-col h-full overflow-y-auto">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
{projectDetails?.emoji ? (
|
||||||
|
<div className="grid h-6 w-6 flex-shrink-0 place-items-center">{renderEmoji(projectDetails.emoji)}</div>
|
||||||
|
) : projectDetails?.icon_prop ? (
|
||||||
|
<div className="h-6 w-6 grid place-items-center flex-shrink-0">
|
||||||
|
{renderEmoji(projectDetails.icon_prop)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span className="grid h-6 w-6 mr-1 flex-shrink-0 place-items-center rounded bg-gray-700 uppercase text-white">
|
||||||
|
{projectDetails?.name.charAt(0)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<h4 className="font-medium break-words">{projectDetails?.name}</h4>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-4 mt-4">
|
||||||
|
<div className="flex items-center gap-2 text-xs">
|
||||||
|
<h6 className="text-custom-text-200">Network</h6>
|
||||||
|
<span>{NETWORK_CHOICES.find((n) => n.key === projectDetails?.network)?.label ?? ""}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
215
web/components/analytics/custom-analytics/sidebar/sidebar.tsx
Normal file
215
web/components/analytics/custom-analytics/sidebar/sidebar.tsx
Normal file
@ -0,0 +1,215 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { mutate } from "swr";
|
||||||
|
// services
|
||||||
|
import { AnalyticsService } from "services/analytics.service";
|
||||||
|
import { TrackEventService } from "services/track_event.service";
|
||||||
|
// hooks
|
||||||
|
import useToast from "hooks/use-toast";
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
|
// components
|
||||||
|
import { CustomAnalyticsSidebarHeader, CustomAnalyticsSidebarProjectsList } from "components/analytics";
|
||||||
|
// ui
|
||||||
|
import { Button } from "@plane/ui";
|
||||||
|
// icons
|
||||||
|
import { ArrowDownTrayIcon, ArrowPathIcon, CalendarDaysIcon } from "@heroicons/react/24/outline";
|
||||||
|
import { LayerDiagonalIcon } from "components/icons";
|
||||||
|
// helpers
|
||||||
|
import { renderShortDate } from "helpers/date-time.helper";
|
||||||
|
// types
|
||||||
|
import { IAnalyticsParams, IAnalyticsResponse, IExportAnalyticsFormData, IWorkspace } from "types";
|
||||||
|
// fetch-keys
|
||||||
|
import { ANALYTICS } from "constants/fetch-keys";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
analytics: IAnalyticsResponse | undefined;
|
||||||
|
params: IAnalyticsParams;
|
||||||
|
fullScreen: boolean;
|
||||||
|
isProjectLevel: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const analyticsService = new AnalyticsService();
|
||||||
|
const trackEventService = new TrackEventService();
|
||||||
|
|
||||||
|
export const CustomAnalyticsSidebar: React.FC<Props> = observer(
|
||||||
|
({ analytics, params, fullScreen, isProjectLevel = false }) => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
||||||
|
|
||||||
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
|
const { user: userStore, project: projectStore, cycle: cycleStore, module: moduleStore } = useMobxStore();
|
||||||
|
|
||||||
|
const user = userStore.currentUser;
|
||||||
|
|
||||||
|
const projects = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : undefined;
|
||||||
|
const projectDetails =
|
||||||
|
workspaceSlug && projectId
|
||||||
|
? projectStore.getProjectById(workspaceSlug.toString(), projectId.toString()) ?? undefined
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const trackExportAnalytics = () => {
|
||||||
|
if (!user) return;
|
||||||
|
|
||||||
|
const eventPayload: any = {
|
||||||
|
workspaceSlug: workspaceSlug?.toString(),
|
||||||
|
params: {
|
||||||
|
x_axis: params.x_axis,
|
||||||
|
y_axis: params.y_axis,
|
||||||
|
group: params.segment,
|
||||||
|
project: params.project,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (projectDetails) {
|
||||||
|
const workspaceDetails = projectDetails.workspace as IWorkspace;
|
||||||
|
|
||||||
|
eventPayload.workspaceId = workspaceDetails.id;
|
||||||
|
eventPayload.workspaceName = workspaceDetails.name;
|
||||||
|
eventPayload.projectId = projectDetails.id;
|
||||||
|
eventPayload.projectIdentifier = projectDetails.identifier;
|
||||||
|
eventPayload.projectName = projectDetails.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cycleDetails || moduleDetails) {
|
||||||
|
const details = cycleDetails || moduleDetails;
|
||||||
|
|
||||||
|
eventPayload.workspaceId = details?.workspace_detail?.id;
|
||||||
|
eventPayload.workspaceName = details?.workspace_detail?.name;
|
||||||
|
eventPayload.projectId = details?.project_detail.id;
|
||||||
|
eventPayload.projectIdentifier = details?.project_detail.identifier;
|
||||||
|
eventPayload.projectName = details?.project_detail.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cycleDetails) {
|
||||||
|
eventPayload.cycleId = cycleDetails.id;
|
||||||
|
eventPayload.cycleName = cycleDetails.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (moduleDetails) {
|
||||||
|
eventPayload.moduleId = moduleDetails.id;
|
||||||
|
eventPayload.moduleName = moduleDetails.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
trackEventService.trackAnalyticsEvent(
|
||||||
|
eventPayload,
|
||||||
|
cycleId
|
||||||
|
? "CYCLE_ANALYTICS_EXPORT"
|
||||||
|
: moduleId
|
||||||
|
? "MODULE_ANALYTICS_EXPORT"
|
||||||
|
: projectId
|
||||||
|
? "PROJECT_ANALYTICS_EXPORT"
|
||||||
|
: "WORKSPACE_ANALYTICS_EXPORT",
|
||||||
|
user
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const exportAnalytics = () => {
|
||||||
|
if (!workspaceSlug) return;
|
||||||
|
|
||||||
|
const data: IExportAnalyticsFormData = {
|
||||||
|
x_axis: params.x_axis,
|
||||||
|
y_axis: params.y_axis,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (params.segment) data.segment = params.segment;
|
||||||
|
if (params.project) data.project = params.project;
|
||||||
|
|
||||||
|
analyticsService
|
||||||
|
.exportAnalytics(workspaceSlug.toString(), data)
|
||||||
|
.then((res) => {
|
||||||
|
setToastAlert({
|
||||||
|
type: "success",
|
||||||
|
title: "Success!",
|
||||||
|
message: res.message,
|
||||||
|
});
|
||||||
|
|
||||||
|
trackExportAnalytics();
|
||||||
|
})
|
||||||
|
.catch(() =>
|
||||||
|
setToastAlert({
|
||||||
|
type: "error",
|
||||||
|
title: "Error!",
|
||||||
|
message: "There was some error in exporting the analytics. Please try again.",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const cycleDetails = cycleId ? cycleStore.getCycleById(cycleId.toString()) : undefined;
|
||||||
|
const moduleDetails = moduleId ? moduleStore.getModuleById(moduleId.toString()) : undefined;
|
||||||
|
|
||||||
|
// fetch cycle details
|
||||||
|
useEffect(() => {
|
||||||
|
if (!workspaceSlug || !projectId || !cycleId || cycleDetails) return;
|
||||||
|
|
||||||
|
cycleStore.fetchCycleWithId(workspaceSlug.toString(), projectId.toString(), cycleId.toString());
|
||||||
|
}, [cycleId, cycleDetails, cycleStore, projectId, workspaceSlug]);
|
||||||
|
|
||||||
|
// fetch module details
|
||||||
|
useEffect(() => {
|
||||||
|
if (!workspaceSlug || !projectId || !moduleId || moduleDetails) return;
|
||||||
|
|
||||||
|
moduleStore.fetchModuleDetails(workspaceSlug.toString(), projectId.toString(), moduleId.toString());
|
||||||
|
}, [moduleId, moduleDetails, moduleStore, projectId, workspaceSlug]);
|
||||||
|
|
||||||
|
const selectedProjects = params.project && params.project.length > 0 ? params.project : projects?.map((p) => p.id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`px-5 py-2.5 flex items-center justify-between space-y-2 ${
|
||||||
|
fullScreen
|
||||||
|
? "border-l border-custom-border-200 md:h-full md:border-l md:border-custom-border-200 md:space-y-4 overflow-hidden md:flex-col md:items-start md:py-5"
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
|
<div className="flex items-center gap-1 bg-custom-background-80 rounded-md px-3 py-1 text-custom-text-200 text-xs">
|
||||||
|
<LayerDiagonalIcon height={14} width={14} />
|
||||||
|
{analytics ? analytics.total : "..."} Issues
|
||||||
|
</div>
|
||||||
|
{isProjectLevel && (
|
||||||
|
<div className="flex items-center gap-1 bg-custom-background-80 rounded-md px-3 py-1 text-custom-text-200 text-xs">
|
||||||
|
<CalendarDaysIcon className="h-3.5 w-3.5" />
|
||||||
|
{renderShortDate(
|
||||||
|
(cycleId
|
||||||
|
? cycleDetails?.created_at
|
||||||
|
: moduleId
|
||||||
|
? moduleDetails?.created_at
|
||||||
|
: projectDetails?.created_at) ?? ""
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="h-full w-full overflow-hidden">
|
||||||
|
{fullScreen ? (
|
||||||
|
<>
|
||||||
|
{!isProjectLevel && selectedProjects && selectedProjects.length > 0 && (
|
||||||
|
<CustomAnalyticsSidebarProjectsList
|
||||||
|
projects={projects?.filter((p) => selectedProjects.includes(p.id)) ?? []}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<CustomAnalyticsSidebarHeader />
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 flex-wrap justify-self-end">
|
||||||
|
<Button
|
||||||
|
variant="neutral-primary"
|
||||||
|
prependIcon={<ArrowPathIcon className="h-3.5 w-3.5" />}
|
||||||
|
onClick={() => {
|
||||||
|
if (!workspaceSlug) return;
|
||||||
|
|
||||||
|
mutate(ANALYTICS(workspaceSlug.toString(), params));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
|
<Button variant="primary" prependIcon={<ArrowDownTrayIcon />} onClick={exportAnalytics}>
|
||||||
|
Export as CSV
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
@ -1,15 +1,13 @@
|
|||||||
// nivo
|
|
||||||
import { BarDatum } from "@nivo/bar";
|
import { BarDatum } from "@nivo/bar";
|
||||||
|
|
||||||
// icons
|
// icons
|
||||||
import { PriorityIcon } from "components/icons";
|
import { PriorityIcon } from "components/icons";
|
||||||
// helpers
|
// helpers
|
||||||
import { addSpaceIfCamelCase } from "helpers/string.helper";
|
import { generateBarColor, generateDisplayName } from "helpers/analytics.helper";
|
||||||
// helpers
|
|
||||||
import { generateBarColor, renderMonthAndYear } from "helpers/analytics.helper";
|
|
||||||
// types
|
// types
|
||||||
import { IAnalyticsParams, IAnalyticsResponse, TIssuePriorities } from "types";
|
import { IAnalyticsParams, IAnalyticsResponse, TIssuePriorities } from "types";
|
||||||
// constants
|
// constants
|
||||||
import { ANALYTICS_X_AXIS_VALUES, ANALYTICS_Y_AXIS_VALUES, DATE_KEYS } from "constants/analytics";
|
import { ANALYTICS_X_AXIS_VALUES, ANALYTICS_Y_AXIS_VALUES } from "constants/analytics";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
analytics: IAnalyticsResponse;
|
analytics: IAnalyticsResponse;
|
||||||
@ -21,112 +19,81 @@ type Props = {
|
|||||||
yAxisKey: "count" | "estimate";
|
yAxisKey: "count" | "estimate";
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AnalyticsTable: React.FC<Props> = ({ analytics, barGraphData, params, yAxisKey }) => {
|
export const AnalyticsTable: React.FC<Props> = ({ analytics, barGraphData, params, yAxisKey }) => (
|
||||||
const renderAssigneeName = (assigneeId: string): string => {
|
<div className="flow-root">
|
||||||
const assignee = analytics.extras.assignee_details.find((a) => a.assignees__id === assigneeId);
|
<div className="overflow-x-auto">
|
||||||
|
<div className="inline-block min-w-full align-middle">
|
||||||
if (!assignee) return "No assignee";
|
<table className="min-w-full divide-y divide-custom-border-200 whitespace-nowrap border-y border-custom-border-200">
|
||||||
|
<thead className="bg-custom-background-80">
|
||||||
return assignee.assignees__display_name || "No assignee";
|
<tr className="divide-x divide-custom-border-200 text-sm text-custom-text-100">
|
||||||
};
|
<th scope="col" className="py-3 px-2.5 text-left font-medium">
|
||||||
|
{ANALYTICS_X_AXIS_VALUES.find((v) => v.value === params.x_axis)?.label}
|
||||||
return (
|
</th>
|
||||||
<div className="flow-root">
|
{params.segment ? (
|
||||||
<div className="overflow-x-auto">
|
barGraphData.xAxisKeys.map((key) => (
|
||||||
<div className="inline-block min-w-full align-middle">
|
<th
|
||||||
<table className="min-w-full divide-y divide-custom-border-200 whitespace-nowrap border-y border-custom-border-200">
|
key={`segment-${key}`}
|
||||||
<thead className="bg-custom-background-80">
|
scope="col"
|
||||||
<tr className="divide-x divide-custom-border-200 text-sm text-custom-text-100">
|
className={`px-2.5 py-3 text-left font-medium ${
|
||||||
<th scope="col" className="py-3 px-2.5 text-left font-medium">
|
params.segment === "priority" || params.segment === "state__group" ? "capitalize" : ""
|
||||||
{ANALYTICS_X_AXIS_VALUES.find((v) => v.value === params.x_axis)?.label}
|
|
||||||
</th>
|
|
||||||
{params.segment ? (
|
|
||||||
barGraphData.xAxisKeys.map((key) => (
|
|
||||||
<th
|
|
||||||
key={`segment-${key}`}
|
|
||||||
scope="col"
|
|
||||||
className={`px-2.5 py-3 text-left font-medium ${
|
|
||||||
params.segment === "priority" || params.segment === "state__group"
|
|
||||||
? "capitalize"
|
|
||||||
: ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{params.segment === "priority" ? (
|
|
||||||
<PriorityIcon priority={key as TIssuePriorities} />
|
|
||||||
) : (
|
|
||||||
<span
|
|
||||||
className="h-3 w-3 flex-shrink-0 rounded"
|
|
||||||
style={{
|
|
||||||
backgroundColor: generateBarColor(key, analytics, params, "segment"),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{params.segment === "assignees__id"
|
|
||||||
? renderAssigneeName(key)
|
|
||||||
: DATE_KEYS.includes(params.segment ?? "")
|
|
||||||
? renderMonthAndYear(key)
|
|
||||||
: key}
|
|
||||||
</div>
|
|
||||||
</th>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<th scope="col" className="py-3 px-2.5 text-left font-medium sm:pr-0">
|
|
||||||
{ANALYTICS_Y_AXIS_VALUES.find((v) => v.value === params.y_axis)?.label}
|
|
||||||
</th>
|
|
||||||
)}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="divide-y divide-custom-border-200">
|
|
||||||
{barGraphData.data.map((item, index) => (
|
|
||||||
<tr
|
|
||||||
key={`table-row-${index}`}
|
|
||||||
className="divide-x divide-custom-border-200 text-xs text-custom-text-200"
|
|
||||||
>
|
|
||||||
<td
|
|
||||||
className={`flex items-center gap-2 whitespace-nowrap py-2 px-2.5 font-medium ${
|
|
||||||
params.x_axis === "priority" || params.x_axis === "state__group"
|
|
||||||
? "capitalize"
|
|
||||||
: ""
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{params.x_axis === "priority" ? (
|
<div className="flex items-center gap-2">
|
||||||
<PriorityIcon priority={item.name as TIssuePriorities} />
|
{params.segment === "priority" ? (
|
||||||
) : (
|
<PriorityIcon priority={key as TIssuePriorities} />
|
||||||
<span
|
) : (
|
||||||
className="h-3 w-3 rounded"
|
<span
|
||||||
style={{
|
className="h-3 w-3 flex-shrink-0 rounded"
|
||||||
backgroundColor: generateBarColor(
|
style={{
|
||||||
`${item.name}`,
|
backgroundColor: generateBarColor(key, analytics, params, "segment"),
|
||||||
analytics,
|
}}
|
||||||
params,
|
/>
|
||||||
"x_axis"
|
)}
|
||||||
),
|
{generateDisplayName(key, analytics, params, "segment")}
|
||||||
}}
|
</div>
|
||||||
/>
|
</th>
|
||||||
)}
|
))
|
||||||
{params.x_axis === "assignees__id"
|
) : (
|
||||||
? renderAssigneeName(`${item.name}`)
|
<th scope="col" className="py-3 px-2.5 text-left font-medium sm:pr-0">
|
||||||
: addSpaceIfCamelCase(`${item.name}`)}
|
{ANALYTICS_Y_AXIS_VALUES.find((v) => v.value === params.y_axis)?.label}
|
||||||
</td>
|
</th>
|
||||||
{params.segment ? (
|
)}
|
||||||
barGraphData.xAxisKeys.map((key, index) => (
|
</tr>
|
||||||
<td
|
</thead>
|
||||||
key={`segment-value-${index}`}
|
<tbody className="divide-y divide-custom-border-200">
|
||||||
className="whitespace-nowrap py-2 px-2.5 sm:pr-0"
|
{barGraphData.data.map((item, index) => (
|
||||||
>
|
<tr key={`table-row-${index}`} className="divide-x divide-custom-border-200 text-xs text-custom-text-200">
|
||||||
{item[key] ?? 0}
|
<td
|
||||||
</td>
|
className={`flex items-center gap-2 whitespace-nowrap py-2 px-2.5 font-medium ${
|
||||||
))
|
params.x_axis === "priority" || params.x_axis === "state__group" ? "capitalize" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{params.x_axis === "priority" ? (
|
||||||
|
<PriorityIcon priority={item.name as TIssuePriorities} />
|
||||||
) : (
|
) : (
|
||||||
<td className="whitespace-nowrap py-2 px-2.5 sm:pr-0">{item[yAxisKey]}</td>
|
<span
|
||||||
|
className="h-3 w-3 rounded"
|
||||||
|
style={{
|
||||||
|
backgroundColor: generateBarColor(`${item.name}`, analytics, params, "x_axis"),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</tr>
|
{generateDisplayName(`${item.name}`, analytics, params, "x_axis")}
|
||||||
))}
|
</td>
|
||||||
</tbody>
|
{params.segment ? (
|
||||||
</table>
|
barGraphData.xAxisKeys.map((key, index) => (
|
||||||
</div>
|
<td key={`segment-value-${index}`} className="whitespace-nowrap py-2 px-2.5 sm:pr-0">
|
||||||
|
{item[key] ?? 0}
|
||||||
|
</td>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<td className="whitespace-nowrap py-2 px-2.5 sm:pr-0">{item[yAxisKey]}</td>
|
||||||
|
)}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
};
|
);
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
export * from "./custom-analytics";
|
export * from "./custom-analytics";
|
||||||
export * from "./scope-and-demand";
|
export * from "./scope-and-demand";
|
||||||
export * from "./select";
|
|
||||||
export * from "./project-modal";
|
export * from "./project-modal";
|
||||||
|
@ -1,225 +0,0 @@
|
|||||||
import React, { Fragment, useState } from "react";
|
|
||||||
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
|
|
||||||
import useSWR from "swr";
|
|
||||||
|
|
||||||
// react-hook-form
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
// headless ui
|
|
||||||
import { Tab } from "@headlessui/react";
|
|
||||||
// services
|
|
||||||
import analyticsService from "services/analytics.service";
|
|
||||||
import projectService from "services/project.service";
|
|
||||||
import cyclesService from "services/cycles.service";
|
|
||||||
import modulesService from "services/modules.service";
|
|
||||||
import trackEventServices from "services/track-event.service";
|
|
||||||
// components
|
|
||||||
import { CustomAnalytics, ScopeAndDemand } from "components/analytics";
|
|
||||||
// icons
|
|
||||||
import {
|
|
||||||
ArrowsPointingInIcon,
|
|
||||||
ArrowsPointingOutIcon,
|
|
||||||
XMarkIcon,
|
|
||||||
} from "@heroicons/react/24/outline";
|
|
||||||
// types
|
|
||||||
import { IAnalyticsParams, IWorkspace } from "types";
|
|
||||||
// fetch-keys
|
|
||||||
import { ANALYTICS, CYCLE_DETAILS, MODULE_DETAILS, PROJECT_DETAILS } from "constants/fetch-keys";
|
|
||||||
import useUserAuth from "hooks/use-user-auth";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const defaultValues: IAnalyticsParams = {
|
|
||||||
x_axis: "priority",
|
|
||||||
y_axis: "issue_count",
|
|
||||||
segment: null,
|
|
||||||
project: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
const tabsList = ["Scope and Demand", "Custom Analytics"];
|
|
||||||
|
|
||||||
export const AnalyticsProjectModal: React.FC<Props> = ({ isOpen, onClose }) => {
|
|
||||||
const [fullScreen, setFullScreen] = useState(false);
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
|
||||||
|
|
||||||
const { user } = useUserAuth();
|
|
||||||
|
|
||||||
const { control, watch, setValue } = useForm<IAnalyticsParams>({ defaultValues });
|
|
||||||
|
|
||||||
const params: IAnalyticsParams = {
|
|
||||||
x_axis: watch("x_axis"),
|
|
||||||
y_axis: watch("y_axis"),
|
|
||||||
segment: watch("segment"),
|
|
||||||
project: projectId ? [projectId.toString()] : watch("project"),
|
|
||||||
cycle: cycleId ? cycleId.toString() : null,
|
|
||||||
module: moduleId ? moduleId.toString() : null,
|
|
||||||
};
|
|
||||||
|
|
||||||
const { data: analytics, error: analyticsError } = useSWR(
|
|
||||||
workspaceSlug ? ANALYTICS(workspaceSlug.toString(), params) : null,
|
|
||||||
workspaceSlug ? () => analyticsService.getAnalytics(workspaceSlug.toString(), params) : null
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data: projectDetails } = useSWR(
|
|
||||||
workspaceSlug && projectId && !(cycleId || moduleId)
|
|
||||||
? PROJECT_DETAILS(projectId.toString())
|
|
||||||
: null,
|
|
||||||
workspaceSlug && projectId && !(cycleId || moduleId)
|
|
||||||
? () => projectService.getProject(workspaceSlug.toString(), projectId.toString())
|
|
||||||
: null
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data: cycleDetails } = useSWR(
|
|
||||||
workspaceSlug && projectId && cycleId ? CYCLE_DETAILS(cycleId.toString()) : null,
|
|
||||||
workspaceSlug && projectId && cycleId
|
|
||||||
? () =>
|
|
||||||
cyclesService.getCycleDetails(
|
|
||||||
workspaceSlug.toString(),
|
|
||||||
projectId.toString(),
|
|
||||||
cycleId.toString()
|
|
||||||
)
|
|
||||||
: null
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data: moduleDetails } = useSWR(
|
|
||||||
workspaceSlug && projectId && moduleId ? MODULE_DETAILS(moduleId.toString()) : null,
|
|
||||||
workspaceSlug && projectId && moduleId
|
|
||||||
? () =>
|
|
||||||
modulesService.getModuleDetails(
|
|
||||||
workspaceSlug.toString(),
|
|
||||||
projectId.toString(),
|
|
||||||
moduleId.toString()
|
|
||||||
)
|
|
||||||
: null
|
|
||||||
);
|
|
||||||
|
|
||||||
const trackAnalyticsEvent = (tab: string) => {
|
|
||||||
const eventPayload: any = {
|
|
||||||
workspaceSlug: workspaceSlug?.toString(),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (projectDetails) {
|
|
||||||
const workspaceDetails = projectDetails.workspace as IWorkspace;
|
|
||||||
|
|
||||||
eventPayload.workspaceId = workspaceDetails.id;
|
|
||||||
eventPayload.workspaceName = workspaceDetails.name;
|
|
||||||
eventPayload.projectId = projectDetails.id;
|
|
||||||
eventPayload.projectIdentifier = projectDetails.identifier;
|
|
||||||
eventPayload.projectName = projectDetails.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cycleDetails || moduleDetails) {
|
|
||||||
const details = cycleDetails || moduleDetails;
|
|
||||||
|
|
||||||
eventPayload.workspaceId = details?.workspace_detail?.id;
|
|
||||||
eventPayload.workspaceName = details?.workspace_detail?.name;
|
|
||||||
eventPayload.projectId = details?.project_detail.id;
|
|
||||||
eventPayload.projectIdentifier = details?.project_detail.identifier;
|
|
||||||
eventPayload.projectName = details?.project_detail.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cycleDetails) {
|
|
||||||
eventPayload.cycleId = cycleDetails.id;
|
|
||||||
eventPayload.cycleName = cycleDetails.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (moduleDetails) {
|
|
||||||
eventPayload.moduleId = moduleDetails.id;
|
|
||||||
eventPayload.moduleName = moduleDetails.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
const eventType =
|
|
||||||
tab === "Scope and Demand" ? "SCOPE_AND_DEMAND_ANALYTICS" : "CUSTOM_ANALYTICS";
|
|
||||||
|
|
||||||
trackEventServices.trackAnalyticsEvent(
|
|
||||||
eventPayload,
|
|
||||||
cycleId ? `CYCLE_${eventType}` : moduleId ? `MODULE_${eventType}` : `PROJECT_${eventType}`,
|
|
||||||
user
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClose = () => {
|
|
||||||
onClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`absolute top-0 z-30 h-full bg-custom-background-90 ${
|
|
||||||
fullScreen ? "p-2 w-full" : "w-1/2"
|
|
||||||
} ${isOpen ? "right-0" : "-right-full"} duration-300 transition-all`}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={`flex h-full flex-col overflow-hidden border-custom-border-200 bg-custom-background-100 text-left ${
|
|
||||||
fullScreen ? "rounded-lg border" : "border-l"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between gap-4 bg-custom-background-100 px-5 py-4 text-sm">
|
|
||||||
<h3 className="break-words">
|
|
||||||
Analytics for{" "}
|
|
||||||
{cycleId ? cycleDetails?.name : moduleId ? moduleDetails?.name : projectDetails?.name}
|
|
||||||
</h3>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="grid place-items-center p-1 text-custom-text-200 hover:text-custom-text-100"
|
|
||||||
onClick={() => setFullScreen((prevData) => !prevData)}
|
|
||||||
>
|
|
||||||
{fullScreen ? (
|
|
||||||
<ArrowsPointingInIcon className="h-4 w-4" />
|
|
||||||
) : (
|
|
||||||
<ArrowsPointingOutIcon className="h-3 w-3" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="grid place-items-center p-1 text-custom-text-200 hover:text-custom-text-100"
|
|
||||||
onClick={handleClose}
|
|
||||||
>
|
|
||||||
<XMarkIcon className="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Tab.Group as={Fragment}>
|
|
||||||
<Tab.List as="div" className="space-x-2 border-b border-custom-border-200 p-5 pt-0">
|
|
||||||
{tabsList.map((tab) => (
|
|
||||||
<Tab
|
|
||||||
key={tab}
|
|
||||||
className={({ selected }) =>
|
|
||||||
`rounded-3xl border border-custom-border-200 px-4 py-2 text-xs hover:bg-custom-background-80 ${
|
|
||||||
selected ? "bg-custom-background-80" : ""
|
|
||||||
}`
|
|
||||||
}
|
|
||||||
onClick={() => trackAnalyticsEvent(tab)}
|
|
||||||
>
|
|
||||||
{tab}
|
|
||||||
</Tab>
|
|
||||||
))}
|
|
||||||
</Tab.List>
|
|
||||||
{/* <h4 className="p-5 pb-0">Analytics for</h4> */}
|
|
||||||
<Tab.Panels as={Fragment}>
|
|
||||||
<Tab.Panel as={Fragment}>
|
|
||||||
<ScopeAndDemand fullScreen={fullScreen} />
|
|
||||||
</Tab.Panel>
|
|
||||||
<Tab.Panel as={Fragment}>
|
|
||||||
<CustomAnalytics
|
|
||||||
analytics={analytics}
|
|
||||||
analyticsError={analyticsError}
|
|
||||||
params={params}
|
|
||||||
control={control}
|
|
||||||
setValue={setValue}
|
|
||||||
fullScreen={fullScreen}
|
|
||||||
user={user}
|
|
||||||
/>
|
|
||||||
</Tab.Panel>
|
|
||||||
</Tab.Panels>
|
|
||||||
</Tab.Group>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
37
web/components/analytics/project-modal/header.tsx
Normal file
37
web/components/analytics/project-modal/header.tsx
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
|
||||||
|
// icons
|
||||||
|
import { Expand, Shrink, X } from "lucide-react";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
fullScreen: boolean;
|
||||||
|
handleClose: () => void;
|
||||||
|
setFullScreen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ProjectAnalyticsModalHeader: React.FC<Props> = observer((props) => {
|
||||||
|
const { fullScreen, handleClose, setFullScreen, title } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between gap-4 bg-custom-background-100 px-5 py-4 text-sm">
|
||||||
|
<h3 className="break-words">Analytics for {title}</h3>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="grid place-items-center p-1 text-custom-text-200 hover:text-custom-text-100"
|
||||||
|
onClick={() => setFullScreen((prevData) => !prevData)}
|
||||||
|
>
|
||||||
|
{fullScreen ? <Shrink size={14} strokeWidth={2} /> : <Expand size={14} strokeWidth={2} />}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="grid place-items-center p-1 text-custom-text-200 hover:text-custom-text-100"
|
||||||
|
onClick={handleClose}
|
||||||
|
>
|
||||||
|
<X size={14} strokeWidth={2} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
3
web/components/analytics/project-modal/index.ts
Normal file
3
web/components/analytics/project-modal/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from "./header";
|
||||||
|
export * from "./main-content";
|
||||||
|
export * from "./modal";
|
114
web/components/analytics/project-modal/main-content.tsx
Normal file
114
web/components/analytics/project-modal/main-content.tsx
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { Tab } from "@headlessui/react";
|
||||||
|
// mobx store
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
|
// services
|
||||||
|
import { TrackEventService } from "services/track_event.service";
|
||||||
|
// components
|
||||||
|
import { CustomAnalytics, ScopeAndDemand } from "components/analytics";
|
||||||
|
// types
|
||||||
|
import { ICycle, IModule, IProject, IWorkspace } from "types";
|
||||||
|
// constants
|
||||||
|
import { ANALYTICS_TABS } from "constants/analytics";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
fullScreen: boolean;
|
||||||
|
cycleDetails: ICycle | undefined;
|
||||||
|
moduleDetails: IModule | undefined;
|
||||||
|
projectDetails: IProject | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const trackEventService = new TrackEventService();
|
||||||
|
|
||||||
|
export const ProjectAnalyticsModalMainContent: React.FC<Props> = observer((props) => {
|
||||||
|
const { fullScreen, cycleDetails, moduleDetails, projectDetails } = props;
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug } = router.query;
|
||||||
|
|
||||||
|
const { user: userStore } = useMobxStore();
|
||||||
|
|
||||||
|
const user = userStore.currentUser;
|
||||||
|
|
||||||
|
const trackAnalyticsEvent = (tab: string) => {
|
||||||
|
if (!workspaceSlug || !user) return;
|
||||||
|
|
||||||
|
const eventPayload: any = {
|
||||||
|
workspaceSlug: workspaceSlug.toString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (projectDetails) {
|
||||||
|
const workspaceDetails = projectDetails.workspace as IWorkspace;
|
||||||
|
|
||||||
|
eventPayload.workspaceId = workspaceDetails.id;
|
||||||
|
eventPayload.workspaceName = workspaceDetails.name;
|
||||||
|
eventPayload.projectId = projectDetails.id;
|
||||||
|
eventPayload.projectIdentifier = projectDetails.identifier;
|
||||||
|
eventPayload.projectName = projectDetails.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cycleDetails || moduleDetails) {
|
||||||
|
const details = cycleDetails || moduleDetails;
|
||||||
|
|
||||||
|
eventPayload.workspaceId = details?.workspace_detail?.id;
|
||||||
|
eventPayload.workspaceName = details?.workspace_detail?.name;
|
||||||
|
eventPayload.projectId = details?.project_detail.id;
|
||||||
|
eventPayload.projectIdentifier = details?.project_detail.identifier;
|
||||||
|
eventPayload.projectName = details?.project_detail.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cycleDetails) {
|
||||||
|
eventPayload.cycleId = cycleDetails.id;
|
||||||
|
eventPayload.cycleName = cycleDetails.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (moduleDetails) {
|
||||||
|
eventPayload.moduleId = moduleDetails.id;
|
||||||
|
eventPayload.moduleName = moduleDetails.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventType = tab === "scope_and_demand" ? "SCOPE_AND_DEMAND_ANALYTICS" : "CUSTOM_ANALYTICS";
|
||||||
|
|
||||||
|
trackEventService.trackAnalyticsEvent(
|
||||||
|
eventPayload,
|
||||||
|
cycleDetails ? `CYCLE_${eventType}` : moduleDetails ? `MODULE_${eventType}` : `PROJECT_${eventType}`,
|
||||||
|
user
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tab.Group as={React.Fragment}>
|
||||||
|
<Tab.List as="div" className="space-x-2 border-b border-custom-border-200 p-5 pt-0">
|
||||||
|
{ANALYTICS_TABS.map((tab) => (
|
||||||
|
<Tab
|
||||||
|
key={tab.key}
|
||||||
|
className={({ selected }) =>
|
||||||
|
`rounded-3xl border border-custom-border-200 px-4 py-2 text-xs hover:bg-custom-background-80 ${
|
||||||
|
selected ? "bg-custom-background-80" : ""
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
onClick={() => trackAnalyticsEvent(tab.key)}
|
||||||
|
>
|
||||||
|
{tab.title}
|
||||||
|
</Tab>
|
||||||
|
))}
|
||||||
|
</Tab.List>
|
||||||
|
<Tab.Panels as={React.Fragment}>
|
||||||
|
<Tab.Panel as={React.Fragment}>
|
||||||
|
<ScopeAndDemand fullScreen={fullScreen} />
|
||||||
|
</Tab.Panel>
|
||||||
|
<Tab.Panel as={React.Fragment}>
|
||||||
|
<CustomAnalytics
|
||||||
|
additionalParams={{
|
||||||
|
cycle: cycleDetails?.id,
|
||||||
|
module: moduleDetails?.id,
|
||||||
|
}}
|
||||||
|
fullScreen={fullScreen}
|
||||||
|
/>
|
||||||
|
</Tab.Panel>
|
||||||
|
</Tab.Panels>
|
||||||
|
</Tab.Group>
|
||||||
|
);
|
||||||
|
});
|
70
web/components/analytics/project-modal/modal.tsx
Normal file
70
web/components/analytics/project-modal/modal.tsx
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
|
|
||||||
|
// components
|
||||||
|
import { ProjectAnalyticsModalHeader, ProjectAnalyticsModalMainContent } from "components/analytics";
|
||||||
|
// types
|
||||||
|
import { ICycle, IModule, IProject } from "types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
cycleDetails?: ICycle | undefined;
|
||||||
|
moduleDetails?: IModule | undefined;
|
||||||
|
projectDetails?: IProject | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ProjectAnalyticsModal: React.FC<Props> = observer((props) => {
|
||||||
|
const { isOpen, onClose, cycleDetails, moduleDetails, projectDetails } = props;
|
||||||
|
|
||||||
|
const [fullScreen, setFullScreen] = useState(false);
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Transition.Root appear show={isOpen} as={React.Fragment}>
|
||||||
|
<Dialog as="div" className="relative z-20" onClose={handleClose}>
|
||||||
|
<div className="fixed inset-0 z-20 h-full w-full overflow-y-auto">
|
||||||
|
<Transition.Child
|
||||||
|
as={React.Fragment}
|
||||||
|
enter="transition-transform duration-300"
|
||||||
|
enterFrom="translate-x-full"
|
||||||
|
enterTo="translate-x-0"
|
||||||
|
leave="transition-transform duration-200"
|
||||||
|
leaveFrom="translate-x-0"
|
||||||
|
leaveTo="translate-x-full"
|
||||||
|
>
|
||||||
|
{/* TODO: fix full screen mode */}
|
||||||
|
<Dialog.Panel
|
||||||
|
className={`fixed z-20 bg-custom-background-100 top-0 right-0 h-full shadow-custom-shadow-md ${
|
||||||
|
fullScreen ? "w-full p-2" : "w-1/2"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`flex h-full flex-col overflow-hidden border-custom-border-200 bg-custom-background-100 text-left ${
|
||||||
|
fullScreen ? "rounded-lg border" : "border-l"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<ProjectAnalyticsModalHeader
|
||||||
|
fullScreen={fullScreen}
|
||||||
|
handleClose={handleClose}
|
||||||
|
setFullScreen={setFullScreen}
|
||||||
|
title={cycleDetails?.name ?? moduleDetails?.name ?? projectDetails?.name ?? ""}
|
||||||
|
/>
|
||||||
|
<ProjectAnalyticsModalMainContent
|
||||||
|
fullScreen={fullScreen}
|
||||||
|
cycleDetails={cycleDetails}
|
||||||
|
moduleDetails={moduleDetails}
|
||||||
|
projectDetails={projectDetails}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Dialog.Panel>
|
||||||
|
</Transition.Child>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</Transition.Root>
|
||||||
|
);
|
||||||
|
});
|
@ -17,12 +17,7 @@ type Props = {
|
|||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AnalyticsLeaderboard: React.FC<Props> = ({
|
export const AnalyticsLeaderBoard: React.FC<Props> = ({ users, title, emptyStateMessage, workspaceSlug }) => (
|
||||||
users,
|
|
||||||
title,
|
|
||||||
emptyStateMessage,
|
|
||||||
workspaceSlug,
|
|
||||||
}) => (
|
|
||||||
<div className="p-3 border border-custom-border-200 rounded-[10px]">
|
<div className="p-3 border border-custom-border-200 rounded-[10px]">
|
||||||
<h6 className="text-base font-medium">{title}</h6>
|
<h6 className="text-base font-medium">{title}</h6>
|
||||||
{users.length > 0 ? (
|
{users.length > 0 ? (
|
||||||
|
@ -3,16 +3,11 @@ import { useRouter } from "next/router";
|
|||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
|
||||||
// services
|
// services
|
||||||
import analyticsService from "services/analytics.service";
|
import { AnalyticsService } from "services/analytics.service";
|
||||||
// components
|
// components
|
||||||
import {
|
import { AnalyticsDemand, AnalyticsLeaderBoard, AnalyticsScope, AnalyticsYearWiseIssues } from "components/analytics";
|
||||||
AnalyticsDemand,
|
|
||||||
AnalyticsLeaderboard,
|
|
||||||
AnalyticsScope,
|
|
||||||
AnalyticsYearWiseIssues,
|
|
||||||
} from "components/analytics";
|
|
||||||
// ui
|
// ui
|
||||||
import { Loader, PrimaryButton } from "components/ui";
|
import { Button, Loader } from "@plane/ui";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { DEFAULT_ANALYTICS } from "constants/fetch-keys";
|
import { DEFAULT_ANALYTICS } from "constants/fetch-keys";
|
||||||
|
|
||||||
@ -20,7 +15,12 @@ type Props = {
|
|||||||
fullScreen?: boolean;
|
fullScreen?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ScopeAndDemand: React.FC<Props> = ({ fullScreen = true }) => {
|
// services
|
||||||
|
const analyticsService = new AnalyticsService();
|
||||||
|
|
||||||
|
export const ScopeAndDemand: React.FC<Props> = (props) => {
|
||||||
|
const { fullScreen = true } = props;
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
||||||
|
|
||||||
@ -40,9 +40,7 @@ export const ScopeAndDemand: React.FC<Props> = ({ fullScreen = true }) => {
|
|||||||
mutate: mutateDefaultAnalytics,
|
mutate: mutateDefaultAnalytics,
|
||||||
} = useSWR(
|
} = useSWR(
|
||||||
workspaceSlug ? DEFAULT_ANALYTICS(workspaceSlug.toString(), params) : null,
|
workspaceSlug ? DEFAULT_ANALYTICS(workspaceSlug.toString(), params) : null,
|
||||||
workspaceSlug
|
workspaceSlug ? () => analyticsService.getDefaultAnalytics(workspaceSlug.toString(), params) : null
|
||||||
? () => analyticsService.getDefaultAnalytics(workspaceSlug.toString(), params)
|
|
||||||
: null
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -53,7 +51,7 @@ export const ScopeAndDemand: React.FC<Props> = ({ fullScreen = true }) => {
|
|||||||
<div className={`grid grid-cols-1 gap-5 ${fullScreen ? "md:grid-cols-2" : ""}`}>
|
<div className={`grid grid-cols-1 gap-5 ${fullScreen ? "md:grid-cols-2" : ""}`}>
|
||||||
<AnalyticsDemand defaultAnalytics={defaultAnalytics} />
|
<AnalyticsDemand defaultAnalytics={defaultAnalytics} />
|
||||||
<AnalyticsScope defaultAnalytics={defaultAnalytics} />
|
<AnalyticsScope defaultAnalytics={defaultAnalytics} />
|
||||||
<AnalyticsLeaderboard
|
<AnalyticsLeaderBoard
|
||||||
users={defaultAnalytics.most_issue_created_user?.map((user) => ({
|
users={defaultAnalytics.most_issue_created_user?.map((user) => ({
|
||||||
avatar: user?.created_by__avatar,
|
avatar: user?.created_by__avatar,
|
||||||
firstName: user?.created_by__first_name,
|
firstName: user?.created_by__first_name,
|
||||||
@ -63,10 +61,10 @@ export const ScopeAndDemand: React.FC<Props> = ({ fullScreen = true }) => {
|
|||||||
id: user?.created_by__id,
|
id: user?.created_by__id,
|
||||||
}))}
|
}))}
|
||||||
title="Most issues created"
|
title="Most issues created"
|
||||||
emptyStateMessage="Co-workers and the number issues created by them appears here."
|
emptyStateMessage="Co-workers and the number of issues created by them appears here."
|
||||||
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
||||||
/>
|
/>
|
||||||
<AnalyticsLeaderboard
|
<AnalyticsLeaderBoard
|
||||||
users={defaultAnalytics.most_issue_closed_user?.map((user) => ({
|
users={defaultAnalytics.most_issue_closed_user?.map((user) => ({
|
||||||
avatar: user?.assignees__avatar,
|
avatar: user?.assignees__avatar,
|
||||||
firstName: user?.assignees__first_name,
|
firstName: user?.assignees__first_name,
|
||||||
@ -76,7 +74,7 @@ export const ScopeAndDemand: React.FC<Props> = ({ fullScreen = true }) => {
|
|||||||
id: user?.assignees__id,
|
id: user?.assignees__id,
|
||||||
}))}
|
}))}
|
||||||
title="Most issues closed"
|
title="Most issues closed"
|
||||||
emptyStateMessage="Co-workers and the number issues closed by them appears here."
|
emptyStateMessage="Co-workers and the number of issues closed by them appears here."
|
||||||
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
||||||
/>
|
/>
|
||||||
<div className={fullScreen ? "md:col-span-2" : ""}>
|
<div className={fullScreen ? "md:col-span-2" : ""}>
|
||||||
@ -97,7 +95,9 @@ export const ScopeAndDemand: React.FC<Props> = ({ fullScreen = true }) => {
|
|||||||
<div className="space-y-4 text-custom-text-200">
|
<div className="space-y-4 text-custom-text-200">
|
||||||
<p className="text-sm">There was some error in fetching the data.</p>
|
<p className="text-sm">There was some error in fetching the data.</p>
|
||||||
<div className="flex items-center justify-center gap-2">
|
<div className="flex items-center justify-center gap-2">
|
||||||
<PrimaryButton onClick={() => mutateDefaultAnalytics()}>Refresh</PrimaryButton>
|
<Button variant="primary" onClick={() => mutateDefaultAnalytics()}>
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -20,18 +20,15 @@ export const AnalyticsYearWiseIssues: React.FC<Props> = ({ defaultAnalytics }) =
|
|||||||
{
|
{
|
||||||
id: "issues_closed",
|
id: "issues_closed",
|
||||||
color: "rgb(var(--color-primary-100))",
|
color: "rgb(var(--color-primary-100))",
|
||||||
data: MONTHS_LIST.map((month) => ({
|
data: Object.entries(MONTHS_LIST).map(([index, month]) => ({
|
||||||
x: month.label.substring(0, 3),
|
x: month.shortTitle,
|
||||||
y:
|
y:
|
||||||
defaultAnalytics.issue_completed_month_wise.find(
|
defaultAnalytics.issue_completed_month_wise.find((data) => data.month === parseInt(index, 10))?.count ||
|
||||||
(data) => data.month === month.value
|
0,
|
||||||
)?.count || 0,
|
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
customYAxisTickValues={defaultAnalytics.issue_completed_month_wise.map(
|
customYAxisTickValues={defaultAnalytics.issue_completed_month_wise.map((data) => data.count)}
|
||||||
(data) => data.count
|
|
||||||
)}
|
|
||||||
height="300px"
|
height="300px"
|
||||||
colors={(datum) => datum.color}
|
colors={(datum) => datum.color}
|
||||||
curve="monotoneX"
|
curve="monotoneX"
|
||||||
|
@ -1,14 +1,11 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
import { mutate } from "swr";
|
import { mutate } from "swr";
|
||||||
|
|
||||||
// services
|
// services
|
||||||
import projectService from "services/project.service";
|
import { ProjectService } from "services/project";
|
||||||
// ui
|
// ui
|
||||||
import { PrimaryButton } from "components/ui";
|
import { Button } from "@plane/ui";
|
||||||
// icons
|
// icons
|
||||||
import { AssignmentClipboardIcon } from "components/icons";
|
import { AssignmentClipboardIcon } from "components/icons";
|
||||||
// images
|
// images
|
||||||
@ -16,6 +13,8 @@ import JoinProjectImg from "public/auth/project-not-authorized.svg";
|
|||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { USER_PROJECT_VIEW } from "constants/fetch-keys";
|
import { USER_PROJECT_VIEW } from "constants/fetch-keys";
|
||||||
|
|
||||||
|
const projectService = new ProjectService();
|
||||||
|
|
||||||
export const JoinProject: React.FC = () => {
|
export const JoinProject: React.FC = () => {
|
||||||
const [isJoiningProject, setIsJoiningProject] = useState(false);
|
const [isJoiningProject, setIsJoiningProject] = useState(false);
|
||||||
|
|
||||||
@ -27,9 +26,7 @@ export const JoinProject: React.FC = () => {
|
|||||||
|
|
||||||
setIsJoiningProject(true);
|
setIsJoiningProject(true);
|
||||||
projectService
|
projectService
|
||||||
.joinProject(workspaceSlug as string, {
|
.joinProject(workspaceSlug as string, [projectId as string])
|
||||||
project_ids: [projectId as string],
|
|
||||||
})
|
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
await mutate(USER_PROJECT_VIEW(projectId.toString()));
|
await mutate(USER_PROJECT_VIEW(projectId.toString()));
|
||||||
setIsJoiningProject(false);
|
setIsJoiningProject(false);
|
||||||
@ -45,25 +42,22 @@ export const JoinProject: React.FC = () => {
|
|||||||
<div className="h-44 w-72">
|
<div className="h-44 w-72">
|
||||||
<Image src={JoinProjectImg} height="176" width="288" alt="JoinProject" />
|
<Image src={JoinProjectImg} height="176" width="288" alt="JoinProject" />
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-xl font-medium text-custom-text-100">
|
<h1 className="text-xl font-medium text-custom-text-100">You are not a member of this project</h1>
|
||||||
You are not a member of this project
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<div className="w-full max-w-md text-base text-custom-text-200">
|
<div className="w-full max-w-md text-base text-custom-text-200">
|
||||||
<p className="mx-auto w-full text-sm md:w-3/4">
|
<p className="mx-auto w-full text-sm md:w-3/4">
|
||||||
You are not a member of this project, but you can join this project by clicking the button
|
You are not a member of this project, but you can join this project by clicking the button below.
|
||||||
below.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<PrimaryButton
|
<Button
|
||||||
className="flex items-center gap-1"
|
variant="primary"
|
||||||
|
prependIcon={<AssignmentClipboardIcon color="white" />}
|
||||||
loading={isJoiningProject}
|
loading={isJoiningProject}
|
||||||
onClick={handleJoin}
|
onClick={handleJoin}
|
||||||
>
|
>
|
||||||
<AssignmentClipboardIcon height={16} width={16} color="white" />
|
|
||||||
{isJoiningProject ? "Joining..." : "Click to join"}
|
{isJoiningProject ? "Joining..." : "Click to join"}
|
||||||
</PrimaryButton>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -3,7 +3,7 @@ import Link from "next/link";
|
|||||||
// layouts
|
// layouts
|
||||||
import DefaultLayout from "layouts/default-layout";
|
import DefaultLayout from "layouts/default-layout";
|
||||||
// ui
|
// ui
|
||||||
import { PrimaryButton, SecondaryButton } from "components/ui";
|
import { Button } from "@plane/ui";
|
||||||
|
|
||||||
export const NotAWorkspaceMember = () => (
|
export const NotAWorkspaceMember = () => (
|
||||||
<DefaultLayout>
|
<DefaultLayout>
|
||||||
@ -12,19 +12,19 @@ export const NotAWorkspaceMember = () => (
|
|||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<h3 className="text-lg font-semibold">Not Authorized!</h3>
|
<h3 className="text-lg font-semibold">Not Authorized!</h3>
|
||||||
<p className="mx-auto w-1/2 text-sm text-custom-text-200">
|
<p className="mx-auto w-1/2 text-sm text-custom-text-200">
|
||||||
You{"'"}re not a member of this workspace. Please contact the workspace admin to get an
|
You{"'"}re not a member of this workspace. Please contact the workspace admin to get an invitation or check
|
||||||
invitation or check your pending invitations.
|
your pending invitations.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-center gap-2">
|
<div className="flex items-center justify-center gap-2">
|
||||||
<Link href="/invitations">
|
<Link href="/invitations">
|
||||||
<a>
|
<a>
|
||||||
<SecondaryButton>Check pending invites</SecondaryButton>
|
<Button variant="neutral-primary">Check pending invites</Button>
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
<Link href="/create-workspace">
|
<Link href="/create-workspace">
|
||||||
<a>
|
<a>
|
||||||
<PrimaryButton>Create new workspace</PrimaryButton>
|
<Button variant="primary">Create new workspace</Button>
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
|
||||||
// component
|
// component
|
||||||
import { CustomSelect, ToggleSwitch } from "components/ui";
|
import { CustomSelect } from "components/ui";
|
||||||
|
import { ToggleSwitch } from "@plane/ui";
|
||||||
import { SelectMonthModal } from "components/automation";
|
import { SelectMonthModal } from "components/automation";
|
||||||
// icon
|
// icon
|
||||||
import { ArchiveRestore } from "lucide-react";
|
import { ArchiveRestore } from "lucide-react";
|
||||||
@ -16,11 +17,7 @@ type Props = {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AutoArchiveAutomation: React.FC<Props> = ({
|
export const AutoArchiveAutomation: React.FC<Props> = ({ projectDetails, handleChange, disabled = false }) => {
|
||||||
projectDetails,
|
|
||||||
handleChange,
|
|
||||||
disabled = false,
|
|
||||||
}) => {
|
|
||||||
const [monthModal, setmonthModal] = useState(false);
|
const [monthModal, setmonthModal] = useState(false);
|
||||||
|
|
||||||
const initialValues: Partial<IProject> = { archive_in: 1 };
|
const initialValues: Partial<IProject> = { archive_in: 1 };
|
||||||
@ -49,9 +46,7 @@ export const AutoArchiveAutomation: React.FC<Props> = ({
|
|||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
value={projectDetails?.archive_in !== 0}
|
value={projectDetails?.archive_in !== 0}
|
||||||
onChange={() =>
|
onChange={() =>
|
||||||
projectDetails?.archive_in === 0
|
projectDetails?.archive_in === 0 ? handleChange({ archive_in: 1 }) : handleChange({ archive_in: 0 })
|
||||||
? handleChange({ archive_in: 1 })
|
|
||||||
: handleChange({ archive_in: 0 })
|
|
||||||
}
|
}
|
||||||
size="sm"
|
size="sm"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
@ -61,15 +56,11 @@ export const AutoArchiveAutomation: React.FC<Props> = ({
|
|||||||
{projectDetails?.archive_in !== 0 && (
|
{projectDetails?.archive_in !== 0 && (
|
||||||
<div className="ml-12">
|
<div className="ml-12">
|
||||||
<div className="flex items-center justify-between rounded px-5 py-4 bg-custom-background-90 border border-custom-border-200 gap-2 w-full">
|
<div className="flex items-center justify-between rounded px-5 py-4 bg-custom-background-90 border border-custom-border-200 gap-2 w-full">
|
||||||
<div className="w-1/2 text-sm font-medium">
|
<div className="w-1/2 text-sm font-medium">Auto-archive issues that are closed for</div>
|
||||||
Auto-archive issues that are closed for
|
|
||||||
</div>
|
|
||||||
<div className="w-1/2">
|
<div className="w-1/2">
|
||||||
<CustomSelect
|
<CustomSelect
|
||||||
value={projectDetails?.archive_in}
|
value={projectDetails?.archive_in}
|
||||||
label={`${projectDetails?.archive_in} ${
|
label={`${projectDetails?.archive_in} ${projectDetails?.archive_in === 1 ? "Month" : "Months"}`}
|
||||||
projectDetails?.archive_in === 1 ? "Month" : "Months"
|
|
||||||
}`}
|
|
||||||
onChange={(val: number) => {
|
onChange={(val: number) => {
|
||||||
handleChange({ archive_in: val });
|
handleChange({ archive_in: val });
|
||||||
}}
|
}}
|
||||||
|
@ -1,18 +1,16 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
// component
|
// component
|
||||||
import { CustomSearchSelect, CustomSelect, Icon, ToggleSwitch } from "components/ui";
|
import { CustomSearchSelect, CustomSelect } from "components/ui";
|
||||||
import { SelectMonthModal } from "components/automation";
|
import { SelectMonthModal } from "components/automation";
|
||||||
|
import { ToggleSwitch } from "@plane/ui";
|
||||||
// icons
|
// icons
|
||||||
import { Squares2X2Icon } from "@heroicons/react/24/outline";
|
import { Squares2X2Icon } from "@heroicons/react/24/outline";
|
||||||
import { StateGroupIcon } from "components/icons";
|
import { StateGroupIcon } from "components/icons";
|
||||||
import { ArchiveX } from "lucide-react";
|
import { ArchiveX } from "lucide-react";
|
||||||
// services
|
// services
|
||||||
import stateService from "services/state.service";
|
import { ProjectStateService } from "services/project";
|
||||||
// constants
|
// constants
|
||||||
import { PROJECT_AUTOMATION_MONTHS } from "constants/project";
|
import { PROJECT_AUTOMATION_MONTHS } from "constants/project";
|
||||||
import { STATES_LIST } from "constants/fetch-keys";
|
import { STATES_LIST } from "constants/fetch-keys";
|
||||||
@ -27,11 +25,9 @@ type Props = {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AutoCloseAutomation: React.FC<Props> = ({
|
const projectStateService = new ProjectStateService();
|
||||||
projectDetails,
|
|
||||||
handleChange,
|
export const AutoCloseAutomation: React.FC<Props> = ({ projectDetails, handleChange, disabled = false }) => {
|
||||||
disabled = false,
|
|
||||||
}) => {
|
|
||||||
const [monthModal, setmonthModal] = useState(false);
|
const [monthModal, setmonthModal] = useState(false);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -40,7 +36,7 @@ export const AutoCloseAutomation: React.FC<Props> = ({
|
|||||||
const { data: stateGroups } = useSWR(
|
const { data: stateGroups } = useSWR(
|
||||||
workspaceSlug && projectId ? STATES_LIST(projectId as string) : null,
|
workspaceSlug && projectId ? STATES_LIST(projectId as string) : null,
|
||||||
workspaceSlug && projectId
|
workspaceSlug && projectId
|
||||||
? () => stateService.getStates(workspaceSlug as string, projectId as string)
|
? () => projectStateService.getStates(workspaceSlug as string, projectId as string)
|
||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
const states = getStatesList(stateGroups);
|
const states = getStatesList(stateGroups);
|
||||||
@ -62,9 +58,7 @@ export const AutoCloseAutomation: React.FC<Props> = ({
|
|||||||
|
|
||||||
const defaultState = stateGroups && stateGroups.cancelled ? stateGroups.cancelled[0].id : null;
|
const defaultState = stateGroups && stateGroups.cancelled ? stateGroups.cancelled[0].id : null;
|
||||||
|
|
||||||
const selectedOption = states?.find(
|
const selectedOption = states?.find((s) => s.id === projectDetails?.default_state ?? defaultState);
|
||||||
(s) => s.id === projectDetails?.default_state ?? defaultState
|
|
||||||
);
|
|
||||||
const currentDefaultState = states?.find((s) => s.id === defaultState);
|
const currentDefaultState = states?.find((s) => s.id === defaultState);
|
||||||
|
|
||||||
const initialValues: Partial<IProject> = {
|
const initialValues: Partial<IProject> = {
|
||||||
@ -111,15 +105,11 @@ export const AutoCloseAutomation: React.FC<Props> = ({
|
|||||||
<div className="ml-12">
|
<div className="ml-12">
|
||||||
<div className="flex flex-col rounded bg-custom-background-90 border border-custom-border-200 p-2">
|
<div className="flex flex-col rounded bg-custom-background-90 border border-custom-border-200 p-2">
|
||||||
<div className="flex items-center justify-between px-5 py-4 gap-2 w-full">
|
<div className="flex items-center justify-between px-5 py-4 gap-2 w-full">
|
||||||
<div className="w-1/2 text-sm font-medium">
|
<div className="w-1/2 text-sm font-medium">Auto-close issues that are inactive for</div>
|
||||||
Auto-close issues that are inactive for
|
|
||||||
</div>
|
|
||||||
<div className="w-1/2">
|
<div className="w-1/2">
|
||||||
<CustomSelect
|
<CustomSelect
|
||||||
value={projectDetails?.close_in}
|
value={projectDetails?.close_in}
|
||||||
label={`${projectDetails?.close_in} ${
|
label={`${projectDetails?.close_in} ${projectDetails?.close_in === 1 ? "Month" : "Months"}`}
|
||||||
projectDetails?.close_in === 1 ? "Month" : "Months"
|
|
||||||
}`}
|
|
||||||
onChange={(val: number) => {
|
onChange={(val: number) => {
|
||||||
handleChange({ close_in: val });
|
handleChange({ close_in: val });
|
||||||
}}
|
}}
|
||||||
@ -149,9 +139,7 @@ export const AutoCloseAutomation: React.FC<Props> = ({
|
|||||||
<div className="w-1/2 text-sm font-medium">Auto-close Status</div>
|
<div className="w-1/2 text-sm font-medium">Auto-close Status</div>
|
||||||
<div className="w-1/2 ">
|
<div className="w-1/2 ">
|
||||||
<CustomSearchSelect
|
<CustomSearchSelect
|
||||||
value={
|
value={projectDetails?.default_state ? projectDetails?.default_state : defaultState}
|
||||||
projectDetails?.default_state ? projectDetails?.default_state : defaultState
|
|
||||||
}
|
|
||||||
label={
|
label={
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{selectedOption ? (
|
{selectedOption ? (
|
||||||
@ -173,9 +161,7 @@ export const AutoCloseAutomation: React.FC<Props> = ({
|
|||||||
)}
|
)}
|
||||||
{selectedOption?.name
|
{selectedOption?.name
|
||||||
? selectedOption.name
|
? selectedOption.name
|
||||||
: currentDefaultState?.name ?? (
|
: currentDefaultState?.name ?? <span className="text-custom-text-200">State</span>}
|
||||||
<span className="text-custom-text-200">State</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
onChange={(val: string) => {
|
onChange={(val: string) => {
|
||||||
|
@ -1,13 +1,11 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
// react-hook-form
|
// react-hook-form
|
||||||
import { useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
// headless ui
|
// headless ui
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
// ui
|
// ui
|
||||||
import { Input, PrimaryButton, SecondaryButton } from "components/ui";
|
import { Button, Input } from "@plane/ui";
|
||||||
// types
|
// types
|
||||||
import type { IProject } from "types";
|
import type { IProject } from "types";
|
||||||
|
|
||||||
@ -20,20 +18,14 @@ type Props = {
|
|||||||
handleChange: (formData: Partial<IProject>) => Promise<void>;
|
handleChange: (formData: Partial<IProject>) => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SelectMonthModal: React.FC<Props> = ({
|
export const SelectMonthModal: React.FC<Props> = ({ type, initialValues, isOpen, handleClose, handleChange }) => {
|
||||||
type,
|
|
||||||
initialValues,
|
|
||||||
isOpen,
|
|
||||||
handleClose,
|
|
||||||
handleChange,
|
|
||||||
}) => {
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
|
||||||
formState: { errors, isSubmitting },
|
formState: { errors, isSubmitting },
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
|
control,
|
||||||
reset,
|
reset,
|
||||||
} = useForm<IProject>({
|
} = useForm<IProject>({
|
||||||
defaultValues: initialValues,
|
defaultValues: initialValues,
|
||||||
@ -50,27 +42,6 @@ export const SelectMonthModal: React.FC<Props> = ({
|
|||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
const inputSection = (name: string) => (
|
|
||||||
<div className="relative flex flex-col gap-1 justify-center w-full">
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
id={name}
|
|
||||||
name={name}
|
|
||||||
placeholder="Enter Months"
|
|
||||||
autoComplete="off"
|
|
||||||
register={register}
|
|
||||||
width="full"
|
|
||||||
validations={{
|
|
||||||
required: "Select a month between 1 and 12.",
|
|
||||||
min: 1,
|
|
||||||
max: 12,
|
|
||||||
}}
|
|
||||||
className="border-custom-border-200"
|
|
||||||
/>
|
|
||||||
<span className="absolute text-sm text-custom-text-200 top-2.5 right-8">Months</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Transition.Root show={isOpen} as={React.Fragment}>
|
<Transition.Root show={isOpen} as={React.Fragment}>
|
||||||
<Dialog as="div" className="relative z-30" onClose={onClose}>
|
<Dialog as="div" className="relative z-30" onClose={onClose}>
|
||||||
@ -100,30 +71,72 @@ export const SelectMonthModal: React.FC<Props> = ({
|
|||||||
<Dialog.Panel className="relative transform rounded-lg bg-custom-background-90 px-4 pt-5 pb-4 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-custom-background-90 px-4 pt-5 pb-4 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6">
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
<div>
|
<div>
|
||||||
<Dialog.Title
|
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-custom-text-100">
|
||||||
as="h3"
|
|
||||||
className="text-lg font-medium leading-6 text-custom-text-100"
|
|
||||||
>
|
|
||||||
Customise Time Range
|
Customise Time Range
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
<div className="mt-8 flex items-center gap-2">
|
<div className="mt-8 flex items-center gap-2">
|
||||||
<div className="flex w-full flex-col gap-1 justify-center">
|
<div className="flex w-full flex-col gap-1 justify-center">
|
||||||
{type === "auto-close" ? (
|
{type === "auto-close" ? (
|
||||||
<>
|
<>
|
||||||
{inputSection("close_in")}
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="close_in"
|
||||||
|
rules={{
|
||||||
|
required: "Select a month between 1 and 12.",
|
||||||
|
min: 1,
|
||||||
|
max: 12,
|
||||||
|
}}
|
||||||
|
render={({ field: { value, onChange, ref } }) => (
|
||||||
|
<div className="relative flex flex-col gap-1 justify-center w-full">
|
||||||
|
<Input
|
||||||
|
id="close_in"
|
||||||
|
name="close_in"
|
||||||
|
type="number"
|
||||||
|
value={value.toString()}
|
||||||
|
onChange={onChange}
|
||||||
|
ref={ref}
|
||||||
|
hasError={Boolean(errors.close_in)}
|
||||||
|
placeholder="Enter Months"
|
||||||
|
className="border-custom-border-200 w-full"
|
||||||
|
/>
|
||||||
|
<span className="absolute text-sm text-custom-text-200 top-2.5 right-8">Months</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
{errors.close_in && (
|
{errors.close_in && (
|
||||||
<span className="text-sm px-1 text-red-500">
|
<span className="text-sm px-1 text-red-500">Select a month between 1 and 12.</span>
|
||||||
Select a month between 1 and 12.
|
|
||||||
</span>
|
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{inputSection("archive_in")}
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="archive_in"
|
||||||
|
rules={{
|
||||||
|
required: "Select a month between 1 and 12.",
|
||||||
|
min: 1,
|
||||||
|
max: 12,
|
||||||
|
}}
|
||||||
|
render={({ field: { value, onChange, ref } }) => (
|
||||||
|
<div className="relative flex flex-col gap-1 justify-center w-full">
|
||||||
|
<Input
|
||||||
|
id="archive_in"
|
||||||
|
name="archive_in"
|
||||||
|
type="number"
|
||||||
|
value={value.toString()}
|
||||||
|
onChange={onChange}
|
||||||
|
ref={ref}
|
||||||
|
hasError={Boolean(errors.archive_in)}
|
||||||
|
placeholder="Enter Months"
|
||||||
|
className="border-custom-border-200 w-full"
|
||||||
|
/>
|
||||||
|
<span className="absolute text-sm text-custom-text-200 top-2.5 right-8">Months</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
{errors.archive_in && (
|
{errors.archive_in && (
|
||||||
<span className="text-sm px-1 text-red-500">
|
<span className="text-sm px-1 text-red-500">Select a month between 1 and 12.</span>
|
||||||
Select a month between 1 and 12.
|
|
||||||
</span>
|
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@ -131,10 +144,12 @@ export const SelectMonthModal: React.FC<Props> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-5 flex justify-end gap-2">
|
<div className="mt-5 flex justify-end gap-2">
|
||||||
<SecondaryButton onClick={onClose}>Cancel</SecondaryButton>
|
<Button variant="neutral-primary" onClick={onClose}>
|
||||||
<PrimaryButton type="submit" loading={isSubmitting}>
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button variant="primary" type="submit" loading={isSubmitting}>
|
||||||
{isSubmitting ? "Submitting..." : "Submit"}
|
{isSubmitting ? "Submitting..." : "Submit"}
|
||||||
</PrimaryButton>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Dialog.Panel>
|
</Dialog.Panel>
|
||||||
|
@ -5,7 +5,6 @@ import { Command } from "cmdk";
|
|||||||
import { THEMES_OBJ } from "constants/themes";
|
import { THEMES_OBJ } from "constants/themes";
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "next-themes";
|
||||||
import { SettingIcon } from "components/icons";
|
import { SettingIcon } from "components/icons";
|
||||||
import userService from "services/user.service";
|
|
||||||
import useUser from "hooks/use-user";
|
import useUser from "hooks/use-user";
|
||||||
// helper
|
// helper
|
||||||
import { unsetCustomCssVariables } from "helpers/theme.helper";
|
import { unsetCustomCssVariables } from "helpers/theme.helper";
|
||||||
@ -25,7 +24,7 @@ export const ChangeInterfaceTheme: React.FC<Props> = observer(({ setIsPaletteOpe
|
|||||||
|
|
||||||
const { setTheme } = useTheme();
|
const { setTheme } = useTheme();
|
||||||
|
|
||||||
const { user, mutateUser } = useUser();
|
const { user } = useUser();
|
||||||
|
|
||||||
const updateUserTheme = (newTheme: string) => {
|
const updateUserTheme = (newTheme: string) => {
|
||||||
if (!user) return;
|
if (!user) return;
|
||||||
|
@ -9,9 +9,9 @@ import { Command } from "cmdk";
|
|||||||
// headless ui
|
// headless ui
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
// services
|
// services
|
||||||
import workspaceService from "services/workspace.service";
|
import { WorkspaceService } from "services/workspace.service";
|
||||||
import issuesService from "services/issues.service";
|
import { IssueService } from "services/issue";
|
||||||
import inboxService from "services/inbox.service";
|
import { InboxService } from "services/inbox.service";
|
||||||
// hooks
|
// hooks
|
||||||
import useProjectDetails from "hooks/use-project-details";
|
import useProjectDetails from "hooks/use-project-details";
|
||||||
import useDebounce from "hooks/use-debounce";
|
import useDebounce from "hooks/use-debounce";
|
||||||
@ -26,7 +26,8 @@ import {
|
|||||||
commandGroups,
|
commandGroups,
|
||||||
} from "components/command-palette";
|
} from "components/command-palette";
|
||||||
// ui
|
// ui
|
||||||
import { Icon, Loader, ToggleSwitch, Tooltip } from "components/ui";
|
import { Icon } from "components/ui";
|
||||||
|
import { Loader, ToggleSwitch, Tooltip } from "@plane/ui";
|
||||||
// icons
|
// icons
|
||||||
import { DiscordIcon, GithubIcon, SettingIcon } from "components/icons";
|
import { DiscordIcon, GithubIcon, SettingIcon } from "components/icons";
|
||||||
import { InboxIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline";
|
import { InboxIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline";
|
||||||
@ -43,6 +44,11 @@ type Props = {
|
|||||||
setIsPaletteOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
setIsPaletteOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// services
|
||||||
|
const workspaceService = new WorkspaceService();
|
||||||
|
const issueService = new IssueService();
|
||||||
|
const inboxService = new InboxService();
|
||||||
|
|
||||||
export const CommandK: React.FC<Props> = ({ deleteIssue, isPaletteOpen, setIsPaletteOpen }) => {
|
export const CommandK: React.FC<Props> = ({ deleteIssue, isPaletteOpen, setIsPaletteOpen }) => {
|
||||||
const [placeholder, setPlaceholder] = useState("Type a command or search...");
|
const [placeholder, setPlaceholder] = useState("Type a command or search...");
|
||||||
|
|
||||||
@ -79,16 +85,13 @@ export const CommandK: React.FC<Props> = ({ deleteIssue, isPaletteOpen, setIsPal
|
|||||||
const { data: issueDetails } = useSWR(
|
const { data: issueDetails } = useSWR(
|
||||||
workspaceSlug && projectId && issueId ? ISSUE_DETAILS(issueId as string) : null,
|
workspaceSlug && projectId && issueId ? ISSUE_DETAILS(issueId as string) : null,
|
||||||
workspaceSlug && projectId && issueId
|
workspaceSlug && projectId && issueId
|
||||||
? () =>
|
? () => issueService.retrieve(workspaceSlug as string, projectId as string, issueId as string)
|
||||||
issuesService.retrieve(workspaceSlug as string, projectId as string, issueId as string)
|
|
||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data: inboxList } = useSWR(
|
const { data: inboxList } = useSWR(
|
||||||
workspaceSlug && projectId ? INBOX_LIST(projectId as string) : null,
|
workspaceSlug && projectId ? INBOX_LIST(projectId as string) : null,
|
||||||
workspaceSlug && projectId
|
workspaceSlug && projectId ? () => inboxService.getInboxes(workspaceSlug as string, projectId as string) : null
|
||||||
? () => inboxService.getInboxes(workspaceSlug as string, projectId as string)
|
|
||||||
: null
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const updateIssue = useCallback(
|
const updateIssue = useCallback(
|
||||||
@ -110,7 +113,7 @@ export const CommandK: React.FC<Props> = ({ deleteIssue, isPaletteOpen, setIsPal
|
|||||||
);
|
);
|
||||||
|
|
||||||
const payload = { ...formData };
|
const payload = { ...formData };
|
||||||
await issuesService
|
await issueService
|
||||||
.patchIssue(workspaceSlug as string, projectId as string, issueId as string, payload, user)
|
.patchIssue(workspaceSlug as string, projectId as string, issueId as string, payload, user)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
mutate(PROJECT_ISSUES_ACTIVITY(issueId as string));
|
mutate(PROJECT_ISSUES_ACTIVITY(issueId as string));
|
||||||
@ -272,8 +275,7 @@ export const CommandK: React.FC<Props> = ({ deleteIssue, isPaletteOpen, setIsPal
|
|||||||
>
|
>
|
||||||
{issueDetails && (
|
{issueDetails && (
|
||||||
<div className="overflow-hidden truncate rounded-md bg-custom-background-80 p-2 text-xs font-medium text-custom-text-200">
|
<div className="overflow-hidden truncate rounded-md bg-custom-background-80 p-2 text-xs font-medium text-custom-text-200">
|
||||||
{issueDetails.project_detail.identifier}-{issueDetails.sequence_id}{" "}
|
{issueDetails.project_detail.identifier}-{issueDetails.sequence_id} {issueDetails.name}
|
||||||
{issueDetails.name}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{projectId && (
|
{projectId && (
|
||||||
@ -324,12 +326,9 @@ export const CommandK: React.FC<Props> = ({ deleteIssue, isPaletteOpen, setIsPal
|
|||||||
</h5>
|
</h5>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!isLoading &&
|
{!isLoading && resultsCount === 0 && searchTerm !== "" && debouncedSearchTerm !== "" && (
|
||||||
resultsCount === 0 &&
|
<div className="my-4 text-center text-custom-text-200">No results found.</div>
|
||||||
searchTerm !== "" &&
|
)}
|
||||||
debouncedSearchTerm !== "" && (
|
|
||||||
<div className="my-4 text-center text-custom-text-200">No results found.</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{(isLoading || isSearching) && (
|
{(isLoading || isSearching) && (
|
||||||
<Command.Loading>
|
<Command.Loading>
|
||||||
@ -362,9 +361,7 @@ export const CommandK: React.FC<Props> = ({ deleteIssue, isPaletteOpen, setIsPal
|
|||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 overflow-hidden text-custom-text-200">
|
<div className="flex items-center gap-2 overflow-hidden text-custom-text-200">
|
||||||
<Icon iconName={currentSection.icon} />
|
<Icon iconName={currentSection.icon} />
|
||||||
<p className="block flex-1 truncate">
|
<p className="block flex-1 truncate">{currentSection.itemName(item)}</p>
|
||||||
{currentSection.itemName(item)}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</Command.Item>
|
</Command.Item>
|
||||||
))}
|
))}
|
||||||
@ -577,9 +574,7 @@ export const CommandK: React.FC<Props> = ({ deleteIssue, isPaletteOpen, setIsPal
|
|||||||
<Command.Item
|
<Command.Item
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
setIsPaletteOpen(false);
|
setIsPaletteOpen(false);
|
||||||
redirect(
|
redirect(`/${workspaceSlug}/projects/${projectId}/inbox/${inboxList?.[0]?.id}`);
|
||||||
`/${workspaceSlug}/projects/${projectId}/inbox/${inboxList?.[0]?.id}`
|
|
||||||
);
|
|
||||||
}}
|
}}
|
||||||
className="focus:outline-none"
|
className="focus:outline-none"
|
||||||
>
|
>
|
||||||
@ -672,10 +667,7 @@ export const CommandK: React.FC<Props> = ({ deleteIssue, isPaletteOpen, setIsPal
|
|||||||
<Command.Item
|
<Command.Item
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
setIsPaletteOpen(false);
|
setIsPaletteOpen(false);
|
||||||
window.open(
|
window.open("https://github.com/makeplane/plane/issues/new/choose", "_blank");
|
||||||
"https://github.com/makeplane/plane/issues/new/choose",
|
|
||||||
"_blank"
|
|
||||||
);
|
|
||||||
}}
|
}}
|
||||||
className="focus:outline-none"
|
className="focus:outline-none"
|
||||||
>
|
>
|
||||||
@ -759,29 +751,15 @@ export const CommandK: React.FC<Props> = ({ deleteIssue, isPaletteOpen, setIsPal
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{page === "change-issue-state" && issueDetails && (
|
{page === "change-issue-state" && issueDetails && (
|
||||||
<ChangeIssueState
|
<ChangeIssueState issue={issueDetails} setIsPaletteOpen={setIsPaletteOpen} user={user} />
|
||||||
issue={issueDetails}
|
|
||||||
setIsPaletteOpen={setIsPaletteOpen}
|
|
||||||
user={user}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
{page === "change-issue-priority" && issueDetails && (
|
{page === "change-issue-priority" && issueDetails && (
|
||||||
<ChangeIssuePriority
|
<ChangeIssuePriority issue={issueDetails} setIsPaletteOpen={setIsPaletteOpen} user={user} />
|
||||||
issue={issueDetails}
|
|
||||||
setIsPaletteOpen={setIsPaletteOpen}
|
|
||||||
user={user}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
{page === "change-issue-assignee" && issueDetails && (
|
{page === "change-issue-assignee" && issueDetails && (
|
||||||
<ChangeIssueAssignee
|
<ChangeIssueAssignee issue={issueDetails} setIsPaletteOpen={setIsPaletteOpen} user={user} />
|
||||||
issue={issueDetails}
|
|
||||||
setIsPaletteOpen={setIsPaletteOpen}
|
|
||||||
user={user}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{page === "change-interface-theme" && (
|
|
||||||
<ChangeInterfaceTheme setIsPaletteOpen={setIsPaletteOpen} />
|
|
||||||
)}
|
)}
|
||||||
|
{page === "change-interface-theme" && <ChangeInterfaceTheme setIsPaletteOpen={setIsPaletteOpen} />}
|
||||||
</Command.List>
|
</Command.List>
|
||||||
</Command>
|
</Command>
|
||||||
</Dialog.Panel>
|
</Dialog.Panel>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React, { useCallback, useEffect, useState } from "react";
|
import React, { useCallback, useEffect, useState } from "react";
|
||||||
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
import useUser from "hooks/use-user";
|
import useUser from "hooks/use-user";
|
||||||
@ -12,19 +12,19 @@ import { CreateUpdateCycleModal } from "components/cycles";
|
|||||||
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
||||||
import { CreateUpdateModuleModal } from "components/modules";
|
import { CreateUpdateModuleModal } from "components/modules";
|
||||||
import { CreateProjectModal } from "components/project";
|
import { CreateProjectModal } from "components/project";
|
||||||
import { CreateUpdateViewModal } from "components/views";
|
import { CreateUpdateProjectViewModal } from "components/views";
|
||||||
import { CreateUpdatePageModal } from "components/pages";
|
import { CreateUpdatePageModal } from "components/pages";
|
||||||
// helpers
|
// helpers
|
||||||
import { copyTextToClipboard } from "helpers/string.helper";
|
import { copyTextToClipboard } from "helpers/string.helper";
|
||||||
// services
|
// services
|
||||||
import issuesService from "services/issues.service";
|
import { IssueService } from "services/issue";
|
||||||
import inboxService from "services/inbox.service";
|
|
||||||
// fetch keys
|
// fetch keys
|
||||||
import { INBOX_LIST, ISSUE_DETAILS } from "constants/fetch-keys";
|
import { ISSUE_DETAILS } from "constants/fetch-keys";
|
||||||
// mobx store
|
// mobx store
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
import { observable } from "mobx";
|
|
||||||
import { observer } from "mobx-react-lite";
|
// services
|
||||||
|
const issueService = new IssueService();
|
||||||
|
|
||||||
export const CommandPalette: React.FC = observer(() => {
|
export const CommandPalette: React.FC = observer(() => {
|
||||||
const store: any = useMobxStore();
|
const store: any = useMobxStore();
|
||||||
@ -50,8 +50,7 @@ export const CommandPalette: React.FC = observer(() => {
|
|||||||
const { data: issueDetails } = useSWR(
|
const { data: issueDetails } = useSWR(
|
||||||
workspaceSlug && projectId && issueId ? ISSUE_DETAILS(issueId as string) : null,
|
workspaceSlug && projectId && issueId ? ISSUE_DETAILS(issueId as string) : null,
|
||||||
workspaceSlug && projectId && issueId
|
workspaceSlug && projectId && issueId
|
||||||
? () =>
|
? () => issueService.retrieve(workspaceSlug as string, projectId as string, issueId as string)
|
||||||
issuesService.retrieve(workspaceSlug as string, projectId as string, issueId as string)
|
|
||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -76,7 +75,7 @@ export const CommandPalette: React.FC = observer(() => {
|
|||||||
|
|
||||||
const handleKeyDown = useCallback(
|
const handleKeyDown = useCallback(
|
||||||
(e: KeyboardEvent) => {
|
(e: KeyboardEvent) => {
|
||||||
const { key, ctrlKey, metaKey, altKey, shiftKey } = e;
|
const { key, ctrlKey, metaKey, altKey } = e;
|
||||||
if (!key) return;
|
if (!key) return;
|
||||||
|
|
||||||
const keyPressed = key.toLowerCase();
|
const keyPressed = key.toLowerCase();
|
||||||
@ -121,7 +120,7 @@ export const CommandPalette: React.FC = observer(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[copyIssueUrlToClipboard]
|
[copyIssueUrlToClipboard, store.theme]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -141,11 +140,7 @@ export const CommandPalette: React.FC = observer(() => {
|
|||||||
<>
|
<>
|
||||||
<ShortcutsModal isOpen={isShortcutsModalOpen} setIsOpen={setIsShortcutsModalOpen} />
|
<ShortcutsModal isOpen={isShortcutsModalOpen} setIsOpen={setIsShortcutsModalOpen} />
|
||||||
{workspaceSlug && (
|
{workspaceSlug && (
|
||||||
<CreateProjectModal
|
<CreateProjectModal isOpen={isProjectModalOpen} setIsOpen={setIsProjectModalOpen} user={user} />
|
||||||
isOpen={isProjectModalOpen}
|
|
||||||
setIsOpen={setIsProjectModalOpen}
|
|
||||||
user={user}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
{projectId && (
|
{projectId && (
|
||||||
<>
|
<>
|
||||||
@ -159,10 +154,9 @@ export const CommandPalette: React.FC = observer(() => {
|
|||||||
setIsOpen={setIsCreateModuleModalOpen}
|
setIsOpen={setIsCreateModuleModalOpen}
|
||||||
user={user}
|
user={user}
|
||||||
/>
|
/>
|
||||||
<CreateUpdateViewModal
|
<CreateUpdateProjectViewModal
|
||||||
handleClose={() => setIsCreateViewModalOpen(false)}
|
|
||||||
isOpen={isCreateViewModalOpen}
|
isOpen={isCreateViewModalOpen}
|
||||||
user={user}
|
onClose={() => setIsCreateViewModalOpen(false)}
|
||||||
/>
|
/>
|
||||||
<CreateUpdatePageModal
|
<CreateUpdatePageModal
|
||||||
isOpen={isCreateUpdatePageModalOpen}
|
isOpen={isCreateUpdatePageModalOpen}
|
||||||
@ -184,11 +178,7 @@ export const CommandPalette: React.FC = observer(() => {
|
|||||||
handleClose={() => setIsIssueModalOpen(false)}
|
handleClose={() => setIsIssueModalOpen(false)}
|
||||||
fieldsToShow={inboxId ? ["name", "description", "priority"] : ["all"]}
|
fieldsToShow={inboxId ? ["name", "description", "priority"] : ["all"]}
|
||||||
prePopulateData={
|
prePopulateData={
|
||||||
cycleId
|
cycleId ? { cycle: cycleId.toString() } : moduleId ? { module: moduleId.toString() } : undefined
|
||||||
? { cycle: cycleId.toString() }
|
|
||||||
: moduleId
|
|
||||||
? { module: moduleId.toString() }
|
|
||||||
: undefined
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<BulkDeleteIssuesModal
|
<BulkDeleteIssuesModal
|
||||||
@ -196,11 +186,7 @@ export const CommandPalette: React.FC = observer(() => {
|
|||||||
setIsOpen={setIsBulkDeleteIssuesModalOpen}
|
setIsOpen={setIsBulkDeleteIssuesModalOpen}
|
||||||
user={user}
|
user={user}
|
||||||
/>
|
/>
|
||||||
<CommandK
|
<CommandK deleteIssue={deleteIssue} isPaletteOpen={isPaletteOpen} setIsPaletteOpen={setIsPaletteOpen} />
|
||||||
deleteIssue={deleteIssue}
|
|
||||||
isPaletteOpen={isPaletteOpen}
|
|
||||||
setIsPaletteOpen={setIsPaletteOpen}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -1,13 +1,9 @@
|
|||||||
import React, { Dispatch, SetStateAction, useCallback } from "react";
|
import { Dispatch, SetStateAction, useCallback, FC } from "react";
|
||||||
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
import { mutate } from "swr";
|
import { mutate } from "swr";
|
||||||
|
|
||||||
// cmdk
|
|
||||||
import { Command } from "cmdk";
|
import { Command } from "cmdk";
|
||||||
// services
|
// services
|
||||||
import issuesService from "services/issues.service";
|
import { IssueService } from "services/issue";
|
||||||
// hooks
|
// hooks
|
||||||
import useProjectMembers from "hooks/use-project-members";
|
import useProjectMembers from "hooks/use-project-members";
|
||||||
// constants
|
// constants
|
||||||
@ -17,22 +13,25 @@ import { Avatar } from "components/ui";
|
|||||||
// icons
|
// icons
|
||||||
import { CheckIcon } from "components/icons";
|
import { CheckIcon } from "components/icons";
|
||||||
// types
|
// types
|
||||||
import { ICurrentUserResponse, IIssue } from "types";
|
import { IUser, IIssue } from "types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
setIsPaletteOpen: Dispatch<SetStateAction<boolean>>;
|
setIsPaletteOpen: Dispatch<SetStateAction<boolean>>;
|
||||||
issue: IIssue;
|
issue: IIssue;
|
||||||
user: ICurrentUserResponse | undefined;
|
user: IUser | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ChangeIssueAssignee: React.FC<Props> = ({ setIsPaletteOpen, issue, user }) => {
|
// services
|
||||||
|
const issueService = new IssueService();
|
||||||
|
|
||||||
|
export const ChangeIssueAssignee: FC<Props> = ({ setIsPaletteOpen, issue, user }) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, issueId } = router.query;
|
const { workspaceSlug, projectId, issueId } = router.query;
|
||||||
|
|
||||||
const { members } = useProjectMembers(workspaceSlug as string, projectId as string);
|
const { members } = useProjectMembers(workspaceSlug as string, projectId as string);
|
||||||
|
|
||||||
const options =
|
const options =
|
||||||
members?.map(({ member }) => ({
|
members?.map(({ member }: any) => ({
|
||||||
value: member.id,
|
value: member.id,
|
||||||
query: member.display_name,
|
query: member.display_name,
|
||||||
content: (
|
content: (
|
||||||
@ -67,7 +66,7 @@ export const ChangeIssueAssignee: React.FC<Props> = ({ setIsPaletteOpen, issue,
|
|||||||
);
|
);
|
||||||
|
|
||||||
const payload = { ...formData };
|
const payload = { ...formData };
|
||||||
await issuesService
|
await issueService
|
||||||
.patchIssue(workspaceSlug as string, projectId as string, issueId as string, payload, user)
|
.patchIssue(workspaceSlug as string, projectId as string, issueId as string, payload, user)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
mutate(PROJECT_ISSUES_ACTIVITY(issueId as string));
|
mutate(PROJECT_ISSUES_ACTIVITY(issueId as string));
|
||||||
@ -94,7 +93,7 @@ export const ChangeIssueAssignee: React.FC<Props> = ({ setIsPaletteOpen, issue,
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{options.map((option) => (
|
{options.map((option: any) => (
|
||||||
<Command.Item
|
<Command.Item
|
||||||
key={option.value}
|
key={option.value}
|
||||||
onSelect={() => handleIssueAssignees(option.value)}
|
onSelect={() => handleIssueAssignees(option.value)}
|
||||||
|
@ -7,9 +7,9 @@ import { mutate } from "swr";
|
|||||||
// cmdk
|
// cmdk
|
||||||
import { Command } from "cmdk";
|
import { Command } from "cmdk";
|
||||||
// services
|
// services
|
||||||
import issuesService from "services/issues.service";
|
import { IssueService } from "services/issue";
|
||||||
// types
|
// types
|
||||||
import { ICurrentUserResponse, IIssue, TIssuePriorities } from "types";
|
import { IIssue, IUser, TIssuePriorities } from "types";
|
||||||
// constants
|
// constants
|
||||||
import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
|
import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
|
||||||
import { PRIORITIES } from "constants/project";
|
import { PRIORITIES } from "constants/project";
|
||||||
@ -19,9 +19,12 @@ import { CheckIcon, PriorityIcon } from "components/icons";
|
|||||||
type Props = {
|
type Props = {
|
||||||
setIsPaletteOpen: Dispatch<SetStateAction<boolean>>;
|
setIsPaletteOpen: Dispatch<SetStateAction<boolean>>;
|
||||||
issue: IIssue;
|
issue: IIssue;
|
||||||
user: ICurrentUserResponse;
|
user: IUser;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// services
|
||||||
|
const issueService = new IssueService();
|
||||||
|
|
||||||
export const ChangeIssuePriority: React.FC<Props> = ({ setIsPaletteOpen, issue, user }) => {
|
export const ChangeIssuePriority: React.FC<Props> = ({ setIsPaletteOpen, issue, user }) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, issueId } = router.query;
|
const { workspaceSlug, projectId, issueId } = router.query;
|
||||||
@ -44,7 +47,7 @@ export const ChangeIssuePriority: React.FC<Props> = ({ setIsPaletteOpen, issue,
|
|||||||
);
|
);
|
||||||
|
|
||||||
const payload = { ...formData };
|
const payload = { ...formData };
|
||||||
await issuesService
|
await issueService
|
||||||
.patchIssue(workspaceSlug as string, projectId as string, issueId as string, payload, user)
|
.patchIssue(workspaceSlug as string, projectId as string, issueId as string, payload, user)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
mutate(PROJECT_ISSUES_ACTIVITY(issueId as string));
|
mutate(PROJECT_ISSUES_ACTIVITY(issueId as string));
|
||||||
@ -64,11 +67,7 @@ export const ChangeIssuePriority: React.FC<Props> = ({ setIsPaletteOpen, issue,
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{PRIORITIES.map((priority) => (
|
{PRIORITIES.map((priority) => (
|
||||||
<Command.Item
|
<Command.Item key={priority} onSelect={() => handleIssueState(priority)} className="focus:outline-none">
|
||||||
key={priority}
|
|
||||||
onSelect={() => handleIssueState(priority)}
|
|
||||||
className="focus:outline-none"
|
|
||||||
>
|
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
<PriorityIcon priority={priority} />
|
<PriorityIcon priority={priority} />
|
||||||
<span className="capitalize">{priority ?? "None"}</span>
|
<span className="capitalize">{priority ?? "None"}</span>
|
||||||
|
@ -7,34 +7,36 @@ import useSWR, { mutate } from "swr";
|
|||||||
// cmdk
|
// cmdk
|
||||||
import { Command } from "cmdk";
|
import { Command } from "cmdk";
|
||||||
// services
|
// services
|
||||||
import issuesService from "services/issues.service";
|
import { IssueService } from "services/issue";
|
||||||
import stateService from "services/state.service";
|
import { ProjectStateService } from "services/project";
|
||||||
// ui
|
// ui
|
||||||
import { Spinner } from "components/ui";
|
import { Spinner } from "@plane/ui";
|
||||||
// icons
|
// icons
|
||||||
import { CheckIcon, StateGroupIcon } from "components/icons";
|
import { CheckIcon, StateGroupIcon } from "components/icons";
|
||||||
// helpers
|
// helpers
|
||||||
import { getStatesList } from "helpers/state.helper";
|
import { getStatesList } from "helpers/state.helper";
|
||||||
// types
|
// types
|
||||||
import { ICurrentUserResponse, IIssue } from "types";
|
import { IUser, IIssue } from "types";
|
||||||
// fetch keys
|
// fetch keys
|
||||||
import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY, STATES_LIST } from "constants/fetch-keys";
|
import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY, STATES_LIST } from "constants/fetch-keys";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
setIsPaletteOpen: Dispatch<SetStateAction<boolean>>;
|
setIsPaletteOpen: Dispatch<SetStateAction<boolean>>;
|
||||||
issue: IIssue;
|
issue: IIssue;
|
||||||
user: ICurrentUserResponse | undefined;
|
user: IUser | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// services
|
||||||
|
const issueService = new IssueService();
|
||||||
|
const stateService = new ProjectStateService();
|
||||||
|
|
||||||
export const ChangeIssueState: React.FC<Props> = ({ setIsPaletteOpen, issue, user }) => {
|
export const ChangeIssueState: React.FC<Props> = ({ setIsPaletteOpen, issue, user }) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, issueId } = router.query;
|
const { workspaceSlug, projectId, issueId } = router.query;
|
||||||
|
|
||||||
const { data: stateGroups, mutate: mutateIssueDetails } = useSWR(
|
const { data: stateGroups, mutate: mutateIssueDetails } = useSWR(
|
||||||
workspaceSlug && projectId ? STATES_LIST(projectId as string) : null,
|
workspaceSlug && projectId ? STATES_LIST(projectId as string) : null,
|
||||||
workspaceSlug && projectId
|
workspaceSlug && projectId ? () => stateService.getStates(workspaceSlug as string, projectId as string) : null
|
||||||
? () => stateService.getStates(workspaceSlug as string, projectId as string)
|
|
||||||
: null
|
|
||||||
);
|
);
|
||||||
const states = getStatesList(stateGroups);
|
const states = getStatesList(stateGroups);
|
||||||
|
|
||||||
@ -55,7 +57,7 @@ export const ChangeIssueState: React.FC<Props> = ({ setIsPaletteOpen, issue, use
|
|||||||
);
|
);
|
||||||
|
|
||||||
const payload = { ...formData };
|
const payload = { ...formData };
|
||||||
await issuesService
|
await issueService
|
||||||
.patchIssue(workspaceSlug as string, projectId as string, issueId as string, payload, user)
|
.patchIssue(workspaceSlug as string, projectId as string, issueId as string, payload, user)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
mutateIssueDetails();
|
mutateIssueDetails();
|
||||||
@ -78,18 +80,9 @@ export const ChangeIssueState: React.FC<Props> = ({ setIsPaletteOpen, issue, use
|
|||||||
{states ? (
|
{states ? (
|
||||||
states.length > 0 ? (
|
states.length > 0 ? (
|
||||||
states.map((state) => (
|
states.map((state) => (
|
||||||
<Command.Item
|
<Command.Item key={state.id} onSelect={() => handleIssueState(state.id)} className="focus:outline-none">
|
||||||
key={state.id}
|
|
||||||
onSelect={() => handleIssueState(state.id)}
|
|
||||||
className="focus:outline-none"
|
|
||||||
>
|
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
<StateGroupIcon
|
<StateGroupIcon stateGroup={state.group} color={state.color} height="16px" width="16px" />
|
||||||
stateGroup={state.group}
|
|
||||||
color={state.color}
|
|
||||||
height="16px"
|
|
||||||
width="16px"
|
|
||||||
/>
|
|
||||||
<p>{state.name}</p>
|
<p>{state.name}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>{state.id === issue.state && <CheckIcon className="h-3 w-3" />}</div>
|
<div>{state.id === issue.state && <CheckIcon className="h-3 w-3" />}</div>
|
||||||
|
@ -6,7 +6,7 @@ import { XMarkIcon } from "@heroicons/react/20/solid";
|
|||||||
import { MagnifyingGlassIcon } from "@heroicons/react/24/outline";
|
import { MagnifyingGlassIcon } from "@heroicons/react/24/outline";
|
||||||
import { CommandIcon } from "components/icons";
|
import { CommandIcon } from "components/icons";
|
||||||
// ui
|
// ui
|
||||||
import { Input } from "components/ui";
|
import { Input } from "@plane/ui";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@ -48,9 +48,7 @@ const allShortcuts = shortcuts.map((i) => i.shortcuts).flat(1);
|
|||||||
export const ShortcutsModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
|
export const ShortcutsModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
const filteredShortcuts = allShortcuts.filter((shortcut) =>
|
const filteredShortcuts = allShortcuts.filter((shortcut) =>
|
||||||
shortcut.description.toLowerCase().includes(query.trim().toLowerCase()) || query === ""
|
shortcut.description.toLowerCase().includes(query.trim().toLowerCase()) || query === "" ? true : false
|
||||||
? true
|
|
||||||
: false
|
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -105,12 +103,13 @@ export const ShortcutsModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
|
|||||||
<div className="flex w-full items-center justify-start gap-1 rounded border-[0.6px] border-custom-border-200 bg-custom-background-90 px-3 py-2">
|
<div className="flex w-full items-center justify-start gap-1 rounded border-[0.6px] border-custom-border-200 bg-custom-background-90 px-3 py-2">
|
||||||
<MagnifyingGlassIcon className="h-3.5 w-3.5 text-custom-text-200" />
|
<MagnifyingGlassIcon className="h-3.5 w-3.5 text-custom-text-200" />
|
||||||
<Input
|
<Input
|
||||||
className="w-full border-none bg-transparent py-1 px-2 text-xs text-custom-text-200 focus:outline-none"
|
|
||||||
id="search"
|
id="search"
|
||||||
name="search"
|
name="search"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search for shortcuts"
|
value={query}
|
||||||
onChange={(e) => setQuery(e.target.value)}
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
|
placeholder="Search for shortcuts"
|
||||||
|
className="w-full border-none bg-transparent py-1 px-2 text-xs text-custom-text-200 focus:outline-none"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -121,9 +120,7 @@ export const ShortcutsModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
|
|||||||
<div key={shortcut.keys} className="flex w-full flex-col">
|
<div key={shortcut.keys} className="flex w-full flex-col">
|
||||||
<div className="flex flex-col gap-y-3">
|
<div className="flex flex-col gap-y-3">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<p className="text-sm text-custom-text-200">
|
<p className="text-sm text-custom-text-200">{shortcut.description}</p>
|
||||||
{shortcut.description}
|
|
||||||
</p>
|
|
||||||
<div className="flex items-center gap-x-2.5">
|
<div className="flex items-center gap-x-2.5">
|
||||||
{shortcut.keys.split(",").map((key, index) => (
|
{shortcut.keys.split(",").map((key, index) => (
|
||||||
<span key={index} className="flex items-center gap-1">
|
<span key={index} className="flex items-center gap-1">
|
||||||
|
2
web/components/common/index.ts
Normal file
2
web/components/common/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./product-updates-modal";
|
||||||
|
export * from "./empty-state";
|
115
web/components/common/product-updates-modal.tsx
Normal file
115
web/components/common/product-updates-modal.tsx
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import useSWR from "swr";
|
||||||
|
|
||||||
|
// headless ui
|
||||||
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
|
// services
|
||||||
|
import { WorkspaceService } from "services/workspace.service";
|
||||||
|
// components
|
||||||
|
import { MarkdownRenderer } from "components/ui";
|
||||||
|
import { Loader } from "@plane/ui";
|
||||||
|
// icons
|
||||||
|
import { XMarkIcon } from "@heroicons/react/20/solid";
|
||||||
|
// helpers
|
||||||
|
import { renderLongDateFormat } from "helpers/date-time.helper";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isOpen: boolean;
|
||||||
|
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// services
|
||||||
|
const workspaceService = new WorkspaceService();
|
||||||
|
|
||||||
|
export const ProductUpdatesModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
|
||||||
|
const { data: updates } = useSWR("PRODUCT_UPDATES", () => workspaceService.getProductUpdates());
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Transition.Root show={isOpen} as={React.Fragment}>
|
||||||
|
<Dialog as="div" className="relative z-20" onClose={setIsOpen}>
|
||||||
|
<Transition.Child
|
||||||
|
as={React.Fragment}
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enterFrom="opacity-0"
|
||||||
|
enterTo="opacity-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
>
|
||||||
|
<div className="fixed inset-0 bg-custom-backdrop bg-opacity-50 transition-opacity" />
|
||||||
|
</Transition.Child>
|
||||||
|
|
||||||
|
<div className="fixed inset-0 z-20 h-full w-full">
|
||||||
|
<div className="grid place-items-center h-full w-full p-4">
|
||||||
|
<Transition.Child
|
||||||
|
as={React.Fragment}
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
>
|
||||||
|
<Dialog.Panel className="relative overflow-hidden rounded-lg bg-custom-background-100 border border-custom-border-100 shadow-custom-shadow-rg] min-w-[100%] sm:min-w-[50%] sm:max-w-[50%]">
|
||||||
|
<div className="flex flex-col p-4 max-h-[90vh] w-full">
|
||||||
|
<Dialog.Title as="h3" className="flex items-center justify-between text-lg font-semibold">
|
||||||
|
<span>Product Updates</span>
|
||||||
|
<span>
|
||||||
|
<button type="button" onClick={() => setIsOpen(false)}>
|
||||||
|
<XMarkIcon
|
||||||
|
className="h-6 w-6 text-custom-text-200 hover:text-custom-text-100"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</Dialog.Title>
|
||||||
|
{updates && updates.length > 0 ? (
|
||||||
|
<div className="h-full overflow-y-auto mt-4 space-y-4">
|
||||||
|
{updates.map((item, index) => (
|
||||||
|
<React.Fragment key={item.id}>
|
||||||
|
<div className="flex items-center gap-3 text-xs text-custom-text-200">
|
||||||
|
<span className="flex items-center rounded-full border border-custom-border-200 bg-custom-background-90 px-3 py-1.5 text-xs">
|
||||||
|
{item.tag_name}
|
||||||
|
</span>
|
||||||
|
<span>{renderLongDateFormat(item.published_at)}</span>
|
||||||
|
{index === 0 && (
|
||||||
|
<span className="flex items-center rounded-full border border-custom-border-200 bg-custom-primary px-3 py-1.5 text-xs text-white">
|
||||||
|
New
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<MarkdownRenderer markdown={item.body} />
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid place-items-center w-full mt-4">
|
||||||
|
<Loader className="space-y-6 w-full">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Loader.Item height="30px" />
|
||||||
|
<Loader.Item height="20px" width="80%" />
|
||||||
|
<Loader.Item height="20px" width="80%" />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Loader.Item height="30px" />
|
||||||
|
<Loader.Item height="20px" width="80%" />
|
||||||
|
<Loader.Item height="20px" width="80%" />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Loader.Item height="30px" />
|
||||||
|
<Loader.Item height="20px" width="80%" />
|
||||||
|
<Loader.Item height="20px" width="80%" />
|
||||||
|
</div>
|
||||||
|
</Loader>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Dialog.Panel>
|
||||||
|
</Transition.Child>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</Transition.Root>
|
||||||
|
);
|
||||||
|
};
|
@ -5,9 +5,10 @@ import useSWR from "swr";
|
|||||||
// hook
|
// hook
|
||||||
import useEstimateOption from "hooks/use-estimate-option";
|
import useEstimateOption from "hooks/use-estimate-option";
|
||||||
// services
|
// services
|
||||||
import issuesService from "services/issues.service";
|
import { IssueLabelService } from "services/issue";
|
||||||
// icons
|
// icons
|
||||||
import { Icon, Tooltip } from "components/ui";
|
import { Tooltip } from "@plane/ui";
|
||||||
|
import { Icon } from "components/ui";
|
||||||
import {
|
import {
|
||||||
TagIcon,
|
TagIcon,
|
||||||
CopyPlus,
|
CopyPlus,
|
||||||
@ -23,12 +24,7 @@ import {
|
|||||||
SignalMediumIcon,
|
SignalMediumIcon,
|
||||||
MessageSquareIcon,
|
MessageSquareIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import {
|
import { BlockedIcon, BlockerIcon, RelatedIcon, StackedLayersHorizontalIcon } from "components/icons";
|
||||||
BlockedIcon,
|
|
||||||
BlockerIcon,
|
|
||||||
RelatedIcon,
|
|
||||||
StackedLayersHorizontalIcon,
|
|
||||||
} from "components/icons";
|
|
||||||
// helpers
|
// helpers
|
||||||
import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
|
import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
|
||||||
import { capitalizeFirstLetter } from "helpers/string.helper";
|
import { capitalizeFirstLetter } from "helpers/string.helper";
|
||||||
@ -37,25 +33,22 @@ import { IIssueActivity } from "types";
|
|||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { WORKSPACE_LABELS } from "constants/fetch-keys";
|
import { WORKSPACE_LABELS } from "constants/fetch-keys";
|
||||||
|
|
||||||
|
// services
|
||||||
|
const issueLabelService = new IssueLabelService();
|
||||||
|
|
||||||
const IssueLink = ({ activity }: { activity: IIssueActivity }) => {
|
const IssueLink = ({ activity }: { activity: IIssueActivity }) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug } = router.query;
|
const { workspaceSlug } = router.query;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<Tooltip tooltipContent={activity.issue_detail ? activity.issue_detail.name : "This issue has been deleted"}>
|
||||||
tooltipContent={
|
|
||||||
activity.issue_detail ? activity.issue_detail.name : "This issue has been deleted"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<a
|
<a
|
||||||
href={`/${workspaceSlug}/projects/${activity.project}/issues/${activity.issue}`}
|
href={`/${workspaceSlug}/projects/${activity.project}/issues/${activity.issue}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
||||||
>
|
>
|
||||||
{activity.issue_detail
|
{activity.issue_detail ? `${activity.project_detail.identifier}-${activity.issue_detail.sequence_id}` : "Issue"}
|
||||||
? `${activity.project_detail.identifier}-${activity.issue_detail.sequence_id}`
|
|
||||||
: "Issue"}
|
|
||||||
<RocketIcon size={12} color="#6b7280" />
|
<RocketIcon size={12} color="#6b7280" />
|
||||||
</a>
|
</a>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@ -84,7 +77,7 @@ const LabelPill = ({ labelId }: { labelId: string }) => {
|
|||||||
|
|
||||||
const { data: labels } = useSWR(
|
const { data: labels } = useSWR(
|
||||||
workspaceSlug ? WORKSPACE_LABELS(workspaceSlug.toString()) : null,
|
workspaceSlug ? WORKSPACE_LABELS(workspaceSlug.toString()) : null,
|
||||||
workspaceSlug ? () => issuesService.getWorkspaceLabels(workspaceSlug.toString()) : null
|
workspaceSlug ? () => issueLabelService.getWorkspaceIssueLabels(workspaceSlug.toString()) : null
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -103,20 +96,14 @@ const EstimatePoint = ({ point }: { point: string }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<span className="font-medium text-custom-text-100">
|
<span className="font-medium text-custom-text-100">
|
||||||
{isEstimateActive
|
{isEstimateActive ? estimateValue : `${currentPoint} ${currentPoint > 1 ? "points" : "point"}`}
|
||||||
? estimateValue
|
|
||||||
: `${currentPoint} ${currentPoint > 1 ? "points" : "point"}`}
|
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const activityDetails: {
|
const activityDetails: {
|
||||||
[key: string]: {
|
[key: string]: {
|
||||||
message: (
|
message: (activity: IIssueActivity, showIssue: boolean, workspaceSlug: string) => React.ReactNode;
|
||||||
activity: IIssueActivity,
|
|
||||||
showIssue: boolean,
|
|
||||||
workspaceSlug: string
|
|
||||||
) => React.ReactNode;
|
|
||||||
icon: React.ReactNode;
|
icon: React.ReactNode;
|
||||||
};
|
};
|
||||||
} = {
|
} = {
|
||||||
@ -209,8 +196,7 @@ const activityDetails: {
|
|||||||
else
|
else
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
removed the blocking issue{" "}
|
removed the blocking issue <span className="font-medium text-custom-text-100">{activity.old_value}</span>.
|
||||||
<span className="font-medium text-custom-text-100">{activity.old_value}</span>.
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -266,8 +252,7 @@ const activityDetails: {
|
|||||||
else
|
else
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
removed the relation from{" "}
|
removed the relation from <span className="font-medium text-custom-text-100">{activity.old_value}</span>.
|
||||||
<span className="font-medium text-custom-text-100">{activity.old_value}</span>.
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -554,8 +539,7 @@ const activityDetails: {
|
|||||||
if (!activity.new_value)
|
if (!activity.new_value)
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
removed the parent{" "}
|
removed the parent <span className="font-medium text-custom-text-100">{activity.old_value}</span>
|
||||||
<span className="font-medium text-custom-text-100">{activity.old_value}</span>
|
|
||||||
{showIssue && (
|
{showIssue && (
|
||||||
<>
|
<>
|
||||||
{" "}
|
{" "}
|
||||||
@ -568,8 +552,7 @@ const activityDetails: {
|
|||||||
else
|
else
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
set the parent to{" "}
|
set the parent to <span className="font-medium text-custom-text-100">{activity.new_value}</span>
|
||||||
<span className="font-medium text-custom-text-100">{activity.new_value}</span>
|
|
||||||
{showIssue && (
|
{showIssue && (
|
||||||
<>
|
<>
|
||||||
{" "}
|
{" "}
|
||||||
@ -580,13 +563,7 @@ const activityDetails: {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: (
|
icon: <Icon iconName="supervised_user_circle" className="!text-xs !text-[#6b7280]" aria-hidden="true" />,
|
||||||
<Icon
|
|
||||||
iconName="supervised_user_circle"
|
|
||||||
className="!text-xs !text-[#6b7280]"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
priority: {
|
priority: {
|
||||||
message: (activity, showIssue) => (
|
message: (activity, showIssue) => (
|
||||||
@ -643,8 +620,7 @@ const activityDetails: {
|
|||||||
state: {
|
state: {
|
||||||
message: (activity, showIssue) => (
|
message: (activity, showIssue) => (
|
||||||
<>
|
<>
|
||||||
set the state to{" "}
|
set the state to <span className="font-medium text-custom-text-100">{activity.new_value}</span>
|
||||||
<span className="font-medium text-custom-text-100">{activity.new_value}</span>
|
|
||||||
{showIssue && (
|
{showIssue && (
|
||||||
<>
|
<>
|
||||||
{" "}
|
{" "}
|
||||||
@ -696,13 +672,7 @@ export const ActivityIcon = ({ activity }: { activity: IIssueActivity }) => (
|
|||||||
<>{activityDetails[activity.field as keyof typeof activityDetails]?.icon}</>
|
<>{activityDetails[activity.field as keyof typeof activityDetails]?.icon}</>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const ActivityMessage = ({
|
export const ActivityMessage = ({ activity, showIssue = false }: { activity: IIssueActivity; showIssue?: boolean }) => {
|
||||||
activity,
|
|
||||||
showIssue = false,
|
|
||||||
}: {
|
|
||||||
activity: IIssueActivity;
|
|
||||||
showIssue?: boolean;
|
|
||||||
}) => {
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug } = router.query;
|
const { workspaceSlug } = router.query;
|
||||||
|
|
||||||
|
@ -1,28 +1,22 @@
|
|||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
|
|
||||||
// react-hook-form
|
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
// react-datepicker
|
|
||||||
import DatePicker from "react-datepicker";
|
import DatePicker from "react-datepicker";
|
||||||
// headless ui
|
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
|
|
||||||
// components
|
// components
|
||||||
import { DateFilterSelect } from "./date-filter-select";
|
import { DateFilterSelect } from "./date-filter-select";
|
||||||
// ui
|
// ui
|
||||||
import { PrimaryButton, SecondaryButton } from "components/ui";
|
import { Button } from "@plane/ui";
|
||||||
// icons
|
// icons
|
||||||
import { XMarkIcon } from "@heroicons/react/20/solid";
|
import { XMarkIcon } from "@heroicons/react/20/solid";
|
||||||
// helpers
|
// helpers
|
||||||
import { renderDateFormat, renderShortDateWithYearFormat } from "helpers/date-time.helper";
|
import { renderDateFormat, renderShortDateWithYearFormat } from "helpers/date-time.helper";
|
||||||
import { IIssueFilterOptions } from "types";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
title: string;
|
title: string;
|
||||||
field: keyof IIssueFilterOptions;
|
|
||||||
filters: IIssueFilterOptions;
|
|
||||||
handleClose: () => void;
|
handleClose: () => void;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onSelect: (option: any) => void;
|
onSelect: (val: string[]) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type TFormValues = {
|
type TFormValues = {
|
||||||
@ -37,14 +31,7 @@ const defaultValues: TFormValues = {
|
|||||||
date2: new Date(new Date().getFullYear(), new Date().getMonth() + 1, new Date().getDate()),
|
date2: new Date(new Date().getFullYear(), new Date().getMonth() + 1, new Date().getDate()),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DateFilterModal: React.FC<Props> = ({
|
export const DateFilterModal: React.FC<Props> = ({ title, handleClose, isOpen, onSelect }) => {
|
||||||
title,
|
|
||||||
field,
|
|
||||||
filters,
|
|
||||||
handleClose,
|
|
||||||
isOpen,
|
|
||||||
onSelect,
|
|
||||||
}) => {
|
|
||||||
const { handleSubmit, watch, control } = useForm<TFormValues>({
|
const { handleSubmit, watch, control } = useForm<TFormValues>({
|
||||||
defaultValues,
|
defaultValues,
|
||||||
});
|
});
|
||||||
@ -52,32 +39,13 @@ export const DateFilterModal: React.FC<Props> = ({
|
|||||||
const handleFormSubmit = (formData: TFormValues) => {
|
const handleFormSubmit = (formData: TFormValues) => {
|
||||||
const { filterType, date1, date2 } = formData;
|
const { filterType, date1, date2 } = formData;
|
||||||
|
|
||||||
if (filterType === "range") {
|
if (filterType === "range") onSelect([`${renderDateFormat(date1)};after`, `${renderDateFormat(date2)};before`]);
|
||||||
onSelect({
|
else onSelect([`${renderDateFormat(date1)};${filterType}`]);
|
||||||
key: field,
|
|
||||||
value: [`${renderDateFormat(date1)};after`, `${renderDateFormat(date2)};before`],
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const filteredArray = (filters?.[field] as string[])?.filter((item) => {
|
|
||||||
if (item?.includes(filterType)) return false;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
const filterOne = filteredArray && filteredArray?.length > 0 ? filteredArray[0] : null;
|
|
||||||
if (filterOne)
|
|
||||||
onSelect({ key: field, value: [filterOne, `${renderDateFormat(date1)};${filterType}`] });
|
|
||||||
else
|
|
||||||
onSelect({
|
|
||||||
key: field,
|
|
||||||
value: [`${renderDateFormat(date1)};${filterType}`],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
handleClose();
|
handleClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
const isInvalid =
|
const isInvalid = watch("filterType") === "range" ? new Date(watch("date1")) > new Date(watch("date2")) : false;
|
||||||
watch("filterType") === "range" ? new Date(watch("date1")) > new Date(watch("date2")) : false;
|
|
||||||
|
|
||||||
const nextDay = new Date(watch("date1"));
|
const nextDay = new Date(watch("date1"));
|
||||||
nextDay.setDate(nextDay.getDate() + 1);
|
nextDay.setDate(nextDay.getDate() + 1);
|
||||||
@ -117,10 +85,7 @@ export const DateFilterModal: React.FC<Props> = ({
|
|||||||
<DateFilterSelect title={title} value={value} onChange={onChange} />
|
<DateFilterSelect title={title} value={value} onChange={onChange} />
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<XMarkIcon
|
<XMarkIcon className="border-base h-4 w-4 cursor-pointer" onClick={handleClose} />
|
||||||
className="border-base h-4 w-4 cursor-pointer"
|
|
||||||
onClick={handleClose}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full justify-between gap-4">
|
<div className="flex w-full justify-between gap-4">
|
||||||
<Controller
|
<Controller
|
||||||
@ -162,16 +127,12 @@ export const DateFilterModal: React.FC<Props> = ({
|
|||||||
</h6>
|
</h6>
|
||||||
)}
|
)}
|
||||||
<div className="flex justify-end gap-4">
|
<div className="flex justify-end gap-4">
|
||||||
<SecondaryButton className="flex items-center gap-2" onClick={handleClose}>
|
<Button variant="neutral-primary" onClick={handleClose}>
|
||||||
Cancel
|
Cancel
|
||||||
</SecondaryButton>
|
</Button>
|
||||||
<PrimaryButton
|
<Button variant="primary" type="submit" disabled={isInvalid}>
|
||||||
type="submit"
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
disabled={isInvalid}
|
|
||||||
>
|
|
||||||
Apply
|
Apply
|
||||||
</PrimaryButton>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Dialog.Panel>
|
</Dialog.Panel>
|
||||||
|
@ -11,7 +11,8 @@ import useEstimateOption from "hooks/use-estimate-option";
|
|||||||
// components
|
// components
|
||||||
import { SelectFilters } from "components/views";
|
import { SelectFilters } from "components/views";
|
||||||
// ui
|
// ui
|
||||||
import { CustomMenu, ToggleSwitch, Tooltip } from "components/ui";
|
import { CustomMenu } from "components/ui";
|
||||||
|
import { ToggleSwitch, Tooltip } from "@plane/ui";
|
||||||
// icons
|
// icons
|
||||||
import { ChevronDownIcon } from "@heroicons/react/24/outline";
|
import { ChevronDownIcon } from "@heroicons/react/24/outline";
|
||||||
import {
|
import {
|
||||||
@ -25,11 +26,11 @@ import {
|
|||||||
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
||||||
import { checkIfArraysHaveSameElements } from "helpers/array.helper";
|
import { checkIfArraysHaveSameElements } from "helpers/array.helper";
|
||||||
// types
|
// types
|
||||||
import { Properties, TIssueViewOptions } from "types";
|
import { Properties, TIssueLayouts } from "types";
|
||||||
// constants
|
// constants
|
||||||
import { GROUP_BY_OPTIONS, ORDER_BY_OPTIONS, FILTER_ISSUE_OPTIONS } from "constants/issue";
|
import { ISSUE_GROUP_BY_OPTIONS, ISSUE_ORDER_BY_OPTIONS, ISSUE_FILTER_OPTIONS } from "constants/issue";
|
||||||
|
|
||||||
const issueViewOptions: { type: TIssueViewOptions; Icon: any }[] = [
|
const issueViewOptions: { type: TIssueLayouts; Icon: any }[] = [
|
||||||
{
|
{
|
||||||
type: "list",
|
type: "list",
|
||||||
Icon: FormatListBulletedOutlined,
|
Icon: FormatListBulletedOutlined,
|
||||||
@ -52,7 +53,7 @@ const issueViewOptions: { type: TIssueViewOptions; Icon: any }[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const issueViewForDraftIssues: { type: TIssueViewOptions; Icon: any }[] = [
|
const issueViewForDraftIssues: { type: TIssueLayouts; Icon: any }[] = [
|
||||||
{
|
{
|
||||||
type: "list",
|
type: "list",
|
||||||
Icon: FormatListBulletedOutlined,
|
Icon: FormatListBulletedOutlined,
|
||||||
@ -69,19 +70,10 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
const isArchivedIssues = router.pathname.includes("archived-issues");
|
const isArchivedIssues = router.pathname.includes("archived-issues");
|
||||||
const isDraftIssues = router.pathname?.split("/")?.[4] === "draft-issues";
|
const isDraftIssues = router.pathname?.split("/")?.[4] === "draft-issues";
|
||||||
|
|
||||||
const {
|
const { displayFilters, setDisplayFilters, filters, setFilters, resetFilterToDefault, setNewFilterDefaultView } =
|
||||||
displayFilters,
|
useIssuesView();
|
||||||
setDisplayFilters,
|
|
||||||
filters,
|
|
||||||
setFilters,
|
|
||||||
resetFilterToDefault,
|
|
||||||
setNewFilterDefaultView,
|
|
||||||
} = useIssuesView();
|
|
||||||
|
|
||||||
const [properties, setProperties] = useIssuesProperties(
|
const [properties, setProperties] = useIssuesProperties(workspaceSlug as string, projectId as string);
|
||||||
workspaceSlug as string,
|
|
||||||
projectId as string
|
|
||||||
);
|
|
||||||
|
|
||||||
const { isEstimateActive } = useEstimateOption();
|
const { isEstimateActive } = useEstimateOption();
|
||||||
|
|
||||||
@ -92,11 +84,7 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
{issueViewOptions.map((option) => (
|
{issueViewOptions.map((option) => (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
key={option.type}
|
key={option.type}
|
||||||
tooltipContent={
|
tooltipContent={<span className="capitalize">{replaceUnderscoreIfSnakeCase(option.type)} Layout</span>}
|
||||||
<span className="capitalize">
|
|
||||||
{replaceUnderscoreIfSnakeCase(option.type)} Layout
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
position="bottom"
|
position="bottom"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
@ -124,9 +112,7 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
{issueViewForDraftIssues.map((option) => (
|
{issueViewForDraftIssues.map((option) => (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
key={option.type}
|
key={option.type}
|
||||||
tooltipContent={
|
tooltipContent={<span className="capitalize">{replaceUnderscoreIfSnakeCase(option.type)} View</span>}
|
||||||
<span className="capitalize">{replaceUnderscoreIfSnakeCase(option.type)} View</span>
|
|
||||||
}
|
|
||||||
position="bottom"
|
position="bottom"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
@ -166,9 +152,7 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
if (valueExists)
|
if (valueExists)
|
||||||
setFilters(
|
setFilters(
|
||||||
{
|
{
|
||||||
[option.key]: ((filters[key] ?? []) as any[])?.filter(
|
[option.key]: ((filters[key] ?? []) as any[])?.filter((val) => val !== option.value),
|
||||||
(val) => val !== option.value
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
!Boolean(viewId)
|
!Boolean(viewId)
|
||||||
);
|
);
|
||||||
@ -189,9 +173,7 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
<>
|
<>
|
||||||
<Popover.Button
|
<Popover.Button
|
||||||
className={`group flex items-center gap-2 rounded-md border border-custom-border-200 px-3 py-1.5 text-xs hover:bg-custom-sidebar-background-90 hover:text-custom-sidebar-text-100 focus:outline-none duration-300 ${
|
className={`group flex items-center gap-2 rounded-md border border-custom-border-200 px-3 py-1.5 text-xs hover:bg-custom-sidebar-background-90 hover:text-custom-sidebar-text-100 focus:outline-none duration-300 ${
|
||||||
open
|
open ? "bg-custom-sidebar-background-90 text-custom-sidebar-text-100" : "text-custom-sidebar-text-200"
|
||||||
? "bg-custom-sidebar-background-90 text-custom-sidebar-text-100"
|
|
||||||
: "text-custom-sidebar-text-200"
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Display
|
Display
|
||||||
@ -218,27 +200,24 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
<div className="w-28">
|
<div className="w-28">
|
||||||
<CustomMenu
|
<CustomMenu
|
||||||
label={
|
label={
|
||||||
GROUP_BY_OPTIONS.find(
|
ISSUE_GROUP_BY_OPTIONS.find((option) => option.key === displayFilters.group_by)
|
||||||
(option) => option.key === displayFilters.group_by
|
?.title ?? "Select"
|
||||||
)?.name ?? "Select"
|
|
||||||
}
|
}
|
||||||
className="!w-full"
|
className="!w-full"
|
||||||
buttonClassName="w-full"
|
buttonClassName="w-full"
|
||||||
>
|
>
|
||||||
{GROUP_BY_OPTIONS.map((option) => {
|
{ISSUE_GROUP_BY_OPTIONS.map((option) => {
|
||||||
if (displayFilters.layout === "kanban" && option.key === null)
|
if (displayFilters.layout === "kanban" && option.key === null) return null;
|
||||||
return null;
|
|
||||||
if (option.key === "project") return null;
|
if (option.key === "project") return null;
|
||||||
|
|
||||||
if (isDraftIssues && option.key === "state_detail.group")
|
if (isDraftIssues && option.key === "state_detail.group") return null;
|
||||||
return null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CustomMenu.MenuItem
|
<CustomMenu.MenuItem
|
||||||
key={option.key}
|
key={option.key}
|
||||||
onClick={() => setDisplayFilters({ group_by: option.key })}
|
onClick={() => setDisplayFilters({ group_by: option.key })}
|
||||||
>
|
>
|
||||||
{option.name}
|
{option.title}
|
||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@ -246,51 +225,47 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{displayFilters.layout !== "calendar" &&
|
{displayFilters.layout !== "calendar" && displayFilters.layout !== "spreadsheet" && (
|
||||||
displayFilters.layout !== "spreadsheet" && (
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center justify-between">
|
<h4 className="text-custom-text-200">Order by</h4>
|
||||||
<h4 className="text-custom-text-200">Order by</h4>
|
<div className="w-28">
|
||||||
<div className="w-28">
|
<CustomMenu
|
||||||
<CustomMenu
|
label={
|
||||||
label={
|
ISSUE_ORDER_BY_OPTIONS.find((option) => option.key === displayFilters.order_by)?.title ??
|
||||||
ORDER_BY_OPTIONS.find(
|
"Select"
|
||||||
(option) => option.key === displayFilters.order_by
|
}
|
||||||
)?.name ?? "Select"
|
className="!w-full"
|
||||||
}
|
buttonClassName="w-full"
|
||||||
className="!w-full"
|
>
|
||||||
buttonClassName="w-full"
|
{ISSUE_ORDER_BY_OPTIONS.map((option) =>
|
||||||
>
|
displayFilters.group_by === "priority" && option.key === "priority" ? null : (
|
||||||
{ORDER_BY_OPTIONS.map((option) =>
|
<CustomMenu.MenuItem
|
||||||
displayFilters.group_by === "priority" &&
|
key={option.key}
|
||||||
option.key === "priority" ? null : (
|
onClick={() => {
|
||||||
<CustomMenu.MenuItem
|
setDisplayFilters({ order_by: option.key });
|
||||||
key={option.key}
|
}}
|
||||||
onClick={() => {
|
>
|
||||||
setDisplayFilters({ order_by: option.key });
|
{option.title}
|
||||||
}}
|
</CustomMenu.MenuItem>
|
||||||
>
|
)
|
||||||
{option.name}
|
)}
|
||||||
</CustomMenu.MenuItem>
|
</CustomMenu>
|
||||||
)
|
|
||||||
)}
|
|
||||||
</CustomMenu>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
|
)}
|
||||||
{!isArchivedIssues && (
|
{!isArchivedIssues && (
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h4 className="text-custom-text-200">Issue type</h4>
|
<h4 className="text-custom-text-200">Issue type</h4>
|
||||||
<div className="w-28">
|
<div className="w-28">
|
||||||
<CustomMenu
|
<CustomMenu
|
||||||
label={
|
label={
|
||||||
FILTER_ISSUE_OPTIONS.find(
|
ISSUE_FILTER_OPTIONS.find((option) => option.key === displayFilters.type)?.title ??
|
||||||
(option) => option.key === displayFilters.type
|
"Select"
|
||||||
)?.name ?? "Select"
|
|
||||||
}
|
}
|
||||||
className="!w-full"
|
className="!w-full"
|
||||||
buttonClassName="w-full"
|
buttonClassName="w-full"
|
||||||
>
|
>
|
||||||
{FILTER_ISSUE_OPTIONS.map((option) => (
|
{ISSUE_FILTER_OPTIONS.map((option) => (
|
||||||
<CustomMenu.MenuItem
|
<CustomMenu.MenuItem
|
||||||
key={option.key}
|
key={option.key}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
@ -299,7 +274,7 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{option.name}
|
{option.title}
|
||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
))}
|
))}
|
||||||
</CustomMenu>
|
</CustomMenu>
|
||||||
@ -307,20 +282,17 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{displayFilters.layout !== "calendar" &&
|
{displayFilters.layout !== "calendar" && displayFilters.layout !== "spreadsheet" && (
|
||||||
displayFilters.layout !== "spreadsheet" && (
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center justify-between">
|
<h4 className="text-custom-text-200">Show sub-issues</h4>
|
||||||
<h4 className="text-custom-text-200">Show sub-issues</h4>
|
<div className="w-28">
|
||||||
<div className="w-28">
|
<ToggleSwitch
|
||||||
<ToggleSwitch
|
value={displayFilters.sub_issue ?? true}
|
||||||
value={displayFilters.sub_issue ?? true}
|
onChange={() => setDisplayFilters({ sub_issue: !displayFilters.sub_issue })}
|
||||||
onChange={() =>
|
/>
|
||||||
setDisplayFilters({ sub_issue: !displayFilters.sub_issue })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
|
)}
|
||||||
{displayFilters.layout !== "calendar" &&
|
{displayFilters.layout !== "calendar" &&
|
||||||
displayFilters.layout !== "spreadsheet" &&
|
displayFilters.layout !== "spreadsheet" &&
|
||||||
displayFilters.layout !== "gantt_chart" && (
|
displayFilters.layout !== "gantt_chart" && (
|
||||||
@ -365,16 +337,11 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
displayFilters.layout === "spreadsheet" &&
|
displayFilters.layout === "spreadsheet" &&
|
||||||
(key === "attachment_count" ||
|
(key === "attachment_count" || key === "link" || key === "sub_issue_count")
|
||||||
key === "link" ||
|
|
||||||
key === "sub_issue_count")
|
|
||||||
)
|
)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
if (
|
if (displayFilters.layout !== "spreadsheet" && (key === "created_on" || key === "updated_on"))
|
||||||
displayFilters.layout !== "spreadsheet" &&
|
|
||||||
(key === "created_on" || key === "updated_on")
|
|
||||||
)
|
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import React from "react";
|
import { FC } from "react";
|
||||||
|
|
||||||
// icons
|
// icons
|
||||||
import { XMarkIcon } from "@heroicons/react/24/outline";
|
import { XMarkIcon } from "@heroicons/react/24/outline";
|
||||||
import { PriorityIcon, StateGroupIcon } from "components/icons";
|
import { PriorityIcon, StateGroupIcon } from "components/icons";
|
||||||
@ -10,13 +9,7 @@ import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
|||||||
// helpers
|
// helpers
|
||||||
import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
|
import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
|
||||||
// types
|
// types
|
||||||
import {
|
import { IIssueLabels, IProject, IUserLite, IWorkspaceIssueFilterOptions, TStateGroups } from "types";
|
||||||
IIssueLabels,
|
|
||||||
IProject,
|
|
||||||
IUserLite,
|
|
||||||
IWorkspaceIssueFilterOptions,
|
|
||||||
TStateGroups,
|
|
||||||
} from "types";
|
|
||||||
// constants
|
// constants
|
||||||
import { STATE_GROUP_COLORS } from "constants/state";
|
import { STATE_GROUP_COLORS } from "constants/state";
|
||||||
|
|
||||||
@ -30,20 +23,12 @@ type Props = {
|
|||||||
project?: IProject[] | undefined;
|
project?: IProject[] | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WorkspaceFiltersList: React.FC<Props> = ({
|
export const WorkspaceFiltersList: FC<Props> = (props) => {
|
||||||
filters,
|
const { filters, setFilters, clearAllFilters, labels, members, project } = props;
|
||||||
setFilters,
|
|
||||||
clearAllFilters,
|
|
||||||
labels,
|
|
||||||
members,
|
|
||||||
stateGroup,
|
|
||||||
project,
|
|
||||||
}) => {
|
|
||||||
if (!filters) return <></>;
|
if (!filters) return <></>;
|
||||||
|
|
||||||
const nullFilters = Object.keys(filters).filter(
|
const nullFilters = Object.keys(filters).filter((key) => filters[key as keyof IWorkspaceIssueFilterOptions] === null);
|
||||||
(key) => filters[key as keyof IWorkspaceIssueFilterOptions] === null
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-1 flex-wrap items-center gap-2 text-xs">
|
<div className="flex flex-1 flex-wrap items-center gap-2 text-xs">
|
||||||
@ -189,9 +174,7 @@ export const WorkspaceFiltersList: React.FC<Props> = ({
|
|||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setFilters({
|
setFilters({
|
||||||
created_by: filters.created_by?.filter(
|
created_by: filters.created_by?.filter((p: any) => p !== memberId),
|
||||||
(p: any) => p !== memberId
|
|
||||||
),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
@ -4,14 +4,14 @@ import { useRouter } from "next/router";
|
|||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { useDropzone } from "react-dropzone";
|
import { useDropzone } from "react-dropzone";
|
||||||
import { Tab, Transition, Popover } from "@headlessui/react";
|
import { Tab, Transition, Popover } from "@headlessui/react";
|
||||||
|
import { Control, Controller } from "react-hook-form";
|
||||||
// services
|
// services
|
||||||
import fileService from "services/file.service";
|
import { FileService } from "services/file.service";
|
||||||
// hooks
|
// hooks
|
||||||
import useWorkspaceDetails from "hooks/use-workspace-details";
|
import useWorkspaceDetails from "hooks/use-workspace-details";
|
||||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||||
// components
|
// components
|
||||||
import { Input, PrimaryButton, SecondaryButton, Loader } from "components/ui";
|
import { Button, Input, Loader } from "@plane/ui";
|
||||||
|
|
||||||
const tabOptions = [
|
const tabOptions = [
|
||||||
{
|
{
|
||||||
@ -31,16 +31,15 @@ const tabOptions = [
|
|||||||
type Props = {
|
type Props = {
|
||||||
label: string | React.ReactNode;
|
label: string | React.ReactNode;
|
||||||
value: string | null;
|
value: string | null;
|
||||||
|
control: Control<any>;
|
||||||
onChange: (data: string) => void;
|
onChange: (data: string) => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ImagePickerPopover: React.FC<Props> = ({
|
// services
|
||||||
label,
|
const fileService = new FileService();
|
||||||
value,
|
|
||||||
onChange,
|
export const ImagePickerPopover: React.FC<Props> = ({ label, value, control, onChange, disabled = false }) => {
|
||||||
disabled = false,
|
|
||||||
}) => {
|
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -64,14 +63,10 @@ export const ImagePickerPopover: React.FC<Props> = ({
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data: projectCoverImages } = useSWR(
|
const { data: projectCoverImages } = useSWR(`PROJECT_COVER_IMAGES`, () => fileService.getProjectCoverImages(), {
|
||||||
`PROJECT_COVER_IMAGES`,
|
revalidateOnFocus: false,
|
||||||
() => fileService.getProjectCoverImages(),
|
revalidateOnReconnect: false,
|
||||||
{
|
});
|
||||||
revalidateOnFocus: false,
|
|
||||||
revalidateOnReconnect: false,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const imagePickerRef = useRef<HTMLDivElement>(null);
|
const imagePickerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
@ -154,8 +149,7 @@ export const ImagePickerPopover: React.FC<Props> = ({
|
|||||||
<Tab.List as="span" className="inline-block rounded bg-custom-background-80 p-1">
|
<Tab.List as="span" className="inline-block rounded bg-custom-background-80 p-1">
|
||||||
{tabOptions.map((tab) => {
|
{tabOptions.map((tab) => {
|
||||||
if (!unsplashImages && unsplashError && tab.key === "unsplash") return null;
|
if (!unsplashImages && unsplashError && tab.key === "unsplash") return null;
|
||||||
if (projectCoverImages && projectCoverImages.length === 0 && tab.key === "images")
|
if (projectCoverImages && projectCoverImages.length === 0 && tab.key === "images") return null;
|
||||||
return null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tab
|
<Tab
|
||||||
@ -175,17 +169,25 @@ export const ImagePickerPopover: React.FC<Props> = ({
|
|||||||
{(unsplashImages || !unsplashError) && (
|
{(unsplashImages || !unsplashError) && (
|
||||||
<Tab.Panel className="h-full w-full space-y-4 mt-4">
|
<Tab.Panel className="h-full w-full space-y-4 mt-4">
|
||||||
<div className="flex gap-x-2">
|
<div className="flex gap-x-2">
|
||||||
<Input
|
<Controller
|
||||||
|
control={control}
|
||||||
name="search"
|
name="search"
|
||||||
className="text-sm"
|
render={({ field: { value, ref } }) => (
|
||||||
id="search"
|
<Input
|
||||||
value={formData.search}
|
id="search"
|
||||||
onChange={(e) => setFormData({ ...formData, search: e.target.value })}
|
name="search"
|
||||||
placeholder="Search for images"
|
type="text"
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => setFormData({ ...formData, search: e.target.value })}
|
||||||
|
ref={ref}
|
||||||
|
placeholder="Search for images"
|
||||||
|
className="text-sm w-full"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
<PrimaryButton onClick={() => setSearchParams(formData.search)} size="sm">
|
<Button variant="primary" onClick={() => setSearchParams(formData.search)} size="sm">
|
||||||
Search
|
Search
|
||||||
</PrimaryButton>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{unsplashImages ? (
|
{unsplashImages ? (
|
||||||
unsplashImages.length > 0 ? (
|
unsplashImages.length > 0 ? (
|
||||||
@ -208,9 +210,7 @@ export const ImagePickerPopover: React.FC<Props> = ({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-center text-custom-text-300 text-xs pt-7">
|
<p className="text-center text-custom-text-300 text-xs pt-7">No images found.</p>
|
||||||
No images found.
|
|
||||||
</p>
|
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<Loader className="grid grid-cols-4 gap-4">
|
<Loader className="grid grid-cols-4 gap-4">
|
||||||
@ -249,9 +249,7 @@ export const ImagePickerPopover: React.FC<Props> = ({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-center text-custom-text-300 text-xs pt-7">
|
<p className="text-center text-custom-text-300 text-xs pt-7">No images found.</p>
|
||||||
No images found.
|
|
||||||
</p>
|
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<Loader className="grid grid-cols-4 gap-4 pt-4">
|
<Loader className="grid grid-cols-4 gap-4 pt-4">
|
||||||
@ -297,9 +295,7 @@ export const ImagePickerPopover: React.FC<Props> = ({
|
|||||||
) : (
|
) : (
|
||||||
<div>
|
<div>
|
||||||
<span className="mt-2 block text-sm font-medium text-custom-text-200">
|
<span className="mt-2 block text-sm font-medium text-custom-text-200">
|
||||||
{isDragActive
|
{isDragActive ? "Drop image here to upload" : "Drag & drop image here"}
|
||||||
? "Drop image here to upload"
|
|
||||||
: "Drag & drop image here"}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -320,23 +316,24 @@ export const ImagePickerPopover: React.FC<Props> = ({
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="flex items-center justify-end gap-2">
|
<div className="flex items-center justify-end gap-2">
|
||||||
<SecondaryButton
|
<Button
|
||||||
className="w-full"
|
variant="neutral-primary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
setImage(null);
|
setImage(null);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</SecondaryButton>
|
</Button>
|
||||||
<PrimaryButton
|
<Button
|
||||||
|
variant="primary"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
disabled={!image}
|
disabled={!image}
|
||||||
loading={isImageUploading}
|
loading={isImageUploading}
|
||||||
>
|
>
|
||||||
{isImageUploading ? "Uploading..." : "Upload & Save"}
|
{isImageUploading ? "Uploading..." : "Upload & Save"}
|
||||||
</PrimaryButton>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
|
@ -1,26 +1,22 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
import useSWR, { mutate } from "swr";
|
import useSWR, { mutate } from "swr";
|
||||||
|
|
||||||
// react hook form
|
// react hook form
|
||||||
import { SubmitHandler, useForm } from "react-hook-form";
|
import { SubmitHandler, useForm } from "react-hook-form";
|
||||||
// headless ui
|
// headless ui
|
||||||
import { Combobox, Dialog, Transition } from "@headlessui/react";
|
import { Combobox, Dialog, Transition } from "@headlessui/react";
|
||||||
// services
|
// services
|
||||||
import issuesServices from "services/issues.service";
|
import { IssueService } from "services/issue";
|
||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
import useIssuesView from "hooks/use-issues-view";
|
import useIssuesView from "hooks/use-issues-view";
|
||||||
import useCalendarIssuesView from "hooks/use-calendar-issues-view";
|
|
||||||
// ui
|
// ui
|
||||||
import { DangerButton, SecondaryButton } from "components/ui";
|
import { Button } from "@plane/ui";
|
||||||
// icons
|
// icons
|
||||||
import { MagnifyingGlassIcon } from "@heroicons/react/24/outline";
|
import { MagnifyingGlassIcon } from "@heroicons/react/24/outline";
|
||||||
import { LayerDiagonalIcon } from "components/icons";
|
import { LayerDiagonalIcon } from "components/icons";
|
||||||
// types
|
// types
|
||||||
import { ICurrentUserResponse, IIssue } from "types";
|
import { IUser, IIssue } from "types";
|
||||||
// fetch keys
|
// fetch keys
|
||||||
import {
|
import {
|
||||||
CYCLE_DETAILS,
|
CYCLE_DETAILS,
|
||||||
@ -39,9 +35,11 @@ type FormInput = {
|
|||||||
type Props = {
|
type Props = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
user: ICurrentUserResponse | undefined;
|
user: IUser | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const issueService = new IssueService();
|
||||||
|
|
||||||
export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen, user }) => {
|
export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen, user }) => {
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
|
|
||||||
@ -49,17 +47,13 @@ export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen, user
|
|||||||
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
|
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
|
||||||
|
|
||||||
const { data: issues } = useSWR(
|
const { data: issues } = useSWR(
|
||||||
workspaceSlug && projectId
|
workspaceSlug && projectId ? PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string) : null,
|
||||||
? PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string)
|
workspaceSlug && projectId ? () => issueService.getIssues(workspaceSlug as string, projectId as string) : null
|
||||||
: null,
|
|
||||||
workspaceSlug && projectId
|
|
||||||
? () => issuesServices.getIssues(workspaceSlug as string, projectId as string)
|
|
||||||
: null
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
const { displayFilters, params } = useIssuesView();
|
const { displayFilters, params } = useIssuesView();
|
||||||
const { params: calendarParams } = useCalendarIssuesView();
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const { order_by, group_by, ...viewGanttParams } = params;
|
const { order_by, group_by, ...viewGanttParams } = params;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -94,14 +88,6 @@ export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen, user
|
|||||||
|
|
||||||
if (!Array.isArray(data.delete_issue_ids)) data.delete_issue_ids = [data.delete_issue_ids];
|
if (!Array.isArray(data.delete_issue_ids)) data.delete_issue_ids = [data.delete_issue_ids];
|
||||||
|
|
||||||
const calendarFetchKey = cycleId
|
|
||||||
? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), calendarParams)
|
|
||||||
: moduleId
|
|
||||||
? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), calendarParams)
|
|
||||||
: viewId
|
|
||||||
? VIEW_ISSUES(viewId.toString(), calendarParams)
|
|
||||||
: PROJECT_ISSUES_LIST_WITH_PARAMS(projectId?.toString() ?? "", calendarParams);
|
|
||||||
|
|
||||||
const ganttFetchKey = cycleId
|
const ganttFetchKey = cycleId
|
||||||
? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString())
|
? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString())
|
||||||
: moduleId
|
: moduleId
|
||||||
@ -110,7 +96,7 @@ export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen, user
|
|||||||
? VIEW_ISSUES(viewId.toString(), viewGanttParams)
|
? VIEW_ISSUES(viewId.toString(), viewGanttParams)
|
||||||
: PROJECT_ISSUES_LIST_WITH_PARAMS(projectId?.toString() ?? "");
|
: PROJECT_ISSUES_LIST_WITH_PARAMS(projectId?.toString() ?? "");
|
||||||
|
|
||||||
await issuesServices
|
await issueService
|
||||||
.bulkDeleteIssues(
|
.bulkDeleteIssues(
|
||||||
workspaceSlug as string,
|
workspaceSlug as string,
|
||||||
projectId as string,
|
projectId as string,
|
||||||
@ -126,8 +112,7 @@ export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen, user
|
|||||||
message: "Issues deleted successfully!",
|
message: "Issues deleted successfully!",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (displayFilters.layout === "calendar") mutate(calendarFetchKey);
|
if (displayFilters.layout === "gantt_chart") mutate(ganttFetchKey);
|
||||||
else if (displayFilters.layout === "gantt_chart") mutate(ganttFetchKey);
|
|
||||||
else {
|
else {
|
||||||
if (cycleId) {
|
if (cycleId) {
|
||||||
mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), params));
|
mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), params));
|
||||||
@ -155,9 +140,7 @@ export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen, user
|
|||||||
: issues?.filter(
|
: issues?.filter(
|
||||||
(issue) =>
|
(issue) =>
|
||||||
issue.name.toLowerCase().includes(query.toLowerCase()) ||
|
issue.name.toLowerCase().includes(query.toLowerCase()) ||
|
||||||
`${issue.project_detail.identifier}-${issue.sequence_id}`
|
`${issue.project_detail.identifier}-${issue.sequence_id}`.toLowerCase().includes(query.toLowerCase())
|
||||||
.toLowerCase()
|
|
||||||
.includes(query.toLowerCase())
|
|
||||||
) ?? [];
|
) ?? [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -216,7 +199,7 @@ export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen, user
|
|||||||
key={issue.id}
|
key={issue.id}
|
||||||
as="div"
|
as="div"
|
||||||
value={issue.id}
|
value={issue.id}
|
||||||
className={({ active, selected }) =>
|
className={({ active }) =>
|
||||||
`flex cursor-pointer select-none items-center justify-between rounded-md px-3 py-2 ${
|
`flex cursor-pointer select-none items-center justify-between rounded-md px-3 py-2 ${
|
||||||
active ? "bg-custom-background-80 text-custom-text-100" : ""
|
active ? "bg-custom-background-80 text-custom-text-100" : ""
|
||||||
}`
|
}`
|
||||||
@ -257,10 +240,12 @@ export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen, user
|
|||||||
|
|
||||||
{filteredIssues.length > 0 && (
|
{filteredIssues.length > 0 && (
|
||||||
<div className="flex items-center justify-end gap-2 p-3">
|
<div className="flex items-center justify-end gap-2 p-3">
|
||||||
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
|
<Button variant="neutral-primary" onClick={handleClose}>
|
||||||
<DangerButton onClick={handleSubmit(handleDelete)} loading={isSubmitting}>
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button variant="danger" onClick={handleSubmit(handleDelete)} loading={isSubmitting}>
|
||||||
{isSubmitting ? "Deleting..." : "Delete selected issues"}
|
{isSubmitting ? "Deleting..." : "Delete selected issues"}
|
||||||
</DangerButton>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</form>
|
</form>
|
||||||
|
@ -7,13 +7,13 @@ import { mutate } from "swr";
|
|||||||
// headless ui
|
// headless ui
|
||||||
import { Combobox, Dialog, Transition } from "@headlessui/react";
|
import { Combobox, Dialog, Transition } from "@headlessui/react";
|
||||||
// services
|
// services
|
||||||
import projectService from "services/project.service";
|
import { ProjectService } from "services/project";
|
||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
import useIssuesView from "hooks/use-issues-view";
|
import useIssuesView from "hooks/use-issues-view";
|
||||||
import useDebounce from "hooks/use-debounce";
|
import useDebounce from "hooks/use-debounce";
|
||||||
// ui
|
// ui
|
||||||
import { Loader, PrimaryButton, SecondaryButton, ToggleSwitch, Tooltip } from "components/ui";
|
import { Button, Loader, ToggleSwitch, Tooltip } from "@plane/ui";
|
||||||
// icons
|
// icons
|
||||||
import { LaunchOutlined } from "@mui/icons-material";
|
import { LaunchOutlined } from "@mui/icons-material";
|
||||||
import { MagnifyingGlassIcon, XMarkIcon } from "@heroicons/react/24/outline";
|
import { MagnifyingGlassIcon, XMarkIcon } from "@heroicons/react/24/outline";
|
||||||
@ -36,6 +36,8 @@ type Props = {
|
|||||||
workspaceLevelToggle?: boolean;
|
workspaceLevelToggle?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const projectService = new ProjectService();
|
||||||
|
|
||||||
export const ExistingIssuesListModal: React.FC<Props> = ({
|
export const ExistingIssuesListModal: React.FC<Props> = ({
|
||||||
isOpen,
|
isOpen,
|
||||||
handleClose: onClose,
|
handleClose: onClose,
|
||||||
@ -117,12 +119,7 @@ export const ExistingIssuesListModal: React.FC<Props> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Transition.Root
|
<Transition.Root show={isOpen} as={React.Fragment} afterLeave={() => setSearchTerm("")} appear>
|
||||||
show={isOpen}
|
|
||||||
as={React.Fragment}
|
|
||||||
afterLeave={() => setSearchTerm("")}
|
|
||||||
appear
|
|
||||||
>
|
|
||||||
<Dialog as="div" className="relative z-20" onClose={handleClose}>
|
<Dialog as="div" className="relative z-20" onClose={handleClose}>
|
||||||
<Transition.Child
|
<Transition.Child
|
||||||
as={React.Fragment}
|
as={React.Fragment}
|
||||||
@ -180,11 +177,7 @@ export const ExistingIssuesListModal: React.FC<Props> = ({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="group p-1"
|
className="group p-1"
|
||||||
onClick={() =>
|
onClick={() => setSelectedIssues((prevData) => prevData.filter((i) => i.id !== issue.id))}
|
||||||
setSelectedIssues((prevData) =>
|
|
||||||
prevData.filter((i) => i.id !== issue.id)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<XMarkIcon className="h-3 w-3 text-custom-text-200 group-hover:text-custom-text-100" />
|
<XMarkIcon className="h-3 w-3 text-custom-text-200 group-hover:text-custom-text-100" />
|
||||||
</button>
|
</button>
|
||||||
@ -232,21 +225,15 @@ export const ExistingIssuesListModal: React.FC<Props> = ({
|
|||||||
</h5>
|
</h5>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!isSearching &&
|
{!isSearching && issues.length === 0 && searchTerm !== "" && debouncedSearchTerm !== "" && (
|
||||||
issues.length === 0 &&
|
<div className="flex flex-col items-center justify-center gap-4 px-3 py-8 text-center">
|
||||||
searchTerm !== "" &&
|
<LayerDiagonalIcon height="52" width="52" />
|
||||||
debouncedSearchTerm !== "" && (
|
<h3 className="text-custom-text-200">
|
||||||
<div className="flex flex-col items-center justify-center gap-4 px-3 py-8 text-center">
|
No issues found. Create a new issue with{" "}
|
||||||
<LayerDiagonalIcon height="52" width="52" />
|
<pre className="inline rounded bg-custom-background-80 px-2 py-1 text-sm">C</pre>.
|
||||||
<h3 className="text-custom-text-200">
|
</h3>
|
||||||
No issues found. Create a new issue with{" "}
|
</div>
|
||||||
<pre className="inline rounded bg-custom-background-80 px-2 py-1 text-sm">
|
)}
|
||||||
C
|
|
||||||
</pre>
|
|
||||||
.
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isSearching ? (
|
{isSearching ? (
|
||||||
<Loader className="space-y-3 p-3">
|
<Loader className="space-y-3 p-3">
|
||||||
@ -256,9 +243,7 @@ export const ExistingIssuesListModal: React.FC<Props> = ({
|
|||||||
<Loader.Item height="40px" />
|
<Loader.Item height="40px" />
|
||||||
</Loader>
|
</Loader>
|
||||||
) : (
|
) : (
|
||||||
<ul
|
<ul className={`text-sm text-custom-text-100 ${issues.length > 0 ? "p-2" : ""}`}>
|
||||||
className={`text-sm text-custom-text-100 ${issues.length > 0 ? "p-2" : ""}`}
|
|
||||||
>
|
|
||||||
{issues.map((issue) => {
|
{issues.map((issue) => {
|
||||||
const selected = selectedIssues.some((i) => i.id === issue.id);
|
const selected = selectedIssues.some((i) => i.id === issue.id);
|
||||||
|
|
||||||
@ -309,10 +294,12 @@ export const ExistingIssuesListModal: React.FC<Props> = ({
|
|||||||
</Combobox>
|
</Combobox>
|
||||||
{selectedIssues.length > 0 && (
|
{selectedIssues.length > 0 && (
|
||||||
<div className="flex items-center justify-end gap-2 p-3">
|
<div className="flex items-center justify-end gap-2 p-3">
|
||||||
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
|
<Button variant="neutral-primary" onClick={handleClose}>
|
||||||
<PrimaryButton onClick={onSubmit} loading={isSubmitting}>
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button variant="primary" onClick={onSubmit} loading={isSubmitting}>
|
||||||
{isSubmitting ? "Adding..." : "Add selected issues"}
|
{isSubmitting ? "Adding..." : "Add selected issues"}
|
||||||
</PrimaryButton>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Dialog.Panel>
|
</Dialog.Panel>
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
import React, { useEffect, useState, useRef } from "react";
|
import React, { useEffect, useState, useRef } from "react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
// react-hook-form
|
// react-hook-form
|
||||||
import { useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
// services
|
// services
|
||||||
import aiService from "services/ai.service";
|
import { AIService } from "services/ai.service";
|
||||||
import trackEventServices from "services/track-event.service";
|
import { TrackEventService } from "services/track_event.service";
|
||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
import useUserAuth from "hooks/use-user-auth";
|
import useUserAuth from "hooks/use-user-auth";
|
||||||
// ui
|
// ui
|
||||||
import { Input, PrimaryButton, SecondaryButton } from "components/ui";
|
import { Button, Input } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { RichReadOnlyEditor, RichReadOnlyEditorWithRef } from "@plane/rich-text-editor";
|
import { RichReadOnlyEditor, RichReadOnlyEditorWithRef } from "@plane/rich-text-editor";
|
||||||
// types
|
// types
|
||||||
@ -32,6 +32,10 @@ type FormData = {
|
|||||||
task: string;
|
task: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// services
|
||||||
|
const aiService = new AIService();
|
||||||
|
const trackEventService = new TrackEventService();
|
||||||
|
|
||||||
export const GptAssistantModal: React.FC<Props> = ({
|
export const GptAssistantModal: React.FC<Props> = ({
|
||||||
isOpen,
|
isOpen,
|
||||||
handleClose,
|
handleClose,
|
||||||
@ -57,7 +61,7 @@ export const GptAssistantModal: React.FC<Props> = ({
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
register,
|
control,
|
||||||
reset,
|
reset,
|
||||||
setFocus,
|
setFocus,
|
||||||
formState: { isSubmitting },
|
formState: { isSubmitting },
|
||||||
@ -111,9 +115,7 @@ export const GptAssistantModal: React.FC<Props> = ({
|
|||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
title: "Error!",
|
title: "Error!",
|
||||||
message:
|
message: error || "You have reached the maximum number of requests of 50 requests per month per user.",
|
||||||
error ||
|
|
||||||
"You have reached the maximum number of requests of 50 requests per month per user.",
|
|
||||||
});
|
});
|
||||||
else
|
else
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
@ -134,8 +136,9 @@ export const GptAssistantModal: React.FC<Props> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`absolute ${inset} z-20 w-full space-y-4 rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-4 shadow ${isOpen ? "block" : "hidden"
|
className={`absolute ${inset} z-20 w-full space-y-4 rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-4 shadow ${
|
||||||
}`}
|
isOpen ? "block" : "hidden"
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
{((content && content !== "") || (htmlContent && htmlContent !== "<p></p>")) && (
|
{((content && content !== "") || (htmlContent && htmlContent !== "<p></p>")) && (
|
||||||
<div className="text-sm">
|
<div className="text-sm">
|
||||||
@ -162,56 +165,50 @@ export const GptAssistantModal: React.FC<Props> = ({
|
|||||||
)}
|
)}
|
||||||
{invalidResponse && (
|
{invalidResponse && (
|
||||||
<div className="text-sm text-red-500">
|
<div className="text-sm text-red-500">
|
||||||
No response could be generated. This may be due to insufficient content or task
|
No response could be generated. This may be due to insufficient content or task information. Please try again.
|
||||||
information. Please try again.
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Input
|
<Controller
|
||||||
type="text"
|
control={control}
|
||||||
name="task"
|
name="task"
|
||||||
register={register}
|
render={({ field: { value, onChange, ref } }) => (
|
||||||
placeholder={`${content && content !== ""
|
<Input
|
||||||
? "Tell AI what action to perform on this content..."
|
id="task"
|
||||||
: "Ask AI anything..."
|
name="task"
|
||||||
}`}
|
type="text"
|
||||||
autoComplete="off"
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
ref={ref}
|
||||||
|
placeholder={`${
|
||||||
|
content && content !== "" ? "Tell AI what action to perform on this content..." : "Ask AI anything..."
|
||||||
|
}`}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
<div className={`flex gap-2 ${response === "" ? "justify-end" : "justify-between"}`}>
|
<div className={`flex gap-2 ${response === "" ? "justify-end" : "justify-between"}`}>
|
||||||
{response !== "" && (
|
{response !== "" && (
|
||||||
<PrimaryButton
|
<Button
|
||||||
|
variant="primary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onResponse(response);
|
onResponse(response);
|
||||||
onClose();
|
onClose();
|
||||||
if (block)
|
if (block && user)
|
||||||
trackEventServices.trackUseGPTResponseEvent(
|
trackEventService.trackUseGPTResponseEvent(block, "USE_GPT_RESPONSE_IN_PAGE_BLOCK", user);
|
||||||
block,
|
else if (issue && user)
|
||||||
"USE_GPT_RESPONSE_IN_PAGE_BLOCK",
|
trackEventService.trackUseGPTResponseEvent(issue, "USE_GPT_RESPONSE_IN_ISSUE", user);
|
||||||
user
|
|
||||||
);
|
|
||||||
else if (issue)
|
|
||||||
trackEventServices.trackUseGPTResponseEvent(
|
|
||||||
issue,
|
|
||||||
"USE_GPT_RESPONSE_IN_ISSUE",
|
|
||||||
user
|
|
||||||
);
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Use this response
|
Use this response
|
||||||
</PrimaryButton>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<SecondaryButton onClick={onClose}>Close</SecondaryButton>
|
<Button variant="neutral-primary" onClick={onClose}>
|
||||||
<PrimaryButton
|
Close
|
||||||
type="button"
|
</Button>
|
||||||
onClick={handleSubmit(handleResponse)}
|
<Button variant="primary" onClick={handleSubmit(handleResponse)} loading={isSubmitting}>
|
||||||
loading={isSubmitting}
|
{isSubmitting ? "Generating response..." : response === "" ? "Generate response" : "Generate again"}
|
||||||
>
|
</Button>
|
||||||
{isSubmitting
|
|
||||||
? "Generating response..."
|
|
||||||
: response === ""
|
|
||||||
? "Generate response"
|
|
||||||
: "Generate again"}
|
|
||||||
</PrimaryButton>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -7,11 +7,11 @@ import { useDropzone } from "react-dropzone";
|
|||||||
// headless ui
|
// headless ui
|
||||||
import { Transition, Dialog } from "@headlessui/react";
|
import { Transition, Dialog } from "@headlessui/react";
|
||||||
// services
|
// services
|
||||||
import fileService from "services/file.service";
|
import { FileService } from "services/file.service";
|
||||||
// hooks
|
// hooks
|
||||||
import useWorkspaceDetails from "hooks/use-workspace-details";
|
import useWorkspaceDetails from "hooks/use-workspace-details";
|
||||||
// ui
|
// ui
|
||||||
import { DangerButton, PrimaryButton, SecondaryButton } from "components/ui";
|
import { Button } from "@plane/ui";
|
||||||
// icons
|
// icons
|
||||||
import { UserCircleIcon } from "components/icons";
|
import { UserCircleIcon } from "components/icons";
|
||||||
|
|
||||||
@ -25,6 +25,9 @@ type Props = {
|
|||||||
userImage?: boolean;
|
userImage?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// services
|
||||||
|
const fileService = new FileService();
|
||||||
|
|
||||||
export const ImageUploadModal: React.FC<Props> = ({
|
export const ImageUploadModal: React.FC<Props> = ({
|
||||||
value,
|
value,
|
||||||
onSuccess,
|
onSuccess,
|
||||||
@ -127,10 +130,7 @@ export const ImageUploadModal: React.FC<Props> = ({
|
|||||||
>
|
>
|
||||||
<Dialog.Panel className="relative transform overflow-hidden rounded-lg border border-custom-border-200 bg-custom-background-100 px-5 py-8 text-left shadow-xl transition-all sm:w-full sm:max-w-xl sm:p-6">
|
<Dialog.Panel className="relative transform overflow-hidden rounded-lg border border-custom-border-200 bg-custom-background-100 px-5 py-8 text-left shadow-xl transition-all sm:w-full sm:max-w-xl sm:p-6">
|
||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
<Dialog.Title
|
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-custom-text-100">
|
||||||
as="h3"
|
|
||||||
className="text-lg font-medium leading-6 text-custom-text-100"
|
|
||||||
>
|
|
||||||
Upload Image
|
Upload Image
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
@ -161,9 +161,7 @@ export const ImageUploadModal: React.FC<Props> = ({
|
|||||||
<div>
|
<div>
|
||||||
<UserCircleIcon className="mx-auto h-16 w-16 text-custom-text-200" />
|
<UserCircleIcon className="mx-auto h-16 w-16 text-custom-text-200" />
|
||||||
<span className="mt-2 block text-sm font-medium text-custom-text-200">
|
<span className="mt-2 block text-sm font-medium text-custom-text-200">
|
||||||
{isDragActive
|
{isDragActive ? "Drop image here to upload" : "Drag & drop image here"}
|
||||||
? "Drop image here to upload"
|
|
||||||
: "Drag & drop image here"}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -185,19 +183,17 @@ export const ImageUploadModal: React.FC<Props> = ({
|
|||||||
</p>
|
</p>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<DangerButton onClick={handleDelete} outline disabled={!value}>
|
<Button variant="danger" onClick={handleDelete} disabled={!value}>
|
||||||
{isRemoving ? "Removing..." : "Remove"}
|
{isRemoving ? "Removing..." : "Remove"}
|
||||||
</DangerButton>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
|
<Button variant="neutral-primary" onClick={handleClose}>
|
||||||
<PrimaryButton
|
Cancel
|
||||||
onClick={handleSubmit}
|
</Button>
|
||||||
disabled={!image}
|
<Button variant="primary" onClick={handleSubmit} disabled={!image} loading={isImageUploading}>
|
||||||
loading={isImageUploading}
|
|
||||||
>
|
|
||||||
{isImageUploading ? "Uploading..." : "Upload & Save"}
|
{isImageUploading ? "Uploading..." : "Upload & Save"}
|
||||||
</PrimaryButton>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Dialog.Panel>
|
</Dialog.Panel>
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import React, { useEffect } from "react";
|
import { FC, useEffect, Fragment } from "react";
|
||||||
|
|
||||||
// react-hook-form
|
// react-hook-form
|
||||||
import { useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
// headless ui
|
// headless ui
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
// ui
|
// ui
|
||||||
import { Input, PrimaryButton, SecondaryButton } from "components/ui";
|
import { Button, Input } from "@plane/ui";
|
||||||
// types
|
// types
|
||||||
import type { IIssueLink, linkDetails, ModuleLink } from "types";
|
import type { IIssueLink, linkDetails, ModuleLink } from "types";
|
||||||
|
|
||||||
@ -23,18 +23,13 @@ const defaultValues: IIssueLink | ModuleLink = {
|
|||||||
url: "",
|
url: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const LinkModal: React.FC<Props> = ({
|
export const LinkModal: FC<Props> = (props) => {
|
||||||
isOpen,
|
const { isOpen, handleClose, createIssueLink, updateIssueLink, status, data } = props;
|
||||||
handleClose,
|
// form info
|
||||||
createIssueLink,
|
|
||||||
updateIssueLink,
|
|
||||||
status,
|
|
||||||
data,
|
|
||||||
}) => {
|
|
||||||
const {
|
const {
|
||||||
register,
|
|
||||||
formState: { errors, isSubmitting },
|
formState: { errors, isSubmitting },
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
|
control,
|
||||||
reset,
|
reset,
|
||||||
} = useForm<ModuleLink>({
|
} = useForm<ModuleLink>({
|
||||||
defaultValues,
|
defaultValues,
|
||||||
@ -70,10 +65,10 @@ export const LinkModal: React.FC<Props> = ({
|
|||||||
}, [data, reset]);
|
}, [data, reset]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Transition.Root show={isOpen} as={React.Fragment}>
|
<Transition.Root show={isOpen} as={Fragment}>
|
||||||
<Dialog as="div" className="relative z-20" onClose={onClose}>
|
<Dialog as="div" className="relative z-20" onClose={onClose}>
|
||||||
<Transition.Child
|
<Transition.Child
|
||||||
as={React.Fragment}
|
as={Fragment}
|
||||||
enter="ease-out duration-300"
|
enter="ease-out duration-300"
|
||||||
enterFrom="opacity-0"
|
enterFrom="opacity-0"
|
||||||
enterTo="opacity-100"
|
enterTo="opacity-100"
|
||||||
@ -87,7 +82,7 @@ export const LinkModal: React.FC<Props> = ({
|
|||||||
<div className="fixed inset-0 z-10 overflow-y-auto">
|
<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="flex min-h-full items-center justify-center p-4 text-center sm:p-0">
|
||||||
<Transition.Child
|
<Transition.Child
|
||||||
as={React.Fragment}
|
as={Fragment}
|
||||||
enter="ease-out duration-300"
|
enter="ease-out duration-300"
|
||||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||||
@ -99,46 +94,65 @@ export const LinkModal: React.FC<Props> = ({
|
|||||||
<form onSubmit={handleSubmit(handleCreateUpdatePage)}>
|
<form onSubmit={handleSubmit(handleCreateUpdatePage)}>
|
||||||
<div>
|
<div>
|
||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
<Dialog.Title
|
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-custom-text-100">
|
||||||
as="h3"
|
|
||||||
className="text-lg font-medium leading-6 text-custom-text-100"
|
|
||||||
>
|
|
||||||
{status ? "Update Link" : "Add Link"}
|
{status ? "Update Link" : "Add Link"}
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
<div className="mt-2 space-y-3">
|
<div className="mt-2 space-y-3">
|
||||||
<div>
|
<div>
|
||||||
<Input
|
<label htmlFor="url" className="text-custom-text-200 mb-2">
|
||||||
id="url"
|
URL
|
||||||
label="URL"
|
</label>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
name="url"
|
name="url"
|
||||||
type="url"
|
rules={{
|
||||||
placeholder="https://..."
|
|
||||||
autoComplete="off"
|
|
||||||
error={errors.url}
|
|
||||||
register={register}
|
|
||||||
validations={{
|
|
||||||
required: "URL is required",
|
required: "URL is required",
|
||||||
}}
|
}}
|
||||||
|
render={({ field: { value, onChange, ref } }) => (
|
||||||
|
<Input
|
||||||
|
id="url"
|
||||||
|
name="url"
|
||||||
|
type="url"
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
ref={ref}
|
||||||
|
hasError={Boolean(errors.url)}
|
||||||
|
placeholder="https://..."
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Input
|
<label htmlFor="title" className="text-custom-text-200 mb-2">
|
||||||
id="title"
|
{`Title (optional)`}
|
||||||
label="Title (optional)"
|
</label>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
name="title"
|
name="title"
|
||||||
type="text"
|
render={({ field: { value, onChange, ref } }) => (
|
||||||
placeholder="Enter title"
|
<Input
|
||||||
autoComplete="off"
|
id="title"
|
||||||
error={errors.title}
|
name="title"
|
||||||
register={register}
|
type="text"
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
ref={ref}
|
||||||
|
hasError={Boolean(errors.title)}
|
||||||
|
placeholder="Enter title"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-5 flex justify-end gap-2">
|
<div className="mt-5 flex justify-end gap-2">
|
||||||
<SecondaryButton onClick={onClose}>Cancel</SecondaryButton>
|
<Button variant="neutral-primary" onClick={onClose}>
|
||||||
<PrimaryButton type="submit" loading={isSubmitting}>
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button variant="primary" type="submit" loading={isSubmitting}>
|
||||||
{status
|
{status
|
||||||
? isSubmitting
|
? isSubmitting
|
||||||
? "Updating Link..."
|
? "Updating Link..."
|
||||||
@ -146,7 +160,7 @@ export const LinkModal: React.FC<Props> = ({
|
|||||||
: isSubmitting
|
: isSubmitting
|
||||||
? "Adding Link..."
|
? "Adding Link..."
|
||||||
: "Add Link"}
|
: "Add Link"}
|
||||||
</PrimaryButton>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Dialog.Panel>
|
</Dialog.Panel>
|
||||||
|
@ -1,24 +1,11 @@
|
|||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
|
|
||||||
// headless ui
|
|
||||||
import { Popover, Transition } from "@headlessui/react";
|
import { Popover, Transition } from "@headlessui/react";
|
||||||
|
|
||||||
// helper
|
// helper
|
||||||
import { renderEmoji } from "helpers/emoji.helper";
|
import { renderEmoji } from "helpers/emoji.helper";
|
||||||
|
|
||||||
// icons
|
// icons
|
||||||
import { Icon } from "components/ui";
|
import { Icon } from "components/ui";
|
||||||
|
|
||||||
const reactionEmojis = [
|
const reactionEmojis = ["128077", "128078", "128516", "128165", "128533", "129505", "9992", "128064"];
|
||||||
"128077",
|
|
||||||
"128078",
|
|
||||||
"128516",
|
|
||||||
"128165",
|
|
||||||
"128533",
|
|
||||||
"129505",
|
|
||||||
"9992",
|
|
||||||
"128064",
|
|
||||||
];
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
size?: "sm" | "md" | "lg";
|
size?: "sm" | "md" | "lg";
|
||||||
@ -28,7 +15,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ReactionSelector: React.FC<Props> = (props) => {
|
export const ReactionSelector: React.FC<Props> = (props) => {
|
||||||
const { value, onSelect, position, size } = props;
|
const { onSelect, position, size } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover className="relative">
|
<Popover className="relative">
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { ProgressBar } from "components/ui";
|
import { ProgressBar } from "@plane/ui";
|
||||||
|
|
||||||
type TSingleProgressStatsProps = {
|
type TSingleProgressStatsProps = {
|
||||||
title: any;
|
title: any;
|
||||||
@ -30,10 +30,7 @@ export const SingleProgressStats: React.FC<TSingleProgressStatsProps> = ({
|
|||||||
<ProgressBar value={completed} maxValue={total} />
|
<ProgressBar value={completed} maxValue={total} />
|
||||||
</span>
|
</span>
|
||||||
<span className="w-8 text-right">
|
<span className="w-8 text-right">
|
||||||
{isNaN(Math.floor((completed / total) * 100))
|
{isNaN(Math.floor((completed / total) * 100)) ? "0" : Math.floor((completed / total) * 100)}%
|
||||||
? "0"
|
|
||||||
: Math.floor((completed / total) * 100)}
|
|
||||||
%
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span>of {total}</span>
|
<span>of {total}</span>
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import React from "react";
|
import { FC, Fragment } from "react";
|
||||||
|
|
||||||
// react-form
|
// react-form
|
||||||
import {
|
import {
|
||||||
|
Control,
|
||||||
|
Controller,
|
||||||
FieldError,
|
FieldError,
|
||||||
FieldErrorsImpl,
|
FieldErrorsImpl,
|
||||||
Merge,
|
Merge,
|
||||||
@ -13,7 +14,7 @@ import {
|
|||||||
import { ColorResult, SketchPicker } from "react-color";
|
import { ColorResult, SketchPicker } from "react-color";
|
||||||
// component
|
// component
|
||||||
import { Popover, Transition } from "@headlessui/react";
|
import { Popover, Transition } from "@headlessui/react";
|
||||||
import { Input } from "components/ui";
|
import { Input } from "@plane/ui";
|
||||||
// icons
|
// icons
|
||||||
import { ColorPickerIcon } from "components/icons";
|
import { ColorPickerIcon } from "components/icons";
|
||||||
// types
|
// types
|
||||||
@ -24,18 +25,14 @@ type Props = {
|
|||||||
position?: "left" | "right";
|
position?: "left" | "right";
|
||||||
watch: UseFormWatch<any>;
|
watch: UseFormWatch<any>;
|
||||||
setValue: UseFormSetValue<any>;
|
setValue: UseFormSetValue<any>;
|
||||||
|
control: Control<ICustomTheme, any>;
|
||||||
error: FieldError | Merge<FieldError, FieldErrorsImpl<any>> | undefined;
|
error: FieldError | Merge<FieldError, FieldErrorsImpl<any>> | undefined;
|
||||||
register: UseFormRegister<any>;
|
register: UseFormRegister<any>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ColorPickerInput: React.FC<Props> = ({
|
export const ColorPickerInput: FC<Props> = (props) => {
|
||||||
name,
|
const { name, position = "left", watch, setValue, error, control } = props;
|
||||||
position = "left",
|
|
||||||
watch,
|
|
||||||
setValue,
|
|
||||||
error,
|
|
||||||
register,
|
|
||||||
}) => {
|
|
||||||
const handleColorChange = (newColor: ColorResult) => {
|
const handleColorChange = (newColor: ColorResult) => {
|
||||||
const { hex } = newColor;
|
const { hex } = newColor;
|
||||||
setValue(name, hex);
|
setValue(name, hex);
|
||||||
@ -60,22 +57,28 @@ export const ColorPickerInput: React.FC<Props> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Input
|
<Controller
|
||||||
id={name}
|
control={control}
|
||||||
name={name}
|
name={name}
|
||||||
type="name"
|
rules={{
|
||||||
placeholder="#FFFFFF"
|
|
||||||
autoComplete="off"
|
|
||||||
error={error}
|
|
||||||
value={watch(name)}
|
|
||||||
register={register}
|
|
||||||
validations={{
|
|
||||||
required: `${getColorText(name)} color is required`,
|
required: `${getColorText(name)} color is required`,
|
||||||
pattern: {
|
pattern: {
|
||||||
value: /^#(?:[0-9a-fA-F]{3}){1,2}$/g,
|
value: /^#(?:[0-9a-fA-F]{3}){1,2}$/g,
|
||||||
message: `${getColorText(name)} color should be hex format`,
|
message: `${getColorText(name)} color should be hex format`,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
|
render={({ field: { onChange, ref } }) => (
|
||||||
|
<Input
|
||||||
|
id={name}
|
||||||
|
name={name}
|
||||||
|
type="text"
|
||||||
|
value={watch("name")}
|
||||||
|
onChange={onChange}
|
||||||
|
ref={ref}
|
||||||
|
hasError={Boolean(error)}
|
||||||
|
placeholder="#FFFFFF"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
<div className="absolute right-4 top-2.5">
|
<div className="absolute right-4 top-2.5">
|
||||||
<Popover className="relative grid place-items-center">
|
<Popover className="relative grid place-items-center">
|
||||||
@ -95,16 +98,12 @@ export const ColorPickerInput: React.FC<Props> = ({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<ColorPickerIcon
|
<ColorPickerIcon height={14} width={14} className="fill-current text-custom-text-100" />
|
||||||
height={14}
|
|
||||||
width={14}
|
|
||||||
className="fill-current text-custom-text-100"
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</Popover.Button>
|
</Popover.Button>
|
||||||
|
|
||||||
<Transition
|
<Transition
|
||||||
as={React.Fragment}
|
as={Fragment}
|
||||||
enter="transition ease-out duration-200"
|
enter="transition ease-out duration-200"
|
||||||
enterFrom="opacity-0 translate-y-1"
|
enterFrom="opacity-0 translate-y-1"
|
||||||
enterTo="opacity-100 translate-y-0"
|
enterTo="opacity-100 translate-y-0"
|
||||||
|
@ -5,8 +5,8 @@ import { useTheme } from "next-themes";
|
|||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
|
|
||||||
// ui
|
// ui
|
||||||
import { PrimaryButton } from "components/ui";
|
|
||||||
import { ColorPickerInput } from "components/core";
|
import { ColorPickerInput } from "components/core";
|
||||||
|
import { Button } from "@plane/ui";
|
||||||
// types
|
// types
|
||||||
import { ICustomTheme } from "types";
|
import { ICustomTheme } from "types";
|
||||||
// mobx react lite
|
// mobx react lite
|
||||||
@ -33,11 +33,14 @@ export const CustomThemeSelector: React.FC<Props> = observer(({ preLoadedData })
|
|||||||
const store: any = useMobxStore();
|
const store: any = useMobxStore();
|
||||||
const { setTheme } = useTheme();
|
const { setTheme } = useTheme();
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const [darkPalette, setDarkPalette] = useState(false);
|
const [darkPalette, setDarkPalette] = useState(false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
formState: { errors, isSubmitting },
|
formState: { errors, isSubmitting },
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
|
control,
|
||||||
watch,
|
watch,
|
||||||
setValue,
|
setValue,
|
||||||
reset,
|
reset,
|
||||||
@ -78,12 +81,11 @@ export const CustomThemeSelector: React.FC<Props> = observer(({ preLoadedData })
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="grid grid-cols-1 gap-x-6 gap-y-4 sm:grid-cols-2 md:grid-cols-3">
|
<div className="grid grid-cols-1 gap-x-6 gap-y-4 sm:grid-cols-2 md:grid-cols-3">
|
||||||
<div className="flex flex-col items-start gap-2">
|
<div className="flex flex-col items-start gap-2">
|
||||||
<h3 className="text-left text-sm font-medium text-custom-text-200">
|
<h3 className="text-left text-sm font-medium text-custom-text-200">Background color</h3>
|
||||||
Background color
|
|
||||||
</h3>
|
|
||||||
<ColorPickerInput
|
<ColorPickerInput
|
||||||
name="background"
|
name="background"
|
||||||
position="right"
|
position="right"
|
||||||
|
control={control}
|
||||||
error={errors.background}
|
error={errors.background}
|
||||||
watch={watch}
|
watch={watch}
|
||||||
setValue={setValue}
|
setValue={setValue}
|
||||||
@ -95,6 +97,7 @@ export const CustomThemeSelector: React.FC<Props> = observer(({ preLoadedData })
|
|||||||
<h3 className="text-left text-sm font-medium text-custom-text-200">Text color</h3>
|
<h3 className="text-left text-sm font-medium text-custom-text-200">Text color</h3>
|
||||||
<ColorPickerInput
|
<ColorPickerInput
|
||||||
name="text"
|
name="text"
|
||||||
|
control={control}
|
||||||
error={errors.text}
|
error={errors.text}
|
||||||
watch={watch}
|
watch={watch}
|
||||||
setValue={setValue}
|
setValue={setValue}
|
||||||
@ -103,12 +106,11 @@ export const CustomThemeSelector: React.FC<Props> = observer(({ preLoadedData })
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col items-start gap-2">
|
<div className="flex flex-col items-start gap-2">
|
||||||
<h3 className="text-left text-sm font-medium text-custom-text-200">
|
<h3 className="text-left text-sm font-medium text-custom-text-200">Primary(Theme) color</h3>
|
||||||
Primary(Theme) color
|
|
||||||
</h3>
|
|
||||||
<ColorPickerInput
|
<ColorPickerInput
|
||||||
name="primary"
|
name="primary"
|
||||||
error={errors.primary}
|
error={errors.primary}
|
||||||
|
control={control}
|
||||||
watch={watch}
|
watch={watch}
|
||||||
setValue={setValue}
|
setValue={setValue}
|
||||||
register={register}
|
register={register}
|
||||||
@ -116,12 +118,11 @@ export const CustomThemeSelector: React.FC<Props> = observer(({ preLoadedData })
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col items-start gap-2">
|
<div className="flex flex-col items-start gap-2">
|
||||||
<h3 className="text-left text-sm font-medium text-custom-text-200">
|
<h3 className="text-left text-sm font-medium text-custom-text-200">Sidebar background color</h3>
|
||||||
Sidebar background color
|
|
||||||
</h3>
|
|
||||||
<ColorPickerInput
|
<ColorPickerInput
|
||||||
name="sidebarBackground"
|
name="sidebarBackground"
|
||||||
position="right"
|
position="right"
|
||||||
|
control={control}
|
||||||
error={errors.sidebarBackground}
|
error={errors.sidebarBackground}
|
||||||
watch={watch}
|
watch={watch}
|
||||||
setValue={setValue}
|
setValue={setValue}
|
||||||
@ -130,11 +131,10 @@ export const CustomThemeSelector: React.FC<Props> = observer(({ preLoadedData })
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col items-start gap-2">
|
<div className="flex flex-col items-start gap-2">
|
||||||
<h3 className="text-left text-sm font-medium text-custom-text-200">
|
<h3 className="text-left text-sm font-medium text-custom-text-200">Sidebar text color</h3>
|
||||||
Sidebar text color
|
|
||||||
</h3>
|
|
||||||
<ColorPickerInput
|
<ColorPickerInput
|
||||||
name="sidebarText"
|
name="sidebarText"
|
||||||
|
control={control}
|
||||||
error={errors.sidebarText}
|
error={errors.sidebarText}
|
||||||
watch={watch}
|
watch={watch}
|
||||||
setValue={setValue}
|
setValue={setValue}
|
||||||
@ -145,9 +145,9 @@ export const CustomThemeSelector: React.FC<Props> = observer(({ preLoadedData })
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-5 flex justify-end gap-2">
|
<div className="mt-5 flex justify-end gap-2">
|
||||||
<PrimaryButton type="submit" loading={isSubmitting}>
|
<Button variant="primary" type="submit" loading={isSubmitting}>
|
||||||
{isSubmitting ? "Creating Theme..." : "Set Theme"}
|
{isSubmitting ? "Creating Theme..." : "Set Theme"}
|
||||||
</PrimaryButton>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
@ -24,7 +24,7 @@ export const ThemeSwitch: React.FC<Props> = observer(
|
|||||||
({ setPreLoadedData, customThemeSelectorOptions, setCustomThemeSelectorOptions }) => {
|
({ setPreLoadedData, customThemeSelectorOptions, setCustomThemeSelectorOptions }) => {
|
||||||
const store: any = useMobxStore();
|
const store: any = useMobxStore();
|
||||||
|
|
||||||
const { user, mutateUser } = useUser();
|
const { user } = useUser();
|
||||||
const { theme, setTheme } = useTheme();
|
const { theme, setTheme } = useTheme();
|
||||||
|
|
||||||
const updateUserTheme = (newTheme: string) => {
|
const updateUserTheme = (newTheme: string) => {
|
||||||
@ -72,19 +72,15 @@ export const ThemeSwitch: React.FC<Props> = observer(
|
|||||||
}
|
}
|
||||||
onChange={({ value, type }: { value: string; type: string }) => {
|
onChange={({ value, type }: { value: string; type: string }) => {
|
||||||
if (value === "custom") {
|
if (value === "custom") {
|
||||||
if (user?.theme.palette) {
|
if (user?.theme?.palette) {
|
||||||
setPreLoadedData({
|
setPreLoadedData({
|
||||||
background: user.theme.background !== "" ? user.theme.background : "#0d101b",
|
background: user.theme?.background !== "" ? user.theme.background : "#0d101b",
|
||||||
text: user.theme.text !== "" ? user.theme.text : "#c5c5c5",
|
text: user.theme.text !== "" ? user.theme.text : "#c5c5c5",
|
||||||
primary: user.theme.primary !== "" ? user.theme.primary : "#3f76ff",
|
primary: user.theme.primary !== "" ? user.theme.primary : "#3f76ff",
|
||||||
sidebarBackground:
|
sidebarBackground: user.theme.sidebarBackground !== "" ? user.theme.sidebarBackground : "#0d101b",
|
||||||
user.theme.sidebarBackground !== "" ? user.theme.sidebarBackground : "#0d101b",
|
|
||||||
sidebarText: user.theme.sidebarText !== "" ? user.theme.sidebarText : "#c5c5c5",
|
sidebarText: user.theme.sidebarText !== "" ? user.theme.sidebarText : "#c5c5c5",
|
||||||
darkPalette: false,
|
darkPalette: false,
|
||||||
palette:
|
palette: user.theme.palette !== ",,,," ? user.theme.palette : "#0d101b,#c5c5c5,#3f76ff,#0d101b,#c5c5c5",
|
||||||
user.theme.palette !== ",,,,"
|
|
||||||
? user.theme.palette
|
|
||||||
: "#0d101b,#c5c5c5,#3f76ff,#0d101b,#c5c5c5",
|
|
||||||
theme: "custom",
|
theme: "custom",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,229 +1,220 @@
|
|||||||
import React, { useCallback, useState } from "react";
|
// import React, { useCallback, useState } from "react";
|
||||||
|
// import { useRouter } from "next/router";
|
||||||
|
// import useSWR from "swr";
|
||||||
|
// import { DragDropContext, DropResult } from "react-beautiful-dnd";
|
||||||
|
// // services
|
||||||
|
// import { ProjectStateService } from "services/project";
|
||||||
|
// // hooks
|
||||||
|
// import useUser from "hooks/use-user";
|
||||||
|
// import { useProjectMyMembership } from "contexts/project-member.context";
|
||||||
|
// import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view";
|
||||||
|
// // components
|
||||||
|
// import StrictModeDroppable from "components/dnd/StrictModeDroppable";
|
||||||
|
// import { AllLists, AllBoards, CalendarView, SpreadsheetView, GanttChartView } from "components/core";
|
||||||
|
// import { EmptyState } from "components/common";
|
||||||
|
// // ui
|
||||||
|
// import { Spinner } from "components/ui";
|
||||||
|
// // icons
|
||||||
|
// import { TrashIcon } from "@heroicons/react/24/outline";
|
||||||
|
// // images
|
||||||
|
// import emptyIssue from "public/empty-state/issue.svg";
|
||||||
|
// import emptyIssueArchive from "public/empty-state/issue-archive.svg";
|
||||||
|
// // helpers
|
||||||
|
// import { getStatesList } from "helpers/state.helper";
|
||||||
|
// // types
|
||||||
|
// import { IIssue, IIssueViewProps } from "types";
|
||||||
|
// // fetch-keys
|
||||||
|
// import { STATES_LIST } from "constants/fetch-keys";
|
||||||
|
|
||||||
import { useRouter } from "next/router";
|
// type Props = {
|
||||||
|
// addIssueToDate: (date: string) => void;
|
||||||
|
// addIssueToGroup: (groupTitle: string) => void;
|
||||||
|
// disableUserActions: boolean;
|
||||||
|
// dragDisabled?: boolean;
|
||||||
|
// emptyState: {
|
||||||
|
// title: string;
|
||||||
|
// description?: string;
|
||||||
|
// primaryButton?: {
|
||||||
|
// icon: any;
|
||||||
|
// text: string;
|
||||||
|
// onClick: () => void;
|
||||||
|
// };
|
||||||
|
// secondaryButton?: React.ReactNode;
|
||||||
|
// };
|
||||||
|
// handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
|
||||||
|
// handleDraftIssueAction?: (issue: IIssue, action: "edit" | "delete") => void;
|
||||||
|
// handleOnDragEnd: (result: DropResult) => Promise<void>;
|
||||||
|
// openIssuesListModal: (() => void) | null;
|
||||||
|
// removeIssue: ((bridgeId: string, issueId: string) => void) | null;
|
||||||
|
// disableAddIssueOption?: boolean;
|
||||||
|
// trashBox: boolean;
|
||||||
|
// setTrashBox: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
// viewProps: IIssueViewProps;
|
||||||
|
// };
|
||||||
|
|
||||||
import useSWR from "swr";
|
// const projectStateService = new ProjectStateService();
|
||||||
|
|
||||||
// react-beautiful-dnd
|
// export const AllViews: React.FC<Props> = ({
|
||||||
import { DragDropContext, DropResult } from "react-beautiful-dnd";
|
// addIssueToDate,
|
||||||
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
|
// addIssueToGroup,
|
||||||
// services
|
// disableUserActions,
|
||||||
import stateService from "services/state.service";
|
// dragDisabled = false,
|
||||||
// hooks
|
// emptyState,
|
||||||
import useUser from "hooks/use-user";
|
// handleIssueAction,
|
||||||
import { useProjectMyMembership } from "contexts/project-member.context";
|
// handleDraftIssueAction,
|
||||||
import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view";
|
// handleOnDragEnd,
|
||||||
// components
|
// openIssuesListModal,
|
||||||
import {
|
// removeIssue,
|
||||||
AllLists,
|
// disableAddIssueOption = false,
|
||||||
AllBoards,
|
// trashBox,
|
||||||
CalendarView,
|
// setTrashBox,
|
||||||
SpreadsheetView,
|
// viewProps,
|
||||||
GanttChartView,
|
// }) => {
|
||||||
} from "components/core";
|
// const router = useRouter();
|
||||||
// ui
|
// const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
||||||
import { EmptyState, Spinner } from "components/ui";
|
|
||||||
// icons
|
|
||||||
import { TrashIcon } from "@heroicons/react/24/outline";
|
|
||||||
// images
|
|
||||||
import emptyIssue from "public/empty-state/issue.svg";
|
|
||||||
import emptyIssueArchive from "public/empty-state/issue-archive.svg";
|
|
||||||
// helpers
|
|
||||||
import { getStatesList } from "helpers/state.helper";
|
|
||||||
// types
|
|
||||||
import { IIssue, IIssueViewProps } from "types";
|
|
||||||
// fetch-keys
|
|
||||||
import { STATES_LIST } from "constants/fetch-keys";
|
|
||||||
|
|
||||||
type Props = {
|
// const [myIssueProjectId, setMyIssueProjectId] = useState<string | null>(null);
|
||||||
addIssueToDate: (date: string) => void;
|
|
||||||
addIssueToGroup: (groupTitle: string) => void;
|
|
||||||
disableUserActions: boolean;
|
|
||||||
dragDisabled?: boolean;
|
|
||||||
emptyState: {
|
|
||||||
title: string;
|
|
||||||
description?: string;
|
|
||||||
primaryButton?: {
|
|
||||||
icon: any;
|
|
||||||
text: string;
|
|
||||||
onClick: () => void;
|
|
||||||
};
|
|
||||||
secondaryButton?: React.ReactNode;
|
|
||||||
};
|
|
||||||
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
|
|
||||||
handleDraftIssueAction?: (issue: IIssue, action: "edit" | "delete") => void;
|
|
||||||
handleOnDragEnd: (result: DropResult) => Promise<void>;
|
|
||||||
openIssuesListModal: (() => void) | null;
|
|
||||||
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
|
|
||||||
disableAddIssueOption?: boolean;
|
|
||||||
trashBox: boolean;
|
|
||||||
setTrashBox: React.Dispatch<React.SetStateAction<boolean>>;
|
|
||||||
viewProps: IIssueViewProps;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const AllViews: React.FC<Props> = ({
|
// const { user } = useUser();
|
||||||
addIssueToDate,
|
// const { memberRole } = useProjectMyMembership();
|
||||||
addIssueToGroup,
|
|
||||||
disableUserActions,
|
|
||||||
dragDisabled = false,
|
|
||||||
emptyState,
|
|
||||||
handleIssueAction,
|
|
||||||
handleDraftIssueAction,
|
|
||||||
handleOnDragEnd,
|
|
||||||
openIssuesListModal,
|
|
||||||
removeIssue,
|
|
||||||
disableAddIssueOption = false,
|
|
||||||
trashBox,
|
|
||||||
setTrashBox,
|
|
||||||
viewProps,
|
|
||||||
}) => {
|
|
||||||
const router = useRouter();
|
|
||||||
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
|
||||||
|
|
||||||
const [myIssueProjectId, setMyIssueProjectId] = useState<string | null>(null);
|
// const { groupedIssues, isEmpty, displayFilters } = viewProps;
|
||||||
|
|
||||||
const { user } = useUser();
|
// const { spreadsheetIssues, mutateIssues } = useSpreadsheetIssuesView();
|
||||||
const { memberRole } = useProjectMyMembership();
|
|
||||||
|
|
||||||
const { groupedIssues, isEmpty, displayFilters } = viewProps;
|
// const { data: stateGroups } = useSWR(
|
||||||
|
// workspaceSlug && projectId ? STATES_LIST(projectId as string) : null,
|
||||||
|
// workspaceSlug ? () => projectStateService.getStates(workspaceSlug as string, projectId as string) : null
|
||||||
|
// );
|
||||||
|
// const states = getStatesList(stateGroups);
|
||||||
|
|
||||||
const { spreadsheetIssues, mutateIssues } = useSpreadsheetIssuesView();
|
// const handleMyIssueOpen = (issue: IIssue) => {
|
||||||
|
// setMyIssueProjectId(issue.project);
|
||||||
|
// };
|
||||||
|
|
||||||
const { data: stateGroups } = useSWR(
|
// const handleTrashBox = useCallback(
|
||||||
workspaceSlug && projectId ? STATES_LIST(projectId as string) : null,
|
// (isDragging: boolean) => {
|
||||||
workspaceSlug
|
// if (isDragging && !trashBox) setTrashBox(true);
|
||||||
? () => stateService.getStates(workspaceSlug as string, projectId as string)
|
// },
|
||||||
: null
|
// [trashBox, setTrashBox]
|
||||||
);
|
// );
|
||||||
const states = getStatesList(stateGroups);
|
|
||||||
|
|
||||||
const handleMyIssueOpen = (issue: IIssue) => {
|
// return (
|
||||||
setMyIssueProjectId(issue.project);
|
// <DragDropContext onDragEnd={handleOnDragEnd}>
|
||||||
};
|
// <StrictModeDroppable droppableId="trashBox">
|
||||||
|
// {(provided, snapshot) => (
|
||||||
|
// <div
|
||||||
|
// className={`${
|
||||||
|
// trashBox ? "pointer-events-auto opacity-100" : "pointer-events-none opacity-0"
|
||||||
|
// } fixed top-4 left-1/2 -translate-x-1/2 z-40 w-72 flex items-center justify-center gap-2 rounded border-2 border-red-500/20 bg-custom-background-100 px-3 py-5 text-xs font-medium italic text-red-500 ${
|
||||||
|
// snapshot.isDraggingOver ? "bg-red-500 blur-2xl opacity-70" : ""
|
||||||
|
// } transition duration-300`}
|
||||||
|
// ref={provided.innerRef}
|
||||||
|
// {...provided.droppableProps}
|
||||||
|
// >
|
||||||
|
// <TrashIcon className="h-4 w-4" />
|
||||||
|
// Drop here to delete the issue.
|
||||||
|
// </div>
|
||||||
|
// )}
|
||||||
|
// </StrictModeDroppable>
|
||||||
|
// {groupedIssues ? (
|
||||||
|
// !isEmpty ||
|
||||||
|
// displayFilters?.layout === "kanban" ||
|
||||||
|
// displayFilters?.layout === "calendar" ||
|
||||||
|
// displayFilters?.layout === "gantt_chart" ? (
|
||||||
|
// <>
|
||||||
|
// {displayFilters?.layout === "list" ? (
|
||||||
|
// <AllLists
|
||||||
|
// states={states}
|
||||||
|
// addIssueToGroup={addIssueToGroup}
|
||||||
|
// handleIssueAction={handleIssueAction}
|
||||||
|
// handleDraftIssueAction={handleDraftIssueAction}
|
||||||
|
// openIssuesListModal={cycleId || moduleId ? openIssuesListModal : null}
|
||||||
|
// removeIssue={removeIssue}
|
||||||
|
// myIssueProjectId={myIssueProjectId}
|
||||||
|
// handleMyIssueOpen={handleMyIssueOpen}
|
||||||
|
// disableUserActions={disableUserActions}
|
||||||
|
// disableAddIssueOption={disableAddIssueOption}
|
||||||
|
// user={user}
|
||||||
|
// userAuth={memberRole}
|
||||||
|
// viewProps={viewProps}
|
||||||
|
// />
|
||||||
|
// ) : displayFilters?.layout === "kanban" ? (
|
||||||
|
// <AllBoards
|
||||||
|
// addIssueToGroup={addIssueToGroup}
|
||||||
|
// disableUserActions={disableUserActions}
|
||||||
|
// disableAddIssueOption={disableAddIssueOption}
|
||||||
|
// dragDisabled={dragDisabled}
|
||||||
|
// handleIssueAction={handleIssueAction}
|
||||||
|
// handleDraftIssueAction={handleDraftIssueAction}
|
||||||
|
// handleTrashBox={handleTrashBox}
|
||||||
|
// openIssuesListModal={cycleId || moduleId ? openIssuesListModal : null}
|
||||||
|
// myIssueProjectId={myIssueProjectId}
|
||||||
|
// handleMyIssueOpen={handleMyIssueOpen}
|
||||||
|
// removeIssue={removeIssue}
|
||||||
|
// states={states}
|
||||||
|
// user={user}
|
||||||
|
// userAuth={memberRole}
|
||||||
|
// viewProps={viewProps}
|
||||||
|
// />
|
||||||
|
// ) : displayFilters?.layout === "calendar" ? (
|
||||||
|
// <CalendarView
|
||||||
|
// handleIssueAction={handleIssueAction}
|
||||||
|
// addIssueToDate={addIssueToDate}
|
||||||
|
// disableUserActions={disableUserActions}
|
||||||
|
// user={user}
|
||||||
|
// userAuth={memberRole}
|
||||||
|
// />
|
||||||
|
// ) : displayFilters?.layout === "spreadsheet" ? (
|
||||||
|
// <SpreadsheetView
|
||||||
|
// handleIssueAction={handleIssueAction}
|
||||||
|
// spreadsheetIssues={spreadsheetIssues}
|
||||||
|
// mutateIssues={mutateIssues}
|
||||||
|
// openIssuesListModal={cycleId || moduleId ? openIssuesListModal : null}
|
||||||
|
// disableUserActions={disableUserActions}
|
||||||
|
// user={user}
|
||||||
|
// userAuth={memberRole}
|
||||||
|
// />
|
||||||
|
// ) : (
|
||||||
|
// displayFilters?.layout === "gantt_chart" && <GanttChartView disableUserActions={disableUserActions} />
|
||||||
|
// )}
|
||||||
|
// </>
|
||||||
|
// ) : router.pathname.includes("archived-issues") ? (
|
||||||
|
// <EmptyState
|
||||||
|
// title="Archived Issues will be shown here"
|
||||||
|
// description="All the issues that have been in the completed or canceled groups for the configured period of time can be viewed here."
|
||||||
|
// image={emptyIssueArchive}
|
||||||
|
// primaryButton={{
|
||||||
|
// text: "Go to Automation Settings",
|
||||||
|
// onClick: () => {
|
||||||
|
// router.push(`/${workspaceSlug}/projects/${projectId}/settings/automations`);
|
||||||
|
// },
|
||||||
|
// }}
|
||||||
|
// />
|
||||||
|
// ) : (
|
||||||
|
// <EmptyState
|
||||||
|
// title={emptyState.title}
|
||||||
|
// description={emptyState.description}
|
||||||
|
// image={emptyIssue}
|
||||||
|
// primaryButton={
|
||||||
|
// emptyState.primaryButton
|
||||||
|
// ? {
|
||||||
|
// icon: emptyState.primaryButton.icon,
|
||||||
|
// text: emptyState.primaryButton.text,
|
||||||
|
// onClick: emptyState.primaryButton.onClick,
|
||||||
|
// }
|
||||||
|
// : undefined
|
||||||
|
// }
|
||||||
|
// secondaryButton={emptyState.secondaryButton}
|
||||||
|
// />
|
||||||
|
// )
|
||||||
|
// ) : (
|
||||||
|
// <div className="flex h-full w-full items-center justify-center">
|
||||||
|
// <Spinner />
|
||||||
|
// </div>
|
||||||
|
// )}
|
||||||
|
// </DragDropContext>
|
||||||
|
// );
|
||||||
|
// };
|
||||||
|
|
||||||
const handleTrashBox = useCallback(
|
export const AllViews = () => <></>;
|
||||||
(isDragging: boolean) => {
|
|
||||||
if (isDragging && !trashBox) setTrashBox(true);
|
|
||||||
},
|
|
||||||
[trashBox, setTrashBox]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DragDropContext onDragEnd={handleOnDragEnd}>
|
|
||||||
<StrictModeDroppable droppableId="trashBox">
|
|
||||||
{(provided, snapshot) => (
|
|
||||||
<div
|
|
||||||
className={`${
|
|
||||||
trashBox ? "pointer-events-auto opacity-100" : "pointer-events-none opacity-0"
|
|
||||||
} fixed top-4 left-1/2 -translate-x-1/2 z-40 w-72 flex items-center justify-center gap-2 rounded border-2 border-red-500/20 bg-custom-background-100 px-3 py-5 text-xs font-medium italic text-red-500 ${
|
|
||||||
snapshot.isDraggingOver ? "bg-red-500 blur-2xl opacity-70" : ""
|
|
||||||
} transition duration-300`}
|
|
||||||
ref={provided.innerRef}
|
|
||||||
{...provided.droppableProps}
|
|
||||||
>
|
|
||||||
<TrashIcon className="h-4 w-4" />
|
|
||||||
Drop here to delete the issue.
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</StrictModeDroppable>
|
|
||||||
{groupedIssues ? (
|
|
||||||
!isEmpty ||
|
|
||||||
displayFilters?.layout === "kanban" ||
|
|
||||||
displayFilters?.layout === "calendar" ||
|
|
||||||
displayFilters?.layout === "gantt_chart" ? (
|
|
||||||
<>
|
|
||||||
{displayFilters?.layout === "list" ? (
|
|
||||||
<AllLists
|
|
||||||
states={states}
|
|
||||||
addIssueToGroup={addIssueToGroup}
|
|
||||||
handleIssueAction={handleIssueAction}
|
|
||||||
handleDraftIssueAction={handleDraftIssueAction}
|
|
||||||
openIssuesListModal={cycleId || moduleId ? openIssuesListModal : null}
|
|
||||||
removeIssue={removeIssue}
|
|
||||||
myIssueProjectId={myIssueProjectId}
|
|
||||||
handleMyIssueOpen={handleMyIssueOpen}
|
|
||||||
disableUserActions={disableUserActions}
|
|
||||||
disableAddIssueOption={disableAddIssueOption}
|
|
||||||
user={user}
|
|
||||||
userAuth={memberRole}
|
|
||||||
viewProps={viewProps}
|
|
||||||
/>
|
|
||||||
) : displayFilters?.layout === "kanban" ? (
|
|
||||||
<AllBoards
|
|
||||||
addIssueToGroup={addIssueToGroup}
|
|
||||||
disableUserActions={disableUserActions}
|
|
||||||
disableAddIssueOption={disableAddIssueOption}
|
|
||||||
dragDisabled={dragDisabled}
|
|
||||||
handleIssueAction={handleIssueAction}
|
|
||||||
handleDraftIssueAction={handleDraftIssueAction}
|
|
||||||
handleTrashBox={handleTrashBox}
|
|
||||||
openIssuesListModal={cycleId || moduleId ? openIssuesListModal : null}
|
|
||||||
myIssueProjectId={myIssueProjectId}
|
|
||||||
handleMyIssueOpen={handleMyIssueOpen}
|
|
||||||
removeIssue={removeIssue}
|
|
||||||
states={states}
|
|
||||||
user={user}
|
|
||||||
userAuth={memberRole}
|
|
||||||
viewProps={viewProps}
|
|
||||||
/>
|
|
||||||
) : displayFilters?.layout === "calendar" ? (
|
|
||||||
<CalendarView
|
|
||||||
handleIssueAction={handleIssueAction}
|
|
||||||
addIssueToDate={addIssueToDate}
|
|
||||||
disableUserActions={disableUserActions}
|
|
||||||
user={user}
|
|
||||||
userAuth={memberRole}
|
|
||||||
/>
|
|
||||||
) : displayFilters?.layout === "spreadsheet" ? (
|
|
||||||
<SpreadsheetView
|
|
||||||
handleIssueAction={handleIssueAction}
|
|
||||||
spreadsheetIssues={spreadsheetIssues}
|
|
||||||
mutateIssues={mutateIssues}
|
|
||||||
openIssuesListModal={cycleId || moduleId ? openIssuesListModal : null}
|
|
||||||
disableUserActions={disableUserActions}
|
|
||||||
user={user}
|
|
||||||
userAuth={memberRole}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
displayFilters?.layout === "gantt_chart" && (
|
|
||||||
<GanttChartView disableUserActions={disableUserActions} />
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
) : router.pathname.includes("archived-issues") ? (
|
|
||||||
<EmptyState
|
|
||||||
title="Archived Issues will be shown here"
|
|
||||||
description="All the issues that have been in the completed or canceled groups for the configured period of time can be viewed here."
|
|
||||||
image={emptyIssueArchive}
|
|
||||||
primaryButton={{
|
|
||||||
text: "Go to Automation Settings",
|
|
||||||
onClick: () => {
|
|
||||||
router.push(`/${workspaceSlug}/projects/${projectId}/settings/automations`);
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<EmptyState
|
|
||||||
title={emptyState.title}
|
|
||||||
description={emptyState.description}
|
|
||||||
image={emptyIssue}
|
|
||||||
primaryButton={
|
|
||||||
emptyState.primaryButton
|
|
||||||
? {
|
|
||||||
icon: emptyState.primaryButton.icon,
|
|
||||||
text: emptyState.primaryButton.text,
|
|
||||||
onClick: emptyState.primaryButton.onClick,
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
secondaryButton={emptyState.secondaryButton}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<div className="flex h-full w-full items-center justify-center">
|
|
||||||
<Spinner />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</DragDropContext>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
@ -1,152 +0,0 @@
|
|||||||
import { useRouter } from "next/router";
|
|
||||||
|
|
||||||
//hook
|
|
||||||
import useMyIssues from "hooks/my-issues/use-my-issues";
|
|
||||||
import useIssuesView from "hooks/use-issues-view";
|
|
||||||
import useProfileIssues from "hooks/use-profile-issues";
|
|
||||||
// components
|
|
||||||
import { SingleBoard } from "components/core/views/board-view/single-board";
|
|
||||||
import { IssuePeekOverview } from "components/issues";
|
|
||||||
// icons
|
|
||||||
import { StateGroupIcon } from "components/icons";
|
|
||||||
// helpers
|
|
||||||
import { addSpaceIfCamelCase } from "helpers/string.helper";
|
|
||||||
// types
|
|
||||||
import { ICurrentUserResponse, IIssue, IIssueViewProps, IState, UserAuth } from "types";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
addIssueToGroup: (groupTitle: string) => void;
|
|
||||||
disableUserActions: boolean;
|
|
||||||
disableAddIssueOption?: boolean;
|
|
||||||
dragDisabled: boolean;
|
|
||||||
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
|
|
||||||
handleDraftIssueAction?: (issue: IIssue, action: "edit" | "delete") => void;
|
|
||||||
handleTrashBox: (isDragging: boolean) => void;
|
|
||||||
openIssuesListModal?: (() => void) | null;
|
|
||||||
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
|
|
||||||
myIssueProjectId?: string | null;
|
|
||||||
handleMyIssueOpen?: (issue: IIssue) => void;
|
|
||||||
states: IState[] | undefined;
|
|
||||||
user: ICurrentUserResponse | undefined;
|
|
||||||
userAuth: UserAuth;
|
|
||||||
viewProps: IIssueViewProps;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const AllBoards: React.FC<Props> = ({
|
|
||||||
addIssueToGroup,
|
|
||||||
disableUserActions,
|
|
||||||
disableAddIssueOption = false,
|
|
||||||
dragDisabled,
|
|
||||||
handleIssueAction,
|
|
||||||
handleDraftIssueAction,
|
|
||||||
handleTrashBox,
|
|
||||||
openIssuesListModal,
|
|
||||||
myIssueProjectId,
|
|
||||||
handleMyIssueOpen,
|
|
||||||
removeIssue,
|
|
||||||
states,
|
|
||||||
user,
|
|
||||||
userAuth,
|
|
||||||
viewProps,
|
|
||||||
}) => {
|
|
||||||
const router = useRouter();
|
|
||||||
const { workspaceSlug, projectId, userId } = router.query;
|
|
||||||
|
|
||||||
const isProfileIssue =
|
|
||||||
router.pathname.includes("assigned") ||
|
|
||||||
router.pathname.includes("created") ||
|
|
||||||
router.pathname.includes("subscribed");
|
|
||||||
|
|
||||||
const isMyIssue = router.pathname.includes("my-issues");
|
|
||||||
|
|
||||||
const { mutateIssues } = useIssuesView();
|
|
||||||
const { mutateMyIssues } = useMyIssues(workspaceSlug?.toString());
|
|
||||||
const { mutateProfileIssues } = useProfileIssues(workspaceSlug?.toString(), userId?.toString());
|
|
||||||
|
|
||||||
const { displayFilters, groupedIssues } = viewProps;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<IssuePeekOverview
|
|
||||||
handleMutation={() =>
|
|
||||||
isMyIssue ? mutateMyIssues() : isProfileIssue ? mutateProfileIssues() : mutateIssues()
|
|
||||||
}
|
|
||||||
projectId={myIssueProjectId ? myIssueProjectId : projectId?.toString() ?? ""}
|
|
||||||
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
|
||||||
readOnly={disableUserActions}
|
|
||||||
/>
|
|
||||||
{groupedIssues ? (
|
|
||||||
<div className="horizontal-scroll-enable flex h-full gap-x-4 p-8 bg-custom-background-90">
|
|
||||||
{Object.keys(groupedIssues).map((singleGroup, index) => {
|
|
||||||
const currentState =
|
|
||||||
displayFilters?.group_by === "state"
|
|
||||||
? states?.find((s) => s.id === singleGroup)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
if (!displayFilters?.show_empty_groups && groupedIssues[singleGroup].length === 0)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SingleBoard
|
|
||||||
key={index}
|
|
||||||
addIssueToGroup={() => addIssueToGroup(singleGroup)}
|
|
||||||
currentState={currentState}
|
|
||||||
disableUserActions={disableUserActions}
|
|
||||||
disableAddIssueOption={disableAddIssueOption}
|
|
||||||
dragDisabled={dragDisabled}
|
|
||||||
groupTitle={singleGroup}
|
|
||||||
handleIssueAction={handleIssueAction}
|
|
||||||
handleDraftIssueAction={handleDraftIssueAction}
|
|
||||||
handleTrashBox={handleTrashBox}
|
|
||||||
openIssuesListModal={openIssuesListModal ?? null}
|
|
||||||
handleMyIssueOpen={handleMyIssueOpen}
|
|
||||||
removeIssue={removeIssue}
|
|
||||||
user={user}
|
|
||||||
userAuth={userAuth}
|
|
||||||
viewProps={viewProps}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
{!displayFilters?.show_empty_groups && (
|
|
||||||
<div className="h-full w-96 flex-shrink-0 space-y-2 p-1">
|
|
||||||
<h2 className="text-lg font-semibold">Hidden groups</h2>
|
|
||||||
<div className="space-y-3">
|
|
||||||
{Object.keys(groupedIssues).map((singleGroup, index) => {
|
|
||||||
const currentState =
|
|
||||||
displayFilters?.group_by === "state"
|
|
||||||
? states?.find((s) => s.id === singleGroup)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
if (groupedIssues[singleGroup].length === 0)
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className="flex items-center justify-between gap-2 rounded bg-custom-background-90 p-2 shadow"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{currentState && (
|
|
||||||
<StateGroupIcon
|
|
||||||
stateGroup={currentState.group}
|
|
||||||
color={currentState.color}
|
|
||||||
height="16px"
|
|
||||||
width="16px"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<h4 className="text-sm capitalize">
|
|
||||||
{displayFilters?.group_by === "state"
|
|
||||||
? addSpaceIfCamelCase(currentState?.name ?? "")
|
|
||||||
: addSpaceIfCamelCase(singleGroup)}
|
|
||||||
</h4>
|
|
||||||
</div>
|
|
||||||
<span className="text-xs text-custom-text-200">0</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,231 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
|
|
||||||
import useSWR from "swr";
|
|
||||||
|
|
||||||
// services
|
|
||||||
import issuesService from "services/issues.service";
|
|
||||||
import projectService from "services/project.service";
|
|
||||||
// hooks
|
|
||||||
import useProjects from "hooks/use-projects";
|
|
||||||
// component
|
|
||||||
import { Avatar, Icon } from "components/ui";
|
|
||||||
// icons
|
|
||||||
import { PlusIcon } from "@heroicons/react/24/outline";
|
|
||||||
import { PriorityIcon, StateGroupIcon } from "components/icons";
|
|
||||||
// helpers
|
|
||||||
import { addSpaceIfCamelCase } from "helpers/string.helper";
|
|
||||||
import { renderEmoji } from "helpers/emoji.helper";
|
|
||||||
// types
|
|
||||||
import { IIssueViewProps, IState, TIssuePriorities, TStateGroups } from "types";
|
|
||||||
// fetch-keys
|
|
||||||
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS, WORKSPACE_LABELS } from "constants/fetch-keys";
|
|
||||||
// constants
|
|
||||||
import { STATE_GROUP_COLORS } from "constants/state";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
currentState?: IState | null;
|
|
||||||
groupTitle: string;
|
|
||||||
addIssueToGroup: () => void;
|
|
||||||
isCollapsed: boolean;
|
|
||||||
setIsCollapsed: React.Dispatch<React.SetStateAction<boolean>>;
|
|
||||||
disableUserActions: boolean;
|
|
||||||
disableAddIssue: boolean;
|
|
||||||
viewProps: IIssueViewProps;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const BoardHeader: React.FC<Props> = ({
|
|
||||||
currentState,
|
|
||||||
groupTitle,
|
|
||||||
addIssueToGroup,
|
|
||||||
isCollapsed,
|
|
||||||
setIsCollapsed,
|
|
||||||
disableUserActions,
|
|
||||||
disableAddIssue,
|
|
||||||
viewProps,
|
|
||||||
}) => {
|
|
||||||
const router = useRouter();
|
|
||||||
const { workspaceSlug, projectId } = router.query;
|
|
||||||
|
|
||||||
const { displayFilters, groupedIssues } = viewProps;
|
|
||||||
|
|
||||||
const { data: issueLabels } = useSWR(
|
|
||||||
workspaceSlug && projectId && displayFilters?.group_by === "labels"
|
|
||||||
? PROJECT_ISSUE_LABELS(projectId.toString())
|
|
||||||
: null,
|
|
||||||
workspaceSlug && projectId && displayFilters?.group_by === "labels"
|
|
||||||
? () => issuesService.getIssueLabels(workspaceSlug.toString(), projectId.toString())
|
|
||||||
: null
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data: workspaceLabels } = useSWR(
|
|
||||||
workspaceSlug && displayFilters?.group_by === "labels"
|
|
||||||
? WORKSPACE_LABELS(workspaceSlug.toString())
|
|
||||||
: null,
|
|
||||||
workspaceSlug && displayFilters?.group_by === "labels"
|
|
||||||
? () => issuesService.getWorkspaceLabels(workspaceSlug.toString())
|
|
||||||
: null
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data: members } = useSWR(
|
|
||||||
workspaceSlug &&
|
|
||||||
projectId &&
|
|
||||||
(displayFilters?.group_by === "created_by" || displayFilters?.group_by === "assignees")
|
|
||||||
? PROJECT_MEMBERS(projectId.toString())
|
|
||||||
: null,
|
|
||||||
workspaceSlug &&
|
|
||||||
projectId &&
|
|
||||||
(displayFilters?.group_by === "created_by" || displayFilters?.group_by === "assignees")
|
|
||||||
? () => projectService.projectMembers(workspaceSlug.toString(), projectId.toString())
|
|
||||||
: null
|
|
||||||
);
|
|
||||||
|
|
||||||
const { projects } = useProjects();
|
|
||||||
|
|
||||||
const getGroupTitle = () => {
|
|
||||||
let title = addSpaceIfCamelCase(groupTitle);
|
|
||||||
|
|
||||||
switch (displayFilters?.group_by) {
|
|
||||||
case "state":
|
|
||||||
title = addSpaceIfCamelCase(currentState?.name ?? "");
|
|
||||||
break;
|
|
||||||
case "labels":
|
|
||||||
title =
|
|
||||||
[...(issueLabels ?? []), ...(workspaceLabels ?? [])]?.find(
|
|
||||||
(label) => label.id === groupTitle
|
|
||||||
)?.name ?? "None";
|
|
||||||
break;
|
|
||||||
case "project":
|
|
||||||
title = projects?.find((p) => p.id === groupTitle)?.name ?? "None";
|
|
||||||
break;
|
|
||||||
case "assignees":
|
|
||||||
case "created_by":
|
|
||||||
const member = members?.find((member) => member.member.id === groupTitle)?.member;
|
|
||||||
title = member ? member.display_name : "None";
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return title;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getGroupIcon = () => {
|
|
||||||
let icon;
|
|
||||||
|
|
||||||
switch (displayFilters?.group_by) {
|
|
||||||
case "state":
|
|
||||||
icon = currentState && (
|
|
||||||
<StateGroupIcon
|
|
||||||
stateGroup={currentState.group}
|
|
||||||
color={currentState.color}
|
|
||||||
height="16px"
|
|
||||||
width="16px"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case "state_detail.group":
|
|
||||||
icon = (
|
|
||||||
<StateGroupIcon
|
|
||||||
stateGroup={groupTitle as TStateGroups}
|
|
||||||
color={STATE_GROUP_COLORS[groupTitle as TStateGroups]}
|
|
||||||
height="16px"
|
|
||||||
width="16px"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case "priority":
|
|
||||||
icon = <PriorityIcon priority={groupTitle as TIssuePriorities} className="text-lg" />;
|
|
||||||
break;
|
|
||||||
case "project":
|
|
||||||
const project = projects?.find((p) => p.id === groupTitle);
|
|
||||||
icon =
|
|
||||||
project &&
|
|
||||||
(project.emoji !== null
|
|
||||||
? renderEmoji(project.emoji)
|
|
||||||
: project.icon_prop !== null
|
|
||||||
? renderEmoji(project.icon_prop)
|
|
||||||
: null);
|
|
||||||
break;
|
|
||||||
case "labels":
|
|
||||||
const labelColor =
|
|
||||||
[...(issueLabels ?? []), ...(workspaceLabels ?? [])]?.find(
|
|
||||||
(label) => label.id === groupTitle
|
|
||||||
)?.color ?? "#000000";
|
|
||||||
icon = (
|
|
||||||
<span
|
|
||||||
className="h-3.5 w-3.5 flex-shrink-0 rounded-full"
|
|
||||||
style={{ backgroundColor: labelColor }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case "assignees":
|
|
||||||
case "created_by":
|
|
||||||
const member = members?.find((member) => member.member.id === groupTitle)?.member;
|
|
||||||
icon = member ? <Avatar user={member} height="24px" width="24px" fontSize="12px" /> : <></>;
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return icon;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`flex items-center justify-between px-1 ${
|
|
||||||
!isCollapsed ? "flex-col rounded-md bg-custom-background-90" : ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className={`flex items-center ${isCollapsed ? "gap-1" : "flex-col gap-2"}`}>
|
|
||||||
<div
|
|
||||||
className={`flex cursor-pointer items-center gap-x-2 max-w-[316px] ${
|
|
||||||
!isCollapsed ? "mb-2 flex-col gap-y-2 py-2" : ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<span className="flex items-center">{getGroupIcon()}</span>
|
|
||||||
<h2
|
|
||||||
className={`text-lg font-semibold truncate ${
|
|
||||||
displayFilters?.group_by === "created_by" ? "" : "capitalize"
|
|
||||||
}`}
|
|
||||||
style={{
|
|
||||||
writingMode: isCollapsed ? "horizontal-tb" : "vertical-rl",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{getGroupTitle()}
|
|
||||||
</h2>
|
|
||||||
<span className={`${isCollapsed ? "ml-0.5" : ""} py-1 text-center text-sm`}>
|
|
||||||
{groupedIssues?.[groupTitle].length ?? 0}
|
|
||||||
</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 text-custom-text-200 outline-none duration-300 hover:bg-custom-background-80"
|
|
||||||
onClick={() => {
|
|
||||||
setIsCollapsed((prevData) => !prevData);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{isCollapsed ? (
|
|
||||||
<Icon
|
|
||||||
iconName="close_fullscreen"
|
|
||||||
className="text-base font-medium text-custom-text-900"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Icon iconName="open_in_full" className="text-base font-medium text-custom-text-900" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
{!disableAddIssue && !disableUserActions && displayFilters?.group_by !== "created_by" && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="grid h-7 w-7 place-items-center rounded p-1 text-custom-text-200 outline-none duration-300 hover:bg-custom-background-80"
|
|
||||||
onClick={addIssueToGroup}
|
|
||||||
>
|
|
||||||
<PlusIcon className="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,5 +0,0 @@
|
|||||||
export * from "./all-boards";
|
|
||||||
export * from "./board-header";
|
|
||||||
export * from "./single-board";
|
|
||||||
export * from "./single-issue";
|
|
||||||
export * from "./inline-create-issue-form";
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user