chore: bug fixes and improvement (#3303)

* refactor: updated preloaded function for the list view quick add

* fix: resolved bug in the assignee dropdown

* chore: issue sidebar link improvement

* fix: resolved subscription store bug

* chore: updated preloaded function for the kanban layout quick add

* chore: resolved issues in the list filters and component

* chore: filter store updated

* fix: issue serializer changed

* chore: quick add preload function updated

* fix: build error

* fix: serializer changed

* fix: minor request change

* chore: resolved build issues and updated the prepopulated data in the quick add issue.

* fix: build fix and code refactor

* fix: spreadsheet layout quick add fix

* fix: issue peek overview link section updated

* fix: cycle status bug fix

* fix: serializer changes

* fix: assignee and labels listing

* chore: issue modal parent_id default value updated

* fix: cycle and module issue serializer change

* fix: cycle list serializer changed

* chore: prepopulated validation in both list and kanban for quick add and group header add issues

* chore: group header validation added

* fix: issue response payload change

* dev: make cycle and module issue create response simillar

* chore: custom control link component added

* dev: make issue create and update response simillar to list and retrieve

* fix: build error

* chore: control link component improvement

* chore: globalise issue peek overview

* chore: control link component improvement

* chore: made changes and optimised the issue peek overview root

* build-error: resolved build erros for issueId dependancy from issue detail store

* chore: peek overview link fix

* dev: update state nullable rule

---------

Co-authored-by: gurusainath <gurusainath007@gmail.com>
Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
This commit is contained in:
Anmol Singh Bhatia 2024-01-05 23:37:13 +05:30 committed by sriram veeraghanta
parent 266f14d550
commit efd3ebf067
65 changed files with 630 additions and 1565 deletions

View File

@ -30,6 +30,8 @@ from plane.db.models import (
CommentReaction,
IssueVote,
IssueRelation,
State,
Project,
)
@ -69,19 +71,16 @@ class IssueProjectLiteSerializer(BaseSerializer):
##TODO: Find a better way to write this serializer
## Find a better approach to save manytomany?
class IssueCreateSerializer(BaseSerializer):
state_detail = StateSerializer(read_only=True, source="state")
created_by_detail = UserLiteSerializer(read_only=True, source="created_by")
project_detail = ProjectLiteSerializer(read_only=True, source="project")
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
assignees = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()),
# ids
state_id = serializers.PrimaryKeyRelatedField(source="state", queryset=State.objects.all(), required=False, allow_null=True)
parent_id = serializers.PrimaryKeyRelatedField(source='parent', queryset=Issue.objects.all(), required=False, allow_null=True)
label_ids = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=Label.objects.all()),
write_only=True,
required=False,
)
labels = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=Label.objects.all()),
assignee_ids = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()),
write_only=True,
required=False,
)
@ -100,8 +99,10 @@ class IssueCreateSerializer(BaseSerializer):
def to_representation(self, instance):
data = super().to_representation(instance)
data['assignees'] = [str(assignee.id) for assignee in instance.assignees.all()]
data['labels'] = [str(label.id) for label in instance.labels.all()]
assignee_ids = self.initial_data.get('assignee_ids')
data['assignee_ids'] = assignee_ids if assignee_ids else []
label_ids = self.initial_data.get('label_ids')
data['label_ids'] = label_ids if label_ids else []
return data
def validate(self, data):
@ -114,8 +115,8 @@ class IssueCreateSerializer(BaseSerializer):
return data
def create(self, validated_data):
assignees = validated_data.pop("assignees", None)
labels = validated_data.pop("labels", None)
assignees = validated_data.pop("assignee_ids", None)
labels = validated_data.pop("label_ids", None)
project_id = self.context["project_id"]
workspace_id = self.context["workspace_id"]
@ -173,8 +174,8 @@ class IssueCreateSerializer(BaseSerializer):
return issue
def update(self, instance, validated_data):
assignees = validated_data.pop("assignees", None)
labels = validated_data.pop("labels", None)
assignees = validated_data.pop("assignee_ids", None)
labels = validated_data.pop("labels_ids", None)
# Related models
project_id = instance.project_id
@ -544,7 +545,7 @@ class IssueSerializer(DynamicBaseSerializer):
attachment_count = serializers.IntegerField(read_only=True)
link_count = serializers.IntegerField(read_only=True)
# is
# is_subscribed
is_subscribed = serializers.BooleanField(read_only=True)
class Meta:

View File

@ -99,6 +99,7 @@ class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator):
response = super().handle_exception(exc)
return response
except Exception as e:
print(e) if settings.DEBUG else print("Server Error")
if isinstance(e, IntegrityError):
return Response(
{"error": "The payload is not valid"},
@ -124,8 +125,7 @@ class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator):
{"error": f"key {e} does not exist"},
status=status.HTTP_400_BAD_REQUEST,
)
print(e) if settings.DEBUG else print("Server Error")
capture_exception(e)
return Response({"error": "Something went wrong please try again later"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

View File

@ -31,6 +31,7 @@ from plane.app.serializers import (
CycleSerializer,
CycleIssueSerializer,
CycleFavoriteSerializer,
IssueSerializer,
IssueStateSerializer,
CycleWriteSerializer,
CycleUserPropertiesSerializer,
@ -46,9 +47,9 @@ from plane.db.models import (
IssueAttachment,
Label,
CycleUserProperties,
IssueSubscriber,
)
from plane.bgtasks.issue_activites_task import issue_activity
from plane.utils.grouper import group_results
from plane.utils.issue_filters import issue_filters
from plane.utils.analytics_plot import burndown_plot
@ -322,6 +323,8 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
project_id=project_id,
owned_by=request.user,
)
cycle = self.get_queryset().filter(pk=serializer.data["id"]).first()
serializer = CycleSerializer(cycle)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
else:
@ -548,6 +551,8 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
.prefetch_related("labels")
.order_by(order_by)
.filter(**filters)
.annotate(cycle_id=F("issue_cycle__cycle_id"))
.annotate(module_id=F("issue_module__module_id"))
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()
@ -560,8 +565,15 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
is_subscribed=Exists(
IssueSubscriber.objects.filter(
subscriber=self.request.user, issue_id=OuterRef("id")
)
)
)
)
serializer = IssueStateSerializer(
serializer = IssueSerializer(
issues, many=True, fields=fields if fields else None
)
return Response(serializer.data, status=status.HTTP_200_OK)
@ -652,8 +664,10 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
)
# Return all Cycle Issues
issues = self.get_queryset().values_list("issue_id", flat=True)
return Response(
CycleIssueSerializer(self.get_queryset(), many=True).data,
IssueSerializer(Issue.objects.filter(pk__in=issues), many=True).data,
status=status.HTTP_200_OK,
)

View File

@ -34,11 +34,11 @@ from rest_framework.parsers import MultiPartParser, FormParser
# Module imports
from . import BaseViewSet, BaseAPIView, WebhookMixin
from plane.app.serializers import (
IssueCreateSerializer,
IssueActivitySerializer,
IssueCommentSerializer,
IssuePropertySerializer,
IssueSerializer,
IssueCreateSerializer,
LabelSerializer,
IssueFlatSerializer,
IssueLinkSerializer,
@ -110,12 +110,7 @@ class IssueViewSet(WebhookMixin, BaseViewSet):
def get_queryset(self):
return (
Issue.issue_objects.annotate(
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
Issue.issue_objects
.filter(project_id=self.kwargs.get("project_id"))
.filter(workspace__slug=self.kwargs.get("slug"))
.select_related("project")
@ -143,13 +138,11 @@ class IssueViewSet(WebhookMixin, BaseViewSet):
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
is_subscribed=Exists(
IssueSubscriber.objects.filter(
subscriber=self.request.user, issue_id=OuterRef("id")
)
)
).annotate(
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
).distinct()
@ -251,16 +244,13 @@ class IssueViewSet(WebhookMixin, BaseViewSet):
current_instance=None,
epoch=int(timezone.now().timestamp()),
)
issue = self.get_queryset().filter(pk=serializer.data["id"]).first()
serializer = IssueSerializer(issue)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def retrieve(self, request, slug, project_id, pk=None):
issue = Issue.issue_objects.annotate(
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
).get(workspace__slug=slug, project_id=project_id, pk=pk)
issue = self.get_queryset().filter(pk=pk).first()
return Response(
IssueSerializer(issue, fields=self.fields, expand=self.expand).data,
status=status.HTTP_200_OK,
@ -284,7 +274,8 @@ class IssueViewSet(WebhookMixin, BaseViewSet):
current_instance=current_instance,
epoch=int(timezone.now().timestamp()),
)
return Response(serializer.data, status=status.HTTP_200_OK)
issue = self.get_queryset().filter(pk=pk).first()
return Response(IssueSerializer(issue).data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def destroy(self, request, slug, project_id, pk=None):
@ -719,13 +710,6 @@ class SubIssuesEndpoint(BaseAPIView):
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
is_subscribed=Exists(
IssueSubscriber.objects.filter(
subscriber=self.request.user, issue_id=OuterRef("id")
)
)
)
.prefetch_related(
Prefetch(
"issue_reactions",
@ -1080,7 +1064,7 @@ class IssueArchiveViewSet(BaseViewSet):
else issue_queryset.filter(parent__isnull=True)
)
issues = IssueLiteSerializer(
issues = IssueSerializer(
issue_queryset, many=True, fields=fields if fields else None
).data
return Response(issues, status=status.HTTP_200_OK)
@ -1163,16 +1147,6 @@ class IssueSubscriberViewSet(BaseViewSet):
project_id=project_id,
is_active=True,
)
.annotate(
is_subscribed=Exists(
IssueSubscriber.objects.filter(
workspace__slug=slug,
project_id=project_id,
issue_id=issue_id,
subscriber=OuterRef("member"),
)
)
)
.select_related("member")
)
serializer = ProjectMemberLiteSerializer(members, many=True)
@ -1613,7 +1587,7 @@ class IssueDraftViewSet(BaseViewSet):
else:
issue_queryset = issue_queryset.order_by(order_by_param)
issues = IssueLiteSerializer(
issues = IssueSerializer(
issue_queryset, many=True, fields=fields if fields else None
).data
return Response(issues, status=status.HTTP_200_OK)

View File

@ -20,7 +20,7 @@ from plane.app.serializers import (
ModuleIssueSerializer,
ModuleLinkSerializer,
ModuleFavoriteSerializer,
IssueStateSerializer,
IssueSerializer,
ModuleUserPropertiesSerializer,
)
from plane.app.permissions import ProjectEntityPermission, ProjectLitePermission
@ -33,6 +33,7 @@ from plane.db.models import (
ModuleFavorite,
IssueLink,
IssueAttachment,
IssueSubscriber,
ModuleUserProperties,
)
from plane.bgtasks.issue_activites_task import issue_activity
@ -353,6 +354,8 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet):
.prefetch_related("labels")
.order_by(order_by)
.filter(**filters)
.annotate(cycle_id=F("issue_cycle__cycle_id"))
.annotate(module_id=F("issue_module__module_id"))
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()
@ -365,8 +368,15 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet):
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
is_subscribed=Exists(
IssueSubscriber.objects.filter(
subscriber=self.request.user, issue_id=OuterRef("id")
)
)
)
)
serializer = IssueStateSerializer(
serializer = IssueSerializer(
issues, many=True, fields=fields if fields else None
)
return Response(serializer.data, status=status.HTTP_200_OK)
@ -447,8 +457,10 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet):
epoch=int(timezone.now().timestamp()),
)
issues = self.get_queryset().values_list("issue_id", flat=True)
return Response(
ModuleIssueSerializer(self.get_queryset(), many=True).data,
IssueSerializer(Issue.objects.filter(pk__in=issues), many=True).data,
status=status.HTTP_200_OK,
)

View File

@ -24,7 +24,7 @@ from . import BaseViewSet, BaseAPIView
from plane.app.serializers import (
GlobalViewSerializer,
IssueViewSerializer,
IssueLiteSerializer,
IssueSerializer,
IssueViewFavoriteSerializer,
)
from plane.app.permissions import (
@ -42,6 +42,7 @@ from plane.db.models import (
IssueReaction,
IssueLink,
IssueAttachment,
IssueSubscriber,
)
from plane.utils.issue_filters import issue_filters
from plane.utils.grouper import group_results
@ -127,6 +128,19 @@ class GlobalViewIssuesViewSet(BaseViewSet):
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
is_subscribed=Exists(
IssueSubscriber.objects.filter(
subscriber=self.request.user, issue_id=OuterRef("id")
)
)
)
)
# Priority Ordering
@ -185,7 +199,7 @@ class GlobalViewIssuesViewSet(BaseViewSet):
else:
issue_queryset = issue_queryset.order_by(order_by_param)
serializer = IssueLiteSerializer(
serializer = IssueSerializer(
issue_queryset, many=True, fields=fields if fields else None
)
return Response(serializer.data, status=status.HTTP_200_OK)

View File

@ -1,7 +1,13 @@
import { TIssue } from "./issues";
import { TIssue } from "./issues/base";
import type { IProjectLite } from "./projects";
export interface IInboxIssue extends TIssue {
export type TInboxIssueExtended = {
completed_at: string | null;
start_date: string | null;
target_date: string | null;
};
export interface IInboxIssue extends TIssue, TInboxIssueExtended {
issue_inbox: {
duplicate_to: string | null;
id: string;
@ -48,7 +54,12 @@ interface StatusDuplicate {
duplicate_to: string;
}
export type TInboxStatus = StatusReject | StatusSnoozed | StatusAccepted | StatusDuplicate | StatePending;
export type TInboxStatus =
| StatusReject
| StatusSnoozed
| StatusAccepted
| StatusDuplicate
| StatePending;
export interface IInboxFilterOptions {
priority?: string[] | null;

View File

@ -1,8 +1,6 @@
import { ReactElement } from "react";
import { KeyedMutator } from "swr";
import type {
IState,
IUser,
ICycle,
IModule,
IUserLite,
@ -12,6 +10,7 @@ import type {
Properties,
IIssueDisplayFilterOptions,
IIssueReaction,
TIssue,
} from "@plane/types";
export interface IIssueCycle {
@ -78,59 +77,6 @@ export interface IssueRelation {
relation: "blocking" | null;
}
export interface IIssue {
archived_at: string;
assignees: string[];
assignee_details: IUser[];
attachment_count: number;
attachments: any[];
issue_relations: IssueRelation[];
issue_reactions: IIssueReaction[];
related_issues: IssueRelation[];
bridge_id?: string | null;
completed_at: Date;
created_at: string;
created_by: string;
cycle: string | null;
cycle_id: string | null;
cycle_detail: ICycle | null;
description: any;
description_html: any;
description_stripped: any;
estimate_point: number | null;
id: string;
// tempId is used for optimistic updates. It is not a part of the API response.
tempId?: string;
issue_cycle: IIssueCycle | null;
issue_link: ILinkDetails[];
issue_module: IIssueModule | null;
labels: string[];
label_details: any[];
is_draft: boolean;
links_list: IIssueLink[];
link_count: number;
module: string | null;
module_id: string | null;
name: string;
parent: string | null;
parent_detail: IIssueParent | null;
priority: TIssuePriorities;
project: string;
project_detail: IProjectLite;
sequence_id: number;
sort_order: number;
sprints: string | null;
start_date: string | null;
state: string;
state_detail: IState;
sub_issues_count: number;
target_date: string | null;
updated_at: string;
updated_by: string;
workspace: string;
workspace_detail: IWorkspaceLite;
}
export interface ISubIssuesState {
backlog: number;
unstarted: number;
@ -283,62 +229,3 @@ export interface IGroupByColumn {
export interface IIssueMap {
[key: string]: TIssue;
}
// new issue structure types
export type TIssue = {
id: string;
name: string;
state_id: string;
description_html: string;
sort_order: number;
completed_at: string | null;
estimate_point: number | null;
priority: TIssuePriorities;
start_date: string | null;
target_date: string | null;
sequence_id: number;
project_id: string;
parent_id: string | null;
cycle_id: string | null;
module_id: string | null;
label_ids: string[];
assignee_ids: string[];
sub_issues_count: number;
created_at: string;
updated_at: string;
created_by: string;
updated_by: string;
attachment_count: number;
link_count: number;
is_subscribed: boolean;
archived_at: boolean;
is_draft: boolean;
// tempId is used for optimistic updates. It is not a part of the API response.
tempId?: string;
// issue details
related_issues: any;
issue_reactions: any;
issue_relations: any;
issue_cycle: any;
issue_module: any;
parent_detail: any;
issue_link: any;
};
export type TIssueMap = {
[issue_id: string]: TIssue;
};
export type TLoader = "init-loader" | "mutation" | undefined;
export type TGroupedIssues = {
[group_id: string]: string[];
};
export type TSubGroupedIssues = {
[sub_grouped_id: string]: {
[group_id: string]: string[];
};
};
export type TUnGroupedIssues = string[];

View File

@ -1,32 +1,41 @@
import { TIssuePriorities } from "../issues";
// new issue structure types
export type TIssue = {
id: string;
sequence_id: number;
name: string;
state_id: string;
description_html: string;
sort_order: number;
completed_at: string | null;
estimate_point: number | null;
state_id: string;
priority: TIssuePriorities;
start_date: string;
target_date: string;
sequence_id: number;
label_ids: string[];
assignee_ids: string[];
estimate_point: number | null;
sub_issues_count: number;
attachment_count: number;
link_count: number;
project_id: string;
parent_id: string | null;
cycle_id: string | null;
module_id: string | null;
label_ids: string[];
assignee_ids: string[];
sub_issues_count: number;
created_at: string;
updated_at: string;
start_date: string | null;
target_date: string | null;
completed_at: string | null;
archived_at: string | null;
created_by: string;
updated_by: string;
attachment_count: number;
link_count: number;
is_subscribed: boolean;
archived_at: boolean;
is_draft: boolean;
is_subscribed: boolean;
// tempId is used for optimistic updates. It is not a part of the API response.
tempId?: string;
};

View File

@ -0,0 +1,27 @@
import * as React from "react";
export type TControlLink = React.AnchorHTMLAttributes<HTMLAnchorElement> & {
href: string;
onClick: () => void;
children: React.ReactNode;
target?: string;
};
export const ControlLink: React.FC<TControlLink> = (props) => {
const { href, onClick, children, target = "_self", ...rest } = props;
const LEFT_CLICK_EVENT_CODE = 0;
const _onClick = (event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
const clickCondition = (event.metaKey || event.ctrlKey) && event.button === LEFT_CLICK_EVENT_CODE;
if (!clickCondition) {
event.preventDefault();
onClick();
}
};
return (
<a href={href} target={target} onClick={_onClick} {...rest}>
{children}
</a>
);
};

View File

@ -0,0 +1 @@
export * from "./control-link";

View File

@ -9,3 +9,4 @@ export * from "./progress";
export * from "./spinners";
export * from "./tooltip";
export * from "./loader";
export * from "./control-link";

View File

@ -116,7 +116,8 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
if (!cycleDetails) return null;
// computed
const cycleStatus = cycleDetails.status.toLocaleLowerCase() as TCycleGroups;
// TODO: change this logic once backend fix the response
const cycleStatus = cycleDetails.status ? (cycleDetails.status.toLocaleLowerCase() as TCycleGroups) : "draft";
const isCompleted = cycleStatus === "completed";
const endDate = new Date(cycleDetails.end_date ?? "");
const startDate = new Date(cycleDetails.start_date ?? "");

View File

@ -22,15 +22,15 @@ export const IssueAttachmentRoot: FC<TIssueAttachmentRoot> = (props) => {
const {
router: { workspaceSlug, projectId },
} = useApplication();
const { issueId, createAttachment, removeAttachment } = useIssueDetail();
const { peekIssue, 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);
if (!workspaceSlug || !projectId || !peekIssue?.issueId) throw new Error("Missing required fields");
await createAttachment(workspaceSlug, projectId, peekIssue?.issueId, data);
setToastAlert({
message: "The attachment has been successfully uploaded",
type: "success",
@ -46,8 +46,8 @@ export const IssueAttachmentRoot: FC<TIssueAttachmentRoot> = (props) => {
},
remove: async (attachmentId: string) => {
try {
if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing required fields");
await removeAttachment(workspaceSlug, projectId, issueId, attachmentId);
if (!workspaceSlug || !projectId || !peekIssue?.issueId) throw new Error("Missing required fields");
await removeAttachment(workspaceSlug, projectId, peekIssue?.issueId, attachmentId);
setToastAlert({
message: "The attachment has been successfully removed",
type: "success",
@ -62,7 +62,7 @@ export const IssueAttachmentRoot: FC<TIssueAttachmentRoot> = (props) => {
}
},
}),
[workspaceSlug, projectId, issueId, createAttachment, removeAttachment, setToastAlert]
[workspaceSlug, projectId, peekIssue, createAttachment, removeAttachment, setToastAlert]
);
return (

View File

@ -3,7 +3,7 @@ import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import { DragDropContext, DropResult } from "@hello-pangea/dnd";
// components
import { CalendarChart, IssuePeekOverview } from "components/issues";
import { CalendarChart } from "components/issues";
// hooks
import useToast from "hooks/use-toast";
// types
@ -34,7 +34,7 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => {
// router
const router = useRouter();
const { workspaceSlug, projectId, peekIssueId, peekProjectId } = router.query;
const { workspaceSlug, projectId } = router.query;
// hooks
const { setToastAlert } = useToast();
@ -113,16 +113,6 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => {
/>
</DragDropContext>
</div>
{workspaceSlug && peekIssueId && peekProjectId && (
<IssuePeekOverview
workspaceSlug={workspaceSlug.toString()}
projectId={peekProjectId.toString()}
issueId={peekIssueId.toString()}
handleIssue={async (issueToUpdate) =>
await handleIssues(issueToUpdate.target_date ?? "", issueToUpdate as TIssue, EIssueActions.UPDATE)
}
/>
)}
</>
);
});

View File

@ -97,7 +97,7 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
formKey="target_date"
groupId={formattedDatePayload}
prePopulatedData={{
target_date: renderFormattedPayloadDate(date.date),
target_date: renderFormattedPayloadDate(date.date) ?? undefined,
}}
quickAddCallback={quickAddCallback}
viewId={viewId}

View File

@ -110,11 +110,11 @@ export const CalendarQuickAddIssueForm: React.FC<Props> = observer((props) => {
}, [errors, setToastAlert]);
const onSubmitHandler = async (formData: TIssue) => {
if (isSubmitting || !groupId || !workspaceDetail || !projectDetail || !workspaceSlug || !projectId) return;
if (isSubmitting || !workspaceSlug || !projectId) return;
reset({ ...defaultValues });
const payload = createIssuePayload(workspaceDetail, projectDetail, {
const payload = createIssuePayload(projectId.toString(), {
...(prePopulatedData ?? {}),
...formData,
});

View File

@ -1,10 +1,10 @@
import React, { useCallback } from "react";
import React from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
// hooks
import { useIssues, useUser } from "hooks/store";
// components
import { IssueGanttBlock, IssuePeekOverview } from "components/issues";
import { IssueGanttBlock } from "components/issues";
import {
GanttChartRoot,
IBlockUpdateData,
@ -32,10 +32,10 @@ interface IBaseGanttRoot {
}
export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGanttRoot) => {
const { issueFiltersStore, issueStore, viewId, issueActions } = props;
const { issueFiltersStore, issueStore, viewId } = props;
// router
const router = useRouter();
const { workspaceSlug, peekIssueId, peekProjectId } = router.query;
const { workspaceSlug } = router.query;
// store hooks
const {
membership: { currentProjectRole },
@ -57,14 +57,6 @@ export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGan
await issueStore.updateIssue(workspaceSlug.toString(), issue.project_id, issue.id, payload, viewId);
};
const handleIssues = useCallback(
async (issue: TIssue, action: EIssueActions) => {
if (issueActions[action]) {
await issueActions[action]!(issue);
}
},
[issueActions]
);
const isAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
return (
@ -92,16 +84,6 @@ export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGan
enableReorder={appliedDisplayFilters?.order_by === "sort_order" && isAllowed}
/>
</div>
{workspaceSlug && peekIssueId && peekProjectId && (
<IssuePeekOverview
workspaceSlug={workspaceSlug.toString()}
projectId={peekProjectId.toString()}
issueId={peekIssueId.toString()}
handleIssue={async (issueToUpdate, action) => {
await handleIssues(issueToUpdate as TIssue, action);
}}
/>
)}
</>
);
});

View File

@ -104,14 +104,11 @@ export const GanttInlineCreateIssueForm: React.FC<Props> = observer((props) => {
const onSubmitHandler = async (formData: TIssue) => {
if (isSubmitting || !workspaceSlug || !projectId) return;
// resetting the form so that user can add another issue quickly
reset({ ...defaultValues, ...(prePopulatedData ?? {}) });
reset({ ...defaultValues });
const payload = createIssuePayload(workspaceDetail!, currentProjectDetails!, {
const payload = createIssuePayload(projectId.toString(), {
...(prePopulatedData ?? {}),
...formData,
start_date: renderFormattedPayloadDate(new Date()),
target_date: renderFormattedPayloadDate(new Date(new Date().getTime() + 24 * 60 * 60 * 1000)),
});
try {

View File

@ -276,14 +276,14 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
</DragDropContext>
</div>
{workspaceSlug && peekIssueId && peekProjectId && (
{/* {workspaceSlug && peekIssueId && peekProjectId && (
<IssuePeekOverview
workspaceSlug={workspaceSlug.toString()}
projectId={peekProjectId.toString()}
issueId={peekIssueId.toString()}
handleIssue={async (issueToUpdate) => await handleIssues(issueToUpdate as TIssue, EIssueActions.UPDATE)}
/>
)}
)} */}
</>
);
});

View File

@ -79,6 +79,8 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
const verticalAlignPosition = (_list: IGroupByColumn) => kanBanToggle?.groupByHeaderMinMax.includes(_list.id);
const isGroupByCreatedBy = group_by === "created_by";
return (
<div className="relative flex h-full w-full gap-3">
{list &&
@ -100,7 +102,7 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
issuePayload={_list.payload}
disableIssueCreation={disableIssueCreation}
disableIssueCreation={disableIssueCreation || isGroupByCreatedBy}
currentStore={currentStore}
addIssuesToView={addIssuesToView}
/>

View File

@ -9,6 +9,8 @@ import {
TUnGroupedIssues,
} from "@plane/types";
import { EIssueActions } from "../types";
// hooks
import { useProjectState } from "hooks/store";
//components
import { KanBanQuickAddIssueForm, KanbanIssueBlocksList } from ".";
@ -56,6 +58,33 @@ export const KanbanGroup = (props: IKanbanGroup) => {
viewId,
} = props;
const projectState = useProjectState();
const prePopulateQuickAddData = (groupByKey: string | null, value: string) => {
const defaultState = projectState.projectStates?.find((state) => state.default);
let preloadedData: object = { state_id: defaultState?.id };
if (groupByKey) {
if (groupByKey === "state") {
preloadedData = { ...preloadedData, state_id: value };
} else if (groupByKey === "priority") {
preloadedData = { ...preloadedData, priority: value };
} else if (groupByKey === "labels" && value != "None") {
preloadedData = { ...preloadedData, label_ids: [value] };
} else if (groupByKey === "assignees" && value != "None") {
preloadedData = { ...preloadedData, assignee_ids: [value] };
} else if (groupByKey === "created_by") {
preloadedData = { ...preloadedData };
} else {
preloadedData = { ...preloadedData, [groupByKey]: value };
}
}
return preloadedData;
};
const isGroupByCreatedBy = group_by === "created_by";
return (
<div className={`${verticalPosition ? `min-h-[150px] w-[0px] overflow-hidden` : `w-full transition-all`}`}>
<Droppable droppableId={`${groupId}__${sub_group_id}`}>
@ -87,13 +116,13 @@ export const KanbanGroup = (props: IKanbanGroup) => {
</Droppable>
<div className="sticky bottom-0 z-[0] w-full flex-shrink-0 bg-custom-background-90 py-1">
{enableQuickIssueCreate && !disableIssueCreation && (
{enableQuickIssueCreate && !disableIssueCreation && !isGroupByCreatedBy && (
<KanBanQuickAddIssueForm
formKey="name"
groupId={groupId}
subGroupId={sub_group_id}
prePopulatedData={{
...(group_by && { [group_by]: groupId }),
...(group_by && prePopulateQuickAddData(group_by, groupId)),
...(sub_group_by && sub_group_id !== "null" && { [sub_group_by]: sub_group_id }),
}}
quickAddCallback={quickAddCallback}

View File

@ -4,7 +4,7 @@ import { useForm } from "react-hook-form";
import { observer } from "mobx-react-lite";
import { PlusIcon } from "lucide-react";
// hooks
import { useProject, useWorkspace } from "hooks/store";
import { useProject } from "hooks/store";
import useToast from "hooks/use-toast";
import useKeypress from "hooks/use-keypress";
import useOutsideClickDetector from "hooks/use-outside-click-detector";
@ -59,10 +59,8 @@ export const KanBanQuickAddIssueForm: React.FC<IKanBanQuickAddIssueForm> = obser
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
// store hooks
const { getWorkspaceBySlug } = useWorkspace();
const { getProjectById } = useProject();
const workspaceDetail = workspaceSlug ? getWorkspaceBySlug(workspaceSlug.toString()) : null;
const projectDetail = projectId ? getProjectById(projectId.toString()) : null;
const ref = useRef<HTMLFormElement>(null);
@ -87,11 +85,11 @@ export const KanBanQuickAddIssueForm: React.FC<IKanBanQuickAddIssueForm> = obser
}, [isOpen, reset]);
const onSubmitHandler = async (formData: TIssue) => {
if (isSubmitting || !groupId || !workspaceDetail || !projectDetail || !workspaceSlug || !projectId) return;
if (isSubmitting || !workspaceSlug || !projectId) return;
reset({ ...defaultValues });
const payload = createIssuePayload(workspaceDetail, projectDetail, {
const payload = createIssuePayload(projectId.toString(), {
...(prePopulatedData ?? {}),
...formData,
});
@ -143,33 +141,6 @@ export const KanBanQuickAddIssueForm: React.FC<IKanBanQuickAddIssueForm> = obser
<span className="text-sm font-medium text-custom-primary-100">New Issue</span>
</div>
)}
{/* {isOpen && (
<form
ref={ref}
onSubmit={handleSubmit(onSubmitHandler)}
className="flex flex-col border-[0.5px] border-custom-border-100 justify-between gap-1.5 group/card relative select-none px-3.5 py-3 h-[118px] mb-3 mx-1.5 rounded bg-custom-background-300 shadow-custom-shadow-sm"
>
<Inputs register={register} setFocus={setFocus} projectDetails={projectDetails} />
</form>
)}
{isOpen && (
<p className="text-xs ml-3 italic mb-2 text-custom-text-200">
Press {"'"}Enter{"'"} to add another issue
</p>
)}
{!isOpen && (
<button
type="button"
className="flex items-center gap-x-[6px] text-custom-primary-100 px-2 py-3 rounded-md"
onClick={() => setIsOpen(true)}
>
<PlusIcon className="h-3.5 w-3.5 stroke-2" />
<span className="text-sm font-medium text-custom-primary-100">New Issue</span>
</button>
)} */}
</div>
);
});

View File

@ -1,13 +1,13 @@
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
// components
import { IssueProperties } from "../properties/all-properties";
// hooks
import { useApplication, useIssueDetail, useProject } from "hooks/store";
// ui
import { Spinner, Tooltip } from "@plane/ui";
import { Spinner, Tooltip, ControlLink } from "@plane/ui";
// types
import { TIssue, IIssueDisplayProperties, TIssueMap } from "@plane/types";
import { EIssueActions } from "../types";
import { useProject } from "hooks/store";
interface IssueBlockProps {
issueId: string;
@ -20,27 +20,29 @@ interface IssueBlockProps {
export const IssueBlock: React.FC<IssueBlockProps> = observer((props: IssueBlockProps) => {
const { issuesMap, issueId, handleIssues, quickActions, displayProperties, canEditProperties } = props;
// router
const router = useRouter();
// hooks
const {
router: { workspaceSlug, projectId },
} = useApplication();
const { getProjectById } = useProject();
const { setPeekIssue } = useIssueDetail();
const updateIssue = (issueToUpdate: TIssue) => {
handleIssues(issueToUpdate, EIssueActions.UPDATE);
};
const handleIssuePeekOverview = (issue: TIssue) =>
workspaceSlug &&
issue &&
issue.project_id &&
issue.id &&
setPeekIssue({ workspaceSlug, projectId: issue.project_id, issueId: issue.id });
const issue = issuesMap[issueId];
if (!issue) return null;
const handleIssuePeekOverview = () => {
const { query } = router;
router.push({
pathname: router.pathname,
query: { ...query, peekIssueId: issue?.id, peekProjectId: issue?.project_id },
});
};
const canEditIssueProperties = canEditProperties(issue.project_id);
const { getProjectById } = useProject();
const projectDetails = getProjectById(issue.project_id);
return (
@ -55,14 +57,17 @@ export const IssueBlock: React.FC<IssueBlockProps> = observer((props: IssueBlock
{issue?.tempId !== undefined && (
<div className="absolute left-0 top-0 z-[99999] h-full w-full animate-pulse bg-custom-background-100/20" />
)}
<Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
<div
className="line-clamp-1 w-full cursor-pointer text-sm font-medium text-custom-text-100"
onClick={handleIssuePeekOverview}
>
{issue.name}
</div>
</Tooltip>
<ControlLink
href={`/${workspaceSlug}/projects/${projectId}/issues/${issueId}`}
target="_blank"
onClick={() => handleIssuePeekOverview(issue)}
className="w-full line-clamp-1 cursor-pointer text-sm font-medium text-custom-text-100"
>
<Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
<span>{issue.name}</span>
</Tooltip>
</ControlLink>
<div className="ml-auto flex flex-shrink-0 items-center gap-2">
{!issue?.tempId ? (

View File

@ -21,7 +21,6 @@ export interface IGroupByList {
issueIds: TGroupedIssues | TUnGroupedIssues | any;
issuesMap: TIssueMap;
group_by: string | null;
is_list?: boolean;
handleIssues: (issue: TIssue, action: EIssueActions) => Promise<void>;
quickActions: (issue: TIssue) => React.ReactNode;
displayProperties: IIssueDisplayProperties | undefined;
@ -45,7 +44,6 @@ const GroupByList: React.FC<IGroupByList> = (props) => {
issueIds,
issuesMap,
group_by,
is_list = false,
handleIssues,
quickActions,
displayProperties,
@ -70,11 +68,27 @@ const GroupByList: React.FC<IGroupByList> = (props) => {
const prePopulateQuickAddData = (groupByKey: string | null, value: any) => {
const defaultState = projectState.projectStates?.find((state) => state.default);
if (groupByKey === null) return { state_id: defaultState?.id };
else {
if (groupByKey === "state") return { state: groupByKey === "state" ? value : defaultState?.id };
else return { state_id: defaultState?.id, [groupByKey]: value };
let preloadedData: object = { state_id: defaultState?.id };
if (groupByKey === null) {
preloadedData = { ...preloadedData };
} else {
if (groupByKey === "state") {
preloadedData = { ...preloadedData, state_id: value };
} else if (groupByKey === "priority") {
preloadedData = { ...preloadedData, priority: value };
} else if (groupByKey === "labels" && value != "None") {
preloadedData = { ...preloadedData, label_ids: [value] };
} else if (groupByKey === "assignees" && value != "None") {
preloadedData = { ...preloadedData, assignee_ids: [value] };
} else if (groupByKey === "created_by") {
preloadedData = { ...preloadedData };
} else {
preloadedData = { ...preloadedData, [groupByKey]: value };
}
}
return preloadedData;
};
const validateEmptyIssueGroups = (issues: TIssue[]) => {
@ -83,6 +97,10 @@ const GroupByList: React.FC<IGroupByList> = (props) => {
return true;
};
const is_list = group_by === null ? true : false;
const isGroupByCreatedBy = group_by === "created_by";
return (
<div className="relative h-full w-full">
{list &&
@ -97,7 +115,7 @@ const GroupByList: React.FC<IGroupByList> = (props) => {
title={_list.name || ""}
count={is_list ? issueIds?.length || 0 : issueIds?.[_list.id]?.length || 0}
issuePayload={_list.payload}
disableIssueCreation={disableIssueCreation}
disableIssueCreation={disableIssueCreation || isGroupByCreatedBy}
currentStore={currentStore}
addIssuesToView={addIssuesToView}
/>
@ -114,7 +132,7 @@ const GroupByList: React.FC<IGroupByList> = (props) => {
/>
)}
{enableIssueQuickAdd && !disableIssueCreation && (
{enableIssueQuickAdd && !disableIssueCreation && !isGroupByCreatedBy && (
<div className="sticky bottom-0 z-[1] w-full flex-shrink-0">
<ListQuickAddIssueForm
prePopulatedData={prePopulateQuickAddData(group_by, _list.id)}

View File

@ -62,9 +62,10 @@ export const ListQuickAddIssueForm: FC<IListQuickAddIssueForm> = observer((props
// router
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
// store hooks
const { currentWorkspace } = useWorkspace();
const { currentProjectDetails } = useProject();
// hooks
const { getProjectById } = useProject();
const projectDetail = (projectId && getProjectById(projectId.toString())) || undefined;
const ref = useRef<HTMLFormElement>(null);
@ -88,11 +89,11 @@ export const ListQuickAddIssueForm: FC<IListQuickAddIssueForm> = observer((props
}, [isOpen, reset]);
const onSubmitHandler = async (formData: TIssue) => {
if (isSubmitting || !currentWorkspace || !currentProjectDetails || !workspaceSlug || !projectId) return;
if (isSubmitting || !workspaceSlug || !projectId) return;
reset({ ...defaultValues });
const payload = createIssuePayload(currentWorkspace, currentProjectDetails, {
const payload = createIssuePayload(projectId.toString(), {
...(prePopulatedData ?? {}),
...formData,
});
@ -127,12 +128,7 @@ export const ListQuickAddIssueForm: FC<IListQuickAddIssueForm> = observer((props
onSubmit={handleSubmit(onSubmitHandler)}
className="flex w-full items-center gap-x-3 border-[0.5px] border-t-0 border-custom-border-100 bg-custom-background-100 px-3"
>
<Inputs
formKey={"name"}
register={register}
setFocus={setFocus}
projectDetail={currentProjectDetails ?? null}
/>
<Inputs formKey={"name"} register={register} setFocus={setFocus} projectDetail={projectDetail ?? null} />
</form>
<div className="px-3 py-2 text-xs italic text-custom-text-200">{`Press 'Enter' to add another issue`}</div>
</div>

View File

@ -141,8 +141,8 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
onChange={handleAssignee}
disabled={isReadOnly}
multiple
buttonVariant={issue.assignee_ids.length > 0 ? "transparent-without-text" : "border-without-text"}
buttonClassName={issue.assignee_ids.length > 0 ? "hover:bg-transparent px-0" : ""}
buttonVariant={issue.assignee_ids?.length > 0 ? "transparent-without-text" : "border-without-text"}
buttonClassName={issue.assignee_ids?.length > 0 ? "hover:bg-transparent px-0" : ""}
/>
</div>
</WithDisplayPropertiesHOC>

View File

@ -1,4 +1,4 @@
import { useRouter } from "next/router";
import { FC } from "react";
import { observer } from "mobx-react-lite";
import useSWR from "swr";
// components
@ -10,21 +10,24 @@ import {
ProjectAppliedFiltersRoot,
ProjectSpreadsheetLayout,
ProjectEmptyState,
IssuePeekOverview,
} from "components/issues";
// ui
import { Spinner } from "@plane/ui";
import { useIssues } from "hooks/store/use-issues";
import { EIssuesStoreType } from "constants/issue";
// hooks
import { useApplication, useIssues } from "hooks/store";
// constants
import { EIssuesStoreType } from "constants/issue";
export const ProjectLayoutRoot: React.FC = observer(() => {
// router
const router = useRouter();
const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string };
export const ProjectLayoutRoot: FC = observer(() => {
// hooks
const {
router: { workspaceSlug, projectId },
} = useApplication();
const { issues, issuesFilter } = useIssues(EIssuesStoreType.PROJECT);
useSWR(
workspaceSlug && projectId ? `PROJECT_ISSUES_V3_${workspaceSlug}_${projectId}` : null,
workspaceSlug && projectId ? `PROJECT_ISSUES_${workspaceSlug}_${projectId}` : null,
async () => {
if (workspaceSlug && projectId) {
await issuesFilter?.fetchFilters(workspaceSlug, projectId);
@ -40,28 +43,35 @@ export const ProjectLayoutRoot: React.FC = observer(() => {
<div className="relative flex h-full w-full flex-col overflow-hidden">
<ProjectAppliedFiltersRoot />
{issues?.loader === "init-loader" || !issues?.groupedIssueIds ? (
{issues?.loader === "init-loader" ? (
<div className="flex h-full w-full items-center justify-center">
<Spinner />
</div>
) : (
<>
{(issues?.groupedIssueIds ?? {}).length == 0 ? (
<ProjectEmptyState />
) : (
<div className="relative h-full w-full overflow-auto bg-custom-background-90">
{activeLayout === "list" ? (
<ListLayout />
) : activeLayout === "kanban" ? (
<KanBanLayout />
) : activeLayout === "calendar" ? (
<CalendarLayout />
) : activeLayout === "gantt_chart" ? (
<GanttLayout />
) : activeLayout === "spreadsheet" ? (
<ProjectSpreadsheetLayout />
) : null}
{!issues?.groupedIssueIds ? (
<div className="relative h-full w-full overflow-y-auto">
<ProjectEmptyState />
</div>
) : (
<>
<div className="relative h-full w-full overflow-auto bg-custom-background-90">
{activeLayout === "list" ? (
<ListLayout />
) : activeLayout === "kanban" ? (
<KanBanLayout />
) : activeLayout === "calendar" ? (
<CalendarLayout />
) : activeLayout === "gantt_chart" ? (
<GanttLayout />
) : activeLayout === "spreadsheet" ? (
<ProjectSpreadsheetLayout />
) : null}
</div>
{/* peek overview */}
<IssuePeekOverview />
</>
)}
</>
)}

View File

@ -29,12 +29,15 @@ export const ProjectViewLayoutRoot: React.FC = observer(() => {
issuesFilter: { issueFilters, fetchFilters },
} = useIssues(EIssuesStoreType.PROJECT_VIEW);
useSWR(workspaceSlug && projectId && viewId ? `PROJECT_ISSUES_V3_${workspaceSlug}_${projectId}` : null, async () => {
if (workspaceSlug && projectId && viewId) {
await fetchFilters(workspaceSlug, projectId, viewId);
await fetchIssues(workspaceSlug, projectId, groupedIssueIds ? "mutation" : "init-loader");
useSWR(
workspaceSlug && projectId && viewId ? `PROJECT_VIEW_ISSUES_${workspaceSlug}_${projectId}` : null,
async () => {
if (workspaceSlug && projectId && viewId) {
await fetchFilters(workspaceSlug, projectId, viewId);
await fetchIssues(workspaceSlug, projectId, groupedIssueIds ? "mutation" : "init-loader");
}
}
});
);
const activeLayout = issueFilters?.displayFilters?.layout;

View File

@ -32,7 +32,7 @@ export const SpreadsheetAssigneeColumn: React.FC<Props> = ({ issueId, onChange,
disabled={disabled}
multiple
placeholder="Assignees"
buttonVariant={issueDetail.assignee_ids.length > 0 ? "transparent-without-text" : "transparent-with-text"}
buttonVariant={issueDetail.assignee_ids?.length > 0 ? "transparent-without-text" : "transparent-with-text"}
buttonClassName="text-left"
buttonContainerClassName="w-full"
/>

View File

@ -1,4 +1,5 @@
import { useEffect, useState, useRef } from "react";
import { useRouter } from "next/router";
import { useForm } from "react-hook-form";
import { observer } from "mobx-react-lite";
import { PlusIcon } from "lucide-react";
@ -55,6 +56,10 @@ const Inputs = (props: any) => {
export const SpreadsheetQuickAddIssueForm: React.FC<Props> = observer((props) => {
const { formKey, prePopulatedData, quickAddCallback, viewId } = props;
// router
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
// store hooks
const { currentWorkspace } = useWorkspace();
const { currentProjectDetails } = useProject();
@ -148,7 +153,7 @@ export const SpreadsheetQuickAddIssueForm: React.FC<Props> = observer((props) =>
reset({ ...defaultValues });
const payload = createIssuePayload(currentWorkspace, currentProjectDetails, {
const payload = createIssuePayload(currentProjectDetails.id, {
...(prePopulatedData ?? {}),
...formData,
});

View File

@ -1,13 +1,7 @@
import React, { useEffect, useRef, useState } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
// components
import {
IssuePeekOverview,
SpreadsheetColumnsList,
SpreadsheetIssuesColumn,
SpreadsheetQuickAddIssueForm,
} from "components/issues";
import { SpreadsheetColumnsList, SpreadsheetIssuesColumn, SpreadsheetQuickAddIssueForm } from "components/issues";
import { Spinner, LayersIcon } from "@plane/ui";
// types
import { TIssue, IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueLabel, IState } from "@plane/types";
@ -56,9 +50,6 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
const [isScrolled, setIsScrolled] = useState(false);
// refs
const containerRef = useRef<HTMLDivElement | null>(null);
// router
const router = useRouter();
const { workspaceSlug, peekIssueId, peekProjectId } = router.query;
const handleScroll = () => {
if (!containerRef.current) return;
@ -186,14 +177,6 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
))} */}
</div>
</div>
{workspaceSlug && peekIssueId && peekProjectId && (
<IssuePeekOverview
workspaceSlug={workspaceSlug.toString()}
projectId={peekProjectId.toString()}
issueId={peekIssueId.toString()}
handleIssue={async (issueToUpdate: any) => await handleIssues(issueToUpdate, EIssueActions.UPDATE)}
/>
)}
</div>
);
});

View File

@ -49,7 +49,7 @@ const getProjectColumns = (project: IProjectStore): IGroupByColumn[] | undefined
id: project.id,
name: project.name,
Icon: <div className="w-6 h-6">{renderEmoji(project.emoji || "")}</div>,
payload: { project: project.id },
payload: { project_id: project.id },
};
}) as any;
};
@ -66,7 +66,7 @@ const getStateColumns = (projectState: IStateStore): IGroupByColumn[] | undefine
<StateGroupIcon stateGroup={state.group} color={state.color} width="14" height="14" />
</div>
),
payload: { state: state.id },
payload: { state_id: state.id },
})) as any;
};
@ -111,7 +111,7 @@ const getLabelsColumns = (projectLabel: ILabelRootStore) => {
Icon: (
<div className="w-[12px] h-[12px] rounded-full" style={{ backgroundColor: label.color ? label.color : "#666" }} />
),
payload: { labels: [label.id] },
payload: label?.id === "None" ? {} : { label_ids: [label.id] },
}));
};
@ -123,17 +123,17 @@ const getAssigneeColumns = (member: IMemberRootStore) => {
if (!projectMemberIds) return;
const assigneeColumns = projectMemberIds.map((memberId) => {
const assigneeColumns: any = projectMemberIds.map((memberId) => {
const member = getUserDetails(memberId);
return {
id: memberId,
name: member?.display_name || "",
Icon: <Avatar name={member?.display_name} src={member?.avatar} size="md" />,
payload: { assignees: [memberId] },
payload: { assignee_ids: [memberId] },
};
});
assigneeColumns.push({ id: "None", name: "None", Icon: <Avatar size="md" />, payload: { assignees: [""] } });
assigneeColumns.push({ id: "None", name: "None", Icon: <Avatar size="md" />, payload: {} });
return assigneeColumns;
};
@ -152,7 +152,7 @@ const getCreatedByColumns = (member: IMemberRootStore) => {
id: memberId,
name: member?.display_name || "",
Icon: <Avatar name={member?.display_name} src={member?.avatar} size="md" />,
payload: { assignees: [memberId] },
payload: {},
};
});
};

View File

@ -1,5 +1,6 @@
import { FC, useState } from "react";
// hooks
import useToast from "hooks/use-toast";
import { useIssueDetail } from "hooks/store";
// ui
import { ExternalLinkIcon, Tooltip } from "@plane/ui";
@ -9,6 +10,7 @@ import { Pencil, Trash2, LinkIcon } from "lucide-react";
import { IssueLinkCreateUpdateModal, TLinkOperationsModal } from "./create-update-link-modal";
// helpers
import { calculateTimeAgo } from "helpers/date-time.helper";
import { copyTextToClipboard } from "helpers/string.helper";
export type TIssueLinkDetail = {
linkId: string;
@ -23,6 +25,8 @@ export const IssueLinkDetail: FC<TIssueLinkDetail> = (props) => {
const {
link: { getLinkById },
} = useIssueDetail();
const { setToastAlert } = useToast();
// state
const [isIssueLinkModalOpen, setIsIssueLinkModalOpen] = useState(false);
const toggleIssueLinkModal = (modalToggle: boolean) => setIsIssueLinkModalOpen(modalToggle);
@ -40,18 +44,23 @@ export const IssueLinkDetail: FC<TIssueLinkDetail> = (props) => {
/>
<div className="relative flex flex-col rounded-md bg-custom-background-90 p-2.5">
<div className="flex w-full items-start justify-between gap-2">
<div
className="flex w-full items-start justify-between gap-2 cursor-pointer"
onClick={() => {
copyTextToClipboard(linkDetail.url);
setToastAlert({
type: "success",
title: "Link copied!",
message: "Link copied to clipboard",
});
}}
>
<div className="flex items-start gap-2 truncate">
<span className="py-1">
<LinkIcon className="h-3 w-3 flex-shrink-0" />
</span>
<Tooltip tooltipContent={linkDetail.title && linkDetail.title !== "" ? linkDetail.title : linkDetail.url}>
<span
className="cursor-pointer truncate text-xs"
// onClick={() =>
// copyToClipboard(linkDetail.title && linkDetail.title !== "" ? linkDetail.title : linkDetail.url)
// }
>
<span className="truncate text-xs">
{linkDetail.title && linkDetail.title !== "" ? linkDetail.title : linkDetail.url}
</span>
</Tooltip>

View File

@ -27,7 +27,7 @@ export const IssueLinkRoot: FC<TIssueLinkRoot> = (props) => {
const {
router: { workspaceSlug, projectId },
} = useApplication();
const { issueId, createLink, updateLink, removeLink } = useIssueDetail();
const { peekIssue, createLink, updateLink, removeLink } = useIssueDetail();
// state
const [isIssueLinkModalOpen, setIsIssueLinkModalOpen] = useState(false);
const toggleIssueLinkModal = (modalToggle: boolean) => setIsIssueLinkModalOpen(modalToggle);
@ -38,8 +38,8 @@ export const IssueLinkRoot: FC<TIssueLinkRoot> = (props) => {
() => ({
create: async (data: Partial<TIssueLink>) => {
try {
if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing required fields");
await createLink(workspaceSlug, projectId, issueId, data);
if (!workspaceSlug || !projectId || !peekIssue?.issueId) throw new Error("Missing required fields");
await createLink(workspaceSlug, projectId, peekIssue?.issueId, data);
setToastAlert({
message: "The link has been successfully created",
type: "success",
@ -56,8 +56,8 @@ export const IssueLinkRoot: FC<TIssueLinkRoot> = (props) => {
},
update: async (linkId: string, data: Partial<TIssueLink>) => {
try {
if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing required fields");
await updateLink(workspaceSlug, projectId, issueId, linkId, data);
if (!workspaceSlug || !projectId || !peekIssue?.issueId) throw new Error("Missing required fields");
await updateLink(workspaceSlug, projectId, peekIssue?.issueId, linkId, data);
setToastAlert({
message: "The link has been successfully updated",
type: "success",
@ -74,8 +74,8 @@ export const IssueLinkRoot: FC<TIssueLinkRoot> = (props) => {
},
remove: async (linkId: string) => {
try {
if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing required fields");
await removeLink(workspaceSlug, projectId, issueId, linkId);
if (!workspaceSlug || !projectId || !peekIssue?.issueId) throw new Error("Missing required fields");
await removeLink(workspaceSlug, projectId, peekIssue?.issueId, linkId);
setToastAlert({
message: "The link has been successfully removed",
type: "success",
@ -91,7 +91,7 @@ export const IssueLinkRoot: FC<TIssueLinkRoot> = (props) => {
}
},
}),
[workspaceSlug, projectId, issueId, createLink, updateLink, removeLink, setToastAlert]
[workspaceSlug, projectId, peekIssue, createLink, updateLink, removeLink, setToastAlert]
);
return (

View File

@ -6,11 +6,17 @@ import { CalendarDays, Link2, Plus, Signal, Tag, Triangle, LayoutPanelTop } from
import { useIssueDetail, useProject, useUser } from "hooks/store";
// ui icons
import { DiceIcon, DoubleCircleIcon, UserGroupIcon, ContrastIcon } from "@plane/ui";
import { SidebarCycleSelect, SidebarLabelSelect, SidebarModuleSelect, SidebarParentSelect } from "components/issues";
import {
IssueLinkRoot,
SidebarCycleSelect,
SidebarLabelSelect,
SidebarModuleSelect,
SidebarParentSelect,
} from "components/issues";
import { EstimateDropdown, PriorityDropdown, ProjectMemberDropdown, StateDropdown } from "components/dropdowns";
// components
import { CustomDatePicker } from "components/ui";
import { LinkModal, LinksList } from "components/core";
import { LinkModal } from "components/core";
// types
import { TIssue, TIssuePriorities, ILinkDetails, IIssueLink } from "@plane/types";
// constants
@ -39,6 +45,9 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const uneditable = currentProjectRole ? [5, 10].includes(currentProjectRole) : false;
const isAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
const handleState = (_state: string) => {
issueUpdate({ ...issue, state_id: _state });
};
@ -274,42 +283,8 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
<span className="border-t border-custom-border-200" />
<div className="flex w-full flex-col gap-5 pt-5">
<div className="flex w-full flex-col gap-2">
<div className="flex w-80 items-center gap-2">
<div className="flex w-40 items-center gap-2 text-sm">
<Link2 className="h-4 w-4 flex-shrink-0" />
<p>Links</p>
</div>
<div>
{!disableUserActions && (
<button
type="button"
className={`flex ${
disableUserActions ? "cursor-not-allowed" : "cursor-pointer hover:bg-custom-background-90"
} items-center gap-1 rounded-2xl border border-custom-border-100 px-2 py-0.5 text-xs text-custom-text-300 hover:text-custom-text-200`}
onClick={() => toggleIssueLinkModal(true)}
disabled={false}
>
<Plus className="h-3 w-3" /> New
</button>
)}
</div>
</div>
<div className="flex flex-col gap-3">
{issue?.issue_link && issue.issue_link.length > 0 ? (
<LinksList
links={issue.issue_link}
handleDeleteLink={issueLinkDelete}
handleEditLink={handleEditLink}
userAuth={{
isGuest: currentProjectRole === EUserProjectRoles.GUEST,
isViewer: currentProjectRole === EUserProjectRoles.VIEWER,
isMember: currentProjectRole === EUserProjectRoles.MEMBER,
isOwner: currentProjectRole === EUserProjectRoles.ADMIN,
}}
/>
) : null}
</div>
<div className="flex flex-col gap-3">
<IssueLinkRoot uneditable={uneditable} isAllowed={isAllowed} />
</div>
</div>
</div>

View File

@ -1,4 +1,5 @@
import { FC, Fragment, ReactNode, useCallback, useEffect } from "react";
import { FC, Fragment, useEffect, useState } from "react";
// router
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
// hooks
@ -13,33 +14,28 @@ import { TIssue, IIssueLink } from "@plane/types";
// constants
import { EUserProjectRoles } from "constants/project";
import { EIssuesStoreType } from "constants/issue";
import { EIssueActions } from "../issue-layouts/types";
interface IIssuePeekOverview {
workspaceSlug: string;
projectId: string;
issueId: string;
handleIssue: (issue: Partial<TIssue>, action: EIssueActions) => void;
isArchived?: boolean;
children?: ReactNode;
}
export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
const { workspaceSlug, projectId, issueId, handleIssue, children, isArchived = false } = props;
const { isArchived = false } = props;
// router
const router = useRouter();
const { peekIssueId } = router.query;
// FIXME
// store hooks
// const {
// archivedIssueDetail: {
// getIssue: getArchivedIssue,
// loader: archivedIssueLoader,
// fetchPeekIssueDetails: fetchArchivedPeekIssueDetails,
// },
// } = useMobxStore();
// hooks
const { currentProjectDetails } = useProject();
const { setToastAlert } = useToast();
const {
membership: { currentProjectRole },
} = useUser();
const {
issues: { removeIssue: removeArchivedIssue },
} = useIssues(EIssuesStoreType.ARCHIVED);
const {
peekIssue,
updateIssue,
removeIssue,
createComment,
updateComment,
removeComment,
@ -53,37 +49,38 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
updateLink,
removeLink,
issue: { getIssueById, fetchIssue },
// loader,
setIssueId,
fetchActivities,
} = useIssueDetail();
const {
issues: { removeIssue },
} = useIssues(EIssuesStoreType.ARCHIVED);
const {
membership: { currentProjectRole },
} = useUser();
const { currentProjectDetails } = useProject();
const { setToastAlert } = useToast();
const fetchIssueDetail = useCallback(async () => {
if (workspaceSlug && projectId && peekIssueId) {
//if (isArchived) await fetchArchivedPeekIssueDetails(workspaceSlug, projectId, peekIssueId as string);
//else
await fetchIssue(workspaceSlug, projectId, peekIssueId.toString());
}
}, [fetchIssue, workspaceSlug, projectId, peekIssueId]);
// state
const [loader, setLoader] = useState(false);
useEffect(() => {
fetchIssueDetail();
}, [workspaceSlug, projectId, peekIssueId, fetchIssueDetail]);
if (peekIssue) {
setLoader(true);
fetchIssue(peekIssue.workspaceSlug, peekIssue.projectId, peekIssue.issueId).finally(() => {
setLoader(false);
});
}
}, [peekIssue, fetchIssue]);
if (!peekIssue) return <></>;
const issue = getIssueById(peekIssue.issueId) || undefined;
const redirectToIssueDetail = () => {
router.push({
pathname: `/${peekIssue.workspaceSlug}/projects/${peekIssue.projectId}/${
isArchived ? "archived-issues" : "issues"
}/${peekIssue.issueId}`,
});
};
const handleCopyText = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
e.preventDefault();
copyUrlToClipboard(
`${workspaceSlug}/projects/${projectId}/${isArchived ? "archived-issues" : "issues"}/${peekIssueId}`
`${peekIssue.workspaceSlug}/projects/${peekIssue.projectId}/${isArchived ? "archived-issues" : "issues"}/${
peekIssue.issueId
}`
).then(() => {
setToastAlert({
type: "success",
@ -93,101 +90,81 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
});
};
const redirectToIssueDetail = () => {
router.push({
pathname: `/${workspaceSlug}/projects/${projectId}/${isArchived ? "archived-issues" : "issues"}/${issueId}`,
});
};
// const issue = isArchived ? getArchivedIssue : getIssue;
// const isLoading = isArchived ? archivedIssueLoader : loader;
const issue = getIssueById(issueId);
const isLoading = false;
const issueUpdate = async (_data: Partial<TIssue>) => {
if (handleIssue) {
await handleIssue(_data, EIssueActions.UPDATE);
fetchActivities(workspaceSlug, projectId, issueId);
}
if (!issue) return;
await updateIssue(peekIssue.workspaceSlug, peekIssue.projectId, peekIssue.issueId, _data);
fetchActivities(peekIssue.workspaceSlug, peekIssue.projectId, peekIssue.issueId);
};
const issueDelete = async () => {
if (!issue) return;
if (isArchived) await removeArchivedIssue(peekIssue.workspaceSlug, peekIssue.projectId, peekIssue.issueId);
else await removeIssue(peekIssue.workspaceSlug, peekIssue.projectId, peekIssue.issueId);
};
const issueReactionCreate = (reaction: string) => createReaction(workspaceSlug, projectId, issueId, reaction);
const issueReactionCreate = (reaction: string) =>
createReaction(peekIssue.workspaceSlug, peekIssue.projectId, peekIssue.issueId, reaction);
const issueReactionRemove = (reaction: string) =>
removeReaction(peekIssue.workspaceSlug, peekIssue.projectId, peekIssue.issueId, reaction);
const issueReactionRemove = (reaction: string) => removeReaction(workspaceSlug, projectId, issueId, reaction);
const issueCommentCreate = (comment: any) => createComment(workspaceSlug, projectId, issueId, comment);
const issueCommentUpdate = (comment: any) => updateComment(workspaceSlug, projectId, issueId, comment?.id, comment);
const issueCommentRemove = (commentId: string) => removeComment(workspaceSlug, projectId, issueId, commentId);
const issueCommentCreate = (comment: any) =>
createComment(peekIssue.workspaceSlug, peekIssue.projectId, peekIssue.issueId, comment);
const issueCommentUpdate = (comment: any) =>
updateComment(peekIssue.workspaceSlug, peekIssue.projectId, peekIssue.issueId, comment?.id, comment);
const issueCommentRemove = (commentId: string) =>
removeComment(peekIssue.workspaceSlug, peekIssue.projectId, peekIssue.issueId, commentId);
const issueCommentReactionCreate = (commentId: string, reaction: string) =>
createCommentReaction(workspaceSlug, projectId, commentId, reaction);
createCommentReaction(peekIssue.workspaceSlug, peekIssue.projectId, commentId, reaction);
const issueCommentReactionRemove = (commentId: string, reaction: string) =>
removeCommentReaction(workspaceSlug, projectId, commentId, reaction);
removeCommentReaction(peekIssue.workspaceSlug, peekIssue.projectId, commentId, reaction);
const issueSubscriptionCreate = () => createSubscription(workspaceSlug, projectId, issueId);
const issueSubscriptionRemove = () => removeSubscription(workspaceSlug, projectId, issueId);
const issueLinkCreate = (formData: IIssueLink) => createLink(workspaceSlug, projectId, issueId, formData);
const issueSubscriptionCreate = () =>
createSubscription(peekIssue.workspaceSlug, peekIssue.projectId, peekIssue.issueId);
const issueSubscriptionRemove = () =>
removeSubscription(peekIssue.workspaceSlug, peekIssue.projectId, peekIssue.issueId);
const issueLinkCreate = (formData: IIssueLink) =>
createLink(peekIssue.workspaceSlug, peekIssue.projectId, peekIssue.issueId, formData);
const issueLinkUpdate = (formData: IIssueLink, linkId: string) =>
updateLink(workspaceSlug, projectId, issueId, linkId, formData);
const issueLinkDelete = (linkId: string) => removeLink(workspaceSlug, projectId, issueId, linkId);
const handleDeleteIssue = async () => {
if (!issue) return;
if (isArchived) await removeIssue(workspaceSlug, projectId, issue?.id);
// FIXME else delete...
const { query } = router;
if (query.peekIssueId) {
setIssueId(undefined);
delete query.peekIssueId;
delete query.peekProjectId;
router.push({
pathname: router.pathname,
query: { ...query },
});
}
};
updateLink(peekIssue.workspaceSlug, peekIssue.projectId, peekIssue.issueId, linkId, formData);
const issueLinkDelete = (linkId: string) =>
removeLink(peekIssue.workspaceSlug, peekIssue.projectId, peekIssue.issueId, linkId);
const userRole = currentProjectRole ?? EUserProjectRoles.GUEST;
const isLoading = !issue || loader ? true : false;
return (
<Fragment>
<IssueView
workspaceSlug={workspaceSlug}
projectId={projectId}
issueId={issueId}
issue={issue}
isLoading={isLoading}
isArchived={isArchived}
handleCopyText={handleCopyText}
redirectToIssueDetail={redirectToIssueDetail}
issueUpdate={issueUpdate}
issueReactionCreate={issueReactionCreate}
issueReactionRemove={issueReactionRemove}
issueCommentCreate={issueCommentCreate}
issueCommentUpdate={issueCommentUpdate}
issueCommentRemove={issueCommentRemove}
issueCommentReactionCreate={issueCommentReactionCreate}
issueCommentReactionRemove={issueCommentReactionRemove}
issueSubscriptionCreate={issueSubscriptionCreate}
issueSubscriptionRemove={issueSubscriptionRemove}
issueLinkCreate={issueLinkCreate}
issueLinkUpdate={issueLinkUpdate}
issueLinkDelete={issueLinkDelete}
handleDeleteIssue={handleDeleteIssue}
disableUserActions={[5, 10].includes(userRole)}
showCommentAccessSpecifier={currentProjectDetails?.is_deployed}
>
{children}
</IssueView>
{isLoading ? (
<></> // TODO: show the spinner
) : (
<IssueView
workspaceSlug={peekIssue.workspaceSlug}
projectId={peekIssue.projectId}
issueId={peekIssue.issueId}
issue={issue}
isLoading={isLoading}
isArchived={isArchived}
handleCopyText={handleCopyText}
redirectToIssueDetail={redirectToIssueDetail}
issueUpdate={issueUpdate}
issueReactionCreate={issueReactionCreate}
issueReactionRemove={issueReactionRemove}
issueCommentCreate={issueCommentCreate}
issueCommentUpdate={issueCommentUpdate}
issueCommentRemove={issueCommentRemove}
issueCommentReactionCreate={issueCommentReactionCreate}
issueCommentReactionRemove={issueCommentReactionRemove}
issueSubscriptionCreate={issueSubscriptionCreate}
issueSubscriptionRemove={issueSubscriptionRemove}
issueLinkCreate={issueLinkCreate}
issueLinkUpdate={issueLinkUpdate}
issueLinkDelete={issueLinkDelete}
handleDeleteIssue={issueDelete}
disableUserActions={[5, 10].includes(userRole)}
showCommentAccessSpecifier={currentProjectDetails?.is_deployed}
/>
)}
</Fragment>
);
});

View File

@ -1,7 +1,5 @@
import { FC, ReactNode, useRef, useState } from "react";
import { useRouter } from "next/router";
import { FC, useRef, useState } from "react";
import { observer } from "mobx-react-lite";
import useSWR from "swr";
import { MoveRight, MoveDiagonal, Bell, Link2, Trash2 } from "lucide-react";
// hooks
import { useIssueDetail, useUser } from "hooks/store";
@ -43,7 +41,6 @@ interface IIssueView {
issueLinkUpdate: (formData: IIssueLink, linkId: string) => Promise<ILinkDetails>;
issueLinkDelete: (linkId: string) => Promise<void>;
handleDeleteIssue: () => Promise<void>;
children: ReactNode;
disableUserActions?: boolean;
showCommentAccessSpecifier?: boolean;
}
@ -92,7 +89,6 @@ export const IssueView: FC<IIssueView> = observer((props) => {
issueLinkUpdate,
issueLinkDelete,
handleDeleteIssue,
children,
disableUserActions = false,
showCommentAccessSpecifier = false,
} = props;
@ -101,58 +97,19 @@ export const IssueView: FC<IIssueView> = observer((props) => {
const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved");
// ref
const issuePeekOverviewRef = useRef<HTMLDivElement>(null);
// router
const router = useRouter();
const { peekIssueId } = router.query;
// store hooks
const {
fetchSubscriptions,
activity,
reaction,
subscription,
setIssueId,
setPeekIssue,
isAnyModalOpen,
isDeleteIssueModalOpen,
toggleDeleteIssueModal,
} = useIssueDetail();
const { currentUser } = useUser();
const updateRoutePeekId = () => {
if (issueId != peekIssueId) {
setIssueId(issueId);
const { query } = router;
router.push({
pathname: router.pathname,
query: { ...query, peekIssueId: issueId, peekProjectId: projectId },
});
}
};
const removeRoutePeekId = () => {
const { query } = router;
if (query.peekIssueId) {
setIssueId(undefined);
delete query.peekIssueId;
delete query.peekProjectId;
router.push({
pathname: router.pathname,
query: { ...query },
});
}
};
useSWR(
workspaceSlug && projectId && issueId && peekIssueId && issueId === peekIssueId
? `ISSUE_PEEK_OVERVIEW_SUBSCRIPTION_${workspaceSlug}_${projectId}_${peekIssueId}`
: null,
async () => {
if (workspaceSlug && projectId && issueId && peekIssueId && issueId === peekIssueId) {
await fetchSubscriptions(workspaceSlug, projectId, issueId);
}
}
);
const removeRoutePeekId = () => setPeekIssue(undefined);
const issueReactions = reaction.getReactionsByIssueId(issueId) || [];
const issueActivity = activity.getActivitiesByIssueId(issueId);
@ -172,6 +129,7 @@ export const IssueView: FC<IIssueView> = observer((props) => {
onSubmit={handleDeleteIssue}
/>
)}
{issue && isArchived && (
<DeleteArchivedIssueModal
data={issue}
@ -180,14 +138,9 @@ export const IssueView: FC<IIssueView> = observer((props) => {
onSubmit={handleDeleteIssue}
/>
)}
<div className="w-full truncate !text-base">
{children && (
<div onClick={updateRoutePeekId} className="w-full cursor-pointer">
{children}
</div>
)}
{issueId === peekIssueId && (
<div className="w-full truncate !text-base">
{issueId && (
<div
ref={issuePeekOverviewRef}
className={`fixed z-20 flex flex-col overflow-hidden rounded border border-custom-border-200 bg-custom-background-100 transition-all duration-300
@ -248,7 +201,7 @@ export const IssueView: FC<IIssueView> = observer((props) => {
<div className="flex items-center gap-4">
{issue?.created_by !== currentUser?.id &&
!issue?.assignee_ids.includes(currentUser?.id ?? "") &&
!router.pathname.includes("[archivedIssueId]") && (
!issue?.archived_at && (
<Button
size="sm"
prependIcon={<Bell className="h-3 w-3" />}

View File

@ -2,7 +2,7 @@ import React, { useState } from "react";
import { useRouter } from "next/router";
import useSWR, { mutate } from "swr";
// hooks
import { useIssues } from "hooks/store";
import { useCycle, useIssues } from "hooks/store";
// services
import { CycleService } from "services/cycle.service";
// ui
@ -32,6 +32,7 @@ export const SidebarCycleSelect: React.FC<Props> = (props) => {
const {
issues: { removeIssueFromCycle, addIssueToCycle },
} = useIssues(EIssuesStoreType.CYCLE);
const { getCycleById } = useCycle();
const [isUpdating, setIsUpdating] = useState(false);
@ -87,17 +88,17 @@ export const SidebarCycleSelect: React.FC<Props> = (props) => {
),
}));
const issueCycle = issueDetail?.issue_cycle;
const issueCycle = (issueDetail && issueDetail.cycle_id && getCycleById(issueDetail.cycle_id)) || undefined;
const disableSelect = disabled || isUpdating;
return (
<div className="flex items-center gap-1">
<CustomSearchSelect
value={issueCycle?.cycle_detail.id}
value={issueDetail?.cycle_id}
onChange={(value: any) => {
value === issueCycle?.cycle_detail.id
? handleRemoveIssueFromCycle(issueCycle?.cycle ?? "")
value === issueDetail?.cycle_id
? handleRemoveIssueFromCycle(issueDetail?.cycle_id ?? "")
: handleCycleChange
? handleCycleChange(value)
: handleCycleStoreChange(value);
@ -105,7 +106,7 @@ export const SidebarCycleSelect: React.FC<Props> = (props) => {
options={options}
customButton={
<div>
<Tooltip position="left" tooltipContent={`${issueCycle ? issueCycle.cycle_detail.name : "No cycle"}`}>
<Tooltip position="left" tooltipContent={`${issueCycle ? issueCycle?.name : "No cycle"}`}>
<button
type="button"
className={`flex w-full items-center rounded bg-custom-background-80 px-2.5 py-0.5 text-xs ${
@ -118,7 +119,7 @@ export const SidebarCycleSelect: React.FC<Props> = (props) => {
}`}
>
<span className="flex-shrink-0">{issueCycle && <ContrastIcon className="h-3.5 w-3.5" />}</span>
<span className="truncate">{issueCycle ? issueCycle.cycle_detail.name : "No cycle"}</span>
<span className="truncate">{issueCycle ? issueCycle?.name : "No cycle"}</span>
</span>
</button>
</Tooltip>

View File

@ -81,17 +81,17 @@ export const SidebarModuleSelect: React.FC<Props> = observer((props) => {
});
// derived values
const issueModule = issueDetail?.issue_module;
const selectedModule = issueModule?.module ? getModuleById(issueModule?.module) : null;
const issueModule = (issueDetail && issueDetail?.module_id && getModuleById(issueDetail.module_id)) || undefined;
const disableSelect = disabled || isUpdating;
return (
<div className="flex items-center gap-1">
<CustomSearchSelect
value={issueModule?.module_detail.id}
value={issueDetail?.module_id}
onChange={(value: any) => {
value === issueModule?.module_detail.id
? handleRemoveIssueFromModule(issueModule?.id ?? "", issueModule?.module ?? "")
value === issueDetail?.module_id
? handleRemoveIssueFromModule(issueModule?.id ?? "", issueDetail?.module_id ?? "")
: handleModuleChange
? handleModuleChange(value)
: handleModuleStoreChange(value);
@ -99,7 +99,7 @@ export const SidebarModuleSelect: React.FC<Props> = observer((props) => {
options={options}
customButton={
<div>
<Tooltip position="left" tooltipContent={`${selectedModule?.name ?? "No module"}`}>
<Tooltip position="left" tooltipContent={`${issueModule?.name ?? "No module"}`}>
<button
type="button"
className={`flex w-full items-center rounded bg-custom-background-80 px-2.5 py-0.5 text-xs ${
@ -112,7 +112,7 @@ export const SidebarModuleSelect: React.FC<Props> = observer((props) => {
}`}
>
<span className="flex-shrink-0">{issueModule && <DiceIcon className="h-3.5 w-3.5" />}</span>
<span className="truncate">{selectedModule?.name ?? "No module"}</span>
<span className="truncate">{issueModule?.name ?? "No module"}</span>
</span>
</button>
</Tooltip>

View File

@ -2,13 +2,14 @@ import React, { useState } from "react";
import { useRouter } from "next/router";
// hooks
import { useIssueDetail, useProject } from "hooks/store";
import { useIssueDetail, useIssues, useProject } from "hooks/store";
// components
import { ParentIssuesListModal } from "components/issues";
// icons
import { X } from "lucide-react";
// types
import { TIssue, ISearchIssueResponse } from "@plane/types";
import { observer } from "mobx-react-lite";
type Props = {
onChange: (value: string) => void;
@ -16,7 +17,7 @@ type Props = {
disabled?: boolean;
};
export const SidebarParentSelect: React.FC<Props> = ({ onChange, issueDetails, disabled = false }) => {
export const SidebarParentSelect: React.FC<Props> = observer(({ onChange, issueDetails, disabled = false }) => {
const [selectedParentIssue, setSelectedParentIssue] = useState<ISearchIssueResponse | null>(null);
const { isParentIssueModalOpen, toggleParentIssueModal } = useIssueDetail();
@ -26,6 +27,7 @@ export const SidebarParentSelect: React.FC<Props> = ({ onChange, issueDetails, d
// hooks
const { getProjectById } = useProject();
const { issueMap } = useIssues();
return (
<>
@ -56,7 +58,7 @@ export const SidebarParentSelect: React.FC<Props> = ({ onChange, issueDetails, d
{selectedParentIssue && issueDetails?.parent_id ? (
`${selectedParentIssue.project__identifier}-${selectedParentIssue.sequence_id}`
) : !selectedParentIssue && issueDetails?.parent_id ? (
`${getProjectById(issueDetails.parent_id)?.identifier}-${issueDetails.parent_detail?.sequence_id}`
`${getProjectById(issueDetails.parent_id)?.identifier}-${issueMap[issueDetails.parent_id]?.sequence_id}`
) : (
<span className="text-custom-text-200">Select issue</span>
)}
@ -64,4 +66,4 @@ export const SidebarParentSelect: React.FC<Props> = ({ onChange, issueDetails, d
</button>
</>
);
};
});

View File

@ -1,10 +1,8 @@
import { useRouter } from "next/router";
import React from "react";
import { ChevronDown, ChevronRight, X, Pencil, Trash, Link as LinkIcon, Loader } from "lucide-react";
// components
import { SubIssuesRootList } from "./issues-list";
import { IssueProperty } from "./properties";
import { IssuePeekOverview } from "components/issues";
// ui
import { CustomMenu, Tooltip } from "@plane/ui";
// types
@ -42,7 +40,6 @@ export const SubIssues: React.FC<ISubIssues> = ({
projectId,
parentIssue,
issueId,
handleIssue,
spacingLeft = 0,
user,
editable,
@ -53,9 +50,6 @@ export const SubIssues: React.FC<ISubIssues> = ({
handleIssueCrudOperation,
handleUpdateIssue,
}) => {
const router = useRouter();
const { peekProjectId, peekIssueId } = router.query;
const {
issue: { getIssueById },
} = useIssueDetail();
@ -68,25 +62,8 @@ export const SubIssues: React.FC<ISubIssues> = ({
(issue?.project_id && getProjectStates(issue?.project_id)?.find((state) => issue?.state_id == state.id)) ||
undefined;
const handleIssuePeekOverview = () => {
const { query } = router;
router.push({
pathname: router.pathname,
query: { ...query, peekIssueId: issue?.id, peekProjectId: issue?.project_id },
});
};
return (
<>
{workspaceSlug && peekProjectId && peekIssueId && peekIssueId === issue?.id && (
<IssuePeekOverview
workspaceSlug={workspaceSlug}
projectId={peekProjectId.toString()}
issueId={peekIssueId.toString()}
handleIssue={async (issueToUpdate) => await handleUpdateIssue(issue, { ...issue, ...issueToUpdate })}
/>
)}
<div>
{issue && (
<div
@ -116,7 +93,7 @@ export const SubIssues: React.FC<ISubIssues> = ({
)}
</div>
<div className="flex w-full cursor-pointer items-center gap-2" onClick={handleIssuePeekOverview}>
<div className="flex w-full cursor-pointer items-center gap-2">
<div
className="h-[6px] w-[6px] flex-shrink-0 rounded-full"
style={{

View File

@ -2,15 +2,7 @@ import { v4 as uuidv4 } from "uuid";
// helpers
import { orderArrayBy } from "helpers/array.helper";
// types
import {
TIssue,
TIssueGroupByOptions,
TIssueLayouts,
TIssueOrderByOptions,
TIssueParams,
IProject,
IWorkspace,
} from "@plane/types";
import { TIssue, TIssueGroupByOptions, TIssueLayouts, TIssueOrderByOptions, TIssueParams } from "@plane/types";
// constants
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
@ -123,76 +115,21 @@ export const handleIssueQueryParamsByLayout = (
*
* @description create a full issue payload with some default values. This function also parse the form field
* like assignees, labels, etc. and add them to the payload
* @param workspaceDetail workspace detail to be added in the issue payload
* @param projectDetail project detail to be added in the issue payload
* @param projectId project id to be added in the issue payload
* @param formData partial issue data from the form. This will override the default values
* @returns full issue payload with some default values
*/
export const createIssuePayload: (
workspaceDetail: IWorkspace,
projectDetail: IProject,
export const createIssuePayload: (projectId: string, formData: Partial<TIssue>) => TIssue = (
projectId: string,
formData: Partial<TIssue>
) => TIssue = (workspaceDetail: IWorkspace, projectDetail: IProject, formData: Partial<TIssue>) => {
const payload = {
archived_at: null,
assignee_details: [],
attachment_count: 0,
attachments: [],
issue_relations: [],
related_issues: [],
bridge_id: null,
completed_at: new Date(),
created_at: "",
created_by: "",
cycle: null,
cycle_id: null,
cycle_detail: null,
description: {},
description_html: "",
description_stripped: "",
estimate_point: null,
issue_cycle: null,
issue_link: [],
issue_module: null,
label_details: [],
is_draft: false,
links_list: [],
link_count: 0,
module: null,
module_id: null,
name: "",
parent: null,
parent_detail: null,
priority: "none",
project: projectDetail.id,
project_detail: projectDetail,
sequence_id: 0,
sort_order: 0,
sprints: null,
start_date: null,
state: projectDetail.default_state,
state_detail: {} as any,
sub_issues_count: 0,
target_date: null,
updated_at: "",
updated_by: "",
workspace: workspaceDetail.id,
workspace_detail: workspaceDetail,
) => {
const payload: TIssue = {
id: uuidv4(),
project_id: projectId,
// tempId is used for optimistic updates. It is not a part of the API response.
tempId: uuidv4(),
// to be overridden by the form data
...formData,
assignee_ids: Array.isArray(formData.assignee_ids)
? formData.assignee_ids
: formData.assignee_ids && formData.assignee_ids !== "none" && formData.assignee_ids !== null
? [formData.assignee_ids]
: [],
label_ids: Array.isArray(formData.label_ids)
? formData.label_ids
: formData.label_ids && formData.label_ids !== "none" && formData.label_ids !== null
? [formData.label_ids]
: [],
} as TIssue;
return payload;

View File

@ -47,7 +47,7 @@ export const AppProvider: FC<IAppProvider> = observer((props) => {
<InstanceLayout>
<StoreWrapper>
<CrispWrapper user={currentUser}>
<PosthogWrapper
{/* <PosthogWrapper
user={currentUser}
workspaceRole={currentWorkspaceRole}
projectRole={currentProjectRole}
@ -55,7 +55,8 @@ export const AppProvider: FC<IAppProvider> = observer((props) => {
posthogHost={envConfig?.posthog_host || null}
>
<SWRConfig value={SWR_CONFIG}>{children}</SWRConfig>
</PosthogWrapper>
</PosthogWrapper> */}
<SWRConfig value={SWR_CONFIG}>{children}</SWRConfig>
</CrispWrapper>
</StoreWrapper>
</InstanceLayout>

View File

@ -30,8 +30,8 @@ const defaultValues: Partial<TIssue> = {
state_id: "",
priority: "low",
target_date: new Date().toString(),
issue_cycle: null,
issue_module: null,
cycle_id: null,
module_id: null,
};
// services

View File

@ -26,8 +26,8 @@ const defaultValues: Partial<TIssue> = {
// description: "",
description_html: "",
estimate_point: null,
issue_cycle: null,
issue_module: null,
cycle_id: null,
module_id: null,
name: "",
priority: "low",
start_date: undefined,
@ -43,7 +43,7 @@ const IssueDetailsPage: NextPageWithLayout = observer(() => {
const router = useRouter();
const { workspaceSlug, projectId, issueId: routeIssueId } = router.query;
const { issueId, fetchIssue } = useIssueDetail();
const { peekIssue, fetchIssue } = useIssueDetail();
useEffect(() => {
if (!workspaceSlug || !projectId || !routeIssueId) return;
fetchIssue(workspaceSlug as string, projectId as string, routeIssueId as string);
@ -54,9 +54,9 @@ const IssueDetailsPage: NextPageWithLayout = observer(() => {
mutate: mutateIssueDetails,
error,
} = useSWR(
workspaceSlug && projectId && issueId ? ISSUE_DETAILS(issueId as string) : null,
workspaceSlug && projectId && issueId
? () => issueService.retrieve(workspaceSlug as string, projectId as string, issueId as string)
workspaceSlug && projectId && peekIssue?.issueId ? ISSUE_DETAILS(peekIssue?.issueId as string) : null,
workspaceSlug && projectId && peekIssue?.issueId
? () => issueService.retrieve(workspaceSlug as string, projectId as string, peekIssue?.issueId as string)
: null
);
@ -66,10 +66,10 @@ const IssueDetailsPage: NextPageWithLayout = observer(() => {
const submitChanges = useCallback(
async (formData: Partial<TIssue>) => {
if (!workspaceSlug || !projectId || !issueId) return;
if (!workspaceSlug || !projectId || !peekIssue?.issueId) return;
mutate<TIssue>(
ISSUE_DETAILS(issueId as string),
ISSUE_DETAILS(peekIssue?.issueId as string),
(prevData) => {
if (!prevData) return prevData;
@ -85,30 +85,30 @@ const IssueDetailsPage: NextPageWithLayout = observer(() => {
...formData,
};
delete payload.related_issues;
delete payload.issue_relations;
// delete payload.related_issues;
// delete payload.issue_relations;
await issueService
.patchIssue(workspaceSlug as string, projectId as string, issueId as string, payload)
.patchIssue(workspaceSlug as string, projectId as string, peekIssue?.issueId as string, payload)
.then(() => {
mutateIssueDetails();
mutate(PROJECT_ISSUES_ACTIVITY(issueId as string));
mutate(PROJECT_ISSUES_ACTIVITY(peekIssue?.issueId as string));
})
.catch((e) => {
console.error(e);
});
},
[workspaceSlug, issueId, projectId, mutateIssueDetails]
[workspaceSlug, peekIssue?.issueId, projectId, mutateIssueDetails]
);
useEffect(() => {
if (!issueDetails) return;
mutate(PROJECT_ISSUES_ACTIVITY(issueId as string));
mutate(PROJECT_ISSUES_ACTIVITY(peekIssue?.issueId as string));
reset({
...issueDetails,
});
}, [issueDetails, reset, issueId]);
}, [issueDetails, reset, peekIssue?.issueId]);
return (
<>
@ -123,7 +123,7 @@ const IssueDetailsPage: NextPageWithLayout = observer(() => {
onClick: () => router.push(`/${workspaceSlug}/projects/${projectId}/issues`),
}}
/>
) : issueDetails && projectId && issueId ? (
) : issueDetails && projectId && peekIssue?.issueId ? (
<div className="flex h-full overflow-hidden">
<div className="h-full w-2/3 space-y-5 divide-y-2 divide-custom-border-300 overflow-y-auto p-5">
<IssueMainContent issueDetails={issueDetails} submitChanges={submitChanges} />

View File

@ -18,7 +18,6 @@ import { AppLayout } from "layouts/app-layout";
// components
import { GptAssistantPopover } from "components/core";
import { PageDetailsHeader } from "components/headers/page-details";
import { IssuePeekOverview } from "components/issues/peek-overview";
import { EmptyState } from "components/common";
// ui
import { DocumentEditorWithRef, DocumentReadOnlyEditorWithRef } from "@plane/document-editor";
@ -49,7 +48,7 @@ const PageDetailsPage: NextPageWithLayout = observer(() => {
const editorRef = useRef<any>(null);
// router
const router = useRouter();
const { workspaceSlug, projectId, pageId, peekIssueId } = router.query;
const { workspaceSlug, projectId, pageId } = router.query;
// store hooks
const {
issues: { updateIssue },
@ -108,12 +107,6 @@ const PageDetailsPage: NextPageWithLayout = observer(() => {
}
);
const handleUpdateIssue = (issueId: string, data: Partial<TIssue>) => {
if (!workspaceSlug || !projectId || !currentUser) return;
updateIssue(workspaceSlug.toString(), projectId.toString(), issueId, data);
};
const fetchIssue = async (issueId: string) => {
const issue = await issueService.retrieve(workspaceSlug as string, projectId as string, issueId as string);
return issue as TIssue;
@ -523,17 +516,6 @@ const PageDetailsPage: NextPageWithLayout = observer(() => {
)}
</div>
)}
<IssuePeekOverview
workspaceSlug={workspaceSlug as string}
projectId={projectId as string}
issueId={peekIssueId ? (peekIssueId as string) : ""}
isArchived={false}
handleIssue={(issueToUpdate) => {
if (peekIssueId && typeof peekIssueId === "string") {
handleUpdateIssue(peekIssueId, issueToUpdate);
}
}}
/>
</div>
</div>
) : (

View File

@ -158,16 +158,23 @@ export class ArchivedIssuesFilter extends IssueFilterHelperStore implements IArc
_filters.displayFilters = { ..._filters.displayFilters, ...updatedDisplayFilters };
// set sub_group_by to null if group_by is set to null
if (_filters.displayFilters.group_by === null) _filters.displayFilters.sub_group_by = null;
if (_filters.displayFilters.group_by === null) {
_filters.displayFilters.sub_group_by = null;
updatedDisplayFilters.sub_group_by = null;
}
// set sub_group_by to null if layout is switched to kanban group_by and sub_group_by are same
if (
_filters.displayFilters.layout === "kanban" &&
_filters.displayFilters.group_by === _filters.displayFilters.sub_group_by
)
) {
_filters.displayFilters.sub_group_by = null;
updatedDisplayFilters.sub_group_by = null;
}
// set group_by to state if layout is switched to kanban and group_by is null
if (_filters.displayFilters.layout === "kanban" && _filters.displayFilters.group_by === null)
if (_filters.displayFilters.layout === "kanban" && _filters.displayFilters.group_by === null) {
_filters.displayFilters.group_by = "state";
updatedDisplayFilters.group_by = "state";
}
runInAction(() => {
Object.keys(updatedDisplayFilters).forEach((_key) => {

View File

@ -145,16 +145,23 @@ export class CycleIssuesFilter extends IssueFilterHelperStore implements ICycleI
_filters.displayFilters = { ..._filters.displayFilters, ...updatedDisplayFilters };
// set sub_group_by to null if group_by is set to null
if (_filters.displayFilters.group_by === null) _filters.displayFilters.sub_group_by = null;
if (_filters.displayFilters.group_by === null) {
_filters.displayFilters.sub_group_by = null;
updatedDisplayFilters.sub_group_by = null;
}
// set sub_group_by to null if layout is switched to kanban group_by and sub_group_by are same
if (
_filters.displayFilters.layout === "kanban" &&
_filters.displayFilters.group_by === _filters.displayFilters.sub_group_by
)
) {
_filters.displayFilters.sub_group_by = null;
updatedDisplayFilters.sub_group_by = null;
}
// set group_by to state if layout is switched to kanban and group_by is null
if (_filters.displayFilters.layout === "kanban" && _filters.displayFilters.group_by === null)
if (_filters.displayFilters.layout === "kanban" && _filters.displayFilters.group_by === null) {
_filters.displayFilters.group_by = "state";
updatedDisplayFilters.group_by = "state";
}
runInAction(() => {
Object.keys(updatedDisplayFilters).forEach((_key) => {

View File

@ -142,16 +142,23 @@ export class DraftIssuesFilter extends IssueFilterHelperStore implements IDraftI
_filters.displayFilters = { ..._filters.displayFilters, ...updatedDisplayFilters };
// set sub_group_by to null if group_by is set to null
if (_filters.displayFilters.group_by === null) _filters.displayFilters.sub_group_by = null;
if (_filters.displayFilters.group_by === null) {
_filters.displayFilters.sub_group_by = null;
updatedDisplayFilters.sub_group_by = null;
}
// set sub_group_by to null if layout is switched to kanban group_by and sub_group_by are same
if (
_filters.displayFilters.layout === "kanban" &&
_filters.displayFilters.group_by === _filters.displayFilters.sub_group_by
)
) {
_filters.displayFilters.sub_group_by = null;
updatedDisplayFilters.sub_group_by = null;
}
// set group_by to state if layout is switched to kanban and group_by is null
if (_filters.displayFilters.layout === "kanban" && _filters.displayFilters.group_by === null)
if (_filters.displayFilters.layout === "kanban" && _filters.displayFilters.group_by === null) {
_filters.displayFilters.group_by = "state";
updatedDisplayFilters.group_by = "state";
}
runInAction(() => {
Object.keys(updatedDisplayFilters).forEach((_key) => {

View File

@ -76,7 +76,10 @@ export class IssueHelperStore implements TIssueHelperStore {
const state_group =
this.rootStore?.stateDetails?.find((_state) => _state.id === _issue?.state_id)?.group || "None";
groupArray = [state_group];
} else groupArray = this.getGroupArray(get(_issue, ISSUE_FILTER_DEFAULT_DATA[groupBy]), isCalendarIssues);
} else {
const groupValue = get(_issue, ISSUE_FILTER_DEFAULT_DATA[groupBy]);
groupArray = groupValue !== undefined ? this.getGroupArray(groupValue, isCalendarIssues) : [];
}
for (const group of groupArray) {
if (group && _issues[group]) _issues[group].push(_issue.id);
@ -116,8 +119,10 @@ export class IssueHelperStore implements TIssueHelperStore {
subGroupArray = [state_group];
groupArray = [state_group];
} else {
subGroupArray = this.getGroupArray(get(_issue, ISSUE_FILTER_DEFAULT_DATA[subGroupBy]));
groupArray = this.getGroupArray(get(_issue, ISSUE_FILTER_DEFAULT_DATA[groupBy]));
const subGroupValue = get(_issue, ISSUE_FILTER_DEFAULT_DATA[subGroupBy]);
const groupValue = get(_issue, ISSUE_FILTER_DEFAULT_DATA[groupBy]);
subGroupArray = subGroupValue != undefined ? this.getGroupArray(subGroupValue) : [];
groupArray = groupValue != undefined ? this.getGroupArray(groupValue) : [];
}
for (const subGroup of subGroupArray) {

View File

@ -49,7 +49,7 @@ export class IssueActivityStore implements IIssueActivityStore {
// computed
get issueActivities() {
const issueId = this.rootIssueDetailStore.issueId;
const issueId = this.rootIssueDetailStore.peekIssue?.issueId;
if (!issueId) return undefined;
return this.activities[issueId] ?? undefined;
}

View File

@ -62,7 +62,7 @@ export class IssueAttachmentStore implements IIssueAttachmentStore {
// computed
get issueAttachments() {
const issueId = this.rootIssueDetailStore.issueId;
const issueId = this.rootIssueDetailStore.peekIssue?.issueId;
if (!issueId) return undefined;
return this.attachments[issueId] ?? undefined;
}

View File

@ -60,7 +60,7 @@ export class IssueLinkStore implements IIssueLinkStore {
// computed
get issueLinks() {
const issueId = this.rootIssueDetailStore.issueId;
const issueId = this.rootIssueDetailStore.peekIssue?.issueId;
if (!issueId) return undefined;
return this.links[issueId] ?? undefined;
}

View File

@ -53,7 +53,7 @@ export class IssueReactionStore implements IIssueReactionStore {
// computed
get issueReactions() {
const issueId = this.rootIssueDetailStore.issueId;
const issueId = this.rootIssueDetailStore.peekIssue?.issueId;
if (!issueId) return undefined;
return this.reactions[issueId] ?? undefined;
}

View File

@ -68,7 +68,7 @@ export class IssueRelationStore implements IIssueRelationStore {
// computed
get issueRelations() {
const issueId = this.rootIssueDetailStore.issueId;
const issueId = this.rootIssueDetailStore.peekIssue?.issueId;
if (!issueId) return undefined;
return this.relationMap?.[issueId] ?? undefined;
}

View File

@ -18,6 +18,12 @@ import { IIssueRelationStore, IssueRelationStore, IIssueRelationStoreActions } f
import { TIssue, IIssueActivity, TIssueLink, TIssueRelationTypes } from "@plane/types";
export type TPeekIssue = {
workspaceSlug: string;
projectId: string;
issueId: string;
};
export interface IIssueDetail
extends IIssueStoreActions,
IIssueReactionStoreActions,
@ -30,14 +36,14 @@ export interface IIssueDetail
IIssueAttachmentStoreActions,
IIssueRelationStoreActions {
// observables
issueId: string | undefined;
peekIssue: TPeekIssue | undefined;
isIssueLinkModalOpen: boolean;
isParentIssueModalOpen: boolean;
isDeleteIssueModalOpen: boolean;
// computed
isAnyModalOpen: boolean;
// actions
setIssueId: (issueId: string | undefined) => void;
setPeekIssue: (peekIssue: TPeekIssue | undefined) => void;
toggleIssueLinkModal: (value: boolean) => void;
toggleParentIssueModal: (value: boolean) => void;
toggleDeleteIssueModal: (value: boolean) => void;
@ -57,7 +63,7 @@ export interface IIssueDetail
export class IssueDetail implements IIssueDetail {
// observables
issueId: string | undefined = undefined;
peekIssue: TPeekIssue | undefined = undefined;
isIssueLinkModalOpen: boolean = false;
isParentIssueModalOpen: boolean = false;
isDeleteIssueModalOpen: boolean = false;
@ -77,14 +83,14 @@ export class IssueDetail implements IIssueDetail {
constructor(rootStore: IIssueRootStore) {
makeObservable(this, {
// observables
issueId: observable.ref,
peekIssue: observable,
isIssueLinkModalOpen: observable.ref,
isParentIssueModalOpen: observable.ref,
isDeleteIssueModalOpen: observable.ref,
// computed
isAnyModalOpen: computed,
// action
setIssueId: action,
setPeekIssue: action,
toggleIssueLinkModal: action,
toggleParentIssueModal: action,
toggleDeleteIssueModal: action,
@ -110,16 +116,14 @@ export class IssueDetail implements IIssueDetail {
}
// actions
setIssueId = (issueId: string | undefined) => (this.issueId = issueId);
setPeekIssue = (peekIssue: TPeekIssue | undefined) => (this.peekIssue = peekIssue);
toggleIssueLinkModal = (value: boolean) => (this.isIssueLinkModalOpen = value);
toggleParentIssueModal = (value: boolean) => (this.isParentIssueModalOpen = value);
toggleDeleteIssueModal = (value: boolean) => (this.isDeleteIssueModalOpen = value);
// issue
fetchIssue = async (workspaceSlug: string, projectId: string, issueId: string) => {
this.issueId = issueId;
return this.issue.fetchIssue(workspaceSlug, projectId, issueId);
};
fetchIssue = async (workspaceSlug: string, projectId: string, issueId: string) =>
this.issue.fetchIssue(workspaceSlug, projectId, issueId);
updateIssue = async (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) =>
this.issue.updateIssue(workspaceSlug, projectId, issueId, data);
removeIssue = async (workspaceSlug: string, projectId: string, issueId: string) =>

View File

@ -46,7 +46,7 @@ export class IssueSubscriptionStore implements IIssueSubscriptionStore {
if (!issueId) return undefined;
const currentUserId = this.rootIssueDetail.rootIssueStore.currentUserId;
if (!currentUserId) return undefined;
return this.subscriptionMap[issueId][currentUserId] ?? undefined;
return this.subscriptionMap[issueId]?.[currentUserId] ?? undefined;
};
fetchSubscriptions = async (workspaceSlug: string, projectId: string, issueId: string) => {

View File

@ -1,757 +0,0 @@
import { observable, action, makeObservable, runInAction, computed, autorun } from "mobx";
// services
import { IssueService, IssueReactionService, IssueCommentService } from "services/issue";
import { NotificationService } from "services/notification.service";
// types
import { IIssueRootStore } from "./root.store";
import type { TIssue, IIssueActivity, IIssueLink, ILinkDetails } from "@plane/types";
// constants
import { groupReactionEmojis } from "constants/issue";
import { RootStore } from "store/root.store";
export interface IIssueDetailStore {
loader: boolean;
error: any | null;
peekId: string | null;
issues: {
[issueId: string]: TIssue;
};
issueReactions: {
[issueId: string]: any;
};
issueActivity: {
[issueId: string]: IIssueActivity[];
};
issueCommentReactions: {
[issueId: string]: {
[comment_id: string]: any;
};
};
issueSubscription: {
[issueId: string]: any;
};
setPeekId: (issueId: string | null) => void;
// computed
getIssue: TIssue | null;
getIssueReactions: any | null;
getIssueActivity: IIssueActivity[] | null;
getIssueCommentReactions: any | null;
getIssueSubscription: any | null;
// fetch issue details
fetchIssueDetails: (workspaceSlug: string, projectId: string, issueId: string) => Promise<TIssue>;
// deleting issue
deleteIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
fetchPeekIssueDetails: (workspaceSlug: string, projectId: string, issueId: string) => Promise<TIssue>;
fetchIssueReactions: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
createIssueReaction: (workspaceSlug: string, projectId: string, issueId: string, reaction: string) => Promise<void>;
removeIssueReaction: (workspaceSlug: string, projectId: string, issueId: string, reaction: string) => Promise<void>;
createIssueLink: (
workspaceSlug: string,
projectId: string,
issueId: string,
data: IIssueLink
) => Promise<ILinkDetails>;
updateIssueLink: (
workspaceSlug: string,
projectId: string,
issueId: string,
linkId: string,
data: IIssueLink
) => Promise<ILinkDetails>;
deleteIssueLink: (workspaceSlug: string, projectId: string, issueId: string, linkId: string) => Promise<void>;
fetchIssueActivity: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
createIssueComment: (workspaceSlug: string, projectId: string, issueId: string, data: any) => Promise<void>;
updateIssueComment: (
workspaceSlug: string,
projectId: string,
issueId: string,
commentId: string,
data: any
) => Promise<void>;
removeIssueComment: (workspaceSlug: string, projectId: string, issueId: string, commentId: string) => Promise<void>;
fetchIssueCommentReactions: (
workspaceSlug: string,
projectId: string,
issueId: string,
commentId: string
) => Promise<void>;
creationIssueCommentReaction: (
workspaceSlug: string,
projectId: string,
issueId: string,
commentId: string,
reaction: string
) => Promise<void>;
removeIssueCommentReaction: (
workspaceSlug: string,
projectId: string,
issueId: string,
commentId: string,
reaction: string
) => Promise<void>;
fetchIssueSubscription: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
createIssueSubscription: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
removeIssueSubscription: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
}
export class IssueDetailStore implements IIssueDetailStore {
loader: boolean = false;
error: any | null = null;
peekId: string | null = null;
issues: {
[issueId: string]: TIssue;
} = {};
issueReactions: {
[issueId: string]: any;
} = {};
issueActivity: {
[issueId: string]: IIssueActivity[];
} = {};
issueCommentReactions: {
[issueId: string]: any;
} = {};
issueSubscription: {
[issueId: string]: any;
} = {};
// root store
issueRootStore;
rootStore;
// service
issueService;
issueReactionService;
issueCommentService;
notificationService;
constructor(_issueRootStore: IIssueRootStore, _rootStore: RootStore) {
makeObservable(this, {
// observable
loader: observable.ref,
error: observable.ref,
peekId: observable.ref,
issues: observable.ref,
issueReactions: observable.ref,
issueActivity: observable.ref,
issueCommentReactions: observable.ref,
issueSubscription: observable.ref,
getIssue: computed,
getIssueReactions: computed,
getIssueActivity: computed,
getIssueCommentReactions: computed,
getIssueSubscription: computed,
setPeekId: action,
fetchIssueDetails: action,
deleteIssue: action,
fetchPeekIssueDetails: action,
fetchIssueReactions: action,
createIssueReaction: action,
removeIssueReaction: action,
createIssueLink: action,
updateIssueLink: action,
deleteIssueLink: action,
fetchIssueActivity: action,
createIssueComment: action,
updateIssueComment: action,
removeIssueComment: action,
fetchIssueCommentReactions: action,
creationIssueCommentReaction: action,
removeIssueCommentReaction: action,
fetchIssueSubscription: action,
createIssueSubscription: action,
removeIssueSubscription: action,
});
this.issueRootStore = _issueRootStore;
this.rootStore = _rootStore;
this.issueService = new IssueService();
this.issueReactionService = new IssueReactionService();
this.issueCommentService = new IssueCommentService();
this.notificationService = new NotificationService();
autorun(() => {
const projectId = this.rootStore?.app.router.projectId;
const peekId = this.peekId;
if (!projectId || !peekId) return;
// FIXME: uncomment and fix
// const issue = this.issueRootStore.projectIssues.issues?.[projectId]?.[peekId];
// if (issue && issue.id)
// runInAction(() => {
// this.issues = {
// ...this.issues,
// [issue.id]: {
// ...this.issues[issue.id],
// ...issue,
// },
// };
// });
});
}
get getIssue() {
if (!this.peekId) return null;
const _issue = this.issues[this.peekId];
return _issue || null;
}
get getIssueReactions() {
if (!this.peekId) return null;
const _reactions = this.issueReactions[this.peekId];
return _reactions || null;
}
get getIssueActivity() {
if (!this.peekId) return null;
const activity = this.issueActivity[this.peekId];
return activity || null;
}
get getIssueCommentReactions() {
if (!this.peekId) return null;
const _commentReactions = this.issueCommentReactions[this.peekId];
return _commentReactions || null;
}
get getIssueSubscription() {
if (!this.peekId) return null;
const _commentSubscription = this.issueSubscription[this.peekId];
return _commentSubscription || null;
}
setPeekId = (issueId: string | null) => (this.peekId = issueId);
fetchIssueDetails = async (workspaceSlug: string, projectId: string, issueId: string) => {
try {
this.loader = true;
this.error = null;
this.peekId = issueId;
const issueDetailsResponse = await this.issueService.retrieve(workspaceSlug, projectId, issueId);
runInAction(() => {
this.loader = false;
this.error = null;
this.issues = {
...this.issues,
[issueId]: issueDetailsResponse,
};
});
return issueDetailsResponse;
} catch (error) {
runInAction(() => {
this.loader = false;
this.error = error;
});
throw error;
}
};
deleteIssue = async (workspaceSlug: string, projectId: string, issueId: string) => {
const newIssues = { ...this.issues };
delete newIssues[issueId];
try {
runInAction(() => {
this.loader = true;
this.error = null;
this.issues = newIssues;
});
const user = this.rootStore.user.currentUser;
if (!user) return;
const response = await this.issueService.deleteIssue(workspaceSlug, projectId, issueId);
runInAction(() => {
this.loader = false;
this.error = null;
});
return response;
} catch (error) {
this.fetchIssueDetails(workspaceSlug, projectId, issueId);
runInAction(() => {
this.loader = false;
this.error = error;
});
return error;
}
};
fetchPeekIssueDetails = async (workspaceSlug: string, projectId: string, issueId: string) => {
try {
this.loader = true;
this.error = null;
this.peekId = issueId;
const issueDetailsResponse = await this.issueService.retrieve(workspaceSlug, projectId, issueId);
await this.fetchIssueReactions(workspaceSlug, projectId, issueId);
await this.fetchIssueActivity(workspaceSlug, projectId, issueId);
runInAction(() => {
this.loader = false;
this.error = null;
this.issues = {
...this.issues,
[issueId]: issueDetailsResponse,
};
});
return issueDetailsResponse;
} catch (error) {
runInAction(() => {
this.loader = false;
this.error = error;
});
throw error;
}
};
// reactions
fetchIssueReactions = async (workspaceSlug: string, projectId: string, issueId: string) => {
try {
const _reactions = await this.issueReactionService.listIssueReactions(workspaceSlug, projectId, issueId);
const _issueReactions = {
...this.issueReactions,
[issueId]: groupReactionEmojis(_reactions),
};
runInAction(() => {
this.issueReactions = _issueReactions;
});
} catch (error) {
console.warn("error creating the issue reaction", error);
throw error;
}
};
createIssueReaction = async (workspaceSlug: string, projectId: string, issueId: string, reaction: string) => {
let _currentReactions = this.getIssueReactions;
try {
const _reaction = await this.issueReactionService.createIssueReaction(workspaceSlug, projectId, issueId, {
reaction,
});
_currentReactions = {
..._currentReactions,
[reaction]: [..._currentReactions[reaction], { ..._reaction }],
};
runInAction(() => {
this.issueReactions = {
...this.issueReactions,
[issueId]: _currentReactions,
};
});
} catch (error) {
runInAction(() => {
this.issueReactions = {
...this.issueReactions,
[issueId]: _currentReactions,
};
});
console.warn("error creating the issue reaction", error);
throw error;
}
};
removeIssueReaction = async (workspaceSlug: string, projectId: string, issueId: string, reaction: string) => {
let _currentReactions = this.getIssueReactions;
try {
const user = this.rootStore.user.currentUser;
if (user) {
_currentReactions = {
..._currentReactions,
[reaction]: [..._currentReactions[reaction].filter((r: any) => r.actor !== user.id)],
};
runInAction(() => {
this.issueReactions = {
...this.issueReactions,
[issueId]: _currentReactions,
};
});
await this.issueReactionService.deleteIssueReaction(workspaceSlug, projectId, issueId, reaction);
}
} catch (error) {
runInAction(() => {
this.issueReactions = {
...this.issueReactions,
[issueId]: _currentReactions,
};
});
console.warn("error removing the issue reaction", error);
throw error;
}
};
fetchIssueActivity = async (workspaceSlug: string, projectId: string, issueId: string) => {
try {
const issueActivityResponse = await this.issueService.getIssueActivities(workspaceSlug, projectId, issueId);
const _issueComments = {
...this.issueActivity,
[issueId]: [...issueActivityResponse],
};
runInAction(() => {
this.issueActivity = _issueComments;
});
} catch (error) {
console.warn("error creating the issue comment", error);
throw error;
}
};
// comments
createIssueComment = async (workspaceSlug: string, projectId: string, issueId: string, data: any) => {
try {
const _issueCommentResponse = await this.issueCommentService.createIssueComment(
workspaceSlug,
projectId,
issueId,
data
);
const _issueComments = {
...this.issueActivity,
[issueId]: [...this.issueActivity[issueId], _issueCommentResponse],
};
runInAction(() => {
this.issueActivity = _issueComments;
});
} catch (error) {
console.warn("error creating the issue comment", error);
throw error;
}
};
updateIssueComment = async (
workspaceSlug: string,
projectId: string,
issueId: string,
commentId: string,
data: any
) => {
try {
const _issueCommentResponse = await this.issueCommentService.patchIssueComment(
workspaceSlug,
projectId,
issueId,
commentId,
data
);
const _issueComments = {
...this.issueActivity,
[issueId]: this.issueActivity[issueId].map((comment) =>
comment.id === commentId ? _issueCommentResponse : comment
),
};
runInAction(() => {
this.issueActivity = _issueComments;
});
} catch (error) {
console.warn("error updating the issue comment", error);
throw error;
}
};
removeIssueComment = async (workspaceSlug: string, projectId: string, issueId: string, commentId: string) => {
try {
const _issueComments = {
...this.issueActivity,
[issueId]: this.issueActivity[issueId]?.filter((comment) => comment.id != commentId),
};
await this.issueCommentService.deleteIssueComment(workspaceSlug, projectId, issueId, commentId);
runInAction(() => {
this.issueActivity = _issueComments;
});
} catch (error) {
console.warn("error removing the issue comment", error);
throw error;
}
};
// comment reactions
fetchIssueCommentReactions = async (workspaceSlug: string, projectId: string, issueId: string, commentId: string) => {
try {
const _reactions = await this.issueReactionService.listIssueCommentReactions(workspaceSlug, projectId, commentId);
const _issueCommentReactions = {
...this.issueCommentReactions,
[issueId]: {
...this.issueCommentReactions[issueId],
[commentId]: groupReactionEmojis(_reactions),
},
};
runInAction(() => {
this.issueCommentReactions = _issueCommentReactions;
});
} catch (error) {
console.warn("error removing the issue comment", error);
throw error;
}
};
creationIssueCommentReaction = async (
workspaceSlug: string,
projectId: string,
issueId: string,
commentId: string,
reaction: string
) => {
let _currentReactions = this.getIssueCommentReactions;
_currentReactions = _currentReactions && commentId ? _currentReactions?.[commentId] : null;
try {
const _reaction = await this.issueReactionService.createIssueCommentReaction(
workspaceSlug,
projectId,
commentId,
{
reaction,
}
);
_currentReactions = {
..._currentReactions,
[reaction]: [..._currentReactions?.[reaction], { ..._reaction }],
};
const _issueCommentReactions = {
...this.issueCommentReactions,
[issueId]: {
...this.issueCommentReactions[issueId],
[commentId]: _currentReactions,
},
};
runInAction(() => {
this.issueCommentReactions = _issueCommentReactions;
});
} catch (error) {
console.warn("error removing the issue comment", error);
throw error;
}
};
removeIssueCommentReaction = async (
workspaceSlug: string,
projectId: string,
issueId: string,
commentId: string,
reaction: string
) => {
let _currentReactions = this.getIssueCommentReactions;
_currentReactions = _currentReactions && commentId ? _currentReactions?.[commentId] : null;
try {
const user = this.rootStore.user.currentUser;
if (user) {
_currentReactions = {
..._currentReactions,
[reaction]: [..._currentReactions?.[reaction].filter((r: any) => r.actor !== user.id)],
};
const _issueCommentReactions = {
...this.issueCommentReactions,
[issueId]: {
...this.issueCommentReactions[issueId],
[commentId]: _currentReactions,
},
};
runInAction(() => {
this.issueCommentReactions = _issueCommentReactions;
});
await this.issueReactionService.deleteIssueCommentReaction(workspaceSlug, projectId, commentId, reaction);
}
} catch (error) {
console.warn("error removing the issue comment", error);
throw error;
}
};
createIssueLink = async (workspaceSlug: string, projectId: string, issueId: string, data: IIssueLink) => {
try {
const response = await this.issueService.createIssueLink(workspaceSlug, projectId, issueId, data);
runInAction(() => {
this.issues = {
...this.issues,
[issueId]: {
...this.issues[issueId],
issue_link: [response, ...this.issues[issueId].issue_link],
},
};
});
return response;
} catch (error) {
console.error("Failed to create link in store", error);
this.fetchIssueDetails(workspaceSlug, projectId, issueId);
runInAction(() => {
this.error = error;
});
throw error;
}
};
updateIssueLink = async (
workspaceSlug: string,
projectId: string,
issueId: string,
linkId: string,
data: IIssueLink
) => {
try {
const response = await this.issueService.updateIssueLink(workspaceSlug, projectId, issueId, linkId, data);
runInAction(() => {
this.issues = {
...this.issues,
[issueId]: {
...this.issues[issueId],
issue_link: this.issues[issueId].issue_link.map((link: any) => (link.id === linkId ? response : link)),
},
};
});
return response;
} catch (error) {
console.error("Failed to update link in issue store", error);
this.fetchIssueDetails(workspaceSlug, projectId, issueId);
runInAction(() => {
this.error = error;
});
throw error;
}
};
deleteIssueLink = async (workspaceSlug: string, projectId: string, issueId: string, linkId: string) => {
try {
runInAction(() => {
this.issues = {
...this.issues,
[issueId]: {
...this.issues[issueId],
issue_link: this.issues[issueId].issue_link.filter((link: any) => link.id !== linkId),
},
};
});
await this.issueService.deleteIssueLink(workspaceSlug, projectId, issueId, linkId);
} catch (error) {
console.error("Failed to delete link in issue store", error);
runInAction(() => {
this.error = error;
});
throw error;
}
};
// subscriptions
fetchIssueSubscription = async (workspaceSlug: string, projectId: string, issueId: string) => {
try {
const _subscription = await this.notificationService.getIssueNotificationSubscriptionStatus(
workspaceSlug,
projectId,
issueId
);
const _issueSubscription = {
...this.issueSubscription,
[issueId]: _subscription,
};
runInAction(() => {
this.issueSubscription = _issueSubscription;
});
} catch (error) {
console.warn("error fetching the issue subscription", error);
throw error;
}
};
createIssueSubscription = async (workspaceSlug: string, projectId: string, issueId: string) => {
try {
await this.notificationService.subscribeToIssueNotifications(workspaceSlug, projectId, issueId);
const _issueSubscription = {
...this.issueSubscription,
[issueId]: { subscribed: true },
};
runInAction(() => {
this.issueSubscription = _issueSubscription;
});
} catch (error) {
console.warn("error creating the issue subscription", error);
throw error;
}
};
removeIssueSubscription = async (workspaceSlug: string, projectId: string, issueId: string) => {
try {
const _issueSubscription = {
...this.issueSubscription,
[issueId]: { subscribed: false },
};
runInAction(() => {
this.issueSubscription = _issueSubscription;
});
await this.notificationService.unsubscribeFromIssueNotifications(workspaceSlug, projectId, issueId);
} catch (error) {
console.warn("error removing the issue subscription", error);
throw error;
}
};
}

View File

@ -40,7 +40,7 @@ export class IssueKanBanViewStore implements IIssueKanBanViewStore {
get canUserDragDrop() {
return true;
if (this.rootStore.issueDetail.issueId) return false;
if (this.rootStore.issueDetail.peekIssue?.issueId) return false;
// FIXME: uncomment and fix
// if (
// this.rootStore?.issueFilter?.userDisplayFilters?.order_by &&

View File

@ -145,16 +145,23 @@ export class ModuleIssuesFilter extends IssueFilterHelperStore implements IModul
_filters.displayFilters = { ..._filters.displayFilters, ...updatedDisplayFilters };
// set sub_group_by to null if group_by is set to null
if (_filters.displayFilters.group_by === null) _filters.displayFilters.sub_group_by = null;
if (_filters.displayFilters.group_by === null) {
_filters.displayFilters.sub_group_by = null;
updatedDisplayFilters.sub_group_by = null;
}
// set sub_group_by to null if layout is switched to kanban group_by and sub_group_by are same
if (
_filters.displayFilters.layout === "kanban" &&
_filters.displayFilters.group_by === _filters.displayFilters.sub_group_by
)
) {
_filters.displayFilters.sub_group_by = null;
updatedDisplayFilters.sub_group_by = null;
}
// set group_by to state if layout is switched to kanban and group_by is null
if (_filters.displayFilters.layout === "kanban" && _filters.displayFilters.group_by === null)
if (_filters.displayFilters.layout === "kanban" && _filters.displayFilters.group_by === null) {
_filters.displayFilters.group_by = "state";
updatedDisplayFilters.group_by = "state";
}
runInAction(() => {
Object.keys(updatedDisplayFilters).forEach((_key) => {

View File

@ -150,16 +150,23 @@ export class ProfileIssuesFilter extends IssueFilterHelperStore implements IProf
_filters.displayFilters = { ..._filters.displayFilters, ...updatedDisplayFilters };
// set sub_group_by to null if group_by is set to null
if (_filters.displayFilters.group_by === null) _filters.displayFilters.sub_group_by = null;
if (_filters.displayFilters.group_by === null) {
_filters.displayFilters.sub_group_by = null;
updatedDisplayFilters.sub_group_by = null;
}
// set sub_group_by to null if layout is switched to kanban group_by and sub_group_by are same
if (
_filters.displayFilters.layout === "kanban" &&
_filters.displayFilters.group_by === _filters.displayFilters.sub_group_by
)
) {
_filters.displayFilters.sub_group_by = null;
updatedDisplayFilters.sub_group_by = null;
}
// set group_by to state if layout is switched to kanban and group_by is null
if (_filters.displayFilters.layout === "kanban" && _filters.displayFilters.group_by === null)
if (_filters.displayFilters.layout === "kanban" && _filters.displayFilters.group_by === null) {
_filters.displayFilters.group_by = "state";
updatedDisplayFilters.group_by = "state";
}
runInAction(() => {
Object.keys(updatedDisplayFilters).forEach((_key) => {

View File

@ -146,16 +146,23 @@ export class ProjectViewIssuesFilter extends IssueFilterHelperStore implements I
_filters.displayFilters = { ..._filters.displayFilters, ...updatedDisplayFilters };
// set sub_group_by to null if group_by is set to null
if (_filters.displayFilters.group_by === null) _filters.displayFilters.sub_group_by = null;
if (_filters.displayFilters.group_by === null) {
_filters.displayFilters.sub_group_by = null;
updatedDisplayFilters.sub_group_by = null;
}
// set sub_group_by to null if layout is switched to kanban group_by and sub_group_by are same
if (
_filters.displayFilters.layout === "kanban" &&
_filters.displayFilters.group_by === _filters.displayFilters.sub_group_by
)
) {
_filters.displayFilters.sub_group_by = null;
updatedDisplayFilters.sub_group_by = null;
}
// set group_by to state if layout is switched to kanban and group_by is null
if (_filters.displayFilters.layout === "kanban" && _filters.displayFilters.group_by === null)
if (_filters.displayFilters.layout === "kanban" && _filters.displayFilters.group_by === null) {
_filters.displayFilters.group_by = "state";
updatedDisplayFilters.group_by = "state";
}
runInAction(() => {
Object.keys(updatedDisplayFilters).forEach((_key) => {

View File

@ -142,16 +142,23 @@ export class ProjectIssuesFilter extends IssueFilterHelperStore implements IProj
_filters.displayFilters = { ..._filters.displayFilters, ...updatedDisplayFilters };
// set sub_group_by to null if group_by is set to null
if (_filters.displayFilters.group_by === null) _filters.displayFilters.sub_group_by = null;
if (_filters.displayFilters.group_by === null) {
_filters.displayFilters.sub_group_by = null;
updatedDisplayFilters.sub_group_by = null;
}
// set sub_group_by to null if layout is switched to kanban group_by and sub_group_by are same
if (
_filters.displayFilters.layout === "kanban" &&
_filters.displayFilters.group_by === _filters.displayFilters.sub_group_by
)
) {
_filters.displayFilters.sub_group_by = null;
updatedDisplayFilters.sub_group_by = null;
}
// set group_by to state if layout is switched to kanban and group_by is null
if (_filters.displayFilters.layout === "kanban" && _filters.displayFilters.group_by === null)
if (_filters.displayFilters.layout === "kanban" && _filters.displayFilters.group_by === null) {
_filters.displayFilters.group_by = "state";
updatedDisplayFilters.group_by = "state";
}
runInAction(() => {
Object.keys(updatedDisplayFilters).forEach((_key) => {

View File

@ -157,16 +157,23 @@ export class WorkspaceIssuesFilter extends IssueFilterHelperStore implements IWo
_filters.displayFilters = { ..._filters.displayFilters, ...updatedDisplayFilters };
// set sub_group_by to null if group_by is set to null
if (_filters.displayFilters.group_by === null) _filters.displayFilters.sub_group_by = null;
if (_filters.displayFilters.group_by === null) {
_filters.displayFilters.sub_group_by = null;
updatedDisplayFilters.sub_group_by = null;
}
// set sub_group_by to null if layout is switched to kanban group_by and sub_group_by are same
if (
_filters.displayFilters.layout === "kanban" &&
_filters.displayFilters.group_by === _filters.displayFilters.sub_group_by
)
) {
_filters.displayFilters.sub_group_by = null;
updatedDisplayFilters.sub_group_by = null;
}
// set group_by to state if layout is switched to kanban and group_by is null
if (_filters.displayFilters.layout === "kanban" && _filters.displayFilters.group_by === null)
if (_filters.displayFilters.layout === "kanban" && _filters.displayFilters.group_by === null) {
_filters.displayFilters.group_by = "state";
updatedDisplayFilters.group_by = "state";
}
runInAction(() => {
Object.keys(updatedDisplayFilters).forEach((_key) => {