refactor: MobX store structure (#3228)

* 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 <narayan3119@gmail.com>

* 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 <hoangluan@tel4vn.com>
Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>

* 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 <rahulramesham@gmail.com>
Co-authored-by: gurusainath <gurusainath007@gmail.com>
Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
Co-authored-by: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com>
Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com>
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 <luandnh98@gmail.com>
Co-authored-by: luanduongtel4vn <hoangluan@tel4vn.com>
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 <nikhilschacko@gmail.com>
Co-authored-by: Anmol Singh Bhatia <anmolsinghbhatia@plane.so>
This commit is contained in:
Aaryan Khandelwal 2024-01-02 18:12:55 +05:30 committed by GitHub
parent 1539340113
commit 804b7d8663
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
940 changed files with 26378 additions and 34411 deletions

View file

@ -11,7 +11,7 @@ import { Loader, Tooltip } from "@plane/ui";
// helpers
import { renderFormattedTime, renderFormattedDate, calculateTimeAgo } from "helpers/date-time.helper";
// types
import { IIssueActivity } from "types";
import { IIssueActivity } from "@plane/types";
import { History } from "lucide-react";
type Props = {

View file

@ -0,0 +1,88 @@
import { FC, useState } from "react";
import Link from "next/link";
import { AlertCircle, X } from "lucide-react";
// hooks
import { useIssueDetail, useMember } from "hooks/store";
// ui
import { Tooltip } from "@plane/ui";
// components
import { IssueAttachmentDeleteModal } from "./delete-attachment-confirmation-modal";
// icons
import { getFileIcon } from "components/icons";
// helper
import { truncateText } from "helpers/string.helper";
import { renderFormattedDate } from "helpers/date-time.helper";
import { convertBytesToSize, getFileExtension, getFileName } from "helpers/attachment.helper";
// type
import { TIssueAttachmentsList } from "./attachments-list";
export type TIssueAttachmentsDetail = TIssueAttachmentsList & {
attachmentId: string;
};
export const IssueAttachmentsDetail: FC<TIssueAttachmentsDetail> = (props) => {
// props
const { attachmentId, handleAttachmentOperations } = props;
// store hooks
const { getUserDetails } = useMember();
const {
attachment: { getAttachmentById },
} = useIssueDetail();
// states
const [attachmentDeleteModal, setAttachmentDeleteModal] = useState<boolean>(false);
const attachment = attachmentId && getAttachmentById(attachmentId);
if (!attachment) return <></>;
return (
<>
<IssueAttachmentDeleteModal
isOpen={attachmentDeleteModal}
setIsOpen={setAttachmentDeleteModal}
handleAttachmentOperations={handleAttachmentOperations}
data={attachment}
/>
<div
key={attachmentId}
className="flex h-[60px] items-center justify-between gap-1 rounded-md border-[2px] border-custom-border-200 bg-custom-background-100 px-4 py-2 text-sm"
>
<Link href={attachment.asset} target="_blank" rel="noopener noreferrer">
<div className="flex items-center gap-3">
<div className="h-7 w-7">{getFileIcon(getFileExtension(attachment.asset))}</div>
<div className="flex flex-col gap-1">
<div className="flex items-center gap-2">
<Tooltip tooltipContent={getFileName(attachment.attributes.name)}>
<span className="text-sm">{truncateText(`${getFileName(attachment.attributes.name)}`, 10)}</span>
</Tooltip>
<Tooltip
tooltipContent={`${
getUserDetails(attachment.updated_by)?.display_name ?? ""
} uploaded on ${renderFormattedDate(attachment.updated_at)}`}
>
<span>
<AlertCircle className="h-3 w-3" />
</span>
</Tooltip>
</div>
<div className="flex items-center gap-3 text-xs text-custom-text-200">
<span>{getFileExtension(attachment.asset).toUpperCase()}</span>
<span>{convertBytesToSize(attachment.attributes.size)}</span>
</div>
</div>
</div>
</Link>
<button
onClick={() => {
setAttachmentDeleteModal(true);
}}
>
<X className="h-4 w-4 text-custom-text-200 hover:text-custom-text-100" />
</button>
</div>
</>
);
};

View file

@ -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<TAttachmentOperations, "remove">;
type Props = {
disabled?: boolean;
handleAttachmentOperations: TAttachmentOperationsModal;
};
const issueAttachmentService = new IssueAttachmentService();
export const IssueAttachmentUpload: React.FC<Props> = 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<Props> = observer((props) => {
})
);
setIsLoading(true);
issueAttachmentService
.uploadIssueAttachment(workspaceSlug as string, projectId as string, issueId as string, formData)
.then((res) => {
mutate<IIssueAttachment[]>(
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
}, []);

View file

@ -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<TAttachmentOperations, "create">;
export type TIssueAttachmentsList = {
handleAttachmentOperations: TAttachmentOperationsRemoveModal;
};
export const IssueAttachmentsList: FC<TIssueAttachmentsList> = observer((props) => {
const { handleAttachmentOperations } = props;
// store hooks
const {
attachment: { issueAttachments },
} = useIssueDetail();
return (
<>
{issueAttachments &&
issueAttachments.length > 0 &&
issueAttachments.map((attachmentId) => (
<IssueAttachmentsDetail attachmentId={attachmentId} handleAttachmentOperations={handleAttachmentOperations} />
))}
</>
);
});

View file

@ -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> = (props) => {
const { editable } = props;
// states
const [deleteAttachment, setDeleteAttachment] = useState<IIssueAttachment | null>(null);
const [attachmentDeleteModal, setAttachmentDeleteModal] = useState<boolean>(false);
const router = useRouter();
const { workspaceSlug, projectId, issueId } = router.query;
const { data: attachments } = useSWR<IIssueAttachment[]>(
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 (
<>
<DeleteAttachmentModal
isOpen={attachmentDeleteModal}
setIsOpen={setAttachmentDeleteModal}
data={deleteAttachment}
/>
{attachments &&
attachments.length > 0 &&
attachments.map((file) => (
<div
key={file.id}
className="flex h-[60px] items-center justify-between gap-1 rounded-md border-[2px] border-custom-border-200 bg-custom-background-100 px-4 py-2 text-sm"
>
<Link href={file.asset} target="_blank" rel="noopener noreferrer">
<div className="flex items-center gap-3">
<div className="h-7 w-7">{getFileIcon(getFileExtension(file.asset))}</div>
<div className="flex flex-col gap-1">
<div className="flex items-center gap-2">
<Tooltip tooltipContent={getFileName(file.attributes.name)}>
<span className="text-sm">{truncateText(`${getFileName(file.attributes.name)}`, 10)}</span>
</Tooltip>
<Tooltip
tooltipContent={`${
people?.find((person) => person.member.id === file.updated_by)?.member.display_name ?? ""
} uploaded on ${renderFormattedDate(file.updated_at)}`}
>
<span>
<AlertCircle className="h-3 w-3" />
</span>
</Tooltip>
</div>
<div className="flex items-center gap-3 text-xs text-custom-text-200">
<span>{getFileExtension(file.asset).toUpperCase()}</span>
<span>{convertBytesToSize(file.attributes.size)}</span>
</div>
</div>
</div>
</Link>
{editable && (
<button
onClick={() => {
setDeleteAttachment(file);
setAttachmentDeleteModal(true);
}}
>
<X className="h-4 w-4 text-custom-text-200 hover:text-custom-text-100" />
</button>
)}
</div>
))}
</>
);
};

View file

@ -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<React.SetStateAction<boolean>>;
data: IIssueAttachment | null;
setIsOpen: Dispatch<SetStateAction<boolean>>;
data: TIssueAttachment;
};
// services
const issueAttachmentService = new IssueAttachmentService();
export const DeleteAttachmentModal: React.FC<Props> = ({ isOpen, setIsOpen, data }) => {
const router = useRouter();
const { workspaceSlug, projectId, issueId } = router.query;
const { setToastAlert } = useToast();
export const IssueAttachmentDeleteModal: FC<Props> = (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<IIssueAttachment[]>(
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 && (
<Transition.Root show={isOpen} as={React.Fragment}>
<Transition.Root show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-20" onClose={handleClose}>
<Transition.Child
as={React.Fragment}
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
@ -80,7 +50,7 @@ export const DeleteAttachmentModal: React.FC<Props> = ({ isOpen, setIsOpen, data
<div className="fixed inset-0 z-20 overflow-y-auto">
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
<Transition.Child
as={React.Fragment}
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
@ -118,10 +88,10 @@ export const DeleteAttachmentModal: React.FC<Props> = ({ isOpen, setIsOpen, data
tabIndex={1}
onClick={() => {
handleDeletion(data.id);
handleClose();
}}
disabled={loader}
>
Delete
{loader ? "Deleting..." : "Delete"}
</Button>
</div>
</Dialog.Panel>

View file

@ -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";

View file

@ -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<void>;
remove: (linkId: string) => Promise<void>;
};
export const IssueAttachmentRoot: FC<TIssueAttachmentRoot> = (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 (
<div className="relative py-3 space-y-3">
<h3 className="text-lg">Attachments</h3>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
<IssueAttachmentUpload disabled={isEditable} handleAttachmentOperations={handleAttachmentOperations} />
<IssueAttachmentsList handleAttachmentOperations={handleAttachmentOperations} />
</div>
</div>
);
};

View file

@ -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<IIssueActivity> = {
access: "INTERNAL",
@ -47,13 +46,14 @@ const commentAccess: commentAccessType[] = [
const fileService = new FileService();
export const AddComment: React.FC<Props> = ({ disabled = false, onSubmit, showAccessSpecifier = false }) => {
// refs
const editorRef = React.useRef<any>(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<Props> = ({ disabled = false, onSubmit, showAc
? { accessValue: accessValue ?? "INTERNAL", onAccessChange, showAccessSpecifier, commentAccess }
: undefined
}
mentionSuggestions={editorSuggestions.mentionSuggestions}
mentionHighlights={editorSuggestions.mentionHighlights}
mentionSuggestions={mentionSuggestions}
mentionHighlights={mentionHighlights}
submitButton={
<Button
variant="primary"

View file

@ -1,12 +1,12 @@
import React, { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { observer } from "mobx-react-lite";
// hooks
import { useMention, useUser } from "hooks/store";
// services
import { FileService } from "services/file.service";
// icons
import { Check, Globe2, Lock, MessageSquare, Pencil, Trash2, X } from "lucide-react";
// hooks
import useUser from "hooks/use-user";
import useEditorSuggestions from "hooks/use-editor-suggestions";
// ui
import { CustomMenu } from "@plane/ui";
import { CommentReaction } from "components/issues";
@ -14,7 +14,7 @@ import { LiteTextEditorWithRef, LiteReadOnlyEditorWithRef } from "@plane/lite-te
// helpers
import { calculateTimeAgo } from "helpers/date-time.helper";
// types
import type { IIssueActivity } from "types";
import type { IIssueActivity } from "@plane/types";
// services
const fileService = new FileService();
@ -27,22 +27,17 @@ type Props = {
workspaceSlug: string;
};
export const CommentCard: React.FC<Props> = ({
comment,
handleCommentDeletion,
onSubmit,
showAccessSpecifier = false,
workspaceSlug,
}) => {
const { user } = useUser();
export const CommentCard: React.FC<Props> = observer((props) => {
const { comment, handleCommentDeletion, onSubmit, showAccessSpecifier = false, workspaceSlug } = props;
// states
const [isEditing, setIsEditing] = useState(false);
// refs
const editorRef = React.useRef<any>(null);
const showEditorRef = React.useRef<any>(null);
const editorSuggestions = useEditorSuggestions();
const [isEditing, setIsEditing] = useState(false);
// store hooks
const { currentUser } = useUser();
const { mentionHighlights, mentionSuggestions } = useMention();
// form info
const {
formState: { isSubmitting },
handleSubmit,
@ -113,8 +108,8 @@ export const CommentCard: React.FC<Props> = ({
debouncedUpdatesEnabled={false}
customClassName="min-h-[50px] p-3 shadow-sm"
onChange={(comment_json: Object, comment_html: string) => setValue("comment_html", comment_html)}
mentionSuggestions={editorSuggestions.mentionSuggestions}
mentionHighlights={editorSuggestions.mentionHighlights}
mentionSuggestions={mentionSuggestions}
mentionHighlights={mentionHighlights}
/>
</div>
<div className="flex gap-1 self-end">
@ -145,13 +140,13 @@ export const CommentCard: React.FC<Props> = ({
ref={showEditorRef}
value={comment.comment_html ?? ""}
customClassName="text-xs border border-custom-border-200 bg-custom-background-100"
mentionHighlights={editorSuggestions.mentionHighlights}
mentionHighlights={mentionHighlights}
/>
<CommentReaction projectId={comment.project} commentId={comment.id} />
</div>
</div>
</div>
{user?.id === comment.actor && (
{currentUser?.id === comment.actor && (
<CustomMenu ellipsis>
<CustomMenu.MenuItem onClick={() => setIsEditing(true)} className="flex items-center gap-1">
<Pencil className="h-3 w-3" />
@ -191,4 +186,4 @@ export const CommentCard: React.FC<Props> = ({
)}
</div>
);
};
});

View file

@ -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> = (props) => {
export const CommentReaction: FC<Props> = 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> = (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> = (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> = (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> = (props) => {
<span>{renderEmoji(reaction)}</span>
<span
className={
commentReactions?.some((r: IssueCommentReaction) => 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> = (props) => {
)}
</div>
);
};
});

View file

@ -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<void>;
};
@ -26,8 +26,11 @@ export const DeleteArchivedIssueModal: React.FC<Props> = 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<Props> = 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<Props> = observer((props) => {
<p className="text-sm text-custom-text-200">
Are you sure you want to delete issue{" "}
<span className="break-words font-medium text-custom-text-100">
{data?.project_detail.identifier}-{data?.sequence_id}
{getProjectById(data?.project_id)?.identifier}-{data?.sequence_id}
</span>
{""}? All of the data related to the archived issue will be permanently removed. This action
cannot be undone.

View file

@ -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> | void;
};
const issueDraftService = new IssueDraftService();
export const DeleteDraftIssueModal: React.FC<Props> = observer((props) => {
export const DeleteDraftIssueModal: React.FC<Props> = (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<Props> = 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<Props> = observer((props) => {
});
})
.catch((error) => {
console.log(error);
console.error(error);
handleClose();
setToastAlert({
title: "Error",
@ -116,7 +113,7 @@ export const DeleteDraftIssueModal: React.FC<Props> = observer((props) => {
<p className="text-sm text-custom-text-200">
Are you sure you want to delete issue{" "}
<span className="break-words font-medium text-custom-text-100">
{data?.project_detail.identifier}-{data?.sequence_id}
{data && getProjectById(data?.project_id)?.identifier}-{data?.sequence_id}
</span>
{""}? 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<Props> = observer((props) => {
</Dialog>
</Transition.Root>
);
});
};

View file

@ -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<void>;
};
export const DeleteIssueModal: React.FC<Props> = (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> = (props) => {
<p className="text-sm text-custom-text-200">
Are you sure you want to delete issue{" "}
<span className="break-words font-medium text-custom-text-100">
{data?.project_detail?.identifier}-{data?.sequence_id}
{getProjectById(issue?.project_id)?.identifier}-{issue?.sequence_id}
</span>
{""}? All of the data related to the issue will be permanently removed. This action cannot be
undone.

View file

@ -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<IssueDetailsProps> = (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<IIssue>({
} = useForm<TIssue>({
defaultValues: {
name: "",
description_html: "",
@ -72,7 +72,7 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = (props) => {
}, [issue.id]); // TODO: verify the exhaustive-deps warning
const handleDescriptionFormSubmit = useCallback(
async (formData: Partial<IIssue>) => {
async (formData: Partial<TIssue>) => {
if (!formData?.name || formData?.name.length === 0 || formData?.name.length > 255) return;
await handleFormSubmit({
@ -135,10 +135,8 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = (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<IssueDetailsProps> = (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<IssueDetailsProps> = (props) => {
onChange(description_html);
debouncedFormSave();
}}
mentionSuggestions={editorSuggestion.mentionSuggestions}
mentionHighlights={editorSuggestion.mentionHighlights}
mentionSuggestions={mentionSuggestions}
mentionHighlights={mentionHighlights}
/>
)}
/>

View file

@ -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<IIssue> = {
project: "",
const defaultValues: Partial<TIssue> = {
project_id: "",
name: "",
description: {
type: "doc",
content: [
{
type: "paragraph",
},
],
},
description_html: "<p></p>",
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<IIssue>,
formData: Partial<TIssue>,
action?: "createDraft" | "createNewIssue" | "updateDraft" | "convertToNewIssue"
) => Promise<void>;
data?: Partial<IIssue> | null;
data?: Partial<TIssue> | null;
isOpen: boolean;
prePopulatedData?: Partial<IIssue> | null;
prePopulatedData?: Partial<TIssue> | null;
projectId: string;
setActiveProject: React.Dispatch<React.SetStateAction<string | null>>;
createMore: boolean;
@ -112,10 +102,12 @@ export const DraftIssueForm: FC<IssueFormProps> = observer((props) => {
const [selectedParentIssue, setSelectedParentIssue] = useState<ISearchIssueResponse | null>(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<any>(null);
// router
@ -123,8 +115,8 @@ export const DraftIssueForm: FC<IssueFormProps> = 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<IssueFormProps> = observer((props) => {
getValues,
setValue,
setFocus,
} = useForm<IIssue>({
} = useForm<TIssue>({
defaultValues: prePopulatedData ?? defaultValues,
reValidateMode: "onChange",
});
const issueName = watch("name");
const payload: Partial<IIssue> = {
const payload: Partial<TIssue> = {
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<IssueFormProps> = observer((props) => {
// };
const handleCreateUpdateIssue = async (
formData: Partial<IIssue>,
formData: Partial<TIssue>,
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: "<p></p>",
});
editorRef?.current?.clearEditor();
@ -222,7 +206,7 @@ export const DraftIssueForm: FC<IssueFormProps> = observer((props) => {
const handleAiAssistance = async (response: string) => {
if (!workspaceSlug || !projectId) return;
setValue("description", {});
// setValue("description", {});
setValue("description_html", `${watch("description_html")}<p>${response}</p>`);
editorRef.current?.setEditorValue(`${watch("description_html")}`);
};
@ -280,7 +264,7 @@ export const DraftIssueForm: FC<IssueFormProps> = observer((props) => {
useEffect(() => {
reset({
...getValues(),
project: projectId,
project_id: projectId,
});
}, [getValues, projectId, reset]);
@ -302,7 +286,7 @@ export const DraftIssueForm: FC<IssueFormProps> = 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<IssueFormProps> = observer((props) => {
{(fieldsToShow.includes("all") || fieldsToShow.includes("project")) && (
<Controller
control={control}
name="project"
name="project_id"
render={({ field: { value, onChange } }) => (
<IssueProjectSelect
<ProjectDropdown
value={value}
onChange={(val: string) => {
onChange={(val) => {
onChange(val);
setActiveProject(val);
}}
buttonVariant="background-with-text"
/>
)}
/>
@ -332,7 +317,7 @@ export const DraftIssueForm: FC<IssueFormProps> = observer((props) => {
{status ? "Update" : "Create"} Issue
</h3>
</div>
{watch("parent") &&
{watch("parent_id") &&
(fieldsToShow.includes("all") || fieldsToShow.includes("parent")) &&
selectedParentIssue && (
<div className="flex w-min items-center gap-2 whitespace-nowrap rounded bg-custom-background-80 p-2 text-xs">
@ -350,7 +335,7 @@ export const DraftIssueForm: FC<IssueFormProps> = observer((props) => {
<X
className="h-3 w-3 cursor-pointer"
onClick={() => {
setValue("parent", null);
setValue("parent_id", null);
setSelectedParentIssue(null);
}}
/>
@ -454,10 +439,9 @@ export const DraftIssueForm: FC<IssueFormProps> = 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<IssueFormProps> = observer((props) => {
{(fieldsToShow.includes("all") || fieldsToShow.includes("state")) && (
<Controller
control={control}
name="state"
name="state_id"
render={({ field: { value, onChange } }) => (
<IssueStateSelect
setIsOpen={setStateModal}
value={value}
onChange={onChange}
projectId={projectId}
/>
<div className="h-7">
<StateDropdown
value={value}
onChange={onChange}
projectId={projectId}
buttonVariant="border-with-text"
/>
</div>
)}
/>
)}
@ -483,80 +469,100 @@ export const DraftIssueForm: FC<IssueFormProps> = observer((props) => {
control={control}
name="priority"
render={({ field: { value, onChange } }) => (
<IssuePrioritySelect value={value} onChange={onChange} />
<div className="h-7">
<PriorityDropdown value={value} onChange={onChange} buttonVariant="background-with-text" />
</div>
)}
/>
)}
{(fieldsToShow.includes("all") || fieldsToShow.includes("assignee")) && (
<Controller
control={control}
name="assignees"
name="assignee_ids"
render={({ field: { value, onChange } }) => (
<IssueAssigneeSelect projectId={projectId} value={value} onChange={onChange} />
<div className="h-7">
<ProjectMemberDropdown
projectId={projectId}
value={value}
onChange={onChange}
multiple
buttonVariant="background-with-text"
/>
</div>
)}
/>
)}
{(fieldsToShow.includes("all") || fieldsToShow.includes("label")) && (
<Controller
control={control}
name="labels"
name="label_ids"
render={({ field: { value, onChange } }) => (
<IssueLabelSelect
setIsOpen={setLabelModal}
value={value}
onChange={onChange}
projectId={projectId}
/>
<div className="h-7">
<IssueLabelSelect
setIsOpen={setLabelModal}
value={value}
onChange={onChange}
projectId={projectId}
/>
</div>
)}
/>
)}
{(fieldsToShow.includes("all") || fieldsToShow.includes("startDate")) && (
<div>
<Controller
control={control}
name="start_date"
render={({ field: { value, onChange } }) => (
<IssueDateSelect
label="Start date"
maxDate={maxDate ?? undefined}
onChange={onChange}
<Controller
control={control}
name="start_date"
render={({ field: { value, onChange } }) => (
<div className="h-7">
<DateDropdown
value={value}
onChange={(date) => onChange(date ? renderFormattedPayloadDate(date) : null)}
buttonVariant="border-with-text"
placeholder="Start date"
maxDate={maxDate ?? undefined}
/>
)}
/>
</div>
</div>
)}
/>
)}
{(fieldsToShow.includes("all") || fieldsToShow.includes("dueDate")) && (
<div>
<Controller
control={control}
name="target_date"
render={({ field: { value, onChange } }) => (
<IssueDateSelect
label="Due date"
minDate={minDate ?? undefined}
onChange={onChange}
<Controller
control={control}
name="target_date"
render={({ field: { value, onChange } }) => (
<div className="h-7">
<DateDropdown
value={value}
onChange={(date) => onChange(date ? renderFormattedPayloadDate(date) : null)}
buttonVariant="border-with-text"
placeholder="Due date"
minDate={minDate ?? undefined}
/>
)}
/>
</div>
</div>
)}
/>
)}
{(fieldsToShow.includes("all") || fieldsToShow.includes("estimate")) && (
<div>
{(fieldsToShow.includes("all") || fieldsToShow.includes("estimate")) &&
areEstimatesActiveForProject(projectId) && (
<Controller
control={control}
name="estimate_point"
render={({ field: { value, onChange } }) => (
<IssueEstimateSelect value={value} onChange={onChange} />
<div className="h-7">
<EstimateDropdown
value={value}
onChange={onChange}
projectId={projectId}
buttonVariant="background-with-text"
/>
</div>
)}
/>
</div>
)}
)}
{(fieldsToShow.includes("all") || fieldsToShow.includes("parent")) && (
<Controller
control={control}
name="parent"
name="parent_id"
render={({ field: { onChange } }) => (
<ParentIssuesListModal
isOpen={parentIssueListModalOpen}
@ -572,12 +578,12 @@ export const DraftIssueForm: FC<IssueFormProps> = observer((props) => {
)}
{(fieldsToShow.includes("all") || fieldsToShow.includes("parent")) && (
<CustomMenu ellipsis>
{watch("parent") ? (
{watch("parent_id") ? (
<>
<CustomMenu.MenuItem onClick={() => setParentIssueListModalOpen(true)}>
Change parent issue
</CustomMenu.MenuItem>
<CustomMenu.MenuItem onClick={() => setValue("parent", null)}>
<CustomMenu.MenuItem onClick={() => setValue("parent_id", null)}>
Remove parent issue
</CustomMenu.MenuItem>
</>

View file

@ -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<IIssue>;
prePopulateData?: Partial<TIssue>;
fieldsToShow?: (
| "project"
| "name"
@ -38,7 +38,7 @@ interface IssuesModalProps {
| "parent"
| "all"
)[];
onSubmit?: (data: Partial<IIssue>) => Promise<void> | void;
onSubmit?: (data: Partial<TIssue>) => Promise<void> | void;
}
// services
@ -59,15 +59,16 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = observer(
// states
const [createMore, setCreateMore] = useState(false);
const [activeProject, setActiveProject] = useState<string | null>(null);
const [prePopulateData, setPreloadedData] = useState<Partial<IIssue> | undefined>(undefined);
const [prePopulateData, setPreloadedData] = useState<Partial<TIssue> | 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<IssuesModalProps> = 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<IssuesModalProps> = 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<IssuesModalProps> = 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<IssuesModalProps> = 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<IIssue>) => {
if (!workspaceSlug || !activeProject || !user) return;
const createDraftIssue = async (payload: Partial<TIssue>) => {
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<IssuesModalProps> = observer(
if (!createMore) onClose();
};
const updateDraftIssue = async (payload: Partial<IIssue>) => {
if (!user) return;
await draftIssueStore
const updateDraftIssue = async (payload: Partial<TIssue>) => {
await draftIssues
.updateIssue(workspaceSlug as string, activeProject ?? "", data?.id ?? "", payload)
.then((res) => {
if (isUpdatingSingleIssue) {
mutate<IIssue>(PROJECT_ISSUES_DETAILS, (prevData) => ({ ...prevData, ...res }), false);
mutate<TIssue>(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<IssuesModalProps> = 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<IssuesModalProps> = 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<IIssue>) => {
if (!workspaceSlug || !activeProject || !user) return;
const createIssue = async (payload: Partial<TIssue>) => {
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<IssuesModalProps> = 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<IssuesModalProps> = observer(
};
const handleFormSubmit = async (
formData: Partial<IIssue>,
formData: Partial<TIssue>,
action: "createDraft" | "createNewIssue" | "updateDraft" | "convertToNewIssue" = "createDraft"
) => {
if (!workspaceSlug || !activeProject) return;
const payload: Partial<IIssue> = {
const payload: Partial<TIssue> = {
...formData,
description: formData.description ?? "",
// description: formData.description ?? "",
description_html: formData.description_html ?? "<p></p>",
};
@ -332,7 +335,7 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = observer(
projectId={activeProject ?? ""}
setActiveProject={setActiveProject}
status={data ? true : false}
user={user ?? undefined}
user={currentUser ?? undefined}
fieldsToShow={fieldsToShow}
/>
</Dialog.Panel>

View file

@ -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<IIssue> = {
project: "",
const defaultValues: Partial<TIssue> = {
project_id: "",
name: "",
description_html: "<p></p>",
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<IIssue>) => Promise<void>;
initialData?: Partial<IIssue>;
handleFormSubmit: (values: Partial<TIssue>) => Promise<void>;
initialData?: Partial<TIssue>;
projectId: string;
setActiveProject: React.Dispatch<React.SetStateAction<string | null>>;
createMore: boolean;
setCreateMore: React.Dispatch<React.SetStateAction<boolean>>;
handleDiscardClose: () => void;
status: boolean;
handleFormDirty: (payload: Partial<IIssue> | null) => void;
handleFormDirty: (payload: Partial<TIssue> | null) => void;
fieldsToShow: (
| "project"
| "name"
@ -106,14 +104,14 @@ export const IssueForm: FC<IssueFormProps> = 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<IssueFormProps> = observer((props) => {
getValues,
setValue,
setFocus,
} = useForm<IIssue>({
} = useForm<TIssue>({
defaultValues: initialData ?? defaultValues,
reValidateMode: "onChange",
});
const issueName = watch("name");
const payload: Partial<IIssue> = {
const payload: Partial<TIssue> = {
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<IIssue>) => {
const handleCreateUpdateIssue = async (formData: Partial<TIssue>) => {
await handleFormSubmit(formData);
setGptAssistantModal(false);
reset({
...defaultValues,
project: projectId,
description: {
type: "doc",
content: [
{
type: "paragraph",
},
],
},
project_id: projectId,
description_html: "<p></p>",
});
editorRef?.current?.clearEditor();
@ -177,18 +169,17 @@ export const IssueForm: FC<IssueFormProps> = observer((props) => {
const handleAiAssistance = async (response: string) => {
if (!workspaceSlug || !projectId) return;
setValue("description", {});
setValue("description_html", `${watch("description_html")}<p>${response}</p>`);
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<IssueFormProps> = observer((props) => {
reset({
...defaultValues,
...initialData,
project: projectId,
});
}, [setFocus, initialData, reset]);
@ -235,7 +225,7 @@ export const IssueForm: FC<IssueFormProps> = observer((props) => {
useEffect(() => {
reset({
...getValues(),
project: projectId,
project_id: projectId,
});
}, [getValues, projectId, reset]);
@ -257,29 +247,31 @@ export const IssueForm: FC<IssueFormProps> = 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])}
/>
</>
)}
<form onSubmit={handleSubmit(handleCreateUpdateIssue)}>
<div className="space-y-5">
<div className="flex items-center gap-x-2">
{(fieldsToShow.includes("all") || fieldsToShow.includes("project")) && (
{(fieldsToShow.includes("all") || fieldsToShow.includes("project")) && !status && (
<Controller
control={control}
name="project"
name="project_id"
rules={{
required: true,
}}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<IssueProjectSelect
value={value}
error={error}
onChange={(val: string) => {
onChange(val);
setActiveProject(val);
}}
/>
render={({ field: { value, onChange } }) => (
<div className="h-7">
<ProjectDropdown
value={value}
onChange={(val) => {
onChange(val);
setActiveProject(val);
}}
buttonVariant="border-with-text"
/>
</div>
)}
/>
)}
@ -287,7 +279,7 @@ export const IssueForm: FC<IssueFormProps> = observer((props) => {
{status ? "Update" : "Create"} Issue
</h3>
</div>
{watch("parent") &&
{watch("parent_id") &&
(fieldsToShow.includes("all") || fieldsToShow.includes("parent")) &&
selectedParentIssue && (
<div className="flex w-min items-center gap-2 whitespace-nowrap rounded bg-custom-background-80 p-2 text-xs">
@ -305,7 +297,7 @@ export const IssueForm: FC<IssueFormProps> = observer((props) => {
<X
className="h-3 w-3 cursor-pointer"
onClick={() => {
setValue("parent", null);
setValue("parent_id", null);
setSelectedParentIssue(null);
}}
/>
@ -408,10 +400,9 @@ export const IssueForm: FC<IssueFormProps> = 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<IssueFormProps> = observer((props) => {
{(fieldsToShow.includes("all") || fieldsToShow.includes("state")) && (
<Controller
control={control}
name="state"
name="state_id"
render={({ field: { value, onChange } }) => (
<IssueStateSelect
setIsOpen={setStateModal}
value={value}
onChange={onChange}
projectId={projectId}
/>
<div className="h-7">
<StateDropdown
value={value}
onChange={onChange}
projectId={projectId}
buttonVariant="border-with-text"
/>
</div>
)}
/>
)}
@ -437,48 +430,63 @@ export const IssueForm: FC<IssueFormProps> = observer((props) => {
control={control}
name="priority"
render={({ field: { value, onChange } }) => (
<IssuePrioritySelect value={value} onChange={onChange} />
<div className="h-7">
<PriorityDropdown value={value} onChange={onChange} buttonVariant="border-with-text" />
</div>
)}
/>
)}
{(fieldsToShow.includes("all") || fieldsToShow.includes("assignee")) && (
<Controller
control={control}
name="assignees"
name="assignee_ids"
render={({ field: { value, onChange } }) => (
<IssueAssigneeSelect projectId={projectId} value={value} onChange={onChange} />
<div className="h-7">
<ProjectMemberDropdown
projectId={projectId}
value={value}
onChange={onChange}
buttonVariant={value?.length > 0 ? "transparent-without-text" : "border-with-text"}
buttonClassName={value?.length > 0 ? "hover:bg-transparent px-0" : ""}
placeholder="Assignees"
multiple
/>
</div>
)}
/>
)}
{(fieldsToShow.includes("all") || fieldsToShow.includes("label")) && (
<Controller
control={control}
name="labels"
name="label_ids"
render={({ field: { value, onChange } }) => (
<IssueLabelSelect
setIsOpen={setLabelModal}
value={value}
onChange={onChange}
projectId={projectId}
/>
<div className="h-7">
<IssueLabelSelect
setIsOpen={setLabelModal}
value={value}
onChange={onChange}
projectId={projectId}
/>
</div>
)}
/>
)}
{(fieldsToShow.includes("all") || fieldsToShow.includes("startDate")) && (
<div>
<Controller
control={control}
name="start_date"
render={({ field: { value, onChange } }) => (
<IssueDateSelect
label="Start date"
maxDate={maxDate ?? undefined}
onChange={onChange}
<Controller
control={control}
name="start_date"
render={({ field: { value, onChange } }) => (
<div className="h-7">
<DateDropdown
value={value}
onChange={(date) => onChange(date ? renderFormattedPayloadDate(date) : null)}
buttonVariant="border-with-text"
placeholder="Start date"
maxDate={maxDate ?? undefined}
/>
)}
/>
</div>
</div>
)}
/>
)}
{(fieldsToShow.includes("all") || fieldsToShow.includes("dueDate")) && (
<div>
@ -486,70 +494,79 @@ export const IssueForm: FC<IssueFormProps> = observer((props) => {
control={control}
name="target_date"
render={({ field: { value, onChange } }) => (
<IssueDateSelect
label="Due date"
minDate={minDate ?? undefined}
onChange={onChange}
value={value}
/>
<div className="h-7">
<DateDropdown
value={value}
onChange={(date) => onChange(date ? renderFormattedPayloadDate(date) : null)}
buttonVariant="border-with-text"
placeholder="Due date"
minDate={minDate ?? undefined}
/>
</div>
)}
/>
</div>
)}
{(fieldsToShow.includes("all") || fieldsToShow.includes("module")) && (
{(fieldsToShow.includes("all") || fieldsToShow.includes("cycle")) && projectDetails?.cycle_view && (
<Controller
control={control}
name="module"
name="cycle_id"
render={({ field: { value, onChange } }) => (
<IssueModuleSelect
workspaceSlug={workspaceSlug as string}
projectId={projectId}
value={value}
onChange={(val: string) => {
onChange(val);
}}
/>
<div className="h-7">
<CycleDropdown
projectId={projectId}
onChange={onChange}
value={value}
buttonVariant="border-with-text"
/>
</div>
)}
/>
)}
{(fieldsToShow.includes("all") || fieldsToShow.includes("cycle")) && (
{(fieldsToShow.includes("all") || fieldsToShow.includes("module")) && projectDetails?.module_view && (
<Controller
control={control}
name="cycle"
name="module_id"
render={({ field: { value, onChange } }) => (
<IssueCycleSelect
workspaceSlug={workspaceSlug as string}
projectId={projectId}
value={value}
onChange={(val: string) => {
onChange(val);
}}
/>
<div className="h-7">
<ModuleDropdown
projectId={projectId}
value={value}
onChange={onChange}
buttonVariant="border-with-text"
/>
</div>
)}
/>
)}
{(fieldsToShow.includes("all") || fieldsToShow.includes("estimate")) && (
<>
{(fieldsToShow.includes("all") || fieldsToShow.includes("estimate")) &&
areEstimatesActiveForProject(projectId) && (
<Controller
control={control}
name="estimate_point"
render={({ field: { value, onChange } }) => (
<IssueEstimateSelect value={value} onChange={onChange} />
<div className="h-7">
<EstimateDropdown
value={value}
onChange={onChange}
projectId={projectId}
buttonVariant="border-with-text"
/>
</div>
)}
/>
</>
)}
)}
{(fieldsToShow.includes("all") || fieldsToShow.includes("parent")) && (
<>
{watch("parent") ? (
{watch("parent_id") ? (
<CustomMenu
customButton={
<button
type="button"
className="flex w-full cursor-pointer items-center justify-between gap-1 rounded border-[0.5px] border-custom-border-300 px-2 py-1 text-xs text-custom-text-200 hover:bg-custom-background-80"
className="h-7 flex items-center justify-between gap-1 rounded border-[0.5px] border-custom-border-300 px-2 py-1 text-xs text-custom-text-200 hover:bg-custom-background-80"
>
<div className="flex items-center gap-1 text-custom-text-200">
<LayoutPanelTop className="h-3 w-3 flex-shrink-0" />
<LayoutPanelTop className="h-2.5 w-2.5 flex-shrink-0" />
<span className="whitespace-nowrap">
{selectedParentIssue &&
`${selectedParentIssue.project__identifier}-
@ -563,26 +580,24 @@ export const IssueForm: FC<IssueFormProps> = observer((props) => {
<CustomMenu.MenuItem className="!p-1" onClick={() => setParentIssueListModalOpen(true)}>
Change parent issue
</CustomMenu.MenuItem>
<CustomMenu.MenuItem className="!p-1" onClick={() => setValue("parent", null)}>
<CustomMenu.MenuItem className="!p-1" onClick={() => setValue("parent_id", null)}>
Remove parent issue
</CustomMenu.MenuItem>
</CustomMenu>
) : (
<button
type="button"
className="flex w-min cursor-pointer items-center justify-between gap-1 rounded border-[0.5px] border-custom-border-300 px-2 py-1 text-xs text-custom-text-200 hover:bg-custom-background-80"
className="h-7 flex items-center justify-between gap-1 rounded border-[0.5px] border-custom-border-300 px-2 py-1 text-xs hover:bg-custom-background-80"
onClick={() => setParentIssueListModalOpen(true)}
>
<div className="flex items-center gap-1 text-custom-text-300">
<LayoutPanelTop className="h-3 w-3 flex-shrink-0" />
<span className="whitespace-nowrap">Add Parent</span>
</div>
<LayoutPanelTop className="h-3 w-3 flex-shrink-0" />
<span className="whitespace-nowrap">Add parent</span>
</button>
)}
<Controller
control={control}
name="parent"
name="parent_id"
render={({ field: { onChange } }) => (
<ParentIssuesListModal
isOpen={parentIssueListModalOpen}

View file

@ -24,3 +24,6 @@ export * from "./delete-draft-issue-modal";
// archived issue
export * from "./delete-archived-issue-modal";
// issue links
export * from "./issue-links";

View file

@ -7,52 +7,42 @@ import { CalendarChart, IssuePeekOverview } from "components/issues";
// hooks
import useToast from "hooks/use-toast";
// types
import { IIssue } from "types";
import {
ICycleIssuesFilterStore,
ICycleIssuesStore,
IModuleIssuesFilterStore,
IModuleIssuesStore,
IProjectIssuesFilterStore,
IProjectIssuesStore,
IViewIssuesFilterStore,
IViewIssuesStore,
} from "store/issues";
import { TGroupedIssues, TIssue } from "@plane/types";
import { IQuickActionProps } from "../list/list-view-types";
import { EIssueActions } from "../types";
import { IGroupedIssues } from "store/issues/types";
import { handleDragDrop } from "./utils";
import { useIssues } from "hooks/store";
import { ICycleIssues, ICycleIssuesFilter } from "store/issue/cycle";
import { IModuleIssues, IModuleIssuesFilter } from "store/issue/module";
import { IProjectIssues, IProjectIssuesFilter } from "store/issue/project";
import { IProjectViewIssues, IProjectViewIssuesFilter } from "store/issue/project-views";
interface IBaseCalendarRoot {
issueStore: IProjectIssuesStore | IModuleIssuesStore | ICycleIssuesStore | IViewIssuesStore;
issuesFilterStore:
| IProjectIssuesFilterStore
| IModuleIssuesFilterStore
| ICycleIssuesFilterStore
| IViewIssuesFilterStore;
issueStore: IProjectIssues | IModuleIssues | ICycleIssues | IProjectViewIssues;
issuesFilterStore: IProjectIssuesFilter | IModuleIssuesFilter | ICycleIssuesFilter | IProjectViewIssuesFilter;
QuickActions: FC<IQuickActionProps>;
issueActions: {
[EIssueActions.DELETE]: (issue: IIssue) => Promise<void>;
[EIssueActions.UPDATE]?: (issue: IIssue) => Promise<void>;
[EIssueActions.REMOVE]?: (issue: IIssue) => Promise<void>;
[EIssueActions.DELETE]: (issue: TIssue) => Promise<void>;
[EIssueActions.UPDATE]?: (issue: TIssue) => Promise<void>;
[EIssueActions.REMOVE]?: (issue: TIssue) => Promise<void>;
};
viewId?: string;
handleDragDrop: (source: any, destination: any, issues: any, issueWithIds: any) => Promise<void>;
}
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) => {
<DragDropContext onDragEnd={onDragEnd}>
<CalendarChart
issuesFilterStore={issuesFilterStore}
issues={issues}
issues={issueMap}
groupedIssueIds={groupedIssueIds}
layout={displayFilters?.calendar?.layout}
showWeekends={displayFilters?.calendar?.show_weekends ?? false}
@ -120,8 +118,8 @@ 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)
}
/>
)}

View file

@ -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<IIssue | undefined>;
) => Promise<TIssue | undefined>;
viewId?: string;
};
export const CalendarChart: React.FC<Props> = 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<Props> = observer((props) => {
return (
<>
<div className="flex h-full w-full flex-col overflow-hidden">
<CalendarHeader issuesFilterStore={issuesFilterStore} />
<CalendarHeader issuesFilterStore={issuesFilterStore} viewId={viewId} />
<CalendarWeekHeader isLoading={!issues} showWeekends={showWeekends} />
<div className="h-full w-full overflow-y-auto">
{layout === "month" && (
@ -91,7 +87,7 @@ export const CalendarChart: React.FC<Props> = observer((props) => {
{layout === "week" && (
<CalendarWeekDays
issuesFilterStore={issuesFilterStore}
week={calendarStore.allDaysOfActiveWeek}
week={issueCalendarView.allDaysOfActiveWeek}
issues={issues}
groupedIssueIds={groupedIssueIds}
enableQuickIssueCreate

View file

@ -7,33 +7,26 @@ import { CalendarIssueBlocks, ICalendarDate, CalendarQuickAddIssueForm } from "c
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
// constants
import { MONTHS_LIST } from "constants/calendar";
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;
issuesFilterStore: IProjectIssuesFilter | IModuleIssuesFilter | ICycleIssuesFilter | IProjectViewIssuesFilter;
date: ICalendarDate;
issues: IIssueResponse | undefined;
groupedIssueIds: IGroupedIssues;
quickActions: (issue: IIssue, customActionButton?: React.ReactElement) => 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<IIssue | undefined>;
) => Promise<TIssue | undefined>;
viewId?: string;
};

View file

@ -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<Props> = 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<HTMLButtonElement | null>(null);
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(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,
});
};

View file

@ -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<ICalendarHeader> = 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<HTMLButtonElement | null>(null);
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
@ -58,15 +53,17 @@ export const CalendarOptionsDropdown: React.FC<ICalendarHeader> = 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<ICalendarHeader> = 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 (

View file

@ -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<ICalendarHeader> = 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<ICalendarHeader> = 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<ICalendarHeader> = observer((props) => {
activeWeekDate.getDate() - 7
);
calendarStore.updateCalendarFilters({
issueCalendarView.updateCalendarFilters({
activeWeekDate: previousWeekDate,
});
}
@ -62,7 +56,7 @@ export const CalendarHeader: React.FC<ICalendarHeader> = 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<ICalendarHeader> = observer((props) => {
activeWeekDate.getDate() + 7
);
calendarStore.updateCalendarFilters({
issueCalendarView.updateCalendarFilters({
activeWeekDate: nextWeekDate,
});
}
@ -82,7 +76,7 @@ export const CalendarHeader: React.FC<ICalendarHeader> = 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<ICalendarHeader> = observer((props) => {
<button type="button" className="grid place-items-center" onClick={handleNext}>
<ChevronRight size={16} strokeWidth={2} />
</button>
<CalendarMonthsDropdown />
<CalendarMonthsDropdown issuesFilterStore={issuesFilterStore} />
</div>
<div className="flex items-center gap-1.5">
<button
@ -107,7 +101,7 @@ export const CalendarHeader: React.FC<ICalendarHeader> = observer((props) => {
>
Today
</button>
<CalendarOptionsDropdown issuesFilterStore={issuesFilterStore} />
<CalendarOptionsDropdown issuesFilterStore={issuesFilterStore} viewId={viewId} />
</div>
</div>
);

View file

@ -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<Props> = 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<HTMLDivElement | null>(null);
const handleIssuePeekOverview = (issue: IIssue, event: React.MouseEvent<HTMLDivElement, 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<Props> = observer((props) => {
</div>
);
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<Props> = observer((props) => {
const issue = issues?.[issueId];
return (
<Draggable key={issue.id} draggableId={issue.id} index={index} isDragDisabled={!isEditable}>
<Draggable key={issue.id} draggableId={issue.id} index={index}>
{(provided, snapshot) => (
<div
className="relative cursor-pointer p-1 px-2"
{...provided.draggableProps}
{...provided.dragHandleProps}
ref={provided.innerRef}
onClick={(e) => handleIssuePeekOverview(issue, e)}
onClick={() => handleIssuePeekOverview(issue)}
>
{issue?.tempId !== undefined && (
<div className="absolute left-0 top-0 z-[99999] h-full w-full animate-pulse bg-custom-background-100/20" />
@ -96,11 +84,13 @@ export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
<span
className="h-full w-0.5 flex-shrink-0 rounded"
style={{
backgroundColor: issue.state_detail.color,
backgroundColor: getProjectStates(issue?.project_id).find(
(state) => state?.id == issue?.state_id
)?.color,
}}
/>
<div className="flex-shrink-0 text-xs text-custom-text-300">
{issue.project_detail.identifier}-{issue.sequence_id}
{getProjectById(issue?.project_id)?.identifier}-{issue.sequence_id}
</div>
<Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
<div className="truncate text-xs">{issue.name}</div>

View file

@ -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<IIssue>;
prePopulatedData?: Partial<TIssue>;
quickAddCallback?: (
workspaceSlug: string,
projectId: string,
data: IIssue,
data: TIssue,
viewId?: string
) => Promise<IIssue | undefined>;
) => Promise<TIssue | undefined>;
viewId?: string;
onOpen?: () => void;
};
const defaultValues: Partial<IIssue> = {
const defaultValues: Partial<TIssue> = {
name: "",
};
@ -62,22 +61,20 @@ export const CalendarQuickAddIssueForm: React.FC<Props> = 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<HTMLDivElement>(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<Props> = observer((props) => {
register,
setFocus,
formState: { errors, isSubmitting },
} = useForm<IIssue>({ defaultValues });
} = useForm<TIssue>({ defaultValues });
const handleClose = () => {
setIsOpen(false);
@ -102,7 +99,7 @@ export const CalendarQuickAddIssueForm: React.FC<Props> = 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<Props> = 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<Props> = observer((props) => {
try {
quickAddCallback &&
(await quickAddCallback(
workspaceSlug,
projectId,
workspaceSlug.toString(),
projectId.toString(),
{
...payload,
},

View file

@ -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 (
<BaseCalendarRoot
issueStore={cycleIssueStore}
issuesFilterStore={cycleIssueFilterStore}
issueStore={issues}
issuesFilterStore={issuesFilter}
QuickActions={CycleIssueQuickActions}
issueActions={issueActions}
viewId={cycleId.toString()}
handleDragDrop={handleDragDrop}
/>
);
});

View file

@ -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 (
<BaseCalendarRoot
issueStore={moduleIssueStore}
issuesFilterStore={moduleIssueFilterStore}
issueStore={issues}
issuesFilterStore={issuesFilter}
QuickActions={ModuleIssueQuickActions}
issueActions={issueActions}
viewId={moduleId}
handleDragDrop={handleDragDrop}
/>
);
});

View file

@ -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 (
<BaseCalendarRoot
issueStore={issueStore}
issuesFilterStore={projectIssueFiltersStore}
issueStore={issues}
issuesFilterStore={issuesFilter}
QuickActions={ProjectIssueQuickActions}
issueActions={issueActions}
handleDragDrop={handleDragDrop}
/>
);
});

View file

@ -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 (
<BaseCalendarRoot
issueStore={projectViewIssuesStore}
issuesFilterStore={projectIssueViewFiltersStore}
issueStore={issues}
issuesFilterStore={issuesFilter}
QuickActions={ProjectIssueQuickActions}
issueActions={issueActions}
handleDragDrop={handleDragDrop}
viewId={viewId?.toString()}
/>
);
});

View file

@ -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);
}
};

View file

@ -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<IIssue | undefined>;
) => Promise<TIssue | undefined>;
viewId?: string;
};

View file

@ -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<Props> = 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<Props> = 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<Props> = observer((props) => {
});
};
const isEditingAllowed = !!userRole && userRole >= EUserWorkspaceRoles.MEMBER;
const isEditingAllowed = !!userRole && userRole >= EUserProjectRoles.MEMBER;
return (
<>
@ -72,7 +73,7 @@ export const CycleEmptyState: React.FC<Props> = observer((props) => {
icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />,
onClick: () => {
setTrackElement("CYCLE_EMPTY_STATE");
commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.CYCLE);
toggleCreateIssueModal(true, EIssuesStoreType.CYCLE);
},
}}
secondaryButton={

View file

@ -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 (
<div className="grid h-full w-full place-items-center">
{!projects || projects?.length === 0 ? (
{!workspaceProjectIds || workspaceProjectIds?.length === 0 ? (
<EmptyState
image={emptyProject}
title="No projects yet"
@ -35,7 +28,7 @@ export const GlobalViewEmptyState: React.FC = observer(() => {
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: <PlusIcon className="h-3 w-3" strokeWidth={2} />,
onClick: () => {
setTrackElement("ALL_ISSUES_EMPTY_STATE");
commandPaletteStore.toggleCreateIssueModal(true);
toggleCreateIssueModal(true);
},
}}
/>

View file

@ -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<Props> = 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<Props> = 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<Props> = observer((props) => {
icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />,
onClick: () => {
setTrackElement("MODULE_EMPTY_STATE");
commandPaletteStore.toggleCreateIssueModal(true);
toggleCreateIssueModal(true);
},
}}
secondaryButton={

View file

@ -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 (
<div className="grid h-full w-full place-items-center">
@ -25,7 +26,7 @@ export const ProjectViewEmptyState: React.FC = observer(() => {
icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />,
onClick: () => {
setTrackElement("VIEW_EMPTY_STATE");
commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.PROJECT_VIEW);
commandPaletteStore.toggleCreateIssueModal(true, EIssuesStoreType.PROJECT_VIEW);
},
}}
/>

View file

@ -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 (
<div className="grid h-full w-full place-items-center">
@ -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: <PlusIcon className="h-3 w-3" strokeWidth={2} />,
onClick: () => {
setTrackElement("PROJECT_EMPTY_STATE");
commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.PROJECT);
},
}
: null
}
primaryButton={{
text: "Create your first issue",
icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />,
onClick: () => {
setTrackElement("PROJECT_EMPTY_STATE");
commandPaletteStore.toggleCreateIssueModal(true, EIssuesStoreType.PROJECT);
},
}}
disabled={!isEditingAllowed}
/>
</div>

View file

@ -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<Props> = 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 (
<div className="flex flex-wrap items-stretch gap-2 bg-custom-background-100">
@ -63,7 +61,6 @@ export const AppliedFiltersList: React.FC<Props> = observer((props) => {
<AppliedMembersFilters
editable={isEditingAllowed}
handleRemove={(val) => handleRemoveFilter(filterKey, val)}
members={members}
values={value}
/>
)}
@ -103,7 +100,6 @@ export const AppliedFiltersList: React.FC<Props> = observer((props) => {
<AppliedProjectFilters
editable={isEditingAllowed}
handleRemove={(val) => handleRemoveFilter("project", val)}
projects={projects}
values={value}
/>
)}

View file

@ -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;

View file

@ -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<Props> = 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;

View file

@ -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;

View file

@ -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<Props> = 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;

View file

@ -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}
/>
<SaveFilterView workspaceSlug={workspaceSlug} projectId={projectId} filterParams={appliedFilters} />

View file

@ -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}
/>
<SaveFilterView workspaceSlug={workspaceSlug} projectId={projectId} filterParams={appliedFilters} />

View file

@ -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}
/>
<SaveFilterView workspaceSlug={workspaceSlug} projectId={projectId} filterParams={appliedFilters} />

View file

@ -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(() => {
<div className="flex items-start justify-between gap-4 p-4">
<AppliedFiltersList
labels={workspaceLabels ?? undefined}
members={workspaceMembers?.map((m) => m.member)}
projects={workspaceProjects ?? undefined}
appliedFilters={appliedFilters ?? {}}
handleClearAllFilters={handleClearAllFilters}
handleRemoveFilter={handleRemoveFilter}

View file

@ -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}
/>
<SaveFilterView workspaceSlug={workspaceSlug} projectId={projectId} filterParams={appliedFilters} />

View file

@ -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={[]}
/>
</div>

View file

@ -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 && (
<SaveFilterView workspaceSlug={workspaceSlug} projectId={projectId} filterParams={appliedFilters} />

View file

@ -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 &&

View file

@ -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;

View file

@ -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;

View file

@ -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 = {

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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> = (props) => {
const { appliedFilters, handleUpdate, members, searchQuery } = props;
export const FilterAssignees: React.FC<Props> = 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> = (props) => {
{filteredOptions ? (
filteredOptions.length > 0 ? (
<>
{filteredOptions.slice(0, itemsToRender).map((member) => (
<FilterOption
key={`assignees-${member.id}`}
isChecked={appliedFilters?.includes(member.id) ? true : false}
onClick={() => handleUpdate(member.id)}
icon={<Avatar name={member.display_name} src={member.avatar} showTooltip={false} size="md" />}
title={member.display_name}
/>
))}
{filteredOptions.slice(0, itemsToRender).map((memberId) => {
const member = getUserDetails(memberId);
if (!member) return null;
return (
<FilterOption
key={`assignees-${member.id}`}
isChecked={appliedFilters?.includes(member.id) ? true : false}
onClick={() => handleUpdate(member.id)}
icon={<Avatar name={member.display_name} src={member.avatar} showTooltip={false} size="md" />}
title={member.display_name}
/>
);
})}
{filteredOptions.length > 5 && (
<button
type="button"
@ -77,4 +85,4 @@ export const FilterAssignees: React.FC<Props> = (props) => {
)}
</>
);
};
});

View file

@ -1,29 +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 FilterCreatedBy: React.FC<Props> = (props) => {
const { appliedFilters, handleUpdate, members, searchQuery } = props;
export const FilterCreatedBy: React.FC<Props> = 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 appliedFiltersCount = appliedFilters?.length ?? 0;
const handleViewToggle = () => {
if (!filteredOptions) return;
@ -44,15 +46,20 @@ export const FilterCreatedBy: React.FC<Props> = (props) => {
{filteredOptions ? (
filteredOptions.length > 0 ? (
<>
{filteredOptions.slice(0, itemsToRender).map((member) => (
<FilterOption
key={`created-by-${member.id}`}
isChecked={appliedFilters?.includes(member.id) ? true : false}
onClick={() => handleUpdate(member.id)}
icon={<Avatar name={member.display_name} src={member.avatar} size="md" />}
title={member.display_name}
/>
))}
{filteredOptions.slice(0, itemsToRender).map((memberId) => {
const member = getUserDetails(memberId);
if (!member) return null;
return (
<FilterOption
key={`created-by-${member.id}`}
isChecked={appliedFilters?.includes(member.id) ? true : false}
onClick={() => handleUpdate(member.id)}
icon={<Avatar name={member.display_name} src={member.avatar} size="md" />}
title={member.display_name}
/>
);
})}
{filteredOptions.length > 5 && (
<button
type="button"
@ -77,4 +84,4 @@ export const FilterCreatedBy: React.FC<Props> = (props) => {
)}
</>
);
};
});

View file

@ -1,4 +1,4 @@
import React, { useState } from "react";
import { useState } from "react";
import { observer } from "mobx-react-lite";
import { Search, X } from "lucide-react";
// components
@ -15,7 +15,7 @@ import {
FilterTargetDate,
} from "components/issues";
// types
import { IIssueFilterOptions, IIssueLabel, IProject, IState, IUserLite } from "types";
import { IIssueFilterOptions, IIssueLabel, IState } from "@plane/types";
// constants
import { ILayoutDisplayFiltersOptions } from "constants/issue";
@ -24,14 +24,13 @@ type Props = {
handleFiltersUpdate: (key: keyof IIssueFilterOptions, value: string | string[]) => void;
layoutDisplayFiltersOptions: ILayoutDisplayFiltersOptions | undefined;
labels?: IIssueLabel[] | undefined;
members?: IUserLite[] | undefined;
projects?: IProject[] | undefined;
memberIds?: string[] | undefined;
states?: IState[] | undefined;
};
export const FilterSelection: React.FC<Props> = observer((props) => {
const { filters, handleFiltersUpdate, layoutDisplayFiltersOptions, labels, members, projects, states } = props;
const { filters, handleFiltersUpdate, layoutDisplayFiltersOptions, labels, memberIds, states } = props;
// states
const [filtersSearchQuery, setFiltersSearchQuery] = useState("");
const isFilterEnabled = (filter: keyof IIssueFilterOptions) => layoutDisplayFiltersOptions?.filters.includes(filter);
@ -97,7 +96,7 @@ export const FilterSelection: React.FC<Props> = observer((props) => {
<FilterAssignees
appliedFilters={filters.assignees ?? null}
handleUpdate={(val) => handleFiltersUpdate("assignees", val)}
members={members}
memberIds={memberIds}
searchQuery={filtersSearchQuery}
/>
</div>
@ -109,7 +108,7 @@ export const FilterSelection: React.FC<Props> = observer((props) => {
<FilterMentions
appliedFilters={filters.mentions ?? null}
handleUpdate={(val) => handleFiltersUpdate("mentions", val)}
members={members}
memberIds={memberIds}
searchQuery={filtersSearchQuery}
/>
</div>
@ -121,7 +120,7 @@ export const FilterSelection: React.FC<Props> = observer((props) => {
<FilterCreatedBy
appliedFilters={filters.created_by ?? null}
handleUpdate={(val) => handleFiltersUpdate("created_by", val)}
members={members}
memberIds={memberIds}
searchQuery={filtersSearchQuery}
/>
</div>
@ -144,7 +143,6 @@ export const FilterSelection: React.FC<Props> = observer((props) => {
<div className="py-2">
<FilterProjects
appliedFilters={filters.project ?? null}
projects={projects}
handleUpdate={(val) => handleFiltersUpdate("project", val)}
searchQuery={filtersSearchQuery}
/>

View file

@ -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 }) => (
<span className="h-2.5 w-2.5 rounded-full" style={{ backgroundColor: color }} />

View file

@ -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> = (props) => {
const { appliedFilters, handleUpdate, members, searchQuery } = props;
export const FilterMentions: React.FC<Props> = 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> = (props) => {
{filteredOptions ? (
filteredOptions.length > 0 ? (
<>
{filteredOptions.slice(0, itemsToRender).map((member) => (
<FilterOption
key={`mentions-${member.id}`}
isChecked={appliedFilters?.includes(member.id) ? true : false}
onClick={() => handleUpdate(member.id)}
icon={<Avatar name={member?.display_name} src={member?.avatar} showTooltip={false} size={"md"} />}
title={member.display_name}
/>
))}
{filteredOptions.slice(0, itemsToRender).map((memberId) => {
const member = getUserDetails(memberId);
if (!member) return null;
return (
<FilterOption
key={`mentions-${member.id}`}
isChecked={appliedFilters?.includes(member.id) ? true : false}
onClick={() => handleUpdate(member.id)}
icon={<Avatar name={member?.display_name} src={member?.avatar} showTooltip={false} size={"md"} />}
title={member.display_name}
/>
);
})}
{filteredOptions.length > 5 && (
<button
type="button"
@ -77,4 +85,4 @@ export const FilterMentions: React.FC<Props> = (props) => {
)}
</>
);
};
});

View file

@ -2,28 +2,29 @@ import React, { useState } from "react";
// components
import { FilterHeader, FilterOption } from "components/issues";
// hooks
import { useProject } from "hooks/store";
// ui
import { Loader } from "@plane/ui";
// helpers
import { renderEmoji } from "helpers/emoji.helper";
// types
import { IProject } from "types";
type Props = {
appliedFilters: string[] | null;
handleUpdate: (val: string) => void;
projects: IProject[] | undefined;
searchQuery: string;
};
export const FilterProjects: React.FC<Props> = (props) => {
const { appliedFilters, handleUpdate, projects, searchQuery } = props;
const { appliedFilters, handleUpdate, searchQuery } = props;
// states
const [itemsToRender, setItemsToRender] = useState(5);
const [previewEnabled, setPreviewEnabled] = useState(true);
// store
const { getProjectById, workspaceProjectIds } = useProject();
// derived values
const projects = workspaceProjectIds?.map((projectId) => getProjectById(projectId)!) ?? null;
const appliedFiltersCount = appliedFilters?.length ?? 0;
const filteredOptions = projects?.filter((project) => project.name.toLowerCase().includes(searchQuery.toLowerCase()));
const handleViewToggle = () => {

View file

@ -4,7 +4,7 @@ import { FilterHeader, FilterOption } from "components/issues";
// ui
import { Loader, StateGroupIcon } from "@plane/ui";
// types
import { IState } from "types";
import { IState } from "@plane/types";
type Props = {
appliedFilters: string[] | null;

View file

@ -3,7 +3,7 @@ import React from "react";
// ui
import { Tooltip } from "@plane/ui";
// types
import { TIssueLayouts } from "types";
import { TIssueLayouts } from "@plane/types";
// constants
import { ISSUE_LAYOUTS } from "constants/issue";

View file

@ -1,8 +1,8 @@
import React, { useCallback } 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, useUser } from "hooks/store";
// components
import { IssueGanttBlock, IssuePeekOverview } from "components/issues";
import {
@ -12,75 +12,60 @@ import {
IssueGanttSidebar,
} from "components/gantt-chart";
// types
import { IIssueUnGroupedStructure } from "store/issue";
import { IIssue } from "types";
import {
ICycleIssuesFilterStore,
ICycleIssuesStore,
IModuleIssuesFilterStore,
IModuleIssuesStore,
IProjectIssuesFilterStore,
IProjectIssuesStore,
IViewIssuesFilterStore,
IViewIssuesStore,
} from "store/issues";
import { TUnGroupedIssues } from "store/issues/types";
import { TIssue, TUnGroupedIssues } from "@plane/types";
import { EUserProjectRoles } from "constants/project";
import { ICycleIssues, ICycleIssuesFilter } from "store/issue/cycle";
import { IModuleIssues, IModuleIssuesFilter } from "store/issue/module";
import { IProjectIssues, IProjectIssuesFilter } from "store/issue/project";
import { IProjectViewIssues, IProjectViewIssuesFilter } from "store/issue/project-views";
import { EIssueActions } from "../types";
// constants
import { EUserWorkspaceRoles } from "constants/workspace";
interface IBaseGanttRoot {
issueFiltersStore:
| IProjectIssuesFilterStore
| IModuleIssuesFilterStore
| ICycleIssuesFilterStore
| IViewIssuesFilterStore;
issueStore: IProjectIssuesStore | IModuleIssuesStore | ICycleIssuesStore | IViewIssuesStore;
issueFiltersStore: IProjectIssuesFilter | IModuleIssuesFilter | ICycleIssuesFilter | IProjectViewIssuesFilter;
issueStore: IProjectIssues | IModuleIssues | ICycleIssues | IProjectViewIssues;
viewId?: string;
issueActions: {
[EIssueActions.DELETE]: (issue: IIssue) => Promise<void>;
[EIssueActions.UPDATE]?: (issue: IIssue) => Promise<void>;
[EIssueActions.REMOVE]?: (issue: IIssue) => Promise<void>;
[EIssueActions.DELETE]: (issue: TIssue) => Promise<void>;
[EIssueActions.UPDATE]?: (issue: TIssue) => Promise<void>;
[EIssueActions.REMOVE]?: (issue: TIssue) => Promise<void>;
};
}
export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGanttRoot) => {
const { issueFiltersStore, issueStore, viewId, issueActions } = props;
// router
const router = useRouter();
const { workspaceSlug, peekIssueId, peekProjectId } = router.query;
// store hooks
const {
user: { currentProjectRole },
} = useMobxStore();
membership: { currentProjectRole },
} = useUser();
const { issueMap } = useIssues();
const appliedDisplayFilters = issueFiltersStore.issueFilters?.displayFilters;
const issuesResponse = issueStore.getIssues;
const issueIds = (issueStore.getIssuesIds ?? []) as TUnGroupedIssues;
const issueIds = (issueStore.groupedIssueIds ?? []) as TUnGroupedIssues;
const { enableIssueCreation } = issueStore?.viewFlags || {};
const issues = issueIds.map((id) => issuesResponse?.[id]);
const issues = issueIds.map((id) => issueMap?.[id]);
const updateIssueBlockStructure = async (issue: IIssue, data: IBlockUpdateData) => {
const updateIssueBlockStructure = async (issue: TIssue, data: IBlockUpdateData) => {
if (!workspaceSlug) return;
const payload: any = { ...data };
if (data.sort_order) payload.sort_order = data.sort_order.newSortOrder;
await issueStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, payload, viewId);
await issueStore.updateIssue(workspaceSlug.toString(), issue.project_id, issue.id, payload, viewId);
};
const handleIssues = useCallback(
async (issue: IIssue, action: EIssueActions) => {
async (issue: TIssue, action: EIssueActions) => {
if (issueActions[action]) {
await issueActions[action]!(issue);
}
},
[issueActions]
);
const isAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
const isAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
return (
<>
@ -89,9 +74,9 @@ export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGan
border={false}
title="Issues"
loaderTitle="Issues"
blocks={issues ? renderIssueBlocksStructure(issues as IIssueUnGroupedStructure) : null}
blocks={issues ? renderIssueBlocksStructure(issues as TIssue[]) : null}
blockUpdateHandler={updateIssueBlockStructure}
blockToRender={(data: IIssue) => <IssueGanttBlock data={data} />}
blockToRender={(data: TIssue) => <IssueGanttBlock data={data} />}
sidebarToRender={(props) => (
<IssueGanttSidebar
{...props}
@ -113,7 +98,7 @@ export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGan
projectId={peekProjectId.toString()}
issueId={peekIssueId.toString()}
handleIssue={async (issueToUpdate, action) => {
await handleIssues(issueToUpdate as IIssue, action);
await handleIssues(issueToUpdate as TIssue, action);
}}
/>
)}

View file

@ -4,28 +4,29 @@ import { Tooltip, StateGroupIcon } from "@plane/ui";
// helpers
import { renderFormattedDate } from "helpers/date-time.helper";
// types
import { IIssue } from "types";
import { TIssue } from "@plane/types";
import { useProject, useProjectState } from "hooks/store";
export const IssueGanttBlock = ({ data }: { data: IIssue }) => {
export const IssueGanttBlock = ({ data }: { data: TIssue }) => {
const router = useRouter();
// hooks
const { getProjectStates } = useProjectState();
const handleIssuePeekOverview = (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
const handleIssuePeekOverview = () => {
const { query } = router;
if (event.ctrlKey || event.metaKey) {
const issueUrl = `/${data?.workspace_detail.slug}/projects/${data?.project_detail.id}/issues/${data?.id}`;
window.open(issueUrl, "_blank"); // Open link in a new tab
} else {
router.push({
pathname: router.pathname,
query: { ...query, peekIssueId: data?.id, peekProjectId: data?.project },
});
}
router.push({
pathname: router.pathname,
query: { ...query, peekIssueId: data?.id, peekProjectId: data?.project_id },
});
};
return (
<div
className="relative flex h-full w-full cursor-pointer items-center rounded"
style={{ backgroundColor: data?.state_detail?.color }}
style={{
backgroundColor: getProjectStates(data?.project_id)?.find((state) => state?.id == data?.state_id)?.color,
}}
onClick={handleIssuePeekOverview}
>
<div className="absolute left-0 top-0 h-full w-full bg-custom-background-100/50" />
@ -47,23 +48,31 @@ export const IssueGanttBlock = ({ data }: { data: IIssue }) => {
};
// rendering issues on gantt sidebar
export const IssueGanttSidebarBlock = ({ data }: { data: IIssue }) => {
export const IssueGanttSidebarBlock = ({ data }: { data: TIssue }) => {
const router = useRouter();
// hooks
const { getProjectStates } = useProjectState();
const { getProjectById } = useProject();
const handleIssuePeekOverview = () => {
const { query } = router;
router.push({
pathname: router.pathname,
query: { ...query, peekIssueId: data?.id, peekProjectId: data?.project },
query: { ...query, peekIssueId: data?.id, peekProjectId: data?.project_id },
});
};
const currentStateDetails =
getProjectStates(data?.project_id)?.find((state) => state?.id == data?.state_id) || undefined;
return (
<div className="relative flex h-full w-full cursor-pointer items-center gap-2" onClick={handleIssuePeekOverview}>
<StateGroupIcon stateGroup={data?.state_detail?.group} color={data?.state_detail?.color} />
{currentStateDetails != undefined && (
<StateGroupIcon stateGroup={currentStateDetails?.group} color={currentStateDetails?.color} />
)}
<div className="flex-shrink-0 text-xs text-custom-text-300">
{data?.project_detail?.identifier} {data?.sequence_id}
{getProjectById(data?.project_id)?.identifier} {data?.sequence_id}
</div>
<Tooltip tooltipHeading="Title" tooltipContent={data.name}>
<span className="flex-grow truncate text-sm font-medium">{data?.name}</span>

View file

@ -1,55 +1,47 @@
import { observer } from "mobx-react-lite";
import { useRouter } from "next/router";
// hooks
import { useMobxStore } from "lib/mobx/store-provider";
import { useCycle, useIssues } from "hooks/store";
// components
import { BaseGanttRoot } from "./base-gantt-root";
import { useRouter } from "next/router";
// types
import { EIssuesStoreType } from "constants/issue";
import { EIssueActions } from "../types";
import { IIssue } from "types";
import { TIssue } from "@plane/types";
export const CycleGanttLayout: React.FC = observer(() => {
// router
const router = useRouter();
const { cycleId, workspaceSlug } = router.query;
const {
cycleIssues: cycleIssueStore,
cycleIssuesFilter: cycleIssueFilterStore,
cycle: { fetchCycleWithId },
} = useMobxStore();
const { workspaceSlug, cycleId } = router.query;
// store hooks
const { issues, issuesFilter } = useIssues(EIssuesStoreType.CYCLE);
const { fetchCycleDetails } = useCycle();
const issueActions = {
[EIssueActions.UPDATE]: async (issue: IIssue) => {
[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());
await issues.updateIssue(workspaceSlug.toString(), issue.project_id, issue.id, issue, cycleId.toString());
fetchCycleDetails(workspaceSlug.toString(), issue.project_id, cycleId.toString());
},
[EIssueActions.DELETE]: async (issue: IIssue) => {
[EIssueActions.DELETE]: async (issue: TIssue) => {
if (!workspaceSlug || !cycleId) return;
await cycleIssueStore.removeIssue(workspaceSlug.toString(), issue.project, issue.id, cycleId.toString());
fetchCycleWithId(workspaceSlug.toString(), issue.project, cycleId.toString());
await issues.removeIssue(workspaceSlug.toString(), issue.project_id, issue.id, cycleId.toString());
fetchCycleDetails(workspaceSlug.toString(), issue.project_id, cycleId.toString());
},
[EIssueActions.REMOVE]: async (issue: IIssue) => {
if (!workspaceSlug || !cycleId || !issue.bridge_id) return;
[EIssueActions.REMOVE]: async (issue: TIssue) => {
if (!workspaceSlug || !cycleId || !issue.id) return;
await cycleIssueStore.removeIssueFromCycle(
workspaceSlug.toString(),
issue.project,
cycleId.toString(),
issue.id,
issue.bridge_id
);
fetchCycleWithId(workspaceSlug.toString(), issue.project, cycleId.toString());
await issues.removeIssueFromCycle(workspaceSlug.toString(), issue.project_id, cycleId.toString(), issue.id);
fetchCycleDetails(workspaceSlug.toString(), issue.project_id, cycleId.toString());
},
};
return (
<BaseGanttRoot
issueActions={issueActions}
issueFiltersStore={cycleIssueFilterStore}
issueStore={cycleIssueStore}
issueFiltersStore={issuesFilter}
issueStore={issues}
viewId={cycleId?.toString()}
/>
);

View file

@ -1,55 +1,47 @@
import { observer } from "mobx-react-lite";
import { useRouter } from "next/router";
// hooks
import { useMobxStore } from "lib/mobx/store-provider";
import { useIssues, useModule } from "hooks/store";
// components
import { BaseGanttRoot } from "./base-gantt-root";
import { useRouter } from "next/router";
// types
import { EIssuesStoreType } from "constants/issue";
import { EIssueActions } from "../types";
import { IIssue } from "types";
import { TIssue } from "@plane/types";
export const ModuleGanttLayout: React.FC = observer(() => {
// router
const router = useRouter();
const { moduleId, workspaceSlug } = router.query;
const {
moduleIssues: moduleIssueStore,
moduleIssuesFilter: moduleIssueFilterStore,
module: { fetchModuleDetails },
} = useMobxStore();
const { workspaceSlug, moduleId } = router.query;
// store hooks
const { issues, issuesFilter } = useIssues(EIssuesStoreType.MODULE);
const { fetchModuleDetails } = useModule();
const issueActions = {
[EIssueActions.UPDATE]: async (issue: IIssue) => {
[EIssueActions.UPDATE]: async (issue: TIssue) => {
if (!workspaceSlug || !moduleId) return;
await moduleIssueStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue, moduleId.toString());
fetchModuleDetails(workspaceSlug.toString(), issue.project, moduleId.toString());
await issues.updateIssue(workspaceSlug.toString(), issue.project_id, issue.id, issue, moduleId.toString());
fetchModuleDetails(workspaceSlug.toString(), issue.project_id, moduleId.toString());
},
[EIssueActions.DELETE]: async (issue: IIssue) => {
[EIssueActions.DELETE]: async (issue: TIssue) => {
if (!workspaceSlug || !moduleId) return;
await moduleIssueStore.removeIssue(workspaceSlug.toString(), issue.project, issue.id, moduleId.toString());
fetchModuleDetails(workspaceSlug.toString(), issue.project, moduleId.toString());
await issues.removeIssue(workspaceSlug.toString(), issue.project_id, issue.id, moduleId.toString());
fetchModuleDetails(workspaceSlug.toString(), issue.project_id, moduleId.toString());
},
[EIssueActions.REMOVE]: async (issue: IIssue) => {
if (!workspaceSlug || !moduleId || !issue.bridge_id) return;
[EIssueActions.REMOVE]: async (issue: TIssue) => {
if (!workspaceSlug || !moduleId || !issue.id) return;
await moduleIssueStore.removeIssueFromModule(
workspaceSlug.toString(),
issue.project,
moduleId.toString(),
issue.id,
issue.bridge_id
);
fetchModuleDetails(workspaceSlug.toString(), issue.project, moduleId.toString());
await issues.removeIssueFromModule(workspaceSlug.toString(), issue.project_id, moduleId.toString(), issue.id);
fetchModuleDetails(workspaceSlug.toString(), issue.project_id, moduleId.toString());
},
};
return (
<BaseGanttRoot
issueActions={issueActions}
issueFiltersStore={moduleIssueFilterStore}
issueStore={moduleIssueStore}
issueFiltersStore={issuesFilter}
issueStore={issues}
viewId={moduleId?.toString()}
/>
);

View file

@ -2,35 +2,32 @@ import React 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 { BaseGanttRoot } from "./base-gantt-root";
// types
import { EIssuesStoreType } from "constants/issue";
import { EIssueActions } from "../types";
import { IIssue } from "types";
import { TIssue } from "@plane/types";
export const GanttLayout: React.FC = observer(() => {
const { projectIssues: projectIssuesStore, projectIssuesFilter: projectIssueFiltersStore } = useMobxStore();
// router
const router = useRouter();
const { workspaceSlug } = router.query;
// store hooks
const { issues, issuesFilter } = useIssues(EIssuesStoreType.PROJECT);
const issueActions = {
[EIssueActions.UPDATE]: async (issue: IIssue) => {
[EIssueActions.UPDATE]: async (issue: TIssue) => {
if (!workspaceSlug) return;
await projectIssuesStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue);
await issues.updateIssue(workspaceSlug.toString(), issue.project_id, issue.id, issue);
},
[EIssueActions.DELETE]: async (issue: IIssue) => {
[EIssueActions.DELETE]: async (issue: TIssue) => {
if (!workspaceSlug) return;
await projectIssuesStore.removeIssue(workspaceSlug.toString(), issue.project, issue.id);
await issues.removeIssue(workspaceSlug.toString(), issue.project_id, issue.id);
},
};
return (
<BaseGanttRoot
issueActions={issueActions}
issueFiltersStore={projectIssueFiltersStore}
issueStore={projectIssuesStore}
/>
);
return <BaseGanttRoot issueFiltersStore={issuesFilter} issueStore={issues} issueActions={issueActions} />;
});

View file

@ -1,35 +1,31 @@
import { observer } from "mobx-react-lite";
import { useRouter } from "next/router";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
import { observer } from "mobx-react-lite";
// hooks
import { useIssues } from "hooks/store";
// components
import { BaseGanttRoot } from "./base-gantt-root";
// types
import { EIssuesStoreType } from "constants/issue";
import { EIssueActions } from "../types";
import { IIssue } from "types";
import { TIssue } from "@plane/types";
export const ProjectViewGanttLayout: React.FC = observer(() => {
const { viewIssues: projectIssueViewStore, viewIssuesFilter: projectIssueViewFiltersStore } = useMobxStore();
const { issues, issuesFilter } = useIssues(EIssuesStoreType.PROJECT_VIEW);
// router
const router = useRouter();
const { workspaceSlug } = router.query;
const issueActions = {
[EIssueActions.UPDATE]: async (issue: IIssue) => {
[EIssueActions.UPDATE]: async (issue: TIssue) => {
if (!workspaceSlug) return;
await projectIssueViewStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue);
await issues.updateIssue(workspaceSlug.toString(), issue.project_id, issue.id, issue);
},
[EIssueActions.DELETE]: async (issue: IIssue) => {
[EIssueActions.DELETE]: async (issue: TIssue) => {
if (!workspaceSlug) return;
await projectIssueViewStore.removeIssue(workspaceSlug.toString(), issue.project, issue.id);
await issues.removeIssue(workspaceSlug.toString(), issue.project_id, issue.id);
},
};
return (
<BaseGanttRoot
issueActions={issueActions}
issueFiltersStore={projectIssueViewFiltersStore}
issueStore={projectIssueViewStore}
/>
);
return <BaseGanttRoot issueFiltersStore={issuesFilter} issueStore={issues} issueActions={issueActions} />;
});

View file

@ -3,32 +3,30 @@ import { useRouter } from "next/router";
import { useForm } from "react-hook-form";
import { observer } from "mobx-react-lite";
import { PlusIcon } from "lucide-react";
// mobx 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 useProjectDetails from "hooks/use-project-details";
import useOutsideClickDetector from "hooks/use-outside-click-detector";
// helpers
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
import { createIssuePayload } from "helpers/issue.helper";
// types
import { IIssue } from "types";
import { TIssue } from "@plane/types";
type Props = {
prePopulatedData?: Partial<IIssue>;
onSuccess?: (data: IIssue) => Promise<void> | void;
prePopulatedData?: Partial<TIssue>;
onSuccess?: (data: TIssue) => Promise<void> | void;
quickAddCallback?: (
workspaceSlug: string,
projectId: string,
data: IIssue,
data: TIssue,
viewId?: string
) => Promise<IIssue | undefined>;
) => Promise<TIssue | undefined>;
viewId?: string;
};
const defaultValues: Partial<IIssue> = {
const defaultValues: Partial<TIssue> = {
name: "",
};
@ -54,23 +52,20 @@ const Inputs = (props: any) => {
export const GanttInlineCreateIssueForm: React.FC<Props> = observer((props) => {
const { prePopulatedData, quickAddCallback, viewId } = props;
// router
const router = useRouter();
const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string };
// store
const { workspace: workspaceStore } = useMobxStore();
const { projectDetails } = useProjectDetails();
const { workspaceSlug, projectId } = router.query;
// store hooks
const { getWorkspaceBySlug } = useWorkspace();
const { currentProjectDetails } = useProject();
// form info
const {
reset,
handleSubmit,
setFocus,
register,
formState: { errors, isSubmitting },
} = useForm<IIssue>({ defaultValues });
} = useForm<TIssue>({ defaultValues });
// ref
const ref = useRef<HTMLFormElement>(null);
@ -86,7 +81,7 @@ export const GanttInlineCreateIssueForm: React.FC<Props> = observer((props) => {
const { setToastAlert } = useToast();
// derived values
const workspaceDetail = workspaceStore.getWorkspaceBySlug(workspaceSlug?.toString()!);
const workspaceDetail = getWorkspaceBySlug(workspaceSlug?.toString()!);
useEffect(() => {
if (!isOpen) reset({ ...defaultValues });
@ -96,7 +91,7 @@ export const GanttInlineCreateIssueForm: React.FC<Props> = 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",
@ -106,13 +101,13 @@ export const GanttInlineCreateIssueForm: React.FC<Props> = observer((props) => {
});
}, [errors, setToastAlert]);
const onSubmitHandler = async (formData: IIssue) => {
const onSubmitHandler = async (formData: TIssue) => {
if (isSubmitting || !workspaceSlug || !projectId) return;
// resetting the form so that user can add another issue quickly
reset({ ...defaultValues, ...(prePopulatedData ?? {}) });
const payload = createIssuePayload(workspaceDetail!, projectDetails!, {
const payload = createIssuePayload(workspaceDetail!, currentProjectDetails!, {
...(prePopulatedData ?? {}),
...formData,
start_date: renderFormattedPayloadDate(new Date()),
@ -121,7 +116,7 @@ export const GanttInlineCreateIssueForm: React.FC<Props> = observer((props) => {
try {
if (quickAddCallback) {
await quickAddCallback(workspaceSlug, projectId, payload, viewId);
await quickAddCallback(workspaceSlug.toString(), projectId.toString(), payload, viewId);
}
setToastAlert({
type: "success",
@ -151,7 +146,7 @@ export const GanttInlineCreateIssueForm: React.FC<Props> = observer((props) => {
onSubmit={handleSubmit(onSubmitHandler)}
>
<div className="h-3 w-3 flex-shrink-0 rounded-full border border-custom-border-1000" />
<h4 className="text-xs text-custom-text-400">{projectDetails?.identifier ?? "..."}</h4>
<h4 className="text-xs text-custom-text-400">{currentProjectDetails?.identifier ?? "..."}</h4>
<Inputs register={register} setFocus={setFocus} />
</form>
)}

View file

@ -2,72 +2,50 @@ import { FC, useCallback, useState } from "react";
import { DragDropContext, DragStart, DraggableLocation, DropResult, Droppable } from "@hello-pangea/dnd";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// hooks
import { useUser } from "hooks/store";
import useToast from "hooks/use-toast";
// ui
import { Spinner } from "@plane/ui";
// types
import { IIssue } from "types";
import { TIssue } from "@plane/types";
import { EIssueActions } from "../types";
import {
ICycleIssuesFilterStore,
ICycleIssuesStore,
IModuleIssuesFilterStore,
IModuleIssuesStore,
IProfileIssuesFilterStore,
IProfileIssuesStore,
IProjectDraftIssuesStore,
IProjectIssuesFilterStore,
IProjectIssuesStore,
IViewIssuesFilterStore,
IViewIssuesStore,
} from "store/issues";
import { IQuickActionProps } from "../list/list-view-types";
import { IIssueKanBanViewStore } from "store/issue";
// hooks
import useToast from "hooks/use-toast";
// constants
import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue";
import { IProjectIssues, IProjectIssuesFilter } from "store/issue/project";
//components
import { KanBan } from "./default";
import { KanBanSwimLanes } from "./swimlanes";
import { EProjectStore } from "store/command-palette.store";
import { DeleteIssueModal, IssuePeekOverview } from "components/issues";
import { EUserWorkspaceRoles } from "constants/workspace";
import { EUserProjectRoles } from "constants/project";
import { useIssues } from "hooks/store/use-issues";
import { handleDragDrop } from "./utils";
import { IssueKanBanViewStore } from "store/issue/issue_kanban_view.store";
import { ICycleIssues, ICycleIssuesFilter } from "store/issue/cycle";
import { IDraftIssues, IDraftIssuesFilter } from "store/issue/draft";
import { IProfileIssues, IProfileIssuesFilter } from "store/issue/profile";
import { IModuleIssues, IModuleIssuesFilter } from "store/issue/module";
import { IProjectViewIssues, IProjectViewIssuesFilter } from "store/issue/project-views";
import { TCreateModalStoreTypes } from "constants/issue";
export interface IBaseKanBanLayout {
issueStore:
| IProjectIssuesStore
| IModuleIssuesStore
| ICycleIssuesStore
| IViewIssuesStore
| IProjectDraftIssuesStore
| IProfileIssuesStore;
issuesFilterStore:
| IProjectIssuesFilterStore
| IModuleIssuesFilterStore
| ICycleIssuesFilterStore
| IViewIssuesFilterStore
| IProfileIssuesFilterStore;
kanbanViewStore: IIssueKanBanViewStore;
issues: IProjectIssues | ICycleIssues | IDraftIssues | IModuleIssues | IProjectViewIssues | IProfileIssues;
issuesFilter:
| IProjectIssuesFilter
| IModuleIssuesFilter
| ICycleIssuesFilter
| IDraftIssuesFilter
| IProjectViewIssuesFilter
| IProfileIssuesFilter;
QuickActions: FC<IQuickActionProps>;
issueActions: {
[EIssueActions.DELETE]: (issue: IIssue) => Promise<void>;
[EIssueActions.UPDATE]?: (issue: IIssue) => Promise<void>;
[EIssueActions.REMOVE]?: (issue: IIssue) => Promise<void>;
[EIssueActions.DELETE]: (issue: TIssue) => Promise<void>;
[EIssueActions.UPDATE]?: (issue: TIssue) => Promise<void>;
[EIssueActions.REMOVE]?: (issue: TIssue) => Promise<void>;
};
showLoader?: boolean;
viewId?: string;
currentStore?: EProjectStore;
handleDragDrop?: (
source: any,
destination: any,
subGroupBy: string | null,
groupBy: string | null,
issues: any,
issueWithIds: any
) => Promise<IIssue | undefined>;
addIssuesToView?: (issueIds: string[]) => Promise<IIssue>;
currentStore?: TCreateModalStoreTypes;
addIssuesToView?: (issueIds: string[]) => Promise<TIssue>;
canEditPropertiesBasedOnProject?: (projectId: string) => boolean;
}
@ -79,66 +57,60 @@ type KanbanDragState = {
export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBaseKanBanLayout) => {
const {
issueStore,
issuesFilterStore,
kanbanViewStore,
issues,
issuesFilter,
QuickActions,
issueActions,
showLoader,
viewId,
currentStore,
handleDragDrop,
addIssuesToView,
canEditPropertiesBasedOnProject,
} = props;
// router
const router = useRouter();
const { workspaceSlug, peekIssueId, peekProjectId } = router.query;
// mobx store
const { workspaceSlug, projectId, peekIssueId, peekProjectId } = router.query;
// store hooks
const {
project: { workspaceProjects },
projectLabel: { projectLabels },
projectMember: { projectMembers },
projectState: projectStateStore,
user: userStore,
} = useMobxStore();
// hooks
membership: { currentProjectRole },
} = useUser();
const { issueMap } = useIssues();
// toast alert
const { setToastAlert } = useToast();
const { currentProjectRole } = userStore;
// FIXME get from filters
const kanbanViewStore: IssueKanBanViewStore = {} as IssueKanBanViewStore;
const issues = issueStore?.getIssues || {};
const issueIds = issueStore?.getIssuesIds || [];
const issueIds = issues?.groupedIssueIds || [];
const displayFilters = issuesFilterStore?.issueFilters?.displayFilters;
const displayProperties = issuesFilterStore?.issueFilters?.displayProperties || null;
const displayFilters = issuesFilter?.issueFilters?.displayFilters;
const displayProperties = issuesFilter?.issueFilters?.displayProperties;
const sub_group_by: string | null = displayFilters?.sub_group_by || null;
const group_by: string | null = displayFilters?.group_by || null;
const order_by: string | null = displayFilters?.order_by || null;
const userDisplayFilters = displayFilters || null;
const currentKanBanView: "swimlanes" | "default" = sub_group_by ? "swimlanes" : "default";
const KanBanView = sub_group_by ? KanBanSwimLanes : KanBan;
const { enableInlineEditing, enableQuickAdd, enableIssueCreation } = issueStore?.viewFlags || {};
const { enableInlineEditing, enableQuickAdd, enableIssueCreation } = issues?.viewFlags || {};
// states
const [isDragStarted, setIsDragStarted] = useState<boolean>(false);
const [dragState, setDragState] = useState<KanbanDragState>({});
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
const canEditProperties = (projectId: string | undefined) => {
const isEditingAllowedBasedOnProject =
canEditPropertiesBasedOnProject && projectId ? canEditPropertiesBasedOnProject(projectId) : isEditingAllowed;
const canEditProperties = useCallback(
(projectId: string | undefined) => {
const isEditingAllowedBasedOnProject =
canEditPropertiesBasedOnProject && projectId ? canEditPropertiesBasedOnProject(projectId) : isEditingAllowed;
return enableInlineEditing && isEditingAllowedBasedOnProject;
};
return enableInlineEditing && isEditingAllowedBasedOnProject;
},
[canEditPropertiesBasedOnProject, enableInlineEditing, isEditingAllowed]
);
const onDragStart = (dragStart: DragStart) => {
setDragState({
@ -171,21 +143,30 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
});
setDeleteIssueModal(true);
} else {
await handleDragDrop(result.source, result.destination, sub_group_by, group_by, issues, issueIds).catch(
(err) => {
setToastAlert({
title: "Error",
type: "error",
message: err.detail ?? "Failed to perform this action",
});
}
);
await handleDragDrop(
result.source,
result.destination,
workspaceSlug?.toString(),
projectId?.toString(),
issues,
sub_group_by,
group_by,
issueMap,
issueIds,
viewId
).catch((err) => {
setToastAlert({
title: "Error",
type: "error",
message: err.detail ?? "Failed to perform this action",
});
});
}
}
};
const handleIssues = useCallback(
async (sub_group_by: string | null, group_by: string | null, issue: IIssue, action: EIssueActions) => {
async (issue: TIssue, action: EIssueActions) => {
if (issueActions[action]) {
await issueActions[action]!(issue);
}
@ -193,34 +174,57 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
[issueActions]
);
const renderQuickActions = useCallback(
(issue: TIssue, customActionButton?: React.ReactElement) => (
<QuickActions
customActionButton={customActionButton}
issue={issue}
handleDelete={async () => handleIssues(issue, EIssueActions.DELETE)}
handleUpdate={
issueActions[EIssueActions.UPDATE] ? async (data) => handleIssues(data, EIssueActions.UPDATE) : undefined
}
handleRemoveFromView={
issueActions[EIssueActions.REMOVE] ? async () => handleIssues(issue, EIssueActions.REMOVE) : undefined
}
/>
),
// eslint-disable-next-line react-hooks/exhaustive-deps
[issueActions, handleIssues]
);
const handleDeleteIssue = async () => {
if (!handleDragDrop) return;
await handleDragDrop(dragState.source, dragState.destination, sub_group_by, group_by, issues, issueIds).finally(
() => {
setDeleteIssueModal(false);
setDragState({});
}
);
await handleDragDrop(
dragState.source,
dragState.destination,
workspaceSlug?.toString(),
projectId?.toString(),
issues,
sub_group_by,
group_by,
issueMap,
issueIds,
viewId
).finally(() => {
setDeleteIssueModal(false);
setDragState({});
});
};
const handleKanBanToggle = (toggle: "groupByHeaderMinMax" | "subgroupByIssuesVisibility", value: string) => {
kanbanViewStore.handleKanBanToggle(toggle, value);
};
const states = projectStateStore?.projectStates || null;
const priorities = ISSUE_PRIORITIES || null;
const stateGroups = ISSUE_STATE_GROUPS || null;
return (
<>
<DeleteIssueModal
data={dragState.draggedIssueId ? issues[dragState.draggedIssueId] : ({} as IIssue)}
dataId={dragState.draggedIssueId}
isOpen={deleteIssueModal}
handleClose={() => setDeleteIssueModal(false)}
onSubmit={handleDeleteIssue}
/>
{showLoader && issueStore?.loader === "init-loader" && (
{showLoader && issues?.loader === "init-loader" && (
<div className="fixed right-2 top-16 z-30 flex h-10 w-10 items-center justify-center rounded bg-custom-background-80 shadow-custom-shadow-sm">
<Spinner className="h-5 w-5" />
</div>
@ -250,94 +254,25 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
</Droppable>
</div>
{currentKanBanView === "default" ? (
<KanBan
issues={issues}
issueIds={issueIds}
sub_group_by={sub_group_by}
group_by={group_by}
order_by={order_by}
handleIssues={handleIssues}
quickActions={(sub_group_by, group_by, issue, customActionButton) => (
<QuickActions
customActionButton={customActionButton}
issue={issue}
handleDelete={async () => handleIssues(sub_group_by, group_by, issue, EIssueActions.DELETE)}
handleUpdate={
issueActions[EIssueActions.UPDATE]
? async (data) => handleIssues(sub_group_by, group_by, data, EIssueActions.UPDATE)
: undefined
}
handleRemoveFromView={
issueActions[EIssueActions.REMOVE]
? async () => handleIssues(sub_group_by, group_by, issue, EIssueActions.REMOVE)
: undefined
}
/>
)}
displayProperties={displayProperties}
kanBanToggle={kanbanViewStore?.kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
states={states}
stateGroups={stateGroups}
priorities={priorities}
labels={projectLabels}
members={projectMembers?.map((m) => m.member) ?? null}
projects={workspaceProjects}
enableQuickIssueCreate={enableQuickAdd}
showEmptyGroup={userDisplayFilters?.show_empty_groups || true}
isDragStarted={isDragStarted}
quickAddCallback={issueStore?.quickAddIssue}
viewId={viewId}
disableIssueCreation={!enableIssueCreation || !isEditingAllowed}
canEditProperties={canEditProperties}
currentStore={currentStore}
addIssuesToView={addIssuesToView}
/>
) : (
<KanBanSwimLanes
issues={issues}
issueIds={issueIds}
sub_group_by={sub_group_by}
group_by={group_by}
order_by={order_by}
handleIssues={handleIssues}
quickActions={(sub_group_by, group_by, issue, customActionButton) => (
<QuickActions
customActionButton={customActionButton}
issue={issue}
handleDelete={async () => handleIssues(sub_group_by, group_by, issue, EIssueActions.DELETE)}
handleUpdate={
issueActions[EIssueActions.UPDATE]
? async (data) => handleIssues(sub_group_by, group_by, data, EIssueActions.UPDATE)
: undefined
}
handleRemoveFromView={
issueActions[EIssueActions.REMOVE]
? async () => handleIssues(sub_group_by, group_by, issue, EIssueActions.REMOVE)
: undefined
}
/>
)}
displayProperties={displayProperties}
kanBanToggle={kanbanViewStore?.kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
states={states}
stateGroups={stateGroups}
priorities={priorities}
labels={projectLabels}
members={projectMembers?.map((m) => m.member) ?? null}
projects={workspaceProjects}
showEmptyGroup={userDisplayFilters?.show_empty_groups || true}
isDragStarted={isDragStarted}
disableIssueCreation={!enableIssueCreation || !isEditingAllowed}
enableQuickIssueCreate={enableQuickAdd}
currentStore={currentStore}
quickAddCallback={issueStore?.quickAddIssue}
addIssuesToView={addIssuesToView}
canEditProperties={canEditProperties}
/>
)}
<KanBanView
issuesMap={issueMap}
issueIds={issueIds}
displayProperties={displayProperties}
sub_group_by={sub_group_by}
group_by={group_by}
handleIssues={handleIssues}
quickActions={renderQuickActions}
kanBanToggle={kanbanViewStore?.kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
enableQuickIssueCreate={enableQuickAdd}
showEmptyGroup={userDisplayFilters?.show_empty_groups || true}
quickAddCallback={issues?.quickAddIssue}
viewId={viewId}
disableIssueCreation={!enableIssueCreation || !isEditingAllowed}
canEditProperties={canEditProperties}
currentStore={currentStore}
addIssuesToView={addIssuesToView}
/>
</DragDropContext>
</div>
@ -346,9 +281,7 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
workspaceSlug={workspaceSlug.toString()}
projectId={peekProjectId.toString()}
issueId={peekIssueId.toString()}
handleIssue={async (issueToUpdate, action: EIssueActions) =>
await handleIssues(sub_group_by, group_by, issueToUpdate as IIssue, action)
}
handleIssue={async (issueToUpdate) => await handleIssues(issueToUpdate as TIssue, EIssueActions.UPDATE)}
/>
)}
</>

View file

@ -1,171 +1,128 @@
import { memo } from "react";
import { Draggable, DraggableStateSnapshot } from "@hello-pangea/dnd";
import isEqual from "lodash/isEqual";
import { DraggableProvided, DraggableStateSnapshot } from "@hello-pangea/dnd";
import { observer } from "mobx-react-lite";
// components
import { KanBanProperties } from "./properties";
import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC";
import { IssueProperties } from "../properties/all-properties";
// ui
import { Tooltip } from "@plane/ui";
// types
import { IIssueDisplayProperties, IIssue } from "types";
import { TIssue, IIssueDisplayProperties, IIssueMap } from "@plane/types";
import { EIssueActions } from "../types";
import { useRouter } from "next/router";
import { useProject } from "hooks/store";
interface IssueBlockProps {
sub_group_id: string;
columnId: string;
index: number;
issue: IIssue;
issueId: string;
issuesMap: IIssueMap;
displayProperties: IIssueDisplayProperties | undefined;
isDragDisabled: boolean;
showEmptyGroup: boolean;
handleIssues: (sub_group_by: string | null, group_by: string | null, issue: IIssue, action: EIssueActions) => void;
quickActions: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => React.ReactNode;
displayProperties: IIssueDisplayProperties | null;
handleIssues: (issue: TIssue, action: EIssueActions) => void;
quickActions: (issue: TIssue) => React.ReactNode;
canEditProperties: (projectId: string | undefined) => boolean;
provided: DraggableProvided;
snapshot: DraggableStateSnapshot;
}
interface IssueDetailsBlockProps {
sub_group_id: string;
columnId: string;
issue: IIssue;
showEmptyGroup: boolean;
handleIssues: (sub_group_by: string | null, group_by: string | null, issue: IIssue, action: EIssueActions) => void;
quickActions: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => React.ReactNode;
displayProperties: IIssueDisplayProperties | null;
issue: TIssue;
displayProperties: IIssueDisplayProperties | undefined;
handleIssues: (issue: TIssue, action: EIssueActions) => void;
quickActions: (issue: TIssue) => React.ReactNode;
isReadOnly: boolean;
snapshot: DraggableStateSnapshot;
isDragDisabled: boolean;
}
const KanbanIssueDetailsBlock: React.FC<IssueDetailsBlockProps> = (props) => {
const {
sub_group_id,
columnId,
issue,
showEmptyGroup,
handleIssues,
quickActions,
displayProperties,
isReadOnly,
snapshot,
isDragDisabled,
} = props;
const KanbanIssueDetailsBlock: React.FC<IssueDetailsBlockProps> = observer((props: IssueDetailsBlockProps) => {
const { issue, handleIssues, quickActions, isReadOnly, displayProperties } = props;
const router = useRouter();
const updateIssue = (sub_group_by: string | null, group_by: string | null, issueToUpdate: IIssue) => {
if (issueToUpdate) handleIssues(sub_group_by, group_by, issueToUpdate, EIssueActions.UPDATE);
// hooks
const { getProjectById } = useProject();
const updateIssue = (issueToUpdate: TIssue) => {
if (issueToUpdate) handleIssues(issueToUpdate, EIssueActions.UPDATE);
};
const handleIssuePeekOverview = (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
const handleIssuePeekOverview = () => {
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 },
});
};
return (
<div
className={`flex flex-col space-y-2 cursor-pointer rounded border-[0.5px] border-custom-border-200 bg-custom-background-100 px-3 py-2 text-sm shadow-custom-shadow-2xs transition-all w-full ${
isDragDisabled ? "" : "hover:cursor-grab"
} ${snapshot.isDragging ? `border-custom-primary-100` : `border-transparent`}`}
onClick={handleIssuePeekOverview}
>
{displayProperties && displayProperties?.key && (
<div className="relative w-full ">
<div className="line-clamp-1 text-xs text-left text-custom-text-300">
{issue.project_detail.identifier}-{issue.sequence_id}
</div>
<div className="absolute -top-1 right-0 hidden group-hover/kanban-block:block">
{quickActions(
!sub_group_id && sub_group_id === "null" ? null : sub_group_id,
!columnId && columnId === "null" ? null : columnId,
issue
)}
<>
<WithDisplayPropertiesHOC displayProperties={displayProperties || {}} displayPropertyKey="key">
<div className="relative">
<div className="line-clamp-1 text-xs text-custom-text-300">
{getProjectById(issue.project_id)?.identifier}-{issue.sequence_id}
</div>
<div className="absolute -top-1 right-0 hidden group-hover/kanban-block:block">{quickActions(issue)}</div>
</div>
)}
</WithDisplayPropertiesHOC>
<Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
<div className="line-clamp-2 text-sm font-medium text-custom-text-100">{issue.name}</div>
<div className="line-clamp-2 text-sm font-medium text-custom-text-100" onClick={handleIssuePeekOverview}>
{issue.name}
</div>
</Tooltip>
<div>
<KanBanProperties
sub_group_id={sub_group_id}
columnId={columnId}
<IssueProperties
className="flex flex-wrap items-center gap-2 whitespace-nowrap"
issue={issue}
displayProperties={displayProperties}
handleIssues={updateIssue}
isReadOnly={isReadOnly}
/>
</>
);
});
export const KanbanIssueBlock: React.FC<IssueBlockProps> = memo((props) => {
const {
issueId,
issuesMap,
displayProperties,
isDragDisabled,
handleIssues,
quickActions,
canEditProperties,
provided,
snapshot,
} = props;
const issue = issuesMap[issueId];
if (!issue) return null;
const canEditIssueProperties = canEditProperties(issue.project_id);
return (
<div
className="group/kanban-block relative p-1.5 hover:cursor-default"
{...provided.draggableProps}
{...provided.dragHandleProps}
ref={provided.innerRef}
>
{issue.tempId !== undefined && (
<div className="absolute left-0 top-0 z-[99999] h-full w-full animate-pulse bg-custom-background-100/20" />
)}
<div
className={`space-y-2 rounded border-[0.5px] border-custom-border-200 bg-custom-background-100 px-3 py-2 text-sm shadow-custom-shadow-2xs transition-all ${
isDragDisabled ? "" : "hover:cursor-grab"
} ${snapshot.isDragging ? `border-custom-primary-100` : `border-transparent`}`}
>
<KanbanIssueDetailsBlock
issue={issue}
handleIssues={updateIssue}
displayProperties={displayProperties}
showEmptyGroup={showEmptyGroup}
isReadOnly={isReadOnly}
handleIssues={handleIssues}
quickActions={quickActions}
isReadOnly={!canEditIssueProperties}
/>
</div>
</div>
);
};
});
const validateMemo = (prevProps: IssueDetailsBlockProps, nextProps: IssueDetailsBlockProps) => {
if (prevProps.issue !== nextProps.issue) return false;
if (!isEqual(prevProps.displayProperties, nextProps.displayProperties)) {
return false;
}
return true;
};
const KanbanIssueMemoBlock = memo(KanbanIssueDetailsBlock, validateMemo);
export const KanbanIssueBlock: React.FC<IssueBlockProps> = (props) => {
const {
sub_group_id,
columnId,
index,
issue,
isDragDisabled,
showEmptyGroup,
handleIssues,
quickActions,
displayProperties,
canEditProperties,
} = props;
let draggableId = issue.id;
if (columnId) draggableId = `${draggableId}__${columnId}`;
if (sub_group_id) draggableId = `${draggableId}__${sub_group_id}`;
const canEditIssueProperties = canEditProperties(issue.project);
return (
<>
<Draggable draggableId={draggableId} index={index} isDragDisabled={!canEditIssueProperties}>
{(provided, snapshot) => (
<div
className="group/kanban-block relative p-1.5"
{...provided.draggableProps}
{...provided.dragHandleProps}
ref={provided.innerRef}
>
{issue.tempId !== undefined && (
<div className="absolute left-0 top-0 z-[99999] h-full w-full animate-pulse bg-custom-background-100/20" />
)}
<KanbanIssueMemoBlock
sub_group_id={sub_group_id}
columnId={columnId}
issue={issue}
showEmptyGroup={showEmptyGroup}
handleIssues={handleIssues}
quickActions={quickActions}
displayProperties={displayProperties}
isReadOnly={!canEditIssueProperties}
snapshot={snapshot}
isDragDisabled={isDragDisabled}
/>
</div>
)}
</Draggable>
</>
);
};
KanbanIssueBlock.displayName = "KanbanIssueBlock";

View file

@ -1,38 +1,33 @@
import { memo } from "react";
//types
import { TIssue, IIssueDisplayProperties, IIssueMap } from "@plane/types";
import { EIssueActions } from "../types";
// components
import { KanbanIssueBlock } from "components/issues";
import { IIssueDisplayProperties, IIssue } from "types";
import { EIssueActions } from "../types";
import { IIssueResponse } from "store/issues/types";
import { Draggable } from "@hello-pangea/dnd";
interface IssueBlocksListProps {
sub_group_id: string;
columnId: string;
issues: IIssueResponse;
issuesMap: IIssueMap;
issueIds: string[];
displayProperties: IIssueDisplayProperties | undefined;
isDragDisabled: boolean;
showEmptyGroup: boolean;
handleIssues: (sub_group_by: string | null, group_by: string | null, issue: IIssue, action: EIssueActions) => void;
quickActions: (
sub_group_by: string | null,
group_by: string | null,
issue: IIssue,
customActionButton?: React.ReactElement
) => React.ReactNode;
displayProperties: IIssueDisplayProperties | null;
handleIssues: (issue: TIssue, action: EIssueActions) => void;
quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode;
canEditProperties: (projectId: string | undefined) => boolean;
}
export const KanbanIssueBlocksList: React.FC<IssueBlocksListProps> = (props) => {
const KanbanIssueBlocksListMemo: React.FC<IssueBlocksListProps> = (props) => {
const {
sub_group_id,
columnId,
issues,
issuesMap,
issueIds,
showEmptyGroup,
displayProperties,
isDragDisabled,
handleIssues,
quickActions,
displayProperties,
canEditProperties,
} = props;
@ -41,34 +36,35 @@ export const KanbanIssueBlocksList: React.FC<IssueBlocksListProps> = (props) =>
{issueIds && issueIds.length > 0 ? (
<>
{issueIds.map((issueId, index) => {
if (!issues[issueId]) return null;
if (!issueId) return null;
const issue = issues[issueId];
let draggableId = issueId;
if (columnId) draggableId = `${draggableId}__${columnId}`;
if (sub_group_id) draggableId = `${draggableId}__${sub_group_id}`;
return (
<KanbanIssueBlock
key={`kanban-issue-block-${issue.id}`}
index={index}
issue={issue}
showEmptyGroup={showEmptyGroup}
handleIssues={handleIssues}
quickActions={quickActions}
displayProperties={displayProperties}
columnId={columnId}
sub_group_id={sub_group_id}
isDragDisabled={isDragDisabled}
canEditProperties={canEditProperties}
/>
<Draggable key={draggableId} draggableId={draggableId} index={index}>
{(provided, snapshot) => (
<KanbanIssueBlock
key={`kanban-issue-block-${issueId}`}
issueId={issueId}
issuesMap={issuesMap}
displayProperties={displayProperties}
handleIssues={handleIssues}
quickActions={quickActions}
provided={provided}
snapshot={snapshot}
isDragDisabled={isDragDisabled}
canEditProperties={canEditProperties}
/>
)}
</Draggable>
);
})}
</>
) : (
!isDragDisabled && (
<div className="absolute left-0 top-0 flex h-full w-full items-center justify-center">
{/* <div className="text-custom-text-300 text-sm">Drop here</div> */}
</div>
)
)}
) : null}
</>
);
};
export const KanbanIssueBlocksList = memo(KanbanIssueBlocksListMemo);

View file

@ -1,77 +1,65 @@
import React from "react";
import { observer } from "mobx-react-lite";
import { Droppable } from "@hello-pangea/dnd";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// hooks
import { useKanbanView, useLabel, useMember, useProject, useProjectState } from "hooks/store";
// components
import { KanBanGroupByHeaderRoot } from "./headers/group-by-root";
import { KanbanIssueBlocksList, KanBanQuickAddIssueForm } from "components/issues";
import { HeaderGroupByCard } from "./headers/group-by-card";
import { KanbanGroup } from "./kanban-group";
// types
import { IIssueDisplayProperties, IIssue, IState } from "types";
import {
GroupByColumnTypes,
IGroupByColumn,
TGroupedIssues,
TIssue,
IIssueDisplayProperties,
IIssueMap,
TSubGroupedIssues,
TUnGroupedIssues,
} from "@plane/types";
// constants
import { getValueFromObject } from "constants/issue";
import { EIssueActions } from "../types";
import { IIssueResponse, IGroupedIssues, ISubGroupedIssues, TUnGroupedIssues } from "store/issues/types";
import { EProjectStore } from "store/command-palette.store";
import { getGroupByColumns } from "../utils";
import { TCreateModalStoreTypes } from "constants/issue";
export interface IGroupByKanBan {
issues: IIssueResponse;
issueIds: any;
issuesMap: IIssueMap;
issueIds: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues;
displayProperties: IIssueDisplayProperties | undefined;
sub_group_by: string | null;
group_by: string | null;
order_by: string | null;
sub_group_id: string;
list: any;
listKey: string;
states: IState[] | null;
isDragDisabled: boolean;
handleIssues: (sub_group_by: string | null, group_by: string | null, issue: IIssue, action: EIssueActions) => void;
showEmptyGroup: boolean;
quickActions: (
sub_group_by: string | null,
group_by: string | null,
issue: IIssue,
customActionButton?: React.ReactElement
) => React.ReactNode;
displayProperties: IIssueDisplayProperties | null;
handleIssues: (issue: TIssue, action: EIssueActions) => void;
quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode;
kanBanToggle: any;
handleKanBanToggle: any;
enableQuickIssueCreate?: boolean;
isDragStarted?: boolean;
quickAddCallback?: (
workspaceSlug: string,
projectId: string,
data: IIssue,
data: TIssue,
viewId?: string
) => Promise<IIssue | undefined>;
) => Promise<TIssue | undefined>;
viewId?: string;
disableIssueCreation?: boolean;
currentStore?: EProjectStore;
addIssuesToView?: (issueIds: string[]) => Promise<IIssue>;
currentStore?: TCreateModalStoreTypes;
addIssuesToView?: (issueIds: string[]) => Promise<TIssue>;
canEditProperties: (projectId: string | undefined) => boolean;
}
const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
const {
issues,
issuesMap,
issueIds,
displayProperties,
sub_group_by,
group_by,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
order_by,
sub_group_id = "null",
list,
listKey,
isDragDisabled,
handleIssues,
showEmptyGroup,
quickActions,
displayProperties,
kanBanToggle,
handleKanBanToggle,
enableQuickIssueCreate,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
isDragStarted,
quickAddCallback,
viewId,
disableIssueCreation,
@ -80,167 +68,108 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
canEditProperties,
} = props;
const verticalAlignPosition = (_list: any) =>
kanBanToggle?.groupByHeaderMinMax.includes(getValueFromObject(_list, listKey) as string);
const member = useMember();
const project = useProject();
const projectLabel = useLabel();
const projectState = useProjectState();
const list = getGroupByColumns(group_by as GroupByColumnTypes, project, projectLabel, projectState, member);
if (!list) return null;
const verticalAlignPosition = (_list: IGroupByColumn) => kanBanToggle?.groupByHeaderMinMax.includes(_list.id);
return (
<div className="relative flex h-full w-full gap-3">
{list &&
list.length > 0 &&
list.map((_list: any) => (
<div
className={`relative flex flex-shrink-0 flex-col ${!verticalAlignPosition(_list) ? `w-[340px]` : ``} group`}
>
{sub_group_by === null && (
<div className="sticky top-0 z-[2] w-full flex-shrink-0 bg-custom-background-90 py-1">
<KanBanGroupByHeaderRoot
column_id={getValueFromObject(_list, listKey) as string}
column_value={_list}
sub_group_by={sub_group_by}
group_by={group_by}
issues_count={issueIds?.[getValueFromObject(_list, listKey) as string]?.length || 0}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
disableIssueCreation={disableIssueCreation}
currentStore={currentStore}
addIssuesToView={addIssuesToView}
/>
</div>
)}
list.map((_list: IGroupByColumn) => {
const verticalPosition = verticalAlignPosition(_list);
<div
className={`${
verticalAlignPosition(_list) ? `min-h-[150px] w-[0px] overflow-hidden` : `w-full transition-all`
}`}
>
<Droppable droppableId={`${getValueFromObject(_list, listKey) as string}__${sub_group_id}`}>
{(provided: any, snapshot: any) => (
<div
className={`relative h-full w-full transition-all ${
snapshot.isDraggingOver ? `bg-custom-background-80` : ``
}`}
{...provided.droppableProps}
ref={provided.innerRef}
>
{issues && !verticalAlignPosition(_list) ? (
<KanbanIssueBlocksList
sub_group_id={sub_group_id}
columnId={getValueFromObject(_list, listKey) as string}
issues={issues}
issueIds={issueIds?.[getValueFromObject(_list, listKey) as string] || []}
isDragDisabled={isDragDisabled}
showEmptyGroup={showEmptyGroup}
handleIssues={handleIssues}
quickActions={quickActions}
displayProperties={displayProperties}
canEditProperties={canEditProperties}
/>
) : (
isDragDisabled && (
<div className="absolute left-0 top-0 flex h-full w-full items-center justify-center text-sm">
{/* <div className="text-custom-text-300 text-sm">Drop here</div> */}
</div>
)
)}
{provided.placeholder}
</div>
)}
</Droppable>
<div className="sticky bottom-0 z-[0] w-full flex-shrink-0 bg-custom-background-90 py-1">
{enableQuickIssueCreate && !disableIssueCreation && (
<KanBanQuickAddIssueForm
formKey="name"
groupId={getValueFromObject(_list, listKey) as string}
subGroupId={sub_group_id}
prePopulatedData={{
...(group_by && { [group_by]: getValueFromObject(_list, listKey) }),
...(sub_group_by && sub_group_id !== "null" && { [sub_group_by]: sub_group_id }),
}}
quickAddCallback={quickAddCallback}
viewId={viewId}
return (
<div className={`relative flex flex-shrink-0 flex-col ${!verticalPosition ? `w-[340px]` : ``} group`}>
{sub_group_by === null && (
<div className="sticky top-0 z-[2] w-full flex-shrink-0 bg-custom-background-90 py-1">
<HeaderGroupByCard
sub_group_by={sub_group_by}
group_by={group_by}
column_id={_list.id}
icon={_list.Icon}
title={_list.name}
count={(issueIds as TGroupedIssues)?.[_list.id]?.length || 0}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
issuePayload={_list.payload}
disableIssueCreation={disableIssueCreation}
currentStore={currentStore}
addIssuesToView={addIssuesToView}
/>
)}
</div>
</div>
{/* {isDragStarted && isDragDisabled && (
<div className="invisible group-hover:visible transition-all text-sm absolute top-12 bottom-10 left-0 right-0 bg-custom-background-100/40 text-center">
<div className="rounded inline-flex mt-80 h-8 px-3 justify-center items-center bg-custom-background-80 text-custom-text-100 font-medium">
{`This board is ordered by "${replaceUnderscoreIfSnakeCase(
order_by ? (order_by[0] === "-" ? order_by.slice(1) : order_by) : "created_at"
)}"`}
</div>
</div>
)} */}
</div>
))}
)}
<KanbanGroup
groupId={_list.id}
issuesMap={issuesMap}
issueIds={issueIds}
displayProperties={displayProperties}
sub_group_by={sub_group_by}
group_by={group_by}
sub_group_id={sub_group_id}
isDragDisabled={isDragDisabled}
handleIssues={handleIssues}
quickActions={quickActions}
enableQuickIssueCreate={enableQuickIssueCreate}
quickAddCallback={quickAddCallback}
viewId={viewId}
disableIssueCreation={disableIssueCreation}
canEditProperties={canEditProperties}
verticalPosition={verticalPosition}
/>
</div>
);
})}
</div>
);
});
export interface IKanBan {
issues: IIssueResponse;
issueIds: IGroupedIssues | ISubGroupedIssues | TUnGroupedIssues;
issuesMap: IIssueMap;
issueIds: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues;
displayProperties: IIssueDisplayProperties | undefined;
sub_group_by: string | null;
group_by: string | null;
order_by: string | null;
sub_group_id?: string;
handleIssues: (sub_group_by: string | null, group_by: string | null, issue: IIssue, action: EIssueActions) => void;
quickActions: (
sub_group_by: string | null,
group_by: string | null,
issue: IIssue,
customActionButton?: React.ReactElement
) => React.ReactNode;
displayProperties: IIssueDisplayProperties | null;
handleIssues: (issue: TIssue, action: EIssueActions) => void;
quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode;
kanBanToggle: any;
handleKanBanToggle: any;
showEmptyGroup: boolean;
states: any;
stateGroups: any;
priorities: any;
labels: any;
members: any;
projects: any;
enableQuickIssueCreate?: boolean;
isDragStarted?: boolean;
quickAddCallback?: (
workspaceSlug: string,
projectId: string,
data: IIssue,
data: TIssue,
viewId?: string
) => Promise<IIssue | undefined>;
) => Promise<TIssue | undefined>;
viewId?: string;
disableIssueCreation?: boolean;
currentStore?: EProjectStore;
addIssuesToView?: (issueIds: string[]) => Promise<IIssue>;
currentStore?: TCreateModalStoreTypes;
addIssuesToView?: (issueIds: string[]) => Promise<TIssue>;
canEditProperties: (projectId: string | undefined) => boolean;
}
export const KanBan: React.FC<IKanBan> = observer((props) => {
const {
issues,
issuesMap,
issueIds,
displayProperties,
sub_group_by,
group_by,
order_by,
sub_group_id = "null",
handleIssues,
quickActions,
displayProperties,
kanBanToggle,
handleKanBanToggle,
showEmptyGroup,
states,
stateGroups,
priorities,
labels,
members,
projects,
enableQuickIssueCreate,
isDragStarted,
quickAddCallback,
viewId,
disableIssueCreation,
@ -249,212 +178,30 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
canEditProperties,
} = props;
const { issueKanBanView: issueKanBanViewStore } = useMobxStore();
const issueKanBanView = useKanbanView();
return (
<div className="relative h-full w-full">
{group_by && group_by === "project" && (
<GroupByKanBan
issues={issues}
issueIds={issueIds}
group_by={group_by}
order_by={order_by}
sub_group_by={sub_group_by}
sub_group_id={sub_group_id}
list={projects}
listKey={`id`}
states={states}
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
showEmptyGroup={showEmptyGroup}
handleIssues={handleIssues}
quickActions={quickActions}
displayProperties={displayProperties}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
enableQuickIssueCreate={enableQuickIssueCreate}
isDragStarted={isDragStarted}
quickAddCallback={quickAddCallback}
viewId={viewId}
disableIssueCreation={disableIssueCreation}
currentStore={currentStore}
addIssuesToView={addIssuesToView}
canEditProperties={canEditProperties}
/>
)}
{group_by && group_by === "state" && (
<GroupByKanBan
issues={issues}
issueIds={issueIds}
group_by={group_by}
order_by={order_by}
sub_group_by={sub_group_by}
sub_group_id={sub_group_id}
list={states}
listKey={`id`}
states={states}
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
showEmptyGroup={showEmptyGroup}
handleIssues={handleIssues}
quickActions={quickActions}
displayProperties={displayProperties}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
enableQuickIssueCreate={enableQuickIssueCreate}
isDragStarted={isDragStarted}
quickAddCallback={quickAddCallback}
viewId={viewId}
disableIssueCreation={disableIssueCreation}
currentStore={currentStore}
addIssuesToView={addIssuesToView}
canEditProperties={canEditProperties}
/>
)}
{group_by && group_by === "state_detail.group" && (
<GroupByKanBan
issues={issues}
issueIds={issueIds}
group_by={group_by}
order_by={order_by}
sub_group_by={sub_group_by}
sub_group_id={sub_group_id}
list={stateGroups}
listKey={`key`}
states={states}
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
showEmptyGroup={showEmptyGroup}
handleIssues={handleIssues}
quickActions={quickActions}
displayProperties={displayProperties}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
enableQuickIssueCreate={enableQuickIssueCreate}
isDragStarted={isDragStarted}
quickAddCallback={quickAddCallback}
viewId={viewId}
disableIssueCreation={disableIssueCreation}
currentStore={currentStore}
addIssuesToView={addIssuesToView}
canEditProperties={canEditProperties}
/>
)}
{group_by && group_by === "priority" && (
<GroupByKanBan
issues={issues}
issueIds={issueIds}
group_by={group_by}
order_by={order_by}
sub_group_by={sub_group_by}
sub_group_id={sub_group_id}
list={priorities}
listKey={`key`}
states={states}
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
showEmptyGroup={showEmptyGroup}
handleIssues={handleIssues}
quickActions={quickActions}
displayProperties={displayProperties}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
enableQuickIssueCreate={enableQuickIssueCreate}
isDragStarted={isDragStarted}
quickAddCallback={quickAddCallback}
viewId={viewId}
disableIssueCreation={disableIssueCreation}
currentStore={currentStore}
addIssuesToView={addIssuesToView}
canEditProperties={canEditProperties}
/>
)}
{group_by && group_by === "labels" && (
<GroupByKanBan
issues={issues}
issueIds={issueIds}
group_by={group_by}
order_by={order_by}
sub_group_by={sub_group_by}
sub_group_id={sub_group_id}
list={labels ? [...labels, { id: "None", name: "None" }] : labels}
listKey={`id`}
states={states}
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
showEmptyGroup={showEmptyGroup}
handleIssues={handleIssues}
quickActions={quickActions}
displayProperties={displayProperties}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
enableQuickIssueCreate={enableQuickIssueCreate}
isDragStarted={isDragStarted}
quickAddCallback={quickAddCallback}
viewId={viewId}
disableIssueCreation={disableIssueCreation}
currentStore={currentStore}
addIssuesToView={addIssuesToView}
canEditProperties={canEditProperties}
/>
)}
{group_by && group_by === "assignees" && (
<GroupByKanBan
issues={issues}
issueIds={issueIds}
group_by={group_by}
order_by={order_by}
sub_group_by={sub_group_by}
sub_group_id={sub_group_id}
list={members ? [...members, { id: "None", display_name: "None" }] : members}
listKey={`id`}
states={states}
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
showEmptyGroup={showEmptyGroup}
handleIssues={handleIssues}
quickActions={quickActions}
displayProperties={displayProperties}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
enableQuickIssueCreate={enableQuickIssueCreate}
isDragStarted={isDragStarted}
quickAddCallback={quickAddCallback}
viewId={viewId}
disableIssueCreation={disableIssueCreation}
currentStore={currentStore}
addIssuesToView={addIssuesToView}
canEditProperties={canEditProperties}
/>
)}
{group_by && group_by === "created_by" && (
<GroupByKanBan
issues={issues}
issueIds={issueIds}
group_by={group_by}
order_by={order_by}
sub_group_by={sub_group_by}
sub_group_id={sub_group_id}
list={members}
listKey={`id`}
states={states}
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
showEmptyGroup={showEmptyGroup}
handleIssues={handleIssues}
quickActions={quickActions}
displayProperties={displayProperties}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
enableQuickIssueCreate={enableQuickIssueCreate}
isDragStarted={isDragStarted}
quickAddCallback={quickAddCallback}
viewId={viewId}
disableIssueCreation={disableIssueCreation}
currentStore={currentStore}
addIssuesToView={addIssuesToView}
canEditProperties={canEditProperties}
/>
)}
<GroupByKanBan
issuesMap={issuesMap}
issueIds={issueIds}
displayProperties={displayProperties}
group_by={group_by}
sub_group_by={sub_group_by}
sub_group_id={sub_group_id}
isDragDisabled={!issueKanBanView?.canUserDragDrop}
handleIssues={handleIssues}
quickActions={quickActions}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
enableQuickIssueCreate={enableQuickIssueCreate}
quickAddCallback={quickAddCallback}
viewId={viewId}
disableIssueCreation={disableIssueCreation}
currentStore={currentStore}
addIssuesToView={addIssuesToView}
canEditProperties={canEditProperties}
/>
</div>
);
});

View file

@ -1,74 +0,0 @@
import { FC } from "react";
import { observer } from "mobx-react-lite";
// components
import { HeaderGroupByCard } from "./group-by-card";
import { HeaderSubGroupByCard } from "./sub-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;
sub_group_by: string | null;
group_by: string | null;
header_type: "group_by" | "sub_group_by";
issues_count: number;
kanBanToggle: any;
handleKanBanToggle: any;
disableIssueCreation?: boolean;
currentStore?: EProjectStore;
addIssuesToView?: (issueIds: string[]) => Promise<IIssue>;
}
export const Icon = ({ user }: any) => <Avatar name={user.display_name} src={user.avatar} size="md" />;
export const AssigneesHeader: FC<IAssigneesHeader> = observer((props) => {
const {
column_id,
column_value,
sub_group_by,
group_by,
header_type,
issues_count,
kanBanToggle,
handleKanBanToggle,
disableIssueCreation,
currentStore,
addIssuesToView,
} = props;
const assignee = column_value ?? null;
return (
<>
{assignee &&
(sub_group_by && header_type === "sub_group_by" ? (
<HeaderSubGroupByCard
column_id={column_id}
icon={<Icon user={assignee} />}
title={assignee?.display_name || ""}
count={issues_count}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
/>
) : (
<HeaderGroupByCard
sub_group_by={sub_group_by}
group_by={group_by}
column_id={column_id}
icon={<Icon user={assignee} />}
title={assignee?.display_name || ""}
count={issues_count}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
issuePayload={{ assignees: [assignee?.id] }}
disableIssueCreation={disableIssueCreation}
currentStore={currentStore}
addIssuesToView={addIssuesToView}
/>
))}
</>
);
});

View file

@ -1,71 +0,0 @@
import { FC } from "react";
import { observer } from "mobx-react-lite";
// components
import { HeaderGroupByCard } from "./group-by-card";
import { HeaderSubGroupByCard } from "./sub-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;
sub_group_by: string | null;
group_by: string | null;
header_type: "group_by" | "sub_group_by";
issues_count: number;
kanBanToggle: any;
handleKanBanToggle: any;
disableIssueCreation?: boolean;
currentStore?: EProjectStore;
addIssuesToView?: (issueIds: string[]) => Promise<IIssue>;
}
export const CreatedByHeader: FC<ICreatedByHeader> = observer((props) => {
const {
column_id,
column_value,
sub_group_by,
group_by,
header_type,
issues_count,
kanBanToggle,
handleKanBanToggle,
disableIssueCreation,
currentStore,
addIssuesToView,
} = props;
const createdBy = column_value ?? null;
return (
<>
{createdBy &&
(sub_group_by && header_type === "sub_group_by" ? (
<HeaderSubGroupByCard
column_id={column_id}
icon={<Icon user={createdBy} />}
title={createdBy?.display_name || ""}
count={issues_count}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
/>
) : (
<HeaderGroupByCard
sub_group_by={sub_group_by}
group_by={group_by}
column_id={column_id}
icon={<Icon user={createdBy} />}
title={createdBy?.display_name || ""}
count={issues_count}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
issuePayload={{ created_by: createdBy?.id }}
disableIssueCreation={disableIssueCreation}
currentStore={currentStore}
addIssuesToView={addIssuesToView}
/>
))}
</>
);
});

View file

@ -12,8 +12,8 @@ import useToast from "hooks/use-toast";
// 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 { TCreateModalStoreTypes } from "constants/issue";
interface IHeaderGroupByCard {
sub_group_by: string | null;
@ -24,10 +24,10 @@ interface IHeaderGroupByCard {
count: number;
kanBanToggle: any;
handleKanBanToggle: any;
issuePayload: Partial<IIssue>;
issuePayload: Partial<TIssue>;
disableIssueCreation?: boolean;
currentStore?: EProjectStore;
addIssuesToView?: (issueIds: string[]) => Promise<IIssue>;
currentStore?: TCreateModalStoreTypes;
addIssuesToView?: (issueIds: string[]) => Promise<TIssue>;
}
export const HeaderGroupByCard: FC<IHeaderGroupByCard> = observer((props) => {
@ -64,14 +64,15 @@ export const HeaderGroupByCard: FC<IHeaderGroupByCard> = observer((props) => {
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 (

View file

@ -1,149 +0,0 @@
// components
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 IKanBanGroupByHeaderRoot {
column_id: string;
column_value: any;
sub_group_by: string | null;
group_by: string | null;
issues_count: number;
kanBanToggle: any;
handleKanBanToggle: any;
disableIssueCreation?: boolean;
currentStore?: EProjectStore;
addIssuesToView?: (issueIds: string[]) => Promise<IIssue>;
}
export const KanBanGroupByHeaderRoot: React.FC<IKanBanGroupByHeaderRoot> = observer(
({
column_id,
column_value,
sub_group_by,
group_by,
issues_count,
kanBanToggle,
disableIssueCreation,
handleKanBanToggle,
currentStore,
addIssuesToView,
}) => (
<>
{group_by && group_by === "project" && (
<ProjectHeader
column_id={column_id}
column_value={column_value}
sub_group_by={sub_group_by}
group_by={group_by}
header_type={`group_by`}
issues_count={issues_count}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
disableIssueCreation={disableIssueCreation}
currentStore={currentStore}
addIssuesToView={addIssuesToView}
/>
)}
{group_by && group_by === "state" && (
<StateHeader
column_id={column_id}
column_value={column_value}
sub_group_by={sub_group_by}
group_by={group_by}
header_type={`group_by`}
issues_count={issues_count}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
disableIssueCreation={disableIssueCreation}
currentStore={currentStore}
addIssuesToView={addIssuesToView}
/>
)}
{group_by && group_by === "state_detail.group" && (
<StateGroupHeader
column_id={column_id}
column_value={column_value}
sub_group_by={sub_group_by}
group_by={group_by}
header_type={`group_by`}
issues_count={issues_count}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
disableIssueCreation={disableIssueCreation}
currentStore={currentStore}
addIssuesToView={addIssuesToView}
/>
)}
{group_by && group_by === "priority" && (
<PriorityHeader
column_id={column_id}
column_value={column_value}
sub_group_by={sub_group_by}
group_by={group_by}
header_type={`group_by`}
issues_count={issues_count}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
disableIssueCreation={disableIssueCreation}
currentStore={currentStore}
addIssuesToView={addIssuesToView}
/>
)}
{group_by && group_by === "labels" && (
<LabelHeader
column_id={column_id}
column_value={column_value}
sub_group_by={sub_group_by}
group_by={group_by}
header_type={`group_by`}
issues_count={issues_count}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
disableIssueCreation={disableIssueCreation}
currentStore={currentStore}
addIssuesToView={addIssuesToView}
/>
)}
{group_by && group_by === "assignees" && (
<AssigneesHeader
column_id={column_id}
column_value={column_value}
sub_group_by={sub_group_by}
group_by={group_by}
header_type={`group_by`}
issues_count={issues_count}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
disableIssueCreation={disableIssueCreation}
currentStore={currentStore}
addIssuesToView={addIssuesToView}
/>
)}
{group_by && group_by === "created_by" && (
<CreatedByHeader
column_id={column_id}
column_value={column_value}
sub_group_by={sub_group_by}
group_by={group_by}
header_type={`group_by`}
issues_count={issues_count}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
disableIssueCreation={disableIssueCreation}
currentStore={currentStore}
addIssuesToView={addIssuesToView}
/>
)}
</>
)
);

View file

@ -1,74 +0,0 @@
import { FC } from "react";
import { observer } from "mobx-react-lite";
// components
import { HeaderGroupByCard } from "./group-by-card";
import { HeaderSubGroupByCard } from "./sub-group-by-card";
import { EProjectStore } from "store/command-palette.store";
import { IIssue } from "types";
export interface ILabelHeader {
column_id: string;
column_value: any;
sub_group_by: string | null;
group_by: string | null;
header_type: "group_by" | "sub_group_by";
issues_count: number;
kanBanToggle: any;
handleKanBanToggle: any;
disableIssueCreation?: boolean;
currentStore?: EProjectStore;
addIssuesToView?: (issueIds: string[]) => Promise<IIssue>;
}
const Icon = ({ color }: any) => (
<div className="h-[12px] w-[12px] rounded-full" style={{ backgroundColor: color ? color : "#666" }} />
);
export const LabelHeader: FC<ILabelHeader> = observer((props) => {
const {
column_id,
column_value,
sub_group_by,
group_by,
header_type,
issues_count,
kanBanToggle,
handleKanBanToggle,
disableIssueCreation,
currentStore,
addIssuesToView,
} = props;
const label = column_value ?? null;
return (
<>
{label &&
(sub_group_by && header_type === "sub_group_by" ? (
<HeaderSubGroupByCard
column_id={column_id}
icon={<Icon color={label?.color} />}
title={label?.name || ""}
count={issues_count}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
/>
) : (
<HeaderGroupByCard
sub_group_by={sub_group_by}
group_by={group_by}
column_id={column_id}
icon={<Icon />}
title={label?.name || ""}
count={issues_count}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
issuePayload={{ labels: [label?.id] }}
disableIssueCreation={disableIssueCreation}
currentStore={currentStore}
addIssuesToView={addIssuesToView}
/>
))}
</>
);
});

View file

@ -1,73 +0,0 @@
import { FC } from "react";
import { observer } from "mobx-react-lite";
// components
import { HeaderGroupByCard } from "./group-by-card";
import { HeaderSubGroupByCard } from "./sub-group-by-card";
// Icons
import { PriorityIcon } from "@plane/ui";
import { EProjectStore } from "store/command-palette.store";
import { IIssue } from "types";
export interface IPriorityHeader {
column_id: string;
column_value: any;
sub_group_by: string | null;
group_by: string | null;
header_type: "group_by" | "sub_group_by";
issues_count: number;
kanBanToggle: any;
handleKanBanToggle: any;
disableIssueCreation?: boolean;
currentStore?: EProjectStore;
addIssuesToView?: (issueIds: string[]) => Promise<IIssue>;
}
export const PriorityHeader: FC<IPriorityHeader> = observer((props) => {
const {
column_id,
column_value,
sub_group_by,
group_by,
header_type,
issues_count,
kanBanToggle,
handleKanBanToggle,
disableIssueCreation,
currentStore,
addIssuesToView,
} = props;
const priority = column_value || null;
return (
<>
{priority &&
(sub_group_by && header_type === "sub_group_by" ? (
<HeaderSubGroupByCard
column_id={column_id}
icon={<PriorityIcon priority={priority?.key} />}
title={priority?.title || ""}
count={issues_count}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
/>
) : (
<HeaderGroupByCard
sub_group_by={sub_group_by}
group_by={group_by}
column_id={column_id}
icon={<PriorityIcon priority={priority?.key} />}
title={priority?.title || ""}
count={issues_count}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
issuePayload={{ priority: priority?.key }}
disableIssueCreation={disableIssueCreation}
currentStore={currentStore}
addIssuesToView={addIssuesToView}
/>
))}
</>
);
});

View file

@ -1,74 +0,0 @@
import { FC } from "react";
import { observer } from "mobx-react-lite";
// components
import { HeaderGroupByCard } from "./group-by-card";
import { HeaderSubGroupByCard } from "./sub-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;
sub_group_by: string | null;
group_by: string | null;
header_type: "group_by" | "sub_group_by";
issues_count: number;
kanBanToggle: any;
handleKanBanToggle: any;
disableIssueCreation?: boolean;
currentStore?: EProjectStore;
addIssuesToView?: (issueIds: string[]) => Promise<IIssue>;
}
const Icon = ({ emoji }: any) => <div className="h-6 w-6">{renderEmoji(emoji)}</div>;
export const ProjectHeader: FC<IProjectHeader> = observer((props) => {
const {
column_id,
column_value,
sub_group_by,
group_by,
header_type,
issues_count,
kanBanToggle,
handleKanBanToggle,
disableIssueCreation,
currentStore,
addIssuesToView,
} = props;
const project = column_value ?? null;
return (
<>
{project &&
(sub_group_by && header_type === "sub_group_by" ? (
<HeaderSubGroupByCard
column_id={column_id}
icon={<Icon emoji={project?.emoji} />}
title={project?.name || ""}
count={issues_count}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
/>
) : (
<HeaderGroupByCard
sub_group_by={sub_group_by}
group_by={group_by}
column_id={column_id}
icon={<Icon emoji={project?.emoji} />}
title={project?.name || ""}
count={issues_count}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
issuePayload={{ project: project?.id }}
disableIssueCreation={disableIssueCreation}
currentStore={currentStore}
addIssuesToView={addIssuesToView}
/>
))}
</>
);
});

View file

@ -1,77 +0,0 @@
import { FC } from "react";
import { observer } from "mobx-react-lite";
// components
import { HeaderGroupByCard } from "./group-by-card";
import { HeaderSubGroupByCard } from "./sub-group-by-card";
import { StateGroupIcon } from "@plane/ui";
import { EProjectStore } from "store/command-palette.store";
import { IIssue } from "types";
export interface IStateGroupHeader {
column_id: string;
column_value: any;
sub_group_by: string | null;
group_by: string | null;
header_type: "group_by" | "sub_group_by";
issues_count: number;
kanBanToggle: any;
handleKanBanToggle: any;
disableIssueCreation?: boolean;
currentStore?: EProjectStore;
addIssuesToView?: (issueIds: string[]) => Promise<IIssue>;
}
export const Icon = ({ stateGroup, color }: { stateGroup: any; color?: any }) => (
<div className="h-3.5 w-3.5 rounded-full">
<StateGroupIcon stateGroup={stateGroup} color={color || null} width="14" height="14" />
</div>
);
export const StateGroupHeader: FC<IStateGroupHeader> = observer((props) => {
const {
column_id,
column_value,
sub_group_by,
group_by,
header_type,
issues_count,
kanBanToggle,
handleKanBanToggle,
disableIssueCreation,
currentStore,
addIssuesToView,
} = props;
const stateGroup = column_value || null;
return (
<>
{stateGroup &&
(sub_group_by && header_type === "sub_group_by" ? (
<HeaderSubGroupByCard
column_id={column_id}
icon={<Icon stateGroup={stateGroup?.key} />}
title={stateGroup?.key || ""}
count={issues_count}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
/>
) : (
<HeaderGroupByCard
sub_group_by={sub_group_by}
group_by={group_by}
column_id={column_id}
icon={<Icon stateGroup={stateGroup?.key} />}
title={stateGroup?.key || ""}
count={issues_count}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
issuePayload={{}}
disableIssueCreation={disableIssueCreation}
currentStore={currentStore}
addIssuesToView={addIssuesToView}
/>
))}
</>
);
});

View file

@ -1,71 +0,0 @@
import { FC } from "react";
import { observer } from "mobx-react-lite";
// components
import { HeaderGroupByCard } from "./group-by-card";
import { HeaderSubGroupByCard } from "./sub-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;
sub_group_by: string | null;
group_by: string | null;
header_type: "group_by" | "sub_group_by";
issues_count: number;
kanBanToggle: any;
handleKanBanToggle: any;
disableIssueCreation?: boolean;
currentStore?: EProjectStore;
addIssuesToView?: (issueIds: string[]) => Promise<IIssue>;
}
export const StateHeader: FC<IStateHeader> = observer((props) => {
const {
column_id,
column_value,
sub_group_by,
group_by,
header_type,
issues_count,
kanBanToggle,
handleKanBanToggle,
disableIssueCreation,
currentStore,
addIssuesToView,
} = props;
const state = column_value ?? null;
return (
<>
{state &&
(sub_group_by && header_type === "sub_group_by" ? (
<HeaderSubGroupByCard
column_id={column_id}
icon={<Icon stateGroup={state?.group} color={state?.color} />}
title={state?.name || ""}
count={issues_count}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
/>
) : (
<HeaderGroupByCard
sub_group_by={sub_group_by}
group_by={group_by}
column_id={column_id}
icon={<Icon stateGroup={state?.group} color={state?.color} />}
title={state?.name || ""}
count={issues_count}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
issuePayload={{ state: state?.id }}
disableIssueCreation={disableIssueCreation}
currentStore={currentStore}
addIssuesToView={addIssuesToView}
/>
))}
</>
);
});

View file

@ -1,134 +0,0 @@
// mobx
import { observer } from "mobx-react-lite";
// components
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";
import { EProjectStore } from "store/command-palette.store";
import { IIssue } from "types";
export interface IKanBanSubGroupByHeaderRoot {
column_id: string;
column_value: any;
sub_group_by: string | null;
group_by: string | null;
issues_count: number;
kanBanToggle: any;
handleKanBanToggle: any;
disableIssueCreation?: boolean;
currentStore?: EProjectStore;
addIssuesToView?: (issueIds: string[]) => Promise<IIssue>;
}
export const KanBanSubGroupByHeaderRoot: React.FC<IKanBanSubGroupByHeaderRoot> = observer((props) => {
const {
column_id,
column_value,
sub_group_by,
group_by,
issues_count,
kanBanToggle,
handleKanBanToggle,
disableIssueCreation,
currentStore,
addIssuesToView,
} = props;
return (
<>
{sub_group_by && sub_group_by === "state" && (
<StateHeader
column_id={column_id}
column_value={column_value}
sub_group_by={sub_group_by}
group_by={group_by}
header_type={`sub_group_by`}
issues_count={issues_count}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
disableIssueCreation={disableIssueCreation}
currentStore={currentStore}
addIssuesToView={addIssuesToView}
/>
)}
{sub_group_by && sub_group_by === "state_detail.group" && (
<StateGroupHeader
column_id={column_id}
column_value={column_value}
sub_group_by={sub_group_by}
group_by={group_by}
header_type={`sub_group_by`}
issues_count={issues_count}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
disableIssueCreation={disableIssueCreation}
currentStore={currentStore}
addIssuesToView={addIssuesToView}
/>
)}
{sub_group_by && sub_group_by === "priority" && (
<PriorityHeader
column_id={column_id}
column_value={column_value}
sub_group_by={sub_group_by}
group_by={group_by}
header_type={`sub_group_by`}
issues_count={issues_count}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
disableIssueCreation={disableIssueCreation}
currentStore={currentStore}
addIssuesToView={addIssuesToView}
/>
)}
{sub_group_by && sub_group_by === "labels" && (
<LabelHeader
column_id={column_id}
column_value={column_value}
sub_group_by={sub_group_by}
group_by={group_by}
header_type={`sub_group_by`}
issues_count={issues_count}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
disableIssueCreation={disableIssueCreation}
currentStore={currentStore}
addIssuesToView={addIssuesToView}
/>
)}
{sub_group_by && sub_group_by === "assignees" && (
<AssigneesHeader
column_id={column_id}
column_value={column_value}
sub_group_by={sub_group_by}
group_by={group_by}
header_type={`sub_group_by`}
issues_count={issues_count}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
disableIssueCreation={disableIssueCreation}
currentStore={currentStore}
addIssuesToView={addIssuesToView}
/>
)}
{sub_group_by && sub_group_by === "created_by" && (
<CreatedByHeader
column_id={column_id}
column_value={column_value}
sub_group_by={sub_group_by}
group_by={group_by}
header_type={`sub_group_by`}
issues_count={issues_count}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
disableIssueCreation={disableIssueCreation}
currentStore={currentStore}
addIssuesToView={addIssuesToView}
/>
)}
</>
);
});

View file

@ -0,0 +1,106 @@
import { Droppable } from "@hello-pangea/dnd";
//types
import {
TGroupedIssues,
TIssue,
IIssueDisplayProperties,
IIssueMap,
TSubGroupedIssues,
TUnGroupedIssues,
} from "@plane/types";
import { EIssueActions } from "../types";
//components
import { KanBanQuickAddIssueForm, KanbanIssueBlocksList } from ".";
interface IKanbanGroup {
groupId: string;
issuesMap: IIssueMap;
issueIds: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues;
displayProperties: IIssueDisplayProperties | undefined;
sub_group_by: string | null;
group_by: string | null;
sub_group_id: string;
isDragDisabled: boolean;
handleIssues: (issue: TIssue, action: EIssueActions) => void;
quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode;
enableQuickIssueCreate?: boolean;
quickAddCallback?: (
workspaceSlug: string,
projectId: string,
data: TIssue,
viewId?: string
) => Promise<TIssue | undefined>;
viewId?: string;
disableIssueCreation?: boolean;
canEditProperties: (projectId: string | undefined) => boolean;
verticalPosition: any;
}
export const KanbanGroup = (props: IKanbanGroup) => {
const {
groupId,
sub_group_id,
group_by,
sub_group_by,
issuesMap,
displayProperties,
verticalPosition,
issueIds,
isDragDisabled,
handleIssues,
quickActions,
canEditProperties,
enableQuickIssueCreate,
disableIssueCreation,
quickAddCallback,
viewId,
} = props;
return (
<div className={`${verticalPosition ? `min-h-[150px] w-[0px] overflow-hidden` : `w-full transition-all`}`}>
<Droppable droppableId={`${groupId}__${sub_group_id}`}>
{(provided: any, snapshot: any) => (
<div
className={`relative h-full w-full transition-all ${
snapshot.isDraggingOver ? `bg-custom-background-80` : ``
}`}
{...provided.droppableProps}
ref={provided.innerRef}
>
{!verticalPosition ? (
<KanbanIssueBlocksList
sub_group_id={sub_group_id}
columnId={groupId}
issuesMap={issuesMap}
issueIds={(issueIds as TGroupedIssues)?.[groupId] || []}
displayProperties={displayProperties}
isDragDisabled={isDragDisabled}
handleIssues={handleIssues}
quickActions={quickActions}
canEditProperties={canEditProperties}
/>
) : null}
{provided.placeholder}
</div>
)}
</Droppable>
<div className="sticky bottom-0 z-[0] w-full flex-shrink-0 bg-custom-background-90 py-1">
{enableQuickIssueCreate && !disableIssueCreation && (
<KanBanQuickAddIssueForm
formKey="name"
groupId={groupId}
subGroupId={sub_group_id}
prePopulatedData={{
...(group_by && { [group_by]: groupId }),
...(sub_group_by && sub_group_id !== "null" && { [sub_group_by]: sub_group_id }),
}}
quickAddCallback={quickAddCallback}
viewId={viewId}
/>
)}
</div>
</div>
);
};

View file

@ -1,197 +0,0 @@
// mobx
import { observer } from "mobx-react-lite";
// lucide icons
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";
import { Tooltip } from "@plane/ui";
import { IIssue, IIssueDisplayProperties, IState, TIssuePriorities } from "types";
export interface IKanBanProperties {
sub_group_id: string;
columnId: string;
issue: IIssue;
handleIssues: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => void;
displayProperties: IIssueDisplayProperties | null;
showEmptyGroup: boolean;
isReadOnly: boolean;
}
export const KanBanProperties: React.FC<IKanBanProperties> = observer((props) => {
const { sub_group_id, columnId: group_id, issue, handleIssues, displayProperties, isReadOnly } = props;
const handleState = (state: IState) => {
handleIssues(
!sub_group_id && sub_group_id === "null" ? null : sub_group_id,
!group_id && group_id === "null" ? null : group_id,
{ ...issue, state: state.id }
);
};
const handlePriority = (value: TIssuePriorities) => {
handleIssues(
!sub_group_id && sub_group_id === "null" ? null : sub_group_id,
!group_id && group_id === "null" ? null : group_id,
{ ...issue, priority: value }
);
};
const handleLabel = (ids: string[]) => {
handleIssues(
!sub_group_id && sub_group_id === "null" ? null : sub_group_id,
!group_id && group_id === "null" ? null : group_id,
{ ...issue, labels: ids }
);
};
const handleAssignee = (ids: string[]) => {
handleIssues(
!sub_group_id && sub_group_id === "null" ? null : sub_group_id,
!group_id && group_id === "null" ? null : group_id,
{ ...issue, assignees: ids }
);
};
const handleStartDate = (date: string | null) => {
handleIssues(
!sub_group_id && sub_group_id === "null" ? null : sub_group_id,
!group_id && group_id === "null" ? null : group_id,
{ ...issue, start_date: date }
);
};
const handleTargetDate = (date: string | null) => {
handleIssues(
!sub_group_id && sub_group_id === "null" ? null : sub_group_id,
!group_id && group_id === "null" ? null : group_id,
{ ...issue, target_date: date }
);
};
const handleEstimate = (value: number | null) => {
handleIssues(
!sub_group_id && sub_group_id === "null" ? null : sub_group_id,
!group_id && group_id === "null" ? null : group_id,
{ ...issue, estimate_point: value }
);
};
return (
<div className="flex flex-wrap items-center gap-2 whitespace-nowrap">
{/* basic properties */}
{/* state */}
{displayProperties && displayProperties?.state && (
<IssuePropertyState
projectId={issue?.project_detail?.id || null}
value={issue?.state || null}
defaultOptions={issue?.state_detail ? [issue.state_detail] : []}
onChange={handleState}
disabled={isReadOnly}
hideDropdownArrow
/>
)}
{/* priority */}
{displayProperties && displayProperties?.priority && (
<IssuePropertyPriority
value={issue?.priority || null}
onChange={handlePriority}
disabled={isReadOnly}
hideDropdownArrow
/>
)}
{/* label */}
{displayProperties && displayProperties?.labels && (
<IssuePropertyLabels
projectId={issue?.project_detail?.id || null}
value={issue?.labels || null}
defaultOptions={issue?.label_details ? issue.label_details : []}
onChange={handleLabel}
disabled={isReadOnly}
hideDropdownArrow
/>
)}
{/* start date */}
{displayProperties && displayProperties?.start_date && (
<IssuePropertyDate
value={issue?.start_date || null}
onChange={(date) => handleStartDate(date)}
disabled={isReadOnly}
type="start_date"
/>
)}
{/* target/due date */}
{displayProperties && displayProperties?.due_date && (
<IssuePropertyDate
value={issue?.target_date || null}
onChange={(date) => handleTargetDate(date)}
disabled={isReadOnly}
type="target_date"
/>
)}
{/* assignee */}
{displayProperties && displayProperties?.assignee && (
<IssuePropertyAssignee
projectId={issue?.project_detail?.id || null}
value={issue?.assignees || null}
defaultOptions={issue?.assignee_details ? issue.assignee_details : []}
hideDropdownArrow
onChange={handleAssignee}
disabled={isReadOnly}
multiple
/>
)}
{/* estimates */}
{displayProperties && displayProperties?.estimate && (
<IssuePropertyEstimates
projectId={issue?.project_detail?.id || null}
value={issue?.estimate_point || null}
onChange={handleEstimate}
disabled={isReadOnly}
hideDropdownArrow
/>
)}
{/* extra render properties */}
{/* sub-issues */}
{displayProperties && displayProperties?.sub_issue_count && !!issue?.sub_issues_count && (
<Tooltip tooltipHeading="Sub-issues" tooltipContent={`${issue.sub_issues_count}`}>
<div className="flex h-5 flex-shrink-0 items-center justify-center gap-2 overflow-hidden rounded border-[0.5px] border-custom-border-300 px-2.5 py-1 cursor-default">
<Layers className="h-3 w-3 flex-shrink-0" strokeWidth={2} />
<div className="text-xs">{issue.sub_issues_count}</div>
</div>
</Tooltip>
)}
{/* attachments */}
{displayProperties && displayProperties?.attachment_count && !!issue?.attachment_count && (
<Tooltip tooltipHeading="Attachments" tooltipContent={`${issue.attachment_count}`}>
<div className="flex h-5 flex-shrink-0 items-center justify-center gap-2 overflow-hidden rounded border-[0.5px] border-custom-border-300 px-2.5 py-1 cursor-default">
<Paperclip className="h-3 w-3 flex-shrink-0" strokeWidth={2} />
<div className="text-xs">{issue.attachment_count}</div>
</div>
</Tooltip>
)}
{/* link */}
{displayProperties && displayProperties?.link && !!issue?.link_count && (
<Tooltip tooltipHeading="Links" tooltipContent={`${issue.link_count}`}>
<div className="flex h-5 flex-shrink-0 items-center justify-center gap-2 overflow-hidden rounded border-[0.5px] border-custom-border-300 px-2.5 py-1 cursor-default">
<Link className="h-3 w-3 flex-shrink-0" strokeWidth={2} />
<div className="text-xs">{issue.link_count}</div>
</div>
</Tooltip>
)}
</div>
);
});

View file

@ -1,18 +1,17 @@
import { useEffect, useState, useRef } from "react";
import { useRouter } from "next/router";
import { useForm } from "react-hook-form";
import { PlusIcon } from "lucide-react";
import { observer } from "mobx-react-lite";
// store
import { useMobxStore } from "lib/mobx/store-provider";
import { PlusIcon } from "lucide-react";
// 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";
// helpers
import { createIssuePayload } from "helpers/issue.helper";
// types
import { IIssue, IProject } from "types";
import { TIssue } from "@plane/types";
const Inputs = (props: any) => {
const { register, setFocus, projectDetail } = props;
@ -37,35 +36,34 @@ const Inputs = (props: any) => {
};
interface IKanBanQuickAddIssueForm {
formKey: keyof IIssue;
formKey: keyof TIssue;
groupId?: string;
subGroupId?: string | null;
prePopulatedData?: Partial<IIssue>;
prePopulatedData?: Partial<TIssue>;
quickAddCallback?: (
workspaceSlug: string,
projectId: string,
data: IIssue,
data: TIssue,
viewId?: string
) => Promise<IIssue | undefined>;
) => Promise<TIssue | undefined>;
viewId?: string;
}
const defaultValues: Partial<IIssue> = {
const defaultValues: Partial<TIssue> = {
name: "",
};
export const KanBanQuickAddIssueForm: React.FC<IKanBanQuickAddIssueForm> = observer((props) => {
const { formKey, groupId, prePopulatedData, quickAddCallback, viewId } = props;
// router
const router = useRouter();
const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string };
const { workspaceSlug, projectId } = router.query;
// store hooks
const { getWorkspaceBySlug } = useWorkspace();
const { getProjectById } = useProject();
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 workspaceDetail = workspaceSlug ? getWorkspaceBySlug(workspaceSlug.toString()) : null;
const projectDetail = projectId ? getProjectById(projectId.toString()) : null;
const ref = useRef<HTMLFormElement>(null);
@ -82,14 +80,14 @@ export const KanBanQuickAddIssueForm: React.FC<IKanBanQuickAddIssueForm> = obser
setFocus,
register,
formState: { isSubmitting },
} = useForm<IIssue>({ defaultValues });
} = useForm<TIssue>({ defaultValues });
useEffect(() => {
if (!isOpen) reset({ ...defaultValues });
}, [isOpen, reset]);
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 });
@ -101,8 +99,8 @@ export const KanBanQuickAddIssueForm: React.FC<IKanBanQuickAddIssueForm> = obser
try {
quickAddCallback &&
(await quickAddCallback(
workspaceSlug,
projectId,
workspaceSlug.toString(),
projectId.toString(),
{
...payload,
},

View file

@ -1,17 +1,16 @@
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";
// ui
import { CycleIssueQuickActions } from "components/issues";
// types
import { IIssue } from "types";
import { TIssue } from "@plane/types";
import { EIssueActions } from "../../types";
// components
import { BaseKanBanRoot } from "../base-kanban-root";
import { EProjectStore } from "store/command-palette.store";
import { IGroupedIssues, IIssueResponse, ISubGroupedIssues, TUnGroupedIssues } from "store/issues/types";
import { EIssuesStoreType } from "constants/issue";
export interface ICycleKanBanLayout {}
@ -20,78 +19,42 @@ export const CycleKanBanLayout: React.FC = observer(() => {
const { workspaceSlug, projectId, cycleId } = router.query;
// store
const {
cycleIssues: cycleIssueStore,
cycleIssuesFilter: cycleIssueFilterStore,
cycleIssueKanBanView: cycleIssueKanBanViewStore,
kanBanHelpers: kanBanHelperStore,
cycle: { fetchCycleWithId },
} = useMobxStore();
const { issues, issuesFilter } = useIssues(EIssuesStoreType.CYCLE);
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 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.toString(), issue.project, issue.id, cycleId.toString());
fetchCycleWithId(workspaceSlug.toString(), issue.project, cycleId.toString());
},
[EIssueActions.REMOVE]: async (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.toString(),
issue.project,
cycleId.toString(),
issue.id,
issue.bridge_id
);
fetchCycleWithId(workspaceSlug.toString(), issue.project, cycleId.toString());
},
};
const handleDragDrop = async (
source: any,
destination: any,
subGroupBy: string | null,
groupBy: string | null,
issues: IIssueResponse | undefined,
issueWithIds: IGroupedIssues | ISubGroupedIssues | TUnGroupedIssues | undefined
) => {
if (workspaceSlug && projectId && cycleId)
return await kanBanHelperStore.handleDragDrop(
source,
destination,
workspaceSlug.toString(),
projectId.toString(),
cycleIssueStore,
subGroupBy,
groupBy,
issues,
issueWithIds,
cycleId.toString()
);
};
await issues.removeIssueFromCycle(workspaceSlug.toString(), issue.project_id, cycleId.toString(), issue.id);
},
}),
[issues, workspaceSlug, cycleId]
);
return (
<BaseKanBanRoot
issueActions={issueActions}
issueStore={cycleIssueStore}
issuesFilterStore={cycleIssueFilterStore}
kanbanViewStore={cycleIssueKanBanViewStore}
issues={issues}
issuesFilter={issuesFilter}
showLoader={true}
QuickActions={CycleIssueQuickActions}
viewId={cycleId?.toString() ?? ""}
currentStore={EProjectStore.CYCLE}
handleDragDrop={handleDragDrop}
addIssuesToView={(issues: string[]) =>
cycleIssueStore.addIssueToCycle(workspaceSlug?.toString() ?? "", cycleId?.toString() ?? "", issues)
}
currentStore={EIssuesStoreType.CYCLE}
addIssuesToView={(issueIds: string[]) => {
if (!workspaceSlug || !projectId || !cycleId) throw new Error();
return issues.addIssueToCycle(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), issueIds);
}}
/>
);
});

View file

@ -1,14 +1,16 @@
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";
// constants
import { EIssueActions } from "../../types";
import { BaseKanBanRoot } from "../base-kanban-root";
import { EIssuesStoreType } from "constants/issue";
import { useMemo } from "react";
export interface IKanBanLayout {}
@ -16,31 +18,30 @@ export const DraftKanBanLayout: React.FC = observer(() => {
const router = useRouter();
const { workspaceSlug } = router.query;
const {
projectDraftIssues: issueStore,
projectDraftIssuesFilter: projectIssuesFilterStore,
issueKanBanView: issueKanBanViewStore,
} = useMobxStore();
// store
const { issues, issuesFilter } = useIssues(EIssuesStoreType.DRAFT);
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);
},
};
await issues.removeIssue(workspaceSlug.toString(), issue.project_id, issue.id);
},
}),
[issues, workspaceSlug]
);
return (
<BaseKanBanRoot
issueActions={issueActions}
issuesFilterStore={projectIssuesFilterStore}
issueStore={issueStore}
kanbanViewStore={issueKanBanViewStore}
issuesFilter={issuesFilter}
issues={issues}
showLoader={true}
QuickActions={ProjectIssueQuickActions}
/>

View file

@ -1,17 +1,16 @@
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";
// hook
import { useIssues } from "hooks/store";
// components
import { ModuleIssueQuickActions } from "components/issues";
// types
import { IIssue } from "types";
import { TIssue } from "@plane/types";
// constants
import { EIssueActions } from "../../types";
import { BaseKanBanRoot } from "../base-kanban-root";
import { EProjectStore } from "store/command-palette.store";
import { IGroupedIssues, IIssueResponse, ISubGroupedIssues, TUnGroupedIssues } from "store/issues/types";
import { EIssuesStoreType } from "constants/issue";
export interface IModuleKanBanLayout {}
@ -20,77 +19,42 @@ export const ModuleKanBanLayout: React.FC = observer(() => {
const { workspaceSlug, projectId, moduleId } = router.query;
// store
const {
moduleIssues: moduleIssueStore,
moduleIssuesFilter: moduleIssueFilterStore,
moduleIssueKanBanView: moduleIssueKanBanViewStore,
kanBanHelpers: kanBanHelperStore,
module: { fetchModuleDetails },
} = useMobxStore();
const { issues, issuesFilter } = useIssues(EIssuesStoreType.MODULE);
const issueActions = {
[EIssueActions.UPDATE]: async (issue: IIssue) => {
if (!workspaceSlug || !moduleId) return;
const issueActions = useMemo(
() => ({
[EIssueActions.UPDATE]: async (issue: TIssue) => {
if (!workspaceSlug || !moduleId) return;
await moduleIssueStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue, moduleId.toString());
fetchModuleDetails(workspaceSlug.toString(), issue.project, moduleId.toString());
},
[EIssueActions.DELETE]: async (issue: IIssue) => {
if (!workspaceSlug || !moduleId) return;
await issues.updateIssue(workspaceSlug.toString(), issue.project_id, issue.id, issue, moduleId.toString());
},
[EIssueActions.DELETE]: async (issue: TIssue) => {
if (!workspaceSlug || !moduleId) return;
await moduleIssueStore.removeIssue(workspaceSlug.toString(), issue.project, issue.id, moduleId.toString());
fetchModuleDetails(workspaceSlug.toString(), issue.project, moduleId.toString());
},
[EIssueActions.REMOVE]: async (issue: IIssue) => {
if (!workspaceSlug || !moduleId || !issue.bridge_id) return;
await issues.removeIssue(workspaceSlug.toString(), issue.project_id, issue.id, moduleId.toString());
},
[EIssueActions.REMOVE]: async (issue: TIssue) => {
if (!workspaceSlug || !moduleId) return;
await moduleIssueStore.removeIssueFromModule(
workspaceSlug.toString(),
issue.project,
moduleId.toString(),
issue.id,
issue.bridge_id
);
fetchModuleDetails(workspaceSlug.toString(), issue.project, moduleId.toString());
},
};
await issues.removeIssueFromModule(workspaceSlug.toString(), issue.project_id, moduleId.toString(), issue.id);
},
}),
[issues, workspaceSlug, moduleId]
);
const handleDragDrop = async (
source: any,
destination: any,
subGroupBy: string | null,
groupBy: string | null,
issues: IIssueResponse | undefined,
issueWithIds: IGroupedIssues | ISubGroupedIssues | TUnGroupedIssues | undefined
) => {
if (workspaceSlug && projectId && moduleId)
return await kanBanHelperStore.handleDragDrop(
source,
destination,
workspaceSlug.toString(),
projectId.toString(),
moduleIssueStore,
subGroupBy,
groupBy,
issues,
issueWithIds,
moduleId.toString()
);
};
return (
<BaseKanBanRoot
issueActions={issueActions}
issueStore={moduleIssueStore}
issuesFilterStore={moduleIssueFilterStore}
kanbanViewStore={moduleIssueKanBanViewStore}
issues={issues}
issuesFilter={issuesFilter}
showLoader={true}
QuickActions={ModuleIssueQuickActions}
viewId={moduleId?.toString() ?? ""}
currentStore={EProjectStore.MODULE}
handleDragDrop={handleDragDrop}
addIssuesToView={(issues: string[]) =>
moduleIssueStore.addIssueToModule(workspaceSlug?.toString() ?? "", moduleId?.toString() ?? "", 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);
}}
/>
);
});

View file

@ -1,56 +1,58 @@
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// hooks
import { useIssues, useUser } from "hooks/store";
// components
import { ProjectIssueQuickActions } from "components/issues";
// types
import { IIssue } from "types";
import { TIssue } from "@plane/types";
// constants
import { EIssueActions } from "../../types";
import { BaseKanBanRoot } from "../base-kanban-root";
import { EProjectStore } from "store/command-palette.store";
import { EUserWorkspaceRoles } from "constants/workspace";
import { EUserProjectRoles } from "constants/project";
import { EIssuesStoreType } from "constants/issue";
import { useMemo } from "react";
export const ProfileIssuesKanBanLayout: React.FC = observer(() => {
const router = useRouter();
const { workspaceSlug, userId } = router.query as { workspaceSlug: string; userId: string };
const { issues, issuesFilter } = useIssues(EIssuesStoreType.PROFILE);
const {
workspaceProfileIssues: profileIssuesStore,
workspaceProfileIssuesFilter: profileIssueFiltersStore,
workspaceMember: { currentWorkspaceUserProjectsRole },
issueKanBanView: issueKanBanViewStore,
} = useMobxStore();
membership: { currentWorkspaceAllProjectsRole },
} = useUser();
const issueActions = {
[EIssueActions.UPDATE]: async (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 (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);
},
};
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];
return !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
return !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
};
return (
<BaseKanBanRoot
issueActions={issueActions}
issuesFilterStore={profileIssueFiltersStore}
issueStore={profileIssuesStore}
kanbanViewStore={issueKanBanViewStore}
issuesFilter={issuesFilter}
issues={issues}
showLoader={true}
QuickActions={ProjectIssueQuickActions}
currentStore={EProjectStore.PROFILE}
currentStore={EIssuesStoreType.PROFILE}
canEditPropertiesBasedOnProject={canEditPropertiesBasedOnProject}
/>
);

View file

@ -1,73 +1,49 @@
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import { useMemo } from "react";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
import { useIssues } from "hooks/store/use-issues";
// components
import { ProjectIssueQuickActions } from "components/issues";
import { BaseKanBanRoot } from "../base-kanban-root";
// types
import { IIssue } from "types";
import { TIssue } from "@plane/types";
// constants
import { EIssueActions } from "../../types";
import { BaseKanBanRoot } from "../base-kanban-root";
import { EProjectStore } from "store/command-palette.store";
import { IGroupedIssues, IIssueResponse, ISubGroupedIssues, TUnGroupedIssues } from "store/issues/types";
import { EIssuesStoreType } from "constants/issue";
export interface IKanBanLayout {}
export const KanBanLayout: React.FC = observer(() => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string };
const { workspaceSlug } = router.query as { workspaceSlug: string; projectId: string };
const {
projectIssues: issueStore,
projectIssuesFilter: issuesFilterStore,
issueKanBanView: issueKanBanViewStore,
kanBanHelpers: kanBanHelperStore,
} = 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, issue.project, issue.id, issue);
},
[EIssueActions.DELETE]: async (issue: IIssue) => {
if (!workspaceSlug) return;
await issues.updateIssue(workspaceSlug, issue.project_id, issue.id, issue);
},
[EIssueActions.DELETE]: async (issue: TIssue) => {
if (!workspaceSlug) return;
await issueStore.removeIssue(workspaceSlug, issue.project, issue.id);
},
};
const handleDragDrop = async (
source: any,
destination: any,
subGroupBy: string | null,
groupBy: string | null,
issues: IIssueResponse | undefined,
issueWithIds: IGroupedIssues | ISubGroupedIssues | TUnGroupedIssues | undefined
) =>
await kanBanHelperStore.handleDragDrop(
source,
destination,
workspaceSlug,
projectId,
issueStore,
subGroupBy,
groupBy,
issues,
issueWithIds
);
await issues.removeIssue(workspaceSlug, issue.project_id, issue.id);
},
}),
[issues, workspaceSlug]
);
return (
<BaseKanBanRoot
issueActions={issueActions}
issuesFilterStore={issuesFilterStore}
issueStore={issueStore}
kanbanViewStore={issueKanBanViewStore}
issues={issues}
issuesFilter={issuesFilter}
showLoader={true}
QuickActions={ProjectIssueQuickActions}
currentStore={EProjectStore.PROJECT}
handleDragDrop={handleDragDrop}
currentStore={EIssuesStoreType.PROJECT}
/>
);
});

View file

@ -1,73 +1,47 @@
import React from "react";
import React, { useMemo } from "react";
import { observer } from "mobx-react-lite";
import { useRouter } from "next/router";
// store
import { useMobxStore } from "lib/mobx/store-provider";
// hooks
import { useIssues } from "hooks/store";
// constant
import { IIssue } from "types";
import { TIssue } from "@plane/types";
import { EIssueActions } from "../../types";
import { ProjectIssueQuickActions } from "../../quick-action-dropdowns";
// components
import { BaseKanBanRoot } from "../base-kanban-root";
import { EProjectStore } from "store/command-palette.store";
import { IGroupedIssues, IIssueResponse, ISubGroupedIssues, TUnGroupedIssues } from "store/issues/types";
import { EIssuesStoreType } from "constants/issue";
export interface IViewKanBanLayout {}
export const ProjectViewKanBanLayout: React.FC = observer(() => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string };
const { workspaceSlug } = router.query as { workspaceSlug: string; projectId: string };
const {
viewIssues: projectViewIssuesStore,
viewIssuesFilter: projectIssueViewFiltersStore,
issueKanBanView: projectViewIssueKanBanViewStore,
kanBanHelpers: kanBanHelperStore,
} = useMobxStore();
const { issues, issuesFilter } = useIssues(EIssuesStoreType.PROJECT_VIEW);
const issueActions = useMemo(
() => ({
[EIssueActions.UPDATE]: async (issue: TIssue) => {
if (!workspaceSlug) return;
const issueActions = {
[EIssueActions.UPDATE]: async (issue: IIssue) => {
if (!workspaceSlug) return;
await issues.updateIssue(workspaceSlug, issue.project_id, issue.id, issue);
},
[EIssueActions.DELETE]: async (issue: TIssue) => {
if (!workspaceSlug) return;
await projectViewIssuesStore.updateIssue(workspaceSlug, issue.project, issue.id, issue);
},
[EIssueActions.DELETE]: async (issue: IIssue) => {
if (!workspaceSlug) return;
await projectViewIssuesStore.removeIssue(workspaceSlug, issue.project, issue.id);
},
};
const handleDragDrop = async (
source: any,
destination: any,
subGroupBy: string | null,
groupBy: string | null,
issues: IIssueResponse | undefined,
issueWithIds: IGroupedIssues | ISubGroupedIssues | TUnGroupedIssues | undefined
) =>
await kanBanHelperStore.handleDragDrop(
source,
destination,
workspaceSlug,
projectId,
projectViewIssuesStore,
subGroupBy,
groupBy,
issues,
issueWithIds
);
await issues.removeIssue(workspaceSlug, issue.project_id, issue.id);
},
}),
[issues, workspaceSlug]
);
return (
<BaseKanBanRoot
issueActions={issueActions}
issuesFilterStore={projectIssueViewFiltersStore}
issueStore={projectViewIssuesStore}
kanbanViewStore={projectViewIssueKanBanViewStore}
issuesFilter={issuesFilter}
issues={issues}
showLoader={true}
QuickActions={ProjectIssueQuickActions}
currentStore={EProjectStore.PROJECT_VIEW}
handleDragDrop={handleDragDrop}
currentStore={EIssuesStoreType.PROJECT_VIEW}
/>
);
});

View file

@ -1,142 +1,111 @@
import React from "react";
import { observer } from "mobx-react-lite";
// components
import { KanBanGroupByHeaderRoot } from "./headers/group-by-root";
import { KanBanSubGroupByHeaderRoot } from "./headers/sub-group-by-root";
import { KanBan } from "./default";
import { HeaderSubGroupByCard } from "./headers/sub-group-by-card";
import { HeaderGroupByCard } from "./headers/group-by-card";
// types
import { IIssue, IIssueDisplayProperties, IIssueLabel, IProject, IState, IUserLite } from "types";
import { IIssueResponse, IGroupedIssues, ISubGroupedIssues, TUnGroupedIssues } from "store/issues/types";
import {
GroupByColumnTypes,
IGroupByColumn,
TGroupedIssues,
TIssue,
IIssueDisplayProperties,
IIssueMap,
TSubGroupedIssues,
TUnGroupedIssues,
} from "@plane/types";
// constants
import { getValueFromObject } from "constants/issue";
import { EIssueActions } from "../types";
import { EProjectStore } from "store/command-palette.store";
import { useLabel, useMember, useProject, useProjectState } from "hooks/store";
import { getGroupByColumns } from "../utils";
import { TCreateModalStoreTypes } from "constants/issue";
interface ISubGroupSwimlaneHeader {
issues: IIssueResponse;
issueIds: any;
issueIds: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues;
sub_group_by: string | null;
group_by: string | null;
list: any;
listKey: string;
list: IGroupByColumn[];
kanBanToggle: any;
handleKanBanToggle: any;
disableIssueCreation?: boolean;
currentStore?: EProjectStore;
addIssuesToView?: (issueIds: string[]) => Promise<IIssue>;
}
const SubGroupSwimlaneHeader: React.FC<ISubGroupSwimlaneHeader> = ({
issueIds,
sub_group_by,
group_by,
list,
listKey,
kanBanToggle,
handleKanBanToggle,
disableIssueCreation,
currentStore,
addIssuesToView,
}) => {
const calculateIssueCount = (column_id: string) => {
let issueCount = 0;
issueIds &&
Object.keys(issueIds)?.forEach((_issueKey: any) => {
issueCount += issueIds?.[_issueKey]?.[column_id]?.length || 0;
});
return issueCount;
};
return (
<div className="relative flex h-max min-h-full w-full items-center">
{list &&
list.length > 0 &&
list.map((_list: any) => (
<div className="flex w-[340px] flex-shrink-0 flex-col">
<KanBanGroupByHeaderRoot
column_id={getValueFromObject(_list, listKey) as string}
column_value={_list}
sub_group_by={sub_group_by}
group_by={group_by}
issues_count={calculateIssueCount(getValueFromObject(_list, listKey) as string)}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
disableIssueCreation={disableIssueCreation}
currentStore={currentStore}
addIssuesToView={addIssuesToView}
/>
</div>
))}
</div>
);
};
}) => (
<div className="relative flex h-max min-h-full w-full items-center">
{list &&
list.length > 0 &&
list.map((_list: IGroupByColumn) => (
<div key={`${sub_group_by}_${_list.id}`} className="flex w-[340px] flex-shrink-0 flex-col">
<HeaderGroupByCard
sub_group_by={sub_group_by}
group_by={group_by}
column_id={_list.id}
icon={_list.Icon}
title={_list.name}
count={(issueIds as TGroupedIssues)?.[_list.id]?.length || 0}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
issuePayload={_list.payload}
/>
</div>
))}
</div>
);
interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader {
issues: IIssueResponse;
issueIds: any;
order_by: string | null;
issuesMap: IIssueMap;
issueIds: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues;
showEmptyGroup: boolean;
states: IState[] | null;
stateGroups: any;
priorities: any;
labels: IIssueLabel[] | null;
members: IUserLite[] | null;
projects: IProject[] | null;
handleIssues: (sub_group_by: string | null, group_by: string | null, issue: IIssue, action: EIssueActions) => void;
quickActions: (
sub_group_by: string | null,
group_by: string | null,
issue: IIssue,
customActionButton?: React.ReactElement
) => React.ReactNode;
displayProperties: IIssueDisplayProperties | null;
displayProperties: IIssueDisplayProperties | undefined;
handleIssues: (issue: TIssue, action: EIssueActions) => void;
quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode;
kanBanToggle: any;
handleKanBanToggle: any;
isDragStarted?: boolean;
disableIssueCreation?: boolean;
currentStore?: EProjectStore;
currentStore?: TCreateModalStoreTypes;
enableQuickIssueCreate: boolean;
canEditProperties: (projectId: string | undefined) => boolean;
addIssuesToView?: (issueIds: string[]) => Promise<TIssue>;
quickAddCallback?: (
workspaceSlug: string,
projectId: string,
data: IIssue,
data: TIssue,
viewId?: string
) => Promise<IIssue | undefined>;
) => Promise<TIssue | undefined>;
viewId?: string;
}
const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
const {
issues,
issuesMap,
issueIds,
sub_group_by,
group_by,
order_by,
list,
listKey,
handleIssues,
quickActions,
displayProperties,
kanBanToggle,
handleKanBanToggle,
showEmptyGroup,
states,
stateGroups,
priorities,
labels,
members,
projects,
isDragStarted,
disableIssueCreation,
enableQuickIssueCreate,
canEditProperties,
addIssuesToView,
quickAddCallback,
viewId,
} = props;
const calculateIssueCount = (column_id: string) => {
let issueCount = 0;
issueIds?.[column_id] &&
Object.keys(issueIds?.[column_id])?.forEach((_list: any) => {
issueCount += issueIds?.[column_id]?.[_list]?.length || 0;
const subGroupedIds = issueIds as TSubGroupedIssues;
subGroupedIds?.[column_id] &&
Object.keys(subGroupedIds?.[column_id])?.forEach((_list: any) => {
issueCount += subGroupedIds?.[column_id]?.[_list]?.length || 0;
});
return issueCount;
};
@ -149,46 +118,36 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
<div className="flex flex-shrink-0 flex-col">
<div className="sticky top-[50px] z-[1] flex w-full items-center bg-custom-background-90 py-1">
<div className="sticky left-0 flex-shrink-0 bg-custom-background-90 pr-2">
<KanBanSubGroupByHeaderRoot
column_id={getValueFromObject(_list, listKey) as string}
column_value={_list}
sub_group_by={sub_group_by}
group_by={group_by}
issues_count={calculateIssueCount(getValueFromObject(_list, listKey) as string)}
<HeaderSubGroupByCard
column_id={_list.id}
icon={_list.Icon}
title={_list.name || ""}
count={calculateIssueCount(_list.id)}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
disableIssueCreation={disableIssueCreation}
addIssuesToView={addIssuesToView}
/>
</div>
<div className="w-full border-b border-dashed border-custom-border-400" />
</div>
{!kanBanToggle?.subgroupByIssuesVisibility.includes(getValueFromObject(_list, listKey) as string) && (
{!kanBanToggle?.subgroupByIssuesVisibility.includes(_list.id) && (
<div className="relative">
<KanBan
issues={issues}
issueIds={issueIds?.[getValueFromObject(_list, listKey) as string]}
issuesMap={issuesMap}
issueIds={(issueIds as TSubGroupedIssues)?.[_list.id]}
displayProperties={displayProperties}
sub_group_by={sub_group_by}
group_by={group_by}
order_by={order_by}
sub_group_id={getValueFromObject(_list, listKey) as string}
sub_group_id={_list.id}
handleIssues={handleIssues}
quickActions={quickActions}
displayProperties={displayProperties}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
showEmptyGroup={showEmptyGroup}
states={states}
stateGroups={stateGroups}
priorities={priorities}
labels={labels}
members={members}
projects={projects}
enableQuickIssueCreate={enableQuickIssueCreate}
isDragStarted={isDragStarted}
canEditProperties={canEditProperties}
addIssuesToView={addIssuesToView}
quickAddCallback={quickAddCallback}
viewId={viewId}
/>
</div>
)}
@ -199,414 +158,101 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
});
export interface IKanBanSwimLanes {
issues: IIssueResponse;
issueIds: IGroupedIssues | ISubGroupedIssues | TUnGroupedIssues;
issuesMap: IIssueMap;
issueIds: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues;
displayProperties: IIssueDisplayProperties | undefined;
sub_group_by: string | null;
group_by: string | null;
order_by: string | null;
handleIssues: (sub_group_by: string | null, group_by: string | null, issue: IIssue, action: EIssueActions) => void;
quickActions: (
sub_group_by: string | null,
group_by: string | null,
issue: IIssue,
customActionButton?: React.ReactElement
) => React.ReactNode;
displayProperties: IIssueDisplayProperties | null;
handleIssues: (issue: TIssue, action: EIssueActions) => void;
quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode;
kanBanToggle: any;
handleKanBanToggle: any;
showEmptyGroup: boolean;
states: IState[] | null;
stateGroups: any;
priorities: any;
labels: IIssueLabel[] | null;
members: IUserLite[] | null;
projects: IProject[] | null;
isDragStarted?: boolean;
disableIssueCreation?: boolean;
currentStore?: EProjectStore;
addIssuesToView?: (issueIds: string[]) => Promise<IIssue>;
currentStore?: TCreateModalStoreTypes;
addIssuesToView?: (issueIds: string[]) => Promise<TIssue>;
enableQuickIssueCreate: boolean;
quickAddCallback?: (
workspaceSlug: string,
projectId: string,
data: IIssue,
data: TIssue,
viewId?: string
) => Promise<IIssue | undefined>;
) => Promise<TIssue | undefined>;
viewId?: string;
canEditProperties: (projectId: string | undefined) => boolean;
}
export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
const {
issues,
issuesMap,
issueIds,
displayProperties,
sub_group_by,
group_by,
order_by,
handleIssues,
quickActions,
displayProperties,
kanBanToggle,
handleKanBanToggle,
showEmptyGroup,
states,
stateGroups,
priorities,
labels,
members,
projects,
isDragStarted,
disableIssueCreation,
enableQuickIssueCreate,
canEditProperties,
currentStore,
addIssuesToView,
quickAddCallback,
viewId,
} = props;
const member = useMember();
const project = useProject();
const projectLabel = useLabel();
const projectState = useProjectState();
const groupByList = getGroupByColumns(group_by as GroupByColumnTypes, project, projectLabel, projectState, member);
const subGroupByList = getGroupByColumns(
sub_group_by as GroupByColumnTypes,
project,
projectLabel,
projectState,
member
);
if (!groupByList || !subGroupByList) return null;
return (
<div className="relative">
<div className="sticky top-0 z-[2] h-[50px] bg-custom-background-90">
{group_by && group_by === "project" && (
<SubGroupSwimlaneHeader
issues={issues}
issueIds={issueIds}
sub_group_by={sub_group_by}
group_by={group_by}
list={projects}
listKey={`id`}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
disableIssueCreation={disableIssueCreation}
currentStore={currentStore}
addIssuesToView={addIssuesToView}
/>
)}
{group_by && group_by === "state" && (
<SubGroupSwimlaneHeader
issues={issues}
issueIds={issueIds}
sub_group_by={sub_group_by}
group_by={group_by}
list={states}
listKey={`id`}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
disableIssueCreation={disableIssueCreation}
currentStore={currentStore}
addIssuesToView={addIssuesToView}
/>
)}
{group_by && group_by === "state_detail.group" && (
<SubGroupSwimlaneHeader
issues={issues}
issueIds={issueIds}
sub_group_by={sub_group_by}
group_by={group_by}
list={stateGroups}
listKey={`key`}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
disableIssueCreation={disableIssueCreation}
currentStore={currentStore}
addIssuesToView={addIssuesToView}
/>
)}
{group_by && group_by === "priority" && (
<SubGroupSwimlaneHeader
issues={issues}
issueIds={issueIds}
sub_group_by={sub_group_by}
group_by={group_by}
list={priorities}
listKey={`key`}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
currentStore={currentStore}
addIssuesToView={addIssuesToView}
/>
)}
{group_by && group_by === "labels" && (
<SubGroupSwimlaneHeader
issues={issues}
issueIds={issueIds}
sub_group_by={sub_group_by}
group_by={group_by}
list={labels ? [...labels, { id: "None", name: "None" }] : labels}
listKey={`id`}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
disableIssueCreation={disableIssueCreation}
currentStore={currentStore}
addIssuesToView={addIssuesToView}
/>
)}
{group_by && group_by === "assignees" && (
<SubGroupSwimlaneHeader
issues={issues}
issueIds={issueIds}
sub_group_by={sub_group_by}
group_by={group_by}
list={members ? [...members, { id: "None", display_name: "None" }] : members}
listKey={`id`}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
disableIssueCreation={disableIssueCreation}
currentStore={currentStore}
addIssuesToView={addIssuesToView}
/>
)}
{group_by && group_by === "created_by" && (
<SubGroupSwimlaneHeader
issues={issues}
issueIds={issueIds}
sub_group_by={sub_group_by}
group_by={group_by}
list={members}
listKey={`id`}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
disableIssueCreation={disableIssueCreation}
currentStore={currentStore}
addIssuesToView={addIssuesToView}
/>
)}
<SubGroupSwimlaneHeader
issueIds={issueIds}
group_by={group_by}
sub_group_by={sub_group_by}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
list={groupByList}
/>
</div>
{sub_group_by && sub_group_by === "project" && (
{sub_group_by && (
<SubGroupSwimlane
issues={issues}
issuesMap={issuesMap}
list={subGroupByList}
issueIds={issueIds}
sub_group_by={sub_group_by}
displayProperties={displayProperties}
group_by={group_by}
order_by={order_by}
list={projects}
listKey={`id`}
sub_group_by={sub_group_by}
handleIssues={handleIssues}
quickActions={quickActions}
displayProperties={displayProperties}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
showEmptyGroup={showEmptyGroup}
states={states}
stateGroups={stateGroups}
priorities={priorities}
labels={labels}
members={members}
projects={projects}
isDragStarted={isDragStarted}
disableIssueCreation={disableIssueCreation}
enableQuickIssueCreate={enableQuickIssueCreate}
canEditProperties={canEditProperties}
quickAddCallback={quickAddCallback}
/>
)}
{sub_group_by && sub_group_by === "state" && (
<SubGroupSwimlane
issues={issues}
issueIds={issueIds}
sub_group_by={sub_group_by}
group_by={group_by}
order_by={order_by}
list={states}
listKey={`id`}
handleIssues={handleIssues}
quickActions={quickActions}
displayProperties={displayProperties}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
showEmptyGroup={showEmptyGroup}
states={states}
stateGroups={stateGroups}
priorities={priorities}
labels={labels}
members={members}
projects={projects}
isDragStarted={isDragStarted}
disableIssueCreation={disableIssueCreation}
enableQuickIssueCreate={enableQuickIssueCreate}
canEditProperties={canEditProperties}
quickAddCallback={quickAddCallback}
/>
)}
{sub_group_by && sub_group_by === "state" && (
<SubGroupSwimlane
issues={issues}
issueIds={issueIds}
sub_group_by={sub_group_by}
group_by={group_by}
order_by={order_by}
list={states}
listKey={`id`}
handleIssues={handleIssues}
quickActions={quickActions}
displayProperties={displayProperties}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
showEmptyGroup={showEmptyGroup}
states={states}
stateGroups={stateGroups}
priorities={priorities}
labels={labels}
members={members}
projects={projects}
isDragStarted={isDragStarted}
disableIssueCreation={disableIssueCreation}
enableQuickIssueCreate={enableQuickIssueCreate}
canEditProperties={canEditProperties}
quickAddCallback={quickAddCallback}
/>
)}
{sub_group_by && sub_group_by === "state_detail.group" && (
<SubGroupSwimlane
issues={issues}
issueIds={issueIds}
sub_group_by={sub_group_by}
group_by={group_by}
order_by={order_by}
list={stateGroups}
listKey={`key`}
handleIssues={handleIssues}
quickActions={quickActions}
displayProperties={displayProperties}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
showEmptyGroup={showEmptyGroup}
states={states}
stateGroups={stateGroups}
priorities={priorities}
labels={labels}
members={members}
projects={projects}
isDragStarted={isDragStarted}
disableIssueCreation={disableIssueCreation}
enableQuickIssueCreate={enableQuickIssueCreate}
canEditProperties={canEditProperties}
quickAddCallback={quickAddCallback}
/>
)}
{sub_group_by && sub_group_by === "priority" && (
<SubGroupSwimlane
issues={issues}
issueIds={issueIds}
sub_group_by={sub_group_by}
group_by={group_by}
order_by={order_by}
list={priorities}
listKey={`key`}
handleIssues={handleIssues}
quickActions={quickActions}
displayProperties={displayProperties}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
showEmptyGroup={showEmptyGroup}
states={states}
stateGroups={stateGroups}
priorities={priorities}
labels={labels}
members={members}
projects={projects}
isDragStarted={isDragStarted}
disableIssueCreation={disableIssueCreation}
enableQuickIssueCreate={enableQuickIssueCreate}
canEditProperties={canEditProperties}
quickAddCallback={quickAddCallback}
/>
)}
{sub_group_by && sub_group_by === "labels" && (
<SubGroupSwimlane
issues={issues}
issueIds={issueIds}
sub_group_by={sub_group_by}
group_by={group_by}
order_by={order_by}
list={labels ? [...labels, { id: "None", name: "None" }] : labels}
listKey={`id`}
handleIssues={handleIssues}
quickActions={quickActions}
displayProperties={displayProperties}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
showEmptyGroup={showEmptyGroup}
states={states}
stateGroups={stateGroups}
priorities={priorities}
labels={labels}
members={members}
projects={projects}
isDragStarted={isDragStarted}
disableIssueCreation={disableIssueCreation}
enableQuickIssueCreate={enableQuickIssueCreate}
canEditProperties={canEditProperties}
quickAddCallback={quickAddCallback}
/>
)}
{sub_group_by && sub_group_by === "assignees" && (
<SubGroupSwimlane
issues={issues}
issueIds={issueIds}
sub_group_by={sub_group_by}
group_by={group_by}
order_by={order_by}
list={members ? [...members, { id: "None", display_name: "None" }] : members}
listKey={`id`}
handleIssues={handleIssues}
quickActions={quickActions}
displayProperties={displayProperties}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
showEmptyGroup={showEmptyGroup}
states={states}
stateGroups={stateGroups}
priorities={priorities}
labels={labels}
members={members}
projects={projects}
isDragStarted={isDragStarted}
disableIssueCreation={disableIssueCreation}
enableQuickIssueCreate={enableQuickIssueCreate}
canEditProperties={canEditProperties}
quickAddCallback={quickAddCallback}
/>
)}
{sub_group_by && sub_group_by === "created_by" && (
<SubGroupSwimlane
issues={issues}
issueIds={issueIds}
sub_group_by={sub_group_by}
group_by={group_by}
order_by={order_by}
list={members}
listKey={`id`}
handleIssues={handleIssues}
quickActions={quickActions}
displayProperties={displayProperties}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
showEmptyGroup={showEmptyGroup}
states={states}
stateGroups={stateGroups}
priorities={priorities}
labels={labels}
members={members}
projects={projects}
isDragStarted={isDragStarted}
disableIssueCreation={disableIssueCreation}
enableQuickIssueCreate={enableQuickIssueCreate}
addIssuesToView={addIssuesToView}
canEditProperties={canEditProperties}
quickAddCallback={quickAddCallback}
viewId={viewId}
/>
)}
</div>

View file

@ -0,0 +1,167 @@
import { DraggableLocation } from "@hello-pangea/dnd";
import { ICycleIssues } from "store/issue/cycle";
import { IDraftIssues } from "store/issue/draft";
import { IModuleIssues } from "store/issue/module";
import { IProfileIssues } from "store/issue/profile";
import { IProjectIssues } from "store/issue/project";
import { IProjectViewIssues } from "store/issue/project-views";
import { IWorkspaceIssues } from "store/issue/workspace";
import { TGroupedIssues, IIssueMap, TSubGroupedIssues, TUnGroupedIssues } from "@plane/types";
export const handleDragDrop = async (
source: DraggableLocation | null | undefined,
destination: DraggableLocation | null | undefined,
workspaceSlug: string | undefined,
projectId: string | undefined, // projectId for all views or user id in profile issues
store:
| IProjectIssues
| ICycleIssues
| IDraftIssues
| IModuleIssues
| IDraftIssues
| IProjectViewIssues
| IProfileIssues
| IWorkspaceIssues,
subGroupBy: string | null,
groupBy: string | null,
issueMap: IIssueMap,
issueWithIds: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues | undefined,
viewId: string | null = null // it can be moduleId, cycleId
) => {
if (!issueMap || !issueWithIds || !source || !destination || !workspaceSlug || !projectId) return;
let updateIssue: any = {};
const sourceColumnId = (source?.droppableId && source?.droppableId.split("__")) || null;
const destinationColumnId = (destination?.droppableId && destination?.droppableId.split("__")) || null;
if (!sourceColumnId || !destinationColumnId) return;
const sourceGroupByColumnId = sourceColumnId[0] || null;
const destinationGroupByColumnId = destinationColumnId[0] || null;
const sourceSubGroupByColumnId = sourceColumnId[1] || null;
const destinationSubGroupByColumnId = destinationColumnId[1] || null;
if (
!workspaceSlug ||
!projectId ||
!groupBy ||
!sourceGroupByColumnId ||
!destinationGroupByColumnId ||
!sourceSubGroupByColumnId ||
!sourceGroupByColumnId
)
return;
if (destinationGroupByColumnId === "issue-trash-box") {
const sourceIssues: string[] = subGroupBy
? (issueWithIds as TSubGroupedIssues)[sourceSubGroupByColumnId][sourceGroupByColumnId]
: (issueWithIds as TGroupedIssues)[sourceGroupByColumnId];
const [removed] = sourceIssues.splice(source.index, 1);
if (removed) {
if (viewId) return await store?.removeIssue(workspaceSlug, projectId, removed); //, viewId);
else return await store?.removeIssue(workspaceSlug, projectId, removed);
}
} else {
const sourceIssues = subGroupBy
? (issueWithIds as TSubGroupedIssues)[sourceSubGroupByColumnId][sourceGroupByColumnId]
: (issueWithIds as TGroupedIssues)[sourceGroupByColumnId];
const destinationIssues = subGroupBy
? (issueWithIds as TSubGroupedIssues)[sourceSubGroupByColumnId][destinationGroupByColumnId]
: (issueWithIds as TGroupedIssues)[destinationGroupByColumnId];
const [removed] = sourceIssues.splice(source.index, 1);
const removedIssueDetail = issueMap[removed];
if (subGroupBy && sourceSubGroupByColumnId && destinationSubGroupByColumnId) {
updateIssue = {
id: removedIssueDetail?.id,
};
// for both horizontal and vertical dnd
updateIssue = {
...updateIssue,
...handleSortOrder(destinationIssues, destination.index, issueMap),
};
if (sourceSubGroupByColumnId === destinationSubGroupByColumnId) {
if (sourceGroupByColumnId != destinationGroupByColumnId) {
if (groupBy === "state") updateIssue = { ...updateIssue, state: destinationGroupByColumnId };
if (groupBy === "priority") updateIssue = { ...updateIssue, priority: destinationGroupByColumnId };
}
} else {
if (subGroupBy === "state")
updateIssue = {
...updateIssue,
state: destinationSubGroupByColumnId,
priority: destinationGroupByColumnId,
};
if (subGroupBy === "priority")
updateIssue = {
...updateIssue,
state: destinationGroupByColumnId,
priority: destinationSubGroupByColumnId,
};
}
} else {
updateIssue = {
id: removedIssueDetail?.id,
};
// for both horizontal and vertical dnd
updateIssue = {
...updateIssue,
...handleSortOrder(destinationIssues, destination.index, issueMap),
};
// for horizontal dnd
if (sourceColumnId != destinationColumnId) {
if (groupBy === "state") updateIssue = { ...updateIssue, state: destinationGroupByColumnId };
if (groupBy === "priority") updateIssue = { ...updateIssue, priority: destinationGroupByColumnId };
}
}
if (updateIssue && updateIssue?.id) {
if (viewId) return await store?.updateIssue(workspaceSlug, projectId, updateIssue.id, updateIssue); //, viewId);
else return await store?.updateIssue(workspaceSlug, projectId, updateIssue.id, updateIssue);
}
}
};
const handleSortOrder = (destinationIssues: string[], destinationIndex: number, issueMap: IIssueMap) => {
const sortOrderDefaultValue = 65535;
let currentIssueState = {};
if (destinationIssues && destinationIssues.length > 0) {
if (destinationIndex === 0) {
const destinationIssueId = destinationIssues[destinationIndex];
currentIssueState = {
...currentIssueState,
sort_order: issueMap[destinationIssueId].sort_order - sortOrderDefaultValue,
};
} else if (destinationIndex === destinationIssues.length) {
const destinationIssueId = destinationIssues[destinationIndex - 1];
currentIssueState = {
...currentIssueState,
sort_order: issueMap[destinationIssueId].sort_order + sortOrderDefaultValue,
};
} else {
const destinationTopIssueId = destinationIssues[destinationIndex - 1];
const destinationBottomIssueId = destinationIssues[destinationIndex];
currentIssueState = {
...currentIssueState,
sort_order: (issueMap[destinationTopIssueId].sort_order + issueMap[destinationBottomIssueId].sort_order) / 2,
};
}
} else {
currentIssueState = {
...currentIssueState,
sort_order: sortOrderDefaultValue,
};
}
return currentIssueState;
};

Some files were not shown because too many files have changed in this diff Show more