fix: Implementing mobx, refactoring service layer and rewriting components (#2441)
* chore: kanban refactoring * chore: Implemented new kanaban board UX and implemented draggable using react beautiful dnd * chore: updated yarn lock * chore: updated the store for issues and issue filters * chore: resolved build error * chore: created filters and updated the issue filters, display_filter and display_properties in mobx and components * chore: implemented filters for issues * chore: UI theming updates * chore: handled single and multi select in filter cards * chore: implemented filters and views in kanaban * chore: updating filters, display_filter and display properties * chore: filter, layout, display filters, extra filters and display properties render validation * chore: clean up and resolved import warnings * chore: type check * chore: renamed gantt key to gantt_chart * chore: filter render UI and Functionality implementation * chore: filter empty state handling in issue filter selection * Implementing list view * chore: kanban drag drop logic * filtering * chore: store setup * chore: handled build issues * chore: store setup * user filter * chore: store setup * chore: store fixes and static data setup * chore: store setup for build fixes * fix: merge conflicts (#2231) * chore: dynamic position dropdown (#2138) * chore: dynamic position state dropdown for issue view * style: state select dropdown styling * fix: state icon attribute names * chore: state select dynamic dropdown * chore: member select dynamic dropdown * chore: label select dynamic dropdown * chore: priority select dynamic dropdown * chore: label select dropdown improvement * refactor: state dropdown location * chore: dropdown improvement and code refactor * chore: dynamic dropdown hook type added --------- Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com> * fix: fields not getting selected in the create issue form (#2212) * fix: hydration error and draft issue workflow * fix: build error * fix: properties getting de-selected after create, module & cycle not getting auto-select on the form * fix: display layout, props being updated directly * chore: sub issues count in individual issue (#2221) * fix: service imports * chore: rename csv service file --------- Co-authored-by: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Co-authored-by: Dakshesh Jain <65905942+dakshesh14@users.noreply.github.com> Co-authored-by: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com> * chore: store fixes * chore: update issue detail store to handle peek overview (#2237) * chore: dynamic position dropdown (#2138) * chore: dynamic position state dropdown for issue view * style: state select dropdown styling * fix: state icon attribute names * chore: state select dynamic dropdown * chore: member select dynamic dropdown * chore: label select dynamic dropdown * chore: priority select dynamic dropdown * chore: label select dropdown improvement * refactor: state dropdown location * chore: dropdown improvement and code refactor * chore: dynamic dropdown hook type added --------- Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com> * fix: fields not getting selected in the create issue form (#2212) * fix: hydration error and draft issue workflow * fix: build error * fix: properties getting de-selected after create, module & cycle not getting auto-select on the form * fix: display layout, props being updated directly * chore: sub issues count in individual issue (#2221) * Implemented nested issues in the sub issues section in issue detail page (#2233) * feat: subissues infinte level * feat: updated UI for sub issues * feat: subissues new ui and nested sub issues in issue detail * chore: removed repeated code * refactor: product updates modal layout (#2225) * fix: handle no issues in custom analytics (#2226) * fix: activity label color (#2227) * fix: profile issues layout switch (#2228) * chore: update service imports * chore: update issue detail store to handle peek overview --------- Co-authored-by: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Co-authored-by: Dakshesh Jain <65905942+dakshesh14@users.noreply.github.com> Co-authored-by: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com> Co-authored-by: guru_sainath <gurusainath007@gmail.com> * chore: minor fixes * workspace project fixes * feat: project issues topbar (#2256) * chore: project issues topbar * style: theming and minor UI fixes * refactor: file structure * chore: layout wise authorization added * style: filter dropdowns * chore: add fetch keys * chore: minor fixes * chore: filters dropdown (#2260) * chore: project issues topbar * style: theming and minor UI fixes * refactor: file structure * chore: layout wise authorization added * style: filter dropdowns * chore: add fetch keys * feat: search option for filters * fix: sticky headers * chore: sub_group_by section added * fix: leave project fixes * refactor: project card component refactor * Implemented swimlanes and kanban view (#2262) * chore: issue store for kanban and calendar * chore: updated ui for kanba and swimlanes * chore: yarn.lock updated * fix: computed filters logic * chore: added sub_group_by in params and handled sub-group-by render error in display filter's * fix: ui package setup and project update form refactor * fix: ui package setup * fix: minor ui fixes * dev: calendar view layout revamp (#2293) * dev: calendar view init * chore: new render logic * chore: implement calendar view * chore: calendar view * refactor: calendar payload * chore: remove active month logic from backend * chore: setup new store for calendar * refactor: issues fetching structure * chore: months dropdown * chore: modify request query params for calendar layout * refactor: remove console logs and add comments * chore: removed demo m-store routes * cycles changes * chore: issues grouped kanban and swimlanes UI and functionality (#2294) * chore: updated the all the group_by and sub_group_by UI and functionality render in kanban * chore: kanban sorting in mobx and ui updates * chore: ui changes and drag and drop functionality changes in kanban * chore: issues count render in kanban default and swimlanes * chore: Added icons to the group_by and sub_group_by in kanban and swimlanes * refactor: filter components, constants and helper functions (#2297) * refactor: filters and display filters to accept handlers as props * refactor: filters and display filters folder structure * refactor: change issue layout options constant structure * chore: display filters validations * chore: view less filters functionality * fix: display filters validation * refactor: wrap functions around useCallback * chore: start and target date filter options added * refactor: query params generator function * fix: query params generator function * dev: gantt chart implementation using MobX (#2302) * dev: fetch project gantt issues using mobx * chore: handle group by options in the kanban layout * dev: spreadsheet layout implementation using MobX (#2306) * dev: implement spreadsheet view using mobx * refactor: remove console logs and props * chore: refactoring cycles list * feat: adding additional ui components * dev: applied filters list implementation using MobX (#2325) * dev: applied filters list UI * fix: filter item height * chore: remove unnecessary classes * fix: params generator * fix: cycles views list and board * fix: cycles list rendering fixes * fix: layout fixes * refactor: filter components (#2359) * fix: calendar layout dividers * refactor: filter selection components * fix: dropdown closing after selection * refactor: filters components * chore: issue properties for list and kanban layouts and implemented estimates in project store (#2363) * chore: issue properties for state, priorit, labels and members * feat: implemented assignee, labels properties * fix: implemented estimates in project store and issue properties * chore: staer_date and due_date and validation properties in kanban * chore: filters import conflict * dev: setup module and module filter store (#2364) * dev: implement module issues using mobx store * dev: module filter store setup * chore: module store crud operations * chore: issue list layout (#2367) * chore: merge develop (#2388) * fix: build erros * chore: cycles, modules store integration, list and kanban layouts and updated kanban logic (#2399) * chore: cycle, cycle-issue, cycle-filters, cycle-kanban, cycle layout setup * chore: cycles kanban and list view store * chore: cycles, modules kanban and list, kanban view store * refactor: change naming convention (#2383) * fix:auth layer revamp * chore: Implemented list and kanban views in project modules (#2402) * chore: updated kanban logic in project cycles and modules * chore: updated list and kanban in module * dev: implement global views using MobX (#2404) * fix: selfhosted fixes (#2154) * fix: selfhosted fixes * fix: updated env example * chore: dynamic position dropdown (#2138) * chore: dynamic position state dropdown for issue view * style: state select dropdown styling * fix: state icon attribute names * chore: state select dynamic dropdown * chore: member select dynamic dropdown * chore: label select dynamic dropdown * chore: priority select dynamic dropdown * chore: label select dropdown improvement * refactor: state dropdown location * chore: dropdown improvement and code refactor * chore: dynamic dropdown hook type added --------- Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com> * fix: fields not getting selected in the create issue form (#2212) * fix: hydration error and draft issue workflow * fix: build error * fix: properties getting de-selected after create, module & cycle not getting auto-select on the form * fix: display layout, props being updated directly * chore: sub issues count in individual issue (#2221) * Implemented nested issues in the sub issues section in issue detail page (#2233) * feat: subissues infinte level * feat: updated UI for sub issues * feat: subissues new ui and nested sub issues in issue detail * chore: removed repeated code * refactor: product updates modal layout (#2225) * fix: handle no issues in custom analytics (#2226) * fix: activity label color (#2227) * fix: profile issues layout switch (#2228) * fix: issues resolved in sub issues (#2238) * fix: aws region name (#2234) * chore: updated docker naming conventions (#2239) * naming convention changes * dev: update docker-compose-hub in consistent with docker-compose * dev: updated docker container name --------- Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com> * chore: added state and priority order in workspace user profile (#2241) * fix: changed priority from None to none (#2229) * fix: cycle and module stats when issues are archived (#2185) * fix: cycle and module stats when issues are archived * fix: added draft filter --------- Co-authored-by: NarayanBavisetti <narayan3119@gmail.com> * feat: quick add (#2240) * feat: quick add * style: made text color muted * chore: added epoch in draft (#2244) * chore: added epoch in draft * chore: removed extra spaces * fix: resolved pending issue graph in analytics, user wishes in dashboard, and typo in projects list (#2247) * style: settings page improvement (#2211) * style: settings page improvement * style: toggle switch styling --------- Co-authored-by: Anmol Singh Bhatia <asb@Anmols-MacBook-Pro.local> * chore: changed priority props in workspace and project (#2253) * fix: bug fix related to fetching dropdown options for the profile issue (#2246) * fix: sub issue state and member select build error (#2254) * rename view to layout (#2255) Co-authored-by: Your Name <you@example.com> * fix: bug fixes and ui improvement (#2250) * dev: remove auto filter endpoint * feat: quick-add placement in spreadsheet and gantt (#2259) * feat: sticking quick-add at the bottom of the screen fix: opening create issue modal instead of quick-add in draft-issues, my-issue and profile page * fix: build error due to dynamic import * fix: draft issue delete not working (#2249) * fix: draft issue not deleting, project can't be changed in draft issue modal * fix: removed mutation for view where draft issues are not shown * fix: inline create issue for draft issue * fix: clearing data from localstorage on discard click * feat: Add peek overview in sub issues and updated UI for empty states. (#2263) * chore: add tooltip to show full time on activity logs (#2235) * fix: issue automation iterable error (#2208) * fix: n+1 queries for cycle list and project member endpoints (#2257) * [fix] nginx continuously rewriting and reloading on index page of spaces app (#2236) * chore: shifted index page to /home route * chore: added rewrite logic, to rewrite index to /home * chore: routed home to login route as login page * chore: updated nginx config to route to login * chore: updated path for home * dev: migration for 0.13 (#2266) * dev: updated migrations * dev: migration for 0.13 * dev: re-split migrations into two different files (#2268) * dev: split issue activity migration separate files * dev: resplit migrations into two different files * dev: changed the batch size * chore: udpate date filters to support dynamic options * fix: bugs in quick-add and draft issues (#2269) * fix: 'Last Drafted Issue' making sidebar look weird on collapsed * feat: scroll to the bottom when issue is created * fix: 'Add Issue' button overlapping issue card in spreadsheet view * fix: wrong placement of quick-add in calender layout * fix: spacing for issue card in spreadsheet view * chore: add instructions to contributing guide (#2270) * chore: add instructions to contributing guide * dev: update contributing.md to use the new configuration --------- Co-authored-by: Nikhil <118773738+pablohashescobar@users.noreply.github.com> * fix: user dashboard greeting timezone (#2267) * chore: user greeting timezone * fix: group by labels not working on workspace level * feat: workspace global view, style: spreadsheet view revamp (#2273) * chore: workspace view types, services and hooks added * style: spreadsheet view revamp and code refactor * feat: workspace view * fix: build fix * chore: sidebar workspace issues redirection updated * style: gantt layout quick-add padding (#2272) * fix: 'Last Drafted Issue' making sidebar look weird on collapsed * feat: scroll to the bottom when issue is created * fix: 'Add Issue' button overlapping issue card in spreadsheet view * fix: wrong placement of quick-add in calender layout * fix: spacing for issue card in spreadsheet view * style: gantt layout quick-add padding style: removed 'State group' from draft issue * style: decrese shadow, quick-add position on calender layout, and 'add issue' sticky * style: button color * fix: block click happening while moving (#2275) * dev: refactor date filters to a single function * chore: handle calendar date range in frontend (#2277) * chore: gantt chart empty state (#2279) * chore: gantt empty state * chore: Add heading to the gantt sidebar * style: calender quick-add same width as single date (#2280) * style: calender quick-add same width as single date * style: margin bottom in quick-add in spreadsheet view * fix: quick add opening in list-layout * style: reduced margin left * chore: updated created at in draft issue (#2278) * chore: make target dates inclusive when filtering (#2276) * chore: sort order and issue props for global views (#2283) * chore: removed project filter (#2284) * fix: inbox issue deletes (#2290) * chore: views (#2288) * chore: global views order by * chore: update permissions for global views --------- Co-authored-by: NarayanBavisetti <narayan3119@gmail.com> * chore: fetch issues from previous and next month in the calendar view (#2282) * fix: issue activity estimate value bug fix (#2281) * fix: issue activity estimate value bug fix * fix: activity typo fix * fix: ui and bugs (#2289) * fix: 24 character limit on first & last name in onboarding page * fix: no option: 'Add Issue' in archive issue page * fix: in archive issue directly sending to issue detail page * fix: issue type showing in archive issue * fix: custom menu overflowing * fix: changing subscriber in filters has no effect * style: border in quick-add * fix: on onboarding member role overflowing * fix: inconsistent icons in issue detail * style: spacing, borders and shadows in quick-add * fix: custom menu truncate * fix: notifications for created by me and assigned to me (#2292) * chore: workspace view display filters and properties , code refactor (#2295) * chore: spreadsheet view context * chore: spreadsheet context provider * chore: spreadsheet view context * chore: display filters and properties added in workspace view and code refactor * fix: build error fix * chore: set sub-issue display option to false for global views --------- Co-authored-by: gurusainath <gurusainath007@gmail.com> * chore: label create error (#2299) * chore: global issues ui improvement and bug fixes (#2300) * chore: workspace view mutation fix ,bug fixes and code refactor (#2301) * chore: workspace view mutation fix ,bug fixes and code refactor * chore: update workspace view toast alert added * chore: workspace view order by removed (#2303) * dev: updated migrations for 0.13-dev (#2305) * chore: epoch migration batch size changed * chore: reoredered the migration files * dev: updated migrations for 0.13-dev * chore: added epoch field * dev: merged the migration files * fix: workspace view filters count fix (#2307) * fix: unsplash api fix (#2310) * fix: workspace view redirection fix, style: spreadsheet view shadow scroll fix (#2314) * fix: workspace view redirection fix * style: spreadsheet view scroll shadow fix * fix: update build workflow for the deploy app (#2315) * fix: workspace view add issue mutation fix (#2317) * dev: create action to sync PR changes to the repo (#2333) * fix: ui package readme added (#2334) * fix: variable name for token (#2336) * dev: update add permissions to the action (#2337) * dev: rename token variables (#2338) * fix: updated readme fixes (#2339) * dev: update sync workflow to run only when the source repo is configured (#2346) * dev: update sync workflow to run only when the source repo is configured * fix: naming convention changes --------- Co-authored-by: sriramveeraghanta <veeraghanta.sriram@gmail.com> * fix: issue relation mutation and draft issue (#2340) * fix: issue relation mutation and draft issue * fix: 'New Issue' in gantt view fix: emoji select going under * fix: profile page typo * fix: sync workflow fixes (#2365) * fix: sync job pr description escaped values fix (#2366) * Update index.tsx (#2343) Fixes #2342 * dev: update apiserver configuration files (#2348) * dev: update apiserver configuration files * dev: add email and minio redirection urls * fix: themening validation in store init. (#2350) * chore: member can change role (#2371) * chore: removed the issue draft log from my profile (#2368) * adding sync info in pr title (#2373) * chore: layout access validation and switch in plane deploy issues route (#2351) * chore: handled route validation and layout access validation in plane deploy issues * chore: impoved validation condition * show current version in the help section dropdown (#2353) * fix: table menu positioning (#2354) * fix: handle cross project issues in the sub-issues. (#2357) * fix: login process validation based on api config (#2361) * dev: configuration endpoint for frontend client (#2355) * dev: configuration endpoint for frontend clients * dev: configuration enable magic and email/password signup * dev: update unsplash keys * dev: add unsplash API and add env for magic login * fix: 404 when redirecting user clicks on Sign In button (#2349) * fix: 404 when redirecting user to login page * fix: next_path redirection not working * fix: authentication workflow update in plane deploy --------- Co-authored-by: gurusainath <gurusainath007@gmail.com> * fix: project setting member role validation (#2369) * fix: project setting member role validation * chore: opacity removed from member setting page * chore: member setting page validation * chore: project covers endpoint (#2370) * chore: project covers endpoint * dev: remove print logs * dev: formatting --------- Co-authored-by: sriramveeraghanta <veeraghanta.sriram@gmail.com> * feat: default project cover images tab on the change cover popover (#2375) * feat: default project cover images tab * chore: remove unnecessary env vars from turbo.json * chore: remove unnecessary OAuth envs (#2378) * chore: remove unnecessary oauth envs * merge conflicts resolved * fix: adding new service --------- Co-authored-by: sriramveeraghanta <veeraghanta.sriram@gmail.com> * fix: added user store variables in mobx store observable (#2380) * fix: state group icons (#2381) * fix: removed default theme setting in the index page (#2382) * fix: removed default theme setting in the index page * fix: empty space * dev: global views and workspace filters store implemented * sync CE Master to EE Develop * refactor: create update view modal * chore: static issue global views * refactor: remove old code * refactor: filters select dropdown * chore: fix calendar layout * chore: mobx store for new applied filters * chore: dded search functionality --------- Co-authored-by: Vamsi Kurama <vamsi.kurama@gmail.com> Co-authored-by: Nikhil <118773738+pablohashescobar@users.noreply.github.com> Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com> Co-authored-by: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Co-authored-by: Dakshesh Jain <65905942+dakshesh14@users.noreply.github.com> Co-authored-by: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com> Co-authored-by: guru_sainath <gurusainath007@gmail.com> Co-authored-by: NarayanBavisetti <narayan3119@gmail.com> Co-authored-by: Anmol Singh Bhatia <asb@Anmols-MacBook-Pro.local> Co-authored-by: Rhea Jain <65884341+rhea0110@users.noreply.github.com> Co-authored-by: Your Name <you@example.com> Co-authored-by: pablohashescobar <nikhilschacko@gmail.com> Co-authored-by: Henit Chobisa <chobisa.henit@gmail.com> Co-authored-by: Thomas <git@thomasync.dev> Co-authored-by: Luis Cruz <55716036+luis-cruzt@users.noreply.github.com> Co-authored-by: Manish Gupta <manish@mgupta.me> * fix: Auth fixes and Layout fixes (#2408) * fix: auth fixes and layout improvements * fix: layout fixes * fix: analytics page fixes * dev: implemented project views using MobX (#2410) * dev: implemented project views list using mobx * style: views list UI * dev: implemented view issues page using mobx * refactor: project view issues fetching * chore: plane ui library component and code refactor (#2406) * chore: swap input component with plane/ui package * chore: swap textarea component with plane/ui package * chore: swap button component with plane/ui package * chore: button component revamp * fix: button type fix * chore: secondary button revamp * chore: button props updated * chore: swap loader component with plane/ui package * fix: build error fix * chore: button component refactor * chore: code refactor * chore: swap toggle switch component with plane/ui package * chore: swap spinner component with plane/ui package * chore: swap progress bar componenet with plan/ui package * chore: code refactor * fix: gitignore fixes * fix: project card fixes * chore: ui component revamp (#2415) * chore: swap tooltip component with plane ui package * chore: swap linear progress component with plane ui package * fix: login button fix * chore: implement new worksapace wrapper for global views (#2412) * chore: implement new worksapace wrapper for global views pages * fix: merge conflicts * fix: merge conflicts * dev: add remaining layouts to cycle (#2413) * fix: workspace auth wrapper changes * chore: project card revamp and refactor (#2416) * removing dist from ui * refactor: analytics (#2419) * refactor: helper functions * chore: updated all the page headers * refactor: custom analytics * refactor: project analytics modal * refactor: folder structure, remove junk code (#2423) * refactor: folder structure * chore: ad order by target date option * refactor: remove old layout components * refactor: inbox folder structure * fix: services fixes * fix: store imports changes * fix: services export fixes * fix: services implementation fixes * fix: build issue fixes * fix: react library fixes * refactor: MobX store folder structure (#2435) * refactor: store folder structure * chore: update import statements * fix: service import errors (#2436) * fix: service imports * chore: update service imports in store * chore: fix remianing service imports * build fixes * editor ts config fixes * fix: turbo and build fixes * fix: Auth screen loading implementation * fix: build issues * fix: turbo settings for ui package --------- Co-authored-by: gurusainath <gurusainath007@gmail.com> Co-authored-by: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Co-authored-by: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Co-authored-by: Dakshesh Jain <65905942+dakshesh14@users.noreply.github.com> Co-authored-by: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com> Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com> Co-authored-by: Vamsi Kurama <vamsi.kurama@gmail.com> Co-authored-by: Nikhil <118773738+pablohashescobar@users.noreply.github.com> Co-authored-by: NarayanBavisetti <narayan3119@gmail.com> Co-authored-by: Anmol Singh Bhatia <asb@Anmols-MacBook-Pro.local> Co-authored-by: Rhea Jain <65884341+rhea0110@users.noreply.github.com> Co-authored-by: Your Name <you@example.com> Co-authored-by: pablohashescobar <nikhilschacko@gmail.com> Co-authored-by: Henit Chobisa <chobisa.henit@gmail.com> Co-authored-by: Thomas <git@thomasync.dev> Co-authored-by: Luis Cruz <55716036+luis-cruzt@users.noreply.github.com> Co-authored-by: Manish Gupta <manish@mgupta.me>
This commit is contained in:
parent
c6e021d41f
commit
d80a593520
758 changed files with 30452 additions and 23632 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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={
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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={
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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" && (
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -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 && (
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
40
web/pages/[workspaceSlug]/workspace-views/[globalViewId].tsx
Normal file
40
web/pages/[workspaceSlug]/workspace-views/[globalViewId].tsx
Normal 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;
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
71
web/pages/accounts/forgot-password.tsx
Normal file
71
web/pages/accounts/forgot-password.tsx
Normal 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;
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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.",
|
||||
})
|
||||
);
|
||||
};
|
||||
|
|
@ -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 (
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
@ -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>
|
||||
</>
|
||||
) : (
|
||||
Loading…
Add table
Add a link
Reference in a new issue