Merge branch 'develop' of gurusainath:makeplane/plane into fix/kanban-sorting

This commit is contained in:
gurusainath 2023-09-15 11:16:36 +05:30
commit d88a0885d5
58 changed files with 2668 additions and 1158 deletions

View File

@ -49,6 +49,7 @@ class IssueFlatSerializer(BaseSerializer):
"target_date",
"sequence_id",
"sort_order",
"is_draft",
]

View File

@ -1038,6 +1038,7 @@ urlpatterns = [
IssueDraftViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-issue-draft",
@ -1047,6 +1048,7 @@ urlpatterns = [
IssueDraftViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),

View File

@ -508,7 +508,7 @@ class IssueActivityEndpoint(BaseAPIView):
issue_activities = (
IssueActivity.objects.filter(issue_id=issue_id)
.filter(
~Q(field__in=["comment", "vote", "reaction"]),
~Q(field__in=["comment", "vote", "reaction", "draft"]),
project__project_projectmember__member=self.request.user,
)
.select_related("actor", "workspace", "issue", "project")
@ -2358,6 +2358,47 @@ class IssueDraftViewSet(BaseViewSet):
serializer_class = IssueFlatSerializer
model = Issue
def perform_update(self, serializer):
requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder)
current_instance = (
self.get_queryset().filter(pk=self.kwargs.get("pk", None)).first()
)
if current_instance is not None:
issue_activity.delay(
type="issue_draft.activity.updated",
requested_data=requested_data,
actor_id=str(self.request.user.id),
issue_id=str(self.kwargs.get("pk", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=json.dumps(
IssueSerializer(current_instance).data, cls=DjangoJSONEncoder
),
)
return super().perform_update(serializer)
def perform_destroy(self, instance):
current_instance = (
self.get_queryset().filter(pk=self.kwargs.get("pk", None)).first()
)
if current_instance is not None:
issue_activity.delay(
type="issue_draft.activity.deleted",
requested_data=json.dumps(
{"issue_id": str(self.kwargs.get("pk", None))}
),
actor_id=str(self.request.user.id),
issue_id=str(self.kwargs.get("pk", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=json.dumps(
IssueSerializer(current_instance).data, cls=DjangoJSONEncoder
),
)
return super().perform_destroy(instance)
def get_queryset(self):
return (
Issue.objects.annotate(
@ -2383,6 +2424,7 @@ class IssueDraftViewSet(BaseViewSet):
)
)
@method_decorator(gzip_page)
def list(self, request, slug, project_id):
try:
@ -2492,6 +2534,40 @@ class IssueDraftViewSet(BaseViewSet):
)
def create(self, request, slug, project_id):
try:
project = Project.objects.get(pk=project_id)
serializer = IssueCreateSerializer(
data=request.data,
context={
"project_id": project_id,
"workspace_id": project.workspace_id,
"default_assignee_id": project.default_assignee_id,
},
)
if serializer.is_valid():
serializer.save(is_draft=True)
# Track the issue
issue_activity.delay(
type="issue_draft.activity.created",
requested_data=json.dumps(self.request.data, cls=DjangoJSONEncoder),
actor_id=str(request.user.id),
issue_id=str(serializer.data.get("id", None)),
project_id=str(project_id),
current_instance=None,
)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except Project.DoesNotExist:
return Response(
{"error": "Project was not found"}, status=status.HTTP_404_NOT_FOUND
)
def retrieve(self, request, slug, project_id, pk=None):
try:
issue = Issue.objects.get(

View File

@ -396,16 +396,16 @@ def track_assignees(
def create_issue_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities
):
issue_activities.append(
IssueActivity(
issue_id=issue_id,
project=project,
workspace=project.workspace,
comment=f"created the issue",
verb="created",
actor=actor,
issue_activities.append(
IssueActivity(
issue_id=issue_id,
project=project,
workspace=project.workspace,
comment=f"created the issue",
verb="created",
actor=actor,
)
)
)
def track_estimate_points(
@ -518,11 +518,6 @@ def update_issue_activity(
"closed_to": track_closed_to,
}
requested_data = json.loads(requested_data) if requested_data is not None else None
current_instance = (
json.loads(current_instance) if current_instance is not None else None
)
for key in requested_data:
func = ISSUE_ACTIVITY_MAPPER.get(key, None)
if func is not None:
@ -1095,6 +1090,69 @@ def delete_issue_relation_activity(
)
def create_draft_issue_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities
):
issue_activities.append(
IssueActivity(
issue_id=issue_id,
project=project,
workspace=project.workspace,
comment=f"drafted the issue",
field="draft",
verb="created",
actor=actor,
)
)
def update_draft_issue_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities
):
requested_data = json.loads(requested_data) if requested_data is not None else None
current_instance = (
json.loads(current_instance) if current_instance is not None else None
)
if requested_data.get("is_draft") is not None and requested_data.get("is_draft") == False:
issue_activities.append(
IssueActivity(
issue_id=issue_id,
project=project,
workspace=project.workspace,
comment=f"created the issue",
verb="updated",
actor=actor,
)
)
else:
issue_activities.append(
IssueActivity(
issue_id=issue_id,
project=project,
workspace=project.workspace,
comment=f"updated the draft issue",
field="draft",
verb="updated",
actor=actor,
)
)
def delete_draft_issue_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities
):
issue_activities.append(
IssueActivity(
project=project,
workspace=project.workspace,
comment=f"deleted the draft issue",
field="draft",
verb="deleted",
actor=actor,
)
)
# Receive message from room group
@shared_task
def issue_activity(
@ -1166,6 +1224,9 @@ def issue_activity(
"comment_reaction.activity.deleted": delete_comment_reaction_activity,
"issue_vote.activity.created": create_issue_vote_activity,
"issue_vote.activity.deleted": delete_issue_vote_activity,
"issue_draft.activity.created": create_draft_issue_activity,
"issue_draft.activity.updated": update_draft_issue_activity,
"issue_draft.activity.deleted": delete_draft_issue_activity,
}
func = ACTIVITY_MAPPER.get(type)

View File

@ -39,14 +39,90 @@ def group_results(results_data, group_by, sub_group_by=False):
for value in results_data:
main_group_attribute = resolve_keys(sub_group_by, value)
if str(main_group_attribute) not in main_responsive_dict:
main_responsive_dict[str(main_group_attribute)] = {}
group_attribute = resolve_keys(group_by, value)
if str(group_attribute) in main_responsive_dict:
main_responsive_dict[str(main_group_attribute)][str(group_attribute)].append(value)
if isinstance(main_group_attribute, list) and not isinstance(group_attribute, list):
if len(main_group_attribute):
for attrib in main_group_attribute:
if str(attrib) not in main_responsive_dict:
main_responsive_dict[str(attrib)] = {}
if str(group_attribute) in main_responsive_dict[str(attrib)]:
main_responsive_dict[str(attrib)][str(group_attribute)].append(value)
else:
main_responsive_dict[str(attrib)][str(group_attribute)] = []
main_responsive_dict[str(attrib)][str(group_attribute)].append(value)
else:
if str(None) not in main_responsive_dict:
main_responsive_dict[str(None)] = {}
if str(group_attribute) in main_responsive_dict[str(None)]:
main_responsive_dict[str(None)][str(group_attribute)].append(value)
else:
main_responsive_dict[str(None)][str(group_attribute)] = []
main_responsive_dict[str(None)][str(group_attribute)].append(value)
elif isinstance(group_attribute, list) and not isinstance(main_group_attribute, list):
if str(main_group_attribute) not in main_responsive_dict:
main_responsive_dict[str(main_group_attribute)] = {}
if len(group_attribute):
for attrib in group_attribute:
if str(attrib) in main_responsive_dict[str(main_group_attribute)]:
main_responsive_dict[str(main_group_attribute)][str(attrib)].append(value)
else:
main_responsive_dict[str(main_group_attribute)][str(attrib)] = []
main_responsive_dict[str(main_group_attribute)][str(attrib)].append(value)
else:
if str(None) in main_responsive_dict[str(main_group_attribute)]:
main_responsive_dict[str(main_group_attribute)][str(None)].append(value)
else:
main_responsive_dict[str(main_group_attribute)][str(None)] = []
main_responsive_dict[str(main_group_attribute)][str(None)].append(value)
elif isinstance(group_attribute, list) and isinstance(main_group_attribute, list):
if len(main_group_attribute):
for main_attrib in main_group_attribute:
if str(main_attrib) not in main_responsive_dict:
main_responsive_dict[str(main_attrib)] = {}
if len(group_attribute):
for attrib in group_attribute:
if str(attrib) in main_responsive_dict[str(main_attrib)]:
main_responsive_dict[str(main_attrib)][str(attrib)].append(value)
else:
main_responsive_dict[str(main_attrib)][str(attrib)] = []
main_responsive_dict[str(main_attrib)][str(attrib)].append(value)
else:
if str(None) in main_responsive_dict[str(main_attrib)]:
main_responsive_dict[str(main_attrib)][str(None)].append(value)
else:
main_responsive_dict[str(main_attrib)][str(None)] = []
main_responsive_dict[str(main_attrib)][str(None)].append(value)
else:
if str(None) not in main_responsive_dict:
main_responsive_dict[str(None)] = {}
if len(group_attribute):
for attrib in group_attribute:
if str(attrib) in main_responsive_dict[str(None)]:
main_responsive_dict[str(None)][str(attrib)].append(value)
else:
main_responsive_dict[str(None)][str(attrib)] = []
main_responsive_dict[str(None)][str(attrib)].append(value)
else:
if str(None) in main_responsive_dict[str(None)]:
main_responsive_dict[str(None)][str(None)].append(value)
else:
main_responsive_dict[str(None)][str(None)] = []
main_responsive_dict[str(None)][str(None)].append(value)
else:
main_responsive_dict[str(main_group_attribute)][str(group_attribute)] = []
main_responsive_dict[str(main_group_attribute)][str(group_attribute)].append(value)
main_group_attribute = resolve_keys(sub_group_by, value)
group_attribute = resolve_keys(group_by, value)
if str(main_group_attribute) not in main_responsive_dict:
main_responsive_dict[str(main_group_attribute)] = {}
if str(group_attribute) in main_responsive_dict[str(main_group_attribute)]:
main_responsive_dict[str(main_group_attribute)][str(group_attribute)].append(value)
else:
main_responsive_dict[str(main_group_attribute)][str(group_attribute)] = []
main_responsive_dict[str(main_group_attribute)][str(group_attribute)].append(value)
return main_responsive_dict

View File

@ -15,17 +15,20 @@
"NEXT_PUBLIC_UNSPLASH_ACCESS",
"NEXT_PUBLIC_UNSPLASH_ENABLED",
"NEXT_PUBLIC_TRACK_EVENTS",
"TRACKER_ACCESS_KEY",
"NEXT_PUBLIC_PLAUSIBLE_DOMAIN",
"NEXT_PUBLIC_CRISP_ID",
"NEXT_PUBLIC_ENABLE_SESSION_RECORDER",
"NEXT_PUBLIC_SESSION_RECORDER_KEY",
"NEXT_PUBLIC_EXTRA_IMAGE_DOMAINS",
"NEXT_PUBLIC_SLACK_CLIENT_ID",
"NEXT_PUBLIC_SLACK_CLIENT_SECRET",
"NEXT_PUBLIC_SUPABASE_URL",
"NEXT_PUBLIC_SUPABASE_ANON_KEY",
"NEXT_PUBLIC_PLAUSIBLE_DOMAIN",
"NEXT_PUBLIC_DEPLOY_WITH_NGINX"
"NEXT_PUBLIC_DEPLOY_WITH_NGINX",
"NEXT_PUBLIC_POSTHOG_KEY",
"NEXT_PUBLIC_POSTHOG_HOST",
"SLACK_OAUTH_URL",
"SLACK_CLIENT_ID",
"SLACK_CLIENT_SECRET",
"JITSU_TRACKER_ACCESS_KEY",
"JITSU_TRACKER_HOST",
"UNSPLASH_ACCESS_KEY"
],
"pipeline": {
"build": {

View File

@ -49,7 +49,7 @@ type Props = {
};
secondaryButton?: React.ReactNode;
};
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit" | "updateDraft") => void;
handleOnDragEnd: (result: DropResult) => Promise<void>;
openIssuesListModal: (() => void) | null;
removeIssue: ((bridgeId: string, issueId: string) => void) | null;

View File

@ -19,7 +19,7 @@ type Props = {
disableUserActions: boolean;
disableAddIssueOption?: boolean;
dragDisabled: boolean;
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit" | "updateDraft") => void;
handleTrashBox: (isDragging: boolean) => void;
openIssuesListModal?: (() => void) | null;
removeIssue: ((bridgeId: string, issueId: string) => void) | null;

View File

@ -19,7 +19,12 @@ import useIssuesProperties from "hooks/use-issue-properties";
import useProjectMembers from "hooks/use-project-members";
// components
import { FiltersList, AllViews } from "components/core";
import { CreateUpdateIssueModal, DeleteIssueModal, IssuePeekOverview } from "components/issues";
import {
CreateUpdateIssueModal,
DeleteIssueModal,
IssuePeekOverview,
CreateUpdateDraftIssueModal,
} from "components/issues";
import { CreateUpdateViewModal } from "components/views";
// ui
import { PrimaryButton, SecondaryButton } from "components/ui";
@ -70,6 +75,9 @@ export const IssuesView: React.FC<Props> = ({
// trash box
const [trashBox, setTrashBox] = useState(false);
// selected draft issue
const [selectedDraftIssue, setSelectedDraftIssue] = useState<IIssue | null>(null);
const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
@ -106,6 +114,8 @@ export const IssuesView: React.FC<Props> = ({
[setDeleteIssueModal, setIssueToDelete]
);
const handleDraftIssueClick = (issue: any) => setSelectedDraftIssue(issue);
const handleOnDragEnd = useCallback(
async (result: DropResult) => {
setTrashBox(false);
@ -335,10 +345,11 @@ export const IssuesView: React.FC<Props> = ({
);
const handleIssueAction = useCallback(
(issue: IIssue, action: "copy" | "edit" | "delete") => {
(issue: IIssue, action: "copy" | "edit" | "delete" | "updateDraft") => {
if (action === "copy") makeIssueCopy(issue);
else if (action === "edit") handleEditIssue(issue);
else if (action === "delete") handleDeleteIssue(issue);
else if (action === "updateDraft") handleDraftIssueClick(issue);
},
[makeIssueCopy, handleEditIssue, handleDeleteIssue]
);
@ -451,6 +462,27 @@ export const IssuesView: React.FC<Props> = ({
...preloadedData,
}}
/>
<CreateUpdateDraftIssueModal
isOpen={selectedDraftIssue !== null}
handleClose={() => setSelectedDraftIssue(null)}
data={
selectedDraftIssue
? {
...selectedDraftIssue,
is_draft: true,
}
: null
}
fieldsToShow={[
"name",
"description",
"label",
"assignee",
"priority",
"dueDate",
"priority",
]}
/>
<CreateUpdateIssueModal
isOpen={editIssueModal && issueToEdit?.actionType !== "delete"}
handleClose={() => setEditIssueModal(false)}

View File

@ -14,7 +14,7 @@ import { ICurrentUserResponse, IIssue, IIssueViewProps, IState, UserAuth } from
type Props = {
states: IState[] | undefined;
addIssueToGroup: (groupTitle: string) => void;
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit" | "updateDraft") => void;
openIssuesListModal?: (() => void) | null;
myIssueProjectId?: string | null;
handleMyIssueOpen?: (issue: IIssue) => void;

View File

@ -61,6 +61,7 @@ type Props = {
makeIssueCopy: () => void;
removeIssue?: (() => void) | null;
handleDeleteIssue: (issue: IIssue) => void;
handleDraftIssueSelect?: (issue: IIssue) => void;
handleMyIssueOpen?: (issue: IIssue) => void;
disableUserActions: boolean;
user: ICurrentUserResponse | undefined;
@ -82,6 +83,7 @@ export const SingleListIssue: React.FC<Props> = ({
user,
userAuth,
viewProps,
handleDraftIssueSelect,
}) => {
// context menu
const [contextMenu, setContextMenu] = useState(false);
@ -90,6 +92,7 @@ export const SingleListIssue: React.FC<Props> = ({
const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId, userId } = router.query;
const isArchivedIssues = router.pathname.includes("archived-issues");
const isDraftIssues = router.pathname?.split("/")?.[4] === "draft-issues";
const { setToastAlert } = useToast();
@ -178,6 +181,8 @@ export const SingleListIssue: React.FC<Props> = ({
const issuePath = isArchivedIssues
? `/${workspaceSlug}/projects/${issue.project}/archived-issues/${issue.id}`
: isDraftIssues
? `#`
: `/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`;
const openPeekOverview = (issue: IIssue) => {
@ -247,7 +252,11 @@ export const SingleListIssue: React.FC<Props> = ({
<button
type="button"
className="truncate text-[0.825rem] text-custom-text-100"
onClick={() => openPeekOverview(issue)}
onClick={() => {
if (!isDraftIssues) openPeekOverview(issue);
if (handleDraftIssueSelect) handleDraftIssueSelect(issue);
}}
>
{issue.name}
</button>

View File

@ -39,7 +39,7 @@ type Props = {
currentState?: IState | null;
groupTitle: string;
addIssueToGroup: () => void;
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit" | "updateDraft") => void;
openIssuesListModal?: (() => void) | null;
handleMyIssueOpen?: (issue: IIssue) => void;
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
@ -253,6 +253,7 @@ export const SingleList: React.FC<Props> = ({
editIssue={() => handleIssueAction(issue, "edit")}
makeIssueCopy={() => handleIssueAction(issue, "copy")}
handleDeleteIssue={() => handleIssueAction(issue, "delete")}
handleDraftIssueSelect={() => handleIssueAction(issue, "updateDraft")}
handleMyIssueOpen={handleMyIssueOpen}
removeIssue={() => {
if (removeIssue !== null && issue.bridge_id)

View File

@ -0,0 +1,93 @@
import React, { useState } from "react";
// headless ui
import { Dialog, Transition } from "@headlessui/react";
// ui
import { SecondaryButton, PrimaryButton } from "components/ui";
type Props = {
isOpen: boolean;
handleClose: () => void;
onDiscard: () => void;
onConfirm: () => Promise<void>;
};
export const ConfirmIssueDiscard: React.FC<Props> = (props) => {
const { isOpen, handleClose, onDiscard, onConfirm } = props;
const [isLoading, setIsLoading] = useState(false);
const onClose = () => {
handleClose();
setIsLoading(false);
};
const handleDeletion = async () => {
setIsLoading(true);
await onConfirm();
setIsLoading(false);
};
return (
<Transition.Root show={isOpen} as={React.Fragment}>
<Dialog as="div" className="relative z-20" onClose={handleClose}>
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-custom-backdrop bg-opacity-50 transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-10 overflow-y-auto">
<div className="my-10 flex items-center justify-center p-4 text-center sm:p-0 md:my-32">
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative transform overflow-hidden rounded-lg border border-custom-border-200 bg-custom-background-100 text-left shadow-xl transition-all sm:my-8 sm:w-[40rem]">
<div className="px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div className="sm:flex sm:items-start">
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<Dialog.Title
as="h3"
className="text-lg font-medium leading-6 text-custom-text-100"
>
Draft Issue
</Dialog.Title>
<div className="mt-2">
<p className="text-sm text-custom-text-200">
Would you like to save this issue in drafts?
</p>
</div>
</div>
</div>
</div>
<div className="flex justify-between gap-2 p-4 sm:px-6">
<div>
<SecondaryButton onClick={onDiscard}>Discard</SecondaryButton>
</div>
<div className="space-x-2">
<SecondaryButton onClick={onClose}>Cancel</SecondaryButton>
<PrimaryButton onClick={handleDeletion} loading={isLoading}>
{isLoading ? "Saving..." : "Save Draft"}
</PrimaryButton>
</div>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
);
};

View File

@ -0,0 +1,580 @@
import React, { FC, useState, useEffect, useRef } from "react";
import { useRouter } from "next/router";
// react-hook-form
import { Controller, useForm } from "react-hook-form";
// services
import aiService from "services/ai.service";
// hooks
import useToast from "hooks/use-toast";
// components
import { GptAssistantModal } from "components/core";
import { ParentIssuesListModal } from "components/issues";
import {
IssueAssigneeSelect,
IssueDateSelect,
IssueEstimateSelect,
IssueLabelSelect,
IssuePrioritySelect,
IssueProjectSelect,
IssueStateSelect,
} from "components/issues/select";
import { CreateStateModal } from "components/states";
import { CreateLabelModal } from "components/labels";
// ui
import { CustomMenu, Input, PrimaryButton, SecondaryButton, ToggleSwitch } from "components/ui";
import { TipTapEditor } from "components/tiptap";
// icons
import { SparklesIcon, XMarkIcon } from "@heroicons/react/24/outline";
// types
import type { ICurrentUserResponse, IIssue, ISearchIssueResponse } from "types";
const defaultValues: Partial<IIssue> = {
project: "",
name: "",
description: {
type: "doc",
content: [
{
type: "paragraph",
},
],
},
description_html: "<p></p>",
estimate_point: null,
state: "",
parent: null,
priority: "none",
assignees: [],
assignees_list: [],
labels: [],
labels_list: [],
start_date: null,
target_date: null,
};
interface IssueFormProps {
handleFormSubmit: (formData: Partial<IIssue>) => Promise<void>;
data?: Partial<IIssue> | null;
prePopulatedData?: Partial<IIssue> | null;
projectId: string;
setActiveProject: React.Dispatch<React.SetStateAction<string | null>>;
createMore: boolean;
setCreateMore: React.Dispatch<React.SetStateAction<boolean>>;
handleClose: () => void;
status: boolean;
user: ICurrentUserResponse | undefined;
fieldsToShow: (
| "project"
| "name"
| "description"
| "state"
| "priority"
| "assignee"
| "label"
| "startDate"
| "dueDate"
| "estimate"
| "parent"
| "all"
)[];
}
export const DraftIssueForm: FC<IssueFormProps> = (props) => {
const {
handleFormSubmit,
data,
prePopulatedData,
projectId,
setActiveProject,
createMore,
setCreateMore,
handleClose,
status,
user,
fieldsToShow,
} = props;
const [stateModal, setStateModal] = useState(false);
const [labelModal, setLabelModal] = useState(false);
const [parentIssueListModalOpen, setParentIssueListModalOpen] = useState(false);
const [selectedParentIssue, setSelectedParentIssue] = useState<ISearchIssueResponse | null>(null);
const [gptAssistantModal, setGptAssistantModal] = useState(false);
const [iAmFeelingLucky, setIAmFeelingLucky] = useState(false);
const editorRef = useRef<any>(null);
const router = useRouter();
const { workspaceSlug } = router.query;
const { setToastAlert } = useToast();
const {
register,
formState: { errors, isSubmitting },
handleSubmit,
reset,
watch,
control,
getValues,
setValue,
setFocus,
} = useForm<IIssue>({
defaultValues: prePopulatedData ?? defaultValues,
reValidateMode: "onChange",
});
const issueName = watch("name");
const onClose = () => {
handleClose();
};
const handleCreateUpdateIssue = async (
formData: Partial<IIssue>,
action: "saveDraft" | "createToNewIssue" = "saveDraft"
) => {
await handleFormSubmit({
...formData,
is_draft: action === "saveDraft",
});
setGptAssistantModal(false);
reset({
...defaultValues,
project: projectId,
description: {
type: "doc",
content: [
{
type: "paragraph",
},
],
},
description_html: "<p></p>",
});
editorRef?.current?.clearEditor();
};
const handleAiAssistance = async (response: string) => {
if (!workspaceSlug || !projectId) return;
setValue("description", {});
setValue("description_html", `${watch("description_html")}<p>${response}</p>`);
editorRef.current?.setEditorValue(`${watch("description_html")}`);
};
const handleAutoGenerateDescription = async () => {
if (!workspaceSlug || !projectId) return;
setIAmFeelingLucky(true);
aiService
.createGptTask(
workspaceSlug as string,
projectId as string,
{
prompt: issueName,
task: "Generate a proper description for this issue.",
},
user
)
.then((res) => {
if (res.response === "")
setToastAlert({
type: "error",
title: "Error!",
message:
"Issue title isn't informative enough to generate the description. Please try with a different title.",
});
else handleAiAssistance(res.response_html);
})
.catch((err) => {
const error = err?.data?.error;
if (err.status === 429)
setToastAlert({
type: "error",
title: "Error!",
message:
error ||
"You have reached the maximum number of requests of 50 requests per month per user.",
});
else
setToastAlert({
type: "error",
title: "Error!",
message: error || "Some error occurred. Please try again.",
});
})
.finally(() => setIAmFeelingLucky(false));
};
useEffect(() => {
setFocus("name");
reset({
...defaultValues,
...(prePopulatedData ?? {}),
...(data ?? {}),
});
}, [setFocus, prePopulatedData, reset, data]);
// update projectId in form when projectId changes
useEffect(() => {
reset({
...getValues(),
project: projectId,
});
}, [getValues, projectId, reset]);
const startDate = watch("start_date");
const targetDate = watch("target_date");
const minDate = startDate ? new Date(startDate) : null;
minDate?.setDate(minDate.getDate());
const maxDate = targetDate ? new Date(targetDate) : null;
maxDate?.setDate(maxDate.getDate());
return (
<>
{projectId && (
<>
<CreateStateModal
isOpen={stateModal}
handleClose={() => setStateModal(false)}
projectId={projectId}
user={user}
/>
<CreateLabelModal
isOpen={labelModal}
handleClose={() => setLabelModal(false)}
projectId={projectId}
user={user}
onSuccess={(response) => {
setValue("labels", [...watch("labels"), response.id]);
setValue("labels_list", [...watch("labels_list"), response.id]);
}}
/>
</>
)}
<form
onSubmit={handleSubmit((formData) => handleCreateUpdateIssue(formData, "createToNewIssue"))}
>
<div className="space-y-5">
<div className="flex items-center gap-x-2">
{(fieldsToShow.includes("all") || fieldsToShow.includes("project")) && (
<Controller
control={control}
name="project"
render={({ field: { value, onChange } }) => (
<IssueProjectSelect
value={value}
onChange={(val: string) => {
onChange(val);
setActiveProject(val);
}}
/>
)}
/>
)}
<h3 className="text-xl font-semibold leading-6 text-custom-text-100">
{status ? "Update" : "Create"} Issue
</h3>
</div>
{watch("parent") &&
(fieldsToShow.includes("all") || fieldsToShow.includes("parent")) &&
selectedParentIssue && (
<div className="flex w-min items-center gap-2 whitespace-nowrap rounded bg-custom-background-80 p-2 text-xs">
<div className="flex items-center gap-2">
<span
className="block h-1.5 w-1.5 rounded-full"
style={{
backgroundColor: selectedParentIssue.state__color,
}}
/>
<span className="flex-shrink-0 text-custom-text-200">
{selectedParentIssue.project__identifier}-{selectedParentIssue.sequence_id}
</span>
<span className="truncate font-medium">
{selectedParentIssue.name.substring(0, 50)}
</span>
<XMarkIcon
className="h-3 w-3 cursor-pointer"
onClick={() => {
setValue("parent", null);
setSelectedParentIssue(null);
}}
/>
</div>
</div>
)}
<div className="space-y-3">
<div className="mt-2 space-y-3">
{(fieldsToShow.includes("all") || fieldsToShow.includes("name")) && (
<div>
<Input
id="name"
name="name"
className="resize-none text-xl"
placeholder="Title"
autoComplete="off"
error={errors.name}
register={register}
validations={{
required: "Title is required",
maxLength: {
value: 255,
message: "Title should be less than 255 characters",
},
}}
/>
</div>
)}
{(fieldsToShow.includes("all") || fieldsToShow.includes("description")) && (
<div className="relative">
<div className="flex justify-end">
{issueName && issueName !== "" && (
<button
type="button"
className={`flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-custom-background-90 ${
iAmFeelingLucky ? "cursor-wait" : ""
}`}
onClick={handleAutoGenerateDescription}
disabled={iAmFeelingLucky}
>
{iAmFeelingLucky ? (
"Generating response..."
) : (
<>
<SparklesIcon className="h-4 w-4" />I{"'"}m feeling lucky
</>
)}
</button>
)}
<button
type="button"
className="flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-custom-background-90"
onClick={() => setGptAssistantModal((prevData) => !prevData)}
>
<SparklesIcon className="h-4 w-4" />
AI
</button>
</div>
<Controller
name="description_html"
control={control}
render={({ field: { value, onChange } }) => {
if (!value && !watch("description_html")) return <></>;
return (
<TipTapEditor
workspaceSlug={workspaceSlug as string}
ref={editorRef}
debouncedUpdatesEnabled={false}
value={
!value ||
value === "" ||
(typeof value === "object" && Object.keys(value).length === 0)
? watch("description_html")
: value
}
customClassName="min-h-[150px]"
onChange={(description: Object, description_html: string) => {
onChange(description_html);
setValue("description", description);
}}
/>
);
}}
/>
<GptAssistantModal
isOpen={gptAssistantModal}
handleClose={() => {
setGptAssistantModal(false);
// this is done so that the title do not reset after gpt popover closed
reset(getValues());
}}
inset="top-2 left-0"
content=""
htmlContent={watch("description_html")}
onResponse={(response) => {
handleAiAssistance(response);
}}
projectId={projectId}
/>
</div>
)}
<div className="flex flex-wrap items-center gap-2">
{(fieldsToShow.includes("all") || fieldsToShow.includes("state")) && (
<Controller
control={control}
name="state"
render={({ field: { value, onChange } }) => (
<IssueStateSelect
setIsOpen={setStateModal}
value={value}
onChange={onChange}
projectId={projectId}
/>
)}
/>
)}
{(fieldsToShow.includes("all") || fieldsToShow.includes("priority")) && (
<Controller
control={control}
name="priority"
render={({ field: { value, onChange } }) => (
<IssuePrioritySelect value={value} onChange={onChange} />
)}
/>
)}
{(fieldsToShow.includes("all") || fieldsToShow.includes("assignee")) && (
<Controller
control={control}
name="assignees"
render={({ field: { value, onChange } }) => (
<IssueAssigneeSelect
projectId={projectId}
value={value}
onChange={onChange}
/>
)}
/>
)}
{(fieldsToShow.includes("all") || fieldsToShow.includes("label")) && (
<Controller
control={control}
name="labels"
render={({ field: { value, onChange } }) => (
<IssueLabelSelect
setIsOpen={setLabelModal}
value={value}
onChange={onChange}
projectId={projectId}
/>
)}
/>
)}
{(fieldsToShow.includes("all") || fieldsToShow.includes("startDate")) && (
<div>
<Controller
control={control}
name="start_date"
render={({ field: { value, onChange } }) => (
<IssueDateSelect
label="Start date"
maxDate={maxDate ?? undefined}
onChange={onChange}
value={value}
/>
)}
/>
</div>
)}
{(fieldsToShow.includes("all") || fieldsToShow.includes("dueDate")) && (
<div>
<Controller
control={control}
name="target_date"
render={({ field: { value, onChange } }) => (
<IssueDateSelect
label="Due date"
minDate={minDate ?? undefined}
onChange={onChange}
value={value}
/>
)}
/>
</div>
)}
{(fieldsToShow.includes("all") || fieldsToShow.includes("estimate")) && (
<div>
<Controller
control={control}
name="estimate_point"
render={({ field: { value, onChange } }) => (
<IssueEstimateSelect value={value} onChange={onChange} />
)}
/>
</div>
)}
{(fieldsToShow.includes("all") || fieldsToShow.includes("parent")) && (
<Controller
control={control}
name="parent"
render={({ field: { onChange } }) => (
<ParentIssuesListModal
isOpen={parentIssueListModalOpen}
handleClose={() => setParentIssueListModalOpen(false)}
onChange={(issue) => {
onChange(issue.id);
setSelectedParentIssue(issue);
}}
projectId={projectId}
/>
)}
/>
)}
{(fieldsToShow.includes("all") || fieldsToShow.includes("parent")) && (
<CustomMenu ellipsis>
{watch("parent") ? (
<>
<CustomMenu.MenuItem
renderAs="button"
onClick={() => setParentIssueListModalOpen(true)}
>
Change parent issue
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
renderAs="button"
onClick={() => setValue("parent", null)}
>
Remove parent issue
</CustomMenu.MenuItem>
</>
) : (
<CustomMenu.MenuItem
renderAs="button"
onClick={() => setParentIssueListModalOpen(true)}
>
Select Parent Issue
</CustomMenu.MenuItem>
)}
</CustomMenu>
)}
</div>
</div>
</div>
</div>
<div className="-mx-5 mt-5 flex items-center justify-between gap-2 border-t border-custom-border-200 px-5 pt-5">
<div
className="flex cursor-pointer items-center gap-1"
onClick={() => setCreateMore((prevData) => !prevData)}
>
<span className="text-xs">Create more</span>
<ToggleSwitch value={createMore} onChange={() => {}} size="md" />
</div>
<div className="flex items-center gap-2">
<SecondaryButton onClick={onClose}>Discard</SecondaryButton>
<SecondaryButton
loading={isSubmitting}
onClick={handleSubmit((formData) => handleCreateUpdateIssue(formData, "saveDraft"))}
>
{isSubmitting ? "Saving..." : "Save Draft"}
</SecondaryButton>
{data && (
<PrimaryButton type="submit" loading={isSubmitting}>
{isSubmitting ? "Saving..." : "Add Issue"}
</PrimaryButton>
)}
</div>
</div>
</form>
</>
);
};

View File

@ -0,0 +1,285 @@
import React, { useEffect, useState } from "react";
import { useRouter } from "next/router";
import { mutate } from "swr";
// headless ui
import { Dialog, Transition } from "@headlessui/react";
// services
import issuesService from "services/issues.service";
// hooks
import useUser from "hooks/use-user";
import useIssuesView from "hooks/use-issues-view";
import useCalendarIssuesView from "hooks/use-calendar-issues-view";
import useToast from "hooks/use-toast";
import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view";
import useProjects from "hooks/use-projects";
import useMyIssues from "hooks/my-issues/use-my-issues";
// components
import { DraftIssueForm } from "components/issues";
// types
import type { IIssue } from "types";
// fetch-keys
import {
PROJECT_ISSUES_DETAILS,
USER_ISSUE,
SUB_ISSUES,
PROJECT_ISSUES_LIST_WITH_PARAMS,
CYCLE_ISSUES_WITH_PARAMS,
MODULE_ISSUES_WITH_PARAMS,
VIEW_ISSUES,
PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS,
} from "constants/fetch-keys";
interface IssuesModalProps {
data?: IIssue | null;
handleClose: () => void;
isOpen: boolean;
isUpdatingSingleIssue?: boolean;
prePopulateData?: Partial<IIssue>;
fieldsToShow?: (
| "project"
| "name"
| "description"
| "state"
| "priority"
| "assignee"
| "label"
| "startDate"
| "dueDate"
| "estimate"
| "parent"
| "all"
)[];
onSubmit?: (data: Partial<IIssue>) => Promise<void> | void;
}
export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = ({
data,
handleClose,
isOpen,
isUpdatingSingleIssue = false,
prePopulateData,
fieldsToShow = ["all"],
onSubmit,
}) => {
// states
const [createMore, setCreateMore] = useState(false);
const [activeProject, setActiveProject] = useState<string | null>(null);
const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
const { displayFilters, params } = useIssuesView();
const { params: calendarParams } = useCalendarIssuesView();
const { ...viewGanttParams } = params;
const { params: spreadsheetParams } = useSpreadsheetIssuesView();
const { user } = useUser();
const { projects } = useProjects();
const { groupedIssues, mutateMyIssues } = useMyIssues(workspaceSlug?.toString());
const { setToastAlert } = useToast();
if (cycleId) prePopulateData = { ...prePopulateData, cycle: cycleId as string };
if (moduleId) prePopulateData = { ...prePopulateData, module: moduleId as string };
if (router.asPath.includes("my-issues") || router.asPath.includes("assigned"))
prePopulateData = {
...prePopulateData,
assignees: [...(prePopulateData?.assignees ?? []), user?.id ?? ""],
};
const onClose = () => {
handleClose();
setActiveProject(null);
};
useEffect(() => {
// if modal is closed, reset active project to null
// and return to avoid activeProject being set to some other project
if (!isOpen) {
setActiveProject(null);
return;
}
// if data is present, set active project to the project of the
// issue. This has more priority than the project in the url.
if (data && data.project) {
setActiveProject(data.project);
return;
}
// if data is not present, set active project to the project
// in the url. This has the least priority.
if (projects && projects.length > 0 && !activeProject)
setActiveProject(projects?.find((p) => p.id === projectId)?.id ?? projects?.[0].id ?? null);
}, [activeProject, data, projectId, projects, isOpen]);
const calendarFetchKey = cycleId
? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), calendarParams)
: moduleId
? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), calendarParams)
: viewId
? VIEW_ISSUES(viewId.toString(), calendarParams)
: PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject?.toString() ?? "", calendarParams);
const spreadsheetFetchKey = cycleId
? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), spreadsheetParams)
: moduleId
? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), spreadsheetParams)
: viewId
? VIEW_ISSUES(viewId.toString(), spreadsheetParams)
: PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject?.toString() ?? "", spreadsheetParams);
const ganttFetchKey = cycleId
? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString())
: moduleId
? MODULE_ISSUES_WITH_PARAMS(moduleId.toString())
: viewId
? VIEW_ISSUES(viewId.toString(), viewGanttParams)
: PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject?.toString() ?? "");
const createIssue = async (payload: Partial<IIssue>) => {
if (!workspaceSlug || !activeProject || !user) return;
await issuesService
.createDraftIssue(workspaceSlug as string, activeProject ?? "", payload, user)
.then(async () => {
mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params));
mutate(PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params));
if (displayFilters.layout === "calendar") mutate(calendarFetchKey);
if (displayFilters.layout === "gantt_chart")
mutate(ganttFetchKey, {
start_target_date: true,
order_by: "sort_order",
});
if (displayFilters.layout === "spreadsheet") mutate(spreadsheetFetchKey);
if (groupedIssues) mutateMyIssues();
setToastAlert({
type: "success",
title: "Success!",
message: "Issue created successfully.",
});
if (payload.assignees_list?.some((assignee) => assignee === user?.id))
mutate(USER_ISSUE(workspaceSlug as string));
if (payload.parent && payload.parent !== "") mutate(SUB_ISSUES(payload.parent));
})
.catch(() => {
setToastAlert({
type: "error",
title: "Error!",
message: "Issue could not be created. Please try again.",
});
});
if (!createMore) onClose();
};
const updateIssue = async (payload: Partial<IIssue>) => {
if (!user) return;
await issuesService
.updateDraftIssue(workspaceSlug as string, activeProject ?? "", data?.id ?? "", payload, user)
.then((res) => {
if (isUpdatingSingleIssue) {
mutate<IIssue>(PROJECT_ISSUES_DETAILS, (prevData) => ({ ...prevData, ...res }), false);
} else {
if (displayFilters.layout === "calendar") mutate(calendarFetchKey);
if (displayFilters.layout === "spreadsheet") mutate(spreadsheetFetchKey);
if (payload.parent) mutate(SUB_ISSUES(payload.parent.toString()));
mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params));
mutate(PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params));
}
if (!createMore) onClose();
setToastAlert({
type: "success",
title: "Success!",
message: "Issue updated successfully.",
});
})
.catch(() => {
setToastAlert({
type: "error",
title: "Error!",
message: "Issue could not be updated. Please try again.",
});
});
};
const handleFormSubmit = async (formData: Partial<IIssue>) => {
if (!workspaceSlug || !activeProject) return;
const payload: Partial<IIssue> = {
...formData,
assignees_list: formData.assignees ?? [],
labels_list: formData.labels ?? [],
description: formData.description ?? "",
description_html: formData.description_html ?? "<p></p>",
};
if (!data) await createIssue(payload);
else await updateIssue(payload);
if (onSubmit) await onSubmit(payload);
};
if (!projects || projects.length === 0) return null;
return (
<>
<Transition.Root show={isOpen} as={React.Fragment}>
<Dialog as="div" className="relative z-20" onClose={onClose}>
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-custom-backdrop bg-opacity-50 transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-10 overflow-y-auto">
<div className="my-10 flex items-center justify-center p-4 text-center sm:p-0 md:my-20">
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative transform rounded-lg border border-custom-border-200 bg-custom-background-100 p-5 text-left shadow-xl transition-all sm:w-full sm:max-w-2xl">
<DraftIssueForm
handleFormSubmit={handleFormSubmit}
prePopulatedData={prePopulateData}
data={data}
createMore={createMore}
setCreateMore={setCreateMore}
handleClose={onClose}
projectId={activeProject ?? ""}
setActiveProject={setActiveProject}
status={data ? true : false}
user={user}
fieldsToShow={fieldsToShow}
/>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
</>
);
};

View File

@ -8,6 +8,7 @@ import { Controller, useForm } from "react-hook-form";
import aiService from "services/ai.service";
// hooks
import useToast from "hooks/use-toast";
import useLocalStorage from "hooks/use-local-storage";
// components
import { GptAssistantModal } from "components/core";
import { ParentIssuesListModal } from "components/issues";
@ -62,8 +63,11 @@ export interface IssueFormProps {
createMore: boolean;
setCreateMore: React.Dispatch<React.SetStateAction<boolean>>;
handleClose: () => void;
handleDiscardClose: () => void;
status: boolean;
user: ICurrentUserResponse | undefined;
setIsConfirmDiscardOpen: React.Dispatch<React.SetStateAction<boolean>>;
handleFormDirty: (payload: Partial<IIssue> | null) => void;
fieldsToShow: (
| "project"
| "name"
@ -80,18 +84,21 @@ export interface IssueFormProps {
)[];
}
export const IssueForm: FC<IssueFormProps> = ({
handleFormSubmit,
initialData,
projectId,
setActiveProject,
createMore,
setCreateMore,
handleClose,
status,
user,
fieldsToShow,
}) => {
export const IssueForm: FC<IssueFormProps> = (props) => {
const {
handleFormSubmit,
initialData,
projectId,
setActiveProject,
createMore,
setCreateMore,
handleDiscardClose,
status,
user,
fieldsToShow,
handleFormDirty,
} = props;
const [stateModal, setStateModal] = useState(false);
const [labelModal, setLabelModal] = useState(false);
const [parentIssueListModalOpen, setParentIssueListModalOpen] = useState(false);
@ -100,6 +107,8 @@ export const IssueForm: FC<IssueFormProps> = ({
const [gptAssistantModal, setGptAssistantModal] = useState(false);
const [iAmFeelingLucky, setIAmFeelingLucky] = useState(false);
const { setValue: setValueInLocalStorage } = useLocalStorage<any>("draftedIssue", null);
const editorRef = useRef<any>(null);
const router = useRouter();
@ -109,7 +118,7 @@ export const IssueForm: FC<IssueFormProps> = ({
const {
register,
formState: { errors, isSubmitting },
formState: { errors, isSubmitting, isDirty },
handleSubmit,
reset,
watch,
@ -124,6 +133,17 @@ export const IssueForm: FC<IssueFormProps> = ({
const issueName = watch("name");
const payload = {
name: getValues("name"),
description: getValues("description"),
};
useEffect(() => {
if (isDirty) handleFormDirty(payload);
else handleFormDirty(null);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [JSON.stringify(payload), isDirty]);
const handleCreateUpdateIssue = async (formData: Partial<IIssue>) => {
await handleFormSubmit(formData);
@ -543,7 +563,15 @@ export const IssueForm: FC<IssueFormProps> = ({
<ToggleSwitch value={createMore} onChange={() => {}} size="md" />
</div>
<div className="flex items-center gap-2">
<SecondaryButton onClick={handleClose}>Discard</SecondaryButton>
<SecondaryButton
onClick={() => {
const data = JSON.stringify(getValues());
setValueInLocalStorage(data);
handleDiscardClose();
}}
>
Discard
</SecondaryButton>
<PrimaryButton type="submit" loading={isSubmitting}>
{status
? isSubmitting

View File

@ -16,3 +16,6 @@ export * from "./sub-issues-list";
export * from "./label";
export * from "./issue-reaction";
export * from "./peek-overview";
export * from "./confirm-issue-discard";
export * from "./draft-issue-form";
export * from "./draft-issue-modal";

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState, useCallback } from "react";
import React, { useEffect, useState } from "react";
import { useRouter } from "next/router";
@ -20,7 +20,7 @@ import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view";
import useProjects from "hooks/use-projects";
import useMyIssues from "hooks/my-issues/use-my-issues";
// components
import { IssueForm } from "components/issues";
import { IssueForm, ConfirmIssueDiscard } from "components/issues";
// types
import type { IIssue } from "types";
// fetch-keys
@ -35,6 +35,7 @@ import {
MODULE_DETAILS,
VIEW_ISSUES,
INBOX_ISSUES,
PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS,
} from "constants/fetch-keys";
// constants
import { INBOX_ISSUE_SOURCE } from "constants/inbox";
@ -73,6 +74,8 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
}) => {
// states
const [createMore, setCreateMore] = useState(false);
const [formDirtyState, setFormDirtyState] = useState<any>(null);
const [showConfirmDiscard, setShowConfirmDiscard] = useState(false);
const [activeProject, setActiveProject] = useState<string | null>(null);
const router = useRouter();
@ -80,7 +83,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
const { displayFilters, params } = useIssuesView();
const { params: calendarParams } = useCalendarIssuesView();
const { order_by, group_by, ...viewGanttParams } = params;
const { ...viewGanttParams } = params;
const { params: inboxParams } = useInboxView();
const { params: spreadsheetParams } = useSpreadsheetIssuesView();
@ -99,10 +102,23 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
assignees: [...(prePopulateData?.assignees ?? []), user?.id ?? ""],
};
const onClose = useCallback(() => {
const onClose = () => {
if (formDirtyState !== null) {
setShowConfirmDiscard(true);
} else {
handleClose();
setActiveProject(null);
}
};
const onDiscardClose = () => {
handleClose();
setActiveProject(null);
}, [handleClose]);
};
const handleFormDirty = (data: any) => {
setFormDirtyState(data);
};
useEffect(() => {
// if modal is closed, reset active project to null
@ -275,10 +291,50 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
});
});
if (!createMore) onClose();
if (!createMore) onDiscardClose();
};
const createDraftIssue = async () => {
if (!workspaceSlug || !activeProject || !user) return;
const payload: Partial<IIssue> = {
...formDirtyState,
};
await issuesService
.createDraftIssue(workspaceSlug as string, activeProject ?? "", payload, user)
.then(() => {
mutate(PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params));
if (groupedIssues) mutateMyIssues();
setToastAlert({
type: "success",
title: "Success!",
message: "Draft Issue created successfully.",
});
handleClose();
setActiveProject(null);
setFormDirtyState(null);
setShowConfirmDiscard(false);
if (payload.assignees_list?.some((assignee) => assignee === user?.id))
mutate(USER_ISSUE(workspaceSlug as string));
if (payload.parent && payload.parent !== "") mutate(SUB_ISSUES(payload.parent));
})
.catch(() => {
setToastAlert({
type: "error",
title: "Error!",
message: "Issue could not be created. Please try again.",
});
});
};
const updateIssue = async (payload: Partial<IIssue>) => {
if (!user) return;
await issuesService
.patchIssue(workspaceSlug as string, activeProject ?? "", data?.id ?? "", payload, user)
.then((res) => {
@ -294,7 +350,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
if (payload.cycle && payload.cycle !== "") addIssueToCycle(res.id, payload.cycle);
if (payload.module && payload.module !== "") addIssueToModule(res.id, payload.module);
if (!createMore) onClose();
if (!createMore) onDiscardClose();
setToastAlert({
type: "success",
@ -331,49 +387,66 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
if (!projects || projects.length === 0) return null;
return (
<Transition.Root show={isOpen} as={React.Fragment}>
<Dialog as="div" className="relative z-20" onClose={onClose}>
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-custom-backdrop bg-opacity-50 transition-opacity" />
</Transition.Child>
<>
<ConfirmIssueDiscard
isOpen={showConfirmDiscard}
handleClose={() => setShowConfirmDiscard(false)}
onConfirm={createDraftIssue}
onDiscard={() => {
handleClose();
setActiveProject(null);
setFormDirtyState(null);
setShowConfirmDiscard(false);
}}
/>
<div className="fixed inset-0 z-10 overflow-y-auto">
<div className="my-10 flex items-center justify-center p-4 text-center sm:p-0 md:my-20">
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative transform rounded-lg border border-custom-border-200 bg-custom-background-100 p-5 text-left shadow-xl transition-all sm:w-full sm:max-w-2xl">
<IssueForm
handleFormSubmit={handleFormSubmit}
initialData={data ?? prePopulateData}
createMore={createMore}
setCreateMore={setCreateMore}
handleClose={onClose}
projectId={activeProject ?? ""}
setActiveProject={setActiveProject}
status={data ? true : false}
user={user}
fieldsToShow={fieldsToShow}
/>
</Dialog.Panel>
</Transition.Child>
<Transition.Root show={isOpen} as={React.Fragment}>
<Dialog as="div" className="relative z-20" onClose={onClose}>
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-custom-backdrop bg-opacity-50 transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-10 overflow-y-auto">
<div className="my-10 flex items-center justify-center p-4 text-center sm:p-0 md:my-20">
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative transform rounded-lg border border-custom-border-200 bg-custom-background-100 p-5 text-left shadow-xl transition-all sm:w-full sm:max-w-2xl">
<IssueForm
handleFormSubmit={handleFormSubmit}
initialData={data ?? prePopulateData}
createMore={createMore}
setCreateMore={setCreateMore}
handleClose={onClose}
handleDiscardClose={onDiscardClose}
setIsConfirmDiscardOpen={setShowConfirmDiscard}
projectId={activeProject ?? ""}
setActiveProject={setActiveProject}
status={data ? true : false}
user={user}
fieldsToShow={fieldsToShow}
handleFormDirty={handleFormDirty}
/>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</div>
</Dialog>
</Transition.Root>
</Dialog>
</Transition.Root>
</>
);
};

View File

@ -205,7 +205,7 @@ export const MyIssuesView: React.FC<Props> = ({
);
const handleIssueAction = useCallback(
(issue: IIssue, action: "copy" | "edit" | "delete") => {
(issue: IIssue, action: "copy" | "edit" | "delete" | "updateDraft") => {
if (action === "copy") makeIssueCopy(issue);
else if (action === "edit") handleEditIssue(issue);
else if (action === "delete") handleDeleteIssue(issue);

View File

@ -204,7 +204,7 @@ export const ProfileIssuesView = () => {
);
const handleIssueAction = useCallback(
(issue: IIssue, action: "copy" | "edit" | "delete") => {
(issue: IIssue, action: "copy" | "edit" | "delete" | "updateDraft") => {
if (action === "copy") makeIssueCopy(issue);
else if (action === "edit") handleEditIssue(issue);
else if (action === "delete") handleDeleteIssue(issue);

View File

@ -25,6 +25,7 @@ import {
PhotoFilterOutlined,
SettingsOutlined,
} from "@mui/icons-material";
import { PenSquare } from "lucide-react";
// helpers
import { renderEmoji } from "helpers/emoji.helper";
// types
@ -288,6 +289,16 @@ export const SingleSidebarProject: React.FC<Props> = observer((props) => {
</div>
</CustomMenu.MenuItem>
)}
<CustomMenu.MenuItem
onClick={() =>
router.push(`/${workspaceSlug}/projects/${project?.id}/draft-issues`)
}
>
<div className="flex items-center justify-start gap-2">
<PenSquare className="!text-base !leading-4 w-[14px] h-[14px] text-custom-text-300" />
<span>Draft Issues</span>
</div>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
onClick={() => router.push(`/${workspaceSlug}/projects/${project?.id}/settings`)}
>

View File

@ -1,8 +1,6 @@
import { Fragment } from "react";
import { useRouter } from "next/router";
import Link from "next/link";
// headless ui
import { Menu, Transition } from "@headlessui/react";
// next-themes
@ -63,8 +61,6 @@ export const WorkspaceSidebarDropdown = () => {
const { user, mutateUser } = useUser();
const { collapsed: sidebarCollapse } = useThemeHook();
const { setTheme } = useTheme();
const { setToastAlert } = useToast();
@ -155,7 +151,7 @@ export const WorkspaceSidebarDropdown = () => {
{workspaces.length > 0 ? (
workspaces.map((workspace) => (
<Menu.Item key={workspace.id}>
{({ active }) => (
{() => (
<button
type="button"
onClick={() => handleWorkspaceNavigation(workspace)}

View File

@ -1,13 +1,10 @@
import React from "react";
import Link from "next/link";
import { useRouter } from "next/router";
// hooks
import useTheme from "hooks/use-theme";
// components
import { NotificationPopover } from "components/notifications";
// ui
import { Tooltip } from "components/ui";
// icons
import {

View File

@ -1,47 +1,142 @@
import React from "react";
import React, { useState } from "react";
// ui
import { Icon } from "components/ui";
import { ChevronDown, PenSquare } from "lucide-react";
// headless ui
import { Menu, Transition } from "@headlessui/react";
// hooks
import useLocalStorage from "hooks/use-local-storage";
// components
import { CreateUpdateDraftIssueModal } from "components/issues";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
export const WorkspaceSidebarQuickAction = () => {
const store: any = useMobxStore();
return (
<div
className={`flex items-center justify-between w-full cursor-pointer px-4 mt-4 ${
store?.theme?.sidebarCollapsed ? "flex-col gap-1" : "gap-2"
}`}
>
<button
className={`flex items-center gap-2 flex-grow rounded flex-shrink-0 py-1.5 ${
store?.theme?.sidebarCollapsed
? "px-2 hover:bg-custom-sidebar-background-80"
: "px-3 shadow border-[0.5px] border-custom-border-300"
}`}
onClick={() => {
const e = new KeyboardEvent("keydown", { key: "c" });
document.dispatchEvent(e);
}}
>
<Icon iconName="edit_square" className="!text-lg !leading-4 text-custom-sidebar-text-300" />
{!store?.theme?.sidebarCollapsed && <span className="text-sm font-medium">New Issue</span>}
</button>
const [isDraftIssueModalOpen, setIsDraftIssueModalOpen] = useState(false);
<button
className={`flex items-center justify-center rounded flex-shrink-0 p-2 ${
store?.theme?.sidebarCollapsed
? "hover:bg-custom-sidebar-background-80"
: "shadow border-[0.5px] border-custom-border-300"
}`}
onClick={() => {
const e = new KeyboardEvent("keydown", { key: "k", ctrlKey: true, metaKey: true });
document.dispatchEvent(e);
const { storedValue, clearValue } = useLocalStorage<any>("draftedIssue", null);
return (
<>
<CreateUpdateDraftIssueModal
isOpen={isDraftIssueModalOpen}
handleClose={() => setIsDraftIssueModalOpen(false)}
prePopulateData={storedValue ? JSON.parse(storedValue) : {}}
onSubmit={() => {
localStorage.removeItem("draftedIssue");
clearValue();
setIsDraftIssueModalOpen(false);
}}
fieldsToShow={[
"name",
"description",
"label",
"assignee",
"priority",
"dueDate",
"priority",
]}
/>
<div
className={`relative flex items-center justify-between w-full cursor-pointer px-4 mt-4 ${
store?.theme?.sidebarCollapsed ? "flex-col gap-1" : "gap-2"
}`}
>
<Icon iconName="search" className="!text-lg !leading-4 text-custom-sidebar-text-300" />
</button>
</div>
<div
className={`flex items-center justify-between w-full rounded cursor-pointer px-4 gap-1 ${
store?.theme?.sidebarCollapsed
? "px-2 hover:bg-custom-sidebar-background-80"
: "px-3 shadow border-[0.5px] border-custom-border-300"
}`}
>
<button
type="button"
className="flex items-center gap-2 flex-grow rounded flex-shrink-0 py-1.5"
onClick={() => {
const e = new KeyboardEvent("keydown", { key: "c" });
document.dispatchEvent(e);
}}
>
<Icon
iconName="edit_square"
className="!text-lg !leading-4 text-custom-sidebar-text-300"
/>
{!store?.theme?.sidebarCollapsed && (
<span className="text-sm font-medium">New Issue</span>
)}
</button>
{storedValue && <div className="h-8 w-0.5 bg-custom-sidebar-background-80" />}
{storedValue && (
<div className="relative">
<Menu as={React.Fragment}>
{({ open }) => (
<>
<div>
<Menu.Button
type="button"
className={`flex items-center justify-center rounded flex-shrink-0 p-2 ${
open ? "rotate-180 pl-0" : "rotate-0 pr-0"
}`}
>
<ChevronDown
size={16}
className="!text-custom-sidebar-text-300 transform transition-transform duration-300"
/>
</Menu.Button>
</div>
<Transition
as={React.Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="absolute -right-4 mt-1 w-52 bg-custom-background-300">
<div className="px-1 py-1 ">
<Menu.Item>
<button
onClick={() => setIsDraftIssueModalOpen(true)}
className="w-full flex text-sm items-center rounded flex-shrink-0 py-[10px] px-3 bg-custom-background-100 shadow border-[0.5px] border-custom-border-300 text-custom-text-300"
>
<PenSquare
size={16}
className="!text-lg !leading-4 text-custom-sidebar-text-300 mx-2"
/>
Last Drafted Issue
</button>
</Menu.Item>
</div>
</Menu.Items>
</Transition>
</>
)}
</Menu>
</div>
)}
</div>
<button
className={`flex items-center justify-center rounded flex-shrink-0 p-2 ${
store?.theme?.sidebarCollapsed
? "hover:bg-custom-sidebar-background-80"
: "shadow border-[0.5px] border-custom-border-300"
}`}
onClick={() => {
const e = new KeyboardEvent("keydown", { key: "k", ctrlKey: true, metaKey: true });
document.dispatchEvent(e);
}}
>
<Icon iconName="search" className="!text-lg !leading-4 text-custom-sidebar-text-300" />
</button>
</div>
</>
);
};

View File

@ -52,7 +52,7 @@ const SingleInvitation: React.FC<Props> = ({
? "bg-custom-background-80 text-custom-text-200"
: "bg-custom-primary text-white"
} text-sm px-4 py-2 border border-custom-border-200 rounded-3xl`}
onClick={(e) => {
onClick={() => {
handleInvitation(
invitation,
invitationsRespond.includes(invitation.id) ? "withdraw" : "accepted"

View File

@ -140,6 +140,15 @@ export const PROJECT_ARCHIVED_ISSUES_LIST_WITH_PARAMS = (projectId: string, para
return `PROJECT_ARCHIVED_ISSUES_LIST_WITH_PARAMS_${projectId.toUpperCase()}_${paramsKey}`;
};
export const PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS = (projectId: string, params?: any) => {
if (!params) return `PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS${projectId.toUpperCase()}`;
const paramsKey = paramsToKey(params);
return `PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS${projectId.toUpperCase()}_${paramsKey}`;
};
export const PROJECT_ISSUES_DETAILS = (issueId: string) =>
`PROJECT_ISSUES_DETAILS_${issueId.toUpperCase()}`;
export const PROJECT_ISSUES_PROPERTIES = (projectId: string) =>

View File

@ -20,6 +20,7 @@ import {
CYCLE_ISSUES_WITH_PARAMS,
MODULE_ISSUES_WITH_PARAMS,
PROJECT_ARCHIVED_ISSUES_LIST_WITH_PARAMS,
PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS,
PROJECT_ISSUES_LIST_WITH_PARAMS,
STATES_LIST,
VIEW_ISSUES,
@ -38,6 +39,7 @@ const useIssuesView = () => {
const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId, viewId, archivedIssueId } = router.query;
const isArchivedIssues = router.pathname.includes("archived-issues");
const isDraftIssues = router.pathname.includes("draft-issues");
const params: any = {
order_by: displayFilters?.order_by,
@ -72,6 +74,15 @@ const useIssuesView = () => {
: null
);
const { data: draftIssues, mutate: mutateDraftIssues } = useSWR(
workspaceSlug && projectId && params && isDraftIssues && !archivedIssueId
? PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS(projectId as string, params)
: null,
workspaceSlug && projectId && params && isDraftIssues && !archivedIssueId
? () => issuesService.getDraftIssues(workspaceSlug as string, projectId as string, params)
: null
);
const { data: cycleIssues, mutate: mutateCycleIssues } = useSWR(
workspaceSlug && projectId && cycleId && params
? CYCLE_ISSUES_WITH_PARAMS(cycleId as string, params)
@ -151,6 +162,8 @@ const useIssuesView = () => {
? viewIssues
: isArchivedIssues
? projectArchivedIssues
: isDraftIssues
? draftIssues
: projectIssues;
if (Array.isArray(issuesToGroup)) return { allIssues: issuesToGroup };
@ -169,6 +182,8 @@ const useIssuesView = () => {
moduleId,
viewId,
isArchivedIssues,
isDraftIssues,
draftIssues,
emptyStatesObject,
]);
@ -191,6 +206,8 @@ const useIssuesView = () => {
? mutateViewIssues
: isArchivedIssues
? mutateProjectArchivedIssues
: isDraftIssues
? mutateDraftIssues
: mutateProjectIssues,
filters,
setFilters,

View File

@ -3,8 +3,6 @@ import { useEffect } from "react";
import { useTheme } from "next-themes";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// theme helpers
import { applyTheme, unsetCustomCssVariables } from "helpers/theme.helper";
const MobxStoreInit = () => {
const store: any = useMobxStore();

View File

@ -2,6 +2,7 @@ require("dotenv").config({ path: ".env" });
const { withSentryConfig } = require("@sentry/nextjs");
const path = require("path");
const extraImageDomains = (process.env.NEXT_PUBLIC_EXTRA_IMAGE_DOMAINS ?? "")
.split(",")
.filter((domain) => domain.length > 0);

View File

@ -97,8 +97,8 @@
"eslint-config-custom": "*",
"eslint-config-next": "12.2.2",
"prettier": "^2.8.7",
"tsconfig": "*",
"tailwind-config-custom": "*",
"tsconfig": "*",
"typescript": "4.7.4"
},
"resolutions": {

View File

@ -15,17 +15,14 @@ import { ICustomTheme } from "types";
import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// next themes
import { useTheme } from "next-themes";
const ProfilePreferences = observer(() => {
const { user: myProfile } = useUserAuth();
const store: any = useMobxStore();
const { theme } = useTheme();
console.log("store", store?.theme?.theme);
console.log("theme", theme);
// console.log("store", store?.theme?.theme);
// console.log("theme", theme);
const [customThemeSelectorOptions, setCustomThemeSelectorOptions] = useState(false);

View File

@ -0,0 +1,73 @@
import { useRouter } from "next/router";
import useSWR from "swr";
// services
import projectService from "services/project.service";
// layouts
import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
// contexts
import { IssueViewContextProvider } from "contexts/issue-view.context";
// helper
import { truncateText } from "helpers/string.helper";
// components
import { IssuesFilterView, IssuesView } from "components/core";
// ui
import { Icon } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// icons
import { X, PenSquare } from "lucide-react";
// types
import type { NextPage } from "next";
// fetch-keys
import { PROJECT_DETAILS } from "constants/fetch-keys";
const ProjectDraftIssues: NextPage = () => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { data: projectDetails } = useSWR(
workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
workspaceSlug && projectId
? () => projectService.getProject(workspaceSlug as string, projectId as string)
: null
);
return (
<IssueViewContextProvider>
<ProjectAuthorizationWrapper
breadcrumbs={
<Breadcrumbs>
<BreadcrumbItem title="Projects" link={`/${workspaceSlug}/projects`} />
<BreadcrumbItem
title={`${truncateText(projectDetails?.name ?? "Project", 32)} Draft Issues`}
/>
</Breadcrumbs>
}
right={
<div className="flex items-center gap-2">
<IssuesFilterView />
</div>
}
>
<div className="h-full w-full flex flex-col">
<div className="flex items-center ga-1 px-4 py-2.5 shadow-sm border-b border-custom-border-200">
<button
type="button"
onClick={() => router.push(`/${workspaceSlug}/projects/${projectId}/issues/`)}
className="flex items-center gap-1.5 rounded-full border border-custom-border-200 px-3 py-1.5 text-xs"
>
<PenSquare className="h-3 w-3 text-custom-text-300" />
<span>Draft Issues</span>
<X className="h-3 w-3" />
</button>
</div>
<IssuesView />
</div>
</ProjectAuthorizationWrapper>
</IssueViewContextProvider>
);
};
export default ProjectDraftIssues;

View File

@ -1,38 +1,22 @@
// next imports
import Head from "next/head";
import dynamic from "next/dynamic";
import Router from "next/router";
// themes
import { ThemeProvider } from "next-themes";
import NProgress from "nprogress";
// styles
import "styles/globals.css";
import "styles/editor.css";
import "styles/command-pallette.css";
import "styles/nprogress.css";
import "styles/react-datepicker.css";
// nprogress
import NProgress from "nprogress";
// contexts
import { UserProvider } from "contexts/user.context";
import { ToastContextProvider } from "contexts/toast.context";
import { ThemeContextProvider } from "contexts/theme.context";
// types
import type { AppProps } from "next/app";
// constants
import { THEMES } from "constants/themes";
// constants
import {
SITE_NAME,
SITE_DESCRIPTION,
SITE_URL,
TWITTER_USER_NAME,
SITE_KEYWORDS,
SITE_TITLE,
} from "constants/seo-variables";
import { SITE_TITLE } from "constants/seo-variables";
// mobx store provider
import { MobxStoreProvider } from "lib/mobx/store-provider";
import MobxStoreInit from "lib/mobx/store-init";
@ -47,33 +31,20 @@ Router.events.on("routeChangeComplete", NProgress.done);
function MyApp({ Component, pageProps }: AppProps) {
return (
// <UserProvider>
// mobx root provider
<MobxStoreProvider {...pageProps}>
<ThemeProvider themes={THEMES} defaultTheme="system">
<ToastContextProvider>
<CrispWithNoSSR />
<Head>
<title>{SITE_TITLE}</title>
<meta property="og:site_name" content={SITE_NAME} />
<meta property="og:title" content={SITE_TITLE} />
<meta property="og:url" content={SITE_URL} />
<meta name="description" content={SITE_DESCRIPTION} />
<meta property="og:description" content={SITE_DESCRIPTION} />
<meta name="keywords" content={SITE_KEYWORDS} />
<meta name="twitter:site" content={`@${TWITTER_USER_NAME}`} />
<link rel="apple-touch-icon" sizes="180x180" href="/favicon/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png" />
<link rel="manifest" href="/site.webmanifest.json" />
<link rel="shortcut icon" href="/favicon/favicon.ico" />
</Head>
<MobxStoreInit />
<Component {...pageProps} />
</ToastContextProvider>
</ThemeProvider>
</MobxStoreProvider>
// </UserProvider>
<>
<Head>
<title>{SITE_TITLE}</title>
</Head>
<MobxStoreProvider {...pageProps}>
<ThemeProvider themes={THEMES} defaultTheme="system">
<ToastContextProvider>
<CrispWithNoSSR />
<MobxStoreInit />
<Component {...pageProps} />
</ToastContextProvider>
</ThemeProvider>
</MobxStoreProvider>
</>
);
}

View File

@ -1,4 +1,14 @@
import Document, { Html, Head, Main, NextScript } from "next/document";
// constants
import {
SITE_NAME,
SITE_DESCRIPTION,
SITE_URL,
TWITTER_USER_NAME,
SITE_KEYWORDS,
SITE_TITLE,
} from "constants/seo-variables";
import Script from "next/script";
class MyDocument extends Document {
render() {
@ -9,9 +19,23 @@ class MyDocument extends Document {
return (
<Html>
<Head>
<link rel="manifest" href="/manifest.json" />
<link rel="apple-touch-icon" href="/icon.png" />
<meta property="og:site_name" content={SITE_NAME} />
<meta property="og:title" content={SITE_TITLE} />
<meta property="og:url" content={SITE_URL} />
<meta name="description" content={SITE_DESCRIPTION} />
<meta property="og:description" content={SITE_DESCRIPTION} />
<meta name="keywords" content={SITE_KEYWORDS} />
<meta name="twitter:site" content={`@${TWITTER_USER_NAME}`} />
<meta name="theme-color" content="#fff" />
<link rel="apple-touch-icon" sizes="180x180" href="/favicon/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png" />
<link rel="manifest" href="/site.webmanifest.json" />
<link rel="shortcut icon" href="/favicon/favicon.ico" />
</Head>
<body>
<Main />
<NextScript />
{process.env.NEXT_PUBLIC_PLAUSIBLE_DOMAIN && (
<script
defer
@ -19,23 +43,21 @@ class MyDocument extends Document {
src="https://plausible.io/js/script.js"
/>
)}
<script defer src="https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2" />
{isSessionRecorderEnabled && process.env.NEXT_PUBLIC_SESSION_RECORDER_KEY && (
<script
defer
dangerouslySetInnerHTML={{
__html: `(function(c,l,a,r,i,t,y){
<Script id="clarity-tracking">
{`(function(c,l,a,r,i,t,y){
c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};
t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;
y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);
})(window, document, "clarity", "script", "${process.env.NEXT_PUBLIC_SESSION_RECORDER_KEY}");`,
}}
/>
})(window, document, "clarity", "script", "${process.env.NEXT_PUBLIC_SESSION_RECORDER_KEY}");`}
</Script>
)}
{process.env.NEXT_PUBLIC_POSTHOG_KEY && process.env.NEXT_PUBLIC_POSTHOG_HOST && (
<Script id="posthog-tracking">
{`!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.async=!0,p.src=s.api_host+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="capture identify alias people.set people.set_once set_config register register_once unregister opt_out_capturing has_opted_out_capturing opt_in_capturing reset isFeatureEnabled onFeatureFlags getFeatureFlag getFeatureFlagPayload reloadFeatureFlags group updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures getActiveMatchingSurveys getSurveys".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
posthog.init('${process.env.NEXT_PUBLIC_POSTHOG_KEY}',{api_host:'${process.env.NEXT_PUBLIC_POSTHOG_HOST}'})`}
</Script>
)}
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);

View File

@ -1,23 +1,23 @@
// pages/api/slack/authorize.js
import axios from "axios";
import { NextApiRequest, NextApiResponse } from "next";
export default async function handleSlackAuthorize(req: NextApiRequest, res: NextApiResponse) {
const { code } = req.body;
try {
const { code } = req.body;
if (!code || code === "") return res.status(400).json({ message: "Code is required" });
if (!code || code === "") return res.status(400).json({ message: "Code is required" });
const response = await axios({
method: "post",
url: "https://slack.com/api/oauth.v2.access",
params: {
client_id: process.env.NEXT_PUBLIC_SLACK_CLIENT_ID,
client_secret: process.env.NEXT_PUBLIC_SLACK_CLIENT_SECRET,
code,
},
});
// if (response?.data?.ok)
res.status(200).json(response.data);
// else res.status(404).json(response.data);
const response = await axios({
method: "post",
url: process.env.SLACK_OAUTH_URL || "",
params: {
client_id: process.env.SLACK_CLIENT_ID,
client_secret: process.env.SLACK_CLIENT_SECRET,
code,
},
});
res.status(200).json(response?.data);
} catch (error) {
res.status(200).json({ message: "Internal Server Error" });
}
}

View File

@ -1,12 +1,10 @@
import type { NextApiRequest, NextApiResponse } from "next";
// jitsu
import { createClient } from "@jitsu/nextjs";
import { convertCookieStringToObject } from "lib/cookie";
const jitsu = createClient({
key: process.env.TRACKER_ACCESS_KEY || "",
tracking_host: "https://t.jitsu.com",
const jitsuClient = createClient({
key: process.env.JITSU_TRACKER_ACCESS_KEY || "",
tracking_host: process.env.JITSU_TRACKER_HOST || "",
});
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
@ -18,18 +16,16 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
if (!user) return res.status(401).json({ message: "Unauthorized" });
// TODO: cache user info
jitsu
jitsuClient
.id({
id: user.id,
email: user.email,
first_name: user.first_name,
last_name: user.last_name,
id: user?.id,
email: user?.email,
first_name: user?.first_name,
last_name: user?.last_name,
display_name: user?.display_name,
})
.then(() => {
jitsu.track(eventName, {
jitsuClient.track(eventName, {
...extra,
});
});

View File

@ -1,7 +1,7 @@
import axios from "axios";
import type { NextApiRequest, NextApiResponse } from "next";
// TODO: remove NEXT_PUBLIC_ prefix from env variable
const unsplashKey = process.env.NEXT_PUBLIC_UNSPLASH_ACCESS;
const unsplashKey = process.env.UNSPLASH_ACCESS_KEY;
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const { query, page, per_page = 20 } = req.query;
@ -10,14 +10,13 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
? `https://api.unsplash.com/search/photos/?client_id=${unsplashKey}&query=${query}&page=${page}&per_page=${per_page}`
: `https://api.unsplash.com/photos/?client_id=${unsplashKey}&page=${page}&per_page=${per_page}`;
const response = await fetch(url, {
const response = await axios({
method: "GET",
url,
headers: {
"Content-Type": "application/json",
},
});
const data = await response.json();
res.status(200).json(data);
res.status(200).json(response);
}

View File

@ -5,9 +5,6 @@ import { ICurrentUserResponse, IGptResponse } from "types";
// helpers
import { API_BASE_URL } from "helpers/common.helper";
const trackEvent =
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
class AiServices extends APIService {
constructor() {
super(API_BASE_URL);
@ -21,7 +18,7 @@ class AiServices extends APIService {
): Promise<IGptResponse> {
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/ai-assistant/`, data)
.then((response) => {
if (trackEvent) trackEventServices.trackAskGptEvent(response?.data, "ASK_GPT", user);
trackEventServices.trackAskGptEvent(response?.data, "ASK_GPT", user);
return response?.data;
})
.catch((error) => {

View File

@ -5,9 +5,6 @@ import trackEventServices from "services/track-event.service";
import type { CycleDateCheckData, ICurrentUserResponse, ICycle, IIssue } from "types";
import { API_BASE_URL } from "helpers/common.helper";
const trackEvent =
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
export class ProjectCycleServices extends APIService {
constructor() {
super(API_BASE_URL);
@ -21,7 +18,7 @@ export class ProjectCycleServices extends APIService {
): Promise<any> {
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/`, data)
.then((response) => {
if (trackEvent) trackEventServices.trackCycleEvent(response?.data, "CYCLE_CREATE", user);
trackEventServices.trackCycleEvent(response?.data, "CYCLE_CREATE", user);
return response?.data;
})
.catch((error) => {
@ -99,7 +96,7 @@ export class ProjectCycleServices extends APIService {
data
)
.then((response) => {
if (trackEvent) trackEventServices.trackCycleEvent(response?.data, "CYCLE_UPDATE", user);
trackEventServices.trackCycleEvent(response?.data, "CYCLE_UPDATE", user);
return response?.data;
})
.catch((error) => {
@ -119,7 +116,7 @@ export class ProjectCycleServices extends APIService {
data
)
.then((response) => {
if (trackEvent) trackEventServices.trackCycleEvent(response?.data, "CYCLE_UPDATE", user);
trackEventServices.trackCycleEvent(response?.data, "CYCLE_UPDATE", user);
return response?.data;
})
.catch((error) => {
@ -135,7 +132,7 @@ export class ProjectCycleServices extends APIService {
): Promise<any> {
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}/`)
.then((response) => {
if (trackEvent) trackEventServices.trackCycleEvent(response?.data, "CYCLE_DELETE", user);
trackEventServices.trackCycleEvent(response?.data, "CYCLE_DELETE", user);
return response?.data;
})
.catch((error) => {

View File

@ -5,9 +5,6 @@ import type { ICurrentUserResponse, IEstimate, IEstimateFormData } from "types";
import trackEventServices from "services/track-event.service";
import { API_BASE_URL } from "helpers/common.helper";
const trackEvent =
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
class ProjectEstimateServices extends APIService {
constructor() {
super(API_BASE_URL);
@ -21,8 +18,7 @@ class ProjectEstimateServices extends APIService {
): Promise<any> {
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/estimates/`, data)
.then((response) => {
if (trackEvent)
trackEventServices.trackIssueEstimateEvent(response?.data, "ESTIMATE_CREATE", user);
trackEventServices.trackIssueEstimateEvent(response?.data, "ESTIMATE_CREATE", user);
return response?.data;
})
.catch((error) => {
@ -42,8 +38,7 @@ class ProjectEstimateServices extends APIService {
data
)
.then((response) => {
if (trackEvent)
trackEventServices.trackIssueEstimateEvent(response?.data, "ESTIMATE_UPDATE", user);
trackEventServices.trackIssueEstimateEvent(response?.data, "ESTIMATE_UPDATE", user);
return response?.data;
})
.catch((error) => {
@ -83,8 +78,7 @@ class ProjectEstimateServices extends APIService {
`/api/workspaces/${workspaceSlug}/projects/${projectId}/estimates/${estimateId}/`
)
.then((response) => {
if (trackEvent)
trackEventServices.trackIssueEstimateEvent(response?.data, "ESTIMATE_DELETE", user);
trackEventServices.trackIssueEstimateEvent(response?.data, "ESTIMATE_DELETE", user);
return response?.data;
})
.catch((error) => {

View File

@ -1,10 +1,6 @@
import APIService from "services/api.service";
import trackEventServices from "services/track-event.service";
import { API_BASE_URL } from "helpers/common.helper";
const trackEvent =
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
// types
import type {
IInboxIssue,
@ -12,7 +8,6 @@ import type {
TInboxStatus,
IInboxIssueDetail,
ICurrentUserResponse,
IInboxFilterOptions,
IInboxQueryParams,
} from "types";
@ -95,8 +90,7 @@ class InboxServices extends APIService {
`/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/inbox-issues/${inboxIssueId}/`
)
.then((response) => {
if (trackEvent)
trackEventServices.trackInboxEvent(response?.data, "INBOX_ISSUE_DELETE", user);
trackEventServices.trackInboxEvent(response?.data, "INBOX_ISSUE_DELETE", user);
return response?.data;
})
.catch((error) => {
@ -125,7 +119,7 @@ class InboxServices extends APIService {
: data.status === 1
? "INBOX_ISSUE_ACCEPTED"
: "INBOX_ISSUE_DUPLICATED";
if (trackEvent) trackEventServices.trackInboxEvent(response?.data, action, user);
trackEventServices.trackInboxEvent(response?.data, action, user);
return response?.data;
})
.catch((error) => {
@ -146,8 +140,7 @@ class InboxServices extends APIService {
data
)
.then((response) => {
if (trackEvent)
trackEventServices.trackInboxEvent(response?.data, "INBOX_ISSUE_UPDATE", user);
trackEventServices.trackInboxEvent(response?.data, "INBOX_ISSUE_UPDATE", user);
return response?.data;
})
.catch((error) => {
@ -167,8 +160,7 @@ class InboxServices extends APIService {
data
)
.then((response) => {
if (trackEvent)
trackEventServices.trackInboxEvent(response?.data, "INBOX_ISSUE_CREATE", user);
trackEventServices.trackInboxEvent(response?.data, "INBOX_ISSUE_CREATE", user);
return response?.data;
})
.catch((error) => {

View File

@ -3,9 +3,6 @@ import trackEventServices from "services/track-event.service";
import { ICurrentUserResponse } from "types";
import { API_BASE_URL } from "helpers/common.helper";
const trackEvent =
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
class CSVIntegrationService extends APIService {
constructor() {
super(API_BASE_URL);
@ -21,14 +18,13 @@ class CSVIntegrationService extends APIService {
): Promise<any> {
return this.post(`/api/workspaces/${workspaceSlug}/export-issues/`, data)
.then((response) => {
if (trackEvent)
trackEventServices.trackExporterEvent(
{
workspaceSlug,
},
"CSV_EXPORTER_CREATE",
user
);
trackEventServices.trackExporterEvent(
{
workspaceSlug,
},
"CSV_EXPORTER_CREATE",
user
);
return response?.data;
})
.catch((error) => {

View File

@ -4,11 +4,6 @@ import { API_BASE_URL } from "helpers/common.helper";
import { ICurrentUserResponse, IGithubRepoInfo, IGithubServiceImportFormData } from "types";
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
const trackEvent =
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
const integrationServiceType: string = "github";
class GithubIntegrationService extends APIService {
constructor() {
@ -48,8 +43,7 @@ class GithubIntegrationService extends APIService {
data
)
.then((response) => {
if (trackEvent)
trackEventServices.trackImporterEvent(response?.data, "GITHUB_IMPORTER_CREATE", user);
trackEventServices.trackImporterEvent(response?.data, "GITHUB_IMPORTER_CREATE", user);
return response?.data;
})
.catch((error) => {

View File

@ -11,9 +11,6 @@ import {
} from "types";
import { API_BASE_URL } from "helpers/common.helper";
const trackEvent =
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
class IntegrationService extends APIService {
constructor() {
super(API_BASE_URL);
@ -78,8 +75,7 @@ class IntegrationService extends APIService {
return this.delete(`/api/workspaces/${workspaceSlug}/importers/${service}/${importerId}/`)
.then((response) => {
const eventName = service === "github" ? "GITHUB_IMPORTER_DELETE" : "JIRA_IMPORTER_DELETE";
if (trackEvent) trackEventServices.trackImporterEvent(response?.data, eventName, user);
trackEventServices.trackImporterEvent(response?.data, eventName, user);
return response?.data;
})
.catch((error) => {

View File

@ -4,11 +4,6 @@ import { API_BASE_URL } from "helpers/common.helper";
// types
import { IJiraMetadata, IJiraResponse, IJiraImporterForm, ICurrentUserResponse } from "types";
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
const trackEvent =
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
class JiraImportedService extends APIService {
constructor() {
super(API_BASE_URL);
@ -31,8 +26,7 @@ class JiraImportedService extends APIService {
): Promise<IJiraResponse> {
return this.post(`/api/workspaces/${workspaceSlug}/projects/importers/jira/`, data)
.then((response) => {
if (trackEvent)
trackEventServices.trackImporterEvent(response?.data, "JIRA_IMPORTER_CREATE", user);
trackEventServices.trackImporterEvent(response?.data, "JIRA_IMPORTER_CREATE", user);
return response?.data;
})
.catch((error) => {

View File

@ -12,9 +12,6 @@ import type {
} from "types";
import { API_BASE_URL } from "helpers/common.helper";
const trackEvent =
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
export class ProjectIssuesServices extends APIService {
constructor() {
super(API_BASE_URL);
@ -28,7 +25,7 @@ export class ProjectIssuesServices extends APIService {
): Promise<any> {
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/`, data)
.then((response) => {
if (trackEvent) trackEventServices.trackIssueEvent(response.data, "ISSUE_CREATE", user);
trackEventServices.trackIssueEvent(response.data, "ISSUE_CREATE", user);
return response?.data;
})
.catch((error) => {
@ -112,20 +109,19 @@ export class ProjectIssuesServices extends APIService {
data
)
.then((response) => {
if (trackEvent)
trackEventServices.trackIssueMovedToCycleOrModuleEvent(
{
workspaceSlug,
workspaceName: response?.data?.[0]?.issue_detail?.workspace_detail?.name,
projectId,
projectIdentifier: response?.data?.[0]?.issue_detail?.project_detail?.identifier,
projectName: response?.data?.[0]?.issue_detail?.project_detail?.name,
issueId: response?.data?.[0]?.issue_detail?.id,
cycleId,
},
response.data.length > 1 ? "ISSUE_MOVED_TO_CYCLE_IN_BULK" : "ISSUE_MOVED_TO_CYCLE",
user
);
trackEventServices.trackIssueMovedToCycleOrModuleEvent(
{
workspaceSlug,
workspaceName: response?.data?.[0]?.issue_detail?.workspace_detail?.name,
projectId,
projectIdentifier: response?.data?.[0]?.issue_detail?.project_detail?.identifier,
projectName: response?.data?.[0]?.issue_detail?.project_detail?.name,
issueId: response?.data?.[0]?.issue_detail?.id,
cycleId,
},
response.data.length > 1 ? "ISSUE_MOVED_TO_CYCLE_IN_BULK" : "ISSUE_MOVED_TO_CYCLE",
user
);
return response?.data;
})
.catch((error) => {
@ -165,8 +161,7 @@ export class ProjectIssuesServices extends APIService {
data
)
.then((response) => {
if (trackEvent)
trackEventServices.trackIssueRelationEvent(response.data, "ISSUE_RELATION_CREATE", user);
trackEventServices.trackIssueRelationEvent(response.data, "ISSUE_RELATION_CREATE", user);
return response?.data;
})
.catch((error) => {
@ -185,8 +180,7 @@ export class ProjectIssuesServices extends APIService {
`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/issue-relation/${relationId}/`
)
.then((response) => {
if (trackEvent)
trackEventServices.trackIssueRelationEvent(response.data, "ISSUE_RELATION_DELETE", user);
trackEventServices.trackIssueRelationEvent(response.data, "ISSUE_RELATION_DELETE", user);
return response?.data;
})
.catch((error) => {
@ -234,8 +228,7 @@ export class ProjectIssuesServices extends APIService {
data
)
.then((response) => {
if (trackEvent)
trackEventServices.trackIssueCommentEvent(response.data, "ISSUE_COMMENT_CREATE", user);
trackEventServices.trackIssueCommentEvent(response.data, "ISSUE_COMMENT_CREATE", user);
return response?.data;
})
.catch((error) => {
@ -256,8 +249,7 @@ export class ProjectIssuesServices extends APIService {
data
)
.then((response) => {
if (trackEvent)
trackEventServices.trackIssueCommentEvent(response.data, "ISSUE_COMMENT_UPDATE", user);
trackEventServices.trackIssueCommentEvent(response.data, "ISSUE_COMMENT_UPDATE", user);
return response?.data;
})
.catch((error) => {
@ -276,15 +268,14 @@ export class ProjectIssuesServices extends APIService {
`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/comments/${commentId}/`
)
.then((response) => {
if (trackEvent)
trackEventServices.trackIssueCommentEvent(
{
issueId,
commentId,
},
"ISSUE_COMMENT_DELETE",
user
);
trackEventServices.trackIssueCommentEvent(
{
issueId,
commentId,
},
"ISSUE_COMMENT_DELETE",
user
);
return response?.data;
})
.catch((error) => {
@ -316,21 +307,20 @@ export class ProjectIssuesServices extends APIService {
): Promise<IIssueLabels> {
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-labels/`, data)
.then((response: { data: IIssueLabels; [key: string]: any }) => {
if (trackEvent)
trackEventServices.trackIssueLabelEvent(
{
workSpaceId: response?.data?.workspace_detail?.id,
workSpaceName: response?.data?.workspace_detail?.name,
workspaceSlug,
projectId,
projectIdentifier: response?.data?.project_detail?.identifier,
projectName: response?.data?.project_detail?.name,
labelId: response?.data?.id,
color: response?.data?.color,
},
"ISSUE_LABEL_CREATE",
user
);
trackEventServices.trackIssueLabelEvent(
{
workSpaceId: response?.data?.workspace_detail?.id,
workSpaceName: response?.data?.workspace_detail?.name,
workspaceSlug,
projectId,
projectIdentifier: response?.data?.project_detail?.identifier,
projectName: response?.data?.project_detail?.name,
labelId: response?.data?.id,
color: response?.data?.color,
},
"ISSUE_LABEL_CREATE",
user
);
return response?.data;
})
.catch((error) => {
@ -350,21 +340,20 @@ export class ProjectIssuesServices extends APIService {
data
)
.then((response) => {
if (trackEvent)
trackEventServices.trackIssueLabelEvent(
{
workSpaceId: response?.data?.workspace_detail?.id,
workSpaceName: response?.data?.workspace_detail?.name,
workspaceSlug,
projectId,
projectIdentifier: response?.data?.project_detail?.identifier,
projectName: response?.data?.project_detail?.name,
labelId: response?.data?.id,
color: response?.data?.color,
},
"ISSUE_LABEL_UPDATE",
user
);
trackEventServices.trackIssueLabelEvent(
{
workSpaceId: response?.data?.workspace_detail?.id,
workSpaceName: response?.data?.workspace_detail?.name,
workspaceSlug,
projectId,
projectIdentifier: response?.data?.project_detail?.identifier,
projectName: response?.data?.project_detail?.name,
labelId: response?.data?.id,
color: response?.data?.color,
},
"ISSUE_LABEL_UPDATE",
user
);
return response?.data;
})
.catch((error) => {
@ -382,15 +371,14 @@ export class ProjectIssuesServices extends APIService {
`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-labels/${labelId}/`
)
.then((response) => {
if (trackEvent)
trackEventServices.trackIssueLabelEvent(
{
workspaceSlug,
projectId,
},
"ISSUE_LABEL_DELETE",
user
);
trackEventServices.trackIssueLabelEvent(
{
workspaceSlug,
projectId,
},
"ISSUE_LABEL_DELETE",
user
);
return response?.data;
})
.catch((error) => {
@ -410,7 +398,7 @@ export class ProjectIssuesServices extends APIService {
data
)
.then((response) => {
if (trackEvent) trackEventServices.trackIssueEvent(response.data, "ISSUE_UPDATE", user);
trackEventServices.trackIssueEvent(response.data, "ISSUE_UPDATE", user);
return response?.data;
})
.catch((error) => {
@ -426,7 +414,7 @@ export class ProjectIssuesServices extends APIService {
): Promise<any> {
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issuesId}/`)
.then((response) => {
if (trackEvent) trackEventServices.trackIssueEvent({ issuesId }, "ISSUE_DELETE", user);
trackEventServices.trackIssueEvent({ issuesId }, "ISSUE_DELETE", user);
return response?.data;
})
.catch((error) => {
@ -445,7 +433,7 @@ export class ProjectIssuesServices extends APIService {
data
)
.then((response) => {
if (trackEvent) trackEventServices.trackIssueBulkDeleteEvent(data, user);
trackEventServices.trackIssueBulkDeleteEvent(data, user);
return response?.data;
})
.catch((error) => {
@ -629,6 +617,68 @@ export class ProjectIssuesServices extends APIService {
throw error?.response?.data;
});
}
async getDraftIssues(workspaceSlug: string, projectId: string, params?: any): Promise<any> {
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-drafts/`, {
params,
})
.then((response) => response?.data)
.catch((error) => {
throw error?.response;
});
}
async createDraftIssue(
workspaceSlug: string,
projectId: string,
data: any,
user: ICurrentUserResponse
): Promise<any> {
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-drafts/`, data)
.then((response) => response?.data)
.catch((error) => {
throw error?.response;
});
}
async updateDraftIssue(
workspaceSlug: string,
projectId: string,
issueId: string,
data: any,
user: ICurrentUserResponse
): Promise<any> {
return this.patch(
`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-drafts/${issueId}/`,
data
)
.then((response) => response?.data)
.catch((error) => {
throw error?.response;
});
}
async deleteDraftIssue(workspaceSlug: string, projectId: string, issueId: string): Promise<any> {
return this.delete(
`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-drafts/${issueId}/`
)
.then((response) => response?.data)
.catch((error) => {
throw error?.response;
});
}
async getDraftIssueById(workspaceSlug: string, projectId: string, issueId: string): Promise<any> {
return this.get(
`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-drafts/${issueId}/`
)
.then((response) => response?.data)
.catch((error) => {
throw error?.response;
});
}
}
export default new ProjectIssuesServices();
const projectIssuesServices = new ProjectIssuesServices();
export default projectIssuesServices;

View File

@ -5,11 +5,6 @@ import trackEventServices from "./track-event.service";
import type { IModule, IIssue, ICurrentUserResponse } from "types";
import { API_BASE_URL } from "helpers/common.helper";
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
const trackEvent =
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
export class ProjectIssuesServices extends APIService {
constructor() {
super(API_BASE_URL);
@ -31,7 +26,7 @@ export class ProjectIssuesServices extends APIService {
): Promise<any> {
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/`, data)
.then((response) => {
if (trackEvent) trackEventServices.trackModuleEvent(response?.data, "MODULE_CREATE", user);
trackEventServices.trackModuleEvent(response?.data, "MODULE_CREATE", user);
return response?.data;
})
.catch((error) => {
@ -51,7 +46,7 @@ export class ProjectIssuesServices extends APIService {
data
)
.then((response) => {
if (trackEvent) trackEventServices.trackModuleEvent(response?.data, "MODULE_UPDATE", user);
trackEventServices.trackModuleEvent(response?.data, "MODULE_UPDATE", user);
return response?.data;
})
.catch((error) => {
@ -83,7 +78,7 @@ export class ProjectIssuesServices extends APIService {
data
)
.then((response) => {
if (trackEvent) trackEventServices.trackModuleEvent(response?.data, "MODULE_UPDATE", user);
trackEventServices.trackModuleEvent(response?.data, "MODULE_UPDATE", user);
return response?.data;
})
.catch((error) => {
@ -101,7 +96,7 @@ export class ProjectIssuesServices extends APIService {
`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/`
)
.then((response) => {
if (trackEvent) trackEventServices.trackModuleEvent(response?.data, "MODULE_DELETE", user);
trackEventServices.trackModuleEvent(response?.data, "MODULE_DELETE", user);
return response?.data;
})
.catch((error) => {
@ -156,20 +151,19 @@ export class ProjectIssuesServices extends APIService {
data
)
.then((response) => {
if (trackEvent)
trackEventServices.trackIssueMovedToCycleOrModuleEvent(
{
workspaceSlug,
workspaceName: response?.data?.[0]?.issue_detail?.workspace_detail?.name,
projectId,
projectIdentifier: response?.data?.[0]?.issue_detail?.project_detail?.identifier,
projectName: response?.data?.[0]?.issue_detail?.project_detail?.name,
issueId: response?.data?.[0]?.issue_detail?.id,
moduleId,
},
response?.data?.length > 1 ? "ISSUE_MOVED_TO_MODULE_IN_BULK" : "ISSUE_MOVED_TO_MODULE",
user
);
trackEventServices.trackIssueMovedToCycleOrModuleEvent(
{
workspaceSlug,
workspaceName: response?.data?.[0]?.issue_detail?.workspace_detail?.name,
projectId,
projectIdentifier: response?.data?.[0]?.issue_detail?.project_detail?.identifier,
projectName: response?.data?.[0]?.issue_detail?.project_detail?.name,
issueId: response?.data?.[0]?.issue_detail?.id,
moduleId,
},
response?.data?.length > 1 ? "ISSUE_MOVED_TO_MODULE_IN_BULK" : "ISSUE_MOVED_TO_MODULE",
user
);
return response?.data;
})
.catch((error) => {

View File

@ -5,9 +5,6 @@ import trackEventServices from "services/track-event.service";
// types
import { IPage, IPageBlock, RecentPagesResponse, IIssue, ICurrentUserResponse } from "types";
const trackEvent =
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
class PageServices extends APIService {
constructor() {
super(API_BASE_URL);
@ -21,7 +18,7 @@ class PageServices extends APIService {
): Promise<IPage> {
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/`, data)
.then((response) => {
if (trackEvent) trackEventServices.trackPageEvent(response?.data, "PAGE_CREATE", user);
trackEventServices.trackPageEvent(response?.data, "PAGE_CREATE", user);
return response?.data;
})
.catch((error) => {
@ -41,7 +38,7 @@ class PageServices extends APIService {
data
)
.then((response) => {
if (trackEvent) trackEventServices.trackPageEvent(response?.data, "PAGE_UPDATE", user);
trackEventServices.trackPageEvent(response?.data, "PAGE_UPDATE", user);
return response?.data;
})
.catch((error) => {
@ -57,7 +54,7 @@ class PageServices extends APIService {
): Promise<any> {
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/`)
.then((response) => {
if (trackEvent) trackEventServices.trackPageEvent(response?.data, "PAGE_DELETE", user);
trackEventServices.trackPageEvent(response?.data, "PAGE_DELETE", user);
return response?.data;
})
.catch((error) => {
@ -140,8 +137,7 @@ class PageServices extends APIService {
data
)
.then((response) => {
if (trackEvent)
trackEventServices.trackPageBlockEvent(response?.data, "PAGE_BLOCK_CREATE", user);
trackEventServices.trackPageBlockEvent(response?.data, "PAGE_BLOCK_CREATE", user);
return response?.data;
})
.catch((error) => {
@ -177,8 +173,7 @@ class PageServices extends APIService {
data
)
.then((response) => {
if (trackEvent)
trackEventServices.trackPageBlockEvent(response?.data, "PAGE_BLOCK_UPDATE", user);
trackEventServices.trackPageBlockEvent(response?.data, "PAGE_BLOCK_UPDATE", user);
return response?.data;
})
.catch((error) => {
@ -197,8 +192,7 @@ class PageServices extends APIService {
`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/page-blocks/${pageBlockId}/`
)
.then((response) => {
if (trackEvent)
trackEventServices.trackPageBlockEvent(response?.data, "PAGE_BLOCK_DELETE", user);
trackEventServices.trackPageBlockEvent(response?.data, "PAGE_BLOCK_DELETE", user);
return response?.data;
})
.catch((error) => {
@ -231,12 +225,11 @@ class PageServices extends APIService {
`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/page-blocks/${blockId}/issues/`
)
.then((response) => {
if (trackEvent)
trackEventServices.trackPageBlockEvent(
response?.data,
"PAGE_BLOCK_CONVERTED_TO_ISSUE",
user
);
trackEventServices.trackPageBlockEvent(
response?.data,
"PAGE_BLOCK_CONVERTED_TO_ISSUE",
user
);
return response?.data;
})
.catch((error) => {

View File

@ -6,9 +6,6 @@ import trackEventServices from "services/track-event.service";
import { ICurrentUserResponse } from "types";
import { IProjectPublishSettings } from "store/project-publish";
const trackEvent =
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
class ProjectServices extends APIService {
constructor() {
super(API_BASE_URL);
@ -23,13 +20,11 @@ class ProjectServices extends APIService {
`/api/workspaces/${workspace_slug}/projects/${project_slug}/project-deploy-boards/`
)
.then((response) => {
if (trackEvent) {
// trackEventServices.trackProjectPublishSettingsEvent(
// response.data,
// "GET_PROJECT_PUBLISH_SETTINGS",
// user
// );
}
// trackEventServices.trackProjectPublishSettingsEvent(
// response.data,
// "GET_PROJECT_PUBLISH_SETTINGS",
// user
// );
return response?.data;
})
.catch((error) => {
@ -48,13 +43,12 @@ class ProjectServices extends APIService {
data
)
.then((response) => {
if (trackEvent) {
// trackEventServices.trackProjectPublishSettingsEvent(
// response.data,
// "CREATE_PROJECT_PUBLISH_SETTINGS",
// user
// );
}
// trackEventServices.trackProjectPublishSettingsEvent(
// response.data,
// "CREATE_PROJECT_PUBLISH_SETTINGS",
// user
// );
return response?.data;
})
.catch((error) => {
@ -74,13 +68,11 @@ class ProjectServices extends APIService {
data
)
.then((response) => {
if (trackEvent) {
// trackEventServices.trackProjectPublishSettingsEvent(
// response.data,
// "UPDATE_PROJECT_PUBLISH_SETTINGS",
// user
// );
}
// trackEventServices.trackProjectPublishSettingsEvent(
// response.data,
// "UPDATE_PROJECT_PUBLISH_SETTINGS",
// user
// );
return response?.data;
})
.catch((error) => {
@ -98,13 +90,11 @@ class ProjectServices extends APIService {
`/api/workspaces/${workspace_slug}/projects/${project_slug}/project-deploy-boards/${project_publish_id}/`
)
.then((response) => {
if (trackEvent) {
// trackEventServices.trackProjectPublishSettingsEvent(
// response.data,
// "DELETE_PROJECT_PUBLISH_SETTINGS",
// user
// );
}
// trackEventServices.trackProjectPublishSettingsEvent(
// response.data,
// "DELETE_PROJECT_PUBLISH_SETTINGS",
// user
// );
return response?.data;
})
.catch((error) => {

View File

@ -16,9 +16,6 @@ import type {
TProjectIssuesSearchParams,
} from "types";
const trackEvent =
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
export class ProjectServices extends APIService {
constructor() {
super(API_BASE_URL);
@ -31,7 +28,7 @@ export class ProjectServices extends APIService {
): Promise<IProject> {
return this.post(`/api/workspaces/${workspaceSlug}/projects/`, data)
.then((response) => {
if (trackEvent) trackEventServices.trackProjectEvent(response.data, "CREATE_PROJECT", user);
trackEventServices.trackProjectEvent(response.data, "CREATE_PROJECT", user);
return response?.data;
})
.catch((error) => {
@ -82,7 +79,7 @@ export class ProjectServices extends APIService {
): Promise<IProject> {
return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/`, data)
.then((response) => {
if (trackEvent) trackEventServices.trackProjectEvent(response.data, "UPDATE_PROJECT", user);
trackEventServices.trackProjectEvent(response.data, "UPDATE_PROJECT", user);
return response?.data;
})
.catch((error) => {
@ -97,7 +94,7 @@ export class ProjectServices extends APIService {
): Promise<any> {
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/`)
.then((response) => {
if (trackEvent) trackEventServices.trackProjectEvent({ projectId }, "DELETE_PROJECT", user);
trackEventServices.trackProjectEvent({ projectId }, "DELETE_PROJECT", user);
return response?.data;
})
.catch((error) => {
@ -113,18 +110,17 @@ export class ProjectServices extends APIService {
): Promise<any> {
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/members/add/`, data)
.then((response) => {
if (trackEvent)
trackEventServices.trackProjectEvent(
{
workspaceId: response?.data?.workspace?.id,
workspaceSlug,
projectId,
projectName: response?.data?.project?.name,
memberEmail: response?.data?.member?.email,
},
"PROJECT_MEMBER_INVITE",
user
);
trackEventServices.trackProjectEvent(
{
workspaceId: response?.data?.workspace?.id,
workspaceSlug,
projectId,
projectName: response?.data?.project?.name,
memberEmail: response?.data?.member?.email,
},
"PROJECT_MEMBER_INVITE",
user
);
return response?.data;
})
.catch((error) => {
@ -147,16 +143,15 @@ export class ProjectServices extends APIService {
): Promise<any> {
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/members/leave/`)
.then((response) => {
if (trackEvent)
trackEventServices.trackProjectEvent(
"PROJECT_MEMBER_LEAVE",
{
workspaceSlug,
projectId,
...response?.data,
},
user
);
trackEventServices.trackProjectEvent(
"PROJECT_MEMBER_LEAVE",
{
workspaceSlug,
projectId,
...response?.data,
},
user
);
return response?.data;
})
.catch((error) => {

View File

@ -11,9 +11,6 @@ import type {
IssueCommentReactionForm,
} from "types";
const trackEvent =
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
class ReactionService extends APIService {
constructor() {
super(API_BASE_URL);
@ -31,8 +28,7 @@ class ReactionService extends APIService {
data
)
.then((response) => {
if (trackEvent)
trackEventServices.trackReactionEvent(response?.data, "ISSUE_REACTION_CREATE", user);
trackEventServices.trackReactionEvent(response?.data, "ISSUE_REACTION_CREATE", user);
return response?.data;
})
.catch((error) => {
@ -65,8 +61,7 @@ class ReactionService extends APIService {
`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/reactions/${reaction}/`
)
.then((response) => {
if (trackEvent)
trackEventServices.trackReactionEvent(response?.data, "ISSUE_REACTION_DELETE", user);
trackEventServices.trackReactionEvent(response?.data, "ISSUE_REACTION_DELETE", user);
return response?.data;
})
.catch((error) => {
@ -86,12 +81,11 @@ class ReactionService extends APIService {
data
)
.then((response) => {
if (trackEvent)
trackEventServices.trackReactionEvent(
response?.data,
"ISSUE_COMMENT_REACTION_CREATE",
user
);
trackEventServices.trackReactionEvent(
response?.data,
"ISSUE_COMMENT_REACTION_CREATE",
user
);
return response?.data;
})
.catch((error) => {
@ -124,12 +118,11 @@ class ReactionService extends APIService {
`/api/workspaces/${workspaceSlug}/projects/${projectId}/comments/${commentId}/reactions/${reaction}/`
)
.then((response) => {
if (trackEvent)
trackEventServices.trackReactionEvent(
response?.data,
"ISSUE_COMMENT_REACTION_DELETE",
user
);
trackEventServices.trackReactionEvent(
response?.data,
"ISSUE_COMMENT_REACTION_DELETE",
user
);
return response?.data;
})
.catch((error) => {

View File

@ -3,10 +3,6 @@ import APIService from "services/api.service";
import trackEventServices from "services/track-event.service";
// helpers
import { API_BASE_URL } from "helpers/common.helper";
const trackEvent =
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
// types
import type { ICurrentUserResponse, IState, IStateResponse } from "types";
@ -23,7 +19,7 @@ export class ProjectStateServices extends APIService {
): Promise<any> {
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/states/`, data)
.then((response) => {
if (trackEvent) trackEventServices.trackStateEvent(response?.data, "STATE_CREATE", user);
trackEventServices.trackStateEvent(response?.data, "STATE_CREATE", user);
return response?.data;
})
.catch((error) => {
@ -68,7 +64,7 @@ export class ProjectStateServices extends APIService {
data
)
.then((response) => {
if (trackEvent) trackEventServices.trackStateEvent(response?.data, "STATE_UPDATE", user);
trackEventServices.trackStateEvent(response?.data, "STATE_UPDATE", user);
return response?.data;
})
.catch((error) => {
@ -88,7 +84,7 @@ export class ProjectStateServices extends APIService {
data
)
.then((response) => {
if (trackEvent) trackEventServices.trackStateEvent(response?.data, "STATE_UPDATE", user);
trackEventServices.trackStateEvent(response?.data, "STATE_UPDATE", user);
return response?.data;
})
.catch((error) => {
@ -104,7 +100,7 @@ export class ProjectStateServices extends APIService {
): Promise<any> {
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/states/${stateId}/`)
.then((response) => {
if (trackEvent) trackEventServices.trackStateEvent(response?.data, "STATE_DELETE", user);
trackEventServices.trackStateEvent(response?.data, "STATE_DELETE", user);
return response?.data;
})
.catch((error) => {

View File

@ -131,6 +131,8 @@ class TrackEventServices extends APIService {
eventName: WorkspaceEventType,
user: ICurrentUserResponse | undefined
): Promise<any> {
if (!trackEvent) return;
let payload: any;
if (
eventName !== "DELETE_WORKSPACE" &&
@ -163,6 +165,8 @@ class TrackEventServices extends APIService {
eventName: ProjectEventType,
user: ICurrentUserResponse | undefined
): Promise<any> {
if (!trackEvent) return;
let payload: any;
if (
eventName !== "DELETE_PROJECT" &&
@ -195,6 +199,8 @@ class TrackEventServices extends APIService {
data: any,
user: ICurrentUserResponse | undefined
): Promise<any> {
if (!trackEvent) return;
return this.request({
url: "/api/track-event",
method: "POST",
@ -212,6 +218,8 @@ class TrackEventServices extends APIService {
data: any,
user: ICurrentUserResponse | undefined
): Promise<any> {
if (!trackEvent) return;
return this.request({
url: "/api/track-event",
method: "POST",
@ -230,6 +238,8 @@ class TrackEventServices extends APIService {
eventName: IssueEventType,
user: ICurrentUserResponse | undefined
): Promise<any> {
if (!trackEvent) return;
let payload: any;
if (eventName !== "ISSUE_DELETE")
payload = {
@ -303,6 +313,8 @@ class TrackEventServices extends APIService {
eventName: IssueCommentEventType,
user: ICurrentUserResponse | undefined
): Promise<any> {
if (!trackEvent) return;
let payload: any;
if (eventName !== "ISSUE_COMMENT_DELETE")
payload = {
@ -333,6 +345,8 @@ class TrackEventServices extends APIService {
eventName: "ISSUE_RELATION_CREATE" | "ISSUE_RELATION_DELETE",
user: ICurrentUserResponse
): Promise<any> {
if (!trackEvent) return;
return this.request({
url: "/api/track-event",
method: "POST",
@ -353,6 +367,8 @@ class TrackEventServices extends APIService {
| "ISSUE_MOVED_TO_MODULE_IN_BULK",
user: ICurrentUserResponse | undefined
): Promise<any> {
if (!trackEvent) return;
return this.request({
url: "/api/track-event",
method: "POST",
@ -367,6 +383,8 @@ class TrackEventServices extends APIService {
}
async trackIssueBulkDeleteEvent(data: any, user: ICurrentUserResponse | undefined): Promise<any> {
if (!trackEvent) return;
return this.request({
url: "/api/track-event",
method: "POST",
@ -385,6 +403,8 @@ class TrackEventServices extends APIService {
eventName: IssueLabelEventType,
user: ICurrentUserResponse | undefined
): Promise<any> {
if (!trackEvent) return;
return this.request({
url: "/api/track-event",
method: "POST",
@ -403,6 +423,8 @@ class TrackEventServices extends APIService {
eventName: StateEventType,
user: ICurrentUserResponse | undefined
): Promise<any> {
if (!trackEvent) return;
let payload: any;
if (eventName !== "STATE_DELETE")
payload = {
@ -434,6 +456,8 @@ class TrackEventServices extends APIService {
eventName: CycleEventType,
user: ICurrentUserResponse | undefined
): Promise<any> {
if (!trackEvent) return;
let payload: any;
if (eventName !== "CYCLE_DELETE")
payload = {
@ -465,6 +489,8 @@ class TrackEventServices extends APIService {
eventName: ModuleEventType,
user: ICurrentUserResponse | undefined
): Promise<any> {
if (!trackEvent) return;
let payload: any;
if (eventName !== "MODULE_DELETE")
payload = {
@ -496,6 +522,8 @@ class TrackEventServices extends APIService {
eventName: PagesEventType,
user: ICurrentUserResponse | undefined
): Promise<any> {
if (!trackEvent) return;
let payload: any;
if (eventName !== "PAGE_DELETE")
payload = {
@ -527,6 +555,8 @@ class TrackEventServices extends APIService {
eventName: PageBlocksEventType,
user: ICurrentUserResponse | undefined
): Promise<any> {
if (!trackEvent) return;
let payload: any;
if (eventName !== "PAGE_BLOCK_DELETE" && eventName !== "PAGE_BLOCK_CONVERTED_TO_ISSUE")
payload = {
@ -569,6 +599,8 @@ class TrackEventServices extends APIService {
eventName: GptEventType,
user: ICurrentUserResponse | undefined
): Promise<any> {
if (!trackEvent) return;
const payload = {
workspaceId: data?.workspace_detail?.id,
workspaceName: data?.workspace_detail?.name,
@ -641,6 +673,8 @@ class TrackEventServices extends APIService {
eventName: ViewEventType,
user: ICurrentUserResponse | undefined
): Promise<any> {
if (!trackEvent) return;
let payload: any;
if (eventName === "VIEW_DELETE") payload = data;
else
@ -670,6 +704,8 @@ class TrackEventServices extends APIService {
eventName: MiscellaneousEventType,
user: ICurrentUserResponse | undefined
): Promise<any> {
if (!trackEvent) return;
return this.request({
url: "/api/track-event",
method: "POST",
@ -688,6 +724,8 @@ class TrackEventServices extends APIService {
eventName: IntegrationEventType,
user: ICurrentUserResponse | undefined
): Promise<any> {
if (!trackEvent) return;
return this.request({
url: "/api/track-event",
method: "POST",
@ -706,6 +744,8 @@ class TrackEventServices extends APIService {
eventName: GitHubSyncEventType,
user: ICurrentUserResponse | undefined
): Promise<any> {
if (!trackEvent) return;
return this.request({
url: "/api/track-event",
method: "POST",
@ -724,6 +764,8 @@ class TrackEventServices extends APIService {
eventName: IssueEstimateEventType,
user: ICurrentUserResponse | undefined
): Promise<any> {
if (!trackEvent) return;
let payload: any;
if (eventName === "ESTIMATE_DELETE") payload = data;
else
@ -755,6 +797,8 @@ class TrackEventServices extends APIService {
eventName: ImporterEventType,
user: ICurrentUserResponse | undefined
): Promise<any> {
if (!trackEvent) return;
let payload: any;
if (eventName === "GITHUB_IMPORTER_DELETE" || eventName === "JIRA_IMPORTER_DELETE")
payload = data;
@ -786,6 +830,8 @@ class TrackEventServices extends APIService {
eventName: AnalyticsEventType,
user: ICurrentUserResponse | undefined
): Promise<any> {
if (!trackEvent) return;
const payload = { ...data };
return this.request({
@ -799,12 +845,13 @@ class TrackEventServices extends APIService {
});
}
// track exporter function\
async trackExporterEvent(
data: any,
eventName: ExporterEventType,
user: ICurrentUserResponse | undefined
): Promise<any> {
if (!trackEvent) return;
const payload = { ...data };
return this.request({
@ -826,6 +873,8 @@ class TrackEventServices extends APIService {
eventName: InboxEventType,
user: ICurrentUserResponse | undefined
): Promise<any> {
if (!trackEvent) return;
let payload: any;
if (eventName !== "INBOX_DELETE")
payload = {
@ -857,6 +906,8 @@ class TrackEventServices extends APIService {
eventName: ReactionEventType,
user: ICurrentUserResponse | undefined
): Promise<any> {
if (!trackEvent) return;
let payload: any;
if (eventName === "ISSUE_REACTION_DELETE" || eventName === "ISSUE_COMMENT_REACTION_DELETE")
payload = data;
@ -878,12 +929,13 @@ class TrackEventServices extends APIService {
});
}
// project publish settings track events starts
async trackProjectPublishSettingsEvent(
data: any,
eventName: string,
user: ICurrentUserResponse | undefined
): Promise<any> {
if (!trackEvent) return;
const payload: any = data;
return this.request({
@ -896,8 +948,6 @@ class TrackEventServices extends APIService {
},
});
}
// project publish settings track events ends
}
const trackEventServices = new TrackEventServices();

View File

@ -14,9 +14,6 @@ import type {
import { API_BASE_URL } from "helpers/common.helper";
const trackEvent =
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
export class UserService extends APIService {
constructor() {
super(API_BASE_URL);
@ -68,13 +65,12 @@ export class UserService extends APIService {
is_onboarded: true,
})
.then((response) => {
if (trackEvent)
trackEventServices.trackUserOnboardingCompleteEvent(
{
user_role: userRole ?? "None",
},
user
);
trackEventServices.trackUserOnboardingCompleteEvent(
{
user_role: userRole ?? "None",
},
user
);
return response?.data;
})
.catch((error) => {
@ -87,13 +83,7 @@ export class UserService extends APIService {
is_tour_completed: true,
})
.then((response) => {
if (trackEvent)
trackEventServices.trackUserTourCompleteEvent(
{
user_role: user.role ?? "None",
},
user
);
trackEventServices.trackUserTourCompleteEvent({ user_role: user.role ?? "None" }, user);
return response?.data;
})
.catch((error) => {

View File

@ -1,18 +1,11 @@
// services
import APIService from "services/api.service";
import trackEventServices from "services/track-event.service";
import { ICurrentUserResponse } from "types";
// types
import { IView } from "types/views";
import { ICurrentUserResponse } from "types";
// helpers
import { API_BASE_URL } from "helpers/common.helper";
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
const trackEvent =
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
export class ViewServices extends APIService {
constructor() {
super(API_BASE_URL);
@ -26,7 +19,7 @@ export class ViewServices extends APIService {
): Promise<any> {
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/views/`, data)
.then((response) => {
if (trackEvent) trackEventServices.trackViewEvent(response?.data, "VIEW_CREATE", user);
trackEventServices.trackViewEvent(response?.data, "VIEW_CREATE", user);
return response?.data;
})
.catch((error) => {
@ -43,7 +36,7 @@ export class ViewServices extends APIService {
): Promise<any> {
return this.put(`/api/workspaces/${workspaceSlug}/projects/${projectId}/views/${viewId}/`, data)
.then((response) => {
if (trackEvent) trackEventServices.trackViewEvent(response?.data, "VIEW_UPDATE", user);
trackEventServices.trackViewEvent(response?.data, "VIEW_UPDATE", user);
return response?.data;
})
.catch((error) => {
@ -63,7 +56,7 @@ export class ViewServices extends APIService {
data
)
.then((response) => {
if (trackEvent) trackEventServices.trackViewEvent(response?.data, "VIEW_UPDATE", user);
trackEventServices.trackViewEvent(response?.data, "VIEW_UPDATE", user);
return response?.data;
})
.catch((error) => {
@ -79,7 +72,7 @@ export class ViewServices extends APIService {
): Promise<any> {
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/views/${viewId}/`)
.then((response) => {
if (trackEvent) trackEventServices.trackViewEvent(response?.data, "VIEW_DELETE", user);
trackEventServices.trackViewEvent(response?.data, "VIEW_DELETE", user);
return response?.data;
})
.catch((error) => {

View File

@ -16,9 +16,6 @@ import {
IWorkspaceViewProps,
} from "types";
const trackEvent =
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
export class WorkspaceService extends APIService {
constructor() {
super(API_BASE_URL);
@ -46,8 +43,7 @@ export class WorkspaceService extends APIService {
): Promise<IWorkspace> {
return this.post("/api/workspaces/", data)
.then((response) => {
if (trackEvent)
trackEventServices.trackWorkspaceEvent(response.data, "CREATE_WORKSPACE", user);
trackEventServices.trackWorkspaceEvent(response.data, "CREATE_WORKSPACE", user);
return response?.data;
})
.catch((error) => {
@ -62,8 +58,7 @@ export class WorkspaceService extends APIService {
): Promise<IWorkspace> {
return this.patch(`/api/workspaces/${workspaceSlug}/`, data)
.then((response) => {
if (trackEvent)
trackEventServices.trackWorkspaceEvent(response.data, "UPDATE_WORKSPACE", user);
trackEventServices.trackWorkspaceEvent(response.data, "UPDATE_WORKSPACE", user);
return response?.data;
})
.catch((error) => {
@ -77,8 +72,7 @@ export class WorkspaceService extends APIService {
): Promise<any> {
return this.delete(`/api/workspaces/${workspaceSlug}/`)
.then((response) => {
if (trackEvent)
trackEventServices.trackWorkspaceEvent({ workspaceSlug }, "DELETE_WORKSPACE", user);
trackEventServices.trackWorkspaceEvent({ workspaceSlug }, "DELETE_WORKSPACE", user);
return response?.data;
})
.catch((error) => {
@ -93,8 +87,7 @@ export class WorkspaceService extends APIService {
): Promise<any> {
return this.post(`/api/workspaces/${workspaceSlug}/invite/`, data)
.then((response) => {
if (trackEvent)
trackEventServices.trackWorkspaceEvent(response.data, "WORKSPACE_USER_INVITE", user);
trackEventServices.trackWorkspaceEvent(response.data, "WORKSPACE_USER_INVITE", user);
return response?.data;
})
.catch((error) => {
@ -116,12 +109,7 @@ export class WorkspaceService extends APIService {
}
)
.then((response) => {
if (trackEvent)
trackEventServices.trackWorkspaceEvent(
response.data,
"WORKSPACE_USER_INVITE_ACCEPT",
user
);
trackEventServices.trackWorkspaceEvent(response.data, "WORKSPACE_USER_INVITE_ACCEPT", user);
return response?.data;
})
.catch((error) => {

View File

@ -118,6 +118,7 @@ export interface IIssue {
issue_module: IIssueModule | null;
labels: string[];
label_details: any[];
is_draft: boolean;
labels_list: string[];
links_list: IIssueLink[];
link_count: number;

1204
yarn.lock

File diff suppressed because it is too large Load Diff