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:
sriram veeraghanta 2023-10-15 23:50:12 +05:30 committed by GitHub
parent c6e021d41f
commit d80a593520
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
758 changed files with 30452 additions and 23632 deletions

View file

@ -6,7 +6,7 @@ import Image from "next/image";
// layouts
import DefaultLayout from "layouts/default-layout";
// ui
import { SecondaryButton } from "components/ui";
import { Button } from "@plane/ui";
// images
import Image404 from "public/404.svg";
// types
@ -22,13 +22,15 @@ const PageNotFound: NextPage = () => (
<div className="space-y-2">
<h3 className="text-lg font-semibold">Oops! Something went wrong.</h3>
<p className="text-sm text-custom-text-200">
Sorry, the page you are looking for cannot be found. It may have been removed, had its
name changed, or is temporarily unavailable.
Sorry, the page you are looking for cannot be found. It may have been removed, had its name changed, or is
temporarily unavailable.
</p>
</div>
<Link href="/">
<a className="block">
<SecondaryButton size="md">Go to Home</SecondaryButton>
<Button variant="neutral-primary" size="md">
Go to Home
</Button>
</a>
</Link>
</div>

View file

@ -1,83 +1,54 @@
import React, { Fragment, useEffect } from "react";
import { useRouter } from "next/router";
import useSWR from "swr";
// react-hook-form
import { useForm } from "react-hook-form";
// hooks
import useUserAuth from "hooks/use-user-auth";
import useProjects from "hooks/use-projects";
// headless ui
import { observer } from "mobx-react-lite";
import { Tab } from "@headlessui/react";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// services
import analyticsService from "services/analytics.service";
import trackEventServices from "services/track-event.service";
import { TrackEventService } from "services/track_event.service";
// layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
import { AppLayout } from "layouts/app-layout";
// components
import { CustomAnalytics, ScopeAndDemand } from "components/analytics";
// ui
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
import { EmptyState } from "components/ui";
import { WorkspaceAnalyticsHeader } from "components/headers";
import { EmptyState } from "components/common";
// icons
import { PlusIcon } from "@heroicons/react/24/outline";
// images
// assets
import emptyAnalytics from "public/empty-state/analytics.svg";
// types
import { IAnalyticsParams } from "types";
// fetch-keys
import { ANALYTICS } from "constants/fetch-keys";
// constants
import { ANALYTICS_TABS } from "constants/analytics";
const defaultValues: IAnalyticsParams = {
x_axis: "priority",
y_axis: "issue_count",
segment: null,
project: null,
};
const trackEventService = new TrackEventService();
const tabsList = ["Scope and Demand", "Custom Analytics"];
const Analytics = () => {
const AnalyticsPage = observer(() => {
// router
const router = useRouter();
const { workspaceSlug } = router.query;
// store
const { project: projectStore, user: userStore } = useMobxStore();
const { user } = useUserAuth();
const { projects } = useProjects();
const { control, watch, setValue } = useForm<IAnalyticsParams>({ defaultValues });
const params: IAnalyticsParams = {
x_axis: watch("x_axis"),
y_axis: watch("y_axis"),
segment: watch("segment"),
project: watch("project"),
};
const { data: analytics, error: analyticsError } = useSWR(
workspaceSlug ? ANALYTICS(workspaceSlug.toString(), params) : null,
workspaceSlug ? () => analyticsService.getAnalytics(workspaceSlug.toString(), params) : null
);
const user = userStore.currentUser;
const projects = workspaceSlug ? projectStore.projects[workspaceSlug?.toString()] : null;
const trackAnalyticsEvent = (tab: string) => {
if (!user) return;
const eventPayload = {
workspaceSlug: workspaceSlug?.toString(),
};
const eventType =
tab === "Scope and Demand"
? "WORKSPACE_SCOPE_AND_DEMAND_ANALYTICS"
: "WORKSPACE_CUSTOM_ANALYTICS";
tab === "scope_and_demand" ? "WORKSPACE_SCOPE_AND_DEMAND_ANALYTICS" : "WORKSPACE_CUSTOM_ANALYTICS";
trackEventServices.trackAnalyticsEvent(eventPayload, eventType, user);
trackEventService.trackAnalyticsEvent(eventPayload, eventType, user);
};
useEffect(() => {
if (!workspaceSlug) return;
if (user && workspaceSlug)
trackEventServices.trackAnalyticsEvent(
trackEventService.trackAnalyticsEvent(
{ workspaceSlug: workspaceSlug?.toString() },
"WORKSPACE_SCOPE_AND_DEMAND_ANALYTICS",
user
@ -85,29 +56,23 @@ const Analytics = () => {
}, [user, workspaceSlug]);
return (
<WorkspaceAuthorizationLayout
breadcrumbs={
<Breadcrumbs>
<BreadcrumbItem title="Workspace Analytics" />
</Breadcrumbs>
}
>
{projects ? (
projects.length > 0 ? (
<AppLayout header={<WorkspaceAnalyticsHeader />}>
<>
{projects && projects.length > 0 ? (
<div className="h-full flex flex-col overflow-hidden bg-custom-background-100">
<Tab.Group as={Fragment}>
<Tab.List as="div" className="space-x-2 border-b border-custom-border-200 px-5 py-3">
{tabsList.map((tab) => (
{ANALYTICS_TABS.map((tab) => (
<Tab
key={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)}
onClick={() => trackAnalyticsEvent(tab.key)}
>
{tab}
{tab.title}
</Tab>
))}
</Tab.List>
@ -116,39 +81,33 @@ const Analytics = () => {
<ScopeAndDemand fullScreen />
</Tab.Panel>
<Tab.Panel as={Fragment}>
<CustomAnalytics
analytics={analytics}
analyticsError={analyticsError}
params={params}
control={control}
setValue={setValue}
user={user}
fullScreen
/>
<CustomAnalytics fullScreen />
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
</div>
) : (
<EmptyState
title="You can see your all projects' analytics here"
description="Let's create your first project and analyse the stats with various graphs."
image={emptyAnalytics}
primaryButton={{
icon: <PlusIcon className="h-4 w-4" />,
text: "New Project",
onClick: () => {
const e = new KeyboardEvent("keydown", {
key: "p",
});
document.dispatchEvent(e);
},
}}
/>
)
) : null}
</WorkspaceAuthorizationLayout>
<>
<EmptyState
title="You can see your all projects' analytics here"
description="Let's create your first project and analyze the stats with various graphs."
image={emptyAnalytics}
primaryButton={{
icon: <PlusIcon className="h-4 w-4" />,
text: "New Project",
onClick: () => {
const e = new KeyboardEvent("keydown", {
key: "p",
});
document.dispatchEvent(e);
},
}}
/>
</>
)}
</>
</AppLayout>
);
};
});
export default Analytics;
export default AnalyticsPage;

View file

@ -1,193 +0,0 @@
import { RichTextEditor } from "@plane/rich-text-editor";
import type { NextPage } from "next";
import { useCallback, useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import issuesService from "services/issues.service";
import { ICurrentUserResponse, IIssue } from "types";
import useReloadConfirmations from "hooks/use-reload-confirmation";
import { Spinner } from "components/ui";
import Image404 from "public/404.svg";
import DefaultLayout from "layouts/default-layout";
import Image from "next/image";
import userService from "services/user.service";
import { useRouter } from "next/router";
import fileService from "services/file.service";
const Editor: NextPage = () => {
const [user, setUser] = useState<ICurrentUserResponse | undefined>();
const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved");
const [isLoading, setIsLoading] = useState("false");
const { setShowAlert } = useReloadConfirmations();
const [cookies, setCookies] = useState<any>({});
const [issueDetail, setIssueDetail] = useState<IIssue | null>(null);
const router = useRouter();
const { editable } = router.query;
const {
handleSubmit,
watch,
setValue,
control,
formState: { errors },
} = useForm<IIssue>({
defaultValues: {
name: "",
description: "",
description_html: "",
},
});
const getCookies = () => {
const cookies = document.cookie.split(";");
const cookieObj: any = {};
cookies.forEach((cookie) => {
const cookieArr = cookie.split("=");
cookieObj[cookieArr[0].trim()] = cookieArr[1];
});
setCookies(cookieObj);
return cookieObj;
};
const getIssueDetail = async (cookiesData: any) => {
try {
setIsLoading("true");
const userData = await userService.currentUser();
setUser(userData);
const issueDetail = await issuesService.retrieve(
cookiesData.MOBILE_slug,
cookiesData.MOBILE_project_id,
cookiesData.MOBILE_issue_id
);
setIssueDetail(issueDetail);
setIsLoading("false");
setValue("description_html", issueDetail.description_html);
setValue("description", issueDetail.description);
} catch (e) {
setIsLoading("error");
console.log(e);
}
};
useEffect(() => {
const cookiesData = getCookies();
getIssueDetail(cookiesData);
}, []);
useEffect(() => {
if (isSubmitting === "submitted") {
setShowAlert(false);
setTimeout(async () => {
setIsSubmitting("saved");
}, 2000);
} else if (isSubmitting === "submitting") {
setShowAlert(true);
}
}, [isSubmitting, setShowAlert]);
const submitChanges = async (
formData: Partial<IIssue>,
workspaceSlug: string,
projectId: string,
issueId: string
) => {
if (!workspaceSlug || !projectId || !issueId) return;
const payload: Partial<IIssue> = {
...formData,
};
delete payload.issue_relations;
delete payload.related_issues;
await issuesService
.patchIssue(workspaceSlug as string, projectId as string, issueId as string, payload, user)
.catch((e) => {
console.log(e);
});
};
const handleDescriptionFormSubmit = useCallback(
async (formData: Partial<IIssue>) => {
if (!formData) return;
await submitChanges(
{
name: issueDetail?.name ?? "",
description: formData.description ?? "",
description_html: formData.description_html ?? "<p></p>",
},
cookies.MOBILE_slug,
cookies.MOBILE_project_id,
cookies.MOBILE_issue_id
);
},
[submitChanges]
);
return isLoading === "error" ? (
<ErrorEncountered />
) : isLoading === "true" ? (
<div className="grid place-items-center h-screen w-full">
<Spinner />
</div>
) : (
<div className="flex blur-none shadow-none backdrop:backdrop-blur-none justify-center items-center">
<Controller
name="description_html"
control={control}
render={({ field: { value, onChange } }) => (
<RichTextEditor
uploadFile={fileService.getUploadFileFunction(cookies.MOBILE_slug ?? "")}
deleteFile={fileService.deleteImage}
borderOnFocus={false}
value={
!value ||
value === "" ||
(typeof value === "object" && Object.keys(value).length === 0)
? watch("description_html")
: value
}
noBorder={true}
debouncedUpdatesEnabled={true}
setShouldShowAlert={setShowAlert}
setIsSubmitting={setIsSubmitting}
customClassName="min-h-[150px] shadow-sm"
editorContentCustomClassNames="pb-9"
onChange={(description: Object, description_html: string) => {
setShowAlert(true);
setIsSubmitting("submitting");
onChange(description_html);
setValue("description", description);
handleSubmit(handleDescriptionFormSubmit)().finally(() => {
setIsSubmitting("submitted");
});
}}
/>
)}
/>
<div
className={`absolute right-5 bottom-5 text-xs text-custom-text-200 border border-custom-border-400 rounded-xl w-[6.5rem] py-1 z-10 flex items-center justify-center ${
isSubmitting === "saved" ? "fadeOut" : "fadeIn"
}`}
>
{isSubmitting === "submitting" ? "Saving..." : "Saved"}
</div>
</div>
);
};
const ErrorEncountered: NextPage = () => (
<DefaultLayout>
<div className="grid max-h-fit place-items-center p-4">
<div className="space-y-8 text-center">
<div className="relative mx-auto h-40 w-40 lg:h-40 lg:w-40">
<Image src={Image404} layout="fill" alt="404- Page not found" />
</div>
<div className="space-y-2">
<h3 className="text-lg font-semibold">Oops! Something went wrong.</h3>
</div>
</div>
</div>
</DefaultLayout>
);
export default Editor;

View file

@ -1,211 +1,14 @@
import React, { useEffect, useState } from "react";
import { useRouter } from "next/router";
import Image from "next/image";
import Link from "next/link";
import useSWR, { mutate } from "swr";
// next-themes
import { useTheme } from "next-themes";
// layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
// services
import userService from "services/user.service";
// hooks
import useUser from "hooks/use-user";
import useProjects from "hooks/use-projects";
// components
import {
CompletedIssuesGraph,
IssuesList,
IssuesPieChart,
IssuesStats,
} from "components/workspace";
import { TourRoot } from "components/onboarding";
// ui
import { PrimaryButton, ProductUpdatesModal } from "components/ui";
// icons
import { BoltOutlined, GridViewOutlined } from "@mui/icons-material";
// images
import emptyDashboard from "public/empty-state/dashboard.svg";
import githubBlackImage from "/public/logos/github-black.png";
import githubWhiteImage from "/public/logos/github-white.png";
// types
import { ICurrentUserResponse } from "types";
import type { NextPage } from "next";
// fetch-keys
import { CURRENT_USER, USER_WORKSPACE_DASHBOARD } from "constants/fetch-keys";
// layouts
import { AppLayout } from "layouts/app-layout";
// components
import { WorkspaceDashboardView } from "components/views";
import { WorkspaceDashboardHeader } from "components/headers/workspace-dashboard";
const Greeting = ({ user }: { user: ICurrentUserResponse | undefined }) => {
const currentTime = new Date();
const hour = new Intl.DateTimeFormat("en-US", {
hour12: false,
hour: "numeric",
}).format(currentTime);
const date = new Intl.DateTimeFormat("en-US", {
month: "short",
day: "numeric",
}).format(currentTime);
const weekDay = new Intl.DateTimeFormat("en-US", {
weekday: "long",
}).format(currentTime);
const timeString = new Intl.DateTimeFormat("en-US", {
timeZone: user?.user_timezone,
hour12: false, // Use 24-hour format
hour: "2-digit",
minute: "2-digit",
}).format(currentTime);
const greeting =
parseInt(hour, 10) < 12 ? "morning" : parseInt(hour, 10) < 18 ? "afternoon" : "evening";
return (
<div>
<h3 className="text-2xl font-semibold">
Good {greeting}, {user?.first_name} {user?.last_name}
</h3>
<h6 className="text-custom-text-400 font-medium flex items-center gap-2">
<div>{greeting === "morning" ? "🌤️" : greeting === "afternoon" ? "🌥️" : "🌙️"}</div>
<div>
{weekDay}, {date} {timeString}
</div>
</h6>
</div>
);
};
const WorkspacePage: NextPage = () => {
const [month, setMonth] = useState(new Date().getMonth() + 1);
const [isProductUpdatesModalOpen, setIsProductUpdatesModalOpen] = useState(false);
const router = useRouter();
const { workspaceSlug } = router.query;
const { theme } = useTheme();
const { user } = useUser();
const { projects } = useProjects();
const { data: workspaceDashboardData } = useSWR(
workspaceSlug ? USER_WORKSPACE_DASHBOARD(workspaceSlug as string) : null,
workspaceSlug ? () => userService.userWorkspaceDashboard(workspaceSlug as string, month) : null
);
useEffect(() => {
if (!workspaceSlug) return;
mutate(USER_WORKSPACE_DASHBOARD(workspaceSlug as string));
}, [month, workspaceSlug]);
return (
<WorkspaceAuthorizationLayout
left={
<div className="flex items-center gap-2 pl-3">
<GridViewOutlined fontSize="small" />
Dashboard
</div>
}
right={
<div className="flex items-center gap-3 px-3">
<button
onClick={() => setIsProductUpdatesModalOpen(true)}
className="flex items-center gap-1.5 bg-custom-background-80 text-xs font-medium py-1.5 px-3 rounded"
>
<BoltOutlined fontSize="small" className="-my-1" />
What{"'"}s New?
</button>
<Link href="https://github.com/makeplane/plane" target="_blank" rel="noopener noreferrer">
<a className="flex items-center gap-1.5 bg-custom-background-80 text-xs font-medium py-1.5 px-3 rounded">
<Image
src={theme === "dark" ? githubWhiteImage : githubBlackImage}
height={16}
width={16}
alt="GitHub Logo"
/>
Star us on GitHub
</a>
</Link>
</div>
}
>
{isProductUpdatesModalOpen && (
<ProductUpdatesModal
isOpen={isProductUpdatesModalOpen}
setIsOpen={setIsProductUpdatesModalOpen}
/>
)}
{user && !user.is_tour_completed && (
<div className="fixed top-0 left-0 h-full w-full bg-custom-backdrop bg-opacity-50 transition-opacity z-20 grid place-items-center">
<TourRoot
onComplete={() => {
mutate<ICurrentUserResponse>(
CURRENT_USER,
(prevData) => {
if (!prevData) return prevData;
return {
...prevData,
is_tour_completed: true,
};
},
false
);
userService.updateUserTourCompleted(user).catch(() => mutate(CURRENT_USER));
}}
/>
</div>
)}
<div className="p-8 space-y-8">
<Greeting user={user} />
{projects ? (
projects.length > 0 ? (
<div className="flex flex-col gap-8">
<IssuesStats data={workspaceDashboardData} />
<div className="grid grid-cols-1 gap-8 lg:grid-cols-2">
<IssuesList issues={workspaceDashboardData?.overdue_issues} type="overdue" />
<IssuesList issues={workspaceDashboardData?.upcoming_issues} type="upcoming" />
<IssuesPieChart groupedIssues={workspaceDashboardData?.state_distribution} />
<CompletedIssuesGraph
issues={workspaceDashboardData?.completed_issues}
month={month}
setMonth={setMonth}
/>
</div>
</div>
) : (
<div className="bg-custom-primary-100/5 flex justify-between gap-5 md:gap-8">
<div className="p-5 md:p-8 pr-0">
<h5 className="text-xl font-semibold">Create a project</h5>
<p className="mt-2 mb-5">
Manage your projects by creating issues, cycles, modules, views and pages.
</p>
<PrimaryButton
onClick={() => {
const e = new KeyboardEvent("keydown", {
key: "p",
});
document.dispatchEvent(e);
}}
>
Create Project
</PrimaryButton>
</div>
<div className="hidden md:block self-end overflow-hidden pt-8">
<Image src={emptyDashboard} alt="Empty Dashboard" />
</div>
</div>
)
) : null}
</div>
</WorkspaceAuthorizationLayout>
);
};
const WorkspacePage: NextPage = () => (
<AppLayout header={<WorkspaceDashboardHeader />}>
<WorkspaceDashboardView />
</AppLayout>
);
export default WorkspacePage;

View file

@ -5,13 +5,13 @@ import { useRouter } from "next/router";
// icons
import { PlusIcon } from "@heroicons/react/24/outline";
// layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout-legacy";
// hooks
import useMyIssuesFilters from "hooks/my-issues/use-my-issues-filter";
// components
import { MyIssuesView, MyIssuesViewOptions } from "components/issues";
// ui
import { PrimaryButton } from "components/ui";
import { Button } from "@plane/ui";
import { Breadcrumbs, BreadcrumbItem } from "components/breadcrumbs";
// types
import type { NextPage } from "next";
@ -85,16 +85,16 @@ const MyIssuesPage: NextPage = () => {
right={
<div className="flex items-center gap-2">
<MyIssuesViewOptions />
<PrimaryButton
className="flex items-center gap-2"
<Button
variant="primary"
prependIcon={<PlusIcon />}
onClick={() => {
const e = new KeyboardEvent("keydown", { key: "c" });
document.dispatchEvent(e);
}}
>
<PlusIcon className="h-4 w-4" />
Add Issue
</PrimaryButton>
</Button>
</div>
}
>
@ -107,9 +107,7 @@ const MyIssuesPage: NextPage = () => {
type="button"
onClick={tab.onClick}
className={`border-b-2 p-4 text-sm font-medium outline-none whitespace-nowrap ${
tab.selected
? "border-custom-primary-100 text-custom-primary-100"
: "border-transparent"
tab.selected ? "border-custom-primary-100 text-custom-primary-100" : "border-transparent"
}`}
>
{tab.label}

View file

@ -1,26 +1,27 @@
import useSWR from "swr";
import { useRouter } from "next/router";
import Link from "next/link";
// services
import userService from "services/user.service";
import { UserService } from "services/user.service";
// layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout-legacy";
// components
import { ActivityIcon, ActivityMessage } from "components/core";
import { RichReadOnlyEditor } from "@plane/rich-text-editor";
// icons
import { ArrowTopRightOnSquareIcon, ChatBubbleLeftEllipsisIcon } from "@heroicons/react/24/outline";
// ui
import { Icon, Loader } from "components/ui";
import { Icon } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
import { Loader } from "@plane/ui";
// fetch-keys
import { USER_ACTIVITY } from "constants/fetch-keys";
// helper
import { timeAgo } from "helpers/date-time.helper";
import { SettingsSidebar } from "components/project";
const userService = new UserService();
const ProfileActivity = () => {
const router = useRouter();
const { workspaceSlug } = router.query;
@ -50,7 +51,7 @@ const ProfileActivity = () => {
</div>
<div className={`flex flex-col gap-2 py-4 w-full`}>
<ul role="list" className="-mb-4">
{userActivity.results.map((activityItem: any, activityIdx: number) => {
{userActivity.results.map((activityItem: any) => {
if (activityItem.field === "comment") {
return (
<div key={activityItem.id} className="mt-2">
@ -60,8 +61,7 @@ const ProfileActivity = () => {
activityItem.new_value === "restore" && (
<Icon iconName="history" className="text-sm text-custom-text-200" />
)
) : activityItem.actor_detail.avatar &&
activityItem.actor_detail.avatar !== "" ? (
) : activityItem.actor_detail.avatar && activityItem.actor_detail.avatar !== "" ? (
<img
src={activityItem.actor_detail.avatar}
alt={activityItem.actor_detail.display_name}
@ -97,11 +97,7 @@ const ProfileActivity = () => {
</div>
<div className="issue-comments-section p-0">
<RichReadOnlyEditor
value={
activityItem?.new_value !== ""
? activityItem.new_value
: activityItem.old_value
}
value={activityItem?.new_value !== "" ? activityItem.new_value : activityItem.old_value}
customClassName="text-xs border border-custom-border-200 bg-custom-background-100"
noBorder
borderOnFocus={false}
@ -115,16 +111,14 @@ const ProfileActivity = () => {
const message =
activityItem.verb === "created" &&
activityItem.field !== "cycles" &&
activityItem.field !== "modules" &&
activityItem.field !== "attachment" &&
activityItem.field !== "link" &&
activityItem.field !== "estimate" ? (
activityItem.field !== "cycles" &&
activityItem.field !== "modules" &&
activityItem.field !== "attachment" &&
activityItem.field !== "link" &&
activityItem.field !== "estimate" ? (
<span className="text-custom-text-200">
created{" "}
<Link
href={`/${workspaceSlug}/projects/${activityItem.project}/issues/${activityItem.issue}`}
>
<Link href={`/${workspaceSlug}/projects/${activityItem.project}/issues/${activityItem.issue}`}>
<a className="inline-flex items-center hover:underline">
this issue. <ArrowTopRightOnSquareIcon className="ml-1 h-3.5 w-3.5" />
</a>
@ -148,10 +142,7 @@ const ProfileActivity = () => {
<div className="flex h-6 w-6 items-center justify-center">
{activityItem.field ? (
activityItem.new_value === "restore" ? (
<Icon
iconName="history"
className="!text-2xl text-custom-text-200"
/>
<Icon iconName="history" className="!text-2xl text-custom-text-200" />
) : (
<ActivityIcon activity={activityItem} />
)
@ -177,26 +168,19 @@ const ProfileActivity = () => {
</div>
<div className="min-w-0 flex-1 py-4 border-b border-custom-border-200">
<div className="text-sm text-custom-text-200 break-words">
{activityItem.field === "archived_at" &&
activityItem.new_value !== "restore" ? (
{activityItem.field === "archived_at" && activityItem.new_value !== "restore" ? (
<span className="text-gray font-medium">Plane</span>
) : activityItem.actor_detail.is_bot ? (
<span className="text-gray font-medium">
{activityItem.actor_detail.first_name} Bot
</span>
) : (
<Link
href={`/${workspaceSlug}/profile/${activityItem.actor_detail.id}`}
>
<a className="text-gray font-medium">
{activityItem.actor_detail.display_name}
</a>
<Link href={`/${workspaceSlug}/profile/${activityItem.actor_detail.id}`}>
<a className="text-gray font-medium">{activityItem.actor_detail.display_name}</a>
</Link>
)}{" "}
{message}{" "}
<span className="whitespace-nowrap">
{timeAgo(activityItem.created_at)}
</span>
<span className="whitespace-nowrap">{timeAgo(activityItem.created_at)}</span>
</div>
</div>
</>

View file

@ -1,22 +1,21 @@
import React, { useEffect, useState } from "react";
import { useRouter } from "next/router";
import Link from "next/link";
// react-hook-form
import { Controller, useForm } from "react-hook-form";
// services
import fileService from "services/file.service";
import userService from "services/user.service";
import { FileService } from "services/file.service";
import { UserService } from "services/user.service";
// hooks
import useUserAuth from "hooks/use-user-auth";
import useToast from "hooks/use-toast";
// layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout-legacy";
// components
import { ImagePickerPopover, ImageUploadModal } from "components/core";
import { SettingsSidebar } from "components/project";
// ui
import { CustomSearchSelect, CustomSelect, Input, PrimaryButton, Spinner } from "components/ui";
import { Button, Input, Spinner } from "@plane/ui";
import { CustomSearchSelect, CustomSelect } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// icons
import { UserIcon } from "@heroicons/react/24/outline";
@ -38,15 +37,17 @@ const defaultValues: Partial<IUser> = {
user_timezone: "Asia/Kolkata",
};
const fileService = new FileService();
const userService = new UserService();
const Profile: NextPage = () => {
const [isRemoving, setIsRemoving] = useState(false);
const [isImageUploadModalOpen, setIsImageUploadModalOpen] = useState(false);
// router
const router = useRouter();
const { workspaceSlug } = router.query;
// form info
const {
register,
handleSubmit,
reset,
watch,
@ -174,12 +175,9 @@ const Profile: NextPage = () => {
<div className={`flex flex-col gap-8 pr-9 py-9 w-full overflow-y-auto`}>
<div className="relative h-44 w-full mt-6">
<img
src={
watch("cover_image") ??
"https://images.unsplash.com/photo-1506383796573-caf02b4a79ab"
}
src={watch("cover_image") ?? "https://images.unsplash.com/photo-1506383796573-caf02b4a79ab"}
className="h-44 w-full rounded-lg object-cover"
alt={myProfile?.name ?? "Cover image"}
alt={myProfile?.first_name ?? "Cover image"}
/>
<div className="flex items-end justify-between absolute left-8 -bottom-6">
<div className="flex gap-3">
@ -214,10 +212,8 @@ const Profile: NextPage = () => {
onChange={(imageUrl) => {
setValue("cover_image", imageUrl);
}}
value={
watch("cover_image") ??
"https://images.unsplash.com/photo-1506383796573-caf02b4a79ab"
}
control={control}
value={watch("cover_image") ?? "https://images.unsplash.com/photo-1506383796573-caf02b4a79ab"}
/>
)}
/>
@ -245,42 +241,66 @@ const Profile: NextPage = () => {
<div className="grid grid-cols-1 lg:grid-cols-2 2xl:grid-cols-3 gap-6 px-8">
<div className="flex flex-col gap-1">
<h4 className="text-sm">First Name</h4>
<Input
<Controller
control={control}
name="first_name"
id="first_name"
register={register}
error={errors.first_name}
placeholder="Enter your first name"
className="!px-3 !py-2 rounded-md font-medium"
autoComplete="off"
maxLength={24}
render={({ field: { value, onChange, ref } }) => (
<Input
id="first_name"
name="first_name"
type="text"
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.first_name)}
placeholder="Enter your first name"
className="rounded-md font-medium w-full"
/>
)}
/>
</div>
<div className="flex flex-col gap-1">
<h4 className="text-sm">Last Name</h4>
<Input
<Controller
control={control}
name="last_name"
register={register}
error={errors.last_name}
id="last_name"
placeholder="Enter your last name"
autoComplete="off"
className="!px-3 !py-2 rounded-md font-medium"
maxLength={24}
render={({ field: { value, onChange, ref } }) => (
<Input
id="last_name"
name="last_name"
type="text"
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.last_name)}
placeholder="Enter your last name"
className="rounded-md font-medium w-full"
/>
)}
/>
</div>
<div className="flex flex-col gap-1">
<h4 className="text-sm">Email</h4>
<Input
id="email"
<Controller
control={control}
name="email"
autoComplete="off"
register={register}
className="!px-3 !py-2 rounded-md font-medium"
error={errors.name}
disabled
render={({ field: { value, onChange, ref } }) => (
<Input
id="email"
name="email"
type="email"
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.email)}
placeholder="Enter your last name"
className="rounded-md font-medium w-full"
disabled
/>
)}
/>
</div>
@ -307,29 +327,20 @@ const Profile: NextPage = () => {
</CustomSelect>
)}
/>
{errors.role && (
<span className="text-xs text-red-500">Please select a role</span>
)}
{errors.role && <span className="text-xs text-red-500">Please select a role</span>}
</div>
<div className="flex flex-col gap-1">
<h4 className="text-sm">Display name </h4>
<Input
id="display_name"
<Controller
control={control}
name="display_name"
autoComplete="off"
register={register}
error={errors.display_name}
className="w-full"
placeholder="Enter your display name"
validations={{
rules={{
required: "Display name is required.",
validate: (value) => {
if (value.trim().length < 1) return "Display name can't be empty.";
if (value.split(" ").length > 1)
return "Display name can't have two consecutive spaces.";
if (value.split(" ").length > 1) return "Display name can't have two consecutive spaces.";
if (value.replace(/\s/g, "").length < 1)
return "Display name must be at least 1 characters long.";
@ -340,6 +351,19 @@ const Profile: NextPage = () => {
return true;
},
}}
render={({ field: { value, onChange, ref } }) => (
<Input
id="display_name"
name="display_name"
type="text"
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.display_name)}
placeholder="Enter your display name"
className="w-full"
/>
)}
/>
</div>
@ -353,11 +377,7 @@ const Profile: NextPage = () => {
render={({ field: { value, onChange } }) => (
<CustomSearchSelect
value={value}
label={
value
? TIME_ZONES.find((t) => t.value === value)?.label ?? value
: "Select a timezone"
}
label={value ? TIME_ZONES.find((t) => t.value === value)?.label ?? value : "Select a timezone"}
options={timeZoneOptions}
onChange={onChange}
optionsClassName="w-full"
@ -365,15 +385,13 @@ const Profile: NextPage = () => {
/>
)}
/>
{errors.role && (
<span className="text-xs text-red-500">Please select a role</span>
)}
{errors.role && <span className="text-xs text-red-500">Please select a role</span>}
</div>
<div className="flex items-center justify-between py-2">
<PrimaryButton type="submit" loading={isSubmitting}>
<Button variant="primary" type="submit" loading={isSubmitting}>
{isSubmitting ? "Updating Profile..." : "Update Profile"}
</PrimaryButton>
</Button>
</div>
</div>
</div>

View file

@ -2,11 +2,11 @@ import { useEffect, useState } from "react";
// hooks
import useUserAuth from "hooks/use-user-auth";
// layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout-legacy";
// components
import { CustomThemeSelector, ThemeSwitch } from "components/core";
// ui
import { Spinner } from "components/ui";
import { Spinner } from "@plane/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// types
import { ICustomTheme } from "types";
@ -36,17 +36,13 @@ const ProfilePreferences = observer(() => {
background: currentTheme.background !== "" ? currentTheme.background : "#0d101b",
text: currentTheme.text !== "" ? currentTheme.text : "#c5c5c5",
primary: currentTheme.primary !== "" ? currentTheme.primary : "#3f76ff",
sidebarBackground:
currentTheme.sidebarBackground !== "" ? currentTheme.sidebarBackground : "#0d101b",
sidebarBackground: currentTheme.sidebarBackground !== "" ? currentTheme.sidebarBackground : "#0d101b",
sidebarText: currentTheme.sidebarText !== "" ? currentTheme.sidebarText : "#c5c5c5",
darkPalette: false,
palette:
currentTheme.palette !== ",,,,"
? currentTheme.palette
: "#0d101b,#c5c5c5,#3f76ff,#0d101b,#c5c5c5",
palette: currentTheme.palette !== ",,,," ? currentTheme.palette : "#0d101b,#c5c5c5,#3f76ff,#0d101b,#c5c5c5",
theme: "custom",
});
setCustomThemeSelectorOptions((prevData) => true);
setCustomThemeSelectorOptions(() => true);
}
}, [store, store?.theme?.theme]);
@ -71,9 +67,7 @@ const ProfilePreferences = observer(() => {
<div className="grid grid-cols-12 gap-4 sm:gap-16 py-6">
<div className="col-span-12 sm:col-span-6">
<h4 className="text-lg font-semibold text-custom-text-100">Theme</h4>
<p className="text-sm text-custom-text-200">
Select or customize your interface color scheme.
</p>
<p className="text-sm text-custom-text-200">Select or customize your interface color scheme.</p>
</div>
<div className="col-span-12 sm:col-span-6">
<ThemeSwitch

View file

@ -5,7 +5,7 @@ import { useRouter } from "next/router";
import useSWR from "swr";
// services
import userService from "services/user.service";
import { UserService } from "services/user.service";
// layouts
import { ProfileAuthWrapper } from "layouts/profile-layout";
// components
@ -23,15 +23,16 @@ import { IUserStateDistribution, TStateGroups } from "types";
import { USER_PROFILE_DATA } from "constants/fetch-keys";
import { GROUP_CHOICES } from "constants/project";
// services
const userService = new UserService();
const ProfileOverview: NextPage = () => {
const router = useRouter();
const { workspaceSlug, userId } = router.query;
const { data: userProfile } = useSWR(
workspaceSlug && userId ? USER_PROFILE_DATA(workspaceSlug.toString(), userId.toString()) : null,
workspaceSlug && userId
? () => userService.getUserProfileData(workspaceSlug.toString(), userId.toString())
: null
workspaceSlug && userId ? () => userService.getUserProfileData(workspaceSlug.toString(), userId.toString()) : null
);
const stateDistribution: IUserStateDistribution[] = Object.keys(GROUP_CHOICES).map((key) => {
@ -48,10 +49,7 @@ const ProfileOverview: NextPage = () => {
<ProfileWorkload stateDistribution={stateDistribution} />
<div className="grid grid-cols-1 xl:grid-cols-2 items-stretch gap-5">
<ProfilePriorityDistribution userProfile={userProfile} />
<ProfileStateDistribution
stateDistribution={stateDistribution}
userProfile={userProfile}
/>
<ProfileStateDistribution stateDistribution={stateDistribution} userProfile={userProfile} />
</div>
<ProfileActivity />
</div>

View file

@ -7,17 +7,18 @@ import useSWR, { mutate } from "swr";
// react-hook-form
import { useForm } from "react-hook-form";
// services
import issuesService from "services/issues.service";
import { IssueService, IssueArchiveService } from "services/issue";
// hooks
import useUserAuth from "hooks/use-user-auth";
import useToast from "hooks/use-toast";
// layouts
import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
import { ProjectAuthorizationWrapper } from "layouts/auth-layout-legacy";
// components
import { IssueDetailsSidebar, IssueMainContent } from "components/issues";
// ui
import { Icon, Loader } from "components/ui";
import { Icon } from "components/ui";
import { Breadcrumbs } from "components/breadcrumbs";
import { Loader } from "@plane/ui";
// types
import { IIssue } from "types";
import type { NextPage } from "next";
@ -40,6 +41,10 @@ const defaultValues: Partial<IIssue> = {
labels_list: [],
};
// services
const issueService = new IssueService();
const issueArchiveService = new IssueArchiveService();
const ArchivedIssueDetailsPage: NextPage = () => {
const [isRestoring, setIsRestoring] = useState(false);
@ -53,7 +58,7 @@ const ArchivedIssueDetailsPage: NextPage = () => {
workspaceSlug && projectId && archivedIssueId ? ISSUE_DETAILS(archivedIssueId as string) : null,
workspaceSlug && projectId && archivedIssueId
? () =>
issuesService.retrieveArchivedIssue(
issueArchiveService.retrieveArchivedIssue(
workspaceSlug as string,
projectId as string,
archivedIssueId as string
@ -86,14 +91,8 @@ const ArchivedIssueDetailsPage: NextPage = () => {
...formData,
};
await issuesService
.patchIssue(
workspaceSlug as string,
projectId as string,
archivedIssueId as string,
payload,
user
)
await issueService
.patchIssue(workspaceSlug as string, projectId as string, archivedIssueId as string, payload, user)
.then(() => {
mutateIssueDetails();
mutate(PROJECT_ISSUES_ACTIVITY(archivedIssueId as string));
@ -111,8 +110,7 @@ const ArchivedIssueDetailsPage: NextPage = () => {
mutate(PROJECT_ISSUES_ACTIVITY(archivedIssueId as string));
reset({
...issueDetails,
assignees_list:
issueDetails.assignees_list ?? issueDetails.assignee_details?.map((user) => user.id),
assignees_list: issueDetails.assignees_list ?? issueDetails.assignee_details?.map((user) => user.id),
labels_list: issueDetails.labels_list ?? issueDetails.labels,
labels: issueDetails.labels_list ?? issueDetails.labels,
});
@ -123,7 +121,7 @@ const ArchivedIssueDetailsPage: NextPage = () => {
setIsRestoring(true);
await issuesService
await issueArchiveService
.unarchiveIssue(workspaceSlug as string, projectId as string, archivedIssueId as string)
.then(() => {
setToastAlert({
@ -181,11 +179,7 @@ const ArchivedIssueDetailsPage: NextPage = () => {
</div>
)}
<div className="space-y-5 divide-y-2 divide-custom-border-200 opacity-60 pointer-events-none">
<IssueMainContent
issueDetails={issueDetails}
submitChanges={submitChanges}
uneditable
/>
<IssueMainContent issueDetails={issueDetails} submitChanges={submitChanges} uneditable />
</div>
</div>
<div className="w-1/3 h-full space-y-5 border-l border-custom-border-300 p-5 overflow-hidden">

View file

@ -3,9 +3,9 @@ import { useRouter } from "next/router";
import useSWR from "swr";
// services
import projectService from "services/project.service";
import { ProjectService } from "services/project";
// layouts
import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
import { ProjectAuthorizationWrapper } from "layouts/auth-layout-legacy";
// contexts
import { IssueViewContextProvider } from "contexts/issue-view.context";
// helper
@ -22,15 +22,16 @@ import type { NextPage } from "next";
// fetch-keys
import { PROJECT_DETAILS } from "constants/fetch-keys";
// services
const projectService = new ProjectService();
const ProjectArchivedIssues: NextPage = () => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { data: projectDetails } = useSWR(
workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
workspaceSlug && projectId
? () => projectService.getProject(workspaceSlug as string, projectId as string)
: null
workspaceSlug && projectId ? () => projectService.getProject(workspaceSlug as string, projectId as string) : null
);
return (
@ -39,9 +40,7 @@ const ProjectArchivedIssues: NextPage = () => {
breadcrumbs={
<Breadcrumbs>
<BreadcrumbItem title="Projects" link={`/${workspaceSlug}/projects`} />
<BreadcrumbItem
title={`${truncateText(projectDetails?.name ?? "Project", 32)} Archived Issues`}
/>
<BreadcrumbItem title={`${truncateText(projectDetails?.name ?? "Project", 32)} Archived Issues`} />
</Breadcrumbs>
}
right={

View file

@ -1,28 +1,26 @@
import React, { useState } from "react";
import { useRouter } from "next/router";
import useSWR from "swr";
// icons
import { ArrowLeftIcon } from "@heroicons/react/24/outline";
import { CyclesIcon } from "components/icons";
// layouts
import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
import { ProjectAuthorizationWrapper } from "layouts/auth-layout-legacy";
// contexts
import { IssueViewContextProvider } from "contexts/issue-view.context";
// components
import { ExistingIssuesListModal, IssuesFilterView, IssuesView } from "components/core";
import { ExistingIssuesListModal } from "components/core";
import { CycleDetailsSidebar, TransferIssues, TransferIssuesModal } from "components/cycles";
import { CycleLayoutRoot } from "components/issues/issue-layouts";
// services
import issuesService from "services/issues.service";
import cycleServices from "services/cycles.service";
import { IssueService } from "services/issue";
import { CycleService } from "services/cycle.service";
// hooks
import useToast from "hooks/use-toast";
import useUserAuth from "hooks/use-user-auth";
// components
import { AnalyticsProjectModal } from "components/analytics";
// ui
import { CustomMenu, EmptyState, SecondaryButton } from "components/ui";
import { CustomMenu } from "components/ui";
import { EmptyState } from "components/common";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// images
import emptyCycle from "public/empty-state/cycle.svg";
@ -33,6 +31,11 @@ import { getDateRangeStatus } from "helpers/date-time.helper";
import { ISearchIssueResponse } from "types";
// fetch-keys
import { CYCLES_LIST, CYCLE_DETAILS } from "constants/fetch-keys";
import { CycleIssuesHeader } from "components/headers";
// services
const issueService = new IssueService();
const cycleService = new CycleService();
const SingleCycle: React.FC = () => {
const [cycleIssuesListModal, setCycleIssuesListModal] = useState(false);
@ -50,19 +53,14 @@ const SingleCycle: React.FC = () => {
const { data: cycles } = useSWR(
workspaceSlug && projectId ? CYCLES_LIST(projectId as string) : null,
workspaceSlug && projectId
? () => cycleServices.getCyclesWithParams(workspaceSlug as string, projectId as string, "all")
? () => cycleService.getCyclesWithParams(workspaceSlug as string, projectId as string, "all")
: null
);
const { data: cycleDetails, error } = useSWR(
workspaceSlug && projectId && cycleId ? CYCLE_DETAILS(cycleId.toString()) : null,
workspaceSlug && projectId && cycleId
? () =>
cycleServices.getCycleDetails(
workspaceSlug.toString(),
projectId.toString(),
cycleId.toString()
)
? () => cycleService.getCycleDetails(workspaceSlug.toString(), projectId.toString(), cycleId.toString())
: null
);
@ -71,10 +69,6 @@ const SingleCycle: React.FC = () => {
? getDateRangeStatus(cycleDetails?.start_date, cycleDetails?.end_date)
: "draft";
const openIssuesListModal = () => {
setCycleIssuesListModal(true);
};
const handleAddIssuesToCycle = async (data: ISearchIssueResponse[]) => {
if (!workspaceSlug || !projectId) return;
@ -82,14 +76,8 @@ const SingleCycle: React.FC = () => {
issues: data.map((i) => i.id),
};
await issuesService
.addIssueToCycle(
workspaceSlug as string,
projectId as string,
cycleId as string,
payload,
user
)
await issueService
.addIssueToCycle(workspaceSlug as string, projectId as string, cycleId as string, payload, user)
.catch(() => {
setToastAlert({
type: "error",
@ -141,14 +129,7 @@ const SingleCycle: React.FC = () => {
}
right={
<div className={`flex flex-shrink-0 items-center gap-2 duration-300`}>
<IssuesFilterView />
<SecondaryButton
onClick={() => setAnalyticsModal(true)}
className="!py-1.5 font-normal rounded-md text-custom-text-200 hover:text-custom-text-100"
outline
>
Analytics
</SecondaryButton>
<CycleIssuesHeader />
<button
type="button"
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-custom-background-90 ${
@ -173,28 +154,16 @@ const SingleCycle: React.FC = () => {
/>
) : (
<>
<TransferIssuesModal
handleClose={() => setTransferIssuesModal(false)}
isOpen={transferIssuesModal}
/>
<AnalyticsProjectModal
isOpen={analyticsModal}
onClose={() => setAnalyticsModal(false)}
/>
<TransferIssuesModal handleClose={() => setTransferIssuesModal(false)} isOpen={transferIssuesModal} />
<div
className={`h-full flex flex-col ${cycleSidebar ? "mr-[24rem]" : ""} ${
className={`relative w-full h-full flex flex-col overflow-auto ${cycleSidebar ? "mr-[24rem]" : ""} ${
analyticsModal ? "mr-[50%]" : ""
} duration-300`}
>
{cycleStatus === "completed" && (
<TransferIssues handleClick={() => setTransferIssuesModal(true)} />
)}
<div className="relative overflow-y-auto w-full h-full">
<IssuesView
openIssuesListModal={openIssuesListModal}
disableUserActions={cycleStatus === "completed" ?? false}
/>
</div>
{cycleStatus === "completed" && <TransferIssues handleClick={() => setTransferIssuesModal(true)} />}
<CycleLayoutRoot />
</div>
<CycleDetailsSidebar
cycleStatus={cycleStatus}

View file

@ -1,26 +1,18 @@
import React, { useEffect, useState } from "react";
import { useRouter } from "next/router";
// headless ui
import { Tab } from "@headlessui/react";
import useSWR from "swr";
// hooks
import useLocalStorage from "hooks/use-local-storage";
import useUserAuth from "hooks/use-user-auth";
import useProjectDetails from "hooks/use-project-details";
// layouts
import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
import { ProjectAuthorizationWrapper } from "layouts/auth-layout-legacy";
// components
import {
ActiveCycleDetails,
AllCyclesList,
CompletedCyclesList,
CreateUpdateCycleModal,
DraftCyclesList,
UpcomingCyclesList,
} from "components/cycles";
import { CyclesView, ActiveCycleDetails, CreateUpdateCycleModal } from "components/cycles";
// ui
import { EmptyState, Icon, PrimaryButton } from "components/ui";
import { Button } from "@plane/ui";
import { EmptyState } from "components/common";
import { Icon } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// icons
import { PlusIcon } from "@heroicons/react/24/outline";
@ -31,54 +23,42 @@ import { SelectCycleType } from "types";
import type { NextPage } from "next";
// helper
import { truncateText } from "helpers/string.helper";
import { useMobxStore } from "lib/mobx/store-provider";
import { observer } from "mobx-react-lite";
// constants
import { CYCLE_TAB_LIST, CYCLE_VIEWS } from "constants/cycle";
const tabsList = ["All", "Active", "Upcoming", "Completed", "Drafts"];
type ICycleAPIFilter = "all" | "current" | "upcoming" | "draft" | "completed" | "incomplete";
type ICycleView = "list" | "board" | "gantt";
const cycleViews = [
{
key: "list",
icon: "list",
},
{
key: "board",
icon: "dataset",
},
{
key: "gantt",
icon: "view_timeline",
},
];
const ProjectCycles: NextPage = () => {
const ProjectCyclesPage: NextPage = observer(() => {
// router
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
// store
const { project: projectStore } = useMobxStore();
const projectDetails = projectId ? projectStore.project_details[projectId.toString()] : null;
// states
const [selectedCycle, setSelectedCycle] = useState<SelectCycleType>();
const [createUpdateCycleModal, setCreateUpdateCycleModal] = useState(false);
const { storedValue: cycleTab, setValue: setCycleTab } = useLocalStorage("cycleTab", "All");
const { storedValue: cyclesView, setValue: setCyclesView } = useLocalStorage("cycleView", "list");
const currentTabValue = (tab: string | null) => {
switch (tab) {
case "All":
return 0;
case "Active":
return 1;
case "Upcoming":
return 2;
case "Completed":
return 3;
case "Drafts":
return 4;
default:
return 0;
}
};
const router = useRouter();
const { workspaceSlug } = router.query;
// local storage
const { storedValue: cycleTab, setValue: setCycleTab } = useLocalStorage("cycle_tab", "all");
const { storedValue: cyclesView, setValue: setCyclesView } = useLocalStorage("cycle_view", "list");
// hooks
const { user } = useUserAuth();
const { projectDetails } = useProjectDetails();
// api call fetch project details
useSWR(
workspaceSlug && projectId ? `PROJECT_DETAILS_${projectId}` : null,
workspaceSlug && projectId
? () => {
projectStore.fetchProjectDetails(workspaceSlug.toString(), projectId.toString());
}
: null
);
/**
* Clearing form data after closing the modal
*/
useEffect(() => {
if (createUpdateCycleModal) return;
@ -88,6 +68,12 @@ const ProjectCycles: NextPage = () => {
}, 500);
}, [createUpdateCycleModal]);
useEffect(() => {
if (cycleTab === "draft" && cyclesView === "gantt") {
setCyclesView("list");
}
}, [cycleTab, cyclesView, setCyclesView]);
return (
<ProjectAuthorizationWrapper
breadcrumbs={
@ -97,16 +83,16 @@ const ProjectCycles: NextPage = () => {
</Breadcrumbs>
}
right={
<PrimaryButton
className="flex items-center gap-2"
<Button
variant="primary"
prependIcon={<PlusIcon />}
onClick={() => {
const e = new KeyboardEvent("keydown", { key: "q" });
document.dispatchEvent(e);
}}
>
<PlusIcon className="h-4 w-4" />
Add Cycle
</PrimaryButton>
</Button>
}
>
<CreateUpdateCycleModal
@ -137,60 +123,42 @@ const ProjectCycles: NextPage = () => {
<Tab.Group
as="div"
className="h-full flex flex-col overflow-hidden"
defaultIndex={currentTabValue(cycleTab)}
selectedIndex={currentTabValue(cycleTab)}
defaultIndex={CYCLE_TAB_LIST.findIndex((i) => i.key === cycleTab)}
selectedIndex={CYCLE_TAB_LIST.findIndex((i) => i.key === cycleTab)}
onChange={(i) => {
switch (i) {
case 0:
return setCycleTab("All");
case 1:
return setCycleTab("Active");
case 2:
return setCycleTab("Upcoming");
case 3:
return setCycleTab("Completed");
case 4:
return setCycleTab("Drafts");
default:
return setCycleTab("All");
try {
setCycleTab(CYCLE_TAB_LIST[i].key);
} catch (e) {
setCycleTab(CYCLE_TAB_LIST[0].key);
}
}}
>
<div className="flex flex-col sm:flex-row gap-4 justify-between border-b border-custom-border-300 px-4 sm:px-5 pb-4 sm:pb-0">
<Tab.List as="div" className="flex items-center overflow-x-scroll">
{tabsList.map((tab, index) => {
if (cyclesView === "gantt_chart" && (tab === "Active" || tab === "Drafts"))
return null;
return (
<Tab
key={index}
className={({ selected }) =>
`border-b-2 p-4 text-sm font-medium outline-none ${
selected
? "border-custom-primary-100 text-custom-primary-100"
: "border-transparent"
}`
}
>
{tab}
</Tab>
);
})}
{CYCLE_TAB_LIST.map((tab) => (
<Tab
key={tab.key}
className={({ selected }) =>
`border-b-2 p-4 text-sm font-medium outline-none ${
selected ? "border-custom-primary-100 text-custom-primary-100" : "border-transparent"
}`
}
>
{tab.name}
</Tab>
))}
</Tab.List>
<div className="justify-end sm:justify-start flex items-center gap-x-1">
{cycleViews.map((view) => {
if (cycleTab === "Active") return null;
if (view.key === "gantt" && cycleTab === "Drafts") return null;
{CYCLE_VIEWS.map((view) => {
if (cycleTab === "active") return null;
if (view.key === "gantt" && cycleTab === "draft") return null;
return (
<button
key={view.key}
type="button"
className={`grid h-8 w-8 place-items-center rounded p-1 outline-none duration-300 hover:bg-custom-background-80 ${
cyclesView === view.key
? "bg-custom-background-80 text-custom-text-100"
: "text-custom-text-200"
cyclesView === view.key ? "bg-custom-background-80 text-custom-text-100" : "text-custom-text-200"
}`}
onClick={() => setCyclesView(view.key)}
>
@ -202,29 +170,53 @@ const ProjectCycles: NextPage = () => {
</div>
<Tab.Panels as={React.Fragment}>
<Tab.Panel as="div" className="p-4 sm:p-5 h-full overflow-y-auto">
<AllCyclesList viewType={cyclesView} />
{cycleTab && cyclesView && workspaceSlug && projectId && (
<CyclesView
filter="all"
view={cyclesView as ICycleView}
workspaceSlug={workspaceSlug?.toString()}
projectId={projectId?.toString()}
/>
)}
</Tab.Panel>
{cyclesView !== "gantt_chart" && (
<Tab.Panel as="div" className="p-4 sm:p-5 space-y-5 h-full overflow-y-auto">
<ActiveCycleDetails />
</Tab.Panel>
)}
<Tab.Panel as="div" className="p-4 sm:p-5 h-full overflow-y-auto">
<UpcomingCyclesList viewType={cyclesView} />
<Tab.Panel as="div" className="p-4 sm:p-5 space-y-5 h-full overflow-y-auto">
<ActiveCycleDetails />
</Tab.Panel>
<Tab.Panel as="div" className="p-4 sm:p-5 h-full overflow-y-auto">
<CompletedCyclesList viewType={cyclesView} />
{cycleTab && cyclesView && workspaceSlug && projectId && (
<CyclesView
filter="upcoming"
view={cyclesView as ICycleView}
workspaceSlug={workspaceSlug?.toString()}
projectId={projectId?.toString()}
/>
)}
</Tab.Panel>
<Tab.Panel as="div" className="p-4 sm:p-5 h-full overflow-y-auto">
{cycleTab && cyclesView && workspaceSlug && projectId && (
<CyclesView
filter="completed"
view={cyclesView as ICycleView}
workspaceSlug={workspaceSlug?.toString()}
projectId={projectId?.toString()}
/>
)}
</Tab.Panel>
<Tab.Panel as="div" className="p-4 sm:p-5 h-full overflow-y-auto">
{cycleTab && cyclesView && workspaceSlug && projectId && (
<CyclesView
filter="draft"
view={cyclesView as ICycleView}
workspaceSlug={workspaceSlug?.toString()}
projectId={projectId?.toString()}
/>
)}
</Tab.Panel>
{cyclesView !== "gantt_chart" && (
<Tab.Panel as="div" className="p-4 sm:p-5 h-full overflow-y-auto">
<DraftCyclesList viewType={cyclesView} />
</Tab.Panel>
)}
</Tab.Panels>
</Tab.Group>
)}
</ProjectAuthorizationWrapper>
);
};
});
export default ProjectCycles;
export default ProjectCyclesPage;

View file

@ -3,9 +3,9 @@ import { useRouter } from "next/router";
import useSWR from "swr";
// services
import projectService from "services/project.service";
import { ProjectService } from "services/project";
// layouts
import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
import { ProjectAuthorizationWrapper } from "layouts/auth-layout-legacy";
// contexts
import { IssueViewContextProvider } from "contexts/issue-view.context";
// helper
@ -13,7 +13,6 @@ import { truncateText } from "helpers/string.helper";
// components
import { IssuesFilterView, IssuesView } from "components/core";
// ui
import { Icon } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// icons
import { X, PenSquare } from "lucide-react";
@ -22,15 +21,16 @@ import type { NextPage } from "next";
// fetch-keys
import { PROJECT_DETAILS } from "constants/fetch-keys";
// services
const projectService = new ProjectService();
const ProjectDraftIssues: NextPage = () => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { data: projectDetails } = useSWR(
workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
workspaceSlug && projectId
? () => projectService.getProject(workspaceSlug as string, projectId as string)
: null
workspaceSlug && projectId ? () => projectService.getProject(workspaceSlug as string, projectId as string) : null
);
return (
@ -39,9 +39,7 @@ const ProjectDraftIssues: NextPage = () => {
breadcrumbs={
<Breadcrumbs>
<BreadcrumbItem title="Projects" link={`/${workspaceSlug}/projects`} />
<BreadcrumbItem
title={`${truncateText(projectDetails?.name ?? "Project", 32)} Draft Issues`}
/>
<BreadcrumbItem title={`${truncateText(projectDetails?.name ?? "Project", 32)} Draft Issues`} />
</Breadcrumbs>
}
right={

View file

@ -3,7 +3,7 @@ import { useRouter } from "next/router";
// hooks
import useProjectDetails from "hooks/use-project-details";
// layouts
import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
import { ProjectAuthorizationWrapper } from "layouts/auth-layout-legacy";
// contexts
import { InboxViewContextProvider } from "contexts/inbox-view-context";
// components
@ -11,7 +11,7 @@ import { InboxActionHeader, InboxMainContent, IssuesListSidebar } from "componen
// helper
import { truncateText } from "helpers/string.helper";
// ui
import { PrimaryButton } from "components/ui";
import { Button } from "@plane/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// icons
import { PlusIcon } from "@heroicons/react/24/outline";
@ -30,23 +30,21 @@ const ProjectInbox: NextPage = () => {
breadcrumbs={
<Breadcrumbs>
<BreadcrumbItem title="Projects" link={`/${workspaceSlug}/projects`} />
<BreadcrumbItem
title={`${truncateText(projectDetails?.name ?? "Project", 32)} Inbox`}
/>
<BreadcrumbItem title={`${truncateText(projectDetails?.name ?? "Project", 32)} Inbox`} />
</Breadcrumbs>
}
right={
<div className="flex items-center gap-2">
<PrimaryButton
className="flex items-center gap-2"
<Button
variant="primary"
prependIcon={<PlusIcon />}
onClick={() => {
const e = new KeyboardEvent("keydown", { key: "c" });
document.dispatchEvent(e);
}}
>
<PlusIcon className="h-4 w-4" />
Add Issue
</PrimaryButton>
</Button>
</div>
}
>

View file

@ -7,16 +7,17 @@ import useSWR, { mutate } from "swr";
// react-hook-form
import { useForm } from "react-hook-form";
// services
import issuesService from "services/issues.service";
import { IssueService } from "services/issue";
// hooks
import useUserAuth from "hooks/use-user-auth";
// layouts
import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
import { ProjectAuthorizationWrapper } from "layouts/auth-layout-legacy";
// components
import { IssueDetailsSidebar, IssueMainContent } from "components/issues";
// ui
import { EmptyState, Loader } from "components/ui";
import { EmptyState } from "components/common";
import { Breadcrumbs } from "components/breadcrumbs";
import { Loader } from "@plane/ui";
// images
import emptyIssue from "public/empty-state/issue.svg";
// types
@ -42,6 +43,9 @@ const defaultValues: Partial<IIssue> = {
target_date: null,
};
// services
const issueService = new IssueService();
const IssueDetailsPage: NextPage = () => {
const router = useRouter();
const { workspaceSlug, projectId, issueId } = router.query;
@ -56,8 +60,7 @@ const IssueDetailsPage: NextPage = () => {
} = useSWR(
workspaceSlug && projectId && issueId ? ISSUE_DETAILS(issueId as string) : null,
workspaceSlug && projectId && issueId
? () =>
issuesService.retrieve(workspaceSlug as string, projectId as string, issueId as string)
? () => issueService.retrieve(workspaceSlug as string, projectId as string, issueId as string)
: null
);
@ -89,7 +92,7 @@ const IssueDetailsPage: NextPage = () => {
delete payload.related_issues;
delete payload.issue_relations;
await issuesService
await issueService
.patchIssue(workspaceSlug as string, projectId as string, issueId as string, payload, user)
.then(() => {
mutateIssueDetails();
@ -108,8 +111,7 @@ const IssueDetailsPage: NextPage = () => {
mutate(PROJECT_ISSUES_ACTIVITY(issueId as string));
reset({
...issueDetails,
assignees_list:
issueDetails.assignees_list ?? issueDetails.assignee_details?.map((user) => user.id),
assignees_list: issueDetails.assignees_list ?? issueDetails.assignee_details?.map((user) => user.id),
labels_list: issueDetails.labels_list ?? issueDetails.labels,
labels: issueDetails.labels_list ?? issueDetails.labels,
});

View file

@ -1,110 +1,83 @@
import { useState } from "react";
import Link from "next/link";
import { useRouter } from "next/router";
import useSWR from "swr";
// mobx
import { useMobxStore } from "lib/mobx/store-provider";
// services
import projectService from "services/project.service";
import inboxService from "services/inbox.service";
import { ProjectService } from "services/project";
// layouts
import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
// contexts
import { IssueViewContextProvider } from "contexts/issue-view.context";
import { ProjectAuthorizationWrapper } from "layouts/auth-layout-legacy";
// helper
import { truncateText } from "helpers/string.helper";
// components
import { IssuesFilterView, IssuesView } from "components/core";
import { AnalyticsProjectModal } from "components/analytics";
import { ProjectLayoutRoot } from "components/issues";
import { ProjectIssuesHeader } from "components/headers";
// ui
import { PrimaryButton, SecondaryButton } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// icons
import { PlusIcon } from "@heroicons/react/24/outline";
// types
import type { NextPage } from "next";
// fetch-keys
import { PROJECT_DETAILS, INBOX_LIST } from "constants/fetch-keys";
import { PROJECT_DETAILS } from "constants/fetch-keys";
// services
const projectService = new ProjectService();
const ProjectIssues: NextPage = () => {
const [analyticsModal, setAnalyticsModal] = useState(false);
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { issueFilter: issueFilterStore, project: projectStore } = useMobxStore();
const { data: projectDetails } = useSWR(
workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
workspaceSlug && projectId ? () => projectService.getProject(workspaceSlug as string, projectId as string) : null
);
// TODO: update the fetch keys
useSWR(
workspaceSlug && projectId ? "REVALIDATE_USER_PROJECT_FILTERS" : null,
workspaceSlug && projectId
? () => projectService.getProject(workspaceSlug as string, projectId as string)
? () => issueFilterStore.fetchUserProjectFilters(workspaceSlug.toString(), projectId.toString())
: null
);
const { data: inboxList } = useSWR(
workspaceSlug && projectId ? INBOX_LIST(projectId as string) : null,
useSWR(
workspaceSlug && projectId ? "REVALIDATE_PROJECT_STATES_LIST" : null,
workspaceSlug && projectId
? () => inboxService.getInboxes(workspaceSlug as string, projectId as string)
? () => projectStore.fetchProjectStates(workspaceSlug.toString(), projectId.toString())
: null
);
useSWR(
workspaceSlug && projectId ? "REVALIDATE_PROJECT_LABELS_LIST" : null,
workspaceSlug && projectId
? () => projectStore.fetchProjectLabels(workspaceSlug.toString(), projectId.toString())
: null
);
useSWR(
workspaceSlug && projectId ? "REVALIDATE_PROJECT_MEMBERS_LIST" : null,
workspaceSlug && projectId
? () => projectStore.fetchProjectMembers(workspaceSlug.toString(), projectId.toString())
: null
);
return (
<IssueViewContextProvider>
<ProjectAuthorizationWrapper
breadcrumbs={
<Breadcrumbs>
<BreadcrumbItem title="Projects" link={`/${workspaceSlug}/projects`} />
<BreadcrumbItem
title={`${truncateText(projectDetails?.name ?? "Project", 32)} Issues`}
/>
</Breadcrumbs>
}
right={
<div className="flex items-center gap-2">
<IssuesFilterView />
<SecondaryButton
onClick={() => setAnalyticsModal(true)}
className="!py-1.5 rounded-md font-normal text-custom-sidebar-text-200 border-custom-border-200 hover:text-custom-text-100 hover:bg-custom-sidebar-background-90"
outline
>
Analytics
</SecondaryButton>
{projectDetails && projectDetails.inbox_view && (
<Link href={`/${workspaceSlug}/projects/${projectId}/inbox/${inboxList?.[0]?.id}`}>
<a>
<SecondaryButton
className="relative !py-1.5 rounded-md font-normal text-custom-sidebar-text-200 border-custom-border-200 hover:text-custom-text-100 hover:bg-custom-sidebar-background-90"
outline
>
<span>Inbox</span>
{inboxList && inboxList?.[0]?.pending_issue_count !== 0 && (
<span className="absolute -top-1 -right-1 h-4 w-4 rounded-full text-custom-text-100 bg-custom-sidebar-background-80 border border-custom-sidebar-border-200">
{inboxList?.[0]?.pending_issue_count}
</span>
)}
</SecondaryButton>
</a>
</Link>
)}
<PrimaryButton
className="flex items-center gap-2"
onClick={() => {
const e = new KeyboardEvent("keydown", { key: "c" });
document.dispatchEvent(e);
}}
>
<PlusIcon className="h-4 w-4" />
Add Issue
</PrimaryButton>
</div>
}
bg="secondary"
>
<AnalyticsProjectModal isOpen={analyticsModal} onClose={() => setAnalyticsModal(false)} />
<div className="h-full w-full flex flex-col">
<IssuesView />
</div>
</ProjectAuthorizationWrapper>
</IssueViewContextProvider>
<ProjectAuthorizationWrapper
breadcrumbs={
<Breadcrumbs>
<BreadcrumbItem title="Projects" link={`/${workspaceSlug}/projects`} />
<BreadcrumbItem title={`${truncateText(projectDetails?.name ?? "Project", 32)} Issues`} />
</Breadcrumbs>
}
right={<ProjectIssuesHeader />}
bg="secondary"
>
<div className="h-full w-full flex flex-col">
<ProjectLayoutRoot />
</div>
</ProjectAuthorizationWrapper>
);
};

View file

@ -1,26 +1,23 @@
import React, { useState } from "react";
import { useRouter } from "next/router";
import useSWR from "swr";
// icons
import { ArrowLeftIcon, RectangleGroupIcon } from "@heroicons/react/24/outline";
import { RectangleGroupIcon } from "@heroicons/react/24/outline";
// services
import modulesService from "services/modules.service";
import { ModuleService } from "services/module.service";
// hooks
import useToast from "hooks/use-toast";
import useUserAuth from "hooks/use-user-auth";
// layouts
import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
// contexts
import { IssueViewContextProvider } from "contexts/issue-view.context";
import { ProjectAuthorizationWrapper } from "layouts/auth-layout-legacy";
// components
import { ExistingIssuesListModal, IssuesFilterView, IssuesView } from "components/core";
import { ExistingIssuesListModal } from "components/core";
import { ModuleDetailsSidebar } from "components/modules";
import { AnalyticsProjectModal } from "components/analytics";
import { ModuleLayoutRoot } from "components/issues";
import { ModuleIssuesHeader } from "components/headers";
// ui
import { CustomMenu, EmptyState, SecondaryButton } from "components/ui";
import { CustomMenu } from "components/ui";
import { EmptyState } from "components/common";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// images
import emptyModule from "public/empty-state/module.svg";
@ -31,10 +28,12 @@ import { ISearchIssueResponse } from "types";
// fetch-keys
import { MODULE_DETAILS, MODULE_ISSUES, MODULE_LIST } from "constants/fetch-keys";
// services
const moduleService = new ModuleService();
const SingleModule: React.FC = () => {
const [moduleIssuesListModal, setModuleIssuesListModal] = useState(false);
const [moduleSidebar, setModuleSidebar] = useState(true);
const [analyticsModal, setAnalyticsModal] = useState(false);
const [moduleSidebar, setModuleSidebar] = useState(false);
const router = useRouter();
const { workspaceSlug, projectId, moduleId } = router.query;
@ -45,32 +44,20 @@ const SingleModule: React.FC = () => {
const { data: modules } = useSWR(
workspaceSlug && projectId ? MODULE_LIST(projectId as string) : null,
workspaceSlug && projectId
? () => modulesService.getModules(workspaceSlug as string, projectId as string)
: null
workspaceSlug && projectId ? () => moduleService.getModules(workspaceSlug as string, projectId as string) : null
);
const { data: moduleIssues } = useSWR(
workspaceSlug && projectId && moduleId ? MODULE_ISSUES(moduleId as string) : null,
workspaceSlug && projectId && moduleId
? () =>
modulesService.getModuleIssues(
workspaceSlug as string,
projectId as string,
moduleId as string
)
? () => moduleService.getModuleIssues(workspaceSlug as string, projectId as string, moduleId as string)
: null
);
const { data: moduleDetails, error } = useSWR(
moduleId ? MODULE_DETAILS(moduleId as string) : null,
workspaceSlug && projectId
? () =>
modulesService.getModuleDetails(
workspaceSlug as string,
projectId as string,
moduleId as string
)
? () => moduleService.getModuleDetails(workspaceSlug as string, projectId as string, moduleId as string)
: null
);
@ -81,14 +68,8 @@ const SingleModule: React.FC = () => {
issues: data.map((i) => i.id),
};
await modulesService
.addIssuesToModule(
workspaceSlug as string,
projectId as string,
moduleId as string,
payload,
user
)
await moduleService
.addIssuesToModule(workspaceSlug as string, projectId as string, moduleId as string, payload, user)
.catch(() =>
setToastAlert({
type: "error",
@ -103,7 +84,7 @@ const SingleModule: React.FC = () => {
};
return (
<IssueViewContextProvider>
<>
<ExistingIssuesListModal
isOpen={moduleIssuesListModal}
handleClose={() => setModuleIssuesListModal(false)}
@ -142,27 +123,7 @@ const SingleModule: React.FC = () => {
))}
</CustomMenu>
}
right={
<div className={`flex items-center gap-2 duration-300`}>
<IssuesFilterView />
<SecondaryButton
onClick={() => setAnalyticsModal(true)}
className="!py-1.5 font-normal rounded-md text-custom-text-200 hover:text-custom-text-100"
outline
>
Analytics
</SecondaryButton>
<button
type="button"
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-custom-background-90 ${
moduleSidebar ? "rotate-180" : ""
}`}
onClick={() => setModuleSidebar((prevData) => !prevData)}
>
<ArrowLeftIcon className="h-4 w-4" />
</button>
</div>
}
right={<ModuleIssuesHeader />}
>
{error ? (
<EmptyState
@ -176,16 +137,12 @@ const SingleModule: React.FC = () => {
/>
) : (
<>
<AnalyticsProjectModal
isOpen={analyticsModal}
onClose={() => setAnalyticsModal(false)}
/>
<div
className={`relative overflow-y-auto h-full flex flex-col ${
moduleSidebar ? "mr-[24rem]" : ""
} ${analyticsModal ? "mr-[50%]" : ""} duration-300`}
} duration-300`}
>
<IssuesView openIssuesListModal={openIssuesListModal} />
<ModuleLayoutRoot />
</div>
<ModuleDetailsSidebar
module={moduleDetails}
@ -196,7 +153,7 @@ const SingleModule: React.FC = () => {
</>
)}
</ProjectAuthorizationWrapper>
</IssueViewContextProvider>
</>
);
};

View file

@ -5,20 +5,18 @@ import { useRouter } from "next/router";
import useSWR from "swr";
// layouts
import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
import { ProjectAuthorizationWrapper } from "layouts/auth-layout-legacy";
// hooks
import useUserAuth from "hooks/use-user-auth";
// services
import projectService from "services/project.service";
import modulesService from "services/modules.service";
import { ProjectService } from "services/project";
import { ModuleService } from "services/module.service";
// components
import {
CreateUpdateModuleModal,
ModulesListGanttChartView,
SingleModuleCard,
} from "components/modules";
import { CreateUpdateModuleModal, ModulesListGanttChartView, SingleModuleCard } from "components/modules";
// ui
import { EmptyState, Icon, Loader, PrimaryButton, Tooltip } from "components/ui";
import { Button, Loader, Tooltip } from "@plane/ui";
import { Icon } from "components/ui";
import { EmptyState } from "components/common";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// icons
import { PlusIcon } from "@heroicons/react/24/outline";
@ -43,6 +41,10 @@ const moduleViewOptions: { type: "grid" | "gantt_chart"; icon: any }[] = [
},
];
// services
const projectService = new ProjectService();
const moduleService = new ModuleService();
const ProjectModules: NextPage = () => {
const [selectedModule, setSelectedModule] = useState<SelectModuleType>();
const [createUpdateModule, setCreateUpdateModule] = useState(false);
@ -56,16 +58,12 @@ const ProjectModules: NextPage = () => {
const { data: activeProject } = useSWR(
workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
workspaceSlug && projectId
? () => projectService.getProject(workspaceSlug as string, projectId as string)
: null
workspaceSlug && projectId ? () => projectService.getProject(workspaceSlug as string, projectId as string) : null
);
const { data: modules, mutate: mutateModules } = useSWR(
workspaceSlug && projectId ? MODULE_LIST(projectId as string) : null,
workspaceSlug && projectId
? () => modulesService.getModules(workspaceSlug as string, projectId as string)
: null
workspaceSlug && projectId ? () => moduleService.getModules(workspaceSlug as string, projectId as string) : null
);
const handleEditModule = (module: IModule) => {
@ -95,37 +93,30 @@ const ProjectModules: NextPage = () => {
{moduleViewOptions.map((option) => (
<Tooltip
key={option.type}
tooltipContent={
<span className="capitalize">{replaceUnderscoreIfSnakeCase(option.type)} Layout</span>
}
tooltipContent={<span className="capitalize">{replaceUnderscoreIfSnakeCase(option.type)} Layout</span>}
position="bottom"
>
<button
type="button"
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none hover:bg-custom-sidebar-background-80 duration-300 ${
modulesView === option.type
? "bg-custom-sidebar-background-80"
: "text-custom-sidebar-text-200"
modulesView === option.type ? "bg-custom-sidebar-background-80" : "text-custom-sidebar-text-200"
}`}
onClick={() => setModulesView(option.type)}
>
<Icon
iconName={option.icon}
className={`!text-base ${option.type === "grid" ? "rotate-90" : ""}`}
/>
<Icon iconName={option.icon} className={`!text-base ${option.type === "grid" ? "rotate-90" : ""}`} />
</button>
</Tooltip>
))}
<PrimaryButton
className="flex items-center gap-2"
<Button
variant="primary"
prependIcon={<PlusIcon />}
onClick={() => {
const e = new KeyboardEvent("keydown", { key: "m" });
document.dispatchEvent(e);
}}
>
<PlusIcon className="h-4 w-4" />
Add Module
</PrimaryButton>
</Button>
</div>
}
>

View file

@ -5,7 +5,7 @@ import { useRouter } from "next/router";
import useSWR, { mutate } from "swr";
// react-hook-form
import { useForm } from "react-hook-form";
import { Controller, useForm } from "react-hook-form";
// headless ui
import { Popover, Transition } from "@headlessui/react";
// react-color
@ -14,28 +14,23 @@ import { TwitterPicker } from "react-color";
import { DragDropContext, DropResult } from "react-beautiful-dnd";
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
// services
import projectService from "services/project.service";
import pagesService from "services/pages.service";
import issuesService from "services/issues.service";
import { ProjectService } from "services/project";
import { PageService } from "services/page.service";
import { IssueLabelService } from "services/issue";
// hooks
import useToast from "hooks/use-toast";
import useUser from "hooks/use-user";
// layouts
import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
import { ProjectAuthorizationWrapper } from "layouts/auth-layout-legacy";
// components
import { CreateUpdateBlockInline, SinglePageBlock } from "components/pages";
import { CreateLabelModal } from "components/labels";
import { CreateBlock } from "components/pages/create-block";
// ui
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
import {
CustomSearchSelect,
EmptyState,
Loader,
TextArea,
ToggleSwitch,
Tooltip,
} from "components/ui";
import { CustomSearchSelect } from "components/ui";
import { EmptyState } from "components/common";
import { TextArea, Loader, ToggleSwitch, Tooltip } from "@plane/ui";
// images
import emptyPage from "public/empty-state/page.svg";
// icons
@ -66,6 +61,11 @@ import {
USER_PROJECT_VIEW,
} from "constants/fetch-keys";
// services
const projectService = new ProjectService();
const pageService = new PageService();
const issueLabelService = new IssueLabelService();
const SinglePage: NextPage = () => {
const [createBlockForm, setCreateBlockForm] = useState(false);
const [labelModal, setLabelModal] = useState(false);
@ -80,45 +80,33 @@ const SinglePage: NextPage = () => {
const { user } = useUser();
const { handleSubmit, reset, watch, setValue } = useForm<IPage>({
const { handleSubmit, reset, watch, setValue, control } = useForm<IPage>({
defaultValues: { name: "" },
});
const { data: projectDetails } = useSWR(
workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
workspaceSlug && projectId
? () => projectService.getProject(workspaceSlug as string, projectId as string)
: null
workspaceSlug && projectId ? () => projectService.getProject(workspaceSlug as string, projectId as string) : null
);
const { data: pageDetails, error } = useSWR(
workspaceSlug && projectId && pageId ? PAGE_DETAILS(pageId as string) : null,
workspaceSlug && projectId
? () =>
pagesService.getPageDetails(
workspaceSlug as string,
projectId as string,
pageId as string
)
? () => pageService.getPageDetails(workspaceSlug as string, projectId as string, pageId as string)
: null
);
const { data: pageBlocks } = useSWR(
workspaceSlug && projectId && pageId ? PAGE_BLOCKS_LIST(pageId as string) : null,
workspaceSlug && projectId
? () =>
pagesService.listPageBlocks(
workspaceSlug as string,
projectId as string,
pageId as string
)
? () => pageService.listPageBlocks(workspaceSlug as string, projectId as string, pageId as string)
: null
);
const { data: labels } = useSWR<IIssueLabels[]>(
workspaceSlug && projectId ? PROJECT_ISSUE_LABELS(projectId as string) : null,
workspaceSlug && projectId
? () => issuesService.getIssueLabels(workspaceSlug as string, projectId as string)
? () => issueLabelService.getProjectIssueLabels(workspaceSlug as string, projectId as string)
: null
);
@ -134,7 +122,7 @@ const SinglePage: NextPage = () => {
if (!formData.name || formData.name.length === 0 || formData.name === "") return;
await pagesService
await pageService
.patchPage(workspaceSlug as string, projectId as string, pageId as string, formData, user)
.then(() => {
mutate<IPage>(
@ -161,7 +149,7 @@ const SinglePage: NextPage = () => {
false
);
await pagesService
await pageService
.patchPage(workspaceSlug as string, projectId as string, pageId as string, formData, user)
.then(() => {
mutate(PAGE_DETAILS(pageId as string));
@ -186,7 +174,7 @@ const SinglePage: NextPage = () => {
});
});
pagesService.addPageToFavorites(workspaceSlug as string, projectId as string, {
pageService.addPageToFavorites(workspaceSlug as string, projectId as string, {
page: pageId as string,
});
};
@ -209,11 +197,7 @@ const SinglePage: NextPage = () => {
});
});
pagesService.removePageFromFavorites(
workspaceSlug as string,
projectId as string,
pageId as string
);
pageService.removePageFromFavorites(workspaceSlug as string, projectId as string, pageId as string);
};
const handleOnDragEnd = (result: DropResult) => {
@ -228,15 +212,9 @@ const SinglePage: NextPage = () => {
newSortOrder = pageBlocks[pageBlocks.length - 1].sort_order + 10000;
else {
if (destination.index > source.index)
newSortOrder =
(pageBlocks[destination.index].sort_order +
pageBlocks[destination.index + 1].sort_order) /
2;
newSortOrder = (pageBlocks[destination.index].sort_order + pageBlocks[destination.index + 1].sort_order) / 2;
else if (destination.index < source.index)
newSortOrder =
(pageBlocks[destination.index - 1].sort_order +
pageBlocks[destination.index].sort_order) /
2;
newSortOrder = (pageBlocks[destination.index - 1].sort_order + pageBlocks[destination.index].sort_order) / 2;
}
const newBlocksList = pageBlocks.map((p) => ({
@ -249,7 +227,7 @@ const SinglePage: NextPage = () => {
false
);
pagesService.patchPageBlock(
pageService.patchPageBlock(
workspaceSlug as string,
projectId as string,
pageId as string,
@ -262,18 +240,15 @@ const SinglePage: NextPage = () => {
};
const handleCopyText = () => {
const originURL =
typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/pages/${pageId}`).then(
() => {
setToastAlert({
type: "success",
title: "Link Copied!",
message: "Page link copied to clipboard.",
});
}
);
copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/pages/${pageId}`).then(() => {
setToastAlert({
type: "success",
title: "Link Copied!",
message: "Page link copied to clipboard.",
});
});
};
const handleShowBlockToggle = async () => {
@ -288,9 +263,7 @@ const SinglePage: NextPage = () => {
};
mutate<IProjectMember>(
(workspaceSlug as string) && (projectId as string)
? USER_PROJECT_VIEW(projectId as string)
: null,
(workspaceSlug as string) && (projectId as string) ? USER_PROJECT_VIEW(projectId as string) : null,
(prevData) => {
if (!prevData) return prevData;
@ -302,15 +275,13 @@ const SinglePage: NextPage = () => {
false
);
await projectService
.setProjectView(workspaceSlug as string, projectId as string, payload)
.catch(() => {
setToastAlert({
type: "error",
title: "Error!",
message: "Something went wrong. Please try again.",
});
await projectService.setProjectView(workspaceSlug as string, projectId as string, payload).catch(() => {
setToastAlert({
type: "error",
title: "Error!",
message: "Something went wrong. Please try again.",
});
});
};
const options = labels?.map((label) => ({
@ -375,17 +346,22 @@ const SinglePage: NextPage = () => {
<ArrowLeftIcon className="h-4 w-4" />
</button>
<TextArea
id="name"
<Controller
name="name"
placeholder="Page Title"
value={watch("name")}
onBlur={handleSubmit(updatePage)}
onChange={(e) => setValue("name", e.target.value)}
required={true}
className="min-h-10 block w-full resize-none overflow-hidden rounded border-none bg-transparent px-3 py-2 text-xl font-semibold outline-none ring-0"
role="textbox"
noPadding
control={control}
render={({ field: { value, onChange } }) => (
<TextArea
id="name"
name="name"
value={watch("name")}
placeholder="Page Title"
onBlur={handleSubmit(updatePage)}
onChange={(e) => setValue("name", e.target.value)}
required={true}
className="min-h-10 block w-full resize-none overflow-hidden rounded border-none bg-transparent !px-3 !py-2 text-xl font-semibold outline-none ring-0"
role="textbox"
/>
)}
/>
</div>
@ -406,16 +382,13 @@ const SinglePage: NextPage = () => {
partialUpdatePage({ labels_list: updatedLabels });
}}
style={{
backgroundColor: `${
label?.color && label.color !== "" ? label.color : "#000000"
}20`,
backgroundColor: `${label?.color && label.color !== "" ? label.color : "#000000"}20`,
}}
>
<span
className="h-1.5 w-1.5 flex-shrink-0 rounded-full"
style={{
backgroundColor:
label?.color && label.color !== "" ? label.color : "#000000",
backgroundColor: label?.color && label.color !== "" ? label.color : "#000000",
}}
/>
{label.name}
@ -489,9 +462,7 @@ const SinglePage: NextPage = () => {
<Popover.Panel className="absolute right-0 z-30 mt-1 w-screen max-w-xs transform rounded-lg border border-custom-border-200 bg-custom-background-90 p-3 shadow-lg">
<div className="relative divide-y-2 divide-custom-border-200">
<div className="flex items-center justify-between">
<span className="text-sm text-custom-text-200">
Show full block content
</span>
<span className="text-sm text-custom-text-200">Show full block content</span>
<ToggleSwitch
value={showBlock}
onChange={(value) => {
@ -591,11 +562,7 @@ const SinglePage: NextPage = () => {
<LockClosedIcon className="h-4 w-4" />
</button>
) : (
<button
onClick={() => partialUpdatePage({ access: 1 })}
type="button"
className="z-10"
>
<button onClick={() => partialUpdatePage({ access: 1 })} type="button" className="z-10">
<LockOpenIcon className="h-4 w-4" />
</button>
)}
@ -646,11 +613,7 @@ const SinglePage: NextPage = () => {
</DragDropContext>
{createBlockForm && (
<div className="mt-4" ref={scrollToRef}>
<CreateUpdateBlockInline
handleClose={() => setCreateBlockForm(false)}
focus="name"
user={user}
/>
<CreateUpdateBlockInline handleClose={() => setCreateBlockForm(false)} focus="name" user={user} />
</div>
)}
{labelModal && typeof projectId === "string" && (

View file

@ -8,18 +8,18 @@ import useSWR from "swr";
// headless ui
import { Tab } from "@headlessui/react";
// services
import projectService from "services/project.service";
import { ProjectService } from "services/project";
// hooks
import useLocalStorage from "hooks/use-local-storage";
import useUserAuth from "hooks/use-user-auth";
// icons
import { PlusIcon } from "components/icons";
// layouts
import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
import { ProjectAuthorizationWrapper } from "layouts/auth-layout-legacy";
// components
import { RecentPagesList, CreateUpdatePageModal, TPagesListProps } from "components/pages";
// ui
import { PrimaryButton } from "components/ui";
import { Button } from "@plane/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// icons
import { ListBulletIcon, Squares2X2Icon } from "@heroicons/react/24/outline";
@ -31,36 +31,27 @@ import { PROJECT_DETAILS } from "constants/fetch-keys";
// helper
import { truncateText } from "helpers/string.helper";
const AllPagesList = dynamic<TPagesListProps>(
() => import("components/pages").then((a) => a.AllPagesList),
{
ssr: false,
}
);
const AllPagesList = dynamic<TPagesListProps>(() => import("components/pages").then((a) => a.AllPagesList), {
ssr: false,
});
const FavoritePagesList = dynamic<TPagesListProps>(
() => import("components/pages").then((a) => a.FavoritePagesList),
{
ssr: false,
}
);
const FavoritePagesList = dynamic<TPagesListProps>(() => import("components/pages").then((a) => a.FavoritePagesList), {
ssr: false,
});
const MyPagesList = dynamic<TPagesListProps>(
() => import("components/pages").then((a) => a.MyPagesList),
{
ssr: false,
}
);
const MyPagesList = dynamic<TPagesListProps>(() => import("components/pages").then((a) => a.MyPagesList), {
ssr: false,
});
const OtherPagesList = dynamic<TPagesListProps>(
() => import("components/pages").then((a) => a.OtherPagesList),
{
ssr: false,
}
);
const OtherPagesList = dynamic<TPagesListProps>(() => import("components/pages").then((a) => a.OtherPagesList), {
ssr: false,
});
const tabsList = ["Recent", "All", "Favorites", "Created by me", "Created by others"];
// services
const projectService = new ProjectService();
const ProjectPages: NextPage = () => {
const [createUpdatePageModal, setCreateUpdatePageModal] = useState(false);
@ -75,9 +66,7 @@ const ProjectPages: NextPage = () => {
const { data: projectDetails } = useSWR(
workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
workspaceSlug && projectId
? () => projectService.getProject(workspaceSlug as string, projectId as string)
: null
workspaceSlug && projectId ? () => projectService.getProject(workspaceSlug as string, projectId as string) : null
);
const currentTabValue = (tab: string | null) => {
@ -109,22 +98,20 @@ const ProjectPages: NextPage = () => {
breadcrumbs={
<Breadcrumbs>
<BreadcrumbItem title="Projects" link={`/${workspaceSlug}/projects`} />
<BreadcrumbItem
title={`${truncateText(projectDetails?.name ?? "Project", 32)} Pages`}
/>
<BreadcrumbItem title={`${truncateText(projectDetails?.name ?? "Project", 32)} Pages`} />
</Breadcrumbs>
}
right={
<PrimaryButton
className="flex items-center gap-2"
<Button
variant="primary"
prependIcon={<PlusIcon />}
onClick={() => {
const e = new KeyboardEvent("keydown", { key: "d" });
document.dispatchEvent(e);
}}
>
<PlusIcon className="h-4 w-4" />
Create Page
</PrimaryButton>
</Button>
}
>
<div className="space-y-5 p-8 h-full overflow-hidden flex flex-col">

View file

@ -5,9 +5,9 @@ import { useRouter } from "next/router";
import useSWR, { mutate } from "swr";
// services
import projectService from "services/project.service";
import { ProjectService } from "services/project";
// layouts
import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
import { ProjectAuthorizationWrapper } from "layouts/auth-layout-legacy";
// hooks
import useUserAuth from "hooks/use-user-auth";
import useProjectDetails from "hooks/use-project-details";
@ -25,6 +25,9 @@ import { PROJECTS_LIST, PROJECT_DETAILS, USER_PROJECT_VIEW } from "constants/fet
// helper
import { truncateText } from "helpers/string.helper";
// services
const projectService = new ProjectService();
const AutomationsSettings: NextPage = () => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
@ -52,8 +55,7 @@ const AutomationsSettings: NextPage = () => {
mutate<IProject[]>(
PROJECTS_LIST(workspaceSlug as string, { is_favorite: "all" }),
(prevData) =>
(prevData ?? []).map((p) => (p.id === projectDetails.id ? { ...p, ...formData } : p)),
(prevData) => (prevData ?? []).map((p) => (p.id === projectDetails.id ? { ...p, ...formData } : p)),
false
);
@ -92,16 +94,8 @@ const AutomationsSettings: NextPage = () => {
<div className="flex items-center py-3.5 border-b border-custom-border-200">
<h3 className="text-xl font-medium">Automations</h3>
</div>
<AutoArchiveAutomation
projectDetails={projectDetails}
handleChange={handleChange}
disabled={!isAdmin}
/>
<AutoCloseAutomation
projectDetails={projectDetails}
handleChange={handleChange}
disabled={!isAdmin}
/>
<AutoArchiveAutomation projectDetails={projectDetails} handleChange={handleChange} disabled={!isAdmin} />
<AutoCloseAutomation projectDetails={projectDetails} handleChange={handleChange} disabled={!isAdmin} />
</section>
</div>
</ProjectAuthorizationWrapper>

View file

@ -1,16 +1,12 @@
import React, { useState } from "react";
import { useRouter } from "next/router";
import useSWR, { mutate } from "swr";
// services
import estimatesService from "services/estimates.service";
import projectService from "services/project.service";
import { ProjectService, ProjectEstimateService } from "services/project";
// hooks
import useProjectDetails from "hooks/use-project-details";
// layouts
import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
import { ProjectAuthorizationWrapper } from "layouts/auth-layout-legacy";
// components
import { CreateUpdateEstimateModal, SingleEstimate } from "components/estimates";
import { SettingsSidebar } from "components/project";
@ -18,7 +14,8 @@ import { SettingsSidebar } from "components/project";
import useToast from "hooks/use-toast";
import useUserAuth from "hooks/use-user-auth";
// ui
import { EmptyState, Loader, PrimaryButton, SecondaryButton } from "components/ui";
import { Button, Loader } from "@plane/ui";
import { EmptyState } from "components/common";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// icons
import { PlusIcon } from "@heroicons/react/24/outline";
@ -32,6 +29,10 @@ import { ESTIMATES_LIST, PROJECT_DETAILS } from "constants/fetch-keys";
// helper
import { truncateText } from "helpers/string.helper";
// services
const projectService = new ProjectService();
const projectEstimateService = new ProjectEstimateService();
const EstimatesSettings: NextPage = () => {
const [estimateFormOpen, setEstimateFormOpen] = useState(false);
@ -49,7 +50,7 @@ const EstimatesSettings: NextPage = () => {
const { data: estimatesList } = useSWR<IEstimate[]>(
workspaceSlug && projectId ? ESTIMATES_LIST(projectId as string) : null,
workspaceSlug && projectId
? () => estimatesService.getEstimatesList(workspaceSlug as string, projectId as string)
? () => projectEstimateService.getEstimatesList(workspaceSlug as string, projectId as string)
: null
);
@ -67,15 +68,13 @@ const EstimatesSettings: NextPage = () => {
false
);
estimatesService
.deleteEstimate(workspaceSlug as string, projectId as string, estimateId, user)
.catch(() => {
setToastAlert({
type: "error",
title: "Error!",
message: "Error: Estimate could not be deleted. Please try again",
});
projectEstimateService.deleteEstimate(workspaceSlug as string, projectId as string, estimateId, user).catch(() => {
setToastAlert({
type: "error",
title: "Error!",
message: "Error: Estimate could not be deleted. Please try again",
});
});
};
const disableEstimates = () => {
@ -91,15 +90,13 @@ const EstimatesSettings: NextPage = () => {
false
);
projectService
.updateProject(workspaceSlug as string, projectId as string, { estimate: null }, user)
.catch(() =>
setToastAlert({
type: "error",
title: "Error!",
message: "Estimate could not be disabled. Please try again",
})
);
projectService.updateProject(workspaceSlug as string, projectId as string, { estimate: null }, user).catch(() =>
setToastAlert({
type: "error",
title: "Error!",
message: "Estimate could not be disabled. Please try again",
})
);
};
return (
@ -134,16 +131,19 @@ const EstimatesSettings: NextPage = () => {
<h3 className="text-xl font-medium">Estimates</h3>
<div className="col-span-12 space-y-5 sm:col-span-7">
<div className="flex items-center gap-2">
<PrimaryButton
<Button
variant="primary"
onClick={() => {
setEstimateToUpdate(undefined);
setEstimateFormOpen(true);
}}
>
Add Estimate
</PrimaryButton>
</Button>
{projectDetails?.estimate && (
<SecondaryButton onClick={disableEstimates}>Disable Estimates</SecondaryButton>
<Button variant="neutral-primary" onClick={disableEstimates}>
Disable Estimates
</Button>
)}
</div>
</div>

View file

@ -1,28 +1,25 @@
import React from "react";
import { useRouter } from "next/router";
import useSWR, { mutate } from "swr";
// services
import projectService from "services/project.service";
import trackEventServices, { MiscellaneousEventType } from "services/track-event.service";
import { ProjectService } from "services/project";
import { TrackEventService, MiscellaneousEventType } from "services/track_event.service";
// layouts
import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
import { ProjectAuthorizationWrapper } from "layouts/auth-layout-legacy";
// hooks
import useToast from "hooks/use-toast";
import useUserAuth from "hooks/use-user-auth";
// components
import { SettingsSidebar } from "components/project";
// ui
import { ToggleSwitch } from "components/ui";
import { ToggleSwitch } from "@plane/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// icons
import { ModuleIcon } from "components/icons";
import { FileText, Inbox, Layers } from "lucide-react";
import { ContrastOutlined } from "@mui/icons-material";
// types
import { IProject } from "types";
import { IProject, IUser } from "types";
import type { NextPage } from "next";
// fetch-keys
import { PROJECTS_LIST, PROJECT_DETAILS, USER_PROJECT_VIEW } from "constants/fetch-keys";
@ -32,39 +29,32 @@ import { truncateText } from "helpers/string.helper";
const featuresList = [
{
title: "Cycles",
description:
"Cycles are enabled for all the projects in this workspace. Access them from the sidebar.",
icon: (
<ContrastOutlined className="!text-base !leading-4 text-purple-500 flex-shrink-0 rotate-180" />
),
description: "Cycles are enabled for all the projects in this workspace. Access them from the sidebar.",
icon: <ContrastOutlined className="!text-base !leading-4 text-purple-500 flex-shrink-0 rotate-180" />,
property: "cycle_view",
},
{
title: "Modules",
description:
"Modules are enabled for all the projects in this workspace. Access it from the sidebar.",
description: "Modules are enabled for all the projects in this workspace. Access it from the sidebar.",
icon: <ModuleIcon width={16} height={16} className="flex-shrink-0" />,
property: "module_view",
},
{
title: "Views",
description:
"Views are enabled for all the projects in this workspace. Access it from the sidebar.",
description: "Views are enabled for all the projects in this workspace. Access it from the sidebar.",
icon: <Layers className="h-4 w-4 text-cyan-500 flex-shrink-0" />,
property: "issue_views_view",
},
{
title: "Pages",
description:
"Pages are enabled for all the projects in this workspace. Access it from the sidebar.",
description: "Pages are enabled for all the projects in this workspace. Access it from the sidebar.",
icon: <FileText className="h-4 w-4 text-red-400 flex-shrink-0" />,
property: "page_view",
},
{
title: "Inbox",
description:
"Inbox are enabled for all the projects in this workspace. Access it from the issues views page.",
description: "Inbox are enabled for all the projects in this workspace. Access it from the issues views page.",
icon: <Inbox className="h-4 w-4 text-fuchsia-500 flex-shrink-0" />,
property: "inbox_view",
},
@ -87,6 +77,10 @@ const getEventType = (feature: string, toggle: boolean): MiscellaneousEventType
}
};
// services
const projectService = new ProjectService();
const trackEventService = new TrackEventService();
const FeaturesSettings: NextPage = () => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
@ -97,9 +91,7 @@ const FeaturesSettings: NextPage = () => {
const { data: projectDetails } = useSWR(
workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
workspaceSlug && projectId
? () => projectService.getProject(workspaceSlug as string, projectId as string)
: null
workspaceSlug && projectId ? () => projectService.getProject(workspaceSlug as string, projectId as string) : null
);
const { data: memberDetails } = useSWR(
@ -136,15 +128,13 @@ const FeaturesSettings: NextPage = () => {
message: "Project feature updated successfully.",
});
await projectService
.updateProject(workspaceSlug as string, projectId as string, formData, user)
.catch(() =>
setToastAlert({
type: "error",
title: "Error!",
message: "Project feature could not be updated. Please try again.",
})
);
await projectService.updateProject(workspaceSlug as string, projectId as string, formData, user).catch(() =>
setToastAlert({
type: "error",
title: "Error!",
message: "Project feature could not be updated. Please try again.",
})
);
};
const isAdmin = memberDetails?.role === 20;
@ -182,15 +172,13 @@ const FeaturesSettings: NextPage = () => {
</div>
<div className="">
<h4 className="text-sm font-medium">{feature.title}</h4>
<p className="text-sm text-custom-text-200 tracking-tight">
{feature.description}
</p>
<p className="text-sm text-custom-text-200 tracking-tight">{feature.description}</p>
</div>
</div>
<ToggleSwitch
value={projectDetails?.[feature.property as keyof IProject]}
onChange={() => {
trackEventServices.trackMiscellaneousEvent(
trackEventService.trackMiscellaneousEvent(
{
workspaceId: (projectDetails?.workspace as any)?.id,
workspaceSlug,
@ -198,11 +186,8 @@ const FeaturesSettings: NextPage = () => {
projectIdentifier: projectDetails?.identifier,
projectName: projectDetails?.name,
},
getEventType(
feature.title,
!projectDetails?.[feature.property as keyof IProject]
),
user
getEventType(feature.title, !projectDetails?.[feature.property as keyof IProject]),
user as IUser
);
handleSubmit({
[feature.property]: !projectDetails?.[feature.property as keyof IProject],

View file

@ -1,71 +1,49 @@
import { useEffect, useState } from "react";
import { useState } from "react";
import { useRouter } from "next/router";
import useSWR, { mutate } from "swr";
// headless ui
import useSWR from "swr";
import { Disclosure, Transition } from "@headlessui/react";
// react-hook-form
import { Controller, useForm } from "react-hook-form";
// layouts
import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
import { ProjectAuthorizationWrapper } from "layouts/auth-layout-legacy";
// services
import projectService from "services/project.service";
import { ProjectService } from "services/project";
// components
import { DeleteProjectModal, SettingsSidebar } from "components/project";
import { ImagePickerPopover } from "components/core";
import EmojiIconPicker from "components/emoji-icon-picker";
// hooks
import useToast from "hooks/use-toast";
import useUserAuth from "hooks/use-user-auth";
// ui
import {
Input,
TextArea,
Loader,
CustomSelect,
DangerButton,
Icon,
PrimaryButton,
} from "components/ui";
import { DeleteProjectModal, ProjectDetailsForm, ProjectDetailsFormLoader, SettingsSidebar } from "components/project";
// components
import { Button, Loader } from "@plane/ui";
import { Icon } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// helpers
import { renderEmoji } from "helpers/emoji.helper";
import { truncateText } from "helpers/string.helper";
import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
// types
import { IProject, IWorkspace } from "types";
import type { NextPage } from "next";
// fetch-keys
import { PROJECTS_LIST, PROJECT_DETAILS, USER_PROJECT_VIEW } from "constants/fetch-keys";
// constants
import { NETWORK_CHOICES } from "constants/project";
import { USER_PROJECT_VIEW } from "constants/fetch-keys";
import { useMobxStore } from "lib/mobx/store-provider";
import { observer } from "mobx-react-lite";
const defaultValues: Partial<IProject> = {
name: "",
description: "",
identifier: "",
network: 0,
};
// services
const projectService = new ProjectService();
const GeneralSettings: NextPage = () => {
const GeneralSettings: NextPage = observer(() => {
const { project: projectStore } = useMobxStore();
// states
const [selectProject, setSelectedProject] = useState<string | null>(null);
const { user } = useUserAuth();
const { setToastAlert } = useToast();
// router
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { data: projectDetails } = useSWR<IProject>(
workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
// derived values
const projectDetails = projectId ? projectStore.project_details[projectId.toString()] : null;
console.log("projectDetails", projectDetails);
console.log("condition", workspaceSlug && projectId && !projectDetails);
console.log("wow", projectId);
// api call to fetch project details
useSWR(
workspaceSlug && projectId ? "PROJECT_DETAILS" : null,
workspaceSlug && projectId
? () => projectService.getProject(workspaceSlug as string, projectId as string)
? () => projectStore.fetchProjectDetails(workspaceSlug.toString(), projectId.toString())
: null
);
// API call to fetch user privileges
const { data: memberDetails } = useSWR(
workspaceSlug && projectId ? USER_PROJECT_VIEW(projectId.toString()) : null,
workspaceSlug && projectId
@ -73,101 +51,8 @@ const GeneralSettings: NextPage = () => {
: null
);
const {
register,
handleSubmit,
reset,
watch,
control,
setValue,
setError,
formState: { errors, isSubmitting },
} = useForm<IProject>({
defaultValues,
});
useEffect(() => {
if (projectDetails)
reset({
...projectDetails,
emoji_and_icon: projectDetails.emoji ?? projectDetails.icon_prop,
workspace: (projectDetails.workspace as IWorkspace).id,
});
}, [projectDetails, reset]);
const updateProject = async (payload: Partial<IProject>) => {
if (!workspaceSlug || !projectDetails) return;
await projectService
.updateProject(workspaceSlug as string, projectDetails.id, payload, user)
.then((res) => {
mutate<IProject>(
PROJECT_DETAILS(projectDetails.id),
(prevData) => ({ ...prevData, ...res }),
false
);
mutate(
PROJECTS_LIST(workspaceSlug as string, {
is_favorite: "all",
})
);
setToastAlert({
type: "success",
title: "Success!",
message: "Project updated successfully",
});
})
.catch(() => {
setToastAlert({
type: "error",
title: "Error!",
message: "Project could not be updated. Please try again.",
});
});
};
const onSubmit = async (formData: IProject) => {
if (!workspaceSlug || !projectDetails) return;
const payload: Partial<IProject> = {
name: formData.name,
network: formData.network,
identifier: formData.identifier,
description: formData.description,
cover_image: formData.cover_image,
};
if (typeof formData.emoji_and_icon === "object") {
payload.emoji = null;
payload.icon_prop = formData.emoji_and_icon;
} else {
payload.emoji = formData.emoji_and_icon;
payload.icon_prop = null;
}
if (projectDetails.identifier !== formData.identifier)
await projectService
.checkProjectIdentifierAvailability(workspaceSlug as string, payload.identifier ?? "")
.then(async (res) => {
if (res.exists) setError("identifier", { message: "Identifier already exists" });
else await updateProject(payload);
});
else await updateProject(payload);
};
const handleIdentifierChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const { value } = event.target;
const alphanumericValue = value.replace(/[^a-zA-Z0-9]/g, "");
const formattedValue = alphanumericValue.toUpperCase();
setValue("identifier", formattedValue);
};
const currentNetwork = NETWORK_CHOICES.find((n) => n.key === projectDetails?.network);
const selectedNetwork = NETWORK_CHOICES.find((n) => n.key === watch("network"));
// const currentNetwork = NETWORK_CHOICES.find((n) => n.key === projectDetails?.network);
// const selectedNetwork = NETWORK_CHOICES.find((n) => n.key === watch("network"));
const isAdmin = memberDetails?.role === 20;
@ -184,269 +69,78 @@ const GeneralSettings: NextPage = () => {
</Breadcrumbs>
}
>
<DeleteProjectModal
data={projectDetails ?? null}
isOpen={Boolean(selectProject)}
onClose={() => setSelectedProject(null)}
user={user}
/>
{projectDetails && (
<DeleteProjectModal
project={projectDetails}
isOpen={Boolean(selectProject)}
onClose={() => setSelectedProject(null)}
/>
)}
<div className="flex flex-row gap-2 h-full">
<div className="w-80 pt-8 overflow-y-hidden flex-shrink-0">
<SettingsSidebar />
</div>
<div className={`pr-9 py-8 w-full overflow-y-auto ${isAdmin ? "" : "opacity-60"}`}>
<form onSubmit={handleSubmit(onSubmit)}>
<div className="relative h-44 w-full mt-6">
<img
src={watch("cover_image")!}
alt={watch("cover_image")!}
className="h-44 w-full rounded-md object-cover"
/>
<div className="flex items-end justify-between gap-3 absolute bottom-4 w-full px-4">
<div className="flex gap-3 flex-grow truncate">
<div className="flex items-center justify-center flex-shrink-0 bg-custom-background-90 h-[52px] w-[52px] rounded-lg">
{projectDetails ? (
<div className="h-7 w-7 grid place-items-center">
<Controller
control={control}
name="emoji_and_icon"
render={({ field: { value, onChange } }) => (
<EmojiIconPicker
label={value ? renderEmoji(value) : "Icon"}
value={value}
onChange={onChange}
disabled={!isAdmin}
/>
{projectDetails && workspaceSlug ? (
<ProjectDetailsForm project={projectDetails} workspaceSlug={workspaceSlug.toString()} isAdmin={isAdmin} />
) : (
<ProjectDetailsFormLoader />
)}
{isAdmin && (
<Disclosure as="div" className="border-t border-custom-border-400">
{({ open }) => (
<div className="w-full">
<Disclosure.Button
as="button"
type="button"
className="flex items-center justify-between w-full py-4"
>
<span className="text-xl tracking-tight">Delete Project</span>
<Icon iconName={open ? "expand_less" : "expand_more"} className="!text-2xl" />
</Disclosure.Button>
<Transition
show={open}
enter="transition duration-100 ease-out"
enterFrom="transform opacity-0"
enterTo="transform opacity-100"
leave="transition duration-75 ease-out"
leaveFrom="transform opacity-100"
leaveTo="transform opacity-0"
>
<Disclosure.Panel>
<div className="flex flex-col gap-8">
<span className="text-sm tracking-tight">
The danger zone of the project delete page is a critical area that requires careful
consideration and attention. When deleting a project, all of the data and resources within
that project will be permanently removed and cannot be recovered.
</span>
<div>
{projectDetails ? (
<div>
<Button variant="danger" onClick={() => setSelectedProject(projectDetails.id ?? null)}>
Delete my project
</Button>
</div>
) : (
<Loader className="mt-2 w-full">
<Loader.Item height="38px" width="144px" />
</Loader>
)}
/>
</div>
) : (
<Loader>
<Loader.Item height="46px" width="46px" />
</Loader>
)}
</div>
<div className="flex flex-col gap-1 text-white truncate">
<span className="text-lg font-semibold truncate">{watch("name")}</span>
<span className="flex items-center gap-2 text-sm">
<span>
{watch("identifier")} . {currentNetwork?.label}
</span>
</span>
</div>
</div>
<div className="flex justify-center flex-shrink-0">
{projectDetails ? (
<div>
<Controller
control={control}
name="cover_image"
render={({ field: { value, onChange } }) => (
<ImagePickerPopover
label={"Change cover"}
onChange={(imageUrl) => {
setValue("cover_image", imageUrl);
}}
value={watch("cover_image")}
disabled={!isAdmin}
/>
)}
/>
</div>
) : (
<Loader>
<Loader.Item height="32px" width="108px" />
</Loader>
)}
</div>
</div>
</div>
<div className="flex flex-col gap-8 my-8">
<div className="flex flex-col gap-1">
<h4 className="text-sm">Project Name</h4>
{projectDetails ? (
<Input
id="name"
name="name"
error={errors.name}
register={register}
className="!p-3 rounded-md font-medium"
placeholder="Project Name"
validations={{
required: "Name is required",
}}
disabled={!isAdmin}
/>
) : (
<Loader>
<Loader.Item height="46px" width="100%" />
</Loader>
)}
</div>
<div className="flex flex-col gap-1">
<h4 className="text-sm">Description</h4>
{projectDetails ? (
<TextArea
id="description"
name="description"
error={errors.description}
register={register}
placeholder="Enter project description"
validations={{}}
className="min-h-[102px] text-sm"
disabled={!isAdmin}
/>
) : (
<Loader className="w-full">
<Loader.Item height="102px" width="full" />
</Loader>
)}
</div>
<div className="flex items-center justify-between gap-10 w-full">
<div className="flex flex-col gap-1 w-1/2">
<h4 className="text-sm">Identifier</h4>
{projectDetails ? (
<Input
id="identifier"
name="identifier"
error={errors.identifier}
register={register}
placeholder="Enter identifier"
onChange={handleIdentifierChange}
validations={{
required: "Identifier is required",
validate: (value) =>
/^[A-Z0-9]+$/.test(value.toUpperCase()) ||
"Identifier must be in uppercase.",
minLength: {
value: 1,
message: "Identifier must at least be of 1 character",
},
maxLength: {
value: 5,
message: "Identifier must at most be of 5 characters",
},
}}
disabled={!isAdmin}
/>
) : (
<Loader>
<Loader.Item height="36px" width="100%" />
</Loader>
)}
</div>
<div className="flex flex-col gap-1 w-1/2">
<h4 className="text-sm">Network</h4>
{projectDetails ? (
<Controller
name="network"
control={control}
render={({ field: { value, onChange } }) => (
<CustomSelect
value={value}
onChange={onChange}
label={selectedNetwork?.label ?? "Select network"}
className="!border-custom-border-200 !shadow-none"
input
disabled={!isAdmin}
>
{NETWORK_CHOICES.map((network) => (
<CustomSelect.Option key={network.key} value={network.key}>
{network.label}
</CustomSelect.Option>
))}
</CustomSelect>
)}
/>
) : (
<Loader className="w-full">
<Loader.Item height="46px" width="100%" />
</Loader>
)}
</div>
</div>
<div className="flex items-center justify-between py-2">
{projectDetails ? (
<>
<PrimaryButton type="submit" loading={isSubmitting} disabled={!isAdmin}>
{isSubmitting ? "Updating Project..." : "Update Project"}
</PrimaryButton>
<span className="text-sm text-custom-sidebar-text-400 italic">
Created on {renderShortDateWithYearFormat(projectDetails?.created_at)}
</span>
</>
) : (
<Loader className="mt-2 w-full">
<Loader.Item height="34px" width="100px" />
</Loader>
)}
</div>
</div>
{isAdmin && (
<Disclosure as="div" className="border-t border-custom-border-400">
{({ open }) => (
<div className="w-full">
<Disclosure.Button
as="button"
type="button"
className="flex items-center justify-between w-full py-4"
>
<span className="text-xl tracking-tight">Delete Project</span>
<Icon iconName={open ? "expand_less" : "expand_more"} className="!text-2xl" />
</Disclosure.Button>
<Transition
show={open}
enter="transition duration-100 ease-out"
enterFrom="transform opacity-0"
enterTo="transform opacity-100"
leave="transition duration-75 ease-out"
leaveFrom="transform opacity-100"
leaveTo="transform opacity-0"
>
<Disclosure.Panel>
<div className="flex flex-col gap-8">
<span className="text-sm tracking-tight">
The danger zone of the project delete page is a critical area that
requires careful consideration and attention. When deleting a project,
all of the data and resources within that project will be permanently
removed and cannot be recovered.
</span>
<div>
{projectDetails ? (
<div>
<DangerButton
onClick={() => setSelectedProject(projectDetails.id ?? null)}
className="!text-sm"
outline
>
Delete my project
</DangerButton>
</div>
) : (
<Loader className="mt-2 w-full">
<Loader.Item height="38px" width="144px" />
</Loader>
)}
</div>
</div>
</Disclosure.Panel>
</Transition>
</div>
)}
</Disclosure>
)}
</form>
</div>
</Disclosure.Panel>
</Transition>
</div>
)}
</Disclosure>
)}
</div>
</div>
</ProjectAuthorizationWrapper>
);
};
});
export default GeneralSettings;

View file

@ -5,17 +5,16 @@ import { useRouter } from "next/router";
import useSWR from "swr";
// layouts
import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
import { ProjectAuthorizationWrapper } from "layouts/auth-layout-legacy";
// services
import IntegrationService from "services/integration";
import projectService from "services/project.service";
import { IntegrationService } from "services/integrations";
import { ProjectService } from "services/project";
// components
import { SettingsSidebar, SingleIntegration } from "components/project";
// ui
import { EmptyState, IntegrationAndImportExportBanner, Loader } from "components/ui";
import { EmptyState } from "components/common";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// icons
import { PlusIcon, PuzzlePieceIcon } from "@heroicons/react/24/outline";
import { Loader } from "@plane/ui";
// images
import emptyIntegration from "public/empty-state/integration.svg";
// types
@ -26,23 +25,22 @@ import { PROJECT_DETAILS, USER_PROJECT_VIEW, WORKSPACE_INTEGRATIONS } from "cons
// helper
import { truncateText } from "helpers/string.helper";
// services
const integrationService = new IntegrationService();
const projectService = new ProjectService();
const ProjectIntegrations: NextPage = () => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { data: projectDetails } = useSWR<IProject>(
workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
workspaceSlug && projectId
? () => projectService.getProject(workspaceSlug as string, projectId as string)
: null
workspaceSlug && projectId ? () => projectService.getProject(workspaceSlug as string, projectId as string) : null
);
const { data: workspaceIntegrations } = useSWR(
workspaceSlug ? WORKSPACE_INTEGRATIONS(workspaceSlug as string) : null,
() =>
workspaceSlug
? IntegrationService.getWorkspaceIntegrationsList(workspaceSlug as string)
: null
() => (workspaceSlug ? integrationService.getWorkspaceIntegrationsList(workspaceSlug as string) : null)
);
const { data: memberDetails } = useSWR(
@ -79,10 +77,7 @@ const ProjectIntegrations: NextPage = () => {
workspaceIntegrations.length > 0 ? (
<div>
{workspaceIntegrations.map((integration) => (
<SingleIntegration
key={integration.integration_detail.id}
integration={integration}
/>
<SingleIntegration key={integration.integration_detail.id} integration={integration} />
))}
</div>
) : (

View file

@ -7,10 +7,10 @@ import useSWR from "swr";
// hooks
import useUserAuth from "hooks/use-user-auth";
// services
import projectService from "services/project.service";
import issuesService from "services/issues.service";
import { ProjectService } from "services/project";
import { IssueLabelService } from "services/issue";
// layouts
import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
import { ProjectAuthorizationWrapper } from "layouts/auth-layout-legacy";
// components
import {
CreateUpdateLabelInline,
@ -21,10 +21,9 @@ import {
} from "components/labels";
import { SettingsSidebar } from "components/project";
// ui
import { EmptyState, Loader, PrimaryButton } from "components/ui";
import { Button, Loader } from "@plane/ui";
import { EmptyState } from "components/common";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// icons
import { PlusIcon } from "@heroicons/react/24/outline";
// images
import emptyLabel from "public/empty-state/label.svg";
// types
@ -35,6 +34,10 @@ import { PROJECT_DETAILS, PROJECT_ISSUE_LABELS } from "constants/fetch-keys";
// helper
import { truncateText } from "helpers/string.helper";
// services
const projectService = new ProjectService();
const issueLabelService = new IssueLabelService();
const LabelsSettings: NextPage = () => {
// create/edit label form
const [labelForm, setLabelForm] = useState(false);
@ -59,15 +62,13 @@ const LabelsSettings: NextPage = () => {
const { data: projectDetails } = useSWR(
workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
workspaceSlug && projectId
? () => projectService.getProject(workspaceSlug as string, projectId as string)
: null
workspaceSlug && projectId ? () => projectService.getProject(workspaceSlug as string, projectId as string) : null
);
const { data: issueLabels } = useSWR(
workspaceSlug && projectId ? PROJECT_ISSUE_LABELS(projectId as string) : null,
workspaceSlug && projectId
? () => issuesService.getIssueLabels(workspaceSlug as string, projectId as string)
? () => issueLabelService.getProjectIssueLabels(workspaceSlug as string, projectId as string)
: null
);
@ -121,13 +122,9 @@ const LabelsSettings: NextPage = () => {
<div className="flex items-center justify-between pt-2 pb-3.5 border-b border-custom-border-200">
<h3 className="text-xl font-medium">Labels</h3>
<PrimaryButton
onClick={newLabel}
size="sm"
className="flex items-center justify-center"
>
<Button variant="primary" onClick={newLabel} size="sm">
Add label
</PrimaryButton>
</Button>
</div>
<div className="space-y-3 py-6 h-full w-full">
{labelForm && (

View file

@ -1,13 +1,10 @@
import { useState, useEffect } from "react";
import { useRouter } from "next/router";
import Link from "next/link";
import useSWR, { mutate } from "swr";
// services
import projectService from "services/project.service";
import workspaceService from "services/workspace.service";
import { ProjectService, ProjectInvitationService } from "services/project";
import { WorkspaceService } from "services/workspace.service";
// hooks
import useToast from "hooks/use-toast";
import useUser from "hooks/use-user";
@ -15,24 +12,17 @@ import useProjectMembers from "hooks/use-project-members";
import useProjectDetails from "hooks/use-project-details";
import { Controller, useForm } from "react-hook-form";
// layouts
import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
import { ProjectAuthorizationWrapper } from "layouts/auth-layout-legacy";
// components
import ConfirmProjectMemberRemove from "components/project/confirm-project-member-remove";
import SendProjectInvitationModal from "components/project/send-project-invitation-modal";
import { MemberSelect, SettingsSidebar } from "components/project";
// ui
import {
CustomMenu,
CustomSearchSelect,
CustomSelect,
Icon,
Loader,
PrimaryButton,
SecondaryButton,
} from "components/ui";
import { Button, Loader } from "@plane/ui";
import { CustomMenu, CustomSelect, Icon } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// icons
import { PlusIcon, XMarkIcon } from "@heroicons/react/24/outline";
import { XMarkIcon } from "@heroicons/react/24/outline";
// types
import type { NextPage } from "next";
import { IProject, IUserLite, IWorkspace } from "types";
@ -41,7 +31,6 @@ import {
PROJECTS_LIST,
PROJECT_DETAILS,
PROJECT_INVITATIONS_WITH_EMAIL,
PROJECT_MEMBERS,
PROJECT_MEMBERS_WITH_EMAIL,
USER_PROJECT_VIEW,
WORKSPACE_DETAILS,
@ -56,6 +45,11 @@ const defaultValues: Partial<IProject> = {
default_assignee: null,
};
// services
const projectService = new ProjectService();
const projectInvitationService = new ProjectInvitationService();
const workspaceService = new WorkspaceService();
const MembersSettings: NextPage = () => {
const [inviteModal, setInviteModal] = useState(false);
const [selectedRemoveMember, setSelectedRemoveMember] = useState<string | null>(null);
@ -74,41 +68,23 @@ const MembersSettings: NextPage = () => {
Boolean(workspaceSlug && projectId)
);
const {
handleSubmit,
reset,
control,
formState: { isSubmitting },
} = useForm<IProject>({ defaultValues });
const { reset, control } = useForm<IProject>({ defaultValues });
const { data: activeWorkspace } = useSWR(
workspaceSlug ? WORKSPACE_DETAILS(workspaceSlug as string) : null,
() => (workspaceSlug ? workspaceService.getWorkspace(workspaceSlug as string) : null)
);
const { data: people } = useSWR(
workspaceSlug && projectId ? PROJECT_MEMBERS(projectId as string) : null,
workspaceSlug && projectId
? () => projectService.projectMembers(workspaceSlug as string, projectId as string)
: null
const { data: activeWorkspace } = useSWR(workspaceSlug ? WORKSPACE_DETAILS(workspaceSlug as string) : null, () =>
workspaceSlug ? workspaceService.getWorkspace(workspaceSlug as string) : null
);
const { data: projectMembers, mutate: mutateMembers } = useSWR(
workspaceSlug && projectId
? PROJECT_MEMBERS_WITH_EMAIL(workspaceSlug.toString(), projectId.toString())
: null,
workspaceSlug && projectId ? PROJECT_MEMBERS_WITH_EMAIL(workspaceSlug.toString(), projectId.toString()) : null,
workspaceSlug && projectId
? () => projectService.projectMembersWithEmail(workspaceSlug as string, projectId as string)
: null
);
const { data: projectInvitations, mutate: mutateInvitations } = useSWR(
workspaceSlug && projectId ? PROJECT_INVITATIONS_WITH_EMAIL(workspaceSlug.toString(), projectId.toString()) : null,
workspaceSlug && projectId
? PROJECT_INVITATIONS_WITH_EMAIL(workspaceSlug.toString(), projectId.toString())
: null,
workspaceSlug && projectId
? () =>
projectService.projectInvitationsWithEmail(workspaceSlug as string, projectId as string)
? () => projectInvitationService.projectInvitationsWithEmail(workspaceSlug as string, projectId as string)
: null
);
@ -148,37 +124,37 @@ const MembersSettings: NextPage = () => {
const currentUser = projectMembers?.find((item) => item.member.id === user?.id);
const handleProjectInvitationSuccess = () => {};
// const handleProjectInvitationSuccess = () => {};
const onSubmit = async (formData: IProject) => {
if (!workspaceSlug || !projectId || !projectDetails) return;
// const onSubmit = async (formData: IProject) => {
// if (!workspaceSlug || !projectId || !projectDetails) return;
const payload: Partial<IProject> = {
default_assignee: formData.default_assignee,
project_lead: formData.project_lead === "none" ? null : formData.project_lead,
};
// const payload: Partial<IProject> = {
// default_assignee: formData.default_assignee,
// project_lead: formData.project_lead === "none" ? null : formData.project_lead,
// };
await projectService
.updateProject(workspaceSlug as string, projectId as string, payload, user)
.then((res) => {
mutate(PROJECT_DETAILS(projectId as string));
// await projectService
// .updateProject(workspaceSlug as string, projectId as string, payload, user)
// .then((res) => {
// mutate(PROJECT_DETAILS(projectId as string));
mutate(
PROJECTS_LIST(workspaceSlug as string, {
is_favorite: "all",
})
);
// mutate(
// PROJECTS_LIST(workspaceSlug as string, {
// is_favorite: "all",
// })
// );
setToastAlert({
title: "Success",
type: "success",
message: "Project updated successfully",
});
})
.catch((err) => {
console.log(err);
});
};
// setToastAlert({
// title: "Success",
// type: "success",
// message: "Project updated successfully",
// });
// })
// .catch((err) => {
// console.log(err);
// });
// };
useEffect(() => {
if (projectDetails)
@ -200,7 +176,7 @@ const MembersSettings: NextPage = () => {
await projectService
.updateProject(workspaceSlug as string, projectId as string, payload, user)
.then((res) => {
.then(() => {
mutate(PROJECT_DETAILS(projectId as string));
mutate(
@ -241,31 +217,21 @@ const MembersSettings: NextPage = () => {
setSelectedRemoveMember(null);
setSelectedInviteRemoveMember(null);
}}
data={members.find(
(item) => item.id === selectedRemoveMember || item.id === selectedInviteRemoveMember
)}
data={members.find((item) => item.id === selectedRemoveMember || item.id === selectedInviteRemoveMember)}
handleDelete={async () => {
if (!activeWorkspace || !projectDetails) return;
if (selectedRemoveMember) {
await projectService.deleteProjectMember(
activeWorkspace.slug,
projectDetails.id,
selectedRemoveMember
);
mutateMembers(
(prevData: any) => prevData?.filter((item: any) => item.id !== selectedRemoveMember),
false
);
await projectService.deleteProjectMember(activeWorkspace.slug, projectDetails.id, selectedRemoveMember);
mutateMembers((prevData: any) => prevData?.filter((item: any) => item.id !== selectedRemoveMember), false);
}
if (selectedInviteRemoveMember) {
await projectService.deleteProjectInvitation(
await projectInvitationService.deleteProjectInvitation(
activeWorkspace.slug,
projectDetails.id,
selectedInviteRemoveMember
);
mutateInvitations(
(prevData: any) =>
prevData?.filter((item: any) => item.id !== selectedInviteRemoveMember),
(prevData: any) => prevData?.filter((item: any) => item.id !== selectedInviteRemoveMember),
false
);
}
@ -347,7 +313,9 @@ const MembersSettings: NextPage = () => {
<div className="flex items-center justify-between gap-4 py-3.5 border-b border-custom-border-200">
<h4 className="text-xl font-medium">Members</h4>
<PrimaryButton onClick={() => setInviteModal(true)}>Add Member</PrimaryButton>
<Button variant="primary" onClick={() => setInviteModal(true)}>
Add Member
</Button>
</div>
{!projectMembers || !projectInvitations ? (
<Loader className="space-y-5">
@ -360,10 +328,7 @@ const MembersSettings: NextPage = () => {
<div className="divide-y divide-custom-border-200">
{members.length > 0
? members.map((member) => (
<div
key={member.id}
className="flex items-center justify-between px-3.5 py-[18px]"
>
<div key={member.id} className="flex items-center justify-between px-3.5 py-[18px]">
<div className="flex items-center gap-x-6 gap-y-2">
{member.avatar && member.avatar !== "" ? (
<div className="relative flex h-10 w-10 items-center justify-center rounded-lg p-4 capitalize text-white">
@ -389,19 +354,13 @@ const MembersSettings: NextPage = () => {
<span>
{member.first_name} {member.last_name}
</span>
<span className="text-custom-text-300 text-sm ml-2">
({member.display_name})
</span>
<span className="text-custom-text-300 text-sm ml-2">({member.display_name})</span>
</a>
</Link>
) : (
<h4 className="text-sm">{member.display_name || member.email}</h4>
)}
{isOwner && (
<p className="mt-0.5 text-xs text-custom-sidebar-text-300">
{member.email}
</p>
)}
{isOwner && <p className="mt-0.5 text-xs text-custom-sidebar-text-300">{member.email}</p>}
</div>
</div>
<div className="flex items-center gap-3 text-xs">
@ -431,45 +390,30 @@ const MembersSettings: NextPage = () => {
mutateMembers(
(prevData: any) =>
prevData.map((m: any) =>
m.id === member.id ? { ...m, role: value } : m
),
prevData.map((m: any) => (m.id === member.id ? { ...m, role: value } : m)),
false
);
projectService
.updateProjectMember(
activeWorkspace.slug,
projectDetails.id,
member.id,
{
role: value,
}
)
.updateProjectMember(activeWorkspace.slug, projectDetails.id, member.id, {
role: value,
})
.catch(() => {
setToastAlert({
type: "error",
title: "Error!",
message:
"An error occurred while updating member role. Please try again.",
message: "An error occurred while updating member role. Please try again.",
});
});
}}
disabled={
member.memberId === user?.id ||
!member.member ||
(currentUser &&
currentUser.role !== 20 &&
currentUser.role < member.role)
(currentUser && currentUser.role !== 20 && currentUser.role < member.role)
}
>
{Object.keys(ROLE).map((key) => {
if (
currentUser &&
currentUser.role !== 20 &&
currentUser.role < parseInt(key)
)
return null;
if (currentUser && currentUser.role !== 20 && currentUser.role < parseInt(key)) return null;
return (
<CustomSelect.Option key={key} value={key}>
@ -488,10 +432,7 @@ const MembersSettings: NextPage = () => {
<span className="flex items-center justify-start gap-2">
<XMarkIcon className="h-4 w-4" />
<span>
{" "}
{member.memberId !== user?.id ? "Remove member" : "Leave project"}
</span>
<span> {member.memberId !== user?.id ? "Remove member" : "Leave project"}</span>
</span>
</CustomMenu.MenuItem>
</CustomMenu>

View file

@ -5,22 +5,17 @@ import { useRouter } from "next/router";
import useSWR from "swr";
// services
import stateService from "services/state.service";
import { ProjectStateService } from "services/project";
// hooks
import useProjectDetails from "hooks/use-project-details";
import useUserAuth from "hooks/use-user-auth";
// layouts
import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
import { ProjectAuthorizationWrapper } from "layouts/auth-layout-legacy";
// components
import {
CreateUpdateStateInline,
DeleteStateModal,
SingleState,
StateGroup,
} from "components/states";
import { CreateUpdateStateInline, DeleteStateModal, SingleState, StateGroup } from "components/states";
import { SettingsSidebar } from "components/project";
// ui
import { Loader } from "components/ui";
import { Loader } from "@plane/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// icons
import { PlusIcon } from "@heroicons/react/24/outline";
@ -32,6 +27,9 @@ import type { NextPage } from "next";
// fetch-keys
import { STATES_LIST } from "constants/fetch-keys";
// services
const projectStateService = new ProjectStateService();
const StatesSettings: NextPage = () => {
const [activeGroup, setActiveGroup] = useState<StateGroup>(null);
const [selectedState, setSelectedState] = useState<string | null>(null);
@ -47,7 +45,7 @@ const StatesSettings: NextPage = () => {
const { data: states } = useSWR(
workspaceSlug && projectId ? STATES_LIST(projectId as string) : null,
workspaceSlug && projectId
? () => stateService.getStates(workspaceSlug as string, projectId as string)
? () => projectStateService.getStates(workspaceSlug as string, projectId as string)
: null
);
const orderedStateGroups = orderStateGroups(states);
@ -88,9 +86,7 @@ const StatesSettings: NextPage = () => {
return (
<div key={key} className="flex flex-col gap-2">
<div className="flex w-full justify-between">
<h4 className="text-base font-medium text-custom-text-200 capitalize">
{key}
</h4>
<h4 className="text-base font-medium text-custom-text-200 capitalize">{key}</h4>
<button
type="button"
className="flex items-center gap-2 text-custom-primary-100 px-2 hover:text-custom-primary-200 outline-none"
@ -124,19 +120,14 @@ const StatesSettings: NextPage = () => {
user={user}
/>
) : (
<div
className="border-b border-custom-border-200 last:border-b-0"
key={state.id}
>
<div className="border-b border-custom-border-200 last:border-b-0" key={state.id}>
<CreateUpdateStateInline
onClose={() => {
setActiveGroup(null);
setSelectedState(null);
}}
groupLength={orderedStateGroups[key].length}
data={
statesList?.find((state) => state.id === selectedState) ?? null
}
data={statesList?.find((state) => state.id === selectedState) ?? null}
selectedGroup={key as keyof StateGroup}
user={user}
/>

View file

@ -3,19 +3,17 @@ import { useRouter } from "next/router";
import useSWR from "swr";
// services
import projectService from "services/project.service";
import viewsService from "services/views.service";
import { ProjectService } from "services/project";
import { ViewService } from "services/view.service";
// layouts
import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
// contexts
import { IssueViewContextProvider } from "contexts/issue-view.context";
import { ProjectAuthorizationWrapper } from "layouts/auth-layout-legacy";
// components
import { IssuesFilterView, IssuesView } from "components/core";
import { ProjectViewLayoutRoot } from "components/issues";
// ui
import { CustomMenu, EmptyState, PrimaryButton } from "components/ui";
import { CustomMenu } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
import { EmptyState } from "components/common";
// icons
import { PlusIcon } from "@heroicons/react/24/outline";
import { StackedLayersIcon } from "components/icons";
// images
import emptyView from "public/empty-state/view.svg";
@ -23,6 +21,11 @@ import emptyView from "public/empty-state/view.svg";
import { truncateText } from "helpers/string.helper";
// fetch-keys
import { PROJECT_DETAILS, VIEWS_LIST, VIEW_DETAILS } from "constants/fetch-keys";
import { ProjectViewIssuesHeader } from "components/headers";
// services
const projectService = new ProjectService();
const viewService = new ViewService();
const SingleView: React.FC = () => {
const router = useRouter();
@ -30,96 +33,69 @@ const SingleView: React.FC = () => {
const { data: activeProject } = useSWR(
workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
workspaceSlug && projectId
? () => projectService.getProject(workspaceSlug as string, projectId as string)
: null
workspaceSlug && projectId ? () => projectService.getProject(workspaceSlug as string, projectId as string) : null
);
const { data: views } = useSWR(
workspaceSlug && projectId ? VIEWS_LIST(projectId as string) : null,
workspaceSlug && projectId
? () => viewsService.getViews(workspaceSlug as string, projectId as string)
: null
workspaceSlug && projectId ? () => viewService.getViews(workspaceSlug as string, projectId as string) : null
);
const { data: viewDetails, error } = useSWR(
workspaceSlug && projectId && viewId ? VIEW_DETAILS(viewId as string) : null,
workspaceSlug && projectId && viewId
? () =>
viewsService.getViewDetails(
workspaceSlug as string,
projectId as string,
viewId as string
)
? () => viewService.getViewDetails(workspaceSlug as string, projectId as string, viewId as string)
: null
);
return (
<IssueViewContextProvider>
<ProjectAuthorizationWrapper
breadcrumbs={
<Breadcrumbs>
<BreadcrumbItem
title={`${activeProject?.name ?? "Project"} Views`}
link={`/${workspaceSlug}/projects/${activeProject?.id}/cycles`}
/>
</Breadcrumbs>
}
left={
<CustomMenu
label={
<>
<StackedLayersIcon height={12} width={12} />
{viewDetails?.name && truncateText(viewDetails.name, 40)}
</>
}
className="ml-1.5"
width="auto"
>
{views?.map((view) => (
<CustomMenu.MenuItem
key={view.id}
renderAs="a"
href={`/${workspaceSlug}/projects/${projectId}/views/${view.id}`}
>
{truncateText(view.name, 40)}
</CustomMenu.MenuItem>
))}
</CustomMenu>
}
right={
<div className="flex items-center gap-2">
<IssuesFilterView />
<PrimaryButton
className="flex items-center gap-2"
onClick={() => {
const e = new KeyboardEvent("keydown", { key: "c" });
document.dispatchEvent(e);
}}
>
<PlusIcon className="h-4 w-4" />
Add Issue
</PrimaryButton>
</div>
}
>
{error ? (
<EmptyState
image={emptyView}
title="View does not exist"
description="The view you are looking for does not exist or has been deleted."
primaryButton={{
text: "View other views",
onClick: () => router.push(`/${workspaceSlug}/projects/${projectId}/views`),
}}
<ProjectAuthorizationWrapper
breadcrumbs={
<Breadcrumbs>
<BreadcrumbItem
title={`${activeProject?.name ?? "Project"} Views`}
link={`/${workspaceSlug}/projects/${activeProject?.id}/cycles`}
/>
) : (
<div className="h-full w-full flex flex-col">
<IssuesView />
</div>
)}
</ProjectAuthorizationWrapper>
</IssueViewContextProvider>
</Breadcrumbs>
}
left={
<CustomMenu
label={
<>
<StackedLayersIcon height={12} width={12} />
{viewDetails?.name && truncateText(viewDetails.name, 40)}
</>
}
className="ml-1.5"
width="auto"
>
{views?.map((view) => (
<CustomMenu.MenuItem
key={view.id}
renderAs="a"
href={`/${workspaceSlug}/projects/${projectId}/views/${view.id}`}
>
{truncateText(view.name, 40)}
</CustomMenu.MenuItem>
))}
</CustomMenu>
}
right={<ProjectViewIssuesHeader />}
>
{error ? (
<EmptyState
image={emptyView}
title="View does not exist"
description="The view you are looking for does not exist or has been deleted."
primaryButton={{
text: "View other views",
onClick: () => router.push(`/${workspaceSlug}/projects/${projectId}/views`),
}}
/>
) : (
<ProjectViewLayoutRoot />
)}
</ProjectAuthorizationWrapper>
);
};

View file

@ -1,142 +1,58 @@
import React, { useState } from "react";
import React from "react";
import { useRouter } from "next/router";
import useSWR from "swr";
// hooks
import useUserAuth from "hooks/use-user-auth";
// services
import viewsService from "services/views.service";
import projectService from "services/project.service";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// layouts
import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
import { ProjectAuthorizationWrapper } from "layouts/auth-layout-legacy";
// components
import { ProjectViewsHeader } from "components/headers";
// ui
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
//icons
import { PlusIcon } from "components/icons";
// images
import emptyView from "public/empty-state/view.svg";
// fetching keys
import { PROJECT_DETAILS, VIEWS_LIST } from "constants/fetch-keys";
// components
import { PrimaryButton, Loader, EmptyState } from "components/ui";
import { DeleteViewModal, CreateUpdateViewModal, SingleViewItem } from "components/views";
import { ProjectViewsList } from "components/views";
// types
import { IView } from "types";
import type { NextPage } from "next";
const ProjectViews: NextPage = () => {
const [createUpdateViewModal, setCreateUpdateViewModal] = useState(false);
const [selectedViewToUpdate, setSelectedViewToUpdate] = useState<IView | null>(null);
const [deleteViewModal, setDeleteViewModal] = useState(false);
const [selectedViewToDelete, setSelectedViewToDelete] = useState<IView | null>(null);
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { user } = useUserAuth();
const { project: projectStore, projectViews: projectViewsStore } = useMobxStore();
const { data: activeProject } = useSWR(
workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
useSWR(
workspaceSlug && projectId ? `PROJECT_DETAILS_${projectId.toString()}` : null,
workspaceSlug && projectId
? () => projectService.getProject(workspaceSlug as string, projectId as string)
? () => projectStore.fetchProjectDetails(workspaceSlug.toString(), projectId.toString())
: null
);
const { data: views } = useSWR(
workspaceSlug && projectId ? VIEWS_LIST(projectId as string) : null,
useSWR(
workspaceSlug && projectId ? `PROJECT_VIEWS_LIST_${workspaceSlug.toString()}_${projectId.toString()}` : null,
workspaceSlug && projectId
? () => viewsService.getViews(workspaceSlug as string, projectId as string)
? () => projectViewsStore.fetchAllViews(workspaceSlug.toString(), projectId.toString())
: null
);
const handleEditView = (view: IView) => {
setSelectedViewToUpdate(view);
setCreateUpdateViewModal(true);
};
const handleDeleteView = (view: IView) => {
setSelectedViewToDelete(view);
setDeleteViewModal(true);
};
const projectDetails =
workspaceSlug && projectId
? projectStore.getProjectById(workspaceSlug.toString(), projectId.toString())
: undefined;
return (
<ProjectAuthorizationWrapper
breadcrumbs={
<Breadcrumbs>
<BreadcrumbItem title="Projects" link={`/${workspaceSlug}/projects`} />
<BreadcrumbItem title={`${activeProject?.name ?? "Project"} Views`} />
<BreadcrumbItem title={`${projectDetails?.name ?? "Project"} Views`} />
</Breadcrumbs>
}
right={
<div className="flex items-center gap-2">
<PrimaryButton
type="button"
className="flex items-center gap-2"
onClick={() => {
const e = new KeyboardEvent("keydown", { key: "v" });
document.dispatchEvent(e);
}}
>
<PlusIcon className="h-4 w-4" />
Create View
</PrimaryButton>
</div>
}
right={<ProjectViewsHeader />}
>
<CreateUpdateViewModal
isOpen={createUpdateViewModal}
handleClose={() => setCreateUpdateViewModal(false)}
data={selectedViewToUpdate}
user={user}
/>
<DeleteViewModal
isOpen={deleteViewModal}
data={selectedViewToDelete}
setIsOpen={setDeleteViewModal}
user={user}
/>
{views ? (
views.length > 0 ? (
<div className="space-y-5 p-8">
<h3 className="text-2xl font-semibold text-custom-text-100">Views</h3>
<div className="divide-y divide-custom-border-200 rounded-[10px] border border-custom-border-200">
{views.map((view) => (
<SingleViewItem
key={view.id}
view={view}
handleEditView={() => handleEditView(view)}
handleDeleteView={() => handleDeleteView(view)}
/>
))}
</div>
</div>
) : (
<EmptyState
title="Get focused with views"
description="Views aid in saving your issues by applying various filters and grouping options."
image={emptyView}
primaryButton={{
icon: <PlusIcon className="h-4 w-4" />,
text: "New View",
onClick: () => {
const e = new KeyboardEvent("keydown", {
key: "v",
});
document.dispatchEvent(e);
},
}}
/>
)
) : (
<Loader className="space-y-3 p-8">
<Loader.Item height="30px" />
<Loader.Item height="30px" />
<Loader.Item height="30px" />
</Loader>
)}
<ProjectViewsList />
</ProjectAuthorizationWrapper>
);
};

View file

@ -1,173 +1,20 @@
import React, { useState } from "react";
import { useRouter } from "next/router";
import { mutate } from "swr";
// services
import projectService from "services/project.service";
// hooks
import useProjects from "hooks/use-projects";
import useWorkspaces from "hooks/use-workspaces";
import useUserAuth from "hooks/use-user-auth";
// layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
// components
import { JoinProjectModal } from "components/project/join-project-modal";
import { DeleteProjectModal, SingleProjectCard } from "components/project";
// ui
import { EmptyState, Icon, Loader, PrimaryButton } from "components/ui";
import { Breadcrumbs, BreadcrumbItem } from "components/breadcrumbs";
// icons
import { PlusIcon } from "@heroicons/react/24/outline";
// images
import emptyProject from "public/empty-state/project.svg";
// types
import type { NextPage } from "next";
// fetch-keys
import { PROJECT_MEMBERS } from "constants/fetch-keys";
// helper
import { truncateText } from "helpers/string.helper";
// types
import { IProject } from "types";
// components
import { ProjectCardList } from "components/project";
import { ProjectsHeader } from "components/headers";
// layouts
import { AppLayout } from "layouts/app-layout";
const ProjectsPage: NextPage = () => {
// router
const router = useRouter();
const { workspaceSlug } = router.query;
const [query, setQuery] = useState("");
const { user } = useUserAuth();
// context data
const { activeWorkspace } = useWorkspaces();
const { projects, mutateProjects } = useProjects();
// states
const [deleteProject, setDeleteProject] = useState<string | null>(null);
const [selectedProjectToJoin, setSelectedProjectToJoin] = useState<string | null>(null);
const filteredProjectList =
query === ""
? projects
: projects?.filter(
(project) =>
project.name.toLowerCase().includes(query.toLowerCase()) ||
project.identifier.toLowerCase().includes(query.toLowerCase())
);
return (
<WorkspaceAuthorizationLayout
breadcrumbs={
<Breadcrumbs>
<BreadcrumbItem
title={`${truncateText(activeWorkspace?.name ?? "Workspace", 32)} Projects`}
unshrinkTitle={false}
/>
</Breadcrumbs>
}
right={
<div className="flex items-center gap-3">
<div className="flex w-full gap-1 items-center justify-start rounded-md px-2 py-1.5 border border-custom-border-300 bg-custom-background-90">
<Icon iconName="search" className="!text-xl !leading-5 !text-custom-sidebar-text-400" />
<input
className="w-full border-none bg-transparent text-xs text-custom-text-200 focus:outline-none"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search"
/>
</div>
<PrimaryButton
className="flex items-center gap-2 flex-shrink-0"
onClick={() => {
const e = new KeyboardEvent("keydown", { key: "p" });
document.dispatchEvent(e);
}}
>
<PlusIcon className="h-4 w-4" />
Add Project
</PrimaryButton>
</div>
}
>
<JoinProjectModal
data={projects?.find((item) => item.id === selectedProjectToJoin)}
onClose={() => setSelectedProjectToJoin(null)}
onJoin={async () => {
const project = projects?.find((item) => item.id === selectedProjectToJoin);
if (!project) return;
await projectService
.joinProject(workspaceSlug as string, {
project_ids: [project.id],
})
.then(async () => {
mutate(PROJECT_MEMBERS(project.id));
mutateProjects<IProject[]>(
(prevData) =>
(prevData ?? []).map((p) => ({
...p,
is_member: p.id === project.id ? true : p.is_member,
})),
false
);
setSelectedProjectToJoin(null);
})
.catch(() => {
setSelectedProjectToJoin(null);
});
}}
/>
<DeleteProjectModal
isOpen={!!deleteProject}
onClose={() => setDeleteProject(null)}
data={projects?.find((item) => item.id === deleteProject) ?? null}
user={user}
/>
{filteredProjectList ? (
<div className="h-full w-full overflow-hidden">
{filteredProjectList.length > 0 ? (
<div className="h-full p-8 overflow-y-auto">
<div className="grid grid-cols-1 gap-9 md:grid-cols-2 lg:grid-cols-3">
{filteredProjectList.map((project) => (
<SingleProjectCard
key={project.id}
project={project}
setToJoinProject={setSelectedProjectToJoin}
setDeleteProject={setDeleteProject}
/>
))}
</div>
</div>
) : (
<EmptyState
image={emptyProject}
title="No projects yet"
description="Get started by creating your first project"
primaryButton={{
icon: <PlusIcon className="h-4 w-4" />,
text: "New Project",
onClick: () => {
const e = new KeyboardEvent("keydown", {
key: "p",
});
document.dispatchEvent(e);
},
}}
/>
)}
</div>
) : (
<Loader className="grid grid-cols-3 gap-4">
<Loader.Item height="100px" />
<Loader.Item height="100px" />
<Loader.Item height="100px" />
<Loader.Item height="100px" />
<Loader.Item height="100px" />
<Loader.Item height="100px" />
</Loader>
)}
</WorkspaceAuthorizationLayout>
<AppLayout header={<ProjectsHeader />}>
<>{workspaceSlug && <ProjectCardList workspaceSlug={workspaceSlug.toString()} />}</>
</AppLayout>
);
};

View file

@ -5,13 +5,13 @@ import { useRouter } from "next/router";
import useSWR from "swr";
// services
import workspaceService from "services/workspace.service";
import { WorkspaceService } from "services/workspace.service";
// layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout-legacy";
// component
import { SettingsSidebar } from "components/project";
// ui
import { SecondaryButton } from "components/ui";
import { Button } from "@plane/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// types
import type { NextPage } from "next";
@ -20,14 +20,16 @@ import { WORKSPACE_DETAILS } from "constants/fetch-keys";
// helper
import { truncateText } from "helpers/string.helper";
// services
const workspaceService = new WorkspaceService();
const BillingSettings: NextPage = () => {
const {
query: { workspaceSlug },
} = useRouter();
const { data: activeWorkspace } = useSWR(
workspaceSlug ? WORKSPACE_DETAILS(workspaceSlug as string) : null,
() => (workspaceSlug ? workspaceService.getWorkspace(workspaceSlug as string) : null)
const { data: activeWorkspace } = useSWR(workspaceSlug ? WORKSPACE_DETAILS(workspaceSlug as string) : null, () =>
workspaceSlug ? workspaceService.getWorkspace(workspaceSlug as string) : null
);
return (
@ -56,11 +58,9 @@ const BillingSettings: NextPage = () => {
<div className="px-4 py-6">
<div>
<h4 className="text-md mb-1 leading-6">Current plan</h4>
<p className="mb-3 text-sm text-custom-text-200">
You are currently using the free plan
</p>
<p className="mb-3 text-sm text-custom-text-200">You are currently using the free plan</p>
<a href="https://plane.so/pricing" target="_blank" rel="noreferrer">
<SecondaryButton outline>View Plans</SecondaryButton>
<Button variant="neutral-primary">View Plans</Button>
</a>
</div>
</div>

View file

@ -3,9 +3,9 @@ import { useRouter } from "next/router";
import useSWR from "swr";
// services
import workspaceService from "services/workspace.service";
import { WorkspaceService } from "services/workspace.service";
// layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout-legacy";
// components
import ExportGuide from "components/exporter/guide";
import { SettingsSidebar } from "components/project";
@ -18,13 +18,15 @@ import { WORKSPACE_DETAILS } from "constants/fetch-keys";
// helper
import { truncateText } from "helpers/string.helper";
// services
const workspaceService = new WorkspaceService();
const ImportExport: NextPage = () => {
const router = useRouter();
const { workspaceSlug } = router.query;
const { data: activeWorkspace } = useSWR(
workspaceSlug ? WORKSPACE_DETAILS(workspaceSlug as string) : null,
() => (workspaceSlug ? workspaceService.getWorkspace(workspaceSlug as string) : null)
const { data: activeWorkspace } = useSWR(workspaceSlug ? WORKSPACE_DETAILS(workspaceSlug as string) : null, () =>
workspaceSlug ? workspaceService.getWorkspace(workspaceSlug as string) : null
);
return (

View file

@ -3,9 +3,9 @@ import { useRouter } from "next/router";
import useSWR from "swr";
// services
import workspaceService from "services/workspace.service";
import { WorkspaceService } from "services/workspace.service";
// layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout-legacy";
// components
import IntegrationGuide from "components/integration/guide";
import { SettingsSidebar } from "components/project";
@ -18,13 +18,15 @@ import { WORKSPACE_DETAILS } from "constants/fetch-keys";
// helper
import { truncateText } from "helpers/string.helper";
// services
const workspaceService = new WorkspaceService();
const ImportExport: NextPage = () => {
const router = useRouter();
const { workspaceSlug } = router.query;
const { data: activeWorkspace } = useSWR(
workspaceSlug ? WORKSPACE_DETAILS(workspaceSlug as string) : null,
() => (workspaceSlug ? workspaceService.getWorkspace(workspaceSlug as string) : null)
const { data: activeWorkspace } = useSWR(workspaceSlug ? WORKSPACE_DETAILS(workspaceSlug as string) : null, () =>
workspaceSlug ? workspaceService.getWorkspace(workspaceSlug as string) : null
);
return (

View file

@ -7,20 +7,21 @@ import useSWR, { mutate } from "swr";
// react-hook-form
import { Controller, useForm } from "react-hook-form";
// services
import workspaceService from "services/workspace.service";
import fileService from "services/file.service";
import { WorkspaceService } from "services/workspace.service";
import { FileService } from "services/file.service";
// hooks
import useToast from "hooks/use-toast";
import useUserAuth from "hooks/use-user-auth";
// layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout-legacy";
// components
import { ImageUploadModal } from "components/core";
import { DeleteWorkspaceModal } from "components/workspace";
import { SettingsSidebar } from "components/project";
// ui
import { Disclosure, Transition } from "@headlessui/react";
import { Spinner, Input, CustomSelect, DangerButton, PrimaryButton, Icon } from "components/ui";
import { CustomSelect, Icon } from "components/ui";
import { Button, Input, Spinner } from "@plane/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// icons
import { Pencil } from "lucide-react";
@ -41,6 +42,10 @@ const defaultValues: Partial<IWorkspace> = {
logo: null,
};
// services
const workspaceService = new WorkspaceService();
const fileService = new FileService();
const WorkspaceSettings: NextPage = () => {
const [isOpen, setIsOpen] = useState(false);
const [isImageUploading, setIsImageUploading] = useState(false);
@ -59,9 +64,8 @@ const WorkspaceSettings: NextPage = () => {
const { setToastAlert } = useToast();
const { data: activeWorkspace } = useSWR(
workspaceSlug ? WORKSPACE_DETAILS(workspaceSlug as string) : null,
() => (workspaceSlug ? workspaceService.getWorkspace(workspaceSlug as string) : null)
const { data: activeWorkspace } = useSWR(workspaceSlug ? WORKSPACE_DETAILS(workspaceSlug as string) : null, () =>
workspaceSlug ? workspaceService.getWorkspace(workspaceSlug as string) : null
);
const {
@ -156,9 +160,7 @@ const WorkspaceSettings: NextPage = () => {
<WorkspaceAuthorizationLayout
breadcrumbs={
<Breadcrumbs>
<BreadcrumbItem
title={`${truncateText(activeWorkspace?.name ?? "Workspace", 32)} Settings`}
/>
<BreadcrumbItem title={`${truncateText(activeWorkspace?.name ?? "Workspace", 32)} Settings`} />
</Breadcrumbs>
}
>
@ -191,11 +193,7 @@ const WorkspaceSettings: NextPage = () => {
<div className={`pr-9 py-8 w-full overflow-y-auto ${isAdmin ? "" : "opacity-60"}`}>
<div className="flex gap-5 items-center pb-7 border-b border-custom-border-200">
<div className="flex flex-col gap-1">
<button
type="button"
onClick={() => setIsImageUploadModalOpen(true)}
disabled={!isAdmin}
>
<button type="button" onClick={() => setIsImageUploadModalOpen(true)} disabled={!isAdmin}>
{watch("logo") && watch("logo") !== null && watch("logo") !== "" ? (
<div className="relative mx-auto flex h-14 w-14">
<img
@ -214,8 +212,7 @@ const WorkspaceSettings: NextPage = () => {
<div className="flex flex-col gap-1">
<h3 className="text-lg font-semibold leading-6">{watch("name")}</h3>
<span className="text-sm tracking-tight">{`${
typeof window !== "undefined" &&
window.location.origin.replace("http://", "").replace("https://", "")
typeof window !== "undefined" && window.location.origin.replace("http://", "").replace("https://", "")
}/${activeWorkspace.slug}`}</span>
<div className="flex item-center gap-2.5">
<button
@ -240,21 +237,30 @@ const WorkspaceSettings: NextPage = () => {
<div className="grid grid-col grid-cols-1 xl:grid-cols-2 2xl:grid-cols-3 items-center justify-between gap-10 w-full">
<div className="flex flex-col gap-1 ">
<h4 className="text-sm">Workspace Name</h4>
<Input
id="name"
<Controller
control={control}
name="name"
placeholder="Name"
autoComplete="off"
register={register}
error={errors.name}
validations={{
rules={{
required: "Name is required",
maxLength: {
value: 80,
message: "Workspace name should not exceed 80 characters",
},
}}
disabled={!isAdmin}
render={({ field: { value, onChange, ref } }) => (
<Input
id="name"
name="name"
type="text"
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.name)}
placeholder="Name"
className="rounded-md font-medium w-full"
disabled={!isAdmin}
/>
)}
/>
</div>
@ -267,9 +273,7 @@ const WorkspaceSettings: NextPage = () => {
<CustomSelect
value={value}
onChange={onChange}
label={
ORGANIZATION_SIZE.find((c) => c === value) ?? "Select organization size"
}
label={ORGANIZATION_SIZE.find((c) => c === value) ?? "Select organization size"}
width="w-full"
input
disabled={!isAdmin}
@ -286,30 +290,33 @@ const WorkspaceSettings: NextPage = () => {
<div className="flex flex-col gap-1 ">
<h4 className="text-sm">Workspace URL</h4>
<Input
id="url"
<Controller
control={control}
name="url"
autoComplete="off"
register={register}
error={errors.url}
className="w-full"
value={`${
typeof window !== "undefined" &&
window.location.origin.replace("http://", "").replace("https://", "")
}/${activeWorkspace.slug}`}
disabled
render={({ field: { value, onChange, ref } }) => (
<Input
id="url"
name="url"
type="url"
value={`${
typeof window !== "undefined" &&
window.location.origin.replace("http://", "").replace("https://", "")
}/${activeWorkspace.slug}`}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.url)}
className="w-full"
disabled
/>
)}
/>
</div>
</div>
<div className="flex items-center justify-between py-2">
<PrimaryButton
onClick={handleSubmit(onSubmit)}
loading={isSubmitting}
disabled={!isAdmin}
>
<Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isSubmitting} disabled={!isAdmin}>
{isSubmitting ? "Updating..." : "Update Workspace"}
</PrimaryButton>
</Button>
</div>
</div>
{isAdmin && (
@ -337,19 +344,14 @@ const WorkspaceSettings: NextPage = () => {
<Disclosure.Panel>
<div className="flex flex-col gap-8">
<span className="text-sm tracking-tight">
The danger zone of the workspace delete page is a critical area that
requires careful consideration and attention. When deleting a workspace,
all of the data and resources within that workspace will be permanently
removed and cannot be recovered.
The danger zone of the workspace delete page is a critical area that requires careful
consideration and attention. When deleting a workspace, all of the data and resources within
that workspace will be permanently removed and cannot be recovered.
</span>
<div>
<DangerButton
onClick={() => setIsOpen(true)}
className="!text-sm"
outline
>
<Button variant="danger" onClick={() => setIsOpen(true)}>
Delete my workspace
</DangerButton>
</Button>
</div>
</div>
</Disclosure.Panel>

View file

@ -5,16 +5,17 @@ import { useRouter } from "next/router";
import useSWR from "swr";
// services
import workspaceService from "services/workspace.service";
import IntegrationService from "services/integration";
import { WorkspaceService } from "services/workspace.service";
import { IntegrationService } from "services/integrations";
// layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout-legacy";
// components
import { SingleIntegrationCard } from "components/integration";
import { SettingsSidebar } from "components/project";
// ui
import { IntegrationAndImportExportBanner, Loader } from "components/ui";
import { IntegrationAndImportExportBanner } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
import { Loader } from "@plane/ui";
// types
import type { NextPage } from "next";
// fetch-keys
@ -22,17 +23,20 @@ import { WORKSPACE_DETAILS, APP_INTEGRATIONS } from "constants/fetch-keys";
// helper
import { truncateText } from "helpers/string.helper";
// services
const workspaceService = new WorkspaceService();
const integrationService = new IntegrationService();
const WorkspaceIntegrations: NextPage = () => {
const router = useRouter();
const { workspaceSlug } = router.query;
const { data: activeWorkspace } = useSWR(
workspaceSlug ? WORKSPACE_DETAILS(workspaceSlug as string) : null,
() => (workspaceSlug ? workspaceService.getWorkspace(workspaceSlug as string) : null)
const { data: activeWorkspace } = useSWR(workspaceSlug ? WORKSPACE_DETAILS(workspaceSlug as string) : null, () =>
workspaceSlug ? workspaceService.getWorkspace(workspaceSlug as string) : null
);
const { data: appIntegrations } = useSWR(workspaceSlug ? APP_INTEGRATIONS : null, () =>
workspaceSlug ? IntegrationService.getAppIntegrationsList() : null
workspaceSlug ? integrationService.getAppIntegrationsList() : null
);
return (

View file

@ -6,35 +6,35 @@ import { useRouter } from "next/router";
import useSWR from "swr";
// services
import workspaceService from "services/workspace.service";
import { WorkspaceService } from "services/workspace.service";
// hooks
import useToast from "hooks/use-toast";
import useUser from "hooks/use-user";
import useWorkspaceMembers from "hooks/use-workspace-members";
// layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout-legacy";
// components
import ConfirmWorkspaceMemberRemove from "components/workspace/confirm-workspace-member-remove";
import SendWorkspaceInvitationModal from "components/workspace/send-workspace-invitation-modal";
import { SettingsSidebar } from "components/project";
// ui
import { CustomMenu, CustomSelect, Icon, Loader, PrimaryButton } from "components/ui";
import { Button, Loader } from "@plane/ui";
import { CustomMenu, CustomSelect, Icon } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// icons
import { XMarkIcon } from "components/icons";
// types
import type { NextPage } from "next";
// fetch-keys
import {
WORKSPACE_DETAILS,
WORKSPACE_INVITATION_WITH_EMAIL,
WORKSPACE_MEMBERS_WITH_EMAIL,
} from "constants/fetch-keys";
import { WORKSPACE_DETAILS, WORKSPACE_INVITATION_WITH_EMAIL, WORKSPACE_MEMBERS_WITH_EMAIL } from "constants/fetch-keys";
// constants
import { ROLE } from "constants/workspace";
// helper
import { truncateText } from "helpers/string.helper";
// services
const workspaceService = new WorkspaceService();
const MembersSettings: NextPage = () => {
const [selectedRemoveMember, setSelectedRemoveMember] = useState<string | null>(null);
const [selectedInviteRemoveMember, setSelectedInviteRemoveMember] = useState<string | null>(null);
@ -49,23 +49,18 @@ const MembersSettings: NextPage = () => {
const { isOwner } = useWorkspaceMembers(workspaceSlug?.toString(), Boolean(workspaceSlug));
const { data: activeWorkspace } = useSWR(
workspaceSlug ? WORKSPACE_DETAILS(workspaceSlug.toString()) : null,
() => (workspaceSlug ? workspaceService.getWorkspace(workspaceSlug.toString()) : null)
const { data: activeWorkspace } = useSWR(workspaceSlug ? WORKSPACE_DETAILS(workspaceSlug.toString()) : null, () =>
workspaceSlug ? workspaceService.getWorkspace(workspaceSlug.toString()) : null
);
const { data: workspaceMembers, mutate: mutateMembers } = useSWR(
workspaceSlug ? WORKSPACE_MEMBERS_WITH_EMAIL(workspaceSlug.toString()) : null,
workspaceSlug
? () => workspaceService.workspaceMembersWithEmail(workspaceSlug.toString())
: null
workspaceSlug ? () => workspaceService.workspaceMembersWithEmail(workspaceSlug.toString()) : null
);
const { data: workspaceInvitations, mutate: mutateInvitations } = useSWR(
workspaceSlug ? WORKSPACE_INVITATION_WITH_EMAIL(workspaceSlug.toString()) : null,
workspaceSlug
? () => workspaceService.workspaceInvitationsWithEmail(workspaceSlug.toString())
: null
workspaceSlug ? () => workspaceService.workspaceInvitationsWithEmail(workspaceSlug.toString()) : null
);
const members = [
@ -143,15 +138,12 @@ const MembersSettings: NextPage = () => {
});
})
.finally(() => {
mutateMembers((prevData: any) =>
prevData?.filter((item: any) => item.id !== selectedRemoveMember)
);
mutateMembers((prevData: any) => prevData?.filter((item: any) => item.id !== selectedRemoveMember));
});
}
if (selectedInviteRemoveMember) {
mutateInvitations(
(prevData: any) =>
prevData?.filter((item: any) => item.id !== selectedInviteRemoveMember),
(prevData: any) => prevData?.filter((item: any) => item.id !== selectedInviteRemoveMember),
false
);
workspaceService
@ -193,7 +185,9 @@ const MembersSettings: NextPage = () => {
<section className="pr-9 py-8 w-full overflow-y-auto">
<div className="flex items-center justify-between gap-4 pt-2 pb-3.5 border-b border-custom-border-200">
<h4 className="text-xl font-medium">Members</h4>
<PrimaryButton onClick={() => setInviteModal(true)}>Add Member</PrimaryButton>
<Button variant="primary" onClick={() => setInviteModal(true)}>
Add Member
</Button>
</div>
{!workspaceMembers || !workspaceInvitations ? (
<Loader className="space-y-5">
@ -206,10 +200,7 @@ const MembersSettings: NextPage = () => {
<div className="divide-y divide-custom-border-200">
{members.length > 0
? members.map((member) => (
<div
key={member.id}
className="group flex items-center justify-between px-3.5 py-[18px]"
>
<div key={member.id} className="group flex items-center justify-between px-3.5 py-[18px]">
<div className="flex items-center gap-x-8 gap-y-2">
{member.avatar && member.avatar !== "" ? (
<Link href={`/${workspaceSlug}/profile/${member.memberId}`}>
@ -239,21 +230,13 @@ const MembersSettings: NextPage = () => {
<span>
{member.first_name} {member.last_name}
</span>
<span className="text-custom-text-300 text-sm ml-2">
({member.display_name})
</span>
<span className="text-custom-text-300 text-sm ml-2">({member.display_name})</span>
</a>
</Link>
) : (
<h4 className="text-sm cursor-default">
{member.display_name || member.email}
</h4>
)}
{isOwner && (
<p className="mt-0.5 text-xs text-custom-sidebar-text-300">
{member.email}
</p>
<h4 className="text-sm cursor-default">{member.display_name || member.email}</h4>
)}
{isOwner && <p className="mt-0.5 text-xs text-custom-sidebar-text-300">{member.email}</p>}
</div>
</div>
<div className="flex items-center gap-3 text-xs">
@ -288,9 +271,7 @@ const MembersSettings: NextPage = () => {
mutateMembers(
(prevData: any) =>
prevData?.map((m: any) =>
m.id === member.id ? { ...m, role: value } : m
),
prevData?.map((m: any) => (m.id === member.id ? { ...m, role: value } : m)),
false
);
@ -302,26 +283,18 @@ const MembersSettings: NextPage = () => {
setToastAlert({
type: "error",
title: "Error!",
message:
"An error occurred while updating member role. Please try again.",
message: "An error occurred while updating member role. Please try again.",
});
});
}}
disabled={
member.memberId === currentUser?.member.id ||
!member.status ||
(currentUser &&
currentUser.role !== 20 &&
currentUser.role < member.role)
(currentUser && currentUser.role !== 20 && currentUser.role < member.role)
}
>
{Object.keys(ROLE).map((key) => {
if (
currentUser &&
currentUser.role !== 20 &&
currentUser.role < parseInt(key)
)
return null;
if (currentUser && currentUser.role !== 20 && currentUser.role < parseInt(key)) return null;
return (
<CustomSelect.Option key={key} value={key}>
@ -343,10 +316,7 @@ const MembersSettings: NextPage = () => {
<span className="flex items-center justify-start gap-2">
<XMarkIcon className="h-4 w-4" />
<span>
{" "}
{user?.id === member.memberId ? "Leave" : "Remove member"}
</span>
<span> {user?.id === member.memberId ? "Leave" : "Remove member"}</span>
</span>
</CustomMenu.MenuItem>
</CustomMenu>

View file

@ -0,0 +1,40 @@
import { useRouter } from "next/router";
import useSWR from "swr";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// layouts
import { AppLayout } from "layouts/app-layout";
// components
import { GlobalViewsHeader } from "components/workspace";
import { GlobalViewLayoutRoot } from "components/issues";
import { GlobalIssuesHeader } from "components/headers";
// types
import { NextPage } from "next";
const GlobalViewIssues: NextPage = () => {
const router = useRouter();
const { workspaceSlug, globalViewId } = router.query;
const { globalViews: globalViewsStore } = useMobxStore();
useSWR(
workspaceSlug && globalViewId ? `GLOBAL_VIEW_DETAILS_${globalViewId.toString()}` : null,
workspaceSlug && globalViewId
? () => globalViewsStore.fetchGlobalViewDetails(workspaceSlug.toString(), globalViewId.toString())
: null
);
return (
<AppLayout header={<GlobalIssuesHeader activeLayout="spreadsheet" />}>
<div className="h-full overflow-hidden bg-custom-background-100">
<div className="h-full w-full flex flex-col border-b border-custom-border-300">
<GlobalViewsHeader />
<GlobalViewLayoutRoot />
</div>
</div>
</AppLayout>
);
};
export default GlobalViewIssues;

View file

@ -1,40 +1,21 @@
// components
import { GlobalViewsHeader } from "components/workspace";
import { GlobalIssuesHeader } from "components/headers";
import { GlobalViewLayoutRoot } from "components/issues";
// layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
import { PrimaryButton } from "components/ui";
// component
import { WorkspaceIssuesViewOptions } from "components/issues/workspace-views/workspace-issue-view-option";
import { WorkspaceAllIssue } from "components/issues/workspace-views/workspace-all-issue";
// icons
import { PlusIcon } from "@heroicons/react/24/outline";
import { CheckCircle } from "lucide-react";
import { AppLayout } from "layouts/app-layout";
// types
import { NextPage } from "next";
const WorkspaceViewAllIssue = () => (
<WorkspaceAuthorizationLayout
breadcrumbs={
<div className="flex gap-2 items-center">
<CheckCircle className="h-[18px] w-[18px] stroke-[1.5]" />
<span className="text-sm font-medium">Workspace issues</span>
const GlobalViewAllIssues: NextPage = () => (
<AppLayout header={<GlobalIssuesHeader activeLayout="spreadsheet" />}>
<div className="h-full overflow-hidden bg-custom-background-100">
<div className="h-full w-full flex flex-col border-b border-custom-border-300">
<GlobalViewsHeader />
<GlobalViewLayoutRoot type="all-issues" />
</div>
}
right={
<div className="flex items-center gap-2">
<WorkspaceIssuesViewOptions />
<PrimaryButton
className="flex items-center gap-2"
onClick={() => {
const e = new KeyboardEvent("keydown", { key: "c" });
document.dispatchEvent(e);
}}
>
<PlusIcon className="h-4 w-4" />
Add Issue
</PrimaryButton>
</div>
}
>
<WorkspaceAllIssue />
</WorkspaceAuthorizationLayout>
</div>
</AppLayout>
);
export default WorkspaceViewAllIssue;
export default GlobalViewAllIssues;

View file

@ -1,40 +1,21 @@
// layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
// components
import { WorkspaceIssuesViewOptions } from "components/issues/workspace-views/workspace-issue-view-option";
import { WorkspaceAssignedIssue } from "components/issues/workspace-views/workspace-assigned-issue";
// ui
import { PrimaryButton } from "components/ui";
// icons
import { PlusIcon } from "@heroicons/react/24/outline";
import { CheckCircle } from "lucide-react";
import { GlobalViewsHeader } from "components/workspace";
import { GlobalIssuesHeader } from "components/headers";
import { GlobalViewLayoutRoot } from "components/issues";
// layouts
import { AppLayout } from "layouts/app-layout";
// types
import { NextPage } from "next";
const WorkspaceViewAssignedIssue: React.FC = () => (
<WorkspaceAuthorizationLayout
breadcrumbs={
<div className="flex gap-2 items-center">
<CheckCircle className="h-[18px] w-[18px] stroke-[1.5]" />
<span className="text-sm font-medium">Workspace Issues</span>
const GlobalViewAssignedIssues: NextPage = () => (
<AppLayout header={<GlobalIssuesHeader activeLayout="spreadsheet" />}>
<div className="h-full overflow-hidden bg-custom-background-100">
<div className="h-full w-full flex flex-col border-b border-custom-border-300">
<GlobalViewsHeader />
<GlobalViewLayoutRoot type="assigned" />
</div>
}
right={
<div className="flex items-center gap-2">
<WorkspaceIssuesViewOptions />
<PrimaryButton
className="flex items-center gap-2"
onClick={() => {
const e = new KeyboardEvent("keydown", { key: "c" });
document.dispatchEvent(e);
}}
>
<PlusIcon className="h-4 w-4" />
Add Issue
</PrimaryButton>
</div>
}
>
<WorkspaceAssignedIssue />
</WorkspaceAuthorizationLayout>
</div>
</AppLayout>
);
export default WorkspaceViewAssignedIssue;
export default GlobalViewAssignedIssues;

View file

@ -1,40 +1,21 @@
// layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
// components
import { WorkspaceIssuesViewOptions } from "components/issues/workspace-views/workspace-issue-view-option";
import { WorkspaceCreatedIssues } from "components/issues/workspace-views/workspace-created-issues";
// ui
import { PrimaryButton } from "components/ui";
// icons
import { PlusIcon } from "@heroicons/react/24/outline";
import { CheckCircle } from "lucide-react";
import { GlobalViewsHeader } from "components/workspace";
import { GlobalIssuesHeader } from "components/headers";
import { GlobalViewLayoutRoot } from "components/issues";
// layouts
import { AppLayout } from "layouts/app-layout";
// types
import { NextPage } from "next";
const WorkspaceViewCreatedIssue: React.FC = () => (
<WorkspaceAuthorizationLayout
breadcrumbs={
<div className="flex gap-2 items-center">
<CheckCircle className="h-[18px] w-[18px] stroke-[1.5]" />
<span className="text-sm font-medium">Workspace Issues</span>
const GlobalViewCreatedIssues: NextPage = () => (
<AppLayout header={<GlobalIssuesHeader activeLayout="spreadsheet" />}>
<div className="h-full overflow-hidden bg-custom-background-100">
<div className="h-full w-full flex flex-col border-b border-custom-border-300">
<GlobalViewsHeader />
<GlobalViewLayoutRoot type="created" />
</div>
}
right={
<div className="flex items-center gap-2">
<WorkspaceIssuesViewOptions />
<PrimaryButton
className="flex items-center gap-2"
onClick={() => {
const e = new KeyboardEvent("keydown", { key: "c" });
document.dispatchEvent(e);
}}
>
<PlusIcon className="h-4 w-4" />
Add Issue
</PrimaryButton>
</div>
}
>
<WorkspaceCreatedIssues />
</WorkspaceAuthorizationLayout>
</div>
</AppLayout>
);
export default WorkspaceViewCreatedIssue;
export default GlobalViewCreatedIssues;

View file

@ -1,205 +1,42 @@
import React, { useState } from "react";
import Link from "next/link";
import { useRouter } from "next/router";
import useSWR from "swr";
// services
import workspaceService from "services/workspace.service";
// layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
import { AppLayout } from "layouts/app-layout";
// components
import { SingleWorkspaceViewItem } from "components/workspace/views/single-workspace-view-item";
import { WorkspaceIssuesViewOptions } from "components/issues/workspace-views/workspace-issue-view-option";
import { CreateUpdateWorkspaceViewModal } from "components/workspace/views/modal";
import { DeleteWorkspaceViewModal } from "components/workspace/views/delete-workspace-view-modal";
import { GlobalDefaultViewListItem, GlobalViewsList } from "components/workspace";
import { GlobalIssuesHeader } from "components/headers";
// ui
import { EmptyState, Input, Loader, PrimaryButton } from "components/ui";
import { Input } from "@plane/ui";
// icons
import { MagnifyingGlassIcon } from "@heroicons/react/24/outline";
import { PlusIcon } from "lucide-react";
import { PhotoFilterOutlined } from "@mui/icons-material";
// image
import emptyView from "public/empty-state/view.svg";
import { Search } from "lucide-react";
// types
import type { NextPage } from "next";
import { IWorkspaceView } from "types/workspace-views";
// constants
import { WORKSPACE_VIEWS_LIST } from "constants/fetch-keys";
// helper
import { truncateText } from "helpers/string.helper";
import { DEFAULT_GLOBAL_VIEWS_LIST } from "constants/workspace";
const WorkspaceViews: NextPage = () => {
const [query, setQuery] = useState("");
const [createUpdateViewModal, setCreateUpdateViewModal] = useState(false);
const [selectedViewToUpdate, setSelectedViewToUpdate] = useState<IWorkspaceView | null>(null);
const [deleteViewModal, setDeleteViewModal] = useState(false);
const [selectedViewToDelete, setSelectedViewToDelete] = useState<IWorkspaceView | null>(null);
const router = useRouter();
const { workspaceSlug } = router.query;
const { data: workspaceViews } = useSWR(
workspaceSlug ? WORKSPACE_VIEWS_LIST(workspaceSlug as string) : null,
workspaceSlug ? () => workspaceService.getAllViews(workspaceSlug as string) : null
);
const defaultWorkspaceViewsList = [
{
key: "all",
label: "All Issues",
href: `/${workspaceSlug}/workspace-views/all-issues`,
},
{
key: "assigned",
label: "Assigned",
href: `/${workspaceSlug}/workspace-views/assigned`,
},
{
key: "created",
label: "Created",
href: `/${workspaceSlug}/workspace-views/created`,
},
{
key: "subscribed",
label: "Subscribed",
href: `/${workspaceSlug}/workspace-views/subscribed`,
},
];
const filteredDefaultOptions =
query === ""
? defaultWorkspaceViewsList
: defaultWorkspaceViewsList?.filter((option) =>
option.label.toLowerCase().includes(query.toLowerCase())
);
const filteredOptions =
query === ""
? workspaceViews
: workspaceViews?.filter((option) => option.name.toLowerCase().includes(query.toLowerCase()));
const handleEditView = (view: IWorkspaceView) => {
setSelectedViewToUpdate(view);
setCreateUpdateViewModal(true);
};
const handleDeleteView = (view: IWorkspaceView) => {
setSelectedViewToDelete(view);
setDeleteViewModal(true);
};
return (
<WorkspaceAuthorizationLayout
breadcrumbs={
<div className="flex gap-2 items-center">
<span className="text-sm font-medium">Workspace Views</span>
</div>
}
right={
<div className="flex items-center gap-2">
<WorkspaceIssuesViewOptions />
<PrimaryButton
className="flex items-center gap-2"
onClick={() => setCreateUpdateViewModal(true)}
>
<PlusIcon className="h-4 w-4" />
New View
</PrimaryButton>
</div>
}
>
<CreateUpdateWorkspaceViewModal
isOpen={createUpdateViewModal}
handleClose={() => {
setCreateUpdateViewModal(false);
setSelectedViewToUpdate(null);
}}
data={selectedViewToUpdate}
/>
<DeleteWorkspaceViewModal
isOpen={deleteViewModal}
data={selectedViewToDelete}
setIsOpen={setDeleteViewModal}
/>
<AppLayout header={<GlobalIssuesHeader activeLayout="list" />}>
<div className="flex flex-col">
<div className="h-full w-full flex flex-col overflow-hidden">
<div className="flex items-center gap-2.5 w-full px-5 py-3 border-b border-custom-border-200">
<MagnifyingGlassIcon className="h-4 w-4 text-custom-text-200" />
<Search className="text-custom-text-200" size={14} strokeWidth={2} />
<Input
className="w-full bg-transparent text-xs leading-5 text-custom-text-200 placeholder:text-custom-text-400 !p-0 focus:outline-none"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search"
mode="trueTransparent"
mode="true-transparent"
/>
</div>
</div>
{filteredDefaultOptions &&
filteredDefaultOptions.length > 0 &&
filteredDefaultOptions.map((option) => (
<div className="group hover:bg-custom-background-90 border-b border-custom-border-200">
<Link href={option.href}>
<a className="flex items-center justify-between relative rounded px-5 py-4 w-full">
<div className="flex items-center justify-between w-full">
<div className="flex items-center gap-4">
<div
className={`flex items-center justify-center h-10 w-10 rounded bg-custom-background-90 group-hover:bg-custom-background-100`}
>
<PhotoFilterOutlined className="!text-base !leading-6" />
</div>
<div className="flex flex-col">
<p className="truncate text-sm leading-4 font-medium">
{truncateText(option.label, 75)}
</p>
</div>
</div>
</div>
</a>
</Link>
</div>
))}
{filteredOptions ? (
filteredOptions.length > 0 ? (
<div>
{filteredOptions.map((view) => (
<SingleWorkspaceViewItem
key={view.id}
view={view}
handleEditView={() => handleEditView(view)}
handleDeleteView={() => handleDeleteView(view)}
/>
))}
</div>
) : (
<EmptyState
title="Get focused with views"
description="Views aid in saving your issues by applying various filters and grouping options."
image={emptyView}
primaryButton={{
icon: <PlusIcon className="h-4 w-4" />,
text: "New View",
onClick: () => setCreateUpdateViewModal(true),
}}
/>
)
) : (
<Loader className="space-y-1.5">
<Loader.Item height="72px" />
<Loader.Item height="72px" />
<Loader.Item height="72px" />
<Loader.Item height="72px" />
<Loader.Item height="72px" />
</Loader>
)}
{DEFAULT_GLOBAL_VIEWS_LIST.filter((v) => v.label.toLowerCase().includes(query.toLowerCase())).map((option) => (
<GlobalDefaultViewListItem key={option.key} view={option} />
))}
<GlobalViewsList searchQuery={query} />
</div>
</WorkspaceAuthorizationLayout>
</AppLayout>
);
};

View file

@ -1,8 +1,8 @@
// layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout-legacy";
// components
import { WorkspaceIssuesViewOptions } from "components/issues/workspace-views/workspace-issue-view-option";
import { WorkspaceViewIssues } from "components/issues/workspace-views/workpace-view-issues";
// import { WorkspaceIssuesViewOptions } from "components/issues/workspace-views/workspace-issue-view-option";
// import { WorkspaceViewIssues } from "components/issues/workspace-views/workpace-view-issues";
// ui
import { PrimaryButton } from "components/ui";
// icons
@ -19,7 +19,7 @@ const WorkspaceView = () => (
}
right={
<div className="flex items-center gap-2">
<WorkspaceIssuesViewOptions />
{/* <WorkspaceIssuesViewOptions /> */}
<PrimaryButton
className="flex items-center gap-2"
onClick={() => {
@ -33,7 +33,7 @@ const WorkspaceView = () => (
</div>
}
>
<WorkspaceViewIssues />
{/* <WorkspaceViewIssues /> */}
</WorkspaceAuthorizationLayout>
);

View file

@ -1,40 +1,21 @@
// layouts
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
import { AppLayout } from "layouts/app-layout";
// components
import { WorkspaceIssuesViewOptions } from "components/issues/workspace-views/workspace-issue-view-option";
import { WorkspaceSubscribedIssues } from "components/issues/workspace-views/workspace-subscribed-issue";
// ui
import { PrimaryButton } from "components/ui";
// icons
import { PlusIcon } from "@heroicons/react/24/outline";
import { CheckCircle } from "lucide-react";
import { GlobalViewsHeader } from "components/workspace";
import { GlobalIssuesHeader } from "components/headers";
import { GlobalViewLayoutRoot } from "components/issues";
// types
import { NextPage } from "next";
const WorkspaceViewSubscribedIssue: React.FC = () => (
<WorkspaceAuthorizationLayout
breadcrumbs={
<div className="flex gap-2 items-center">
<CheckCircle className="h-[18px] w-[18px] stroke-[1.5]" />
<span className="text-sm font-medium">Workspace Issue</span>
const GlobalViewSubscribedIssues: NextPage = () => (
<AppLayout header={<GlobalIssuesHeader activeLayout="spreadsheet" />}>
<div className="h-full overflow-hidden bg-custom-background-100">
<div className="h-full w-full flex flex-col border-b border-custom-border-300">
<GlobalViewsHeader />
<GlobalViewLayoutRoot type="subscribed" />
</div>
}
right={
<div className="flex items-center gap-2">
<WorkspaceIssuesViewOptions />
<PrimaryButton
className="flex items-center gap-2"
onClick={() => {
const e = new KeyboardEvent("keydown", { key: "c" });
document.dispatchEvent(e);
}}
>
<PlusIcon className="h-4 w-4" />
Add Issue
</PrimaryButton>
</div>
}
>
<WorkspaceSubscribedIssues />
</WorkspaceAuthorizationLayout>
</div>
</AppLayout>
);
export default WorkspaceViewSubscribedIssue;
export default GlobalViewSubscribedIssues;

View file

@ -3,13 +3,16 @@ import * as Sentry from "@sentry/nextjs";
import { useRouter } from "next/router";
// services
import authenticationService from "services/authentication.service";
import { AuthService } from "services/auth.service";
// hooks
import useToast from "hooks/use-toast";
// layouts
import DefaultLayout from "layouts/default-layout";
// ui
import { PrimaryButton, SecondaryButton } from "components/ui";
import { Button } from "@plane/ui";
// services
const authService = new AuthService();
const CustomErrorComponent = () => {
const router = useRouter();
@ -17,7 +20,7 @@ const CustomErrorComponent = () => {
const { setToastAlert } = useToast();
const handleSignOut = async () => {
await authenticationService
await authService
.signOut()
.catch(() =>
setToastAlert({
@ -36,9 +39,8 @@ const CustomErrorComponent = () => {
<div className="space-y-2">
<h3 className="text-lg font-semibold">Exception Detected!</h3>
<p className="text-sm text-custom-text-200 w-1/2 mx-auto">
We{"'"}re Sorry! An exception has been detected, and our engineering team has been
notified. We apologize for any inconvenience this may have caused. Please reach out to
our engineering team at{" "}
We{"'"}re Sorry! An exception has been detected, and our engineering team has been notified. We apologize
for any inconvenience this may have caused. Please reach out to our engineering team at{" "}
<a href="mailto:support@plane.so" className="text-custom-primary">
support@plane.so
</a>{" "}
@ -55,12 +57,12 @@ const CustomErrorComponent = () => {
</p>
</div>
<div className="flex items-center gap-2 justify-center">
<PrimaryButton size="md" onClick={() => router.back()}>
<Button variant="primary" size="md" onClick={() => router.back()}>
Go back
</PrimaryButton>
<SecondaryButton size="md" onClick={handleSignOut}>
</Button>
<Button variant="neutral-primary" size="md" onClick={handleSignOut}>
Sign out
</SecondaryButton>
</Button>
</div>
</div>
</div>

View file

@ -0,0 +1,71 @@
import { NextPage } from "next";
import Image from "next/image";
// components
import { EmailForgotPasswordForm, EmailForgotPasswordFormValues } from "components/account";
// layouts
import DefaultLayout from "layouts/default-layout";
// services
import { UserService } from "services/user.service";
// hooks
import useToast from "hooks/use-toast";
// images
import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png";
const userService = new UserService();
const ForgotPasswordPage: NextPage = () => {
// toast
const { setToastAlert } = useToast();
const handleForgotPassword = (formData: EmailForgotPasswordFormValues) => {
const payload = {
email: formData.email,
};
return 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 (
<DefaultLayout>
<>
<div className="hidden sm:block sm:fixed border-r-[0.5px] border-custom-border-200 h-screen w-[0.5px] top-0 left-20 lg:left-32" />
<div className="fixed grid place-items-center bg-custom-background-100 sm:py-5 top-11 sm:top-12 left-7 sm:left-16 lg:left-28">
<div className="grid place-items-center bg-custom-background-100">
<div className="h-[30px] w-[30px]">
<Image src={BluePlaneLogoWithoutText} alt="Plane Logo" />
</div>
</div>
</div>
</>
<div className="grid place-items-center h-full overflow-y-auto py-6 px-7">
<div>
<h1 className="text-center text-2xl sm:text-2.5xl font-semibold text-custom-text-100">Forgot Password</h1>
<EmailForgotPasswordForm onSubmit={handleForgotPassword} />
</div>
</div>
</DefaultLayout>
);
};
export default ForgotPasswordPage;

View file

@ -7,13 +7,15 @@ import { useTheme } from "next-themes";
// layouts
import DefaultLayout from "layouts/default-layout";
// services
import authenticationService from "services/authentication.service";
import { AuthService } from "services/auth.service";
// hooks
import useUserAuth from "hooks/use-user-auth";
import useToast from "hooks/use-toast";
// types
import type { NextPage } from "next";
const authService = new AuthService();
const MagicSignIn: NextPage = () => {
const router = useRouter();
const { password, key } = router.query;
@ -39,9 +41,9 @@ const MagicSignIn: NextPage = () => {
return;
} else {
setIsSigningIn(() => true);
authenticationService
authService
.magicSignIn({ token: password, key })
.then(async (res) => {
.then(async () => {
setIsSigningIn(false);
await mutateUser();
})
@ -70,7 +72,7 @@ const MagicSignIn: NextPage = () => {
<span
className="cursor-pointer underline"
onClick={() => {
authenticationService
authService
.emailCode({ email: (key as string).split("_")[1] })
.then(() => {
setToastAlert({
@ -95,9 +97,7 @@ const MagicSignIn: NextPage = () => {
) : (
<div className="flex h-full w-full flex-col items-center justify-center gap-y-2">
<h2 className="text-4xl font-medium">Success</h2>
<p className="text-sm font-medium text-custom-text-200">
Redirecting you to the app...
</p>
<p className="text-sm font-medium text-custom-text-200">Redirecting you to the app...</p>
</div>
)}
</div>

View file

@ -6,15 +6,15 @@ import Image from "next/image";
// next-themes
import { useTheme } from "next-themes";
// react-hook-form
import { useForm } from "react-hook-form";
import { Controller, useForm } from "react-hook-form";
// hooks
import useToast from "hooks/use-toast";
// services
import userService from "services/user.service";
import { UserService } from "services/user.service";
// layouts
import DefaultLayout from "layouts/default-layout";
// ui
import { Input, PrimaryButton, Spinner } from "components/ui";
import { Button, Input, Spinner } from "@plane/ui";
// images
import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png";
// types
@ -25,6 +25,9 @@ type FormData = {
confirmPassword: string;
};
// services
const userService = new UserService();
const ResetPasswordPage: NextPage = () => {
const [isLoading, setIsLoading] = useState(true);
@ -36,8 +39,8 @@ const ResetPasswordPage: NextPage = () => {
const { setTheme } = useTheme();
const {
register,
handleSubmit,
control,
formState: { errors, isSubmitting },
} = useForm<FormData>();
@ -73,9 +76,7 @@ const ResetPasswordPage: NextPage = () => {
setToastAlert({
type: "error",
title: "Error!",
message:
err?.error ||
"Something went wrong. Please try again later or contact the support team.",
message: err?.error || "Something went wrong. Please try again later or contact the support team.",
})
);
};
@ -110,49 +111,56 @@ const ResetPasswordPage: NextPage = () => {
</>
<div className="grid place-items-center h-full w-full overflow-y-auto py-5 px-7">
<div className="w-full">
<h1 className="text-center text-2xl sm:text-2.5xl font-semibold text-custom-text-100">
Reset your password
</h1>
<h1 className="text-center text-2xl sm:text-2.5xl font-semibold text-custom-text-100">Reset your password</h1>
<form
className="space-y-4 mt-10 w-full sm:w-[360px] mx-auto"
onSubmit={handleSubmit(onSubmit)}
>
<form className="space-y-4 mt-10 w-full sm:w-[360px] mx-auto" onSubmit={handleSubmit(onSubmit)}>
<div className="space-y-1">
<Input
id="password"
type="password"
<Controller
control={control}
name="password"
register={register}
validations={{
rules={{
required: "Password is required",
}}
error={errors.password}
placeholder="Enter new password..."
className="border-custom-border-300 h-[46px]"
render={({ field: { value, onChange, ref } }) => (
<Input
id="password"
name="password"
type="password"
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.password)}
placeholder="Enter new password..."
className="border-custom-border-300 h-[46px] w-full"
/>
)}
/>
</div>
<div className="space-y-1">
<Input
id="confirmPassword"
type="password"
<Controller
control={control}
name="confirmPassword"
register={register}
validations={{
required: "Password confirmation is required",
rules={{
required: "Password is required",
}}
error={errors.confirmPassword}
placeholder="Confirm new password..."
className="border-custom-border-300 h-[46px]"
render={({ field: { value, onChange, ref } }) => (
<Input
id="confirmPassword"
name="confirmPassword"
type="password"
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.confirmPassword)}
placeholder="Confirm new password..."
className="border-custom-border-300 h-[46px] w-full"
/>
)}
/>
</div>
<PrimaryButton
type="submit"
className="w-full text-center h-[46px]"
loading={isSubmitting}
>
<Button variant="primary" type="submit" className="w-full" loading={isSubmitting}>
{isSubmitting ? "Resetting..." : "Reset"}
</PrimaryButton>
</Button>
</form>
</div>
</div>

View file

@ -4,14 +4,14 @@ import { useRouter } from "next/router";
// next-themes
import { useTheme } from "next-themes";
// services
import authenticationService from "services/authentication.service";
import { AuthService } from "services/auth.service";
// hooks
import useUserAuth from "hooks/use-user-auth";
import useToast from "hooks/use-toast";
// layouts
import DefaultLayout from "layouts/default-layout";
// components
import { EmailPasswordForm, EmailSignUpForm } from "components/account";
import { EmailSignUpForm } from "components/account";
// images
import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png";
// types
@ -22,6 +22,9 @@ type EmailPasswordFormValues = {
medium?: string;
};
// services
const authService = new AuthService();
const SignUp: NextPage = () => {
const router = useRouter();
@ -37,7 +40,7 @@ const SignUp: NextPage = () => {
password: formData.password ?? "",
};
await authenticationService
await authService
.emailSignUp(payload)
.then(async (response) => {
setToastAlert({
@ -53,9 +56,7 @@ const SignUp: NextPage = () => {
setToastAlert({
type: "error",
title: "Error!",
message:
err?.error ||
"Something went wrong. Please try again later or contact the support team.",
message: err?.error || "Something went wrong. Please try again later or contact the support team.",
})
);
};

View file

@ -1,30 +1,29 @@
import React, { useState } from "react";
import { useRouter } from "next/router";
import Image from "next/image";
import { mutate } from "swr";
// next-themes
import { useTheme } from "next-themes";
// services
import userService from "services/user.service";
import { UserService } from "services/user.service";
// hooks
import useUser from "hooks/use-user";
// layouts
import DefaultLayout from "layouts/default-layout";
import { UserAuthorizationLayout } from "layouts/auth-layout/user-authorization-wrapper";
import { UserAuthorizationLayout } from "layouts/auth-layout-legacy/user-authorization-wrapper";
// components
import { CreateWorkspaceForm } from "components/workspace";
// images
import BlackHorizontalLogo from "public/plane-logos/black-horizontal-with-blue-logo.svg";
import WhiteHorizontalLogo from "public/plane-logos/white-horizontal-with-blue-logo.svg";
// types
import { ICurrentUserResponse, IWorkspace } from "types";
import { IUser, IWorkspace } from "types";
import type { NextPage } from "next";
// fetch-keys
import { CURRENT_USER } from "constants/fetch-keys";
// services
const userService = new UserService();
const CreateWorkspace: NextPage = () => {
const [defaultValues, setDefaultValues] = useState({
name: "",
@ -39,7 +38,7 @@ const CreateWorkspace: NextPage = () => {
const { user } = useUser();
const onSubmit = async (workspace: IWorkspace) => {
mutate<ICurrentUserResponse>(
mutate<IUser>(
CURRENT_USER,
(prevData) => {
if (!prevData) return prevData;
@ -47,21 +46,19 @@ const CreateWorkspace: NextPage = () => {
return {
...prevData,
last_workspace_id: workspace.id,
workspace: {
...prevData.workspace,
fallback_workspace_id: workspace.id,
fallback_workspace_slug: workspace.slug,
last_workspace_id: workspace.id,
last_workspace_slug: workspace.slug,
},
// workspace: {
// ...prevData.workspace,
// fallback_workspace_id: workspace.id,
// fallback_workspace_slug: workspace.slug,
// last_workspace_id: workspace.id,
// last_workspace_slug: workspace.slug,
// },
};
},
false
);
await userService
.updateUser({ last_workspace_id: workspace.id })
.then(() => router.push(`/${workspace.slug}`));
await userService.updateUser({ last_workspace_id: workspace.id }).then(() => router.push(`/${workspace.slug}`));
};
return (

View file

@ -1,16 +0,0 @@
import React from "react";
// layouts
import DefaultLayout from "layouts/default-layout";
// types
import type { NextPage } from "next";
const ErrorPage: NextPage = () => (
<DefaultLayout>
<div className="h-full w-full">
<h2 className="text-3xl">Error!</h2>
</div>
</DefaultLayout>
);
export default ErrorPage;

View file

@ -1,234 +1,13 @@
import React, { useEffect, useState } from "react";
import Image from "next/image";
import type { NextPage } from "next";
import { useTheme } from "next-themes";
import useSWR from "swr";
import { observer } from "mobx-react-lite";
// layouts
import DefaultLayout from "layouts/default-layout";
// services
import authenticationService from "services/authentication.service";
import { AppConfigService } from "services/app-config.service";
// hooks
import useUserAuth from "hooks/use-user-auth";
import useToast from "hooks/use-toast";
// components
import {
GoogleLoginButton,
GithubLoginButton,
EmailCodeForm,
EmailPasswordForm,
EmailResetPasswordForm,
} from "components/account";
// ui
import { Spinner } from "components/ui";
// images
import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// types
import { IUser } from "types";
import { SignInView } from "components/page-views";
const appConfig = new AppConfigService();
// types
type EmailPasswordFormValues = {
email: string;
password?: string;
medium?: string;
};
const HomePage: NextPage = observer(() => {
const store: any = useMobxStore();
// theme
const { setTheme } = useTheme();
// user
const { isLoading, mutateUser } = useUserAuth("sign-in");
// states
const [isResettingPassword, setIsResettingPassword] = useState(false);
// toast
const { setToastAlert } = useToast();
// fetch app config
const { data } = useSWR("APP_CONFIG", () => appConfig.envConfig());
const handleTheme = (user: IUser) => {
const currentTheme = user.theme.theme ?? "system";
setTheme(currentTheme);
store?.user?.setCurrentUserSettings();
};
const handleGoogleSignIn = async ({ clientId, credential }: any) => {
try {
if (clientId && credential) {
const socialAuthPayload = {
medium: "google",
credential,
clientId,
};
const response = await authenticationService.socialAuth(socialAuthPayload);
if (response && response?.user) {
mutateUser();
handleTheme(response?.user);
}
} else {
throw Error("Cant find credentials");
}
} catch (err: any) {
setToastAlert({
title: "Error signing in!",
type: "error",
message:
err?.error || "Something went wrong. Please try again later or contact the support team.",
});
}
};
const handleGitHubSignIn = async (credential: string) => {
try {
if (data && data.github && credential) {
const socialAuthPayload = {
medium: "github",
credential,
clientId: data.github,
};
const response = await authenticationService.socialAuth(socialAuthPayload);
if (response && response?.user) {
mutateUser();
handleTheme(response?.user);
}
} else {
throw Error("Cant find credentials");
}
} catch (err: any) {
setToastAlert({
title: "Error signing in!",
type: "error",
message:
err?.error || "Something went wrong. Please try again later or contact the support team.",
});
}
};
const handlePasswordSignIn = async (formData: EmailPasswordFormValues) => {
await authenticationService
.emailLogin(formData)
.then((response) => {
try {
if (response) {
mutateUser();
handleTheme(response?.user);
}
} catch (err: any) {
setToastAlert({
type: "error",
title: "Error!",
message:
err?.error ||
"Something went wrong. Please try again later or contact the support team.",
});
}
})
.catch((err) =>
setToastAlert({
type: "error",
title: "Error!",
message:
err?.error ||
"Something went wrong. Please try again later or contact the support team.",
})
);
};
const handleEmailCodeSignIn = async (response: any) => {
try {
if (response) {
mutateUser();
handleTheme(response?.user);
}
} catch (err: any) {
setToastAlert({
type: "error",
title: "Error!",
message:
err?.error || "Something went wrong. Please try again later or contact the support team.",
});
}
};
return (
<DefaultLayout>
{isLoading ? (
<div className="grid place-items-center h-screen">
<Spinner />
</div>
) : (
<>
<>
<div className="hidden sm:block sm:fixed border-r-[0.5px] border-custom-border-200 h-screen w-[0.5px] top-0 left-20 lg:left-32" />
<div className="fixed grid place-items-center bg-custom-background-100 sm:py-5 top-11 sm:top-12 left-7 sm:left-16 lg:left-28">
<div className="grid place-items-center bg-custom-background-100">
<div className="h-[30px] w-[30px]">
<Image src={BluePlaneLogoWithoutText} alt="Plane Logo" />
</div>
</div>
</div>
</>
<div className="grid place-items-center h-full overflow-y-auto py-5 px-7">
<div>
<h1 className="text-center text-2xl sm:text-2.5xl font-semibold text-custom-text-100">
{isResettingPassword ? "Reset your password" : "Sign in to Plane"}
</h1>
{isResettingPassword ? (
<EmailResetPasswordForm setIsResettingPassword={setIsResettingPassword} />
) : (
<>
{data?.email_password_login && (
<EmailPasswordForm
onSubmit={handlePasswordSignIn}
setIsResettingPassword={setIsResettingPassword}
/>
)}
{data?.magic_login && (
<div className="flex flex-col divide-y divide-custom-border-200">
<div className="pb-7">
<EmailCodeForm handleSignIn={handleEmailCodeSignIn} />
</div>
</div>
)}
<div className="flex flex-col items-center justify-center gap-4 pt-7 sm:w-[360px] mx-auto overflow-hidden">
{data?.google && (
<GoogleLoginButton
clientId={data?.google}
handleSignIn={handleGoogleSignIn}
/>
)}
{data?.github && (
<GithubLoginButton
clientId={data?.github}
handleSignIn={handleGitHubSignIn}
/>
)}
</div>
</>
)}
<p className="pt-16 text-custom-text-200 text-sm text-center">
By signing up, you agree to the{" "}
<a
href="https://plane.so/terms-and-conditions"
target="_blank"
rel="noopener noreferrer"
className="font-medium underline"
>
Terms & Conditions
</a>
</p>
</div>
</div>
</>
)}
</DefaultLayout>
);
});
const HomePage: NextPage = () => (
<DefaultLayout>
<SignInView />
</DefaultLayout>
);
export default HomePage;

View file

@ -3,9 +3,12 @@ import React, { useEffect } from "react";
import { useRouter } from "next/router";
// services
import appInstallationsService from "services/app-installations.service";
import { AppInstallationService } from "services/app_installation.service";
// ui
import { Spinner } from "components/ui";
import { Spinner } from "@plane/ui";
// services
const appInstallationService = new AppInstallationService();
const AppPostInstallation = () => {
const router = useRouter();
@ -13,7 +16,7 @@ const AppPostInstallation = () => {
useEffect(() => {
if (provider === "github" && state && installation_id) {
appInstallationsService
appInstallationService
.addInstallationApp(state.toString(), provider, { installation_id })
.then(() => {
window.opener = null;
@ -24,7 +27,7 @@ const AppPostInstallation = () => {
console.log(err);
});
} else if (provider === "slack" && state && code) {
appInstallationsService
appInstallationService
.getSlackAuthDetails(code.toString())
.then((res) => {
const [workspaceSlug, projectId, integrationId] = state.toString().split(",");
@ -36,7 +39,7 @@ const AppPostInstallation = () => {
},
};
appInstallationsService
appInstallationService
.addInstallationApp(state.toString(), provider, payload)
.then((r) => {
window.opener = null;
@ -56,7 +59,7 @@ const AppPostInstallation = () => {
team_name: res.team.name,
scopes: res.scope,
};
appInstallationsService
appInstallationService
.addSlackChannel(workspaceSlug, projectId, integrationId, payload)
.then((r) => {
window.opener = null;

View file

@ -1,23 +1,20 @@
import React, { useState } from "react";
import Link from "next/link";
import Image from "next/image";
import { useRouter } from "next/router";
import useSWR, { mutate } from "swr";
// next-themes
import { useTheme } from "next-themes";
// services
import workspaceService from "services/workspace.service";
import { WorkspaceService } from "services/workspace.service";
import { UserService } from "services/user.service";
// hooks
import useUser from "hooks/use-user";
import useToast from "hooks/use-toast";
// layouts
import DefaultLayout from "layouts/default-layout";
import { UserAuthorizationLayout } from "layouts/auth-layout/user-authorization-wrapper";
import { UserAuthorizationLayout } from "layouts/auth-layout-legacy/user-authorization-wrapper";
// ui
import { SecondaryButton, PrimaryButton, EmptyState } from "components/ui";
import { Button } from "@plane/ui";
// icons
import { CheckCircleIcon } from "@heroicons/react/24/outline";
// images
@ -29,13 +26,16 @@ import { truncateText } from "helpers/string.helper";
// types
import type { NextPage } from "next";
import type { IWorkspaceMemberInvitation } from "types";
// fetch-keys
import { USER_WORKSPACE_INVITATIONS } from "constants/fetch-keys";
// constants
import { ROLE } from "constants/workspace";
import userService from "services/user.service";
// components
import { EmptyState } from "components/common";
const OnBoard: NextPage = () => {
// services
const workspaceService = new WorkspaceService();
const userService = new UserService();
const UserInvitationsPage: NextPage = () => {
const [invitationsRespond, setInvitationsRespond] = useState<string[]>([]);
const [isJoiningWorkspaces, setIsJoiningWorkspaces] = useState(false);
@ -47,20 +47,15 @@ const OnBoard: NextPage = () => {
const { setToastAlert } = useToast();
const { data: invitations, mutate: mutateInvitations } = useSWR(USER_WORKSPACE_INVITATIONS, () =>
const { data: invitations } = useSWR<IWorkspaceMemberInvitation[]>("USER_WORKSPACE_INVITATIONS", () =>
workspaceService.userWorkspaceInvitations()
);
const handleInvitation = (
workspace_invitation: IWorkspaceMemberInvitation,
action: "accepted" | "withdraw"
) => {
const handleInvitation = (workspace_invitation: IWorkspaceMemberInvitation, action: "accepted" | "withdraw") => {
if (action === "accepted") {
setInvitationsRespond((prevData) => [...prevData, workspace_invitation.id]);
} else if (action === "withdraw") {
setInvitationsRespond((prevData) =>
prevData.filter((item: string) => item !== workspace_invitation.id)
);
setInvitationsRespond((prevData) => prevData.filter((item: string) => item !== workspace_invitation.id));
}
};
@ -144,9 +139,7 @@ const OnBoard: NextPage = () => {
? "border-custom-primary-100"
: "border-custom-border-200 hover:bg-custom-background-80"
}`}
onClick={() =>
handleInvitation(invitation, isSelected ? "withdraw" : "accepted")
}
onClick={() => handleInvitation(invitation, isSelected ? "withdraw" : "accepted")}
>
<div className="flex-shrink-0">
<div className="grid place-items-center h-9 w-9 rounded">
@ -166,9 +159,7 @@ const OnBoard: NextPage = () => {
</div>
</div>
<div className="min-w-0 flex-1">
<div className="text-sm font-medium">
{truncateText(invitation.workspace.name, 30)}
</div>
<div className="text-sm font-medium">{truncateText(invitation.workspace.name, 30)}</div>
<p className="text-xs text-custom-text-200">{ROLE[invitation.role]}</p>
</div>
<span
@ -183,7 +174,8 @@ const OnBoard: NextPage = () => {
})}
</div>
<div className="flex items-center gap-3">
<PrimaryButton
<Button
variant="primary"
type="submit"
size="md"
onClick={submitInvitations}
@ -191,12 +183,12 @@ const OnBoard: NextPage = () => {
loading={isJoiningWorkspaces}
>
Accept & Join
</PrimaryButton>
</Button>
<Link href="/">
<a>
<SecondaryButton size="md" outline>
<Button variant="neutral-primary" size="md">
Go Home
</SecondaryButton>
</Button>
</a>
</Link>
</div>
@ -222,4 +214,4 @@ const OnBoard: NextPage = () => {
);
};
export default OnBoard;
export default UserInvitationsPage;

View file

@ -14,10 +14,11 @@ import { Controller, useForm } from "react-hook-form";
import WebViewLayout from "layouts/web-view-layout";
// components
import { Button, Spinner } from "@plane/ui";
import { RichTextEditor } from "@plane/rich-text-editor";
import { PrimaryButton, Spinner } from "components/ui";
import fileService from "services/file.service";
// services
import { FileService } from "services/file.service";
const fileService = new FileService();
const Editor: NextPage = () => {
const [isLoading, setIsLoading] = useState(false);
@ -59,9 +60,7 @@ const Editor: NextPage = () => {
deleteFile={fileService.deleteImage}
borderOnFocus={false}
value={
!value ||
value === "" ||
(typeof value === "object" && Object.keys(value).length === 0)
!value || value === "" || (typeof value === "object" && Object.keys(value).length === 0)
? watch("data_html")
: value
}
@ -77,8 +76,9 @@ const Editor: NextPage = () => {
)}
/>
{isEditable && (
<PrimaryButton
className="mt-4 w-[calc(100%-30px)] h-[45px] mx-[15px] text-[17px] my-[15px]"
<Button
variant="primary"
className="mt-4 w-[calc(100%-30px)] h-[45px] mx-[15px] text-[17px]"
onClick={() => {
console.log(
"submitted",
@ -89,7 +89,7 @@ const Editor: NextPage = () => {
}}
>
Submit
</PrimaryButton>
</Button>
)}
</div>
)}

View file

@ -1,28 +1,19 @@
// react
import React, { useCallback, useEffect } from "react";
// next
import { useRouter } from "next/router";
// swr
import useSWR, { mutate } from "swr";
// react hook forms
import { useFormContext, useForm, FormProvider } from "react-hook-form";
// services
import issuesService from "services/issues.service";
import { IssueService, IssueArchiveService } from "services/issue";
// fetch key
import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
// hooks
import useUser from "hooks/use-user";
import useProjectMembers from "hooks/use-project-members";
// layouts
import WebViewLayout from "layouts/web-view-layout";
// ui
import { Spinner } from "@plane/ui";
// components
import {
IssueWebViewForm,
@ -31,23 +22,21 @@ import {
IssuePropertiesDetail,
IssueLinks,
IssueActivity,
Spinner,
} from "components/web-view";
// types
import type { IIssue } from "types";
// services
const issueService = new IssueService();
const issueArchiveService = new IssueArchiveService();
const MobileWebViewIssueDetail_ = () => {
const router = useRouter();
const { workspaceSlug, projectId, issueId } = router.query;
const isArchive = Boolean(router.query.archive);
const memberRole = useProjectMembers(
workspaceSlug as string,
projectId as string,
!!workspaceSlug && !!projectId
);
const memberRole = useProjectMembers(workspaceSlug as string, projectId as string, !!workspaceSlug && !!projectId);
const isAllowed = Boolean((memberRole.isMember || memberRole.isOwner) && !isArchive);
@ -63,25 +52,20 @@ const MobileWebViewIssueDetail_ = () => {
} = useSWR(
workspaceSlug && projectId && issueId && !isArchive ? ISSUE_DETAILS(issueId.toString()) : null,
workspaceSlug && projectId && issueId && !isArchive
? () =>
issuesService.retrieve(workspaceSlug.toString(), projectId.toString(), issueId.toString())
? () => issueService.retrieve(workspaceSlug.toString(), projectId.toString(), issueId.toString())
: null
);
const { data: archiveIssueDetails, mutate: mutateaArchiveIssue } = useSWR<IIssue | undefined>(
const { data: archiveIssueDetails, mutate: mutateArchiveIssue } = useSWR<IIssue | undefined>(
workspaceSlug && projectId && issueId && isArchive ? ISSUE_DETAILS(issueId as string) : null,
workspaceSlug && projectId && issueId && isArchive
? () =>
issuesService.retrieveArchivedIssue(
workspaceSlug.toString(),
projectId.toString(),
issueId.toString()
)
issueArchiveService.retrieveArchivedIssue(workspaceSlug.toString(), projectId.toString(), issueId.toString())
: null
);
const issueDetails = isArchive ? archiveIssueDetails : issue;
const mutateIssueDetails = isArchive ? mutateaArchiveIssue : mutateIssue;
const mutateIssueDetails = isArchive ? mutateArchiveIssue : mutateIssue;
useEffect(() => {
if (!issueDetails) return;
@ -91,8 +75,7 @@ const MobileWebViewIssueDetail_ = () => {
description: issueDetails.description,
description_html: issueDetails.description_html,
state: issueDetails.state,
assignees_list:
issueDetails.assignees_list ?? issueDetails.assignee_details?.map((user) => user.id),
assignees_list: issueDetails.assignees_list ?? issueDetails.assignee_details?.map((user) => user.id),
labels_list: issueDetails.labels_list ?? issueDetails.labels,
labels: issueDetails.labels_list ?? issueDetails.labels,
});
@ -122,7 +105,7 @@ const MobileWebViewIssueDetail_ = () => {
delete payload.issue_relations;
delete payload.related_issues;
await issuesService
await issueService
.patchIssue(workspaceSlug as string, projectId as string, issueId as string, payload, user)
.then(() => {
mutateIssueDetails();
@ -155,9 +138,7 @@ const MobileWebViewIssueDetail_ = () => {
return (
<WebViewLayout>
{isArchive && (
<div className="w-full h-screen top-0 left-0 fixed z-50 bg-white/20 pointer-events-none" />
)}
{isArchive && <div className="w-full h-screen top-0 left-0 fixed z-50 bg-white/20 pointer-events-none" />}
<div className="px-6 py-2 h-full overflow-auto space-y-3">
<IssueWebViewForm

View file

@ -7,8 +7,8 @@ import useSWR, { mutate } from "swr";
// next-themes
import { useTheme } from "next-themes";
// services
import userService from "services/user.service";
import workspaceService from "services/workspace.service";
import { UserService } from "services/user.service";
import { WorkspaceService } from "services/workspace.service";
// hooks
import useUserAuth from "hooks/use-user-auth";
import useWorkspaces from "hooks/use-workspaces";
@ -17,17 +17,21 @@ import DefaultLayout from "layouts/default-layout";
// components
import { InviteMembers, JoinWorkspaces, UserDetails, Workspace } from "components/onboarding";
// ui
import { Spinner } from "components/ui";
import { Spinner } from "@plane/ui";
// images
import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png";
import BlackHorizontalLogo from "public/plane-logos/black-horizontal-with-blue-logo.svg";
import WhiteHorizontalLogo from "public/plane-logos/white-horizontal-with-blue-logo.svg";
// types
import { ICurrentUserResponse, IUser, TOnboardingSteps } from "types";
import { IUser, TOnboardingSteps } from "types";
import type { NextPage } from "next";
// fetch-keys
import { CURRENT_USER, USER_WORKSPACE_INVITATIONS } from "constants/fetch-keys";
// services
const userService = new UserService();
const workspaceService = new WorkspaceService();
const Onboarding: NextPage = () => {
const [step, setStep] = useState<number | null>(null);
@ -38,15 +42,13 @@ const Onboarding: NextPage = () => {
const { workspaces } = useWorkspaces();
const userWorkspaces = workspaces?.filter((w) => w.created_by === user?.id);
const { data: invitations } = useSWR(USER_WORKSPACE_INVITATIONS, () =>
workspaceService.userWorkspaceInvitations()
);
const { data: invitations } = useSWR(USER_WORKSPACE_INVITATIONS, () => workspaceService.userWorkspaceInvitations());
// update last active workspace details
const updateLastWorkspace = async () => {
if (!workspaces) return;
await mutate<ICurrentUserResponse>(
await mutate<IUser>(
CURRENT_USER,
(prevData) => {
if (!prevData) return prevData;
@ -54,13 +56,13 @@ const Onboarding: NextPage = () => {
return {
...prevData,
last_workspace_id: workspaces[0]?.id,
workspace: {
...prevData.workspace,
fallback_workspace_id: workspaces[0]?.id,
fallback_workspace_slug: workspaces[0]?.slug,
last_workspace_id: workspaces[0]?.id,
last_workspace_slug: workspaces[0]?.slug,
},
// workspace: {
// ...prevData.workspace,
// fallback_workspace_id: workspaces[0]?.id,
// fallback_workspace_slug: workspaces[0]?.slug,
// last_workspace_id: workspaces[0]?.id,
// last_workspace_slug: workspaces[0]?.slug,
// },
};
},
false
@ -80,7 +82,7 @@ const Onboarding: NextPage = () => {
},
};
mutate<ICurrentUserResponse>(
mutate<IUser>(
CURRENT_USER,
(prevData) => {
if (!prevData) return prevData;
@ -100,7 +102,7 @@ const Onboarding: NextPage = () => {
const finishOnboarding = async () => {
if (!user) return;
mutate<ICurrentUserResponse>(
mutate<IUser>(
CURRENT_USER,
(prevData) => {
if (!prevData) return prevData;
@ -129,13 +131,8 @@ const Onboarding: NextPage = () => {
if (!onboardingStep.profile_complete && step !== 1) setStep(1);
if (onboardingStep.profile_complete) {
if (!onboardingStep.workspace_join && invitations.length > 0 && step !== 2 && step !== 4)
setStep(4);
else if (
!onboardingStep.workspace_create &&
(step !== 4 || onboardingStep.workspace_join) &&
step !== 2
)
if (!onboardingStep.workspace_join && invitations.length > 0 && step !== 2 && step !== 4) setStep(4);
else if (!onboardingStep.workspace_create && (step !== 4 || onboardingStep.workspace_join) && step !== 2)
setStep(2);
}

View file

@ -3,23 +3,16 @@ import React from "react";
import { useRouter } from "next/router";
import useSWR from "swr";
import {
CheckIcon,
CubeIcon,
ShareIcon,
StarIcon,
UserIcon,
XMarkIcon,
} from "@heroicons/react/24/outline";
import { CheckIcon, CubeIcon, ShareIcon, StarIcon, UserIcon, XMarkIcon } from "@heroicons/react/24/outline";
// swr
// services
import workspaceService from "services/workspace.service";
import { WorkspaceService } from "services/workspace.service";
// hooks
import useUser from "hooks/use-user";
// layouts
import DefaultLayout from "layouts/default-layout";
// ui
import { Spinner } from "components/ui";
import { Spinner } from "@plane/ui";
// icons
import { EmptySpace, EmptySpaceItem } from "components/ui/empty-space";
// types
@ -27,6 +20,9 @@ import type { NextPage } from "next";
// constants
import { WORKSPACE_INVITATION } from "constants/fetch-keys";
// services
const workspaceService = new WorkspaceService();
const WorkspaceInvitation: NextPage = () => {
const router = useRouter();
@ -77,11 +73,7 @@ const WorkspaceInvitation: NextPage = () => {
title={`You are already a member of ${invitationDetail.workspace.name}`}
description="Your workspace is where you'll create projects, collaborate on your issues, and organize different streams of work in your Plane account."
>
<EmptySpaceItem
Icon={CubeIcon}
title="Continue to Dashboard"
action={() => router.push("/")}
/>
<EmptySpaceItem Icon={CubeIcon} title="Continue to Dashboard" action={() => router.push("/")} />
</EmptySpace>
</>
) : (