mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
Merge branch 'develop' of github.com:makeplane/plane into dev/migration_priority_drafts_relation_props
This commit is contained in:
commit
4ccad92e2d
@ -293,12 +293,12 @@ class IssueLabelSerializer(BaseSerializer):
|
||||
|
||||
|
||||
class IssueRelationSerializer(BaseSerializer):
|
||||
related_issue_detail = IssueProjectLiteSerializer(read_only=True, source="related_issue")
|
||||
issue_detail = IssueProjectLiteSerializer(read_only=True, source="related_issue")
|
||||
|
||||
class Meta:
|
||||
model = IssueRelation
|
||||
fields = [
|
||||
"related_issue_detail",
|
||||
"issue_detail",
|
||||
"relation_type",
|
||||
"related_issue",
|
||||
"issue",
|
||||
|
@ -53,6 +53,7 @@ from plane.api.serializers import (
|
||||
CommentReactionSerializer,
|
||||
IssueVoteSerializer,
|
||||
IssueRelationSerializer,
|
||||
RelatedIssueSerializer,
|
||||
IssuePublicSerializer,
|
||||
)
|
||||
from plane.api.permissions import (
|
||||
@ -2085,9 +2086,10 @@ class IssueRelationViewSet(BaseViewSet):
|
||||
def create(self, request, slug, project_id, issue_id):
|
||||
try:
|
||||
related_list = request.data.get("related_list", [])
|
||||
relation = request.data.get("relation", None)
|
||||
project = Project.objects.get(pk=project_id)
|
||||
|
||||
issueRelation = IssueRelation.objects.bulk_create(
|
||||
issue_relation = IssueRelation.objects.bulk_create(
|
||||
[
|
||||
IssueRelation(
|
||||
issue_id=related_issue["issue"],
|
||||
@ -2112,11 +2114,17 @@ class IssueRelationViewSet(BaseViewSet):
|
||||
project_id=str(project_id),
|
||||
current_instance=None,
|
||||
)
|
||||
|
||||
return Response(
|
||||
IssueRelationSerializer(issueRelation, many=True).data,
|
||||
status=status.HTTP_201_CREATED,
|
||||
)
|
||||
|
||||
if relation == "blocking":
|
||||
return Response(
|
||||
RelatedIssueSerializer(issue_relation, many=True).data,
|
||||
status=status.HTTP_201_CREATED,
|
||||
)
|
||||
else:
|
||||
return Response(
|
||||
IssueRelationSerializer(issue_relation, many=True).data,
|
||||
status=status.HTTP_201_CREATED,
|
||||
)
|
||||
except IntegrityError as e:
|
||||
if "already exists" in str(e):
|
||||
return Response(
|
||||
|
@ -1048,6 +1048,25 @@ def create_issue_relation_activity(
|
||||
)
|
||||
if current_instance is None and requested_data.get("related_list") is not None:
|
||||
for issue_relation in requested_data.get("related_list"):
|
||||
if issue_relation.get("relation_type") == "blocked_by":
|
||||
relation_type = "blocking"
|
||||
else:
|
||||
relation_type = issue_relation.get("relation_type")
|
||||
issue = Issue.objects.get(pk=issue_relation.get("issue"))
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_relation.get("related_issue"),
|
||||
actor=actor,
|
||||
verb="created",
|
||||
old_value="",
|
||||
new_value=f"{project.identifier}-{issue.sequence_id}",
|
||||
field=relation_type,
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f'added {relation_type} relation',
|
||||
old_identifier=issue_relation.get("issue"),
|
||||
)
|
||||
)
|
||||
issue = Issue.objects.get(pk=issue_relation.get("related_issue"))
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
@ -1060,7 +1079,7 @@ def create_issue_relation_activity(
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f'added {issue_relation.get("relation_type")} relation',
|
||||
old_identifier=issue_relation.get("issue"),
|
||||
old_identifier=issue_relation.get("related_issue"),
|
||||
)
|
||||
)
|
||||
|
||||
@ -1073,21 +1092,40 @@ def delete_issue_relation_activity(
|
||||
json.loads(current_instance) if current_instance is not None else None
|
||||
)
|
||||
if current_instance is not None and requested_data.get("related_list") is None:
|
||||
issue = Issue.objects.get(pk=current_instance.get("issue"))
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=current_instance.get("issue"),
|
||||
actor=actor,
|
||||
verb="deleted",
|
||||
old_value=f"{project.identifier}-{issue.sequence_id}",
|
||||
new_value="",
|
||||
field=f'{current_instance.get("relation_type")}',
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f'deleted the {current_instance.get("relation_type")} relation',
|
||||
old_identifier=current_instance.get("issue"),
|
||||
if current_instance.get("relation_type") == "blocked_by":
|
||||
relation_type = "blocking"
|
||||
else:
|
||||
relation_type = current_instance.get("relation_type")
|
||||
issue = Issue.objects.get(pk=current_instance.get("issue"))
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=current_instance.get("related_issue"),
|
||||
actor=actor,
|
||||
verb="deleted",
|
||||
old_value="",
|
||||
new_value=f"{project.identifier}-{issue.sequence_id}",
|
||||
field=relation_type,
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f'deleted {relation_type} relation',
|
||||
old_identifier=current_instance.get("issue"),
|
||||
)
|
||||
)
|
||||
issue = Issue.objects.get(pk=current_instance.get("related_issue"))
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=current_instance.get("issue"),
|
||||
actor=actor,
|
||||
verb="deleted",
|
||||
old_value="",
|
||||
new_value=f"{project.identifier}-{issue.sequence_id}",
|
||||
field=f'{current_instance.get("relation_type")}',
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f'deleted {current_instance.get("relation_type")} relation',
|
||||
old_identifier=current_instance.get("related_issue"),
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def create_draft_issue_activity(
|
||||
|
23
apiserver/plane/db/migrations/0045_auto_20230915_0655.py
Normal file
23
apiserver/plane/db/migrations/0045_auto_20230915_0655.py
Normal file
@ -0,0 +1,23 @@
|
||||
# Generated by Django 4.2.3 on 2023-09-15 06:55
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
def update_issue_activity(apps, schema_editor):
|
||||
IssueActivityModel = apps.get_model("db", "IssueActivity")
|
||||
updated_issue_activity = []
|
||||
for obj in IssueActivityModel.objects.all():
|
||||
if obj.field == "blocks":
|
||||
obj.field = "blocked_by"
|
||||
updated_issue_activity.append(obj)
|
||||
IssueActivityModel.objects.bulk_update(updated_issue_activity, ["field"], batch_size=100)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('db', '0044_auto_20230913_0709'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(update_issue_activity),
|
||||
]
|
17
turbo.json
17
turbo.json
@ -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": {
|
||||
|
@ -52,10 +52,22 @@ const issueViewOptions: { type: TIssueViewOptions; Icon: any }[] = [
|
||||
},
|
||||
];
|
||||
|
||||
const issueViewForDraftIssues: { type: TIssueViewOptions; Icon: any }[] = [
|
||||
{
|
||||
type: "list",
|
||||
Icon: FormatListBulletedOutlined,
|
||||
},
|
||||
{
|
||||
type: "kanban",
|
||||
Icon: GridViewOutlined,
|
||||
},
|
||||
];
|
||||
|
||||
export const IssuesFilterView: React.FC = () => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, viewId } = router.query;
|
||||
const isArchivedIssues = router.pathname.includes("archived-issues");
|
||||
const isDraftIssues = router.pathname.includes("draft-issues");
|
||||
|
||||
const {
|
||||
displayFilters,
|
||||
@ -75,7 +87,7 @@ export const IssuesFilterView: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
{!isArchivedIssues && (
|
||||
{!isArchivedIssues && !isDraftIssues && (
|
||||
<div className="flex items-center gap-x-1">
|
||||
{issueViewOptions.map((option) => (
|
||||
<Tooltip
|
||||
@ -105,6 +117,36 @@ export const IssuesFilterView: React.FC = () => {
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{isDraftIssues && (
|
||||
<div className="flex items-center gap-x-1">
|
||||
{issueViewForDraftIssues.map((option) => (
|
||||
<Tooltip
|
||||
key={option.type}
|
||||
tooltipContent={
|
||||
<span className="capitalize">{replaceUnderscoreIfSnakeCase(option.type)} View</span>
|
||||
}
|
||||
position="bottom"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none hover:bg-custom-sidebar-background-80 duration-300 ${
|
||||
displayFilters.layout === option.type
|
||||
? "bg-custom-sidebar-background-80"
|
||||
: "text-custom-sidebar-text-200"
|
||||
}`}
|
||||
onClick={() => setDisplayFilters({ layout: option.type })}
|
||||
>
|
||||
<option.Icon
|
||||
sx={{
|
||||
fontSize: 16,
|
||||
}}
|
||||
className={option.type === "gantt_chart" ? "rotate-90" : ""}
|
||||
/>
|
||||
</button>
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<SelectFilters
|
||||
filters={filters}
|
||||
onSelect={(option) => {
|
||||
|
@ -50,6 +50,7 @@ type Props = {
|
||||
secondaryButton?: React.ReactNode;
|
||||
};
|
||||
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
|
||||
handleDraftIssueAction?: (issue: IIssue, action: "edit" | "delete") => void;
|
||||
handleOnDragEnd: (result: DropResult) => Promise<void>;
|
||||
openIssuesListModal: (() => void) | null;
|
||||
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
|
||||
@ -66,6 +67,7 @@ export const AllViews: React.FC<Props> = ({
|
||||
dragDisabled = false,
|
||||
emptyState,
|
||||
handleIssueAction,
|
||||
handleDraftIssueAction,
|
||||
handleOnDragEnd,
|
||||
openIssuesListModal,
|
||||
removeIssue,
|
||||
@ -132,6 +134,7 @@ export const AllViews: React.FC<Props> = ({
|
||||
states={states}
|
||||
addIssueToGroup={addIssueToGroup}
|
||||
handleIssueAction={handleIssueAction}
|
||||
handleDraftIssueAction={handleDraftIssueAction}
|
||||
openIssuesListModal={cycleId || moduleId ? openIssuesListModal : null}
|
||||
removeIssue={removeIssue}
|
||||
myIssueProjectId={myIssueProjectId}
|
||||
@ -149,6 +152,7 @@ export const AllViews: React.FC<Props> = ({
|
||||
disableAddIssueOption={disableAddIssueOption}
|
||||
dragDisabled={dragDisabled}
|
||||
handleIssueAction={handleIssueAction}
|
||||
handleDraftIssueAction={handleDraftIssueAction}
|
||||
handleTrashBox={handleTrashBox}
|
||||
openIssuesListModal={cycleId || moduleId ? openIssuesListModal : null}
|
||||
myIssueProjectId={myIssueProjectId}
|
||||
|
@ -20,6 +20,7 @@ type Props = {
|
||||
disableAddIssueOption?: boolean;
|
||||
dragDisabled: boolean;
|
||||
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
|
||||
handleDraftIssueAction?: (issue: IIssue, action: "edit" | "delete") => void;
|
||||
handleTrashBox: (isDragging: boolean) => void;
|
||||
openIssuesListModal?: (() => void) | null;
|
||||
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
|
||||
@ -37,6 +38,7 @@ export const AllBoards: React.FC<Props> = ({
|
||||
disableAddIssueOption = false,
|
||||
dragDisabled,
|
||||
handleIssueAction,
|
||||
handleDraftIssueAction,
|
||||
handleTrashBox,
|
||||
openIssuesListModal,
|
||||
myIssueProjectId,
|
||||
@ -94,6 +96,7 @@ export const AllBoards: React.FC<Props> = ({
|
||||
dragDisabled={dragDisabled}
|
||||
groupTitle={singleGroup}
|
||||
handleIssueAction={handleIssueAction}
|
||||
handleDraftIssueAction={handleDraftIssueAction}
|
||||
handleTrashBox={handleTrashBox}
|
||||
openIssuesListModal={openIssuesListModal ?? null}
|
||||
handleMyIssueOpen={handleMyIssueOpen}
|
||||
|
@ -24,6 +24,7 @@ type Props = {
|
||||
dragDisabled: boolean;
|
||||
groupTitle: string;
|
||||
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
|
||||
handleDraftIssueAction?: (issue: IIssue, action: "edit" | "delete") => void;
|
||||
handleTrashBox: (isDragging: boolean) => void;
|
||||
openIssuesListModal?: (() => void) | null;
|
||||
handleMyIssueOpen?: (issue: IIssue) => void;
|
||||
@ -41,6 +42,7 @@ export const SingleBoard: React.FC<Props> = ({
|
||||
disableAddIssueOption = false,
|
||||
dragDisabled,
|
||||
handleIssueAction,
|
||||
handleDraftIssueAction,
|
||||
handleTrashBox,
|
||||
openIssuesListModal,
|
||||
handleMyIssueOpen,
|
||||
@ -136,6 +138,16 @@ export const SingleBoard: React.FC<Props> = ({
|
||||
editIssue={() => handleIssueAction(issue, "edit")}
|
||||
makeIssueCopy={() => handleIssueAction(issue, "copy")}
|
||||
handleDeleteIssue={() => handleIssueAction(issue, "delete")}
|
||||
handleDraftIssueEdit={
|
||||
handleDraftIssueAction
|
||||
? () => handleDraftIssueAction(issue, "edit")
|
||||
: undefined
|
||||
}
|
||||
handleDraftIssueDelete={() =>
|
||||
handleDraftIssueAction
|
||||
? handleDraftIssueAction(issue, "delete")
|
||||
: undefined
|
||||
}
|
||||
handleTrashBox={handleTrashBox}
|
||||
handleMyIssueOpen={handleMyIssueOpen}
|
||||
removeIssue={() => {
|
||||
@ -155,7 +167,7 @@ export const SingleBoard: React.FC<Props> = ({
|
||||
display: displayFilters?.order_by === "sort_order" ? "inline" : "none",
|
||||
}}
|
||||
>
|
||||
{provided.placeholder}
|
||||
<>{provided.placeholder}</>
|
||||
</span>
|
||||
</div>
|
||||
{displayFilters?.group_by !== "created_by" && (
|
||||
|
@ -60,6 +60,8 @@ type Props = {
|
||||
handleMyIssueOpen?: (issue: IIssue) => void;
|
||||
removeIssue?: (() => void) | null;
|
||||
handleDeleteIssue: (issue: IIssue) => void;
|
||||
handleDraftIssueEdit?: () => void;
|
||||
handleDraftIssueDelete?: () => void;
|
||||
handleTrashBox: (isDragging: boolean) => void;
|
||||
disableUserActions: boolean;
|
||||
user: ICurrentUserResponse | undefined;
|
||||
@ -79,6 +81,8 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
removeIssue,
|
||||
groupTitle,
|
||||
handleDeleteIssue,
|
||||
handleDraftIssueEdit,
|
||||
handleDraftIssueDelete,
|
||||
handleTrashBox,
|
||||
disableUserActions,
|
||||
user,
|
||||
@ -99,6 +103,8 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
||||
|
||||
const isDraftIssue = router.pathname.includes("draft-issues");
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const partialUpdateIssue = useCallback(
|
||||
@ -211,29 +217,47 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
>
|
||||
{!isNotAllowed && (
|
||||
<>
|
||||
<ContextMenu.Item Icon={PencilIcon} onClick={editIssue}>
|
||||
<ContextMenu.Item
|
||||
Icon={PencilIcon}
|
||||
onClick={() => {
|
||||
if (isDraftIssue && handleDraftIssueEdit) handleDraftIssueEdit();
|
||||
else editIssue();
|
||||
}}
|
||||
>
|
||||
Edit issue
|
||||
</ContextMenu.Item>
|
||||
<ContextMenu.Item Icon={ClipboardDocumentCheckIcon} onClick={makeIssueCopy}>
|
||||
Make a copy...
|
||||
</ContextMenu.Item>
|
||||
<ContextMenu.Item Icon={TrashIcon} onClick={() => handleDeleteIssue(issue)}>
|
||||
{!isDraftIssue && (
|
||||
<ContextMenu.Item Icon={ClipboardDocumentCheckIcon} onClick={makeIssueCopy}>
|
||||
Make a copy...
|
||||
</ContextMenu.Item>
|
||||
)}
|
||||
<ContextMenu.Item
|
||||
Icon={TrashIcon}
|
||||
onClick={() => {
|
||||
if (isDraftIssue && handleDraftIssueDelete) handleDraftIssueDelete();
|
||||
else handleDeleteIssue(issue);
|
||||
}}
|
||||
>
|
||||
Delete issue
|
||||
</ContextMenu.Item>
|
||||
</>
|
||||
)}
|
||||
<ContextMenu.Item Icon={LinkIcon} onClick={handleCopyText}>
|
||||
Copy issue link
|
||||
</ContextMenu.Item>
|
||||
<a
|
||||
href={`/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
<ContextMenu.Item Icon={ArrowTopRightOnSquareIcon}>
|
||||
Open issue in new tab
|
||||
{!isDraftIssue && (
|
||||
<ContextMenu.Item Icon={LinkIcon} onClick={handleCopyText}>
|
||||
Copy issue link
|
||||
</ContextMenu.Item>
|
||||
</a>
|
||||
)}
|
||||
{!isDraftIssue && (
|
||||
<a
|
||||
href={`/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
<ContextMenu.Item Icon={ArrowTopRightOnSquareIcon}>
|
||||
Open issue in new tab
|
||||
</ContextMenu.Item>
|
||||
</a>
|
||||
)}
|
||||
</ContextMenu>
|
||||
<div
|
||||
className={`mb-3 rounded bg-custom-background-100 shadow ${
|
||||
@ -268,13 +292,18 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
</button>
|
||||
}
|
||||
>
|
||||
<CustomMenu.MenuItem onClick={editIssue}>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
if (isDraftIssue && handleDraftIssueEdit) handleDraftIssueEdit();
|
||||
else editIssue();
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
<PencilIcon className="h-4 w-4" />
|
||||
<span>Edit issue</span>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
{type !== "issue" && removeIssue && (
|
||||
{type !== "issue" && removeIssue && !isDraftIssue && (
|
||||
<CustomMenu.MenuItem onClick={removeIssue}>
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
<XMarkIcon className="h-4 w-4" />
|
||||
@ -282,18 +311,25 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
<CustomMenu.MenuItem onClick={() => handleDeleteIssue(issue)}>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
if (isDraftIssue && handleDraftIssueDelete) handleDraftIssueDelete();
|
||||
else handleDeleteIssue(issue);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
<span>Delete issue</span>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem onClick={handleCopyText}>
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
<LinkIcon className="h-4 w-4" />
|
||||
<span>Copy issue Link</span>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
{!isDraftIssue && (
|
||||
<CustomMenu.MenuItem onClick={handleCopyText}>
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
<LinkIcon className="h-4 w-4" />
|
||||
<span>Copy issue Link</span>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
</CustomMenu>
|
||||
)}
|
||||
</div>
|
||||
@ -308,7 +344,10 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
<button
|
||||
type="button"
|
||||
className="text-sm text-left break-words line-clamp-2"
|
||||
onClick={openPeekOverview}
|
||||
onClick={() => {
|
||||
if (isDraftIssue && handleDraftIssueEdit) handleDraftIssueEdit();
|
||||
else openPeekOverview();
|
||||
}}
|
||||
>
|
||||
{issue.name}
|
||||
</button>
|
||||
|
@ -19,7 +19,13 @@ 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,
|
||||
DeleteDraftIssueModal,
|
||||
IssuePeekOverview,
|
||||
CreateUpdateDraftIssueModal,
|
||||
} from "components/issues";
|
||||
import { CreateUpdateViewModal } from "components/views";
|
||||
// ui
|
||||
import { PrimaryButton, SecondaryButton } from "components/ui";
|
||||
@ -70,8 +76,13 @@ export const IssuesView: React.FC<Props> = ({
|
||||
// trash box
|
||||
const [trashBox, setTrashBox] = useState(false);
|
||||
|
||||
// selected draft issue
|
||||
const [selectedDraftIssue, setSelectedDraftIssue] = useState<IIssue | null>(null);
|
||||
const [selectedDraftForDelete, setSelectDraftForDelete] = useState<IIssue | null>(null);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
|
||||
const isDraftIssues = router.asPath.includes("draft-issues");
|
||||
|
||||
const { user } = useUserAuth();
|
||||
|
||||
@ -106,6 +117,9 @@ export const IssuesView: React.FC<Props> = ({
|
||||
[setDeleteIssueModal, setIssueToDelete]
|
||||
);
|
||||
|
||||
const handleDraftIssueClick = useCallback((issue: any) => setSelectedDraftIssue(issue), []);
|
||||
const handleDraftIssueDelete = useCallback((issue: any) => setSelectDraftForDelete(issue), []);
|
||||
|
||||
const handleOnDragEnd = useCallback(
|
||||
async (result: DropResult) => {
|
||||
setTrashBox(false);
|
||||
@ -343,6 +357,14 @@ export const IssuesView: React.FC<Props> = ({
|
||||
[makeIssueCopy, handleEditIssue, handleDeleteIssue]
|
||||
);
|
||||
|
||||
const handleDraftIssueAction = useCallback(
|
||||
(issue: IIssue, action: "edit" | "delete") => {
|
||||
if (action === "edit") handleDraftIssueClick(issue);
|
||||
else if (action === "delete") handleDraftIssueDelete(issue);
|
||||
},
|
||||
[handleDraftIssueClick, handleDraftIssueDelete]
|
||||
);
|
||||
|
||||
const removeIssueFromCycle = useCallback(
|
||||
(bridgeId: string, issueId: string) => {
|
||||
if (!workspaceSlug || !projectId || !cycleId) return;
|
||||
@ -451,6 +473,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)}
|
||||
@ -462,6 +505,11 @@ export const IssuesView: React.FC<Props> = ({
|
||||
data={issueToDelete}
|
||||
user={user}
|
||||
/>
|
||||
<DeleteDraftIssueModal
|
||||
data={selectedDraftForDelete}
|
||||
isOpen={selectedDraftForDelete !== null}
|
||||
handleClose={() => setSelectDraftForDelete(null)}
|
||||
/>
|
||||
|
||||
{areFiltersApplied && (
|
||||
<>
|
||||
@ -518,23 +566,28 @@ export const IssuesView: React.FC<Props> = ({
|
||||
displayFilters.group_by === "assignees"
|
||||
}
|
||||
emptyState={{
|
||||
title: cycleId
|
||||
title: isDraftIssues
|
||||
? "Draft issues will appear here"
|
||||
: cycleId
|
||||
? "Cycle issues will appear here"
|
||||
: moduleId
|
||||
? "Module issues will appear here"
|
||||
: "Project issues will appear here",
|
||||
description:
|
||||
"Issues help you track individual pieces of work. With Issues, keep track of what's going on, who is working on it, and what's done.",
|
||||
primaryButton: {
|
||||
icon: <PlusIcon className="h-4 w-4" />,
|
||||
text: "New Issue",
|
||||
onClick: () => {
|
||||
const e = new KeyboardEvent("keydown", {
|
||||
key: "c",
|
||||
});
|
||||
document.dispatchEvent(e);
|
||||
},
|
||||
},
|
||||
description: isDraftIssues
|
||||
? "Draft issues are issues that are not yet created."
|
||||
: "Issues help you track individual pieces of work. With Issues, keep track of what's going on, who is working on it, and what's done.",
|
||||
primaryButton: !isDraftIssues
|
||||
? {
|
||||
icon: <PlusIcon className="h-4 w-4" />,
|
||||
text: "New Issue",
|
||||
onClick: () => {
|
||||
const e = new KeyboardEvent("keydown", {
|
||||
key: "c",
|
||||
});
|
||||
document.dispatchEvent(e);
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
secondaryButton:
|
||||
cycleId || moduleId ? (
|
||||
<SecondaryButton
|
||||
@ -548,6 +601,7 @@ export const IssuesView: React.FC<Props> = ({
|
||||
}}
|
||||
handleOnDragEnd={handleOnDragEnd}
|
||||
handleIssueAction={handleIssueAction}
|
||||
handleDraftIssueAction={handleDraftIssueAction}
|
||||
openIssuesListModal={openIssuesListModal ?? null}
|
||||
removeIssue={cycleId ? removeIssueFromCycle : moduleId ? removeIssueFromModule : null}
|
||||
trashBox={trashBox}
|
||||
|
@ -15,6 +15,7 @@ type Props = {
|
||||
states: IState[] | undefined;
|
||||
addIssueToGroup: (groupTitle: string) => void;
|
||||
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
|
||||
handleDraftIssueAction?: (issue: IIssue, action: "edit" | "delete") => void;
|
||||
openIssuesListModal?: (() => void) | null;
|
||||
myIssueProjectId?: string | null;
|
||||
handleMyIssueOpen?: (issue: IIssue) => void;
|
||||
@ -36,6 +37,7 @@ export const AllLists: React.FC<Props> = ({
|
||||
myIssueProjectId,
|
||||
removeIssue,
|
||||
states,
|
||||
handleDraftIssueAction,
|
||||
user,
|
||||
userAuth,
|
||||
viewProps,
|
||||
@ -82,6 +84,7 @@ export const AllLists: React.FC<Props> = ({
|
||||
groupTitle={singleGroup}
|
||||
currentState={currentState}
|
||||
addIssueToGroup={() => addIssueToGroup(singleGroup)}
|
||||
handleDraftIssueAction={handleDraftIssueAction}
|
||||
handleIssueAction={handleIssueAction}
|
||||
handleMyIssueOpen={handleMyIssueOpen}
|
||||
openIssuesListModal={openIssuesListModal}
|
||||
|
@ -1,6 +1,5 @@
|
||||
import React, { useCallback, useState } from "react";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { mutate } from "swr";
|
||||
@ -18,6 +17,7 @@ import {
|
||||
ViewPrioritySelect,
|
||||
ViewStartDateSelect,
|
||||
ViewStateSelect,
|
||||
CreateUpdateDraftIssueModal,
|
||||
} from "components/issues";
|
||||
// ui
|
||||
import { Tooltip, CustomMenu, ContextMenu } from "components/ui";
|
||||
@ -61,6 +61,8 @@ type Props = {
|
||||
makeIssueCopy: () => void;
|
||||
removeIssue?: (() => void) | null;
|
||||
handleDeleteIssue: (issue: IIssue) => void;
|
||||
handleDraftIssueSelect?: (issue: IIssue) => void;
|
||||
handleDraftIssueDelete?: (issue: IIssue) => void;
|
||||
handleMyIssueOpen?: (issue: IIssue) => void;
|
||||
disableUserActions: boolean;
|
||||
user: ICurrentUserResponse | undefined;
|
||||
@ -76,12 +78,14 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
makeIssueCopy,
|
||||
removeIssue,
|
||||
groupTitle,
|
||||
handleDraftIssueDelete,
|
||||
handleDeleteIssue,
|
||||
handleMyIssueOpen,
|
||||
disableUserActions,
|
||||
user,
|
||||
userAuth,
|
||||
viewProps,
|
||||
handleDraftIssueSelect,
|
||||
}) => {
|
||||
// context menu
|
||||
const [contextMenu, setContextMenu] = useState(false);
|
||||
@ -90,6 +94,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 +183,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) => {
|
||||
@ -203,26 +210,45 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
>
|
||||
{!isNotAllowed && (
|
||||
<>
|
||||
<ContextMenu.Item Icon={PencilIcon} onClick={editIssue}>
|
||||
<ContextMenu.Item
|
||||
Icon={PencilIcon}
|
||||
onClick={() => {
|
||||
if (isDraftIssues && handleDraftIssueSelect) handleDraftIssueSelect(issue);
|
||||
else editIssue();
|
||||
}}
|
||||
>
|
||||
Edit issue
|
||||
</ContextMenu.Item>
|
||||
<ContextMenu.Item Icon={ClipboardDocumentCheckIcon} onClick={makeIssueCopy}>
|
||||
Make a copy...
|
||||
</ContextMenu.Item>
|
||||
<ContextMenu.Item Icon={TrashIcon} onClick={() => handleDeleteIssue(issue)}>
|
||||
{!isDraftIssues && (
|
||||
<ContextMenu.Item Icon={ClipboardDocumentCheckIcon} onClick={makeIssueCopy}>
|
||||
Make a copy...
|
||||
</ContextMenu.Item>
|
||||
)}
|
||||
<ContextMenu.Item
|
||||
Icon={TrashIcon}
|
||||
onClick={() => {
|
||||
if (isDraftIssues && handleDraftIssueDelete) handleDraftIssueDelete(issue);
|
||||
else handleDeleteIssue(issue);
|
||||
}}
|
||||
>
|
||||
Delete issue
|
||||
</ContextMenu.Item>
|
||||
</>
|
||||
)}
|
||||
<ContextMenu.Item Icon={LinkIcon} onClick={handleCopyText}>
|
||||
Copy issue link
|
||||
</ContextMenu.Item>
|
||||
<a href={issuePath} target="_blank" rel="noreferrer noopener">
|
||||
<ContextMenu.Item Icon={ArrowTopRightOnSquareIcon}>
|
||||
Open issue in new tab
|
||||
</ContextMenu.Item>
|
||||
</a>
|
||||
{!isDraftIssues && (
|
||||
<>
|
||||
<ContextMenu.Item Icon={LinkIcon} onClick={handleCopyText}>
|
||||
Copy issue link
|
||||
</ContextMenu.Item>
|
||||
<a href={issuePath} target="_blank" rel="noreferrer noopener">
|
||||
<ContextMenu.Item Icon={ArrowTopRightOnSquareIcon}>
|
||||
Open issue in new tab
|
||||
</ContextMenu.Item>
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
</ContextMenu>
|
||||
|
||||
<div
|
||||
className="flex items-center justify-between px-4 py-2.5 gap-10 border-b border-custom-border-200 bg-custom-background-100 last:border-b-0"
|
||||
onContextMenu={(e) => {
|
||||
@ -247,7 +273,10 @@ 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 (isDraftIssues && handleDraftIssueSelect) handleDraftIssueSelect(issue);
|
||||
}}
|
||||
>
|
||||
{issue.name}
|
||||
</button>
|
||||
@ -345,7 +374,12 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
)}
|
||||
{type && !isNotAllowed && (
|
||||
<CustomMenu width="auto" ellipsis>
|
||||
<CustomMenu.MenuItem onClick={editIssue}>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
if (isDraftIssues && handleDraftIssueSelect) handleDraftIssueSelect(issue);
|
||||
else editIssue();
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
<PencilIcon className="h-4 w-4" />
|
||||
<span>Edit issue</span>
|
||||
@ -359,18 +393,25 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
<CustomMenu.MenuItem onClick={() => handleDeleteIssue(issue)}>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
if (isDraftIssues && handleDraftIssueDelete) handleDraftIssueDelete(issue);
|
||||
else handleDeleteIssue(issue);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
<span>Delete issue</span>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem onClick={handleCopyText}>
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
<LinkIcon className="h-4 w-4" />
|
||||
<span>Copy issue link</span>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
{!isDraftIssues && (
|
||||
<CustomMenu.MenuItem onClick={handleCopyText}>
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
<LinkIcon className="h-4 w-4" />
|
||||
<span>Copy issue link</span>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
</CustomMenu>
|
||||
)}
|
||||
</div>
|
||||
|
@ -40,6 +40,7 @@ type Props = {
|
||||
groupTitle: string;
|
||||
addIssueToGroup: () => void;
|
||||
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
|
||||
handleDraftIssueAction?: (issue: IIssue, action: "edit" | "delete") => void;
|
||||
openIssuesListModal?: (() => void) | null;
|
||||
handleMyIssueOpen?: (issue: IIssue) => void;
|
||||
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
|
||||
@ -56,6 +57,7 @@ export const SingleList: React.FC<Props> = ({
|
||||
addIssueToGroup,
|
||||
handleIssueAction,
|
||||
openIssuesListModal,
|
||||
handleDraftIssueAction,
|
||||
handleMyIssueOpen,
|
||||
removeIssue,
|
||||
disableUserActions,
|
||||
@ -253,6 +255,16 @@ export const SingleList: React.FC<Props> = ({
|
||||
editIssue={() => handleIssueAction(issue, "edit")}
|
||||
makeIssueCopy={() => handleIssueAction(issue, "copy")}
|
||||
handleDeleteIssue={() => handleIssueAction(issue, "delete")}
|
||||
handleDraftIssueSelect={
|
||||
handleDraftIssueAction
|
||||
? () => handleDraftIssueAction(issue, "edit")
|
||||
: undefined
|
||||
}
|
||||
handleDraftIssueDelete={
|
||||
handleDraftIssueAction
|
||||
? () => handleDraftIssueAction(issue, "delete")
|
||||
: undefined
|
||||
}
|
||||
handleMyIssueOpen={handleMyIssueOpen}
|
||||
removeIssue={() => {
|
||||
if (removeIssue !== null && issue.bridge_id)
|
||||
|
93
web/components/issues/confirm-issue-discard.tsx
Normal file
93
web/components/issues/confirm-issue-discard.tsx
Normal 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>
|
||||
);
|
||||
};
|
145
web/components/issues/delete-draft-issue-modal.tsx
Normal file
145
web/components/issues/delete-draft-issue-modal.tsx
Normal file
@ -0,0 +1,145 @@
|
||||
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 issueServices from "services/issues.service";
|
||||
// hooks
|
||||
import useIssuesView from "hooks/use-issues-view";
|
||||
import useToast from "hooks/use-toast";
|
||||
// icons
|
||||
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
|
||||
// ui
|
||||
import { SecondaryButton, DangerButton } from "components/ui";
|
||||
// types
|
||||
import type { IIssue, ICurrentUserResponse } from "types";
|
||||
// fetch-keys
|
||||
import { PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS } from "constants/fetch-keys";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
handleClose: () => void;
|
||||
data: IIssue | null;
|
||||
user?: ICurrentUserResponse;
|
||||
onSubmit?: () => Promise<void> | void;
|
||||
};
|
||||
|
||||
export const DeleteDraftIssueModal: React.FC<Props> = (props) => {
|
||||
const { isOpen, handleClose, data, user, onSubmit } = props;
|
||||
|
||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const { params } = useIssuesView();
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
useEffect(() => {
|
||||
setIsDeleteLoading(false);
|
||||
}, [isOpen]);
|
||||
|
||||
const onClose = () => {
|
||||
setIsDeleteLoading(false);
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const handleDeletion = async () => {
|
||||
if (!workspaceSlug || !data) return;
|
||||
|
||||
setIsDeleteLoading(true);
|
||||
|
||||
await issueServices
|
||||
.deleteDraftIssue(workspaceSlug as string, data.project, data.id)
|
||||
.then(() => {
|
||||
setIsDeleteLoading(false);
|
||||
handleClose();
|
||||
mutate(PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS(projectId as string, params));
|
||||
setToastAlert({
|
||||
title: "Success",
|
||||
message: "Draft Issue deleted successfully",
|
||||
type: "success",
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
handleClose();
|
||||
setToastAlert({
|
||||
title: "Error",
|
||||
message: "Something went wrong",
|
||||
type: "error",
|
||||
});
|
||||
setIsDeleteLoading(false);
|
||||
});
|
||||
if (onSubmit) await onSubmit();
|
||||
};
|
||||
|
||||
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="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
||||
<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-full sm:max-w-2xl">
|
||||
<div className="flex flex-col gap-6 p-6">
|
||||
<div className="flex w-full items-center justify-start gap-6">
|
||||
<span className="place-items-center rounded-full bg-red-500/20 p-4">
|
||||
<ExclamationTriangleIcon
|
||||
className="h-6 w-6 text-red-600"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</span>
|
||||
<span className="flex items-center justify-start">
|
||||
<h3 className="text-xl font-medium 2xl:text-2xl">Delete Draft Issue</h3>
|
||||
</span>
|
||||
</div>
|
||||
<span>
|
||||
<p className="text-sm text-custom-text-200">
|
||||
Are you sure you want to delete issue{" "}
|
||||
<span className="break-words font-medium text-custom-text-100">
|
||||
{data?.project_detail.identifier}-{data?.sequence_id}
|
||||
</span>
|
||||
{""}? All of the data related to the draft issue will be permanently removed.
|
||||
This action cannot be undone.
|
||||
</p>
|
||||
</span>
|
||||
<div className="flex justify-end gap-2">
|
||||
<SecondaryButton onClick={onClose}>Cancel</SecondaryButton>
|
||||
<DangerButton onClick={handleDeletion} loading={isDeleteLoading}>
|
||||
{isDeleteLoading ? "Deleting..." : "Delete Issue"}
|
||||
</DangerButton>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
};
|
580
web/components/issues/draft-issue-form.tsx
Normal file
580
web/components/issues/draft-issue-form.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
};
|
285
web/components/issues/draft-issue-modal.tsx
Normal file
285
web/components/issues/draft-issue-modal.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
};
|
@ -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,23 @@ export const IssueForm: FC<IssueFormProps> = ({
|
||||
|
||||
const issueName = watch("name");
|
||||
|
||||
const payload: Partial<IIssue> = {
|
||||
name: getValues("name"),
|
||||
description: getValues("description"),
|
||||
state: getValues("state"),
|
||||
priority: getValues("priority"),
|
||||
assignees: getValues("assignees"),
|
||||
target_date: getValues("target_date"),
|
||||
labels: getValues("labels"),
|
||||
project: getValues("project"),
|
||||
};
|
||||
|
||||
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 +569,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
|
||||
|
@ -16,3 +16,9 @@ export * from "./sub-issues-list";
|
||||
export * from "./label";
|
||||
export * from "./issue-reaction";
|
||||
export * from "./peek-overview";
|
||||
export * from "./confirm-issue-discard";
|
||||
|
||||
// draft issue
|
||||
export * from "./draft-issue-form";
|
||||
export * from "./draft-issue-modal";
|
||||
export * from "./delete-draft-issue-modal";
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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`)}
|
||||
>
|
||||
|
@ -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)}
|
||||
|
@ -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 {
|
||||
|
@ -1,47 +1,145 @@
|
||||
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",
|
||||
"state",
|
||||
"startDate",
|
||||
"project",
|
||||
]}
|
||||
/>
|
||||
|
||||
<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-2 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-1.5 ${
|
||||
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 mr-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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -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"
|
||||
|
@ -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) =>
|
||||
|
@ -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,
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
|
@ -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": {
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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 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;
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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" });
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
});
|
||||
});
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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) => {
|
||||
|
@ -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";
|
||||
|
||||
class ProjectCycleServices extends APIService {
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
@ -21,7 +18,7 @@ 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 @@ 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 @@ 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 @@ 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) => {
|
||||
|
@ -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) => {
|
||||
|
@ -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) => {
|
||||
|
@ -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) => {
|
||||
|
@ -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) => {
|
||||
|
@ -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) => {
|
||||
|
@ -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) => {
|
||||
|
@ -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";
|
||||
|
||||
class ProjectIssuesServices extends APIService {
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
@ -28,7 +25,7 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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;
|
||||
|
@ -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";
|
||||
|
||||
class ProjectIssuesServices extends APIService {
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
@ -31,7 +26,7 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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) => {
|
||||
|
@ -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) => {
|
||||
|
@ -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) => {
|
||||
|
@ -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) => {
|
||||
|
@ -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) => {
|
||||
|
@ -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 @@ 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 @@ 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 @@ 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 @@ 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) => {
|
||||
|
@ -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();
|
||||
|
@ -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";
|
||||
|
||||
class UserService extends APIService {
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
@ -68,13 +65,12 @@ 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 @@ 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) => {
|
||||
|
@ -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";
|
||||
|
||||
class ViewServices extends APIService {
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
@ -26,7 +19,7 @@ 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 @@ 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 @@ 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 @@ 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) => {
|
||||
|
@ -16,9 +16,6 @@ import {
|
||||
IWorkspaceViewProps,
|
||||
} from "types";
|
||||
|
||||
const trackEvent =
|
||||
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
|
||||
|
||||
class WorkspaceService extends APIService {
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
@ -46,8 +43,7 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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) => {
|
||||
|
1
web/types/issues.d.ts
vendored
1
web/types/issues.d.ts
vendored
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user