From 4dec6761d02c7a075ed245f0710a942af1ff0277 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Wed, 11 Oct 2023 12:07:04 +0530 Subject: [PATCH] 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 * 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 * 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 * 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 * 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 * 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 * 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 * 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 * 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 * 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 * 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 * 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 Co-authored-by: Nikhil <118773738+pablohashescobar@users.noreply.github.com> Co-authored-by: sriram veeraghanta 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 Co-authored-by: NarayanBavisetti Co-authored-by: Anmol Singh Bhatia Co-authored-by: Rhea Jain <65884341+rhea0110@users.noreply.github.com> Co-authored-by: Your Name Co-authored-by: pablohashescobar Co-authored-by: Henit Chobisa Co-authored-by: Thomas Co-authored-by: Luis Cruz <55716036+luis-cruzt@users.noreply.github.com> Co-authored-by: Manish Gupta --- .../assignee-column/assignee-column.tsx | 13 +- .../spreadsheet-assignee-column.tsx | 8 +- .../created-on-column/created-on-column.tsx | 15 +- .../spreadsheet-created-on-column.tsx | 18 +- .../issue-column/issue-column.tsx | 22 +- .../issue-column/spreadsheet-issue-column.tsx | 6 +- .../spreadsheet-view/spreadsheet-view.tsx | 540 +++++++----------- .../state-column/spreadsheet-state-column.tsx | 10 +- .../state-column/state-column.tsx | 22 +- web/components/headers/global-issues.tsx | 158 +++++ web/components/headers/index.ts | 1 + web/components/headers/module-issues.tsx | 19 +- web/components/headers/project-issues.tsx | 19 +- .../issue-layouts/calendar/calendar.tsx | 8 +- .../issue-layouts/calendar/module-root.tsx | 7 +- .../issues/issue-layouts/calendar/root.tsx | 7 +- .../filters/applied-filters/filters-list.tsx | 34 +- .../applied-filters/global-views-root.tsx | 102 ++++ .../filters/applied-filters/index.ts | 2 + .../filters/applied-filters/project.tsx | 49 ++ .../filters/applied-filters/root.tsx | 2 +- .../display-filters-selection.tsx | 44 +- .../display-filters/display-properties.tsx | 29 +- .../header/filters/filters-selection.tsx | 55 +- .../filters/header/filters/index.ts | 1 + .../filters/header/filters/project.tsx | 81 +++ .../filters/header/helpers/dropdown.tsx | 50 +- .../filters/header/helpers/filter-option.tsx | 2 +- .../global-views-all-layouts.tsx | 88 +++ web/components/issues/issue-layouts/index.ts | 1 + web/components/issues/modal.tsx | 9 +- .../workspace-views/workpace-view-issues.tsx | 232 -------- .../workspace-views/workspace-all-issue.tsx | 236 -------- .../workspace-assigned-issue.tsx | 148 ----- .../workspace-created-issues.tsx | 147 ----- .../workspace-issue-view-option.tsx | 116 ---- .../workspace-subscribed-issue.tsx | 148 ----- web/components/workspace/index.ts | 1 + .../views/default-view-list-item.tsx | 36 ++ ...e-view-modal.tsx => delete-view-modal.tsx} | 78 +-- web/components/workspace/views/form.tsx | 189 +++--- .../workspace/views/global-select-filters.tsx | 301 ---------- web/components/workspace/views/header.tsx | 67 +++ web/components/workspace/views/index.ts | 7 + web/components/workspace/views/modal.tsx | 108 ++-- .../views/single-workspace-view-item.tsx | 110 ---- .../workspace/views/view-list-item.tsx | 97 ++++ web/components/workspace/views/views-list.tsx | 41 ++ .../views/workpace-view-navigation.tsx | 105 ---- web/constants/fetch-keys.ts | 9 +- web/constants/issue.ts | 19 +- web/constants/workspace.ts | 23 + web/contexts/workspace-member.context.tsx | 3 +- web/helpers/emoji.helper.tsx | 19 +- web/helpers/filter.helper.ts | 19 + web/lib/mobx/store-init.tsx | 6 +- web/package.json | 4 +- .../workspace-views/[globalViewId].tsx | 57 ++ .../workspace-views/all-issues.tsx | 76 +-- .../workspace-views/assigned.tsx | 78 +-- .../workspace-views/created.tsx | 78 +-- .../[workspaceSlug]/workspace-views/index.tsx | 177 +----- .../workspace-views/issues.tsx | 40 -- .../workspace-views/subscribed.tsx | 78 +-- web/services/workspace.service.ts | 15 +- web/store/global-views.ts | 0 web/store/global_view_filters.ts | 70 +++ web/store/global_view_issues.ts | 167 ++++++ web/store/global_views.ts | 207 +++++++ web/store/project.ts | 6 +- web/store/root.ts | 24 +- web/store/workspace.ts | 100 +++- web/store/workspace_filters.ts | 195 +++++++ web/types/index.d.ts | 1 + web/types/view-props.d.ts | 46 +- web/types/workspace-views.d.ts | 8 +- 76 files changed, 2373 insertions(+), 2741 deletions(-) create mode 100644 web/components/headers/global-issues.tsx create mode 100644 web/components/issues/issue-layouts/filters/applied-filters/global-views-root.tsx create mode 100644 web/components/issues/issue-layouts/filters/applied-filters/project.tsx create mode 100644 web/components/issues/issue-layouts/filters/header/filters/project.tsx create mode 100644 web/components/issues/issue-layouts/global-views-all-layouts.tsx delete mode 100644 web/components/issues/workspace-views/workpace-view-issues.tsx delete mode 100644 web/components/issues/workspace-views/workspace-all-issue.tsx delete mode 100644 web/components/issues/workspace-views/workspace-assigned-issue.tsx delete mode 100644 web/components/issues/workspace-views/workspace-created-issues.tsx delete mode 100644 web/components/issues/workspace-views/workspace-issue-view-option.tsx delete mode 100644 web/components/issues/workspace-views/workspace-subscribed-issue.tsx create mode 100644 web/components/workspace/views/default-view-list-item.tsx rename web/components/workspace/views/{delete-workspace-view-modal.tsx => delete-view-modal.tsx} (69%) delete mode 100644 web/components/workspace/views/global-select-filters.tsx create mode 100644 web/components/workspace/views/header.tsx create mode 100644 web/components/workspace/views/index.ts delete mode 100644 web/components/workspace/views/single-workspace-view-item.tsx create mode 100644 web/components/workspace/views/view-list-item.tsx create mode 100644 web/components/workspace/views/views-list.tsx delete mode 100644 web/components/workspace/views/workpace-view-navigation.tsx create mode 100644 web/helpers/filter.helper.ts create mode 100644 web/pages/[workspaceSlug]/workspace-views/[globalViewId].tsx delete mode 100644 web/pages/[workspaceSlug]/workspace-views/issues.tsx delete mode 100644 web/store/global-views.ts create mode 100644 web/store/global_view_filters.ts create mode 100644 web/store/global_view_issues.ts create mode 100644 web/store/global_views.ts create mode 100644 web/store/workspace_filters.ts diff --git a/web/components/core/views/spreadsheet-view/assignee-column/assignee-column.tsx b/web/components/core/views/spreadsheet-view/assignee-column/assignee-column.tsx index 86c929ab713..29ba6886571 100644 --- a/web/components/core/views/spreadsheet-view/assignee-column/assignee-column.tsx +++ b/web/components/core/views/spreadsheet-view/assignee-column/assignee-column.tsx @@ -12,20 +12,13 @@ import { ICurrentUserResponse, IIssue, Properties } from "types"; type Props = { issue: IIssue; projectId: string; - partialUpdateIssue: (formData: Partial, issue: IIssue) => void; + onChange: (formData: Partial) => void; properties: Properties; user: ICurrentUserResponse | undefined; isNotAllowed: boolean; }; -export const AssigneeColumn: React.FC = ({ - issue, - projectId, - partialUpdateIssue, - properties, - user, - isNotAllowed, -}) => { +export const AssigneeColumn: React.FC = ({ issue, projectId, onChange, properties, user, isNotAllowed }) => { const router = useRouter(); const { workspaceSlug } = router.query; @@ -36,7 +29,7 @@ export const AssigneeColumn: React.FC = ({ if (newData.includes(data)) newData.splice(newData.indexOf(data), 1); else newData.push(data); - partialUpdateIssue({ assignees_list: data }, issue); + onChange({ assignees_list: data }); trackEventServices.trackIssuePartialPropertyUpdateEvent( { diff --git a/web/components/core/views/spreadsheet-view/assignee-column/spreadsheet-assignee-column.tsx b/web/components/core/views/spreadsheet-view/assignee-column/spreadsheet-assignee-column.tsx index a864126c616..75370d193e5 100644 --- a/web/components/core/views/spreadsheet-view/assignee-column/spreadsheet-assignee-column.tsx +++ b/web/components/core/views/spreadsheet-view/assignee-column/spreadsheet-assignee-column.tsx @@ -10,7 +10,7 @@ import { ICurrentUserResponse, IIssue, Properties } from "types"; type Props = { issue: IIssue; projectId: string; - partialUpdateIssue: (formData: Partial, issue: IIssue) => void; + handleUpdateIssue: (issueId: string, data: Partial) => void; expandedIssues: string[]; properties: Properties; user: ICurrentUserResponse | undefined; @@ -20,7 +20,7 @@ type Props = { export const SpreadsheetAssigneeColumn: React.FC = ({ issue, projectId, - partialUpdateIssue, + handleUpdateIssue, expandedIssues, properties, user, @@ -36,7 +36,7 @@ export const SpreadsheetAssigneeColumn: React.FC = ({ issue={issue} projectId={projectId} properties={properties} - partialUpdateIssue={partialUpdateIssue} + onChange={(data) => handleUpdateIssue(issue.id, data)} user={user} isNotAllowed={isNotAllowed} /> @@ -50,7 +50,7 @@ export const SpreadsheetAssigneeColumn: React.FC = ({ key={subIssue.id} issue={subIssue} projectId={subIssue.project_detail.id} - partialUpdateIssue={partialUpdateIssue} + handleUpdateIssue={handleUpdateIssue} expandedIssues={expandedIssues} properties={properties} user={user} diff --git a/web/components/core/views/spreadsheet-view/created-on-column/created-on-column.tsx b/web/components/core/views/spreadsheet-view/created-on-column/created-on-column.tsx index cff1f99aaa2..12c33abfc90 100644 --- a/web/components/core/views/spreadsheet-view/created-on-column/created-on-column.tsx +++ b/web/components/core/views/spreadsheet-view/created-on-column/created-on-column.tsx @@ -1,27 +1,16 @@ import React from "react"; // types -import { ICurrentUserResponse, IIssue, Properties } from "types"; +import { IIssue, Properties } from "types"; // helper import { renderLongDetailDateFormat } from "helpers/date-time.helper"; type Props = { issue: IIssue; - projectId: string; - partialUpdateIssue: (formData: Partial, issue: IIssue) => void; properties: Properties; - user: ICurrentUserResponse | undefined; - isNotAllowed: boolean; }; -export const CreatedOnColumn: React.FC = ({ - issue, - projectId, - partialUpdateIssue, - properties, - user, - isNotAllowed, -}) => ( +export const CreatedOnColumn: React.FC = ({ issue, properties }) => (
{properties.created_on && ( diff --git a/web/components/core/views/spreadsheet-view/created-on-column/spreadsheet-created-on-column.tsx b/web/components/core/views/spreadsheet-view/created-on-column/spreadsheet-created-on-column.tsx index 3ce3f2dbe57..2b4513b1079 100644 --- a/web/components/core/views/spreadsheet-view/created-on-column/spreadsheet-created-on-column.tsx +++ b/web/components/core/views/spreadsheet-view/created-on-column/spreadsheet-created-on-column.tsx @@ -9,8 +9,7 @@ import { ICurrentUserResponse, IIssue, Properties } from "types"; type Props = { issue: IIssue; - projectId: string; - partialUpdateIssue: (formData: Partial, issue: IIssue) => void; + handleUpdateIssue: (formData: Partial) => void; expandedIssues: string[]; properties: Properties; user: ICurrentUserResponse | undefined; @@ -19,8 +18,7 @@ type Props = { export const SpreadsheetCreatedOnColumn: React.FC = ({ issue, - projectId, - partialUpdateIssue, + handleUpdateIssue, expandedIssues, properties, user, @@ -32,14 +30,7 @@ export const SpreadsheetCreatedOnColumn: React.FC = ({ return (
- + {isExpanded && !isLoading && @@ -49,8 +40,7 @@ export const SpreadsheetCreatedOnColumn: React.FC = ({ void; setCurrentProjectId: React.Dispatch>; disableUserActions: boolean; - userAuth: UserAuth; nestingLevel: number; }; @@ -43,7 +37,6 @@ export const IssueColumn: React.FC = ({ handleDeleteIssue, setCurrentProjectId, disableUserActions, - userAuth, nestingLevel, }) => { const [isOpen, setIsOpen] = useState(false); @@ -64,11 +57,8 @@ export const IssueColumn: React.FC = ({ }; const handleCopyText = () => { - const originURL = - typeof window !== "undefined" && window.location.origin ? window.location.origin : ""; - copyTextToClipboard( - `${originURL}/${workspaceSlug}/projects/${projectId}/issues/${issue.id}` - ).then(() => { + const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : ""; + copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`).then(() => { setToastAlert({ type: "success", title: "Link Copied!", @@ -79,8 +69,6 @@ export const IssueColumn: React.FC = ({ const paddingLeft = `${nestingLevel * 54}px`; - const isNotAllowed = userAuth.isGuest || userAuth.isViewer; - return (
{properties.key && ( @@ -93,7 +81,7 @@ export const IssueColumn: React.FC = ({ {issue.project_detail?.identifier}-{issue.sequence_id} - {!isNotAllowed && !disableUserActions && ( + {!disableUserActions && (
void; setCurrentProjectId: React.Dispatch>; disableUserActions: boolean; - userAuth: UserAuth; nestingLevel?: number; }; @@ -29,7 +28,6 @@ export const SpreadsheetIssuesColumn: React.FC = ({ handleIssueAction, setCurrentProjectId, disableUserActions, - userAuth, nestingLevel = 0, }) => { const handleToggleExpand = (issueId: string) => { @@ -61,7 +59,6 @@ export const SpreadsheetIssuesColumn: React.FC = ({ handleDeleteIssue={() => handleIssueAction(issue, "delete")} setCurrentProjectId={setCurrentProjectId} disableUserActions={disableUserActions} - userAuth={userAuth} nestingLevel={nestingLevel} /> @@ -80,7 +77,6 @@ export const SpreadsheetIssuesColumn: React.FC = ({ handleIssueAction={handleIssueAction} setCurrentProjectId={setCurrentProjectId} disableUserActions={disableUserActions} - userAuth={userAuth} nestingLevel={nestingLevel + 1} /> ))} diff --git a/web/components/core/views/spreadsheet-view/spreadsheet-view.tsx b/web/components/core/views/spreadsheet-view/spreadsheet-view.tsx index 94c30e309b4..152ede98d05 100644 --- a/web/components/core/views/spreadsheet-view/spreadsheet-view.tsx +++ b/web/components/core/views/spreadsheet-view/spreadsheet-view.tsx @@ -1,10 +1,9 @@ -import React, { useCallback, useEffect, useRef, useState } from "react"; - -// next +import React, { useEffect, useRef, useState } from "react"; import { useRouter } from "next/router"; +import { observer } from "mobx-react-lite"; -import { KeyedMutator, mutate } from "swr"; - +// hooks +import useLocalStorage from "hooks/use-local-storage"; // components import { ListInlineCreateIssueForm, @@ -21,50 +20,34 @@ import { } from "components/core"; import { CustomMenu, Icon, Spinner } from "components/ui"; import { IssuePeekOverview } from "components/issues"; -// hooks -import useIssuesProperties from "hooks/use-issue-properties"; -import useLocalStorage from "hooks/use-local-storage"; -import { useWorkspaceView } from "hooks/use-workspace-view"; // types -import { ICurrentUserResponse, IIssue, ISubIssueResponse, TIssueOrderByOptions, UserAuth } from "types"; -import { - CYCLE_DETAILS, - CYCLE_ISSUES_WITH_PARAMS, - MODULE_DETAILS, - MODULE_ISSUES_WITH_PARAMS, - PROJECT_ISSUES_LIST_WITH_PARAMS, - SUB_ISSUES, - VIEW_ISSUES, - WORKSPACE_VIEW_ISSUES, -} from "constants/fetch-keys"; -import issueService from "services/issue.service"; +import { IIssue, IIssueDisplayFilterOptions, IIssueDisplayProperties, TIssueOrderByOptions } from "types"; // icon import { CheckIcon, ChevronDownIcon, PlusIcon } from "lucide-react"; type Props = { - spreadsheetIssues: IIssue[]; - mutateIssues: KeyedMutator< - | IIssue[] - | { - [key: string]: IIssue[]; - } - >; + displayProperties: IIssueDisplayProperties; + displayFilters: IIssueDisplayFilterOptions; + handleDisplayFilterUpdate: (data: Partial) => void; + issues: IIssue[] | undefined; handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void; + handleUpdateIssue: (issueId: string, data: Partial) => void; openIssuesListModal?: (() => void) | null; disableUserActions: boolean; - user: ICurrentUserResponse | undefined; - userAuth: UserAuth; }; -export const SpreadsheetView: React.FC = ({ - spreadsheetIssues, - mutateIssues, - handleIssueAction, - openIssuesListModal, - disableUserActions, - user, - userAuth, -}) => { +export const SpreadsheetView: React.FC = observer((props) => { + const { + displayProperties, + displayFilters, + handleDisplayFilterUpdate, + issues, + handleIssueAction, + handleUpdateIssue, + openIssuesListModal, + disableUserActions, + } = props; + const [expandedIssues, setExpandedIssues] = useState([]); const [currentProjectId, setCurrentProjectId] = useState(null); @@ -75,12 +58,10 @@ export const SpreadsheetView: React.FC = ({ const containerRef = useRef(null); const router = useRouter(); - const { workspaceSlug, projectId, cycleId, moduleId, viewId, globalViewId } = router.query; + const { workspaceSlug, cycleId, moduleId } = router.query; const type = cycleId ? "cycle" : moduleId ? "module" : "issue"; - const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string); - const { storedValue: selectedMenuItem, setValue: setSelectedMenuItem } = useLocalStorage( "spreadsheetViewSorting", "" @@ -90,135 +71,9 @@ export const SpreadsheetView: React.FC = ({ "" ); - const workspaceIssuesPath = [ - { - params: { - sub_issue: false, - }, - path: "workspace-views/all-issues", - }, - { - params: { - assignees: user?.id ?? undefined, - sub_issue: false, - }, - path: "workspace-views/assigned", - }, - { - params: { - created_by: user?.id ?? undefined, - sub_issue: false, - }, - path: "workspace-views/created", - }, - { - params: { - subscriber: user?.id ?? undefined, - sub_issue: false, - }, - path: "workspace-views/subscribed", - }, - ]; - - const currentWorkspaceIssuePath = workspaceIssuesPath.find((path) => router.pathname.includes(path.path)); - - const { params: workspaceViewParams, filters: workspaceViewFilters, handleFilters } = useWorkspaceView(); - - const workspaceViewProperties = workspaceViewFilters.display_properties; - - const isWorkspaceView = globalViewId || currentWorkspaceIssuePath; - - const currentViewProperties = isWorkspaceView ? workspaceViewProperties : properties; - - const params = {}; - - const partialUpdateIssue = useCallback( - (formData: Partial, issue: IIssue) => { - if (!workspaceSlug || !issue) return; - - const fetchKey = cycleId - ? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), params) - : moduleId - ? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), params) - : viewId - ? VIEW_ISSUES(viewId.toString(), params) - : globalViewId - ? WORKSPACE_VIEW_ISSUES(globalViewId.toString(), workspaceViewParams) - : currentWorkspaceIssuePath - ? WORKSPACE_VIEW_ISSUES(workspaceSlug.toString(), currentWorkspaceIssuePath?.params) - : PROJECT_ISSUES_LIST_WITH_PARAMS(issue.project_detail.id, params); - - if (issue.parent) - mutate( - SUB_ISSUES(issue.parent.toString()), - (prevData) => { - if (!prevData) return prevData; - - return { - ...prevData, - sub_issues: (prevData.sub_issues ?? []).map((i) => { - if (i.id === issue.id) { - return { - ...i, - ...formData, - }; - } - return i; - }), - }; - }, - false - ); - else - mutate( - fetchKey, - (prevData) => - (prevData ?? []).map((p) => { - if (p.id === issue.id) { - return { - ...p, - ...formData, - }; - } - return p; - }), - false - ); - - issueService - .patchIssue(workspaceSlug as string, issue.project_detail.id, issue.id as string, formData, user) - .then(() => { - if (issue.parent) { - mutate(SUB_ISSUES(issue.parent as string)); - } else { - mutate(fetchKey); - - if (cycleId) mutate(CYCLE_DETAILS(cycleId as string)); - if (moduleId) mutate(MODULE_DETAILS(moduleId as string)); - } - }) - .catch((error) => { - console.log(error); - }); - }, - [ - workspaceSlug, - cycleId, - moduleId, - viewId, - globalViewId, - workspaceViewParams, - currentWorkspaceIssuePath, - params, - user, - ] - ); - - const isNotAllowed = userAuth.isGuest || userAuth.isViewer; - const handleOrderBy = (order: TIssueOrderByOptions, itemKey: string) => { - if (globalViewId) handleFilters("display_filters", { order_by: order }); - else setDisplayFilters({ order_by: order }); + handleDisplayFilterUpdate({ order_by: order }); + setSelectedMenuItem(`${order}_${itemKey}`); setActiveSortingProperty(order === "-created_at" ? "" : itemKey); }; @@ -232,182 +87,177 @@ export const SpreadsheetView: React.FC = ({ ) => (
- {currentWorkspaceIssuePath ? ( - {header} - ) : ( - - {activeSortingProperty === propertyName && ( -
- -
- )} - - {header} -
- } - width="xl" - > - { - handleOrderBy(ascendingOrder, propertyName); - }} + -
-
- {propertyName === "assignee" || propertyName === "labels" ? ( - <> - - - - - A - - Z - - ) : propertyName === "due_date" || propertyName === "created_on" || propertyName === "updated_on" ? ( - <> - - - - - New - - Old - - ) : ( - <> - - - - - First - - Last - - )} + {activeSortingProperty === propertyName && ( +
+
+ )} - + {header} +
+ } + width="xl" + > + { + handleOrderBy(ascendingOrder, propertyName); + }} + > +
+
+ {propertyName === "assignee" || propertyName === "labels" ? ( + <> + + + + + A + + Z + + ) : propertyName === "due_date" || propertyName === "created_on" || propertyName === "updated_on" ? ( + <> + + + + + New + + Old + + ) : ( + <> + + + + + First + + Last + + )}
- - +
+
+ { + handleOrderBy(descendingOrder, propertyName); + }} + > +
{ - handleOrderBy(descendingOrder, propertyName); - }} > -
+ {propertyName === "assignee" || propertyName === "labels" ? ( + <> + + + + + Z + + A + + ) : propertyName === "due_date" ? ( + <> + + + + + Old + + New + + ) : ( + <> + + + + + Last + + First + + )} +
+ + +
+
+ {selectedMenuItem && + selectedMenuItem !== "" && + displayFilters?.order_by !== "-created_at" && + selectedMenuItem.includes(propertyName) && ( + { + handleOrderBy("-created_at", propertyName); + }} > -
- {propertyName === "assignee" || propertyName === "labels" ? ( - <> - - - - - Z - - A - - ) : propertyName === "due_date" ? ( - <> - - - - - Old - - New - - ) : ( - <> - - - - - Last - - First - - )} -
- - -
-
- {selectedMenuItem && - selectedMenuItem !== "" && - displayFilters?.order_by !== "-created_at" && - selectedMenuItem.includes(propertyName) && ( - { - handleOrderBy("-created_at", propertyName); - }} - > -
-
- - - +
+
+ + + - Clear sorting -
+ Clear sorting
- - )} - - )} +
+ + )} +
- {spreadsheetIssues.map((issue: IIssue, index) => ( + {issues?.map((issue) => ( ))}
@@ -438,7 +288,6 @@ export const SpreadsheetView: React.FC = ({ return ( <> mutateIssues()} projectId={currentProjectId ?? ""} workspaceSlug={workspaceSlug?.toString() ?? ""} readOnly={disableUserActions} @@ -446,7 +295,7 @@ export const SpreadsheetView: React.FC = ({
- {spreadsheetIssues ? ( + {issues ? ( <>
= ({ }} >
- {currentViewProperties.key && ( + {displayProperties.key && ( ID )} Issue
- {spreadsheetIssues.map((issue: IIssue, index) => ( + {issues.map((issue: IIssue, index) => ( = ({ expandedIssues={expandedIssues} setExpandedIssues={setExpandedIssues} setCurrentProjectId={setCurrentProjectId} - properties={currentViewProperties} + properties={displayProperties} handleIssueAction={handleIssueAction} disableUserActions={disableUserActions} - userAuth={userAuth} /> ))}
- {currentViewProperties.state && + {displayProperties.state && renderColumn("State", "state", SpreadsheetStateColumn, "state__name", "-state__name")} - {currentViewProperties.priority && + {displayProperties.priority && renderColumn("Priority", "priority", SpreadsheetPriorityColumn, "priority", "-priority")} - {currentViewProperties.assignee && + {displayProperties.assignee && renderColumn( "Assignees", "assignee", @@ -491,17 +339,17 @@ export const SpreadsheetView: React.FC = ({ "assignees__first_name", "-assignees__first_name" )} - {currentViewProperties.labels && + {displayProperties.labels && renderColumn("Label", "labels", SpreadsheetLabelColumn, "labels__name", "-labels__name")} - {currentViewProperties.start_date && + {displayProperties.start_date && renderColumn("Start Date", "start_date", SpreadsheetStartDateColumn, "-start_date", "start_date")} - {currentViewProperties.due_date && + {displayProperties.due_date && renderColumn("Due Date", "due_date", SpreadsheetDueDateColumn, "-target_date", "target_date")} - {currentViewProperties.estimate && + {displayProperties.estimate && renderColumn("Estimate", "estimate", SpreadsheetEstimateColumn, "estimate_point", "-estimate_point")} - {currentViewProperties.created_on && + {displayProperties.created_on && renderColumn("Created On", "created_on", SpreadsheetCreatedOnColumn, "-created_at", "created_at")} - {currentViewProperties.updated_on && + {displayProperties.updated_on && renderColumn("Updated On", "updated_on", SpreadsheetUpdatedOnColumn, "-updated_at", "updated_at")} ) : ( @@ -565,4 +413,4 @@ export const SpreadsheetView: React.FC = ({
); -}; +}); diff --git a/web/components/core/views/spreadsheet-view/state-column/spreadsheet-state-column.tsx b/web/components/core/views/spreadsheet-view/state-column/spreadsheet-state-column.tsx index 606f3e28a28..5b91aa1f4fa 100644 --- a/web/components/core/views/spreadsheet-view/state-column/spreadsheet-state-column.tsx +++ b/web/components/core/views/spreadsheet-view/state-column/spreadsheet-state-column.tsx @@ -10,7 +10,7 @@ import { ICurrentUserResponse, IIssue, Properties } from "types"; type Props = { issue: IIssue; projectId: string; - partialUpdateIssue: (formData: Partial, issue: IIssue) => void; + handleUpdateIssue: (issueId: string, data: Partial) => void; expandedIssues: string[]; properties: Properties; user: ICurrentUserResponse | undefined; @@ -20,7 +20,7 @@ type Props = { export const SpreadsheetStateColumn: React.FC = ({ issue, projectId, - partialUpdateIssue, + handleUpdateIssue, expandedIssues, properties, user, @@ -36,7 +36,7 @@ export const SpreadsheetStateColumn: React.FC = ({ issue={issue} projectId={projectId} properties={properties} - partialUpdateIssue={partialUpdateIssue} + onChange={(data) => handleUpdateIssue(issue.id, data)} user={user} isNotAllowed={isNotAllowed} /> @@ -45,12 +45,12 @@ export const SpreadsheetStateColumn: React.FC = ({ !isLoading && subIssues && subIssues.length > 0 && - subIssues.map((subIssue: IIssue) => ( + subIssues.map((subIssue) => ( , issue: IIssue) => void; + onChange: (formData: Partial) => void; properties: Properties; user: ICurrentUserResponse | undefined; isNotAllowed: boolean; }; -export const StateColumn: React.FC = ({ - issue, - projectId, - partialUpdateIssue, - properties, - user, - isNotAllowed, -}) => { +export const StateColumn: React.FC = ({ issue, projectId, onChange, properties, user, isNotAllowed }) => { const router = useRouter(); const { workspaceSlug } = router.query; @@ -34,13 +27,10 @@ export const StateColumn: React.FC = ({ const oldState = states?.find((s) => s.id === issue.state); const newState = states?.find((s) => s.id === data); - partialUpdateIssue( - { - state: data, - state_detail: newState, - }, - issue - ); + onChange({ + state: data, + state_detail: newState, + }); trackEventServices.trackIssuePartialPropertyUpdateEvent( { workspaceSlug, diff --git a/web/components/headers/global-issues.tsx b/web/components/headers/global-issues.tsx new file mode 100644 index 00000000000..962eb85b303 --- /dev/null +++ b/web/components/headers/global-issues.tsx @@ -0,0 +1,158 @@ +import { useCallback, useState } from "react"; +import Link from "next/link"; +import { useRouter } from "next/router"; +import { observer } from "mobx-react-lite"; +import useSWR from "swr"; + +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; +// components +import { DisplayFiltersSelection, FiltersDropdown, FilterSelection } from "components/issues"; +import { CreateUpdateWorkspaceViewModal } from "components/workspace"; +// ui +import { PrimaryButton, Tooltip } from "components/ui"; +// icons +import { List, PlusIcon, Sheet } from "lucide-react"; +// types +import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TStaticViewTypes } from "types"; +// constants +import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; + +const GLOBAL_VIEW_LAYOUTS = [ + { key: "list", title: "List", link: "/workspace-views", icon: List }, + { key: "spreadsheet", title: "Spreadsheet", link: "/workspace-views/all-issues", icon: Sheet }, +]; + +type Props = { + activeLayout: "list" | "spreadsheet"; +}; + +const STATIC_VIEW_TYPES: TStaticViewTypes[] = ["all-issues", "assigned", "created", "subscribed"]; + +export const GlobalIssuesHeader: React.FC = observer((props) => { + const { activeLayout } = props; + + const [createViewModal, setCreateViewModal] = useState(false); + + const router = useRouter(); + const { workspaceSlug, globalViewId } = router.query; + + const { + globalViews: globalViewsStore, + globalViewFilters: globalViewFiltersStore, + workspaceFilter: workspaceFilterStore, + workspace: workspaceStore, + project: projectStore, + } = useMobxStore(); + + const queryData = globalViewId ? globalViewsStore.globalViewDetails[globalViewId.toString()]?.query_data : undefined; + + const storedFilters = globalViewId ? globalViewFiltersStore.storedFilters[globalViewId.toString()] : undefined; + + const handleFiltersUpdate = useCallback( + (key: keyof IIssueFilterOptions, value: string | string[]) => { + if (!workspaceSlug || !globalViewId) return; + + const newValues = queryData?.filters?.[key] ?? []; + + if (Array.isArray(value)) { + value.forEach((val) => { + if (!newValues.includes(val)) newValues.push(val); + }); + } else { + if (queryData?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1); + else newValues.push(value); + } + + globalViewFiltersStore.updateStoredFilters(globalViewId.toString(), { + [key]: newValues, + }); + }, + [globalViewId, globalViewFiltersStore, queryData, workspaceSlug] + ); + + const handleDisplayFiltersUpdate = useCallback( + (updatedDisplayFilter: Partial) => { + if (!workspaceSlug) return; + + workspaceFilterStore.updateWorkspaceFilters(workspaceSlug.toString(), { + display_filters: updatedDisplayFilter, + }); + }, + [workspaceFilterStore, workspaceSlug] + ); + + const handleDisplayPropertiesUpdate = useCallback( + (property: Partial) => { + if (!workspaceSlug) return; + + workspaceFilterStore.updateWorkspaceFilters(workspaceSlug.toString(), { + display_properties: property, + }); + }, + [workspaceFilterStore, workspaceSlug] + ); + + useSWR( + workspaceSlug ? "USER_WORKSPACE_DISPLAY_FILTERS" : null, + workspaceSlug ? () => workspaceFilterStore.fetchUserWorkspaceFilters(workspaceSlug.toString()) : null + ); + + return ( + <> + setCreateViewModal(false)} /> +
+
+ {GLOBAL_VIEW_LAYOUTS.map((layout) => ( + + + +
+ +
+
+
+ + ))} +
+ {activeLayout === "spreadsheet" && ( + <> + {!STATIC_VIEW_TYPES.some((word) => router.pathname.includes(word)) && ( + + + + )} + + + + + + )} + setCreateViewModal(true)}> + + New View + +
+ + ); +}); diff --git a/web/components/headers/index.ts b/web/components/headers/index.ts index 72f1e743ea6..874d50677a6 100644 --- a/web/components/headers/index.ts +++ b/web/components/headers/index.ts @@ -1,2 +1,3 @@ +export * from "./global-issues"; export * from "./module-issues"; export * from "./project-issues"; diff --git a/web/components/headers/module-issues.tsx b/web/components/headers/module-issues.tsx index cf845e258f7..3530be98a32 100644 --- a/web/components/headers/module-issues.tsx +++ b/web/components/headers/module-issues.tsx @@ -7,7 +7,7 @@ import { useMobxStore } from "lib/mobx/store-provider"; // components import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues"; // types -import { IIssueDisplayFilterOptions, IIssueFilterOptions, TIssueLayouts } from "types"; +import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "types"; // constants import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; @@ -15,7 +15,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => { const router = useRouter(); const { workspaceSlug, projectId, moduleId } = router.query; - const { issueFilter: issueFilterStore, moduleFilter: moduleFilterStore } = useMobxStore(); + const { issueFilter: issueFilterStore, moduleFilter: moduleFilterStore, project: projectStore } = useMobxStore(); const activeLayout = issueFilterStore.userDisplayFilters.layout; @@ -67,6 +67,15 @@ export const ModuleIssuesHeader: React.FC = observer(() => { [issueFilterStore, projectId, workspaceSlug] ); + const handleDisplayPropertiesUpdate = useCallback( + (property: Partial) => { + if (!workspaceSlug || !projectId) return; + + issueFilterStore.updateDisplayProperties(workspaceSlug.toString(), projectId.toString(), property); + }, + [issueFilterStore, projectId, workspaceSlug] + ); + return (
{ filters={moduleFilterStore.moduleFilters} handleFiltersUpdate={handleFiltersUpdate} layoutDisplayFiltersOptions={activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined} - projectId={projectId?.toString() ?? ""} + labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? undefined} + members={projectStore.members?.[projectId?.toString() ?? ""]?.map((m) => m.member)} + states={projectStore.states?.[projectId?.toString() ?? ""] ?? undefined} /> diff --git a/web/components/headers/project-issues.tsx b/web/components/headers/project-issues.tsx index e5afd6f5cfc..8c351d1e2b9 100644 --- a/web/components/headers/project-issues.tsx +++ b/web/components/headers/project-issues.tsx @@ -7,7 +7,7 @@ import { useMobxStore } from "lib/mobx/store-provider"; // components import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues"; // types -import { IIssueDisplayFilterOptions, IIssueFilterOptions, TIssueLayouts } from "types"; +import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "types"; // constants import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; @@ -15,7 +15,7 @@ export const ProjectIssuesHeader: React.FC = observer(() => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; - const { issueFilter: issueFilterStore } = useMobxStore(); + const { issueFilter: issueFilterStore, project: projectStore } = useMobxStore(); const activeLayout = issueFilterStore.userDisplayFilters.layout; @@ -69,6 +69,15 @@ export const ProjectIssuesHeader: React.FC = observer(() => { [issueFilterStore, projectId, workspaceSlug] ); + const handleDisplayPropertiesUpdate = useCallback( + (property: Partial) => { + if (!workspaceSlug || !projectId) return; + + issueFilterStore.updateDisplayProperties(workspaceSlug.toString(), projectId.toString(), property); + }, + [issueFilterStore, projectId, workspaceSlug] + ); + return (
{ filters={issueFilterStore.userFilters} handleFiltersUpdate={handleFiltersUpdate} layoutDisplayFiltersOptions={activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined} - projectId={projectId?.toString() ?? ""} + labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? undefined} + members={projectStore.members?.[projectId?.toString() ?? ""]?.map((m) => m.member)} + states={projectStore.states?.[projectId?.toString() ?? ""] ?? undefined} /> diff --git a/web/components/issues/issue-layouts/calendar/calendar.tsx b/web/components/issues/issue-layouts/calendar/calendar.tsx index 507732e983f..df359e97cf1 100644 --- a/web/components/issues/issue-layouts/calendar/calendar.tsx +++ b/web/components/issues/issue-layouts/calendar/calendar.tsx @@ -12,14 +12,14 @@ import { IIssueGroupedStructure } from "store/issue"; type Props = { issues: IIssueGroupedStructure | null; + layout: "month" | "week" | undefined; }; export const CalendarChart: React.FC = observer((props) => { - const { issues } = props; + const { issues, layout } = props; - const { calendar: calendarStore, issueFilter: issueFilterStore } = useMobxStore(); + const { calendar: calendarStore } = useMobxStore(); - const calendarLayout = issueFilterStore.userDisplayFilters.calendar?.layout ?? "month"; const calendarPayload = calendarStore.calendarPayload; const allWeeksOfActiveMonth = calendarStore.allWeeksOfActiveMonth; @@ -37,7 +37,7 @@ export const CalendarChart: React.FC = observer((props) => {
- {calendarLayout === "month" ? ( + {layout === "month" ? (
{allWeeksOfActiveMonth && Object.values(allWeeksOfActiveMonth).map((week: ICalendarWeek, weekIndex) => ( diff --git a/web/components/issues/issue-layouts/calendar/module-root.tsx b/web/components/issues/issue-layouts/calendar/module-root.tsx index 7fc57f8671c..02c34820bc4 100644 --- a/web/components/issues/issue-layouts/calendar/module-root.tsx +++ b/web/components/issues/issue-layouts/calendar/module-root.tsx @@ -9,7 +9,7 @@ import { CalendarChart } from "components/issues"; import { IIssueGroupedStructure } from "store/issue"; export const ModuleCalendarLayout: React.FC = observer(() => { - const { module: moduleStore } = useMobxStore(); + const { module: moduleStore, issueFilter: issueFilterStore } = useMobxStore(); // TODO: add drag and drop functionality const onDragEnd = (result: DropResult) => { @@ -29,7 +29,10 @@ export const ModuleCalendarLayout: React.FC = observer(() => { return (
- +
); diff --git a/web/components/issues/issue-layouts/calendar/root.tsx b/web/components/issues/issue-layouts/calendar/root.tsx index 537836f8915..1482c426250 100644 --- a/web/components/issues/issue-layouts/calendar/root.tsx +++ b/web/components/issues/issue-layouts/calendar/root.tsx @@ -9,7 +9,7 @@ import { CalendarChart } from "components/issues"; import { IIssueGroupedStructure } from "store/issue"; export const CalendarLayout: React.FC = observer(() => { - const { issue: issueStore } = useMobxStore(); + const { issue: issueStore, issueFilter: issueFilterStore } = useMobxStore(); // TODO: add drag and drop functionality const onDragEnd = (result: DropResult) => { @@ -29,7 +29,10 @@ export const CalendarLayout: React.FC = observer(() => { return (
- +
); diff --git a/web/components/issues/issue-layouts/filters/applied-filters/filters-list.tsx b/web/components/issues/issue-layouts/filters/applied-filters/filters-list.tsx index 7ae70ef6472..f379f675aa4 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/filters-list.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/filters-list.tsx @@ -6,6 +6,7 @@ import { AppliedLabelsFilters, AppliedMembersFilters, AppliedPriorityFilters, + AppliedProjectFilters, AppliedStateFilters, AppliedStateGroupFilters, } from "components/issues"; @@ -14,39 +15,49 @@ import { X } from "lucide-react"; // helpers import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper"; // types -import { IIssueFilterOptions, IIssueLabels, IStateResponse, IUserLite } from "types"; +import { IIssueFilterOptions, IIssueLabels, IProject, IStateResponse, IUserLite } from "types"; type Props = { appliedFilters: IIssueFilterOptions; handleClearAllFilters: () => void; handleRemoveFilter: (key: keyof IIssueFilterOptions, value: string | null) => void; - labels: IIssueLabels[] | undefined; - members: IUserLite[] | undefined; - states: IStateResponse | undefined; + labels?: IIssueLabels[] | undefined; + members?: IUserLite[] | undefined; + projects?: IProject[] | undefined; + states?: IStateResponse | undefined; }; +const membersFilters = ["assignees", "created_by", "subscriber"]; +const dateFilters = ["start_date", "target_date"]; + export const AppliedFiltersList: React.FC = observer((props) => { - const { appliedFilters, handleClearAllFilters, handleRemoveFilter, labels, members, states } = props; + const { appliedFilters, handleClearAllFilters, handleRemoveFilter, labels, members, projects, states } = props; + + if (!appliedFilters) return null; + + if (Object.keys(appliedFilters).length === 0) return null; return ( -
+
{Object.entries(appliedFilters).map(([key, value]) => { const filterKey = key as keyof IIssueFilterOptions; + if (!value) return; + return (
{replaceUnderscoreIfSnakeCase(filterKey)} - {(filterKey === "assignees" || filterKey === "created_by" || filterKey === "subscriber") && ( + {membersFilters.includes(filterKey) && ( handleRemoveFilter(filterKey, val)} members={members} values={value} /> )} - {(filterKey === "start_date" || filterKey === "target_date") && ( + {dateFilters.includes(filterKey) && ( handleRemoveFilter(filterKey, val)} values={value} /> )} {filterKey === "labels" && ( @@ -69,6 +80,13 @@ export const AppliedFiltersList: React.FC = observer((props) => { {filterKey === "state_group" && ( handleRemoveFilter("state_group", val)} values={value} /> )} + {filterKey === "project" && ( + handleRemoveFilter("project", val)} + projects={projects} + values={value} + /> + )} +
+ ); + })} +
+ ); +}); diff --git a/web/components/issues/issue-layouts/filters/applied-filters/root.tsx b/web/components/issues/issue-layouts/filters/applied-filters/root.tsx index c565fcaa1ef..fa1d9e9b5cf 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/root.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/root.tsx @@ -12,7 +12,7 @@ export const AppliedFiltersRoot: React.FC = observer(() => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; - const { issueFilter: issueFilterStore, project: projectStore, moduleFilter: moduleFilterStore } = useMobxStore(); + const { issueFilter: issueFilterStore, project: projectStore } = useMobxStore(); const userFilters = issueFilterStore.userFilters; diff --git a/web/components/issues/issue-layouts/filters/header/display-filters/display-filters-selection.tsx b/web/components/issues/issue-layouts/filters/header/display-filters/display-filters-selection.tsx index cc2cebf388d..3a18e7cc4c9 100644 --- a/web/components/issues/issue-layouts/filters/header/display-filters/display-filters-selection.tsx +++ b/web/components/issues/issue-layouts/filters/header/display-filters/display-filters-selection.tsx @@ -11,17 +11,25 @@ import { FilterSubGroupBy, } from "components/issues"; // types -import { IIssueDisplayFilterOptions } from "types"; +import { IIssueDisplayFilterOptions, IIssueDisplayProperties } from "types"; import { ILayoutDisplayFiltersOptions } from "constants/issue"; type Props = { displayFilters: IIssueDisplayFilterOptions; + displayProperties: IIssueDisplayProperties; handleDisplayFiltersUpdate: (updatedDisplayFilter: Partial) => void; + handleDisplayPropertiesUpdate: (updatedDisplayProperties: Partial) => void; layoutDisplayFiltersOptions: ILayoutDisplayFiltersOptions | undefined; }; export const DisplayFiltersSelection: React.FC = observer((props) => { - const { displayFilters, handleDisplayFiltersUpdate, layoutDisplayFiltersOptions } = props; + const { + displayFilters, + displayProperties, + handleDisplayFiltersUpdate, + handleDisplayPropertiesUpdate, + layoutDisplayFiltersOptions, + } = props; const isDisplayFilterEnabled = (displayFilter: keyof IIssueDisplayFilterOptions) => Object.keys(layoutDisplayFiltersOptions?.display_filters ?? {}).includes(displayFilter); @@ -31,7 +39,7 @@ export const DisplayFiltersSelection: React.FC = observer((props) => { {/* display properties */} {layoutDisplayFiltersOptions?.display_properties && (
- +
)} @@ -52,20 +60,22 @@ export const DisplayFiltersSelection: React.FC = observer((props) => { )} {/* sub-group by */} - {isDisplayFilterEnabled("sub_group_by") && displayFilters.group_by !== null && ( -
- - handleDisplayFiltersUpdate({ - sub_group_by: val, - }) - } - subGroupByOptions={layoutDisplayFiltersOptions?.display_filters.sub_group_by ?? []} - /> -
- )} + {isDisplayFilterEnabled("sub_group_by") && + displayFilters.group_by !== null && + displayFilters.layout === "kanban" && ( +
+ + handleDisplayFiltersUpdate({ + sub_group_by: val, + }) + } + subGroupByOptions={layoutDisplayFiltersOptions?.display_filters.sub_group_by ?? []} + /> +
+ )} {/* order by */} {isDisplayFilterEnabled("order_by") && ( diff --git a/web/components/issues/issue-layouts/filters/header/display-filters/display-properties.tsx b/web/components/issues/issue-layouts/filters/header/display-filters/display-properties.tsx index 3b3c92df923..c5d4a724d43 100644 --- a/web/components/issues/issue-layouts/filters/header/display-filters/display-properties.tsx +++ b/web/components/issues/issue-layouts/filters/header/display-filters/display-properties.tsx @@ -1,10 +1,6 @@ import React from "react"; - -import { useRouter } from "next/router"; - -// mobx import { observer } from "mobx-react-lite"; -import { useMobxStore } from "lib/mobx/store-provider"; + // components import { FilterHeader } from "../helpers/filter-header"; // types @@ -12,21 +8,16 @@ import { IIssueDisplayProperties } from "types"; // constants import { ISSUE_DISPLAY_PROPERTIES } from "constants/issue"; -export const FilterDisplayProperties = observer(() => { - const router = useRouter(); - const { workspaceSlug, projectId } = router.query; +type Props = { + displayProperties: IIssueDisplayProperties; + handleUpdate: (updatedDisplayProperties: Partial) => void; +}; - const store = useMobxStore(); - const { issueFilter: issueFilterStore } = store; +export const FilterDisplayProperties: React.FC = observer((props) => { + const { displayProperties, handleUpdate } = props; const [previewEnabled, setPreviewEnabled] = React.useState(true); - const handleDisplayProperties = (property: Partial) => { - if (!workspaceSlug || !projectId) return; - - issueFilterStore.updateDisplayProperties(workspaceSlug.toString(), projectId.toString(), property); - }; - return ( <> { key={displayProperty.key} type="button" className={`rounded transition-all text-xs border px-2 py-0.5 ${ - issueFilterStore?.userDisplayProperties?.[displayProperty.key] + displayProperties?.[displayProperty.key] ? "bg-custom-primary-100 border-custom-primary-100 text-white" : "border-custom-border-200 hover:bg-custom-background-80" }`} onClick={() => - handleDisplayProperties({ - [displayProperty.key]: !issueFilterStore?.userDisplayProperties?.[displayProperty.key], + handleUpdate({ + [displayProperty.key]: !displayProperties?.[displayProperty.key], }) } > diff --git a/web/components/issues/issue-layouts/filters/header/filters/filters-selection.tsx b/web/components/issues/issue-layouts/filters/header/filters/filters-selection.tsx index 50f98a83428..51857217e46 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/filters-selection.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/filters-selection.tsx @@ -1,14 +1,13 @@ import React, { useState } from "react"; import { observer } from "mobx-react-lite"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; // components import { FilterAssignees, FilterCreatedBy, FilterLabels, FilterPriority, + FilterProjects, FilterStartDate, FilterState, FilterStateGroup, @@ -19,7 +18,7 @@ import { Search, X } from "lucide-react"; // helpers import { getStatesList } from "helpers/state.helper"; // types -import { IIssueFilterOptions } from "types"; +import { IIssueFilterOptions, IIssueLabels, IProject, IStateResponse, IUserLite } from "types"; // constants import { ILayoutDisplayFiltersOptions, ISSUE_PRIORITIES, ISSUE_STATE_GROUPS } from "constants/issue"; import { DATE_FILTER_OPTIONS } from "constants/filters"; @@ -28,7 +27,10 @@ type Props = { filters: IIssueFilterOptions; handleFiltersUpdate: (key: keyof IIssueFilterOptions, value: string | string[]) => void; layoutDisplayFiltersOptions: ILayoutDisplayFiltersOptions | undefined; - projectId: string; + labels?: IIssueLabels[] | undefined; + members?: IUserLite[] | undefined; + projects?: IProject[] | undefined; + states?: IStateResponse | undefined; }; type ViewButtonProps = { @@ -55,13 +57,11 @@ const ViewButtons = ({ handleLess, handleMore, isViewLessVisible, isViewMoreVisi ); export const FilterSelection: React.FC = observer((props) => { - const { filters, handleFiltersUpdate, layoutDisplayFiltersOptions, projectId } = props; + const { filters, handleFiltersUpdate, layoutDisplayFiltersOptions, labels, members, projects, states } = props; const [filtersSearchQuery, setFiltersSearchQuery] = useState(""); - const { project: projectStore } = useMobxStore(); - - const statesList = getStatesList(projectStore.states?.[projectId?.toString() ?? ""]); + const statesList = getStatesList(states); const [filtersToRender, setFiltersToRender] = useState<{ [key in keyof IIssueFilterOptions]: { @@ -71,20 +71,24 @@ export const FilterSelection: React.FC = observer((props) => { }>({ assignees: { currentLength: 5, - totalLength: projectStore.members?.[projectId]?.length ?? 0, + totalLength: members?.length ?? 0, }, created_by: { currentLength: 5, - totalLength: projectStore.members?.[projectId]?.length ?? 0, + totalLength: members?.length ?? 0, }, labels: { currentLength: 5, - totalLength: projectStore.labels?.[projectId]?.length ?? 0, + totalLength: labels?.length ?? 0, }, priority: { currentLength: 5, totalLength: ISSUE_PRIORITIES.length, }, + project: { + currentLength: 5, + totalLength: projects?.length ?? 0, + }, state_group: { currentLength: 5, totalLength: ISSUE_STATE_GROUPS.length, @@ -219,7 +223,7 @@ export const FilterSelection: React.FC = observer((props) => { handleUpdate={(val) => handleFiltersUpdate("state", val)} itemsToRender={filtersToRender.state?.currentLength ?? 0} searchQuery={filtersSearchQuery} - states={projectStore.states?.[projectId]} + states={states} viewButtons={ = observer((props) => { appliedFilters={filters.assignees ?? null} handleUpdate={(val) => handleFiltersUpdate("assignees", val)} itemsToRender={filtersToRender.assignees?.currentLength ?? 0} - members={projectStore.members?.[projectId]?.map((m) => m.member) ?? undefined} + members={members} searchQuery={filtersSearchQuery} viewButtons={ = observer((props) => { appliedFilters={filters.created_by ?? null} handleUpdate={(val) => handleFiltersUpdate("created_by", val)} itemsToRender={filtersToRender.created_by?.currentLength ?? 0} - members={projectStore.members?.[projectId]?.map((m) => m.member) ?? undefined} + members={members} searchQuery={filtersSearchQuery} viewButtons={ = observer((props) => { appliedFilters={filters.labels ?? null} handleUpdate={(val) => handleFiltersUpdate("labels", val)} itemsToRender={filtersToRender.labels?.currentLength ?? 0} - labels={projectStore.labels?.[projectId] ?? undefined} + labels={labels} searchQuery={filtersSearchQuery} viewButtons={ = observer((props) => {
)} + {/* project */} + {isFilterEnabled("project") && ( +
+ handleFiltersUpdate("project", val)} + itemsToRender={filtersToRender.project?.currentLength ?? 0} + searchQuery={filtersSearchQuery} + viewButtons={ + handleViewLess("project")} + handleMore={() => handleViewMore("project")} + /> + } + /> +
+ )} + {/* start_date */} {isFilterEnabled("start_date") && (
diff --git a/web/components/issues/issue-layouts/filters/header/filters/index.ts b/web/components/issues/issue-layouts/filters/header/filters/index.ts index 847b3087445..5c381709e83 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/index.ts +++ b/web/components/issues/issue-layouts/filters/header/filters/index.ts @@ -3,6 +3,7 @@ export * from "./created-by"; export * from "./filters-selection"; export * from "./labels"; export * from "./priority"; +export * from "./project"; export * from "./start-date"; export * from "./state-group"; export * from "./state"; diff --git a/web/components/issues/issue-layouts/filters/header/filters/project.tsx b/web/components/issues/issue-layouts/filters/header/filters/project.tsx new file mode 100644 index 00000000000..93163dd9142 --- /dev/null +++ b/web/components/issues/issue-layouts/filters/header/filters/project.tsx @@ -0,0 +1,81 @@ +import React, { useState } from "react"; + +// components +import { FilterHeader, FilterOption } from "components/issues"; +// ui +import { Loader } from "components/ui"; +// helpers +import { renderEmoji } from "helpers/emoji.helper"; +// types +import { IProject } from "types"; + +type Props = { + appliedFilters: string[] | null; + handleUpdate: (val: string) => void; + itemsToRender: number; + projects: IProject[] | undefined; + searchQuery: string; + viewButtons: React.ReactNode; +}; + +export const FilterProjects: React.FC = (props) => { + const { appliedFilters, handleUpdate, itemsToRender, projects, searchQuery, viewButtons } = props; + + const [previewEnabled, setPreviewEnabled] = useState(true); + + const appliedFiltersCount = appliedFilters?.length ?? 0; + + const filteredOptions = projects?.filter((project) => project.name.toLowerCase().includes(searchQuery.toLowerCase())); + + return ( + <> + 0 ? ` (${appliedFiltersCount})` : ""}`} + isPreviewEnabled={previewEnabled} + handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)} + /> + {previewEnabled && ( +
+ {filteredOptions ? ( + filteredOptions.length > 0 ? ( + <> + {filteredOptions.slice(0, itemsToRender).map((project) => ( + handleUpdate(project.id)} + icon={ + project.emoji ? ( + + {renderEmoji(project.emoji)} + + ) : project.icon_prop ? ( +
+ {renderEmoji(project.icon_prop)} +
+ ) : ( + + {project?.name.charAt(0)} + + ) + } + title={project.name} + /> + ))} + {viewButtons} + + ) : ( +

No matches found

+ ) + ) : ( + + + + + + )} +
+ )} + + ); +}; diff --git a/web/components/issues/issue-layouts/filters/header/helpers/dropdown.tsx b/web/components/issues/issue-layouts/filters/header/helpers/dropdown.tsx index 00c589a7665..62bf2f6850c 100644 --- a/web/components/issues/issue-layouts/filters/header/helpers/dropdown.tsx +++ b/web/components/issues/issue-layouts/filters/header/helpers/dropdown.tsx @@ -1,7 +1,7 @@ -import React, { Fragment } from "react"; - -// headless ui +import React, { Fragment, useState } from "react"; +import { usePopper } from "react-popper"; import { Popover, Transition } from "@headlessui/react"; + // icons import { ChevronUp } from "lucide-react"; @@ -13,24 +13,35 @@ type Props = { export const FiltersDropdown: React.FC = (props) => { const { children, title = "Dropdown" } = props; + const [referenceElement, setReferenceElement] = useState(null); + const [popperElement, setPopperElement] = useState(null); + + const { styles, attributes } = usePopper(referenceElement, popperElement, { + placement: "auto", + }); + return ( - + {({ open }) => { if (open) { } return ( <> - -
{title}
-
+
+
{title}
+
+ +
+
= (props) => { leaveFrom="opacity-100 translate-y-0" leaveTo="opacity-0 translate-y-1" > - -
{children}
+ +
+
{children}
+
diff --git a/web/components/issues/issue-layouts/filters/header/helpers/filter-option.tsx b/web/components/issues/issue-layouts/filters/header/helpers/filter-option.tsx index af1653f8b56..4b6f1b0417c 100644 --- a/web/components/issues/issue-layouts/filters/header/helpers/filter-option.tsx +++ b/web/components/issues/issue-layouts/filters/header/helpers/filter-option.tsx @@ -27,7 +27,7 @@ export const FilterOption: React.FC = (props) => { {isChecked && }
-
{icon}
+ {icon &&
{icon}
}
{title}
diff --git a/web/components/issues/issue-layouts/global-views-all-layouts.tsx b/web/components/issues/issue-layouts/global-views-all-layouts.tsx new file mode 100644 index 00000000000..59f0a030b7b --- /dev/null +++ b/web/components/issues/issue-layouts/global-views-all-layouts.tsx @@ -0,0 +1,88 @@ +import React, { useCallback } from "react"; +import { useRouter } from "next/router"; +import { observer } from "mobx-react-lite"; +import useSWR from "swr"; + +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; +// components +import { SpreadsheetView } from "components/core"; +import { GlobalViewsAppliedFiltersRoot } from "components/issues"; +// types +import { IIssueDisplayFilterOptions, TStaticViewTypes } from "types"; +// fetch-keys +import { GLOBAL_VIEW_ISSUES } from "constants/fetch-keys"; + +type Props = { + type?: TStaticViewTypes; +}; + +export const GlobalViewsAllLayouts: React.FC = observer((props) => { + const { type } = props; + + const router = useRouter(); + const { workspaceSlug, globalViewId } = router.query; + + const { + globalViews: globalViewsStore, + globalViewIssues: globalViewIssuesStore, + globalViewFilters: globalViewFiltersStore, + workspaceFilter: workspaceFilterStore, + } = useMobxStore(); + + const viewDetails = globalViewId ? globalViewsStore.globalViewDetails[globalViewId.toString()] : undefined; + + const storedFilters = globalViewId ? globalViewFiltersStore.storedFilters[globalViewId.toString()] : undefined; + + useSWR( + workspaceSlug && globalViewId && viewDetails ? GLOBAL_VIEW_ISSUES(globalViewId.toString()) : null, + workspaceSlug && globalViewId && viewDetails + ? () => { + globalViewIssuesStore.fetchViewIssues(workspaceSlug.toString(), globalViewId.toString(), storedFilters ?? {}); + } + : null + ); + + useSWR( + workspaceSlug && type ? GLOBAL_VIEW_ISSUES(type) : null, + workspaceSlug && type + ? () => { + globalViewIssuesStore.fetchStaticIssues(workspaceSlug.toString(), type); + } + : null + ); + + const handleDisplayFiltersUpdate = useCallback( + (updatedDisplayFilter: Partial) => { + if (!workspaceSlug) return; + + workspaceFilterStore.updateWorkspaceFilters(workspaceSlug.toString(), { + display_filters: updatedDisplayFilter, + }); + }, + [workspaceFilterStore, workspaceSlug] + ); + + const issues = type + ? globalViewIssuesStore.viewIssues?.[type] + : globalViewId + ? globalViewIssuesStore.viewIssues?.[globalViewId.toString()] + : undefined; + + return ( +
+ +
+ {}} + handleUpdateIssue={() => {}} + disableUserActions={false} + /> +
+
+ ); +}); diff --git a/web/components/issues/issue-layouts/index.ts b/web/components/issues/issue-layouts/index.ts index e8439b236e0..9307a19f79f 100644 --- a/web/components/issues/issue-layouts/index.ts +++ b/web/components/issues/issue-layouts/index.ts @@ -7,6 +7,7 @@ export * from "./calendar"; export * from "./gantt"; export * from "./kanban"; export * from "./spreadsheet"; +export * from "./global-views-all-layouts"; // cycle root layout export * from "./cycle-layout-root"; diff --git a/web/components/issues/modal.tsx b/web/components/issues/modal.tsx index e57b6f22364..0247bc027f8 100644 --- a/web/components/issues/modal.tsx +++ b/web/components/issues/modal.tsx @@ -18,7 +18,6 @@ import useInboxView from "hooks/use-inbox-view"; import useProjects from "hooks/use-projects"; import useMyIssues from "hooks/my-issues/use-my-issues"; import useLocalStorage from "hooks/use-local-storage"; -import { useWorkspaceView } from "hooks/use-workspace-view"; // components import { IssueForm, ConfirmIssueDiscard } from "components/issues"; // types @@ -36,7 +35,7 @@ import { VIEW_ISSUES, INBOX_ISSUES, PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS, - WORKSPACE_VIEW_ISSUES, + GLOBAL_VIEW_ISSUES, } from "constants/fetch-keys"; // constants import { INBOX_ISSUE_SOURCE } from "constants/inbox"; @@ -92,7 +91,7 @@ export const CreateUpdateIssueModal: React.FC = ({ const { groupedIssues, mutateMyIssues } = useMyIssues(workspaceSlug?.toString()); - const { params: globalViewParams } = useWorkspaceView(); + const globalViewParams = {}; const { setValue: setValueInLocalStorage, clearValue: clearLocalStorageValue } = useLocalStorage( "draftedIssue", @@ -342,10 +341,10 @@ export const CreateUpdateIssueModal: React.FC = ({ if (payload.parent && payload.parent !== "") mutate(SUB_ISSUES(payload.parent)); - if (globalViewId) mutate(WORKSPACE_VIEW_ISSUES(globalViewId.toString(), globalViewParams)); + if (globalViewId) mutate(GLOBAL_VIEW_ISSUES(globalViewId.toString(), globalViewParams)); if (currentWorkspaceIssuePath) - mutate(WORKSPACE_VIEW_ISSUES(workspaceSlug.toString(), currentWorkspaceIssuePath?.params)); + mutate(GLOBAL_VIEW_ISSUES(workspaceSlug.toString(), currentWorkspaceIssuePath?.params)); }) .catch(() => { setToastAlert({ diff --git a/web/components/issues/workspace-views/workpace-view-issues.tsx b/web/components/issues/workspace-views/workpace-view-issues.tsx deleted file mode 100644 index 78a12f80706..00000000000 --- a/web/components/issues/workspace-views/workpace-view-issues.tsx +++ /dev/null @@ -1,232 +0,0 @@ -import React, { useCallback, useState } from "react"; - -import useSWR from "swr"; - -import { useRouter } from "next/router"; - -// context -import { useProjectMyMembership } from "contexts/project-member.context"; -// service -import projectIssuesServices from "services/issues.service"; -// hooks -import useProjects from "hooks/use-projects"; -import useUser from "hooks/use-user"; -import { useWorkspaceView } from "hooks/use-workspace-view"; -import useWorkspaceMembers from "hooks/use-workspace-members"; -import useToast from "hooks/use-toast"; -// components -import { WorkspaceViewsNavigation } from "components/workspace/views/workpace-view-navigation"; -import { EmptyState, PrimaryButton } from "components/ui"; -import { SpreadsheetView, WorkspaceFiltersList } from "components/core"; -import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; -import { CreateUpdateWorkspaceViewModal } from "components/workspace/views/modal"; -// icon -import { PlusIcon } from "components/icons"; -// image -import emptyView from "public/empty-state/view.svg"; -// constants -import { WORKSPACE_LABELS } from "constants/fetch-keys"; -import { STATE_GROUP } from "constants/project"; -// types -import { IIssue, IWorkspaceIssueFilterOptions } from "types"; - -export const WorkspaceViewIssues = () => { - const router = useRouter(); - const { workspaceSlug, globalViewId } = router.query; - - const { setToastAlert } = useToast(); - - const { memberRole } = useProjectMyMembership(); - const { user } = useUser(); - const { isGuest, isViewer } = useWorkspaceMembers( - workspaceSlug?.toString(), - Boolean(workspaceSlug) - ); - const { filters, viewIssues, mutateViewIssues, handleFilters } = useWorkspaceView(); - - const [createViewModal, setCreateViewModal] = useState(null); - - // create issue modal - const [createIssueModal, setCreateIssueModal] = useState(false); - const [preloadedData, setPreloadedData] = useState< - (Partial & { actionType: "createIssue" | "edit" | "delete" }) | undefined - >(undefined); - - // update issue modal - const [editIssueModal, setEditIssueModal] = useState(false); - const [issueToEdit, setIssueToEdit] = useState< - (IIssue & { actionType: "edit" | "delete" }) | undefined - >(undefined); - - // delete issue modal - const [deleteIssueModal, setDeleteIssueModal] = useState(false); - const [issueToDelete, setIssueToDelete] = useState(null); - - const { projects: allProjects } = useProjects(); - const joinedProjects = allProjects?.filter((p) => p.is_member); - - const { data: workspaceLabels } = useSWR( - workspaceSlug ? WORKSPACE_LABELS(workspaceSlug.toString()) : null, - workspaceSlug ? () => projectIssuesServices.getWorkspaceLabels(workspaceSlug.toString()) : null - ); - - const { workspaceMembers } = useWorkspaceMembers(workspaceSlug?.toString() ?? ""); - - const makeIssueCopy = useCallback( - (issue: IIssue) => { - setCreateIssueModal(true); - - setPreloadedData({ ...issue, name: `${issue.name} (Copy)`, actionType: "createIssue" }); - }, - [setCreateIssueModal, setPreloadedData] - ); - - const handleEditIssue = useCallback( - (issue: IIssue) => { - setEditIssueModal(true); - setIssueToEdit({ - ...issue, - actionType: "edit", - cycle: issue.issue_cycle ? issue.issue_cycle.cycle : null, - module: issue.issue_module ? issue.issue_module.module : null, - }); - }, - [setEditIssueModal, setIssueToEdit] - ); - - const handleDeleteIssue = useCallback( - (issue: IIssue) => { - setDeleteIssueModal(true); - setIssueToDelete(issue); - }, - [setDeleteIssueModal, setIssueToDelete] - ); - - const handleIssueAction = useCallback( - (issue: IIssue, action: "copy" | "edit" | "delete" | "updateDraft") => { - if (action === "copy") makeIssueCopy(issue); - else if (action === "edit") handleEditIssue(issue); - else if (action === "delete") handleDeleteIssue(issue); - }, - [makeIssueCopy, handleEditIssue, handleDeleteIssue] - ); - - const nullFilters = - filters.filters && - Object.keys(filters.filters).filter( - (key) => - filters.filters[key as keyof IWorkspaceIssueFilterOptions] === null || - (filters.filters[key as keyof IWorkspaceIssueFilterOptions]?.length ?? 0) <= 0 - ); - - const areFiltersApplied = - filters.filters && - Object.keys(filters.filters).length > 0 && - nullFilters.length !== Object.keys(filters.filters).length; - - const isNotAllowed = isGuest || isViewer; - return ( - <> - setCreateIssueModal(false)} - prePopulateData={{ - ...preloadedData, - }} - onSubmit={async () => mutateViewIssues()} - /> - setEditIssueModal(false)} - data={issueToEdit} - onSubmit={async () => mutateViewIssues()} - /> - setDeleteIssueModal(false)} - isOpen={deleteIssueModal} - data={issueToDelete} - user={user} - onSubmit={async () => mutateViewIssues()} - /> - setCreateViewModal(null)} - preLoadedData={createViewModal} - /> -
-
- setCreateViewModal(true)} /> - {false ? ( - router.push(`/${workspaceSlug}/workspace-views`), - }} - /> - ) : ( -
- {areFiltersApplied && ( - <> -
- handleFilters("filters", updatedFilter)} - labels={workspaceLabels} - members={workspaceMembers?.map((m) => m.member)} - stateGroup={STATE_GROUP} - project={joinedProjects} - clearAllFilters={() => - handleFilters("filters", { - assignees: null, - created_by: null, - labels: null, - priority: null, - state_group: null, - start_date: null, - target_date: null, - subscriber: null, - project: null, - }) - } - /> - { - if (globalViewId) { - handleFilters("filters", filters.filters, true); - setToastAlert({ - title: "View updated", - message: "Your view has been updated", - type: "success", - }); - } else - setCreateViewModal({ - query: filters.filters, - }); - }} - className="flex items-center gap-2 text-sm" - > - {!globalViewId && } - {globalViewId ? "Update" : "Save"} view - -
- {
} - - )} - -
- )} -
-
- - ); -}; diff --git a/web/components/issues/workspace-views/workspace-all-issue.tsx b/web/components/issues/workspace-views/workspace-all-issue.tsx deleted file mode 100644 index 4618c331d1d..00000000000 --- a/web/components/issues/workspace-views/workspace-all-issue.tsx +++ /dev/null @@ -1,236 +0,0 @@ -import { useCallback, useState } from "react"; -import { useRouter } from "next/router"; - -import useSWR from "swr"; - -// hook -import useUser from "hooks/use-user"; -import useWorkspaceMembers from "hooks/use-workspace-members"; -import useProjects from "hooks/use-projects"; -import { useWorkspaceView } from "hooks/use-workspace-view"; -// context -import { useProjectMyMembership } from "contexts/project-member.context"; -// services -import workspaceService from "services/workspace.service"; -import projectIssuesServices from "services/issues.service"; -// components -import { SpreadsheetView, WorkspaceFiltersList } from "components/core"; -import { WorkspaceViewsNavigation } from "components/workspace/views/workpace-view-navigation"; -import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; -import { CreateUpdateWorkspaceViewModal } from "components/workspace/views/modal"; -// ui -import { PrimaryButton } from "components/ui"; -// icons -import { PlusIcon } from "@heroicons/react/24/outline"; -// fetch-keys -import { WORKSPACE_LABELS, WORKSPACE_VIEW_ISSUES } from "constants/fetch-keys"; -// constants -import { STATE_GROUP } from "constants/project"; -// types -import { IIssue, IWorkspaceIssueFilterOptions } from "types"; - -export const WorkspaceAllIssue = () => { - const router = useRouter(); - const { workspaceSlug, globalViewId } = router.query; - - const [createViewModal, setCreateViewModal] = useState(null); - - // create issue modal - const [createIssueModal, setCreateIssueModal] = useState(false); - const [preloadedData, setPreloadedData] = useState< - (Partial & { actionType: "createIssue" | "edit" | "delete" }) | undefined - >(undefined); - - // update issue modal - const [editIssueModal, setEditIssueModal] = useState(false); - const [issueToEdit, setIssueToEdit] = useState< - (IIssue & { actionType: "edit" | "delete" }) | undefined - >(undefined); - - // delete issue modal - const [deleteIssueModal, setDeleteIssueModal] = useState(false); - const [issueToDelete, setIssueToDelete] = useState(null); - - const { user } = useUser(); - const { memberRole } = useProjectMyMembership(); - - const { workspaceMembers } = useWorkspaceMembers(workspaceSlug?.toString() ?? ""); - - const { data: workspaceLabels } = useSWR( - workspaceSlug ? WORKSPACE_LABELS(workspaceSlug.toString()) : null, - workspaceSlug ? () => projectIssuesServices.getWorkspaceLabels(workspaceSlug.toString()) : null - ); - - const { filters, handleFilters } = useWorkspaceView(); - - const params: any = { - assignees: filters?.filters?.assignees ? filters?.filters?.assignees.join(",") : undefined, - subscriber: filters?.filters?.subscriber ? filters?.filters?.subscriber.join(",") : undefined, - state_group: filters?.filters?.state_group - ? filters?.filters?.state_group.join(",") - : undefined, - priority: filters?.filters?.priority ? filters?.filters?.priority.join(",") : undefined, - labels: filters?.filters?.labels ? filters?.filters?.labels.join(",") : undefined, - created_by: filters?.filters?.created_by ? filters?.filters?.created_by.join(",") : undefined, - start_date: filters?.filters?.start_date ? filters?.filters?.start_date.join(",") : undefined, - target_date: filters?.filters?.target_date - ? filters?.filters?.target_date.join(",") - : undefined, - project: filters?.filters?.project ? filters?.filters?.project.join(",") : undefined, - sub_issue: false, - type: undefined, - }; - - const { data: viewIssues, mutate: mutateViewIssues } = useSWR( - workspaceSlug ? WORKSPACE_VIEW_ISSUES(workspaceSlug.toString(), params) : null, - workspaceSlug ? () => workspaceService.getViewIssues(workspaceSlug.toString(), params) : null - ); - - const makeIssueCopy = useCallback( - (issue: IIssue) => { - setCreateIssueModal(true); - - setPreloadedData({ ...issue, name: `${issue.name} (Copy)`, actionType: "createIssue" }); - }, - [setCreateIssueModal, setPreloadedData] - ); - - const handleEditIssue = useCallback( - (issue: IIssue) => { - setEditIssueModal(true); - setIssueToEdit({ - ...issue, - actionType: "edit", - cycle: issue.issue_cycle ? issue.issue_cycle.cycle : null, - module: issue.issue_module ? issue.issue_module.module : null, - }); - }, - [setEditIssueModal, setIssueToEdit] - ); - - const handleDeleteIssue = useCallback( - (issue: IIssue) => { - setDeleteIssueModal(true); - setIssueToDelete(issue); - }, - [setDeleteIssueModal, setIssueToDelete] - ); - - const handleIssueAction = useCallback( - (issue: IIssue, action: "copy" | "edit" | "delete" | "updateDraft") => { - if (action === "copy") makeIssueCopy(issue); - else if (action === "edit") handleEditIssue(issue); - else if (action === "delete") handleDeleteIssue(issue); - }, - [makeIssueCopy, handleEditIssue, handleDeleteIssue] - ); - - const nullFilters = - filters.filters && - Object.keys(filters.filters).filter( - (key) => - filters.filters[key as keyof IWorkspaceIssueFilterOptions] === null || - (filters.filters[key as keyof IWorkspaceIssueFilterOptions]?.length ?? 0) <= 0 - ); - - const areFiltersApplied = - filters.filters && - Object.keys(filters.filters).length > 0 && - nullFilters.length !== Object.keys(filters.filters).length; - - const { projects: allProjects } = useProjects(); - const joinedProjects = allProjects?.filter((p) => p.is_member); - - return ( - <> - setCreateIssueModal(false)} - prePopulateData={{ - ...preloadedData, - }} - onSubmit={async () => { - mutateViewIssues(); - }} - /> - setEditIssueModal(false)} - data={issueToEdit} - onSubmit={async () => { - mutateViewIssues(); - }} - /> - setDeleteIssueModal(false)} - isOpen={deleteIssueModal} - data={issueToDelete} - user={user} - onSubmit={async () => { - mutateViewIssues(); - }} - /> - setCreateViewModal(null)} - preLoadedData={createViewModal} - /> -
-
- setCreateViewModal(true)} /> -
- {areFiltersApplied && ( - <> -
- handleFilters("filters", updatedFilter)} - labels={workspaceLabels} - members={workspaceMembers?.map((m) => m.member)} - stateGroup={STATE_GROUP} - project={joinedProjects} - clearAllFilters={() => - handleFilters("filters", { - assignees: null, - created_by: null, - labels: null, - priority: null, - state_group: null, - start_date: null, - target_date: null, - subscriber: null, - project: null, - }) - } - /> - { - if (globalViewId) handleFilters("filters", filters.filters, true); - else - setCreateViewModal({ - query: filters.filters, - }); - }} - className="flex items-center gap-2 text-sm" - > - {!globalViewId && } - {globalViewId ? "Update" : "Save"} view - -
- {
} - - )} - -
-
-
- - ); -}; diff --git a/web/components/issues/workspace-views/workspace-assigned-issue.tsx b/web/components/issues/workspace-views/workspace-assigned-issue.tsx deleted file mode 100644 index 4469804ac86..00000000000 --- a/web/components/issues/workspace-views/workspace-assigned-issue.tsx +++ /dev/null @@ -1,148 +0,0 @@ -import React, { useCallback, useState } from "react"; -import { useRouter } from "next/router"; - -import useSWR from "swr"; - -// hook -import useUser from "hooks/use-user"; -// context -import { useProjectMyMembership } from "contexts/project-member.context"; -// services -import workspaceService from "services/workspace.service"; -// components -import { SpreadsheetView } from "components/core"; -import { WorkspaceViewsNavigation } from "components/workspace/views/workpace-view-navigation"; -import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; -import { CreateUpdateWorkspaceViewModal } from "components/workspace/views/modal"; -// fetch-keys -import { WORKSPACE_VIEW_ISSUES } from "constants/fetch-keys"; -// types -import { IIssue } from "types"; - -export const WorkspaceAssignedIssue = () => { - const [createViewModal, setCreateViewModal] = useState(null); - - // create issue modal - const [createIssueModal, setCreateIssueModal] = useState(false); - const [preloadedData, setPreloadedData] = useState< - (Partial & { actionType: "createIssue" | "edit" | "delete" }) | undefined - >(undefined); - - // update issue modal - const [editIssueModal, setEditIssueModal] = useState(false); - const [issueToEdit, setIssueToEdit] = useState< - (IIssue & { actionType: "edit" | "delete" }) | undefined - >(undefined); - - // delete issue modal - const [deleteIssueModal, setDeleteIssueModal] = useState(false); - const [issueToDelete, setIssueToDelete] = useState(null); - - const router = useRouter(); - const { workspaceSlug } = router.query; - - const { user } = useUser(); - - const { memberRole } = useProjectMyMembership(); - - const params: any = { - assignees: user?.id ?? undefined, - sub_issue: false, - }; - - const { data: viewIssues, mutate: mutateIssues } = useSWR( - workspaceSlug ? WORKSPACE_VIEW_ISSUES(workspaceSlug.toString(), params) : null, - workspaceSlug ? () => workspaceService.getViewIssues(workspaceSlug.toString(), params) : null - ); - - const makeIssueCopy = useCallback( - (issue: IIssue) => { - setCreateIssueModal(true); - - setPreloadedData({ ...issue, name: `${issue.name} (Copy)`, actionType: "createIssue" }); - }, - [setCreateIssueModal, setPreloadedData] - ); - - const handleEditIssue = useCallback( - (issue: IIssue) => { - setEditIssueModal(true); - setIssueToEdit({ - ...issue, - actionType: "edit", - cycle: issue.issue_cycle ? issue.issue_cycle.cycle : null, - module: issue.issue_module ? issue.issue_module.module : null, - }); - }, - [setEditIssueModal, setIssueToEdit] - ); - - const handleDeleteIssue = useCallback( - (issue: IIssue) => { - setDeleteIssueModal(true); - setIssueToDelete(issue); - }, - [setDeleteIssueModal, setIssueToDelete] - ); - - const handleIssueAction = useCallback( - (issue: IIssue, action: "copy" | "edit" | "delete" | "updateDraft") => { - if (action === "copy") makeIssueCopy(issue); - else if (action === "edit") handleEditIssue(issue); - else if (action === "delete") handleDeleteIssue(issue); - }, - [makeIssueCopy, handleEditIssue, handleDeleteIssue] - ); - return ( - <> - setCreateIssueModal(false)} - prePopulateData={{ - ...preloadedData, - }} - onSubmit={async () => { - mutateIssues(); - }} - /> - setEditIssueModal(false)} - data={issueToEdit} - onSubmit={async () => { - mutateIssues(); - }} - /> - setDeleteIssueModal(false)} - isOpen={deleteIssueModal} - data={issueToDelete} - user={user} - onSubmit={async () => { - mutateIssues(); - }} - /> - setCreateViewModal(null)} - preLoadedData={createViewModal} - /> -
-
- setCreateViewModal(true)} /> - -
- -
-
-
- - ); -}; diff --git a/web/components/issues/workspace-views/workspace-created-issues.tsx b/web/components/issues/workspace-views/workspace-created-issues.tsx deleted file mode 100644 index bcc83c38b4b..00000000000 --- a/web/components/issues/workspace-views/workspace-created-issues.tsx +++ /dev/null @@ -1,147 +0,0 @@ -import React, { useCallback, useState } from "react"; - -import { useRouter } from "next/router"; - -import useSWR from "swr"; - -// hook -import useUser from "hooks/use-user"; -// context -import { useProjectMyMembership } from "contexts/project-member.context"; -// services -import workspaceService from "services/workspace.service"; -// components -import { SpreadsheetView } from "components/core"; -import { WorkspaceViewsNavigation } from "components/workspace/views/workpace-view-navigation"; -import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; -import { CreateUpdateWorkspaceViewModal } from "components/workspace/views/modal"; -// fetch-keys -import { WORKSPACE_VIEW_ISSUES } from "constants/fetch-keys"; -// types -import { IIssue } from "types"; - -export const WorkspaceCreatedIssues = () => { - const [createViewModal, setCreateViewModal] = useState(null); - - // create issue modal - const [createIssueModal, setCreateIssueModal] = useState(false); - const [preloadedData, setPreloadedData] = useState< - (Partial & { actionType: "createIssue" | "edit" | "delete" }) | undefined - >(undefined); - - // update issue modal - const [editIssueModal, setEditIssueModal] = useState(false); - const [issueToEdit, setIssueToEdit] = useState< - (IIssue & { actionType: "edit" | "delete" }) | undefined - >(undefined); - - // delete issue modal - const [deleteIssueModal, setDeleteIssueModal] = useState(false); - const [issueToDelete, setIssueToDelete] = useState(null); - - const router = useRouter(); - const { workspaceSlug } = router.query; - - const { user } = useUser(); - const { memberRole } = useProjectMyMembership(); - - const params: any = { - created_by: user?.id ?? undefined, - sub_issue: false, - }; - - const { data: viewIssues, mutate: mutateIssues } = useSWR( - workspaceSlug ? WORKSPACE_VIEW_ISSUES(workspaceSlug.toString(), params) : null, - workspaceSlug ? () => workspaceService.getViewIssues(workspaceSlug.toString(), params) : null - ); - - const makeIssueCopy = useCallback( - (issue: IIssue) => { - setCreateIssueModal(true); - - setPreloadedData({ ...issue, name: `${issue.name} (Copy)`, actionType: "createIssue" }); - }, - [setCreateIssueModal, setPreloadedData] - ); - - const handleEditIssue = useCallback( - (issue: IIssue) => { - setEditIssueModal(true); - setIssueToEdit({ - ...issue, - actionType: "edit", - cycle: issue.issue_cycle ? issue.issue_cycle.cycle : null, - module: issue.issue_module ? issue.issue_module.module : null, - }); - }, - [setEditIssueModal, setIssueToEdit] - ); - - const handleDeleteIssue = useCallback( - (issue: IIssue) => { - setDeleteIssueModal(true); - setIssueToDelete(issue); - }, - [setDeleteIssueModal, setIssueToDelete] - ); - - const handleIssueAction = useCallback( - (issue: IIssue, action: "copy" | "edit" | "delete" | "updateDraft") => { - if (action === "copy") makeIssueCopy(issue); - else if (action === "edit") handleEditIssue(issue); - else if (action === "delete") handleDeleteIssue(issue); - }, - [makeIssueCopy, handleEditIssue, handleDeleteIssue] - ); - return ( - <> - setCreateIssueModal(false)} - prePopulateData={{ - ...preloadedData, - }} - onSubmit={async () => { - mutateIssues(); - }} - /> - setEditIssueModal(false)} - data={issueToEdit} - onSubmit={async () => { - mutateIssues(); - }} - /> - setDeleteIssueModal(false)} - isOpen={deleteIssueModal} - data={issueToDelete} - user={user} - onSubmit={async () => { - mutateIssues(); - }} - /> - setCreateViewModal(null)} - preLoadedData={createViewModal} - /> -
-
- setCreateViewModal(true)} /> -
- -
-
-
- - ); -}; diff --git a/web/components/issues/workspace-views/workspace-issue-view-option.tsx b/web/components/issues/workspace-views/workspace-issue-view-option.tsx deleted file mode 100644 index 25ddc338a11..00000000000 --- a/web/components/issues/workspace-views/workspace-issue-view-option.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import React from "react"; - -import { useRouter } from "next/router"; - -// hooks -import { useWorkspaceView } from "hooks/use-workspace-view"; -// components -import { GlobalSelectFilters } from "components/workspace/views/global-select-filters"; -// ui -import { Tooltip } from "components/ui"; -// icons -import { FormatListBulletedOutlined } from "@mui/icons-material"; -import { CreditCard } from "lucide-react"; -// helpers -import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper"; -import { checkIfArraysHaveSameElements } from "helpers/array.helper"; -// types -import { TIssueViewOptions } from "types"; - -const issueViewOptions: { type: TIssueViewOptions; Icon: any }[] = [ - { - type: "list", - Icon: FormatListBulletedOutlined, - }, - { - type: "spreadsheet", - Icon: CreditCard, - }, -]; - -export const WorkspaceIssuesViewOptions: React.FC = () => { - const router = useRouter(); - const { workspaceSlug, globalViewId } = router.query; - - const { filters, handleFilters } = useWorkspaceView(); - - const isWorkspaceViewPath = router.pathname.includes("workspace-views/all-issues"); - - const showFilters = isWorkspaceViewPath || globalViewId; - - return ( -
-
- {issueViewOptions.map((option) => ( - {replaceUnderscoreIfSnakeCase(option.type)} View - } - position="bottom" - > - - - ))} -
- - {showFilters && ( - <> - { - const key = option.key as keyof typeof filters.filters; - - if (key === "start_date" || key === "target_date") { - const valueExists = checkIfArraysHaveSameElements( - filters.filters?.[key] ?? [], - option.value - ); - - handleFilters("filters", { - ...filters, - [key]: valueExists ? null : option.value, - }); - } else { - if (!filters?.filters?.[key]?.includes(option.value)) - handleFilters("filters", { - ...filters, - [key]: [...((filters?.filters?.[key] as any[]) ?? []), option.value], - }); - else { - handleFilters("filters", { - ...filters, - [key]: (filters?.filters?.[key] as any[])?.filter( - (item) => item !== option.value - ), - }); - } - } - }} - direction="left" - /> - - )} -
- ); -}; diff --git a/web/components/issues/workspace-views/workspace-subscribed-issue.tsx b/web/components/issues/workspace-views/workspace-subscribed-issue.tsx deleted file mode 100644 index d9db2f347fe..00000000000 --- a/web/components/issues/workspace-views/workspace-subscribed-issue.tsx +++ /dev/null @@ -1,148 +0,0 @@ -import React, { useCallback, useState } from "react"; - -import { useRouter } from "next/router"; - -import useSWR from "swr"; - -// hook -import useUser from "hooks/use-user"; -// context -import { useProjectMyMembership } from "contexts/project-member.context"; -// services -import workspaceService from "services/workspace.service"; -// components -import { SpreadsheetView } from "components/core"; -import { WorkspaceViewsNavigation } from "components/workspace/views/workpace-view-navigation"; -import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; -import { CreateUpdateWorkspaceViewModal } from "components/workspace/views/modal"; -// fetch-keys -import { WORKSPACE_VIEW_ISSUES } from "constants/fetch-keys"; -// types -import { IIssue } from "types"; - -export const WorkspaceSubscribedIssues = () => { - const [createViewModal, setCreateViewModal] = useState(null); - - // create issue modal - const [createIssueModal, setCreateIssueModal] = useState(false); - const [preloadedData, setPreloadedData] = useState< - (Partial & { actionType: "createIssue" | "edit" | "delete" }) | undefined - >(undefined); - - // update issue modal - const [editIssueModal, setEditIssueModal] = useState(false); - const [issueToEdit, setIssueToEdit] = useState< - (IIssue & { actionType: "edit" | "delete" }) | undefined - >(undefined); - - // delete issue modal - const [deleteIssueModal, setDeleteIssueModal] = useState(false); - const [issueToDelete, setIssueToDelete] = useState(null); - - const router = useRouter(); - const { workspaceSlug } = router.query; - - const { user } = useUser(); - const { memberRole } = useProjectMyMembership(); - - const params: any = { - subscriber: user?.id ?? undefined, - sub_issue: false, - }; - - const { data: viewIssues, mutate: mutateIssues } = useSWR( - workspaceSlug ? WORKSPACE_VIEW_ISSUES(workspaceSlug.toString(), params) : null, - workspaceSlug ? () => workspaceService.getViewIssues(workspaceSlug.toString(), params) : null - ); - - const makeIssueCopy = useCallback( - (issue: IIssue) => { - setCreateIssueModal(true); - - setPreloadedData({ ...issue, name: `${issue.name} (Copy)`, actionType: "createIssue" }); - }, - [setCreateIssueModal, setPreloadedData] - ); - - const handleEditIssue = useCallback( - (issue: IIssue) => { - setEditIssueModal(true); - setIssueToEdit({ - ...issue, - actionType: "edit", - cycle: issue.issue_cycle ? issue.issue_cycle.cycle : null, - module: issue.issue_module ? issue.issue_module.module : null, - }); - }, - [setEditIssueModal, setIssueToEdit] - ); - - const handleDeleteIssue = useCallback( - (issue: IIssue) => { - setDeleteIssueModal(true); - setIssueToDelete(issue); - }, - [setDeleteIssueModal, setIssueToDelete] - ); - - const handleIssueAction = useCallback( - (issue: IIssue, action: "copy" | "edit" | "delete" | "updateDraft") => { - if (action === "copy") makeIssueCopy(issue); - else if (action === "edit") handleEditIssue(issue); - else if (action === "delete") handleDeleteIssue(issue); - }, - [makeIssueCopy, handleEditIssue, handleDeleteIssue] - ); - return ( - <> - setCreateIssueModal(false)} - prePopulateData={{ - ...preloadedData, - }} - onSubmit={async () => { - mutateIssues(); - }} - /> - setEditIssueModal(false)} - data={issueToEdit} - onSubmit={async () => { - mutateIssues(); - }} - /> - setDeleteIssueModal(false)} - isOpen={deleteIssueModal} - data={issueToDelete} - user={user} - onSubmit={async () => { - mutateIssues(); - }} - /> - setCreateViewModal(null)} - preLoadedData={createViewModal} - /> -
-
- setCreateViewModal(true)} /> - -
- -
-
-
- - ); -}; diff --git a/web/components/workspace/index.ts b/web/components/workspace/index.ts index bb0f28a9367..a3fcfc03ccf 100644 --- a/web/components/workspace/index.ts +++ b/web/components/workspace/index.ts @@ -1,3 +1,4 @@ +export * from "./views"; export * from "./activity-graph"; export * from "./completed-issues-graph"; export * from "./create-workspace-form"; diff --git a/web/components/workspace/views/default-view-list-item.tsx b/web/components/workspace/views/default-view-list-item.tsx new file mode 100644 index 00000000000..e3298daeef7 --- /dev/null +++ b/web/components/workspace/views/default-view-list-item.tsx @@ -0,0 +1,36 @@ +import { useRouter } from "next/router"; +import Link from "next/link"; +import { observer } from "mobx-react-lite"; + +// icons +import { Sparkles } from "lucide-react"; +// helpers +import { truncateText } from "helpers/string.helper"; + +type Props = { view: { key: string; label: string } }; + +export const GlobalDefaultViewListItem: React.FC = observer((props) => { + const { view } = props; + + const router = useRouter(); + const { workspaceSlug } = router.query; + + return ( + + ); +}); diff --git a/web/components/workspace/views/delete-workspace-view-modal.tsx b/web/components/workspace/views/delete-view-modal.tsx similarity index 69% rename from web/components/workspace/views/delete-workspace-view-modal.tsx rename to web/components/workspace/views/delete-view-modal.tsx index 6030f630f11..8943433e194 100644 --- a/web/components/workspace/views/delete-workspace-view-modal.tsx +++ b/web/components/workspace/views/delete-view-modal.tsx @@ -1,13 +1,10 @@ import React, { useState } from "react"; - import { useRouter } from "next/router"; - -import { mutate } from "swr"; - -// headless ui import { Dialog, Transition } from "@headlessui/react"; -// services -import workspaceService from "services/workspace.service"; +import { observer } from "mobx-react-lite"; + +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; // hooks import useToast from "hooks/use-toast"; // ui @@ -16,58 +13,50 @@ import { DangerButton, SecondaryButton } from "components/ui"; import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; // types import { IWorkspaceView } from "types/workspace-views"; -// fetch-keys -import { WORKSPACE_VIEWS_LIST } from "constants/fetch-keys"; type Props = { + data: IWorkspaceView; isOpen: boolean; - setIsOpen: React.Dispatch>; - data: IWorkspaceView | null; + onClose: () => void; }; -export const DeleteWorkspaceViewModal: React.FC = ({ isOpen, data, setIsOpen }) => { +export const DeleteGlobalViewModal: React.FC = observer((props) => { + const { data, isOpen, onClose } = props; + const [isDeleteLoading, setIsDeleteLoading] = useState(false); const router = useRouter(); const { workspaceSlug } = router.query; + const { globalViews: globalViewsStore } = useMobxStore(); + const { setToastAlert } = useToast(); const handleClose = () => { - setIsOpen(false); - setIsDeleteLoading(false); + onClose(); }; const handleDeletion = async () => { - setIsDeleteLoading(true); - - if (!workspaceSlug || !data) return; + if (!workspaceSlug) return; - await workspaceService - .deleteView(workspaceSlug as string, data.id) - .then(() => { - mutate(WORKSPACE_VIEWS_LIST(workspaceSlug as string), (views) => - views?.filter((view) => view.id !== data.id) - ); - - handleClose(); + setIsDeleteLoading(true); - setToastAlert({ - type: "success", - title: "Success!", - message: "View deleted successfully.", - }); - }) - .catch(() => { + await globalViewsStore + .deleteGlobalView(workspaceSlug.toString(), data.id) + .catch(() => setToastAlert({ type: "error", title: "Error!", - message: "View could not be deleted. Please try again.", - }); - }) + message: "Something went wrong while deleting the view. Please try again.", + }) + ) .finally(() => { setIsDeleteLoading(false); + handleClose(); }); + + // remove filters from local storage + localStorage.removeItem(`global_view_filters/${data.id}`); }; return ( @@ -100,26 +89,17 @@ export const DeleteWorkspaceViewModal: React.FC = ({ isOpen, data, setIsO
-
- + Delete View

Are you sure you want to delete view-{" "} - - {data?.name} - - ? All of the data related to the view will be permanently removed. This - action cannot be undone. + {data?.name}? All of the + data related to the view will be permanently removed. This action cannot be undone.

@@ -138,4 +118,4 @@ export const DeleteWorkspaceViewModal: React.FC = ({ isOpen, data, setIsO ); -}; +}); diff --git a/web/components/workspace/views/form.tsx b/web/components/workspace/views/form.tsx index b16d613990c..ffe5e2b795e 100644 --- a/web/components/workspace/views/form.tsx +++ b/web/components/workspace/views/form.tsx @@ -1,38 +1,26 @@ import { useEffect } from "react"; - import { useRouter } from "next/router"; +import { observer } from "mobx-react-lite"; +import { Controller, useForm } from "react-hook-form"; -import useSWR from "swr"; - -// react-hook-form -import { useForm } from "react-hook-form"; -// services -import issuesService from "services/issues.service"; - +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; // hooks -import useProjects from "hooks/use-projects"; import useWorkspaceMembers from "hooks/use-workspace-members"; // components -import { WorkspaceFiltersList } from "components/core"; -import { GlobalSelectFilters } from "components/workspace/views/global-select-filters"; - +import { AppliedFiltersList, FilterSelection, FiltersDropdown } from "components/issues"; // ui import { Input, PrimaryButton, SecondaryButton, TextArea } from "components/ui"; -// helpers -import { checkIfArraysHaveSameElements } from "helpers/array.helper"; // types -import { IQuery } from "types"; -import { IWorkspaceView } from "types/workspace-views"; -// fetch-keys -import { WORKSPACE_LABELS } from "constants/fetch-keys"; -import { STATE_GROUP } from "constants/project"; +import { IWorkspaceView } from "types"; +// constants +import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; type Props = { - handleFormSubmit: (values: IWorkspaceView) => Promise; + handleFormSubmit: (values: Partial) => Promise; handleClose: () => void; - status: boolean; - data?: IWorkspaceView | null; - preLoadedData?: Partial | null; + data?: IWorkspaceView; + preLoadedData?: Partial; }; const defaultValues: Partial = { @@ -40,41 +28,31 @@ const defaultValues: Partial = { description: "", }; -export const WorkspaceViewForm: React.FC = ({ - handleFormSubmit, - handleClose, - status, - data, - preLoadedData, -}) => { +export const WorkspaceViewForm: React.FC = observer((props) => { + const { handleFormSubmit, handleClose, data, preLoadedData } = props; + const router = useRouter(); const { workspaceSlug } = router.query; + const { workspace: workspaceStore, project: projectStore } = useMobxStore(); + const { - register, + control, formState: { errors, isSubmitting }, handleSubmit, + register, reset, - watch, setValue, - } = useForm({ + watch, + } = useForm({ defaultValues, }); - const filters = watch("query"); - - const { data: labelOptions } = useSWR( - workspaceSlug ? WORKSPACE_LABELS(workspaceSlug.toString()) : null, - workspaceSlug ? () => issuesService.getWorkspaceLabels(workspaceSlug.toString()) : null - ); const { workspaceMembers } = useWorkspaceMembers(workspaceSlug?.toString() ?? ""); const memberOptions = workspaceMembers?.map((m) => m.member); - const { projects: allProjects } = useProjects(); - const joinedProjects = allProjects?.filter((p) => p.is_member); - - const handleCreateUpdateView = async (formData: IWorkspaceView) => { + const handleCreateUpdateView = async (formData: Partial) => { await handleFormSubmit(formData); reset({ @@ -82,20 +60,6 @@ export const WorkspaceViewForm: React.FC = ({ }); }; - const clearAllFilters = () => { - setValue("query", { - assignees: null, - created_by: null, - subscriber: null, - labels: null, - priority: null, - state_group: null, - start_date: null, - target_date: null, - project: null, - }); - }; - useEffect(() => { reset({ ...defaultValues, @@ -104,18 +68,24 @@ export const WorkspaceViewForm: React.FC = ({ }); }, [data, preLoadedData, reset]); + const selectedFilters = watch("query_data")?.filters; + + const clearAllFilters = () => { + if (!selectedFilters) return; + + setValue("query_data.filters", {}); + }; + useEffect(() => { - if (status && data) { - setValue("query", data.query_data); - } - }, [data, status, setValue]); + if (!data) return; + + reset({ ...data }); + }, [data, reset]); return (
-

- {status ? "Update" : "Create"} View -

+

{data ? "Update" : "Create"} View

= ({ />
- { - const key = option.key as keyof typeof filters; - - if (key === "start_date" || key === "target_date") { - const valueExists = checkIfArraysHaveSameElements( - filters?.[key] ?? [], - option.value - ); - - setValue("query", { - ...filters, - [key]: valueExists ? null : option.value, - } as IQuery); - } else { - if (!filters?.[key]?.includes(option.value)) - setValue("query", { - ...filters, - [key]: [...((filters?.[key] as any[]) ?? []), option.value], - }); - else { - setValue("query", { - ...filters, - [key]: (filters?.[key] as any[])?.filter((item) => item !== option.value), - }); - } - } - }} - /> -
-
- { - setValue("query", { - ...filters, - ...query, - }); - }} + ( + + { + const newValues = filters?.[key] ?? []; + + if (Array.isArray(value)) { + value.forEach((val) => { + if (!newValues.includes(val)) newValues.push(val); + }); + } else { + if (filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1); + else newValues.push(value); + } + + onChange({ + ...filters, + [key]: newValues, + }); + }} + layoutDisplayFiltersOptions={ISSUE_DISPLAY_FILTERS_BY_LAYOUT.my_issues.spreadsheet} + labels={workspaceStore.workspaceLabels ?? undefined} + projects={workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : undefined} + /> + + )} />
+ {selectedFilters && Object.keys(selectedFilters).length > 0 && ( +
+ {}} + labels={workspaceStore.workspaceLabels ?? undefined} + members={memberOptions} + states={undefined} + /> +
+ )}
Cancel - {status + {data ? isSubmitting ? "Updating View..." : "Update View" @@ -210,4 +177,4 @@ export const WorkspaceViewForm: React.FC = ({
); -}; +}); diff --git a/web/components/workspace/views/global-select-filters.tsx b/web/components/workspace/views/global-select-filters.tsx deleted file mode 100644 index 967ee1bf800..00000000000 --- a/web/components/workspace/views/global-select-filters.tsx +++ /dev/null @@ -1,301 +0,0 @@ -import { useState } from "react"; - -import { useRouter } from "next/router"; - -import useSWR from "swr"; - -// hook -import useProjects from "hooks/use-projects"; -import useWorkspaceMembers from "hooks/use-workspace-members"; -// services -import issuesService from "services/issues.service"; -// components -import { DateFilterModal } from "components/core"; -// ui -import { Avatar, MultiLevelDropdown } from "components/ui"; -// icons -import { PriorityIcon, StateGroupIcon } from "components/icons"; -// helpers -import { checkIfArraysHaveSameElements } from "helpers/array.helper"; -// types -import { IWorkspaceIssueFilterOptions, TStateGroups } from "types"; -// fetch-keys -import { WORKSPACE_LABELS } from "constants/fetch-keys"; -// constants -import { GROUP_CHOICES, PRIORITIES } from "constants/project"; -import { DATE_FILTER_OPTIONS } from "constants/filters"; - -type Props = { - filters: Partial; - onSelect: (option: any) => void; - direction?: "left" | "right"; - height?: "sm" | "md" | "rg" | "lg"; -}; - -export const GlobalSelectFilters: React.FC = ({ - filters, - onSelect, - direction = "right", - height = "md", -}) => { - const [isDateFilterModalOpen, setIsDateFilterModalOpen] = useState(false); - const [dateFilterType, setDateFilterType] = useState<{ - title: string; - type: "start_date" | "target_date"; - }>({ - title: "", - type: "start_date", - }); - - const router = useRouter(); - const { workspaceSlug } = router.query; - - const { workspaceMembers } = useWorkspaceMembers(workspaceSlug?.toString() ?? ""); - - const { data: workspaceLabels } = useSWR( - workspaceSlug ? WORKSPACE_LABELS(workspaceSlug.toString()) : null, - workspaceSlug ? () => issuesService.getWorkspaceLabels(workspaceSlug.toString()) : null - ); - - const { projects: allProjects } = useProjects(); - const joinedProjects = allProjects?.filter((p) => p.is_member); - - const workspaceFilterOption = [ - { - id: "project", - label: "Project", - value: joinedProjects, - hasChildren: true, - children: joinedProjects?.map((project) => ({ - id: project.id, - label:
{project.name}
, - value: { - key: "project", - value: project.id, - }, - selected: filters?.project?.includes(project.id), - })), - }, - { - id: "state_group", - label: "State groups", - value: GROUP_CHOICES, - hasChildren: true, - children: [ - ...Object.keys(GROUP_CHOICES).map((key) => ({ - id: key, - label: ( -
- - {GROUP_CHOICES[key as keyof typeof GROUP_CHOICES]} -
- ), - value: { - key: "state_group", - value: key, - }, - selected: filters?.state_group?.includes(key), - })), - ], - }, - { - id: "labels", - label: "Labels", - value: workspaceLabels, - hasChildren: true, - children: workspaceLabels?.map((label) => ({ - id: label.id, - label: ( -
-
- {label.name} -
- ), - value: { - key: "labels", - value: label.id, - }, - selected: filters?.labels?.includes(label.id), - })), - }, - { - id: "priority", - label: "Priority", - value: PRIORITIES, - hasChildren: true, - children: PRIORITIES.map((priority) => ({ - id: priority === null ? "null" : priority, - label: ( -
- - {priority ?? "None"} -
- ), - value: { - key: "priority", - value: priority === null ? "null" : priority, - }, - selected: filters?.priority?.includes(priority === null ? "null" : priority), - })), - }, - { - id: "created_by", - label: "Created by", - value: workspaceMembers, - hasChildren: true, - children: workspaceMembers?.map((member) => ({ - id: member.member.id, - label: ( -
- - {member.member.display_name} -
- ), - value: { - key: "created_by", - value: member.member.id, - }, - selected: filters?.created_by?.includes(member.member.id), - })), - }, - { - id: "assignees", - label: "Assignees", - value: workspaceMembers, - hasChildren: true, - children: workspaceMembers?.map((member) => ({ - id: member.member.id, - label: ( -
- - {member.member.display_name} -
- ), - value: { - key: "assignees", - value: member.member.id, - }, - selected: filters?.assignees?.includes(member.member.id), - })), - }, - { - id: "subscriber", - label: "Subscriber", - value: workspaceMembers, - hasChildren: true, - children: workspaceMembers?.map((member) => ({ - id: member.member.id, - label: ( -
- - {member.member.display_name} -
- ), - value: { - key: "subscriber", - value: member.member.id, - }, - selected: filters?.subscriber?.includes(member.member.id), - })), - }, - { - id: "start_date", - label: "Start date", - value: DATE_FILTER_OPTIONS, - hasChildren: true, - children: [ - ...DATE_FILTER_OPTIONS.map((option) => ({ - id: option.name, - label: option.name, - value: { - key: "start_date", - value: option.value, - }, - selected: checkIfArraysHaveSameElements(filters?.start_date ?? [], option.value), - })), - { - id: "custom", - label: "Custom", - value: "custom", - element: ( - - ), - }, - ], - }, - { - id: "target_date", - label: "Due date", - value: DATE_FILTER_OPTIONS, - hasChildren: true, - children: [ - ...DATE_FILTER_OPTIONS.map((option) => ({ - id: option.name, - label: option.name, - value: { - key: "target_date", - value: option.value, - }, - selected: checkIfArraysHaveSameElements(filters?.target_date ?? [], option.value), - })), - { - id: "custom", - label: "Custom", - value: "custom", - element: ( - - ), - }, - ], - }, - ]; - - return ( - <> - {isDateFilterModalOpen && ( - setIsDateFilterModalOpen(false)} - isOpen={isDateFilterModalOpen} - onSelect={onSelect} - /> - )} - - - ); -}; diff --git a/web/components/workspace/views/header.tsx b/web/components/workspace/views/header.tsx new file mode 100644 index 00000000000..20692d414be --- /dev/null +++ b/web/components/workspace/views/header.tsx @@ -0,0 +1,67 @@ +import React, { useState } from "react"; +import { useRouter } from "next/router"; +import Link from "next/link"; +import { observer } from "mobx-react-lite"; + +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; +// components +import { CreateUpdateWorkspaceViewModal } from "components/workspace"; +// icon +import { PlusIcon } from "lucide-react"; +// constants +import { DEFAULT_GLOBAL_VIEWS_LIST } from "constants/workspace"; + +export const GlobalViewsHeader: React.FC = observer(() => { + const [createViewModal, setCreateViewModal] = useState(false); + + const router = useRouter(); + const { workspaceSlug, globalViewId } = router.query; + + const { globalViews: globalViewsStore } = useMobxStore(); + + const isTabSelected = (tabKey: string) => router.pathname.includes(tabKey); + + return ( + <> + setCreateViewModal(false)} /> +
+ {DEFAULT_GLOBAL_VIEWS_LIST.map((tab) => ( + + + {tab.label} + + + ))} + + {globalViewsStore.globalViewsList?.map((view) => ( + + + {view.name} + + + ))} + + +
+ + ); +}); diff --git a/web/components/workspace/views/index.ts b/web/components/workspace/views/index.ts new file mode 100644 index 00000000000..7d0547f649c --- /dev/null +++ b/web/components/workspace/views/index.ts @@ -0,0 +1,7 @@ +export * from "./default-view-list-item"; +export * from "./delete-view-modal"; +export * from "./form"; +export * from "./header"; +export * from "./modal"; +export * from "./view-list-item"; +export * from "./views-list"; diff --git a/web/components/workspace/views/modal.tsx b/web/components/workspace/views/modal.tsx index f0f02746882..37027ad1f38 100644 --- a/web/components/workspace/views/modal.tsx +++ b/web/components/workspace/views/modal.tsx @@ -1,116 +1,103 @@ import React from "react"; - import { useRouter } from "next/router"; - -import { mutate } from "swr"; - -// headless ui +import { observer } from "mobx-react-lite"; import { Dialog, Transition } from "@headlessui/react"; -// services -import workspaceService from "services/workspace.service"; + +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; // hooks import useToast from "hooks/use-toast"; // components -import { WorkspaceViewForm } from "components/workspace/views/form"; +import { WorkspaceViewForm } from "components/workspace"; // types import { IWorkspaceView } from "types/workspace-views"; -// fetch-keys -import { WORKSPACE_VIEWS_LIST } from "constants/fetch-keys"; type Props = { + data?: IWorkspaceView; isOpen: boolean; - handleClose: () => void; - data?: IWorkspaceView | null; - preLoadedData?: Partial | null; + onClose: () => void; + preLoadedData?: Partial; }; -export const CreateUpdateWorkspaceViewModal: React.FC = ({ - isOpen, - handleClose, - data, - preLoadedData, -}) => { +export const CreateUpdateWorkspaceViewModal: React.FC = observer((props) => { + const { isOpen, onClose, data, preLoadedData } = props; + const router = useRouter(); const { workspaceSlug } = router.query; + const { globalViews: globalViewsStore } = useMobxStore(); + const { setToastAlert } = useToast(); - const onClose = () => { - handleClose(); + const handleClose = () => { + onClose(); }; - const createView = async (payload: any) => { - const payloadData = { + const createView = async (payload: Partial) => { + if (!workspaceSlug) return; + + const payloadData: Partial = { ...payload, - query_data: { - filters: payload.query, + query: { + ...payload.query_data?.filters, }, }; - await workspaceService - .createView(workspaceSlug as string, payloadData) - .then((res) => { - mutate(WORKSPACE_VIEWS_LIST(workspaceSlug as string)); - handleClose(); - - router.replace(`/${workspaceSlug}/workspace-views/issues?globalViewId=${res.id}`); + await globalViewsStore + .createGlobalView(workspaceSlug.toString(), payloadData) + .then((res) => { setToastAlert({ type: "success", title: "Success!", message: "View created successfully.", }); + + router.push(`/${workspaceSlug}/workspace-views/${res.id}`); }) - .catch(() => { + .catch(() => setToastAlert({ type: "error", title: "Error!", message: "View could not be created. Please try again.", - }); - }); + }) + ); }; - const updateView = async (payload: any) => { - const payloadData = { + const updateView = async (payload: Partial) => { + if (!workspaceSlug || !data) return; + + const payloadData: Partial = { ...payload, - query_data: { - filters: payload.query, + query: { + ...payload.query_data?.filters, }, }; - await workspaceService - .updateView(workspaceSlug as string, data?.id ?? "", payloadData) - .then((res) => { - mutate( - WORKSPACE_VIEWS_LIST(workspaceSlug as string), - (prevData) => - prevData?.map((p) => { - if (p.id === res.id) return { ...p, ...payloadData }; - - return p; - }), - false - ); - onClose(); + await globalViewsStore + .updateGlobalView(workspaceSlug.toString(), data.id, payloadData) + .then(() => setToastAlert({ type: "success", title: "Success!", message: "View updated successfully.", - }); - }) - .catch(() => { + }) + ) + .catch(() => setToastAlert({ type: "error", title: "Error!", message: "View could not be updated. Please try again.", - }); - }); + }) + ); }; - const handleFormSubmit = async (formData: any) => { + const handleFormSubmit = async (formData: Partial) => { if (!workspaceSlug) return; if (!data) await createView(formData); else await updateView(formData); + + handleClose(); }; return ( @@ -143,7 +130,6 @@ export const CreateUpdateWorkspaceViewModal: React.FC = ({ @@ -154,4 +140,4 @@ export const CreateUpdateWorkspaceViewModal: React.FC = ({ ); -}; +}); diff --git a/web/components/workspace/views/single-workspace-view-item.tsx b/web/components/workspace/views/single-workspace-view-item.tsx deleted file mode 100644 index fd153bdfcb7..00000000000 --- a/web/components/workspace/views/single-workspace-view-item.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import React from "react"; - -import Link from "next/link"; -import { useRouter } from "next/router"; - -// icons -import { TrashIcon, PencilIcon } from "@heroicons/react/24/outline"; -import { PhotoFilterOutlined } from "@mui/icons-material"; -//components -import { CustomMenu } from "components/ui"; -import { IWorkspaceView } from "types/workspace-views"; -// helpers -import { truncateText } from "helpers/string.helper"; - -type Props = { - view: IWorkspaceView; - handleEditView: () => void; - handleDeleteView: () => void; -}; - -export const SingleWorkspaceViewItem: React.FC = ({ - view, - handleEditView, - handleDeleteView, -}) => { - const router = useRouter(); - const { workspaceSlug } = router.query; - - const viewRedirectionUrl = `/${workspaceSlug}/workspace-views/issues?globalViewId=${view.id}`; - - return ( -
- - -
-
-
- -
-
-

- {truncateText(view.name, 75)} -

- {view?.description && ( -

{view.description}

- )} -
-
-
-
-

- {view.query_data.filters && Object.keys(view.query_data.filters).length > 0 - ? `${Object.keys(view.query_data.filters) - .map((key: string) => - view.query_data.filters[key as keyof typeof view.query_data.filters] !== - null - ? isNaN( - ( - view.query_data.filters[ - key as keyof typeof view.query_data.filters - ] as any - ).length - ) - ? 0 - : ( - view.query_data.filters[ - key as keyof typeof view.query_data.filters - ] as any - ).length - : 0 - ) - .reduce((curr, prev) => curr + prev, 0)} filters` - : "0 filters"} -

- - { - e.preventDefault(); - e.stopPropagation(); - handleEditView(); - }} - > - - - Edit View - - - { - e.preventDefault(); - e.stopPropagation(); - handleDeleteView(); - }} - > - - - Delete View - - - -
-
-
-
- -
- ); -}; diff --git a/web/components/workspace/views/view-list-item.tsx b/web/components/workspace/views/view-list-item.tsx new file mode 100644 index 00000000000..e3dc1d64163 --- /dev/null +++ b/web/components/workspace/views/view-list-item.tsx @@ -0,0 +1,97 @@ +import { useRouter } from "next/router"; +import Link from "next/link"; +import { observer } from "mobx-react-lite"; +import { useState } from "react"; + +// components +import { CreateUpdateWorkspaceViewModal, DeleteGlobalViewModal } from "components/workspace"; +// ui +import { CustomMenu } from "components/ui"; +// icons +import { PencilIcon, Sparkles, TrashIcon } from "lucide-react"; +// helpers +import { truncateText } from "helpers/string.helper"; +// types +import { IWorkspaceView } from "types/workspace-views"; + +type Props = { view: IWorkspaceView }; + +export const GlobalViewListItem: React.FC = observer((props) => { + const { view } = props; + + const [updateViewModal, setUpdateViewModal] = useState(false); + const [deleteViewModal, setDeleteViewModal] = useState(false); + + const router = useRouter(); + const { workspaceSlug } = router.query; + + const totalFilters = + view?.query_data?.filters && Object.keys(view.query_data.filters).length > 0 + ? Object.keys(view.query_data.filters) + .map((key) => + view.query_data.filters[key as keyof typeof view.query_data.filters] !== null + ? isNaN((view.query_data.filters[key as keyof typeof view.query_data.filters] as any).length) + ? 0 + : (view.query_data.filters[key as keyof typeof view.query_data.filters] as any).length + : 0 + ) + .reduce((curr, prev) => curr + prev, 0) + : 0; + + return ( + <> + setUpdateViewModal(false)} /> + setDeleteViewModal(false)} /> + + + ); +}); diff --git a/web/components/workspace/views/views-list.tsx b/web/components/workspace/views/views-list.tsx new file mode 100644 index 00000000000..ccfeba75b80 --- /dev/null +++ b/web/components/workspace/views/views-list.tsx @@ -0,0 +1,41 @@ +import { observer } from "mobx-react-lite"; + +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; +// components +import { GlobalViewListItem } from "components/workspace"; +// ui +import { Loader } from "components/ui"; + +type Props = { + searchQuery: string; +}; + +export const GlobalViewsList: React.FC = observer((props) => { + const { searchQuery } = props; + + const { globalViews: globalViewsStore } = useMobxStore(); + + const viewsList = globalViewsStore.globalViewsList; + + if (!viewsList) + return ( + + + + + + + + ); + + const filteredViewsList = viewsList.filter((v) => v.name.toLowerCase().includes(searchQuery.toLowerCase())); + + return ( + <> + {filteredViewsList.map((view) => ( + + ))} + + ); +}); diff --git a/web/components/workspace/views/workpace-view-navigation.tsx b/web/components/workspace/views/workpace-view-navigation.tsx deleted file mode 100644 index 867764a5d7d..00000000000 --- a/web/components/workspace/views/workpace-view-navigation.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import React from "react"; - -import { useRouter } from "next/router"; - -import useSWR from "swr"; - -// icon -import { PlusIcon } from "lucide-react"; -// constant -import { WORKSPACE_VIEWS_LIST } from "constants/fetch-keys"; -// service -import workspaceService from "services/workspace.service"; - -type Props = { - handleAddView: () => void; -}; - -export const WorkspaceViewsNavigation: React.FC = ({ handleAddView }) => { - const router = useRouter(); - const { workspaceSlug, globalViewId } = router.query; - - const { data: workspaceViews } = useSWR( - workspaceSlug ? WORKSPACE_VIEWS_LIST(workspaceSlug.toString()) : null, - workspaceSlug ? () => workspaceService.getAllViews(workspaceSlug.toString()) : null - ); - - const isSelected = (pathName: string) => router.pathname.includes(pathName); - React.useEffect(() => { - const activeTabElement = document.getElementById("active-tab-global-view"); - if (activeTabElement) activeTabElement.scrollIntoView({ behavior: "smooth", inline: "center" }); - }, [globalViewId, workspaceViews]); - - const tabsList = [ - { - key: "all", - label: "All Issues", - selected: isSelected("workspace-views/all-issues"), - onClick: () => router.replace(`/${workspaceSlug}/workspace-views/all-issues`), - }, - { - key: "assigned", - label: "Assigned", - selected: isSelected("workspace-views/assigned"), - onClick: () => router.replace(`/${workspaceSlug}/workspace-views/assigned`), - }, - { - key: "created", - label: "Created", - selected: isSelected("workspace-views/created"), - onClick: () => router.replace(`/${workspaceSlug}/workspace-views/created`), - }, - { - key: "subscribed", - label: "Subscribed", - selected: isSelected("workspace-views/subscribed"), - onClick: () => router.replace(`/${workspaceSlug}/workspace-views/subscribed`), - }, - ]; - - return ( -
- {tabsList.map((tab) => ( - - ))} - - {workspaceViews && - workspaceViews.length > 0 && - workspaceViews?.map((view) => ( - - ))} - - -
- ); -}; diff --git a/web/constants/fetch-keys.ts b/web/constants/fetch-keys.ts index df1ff8e4402..e5b22fb9531 100644 --- a/web/constants/fetch-keys.ts +++ b/web/constants/fetch-keys.ts @@ -146,12 +146,9 @@ export const PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS = (projectId: string, params? return `PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS${projectId.toUpperCase()}_${paramsKey}`; }; -export const WORKSPACE_VIEWS_LIST = (workspaceSlug: string) => `WORKSPACE_VIEWS_LIST_${workspaceSlug.toUpperCase()}`; -export const WORKSPACE_VIEW_DETAILS = (globalViewId: string) => `WORKSPACE_VIEW_DETAILS_${globalViewId.toUpperCase()}`; -export const WORKSPACE_VIEW_ISSUES = (globalViewId: string, params: any) => { - if (!params) return `WORKSPACE_VIEW_ISSUES_${globalViewId.toUpperCase()}`; - return `WORKSPACE_VIEW_ISSUES_${globalViewId.toUpperCase()}_${paramsToKey(params).toUpperCase()}`; -}; +export const GLOBAL_VIEWS_LIST = (workspaceSlug: string) => `GLOBAL_VIEWS_LIST_${workspaceSlug.toUpperCase()}`; +export const GLOBAL_VIEW_DETAILS = (globalViewId: string) => `GLOBAL_VIEW_DETAILS_${globalViewId.toUpperCase()}`; +export const GLOBAL_VIEW_ISSUES = (globalViewId: string) => `GLOBAL_VIEW_ISSUES_${globalViewId.toUpperCase()}`; export const PROJECT_ISSUES_DETAILS = (issueId: string) => `PROJECT_ISSUES_DETAILS_${issueId.toUpperCase()}`; export const PROJECT_ISSUES_PROPERTIES = (projectId: string) => `PROJECT_ISSUES_PROPERTIES_${projectId.toUpperCase()}`; diff --git a/web/constants/issue.ts b/web/constants/issue.ts index 1c69858aee8..145125b9564 100644 --- a/web/constants/issue.ts +++ b/web/constants/issue.ts @@ -220,25 +220,10 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: { [pageType: string]: { [layoutType: string]: ILayoutDisplayFiltersOptions }; } = { my_issues: { - list: { - filters: ["priority", "state_group", "labels", "start_date", "target_date"], - display_properties: true, - display_filters: { - group_by: ["state_detail.group", "project", "priority", "labels", null], - order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "priority"], - type: [null, "active", "backlog"], - }, - extra_options: { - access: true, - values: ["show_empty_groups", "sub_issue"], - }, - }, - kanban: { - filters: ["priority", "state_group", "labels", "start_date", "target_date"], + spreadsheet: { + filters: ["priority", "state_group", "labels", "assignees", "created_by", "project", "start_date", "target_date"], display_properties: true, display_filters: { - group_by: ["state_detail.group", "project", "priority", "labels"], - sub_group_by: ["state_detail.group", "project", "priority", "labels", null], order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "priority"], type: [null, "active", "backlog"], }, diff --git a/web/constants/workspace.ts b/web/constants/workspace.ts index 0683e39af6f..8f55fac4c40 100644 --- a/web/constants/workspace.ts +++ b/web/constants/workspace.ts @@ -4,6 +4,7 @@ import JiraLogo from "public/services/jira.png"; import CSVLogo from "public/services/csv.svg"; import ExcelLogo from "public/services/excel.svg"; import JSONLogo from "public/services/json.svg"; +import { TStaticViewTypes } from "types"; export const ROLE = { 5: "Guest", @@ -67,3 +68,25 @@ export const EXPORTERS_LIST = [ logo: JSONLogo, }, ]; + +export const DEFAULT_GLOBAL_VIEWS_LIST: { + key: TStaticViewTypes; + label: string; +}[] = [ + { + key: "all-issues", + label: "All issues", + }, + { + key: "assigned", + label: "Assigned", + }, + { + key: "created", + label: "Created", + }, + { + key: "subscribed", + label: "Subscribed", + }, +]; diff --git a/web/contexts/workspace-member.context.tsx b/web/contexts/workspace-member.context.tsx index 5f34bd28b23..dfba45b24f5 100644 --- a/web/contexts/workspace-member.context.tsx +++ b/web/contexts/workspace-member.context.tsx @@ -46,8 +46,7 @@ export const WorkspaceMemberProvider: React.FC = (props) => { export const useWorkspaceMyMembership = () => { const context = useContext(WorkspaceMemberContext); - if (context === undefined) - throw new Error(`useWorkspaceMember must be used within a WorkspaceMemberProvider.`); + if (context === undefined) throw new Error(`useWorkspaceMember must be used within a WorkspaceMemberProvider.`); return { ...context, diff --git a/web/helpers/emoji.helper.tsx b/web/helpers/emoji.helper.tsx index 7c9f3cfcb2d..38b830071cc 100644 --- a/web/helpers/emoji.helper.tsx +++ b/web/helpers/emoji.helper.tsx @@ -30,7 +30,7 @@ export const renderEmoji = ( if (typeof emoji === "object") return ( - + {emoji.name} ); @@ -41,16 +41,13 @@ export const groupReactions: (reactions: any[], key: string) => { [key: string]: reactions: any, key: string ) => { - const groupedReactions = reactions.reduce( - (acc: any, reaction: any) => { - if (!acc[reaction[key]]) { - acc[reaction[key]] = []; - } - acc[reaction[key]].push(reaction); - return acc; - }, - {} as { [key: string]: any[] } - ); + const groupedReactions = reactions.reduce((acc: any, reaction: any) => { + if (!acc[reaction[key]]) { + acc[reaction[key]] = []; + } + acc[reaction[key]].push(reaction); + return acc; + }, {} as { [key: string]: any[] }); return groupedReactions; }; diff --git a/web/helpers/filter.helper.ts b/web/helpers/filter.helper.ts new file mode 100644 index 00000000000..8c392b5ae5f --- /dev/null +++ b/web/helpers/filter.helper.ts @@ -0,0 +1,19 @@ +// types +import { IIssueFilterOptions } from "types"; + +// check if there is any difference between the saved filters and the current filters +export const areFiltersDifferent = (filtersSet1: IIssueFilterOptions, filtersSet2: IIssueFilterOptions) => { + for (const [key, value] of Object.entries(filtersSet1) as [keyof IIssueFilterOptions, string[] | null][]) { + if (value) { + if (Array.isArray(value) && Array.isArray(filtersSet2[key])) { + if (value.length !== filtersSet2[key]?.length) return true; + + for (let i = 0; i < value.length; i++) { + if (!filtersSet2[key]?.includes(value[i])) return true; + } + } else if (value !== filtersSet2[key]) return true; + } + } + + return false; +}; diff --git a/web/lib/mobx/store-init.tsx b/web/lib/mobx/store-init.tsx index 2c1c8fb9bac..9383376bf28 100644 --- a/web/lib/mobx/store-init.tsx +++ b/web/lib/mobx/store-init.tsx @@ -12,12 +12,13 @@ const MobxStoreInit = () => { workspace: workspaceStore, project: projectStore, module: moduleStore, + globalViews: globalViewsStore, } = useMobxStore(); // theme const { setTheme } = useTheme(); // router const router = useRouter(); - const { workspaceSlug, projectId, moduleId } = router.query; + const { workspaceSlug, projectId, moduleId, globalViewId } = router.query; useEffect(() => { // sidebar collapsed toggle @@ -47,7 +48,8 @@ const MobxStoreInit = () => { if (workspaceSlug) workspaceStore.setWorkspaceSlug(workspaceSlug.toString()); if (projectId) projectStore.setProjectId(projectId.toString()); if (moduleId) moduleStore.setModuleId(moduleId.toString()); - }, [workspaceSlug, projectId, moduleId, workspaceStore, projectStore, moduleStore]); + if (globalViewId) globalViewsStore.setGlobalViewId(globalViewId.toString()); + }, [workspaceSlug, projectId, moduleId, globalViewId, workspaceStore, projectStore, moduleStore, globalViewsStore]); return <>; }; diff --git a/web/package.json b/web/package.json index 2d2a1ddf423..e9f6bbf2c2b 100644 --- a/web/package.json +++ b/web/package.json @@ -27,6 +27,7 @@ "@nivo/pie": "0.80.0", "@nivo/scatterplot": "0.80.0", "@plane/ui": "*", + "@popperjs/core": "^2.11.8", "@sentry/nextjs": "^7.36.0", "@tiptap/extension-code-block-lowlight": "^2.0.4", "@tiptap/extension-color": "^2.0.4", @@ -74,6 +75,7 @@ "react-hook-form": "^7.38.0", "react-markdown": "^8.0.7", "react-moveable": "^0.54.1", + "react-popper": "^2.3.0", "sharp": "^0.32.1", "sonner": "^0.6.2", "swr": "^2.1.3", @@ -84,6 +86,7 @@ "uuid": "^9.0.0" }, "devDependencies": { + "@plane/ui": "*", "@types/js-cookie": "^3.0.2", "@types/node": "18.0.6", "@types/nprogress": "^0.2.0", @@ -100,7 +103,6 @@ "prettier": "^2.8.7", "tailwind-config-custom": "*", "tsconfig": "*", - "@plane/ui": "*", "typescript": "4.7.4" }, "resolutions": { diff --git a/web/pages/[workspaceSlug]/workspace-views/[globalViewId].tsx b/web/pages/[workspaceSlug]/workspace-views/[globalViewId].tsx new file mode 100644 index 00000000000..17d7259e21f --- /dev/null +++ b/web/pages/[workspaceSlug]/workspace-views/[globalViewId].tsx @@ -0,0 +1,57 @@ +import { useRouter } from "next/router"; +import useSWR from "swr"; + +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; +// layouts +import { WorkspaceAuthorizationLayout } from "layouts/auth-layout-legacy"; +// components +import { GlobalViewsHeader } from "components/workspace"; +import { GlobalViewsAllLayouts } from "components/issues"; +import { GlobalIssuesHeader } from "components/headers"; +// icons +import { CheckCircle } from "lucide-react"; +// types +import { NextPage } from "next"; +// fetch-keys +import { GLOBAL_VIEWS_LIST, GLOBAL_VIEW_DETAILS } from "constants/fetch-keys"; + +const GlobalViewIssues: NextPage = () => { + const router = useRouter(); + const { workspaceSlug, globalViewId } = router.query; + + const { globalViews: globalViewsStore } = useMobxStore(); + + useSWR( + workspaceSlug ? GLOBAL_VIEWS_LIST(workspaceSlug.toString()) : null, + workspaceSlug ? () => globalViewsStore.fetchAllGlobalViews(workspaceSlug.toString()) : null + ); + + useSWR( + workspaceSlug && globalViewId ? GLOBAL_VIEW_DETAILS(globalViewId.toString()) : null, + workspaceSlug && globalViewId + ? () => globalViewsStore.fetchGlobalViewDetails(workspaceSlug.toString(), globalViewId.toString()) + : null + ); + + return ( + + + Workspace issues +
+ } + right={} + > +
+
+ + +
+
+ + ); +}; + +export default GlobalViewIssues; diff --git a/web/pages/[workspaceSlug]/workspace-views/all-issues.tsx b/web/pages/[workspaceSlug]/workspace-views/all-issues.tsx index e41b50a4aa1..6761f70401d 100644 --- a/web/pages/[workspaceSlug]/workspace-views/all-issues.tsx +++ b/web/pages/[workspaceSlug]/workspace-views/all-issues.tsx @@ -1,40 +1,50 @@ +import { useRouter } from "next/router"; +import useSWR from "swr"; + +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; +// components +import { GlobalViewsHeader } from "components/workspace"; +import { GlobalIssuesHeader } from "components/headers"; +import { GlobalViewsAllLayouts } 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"; +import { WorkspaceAuthorizationLayout } from "layouts/auth-layout-legacy"; // icons -import { PlusIcon } from "@heroicons/react/24/outline"; import { CheckCircle } from "lucide-react"; +// types +import { NextPage } from "next"; +// fetch-keys +import { GLOBAL_VIEWS_LIST } from "constants/fetch-keys"; -const WorkspaceViewAllIssue = () => ( - - - Workspace issues -
- } - right={ -
- +const GlobalViewAllIssues: NextPage = () => { + const router = useRouter(); + const { workspaceSlug } = router.query; + + const { globalViews: globalViewsStore } = useMobxStore(); + + useSWR( + workspaceSlug ? GLOBAL_VIEWS_LIST(workspaceSlug.toString()) : null, + workspaceSlug ? () => globalViewsStore.fetchAllGlobalViews(workspaceSlug.toString()) : null + ); - { - const e = new KeyboardEvent("keydown", { key: "c" }); - document.dispatchEvent(e); - }} - > - - Add Issue - + return ( + + + Workspace issues +
+ } + right={} + > +
+
+ + +
- } - > - - -); + + ); +}; -export default WorkspaceViewAllIssue; +export default GlobalViewAllIssues; diff --git a/web/pages/[workspaceSlug]/workspace-views/assigned.tsx b/web/pages/[workspaceSlug]/workspace-views/assigned.tsx index 0edef4d81b2..5d6aafc009b 100644 --- a/web/pages/[workspaceSlug]/workspace-views/assigned.tsx +++ b/web/pages/[workspaceSlug]/workspace-views/assigned.tsx @@ -1,40 +1,50 @@ -// layouts -import { WorkspaceAuthorizationLayout } from "layouts/auth-layout"; +import { useRouter } from "next/router"; +import useSWR from "swr"; + +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; // 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"; +import { GlobalViewsHeader } from "components/workspace"; +import { GlobalIssuesHeader } from "components/headers"; +import { GlobalViewsAllLayouts } from "components/issues"; +// layouts +import { WorkspaceAuthorizationLayout } from "layouts/auth-layout-legacy"; // icons -import { PlusIcon } from "@heroicons/react/24/outline"; import { CheckCircle } from "lucide-react"; +// types +import { NextPage } from "next"; +// fetch-keys +import { GLOBAL_VIEWS_LIST } from "constants/fetch-keys"; -const WorkspaceViewAssignedIssue: React.FC = () => ( - - - Workspace Issues -
- } - right={ -
- - { - const e = new KeyboardEvent("keydown", { key: "c" }); - document.dispatchEvent(e); - }} - > - - Add Issue - +const GlobalViewAssignedIssues: NextPage = () => { + const router = useRouter(); + const { workspaceSlug } = router.query; + + const { globalViews: globalViewsStore } = useMobxStore(); + + useSWR( + workspaceSlug ? GLOBAL_VIEWS_LIST(workspaceSlug.toString()) : null, + workspaceSlug ? () => globalViewsStore.fetchAllGlobalViews(workspaceSlug.toString()) : null + ); + + return ( + + + Workspace issues +
+ } + right={} + > +
+
+ + +
- } - > - - -); + + ); +}; -export default WorkspaceViewAssignedIssue; +export default GlobalViewAssignedIssues; diff --git a/web/pages/[workspaceSlug]/workspace-views/created.tsx b/web/pages/[workspaceSlug]/workspace-views/created.tsx index 07ddbbae996..699133fcbfd 100644 --- a/web/pages/[workspaceSlug]/workspace-views/created.tsx +++ b/web/pages/[workspaceSlug]/workspace-views/created.tsx @@ -1,40 +1,50 @@ -// layouts -import { WorkspaceAuthorizationLayout } from "layouts/auth-layout"; +import { useRouter } from "next/router"; +import useSWR from "swr"; + +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; // 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"; +import { GlobalViewsHeader } from "components/workspace"; +import { GlobalIssuesHeader } from "components/headers"; +import { GlobalViewsAllLayouts } from "components/issues"; +// layouts +import { WorkspaceAuthorizationLayout } from "layouts/auth-layout-legacy"; // icons -import { PlusIcon } from "@heroicons/react/24/outline"; import { CheckCircle } from "lucide-react"; +// types +import { NextPage } from "next"; +// fetch-keys +import { GLOBAL_VIEWS_LIST } from "constants/fetch-keys"; -const WorkspaceViewCreatedIssue: React.FC = () => ( - - - Workspace Issues -
- } - right={ -
- - { - const e = new KeyboardEvent("keydown", { key: "c" }); - document.dispatchEvent(e); - }} - > - - Add Issue - +const GlobalViewCreatedIssues: NextPage = () => { + const router = useRouter(); + const { workspaceSlug } = router.query; + + const { globalViews: globalViewsStore } = useMobxStore(); + + useSWR( + workspaceSlug ? GLOBAL_VIEWS_LIST(workspaceSlug.toString()) : null, + workspaceSlug ? () => globalViewsStore.fetchAllGlobalViews(workspaceSlug.toString()) : null + ); + + return ( + + + Workspace issues +
+ } + right={} + > +
+
+ + +
- } - > - - -); + + ); +}; -export default WorkspaceViewCreatedIssue; +export default GlobalViewCreatedIssues; diff --git a/web/pages/[workspaceSlug]/workspace-views/index.tsx b/web/pages/[workspaceSlug]/workspace-views/index.tsx index 5e1ef61a875..3d9285b7688 100644 --- a/web/pages/[workspaceSlug]/workspace-views/index.tsx +++ b/web/pages/[workspaceSlug]/workspace-views/index.tsx @@ -1,97 +1,37 @@ 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"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; // layouts -import { WorkspaceAuthorizationLayout } from "layouts/auth-layout"; +import { WorkspaceAuthorizationLayout } from "layouts/auth-layout-legacy"; // 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 "components/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"; // 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"; +// fetch-keys +import { GLOBAL_VIEWS_LIST } from "constants/fetch-keys"; const WorkspaceViews: NextPage = () => { const [query, setQuery] = useState(""); - const [createUpdateViewModal, setCreateUpdateViewModal] = useState(false); - const [selectedViewToUpdate, setSelectedViewToUpdate] = useState(null); - - const [deleteViewModal, setDeleteViewModal] = useState(false); - const [selectedViewToDelete, setSelectedViewToDelete] = useState(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 { globalViews: globalViewsStore } = useMobxStore(); - 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); - }; + useSWR( + workspaceSlug ? GLOBAL_VIEWS_LIST(workspaceSlug.toString()) : null, + workspaceSlug ? () => globalViewsStore.fetchAllGlobalViews(workspaceSlug.toString()) : null + ); return ( { Workspace Views
} - right={ -
- - - setCreateUpdateViewModal(true)} - > - - New View - -
- } + right={} > - { - setCreateUpdateViewModal(false); - setSelectedViewToUpdate(null); - }} - data={selectedViewToUpdate} - /> -
@@ -140,64 +55,10 @@ const WorkspaceViews: NextPage = () => { />
- {filteredDefaultOptions && - filteredDefaultOptions.length > 0 && - filteredDefaultOptions.map((option) => ( - - ))} - - {filteredOptions ? ( - filteredOptions.length > 0 ? ( -
- {filteredOptions.map((view) => ( - handleEditView(view)} - handleDeleteView={() => handleDeleteView(view)} - /> - ))} -
- ) : ( - , - text: "New View", - onClick: () => setCreateUpdateViewModal(true), - }} - /> - ) - ) : ( - - - - - - - - )} + {DEFAULT_GLOBAL_VIEWS_LIST.filter((v) => v.label.toLowerCase().includes(query.toLowerCase())).map((option) => ( + + ))} +
); diff --git a/web/pages/[workspaceSlug]/workspace-views/issues.tsx b/web/pages/[workspaceSlug]/workspace-views/issues.tsx deleted file mode 100644 index 1f94604c700..00000000000 --- a/web/pages/[workspaceSlug]/workspace-views/issues.tsx +++ /dev/null @@ -1,40 +0,0 @@ -// layouts -import { WorkspaceAuthorizationLayout } from "layouts/auth-layout"; -// components -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 -import { PlusIcon } from "@heroicons/react/24/outline"; -import { CheckCircle } from "lucide-react"; - -const WorkspaceView = () => ( - - - Workspace issues -
- } - right={ -
- - { - const e = new KeyboardEvent("keydown", { key: "c" }); - document.dispatchEvent(e); - }} - > - - Add Issue - -
- } - > - - -); - -export default WorkspaceView; diff --git a/web/pages/[workspaceSlug]/workspace-views/subscribed.tsx b/web/pages/[workspaceSlug]/workspace-views/subscribed.tsx index 50da24a82d2..74984df5b5d 100644 --- a/web/pages/[workspaceSlug]/workspace-views/subscribed.tsx +++ b/web/pages/[workspaceSlug]/workspace-views/subscribed.tsx @@ -1,40 +1,50 @@ -// layouts -import { WorkspaceAuthorizationLayout } from "layouts/auth-layout"; +import { useRouter } from "next/router"; +import useSWR from "swr"; + +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; // 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"; +import { GlobalViewsHeader } from "components/workspace"; +import { GlobalIssuesHeader } from "components/headers"; +import { GlobalViewsAllLayouts } from "components/issues"; +// layouts +import { WorkspaceAuthorizationLayout } from "layouts/auth-layout-legacy"; // icons -import { PlusIcon } from "@heroicons/react/24/outline"; import { CheckCircle } from "lucide-react"; +// types +import { NextPage } from "next"; +// fetch-keys +import { GLOBAL_VIEWS_LIST } from "constants/fetch-keys"; -const WorkspaceViewSubscribedIssue: React.FC = () => ( - - - Workspace Issue -
- } - right={ -
- - { - const e = new KeyboardEvent("keydown", { key: "c" }); - document.dispatchEvent(e); - }} - > - - Add Issue - +const GlobalViewSubscribedIssues: NextPage = () => { + const router = useRouter(); + const { workspaceSlug } = router.query; + + const { globalViews: globalViewsStore } = useMobxStore(); + + useSWR( + workspaceSlug ? GLOBAL_VIEWS_LIST(workspaceSlug.toString()) : null, + workspaceSlug ? () => globalViewsStore.fetchAllGlobalViews(workspaceSlug.toString()) : null + ); + + return ( + + + Workspace issues +
+ } + right={} + > +
+
+ + +
- } - > - - -); + + ); +}; -export default WorkspaceViewSubscribedIssue; +export default GlobalViewSubscribedIssues; diff --git a/web/services/workspace.service.ts b/web/services/workspace.service.ts index 0351c2bdb8e..bdeb4bffb63 100644 --- a/web/services/workspace.service.ts +++ b/web/services/workspace.service.ts @@ -14,9 +14,9 @@ import { ICurrentUserResponse, IWorkspaceBulkInviteFormData, IWorkspaceViewProps, - IWorkspaceViewIssuesParams, } from "types"; import { IWorkspaceView } from "types/workspace-views"; +import { IIssueGroupWithSubGroupsStructure, IIssueGroupedStructure, IIssueUnGroupedStructure } from "store/issue"; export class WorkspaceService extends APIService { constructor() { @@ -249,7 +249,7 @@ export class WorkspaceService extends APIService { }); } - async createView(workspaceSlug: string, data: IWorkspaceView): Promise { + async createView(workspaceSlug: string, data: Partial): Promise { return this.post(`/api/workspaces/${workspaceSlug}/views/`, data) .then((response) => response?.data) .catch((error) => { @@ -257,11 +257,7 @@ export class WorkspaceService extends APIService { }); } - async updateView( - workspaceSlug: string, - viewId: string, - data: Partial - ): Promise { + async updateView(workspaceSlug: string, viewId: string, data: Partial): Promise { return this.patch(`/api/workspaces/${workspaceSlug}/views/${viewId}/`, data) .then((response) => response?.data) .catch((error) => { @@ -293,7 +289,10 @@ export class WorkspaceService extends APIService { }); } - async getViewIssues(workspaceSlug: string, params: IWorkspaceViewIssuesParams): Promise { + async getViewIssues( + workspaceSlug: string, + params: any + ): Promise { return this.get(`/api/workspaces/${workspaceSlug}/issues/`, { params, }) diff --git a/web/store/global-views.ts b/web/store/global-views.ts deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/web/store/global_view_filters.ts b/web/store/global_view_filters.ts new file mode 100644 index 00000000000..8a5157c8b9d --- /dev/null +++ b/web/store/global_view_filters.ts @@ -0,0 +1,70 @@ +import { observable, action, makeObservable, runInAction } from "mobx"; +// types +import { RootStore } from "./root"; +import { IIssueFilterOptions } from "types"; + +export interface IGlobalViewFiltersStore { + // states + loader: boolean; + error: any | null; + + // observables + storedFilters: { + [viewId: string]: IIssueFilterOptions; + }; + + // actions + updateStoredFilters: (viewId: string, filters: Partial) => void; + deleteStoredFilters: (viewId: string) => void; +} + +class GlobalViewFiltersStore implements IGlobalViewFiltersStore { + // states + loader: boolean = false; + error: any | null = null; + + // observables + storedFilters: { + [viewId: string]: IIssueFilterOptions; + } = {}; + + // root store + rootStore; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + // states + loader: observable.ref, + error: observable.ref, + + // observables + storedFilters: observable.ref, + + // actions + updateStoredFilters: action, + deleteStoredFilters: action, + }); + + this.rootStore = _rootStore; + } + + updateStoredFilters = (viewId: string, filters: Partial) => { + runInAction(() => { + this.storedFilters = { + ...this.storedFilters, + [viewId]: { ...this.storedFilters[viewId], ...filters }, + }; + }); + }; + + deleteStoredFilters = (viewId: string) => { + const updatedStoredFilters = { ...this.storedFilters }; + delete updatedStoredFilters[viewId]; + + runInAction(() => { + this.storedFilters = updatedStoredFilters; + }); + }; +} + +export default GlobalViewFiltersStore; diff --git a/web/store/global_view_issues.ts b/web/store/global_view_issues.ts new file mode 100644 index 00000000000..48e7bc41fbd --- /dev/null +++ b/web/store/global_view_issues.ts @@ -0,0 +1,167 @@ +import { observable, action, makeObservable, runInAction } from "mobx"; +// services +import { ProjectService } from "services/project.service"; +import { WorkspaceService } from "services/workspace.service"; +// helpers +import { handleIssueQueryParamsByLayout } from "helpers/issue.helper"; +// types +import { RootStore } from "./root"; +import { IIssue, IIssueFilterOptions, TStaticViewTypes } from "types"; + +export interface IGlobalViewIssuesStore { + // states + loader: boolean; + error: any | null; + + // observables + viewIssues: { + [viewId: string]: IIssue[]; + }; + + // actions + fetchViewIssues: (workspaceSlug: string, viewId: string, filters: IIssueFilterOptions) => Promise; + fetchStaticIssues: (workspaceSlug: string, type: TStaticViewTypes) => Promise; +} + +class GlobalViewIssuesStore implements IGlobalViewIssuesStore { + // states + loader: boolean = false; + error: any | null = null; + + // observables + viewIssues: { + [viewId: string]: IIssue[]; + } = {}; + + // root store + rootStore; + + // services + projectService; + workspaceService; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + // states + loader: observable.ref, + error: observable.ref, + + // observables + viewIssues: observable.ref, + + // actions + fetchViewIssues: action, + fetchStaticIssues: action, + }); + + this.rootStore = _rootStore; + + this.projectService = new ProjectService(); + this.workspaceService = new WorkspaceService(); + } + + computedFilter = (filters: any, filteredParams: any) => { + const computedFilters: any = {}; + Object.keys(filters).map((key) => { + if (filters[key] != undefined && filteredParams.includes(key)) + computedFilters[key] = + typeof filters[key] === "string" || typeof filters[key] === "boolean" ? filters[key] : filters[key].join(","); + }); + + return computedFilters; + }; + + fetchViewIssues = async (workspaceSlug: string, viewId: string, filters: IIssueFilterOptions) => { + try { + runInAction(() => { + this.loader = true; + }); + + const displayFilters = this.rootStore.workspaceFilter.workspaceDisplayFilters; + + let filteredRouteParams: any = { + priority: filters?.priority || undefined, + project: filters?.project || undefined, + state_group: filters?.state_group || undefined, + state: filters?.state || undefined, + assignees: filters?.assignees || undefined, + created_by: filters?.created_by || undefined, + labels: filters?.labels || undefined, + start_date: filters?.start_date || undefined, + target_date: filters?.target_date || undefined, + order_by: displayFilters?.order_by || "-created_at", + type: displayFilters?.type || undefined, + sub_issue: false, + }; + + const filteredParams = handleIssueQueryParamsByLayout("spreadsheet", "my_issues"); + if (filteredParams) filteredRouteParams = this.computedFilter(filteredRouteParams, filteredParams); + + const response = await this.workspaceService.getViewIssues(workspaceSlug, filteredRouteParams); + + runInAction(() => { + this.loader = false; + this.viewIssues = { + ...this.viewIssues, + [viewId]: response as IIssue[], + }; + }); + + return response; + } catch (error) { + runInAction(() => { + this.loader = false; + this.error = error; + }); + + throw error; + } + }; + + fetchStaticIssues = async (workspaceSlug: string, type: TStaticViewTypes) => { + try { + runInAction(() => { + this.loader = true; + }); + + const workspaceMemberResponse = await this.rootStore.workspaceFilter.fetchUserWorkspaceFilters(workspaceSlug); + const displayFilters = workspaceMemberResponse.view_props.display_filters; + + let filteredRouteParams: any = { + order_by: displayFilters?.order_by || "-created_at", + type: displayFilters?.type || undefined, + sub_issue: false, + }; + + const filteredParams = handleIssueQueryParamsByLayout("spreadsheet", "my_issues"); + if (filteredParams) filteredRouteParams = this.computedFilter(filteredRouteParams, filteredParams); + + const currentUser = this.rootStore.user.currentUser; + + if (type === "assigned" && currentUser) filteredRouteParams.assignees = currentUser.id; + if (type === "created" && currentUser) filteredRouteParams.created_by = currentUser.id; + if (type === "subscribed" && currentUser) filteredRouteParams.subscriber = currentUser.id; + + const response = await this.workspaceService.getViewIssues(workspaceSlug, filteredRouteParams); + + runInAction(() => { + this.loader = false; + this.viewIssues = { + ...this.viewIssues, + [type]: response as IIssue[], + }; + }); + + return response; + } catch (error) { + runInAction(() => { + this.loader = false; + this.error = error; + }); + + throw error; + } + }; +} + +export default GlobalViewIssuesStore; diff --git a/web/store/global_views.ts b/web/store/global_views.ts new file mode 100644 index 00000000000..53e67ca22de --- /dev/null +++ b/web/store/global_views.ts @@ -0,0 +1,207 @@ +import { observable, action, makeObservable, runInAction } from "mobx"; +// services +import { ProjectService } from "services/project.service"; +import { WorkspaceService } from "services/workspace.service"; +// types +import { RootStore } from "./root"; +import { IWorkspaceView } from "types/workspace-views"; + +export interface IGlobalViewsStore { + // states + loader: boolean; + error: any | null; + + // observables + globalViewId: string | null; + globalViewsList: IWorkspaceView[] | null; + globalViewDetails: { + [viewId: string]: IWorkspaceView; + }; + + // actions + setGlobalViewId: (viewId: string) => void; + + fetchAllGlobalViews: (workspaceSlug: string) => Promise; + fetchGlobalViewDetails: (workspaceSlug: string, viewId: string) => Promise; + createGlobalView: (workspaceSlug: string, data: Partial) => Promise; + updateGlobalView: (workspaceSlug: string, viewId: string, data: Partial) => Promise; + deleteGlobalView: (workspaceSlug: string, viewId: string) => Promise; +} + +class GlobalViewsStore implements IGlobalViewsStore { + // states + loader: boolean = false; + error: any | null = null; + + // observables + globalViewId: string | null = null; + globalViewsList: IWorkspaceView[] | null = null; + globalViewDetails: { [viewId: string]: IWorkspaceView } = {}; + + // root store + rootStore; + + // services + projectService; + workspaceService; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + // states + loader: observable.ref, + error: observable.ref, + + // observables + globalViewId: observable.ref, + globalViewsList: observable.ref, + globalViewDetails: observable.ref, + + // actions + setGlobalViewId: action, + + fetchAllGlobalViews: action, + fetchGlobalViewDetails: action, + createGlobalView: action, + updateGlobalView: action, + deleteGlobalView: action, + }); + + this.rootStore = _rootStore; + + this.projectService = new ProjectService(); + this.workspaceService = new WorkspaceService(); + } + + setGlobalViewId = (viewId: string) => { + this.globalViewId = viewId; + }; + + fetchAllGlobalViews = async (workspaceSlug: string): Promise => { + try { + runInAction(() => { + this.loader = true; + }); + + const response = await this.workspaceService.getAllViews(workspaceSlug); + + runInAction(() => { + this.loader = false; + this.globalViewsList = response; + }); + + return response; + } catch (error) { + runInAction(() => { + this.loader = false; + this.error = error; + }); + + throw error; + } + }; + + fetchGlobalViewDetails = async (workspaceSlug: string, viewId: string): Promise => { + try { + runInAction(() => { + this.loader = true; + }); + + const response = await this.workspaceService.getViewDetails(workspaceSlug, viewId); + + runInAction(() => { + this.loader = false; + this.globalViewDetails = { + ...this.globalViewDetails, + [response.id]: response, + }; + }); + + return response; + } catch (error) { + runInAction(() => { + this.loader = false; + this.error = error; + }); + + throw error; + } + }; + + createGlobalView = async (workspaceSlug: string, data: Partial): Promise => { + try { + const response = await this.workspaceService.createView(workspaceSlug, data); + + runInAction(() => { + this.globalViewsList = [response, ...(this.globalViewsList ?? [])]; + this.globalViewDetails = { + ...this.globalViewDetails, + [response.id]: response, + }; + }); + + return response; + } catch (error) { + runInAction(() => { + this.error = error; + }); + + throw error; + } + }; + + updateGlobalView = async ( + workspaceSlug: string, + viewId: string, + data: Partial + ): Promise => { + const viewToUpdate = { ...this.globalViewDetails[viewId], ...data }; + + try { + runInAction(() => { + this.globalViewsList = (this.globalViewsList ?? []).map((view) => { + if (view.id === viewId) return viewToUpdate; + + return view; + }); + this.globalViewDetails = { + ...this.globalViewDetails, + [viewId]: viewToUpdate, + }; + }); + + const response = await this.workspaceService.updateView(workspaceSlug, viewId, data); + + return response; + } catch (error) { + this.fetchGlobalViewDetails(workspaceSlug, viewId); + + runInAction(() => { + this.error = error; + }); + + throw error; + } + }; + + deleteGlobalView = async (workspaceSlug: string, viewId: string): Promise => { + const newViewsList = (this.globalViewsList ?? []).filter((view) => view.id !== viewId); + + try { + runInAction(() => { + this.globalViewsList = newViewsList; + }); + + await this.workspaceService.deleteView(workspaceSlug, viewId); + } catch (error) { + this.fetchAllGlobalViews(workspaceSlug); + + runInAction(() => { + this.error = error; + }); + + throw error; + } + }; +} + +export default GlobalViewsStore; diff --git a/web/store/project.ts b/web/store/project.ts index 057337c4fbb..558498b77af 100644 --- a/web/store/project.ts +++ b/web/store/project.ts @@ -7,10 +7,6 @@ import { ProjectService } from "services/project.service"; import { IssueService } from "services/issue.service"; import { ProjectStateServices } from "services/project_state.service"; import { ProjectEstimateServices } from "services/project_estimates.service"; -import { CycleService } from "services/cycles.service"; -import { ModuleService } from "services/modules.service"; -import { ViewService } from "services/views.service"; -import { PageService } from "services/page.service"; export interface IProjectStore { loader: boolean; @@ -18,7 +14,7 @@ export interface IProjectStore { searchQuery: string; projectId: string | null; - projects: { [key: string]: IProject[] }; + projects: { [workspaceSlug: string]: IProject[] }; project_details: { [projectId: string]: IProject; // projectId: project Info }; diff --git a/web/store/root.ts b/web/store/root.ts index 69e67b36748..60cdefd5ac5 100644 --- a/web/store/root.ts +++ b/web/store/root.ts @@ -21,15 +21,21 @@ import IssueFilterStore, { IIssueFilterStore } from "./issue_filters"; import IssueViewDetailStore from "./issue_detail"; import IssueKanBanViewStore from "./kanban_view"; import CalendarStore, { ICalendarStore } from "./calendar"; +import GlobalViewsStore, { IGlobalViewsStore } from "./global_views"; +import GlobalViewIssuesStore, { IGlobalViewIssuesStore } from "./global_view_issues"; +import WorkspaceFilterStore, { IWorkspaceFilterStore } from "./workspace_filters"; +import GlobalViewFiltersStore, { IGlobalViewFiltersStore } from "./global_view_filters"; enableStaticRendering(typeof window === "undefined"); export class RootStore { user; theme; - projectPublish: IProjectPublishStore; - draftIssuesStore: DraftIssuesStore; + workspace: IWorkspaceStore; + workspaceFilter: IWorkspaceFilterStore; + + projectPublish: IProjectPublishStore; project: IProjectStore; issue: IIssueStore; @@ -47,12 +53,21 @@ export class RootStore { issueFilter: IIssueFilterStore; issueDetail: IssueViewDetailStore; issueKanBanView: IssueKanBanViewStore; + draftIssuesStore: DraftIssuesStore; + calendar: ICalendarStore; + globalViews: IGlobalViewsStore; + globalViewIssues: IGlobalViewIssuesStore; + globalViewFilters: IGlobalViewFiltersStore; + constructor() { this.user = new UserStore(this); this.theme = new ThemeStore(this); + this.workspace = new WorkspaceStore(this); + this.workspaceFilter = new WorkspaceFilterStore(this); + this.project = new ProjectStore(this); this.projectPublish = new ProjectPublishStore(this); @@ -72,6 +87,11 @@ export class RootStore { this.issueDetail = new IssueViewDetailStore(this); this.issueKanBanView = new IssueKanBanViewStore(this); this.draftIssuesStore = new DraftIssuesStore(this); + this.calendar = new CalendarStore(this); + + this.globalViews = new GlobalViewsStore(this); + this.globalViewIssues = new GlobalViewIssuesStore(this); + this.globalViewFilters = new GlobalViewFiltersStore(this); } } diff --git a/web/store/workspace.ts b/web/store/workspace.ts index 0c2d48d72f6..b9d3771ca62 100644 --- a/web/store/workspace.ts +++ b/web/store/workspace.ts @@ -1,62 +1,81 @@ import { action, computed, observable, makeObservable, runInAction } from "mobx"; import { RootStore } from "./root"; // types -import { IIssueLabels, IProject, IWorkspace } from "types"; +import { IIssueLabels, IProject, IWorkspace, IWorkspaceMember } from "types"; // services import { WorkspaceService } from "services/workspace.service"; import { ProjectService } from "services/project.service"; import { IssueService } from "services/issue.service"; export interface IWorkspaceStore { + // states loader: boolean; error: any | null; + // observables - workspaces: IWorkspace[]; - labels: { [key: string]: IIssueLabels[] } | {}; // workspace_id: labels[] workspaceSlug: string | null; - // computed - currentWorkspace: IWorkspace | null; - workspaceLabels: IIssueLabels[]; + workspaces: IWorkspace[]; + labels: { [workspaceSlug: string]: IIssueLabels[] } | {}; // workspaceSlug: labels[] + members: { [workspaceSlug: string]: IWorkspaceMember[] } | {}; // workspaceSlug: members[] + // actions setWorkspaceSlug: (workspaceSlug: string) => void; getWorkspaceBySlug: (workspaceSlug: string) => IWorkspace | null; getWorkspaceLabelById: (workspaceSlug: string, labelId: string) => IIssueLabels | null; fetchWorkspaces: () => Promise; fetchWorkspaceLabels: (workspaceSlug: string) => Promise; + fetchWorkspaceMembers: (workspaceSlug: string) => Promise; + + // computed + currentWorkspace: IWorkspace | null; + workspaceLabels: IIssueLabels[] | null; + workspaceMembers: IWorkspaceMember[] | null; } class WorkspaceStore implements IWorkspaceStore { + // states loader: boolean = false; error: any | null = null; + // observables workspaceSlug: string | null = null; workspaces: IWorkspace[] = []; projects: { [workspaceSlug: string]: IProject[] } = {}; // workspace_id: project[] labels: { [workspaceSlug: string]: IIssueLabels[] } = {}; - // root store - rootStore; + members: { [workspaceSlug: string]: IWorkspaceMember[] } = {}; + // services workspaceService; projectService; issueService; + // root store + rootStore; + constructor(_rootStore: RootStore) { makeObservable(this, { + // states loader: observable.ref, error: observable.ref, - // objects + + // observables + workspaceSlug: observable.ref, workspaces: observable.ref, labels: observable.ref, - workspaceSlug: observable.ref, - // computed - currentWorkspace: computed, - workspaceLabels: computed, + members: observable.ref, + // actions setWorkspaceSlug: action, getWorkspaceBySlug: action, getWorkspaceLabelById: action, fetchWorkspaces: action, fetchWorkspaceLabels: action, + fetchWorkspaceMembers: action, + + // computed + currentWorkspace: computed, + workspaceLabels: computed, + workspaceMembers: computed, }); this.rootStore = _rootStore; @@ -70,11 +89,12 @@ class WorkspaceStore implements IWorkspaceStore { */ get currentWorkspace() { if (!this.workspaceSlug) return null; + return this.workspaces?.find((workspace) => workspace.slug === this.workspaceSlug) || null; } /** - * computed value of workspace labels using the workspace id from the store + * computed value of workspace labels using the workspace slug from the store */ get workspaceLabels() { if (!this.workspaceSlug) return []; @@ -82,6 +102,15 @@ class WorkspaceStore implements IWorkspaceStore { return _labels && Object.keys(_labels).length > 0 ? _labels : []; } + /** + * computed value of workspace members using the workspace slug from the store + */ + get workspaceMembers() { + if (!this.workspaceSlug) return []; + const _members = this.members?.[this.workspaceSlug]; + return _members && Object.keys(_members).length > 0 ? _members : []; + } + /** * set workspace slug in the store * @param workspaceSlug @@ -132,8 +161,10 @@ class WorkspaceStore implements IWorkspaceStore { */ fetchWorkspaceLabels = async (workspaceSlug: string) => { try { - this.loader = true; - this.error = null; + runInAction(() => { + this.loader = true; + this.error = null; + }); const labelsResponse = await this.issueService.getWorkspaceLabels(workspaceSlug); @@ -146,9 +177,40 @@ class WorkspaceStore implements IWorkspaceStore { this.error = null; }); } catch (error) { - console.log(error); - this.loader = false; - this.error = error; + runInAction(() => { + this.loader = false; + this.error = error; + }); + } + }; + + /** + * fetch workspace members using workspace slug + * @param workspaceSlug + */ + + fetchWorkspaceMembers = async (workspaceSlug: string) => { + try { + runInAction(() => { + this.loader = true; + this.error = null; + }); + + const membersResponse = await this.workspaceService.workspaceMembers(workspaceSlug); + + runInAction(() => { + this.members = { + ...this.members, + [workspaceSlug]: membersResponse, + }; + this.loader = false; + this.error = null; + }); + } catch (error) { + runInAction(() => { + this.loader = false; + this.error = error; + }); } }; } diff --git a/web/store/workspace_filters.ts b/web/store/workspace_filters.ts new file mode 100644 index 00000000000..b576a25466e --- /dev/null +++ b/web/store/workspace_filters.ts @@ -0,0 +1,195 @@ +import { observable, action, computed, makeObservable, runInAction } from "mobx"; +// services +import { WorkspaceService } from "services/workspace.service"; +// helpers +import { handleIssueQueryParamsByLayout } from "helpers/issue.helper"; +// types +import { RootStore } from "./root"; +import { + IIssueDisplayFilterOptions, + IIssueDisplayProperties, + IIssueFilterOptions, + IWorkspaceMember, + IWorkspaceViewProps, + TIssueParams, +} from "types"; + +export interface IWorkspaceFilterStore { + // states + loader: boolean; + error: any | null; + + // observables + workspaceFilters: IIssueFilterOptions; + workspaceDisplayFilters: IIssueDisplayFilterOptions; + workspaceDisplayProperties: IIssueDisplayProperties; + + // actions + fetchUserWorkspaceFilters: (workspaceSlug: string) => Promise; + updateWorkspaceFilters: (workspaceSlug: string, filterToUpdate: Partial) => Promise; + + // computed + appliedFilters: TIssueParams[] | null; +} + +class WorkspaceFilterStore implements IWorkspaceFilterStore { + // states + loader: boolean = false; + error: any | null = null; + + // observables + workspaceFilters: IIssueFilterOptions = {}; + workspaceDisplayFilters: IIssueDisplayFilterOptions = {}; + workspaceDisplayProperties: IIssueDisplayProperties = { + assignee: true, + start_date: true, + due_date: true, + labels: true, + key: true, + priority: true, + state: true, + sub_issue_count: true, + link: true, + attachment_count: true, + estimate: true, + created_on: true, + updated_on: true, + }; + + // root store + rootStore; + + // services + workspaceService; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + // states + loader: observable.ref, + error: observable.ref, + + // observables + workspaceFilters: observable.ref, + workspaceDisplayFilters: observable.ref, + workspaceDisplayProperties: observable.ref, + + // actions + fetchUserWorkspaceFilters: action, + updateWorkspaceFilters: action, + + // computed + appliedFilters: computed, + }); + + this.rootStore = _rootStore; + + this.workspaceService = new WorkspaceService(); + } + + computedFilter = (filters: any, filteredParams: any) => { + const computedFilters: any = {}; + Object.keys(filters).map((key) => { + if (filters[key] != undefined && filteredParams.includes(key)) + computedFilters[key] = + typeof filters[key] === "string" || typeof filters[key] === "boolean" ? filters[key] : filters[key].join(","); + }); + + return computedFilters; + }; + + get appliedFilters(): TIssueParams[] | null { + if (!this.workspaceFilters || !this.workspaceDisplayFilters) return null; + + let filteredRouteParams: any = { + priority: this.workspaceFilters?.priority || undefined, + state_group: this.workspaceFilters?.state_group || undefined, + state: this.workspaceFilters?.state || undefined, + assignees: this.workspaceFilters?.assignees || undefined, + created_by: this.workspaceFilters?.created_by || undefined, + labels: this.workspaceFilters?.labels || undefined, + start_date: this.workspaceFilters?.start_date || undefined, + target_date: this.workspaceFilters?.target_date || undefined, + group_by: this.workspaceDisplayFilters?.group_by || "state", + order_by: this.workspaceDisplayFilters?.order_by || "-created_at", + sub_group_by: this.workspaceDisplayFilters?.sub_group_by || undefined, + type: this.workspaceDisplayFilters?.type || undefined, + sub_issue: this.workspaceDisplayFilters?.sub_issue || true, + show_empty_groups: this.workspaceDisplayFilters?.show_empty_groups || true, + start_target_date: this.workspaceDisplayFilters?.start_target_date || true, + }; + + const filteredParams = handleIssueQueryParamsByLayout(this.workspaceDisplayFilters.layout, "issues"); + if (filteredParams) filteredRouteParams = this.computedFilter(filteredRouteParams, filteredParams); + + if (this.workspaceDisplayFilters.layout === "calendar") filteredRouteParams.group_by = "target_date"; + if (this.workspaceDisplayFilters.layout === "gantt_chart") filteredRouteParams.start_target_date = true; + + return filteredRouteParams; + } + + fetchUserWorkspaceFilters = async (workspaceSlug: string) => { + try { + const memberResponse = await this.workspaceService.workspaceMemberMe(workspaceSlug); + + runInAction(() => { + this.workspaceFilters = memberResponse?.view_props?.filters; + this.workspaceDisplayFilters = memberResponse?.view_props?.display_filters ?? {}; + this.workspaceDisplayProperties = memberResponse?.view_props?.display_properties; + }); + + return memberResponse; + } catch (error) { + runInAction(() => { + this.error = error; + }); + + throw error; + } + }; + + updateWorkspaceFilters = async (workspaceSlug: string, filterToUpdate: Partial) => { + const newViewProps = { + display_filters: { + ...this.workspaceDisplayFilters, + ...filterToUpdate.display_filters, + }, + display_properties: { + ...this.workspaceDisplayProperties, + ...filterToUpdate.display_properties, + }, + filters: { + ...this.workspaceFilters, + ...filterToUpdate.filters, + }, + }; + + // set sub_group_by to null if group_by is set to null + if (newViewProps.display_filters.group_by === null) newViewProps.display_filters.sub_group_by = null; + + // set group_by to state if layout is switched to kanban and group_by is null + if (newViewProps.display_filters.layout === "kanban" && newViewProps.display_filters.group_by === null) + newViewProps.display_filters.group_by = "state"; + + try { + runInAction(() => { + this.workspaceDisplayFilters = newViewProps.display_filters; + this.workspaceDisplayProperties = newViewProps.display_properties; + this.workspaceFilters = newViewProps.filters; + }); + + this.workspaceService.updateWorkspaceView(workspaceSlug, { + view_props: newViewProps, + }); + } catch (error) { + this.fetchUserWorkspaceFilters(workspaceSlug); + + runInAction(() => { + this.error = error; + }); + + console.log("Failed to update user filters in issue filter store", error); + } + }; +} + +export default WorkspaceFilterStore; diff --git a/web/types/index.d.ts b/web/types/index.d.ts index b6811a287f8..b350901066f 100644 --- a/web/types/index.d.ts +++ b/web/types/index.d.ts @@ -19,6 +19,7 @@ export * from "./notifications"; export * from "./waitlist"; export * from "./reaction"; export * from "./view-props"; +export * from "./workspace-views"; export type NestedKeyOf = { [Key in keyof ObjectType & (string | number)]: ObjectType[Key] extends object diff --git a/web/types/view-props.d.ts b/web/types/view-props.d.ts index b78d710f35f..148dee59bfa 100644 --- a/web/types/view-props.d.ts +++ b/web/types/view-props.d.ts @@ -45,6 +45,7 @@ export type TIssueParams = | "labels" | "start_date" | "target_date" + | "project" | "group_by" | "sub_group_by" | "order_by" @@ -60,9 +61,10 @@ export interface IIssueFilterOptions { created_by?: string[] | null; labels?: string[] | null; priority?: string[] | null; - start_date?: TStateGroups[] | null; + project?: string[] | null; + start_date?: string[] | null; state?: string[] | null; - state_group?: TStateGroups[] | null; + state_group?: string[] | null; subscriber?: string[] | null; target_date?: string[] | null; } @@ -97,41 +99,6 @@ export interface IIssueDisplayProperties { updated_on: boolean; } -export interface IWorkspaceIssueFilterOptions { - assignees?: string[] | null; - created_by?: string[] | null; - labels?: string[] | null; - priority?: string[] | null; - state_group?: string[] | null; - subscriber?: string[] | null; - start_date?: string[] | null; - target_date?: string[] | null; - project?: string[] | null; -} - -export interface IWorkspaceGlobalViewDisplayFilterOptions { - order_by?: string | undefined; - type?: "active" | "backlog" | null; - sub_issue?: boolean; - layout?: TIssueViewOptions; -} - -export interface IWorkspaceViewIssuesParams { - assignees?: string | undefined; - created_by?: string | undefined; - labels?: string | undefined; - priority?: string | undefined; - start_date?: string | undefined; - state?: string | undefined; - state_group?: string | undefined; - subscriber?: string | undefined; - target_date?: string | undefined; - project?: string | undefined; - order_by?: string | undefined; - type?: "active" | "backlog" | undefined; - sub_issue?: boolean; -} - export interface IProjectViewProps { display_filters: IIssueDisplayFilterOptions | undefined; filters: IIssueFilterOptions; @@ -142,8 +109,3 @@ export interface IWorkspaceViewProps { display_filters: IIssueDisplayFilterOptions | undefined; display_properties: Properties; } -export interface IWorkspaceGlobalViewProps { - filters: IWorkspaceIssueFilterOptions; - display_filters: IWorkspaceIssueDisplayFilterOptions | undefined; - display_properties: Properties; -} diff --git a/web/types/workspace-views.d.ts b/web/types/workspace-views.d.ts index 0796e5caecc..754e637118e 100644 --- a/web/types/workspace-views.d.ts +++ b/web/types/workspace-views.d.ts @@ -1,4 +1,4 @@ -import { IWorkspaceGlobalViewProps } from "./view-props"; +import { IWorkspaceViewProps } from "./view-props"; export interface IWorkspaceView { id: string; @@ -10,8 +10,8 @@ export interface IWorkspaceView { updated_by: string; name: string; description: string; - query: IWorkspaceGlobalViewProps; - query_data: IWorkspaceGlobalViewProps; + query: any; + query_data: IWorkspaceViewProps; project: string; workspace: string; workspace_detail?: { @@ -20,3 +20,5 @@ export interface IWorkspaceView { slug: string; }; } + +export type TStaticViewTypes = "all-issues" | "assigned" | "created" | "subscribed";