From 804b7d8663eca99c0c22378a2ba7ccb82870fd6d 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 | 305 ++-- .../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 | 65 +- .../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 | 100 +- 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 | 77 +- web/components/cycles/form.tsx | 79 +- web/components/cycles/gantt-chart/blocks.tsx | 2 +- .../cycles/gantt-chart/cycles-list-layout.tsx | 63 +- web/components/cycles/modal.tsx | 32 +- web/components/cycles/sidebar.tsx | 65 +- .../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 | 115 +- 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 | 258 +-- 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 | 225 ++- .../issue-layouts/kanban/blocks-list.tsx | 74 +- .../issues/issue-layouts/kanban/default.tsx | 473 ++---- .../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 | 97 +- .../kanban/roots/draft-issue-root.tsx | 43 +- .../kanban/roots/module-root.tsx | 98 +- .../kanban/roots/profile-issues-root.tsx | 54 +- .../kanban/roots/project-root.tsx | 70 +- .../kanban/roots/project-view-root.tsx | 72 +- .../issues/issue-layouts/kanban/swimlanes.tsx | 572 ++----- .../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 | 43 +- .../issue-layouts/list/roots/module-root.tsx | 70 +- .../list/roots/profile-issues-root.tsx | 62 +- .../issue-layouts/list/roots/project-root.tsx | 44 +- .../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 | 64 +- 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 | 180 +-- .../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 | 29 +- 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 | 84 +- 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 | 21 +- .../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 | 148 +- ...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} | 134 +- 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 | 163 +- web/store/issue/issue_detail.store.ts | 64 +- 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 | 533 ++++--- 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 | 1416 +++++++++-------- 940 files changed, 26378 insertions(+), 34411 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 5b5f958d3..0f85e940c 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 b96422501..4e88597c7 100644 --- a/apiserver/plane/api/serializers/base.py +++ b/apiserver/plane/api/serializers/base.py @@ -97,7 +97,7 @@ class BaseSerializer(serializers.ModelSerializer): 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 c406453b7..4e0c12fe5 100644 --- a/apiserver/plane/app/serializers/__init__.py +++ b/apiserver/plane/app/serializers/__init__.py @@ -17,6 +17,7 @@ from .workspace import ( WorkspaceThemeSerializer, WorkspaceMemberAdminSerializer, WorkspaceMemberMeSerializer, + WorkspaceUserPropertiesSerializer, ) from .project import ( ProjectSerializer, @@ -31,6 +32,7 @@ from .project import ( ProjectDeployBoardSerializer, ProjectMemberAdminSerializer, ProjectPublicMemberSerializer, + ProjectMemberRoleSerializer, ) from .state import StateSerializer, StateLiteSerializer from .view import GlobalViewSerializer, IssueViewSerializer, IssueViewFavoriteSerializer @@ -39,6 +41,7 @@ from .cycle import ( CycleIssueSerializer, CycleFavoriteSerializer, CycleWriteSerializer, + CycleUserPropertiesSerializer, ) from .asset import FileAssetSerializer from .issue import ( @@ -61,6 +64,8 @@ from .issue import ( IssueRelationSerializer, RelatedIssueSerializer, IssuePublicSerializer, + IssueRelationLiteSerializer, + ) from .module import ( @@ -69,6 +74,7 @@ from .module import ( 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 89c9725d9..f67f5cf52 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 @@ class DynamicBaseSerializer(BaseSerializer): 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 63abf3a03..f0ee8f9da 100644 --- a/apiserver/plane/app/serializers/cycle.py +++ b/apiserver/plane/app/serializers/cycle.py @@ -7,7 +7,7 @@ from .user import UserLiteSerializer 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 CycleFavoriteSerializer(BaseSerializer): "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 f52a90660..cdc2646dd 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 b13d03e35..6d39f1760 100644 --- a/apiserver/plane/app/serializers/issue.py +++ b/apiserver/plane/app/serializers/issue.py @@ -278,17 +278,28 @@ class IssueLabelSerializer(BaseSerializer): ] +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 IssueRelationSerializer(BaseSerializer): ] 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 IssueStateSerializer(DynamicBaseSerializer): 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 48f773b0f..b38d05b2c 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 @@ from plane.db.models import ( ModuleIssue, ModuleLink, ModuleFavorite, + ModuleUserProperties, ) @@ -159,7 +160,7 @@ class ModuleLinkSerializer(BaseSerializer): 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 ModuleFavoriteSerializer(BaseSerializer): "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 aef715e33..b3122962b 100644 --- a/apiserver/plane/app/serializers/project.py +++ b/apiserver/plane/app/serializers/project.py @@ -159,6 +159,11 @@ class ProjectMemberAdminSerializer(BaseSerializer): 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 e7502609a..db44a2fc0 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 @@ class GlobalViewSerializer(BaseSerializer): 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 IssueViewFavoriteSerializer(BaseSerializer): "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 f0ad4b4ab..fe014f364 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 @@ from plane.db.models import ( 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 WorkspaceLiteSerializer(BaseSerializer): -class WorkSpaceMemberSerializer(BaseSerializer): +class WorkSpaceMemberSerializer(DynamicBaseSerializer): member = UserLiteSerializer(read_only=True) workspace = WorkspaceLiteSerializer(read_only=True) @@ -78,7 +79,7 @@ class WorkspaceMemberMeSerializer(BaseSerializer): fields = "__all__" -class WorkspaceMemberAdminSerializer(BaseSerializer): +class WorkspaceMemberAdminSerializer(DynamicBaseSerializer): member = UserAdminLiteSerializer(read_only=True) workspace = WorkspaceLiteSerializer(read_only=True) @@ -161,3 +162,13 @@ class WorkspaceThemeSerializer(BaseSerializer): "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 46e6a5e84..5fef437c6 100644 --- a/apiserver/plane/app/urls/cycle.py +++ b/apiserver/plane/app/urls/cycle.py @@ -7,6 +7,7 @@ from plane.app.views import ( CycleDateCheckEndpoint, CycleFavoriteViewSet, TransferCycleIssueEndpoint, + CycleUserPropertiesEndpoint, ) @@ -44,7 +45,7 @@ urlpatterns = [ name="project-issue-cycle", ), path( - "workspaces//projects//cycles//cycle-issues//", + "workspaces//projects//cycles//cycle-issues//", CycleIssueViewSet.as_view( { "get": "retrieve", @@ -84,4 +85,9 @@ urlpatterns = [ 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 16ea40b21..e9ec4e335 100644 --- a/apiserver/plane/app/urls/inbox.py +++ b/apiserver/plane/app/urls/inbox.py @@ -40,7 +40,7 @@ urlpatterns = [ 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 971fbc395..234c2824d 100644 --- a/apiserver/plane/app/urls/issue.py +++ b/apiserver/plane/app/urls/issue.py @@ -235,7 +235,7 @@ urlpatterns = [ ## 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 @@ urlpatterns = [ "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 5507b3a37..961fff0db 100644 --- a/apiserver/plane/app/urls/module.py +++ b/apiserver/plane/app/urls/module.py @@ -7,6 +7,7 @@ from plane.app.views import ( ModuleLinkViewSet, ModuleFavoriteViewSet, BulkImportModulesEndpoint, + ModuleUserPropertiesEndpoint ) @@ -44,7 +45,7 @@ urlpatterns = [ name="project-module-issues", ), path( - "workspaces//projects//modules//module-issues//", + "workspaces//projects//modules//module-issues//", ModuleIssueViewSet.as_view( { "get": "retrieve", @@ -101,4 +102,9 @@ urlpatterns = [ 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 3d45b627a..f78f17869 100644 --- a/apiserver/plane/app/urls/views.py +++ b/apiserver/plane/app/urls/views.py @@ -5,7 +5,7 @@ from plane.app.views import ( IssueViewViewSet, GlobalViewViewSet, GlobalViewIssuesViewSet, - IssueViewFavoriteViewSet, + IssueViewFavoriteViewSet, ) diff --git a/apiserver/plane/app/urls/workspace.py b/apiserver/plane/app/urls/workspace.py index 2c3638842..cc78881f9 100644 --- a/apiserver/plane/app/urls/workspace.py +++ b/apiserver/plane/app/urls/workspace.py @@ -18,6 +18,8 @@ from plane.app.views import ( WorkspaceUserProfileEndpoint, WorkspaceUserProfileIssuesEndpoint, WorkspaceLabelsEndpoint, + WorkspaceProjectMemberEndpoint, + WorkspaceUserPropertiesEndpoint, ) @@ -92,6 +94,11 @@ urlpatterns = [ 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 @@ urlpatterns = [ 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 c122dce9f..520a3fd38 100644 --- a/apiserver/plane/app/views/__init__.py +++ b/apiserver/plane/app/views/__init__.py @@ -45,6 +45,8 @@ from .workspace import ( WorkspaceUserProfileEndpoint, WorkspaceUserProfileIssuesEndpoint, WorkspaceLabelsEndpoint, + WorkspaceProjectMemberEndpoint, + WorkspaceUserPropertiesEndpoint, ) from .state import StateViewSet from .view import ( @@ -59,6 +61,7 @@ from .cycle import ( CycleDateCheckEndpoint, CycleFavoriteViewSet, TransferCycleIssueEndpoint, + CycleUserPropertiesEndpoint, ) from .asset import FileAssetEndpoint, UserAssetsEndpoint, FileAssetViewSet from .issue import ( @@ -103,6 +106,7 @@ from .module import ( 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 32449597b..5bd79cb96 100644 --- a/apiserver/plane/app/views/base.py +++ b/apiserver/plane/app/views/base.py @@ -159,6 +159,21 @@ class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator): 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 @@ class BaseAPIView(TimezoneMixin, APIView, BasePaginator): @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 02f259de3..73741b983 100644 --- a/apiserver/plane/app/views/cycle.py +++ b/apiserver/plane/app/views/cycle.py @@ -14,7 +14,7 @@ from django.db.models import ( Case, When, Value, - CharField + CharField, ) from django.core import serializers from django.utils import timezone @@ -33,8 +33,9 @@ from plane.app.serializers import ( 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 @@ from plane.db.models import ( IssueLink, IssueAttachment, Label, + CycleUserProperties, ) from plane.bgtasks.issue_activites_task import issue_activity from plane.utils.grouper import group_results @@ -164,23 +166,18 @@ class CycleViewSet(WebhookMixin, BaseViewSet): .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 @@ class CycleViewSet(WebhookMixin, BaseViewSet): 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 @@ class CycleViewSet(WebhookMixin, BaseViewSet): 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 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet): .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 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet): .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 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet): 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 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet): } ), 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 @@ class TransferCycleIssueEndpoint(BaseAPIView): ) 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 331ee2175..32f38d97c 100644 --- a/apiserver/plane/app/views/inbox.py +++ b/apiserver/plane/app/views/inbox.py @@ -107,7 +107,6 @@ class InboxIssueViewSet(BaseViewSet): 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 @@ class InboxIssueViewSet(BaseViewSet): 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 @@ class InboxIssueViewSet(BaseViewSet): 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 @@ class InboxIssueViewSet(BaseViewSet): 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 d489629ba..6c88ef090 100644 --- a/apiserver/plane/app/views/issue.py +++ b/apiserver/plane/app/views/issue.py @@ -52,6 +52,7 @@ from plane.app.serializers import ( IssueRelationSerializer, RelatedIssueSerializer, IssuePublicSerializer, + IssueRelationLiteSerializer, ) from plane.app.permissions import ( ProjectEntityPermission, @@ -129,22 +130,6 @@ class IssueViewSet(WebhookMixin, BaseViewSet): 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 @@ class IssueViewSet(WebhookMixin, BaseViewSet): .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 @@ class IssueViewSet(WebhookMixin, BaseViewSet): 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 @@ class IssueViewSet(WebhookMixin, BaseViewSet): .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 @@ class SubIssuesEndpoint(BaseAPIView): .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 @@ class SubIssuesEndpoint(BaseAPIView): item["state_group"]: item["state_count"] for item in state_distribution } - serializer = IssueLiteSerializer( + serializer = IssueSerializer( sub_issues, many=True, ) @@ -775,7 +793,7 @@ class SubIssuesEndpoint(BaseAPIView): ] 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 @@ class IssueArchiveViewSet(BaseViewSet): 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 @@ class IssueRelationViewSet(BaseViewSet): .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 @@ class IssueRelationViewSet(BaseViewSet): 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 @@ class IssueRelationViewSet(BaseViewSet): 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 @@ class IssueRelationViewSet(BaseViewSet): 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 @@ class IssueDraftViewSet(BaseViewSet): 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 @@ class IssueDraftViewSet(BaseViewSet): 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 a8a8655c3..6baf23121 100644 --- a/apiserver/plane/app/views/module.py +++ b/apiserver/plane/app/views/module.py @@ -21,8 +21,9 @@ from plane.app.serializers import ( 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 @@ from plane.db.models import ( ModuleFavorite, IssueLink, IssueAttachment, + ModuleUserProperties, ) from plane.bgtasks.issue_activites_task import issue_activity from plane.utils.grouper import group_results @@ -54,7 +56,6 @@ class ModuleViewSet(WebhookMixin, BaseViewSet): ) def get_queryset(self): - subquery = ModuleFavorite.objects.filter( user=self.request.user, module_id=OuterRef("pk"), @@ -136,7 +137,7 @@ class ModuleViewSet(WebhookMixin, BaseViewSet): ), ) ) - .order_by("-is_favorite","-created_at") + .order_by("-is_favorite", "-created_at") ) def create(self, request, slug, project_id): @@ -153,6 +154,14 @@ class ModuleViewSet(WebhookMixin, BaseViewSet): 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 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet): .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 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet): .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 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet): 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 @@ class ModuleFavoriteViewSet(BaseViewSet): 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 9bd1f1dd4..482bdfbfe 100644 --- a/apiserver/plane/app/views/page.py +++ b/apiserver/plane/app/views/page.py @@ -157,9 +157,8 @@ class PageViewSet(BaseViewSet): 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 @@ class PageViewSet(BaseViewSet): 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 5b88e3652..c5caac666 100644 --- a/apiserver/plane/app/views/project.py +++ b/apiserver/plane/app/views/project.py @@ -36,6 +36,7 @@ from plane.app.serializers import ( ProjectFavoriteSerializer, ProjectDeployBoardSerializer, ProjectMemberAdminSerializer, + ProjectMemberRoleSerializer, ) from plane.app.permissions import ( @@ -180,12 +181,9 @@ class ProjectViewSet(WebhookMixin, BaseViewSet): 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 @@ class ProjectMemberViewSet(BaseViewSet): 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 @@ class ProjectMemberViewSet(BaseViewSet): 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 @@ class ProjectPublicCoverImagesEndpoint(BaseAPIView): 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 eb76407b7..a2f00a819 100644 --- a/apiserver/plane/app/views/view.py +++ b/apiserver/plane/app/views/view.py @@ -27,7 +27,12 @@ from plane.app.serializers import ( 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 @@ from plane.utils.grouper import group_results class GlobalViewViewSet(BaseViewSet): - serializer_class = GlobalViewSerializer - model = GlobalView + serializer_class = IssueViewSerializer + model = IssueView permission_classes = [ WorkspaceEntityPermission, ] @@ -58,6 +63,7 @@ class GlobalViewViewSet(BaseViewSet): 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 @@ class GlobalViewIssuesViewSet(BaseViewSet): 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 @@ class IssueViewViewSet(BaseViewSet): .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 @@ class IssueViewFavoriteViewSet(BaseViewSet): 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 11170114a..f51e1ac1e 100644 --- a/apiserver/plane/app/views/workspace.py +++ b/apiserver/plane/app/views/workspace.py @@ -44,6 +44,8 @@ from plane.app.serializers import ( IssueLiteSerializer, WorkspaceMemberAdminSerializer, WorkspaceMemberMeSerializer, + ProjectMemberRoleSerializer, + WorkspaceUserPropertiesSerializer, ) from plane.app.views.base import BaseAPIView from . import BaseViewSet @@ -64,6 +66,7 @@ from plane.db.models import ( WorkspaceMember, CycleIssue, IssueReaction, + WorkspaceUserProperties ) from plane.app.permissions import ( WorkSpaceBasePermission, @@ -71,11 +74,13 @@ from plane.app.permissions import ( 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 @@ class UserWorkSpacesEndpoint(BaseAPIView): ) .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 @@ class WorkspaceJoinEndpoint(BaseAPIView): # 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 @@ class WorkSpaceMemberViewSet(BaseViewSet): 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 @@ class WorkSpaceMemberViewSet(BaseViewSet): 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 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView): 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 @@ class WorkspaceLabelsEndpoint(BaseAPIView): 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 5d4c0650c..2552ffbc5 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 4bc27d3ee..d33b883bb 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 000000000..b61122ef8 --- /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 000000000..b8386bf46 --- /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 c76df6e5b..b88ee8e46 100644 --- a/apiserver/plane/db/models/__init__.py +++ b/apiserver/plane/db/models/__init__.py @@ -9,6 +9,8 @@ from .workspace import ( WorkspaceMemberInvite, TeamMember, WorkspaceTheme, + WorkspaceUserProperties, + WorkspaceBaseModel, ) from .project import ( @@ -48,11 +50,11 @@ from .social_connection import SocialLoginConnection 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 e5e2c355b..a441057e1 100644 --- a/apiserver/plane/db/models/cycle.py +++ b/apiserver/plane/db/models/cycle.py @@ -6,6 +6,47 @@ from django.conf import settings 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 CycleFavorite(ProjectBaseModel): 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 54acd5c5d..b14376bc5 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 e485eea62..cc8185946 100644 --- a/apiserver/plane/db/models/module.py +++ b/apiserver/plane/db/models/module.py @@ -6,6 +6,47 @@ from django.conf import settings 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 ModuleFavorite(ProjectBaseModel): 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 44bc994d0..8a77f0586 100644 --- a/apiserver/plane/db/models/view.py +++ b/apiserver/plane/db/models/view.py @@ -3,9 +3,50 @@ from django.db import models 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 @@ class GlobalView(BaseModel): 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 505bfbcfa..f0d64ecae 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 Workspace(BaseModel): 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 WorkspaceTheme(BaseModel): 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 0e7a18fa8..6832297e9 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 15150aa40..645e99cb8 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 b5d997662..aad104784 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 000000000..a9dfbb8e0 --- /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 6c933a033..ce8bcbadb 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 0122cf73a..4d938ce26 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 4f243deeb..6723b3946 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 32925c793..96b584ce1 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 81e1bb22f..877c07196 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 10fc37b31..1b474c3ab 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 9f27e818c..4bbed28d3 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 09f21eb3a..c0ad7bc7f 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 000000000..08daceb16 --- /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 000000000..e9ec14528 --- /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 000000000..2ce22b361 --- /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 000000000..90daa08fa --- /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 000000000..8a3695e85 --- /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 000000000..2c469e682 --- /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 000000000..2fe646246 --- /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 000000000..0d959ff6b --- /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 000000000..76dcf1288 --- /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 000000000..e69de29bb 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 733b8f7de..0e49da7fe 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 a1c241f6a..29552b94c 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 129b0bb3b..a412180b8 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 3fdbaa2d3..822b99f17 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 301c1d7c0..bbca953f6 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 c8c47576b..282fc5a9c 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 4f55e8c74..db30554a8 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 754e63711..29aa56742 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 fb2aca722..2fc8d6912 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 198391adb..e814233d7 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"; -export const PriorityIcon: React.FC = ({ priority, className = "", transparentBg = false }) => { - if (!className || className === "") className = "h-4 w-4"; +interface IPriorityIcon { + className?: string; + priority: TIssuePriorities; + size?: number; +} + +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 65b188e4c..4a04c948b 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 53ac1df50..307a65ad2 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 6b6071475..c1e124eab 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 aec82cfa5..9ed4e7e5f 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 a75a450e2..ef9edbfbc 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 f7ec6b593..616f4809f 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 2335226ce..bcecef20a 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 17dbd2ad4..788142d80 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 1a4fa0e49..433fea00a 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 635fbee7f..a3c083b02 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 9917d0f58..ec7c40195 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 06431ab02..51b4089c4 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 5cfd15482..3c199f807 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 f3d7a9993..19f83e40b 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 7251c5073..ee3dce6d6 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 4efc6a211..b45c1fa55 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 66582a1e9..237582ba0 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 3f7348cce..604457945 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 41770eec8..d09e8def4 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 ac75be686..d46cad191 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 951ed3602..59013a3e3 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; +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 { setToastAlert } = useToast(); + const projectDetails = projectId ? getProjectById(projectId.toString()) ?? undefined : undefined; - const { user: userStore, project: projectStore, cycle: cycleStore, module: moduleStore } = useMobxStore(); + const trackExportAnalytics = () => { + if (!currentUser) return; - 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 = { + 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; + if (projectDetails) { + const workspaceDetails = projectDetails.workspace as IWorkspace; - // fetch cycle details - useEffect(() => { - if (!workspaceSlug || !projectId || !cycleId || cycleDetails) return; + eventPayload.workspaceId = workspaceDetails.id; + eventPayload.workspaceName = workspaceDetails.name; + eventPayload.projectId = projectDetails.id; + eventPayload.projectIdentifier = projectDetails.identifier; + eventPayload.projectName = projectDetails.name; + } - cycleStore.fetchCycleWithId(workspaceSlug.toString(), projectId.toString(), cycleId.toString()); - }, [cycleId, cycleDetails, cycleStore, projectId, workspaceSlug]); + if (cycleDetails || moduleDetails) { + const details = cycleDetails || moduleDetails; - // fetch module details - useEffect(() => { - if (!workspaceSlug || !projectId || !moduleId || moduleDetails) return; + 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; + } - moduleStore.fetchModuleDetails(workspaceSlug.toString(), projectId.toString(), moduleId.toString()); - }, [moduleId, moduleDetails, moduleStore, projectId, workspaceSlug]); + if (cycleDetails) { + eventPayload.cycleId = cycleDetails.id; + eventPayload.cycleName = cycleDetails.name; + } - const selectedProjects = params.project && params.project.length > 0 ? params.project : projects?.map((p) => p.id); + if (moduleDetails) { + eventPayload.moduleId = moduleDetails.id; + eventPayload.moduleName = moduleDetails.name; + } + }; - return ( -
-
+ const exportAnalytics = () => { + if (!workspaceSlug) return; + + const data: IExportAnalyticsFormData = { + x_axis: params.x_axis, + y_axis: params.y_axis, + }; + + if (params.segment) data.segment = params.segment; + if (params.project) data.project = params.project; + + analyticsService + .exportAnalytics(workspaceSlug.toString(), data) + .then((res) => { + setToastAlert({ + type: "success", + title: "Success!", + message: res.message, + }); + + trackExportAnalytics(); + }) + .catch(() => + setToastAlert({ + type: "error", + title: "Error!", + message: "There was some error in exporting the analytics. Please try again.", + }) + ); + }; + + const cycleDetails = cycleId ? 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 2066292c8..c09f26d76 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 55ed1d403..09423e6dd 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 6dfbfdd6b..a4b82c4b6 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 df679fbc5..2ff438a39 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 4c69a23c5..ea1a51937 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 aec15d9ac..2a62c99d4 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 ed61d3546..993289c10 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 5df1275ba..b3fc3df78 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 a04968dac..ae7717b39 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 1ffa69a78..f28ea3481 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 37bb968d3..2de731222 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 f0a3e3d90..8d9d6ecd4 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 7ee4feacd..35b0b9b49 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 6471bc9cf..3d5f6352e 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 d21eb8b80..49dd77e10 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 eff42bb2d..1d306bb04 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 859a6d23a..4aaaab33a 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 8e188df7b..55f72c85d 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 57af2b62a..96fba41f6 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); - const handleUpdateIssue = async (formData: Partial) => { + 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) => { 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 81b9f7ae9..8d1c48261 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 0ce05bd7b..7841a4a1e 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 1e10b3a46..44b5e6111 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 791c62656..769a26be7 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 f7266a48a..976a63c87 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 005e570e7..342827825 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 0488455fb..e5f781dd9 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 8bf0c9938..44fc55bbe 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 192ef8ef9..0d2e042a7 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 7bad18734..dbe654e11 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 0fd9e90f1..99396dda2 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 41fe05b3f..9f8023833 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 000000000..8fa8dabda --- /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 d745e1111..6bb646821 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 43d8b4f89..ee3144f3b 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 9f0ec41bc..1c1372e8d 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 11bda44ce..6debc2c15 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 166e911f5..e04ccf820 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 3edcb9066..52b1e9de1 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 3c445760d..9e9a4bac8 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 8cea3784f..6d89981cd 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 f47c1349f..19cd519cb 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 c55170702..bd6f43569 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 c184f80c9..5ef912572 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 2c9339892..524b02dd0 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 d6806eaf0..b7acff358 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 ca0d487e8..f6836269c 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); - // 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 isDateValid = cycle.start_date || cycle.end_date; - - const { currentProjectRole } = userStore; - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER; - + // 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 cycleDetails = getCycleById(cycleId); + + if (!cycleDetails) return null; + + const cycleStatus = cycleDetails.status.toLocaleLowerCase(); + const isCompleted = cycleStatus === "completed"; + const endDate = new Date(cycleDetails.end_date ?? ""); + const startDate = new Date(cycleDetails.start_date ?? ""); + const isDateValid = cycleDetails.start_date || cycleDetails.end_date; + + const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER; 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 af234b9dc..967e8a395 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 226807b78..686937b71 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 33c6254df..44da175b4 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"; +// hooks +import { useApplication, useCycle } from "hooks/store"; +import useToast from "hooks/use-toast"; // components import { Button } from "@plane/ui"; -// hooks -import useToast from "hooks/use-toast"; // 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`); - - handleClose(); - } catch (error) { - setToastAlert({ - type: "error", - title: "Warning!", - message: "Something went wrong please try again later.", }); - } - else + + 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.", }); + } setLoader(false); }; diff --git a/web/components/cycles/form.tsx b/web/components/cycles/form.tsx index 2cc087eda..2396d040a 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(date ? renderFormattedPayloadDate(date) : null)} + buttonVariant="border-with-text" + placeholder="Start date" + maxDate={maxDate ?? undefined} + /> +
+ )} + /> +
+ ( +
+ onChange(val)} - minDate={new Date()} - maxDate={maxDate ?? undefined} + onChange={(date) => onChange(date ? renderFormattedPayloadDate(date) : null)} + buttonVariant="border-with-text" + placeholder="End date" + minDate={minDate} /> - )} - /> -
-
- ( - onChange(val)} minDate={minDate} /> - )} - /> -
+
+ )} + />
diff --git a/web/components/cycles/gantt-chart/blocks.tsx b/web/components/cycles/gantt-chart/blocks.tsx index 8f05c45ab..46bc04039 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 9671c22af..26d04e103 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 blockFormat = (blocks: (ICycle | null)[]) => { + if (!blocks) return []; - const isAllowed = projectDetails?.member_role === 20 || projectDetails?.member_role === 15; + 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 665f9865b..bfd30cdf6 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 e73168008..f2f7792f6 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] ?? []; + // 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); - } + // 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] - ); + // 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 f47c1ddaa..5956e4a1e 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 000000000..4d5c60acd --- /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 000000000..e791413b4 --- /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 000000000..8d538f53f --- /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 000000000..036ed9f75 --- /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 000000000..53003473d --- /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 000000000..bc976b46a --- /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 000000000..18d317a56 --- /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 000000000..4c0bff67b --- /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 000000000..f50da72c8 --- /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 000000000..ff35c26b6 --- /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 000000000..5c467a7b6 --- /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 000000000..65169dd88 --- /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 000000000..c7ba9eced --- /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 000000000..f23914daa --- /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 b24172688..0a607e88d 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 c9d34fe8e..8055ddb90 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 65764e5d2..b6effa711 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 e7d656eca..000000000 --- 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 29debfedb..323cbe888 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 fbbf92c26..87bf0604a 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 d2502cefb..34e41fc35 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 ea51c3b12..bc59624a5 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 0d4309cf8..c366bcfed 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 2526199b5..7873ea691 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 { - cycle: cycleStore, - projectIssuesFilter: projectIssueFiltersStore, - project: { currentProjectDetails }, - projectMember: { projectMembers }, - projectLabel: { projectLabels }, - projectState: projectStateStore, - commandPalette: commandPaletteStore, - trackEvent: { setTrackElement }, - cycleIssuesFilter: { issueFilters, updateFilters }, - user: { currentProjectRole }, - } = useMobxStore(); + 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 { + project: { projectLabels }, + } = useLabel(); + const { + project: { projectMemberIds }, + } = useMember(); - const activeLayout = projectIssueFiltersStore.issueFilters?.displayFilters?.layout; + 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 370dfe6d4..825af560d 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 303d0f765..a8e87e55c 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 44248a438..b23f56eab 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 df3b57dd8..c408333f1 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 47f0317f4..cab4be600 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 c1b323e74..c53574cb4 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 000000000..6644d7e8c --- /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 da751cbe3..000000000 --- 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 d4f391459..6c26bf850 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 9546de31e..d4385e7da 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 000000000..9d8a31b05 --- /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 658e825bf..1bd2c83d6 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 c920caeba..eb80b0323 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 14ecd7edd..49d9e19dd 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 955d8ac78..6a2caba18 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 2f53a825f..e2d4a4a00 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 677ab5e22..3f463496e 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 2e34fe331..2d79f4ee1 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"; +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"; -// hooks -import useToast from "hooks/use-toast"; -import useLocalStorage from "hooks/use-local-storage"; // 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"; -// ui -import {} from "components/ui"; -import { Button, CustomMenu, Input, ToggleSwitch } from "@plane/ui"; -// icons -import { Sparkle, X } from "lucide-react"; -// 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 { + DateDropdown, + EstimateDropdown, + PriorityDropdown, + ProjectDropdown, + ProjectMemberDropdown, + StateDropdown, +} from "components/dropdowns"; +// ui +import { Button, CustomMenu, Input, ToggleSwitch } from "@plane/ui"; +// helpers +import { renderFormattedPayloadDate } from "helpers/date-time.helper"; +// types +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 51ff30d40..4008e6383 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 3e55ca70c..0f0b5cea3 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 a2626b023..1652aa89b 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 { - calendar: calendarStore, - projectIssues: issueStore, - user: { currentProjectRole }, - } = useMobxStore(); + issues: { viewFlags }, + } = useIssues(EIssuesStoreType.PROJECT); + const issueCalendarView = useCalendarView(); + const { + 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 ae2b55a55..2443ae17b 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 c1778b334..0abe8580d 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 1a2280d05..ebbb510fc 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 f8eead33f..be30560fb 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 85a74a997..7a3c01417 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 88025ad68..585b1a5e1 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 4a7cfbd3f..d2b23e176 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 e71cc7e3b..40f72e7b8 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 95d746eec..43e59dc76 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 { workspaceSlug, projectId, viewId } = router.query; - const issueActions = { - [EIssueActions.UPDATE]: async (issue: IIssue) => { - if (!workspaceSlug) return; + const issueActions = useMemo( + () => ({ + [EIssueActions.UPDATE]: async (issue: TIssue) => { + if (!workspaceSlug || !projectId) return; - await projectViewIssuesStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue); - }, - [EIssueActions.DELETE]: async (issue: IIssue) => { - if (!workspaceSlug) return; + await issues.updateIssue(workspaceSlug.toString(), projectId.toString(), issue.id, issue); + }, + [EIssueActions.DELETE]: async (issue: TIssue) => { + if (!workspaceSlug || !projectId) return; - await projectViewIssuesStore.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(), - 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 000000000..82d9ce0ce --- /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 3160dcfba..c34aaef97 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 52baa2eb8..1c3ba1628 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 { - cycleIssues: cycleIssueStore, - commandPalette: commandPaletteStore, - trackEvent: { setTrackElement }, - user: { currentProjectRole: userRole }, - } = useMobxStore(); + commandPalette: { toggleCreateIssueModal }, + eventTracker: { setTrackElement }, + } = useApplication(); + const { + 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 d4348c4bf..cd4070186 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 ed7f73358..cb70a5a22 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 2fd297a90..919decd51 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 7db04b36a..592264d82 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 7ff8056b9..18eac8525 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 08e7aee44..799233d01 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 1dd61d339..94ea9221e 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 88b39dc00..be3240b55 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 b1e17cfe3..4c5affe8d 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 2b6571d3b..b09bc7628 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 b7c8b6889..f402c9807 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 { + issuesFilter: { issueFilters, updateFilters }, + } = useIssues(EIssuesStoreType.CYCLE); const { - projectLabel: { projectLabels }, - projectState: projectStateStore, - projectMember: { projectMembers }, - cycleIssuesFilter: { issueFilters, updateFilters }, - } = useMobxStore(); - + 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 d3d56266d..f650c0bd5 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 543d18645..87bb719c4 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 62cd4b3d8..a9a7832c6 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 89870d98a..0c45c025e 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 31317366c..98ecf50b4 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 { - projectLabel: { projectLabels }, - projectState: projectStateStore, - projectMember: { projectMembers }, - projectIssuesFilter: { issueFilters, updateFilters }, - user: { currentProjectRole }, - } = useMobxStore(); + project: { projectLabels }, + } = useLabel(); + const { + 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 6b037a031..ffbae9ac8 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 64f95983e..620a8f781 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 9cff84d9b..59a873162 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 412e54794..3c94b4f3f 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 0abe6442a..3ea1453e8 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 cb75b53f4..0feb1d891 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 aa057e417..659d86d08 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 a6fa2bf06..59c83a200 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 004d1b6e9..e417c650e 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 f66422427..331051161 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 0a1ecf3ea..168e31bc0 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 de6b73596..3e23ae07b 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 8e2f4b402..a6af9833a 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 7270ae06d..729cd6c68 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 24781bb41..9bf7cfc78 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 d129774aa..000000000 --- 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 77306998b..000000000 --- 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 c7b16fe26..000000000 --- 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 c703ea66b..35c7f77cf 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 50ed9ad98..000000000 --- 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 b4d740e37..000000000 --- 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 5eb19fbfd..000000000 --- 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 7578214b2..000000000 --- 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 421a1da8f..000000000 --- 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 926743464..000000000 --- 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 efdd79cfc..674ae92d1 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 07129910f..000000000 --- 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 9237d8a1f..b6e39606e 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 cf4c74063..388699dc7 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 de579473b..c1db51411 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 6049ec3bc..ef1edc831 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 { issues, issuesFilter } = useIssues(EIssuesStoreType.DRAFT); - const issueActions = { - [EIssueActions.UPDATE]: async (group_by: string | null, issue: IIssue) => { - if (!workspaceSlug || !projectId) return; + const issueActions = useMemo( + () => ({ + [EIssueActions.UPDATE]: async (issue: TIssue) => { + 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 issues.updateIssue(workspaceSlug, projectId, issue.id, issue); + }, + [EIssueActions.DELETE]: async (issue: TIssue) => { + if (!workspaceSlug || !projectId) return; - await projectIssuesStore.removeIssue(workspaceSlug, projectId, issue.id); - }, - }; - - 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/list/roots/module-root.tsx b/web/components/issues/issue-layouts/list/roots/module-root.tsx index 5d076a0cc..947cfe55b 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 eedf7ae81..55db4cd71 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(); + membership: { currentWorkspaceAllProjectsRole }, + } = useUser(); - const issueActions = { - [EIssueActions.UPDATE]: async (group_by: string | null, issue: IIssue) => { - if (!workspaceSlug || !userId) return; + const issueActions = useMemo( + () => ({ + [EIssueActions.UPDATE]: async (issue: TIssue) => { + 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 issues.updateIssue(workspaceSlug, userId, issue.id, issue); + }, + [EIssueActions.DELETE]: async (issue: TIssue) => { + if (!workspaceSlug || !userId) return; - await profileIssuesStore.removeIssue(workspaceSlug, issue.project, issue.id, userId); - }, - }; - - const getProjects = (projectStore: IProjectStore) => projectStore.workspaceProjects; + 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 0d23f7656..b99b431c8 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 { issuesFilter, issues } = useIssues(EIssuesStoreType.PROJECT); - const issueActions = { - [EIssueActions.UPDATE]: async (group_by: string | null, issue: IIssue) => { - if (!workspaceSlug || !projectId) return; + const issueActions = useMemo( + () => ({ + [EIssueActions.UPDATE]: async (issue: TIssue) => { + 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 issues.updateIssue(workspaceSlug, projectId, issue.id, issue); + }, + [EIssueActions.DELETE]: async (issue: TIssue) => { + if (!workspaceSlug || !projectId) return; - await projectIssuesStore.removeIssue(workspaceSlug, projectId, issue.id); - }, - }; - - const getProjects = (projectStore: IProjectStore) => projectStore.workspaceProjects; + 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 52fa1a759..8139307e6 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; + const issueActions = useMemo( + () => ({ + [EIssueActions.UPDATE]: async (issue: TIssue) => { + 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; + await issues.updateIssue(workspaceSlug, projectId, issue.id, issue); + }, + [EIssueActions.DELETE]: async (issue: TIssue) => { + if (!workspaceSlug || !projectId) return; - await projectViewIssueStore.removeIssue(workspaceSlug, projectId, issue.id); - }, - }; - - 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 000000000..fe05d834b --- /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 01dec9b83..000000000 --- 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 b66d2e5b6..000000000 --- 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 e3f617958..000000000 --- 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 000000000..95f3ce21f --- /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 3e2e2acd6..000000000 --- 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 d0045c3d4..b22083c07 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 &&