Compare commits

..
180 changed files with 1307 additions and 4810 deletions
-2
View File
@@ -23,8 +23,6 @@ NEXT_PUBLIC_SLACK_CLIENT_ID=""
NEXT_PUBLIC_PLAUSIBLE_DOMAIN=""
# public boards deploy url
NEXT_PUBLIC_DEPLOY_URL=""
# plane deploy using nginx
NEXT_PUBLIC_DEPLOY_WITH_NGINX=1
# Backend
# Debug value for api server use it as 0 for production use
+1 -1
View File
@@ -2,7 +2,7 @@ name: Update Docker Images for Plane on Release
on:
release:
types: [released, prereleased]
types: [released]
jobs:
build_push_backend:
+1 -8
View File
@@ -1575,7 +1575,7 @@ class IssueCommentPublicViewSet(BaseViewSet):
)
)
.distinct()
).order_by("created_at")
)
else:
return IssueComment.objects.none()
except ProjectDeployBoard.DoesNotExist:
@@ -2100,12 +2100,6 @@ class ProjectIssuesPublicEndpoint(BaseAPIView):
queryset=IssueReaction.objects.select_related("actor"),
)
)
.prefetch_related(
Prefetch(
"votes",
queryset=IssueVote.objects.select_related("actor"),
)
)
.filter(**filters)
.annotate(cycle_id=F("issue_cycle__cycle_id"))
.annotate(module_id=F("issue_module__module_id"))
@@ -2195,7 +2189,6 @@ class ProjectIssuesPublicEndpoint(BaseAPIView):
states = (
State.objects.filter(
~Q(name="Triage"),
workspace__slug=slug,
project_id=project_id,
)
+3 -2
View File
@@ -482,7 +482,7 @@ class UserProjectInvitationsViewset(BaseViewSet):
# Delete joined project invites
project_invitations.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
return Response(status=status.HTTP_200_OK)
except Exception as e:
capture_exception(e)
return Response(
@@ -924,7 +924,8 @@ class ProjectUserViewsEndpoint(BaseAPIView):
project_member.save()
return Response(status=status.HTTP_204_NO_CONTENT)
return Response(status=status.HTTP_200_OK)
except Project.DoesNotExist:
return Response(
{"error": "The requested resource does not exists"},
+5 -5
View File
@@ -116,7 +116,7 @@ class WorkSpaceViewSet(BaseViewSet):
)
issue_count = (
Issue.issue_objects.filter(workspace=OuterRef("id"))
Issue.objects.filter(workspace=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
@@ -203,7 +203,7 @@ class UserWorkSpacesEndpoint(BaseAPIView):
)
issue_count = (
Issue.issue_objects.filter(workspace=OuterRef("id"))
Issue.objects.filter(workspace=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
@@ -532,7 +532,7 @@ class UserWorkspaceInvitationsEndpoint(BaseViewSet):
# Delete joined workspace invites
workspace_invitations.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
return Response(status=status.HTTP_200_OK)
except Exception as e:
capture_exception(e)
return Response(
@@ -846,7 +846,7 @@ class WorkspaceMemberUserViewsEndpoint(BaseAPIView):
workspace_member.view_props = request.data.get("view_props", {})
workspace_member.save()
return Response(status=status.HTTP_204_NO_CONTENT)
return Response(status=status.HTTP_200_OK)
except WorkspaceMember.DoesNotExist:
return Response(
{"error": "User not a member of workspace"},
@@ -1075,7 +1075,7 @@ class WorkspaceUserProfileStatsEndpoint(BaseAPIView):
priority_order = ["urgent", "high", "medium", "low", None]
priority_distribution = (
Issue.issue_objects.filter(
Issue.objects.filter(
workspace__slug=slug,
assignees__in=[user_id],
project__project_projectmember__member=request.user,
@@ -32,7 +32,7 @@ def archive_old_issues():
archive_in = project.archive_in
# Get all the issues whose updated_at in less that the archive_in month
issues = Issue.issue_objects.filter(
issues = Issue.objects.filter(
Q(
project=project_id,
archived_at__isnull=True,
@@ -64,22 +64,21 @@ def archive_old_issues():
issues_to_update.append(issue)
# Bulk Update the issues and log the activity
if issues_to_update:
updated_issues = Issue.objects.bulk_update(
issues_to_update, ["archived_at"], batch_size=100
updated_issues = Issue.objects.bulk_update(
issues_to_update, ["archived_at"], batch_size=100
)
[
issue_activity.delay(
type="issue.activity.updated",
requested_data=json.dumps({"archived_at": str(issue.archived_at)}),
actor_id=str(project.created_by_id),
issue_id=issue.id,
project_id=project_id,
current_instance=None,
subscriber=False,
)
[
issue_activity.delay(
type="issue.activity.updated",
requested_data=json.dumps({"archived_at": str(issue.archived_at)}),
actor_id=str(project.created_by_id),
issue_id=issue.id,
project_id=project_id,
current_instance=None,
subscriber=False,
)
for issue in updated_issues
]
for issue in updated_issues
]
return
except Exception as e:
if settings.DEBUG:
@@ -100,7 +99,7 @@ def close_old_issues():
close_in = project.close_in
# Get all the issues whose updated_at in less that the close_in month
issues = Issue.issue_objects.filter(
issues = Issue.objects.filter(
Q(
project=project_id,
archived_at__isnull=True,
@@ -137,20 +136,19 @@ def close_old_issues():
issues_to_update.append(issue)
# Bulk Update the issues and log the activity
if issues_to_update:
updated_issues = Issue.objects.bulk_update(issues_to_update, ["state"], batch_size=100)
[
issue_activity.delay(
type="issue.activity.updated",
requested_data=json.dumps({"closed_to": str(issue.state_id)}),
actor_id=str(project.created_by_id),
issue_id=issue.id,
project_id=project_id,
current_instance=None,
subscriber=False,
)
for issue in updated_issues
]
updated_issues = Issue.objects.bulk_update(issues_to_update, ["state"], batch_size=100)
[
issue_activity.delay(
type="issue.activity.updated",
requested_data=json.dumps({"closed_to": str(issue.state_id)}),
actor_id=str(project.created_by_id),
issue_id=issue.id,
project_id=project_id,
current_instance=None,
subscriber=False,
)
for issue in updated_issues
]
return
except Exception as e:
if settings.DEBUG:
+2 -2
View File
@@ -96,7 +96,7 @@ def burndown_plot(queryset, slug, project_id, cycle_id=None, module_id=None):
chart_data = {str(date): 0 for date in date_range}
completed_issues_distribution = (
Issue.issue_objects.filter(
Issue.objects.filter(
workspace__slug=slug,
project_id=project_id,
issue_cycle__cycle_id=cycle_id,
@@ -118,7 +118,7 @@ def burndown_plot(queryset, slug, project_id, cycle_id=None, module_id=None):
chart_data = {str(date): 0 for date in date_range}
completed_issues_distribution = (
Issue.issue_objects.filter(
Issue.objects.filter(
workspace__slug=slug,
project_id=project_id,
issue_module__module_id=module_id,
+13 -13
View File
@@ -1,36 +1,36 @@
# base requirements
Django==4.2.5
Django==4.2.3
django-braces==1.15.0
django-taggit==4.0.0
psycopg==3.1.10
psycopg==3.1.9
django-oauth-toolkit==2.3.0
mistune==3.0.1
djangorestframework==3.14.0
redis==4.6.0
django-nested-admin==4.0.2
django-cors-headers==4.2.0
django-cors-headers==4.1.0
whitenoise==6.5.0
django-allauth==0.55.2
django-allauth==0.54.0
faker==18.11.2
django-filter==23.2
jsonmodels==2.6.0
djangorestframework-simplejwt==5.3.0
sentry-sdk==1.30.0
djangorestframework-simplejwt==5.2.2
sentry-sdk==1.27.0
django-s3-storage==0.14.0
django-crum==0.7.9
django-guardian==2.4.0
dj_rest_auth==2.2.5
google-auth==2.22.0
google-api-python-client==2.97.0
google-auth==2.21.0
google-api-python-client==2.92.0
django-redis==5.3.0
uvicorn==0.23.2
uvicorn==0.22.0
channels==4.0.0
openai==0.28.0
openai==0.27.8
slack-sdk==3.21.3
celery==5.3.4
celery==5.3.1
django_celery_beat==2.5.0
psycopg-binary==3.1.10
psycopg-c==3.1.10
psycopg-binary==3.1.9
psycopg-c==3.1.9
scout-apm==2.26.1
openpyxl==3.1.2
+5 -5
View File
@@ -1,11 +1,11 @@
-r base.txt
dj-database-url==2.1.0
gunicorn==21.2.0
dj-database-url==2.0.0
gunicorn==20.1.0
whitenoise==6.5.0
django-storages==1.14
boto3==1.28.40
django-anymail==10.1
django-storages==1.13.2
boto3==1.27.0
django-anymail==10.0
django-debug-toolbar==4.1.0
gevent==23.7.0
psycogreen==1.0.2
+2 -2
View File
@@ -85,7 +85,7 @@ services:
plane-worker:
container_name: planebgworker
image: makeplane/plane-backend:latest
image: makeplane/plane-worker:latest
restart: always
command: ./bin/worker
env_file:
@@ -99,7 +99,7 @@ services:
plane-beat-worker:
container_name: planebeatworker
image: makeplane/plane-backend:latest
image: makeplane/plane-worker:latest
restart: always
command: ./bin/beat
env_file:
+2
View File
@@ -90,6 +90,8 @@ services:
DOCKER_BUILDKIT: 1
restart: always
command: ./bin/takeoff
ports:
- 8000:8000
env_file:
- .env
environment:
+1 -1
View File
@@ -12,4 +12,4 @@ fi
# Only perform action if $FROM and $TO are different.
echo "Replacing all statically built instances of $FROM with this string $TO ."
grep -R -la "${FROM}" $DIRECTORY/.next | xargs -I{} sed -i "s|$FROM|$TO|g" "{}"
grep -R -la "${FROM}" apps/$DIRECTORY/.next | xargs -I{} sed -i "s|$FROM|$TO|g" "{}"
-2
View File
@@ -1,2 +0,0 @@
// additional.d.ts
/// <reference types="next-images" />
@@ -131,7 +131,7 @@ export const OnBoardingForm: React.FC<Props> = observer(({ user }) => {
type="button"
className={`flex items-center justify-between gap-1 w-full rounded-md border border-custom-border-300 shadow-sm duration-300 focus:outline-none px-3 py-2 text-sm`}
>
<span className={value ? "" : "text-custom-text-400"}>{value || "Select your role..."}</span>
<span className="text-custom-text-400">{value || "Select your role..."}</span>
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
</Listbox.Button>
+5 -6
View File
@@ -13,12 +13,13 @@ import useToast from "hooks/use-toast";
// components
import { EmailPasswordForm, GithubLoginButton, GoogleLoginButton, EmailCodeForm } from "components/accounts";
// images
const imagePrefix = process.env.NEXT_PUBLIC_DEPLOY_WITH_NGINX ? "/spaces/" : "";
import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.svg";
export const SignInView = observer(() => {
const { user: userStore } = useMobxStore();
const router = useRouter();
const { next_path } = router.query;
const { setToastAlert } = useToast();
@@ -33,15 +34,13 @@ export const SignInView = observer(() => {
const onSignInSuccess = (response: any) => {
const isOnboarded = response?.user?.onboarding_step?.profile_complete || false;
const nextPath = router.asPath.includes("next_path") ? router.asPath.split("/?next_path=")[1] : "/";
userStore.setCurrentUser(response?.user);
if (!isOnboarded) {
router.push(`/onboarding?next_path=${nextPath}`);
router.push(`/onboarding?next_path=${next_path}`);
return;
}
router.push((nextPath ?? "/").toString());
router.push((next_path ?? "/").toString());
};
const handleGoogleSignIn = async ({ clientId, credential }: any) => {
@@ -112,7 +111,7 @@ export const SignInView = observer(() => {
<div className="fixed grid place-items-center bg-custom-background-100 sm:py-5 top-11 sm:top-12 left-7 sm:left-16 lg:left-28">
<div className="grid place-items-center bg-custom-background-100">
<div className="h-[30px] w-[30px]">
<img src={`${imagePrefix}/plane-logos/blue-without-text.png`} alt="Plane Logo" />
<Image src={BluePlaneLogoWithoutText} alt="Plane Logo" />
</div>
</div>
</div>
@@ -1,9 +1,17 @@
"use client";
// helpers
import { renderFullDate } from "helpers/date-time.helper";
import { renderFullDate } from "constants/helpers";
export const dueDateIconDetails = (
export const findHowManyDaysLeft = (date: string | Date) => {
const today = new Date();
const eventDate = new Date(date);
const timeDiff = Math.abs(eventDate.getTime() - today.getTime());
return Math.ceil(timeDiff / (1000 * 3600 * 24));
};
const dueDateIcon = (
date: string,
stateGroup: string
): {
@@ -18,24 +26,17 @@ export const dueDateIconDetails = (
className = "";
} else {
const today = new Date();
today.setHours(0, 0, 0, 0);
const targetDate = new Date(date);
targetDate.setHours(0, 0, 0, 0);
const dueDate = new Date(date);
const timeDifference = targetDate.getTime() - today.getTime();
if (timeDifference < 0) {
if (dueDate < today) {
iconName = "event_busy";
className = "text-red-500";
} else if (timeDifference === 0) {
iconName = "today";
className = "text-red-500";
} else if (timeDifference === 24 * 60 * 60 * 1000) {
iconName = "event";
className = "text-yellow-500";
} else {
} else if (dueDate > today) {
iconName = "calendar_today";
className = "";
} else {
iconName = "today";
className = "text-red-500";
}
}
@@ -46,7 +47,7 @@ export const dueDateIconDetails = (
};
export const IssueBlockDueDate = ({ due_date, group }: { due_date: string; group: string }) => {
const iconDetails = dueDateIconDetails(due_date, group);
const iconDetails = dueDateIcon(due_date, group);
return (
<div className="rounded flex px-2.5 py-1 items-center border-[0.5px] border-custom-border-300 gap-1 text-custom-text-100 text-xs">
@@ -1,22 +1,17 @@
import React, { useState } from "react";
// mobx
import React, { useEffect, useState, useRef } from "react";
import { useForm, Controller } from "react-hook-form";
import { observer } from "mobx-react-lite";
// react-hook-form
import { Controller, useForm } from "react-hook-form";
// headless ui
import { Menu, Transition } from "@headlessui/react";
// lib
import { useMobxStore } from "lib/mobx/store-provider";
// components
import { TipTapEditor } from "components/tiptap";
import { CommentReactions } from "components/issues/peek-overview";
// icons
import { ChatBubbleLeftEllipsisIcon, CheckIcon, XMarkIcon, EllipsisVerticalIcon } from "@heroicons/react/24/outline";
// helpers
import { timeAgo } from "helpers/date-time.helper";
// types
import { Comment } from "types/issue";
// components
import { TipTapEditor } from "components/tiptap";
type Props = {
workspaceSlug: string;
@@ -30,13 +25,10 @@ export const CommentCard: React.FC<Props> = observer((props) => {
// states
const [isEditing, setIsEditing] = useState(false);
const editorRef = React.useRef<any>(null);
const showEditorRef = React.useRef<any>(null);
const {
control,
formState: { isSubmitting },
handleSubmit,
control,
} = useForm<any>({
defaultValues: { comment_html: comment.comment_html },
});
@@ -50,9 +42,6 @@ export const CommentCard: React.FC<Props> = observer((props) => {
if (!workspaceSlug || !issueDetailStore.peekId) return;
issueDetailStore.updateIssueComment(workspaceSlug, comment.project, issueDetailStore.peekId, comment.id, formData);
setIsEditing(false);
editorRef.current?.setEditorValue(formData.comment_html);
showEditorRef.current?.setEditorValue(formData.comment_html);
};
return (
@@ -87,7 +76,7 @@ export const CommentCard: React.FC<Props> = observer((props) => {
{comment.actor_detail.is_bot ? comment.actor_detail.first_name + " Bot" : comment.actor_detail.display_name}
</div>
<p className="mt-0.5 text-xs text-custom-text-200">
<>commented {timeAgo(comment.created_at)}</>
<>Commented {timeAgo(comment.created_at)}</>
</p>
</div>
<div className="issue-comments-section p-0">
@@ -102,7 +91,6 @@ export const CommentCard: React.FC<Props> = observer((props) => {
render={({ field: { onChange, value } }) => (
<TipTapEditor
workspaceSlug={workspaceSlug as string}
ref={editorRef}
value={value}
debouncedUpdatesEnabled={false}
customClassName="min-h-[50px] p-3 shadow-sm"
@@ -132,13 +120,11 @@ export const CommentCard: React.FC<Props> = observer((props) => {
</form>
<div className={`${isEditing ? "hidden" : ""}`}>
<TipTapEditor
workspaceSlug={workspaceSlug as string}
ref={showEditorRef}
workspaceSlug={workspaceSlug.toString()}
value={comment.comment_html}
editable={false}
customClassName="text-xs border border-custom-border-200 bg-custom-background-100"
/>
<CommentReactions commentId={comment.id} projectId={comment.project} />
</div>
</div>
</div>
@@ -1,131 +0,0 @@
import React from "react";
import { useRouter } from "next/router";
// mobx
import { observer } from "mobx-react-lite";
import { useMobxStore } from "lib/mobx/store-provider";
// ui
import { ReactionSelector, Tooltip } from "components/ui";
// helpers
import { groupReactions, renderEmoji } from "helpers/emoji.helper";
type Props = {
commentId: string;
projectId: string;
};
export const CommentReactions: React.FC<Props> = observer((props) => {
const { commentId, projectId } = props;
const router = useRouter();
const { workspace_slug } = router.query;
const { issueDetails: issueDetailsStore, user: userStore } = useMobxStore();
const peekId = issueDetailsStore.peekId;
const user = userStore.currentUser;
const commentReactions = peekId
? issueDetailsStore.details[peekId].comments.find((c) => c.id === commentId)?.comment_reactions
: [];
const groupedReactions = peekId ? groupReactions(commentReactions ?? [], "reaction") : {};
const userReactions = commentReactions?.filter((r) => r.actor_detail.id === user?.id);
const handleAddReaction = (reactionHex: string) => {
if (!workspace_slug || !projectId || !peekId) return;
issueDetailsStore.addCommentReaction(
workspace_slug.toString(),
projectId.toString(),
peekId,
commentId,
reactionHex
);
};
const handleRemoveReaction = (reactionHex: string) => {
if (!workspace_slug || !projectId || !peekId) return;
issueDetailsStore.removeCommentReaction(
workspace_slug.toString(),
projectId.toString(),
peekId,
commentId,
reactionHex
);
};
const handleReactionClick = (reactionHex: string) => {
const userReaction = userReactions?.find((r) => r.actor_detail.id === user?.id && r.reaction === reactionHex);
if (userReaction) handleRemoveReaction(reactionHex);
else handleAddReaction(reactionHex);
};
return (
<div className="flex gap-1.5 items-center mt-2">
<ReactionSelector
onSelect={(value) => {
userStore.requiredLogin(() => {
handleReactionClick(value);
});
}}
position="top"
selected={userReactions?.map((r) => r.reaction)}
size="md"
/>
{Object.keys(groupedReactions || {}).map((reaction) => {
const reactions = groupedReactions?.[reaction] ?? [];
const REACTIONS_LIMIT = 1000;
if (reactions.length > 0)
return (
<Tooltip
key={reaction}
tooltipContent={
<div>
{reactions
.map((r) => r.actor_detail.display_name)
.splice(0, REACTIONS_LIMIT)
.join(", ")}
{reactions.length > REACTIONS_LIMIT && " and " + (reactions.length - REACTIONS_LIMIT) + " more"}
</div>
}
>
<button
type="button"
onClick={() => {
userStore.requiredLogin(() => {
handleReactionClick(reaction);
});
}}
className={`flex items-center gap-1 text-custom-text-100 text-sm h-full px-2 py-1 rounded-md ${
commentReactions?.some(
(r) => r.actor_detail.id === userStore.currentUser?.id && r.reaction === reaction
)
? "bg-custom-primary-100/10"
: "bg-custom-background-80"
}`}
>
<span>{renderEmoji(reaction)}</span>
<span
className={
commentReactions?.some(
(r) => r.actor_detail.id === userStore.currentUser?.id && r.reaction === reaction
)
? "text-custom-primary-100"
: ""
}
>
{groupedReactions?.[reaction].length}{" "}
</span>
</button>
</Tooltip>
);
})}
</div>
);
});
@@ -1,3 +0,0 @@
export * from "./add-comment";
export * from "./comment-detail-card";
export * from "./comment-reactions";
@@ -1,7 +1,5 @@
import React from "react";
// mobx
import { observer } from "mobx-react-lite";
// headless ui
import { Listbox, Transition } from "@headlessui/react";
// hooks
@@ -43,7 +41,7 @@ const peekModes: {
},
];
export const PeekOverviewHeader: React.FC<Props> = observer((props) => {
export const PeekOverviewHeader: React.FC<Props> = (props) => {
const { handleClose, issueDetails } = props;
const { issueDetails: issueDetailStore }: RootStore = useMobxStore();
@@ -139,4 +137,4 @@ export const PeekOverviewHeader: React.FC<Props> = observer((props) => {
</div>
</>
);
});
};
@@ -1,4 +1,3 @@
export * from "./comment";
export * from "./full-screen-peek-view";
export * from "./header";
export * from "./issue-activity";
@@ -9,3 +8,5 @@ export * from "./side-peek-view";
export * from "./issue-reaction";
export * from "./issue-vote-reactions";
export * from "./issue-emoji-reactions";
export * from "./comment-detail-card";
export * from "./add-comment";
@@ -20,25 +20,16 @@ export const IssueEmojiReactions: React.FC = observer(() => {
const reactions = issueId ? issueDetailsStore.details[issueId]?.reactions || [] : [];
const groupedReactions = groupReactions(reactions, "reaction");
const userReactions = reactions?.filter((r) => r.actor_detail.id === user?.id);
const handleAddReaction = (reactionHex: string) => {
const handleReactionSelectClick = (reactionHex: string) => {
if (!workspace_slug || !project_slug || !issueId) return;
const userReaction = reactions?.find((r) => r.actor_detail.id === user?.id && r.reaction === reactionHex);
if (userReaction) return;
issueDetailsStore.addIssueReaction(workspace_slug.toString(), project_slug.toString(), issueId, reactionHex);
};
const handleRemoveReaction = (reactionHex: string) => {
if (!workspace_slug || !project_slug || !issueId) return;
issueDetailsStore.removeIssueReaction(workspace_slug.toString(), project_slug.toString(), issueId, reactionHex);
};
const handleReactionClick = (reactionHex: string) => {
const userReaction = userReactions?.find((r) => r.actor_detail.id === user?.id && r.reaction === reactionHex);
if (userReaction) handleRemoveReaction(reactionHex);
else handleAddReaction(reactionHex);
if (!workspace_slug || !project_slug || !issueId) return;
issueDetailsStore.removeIssueReaction(workspace_slug.toString(), project_slug.toString(), issueId, reactionHex);
};
useEffect(() => {
@@ -51,10 +42,9 @@ export const IssueEmojiReactions: React.FC = observer(() => {
<ReactionSelector
onSelect={(value) => {
userStore.requiredLogin(() => {
handleReactionClick(value);
handleReactionSelectClick(value);
});
}}
selected={userReactions?.map((r) => r.reaction)}
size="md"
/>
<div className="flex items-center gap-2 flex-wrap">
@@ -2,10 +2,9 @@
import useToast from "hooks/use-toast";
// icons
import { Icon } from "components/ui";
// helpers
import { copyTextToClipboard, addSpaceIfCamelCase } from "helpers/string.helper";
import { renderFullDate } from "helpers/date-time.helper";
import { dueDateIconDetails } from "../board-views/block-due-date";
// helpers
import { renderDateFormat } from "constants/helpers";
// types
import { IIssue } from "types/issue";
import { IPeekMode } from "store/issue_details";
@@ -17,16 +16,35 @@ type Props = {
mode?: IPeekMode;
};
const validDate = (date: any, state: string): string => {
if (date === null || ["backlog", "unstarted", "cancelled"].includes(state))
return `bg-gray-500/10 text-gray-500 border-gray-500/50`;
else {
const today = new Date();
const dueDate = new Date(date);
if (dueDate < today) return `bg-red-500/10 text-red-500 border-red-500/50`;
else return `bg-green-500/10 text-green-500 border-green-500/50`;
}
};
export const PeekOverviewIssueProperties: React.FC<Props> = ({ issueDetails, mode }) => {
const { setToastAlert } = useToast();
const startDate = issueDetails.start_date;
const targetDate = issueDetails.target_date;
const minDate = startDate ? new Date(startDate) : null;
minDate?.setDate(minDate.getDate());
const maxDate = targetDate ? new Date(targetDate) : null;
maxDate?.setDate(maxDate.getDate());
const state = issueDetails.state_detail;
const stateGroup = issueGroupFilter(state.group);
const priority = issueDetails.priority ? issuePriorityFilter(issueDetails.priority) : null;
const dueDateIcon = dueDateIconDetails(issueDetails.target_date, state.group);
const handleCopyLink = () => {
const urlToCopy = window.location.href;
@@ -44,6 +62,7 @@ export const PeekOverviewIssueProperties: React.FC<Props> = ({ issueDetails, mod
{mode === "full" && (
<div className="flex justify-between gap-2 pb-3">
<h6 className="flex items-center gap-2 font-medium">
{/* {getStateGroupIcon(issue.state_detail.group, "16", "16", issue.state_detail.color)} */}
{issueDetails.project_detail.identifier}-{issueDetails.sequence_id}
</h6>
<div className="flex items-center gap-2">
@@ -106,11 +125,11 @@ export const PeekOverviewIssueProperties: React.FC<Props> = ({ issueDetails, mod
</div>
<div>
{issueDetails.target_date ? (
<div className="h-6 rounded flex items-center gap-1 px-2.5 py-1 border border-custom-border-100 text-custom-text-100 text-xs bg-custom-background-80">
<span className={`material-symbols-rounded text-sm -my-0.5 ${dueDateIcon.className}`}>
{dueDateIcon.iconName}
</span>
{renderFullDate(issueDetails.target_date)}
<div
className={`h-[24px] rounded-md flex px-2.5 py-1 items-center border border-custom-border-100 gap-1 text-custom-text-100 text-xs font-medium
${validDate(issueDetails.target_date, state)}`}
>
{renderDateFormat(issueDetails.target_date)}
</div>
) : (
<span className="text-custom-text-200">Empty</span>
+8 -10
View File
@@ -77,16 +77,14 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
{...bubbleMenuProps}
className="flex w-fit divide-x divide-custom-border-300 rounded border border-custom-border-300 bg-custom-background-100 shadow-xl"
>
{!props.editor.isActive("table") && (
<NodeSelector
editor={props.editor!}
isOpen={isNodeSelectorOpen}
setIsOpen={() => {
setIsNodeSelectorOpen(!isNodeSelectorOpen);
setIsLinkSelectorOpen(false);
}}
/>
)}
<NodeSelector
editor={props.editor!}
isOpen={isNodeSelectorOpen}
setIsOpen={() => {
setIsNodeSelectorOpen(!isNodeSelectorOpen);
setIsLinkSelectorOpen(false);
}}
/>
<LinkSelector
editor={props.editor!!}
isOpen={isLinkSelectorOpen}
@@ -28,10 +28,7 @@ export const NodeSelector: FC<NodeSelectorProps> = ({ editor, isOpen, setIsOpen
name: "Text",
icon: TextIcon,
command: () => editor.chain().focus().toggleNode("paragraph", "paragraph").run(),
isActive: () =>
editor.isActive("paragraph") &&
!editor.isActive("bulletList") &&
!editor.isActive("orderedList"),
isActive: () => editor.isActive("paragraph") && !editor.isActive("bulletList") && !editor.isActive("orderedList"),
},
{
name: "H1",
@@ -72,8 +69,7 @@ export const NodeSelector: FC<NodeSelectorProps> = ({ editor, isOpen, setIsOpen
{
name: "Quote",
icon: TextQuote,
command: () =>
editor.chain().focus().toggleNode("paragraph", "paragraph").toggleBlockquote().run(),
command: () => editor.chain().focus().toggleNode("paragraph", "paragraph").toggleBlockquote().run(),
isActive: () => editor.isActive("blockquote"),
},
{
+103 -117
View File
@@ -13,7 +13,6 @@ import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight";
import { lowlight } from "lowlight/lib/core";
import SlashCommand from "../slash-command";
import { InputRule } from "@tiptap/core";
import Gapcursor from "@tiptap/extension-gapcursor";
import ts from "highlight.js/lib/languages/typescript";
@@ -21,10 +20,6 @@ import "highlight.js/styles/github-dark.css";
import UniqueID from "@tiptap-pro/extension-unique-id";
import UpdatedImage from "./updated-image";
import isValidHttpUrl from "../bubble-menu/utils/link-validator";
import { CustomTableCell } from "./table/table-cell";
import { Table } from "./table/table";
import { TableHeader } from "./table/table-header";
import { TableRow } from "@tiptap/extension-table-row";
lowlight.registerLanguage("ts", ts);
@@ -32,122 +27,113 @@ export const TiptapExtensions = (
workspaceSlug: string,
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void
) => [
StarterKit.configure({
bulletList: {
HTMLAttributes: {
class: "list-disc list-outside leading-3 -mt-2",
},
StarterKit.configure({
bulletList: {
HTMLAttributes: {
class: "list-disc list-outside leading-3 -mt-2",
},
orderedList: {
HTMLAttributes: {
class: "list-decimal list-outside leading-3 -mt-2",
},
},
orderedList: {
HTMLAttributes: {
class: "list-decimal list-outside leading-3 -mt-2",
},
listItem: {
HTMLAttributes: {
class: "leading-normal -mb-2",
},
},
listItem: {
HTMLAttributes: {
class: "leading-normal -mb-2",
},
blockquote: {
HTMLAttributes: {
class: "border-l-4 border-custom-border-300",
},
},
blockquote: {
HTMLAttributes: {
class: "border-l-4 border-custom-border-300",
},
code: {
HTMLAttributes: {
class:
"rounded-md bg-custom-primary-30 mx-1 px-1 py-1 font-mono font-medium text-custom-text-1000",
spellcheck: "false",
},
},
code: {
HTMLAttributes: {
class: "rounded-md bg-custom-primary-30 mx-1 px-1 py-1 font-mono font-medium text-custom-text-1000",
spellcheck: "false",
},
codeBlock: false,
horizontalRule: false,
dropcursor: {
color: "rgba(var(--color-text-100))",
width: 2,
},
gapcursor: false,
}),
CodeBlockLowlight.configure({
lowlight,
}),
HorizontalRule.extend({
addInputRules() {
return [
new InputRule({
find: /^(?:---|—-|___\s|\*\*\*\s)$/,
handler: ({ state, range, commands }) => {
commands.splitBlock();
},
codeBlock: false,
horizontalRule: false,
dropcursor: {
color: "#DBEAFE",
width: 2,
},
gapcursor: false,
}),
CodeBlockLowlight.configure({
lowlight,
}),
HorizontalRule.extend({
addInputRules() {
return [
new InputRule({
find: /^(?:---|—-|___\s|\*\*\*\s)$/,
handler: ({ state, range, commands }) => {
commands.splitBlock();
const attributes = {};
const { tr } = state;
const start = range.from;
const end = range.to;
// @ts-ignore
tr.replaceWith(start - 1, end, this.type.create(attributes));
},
}),
];
},
}).configure({
HTMLAttributes: {
class: "mb-6 border-t border-custom-border-300",
},
}),
Gapcursor,
TiptapLink.configure({
protocols: ["http", "https"],
validate: (url) => isValidHttpUrl(url),
HTMLAttributes: {
class:
"text-custom-primary-300 underline underline-offset-[3px] hover:text-custom-primary-500 transition-colors cursor-pointer",
},
}),
UpdatedImage.configure({
HTMLAttributes: {
class: "rounded-lg border border-custom-border-300",
},
}),
Placeholder.configure({
placeholder: ({ node }) => {
if (node.type.name === "heading") {
return `Heading ${node.attrs.level}`;
}
if (node.type.name === "image" || node.type.name === "table") {
return "";
}
const attributes = {};
const { tr } = state;
const start = range.from;
const end = range.to;
// @ts-ignore
tr.replaceWith(start - 1, end, this.type.create(attributes));
},
}),
];
},
}).configure({
HTMLAttributes: {
class: "mb-6 border-t border-custom-border-300",
},
}),
TiptapLink.configure({
protocols: ["http", "https"],
validate: (url) => isValidHttpUrl(url),
HTMLAttributes: {
class:
"text-custom-primary-300 underline underline-offset-[3px] hover:text-custom-primary-500 transition-colors cursor-pointer",
},
}),
UpdatedImage.configure({
HTMLAttributes: {
class: "rounded-lg border border-custom-border-300",
},
}),
Placeholder.configure({
placeholder: ({ node }) => {
if (node.type.name === "heading") {
return `Heading ${node.attrs.level}`;
}
return "Press '/' for commands...";
},
includeChildren: true,
}),
UniqueID.configure({
types: ["image"],
}),
SlashCommand(workspaceSlug, setIsSubmitting),
TiptapUnderline,
TextStyle,
Color,
Highlight.configure({
multicolor: true,
}),
TaskList.configure({
HTMLAttributes: {
class: "not-prose pl-2",
},
}),
TaskItem.configure({
HTMLAttributes: {
class: "flex items-start my-4",
},
nested: true,
}),
Markdown.configure({
html: true,
transformCopiedText: true,
}),
Table,
TableHeader,
CustomTableCell,
TableRow,
];
return "Press '/' for commands...";
},
includeChildren: true,
}),
UniqueID.configure({
types: ["image"],
}),
SlashCommand(workspaceSlug, setIsSubmitting),
TiptapUnderline,
TextStyle,
Color,
Highlight.configure({
multicolor: true,
}),
TaskList.configure({
HTMLAttributes: {
class: "not-prose pl-2",
},
}),
TaskItem.configure({
HTMLAttributes: {
class: "flex items-start my-4",
},
nested: true,
}),
Markdown.configure({
html: true,
transformCopiedText: true,
}),
];
@@ -1,32 +0,0 @@
import { TableCell } from "@tiptap/extension-table-cell";
export const CustomTableCell = TableCell.extend({
addAttributes() {
return {
...this.parent?.(),
isHeader: {
default: false,
parseHTML: (element) => {
isHeader: element.tagName === "TD";
},
renderHTML: (attributes) => {
tag: attributes.isHeader ? "th" : "td";
},
},
};
},
renderHTML({ HTMLAttributes }) {
if (HTMLAttributes.isHeader) {
return [
"th",
{
...HTMLAttributes,
class: `relative ${HTMLAttributes.class}`,
},
["span", { class: "absolute top-0 right-0" }],
0,
];
}
return ["td", HTMLAttributes, 0];
},
});
@@ -1,7 +0,0 @@
import { TableHeader as BaseTableHeader } from "@tiptap/extension-table-header";
const TableHeader = BaseTableHeader.extend({
content: "paragraph",
});
export { TableHeader };
@@ -1,9 +0,0 @@
import { Table as BaseTable } from "@tiptap/extension-table";
const Table = BaseTable.configure({
resizable: true,
cellMinWidth: 100,
allowTableNodeSelection: true,
});
export { Table };
+8 -5
View File
@@ -6,7 +6,6 @@ import { EditorBubbleMenu } from "./bubble-menu";
import { TiptapExtensions } from "./extensions";
import { TiptapEditorProps } from "./props";
import { ImageResizer } from "./extensions/image-resize";
import { TableMenu } from "./table-menu";
export interface ITipTapRichTextEditor {
value: string;
@@ -38,7 +37,6 @@ const Tiptap = (props: ITipTapRichTextEditor) => {
borderOnFocus,
customClassName,
} = props;
const editor = useEditor({
editable: editable ?? true,
editorProps: TiptapEditorProps(workspaceSlug, setIsSubmitting),
@@ -56,6 +54,12 @@ const Tiptap = (props: ITipTapRichTextEditor) => {
},
});
useEffect(() => {
if (editor) {
editor.commands.setContent(value);
}
}, [value]);
const editorRef: React.MutableRefObject<Editor | null> = useRef(null);
useImperativeHandle(forwardedRef, () => ({
@@ -77,8 +81,8 @@ const Tiptap = (props: ITipTapRichTextEditor) => {
const editorClassNames = `relative w-full max-w-full sm:rounded-lg mt-2 p-3 relative focus:outline-none rounded-md
${noBorder ? "" : "border border-custom-border-200"} ${
borderOnFocus ? "focus:border border-custom-border-300" : "focus:border-0"
} ${customClassName}`;
borderOnFocus ? "focus:border border-custom-border-300" : "focus:border-0"
} ${customClassName}`;
if (!editor) return null;
editorRef.current = editor;
@@ -94,7 +98,6 @@ const Tiptap = (props: ITipTapRichTextEditor) => {
{editor && <EditorBubbleMenu editor={editor} />}
<div className={`${editorContentCustomClassNames}`}>
<EditorContent editor={editor} />
<TableMenu editor={editor} />
{editor?.isActive("image") && <ImageResizer editor={editor} />}
</div>
</div>
@@ -1,51 +1,43 @@
import { EditorState, Plugin, PluginKey, Transaction } from "@tiptap/pm/state";
import { Plugin, PluginKey } from "@tiptap/pm/state";
import { Node as ProseMirrorNode } from "@tiptap/pm/model";
import fileService from "services/file.service";
const deleteKey = new PluginKey("delete-image");
const IMAGE_NODE_TYPE = "image";
interface ImageNode extends ProseMirrorNode {
attrs: {
src: string;
id: string;
};
}
const TrackImageDeletionPlugin = (): Plugin =>
const TrackImageDeletionPlugin = () =>
new Plugin({
key: deleteKey,
appendTransaction: (transactions: readonly Transaction[], oldState: EditorState, newState: EditorState) => {
const newImageSources = new Set();
newState.doc.descendants((node) => {
if (node.type.name === IMAGE_NODE_TYPE) {
newImageSources.add(node.attrs.src);
}
});
appendTransaction: (transactions, oldState, newState) => {
transactions.forEach((transaction) => {
if (!transaction.docChanged) return;
const removedImages: ImageNode[] = [];
const removedImages: ProseMirrorNode[] = [];
oldState.doc.descendants((oldNode, oldPos) => {
if (oldNode.type.name !== IMAGE_NODE_TYPE) return;
if (oldPos < 0 || oldPos > newState.doc.content.size) return;
if (!newState.doc.resolve(oldPos).parent) return;
if (oldNode.type.name !== "image") return;
if (!newState.doc.resolve(oldPos).parent) return;
const newNode = newState.doc.nodeAt(oldPos);
// Check if the node has been deleted or replaced
if (!newNode || newNode.type.name !== IMAGE_NODE_TYPE) {
if (!newImageSources.has(oldNode.attrs.src)) {
removedImages.push(oldNode as ImageNode);
if (!newNode || newNode.type.name !== "image") {
// Check if the node still exists elsewhere in the document
let nodeExists = false;
newState.doc.descendants((node) => {
if (node.attrs.id === oldNode.attrs.id) {
nodeExists = true;
}
});
if (!nodeExists) {
removedImages.push(oldNode as ProseMirrorNode);
}
}
});
removedImages.forEach(async (node) => {
removedImages.forEach((node) => {
const src = node.attrs.src;
await onNodeDeleted(src);
onNodeDeleted(src);
});
});
@@ -55,14 +47,10 @@ const TrackImageDeletionPlugin = (): Plugin =>
export default TrackImageDeletionPlugin;
async function onNodeDeleted(src: string): Promise<void> {
try {
const assetUrlWithWorkspaceId = new URL(src).pathname.substring(1);
const resStatus = await fileService.deleteImage(assetUrlWithWorkspaceId);
if (resStatus === 204) {
console.log("Image deleted successfully");
}
} catch (error) {
console.error("Error deleting image: ", error);
async function onNodeDeleted(src: string) {
const assetUrlWithWorkspaceId = new URL(src).pathname.substring(1);
const resStatus = await fileService.deleteImage(assetUrlWithWorkspaceId);
if (resStatus === 204) {
console.log("Image deleted successfully");
}
}
@@ -1,3 +1,4 @@
// @ts-nocheck
import { EditorState, Plugin, PluginKey } from "@tiptap/pm/state";
import { Decoration, DecorationSet, EditorView } from "@tiptap/pm/view";
import fileService from "services/file.service";
@@ -45,11 +46,7 @@ export default UploadImagesPlugin;
function findPlaceholder(state: EditorState, id: {}) {
const decos = uploadKey.getState(state);
const found = decos.find(
undefined,
undefined,
(spec: { id: number | undefined }) => spec.id == id
);
const found = decos.find(undefined, undefined, (spec: { id: number | undefined }) => spec.id == id);
return found.length ? found[0].from : null;
}
@@ -62,6 +59,8 @@ export async function startImageUpload(
) {
if (!file.type.includes("image/")) {
return;
} else if (file.size / 1024 / 1024 > 20) {
return;
}
const id = {};
@@ -94,9 +93,7 @@ export async function startImageUpload(
const imageSrc = typeof src === "object" ? reader.result : src;
const node = schema.nodes.image.create({ src: imageSrc });
const transaction = view.state.tr
.replaceWith(pos, pos, node)
.setMeta(uploadKey, { remove: { id } });
const transaction = view.state.tr.replaceWith(pos, pos, node).setMeta(uploadKey, { remove: { id } });
view.dispatch(transaction);
}
@@ -110,9 +107,7 @@ const UploadImageHandler = (file: File, workspaceSlug: string): Promise<string>
formData.append("attributes", JSON.stringify({}));
return new Promise(async (resolve, reject) => {
const imageUrl = await fileService
.uploadFile(workspaceSlug, formData)
.then((response) => response.asset);
const imageUrl = await fileService.uploadFile(workspaceSlug, formData).then((response) => response.asset);
const image = new Image();
image.src = imageUrl;
-19
View File
@@ -1,6 +1,5 @@
import { EditorProps } from "@tiptap/pm/view";
import { startImageUpload } from "./plugins/upload-image";
import { findTableAncestor } from "./table-menu";
export function TiptapEditorProps(
workspaceSlug: string,
@@ -22,15 +21,6 @@ export function TiptapEditorProps(
},
},
handlePaste: (view, event) => {
if (typeof window !== "undefined") {
const selection: any = window?.getSelection();
if (selection.rangeCount !== 0) {
const range = selection.getRangeAt(0);
if (findTableAncestor(range.startContainer)) {
return;
}
}
}
if (event.clipboardData && event.clipboardData.files && event.clipboardData.files[0]) {
event.preventDefault();
const file = event.clipboardData.files[0];
@@ -41,15 +31,6 @@ export function TiptapEditorProps(
return false;
},
handleDrop: (view, event, _slice, moved) => {
if (typeof window !== "undefined") {
const selection: any = window?.getSelection();
if (selection.rangeCount !== 0) {
const range = selection.getRangeAt(0);
if (findTableAncestor(range.startContainer)) {
return;
}
}
}
if (!moved && event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files[0]) {
event.preventDefault();
const file = event.dataTransfer.files[0];
@@ -15,7 +15,6 @@ import {
MinusSquare,
CheckSquare,
ImageIcon,
Table,
} from "lucide-react";
import { startImageUpload } from "../plugins/upload-image";
import { cn } from "../utils";
@@ -47,9 +46,6 @@ const Command = Extension.create({
return [
Suggestion({
editor: this.editor,
allow({ editor }) {
return !editor.isActive("table");
},
...this.options.suggestion,
}),
];
@@ -57,10 +53,7 @@ const Command = Extension.create({
});
const getSuggestionItems =
(
workspaceSlug: string,
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void
) =>
(workspaceSlug: string, setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void) =>
({ query }: { query: string }) =>
[
{
@@ -126,20 +119,6 @@ const getSuggestionItems =
editor.chain().focus().deleteRange(range).setHorizontalRule().run();
},
},
{
title: "Table",
description: "Create a Table",
searchTerms: ["table", "cell", "db", "data", "tabular"],
icon: <Table size={18} />,
command: ({ editor, range }: CommandProps) => {
editor
.chain()
.focus()
.deleteRange(range)
.insertTable({ rows: 3, cols: 3, withHeaderRow: true })
.run();
},
},
{
title: "Numbered List",
description: "Create a list with numbering.",
@@ -155,21 +134,14 @@ const getSuggestionItems =
searchTerms: ["blockquote"],
icon: <TextQuote size={18} />,
command: ({ editor, range }: CommandProps) =>
editor
.chain()
.focus()
.deleteRange(range)
.toggleNode("paragraph", "paragraph")
.toggleBlockquote()
.run(),
editor.chain().focus().deleteRange(range).toggleNode("paragraph", "paragraph").toggleBlockquote().run(),
},
{
title: "Code",
description: "Capture a code snippet.",
searchTerms: ["codeblock"],
icon: <Code size={18} />,
command: ({ editor, range }: CommandProps) =>
editor.chain().focus().deleteRange(range).toggleCodeBlock().run(),
command: ({ editor, range }: CommandProps) => editor.chain().focus().deleteRange(range).toggleCodeBlock().run(),
},
{
title: "Image",
@@ -218,15 +190,7 @@ export const updateScrollView = (container: HTMLElement, item: HTMLElement) => {
}
};
const CommandList = ({
items,
command,
}: {
items: CommandItemProps[];
command: any;
editor: any;
range: any;
}) => {
const CommandList = ({ items, command }: { items: CommandItemProps[]; command: any; editor: any; range: any }) => {
const [selectedIndex, setSelectedIndex] = useState(0);
const selectItem = useCallback(
@@ -1,16 +0,0 @@
const InsertBottomTableIcon = (props: any) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={24}
height={24}
viewBox="0 -960 960 960"
{...props}
>
<path
d="M212.309-152.31q-30.308 0-51.308-21t-21-51.307V-360q0-30.307 21-51.307 21-21 51.308-21h535.382q30.308 0 51.308 21t21 51.307v135.383q0 30.307-21 51.307-21 21-51.308 21H212.309Zm0-375.383q-30.308 0-51.308-21t-21-51.307v-135.383q0-30.307 21-51.307 21-21 51.308-21h535.382q30.308 0 51.308 21t21 51.307V-600q0 30.307-21 51.307-21 21-51.308 21H212.309Zm535.382-219.998H212.309q-4.616 0-8.463 3.846-3.846 3.846-3.846 8.462V-600q0 4.616 3.846 8.462 3.847 3.847 8.463 3.847h535.382q4.616 0 8.463-3.847Q760-595.384 760-600v-135.383q0-4.616-3.846-8.462-3.847-3.846-8.463-3.846ZM200-587.691v-160 160Z"
fill="rgb(var(--color-text-300))"
/>
</svg>
);
export default InsertBottomTableIcon;
@@ -1,15 +0,0 @@
const InsertLeftTableIcon = (props: any) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={24}
height={24}
viewBox="0 -960 960 960"
{...props}
>
<path
d="M224.617-140.001q-30.307 0-51.307-21-21-21-21-51.308v-535.382q0-30.308 21-51.308t51.307-21H360q30.307 0 51.307 21 21 21 21 51.308v535.382q0 30.308-21 51.308t-51.307 21H224.617Zm375.383 0q-30.307 0-51.307-21-21-21-21-51.308v-535.382q0-30.308 21-51.308t51.307-21h135.383q30.307 0 51.307 21 21 21 21 51.308v535.382q0 30.308-21 51.308t-51.307 21H600Zm147.691-607.69q0-4.616-3.846-8.463-3.846-3.846-8.462-3.846H600q-4.616 0-8.462 3.846-3.847 3.847-3.847 8.463v535.382q0 4.616 3.847 8.463Q595.384-200 600-200h135.383q4.616 0 8.462-3.846 3.846-3.847 3.846-8.463v-535.382ZM587.691-200h160-160Z"
fill="rgb(var(--color-text-300))"
/>
</svg>
);
export default InsertLeftTableIcon;
@@ -1,16 +0,0 @@
const InsertRightTableIcon = (props: any) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={24}
height={24}
viewBox="0 -960 960 960"
{...props}
>
<path
d="M600-140.001q-30.307 0-51.307-21-21-21-21-51.308v-535.382q0-30.308 21-51.308t51.307-21h135.383q30.307 0 51.307 21 21 21 21 51.308v535.382q0 30.308-21 51.308t-51.307 21H600Zm-375.383 0q-30.307 0-51.307-21-21-21-21-51.308v-535.382q0-30.308 21-51.308t51.307-21H360q30.307 0 51.307 21 21 21 21 51.308v535.382q0 30.308-21 51.308t-51.307 21H224.617Zm-12.308-607.69v535.382q0 4.616 3.846 8.463 3.846 3.846 8.462 3.846H360q4.616 0 8.462-3.846 3.847-3.847 3.847-8.463v-535.382q0-4.616-3.847-8.463Q364.616-760 360-760H224.617q-4.616 0-8.462 3.846-3.846 3.847-3.846 8.463Zm160 547.691h-160 160Z"
fill="rgb(var(--color-text-300))"
/>
</svg>
);
export default InsertRightTableIcon;
@@ -1,15 +0,0 @@
const InsertTopTableIcon = (props: any) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={24}
height={24}
viewBox="0 -960 960 960"
{...props}
>
<path
d="M212.309-527.693q-30.308 0-51.308-21t-21-51.307v-135.383q0-30.307 21-51.307 21-21 51.308-21h535.382q30.308 0 51.308 21t21 51.307V-600q0 30.307-21 51.307-21 21-51.308 21H212.309Zm0 375.383q-30.308 0-51.308-21t-21-51.307V-360q0-30.307 21-51.307 21-21 51.308-21h535.382q30.308 0 51.308 21t21 51.307v135.383q0 30.307-21 51.307-21 21-51.308 21H212.309Zm0-59.999h535.382q4.616 0 8.463-3.846 3.846-3.846 3.846-8.462V-360q0-4.616-3.846-8.462-3.847-3.847-8.463-3.847H212.309q-4.616 0-8.463 3.847Q200-364.616 200-360v135.383q0 4.616 3.846 8.462 3.847 3.846 8.463 3.846Zm-12.309-160v160-160Z"
fill="rgb(var(--color-text-300))"
/>
</svg>
);
export default InsertTopTableIcon;
@@ -1,143 +0,0 @@
import { useState, useEffect } from "react";
import { Rows, Columns, ToggleRight } from "lucide-react";
import { cn } from "../utils";
import { Tooltip } from "components/ui";
import InsertLeftTableIcon from "./InsertLeftTableIcon";
import InsertRightTableIcon from "./InsertRightTableIcon";
import InsertTopTableIcon from "./InsertTopTableIcon";
import InsertBottomTableIcon from "./InsertBottomTableIcon";
interface TableMenuItem {
command: () => void;
icon: any;
key: string;
name: string;
}
export const findTableAncestor = (node: Node | null): HTMLTableElement | null => {
while (node !== null && node.nodeName !== "TABLE") {
node = node.parentNode;
}
return node as HTMLTableElement;
};
export const TableMenu = ({ editor }: { editor: any }) => {
const [tableLocation, setTableLocation] = useState({ bottom: 0, left: 0 });
const isOpen = editor?.isActive("table");
const items: TableMenuItem[] = [
{
command: () => editor.chain().focus().addColumnBefore().run(),
icon: InsertLeftTableIcon,
key: "insert-column-left",
name: "Insert 1 column left",
},
{
command: () => editor.chain().focus().addColumnAfter().run(),
icon: InsertRightTableIcon,
key: "insert-column-right",
name: "Insert 1 column right",
},
{
command: () => editor.chain().focus().addRowBefore().run(),
icon: InsertTopTableIcon,
key: "insert-row-above",
name: "Insert 1 row above",
},
{
command: () => editor.chain().focus().addRowAfter().run(),
icon: InsertBottomTableIcon,
key: "insert-row-below",
name: "Insert 1 row below",
},
{
command: () => editor.chain().focus().deleteColumn().run(),
icon: Columns,
key: "delete-column",
name: "Delete column",
},
{
command: () => editor.chain().focus().deleteRow().run(),
icon: Rows,
key: "delete-row",
name: "Delete row",
},
{
command: () => editor.chain().focus().toggleHeaderRow().run(),
icon: ToggleRight,
key: "toggle-header-row",
name: "Toggle header row",
},
];
useEffect(() => {
if (!window) return;
const handleWindowClick = () => {
const selection: any = window?.getSelection();
if (selection.rangeCount !== 0) {
const range = selection.getRangeAt(0);
const tableNode = findTableAncestor(range.startContainer);
let parent = tableNode?.parentElement;
if (tableNode) {
const tableRect = tableNode.getBoundingClientRect();
const tableCenter = tableRect.left + tableRect.width / 2;
const menuWidth = 45;
const menuLeft = tableCenter - menuWidth / 2;
const tableBottom = tableRect.bottom;
setTableLocation({ bottom: tableBottom, left: menuLeft });
while (parent) {
if (!parent.classList.contains("disable-scroll"))
parent.classList.add("disable-scroll");
parent = parent.parentElement;
}
} else {
const scrollDisabledContainers = document.querySelectorAll(".disable-scroll");
scrollDisabledContainers.forEach((container) => {
container.classList.remove("disable-scroll");
});
}
}
};
window.addEventListener("click", handleWindowClick);
return () => {
window.removeEventListener("click", handleWindowClick);
};
}, [tableLocation, editor]);
return (
<section
className={`fixed left-1/2 transform -translate-x-1/2 overflow-hidden rounded border border-custom-border-300 bg-custom-background-100 shadow-custom-shadow-sm p-1 ${
isOpen ? "block" : "hidden"
}`}
style={{
bottom: `calc(100vh - ${tableLocation.bottom + 45}px)`,
left: `${tableLocation.left}px`,
}}
>
{items.map((item, index) => (
<Tooltip key={index} tooltipContent={item.name}>
<button
onClick={item.command}
className="p-1.5 text-custom-text-200 hover:bg-text-custom-text-100 hover:bg-custom-background-80 active:bg-custom-background-80 rounded"
title={item.name}
>
<item.icon
className={cn("h-4 w-4 text-lg", {
"text-red-600": item.key.includes("delete"),
})}
/>
</button>
</Tooltip>
))}
</section>
);
};
+5 -8
View File
@@ -12,14 +12,13 @@ import { Icon } from "components/ui";
const reactionEmojis = ["128077", "128078", "128516", "128165", "128533", "129505", "9992", "128064"];
interface Props {
onSelect: (emoji: string) => void;
position?: "top" | "bottom";
selected?: string[];
size?: "sm" | "md" | "lg";
position?: "top" | "bottom";
onSelect: (emoji: string) => void;
}
export const ReactionSelector: React.FC<Props> = (props) => {
const { onSelect, position, selected = [], size } = props;
const { onSelect, position, size } = props;
return (
<Popover className="relative">
@@ -52,7 +51,7 @@ export const ReactionSelector: React.FC<Props> = (props) => {
position === "top" ? "-top-12" : "-bottom-12"
}`}
>
<div className="bg-custom-sidebar-background-100 border border-custom-border-200 shadow-custom-shadow-sm rounded-md p-1">
<div className="bg-custom-sidebar-background-100 border border-custom-border-200 rounded-md p-1">
<div className="flex gap-x-1">
{reactionEmojis.map((emoji) => (
<button
@@ -62,9 +61,7 @@ export const ReactionSelector: React.FC<Props> = (props) => {
onSelect(emoji);
closePopover();
}}
className={`grid place-items-center select-none rounded-md text-sm p-1 ${
selected.includes(emoji) ? "bg-custom-primary-100/10" : "hover:bg-custom-sidebar-background-80"
}`}
className="flex select-none items-center justify-between rounded-md text-sm p-1 hover:bg-custom-sidebar-background-90"
>
{renderEmoji(emoji)}
</button>
+36
View File
@@ -0,0 +1,36 @@
export const renderDateFormat = (date: string | Date | null) => {
if (!date) return "N/A";
var d = new Date(date),
month = "" + (d.getMonth() + 1),
day = "" + d.getDate(),
year = d.getFullYear();
if (month.length < 2) month = "0" + month;
if (day.length < 2) day = "0" + day;
return [year, month, day].join("-");
};
/**
* @description Returns date and month, if date is of the current year
* @description Returns date, month adn year, if date is of a different year than current
* @param {string} date
* @example renderFullDate("2023-01-01") // 1 Jan
* @example renderFullDate("2021-01-01") // 1 Jan, 2021
*/
export const renderFullDate = (date: string): string => {
if (!date) return "";
const months: string[] = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
const currentDate: Date = new Date();
const [year, month, day]: number[] = date.split("-").map(Number);
const formattedMonth: string = months[month - 1];
const formattedDay: string = day < 10 ? `0${day}` : day.toString();
if (currentDate.getFullYear() === year) return `${formattedDay} ${formattedMonth}`;
else return `${formattedDay} ${formattedMonth}, ${year}`;
};
-23
View File
@@ -12,26 +12,3 @@ export const timeAgo = (time: any) => {
time = +new Date();
}
};
/**
* @description Returns date and month, if date is of the current year
* @description Returns date, month adn year, if date is of a different year than current
* @param {string} date
* @example renderFullDate("2023-01-01") // 1 Jan
* @example renderFullDate("2021-01-01") // 1 Jan, 2021
*/
export const renderFullDate = (date: string): string => {
if (!date) return "";
const months: string[] = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
const currentDate: Date = new Date();
const [year, month, day]: number[] = date.split("-").map(Number);
const formattedMonth: string = months[month - 1];
const formattedDay: string = day < 10 ? `0${day}` : day.toString();
if (currentDate.getFullYear() === year) return `${formattedDay} ${formattedMonth}`;
else return `${formattedDay} ${formattedMonth}, ${year}`;
};
+1 -4
View File
@@ -12,10 +12,7 @@ const nextConfig = {
};
if (parseInt(process.env.NEXT_PUBLIC_DEPLOY_WITH_NGINX || "0")) {
const nextConfigWithNginx = withImages({
basePath: "/spaces",
...nextConfig,
});
const nextConfigWithNginx = withImages({ basePath: "/spaces", ...nextConfig });
module.exports = nextConfigWithNginx;
} else {
module.exports = nextConfig;
-1
View File
@@ -69,7 +69,6 @@
"@types/react": "18.0.28",
"@types/react-dom": "18.0.11",
"@types/uuid": "^9.0.1",
"@typescript-eslint/eslint-plugin": "^5.48.2",
"autoprefixer": "^10.4.13",
"eslint": "8.34.0",
"eslint-config-custom": "*",
@@ -1,8 +1,7 @@
import Head from "next/head";
import { useRouter } from "next/router";
import useSWR from "swr";
import type { GetServerSideProps } from "next";
import { useRouter } from "next/router";
import Head from "next/head";
/// layouts
import ProjectLayout from "layouts/project-layout";
// components
@@ -40,4 +39,12 @@ const WorkspaceProjectPage = (props: any) => {
);
};
// export const getServerSideProps: GetServerSideProps<any> = async ({ query: { workspace_slug, project_slug } }) => {
// const res = await fetch(
// `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/public/workspaces/${workspace_slug}/project-boards/${project_slug}/settings/`
// );
// const project_settings = await res.json();
// return { props: { project_settings } };
// };
export default WorkspaceProjectPage;
+4 -3
View File
@@ -1,4 +1,7 @@
import React, { useEffect } from "react";
import Image from "next/image";
// assets
import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.svg";
// mobx
import { observer } from "mobx-react-lite";
import { useMobxStore } from "lib/mobx/store-provider";
@@ -9,8 +12,6 @@ import useToast from "hooks/use-toast";
// components
import { OnBoardingForm } from "components/accounts/onboarding-form";
const imagePrefix = process.env.NEXT_PUBLIC_DEPLOY_WITH_NGINX ? "/spaces/" : "";
const OnBoardingPage = () => {
const { user: userStore } = useMobxStore();
@@ -33,7 +34,7 @@ const OnBoardingPage = () => {
<div className="absolute border-b-[0.5px] sm:border-r-[0.5px] border-custom-border-200 h-[0.5px] w-full top-1/2 left-0 -translate-y-1/2 sm:h-screen sm:w-[0.5px] sm:top-0 sm:left-1/2 md:left-1/3 sm:-translate-x-1/2 sm:translate-y-0 z-10" />
<div className="absolute grid place-items-center bg-custom-background-100 px-3 sm:px-0 py-5 left-2 sm:left-1/2 md:left-1/3 sm:-translate-x-1/2 top-1/2 -translate-y-1/2 sm:translate-y-0 sm:top-12 z-10">
<div className="h-[30px] w-[30px]">
<img src={`${imagePrefix}/plane-logos/blue-without-text.png`} alt="Plane logo" />
<Image src={BluePlaneLogoWithoutText} alt="Plane logo" />
</div>
</div>
<div className="absolute sm:fixed text-custom-text-100 text-sm font-medium right-4 top-1/4 sm:top-12 -translate-y-1/2 sm:translate-y-0 sm:right-16 sm:py-5">
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

@@ -0,0 +1,15 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="276.000000pt" height="276.000000pt" viewBox="0 0 276.000000 276.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,276.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M930 2300 l0 -450 460 0 460 0 0 -460 0 -460 450 0 450 0 0 910 0
910 -910 0 -910 0 0 -450z"/>
<path d="M10 1380 l0 -450 450 0 450 0 0 450 0 450 -450 0 -450 0 0 -450z"/>
<path d="M930 460 l0 -450 450 0 450 0 0 450 0 450 -450 0 -450 0 0 -450z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 690 B

+10 -33
View File
@@ -93,6 +93,16 @@ class IssueService extends APIService {
});
}
async getCommentsReactions(workspaceSlug: string, projectId: string, commentId: string): Promise<any> {
return this.get(
`/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/comments/${commentId}/reactions/`
)
.then((response) => response?.data)
.catch((error) => {
throw error?.response;
});
}
async createIssueComment(workspaceSlug: string, projectId: string, issueId: string, data: any): Promise<any> {
return this.post(
`/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/issues/${issueId}/comments/`,
@@ -130,39 +140,6 @@ class IssueService extends APIService {
throw error?.response;
});
}
async createCommentReaction(
workspaceSlug: string,
projectId: string,
commentId: string,
data: {
reaction: string;
}
): Promise<any> {
return this.post(
`/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/comments/${commentId}/reactions/`,
data
)
.then((response) => response?.data)
.catch((error) => {
throw error?.response;
});
}
async deleteCommentReaction(
workspaceSlug: string,
projectId: string,
commentId: string,
reactionHex: string
): Promise<any> {
return this.delete(
`/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/comments/${commentId}/reactions/${reactionHex}/`
)
.then((response) => response?.data)
.catch((error) => {
throw error?.response;
});
}
}
export default IssueService;
+22 -136
View File
@@ -32,20 +32,6 @@ export interface IIssueDetailStore {
data: any
) => Promise<any>;
deleteIssueComment: (workspaceId: string, projectId: string, issueId: string, comment_id: string) => void;
addCommentReaction: (
workspaceId: string,
projectId: string,
issueId: string,
commentId: string,
reactionHex: string
) => void;
removeCommentReaction: (
workspaceId: string,
projectId: string,
issueId: string,
commentId: string,
reactionHex: string
) => void;
// issue reactions
addIssueReaction: (workspaceId: string, projectId: string, issueId: string, reactionHex: string) => void;
removeIssueReaction: (workspaceId: string, projectId: string, issueId: string, reactionHex: string) => void;
@@ -75,17 +61,8 @@ class IssueDetailStore implements IssueDetailStore {
details: observable.ref,
// actions
setPeekId: action,
setPeekMode: action,
fetchIssueDetails: action,
addIssueComment: action,
updateIssueComment: action,
deleteIssueComment: action,
addCommentReaction: action,
removeCommentReaction: action,
addIssueReaction: action,
removeIssueReaction: action,
addIssueVote: action,
removeIssueVote: action,
setPeekMode: action,
});
this.issueService = new IssueService();
this.rootStore = _rootStore;
@@ -154,32 +131,29 @@ class IssueDetailStore implements IssueDetailStore {
data: any
) => {
try {
runInAction(() => {
this.details = {
...this.details,
[issueId]: {
...this.details[issueId],
comments: this.details[issueId].comments.map((c) => ({
...c,
...(c.id === commentId ? data : {}),
})),
},
};
});
const issueCommentUpdateResponse = await this.issueService.updateIssueComment(
workspaceSlug,
projectId,
issueId,
commentId,
data
);
await this.issueService.updateIssueComment(workspaceSlug, projectId, issueId, commentId, data);
if (issueCommentUpdateResponse) {
const remainingComments = this.details[issueId].comments.filter((com) => com.id != commentId);
runInAction(() => {
this.details = {
...this.details,
[issueId]: {
...this.details[issueId],
comments: [...remainingComments, issueCommentUpdateResponse],
},
};
});
}
return issueCommentUpdateResponse;
} catch (error) {
const issueComments = await this.issueService.getIssueComments(workspaceSlug, projectId, issueId);
runInAction(() => {
this.details = {
...this.details,
[issueId]: {
...this.details[issueId],
comments: issueComments,
},
};
});
console.log("Failed to add issue comment");
}
};
@@ -201,94 +175,6 @@ class IssueDetailStore implements IssueDetailStore {
}
};
addCommentReaction = async (
workspaceSlug: string,
projectId: string,
issueId: string,
commentId: string,
reactionHex: string
) => {
const newReaction = {
id: uuidv4(),
comment: commentId,
reaction: reactionHex,
actor_detail: this.rootStore.user.currentActor,
};
const newComments = this.details[issueId].comments.map((comment) => ({
...comment,
comment_reactions:
comment.id === commentId ? [...comment.comment_reactions, newReaction] : comment.comment_reactions,
}));
try {
runInAction(() => {
this.details = {
...this.details,
[issueId]: {
...this.details[issueId],
comments: [...newComments],
},
};
});
await this.issueService.createCommentReaction(workspaceSlug, projectId, commentId, {
reaction: reactionHex,
});
} catch (error) {
const issueComments = await this.issueService.getIssueComments(workspaceSlug, projectId, issueId);
runInAction(() => {
this.details = {
...this.details,
[issueId]: {
...this.details[issueId],
comments: issueComments,
},
};
});
}
};
removeCommentReaction = async (
workspaceSlug: string,
projectId: string,
issueId: string,
commentId: string,
reactionHex: string
) => {
try {
const comment = this.details[issueId].comments.find((c) => c.id === commentId);
const newCommentReactions = comment?.comment_reactions.filter((r) => r.reaction !== reactionHex) ?? [];
runInAction(() => {
this.details = {
...this.details,
[issueId]: {
...this.details[issueId],
comments: this.details[issueId].comments.map((c) => ({
...c,
comment_reactions: c.id === commentId ? newCommentReactions : c.comment_reactions,
})),
},
};
});
await this.issueService.deleteCommentReaction(workspaceSlug, projectId, commentId, reactionHex);
} catch (error) {
const issueComments = await this.issueService.getIssueComments(workspaceSlug, projectId, issueId);
runInAction(() => {
this.details = {
...this.details,
[issueId]: {
...this.details[issueId],
comments: issueComments,
},
};
});
}
};
addIssueReaction = async (workspaceSlug: string, projectId: string, issueId: string, reactionHex: string) => {
try {
runInAction(() => {
+8 -4
View File
@@ -62,13 +62,17 @@ class UserStore implements IUserStore {
return;
}
const currentPath = window.location.pathname + window.location.search;
this.fetchCurrentUser()
.then(() => {
if (!this.currentUser) window.location.href = `/?next_path=${currentPath}`;
else callback();
if (!this.currentUser) {
const currentPath = window.location.pathname;
window.location.href = `/?next_path=${currentPath}`;
} else callback();
})
.catch(() => (window.location.href = `/?next_path=${currentPath}`));
.catch(() => {
const currentPath = window.location.pathname;
window.location.href = `/?next_path=${currentPath}`;
});
};
fetchCurrentUser = async () => {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"extends": "tsconfig/nextjs.json",
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "additional.d.ts"],
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"],
"compilerOptions": {
"baseUrl": ".",
+15 -20
View File
@@ -68,30 +68,25 @@ export interface IVote {
}
export interface Comment {
actor_detail: ActorDetail;
access: string;
actor: string;
attachments: any[];
comment_html: string;
comment_reactions: {
actor_detail: ActorDetail;
comment: string;
id: string;
reaction: string;
}[];
comment_stripped: string;
created_at: Date;
created_by: string;
id: string;
is_member: boolean;
issue: string;
actor_detail: ActorDetail;
issue_detail: IssueDetail;
project: string;
project_detail: ProjectDetail;
updated_at: Date;
updated_by: string;
workspace: string;
workspace_detail: WorkspaceDetail;
comment_reactions: any[];
is_member: boolean;
created_at: Date;
updated_at: Date;
comment_stripped: string;
comment_html: string;
attachments: any[];
access: string;
created_by: string;
updated_by: string;
project: string;
workspace: string;
issue: string;
actor: string;
}
export interface IIssueReaction {
@@ -1,13 +1,13 @@
// nivo
import { BarDatum } from "@nivo/bar";
// icons
import { PriorityIcon } from "components/icons";
import { getPriorityIcon } from "components/icons";
// helpers
import { addSpaceIfCamelCase } from "helpers/string.helper";
// helpers
import { generateBarColor, renderMonthAndYear } from "helpers/analytics.helper";
// types
import { IAnalyticsParams, IAnalyticsResponse, TIssuePriorities } from "types";
import { IAnalyticsParams, IAnalyticsResponse } from "types";
// constants
import { ANALYTICS_X_AXIS_VALUES, ANALYTICS_Y_AXIS_VALUES, DATE_KEYS } from "constants/analytics";
@@ -53,7 +53,7 @@ export const AnalyticsTable: React.FC<Props> = ({ analytics, barGraphData, param
>
<div className="flex items-center gap-2">
{params.segment === "priority" ? (
<PriorityIcon priority={key as TIssuePriorities} />
getPriorityIcon(key)
) : (
<span
className="h-3 w-3 flex-shrink-0 rounded"
@@ -91,7 +91,7 @@ export const AnalyticsTable: React.FC<Props> = ({ analytics, barGraphData, param
}`}
>
{params.x_axis === "priority" ? (
<PriorityIcon priority={item.name as TIssuePriorities} />
getPriorityIcon(`${item.name}`)
) : (
<span
className="h-3 w-3 rounded"
@@ -1,7 +1,7 @@
// icons
import { PlayIcon } from "@heroicons/react/24/outline";
// types
import { IDefaultAnalyticsResponse, TStateGroups } from "types";
import { IDefaultAnalyticsResponse } from "types";
// constants
import { STATE_GROUP_COLORS } from "constants/state";
@@ -27,7 +27,7 @@ export const AnalyticsDemand: React.FC<Props> = ({ defaultAnalytics }) => (
<span
className="h-2 w-2 rounded-full"
style={{
backgroundColor: STATE_GROUP_COLORS[group.state_group as TStateGroups],
backgroundColor: STATE_GROUP_COLORS[group.state_group],
}}
/>
<h6 className="capitalize">{group.state_group}</h6>
@@ -42,7 +42,7 @@ export const AnalyticsDemand: React.FC<Props> = ({ defaultAnalytics }) => (
className="absolute top-0 left-0 h-1 rounded duration-300"
style={{
width: `${percentage}%`,
backgroundColor: STATE_GROUP_COLORS[group.state_group as TStateGroups],
backgroundColor: STATE_GROUP_COLORS[group.state_group],
}}
/>
</div>
@@ -9,7 +9,7 @@ import { CustomSearchSelect, CustomSelect, ToggleSwitch } from "components/ui";
import { SelectMonthModal } from "components/automation";
// icons
import { ChevronDownIcon, Squares2X2Icon } from "@heroicons/react/24/outline";
import { StateGroupIcon } from "components/icons";
import { getStateGroupIcon } from "components/icons";
// services
import stateService from "services/state.service";
// constants
@@ -46,7 +46,7 @@ export const AutoCloseAutomation: React.FC<Props> = ({ projectDetails, handleCha
query: state.name,
content: (
<div className="flex items-center gap-2">
<StateGroupIcon stateGroup={state.group} color={state.color} height="16px" width="16px" />
{getStateGroupIcon(state.group, "16", "16", state.color)}
{state.name}
</div>
),
@@ -140,19 +140,14 @@ export const AutoCloseAutomation: React.FC<Props> = ({ projectDetails, handleCha
label={
<div className="flex items-center gap-2">
{selectedOption ? (
<StateGroupIcon
stateGroup={selectedOption.group}
color={selectedOption.color}
height="16px"
width="16px"
/>
getStateGroupIcon(selectedOption.group, "16", "16", selectedOption.color)
) : currentDefaultState ? (
<StateGroupIcon
stateGroup={currentDefaultState.group}
color={currentDefaultState.color}
height="16px"
width="16px"
/>
getStateGroupIcon(
currentDefaultState.group,
"16",
"16",
currentDefaultState.color
)
) : (
<Squares2X2Icon className="h-3.5 w-3.5 text-custom-text-200" />
)}
@@ -1,7 +1,5 @@
import React, { Dispatch, SetStateAction, useCallback } from "react";
import { useRouter } from "next/router";
import React, { Dispatch, SetStateAction, useCallback } from "react";
import { mutate } from "swr";
// cmdk
@@ -9,12 +7,12 @@ import { Command } from "cmdk";
// services
import issuesService from "services/issues.service";
// types
import { ICurrentUserResponse, IIssue, TIssuePriorities } from "types";
import { ICurrentUserResponse, IIssue } from "types";
// constants
import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
import { PRIORITIES } from "constants/project";
// icons
import { CheckIcon, PriorityIcon } from "components/icons";
import { CheckIcon, getPriorityIcon } from "components/icons";
type Props = {
setIsPaletteOpen: Dispatch<SetStateAction<boolean>>;
@@ -56,7 +54,7 @@ export const ChangeIssuePriority: React.FC<Props> = ({ setIsPaletteOpen, issue,
[workspaceSlug, issueId, projectId, user]
);
const handleIssueState = (priority: TIssuePriorities) => {
const handleIssueState = (priority: string | null) => {
submitChanges({ priority });
setIsPaletteOpen(false);
};
@@ -70,7 +68,7 @@ export const ChangeIssuePriority: React.FC<Props> = ({ setIsPaletteOpen, issue,
className="focus:outline-none"
>
<div className="flex items-center space-x-3">
<PriorityIcon priority={priority} />
{getPriorityIcon(priority)}
<span className="capitalize">{priority ?? "None"}</span>
</div>
<div>{priority === issue.priority && <CheckIcon className="h-3 w-3" />}</div>
@@ -1,24 +1,22 @@
import React, { Dispatch, SetStateAction, useCallback } from "react";
import { useRouter } from "next/router";
import React, { Dispatch, SetStateAction, useCallback } from "react";
import useSWR, { mutate } from "swr";
// cmdk
import { Command } from "cmdk";
// ui
import { Spinner } from "components/ui";
// helpers
import { getStatesList } from "helpers/state.helper";
// services
import issuesService from "services/issues.service";
import stateService from "services/state.service";
// ui
import { Spinner } from "components/ui";
// icons
import { CheckIcon, StateGroupIcon } from "components/icons";
// helpers
import { getStatesList } from "helpers/state.helper";
// types
import { ICurrentUserResponse, IIssue } from "types";
// fetch keys
import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY, STATES_LIST } from "constants/fetch-keys";
// icons
import { CheckIcon, getStateGroupIcon } from "components/icons";
type Props = {
setIsPaletteOpen: Dispatch<SetStateAction<boolean>>;
@@ -84,12 +82,7 @@ export const ChangeIssueState: React.FC<Props> = ({ setIsPaletteOpen, issue, use
className="focus:outline-none"
>
<div className="flex items-center space-x-3">
<StateGroupIcon
stateGroup={state.group}
color={state.color}
height="16px"
width="16px"
/>
{getStateGroupIcon(state.group, "16", "16", state.color)}
<p>{state.name}</p>
</div>
<div>{state.id === issue.state && <CheckIcon className="h-3 w-3" />}</div>
@@ -1,11 +1,15 @@
import { Fragment } from "react";
import { useRouter } from "next/router";
// react-hook-form
import { Controller, useForm } from "react-hook-form";
// react-datepicker
import DatePicker from "react-datepicker";
// headless ui
import { Dialog, Transition } from "@headlessui/react";
// hooks
import useIssuesView from "hooks/use-issues-view";
// components
import { DateFilterSelect } from "./date-filter-select";
// ui
@@ -19,10 +23,8 @@ import { IIssueFilterOptions } from "types";
type Props = {
title: string;
field: keyof IIssueFilterOptions;
filters: IIssueFilterOptions;
handleClose: () => void;
isOpen: boolean;
onSelect: (option: any) => void;
handleClose: () => void;
};
type TFormValues = {
@@ -37,14 +39,12 @@ const defaultValues: TFormValues = {
date2: new Date(new Date().getFullYear(), new Date().getMonth() + 1, new Date().getDate()),
};
export const DateFilterModal: React.FC<Props> = ({
title,
field,
filters,
handleClose,
isOpen,
onSelect,
}) => {
export const DateFilterModal: React.FC<Props> = ({ title, field, isOpen, handleClose }) => {
const { filters, setFilters } = useIssuesView();
const router = useRouter();
const { viewId } = router.query;
const { handleSubmit, watch, control } = useForm<TFormValues>({
defaultValues,
});
@@ -53,10 +53,10 @@ export const DateFilterModal: React.FC<Props> = ({
const { filterType, date1, date2 } = formData;
if (filterType === "range") {
onSelect({
key: field,
value: [`${renderDateFormat(date1)};after`, `${renderDateFormat(date2)};before`],
});
setFilters(
{ [field]: [`${renderDateFormat(date1)};after`, `${renderDateFormat(date2)};before`] },
!Boolean(viewId)
);
} else {
const filteredArray = (filters?.[field] as string[])?.filter((item) => {
if (item?.includes(filterType)) return false;
@@ -66,12 +66,17 @@ export const DateFilterModal: React.FC<Props> = ({
const filterOne = filteredArray && filteredArray?.length > 0 ? filteredArray[0] : null;
if (filterOne)
onSelect({ key: field, value: [filterOne, `${renderDateFormat(date1)};${filterType}`] });
setFilters(
{ [field]: [filterOne, `${renderDateFormat(date1)};${filterType}`] },
!Boolean(viewId)
);
else
onSelect({
key: field,
value: [`${renderDateFormat(date1)};${filterType}`],
});
setFilters(
{
[field]: [`${renderDateFormat(date1)};${filterType}`],
},
!Boolean(viewId)
);
}
handleClose();
};
+9 -11
View File
@@ -2,7 +2,7 @@ import React from "react";
// icons
import { XMarkIcon } from "@heroicons/react/24/outline";
import { PriorityIcon, StateGroupIcon } from "components/icons";
import { getPriorityIcon, getStateGroupIcon } from "components/icons";
// ui
import { Avatar } from "components/ui";
// helpers
@@ -71,10 +71,12 @@ export const FiltersList: React.FC<Props> = ({
}}
>
<span>
<StateGroupIcon
stateGroup={state?.group ?? "backlog"}
color={state?.color}
/>
{getStateGroupIcon(
state?.group ?? "backlog",
"12",
"12",
state?.color
)}
</span>
<span>{state?.name ?? ""}</span>
<span
@@ -103,9 +105,7 @@ export const FiltersList: React.FC<Props> = ({
backgroundColor: `${STATE_GROUP_COLORS[group]}20`,
}}
>
<span>
<StateGroupIcon stateGroup={group} color={undefined} />
</span>
<span>{getStateGroupIcon(group, "16", "16")}</span>
<span>{group}</span>
<span
className="cursor-pointer"
@@ -136,9 +136,7 @@ export const FiltersList: React.FC<Props> = ({
: "bg-custom-background-90 text-custom-text-200"
}`}
>
<span>
<PriorityIcon priority={priority} />
</span>
<span>{getPriorityIcon(priority)}</span>
<span>{priority === "null" ? "None" : priority}</span>
<span
className="cursor-pointer"
@@ -212,7 +212,7 @@ export const ExistingIssuesListModal: React.FC<Props> = ({
onClick={() => setIsWorkspaceLevel((prevData) => !prevData)}
className="flex-shrink-0"
>
Workspace Level
workspace level
</button>
</div>
</Tooltip>
+1 -1
View File
@@ -61,7 +61,7 @@ export const ReactionSelector: React.FC<Props> = (props) => {
position === "top" ? "-top-12" : "-bottom-12"
}`}
>
<div className="bg-custom-sidebar-background-100 border border-custom-border-200 shadow-custom-shadow-sm rounded-md p-1">
<div className="bg-custom-sidebar-background-100 border border-custom-border-200 rounded-md p-1">
<div className="flex gap-x-1">
{reactionEmojis.map((emoji) => (
<button
@@ -15,7 +15,6 @@ import {
TAssigneesDistribution,
TCompletionChartDistribution,
TLabelsDistribution,
TStateGroups,
} from "types";
// constants
import { STATE_GROUP_COLORS } from "constants/state";
@@ -216,7 +215,7 @@ export const SidebarProgressStats: React.FC<Props> = ({
<span
className="block h-3 w-3 rounded-full "
style={{
backgroundColor: STATE_GROUP_COLORS[group as TStateGroups],
backgroundColor: STATE_GROUP_COLORS[group],
}}
/>
<span className="text-xs capitalize">{group}</span>
@@ -1,7 +1,7 @@
// components
import { SingleBoard } from "components/core/views/board-view/single-board";
// icons
import { StateGroupIcon } from "components/icons";
import { getStateGroupIcon } from "components/icons";
// helpers
import { addSpaceIfCamelCase } from "helpers/string.helper";
// types
@@ -82,14 +82,8 @@ export const AllBoards: React.FC<Props> = ({
className="flex items-center justify-between gap-2 rounded bg-custom-background-90 p-2 shadow"
>
<div className="flex items-center gap-2">
{currentState && (
<StateGroupIcon
stateGroup={currentState.group}
color={currentState.color}
height="16px"
width="16px"
/>
)}
{currentState &&
getStateGroupIcon(currentState.group, "16", "16", currentState.color)}
<h4 className="text-sm capitalize">
{selectedGroup === "state"
? addSpaceIfCamelCase(currentState?.name ?? "")
@@ -13,16 +13,14 @@ import useProjects from "hooks/use-projects";
import { Avatar, Icon } from "components/ui";
// icons
import { PlusIcon } from "@heroicons/react/24/outline";
import { PriorityIcon, StateGroupIcon } from "components/icons";
import { getPriorityIcon, getStateGroupIcon } from "components/icons";
// helpers
import { addSpaceIfCamelCase } from "helpers/string.helper";
import { renderEmoji } from "helpers/emoji.helper";
// types
import { IIssueViewProps, IState, TIssuePriorities, TStateGroups } from "types";
import { IIssueViewProps, IState } from "types";
// fetch-keys
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS } from "constants/fetch-keys";
// constants
import { STATE_GROUP_COLORS } from "constants/state";
type Props = {
currentState?: IState | null;
@@ -99,27 +97,14 @@ export const BoardHeader: React.FC<Props> = ({
switch (selectedGroup) {
case "state":
icon = currentState && (
<StateGroupIcon
stateGroup={currentState.group}
color={currentState.color}
height="16px"
width="16px"
/>
);
icon =
currentState && getStateGroupIcon(currentState.group, "16", "16", currentState.color);
break;
case "state_detail.group":
icon = (
<StateGroupIcon
stateGroup={groupTitle as TStateGroups}
color={STATE_GROUP_COLORS[groupTitle as TStateGroups]}
height="16px"
width="16px"
/>
);
icon = getStateGroupIcon(groupTitle as any, "16", "16");
break;
case "priority":
icon = <PriorityIcon priority={groupTitle as TIssuePriorities} className="text-lg" />;
icon = getPriorityIcon(groupTitle, "text-lg");
break;
case "project":
const project = projects?.find((p) => p.id === groupTitle);
+2 -3
View File
@@ -29,7 +29,7 @@ import { PlusIcon } from "@heroicons/react/24/outline";
import { getStatesList } from "helpers/state.helper";
import { orderArrayBy } from "helpers/array.helper";
// types
import { IIssue, IIssueFilterOptions, IState, TIssuePriorities } from "types";
import { IIssue, IIssueFilterOptions, IState } from "types";
// fetch-keys
import {
CYCLE_DETAILS,
@@ -184,8 +184,7 @@ export const IssuesView: React.FC<Props> = ({
// if the issue is moved to a different group, then we will change the group of the
// dragged item(or issue)
if (selectedGroup === "priority")
draggedItem.priority = destinationGroup as TIssuePriorities;
if (selectedGroup === "priority") draggedItem.priority = destinationGroup;
else if (selectedGroup === "state") {
draggedItem.state = destinationGroup;
draggedItem.state_detail = states?.find((s) => s.id === destinationGroup) as IState;
@@ -15,7 +15,7 @@ import { SingleListIssue } from "components/core";
import { Avatar, CustomMenu } from "components/ui";
// icons
import { PlusIcon } from "@heroicons/react/24/outline";
import { PriorityIcon, StateGroupIcon } from "components/icons";
import { getPriorityIcon, getStateGroupIcon } from "components/icons";
// helpers
import { addSpaceIfCamelCase } from "helpers/string.helper";
import { renderEmoji } from "helpers/emoji.helper";
@@ -26,14 +26,10 @@ import {
IIssueLabels,
IIssueViewProps,
IState,
TIssuePriorities,
TStateGroups,
UserAuth,
} from "types";
// fetch-keys
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS } from "constants/fetch-keys";
// constants
import { STATE_GROUP_COLORS } from "constants/state";
type Props = {
currentState?: IState | null;
@@ -115,27 +111,14 @@ export const SingleList: React.FC<Props> = ({
switch (selectedGroup) {
case "state":
icon = currentState && (
<StateGroupIcon
stateGroup={currentState.group}
color={currentState.color}
height="16px"
width="16px"
/>
);
icon =
currentState && getStateGroupIcon(currentState.group, "16", "16", currentState.color);
break;
case "state_detail.group":
icon = (
<StateGroupIcon
stateGroup={groupTitle as TStateGroups}
color={STATE_GROUP_COLORS[groupTitle as TStateGroups]}
height="16px"
width="16px"
/>
);
icon = getStateGroupIcon(groupTitle as any, "16", "16");
break;
case "priority":
icon = <PriorityIcon priority={groupTitle as TIssuePriorities} className="text-lg" />;
icon = getPriorityIcon(groupTitle, "text-lg");
break;
case "project":
const project = projects?.find((p) => p.id === groupTitle);
@@ -19,7 +19,7 @@ import { ActiveCycleProgressStats } from "components/cycles";
// icons
import { CalendarDaysIcon } from "@heroicons/react/20/solid";
import { PriorityIcon } from "components/icons/priority-icon";
import { getPriorityIcon } from "components/icons/priority-icon";
import {
TargetIcon,
ContrastIcon,
@@ -28,7 +28,7 @@ import {
TriangleExclamationIcon,
AlarmClockIcon,
LayerDiagonalIcon,
StateGroupIcon,
CompletedStateIcon,
} from "components/icons";
import { StarIcon } from "@heroicons/react/24/outline";
// components
@@ -385,8 +385,8 @@ export const ActiveCycleDetails: React.FC = () => {
<LayerDiagonalIcon className="h-4 w-4 flex-shrink-0" />
{cycle.total_issues} issues
</div>
<div className="flex items-center gap-2">
<StateGroupIcon stateGroup="completed" height="14px" width="14px" />
<div className="flex gap-2">
<CompletedStateIcon width={16} height={16} color="#438AF3" />
{cycle.completed_issues} issues
</div>
</div>
@@ -477,7 +477,7 @@ export const ActiveCycleDetails: React.FC = () => {
: "border-orange-500/20 bg-orange-500/20 text-orange-500"
}`}
>
<PriorityIcon priority={issue.priority} className="text-sm" />
{getPriorityIcon(issue.priority, "text-sm")}
</div>
<ViewIssueLabel labelDetails={issue.label_details} maxRender={2} />
<div className={`flex items-center gap-2 text-custom-text-200`}>
@@ -63,9 +63,7 @@ export const CyclesListGanttChartView: FC<Props> = ({ cycles, mutateCycles }) =>
const blockFormat = (blocks: ICycle[]) =>
blocks && blocks.length > 0
? blocks
.filter(
(b) => b.start_date && b.end_date && new Date(b.start_date) <= new Date(b.end_date)
)
.filter((b) => b.start_date && b.end_date)
.map((block) => ({
data: block,
id: block.id,
+1 -1
View File
@@ -41,7 +41,7 @@ const IntegrationGuide = () => {
);
const handleCsvClose = () => {
router.replace(`/${workspaceSlug?.toString()}/settings/exports`);
router.replace(`/plane/settings/exports`);
};
return (
@@ -4,13 +4,11 @@ import { IGanttBlock } from "components/gantt-chart";
export const renderIssueBlocksStructure = (blocks: IIssue[]): IGanttBlock[] =>
blocks && blocks.length > 0
? blocks
.filter((b) => new Date(b?.start_date ?? "") <= new Date(b?.target_date ?? ""))
.map((block) => ({
data: block,
id: block.id,
sort_order: block.sort_order,
start_date: new Date(block.start_date ?? ""),
target_date: new Date(block.target_date ?? ""),
}))
? blocks.map((block) => ({
data: block,
id: block.id,
sort_order: block.sort_order,
start_date: new Date(block.start_date ?? ""),
target_date: new Date(block.target_date ?? ""),
}))
: [];
@@ -0,0 +1,21 @@
import React from "react";
import type { Props } from "./types";
export const BacklogStateIcon: React.FC<Props> = ({
width = "20",
height = "20",
className,
color = "rgb(var(--color-text-200))",
}) => (
<svg
width={width}
height={height}
className={className}
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle cx="10" cy="10" r="9" stroke={color} strokeLinecap="round" strokeDasharray="4 4" />
</svg>
);
@@ -0,0 +1,78 @@
import React from "react";
import type { Props } from "./types";
export const CancelledStateIcon: React.FC<Props> = ({
width = "20",
height = "20",
className,
color = "#f2655a",
}) => (
<svg
width={width}
height={height}
className={className}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 84.36 84.36"
>
<g id="Layer_2" data-name="Layer 2">
<g id="Layer_1-2" data-name="Layer 1">
<path
className="cls-1"
fill="none"
strokeWidth={3}
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
d="M20.45,7.69a39.74,39.74,0,0,1,43.43.54"
/>
<path
className="cls-1"
fill="none"
strokeWidth={3}
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
d="M76.67,20.45a39.76,39.76,0,0,1-.53,43.43"
/>
<path
className="cls-1"
fill="none"
strokeWidth={3}
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
d="M63.92,76.67a39.78,39.78,0,0,1-43.44-.53"
/>
<path
className="cls-1"
fill="none"
strokeWidth={3}
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
d="M7.69,63.92a39.75,39.75,0,0,1,.54-43.44"
/>
<circle className="cls-2" fill={color} cx="42.18" cy="42.18" r="31.04" />
<path
className="cls-3"
fill="none"
strokeWidth={3}
stroke="#ffffff"
strokeLinecap="square"
strokeMiterlimit={10}
d="M32.64,32.44q9.54,9.75,19.09,19.48"
/>
<path
className="cls-3"
fill="none"
strokeWidth={3}
stroke="#ffffff"
strokeLinecap="square"
strokeMiterlimit={10}
d="M32.64,51.92,51.73,32.44"
/>
</g>
</g>
</svg>
);
@@ -0,0 +1,69 @@
import React from "react";
import type { Props } from "./types";
export const CompletedStateIcon: React.FC<Props> = ({
width = "20",
height = "20",
className,
color = "#438af3",
}) => (
<svg
width={width}
height={height}
className={className}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 84.36 84.36"
>
<g id="Layer_2" data-name="Layer 2">
<g id="Layer_1-2" data-name="Layer 1">
<path
className="cls-1"
fill="none"
strokeWidth={3}
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
d="M20.45,7.69a39.74,39.74,0,0,1,43.43.54"
/>
<path
className="cls-1"
fill="none"
strokeWidth={3}
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
d="M76.67,20.45a39.76,39.76,0,0,1-.53,43.43"
/>
<path
className="cls-1"
fill="none"
strokeWidth={3}
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
d="M63.92,76.67a39.78,39.78,0,0,1-43.44-.53"
/>
<path
className="cls-1"
fill="none"
strokeWidth={3}
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
d="M7.69,63.92a39.75,39.75,0,0,1,.54-43.44"
/>
<circle className="cls-2" fill={color} cx="42.18" cy="42.18" r="31.04" />
<path
className="cls-3"
fill="none"
strokeWidth={3}
stroke="#ffffff"
strokeLinecap="square"
strokeMiterlimit={10}
d="M30.45,43.75l6.61,6.61L53.92,34"
/>
</g>
</g>
</svg>
);
+8 -2
View File
@@ -1,7 +1,6 @@
export * from "./module";
export * from "./state";
export * from "./alarm-clock-icon";
export * from "./attachment-icon";
export * from "./backlog-state-icon";
export * from "./blocked-icon";
export * from "./blocker-icon";
export * from "./bolt-icon";
@@ -9,10 +8,12 @@ export * from "./calendar-before-icon";
export * from "./calendar-after-icon";
export * from "./calendar-month-icon";
export * from "./cancel-icon";
export * from "./cancelled-state-icon";
export * from "./clipboard-icon";
export * from "./color-pallette-icon";
export * from "./comment-icon";
export * from "./completed-cycle-icon";
export * from "./completed-state-icon";
export * from "./current-cycle-icon";
export * from "./cycle-icon";
export * from "./discord-icon";
@@ -22,9 +23,11 @@ export * from "./ellipsis-horizontal-icon";
export * from "./external-link-icon";
export * from "./github-icon";
export * from "./heartbeat-icon";
export * from "./started-state-icon";
export * from "./layer-diagonal-icon";
export * from "./lock-icon";
export * from "./menu-icon";
export * from "./module";
export * from "./pencil-scribble-icon";
export * from "./plus-icon";
export * from "./person-running-icon";
@@ -33,8 +36,11 @@ export * from "./question-mark-circle-icon";
export * from "./setting-icon";
export * from "./signal-cellular-icon";
export * from "./stacked-layers-icon";
export * from "./started-state-icon";
export * from "./state-group-icon";
export * from "./tag-icon";
export * from "./tune-icon";
export * from "./unstarted-state-icon";
export * from "./upcoming-cycle-icon";
export * from "./user-group-icon";
export * from "./user-icon-circle";
+19 -22
View File
@@ -1,25 +1,22 @@
// types
import { TIssuePriorities } from "types";
type Props = {
priority: TIssuePriorities | null;
className?: string;
};
export const PriorityIcon: React.FC<Props> = ({ priority, className = "" }) => {
export const getPriorityIcon = (priority: string | null, className?: string) => {
if (!className || className === "") className = "text-xs flex items-center";
return (
<span className={`material-symbols-rounded ${className}`}>
{priority === "urgent"
? "error"
: priority === "high"
? "signal_cellular_alt"
: priority === "medium"
? "signal_cellular_alt_2_bar"
: priority === "low"
? "signal_cellular_alt_1_bar"
: "block"}
</span>
);
priority = priority?.toLowerCase() ?? null;
switch (priority) {
case "urgent":
return <span className={`material-symbols-rounded ${className}`}>error</span>;
case "high":
return <span className={`material-symbols-rounded ${className}`}>signal_cellular_alt</span>;
case "medium":
return (
<span className={`material-symbols-rounded ${className}`}>signal_cellular_alt_2_bar</span>
);
case "low":
return (
<span className={`material-symbols-rounded ${className}`}>signal_cellular_alt_1_bar</span>
);
default:
return <span className={`material-symbols-rounded ${className}`}>block</span>;
}
};
@@ -0,0 +1,77 @@
import React from "react";
import type { Props } from "./types";
export const StartedStateIcon: React.FC<Props> = ({
width = "20",
height = "20",
className,
color = "#fbb040",
}) => (
<svg
width={width}
height={height}
className={className}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 83.36 83.36"
>
<g id="Layer_2" data-name="Layer 2">
<g id="Layer_1-2" data-name="Layer 1">
<path
className="cls-1"
fill="none"
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={3}
d="M20,7.19a39.74,39.74,0,0,1,43.43.54"
/>
<path
className="cls-1"
fill="none"
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={3}
d="M76.17,20a39.76,39.76,0,0,1-.53,43.43"
/>
<path
className="cls-1"
fill="none"
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={3}
d="M63.42,76.17A39.78,39.78,0,0,1,20,75.64"
/>
<path
className="cls-1"
fill="none"
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={3}
d="M7.19,63.42A39.75,39.75,0,0,1,7.73,20"
/>
<path
className="cls-2"
fill={color}
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={3}
d="M42.32,41.21q9.57-14.45,19.13-28.9a35.8,35.8,0,0,0-39.09,0Z"
/>
<path
className="cls-2"
fill={color}
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={3}
d="M42.32,41.7,61.45,70.6a35.75,35.75,0,0,1-39.09,0Z"
/>
</g>
</g>
</svg>
);
+66
View File
@@ -0,0 +1,66 @@
import {
BacklogStateIcon,
CancelledStateIcon,
CompletedStateIcon,
StartedStateIcon,
UnstartedStateIcon,
} from "components/icons";
// constants
import { STATE_GROUP_COLORS } from "constants/state";
export const getStateGroupIcon = (
stateGroup: "backlog" | "unstarted" | "started" | "completed" | "cancelled",
width = "20",
height = "20",
color?: string
) => {
switch (stateGroup) {
case "backlog":
return (
<BacklogStateIcon
width={width}
height={height}
color={color ?? STATE_GROUP_COLORS["backlog"]}
className="flex-shrink-0"
/>
);
case "unstarted":
return (
<UnstartedStateIcon
width={width}
height={height}
color={color ?? STATE_GROUP_COLORS["unstarted"]}
className="flex-shrink-0"
/>
);
case "started":
return (
<StartedStateIcon
width={width}
height={height}
color={color ?? STATE_GROUP_COLORS["started"]}
className="flex-shrink-0"
/>
);
case "completed":
return (
<CompletedStateIcon
width={width}
height={height}
color={color ?? STATE_GROUP_COLORS["completed"]}
className="flex-shrink-0"
/>
);
case "cancelled":
return (
<CancelledStateIcon
width={width}
height={height}
color={color ?? STATE_GROUP_COLORS["cancelled"]}
className="flex-shrink-0"
/>
);
default:
return <></>;
}
};
-24
View File
@@ -1,24 +0,0 @@
type Props = {
width?: string;
height?: string;
className?: string;
color?: string;
};
export const StateGroupBacklogIcon: React.FC<Props> = ({
width = "20",
height = "20",
className,
color = "#a3a3a3",
}) => (
<svg
height={height}
width={width}
className={className}
viewBox="0 0 12 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle cx="6" cy="6" r="5.6" stroke={color} stroke-width="0.8" stroke-dasharray="4 4" />
</svg>
);
-34
View File
@@ -1,34 +0,0 @@
type Props = {
width?: string;
height?: string;
className?: string;
color?: string;
};
export const StateGroupCancelledIcon: React.FC<Props> = ({
width = "20",
height = "20",
className,
color = "#ef4444",
}) => (
<svg
height={height}
width={width}
className={className}
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clip-path="url(#clip0_4052_100277)">
<path
d="M8 8.84L10.58 11.42C10.7 11.54 10.84 11.6 11 11.6C11.16 11.6 11.3 11.54 11.42 11.42C11.54 11.3 11.6 11.16 11.6 11C11.6 10.84 11.54 10.7 11.42 10.58L8.84 8L11.42 5.42C11.54 5.3 11.6 5.16 11.6 5C11.6 4.84 11.54 4.7 11.42 4.58C11.3 4.46 11.16 4.4 11 4.4C10.84 4.4 10.7 4.46 10.58 4.58L8 7.16L5.42 4.58C5.3 4.46 5.16 4.4 5 4.4C4.84 4.4 4.7 4.46 4.58 4.58C4.46 4.7 4.4 4.84 4.4 5C4.4 5.16 4.46 5.3 4.58 5.42L7.16 8L4.58 10.58C4.46 10.7 4.4 10.84 4.4 11C4.4 11.16 4.46 11.3 4.58 11.42C4.7 11.54 4.84 11.6 5 11.6C5.16 11.6 5.3 11.54 5.42 11.42L8 8.84ZM8 16C6.90667 16 5.87333 15.79 4.9 15.37C3.92667 14.95 3.07667 14.3767 2.35 13.65C1.62333 12.9233 1.05 12.0733 0.63 11.1C0.21 10.1267 0 9.09333 0 8C0 6.89333 0.21 5.85333 0.63 4.88C1.05 3.90667 1.62333 3.06 2.35 2.34C3.07667 1.62 3.92667 1.05 4.9 0.63C5.87333 0.21 6.90667 0 8 0C9.10667 0 10.1467 0.21 11.12 0.63C12.0933 1.05 12.94 1.62 13.66 2.34C14.38 3.06 14.95 3.90667 15.37 4.88C15.79 5.85333 16 6.89333 16 8C16 9.09333 15.79 10.1267 15.37 11.1C14.95 12.0733 14.38 12.9233 13.66 13.65C12.94 14.3767 12.0933 14.95 11.12 15.37C10.1467 15.79 9.10667 16 8 16ZM8 14.8C9.89333 14.8 11.5 14.1367 12.82 12.81C14.14 11.4833 14.8 9.88 14.8 8C14.8 6.10667 14.14 4.5 12.82 3.18C11.5 1.86 9.89333 1.2 8 1.2C6.12 1.2 4.51667 1.86 3.19 3.18C1.86333 4.5 1.2 6.10667 1.2 8C1.2 9.88 1.86333 11.4833 3.19 12.81C4.51667 14.1367 6.12 14.8 8 14.8Z"
fill={color}
/>
</g>
<defs>
<clipPath id="clip0_4052_100277">
<rect width="16" height="16" fill="white" />
</clipPath>
</defs>
</svg>
);
-27
View File
@@ -1,27 +0,0 @@
type Props = {
width?: string;
height?: string;
className?: string;
color?: string;
};
export const StateGroupCompletedIcon: React.FC<Props> = ({
width = "20",
height = "20",
className,
color = "#16a34a",
}) => (
<svg
height={height}
width={width}
className={className}
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6.80486 9.80731L4.84856 7.85103C4.73197 7.73443 4.58542 7.67478 4.4089 7.67208C4.23238 7.66937 4.08312 7.72902 3.96113 7.85103C3.83913 7.97302 3.77814 8.12093 3.77814 8.29474C3.77814 8.46855 3.83913 8.61645 3.96113 8.73844L6.27206 11.0494C6.42428 11.2016 6.60188 11.2777 6.80486 11.2777C7.00782 11.2777 7.18541 11.2016 7.33764 11.0494L12.0227 6.36435C12.1393 6.24776 12.1989 6.10121 12.2016 5.92469C12.2043 5.74817 12.1447 5.59891 12.0227 5.47692C11.9007 5.35493 11.7528 5.29393 11.579 5.29393C11.4051 5.29393 11.2572 5.35493 11.1353 5.47692L6.80486 9.80731ZM8.00141 16C6.89494 16 5.85491 15.79 4.88132 15.3701C3.90772 14.9502 3.06082 14.3803 2.34064 13.6604C1.62044 12.9405 1.05028 12.094 0.63017 11.1208C0.210057 10.1477 0 9.10788 0 8.00141C0 6.89494 0.209966 5.85491 0.629896 4.88132C1.04983 3.90772 1.61972 3.06082 2.33958 2.34064C3.05946 1.62044 3.90598 1.05028 4.87915 0.630171C5.8523 0.210058 6.89212 0 7.99859 0C9.10506 0 10.1451 0.209966 11.1187 0.629897C12.0923 1.04983 12.9392 1.61972 13.6594 2.33959C14.3796 3.05946 14.9497 3.90598 15.3698 4.87915C15.7899 5.8523 16 6.89212 16 7.99859C16 9.10506 15.79 10.1451 15.3701 11.1187C14.9502 12.0923 14.3803 12.9392 13.6604 13.6594C12.9405 14.3796 12.094 14.9497 11.1208 15.3698C10.1477 15.7899 9.10788 16 8.00141 16ZM8 14.7369C9.88071 14.7369 11.4737 14.0842 12.779 12.779C14.0842 11.4737 14.7369 9.88071 14.7369 8C14.7369 6.11929 14.0842 4.52631 12.779 3.22104C11.4737 1.91577 9.88071 1.26314 8 1.26314C6.11929 1.26314 4.52631 1.91577 3.22104 3.22104C1.91577 4.52631 1.26314 6.11929 1.26314 8C1.26314 9.88071 1.91577 11.4737 3.22104 12.779C4.52631 14.0842 6.11929 14.7369 8 14.7369Z"
fill={color}
/>
</svg>
);
-6
View File
@@ -1,6 +0,0 @@
export * from "./backlog";
export * from "./cancelled";
export * from "./completed";
export * from "./started";
export * from "./state-group-icon";
export * from "./unstarted";
-25
View File
@@ -1,25 +0,0 @@
type Props = {
width?: string;
height?: string;
className?: string;
color?: string;
};
export const StateGroupStartedIcon: React.FC<Props> = ({
width = "20",
height = "20",
className,
color = "#f59e0b",
}) => (
<svg
height={height}
width={width}
className={className}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 12 12"
fill="none"
>
<circle cx="6" cy="6" r="5.6" stroke={color} stroke-width="0.8" />
<circle cx="6" cy="6" r="3.35" stroke={color} stroke-width="0.8" stroke-dasharray="2.4 2.4" />
</svg>
);
@@ -1,74 +0,0 @@
// icons
import {
StateGroupBacklogIcon,
StateGroupCancelledIcon,
StateGroupCompletedIcon,
StateGroupStartedIcon,
StateGroupUnstartedIcon,
} from "components/icons";
// types
import { TStateGroups } from "types";
// constants
import { STATE_GROUP_COLORS } from "constants/state";
type Props = {
className?: string;
color?: string;
height?: string;
stateGroup: TStateGroups;
width?: string;
};
export const StateGroupIcon: React.FC<Props> = ({
className = "",
color,
height = "12px",
width = "12px",
stateGroup,
}) => {
if (stateGroup === "backlog")
return (
<StateGroupBacklogIcon
width={width}
height={height}
color={color ?? STATE_GROUP_COLORS["backlog"]}
className={`flex-shrink-0 ${className}`}
/>
);
else if (stateGroup === "cancelled")
return (
<StateGroupCancelledIcon
width={width}
height={height}
color={color ?? STATE_GROUP_COLORS["cancelled"]}
className={`flex-shrink-0 ${className}`}
/>
);
else if (stateGroup === "completed")
return (
<StateGroupCompletedIcon
width={width}
height={height}
color={color ?? STATE_GROUP_COLORS["completed"]}
className={`flex-shrink-0 ${className}`}
/>
);
else if (stateGroup === "started")
return (
<StateGroupStartedIcon
width={width}
height={height}
color={color ?? STATE_GROUP_COLORS["started"]}
className={`flex-shrink-0 ${className}`}
/>
);
else
return (
<StateGroupUnstartedIcon
width={width}
height={height}
color={color ?? STATE_GROUP_COLORS["unstarted"]}
className={`flex-shrink-0 ${className}`}
/>
);
};
-24
View File
@@ -1,24 +0,0 @@
type Props = {
width?: string;
height?: string;
className?: string;
color?: string;
};
export const StateGroupUnstartedIcon: React.FC<Props> = ({
width = "20",
height = "20",
className,
color = "#3a3a3a",
}) => (
<svg
height={height}
width={width}
className={className}
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle cx="8" cy="8" r="7.4" stroke={color} stroke-width="1.2" />
</svg>
);
@@ -0,0 +1,59 @@
import React from "react";
import type { Props } from "./types";
export const UnstartedStateIcon: React.FC<Props> = ({
width = "20",
height = "20",
className,
color = "rgb(var(--color-text-200))",
}) => (
<svg
width={width}
height={height}
className={className}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 84.36 84.36"
>
<g id="Layer_2" data-name="Layer 2">
<g id="Layer_1-2" data-name="Layer 1">
<path
className="cls-1"
fill="none"
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={3}
d="M20.45,7.69a39.74,39.74,0,0,1,43.43.54"
/>
<path
className="cls-1"
fill="none"
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={3}
d="M76.67,20.45a39.76,39.76,0,0,1-.53,43.43"
/>
<path
className="cls-1"
fill="none"
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={3}
d="M63.92,76.67a39.78,39.78,0,0,1-43.44-.53"
/>
<path
className="cls-1"
fill="none"
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={3}
d="M7.69,63.92a39.75,39.75,0,0,1,.54-43.44"
/>
</g>
</g>
</svg>
);
+2 -2
View File
@@ -3,7 +3,7 @@ import useInboxView from "hooks/use-inbox-view";
// ui
import { MultiLevelDropdown } from "components/ui";
// icons
import { PriorityIcon } from "components/icons";
import { getPriorityIcon } from "components/icons";
// constants
import { PRIORITIES } from "constants/project";
import { INBOX_STATUS } from "constants/inbox";
@@ -42,7 +42,7 @@ export const FiltersDropdown: React.FC = () => {
id: priority === null ? "null" : priority,
label: (
<div className="flex items-center gap-2 capitalize">
<PriorityIcon priority={priority} /> {priority ?? "None"}
{getPriorityIcon(priority)} {priority ?? "None"}
</div>
),
value: {
+2 -6
View File
@@ -2,11 +2,9 @@
import useInboxView from "hooks/use-inbox-view";
// icons
import { XMarkIcon } from "@heroicons/react/24/outline";
import { PriorityIcon } from "components/icons";
import { getPriorityIcon } from "components/icons";
// helpers
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
// types
import { TIssuePriorities } from "types";
// constants
import { INBOX_STATUS } from "constants/inbox";
@@ -50,9 +48,7 @@ export const InboxFiltersList = () => {
: "bg-custom-background-90 text-custom-text-200"
}`}
>
<span>
<PriorityIcon priority={priority as TIssuePriorities} />
</span>
<span>{getPriorityIcon(priority)}</span>
<button
type="button"
className="cursor-pointer"
@@ -38,7 +38,7 @@ export const InboxIssueActivity: React.FC<Props> = ({ issueDetails }) => {
: null
);
const handleCommentUpdate = async (commentId: string, data: Partial<IIssueComment>) => {
const handleCommentUpdate = async (comment: IIssueComment) => {
if (!workspaceSlug || !projectId || !inboxIssueId) return;
await issuesService
@@ -46,8 +46,8 @@ export const InboxIssueActivity: React.FC<Props> = ({ issueDetails }) => {
workspaceSlug as string,
projectId as string,
inboxIssueId as string,
commentId,
data,
comment.id,
comment,
user
)
.then(() => mutateIssueActivity());
+5 -2
View File
@@ -4,7 +4,7 @@ import Link from "next/link";
// ui
import { Tooltip } from "components/ui";
// icons
import { PriorityIcon } from "components/icons";
import { getPriorityIcon } from "components/icons";
import {
CalendarDaysIcon,
CheckCircleIcon,
@@ -65,7 +65,10 @@ export const InboxIssueCard: React.FC<Props> = (props) => {
: "border-custom-border-200"
}`}
>
<PriorityIcon priority={issue.priority ?? null} className="text-sm" />
{getPriorityIcon(
issue.priority && issue.priority !== "" ? issue.priority ?? "" : "None",
"text-sm"
)}
</div>
</Tooltip>
<Tooltip
+1 -1
View File
@@ -35,7 +35,7 @@ import type { IInboxIssue, IIssue } from "types";
// fetch-keys
import { INBOX_ISSUES, INBOX_ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
const defaultValues: Partial<IInboxIssue> = {
const defaultValues = {
name: "",
description_html: "",
estimate_point: null,
@@ -4,6 +4,8 @@ import { useRouter } from "next/router";
import useSWRInfinite from "swr/infinite";
import getConfig from "next/config";
// services
import projectService from "services/project.service";
// ui
@@ -31,11 +33,13 @@ export const SelectRepository: React.FC<Props> = ({
const router = useRouter();
const { workspaceSlug } = router.query;
const { publicRuntimeConfig: { NEXT_PUBLIC_API_BASE_URL } } = getConfig();
const getKey = (pageIndex: number) => {
if (!workspaceSlug || !integration) return;
return `${
process.env.NEXT_PUBLIC_API_BASE_URL
NEXT_PUBLIC_API_BASE_URL
}/api/workspaces/${workspaceSlug}/workspace-integrations/${
integration.id
}/github-repositories/?page=${++pageIndex}`;
+4 -7
View File
@@ -15,16 +15,14 @@ import { IIssueActivity, IIssueComment } from "types";
type Props = {
activity: IIssueActivity[] | undefined;
handleCommentUpdate: (commentId: string, data: Partial<IIssueComment>) => Promise<void>;
handleCommentUpdate: (comment: IIssueComment) => Promise<void>;
handleCommentDelete: (commentId: string) => Promise<void>;
showAccessSpecifier?: boolean;
};
export const IssueActivitySection: React.FC<Props> = ({
activity,
handleCommentUpdate,
handleCommentDelete,
showAccessSpecifier = false,
}) => {
const router = useRouter();
const { workspaceSlug } = router.query;
@@ -133,11 +131,10 @@ export const IssueActivitySection: React.FC<Props> = ({
return (
<div key={activityItem.id} className="mt-4">
<CommentCard
comment={activityItem as IIssueComment}
handleCommentDeletion={handleCommentDelete}
onSubmit={handleCommentUpdate}
showAccessSpecifier={showAccessSpecifier}
workspaceSlug={workspaceSlug as string}
comment={activityItem as IIssueComment}
onSubmit={handleCommentUpdate}
handleCommentDeletion={handleCommentDelete}
/>
</div>
);
+12 -53
View File
@@ -7,7 +7,7 @@ import { ChatBubbleLeftEllipsisIcon, CheckIcon, XMarkIcon } from "@heroicons/rea
// hooks
import useUser from "hooks/use-user";
// ui
import { CustomMenu, Icon } from "components/ui";
import { CustomMenu } from "components/ui";
import { CommentReaction } from "components/issues";
import { TipTapEditor } from "components/tiptap";
// helpers
@@ -16,19 +16,17 @@ import { timeAgo } from "helpers/date-time.helper";
import type { IIssueComment } from "types";
type Props = {
comment: IIssueComment;
handleCommentDeletion: (comment: string) => void;
onSubmit: (commentId: string, data: Partial<IIssueComment>) => void;
showAccessSpecifier?: boolean;
workspaceSlug: string;
comment: IIssueComment;
onSubmit: (comment: IIssueComment) => void;
handleCommentDeletion: (comment: string) => void;
};
export const CommentCard: React.FC<Props> = ({
comment,
handleCommentDeletion,
onSubmit,
showAccessSpecifier = false,
workspaceSlug,
onSubmit,
handleCommentDeletion,
}) => {
const { user } = useUser();
@@ -47,11 +45,11 @@ export const CommentCard: React.FC<Props> = ({
defaultValues: comment,
});
const onEnter = (formData: Partial<IIssueComment>) => {
const onEnter = (formData: IIssueComment) => {
if (isSubmitting) return;
setIsEditing(false);
onSubmit(comment.id, formData);
onSubmit(formData);
editorRef.current?.setEditorValue(formData.comment_html);
showEditorRef.current?.setEditorValue(formData.comment_html);
@@ -101,7 +99,7 @@ export const CommentCard: React.FC<Props> = ({
: comment.actor_detail.display_name}
</div>
<p className="mt-0.5 text-xs text-custom-text-200">
commented {timeAgo(comment.created_at)}
Commented {timeAgo(comment.created_at)}
</p>
</div>
<div className="issue-comments-section p-0">
@@ -139,15 +137,7 @@ export const CommentCard: React.FC<Props> = ({
</button>
</div>
</form>
<div className={`relative ${isEditing ? "hidden" : ""}`}>
{showAccessSpecifier && (
<div className="absolute top-1 right-1.5 z-[1] text-custom-text-300">
<Icon
iconName={comment.access === "INTERNAL" ? "lock" : "public"}
className="!text-xs"
/>
</div>
)}
<div className={`${isEditing ? "hidden" : ""}`}>
<TipTapEditor
workspaceSlug={workspaceSlug as string}
ref={showEditorRef}
@@ -161,44 +151,13 @@ export const CommentCard: React.FC<Props> = ({
</div>
{user?.id === comment.actor && (
<CustomMenu ellipsis>
<CustomMenu.MenuItem
onClick={() => setIsEditing(true)}
className="flex items-center gap-1"
>
<Icon iconName="edit" />
Edit comment
</CustomMenu.MenuItem>
{showAccessSpecifier && (
<>
{comment.access === "INTERNAL" ? (
<CustomMenu.MenuItem
renderAs="button"
onClick={() => onSubmit(comment.id, { access: "EXTERNAL" })}
className="flex items-center gap-1"
>
<Icon iconName="public" />
Switch to public comment
</CustomMenu.MenuItem>
) : (
<CustomMenu.MenuItem
renderAs="button"
onClick={() => onSubmit(comment.id, { access: "INTERNAL" })}
className="flex items-center gap-1"
>
<Icon iconName="lock" />
Switch to private comment
</CustomMenu.MenuItem>
)}
</>
)}
<CustomMenu.MenuItem onClick={() => setIsEditing(true)}>Edit</CustomMenu.MenuItem>
<CustomMenu.MenuItem
onClick={() => {
handleCommentDeletion(comment.id);
}}
className="flex items-center gap-1"
>
<Icon iconName="delete" />
Delete comment
Delete
</CustomMenu.MenuItem>
</CustomMenu>
)}
+2 -2
View File
@@ -3,7 +3,7 @@ import { useRouter } from "next/router";
// ui
import { Tooltip } from "components/ui";
// icons
import { StateGroupIcon } from "components/icons";
import { getStateGroupIcon } from "components/icons";
// helpers
import { findTotalDaysInRange, renderShortDate } from "helpers/date-time.helper";
// types
@@ -52,7 +52,7 @@ export const IssueGanttSidebarBlock = ({ data }: { data: IIssue }) => {
className="relative w-full flex items-center gap-2 h-full cursor-pointer"
onClick={() => router.push(`/${workspaceSlug}/projects/${data?.project}/issues/${data?.id}`)}
>
<StateGroupIcon stateGroup={data?.state_detail?.group} color={data?.state_detail?.color} />
{getStateGroupIcon(data?.state_detail?.group, "14", "14", data?.state_detail?.color)}
<div className="text-xs text-custom-text-300 flex-shrink-0">
{data?.project_detail?.identifier} {data?.sequence_id}
</div>
+3 -4
View File
@@ -77,7 +77,7 @@ export const IssueMainContent: React.FC<Props> = ({
: null
);
const handleCommentUpdate = async (commentId: string, data: Partial<IIssueComment>) => {
const handleCommentUpdate = async (comment: IIssueComment) => {
if (!workspaceSlug || !projectId || !issueId) return;
await issuesService
@@ -85,8 +85,8 @@ export const IssueMainContent: React.FC<Props> = ({
workspaceSlug as string,
projectId as string,
issueId as string,
commentId,
data,
comment.id,
comment,
user
)
.then(() => mutateIssueActivity());
@@ -222,7 +222,6 @@ export const IssueMainContent: React.FC<Props> = ({
activity={issueActivity}
handleCommentUpdate={handleCommentUpdate}
handleCommentDelete={handleCommentDelete}
showAccessSpecifier={projectDetails && projectDetails.is_deployed}
/>
<AddComment
onSubmit={handleAddComment}
@@ -11,11 +11,11 @@ import { DateFilterModal } from "components/core";
// ui
import { MultiLevelDropdown } from "components/ui";
// icons
import { PriorityIcon, StateGroupIcon } from "components/icons";
import { getPriorityIcon, getStateGroupIcon } from "components/icons";
// helpers
import { checkIfArraysHaveSameElements } from "helpers/array.helper";
// types
import { IIssueFilterOptions, IQuery, TStateGroups } from "types";
import { IIssueFilterOptions, IQuery } from "types";
// fetch-keys
import { WORKSPACE_LABELS } from "constants/fetch-keys";
// constants
@@ -61,10 +61,8 @@ export const MyIssuesSelectFilters: React.FC<Props> = ({
<DateFilterModal
title={dateFilterType.title}
field={dateFilterType.type}
filters={filters as IIssueFilterOptions}
handleClose={() => setIsDateFilterModalOpen(false)}
isOpen={isDateFilterModalOpen}
onSelect={onSelect}
handleClose={() => setIsDateFilterModalOpen(false)}
/>
)}
<MultiLevelDropdown
@@ -83,7 +81,7 @@ export const MyIssuesSelectFilters: React.FC<Props> = ({
id: priority === null ? "null" : priority,
label: (
<div className="flex items-center gap-2 capitalize">
<PriorityIcon priority={priority} /> {priority ?? "None"}
{getPriorityIcon(priority)} {priority ?? "None"}
</div>
),
value: {
@@ -104,7 +102,7 @@ export const MyIssuesSelectFilters: React.FC<Props> = ({
id: key,
label: (
<div className="flex items-center gap-2">
<StateGroupIcon stateGroup={key as TStateGroups} />
{getStateGroupIcon(key as any, "16", "16")}{" "}
{GROUP_CHOICES[key as keyof typeof GROUP_CHOICES]}
</div>
),
@@ -18,7 +18,7 @@ import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
// helpers
import { orderArrayBy } from "helpers/array.helper";
// types
import { IIssue, IIssueFilterOptions, TIssuePriorities } from "types";
import { IIssue, IIssueFilterOptions } from "types";
// fetch-keys
import { USER_ISSUES, WORKSPACE_LABELS } from "constants/fetch-keys";
import { PlusIcon } from "@heroicons/react/24/outline";
@@ -96,7 +96,7 @@ export const MyIssuesView: React.FC<Props> = ({
const sourceGroup = source.droppableId;
const destinationGroup = destination.droppableId;
draggedItem[groupBy] = destinationGroup as TIssuePriorities;
draggedItem[groupBy] = destinationGroup;
mutate<{
[key: string]: IIssue[];
@@ -136,7 +136,7 @@ export const ParentIssuesListModal: React.FC<Props> = ({
onClick={() => setIsWorkspaceLevel((prevData) => !prevData)}
className="flex-shrink-0"
>
Workspace Level
workspace level
</button>
</div>
</Tooltip>

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