mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
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:
parent
266f14d550
commit
efd3ebf067
@ -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:
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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,
|
||||
)
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
)
|
||||
|
||||
|
@ -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)
|
||||
|
17
packages/types/src/inbox.d.ts
vendored
17
packages/types/src/inbox.d.ts
vendored
@ -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;
|
||||
|
115
packages/types/src/issues.d.ts
vendored
115
packages/types/src/issues.d.ts
vendored
@ -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[];
|
||||
|
35
packages/types/src/issues/issue.d.ts
vendored
35
packages/types/src/issues/issue.d.ts
vendored
@ -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;
|
||||
};
|
||||
|
27
packages/ui/src/control-link/control-link.tsx
Normal file
27
packages/ui/src/control-link/control-link.tsx
Normal 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>
|
||||
);
|
||||
};
|
1
packages/ui/src/control-link/index.ts
Normal file
1
packages/ui/src/control-link/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./control-link";
|
@ -9,3 +9,4 @@ export * from "./progress";
|
||||
export * from "./spinners";
|
||||
export * from "./tooltip";
|
||||
export * from "./loader";
|
||||
export * from "./control-link";
|
||||
|
@ -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 ?? "");
|
||||
|
@ -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 (
|
||||
|
@ -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)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
@ -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}
|
||||
|
@ -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,
|
||||
});
|
||||
|
@ -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);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
@ -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 {
|
||||
|
@ -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)}
|
||||
/>
|
||||
)}
|
||||
)} */}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
@ -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 ? (
|
||||
|
@ -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)}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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 />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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"
|
||||
/>
|
||||
|
@ -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,
|
||||
});
|
||||
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
@ -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: {},
|
||||
};
|
||||
});
|
||||
};
|
||||
|
@ -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>
|
||||
|
@ -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 (
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
@ -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" />}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -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={{
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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} />
|
||||
|
@ -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>
|
||||
) : (
|
||||
|
@ -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) => {
|
||||
|
@ -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) => {
|
||||
|
@ -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) => {
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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) =>
|
||||
|
@ -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) => {
|
||||
|
@ -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;
|
||||
}
|
||||
};
|
||||
}
|
@ -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 &&
|
||||
|
@ -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) => {
|
||||
|
@ -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) => {
|
||||
|
@ -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) => {
|
||||
|
@ -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) => {
|
||||
|
@ -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) => {
|
||||
|
Loading…
Reference in New Issue
Block a user