From 8c5ce65fe64f1574eff217fd0cc7bb3904ec633a Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Tue, 2 Jan 2024 18:12:55 +0530 Subject: [PATCH] refactor: MobX store structure (#3228) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * query params from router as computed * chore: setup workspace store and sub-stores * chore: update router query store * chore: update store types * fix: pages store changes * change observables and retain object reference * fix build errors * chore: changed the structure of workspace, project, cycle, module and pages * fix: pages fixes * fix: merge conflicts resolved * chore: fixed workspace list * chore: update workspace store accroding to the new response * fix: adding page details to store * fix: adding new contexts and providers * dev: issues store and filters in new store * dev: optimised the issue fetching in issue base store * chore: project views id mapped * update lodash set to directly run inside runInaction since it mutates the object * fix: context changes * code refactor kanban for better mainatinability * optimize Kanban for performance * chore: implemented hooks for all the created stores * chore: removed bridge id * css change and refactor * chore: update cycle store structure * chore: implement the new label root store * chore: removed object structure * chore: implement project view hook * Kanban new store implementation for project issues * fix project root for kanban * feat: workspace and project members endpoint (#3092) * fix: merge conflicts resolved * issue properties optimization * chore: user stores * chore: create new store context and update hooks * chore: setup inbox store and implement router store * chore: initialize and implement project estimate store * chore: initialize global view store * kanban and list view optimization * chore: use new cycle and module store. (#3172) * chore: use new cycle and module store. * chore: minor improvements. * Revert "chore: merge develop" This reverts commit 9d2e0e29e7370b55b48fc2fee4fd126093a6cc48, reversing changes made to 9595493c42be3ea0ddd17b23a0b124555075c062. * chore: implement useGlobalView hook * refactor: projects & inbox store instances (#3179) * refactor: projects & inbox store instances * fix: formatting * fix: action usage * chore: implement useProjectState hook. (#3185) * dev: issue, cycle store optimiation * fix build for code * dev: removed dummy variables * dev: issue store * fix: adding todos * chore: removing legacy store * dev: issues store types and typos * chore: cycle module user properties * fix legacy store deletion issues * chore: change POST to PATCH * fix issues rendering for project root * chore: removed workspace details in workpsaceinvite * chore: created models for display properties * chore: setup member store and implement it everywhere * refactor: module store (#3202) * refactor: cycle store (#3192) * refator: cycle store * some more improvements. * chore: implement useLabel hook. (#3190) * refactor: inbox & project related stores. (#3193) * refactor: inbox -> filter, issues, inoxes & project -> publish, projects store * refactor: workspace-project-id name * fix kanban dropdown overlapping issue * fix kanban layout minor re rendering * chore: implement useMember store everywhere * chore: create and implement editor mention store * chore: removed the issue view user property * chore: created at id changed * dev: segway intgegration (#3132) * feat: implemented rabbitmq * dev: initialize segway with queue setup * dev: import refactors * dev: create communication with the segway server * dev: create new workers * dev: create celery node queue for consuming messages from django * dev: node to celery connection * dev: setup segway and django connection * dev: refactor the structure and add database integration to the app * dev: add external id and source added --------- Co-authored-by: NarayanBavisetti * dev: github importer (#3205) * dev: initiate github import * dev: github importer all issues import * dev: github comments and links for the imported issues * dev: update controller to use logger and spread the resultData in getAllEntities * dev: removed console log * dev: update code structure and sync functions * dev: updated retry logic when exception * dev: add imported data as well * dev: update logger and repo fetch * dev: update jira integration to new structure * dev: update migrations * dev: update the reason field * chore: workspace object id removed * chore: view's creation fixed * refactor: mobx store improvements. (#3213) * fix: state and label errors * chore: remove legacy code * fix: branch build fix (#3214) * branch build fix for release-* in case of space,backend,proxy * fixes * chore: update store names and types * fix - file size limit not work on plane.settings.production (#3160) * fix - file size limit not work on plane.settings.production * fix - file size limit not work on plane.settings.production * fix - file size limit not work on plane.settings.production, move to common.py --------- Co-authored-by: luanduongtel4vn Co-authored-by: sriram veeraghanta * style: instance admin email settings ui & ux update. (#3186) * refactor: use-user-auth hook (#3215) * refactor: use-user-auth hook * fix: user store currentUserLoader * refactor: project-view & application related stores (#3207) * refactor: project-view & application related stores * rename: projectViews -> projectViewIds * fix: project-view favourite state in store * chore: remove unnecessary hooks and contexts (#3217) * chore: update issue assignee property component * chore: bug fixes & improvement (#3218) * chore: draft issue validation added to prevent saving empty or whitespace title * chore: resolve scrolling issue in page empty state * chore: kanban layout quick add issue improvement * fix: bugs & improvements (#3189) * fix: workspace invitation modal form values reset * fix: profile sidebar avatar letter * [refactor] Editor code refactoring (#3194) * removed relative imports from editor core * Update issue widget file paths and imports to use kebab case instead of camel case, to align with coding conventions and improve consistency. * Update Tiptap core and extensions versions to 2.1.13 and Tiptap React version to 2.1.13. Update Tiptap table imports to use the new location in package @tiptap/pm/tables. Update AlertLabel component to use the new type definition for LucideIcon. * updated lock file * removed default exports from editor/core * fixed injecting css into the core package itself * seperated css code to have single source of origin wrt to the package * removed default imports from document editor * all instances using index as key while mapping fixed * Update Lite Text Editor package.json to remove @plane/editor-types as a dependency. Update Lite Text Editor index.ts to update the import of IMentionSuggestion and IMentionHighlight from @plane/editor-types to @plane/editor-core. Update Lite Text Editor ui/index.tsx to update the import of UploadImage, DeleteImage, IMentionSuggestion, and RestoreImage from @plane/editor-types to @plane/editor-core. Update Lite Text Editor ui/menus/fixed-menu/index.tsx to update the import of UploadImage from @plane/editor-types to @plane/editor-core. Update turbo.json to remove @plane/editor-types#build as a dependency for @plane/lite-text-editor#build, @plane/rich-text-editor#build, and @plane/document-editor#build. * Remove deprecated import and adjust tippy.js usage in the slash-commands.tsx file of the editor extensions package. * Update dependencies in `rich-text-editor/package.json`, remove `@plane/editor-types` and add `@plane/editor-core` in `rich-text-editor/src/index.ts`, and update imports in `rich-text-editor/src/ui/extensions/index.tsx` and `rich-text-editor/src/ui/index.tsx` to use `@plane/editor-core` instead of `@plane/editor-types`. * Update package.json dependencies and add new types for image deletion, upload, restore, mention highlight, mention suggestion, and slash command item. * Update import statements in various files to use the new package "@plane/editor-core" instead of "@plane/editor-types". * fixed document editor to follow conventions * Refactor imports in the Rich Text Editor package to use relative paths instead of absolute paths. - Updated imports in `index.ts`, `ui/index.tsx`, and `ui/menus/bubble-menu/index.tsx` to use relative paths. - Updated `tsconfig.json` to include the `baseUrl` compiler option and adjust the `include` and `exclude` paths. * Refactor Lite Text Editor code to use relative import paths instead of absolute import paths. * Added LucideIconType to the exports in index.ts for use in other files. Created a new file lucide-icon.ts which contains the type LucideIconType. Updated the icon type in HeadingOneItem in menu-items/index.tsx to use LucideIconType. Updated the Icon type in AlertLabel in alert-label.tsx to use LucideIconType. Updated the Icon type in VerticalDropdownItemProps in vertical-dropdown-menu.tsx to use LucideIconType. Updated the Icon type in BubbleMenuItem in fixed-menu/index.tsx to use LucideIconType. Deleted the file tooltip.tsx since it is no longer used. Updated the Icon type in BubbleMenuItem in bubble-menu/index.tsx to use LucideIconType. * ♻️ refactor: simplify rendering logic in slash-commands.tsx The rendering logic in the file "slash-commands.tsx" has been simplified. Previously, the code used inline positioning for the popup, but it has now been removed. Instead of appending the popup to the document body, it is now appended to the element with the ID "tiptap-container". The "flip" option has also been removed. These changes have improved the readability and maintainability of the code. * fixed build errors caused due to core's internal imports * regression: fixed pages not saving issue and not duplicating with proper content issue * build: Update @tiptap dependencies Updated the @tiptap dependencies in the package.json files of `document-editor`, `extensions`, and `rich-text-editor` packages to version 2.1.13. * 🚑 fix: Correct appendTo selector in slash-commands.tsx Update the `appendTo` function call in `slash-commands.tsx` to use the correct selector `#editor-container` instead of `#tiptap-container`. This ensures that the component is appended to the appropriate container in the editor extension. Note: The commit message assumes that the change is a fix for an issue or error. If it's not a fix, please provide more context so that an appropriate commit type can be determined. * style: email placeholder changed across the platform (#3206) * style: email placeholder changed across the platform * fix: placeholder text * dev: updated new filter endpoints and restructured issue and issue filters store * implement issues and replace useMobxStore * remove all store legacy references * dev: updated the orderby and subgroupby filters data * dev:added projectId in issue filters for consistency * fix more build errors * dev: updated profile issues * dev: removed store legacy * dev: active cycle issues in the cycle issue store * fix additional build errors and memoize issueActions in each layout component * change store enums * remove all useMobxStore references * fix more build errors * dev: reverted workspace invitation * fix: build errors and warnings * fix: optimistic update for instant operations (#3221) * fix: update functions failed case * fix: typo * chore: revert back to optimistic update approach for all `update related actions` (#3219) * fix: merge conflicts resolved * chore: update memberMap logic in components * add assignees to kanban groups and properties * dev: migration fixes * final bit of optimization on list view * change all TODOs that are to be done before this release to FIXME * change base Kanban TODOs that are to be done before this release to FIXME * dev: add fields and expand for app serializers * dev: issue detail store * dev: update issue serializer to return object ids * fix: Instance key added in settings and converted issues list api to arry instead of dict * fix: removing segway files * dev: control expand through query parameters * revert: github importer * Revert "dev: segway intgegration (#3132)" This reverts commit 1cc18a09156d1790d114061dbac8c901e0f2754c. * dev: remove migrations for segway * dev: issue structure change and created workspacebasemodel * dev: issue detail serializer * fix: changed workspace dict * dev: updated new issue structure * chore: build fix * dev: issue detail store refactor * dev: created list endpoint for issue-relation * dev: added issue attachments in issue detail store * dev: added issue activity computed * fix: build error * chore: peek overview modal context added * chore: build error fix * dev: added sub_issues in issue details store * dev: added complete issue serializer for sub issues * dev: resolved type errors in issue root store * dev: changed the issue relation structure * chore: new global dropdowns * chore: build error fix * chore: cycle and module selection if disabled * dev: removed unnecessary code from the workspace root * chore: build error fix * chore: issue relation remove endpoint * fix: build error * dev: typos and implemented issue relation store * fix: yarn lock updated * style: update the UI of all the dropdowns * fix: state store fixes * fix: key issue * fix: state store console logs removed * refactor: member dropdowns * fix: moving types to packages * fix: dropdown arrow positioning * dev: removed logs * style: label dropdown * chore: restrict description notifications * chore: description changes * chore: update spreadsheet layout dropdowns * fix: build errors * chore: duplicate key change * fix: ui bugs * chore: relation activity change * chore: comment activity changes * chore: blocking issue removal * chore: added project_id for relation * chore: issue relation store and component * chore: issue redirection issue in the issue realtion in detail page * chore: created activity changed * chore: issue links new store implementation on the issue detail * chore: issue relation deletion acitivity changed * chore: issue attachments new store implementation on the issue detail * chore: workspace level issues * fix: build errors --------- Co-authored-by: rahulramesha Co-authored-by: gurusainath Co-authored-by: sriram veeraghanta Co-authored-by: NarayanBavisetti Co-authored-by: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com> Co-authored-by: Prateek Shourya Co-authored-by: Lakhan Baheti <94619783+1akhanBaheti@users.noreply.github.com> Co-authored-by: Nikhil <118773738+pablohashescobar@users.noreply.github.com> Co-authored-by: Manish Gupta <59428681+mguptahub@users.noreply.github.com> Co-authored-by: Hoang Luan Co-authored-by: luanduongtel4vn Co-authored-by: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Co-authored-by: M. Palanikannan <73993394+Palanikannan1437@users.noreply.github.com> Co-authored-by: pablohashescobar Co-authored-by: Anmol Singh Bhatia --- .github/workflows/create-sync-pr.yml | 15 +- apiserver/plane/api/serializers/base.py | 2 +- apiserver/plane/app/serializers/__init__.py | 6 + apiserver/plane/app/serializers/base.py | 96 +- apiserver/plane/app/serializers/cycle.py | 14 +- apiserver/plane/app/serializers/inbox.py | 1 - apiserver/plane/app/serializers/issue.py | 92 +- apiserver/plane/app/serializers/module.py | 16 +- apiserver/plane/app/serializers/project.py | 5 + apiserver/plane/app/serializers/view.py | 6 +- apiserver/plane/app/serializers/workspace.py | 19 +- apiserver/plane/app/urls/cycle.py | 8 +- apiserver/plane/app/urls/inbox.py | 2 +- apiserver/plane/app/urls/issue.py | 7 +- apiserver/plane/app/urls/module.py | 8 +- apiserver/plane/app/urls/views.py | 2 +- apiserver/plane/app/urls/workspace.py | 12 + apiserver/plane/app/views/__init__.py | 4 + apiserver/plane/app/views/base.py | 29 + apiserver/plane/app/views/cycle.py | 123 +- apiserver/plane/app/views/inbox.py | 18 +- apiserver/plane/app/views/issue.py | 163 +- apiserver/plane/app/views/module.py | 75 +- apiserver/plane/app/views/page.py | 11 +- apiserver/plane/app/views/project.py | 54 +- apiserver/plane/app/views/view.py | 30 +- apiserver/plane/app/views/workspace.py | 91 +- .../plane/bgtasks/issue_activites_task.py | 126 +- apiserver/plane/bgtasks/notification_task.py | 5 +- ...emove_issueproperty_properties_and_more.py | 136 ++ .../db/migrations/0052_auto_20231220_1141.py | 65 + apiserver/plane/db/models/__init__.py | 6 +- apiserver/plane/db/models/cycle.py | 66 + apiserver/plane/db/models/issue.py | 46 +- apiserver/plane/db/models/module.py | 66 + apiserver/plane/db/models/view.py | 50 +- apiserver/plane/db/models/workspace.py | 86 + apiserver/requirements/base.txt | 2 +- deploy/selfhost/install.sh | 2 +- package.json | 3 +- packages/types/package.json | 7 + {web/types => packages/types/src}/ai.d.ts | 2 +- .../types/src}/analytics.d.ts | 0 .../types/src}/api_token.d.ts | 0 {web/types => packages/types/src}/app.d.ts | 4 +- {web/types => packages/types/src}/auth.d.ts | 0 .../types/src/calendar.d.ts | 0 {web/types => packages/types/src}/cycles.d.ts | 6 +- .../types/src}/estimate.d.ts | 14 +- .../types/src}/importer/github-importer.d.ts | 0 .../types/src/importer/index.d.ts | 4 +- .../types/src}/importer/jira-importer.d.ts | 0 {web/types => packages/types/src}/inbox.d.ts | 4 +- {web/types => packages/types/src}/index.d.ts | 5 + .../types/src}/instance.d.ts | 0 .../types/src}/integration.d.ts | 0 {web/types => packages/types/src}/issues.d.ts | 98 +- packages/types/src/issues/base.d.ts | 23 + packages/types/src/issues/issue.d.ts | 36 + packages/types/src/issues/issue_activity.d.ts | 41 + .../types/src/issues/issue_attachment.d.ts | 23 + .../src/issues/issue_comment_reaction.d.ts | 20 + packages/types/src/issues/issue_link.d.ts | 20 + packages/types/src/issues/issue_reaction.d.ts | 21 + packages/types/src/issues/issue_relation.d.ts | 20 + .../types/src/issues/issue_sub_issues.d.ts | 22 + .../types/src/issues/issue_subscription.d.ts | 0 .../types => packages/types/src}/modules.d.ts | 8 +- .../types/src}/notifications.d.ts | 0 {web/types => packages/types/src}/pages.d.ts | 18 +- .../types/src}/projects.d.ts | 37 +- .../types/src}/reaction.d.ts | 0 {web/types => packages/types/src}/state.d.ts | 2 +- {web/types => packages/types/src}/users.d.ts | 6 +- .../types/src}/view-props.d.ts | 12 + {web/types => packages/types/src}/views.d.ts | 5 +- .../types/src}/waitlist.d.ts | 0 .../types => packages/types/src}/webhook.d.ts | 0 .../types/src}/workspace-views.d.ts | 10 +- .../types/src}/workspace.d.ts | 32 +- packages/ui/src/icons/priority-icon.tsx | 41 +- packages/ui/src/icons/type.d.ts | 8 - .../account/deactivate-account-modal.tsx | 8 +- .../account/sign-in-forms/email-form.tsx | 2 +- .../account/sign-in-forms/o-auth-options.tsx | 7 +- .../account/sign-in-forms/password.tsx | 2 +- web/components/account/sign-in-forms/root.tsx | 7 +- .../sign-in-forms/self-hosted-sign-in.tsx | 2 +- .../sign-in-forms/set-password-link.tsx | 2 +- .../account/sign-in-forms/unique-code.tsx | 6 +- .../custom-analytics/custom-analytics.tsx | 2 +- .../custom-analytics/graph/custom-tooltip.tsx | 6 +- .../custom-analytics/graph/index.tsx | 6 +- .../custom-analytics/main-content.tsx | 2 +- .../analytics/custom-analytics/select-bar.tsx | 21 +- .../custom-analytics/select/project.tsx | 44 +- .../custom-analytics/select/segment.tsx | 2 +- .../custom-analytics/select/x-axis.tsx | 2 +- .../custom-analytics/select/y-axis.tsx | 2 +- .../sidebar/projects-list.tsx | 95 +- .../sidebar/sidebar-header.tsx | 19 +- .../custom-analytics/sidebar/sidebar.tsx | 321 ++-- .../analytics/custom-analytics/table.tsx | 2 +- .../analytics/project-modal/main-content.tsx | 2 +- .../analytics/project-modal/modal.tsx | 2 +- .../analytics/scope-and-demand/demand.tsx | 2 +- .../analytics/scope-and-demand/scope.tsx | 2 +- .../scope-and-demand/year-wise-issues.tsx | 2 +- .../api-token/delete-token-modal.tsx | 2 +- .../api-token/modal/create-token-modal.tsx | 2 +- web/components/api-token/modal/form.tsx | 10 +- .../modal/generated-token-details.tsx | 2 +- web/components/api-token/token-list-item.tsx | 2 +- .../auth-screens/not-authorized-view.tsx | 17 +- .../auth-screens/project/join-project.tsx | 22 +- .../automation/auto-archive-automation.tsx | 37 +- .../automation/auto-close-automation.tsx | 50 +- .../automation/select-month-modal.tsx | 2 +- .../command-palette/actions/help-actions.tsx | 6 +- .../actions/issue-actions/actions-list.tsx | 25 +- .../actions/issue-actions/change-assignee.tsx | 63 +- .../actions/issue-actions/change-priority.tsx | 24 +- .../actions/issue-actions/change-state.tsx | 26 +- .../actions/project-actions.tsx | 8 +- .../actions/search-results.tsx | 2 +- .../command-palette/actions/theme-actions.tsx | 6 +- .../command-palette/command-modal.tsx | 16 +- ...mmand-pallette.tsx => command-palette.tsx} | 19 +- web/components/command-palette/helpers.tsx | 2 +- web/components/command-palette/index.ts | 2 +- web/components/common/new-empty-state.tsx | 2 +- web/components/core/activity.tsx | 30 +- web/components/core/image-picker-popover.tsx | 21 +- .../modals/bulk-delete-issues-modal-item.tsx | 38 + .../core/modals/bulk-delete-issues-modal.tsx | 69 +- .../modals/existing-issues-list-modal.tsx | 2 +- web/components/core/modals/link-modal.tsx | 2 +- .../core/modals/user-image-upload-modal.tsx | 12 +- .../modals/workspace-image-upload-modal.tsx | 10 +- web/components/core/sidebar/links-list.tsx | 28 +- .../core/sidebar/progress-chart.tsx | 2 +- .../core/sidebar/sidebar-progress-stats.tsx | 44 +- .../core/theme/color-picker-input.tsx | 2 +- .../core/theme/custom-theme-selector.tsx | 12 +- .../cycles/active-cycle-details.tsx | 158 +- web/components/cycles/active-cycle-stats.tsx | 2 +- web/components/cycles/cycle-peek-overview.tsx | 19 +- web/components/cycles/cycles-board-card.tsx | 94 +- web/components/cycles/cycles-board.tsx | 20 +- web/components/cycles/cycles-list-item.tsx | 111 +- web/components/cycles/cycles-list.tsx | 24 +- web/components/cycles/cycles-view.tsx | 41 +- web/components/cycles/delete-modal.tsx | 75 +- web/components/cycles/form.tsx | 77 +- web/components/cycles/gantt-chart/blocks.tsx | 2 +- .../cycles/gantt-chart/cycles-list-layout.tsx | 65 +- web/components/cycles/modal.tsx | 32 +- web/components/cycles/sidebar.tsx | 69 +- .../cycles/transfer-issues-modal.tsx | 91 +- web/components/dropdowns/cycle.tsx | 293 ++++ web/components/dropdowns/date.tsx | 243 +++ web/components/dropdowns/estimate.tsx | 287 ++++ web/components/dropdowns/index.ts | 8 + web/components/dropdowns/member/buttons.tsx | 113 ++ web/components/dropdowns/member/index.ts | 3 + .../dropdowns/member/project-member.tsx | 224 +++ web/components/dropdowns/member/types.d.ts | 25 + .../dropdowns/member/workspace-member.tsx | 209 +++ web/components/dropdowns/module.tsx | 293 ++++ web/components/dropdowns/priority.tsx | 398 +++++ web/components/dropdowns/project.tsx | 273 +++ web/components/dropdowns/state.tsx | 271 +++ web/components/dropdowns/types.d.ts | 7 + .../create-update-estimate-modal.tsx | 22 +- .../estimates/delete-estimate-modal.tsx | 27 +- .../estimates/estimate-list-item.tsx | 13 +- web/components/estimates/estimate-select.tsx | 160 -- web/components/estimates/estimates-list.tsx | 37 +- web/components/estimates/index.ts | 1 - web/components/exporter/export-modal.tsx | 54 +- web/components/exporter/guide.tsx | 17 +- web/components/exporter/single-export.tsx | 14 +- .../gantt-chart/helpers/block-structure.tsx | 4 +- .../gantt-chart/sidebar/sidebar.tsx | 6 +- web/components/headers/cycle-issues.tsx | 117 +- web/components/headers/cycles.tsx | 25 +- web/components/headers/global-issues.tsx | 71 +- web/components/headers/module-issues.tsx | 114 +- web/components/headers/modules-list.tsx | 18 +- web/components/headers/page-details.tsx | 19 +- web/components/headers/pages.tsx | 18 +- .../project-archived-issue-details.tsx | 17 +- .../headers/project-archived-issues.tsx | 64 +- .../headers/project-draft-issues.tsx | 40 +- web/components/headers/project-inbox.tsx | 12 +- .../headers/project-issue-details.tsx | 17 +- web/components/headers/project-issues.tsx | 74 +- web/components/headers/project-settings.tsx | 19 +- .../headers/project-view-issues.tsx | 101 +- web/components/headers/project-views.tsx | 23 +- web/components/headers/projects.tsx | 30 +- .../icons/module/module-status-icon.tsx | 2 +- web/components/icons/priority-icon.tsx | 14 +- .../icons/state/state-group-icon.tsx | 2 +- web/components/inbox/actions-header.tsx | 70 +- web/components/inbox/filters-dropdown.tsx | 26 +- web/components/inbox/filters-list.tsx | 30 +- web/components/inbox/issue-activity.tsx | 39 +- web/components/inbox/issue-card.tsx | 28 +- web/components/inbox/issues-list-sidebar.tsx | 12 +- web/components/inbox/main-content.tsx | 142 +- .../inbox/modals/accept-issue-modal.tsx | 7 +- .../inbox/modals/create-issue-modal.tsx | 78 +- .../inbox/modals/decline-issue-modal.tsx | 7 +- .../inbox/modals/delete-issue-modal.tsx | 30 +- .../inbox/modals/select-duplicate.tsx | 13 +- web/components/instance/ai-form.tsx | 10 +- web/components/instance/email-form.tsx | 9 +- web/components/instance/general-form.tsx | 8 +- .../instance/github-config-form.tsx | 12 +- .../instance/google-config-form.tsx | 12 +- web/components/instance/help-section.tsx | 8 +- web/components/instance/image-config-form.tsx | 12 +- web/components/instance/setup-done-view.tsx | 7 +- .../instance/setup-form/sign-in-form.tsx | 12 +- web/components/instance/setup-view.tsx | 8 +- web/components/instance/sidebar-dropdown.tsx | 13 +- web/components/instance/sidebar-menu.tsx | 8 +- .../integration/delete-import-modal.tsx | 4 +- web/components/integration/github/auth.tsx | 11 +- .../integration/github/import-configure.tsx | 2 +- .../integration/github/import-data.tsx | 38 +- web/components/integration/github/root.tsx | 8 +- .../integration/github/select-repository.tsx | 2 +- .../integration/github/single-user-select.tsx | 2 +- web/components/integration/guide.tsx | 23 +- .../integration/jira/confirm-import.tsx | 2 +- .../integration/jira/give-details.tsx | 43 +- .../integration/jira/import-users.tsx | 2 +- web/components/integration/jira/index.ts | 2 +- .../integration/jira/jira-project-detail.tsx | 2 +- web/components/integration/jira/root.tsx | 10 +- web/components/integration/single-import.tsx | 10 +- .../integration/single-integration-card.tsx | 29 +- .../integration/slack/select-channel.tsx | 17 +- web/components/issues/activity.tsx | 2 +- .../issues/attachment/attachment-detail.tsx | 88 + .../issues/attachment/attachment-upload.tsx | 61 +- .../issues/attachment/attachments-list.tsx | 32 + .../issues/attachment/attachments.tsx | 110 -- ... delete-attachment-confirmation-modal.tsx} | 68 +- web/components/issues/attachment/index.ts | 8 +- web/components/issues/attachment/root.tsx | 77 + web/components/issues/comment/add-comment.tsx | 20 +- .../issues/comment/comment-card.tsx | 41 +- .../issues/comment/comment-reaction.tsx | 28 +- .../issues/delete-archived-issue-modal.tsx | 18 +- .../issues/delete-draft-issue-modal.tsx | 31 +- web/components/issues/delete-issue-modal.tsx | 19 +- web/components/issues/description-form.tsx | 28 +- web/components/issues/draft-issue-form.tsx | 254 +-- web/components/issues/draft-issue-modal.tsx | 111 +- web/components/issues/form.tsx | 317 ++-- web/components/issues/index.ts | 3 + .../calendar/base-calendar-root.tsx | 60 +- .../issue-layouts/calendar/calendar.tsx | 62 +- .../issue-layouts/calendar/day-tile.tsx | 29 +- .../calendar/dropdowns/months-dropdown.tsx | 25 +- .../calendar/dropdowns/options-dropdown.tsx | 53 +- .../issues/issue-layouts/calendar/header.tsx | 40 +- .../issue-layouts/calendar/issue-blocks.tsx | 48 +- .../calendar/quick-add-issue-form.tsx | 45 +- .../calendar/roots/cycle-root.tsx | 74 +- .../calendar/roots/module-root.tsx | 70 +- .../calendar/roots/project-root.tsx | 59 +- .../calendar/roots/project-view-root.tsx | 59 +- .../issues/issue-layouts/calendar/utils.ts | 42 + .../issue-layouts/calendar/week-days.tsx | 29 +- .../issue-layouts/empty-states/cycle.tsx | 29 +- .../empty-states/global-view.tsx | 29 +- .../issue-layouts/empty-states/module.tsx | 54 +- .../empty-states/project-view.tsx | 13 +- .../issue-layouts/empty-states/project.tsx | 39 +- .../filters/applied-filters/filters-list.tsx | 24 +- .../filters/applied-filters/label.tsx | 2 +- .../filters/applied-filters/members.tsx | 11 +- .../filters/applied-filters/priority.tsx | 2 +- .../filters/applied-filters/project.tsx | 14 +- .../applied-filters/roots/archived-issue.tsx | 34 +- .../applied-filters/roots/cycle-root.tsx | 56 +- .../applied-filters/roots/draft-issue.tsx | 34 +- .../roots/global-view-root.tsx | 54 +- .../applied-filters/roots/module-root.tsx | 55 +- .../roots/profile-issues-root.tsx | 45 +- .../applied-filters/roots/project-root.tsx | 42 +- .../roots/project-view-root.tsx | 60 +- .../filters/applied-filters/state-group.tsx | 2 +- .../filters/applied-filters/state.tsx | 2 +- .../display-filters-selection.tsx | 2 +- .../display-filters/display-properties.tsx | 2 +- .../header/display-filters/extra-options.tsx | 2 +- .../header/display-filters/group-by.tsx | 2 +- .../header/display-filters/issue-type.tsx | 2 +- .../header/display-filters/order-by.tsx | 2 +- .../header/display-filters/sub-group-by.tsx | 2 +- .../filters/header/filters/assignee.tsx | 46 +- .../filters/header/filters/created-by.tsx | 49 +- .../header/filters/filters-selection.tsx | 18 +- .../filters/header/filters/labels.tsx | 2 +- .../filters/header/filters/mentions.tsx | 46 +- .../filters/header/filters/project.tsx | 15 +- .../filters/header/filters/state.tsx | 2 +- .../filters/header/layout-selection.tsx | 2 +- .../issue-layouts/gantt/base-gantt-root.tsx | 69 +- .../issues/issue-layouts/gantt/blocks.tsx | 43 +- .../issues/issue-layouts/gantt/cycle-root.tsx | 50 +- .../issue-layouts/gantt/module-root.tsx | 50 +- .../issue-layouts/gantt/project-root.tsx | 27 +- .../issue-layouts/gantt/project-view-root.tsx | 30 +- .../gantt/quick-add-issue-form.tsx | 43 +- .../issue-layouts/kanban/base-kanban-root.tsx | 319 ++-- .../issues/issue-layouts/kanban/block.tsx | 213 +-- .../issue-layouts/kanban/blocks-list.tsx | 74 +- .../issues/issue-layouts/kanban/default.tsx | 475 ++---- .../issue-layouts/kanban/headers/assignee.tsx | 74 - .../kanban/headers/created_by.tsx | 71 - .../kanban/headers/group-by-card.tsx | 25 +- .../kanban/headers/group-by-root.tsx | 149 -- .../issue-layouts/kanban/headers/label.tsx | 74 - .../issue-layouts/kanban/headers/priority.tsx | 73 - .../issue-layouts/kanban/headers/project.tsx | 74 - .../kanban/headers/state-group.tsx | 77 - .../issue-layouts/kanban/headers/state.tsx | 71 - .../kanban/headers/sub-group-by-root.tsx | 134 -- .../issue-layouts/kanban/kanban-group.tsx | 106 ++ .../issue-layouts/kanban/properties.tsx | 197 --- .../kanban/quick-add-issue-form.tsx | 40 +- .../issue-layouts/kanban/roots/cycle-root.tsx | 105 +- .../kanban/roots/draft-issue-root.tsx | 43 +- .../kanban/roots/module-root.tsx | 106 +- .../kanban/roots/profile-issues-root.tsx | 60 +- .../kanban/roots/project-root.tsx | 70 +- .../kanban/roots/project-view-root.tsx | 78 +- .../issues/issue-layouts/kanban/swimlanes.tsx | 558 ++---- .../issues/issue-layouts/kanban/utils.ts | 167 ++ .../issue-layouts/list/base-list-root.tsx | 232 ++- .../issues/issue-layouts/list/block.tsx | 69 +- .../issues/issue-layouts/list/blocks-list.tsx | 44 +- .../issues/issue-layouts/list/default.tsx | 309 +--- .../issue-layouts/list/headers/assignee.tsx | 41 - .../issue-layouts/list/headers/created-by.tsx | 38 - .../list/headers/empty-group.tsx | 29 - .../list/headers/group-by-card.tsx | 31 +- .../list/headers/group-by-root.tsx | 114 -- .../issue-layouts/list/headers/label.tsx | 41 - .../issue-layouts/list/headers/priority.tsx | 64 - .../issue-layouts/list/headers/project.tsx | 41 - .../list/headers/state-group.tsx | 47 - .../issue-layouts/list/headers/state.tsx | 38 - .../issue-layouts/list/list-view-types.d.ts | 4 +- .../issues/issue-layouts/list/properties.tsx | 168 -- .../list/quick-add-issue-form.tsx | 44 +- .../list/roots/archived-issue-root.tsx | 40 +- .../issue-layouts/list/roots/cycle-root.tsx | 71 +- .../list/roots/draft-issue-root.tsx | 49 +- .../issue-layouts/list/roots/module-root.tsx | 70 +- .../list/roots/profile-issues-root.tsx | 68 +- .../issue-layouts/list/roots/project-root.tsx | 50 +- .../list/roots/project-view-root.tsx | 47 +- .../properties/all-properties.tsx | 207 +++ .../issue-layouts/properties/assignee.tsx | 204 --- .../issues/issue-layouts/properties/date.tsx | 124 -- .../issue-layouts/properties/estimates.tsx | 177 -- .../issues/issue-layouts/properties/index.ts | 1 + .../issues/issue-layouts/properties/index.tsx | 6 - .../issue-layouts/properties/labels.tsx | 35 +- .../issue-layouts/properties/priority.tsx | 25 - .../issues/issue-layouts/properties/state.tsx | 189 --- .../with-display-properties-HOC.tsx | 22 + .../quick-action-dropdowns/all-issue.tsx | 15 +- .../quick-action-dropdowns/archived-issue.tsx | 7 +- .../quick-action-dropdowns/cycle-issue.tsx | 15 +- .../quick-action-dropdowns/module-issue.tsx | 16 +- .../quick-action-dropdowns/project-issue.tsx | 33 +- .../roots/all-issue-layout-root.tsx | 87 +- .../roots/archived-issue-layout-root.tsx | 15 +- .../issue-layouts/roots/cycle-layout-root.tsx | 43 +- .../roots/draft-issue-layout-root.tsx | 15 +- .../roots/module-layout-root.tsx | 34 +- .../roots/project-layout-root.tsx | 35 +- .../roots/project-view-layout-root.tsx | 20 +- .../spreadsheet/base-spreadsheet-root.tsx | 82 +- .../spreadsheet/columns/assignee-column.tsx | 75 +- .../spreadsheet/columns/attachment-column.tsx | 23 +- .../spreadsheet/columns/columns-list.tsx | 18 +- .../spreadsheet/columns/created-on-column.tsx | 28 +- .../spreadsheet/columns/due-date-column.tsx | 69 +- .../spreadsheet/columns/estimate-column.tsx | 68 +- .../columns/issue/issue-column.tsx | 33 +- .../issue/spreadsheet-issue-column.tsx | 46 +- .../spreadsheet/columns/label-column.tsx | 63 +- .../spreadsheet/columns/link-column.tsx | 23 +- .../spreadsheet/columns/priority-column.tsx | 71 +- .../spreadsheet/columns/start-date-column.tsx | 70 +- .../spreadsheet/columns/state-column.tsx | 72 +- .../spreadsheet/columns/sub-issue-column.tsx | 23 +- .../spreadsheet/columns/updated-on-column.tsx | 29 +- .../spreadsheet/quick-add-issue-form.tsx | 55 +- .../spreadsheet/roots/cycle-root.tsx | 53 +- .../spreadsheet/roots/module-root.tsx | 54 +- .../spreadsheet/roots/project-root.tsx | 40 +- .../spreadsheet/roots/project-view-root.tsx | 38 +- .../spreadsheet/spreadsheet-column.tsx | 49 +- .../spreadsheet/spreadsheet-view.tsx | 19 +- web/components/issues/issue-layouts/utils.tsx | 158 ++ .../issue-links/create-update-link-modal.tsx | 167 ++ web/components/issues/issue-links/index.ts | 1 + .../issues/issue-links/link-detail.tsx | 109 ++ web/components/issues/issue-links/links.tsx | 39 + web/components/issues/issue-links/root.tsx | 128 ++ web/components/issues/issue-reaction.tsx | 8 +- web/components/issues/issue-update-status.tsx | 10 +- web/components/issues/main-content.tsx | 104 +- web/components/issues/modal.tsx | 148 +- .../issues/parent-issues-list-modal.tsx | 2 +- .../issues/peek-overview/activity/card.tsx | 10 +- .../peek-overview/activity/comment-card.tsx | 23 +- .../peek-overview/activity/comment-editor.tsx | 20 +- .../activity/comment-reaction.tsx | 12 +- .../issues/peek-overview/activity/view.tsx | 4 +- .../issues/peek-overview/issue-detail.tsx | 53 +- .../issues/peek-overview/properties.tsx | 167 +- .../issues/peek-overview/reactions/root.tsx | 2 +- web/components/issues/peek-overview/root.tsx | 129 +- web/components/issues/peek-overview/view.tsx | 65 +- web/components/issues/select/assignee.tsx | 71 - web/components/issues/select/cycle.tsx | 153 -- web/components/issues/select/date.tsx | 88 - web/components/issues/select/estimate.tsx | 56 - web/components/issues/select/index.ts | 8 - web/components/issues/select/label.tsx | 79 +- web/components/issues/select/module.tsx | 147 -- web/components/issues/select/priority.tsx | 40 - web/components/issues/select/project.tsx | 133 -- web/components/issues/select/state.tsx | 81 - .../issues/sidebar-select/assignee.tsx | 79 - .../issues/sidebar-select/blocked.tsx | 161 -- .../issues/sidebar-select/blocker.tsx | 171 -- .../issues/sidebar-select/cycle.tsx | 36 +- .../issues/sidebar-select/duplicate.tsx | 167 -- .../issues/sidebar-select/estimate.tsx | 59 - web/components/issues/sidebar-select/index.ts | 9 +- .../issues/sidebar-select/label.tsx | 25 +- .../issues/sidebar-select/module.tsx | 86 +- .../issues/sidebar-select/parent.tsx | 63 +- .../issues/sidebar-select/priority.tsx | 56 - .../issues/sidebar-select/relates-to.tsx | 168 -- .../issues/sidebar-select/relation.tsx | 167 ++ .../issues/sidebar-select/state.tsx | 75 - web/components/issues/sidebar.tsx | 322 ++-- web/components/issues/sub-issues/issue.tsx | 71 +- .../issues/sub-issues/issues-list.tsx | 63 +- .../issues/sub-issues/properties.tsx | 60 +- web/components/issues/sub-issues/root.tsx | 63 +- .../issues/view-select/due-date.tsx | 4 +- .../issues/view-select/estimate.tsx | 32 +- .../issues/view-select/start-date.tsx | 4 +- web/components/labels/create-label-modal.tsx | 28 +- .../labels/create-update-label-inline.tsx | 30 +- web/components/labels/delete-label-modal.tsx | 20 +- web/components/labels/index.ts | 1 - .../labels/label-block/label-item-block.tsx | 13 +- web/components/labels/label-select.tsx | 190 --- web/components/labels/labels-list-modal.tsx | 25 +- .../labels/project-setting-label-group.tsx | 4 +- .../labels/project-setting-label-item.tsx | 28 +- .../labels/project-setting-label-list.tsx | 190 ++- .../modules/delete-module-modal.tsx | 23 +- web/components/modules/form.tsx | 109 +- web/components/modules/gantt-chart/blocks.tsx | 2 +- .../gantt-chart/modules-list-layout.tsx | 40 +- web/components/modules/modal.tsx | 47 +- web/components/modules/module-card-item.tsx | 112 +- web/components/modules/module-list-item.tsx | 91 +- .../modules/module-peek-overview.tsx | 14 +- web/components/modules/modules-list-view.tsx | 46 +- web/components/modules/select/index.ts | 2 - web/components/modules/select/lead.tsx | 78 - web/components/modules/select/members.tsx | 72 - web/components/modules/select/status.tsx | 2 +- .../modules/sidebar-select/index.ts | 2 - .../modules/sidebar-select/select-lead.tsx | 92 - .../modules/sidebar-select/select-members.tsx | 84 - .../modules/sidebar-select/select-status.tsx | 2 +- web/components/modules/sidebar.tsx | 182 +- .../notifications/notification-card.tsx | 6 +- .../notifications/notification-header.tsx | 6 +- .../notifications/notification-popover.tsx | 6 +- .../select-snooze-till-modal.tsx | 2 +- web/components/onboarding/invitations.tsx | 42 +- web/components/onboarding/invite-members.tsx | 6 +- web/components/onboarding/join-workspaces.tsx | 18 +- .../onboarding/onboarding-sidebar.tsx | 25 +- .../switch-delete-account-modal.tsx | 12 +- web/components/onboarding/tour/root.tsx | 18 +- web/components/onboarding/user-details.tsx | 20 +- web/components/onboarding/workspace.tsx | 27 +- web/components/page-views/signin.tsx | 11 +- .../page-views/workspace-dashboard.tsx | 69 +- web/components/pages/create-block.tsx | 117 -- .../pages/create-update-page-modal.tsx | 25 +- web/components/pages/delete-page-modal.tsx | 20 +- web/components/pages/index.ts | 1 - web/components/pages/page-form.tsx | 2 +- .../pages/pages-list/all-pages-list.tsx | 12 +- .../pages/pages-list/archived-pages-list.tsx | 10 +- .../pages/pages-list/favorite-pages-list.tsx | 10 +- web/components/pages/pages-list/list-item.tsx | 108 +- web/components/pages/pages-list/list-view.tsx | 47 +- .../pages/pages-list/private-page-list.tsx | 12 +- .../pages/pages-list/recent-pages-list.tsx | 34 +- .../pages/pages-list/shared-pages-list.tsx | 10 +- web/components/pages/pages-list/types.ts | 2 +- .../overview/priority-distribution.tsx | 2 +- .../profile/overview/state-distribution.tsx | 2 +- web/components/profile/overview/stats.tsx | 2 +- web/components/profile/overview/workload.tsx | 6 +- .../profile/profile-issues-filter.tsx | 78 +- web/components/profile/profile-issues.tsx | 14 +- web/components/profile/sidebar.tsx | 19 +- web/components/project/card-list.tsx | 64 +- web/components/project/card.tsx | 27 +- .../project/confirm-project-member-remove.tsx | 18 +- .../project/create-project-modal.tsx | 89 +- .../project/delete-project-modal.tsx | 27 +- web/components/project/form.tsx | 44 +- web/components/project/index.ts | 2 - web/components/project/integration-card.tsx | 5 +- web/components/project/join-project-modal.tsx | 17 +- .../project/leave-project-modal.tsx | 19 +- web/components/project/member-list-item.tsx | 98 +- web/components/project/member-list.tsx | 56 +- web/components/project/member-select.tsx | 49 +- web/components/project/members-select.tsx | 179 -- web/components/project/priority-select.tsx | 154 -- .../project-settings-member-defaults.tsx | 43 +- .../project/publish-project/modal.tsx | 103 +- .../project/send-project-invitation-modal.tsx | 103 +- .../settings/delete-project-section.tsx | 2 +- .../project/settings/features-list.tsx | 30 +- web/components/project/sidebar-list-item.tsx | 51 +- web/components/project/sidebar-list.tsx | 65 +- web/components/states/create-state-modal.tsx | 20 +- .../states/create-update-state-inline.tsx | 46 +- web/components/states/delete-state-modal.tsx | 27 +- web/components/states/index.ts | 1 - .../project-setting-state-list-item.tsx | 24 +- .../states/project-setting-state-list.tsx | 19 +- web/components/states/state-select.tsx | 151 -- web/components/ui/date.tsx | 68 - web/components/ui/index.ts | 2 - web/components/ui/labels-list.tsx | 2 +- web/components/ui/product-updates-modal.tsx | 109 -- web/components/user/user-greetings.tsx | 2 +- web/components/views/delete-view-modal.tsx | 22 +- web/components/views/form.tsx | 34 +- web/components/views/modal.tsx | 30 +- web/components/views/view-list-item.tsx | 27 +- web/components/views/views-list.tsx | 51 +- .../web-hooks/create-webhook-modal.tsx | 12 +- .../web-hooks/delete-webhook-modal.tsx | 9 +- web/components/web-hooks/form/event-types.tsx | 2 +- web/components/web-hooks/form/form.tsx | 22 +- .../form/individual-event-options.tsx | 2 +- web/components/web-hooks/form/secret-key.tsx | 19 +- web/components/web-hooks/form/toggle.tsx | 2 +- .../web-hooks/generated-hook-details.tsx | 2 +- web/components/web-hooks/utils.ts | 2 +- .../web-hooks/webhooks-list-item.tsx | 12 +- web/components/web-hooks/webhooks-list.tsx | 9 +- web/components/workspace/activity-graph.tsx | 2 +- .../confirm-workspace-member-remove.tsx | 37 +- .../workspace/create-workspace-form.tsx | 24 +- .../workspace/delete-workspace-modal.tsx | 22 +- web/components/workspace/help-section.tsx | 7 +- web/components/workspace/index.ts | 2 - web/components/workspace/issues-list.tsx | 2 +- web/components/workspace/issues-pie-chart.tsx | 2 +- web/components/workspace/issues-stats.tsx | 4 +- web/components/workspace/member-select.tsx | 148 -- .../send-workspace-invitation-modal.tsx | 16 +- web/components/workspace/settings/index.ts | 1 + .../settings/invitations-list-item.tsx | 155 ++ .../workspace/settings/members-list-item.tsx | 172 +- .../workspace/settings/members-list.tsx | 43 +- .../workspace/settings/workspace-details.tsx | 20 +- web/components/workspace/sidebar-dropdown.tsx | 201 ++- web/components/workspace/sidebar-menu.tsx | 17 +- .../workspace/sidebar-quick-action.tsx | 21 +- .../workspace/single-invitation.tsx | 62 - .../views/default-view-list-item.tsx | 3 +- .../workspace/views/delete-view-modal.tsx | 24 +- web/components/workspace/views/form.tsx | 31 +- web/components/workspace/views/header.tsx | 82 +- web/components/workspace/views/modal.tsx | 40 +- .../workspace/views/view-list-item.tsx | 18 +- web/components/workspace/views/views-list.tsx | 25 +- web/constants/analytics.ts | 2 +- web/constants/calendar.ts | 2 +- web/constants/common.ts | 6 + web/constants/cycle.ts | 13 +- web/constants/fetch-keys.ts | 2 +- web/constants/issue.ts | 49 +- web/constants/kanban-helpers.ts | 19 - web/constants/module.ts | 2 +- web/constants/project.ts | 11 +- web/constants/spreadsheet.ts | 2 +- web/constants/state.ts | 2 +- web/constants/workspace.ts | 2 +- web/contexts/issue-view.context.tsx | 445 ----- web/contexts/profile-issues-context.tsx | 191 --- web/contexts/store-context.tsx | 19 + web/contexts/user-notification-context.tsx | 2 +- web/contexts/user.context.tsx | 40 - web/contexts/workspace-member.context.tsx | 64 - web/helpers/analytics.helper.ts | 22 +- web/helpers/array.helper.ts | 2 +- web/helpers/event-tracker.helper.ts | 2 - web/helpers/filter.helper.ts | 2 +- web/helpers/issue.helper.ts | 46 +- web/helpers/state.helper.ts | 2 +- web/hooks/store/index.ts | 23 + web/hooks/store/use-application.ts | 11 + web/hooks/store/use-calendar-view.ts | 11 + web/hooks/store/use-cycle.ts | 11 + web/hooks/store/use-estimate.ts | 11 + web/hooks/store/use-global-view.ts | 11 + web/hooks/store/use-inbox-filters.ts | 11 + web/hooks/store/use-inbox-issues.ts | 11 + web/hooks/store/use-inbox.ts | 11 + web/hooks/store/use-issue-detail.ts | 11 + web/hooks/store/use-issues.ts | 125 ++ web/hooks/store/use-kanban-view.ts | 11 + web/hooks/store/use-label.ts | 11 + web/hooks/store/use-member.ts | 11 + web/hooks/store/use-mention.ts | 11 + web/hooks/store/use-module.ts | 11 + web/hooks/store/use-page.ts | 11 + web/hooks/store/use-project-publish.ts | 11 + web/hooks/store/use-project-state.ts | 11 + web/hooks/store/use-project-view.ts | 11 + web/hooks/store/use-project.ts | 11 + web/hooks/store/use-user.ts | 11 + web/hooks/store/use-webhook.ts | 11 + web/hooks/store/use-workspace.ts | 11 + web/hooks/use-comment-reaction.tsx | 5 +- web/hooks/use-editor-suggestions.tsx | 13 - web/hooks/use-estimate-option.tsx | 45 - .../use-issue-notification-subscription.tsx | 5 +- web/hooks/use-issue-reaction.tsx | 5 +- web/hooks/use-project-details.tsx | 32 - web/hooks/use-sign-in-redirection.ts | 12 +- web/hooks/use-sub-issue.tsx | 55 - web/hooks/use-user-auth.tsx | 44 +- web/hooks/use-user-notifications.tsx | 2 +- web/hooks/use-user.tsx | 2 +- web/layouts/admin-layout/layout.tsx | 8 +- web/layouts/admin-layout/sidebar.tsx | 12 +- web/layouts/app-layout/layout.tsx | 26 +- web/layouts/app-layout/sidebar.tsx | 8 +- web/layouts/auth-layout/admin-wrapper.tsx | 12 +- web/layouts/auth-layout/project-wrapper.tsx | 107 +- web/layouts/auth-layout/user-wrapper.tsx | 26 +- web/layouts/auth-layout/workspace-wrapper.tsx | 44 +- web/layouts/instance-layout/index.tsx | 11 +- .../settings-layout/profile/sidebar.tsx | 23 +- .../settings-layout/project/layout.tsx | 19 +- .../settings-layout/workspace/sidebar.tsx | 10 +- web/layouts/user-profile-layout/layout.tsx | 10 +- web/lib/app-provider.tsx | 15 +- web/lib/local-storage.ts | 42 +- web/lib/mobx/store-provider.tsx | 28 - web/lib/types.d.ts | 3 + web/lib/wrappers/crisp-wrapper.tsx | 2 +- web/lib/wrappers/posthog-wrapper.tsx | 4 +- web/lib/wrappers/store-wrapper.tsx | 75 +- web/package.json | 1 + web/pages/[workspaceSlug]/analytics.tsx | 20 +- web/pages/[workspaceSlug]/index.tsx | 2 +- .../profile/[userId]/assigned.tsx | 2 +- .../profile/[userId]/created.tsx | 2 +- .../profile/[userId]/index.tsx | 4 +- .../profile/[userId]/subscribed.tsx | 2 +- .../archived-issues/[archivedIssueId].tsx | 28 +- .../[projectId]/archived-issues/index.tsx | 11 +- .../projects/[projectId]/cycles/[cycleId].tsx | 18 +- .../projects/[projectId]/cycles/index.tsx | 117 +- .../[projectId]/draft-issues/index.tsx | 19 +- .../projects/[projectId]/inbox/[inboxId].tsx | 8 +- .../projects/[projectId]/issues/[issueId].tsx | 38 +- .../projects/[projectId]/issues/index.tsx | 2 +- .../[projectId]/modules/[moduleId].tsx | 11 +- .../projects/[projectId]/modules/index.tsx | 2 +- .../projects/[projectId]/pages/[pageId].tsx | 75 +- .../projects/[projectId]/pages/index.tsx | 16 +- .../[projectId]/settings/automations.tsx | 25 +- .../[projectId]/settings/estimates.tsx | 16 +- .../[projectId]/settings/features.tsx | 10 +- .../projects/[projectId]/settings/index.tsx | 18 +- .../[projectId]/settings/integrations.tsx | 4 +- .../projects/[projectId]/settings/labels.tsx | 2 +- .../projects/[projectId]/settings/members.tsx | 2 +- .../projects/[projectId]/settings/states.tsx | 2 +- .../projects/[projectId]/views/[viewId].tsx | 13 +- .../projects/[projectId]/views/index.tsx | 22 +- web/pages/[workspaceSlug]/projects/index.tsx | 11 +- .../[workspaceSlug]/settings/api-tokens.tsx | 12 +- .../[workspaceSlug]/settings/billing.tsx | 11 +- .../[workspaceSlug]/settings/exports.tsx | 11 +- .../[workspaceSlug]/settings/imports.tsx | 11 +- web/pages/[workspaceSlug]/settings/index.tsx | 2 +- .../[workspaceSlug]/settings/integrations.tsx | 12 +- .../[workspaceSlug]/settings/members.tsx | 34 +- .../settings/webhooks/[webhookId].tsx | 21 +- .../settings/webhooks/create.tsx | 108 ++ .../settings/webhooks/index.tsx | 14 +- .../workspace-views/[globalViewId].tsx | 2 +- .../workspace-views/all-issues.tsx | 2 +- .../workspace-views/assigned.tsx | 2 +- .../workspace-views/created.tsx | 2 +- .../[workspaceSlug]/workspace-views/index.tsx | 2 +- .../workspace-views/subscribed.tsx | 2 +- web/pages/_app.tsx | 9 +- web/pages/accounts/password.tsx | 2 +- web/pages/accounts/sign-up.tsx | 21 +- web/pages/create-workspace.tsx | 14 +- web/pages/god-mode/ai.tsx | 8 +- web/pages/god-mode/authorization.tsx | 8 +- web/pages/god-mode/email.tsx | 8 +- web/pages/god-mode/image.tsx | 8 +- web/pages/god-mode/index.tsx | 10 +- web/pages/index.tsx | 2 +- web/pages/installations/[provider]/index.tsx | 2 +- web/pages/invitations/index.tsx | 49 +- web/pages/onboarding/index.tsx | 46 +- web/pages/profile/activity.tsx | 6 +- web/pages/profile/change-password.tsx | 12 +- web/pages/profile/index.tsx | 40 +- web/pages/profile/preferences.tsx | 11 +- web/pages/workspace-invitations/index.tsx | 30 +- web/services/ai.service.ts | 2 +- web/services/analytics.service.ts | 2 +- web/services/api_token.service.ts | 2 +- web/services/app_config.service.ts | 2 +- web/services/auth.service.ts | 2 +- web/services/cycle.service.ts | 15 +- web/services/inbox.service.ts | 2 +- web/services/instance.service.ts | 7 +- web/services/integrations/github.service.ts | 2 +- .../integrations/integration.service.ts | 2 +- web/services/integrations/jira.service.ts | 2 +- web/services/issue/index.ts | 1 + web/services/issue/issue.service.ts | 34 +- .../issue/issue_attachment.service.ts | 13 +- web/services/issue/issue_comment.service.ts | 2 +- web/services/issue/issue_draft.service.tsx | 4 +- web/services/issue/issue_label.service.ts | 4 +- web/services/issue/issue_reaction.service.ts | 10 +- web/services/issue/issue_relation.service.ts | 45 + web/services/issue_filter.service.ts | 95 ++ web/services/module.service.ts | 18 +- web/services/notification.service.ts | 2 +- web/services/page.service.ts | 26 +- .../project/project-estimate.service.ts | 11 +- .../project/project-member.service.ts | 10 +- .../project/project-publish.service.ts | 2 +- web/services/project/project-state.service.ts | 2 +- web/services/project/project.service.ts | 2 +- web/services/user.service.ts | 14 +- web/services/view.service.ts | 2 +- web/services/webhook.service.ts | 2 +- web/services/workspace.service.ts | 11 +- .../{ => application}/app-config.store.ts | 28 +- .../command-palette.store.ts | 98 +- .../{ => application}/event-tracker.store.ts | 30 +- web/store/application/index.ts | 35 + .../instance.store.ts | 84 +- web/store/application/router.store.ts | 145 ++ web/store/{ => application}/theme.store.ts | 25 +- web/store/archived-issues/index.ts | 3 - web/store/archived-issues/issue.store.ts | 232 --- .../archived-issues/issue_detail.store.ts | 198 --- .../archived-issues/issue_filters.store.ts | 247 --- web/store/calendar.store.ts | 120 -- web/store/cycle-issues/index.ts | 1 - web/store/cycle-issues/issue_filters.store.ts | 201 --- web/store/cycle.store.ts | 366 ++++ web/store/cycle/cycle_issue.store.ts | 358 ---- .../cycle/cycle_issue_calendar_view.store.ts | 89 - web/store/cycle/cycle_issue_filters.store.ts | 147 -- .../cycle/cycle_issue_kanban_view.store.ts | 448 ----- web/store/cycle/cycles.store.ts | 439 ----- web/store/cycle/index.ts | 5 - web/store/draft-issues/index.ts | 2 - web/store/draft-issues/issue.store.ts | 184 -- web/store/draft-issues/issue_filters.store.ts | 109 -- web/store/editor/index.ts | 1 - web/store/editor/mentions.store.ts | 46 - web/store/estimate.store.ts | 196 +++ web/store/global-view.store.ts | 164 ++ .../global-view/global_view_filters.store.ts | 68 - .../global-view/global_view_issues.store.ts | 204 --- web/store/global-view/global_views.store.ts | 205 --- web/store/global-view/index.ts | 3 - web/store/inbox/inbox.store.ts | 142 +- ...filters.store.ts => inbox_filter.store.ts} | 131 +- web/store/inbox/inbox_issue.store.ts | 243 +++ web/store/inbox/inbox_issue_detail.store.ts | 254 --- web/store/inbox/inbox_issues.store.ts | 93 - web/store/inbox/index.ts | 29 +- web/store/instance/index.ts | 1 - web/store/issue/archived/filter.store.ts | 213 +++ web/store/issue/archived/index.ts | 2 + web/store/issue/archived/issue.store.ts | 140 ++ web/store/issue/cycle/filter.store.ts | 200 +++ web/store/issue/cycle/index.ts | 2 + web/store/issue/cycle/issue.store.ts | 333 ++++ web/store/issue/draft/filter.store.ts | 197 +++ web/store/issue/draft/index.ts | 2 + web/store/issue/draft/issue.store.ts | 167 ++ .../helpers/issue-filter-helper.store.ts | 210 +++ .../helpers/issue-helper.store.ts} | 136 +- web/store/issue/index.ts | 7 - .../issue/issue-details/activity.store.ts | 86 + .../issue/issue-details/attachment.store.ts | 133 ++ .../issue/issue-details/comment.store.ts | 109 ++ .../issue-details/comment_reaction.store.ts | 133 ++ web/store/issue/issue-details/issue.store.ts | 116 ++ web/store/issue/issue-details/link.store.ts | 149 ++ .../issue/issue-details/reaction.store.ts | 123 ++ .../issue/issue-details/relation.store.ts | 164 ++ web/store/issue/issue-details/root.store.ts | 223 +++ .../issue/issue-details/sub_issues.store.ts | 116 ++ .../issue/issue-details/subscription.store.ts | 112 ++ web/store/issue/issue.store.ts | 372 +--- web/store/issue/issue_calendar_view.store.ts | 151 +- web/store/issue/issue_detail.store.ts | 62 +- web/store/issue/issue_draft.store.ts | 169 -- web/store/issue/issue_filters.store.ts | 249 --- web/store/issue/issue_kanban_view.store.ts | 409 +---- web/store/issue/issue_quick_add.store.ts | 123 -- web/store/issue/module/filter.store.ts | 200 +++ web/store/issue/module/index.ts | 2 + web/store/issue/module/issue.store.ts | 281 ++++ web/store/issue/profile/filter.store.ts | 205 +++ web/store/issue/profile/index.ts | 2 + web/store/issue/profile/issue.store.ts | 246 +++ web/store/issue/project-views/filter.store.ts | 201 +++ web/store/issue/project-views/index.ts | 2 + web/store/issue/project-views/issue.store.ts | 240 +++ web/store/issue/project/filter.store.ts | 197 +++ web/store/issue/project/index.ts | 2 + web/store/issue/project/issue.store.ts | 190 +++ web/store/issue/root.store.ts | 182 ++ web/store/issue/workspace/filter.store.ts | 220 +++ web/store/issue/workspace/index.ts | 2 + web/store/issue/workspace/issue.store.ts | 191 +++ .../base-issue-calendar-helper.store.ts | 53 - .../issues/base-issue-kanban-helper.store.ts | 191 --- web/store/issues/global/filter.store.ts | 432 ----- web/store/issues/global/issue.store.ts | 224 --- web/store/issues/index.ts | 48 - web/store/issues/profile/filter.store.ts | 341 ---- web/store/issues/profile/issue.store.ts | 334 ---- .../project-issues/archived/filter.store.ts | 145 -- .../project-issues/archived/issue.store.ts | 158 -- .../project-issues/base-issue-filter.store.ts | 29 - .../project-issues/cycle/filter.store.ts | 258 --- .../project-issues/cycle/issue.store.ts | 396 ----- .../project-issues/draft/filter.store.ts | 142 -- .../project-issues/draft/issue.store.ts | 195 --- .../project-issues/issue-filters.store.ts | 252 --- .../project-issues/module/filter.store.ts | 266 --- .../project-issues/module/issue.store.ts | 371 ---- .../project-view/filter.store.ts | 260 --- .../project-view/issue.store.ts | 231 --- .../project-issues/project/filter.store.ts | 145 -- .../project-issues/project/issue.store.ts | 232 --- web/store/issues/project-issues/utils.ts | 5 - web/store/issues/types.ts | 33 - web/store/label/index.ts | 42 + web/store/label/project-label.store.ts | 213 +++ web/store/label/workspace-label.store.ts | 64 + web/store/member/index.ts | 42 + web/store/member/project-member.store.ts | 216 +++ web/store/member/workspace-member.store.ts | 311 ++++ web/store/mention.store.ts | 54 + web/store/module-issues/index.ts | 1 - .../module-issues/issue_filters.store.ts | 201 --- web/store/module.store.ts | 322 ++++ web/store/module/index.ts | 5 - web/store/module/module_filters.store.ts | 170 -- web/store/module/module_issue.store.ts | 373 ---- .../module_issue_calendar_view.store.ts | 89 - .../module/module_issue_kanban_view.store.ts | 448 ----- web/store/module/modules.store.ts | 531 ------ web/store/page.store.ts | 529 +++--- web/store/profile-issues/index.ts | 2 - web/store/profile-issues/issue.store.ts | 282 ---- .../profile-issues/issue_filters.store.ts | 136 -- web/store/project-view.store.ts | 212 +++ web/store/project-view/index.ts | 5 - .../project_view_filters.store.ts | 68 - .../project_view_issue_calendar_view.store.ts | 89 - .../project-view/project_view_issues.store.ts | 379 ----- web/store/project-view/project_views.store.ts | 282 ---- web/store/project/index.ts | 24 +- web/store/project/project-estimates.store.ts | 188 --- web/store/project/project-label.store.ts | 255 --- web/store/project/project-members.store.ts | 201 --- web/store/project/project-publish.store.ts | 135 +- web/store/project/project-state.store.ts | 268 --- web/store/project/project.store.ts | 325 ++-- web/store/root.store.ts | 59 + web/store/root.ts | 404 ----- web/store/state.store.ts | 243 +++ web/store/user.store.ts | 452 ----- web/store/user/index.ts | 265 +++ web/store/user/user-membership.store.ts | 254 +++ web/store/webhook.store.ts | 207 --- web/store/workspace/api-token.store.ts | 114 ++ web/store/workspace/index.ts | 159 +- web/store/workspace/webhook.store.ts | 194 +++ web/store/workspace/workspace-member.store.ts | 384 ----- web/store/workspace/workspace.store.ts | 296 ---- .../workspace/workspace_filters.store.ts | 193 --- web/styles/globals.css | 2 +- web/styles/react-datepicker.css | 6 +- web/tsconfig.json | 3 +- yarn.lock | 1496 +++++++++-------- 940 files changed, 26418 insertions(+), 34451 deletions(-) create mode 100644 apiserver/plane/db/migrations/0051_remove_issueproperty_properties_and_more.py create mode 100644 apiserver/plane/db/migrations/0052_auto_20231220_1141.py create mode 100644 packages/types/package.json rename {web/types => packages/types/src}/ai.d.ts (73%) rename {web/types => packages/types/src}/analytics.d.ts (100%) rename {web/types => packages/types/src}/api_token.d.ts (100%) rename {web/types => packages/types/src}/app.d.ts (77%) rename {web/types => packages/types/src}/auth.d.ts (100%) rename web/types/calendar.ts => packages/types/src/calendar.d.ts (100%) rename {web/types => packages/types/src}/cycles.d.ts (91%) rename {web/types => packages/types/src}/estimate.d.ts (100%) rename {web/types => packages/types/src}/importer/github-importer.d.ts (100%) rename web/types/importer/index.ts => packages/types/src/importer/index.d.ts (92%) rename {web/types => packages/types/src}/importer/jira-importer.d.ts (100%) rename {web/types => packages/types/src}/inbox.d.ts (93%) rename {web/types => packages/types/src}/index.d.ts (81%) rename {web/types => packages/types/src}/instance.d.ts (100%) rename {web/types => packages/types/src}/integration.d.ts (100%) rename {web/types => packages/types/src}/issues.d.ts (72%) create mode 100644 packages/types/src/issues/base.d.ts create mode 100644 packages/types/src/issues/issue.d.ts create mode 100644 packages/types/src/issues/issue_activity.d.ts create mode 100644 packages/types/src/issues/issue_attachment.d.ts create mode 100644 packages/types/src/issues/issue_comment_reaction.d.ts create mode 100644 packages/types/src/issues/issue_link.d.ts create mode 100644 packages/types/src/issues/issue_reaction.d.ts create mode 100644 packages/types/src/issues/issue_relation.d.ts create mode 100644 packages/types/src/issues/issue_sub_issues.d.ts create mode 100644 packages/types/src/issues/issue_subscription.d.ts rename {web/types => packages/types/src}/modules.d.ts (93%) rename {web/types => packages/types/src}/notifications.d.ts (100%) rename {web/types => packages/types/src}/pages.d.ts (79%) rename {web/types => packages/types/src}/projects.d.ts (78%) rename {web/types => packages/types/src}/reaction.d.ts (100%) rename {web/types => packages/types/src}/state.d.ts (90%) rename {web/types => packages/types/src}/users.d.ts (97%) rename {web/types => packages/types/src}/view-props.d.ts (91%) rename {web/types => packages/types/src}/views.d.ts (58%) rename {web/types => packages/types/src}/waitlist.d.ts (100%) rename {web/types => packages/types/src}/webhook.d.ts (100%) rename {web/types => packages/types/src}/workspace-views.d.ts (64%) rename {web/types => packages/types/src}/workspace.d.ts (88%) rename web/components/command-palette/{command-pallette.tsx => command-palette.tsx} (94%) create mode 100644 web/components/core/modals/bulk-delete-issues-modal-item.tsx create mode 100644 web/components/dropdowns/cycle.tsx create mode 100644 web/components/dropdowns/date.tsx create mode 100644 web/components/dropdowns/estimate.tsx create mode 100644 web/components/dropdowns/index.ts create mode 100644 web/components/dropdowns/member/buttons.tsx create mode 100644 web/components/dropdowns/member/index.ts create mode 100644 web/components/dropdowns/member/project-member.tsx create mode 100644 web/components/dropdowns/member/types.d.ts create mode 100644 web/components/dropdowns/member/workspace-member.tsx create mode 100644 web/components/dropdowns/module.tsx create mode 100644 web/components/dropdowns/priority.tsx create mode 100644 web/components/dropdowns/project.tsx create mode 100644 web/components/dropdowns/state.tsx create mode 100644 web/components/dropdowns/types.d.ts delete mode 100644 web/components/estimates/estimate-select.tsx create mode 100644 web/components/issues/attachment/attachment-detail.tsx create mode 100644 web/components/issues/attachment/attachments-list.tsx delete mode 100644 web/components/issues/attachment/attachments.tsx rename web/components/issues/attachment/{delete-attachment-modal.tsx => delete-attachment-confirmation-modal.tsx} (69%) create mode 100644 web/components/issues/attachment/root.tsx create mode 100644 web/components/issues/issue-layouts/calendar/utils.ts delete mode 100644 web/components/issues/issue-layouts/kanban/headers/assignee.tsx delete mode 100644 web/components/issues/issue-layouts/kanban/headers/created_by.tsx delete mode 100644 web/components/issues/issue-layouts/kanban/headers/group-by-root.tsx delete mode 100644 web/components/issues/issue-layouts/kanban/headers/label.tsx delete mode 100644 web/components/issues/issue-layouts/kanban/headers/priority.tsx delete mode 100644 web/components/issues/issue-layouts/kanban/headers/project.tsx delete mode 100644 web/components/issues/issue-layouts/kanban/headers/state-group.tsx delete mode 100644 web/components/issues/issue-layouts/kanban/headers/state.tsx delete mode 100644 web/components/issues/issue-layouts/kanban/headers/sub-group-by-root.tsx create mode 100644 web/components/issues/issue-layouts/kanban/kanban-group.tsx delete mode 100644 web/components/issues/issue-layouts/kanban/properties.tsx create mode 100644 web/components/issues/issue-layouts/kanban/utils.ts delete mode 100644 web/components/issues/issue-layouts/list/headers/assignee.tsx delete mode 100644 web/components/issues/issue-layouts/list/headers/created-by.tsx delete mode 100644 web/components/issues/issue-layouts/list/headers/empty-group.tsx delete mode 100644 web/components/issues/issue-layouts/list/headers/group-by-root.tsx delete mode 100644 web/components/issues/issue-layouts/list/headers/label.tsx delete mode 100644 web/components/issues/issue-layouts/list/headers/priority.tsx delete mode 100644 web/components/issues/issue-layouts/list/headers/project.tsx delete mode 100644 web/components/issues/issue-layouts/list/headers/state-group.tsx delete mode 100644 web/components/issues/issue-layouts/list/headers/state.tsx delete mode 100644 web/components/issues/issue-layouts/list/properties.tsx create mode 100644 web/components/issues/issue-layouts/properties/all-properties.tsx delete mode 100644 web/components/issues/issue-layouts/properties/assignee.tsx delete mode 100644 web/components/issues/issue-layouts/properties/date.tsx delete mode 100644 web/components/issues/issue-layouts/properties/estimates.tsx create mode 100644 web/components/issues/issue-layouts/properties/index.ts delete mode 100644 web/components/issues/issue-layouts/properties/index.tsx delete mode 100644 web/components/issues/issue-layouts/properties/priority.tsx delete mode 100644 web/components/issues/issue-layouts/properties/state.tsx create mode 100644 web/components/issues/issue-layouts/properties/with-display-properties-HOC.tsx create mode 100644 web/components/issues/issue-layouts/utils.tsx create mode 100644 web/components/issues/issue-links/create-update-link-modal.tsx create mode 100644 web/components/issues/issue-links/index.ts create mode 100644 web/components/issues/issue-links/link-detail.tsx create mode 100644 web/components/issues/issue-links/links.tsx create mode 100644 web/components/issues/issue-links/root.tsx delete mode 100644 web/components/issues/select/assignee.tsx delete mode 100644 web/components/issues/select/cycle.tsx delete mode 100644 web/components/issues/select/date.tsx delete mode 100644 web/components/issues/select/estimate.tsx delete mode 100644 web/components/issues/select/module.tsx delete mode 100644 web/components/issues/select/priority.tsx delete mode 100644 web/components/issues/select/project.tsx delete mode 100644 web/components/issues/select/state.tsx delete mode 100644 web/components/issues/sidebar-select/assignee.tsx delete mode 100644 web/components/issues/sidebar-select/blocked.tsx delete mode 100644 web/components/issues/sidebar-select/blocker.tsx delete mode 100644 web/components/issues/sidebar-select/duplicate.tsx delete mode 100644 web/components/issues/sidebar-select/estimate.tsx delete mode 100644 web/components/issues/sidebar-select/priority.tsx delete mode 100644 web/components/issues/sidebar-select/relates-to.tsx create mode 100644 web/components/issues/sidebar-select/relation.tsx delete mode 100644 web/components/issues/sidebar-select/state.tsx delete mode 100644 web/components/labels/label-select.tsx delete mode 100644 web/components/modules/select/lead.tsx delete mode 100644 web/components/modules/select/members.tsx delete mode 100644 web/components/modules/sidebar-select/select-lead.tsx delete mode 100644 web/components/modules/sidebar-select/select-members.tsx delete mode 100644 web/components/pages/create-block.tsx delete mode 100644 web/components/project/members-select.tsx delete mode 100644 web/components/project/priority-select.tsx delete mode 100644 web/components/states/state-select.tsx delete mode 100644 web/components/ui/date.tsx delete mode 100644 web/components/ui/product-updates-modal.tsx delete mode 100644 web/components/workspace/member-select.tsx create mode 100644 web/components/workspace/settings/invitations-list-item.tsx delete mode 100644 web/components/workspace/single-invitation.tsx delete mode 100644 web/constants/kanban-helpers.ts delete mode 100644 web/contexts/issue-view.context.tsx delete mode 100644 web/contexts/profile-issues-context.tsx create mode 100644 web/contexts/store-context.tsx delete mode 100644 web/contexts/user.context.tsx delete mode 100644 web/contexts/workspace-member.context.tsx create mode 100644 web/hooks/store/index.ts create mode 100644 web/hooks/store/use-application.ts create mode 100644 web/hooks/store/use-calendar-view.ts create mode 100644 web/hooks/store/use-cycle.ts create mode 100644 web/hooks/store/use-estimate.ts create mode 100644 web/hooks/store/use-global-view.ts create mode 100644 web/hooks/store/use-inbox-filters.ts create mode 100644 web/hooks/store/use-inbox-issues.ts create mode 100644 web/hooks/store/use-inbox.ts create mode 100644 web/hooks/store/use-issue-detail.ts create mode 100644 web/hooks/store/use-issues.ts create mode 100644 web/hooks/store/use-kanban-view.ts create mode 100644 web/hooks/store/use-label.ts create mode 100644 web/hooks/store/use-member.ts create mode 100644 web/hooks/store/use-mention.ts create mode 100644 web/hooks/store/use-module.ts create mode 100644 web/hooks/store/use-page.ts create mode 100644 web/hooks/store/use-project-publish.ts create mode 100644 web/hooks/store/use-project-state.ts create mode 100644 web/hooks/store/use-project-view.ts create mode 100644 web/hooks/store/use-project.ts create mode 100644 web/hooks/store/use-user.ts create mode 100644 web/hooks/store/use-webhook.ts create mode 100644 web/hooks/store/use-workspace.ts delete mode 100644 web/hooks/use-editor-suggestions.tsx delete mode 100644 web/hooks/use-estimate-option.tsx delete mode 100644 web/hooks/use-project-details.tsx delete mode 100644 web/hooks/use-sub-issue.tsx delete mode 100644 web/lib/mobx/store-provider.tsx create mode 100644 web/lib/types.d.ts create mode 100644 web/pages/[workspaceSlug]/settings/webhooks/create.tsx create mode 100644 web/services/issue/issue_relation.service.ts create mode 100644 web/services/issue_filter.service.ts rename web/store/{ => application}/app-config.store.ts (61%) rename web/store/{ => application}/command-palette.store.ts (76%) rename web/store/{ => application}/event-tracker.store.ts (65%) create mode 100644 web/store/application/index.ts rename web/store/{instance => application}/instance.store.ts (66%) create mode 100644 web/store/application/router.store.ts rename web/store/{ => application}/theme.store.ts (87%) delete mode 100644 web/store/archived-issues/index.ts delete mode 100644 web/store/archived-issues/issue.store.ts delete mode 100644 web/store/archived-issues/issue_detail.store.ts delete mode 100644 web/store/archived-issues/issue_filters.store.ts delete mode 100644 web/store/calendar.store.ts delete mode 100644 web/store/cycle-issues/index.ts delete mode 100644 web/store/cycle-issues/issue_filters.store.ts create mode 100644 web/store/cycle.store.ts delete mode 100644 web/store/cycle/cycle_issue.store.ts delete mode 100644 web/store/cycle/cycle_issue_calendar_view.store.ts delete mode 100644 web/store/cycle/cycle_issue_filters.store.ts delete mode 100644 web/store/cycle/cycle_issue_kanban_view.store.ts delete mode 100644 web/store/cycle/cycles.store.ts delete mode 100644 web/store/cycle/index.ts delete mode 100644 web/store/draft-issues/index.ts delete mode 100644 web/store/draft-issues/issue.store.ts delete mode 100644 web/store/draft-issues/issue_filters.store.ts delete mode 100644 web/store/editor/index.ts delete mode 100644 web/store/editor/mentions.store.ts create mode 100644 web/store/estimate.store.ts create mode 100644 web/store/global-view.store.ts delete mode 100644 web/store/global-view/global_view_filters.store.ts delete mode 100644 web/store/global-view/global_view_issues.store.ts delete mode 100644 web/store/global-view/global_views.store.ts delete mode 100644 web/store/global-view/index.ts rename web/store/inbox/{inbox_filters.store.ts => inbox_filter.store.ts} (58%) create mode 100644 web/store/inbox/inbox_issue.store.ts delete mode 100644 web/store/inbox/inbox_issue_detail.store.ts delete mode 100644 web/store/inbox/inbox_issues.store.ts delete mode 100644 web/store/instance/index.ts create mode 100644 web/store/issue/archived/filter.store.ts create mode 100644 web/store/issue/archived/index.ts create mode 100644 web/store/issue/archived/issue.store.ts create mode 100644 web/store/issue/cycle/filter.store.ts create mode 100644 web/store/issue/cycle/index.ts create mode 100644 web/store/issue/cycle/issue.store.ts create mode 100644 web/store/issue/draft/filter.store.ts create mode 100644 web/store/issue/draft/index.ts create mode 100644 web/store/issue/draft/issue.store.ts create mode 100644 web/store/issue/helpers/issue-filter-helper.store.ts rename web/store/{issues/project-issues/base-issue.store.ts => issue/helpers/issue-helper.store.ts} (59%) delete mode 100644 web/store/issue/index.ts create mode 100644 web/store/issue/issue-details/activity.store.ts create mode 100644 web/store/issue/issue-details/attachment.store.ts create mode 100644 web/store/issue/issue-details/comment.store.ts create mode 100644 web/store/issue/issue-details/comment_reaction.store.ts create mode 100644 web/store/issue/issue-details/issue.store.ts create mode 100644 web/store/issue/issue-details/link.store.ts create mode 100644 web/store/issue/issue-details/reaction.store.ts create mode 100644 web/store/issue/issue-details/relation.store.ts create mode 100644 web/store/issue/issue-details/root.store.ts create mode 100644 web/store/issue/issue-details/sub_issues.store.ts create mode 100644 web/store/issue/issue-details/subscription.store.ts delete mode 100644 web/store/issue/issue_draft.store.ts delete mode 100644 web/store/issue/issue_filters.store.ts delete mode 100644 web/store/issue/issue_quick_add.store.ts create mode 100644 web/store/issue/module/filter.store.ts create mode 100644 web/store/issue/module/index.ts create mode 100644 web/store/issue/module/issue.store.ts create mode 100644 web/store/issue/profile/filter.store.ts create mode 100644 web/store/issue/profile/index.ts create mode 100644 web/store/issue/profile/issue.store.ts create mode 100644 web/store/issue/project-views/filter.store.ts create mode 100644 web/store/issue/project-views/index.ts create mode 100644 web/store/issue/project-views/issue.store.ts create mode 100644 web/store/issue/project/filter.store.ts create mode 100644 web/store/issue/project/index.ts create mode 100644 web/store/issue/project/issue.store.ts create mode 100644 web/store/issue/root.store.ts create mode 100644 web/store/issue/workspace/filter.store.ts create mode 100644 web/store/issue/workspace/index.ts create mode 100644 web/store/issue/workspace/issue.store.ts delete mode 100644 web/store/issues/base-issue-calendar-helper.store.ts delete mode 100644 web/store/issues/base-issue-kanban-helper.store.ts delete mode 100644 web/store/issues/global/filter.store.ts delete mode 100644 web/store/issues/global/issue.store.ts delete mode 100644 web/store/issues/index.ts delete mode 100644 web/store/issues/profile/filter.store.ts delete mode 100644 web/store/issues/profile/issue.store.ts delete mode 100644 web/store/issues/project-issues/archived/filter.store.ts delete mode 100644 web/store/issues/project-issues/archived/issue.store.ts delete mode 100644 web/store/issues/project-issues/base-issue-filter.store.ts delete mode 100644 web/store/issues/project-issues/cycle/filter.store.ts delete mode 100644 web/store/issues/project-issues/cycle/issue.store.ts delete mode 100644 web/store/issues/project-issues/draft/filter.store.ts delete mode 100644 web/store/issues/project-issues/draft/issue.store.ts delete mode 100644 web/store/issues/project-issues/issue-filters.store.ts delete mode 100644 web/store/issues/project-issues/module/filter.store.ts delete mode 100644 web/store/issues/project-issues/module/issue.store.ts delete mode 100644 web/store/issues/project-issues/project-view/filter.store.ts delete mode 100644 web/store/issues/project-issues/project-view/issue.store.ts delete mode 100644 web/store/issues/project-issues/project/filter.store.ts delete mode 100644 web/store/issues/project-issues/project/issue.store.ts delete mode 100644 web/store/issues/project-issues/utils.ts delete mode 100644 web/store/issues/types.ts create mode 100644 web/store/label/index.ts create mode 100644 web/store/label/project-label.store.ts create mode 100644 web/store/label/workspace-label.store.ts create mode 100644 web/store/member/index.ts create mode 100644 web/store/member/project-member.store.ts create mode 100644 web/store/member/workspace-member.store.ts create mode 100644 web/store/mention.store.ts delete mode 100644 web/store/module-issues/index.ts delete mode 100644 web/store/module-issues/issue_filters.store.ts create mode 100644 web/store/module.store.ts delete mode 100644 web/store/module/index.ts delete mode 100644 web/store/module/module_filters.store.ts delete mode 100644 web/store/module/module_issue.store.ts delete mode 100644 web/store/module/module_issue_calendar_view.store.ts delete mode 100644 web/store/module/module_issue_kanban_view.store.ts delete mode 100644 web/store/module/modules.store.ts delete mode 100644 web/store/profile-issues/index.ts delete mode 100644 web/store/profile-issues/issue.store.ts delete mode 100644 web/store/profile-issues/issue_filters.store.ts create mode 100644 web/store/project-view.store.ts delete mode 100644 web/store/project-view/index.ts delete mode 100644 web/store/project-view/project_view_filters.store.ts delete mode 100644 web/store/project-view/project_view_issue_calendar_view.store.ts delete mode 100644 web/store/project-view/project_view_issues.store.ts delete mode 100644 web/store/project-view/project_views.store.ts delete mode 100644 web/store/project/project-estimates.store.ts delete mode 100644 web/store/project/project-label.store.ts delete mode 100644 web/store/project/project-members.store.ts delete mode 100644 web/store/project/project-state.store.ts create mode 100644 web/store/root.store.ts delete mode 100644 web/store/root.ts create mode 100644 web/store/state.store.ts delete mode 100644 web/store/user.store.ts create mode 100644 web/store/user/index.ts create mode 100644 web/store/user/user-membership.store.ts delete mode 100644 web/store/webhook.store.ts create mode 100644 web/store/workspace/api-token.store.ts create mode 100644 web/store/workspace/webhook.store.ts delete mode 100644 web/store/workspace/workspace-member.store.ts delete mode 100644 web/store/workspace/workspace.store.ts delete mode 100644 web/store/workspace/workspace_filters.store.ts diff --git a/.github/workflows/create-sync-pr.yml b/.github/workflows/create-sync-pr.yml index 5b5f958d3b6..0f85e940c9e 100644 --- a/.github/workflows/create-sync-pr.yml +++ b/.github/workflows/create-sync-pr.yml @@ -3,7 +3,7 @@ name: Create Sync Action on: pull_request: branches: - - preview + - develop # Change this to preview types: - closed env: @@ -33,14 +33,23 @@ jobs: sudo apt update sudo apt install gh -y - - name: Push Changes to Target Repo + - name: Create Pull Request env: GH_TOKEN: ${{ secrets.ACCESS_TOKEN }} run: | TARGET_REPO="${{ secrets.SYNC_TARGET_REPO_NAME }}" TARGET_BRANCH="${{ secrets.SYNC_TARGET_BRANCH_NAME }}" + TARGET_BASE_BRANCH="${{ secrets.SYNC_TARGET_BASE_BRANCH_NAME }}" SOURCE_BRANCH="${{ env.SOURCE_BRANCH_NAME }}" git checkout $SOURCE_BRANCH git remote add target-origin "https://$GH_TOKEN@github.com/$TARGET_REPO.git" - git push target-origin $SOURCE_BRANCH:$TARGET_BRANCH \ No newline at end of file + git push target-origin $SOURCE_BRANCH:$TARGET_BRANCH + + PR_TITLE=${{secrets.SYNC_PR_TITLE}} + + gh pr create \ + --base $TARGET_BASE_BRANCH \ + --head $TARGET_BRANCH \ + --title "$PR_TITLE" \ + --repo $TARGET_REPO diff --git a/apiserver/plane/api/serializers/base.py b/apiserver/plane/api/serializers/base.py index b964225011d..4e88597c783 100644 --- a/apiserver/plane/api/serializers/base.py +++ b/apiserver/plane/api/serializers/base.py @@ -97,7 +97,7 @@ def to_representation(self, instance): exp_serializer = expansion[expand]( getattr(instance, expand) ) - response[expand] = exp_serializer.data + response[expand] = exp_serializer.data else: # You might need to handle this case differently response[expand] = getattr(instance, f"{expand}_id", None) diff --git a/apiserver/plane/app/serializers/__init__.py b/apiserver/plane/app/serializers/__init__.py index c406453b728..4e0c12fe515 100644 --- a/apiserver/plane/app/serializers/__init__.py +++ b/apiserver/plane/app/serializers/__init__.py @@ -17,6 +17,7 @@ WorkspaceThemeSerializer, WorkspaceMemberAdminSerializer, WorkspaceMemberMeSerializer, + WorkspaceUserPropertiesSerializer, ) from .project import ( ProjectSerializer, @@ -31,6 +32,7 @@ ProjectDeployBoardSerializer, ProjectMemberAdminSerializer, ProjectPublicMemberSerializer, + ProjectMemberRoleSerializer, ) from .state import StateSerializer, StateLiteSerializer from .view import GlobalViewSerializer, IssueViewSerializer, IssueViewFavoriteSerializer @@ -39,6 +41,7 @@ CycleIssueSerializer, CycleFavoriteSerializer, CycleWriteSerializer, + CycleUserPropertiesSerializer, ) from .asset import FileAssetSerializer from .issue import ( @@ -61,6 +64,8 @@ IssueRelationSerializer, RelatedIssueSerializer, IssuePublicSerializer, + IssueRelationLiteSerializer, + ) from .module import ( @@ -69,6 +74,7 @@ ModuleIssueSerializer, ModuleLinkSerializer, ModuleFavoriteSerializer, + ModuleUserPropertiesSerializer, ) from .api import APITokenSerializer, APITokenReadSerializer diff --git a/apiserver/plane/app/serializers/base.py b/apiserver/plane/app/serializers/base.py index 89c9725d951..f67f5cf523a 100644 --- a/apiserver/plane/app/serializers/base.py +++ b/apiserver/plane/app/serializers/base.py @@ -9,11 +9,12 @@ class DynamicBaseSerializer(BaseSerializer): def __init__(self, *args, **kwargs): # If 'fields' is provided in the arguments, remove it and store it separately. # This is done so as not to pass this custom argument up to the superclass. - fields = kwargs.pop("fields", None) + fields = kwargs.pop("fields", []) + self.expand = kwargs.pop("expand", []) or [] + fields = self.expand # Call the initialization of the superclass. super().__init__(*args, **kwargs) - # If 'fields' was provided, filter the fields of the serializer accordingly. if fields is not None: self.fields = self._filter_fields(fields) @@ -47,12 +48,91 @@ def _filter_fields(self, fields): elif isinstance(item, dict): allowed.append(list(item.keys())[0]) - # Convert the current serializer's fields and the allowed fields to sets. - existing = set(self.fields) - allowed = set(allowed) + for field in allowed: + if field not in self.fields: + from . import ( + WorkspaceLiteSerializer, + ProjectLiteSerializer, + UserLiteSerializer, + StateLiteSerializer, + IssueSerializer, + LabelSerializer, + CycleIssueSerializer, + IssueFlatSerializer, + ) - # Remove fields from the serializer that aren't in the 'allowed' list. - for field_name in (existing - allowed): - self.fields.pop(field_name) + # Expansion mapper + expansion = { + "user": UserLiteSerializer, + "workspace": WorkspaceLiteSerializer, + "project": ProjectLiteSerializer, + "default_assignee": UserLiteSerializer, + "project_lead": UserLiteSerializer, + "state": StateLiteSerializer, + "created_by": UserLiteSerializer, + "issue": IssueSerializer, + "actor": UserLiteSerializer, + "owned_by": UserLiteSerializer, + "members": UserLiteSerializer, + "assignees": UserLiteSerializer, + "labels": LabelSerializer, + "issue_cycle": CycleIssueSerializer, + "parent": IssueFlatSerializer, + } + + self.fields[field] = expansion[field](many=True if field in ["members", "assignees", "labels", "issue_cycle"] else False) return self.fields + + + def to_representation(self, instance): + response = super().to_representation(instance) + + # Ensure 'expand' is iterable before processing + if self.expand: + for expand in self.expand: + if expand in self.fields: + # Import all the expandable serializers + from . import ( + WorkspaceLiteSerializer, + ProjectLiteSerializer, + UserLiteSerializer, + StateLiteSerializer, + IssueSerializer, + LabelSerializer, + CycleIssueSerializer, + ) + + # Expansion mapper + expansion = { + "user": UserLiteSerializer, + "workspace": WorkspaceLiteSerializer, + "project": ProjectLiteSerializer, + "default_assignee": UserLiteSerializer, + "project_lead": UserLiteSerializer, + "state": StateLiteSerializer, + "created_by": UserLiteSerializer, + "issue": IssueSerializer, + "actor": UserLiteSerializer, + "owned_by": UserLiteSerializer, + "members": UserLiteSerializer, + "assignees": UserLiteSerializer, + "labels": LabelSerializer, + "issue_cycle": CycleIssueSerializer, + } + # Check if field in expansion then expand the field + if expand in expansion: + if isinstance(response.get(expand), list): + exp_serializer = expansion[expand]( + getattr(instance, expand), many=True + ) + else: + exp_serializer = expansion[expand]( + getattr(instance, expand) + ) + response[expand] = exp_serializer.data + else: + # You might need to handle this case differently + response[expand] = getattr(instance, f"{expand}_id", None) + + return response diff --git a/apiserver/plane/app/serializers/cycle.py b/apiserver/plane/app/serializers/cycle.py index 63abf3a033f..f0ee8f9dae7 100644 --- a/apiserver/plane/app/serializers/cycle.py +++ b/apiserver/plane/app/serializers/cycle.py @@ -7,7 +7,7 @@ from .issue import IssueStateSerializer from .workspace import WorkspaceLiteSerializer from .project import ProjectLiteSerializer -from plane.db.models import Cycle, CycleIssue, CycleFavorite +from plane.db.models import Cycle, CycleIssue, CycleFavorite, CycleUserProperties class CycleWriteSerializer(BaseSerializer): @@ -106,3 +106,15 @@ class Meta: "project", "user", ] + + +class CycleUserPropertiesSerializer(BaseSerializer): + class Meta: + model = CycleUserProperties + fields = "__all__" + read_only_fields = [ + "workspace", + "project", + "cycle" + "user", + ] \ No newline at end of file diff --git a/apiserver/plane/app/serializers/inbox.py b/apiserver/plane/app/serializers/inbox.py index f52a90660be..cdc2646dd35 100644 --- a/apiserver/plane/app/serializers/inbox.py +++ b/apiserver/plane/app/serializers/inbox.py @@ -49,7 +49,6 @@ class IssueStateInboxSerializer(BaseSerializer): label_details = LabelLiteSerializer(read_only=True, source="labels", many=True) assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True) sub_issues_count = serializers.IntegerField(read_only=True) - bridge_id = serializers.UUIDField(read_only=True) issue_inbox = InboxIssueLiteSerializer(read_only=True, many=True) class Meta: diff --git a/apiserver/plane/app/serializers/issue.py b/apiserver/plane/app/serializers/issue.py index b13d03e35a4..6d39f17603e 100644 --- a/apiserver/plane/app/serializers/issue.py +++ b/apiserver/plane/app/serializers/issue.py @@ -278,17 +278,28 @@ class Meta: ] +class IssueRelationLiteSerializer(DynamicBaseSerializer): + project_id = serializers.PrimaryKeyRelatedField(read_only=True) + class Meta: + model = Issue + fields = [ + "id", + "project_id", + "sequence_id", + ] + read_only_fields = [ + "workspace", + "project", + ] + + class IssueRelationSerializer(BaseSerializer): - issue_detail = IssueProjectLiteSerializer(read_only=True, source="related_issue") + issue_detail = IssueRelationLiteSerializer(read_only=True, source="related_issue") class Meta: model = IssueRelation fields = [ "issue_detail", - "relation_type", - "related_issue", - "issue", - "id" ] read_only_fields = [ "workspace", @@ -296,16 +307,12 @@ class Meta: ] class RelatedIssueSerializer(BaseSerializer): - issue_detail = IssueProjectLiteSerializer(read_only=True, source="issue") + issue_detail = IssueRelationLiteSerializer(read_only=True, source="issue") class Meta: model = IssueRelation fields = [ "issue_detail", - "relation_type", - "related_issue", - "issue", - "id" ] read_only_fields = [ "workspace", @@ -512,7 +519,6 @@ class IssueStateSerializer(DynamicBaseSerializer): project_detail = ProjectLiteSerializer(read_only=True, source="project") assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True) sub_issues_count = serializers.IntegerField(read_only=True) - bridge_id = serializers.UUIDField(read_only=True) attachment_count = serializers.IntegerField(read_only=True) link_count = serializers.IntegerField(read_only=True) @@ -521,32 +527,58 @@ class Meta: fields = "__all__" -class IssueSerializer(BaseSerializer): - project_detail = ProjectLiteSerializer(read_only=True, source="project") - state_detail = StateSerializer(read_only=True, source="state") - parent_detail = IssueStateFlatSerializer(read_only=True, source="parent") - label_details = LabelSerializer(read_only=True, source="labels", many=True) - assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True) - related_issues = IssueRelationSerializer(read_only=True, source="issue_relation", many=True) - issue_relations = RelatedIssueSerializer(read_only=True, source="issue_related", many=True) - issue_cycle = IssueCycleDetailSerializer(read_only=True) - issue_module = IssueModuleDetailSerializer(read_only=True) - issue_link = IssueLinkSerializer(read_only=True, many=True) - issue_attachment = IssueAttachmentSerializer(read_only=True, many=True) +class IssueSerializer(DynamicBaseSerializer): + # ids + project_id = serializers.PrimaryKeyRelatedField(read_only=True) + state_id = serializers.PrimaryKeyRelatedField(read_only=True) + parent_id = serializers.PrimaryKeyRelatedField(read_only=True) + cycle_id = serializers.PrimaryKeyRelatedField(read_only=True) + module_id = serializers.PrimaryKeyRelatedField(read_only=True) + + # Many to many + label_ids = serializers.PrimaryKeyRelatedField(read_only=True, many=True, source="labels") + assignee_ids = serializers.PrimaryKeyRelatedField(read_only=True, many=True, source="assignees") + + # Count items sub_issues_count = serializers.IntegerField(read_only=True) - issue_reactions = IssueReactionSerializer(read_only=True, many=True) + attachment_count = serializers.IntegerField(read_only=True) + link_count = serializers.IntegerField(read_only=True) + + # is + is_subscribed = serializers.BooleanField(read_only=True) class Meta: model = Issue - fields = "__all__" - read_only_fields = [ - "workspace", - "project", - "created_by", - "updated_by", + fields = [ + "id", + "name", + "state_id", + "description_html", + "sort_order", + "completed_at", + "estimate_point", + "priority", + "start_date", + "target_date", + "sequence_id", + "project_id", + "parent_id", + "cycle_id", + "module_id", + "label_ids", + "assignee_ids", + "sub_issues_count", "created_at", "updated_at", + "created_by", + "updated_by", + "attachment_count", + "link_count", + "is_subscribed", + "is_draft", + "archived_at", ] + read_only_fields = fields class IssueLiteSerializer(DynamicBaseSerializer): diff --git a/apiserver/plane/app/serializers/module.py b/apiserver/plane/app/serializers/module.py index 48f773b0f81..b38d05b2c1f 100644 --- a/apiserver/plane/app/serializers/module.py +++ b/apiserver/plane/app/serializers/module.py @@ -2,7 +2,7 @@ from rest_framework import serializers # Module imports -from .base import BaseSerializer +from .base import BaseSerializer, DynamicBaseSerializer from .user import UserLiteSerializer from .project import ProjectLiteSerializer from .workspace import WorkspaceLiteSerializer @@ -14,6 +14,7 @@ ModuleIssue, ModuleLink, ModuleFavorite, + ModuleUserProperties, ) @@ -159,7 +160,7 @@ def create(self, validated_data): return ModuleLink.objects.create(**validated_data) -class ModuleSerializer(BaseSerializer): +class ModuleSerializer(DynamicBaseSerializer): project_detail = ProjectLiteSerializer(read_only=True, source="project") lead_detail = UserLiteSerializer(read_only=True, source="lead") members_detail = UserLiteSerializer(read_only=True, many=True, source="members") @@ -196,3 +197,14 @@ class Meta: "project", "user", ] + +class ModuleUserPropertiesSerializer(BaseSerializer): + class Meta: + model = ModuleUserProperties + fields = "__all__" + read_only_fields = [ + "workspace", + "project", + "module", + "user" + ] \ No newline at end of file diff --git a/apiserver/plane/app/serializers/project.py b/apiserver/plane/app/serializers/project.py index aef715e33a8..b3122962b13 100644 --- a/apiserver/plane/app/serializers/project.py +++ b/apiserver/plane/app/serializers/project.py @@ -159,6 +159,11 @@ class Meta: model = ProjectMember fields = "__all__" +class ProjectMemberRoleSerializer(DynamicBaseSerializer): + + class Meta: + model = ProjectMember + fields = ("id", "role", "member", "project") class ProjectMemberInviteSerializer(BaseSerializer): project = ProjectLiteSerializer(read_only=True) diff --git a/apiserver/plane/app/serializers/view.py b/apiserver/plane/app/serializers/view.py index e7502609a72..db44a2fc0b6 100644 --- a/apiserver/plane/app/serializers/view.py +++ b/apiserver/plane/app/serializers/view.py @@ -2,7 +2,7 @@ from rest_framework import serializers # Module imports -from .base import BaseSerializer +from .base import BaseSerializer, DynamicBaseSerializer from .workspace import WorkspaceLiteSerializer from .project import ProjectLiteSerializer from plane.db.models import GlobalView, IssueView, IssueViewFavorite @@ -38,7 +38,7 @@ def update(self, instance, validated_data): return super().update(instance, validated_data) -class IssueViewSerializer(BaseSerializer): +class IssueViewSerializer(DynamicBaseSerializer): is_favorite = serializers.BooleanField(read_only=True) project_detail = ProjectLiteSerializer(source="project", read_only=True) workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True) @@ -80,4 +80,4 @@ class Meta: "workspace", "project", "user", - ] + ] \ No newline at end of file diff --git a/apiserver/plane/app/serializers/workspace.py b/apiserver/plane/app/serializers/workspace.py index f0ad4b4ab65..fe014f364ce 100644 --- a/apiserver/plane/app/serializers/workspace.py +++ b/apiserver/plane/app/serializers/workspace.py @@ -2,7 +2,7 @@ from rest_framework import serializers # Module imports -from .base import BaseSerializer +from .base import BaseSerializer, DynamicBaseSerializer from .user import UserLiteSerializer, UserAdminLiteSerializer from plane.db.models import ( @@ -13,10 +13,11 @@ TeamMember, WorkspaceMemberInvite, WorkspaceTheme, + WorkspaceUserProperties, ) -class WorkSpaceSerializer(BaseSerializer): +class WorkSpaceSerializer(DynamicBaseSerializer): owner = UserLiteSerializer(read_only=True) total_members = serializers.IntegerField(read_only=True) total_issues = serializers.IntegerField(read_only=True) @@ -62,7 +63,7 @@ class Meta: -class WorkSpaceMemberSerializer(BaseSerializer): +class WorkSpaceMemberSerializer(DynamicBaseSerializer): member = UserLiteSerializer(read_only=True) workspace = WorkspaceLiteSerializer(read_only=True) @@ -78,7 +79,7 @@ class Meta: fields = "__all__" -class WorkspaceMemberAdminSerializer(BaseSerializer): +class WorkspaceMemberAdminSerializer(DynamicBaseSerializer): member = UserAdminLiteSerializer(read_only=True) workspace = WorkspaceLiteSerializer(read_only=True) @@ -161,3 +162,13 @@ class Meta: "workspace", "actor", ] + + +class WorkspaceUserPropertiesSerializer(BaseSerializer): + class Meta: + model = WorkspaceUserProperties + fields = "__all__" + read_only_fields = [ + "workspace", + "user", + ] \ No newline at end of file diff --git a/apiserver/plane/app/urls/cycle.py b/apiserver/plane/app/urls/cycle.py index 46e6a5e847c..5fef437c60a 100644 --- a/apiserver/plane/app/urls/cycle.py +++ b/apiserver/plane/app/urls/cycle.py @@ -7,6 +7,7 @@ CycleDateCheckEndpoint, CycleFavoriteViewSet, TransferCycleIssueEndpoint, + CycleUserPropertiesEndpoint, ) @@ -44,7 +45,7 @@ name="project-issue-cycle", ), path( - "workspaces//projects//cycles//cycle-issues//", + "workspaces//projects//cycles//cycle-issues//", CycleIssueViewSet.as_view( { "get": "retrieve", @@ -84,4 +85,9 @@ TransferCycleIssueEndpoint.as_view(), name="transfer-issues", ), + path( + "workspaces//projects//cycles//user-properties/", + CycleUserPropertiesEndpoint.as_view(), + name="cycle-user-filters", + ) ] diff --git a/apiserver/plane/app/urls/inbox.py b/apiserver/plane/app/urls/inbox.py index 16ea40b21a9..e9ec4e335a1 100644 --- a/apiserver/plane/app/urls/inbox.py +++ b/apiserver/plane/app/urls/inbox.py @@ -40,7 +40,7 @@ name="inbox-issue", ), path( - "workspaces//projects//inboxes//inbox-issues//", + "workspaces//projects//inboxes//inbox-issues//", InboxIssueViewSet.as_view( { "get": "retrieve", diff --git a/apiserver/plane/app/urls/issue.py b/apiserver/plane/app/urls/issue.py index 971fbc395df..234c2824dd7 100644 --- a/apiserver/plane/app/urls/issue.py +++ b/apiserver/plane/app/urls/issue.py @@ -235,7 +235,7 @@ ## End Comment Reactions ## IssueProperty path( - "workspaces//projects//issue-display-properties/", + "workspaces//projects//user-properties/", IssueUserDisplayPropertyEndpoint.as_view(), name="project-issue-display-properties", ), @@ -275,16 +275,17 @@ "workspaces//projects//issues//issue-relation/", IssueRelationViewSet.as_view( { + "get": "list", "post": "create", } ), name="issue-relation", ), path( - "workspaces//projects//issues//issue-relation//", + "workspaces//projects//issues//remove-relation/", IssueRelationViewSet.as_view( { - "delete": "destroy", + "post": "remove_relation", } ), name="issue-relation", diff --git a/apiserver/plane/app/urls/module.py b/apiserver/plane/app/urls/module.py index 5507b3a379d..961fff0dbe7 100644 --- a/apiserver/plane/app/urls/module.py +++ b/apiserver/plane/app/urls/module.py @@ -7,6 +7,7 @@ ModuleLinkViewSet, ModuleFavoriteViewSet, BulkImportModulesEndpoint, + ModuleUserPropertiesEndpoint ) @@ -44,7 +45,7 @@ name="project-module-issues", ), path( - "workspaces//projects//modules//module-issues//", + "workspaces//projects//modules//module-issues//", ModuleIssueViewSet.as_view( { "get": "retrieve", @@ -101,4 +102,9 @@ BulkImportModulesEndpoint.as_view(), name="bulk-modules-create", ), + path( + "workspaces//projects//modules//user-properties/", + ModuleUserPropertiesEndpoint.as_view(), + name="cycle-user-filters", + ) ] diff --git a/apiserver/plane/app/urls/views.py b/apiserver/plane/app/urls/views.py index 3d45b627a60..f78f17869cd 100644 --- a/apiserver/plane/app/urls/views.py +++ b/apiserver/plane/app/urls/views.py @@ -5,7 +5,7 @@ IssueViewViewSet, GlobalViewViewSet, GlobalViewIssuesViewSet, - IssueViewFavoriteViewSet, + IssueViewFavoriteViewSet, ) diff --git a/apiserver/plane/app/urls/workspace.py b/apiserver/plane/app/urls/workspace.py index 2c3638842c9..cc78881f9d5 100644 --- a/apiserver/plane/app/urls/workspace.py +++ b/apiserver/plane/app/urls/workspace.py @@ -18,6 +18,8 @@ WorkspaceUserProfileEndpoint, WorkspaceUserProfileIssuesEndpoint, WorkspaceLabelsEndpoint, + WorkspaceProjectMemberEndpoint, + WorkspaceUserPropertiesEndpoint, ) @@ -92,6 +94,11 @@ WorkSpaceMemberViewSet.as_view({"get": "list"}), name="workspace-member", ), + path( + "workspaces//project-members/", + WorkspaceProjectMemberEndpoint.as_view(), + name="workspace-member-roles", + ), path( "workspaces//members//", WorkSpaceMemberViewSet.as_view( @@ -195,4 +202,9 @@ WorkspaceLabelsEndpoint.as_view(), name="workspace-labels", ), + path( + "workspaces//user-properties/", + WorkspaceUserPropertiesEndpoint.as_view(), + name="workspace-user-filters", + ) ] diff --git a/apiserver/plane/app/views/__init__.py b/apiserver/plane/app/views/__init__.py index c122dce9f8f..520a3fd38e4 100644 --- a/apiserver/plane/app/views/__init__.py +++ b/apiserver/plane/app/views/__init__.py @@ -45,6 +45,8 @@ WorkspaceUserProfileEndpoint, WorkspaceUserProfileIssuesEndpoint, WorkspaceLabelsEndpoint, + WorkspaceProjectMemberEndpoint, + WorkspaceUserPropertiesEndpoint, ) from .state import StateViewSet from .view import ( @@ -59,6 +61,7 @@ CycleDateCheckEndpoint, CycleFavoriteViewSet, TransferCycleIssueEndpoint, + CycleUserPropertiesEndpoint, ) from .asset import FileAssetEndpoint, UserAssetsEndpoint, FileAssetViewSet from .issue import ( @@ -103,6 +106,7 @@ ModuleIssueViewSet, ModuleLinkViewSet, ModuleFavoriteViewSet, + ModuleUserPropertiesEndpoint, ) from .api import ApiTokenEndpoint diff --git a/apiserver/plane/app/views/base.py b/apiserver/plane/app/views/base.py index 32449597bd7..5bd79cb961a 100644 --- a/apiserver/plane/app/views/base.py +++ b/apiserver/plane/app/views/base.py @@ -159,6 +159,21 @@ def project_id(self): if resolve(self.request.path_info).url_name == "project": return self.kwargs.get("pk", None) + @property + def fields(self): + fields = [ + field for field in self.request.GET.get("fields", "").split(",") if field + ] + return fields if fields else None + + @property + def expand(self): + expand = [ + expand for expand in self.request.GET.get("expand", "").split(",") if expand + ] + return expand if expand else None + + class BaseAPIView(TimezoneMixin, APIView, BasePaginator): permission_classes = [ @@ -239,3 +254,17 @@ def workspace_slug(self): @property def project_id(self): return self.kwargs.get("project_id", None) + + @property + def fields(self): + fields = [ + field for field in self.request.GET.get("fields", "").split(",") if field + ] + return fields if fields else None + + @property + def expand(self): + expand = [ + expand for expand in self.request.GET.get("expand", "").split(",") if expand + ] + return expand if expand else None diff --git a/apiserver/plane/app/views/cycle.py b/apiserver/plane/app/views/cycle.py index 02f259de31d..73741b983de 100644 --- a/apiserver/plane/app/views/cycle.py +++ b/apiserver/plane/app/views/cycle.py @@ -14,7 +14,7 @@ Case, When, Value, - CharField + CharField, ) from django.core import serializers from django.utils import timezone @@ -33,8 +33,9 @@ CycleFavoriteSerializer, IssueStateSerializer, CycleWriteSerializer, + CycleUserPropertiesSerializer, ) -from plane.app.permissions import ProjectEntityPermission +from plane.app.permissions import ProjectEntityPermission, ProjectLitePermission from plane.db.models import ( User, Cycle, @@ -44,6 +45,7 @@ IssueLink, IssueAttachment, Label, + CycleUserProperties, ) from plane.bgtasks.issue_activites_task import issue_activity from plane.utils.grouper import group_results @@ -164,23 +166,18 @@ def get_queryset(self): .annotate( status=Case( When( - Q(start_date__lte=timezone.now()) & Q(end_date__gte=timezone.now()), - then=Value("CURRENT") - ), - When( - start_date__gt=timezone.now(), - then=Value("UPCOMING") - ), - When( - end_date__lt=timezone.now(), - then=Value("COMPLETED") + Q(start_date__lte=timezone.now()) + & Q(end_date__gte=timezone.now()), + then=Value("CURRENT"), ), + When(start_date__gt=timezone.now(), then=Value("UPCOMING")), + When(end_date__lt=timezone.now(), then=Value("COMPLETED")), When( Q(start_date__isnull=True) & Q(end_date__isnull=True), - then=Value("DRAFT") + then=Value("DRAFT"), ), - default=Value("DRAFT"), - output_field=CharField(), + default=Value("DRAFT"), + output_field=CharField(), ) ) .prefetch_related( @@ -202,6 +199,7 @@ def get_queryset(self): def list(self, request, slug, project_id): queryset = self.get_queryset() cycle_view = request.GET.get("cycle_view", "all") + fields = [field for field in request.GET.get("fields", "").split(",") if field] queryset = queryset.order_by("-is_favorite", "-created_at") @@ -307,44 +305,8 @@ def list(self, request, slug, project_id): return Response(data, status=status.HTTP_200_OK) - # Upcoming Cycles - if cycle_view == "upcoming": - queryset = queryset.filter(start_date__gt=timezone.now()) - return Response( - CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK - ) - - # Completed Cycles - if cycle_view == "completed": - queryset = queryset.filter(end_date__lt=timezone.now()) - return Response( - CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK - ) - - # Draft Cycles - if cycle_view == "draft": - queryset = queryset.filter( - end_date=None, - start_date=None, - ) - - return Response( - CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK - ) - - # Incomplete Cycles - if cycle_view == "incomplete": - queryset = queryset.filter( - Q(end_date__gte=timezone.now().date()) | Q(end_date__isnull=True), - ) - return Response( - CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK - ) - - # If no matching view is found return all cycles - return Response( - CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK - ) + cycles = CycleSerializer(queryset, many=True).data + return Response(cycles, status=status.HTTP_200_OK) def create(self, request, slug, project_id): if ( @@ -576,7 +538,6 @@ def list(self, request, slug, project_id, cycle_id): .annotate(count=Func(F("id"), function="Count")) .values("count") ) - .annotate(bridge_id=F("issue_cycle__id")) .filter(project_id=project_id) .filter(workspace__slug=slug) .select_related("project") @@ -600,12 +561,10 @@ def list(self, request, slug, project_id, cycle_id): .values("count") ) ) - - issues = IssueStateSerializer( + serializer = IssueStateSerializer( issues, many=True, fields=fields if fields else None - ).data - issue_dict = {str(issue["id"]): issue for issue in issues} - return Response(issue_dict, status=status.HTTP_200_OK) + ) + return Response(serializer.data, status=status.HTTP_200_OK) def create(self, request, slug, project_id, cycle_id): issues = request.data.get("issues", []) @@ -698,11 +657,13 @@ def create(self, request, slug, project_id, cycle_id): status=status.HTTP_200_OK, ) - def destroy(self, request, slug, project_id, cycle_id, pk): + def destroy(self, request, slug, project_id, cycle_id, issue_id): cycle_issue = CycleIssue.objects.get( - pk=pk, workspace__slug=slug, project_id=project_id, cycle_id=cycle_id + issue_id=issue_id, + workspace__slug=slug, + project_id=project_id, + cycle_id=cycle_id, ) - issue_id = cycle_issue.issue_id issue_activity.delay( type="cycle.activity.deleted", requested_data=json.dumps( @@ -712,7 +673,7 @@ def destroy(self, request, slug, project_id, cycle_id, pk): } ), actor_id=str(self.request.user.id), - issue_id=str(cycle_issue.issue_id), + issue_id=str(issue_id), project_id=str(self.kwargs.get("project_id", None)), current_instance=None, epoch=int(timezone.now().timestamp()), @@ -834,3 +795,39 @@ def post(self, request, slug, project_id, cycle_id): ) return Response({"message": "Success"}, status=status.HTTP_200_OK) + + +class CycleUserPropertiesEndpoint(BaseAPIView): + permission_classes = [ + ProjectLitePermission, + ] + + def patch(self, request, slug, project_id, cycle_id): + cycle_properties = CycleUserProperties.objects.get( + user=request.user, + cycle_id=cycle_id, + project_id=project_id, + workspace__slug=slug, + ) + + cycle_properties.filters = request.data.get("filters", cycle_properties.filters) + cycle_properties.display_filters = request.data.get( + "display_filters", cycle_properties.display_filters + ) + cycle_properties.display_properties = request.data.get( + "display_properties", cycle_properties.display_properties + ) + cycle_properties.save() + + serializer = CycleUserPropertiesSerializer(cycle_properties) + return Response(serializer.data, status=status.HTTP_201_CREATED) + + def get(self, request, slug, project_id, cycle_id): + cycle_properties, _ = CycleUserProperties.objects.get_or_create( + user=request.user, + project_id=project_id, + cycle_id=cycle_id, + workspace__slug=slug, + ) + serializer = CycleUserPropertiesSerializer(cycle_properties) + return Response(serializer.data, status=status.HTTP_200_OK) diff --git a/apiserver/plane/app/views/inbox.py b/apiserver/plane/app/views/inbox.py index 331ee21753e..32f38d97c7f 100644 --- a/apiserver/plane/app/views/inbox.py +++ b/apiserver/plane/app/views/inbox.py @@ -107,7 +107,6 @@ def list(self, request, slug, project_id, inbox_id): project_id=project_id, ) .filter(**filters) - .annotate(bridge_id=F("issue_inbox__id")) .select_related("workspace", "project", "state", "parent") .prefetch_related("assignees", "labels") .order_by("issue_inbox__snoozed_till", "issue_inbox__status") @@ -204,9 +203,9 @@ def create(self, request, slug, project_id, inbox_id): serializer = IssueStateInboxSerializer(issue) return Response(serializer.data, status=status.HTTP_200_OK) - def partial_update(self, request, slug, project_id, inbox_id, pk): + def partial_update(self, request, slug, project_id, inbox_id, issue_id): inbox_issue = InboxIssue.objects.get( - pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id + issue_id=issue_id, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id ) # Get the project member project_member = ProjectMember.objects.get( @@ -316,19 +315,16 @@ def partial_update(self, request, slug, project_id, inbox_id, pk): InboxIssueSerializer(inbox_issue).data, status=status.HTTP_200_OK ) - def retrieve(self, request, slug, project_id, inbox_id, pk): - inbox_issue = InboxIssue.objects.get( - pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id - ) + def retrieve(self, request, slug, project_id, inbox_id, issue_id): issue = Issue.objects.get( - pk=inbox_issue.issue_id, workspace__slug=slug, project_id=project_id + pk=issue_id, workspace__slug=slug, project_id=project_id ) serializer = IssueStateInboxSerializer(issue) return Response(serializer.data, status=status.HTTP_200_OK) - def destroy(self, request, slug, project_id, inbox_id, pk): + def destroy(self, request, slug, project_id, inbox_id, issue_id): inbox_issue = InboxIssue.objects.get( - pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id + issue_id=issue_id, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id ) # Get the project member project_member = ProjectMember.objects.get( @@ -350,7 +346,7 @@ def destroy(self, request, slug, project_id, inbox_id, pk): if inbox_issue.status in [-2, -1, 0, 2]: # Delete the issue also Issue.objects.filter( - workspace__slug=slug, project_id=project_id, pk=inbox_issue.issue_id + workspace__slug=slug, project_id=project_id, pk=issue_id ).delete() inbox_issue.delete() diff --git a/apiserver/plane/app/views/issue.py b/apiserver/plane/app/views/issue.py index d489629bada..6c88ef0909b 100644 --- a/apiserver/plane/app/views/issue.py +++ b/apiserver/plane/app/views/issue.py @@ -52,6 +52,7 @@ IssueRelationSerializer, RelatedIssueSerializer, IssuePublicSerializer, + IssueRelationLiteSerializer, ) from plane.app.permissions import ( ProjectEntityPermission, @@ -129,22 +130,6 @@ def get_queryset(self): queryset=IssueReaction.objects.select_related("actor"), ) ) - ).distinct() - - @method_decorator(gzip_page) - def list(self, request, slug, project_id): - fields = [field for field in request.GET.get("fields", "").split(",") if field] - filters = issue_filters(request.query_params, "GET") - - # Custom ordering for priority and state - priority_order = ["urgent", "high", "medium", "low", "none"] - state_order = ["backlog", "unstarted", "started", "completed", "cancelled"] - - order_by_param = request.GET.get("order_by", "-created_at") - - issue_queryset = ( - self.get_queryset() - .filter(**filters) .annotate(cycle_id=F("issue_cycle__cycle_id")) .annotate(module_id=F("issue_module__module_id")) .annotate( @@ -159,7 +144,26 @@ def list(self, request, slug, project_id): .annotate(count=Func(F("id"), function="Count")) .values("count") ) - ) + .annotate( + is_subscribed=Exists( + IssueSubscriber.objects.filter( + subscriber=self.request.user, issue_id=OuterRef("id") + ) + ) + ) + ).distinct() + + @method_decorator(gzip_page) + def list(self, request, slug, project_id): + filters = issue_filters(request.query_params, "GET") + + # Custom ordering for priority and state + priority_order = ["urgent", "high", "medium", "low", "none"] + state_order = ["backlog", "unstarted", "started", "completed", "cancelled"] + + order_by_param = request.GET.get("order_by", "-created_at") + + issue_queryset = self.get_queryset().filter(**filters) # Priority Ordering if order_by_param == "priority" or order_by_param == "-priority": @@ -217,9 +221,10 @@ def list(self, request, slug, project_id): else: issue_queryset = issue_queryset.order_by(order_by_param) - issues = IssueLiteSerializer(issue_queryset, many=True, fields=fields if fields else None).data - issue_dict = {str(issue["id"]): issue for issue in issues} - return Response(issue_dict, status=status.HTTP_200_OK) + issues = IssueSerializer( + issue_queryset, many=True, fields=self.fields, expand=self.expand + ).data + return Response(issues, status=status.HTTP_200_OK) def create(self, request, slug, project_id): project = Project.objects.get(pk=project_id) @@ -256,7 +261,10 @@ def retrieve(self, request, slug, project_id, pk=None): .annotate(count=Func(F("id"), function="Count")) .values("count") ).get(workspace__slug=slug, project_id=project_id, pk=pk) - return Response(IssueSerializer(issue).data, status=status.HTTP_200_OK) + return Response( + IssueSerializer(issue, fields=self.fields, expand=self.expand).data, + status=status.HTTP_200_OK, + ) def partial_update(self, request, slug, project_id, pk=None): issue = Issue.objects.get(workspace__slug=slug, project_id=project_id, pk=pk) @@ -590,16 +598,19 @@ class IssueUserDisplayPropertyEndpoint(BaseAPIView): ProjectLitePermission, ] - def post(self, request, slug, project_id): - issue_property, created = IssueProperty.objects.get_or_create( + def patch(self, request, slug, project_id): + issue_property = IssueProperty.objects.get( user=request.user, project_id=project_id, ) - if not created: - issue_property.properties = request.data.get("properties", {}) - issue_property.save() - issue_property.properties = request.data.get("properties", {}) + issue_property.filters = request.data.get("filters", issue_property.filters) + issue_property.display_filters = request.data.get( + "display_filters", issue_property.display_filters + ) + issue_property.display_properties = request.data.get( + "display_properties", issue_property.display_properties + ) issue_property.save() serializer = IssuePropertySerializer(issue_property) return Response(serializer.data, status=status.HTTP_201_CREATED) @@ -708,6 +719,13 @@ def get(self, request, slug, project_id, issue_id): .annotate(count=Func(F("id"), function="Count")) .values("count") ) + .annotate( + is_subscribed=Exists( + IssueSubscriber.objects.filter( + subscriber=self.request.user, issue_id=OuterRef("id") + ) + ) + ) .prefetch_related( Prefetch( "issue_reactions", @@ -728,7 +746,7 @@ def get(self, request, slug, project_id, issue_id): item["state_group"]: item["state_count"] for item in state_distribution } - serializer = IssueLiteSerializer( + serializer = IssueSerializer( sub_issues, many=True, ) @@ -775,7 +793,7 @@ def post(self, request, slug, project_id, issue_id): ] return Response( - IssueFlatSerializer(updated_sub_issues, many=True).data, + IssueSerializer(updated_sub_issues, many=True).data, status=status.HTTP_200_OK, ) @@ -1062,9 +1080,10 @@ def list(self, request, slug, project_id): else issue_queryset.filter(parent__isnull=True) ) - issues = IssueLiteSerializer(issue_queryset, many=True, fields=fields if fields else None).data - issue_dict = {str(issue["id"]): issue for issue in issues} - return Response(issue_dict, status=status.HTTP_200_OK) + issues = IssueLiteSerializer( + issue_queryset, many=True, fields=fields if fields else None + ).data + return Response(issues, status=status.HTTP_200_OK) def retrieve(self, request, slug, project_id, pk=None): issue = Issue.objects.get( @@ -1365,23 +1384,62 @@ def get_queryset(self): .distinct() ) + def list(self, request, slug, project_id, issue_id): + issue_relations = ( + IssueRelation.objects.filter(Q(issue_id=issue_id) | Q(related_issue=issue_id)) + .filter(workspace__slug=self.kwargs.get("slug")) + .select_related("project") + .select_related("workspace") + .select_related("issue") + .order_by("-created_at") + .distinct() + ) + + blocking_issues = issue_relations.filter(relation_type="blocked_by", related_issue_id=issue_id) + blocked_by_issues = issue_relations.filter(relation_type="blocked_by", issue_id=issue_id) + duplicate_issues = issue_relations.filter(issue_id=issue_id, relation_type="duplicate") + duplicate_issues_related = issue_relations.filter(related_issue_id=issue_id, relation_type="duplicate") + relates_to_issues = issue_relations.filter(issue_id=issue_id, relation_type="relates_to") + relates_to_issues_related = issue_relations.filter(related_issue_id=issue_id, relation_type="relates_to") + + blocked_by_issues_serialized = IssueRelationSerializer(blocked_by_issues, many=True).data + duplicate_issues_serialized = IssueRelationSerializer(duplicate_issues, many=True).data + relates_to_issues_serialized = IssueRelationSerializer(relates_to_issues, many=True).data + + # revere relation for blocked by issues + blocking_issues_serialized = RelatedIssueSerializer(blocking_issues, many=True).data + # reverse relation for duplicate issues + duplicate_issues_related_serialized = RelatedIssueSerializer(duplicate_issues_related, many=True).data + # reverse relation for related issues + relates_to_issues_related_serialized = RelatedIssueSerializer(relates_to_issues_related, many=True).data + + response_data = { + 'blocking': blocking_issues_serialized, + 'blocked_by': blocked_by_issues_serialized, + 'duplicate': duplicate_issues_serialized + duplicate_issues_related_serialized, + 'relates_to': relates_to_issues_serialized + relates_to_issues_related_serialized, + } + + return Response(response_data, status=status.HTTP_200_OK) + + def create(self, request, slug, project_id, issue_id): - related_list = request.data.get("related_list", []) - relation = request.data.get("relation", None) + relation_type = request.data.get("relation_type", None) + issues = request.data.get("issues", []) project = Project.objects.get(pk=project_id) issue_relation = IssueRelation.objects.bulk_create( [ IssueRelation( - issue_id=related_issue["issue"], - related_issue_id=related_issue["related_issue"], - relation_type=related_issue["relation_type"], + issue_id=issue if relation_type == "blocking" else issue_id, + related_issue_id=issue_id if relation_type == "blocking" else issue, + relation_type="blocked_by" if relation_type == "blocking" else relation_type, project_id=project_id, workspace_id=project.workspace_id, created_by=request.user, updated_by=request.user, ) - for related_issue in related_list + for issue in issues ], batch_size=10, ignore_conflicts=True, @@ -1397,7 +1455,7 @@ def create(self, request, slug, project_id, issue_id): epoch=int(timezone.now().timestamp()), ) - if relation == "blocking": + if relation_type == "blocking": return Response( RelatedIssueSerializer(issue_relation, many=True).data, status=status.HTTP_201_CREATED, @@ -1408,10 +1466,18 @@ def create(self, request, slug, project_id, issue_id): status=status.HTTP_201_CREATED, ) - def destroy(self, request, slug, project_id, issue_id, pk): - issue_relation = IssueRelation.objects.get( - workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk - ) + def remove_relation(self, request, slug, project_id, issue_id): + relation_type = request.data.get("relation_type", None) + related_issue = request.data.get("related_issue", None) + + if relation_type == "blocking": + issue_relation = IssueRelation.objects.get( + workspace__slug=slug, project_id=project_id, issue_id=related_issue, related_issue_id=issue_id + ) + else: + issue_relation = IssueRelation.objects.get( + workspace__slug=slug, project_id=project_id, issue_id=issue_id, related_issue_id=related_issue + ) current_instance = json.dumps( IssueRelationSerializer(issue_relation).data, cls=DjangoJSONEncoder, @@ -1419,7 +1485,7 @@ def destroy(self, request, slug, project_id, issue_id, pk): issue_relation.delete() issue_activity.delay( type="issue_relation.activity.deleted", - requested_data=json.dumps({"related_list": None}), + requested_data=json.dumps(request.data, cls=DjangoJSONEncoder), actor_id=str(request.user.id), issue_id=str(issue_id), project_id=str(project_id), @@ -1547,9 +1613,10 @@ def list(self, request, slug, project_id): else: issue_queryset = issue_queryset.order_by(order_by_param) - issues = IssueLiteSerializer(issue_queryset, many=True, fields=fields if fields else None).data - issue_dict = {str(issue["id"]): issue for issue in issues} - return Response(issue_dict, status=status.HTTP_200_OK) + issues = IssueLiteSerializer( + issue_queryset, many=True, fields=fields if fields else None + ).data + return Response(issues, status=status.HTTP_200_OK) def create(self, request, slug, project_id): project = Project.objects.get(pk=project_id) @@ -1626,4 +1693,4 @@ def destroy(self, request, slug, project_id, pk=None): current_instance=current_instance, epoch=int(timezone.now().timestamp()), ) - return Response(status=status.HTTP_204_NO_CONTENT) \ No newline at end of file + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/app/views/module.py b/apiserver/plane/app/views/module.py index a8a8655c316..6baf2312139 100644 --- a/apiserver/plane/app/views/module.py +++ b/apiserver/plane/app/views/module.py @@ -21,8 +21,9 @@ ModuleLinkSerializer, ModuleFavoriteSerializer, IssueStateSerializer, + ModuleUserPropertiesSerializer, ) -from plane.app.permissions import ProjectEntityPermission +from plane.app.permissions import ProjectEntityPermission, ProjectLitePermission from plane.db.models import ( Module, ModuleIssue, @@ -32,6 +33,7 @@ ModuleFavorite, IssueLink, IssueAttachment, + ModuleUserProperties, ) from plane.bgtasks.issue_activites_task import issue_activity from plane.utils.grouper import group_results @@ -54,7 +56,6 @@ def get_serializer_class(self): ) def get_queryset(self): - subquery = ModuleFavorite.objects.filter( user=self.request.user, module_id=OuterRef("pk"), @@ -136,7 +137,7 @@ def get_queryset(self): ), ) ) - .order_by("-is_favorite","-created_at") + .order_by("-is_favorite", "-created_at") ) def create(self, request, slug, project_id): @@ -153,6 +154,14 @@ def create(self, request, slug, project_id): return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + def list(self, request, slug, project_id): + queryset = self.get_queryset() + fields = [field for field in request.GET.get("fields", "").split(",") if field] + modules = ModuleSerializer( + queryset, many=True, fields=fields if fields else None + ).data + return Response(modules, status=status.HTTP_200_OK) + def retrieve(self, request, slug, project_id, pk): queryset = self.get_queryset().get(pk=pk) @@ -289,7 +298,6 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet): webhook_event = "module_issue" bulk = True - filterset_fields = [ "issue__labels__id", "issue__assignees__id", @@ -335,7 +343,6 @@ def list(self, request, slug, project_id, module_id): .annotate(count=Func(F("id"), function="Count")) .values("count") ) - .annotate(bridge_id=F("issue_module__id")) .filter(project_id=project_id) .filter(workspace__slug=slug) .select_related("project") @@ -359,9 +366,10 @@ def list(self, request, slug, project_id, module_id): .values("count") ) ) - issues = IssueStateSerializer(issues, many=True, fields=fields if fields else None).data - issue_dict = {str(issue["id"]): issue for issue in issues} - return Response(issue_dict, status=status.HTTP_200_OK) + serializer = IssueStateSerializer( + issues, many=True, fields=fields if fields else None + ) + return Response(serializer.data, status=status.HTTP_200_OK) def create(self, request, slug, project_id, module_id): issues = request.data.get("issues", []) @@ -444,20 +452,23 @@ def create(self, request, slug, project_id, module_id): status=status.HTTP_200_OK, ) - def destroy(self, request, slug, project_id, module_id, pk): + def destroy(self, request, slug, project_id, module_id, issue_id): module_issue = ModuleIssue.objects.get( - workspace__slug=slug, project_id=project_id, module_id=module_id, pk=pk + workspace__slug=slug, + project_id=project_id, + module_id=module_id, + issue_id=issue_id, ) issue_activity.delay( type="module.activity.deleted", requested_data=json.dumps( { "module_id": str(module_id), - "issues": [str(module_issue.issue_id)], + "issues": [str(issue_id)], } ), actor_id=str(request.user.id), - issue_id=str(module_issue.issue_id), + issue_id=str(issue_id), project_id=str(project_id), current_instance=None, epoch=int(timezone.now().timestamp()), @@ -521,4 +532,42 @@ def destroy(self, request, slug, project_id, module_id): module_id=module_id, ) module_favorite.delete() - return Response(status=status.HTTP_204_NO_CONTENT) \ No newline at end of file + return Response(status=status.HTTP_204_NO_CONTENT) + + +class ModuleUserPropertiesEndpoint(BaseAPIView): + permission_classes = [ + ProjectLitePermission, + ] + + def patch(self, request, slug, project_id, module_id): + module_properties = ModuleUserProperties.objects.get( + user=request.user, + module_id=module_id, + project_id=project_id, + workspace__slug=slug, + ) + + module_properties.filters = request.data.get( + "filters", module_properties.filters + ) + module_properties.display_filters = request.data.get( + "display_filters", module_properties.display_filters + ) + module_properties.display_properties = request.data.get( + "display_properties", module_properties.display_properties + ) + module_properties.save() + + serializer = ModuleUserPropertiesSerializer(module_properties) + return Response(serializer.data, status=status.HTTP_201_CREATED) + + def get(self, request, slug, project_id, module_id): + module_properties, _ = ModuleUserProperties.objects.get_or_create( + user=request.user, + project_id=project_id, + module_id=module_id, + workspace__slug=slug, + ) + serializer = ModuleUserPropertiesSerializer(module_properties) + return Response(serializer.data, status=status.HTTP_200_OK) diff --git a/apiserver/plane/app/views/page.py b/apiserver/plane/app/views/page.py index 9bd1f1dd4d5..482bdfbfe62 100644 --- a/apiserver/plane/app/views/page.py +++ b/apiserver/plane/app/views/page.py @@ -157,9 +157,8 @@ def unlock(self, request, slug, project_id, page_id): def list(self, request, slug, project_id): queryset = self.get_queryset().filter(archived_at__isnull=True) - return Response( - PageSerializer(queryset, many=True).data, status=status.HTTP_200_OK - ) + pages = PageSerializer(queryset, many=True).data + return Response(pages, status=status.HTTP_200_OK) def archive(self, request, slug, project_id, page_id): page = Page.objects.get(pk=page_id, workspace__slug=slug, project_id=project_id) @@ -210,9 +209,9 @@ def archive_list(self, request, slug, project_id): workspace__slug=slug, ).filter(archived_at__isnull=False) - return Response( - PageSerializer(pages, many=True).data, status=status.HTTP_200_OK - ) + pages = PageSerializer(pages, many=True).data + return Response(pages, status=status.HTTP_200_OK) + def destroy(self, request, slug, project_id, pk): page = Page.objects.get(pk=pk, workspace__slug=slug, project_id=project_id) diff --git a/apiserver/plane/app/views/project.py b/apiserver/plane/app/views/project.py index 5b88e3652d1..c5caac666f7 100644 --- a/apiserver/plane/app/views/project.py +++ b/apiserver/plane/app/views/project.py @@ -36,6 +36,7 @@ ProjectFavoriteSerializer, ProjectDeployBoardSerializer, ProjectMemberAdminSerializer, + ProjectMemberRoleSerializer, ) from plane.app.permissions import ( @@ -180,12 +181,9 @@ def list(self, request, slug): projects, many=True ).data, ) + projects = ProjectListSerializer(projects, many=True, fields=fields if fields else None).data + return Response(projects, status=status.HTTP_200_OK) - return Response( - ProjectListSerializer( - projects, many=True, fields=fields if fields else None - ).data - ) def create(self, request, slug): try: @@ -713,13 +711,7 @@ def create(self, request, slug, project_id): return Response(serializer.data, status=status.HTTP_201_CREATED) def list(self, request, slug, project_id): - project_member = ProjectMember.objects.get( - member=request.user, - workspace__slug=slug, - project_id=project_id, - is_active=True, - ) - + # Get the list of project members for the project project_members = ProjectMember.objects.filter( project_id=project_id, workspace__slug=slug, @@ -727,10 +719,7 @@ def list(self, request, slug, project_id): is_active=True, ).select_related("project", "member", "workspace") - if project_member.role > 10: - serializer = ProjectMemberAdminSerializer(project_members, many=True) - else: - serializer = ProjectMemberSerializer(project_members, many=True) + serializer = ProjectMemberRoleSerializer(project_members, fields=("id", "member", "role"), many=True) return Response(serializer.data, status=status.HTTP_200_OK) def partial_update(self, request, slug, project_id, pk): @@ -1010,18 +999,11 @@ class ProjectPublicCoverImagesEndpoint(BaseAPIView): def get(self, request): files = [] - s3_client_params = { - "service_name": "s3", - "aws_access_key_id": settings.AWS_ACCESS_KEY_ID, - "aws_secret_access_key": settings.AWS_SECRET_ACCESS_KEY, - } - - # Use AWS_S3_ENDPOINT_URL if it is present in the settings - if hasattr(settings, "AWS_S3_ENDPOINT_URL") and settings.AWS_S3_ENDPOINT_URL: - s3_client_params["endpoint_url"] = settings.AWS_S3_ENDPOINT_URL - - s3 = boto3.client(**s3_client_params) - + s3 = boto3.client( + "s3", + aws_access_key_id=settings.AWS_ACCESS_KEY_ID, + aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY, + ) params = { "Bucket": settings.AWS_STORAGE_BUCKET_NAME, "Prefix": "static/project-cover/", @@ -1034,19 +1016,9 @@ def get(self, request): if not content["Key"].endswith( "/" ): # This line ensures we're only getting files, not "sub-folders" - if ( - hasattr(settings, "AWS_S3_CUSTOM_DOMAIN") - and settings.AWS_S3_CUSTOM_DOMAIN - and hasattr(settings, "AWS_S3_URL_PROTOCOL") - and settings.AWS_S3_URL_PROTOCOL - ): - files.append( - f"{settings.AWS_S3_URL_PROTOCOL}//{settings.AWS_S3_CUSTOM_DOMAIN}/{content['Key']}" - ) - else: - files.append( - f"https://{settings.AWS_STORAGE_BUCKET_NAME}.s3.{settings.AWS_REGION}.amazonaws.com/{content['Key']}" - ) + files.append( + f"https://{settings.AWS_STORAGE_BUCKET_NAME}.s3.{settings.AWS_REGION}.amazonaws.com/{content['Key']}" + ) return Response(files, status=status.HTTP_200_OK) diff --git a/apiserver/plane/app/views/view.py b/apiserver/plane/app/views/view.py index eb76407b711..a2f00a81977 100644 --- a/apiserver/plane/app/views/view.py +++ b/apiserver/plane/app/views/view.py @@ -27,7 +27,12 @@ IssueLiteSerializer, IssueViewFavoriteSerializer, ) -from plane.app.permissions import WorkspaceEntityPermission, ProjectEntityPermission +from plane.app.permissions import ( + WorkspaceEntityPermission, + ProjectEntityPermission, + WorkspaceViewerPermission, + ProjectLitePermission, +) from plane.db.models import ( Workspace, GlobalView, @@ -43,8 +48,8 @@ class GlobalViewViewSet(BaseViewSet): - serializer_class = GlobalViewSerializer - model = GlobalView + serializer_class = IssueViewSerializer + model = IssueView permission_classes = [ WorkspaceEntityPermission, ] @@ -58,6 +63,7 @@ def get_queryset(self): super() .get_queryset() .filter(workspace__slug=self.kwargs.get("slug")) + .filter(project__isnull=True) .select_related("workspace") .order_by(self.request.GET.get("order_by", "-created_at")) .distinct() @@ -179,12 +185,10 @@ def list(self, request, slug): else: issue_queryset = issue_queryset.order_by(order_by_param) - issues = IssueLiteSerializer(issue_queryset, many=True, fields=fields if fields else None).data - issue_dict = {str(issue["id"]): issue for issue in issues} - return Response( - issue_dict, - status=status.HTTP_200_OK, + serializer = IssueLiteSerializer( + issue_queryset, many=True, fields=fields if fields else None ) + return Response(serializer.data, status=status.HTTP_200_OK) class IssueViewViewSet(BaseViewSet): @@ -217,6 +221,14 @@ def get_queryset(self): .distinct() ) + def list(self, request, slug, project_id): + queryset = self.get_queryset() + fields = [field for field in request.GET.get("fields", "").split(",") if field] + views = IssueViewSerializer( + queryset, many=True, fields=fields if fields else None + ).data + return Response(views, status=status.HTTP_200_OK) + class IssueViewFavoriteViewSet(BaseViewSet): serializer_class = IssueViewFavoriteSerializer @@ -246,4 +258,4 @@ def destroy(self, request, slug, project_id, view_id): view_id=view_id, ) view_favourite.delete() - return Response(status=status.HTTP_204_NO_CONTENT) \ No newline at end of file + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/app/views/workspace.py b/apiserver/plane/app/views/workspace.py index 11170114aaa..f51e1ac1e12 100644 --- a/apiserver/plane/app/views/workspace.py +++ b/apiserver/plane/app/views/workspace.py @@ -44,6 +44,8 @@ IssueLiteSerializer, WorkspaceMemberAdminSerializer, WorkspaceMemberMeSerializer, + ProjectMemberRoleSerializer, + WorkspaceUserPropertiesSerializer, ) from plane.app.views.base import BaseAPIView from . import BaseViewSet @@ -64,6 +66,7 @@ WorkspaceMember, CycleIssue, IssueReaction, + WorkspaceUserProperties ) from plane.app.permissions import ( WorkSpaceBasePermission, @@ -71,11 +74,13 @@ WorkspaceEntityPermission, WorkspaceViewerPermission, WorkspaceUserPermission, + ProjectLitePermission, ) from plane.bgtasks.workspace_invitation_task import workspace_invitation from plane.utils.issue_filters import issue_filters from plane.bgtasks.event_tracking_task import workspace_invite_event + class WorkSpaceViewSet(BaseViewSet): model = Workspace serializer_class = WorkSpaceSerializer @@ -173,6 +178,7 @@ class UserWorkSpacesEndpoint(BaseAPIView): ] def get(self, request): + fields = [field for field in request.GET.get("fields", "").split(",") if field] member_count = ( WorkspaceMember.objects.filter( workspace=OuterRef("id"), @@ -208,9 +214,12 @@ def get(self, request): ) .distinct() ) - - serializer = WorkSpaceSerializer(self.filter_queryset(workspace), many=True) - return Response(serializer.data, status=status.HTTP_200_OK) + workspaces = WorkSpaceSerializer( + self.filter_queryset(workspace), + fields=fields if fields else None, + many=True, + ).data + return Response(workspaces, status=status.HTTP_200_OK) class WorkSpaceAvailabilityCheckEndpoint(BaseAPIView): @@ -407,7 +416,7 @@ def post(self, request, slug, pk): # Delete the invitation workspace_invite.delete() - + # Send event workspace_invite_event.delay( user=user.id if user is not None else None, @@ -537,10 +546,15 @@ def list(self, request, slug): workspace_members = self.get_queryset() if workspace_member.role > 10: - serializer = WorkspaceMemberAdminSerializer(workspace_members, many=True) + serializer = WorkspaceMemberAdminSerializer( + workspace_members, + fields=("id", "member", "role"), + many=True, + ) else: serializer = WorkSpaceMemberSerializer( workspace_members, + fields=("id", "member", "role"), many=True, ) return Response(serializer.data, status=status.HTTP_200_OK) @@ -705,6 +719,43 @@ def leave(self, request, slug): return Response(status=status.HTTP_204_NO_CONTENT) +class WorkspaceProjectMemberEndpoint(BaseAPIView): + serializer_class = ProjectMemberRoleSerializer + model = ProjectMember + + permission_classes = [ + WorkspaceEntityPermission, + ] + + def get(self, request, slug): + # Fetch all project IDs where the user is involved + project_ids = ProjectMember.objects.filter( + member=request.user, + member__is_bot=False, + is_active=True, + ).values_list('project_id', flat=True).distinct() + + # Get all the project members in which the user is involved + project_members = ProjectMember.objects.filter( + workspace__slug=slug, + member__is_bot=False, + project_id__in=project_ids, + is_active=True, + ).select_related("project", "member", "workspace") + project_members = ProjectMemberRoleSerializer(project_members, many=True).data + + project_members_dict = dict() + + # Construct a dictionary with project_id as key and project_members as value + for project_member in project_members: + project_id = project_member.pop("project") + if str(project_id) not in project_members_dict: + project_members_dict[str(project_id)] = [] + project_members_dict[str(project_id)].append(project_member) + + return Response(project_members_dict, status=status.HTTP_200_OK) + + class TeamMemberViewSet(BaseViewSet): serializer_class = TeamSerializer model = Team @@ -1334,8 +1385,7 @@ def get(self, request, slug, user_id): issues = IssueLiteSerializer( issue_queryset, many=True, fields=fields if fields else None ).data - issue_dict = {str(issue["id"]): issue for issue in issues} - return Response(issue_dict, status=status.HTTP_200_OK) + return Response(issues, status=status.HTTP_200_OK) class WorkspaceLabelsEndpoint(BaseAPIView): @@ -1349,3 +1399,30 @@ def get(self, request, slug): project__project_projectmember__member=request.user, ).values("parent", "name", "color", "id", "project_id", "workspace__slug") return Response(labels, status=status.HTTP_200_OK) + + +class WorkspaceUserPropertiesEndpoint(BaseAPIView): + permission_classes = [ + WorkspaceViewerPermission, + ] + + def patch(self, request, slug): + workspace_properties = WorkspaceUserProperties.objects.get( + user=request.user, + workspace__slug=slug, + ) + + workspace_properties.filters = request.data.get("filters", workspace_properties.filters) + workspace_properties.display_filters = request.data.get("display_filters", workspace_properties.display_filters) + workspace_properties.display_properties = request.data.get("display_properties", workspace_properties.display_properties) + workspace_properties.save() + + serializer = WorkspaceUserPropertiesSerializer(workspace_properties) + return Response(serializer.data, status=status.HTTP_201_CREATED) + + def get(self, request, slug): + workspace_properties, _ = WorkspaceUserProperties.objects.get_or_create( + user=request.user, workspace__slug=slug + ) + serializer = WorkspaceUserPropertiesSerializer(workspace_properties) + return Response(serializer.data, status=status.HTTP_200_OK) \ No newline at end of file diff --git a/apiserver/plane/bgtasks/issue_activites_task.py b/apiserver/plane/bgtasks/issue_activites_task.py index 5d4c0650c30..2552ffbc58e 100644 --- a/apiserver/plane/bgtasks/issue_activites_task.py +++ b/apiserver/plane/bgtasks/issue_activites_task.py @@ -112,8 +112,16 @@ def track_parent( epoch, ): if current_instance.get("parent") != requested_data.get("parent"): - old_parent = Issue.objects.filter(pk=current_instance.get("parent")).first() if current_instance.get("parent") is not None else None - new_parent = Issue.objects.filter(pk=requested_data.get("parent")).first() if requested_data.get("parent") is not None else None + old_parent = ( + Issue.objects.filter(pk=current_instance.get("parent")).first() + if current_instance.get("parent") is not None + else None + ) + new_parent = ( + Issue.objects.filter(pk=requested_data.get("parent")).first() + if requested_data.get("parent") is not None + else None + ) issue_activities.append( IssueActivity( @@ -714,7 +722,9 @@ def create_cycle_issue_activity( cycle = Cycle.objects.filter( pk=created_record.get("fields").get("cycle") ).first() - issue = Issue.objects.filter(pk=created_record.get("fields").get("issue")).first() + issue = Issue.objects.filter( + pk=created_record.get("fields").get("issue") + ).first() if issue: issue.updated_at = timezone.now() issue.save(update_fields=["updated_at"]) @@ -830,7 +840,9 @@ def create_module_issue_activity( module = Module.objects.filter( pk=created_record.get("fields").get("module") ).first() - issue = Issue.objects.filter(pk=created_record.get("fields").get("issue")).first() + issue = Issue.objects.filter( + pk=created_record.get("fields").get("issue") + ).first() if issue: issue.updated_at = timezone.now() issue.save(update_fields=["updated_at"]) @@ -1276,40 +1288,42 @@ def create_issue_relation_activity( current_instance = ( json.loads(current_instance) if current_instance is not None else None ) - if current_instance is None and requested_data.get("related_list") is not None: - for issue_relation in requested_data.get("related_list"): - if issue_relation.get("relation_type") == "blocked_by": - relation_type = "blocking" - else: - relation_type = issue_relation.get("relation_type") - issue = Issue.objects.get(pk=issue_relation.get("issue")) + if current_instance is None and requested_data.get("issues") is not None: + for related_issue in requested_data.get("issues"): + issue = Issue.objects.get(pk=related_issue) issue_activities.append( IssueActivity( - issue_id=issue_relation.get("related_issue"), + issue_id=issue_id, actor_id=actor_id, verb="created", old_value="", new_value=f"{issue.project.identifier}-{issue.sequence_id}", - field=relation_type, + field=requested_data.get("relation_type"), project_id=project_id, workspace_id=workspace_id, - comment=f"added {relation_type} relation", - old_identifier=issue_relation.get("issue"), + comment=f"added {requested_data.get('relation_type')} relation", + old_identifier=related_issue, ) ) - issue = Issue.objects.get(pk=issue_relation.get("related_issue")) + issue = Issue.objects.get(pk=issue_id) issue_activities.append( IssueActivity( - issue_id=issue_relation.get("issue"), + issue_id=related_issue, actor_id=actor_id, verb="created", old_value="", new_value=f"{issue.project.identifier}-{issue.sequence_id}", - field=f'{issue_relation.get("relation_type")}', + field="blocking" + if requested_data.get("relation_type") == "blocked_by" + else ( + "blocked_by" + if requested_data.get("relation_type") == "blocking" + else requested_data.get("relation_type") + ), project_id=project_id, workspace_id=workspace_id, - comment=f'added {issue_relation.get("relation_type")} relation', - old_identifier=issue_relation.get("related_issue"), + comment=f'added {"blocking" if requested_data.get("relation_type") == "blocked_by" else ("blocked_by" if requested_data.get("relation_type") == "blocking" else requested_data.get("relation_type")),} relation', + old_identifier=issue_id, epoch=epoch, ) ) @@ -1329,44 +1343,44 @@ def delete_issue_relation_activity( current_instance = ( json.loads(current_instance) if current_instance is not None else None ) - if current_instance is not None and requested_data.get("related_list") is None: - if current_instance.get("relation_type") == "blocked_by": - relation_type = "blocking" - else: - relation_type = current_instance.get("relation_type") - issue = Issue.objects.get(pk=current_instance.get("issue")) - issue_activities.append( - IssueActivity( - issue_id=current_instance.get("related_issue"), - actor_id=actor_id, - verb="deleted", - old_value=f"{issue.project.identifier}-{issue.sequence_id}", - new_value="", - field=relation_type, - project_id=project_id, - workspace_id=workspace_id, - comment=f"deleted {relation_type} relation", - old_identifier=current_instance.get("issue"), - epoch=epoch, - ) + issue = Issue.objects.get(pk=requested_data.get("related_issue")) + issue_activities.append( + IssueActivity( + issue_id=issue_id, + actor_id=actor_id, + verb="deleted", + old_value=f"{issue.project.identifier}-{issue.sequence_id}", + new_value="", + field=requested_data.get("relation_type"), + project_id=project_id, + workspace_id=workspace_id, + comment=f"deleted {requested_data.get('relation_type')} relation", + old_identifier=requested_data.get("related_issue"), + epoch=epoch, ) - issue = Issue.objects.get(pk=current_instance.get("related_issue")) - issue_activities.append( - IssueActivity( - issue_id=current_instance.get("issue"), - actor_id=actor_id, - verb="deleted", - old_value=f"{issue.project.identifier}-{issue.sequence_id}", - new_value="", - field=f'{current_instance.get("relation_type")}', - project_id=project_id, - workspace_id=workspace_id, - comment=f'deleted {current_instance.get("relation_type")} relation', - old_identifier=current_instance.get("related_issue"), - epoch=epoch, - ) + ) + issue = Issue.objects.get(pk=issue_id) + issue_activities.append( + IssueActivity( + issue_id=requested_data.get("related_issue"), + actor_id=actor_id, + verb="deleted", + old_value=f"{issue.project.identifier}-{issue.sequence_id}", + new_value="", + field="blocking" + if requested_data.get("relation_type") == "blocked_by" + else ( + "blocked_by" + if requested_data.get("relation_type") == "blocking" + else requested_data.get("relation_type") + ), + project_id=project_id, + workspace_id=workspace_id, + comment=f'deleted {requested_data.get("relation_type")} relation', + old_identifier=requested_data.get("related_issue"), + epoch=epoch, ) - + ) def create_draft_issue_activity( requested_data, diff --git a/apiserver/plane/bgtasks/notification_task.py b/apiserver/plane/bgtasks/notification_task.py index 4bc27d3ee09..d33b883bb0b 100644 --- a/apiserver/plane/bgtasks/notification_task.py +++ b/apiserver/plane/bgtasks/notification_task.py @@ -291,6 +291,9 @@ def notifications(type, issue_id, project_id, actor_id, subscriber, issue_activi sender = "in_app:issue_activities:assigned" for issue_activity in issue_activities_created: + # Do not send notification for description update + if issue_activity.get("field") == "description": + continue; issue_comment = issue_activity.get("issue_comment") if issue_comment is not None: issue_comment = IssueComment.objects.get( @@ -341,7 +344,7 @@ def notifications(type, issue_id, project_id, actor_id, subscriber, issue_activi .order_by("-created_at") .first() ) - + actor = User.objects.get(pk=actor_id) for mention_id in comment_mentions: diff --git a/apiserver/plane/db/migrations/0051_remove_issueproperty_properties_and_more.py b/apiserver/plane/db/migrations/0051_remove_issueproperty_properties_and_more.py new file mode 100644 index 00000000000..b61122ef83b --- /dev/null +++ b/apiserver/plane/db/migrations/0051_remove_issueproperty_properties_and_more.py @@ -0,0 +1,136 @@ +# Generated by Django 4.2.7 on 2023-12-20 11:14 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import plane.db.models.cycle +import plane.db.models.issue +import plane.db.models.module +import plane.db.models.view +import plane.db.models.workspace +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('db', '0050_user_use_case_alter_workspace_organization_size'), + ] + + operations = [ + migrations.RenameField( + model_name='issueview', + old_name='query_data', + new_name='filters', + ), + migrations.RenameField( + model_name='issueproperty', + old_name='properties', + new_name='display_properties', + ), + migrations.AlterField( + model_name='issueproperty', + name='display_properties', + field=models.JSONField(default=plane.db.models.issue.get_default_display_properties), + ), + migrations.AddField( + model_name='issueproperty', + name='display_filters', + field=models.JSONField(default=plane.db.models.issue.get_default_display_filters), + ), + migrations.AddField( + model_name='issueproperty', + name='filters', + field=models.JSONField(default=plane.db.models.issue.get_default_filters), + ), + migrations.AddField( + model_name='issueview', + name='display_filters', + field=models.JSONField(default=plane.db.models.view.get_default_display_filters), + ), + migrations.AddField( + model_name='issueview', + name='display_properties', + field=models.JSONField(default=plane.db.models.view.get_default_display_properties), + ), + migrations.AddField( + model_name='issueview', + name='sort_order', + field=models.FloatField(default=65535), + ), + migrations.AlterField( + model_name='issueview', + name='project', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'), + ), + migrations.CreateModel( + name='WorkspaceUserProperties', + fields=[ + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), + ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), + ('filters', models.JSONField(default=plane.db.models.workspace.get_default_filters)), + ('display_filters', models.JSONField(default=plane.db.models.workspace.get_default_display_filters)), + ('display_properties', models.JSONField(default=plane.db.models.workspace.get_default_display_properties)), + ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), + ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_user_properties', to=settings.AUTH_USER_MODEL)), + ('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_user_properties', to='db.workspace')), + ], + options={ + 'verbose_name': 'Workspace User Property', + 'verbose_name_plural': 'Workspace User Property', + 'db_table': 'Workspace_user_properties', + 'ordering': ('-created_at',), + 'unique_together': {('workspace', 'user')}, + }, + ), + migrations.CreateModel( + name='ModuleUserProperties', + fields=[ + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), + ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), + ('filters', models.JSONField(default=plane.db.models.module.get_default_filters)), + ('display_filters', models.JSONField(default=plane.db.models.module.get_default_display_filters)), + ('display_properties', models.JSONField(default=plane.db.models.module.get_default_display_properties)), + ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), + ('module', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='module_user_properties', to='db.module')), + ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project')), + ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='module_user_properties', to=settings.AUTH_USER_MODEL)), + ('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace')), + ], + options={ + 'verbose_name': 'Module User Property', + 'verbose_name_plural': 'Module User Property', + 'db_table': 'module_user_properties', + 'ordering': ('-created_at',), + 'unique_together': {('module', 'user')}, + }, + ), + migrations.CreateModel( + name='CycleUserProperties', + fields=[ + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), + ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), + ('filters', models.JSONField(default=plane.db.models.cycle.get_default_filters)), + ('display_filters', models.JSONField(default=plane.db.models.cycle.get_default_display_filters)), + ('display_properties', models.JSONField(default=plane.db.models.cycle.get_default_display_properties)), + ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), + ('cycle', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='cycle_user_properties', to='db.cycle')), + ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project')), + ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='cycle_user_properties', to=settings.AUTH_USER_MODEL)), + ('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace')), + ], + options={ + 'verbose_name': 'Cycle User Property', + 'verbose_name_plural': 'Cycle User Properties', + 'db_table': 'cycle_user_properties', + 'ordering': ('-created_at',), + 'unique_together': {('cycle', 'user')}, + }, + ), + ] diff --git a/apiserver/plane/db/migrations/0052_auto_20231220_1141.py b/apiserver/plane/db/migrations/0052_auto_20231220_1141.py new file mode 100644 index 00000000000..b8386bf46be --- /dev/null +++ b/apiserver/plane/db/migrations/0052_auto_20231220_1141.py @@ -0,0 +1,65 @@ +# Generated by Django 4.2.7 on 2023-12-19 19:11 +from plane.db.models import WorkspaceUserProperties, ProjectMember, IssueView +from django.db import migrations + + +def workspace_user_properties(apps, schema_editor): + WorkspaceMember = apps.get_model("db", "WorkspaceMember") + updated_workspace_user_properties = [] + for workspace_members in WorkspaceMember.objects.all(): + updated_workspace_user_properties.append( + WorkspaceUserProperties( + user_id=workspace_members.member_id, + display_filters=workspace_members.view_props.get("display_filters"), + display_properties=workspace_members.view_props.get("display_properties"), + workspace_id=workspace_members.workspace_id, + ) + ) + WorkspaceUserProperties.objects.bulk_create(updated_workspace_user_properties, batch_size=2000) + + +def project_user_properties(apps, schema_editor): + IssueProperty = apps.get_model("db", "IssueProperty") + updated_issue_user_properties = [] + for issue_property in IssueProperty.objects.all(): + project_member = ProjectMember.objects.filter(project_id=issue_property.project_id, member_id=issue_property.user_id).first() + if project_member: + issue_property.filters = project_member.view_props.get("filters") + issue_property.display_filters = project_member.view_props.get("display_filters") + updated_issue_user_properties.append(issue_property) + + IssueProperty.objects.bulk_update(updated_issue_user_properties, ["filters", "display_filters"], batch_size=2000) + + +def issue_view(apps, schema_editor): + GlobalView = apps.get_model("db", "GlobalView") + updated_issue_views = [] + + for global_view in GlobalView.objects.all(): + updated_issue_views.append( + IssueView( + workspace_id=global_view.workspace_id, + name=global_view.name, + description=global_view.description, + query=global_view.query, + access=global_view.access, + filters=global_view.query_data.get("filters", {}), + sort_order=global_view.sort_order, + created_by_id=global_view.created_by_id, + updated_by_id=global_view.updated_by_id, + ) + ) + IssueView.objects.bulk_create(updated_issue_views, batch_size=100) + + +class Migration(migrations.Migration): + + dependencies = [ + ('db', '0051_remove_issueproperty_properties_and_more'), + ] + + operations = [ + migrations.RunPython(workspace_user_properties), + migrations.RunPython(project_user_properties), + migrations.RunPython(issue_view), + ] \ No newline at end of file diff --git a/apiserver/plane/db/models/__init__.py b/apiserver/plane/db/models/__init__.py index c76df6e5bfa..b88ee8e4688 100644 --- a/apiserver/plane/db/models/__init__.py +++ b/apiserver/plane/db/models/__init__.py @@ -9,6 +9,8 @@ WorkspaceMemberInvite, TeamMember, WorkspaceTheme, + WorkspaceUserProperties, + WorkspaceBaseModel, ) from .project import ( @@ -48,11 +50,11 @@ from .state import State -from .cycle import Cycle, CycleIssue, CycleFavorite +from .cycle import Cycle, CycleIssue, CycleFavorite, CycleUserProperties from .view import GlobalView, IssueView, IssueViewFavorite -from .module import Module, ModuleMember, ModuleIssue, ModuleLink, ModuleFavorite +from .module import Module, ModuleMember, ModuleIssue, ModuleLink, ModuleFavorite, ModuleUserProperties from .api import APIToken, APIActivityLog diff --git a/apiserver/plane/db/models/cycle.py b/apiserver/plane/db/models/cycle.py index e5e2c355b3b..a441057e1fa 100644 --- a/apiserver/plane/db/models/cycle.py +++ b/apiserver/plane/db/models/cycle.py @@ -6,6 +6,47 @@ from . import ProjectBaseModel +def get_default_filters(): + return { + "priority": None, + "state": None, + "state_group": None, + "assignees": None, + "created_by": None, + "labels": None, + "start_date": None, + "target_date": None, + "subscriber": None, + } + +def get_default_display_filters(): + return { + "group_by": None, + "order_by": "-created_at", + "type": None, + "sub_issue": True, + "show_empty_groups": True, + "layout": "list", + "calendar_date_range": "", + } + +def get_default_display_properties(): + return { + "assignee": True, + "attachment_count": True, + "created_on": True, + "due_date": True, + "estimate": True, + "key": True, + "labels": True, + "link": True, + "priority": True, + "start_date": True, + "state": True, + "sub_issue_count": True, + "updated_on": True, + } + class Cycle(ProjectBaseModel): name = models.CharField(max_length=255, verbose_name="Cycle Name") description = models.TextField(verbose_name="Cycle Description", blank=True) @@ -89,3 +130,28 @@ class Meta: def __str__(self): """Return user and the cycle""" return f"{self.user.email} <{self.cycle.name}>" + + +class CycleUserProperties(ProjectBaseModel): + cycle = models.ForeignKey( + "db.Cycle", on_delete=models.CASCADE, related_name="cycle_user_properties" + ) + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name="cycle_user_properties", + ) + filters = models.JSONField(default=get_default_filters) + display_filters = models.JSONField(default=get_default_display_filters) + display_properties = models.JSONField(default=get_default_display_properties) + + + class Meta: + unique_together = ["cycle", "user"] + verbose_name = "Cycle User Property" + verbose_name_plural = "Cycle User Properties" + db_table = "cycle_user_properties" + ordering = ("-created_at",) + + def __str__(self): + return f"{self.cycle.name} {self.user.email}" \ No newline at end of file diff --git a/apiserver/plane/db/models/issue.py b/apiserver/plane/db/models/issue.py index 54acd5c5d0b..b14376bc5aa 100644 --- a/apiserver/plane/db/models/issue.py +++ b/apiserver/plane/db/models/issue.py @@ -33,6 +33,48 @@ def get_default_properties(): } +def get_default_filters(): + return { + "priority": None, + "state": None, + "state_group": None, + "assignees": None, + "created_by": None, + "labels": None, + "start_date": None, + "target_date": None, + "subscriber": None, + } + +def get_default_display_filters(): + return { + "group_by": None, + "order_by": "-created_at", + "type": None, + "sub_issue": True, + "show_empty_groups": True, + "layout": "list", + "calendar_date_range": "", + } + +def get_default_display_properties(): + return { + "assignee": True, + "attachment_count": True, + "created_on": True, + "due_date": True, + "estimate": True, + "key": True, + "labels": True, + "link": True, + "priority": True, + "start_date": True, + "state": True, + "sub_issue_count": True, + "updated_on": True, + } + + # TODO: Handle identifiers for Bulk Inserts - nk class IssueManager(models.Manager): def get_queryset(self): @@ -394,7 +436,9 @@ class IssueProperty(ProjectBaseModel): on_delete=models.CASCADE, related_name="issue_property_user", ) - properties = models.JSONField(default=get_default_properties) + filters = models.JSONField(default=get_default_filters) + display_filters = models.JSONField(default=get_default_display_filters) + display_properties = models.JSONField(default=get_default_display_properties) class Meta: verbose_name = "Issue Property" diff --git a/apiserver/plane/db/models/module.py b/apiserver/plane/db/models/module.py index e485eea6257..cc8185946b5 100644 --- a/apiserver/plane/db/models/module.py +++ b/apiserver/plane/db/models/module.py @@ -6,6 +6,47 @@ from . import ProjectBaseModel +def get_default_filters(): + return { + "priority": None, + "state": None, + "state_group": None, + "assignees": None, + "created_by": None, + "labels": None, + "start_date": None, + "target_date": None, + "subscriber": None, + }, + +def get_default_display_filters(): + return { + "group_by": None, + "order_by": "-created_at", + "type": None, + "sub_issue": True, + "show_empty_groups": True, + "layout": "list", + "calendar_date_range": "", + } + +def get_default_display_properties(): + return { + "assignee": True, + "attachment_count": True, + "created_on": True, + "due_date": True, + "estimate": True, + "key": True, + "labels": True, + "link": True, + "priority": True, + "start_date": True, + "state": True, + "sub_issue_count": True, + "updated_on": True, + } + class Module(ProjectBaseModel): name = models.CharField(max_length=255, verbose_name="Module Name") description = models.TextField(verbose_name="Module Description", blank=True) @@ -141,3 +182,28 @@ class Meta: def __str__(self): """Return user and the module""" return f"{self.user.email} <{self.module.name}>" + + +class ModuleUserProperties(ProjectBaseModel): + module = models.ForeignKey( + "db.Module", on_delete=models.CASCADE, related_name="module_user_properties" + ) + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name="module_user_properties", + ) + filters = models.JSONField(default=get_default_filters) + display_filters = models.JSONField(default=get_default_display_filters) + display_properties = models.JSONField(default=get_default_display_properties) + + + class Meta: + unique_together = ["module", "user"] + verbose_name = "Module User Property" + verbose_name_plural = "Module User Property" + db_table = "module_user_properties" + ordering = ("-created_at",) + + def __str__(self): + return f"{self.module.name} {self.user.email}" \ No newline at end of file diff --git a/apiserver/plane/db/models/view.py b/apiserver/plane/db/models/view.py index 44bc994d0c8..8a77f058625 100644 --- a/apiserver/plane/db/models/view.py +++ b/apiserver/plane/db/models/view.py @@ -3,9 +3,50 @@ from django.conf import settings # Module import -from . import ProjectBaseModel, BaseModel +from . import ProjectBaseModel, BaseModel, WorkspaceBaseModel +def get_default_filters(): + return { + "priority": None, + "state": None, + "state_group": None, + "assignees": None, + "created_by": None, + "labels": None, + "start_date": None, + "target_date": None, + "subscriber": None, + } + +def get_default_display_filters(): + return { + "group_by": None, + "order_by": "-created_at", + "type": None, + "sub_issue": True, + "show_empty_groups": True, + "layout": "list", + "calendar_date_range": "", + } + +def get_default_display_properties(): + return { + "assignee": True, + "attachment_count": True, + "created_on": True, + "due_date": True, + "estimate": True, + "key": True, + "labels": True, + "link": True, + "priority": True, + "start_date": True, + "state": True, + "sub_issue_count": True, + "updated_on": True, + } + class GlobalView(BaseModel): workspace = models.ForeignKey( "db.Workspace", on_delete=models.CASCADE, related_name="global_views" @@ -40,14 +81,17 @@ def __str__(self): return f"{self.name} <{self.workspace.name}>" -class IssueView(ProjectBaseModel): +class IssueView(WorkspaceBaseModel): name = models.CharField(max_length=255, verbose_name="View Name") description = models.TextField(verbose_name="View Description", blank=True) query = models.JSONField(verbose_name="View Query") + filters = models.JSONField(default=dict) + display_filters = models.JSONField(default=get_default_display_filters) + display_properties = models.JSONField(default=get_default_display_properties) access = models.PositiveSmallIntegerField( default=1, choices=((0, "Private"), (1, "Public")) ) - query_data = models.JSONField(default=dict) + sort_order = models.FloatField(default=65535) class Meta: verbose_name = "Issue View" diff --git a/apiserver/plane/db/models/workspace.py b/apiserver/plane/db/models/workspace.py index 505bfbcfa63..f0d64ecaedc 100644 --- a/apiserver/plane/db/models/workspace.py +++ b/apiserver/plane/db/models/workspace.py @@ -54,6 +54,51 @@ def get_default_props(): }, } +def get_default_filters(): + return { + "priority": None, + "state": None, + "state_group": None, + "assignees": None, + "created_by": None, + "labels": None, + "start_date": None, + "target_date": None, + "subscriber": None, + } + +def get_default_display_filters(): + return { + "display_filters": { + "group_by": None, + "order_by": "-created_at", + "type": None, + "sub_issue": True, + "show_empty_groups": True, + "layout": "list", + "calendar_date_range": "", + }, + } + +def get_default_display_properties(): + return { + "display_properties": { + "assignee": True, + "attachment_count": True, + "created_on": True, + "due_date": True, + "estimate": True, + "key": True, + "labels": True, + "link": True, + "priority": True, + "start_date": True, + "state": True, + "sub_issue_count": True, + "updated_on": True, + }, + } + def get_issue_props(): return { @@ -103,6 +148,22 @@ class Meta: ordering = ("-created_at",) +class WorkspaceBaseModel(BaseModel): + workspace = models.ForeignKey( + "db.Workspace", models.CASCADE, related_name="workspace_%(class)s" + ) + project = models.ForeignKey( + "db.Project", models.CASCADE, related_name="project_%(class)s", null=True + ) + + class Meta: + abstract = True + + def save(self, *args, **kwargs): + if self.project: + self.workspace = self.project.workspace + super(WorkspaceBaseModel, self).save(*args, **kwargs) + class WorkspaceMember(BaseModel): workspace = models.ForeignKey( "db.Workspace", on_delete=models.CASCADE, related_name="workspace_member" @@ -218,3 +279,28 @@ class Meta: verbose_name_plural = "Workspace Themes" db_table = "workspace_themes" ordering = ("-created_at",) + + +class WorkspaceUserProperties(BaseModel): + workspace = models.ForeignKey( + "db.Workspace", on_delete=models.CASCADE, related_name="workspace_user_properties" + ) + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name="workspace_user_properties", + ) + filters = models.JSONField(default=get_default_filters) + display_filters = models.JSONField(default=get_default_display_filters) + display_properties = models.JSONField(default=get_default_display_properties) + + + class Meta: + unique_together = ["workspace", "user"] + verbose_name = "Workspace User Property" + verbose_name_plural = "Workspace User Property" + db_table = "Workspace_user_properties" + ordering = ("-created_at",) + + def __str__(self): + return f"{self.workspace.name} {self.user.email}" \ No newline at end of file diff --git a/apiserver/requirements/base.txt b/apiserver/requirements/base.txt index 0e7a18fa86b..6832297e975 100644 --- a/apiserver/requirements/base.txt +++ b/apiserver/requirements/base.txt @@ -30,7 +30,7 @@ openpyxl==3.1.2 beautifulsoup4==4.12.2 dj-database-url==2.1.0 posthog==3.0.2 -cryptography==41.0.6 +cryptography==41.0.5 lxml==4.9.3 boto3==1.28.40 diff --git a/deploy/selfhost/install.sh b/deploy/selfhost/install.sh index 15150aa40e7..645e99cb8e6 100755 --- a/deploy/selfhost/install.sh +++ b/deploy/selfhost/install.sh @@ -39,7 +39,7 @@ function download(){ echo "" echo "Latest version is now available for you to use" echo "" - echo "In case of Upgrade, your new setting file is available as 'variables-upgrade.env'. Please compare and set the required values in '.env 'file." + echo "In case of Upgrade, your new setting file is availabe as 'variables-upgrade.env'. Please compare and set the required values in '.env 'file." echo "" } diff --git a/package.json b/package.json index b5d99766252..aad104784f2 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "packages/eslint-config-custom", "packages/tailwind-config-custom", "packages/tsconfig", - "packages/ui" + "packages/ui", + "packages/types" ], "scripts": { "build": "turbo run build", diff --git a/packages/types/package.json b/packages/types/package.json new file mode 100644 index 00000000000..a9dfbb8e0aa --- /dev/null +++ b/packages/types/package.json @@ -0,0 +1,7 @@ +{ + "name": "@plane/types", + "version": "0.14.0", + "private": true, + "main": "./src/index.d.ts" +} + \ No newline at end of file diff --git a/web/types/ai.d.ts b/packages/types/src/ai.d.ts similarity index 73% rename from web/types/ai.d.ts rename to packages/types/src/ai.d.ts index 6c933a03397..ce8bcbadb2f 100644 --- a/web/types/ai.d.ts +++ b/packages/types/src/ai.d.ts @@ -1,4 +1,4 @@ -import { IProjectLite, IWorkspaceLite } from "types"; +import { IProjectLite, IWorkspaceLite } from "@plane/types"; export interface IGptResponse { response: string; diff --git a/web/types/analytics.d.ts b/packages/types/src/analytics.d.ts similarity index 100% rename from web/types/analytics.d.ts rename to packages/types/src/analytics.d.ts diff --git a/web/types/api_token.d.ts b/packages/types/src/api_token.d.ts similarity index 100% rename from web/types/api_token.d.ts rename to packages/types/src/api_token.d.ts diff --git a/web/types/app.d.ts b/packages/types/src/app.d.ts similarity index 77% rename from web/types/app.d.ts rename to packages/types/src/app.d.ts index 0122cf73a7e..4d938ce2618 100644 --- a/web/types/app.d.ts +++ b/packages/types/src/app.d.ts @@ -1,6 +1,4 @@ -export type NextPageWithLayout

= NextPage & { - getLayout?: (page: ReactElement) => ReactNode; -}; + export interface IAppConfig { email_password_login: boolean; diff --git a/web/types/auth.d.ts b/packages/types/src/auth.d.ts similarity index 100% rename from web/types/auth.d.ts rename to packages/types/src/auth.d.ts diff --git a/web/types/calendar.ts b/packages/types/src/calendar.d.ts similarity index 100% rename from web/types/calendar.ts rename to packages/types/src/calendar.d.ts diff --git a/web/types/cycles.d.ts b/packages/types/src/cycles.d.ts similarity index 91% rename from web/types/cycles.d.ts rename to packages/types/src/cycles.d.ts index 4f243deeb23..6723b39465d 100644 --- a/web/types/cycles.d.ts +++ b/packages/types/src/cycles.d.ts @@ -1,4 +1,4 @@ -import type { IUser, IIssue, IProjectLite, IWorkspaceLite, IIssueFilterOptions, IUserLite } from "types"; +import type { IUser, TIssue, IProjectLite, IWorkspaceLite, IIssueFilterOptions, IUserLite } from "@plane/types"; export type TCycleView = "all" | "active" | "upcoming" | "completed" | "draft"; @@ -68,7 +68,7 @@ export type TLabelsDistribution = { export interface CycleIssueResponse { id: string; - issue_detail: IIssue; + issue_detail: TIssue; created_at: Date; updated_at: Date; created_by: string; @@ -82,7 +82,7 @@ export interface CycleIssueResponse { export type SelectCycleType = (ICycle & { actionType: "edit" | "delete" | "create-issue" }) | undefined; -export type SelectIssue = (IIssue & { actionType: "edit" | "delete" | "create" }) | null; +export type SelectIssue = (TIssue & { actionType: "edit" | "delete" | "create" }) | null; export type CycleDateCheckData = { start_date: string; diff --git a/web/types/estimate.d.ts b/packages/types/src/estimate.d.ts similarity index 100% rename from web/types/estimate.d.ts rename to packages/types/src/estimate.d.ts index 32925c79328..96b584ce1fc 100644 --- a/web/types/estimate.d.ts +++ b/packages/types/src/estimate.d.ts @@ -1,24 +1,24 @@ export interface IEstimate { - id: string; created_at: Date; - updated_at: Date; - name: string; - description: string; created_by: string; - updated_by: string; - points: IEstimatePoint[]; + description: string; + id: string; + name: string; project: string; project_detail: IProject; + updated_at: Date; + updated_by: string; + points: IEstimatePoint[]; workspace: string; workspace_detail: IWorkspace; } export interface IEstimatePoint { - id: string; created_at: string; created_by: string; description: string; estimate: string; + id: string; key: number; project: string; updated_at: string; diff --git a/web/types/importer/github-importer.d.ts b/packages/types/src/importer/github-importer.d.ts similarity index 100% rename from web/types/importer/github-importer.d.ts rename to packages/types/src/importer/github-importer.d.ts diff --git a/web/types/importer/index.ts b/packages/types/src/importer/index.d.ts similarity index 92% rename from web/types/importer/index.ts rename to packages/types/src/importer/index.d.ts index 81e1bb22fab..877c0719690 100644 --- a/web/types/importer/index.ts +++ b/packages/types/src/importer/index.d.ts @@ -1,9 +1,9 @@ export * from "./github-importer"; export * from "./jira-importer"; -import { IProjectLite } from "types/projects"; +import { IProjectLite } from "../projects"; // types -import { IUserLite } from "types/users"; +import { IUserLite } from "../users"; export interface IImporterService { created_at: string; diff --git a/web/types/importer/jira-importer.d.ts b/packages/types/src/importer/jira-importer.d.ts similarity index 100% rename from web/types/importer/jira-importer.d.ts rename to packages/types/src/importer/jira-importer.d.ts diff --git a/web/types/inbox.d.ts b/packages/types/src/inbox.d.ts similarity index 93% rename from web/types/inbox.d.ts rename to packages/types/src/inbox.d.ts index 10fc37b31f1..1b474c3abaa 100644 --- a/web/types/inbox.d.ts +++ b/packages/types/src/inbox.d.ts @@ -1,7 +1,7 @@ -import { IIssue } from "./issues"; +import { TIssue } from "./issues"; import type { IProjectLite } from "./projects"; -export interface IInboxIssue extends IIssue { +export interface IInboxIssue extends TIssue { issue_inbox: { duplicate_to: string | null; id: string; diff --git a/web/types/index.d.ts b/packages/types/src/index.d.ts similarity index 81% rename from web/types/index.d.ts rename to packages/types/src/index.d.ts index 9f27e818ca1..4bbed28d341 100644 --- a/web/types/index.d.ts +++ b/packages/types/src/index.d.ts @@ -21,6 +21,11 @@ export * from "./reaction"; export * from "./view-props"; export * from "./workspace-views"; export * from "./webhook"; +export * from "./issues/base"; // TODO: Remove this after development and the refactor/mobx-store-issue branch is stable +export * from "./auth"; +export * from "./api_token"; +export * from "./instance"; +export * from "./app"; export type NestedKeyOf = { [Key in keyof ObjectType & (string | number)]: ObjectType[Key] extends object diff --git a/web/types/instance.d.ts b/packages/types/src/instance.d.ts similarity index 100% rename from web/types/instance.d.ts rename to packages/types/src/instance.d.ts diff --git a/web/types/integration.d.ts b/packages/types/src/integration.d.ts similarity index 100% rename from web/types/integration.d.ts rename to packages/types/src/integration.d.ts diff --git a/web/types/issues.d.ts b/packages/types/src/issues.d.ts similarity index 72% rename from web/types/issues.d.ts rename to packages/types/src/issues.d.ts index 09f21eb3a5d..c0ad7bc7f75 100644 --- a/web/types/issues.d.ts +++ b/packages/types/src/issues.d.ts @@ -1,3 +1,4 @@ +import { ReactElement } from "react"; import { KeyedMutator } from "swr"; import type { IState, @@ -10,7 +11,8 @@ import type { IStateLite, Properties, IIssueDisplayFilterOptions, -} from "types"; + IIssueReaction, +} from "@plane/types"; export interface IIssueCycle { id: string; @@ -83,6 +85,7 @@ export interface IIssue { attachment_count: number; attachments: any[]; issue_relations: IssueRelation[]; + issue_reactions: IIssueReaction[]; related_issues: IssueRelation[]; bridge_id?: string | null; completed_at: Date; @@ -138,7 +141,7 @@ export interface ISubIssuesState { export interface ISubIssueResponse { state_distribution: ISubIssuesState; - sub_issues: IIssue[]; + sub_issues: TIssue[]; } export interface BlockeIssueDetail { @@ -240,13 +243,13 @@ export interface IIssueAttachment { } export interface IIssueViewProps { - groupedIssues: { [key: string]: IIssue[] } | undefined; + groupedIssues: { [key: string]: TIssue[] } | undefined; displayFilters: IIssueDisplayFilterOptions | undefined; isEmpty: boolean; mutateIssues: KeyedMutator< - | IIssue[] + | TIssue[] | { - [key: string]: IIssue[]; + [key: string]: TIssue[]; } >; params: any; @@ -254,3 +257,88 @@ export interface IIssueViewProps { } export type TIssuePriorities = "urgent" | "high" | "medium" | "low" | "none"; + +export interface ViewFlags { + enableQuickAdd: boolean; + enableIssueCreation: boolean; + enableInlineEditing: boolean; +} + +export type GroupByColumnTypes = + | "project" + | "state" + | "state_detail.group" + | "priority" + | "labels" + | "assignees" + | "created_by"; + +export interface IGroupByColumn { + id: string; + name: string; + Icon: ReactElement | undefined; + payload: Partial; +} + +export interface IIssueMap { + [key: string]: TIssue; +} + +// new issue structure types +export type TIssue = { + id: string; + name: string; + state_id: string; + description_html: string; + sort_order: number; + completed_at: string | null; + estimate_point: number | null; + priority: TIssuePriorities; + start_date: string | null; + target_date: string | null; + sequence_id: number; + project_id: string; + parent_id: string | null; + cycle_id: string | null; + module_id: string | null; + label_ids: string[]; + assignee_ids: string[]; + sub_issues_count: number; + created_at: string; + updated_at: string; + created_by: string; + updated_by: string; + attachment_count: number; + link_count: number; + is_subscribed: boolean; + archived_at: boolean; + is_draft: boolean; + // tempId is used for optimistic updates. It is not a part of the API response. + tempId?: string; + // issue details + related_issues: any; + issue_reactions: any; + issue_relations: any; + issue_cycle: any; + issue_module: any; + parent_detail: any; + issue_link: any; +}; + +export type TIssueMap = { + [issue_id: string]: TIssue; +}; + +export type TLoader = "init-loader" | "mutation" | undefined; + +export type TGroupedIssues = { + [group_id: string]: string[]; +}; + +export type TSubGroupedIssues = { + [sub_grouped_id: string]: { + [group_id: string]: string[]; + }; +}; + +export type TUnGroupedIssues = string[]; diff --git a/packages/types/src/issues/base.d.ts b/packages/types/src/issues/base.d.ts new file mode 100644 index 00000000000..08daceb1634 --- /dev/null +++ b/packages/types/src/issues/base.d.ts @@ -0,0 +1,23 @@ +// issues +export * from "./issue"; +export * from "./issue_reaction"; +export * from "./issue_link"; +export * from "./issue_attachment"; +export * from "./issue_relation"; +export * from "./issue_activity"; +export * from "./issue_comment_reaction"; +export * from "./issue_sub_issues"; + +export type TLoader = "init-loader" | "mutation" | undefined; + +export type TGroupedIssues = { + [group_id: string]: string[]; +}; + +export type TSubGroupedIssues = { + [sub_grouped_id: string]: { + [group_id: string]: string[]; + }; +}; + +export type TUnGroupedIssues = string[]; diff --git a/packages/types/src/issues/issue.d.ts b/packages/types/src/issues/issue.d.ts new file mode 100644 index 00000000000..e9ec14528a6 --- /dev/null +++ b/packages/types/src/issues/issue.d.ts @@ -0,0 +1,36 @@ +// new issue structure types +export type TIssue = { + id: string; + name: string; + state_id: string; + description_html: string; + sort_order: number; + completed_at: string | null; + estimate_point: number | null; + priority: TIssuePriorities; + start_date: string; + target_date: string; + sequence_id: number; + project_id: string; + parent_id: string | null; + cycle_id: string | null; + module_id: string | null; + label_ids: string[]; + assignee_ids: string[]; + sub_issues_count: number; + created_at: string; + updated_at: string; + created_by: string; + updated_by: string; + attachment_count: number; + link_count: number; + is_subscribed: boolean; + archived_at: boolean; + is_draft: boolean; + // tempId is used for optimistic updates. It is not a part of the API response. + tempId?: string; +}; + +export type TIssueMap = { + [issue_id: string]: TIssue; +}; diff --git a/packages/types/src/issues/issue_activity.d.ts b/packages/types/src/issues/issue_activity.d.ts new file mode 100644 index 00000000000..2ce22b361a0 --- /dev/null +++ b/packages/types/src/issues/issue_activity.d.ts @@ -0,0 +1,41 @@ +export type TIssueActivity = { + access?: "EXTERNAL" | "INTERNAL"; + actor: string; + actor_detail: IUserLite; + attachments: any[]; + comment?: string; + comment_html?: string; + comment_stripped?: string; + created_at: Date; + created_by: string; + field: string | null; + id: string; + issue: string | null; + issue_comment?: string | null; + issue_detail: { + description_html: string; + id: string; + name: string; + priority: string | null; + sequence_id: string; + } | null; + new_identifier: string | null; + new_value: string | null; + old_identifier: string | null; + old_value: string | null; + project: string; + project_detail: IProjectLite; + updated_at: Date; + updated_by: string; + verb: string; + workspace: string; + workspace_detail?: IWorkspaceLite; +}; + +export type TIssueActivityMap = { + [issue_id: string]: TIssueActivity; +}; + +export type TIssueActivityIdMap = { + [issue_id: string]: string[]; +}; diff --git a/packages/types/src/issues/issue_attachment.d.ts b/packages/types/src/issues/issue_attachment.d.ts new file mode 100644 index 00000000000..90daa08faeb --- /dev/null +++ b/packages/types/src/issues/issue_attachment.d.ts @@ -0,0 +1,23 @@ +export type TIssueAttachment = { + id: string; + created_at: string; + updated_at: string; + attributes: { + name: string; + size: number; + }; + asset: string; + created_by: string; + updated_by: string; + project: string; + workspace: string; + issue: string; +}; + +export type TIssueAttachmentMap = { + [issue_id: string]: TIssueAttachment; +}; + +export type TIssueAttachmentIdMap = { + [issue_id: string]: string[]; +}; diff --git a/packages/types/src/issues/issue_comment_reaction.d.ts b/packages/types/src/issues/issue_comment_reaction.d.ts new file mode 100644 index 00000000000..8a3695e8518 --- /dev/null +++ b/packages/types/src/issues/issue_comment_reaction.d.ts @@ -0,0 +1,20 @@ +export type TIssueCommentReaction = { + id: string; + created_at: Date; + updated_at: Date; + reaction: string; + created_by: string; + updated_by: string; + project: string; + workspace: string; + actor: string; + comment: string; +}; + +export type TIssueCommentReactionMap = { + [issue_id: string]: TIssueCommentReaction; +}; + +export type TIssueCommentReactionIdMap = { + [issue_id: string]: string[]; +}; diff --git a/packages/types/src/issues/issue_link.d.ts b/packages/types/src/issues/issue_link.d.ts new file mode 100644 index 00000000000..2c469e6829b --- /dev/null +++ b/packages/types/src/issues/issue_link.d.ts @@ -0,0 +1,20 @@ +export type TIssueLinkEditableFields = { + title: string; + url: string; +}; + +export type TIssueLink = TIssueLinkEditableFields & { + created_at: Date; + created_by: string; + created_by_detail: IUserLite; + id: string; + metadata: any; +}; + +export type TIssueLinkMap = { + [issue_id: string]: TIssueLink; +}; + +export type TIssueLinkIdMap = { + [issue_id: string]: string[]; +}; diff --git a/packages/types/src/issues/issue_reaction.d.ts b/packages/types/src/issues/issue_reaction.d.ts new file mode 100644 index 00000000000..2fe64624621 --- /dev/null +++ b/packages/types/src/issues/issue_reaction.d.ts @@ -0,0 +1,21 @@ +export type TIssueReaction = { + actor: string; + actor_detail: IUserLite; + created_at: Date; + created_by: string; + id: string; + issue: string; + project: string; + reaction: string; + updated_at: Date; + updated_by: string; + workspace: string; +}; + +export type TIssueReactionMap = { + [issue_id: string]: TIssueReaction; +}; + +export type TIssueReactionIdMap = { + [issue_id: string]: string[]; +}; diff --git a/packages/types/src/issues/issue_relation.d.ts b/packages/types/src/issues/issue_relation.d.ts new file mode 100644 index 00000000000..0d959ff6bfd --- /dev/null +++ b/packages/types/src/issues/issue_relation.d.ts @@ -0,0 +1,20 @@ +import { TIssue } from "./issues"; + +export type TIssueRelationTypes = + | "blocking" + | "blocked_by" + | "duplicate" + | "relates_to"; + +export type TIssueRelationObject = { issue_detail: TIssue }; + +export type TIssueRelation = Record< + TIssueRelationTypes, + TIssueRelationObject[] +>; + +export type TIssueRelationMap = { + [issue_id: string]: Record; +}; + +export type TIssueRelationIdMap = Record; diff --git a/packages/types/src/issues/issue_sub_issues.d.ts b/packages/types/src/issues/issue_sub_issues.d.ts new file mode 100644 index 00000000000..76dcf1288ed --- /dev/null +++ b/packages/types/src/issues/issue_sub_issues.d.ts @@ -0,0 +1,22 @@ +import { TIssue } from "./issue"; + +export type TSubIssuesStateDistribution = { + backlog: number; + unstarted: number; + started: number; + completed: number; + cancelled: number; +}; + +export type TIssueSubIssues = { + state_distribution: TSubIssuesStateDistribution; + sub_issues: TIssue[]; +}; + +export type TIssueSubIssuesStateDistributionMap = { + [issue_id: string]: TSubIssuesStateDistribution; +}; + +export type TIssueSubIssuesIdMap = { + [issue_id: string]: string[]; +}; diff --git a/packages/types/src/issues/issue_subscription.d.ts b/packages/types/src/issues/issue_subscription.d.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/web/types/modules.d.ts b/packages/types/src/modules.d.ts similarity index 93% rename from web/types/modules.d.ts rename to packages/types/src/modules.d.ts index 733b8f7ded9..0e49da7fe07 100644 --- a/web/types/modules.d.ts +++ b/packages/types/src/modules.d.ts @@ -1,14 +1,14 @@ import type { IUser, IUserLite, - IIssue, + TIssue, IProject, IWorkspace, IWorkspaceLite, IProjectLite, IIssueFilterOptions, ILinkDetails, -} from "types"; +} from "@plane/types"; export type TModuleStatus = "backlog" | "planned" | "in-progress" | "paused" | "completed" | "cancelled"; @@ -58,7 +58,7 @@ export interface ModuleIssueResponse { created_by: string; id: string; issue: string; - issue_detail: IIssue; + issue_detail: TIssue; module: string; module_detail: IModule; project: string; @@ -75,4 +75,4 @@ export type ModuleLink = { export type SelectModuleType = (IModule & { actionType: "edit" | "delete" | "create-issue" }) | undefined; -export type SelectIssue = (IIssue & { actionType: "edit" | "delete" | "create" }) | undefined; +export type SelectIssue = (TIssue & { actionType: "edit" | "delete" | "create" }) | undefined; diff --git a/web/types/notifications.d.ts b/packages/types/src/notifications.d.ts similarity index 100% rename from web/types/notifications.d.ts rename to packages/types/src/notifications.d.ts diff --git a/web/types/pages.d.ts b/packages/types/src/pages.d.ts similarity index 79% rename from web/types/pages.d.ts rename to packages/types/src/pages.d.ts index a1c241f6a1a..29552b94cf6 100644 --- a/web/types/pages.d.ts +++ b/packages/types/src/pages.d.ts @@ -1,5 +1,5 @@ // types -import { IIssue, IIssueLabel, IWorkspaceLite, IProjectLite } from "types"; +import { TIssue, IIssueLabel, IWorkspaceLite, IProjectLite } from "@plane/types"; export interface IPage { access: number; @@ -27,15 +27,11 @@ export interface IPage { } export interface IRecentPages { - today: IPage[]; - yesterday: IPage[]; - this_week: IPage[]; - older: IPage[]; - [key: string]: IPage[]; -} - -export interface RecentPagesResponse { - [key: string]: IPage[]; + today: string[]; + yesterday: string[]; + this_week: string[]; + older: string[]; + [key: string]: string[]; } export interface IPageBlock { @@ -47,7 +43,7 @@ export interface IPageBlock { description_stripped: any; id: string; issue: string | null; - issue_detail: IIssue | null; + issue_detail: TIssue | null; name: string; page: string; project: string; diff --git a/web/types/projects.d.ts b/packages/types/src/projects.d.ts similarity index 78% rename from web/types/projects.d.ts rename to packages/types/src/projects.d.ts index 129b0bb3b82..a412180b80a 100644 --- a/web/types/projects.d.ts +++ b/packages/types/src/projects.d.ts @@ -1,6 +1,5 @@ -import type { IUserLite, IWorkspace, IWorkspaceLite, IUserMemberLite, TStateGroups, IProjectViewProps } from "."; - -export type TUserProjectRole = 5 | 10 | 15 | 20; +import { EUserProjectRoles } from "constants/project"; +import type { IUser, IUserLite, IWorkspace, IWorkspaceLite, TStateGroups } from "."; export interface IProject { archive_in: number; @@ -34,13 +33,10 @@ export interface IProject { is_deployed: boolean; is_favorite: boolean; is_member: boolean; - member_role: TUserProjectRole | null; + member_role: EUserProjectRoles | null; members: IProjectMemberLite[]; - issue_views_view: boolean; - module_view: boolean; name: string; network: number; - page_view: boolean; project_lead: IUserLite | string | null; sort_order: number | null; total_cycles: number; @@ -64,6 +60,10 @@ type ProjectPreferences = { }; }; +export interface IProjectMap { + [id: string]: IProject; +} + export interface IProjectMemberLite { id: string; member__avatar: string; @@ -77,7 +77,7 @@ export interface IProjectMember { project: IProjectLite; workspace: IWorkspaceLite; comment: string; - role: TUserProjectRole; + role: EUserProjectRoles; preferences: ProjectPreferences; @@ -90,27 +90,14 @@ export interface IProjectMember { updated_by: string; } -export interface IProjectMemberInvitation { +export interface IProjectMembership { id: string; - - project: IProject; - workspace: IWorkspace; - - email: string; - accepted: boolean; - token: string; - message: string; - responded_at: Date; - role: TUserProjectRole; - - created_at: Date; - updated_at: Date; - created_by: string; - updated_by: string; + member: string; + role: EUserProjectRoles; } export interface IProjectBulkAddFormData { - members: { role: TUserProjectRole; member_id: string }[]; + members: { role: EUserProjectRoles; member_id: string }[]; } export interface IGithubRepository { diff --git a/web/types/reaction.d.ts b/packages/types/src/reaction.d.ts similarity index 100% rename from web/types/reaction.d.ts rename to packages/types/src/reaction.d.ts diff --git a/web/types/state.d.ts b/packages/types/src/state.d.ts similarity index 90% rename from web/types/state.d.ts rename to packages/types/src/state.d.ts index 3fdbaa2d323..822b99f17e5 100644 --- a/web/types/state.d.ts +++ b/packages/types/src/state.d.ts @@ -1,4 +1,4 @@ -import { IProject, IProjectLite, IWorkspaceLite } from "types"; +import { IProject, IProjectLite, IWorkspaceLite } from "@plane/types"; export type TStateGroups = "backlog" | "unstarted" | "started" | "completed" | "cancelled"; diff --git a/web/types/users.d.ts b/packages/types/src/users.d.ts similarity index 97% rename from web/types/users.d.ts rename to packages/types/src/users.d.ts index 301c1d7c0ab..bbca953f615 100644 --- a/web/types/users.d.ts +++ b/packages/types/src/users.d.ts @@ -1,3 +1,4 @@ +import { EUserProjectRoles } from "constants/project"; import { IIssueActivity, IIssueLite, TStateGroups } from "."; export interface IUser { @@ -61,11 +62,10 @@ export interface IUserTheme { export interface IUserLite { avatar: string; - created_at: Date; display_name: string; email?: string; first_name: string; - readonly id: string; + id: string; is_bot: boolean; last_name: string; } @@ -163,7 +163,7 @@ export interface IUserProfileProjectSegregation { } export interface IUserProjectsRole { - [project_id: string]: number; + [projectId: string]: EUserProjectRoles; } // export interface ICurrentUser { diff --git a/web/types/view-props.d.ts b/packages/types/src/view-props.d.ts similarity index 91% rename from web/types/view-props.d.ts rename to packages/types/src/view-props.d.ts index c8c47576b9a..282fc5a9c7f 100644 --- a/web/types/view-props.d.ts +++ b/packages/types/src/view-props.d.ts @@ -108,6 +108,18 @@ export interface IIssueDisplayProperties { updated_on?: boolean; } +export interface IIssueFilters { + filters: IIssueFilterOptions | undefined; + displayFilters: IIssueDisplayFilterOptions | undefined; + displayProperties: IIssueDisplayProperties | undefined; +} + +export interface IIssueFiltersResponse { + filters: IIssueFilterOptions; + display_filters: IIssueDisplayFilterOptions; + display_properties: IIssueDisplayProperties; +} + export interface IWorkspaceIssueFilterOptions { assignees?: string[] | null; created_by?: string[] | null; diff --git a/web/types/views.d.ts b/packages/types/src/views.d.ts similarity index 58% rename from web/types/views.d.ts rename to packages/types/src/views.d.ts index 4f55e8c7459..db30554a847 100644 --- a/web/types/views.d.ts +++ b/packages/types/src/views.d.ts @@ -1,4 +1,4 @@ -import { IIssueFilterOptions } from "./view-props"; +import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "./view-props"; export interface IProjectView { id: string; @@ -10,6 +10,9 @@ export interface IProjectView { updated_by: string; name: string; description: string; + filters: IIssueFilterOptions; + display_filters: IIssueDisplayFilterOptions; + display_properties: IIssueDisplayProperties; query: IIssueFilterOptions; query_data: IIssueFilterOptions; project: string; diff --git a/web/types/waitlist.d.ts b/packages/types/src/waitlist.d.ts similarity index 100% rename from web/types/waitlist.d.ts rename to packages/types/src/waitlist.d.ts diff --git a/web/types/webhook.d.ts b/packages/types/src/webhook.d.ts similarity index 100% rename from web/types/webhook.d.ts rename to packages/types/src/webhook.d.ts diff --git a/web/types/workspace-views.d.ts b/packages/types/src/workspace-views.d.ts similarity index 64% rename from web/types/workspace-views.d.ts rename to packages/types/src/workspace-views.d.ts index 754e637118e..29aa56742c6 100644 --- a/web/types/workspace-views.d.ts +++ b/packages/types/src/workspace-views.d.ts @@ -1,4 +1,9 @@ -import { IWorkspaceViewProps } from "./view-props"; +import { + IWorkspaceViewProps, + IIssueDisplayFilterOptions, + IIssueDisplayProperties, + IIssueFilterOptions, +} from "./view-props"; export interface IWorkspaceView { id: string; @@ -10,6 +15,9 @@ export interface IWorkspaceView { updated_by: string; name: string; description: string; + filters: IIssueIIFilterOptions; + display_filters: IIssueDisplayFilterOptions; + display_properties: IIssueDisplayProperties; query: any; query_data: IWorkspaceViewProps; project: string; diff --git a/web/types/workspace.d.ts b/packages/types/src/workspace.d.ts similarity index 88% rename from web/types/workspace.d.ts rename to packages/types/src/workspace.d.ts index fb2aca72284..2fc8d69121b 100644 --- a/web/types/workspace.d.ts +++ b/packages/types/src/workspace.d.ts @@ -1,6 +1,5 @@ -import type { IProjectMember, IUser, IUserLite, IWorkspaceViewProps } from "types"; - -export type TUserWorkspaceRole = 5 | 10 | 15 | 20; +import { EUserWorkspaceRoles } from "constants/workspace"; +import type { IProjectMember, IUser, IUserLite, IWorkspaceViewProps } from "@plane/types"; export interface IWorkspace { readonly id: string; @@ -27,18 +26,23 @@ export interface IWorkspaceLite { export interface IWorkspaceMemberInvitation { accepted: boolean; - readonly id: string; email: string; - token: string; + id: string; message: string; responded_at: Date; - role: TUserWorkspaceRole; - created_by_detail: IUser; - workspace: IWorkspace; + role: EUserWorkspaceRoles; + token: string; + workspace: string; + workspace_detail: { + id: string; + logo: string; + name: string; + slug: string; + }; } export interface IWorkspaceBulkInviteFormData { - emails: { email: string; role: TUserWorkspaceRole }[]; + emails: { email: string; role: EUserWorkspaceRoles }[]; } export type Properties = { @@ -58,15 +62,9 @@ export type Properties = { }; export interface IWorkspaceMember { - company_role: string | null; - created_at: Date; - created_by: string; id: string; member: IUserLite; - role: TUserWorkspaceRole; - updated_at: Date; - updated_by: string; - workspace: IWorkspaceLite; + role: EUserWorkspaceRoles; } export interface IWorkspaceMemberMe { @@ -76,7 +74,7 @@ export interface IWorkspaceMemberMe { default_props: IWorkspaceViewProps; id: string; member: string; - role: TUserWorkspaceRole; + role: EUserWorkspaceRoles; updated_at: Date; updated_by: string; view_props: IWorkspaceViewProps; diff --git a/packages/ui/src/icons/priority-icon.tsx b/packages/ui/src/icons/priority-icon.tsx index 198391adbd8..e814233d779 100644 --- a/packages/ui/src/icons/priority-icon.tsx +++ b/packages/ui/src/icons/priority-icon.tsx @@ -1,13 +1,16 @@ import * as React from "react"; - -// icons import { AlertCircle, Ban, SignalHigh, SignalLow, SignalMedium } from "lucide-react"; -// types -import { IPriorityIcon } from "./type"; +type TIssuePriorities = "urgent" | "high" | "medium" | "low" | "none"; + +interface IPriorityIcon { + className?: string; + priority: TIssuePriorities; + size?: number; +} -export const PriorityIcon: React.FC = ({ priority, className = "", transparentBg = false }) => { - if (!className || className === "") className = "h-4 w-4"; +export const PriorityIcon: React.FC = (props) => { + const { priority, className = "", size = 14 } = props; // Convert to lowercase for string comparison const lowercasePriority = priority?.toLowerCase(); @@ -16,31 +19,17 @@ export const PriorityIcon: React.FC = ({ priority, className = "" const getPriorityIcon = (): React.ReactNode => { switch (lowercasePriority) { case "urgent": - return ; + return ; case "high": - return ; + return ; case "medium": - return ; + return ; case "low": - return ; + return ; default: - return ; + return ; } }; - return ( - <> - {transparentBg ? ( - getPriorityIcon() - ) : ( -

- {getPriorityIcon()} -
- )} - - ); + return <>{getPriorityIcon()}; }; diff --git a/packages/ui/src/icons/type.d.ts b/packages/ui/src/icons/type.d.ts index 65b188e4c8c..4a04c948b62 100644 --- a/packages/ui/src/icons/type.d.ts +++ b/packages/ui/src/icons/type.d.ts @@ -1,11 +1,3 @@ export interface ISvgIcons extends React.SVGAttributes { className?: string | undefined; } - -export type TIssuePriorities = "urgent" | "high" | "medium" | "low" | "none"; - -export interface IPriorityIcon { - priority: TIssuePriorities | null; - className?: string; - transparentBg?: boolean | false; -} diff --git a/web/components/account/deactivate-account-modal.tsx b/web/components/account/deactivate-account-modal.tsx index 53ac1df5092..307a65ad2e3 100644 --- a/web/components/account/deactivate-account-modal.tsx +++ b/web/components/account/deactivate-account-modal.tsx @@ -4,8 +4,8 @@ import { useTheme } from "next-themes"; import { Dialog, Transition } from "@headlessui/react"; import { Trash2 } from "lucide-react"; import { mutate } from "swr"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +// hooks +import { useUser } from "hooks/store"; // ui import { Button } from "@plane/ui"; // hooks @@ -22,9 +22,7 @@ export const DeactivateAccountModal: React.FC = (props) => { // states const [isDeactivating, setIsDeactivating] = useState(false); - const { - user: { deactivateAccount }, - } = useMobxStore(); + const { deactivateAccount } = useUser(); const router = useRouter(); diff --git a/web/components/account/sign-in-forms/email-form.tsx b/web/components/account/sign-in-forms/email-form.tsx index 6b607147568..c1e124eab48 100644 --- a/web/components/account/sign-in-forms/email-form.tsx +++ b/web/components/account/sign-in-forms/email-form.tsx @@ -10,7 +10,7 @@ import { Button, Input } from "@plane/ui"; // helpers import { checkEmailValidity } from "helpers/string.helper"; // types -import { IEmailCheckData } from "types/auth"; +import { IEmailCheckData } from "@plane/types"; // constants import { ESignInSteps } from "components/account"; diff --git a/web/components/account/sign-in-forms/o-auth-options.tsx b/web/components/account/sign-in-forms/o-auth-options.tsx index aec82cfa52e..9ed4e7e5ff1 100644 --- a/web/components/account/sign-in-forms/o-auth-options.tsx +++ b/web/components/account/sign-in-forms/o-auth-options.tsx @@ -1,9 +1,8 @@ import { observer } from "mobx-react-lite"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; // services import { AuthService } from "services/auth.service"; // hooks +import { useApplication } from "hooks/store"; import useToast from "hooks/use-toast"; // components import { GitHubSignInButton, GoogleSignInButton } from "components/account"; @@ -21,8 +20,8 @@ export const OAuthOptions: React.FC = observer((props) => { const { setToastAlert } = useToast(); // mobx store const { - appConfig: { envConfig }, - } = useMobxStore(); + config: { envConfig }, + } = useApplication(); const handleGoogleSignIn = async ({ clientId, credential }: any) => { try { diff --git a/web/components/account/sign-in-forms/password.tsx b/web/components/account/sign-in-forms/password.tsx index a75a450e25b..ef9edbfbcf0 100644 --- a/web/components/account/sign-in-forms/password.tsx +++ b/web/components/account/sign-in-forms/password.tsx @@ -11,7 +11,7 @@ import { Button, Input } from "@plane/ui"; // helpers import { checkEmailValidity } from "helpers/string.helper"; // types -import { IPasswordSignInData } from "types/auth"; +import { IPasswordSignInData } from "@plane/types"; // constants import { ESignInSteps } from "components/account"; diff --git a/web/components/account/sign-in-forms/root.tsx b/web/components/account/sign-in-forms/root.tsx index f7ec6b59341..616f4809fce 100644 --- a/web/components/account/sign-in-forms/root.tsx +++ b/web/components/account/sign-in-forms/root.tsx @@ -1,8 +1,7 @@ import React, { useState } from "react"; import { observer } from "mobx-react-lite"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; // hooks +import { useApplication } from "hooks/store"; import useSignInRedirection from "hooks/use-sign-in-redirection"; // components import { LatestFeatureBlock } from "components/common"; @@ -38,8 +37,8 @@ export const SignInRoot = observer(() => { const { handleRedirection } = useSignInRedirection(); // mobx store const { - appConfig: { envConfig }, - } = useMobxStore(); + config: { envConfig }, + } = useApplication(); const isOAuthEnabled = envConfig && (envConfig.google_client_id || envConfig.github_client_id); diff --git a/web/components/account/sign-in-forms/self-hosted-sign-in.tsx b/web/components/account/sign-in-forms/self-hosted-sign-in.tsx index 2335226ce3e..bcecef20aad 100644 --- a/web/components/account/sign-in-forms/self-hosted-sign-in.tsx +++ b/web/components/account/sign-in-forms/self-hosted-sign-in.tsx @@ -11,7 +11,7 @@ import { Button, Input } from "@plane/ui"; // helpers import { checkEmailValidity } from "helpers/string.helper"; // types -import { IPasswordSignInData } from "types/auth"; +import { IPasswordSignInData } from "@plane/types"; type Props = { email: string; diff --git a/web/components/account/sign-in-forms/set-password-link.tsx b/web/components/account/sign-in-forms/set-password-link.tsx index 17dbd2ad412..788142d8037 100644 --- a/web/components/account/sign-in-forms/set-password-link.tsx +++ b/web/components/account/sign-in-forms/set-password-link.tsx @@ -9,7 +9,7 @@ import { Button, Input } from "@plane/ui"; // helpers import { checkEmailValidity } from "helpers/string.helper"; // types -import { IEmailCheckData } from "types/auth"; +import { IEmailCheckData } from "@plane/types"; type Props = { email: string; diff --git a/web/components/account/sign-in-forms/unique-code.tsx b/web/components/account/sign-in-forms/unique-code.tsx index 1a4fa0e493a..433fea00a17 100644 --- a/web/components/account/sign-in-forms/unique-code.tsx +++ b/web/components/account/sign-in-forms/unique-code.tsx @@ -13,7 +13,7 @@ import { Button, Input } from "@plane/ui"; // helpers import { checkEmailValidity } from "helpers/string.helper"; // types -import { IEmailCheckData, IMagicSignInData } from "types/auth"; +import { IEmailCheckData, IMagicSignInData } from "@plane/types"; // constants import { ESignInSteps } from "components/account"; @@ -233,8 +233,8 @@ export const UniqueCodeForm: React.FC = (props) => { {resendTimerCode > 0 ? `Request new code in ${resendTimerCode}s` : isRequestingNewCode - ? "Requesting new code" - : "Request new code"} + ? "Requesting new code" + : "Request new code"} diff --git a/web/components/analytics/custom-analytics/custom-analytics.tsx b/web/components/analytics/custom-analytics/custom-analytics.tsx index 635fbee7f92..a3c083b027c 100644 --- a/web/components/analytics/custom-analytics/custom-analytics.tsx +++ b/web/components/analytics/custom-analytics/custom-analytics.tsx @@ -7,7 +7,7 @@ import { AnalyticsService } from "services/analytics.service"; // components import { CustomAnalyticsSelectBar, CustomAnalyticsMainContent, CustomAnalyticsSidebar } from "components/analytics"; // types -import { IAnalyticsParams } from "types"; +import { IAnalyticsParams } from "@plane/types"; // fetch-keys import { ANALYTICS } from "constants/fetch-keys"; diff --git a/web/components/analytics/custom-analytics/graph/custom-tooltip.tsx b/web/components/analytics/custom-analytics/graph/custom-tooltip.tsx index 9917d0f58e1..ec7c4019507 100644 --- a/web/components/analytics/custom-analytics/graph/custom-tooltip.tsx +++ b/web/components/analytics/custom-analytics/graph/custom-tooltip.tsx @@ -3,7 +3,7 @@ import { BarTooltipProps } from "@nivo/bar"; import { DATE_KEYS } from "constants/analytics"; import { renderMonthAndYear } from "helpers/analytics.helper"; // types -import { IAnalyticsParams, IAnalyticsResponse } from "types"; +import { IAnalyticsParams, IAnalyticsResponse } from "@plane/types"; type Props = { datum: BarTooltipProps; @@ -60,8 +60,8 @@ export const CustomTooltip: React.FC = ({ datum, analytics, params }) => ? "capitalize" : "" : params.x_axis === "priority" || params.x_axis === "state__group" - ? "capitalize" - : "" + ? "capitalize" + : "" }`} > {params.segment === "assignees__id" ? renderAssigneeName(tooltipValue.toString()) : tooltipValue}: diff --git a/web/components/analytics/custom-analytics/graph/index.tsx b/web/components/analytics/custom-analytics/graph/index.tsx index 06431ab02dd..51b4089c4f2 100644 --- a/web/components/analytics/custom-analytics/graph/index.tsx +++ b/web/components/analytics/custom-analytics/graph/index.tsx @@ -9,7 +9,7 @@ import { BarGraph } from "components/ui"; import { findStringWithMostCharacters } from "helpers/array.helper"; import { generateBarColor, generateDisplayName } from "helpers/analytics.helper"; // types -import { IAnalyticsParams, IAnalyticsResponse } from "types"; +import { IAnalyticsParams, IAnalyticsResponse } from "@plane/types"; type Props = { analytics: IAnalyticsResponse; @@ -101,8 +101,8 @@ export const AnalyticsGraph: React.FC = ({ analytics, barGraphData, param ? generateDisplayName(datum.value, analytics, params, "x_axis")[0].toUpperCase() : "?" : datum.value && datum.value !== "None" - ? `${datum.value}`.toUpperCase()[0] - : "?"} + ? `${datum.value}`.toUpperCase()[0] + : "?"} diff --git a/web/components/analytics/custom-analytics/main-content.tsx b/web/components/analytics/custom-analytics/main-content.tsx index 5cfd1548298..3c199f8078c 100644 --- a/web/components/analytics/custom-analytics/main-content.tsx +++ b/web/components/analytics/custom-analytics/main-content.tsx @@ -8,7 +8,7 @@ import { Button, Loader } from "@plane/ui"; // helpers import { convertResponseToBarGraphData } from "helpers/analytics.helper"; // types -import { IAnalyticsParams, IAnalyticsResponse } from "types"; +import { IAnalyticsParams, IAnalyticsResponse } from "@plane/types"; // fetch-keys import { ANALYTICS } from "constants/fetch-keys"; diff --git a/web/components/analytics/custom-analytics/select-bar.tsx b/web/components/analytics/custom-analytics/select-bar.tsx index f3d7a99937c..19f83e40b99 100644 --- a/web/components/analytics/custom-analytics/select-bar.tsx +++ b/web/components/analytics/custom-analytics/select-bar.tsx @@ -1,13 +1,11 @@ -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import { Control, Controller, UseFormSetValue } from "react-hook-form"; - -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +// hooks +import { useProject } from "hooks/store"; // components import { SelectProject, SelectSegment, SelectXAxis, SelectYAxis } from "components/analytics"; // types -import { IAnalyticsParams } from "types"; +import { IAnalyticsParams } from "@plane/types"; type Props = { control: Control; @@ -20,12 +18,7 @@ type Props = { export const CustomAnalyticsSelectBar: React.FC = observer((props) => { const { control, setValue, params, fullScreen, isProjectLevel } = props; - const router = useRouter(); - const { workspaceSlug } = router.query; - - const { project: projectStore } = useMobxStore(); - - const projectsList = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : null; + const { workspaceProjectIds: workspaceProjectIds } = useProject(); return (
= observer((props) => { name="project" control={control} render={({ field: { value, onChange } }) => ( - + )} />
diff --git a/web/components/analytics/custom-analytics/select/project.tsx b/web/components/analytics/custom-analytics/select/project.tsx index 7251c507394..ee3dce6d6f6 100644 --- a/web/components/analytics/custom-analytics/select/project.tsx +++ b/web/components/analytics/custom-analytics/select/project.tsx @@ -1,25 +1,33 @@ +import { observer } from "mobx-react-lite"; +// hooks +import { useProject } from "hooks/store"; // ui import { CustomSearchSelect } from "@plane/ui"; -// types -import { IProject } from "types"; type Props = { value: string[] | undefined; onChange: (val: string[] | null) => void; - projects: IProject[] | undefined; + projectIds: string[] | undefined; }; -export const SelectProject: React.FC = ({ value, onChange, projects }) => { - const options = projects?.map((project) => ({ - value: project.id, - query: project.name + project.identifier, - content: ( -
- {project.identifier} - {project.name} -
- ), - })); +export const SelectProject: React.FC = observer((props) => { + const { value, onChange, projectIds } = props; + const { getProjectById } = useProject(); + + const options = projectIds?.map((projectId) => { + const projectDetails = getProjectById(projectId); + + return { + value: projectDetails?.id, + query: `${projectDetails?.name} ${projectDetails?.identifier}`, + content: ( +
+ {projectDetails?.identifier} + {projectDetails?.name} +
+ ), + }; + }); return ( = ({ value, onChange, projects }) => options={options} label={ value && value.length > 0 - ? projects - ?.filter((p) => value.includes(p.id)) - .map((p) => p.identifier) + ? projectIds + ?.filter((p) => value.includes(p)) + .map((p) => getProjectById(p)?.name) .join(", ") : "All projects" } @@ -38,4 +46,4 @@ export const SelectProject: React.FC = ({ value, onChange, projects }) => multiple /> ); -}; +}); diff --git a/web/components/analytics/custom-analytics/select/segment.tsx b/web/components/analytics/custom-analytics/select/segment.tsx index 4efc6a21195..b45c1fa5535 100644 --- a/web/components/analytics/custom-analytics/select/segment.tsx +++ b/web/components/analytics/custom-analytics/select/segment.tsx @@ -3,7 +3,7 @@ import { useRouter } from "next/router"; // ui import { CustomSelect } from "@plane/ui"; // types -import { IAnalyticsParams, TXAxisValues } from "types"; +import { IAnalyticsParams, TXAxisValues } from "@plane/types"; // constants import { ANALYTICS_X_AXIS_VALUES } from "constants/analytics"; diff --git a/web/components/analytics/custom-analytics/select/x-axis.tsx b/web/components/analytics/custom-analytics/select/x-axis.tsx index 66582a1e97b..237582ba085 100644 --- a/web/components/analytics/custom-analytics/select/x-axis.tsx +++ b/web/components/analytics/custom-analytics/select/x-axis.tsx @@ -3,7 +3,7 @@ import { useRouter } from "next/router"; // ui import { CustomSelect } from "@plane/ui"; // types -import { IAnalyticsParams, TXAxisValues } from "types"; +import { IAnalyticsParams, TXAxisValues } from "@plane/types"; // constants import { ANALYTICS_X_AXIS_VALUES } from "constants/analytics"; diff --git a/web/components/analytics/custom-analytics/select/y-axis.tsx b/web/components/analytics/custom-analytics/select/y-axis.tsx index 3f7348cce79..60445794578 100644 --- a/web/components/analytics/custom-analytics/select/y-axis.tsx +++ b/web/components/analytics/custom-analytics/select/y-axis.tsx @@ -1,7 +1,7 @@ // ui import { CustomSelect } from "@plane/ui"; // types -import { TYAxisValues } from "types"; +import { TYAxisValues } from "@plane/types"; // constants import { ANALYTICS_Y_AXIS_VALUES } from "constants/analytics"; diff --git a/web/components/analytics/custom-analytics/sidebar/projects-list.tsx b/web/components/analytics/custom-analytics/sidebar/projects-list.tsx index 41770eec8db..d09e8def49a 100644 --- a/web/components/analytics/custom-analytics/sidebar/projects-list.tsx +++ b/web/components/analytics/custom-analytics/sidebar/projects-list.tsx @@ -1,65 +1,74 @@ +import { observer } from "mobx-react-lite"; +// hooks +import { useProject } from "hooks/store"; // icons import { Contrast, LayoutGrid, Users } from "lucide-react"; // helpers import { renderEmoji } from "helpers/emoji.helper"; import { truncateText } from "helpers/string.helper"; -// types -import { IProject } from "types"; type Props = { - projects: IProject[]; + projectIds: string[]; }; -export const CustomAnalyticsSidebarProjectsList: React.FC = (props) => { - const { projects } = props; +export const CustomAnalyticsSidebarProjectsList: React.FC = observer((props) => { + const { projectIds } = props; + + const { getProjectById } = useProject(); return (

Selected Projects

- {projects.map((project) => ( -
-
- {project.emoji ? ( - {renderEmoji(project.emoji)} - ) : project.icon_prop ? ( -
{renderEmoji(project.icon_prop)}
- ) : ( - - {project?.name.charAt(0)} - - )} -
-

{truncateText(project.name, 20)}

- ({project.identifier}) -
-
-
-
-
- -
Total members
-
- {project.total_members} + {projectIds.map((projectId) => { + const project = getProjectById(projectId); + + if (!project) return; + + return ( +
+
+ {project.emoji ? ( + {renderEmoji(project.emoji)} + ) : project.icon_prop ? ( +
{renderEmoji(project.icon_prop)}
+ ) : ( + + {project?.name.charAt(0)} + + )} +
+

{truncateText(project.name, 20)}

+ ({project.identifier}) +
-
-
- -
Total cycles
+
+
+
+ +
Total members
+
+ {project.total_members}
- {project.total_cycles} -
-
-
- -
Total modules
+
+
+ +
Total cycles
+
+ {project.total_cycles} +
+
+
+ +
Total modules
+
+ {project.total_modules}
- {project.total_modules}
-
- ))} + ); + })}
); -}; +}); diff --git a/web/components/analytics/custom-analytics/sidebar/sidebar-header.tsx b/web/components/analytics/custom-analytics/sidebar/sidebar-header.tsx index ac75be68666..d46cad191b2 100644 --- a/web/components/analytics/custom-analytics/sidebar/sidebar-header.tsx +++ b/web/components/analytics/custom-analytics/sidebar/sidebar-header.tsx @@ -1,7 +1,7 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +// hooks +import { useCycle, useModule, useProject } from "hooks/store"; // helpers import { renderEmoji } from "helpers/emoji.helper"; import { renderFormattedDate } from "helpers/date-time.helper"; @@ -10,16 +10,15 @@ import { NETWORK_CHOICES } from "constants/project"; export const CustomAnalyticsSidebarHeader = observer(() => { const router = useRouter(); - const { workspaceSlug, projectId, cycleId, moduleId } = router.query; + const { projectId, cycleId, moduleId } = router.query; - const { cycle: cycleStore, module: moduleStore, project: projectStore } = useMobxStore(); + const { getProjectById } = useProject(); + const { getCycleById } = useCycle(); + const { getModuleById } = useModule(); - const cycleDetails = cycleId ? cycleStore.getCycleById(cycleId.toString()) : undefined; - const moduleDetails = moduleId ? moduleStore.getModuleById(moduleId.toString()) : undefined; - const projectDetails = - workspaceSlug && projectId - ? projectStore.getProjectById(workspaceSlug.toString(), projectId.toString()) - : undefined; + const cycleDetails = cycleId ? getCycleById(cycleId.toString()) : undefined; + const moduleDetails = moduleId ? getModuleById(moduleId.toString()) : undefined; + const projectDetails = projectId ? getProjectById(projectId.toString()) : undefined; return ( <> diff --git a/web/components/analytics/custom-analytics/sidebar/sidebar.tsx b/web/components/analytics/custom-analytics/sidebar/sidebar.tsx index 951ed36021c..59013a3e3e2 100644 --- a/web/components/analytics/custom-analytics/sidebar/sidebar.tsx +++ b/web/components/analytics/custom-analytics/sidebar/sidebar.tsx @@ -5,8 +5,8 @@ import { mutate } from "swr"; // services import { AnalyticsService } from "services/analytics.service"; // hooks +import { useCycle, useModule, useProject, useUser } from "hooks/store"; import useToast from "hooks/use-toast"; -import { useMobxStore } from "lib/mobx/store-provider"; // components import { CustomAnalyticsSidebarHeader, CustomAnalyticsSidebarProjectsList } from "components/analytics"; // ui @@ -16,7 +16,7 @@ import { CalendarDays, Download, RefreshCw } from "lucide-react"; // helpers import { renderFormattedDate } from "helpers/date-time.helper"; // types -import { IAnalyticsParams, IAnalyticsResponse, IExportAnalyticsFormData, IWorkspace } from "types"; +import { IAnalyticsParams, IAnalyticsResponse, IExportAnalyticsFormData, IWorkspace } from "@plane/types"; // fetch-keys import { ANALYTICS } from "constants/fetch-keys"; @@ -29,172 +29,167 @@ type Props = { const analyticsService = new AnalyticsService(); -export const CustomAnalyticsSidebar: React.FC = observer( - ({ analytics, params, fullScreen, isProjectLevel = false }) => { - const router = useRouter(); - const { workspaceSlug, projectId, cycleId, moduleId } = router.query; - - const { setToastAlert } = useToast(); - - const { user: userStore, project: projectStore, cycle: cycleStore, module: moduleStore } = useMobxStore(); - - const user = userStore.currentUser; - - const projects = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : undefined; - const projectDetails = - workspaceSlug && projectId - ? projectStore.getProjectById(workspaceSlug.toString(), projectId.toString()) ?? undefined - : undefined; - - const trackExportAnalytics = () => { - if (!user) return; - - const eventPayload: any = { - workspaceSlug: workspaceSlug?.toString(), - params: { - x_axis: params.x_axis, - y_axis: params.y_axis, - group: params.segment, - project: params.project, - }, - }; - - if (projectDetails) { - const workspaceDetails = projectDetails.workspace as IWorkspace; - - eventPayload.workspaceId = workspaceDetails.id; - eventPayload.workspaceName = workspaceDetails.name; - eventPayload.projectId = projectDetails.id; - eventPayload.projectIdentifier = projectDetails.identifier; - eventPayload.projectName = projectDetails.name; - } - - if (cycleDetails || moduleDetails) { - const details = cycleDetails || moduleDetails; - - eventPayload.workspaceId = details?.workspace_detail?.id; - eventPayload.workspaceName = details?.workspace_detail?.name; - eventPayload.projectId = details?.project_detail.id; - eventPayload.projectIdentifier = details?.project_detail.identifier; - eventPayload.projectName = details?.project_detail.name; - } - - if (cycleDetails) { - eventPayload.cycleId = cycleDetails.id; - eventPayload.cycleName = cycleDetails.name; - } - - if (moduleDetails) { - eventPayload.moduleId = moduleDetails.id; - eventPayload.moduleName = moduleDetails.name; - } - }; - - const exportAnalytics = () => { - if (!workspaceSlug) return; - - const data: IExportAnalyticsFormData = { +export const CustomAnalyticsSidebar: React.FC = observer((props) => { + const { analytics, params, fullScreen, isProjectLevel = false } = props; + // router + const router = useRouter(); + const { workspaceSlug, projectId, cycleId, moduleId } = router.query; + // toast alert + const { setToastAlert } = useToast(); + // store hooks + const { currentUser } = useUser(); + const { workspaceProjectIds, getProjectById } = useProject(); + const { fetchCycleDetails, getCycleById } = useCycle(); + const { fetchModuleDetails, getModuleById } = useModule(); + + const projectDetails = projectId ? getProjectById(projectId.toString()) ?? undefined : undefined; + + const trackExportAnalytics = () => { + if (!currentUser) return; + + const eventPayload: any = { + workspaceSlug: workspaceSlug?.toString(), + params: { x_axis: params.x_axis, y_axis: params.y_axis, - }; - - if (params.segment) data.segment = params.segment; - if (params.project) data.project = params.project; - - analyticsService - .exportAnalytics(workspaceSlug.toString(), data) - .then((res) => { - setToastAlert({ - type: "success", - title: "Success!", - message: res.message, - }); - - trackExportAnalytics(); - }) - .catch(() => - setToastAlert({ - type: "error", - title: "Error!", - message: "There was some error in exporting the analytics. Please try again.", - }) - ); + group: params.segment, + project: params.project, + }, }; - const cycleDetails = cycleId ? cycleStore.getCycleById(cycleId.toString()) : undefined; - const moduleDetails = moduleId ? moduleStore.getModuleById(moduleId.toString()) : undefined; - - // fetch cycle details - useEffect(() => { - if (!workspaceSlug || !projectId || !cycleId || cycleDetails) return; - - cycleStore.fetchCycleWithId(workspaceSlug.toString(), projectId.toString(), cycleId.toString()); - }, [cycleId, cycleDetails, cycleStore, projectId, workspaceSlug]); - - // fetch module details - useEffect(() => { - if (!workspaceSlug || !projectId || !moduleId || moduleDetails) return; - - moduleStore.fetchModuleDetails(workspaceSlug.toString(), projectId.toString(), moduleId.toString()); - }, [moduleId, moduleDetails, moduleStore, projectId, workspaceSlug]); - - const selectedProjects = params.project && params.project.length > 0 ? params.project : projects?.map((p) => p.id); + if (projectDetails) { + const workspaceDetails = projectDetails.workspace as IWorkspace; + + eventPayload.workspaceId = workspaceDetails.id; + eventPayload.workspaceName = workspaceDetails.name; + eventPayload.projectId = projectDetails.id; + eventPayload.projectIdentifier = projectDetails.identifier; + eventPayload.projectName = projectDetails.name; + } + + if (cycleDetails || moduleDetails) { + const details = cycleDetails || moduleDetails; + + eventPayload.workspaceId = details?.workspace_detail?.id; + eventPayload.workspaceName = details?.workspace_detail?.name; + eventPayload.projectId = details?.project_detail.id; + eventPayload.projectIdentifier = details?.project_detail.identifier; + eventPayload.projectName = details?.project_detail.name; + } + + if (cycleDetails) { + eventPayload.cycleId = cycleDetails.id; + eventPayload.cycleName = cycleDetails.name; + } + + if (moduleDetails) { + eventPayload.moduleId = moduleDetails.id; + eventPayload.moduleName = moduleDetails.name; + } + }; + + const exportAnalytics = () => { + if (!workspaceSlug) return; + + const data: IExportAnalyticsFormData = { + x_axis: params.x_axis, + y_axis: params.y_axis, + }; - return ( -
-
+ if (params.segment) data.segment = params.segment; + if (params.project) data.project = params.project; + + analyticsService + .exportAnalytics(workspaceSlug.toString(), data) + .then((res) => { + setToastAlert({ + type: "success", + title: "Success!", + message: res.message, + }); + + trackExportAnalytics(); + }) + .catch(() => + setToastAlert({ + type: "error", + title: "Error!", + message: "There was some error in exporting the analytics. Please try again.", + }) + ); + }; + + const cycleDetails = cycleId ? getCycleById(cycleId.toString()) : undefined; + const moduleDetails = moduleId ? getModuleById(moduleId.toString()) : undefined; + + // fetch cycle details + useEffect(() => { + if (!workspaceSlug || !projectId || !cycleId || cycleDetails) return; + + fetchCycleDetails(workspaceSlug.toString(), projectId.toString(), cycleId.toString()); + }, [cycleId, cycleDetails, fetchCycleDetails, projectId, workspaceSlug]); + + // fetch module details + useEffect(() => { + if (!workspaceSlug || !projectId || !moduleId || moduleDetails) return; + + fetchModuleDetails(workspaceSlug.toString(), projectId.toString(), moduleId.toString()); + }, [moduleId, moduleDetails, fetchModuleDetails, projectId, workspaceSlug]); + + const selectedProjects = params.project && params.project.length > 0 ? params.project : workspaceProjectIds; + + return ( +
+
+
+ + {analytics ? analytics.total : "..."} Issues +
+ {isProjectLevel && (
- - {analytics ? analytics.total : "..."} Issues + + {renderFormattedDate( + (cycleId + ? cycleDetails?.created_at + : moduleId + ? moduleDetails?.created_at + : projectDetails?.created_at) ?? "" + )}
- {isProjectLevel && ( -
- - {renderFormattedDate( - (cycleId - ? cycleDetails?.created_at - : moduleId - ? moduleDetails?.created_at - : projectDetails?.created_at) ?? "" - )} -
- )} -
-
- {fullScreen ? ( - <> - {!isProjectLevel && selectedProjects && selectedProjects.length > 0 && ( - selectedProjects.includes(p.id)) ?? []} - /> - )} - - - ) : null} -
-
- - -
+ )} +
+
+ {fullScreen ? ( + <> + {!isProjectLevel && selectedProjects && selectedProjects.length > 0 && ( + + )} + + + ) : null} +
+
+ +
- ); - } -); +
+ ); +}); diff --git a/web/components/analytics/custom-analytics/table.tsx b/web/components/analytics/custom-analytics/table.tsx index 2066292c8e3..c09f26d7657 100644 --- a/web/components/analytics/custom-analytics/table.tsx +++ b/web/components/analytics/custom-analytics/table.tsx @@ -5,7 +5,7 @@ import { PriorityIcon } from "@plane/ui"; // helpers import { generateBarColor, generateDisplayName } from "helpers/analytics.helper"; // types -import { IAnalyticsParams, IAnalyticsResponse, TIssuePriorities } from "types"; +import { IAnalyticsParams, IAnalyticsResponse, TIssuePriorities } from "@plane/types"; // constants import { ANALYTICS_X_AXIS_VALUES, ANALYTICS_Y_AXIS_VALUES } from "constants/analytics"; diff --git a/web/components/analytics/project-modal/main-content.tsx b/web/components/analytics/project-modal/main-content.tsx index 55ed1d40392..09423e6dd71 100644 --- a/web/components/analytics/project-modal/main-content.tsx +++ b/web/components/analytics/project-modal/main-content.tsx @@ -4,7 +4,7 @@ import { Tab } from "@headlessui/react"; // components import { CustomAnalytics, ScopeAndDemand } from "components/analytics"; // types -import { ICycle, IModule, IProject } from "types"; +import { ICycle, IModule, IProject } from "@plane/types"; // constants import { ANALYTICS_TABS } from "constants/analytics"; diff --git a/web/components/analytics/project-modal/modal.tsx b/web/components/analytics/project-modal/modal.tsx index 6dfbfdd6b51..a4b82c4b6d0 100644 --- a/web/components/analytics/project-modal/modal.tsx +++ b/web/components/analytics/project-modal/modal.tsx @@ -5,7 +5,7 @@ import { Dialog, Transition } from "@headlessui/react"; // components import { ProjectAnalyticsModalHeader, ProjectAnalyticsModalMainContent } from "components/analytics"; // types -import { ICycle, IModule, IProject } from "types"; +import { ICycle, IModule, IProject } from "@plane/types"; type Props = { isOpen: boolean; diff --git a/web/components/analytics/scope-and-demand/demand.tsx b/web/components/analytics/scope-and-demand/demand.tsx index df679fbc5c4..2ff438a398f 100644 --- a/web/components/analytics/scope-and-demand/demand.tsx +++ b/web/components/analytics/scope-and-demand/demand.tsx @@ -1,7 +1,7 @@ // icons import { Triangle } from "lucide-react"; // types -import { IDefaultAnalyticsResponse, TStateGroups } from "types"; +import { IDefaultAnalyticsResponse, TStateGroups } from "@plane/types"; // constants import { STATE_GROUP_COLORS } from "constants/state"; diff --git a/web/components/analytics/scope-and-demand/scope.tsx b/web/components/analytics/scope-and-demand/scope.tsx index 4c69a23c5f8..ea1a51937d4 100644 --- a/web/components/analytics/scope-and-demand/scope.tsx +++ b/web/components/analytics/scope-and-demand/scope.tsx @@ -3,7 +3,7 @@ import { BarGraph, ProfileEmptyState } from "components/ui"; // image import emptyBarGraph from "public/empty-state/empty_bar_graph.svg"; // types -import { IDefaultAnalyticsResponse } from "types"; +import { IDefaultAnalyticsResponse } from "@plane/types"; type Props = { defaultAnalytics: IDefaultAnalyticsResponse; diff --git a/web/components/analytics/scope-and-demand/year-wise-issues.tsx b/web/components/analytics/scope-and-demand/year-wise-issues.tsx index aec15d9acd3..2a62c99d4bd 100644 --- a/web/components/analytics/scope-and-demand/year-wise-issues.tsx +++ b/web/components/analytics/scope-and-demand/year-wise-issues.tsx @@ -3,7 +3,7 @@ import { LineGraph, ProfileEmptyState } from "components/ui"; // image import emptyGraph from "public/empty-state/empty_graph.svg"; // types -import { IDefaultAnalyticsResponse } from "types"; +import { IDefaultAnalyticsResponse } from "@plane/types"; // constants import { MONTHS_LIST } from "constants/calendar"; diff --git a/web/components/api-token/delete-token-modal.tsx b/web/components/api-token/delete-token-modal.tsx index ed61d3546dd..993289c10c7 100644 --- a/web/components/api-token/delete-token-modal.tsx +++ b/web/components/api-token/delete-token-modal.tsx @@ -9,7 +9,7 @@ import useToast from "hooks/use-toast"; // ui import { Button } from "@plane/ui"; // types -import { IApiToken } from "types/api_token"; +import { IApiToken } from "@plane/types"; // fetch-keys import { API_TOKENS_LIST } from "constants/fetch-keys"; diff --git a/web/components/api-token/modal/create-token-modal.tsx b/web/components/api-token/modal/create-token-modal.tsx index 5df1275baaa..b3fc3df78ec 100644 --- a/web/components/api-token/modal/create-token-modal.tsx +++ b/web/components/api-token/modal/create-token-modal.tsx @@ -12,7 +12,7 @@ import { CreateApiTokenForm, GeneratedTokenDetails } from "components/api-token" import { csvDownload } from "helpers/download.helper"; import { renderFormattedDate } from "helpers/date-time.helper"; // types -import { IApiToken } from "types/api_token"; +import { IApiToken } from "@plane/types"; // fetch-keys import { API_TOKENS_LIST } from "constants/fetch-keys"; diff --git a/web/components/api-token/modal/form.tsx b/web/components/api-token/modal/form.tsx index a04968dac07..ae7717b3933 100644 --- a/web/components/api-token/modal/form.tsx +++ b/web/components/api-token/modal/form.tsx @@ -11,7 +11,7 @@ import { Button, CustomSelect, Input, TextArea, ToggleSwitch } from "@plane/ui"; // helpers import { renderFormattedDate, renderFormattedPayloadDate } from "helpers/date-time.helper"; // types -import { IApiToken } from "types/api_token"; +import { IApiToken } from "@plane/types"; type Props = { handleClose: () => void; @@ -175,8 +175,8 @@ export const CreateApiTokenForm: React.FC = (props) => { {value === "custom" ? "Custom date" : selectedOption - ? selectedOption.label - : "Set expiration date"} + ? selectedOption.label + : "Set expiration date"}
} value={value} @@ -219,8 +219,8 @@ export const CreateApiTokenForm: React.FC = (props) => { ? `Expires ${renderFormattedDate(customDate)}` : null : watch("expired_at") - ? `Expires ${getExpiryDate(watch("expired_at") ?? "")}` - : null} + ? `Expires ${getExpiryDate(watch("expired_at") ?? "")}` + : null} )}
diff --git a/web/components/api-token/modal/generated-token-details.tsx b/web/components/api-token/modal/generated-token-details.tsx index 1ffa69a78a8..f28ea348126 100644 --- a/web/components/api-token/modal/generated-token-details.tsx +++ b/web/components/api-token/modal/generated-token-details.tsx @@ -7,7 +7,7 @@ import { Button, Tooltip } from "@plane/ui"; import { renderFormattedDate } from "helpers/date-time.helper"; import { copyTextToClipboard } from "helpers/string.helper"; // types -import { IApiToken } from "types/api_token"; +import { IApiToken } from "@plane/types"; type Props = { handleClose: () => void; diff --git a/web/components/api-token/token-list-item.tsx b/web/components/api-token/token-list-item.tsx index 37bb968d34b..2de73122280 100644 --- a/web/components/api-token/token-list-item.tsx +++ b/web/components/api-token/token-list-item.tsx @@ -7,7 +7,7 @@ import { Tooltip } from "@plane/ui"; // helpers import { renderFormattedDate, calculateTimeAgo } from "helpers/date-time.helper"; // types -import { IApiToken } from "types/api_token"; +import { IApiToken } from "@plane/types"; type Props = { token: IApiToken; diff --git a/web/components/auth-screens/not-authorized-view.tsx b/web/components/auth-screens/not-authorized-view.tsx index f0a3e3d904c..8d9d6ecd4d9 100644 --- a/web/components/auth-screens/not-authorized-view.tsx +++ b/web/components/auth-screens/not-authorized-view.tsx @@ -1,12 +1,12 @@ import React from "react"; -// next import Link from "next/link"; import Image from "next/image"; import { useRouter } from "next/router"; +import { observer } from "mobx-react-lite"; +// hooks +import { useUser } from "hooks/store"; // layouts import DefaultLayout from "layouts/default-layout"; -// hooks -import useUser from "hooks/use-user"; // images import ProjectNotAuthorizedImg from "public/auth/project-not-authorized.svg"; import WorkspaceNotAuthorizedImg from "public/auth/workspace-not-authorized.svg"; @@ -16,8 +16,9 @@ type Props = { type: "project" | "workspace"; }; -export const NotAuthorizedView: React.FC = ({ actionButton, type }) => { - const { user } = useUser(); +export const NotAuthorizedView: React.FC = observer((props) => { + const { actionButton, type } = props; + const { currentUser } = useUser(); const { query } = useRouter(); const { next_path } = query; @@ -35,9 +36,9 @@ export const NotAuthorizedView: React.FC = ({ actionButton, type }) => {

Oops! You are not authorized to view this page

- {user ? ( + {currentUser ? (

- You have signed in as {user.email}.
+ You have signed in as {currentUser.email}.
Sign in {" "} @@ -58,4 +59,4 @@ export const NotAuthorizedView: React.FC = ({ actionButton, type }) => {

); -}; +}); diff --git a/web/components/auth-screens/project/join-project.tsx b/web/components/auth-screens/project/join-project.tsx index 7ee4feacd9d..35b0b9b498b 100644 --- a/web/components/auth-screens/project/join-project.tsx +++ b/web/components/auth-screens/project/join-project.tsx @@ -1,9 +1,8 @@ import { useState } from "react"; import Image from "next/image"; import { useRouter } from "next/router"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; -import { RootStore } from "store/root"; +// hooks +import { useProject, useUser } from "hooks/store"; // ui import { Button } from "@plane/ui"; // icons @@ -12,12 +11,13 @@ import { ClipboardList } from "lucide-react"; import JoinProjectImg from "public/auth/project-not-authorized.svg"; export const JoinProject: React.FC = () => { + // states const [isJoiningProject, setIsJoiningProject] = useState(false); - + // store hooks const { - project: projectStore, - user: { joinProject }, - }: RootStore = useMobxStore(); + membership: { joinProject }, + } = useUser(); + const { fetchProjects } = useProject(); const router = useRouter(); const { workspaceSlug, projectId } = router.query; @@ -28,12 +28,8 @@ export const JoinProject: React.FC = () => { setIsJoiningProject(true); joinProject(workspaceSlug.toString(), [projectId.toString()]) - .then(() => { - projectStore.fetchProjects(workspaceSlug.toString()); - }) - .finally(() => { - setIsJoiningProject(false); - }); + .then(() => fetchProjects(workspaceSlug.toString())) + .finally(() => setIsJoiningProject(false)); }; return ( diff --git a/web/components/automation/auto-archive-automation.tsx b/web/components/automation/auto-archive-automation.tsx index 6471bc9cfab..3d5f6352e5f 100644 --- a/web/components/automation/auto-archive-automation.tsx +++ b/web/components/automation/auto-archive-automation.tsx @@ -1,17 +1,16 @@ import React, { useState } from "react"; import { observer } from "mobx-react-lite"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +// hooks +import { useProject, useUser } from "hooks/store"; // component import { CustomSelect, Loader, ToggleSwitch } from "@plane/ui"; import { SelectMonthModal } from "components/automation"; // icon import { ArchiveRestore } from "lucide-react"; // constants -import { PROJECT_AUTOMATION_MONTHS } from "constants/project"; +import { EUserProjectRoles, PROJECT_AUTOMATION_MONTHS } from "constants/project"; // types -import { IProject } from "types"; -import { EUserWorkspaceRoles } from "constants/workspace"; +import { IProject } from "@plane/types"; type Props = { handleChange: (formData: Partial) => Promise; @@ -23,13 +22,13 @@ export const AutoArchiveAutomation: React.FC = observer((props) => { const { handleChange } = props; // states const [monthModal, setmonthModal] = useState(false); + // store hooks + const { + membership: { currentProjectRole }, + } = useUser(); + const { currentProjectDetails } = useProject(); - const { user: userStore, project: projectStore } = useMobxStore(); - - const projectDetails = projectStore.currentProjectDetails; - const userRole = userStore.currentProjectRole; - - const isAdmin = userRole === EUserWorkspaceRoles.ADMIN; + const isAdmin = currentProjectRole === EUserProjectRoles.ADMIN; return ( <> @@ -54,24 +53,28 @@ export const AutoArchiveAutomation: React.FC = observer((props) => {
- projectDetails?.archive_in === 0 ? handleChange({ archive_in: 1 }) : handleChange({ archive_in: 0 }) + currentProjectDetails?.archive_in === 0 + ? handleChange({ archive_in: 1 }) + : handleChange({ archive_in: 0 }) } size="sm" disabled={!isAdmin} />
- {projectDetails ? ( - projectDetails.archive_in !== 0 && ( + {currentProjectDetails ? ( + currentProjectDetails.archive_in !== 0 && (
Auto-archive issues that are closed for
{ handleChange({ archive_in: val }); }} diff --git a/web/components/automation/auto-close-automation.tsx b/web/components/automation/auto-close-automation.tsx index d21eb8b8097..49dd77e107d 100644 --- a/web/components/automation/auto-close-automation.tsx +++ b/web/components/automation/auto-close-automation.tsx @@ -1,17 +1,16 @@ import React, { useState } from "react"; import { observer } from "mobx-react-lite"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +// hooks +import { useProject, useProjectState, useUser } from "hooks/store"; // component import { SelectMonthModal } from "components/automation"; import { CustomSelect, CustomSearchSelect, ToggleSwitch, StateGroupIcon, DoubleCircleIcon, Loader } from "@plane/ui"; // icons import { ArchiveX } from "lucide-react"; // types -import { IProject } from "types"; -// fetch keys -import { PROJECT_AUTOMATION_MONTHS } from "constants/project"; -import { EUserWorkspaceRoles } from "constants/workspace"; +import { IProject } from "@plane/types"; +// constants +import { EUserProjectRoles, PROJECT_AUTOMATION_MONTHS } from "constants/project"; type Props = { handleChange: (formData: Partial) => Promise; @@ -21,15 +20,16 @@ export const AutoCloseAutomation: React.FC = observer((props) => { const { handleChange } = props; // states const [monthModal, setmonthModal] = useState(false); + // store hooks + const { + membership: { currentProjectRole }, + } = useUser(); + const { currentProjectDetails } = useProject(); + const { projectStates } = useProjectState(); - const { user: userStore, project: projectStore, projectState: projectStateStore } = useMobxStore(); - - const userRole = userStore.currentProjectRole; - const projectDetails = projectStore.currentProjectDetails; // const stateGroups = projectStateStore.groupedProjectStates ?? undefined; - const states = projectStateStore.projectStates; - const options = states + const options = projectStates ?.filter((state) => state.group === "cancelled") .map((state) => ({ value: state.id, @@ -44,17 +44,17 @@ export const AutoCloseAutomation: React.FC = observer((props) => { const multipleOptions = (options ?? []).length > 1; - const defaultState = states?.find((s) => s.group === "cancelled")?.id || null; + const defaultState = projectStates?.find((s) => s.group === "cancelled")?.id || null; - const selectedOption = states?.find((s) => s.id === projectDetails?.default_state ?? defaultState); - const currentDefaultState = states?.find((s) => s.id === defaultState); + const selectedOption = projectStates?.find((s) => s.id === currentProjectDetails?.default_state ?? defaultState); + const currentDefaultState = projectStates?.find((s) => s.id === defaultState); const initialValues: Partial = { close_in: 1, default_state: defaultState, }; - const isAdmin = userRole === EUserWorkspaceRoles.ADMIN; + const isAdmin = currentProjectRole === EUserProjectRoles.ADMIN; return ( <> @@ -79,9 +79,9 @@ export const AutoCloseAutomation: React.FC = observer((props) => {
- projectDetails?.close_in === 0 + currentProjectDetails?.close_in === 0 ? handleChange({ close_in: 1, default_state: defaultState }) : handleChange({ close_in: 0, default_state: null }) } @@ -90,16 +90,18 @@ export const AutoCloseAutomation: React.FC = observer((props) => { />
- {projectDetails ? ( - projectDetails.close_in !== 0 && ( + {currentProjectDetails ? ( + currentProjectDetails.close_in !== 0 && (
Auto-close issues that are inactive for
{ handleChange({ close_in: val }); }} @@ -118,7 +120,7 @@ export const AutoCloseAutomation: React.FC = observer((props) => { className="flex w-full select-none items-center rounded px-1 py-1.5 text-custom-text-200 hover:bg-custom-background-80" onClick={() => setmonthModal(true)} > - Customise Time Range + Customize Time Range @@ -129,7 +131,7 @@ export const AutoCloseAutomation: React.FC = observer((props) => {
Auto-close Status
{selectedOption ? ( diff --git a/web/components/automation/select-month-modal.tsx b/web/components/automation/select-month-modal.tsx index eff42bb2d78..1d306bb0401 100644 --- a/web/components/automation/select-month-modal.tsx +++ b/web/components/automation/select-month-modal.tsx @@ -7,7 +7,7 @@ import { Dialog, Transition } from "@headlessui/react"; // ui import { Button, Input } from "@plane/ui"; // types -import type { IProject } from "types"; +import type { IProject } from "@plane/types"; // types type Props = { diff --git a/web/components/command-palette/actions/help-actions.tsx b/web/components/command-palette/actions/help-actions.tsx index 859a6d23a19..4aaaab33a95 100644 --- a/web/components/command-palette/actions/help-actions.tsx +++ b/web/components/command-palette/actions/help-actions.tsx @@ -1,7 +1,7 @@ import { Command } from "cmdk"; import { FileText, GithubIcon, MessageSquare, Rocket } from "lucide-react"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +// hooks +import { useApplication } from "hooks/store"; // ui import { DiscordIcon } from "@plane/ui"; @@ -14,7 +14,7 @@ export const CommandPaletteHelpActions: React.FC = (props) => { const { commandPalette: { toggleShortcutModal }, - } = useMobxStore(); + } = useApplication(); return ( diff --git a/web/components/command-palette/actions/issue-actions/actions-list.tsx b/web/components/command-palette/actions/issue-actions/actions-list.tsx index 8e188df7b3c..55f72c85d11 100644 --- a/web/components/command-palette/actions/issue-actions/actions-list.tsx +++ b/web/components/command-palette/actions/issue-actions/actions-list.tsx @@ -2,8 +2,8 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import { Command } from "cmdk"; import { LinkIcon, Signal, Trash2, UserMinus2, UserPlus2 } from "lucide-react"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +// hooks +import { useApplication, useUser, useIssues } from "hooks/store"; // hooks import useToast from "hooks/use-toast"; // ui @@ -11,11 +11,12 @@ import { DoubleCircleIcon, UserGroupIcon } from "@plane/ui"; // helpers import { copyTextToClipboard } from "helpers/string.helper"; // types -import { IIssue } from "types"; +import { TIssue } from "@plane/types"; +import { EIssuesStoreType } from "constants/issue"; type Props = { closePalette: () => void; - issueDetails: IIssue | undefined; + issueDetails: TIssue | undefined; pages: string[]; setPages: (pages: string[]) => void; setPlaceholder: (placeholder: string) => void; @@ -28,15 +29,17 @@ export const CommandPaletteIssueActions: React.FC = observer((props) => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; + const { + issues: { updateIssue }, + } = useIssues(EIssuesStoreType.PROJECT); const { commandPalette: { toggleCommandPaletteModal, toggleDeleteIssueModal }, - projectIssues: { updateIssue }, - user: { currentUser }, - } = useMobxStore(); + } = useApplication(); + const { currentUser } = useUser(); const { setToastAlert } = useToast(); - const handleUpdateIssue = async (formData: Partial) => { + const handleUpdateIssue = async (formData: Partial) => { if (!workspaceSlug || !projectId || !issueDetails) return; const payload = { ...formData }; @@ -49,12 +52,12 @@ export const CommandPaletteIssueActions: React.FC = observer((props) => { if (!issueDetails || !assignee) return; closePalette(); - const updatedAssignees = issueDetails.assignees ?? []; + const updatedAssignees = issueDetails.assignee_ids ?? []; if (updatedAssignees.includes(assignee)) updatedAssignees.splice(updatedAssignees.indexOf(assignee), 1); else updatedAssignees.push(assignee); - handleUpdateIssue({ assignees: updatedAssignees }); + handleUpdateIssue({ assignee_ids: updatedAssignees }); }; const deleteIssue = () => { @@ -130,7 +133,7 @@ export const CommandPaletteIssueActions: React.FC = observer((props) => { className="focus:outline-none" >
- {issueDetails?.assignees.includes(currentUser?.id ?? "") ? ( + {issueDetails?.assignee_ids.includes(currentUser?.id ?? "") ? ( <> Un-assign from me diff --git a/web/components/command-palette/actions/issue-actions/change-assignee.tsx b/web/components/command-palette/actions/issue-actions/change-assignee.tsx index 57af2b62a73..96fba41f6e2 100644 --- a/web/components/command-palette/actions/issue-actions/change-assignee.tsx +++ b/web/components/command-palette/actions/issue-actions/change-assignee.tsx @@ -3,15 +3,16 @@ import { observer } from "mobx-react-lite"; import { Command } from "cmdk"; import { Check } from "lucide-react"; // mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +import { useIssues, useMember } from "hooks/store"; // ui import { Avatar } from "@plane/ui"; // types -import { IIssue } from "types"; +import { TIssue } from "@plane/types"; +import { EIssuesStoreType } from "constants/issue"; type Props = { closePalette: () => void; - issue: IIssue; + issue: TIssue; }; export const ChangeIssueAssignee: React.FC = observer((props) => { @@ -21,30 +22,40 @@ export const ChangeIssueAssignee: React.FC = observer((props) => { const { workspaceSlug, projectId } = router.query; // store const { - projectIssues: { updateIssue }, - projectMember: { projectMembers }, - } = useMobxStore(); + issues: { updateIssue }, + } = useIssues(EIssuesStoreType.PROJECT); + const { + project: { projectMemberIds, getProjectMemberDetails }, + } = useMember(); const options = - projectMembers?.map(({ member }) => ({ - value: member.id, - query: member.display_name, - content: ( - <> -
- - {member.display_name} -
- {issue.assignees.includes(member.id) && ( -
- + projectMemberIds?.map((userId) => { + const memberDetails = getProjectMemberDetails(userId); + + return { + value: `${memberDetails?.member?.id}`, + query: `${memberDetails?.member?.display_name}`, + content: ( + <> +
+ + {memberDetails?.member?.display_name}
- )} - - ), - })) ?? []; + {issue.assignee_ids.includes(memberDetails?.member?.id ?? "") && ( +
+ +
+ )} + + ), + }; + }) ?? []; - const handleUpdateIssue = async (formData: Partial) => { + const handleUpdateIssue = async (formData: Partial) => { if (!workspaceSlug || !projectId || !issue) return; const payload = { ...formData }; @@ -54,18 +65,18 @@ export const ChangeIssueAssignee: React.FC = observer((props) => { }; const handleIssueAssignees = (assignee: string) => { - const updatedAssignees = issue.assignees ?? []; + const updatedAssignees = issue.assignee_ids ?? []; if (updatedAssignees.includes(assignee)) updatedAssignees.splice(updatedAssignees.indexOf(assignee), 1); else updatedAssignees.push(assignee); - handleUpdateIssue({ assignees: updatedAssignees }); + handleUpdateIssue({ assignee_ids: updatedAssignees }); closePalette(); }; return ( <> - {options.map((option: any) => ( + {options.map((option) => ( handleIssueAssignees(option.value)} diff --git a/web/components/command-palette/actions/issue-actions/change-priority.tsx b/web/components/command-palette/actions/issue-actions/change-priority.tsx index 81b9f7ae90c..8d1c482610a 100644 --- a/web/components/command-palette/actions/issue-actions/change-priority.tsx +++ b/web/components/command-palette/actions/issue-actions/change-priority.tsx @@ -3,17 +3,17 @@ import { observer } from "mobx-react-lite"; import { Command } from "cmdk"; import { Check } from "lucide-react"; // mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +import { useIssues } from "hooks/store"; // ui import { PriorityIcon } from "@plane/ui"; // types -import { IIssue, TIssuePriorities } from "types"; +import { TIssue, TIssuePriorities } from "@plane/types"; // constants -import { PRIORITIES } from "constants/project"; +import { EIssuesStoreType, ISSUE_PRIORITIES } from "constants/issue"; type Props = { closePalette: () => void; - issue: IIssue; + issue: TIssue; }; export const ChangeIssuePriority: React.FC = observer((props) => { @@ -23,10 +23,10 @@ export const ChangeIssuePriority: React.FC = observer((props) => { const { workspaceSlug, projectId } = router.query; const { - projectIssues: { updateIssue }, - } = useMobxStore(); + issues: { updateIssue }, + } = useIssues(EIssuesStoreType.PROJECT); - const submitChanges = async (formData: Partial) => { + const submitChanges = async (formData: Partial) => { if (!workspaceSlug || !projectId || !issue) return; const payload = { ...formData }; @@ -42,13 +42,13 @@ export const ChangeIssuePriority: React.FC = observer((props) => { return ( <> - {PRIORITIES.map((priority) => ( - handleIssueState(priority)} className="focus:outline-none"> + {ISSUE_PRIORITIES.map((priority) => ( + handleIssueState(priority.key)} className="focus:outline-none">
- - {priority ?? "None"} + + {priority.title ?? "None"}
-
{priority === issue.priority && }
+
{priority.key === issue.priority && }
))} diff --git a/web/components/command-palette/actions/issue-actions/change-state.tsx b/web/components/command-palette/actions/issue-actions/change-state.tsx index 0ce05bd7b0f..7841a4a1e65 100644 --- a/web/components/command-palette/actions/issue-actions/change-state.tsx +++ b/web/components/command-palette/actions/issue-actions/change-state.tsx @@ -1,33 +1,33 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; -// cmdk import { Command } from "cmdk"; +// hooks +import { useProjectState, useIssues } from "hooks/store"; // ui import { Spinner, StateGroupIcon } from "@plane/ui"; // icons import { Check } from "lucide-react"; // types -import { IIssue } from "types"; +import { TIssue } from "@plane/types"; +import { EIssuesStoreType } from "constants/issue"; type Props = { closePalette: () => void; - issue: IIssue; + issue: TIssue; }; export const ChangeIssueState: React.FC = observer((props) => { const { closePalette, issue } = props; - + // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; - + // store hooks const { - projectState: { projectStates }, - projectIssues: { updateIssue }, - } = useMobxStore(); + issues: { updateIssue }, + } = useIssues(EIssuesStoreType.PROJECT); + const { projectStates } = useProjectState(); - const submitChanges = async (formData: Partial) => { + const submitChanges = async (formData: Partial) => { if (!workspaceSlug || !projectId || !issue) return; const payload = { ...formData }; @@ -37,7 +37,7 @@ export const ChangeIssueState: React.FC = observer((props) => { }; const handleIssueState = (stateId: string) => { - submitChanges({ state: stateId }); + submitChanges({ state_id: stateId }); closePalette(); }; @@ -51,7 +51,7 @@ export const ChangeIssueState: React.FC = observer((props) => {

{state.name}

-
{state.id === issue.state && }
+
{state.id === issue.state_id && }
)) ) : ( diff --git a/web/components/command-palette/actions/project-actions.tsx b/web/components/command-palette/actions/project-actions.tsx index 1e10b3a4645..44b5e6111fd 100644 --- a/web/components/command-palette/actions/project-actions.tsx +++ b/web/components/command-palette/actions/project-actions.tsx @@ -1,7 +1,7 @@ import { Command } from "cmdk"; import { ContrastIcon, FileText } from "lucide-react"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +// hooks +import { useApplication } from "hooks/store"; // ui import { DiceIcon, PhotoFilterIcon } from "@plane/ui"; @@ -14,8 +14,8 @@ export const CommandPaletteProjectActions: React.FC = (props) => { const { commandPalette: { toggleCreateCycleModal, toggleCreateModuleModal, toggleCreatePageModal, toggleCreateViewModal }, - trackEvent: { setTrackElement }, - } = useMobxStore(); + eventTracker: { setTrackElement }, + } = useApplication(); return ( <> diff --git a/web/components/command-palette/actions/search-results.tsx b/web/components/command-palette/actions/search-results.tsx index 791c6265610..769a26be7f4 100644 --- a/web/components/command-palette/actions/search-results.tsx +++ b/web/components/command-palette/actions/search-results.tsx @@ -3,7 +3,7 @@ import { Command } from "cmdk"; // helpers import { commandGroups } from "components/command-palette"; // types -import { IWorkspaceSearchResults } from "types"; +import { IWorkspaceSearchResults } from "@plane/types"; type Props = { closePalette: () => void; diff --git a/web/components/command-palette/actions/theme-actions.tsx b/web/components/command-palette/actions/theme-actions.tsx index f7266a48a69..976a63c871d 100644 --- a/web/components/command-palette/actions/theme-actions.tsx +++ b/web/components/command-palette/actions/theme-actions.tsx @@ -4,8 +4,8 @@ import { useTheme } from "next-themes"; import { Settings } from "lucide-react"; import { observer } from "mobx-react-lite"; // hooks +import { useUser } from "hooks/store"; import useToast from "hooks/use-toast"; -import { useMobxStore } from "lib/mobx/store-provider"; // constants import { THEME_OPTIONS } from "constants/themes"; @@ -18,9 +18,7 @@ export const CommandPaletteThemeActions: FC = observer((props) => { // states const [mounted, setMounted] = useState(false); // store - const { - user: { updateCurrentUserTheme }, - } = useMobxStore(); + const { updateCurrentUserTheme } = useUser(); // hooks const { setTheme } = useTheme(); const { setToastAlert } = useToast(); diff --git a/web/components/command-palette/command-modal.tsx b/web/components/command-palette/command-modal.tsx index 005e570e70e..34282782536 100644 --- a/web/components/command-palette/command-modal.tsx +++ b/web/components/command-palette/command-modal.tsx @@ -5,8 +5,8 @@ import { Command } from "cmdk"; import { Dialog, Transition } from "@headlessui/react"; import { observer } from "mobx-react-lite"; import { FolderPlus, Search, Settings } from "lucide-react"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +// hooks +import { useApplication, useProject } from "hooks/store"; // services import { WorkspaceService } from "services/workspace.service"; import { IssueService } from "services/issue"; @@ -26,7 +26,7 @@ import { } from "components/command-palette"; import { LayersIcon, Loader, ToggleSwitch, Tooltip } from "@plane/ui"; // types -import { IWorkspaceSearchResults } from "types"; +import { IWorkspaceSearchResults } from "@plane/types"; // fetch-keys import { ISSUE_DETAILS } from "constants/fetch-keys"; @@ -35,6 +35,8 @@ const workspaceService = new WorkspaceService(); const issueService = new IssueService(); export const CommandModal: React.FC = observer(() => { + // hooks + const { getProjectById } = useProject(); // states const [placeholder, setPlaceholder] = useState("Type a command or search..."); const [resultsCount, setResultsCount] = useState(0); @@ -62,8 +64,8 @@ export const CommandModal: React.FC = observer(() => { toggleCreateIssueModal, toggleCreateProjectModal, }, - trackEvent: { setTrackElement }, - } = useMobxStore(); + eventTracker: { setTrackElement }, + } = useApplication(); // router const router = useRouter(); @@ -135,6 +137,8 @@ export const CommandModal: React.FC = observer(() => { [debouncedSearchTerm, isWorkspaceLevel, projectId, workspaceSlug] // Only call effect if debounced search term changes ); + const projectDetails = getProjectById(issueDetails?.project_id ?? ""); + return ( setSearchTerm("")} as={React.Fragment}> closePalette()}> @@ -188,7 +192,7 @@ export const CommandModal: React.FC = observer(() => { > {issueDetails && (
- {issueDetails.project_detail.identifier}-{issueDetails.sequence_id} {issueDetails.name} + {projectDetails?.identifier}-{issueDetails.sequence_id} {issueDetails.name}
)} {projectId && ( diff --git a/web/components/command-palette/command-pallette.tsx b/web/components/command-palette/command-palette.tsx similarity index 94% rename from web/components/command-palette/command-pallette.tsx rename to web/components/command-palette/command-palette.tsx index 0488455fb9d..e5f781dd92e 100644 --- a/web/components/command-palette/command-pallette.tsx +++ b/web/components/command-palette/command-palette.tsx @@ -3,6 +3,7 @@ import { useRouter } from "next/router"; import useSWR from "swr"; import { observer } from "mobx-react-lite"; // hooks +import { useApplication, useIssues, useUser } from "hooks/store"; import useToast from "hooks/use-toast"; // components import { CommandModal, ShortcutsModal } from "components/command-palette"; @@ -19,8 +20,7 @@ import { copyTextToClipboard } from "helpers/string.helper"; import { IssueService } from "services/issue"; // fetch keys import { ISSUE_DETAILS } from "constants/fetch-keys"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +import { EIssuesStoreType } from "constants/issue"; // services const issueService = new IssueService(); @@ -28,14 +28,17 @@ const issueService = new IssueService(); export const CommandPalette: FC = observer(() => { const router = useRouter(); const { workspaceSlug, projectId, issueId, cycleId, moduleId } = router.query; - // store + const { commandPalette, theme: { toggleSidebar }, - user: { currentUser }, - trackEvent: { setTrackElement }, - projectIssues: { removeIssue }, - } = useMobxStore(); + eventTracker: { setTrackElement }, + } = useApplication(); + const { currentUser } = useUser(); + const { + issues: { removeIssue }, + } = useIssues(EIssuesStoreType.PROJECT); + const { toggleCommandPaletteModal, isCreateIssueModalOpen, @@ -214,7 +217,7 @@ export const CommandPalette: FC = observer(() => { isOpen={isCreateIssueModalOpen} handleClose={() => toggleCreateIssueModal(false)} prePopulateData={ - cycleId ? { cycle: cycleId.toString() } : moduleId ? { module: moduleId.toString() } : undefined + cycleId ? { cycle_id: cycleId.toString() } : moduleId ? { module_id: moduleId.toString() } : undefined } currentStore={createIssueStoreType} /> diff --git a/web/components/command-palette/helpers.tsx b/web/components/command-palette/helpers.tsx index 8bf0c9938f7..44fc55bbeb5 100644 --- a/web/components/command-palette/helpers.tsx +++ b/web/components/command-palette/helpers.tsx @@ -6,7 +6,7 @@ import { IWorkspaceIssueSearchResult, IWorkspaceProjectSearchResult, IWorkspaceSearchResult, -} from "types"; +} from "@plane/types"; export const commandGroups: { [key: string]: { diff --git a/web/components/command-palette/index.ts b/web/components/command-palette/index.ts index 192ef8ef9fc..0d2e042a7fe 100644 --- a/web/components/command-palette/index.ts +++ b/web/components/command-palette/index.ts @@ -1,5 +1,5 @@ export * from "./actions"; export * from "./shortcuts-modal"; export * from "./command-modal"; -export * from "./command-pallette"; +export * from "./command-palette"; export * from "./helpers"; diff --git a/web/components/common/new-empty-state.tsx b/web/components/common/new-empty-state.tsx index 7bad18734ec..dbe654e1131 100644 --- a/web/components/common/new-empty-state.tsx +++ b/web/components/common/new-empty-state.tsx @@ -19,7 +19,7 @@ type Props = { icon?: any; text: string; onClick: () => void; - } | null; + }; disabled?: boolean; }; diff --git a/web/components/core/activity.tsx b/web/components/core/activity.tsx index 0fd9e90f16a..99396dda233 100644 --- a/web/components/core/activity.tsx +++ b/web/components/core/activity.tsx @@ -1,9 +1,8 @@ import { useRouter } from "next/router"; +import { useEffect } from "react"; import { observer } from "mobx-react-lite"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; -// hook -import useEstimateOption from "hooks/use-estimate-option"; +// store hooks +import { useEstimate, useLabel } from "hooks/store"; // icons import { Tooltip, BlockedIcon, BlockerIcon, RelatedIcon, LayersIcon, DiceIcon } from "@plane/ui"; import { @@ -25,8 +24,7 @@ import { import { renderFormattedDate } from "helpers/date-time.helper"; import { capitalizeFirstLetter } from "helpers/string.helper"; // types -import { IIssueActivity } from "types"; -import { useEffect } from "react"; +import { IIssueActivity } from "@plane/types"; const IssueLink = ({ activity }: { activity: IIssueActivity }) => { const router = useRouter(); @@ -73,11 +71,10 @@ const UserLink = ({ activity }: { activity: IIssueActivity }) => { }; const LabelPill = observer(({ labelId, workspaceSlug }: { labelId: string; workspaceSlug: string }) => { + // store hooks const { - workspace: { labels, fetchWorkspaceLabels }, - } = useMobxStore(); - - const workspaceLabels = labels[workspaceSlug]; + workspace: { workspaceLabels, fetchWorkspaceLabels }, + } = useLabel(); useEffect(() => { if (!workspaceLabels) fetchWorkspaceLabels(workspaceSlug); @@ -94,16 +91,21 @@ const LabelPill = observer(({ labelId, workspaceSlug }: { labelId: string; works ); }); -const EstimatePoint = ({ point }: { point: string }) => { - const { estimateValue, isEstimateActive } = useEstimateOption(Number(point)); +const EstimatePoint = observer((props: { point: string }) => { + const { point } = props; + const { areEstimatesEnabledForCurrentProject, getEstimatePointValue } = useEstimate(); const currentPoint = Number(point) + 1; + const estimateValue = getEstimatePointValue(Number(point)); + return ( - {isEstimateActive ? estimateValue : `${currentPoint} ${currentPoint > 1 ? "points" : "point"}`} + {areEstimatesEnabledForCurrentProject + ? estimateValue + : `${currentPoint} ${currentPoint > 1 ? "points" : "point"}`} ); -}; +}); const activityDetails: { [key: string]: { diff --git a/web/components/core/image-picker-popover.tsx b/web/components/core/image-picker-popover.tsx index 41fe05b3f74..9f8023833e2 100644 --- a/web/components/core/image-picker-popover.tsx +++ b/web/components/core/image-picker-popover.tsx @@ -6,8 +6,8 @@ import useSWR from "swr"; import { useDropzone } from "react-dropzone"; import { Tab, Transition, Popover } from "@headlessui/react"; import { Control, Controller } from "react-hook-form"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +// hooks +import { useApplication, useWorkspace } from "hooks/store"; // services import { FileService } from "services/file.service"; // hooks @@ -45,25 +45,24 @@ const fileService = new FileService(); export const ImagePickerPopover: React.FC = observer((props) => { const { label, value, control, onChange, disabled = false } = props; - + // states const [image, setImage] = useState(null); const [isImageUploading, setIsImageUploading] = useState(false); - const [isOpen, setIsOpen] = useState(false); const [searchParams, setSearchParams] = useState(""); const [formData, setFormData] = useState({ search: "", }); - + // refs const ref = useRef(null); - + // router const router = useRouter(); const { workspaceSlug } = router.query; - + // store hooks const { - workspace: { currentWorkspace }, - appConfig: { envConfig }, - } = useMobxStore(); + config: { envConfig }, + } = useApplication(); + const { currentWorkspace } = useWorkspace(); const { data: unsplashImages, error: unsplashError } = useSWR( `UNSPLASH_IMAGES_${searchParams}`, @@ -119,7 +118,7 @@ export const ImagePickerPopover: React.FC = observer((props) => { if (oldValue && currentWorkspace) fileService.deleteFile(currentWorkspace.id, oldValue); }) .catch((err) => { - console.log(err); + console.error(err); }); }; diff --git a/web/components/core/modals/bulk-delete-issues-modal-item.tsx b/web/components/core/modals/bulk-delete-issues-modal-item.tsx new file mode 100644 index 00000000000..8fa8dabda6e --- /dev/null +++ b/web/components/core/modals/bulk-delete-issues-modal-item.tsx @@ -0,0 +1,38 @@ +import { observer } from "mobx-react-lite"; +import { Combobox } from "@headlessui/react"; +// hooks +import { useProjectState } from "hooks/store"; + +export const BulkDeleteIssuesModalItem: React.FC = observer((props) => { + const { issue, delete_issue_ids, identifier } = props; + const { getStateById } = useProjectState(); + + const color = getStateById(issue.state_id)?.color; + + return ( + + `flex cursor-pointer select-none items-center justify-between rounded-md px-3 py-2 ${ + active ? "bg-custom-background-80 text-custom-text-100" : "" + }` + } + > +
+ + + + {identifier}-{issue.sequence_id} + + {issue.name} +
+
+ ); +}); diff --git a/web/components/core/modals/bulk-delete-issues-modal.tsx b/web/components/core/modals/bulk-delete-issues-modal.tsx index d745e111180..6bb64682187 100644 --- a/web/components/core/modals/bulk-delete-issues-modal.tsx +++ b/web/components/core/modals/bulk-delete-issues-modal.tsx @@ -1,22 +1,25 @@ import React, { useState } from "react"; import { useRouter } from "next/router"; +import useSWR from "swr"; import { observer } from "mobx-react-lite"; import { SubmitHandler, useForm } from "react-hook-form"; import { Combobox, Dialog, Transition } from "@headlessui/react"; -import useSWR from "swr"; -// hooks -import { useMobxStore } from "lib/mobx/store-provider"; -import useToast from "hooks/use-toast"; // services import { IssueService } from "services/issue"; +// hooks +import useToast from "hooks/use-toast"; // ui import { Button, LayersIcon } from "@plane/ui"; // icons import { Search } from "lucide-react"; // types -import { IUser, IIssue } from "types"; +import { IUser, TIssue } from "@plane/types"; // fetch keys import { PROJECT_ISSUES_LIST } from "constants/fetch-keys"; +// store hooks +import { useProject } from "hooks/store"; +// components +import { BulkDeleteIssuesModalItem } from "./bulk-delete-issues-modal-item"; type FormInput = { delete_issue_ids: string[]; @@ -32,23 +35,17 @@ const issueService = new IssueService(); export const BulkDeleteIssuesModal: React.FC = observer((props) => { const { isOpen, onClose } = props; - // states - const [query, setQuery] = useState(""); // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; - // store hooks - const { - user: { hasPermissionToCurrentProject }, - } = useMobxStore(); + // hooks + const { getProjectById } = useProject(); + // states + const [query, setQuery] = useState(""); // fetching project issues. const { data: issues } = useSWR( - workspaceSlug && projectId && hasPermissionToCurrentProject - ? PROJECT_ISSUES_LIST(workspaceSlug.toString(), projectId.toString()) - : null, - workspaceSlug && projectId && hasPermissionToCurrentProject - ? () => issueService.getIssues(workspaceSlug.toString(), projectId.toString()) - : null + workspaceSlug && projectId ? PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string) : null, + workspaceSlug && projectId ? () => issueService.getIssues(workspaceSlug as string, projectId as string) : null ); const { setToastAlert } = useToast(); @@ -107,13 +104,15 @@ export const BulkDeleteIssuesModal: React.FC = observer((props) => { ); }; - const filteredIssues: IIssue[] = + const projectDetails = getProjectById(projectId as string); + + const filteredIssues: TIssue[] = query === "" ? Object.values(issues ?? {}) : Object.values(issues ?? {})?.filter( (issue) => issue.name.toLowerCase().includes(query.toLowerCase()) || - `${issue.project_detail.identifier}-${issue.sequence_id}`.toLowerCase().includes(query.toLowerCase()) + `${projectDetails?.identifier}-${issue.sequence_id}`.toLowerCase().includes(query.toLowerCase()) ) ?? []; return ( @@ -169,34 +168,12 @@ export const BulkDeleteIssuesModal: React.FC = observer((props) => { )}
    {filteredIssues.map((issue) => ( - - `flex cursor-pointer select-none items-center justify-between rounded-md px-3 py-2 ${ - active ? "bg-custom-background-80 text-custom-text-100" : "" - }` - } - > -
    - - - - {issue.project_detail.identifier}-{issue.sequence_id} - - {issue.name} -
    -
    + /> ))}
diff --git a/web/components/core/modals/existing-issues-list-modal.tsx b/web/components/core/modals/existing-issues-list-modal.tsx index 43d8b4f8963..ee3144f3bc5 100644 --- a/web/components/core/modals/existing-issues-list-modal.tsx +++ b/web/components/core/modals/existing-issues-list-modal.tsx @@ -10,7 +10,7 @@ import useDebounce from "hooks/use-debounce"; // ui import { Button, LayersIcon, Loader, ToggleSwitch, Tooltip } from "@plane/ui"; // types -import { ISearchIssueResponse, TProjectIssuesSearchParams } from "types"; +import { ISearchIssueResponse, TProjectIssuesSearchParams } from "@plane/types"; type Props = { isOpen: boolean; diff --git a/web/components/core/modals/link-modal.tsx b/web/components/core/modals/link-modal.tsx index 9f0ec41bce0..1c1372e8d9b 100644 --- a/web/components/core/modals/link-modal.tsx +++ b/web/components/core/modals/link-modal.tsx @@ -7,7 +7,7 @@ import { Dialog, Transition } from "@headlessui/react"; // ui import { Button, Input } from "@plane/ui"; // types -import type { IIssueLink, ILinkDetails, ModuleLink } from "types"; +import type { IIssueLink, ILinkDetails, ModuleLink } from "@plane/types"; type Props = { isOpen: boolean; diff --git a/web/components/core/modals/user-image-upload-modal.tsx b/web/components/core/modals/user-image-upload-modal.tsx index 11bda44cee3..6debc2c15e2 100644 --- a/web/components/core/modals/user-image-upload-modal.tsx +++ b/web/components/core/modals/user-image-upload-modal.tsx @@ -2,8 +2,8 @@ import React, { useState } from "react"; import { observer } from "mobx-react-lite"; import { useDropzone } from "react-dropzone"; import { Transition, Dialog } from "@headlessui/react"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +// hooks +import { useApplication } from "hooks/store"; // services import { FileService } from "services/file.service"; // hooks @@ -32,12 +32,12 @@ export const UserImageUploadModal: React.FC = observer((props) => { // states const [image, setImage] = useState(null); const [isImageUploading, setIsImageUploading] = useState(false); - + // toast alert const { setToastAlert } = useToast(); - + // store hooks const { - appConfig: { envConfig }, - } = useMobxStore(); + config: { envConfig }, + } = useApplication(); const onDrop = (acceptedFiles: File[]) => setImage(acceptedFiles[0]); diff --git a/web/components/core/modals/workspace-image-upload-modal.tsx b/web/components/core/modals/workspace-image-upload-modal.tsx index 166e911f57b..e04ccf8209d 100644 --- a/web/components/core/modals/workspace-image-upload-modal.tsx +++ b/web/components/core/modals/workspace-image-upload-modal.tsx @@ -3,8 +3,8 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import { useDropzone } from "react-dropzone"; import { Transition, Dialog } from "@headlessui/react"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +// hooks +import { useApplication, useWorkspace } from "hooks/store"; // services import { FileService } from "services/file.service"; // hooks @@ -40,9 +40,9 @@ export const WorkspaceImageUploadModal: React.FC = observer((props) => { const { setToastAlert } = useToast(); const { - workspace: { currentWorkspace }, - appConfig: { envConfig }, - } = useMobxStore(); + config: { envConfig }, + } = useApplication(); + const { currentWorkspace } = useWorkspace(); const onDrop = (acceptedFiles: File[]) => setImage(acceptedFiles[0]); diff --git a/web/components/core/sidebar/links-list.tsx b/web/components/core/sidebar/links-list.tsx index 3edcb906612..52b1e9de1b6 100644 --- a/web/components/core/sidebar/links-list.tsx +++ b/web/components/core/sidebar/links-list.tsx @@ -5,7 +5,7 @@ import { Pencil, Trash2, LinkIcon } from "lucide-react"; // helpers import { calculateTimeAgo } from "helpers/date-time.helper"; // types -import { ILinkDetails, UserAuth } from "types"; +import { ILinkDetails, UserAuth } from "@plane/types"; // hooks import useToast from "hooks/use-toast"; @@ -50,8 +50,8 @@ export const LinksList: React.FC = ({ links, handleDeleteLink, handleEdit
-
- {!isNotAllowed && ( + {!isNotAllowed && ( +
- )} - - - - {!isNotAllowed && ( + + + - )} -
+
+ )}

diff --git a/web/components/core/sidebar/progress-chart.tsx b/web/components/core/sidebar/progress-chart.tsx index 3c445760d9c..9e9a4bac837 100644 --- a/web/components/core/sidebar/progress-chart.tsx +++ b/web/components/core/sidebar/progress-chart.tsx @@ -5,7 +5,7 @@ import { LineGraph } from "components/ui"; // helpers import { renderFormattedDateWithoutYear } from "helpers/date-time.helper"; //types -import { TCompletionChartDistribution } from "types"; +import { TCompletionChartDistribution } from "@plane/types"; type Props = { distribution: TCompletionChartDistribution; diff --git a/web/components/core/sidebar/sidebar-progress-stats.tsx b/web/components/core/sidebar/sidebar-progress-stats.tsx index 8cea3784fd3..6d89981cd93 100644 --- a/web/components/core/sidebar/sidebar-progress-stats.tsx +++ b/web/components/core/sidebar/sidebar-progress-stats.tsx @@ -14,13 +14,12 @@ import { SingleProgressStats } from "components/core"; import { Avatar, StateGroupIcon } from "@plane/ui"; // types import { - IIssueFilterOptions, IModule, TAssigneesDistribution, TCompletionChartDistribution, TLabelsDistribution, TStateGroups, -} from "types"; +} from "@plane/types"; type Props = { distribution: { @@ -36,9 +35,6 @@ type Props = { roundedTab?: boolean; noBackground?: boolean; isPeekView?: boolean; - isCompleted?: boolean; - filters?: IIssueFilterOptions; - handleFiltersUpdate: (key: keyof IIssueFilterOptions, value: string | string[]) => void; }; export const SidebarProgressStats: React.FC = ({ @@ -48,10 +44,7 @@ export const SidebarProgressStats: React.FC = ({ module, roundedTab, noBackground, - isCompleted = false, isPeekView = false, - filters, - handleFiltersUpdate, }) => { const { storedValue: tab, setValue: setTab } = useLocalStorage("tab", "Assignees"); @@ -147,11 +140,20 @@ export const SidebarProgressStats: React.FC = ({ } completed={assignee.completed_issues} total={assignee.total_issues} - {...(!isPeekView && - !isCompleted && { - onClick: () => handleFiltersUpdate("assignees", assignee.assignee_id ?? ""), - selected: filters?.assignees?.includes(assignee.assignee_id ?? ""), - })} + {...(!isPeekView && { + onClick: () => { + // TODO: set filters here + // if (filters?.assignees?.includes(assignee.assignee_id ?? "")) + // setFilters({ + // assignees: filters?.assignees?.filter((a) => a !== assignee.assignee_id), + // }); + // else + // setFilters({ + // assignees: [...(filters?.assignees ?? []), assignee.assignee_id ?? ""], + // }); + }, + // selected: filters?.assignees?.includes(assignee.assignee_id ?? ""), + })} /> ); else @@ -198,11 +200,17 @@ export const SidebarProgressStats: React.FC = ({ } completed={label.completed_issues} total={label.total_issues} - {...(!isPeekView && - !isCompleted && { - onClick: () => handleFiltersUpdate("labels", label.label_id ?? ""), - selected: filters?.labels?.includes(label.label_id ?? `no-label-${index}`), - })} + {...(!isPeekView && { + // TODO: set filters here + onClick: () => { + // if (filters.labels?.includes(label.label_id ?? "")) + // setFilters({ + // labels: filters?.labels?.filter((l) => l !== label.label_id), + // }); + // else setFilters({ labels: [...(filters?.labels ?? []), label.label_id ?? ""] }); + }, + // selected: filters?.labels?.includes(label.label_id ?? ""), + })} /> )) ) : ( diff --git a/web/components/core/theme/color-picker-input.tsx b/web/components/core/theme/color-picker-input.tsx index f47c1349f24..19cd519cbff 100644 --- a/web/components/core/theme/color-picker-input.tsx +++ b/web/components/core/theme/color-picker-input.tsx @@ -18,7 +18,7 @@ import { Input } from "@plane/ui"; // icons import { Palette } from "lucide-react"; // types -import { IUserTheme } from "types"; +import { IUserTheme } from "@plane/types"; type Props = { name: keyof IUserTheme; diff --git a/web/components/core/theme/custom-theme-selector.tsx b/web/components/core/theme/custom-theme-selector.tsx index c5517070284..bd6f4356921 100644 --- a/web/components/core/theme/custom-theme-selector.tsx +++ b/web/components/core/theme/custom-theme-selector.tsx @@ -1,12 +1,12 @@ import { observer } from "mobx-react-lite"; import { Controller, useForm } from "react-hook-form"; import { useTheme } from "next-themes"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +// hooks +import { useUser } from "hooks/store"; // ui import { Button, InputColorPicker } from "@plane/ui"; // types -import { IUserTheme } from "types"; +import { IUserTheme } from "@plane/types"; const inputRules = { required: "Background color is required", @@ -25,8 +25,8 @@ const inputRules = { }; export const CustomThemeSelector: React.FC = observer(() => { - const { user: userStore } = useMobxStore(); - const userTheme = userStore?.currentUser?.theme; + const { currentUser, updateCurrentUser } = useUser(); + const userTheme = currentUser?.theme; // hooks const { setTheme } = useTheme(); @@ -61,7 +61,7 @@ export const CustomThemeSelector: React.FC = observer(() => { setTheme("custom"); - return userStore.updateCurrentUser({ theme: payload }); + return updateCurrentUser({ theme: payload }); }; const handleValueChange = (val: string | undefined, onChange: any) => { diff --git a/web/components/cycles/active-cycle-details.tsx b/web/components/cycles/active-cycle-details.tsx index c184f80c9b6..5ef912572cb 100644 --- a/web/components/cycles/active-cycle-details.tsx +++ b/web/components/cycles/active-cycle-details.tsx @@ -3,9 +3,8 @@ 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"; // hooks +import { useApplication, useCycle, useIssues, useProjectState } from "hooks/store"; import useToast from "hooks/use-toast"; // ui import { SingleProgressStats } from "components/core"; @@ -31,7 +30,9 @@ import { AlarmClock, AlertTriangle, ArrowRight, CalendarDays, Star, Target } fro import { renderFormattedDate, findHowManyDaysLeft } from "helpers/date-time.helper"; import { truncateText } from "helpers/string.helper"; // types -import { ICycle } from "types"; +import { ICycle } from "@plane/types"; +import { EIssuesStoreType } from "constants/issue"; +import { ACTIVE_CYCLE_ISSUES } from "store/issue/cycle"; const stateGroups = [ { @@ -67,41 +68,53 @@ interface IActiveCycleDetails { } export const ActiveCycleDetails: React.FC = observer((props) => { + // router const router = useRouter(); - const { workspaceSlug, projectId } = props; - const { cycle: cycleStore, commandPalette: commandPaletteStore } = useMobxStore(); - + const { + issues: { issues }, + issueMap, + } = useIssues(EIssuesStoreType.CYCLE); + // store hooks + const { + commandPalette: { toggleCreateCycleModal }, + } = useApplication(); + const { + fetchActiveCycle, + currentProjectActiveCycleId, + getActiveCycleById, + addCycleToFavorites, + removeCycleFromFavorites, + } = useCycle(); + const { getProjectStates } = useProjectState(); + // toast alert const { setToastAlert } = useToast(); const { isLoading } = useSWR( - workspaceSlug && projectId ? `ACTIVE_CYCLE_ISSUE_${projectId}_CURRENT` : null, - workspaceSlug && projectId ? () => cycleStore.fetchCycles(workspaceSlug, projectId, "current") : null + workspaceSlug && projectId ? `PROJECT_ACTIVE_CYCLE_${projectId}` : null, + workspaceSlug && projectId ? () => fetchActiveCycle(workspaceSlug, projectId) : null ); - const activeCycle = cycleStore.cycles?.[projectId]?.current || null; - const cycle = activeCycle ? activeCycle[0] : null; - const issues = (cycleStore?.active_cycle_issues as any) || null; + const activeCycle = currentProjectActiveCycleId ? getActiveCycleById(currentProjectActiveCycleId) : null; + const issueIds = issues?.[ACTIVE_CYCLE_ISSUES]; - // const { data: issues } = useSWR( - // workspaceSlug && projectId && cycle?.id ? CYCLE_ISSUES_WITH_PARAMS(cycle?.id, { priority: "urgent,high" }) : null, - // workspaceSlug && projectId && cycle?.id + // useSWR( + // workspaceSlug && projectId && cycleId ? CYCLE_ISSUES_WITH_PARAMS(cycleId, { priority: "urgent,high" }) : null, + // workspaceSlug && projectId && cycleId // ? () => - // cycleService.getCycleIssuesWithParams(workspaceSlug as string, projectId as string, cycle.id, { - // priority: "urgent,high", - // }) + // fetchActiveCycleIssues(workspaceSlug, projectId, ) // : null - // ) as { data: IIssue[] | undefined }; + // ); - if (!cycle && isLoading) + if (!activeCycle && isLoading) return ( ); - if (!cycle) + if (!activeCycle) return (

@@ -118,7 +131,7 @@ export const ActiveCycleDetails: React.FC = observer((props @@ -126,24 +139,24 @@ export const ActiveCycleDetails: React.FC = observer((props
); - const endDate = new Date(cycle.end_date ?? ""); - const startDate = new Date(cycle.start_date ?? ""); + const endDate = new Date(activeCycle.end_date ?? ""); + const startDate = new Date(activeCycle.start_date ?? ""); const groupedIssues: any = { - backlog: cycle.backlog_issues, - unstarted: cycle.unstarted_issues, - started: cycle.started_issues, - completed: cycle.completed_issues, - cancelled: cycle.cancelled_issues, + backlog: activeCycle.backlog_issues, + unstarted: activeCycle.unstarted_issues, + started: activeCycle.started_issues, + completed: activeCycle.completed_issues, + cancelled: activeCycle.cancelled_issues, }; - const cycleStatus = cycle.status.toLocaleLowerCase(); + const cycleStatus = activeCycle.status.toLocaleLowerCase(); const handleAddToFavorites = (e: MouseEvent) => { e.preventDefault(); if (!workspaceSlug || !projectId) return; - cycleStore.addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycle).catch(() => { + addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), activeCycle.id).catch(() => { setToastAlert({ type: "error", title: "Error!", @@ -156,7 +169,7 @@ export const ActiveCycleDetails: React.FC = observer((props e.preventDefault(); if (!workspaceSlug || !projectId) return; - cycleStore.removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycle).catch(() => { + removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), activeCycle.id).catch(() => { setToastAlert({ type: "error", title: "Error!", @@ -168,7 +181,10 @@ export const ActiveCycleDetails: React.FC = observer((props const progressIndicatorData = stateGroups.map((group, index) => ({ id: index, name: group.title, - value: cycle.total_issues > 0 ? ((cycle[group.key as keyof ICycle] as number) / cycle.total_issues) * 100 : 0, + value: + activeCycle.total_issues > 0 + ? ((activeCycle[group.key as keyof ICycle] as number) / activeCycle.total_issues) * 100 + : 0, color: group.color, })); @@ -196,8 +212,8 @@ export const ActiveCycleDetails: React.FC = observer((props }`} /> - -

{truncateText(cycle.name, 70)}

+ +

{truncateText(activeCycle.name, 70)}

@@ -218,19 +234,19 @@ export const ActiveCycleDetails: React.FC = observer((props {cycleStatus === "current" ? ( - {findHowManyDaysLeft(cycle.end_date ?? new Date())} Days Left + {findHowManyDaysLeft(activeCycle.end_date ?? new Date())} Days Left ) : cycleStatus === "upcoming" ? ( - {findHowManyDaysLeft(cycle.start_date ?? new Date())} Days Left + {findHowManyDaysLeft(activeCycle.start_date ?? new Date())} Days Left ) : cycleStatus === "completed" ? ( - {cycle.total_issues - cycle.completed_issues > 0 && ( + {activeCycle.total_issues - activeCycle.completed_issues > 0 && ( @@ -244,7 +260,7 @@ export const ActiveCycleDetails: React.FC = observer((props cycleStatus )} - {cycle.is_favorite ? ( + {activeCycle.is_favorite ? (
- +
@@ -363,9 +379,9 @@ export const ActiveCycleDetails: React.FC = observer((props
High Priority Issues
- {issues ? ( - issues.length > 0 ? ( - issues.map((issue: any) => ( + {issueIds ? ( + issueIds.length > 0 ? ( + issueIds.map((issue: any) => (
router.push(`/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`)} @@ -428,24 +444,33 @@ export const ActiveCycleDetails: React.FC = observer((props
- {issues && issues.length > 0 && ( + {issueIds && issueIds.length > 0 && (
issue?.state_detail?.group === "completed")?.length / - issues.length) * + (issueIds.filter((issue: any) => issue?.state_detail?.group === "completed")?.length / + issueIds.length) * 100 ?? 0 }%`, }} />
- {issues?.filter((issue: any) => issue?.state_detail?.group === "completed")?.length} of {issues?.length} + of{" "} + { + issueIds?.filter( + (issueId) => + getProjectStates(issueMap[issueId]?.project_id).find( + (issue) => issue.id === issueMap[issueId]?.state_id + )?.group === "completed" + )?.length + }{" "} + of {issueIds?.length}
)} @@ -466,15 +491,18 @@ export const ActiveCycleDetails: React.FC = observer((props - Pending Issues - {cycle.total_issues - (cycle.completed_issues + cycle.cancelled_issues)} + + Pending Issues -{" "} + {activeCycle.total_issues - (activeCycle.completed_issues + activeCycle.cancelled_issues)} +
diff --git a/web/components/cycles/active-cycle-stats.tsx b/web/components/cycles/active-cycle-stats.tsx index 2c933989215..524b02dd053 100644 --- a/web/components/cycles/active-cycle-stats.tsx +++ b/web/components/cycles/active-cycle-stats.tsx @@ -7,7 +7,7 @@ import { SingleProgressStats } from "components/core"; // ui import { Avatar } from "@plane/ui"; // types -import { ICycle } from "types"; +import { ICycle } from "@plane/types"; type Props = { cycle: ICycle; diff --git a/web/components/cycles/cycle-peek-overview.tsx b/web/components/cycles/cycle-peek-overview.tsx index d6806eaf062..b7acff358ab 100644 --- a/web/components/cycles/cycle-peek-overview.tsx +++ b/web/components/cycles/cycle-peek-overview.tsx @@ -1,10 +1,8 @@ import React, { useEffect } from "react"; - import { useRouter } from "next/router"; - -// mobx import { observer } from "mobx-react-lite"; -import { useMobxStore } from "lib/mobx/store-provider"; +// hooks +import { useCycle } from "hooks/store"; // components import { CycleDetailsSidebar } from "./sidebar"; @@ -14,14 +12,13 @@ type Props = { }; export const CyclePeekOverview: React.FC = observer(({ projectId, workspaceSlug }) => { + // router const router = useRouter(); const { peekCycle } = router.query; - + // refs const ref = React.useRef(null); - - const { cycle: cycleStore } = useMobxStore(); - - const { fetchCycleWithId } = cycleStore; + // store hooks + const { fetchCycleDetails } = useCycle(); const handleClose = () => { delete router.query.peekCycle; @@ -33,8 +30,8 @@ export const CyclePeekOverview: React.FC = observer(({ projectId, workspa useEffect(() => { if (!peekCycle) return; - fetchCycleWithId(workspaceSlug, projectId, peekCycle.toString()); - }, [fetchCycleWithId, peekCycle, projectId, workspaceSlug]); + fetchCycleDetails(workspaceSlug, projectId, peekCycle.toString()); + }, [fetchCycleDetails, peekCycle, projectId, workspaceSlug]); return ( <> diff --git a/web/components/cycles/cycles-board-card.tsx b/web/components/cycles/cycles-board-card.tsx index ca0d487e852..f6836269c75 100644 --- a/web/components/cycles/cycles-board-card.tsx +++ b/web/components/cycles/cycles-board-card.tsx @@ -2,6 +2,7 @@ import { FC, MouseEvent, useState } from "react"; import { useRouter } from "next/router"; import Link from "next/link"; // hooks +import { useApplication, useCycle, useUser } from "hooks/store"; import useToast from "hooks/use-toast"; // components import { CycleCreateUpdateModal, CycleDeleteModal } from "components/cycles"; @@ -12,62 +13,65 @@ import { Info, LinkIcon, Pencil, Star, Trash2 } from "lucide-react"; // helpers import { findHowManyDaysLeft, renderFormattedDate } from "helpers/date-time.helper"; import { copyTextToClipboard } from "helpers/string.helper"; -// types -import { ICycle, TCycleGroups } from "types"; -// store -import { useMobxStore } from "lib/mobx/store-provider"; // constants import { CYCLE_STATUS } from "constants/cycle"; import { EUserWorkspaceRoles } from "constants/workspace"; +//.types +import { TCycleGroups } from "@plane/types"; export interface ICyclesBoardCard { workspaceSlug: string; projectId: string; - cycle: ICycle; + cycleId: string; } export const CyclesBoardCard: FC = (props) => { - const { cycle, workspaceSlug, projectId } = props; - // store - const { - cycle: cycleStore, - trackEvent: { setTrackElement }, - user: userStore, - } = useMobxStore(); - // toast - const { setToastAlert } = useToast(); + const { cycleId, workspaceSlug, projectId } = props; // states const [updateModal, setUpdateModal] = useState(false); const [deleteModal, setDeleteModal] = useState(false); + // router + const router = useRouter(); + // store + const { + eventTracker: { setTrackElement }, + } = useApplication(); + const { + membership: { currentProjectRole }, + } = useUser(); + const { addCycleToFavorites, removeCycleFromFavorites, getCycleById } = useCycle(); + // toast alert + const { setToastAlert } = useToast(); // computed - const cycleStatus = cycle.status.toLocaleLowerCase() as TCycleGroups; + const cycleDetails = getCycleById(cycleId); + + if (!cycleDetails) return null; + + const cycleStatus = cycleDetails.status.toLocaleLowerCase(); const isCompleted = cycleStatus === "completed"; - const endDate = new Date(cycle.end_date ?? ""); - const startDate = new Date(cycle.start_date ?? ""); - const isDateValid = cycle.start_date || cycle.end_date; + const endDate = new Date(cycleDetails.end_date ?? ""); + const startDate = new Date(cycleDetails.start_date ?? ""); + const isDateValid = cycleDetails.start_date || cycleDetails.end_date; - const { currentProjectRole } = userStore; const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER; - const router = useRouter(); - const currentCycle = CYCLE_STATUS.find((status) => status.value === cycleStatus); const cycleTotalIssues = - cycle.backlog_issues + - cycle.unstarted_issues + - cycle.started_issues + - cycle.completed_issues + - cycle.cancelled_issues; + cycleDetails.backlog_issues + + cycleDetails.unstarted_issues + + cycleDetails.started_issues + + cycleDetails.completed_issues + + cycleDetails.cancelled_issues; - const completionPercentage = (cycle.completed_issues / cycleTotalIssues) * 100; + const completionPercentage = (cycleDetails.completed_issues / cycleTotalIssues) * 100; - const issueCount = cycle + const issueCount = cycleDetails ? cycleTotalIssues === 0 ? "0 Issue" - : cycleTotalIssues === cycle.completed_issues + : cycleTotalIssues === cycleDetails.completed_issues ? `${cycleTotalIssues} Issue${cycleTotalIssues > 1 ? "s" : ""}` - : `${cycle.completed_issues}/${cycleTotalIssues} Issues` + : `${cycleDetails.completed_issues}/${cycleTotalIssues} Issues` : "0 Issue"; const handleCopyText = (e: MouseEvent) => { @@ -75,7 +79,7 @@ export const CyclesBoardCard: FC = (props) => { e.stopPropagation(); const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : ""; - copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`).then(() => { + copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}`).then(() => { setToastAlert({ type: "success", title: "Link Copied!", @@ -88,7 +92,7 @@ export const CyclesBoardCard: FC = (props) => { e.preventDefault(); if (!workspaceSlug || !projectId) return; - cycleStore.addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycle).catch(() => { + addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId).catch(() => { setToastAlert({ type: "error", title: "Error!", @@ -101,7 +105,7 @@ export const CyclesBoardCard: FC = (props) => { e.preventDefault(); if (!workspaceSlug || !projectId) return; - cycleStore.removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycle).catch(() => { + removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId).catch(() => { setToastAlert({ type: "error", title: "Error!", @@ -130,14 +134,14 @@ export const CyclesBoardCard: FC = (props) => { router.push({ pathname: router.pathname, - query: { ...query, peekCycle: cycle.id }, + query: { ...query, peekCycle: cycleId }, }); }; return (
setUpdateModal(false)} workspaceSlug={workspaceSlug} @@ -145,22 +149,22 @@ export const CyclesBoardCard: FC = (props) => { /> setDeleteModal(false)} workspaceSlug={workspaceSlug} projectId={projectId} /> - +
- + - - {cycle.name} + + {cycleDetails.name}
@@ -173,7 +177,7 @@ export const CyclesBoardCard: FC = (props) => { }} > {currentCycle.value === "current" - ? `${findHowManyDaysLeft(cycle.end_date ?? new Date())} ${currentCycle.label}` + ? `${findHowManyDaysLeft(cycleDetails.end_date ?? new Date())} ${currentCycle.label}` : `${currentCycle.label}`} )} @@ -189,11 +193,11 @@ export const CyclesBoardCard: FC = (props) => { {issueCount}
- {cycle.assignees.length > 0 && ( - + {cycleDetails.assignees.length > 0 && ( +
- {cycle.assignees.map((assignee) => ( + {cycleDetails.assignees.map((assignee) => ( ))} @@ -233,7 +237,7 @@ export const CyclesBoardCard: FC = (props) => { )}
{isEditingAllowed && - (cycle.is_favorite ? ( + (cycleDetails.is_favorite ? ( diff --git a/web/components/cycles/cycles-board.tsx b/web/components/cycles/cycles-board.tsx index af234b9dc45..967e8a39527 100644 --- a/web/components/cycles/cycles-board.tsx +++ b/web/components/cycles/cycles-board.tsx @@ -1,14 +1,12 @@ import { FC } from "react"; import { observer } from "mobx-react-lite"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +// hooks +import { useApplication } from "hooks/store"; // components import { CyclePeekOverview, CyclesBoardCard } from "components/cycles"; -// types -import { ICycle } from "types"; export interface ICyclesBoard { - cycles: ICycle[]; + cycleIds: string[]; filter: string; workspaceSlug: string; projectId: string; @@ -16,13 +14,13 @@ export interface ICyclesBoard { } export const CyclesBoard: FC = observer((props) => { - const { cycles, filter, workspaceSlug, projectId, peekCycle } = props; - - const { commandPalette: commandPaletteStore } = useMobxStore(); + const { cycleIds, filter, workspaceSlug, projectId, peekCycle } = props; + // store hooks + const { commandPalette: commandPaletteStore } = useApplication(); return ( <> - {cycles.length > 0 ? ( + {cycleIds?.length > 0 ? (
= observer((props) => { : "lg:grid-cols-2 xl:grid-cols-3 3xl:grid-cols-4" } auto-rows-max transition-all `} > - {cycles.map((cycle) => ( - + {cycleIds.map((cycleId) => ( + ))}
void; handleDeleteCycle?: () => void; handleAddToFavorites?: () => void; @@ -31,50 +30,29 @@ type TCyclesListItem = { }; export const CyclesListItem: FC = (props) => { - const { cycle, workspaceSlug, projectId } = props; - // store - const { - cycle: cycleStore, - trackEvent: { setTrackElement }, - user: userStore, - } = useMobxStore(); - // toast - const { setToastAlert } = useToast(); + const { cycleId, workspaceSlug, projectId } = props; // states const [updateModal, setUpdateModal] = useState(false); const [deleteModal, setDeleteModal] = useState(false); - // computed - const cycleStatus = cycle.status.toLocaleLowerCase() as TCycleGroups; - const isCompleted = cycleStatus === "completed"; - const endDate = new Date(cycle.end_date ?? ""); - const startDate = new Date(cycle.start_date ?? ""); - - const { currentProjectRole } = userStore; - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER; - + // router const router = useRouter(); - - const cycleTotalIssues = - cycle.backlog_issues + - cycle.unstarted_issues + - cycle.started_issues + - cycle.completed_issues + - cycle.cancelled_issues; - - const renderDate = cycle.start_date || cycle.end_date; - - const completionPercentage = (cycle.completed_issues / cycleTotalIssues) * 100; - - const progress = isNaN(completionPercentage) ? 0 : Math.floor(completionPercentage); - - const currentCycle = CYCLE_STATUS.find((status) => status.value === cycleStatus); + // store hooks + const { + eventTracker: { setTrackElement }, + } = useApplication(); + const { + membership: { currentProjectRole }, + } = useUser(); + const { getCycleById, addCycleToFavorites, removeCycleFromFavorites } = useCycle(); + // toast alert + const { setToastAlert } = useToast(); const handleCopyText = (e: MouseEvent) => { e.preventDefault(); e.stopPropagation(); const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : ""; - copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`).then(() => { + copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}`).then(() => { setToastAlert({ type: "success", title: "Link Copied!", @@ -87,7 +65,7 @@ export const CyclesListItem: FC = (props) => { e.preventDefault(); if (!workspaceSlug || !projectId) return; - cycleStore.addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycle).catch(() => { + addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId).catch(() => { setToastAlert({ type: "error", title: "Error!", @@ -100,7 +78,7 @@ export const CyclesListItem: FC = (props) => { e.preventDefault(); if (!workspaceSlug || !projectId) return; - cycleStore.removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycle).catch(() => { + removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId).catch(() => { setToastAlert({ type: "error", title: "Error!", @@ -129,27 +107,56 @@ export const CyclesListItem: FC = (props) => { router.push({ pathname: router.pathname, - query: { ...query, peekCycle: cycle.id }, + query: { ...query, peekCycle: cycleId }, }); }; + const cycleDetails = getCycleById(cycleId); + + if (!cycleDetails) return null; + + // computed + const cycleStatus = cycleDetails.status.toLocaleLowerCase() as TCycleGroups; + const isCompleted = cycleStatus === "completed"; + const endDate = new Date(cycleDetails.end_date ?? ""); + const startDate = new Date(cycleDetails.start_date ?? ""); + + const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER; + + const cycleTotalIssues = + cycleDetails.backlog_issues + + cycleDetails.unstarted_issues + + cycleDetails.started_issues + + cycleDetails.completed_issues + + cycleDetails.cancelled_issues; + + const renderDate = cycleDetails.start_date || cycleDetails.end_date; + + // const areYearsEqual = startDate.getFullYear() === endDate.getFullYear(); + + const completionPercentage = (cycleDetails.completed_issues / cycleTotalIssues) * 100; + + const progress = isNaN(completionPercentage) ? 0 : Math.floor(completionPercentage); + + const currentCycle = CYCLE_STATUS.find((status) => status.value === cycleStatus); + return ( <> setUpdateModal(false)} workspaceSlug={workspaceSlug} projectId={projectId} /> setDeleteModal(false)} workspaceSlug={workspaceSlug} projectId={projectId} /> - +
@@ -173,8 +180,8 @@ export const CyclesListItem: FC = (props) => { - - {cycle.name} + + {cycleDetails.name}
@@ -194,7 +201,7 @@ export const CyclesListItem: FC = (props) => { }} > {currentCycle.value === "current" - ? `${findHowManyDaysLeft(cycle.end_date ?? new Date())} ${currentCycle.label}` + ? `${findHowManyDaysLeft(cycleDetails.end_date ?? new Date())} ${currentCycle.label}` : `${currentCycle.label}`} )} @@ -206,11 +213,11 @@ export const CyclesListItem: FC = (props) => { )} - +
- {cycle.assignees.length > 0 ? ( + {cycleDetails.assignees.length > 0 ? ( - {cycle.assignees.map((assignee) => ( + {cycleDetails.assignees.map((assignee) => ( ))} @@ -222,7 +229,7 @@ export const CyclesListItem: FC = (props) => {
{isEditingAllowed && - (cycle.is_favorite ? ( + (cycleDetails.is_favorite ? ( diff --git a/web/components/cycles/cycles-list.tsx b/web/components/cycles/cycles-list.tsx index 226807b782e..686937b712a 100644 --- a/web/components/cycles/cycles-list.tsx +++ b/web/components/cycles/cycles-list.tsx @@ -1,39 +1,37 @@ import { FC } from "react"; import { observer } from "mobx-react-lite"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +// hooks +import { useApplication } from "hooks/store"; // components import { CyclePeekOverview, CyclesListItem } from "components/cycles"; // ui import { Loader } from "@plane/ui"; -// types -import { ICycle } from "types"; export interface ICyclesList { - cycles: ICycle[]; + cycleIds: string[]; filter: string; workspaceSlug: string; projectId: string; } export const CyclesList: FC = observer((props) => { - const { cycles, filter, workspaceSlug, projectId } = props; - + const { cycleIds, filter, workspaceSlug, projectId } = props; + // store hooks const { commandPalette: commandPaletteStore, - trackEvent: { setTrackElement }, - } = useMobxStore(); + eventTracker: { setTrackElement }, + } = useApplication(); return ( <> - {cycles ? ( + {cycleIds ? ( <> - {cycles.length > 0 ? ( + {cycleIds.length > 0 ? (
- {cycles.map((cycle) => ( - + {cycleIds.map((cycleId) => ( + ))}
= observer((props) => { const { filter, layout, workspaceSlug, projectId, peekCycle } = props; - - // store - const { cycle: cycleStore } = useMobxStore(); - - // api call to fetch cycles list - useSWR( - workspaceSlug && projectId && filter ? `CYCLES_LIST_${projectId}_${filter}` : null, - workspaceSlug && projectId && filter ? () => cycleStore.fetchCycles(workspaceSlug, projectId, filter) : null - ); + // store hooks + const { + currentProjectCompletedCycleIds, + currentProjectDraftCycleIds, + currentProjectUpcomingCycleIds, + currentProjectCycleIds, + } = useCycle(); const cyclesList = filter === "completed" - ? cycleStore.projectCompletedCycles + ? currentProjectCompletedCycleIds : filter === "draft" - ? cycleStore.projectDraftCycles - : filter === "upcoming" - ? cycleStore.projectUpcomingCycles - : cycleStore.projectCycles; + ? currentProjectDraftCycleIds + : filter === "upcoming" + ? currentProjectUpcomingCycleIds + : currentProjectCycleIds; return ( <> {layout === "list" && ( <> {cyclesList ? ( - + ) : ( @@ -59,7 +56,7 @@ export const CyclesView: FC = observer((props) => { <> {cyclesList ? ( = observer((props) => { {layout === "gantt" && ( <> {cyclesList ? ( - + ) : ( diff --git a/web/components/cycles/delete-modal.tsx b/web/components/cycles/delete-modal.tsx index 33c6254df04..44da175b49a 100644 --- a/web/components/cycles/delete-modal.tsx +++ b/web/components/cycles/delete-modal.tsx @@ -1,17 +1,15 @@ import { Fragment, useState } from "react"; -// next import { useRouter } from "next/router"; import { Dialog, Transition } from "@headlessui/react"; import { observer } from "mobx-react-lite"; import { AlertTriangle } from "lucide-react"; -// components -import { Button } from "@plane/ui"; // hooks +import { useApplication, useCycle } from "hooks/store"; import useToast from "hooks/use-toast"; +// components +import { Button } from "@plane/ui"; // types -import { ICycle } from "types"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +import { ICycle } from "@plane/types"; interface ICycleDelete { cycle: ICycle; @@ -23,56 +21,51 @@ interface ICycleDelete { export const CycleDeleteModal: React.FC = observer((props) => { const { isOpen, handleClose, cycle, workspaceSlug, projectId } = props; - // store - const { - cycle: cycleStore, - trackEvent: { postHogEventTracker }, - } = useMobxStore(); - // toast - const { setToastAlert } = useToast(); // states const [loader, setLoader] = useState(false); + // router const router = useRouter(); const { cycleId, peekCycle } = router.query; + // store hooks + const { + eventTracker: { postHogEventTracker }, + } = useApplication(); + const { deleteCycle } = useCycle(); + // toast alert + const { setToastAlert } = useToast(); const formSubmit = async () => { + if (!cycle) return; + setLoader(true); - if (cycle?.id) - try { - await cycleStore - .removeCycle(workspaceSlug, projectId, cycle?.id) - .then(() => { - setToastAlert({ - type: "success", - title: "Success!", - message: "Cycle deleted successfully.", - }); - postHogEventTracker("CYCLE_DELETE", { - state: "SUCCESS", - }); - }) - .catch(() => { - postHogEventTracker("CYCLE_DELETE", { - state: "FAILED", - }); + try { + await deleteCycle(workspaceSlug, projectId, cycle.id) + .then(() => { + setToastAlert({ + type: "success", + title: "Success!", + message: "Cycle deleted successfully.", + }); + postHogEventTracker("CYCLE_DELETE", { + state: "SUCCESS", }); + }) + .catch(() => { + postHogEventTracker("CYCLE_DELETE", { + state: "FAILED", + }); + }); - if (cycleId || peekCycle) router.push(`/${workspaceSlug}/projects/${projectId}/cycles`); + if (cycleId || peekCycle) router.push(`/${workspaceSlug}/projects/${projectId}/cycles`); - handleClose(); - } catch (error) { - setToastAlert({ - type: "error", - title: "Warning!", - message: "Something went wrong please try again later.", - }); - } - else + handleClose(); + } catch (error) { setToastAlert({ type: "error", title: "Warning!", message: "Something went wrong please try again later.", }); + } setLoader(false); }; diff --git a/web/components/cycles/form.tsx b/web/components/cycles/form.tsx index 2cc087eda27..2396d040ae9 100644 --- a/web/components/cycles/form.tsx +++ b/web/components/cycles/form.tsx @@ -1,10 +1,12 @@ import { Controller, useForm } from "react-hook-form"; +// components +import { DateDropdown, ProjectDropdown } from "components/dropdowns"; // ui import { Button, Input, TextArea } from "@plane/ui"; -import { DateSelect } from "components/ui"; -import { IssueProjectSelect } from "components/issues/select"; +// helpers +import { renderFormattedPayloadDate } from "helpers/date-time.helper"; // types -import { ICycle } from "types"; +import { ICycle } from "@plane/types"; type Props = { handleFormSubmit: (values: Partial) => Promise; @@ -45,19 +47,22 @@ export const CycleForm: React.FC = (props) => {
- ( - { - onChange(val); - setActiveProject(val); - }} - /> - )} - /> + {!status && ( + ( + { + onChange(val); + setActiveProject(val); + }} + buttonVariant="background-with-text" + /> + )} + /> + )}

{status ? "Update" : "New"} Cycle

@@ -112,25 +117,33 @@ export const CycleForm: React.FC = (props) => { control={control} name="start_date" render={({ field: { value, onChange } }) => ( - onChange(val)} - minDate={new Date()} - maxDate={maxDate ?? undefined} - /> - )} - /> -
-
- ( - onChange(val)} minDate={minDate} /> +
+ onChange(date ? renderFormattedPayloadDate(date) : null)} + buttonVariant="border-with-text" + placeholder="Start date" + maxDate={maxDate ?? undefined} + /> +
)} />
+ ( +
+ onChange(date ? renderFormattedPayloadDate(date) : null)} + buttonVariant="border-with-text" + placeholder="End date" + minDate={minDate} + /> +
+ )} + />
diff --git a/web/components/cycles/gantt-chart/blocks.tsx b/web/components/cycles/gantt-chart/blocks.tsx index 8f05c45abad..46bc04039d7 100644 --- a/web/components/cycles/gantt-chart/blocks.tsx +++ b/web/components/cycles/gantt-chart/blocks.tsx @@ -4,7 +4,7 @@ import { Tooltip, ContrastIcon } from "@plane/ui"; // helpers import { renderFormattedDate } from "helpers/date-time.helper"; // types -import { ICycle } from "types"; +import { ICycle } from "@plane/types"; export const CycleGanttBlock = ({ data }: { data: ICycle }) => { const router = useRouter(); diff --git a/web/components/cycles/gantt-chart/cycles-list-layout.tsx b/web/components/cycles/gantt-chart/cycles-list-layout.tsx index 9671c22af58..26d04e103ae 100644 --- a/web/components/cycles/gantt-chart/cycles-list-layout.tsx +++ b/web/components/cycles/gantt-chart/cycles-list-layout.tsx @@ -1,38 +1,41 @@ import { FC } from "react"; - import { useRouter } from "next/router"; - +import { observer } from "mobx-react-lite"; import { KeyedMutator } from "swr"; - +// hooks +import { useCycle, useUser } from "hooks/store"; // services import { CycleService } from "services/cycle.service"; -// hooks -import useUser from "hooks/use-user"; -import useProjectDetails from "hooks/use-project-details"; // components import { GanttChartRoot, IBlockUpdateData, CycleGanttSidebar } from "components/gantt-chart"; import { CycleGanttBlock } from "components/cycles"; // types -import { ICycle } from "types"; +import { ICycle } from "@plane/types"; +// constants +import { EUserProjectRoles } from "constants/project"; type Props = { workspaceSlug: string; - cycles: ICycle[]; + cycleIds: string[]; mutateCycles?: KeyedMutator; }; // services const cycleService = new CycleService(); -export const CyclesListGanttChartView: FC = ({ cycles, mutateCycles }) => { +export const CyclesListGanttChartView: FC = observer((props) => { + const { cycleIds, mutateCycles } = props; + // router const router = useRouter(); const { workspaceSlug } = router.query; - - const { user } = useUser(); - const { projectDetails } = useProjectDetails(); + // store hooks + const { + membership: { currentProjectRole }, + } = useUser(); + const { getCycleById } = useCycle(); const handleCycleUpdate = (cycle: ICycle, payload: IBlockUpdateData) => { - if (!workspaceSlug || !user) return; + if (!workspaceSlug) return; mutateCycles && mutateCycles((prevData: any) => { if (!prevData) return prevData; @@ -63,27 +66,31 @@ export const CyclesListGanttChartView: FC = ({ cycles, mutateCycles }) => cycleService.patchCycle(workspaceSlug.toString(), cycle.project, cycle.id, newPayload); }; - const blockFormat = (blocks: ICycle[]) => - blocks && blocks.length > 0 - ? blocks - .filter((b) => b.start_date && b.end_date && new Date(b.start_date) <= new Date(b.end_date)) - .map((block) => ({ - data: block, - id: block.id, - sort_order: block.sort_order, - start_date: new Date(block.start_date ?? ""), - target_date: new Date(block.end_date ?? ""), - })) - : []; - - const isAllowed = projectDetails?.member_role === 20 || projectDetails?.member_role === 15; + const blockFormat = (blocks: (ICycle | null)[]) => { + if (!blocks) return []; + + const filteredBlocks = blocks.filter((b) => b !== null && b.start_date && b.end_date); + + const structuredBlocks = filteredBlocks.map((block) => ({ + data: block, + id: block?.id ?? "", + sort_order: block?.sort_order ?? 0, + start_date: new Date(block?.start_date ?? ""), + target_date: new Date(block?.end_date ?? ""), + })); + + return structuredBlocks; + }; + + const isAllowed = + currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole); return (
getCycleById(c))) : null} blockUpdateHandler={(block, payload) => handleCycleUpdate(block, payload)} sidebarToRender={(props) => } blockToRender={(data: ICycle) => } @@ -94,4 +101,4 @@ export const CyclesListGanttChartView: FC = ({ cycles, mutateCycles }) => />
); -}; +}); diff --git a/web/components/cycles/modal.tsx b/web/components/cycles/modal.tsx index 665f9865b12..bfd30cdf64e 100644 --- a/web/components/cycles/modal.tsx +++ b/web/components/cycles/modal.tsx @@ -3,12 +3,12 @@ import { Dialog, Transition } from "@headlessui/react"; // services import { CycleService } from "services/cycle.service"; // hooks +import { useApplication, useCycle } from "hooks/store"; import useToast from "hooks/use-toast"; -import { useMobxStore } from "lib/mobx/store-provider"; // components import { CycleForm } from "components/cycles"; // types -import type { CycleDateCheckData, ICycle } from "types"; +import type { CycleDateCheckData, ICycle } from "@plane/types"; type CycleModalProps = { isOpen: boolean; @@ -23,21 +23,21 @@ const cycleService = new CycleService(); export const CycleCreateUpdateModal: React.FC = (props) => { const { isOpen, handleClose, data, workspaceSlug, projectId } = props; - // store - const { - cycle: cycleStore, - trackEvent: { postHogEventTracker }, - } = useMobxStore(); // states const [activeProject, setActiveProject] = useState(projectId); - // toast + // store hooks + const { + eventTracker: { postHogEventTracker }, + } = useApplication(); + const { createCycle, updateCycleDetails } = useCycle(); + // toast alert const { setToastAlert } = useToast(); - const createCycle = async (payload: Partial) => { + const handleCreateCycle = async (payload: Partial) => { if (!workspaceSlug || !projectId) return; + const selectedProjectId = payload.project ?? projectId.toString(); - await cycleStore - .createCycle(workspaceSlug, selectedProjectId, payload) + await createCycle(workspaceSlug, selectedProjectId, payload) .then((res) => { setToastAlert({ type: "success", @@ -61,11 +61,11 @@ export const CycleCreateUpdateModal: React.FC = (props) => { }); }; - const updateCycle = async (cycleId: string, payload: Partial) => { + const handleUpdateCycle = async (cycleId: string, payload: Partial) => { if (!workspaceSlug || !projectId) return; + const selectedProjectId = payload.project ?? projectId.toString(); - await cycleStore - .patchCycle(workspaceSlug, selectedProjectId, cycleId, payload) + await updateCycleDetails(workspaceSlug, selectedProjectId, cycleId, payload) .then(() => { setToastAlert({ type: "success", @@ -116,8 +116,8 @@ export const CycleCreateUpdateModal: React.FC = (props) => { } if (isDateValid) { - if (data) await updateCycle(data.id, payload); - else await createCycle(payload); + if (data) await handleUpdateCycle(data.id, payload); + else await handleCreateCycle(payload); handleClose(); } else setToastAlert({ diff --git a/web/components/cycles/sidebar.tsx b/web/components/cycles/sidebar.tsx index e7316800831..f2f7792f6e9 100644 --- a/web/components/cycles/sidebar.tsx +++ b/web/components/cycles/sidebar.tsx @@ -1,13 +1,12 @@ -import React, { useCallback, useEffect, useState } from "react"; +import React, { useEffect, useState } from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import { useForm } from "react-hook-form"; import { Disclosure, Popover, Transition } from "@headlessui/react"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; // services import { CycleService } from "services/cycle.service"; // hooks +import { useApplication, useCycle, useUser } from "hooks/store"; import useToast from "hooks/use-toast"; // components import { SidebarProgressStats } from "components/core"; @@ -36,8 +35,7 @@ import { renderFormattedDate, } from "helpers/date-time.helper"; // types -import { ICycle, IIssueFilterOptions } from "types"; -import { EFilterType } from "store/issues/types"; +import { ICycle } from "@plane/types"; // constants import { EUserWorkspaceRoles } from "constants/workspace"; // fetch-keys @@ -54,20 +52,21 @@ const cycleService = new CycleService(); // TODO: refactor the whole component export const CycleDetailsSidebar: React.FC = observer((props) => { const { cycleId, handleClose } = props; - + // states const [cycleDeleteModal, setCycleDeleteModal] = useState(false); - + // router const router = useRouter(); const { workspaceSlug, projectId, peekCycle } = router.query; - + // store hooks const { - cycle: cycleDetailsStore, - cycleIssuesFilter: { issueFilters, updateFilters }, - trackEvent: { setTrackElement }, - user: { currentProjectRole }, - } = useMobxStore(); + eventTracker: { setTrackElement }, + } = useApplication(); + const { + membership: { currentProjectRole }, + } = useUser(); + const { getCycleById, updateCycleDetails } = useCycle(); - const cycleDetails = cycleDetailsStore.cycle_details[cycleId] ?? undefined; + const cycleDetails = getCycleById(cycleId); const { setToastAlert } = useToast(); @@ -83,7 +82,7 @@ export const CycleDetailsSidebar: React.FC = observer((props) => { const submitChanges = (data: Partial) => { if (!workspaceSlug || !projectId || !cycleId) return; - cycleDetailsStore.patchCycle(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), data); + updateCycleDetails(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), data); }; const handleCopyText = () => { @@ -254,24 +253,25 @@ export const CycleDetailsSidebar: React.FC = observer((props) => { } }; - const handleFiltersUpdate = useCallback( - (key: keyof IIssueFilterOptions, value: string | string[]) => { - if (!workspaceSlug || !projectId) return; - const newValues = issueFilters?.filters?.[key] ?? []; - - if (Array.isArray(value)) { - value.forEach((val) => { - if (!newValues.includes(val)) newValues.push(val); - }); - } else { - if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1); - else newValues.push(value); - } - - updateFilters(workspaceSlug.toString(), projectId.toString(), EFilterType.FILTERS, { [key]: newValues }, cycleId); - }, - [workspaceSlug, projectId, cycleId, issueFilters, updateFilters] - ); + // TODO: refactor this + // const handleFiltersUpdate = useCallback( + // (key: keyof IIssueFilterOptions, value: string | string[]) => { + // if (!workspaceSlug || !projectId) return; + // const newValues = issueFilters?.filters?.[key] ?? []; + + // if (Array.isArray(value)) { + // value.forEach((val) => { + // if (!newValues.includes(val)) newValues.push(val); + // }); + // } else { + // if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1); + // else newValues.push(value); + // } + + // updateFilters(workspaceSlug.toString(), projectId.toString(), EFilterType.FILTERS, { [key]: newValues }, cycleId); + // }, + // [workspaceSlug, projectId, cycleId, issueFilters, updateFilters] + // ); const cycleStatus = cycleDetails?.status.toLocaleLowerCase(); const isCompleted = cycleStatus === "completed"; @@ -587,9 +587,6 @@ export const CycleDetailsSidebar: React.FC = observer((props) => { }} totalIssues={cycleDetails.total_issues} isPeekView={Boolean(peekCycle)} - isCompleted={isCompleted} - filters={issueFilters?.filters} - handleFiltersUpdate={handleFiltersUpdate} />
)} diff --git a/web/components/cycles/transfer-issues-modal.tsx b/web/components/cycles/transfer-issues-modal.tsx index f47c1ddaaa5..5956e4a1e23 100644 --- a/web/components/cycles/transfer-issues-modal.tsx +++ b/web/components/cycles/transfer-issues-modal.tsx @@ -1,32 +1,31 @@ import React, { useState } from "react"; import { useRouter } from "next/router"; -import useSWR from "swr"; import { Dialog, Transition } from "@headlessui/react"; import { observer } from "mobx-react-lite"; -// services -import { CycleService } from "services/cycle.service"; // hooks import useToast from "hooks/use-toast"; -import { useMobxStore } from "lib/mobx/store-provider"; +import { useCycle, useIssues } from "hooks/store"; //icons import { ContrastIcon, TransferIcon } from "@plane/ui"; import { AlertCircle, Search, X } from "lucide-react"; -// fetch-key -import { INCOMPLETE_CYCLES_LIST } from "constants/fetch-keys"; -// types -import { ICycle } from "types"; +// constants +import { EIssuesStoreType } from "constants/issue"; type Props = { isOpen: boolean; handleClose: () => void; }; -const cycleService = new CycleService(); - -export const TransferIssuesModal: React.FC = observer(({ isOpen, handleClose }) => { +export const TransferIssuesModal: React.FC = observer((props) => { + const { isOpen, handleClose } = props; + // states const [query, setQuery] = useState(""); - const { cycleIssues: cycleIssueStore } = useMobxStore(); + // store hooks + const { currentProjectIncompleteCycleIds, getCycleById } = useCycle(); + const { + issues: { transferIssuesFromCycle }, + } = useIssues(EIssuesStoreType.CYCLE); const router = useRouter(); const { workspaceSlug, projectId, cycleId } = router.query; @@ -34,12 +33,14 @@ export const TransferIssuesModal: React.FC = observer(({ isOpen, handleCl const { setToastAlert } = useToast(); const transferIssue = async (payload: any) => { - await cycleIssueStore - .transferIssuesFromCycle(workspaceSlug as string, projectId as string, cycleId as string, payload) + if (!workspaceSlug || !projectId || !cycleId) return; + + // TODO: import transferIssuesFromCycle from store + await transferIssuesFromCycle(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), payload) .then(() => { setToastAlert({ type: "success", - title: "Issues transfered successfully", + title: "Issues transferred successfully", message: "Issues have been transferred successfully", }); }) @@ -52,17 +53,11 @@ export const TransferIssuesModal: React.FC = observer(({ isOpen, handleCl }); }; - const { data: incompleteCycles } = useSWR( - workspaceSlug && projectId ? INCOMPLETE_CYCLES_LIST(projectId as string) : null, - workspaceSlug && projectId - ? () => cycleService.getCyclesWithParams(workspaceSlug as string, projectId as string, "incomplete") - : null - ); + const filteredOptions = currentProjectIncompleteCycleIds?.filter((optionId) => { + const cycleDetails = getCycleById(optionId); - const filteredOptions = - query === "" - ? incompleteCycles - : incompleteCycles?.filter((option) => option.name.toLowerCase().includes(query.toLowerCase())); + return cycleDetails?.name.toLowerCase().includes(query.toLowerCase()); + }); // useEffect(() => { // const handleKeyDown = (e: KeyboardEvent) => { @@ -121,26 +116,32 @@ export const TransferIssuesModal: React.FC = observer(({ isOpen, handleCl
{filteredOptions ? ( filteredOptions.length > 0 ? ( - filteredOptions.map((option: ICycle) => ( - - )) + filteredOptions.map((optionId) => { + const cycleDetails = getCycleById(optionId); + + if (!cycleDetails) return; + + return ( + + ); + }) ) : (
diff --git a/web/components/dropdowns/cycle.tsx b/web/components/dropdowns/cycle.tsx new file mode 100644 index 00000000000..4d5c60acd23 --- /dev/null +++ b/web/components/dropdowns/cycle.tsx @@ -0,0 +1,293 @@ +import { Fragment, ReactNode, useEffect, useState } from "react"; +import { observer } from "mobx-react-lite"; +import { Combobox } from "@headlessui/react"; +import { usePopper } from "react-popper"; +import { Placement } from "@popperjs/core"; +import { Check, ChevronDown, Search } from "lucide-react"; +// hooks +import { useApplication, useCycle } from "hooks/store"; +// icons +import { ContrastIcon } from "@plane/ui"; +// helpers +import { cn } from "helpers/common.helper"; +// types +import { ICycle } from "@plane/types"; +import { TButtonVariants } from "./types"; + +type Props = { + button?: ReactNode; + buttonClassName?: string; + buttonContainerClassName?: string; + buttonVariant: TButtonVariants; + className?: string; + disabled?: boolean; + dropdownArrow?: boolean; + onChange: (val: string | null) => void; + placement?: Placement; + projectId: string; + value: string | null; +}; + +type ButtonProps = { + className?: string; + cycle: ICycle | null; + hideText?: boolean; + dropdownArrow: boolean; +}; + +type DropdownOptions = + | { + value: string | null; + query: string; + content: JSX.Element; + }[] + | undefined; + +const BorderButton = (props: ButtonProps) => { + const { className, cycle, dropdownArrow, hideText = false } = props; + + return ( +
+ + {!hideText && {cycle?.name ?? "Cycle"}} + {dropdownArrow &&
+ ); +}; + +const BackgroundButton = (props: ButtonProps) => { + const { className, cycle, dropdownArrow, hideText = false } = props; + + return ( +
+ + {!hideText && {cycle?.name ?? "Cycle"}} + {dropdownArrow &&
+ ); +}; + +const TransparentButton = (props: ButtonProps) => { + const { className, cycle, dropdownArrow, hideText = false } = props; + + return ( +
+ + {!hideText && {cycle?.name ?? "Cycle"}} + {dropdownArrow &&
+ ); +}; + +export const CycleDropdown: React.FC = observer((props) => { + const { + button, + buttonClassName, + buttonContainerClassName, + buttonVariant, + className = "", + disabled = false, + dropdownArrow = false, + onChange, + placement, + projectId, + value, + } = props; + // states + const [query, setQuery] = useState(""); + // popper-js refs + const [referenceElement, setReferenceElement] = useState(null); + const [popperElement, setPopperElement] = useState(null); + // popper-js init + const { styles, attributes } = usePopper(referenceElement, popperElement, { + placement: placement ?? "bottom-start", + modifiers: [ + { + name: "preventOverflow", + options: { + padding: 12, + }, + }, + ], + }); + // store hooks + const { + router: { workspaceSlug }, + } = useApplication(); + const { getProjectCycleIds, fetchAllCycles, getCycleById } = useCycle(); + const cycleIds = getProjectCycleIds(projectId); + + const options: DropdownOptions = cycleIds?.map((cycleId) => { + const cycleDetails = getCycleById(cycleId); + + return { + value: cycleId, + query: `${cycleDetails?.name}`, + content: ( +
+ + {cycleDetails?.name} +
+ ), + }; + }); + options?.unshift({ + value: null, + query: "No cycle", + content: ( +
+ + No cycle +
+ ), + }); + + const filteredOptions = + query === "" ? options : options?.filter((o) => o.query.toLowerCase().includes(query.toLowerCase())); + + // fetch cycles of the project if not already present in the store + useEffect(() => { + if (!workspaceSlug) return; + + if (!cycleIds) fetchAllCycles(workspaceSlug, projectId); + }, [cycleIds, fetchAllCycles, projectId, workspaceSlug]); + + const selectedCycle = value ? getCycleById(value) : null; + + return ( + + + {button ? ( + + ) : ( + + )} + + +
+
+ + setQuery(e.target.value)} + placeholder="Search" + displayValue={(assigned: any) => assigned?.name} + /> +
+
+ {filteredOptions ? ( + filteredOptions.length > 0 ? ( + filteredOptions.map((option) => ( + + `w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none ${ + active ? "bg-custom-background-80" : "" + } ${selected ? "text-custom-text-100" : "text-custom-text-200"}` + } + > + {({ selected }) => ( + <> + {option.content} + {selected && } + + )} + + )) + ) : ( +

No matches found

+ ) + ) : ( +

Loading...

+ )} +
+
+
+
+ ); +}); diff --git a/web/components/dropdowns/date.tsx b/web/components/dropdowns/date.tsx new file mode 100644 index 00000000000..e791413b4b3 --- /dev/null +++ b/web/components/dropdowns/date.tsx @@ -0,0 +1,243 @@ +import React, { useState } from "react"; +import { Popover } from "@headlessui/react"; +import DatePicker from "react-datepicker"; +import { usePopper } from "react-popper"; +import { CalendarDays, X } from "lucide-react"; +// import "react-datepicker/dist/react-datepicker.css"; +// helpers +import { renderFormattedDate } from "helpers/date-time.helper"; +import { cn } from "helpers/common.helper"; +// types +import { TButtonVariants } from "./types"; +import { Placement } from "@popperjs/core"; + +type Props = { + buttonClassName?: string; + buttonContainerClassName?: string; + buttonVariant: TButtonVariants; + disabled?: boolean; + icon?: React.ReactNode; + isClearable?: boolean; + minDate?: Date; + maxDate?: Date; + onChange: (val: Date | null) => void; + placeholder: string; + placement?: Placement; + value: Date | string | null; + closeOnSelect?: boolean; +}; + +type ButtonProps = { + className?: string; + date: string | Date | null; + icon: React.ReactNode; + isClearable: boolean; + hideText?: boolean; + onClear: () => void; + placeholder: string; +}; + +const BorderButton = (props: ButtonProps) => { + const { className, date, icon, isClearable, hideText = false, onClear, placeholder } = props; + + return ( +
+ {icon} + {!hideText && {date ? renderFormattedDate(date) : placeholder}} + {isClearable && ( + { + e.stopPropagation(); + onClear(); + }} + /> + )} +
+ ); +}; + +const BackgroundButton = (props: ButtonProps) => { + const { className, date, icon, isClearable, hideText = false, onClear, placeholder } = props; + + return ( +
+ {icon} + {!hideText && {date ? renderFormattedDate(date) : placeholder}} + {isClearable && ( + { + e.stopPropagation(); + onClear(); + }} + /> + )} +
+ ); +}; + +const TransparentButton = (props: ButtonProps) => { + const { className, date, icon, isClearable, hideText = false, onClear, placeholder } = props; + + return ( +
+ {icon} + {!hideText && {date ? renderFormattedDate(date) : placeholder}} + {isClearable && ( + { + e.stopPropagation(); + onClear(); + }} + /> + )} +
+ ); +}; + +export const DateDropdown: React.FC = (props) => { + const { + buttonClassName = "", + buttonContainerClassName, + buttonVariant, + disabled = false, + icon = , + isClearable = true, + minDate, + maxDate, + onChange, + placeholder, + placement, + value, + closeOnSelect = true, + } = props; + // popper-js refs + const [referenceElement, setReferenceElement] = useState(null); + const [popperElement, setPopperElement] = useState(null); + // popper-js init + const { styles, attributes } = usePopper(referenceElement, popperElement, { + placement: placement ?? "bottom-start", + modifiers: [ + { + name: "preventOverflow", + options: { + padding: 12, + }, + }, + ], + }); + + const isDateSelected = value !== null && value !== undefined && value.toString().trim() !== ""; + + return ( + + {({ close }) => ( + <> + + + + +
+ { + onChange(val); + if (closeOnSelect) close(); + }} + dateFormat="dd-MM-yyyy" + minDate={minDate} + maxDate={maxDate} + calendarClassName="shadow-custom-shadow-rg rounded" + inline + /> +
+
+ + )} +
+ ); +}; diff --git a/web/components/dropdowns/estimate.tsx b/web/components/dropdowns/estimate.tsx new file mode 100644 index 00000000000..8d538f53f9b --- /dev/null +++ b/web/components/dropdowns/estimate.tsx @@ -0,0 +1,287 @@ +import { Fragment, ReactNode, useEffect, useState } from "react"; +import { observer } from "mobx-react-lite"; +import { Combobox } from "@headlessui/react"; +import { usePopper } from "react-popper"; +import { Placement } from "@popperjs/core"; +import { Check, ChevronDown, Search, Triangle } from "lucide-react"; +import sortBy from "lodash/sortBy"; +// hooks +import { useApplication, useEstimate } from "hooks/store"; +// helpers +import { cn } from "helpers/common.helper"; +// types +import { TButtonVariants } from "./types"; + +type Props = { + button?: ReactNode; + buttonClassName?: string; + buttonContainerClassName?: string; + buttonVariant: TButtonVariants; + className?: string; + disabled?: boolean; + dropdownArrow?: boolean; + onChange: (val: number | null) => void; + placement?: Placement; + projectId: string; + value: number | null; +}; + +type ButtonProps = { + className?: string; + estimatePoint: string | null; + dropdownArrow: boolean; + hideText?: boolean; +}; + +type DropdownOptions = + | { + value: number | null; + query: string; + content: JSX.Element; + }[] + | undefined; + +const BorderButton = (props: ButtonProps) => { + const { className, estimatePoint, dropdownArrow, hideText = false } = props; + + return ( +
+ + {!hideText && {estimatePoint !== null ? estimatePoint : "Estimate"}} + {dropdownArrow &&
+ ); +}; + +const BackgroundButton = (props: ButtonProps) => { + const { className, estimatePoint, dropdownArrow, hideText = false } = props; + + return ( +
+ + {!hideText && {estimatePoint !== null ? estimatePoint : "Estimate"}} + {dropdownArrow &&
+ ); +}; + +const TransparentButton = (props: ButtonProps) => { + const { className, estimatePoint, dropdownArrow, hideText = false } = props; + + return ( +
+ + {!hideText && {estimatePoint !== null ? estimatePoint : "Estimate"}} + {dropdownArrow &&
+ ); +}; + +export const EstimateDropdown: React.FC = observer((props) => { + const { + button, + buttonClassName, + buttonContainerClassName, + buttonVariant, + className = "", + disabled = false, + dropdownArrow = false, + onChange, + placement, + projectId, + value, + } = props; + // states + const [query, setQuery] = useState(""); + // popper-js refs + const [referenceElement, setReferenceElement] = useState(null); + const [popperElement, setPopperElement] = useState(null); + // popper-js init + const { styles, attributes } = usePopper(referenceElement, popperElement, { + placement: placement ?? "bottom-start", + modifiers: [ + { + name: "preventOverflow", + options: { + padding: 12, + }, + }, + ], + }); + // store hooks + const { + router: { workspaceSlug }, + } = useApplication(); + const { fetchProjectEstimates, getProjectActiveEstimateDetails, getEstimatePointValue } = useEstimate(); + const activeEstimate = getProjectActiveEstimateDetails(projectId); + + const options: DropdownOptions = sortBy(activeEstimate?.points ?? [], "key")?.map((point) => ({ + value: point.key, + query: `${point?.value}`, + content: ( +
+ + {point.value} +
+ ), + })); + options?.unshift({ + value: null, + query: "No estimate", + content: ( +
+ + No estimate +
+ ), + }); + + const filteredOptions = + query === "" ? options : options?.filter((o) => o.query.toLowerCase().includes(query.toLowerCase())); + + // fetch cycles of the project if not already present in the store + useEffect(() => { + if (!workspaceSlug) return; + + if (!activeEstimate) fetchProjectEstimates(workspaceSlug, projectId); + }, [activeEstimate, fetchProjectEstimates, projectId, workspaceSlug]); + + const selectedEstimate = value !== null ? getEstimatePointValue(value) : null; + + return ( + + + {button ? ( + + ) : ( + + )} + + +
+
+ + setQuery(e.target.value)} + placeholder="Search" + displayValue={(assigned: any) => assigned?.name} + /> +
+
+ {filteredOptions ? ( + filteredOptions.length > 0 ? ( + filteredOptions.map((option) => ( + + `w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none ${ + active ? "bg-custom-background-80" : "" + } ${selected ? "text-custom-text-100" : "text-custom-text-200"}` + } + > + {({ selected }) => ( + <> + {option.content} + {selected && } + + )} + + )) + ) : ( +

No matching results

+ ) + ) : ( +

Loading...

+ )} +
+
+
+
+ ); +}); diff --git a/web/components/dropdowns/index.ts b/web/components/dropdowns/index.ts new file mode 100644 index 00000000000..036ed9f757f --- /dev/null +++ b/web/components/dropdowns/index.ts @@ -0,0 +1,8 @@ +export * from "./member"; +export * from "./cycle"; +export * from "./date"; +export * from "./estimate"; +export * from "./module"; +export * from "./priority"; +export * from "./project"; +export * from "./state"; diff --git a/web/components/dropdowns/member/buttons.tsx b/web/components/dropdowns/member/buttons.tsx new file mode 100644 index 00000000000..53003473d9d --- /dev/null +++ b/web/components/dropdowns/member/buttons.tsx @@ -0,0 +1,113 @@ +import { observer } from "mobx-react-lite"; +import { ChevronDown } from "lucide-react"; +// hooks +import { useMember } from "hooks/store"; +// ui +import { Avatar, AvatarGroup, UserGroupIcon } from "@plane/ui"; +// helpers +import { cn } from "helpers/common.helper"; + +type ButtonProps = { + className?: string; + dropdownArrow: boolean; + placeholder: string; + hideText?: boolean; + userIds: string | string[] | null; +}; + +const ButtonAvatars = observer(({ userIds }: { userIds: string | string[] | null }) => { + const { getUserDetails } = useMember(); + + if (Array.isArray(userIds)) { + if (userIds.length > 0) + return ( + + {userIds.map((userId) => { + const userDetails = getUserDetails(userId); + + if (!userDetails) return; + return ; + })} + + ); + } else { + if (userIds) { + const userDetails = getUserDetails(userIds); + return ; + } + } + + return ; +}); + +export const BorderButton = observer((props: ButtonProps) => { + const { className, dropdownArrow, hideText = false, placeholder, userIds } = props; + // store hooks + const { getUserDetails } = useMember(); + + const isMultiple = Array.isArray(userIds); + + return ( +
+ + {!hideText && ( + + {userIds ? (isMultiple ? placeholder : getUserDetails(userIds)?.display_name) : placeholder} + + )} + {dropdownArrow &&
+ ); +}); + +export const BackgroundButton = observer((props: ButtonProps) => { + const { className, dropdownArrow, hideText = false, placeholder, userIds } = props; + // store hooks + const { getUserDetails } = useMember(); + + const isMultiple = Array.isArray(userIds); + + return ( +
+ + {!hideText && ( + + {userIds ? (isMultiple ? placeholder : getUserDetails(userIds)?.display_name) : placeholder} + + )} + {dropdownArrow &&
+ ); +}); + +export const TransparentButton = observer((props: ButtonProps) => { + const { className, dropdownArrow, hideText = false, placeholder, userIds } = props; + // store hooks + const { getUserDetails } = useMember(); + + const isMultiple = Array.isArray(userIds); + + return ( +
+ + {!hideText && ( + + {userIds ? (isMultiple ? placeholder : getUserDetails(userIds)?.display_name) : placeholder} + + )} + {dropdownArrow &&
+ ); +}); diff --git a/web/components/dropdowns/member/index.ts b/web/components/dropdowns/member/index.ts new file mode 100644 index 00000000000..bc976b46a2e --- /dev/null +++ b/web/components/dropdowns/member/index.ts @@ -0,0 +1,3 @@ +export * from "./buttons"; +export * from "./project-member"; +export * from "./workspace-member"; diff --git a/web/components/dropdowns/member/project-member.tsx b/web/components/dropdowns/member/project-member.tsx new file mode 100644 index 00000000000..18d317a5698 --- /dev/null +++ b/web/components/dropdowns/member/project-member.tsx @@ -0,0 +1,224 @@ +import { Fragment, useEffect, useState } from "react"; +import { observer } from "mobx-react-lite"; +import { Combobox } from "@headlessui/react"; +import { usePopper } from "react-popper"; +import { Check, Search } from "lucide-react"; +// hooks +import { useApplication, useMember, useUser } from "hooks/store"; +// components +import { BackgroundButton, BorderButton, TransparentButton } from "components/dropdowns"; +// icons +import { Avatar } from "@plane/ui"; +// helpers +import { cn } from "helpers/common.helper"; +// types +import { MemberDropdownProps } from "./types"; + +type Props = { + projectId: string; +} & MemberDropdownProps; + +export const ProjectMemberDropdown: React.FC = observer((props) => { + const { + button, + buttonClassName, + buttonContainerClassName, + buttonVariant, + className = "", + disabled = false, + dropdownArrow = false, + multiple, + onChange, + placeholder = "Members", + placement, + projectId, + value, + } = props; + // states + const [query, setQuery] = useState(""); + // popper-js refs + const [referenceElement, setReferenceElement] = useState(null); + const [popperElement, setPopperElement] = useState(null); + // popper-js init + const { styles, attributes } = usePopper(referenceElement, popperElement, { + placement: placement ?? "bottom-start", + modifiers: [ + { + name: "preventOverflow", + options: { + padding: 12, + }, + }, + ], + }); + // store hooks + const { + router: { workspaceSlug }, + } = useApplication(); + const { currentUser } = useUser(); + const { + getUserDetails, + project: { getProjectMemberIds, fetchProjectMembers }, + } = useMember(); + const projectMemberIds = getProjectMemberIds(projectId); + + const options = projectMemberIds?.map((userId) => { + const userDetails = getUserDetails(userId); + + return { + value: userId, + query: `${userDetails?.display_name} ${userDetails?.first_name} ${userDetails?.last_name}`, + content: ( +
+ + {currentUser?.id === userId ? "You" : userDetails?.display_name} +
+ ), + }; + }); + + const filteredOptions = + query === "" ? options : options?.filter((o) => o.query.toLowerCase().includes(query.toLowerCase())); + + const comboboxProps: any = { + value, + onChange, + disabled, + }; + if (multiple) comboboxProps.multiple = true; + + useEffect(() => { + if (!workspaceSlug) return; + + if (!projectMemberIds) fetchProjectMembers(workspaceSlug, projectId); + }, [fetchProjectMembers, projectId, projectMemberIds, workspaceSlug]); + + return ( + + + {button ? ( + + ) : ( + + )} + + +
+
+ + setQuery(e.target.value)} + placeholder="Search" + displayValue={(assigned: any) => assigned?.name} + /> +
+
+ {filteredOptions ? ( + filteredOptions.length > 0 ? ( + filteredOptions.map((option) => ( + + `w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none ${ + active ? "bg-custom-background-80" : "" + } ${selected ? "text-custom-text-100" : "text-custom-text-200"}` + } + > + {({ selected }) => ( + <> + {option.content} + {selected && } + + )} + + )) + ) : ( +

No matching results

+ ) + ) : ( +

Loading...

+ )} +
+
+
+
+ ); +}); diff --git a/web/components/dropdowns/member/types.d.ts b/web/components/dropdowns/member/types.d.ts new file mode 100644 index 00000000000..4c0bff67ba5 --- /dev/null +++ b/web/components/dropdowns/member/types.d.ts @@ -0,0 +1,25 @@ +import { Placement } from "@popperjs/core"; +import { TButtonVariants } from "../types"; + +export type MemberDropdownProps = { + button?: ReactNode; + buttonClassName?: string; + buttonContainerClassName?: string; + buttonVariant: TButtonVariants; + className?: string; + disabled?: boolean; + dropdownArrow?: boolean; + placeholder?: string; + placement?: Placement; +} & ( + | { + multiple: false; + onChange: (val: string | null) => void; + value: string | null; + } + | { + multiple: true; + onChange: (val: string[]) => void; + value: string[]; + } +); diff --git a/web/components/dropdowns/member/workspace-member.tsx b/web/components/dropdowns/member/workspace-member.tsx new file mode 100644 index 00000000000..f50da72c8c7 --- /dev/null +++ b/web/components/dropdowns/member/workspace-member.tsx @@ -0,0 +1,209 @@ +import { Fragment, useState } from "react"; +import { observer } from "mobx-react-lite"; +import { Combobox } from "@headlessui/react"; +import { usePopper } from "react-popper"; +import { Check, Search } from "lucide-react"; +// hooks +import { useMember, useUser } from "hooks/store"; +// components +import { BackgroundButton, BorderButton, TransparentButton } from "components/dropdowns"; +// icons +import { Avatar } from "@plane/ui"; +// helpers +import { cn } from "helpers/common.helper"; +// types +import { MemberDropdownProps } from "./types"; + +export const WorkspaceMemberDropdown: React.FC = observer((props) => { + const { + button, + buttonClassName, + buttonContainerClassName, + buttonVariant, + className = "", + disabled = false, + dropdownArrow = false, + multiple, + onChange, + placeholder = "Members", + placement, + value, + } = props; + // states + const [query, setQuery] = useState(""); + // popper-js refs + const [referenceElement, setReferenceElement] = useState(null); + const [popperElement, setPopperElement] = useState(null); + // popper-js init + const { styles, attributes } = usePopper(referenceElement, popperElement, { + placement: placement ?? "bottom-start", + modifiers: [ + { + name: "preventOverflow", + options: { + padding: 12, + }, + }, + ], + }); + // store hooks + const { currentUser } = useUser(); + const { + getUserDetails, + workspace: { workspaceMemberIds }, + } = useMember(); + + const options = workspaceMemberIds?.map((userId) => { + const userDetails = getUserDetails(userId); + + return { + value: userId, + query: `${userDetails?.display_name} ${userDetails?.first_name} ${userDetails?.last_name}`, + content: ( +
+ + {currentUser?.id === userId ? "You" : userDetails?.display_name} +
+ ), + }; + }); + + const filteredOptions = + query === "" ? options : options?.filter((o) => o.query.toLowerCase().includes(query.toLowerCase())); + + const comboboxProps: any = { + value, + onChange, + disabled, + }; + if (multiple) comboboxProps.multiple = true; + + return ( + + + {button ? ( + + ) : ( + + )} + + +
+
+ + setQuery(e.target.value)} + placeholder="Search" + displayValue={(assigned: any) => assigned?.name} + /> +
+
+ {filteredOptions ? ( + filteredOptions.length > 0 ? ( + filteredOptions.map((option) => ( + + `w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none ${ + active ? "bg-custom-background-80" : "" + } ${selected ? "text-custom-text-100" : "text-custom-text-200"}` + } + > + {({ selected }) => ( + <> + {option.content} + {selected && } + + )} + + )) + ) : ( +

No matching results

+ ) + ) : ( +

Loading...

+ )} +
+
+
+
+ ); +}); diff --git a/web/components/dropdowns/module.tsx b/web/components/dropdowns/module.tsx new file mode 100644 index 00000000000..ff35c26b635 --- /dev/null +++ b/web/components/dropdowns/module.tsx @@ -0,0 +1,293 @@ +import { Fragment, ReactNode, useEffect, useState } from "react"; +import { observer } from "mobx-react-lite"; +import { Combobox } from "@headlessui/react"; +import { usePopper } from "react-popper"; +import { Placement } from "@popperjs/core"; +import { Check, ChevronDown, Search } from "lucide-react"; +// hooks +import { useApplication, useModule } from "hooks/store"; +// icons +import { DiceIcon } from "@plane/ui"; +// helpers +import { cn } from "helpers/common.helper"; +// types +import { IModule } from "@plane/types"; +import { TButtonVariants } from "./types"; + +type Props = { + button?: ReactNode; + buttonClassName?: string; + buttonContainerClassName?: string; + buttonVariant: TButtonVariants; + className?: string; + disabled?: boolean; + dropdownArrow?: boolean; + onChange: (val: string | null) => void; + placement?: Placement; + projectId: string; + value: string | null; +}; + +type DropdownOptions = + | { + value: string | null; + query: string; + content: JSX.Element; + }[] + | undefined; + +type ButtonProps = { + className?: string; + dropdownArrow: boolean; + hideText?: boolean; + module: IModule | null; +}; + +const BorderButton = (props: ButtonProps) => { + const { className, dropdownArrow, hideText = false, module } = props; + + return ( +
+ + {!hideText && {module?.name ?? "Module"}} + {dropdownArrow &&
+ ); +}; + +const BackgroundButton = (props: ButtonProps) => { + const { className, dropdownArrow, hideText = false, module } = props; + + return ( +
+ + {!hideText && {module?.name ?? "Module"}} + {dropdownArrow &&
+ ); +}; + +const TransparentButton = (props: ButtonProps) => { + const { className, dropdownArrow, hideText = false, module } = props; + + return ( +
+ + {!hideText && {module?.name ?? "Module"}} + {dropdownArrow &&
+ ); +}; + +export const ModuleDropdown: React.FC = observer((props) => { + const { + button, + buttonClassName, + buttonContainerClassName, + buttonVariant, + className = "", + disabled = false, + dropdownArrow = false, + onChange, + placement, + projectId, + value, + } = props; + // states + const [query, setQuery] = useState(""); + // popper-js refs + const [referenceElement, setReferenceElement] = useState(null); + const [popperElement, setPopperElement] = useState(null); + // popper-js init + const { styles, attributes } = usePopper(referenceElement, popperElement, { + placement: placement ?? "bottom-start", + modifiers: [ + { + name: "preventOverflow", + options: { + padding: 12, + }, + }, + ], + }); + // store hooks + const { + router: { workspaceSlug }, + } = useApplication(); + const { getProjectModuleIds, fetchModules, getModuleById } = useModule(); + const moduleIds = getProjectModuleIds(projectId); + + const options: DropdownOptions = moduleIds?.map((moduleId) => { + const moduleDetails = getModuleById(moduleId); + + return { + value: moduleId, + query: `${moduleDetails?.name}`, + content: ( +
+ + {moduleDetails?.name} +
+ ), + }; + }); + options?.unshift({ + value: null, + query: "No module", + content: ( +
+ + No module +
+ ), + }); + + const filteredOptions = + query === "" ? options : options?.filter((o) => o.query.toLowerCase().includes(query.toLowerCase())); + + // fetch modules of the project if not already present in the store + useEffect(() => { + if (!workspaceSlug) return; + + if (!moduleIds) fetchModules(workspaceSlug, projectId); + }, [moduleIds, fetchModules, projectId, workspaceSlug]); + + const selectedModule = value ? getModuleById(value) : null; + + return ( + + + {button ? ( + + ) : ( + + )} + + +
+
+ + setQuery(e.target.value)} + placeholder="Search" + displayValue={(assigned: any) => assigned?.name} + /> +
+
+ {filteredOptions ? ( + filteredOptions.length > 0 ? ( + filteredOptions.map((option) => ( + + `w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none ${ + active ? "bg-custom-background-80" : "" + } ${selected ? "text-custom-text-100" : "text-custom-text-200"}` + } + > + {({ selected }) => ( + <> + {option.content} + {selected && } + + )} + + )) + ) : ( +

No matching results

+ ) + ) : ( +

Loading...

+ )} +
+
+
+
+ ); +}); diff --git a/web/components/dropdowns/priority.tsx b/web/components/dropdowns/priority.tsx new file mode 100644 index 00000000000..5c467a7b6da --- /dev/null +++ b/web/components/dropdowns/priority.tsx @@ -0,0 +1,398 @@ +import { Fragment, ReactNode, useState } from "react"; +import { Combobox } from "@headlessui/react"; +import { usePopper } from "react-popper"; +import { Placement } from "@popperjs/core"; +import { Check, ChevronDown, Search } from "lucide-react"; +// icons +import { PriorityIcon } from "@plane/ui"; +// helpers +import { cn } from "helpers/common.helper"; +// types +import { TIssuePriorities } from "@plane/types"; +import { TButtonVariants } from "./types"; +// constants +import { ISSUE_PRIORITIES } from "constants/issue"; + +type Props = { + button?: ReactNode; + buttonClassName?: string; + buttonContainerClassName?: string; + buttonVariant: TButtonVariants; + className?: string; + disabled?: boolean; + dropdownArrow?: boolean; + highlightUrgent?: boolean; + onChange: (val: TIssuePriorities) => void; + placement?: Placement; + value: TIssuePriorities; +}; + +type ButtonProps = { + className?: string; + dropdownArrow: boolean; + hideText?: boolean; + highlightUrgent: boolean; + priority: TIssuePriorities; +}; + +const BorderButton = (props: ButtonProps) => { + const { className, dropdownArrow, hideText = false, highlightUrgent, priority } = props; + + const priorityDetails = ISSUE_PRIORITIES.find((p) => p.key === priority); + + const priorityClasses = { + urgent: "bg-red-500/20 text-red-950 border-red-500", + high: "bg-orange-500/20 text-orange-950 border-orange-500", + medium: "bg-yellow-500/20 text-yellow-950 border-yellow-500", + low: "bg-custom-primary-100/20 text-custom-primary-950 border-custom-primary-100", + none: "bg-custom-background-80 border-custom-border-300", + }; + + return ( + + ); +}; + +const BackgroundButton = (props: ButtonProps) => { + const { className, dropdownArrow, hideText = false, highlightUrgent, priority } = props; + + const priorityDetails = ISSUE_PRIORITIES.find((p) => p.key === priority); + + const priorityClasses = { + urgent: "bg-red-500/20 text-red-950", + high: "bg-orange-500/20 text-orange-950", + medium: "bg-yellow-500/20 text-yellow-950", + low: "bg-blue-500/20 text-blue-950", + none: "bg-custom-background-80", + }; + + return ( + + ); +}; + +const TransparentButton = (props: ButtonProps) => { + const { className, dropdownArrow, hideText = false, highlightUrgent, priority } = props; + + const priorityDetails = ISSUE_PRIORITIES.find((p) => p.key === priority); + + const priorityClasses = { + urgent: "text-red-950", + high: "text-orange-950", + medium: "text-yellow-950", + low: "text-blue-950", + none: "", + }; + + return ( + + ); +}; + +export const PriorityDropdown: React.FC = (props) => { + const { + button, + buttonClassName, + buttonContainerClassName, + buttonVariant, + className = "", + disabled = false, + dropdownArrow = false, + highlightUrgent = true, + onChange, + placement, + value, + } = props; + // states + const [query, setQuery] = useState(""); + // popper-js refs + const [referenceElement, setReferenceElement] = useState(null); + const [popperElement, setPopperElement] = useState(null); + // popper-js init + const { styles, attributes } = usePopper(referenceElement, popperElement, { + placement: placement ?? "bottom-start", + modifiers: [ + { + name: "preventOverflow", + options: { + padding: 12, + }, + }, + ], + }); + + const options = ISSUE_PRIORITIES.map((priority) => { + const priorityClasses = { + urgent: "bg-red-500/20 text-red-950 border-red-500", + high: "bg-orange-500/20 text-orange-950 border-orange-500", + medium: "bg-yellow-500/20 text-yellow-950 border-yellow-500", + low: "bg-custom-primary-100/20 text-custom-primary-950 border-custom-primary-100", + none: "bg-custom-background-80 border-custom-border-300", + }; + + return { + value: priority.key, + query: priority.key, + content: ( +
+
+
+ {priority.title} +
+ ), + }; + }); + + const filteredOptions = + query === "" ? options : options.filter((o) => o.query.toLowerCase().includes(query.toLowerCase())); + + return ( + + + {button ? ( + + ) : ( + + )} + + +
+
+ + setQuery(e.target.value)} + placeholder="Search" + displayValue={(assigned: any) => assigned?.name} + /> +
+
+ {filteredOptions.length > 0 ? ( + filteredOptions.map((option) => ( + + `w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none ${ + active ? "bg-custom-background-80" : "" + } ${selected ? "text-custom-text-100" : "text-custom-text-200"}` + } + > + {({ selected }) => ( + <> + {option.content} + {selected && } + + )} + + )) + ) : ( +

No matching results

+ )} +
+
+
+
+ ); +}; diff --git a/web/components/dropdowns/project.tsx b/web/components/dropdowns/project.tsx new file mode 100644 index 00000000000..65169dd88f7 --- /dev/null +++ b/web/components/dropdowns/project.tsx @@ -0,0 +1,273 @@ +import { Fragment, ReactNode, useState } from "react"; +import { observer } from "mobx-react-lite"; +import { Combobox } from "@headlessui/react"; +import { usePopper } from "react-popper"; +import { Placement } from "@popperjs/core"; +import { Check, ChevronDown, Search } from "lucide-react"; +// hooks +import { useProject } from "hooks/store"; +// helpers +import { cn } from "helpers/common.helper"; +import { renderEmoji } from "helpers/emoji.helper"; +// types +import { IProject } from "@plane/types"; +import { TButtonVariants } from "./types"; + +type Props = { + button?: ReactNode; + buttonClassName?: string; + buttonContainerClassName?: string; + buttonVariant: TButtonVariants; + className?: string; + disabled?: boolean; + dropdownArrow?: boolean; + onChange: (val: string) => void; + placement?: Placement; + value: string | null; +}; + +type ButtonProps = { + className?: string; + dropdownArrow: boolean; + hideText?: boolean; + project: IProject | null; +}; + +const BorderButton = (props: ButtonProps) => { + const { className, dropdownArrow, hideText = false, project } = props; + + return ( +
+ + {project?.emoji ? renderEmoji(project?.emoji) : project?.icon_prop ? renderEmoji(project?.icon_prop) : null} + + {!hideText && {project?.name ?? "Project"}} + {dropdownArrow &&
+ ); +}; + +const BackgroundButton = (props: ButtonProps) => { + const { className, dropdownArrow, hideText = false, project } = props; + + return ( +
+ + {project?.emoji ? renderEmoji(project?.emoji) : project?.icon_prop ? renderEmoji(project?.icon_prop) : null} + + {!hideText && {project?.name ?? "Project"}} + {dropdownArrow &&
+ ); +}; + +const TransparentButton = (props: ButtonProps) => { + const { className, dropdownArrow, hideText = false, project } = props; + + return ( +
+ + {project?.emoji ? renderEmoji(project?.emoji) : project?.icon_prop ? renderEmoji(project?.icon_prop) : null} + + {!hideText && {project?.name ?? "Project"}} + {dropdownArrow &&
+ ); +}; + +export const ProjectDropdown: React.FC = observer((props) => { + const { + button, + buttonClassName, + buttonContainerClassName, + buttonVariant, + className = "", + disabled = false, + dropdownArrow = false, + onChange, + placement, + value, + } = props; + // states + const [query, setQuery] = useState(""); + // popper-js refs + const [referenceElement, setReferenceElement] = useState(null); + const [popperElement, setPopperElement] = useState(null); + // popper-js init + const { styles, attributes } = usePopper(referenceElement, popperElement, { + placement: placement ?? "bottom-start", + modifiers: [ + { + name: "preventOverflow", + options: { + padding: 12, + }, + }, + ], + }); + // store hooks + const { joinedProjectIds, getProjectById } = useProject(); + + const options = joinedProjectIds?.map((projectId) => { + const projectDetails = getProjectById(projectId); + + return { + value: projectId, + query: `${projectDetails?.name}`, + content: ( +
+ + {projectDetails?.emoji + ? renderEmoji(projectDetails?.emoji) + : projectDetails?.icon_prop + ? renderEmoji(projectDetails?.icon_prop) + : null} + + {projectDetails?.name} +
+ ), + }; + }); + + const filteredOptions = + query === "" ? options : options?.filter((o) => o.query.toLowerCase().includes(query.toLowerCase())); + + const selectedProject = value ? getProjectById(value) : null; + + return ( + + + {button ? ( + + ) : ( + + )} + + +
+
+ + setQuery(e.target.value)} + placeholder="Search" + displayValue={(assigned: any) => assigned?.name} + /> +
+
+ {filteredOptions ? ( + filteredOptions.length > 0 ? ( + filteredOptions.map((option) => ( + + `w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none ${ + active ? "bg-custom-background-80" : "" + } ${selected ? "text-custom-text-100" : "text-custom-text-200"}` + } + > + {({ selected }) => ( + <> + {option.content} + {selected && } + + )} + + )) + ) : ( +

No matching results

+ ) + ) : ( +

Loading...

+ )} +
+
+
+
+ ); +}); diff --git a/web/components/dropdowns/state.tsx b/web/components/dropdowns/state.tsx new file mode 100644 index 00000000000..c7ba9eced01 --- /dev/null +++ b/web/components/dropdowns/state.tsx @@ -0,0 +1,271 @@ +import { Fragment, ReactNode, useEffect, useState } from "react"; +import { observer } from "mobx-react-lite"; +import { Combobox } from "@headlessui/react"; +import { usePopper } from "react-popper"; +import { Placement } from "@popperjs/core"; +import { Check, ChevronDown, Search } from "lucide-react"; +// hooks +import { useApplication, useProjectState } from "hooks/store"; +// icons +import { StateGroupIcon } from "@plane/ui"; +// helpers +import { cn } from "helpers/common.helper"; +// types +import { IState } from "@plane/types"; +import { TButtonVariants } from "./types"; + +type Props = { + button?: ReactNode; + buttonClassName?: string; + buttonContainerClassName?: string; + buttonVariant: TButtonVariants; + className?: string; + disabled?: boolean; + dropdownArrow?: boolean; + onChange: (val: string) => void; + placement?: Placement; + projectId: string; + value: string; +}; + +type ButtonProps = { + className?: string; + dropdownArrow: boolean; + hideText?: boolean; + state: IState | undefined; +}; + +const BorderButton = (props: ButtonProps) => { + const { className, dropdownArrow, hideText = false, state } = props; + + return ( +
+ + {!hideText && {state?.name ?? "State"}} + {dropdownArrow &&
+ ); +}; + +const BackgroundButton = (props: ButtonProps) => { + const { className, dropdownArrow, hideText = false, state } = props; + + return ( +
+ + {!hideText && {state?.name ?? "State"}} + {dropdownArrow &&
+ ); +}; + +const TransparentButton = (props: ButtonProps) => { + const { className, dropdownArrow, hideText = false, state } = props; + + return ( +
+ + {!hideText && {state?.name ?? "State"}} + {dropdownArrow &&
+ ); +}; + +export const StateDropdown: React.FC = observer((props) => { + const { + button, + buttonClassName, + buttonContainerClassName, + buttonVariant, + className = "", + disabled = false, + dropdownArrow = false, + onChange, + placement, + projectId, + value, + } = props; + // states + const [query, setQuery] = useState(""); + // popper-js refs + const [referenceElement, setReferenceElement] = useState(null); + const [popperElement, setPopperElement] = useState(null); + // popper-js init + const { styles, attributes } = usePopper(referenceElement, popperElement, { + placement: placement ?? "bottom-start", + modifiers: [ + { + name: "preventOverflow", + options: { + padding: 12, + }, + }, + ], + }); + // store hooks + const { + router: { workspaceSlug }, + } = useApplication(); + const { fetchProjectStates, getProjectStates, getStateById } = useProjectState(); + const statesList = getProjectStates(projectId); + + const options = statesList?.map((state) => ({ + value: state.id, + query: `${state?.name}`, + content: ( +
+ + {state?.name} +
+ ), + })); + + const filteredOptions = + query === "" ? options : options?.filter((o) => o.query.toLowerCase().includes(query.toLowerCase())); + + // fetch states of the project if not already present in the store + useEffect(() => { + if (!workspaceSlug) return; + + if (!statesList) fetchProjectStates(workspaceSlug, projectId); + }, [fetchProjectStates, projectId, statesList, workspaceSlug]); + + const selectedState = getStateById(value); + + return ( + + + {button ? ( + + ) : ( + + )} + + +
+
+ + setQuery(e.target.value)} + placeholder="Search" + displayValue={(assigned: any) => assigned?.name} + /> +
+
+ {filteredOptions ? ( + filteredOptions.length > 0 ? ( + filteredOptions.map((option) => ( + + `w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none ${ + active ? "bg-custom-background-80" : "" + } ${selected ? "text-custom-text-100" : "text-custom-text-200"}` + } + > + {({ selected }) => ( + <> + {option.content} + {selected && } + + )} + + )) + ) : ( +

No matches found

+ ) + ) : ( +

Loading...

+ )} +
+
+
+
+ ); +}); diff --git a/web/components/dropdowns/types.d.ts b/web/components/dropdowns/types.d.ts new file mode 100644 index 00000000000..f23914daa10 --- /dev/null +++ b/web/components/dropdowns/types.d.ts @@ -0,0 +1,7 @@ +export type TButtonVariants = + | "border-with-text" + | "border-without-text" + | "background-with-text" + | "background-without-text" + | "transparent-with-text" + | "transparent-without-text"; diff --git a/web/components/estimates/create-update-estimate-modal.tsx b/web/components/estimates/create-update-estimate-modal.tsx index b24172688c7..0a607e88db1 100644 --- a/web/components/estimates/create-update-estimate-modal.tsx +++ b/web/components/estimates/create-update-estimate-modal.tsx @@ -2,18 +2,16 @@ import React, { useEffect } from "react"; import { useRouter } from "next/router"; import { Controller, useForm } from "react-hook-form"; import { Dialog, Transition } from "@headlessui/react"; - -// store import { observer } from "mobx-react-lite"; -import { useMobxStore } from "lib/mobx/store-provider"; -// hooks +// store hooks +import { useEstimate } from "hooks/store"; import useToast from "hooks/use-toast"; // ui import { Button, Input, TextArea } from "@plane/ui"; // helpers import { checkDuplicates } from "helpers/array.helper"; // types -import { IEstimate, IEstimateFormData } from "types"; +import { IEstimate, IEstimateFormData } from "@plane/types"; type Props = { isOpen: boolean; @@ -36,16 +34,14 @@ type FormValues = typeof defaultValues; export const CreateUpdateEstimateModal: React.FC = observer((props) => { const { handleClose, data, isOpen } = props; - // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; - - // store - const { - projectEstimates: { createEstimate, updateEstimate }, - } = useMobxStore(); - + // store hooks + const { createEstimate, updateEstimate } = useEstimate(); + // form info + // toast alert + const { setToastAlert } = useToast(); const { formState: { errors, isSubmitting }, handleSubmit, @@ -60,8 +56,6 @@ export const CreateUpdateEstimateModal: React.FC = observer((props) => { reset(); }; - const { setToastAlert } = useToast(); - const handleCreateEstimate = async (payload: IEstimateFormData) => { if (!workspaceSlug || !projectId) return; diff --git a/web/components/estimates/delete-estimate-modal.tsx b/web/components/estimates/delete-estimate-modal.tsx index c9d34fe8ed2..8055ddb90d0 100644 --- a/web/components/estimates/delete-estimate-modal.tsx +++ b/web/components/estimates/delete-estimate-modal.tsx @@ -1,15 +1,13 @@ import React, { useEffect, useState } from "react"; import { useRouter } from "next/router"; import { Dialog, Transition } from "@headlessui/react"; -// store import { observer } from "mobx-react-lite"; -import { useMobxStore } from "lib/mobx/store-provider"; -// hooks +import { AlertTriangle } from "lucide-react"; +// store hooks +import { useEstimate } from "hooks/store"; import useToast from "hooks/use-toast"; // types -import { IEstimate } from "types"; -// icons -import { AlertTriangle } from "lucide-react"; +import { IEstimate } from "@plane/types"; // ui import { Button } from "@plane/ui"; @@ -21,18 +19,14 @@ type Props = { export const DeleteEstimateModal: React.FC = observer((props) => { const { isOpen, handleClose, data } = props; - + // states + const [isDeleteLoading, setIsDeleteLoading] = useState(false); // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; - - // store - const { projectEstimates: projectEstimatesStore } = useMobxStore(); - - // states - const [isDeleteLoading, setIsDeleteLoading] = useState(false); - - // hooks + // store hooks + const { deleteEstimate } = useEstimate(); + // toast alert const { setToastAlert } = useToast(); const handleEstimateDelete = () => { @@ -40,8 +34,7 @@ export const DeleteEstimateModal: React.FC = observer((props) => { const estimateId = data?.id!; - projectEstimatesStore - .deleteEstimate(workspaceSlug.toString(), projectId.toString(), estimateId) + deleteEstimate(workspaceSlug.toString(), projectId.toString(), estimateId) .then(() => { setIsDeleteLoading(false); handleClose(); diff --git a/web/components/estimates/estimate-list-item.tsx b/web/components/estimates/estimate-list-item.tsx index 65764e5d27a..b6effa71183 100644 --- a/web/components/estimates/estimate-list-item.tsx +++ b/web/components/estimates/estimate-list-item.tsx @@ -1,11 +1,8 @@ import React from "react"; - import { useRouter } from "next/router"; - -// store import { observer } from "mobx-react-lite"; -import { useMobxStore } from "lib/mobx/store-provider"; // hooks +import { useProject } from "hooks/store"; import useToast from "hooks/use-toast"; // ui import { Button, CustomMenu } from "@plane/ui"; @@ -14,7 +11,7 @@ import { Pencil, Trash2 } from "lucide-react"; // helpers import { orderArrayBy } from "helpers/array.helper"; // types -import { IEstimate } from "types"; +import { IEstimate } from "@plane/types"; type Props = { estimate: IEstimate; @@ -27,10 +24,8 @@ export const EstimateListItem: React.FC = observer((props) => { // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; - // store - const { - project: { currentProjectDetails, updateProject }, - } = useMobxStore(); + // store hooks + const { currentProjectDetails, updateProject } = useProject(); // hooks const { setToastAlert } = useToast(); diff --git a/web/components/estimates/estimate-select.tsx b/web/components/estimates/estimate-select.tsx deleted file mode 100644 index e7d656eca5b..00000000000 --- a/web/components/estimates/estimate-select.tsx +++ /dev/null @@ -1,160 +0,0 @@ -import React, { useState } from "react"; -import { usePopper } from "react-popper"; -import { Combobox } from "@headlessui/react"; -import { Check, ChevronDown, Search, Triangle } from "lucide-react"; -// types -import { Tooltip } from "@plane/ui"; -import { Placement } from "@popperjs/core"; -// constants -import { IEstimatePoint } from "types"; - -type Props = { - value: number | null; - onChange: (value: number | null) => void; - estimatePoints: IEstimatePoint[] | undefined; - className?: string; - buttonClassName?: string; - optionsClassName?: string; - placement?: Placement; - hideDropdownArrow?: boolean; - disabled?: boolean; -}; - -export const EstimateSelect: React.FC = (props) => { - const { - value, - onChange, - estimatePoints, - className = "", - buttonClassName = "", - optionsClassName = "", - placement, - hideDropdownArrow = false, - disabled = false, - } = props; - - const [query, setQuery] = useState(""); - - const [referenceElement, setReferenceElement] = useState(null); - const [popperElement, setPopperElement] = useState(null); - - const { styles, attributes } = usePopper(referenceElement, popperElement, { - placement: placement ?? "bottom-start", - modifiers: [ - { - name: "preventOverflow", - options: { - padding: 12, - }, - }, - ], - }); - - const options: { value: number | null; query: string; content: any }[] | undefined = estimatePoints?.map( - (estimate) => ({ - value: estimate.key, - query: estimate.value, - content: ( -
- - {estimate.value} -
- ), - }) - ); - options?.unshift({ - value: null, - query: "none", - content: ( -
- - None -
- ), - }); - - const filteredOptions = - query === "" ? options : options?.filter((option) => option.query.toLowerCase().includes(query.toLowerCase())); - - const selectedEstimate = estimatePoints?.find((e) => e.key === value); - const label = ( - -
- - {selectedEstimate?.value ?? "None"} -
-
- ); - - return ( - onChange(val as number | null)} - disabled={disabled} - > - - - - -
-
- - setQuery(e.target.value)} - placeholder="Search" - displayValue={(assigned: any) => assigned?.name} - /> -
-
- {filteredOptions ? ( - filteredOptions.length > 0 ? ( - filteredOptions.map((option) => ( - - `flex cursor-pointer select-none items-center justify-between gap-2 truncate rounded px-1 py-1.5 ${ - active ? "bg-custom-background-80" : "" - } ${selected ? "text-custom-text-100" : "text-custom-text-200"}` - } - > - {({ selected }) => ( - <> - {option.content} - {selected && } - - )} - - )) - ) : ( - -

No matching results

-
- ) - ) : ( -

Loading...

- )} -
-
-
-
- ); -}; diff --git a/web/components/estimates/estimates-list.tsx b/web/components/estimates/estimates-list.tsx index 29debfedb90..323cbe888d9 100644 --- a/web/components/estimates/estimates-list.tsx +++ b/web/components/estimates/estimates-list.tsx @@ -1,40 +1,33 @@ import React, { useState } from "react"; import { useRouter } from "next/router"; -// store import { observer } from "mobx-react-lite"; -import { useMobxStore } from "lib/mobx/store-provider"; +import { Plus } from "lucide-react"; +// store hooks +import { useEstimate, useProject } from "hooks/store"; +import useToast from "hooks/use-toast"; // components import { CreateUpdateEstimateModal, DeleteEstimateModal, EstimateListItem } from "components/estimates"; -//hooks -import useToast from "hooks/use-toast"; // ui import { Button, Loader } from "@plane/ui"; import { EmptyState } from "components/common"; -// icons -import { Plus } from "lucide-react"; // images import emptyEstimate from "public/empty-state/estimate.svg"; // types -import { IEstimate } from "types"; +import { IEstimate } from "@plane/types"; export const EstimatesList: React.FC = observer(() => { - // router - const router = useRouter(); - const { workspaceSlug, projectId } = router.query; - - // store - const { - project: { currentProjectDetails, updateProject }, - projectEstimates: { projectEstimates, getProjectEstimateById }, - } = useMobxStore(); // states const [estimateFormOpen, setEstimateFormOpen] = useState(false); const [estimateToDelete, setEstimateToDelete] = useState(null); const [estimateToUpdate, setEstimateToUpdate] = useState(); - // hooks + // router + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + // store hooks + const { updateProject, currentProjectDetails } = useProject(); + const { projectEstimates, getProjectEstimateById } = useEstimate(); + // toast alert const { setToastAlert } = useToast(); - // derived values - const estimatesList = projectEstimates; const editEstimate = (estimate: IEstimate) => { setEstimateFormOpen(true); @@ -96,10 +89,10 @@ export const EstimatesList: React.FC = observer(() => {
- {estimatesList ? ( - estimatesList.length > 0 ? ( + {projectEstimates ? ( + projectEstimates.length > 0 ? (
- {estimatesList.map((estimate) => ( + {projectEstimates.map((estimate) => ( void; data: IImporterService | null; - user: IUser | undefined; + user: IUser | null; provider: string | string[]; mutateServices: () => void; }; @@ -26,28 +26,30 @@ const projectExportService = new ProjectExportService(); export const Exporter: React.FC = observer((props) => { const { isOpen, handleClose, user, provider, mutateServices } = props; - + // states const [exportLoading, setExportLoading] = useState(false); - + // router const router = useRouter(); const { workspaceSlug } = router.query; - - const { project: projectStore } = useMobxStore(); - - const projects = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : undefined; - + // store hooks + const { workspaceProjectIds, getProjectById } = useProject(); + // toast alert const { setToastAlert } = useToast(); - const options = projects?.map((project) => ({ - value: project.id, - query: project.name + project.identifier, - content: ( -
- {project.identifier} - {project.name} -
- ), - })); + const options = workspaceProjectIds?.map((projectId) => { + const projectDetails = getProjectById(projectId); + + return { + value: projectDetails?.id, + query: `${projectDetails?.name} ${projectDetails?.identifier}`, + content: ( +
+ {projectDetails?.identifier} + {projectDetails?.name} +
+ ), + }; + }); const [value, setValue] = React.useState([]); const [multiple, setMultiple] = React.useState(false); @@ -131,10 +133,12 @@ export const Exporter: React.FC = observer((props) => { input label={ value && value.length > 0 - ? projects && - projects - .filter((p) => value.includes(p.id)) - .map((p) => p.identifier) + ? value + .map((projectId) => { + const projectDetails = getProjectById(projectId); + + return projectDetails?.identifier; + }) .join(", ") : "All projects" } diff --git a/web/components/exporter/guide.tsx b/web/components/exporter/guide.tsx index fbbf92c2638..87bf0604a9f 100644 --- a/web/components/exporter/guide.tsx +++ b/web/components/exporter/guide.tsx @@ -20,19 +20,24 @@ import { MoveLeft, MoveRight, RefreshCw } from "lucide-react"; import { EXPORT_SERVICES_LIST } from "constants/fetch-keys"; // constants import { EXPORTERS_LIST } from "constants/workspace"; +import { observer } from "mobx-react-lite"; +import { useUser } from "hooks/store"; // services const integrationService = new IntegrationService(); -const IntegrationGuide = () => { +const IntegrationGuide = observer(() => { + // states const [refreshing, setRefreshing] = useState(false); const per_page = 10; const [cursor, setCursor] = useState(`10:0:0`); - + // router const router = useRouter(); const { workspaceSlug, provider } = router.query; - - const { user } = useUserAuth(); + // store hooks + const { currentUser, currentUserLoader } = useUser(); + // custom hooks + const {} = useUserAuth({ user: currentUser, isLoading: currentUserLoader }); const { data: exporterServices } = useSWR( workspaceSlug && cursor ? EXPORT_SERVICES_LIST(workspaceSlug as string, cursor, `${per_page}`) : null, @@ -153,7 +158,7 @@ const IntegrationGuide = () => { isOpen handleClose={() => handleCsvClose()} data={null} - user={user} + user={currentUser} provider={provider} mutateServices={() => mutate(EXPORT_SERVICES_LIST(workspaceSlug as string, `${cursor}`, `${per_page}`))} /> @@ -161,6 +166,6 @@ const IntegrationGuide = () => {
); -}; +}); export default IntegrationGuide; diff --git a/web/components/exporter/single-export.tsx b/web/components/exporter/single-export.tsx index d2502cefb9e..34e41fc3585 100644 --- a/web/components/exporter/single-export.tsx +++ b/web/components/exporter/single-export.tsx @@ -4,7 +4,7 @@ import { Button } from "@plane/ui"; // helpers import { renderFormattedDate } from "helpers/date-time.helper"; // types -import { IExportData } from "types"; +import { IExportData } from "@plane/types"; type Props = { service: IExportData; @@ -38,12 +38,12 @@ export const SingleExport: FC = ({ service, refreshing }) => { service.status === "completed" ? "bg-green-500/20 text-green-500" : service.status === "processing" - ? "bg-yellow-500/20 text-yellow-500" - : service.status === "failed" - ? "bg-red-500/20 text-red-500" - : service.status === "expired" - ? "bg-orange-500/20 text-orange-500" - : "" + ? "bg-yellow-500/20 text-yellow-500" + : service.status === "failed" + ? "bg-red-500/20 text-red-500" + : service.status === "expired" + ? "bg-orange-500/20 text-orange-500" + : "" }`} > {refreshing ? "Refreshing..." : service.status} diff --git a/web/components/gantt-chart/helpers/block-structure.tsx b/web/components/gantt-chart/helpers/block-structure.tsx index ea51c3b1282..bc59624a5dc 100644 --- a/web/components/gantt-chart/helpers/block-structure.tsx +++ b/web/components/gantt-chart/helpers/block-structure.tsx @@ -1,8 +1,8 @@ // types -import { IIssue } from "types"; +import { TIssue } from "@plane/types"; import { IGanttBlock } from "components/gantt-chart"; -export const renderIssueBlocksStructure = (blocks: IIssue[]): IGanttBlock[] => +export const renderIssueBlocksStructure = (blocks: TIssue[]): IGanttBlock[] => blocks && blocks.length > 0 ? blocks .filter((b) => new Date(b?.start_date ?? "") <= new Date(b?.target_date ?? "")) diff --git a/web/components/gantt-chart/sidebar/sidebar.tsx b/web/components/gantt-chart/sidebar/sidebar.tsx index 0d4309cf8d1..c366bcfedb1 100644 --- a/web/components/gantt-chart/sidebar/sidebar.tsx +++ b/web/components/gantt-chart/sidebar/sidebar.tsx @@ -12,7 +12,7 @@ import { GanttInlineCreateIssueForm, IssueGanttSidebarBlock } from "components/i import { findTotalDaysInRange } from "helpers/date-time.helper"; // types import { IGanttBlock, IBlockUpdateData } from "components/gantt-chart/types"; -import { IIssue } from "types"; +import { TIssue } from "@plane/types"; type Props = { blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void; @@ -22,9 +22,9 @@ type Props = { quickAddCallback?: ( workspaceSlug: string, projectId: string, - data: IIssue, + data: TIssue, viewId?: string - ) => Promise; + ) => Promise; viewId?: string; disableIssueCreation?: boolean; }; diff --git a/web/components/headers/cycle-issues.tsx b/web/components/headers/cycle-issues.tsx index 2526199b594..7873ea69106 100644 --- a/web/components/headers/cycle-issues.tsx +++ b/web/components/headers/cycle-issues.tsx @@ -1,9 +1,17 @@ import { useCallback, useState } from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; // hooks +import { + useApplication, + useCycle, + useLabel, + useMember, + useProject, + useProjectState, + useUser, + useIssues, +} from "hooks/store"; import useLocalStorage from "hooks/use-local-storage"; // components import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues"; @@ -16,37 +24,67 @@ import { ArrowRight, Plus } from "lucide-react"; import { truncateText } from "helpers/string.helper"; import { renderEmoji } from "helpers/emoji.helper"; // types -import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "types"; +import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types"; // constants -import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; -import { EFilterType } from "store/issues/types"; -import { EProjectStore } from "store/command-palette.store"; -import { EUserWorkspaceRoles } from "constants/workspace"; +import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; +import { EUserProjectRoles } from "constants/project"; + +const CycleDropdownOption: React.FC<{ cycleId: string }> = ({ cycleId }) => { + // router + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + // store hooks + const { getCycleById } = useCycle(); + // derived values + const cycle = getCycleById(cycleId); + + if (!cycle) return null; + + return ( + router.push(`/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`)} + > +
+ + {truncateText(cycle.name, 40)} +
+
+ ); +}; export const CycleIssuesHeader: React.FC = observer(() => { + // states const [analyticsModal, setAnalyticsModal] = useState(false); - + // router const router = useRouter(); const { workspaceSlug, projectId, cycleId } = router.query as { workspaceSlug: string; projectId: string; cycleId: string; }; - + // store hooks + const { + issuesFilter: { issueFilters, updateFilters }, + } = useIssues(EIssuesStoreType.CYCLE); + const { currentProjectCycleIds, getCycleById } = useCycle(); + const { + commandPalette: { toggleCreateIssueModal }, + eventTracker: { setTrackElement }, + } = useApplication(); + const { + membership: { currentProjectRole }, + } = useUser(); + const { currentProjectDetails } = useProject(); + const { projectStates } = useProjectState(); const { - cycle: cycleStore, - projectIssuesFilter: projectIssueFiltersStore, - project: { currentProjectDetails }, - projectMember: { projectMembers }, - projectLabel: { projectLabels }, - projectState: projectStateStore, - commandPalette: commandPaletteStore, - trackEvent: { setTrackElement }, - cycleIssuesFilter: { issueFilters, updateFilters }, - user: { currentProjectRole }, - } = useMobxStore(); - - const activeLayout = projectIssueFiltersStore.issueFilters?.displayFilters?.layout; + project: { projectLabels }, + } = useLabel(); + const { + project: { projectMemberIds }, + } = useMember(); + + const activeLayout = issueFilters?.displayFilters?.layout; const { setValue, storedValue } = useLocalStorage("cycle_sidebar_collapsed", "false"); @@ -58,7 +96,7 @@ export const CycleIssuesHeader: React.FC = observer(() => { const handleLayoutChange = useCallback( (layout: TIssueLayouts) => { if (!workspaceSlug || !projectId) return; - updateFilters(workspaceSlug, projectId, EFilterType.DISPLAY_FILTERS, { layout: layout }, cycleId); + updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout }, cycleId); }, [workspaceSlug, projectId, cycleId, updateFilters] ); @@ -77,7 +115,7 @@ export const CycleIssuesHeader: React.FC = observer(() => { else newValues.push(value); } - updateFilters(workspaceSlug, projectId, EFilterType.FILTERS, { [key]: newValues }, cycleId); + updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { [key]: newValues }, cycleId); }, [workspaceSlug, projectId, cycleId, issueFilters, updateFilters] ); @@ -85,7 +123,7 @@ export const CycleIssuesHeader: React.FC = observer(() => { const handleDisplayFilters = useCallback( (updatedDisplayFilter: Partial) => { if (!workspaceSlug || !projectId) return; - updateFilters(workspaceSlug, projectId, EFilterType.DISPLAY_FILTERS, updatedDisplayFilter, cycleId); + updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, updatedDisplayFilter, cycleId); }, [workspaceSlug, projectId, cycleId, updateFilters] ); @@ -93,16 +131,15 @@ export const CycleIssuesHeader: React.FC = observer(() => { const handleDisplayProperties = useCallback( (property: Partial) => { if (!workspaceSlug || !projectId) return; - updateFilters(workspaceSlug, projectId, EFilterType.DISPLAY_PROPERTIES, property, cycleId); + updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_PROPERTIES, property, cycleId); }, [workspaceSlug, projectId, cycleId, updateFilters] ); - const cyclesList = cycleStore.projectCycles; - const cycleDetails = cycleId ? cycleStore.getCycleById(cycleId.toString()) : undefined; - + // derived values + const cycleDetails = cycleId ? getCycleById(cycleId.toString()) : undefined; const canUserCreateIssue = - currentProjectRole && [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentProjectRole); + currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole); return ( <> @@ -150,16 +187,8 @@ export const CycleIssuesHeader: React.FC = observer(() => { width="auto" placement="bottom-start" > - {cyclesList?.map((cycle) => ( - router.push(`/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`)} - > -
- - {truncateText(cycle.name, 40)} -
-
+ {currentProjectCycleIds?.map((cycleId) => ( + ))} } @@ -179,9 +208,9 @@ export const CycleIssuesHeader: React.FC = observer(() => { layoutDisplayFiltersOptions={ activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined } - labels={projectLabels ?? undefined} - members={projectMembers?.map((m) => m.member)} - states={projectStateStore.states?.[projectId ?? ""] ?? undefined} + labels={projectLabels} + memberIds={projectMemberIds ?? undefined} + states={projectStates} /> @@ -204,7 +233,7 @@ export const CycleIssuesHeader: React.FC = observer(() => { diff --git a/web/components/headers/projects.tsx b/web/components/headers/projects.tsx index 370dfe6d439..825af560d79 100644 --- a/web/components/headers/projects.tsx +++ b/web/components/headers/projects.tsx @@ -1,26 +1,22 @@ -import { useRouter } from "next/router"; +import { observer } from "mobx-react-lite"; import { Search, Plus, Briefcase } from "lucide-react"; +// hooks +import { useApplication, useProject, useUser } from "hooks/store"; // ui import { Breadcrumbs, Button } from "@plane/ui"; -// hooks -import { useMobxStore } from "lib/mobx/store-provider"; -import { observer } from "mobx-react-lite"; // constants import { EUserWorkspaceRoles } from "constants/workspace"; export const ProjectsHeader = observer(() => { - const router = useRouter(); - const { workspaceSlug } = router.query; - - // store + // store hooks const { - project: projectStore, commandPalette: commandPaletteStore, - trackEvent: { setTrackElement }, - user: { currentWorkspaceRole }, - } = useMobxStore(); - - const projectsList = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : []; + eventTracker: { setTrackElement }, + } = useApplication(); + const { + membership: { currentWorkspaceRole }, + } = useUser(); + const { workspaceProjectIds, searchQuery, setSearchQuery } = useProject(); const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER; @@ -38,13 +34,13 @@ export const ProjectsHeader = observer(() => {
- {projectsList?.length > 0 && ( + {workspaceProjectIds && workspaceProjectIds?.length > 0 && (
projectStore.setSearchQuery(e.target.value)} + value={searchQuery} + onChange={(e) => setSearchQuery(e.target.value)} placeholder="Search" />
diff --git a/web/components/icons/module/module-status-icon.tsx b/web/components/icons/module/module-status-icon.tsx index 303d0f7658c..a8e87e55c9c 100644 --- a/web/components/icons/module/module-status-icon.tsx +++ b/web/components/icons/module/module-status-icon.tsx @@ -8,7 +8,7 @@ import { ModulePlannedIcon, } from "components/icons"; // types -import { TModuleStatus } from "types"; +import { TModuleStatus } from "@plane/types"; type Props = { status: TModuleStatus; diff --git a/web/components/icons/priority-icon.tsx b/web/components/icons/priority-icon.tsx index 44248a438fc..b23f56eabac 100644 --- a/web/components/icons/priority-icon.tsx +++ b/web/components/icons/priority-icon.tsx @@ -1,5 +1,5 @@ // types -import { TIssuePriorities } from "types"; +import { TIssuePriorities } from "@plane/types"; type Props = { priority: TIssuePriorities | null; @@ -14,12 +14,12 @@ export const PriorityIcon: React.FC = ({ priority, className = "" }) => { {priority === "urgent" ? "error" : priority === "high" - ? "signal_cellular_alt" - : priority === "medium" - ? "signal_cellular_alt_2_bar" - : priority === "low" - ? "signal_cellular_alt_1_bar" - : "block"} + ? "signal_cellular_alt" + : priority === "medium" + ? "signal_cellular_alt_2_bar" + : priority === "low" + ? "signal_cellular_alt_1_bar" + : "block"} ); }; diff --git a/web/components/icons/state/state-group-icon.tsx b/web/components/icons/state/state-group-icon.tsx index df3b57dd8a8..c408333f15c 100644 --- a/web/components/icons/state/state-group-icon.tsx +++ b/web/components/icons/state/state-group-icon.tsx @@ -7,7 +7,7 @@ import { StateGroupUnstartedIcon, } from "components/icons"; // types -import { TStateGroups } from "types"; +import { TStateGroups } from "@plane/types"; // constants import { STATE_GROUP_COLORS } from "constants/state"; diff --git a/web/components/inbox/actions-header.tsx b/web/components/inbox/actions-header.tsx index 47f0317f4b0..cab4be6001c 100644 --- a/web/components/inbox/actions-header.tsx +++ b/web/components/inbox/actions-header.tsx @@ -3,10 +3,8 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import DatePicker from "react-datepicker"; import { Popover } from "@headlessui/react"; - -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; // hooks +import { useUser, useInboxIssues } from "hooks/store"; import useToast from "hooks/use-toast"; // components import { @@ -19,51 +17,51 @@ import { // ui import { Button } from "@plane/ui"; // icons -import { CheckCircle2, ChevronDown, ChevronUp, Clock, FileStack, Inbox, Trash2, XCircle } from "lucide-react"; +import { CheckCircle2, Clock, FileStack, Inbox, Trash2, XCircle } from "lucide-react"; // types -import type { TInboxStatus } from "types"; -import { EUserWorkspaceRoles } from "constants/workspace"; +import type { TInboxStatus } from "@plane/types"; +import { EUserProjectRoles } from "constants/project"; export const InboxActionsHeader = observer(() => { + // states const [date, setDate] = useState(new Date()); const [selectDuplicateIssue, setSelectDuplicateIssue] = useState(false); const [acceptIssueModal, setAcceptIssueModal] = useState(false); const [declineIssueModal, setDeclineIssueModal] = useState(false); const [deleteIssueModal, setDeleteIssueModal] = useState(false); - + // router const router = useRouter(); const { workspaceSlug, projectId, inboxId, inboxIssueId } = router.query; - - const { inboxIssues: inboxIssuesStore, inboxIssueDetails: inboxIssueDetailsStore, user: userStore } = useMobxStore(); - - const user = userStore?.currentUser; - const userRole = userStore.currentProjectRole; - const issuesList = inboxId ? inboxIssuesStore.inboxIssues[inboxId.toString()] : null; - + // store hooks + const { updateIssueStatus, getIssueById } = useInboxIssues(); + const { + currentUser, + membership: { currentProjectRole }, + } = useUser(); + // toast const { setToastAlert } = useToast(); + // derived values + const issue = getIssueById(inboxId as string, inboxIssueId as string); const markInboxStatus = async (data: TInboxStatus) => { - if (!workspaceSlug || !projectId || !inboxId || !inboxIssueId || !issuesList) return; + if (!workspaceSlug || !projectId || !inboxId || !inboxIssueId || !issue) return; - await inboxIssueDetailsStore - .updateIssueStatus( - workspaceSlug.toString(), - projectId.toString(), - inboxId.toString(), - issuesList.find((inboxIssue: any) => inboxIssue.issue_inbox[0].id === inboxIssueId)?.issue_inbox[0].id!, - data - ) - .catch(() => - setToastAlert({ - type: "error", - title: "Error!", - message: "Something went wrong while updating inbox status. Please try again.", - }) - ); + await updateIssueStatus( + workspaceSlug.toString(), + projectId.toString(), + inboxId.toString(), + issue.issue_inbox[0].id!, + data + ).catch(() => + setToastAlert({ + type: "error", + title: "Error!", + message: "Something went wrong while updating inbox status. Please try again.", + }) + ); }; - const issue = issuesList?.find((issue) => issue.issue_inbox[0].id === inboxIssueId); - const currentIssueIndex = issuesList?.findIndex((issue) => issue.issue_inbox[0].id === inboxIssueId) ?? 0; + // const currentIssueIndex = issuesList?.findIndex((issue) => issue.issue_inbox[0].id === inboxIssueId) ?? 0; useEffect(() => { if (!issue?.issue_inbox[0].snoozed_till) return; @@ -72,7 +70,7 @@ export const InboxActionsHeader = observer(() => { }, [issue]); const issueStatus = issue?.issue_inbox[0].status; - const isAllowed = !!userRole && userRole >= EUserWorkspaceRoles.MEMBER; + const isAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; const today = new Date(); const tomorrow = new Date(today); @@ -127,7 +125,7 @@ export const InboxActionsHeader = observer(() => {
{inboxIssueId && (
-
+ {/*
+
*/}
{isAllowed && (issueStatus === 0 || issueStatus === -2) && (
@@ -228,7 +226,7 @@ export const InboxActionsHeader = observer(() => {
)} - {(isAllowed || user?.id === issue?.created_by) && ( + {(isAllowed || currentUser?.id === issue?.created_by) && (
+
+ + ); +}; diff --git a/web/components/issues/attachment/attachment-upload.tsx b/web/components/issues/attachment/attachment-upload.tsx index c1b323e7429..c53574cb4a6 100644 --- a/web/components/issues/attachment/attachment-upload.tsx +++ b/web/components/issues/attachment/attachment-upload.tsx @@ -1,40 +1,29 @@ import { useCallback, useState } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; -import { mutate } from "swr"; import { useDropzone } from "react-dropzone"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; -// services -import { IssueAttachmentService } from "services/issue"; // hooks -import useToast from "hooks/use-toast"; -// types -import { IIssueAttachment } from "types"; -// fetch-keys -import { ISSUE_ATTACHMENTS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys"; +import { useApplication } from "hooks/store"; // constants import { MAX_FILE_SIZE } from "constants/common"; +// types +import { TAttachmentOperations } from "./root"; + +type TAttachmentOperationsModal = Exclude; type Props = { disabled?: boolean; + handleAttachmentOperations: TAttachmentOperationsModal; }; -const issueAttachmentService = new IssueAttachmentService(); - export const IssueAttachmentUpload: React.FC = observer((props) => { - const { disabled = false } = props; + const { disabled = false, handleAttachmentOperations } = props; + // store hooks + const { + router: { workspaceSlug }, + config: { envConfig }, + } = useApplication(); // states const [isLoading, setIsLoading] = useState(false); - // router - const router = useRouter(); - const { workspaceSlug, projectId, issueId } = router.query; - - const { setToastAlert } = useToast(); - - const { - appConfig: { envConfig }, - } = useMobxStore(); const onDrop = useCallback((acceptedFiles: File[]) => { if (!acceptedFiles[0] || !workspaceSlug) return; @@ -49,31 +38,7 @@ export const IssueAttachmentUpload: React.FC = observer((props) => { }) ); setIsLoading(true); - - issueAttachmentService - .uploadIssueAttachment(workspaceSlug as string, projectId as string, issueId as string, formData) - .then((res) => { - mutate( - ISSUE_ATTACHMENTS(issueId as string), - (prevData) => [res, ...(prevData ?? [])], - false - ); - mutate(PROJECT_ISSUES_ACTIVITY(issueId as string)); - setToastAlert({ - type: "success", - title: "Success!", - message: "File added successfully.", - }); - setIsLoading(false); - }) - .catch(() => { - setIsLoading(false); - setToastAlert({ - type: "error", - title: "error!", - message: "Something went wrong. please check file type & size (max 5 MB)", - }); - }); + handleAttachmentOperations.create(formData).finally(() => setIsLoading(false)); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); diff --git a/web/components/issues/attachment/attachments-list.tsx b/web/components/issues/attachment/attachments-list.tsx new file mode 100644 index 00000000000..6644d7e8c9f --- /dev/null +++ b/web/components/issues/attachment/attachments-list.tsx @@ -0,0 +1,32 @@ +import { FC } from "react"; +import { observer } from "mobx-react-lite"; +// hooks +import { useIssueDetail } from "hooks/store"; +// components +import { IssueAttachmentsDetail } from "./attachment-detail"; +// types +import { TAttachmentOperations } from "./root"; + +export type TAttachmentOperationsRemoveModal = Exclude; + +export type TIssueAttachmentsList = { + handleAttachmentOperations: TAttachmentOperationsRemoveModal; +}; + +export const IssueAttachmentsList: FC = observer((props) => { + const { handleAttachmentOperations } = props; + // store hooks + const { + attachment: { issueAttachments }, + } = useIssueDetail(); + + return ( + <> + {issueAttachments && + issueAttachments.length > 0 && + issueAttachments.map((attachmentId) => ( + + ))} + + ); +}); diff --git a/web/components/issues/attachment/attachments.tsx b/web/components/issues/attachment/attachments.tsx deleted file mode 100644 index da751cbe310..00000000000 --- a/web/components/issues/attachment/attachments.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import React, { useState } from "react"; -import Link from "next/link"; -import { useRouter } from "next/router"; -import useSWR from "swr"; -// ui -import { Tooltip } from "@plane/ui"; -import { DeleteAttachmentModal } from "./delete-attachment-modal"; -// icons -import { getFileIcon } from "components/icons"; -import { AlertCircle, X } from "lucide-react"; -// services -import { IssueAttachmentService } from "services/issue"; -import { ProjectMemberService } from "services/project"; -// fetch-key -import { ISSUE_ATTACHMENTS, PROJECT_MEMBERS } from "constants/fetch-keys"; -// helper -import { truncateText } from "helpers/string.helper"; -import { renderFormattedDate } from "helpers/date-time.helper"; -import { convertBytesToSize, getFileExtension, getFileName } from "helpers/attachment.helper"; -// type -import { IIssueAttachment } from "types"; - -// services -const issueAttachmentService = new IssueAttachmentService(); -const projectMemberService = new ProjectMemberService(); - -type Props = { - editable: boolean; -}; - -export const IssueAttachments: React.FC = (props) => { - const { editable } = props; - - // states - const [deleteAttachment, setDeleteAttachment] = useState(null); - const [attachmentDeleteModal, setAttachmentDeleteModal] = useState(false); - - const router = useRouter(); - const { workspaceSlug, projectId, issueId } = router.query; - - const { data: attachments } = useSWR( - workspaceSlug && projectId && issueId ? ISSUE_ATTACHMENTS(issueId as string) : null, - workspaceSlug && projectId && issueId - ? () => issueAttachmentService.getIssueAttachment(workspaceSlug as string, projectId as string, issueId as string) - : null - ); - - const { data: people } = useSWR( - workspaceSlug && projectId ? PROJECT_MEMBERS(projectId as string) : null, - workspaceSlug && projectId - ? () => projectMemberService.fetchProjectMembers(workspaceSlug as string, projectId as string) - : null - ); - - return ( - <> - - {attachments && - attachments.length > 0 && - attachments.map((file) => ( -
- -
-
{getFileIcon(getFileExtension(file.asset))}
-
-
- - {truncateText(`${getFileName(file.attributes.name)}`, 10)} - - person.member.id === file.updated_by)?.member.display_name ?? "" - } uploaded on ${renderFormattedDate(file.updated_at)}`} - > - - - - -
- -
- {getFileExtension(file.asset).toUpperCase()} - {convertBytesToSize(file.attributes.size)} -
-
-
- - - {editable && ( - - )} -
- ))} - - ); -}; diff --git a/web/components/issues/attachment/delete-attachment-modal.tsx b/web/components/issues/attachment/delete-attachment-confirmation-modal.tsx similarity index 69% rename from web/components/issues/attachment/delete-attachment-modal.tsx rename to web/components/issues/attachment/delete-attachment-confirmation-modal.tsx index d4f39145975..6c26bf85080 100644 --- a/web/components/issues/attachment/delete-attachment-modal.tsx +++ b/web/components/issues/attachment/delete-attachment-confirmation-modal.tsx @@ -1,72 +1,42 @@ -import React from "react"; - -import { useRouter } from "next/router"; - -import { mutate } from "swr"; - +import { FC, Fragment, Dispatch, SetStateAction, useState } from "react"; +import { AlertTriangle } from "lucide-react"; // headless ui import { Dialog, Transition } from "@headlessui/react"; -// services -import { IssueAttachmentService } from "services/issue"; -// hooks -import useToast from "hooks/use-toast"; // ui import { Button } from "@plane/ui"; -// icons -import { AlertTriangle } from "lucide-react"; // helper import { getFileName } from "helpers/attachment.helper"; // types -import type { IIssueAttachment } from "types"; -// fetch-keys -import { ISSUE_ATTACHMENTS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys"; +import type { TIssueAttachment } from "@plane/types"; +import { TIssueAttachmentsList } from "./attachments-list"; -type Props = { +type Props = TIssueAttachmentsList & { isOpen: boolean; - setIsOpen: React.Dispatch>; - data: IIssueAttachment | null; + setIsOpen: Dispatch>; + data: TIssueAttachment; }; -// services -const issueAttachmentService = new IssueAttachmentService(); - -export const DeleteAttachmentModal: React.FC = ({ isOpen, setIsOpen, data }) => { - const router = useRouter(); - const { workspaceSlug, projectId, issueId } = router.query; - - const { setToastAlert } = useToast(); +export const IssueAttachmentDeleteModal: FC = (props) => { + const { isOpen, setIsOpen, data, handleAttachmentOperations } = props; + // state + const [loader, setLoader] = useState(false); const handleClose = () => { setIsOpen(false); + setLoader(false); }; const handleDeletion = async (assetId: string) => { - if (!workspaceSlug || !projectId || !data) return; - - mutate( - ISSUE_ATTACHMENTS(issueId as string), - (prevData) => (prevData ?? [])?.filter((p) => p.id !== assetId), - false - ); - - await issueAttachmentService - .deleteIssueAttachment(workspaceSlug as string, projectId as string, issueId as string, assetId as string) - .then(() => mutate(PROJECT_ISSUES_ACTIVITY(issueId as string))) - .catch(() => { - setToastAlert({ - type: "error", - title: "error!", - message: "Something went wrong please try again.", - }); - }); + setLoader(true); + handleAttachmentOperations.remove(assetId).finally(() => handleClose()); }; return ( data && ( - + = ({ isOpen, setIsOpen, data
= ({ isOpen, setIsOpen, data tabIndex={1} onClick={() => { handleDeletion(data.id); - handleClose(); }} + disabled={loader} > - Delete + {loader ? "Deleting..." : "Delete"}
diff --git a/web/components/issues/attachment/index.ts b/web/components/issues/attachment/index.ts index 9546de31e7e..d4385e7da7c 100644 --- a/web/components/issues/attachment/index.ts +++ b/web/components/issues/attachment/index.ts @@ -1,3 +1,7 @@ +export * from "./root"; + export * from "./attachment-upload"; -export * from "./attachments"; -export * from "./delete-attachment-modal"; +export * from "./delete-attachment-confirmation-modal"; + +export * from "./attachments-list"; +export * from "./attachment-detail"; diff --git a/web/components/issues/attachment/root.tsx b/web/components/issues/attachment/root.tsx new file mode 100644 index 00000000000..9d8a31b05b4 --- /dev/null +++ b/web/components/issues/attachment/root.tsx @@ -0,0 +1,77 @@ +import { FC, useMemo } from "react"; +// hooks +import { useApplication, useIssueDetail } from "hooks/store"; +import useToast from "hooks/use-toast"; +// components +import { IssueAttachmentUpload } from "./attachment-upload"; +import { IssueAttachmentsList } from "./attachments-list"; + +export type TIssueAttachmentRoot = { + isEditable: boolean; +}; + +export type TAttachmentOperations = { + create: (data: FormData) => Promise; + remove: (linkId: string) => Promise; +}; + +export const IssueAttachmentRoot: FC = (props) => { + // props + const { isEditable } = props; + // hooks + const { + router: { workspaceSlug, projectId }, + } = useApplication(); + const { issueId, createAttachment, removeAttachment } = useIssueDetail(); + const { setToastAlert } = useToast(); + + const handleAttachmentOperations: TAttachmentOperations = useMemo( + () => ({ + create: async (data: FormData) => { + try { + if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing required fields"); + await createAttachment(workspaceSlug, projectId, issueId, data); + setToastAlert({ + message: "The attachment has been successfully uploaded", + type: "success", + title: "Attachment uploaded", + }); + } catch (error) { + setToastAlert({ + message: "The attachment could not be uploaded", + type: "error", + title: "Attachment not uploaded", + }); + } + }, + remove: async (attachmentId: string) => { + try { + if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing required fields"); + await removeAttachment(workspaceSlug, projectId, issueId, attachmentId); + setToastAlert({ + message: "The attachment has been successfully removed", + type: "success", + title: "Attachment removed", + }); + } catch (error) { + setToastAlert({ + message: "The Attachment could not be removed", + type: "error", + title: "Attachment not removed", + }); + } + }, + }), + [workspaceSlug, projectId, issueId, createAttachment, removeAttachment, setToastAlert] + ); + + return ( +
+

Attachments

+
+ + +
+
+ ); +}; diff --git a/web/components/issues/comment/add-comment.tsx b/web/components/issues/comment/add-comment.tsx index 658e825bff1..1bd2c83d681 100644 --- a/web/components/issues/comment/add-comment.tsx +++ b/web/components/issues/comment/add-comment.tsx @@ -1,7 +1,8 @@ import React from "react"; import { useRouter } from "next/router"; import { useForm, Controller } from "react-hook-form"; - +// hooks +import { useMention } from "hooks/store"; // services import { FileService } from "services/file.service"; // components @@ -9,10 +10,8 @@ import { LiteTextEditorWithRef } from "@plane/lite-text-editor"; // ui import { Button } from "@plane/ui"; import { Globe2, Lock } from "lucide-react"; - // types -import type { IIssueActivity } from "types"; -import useEditorSuggestions from "hooks/use-editor-suggestions"; +import type { IIssueActivity } from "@plane/types"; const defaultValues: Partial = { access: "INTERNAL", @@ -47,13 +46,14 @@ const commentAccess: commentAccessType[] = [ const fileService = new FileService(); export const AddComment: React.FC = ({ disabled = false, onSubmit, showAccessSpecifier = false }) => { + // refs const editorRef = React.useRef(null); - + // router const router = useRouter(); const { workspaceSlug } = router.query; - - const editorSuggestions = useEditorSuggestions(); - + // store hooks + const { mentionHighlights, mentionSuggestions } = useMention(); + // form info const { control, formState: { isSubmitting }, @@ -99,8 +99,8 @@ export const AddComment: React.FC = ({ disabled = false, onSubmit, showAc ? { accessValue: accessValue ?? "INTERNAL", onAccessChange, showAccessSpecifier, commentAccess } : undefined } - mentionSuggestions={editorSuggestions.mentionSuggestions} - mentionHighlights={editorSuggestions.mentionHighlights} + mentionSuggestions={mentionSuggestions} + mentionHighlights={mentionHighlights} submitButton={
@@ -145,13 +140,13 @@ export const CommentCard: React.FC = ({ ref={showEditorRef} value={comment.comment_html ?? ""} customClassName="text-xs border border-custom-border-200 bg-custom-background-100" - mentionHighlights={editorSuggestions.mentionHighlights} + mentionHighlights={mentionHighlights} />
- {user?.id === comment.actor && ( + {currentUser?.id === comment.actor && ( setIsEditing(true)} className="flex items-center gap-1"> @@ -191,4 +186,4 @@ export const CommentCard: React.FC = ({ )}
); -}; +}); diff --git a/web/components/issues/comment/comment-reaction.tsx b/web/components/issues/comment/comment-reaction.tsx index c920caeba74..eb80b0323ad 100644 --- a/web/components/issues/comment/comment-reaction.tsx +++ b/web/components/issues/comment/comment-reaction.tsx @@ -1,13 +1,15 @@ import { FC } from "react"; import { useRouter } from "next/router"; +import { observer } from "mobx-react-lite"; // hooks -import useUser from "hooks/use-user"; +import { useUser } from "hooks/store"; import useCommentReaction from "hooks/use-comment-reaction"; // ui import { ReactionSelector } from "components/core"; // helper import { renderEmoji } from "helpers/emoji.helper"; -import { IssueCommentReaction } from "types"; +// types +import { IssueCommentReaction } from "@plane/types"; type Props = { projectId?: string | string[]; @@ -15,13 +17,13 @@ type Props = { readonly?: boolean; }; -export const CommentReaction: FC = (props) => { +export const CommentReaction: FC = observer((props) => { const { projectId, commentId, readonly = false } = props; - + // router const router = useRouter(); const { workspaceSlug } = router.query; - - const { user } = useUser(); + // store hooks + const { currentUser } = useUser(); const { commentReactions, groupedReactions, handleReactionCreate, handleReactionDelete } = useCommentReaction( workspaceSlug, @@ -33,7 +35,7 @@ export const CommentReaction: FC = (props) => { if (!workspaceSlug || !projectId || !commentId) return; const isSelected = commentReactions?.some( - (r: IssueCommentReaction) => r.actor === user?.id && r.reaction === reaction + (r: IssueCommentReaction) => r.actor === currentUser?.id && r.reaction === reaction ); if (isSelected) { @@ -51,7 +53,7 @@ export const CommentReaction: FC = (props) => { position="top" value={ commentReactions - ?.filter((reaction: IssueCommentReaction) => reaction.actor === user?.id) + ?.filter((reaction: IssueCommentReaction) => reaction.actor === currentUser?.id) .map((r: IssueCommentReaction) => r.reaction) || [] } onSelect={handleReactionClick} @@ -70,7 +72,9 @@ export const CommentReaction: FC = (props) => { }} key={reaction} className={`flex h-full items-center gap-1 rounded-md px-2 py-1 text-sm text-custom-text-100 ${ - commentReactions?.some((r: IssueCommentReaction) => r.actor === user?.id && r.reaction === reaction) + commentReactions?.some( + (r: IssueCommentReaction) => r.actor === currentUser?.id && r.reaction === reaction + ) ? "bg-custom-primary-100/10" : "bg-custom-background-80" }`} @@ -78,7 +82,9 @@ export const CommentReaction: FC = (props) => { {renderEmoji(reaction)} r.actor === user?.id && r.reaction === reaction) + commentReactions?.some( + (r: IssueCommentReaction) => r.actor === currentUser?.id && r.reaction === reaction + ) ? "text-custom-primary-100" : "" } @@ -90,4 +96,4 @@ export const CommentReaction: FC = (props) => { )}
); -}; +}); diff --git a/web/components/issues/delete-archived-issue-modal.tsx b/web/components/issues/delete-archived-issue-modal.tsx index 14ecd7edd16..49d9e19ddee 100644 --- a/web/components/issues/delete-archived-issue-modal.tsx +++ b/web/components/issues/delete-archived-issue-modal.tsx @@ -3,19 +3,19 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import { Dialog, Transition } from "@headlessui/react"; import { AlertTriangle } from "lucide-react"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; // hooks import useToast from "hooks/use-toast"; +import { useIssues, useProject } from "hooks/store"; // ui import { Button } from "@plane/ui"; // types -import type { IIssue } from "types"; +import type { TIssue } from "@plane/types"; +import { EIssuesStoreType } from "constants/issue"; type Props = { isOpen: boolean; handleClose: () => void; - data: IIssue; + data: TIssue; onSubmit?: () => Promise; }; @@ -26,8 +26,11 @@ export const DeleteArchivedIssueModal: React.FC = observer((props) => { const { workspaceSlug } = router.query; const { setToastAlert } = useToast(); + const { getProjectById } = useProject(); - const { archivedIssueDetail: archivedIssueDetailStore } = useMobxStore(); + const { + issues: { removeIssue }, + } = useIssues(EIssuesStoreType.ARCHIVED); const [isDeleteLoading, setIsDeleteLoading] = useState(false); @@ -45,8 +48,7 @@ export const DeleteArchivedIssueModal: React.FC = observer((props) => { setIsDeleteLoading(true); - await archivedIssueDetailStore - .deleteArchivedIssue(workspaceSlug.toString(), data.project, data.id) + await removeIssue(workspaceSlug.toString(), data.project_id, data.id) .then(() => { if (onSubmit) onSubmit(); }) @@ -106,7 +108,7 @@ export const DeleteArchivedIssueModal: React.FC = observer((props) => {

Are you sure you want to delete issue{" "} - {data?.project_detail.identifier}-{data?.sequence_id} + {getProjectById(data?.project_id)?.identifier}-{data?.sequence_id} {""}? All of the data related to the archived issue will be permanently removed. This action cannot be undone. diff --git a/web/components/issues/delete-draft-issue-modal.tsx b/web/components/issues/delete-draft-issue-modal.tsx index 955d8ac78b6..6a2caba1801 100644 --- a/web/components/issues/delete-draft-issue-modal.tsx +++ b/web/components/issues/delete-draft-issue-modal.tsx @@ -1,9 +1,6 @@ import React, { useEffect, useState } from "react"; import { useRouter } from "next/router"; -import { observer } from "mobx-react-lite"; import { Dialog, Transition } from "@headlessui/react"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; // services import { IssueDraftService } from "services/issue"; // hooks @@ -13,29 +10,29 @@ import { AlertTriangle } from "lucide-react"; // ui import { Button } from "@plane/ui"; // types -import type { IIssue } from "types"; +import type { TIssue } from "@plane/types"; +import { useProject } from "hooks/store"; type Props = { isOpen: boolean; handleClose: () => void; - data: IIssue | null; + data: TIssue | null; onSubmit?: () => Promise | void; }; const issueDraftService = new IssueDraftService(); -export const DeleteDraftIssueModal: React.FC = observer((props) => { +export const DeleteDraftIssueModal: React.FC = (props) => { const { isOpen, handleClose, data, onSubmit } = props; - + // states const [isDeleteLoading, setIsDeleteLoading] = useState(false); - - const { user: userStore } = useMobxStore(); - const user = userStore.currentUser; - + // router const router = useRouter(); const { workspaceSlug } = router.query; - + // toast alert const { setToastAlert } = useToast(); + // hooks + const { getProjectById } = useProject(); useEffect(() => { setIsDeleteLoading(false); @@ -47,12 +44,12 @@ export const DeleteDraftIssueModal: React.FC = observer((props) => { }; const handleDeletion = async () => { - if (!workspaceSlug || !data || !user) return; + if (!workspaceSlug || !data) return; setIsDeleteLoading(true); await issueDraftService - .deleteDraftIssue(workspaceSlug as string, data.project, data.id) + .deleteDraftIssue(workspaceSlug.toString(), data.project_id, data.id) .then(() => { setIsDeleteLoading(false); handleClose(); @@ -64,7 +61,7 @@ export const DeleteDraftIssueModal: React.FC = observer((props) => { }); }) .catch((error) => { - console.log(error); + console.error(error); handleClose(); setToastAlert({ title: "Error", @@ -116,7 +113,7 @@ export const DeleteDraftIssueModal: React.FC = observer((props) => {

Are you sure you want to delete issue{" "} - {data?.project_detail.identifier}-{data?.sequence_id} + {data && getProjectById(data?.project_id)?.identifier}-{data?.sequence_id} {""}? All of the data related to the draft issue will be permanently removed. This action cannot be undone. @@ -138,4 +135,4 @@ export const DeleteDraftIssueModal: React.FC = observer((props) => { ); -}); +}; diff --git a/web/components/issues/delete-issue-modal.tsx b/web/components/issues/delete-issue-modal.tsx index 2f53a825f79..e2d4a4a00fe 100644 --- a/web/components/issues/delete-issue-modal.tsx +++ b/web/components/issues/delete-issue-modal.tsx @@ -6,26 +6,37 @@ import { Button } from "@plane/ui"; // hooks import useToast from "hooks/use-toast"; // types -import type { IIssue } from "types"; +import { useIssues } from "hooks/store/use-issues"; +import { TIssue } from "@plane/types"; +import { useProject } from "hooks/store"; type Props = { isOpen: boolean; handleClose: () => void; - data: IIssue; + dataId?: string | null | undefined; + data?: TIssue; onSubmit?: () => Promise; }; export const DeleteIssueModal: React.FC = (props) => { - const { data, isOpen, handleClose, onSubmit } = props; + const { dataId, data, isOpen, handleClose, onSubmit } = props; + + const { issueMap } = useIssues(); const [isDeleteLoading, setIsDeleteLoading] = useState(false); const { setToastAlert } = useToast(); + // hooks + const { getProjectById } = useProject(); useEffect(() => { setIsDeleteLoading(false); }, [isOpen]); + if (!dataId && !data) return null; + + const issue = data ? data : issueMap[dataId!]; + const onClose = () => { setIsDeleteLoading(false); handleClose(); @@ -93,7 +104,7 @@ export const DeleteIssueModal: React.FC = (props) => {

Are you sure you want to delete issue{" "} - {data?.project_detail?.identifier}-{data?.sequence_id} + {getProjectById(issue?.project_id)?.identifier}-{issue?.sequence_id} {""}? All of the data related to the issue will be permanently removed. This action cannot be undone. diff --git a/web/components/issues/description-form.tsx b/web/components/issues/description-form.tsx index 677ab5e2292..3f463496e40 100644 --- a/web/components/issues/description-form.tsx +++ b/web/components/issues/description-form.tsx @@ -7,10 +7,10 @@ import debounce from "lodash/debounce"; import { TextArea } from "@plane/ui"; import { RichTextEditor } from "@plane/rich-text-editor"; // types -import { IIssue } from "types"; +import { TIssue } from "@plane/types"; // services import { FileService } from "services/file.service"; -import useEditorSuggestions from "hooks/use-editor-suggestions"; +import { useMention } from "hooks/store"; export interface IssueDescriptionFormValues { name: string; @@ -39,16 +39,16 @@ export const IssueDescriptionForm: FC = (props) => { const [characterLimit, setCharacterLimit] = useState(false); const { setShowAlert } = useReloadConfirmations(); - - const editorSuggestion = useEditorSuggestions(); - + // store hooks + const { mentionHighlights, mentionSuggestions } = useMention(); + // form info const { handleSubmit, watch, reset, control, formState: { errors }, - } = useForm({ + } = useForm({ defaultValues: { name: "", description_html: "", @@ -72,7 +72,7 @@ export const IssueDescriptionForm: FC = (props) => { }, [issue.id]); // TODO: verify the exhaustive-deps warning const handleDescriptionFormSubmit = useCallback( - async (formData: Partial) => { + async (formData: Partial) => { if (!formData?.name || formData?.name.length === 0 || formData?.name.length > 255) return; await handleFormSubmit({ @@ -135,10 +135,8 @@ export const IssueDescriptionForm: FC = (props) => { debouncedFormSave(); }} required - className={`min-h-min block w-full resize-none overflow-hidden rounded border-none bg-transparent px-3 py-2 text-2xl font-medium outline-none ring-0 focus:ring-1 focus:ring-custom-primary ${ - !isAllowed ? "hover:cursor-not-allowed" : "" - }`} - hasError={Boolean(errors?.description)} + className="min-h-min block w-full resize-none overflow-hidden rounded border-none bg-transparent px-3 py-2 text-2xl font-medium outline-none ring-0 focus:ring-1 focus:ring-custom-primary" + hasError={Boolean(errors?.name)} role="textbox" disabled={!isAllowed} /> @@ -172,9 +170,7 @@ export const IssueDescriptionForm: FC = (props) => { setShouldShowAlert={setShowAlert} setIsSubmitting={setIsSubmitting} dragDropEnabled - customClassName={ - isAllowed ? "min-h-[150px] shadow-sm" : "!p-0 !pt-2 text-custom-text-200 pointer-events-none" - } + customClassName={isAllowed ? "min-h-[150px] shadow-sm" : "!p-0 !pt-2 text-custom-text-200"} noBorder={!isAllowed} onChange={(description: Object, description_html: string) => { setShowAlert(true); @@ -182,8 +178,8 @@ export const IssueDescriptionForm: FC = (props) => { onChange(description_html); debouncedFormSave(); }} - mentionSuggestions={editorSuggestion.mentionSuggestions} - mentionHighlights={editorSuggestion.mentionHighlights} + mentionSuggestions={mentionSuggestions} + mentionHighlights={mentionHighlights} /> )} /> diff --git a/web/components/issues/draft-issue-form.tsx b/web/components/issues/draft-issue-form.tsx index 2e34fe3312c..2d79f4ee16a 100644 --- a/web/components/issues/draft-issue-form.tsx +++ b/web/components/issues/draft-issue-form.tsx @@ -1,72 +1,62 @@ import React, { FC, useState, useEffect, useRef } from "react"; import { useRouter } from "next/router"; import { Controller, useForm } from "react-hook-form"; -// services -import { AIService } from "services/ai.service"; -import { FileService } from "services/file.service"; +import { observer } from "mobx-react-lite"; +import { Sparkle, X } from "lucide-react"; // hooks +import { useApplication, useEstimate, useMention } from "hooks/store"; import useToast from "hooks/use-toast"; import useLocalStorage from "hooks/use-local-storage"; +// services +import { AIService } from "services/ai.service"; +import { FileService } from "services/file.service"; // components import { GptAssistantPopover } from "components/core"; import { ParentIssuesListModal } from "components/issues"; -import { - IssueAssigneeSelect, - IssueDateSelect, - IssueEstimateSelect, - IssueLabelSelect, - IssuePrioritySelect, - IssueProjectSelect, - IssueStateSelect, -} from "components/issues/select"; +import { IssueLabelSelect } from "components/issues/select"; import { CreateStateModal } from "components/states"; import { CreateLabelModal } from "components/labels"; +import { RichTextEditorWithRef } from "@plane/rich-text-editor"; +import { + DateDropdown, + EstimateDropdown, + PriorityDropdown, + ProjectDropdown, + ProjectMemberDropdown, + StateDropdown, +} from "components/dropdowns"; // ui -import {} from "components/ui"; import { Button, CustomMenu, Input, ToggleSwitch } from "@plane/ui"; -// icons -import { Sparkle, X } from "lucide-react"; +// helpers +import { renderFormattedPayloadDate } from "helpers/date-time.helper"; // types -import type { IUser, IIssue, ISearchIssueResponse } from "types"; -// components -import { RichTextEditorWithRef } from "@plane/rich-text-editor"; -import useEditorSuggestions from "hooks/use-editor-suggestions"; -import { observer } from "mobx-react-lite"; -import { useMobxStore } from "lib/mobx/store-provider"; +import type { IUser, TIssue, ISearchIssueResponse } from "@plane/types"; const aiService = new AIService(); const fileService = new FileService(); -const defaultValues: Partial = { - project: "", +const defaultValues: Partial = { + project_id: "", name: "", - description: { - type: "doc", - content: [ - { - type: "paragraph", - }, - ], - }, description_html: "

", estimate_point: null, - state: "", - parent: null, + state_id: "", + parent_id: null, priority: "none", - assignees: [], - labels: [], - start_date: null, - target_date: null, + assignee_ids: [], + label_ids: [], + start_date: undefined, + target_date: undefined, }; interface IssueFormProps { handleFormSubmit: ( - formData: Partial, + formData: Partial, action?: "createDraft" | "createNewIssue" | "updateDraft" | "convertToNewIssue" ) => Promise; - data?: Partial | null; + data?: Partial | null; isOpen: boolean; - prePopulatedData?: Partial | null; + prePopulatedData?: Partial | null; projectId: string; setActiveProject: React.Dispatch>; createMore: boolean; @@ -112,10 +102,12 @@ export const DraftIssueForm: FC = observer((props) => { const [selectedParentIssue, setSelectedParentIssue] = useState(null); const [gptAssistantModal, setGptAssistantModal] = useState(false); const [iAmFeelingLucky, setIAmFeelingLucky] = useState(false); + // store hooks + const { areEstimatesActiveForProject } = useEstimate(); + const { mentionHighlights, mentionSuggestions } = useMention(); // hooks const { setValue: setLocalStorageValue } = useLocalStorage("draftedIssue", {}); const { setToastAlert } = useToast(); - const editorSuggestions = useEditorSuggestions(); // refs const editorRef = useRef(null); // router @@ -123,8 +115,8 @@ export const DraftIssueForm: FC = observer((props) => { const { workspaceSlug } = router.query; // store const { - appConfig: { envConfig }, - } = useMobxStore(); + config: { envConfig }, + } = useApplication(); // form info const { formState: { errors, isSubmitting }, @@ -135,27 +127,26 @@ export const DraftIssueForm: FC = observer((props) => { getValues, setValue, setFocus, - } = useForm({ + } = useForm({ defaultValues: prePopulatedData ?? defaultValues, reValidateMode: "onChange", }); const issueName = watch("name"); - const payload: Partial = { + const payload: Partial = { name: watch("name"), - description: watch("description"), description_html: watch("description_html"), - state: watch("state"), + state_id: watch("state_id"), priority: watch("priority"), - assignees: watch("assignees"), - labels: watch("labels"), + assignee_ids: watch("assignee_ids"), + label_ids: watch("label_ids"), start_date: watch("start_date"), target_date: watch("target_date"), - project: watch("project"), - parent: watch("parent"), - cycle: watch("cycle"), - module: watch("module"), + project_id: watch("project_id"), + parent_id: watch("parent_id"), + cycle_id: watch("cycle_id"), + module_id: watch("module_id"), }; useEffect(() => { @@ -189,31 +180,24 @@ export const DraftIssueForm: FC = observer((props) => { // }; const handleCreateUpdateIssue = async ( - formData: Partial, + formData: Partial, action: "createDraft" | "createNewIssue" | "updateDraft" | "convertToNewIssue" = "createDraft" ) => { await handleFormSubmit( { ...(data ?? {}), ...formData, - is_draft: action === "createDraft" || action === "updateDraft", + // is_draft: action === "createDraft" || action === "updateDraft", }, action ); + // TODO: check_with_backend setGptAssistantModal(false); reset({ ...defaultValues, - project: projectId, - description: { - type: "doc", - content: [ - { - type: "paragraph", - }, - ], - }, + project_id: projectId, description_html: "

", }); editorRef?.current?.clearEditor(); @@ -222,7 +206,7 @@ export const DraftIssueForm: FC = observer((props) => { const handleAiAssistance = async (response: string) => { if (!workspaceSlug || !projectId) return; - setValue("description", {}); + // setValue("description", {}); setValue("description_html", `${watch("description_html")}

${response}

`); editorRef.current?.setEditorValue(`${watch("description_html")}`); }; @@ -280,7 +264,7 @@ export const DraftIssueForm: FC = observer((props) => { useEffect(() => { reset({ ...getValues(), - project: projectId, + project_id: projectId, }); }, [getValues, projectId, reset]); @@ -302,7 +286,7 @@ export const DraftIssueForm: FC = observer((props) => { isOpen={labelModal} handleClose={() => setLabelModal(false)} projectId={projectId} - onSuccess={(response) => setValue("labels", [...watch("labels"), response.id])} + onSuccess={(response) => setValue("label_ids", [...watch("label_ids"), response.id])} /> )} @@ -316,14 +300,15 @@ export const DraftIssueForm: FC = observer((props) => { {(fieldsToShow.includes("all") || fieldsToShow.includes("project")) && ( ( - { + onChange={(val) => { onChange(val); setActiveProject(val); }} + buttonVariant="background-with-text" /> )} /> @@ -332,7 +317,7 @@ export const DraftIssueForm: FC = observer((props) => { {status ? "Update" : "Create"} Issue
- {watch("parent") && + {watch("parent_id") && (fieldsToShow.includes("all") || fieldsToShow.includes("parent")) && selectedParentIssue && (
@@ -350,7 +335,7 @@ export const DraftIssueForm: FC = observer((props) => { { - setValue("parent", null); + setValue("parent_id", null); setSelectedParentIssue(null); }} /> @@ -454,10 +439,9 @@ export const DraftIssueForm: FC = observer((props) => { customClassName="min-h-[150px]" onChange={(description: Object, description_html: string) => { onChange(description_html); - setValue("description", description); }} - mentionHighlights={editorSuggestions.mentionHighlights} - mentionSuggestions={editorSuggestions.mentionSuggestions} + mentionHighlights={mentionHighlights} + mentionSuggestions={mentionSuggestions} /> )} /> @@ -467,14 +451,16 @@ export const DraftIssueForm: FC = observer((props) => { {(fieldsToShow.includes("all") || fieldsToShow.includes("state")) && ( ( - +
+ +
)} /> )} @@ -483,80 +469,100 @@ export const DraftIssueForm: FC = observer((props) => { control={control} name="priority" render={({ field: { value, onChange } }) => ( - +
+ +
)} /> )} {(fieldsToShow.includes("all") || fieldsToShow.includes("assignee")) && ( ( - +
+ +
)} /> )} {(fieldsToShow.includes("all") || fieldsToShow.includes("label")) && ( ( - +
+ +
)} /> )} {(fieldsToShow.includes("all") || fieldsToShow.includes("startDate")) && ( -
- ( - ( +
+ onChange(date ? renderFormattedPayloadDate(date) : null)} + buttonVariant="border-with-text" + placeholder="Start date" + maxDate={maxDate ?? undefined} /> - )} - /> -
+
+ )} + /> )} {(fieldsToShow.includes("all") || fieldsToShow.includes("dueDate")) && ( -
- ( - ( +
+ onChange(date ? renderFormattedPayloadDate(date) : null)} + buttonVariant="border-with-text" + placeholder="Due date" + minDate={minDate ?? undefined} /> - )} - /> -
+
+ )} + /> )} - {(fieldsToShow.includes("all") || fieldsToShow.includes("estimate")) && ( -
+ {(fieldsToShow.includes("all") || fieldsToShow.includes("estimate")) && + areEstimatesActiveForProject(projectId) && ( ( - +
+ +
)} /> -
- )} + )} {(fieldsToShow.includes("all") || fieldsToShow.includes("parent")) && ( ( = observer((props) => { )} {(fieldsToShow.includes("all") || fieldsToShow.includes("parent")) && ( - {watch("parent") ? ( + {watch("parent_id") ? ( <> setParentIssueListModalOpen(true)}> Change parent issue - setValue("parent", null)}> + setValue("parent_id", null)}> Remove parent issue diff --git a/web/components/issues/draft-issue-modal.tsx b/web/components/issues/draft-issue-modal.tsx index 51ff30d4060..4008e6383f1 100644 --- a/web/components/issues/draft-issue-modal.tsx +++ b/web/components/issues/draft-issue-modal.tsx @@ -3,27 +3,27 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import { mutate } from "swr"; import { Dialog, Transition } from "@headlessui/react"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; // services import { IssueService } from "services/issue"; import { ModuleService } from "services/module.service"; // hooks import useToast from "hooks/use-toast"; import useLocalStorage from "hooks/use-local-storage"; +import { useIssues, useProject, useUser } from "hooks/store"; // components import { DraftIssueForm } from "components/issues"; // types -import type { IIssue } from "types"; +import type { TIssue } from "@plane/types"; +import { EIssuesStoreType } from "constants/issue"; // fetch-keys import { PROJECT_ISSUES_DETAILS, USER_ISSUE, SUB_ISSUES } from "constants/fetch-keys"; interface IssuesModalProps { - data?: IIssue | null; + data?: TIssue | null; handleClose: () => void; isOpen: boolean; isUpdatingSingleIssue?: boolean; - prePopulateData?: Partial; + prePopulateData?: Partial; fieldsToShow?: ( | "project" | "name" @@ -38,7 +38,7 @@ interface IssuesModalProps { | "parent" | "all" )[]; - onSubmit?: (data: Partial) => Promise | void; + onSubmit?: (data: Partial) => Promise | void; } // services @@ -59,15 +59,16 @@ export const CreateUpdateDraftIssueModal: React.FC = observer( // states const [createMore, setCreateMore] = useState(false); const [activeProject, setActiveProject] = useState(null); - const [prePopulateData, setPreloadedData] = useState | undefined>(undefined); - + const [prePopulateData, setPreloadedData] = useState | undefined>(undefined); + // router const router = useRouter(); const { workspaceSlug, projectId, cycleId, moduleId } = router.query; - - const { project: projectStore, user: userStore, projectDraftIssues: draftIssueStore } = useMobxStore(); - - const user = userStore.currentUser; - const projects = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : undefined; + // store + const { issues: draftIssues } = useIssues(EIssuesStoreType.DRAFT); + const { currentUser } = useUser(); + const { workspaceProjectIds: workspaceProjects } = useProject(); + // derived values + const projects = workspaceProjects; const { clearValue: clearDraftIssueLocalStorage } = useLocalStorage("draftedIssue", {}); @@ -86,14 +87,14 @@ export const CreateUpdateDraftIssueModal: React.FC = observer( useEffect(() => { setPreloadedData(prePopulateDataProps ?? {}); - if (cycleId && !prePopulateDataProps?.cycle) { + if (cycleId && !prePopulateDataProps?.cycle_id) { setPreloadedData((prevData) => ({ ...(prevData ?? {}), ...prePopulateDataProps, cycle: cycleId.toString(), })); } - if (moduleId && !prePopulateDataProps?.module) { + if (moduleId && !prePopulateDataProps?.module_id) { setPreloadedData((prevData) => ({ ...(prevData ?? {}), ...prePopulateDataProps, @@ -102,27 +103,27 @@ export const CreateUpdateDraftIssueModal: React.FC = observer( } if ( (router.asPath.includes("my-issues") || router.asPath.includes("assigned")) && - !prePopulateDataProps?.assignees + !prePopulateDataProps?.assignee_ids ) { setPreloadedData((prevData) => ({ ...(prevData ?? {}), ...prePopulateDataProps, - assignees: prePopulateDataProps?.assignees ?? [user?.id ?? ""], + assignees: prePopulateDataProps?.assignee_ids ?? [currentUser?.id ?? ""], })); } - }, [prePopulateDataProps, cycleId, moduleId, router.asPath, user?.id]); + }, [prePopulateDataProps, cycleId, moduleId, router.asPath, currentUser?.id]); useEffect(() => { setPreloadedData(prePopulateDataProps ?? {}); - if (cycleId && !prePopulateDataProps?.cycle) { + if (cycleId && !prePopulateDataProps?.cycle_id) { setPreloadedData((prevData) => ({ ...(prevData ?? {}), ...prePopulateDataProps, cycle: cycleId.toString(), })); } - if (moduleId && !prePopulateDataProps?.module) { + if (moduleId && !prePopulateDataProps?.module_id) { setPreloadedData((prevData) => ({ ...(prevData ?? {}), ...prePopulateDataProps, @@ -131,15 +132,15 @@ export const CreateUpdateDraftIssueModal: React.FC = observer( } if ( (router.asPath.includes("my-issues") || router.asPath.includes("assigned")) && - !prePopulateDataProps?.assignees + !prePopulateDataProps?.assignee_ids ) { setPreloadedData((prevData) => ({ ...(prevData ?? {}), ...prePopulateDataProps, - assignees: prePopulateDataProps?.assignees ?? [user?.id ?? ""], + assignees: prePopulateDataProps?.assignee_ids ?? [currentUser?.id ?? ""], })); } - }, [prePopulateDataProps, cycleId, moduleId, router.asPath, user?.id]); + }, [prePopulateDataProps, cycleId, moduleId, router.asPath, currentUser?.id]); useEffect(() => { // if modal is closed, reset active project to null @@ -151,32 +152,35 @@ export const CreateUpdateDraftIssueModal: React.FC = observer( // if data is present, set active project to the project of the // issue. This has more priority than the project in the url. - if (data && data.project) return setActiveProject(data.project); + if (data && data.project_id) return setActiveProject(data.project_id); - if (prePopulateData && prePopulateData.project && !activeProject) return setActiveProject(prePopulateData.project); + if (prePopulateData && prePopulateData.project_id && !activeProject) + return setActiveProject(prePopulateData.project_id); - if (prePopulateData && prePopulateData.project && !activeProject) return setActiveProject(prePopulateData.project); + if (prePopulateData && prePopulateData.project_id && !activeProject) + return setActiveProject(prePopulateData.project_id); // if data is not present, set active project to the project // in the url. This has the least priority. if (projects && projects.length > 0 && !activeProject) - setActiveProject(projects?.find((p) => p.id === projectId)?.id ?? projects?.[0].id ?? null); + setActiveProject(projects?.find((id) => id === projectId) ?? projects?.[0] ?? null); }, [activeProject, data, projectId, projects, isOpen, prePopulateData]); - const createDraftIssue = async (payload: Partial) => { - if (!workspaceSlug || !activeProject || !user) return; + const createDraftIssue = async (payload: Partial) => { + if (!workspaceSlug || !activeProject || !currentUser) return; - await draftIssueStore + await draftIssues .createIssue(workspaceSlug as string, activeProject ?? "", payload) .then(async () => { - await draftIssueStore.fetchIssues(workspaceSlug as string, activeProject ?? "", "mutation"); + await draftIssues.fetchIssues(workspaceSlug as string, activeProject ?? "", "mutation"); setToastAlert({ type: "success", title: "Success!", message: "Issue created successfully.", }); - if (payload.assignees?.some((assignee) => assignee === user?.id)) mutate(USER_ISSUE(workspaceSlug.toString())); + if (payload.assignee_ids?.some((assignee) => assignee === currentUser?.id)) + mutate(USER_ISSUE(workspaceSlug.toString())); }) .catch(() => { setToastAlert({ @@ -189,22 +193,20 @@ export const CreateUpdateDraftIssueModal: React.FC = observer( if (!createMore) onClose(); }; - const updateDraftIssue = async (payload: Partial) => { - if (!user) return; - - await draftIssueStore + const updateDraftIssue = async (payload: Partial) => { + await draftIssues .updateIssue(workspaceSlug as string, activeProject ?? "", data?.id ?? "", payload) .then((res) => { if (isUpdatingSingleIssue) { - mutate(PROJECT_ISSUES_DETAILS, (prevData) => ({ ...prevData, ...res }), false); + mutate(PROJECT_ISSUES_DETAILS, (prevData) => ({ ...prevData, ...res }), false); } else { - if (payload.parent) mutate(SUB_ISSUES(payload.parent.toString())); + if (payload.parent_id) mutate(SUB_ISSUES(payload.parent_id.toString())); } - if (!payload.is_draft) { - if (payload.cycle && payload.cycle !== "") addIssueToCycle(res.id, payload.cycle); - if (payload.module && payload.module !== "") addIssueToModule(res.id, payload.module); - } + // if (!payload.is_draft) { // TODO: check_with_backend + // if (payload.cycle_id && payload.cycle_id !== "") addIssueToCycle(res.id, payload.cycle_id); + // if (payload.module_id && payload.module_id !== "") addIssueToModule(res.id, payload.module_id); + // } if (!createMore) onClose(); @@ -224,7 +226,7 @@ export const CreateUpdateDraftIssueModal: React.FC = observer( }; const addIssueToCycle = async (issueId: string, cycleId: string) => { - if (!workspaceSlug || !activeProject || !user) return; + if (!workspaceSlug || !activeProject) return; await issueService.addIssueToCycle(workspaceSlug as string, activeProject ?? "", cycleId, { issues: [issueId], @@ -232,21 +234,21 @@ export const CreateUpdateDraftIssueModal: React.FC = observer( }; const addIssueToModule = async (issueId: string, moduleId: string) => { - if (!workspaceSlug || !activeProject || !user) return; + if (!workspaceSlug || !activeProject) return; await moduleService.addIssuesToModule(workspaceSlug as string, activeProject ?? "", moduleId as string, { issues: [issueId], }); }; - const createIssue = async (payload: Partial) => { - if (!workspaceSlug || !activeProject || !user) return; + const createIssue = async (payload: Partial) => { + if (!workspaceSlug || !activeProject) return; await issueService .createIssue(workspaceSlug.toString(), activeProject, payload) .then(async (res) => { - if (payload.cycle && payload.cycle !== "") await addIssueToCycle(res.id, payload.cycle); - if (payload.module && payload.module !== "") await addIssueToModule(res.id, payload.module); + if (payload.cycle_id && payload.cycle_id !== "") await addIssueToCycle(res.id, payload.cycle_id); + if (payload.module_id && payload.module_id !== "") await addIssueToModule(res.id, payload.module_id); setToastAlert({ type: "success", @@ -256,9 +258,10 @@ export const CreateUpdateDraftIssueModal: React.FC = observer( if (!createMore) onClose(); - if (payload.assignees?.some((assignee) => assignee === user?.id)) mutate(USER_ISSUE(workspaceSlug as string)); + if (payload.assignee_ids?.some((assignee) => assignee === currentUser?.id)) + mutate(USER_ISSUE(workspaceSlug as string)); - if (payload.parent && payload.parent !== "") mutate(SUB_ISSUES(payload.parent)); + if (payload.parent_id && payload.parent_id !== "") mutate(SUB_ISSUES(payload.parent_id)); }) .catch(() => { setToastAlert({ @@ -270,14 +273,14 @@ export const CreateUpdateDraftIssueModal: React.FC = observer( }; const handleFormSubmit = async ( - formData: Partial, + formData: Partial, action: "createDraft" | "createNewIssue" | "updateDraft" | "convertToNewIssue" = "createDraft" ) => { if (!workspaceSlug || !activeProject) return; - const payload: Partial = { + const payload: Partial = { ...formData, - description: formData.description ?? "", + // description: formData.description ?? "", description_html: formData.description_html ?? "

", }; @@ -332,7 +335,7 @@ export const CreateUpdateDraftIssueModal: React.FC = observer( projectId={activeProject ?? ""} setActiveProject={setActiveProject} status={data ? true : false} - user={user ?? undefined} + user={currentUser ?? undefined} fieldsToShow={fieldsToShow} /> diff --git a/web/components/issues/form.tsx b/web/components/issues/form.tsx index 3e55ca70c20..0f0b5cea315 100644 --- a/web/components/issues/form.tsx +++ b/web/components/issues/form.tsx @@ -2,63 +2,61 @@ import React, { FC, useState, useEffect, useRef } from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import { Controller, useForm } from "react-hook-form"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +import { LayoutPanelTop, Sparkle, X } from "lucide-react"; +// hooks +import { useApplication, useEstimate, useMention, useProject } from "hooks/store"; +import useToast from "hooks/use-toast"; // services import { AIService } from "services/ai.service"; import { FileService } from "services/file.service"; -// hooks -import useToast from "hooks/use-toast"; // components import { GptAssistantPopover } from "components/core"; import { ParentIssuesListModal } from "components/issues"; -import { - IssueAssigneeSelect, - IssueDateSelect, - IssueEstimateSelect, - IssueLabelSelect, - IssuePrioritySelect, - IssueProjectSelect, - IssueStateSelect, - IssueModuleSelect, - IssueCycleSelect, -} from "components/issues/select"; +import { IssueLabelSelect } from "components/issues/select"; import { CreateStateModal } from "components/states"; import { CreateLabelModal } from "components/labels"; +import { RichTextEditorWithRef } from "@plane/rich-text-editor"; +import { + CycleDropdown, + DateDropdown, + EstimateDropdown, + ModuleDropdown, + PriorityDropdown, + ProjectDropdown, + ProjectMemberDropdown, + StateDropdown, +} from "components/dropdowns"; // ui import { Button, CustomMenu, Input, ToggleSwitch } from "@plane/ui"; -// icons -import { LayoutPanelTop, Sparkle, X } from "lucide-react"; +// helpers +import { renderFormattedPayloadDate } from "helpers/date-time.helper"; // types -import type { IIssue, ISearchIssueResponse } from "types"; -// components -import { RichTextEditorWithRef } from "@plane/rich-text-editor"; -import useEditorSuggestions from "hooks/use-editor-suggestions"; +import type { TIssue, ISearchIssueResponse } from "@plane/types"; -const defaultValues: Partial = { - project: "", +const defaultValues: Partial = { + project_id: "", name: "", description_html: "

", estimate_point: null, - state: "", - parent: null, + state_id: "", + parent_id: null, priority: "none", - assignees: [], - labels: [], - start_date: null, - target_date: null, + assignee_ids: [], + label_ids: [], + start_date: undefined, + target_date: undefined, }; export interface IssueFormProps { - handleFormSubmit: (values: Partial) => Promise; - initialData?: Partial; + handleFormSubmit: (values: Partial) => Promise; + initialData?: Partial; projectId: string; setActiveProject: React.Dispatch>; createMore: boolean; setCreateMore: React.Dispatch>; handleDiscardClose: () => void; status: boolean; - handleFormDirty: (payload: Partial | null) => void; + handleFormDirty: (payload: Partial | null) => void; fieldsToShow: ( | "project" | "name" @@ -106,14 +104,14 @@ export const IssueForm: FC = observer((props) => { // router const router = useRouter(); const { workspaceSlug } = router.query; - // store + // store hooks const { - user: userStore, - appConfig: { envConfig }, - } = useMobxStore(); - const user = userStore.currentUser; - // hooks - const editorSuggestion = useEditorSuggestions(); + config: { envConfig }, + } = useApplication(); + const { getProjectById } = useProject(); + const { areEstimatesActiveForProject } = useEstimate(); + const { mentionHighlights, mentionSuggestions } = useMention(); + // toast alert const { setToastAlert } = useToast(); // form info const { @@ -125,50 +123,44 @@ export const IssueForm: FC = observer((props) => { getValues, setValue, setFocus, - } = useForm({ + } = useForm({ defaultValues: initialData ?? defaultValues, reValidateMode: "onChange", }); const issueName = watch("name"); - const payload: Partial = { + const payload: Partial = { name: getValues("name"), - description: getValues("description"), - state: getValues("state"), + state_id: getValues("state_id"), priority: getValues("priority"), - assignees: getValues("assignees"), - labels: getValues("labels"), + assignee_ids: getValues("assignee_ids"), + label_ids: getValues("label_ids"), start_date: getValues("start_date"), target_date: getValues("target_date"), - project: getValues("project"), - parent: getValues("parent"), - cycle: getValues("cycle"), - module: getValues("module"), + project_id: getValues("project_id"), + parent_id: getValues("parent_id"), + cycle_id: getValues("cycle_id"), + module_id: getValues("module_id"), }; + // derived values + const projectDetails = getProjectById(projectId); + useEffect(() => { if (isDirty) handleFormDirty(payload); else handleFormDirty(null); // eslint-disable-next-line react-hooks/exhaustive-deps }, [JSON.stringify(payload), isDirty]); - const handleCreateUpdateIssue = async (formData: Partial) => { + const handleCreateUpdateIssue = async (formData: Partial) => { await handleFormSubmit(formData); setGptAssistantModal(false); reset({ ...defaultValues, - project: projectId, - description: { - type: "doc", - content: [ - { - type: "paragraph", - }, - ], - }, + project_id: projectId, description_html: "

", }); editorRef?.current?.clearEditor(); @@ -177,18 +169,17 @@ export const IssueForm: FC = observer((props) => { const handleAiAssistance = async (response: string) => { if (!workspaceSlug || !projectId) return; - setValue("description", {}); setValue("description_html", `${watch("description_html")}

${response}

`); editorRef.current?.setEditorValue(`${watch("description_html")}`); }; const handleAutoGenerateDescription = async () => { - if (!workspaceSlug || !projectId || !user) return; + if (!workspaceSlug || !projectId) return; setIAmFeelingLucky(true); aiService - .createGptTask(workspaceSlug as string, projectId as string, { + .createGptTask(workspaceSlug.toString(), projectId.toString(), { prompt: issueName, task: "Generate a proper description for this issue.", }) @@ -227,7 +218,6 @@ export const IssueForm: FC = observer((props) => { reset({ ...defaultValues, ...initialData, - project: projectId, }); }, [setFocus, initialData, reset]); @@ -235,7 +225,7 @@ export const IssueForm: FC = observer((props) => { useEffect(() => { reset({ ...getValues(), - project: projectId, + project_id: projectId, }); }, [getValues, projectId, reset]); @@ -257,29 +247,31 @@ export const IssueForm: FC = observer((props) => { isOpen={labelModal} handleClose={() => setLabelModal(false)} projectId={projectId} - onSuccess={(response) => setValue("labels", [...watch("labels"), response.id])} + onSuccess={(response) => setValue("label_ids", [...watch("label_ids"), response.id])} /> )}
- {(fieldsToShow.includes("all") || fieldsToShow.includes("project")) && ( + {(fieldsToShow.includes("all") || fieldsToShow.includes("project")) && !status && ( ( - { - onChange(val); - setActiveProject(val); - }} - /> + render={({ field: { value, onChange } }) => ( +
+ { + onChange(val); + setActiveProject(val); + }} + buttonVariant="border-with-text" + /> +
)} /> )} @@ -287,7 +279,7 @@ export const IssueForm: FC = observer((props) => { {status ? "Update" : "Create"} Issue
- {watch("parent") && + {watch("parent_id") && (fieldsToShow.includes("all") || fieldsToShow.includes("parent")) && selectedParentIssue && (
@@ -305,7 +297,7 @@ export const IssueForm: FC = observer((props) => { { - setValue("parent", null); + setValue("parent_id", null); setSelectedParentIssue(null); }} /> @@ -408,10 +400,9 @@ export const IssueForm: FC = observer((props) => { customClassName="min-h-[7rem] border-custom-border-100" onChange={(description: Object, description_html: string) => { onChange(description_html); - setValue("description", description); }} - mentionHighlights={editorSuggestion.mentionHighlights} - mentionSuggestions={editorSuggestion.mentionSuggestions} + mentionHighlights={mentionHighlights} + mentionSuggestions={mentionSuggestions} /> )} /> @@ -421,14 +412,16 @@ export const IssueForm: FC = observer((props) => { {(fieldsToShow.includes("all") || fieldsToShow.includes("state")) && ( ( - +
+ +
)} /> )} @@ -437,48 +430,63 @@ export const IssueForm: FC = observer((props) => { control={control} name="priority" render={({ field: { value, onChange } }) => ( - +
+ +
)} /> )} {(fieldsToShow.includes("all") || fieldsToShow.includes("assignee")) && ( ( - +
+ 0 ? "transparent-without-text" : "border-with-text"} + buttonClassName={value?.length > 0 ? "hover:bg-transparent px-0" : ""} + placeholder="Assignees" + multiple + /> +
)} /> )} {(fieldsToShow.includes("all") || fieldsToShow.includes("label")) && ( ( - +
+ +
)} /> )} {(fieldsToShow.includes("all") || fieldsToShow.includes("startDate")) && ( -
- ( - ( +
+ onChange(date ? renderFormattedPayloadDate(date) : null)} + buttonVariant="border-with-text" + placeholder="Start date" + maxDate={maxDate ?? undefined} /> - )} - /> -
+
+ )} + /> )} {(fieldsToShow.includes("all") || fieldsToShow.includes("dueDate")) && (
@@ -486,70 +494,79 @@ export const IssueForm: FC = observer((props) => { control={control} name="target_date" render={({ field: { value, onChange } }) => ( - +
+ onChange(date ? renderFormattedPayloadDate(date) : null)} + buttonVariant="border-with-text" + placeholder="Due date" + minDate={minDate ?? undefined} + /> +
)} />
)} - {(fieldsToShow.includes("all") || fieldsToShow.includes("module")) && ( + {(fieldsToShow.includes("all") || fieldsToShow.includes("cycle")) && projectDetails?.cycle_view && ( ( - { - onChange(val); - }} - /> +
+ +
)} /> )} - {(fieldsToShow.includes("all") || fieldsToShow.includes("cycle")) && ( + {(fieldsToShow.includes("all") || fieldsToShow.includes("module")) && projectDetails?.module_view && ( ( - { - onChange(val); - }} - /> +
+ +
)} /> )} - {(fieldsToShow.includes("all") || fieldsToShow.includes("estimate")) && ( - <> + {(fieldsToShow.includes("all") || fieldsToShow.includes("estimate")) && + areEstimatesActiveForProject(projectId) && ( ( - +
+ +
)} /> - - )} + )} {(fieldsToShow.includes("all") || fieldsToShow.includes("parent")) && ( <> - {watch("parent") ? ( + {watch("parent_id") ? (
- + {selectedParentIssue && `${selectedParentIssue.project__identifier}- @@ -563,26 +580,24 @@ export const IssueForm: FC = observer((props) => { setParentIssueListModalOpen(true)}> Change parent issue - setValue("parent", null)}> + setValue("parent_id", null)}> Remove parent issue ) : ( )} ( ; issueActions: { - [EIssueActions.DELETE]: (issue: IIssue) => Promise; - [EIssueActions.UPDATE]?: (issue: IIssue) => Promise; - [EIssueActions.REMOVE]?: (issue: IIssue) => Promise; + [EIssueActions.DELETE]: (issue: TIssue) => Promise; + [EIssueActions.UPDATE]?: (issue: TIssue) => Promise; + [EIssueActions.REMOVE]?: (issue: TIssue) => Promise; }; viewId?: string; - handleDragDrop: (source: any, destination: any, issues: any, issueWithIds: any) => Promise; } export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => { - const { issueStore, issuesFilterStore, QuickActions, issueActions, viewId, handleDragDrop } = props; + const { issueStore, issuesFilterStore, QuickActions, issueActions, viewId } = props; // router const router = useRouter(); - const { workspaceSlug, peekIssueId, peekProjectId } = router.query; + const { workspaceSlug, projectId, peekIssueId, peekProjectId } = router.query; // hooks const { setToastAlert } = useToast(); + const { issueMap } = useIssues(); const displayFilters = issuesFilterStore.issueFilters?.displayFilters; - const issues = issueStore.getIssues; - const groupedIssueIds = (issueStore.getIssuesIds ?? {}) as IGroupedIssues; + const groupedIssueIds = (issueStore.groupedIssueIds ?? {}) as TGroupedIssues; const onDragEnd = async (result: DropResult) => { if (!result) return; @@ -64,7 +54,15 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => { if (result.destination.droppableId === result.source.droppableId) return; if (handleDragDrop) { - await handleDragDrop(result.source, result.destination, issues, groupedIssueIds).catch((err) => { + await handleDragDrop( + result.source, + result.destination, + workspaceSlug?.toString(), + projectId?.toString(), + issueStore, + issueMap, + groupedIssueIds + ).catch((err) => { setToastAlert({ title: "Error", type: "error", @@ -75,7 +73,7 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => { }; const handleIssues = useCallback( - async (date: string, issue: IIssue, action: EIssueActions) => { + async (date: string, issue: TIssue, action: EIssueActions) => { if (issueActions[action]) { await issueActions[action]!(issue); } @@ -89,7 +87,7 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => { { workspaceSlug={workspaceSlug.toString()} projectId={peekProjectId.toString()} issueId={peekIssueId.toString()} - handleIssue={async (issueToUpdate, action: EIssueActions) => - await handleIssues(issueToUpdate.target_date ?? "", issueToUpdate as IIssue, action) + handleIssue={async (issueToUpdate) => + await handleIssues(issueToUpdate.target_date ?? "", issueToUpdate as TIssue, EIssueActions.UPDATE) } /> )} diff --git a/web/components/issues/issue-layouts/calendar/calendar.tsx b/web/components/issues/issue-layouts/calendar/calendar.tsx index a2626b0237e..1652aa89bf1 100644 --- a/web/components/issues/issue-layouts/calendar/calendar.tsx +++ b/web/components/issues/issue-layouts/calendar/calendar.tsx @@ -1,60 +1,56 @@ import { observer } from "mobx-react-lite"; - -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +// hooks +import { useIssues, useUser } from "hooks/store"; // components import { CalendarHeader, CalendarWeekDays, CalendarWeekHeader } from "components/issues"; // ui import { Spinner } from "@plane/ui"; // types import { ICalendarWeek } from "./types"; -import { IIssue } from "types"; -import { IGroupedIssues, IIssueResponse } from "store/issues/types"; -import { - ICycleIssuesFilterStore, - IModuleIssuesFilterStore, - IProjectIssuesFilterStore, - IViewIssuesFilterStore, -} from "store/issues"; +import { TGroupedIssues, TIssue, TIssueMap } from "@plane/types"; // constants -import { EUserWorkspaceRoles } from "constants/workspace"; +import { EUserProjectRoles } from "constants/project"; +import { useCalendarView } from "hooks/store/use-calendar-view"; +import { EIssuesStoreType } from "constants/issue"; +import { ICycleIssuesFilter } from "store/issue/cycle"; +import { IModuleIssuesFilter } from "store/issue/module"; +import { IProjectIssuesFilter } from "store/issue/project"; +import { IProjectViewIssuesFilter } from "store/issue/project-views"; type Props = { - issuesFilterStore: - | IProjectIssuesFilterStore - | IModuleIssuesFilterStore - | ICycleIssuesFilterStore - | IViewIssuesFilterStore; - issues: IIssueResponse | undefined; - groupedIssueIds: IGroupedIssues; + issuesFilterStore: IProjectIssuesFilter | IModuleIssuesFilter | ICycleIssuesFilter | IProjectViewIssuesFilter; + issues: TIssueMap | undefined; + groupedIssueIds: TGroupedIssues; layout: "month" | "week" | undefined; showWeekends: boolean; - quickActions: (issue: IIssue, customActionButton?: React.ReactElement) => React.ReactNode; + quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode; quickAddCallback?: ( workspaceSlug: string, projectId: string, - data: IIssue, + data: TIssue, viewId?: string - ) => Promise; + ) => Promise; viewId?: string; }; export const CalendarChart: React.FC = observer((props) => { const { issuesFilterStore, issues, groupedIssueIds, layout, showWeekends, quickActions, quickAddCallback, viewId } = props; - + // store hooks + const { + issues: { viewFlags }, + } = useIssues(EIssuesStoreType.PROJECT); + const issueCalendarView = useCalendarView(); const { - calendar: calendarStore, - projectIssues: issueStore, - user: { currentProjectRole }, - } = useMobxStore(); + membership: { currentProjectRole }, + } = useUser(); - const { enableIssueCreation } = issueStore?.viewFlags || {}; - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER; + const { enableIssueCreation } = viewFlags || {}; + const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; - const calendarPayload = calendarStore.calendarPayload; + const calendarPayload = issueCalendarView.calendarPayload; - const allWeeksOfActiveMonth = calendarStore.allWeeksOfActiveMonth; + const allWeeksOfActiveMonth = issueCalendarView.allWeeksOfActiveMonth; if (!calendarPayload) return ( @@ -66,7 +62,7 @@ export const CalendarChart: React.FC = observer((props) => { return ( <>
- +
{layout === "month" && ( @@ -91,7 +87,7 @@ export const CalendarChart: React.FC = observer((props) => { {layout === "week" && ( React.ReactNode; + issues: TIssueMap | undefined; + groupedIssueIds: TGroupedIssues; + quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode; enableQuickIssueCreate?: boolean; disableIssueCreation?: boolean; quickAddCallback?: ( workspaceSlug: string, projectId: string, - data: IIssue, + data: TIssue, viewId?: string - ) => Promise; + ) => Promise; viewId?: string; }; diff --git a/web/components/issues/issue-layouts/calendar/dropdowns/months-dropdown.tsx b/web/components/issues/issue-layouts/calendar/dropdowns/months-dropdown.tsx index ae2b55a5583..2443ae17bf6 100644 --- a/web/components/issues/issue-layouts/calendar/dropdowns/months-dropdown.tsx +++ b/web/components/issues/issue-layouts/calendar/dropdowns/months-dropdown.tsx @@ -2,17 +2,26 @@ import React, { useState } from "react"; import { Popover, Transition } from "@headlessui/react"; import { observer } from "mobx-react-lite"; import { usePopper } from "react-popper"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +//hooks +import { useCalendarView } from "hooks/store"; // icons import { ChevronLeft, ChevronRight } from "lucide-react"; // constants import { MONTHS_LIST } from "constants/calendar"; +import { ICycleIssuesFilter } from "store/issue/cycle"; +import { IModuleIssuesFilter } from "store/issue/module"; +import { IProjectIssuesFilter } from "store/issue/project"; +import { IProjectViewIssuesFilter } from "store/issue/project-views"; -export const CalendarMonthsDropdown: React.FC = observer(() => { - const { calendar: calendarStore, issueFilter: issueFilterStore } = useMobxStore(); +interface Props { + issuesFilterStore: IProjectIssuesFilter | IModuleIssuesFilter | ICycleIssuesFilter | IProjectViewIssuesFilter; +} +export const CalendarMonthsDropdown: React.FC = observer((props: Props) => { + const { issuesFilterStore } = props; - const calendarLayout = issueFilterStore.userDisplayFilters.calendar?.layout ?? "month"; + const issueCalendarView = useCalendarView(); + + const calendarLayout = issuesFilterStore.issueFilters?.displayFilters?.calendar?.layout ?? "month"; const [referenceElement, setReferenceElement] = useState(null); const [popperElement, setPopperElement] = useState(null); @@ -29,10 +38,10 @@ export const CalendarMonthsDropdown: React.FC = observer(() => { ], }); - const { activeMonthDate } = calendarStore.calendarFilters; + const { activeMonthDate } = issueCalendarView.calendarFilters; const getWeekLayoutHeader = (): string => { - const allDaysOfActiveWeek = calendarStore.allDaysOfActiveWeek; + const allDaysOfActiveWeek = issueCalendarView.allDaysOfActiveWeek; if (!allDaysOfActiveWeek) return "Week view"; @@ -55,7 +64,7 @@ export const CalendarMonthsDropdown: React.FC = observer(() => { }; const handleDateChange = (date: Date) => { - calendarStore.updateCalendarFilters({ + issueCalendarView.updateCalendarFilters({ activeMonthDate: date, }); }; diff --git a/web/components/issues/issue-layouts/calendar/dropdowns/options-dropdown.tsx b/web/components/issues/issue-layouts/calendar/dropdowns/options-dropdown.tsx index c1778b33466..0abe8580de7 100644 --- a/web/components/issues/issue-layouts/calendar/dropdowns/options-dropdown.tsx +++ b/web/components/issues/issue-layouts/calendar/dropdowns/options-dropdown.tsx @@ -3,39 +3,34 @@ import { useRouter } from "next/router"; import { Popover, Transition } from "@headlessui/react"; import { observer } from "mobx-react-lite"; import { usePopper } from "react-popper"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +// hooks +import { useCalendarView } from "hooks/store"; // ui import { ToggleSwitch } from "@plane/ui"; // icons import { Check, ChevronUp } from "lucide-react"; // types -import { TCalendarLayouts } from "types"; +import { TCalendarLayouts } from "@plane/types"; // constants import { CALENDAR_LAYOUTS } from "constants/calendar"; -import { EFilterType } from "store/issues/types"; -import { - ICycleIssuesFilterStore, - IModuleIssuesFilterStore, - IProjectIssuesFilterStore, - IViewIssuesFilterStore, -} from "store/issues"; +import { EIssueFilterType } from "constants/issue"; +import { ICycleIssuesFilter } from "store/issue/cycle"; +import { IModuleIssuesFilter } from "store/issue/module"; +import { IProjectIssuesFilter } from "store/issue/project"; +import { IProjectViewIssuesFilter } from "store/issue/project-views"; interface ICalendarHeader { - issuesFilterStore: - | IProjectIssuesFilterStore - | IModuleIssuesFilterStore - | ICycleIssuesFilterStore - | IViewIssuesFilterStore; + issuesFilterStore: IProjectIssuesFilter | IModuleIssuesFilter | ICycleIssuesFilter | IProjectViewIssuesFilter; + viewId?: string; } export const CalendarOptionsDropdown: React.FC = observer((props) => { - const { issuesFilterStore } = props; + const { issuesFilterStore, viewId } = props; const router = useRouter(); const { workspaceSlug, projectId } = router.query; - const { calendar: calendarStore } = useMobxStore(); + const issueCalendarView = useCalendarView(); const [referenceElement, setReferenceElement] = useState(null); const [popperElement, setPopperElement] = useState(null); @@ -58,15 +53,17 @@ export const CalendarOptionsDropdown: React.FC = observer((prop const handleLayoutChange = (layout: TCalendarLayouts) => { if (!workspaceSlug || !projectId) return; - issuesFilterStore.updateFilters(workspaceSlug.toString(), projectId.toString(), EFilterType.DISPLAY_FILTERS, { + issuesFilterStore.updateFilters(workspaceSlug.toString(), projectId.toString(), EIssueFilterType.DISPLAY_FILTERS, { calendar: { ...issuesFilterStore.issueFilters?.displayFilters?.calendar, layout, }, }); - calendarStore.updateCalendarPayload( - layout === "month" ? calendarStore.calendarFilters.activeMonthDate : calendarStore.calendarFilters.activeWeekDate + issueCalendarView.updateCalendarPayload( + layout === "month" + ? issueCalendarView.calendarFilters.activeMonthDate + : issueCalendarView.calendarFilters.activeWeekDate ); }; @@ -75,12 +72,18 @@ export const CalendarOptionsDropdown: React.FC = observer((prop if (!workspaceSlug || !projectId) return; - issuesFilterStore.updateFilters(workspaceSlug.toString(), projectId.toString(), EFilterType.DISPLAY_FILTERS, { - calendar: { - ...issuesFilterStore.issueFilters?.displayFilters?.calendar, - show_weekends: !showWeekends, + issuesFilterStore.updateFilters( + workspaceSlug.toString(), + projectId.toString(), + EIssueFilterType.DISPLAY_FILTERS, + { + calendar: { + ...issuesFilterStore.issueFilters?.displayFilters?.calendar, + show_weekends: !showWeekends, + }, }, - }); + viewId + ); }; return ( diff --git a/web/components/issues/issue-layouts/calendar/header.tsx b/web/components/issues/issue-layouts/calendar/header.tsx index 1a2280d05dd..ebbb510fc68 100644 --- a/web/components/issues/issue-layouts/calendar/header.tsx +++ b/web/components/issues/issue-layouts/calendar/header.tsx @@ -1,34 +1,28 @@ import { observer } from "mobx-react-lite"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; // components import { CalendarMonthsDropdown, CalendarOptionsDropdown } from "components/issues"; // icons import { ChevronLeft, ChevronRight } from "lucide-react"; -import { - ICycleIssuesFilterStore, - IModuleIssuesFilterStore, - IProjectIssuesFilterStore, - IViewIssuesFilterStore, -} from "store/issues"; +import { useCalendarView } from "hooks/store/use-calendar-view"; +import { ICycleIssuesFilter } from "store/issue/cycle"; +import { IModuleIssuesFilter } from "store/issue/module"; +import { IProjectIssuesFilter } from "store/issue/project"; +import { IProjectViewIssuesFilter } from "store/issue/project-views"; interface ICalendarHeader { - issuesFilterStore: - | IProjectIssuesFilterStore - | IModuleIssuesFilterStore - | ICycleIssuesFilterStore - | IViewIssuesFilterStore; + issuesFilterStore: IProjectIssuesFilter | IModuleIssuesFilter | ICycleIssuesFilter | IProjectViewIssuesFilter; + viewId?: string; } export const CalendarHeader: React.FC = observer((props) => { - const { issuesFilterStore } = props; + const { issuesFilterStore, viewId } = props; - const { calendar: calendarStore } = useMobxStore(); + const issueCalendarView = useCalendarView(); const calendarLayout = issuesFilterStore.issueFilters?.displayFilters?.calendar?.layout ?? "month"; - const { activeMonthDate, activeWeekDate } = calendarStore.calendarFilters; + const { activeMonthDate, activeWeekDate } = issueCalendarView.calendarFilters; const handlePrevious = () => { if (calendarLayout === "month") { @@ -38,7 +32,7 @@ export const CalendarHeader: React.FC = observer((props) => { const previousMonthFirstDate = new Date(previousMonthYear, previousMonthMonth, 1); - calendarStore.updateCalendarFilters({ + issueCalendarView.updateCalendarFilters({ activeMonthDate: previousMonthFirstDate, }); } else { @@ -48,7 +42,7 @@ export const CalendarHeader: React.FC = observer((props) => { activeWeekDate.getDate() - 7 ); - calendarStore.updateCalendarFilters({ + issueCalendarView.updateCalendarFilters({ activeWeekDate: previousWeekDate, }); } @@ -62,7 +56,7 @@ export const CalendarHeader: React.FC = observer((props) => { const nextMonthFirstDate = new Date(nextMonthYear, nextMonthMonth, 1); - calendarStore.updateCalendarFilters({ + issueCalendarView.updateCalendarFilters({ activeMonthDate: nextMonthFirstDate, }); } else { @@ -72,7 +66,7 @@ export const CalendarHeader: React.FC = observer((props) => { activeWeekDate.getDate() + 7 ); - calendarStore.updateCalendarFilters({ + issueCalendarView.updateCalendarFilters({ activeWeekDate: nextWeekDate, }); } @@ -82,7 +76,7 @@ export const CalendarHeader: React.FC = observer((props) => { const today = new Date(); const firstDayOfCurrentMonth = new Date(today.getFullYear(), today.getMonth(), 1); - calendarStore.updateCalendarFilters({ + issueCalendarView.updateCalendarFilters({ activeMonthDate: firstDayOfCurrentMonth, activeWeekDate: today, }); @@ -97,7 +91,7 @@ export const CalendarHeader: React.FC = observer((props) => { - +
- +
); diff --git a/web/components/issues/issue-layouts/calendar/issue-blocks.tsx b/web/components/issues/issue-layouts/calendar/issue-blocks.tsx index f8eead33fbc..be30560fb11 100644 --- a/web/components/issues/issue-layouts/calendar/issue-blocks.tsx +++ b/web/components/issues/issue-layouts/calendar/issue-blocks.tsx @@ -8,16 +8,13 @@ import { Tooltip } from "@plane/ui"; // hooks import useOutsideClickDetector from "hooks/use-outside-click-detector"; // types -import { IIssue } from "types"; -import { IIssueResponse } from "store/issues/types"; -import { useMobxStore } from "lib/mobx/store-provider"; -// constants -import { EUserWorkspaceRoles } from "constants/workspace"; +import { TIssue, TIssueMap } from "@plane/types"; +import { useProject, useProjectState } from "hooks/store"; type Props = { - issues: IIssueResponse | undefined; + issues: TIssueMap | undefined; issueIdList: string[] | null; - quickActions: (issue: IIssue, customActionButton?: React.ReactElement) => React.ReactNode; + quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode; showAllIssues?: boolean; }; @@ -25,28 +22,21 @@ export const CalendarIssueBlocks: React.FC = observer((props) => { const { issues, issueIdList, quickActions, showAllIssues = false } = props; // router const router = useRouter(); - + // hooks + const { getProjectById } = useProject(); + const { getProjectStates } = useProjectState(); // states const [isMenuActive, setIsMenuActive] = useState(false); - // mobx store - const { - user: { currentProjectRole }, - } = useMobxStore(); - const menuActionRef = useRef(null); - const handleIssuePeekOverview = (issue: IIssue, event: React.MouseEvent) => { + const handleIssuePeekOverview = (issue: TIssue) => { const { query } = router; - if (event.ctrlKey || event.metaKey) { - const issueUrl = `/${issue.workspace_detail.slug}/projects/${issue.project_detail.id}/issues/${issue?.id}`; - window.open(issueUrl, "_blank"); // Open link in a new tab - } else { - router.push({ - pathname: router.pathname, - query: { ...query, peekIssueId: issue?.id, peekProjectId: issue?.project }, - }); - } + + router.push({ + pathname: router.pathname, + query: { ...query, peekIssueId: issue?.id, peekProjectId: issue?.project_id }, + }); }; useOutsideClickDetector(menuActionRef, () => setIsMenuActive(false)); @@ -63,8 +53,6 @@ export const CalendarIssueBlocks: React.FC = observer((props) => {
); - const isEditable = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER; - return ( <> {issueIdList?.slice(0, showAllIssues ? issueIdList.length : 4).map((issueId, index) => { @@ -72,14 +60,14 @@ export const CalendarIssueBlocks: React.FC = observer((props) => { const issue = issues?.[issueId]; return ( - + {(provided, snapshot) => (
handleIssuePeekOverview(issue, e)} + onClick={() => handleIssuePeekOverview(issue)} > {issue?.tempId !== undefined && (
@@ -96,11 +84,13 @@ export const CalendarIssueBlocks: React.FC = observer((props) => { state?.id == issue?.state_id + )?.color, }} />
- {issue.project_detail.identifier}-{issue.sequence_id} + {getProjectById(issue?.project_id)?.identifier}-{issue.sequence_id}
{issue.name}
diff --git a/web/components/issues/issue-layouts/calendar/quick-add-issue-form.tsx b/web/components/issues/issue-layouts/calendar/quick-add-issue-form.tsx index 85a74a997e5..7a3c0141774 100644 --- a/web/components/issues/issue-layouts/calendar/quick-add-issue-form.tsx +++ b/web/components/issues/issue-layouts/calendar/quick-add-issue-form.tsx @@ -2,9 +2,8 @@ import { useEffect, useRef, useState } from "react"; import { useRouter } from "next/router"; import { useForm } from "react-hook-form"; import { observer } from "mobx-react-lite"; -// store -import { useMobxStore } from "lib/mobx/store-provider"; // hooks +import { useProject, useWorkspace } from "hooks/store"; import useToast from "hooks/use-toast"; import useKeypress from "hooks/use-keypress"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; @@ -13,24 +12,24 @@ import { createIssuePayload } from "helpers/issue.helper"; // icons import { PlusIcon } from "lucide-react"; // types -import { IIssue, IProject } from "types"; +import { TIssue } from "@plane/types"; type Props = { - formKey: keyof IIssue; + formKey: keyof TIssue; groupId?: string; subGroupId?: string | null; - prePopulatedData?: Partial; + prePopulatedData?: Partial; quickAddCallback?: ( workspaceSlug: string, projectId: string, - data: IIssue, + data: TIssue, viewId?: string - ) => Promise; + ) => Promise; viewId?: string; onOpen?: () => void; }; -const defaultValues: Partial = { +const defaultValues: Partial = { name: "", }; @@ -62,22 +61,20 @@ export const CalendarQuickAddIssueForm: React.FC = observer((props) => { // router const router = useRouter(); - const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string }; - - const { workspace: workspaceStore, project: projectStore } = useMobxStore(); - - // ref + const { workspaceSlug, projectId } = router.query; + // store hooks + const { getProjectById } = useProject(); + const { getWorkspaceBySlug } = useWorkspace(); + // refs const ref = useRef(null); - // states const [isOpen, setIsOpen] = useState(false); - + // toast alert const { setToastAlert } = useToast(); // derived values - const workspaceDetail = (workspaceSlug && workspaceStore.getWorkspaceBySlug(workspaceSlug)) || null; - const projectDetail: IProject | null = - (workspaceSlug && projectId && projectStore.getProjectById(workspaceSlug, projectId)) || null; + const workspaceDetail = (workspaceSlug && getWorkspaceBySlug(workspaceSlug.toString())) || null; + const projectDetail = projectId ? getProjectById(projectId.toString()) : null; const { reset, @@ -85,7 +82,7 @@ export const CalendarQuickAddIssueForm: React.FC = observer((props) => { register, setFocus, formState: { errors, isSubmitting }, - } = useForm({ defaultValues }); + } = useForm({ defaultValues }); const handleClose = () => { setIsOpen(false); @@ -102,7 +99,7 @@ export const CalendarQuickAddIssueForm: React.FC = observer((props) => { if (!errors) return; Object.keys(errors).forEach((key) => { - const error = errors[key as keyof IIssue]; + const error = errors[key as keyof TIssue]; setToastAlert({ type: "error", @@ -112,8 +109,8 @@ export const CalendarQuickAddIssueForm: React.FC = observer((props) => { }); }, [errors, setToastAlert]); - const onSubmitHandler = async (formData: IIssue) => { - if (isSubmitting || !groupId || !workspaceDetail || !projectDetail) return; + const onSubmitHandler = async (formData: TIssue) => { + if (isSubmitting || !groupId || !workspaceDetail || !projectDetail || !workspaceSlug || !projectId) return; reset({ ...defaultValues }); @@ -125,8 +122,8 @@ export const CalendarQuickAddIssueForm: React.FC = observer((props) => { try { quickAddCallback && (await quickAddCallback( - workspaceSlug, - projectId, + workspaceSlug.toString(), + projectId.toString(), { ...payload, }, diff --git a/web/components/issues/issue-layouts/calendar/roots/cycle-root.tsx b/web/components/issues/issue-layouts/calendar/roots/cycle-root.tsx index 88025ad6859..585b1a5e127 100644 --- a/web/components/issues/issue-layouts/calendar/roots/cycle-root.tsx +++ b/web/components/issues/issue-layouts/calendar/roots/cycle-root.tsx @@ -1,74 +1,50 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +//hooks +import { useIssues } from "hooks/store"; // components import { CycleIssueQuickActions } from "components/issues"; // types -import { IIssue } from "types"; +import { TIssue } from "@plane/types"; import { EIssueActions } from "../../types"; import { BaseCalendarRoot } from "../base-calendar-root"; +import { EIssuesStoreType } from "constants/issue"; +import { useMemo } from "react"; export const CycleCalendarLayout: React.FC = observer(() => { - const { - cycleIssues: cycleIssueStore, - cycleIssuesFilter: cycleIssueFilterStore, - calendarHelpers: { handleDragDrop: handleCalenderDragDrop }, - cycle: { fetchCycleWithId }, - } = useMobxStore(); + const { issues, issuesFilter } = useIssues(EIssuesStoreType.CYCLE); const router = useRouter(); const { workspaceSlug, projectId, cycleId } = router.query; - const issueActions = { - [EIssueActions.UPDATE]: async (issue: IIssue) => { - if (!workspaceSlug || !cycleId) return; + const issueActions = useMemo( + () => ({ + [EIssueActions.UPDATE]: async (issue: TIssue) => { + if (!workspaceSlug || !cycleId) return; - await cycleIssueStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue, cycleId.toString()); - fetchCycleWithId(workspaceSlug.toString(), issue.project, cycleId.toString()); - }, - [EIssueActions.DELETE]: async (issue: IIssue) => { - if (!workspaceSlug || !cycleId) return; - await cycleIssueStore.removeIssue(workspaceSlug.toString(), issue.project, issue.id, cycleId.toString()); - fetchCycleWithId(workspaceSlug.toString(), issue.project, cycleId.toString()); - }, - [EIssueActions.REMOVE]: async (issue: IIssue) => { - if (!workspaceSlug || !cycleId || !projectId || !issue.bridge_id) return; - await cycleIssueStore.removeIssueFromCycle( - workspaceSlug.toString(), - issue.project, - cycleId.toString(), - issue.id, - issue.bridge_id - ); - fetchCycleWithId(workspaceSlug.toString(), issue.project, cycleId.toString()); - }, - }; - - const handleDragDrop = async (source: any, destination: any, issues: IIssue[], issueWithIds: any) => { - if (workspaceSlug && projectId && cycleId) - await handleCalenderDragDrop( - source, - destination, - workspaceSlug.toString(), - projectId.toString(), - cycleIssueStore, - issues, - issueWithIds, - cycleId.toString() - ); - }; + await issues.updateIssue(workspaceSlug.toString(), issue.project_id, issue.id, issue, cycleId.toString()); + }, + [EIssueActions.DELETE]: async (issue: TIssue) => { + if (!workspaceSlug || !cycleId) return; + await issues.removeIssue(workspaceSlug.toString(), issue.project_id, issue.id, cycleId.toString()); + }, + [EIssueActions.REMOVE]: async (issue: TIssue) => { + if (!workspaceSlug || !cycleId || !projectId) return; + await issues.removeIssueFromCycle(workspaceSlug.toString(), issue.project_id, cycleId.toString(), issue.id); + }, + }), + [issues, workspaceSlug, cycleId, projectId] + ); if (!cycleId) return null; return ( ); }); diff --git a/web/components/issues/issue-layouts/calendar/roots/module-root.tsx b/web/components/issues/issue-layouts/calendar/roots/module-root.tsx index 4a7cfbd3f1d..d2b23e17614 100644 --- a/web/components/issues/issue-layouts/calendar/roots/module-root.tsx +++ b/web/components/issues/issue-layouts/calendar/roots/module-root.tsx @@ -1,68 +1,50 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +// hoks +import { useIssues } from "hooks/store"; // components import { ModuleIssueQuickActions } from "components/issues"; // types -import { IIssue } from "types"; +import { TIssue } from "@plane/types"; import { EIssueActions } from "../../types"; import { BaseCalendarRoot } from "../base-calendar-root"; +import { EIssuesStoreType } from "constants/issue"; +import { useMemo } from "react"; export const ModuleCalendarLayout: React.FC = observer(() => { - const { - moduleIssues: moduleIssueStore, - moduleIssuesFilter: moduleIssueFilterStore, - calendarHelpers: { handleDragDrop: handleCalenderDragDrop }, - module: { fetchModuleDetails }, - } = useMobxStore(); - + const { issues, issuesFilter } = useIssues(EIssuesStoreType.MODULE); const router = useRouter(); - const { workspaceSlug, projectId, moduleId } = router.query as { + const { workspaceSlug, moduleId } = router.query as { workspaceSlug: string; projectId: string; moduleId: string; }; - const issueActions = { - [EIssueActions.UPDATE]: async (issue: IIssue) => { - if (!workspaceSlug || !moduleId) return; - await moduleIssueStore.updateIssue(workspaceSlug, issue.project, issue.id, issue, moduleId); - fetchModuleDetails(workspaceSlug, issue.project, moduleId); - }, - [EIssueActions.DELETE]: async (issue: IIssue) => { - if (!workspaceSlug || !moduleId) return; - await moduleIssueStore.removeIssue(workspaceSlug, issue.project, issue.id, moduleId); - fetchModuleDetails(workspaceSlug, issue.project, moduleId); - }, - [EIssueActions.REMOVE]: async (issue: IIssue) => { - if (!workspaceSlug || !moduleId || !issue.bridge_id) return; - await moduleIssueStore.removeIssueFromModule(workspaceSlug, issue.project, moduleId, issue.id, issue.bridge_id); - fetchModuleDetails(workspaceSlug, issue.project, moduleId); - }, - }; - - const handleDragDrop = async (source: any, destination: any, issues: IIssue[], issueWithIds: any) => { - await handleCalenderDragDrop( - source, - destination, - workspaceSlug, - projectId, - moduleIssueStore, - issues, - issueWithIds, - moduleId - ); - }; + const issueActions = useMemo( + () => ({ + [EIssueActions.UPDATE]: async (issue: TIssue) => { + if (!workspaceSlug || !moduleId) return; + await issues.updateIssue(workspaceSlug, issue.project_id, issue.id, issue, moduleId); + }, + [EIssueActions.DELETE]: async (issue: TIssue) => { + if (!workspaceSlug || !moduleId) return; + await issues.removeIssue(workspaceSlug, issue.project_id, issue.id, moduleId); + }, + [EIssueActions.REMOVE]: async (issue: TIssue) => { + if (!workspaceSlug || !moduleId) return; + await issues.removeIssueFromModule(workspaceSlug, issue.project_id, moduleId, issue.id); + }, + }), + [issues, workspaceSlug, moduleId] + ); return ( ); }); diff --git a/web/components/issues/issue-layouts/calendar/roots/project-root.tsx b/web/components/issues/issue-layouts/calendar/roots/project-root.tsx index e71cc7e3b0c..40f72e7b871 100644 --- a/web/components/issues/issue-layouts/calendar/roots/project-root.tsx +++ b/web/components/issues/issue-layouts/calendar/roots/project-root.tsx @@ -1,56 +1,43 @@ import { observer } from "mobx-react-lite"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +import { useRouter } from "next/router"; +// hooks +import { useIssues } from "hooks/store"; // components import { ProjectIssueQuickActions } from "components/issues"; import { BaseCalendarRoot } from "../base-calendar-root"; import { EIssueActions } from "../../types"; -import { IIssue } from "types"; -import { useRouter } from "next/router"; +import { TIssue } from "@plane/types"; +import { EIssuesStoreType } from "constants/issue"; +import { useMemo } from "react"; export const CalendarLayout: React.FC = observer(() => { const router = useRouter(); - const { workspaceSlug, projectId } = router.query; + const { workspaceSlug } = router.query; - const { - projectIssues: issueStore, - projectIssuesFilter: projectIssueFiltersStore, - calendarHelpers: { handleDragDrop: handleCalenderDragDrop }, - } = useMobxStore(); + const { issues, issuesFilter } = useIssues(EIssuesStoreType.PROJECT); - const issueActions = { - [EIssueActions.UPDATE]: async (issue: IIssue) => { - if (!workspaceSlug) return; + const issueActions = useMemo( + () => ({ + [EIssueActions.UPDATE]: async (issue: TIssue) => { + if (!workspaceSlug) return; - await issueStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue); - }, - [EIssueActions.DELETE]: async (issue: IIssue) => { - if (!workspaceSlug) return; + await issues.updateIssue(workspaceSlug.toString(), issue.project_id, issue.id, issue); + }, + [EIssueActions.DELETE]: async (issue: TIssue) => { + if (!workspaceSlug) return; - await issueStore.removeIssue(workspaceSlug.toString(), issue.project, issue.id); - }, - }; - - const handleDragDrop = async (source: any, destination: any, issues: IIssue[], issueWithIds: any) => { - if (workspaceSlug && projectId) - await handleCalenderDragDrop( - source, - destination, - workspaceSlug.toString(), - projectId.toString(), - issueStore, - issues, - issueWithIds - ); - }; + await issues.removeIssue(workspaceSlug.toString(), issue.project_id, issue.id); + }, + }), + [issues, workspaceSlug] + ); return ( ); }); diff --git a/web/components/issues/issue-layouts/calendar/roots/project-view-root.tsx b/web/components/issues/issue-layouts/calendar/roots/project-view-root.tsx index 95d746eeca5..43e59dc7684 100644 --- a/web/components/issues/issue-layouts/calendar/roots/project-view-root.tsx +++ b/web/components/issues/issue-layouts/calendar/roots/project-view-root.tsx @@ -1,57 +1,44 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +// hooks +import { useIssues } from "hooks/store"; // components import { ProjectIssueQuickActions } from "components/issues"; // types -import { IIssue } from "types"; +import { TIssue } from "@plane/types"; import { EIssueActions } from "../../types"; import { BaseCalendarRoot } from "../base-calendar-root"; +import { EIssuesStoreType } from "constants/issue"; +import { useMemo } from "react"; export const ProjectViewCalendarLayout: React.FC = observer(() => { - const { - viewIssues: projectViewIssuesStore, - viewIssuesFilter: projectIssueViewFiltersStore, - calendarHelpers: { handleDragDrop: handleCalenderDragDrop }, - } = useMobxStore(); - + const { issues, issuesFilter } = useIssues(EIssuesStoreType.PROJECT_VIEW); const router = useRouter(); - const { workspaceSlug, projectId } = router.query; - - const issueActions = { - [EIssueActions.UPDATE]: async (issue: IIssue) => { - if (!workspaceSlug) return; + const { workspaceSlug, projectId, viewId } = router.query; - await projectViewIssuesStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue); - }, - [EIssueActions.DELETE]: async (issue: IIssue) => { - if (!workspaceSlug) return; + const issueActions = useMemo( + () => ({ + [EIssueActions.UPDATE]: async (issue: TIssue) => { + if (!workspaceSlug || !projectId) return; - await projectViewIssuesStore.removeIssue(workspaceSlug.toString(), issue.project, issue.id); - }, - }; + await issues.updateIssue(workspaceSlug.toString(), projectId.toString(), issue.id, issue); + }, + [EIssueActions.DELETE]: async (issue: TIssue) => { + if (!workspaceSlug || !projectId) return; - const handleDragDrop = async (source: any, destination: any, issues: IIssue[], issueWithIds: any) => { - if (workspaceSlug && projectId) - await handleCalenderDragDrop( - source, - destination, - workspaceSlug.toString(), - projectId.toString(), - projectViewIssuesStore, - issues, - issueWithIds - ); - }; + await issues.removeIssue(workspaceSlug.toString(), projectId.toString(), issue.id); + }, + }), + [issues, workspaceSlug, projectId] + ); return ( ); }); diff --git a/web/components/issues/issue-layouts/calendar/utils.ts b/web/components/issues/issue-layouts/calendar/utils.ts new file mode 100644 index 00000000000..82d9ce0ce5f --- /dev/null +++ b/web/components/issues/issue-layouts/calendar/utils.ts @@ -0,0 +1,42 @@ +import { DraggableLocation } from "@hello-pangea/dnd"; +import { ICycleIssues } from "store/issue/cycle"; +import { IModuleIssues } from "store/issue/module"; +import { IProjectIssues } from "store/issue/project"; +import { IProjectViewIssues } from "store/issue/project-views"; +import { TGroupedIssues, IIssueMap } from "@plane/types"; + +export const handleDragDrop = async ( + source: DraggableLocation, + destination: DraggableLocation, + workspaceSlug: string | undefined, + projectId: string | undefined, + store: IProjectIssues | IModuleIssues | ICycleIssues | IProjectViewIssues, + issueMap: IIssueMap, + issueWithIds: TGroupedIssues, + viewId: string | null = null // it can be moduleId, cycleId +) => { + if (!issueMap || !issueWithIds || !workspaceSlug || !projectId) return; + + const sourceColumnId = source?.droppableId || null; + const destinationColumnId = destination?.droppableId || null; + + if (!workspaceSlug || !projectId || !sourceColumnId || !destinationColumnId) return; + + if (sourceColumnId === destinationColumnId) return; + + // horizontal + if (sourceColumnId != destinationColumnId) { + const sourceIssues = issueWithIds[sourceColumnId] || []; + + const [removed] = sourceIssues.splice(source.index, 1); + const removedIssueDetail = issueMap[removed]; + + const updateIssue = { + id: removedIssueDetail?.id, + target_date: destinationColumnId, + }; + + if (viewId) return await store?.updateIssue(workspaceSlug, projectId, updateIssue.id, updateIssue, viewId); + else return await store?.updateIssue(workspaceSlug, projectId, updateIssue.id, updateIssue); + } +}; diff --git a/web/components/issues/issue-layouts/calendar/week-days.tsx b/web/components/issues/issue-layouts/calendar/week-days.tsx index 3160dcfbabf..c34aaef972c 100644 --- a/web/components/issues/issue-layouts/calendar/week-days.tsx +++ b/web/components/issues/issue-layouts/calendar/week-days.tsx @@ -5,33 +5,26 @@ import { CalendarDayTile } from "components/issues"; import { renderFormattedPayloadDate } from "helpers/date-time.helper"; // types import { ICalendarDate, ICalendarWeek } from "./types"; -import { IIssue } from "types"; -import { IGroupedIssues, IIssueResponse } from "store/issues/types"; -import { - ICycleIssuesFilterStore, - IModuleIssuesFilterStore, - IProjectIssuesFilterStore, - IViewIssuesFilterStore, -} from "store/issues"; +import { TGroupedIssues, TIssue, TIssueMap } from "@plane/types"; +import { ICycleIssuesFilter } from "store/issue/cycle"; +import { IModuleIssuesFilter } from "store/issue/module"; +import { IProjectIssuesFilter } from "store/issue/project"; +import { IProjectViewIssuesFilter } from "store/issue/project-views"; type Props = { - issuesFilterStore: - | IProjectIssuesFilterStore - | IModuleIssuesFilterStore - | ICycleIssuesFilterStore - | IViewIssuesFilterStore; - issues: IIssueResponse | undefined; - groupedIssueIds: IGroupedIssues; + issuesFilterStore: IProjectIssuesFilter | IModuleIssuesFilter | ICycleIssuesFilter | IProjectViewIssuesFilter; + issues: TIssueMap | undefined; + groupedIssueIds: TGroupedIssues; week: ICalendarWeek | undefined; - quickActions: (issue: IIssue, customActionButton?: React.ReactElement) => React.ReactNode; + quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode; enableQuickIssueCreate?: boolean; disableIssueCreation?: boolean; quickAddCallback?: ( workspaceSlug: string, projectId: string, - data: IIssue, + data: TIssue, viewId?: string - ) => Promise; + ) => Promise; viewId?: string; }; diff --git a/web/components/issues/issue-layouts/empty-states/cycle.tsx b/web/components/issues/issue-layouts/empty-states/cycle.tsx index 52baa2eb87e..1c3ba1628ac 100644 --- a/web/components/issues/issue-layouts/empty-states/cycle.tsx +++ b/web/components/issues/issue-layouts/empty-states/cycle.tsx @@ -1,9 +1,8 @@ import { useState } from "react"; import { observer } from "mobx-react-lite"; import { PlusIcon } from "lucide-react"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; // hooks +import { useApplication, useIssues, useUser } from "hooks/store"; import useToast from "hooks/use-toast"; // components import { EmptyState } from "components/common"; @@ -13,10 +12,10 @@ import { Button } from "@plane/ui"; // assets import emptyIssue from "public/empty-state/issue.svg"; // types -import { ISearchIssueResponse } from "types"; -import { EProjectStore } from "store/command-palette.store"; +import { ISearchIssueResponse } from "@plane/types"; // constants -import { EUserWorkspaceRoles } from "constants/workspace"; +import { EUserProjectRoles } from "constants/project"; +import { EIssuesStoreType } from "constants/issue"; type Props = { workspaceSlug: string | undefined; @@ -28,13 +27,15 @@ export const CycleEmptyState: React.FC = observer((props) => { const { workspaceSlug, projectId, cycleId } = props; // states const [cycleIssuesListModal, setCycleIssuesListModal] = useState(false); - + // store hooks + const { issues } = useIssues(EIssuesStoreType.CYCLE); + const { + commandPalette: { toggleCreateIssueModal }, + eventTracker: { setTrackElement }, + } = useApplication(); const { - cycleIssues: cycleIssueStore, - commandPalette: commandPaletteStore, - trackEvent: { setTrackElement }, - user: { currentProjectRole: userRole }, - } = useMobxStore(); + membership: { currentProjectRole: userRole }, + } = useUser(); const { setToastAlert } = useToast(); @@ -43,7 +44,7 @@ export const CycleEmptyState: React.FC = observer((props) => { const issueIds = data.map((i) => i.id); - await cycleIssueStore.addIssueToCycle(workspaceSlug.toString(), cycleId.toString(), issueIds).catch(() => { + await issues.addIssueToCycle(workspaceSlug.toString(), projectId, cycleId.toString(), issueIds).catch(() => { setToastAlert({ type: "error", title: "Error!", @@ -52,7 +53,7 @@ export const CycleEmptyState: React.FC = observer((props) => { }); }; - const isEditingAllowed = !!userRole && userRole >= EUserWorkspaceRoles.MEMBER; + const isEditingAllowed = !!userRole && userRole >= EUserProjectRoles.MEMBER; return ( <> @@ -72,7 +73,7 @@ export const CycleEmptyState: React.FC = observer((props) => { icon: , onClick: () => { setTrackElement("CYCLE_EMPTY_STATE"); - commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.CYCLE); + toggleCreateIssueModal(true, EIssuesStoreType.CYCLE); }, }} secondaryButton={ diff --git a/web/components/issues/issue-layouts/empty-states/global-view.tsx b/web/components/issues/issue-layouts/empty-states/global-view.tsx index d4348c4bf6a..cd4070186a6 100644 --- a/web/components/issues/issue-layouts/empty-states/global-view.tsx +++ b/web/components/issues/issue-layouts/empty-states/global-view.tsx @@ -1,31 +1,24 @@ -// next -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +import { Plus, PlusIcon } from "lucide-react"; +// hooks +import { useApplication, useProject } from "hooks/store"; // components import { EmptyState } from "components/common"; // assets import emptyIssue from "public/empty-state/issue.svg"; import emptyProject from "public/empty-state/project.svg"; -// icons -import { Plus, PlusIcon } from "lucide-react"; export const GlobalViewEmptyState: React.FC = observer(() => { - const router = useRouter(); - const { workspaceSlug } = router.query; - + // store hooks const { - commandPalette: commandPaletteStore, - project: projectStore, - trackEvent: { setTrackElement }, - } = useMobxStore(); - - const projects = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : null; + commandPalette: { toggleCreateIssueModal, toggleCreateProjectModal }, + eventTracker: { setTrackElement }, + } = useApplication(); + const { workspaceProjectIds } = useProject(); return (
- {!projects || projects?.length === 0 ? ( + {!workspaceProjectIds || workspaceProjectIds?.length === 0 ? ( { text: "New Project", onClick: () => { setTrackElement("ALL_ISSUES_EMPTY_STATE"); - commandPaletteStore.toggleCreateProjectModal(true); + toggleCreateProjectModal(true); }, }} /> @@ -49,7 +42,7 @@ export const GlobalViewEmptyState: React.FC = observer(() => { icon: , onClick: () => { setTrackElement("ALL_ISSUES_EMPTY_STATE"); - commandPaletteStore.toggleCreateIssueModal(true); + toggleCreateIssueModal(true); }, }} /> diff --git a/web/components/issues/issue-layouts/empty-states/module.tsx b/web/components/issues/issue-layouts/empty-states/module.tsx index ed7f73358f8..cb70a5a226d 100644 --- a/web/components/issues/issue-layouts/empty-states/module.tsx +++ b/web/components/issues/issue-layouts/empty-states/module.tsx @@ -1,17 +1,21 @@ +import { useState } from "react"; +import { observer } from "mobx-react-lite"; import { PlusIcon } from "lucide-react"; +// hooks +import { useApplication, useIssues, useUser } from "hooks/store"; +import useToast from "hooks/use-toast"; // components import { EmptyState } from "components/common"; +import { ExistingIssuesListModal } from "components/core"; +// ui import { Button } from "@plane/ui"; // assets import emptyIssue from "public/empty-state/issue.svg"; -import { ExistingIssuesListModal } from "components/core"; -import { observer } from "mobx-react-lite"; -import { useMobxStore } from "lib/mobx/store-provider"; -import { ISearchIssueResponse } from "types"; -import useToast from "hooks/use-toast"; -import { useState } from "react"; +// types +import { ISearchIssueResponse } from "@plane/types"; // constants -import { EUserWorkspaceRoles } from "constants/workspace"; +import { EUserProjectRoles } from "constants/project"; +import { EIssuesStoreType } from "constants/issue"; type Props = { workspaceSlug: string | undefined; @@ -23,14 +27,16 @@ export const ModuleEmptyState: React.FC = observer((props) => { const { workspaceSlug, projectId, moduleId } = props; // states const [moduleIssuesListModal, setModuleIssuesListModal] = useState(false); - + // store hooks + const { issues } = useIssues(EIssuesStoreType.MODULE); const { - moduleIssues: moduleIssueStore, - commandPalette: commandPaletteStore, - trackEvent: { setTrackElement }, - user: { currentProjectRole: userRole }, - } = useMobxStore(); - + commandPalette: { toggleCreateIssueModal }, + eventTracker: { setTrackElement }, + } = useApplication(); + const { + membership: { currentProjectRole: userRole }, + } = useUser(); + // toast alert const { setToastAlert } = useToast(); const handleAddIssuesToModule = async (data: ISearchIssueResponse[]) => { @@ -38,16 +44,18 @@ export const ModuleEmptyState: React.FC = observer((props) => { const issueIds = data.map((i) => i.id); - await moduleIssueStore.addIssueToModule(workspaceSlug.toString(), moduleId.toString(), issueIds).catch(() => - setToastAlert({ - type: "error", - title: "Error!", - message: "Selected issues could not be added to the module. Please try again.", - }) - ); + await issues + .addIssueToModule(workspaceSlug.toString(), projectId?.toString(), moduleId.toString(), issueIds) + .catch(() => + setToastAlert({ + type: "error", + title: "Error!", + message: "Selected issues could not be added to the module. Please try again.", + }) + ); }; - const isEditingAllowed = !!userRole && userRole >= EUserWorkspaceRoles.MEMBER; + const isEditingAllowed = !!userRole && userRole >= EUserProjectRoles.MEMBER; return ( <> @@ -67,7 +75,7 @@ export const ModuleEmptyState: React.FC = observer((props) => { icon: , onClick: () => { setTrackElement("MODULE_EMPTY_STATE"); - commandPaletteStore.toggleCreateIssueModal(true); + toggleCreateIssueModal(true); }, }} secondaryButton={ diff --git a/web/components/issues/issue-layouts/empty-states/project-view.tsx b/web/components/issues/issue-layouts/empty-states/project-view.tsx index 2fd297a90d8..919decd5106 100644 --- a/web/components/issues/issue-layouts/empty-states/project-view.tsx +++ b/web/components/issues/issue-layouts/empty-states/project-view.tsx @@ -1,18 +1,19 @@ import { observer } from "mobx-react-lite"; import { PlusIcon } from "lucide-react"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +// hooks +import { useApplication } from "hooks/store"; // components import { EmptyState } from "components/common"; // assets import emptyIssue from "public/empty-state/issue.svg"; -import { EProjectStore } from "store/command-palette.store"; +import { EIssuesStoreType } from "constants/issue"; export const ProjectViewEmptyState: React.FC = observer(() => { + // store hooks const { commandPalette: commandPaletteStore, - trackEvent: { setTrackElement }, - } = useMobxStore(); + eventTracker: { setTrackElement }, + } = useApplication(); return (
@@ -25,7 +26,7 @@ export const ProjectViewEmptyState: React.FC = observer(() => { icon: , onClick: () => { setTrackElement("VIEW_EMPTY_STATE"); - commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.PROJECT_VIEW); + commandPaletteStore.toggleCreateIssueModal(true, EIssuesStoreType.PROJECT_VIEW); }, }} /> diff --git a/web/components/issues/issue-layouts/empty-states/project.tsx b/web/components/issues/issue-layouts/empty-states/project.tsx index 7db04b36a15..592264d82da 100644 --- a/web/components/issues/issue-layouts/empty-states/project.tsx +++ b/web/components/issues/issue-layouts/empty-states/project.tsx @@ -1,23 +1,26 @@ import { observer } from "mobx-react-lite"; import { PlusIcon } from "lucide-react"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +// hooks +import { useApplication, useUser } from "hooks/store"; // components import { NewEmptyState } from "components/common/new-empty-state"; // constants -import { EUserWorkspaceRoles } from "constants/workspace"; +import { EUserProjectRoles } from "constants/project"; // assets import emptyIssue from "public/empty-state/empty_issues.webp"; -import { EProjectStore } from "store/command-palette.store"; +import { EIssuesStoreType } from "constants/issue"; export const ProjectEmptyState: React.FC = observer(() => { + // store hooks const { commandPalette: commandPaletteStore, - trackEvent: { setTrackElement }, - user: { currentProjectRole }, - } = useMobxStore(); + eventTracker: { setTrackElement }, + } = useApplication(); + const { + membership: { currentProjectRole }, + } = useUser(); - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER; + const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; return (
@@ -31,18 +34,14 @@ export const ProjectEmptyState: React.FC = observer(() => { description: "Redesign the Plane UI, Rebrand the company, or Launch the new fuel injection system are examples of issues that likely have sub-issues.", }} - primaryButton={ - isEditingAllowed - ? { - text: "Create your first issue", - icon: , - onClick: () => { - setTrackElement("PROJECT_EMPTY_STATE"); - commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.PROJECT); - }, - } - : null - } + primaryButton={{ + text: "Create your first issue", + icon: , + onClick: () => { + setTrackElement("PROJECT_EMPTY_STATE"); + commandPaletteStore.toggleCreateIssueModal(true, EIssuesStoreType.PROJECT); + }, + }} disabled={!isEditingAllowed} />
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 7ff8056b9e1..18eac852541 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 @@ -1,5 +1,7 @@ import { observer } from "mobx-react-lite"; -import { useMobxStore } from "lib/mobx/store-provider"; +import { X } from "lucide-react"; +// hooks +import { useUser } from "hooks/store"; // components import { AppliedDateFilters, @@ -10,22 +12,18 @@ import { AppliedStateFilters, AppliedStateGroupFilters, } from "components/issues"; -// icons -import { X } from "lucide-react"; // helpers import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper"; // types -import { IIssueFilterOptions, IIssueLabel, IProject, IState, IUserLite } from "types"; +import { IIssueFilterOptions, IIssueLabel, IState } from "@plane/types"; // constants -import { EUserWorkspaceRoles } from "constants/workspace"; +import { EUserProjectRoles } from "constants/project"; type Props = { appliedFilters: IIssueFilterOptions; handleClearAllFilters: () => void; handleRemoveFilter: (key: keyof IIssueFilterOptions, value: string | null) => void; labels?: IIssueLabel[] | undefined; - members?: IUserLite[] | undefined; - projects?: IProject[] | undefined; states?: IState[] | undefined; }; @@ -33,17 +31,17 @@ const membersFilters = ["assignees", "mentions", "created_by", "subscriber"]; const dateFilters = ["start_date", "target_date"]; export const AppliedFiltersList: React.FC = observer((props) => { - const { appliedFilters, handleClearAllFilters, handleRemoveFilter, labels, members, projects, states } = props; - + const { appliedFilters, handleClearAllFilters, handleRemoveFilter, labels, states } = props; + // store hooks const { - user: { currentProjectRole }, - } = useMobxStore(); + membership: { currentProjectRole }, + } = useUser(); if (!appliedFilters) return null; if (Object.keys(appliedFilters).length === 0) return null; - const isEditingAllowed = currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER; + const isEditingAllowed = currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; return (
@@ -63,7 +61,6 @@ export const AppliedFiltersList: React.FC = observer((props) => { handleRemoveFilter(filterKey, val)} - members={members} values={value} /> )} @@ -103,7 +100,6 @@ export const AppliedFiltersList: React.FC = observer((props) => { handleRemoveFilter("project", val)} - projects={projects} values={value} /> )} diff --git a/web/components/issues/issue-layouts/filters/applied-filters/label.tsx b/web/components/issues/issue-layouts/filters/applied-filters/label.tsx index 08e7aee4458..799233d0135 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/label.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/label.tsx @@ -3,7 +3,7 @@ import { observer } from "mobx-react-lite"; // icons import { X } from "lucide-react"; // types -import { IIssueLabel } from "types"; +import { IIssueLabel } from "@plane/types"; type Props = { handleRemove: (val: string) => void; diff --git a/web/components/issues/issue-layouts/filters/applied-filters/members.tsx b/web/components/issues/issue-layouts/filters/applied-filters/members.tsx index 1dd61d3390b..94ea9221efb 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/members.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/members.tsx @@ -3,22 +3,25 @@ import { X } from "lucide-react"; // ui import { Avatar } from "@plane/ui"; // types -import { IUserLite } from "types"; +import { useMember } from "hooks/store"; type Props = { handleRemove: (val: string) => void; - members: IUserLite[] | undefined; values: string[]; editable: boolean | undefined; }; export const AppliedMembersFilters: React.FC = observer((props) => { - const { handleRemove, members, values, editable } = props; + const { handleRemove, values, editable } = props; + + const { + project: { getProjectMemberDetails }, + } = useMember(); return ( <> {values.map((memberId) => { - const memberDetails = members?.find((m) => m.id === memberId); + const memberDetails = getProjectMemberDetails(memberId)?.member; if (!memberDetails) return null; diff --git a/web/components/issues/issue-layouts/filters/applied-filters/priority.tsx b/web/components/issues/issue-layouts/filters/applied-filters/priority.tsx index 88b39dc0033..be3240b5511 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/priority.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/priority.tsx @@ -4,7 +4,7 @@ import { observer } from "mobx-react-lite"; import { PriorityIcon } from "@plane/ui"; import { X } from "lucide-react"; // types -import { TIssuePriorities } from "types"; +import { TIssuePriorities } from "@plane/types"; type Props = { handleRemove: (val: string) => void; diff --git a/web/components/issues/issue-layouts/filters/applied-filters/project.tsx b/web/components/issues/issue-layouts/filters/applied-filters/project.tsx index b1e17cfe3dc..4c5affe8d4a 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/project.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/project.tsx @@ -1,25 +1,25 @@ import { observer } from "mobx-react-lite"; - -// icons import { X } from "lucide-react"; -// types -import { IProject } from "types"; +// hooks +import { useProject } from "hooks/store"; +// helpers import { renderEmoji } from "helpers/emoji.helper"; type Props = { handleRemove: (val: string) => void; - projects: IProject[] | undefined; values: string[]; editable: boolean | undefined; }; export const AppliedProjectFilters: React.FC = observer((props) => { - const { handleRemove, projects, values, editable } = props; + const { handleRemove, values, editable } = props; + // store hooks + const { projectMap } = useProject(); return ( <> {values.map((projectId) => { - const projectDetails = projects?.find((p) => p.id === projectId); + const projectDetails = projectMap?.[projectId] ?? null; if (!projectDetails) return null; diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/archived-issue.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/archived-issue.tsx index 2b6571d3b28..b09bc76286a 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/archived-issue.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/archived-issue.tsx @@ -1,27 +1,28 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; - -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +// hooks +import { useIssues, useLabel, useProjectState } from "hooks/store"; // components import { AppliedFiltersList, SaveFilterView } from "components/issues"; // types -import { IIssueFilterOptions } from "types"; -import { EFilterType } from "store/issues/types"; +import { IIssueFilterOptions } from "@plane/types"; +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; export const ArchivedIssueAppliedFiltersRoot: React.FC = observer(() => { + // router const router = useRouter(); const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string }; + // store hooks const { - projectArchivedIssuesFilter: { issueFilters, updateFilters }, - projectLabel: { projectLabels }, - projectMember: { projectMembers }, - projectState: projectStateStore, - } = useMobxStore(); - + issuesFilter: { issueFilters, updateFilters }, + } = useIssues(EIssuesStoreType.ARCHIVED); + const { + project: { projectLabels }, + } = useLabel(); + const { projectStates } = useProjectState(); + // derived values const userFilters = issueFilters?.filters; - // filters whose value not null or empty array const appliedFilters: IIssueFilterOptions = {}; Object.entries(userFilters ?? {}).forEach(([key, value]) => { @@ -37,7 +38,7 @@ export const ArchivedIssueAppliedFiltersRoot: React.FC = observer(() => { // remove all values of the key if value is null if (!value) { - updateFilters(workspaceSlug.toString(), projectId.toString(), EFilterType.FILTERS, { + updateFilters(workspaceSlug.toString(), projectId.toString(), EIssueFilterType.FILTERS, { [key]: null, }); return; @@ -47,7 +48,7 @@ export const ArchivedIssueAppliedFiltersRoot: React.FC = observer(() => { let newValues = issueFilters?.filters?.[key] ?? []; newValues = newValues.filter((val) => val !== value); - updateFilters(workspaceSlug.toString(), projectId.toString(), EFilterType.FILTERS, { + updateFilters(workspaceSlug.toString(), projectId.toString(), EIssueFilterType.FILTERS, { [key]: newValues, }); }; @@ -60,7 +61,7 @@ export const ArchivedIssueAppliedFiltersRoot: React.FC = observer(() => { newFilters[key as keyof IIssueFilterOptions] = null; }); - updateFilters(workspaceSlug.toString(), projectId.toString(), EFilterType.FILTERS, { + updateFilters(workspaceSlug.toString(), projectId.toString(), EIssueFilterType.FILTERS, { ...newFilters, }); }; @@ -75,8 +76,7 @@ export const ArchivedIssueAppliedFiltersRoot: React.FC = observer(() => { handleClearAllFilters={handleClearAllFilters} handleRemoveFilter={handleRemoveFilter} labels={projectLabels ?? []} - members={projectMembers?.map((m) => m.member)} - states={projectStateStore.states?.[projectId?.toString() ?? ""]} + states={projectStates} /> diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/cycle-root.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/cycle-root.tsx index b7c8b688906..f402c980709 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/cycle-root.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/cycle-root.tsx @@ -1,31 +1,32 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; - -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +// hooks +import { useIssues, useLabel, useProjectState } from "hooks/store"; // components import { AppliedFiltersList, SaveFilterView } from "components/issues"; // types -import { IIssueFilterOptions } from "types"; -import { EFilterType } from "store/issues/types"; +import { IIssueFilterOptions } from "@plane/types"; +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; export const CycleAppliedFiltersRoot: React.FC = observer(() => { + // router const router = useRouter(); const { workspaceSlug, projectId, cycleId } = router.query as { workspaceSlug: string; projectId: string; cycleId: string; }; - + // store hooks const { - projectLabel: { projectLabels }, - projectState: projectStateStore, - projectMember: { projectMembers }, - cycleIssuesFilter: { issueFilters, updateFilters }, - } = useMobxStore(); + issuesFilter: { issueFilters, updateFilters }, + } = useIssues(EIssuesStoreType.CYCLE); + const { + project: { projectLabels }, + } = useLabel(); + const { projectStates } = useProjectState(); + // derived values const userFilters = issueFilters?.filters; - // filters whose value not null or empty array const appliedFilters: IIssueFilterOptions = {}; Object.entries(userFilters ?? {}).forEach(([key, value]) => { @@ -35,32 +36,20 @@ export const CycleAppliedFiltersRoot: React.FC = observer(() => { }); const handleRemoveFilter = (key: keyof IIssueFilterOptions, value: string | null) => { - if (!workspaceSlug || !projectId) return; + if (!workspaceSlug || !projectId || !cycleId) return; if (!value) { - updateFilters( - workspaceSlug, - projectId, - EFilterType.FILTERS, - { - [key]: null, - }, - cycleId - ); + updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { + [key]: null, + }); return; } let newValues = issueFilters?.filters?.[key] ?? []; newValues = newValues.filter((val) => val !== value); - updateFilters( - workspaceSlug, - projectId, - EFilterType.FILTERS, - { - [key]: newValues, - }, - cycleId - ); + updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { + [key]: newValues, + }); }; const handleClearAllFilters = () => { @@ -69,7 +58,7 @@ export const CycleAppliedFiltersRoot: React.FC = observer(() => { Object.keys(userFilters ?? {}).forEach((key) => { newFilters[key as keyof IIssueFilterOptions] = null; }); - updateFilters(workspaceSlug, projectId, EFilterType.FILTERS, { ...newFilters }, cycleId); + updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { ...newFilters }, cycleId); }; // return if no filters are applied @@ -82,8 +71,7 @@ export const CycleAppliedFiltersRoot: React.FC = observer(() => { handleClearAllFilters={handleClearAllFilters} handleRemoveFilter={handleRemoveFilter} labels={projectLabels ?? []} - members={projectMembers?.map((m) => m.member)} - states={projectStateStore.states?.[cycleId ?? ""]} + states={projectStates} /> diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/draft-issue.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/draft-issue.tsx index d3d56266dbe..f650c0bd547 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/draft-issue.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/draft-issue.tsx @@ -1,25 +1,26 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; - -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +// hooks +import { useIssues, useLabel, useProjectState } from "hooks/store"; // components import { AppliedFiltersList, SaveFilterView } from "components/issues"; // types -import { IIssueFilterOptions } from "types"; -import { EFilterType } from "store/issues/types"; +import { IIssueFilterOptions } from "@plane/types"; +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; export const DraftIssueAppliedFiltersRoot: React.FC = observer(() => { + // router const router = useRouter(); const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string }; - + // store hooks const { - projectDraftIssuesFilter: { issueFilters, updateFilters }, - projectLabel: { projectLabels }, - projectMember: { projectMembers }, - projectState: projectStateStore, - } = useMobxStore(); - + issuesFilter: { issueFilters, updateFilters }, + } = useIssues(EIssuesStoreType.DRAFT); + const { + project: { projectLabels }, + } = useLabel(); + const { projectStates } = useProjectState(); + // derived values const userFilters = issueFilters?.filters; // filters whose value not null or empty array const appliedFilters: IIssueFilterOptions = {}; @@ -34,7 +35,7 @@ export const DraftIssueAppliedFiltersRoot: React.FC = observer(() => { // remove all values of the key if value is null if (!value) { - updateFilters(workspaceSlug.toString(), projectId.toString(), EFilterType.FILTERS, { + updateFilters(workspaceSlug.toString(), projectId.toString(), EIssueFilterType.FILTERS, { [key]: null, }); return; @@ -44,7 +45,7 @@ export const DraftIssueAppliedFiltersRoot: React.FC = observer(() => { let newValues = issueFilters?.filters?.[key] ?? []; newValues = newValues.filter((val) => val !== value); - updateFilters(workspaceSlug.toString(), projectId.toString(), EFilterType.FILTERS, { + updateFilters(workspaceSlug.toString(), projectId.toString(), EIssueFilterType.FILTERS, { [key]: newValues, }); }; @@ -57,7 +58,7 @@ export const DraftIssueAppliedFiltersRoot: React.FC = observer(() => { newFilters[key as keyof IIssueFilterOptions] = null; }); - updateFilters(workspaceSlug.toString(), projectId.toString(), EFilterType.FILTERS, { ...newFilters }); + updateFilters(workspaceSlug.toString(), projectId.toString(), EIssueFilterType.FILTERS, { ...newFilters }); }; // return if no filters are applied @@ -70,8 +71,7 @@ export const DraftIssueAppliedFiltersRoot: React.FC = observer(() => { handleClearAllFilters={handleClearAllFilters} handleRemoveFilter={handleRemoveFilter} labels={projectLabels ?? []} - members={projectMembers?.map((m) => m.member)} - states={projectStateStore.states?.[projectId?.toString() ?? ""]} + states={projectStates} /> diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/global-view-root.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/global-view-root.tsx index 543d186451d..87bb719c4e1 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/global-view-root.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/global-view-root.tsx @@ -1,25 +1,25 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; - -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +// hooks +import { useIssues, useLabel } from "hooks/store"; // components import { AppliedFiltersList } from "components/issues"; // types -import { IIssueFilterOptions } from "types"; -import { EFilterType } from "store/issues/types"; +import { IIssueFilterOptions } from "@plane/types"; +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; export const GlobalViewsAppliedFiltersRoot = observer(() => { + // router const router = useRouter(); - const { workspaceSlug } = router.query as { workspaceSlug: string; globalViewId: string }; - + const { workspaceSlug, globalViewId } = router.query; + // store hooks + const { + issuesFilter: { issueFilters, updateFilters }, + } = useIssues(EIssuesStoreType.GLOBAL); const { - project: { workspaceProjects }, workspace: { workspaceLabels }, - workspaceMember: { workspaceMembers }, - workspaceGlobalIssuesFilter: { issueFilters, updateFilters }, - } = useMobxStore(); - + } = useLabel(); + // derived values const userFilters = issueFilters?.filters; // filters whose value not null or empty array @@ -31,23 +31,43 @@ export const GlobalViewsAppliedFiltersRoot = observer(() => { }); const handleRemoveFilter = (key: keyof IIssueFilterOptions, value: string | null) => { + if (!workspaceSlug || !globalViewId) return; + if (!value) { - updateFilters(workspaceSlug, EFilterType.FILTERS, { [key]: null }); + updateFilters( + workspaceSlug.toString(), + undefined, + EIssueFilterType.FILTERS, + { [key]: null }, + globalViewId.toString() + ); return; } let newValues = userFilters?.[key] ?? []; newValues = newValues.filter((val) => val !== value); - updateFilters(workspaceSlug, EFilterType.FILTERS, { [key]: newValues }); + updateFilters( + workspaceSlug.toString(), + undefined, + EIssueFilterType.FILTERS, + { [key]: newValues }, + globalViewId.toString() + ); }; const handleClearAllFilters = () => { - if (!workspaceSlug) return; + if (!workspaceSlug || !globalViewId) return; const newFilters: IIssueFilterOptions = {}; Object.keys(userFilters ?? {}).forEach((key) => { newFilters[key as keyof IIssueFilterOptions] = null; }); - updateFilters(workspaceSlug, EFilterType.FILTERS, { ...newFilters }); + updateFilters( + workspaceSlug.toString(), + undefined, + EIssueFilterType.FILTERS, + { ...newFilters }, + globalViewId.toString() + ); }; // const handleUpdateView = () => { @@ -78,8 +98,6 @@ export const GlobalViewsAppliedFiltersRoot = observer(() => {
m.member)} - projects={workspaceProjects ?? undefined} appliedFilters={appliedFilters ?? {}} handleClearAllFilters={handleClearAllFilters} handleRemoveFilter={handleRemoveFilter} diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/module-root.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/module-root.tsx index 62cd4b3d878..a9a7832c604 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/module-root.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/module-root.tsx @@ -1,31 +1,31 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; - -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +// hooks +import { useIssues, useLabel, useProjectState } from "hooks/store"; // components import { AppliedFiltersList, SaveFilterView } from "components/issues"; // types -import { IIssueFilterOptions } from "types"; -import { EFilterType } from "store/issues/types"; +import { IIssueFilterOptions } from "@plane/types"; +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; export const ModuleAppliedFiltersRoot: React.FC = observer(() => { + // router const router = useRouter(); const { workspaceSlug, projectId, moduleId } = router.query as { workspaceSlug: string; projectId: string; moduleId: string; }; - + // store hooks const { - projectLabel: { projectLabels }, - projectState: projectStateStore, - projectMember: { projectMembers }, - moduleIssuesFilter: { issueFilters, updateFilters }, - } = useMobxStore(); - + issuesFilter: { issueFilters, updateFilters }, + } = useIssues(EIssuesStoreType.MODULE); + const { + project: { projectLabels }, + } = useLabel(); + const { projectStates } = useProjectState(); + // derived values const userFilters = issueFilters?.filters; - // filters whose value not null or empty array const appliedFilters: IIssueFilterOptions = {}; Object.entries(userFilters ?? {}).forEach(([key, value]) => { @@ -37,30 +37,18 @@ export const ModuleAppliedFiltersRoot: React.FC = observer(() => { const handleRemoveFilter = (key: keyof IIssueFilterOptions, value: string | null) => { if (!workspaceSlug || !projectId) return; if (!value) { - updateFilters( - workspaceSlug, - projectId, - EFilterType.FILTERS, - { - [key]: null, - }, - moduleId - ); + updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { + [key]: null, + }); return; } let newValues = issueFilters?.filters?.[key] ?? []; newValues = newValues.filter((val) => val !== value); - updateFilters( - workspaceSlug, - projectId, - EFilterType.FILTERS, - { - [key]: newValues, - }, - moduleId - ); + updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { + [key]: newValues, + }); }; const handleClearAllFilters = () => { @@ -69,7 +57,7 @@ export const ModuleAppliedFiltersRoot: React.FC = observer(() => { Object.keys(userFilters ?? {}).forEach((key) => { newFilters[key as keyof IIssueFilterOptions] = null; }); - updateFilters(workspaceSlug, projectId, EFilterType.FILTERS, { ...newFilters }, moduleId); + updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { ...newFilters }, moduleId); }; // return if no filters are applied @@ -82,8 +70,7 @@ export const ModuleAppliedFiltersRoot: React.FC = observer(() => { handleClearAllFilters={handleClearAllFilters} handleRemoveFilter={handleRemoveFilter} labels={projectLabels ?? []} - members={projectMembers?.map((m) => m.member)} - states={projectStateStore.states?.[moduleId ?? ""]} + states={projectStates} /> diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/profile-issues-root.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/profile-issues-root.tsx index 89870d98a13..0c45c025ea9 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/profile-issues-root.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/profile-issues-root.tsx @@ -1,26 +1,26 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; - -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +// hooks +import { useIssues, useLabel } from "hooks/store"; // components import { AppliedFiltersList } from "components/issues"; // types -import { IIssueFilterOptions } from "types"; -import { EFilterType } from "store/issues/types"; +import { IIssueFilterOptions } from "@plane/types"; +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; export const ProfileIssuesAppliedFiltersRoot: React.FC = observer(() => { + // router const router = useRouter(); - const { workspaceSlug } = router.query as { - workspaceSlug: string; - }; + const { workspaceSlug, userId } = router.query; + // store hooks + const { + issuesFilter: { issueFilters, updateFilters }, + } = useIssues(EIssuesStoreType.PROFILE); const { workspace: { workspaceLabels }, - workspaceProfileIssuesFilter: { issueFilters, updateFilters }, - projectMember: { projectMembers }, - } = useMobxStore(); - + } = useLabel(); + // derived values const userFilters = issueFilters?.filters; // filters whose value not null or empty array @@ -32,27 +32,33 @@ export const ProfileIssuesAppliedFiltersRoot: React.FC = observer(() => { }); const handleRemoveFilter = (key: keyof IIssueFilterOptions, value: string | null) => { - if (!workspaceSlug) return; + if (!workspaceSlug || !userId) return; if (!value) { - updateFilters(workspaceSlug, EFilterType.FILTERS, { [key]: null }); + updateFilters(workspaceSlug.toString(), undefined, EIssueFilterType.FILTERS, { [key]: null }, userId.toString()); return; } let newValues = issueFilters?.filters?.[key] ?? []; newValues = newValues.filter((val) => val !== value); - updateFilters(workspaceSlug, EFilterType.FILTERS, { - [key]: newValues, - }); + updateFilters( + workspaceSlug.toString(), + undefined, + EIssueFilterType.FILTERS, + { + [key]: newValues, + }, + userId.toString() + ); }; const handleClearAllFilters = () => { - if (!workspaceSlug) return; + if (!workspaceSlug || !userId) return; const newFilters: IIssueFilterOptions = {}; Object.keys(userFilters ?? {}).forEach((key) => { newFilters[key as keyof IIssueFilterOptions] = null; }); - updateFilters(workspaceSlug, EFilterType.FILTERS, { ...newFilters }); + updateFilters(workspaceSlug.toString(), undefined, EIssueFilterType.FILTERS, { ...newFilters }, userId.toString()); }; // return if no filters are applied @@ -65,7 +71,6 @@ export const ProfileIssuesAppliedFiltersRoot: React.FC = observer(() => { handleClearAllFilters={handleClearAllFilters} handleRemoveFilter={handleRemoveFilter} labels={workspaceLabels ?? []} - members={projectMembers?.map((m) => m.member)} states={[]} />
diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/project-root.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/project-root.tsx index 31317366c25..98ecf50b43c 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/project-root.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/project-root.tsx @@ -1,14 +1,15 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +// hooks +import { useLabel, useProjectState, useUser } from "hooks/store"; +import { useIssues } from "hooks/store/use-issues"; // components import { AppliedFiltersList, SaveFilterView } from "components/issues"; -// types -import { IIssueFilterOptions } from "types"; -import { EFilterType } from "store/issues/types"; // constants -import { EUserWorkspaceRoles } from "constants/workspace"; +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; +import { EUserProjectRoles } from "constants/project"; +// types +import { IIssueFilterOptions } from "@plane/types"; export const ProjectAppliedFiltersRoot: React.FC = observer(() => { // router @@ -17,18 +18,20 @@ export const ProjectAppliedFiltersRoot: React.FC = observer(() => { workspaceSlug: string; projectId: string; }; - // mobx stores + // store hooks + const { + project: { projectLabels }, + } = useLabel(); const { - projectLabel: { projectLabels }, - projectState: projectStateStore, - projectMember: { projectMembers }, - projectIssuesFilter: { issueFilters, updateFilters }, - user: { currentProjectRole }, - } = useMobxStore(); + issuesFilter: { issueFilters, updateFilters }, + } = useIssues(EIssuesStoreType.PROJECT); + const { + membership: { currentProjectRole }, + } = useUser(); + const { projectStates } = useProjectState(); // derived values - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER; + const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; const userFilters = issueFilters?.filters; - // filters whose value not null or empty array const appliedFilters: IIssueFilterOptions = {}; Object.entries(userFilters ?? {}).forEach(([key, value]) => { @@ -40,7 +43,7 @@ export const ProjectAppliedFiltersRoot: React.FC = observer(() => { const handleRemoveFilter = (key: keyof IIssueFilterOptions, value: string | null) => { if (!workspaceSlug || !projectId) return; if (!value) { - updateFilters(workspaceSlug.toString(), projectId.toString(), EFilterType.FILTERS, { + updateFilters(workspaceSlug.toString(), projectId.toString(), EIssueFilterType.FILTERS, { [key]: null, }); return; @@ -49,7 +52,7 @@ export const ProjectAppliedFiltersRoot: React.FC = observer(() => { let newValues = issueFilters?.filters?.[key] ?? []; newValues = newValues.filter((val) => val !== value); - updateFilters(workspaceSlug.toString(), projectId.toString(), EFilterType.FILTERS, { + updateFilters(workspaceSlug.toString(), projectId.toString(), EIssueFilterType.FILTERS, { [key]: newValues, }); }; @@ -60,7 +63,7 @@ export const ProjectAppliedFiltersRoot: React.FC = observer(() => { Object.keys(userFilters ?? {}).forEach((key) => { newFilters[key as keyof IIssueFilterOptions] = null; }); - updateFilters(workspaceSlug.toString(), projectId.toString(), EFilterType.FILTERS, { ...newFilters }); + updateFilters(workspaceSlug.toString(), projectId.toString(), EIssueFilterType.FILTERS, { ...newFilters }); }; // return if no filters are applied @@ -73,8 +76,7 @@ export const ProjectAppliedFiltersRoot: React.FC = observer(() => { handleClearAllFilters={handleClearAllFilters} handleRemoveFilter={handleRemoveFilter} labels={projectLabels ?? []} - members={projectMembers?.map((m) => m.member)} - states={projectStateStore.states?.[projectId?.toString() ?? ""]} + states={projectStates} /> {isEditingAllowed && ( diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/project-view-root.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/project-view-root.tsx index 6b037a031d0..ffbae9ac843 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/project-view-root.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/project-view-root.tsx @@ -1,7 +1,7 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +// hooks +import { useIssues, useLabel, useProjectState, useProjectView } from "hooks/store"; // components import { AppliedFiltersList } from "components/issues"; // ui @@ -9,27 +9,28 @@ import { Button } from "@plane/ui"; // helpers import { areFiltersDifferent } from "helpers/filter.helper"; // types -import { IIssueFilterOptions } from "types"; -import { EFilterType } from "store/issues/types"; +import { IIssueFilterOptions } from "@plane/types"; +import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => { + // router const router = useRouter(); const { workspaceSlug, projectId, viewId } = router.query as { workspaceSlug: string; projectId: string; viewId: string; }; - + // store hooks const { - projectLabel: { projectLabels }, - projectState: projectStateStore, - projectMember: { projectMembers }, - projectViews: projectViewsStore, - viewIssuesFilter: { issueFilters, updateFilters }, - } = useMobxStore(); - - const viewDetails = viewId ? projectViewsStore.viewDetails[viewId.toString()] : undefined; - + issuesFilter: { issueFilters, updateFilters }, + } = useIssues(EIssuesStoreType.PROJECT_VIEW); + const { + project: { projectLabels }, + } = useLabel(); + const { projectStates } = useProjectState(); + const { getViewById, updateView } = useProjectView(); + // derived values + const viewDetails = viewId ? getViewById(viewId.toString()) : null; const userFilters = issueFilters?.filters; // filters whose value not null or empty array const appliedFilters: IIssueFilterOptions = {}; @@ -42,30 +43,18 @@ export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => { const handleRemoveFilter = (key: keyof IIssueFilterOptions, value: string | null) => { if (!workspaceSlug || !projectId) return; if (!value) { - updateFilters( - workspaceSlug, - projectId, - EFilterType.FILTERS, - { - [key]: null, - }, - viewId - ); + updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { + [key]: null, + }); return; } let newValues = issueFilters?.filters?.[key] ?? []; newValues = newValues.filter((val) => val !== value); - updateFilters( - workspaceSlug, - projectId, - EFilterType.FILTERS, - { - [key]: newValues, - }, - viewId - ); + updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { + [key]: newValues, + }); }; const handleClearAllFilters = () => { @@ -74,7 +63,7 @@ export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => { Object.keys(userFilters ?? {}).forEach((key) => { newFilters[key as keyof IIssueFilterOptions] = null; }); - updateFilters(workspaceSlug, projectId, EFilterType.FILTERS, { ...newFilters }, viewId); + updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { ...newFilters }, viewId); }; // return if no filters are applied @@ -83,7 +72,7 @@ export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => { const handleUpdateView = () => { if (!workspaceSlug || !projectId || !viewId || !viewDetails) return; - projectViewsStore.updateView(workspaceSlug.toString(), projectId.toString(), viewId.toString(), { + updateView(workspaceSlug.toString(), projectId.toString(), viewId.toString(), { query_data: { ...viewDetails.query_data, ...(appliedFilters ?? {}), @@ -98,8 +87,7 @@ export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => { handleClearAllFilters={handleClearAllFilters} handleRemoveFilter={handleRemoveFilter} labels={projectLabels ?? []} - members={projectMembers?.map((m) => m.member)} - states={projectStateStore.states?.[projectId?.toString() ?? ""]} + states={projectStates} /> {appliedFilters && diff --git a/web/components/issues/issue-layouts/filters/applied-filters/state-group.tsx b/web/components/issues/issue-layouts/filters/applied-filters/state-group.tsx index 64f95983e46..620a8f78138 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/state-group.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/state-group.tsx @@ -3,7 +3,7 @@ import { observer } from "mobx-react-lite"; // icons import { StateGroupIcon } from "@plane/ui"; import { X } from "lucide-react"; -import { TStateGroups } from "types"; +import { TStateGroups } from "@plane/types"; type Props = { handleRemove: (val: string) => void; diff --git a/web/components/issues/issue-layouts/filters/applied-filters/state.tsx b/web/components/issues/issue-layouts/filters/applied-filters/state.tsx index 9cff84d9b7a..59a873162ec 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/state.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/state.tsx @@ -4,7 +4,7 @@ import { observer } from "mobx-react-lite"; import { StateGroupIcon } from "@plane/ui"; import { X } from "lucide-react"; // types -import { IState } from "types"; +import { IState } from "@plane/types"; type Props = { handleRemove: (val: string) => void; 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 412e5479449..3c94b4f3fc5 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,7 +11,7 @@ import { FilterSubGroupBy, } from "components/issues"; // types -import { IIssueDisplayFilterOptions, IIssueDisplayProperties } from "types"; +import { IIssueDisplayFilterOptions, IIssueDisplayProperties } from "@plane/types"; import { ILayoutDisplayFiltersOptions } from "constants/issue"; type Props = { 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 0abe6442a29..3ea1453e824 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 @@ -4,7 +4,7 @@ import { observer } from "mobx-react-lite"; // components import { FilterHeader } from "../helpers/filter-header"; // types -import { IIssueDisplayProperties } from "types"; +import { IIssueDisplayProperties } from "@plane/types"; // constants import { ISSUE_DISPLAY_PROPERTIES } from "constants/issue"; diff --git a/web/components/issues/issue-layouts/filters/header/display-filters/extra-options.tsx b/web/components/issues/issue-layouts/filters/header/display-filters/extra-options.tsx index cb75b53f4af..0feb1d89190 100644 --- a/web/components/issues/issue-layouts/filters/header/display-filters/extra-options.tsx +++ b/web/components/issues/issue-layouts/filters/header/display-filters/extra-options.tsx @@ -4,7 +4,7 @@ import { observer } from "mobx-react-lite"; // components import { FilterOption } from "components/issues"; // types -import { IIssueDisplayFilterOptions, TIssueExtraOptions } from "types"; +import { IIssueDisplayFilterOptions, TIssueExtraOptions } from "@plane/types"; // constants import { ISSUE_EXTRA_OPTIONS } from "constants/issue"; diff --git a/web/components/issues/issue-layouts/filters/header/display-filters/group-by.tsx b/web/components/issues/issue-layouts/filters/header/display-filters/group-by.tsx index aa057e4174f..659d86d089f 100644 --- a/web/components/issues/issue-layouts/filters/header/display-filters/group-by.tsx +++ b/web/components/issues/issue-layouts/filters/header/display-filters/group-by.tsx @@ -4,7 +4,7 @@ import { observer } from "mobx-react-lite"; // components import { FilterHeader, FilterOption } from "components/issues"; // types -import { IIssueDisplayFilterOptions, TIssueGroupByOptions } from "types"; +import { IIssueDisplayFilterOptions, TIssueGroupByOptions } from "@plane/types"; // constants import { ISSUE_GROUP_BY_OPTIONS } from "constants/issue"; diff --git a/web/components/issues/issue-layouts/filters/header/display-filters/issue-type.tsx b/web/components/issues/issue-layouts/filters/header/display-filters/issue-type.tsx index a6fa2bf0612..59c83a2003a 100644 --- a/web/components/issues/issue-layouts/filters/header/display-filters/issue-type.tsx +++ b/web/components/issues/issue-layouts/filters/header/display-filters/issue-type.tsx @@ -4,7 +4,7 @@ import { observer } from "mobx-react-lite"; // components import { FilterHeader, FilterOption } from "components/issues"; // types -import { TIssueTypeFilters } from "types"; +import { TIssueTypeFilters } from "@plane/types"; // constants import { ISSUE_FILTER_OPTIONS } from "constants/issue"; diff --git a/web/components/issues/issue-layouts/filters/header/display-filters/order-by.tsx b/web/components/issues/issue-layouts/filters/header/display-filters/order-by.tsx index 004d1b6e94f..e417c650ecd 100644 --- a/web/components/issues/issue-layouts/filters/header/display-filters/order-by.tsx +++ b/web/components/issues/issue-layouts/filters/header/display-filters/order-by.tsx @@ -4,7 +4,7 @@ import { observer } from "mobx-react-lite"; // components import { FilterHeader, FilterOption } from "components/issues"; // types -import { TIssueOrderByOptions } from "types"; +import { TIssueOrderByOptions } from "@plane/types"; // constants import { ISSUE_ORDER_BY_OPTIONS } from "constants/issue"; diff --git a/web/components/issues/issue-layouts/filters/header/display-filters/sub-group-by.tsx b/web/components/issues/issue-layouts/filters/header/display-filters/sub-group-by.tsx index f6642242778..3310511619d 100644 --- a/web/components/issues/issue-layouts/filters/header/display-filters/sub-group-by.tsx +++ b/web/components/issues/issue-layouts/filters/header/display-filters/sub-group-by.tsx @@ -4,7 +4,7 @@ import { observer } from "mobx-react-lite"; // components import { FilterHeader, FilterOption } from "components/issues"; // types -import { IIssueDisplayFilterOptions, TIssueGroupByOptions } from "types"; +import { IIssueDisplayFilterOptions, TIssueGroupByOptions } from "@plane/types"; // constants import { ISSUE_GROUP_BY_OPTIONS } from "constants/issue"; diff --git a/web/components/issues/issue-layouts/filters/header/filters/assignee.tsx b/web/components/issues/issue-layouts/filters/header/filters/assignee.tsx index 0a1ecf3eabe..168e31bc0f3 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/assignee.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/assignee.tsx @@ -1,28 +1,31 @@ -import React, { useState } from "react"; +import { useState } from "react"; +import { observer } from "mobx-react-lite"; +// hooks +import { useMember } from "hooks/store"; // components import { FilterHeader, FilterOption } from "components/issues"; // ui import { Avatar, Loader } from "@plane/ui"; -// types -import { IUserLite } from "types"; type Props = { appliedFilters: string[] | null; handleUpdate: (val: string) => void; - members: IUserLite[] | undefined; + memberIds: string[] | undefined; searchQuery: string; }; -export const FilterAssignees: React.FC = (props) => { - const { appliedFilters, handleUpdate, members, searchQuery } = props; - +export const FilterAssignees: React.FC = observer((props: Props) => { + const { appliedFilters, handleUpdate, memberIds, searchQuery } = props; + // states const [itemsToRender, setItemsToRender] = useState(5); const [previewEnabled, setPreviewEnabled] = useState(true); + // store hooks + const { getUserDetails } = useMember(); const appliedFiltersCount = appliedFilters?.length ?? 0; - const filteredOptions = members?.filter((member) => - member.display_name.toLowerCase().includes(searchQuery.toLowerCase()) + const filteredOptions = memberIds?.filter((memberId) => + getUserDetails(memberId)?.display_name.toLowerCase().includes(searchQuery.toLowerCase()) ); const handleViewToggle = () => { @@ -44,15 +47,20 @@ export const FilterAssignees: React.FC = (props) => { {filteredOptions ? ( filteredOptions.length > 0 ? ( <> - {filteredOptions.slice(0, itemsToRender).map((member) => ( - handleUpdate(member.id)} - icon={} - title={member.display_name} - /> - ))} + {filteredOptions.slice(0, itemsToRender).map((memberId) => { + const member = getUserDetails(memberId); + + if (!member) return null; + return ( + handleUpdate(member.id)} + icon={} + title={member.display_name} + /> + ); + })} {filteredOptions.length > 5 && (
@@ -109,7 +108,7 @@ export const FilterSelection: React.FC = observer((props) => { handleFiltersUpdate("mentions", val)} - members={members} + memberIds={memberIds} searchQuery={filtersSearchQuery} />
@@ -121,7 +120,7 @@ export const FilterSelection: React.FC = observer((props) => { handleFiltersUpdate("created_by", val)} - members={members} + memberIds={memberIds} searchQuery={filtersSearchQuery} />
@@ -144,7 +143,6 @@ export const FilterSelection: React.FC = observer((props) => {
handleFiltersUpdate("project", val)} searchQuery={filtersSearchQuery} /> diff --git a/web/components/issues/issue-layouts/filters/header/filters/labels.tsx b/web/components/issues/issue-layouts/filters/header/filters/labels.tsx index de6b73596ef..3e23ae07b8e 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/labels.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/labels.tsx @@ -5,7 +5,7 @@ import { FilterHeader, FilterOption } from "components/issues"; // ui import { Loader } from "@plane/ui"; // types -import { IIssueLabel } from "types"; +import { IIssueLabel } from "@plane/types"; const LabelIcons = ({ color }: { color: string }) => ( diff --git a/web/components/issues/issue-layouts/filters/header/filters/mentions.tsx b/web/components/issues/issue-layouts/filters/header/filters/mentions.tsx index 8e2f4b402a6..a6af9833a4e 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/mentions.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/mentions.tsx @@ -1,28 +1,31 @@ -import React, { useState } from "react"; +import { useState } from "react"; +import { observer } from "mobx-react-lite"; +// hooks +import { useMember } from "hooks/store"; // components import { FilterHeader, FilterOption } from "components/issues"; // ui import { Loader, Avatar } from "@plane/ui"; -// types -import { IUserLite } from "types"; type Props = { appliedFilters: string[] | null; handleUpdate: (val: string) => void; - members: IUserLite[] | undefined; + memberIds: string[] | undefined; searchQuery: string; }; -export const FilterMentions: React.FC = (props) => { - const { appliedFilters, handleUpdate, members, searchQuery } = props; - +export const FilterMentions: React.FC = observer((props: Props) => { + const { appliedFilters, handleUpdate, memberIds, searchQuery } = props; + // states const [itemsToRender, setItemsToRender] = useState(5); const [previewEnabled, setPreviewEnabled] = useState(true); + // store hooks + const { getUserDetails } = useMember(); const appliedFiltersCount = appliedFilters?.length ?? 0; - const filteredOptions = members?.filter((member) => - member.display_name.toLowerCase().includes(searchQuery.toLowerCase()) + const filteredOptions = memberIds?.filter((memberId) => + getUserDetails(memberId)?.display_name.toLowerCase().includes(searchQuery.toLowerCase()) ); const handleViewToggle = () => { @@ -44,15 +47,20 @@ export const FilterMentions: React.FC = (props) => { {filteredOptions ? ( filteredOptions.length > 0 ? ( <> - {filteredOptions.slice(0, itemsToRender).map((member) => ( - handleUpdate(member.id)} - icon={} - title={member.display_name} - /> - ))} + {filteredOptions.slice(0, itemsToRender).map((memberId) => { + const member = getUserDetails(memberId); + + if (!member) return null; + return ( + handleUpdate(member.id)} + icon={} + title={member.display_name} + /> + ); + })} {filteredOptions.length > 5 && ( +
); -}; +}); diff --git a/web/components/issues/issue-layouts/list/blocks-list.tsx b/web/components/issues/issue-layouts/list/blocks-list.tsx index 7270ae06da6..729cd6c68b7 100644 --- a/web/components/issues/issue-layouts/list/blocks-list.tsx +++ b/web/components/issues/issue-layouts/list/blocks-list.tsx @@ -2,41 +2,39 @@ import { FC } from "react"; // components import { IssueBlock } from "components/issues"; // types -import { IIssue, IIssueDisplayProperties } from "types"; -import { IIssueResponse, IGroupedIssues, TUnGroupedIssues } from "store/issues/types"; +import { TGroupedIssues, TIssue, IIssueDisplayProperties, TIssueMap, TUnGroupedIssues } from "@plane/types"; import { EIssueActions } from "../types"; interface Props { - columnId: string; - issueIds: IGroupedIssues | TUnGroupedIssues | any; - issues: IIssueResponse; + issueIds: TGroupedIssues | TUnGroupedIssues | any; + issuesMap: TIssueMap; canEditProperties: (projectId: string | undefined) => boolean; - handleIssues: (issue: IIssue, action: EIssueActions) => void; - quickActions: (group_by: string | null, issue: IIssue) => React.ReactNode; + handleIssues: (issue: TIssue, action: EIssueActions) => void; + quickActions: (issue: TIssue) => React.ReactNode; displayProperties: IIssueDisplayProperties | undefined; } export const IssueBlocksList: FC = (props) => { - const { columnId, issueIds, issues, handleIssues, quickActions, displayProperties, canEditProperties } = props; + const { issueIds, issuesMap, handleIssues, quickActions, displayProperties, canEditProperties } = props; return (
{issueIds && issueIds.length > 0 ? ( - issueIds.map( - (issueId: string) => - issueId != undefined && - issues[issueId] && ( - - ) - ) + issueIds.map((issueId: string) => { + if (!issueId) return null; + + return ( + + ); + }) ) : (
No issues
)} diff --git a/web/components/issues/issue-layouts/list/default.tsx b/web/components/issues/issue-layouts/list/default.tsx index 24781bb412a..9bf7cfc78e6 100644 --- a/web/components/issues/issue-layouts/list/default.tsx +++ b/web/components/issues/issue-layouts/list/default.tsx @@ -1,25 +1,29 @@ -import React from "react"; // components -import { ListGroupByHeaderRoot } from "./headers/group-by-root"; import { IssueBlocksList, ListQuickAddIssueForm } from "components/issues"; +// hooks +import { useLabel, useMember, useProject, useProjectState } from "hooks/store"; // types -import { IIssue, IIssueDisplayProperties, IIssueLabel, IProject, IState, IUserLite } from "types"; -import { IIssueResponse, IGroupedIssues, TUnGroupedIssues } from "store/issues/types"; +import { + GroupByColumnTypes, + TGroupedIssues, + TIssue, + IIssueDisplayProperties, + TIssueMap, + TUnGroupedIssues, +} from "@plane/types"; import { EIssueActions } from "../types"; // constants -import { getValueFromObject } from "constants/issue"; -import { EProjectStore } from "store/command-palette.store"; +import { HeaderGroupByCard } from "./headers/group-by-card"; +import { getGroupByColumns } from "../utils"; +import { TCreateModalStoreTypes } from "constants/issue"; export interface IGroupByList { - issueIds: IGroupedIssues | TUnGroupedIssues | any; - issues: any; + issueIds: TGroupedIssues | TUnGroupedIssues | any; + issuesMap: TIssueMap; group_by: string | null; - list: any; - listKey: string; - states: IState[] | null; is_list?: boolean; - handleIssues: (issue: IIssue, action: EIssueActions) => Promise; - quickActions: (group_by: string | null, issue: IIssue) => React.ReactNode; + handleIssues: (issue: TIssue, action: EIssueActions) => Promise; + quickActions: (issue: TIssue) => React.ReactNode; displayProperties: IIssueDisplayProperties | undefined; enableIssueQuickAdd: boolean; showEmptyGroup?: boolean; @@ -27,24 +31,21 @@ export interface IGroupByList { quickAddCallback?: ( workspaceSlug: string, projectId: string, - data: IIssue, + data: TIssue, viewId?: string - ) => Promise; + ) => Promise; disableIssueCreation?: boolean; - currentStore: EProjectStore; - addIssuesToView?: (issueIds: string[]) => Promise; + currentStore: TCreateModalStoreTypes; + addIssuesToView?: (issueIds: string[]) => Promise; viewId?: string; } const GroupByList: React.FC = (props) => { const { issueIds, - issues, + issuesMap, group_by, - list, - listKey, is_list = false, - states, handleIssues, quickActions, displayProperties, @@ -57,17 +58,26 @@ const GroupByList: React.FC = (props) => { currentStore, addIssuesToView, } = props; + // store hooks + const member = useMember(); + const project = useProject(); + const projectLabel = useLabel(); + const projectState = useProjectState(); + + const list = getGroupByColumns(group_by as GroupByColumnTypes, project, projectLabel, projectState, member, true); + + if (!list) return null; const prePopulateQuickAddData = (groupByKey: string | null, value: any) => { - const defaultState = states?.find((state) => state.default); - if (groupByKey === null) return { state: defaultState?.id }; + const defaultState = projectState.projectStates?.find((state) => state.default); + if (groupByKey === null) return { state_id: defaultState?.id }; else { if (groupByKey === "state") return { state: groupByKey === "state" ? value : defaultState?.id }; - else return { state: defaultState?.id, [groupByKey]: value }; + else return { state_id: defaultState?.id, [groupByKey]: value }; } }; - const validateEmptyIssueGroups = (issues: IIssue[]) => { + const validateEmptyIssueGroups = (issues: TIssue[]) => { const issuesCount = issues?.length || 0; if (!showEmptyGroup && issuesCount <= 0) return false; return true; @@ -79,29 +89,24 @@ const GroupByList: React.FC = (props) => { list.length > 0 && list.map( (_list: any) => - validateEmptyIssueGroups(is_list ? issueIds : issueIds?.[getValueFromObject(_list, listKey) as string]) && ( -
+ validateEmptyIssueGroups(is_list ? issueIds : issueIds?.[_list.id]) && ( +
-
- {issues && ( + {issueIds && ( = (props) => { {enableIssueQuickAdd && !disableIssueCreation && (
@@ -126,37 +131,31 @@ const GroupByList: React.FC = (props) => { }; export interface IList { - issueIds: IGroupedIssues | TUnGroupedIssues | any; - issues: IIssueResponse | undefined; + issueIds: TGroupedIssues | TUnGroupedIssues | any; + issuesMap: TIssueMap; group_by: string | null; - handleIssues: (issue: IIssue, action: EIssueActions) => Promise; - quickActions: (group_by: string | null, issue: IIssue) => React.ReactNode; + handleIssues: (issue: TIssue, action: EIssueActions) => Promise; + quickActions: (issue: TIssue) => React.ReactNode; displayProperties: IIssueDisplayProperties | undefined; showEmptyGroup: boolean; enableIssueQuickAdd: boolean; canEditProperties: (projectId: string | undefined) => boolean; - states: IState[] | null; - labels: IIssueLabel[] | null; - members: IUserLite[] | null; - projects: IProject[] | null; - stateGroups: any; - priorities: any; quickAddCallback?: ( workspaceSlug: string, projectId: string, - data: IIssue, + data: TIssue, viewId?: string - ) => Promise; + ) => Promise; viewId?: string; disableIssueCreation?: boolean; - currentStore: EProjectStore; - addIssuesToView?: (issueIds: string[]) => Promise; + currentStore: TCreateModalStoreTypes; + addIssuesToView?: (issueIds: string[]) => Promise; } export const List: React.FC = (props) => { const { issueIds, - issues, + issuesMap, group_by, handleIssues, quickActions, @@ -167,194 +166,28 @@ export const List: React.FC = (props) => { enableIssueQuickAdd, canEditProperties, disableIssueCreation, - states, - stateGroups, - priorities, - labels, - members, - projects, currentStore, addIssuesToView, } = props; return (
- {group_by === null && ( - - )} - - {group_by && group_by === "project" && projects && ( - - )} - - {group_by && group_by === "state" && states && ( - - )} - - {group_by && group_by === "state_detail.group" && stateGroups && ( - - )} - - {group_by && group_by === "priority" && priorities && ( - - )} - - {group_by && group_by === "labels" && labels && ( - - )} - - {group_by && group_by === "assignees" && members && ( - - )} - - {group_by && group_by === "created_by" && members && ( - - )} +
); }; diff --git a/web/components/issues/issue-layouts/list/headers/assignee.tsx b/web/components/issues/issue-layouts/list/headers/assignee.tsx deleted file mode 100644 index d129774aa8b..00000000000 --- a/web/components/issues/issue-layouts/list/headers/assignee.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { FC } from "react"; -import { observer } from "mobx-react-lite"; -// components -import { HeaderGroupByCard } from "./group-by-card"; -// ui -import { Avatar } from "@plane/ui"; -import { EProjectStore } from "store/command-palette.store"; -import { IIssue } from "types"; - -export interface IAssigneesHeader { - column_id: string; - column_value: any; - issues_count: number; - disableIssueCreation?: boolean; - currentStore: EProjectStore; - addIssuesToView?: (issueIds: string[]) => Promise; -} - -export const Icon = ({ user }: any) => ; - -export const AssigneesHeader: FC = observer((props) => { - const { column_value, issues_count, disableIssueCreation, currentStore, addIssuesToView } = props; - - const assignee = column_value ?? null; - - return ( - <> - {assignee && ( - } - title={assignee?.display_name || ""} - count={issues_count} - issuePayload={{ assignees: [assignee?.member?.id] }} - disableIssueCreation={disableIssueCreation} - currentStore={currentStore} - addIssuesToView={addIssuesToView} - /> - )} - - ); -}); diff --git a/web/components/issues/issue-layouts/list/headers/created-by.tsx b/web/components/issues/issue-layouts/list/headers/created-by.tsx deleted file mode 100644 index 77306998b6b..00000000000 --- a/web/components/issues/issue-layouts/list/headers/created-by.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { FC } from "react"; -import { observer } from "mobx-react-lite"; -// components -import { HeaderGroupByCard } from "./group-by-card"; -import { Icon } from "./assignee"; -import { EProjectStore } from "store/command-palette.store"; -import { IIssue } from "types"; - -export interface ICreatedByHeader { - column_id: string; - column_value: any; - issues_count: number; - disableIssueCreation?: boolean; - currentStore: EProjectStore; - addIssuesToView?: (issueIds: string[]) => Promise; -} - -export const CreatedByHeader: FC = observer((props) => { - const { column_value, issues_count, disableIssueCreation, currentStore, addIssuesToView } = props; - - const createdBy = column_value ?? null; - - return ( - <> - {createdBy && ( - } - title={createdBy?.display_name || ""} - count={issues_count} - issuePayload={{ created_by: createdBy?.member?.id }} - disableIssueCreation={disableIssueCreation} - currentStore={currentStore} - addIssuesToView={addIssuesToView} - /> - )} - - ); -}); diff --git a/web/components/issues/issue-layouts/list/headers/empty-group.tsx b/web/components/issues/issue-layouts/list/headers/empty-group.tsx deleted file mode 100644 index c7b16fe2624..00000000000 --- a/web/components/issues/issue-layouts/list/headers/empty-group.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { observer } from "mobx-react-lite"; -// components -import { HeaderGroupByCard } from "./group-by-card"; -import { EProjectStore } from "store/command-palette.store"; -import { IIssue } from "types"; - -export interface IEmptyHeader { - column_id: string; - column_value: any; - issues_count: number; - disableIssueCreation?: boolean; - currentStore: EProjectStore; - addIssuesToView?: (issueIds: string[]) => Promise; -} - -export const EmptyHeader: React.FC = observer((props) => { - const { column_value, issues_count, disableIssueCreation, currentStore, addIssuesToView } = props; - - return ( - - ); -}); diff --git a/web/components/issues/issue-layouts/list/headers/group-by-card.tsx b/web/components/issues/issue-layouts/list/headers/group-by-card.tsx index c703ea66b82..35c7f77cfeb 100644 --- a/web/components/issues/issue-layouts/list/headers/group-by-card.tsx +++ b/web/components/issues/issue-layouts/list/headers/group-by-card.tsx @@ -1,4 +1,3 @@ -import React from "react"; import { useRouter } from "next/router"; // lucide icons import { CircleDashed, Plus } from "lucide-react"; @@ -10,18 +9,19 @@ import { CustomMenu } from "@plane/ui"; // mobx import { observer } from "mobx-react-lite"; // types -import { IIssue, ISearchIssueResponse } from "types"; -import { EProjectStore } from "store/command-palette.store"; +import { TIssue, ISearchIssueResponse } from "@plane/types"; import useToast from "hooks/use-toast"; +import { useState } from "react"; +import { TCreateModalStoreTypes } from "constants/issue"; interface IHeaderGroupByCard { icon?: React.ReactNode; title: string; count: number; - issuePayload: Partial; + issuePayload: Partial; disableIssueCreation?: boolean; - currentStore: EProjectStore; - addIssuesToView?: (issueIds: string[]) => Promise; + currentStore: TCreateModalStoreTypes; + addIssuesToView?: (issueIds: string[]) => Promise; } export const HeaderGroupByCard = observer( @@ -29,9 +29,9 @@ export const HeaderGroupByCard = observer( const router = useRouter(); const { workspaceSlug, projectId, moduleId, cycleId } = router.query; - const [isOpen, setIsOpen] = React.useState(false); + const [isOpen, setIsOpen] = useState(false); - const [openExistingIssueListModal, setOpenExistingIssueListModal] = React.useState(false); + const [openExistingIssueListModal, setOpenExistingIssueListModal] = useState(false); const isDraftIssue = router.pathname.includes("draft-issue"); @@ -45,14 +45,15 @@ export const HeaderGroupByCard = observer( const issues = data.map((i) => i.id); - addIssuesToView && - addIssuesToView(issues)?.catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "Selected issues could not be added to the cycle. Please try again.", - }); + try { + addIssuesToView && addIssuesToView(issues); + } catch (error) { + setToastAlert({ + type: "error", + title: "Error!", + message: "Selected issues could not be added to the cycle. Please try again.", }); + } }; return ( diff --git a/web/components/issues/issue-layouts/list/headers/group-by-root.tsx b/web/components/issues/issue-layouts/list/headers/group-by-root.tsx deleted file mode 100644 index 50ed9ad982f..00000000000 --- a/web/components/issues/issue-layouts/list/headers/group-by-root.tsx +++ /dev/null @@ -1,114 +0,0 @@ -// components -import { EmptyHeader } from "./empty-group"; -import { ProjectHeader } from "./project"; -import { StateHeader } from "./state"; -import { StateGroupHeader } from "./state-group"; -import { AssigneesHeader } from "./assignee"; -import { PriorityHeader } from "./priority"; -import { LabelHeader } from "./label"; -import { CreatedByHeader } from "./created-by"; -// mobx -import { observer } from "mobx-react-lite"; -import { EProjectStore } from "store/command-palette.store"; -import { IIssue } from "types"; - -export interface IListGroupByHeaderRoot { - column_id: string; - column_value: any; - group_by: string | null; - issues_count: number; - disableIssueCreation?: boolean; - currentStore: EProjectStore; - addIssuesToView?: (issueIds: string[]) => Promise; -} - -export const ListGroupByHeaderRoot: React.FC = observer((props) => { - const { column_id, column_value, group_by, issues_count, disableIssueCreation, currentStore, addIssuesToView } = - props; - - return ( - <> - {!group_by && group_by === null && ( - - )} - {group_by && group_by === "project" && ( - - )} - - {group_by && group_by === "state" && ( - - )} - {group_by && group_by === "state_detail.group" && ( - - )} - {group_by && group_by === "priority" && ( - - )} - {group_by && group_by === "labels" && ( - - )} - {group_by && group_by === "assignees" && ( - - )} - {group_by && group_by === "created_by" && ( - - )} - - ); -}); diff --git a/web/components/issues/issue-layouts/list/headers/label.tsx b/web/components/issues/issue-layouts/list/headers/label.tsx deleted file mode 100644 index b4d740e37e0..00000000000 --- a/web/components/issues/issue-layouts/list/headers/label.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { FC } from "react"; -import { observer } from "mobx-react-lite"; -// components -import { HeaderGroupByCard } from "./group-by-card"; -import { EProjectStore } from "store/command-palette.store"; -import { IIssue } from "types"; - -export interface ILabelHeader { - column_id: string; - column_value: any; - issues_count: number; - disableIssueCreation?: boolean; - currentStore: EProjectStore; - addIssuesToView?: (issueIds: string[]) => Promise; -} - -const Icon = ({ color }: any) => ( -
-); - -export const LabelHeader: FC = observer((props) => { - const { column_value, issues_count, disableIssueCreation, currentStore, addIssuesToView } = props; - - const label = column_value ?? null; - - return ( - <> - {column_value && ( - } - title={column_value?.name || ""} - count={issues_count} - issuePayload={{ labels: [label.id] }} - disableIssueCreation={disableIssueCreation} - currentStore={currentStore} - addIssuesToView={addIssuesToView} - /> - )} - - ); -}); diff --git a/web/components/issues/issue-layouts/list/headers/priority.tsx b/web/components/issues/issue-layouts/list/headers/priority.tsx deleted file mode 100644 index 5eb19fbfd30..00000000000 --- a/web/components/issues/issue-layouts/list/headers/priority.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { FC } from "react"; -import { observer } from "mobx-react-lite"; -import { AlertCircle, SignalHigh, SignalMedium, SignalLow, Ban } from "lucide-react"; -// components -import { HeaderGroupByCard } from "./group-by-card"; -import { EProjectStore } from "store/command-palette.store"; -import { IIssue } from "types"; - -export interface IPriorityHeader { - column_id: string; - column_value: any; - issues_count: number; - disableIssueCreation?: boolean; - currentStore: EProjectStore; - addIssuesToView?: (issueIds: string[]) => Promise; -} - -const Icon = ({ priority }: any) => ( -
- {priority === "urgent" ? ( -
- -
- ) : priority === "high" ? ( -
- -
- ) : priority === "medium" ? ( -
- -
- ) : priority === "low" ? ( -
- -
- ) : ( -
- -
- )} -
-); - -export const PriorityHeader: FC = observer((props) => { - const { column_value, issues_count, disableIssueCreation, currentStore, addIssuesToView } = props; - - const priority = column_value ?? null; - - return ( - <> - {priority && ( - } - title={priority?.title || ""} - count={issues_count} - issuePayload={{ priority: priority?.key }} - disableIssueCreation={disableIssueCreation} - currentStore={currentStore} - addIssuesToView={addIssuesToView} - /> - )} - - ); -}); diff --git a/web/components/issues/issue-layouts/list/headers/project.tsx b/web/components/issues/issue-layouts/list/headers/project.tsx deleted file mode 100644 index 7578214b245..00000000000 --- a/web/components/issues/issue-layouts/list/headers/project.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { FC } from "react"; -import { observer } from "mobx-react-lite"; -// components -import { HeaderGroupByCard } from "./group-by-card"; -// emoji helper -import { renderEmoji } from "helpers/emoji.helper"; -import { EProjectStore } from "store/command-palette.store"; -import { IIssue } from "types"; - -export interface IProjectHeader { - column_id: string; - column_value: any; - issues_count: number; - disableIssueCreation?: boolean; - currentStore: EProjectStore; - addIssuesToView?: (issueIds: string[]) => Promise; -} - -const Icon = ({ emoji }: any) =>
{renderEmoji(emoji)}
; - -export const ProjectHeader: FC = observer((props) => { - const { column_value, issues_count, disableIssueCreation, currentStore, addIssuesToView } = props; - - const project = column_value ?? null; - - return ( - <> - {project && ( - } - title={project?.name || ""} - count={issues_count} - issuePayload={{ project: project?.id ?? "" }} - disableIssueCreation={disableIssueCreation} - currentStore={currentStore} - addIssuesToView={addIssuesToView} - /> - )} - - ); -}); diff --git a/web/components/issues/issue-layouts/list/headers/state-group.tsx b/web/components/issues/issue-layouts/list/headers/state-group.tsx deleted file mode 100644 index 421a1da8f38..00000000000 --- a/web/components/issues/issue-layouts/list/headers/state-group.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { FC } from "react"; -import { observer } from "mobx-react-lite"; -// components -import { HeaderGroupByCard } from "./group-by-card"; -// ui -import { StateGroupIcon } from "@plane/ui"; -// helpers -import { capitalizeFirstLetter } from "helpers/string.helper"; -import { EProjectStore } from "store/command-palette.store"; -import { IIssue } from "types"; - -export interface IStateGroupHeader { - column_id: string; - column_value: any; - issues_count: number; - disableIssueCreation?: boolean; - currentStore: EProjectStore; - addIssuesToView?: (issueIds: string[]) => Promise; -} - -export const Icon = ({ stateGroup, color }: { stateGroup: any; color?: any }) => ( -
- -
-); - -export const StateGroupHeader: FC = observer((props) => { - const { column_value, issues_count, disableIssueCreation, currentStore, addIssuesToView } = props; - - const stateGroup = column_value ?? null; - - return ( - <> - {stateGroup && ( - } - title={capitalizeFirstLetter(stateGroup?.key) || ""} - count={issues_count} - issuePayload={{}} - disableIssueCreation={disableIssueCreation} - currentStore={currentStore} - addIssuesToView={addIssuesToView} - /> - )} - - ); -}); diff --git a/web/components/issues/issue-layouts/list/headers/state.tsx b/web/components/issues/issue-layouts/list/headers/state.tsx deleted file mode 100644 index 926743464b9..00000000000 --- a/web/components/issues/issue-layouts/list/headers/state.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { FC } from "react"; -import { observer } from "mobx-react-lite"; -// components -import { HeaderGroupByCard } from "./group-by-card"; -import { Icon } from "./state-group"; -import { EProjectStore } from "store/command-palette.store"; -import { IIssue } from "types"; - -export interface IStateHeader { - column_id: string; - column_value: any; - issues_count: number; - disableIssueCreation?: boolean; - currentStore: EProjectStore; - addIssuesToView?: (issueIds: string[]) => Promise; -} - -export const StateHeader: FC = observer((props) => { - const { column_value, issues_count, disableIssueCreation, currentStore, addIssuesToView } = props; - - const state = column_value ?? null; - - return ( - <> - {state && ( - } - title={state?.name || ""} - count={issues_count} - issuePayload={{ state: state?.id }} - disableIssueCreation={disableIssueCreation} - currentStore={currentStore} - addIssuesToView={addIssuesToView} - /> - )} - - ); -}); diff --git a/web/components/issues/issue-layouts/list/list-view-types.d.ts b/web/components/issues/issue-layouts/list/list-view-types.d.ts index efdd79cfc7f..674ae92d115 100644 --- a/web/components/issues/issue-layouts/list/list-view-types.d.ts +++ b/web/components/issues/issue-layouts/list/list-view-types.d.ts @@ -1,7 +1,7 @@ export interface IQuickActionProps { - issue: IIssue; + issue: TIssue; handleDelete: () => Promise; - handleUpdate?: (data: IIssue) => Promise; + handleUpdate?: (data: TIssue) => Promise; handleRemoveFromView?: () => Promise; customActionButton?: React.ReactElement; } diff --git a/web/components/issues/issue-layouts/list/properties.tsx b/web/components/issues/issue-layouts/list/properties.tsx deleted file mode 100644 index 07129910fe7..00000000000 --- a/web/components/issues/issue-layouts/list/properties.tsx +++ /dev/null @@ -1,168 +0,0 @@ -import { FC } from "react"; -import { observer } from "mobx-react-lite"; -import { Layers, Link, Paperclip } from "lucide-react"; -// components -import { IssuePropertyState } from "../properties/state"; -import { IssuePropertyPriority } from "../properties/priority"; -import { IssuePropertyLabels } from "../properties/labels"; -import { IssuePropertyAssignee } from "../properties/assignee"; -import { IssuePropertyEstimates } from "../properties/estimates"; -import { IssuePropertyDate } from "../properties/date"; -// ui -import { Tooltip } from "@plane/ui"; -// types -import { IIssue, IIssueDisplayProperties, IState, TIssuePriorities } from "types"; - -export interface IListProperties { - columnId: string; - issue: IIssue; - handleIssues: (group_by: string | null, issue: IIssue) => void; - displayProperties: IIssueDisplayProperties | undefined; - isReadonly?: boolean; -} - -export const ListProperties: FC = observer((props) => { - const { columnId: group_id, issue, handleIssues, displayProperties, isReadonly } = props; - - const handleState = (state: IState) => { - handleIssues(!group_id && group_id === "null" ? null : group_id, { ...issue, state: state.id }); - }; - - const handlePriority = (value: TIssuePriorities) => { - handleIssues(!group_id && group_id === "null" ? null : group_id, { ...issue, priority: value }); - }; - - const handleLabel = (ids: string[]) => { - handleIssues(!group_id && group_id === "null" ? null : group_id, { ...issue, labels: ids }); - }; - - const handleAssignee = (ids: string[]) => { - handleIssues(!group_id && group_id === "null" ? null : group_id, { ...issue, assignees: ids }); - }; - - const handleStartDate = (date: string | null) => { - handleIssues(!group_id && group_id === "null" ? null : group_id, { ...issue, start_date: date }); - }; - - const handleTargetDate = (date: string | null) => { - handleIssues(!group_id && group_id === "null" ? null : group_id, { ...issue, target_date: date }); - }; - - const handleEstimate = (value: number | null) => { - handleIssues(!group_id && group_id === "null" ? null : group_id, { ...issue, estimate_point: value }); - }; - - return ( -
- {/* basic properties */} - {/* state */} - {displayProperties && displayProperties?.state && ( - - )} - - {/* priority */} - {displayProperties && displayProperties?.priority && ( - - )} - - {/* label */} - {displayProperties && displayProperties?.labels && ( - - )} - - {/* assignee */} - {displayProperties && displayProperties?.assignee && ( - - )} - - {/* start date */} - {displayProperties && displayProperties?.start_date && ( - handleStartDate(date)} - disabled={isReadonly} - type="start_date" - /> - )} - - {/* target/due date */} - {displayProperties && displayProperties?.due_date && ( - handleTargetDate(date)} - disabled={isReadonly} - type="target_date" - /> - )} - - {/* estimates */} - {displayProperties && displayProperties?.estimate && ( - - )} - - {/* extra render properties */} - {/* sub-issues */} - {displayProperties && displayProperties?.sub_issue_count && !!issue?.sub_issues_count && ( - -
- -
{issue.sub_issues_count}
-
-
- )} - - {/* attachments */} - {displayProperties && displayProperties?.attachment_count && !!issue?.attachment_count && ( - -
- -
{issue.attachment_count}
-
-
- )} - - {/* link */} - {displayProperties && displayProperties?.link && !!issue?.link_count && ( - -
- -
{issue.link_count}
-
-
- )} -
- ); -}); diff --git a/web/components/issues/issue-layouts/list/quick-add-issue-form.tsx b/web/components/issues/issue-layouts/list/quick-add-issue-form.tsx index 9237d8a1f78..b6e39606efa 100644 --- a/web/components/issues/issue-layouts/list/quick-add-issue-form.tsx +++ b/web/components/issues/issue-layouts/list/quick-add-issue-form.tsx @@ -4,13 +4,12 @@ import { useForm } from "react-hook-form"; import { PlusIcon } from "lucide-react"; import { observer } from "mobx-react-lite"; // hooks +import { useProject, useWorkspace } from "hooks/store"; import useToast from "hooks/use-toast"; import useKeypress from "hooks/use-keypress"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; -// store -import { useMobxStore } from "lib/mobx/store-provider"; // constants -import { IIssue, IProject } from "types"; +import { TIssue, IProject } from "@plane/types"; // types import { createIssuePayload } from "helpers/issue.helper"; @@ -44,31 +43,28 @@ const Inputs: FC = (props) => { }; interface IListQuickAddIssueForm { - prePopulatedData?: Partial; + prePopulatedData?: Partial; quickAddCallback?: ( workspaceSlug: string, projectId: string, - data: IIssue, + data: TIssue, viewId?: string - ) => Promise; + ) => Promise; viewId?: string; } -const defaultValues: Partial = { +const defaultValues: Partial = { name: "", }; export const ListQuickAddIssueForm: FC = observer((props) => { const { prePopulatedData, quickAddCallback, viewId } = props; - + // router const router = useRouter(); - const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string }; - - const { workspace: workspaceStore, project: projectStore } = useMobxStore(); - - const workspaceDetail = (workspaceSlug && workspaceStore.getWorkspaceBySlug(workspaceSlug)) || null; - const projectDetail: IProject | null = - (workspaceSlug && projectId && projectStore.getProjectById(workspaceSlug, projectId)) || null; + const { workspaceSlug, projectId } = router.query; + // store hooks + const { currentWorkspace } = useWorkspace(); + const { currentProjectDetails } = useProject(); const ref = useRef(null); @@ -85,24 +81,25 @@ export const ListQuickAddIssueForm: FC = observer((props setFocus, register, formState: { errors, isSubmitting }, - } = useForm({ defaultValues }); + } = useForm({ defaultValues }); useEffect(() => { if (!isOpen) reset({ ...defaultValues }); }, [isOpen, reset]); - const onSubmitHandler = async (formData: IIssue) => { - if (isSubmitting || !workspaceDetail || !projectDetail) return; + const onSubmitHandler = async (formData: TIssue) => { + if (isSubmitting || !currentWorkspace || !currentProjectDetails || !workspaceSlug || !projectId) return; reset({ ...defaultValues }); - const payload = createIssuePayload(workspaceDetail, projectDetail, { + const payload = createIssuePayload(currentWorkspace, currentProjectDetails, { ...(prePopulatedData ?? {}), ...formData, }); try { - quickAddCallback && (await quickAddCallback(workspaceSlug, projectId, { ...payload }, viewId)); + quickAddCallback && + (await quickAddCallback(workspaceSlug.toString(), projectId.toString(), { ...payload }, viewId)); setToastAlert({ type: "success", title: "Success!", @@ -130,7 +127,12 @@ export const ListQuickAddIssueForm: FC = observer((props onSubmit={handleSubmit(onSubmitHandler)} className="flex w-full items-center gap-x-3 border-[0.5px] border-t-0 border-custom-border-100 bg-custom-background-100 px-3" > - +
{`Press 'Enter' to add another issue`}
diff --git a/web/components/issues/issue-layouts/list/roots/archived-issue-root.tsx b/web/components/issues/issue-layouts/list/roots/archived-issue-root.tsx index cf4c74063fb..388699dc707 100644 --- a/web/components/issues/issue-layouts/list/roots/archived-issue-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/archived-issue-root.tsx @@ -1,46 +1,40 @@ -import { FC } from "react"; +import { FC, useMemo } from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; // hooks -import { useMobxStore } from "lib/mobx/store-provider"; +import { useIssues } from "hooks/store"; // components import { ArchivedIssueQuickActions } from "components/issues"; // types -import { IIssue } from "types"; +import { TIssue } from "@plane/types"; // constants import { BaseListRoot } from "../base-list-root"; -import { IProjectStore } from "store/project"; import { EIssueActions } from "../../types"; -import { EProjectStore } from "store/command-palette.store"; +import { EIssuesStoreType } from "constants/issue"; export const ArchivedIssueListLayout: FC = observer(() => { const router = useRouter(); const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string }; - const { projectArchivedIssues: archivedIssueStore, projectArchivedIssuesFilter: archivedIssueFiltersStore } = - useMobxStore(); + const { issues, issuesFilter } = useIssues(EIssuesStoreType.ARCHIVED); + const issueActions = useMemo( + () => ({ + [EIssueActions.DELETE]: async (issue: TIssue) => { + if (!workspaceSlug || !projectId) return; - const issueActions = { - [EIssueActions.DELETE]: async (group_by: string | null, issue: IIssue) => { - if (!workspaceSlug || !projectId) return; - - await archivedIssueStore.removeIssue(workspaceSlug, projectId, issue.id); - }, - }; - - const getProjects = (projectStore: IProjectStore) => { - if (!workspaceSlug) return null; - return projectStore?.projects[workspaceSlug.toString()] || null; - }; + await issues.removeIssue(workspaceSlug, projectId, issue.id); + }, + }), + [issues, workspaceSlug, projectId] + ); return ( ); }); diff --git a/web/components/issues/issue-layouts/list/roots/cycle-root.tsx b/web/components/issues/issue-layouts/list/roots/cycle-root.tsx index de579473bf9..c1db5141167 100644 --- a/web/components/issues/issue-layouts/list/roots/cycle-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/cycle-root.tsx @@ -1,65 +1,58 @@ -import React from "react"; +import React, { useMemo } from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +// hooks +import { useIssues } from "hooks/store"; // components import { CycleIssueQuickActions } from "components/issues"; // types -import { IIssue } from "types"; +import { TIssue } from "@plane/types"; // constants import { BaseListRoot } from "../base-list-root"; -import { IProjectStore } from "store/project"; import { EIssueActions } from "../../types"; -import { EProjectStore } from "store/command-palette.store"; +import { EIssuesStoreType } from "constants/issue"; export interface ICycleListLayout {} export const CycleListLayout: React.FC = observer(() => { const router = useRouter(); - const { workspaceSlug, cycleId } = router.query as { workspaceSlug: string; cycleId: string }; + const { workspaceSlug, projectId, cycleId } = router.query; // store - const { - cycleIssues: cycleIssueStore, - cycleIssuesFilter: cycleIssueFilterStore, - cycle: { fetchCycleWithId }, - } = useMobxStore(); + const { issues, issuesFilter } = useIssues(EIssuesStoreType.CYCLE); - const issueActions = { - [EIssueActions.UPDATE]: async (group_by: string | null, issue: IIssue) => { - if (!workspaceSlug || !cycleId) return; + const issueActions = useMemo( + () => ({ + [EIssueActions.UPDATE]: async (issue: TIssue) => { + if (!workspaceSlug || !cycleId) return; - await cycleIssueStore.updateIssue(workspaceSlug, issue.project, issue.id, issue, cycleId); - fetchCycleWithId(workspaceSlug, issue.project, cycleId); - }, - [EIssueActions.DELETE]: async (group_by: string | null, issue: IIssue) => { - if (!workspaceSlug || !cycleId) return; + await issues.updateIssue(workspaceSlug.toString(), issue.project_id, issue.id, issue, cycleId.toString()); + }, + [EIssueActions.DELETE]: async (issue: TIssue) => { + if (!workspaceSlug || !cycleId) return; - await cycleIssueStore.removeIssue(workspaceSlug, issue.project, issue.id, cycleId); - fetchCycleWithId(workspaceSlug, issue.project, cycleId); - }, - [EIssueActions.REMOVE]: async (group_by: string | null, issue: IIssue) => { - if (!workspaceSlug || !cycleId || !issue.bridge_id) return; + await issues.removeIssue(workspaceSlug.toString(), issue.project_id, issue.id, cycleId.toString()); + }, + [EIssueActions.REMOVE]: async (issue: TIssue) => { + if (!workspaceSlug || !cycleId) return; - await cycleIssueStore.removeIssueFromCycle(workspaceSlug, issue.project, cycleId, issue.id, issue.bridge_id); - fetchCycleWithId(workspaceSlug, issue.project, cycleId); - }, - }; - const getProjects = (projectStore: IProjectStore) => { - if (!workspaceSlug) return null; - return projectStore?.projects[workspaceSlug] || null; - }; + await issues.removeIssueFromCycle(workspaceSlug.toString(), issue.project_id, cycleId.toString(), issue.id); + }, + }), + [issues, workspaceSlug, cycleId] + ); return ( cycleIssueStore.addIssueToCycle(workspaceSlug, cycleId, issues)} + viewId={cycleId?.toString()} + currentStore={EIssuesStoreType.CYCLE} + addIssuesToView={(issueIds: string[]) => { + if (!workspaceSlug || !projectId || !cycleId) throw new Error(); + return issues.addIssueToCycle(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), issueIds); + }} /> ); }); diff --git a/web/components/issues/issue-layouts/list/roots/draft-issue-root.tsx b/web/components/issues/issue-layouts/list/roots/draft-issue-root.tsx index 6049ec3bc4d..ef1edc8315f 100644 --- a/web/components/issues/issue-layouts/list/roots/draft-issue-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/draft-issue-root.tsx @@ -1,17 +1,16 @@ -import { FC } from "react"; +import { FC, useMemo } from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; // hooks -import { useMobxStore } from "lib/mobx/store-provider"; +import { useIssues } from "hooks/store"; // components import { ProjectIssueQuickActions } from "components/issues"; // types -import { IIssue } from "types"; +import { TIssue } from "@plane/types"; import { EIssueActions } from "../../types"; // constants import { BaseListRoot } from "../base-list-root"; -import { IProjectStore } from "store/project"; -import { EProjectStore } from "store/command-palette.store"; +import { EIssuesStoreType } from "constants/issue"; export const DraftIssueListLayout: FC = observer(() => { const router = useRouter(); @@ -20,31 +19,31 @@ export const DraftIssueListLayout: FC = observer(() => { if (!workspaceSlug || !projectId) return null; // store - const { projectDraftIssuesFilter: projectIssuesFilterStore, projectDraftIssues: projectIssuesStore } = useMobxStore(); - - const issueActions = { - [EIssueActions.UPDATE]: async (group_by: string | null, issue: IIssue) => { - if (!workspaceSlug || !projectId) return; - - await projectIssuesStore.updateIssue(workspaceSlug, projectId, issue.id, issue); - }, - [EIssueActions.DELETE]: async (group_by: string | null, issue: IIssue) => { - if (!workspaceSlug || !projectId) return; - - await projectIssuesStore.removeIssue(workspaceSlug, projectId, issue.id); - }, - }; - - const getProjects = (projectStore: IProjectStore) => projectStore.workspaceProjects; + const { issues, issuesFilter } = useIssues(EIssuesStoreType.DRAFT); + + const issueActions = useMemo( + () => ({ + [EIssueActions.UPDATE]: async (issue: TIssue) => { + if (!workspaceSlug || !projectId) return; + + await issues.updateIssue(workspaceSlug, projectId, issue.id, issue); + }, + [EIssueActions.DELETE]: async (issue: TIssue) => { + if (!workspaceSlug || !projectId) return; + + await issues.removeIssue(workspaceSlug, projectId, issue.id); + }, + }), + [issues, workspaceSlug, projectId] + ); return ( ); }); diff --git a/web/components/issues/issue-layouts/list/roots/module-root.tsx b/web/components/issues/issue-layouts/list/roots/module-root.tsx index 5d076a0cca4..947cfe55b1d 100644 --- a/web/components/issues/issue-layouts/list/roots/module-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/module-root.tsx @@ -1,66 +1,58 @@ -import React from "react"; +import React, { useMemo } from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; // mobx store -import { useMobxStore } from "lib/mobx/store-provider"; +import { useIssues } from "hooks/store"; // components import { ModuleIssueQuickActions } from "components/issues"; // types -import { IIssue } from "types"; +import { TIssue } from "@plane/types"; import { EIssueActions } from "../../types"; // constants import { BaseListRoot } from "../base-list-root"; -import { IProjectStore } from "store/project"; -import { EProjectStore } from "store/command-palette.store"; +import { EIssuesStoreType } from "constants/issue"; export interface IModuleListLayout {} export const ModuleListLayout: React.FC = observer(() => { const router = useRouter(); - const { workspaceSlug, moduleId } = router.query as { workspaceSlug: string; moduleId: string }; + const { workspaceSlug, projectId, moduleId } = router.query; - const { - moduleIssues: moduleIssueStore, - moduleIssuesFilter: moduleIssueFilterStore, - module: { fetchModuleDetails }, - } = useMobxStore(); + const { issues, issuesFilter } = useIssues(EIssuesStoreType.MODULE); - const issueActions = { - [EIssueActions.UPDATE]: async (group_by: string | null, issue: IIssue) => { - if (!workspaceSlug || !moduleId) return; + const issueActions = useMemo( + () => ({ + [EIssueActions.UPDATE]: async (issue: TIssue) => { + if (!workspaceSlug || !moduleId) return; - await moduleIssueStore.updateIssue(workspaceSlug, issue.project, issue.id, issue, moduleId); - fetchModuleDetails(workspaceSlug, issue.project, moduleId); - }, - [EIssueActions.DELETE]: async (group_by: string | null, issue: IIssue) => { - if (!workspaceSlug || !moduleId) return; + await issues.updateIssue(workspaceSlug.toString(), issue.project_id, issue.id, issue); + }, + [EIssueActions.DELETE]: async (issue: TIssue) => { + if (!workspaceSlug || !moduleId) return; - await moduleIssueStore.removeIssue(workspaceSlug, issue.project, issue.id, moduleId); - fetchModuleDetails(workspaceSlug, issue.project, moduleId); - }, - [EIssueActions.REMOVE]: async (group_by: string | null, issue: IIssue) => { - if (!workspaceSlug || !moduleId || !issue.bridge_id) return; + await issues.removeIssue(workspaceSlug.toString(), issue.project_id, issue.id); + }, + [EIssueActions.REMOVE]: async (issue: TIssue) => { + if (!workspaceSlug || !moduleId) return; - await moduleIssueStore.removeIssueFromModule(workspaceSlug, issue.project, moduleId, issue.id, issue.bridge_id); - fetchModuleDetails(workspaceSlug, issue.project, moduleId); - }, - }; - - const getProjects = (projectStore: IProjectStore) => { - if (!workspaceSlug) return null; - return projectStore?.projects[workspaceSlug] || null; - }; + await issues.removeIssueFromModule(workspaceSlug.toString(), issue.project_id, moduleId.toString(), issue.id); + }, + }), + [issues, workspaceSlug, moduleId] + ); return ( moduleIssueStore.addIssueToModule(workspaceSlug, moduleId, issues)} + viewId={moduleId?.toString()} + currentStore={EIssuesStoreType.MODULE} + addIssuesToView={(issueIds: string[]) => { + if (!workspaceSlug || !projectId || !moduleId) throw new Error(); + return issues.addIssueToModule(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), issueIds); + }} /> ); }); diff --git a/web/components/issues/issue-layouts/list/roots/profile-issues-root.tsx b/web/components/issues/issue-layouts/list/roots/profile-issues-root.tsx index eedf7ae8165..55db4cd71ff 100644 --- a/web/components/issues/issue-layouts/list/roots/profile-issues-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/profile-issues-root.tsx @@ -1,64 +1,58 @@ -import { FC } from "react"; +import { FC, useMemo } from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; // hooks -import { useMobxStore } from "lib/mobx/store-provider"; +import { useIssues, useUser } from "hooks/store"; // components import { ProjectIssueQuickActions } from "components/issues"; // types -import { IIssue } from "types"; +import { TIssue } from "@plane/types"; import { EIssueActions } from "../../types"; // constants import { BaseListRoot } from "../base-list-root"; -import { IProjectStore } from "store/project"; -import { EProjectStore } from "store/command-palette.store"; -import { EUserWorkspaceRoles } from "constants/workspace"; +import { EUserProjectRoles } from "constants/project"; +import { EIssuesStoreType } from "constants/issue"; export const ProfileIssuesListLayout: FC = observer(() => { + // router const router = useRouter(); const { workspaceSlug, userId } = router.query as { workspaceSlug: string; userId: string }; + // store hooks + const { issues, issuesFilter } = useIssues(EIssuesStoreType.PROFILE); - // store const { - workspaceProfileIssuesFilter: profileIssueFiltersStore, - workspaceProfileIssues: profileIssuesStore, - workspaceMember: { currentWorkspaceUserProjectsRole }, - } = useMobxStore(); - - const issueActions = { - [EIssueActions.UPDATE]: async (group_by: string | null, issue: IIssue) => { - if (!workspaceSlug || !userId) return; - - await profileIssuesStore.updateIssue(workspaceSlug, userId, issue.id, issue); - }, - [EIssueActions.DELETE]: async (group_by: string | null, issue: IIssue) => { - if (!workspaceSlug || !userId) return; - - await profileIssuesStore.removeIssue(workspaceSlug, issue.project, issue.id, userId); - }, - }; - - const getProjects = (projectStore: IProjectStore) => projectStore.workspaceProjects; + membership: { currentWorkspaceAllProjectsRole }, + } = useUser(); + + const issueActions = useMemo( + () => ({ + [EIssueActions.UPDATE]: async (issue: TIssue) => { + if (!workspaceSlug || !userId) return; + + await issues.updateIssue(workspaceSlug, userId, issue.id, issue); + }, + [EIssueActions.DELETE]: async (issue: TIssue) => { + if (!workspaceSlug || !userId) return; + + await issues.removeIssue(workspaceSlug, issue.project_id, issue.id, userId); + }, + }), + [issues, workspaceSlug, userId] + ); const canEditPropertiesBasedOnProject = (projectId: string) => { - const currentProjectRole = currentWorkspaceUserProjectsRole && currentWorkspaceUserProjectsRole[projectId]; + const currentProjectRole = currentWorkspaceAllProjectsRole && currentWorkspaceAllProjectsRole[projectId]; - console.log( - projectId, - currentWorkspaceUserProjectsRole, - !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER - ); - return !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER; + return !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; }; return ( ); diff --git a/web/components/issues/issue-layouts/list/roots/project-root.tsx b/web/components/issues/issue-layouts/list/roots/project-root.tsx index 0d23f765696..b99b431c86c 100644 --- a/web/components/issues/issue-layouts/list/roots/project-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/project-root.tsx @@ -1,17 +1,16 @@ -import { FC } from "react"; +import { FC, useMemo } from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; // hooks -import { useMobxStore } from "lib/mobx/store-provider"; +import { useIssues } from "hooks/store"; // components import { ProjectIssueQuickActions } from "components/issues"; // types -import { IIssue } from "types"; +import { TIssue } from "@plane/types"; import { EIssueActions } from "../../types"; // constants import { BaseListRoot } from "../base-list-root"; -import { IProjectStore } from "store/project"; -import { EProjectStore } from "store/command-palette.store"; +import { EIssuesStoreType } from "constants/issue"; export const ListLayout: FC = observer(() => { const router = useRouter(); @@ -20,31 +19,32 @@ export const ListLayout: FC = observer(() => { if (!workspaceSlug || !projectId) return null; // store - const { projectIssuesFilter: projectIssuesFilterStore, projectIssues: projectIssuesStore } = useMobxStore(); - - const issueActions = { - [EIssueActions.UPDATE]: async (group_by: string | null, issue: IIssue) => { - if (!workspaceSlug || !projectId) return; - - await projectIssuesStore.updateIssue(workspaceSlug, projectId, issue.id, issue); - }, - [EIssueActions.DELETE]: async (group_by: string | null, issue: IIssue) => { - if (!workspaceSlug || !projectId) return; - - await projectIssuesStore.removeIssue(workspaceSlug, projectId, issue.id); - }, - }; - - const getProjects = (projectStore: IProjectStore) => projectStore.workspaceProjects; + const { issuesFilter, issues } = useIssues(EIssuesStoreType.PROJECT); + + const issueActions = useMemo( + () => ({ + [EIssueActions.UPDATE]: async (issue: TIssue) => { + if (!workspaceSlug || !projectId) return; + + await issues.updateIssue(workspaceSlug, projectId, issue.id, issue); + }, + [EIssueActions.DELETE]: async (issue: TIssue) => { + if (!workspaceSlug || !projectId) return; + + await issues.removeIssue(workspaceSlug, projectId, issue.id); + }, + }), + // eslint-disable-next-line react-hooks/exhaustive-deps + [issues] + ); return ( ); }); diff --git a/web/components/issues/issue-layouts/list/roots/project-view-root.tsx b/web/components/issues/issue-layouts/list/roots/project-view-root.tsx index 52fa1a759db..8139307e6d0 100644 --- a/web/components/issues/issue-layouts/list/roots/project-view-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/project-view-root.tsx @@ -1,53 +1,50 @@ -import React from "react"; +import React, { useMemo } from "react"; import { observer } from "mobx-react-lite"; - // store -import { useMobxStore } from "lib/mobx/store-provider"; -import { RootStore } from "store/root"; +import { useIssues } from "hooks/store"; // constants import { useRouter } from "next/router"; import { EIssueActions } from "../../types"; -import { IProjectStore } from "store/project"; -import { IIssue } from "types"; +import { TIssue } from "@plane/types"; // components import { BaseListRoot } from "../base-list-root"; import { ProjectIssueQuickActions } from "../../quick-action-dropdowns"; -import { EProjectStore } from "store/command-palette.store"; +import { EIssuesStoreType } from "constants/issue"; export interface IViewListLayout {} export const ProjectViewListLayout: React.FC = observer(() => { - const { viewIssues: projectViewIssueStore, viewIssuesFilter: projectViewIssueFilterStore }: RootStore = - useMobxStore(); + // store + const { issuesFilter, issues } = useIssues(EIssuesStoreType.PROJECT_VIEW); const router = useRouter(); const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string }; if (!workspaceSlug || !projectId) return null; - const issueActions = { - [EIssueActions.UPDATE]: async (group_by: string | null, issue: IIssue) => { - if (!workspaceSlug || !projectId) return; - - await projectViewIssueStore.updateIssue(workspaceSlug, projectId, issue.id, issue); - }, - [EIssueActions.DELETE]: async (group_by: string | null, issue: IIssue) => { - if (!workspaceSlug || !projectId) return; + const issueActions = useMemo( + () => ({ + [EIssueActions.UPDATE]: async (issue: TIssue) => { + if (!workspaceSlug || !projectId) return; - await projectViewIssueStore.removeIssue(workspaceSlug, projectId, issue.id); - }, - }; + await issues.updateIssue(workspaceSlug, projectId, issue.id, issue); + }, + [EIssueActions.DELETE]: async (issue: TIssue) => { + if (!workspaceSlug || !projectId) return; - const getProjects = (projectStore: IProjectStore) => projectStore.workspaceProjects; + await issues.removeIssue(workspaceSlug, projectId, issue.id); + }, + }), + [issues, workspaceSlug, projectId] + ); return ( ); }); diff --git a/web/components/issues/issue-layouts/properties/all-properties.tsx b/web/components/issues/issue-layouts/properties/all-properties.tsx new file mode 100644 index 00000000000..fe05d834b18 --- /dev/null +++ b/web/components/issues/issue-layouts/properties/all-properties.tsx @@ -0,0 +1,207 @@ +import { observer } from "mobx-react-lite"; +import { CalendarCheck2, CalendarClock, Layers, Link, Paperclip } from "lucide-react"; +// hooks +import { useLabel } from "hooks/store"; +// components +import { IssuePropertyLabels } from "../properties/labels"; +import { Tooltip } from "@plane/ui"; +import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC"; +import { + DateDropdown, + EstimateDropdown, + PriorityDropdown, + ProjectMemberDropdown, + StateDropdown, +} from "components/dropdowns"; +// helpers +import { renderFormattedPayloadDate } from "helpers/date-time.helper"; +// types +import { TIssue, IIssueDisplayProperties, TIssuePriorities } from "@plane/types"; + +export interface IIssueProperties { + issue: TIssue; + handleIssues: (issue: TIssue) => void; + displayProperties: IIssueDisplayProperties | undefined; + isReadOnly: boolean; + className: string; +} + +export const IssueProperties: React.FC = observer((props) => { + const { issue, handleIssues, displayProperties, isReadOnly, className } = props; + const { labelMap } = useLabel(); + + const handleState = (stateId: string) => { + handleIssues({ ...issue, state_id: stateId }); + }; + + const handlePriority = (value: TIssuePriorities) => { + handleIssues({ ...issue, priority: value }); + }; + + const handleLabel = (ids: string[]) => { + handleIssues({ ...issue, label_ids: ids }); + }; + + const handleAssignee = (ids: string[]) => { + handleIssues({ ...issue, assignee_ids: ids }); + }; + + const handleStartDate = (date: Date | null) => { + handleIssues({ ...issue, start_date: date ? renderFormattedPayloadDate(date) : null }); + }; + + const handleTargetDate = (date: Date | null) => { + handleIssues({ ...issue, target_date: date ? renderFormattedPayloadDate(date) : null }); + }; + + const handleEstimate = (value: number | null) => { + handleIssues({ ...issue, estimate_point: value }); + }; + + if (!displayProperties) return null; + + const defaultLabelOptions = issue?.label_ids?.map((id) => labelMap[id]) || []; + + return ( +
+ {/* basic properties */} + {/* state */} + +
+ +
+
+ + {/* priority */} + +
+ +
+
+ + {/* label */} + + + + + + {/* start date */} + +
+ } + placeholder="Start date" + buttonVariant={issue.start_date ? "border-with-text" : "border-without-text"} + disabled={isReadOnly} + /> +
+
+ + {/* target/due date */} + +
+ } + placeholder="Due date" + buttonVariant={issue.target_date ? "border-with-text" : "border-without-text"} + disabled={isReadOnly} + /> +
+
+ + {/* assignee */} + +
+ 0 ? "transparent-without-text" : "border-without-text"} + buttonClassName={issue.assignee_ids.length > 0 ? "hover:bg-transparent px-0" : ""} + /> +
+
+ + {/* estimates */} + +
+ +
+
+ + {/* extra render properties */} + {/* sub-issues */} + + +
+ +
{issue.sub_issues_count}
+
+
+
+ + {/* attachments */} + + +
+ +
{issue.attachment_count}
+
+
+
+ + {/* link */} + + +
+ +
{issue.link_count}
+
+
+
+
+ ); +}); diff --git a/web/components/issues/issue-layouts/properties/assignee.tsx b/web/components/issues/issue-layouts/properties/assignee.tsx deleted file mode 100644 index 01dec9b8379..00000000000 --- a/web/components/issues/issue-layouts/properties/assignee.tsx +++ /dev/null @@ -1,204 +0,0 @@ -import { Fragment, useState } from "react"; -import { observer } from "mobx-react-lite"; -import { useMobxStore } from "lib/mobx/store-provider"; -import { usePopper } from "react-popper"; -import { Combobox } from "@headlessui/react"; -import { Check, ChevronDown, CircleUser, Search } from "lucide-react"; -// ui -import { Avatar, AvatarGroup, Tooltip } from "@plane/ui"; -// types -import { Placement } from "@popperjs/core"; -import { IProjectMember } from "types"; - -export interface IIssuePropertyAssignee { - projectId: string | null; - value: string[] | string; - defaultOptions?: any; - onChange: (data: string[]) => void; - disabled?: boolean; - hideDropdownArrow?: boolean; - className?: string; - buttonClassName?: string; - optionsClassName?: string; - placement?: Placement; - multiple?: true; - noLabelBorder?: boolean; -} - -export const IssuePropertyAssignee: React.FC = observer((props) => { - const { - projectId, - value, - defaultOptions = [], - onChange, - disabled = false, - hideDropdownArrow = false, - className, - buttonClassName, - optionsClassName, - placement, - multiple = false, - } = props; - // store - const { - workspace: workspaceStore, - projectMember: { members: _members, fetchProjectMembers }, - } = useMobxStore(); - const workspaceSlug = workspaceStore?.workspaceSlug; - // states - const [query, setQuery] = useState(""); - const [referenceElement, setReferenceElement] = useState(null); - const [popperElement, setPopperElement] = useState(null); - const [isLoading, setIsLoading] = useState(false); - - const getProjectMembers = () => { - setIsLoading(true); - if (workspaceSlug && projectId) fetchProjectMembers(workspaceSlug, projectId).then(() => setIsLoading(false)); - }; - - const updatedDefaultOptions: IProjectMember[] = - defaultOptions.map((member: any) => ({ member: { ...member } })) ?? []; - const projectMembers = projectId && _members[projectId] ? _members[projectId] : updatedDefaultOptions; - - const options = projectMembers?.map((member) => ({ - value: member.member.id, - query: member.member.display_name, - content: ( -
- - {member.member.display_name} -
- ), - })); - - const filteredOptions = - query === "" ? options : options?.filter((option) => option.query.toLowerCase().includes(query.toLowerCase())); - - const getTooltipContent = (): string => { - if (!value || value.length === 0) return "No Assignee"; - - // if multiple assignees - if (Array.isArray(value)) { - const assignees = projectMembers?.filter((m) => value.includes(m.member.id)); - - if (!assignees || assignees.length === 0) return "No Assignee"; - - // if only one assignee in list - if (assignees.length === 1) { - return "1 assignee"; - } else return `${assignees.length} assignees`; - } - - // if single assignee - const assignee = projectMembers?.find((m) => m.member.id === value)?.member; - - if (!assignee) return "No Assignee"; - - // if assignee not null & not list - return "1 assignee"; - }; - - const label = ( - -
- {value && value.length > 0 && Array.isArray(value) ? ( - - {value.map((assigneeId) => { - const member = projectMembers?.find((m) => m.member.id === assigneeId)?.member; - if (!member) return null; - return ; - })} - - ) : ( - - - - )} -
-
- ); - - const { styles, attributes } = usePopper(referenceElement, popperElement, { - placement: placement ?? "bottom-start", - modifiers: [ - { - name: "preventOverflow", - options: { - padding: 12, - }, - }, - ], - }); - - const comboboxProps: any = { value, onChange, disabled }; - if (multiple) comboboxProps.multiple = true; - - return ( - - - - - -
-
- - setQuery(e.target.value)} - placeholder="Search" - displayValue={(assigned: any) => assigned?.name} - /> -
-
- {isLoading ? ( -

Loading...

- ) : filteredOptions && filteredOptions.length > 0 ? ( - filteredOptions.map((option) => ( - - `flex cursor-pointer select-none items-center justify-between gap-2 truncate rounded px-1 py-1.5 ${ - active && !selected ? "bg-custom-background-80" : "" - } ${selected ? "text-custom-text-100" : "text-custom-text-200"}` - } - onClick={(e) => e.stopPropagation()} - > - {({ selected }) => ( - <> - {option.content} - {selected && } - - )} - - )) - ) : ( - -

No matching results

-
- )} -
-
-
-
- ); -}); diff --git a/web/components/issues/issue-layouts/properties/date.tsx b/web/components/issues/issue-layouts/properties/date.tsx deleted file mode 100644 index b66d2e5b6ed..00000000000 --- a/web/components/issues/issue-layouts/properties/date.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import React from "react"; -// headless ui -import { Popover } from "@headlessui/react"; -// lucide icons -import { CalendarCheck2, CalendarClock, X } from "lucide-react"; -// react date picker -import DatePicker from "react-datepicker"; -// mobx -import { observer } from "mobx-react-lite"; -// components -import { Tooltip } from "@plane/ui"; -// hooks -import useDynamicDropdownPosition from "hooks/use-dynamic-dropdown"; -// helpers -import { renderFormattedPayloadDate, renderFormattedDate } from "helpers/date-time.helper"; - -export interface IIssuePropertyDate { - value: string | null; - onChange: (date: string | null) => void; - disabled?: boolean; - type: "start_date" | "target_date"; -} - -const DATE_OPTIONS = { - start_date: { - key: "start_date", - placeholder: "Start date", - icon: CalendarClock, - }, - target_date: { - key: "target_date", - placeholder: "Target date", - icon: CalendarCheck2, - }, -}; - -export const IssuePropertyDate: React.FC = observer((props) => { - const { value, onChange, disabled, type } = props; - - const dropdownBtn = React.useRef(null); - const dropdownOptions = React.useRef(null); - - const [isOpen, setIsOpen] = React.useState(false); - - useDynamicDropdownPosition(isOpen, () => setIsOpen(false), dropdownBtn, dropdownOptions); - - const dateOptionDetails = DATE_OPTIONS[type]; - - return ( - - {({ open }) => { - if (open) { - if (!isOpen) setIsOpen(true); - } else if (isOpen) setIsOpen(false); - - return ( - <> - e.stopPropagation()} - disabled={disabled} - > - -
-
- - {value && ( - <> -
{value}
-
{ - if (onChange) onChange(null); - }} - > - -
- - )} -
-
-
-
- -
- - {({ close }) => ( - { - e?.stopPropagation(); - if (onChange && val) { - onChange(renderFormattedPayloadDate(val)); - close(); - } - }} - dateFormat="dd-MM-yyyy" - calendarClassName="h-full" - inline - /> - )} - -
- - ); - }} -
- ); -}); diff --git a/web/components/issues/issue-layouts/properties/estimates.tsx b/web/components/issues/issue-layouts/properties/estimates.tsx deleted file mode 100644 index e3f61795894..00000000000 --- a/web/components/issues/issue-layouts/properties/estimates.tsx +++ /dev/null @@ -1,177 +0,0 @@ -import { Fragment, useState } from "react"; -import { usePopper } from "react-popper"; -import { observer } from "mobx-react-lite"; -import { Combobox } from "@headlessui/react"; -import { Check, ChevronDown, Search, Triangle } from "lucide-react"; -// ui -import { Tooltip } from "@plane/ui"; -// types -import { Placement } from "@popperjs/core"; -import { useMobxStore } from "lib/mobx/store-provider"; - -export interface IIssuePropertyEstimates { - view?: "profile" | "workspace" | "project"; - projectId: string | null; - value: number | null; - onChange: (value: number | null) => void; - disabled?: boolean; - hideDropdownArrow?: boolean; - className?: string; - buttonClassName?: string; - optionsClassName?: string; - placement?: Placement; -} - -export const IssuePropertyEstimates: React.FC = observer((props) => { - const { - projectId, - value, - onChange, - disabled, - hideDropdownArrow = false, - className = "", - buttonClassName = "", - optionsClassName = "", - placement, - } = props; - - const [query, setQuery] = useState(""); - - const [referenceElement, setReferenceElement] = useState(null); - const [popperElement, setPopperElement] = useState(null); - - const { styles, attributes } = usePopper(referenceElement, popperElement, { - placement: placement ?? "bottom-start", - modifiers: [ - { - name: "preventOverflow", - options: { - padding: 12, - }, - }, - ], - }); - - const { - project: { project_details }, - projectEstimates: { projectEstimates }, - } = useMobxStore(); - - const projectDetails = projectId ? project_details[projectId] : null; - const isEstimateEnabled = projectDetails?.estimate !== null; - const estimates = projectEstimates; - const estimatePoints = - projectDetails && isEstimateEnabled ? estimates?.find((e) => e.id === projectDetails.estimate)?.points : null; - - const options: { value: number | null; query: string; content: any }[] | undefined = (estimatePoints ?? []).map( - (estimate) => ({ - value: estimate.key, - query: estimate.value, - content: ( -
- - {estimate.value} -
- ), - }) - ); - options?.unshift({ - value: null, - query: "none", - content: ( -
- - None -
- ), - }); - - const filteredOptions = - query === "" ? options : options?.filter((option) => option.query.toLowerCase().includes(query.toLowerCase())); - - const selectedEstimate = estimatePoints?.find((e) => e.key === value); - const label = ( - -
- - {selectedEstimate?.value ?? "None"} -
-
- ); - - if (!isEstimateEnabled) return null; - - return ( - onChange(val as number | null)} - disabled={disabled} - > - - - - -
-
- - setQuery(e.target.value)} - placeholder="Search" - displayValue={(assigned: any) => assigned?.name} - /> -
-
- {filteredOptions ? ( - filteredOptions.length > 0 ? ( - filteredOptions.map((option) => ( - - `flex cursor-pointer select-none items-center justify-between gap-2 truncate rounded px-1 py-1.5 ${ - active ? "bg-custom-background-80" : "" - } ${selected ? "text-custom-text-100" : "text-custom-text-200"}` - } - onClick={(e) => e.stopPropagation()} - > - {({ selected }) => ( - <> - {option.content} - {selected && } - - )} - - )) - ) : ( - -

No matching results

-
- ) - ) : ( -

Loading...

- )} -
-
-
-
- ); -}); diff --git a/web/components/issues/issue-layouts/properties/index.ts b/web/components/issues/issue-layouts/properties/index.ts new file mode 100644 index 00000000000..95f3ce21fd5 --- /dev/null +++ b/web/components/issues/issue-layouts/properties/index.ts @@ -0,0 +1 @@ +export * from "./labels"; diff --git a/web/components/issues/issue-layouts/properties/index.tsx b/web/components/issues/issue-layouts/properties/index.tsx deleted file mode 100644 index 3e2e2acd67d..00000000000 --- a/web/components/issues/issue-layouts/properties/index.tsx +++ /dev/null @@ -1,6 +0,0 @@ -export * from "./assignee"; -export * from "./date"; -export * from "./estimates"; -export * from "./labels"; -export * from "./priority"; -export * from "./state"; diff --git a/web/components/issues/issue-layouts/properties/labels.tsx b/web/components/issues/issue-layouts/properties/labels.tsx index d0045c3d447..b22083c077b 100644 --- a/web/components/issues/issue-layouts/properties/labels.tsx +++ b/web/components/issues/issue-layouts/properties/labels.tsx @@ -1,16 +1,15 @@ import { Fragment, useState } from "react"; import { observer } from "mobx-react-lite"; -import { useMobxStore } from "lib/mobx/store-provider"; -// hooks import { usePopper } from "react-popper"; +import { Check, ChevronDown, Search, Tags } from "lucide-react"; +// hooks +import { useApplication, useLabel } from "hooks/store"; // components import { Combobox } from "@headlessui/react"; import { Tooltip } from "@plane/ui"; -import { Check, ChevronDown, Search, Tags } from "lucide-react"; // types import { Placement } from "@popperjs/core"; -import { RootStore } from "store/root"; -import { IIssueLabel } from "types"; +import { IIssueLabel } from "@plane/types"; export interface IIssuePropertyLabels { projectId: string | null; @@ -44,18 +43,19 @@ export const IssuePropertyLabels: React.FC = observer((pro noLabelBorder = false, placeholderText, } = props; - - const { - workspace: workspaceStore, - projectLabel: { fetchProjectLabels, labels }, - }: RootStore = useMobxStore(); - const workspaceSlug = workspaceStore?.workspaceSlug; - + // states const [query, setQuery] = useState(""); - + // popper-js refs const [referenceElement, setReferenceElement] = useState(null); const [popperElement, setPopperElement] = useState(null); const [isLoading, setIsLoading] = useState(false); + // store hooks + const { + router: { workspaceSlug }, + } = useApplication(); + const { + project: { fetchProjectLabels, projectLabels: storeLabels }, + } = useLabel(); const fetchLabels = () => { setIsLoading(true); @@ -65,7 +65,6 @@ export const IssuePropertyLabels: React.FC = observer((pro if (!value) return null; let projectLabels: IIssueLabel[] = defaultOptions; - const storeLabels = projectId && labels ? labels[projectId] : []; if (storeLabels && storeLabels.length > 0) projectLabels = storeLabels; const options = projectLabels.map((label) => ({ @@ -107,7 +106,7 @@ export const IssuePropertyLabels: React.FC = observer((pro {projectLabels ?.filter((l) => value.includes(l.id)) .map((label) => ( - +
= observer((pro ? "cursor-pointer" : "cursor-pointer hover:bg-custom-background-80" } ${buttonClassName}`} - onClick={(e) => { - e.stopPropagation(); - !storeLabels && fetchLabels(); - }} + onClick={() => !storeLabels && fetchLabels()} > {label} {!hideDropdownArrow && !disabled &&