Merge pull request #2143 from makeplane/stage-release

promote: stage-release to master
This commit is contained in:
sriram veeraghanta 2023-09-11 18:21:51 +05:30 committed by GitHub
commit cc63f67654
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
147 changed files with 4761 additions and 1212 deletions

View File

@ -2,7 +2,7 @@ name: Update Docker Images for Plane on Release
on: on:
release: release:
types: [released] types: [released, prereleased]
jobs: jobs:
build_push_backend: build_push_backend:

View File

@ -41,9 +41,9 @@ class GPTIntegrationEndpoint(BaseAPIView):
final_text = task + "\n" + prompt final_text = task + "\n" + prompt
openai.api_key = settings.OPENAI_API_KEY openai.api_key = settings.OPENAI_API_KEY
response = openai.Completion.create( response = openai.ChatCompletion.create(
model=settings.GPT_ENGINE, model=settings.GPT_ENGINE,
prompt=final_text, messages=[{"role": "user", "content": final_text}],
temperature=0.7, temperature=0.7,
max_tokens=1024, max_tokens=1024,
) )
@ -51,7 +51,7 @@ class GPTIntegrationEndpoint(BaseAPIView):
workspace = Workspace.objects.get(slug=slug) workspace = Workspace.objects.get(slug=slug)
project = Project.objects.get(pk=project_id) project = Project.objects.get(pk=project_id)
text = response.choices[0].text.strip() text = response.choices[0].message.content.strip()
text_html = text.replace("\n", "<br/>") text_html = text.replace("\n", "<br/>")
return Response( return Response(
{ {

View File

@ -1575,7 +1575,7 @@ class IssueCommentPublicViewSet(BaseViewSet):
) )
) )
.distinct() .distinct()
) ).order_by("created_at")
else: else:
return IssueComment.objects.none() return IssueComment.objects.none()
except ProjectDeployBoard.DoesNotExist: except ProjectDeployBoard.DoesNotExist:
@ -2100,6 +2100,12 @@ class ProjectIssuesPublicEndpoint(BaseAPIView):
queryset=IssueReaction.objects.select_related("actor"), queryset=IssueReaction.objects.select_related("actor"),
) )
) )
.prefetch_related(
Prefetch(
"votes",
queryset=IssueVote.objects.select_related("actor"),
)
)
.filter(**filters) .filter(**filters)
.annotate(cycle_id=F("issue_cycle__cycle_id")) .annotate(cycle_id=F("issue_cycle__cycle_id"))
.annotate(module_id=F("issue_module__module_id")) .annotate(module_id=F("issue_module__module_id"))
@ -2189,6 +2195,7 @@ class ProjectIssuesPublicEndpoint(BaseAPIView):
states = ( states = (
State.objects.filter( State.objects.filter(
~Q(name="Triage"),
workspace__slug=slug, workspace__slug=slug,
project_id=project_id, project_id=project_id,
) )

View File

@ -482,7 +482,7 @@ class UserProjectInvitationsViewset(BaseViewSet):
# Delete joined project invites # Delete joined project invites
project_invitations.delete() project_invitations.delete()
return Response(status=status.HTTP_200_OK) return Response(status=status.HTTP_204_NO_CONTENT)
except Exception as e: except Exception as e:
capture_exception(e) capture_exception(e)
return Response( return Response(
@ -924,8 +924,7 @@ class ProjectUserViewsEndpoint(BaseAPIView):
project_member.save() project_member.save()
return Response(status=status.HTTP_200_OK) return Response(status=status.HTTP_204_NO_CONTENT)
except Project.DoesNotExist: except Project.DoesNotExist:
return Response( return Response(
{"error": "The requested resource does not exists"}, {"error": "The requested resource does not exists"},

View File

@ -116,7 +116,7 @@ class WorkSpaceViewSet(BaseViewSet):
) )
issue_count = ( issue_count = (
Issue.objects.filter(workspace=OuterRef("id")) Issue.issue_objects.filter(workspace=OuterRef("id"))
.order_by() .order_by()
.annotate(count=Func(F("id"), function="Count")) .annotate(count=Func(F("id"), function="Count"))
.values("count") .values("count")
@ -203,7 +203,7 @@ class UserWorkSpacesEndpoint(BaseAPIView):
) )
issue_count = ( issue_count = (
Issue.objects.filter(workspace=OuterRef("id")) Issue.issue_objects.filter(workspace=OuterRef("id"))
.order_by() .order_by()
.annotate(count=Func(F("id"), function="Count")) .annotate(count=Func(F("id"), function="Count"))
.values("count") .values("count")
@ -532,7 +532,7 @@ class UserWorkspaceInvitationsEndpoint(BaseViewSet):
# Delete joined workspace invites # Delete joined workspace invites
workspace_invitations.delete() workspace_invitations.delete()
return Response(status=status.HTTP_200_OK) return Response(status=status.HTTP_204_NO_CONTENT)
except Exception as e: except Exception as e:
capture_exception(e) capture_exception(e)
return Response( return Response(
@ -846,7 +846,7 @@ class WorkspaceMemberUserViewsEndpoint(BaseAPIView):
workspace_member.view_props = request.data.get("view_props", {}) workspace_member.view_props = request.data.get("view_props", {})
workspace_member.save() workspace_member.save()
return Response(status=status.HTTP_200_OK) return Response(status=status.HTTP_204_NO_CONTENT)
except WorkspaceMember.DoesNotExist: except WorkspaceMember.DoesNotExist:
return Response( return Response(
{"error": "User not a member of workspace"}, {"error": "User not a member of workspace"},
@ -1075,7 +1075,7 @@ class WorkspaceUserProfileStatsEndpoint(BaseAPIView):
priority_order = ["urgent", "high", "medium", "low", None] priority_order = ["urgent", "high", "medium", "low", None]
priority_distribution = ( priority_distribution = (
Issue.objects.filter( Issue.issue_objects.filter(
workspace__slug=slug, workspace__slug=slug,
assignees__in=[user_id], assignees__in=[user_id],
project__project_projectmember__member=request.user, project__project_projectmember__member=request.user,

View File

@ -32,7 +32,7 @@ def archive_old_issues():
archive_in = project.archive_in archive_in = project.archive_in
# Get all the issues whose updated_at in less that the archive_in month # Get all the issues whose updated_at in less that the archive_in month
issues = Issue.objects.filter( issues = Issue.issue_objects.filter(
Q( Q(
project=project_id, project=project_id,
archived_at__isnull=True, archived_at__isnull=True,
@ -64,21 +64,22 @@ def archive_old_issues():
issues_to_update.append(issue) issues_to_update.append(issue)
# Bulk Update the issues and log the activity # Bulk Update the issues and log the activity
updated_issues = Issue.objects.bulk_update( if issues_to_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,
) )
for issue in updated_issues [
] 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
]
return return
except Exception as e: except Exception as e:
if settings.DEBUG: if settings.DEBUG:
@ -99,7 +100,7 @@ def close_old_issues():
close_in = project.close_in close_in = project.close_in
# Get all the issues whose updated_at in less that the close_in month # Get all the issues whose updated_at in less that the close_in month
issues = Issue.objects.filter( issues = Issue.issue_objects.filter(
Q( Q(
project=project_id, project=project_id,
archived_at__isnull=True, archived_at__isnull=True,
@ -136,19 +137,20 @@ def close_old_issues():
issues_to_update.append(issue) issues_to_update.append(issue)
# Bulk Update the issues and log the activity # Bulk Update the issues and log the activity
updated_issues = Issue.objects.bulk_update(issues_to_update, ["state"], batch_size=100) if issues_to_update:
[ updated_issues = Issue.objects.bulk_update(issues_to_update, ["state"], batch_size=100)
issue_activity.delay( [
type="issue.activity.updated", issue_activity.delay(
requested_data=json.dumps({"closed_to": str(issue.state_id)}), type="issue.activity.updated",
actor_id=str(project.created_by_id), requested_data=json.dumps({"closed_to": str(issue.state_id)}),
issue_id=issue.id, actor_id=str(project.created_by_id),
project_id=project_id, issue_id=issue.id,
current_instance=None, project_id=project_id,
subscriber=False, current_instance=None,
) subscriber=False,
for issue in updated_issues )
] for issue in updated_issues
]
return return
except Exception as e: except Exception as e:
if settings.DEBUG: if settings.DEBUG:

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} chart_data = {str(date): 0 for date in date_range}
completed_issues_distribution = ( completed_issues_distribution = (
Issue.objects.filter( Issue.issue_objects.filter(
workspace__slug=slug, workspace__slug=slug,
project_id=project_id, project_id=project_id,
issue_cycle__cycle_id=cycle_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} chart_data = {str(date): 0 for date in date_range}
completed_issues_distribution = ( completed_issues_distribution = (
Issue.objects.filter( Issue.issue_objects.filter(
workspace__slug=slug, workspace__slug=slug,
project_id=project_id, project_id=project_id,
issue_module__module_id=module_id, issue_module__module_id=module_id,

View File

@ -1,36 +1,36 @@
# base requirements # base requirements
Django==4.2.3 Django==4.2.5
django-braces==1.15.0 django-braces==1.15.0
django-taggit==4.0.0 django-taggit==4.0.0
psycopg==3.1.9 psycopg==3.1.10
django-oauth-toolkit==2.3.0 django-oauth-toolkit==2.3.0
mistune==3.0.1 mistune==3.0.1
djangorestframework==3.14.0 djangorestframework==3.14.0
redis==4.6.0 redis==4.6.0
django-nested-admin==4.0.2 django-nested-admin==4.0.2
django-cors-headers==4.1.0 django-cors-headers==4.2.0
whitenoise==6.5.0 whitenoise==6.5.0
django-allauth==0.54.0 django-allauth==0.55.2
faker==18.11.2 faker==18.11.2
django-filter==23.2 django-filter==23.2
jsonmodels==2.6.0 jsonmodels==2.6.0
djangorestframework-simplejwt==5.2.2 djangorestframework-simplejwt==5.3.0
sentry-sdk==1.27.0 sentry-sdk==1.30.0
django-s3-storage==0.14.0 django-s3-storage==0.14.0
django-crum==0.7.9 django-crum==0.7.9
django-guardian==2.4.0 django-guardian==2.4.0
dj_rest_auth==2.2.5 dj_rest_auth==2.2.5
google-auth==2.21.0 google-auth==2.22.0
google-api-python-client==2.92.0 google-api-python-client==2.97.0
django-redis==5.3.0 django-redis==5.3.0
uvicorn==0.22.0 uvicorn==0.23.2
channels==4.0.0 channels==4.0.0
openai==0.27.8 openai==0.28.0
slack-sdk==3.21.3 slack-sdk==3.21.3
celery==5.3.1 celery==5.3.4
django_celery_beat==2.5.0 django_celery_beat==2.5.0
psycopg-binary==3.1.9 psycopg-binary==3.1.10
psycopg-c==3.1.9 psycopg-c==3.1.10
scout-apm==2.26.1 scout-apm==2.26.1
openpyxl==3.1.2 openpyxl==3.1.2

View File

@ -1,11 +1,11 @@
-r base.txt -r base.txt
dj-database-url==2.0.0 dj-database-url==2.1.0
gunicorn==20.1.0 gunicorn==21.2.0
whitenoise==6.5.0 whitenoise==6.5.0
django-storages==1.13.2 django-storages==1.14
boto3==1.27.0 boto3==1.28.40
django-anymail==10.0 django-anymail==10.1
django-debug-toolbar==4.1.0 django-debug-toolbar==4.1.0
gevent==23.7.0 gevent==23.7.0
psycogreen==1.0.2 psycogreen==1.0.2

View File

@ -85,7 +85,7 @@ services:
plane-worker: plane-worker:
container_name: planebgworker container_name: planebgworker
image: makeplane/plane-worker:latest image: makeplane/plane-backend:latest
restart: always restart: always
command: ./bin/worker command: ./bin/worker
env_file: env_file:
@ -99,7 +99,7 @@ services:
plane-beat-worker: plane-beat-worker:
container_name: planebeatworker container_name: planebeatworker
image: makeplane/plane-worker:latest image: makeplane/plane-backend:latest
restart: always restart: always
command: ./bin/beat command: ./bin/beat
env_file: env_file:

View File

@ -90,8 +90,6 @@ services:
DOCKER_BUILDKIT: 1 DOCKER_BUILDKIT: 1
restart: always restart: always
command: ./bin/takeoff command: ./bin/takeoff
ports:
- 8000:8000
env_file: env_file:
- .env - .env
environment: environment:

View File

@ -8,7 +8,6 @@
"packages/*" "packages/*"
], ],
"scripts": { "scripts": {
"prepare": "husky install",
"build": "turbo run build", "build": "turbo run build",
"dev": "turbo run dev", "dev": "turbo run dev",
"start": "turbo run start", "start": "turbo run start",

View File

@ -131,7 +131,7 @@ export const OnBoardingForm: React.FC<Props> = observer(({ user }) => {
type="button" 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`} 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="text-custom-text-400">{value || "Select your role..."}</span> <span className={value ? "" : "text-custom-text-400"}>{value || "Select your role..."}</span>
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" /> <ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
</Listbox.Button> </Listbox.Button>

View File

@ -13,13 +13,12 @@ import useToast from "hooks/use-toast";
// components // components
import { EmailPasswordForm, GithubLoginButton, GoogleLoginButton, EmailCodeForm } from "components/accounts"; import { EmailPasswordForm, GithubLoginButton, GoogleLoginButton, EmailCodeForm } from "components/accounts";
// images // images
import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.svg"; import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png";
export const SignInView = observer(() => { export const SignInView = observer(() => {
const { user: userStore } = useMobxStore(); const { user: userStore } = useMobxStore();
const router = useRouter(); const router = useRouter();
const { next_path } = router.query;
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
@ -34,13 +33,15 @@ export const SignInView = observer(() => {
const onSignInSuccess = (response: any) => { const onSignInSuccess = (response: any) => {
const isOnboarded = response?.user?.onboarding_step?.profile_complete || false; 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); userStore.setCurrentUser(response?.user);
if (!isOnboarded) { if (!isOnboarded) {
router.push(`/onboarding?next_path=${next_path}`); router.push(`/onboarding?next_path=${nextPath}`);
return; return;
} }
router.push((next_path ?? "/").toString()); router.push((nextPath ?? "/").toString());
}; };
const handleGoogleSignIn = async ({ clientId, credential }: any) => { const handleGoogleSignIn = async ({ clientId, credential }: any) => {

View File

@ -1,17 +1,9 @@
"use client"; "use client";
// helpers // helpers
import { renderFullDate } from "constants/helpers"; import { renderFullDate } from "helpers/date-time.helper";
export const findHowManyDaysLeft = (date: string | Date) => { export const dueDateIconDetails = (
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, date: string,
stateGroup: string stateGroup: string
): { ): {
@ -26,17 +18,24 @@ const dueDateIcon = (
className = ""; className = "";
} else { } else {
const today = new Date(); const today = new Date();
const dueDate = new Date(date); today.setHours(0, 0, 0, 0);
const targetDate = new Date(date);
targetDate.setHours(0, 0, 0, 0);
if (dueDate < today) { const timeDifference = targetDate.getTime() - today.getTime();
if (timeDifference < 0) {
iconName = "event_busy"; iconName = "event_busy";
className = "text-red-500"; className = "text-red-500";
} else if (dueDate > today) { } else if (timeDifference === 0) {
iconName = "calendar_today";
className = "";
} else {
iconName = "today"; iconName = "today";
className = "text-red-500"; className = "text-red-500";
} else if (timeDifference === 24 * 60 * 60 * 1000) {
iconName = "event";
className = "text-yellow-500";
} else {
iconName = "calendar_today";
className = "";
} }
} }
@ -47,7 +46,7 @@ const dueDateIcon = (
}; };
export const IssueBlockDueDate = ({ due_date, group }: { due_date: string; group: string }) => { export const IssueBlockDueDate = ({ due_date, group }: { due_date: string; group: string }) => {
const iconDetails = dueDateIcon(due_date, group); const iconDetails = dueDateIconDetails(due_date, group);
return ( 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"> <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">

View File

@ -1,17 +1,22 @@
import React, { useEffect, useState, useRef } from "react"; import React, { useState } from "react";
import { useForm, Controller } from "react-hook-form";
// mobx
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// react-hook-form
import { Controller, useForm } from "react-hook-form";
// headless ui
import { Menu, Transition } from "@headlessui/react"; import { Menu, Transition } from "@headlessui/react";
// lib // lib
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
// components
import { TipTapEditor } from "components/tiptap";
import { CommentReactions } from "components/issues/peek-overview";
// icons // icons
import { ChatBubbleLeftEllipsisIcon, CheckIcon, XMarkIcon, EllipsisVerticalIcon } from "@heroicons/react/24/outline"; import { ChatBubbleLeftEllipsisIcon, CheckIcon, XMarkIcon, EllipsisVerticalIcon } from "@heroicons/react/24/outline";
// helpers // helpers
import { timeAgo } from "helpers/date-time.helper"; import { timeAgo } from "helpers/date-time.helper";
// types // types
import { Comment } from "types/issue"; import { Comment } from "types/issue";
// components
import { TipTapEditor } from "components/tiptap";
type Props = { type Props = {
workspaceSlug: string; workspaceSlug: string;
@ -25,10 +30,13 @@ export const CommentCard: React.FC<Props> = observer((props) => {
// states // states
const [isEditing, setIsEditing] = useState(false); const [isEditing, setIsEditing] = useState(false);
const editorRef = React.useRef<any>(null);
const showEditorRef = React.useRef<any>(null);
const { const {
control,
formState: { isSubmitting }, formState: { isSubmitting },
handleSubmit, handleSubmit,
control,
} = useForm<any>({ } = useForm<any>({
defaultValues: { comment_html: comment.comment_html }, defaultValues: { comment_html: comment.comment_html },
}); });
@ -42,6 +50,9 @@ export const CommentCard: React.FC<Props> = observer((props) => {
if (!workspaceSlug || !issueDetailStore.peekId) return; if (!workspaceSlug || !issueDetailStore.peekId) return;
issueDetailStore.updateIssueComment(workspaceSlug, comment.project, issueDetailStore.peekId, comment.id, formData); issueDetailStore.updateIssueComment(workspaceSlug, comment.project, issueDetailStore.peekId, comment.id, formData);
setIsEditing(false); setIsEditing(false);
editorRef.current?.setEditorValue(formData.comment_html);
showEditorRef.current?.setEditorValue(formData.comment_html);
}; };
return ( return (
@ -76,7 +87,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} {comment.actor_detail.is_bot ? comment.actor_detail.first_name + " Bot" : comment.actor_detail.display_name}
</div> </div>
<p className="mt-0.5 text-xs text-custom-text-200"> <p className="mt-0.5 text-xs text-custom-text-200">
<>Commented {timeAgo(comment.created_at)}</> <>commented {timeAgo(comment.created_at)}</>
</p> </p>
</div> </div>
<div className="issue-comments-section p-0"> <div className="issue-comments-section p-0">
@ -91,6 +102,7 @@ export const CommentCard: React.FC<Props> = observer((props) => {
render={({ field: { onChange, value } }) => ( render={({ field: { onChange, value } }) => (
<TipTapEditor <TipTapEditor
workspaceSlug={workspaceSlug as string} workspaceSlug={workspaceSlug as string}
ref={editorRef}
value={value} value={value}
debouncedUpdatesEnabled={false} debouncedUpdatesEnabled={false}
customClassName="min-h-[50px] p-3 shadow-sm" customClassName="min-h-[50px] p-3 shadow-sm"
@ -120,11 +132,13 @@ export const CommentCard: React.FC<Props> = observer((props) => {
</form> </form>
<div className={`${isEditing ? "hidden" : ""}`}> <div className={`${isEditing ? "hidden" : ""}`}>
<TipTapEditor <TipTapEditor
workspaceSlug={workspaceSlug.toString()} workspaceSlug={workspaceSlug as string}
ref={showEditorRef}
value={comment.comment_html} value={comment.comment_html}
editable={false} editable={false}
customClassName="text-xs border border-custom-border-200 bg-custom-background-100" customClassName="text-xs border border-custom-border-200 bg-custom-background-100"
/> />
<CommentReactions commentId={comment.id} projectId={comment.project} />
</div> </div>
</div> </div>
</div> </div>

View File

@ -0,0 +1,131 @@
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>
);
});

View File

@ -0,0 +1,3 @@
export * from "./add-comment";
export * from "./comment-detail-card";
export * from "./comment-reactions";

View File

@ -1,5 +1,7 @@
import React from "react"; import React from "react";
// mobx
import { observer } from "mobx-react-lite";
// headless ui // headless ui
import { Listbox, Transition } from "@headlessui/react"; import { Listbox, Transition } from "@headlessui/react";
// hooks // hooks
@ -41,7 +43,7 @@ const peekModes: {
}, },
]; ];
export const PeekOverviewHeader: React.FC<Props> = (props) => { export const PeekOverviewHeader: React.FC<Props> = observer((props) => {
const { handleClose, issueDetails } = props; const { handleClose, issueDetails } = props;
const { issueDetails: issueDetailStore }: RootStore = useMobxStore(); const { issueDetails: issueDetailStore }: RootStore = useMobxStore();
@ -137,4 +139,4 @@ export const PeekOverviewHeader: React.FC<Props> = (props) => {
</div> </div>
</> </>
); );
}; });

View File

@ -1,3 +1,4 @@
export * from "./comment";
export * from "./full-screen-peek-view"; export * from "./full-screen-peek-view";
export * from "./header"; export * from "./header";
export * from "./issue-activity"; export * from "./issue-activity";
@ -8,5 +9,3 @@ export * from "./side-peek-view";
export * from "./issue-reaction"; export * from "./issue-reaction";
export * from "./issue-vote-reactions"; export * from "./issue-vote-reactions";
export * from "./issue-emoji-reactions"; export * from "./issue-emoji-reactions";
export * from "./comment-detail-card";
export * from "./add-comment";

View File

@ -20,18 +20,27 @@ export const IssueEmojiReactions: React.FC = observer(() => {
const reactions = issueId ? issueDetailsStore.details[issueId]?.reactions || [] : []; const reactions = issueId ? issueDetailsStore.details[issueId]?.reactions || [] : [];
const groupedReactions = groupReactions(reactions, "reaction"); const groupedReactions = groupReactions(reactions, "reaction");
const handleReactionSelectClick = (reactionHex: string) => { const userReactions = reactions?.filter((r) => r.actor_detail.id === user?.id);
const handleAddReaction = (reactionHex: string) => {
if (!workspace_slug || !project_slug || !issueId) return; 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); issueDetailsStore.addIssueReaction(workspace_slug.toString(), project_slug.toString(), issueId, reactionHex);
}; };
const handleReactionClick = (reactionHex: string) => { const handleRemoveReaction = (reactionHex: string) => {
if (!workspace_slug || !project_slug || !issueId) return; if (!workspace_slug || !project_slug || !issueId) return;
issueDetailsStore.removeIssueReaction(workspace_slug.toString(), project_slug.toString(), issueId, reactionHex); 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);
};
useEffect(() => { useEffect(() => {
if (user) return; if (user) return;
userStore.fetchCurrentUser(); userStore.fetchCurrentUser();
@ -42,9 +51,10 @@ export const IssueEmojiReactions: React.FC = observer(() => {
<ReactionSelector <ReactionSelector
onSelect={(value) => { onSelect={(value) => {
userStore.requiredLogin(() => { userStore.requiredLogin(() => {
handleReactionSelectClick(value); handleReactionClick(value);
}); });
}} }}
selected={userReactions?.map((r) => r.reaction)}
size="md" size="md"
/> />
<div className="flex items-center gap-2 flex-wrap"> <div className="flex items-center gap-2 flex-wrap">

View File

@ -2,9 +2,10 @@
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
// icons // icons
import { Icon } from "components/ui"; import { Icon } from "components/ui";
import { copyTextToClipboard, addSpaceIfCamelCase } from "helpers/string.helper";
// helpers // helpers
import { renderDateFormat } from "constants/helpers"; import { copyTextToClipboard, addSpaceIfCamelCase } from "helpers/string.helper";
import { renderFullDate } from "helpers/date-time.helper";
import { dueDateIconDetails } from "../board-views/block-due-date";
// types // types
import { IIssue } from "types/issue"; import { IIssue } from "types/issue";
import { IPeekMode } from "store/issue_details"; import { IPeekMode } from "store/issue_details";
@ -16,35 +17,16 @@ type Props = {
mode?: IPeekMode; 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 }) => { export const PeekOverviewIssueProperties: React.FC<Props> = ({ issueDetails, mode }) => {
const { setToastAlert } = useToast(); 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 state = issueDetails.state_detail;
const stateGroup = issueGroupFilter(state.group); const stateGroup = issueGroupFilter(state.group);
const priority = issueDetails.priority ? issuePriorityFilter(issueDetails.priority) : null; const priority = issueDetails.priority ? issuePriorityFilter(issueDetails.priority) : null;
const dueDateIcon = dueDateIconDetails(issueDetails.target_date, state.group);
const handleCopyLink = () => { const handleCopyLink = () => {
const urlToCopy = window.location.href; const urlToCopy = window.location.href;
@ -62,7 +44,6 @@ export const PeekOverviewIssueProperties: React.FC<Props> = ({ issueDetails, mod
{mode === "full" && ( {mode === "full" && (
<div className="flex justify-between gap-2 pb-3"> <div className="flex justify-between gap-2 pb-3">
<h6 className="flex items-center gap-2 font-medium"> <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} {issueDetails.project_detail.identifier}-{issueDetails.sequence_id}
</h6> </h6>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@ -125,11 +106,11 @@ export const PeekOverviewIssueProperties: React.FC<Props> = ({ issueDetails, mod
</div> </div>
<div> <div>
{issueDetails.target_date ? ( {issueDetails.target_date ? (
<div <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">
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 <span className={`material-symbols-rounded text-sm -my-0.5 ${dueDateIcon.className}`}>
${validDate(issueDetails.target_date, state)}`} {dueDateIcon.iconName}
> </span>
{renderDateFormat(issueDetails.target_date)} {renderFullDate(issueDetails.target_date)}
</div> </div>
) : ( ) : (
<span className="text-custom-text-200">Empty</span> <span className="text-custom-text-200">Empty</span>

View File

@ -77,14 +77,16 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
{...bubbleMenuProps} {...bubbleMenuProps}
className="flex w-fit divide-x divide-custom-border-300 rounded border border-custom-border-300 bg-custom-background-100 shadow-xl" className="flex w-fit divide-x divide-custom-border-300 rounded border border-custom-border-300 bg-custom-background-100 shadow-xl"
> >
<NodeSelector {!props.editor.isActive("table") && (
editor={props.editor!} <NodeSelector
isOpen={isNodeSelectorOpen} editor={props.editor!}
setIsOpen={() => { isOpen={isNodeSelectorOpen}
setIsNodeSelectorOpen(!isNodeSelectorOpen); setIsOpen={() => {
setIsLinkSelectorOpen(false); setIsNodeSelectorOpen(!isNodeSelectorOpen);
}} setIsLinkSelectorOpen(false);
/> }}
/>
)}
<LinkSelector <LinkSelector
editor={props.editor!!} editor={props.editor!!}
isOpen={isLinkSelectorOpen} isOpen={isLinkSelectorOpen}

View File

@ -28,7 +28,10 @@ export const NodeSelector: FC<NodeSelectorProps> = ({ editor, isOpen, setIsOpen
name: "Text", name: "Text",
icon: TextIcon, icon: TextIcon,
command: () => editor.chain().focus().toggleNode("paragraph", "paragraph").run(), 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", name: "H1",
@ -69,7 +72,8 @@ export const NodeSelector: FC<NodeSelectorProps> = ({ editor, isOpen, setIsOpen
{ {
name: "Quote", name: "Quote",
icon: TextQuote, icon: TextQuote,
command: () => editor.chain().focus().toggleNode("paragraph", "paragraph").toggleBlockquote().run(), command: () =>
editor.chain().focus().toggleNode("paragraph", "paragraph").toggleBlockquote().run(),
isActive: () => editor.isActive("blockquote"), isActive: () => editor.isActive("blockquote"),
}, },
{ {

View File

@ -13,6 +13,7 @@ import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight";
import { lowlight } from "lowlight/lib/core"; import { lowlight } from "lowlight/lib/core";
import SlashCommand from "../slash-command"; import SlashCommand from "../slash-command";
import { InputRule } from "@tiptap/core"; import { InputRule } from "@tiptap/core";
import Gapcursor from "@tiptap/extension-gapcursor";
import ts from "highlight.js/lib/languages/typescript"; import ts from "highlight.js/lib/languages/typescript";
@ -20,6 +21,10 @@ import "highlight.js/styles/github-dark.css";
import UniqueID from "@tiptap-pro/extension-unique-id"; import UniqueID from "@tiptap-pro/extension-unique-id";
import UpdatedImage from "./updated-image"; import UpdatedImage from "./updated-image";
import isValidHttpUrl from "../bubble-menu/utils/link-validator"; 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); lowlight.registerLanguage("ts", ts);
@ -27,113 +32,122 @@ export const TiptapExtensions = (
workspaceSlug: string, workspaceSlug: string,
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void
) => [ ) => [
StarterKit.configure({ StarterKit.configure({
bulletList: { bulletList: {
HTMLAttributes: { HTMLAttributes: {
class: "list-disc list-outside leading-3 -mt-2", class: "list-disc list-outside leading-3 -mt-2",
},
}, },
}, orderedList: {
orderedList: { HTMLAttributes: {
HTMLAttributes: { class: "list-decimal list-outside leading-3 -mt-2",
class: "list-decimal list-outside leading-3 -mt-2", },
}, },
}, listItem: {
listItem: { HTMLAttributes: {
HTMLAttributes: { class: "leading-normal -mb-2",
class: "leading-normal -mb-2", },
}, },
}, blockquote: {
blockquote: { HTMLAttributes: {
HTMLAttributes: { class: "border-l-4 border-custom-border-300",
class: "border-l-4 border-custom-border-300", },
}, },
}, code: {
code: { HTMLAttributes: {
HTMLAttributes: { class:
class: "rounded-md bg-custom-primary-30 mx-1 px-1 py-1 font-mono font-medium text-custom-text-1000", "rounded-md bg-custom-primary-30 mx-1 px-1 py-1 font-mono font-medium text-custom-text-1000",
spellcheck: "false", spellcheck: "false",
},
}, },
}, codeBlock: false,
codeBlock: false, horizontalRule: false,
horizontalRule: false, dropcursor: {
dropcursor: { color: "rgba(var(--color-text-100))",
color: "#DBEAFE", width: 2,
width: 2, },
}, gapcursor: false,
gapcursor: false, }),
}), CodeBlockLowlight.configure({
CodeBlockLowlight.configure({ lowlight,
lowlight, }),
}), HorizontalRule.extend({
HorizontalRule.extend({ addInputRules() {
addInputRules() { return [
return [ new InputRule({
new InputRule({ find: /^(?:---|—-|___\s|\*\*\*\s)$/,
find: /^(?:---|—-|___\s|\*\*\*\s)$/, handler: ({ state, range, commands }) => {
handler: ({ state, range, commands }) => { commands.splitBlock();
commands.splitBlock();
const attributes = {}; const attributes = {};
const { tr } = state; const { tr } = state;
const start = range.from; const start = range.from;
const end = range.to; const end = range.to;
// @ts-ignore // @ts-ignore
tr.replaceWith(start - 1, end, this.type.create(attributes)); tr.replaceWith(start - 1, end, this.type.create(attributes));
}, },
}), }),
]; ];
}, },
}).configure({ }).configure({
HTMLAttributes: { HTMLAttributes: {
class: "mb-6 border-t border-custom-border-300", class: "mb-6 border-t border-custom-border-300",
}, },
}), }),
TiptapLink.configure({ Gapcursor,
protocols: ["http", "https"], TiptapLink.configure({
validate: (url) => isValidHttpUrl(url), protocols: ["http", "https"],
HTMLAttributes: { validate: (url) => isValidHttpUrl(url),
class: HTMLAttributes: {
"text-custom-primary-300 underline underline-offset-[3px] hover:text-custom-primary-500 transition-colors cursor-pointer", class:
}, "text-custom-primary-300 underline underline-offset-[3px] hover:text-custom-primary-500 transition-colors cursor-pointer",
}), },
UpdatedImage.configure({ }),
HTMLAttributes: { UpdatedImage.configure({
class: "rounded-lg border border-custom-border-300", HTMLAttributes: {
}, class: "rounded-lg border border-custom-border-300",
}), },
Placeholder.configure({ }),
placeholder: ({ node }) => { Placeholder.configure({
if (node.type.name === "heading") { placeholder: ({ node }) => {
return `Heading ${node.attrs.level}`; if (node.type.name === "heading") {
} return `Heading ${node.attrs.level}`;
}
if (node.type.name === "image" || node.type.name === "table") {
return "";
}
return "Press '/' for commands..."; return "Press '/' for commands...";
}, },
includeChildren: true, includeChildren: true,
}), }),
UniqueID.configure({ UniqueID.configure({
types: ["image"], types: ["image"],
}), }),
SlashCommand(workspaceSlug, setIsSubmitting), SlashCommand(workspaceSlug, setIsSubmitting),
TiptapUnderline, TiptapUnderline,
TextStyle, TextStyle,
Color, Color,
Highlight.configure({ Highlight.configure({
multicolor: true, multicolor: true,
}), }),
TaskList.configure({ TaskList.configure({
HTMLAttributes: { HTMLAttributes: {
class: "not-prose pl-2", class: "not-prose pl-2",
}, },
}), }),
TaskItem.configure({ TaskItem.configure({
HTMLAttributes: { HTMLAttributes: {
class: "flex items-start my-4", class: "flex items-start my-4",
}, },
nested: true, nested: true,
}), }),
Markdown.configure({ Markdown.configure({
html: true, html: true,
transformCopiedText: true, transformCopiedText: true,
}), }),
]; Table,
TableHeader,
CustomTableCell,
TableRow,
];

View File

@ -0,0 +1,32 @@
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];
},
});

View File

@ -0,0 +1,7 @@
import { TableHeader as BaseTableHeader } from "@tiptap/extension-table-header";
const TableHeader = BaseTableHeader.extend({
content: "paragraph",
});
export { TableHeader };

View File

@ -0,0 +1,9 @@
import { Table as BaseTable } from "@tiptap/extension-table";
const Table = BaseTable.configure({
resizable: true,
cellMinWidth: 100,
allowTableNodeSelection: true,
});
export { Table };

View File

@ -6,6 +6,7 @@ import { EditorBubbleMenu } from "./bubble-menu";
import { TiptapExtensions } from "./extensions"; import { TiptapExtensions } from "./extensions";
import { TiptapEditorProps } from "./props"; import { TiptapEditorProps } from "./props";
import { ImageResizer } from "./extensions/image-resize"; import { ImageResizer } from "./extensions/image-resize";
import { TableMenu } from "./table-menu";
export interface ITipTapRichTextEditor { export interface ITipTapRichTextEditor {
value: string; value: string;
@ -37,6 +38,7 @@ const Tiptap = (props: ITipTapRichTextEditor) => {
borderOnFocus, borderOnFocus,
customClassName, customClassName,
} = props; } = props;
const editor = useEditor({ const editor = useEditor({
editable: editable ?? true, editable: editable ?? true,
editorProps: TiptapEditorProps(workspaceSlug, setIsSubmitting), editorProps: TiptapEditorProps(workspaceSlug, setIsSubmitting),
@ -54,12 +56,6 @@ const Tiptap = (props: ITipTapRichTextEditor) => {
}, },
}); });
useEffect(() => {
if (editor) {
editor.commands.setContent(value);
}
}, [value]);
const editorRef: React.MutableRefObject<Editor | null> = useRef(null); const editorRef: React.MutableRefObject<Editor | null> = useRef(null);
useImperativeHandle(forwardedRef, () => ({ useImperativeHandle(forwardedRef, () => ({
@ -81,8 +77,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 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"} ${ ${noBorder ? "" : "border border-custom-border-200"} ${
borderOnFocus ? "focus:border border-custom-border-300" : "focus:border-0" borderOnFocus ? "focus:border border-custom-border-300" : "focus:border-0"
} ${customClassName}`; } ${customClassName}`;
if (!editor) return null; if (!editor) return null;
editorRef.current = editor; editorRef.current = editor;
@ -98,6 +94,7 @@ const Tiptap = (props: ITipTapRichTextEditor) => {
{editor && <EditorBubbleMenu editor={editor} />} {editor && <EditorBubbleMenu editor={editor} />}
<div className={`${editorContentCustomClassNames}`}> <div className={`${editorContentCustomClassNames}`}>
<EditorContent editor={editor} /> <EditorContent editor={editor} />
<TableMenu editor={editor} />
{editor?.isActive("image") && <ImageResizer editor={editor} />} {editor?.isActive("image") && <ImageResizer editor={editor} />}
</div> </div>
</div> </div>

View File

@ -1,43 +1,51 @@
import { Plugin, PluginKey } from "@tiptap/pm/state"; import { EditorState, Plugin, PluginKey, Transaction } from "@tiptap/pm/state";
import { Node as ProseMirrorNode } from "@tiptap/pm/model"; import { Node as ProseMirrorNode } from "@tiptap/pm/model";
import fileService from "services/file.service"; import fileService from "services/file.service";
const deleteKey = new PluginKey("delete-image"); const deleteKey = new PluginKey("delete-image");
const IMAGE_NODE_TYPE = "image";
const TrackImageDeletionPlugin = () => interface ImageNode extends ProseMirrorNode {
attrs: {
src: string;
id: string;
};
}
const TrackImageDeletionPlugin = (): Plugin =>
new Plugin({ new Plugin({
key: deleteKey, key: deleteKey,
appendTransaction: (transactions, oldState, newState) => { 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);
}
});
transactions.forEach((transaction) => { transactions.forEach((transaction) => {
if (!transaction.docChanged) return; if (!transaction.docChanged) return;
const removedImages: ProseMirrorNode[] = []; const removedImages: ImageNode[] = [];
oldState.doc.descendants((oldNode, oldPos) => { oldState.doc.descendants((oldNode, oldPos) => {
if (oldNode.type.name !== "image") return; 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 (!newState.doc.resolve(oldPos).parent) return;
const newNode = newState.doc.nodeAt(oldPos); const newNode = newState.doc.nodeAt(oldPos);
// Check if the node has been deleted or replaced // Check if the node has been deleted or replaced
if (!newNode || newNode.type.name !== "image") { if (!newNode || newNode.type.name !== IMAGE_NODE_TYPE) {
// Check if the node still exists elsewhere in the document if (!newImageSources.has(oldNode.attrs.src)) {
let nodeExists = false; removedImages.push(oldNode as ImageNode);
newState.doc.descendants((node) => {
if (node.attrs.id === oldNode.attrs.id) {
nodeExists = true;
}
});
if (!nodeExists) {
removedImages.push(oldNode as ProseMirrorNode);
} }
} }
}); });
removedImages.forEach((node) => { removedImages.forEach(async (node) => {
const src = node.attrs.src; const src = node.attrs.src;
onNodeDeleted(src); await onNodeDeleted(src);
}); });
}); });
@ -47,10 +55,14 @@ const TrackImageDeletionPlugin = () =>
export default TrackImageDeletionPlugin; export default TrackImageDeletionPlugin;
async function onNodeDeleted(src: string) { async function onNodeDeleted(src: string): Promise<void> {
const assetUrlWithWorkspaceId = new URL(src).pathname.substring(1); try {
const resStatus = await fileService.deleteImage(assetUrlWithWorkspaceId); const assetUrlWithWorkspaceId = new URL(src).pathname.substring(1);
if (resStatus === 204) { const resStatus = await fileService.deleteImage(assetUrlWithWorkspaceId);
console.log("Image deleted successfully"); if (resStatus === 204) {
console.log("Image deleted successfully");
}
} catch (error) {
console.error("Error deleting image: ", error);
} }
} }

View File

@ -1,4 +1,3 @@
// @ts-nocheck
import { EditorState, Plugin, PluginKey } from "@tiptap/pm/state"; import { EditorState, Plugin, PluginKey } from "@tiptap/pm/state";
import { Decoration, DecorationSet, EditorView } from "@tiptap/pm/view"; import { Decoration, DecorationSet, EditorView } from "@tiptap/pm/view";
import fileService from "services/file.service"; import fileService from "services/file.service";
@ -46,7 +45,11 @@ export default UploadImagesPlugin;
function findPlaceholder(state: EditorState, id: {}) { function findPlaceholder(state: EditorState, id: {}) {
const decos = uploadKey.getState(state); 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; return found.length ? found[0].from : null;
} }
@ -59,8 +62,6 @@ export async function startImageUpload(
) { ) {
if (!file.type.includes("image/")) { if (!file.type.includes("image/")) {
return; return;
} else if (file.size / 1024 / 1024 > 20) {
return;
} }
const id = {}; const id = {};
@ -93,7 +94,9 @@ export async function startImageUpload(
const imageSrc = typeof src === "object" ? reader.result : src; const imageSrc = typeof src === "object" ? reader.result : src;
const node = schema.nodes.image.create({ src: imageSrc }); 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); view.dispatch(transaction);
} }
@ -107,7 +110,9 @@ const UploadImageHandler = (file: File, workspaceSlug: string): Promise<string>
formData.append("attributes", JSON.stringify({})); formData.append("attributes", JSON.stringify({}));
return new Promise(async (resolve, reject) => { 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(); const image = new Image();
image.src = imageUrl; image.src = imageUrl;

View File

@ -1,5 +1,6 @@
import { EditorProps } from "@tiptap/pm/view"; import { EditorProps } from "@tiptap/pm/view";
import { startImageUpload } from "./plugins/upload-image"; import { startImageUpload } from "./plugins/upload-image";
import { findTableAncestor } from "./table-menu";
export function TiptapEditorProps( export function TiptapEditorProps(
workspaceSlug: string, workspaceSlug: string,
@ -21,6 +22,15 @@ export function TiptapEditorProps(
}, },
}, },
handlePaste: (view, event) => { 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]) { if (event.clipboardData && event.clipboardData.files && event.clipboardData.files[0]) {
event.preventDefault(); event.preventDefault();
const file = event.clipboardData.files[0]; const file = event.clipboardData.files[0];
@ -31,6 +41,15 @@ export function TiptapEditorProps(
return false; return false;
}, },
handleDrop: (view, event, _slice, moved) => { 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]) { if (!moved && event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files[0]) {
event.preventDefault(); event.preventDefault();
const file = event.dataTransfer.files[0]; const file = event.dataTransfer.files[0];

View File

@ -15,6 +15,7 @@ import {
MinusSquare, MinusSquare,
CheckSquare, CheckSquare,
ImageIcon, ImageIcon,
Table,
} from "lucide-react"; } from "lucide-react";
import { startImageUpload } from "../plugins/upload-image"; import { startImageUpload } from "../plugins/upload-image";
import { cn } from "../utils"; import { cn } from "../utils";
@ -46,6 +47,9 @@ const Command = Extension.create({
return [ return [
Suggestion({ Suggestion({
editor: this.editor, editor: this.editor,
allow({ editor }) {
return !editor.isActive("table");
},
...this.options.suggestion, ...this.options.suggestion,
}), }),
]; ];
@ -53,7 +57,10 @@ const Command = Extension.create({
}); });
const getSuggestionItems = const getSuggestionItems =
(workspaceSlug: string, setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void) => (
workspaceSlug: string,
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void
) =>
({ query }: { query: string }) => ({ query }: { query: string }) =>
[ [
{ {
@ -119,6 +126,20 @@ const getSuggestionItems =
editor.chain().focus().deleteRange(range).setHorizontalRule().run(); 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", title: "Numbered List",
description: "Create a list with numbering.", description: "Create a list with numbering.",
@ -134,14 +155,21 @@ const getSuggestionItems =
searchTerms: ["blockquote"], searchTerms: ["blockquote"],
icon: <TextQuote size={18} />, icon: <TextQuote size={18} />,
command: ({ editor, range }: CommandProps) => 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", title: "Code",
description: "Capture a code snippet.", description: "Capture a code snippet.",
searchTerms: ["codeblock"], searchTerms: ["codeblock"],
icon: <Code size={18} />, 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", title: "Image",
@ -190,7 +218,15 @@ 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 [selectedIndex, setSelectedIndex] = useState(0);
const selectItem = useCallback( const selectItem = useCallback(

View File

@ -0,0 +1,16 @@
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;

View File

@ -0,0 +1,15 @@
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;

View File

@ -0,0 +1,16 @@
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;

View File

@ -0,0 +1,15 @@
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;

View File

@ -0,0 +1,143 @@
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>
);
};

View File

@ -12,13 +12,14 @@ import { Icon } from "components/ui";
const reactionEmojis = ["128077", "128078", "128516", "128165", "128533", "129505", "9992", "128064"]; const reactionEmojis = ["128077", "128078", "128516", "128165", "128533", "129505", "9992", "128064"];
interface Props { interface Props {
size?: "sm" | "md" | "lg";
position?: "top" | "bottom";
onSelect: (emoji: string) => void; onSelect: (emoji: string) => void;
position?: "top" | "bottom";
selected?: string[];
size?: "sm" | "md" | "lg";
} }
export const ReactionSelector: React.FC<Props> = (props) => { export const ReactionSelector: React.FC<Props> = (props) => {
const { onSelect, position, size } = props; const { onSelect, position, selected = [], size } = props;
return ( return (
<Popover className="relative"> <Popover className="relative">
@ -51,7 +52,7 @@ export const ReactionSelector: React.FC<Props> = (props) => {
position === "top" ? "-top-12" : "-bottom-12" position === "top" ? "-top-12" : "-bottom-12"
}`} }`}
> >
<div className="bg-custom-sidebar-background-100 border border-custom-border-200 rounded-md p-1"> <div className="bg-custom-sidebar-background-100 border border-custom-border-200 shadow-custom-shadow-sm rounded-md p-1">
<div className="flex gap-x-1"> <div className="flex gap-x-1">
{reactionEmojis.map((emoji) => ( {reactionEmojis.map((emoji) => (
<button <button
@ -61,7 +62,9 @@ export const ReactionSelector: React.FC<Props> = (props) => {
onSelect(emoji); onSelect(emoji);
closePopover(); closePopover();
}} }}
className="flex select-none items-center justify-between rounded-md text-sm p-1 hover:bg-custom-sidebar-background-90" 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"
}`}
> >
{renderEmoji(emoji)} {renderEmoji(emoji)}
</button> </button>

View File

@ -1,36 +0,0 @@
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}`;
};

View File

@ -12,3 +12,26 @@ export const timeAgo = (time: any) => {
time = +new Date(); 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}`;
};

View File

@ -1,7 +1,8 @@
import useSWR from "swr";
import type { GetServerSideProps } from "next";
import { useRouter } from "next/router";
import Head from "next/head"; import Head from "next/head";
import { useRouter } from "next/router";
import useSWR from "swr";
/// layouts /// layouts
import ProjectLayout from "layouts/project-layout"; import ProjectLayout from "layouts/project-layout";
// components // components
@ -39,12 +40,4 @@ 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; export default WorkspaceProjectPage;

View File

@ -1,7 +1,7 @@
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import Image from "next/image"; import Image from "next/image";
// assets // assets
import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.svg"; import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png";
// mobx // mobx
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -1,15 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 690 B

View File

@ -93,16 +93,6 @@ 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> { async createIssueComment(workspaceSlug: string, projectId: string, issueId: string, data: any): Promise<any> {
return this.post( return this.post(
`/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/issues/${issueId}/comments/`, `/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/issues/${issueId}/comments/`,
@ -140,6 +130,39 @@ class IssueService extends APIService {
throw error?.response; 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; export default IssueService;

View File

@ -32,6 +32,20 @@ export interface IIssueDetailStore {
data: any data: any
) => Promise<any>; ) => Promise<any>;
deleteIssueComment: (workspaceId: string, projectId: string, issueId: string, comment_id: string) => void; 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 // issue reactions
addIssueReaction: (workspaceId: string, projectId: string, issueId: string, reactionHex: string) => void; addIssueReaction: (workspaceId: string, projectId: string, issueId: string, reactionHex: string) => void;
removeIssueReaction: (workspaceId: string, projectId: string, issueId: string, reactionHex: string) => void; removeIssueReaction: (workspaceId: string, projectId: string, issueId: string, reactionHex: string) => void;
@ -61,8 +75,17 @@ class IssueDetailStore implements IssueDetailStore {
details: observable.ref, details: observable.ref,
// actions // actions
setPeekId: action, setPeekId: action,
fetchIssueDetails: action,
setPeekMode: action, setPeekMode: action,
fetchIssueDetails: action,
addIssueComment: action,
updateIssueComment: action,
deleteIssueComment: action,
addCommentReaction: action,
removeCommentReaction: action,
addIssueReaction: action,
removeIssueReaction: action,
addIssueVote: action,
removeIssueVote: action,
}); });
this.issueService = new IssueService(); this.issueService = new IssueService();
this.rootStore = _rootStore; this.rootStore = _rootStore;
@ -131,29 +154,32 @@ class IssueDetailStore implements IssueDetailStore {
data: any data: any
) => { ) => {
try { try {
const issueCommentUpdateResponse = await this.issueService.updateIssueComment( runInAction(() => {
workspaceSlug, this.details = {
projectId, ...this.details,
issueId, [issueId]: {
commentId, ...this.details[issueId],
data comments: this.details[issueId].comments.map((c) => ({
); ...c,
...(c.id === commentId ? data : {}),
})),
},
};
});
if (issueCommentUpdateResponse) { await this.issueService.updateIssueComment(workspaceSlug, projectId, issueId, commentId, data);
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) { } catch (error) {
console.log("Failed to add issue comment"); const issueComments = await this.issueService.getIssueComments(workspaceSlug, projectId, issueId);
runInAction(() => {
this.details = {
...this.details,
[issueId]: {
...this.details[issueId],
comments: issueComments,
},
};
});
} }
}; };
@ -175,6 +201,94 @@ 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) => { addIssueReaction = async (workspaceSlug: string, projectId: string, issueId: string, reactionHex: string) => {
try { try {
runInAction(() => { runInAction(() => {

View File

@ -62,17 +62,13 @@ class UserStore implements IUserStore {
return; return;
} }
const currentPath = window.location.pathname + window.location.search;
this.fetchCurrentUser() this.fetchCurrentUser()
.then(() => { .then(() => {
if (!this.currentUser) { if (!this.currentUser) window.location.href = `/?next_path=${currentPath}`;
const currentPath = window.location.pathname; else callback();
window.location.href = `/?next_path=${currentPath}`;
} else callback();
}) })
.catch(() => { .catch(() => (window.location.href = `/?next_path=${currentPath}`));
const currentPath = window.location.pathname;
window.location.href = `/?next_path=${currentPath}`;
});
}; };
fetchCurrentUser = async () => { fetchCurrentUser = async () => {

View File

@ -68,25 +68,30 @@ export interface IVote {
} }
export interface Comment { export interface Comment {
id: string;
actor_detail: ActorDetail; actor_detail: ActorDetail;
issue_detail: IssueDetail;
project_detail: ProjectDetail;
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; access: string;
created_by: string;
updated_by: string;
project: string;
workspace: string;
issue: string;
actor: 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;
issue_detail: IssueDetail;
project: string;
project_detail: ProjectDetail;
updated_at: Date;
updated_by: string;
workspace: string;
workspace_detail: WorkspaceDetail;
} }
export interface IIssueReaction { export interface IIssueReaction {

View File

@ -1,13 +1,13 @@
// nivo // nivo
import { BarDatum } from "@nivo/bar"; import { BarDatum } from "@nivo/bar";
// icons // icons
import { getPriorityIcon } from "components/icons"; import { PriorityIcon } from "components/icons";
// helpers // helpers
import { addSpaceIfCamelCase } from "helpers/string.helper"; import { addSpaceIfCamelCase } from "helpers/string.helper";
// helpers // helpers
import { generateBarColor, renderMonthAndYear } from "helpers/analytics.helper"; import { generateBarColor, renderMonthAndYear } from "helpers/analytics.helper";
// types // types
import { IAnalyticsParams, IAnalyticsResponse } from "types"; import { IAnalyticsParams, IAnalyticsResponse, TIssuePriorities } from "types";
// constants // constants
import { ANALYTICS_X_AXIS_VALUES, ANALYTICS_Y_AXIS_VALUES, DATE_KEYS } from "constants/analytics"; 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"> <div className="flex items-center gap-2">
{params.segment === "priority" ? ( {params.segment === "priority" ? (
getPriorityIcon(key) <PriorityIcon priority={key as TIssuePriorities} />
) : ( ) : (
<span <span
className="h-3 w-3 flex-shrink-0 rounded" 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" ? ( {params.x_axis === "priority" ? (
getPriorityIcon(`${item.name}`) <PriorityIcon priority={item.name as TIssuePriorities} />
) : ( ) : (
<span <span
className="h-3 w-3 rounded" className="h-3 w-3 rounded"

View File

@ -1,7 +1,7 @@
// icons // icons
import { PlayIcon } from "@heroicons/react/24/outline"; import { PlayIcon } from "@heroicons/react/24/outline";
// types // types
import { IDefaultAnalyticsResponse } from "types"; import { IDefaultAnalyticsResponse, TStateGroups } from "types";
// constants // constants
import { STATE_GROUP_COLORS } from "constants/state"; import { STATE_GROUP_COLORS } from "constants/state";
@ -27,7 +27,7 @@ export const AnalyticsDemand: React.FC<Props> = ({ defaultAnalytics }) => (
<span <span
className="h-2 w-2 rounded-full" className="h-2 w-2 rounded-full"
style={{ style={{
backgroundColor: STATE_GROUP_COLORS[group.state_group], backgroundColor: STATE_GROUP_COLORS[group.state_group as TStateGroups],
}} }}
/> />
<h6 className="capitalize">{group.state_group}</h6> <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" className="absolute top-0 left-0 h-1 rounded duration-300"
style={{ style={{
width: `${percentage}%`, width: `${percentage}%`,
backgroundColor: STATE_GROUP_COLORS[group.state_group], backgroundColor: STATE_GROUP_COLORS[group.state_group as TStateGroups],
}} }}
/> />
</div> </div>

View File

@ -9,7 +9,7 @@ import { CustomSearchSelect, CustomSelect, ToggleSwitch } from "components/ui";
import { SelectMonthModal } from "components/automation"; import { SelectMonthModal } from "components/automation";
// icons // icons
import { ChevronDownIcon, Squares2X2Icon } from "@heroicons/react/24/outline"; import { ChevronDownIcon, Squares2X2Icon } from "@heroicons/react/24/outline";
import { getStateGroupIcon } from "components/icons"; import { StateGroupIcon } from "components/icons";
// services // services
import stateService from "services/state.service"; import stateService from "services/state.service";
// constants // constants
@ -46,7 +46,7 @@ export const AutoCloseAutomation: React.FC<Props> = ({ projectDetails, handleCha
query: state.name, query: state.name,
content: ( content: (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{getStateGroupIcon(state.group, "16", "16", state.color)} <StateGroupIcon stateGroup={state.group} color={state.color} height="16px" width="16px" />
{state.name} {state.name}
</div> </div>
), ),
@ -140,14 +140,19 @@ export const AutoCloseAutomation: React.FC<Props> = ({ projectDetails, handleCha
label={ label={
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{selectedOption ? ( {selectedOption ? (
getStateGroupIcon(selectedOption.group, "16", "16", selectedOption.color) <StateGroupIcon
stateGroup={selectedOption.group}
color={selectedOption.color}
height="16px"
width="16px"
/>
) : currentDefaultState ? ( ) : currentDefaultState ? (
getStateGroupIcon( <StateGroupIcon
currentDefaultState.group, stateGroup={currentDefaultState.group}
"16", color={currentDefaultState.color}
"16", height="16px"
currentDefaultState.color width="16px"
) />
) : ( ) : (
<Squares2X2Icon className="h-3.5 w-3.5 text-custom-text-200" /> <Squares2X2Icon className="h-3.5 w-3.5 text-custom-text-200" />
)} )}

View File

@ -1,5 +1,7 @@
import { useRouter } from "next/router";
import React, { Dispatch, SetStateAction, useCallback } from "react"; import React, { Dispatch, SetStateAction, useCallback } from "react";
import { useRouter } from "next/router";
import { mutate } from "swr"; import { mutate } from "swr";
// cmdk // cmdk
@ -7,12 +9,12 @@ import { Command } from "cmdk";
// services // services
import issuesService from "services/issues.service"; import issuesService from "services/issues.service";
// types // types
import { ICurrentUserResponse, IIssue } from "types"; import { ICurrentUserResponse, IIssue, TIssuePriorities } from "types";
// constants // constants
import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys"; import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
import { PRIORITIES } from "constants/project"; import { PRIORITIES } from "constants/project";
// icons // icons
import { CheckIcon, getPriorityIcon } from "components/icons"; import { CheckIcon, PriorityIcon } from "components/icons";
type Props = { type Props = {
setIsPaletteOpen: Dispatch<SetStateAction<boolean>>; setIsPaletteOpen: Dispatch<SetStateAction<boolean>>;
@ -54,7 +56,7 @@ export const ChangeIssuePriority: React.FC<Props> = ({ setIsPaletteOpen, issue,
[workspaceSlug, issueId, projectId, user] [workspaceSlug, issueId, projectId, user]
); );
const handleIssueState = (priority: string | null) => { const handleIssueState = (priority: TIssuePriorities) => {
submitChanges({ priority }); submitChanges({ priority });
setIsPaletteOpen(false); setIsPaletteOpen(false);
}; };
@ -68,7 +70,7 @@ export const ChangeIssuePriority: React.FC<Props> = ({ setIsPaletteOpen, issue,
className="focus:outline-none" className="focus:outline-none"
> >
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
{getPriorityIcon(priority)} <PriorityIcon priority={priority} />
<span className="capitalize">{priority ?? "None"}</span> <span className="capitalize">{priority ?? "None"}</span>
</div> </div>
<div>{priority === issue.priority && <CheckIcon className="h-3 w-3" />}</div> <div>{priority === issue.priority && <CheckIcon className="h-3 w-3" />}</div>

View File

@ -1,22 +1,24 @@
import { useRouter } from "next/router";
import React, { Dispatch, SetStateAction, useCallback } from "react"; import React, { Dispatch, SetStateAction, useCallback } from "react";
import { useRouter } from "next/router";
import useSWR, { mutate } from "swr"; import useSWR, { mutate } from "swr";
// cmdk // cmdk
import { Command } from "cmdk"; import { Command } from "cmdk";
// ui
import { Spinner } from "components/ui";
// helpers
import { getStatesList } from "helpers/state.helper";
// services // services
import issuesService from "services/issues.service"; import issuesService from "services/issues.service";
import stateService from "services/state.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 // types
import { ICurrentUserResponse, IIssue } from "types"; import { ICurrentUserResponse, IIssue } from "types";
// fetch keys // fetch keys
import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY, STATES_LIST } from "constants/fetch-keys"; import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY, STATES_LIST } from "constants/fetch-keys";
// icons
import { CheckIcon, getStateGroupIcon } from "components/icons";
type Props = { type Props = {
setIsPaletteOpen: Dispatch<SetStateAction<boolean>>; setIsPaletteOpen: Dispatch<SetStateAction<boolean>>;
@ -82,7 +84,12 @@ export const ChangeIssueState: React.FC<Props> = ({ setIsPaletteOpen, issue, use
className="focus:outline-none" className="focus:outline-none"
> >
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
{getStateGroupIcon(state.group, "16", "16", state.color)} <StateGroupIcon
stateGroup={state.group}
color={state.color}
height="16px"
width="16px"
/>
<p>{state.name}</p> <p>{state.name}</p>
</div> </div>
<div>{state.id === issue.state && <CheckIcon className="h-3 w-3" />}</div> <div>{state.id === issue.state && <CheckIcon className="h-3 w-3" />}</div>

View File

@ -1,15 +1,11 @@
import { Fragment } from "react"; import { Fragment } from "react";
import { useRouter } from "next/router";
// react-hook-form // react-hook-form
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
// react-datepicker // react-datepicker
import DatePicker from "react-datepicker"; import DatePicker from "react-datepicker";
// headless ui // headless ui
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
// hooks
import useIssuesView from "hooks/use-issues-view";
// components // components
import { DateFilterSelect } from "./date-filter-select"; import { DateFilterSelect } from "./date-filter-select";
// ui // ui
@ -23,8 +19,10 @@ import { IIssueFilterOptions } from "types";
type Props = { type Props = {
title: string; title: string;
field: keyof IIssueFilterOptions; field: keyof IIssueFilterOptions;
isOpen: boolean; filters: IIssueFilterOptions;
handleClose: () => void; handleClose: () => void;
isOpen: boolean;
onSelect: (option: any) => void;
}; };
type TFormValues = { type TFormValues = {
@ -39,12 +37,14 @@ const defaultValues: TFormValues = {
date2: new Date(new Date().getFullYear(), new Date().getMonth() + 1, new Date().getDate()), date2: new Date(new Date().getFullYear(), new Date().getMonth() + 1, new Date().getDate()),
}; };
export const DateFilterModal: React.FC<Props> = ({ title, field, isOpen, handleClose }) => { export const DateFilterModal: React.FC<Props> = ({
const { filters, setFilters } = useIssuesView(); title,
field,
const router = useRouter(); filters,
const { viewId } = router.query; handleClose,
isOpen,
onSelect,
}) => {
const { handleSubmit, watch, control } = useForm<TFormValues>({ const { handleSubmit, watch, control } = useForm<TFormValues>({
defaultValues, defaultValues,
}); });
@ -53,10 +53,10 @@ export const DateFilterModal: React.FC<Props> = ({ title, field, isOpen, handleC
const { filterType, date1, date2 } = formData; const { filterType, date1, date2 } = formData;
if (filterType === "range") { if (filterType === "range") {
setFilters( onSelect({
{ [field]: [`${renderDateFormat(date1)};after`, `${renderDateFormat(date2)};before`] }, key: field,
!Boolean(viewId) value: [`${renderDateFormat(date1)};after`, `${renderDateFormat(date2)};before`],
); });
} else { } else {
const filteredArray = (filters?.[field] as string[])?.filter((item) => { const filteredArray = (filters?.[field] as string[])?.filter((item) => {
if (item?.includes(filterType)) return false; if (item?.includes(filterType)) return false;
@ -66,17 +66,12 @@ export const DateFilterModal: React.FC<Props> = ({ title, field, isOpen, handleC
const filterOne = filteredArray && filteredArray?.length > 0 ? filteredArray[0] : null; const filterOne = filteredArray && filteredArray?.length > 0 ? filteredArray[0] : null;
if (filterOne) if (filterOne)
setFilters( onSelect({ key: field, value: [filterOne, `${renderDateFormat(date1)};${filterType}`] });
{ [field]: [filterOne, `${renderDateFormat(date1)};${filterType}`] },
!Boolean(viewId)
);
else else
setFilters( onSelect({
{ key: field,
[field]: [`${renderDateFormat(date1)};${filterType}`], value: [`${renderDateFormat(date1)};${filterType}`],
}, });
!Boolean(viewId)
);
} }
handleClose(); handleClose();
}; };

View File

@ -2,7 +2,7 @@ import React from "react";
// icons // icons
import { XMarkIcon } from "@heroicons/react/24/outline"; import { XMarkIcon } from "@heroicons/react/24/outline";
import { getPriorityIcon, getStateGroupIcon } from "components/icons"; import { PriorityIcon, StateGroupIcon } from "components/icons";
// ui // ui
import { Avatar } from "components/ui"; import { Avatar } from "components/ui";
// helpers // helpers
@ -71,12 +71,10 @@ export const FiltersList: React.FC<Props> = ({
}} }}
> >
<span> <span>
{getStateGroupIcon( <StateGroupIcon
state?.group ?? "backlog", stateGroup={state?.group ?? "backlog"}
"12", color={state?.color}
"12", />
state?.color
)}
</span> </span>
<span>{state?.name ?? ""}</span> <span>{state?.name ?? ""}</span>
<span <span
@ -105,7 +103,9 @@ export const FiltersList: React.FC<Props> = ({
backgroundColor: `${STATE_GROUP_COLORS[group]}20`, backgroundColor: `${STATE_GROUP_COLORS[group]}20`,
}} }}
> >
<span>{getStateGroupIcon(group, "16", "16")}</span> <span>
<StateGroupIcon stateGroup={group} color={undefined} />
</span>
<span>{group}</span> <span>{group}</span>
<span <span
className="cursor-pointer" className="cursor-pointer"
@ -136,7 +136,9 @@ export const FiltersList: React.FC<Props> = ({
: "bg-custom-background-90 text-custom-text-200" : "bg-custom-background-90 text-custom-text-200"
}`} }`}
> >
<span>{getPriorityIcon(priority)}</span> <span>
<PriorityIcon priority={priority} />
</span>
<span>{priority === "null" ? "None" : priority}</span> <span>{priority === "null" ? "None" : priority}</span>
<span <span
className="cursor-pointer" className="cursor-pointer"

View File

@ -212,7 +212,7 @@ export const ExistingIssuesListModal: React.FC<Props> = ({
onClick={() => setIsWorkspaceLevel((prevData) => !prevData)} onClick={() => setIsWorkspaceLevel((prevData) => !prevData)}
className="flex-shrink-0" className="flex-shrink-0"
> >
workspace level Workspace Level
</button> </button>
</div> </div>
</Tooltip> </Tooltip>

View File

@ -61,7 +61,7 @@ export const ReactionSelector: React.FC<Props> = (props) => {
position === "top" ? "-top-12" : "-bottom-12" position === "top" ? "-top-12" : "-bottom-12"
}`} }`}
> >
<div className="bg-custom-sidebar-background-100 border border-custom-border-200 rounded-md p-1"> <div className="bg-custom-sidebar-background-100 border border-custom-border-200 shadow-custom-shadow-sm rounded-md p-1">
<div className="flex gap-x-1"> <div className="flex gap-x-1">
{reactionEmojis.map((emoji) => ( {reactionEmojis.map((emoji) => (
<button <button

View File

@ -15,6 +15,7 @@ import {
TAssigneesDistribution, TAssigneesDistribution,
TCompletionChartDistribution, TCompletionChartDistribution,
TLabelsDistribution, TLabelsDistribution,
TStateGroups,
} from "types"; } from "types";
// constants // constants
import { STATE_GROUP_COLORS } from "constants/state"; import { STATE_GROUP_COLORS } from "constants/state";
@ -215,7 +216,7 @@ export const SidebarProgressStats: React.FC<Props> = ({
<span <span
className="block h-3 w-3 rounded-full " className="block h-3 w-3 rounded-full "
style={{ style={{
backgroundColor: STATE_GROUP_COLORS[group], backgroundColor: STATE_GROUP_COLORS[group as TStateGroups],
}} }}
/> />
<span className="text-xs capitalize">{group}</span> <span className="text-xs capitalize">{group}</span>

View File

@ -1,7 +1,7 @@
// components // components
import { SingleBoard } from "components/core/views/board-view/single-board"; import { SingleBoard } from "components/core/views/board-view/single-board";
// icons // icons
import { getStateGroupIcon } from "components/icons"; import { StateGroupIcon } from "components/icons";
// helpers // helpers
import { addSpaceIfCamelCase } from "helpers/string.helper"; import { addSpaceIfCamelCase } from "helpers/string.helper";
// types // types
@ -82,8 +82,14 @@ export const AllBoards: React.FC<Props> = ({
className="flex items-center justify-between gap-2 rounded bg-custom-background-90 p-2 shadow" className="flex items-center justify-between gap-2 rounded bg-custom-background-90 p-2 shadow"
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{currentState && {currentState && (
getStateGroupIcon(currentState.group, "16", "16", currentState.color)} <StateGroupIcon
stateGroup={currentState.group}
color={currentState.color}
height="16px"
width="16px"
/>
)}
<h4 className="text-sm capitalize"> <h4 className="text-sm capitalize">
{selectedGroup === "state" {selectedGroup === "state"
? addSpaceIfCamelCase(currentState?.name ?? "") ? addSpaceIfCamelCase(currentState?.name ?? "")

View File

@ -13,14 +13,16 @@ import useProjects from "hooks/use-projects";
import { Avatar, Icon } from "components/ui"; import { Avatar, Icon } from "components/ui";
// icons // icons
import { PlusIcon } from "@heroicons/react/24/outline"; import { PlusIcon } from "@heroicons/react/24/outline";
import { getPriorityIcon, getStateGroupIcon } from "components/icons"; import { PriorityIcon, StateGroupIcon } from "components/icons";
// helpers // helpers
import { addSpaceIfCamelCase } from "helpers/string.helper"; import { addSpaceIfCamelCase } from "helpers/string.helper";
import { renderEmoji } from "helpers/emoji.helper"; import { renderEmoji } from "helpers/emoji.helper";
// types // types
import { IIssueViewProps, IState } from "types"; import { IIssueViewProps, IState, TIssuePriorities, TStateGroups } from "types";
// fetch-keys // fetch-keys
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS } from "constants/fetch-keys"; import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS } from "constants/fetch-keys";
// constants
import { STATE_GROUP_COLORS } from "constants/state";
type Props = { type Props = {
currentState?: IState | null; currentState?: IState | null;
@ -97,14 +99,27 @@ export const BoardHeader: React.FC<Props> = ({
switch (selectedGroup) { switch (selectedGroup) {
case "state": case "state":
icon = icon = currentState && (
currentState && getStateGroupIcon(currentState.group, "16", "16", currentState.color); <StateGroupIcon
stateGroup={currentState.group}
color={currentState.color}
height="16px"
width="16px"
/>
);
break; break;
case "state_detail.group": case "state_detail.group":
icon = getStateGroupIcon(groupTitle as any, "16", "16"); icon = (
<StateGroupIcon
stateGroup={groupTitle as TStateGroups}
color={STATE_GROUP_COLORS[groupTitle as TStateGroups]}
height="16px"
width="16px"
/>
);
break; break;
case "priority": case "priority":
icon = getPriorityIcon(groupTitle, "text-lg"); icon = <PriorityIcon priority={groupTitle as TIssuePriorities} className="text-lg" />;
break; break;
case "project": case "project":
const project = projects?.find((p) => p.id === groupTitle); const project = projects?.find((p) => p.id === groupTitle);

View File

@ -29,7 +29,7 @@ import { PlusIcon } from "@heroicons/react/24/outline";
import { getStatesList } from "helpers/state.helper"; import { getStatesList } from "helpers/state.helper";
import { orderArrayBy } from "helpers/array.helper"; import { orderArrayBy } from "helpers/array.helper";
// types // types
import { IIssue, IIssueFilterOptions, IState } from "types"; import { IIssue, IIssueFilterOptions, IState, TIssuePriorities } from "types";
// fetch-keys // fetch-keys
import { import {
CYCLE_DETAILS, CYCLE_DETAILS,
@ -184,7 +184,8 @@ export const IssuesView: React.FC<Props> = ({
// if the issue is moved to a different group, then we will change the group of the // if the issue is moved to a different group, then we will change the group of the
// dragged item(or issue) // dragged item(or issue)
if (selectedGroup === "priority") draggedItem.priority = destinationGroup; if (selectedGroup === "priority")
draggedItem.priority = destinationGroup as TIssuePriorities;
else if (selectedGroup === "state") { else if (selectedGroup === "state") {
draggedItem.state = destinationGroup; draggedItem.state = destinationGroup;
draggedItem.state_detail = states?.find((s) => s.id === destinationGroup) as IState; draggedItem.state_detail = states?.find((s) => s.id === destinationGroup) as IState;

View File

@ -15,7 +15,7 @@ import { SingleListIssue } from "components/core";
import { Avatar, CustomMenu } from "components/ui"; import { Avatar, CustomMenu } from "components/ui";
// icons // icons
import { PlusIcon } from "@heroicons/react/24/outline"; import { PlusIcon } from "@heroicons/react/24/outline";
import { getPriorityIcon, getStateGroupIcon } from "components/icons"; import { PriorityIcon, StateGroupIcon } from "components/icons";
// helpers // helpers
import { addSpaceIfCamelCase } from "helpers/string.helper"; import { addSpaceIfCamelCase } from "helpers/string.helper";
import { renderEmoji } from "helpers/emoji.helper"; import { renderEmoji } from "helpers/emoji.helper";
@ -26,10 +26,14 @@ import {
IIssueLabels, IIssueLabels,
IIssueViewProps, IIssueViewProps,
IState, IState,
TIssuePriorities,
TStateGroups,
UserAuth, UserAuth,
} from "types"; } from "types";
// fetch-keys // fetch-keys
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS } from "constants/fetch-keys"; import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS } from "constants/fetch-keys";
// constants
import { STATE_GROUP_COLORS } from "constants/state";
type Props = { type Props = {
currentState?: IState | null; currentState?: IState | null;
@ -111,14 +115,27 @@ export const SingleList: React.FC<Props> = ({
switch (selectedGroup) { switch (selectedGroup) {
case "state": case "state":
icon = icon = currentState && (
currentState && getStateGroupIcon(currentState.group, "16", "16", currentState.color); <StateGroupIcon
stateGroup={currentState.group}
color={currentState.color}
height="16px"
width="16px"
/>
);
break; break;
case "state_detail.group": case "state_detail.group":
icon = getStateGroupIcon(groupTitle as any, "16", "16"); icon = (
<StateGroupIcon
stateGroup={groupTitle as TStateGroups}
color={STATE_GROUP_COLORS[groupTitle as TStateGroups]}
height="16px"
width="16px"
/>
);
break; break;
case "priority": case "priority":
icon = getPriorityIcon(groupTitle, "text-lg"); icon = <PriorityIcon priority={groupTitle as TIssuePriorities} className="text-lg" />;
break; break;
case "project": case "project":
const project = projects?.find((p) => p.id === groupTitle); const project = projects?.find((p) => p.id === groupTitle);

View File

@ -19,7 +19,7 @@ import { ActiveCycleProgressStats } from "components/cycles";
// icons // icons
import { CalendarDaysIcon } from "@heroicons/react/20/solid"; import { CalendarDaysIcon } from "@heroicons/react/20/solid";
import { getPriorityIcon } from "components/icons/priority-icon"; import { PriorityIcon } from "components/icons/priority-icon";
import { import {
TargetIcon, TargetIcon,
ContrastIcon, ContrastIcon,
@ -28,7 +28,7 @@ import {
TriangleExclamationIcon, TriangleExclamationIcon,
AlarmClockIcon, AlarmClockIcon,
LayerDiagonalIcon, LayerDiagonalIcon,
CompletedStateIcon, StateGroupIcon,
} from "components/icons"; } from "components/icons";
import { StarIcon } from "@heroicons/react/24/outline"; import { StarIcon } from "@heroicons/react/24/outline";
// components // components
@ -385,8 +385,8 @@ export const ActiveCycleDetails: React.FC = () => {
<LayerDiagonalIcon className="h-4 w-4 flex-shrink-0" /> <LayerDiagonalIcon className="h-4 w-4 flex-shrink-0" />
{cycle.total_issues} issues {cycle.total_issues} issues
</div> </div>
<div className="flex gap-2"> <div className="flex items-center gap-2">
<CompletedStateIcon width={16} height={16} color="#438AF3" /> <StateGroupIcon stateGroup="completed" height="14px" width="14px" />
{cycle.completed_issues} issues {cycle.completed_issues} issues
</div> </div>
</div> </div>
@ -477,7 +477,7 @@ export const ActiveCycleDetails: React.FC = () => {
: "border-orange-500/20 bg-orange-500/20 text-orange-500" : "border-orange-500/20 bg-orange-500/20 text-orange-500"
}`} }`}
> >
{getPriorityIcon(issue.priority, "text-sm")} <PriorityIcon priority={issue.priority} className="text-sm" />
</div> </div>
<ViewIssueLabel labelDetails={issue.label_details} maxRender={2} /> <ViewIssueLabel labelDetails={issue.label_details} maxRender={2} />
<div className={`flex items-center gap-2 text-custom-text-200`}> <div className={`flex items-center gap-2 text-custom-text-200`}>

View File

@ -63,7 +63,9 @@ export const CyclesListGanttChartView: FC<Props> = ({ cycles, mutateCycles }) =>
const blockFormat = (blocks: ICycle[]) => const blockFormat = (blocks: ICycle[]) =>
blocks && blocks.length > 0 blocks && blocks.length > 0
? blocks ? blocks
.filter((b) => b.start_date && b.end_date) .filter(
(b) => b.start_date && b.end_date && new Date(b.start_date) <= new Date(b.end_date)
)
.map((block) => ({ .map((block) => ({
data: block, data: block,
id: block.id, id: block.id,

View File

@ -41,7 +41,7 @@ const IntegrationGuide = () => {
); );
const handleCsvClose = () => { const handleCsvClose = () => {
router.replace(`/plane/settings/exports`); router.replace(`/${workspaceSlug?.toString()}/settings/exports`);
}; };
return ( return (

View File

@ -4,11 +4,13 @@ import { IGanttBlock } from "components/gantt-chart";
export const renderIssueBlocksStructure = (blocks: IIssue[]): IGanttBlock[] => export const renderIssueBlocksStructure = (blocks: IIssue[]): IGanttBlock[] =>
blocks && blocks.length > 0 blocks && blocks.length > 0
? blocks.map((block) => ({ ? blocks
data: block, .filter((b) => new Date(b?.start_date ?? "") <= new Date(b?.target_date ?? ""))
id: block.id, .map((block) => ({
sort_order: block.sort_order, data: block,
start_date: new Date(block.start_date ?? ""), id: block.id,
target_date: new Date(block.target_date ?? ""), sort_order: block.sort_order,
})) start_date: new Date(block.start_date ?? ""),
target_date: new Date(block.target_date ?? ""),
}))
: []; : [];

View File

@ -1,21 +0,0 @@
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>
);

View File

@ -1,78 +0,0 @@
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>
);

View File

@ -1,69 +0,0 @@
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>
);

View File

@ -1,6 +1,7 @@
export * from "./module";
export * from "./state";
export * from "./alarm-clock-icon"; export * from "./alarm-clock-icon";
export * from "./attachment-icon"; export * from "./attachment-icon";
export * from "./backlog-state-icon";
export * from "./blocked-icon"; export * from "./blocked-icon";
export * from "./blocker-icon"; export * from "./blocker-icon";
export * from "./bolt-icon"; export * from "./bolt-icon";
@ -8,12 +9,10 @@ export * from "./calendar-before-icon";
export * from "./calendar-after-icon"; export * from "./calendar-after-icon";
export * from "./calendar-month-icon"; export * from "./calendar-month-icon";
export * from "./cancel-icon"; export * from "./cancel-icon";
export * from "./cancelled-state-icon";
export * from "./clipboard-icon"; export * from "./clipboard-icon";
export * from "./color-pallette-icon"; export * from "./color-pallette-icon";
export * from "./comment-icon"; export * from "./comment-icon";
export * from "./completed-cycle-icon"; export * from "./completed-cycle-icon";
export * from "./completed-state-icon";
export * from "./current-cycle-icon"; export * from "./current-cycle-icon";
export * from "./cycle-icon"; export * from "./cycle-icon";
export * from "./discord-icon"; export * from "./discord-icon";
@ -23,11 +22,9 @@ export * from "./ellipsis-horizontal-icon";
export * from "./external-link-icon"; export * from "./external-link-icon";
export * from "./github-icon"; export * from "./github-icon";
export * from "./heartbeat-icon"; export * from "./heartbeat-icon";
export * from "./started-state-icon";
export * from "./layer-diagonal-icon"; export * from "./layer-diagonal-icon";
export * from "./lock-icon"; export * from "./lock-icon";
export * from "./menu-icon"; export * from "./menu-icon";
export * from "./module";
export * from "./pencil-scribble-icon"; export * from "./pencil-scribble-icon";
export * from "./plus-icon"; export * from "./plus-icon";
export * from "./person-running-icon"; export * from "./person-running-icon";
@ -36,11 +33,8 @@ export * from "./question-mark-circle-icon";
export * from "./setting-icon"; export * from "./setting-icon";
export * from "./signal-cellular-icon"; export * from "./signal-cellular-icon";
export * from "./stacked-layers-icon"; export * from "./stacked-layers-icon";
export * from "./started-state-icon";
export * from "./state-group-icon";
export * from "./tag-icon"; export * from "./tag-icon";
export * from "./tune-icon"; export * from "./tune-icon";
export * from "./unstarted-state-icon";
export * from "./upcoming-cycle-icon"; export * from "./upcoming-cycle-icon";
export * from "./user-group-icon"; export * from "./user-group-icon";
export * from "./user-icon-circle"; export * from "./user-icon-circle";

View File

@ -1,22 +1,25 @@
export const getPriorityIcon = (priority: string | null, className?: string) => { // types
import { TIssuePriorities } from "types";
type Props = {
priority: TIssuePriorities | null;
className?: string;
};
export const PriorityIcon: React.FC<Props> = ({ priority, className = "" }) => {
if (!className || className === "") className = "text-xs flex items-center"; if (!className || className === "") className = "text-xs flex items-center";
priority = priority?.toLowerCase() ?? null; return (
<span className={`material-symbols-rounded ${className}`}>
switch (priority) { {priority === "urgent"
case "urgent": ? "error"
return <span className={`material-symbols-rounded ${className}`}>error</span>; : priority === "high"
case "high": ? "signal_cellular_alt"
return <span className={`material-symbols-rounded ${className}`}>signal_cellular_alt</span>; : priority === "medium"
case "medium": ? "signal_cellular_alt_2_bar"
return ( : priority === "low"
<span className={`material-symbols-rounded ${className}`}>signal_cellular_alt_2_bar</span> ? "signal_cellular_alt_1_bar"
); : "block"}
case "low": </span>
return ( );
<span className={`material-symbols-rounded ${className}`}>signal_cellular_alt_1_bar</span>
);
default:
return <span className={`material-symbols-rounded ${className}`}>block</span>;
}
}; };

View File

@ -1,77 +0,0 @@
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>
);

View File

@ -1,66 +0,0 @@
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 <></>;
}
};

View File

@ -0,0 +1,24 @@
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>
);

View File

@ -0,0 +1,34 @@
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>
);

View File

@ -0,0 +1,27 @@
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>
);

View File

@ -0,0 +1,6 @@
export * from "./backlog";
export * from "./cancelled";
export * from "./completed";
export * from "./started";
export * from "./state-group-icon";
export * from "./unstarted";

View File

@ -0,0 +1,25 @@
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>
);

View File

@ -0,0 +1,74 @@
// 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}`}
/>
);
};

View File

@ -0,0 +1,24 @@
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>
);

View File

@ -1,59 +0,0 @@
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>
);

View File

@ -3,7 +3,7 @@ import useInboxView from "hooks/use-inbox-view";
// ui // ui
import { MultiLevelDropdown } from "components/ui"; import { MultiLevelDropdown } from "components/ui";
// icons // icons
import { getPriorityIcon } from "components/icons"; import { PriorityIcon } from "components/icons";
// constants // constants
import { PRIORITIES } from "constants/project"; import { PRIORITIES } from "constants/project";
import { INBOX_STATUS } from "constants/inbox"; import { INBOX_STATUS } from "constants/inbox";
@ -42,7 +42,7 @@ export const FiltersDropdown: React.FC = () => {
id: priority === null ? "null" : priority, id: priority === null ? "null" : priority,
label: ( label: (
<div className="flex items-center gap-2 capitalize"> <div className="flex items-center gap-2 capitalize">
{getPriorityIcon(priority)} {priority ?? "None"} <PriorityIcon priority={priority} /> {priority ?? "None"}
</div> </div>
), ),
value: { value: {

View File

@ -2,9 +2,11 @@
import useInboxView from "hooks/use-inbox-view"; import useInboxView from "hooks/use-inbox-view";
// icons // icons
import { XMarkIcon } from "@heroicons/react/24/outline"; import { XMarkIcon } from "@heroicons/react/24/outline";
import { getPriorityIcon } from "components/icons"; import { PriorityIcon } from "components/icons";
// helpers // helpers
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper"; import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
// types
import { TIssuePriorities } from "types";
// constants // constants
import { INBOX_STATUS } from "constants/inbox"; import { INBOX_STATUS } from "constants/inbox";
@ -48,7 +50,9 @@ export const InboxFiltersList = () => {
: "bg-custom-background-90 text-custom-text-200" : "bg-custom-background-90 text-custom-text-200"
}`} }`}
> >
<span>{getPriorityIcon(priority)}</span> <span>
<PriorityIcon priority={priority as TIssuePriorities} />
</span>
<button <button
type="button" type="button"
className="cursor-pointer" className="cursor-pointer"

View File

@ -38,7 +38,7 @@ export const InboxIssueActivity: React.FC<Props> = ({ issueDetails }) => {
: null : null
); );
const handleCommentUpdate = async (comment: IIssueComment) => { const handleCommentUpdate = async (commentId: string, data: Partial<IIssueComment>) => {
if (!workspaceSlug || !projectId || !inboxIssueId) return; if (!workspaceSlug || !projectId || !inboxIssueId) return;
await issuesService await issuesService
@ -46,8 +46,8 @@ export const InboxIssueActivity: React.FC<Props> = ({ issueDetails }) => {
workspaceSlug as string, workspaceSlug as string,
projectId as string, projectId as string,
inboxIssueId as string, inboxIssueId as string,
comment.id, commentId,
comment, data,
user user
) )
.then(() => mutateIssueActivity()); .then(() => mutateIssueActivity());

View File

@ -4,7 +4,7 @@ import Link from "next/link";
// ui // ui
import { Tooltip } from "components/ui"; import { Tooltip } from "components/ui";
// icons // icons
import { getPriorityIcon } from "components/icons"; import { PriorityIcon } from "components/icons";
import { import {
CalendarDaysIcon, CalendarDaysIcon,
CheckCircleIcon, CheckCircleIcon,
@ -65,10 +65,7 @@ export const InboxIssueCard: React.FC<Props> = (props) => {
: "border-custom-border-200" : "border-custom-border-200"
}`} }`}
> >
{getPriorityIcon( <PriorityIcon priority={issue.priority ?? null} className="text-sm" />
issue.priority && issue.priority !== "" ? issue.priority ?? "" : "None",
"text-sm"
)}
</div> </div>
</Tooltip> </Tooltip>
<Tooltip <Tooltip

View File

@ -35,7 +35,7 @@ import type { IInboxIssue, IIssue } from "types";
// fetch-keys // fetch-keys
import { INBOX_ISSUES, INBOX_ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys"; import { INBOX_ISSUES, INBOX_ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
const defaultValues = { const defaultValues: Partial<IInboxIssue> = {
name: "", name: "",
description_html: "", description_html: "",
estimate_point: null, estimate_point: null,

View File

@ -15,14 +15,16 @@ import { IIssueActivity, IIssueComment } from "types";
type Props = { type Props = {
activity: IIssueActivity[] | undefined; activity: IIssueActivity[] | undefined;
handleCommentUpdate: (comment: IIssueComment) => Promise<void>; handleCommentUpdate: (commentId: string, data: Partial<IIssueComment>) => Promise<void>;
handleCommentDelete: (commentId: string) => Promise<void>; handleCommentDelete: (commentId: string) => Promise<void>;
showAccessSpecifier?: boolean;
}; };
export const IssueActivitySection: React.FC<Props> = ({ export const IssueActivitySection: React.FC<Props> = ({
activity, activity,
handleCommentUpdate, handleCommentUpdate,
handleCommentDelete, handleCommentDelete,
showAccessSpecifier = false,
}) => { }) => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
@ -131,10 +133,11 @@ export const IssueActivitySection: React.FC<Props> = ({
return ( return (
<div key={activityItem.id} className="mt-4"> <div key={activityItem.id} className="mt-4">
<CommentCard <CommentCard
workspaceSlug={workspaceSlug as string}
comment={activityItem as IIssueComment} comment={activityItem as IIssueComment}
onSubmit={handleCommentUpdate}
handleCommentDeletion={handleCommentDelete} handleCommentDeletion={handleCommentDelete}
onSubmit={handleCommentUpdate}
showAccessSpecifier={showAccessSpecifier}
workspaceSlug={workspaceSlug as string}
/> />
</div> </div>
); );

View File

@ -7,7 +7,7 @@ import { ChatBubbleLeftEllipsisIcon, CheckIcon, XMarkIcon } from "@heroicons/rea
// hooks // hooks
import useUser from "hooks/use-user"; import useUser from "hooks/use-user";
// ui // ui
import { CustomMenu } from "components/ui"; import { CustomMenu, Icon } from "components/ui";
import { CommentReaction } from "components/issues"; import { CommentReaction } from "components/issues";
import { TipTapEditor } from "components/tiptap"; import { TipTapEditor } from "components/tiptap";
// helpers // helpers
@ -16,17 +16,19 @@ import { timeAgo } from "helpers/date-time.helper";
import type { IIssueComment } from "types"; import type { IIssueComment } from "types";
type Props = { type Props = {
workspaceSlug: string;
comment: IIssueComment; comment: IIssueComment;
onSubmit: (comment: IIssueComment) => void;
handleCommentDeletion: (comment: string) => void; handleCommentDeletion: (comment: string) => void;
onSubmit: (commentId: string, data: Partial<IIssueComment>) => void;
showAccessSpecifier?: boolean;
workspaceSlug: string;
}; };
export const CommentCard: React.FC<Props> = ({ export const CommentCard: React.FC<Props> = ({
comment, comment,
workspaceSlug,
onSubmit,
handleCommentDeletion, handleCommentDeletion,
onSubmit,
showAccessSpecifier = false,
workspaceSlug,
}) => { }) => {
const { user } = useUser(); const { user } = useUser();
@ -45,11 +47,11 @@ export const CommentCard: React.FC<Props> = ({
defaultValues: comment, defaultValues: comment,
}); });
const onEnter = (formData: IIssueComment) => { const onEnter = (formData: Partial<IIssueComment>) => {
if (isSubmitting) return; if (isSubmitting) return;
setIsEditing(false); setIsEditing(false);
onSubmit(formData); onSubmit(comment.id, formData);
editorRef.current?.setEditorValue(formData.comment_html); editorRef.current?.setEditorValue(formData.comment_html);
showEditorRef.current?.setEditorValue(formData.comment_html); showEditorRef.current?.setEditorValue(formData.comment_html);
@ -99,7 +101,7 @@ export const CommentCard: React.FC<Props> = ({
: comment.actor_detail.display_name} : comment.actor_detail.display_name}
</div> </div>
<p className="mt-0.5 text-xs text-custom-text-200"> <p className="mt-0.5 text-xs text-custom-text-200">
Commented {timeAgo(comment.created_at)} commented {timeAgo(comment.created_at)}
</p> </p>
</div> </div>
<div className="issue-comments-section p-0"> <div className="issue-comments-section p-0">
@ -137,7 +139,15 @@ export const CommentCard: React.FC<Props> = ({
</button> </button>
</div> </div>
</form> </form>
<div className={`${isEditing ? "hidden" : ""}`}> <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>
)}
<TipTapEditor <TipTapEditor
workspaceSlug={workspaceSlug as string} workspaceSlug={workspaceSlug as string}
ref={showEditorRef} ref={showEditorRef}
@ -151,13 +161,44 @@ export const CommentCard: React.FC<Props> = ({
</div> </div>
{user?.id === comment.actor && ( {user?.id === comment.actor && (
<CustomMenu ellipsis> <CustomMenu ellipsis>
<CustomMenu.MenuItem onClick={() => setIsEditing(true)}>Edit</CustomMenu.MenuItem> <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 <CustomMenu.MenuItem
onClick={() => { onClick={() => {
handleCommentDeletion(comment.id); handleCommentDeletion(comment.id);
}} }}
className="flex items-center gap-1"
> >
Delete <Icon iconName="delete" />
Delete comment
</CustomMenu.MenuItem> </CustomMenu.MenuItem>
</CustomMenu> </CustomMenu>
)} )}

View File

@ -3,7 +3,7 @@ import { useRouter } from "next/router";
// ui // ui
import { Tooltip } from "components/ui"; import { Tooltip } from "components/ui";
// icons // icons
import { getStateGroupIcon } from "components/icons"; import { StateGroupIcon } from "components/icons";
// helpers // helpers
import { findTotalDaysInRange, renderShortDate } from "helpers/date-time.helper"; import { findTotalDaysInRange, renderShortDate } from "helpers/date-time.helper";
// types // types
@ -52,7 +52,7 @@ export const IssueGanttSidebarBlock = ({ data }: { data: IIssue }) => {
className="relative w-full flex items-center gap-2 h-full cursor-pointer" className="relative w-full flex items-center gap-2 h-full cursor-pointer"
onClick={() => router.push(`/${workspaceSlug}/projects/${data?.project}/issues/${data?.id}`)} onClick={() => router.push(`/${workspaceSlug}/projects/${data?.project}/issues/${data?.id}`)}
> >
{getStateGroupIcon(data?.state_detail?.group, "14", "14", data?.state_detail?.color)} <StateGroupIcon stateGroup={data?.state_detail?.group} color={data?.state_detail?.color} />
<div className="text-xs text-custom-text-300 flex-shrink-0"> <div className="text-xs text-custom-text-300 flex-shrink-0">
{data?.project_detail?.identifier} {data?.sequence_id} {data?.project_detail?.identifier} {data?.sequence_id}
</div> </div>

View File

@ -77,7 +77,7 @@ export const IssueMainContent: React.FC<Props> = ({
: null : null
); );
const handleCommentUpdate = async (comment: IIssueComment) => { const handleCommentUpdate = async (commentId: string, data: Partial<IIssueComment>) => {
if (!workspaceSlug || !projectId || !issueId) return; if (!workspaceSlug || !projectId || !issueId) return;
await issuesService await issuesService
@ -85,8 +85,8 @@ export const IssueMainContent: React.FC<Props> = ({
workspaceSlug as string, workspaceSlug as string,
projectId as string, projectId as string,
issueId as string, issueId as string,
comment.id, commentId,
comment, data,
user user
) )
.then(() => mutateIssueActivity()); .then(() => mutateIssueActivity());
@ -222,6 +222,7 @@ export const IssueMainContent: React.FC<Props> = ({
activity={issueActivity} activity={issueActivity}
handleCommentUpdate={handleCommentUpdate} handleCommentUpdate={handleCommentUpdate}
handleCommentDelete={handleCommentDelete} handleCommentDelete={handleCommentDelete}
showAccessSpecifier={projectDetails && projectDetails.is_deployed}
/> />
<AddComment <AddComment
onSubmit={handleAddComment} onSubmit={handleAddComment}

View File

@ -11,11 +11,11 @@ import { DateFilterModal } from "components/core";
// ui // ui
import { MultiLevelDropdown } from "components/ui"; import { MultiLevelDropdown } from "components/ui";
// icons // icons
import { getPriorityIcon, getStateGroupIcon } from "components/icons"; import { PriorityIcon, StateGroupIcon } from "components/icons";
// helpers // helpers
import { checkIfArraysHaveSameElements } from "helpers/array.helper"; import { checkIfArraysHaveSameElements } from "helpers/array.helper";
// types // types
import { IIssueFilterOptions, IQuery } from "types"; import { IIssueFilterOptions, IQuery, TStateGroups } from "types";
// fetch-keys // fetch-keys
import { WORKSPACE_LABELS } from "constants/fetch-keys"; import { WORKSPACE_LABELS } from "constants/fetch-keys";
// constants // constants
@ -61,8 +61,10 @@ export const MyIssuesSelectFilters: React.FC<Props> = ({
<DateFilterModal <DateFilterModal
title={dateFilterType.title} title={dateFilterType.title}
field={dateFilterType.type} field={dateFilterType.type}
isOpen={isDateFilterModalOpen} filters={filters as IIssueFilterOptions}
handleClose={() => setIsDateFilterModalOpen(false)} handleClose={() => setIsDateFilterModalOpen(false)}
isOpen={isDateFilterModalOpen}
onSelect={onSelect}
/> />
)} )}
<MultiLevelDropdown <MultiLevelDropdown
@ -81,7 +83,7 @@ export const MyIssuesSelectFilters: React.FC<Props> = ({
id: priority === null ? "null" : priority, id: priority === null ? "null" : priority,
label: ( label: (
<div className="flex items-center gap-2 capitalize"> <div className="flex items-center gap-2 capitalize">
{getPriorityIcon(priority)} {priority ?? "None"} <PriorityIcon priority={priority} /> {priority ?? "None"}
</div> </div>
), ),
value: { value: {
@ -102,7 +104,7 @@ export const MyIssuesSelectFilters: React.FC<Props> = ({
id: key, id: key,
label: ( label: (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{getStateGroupIcon(key as any, "16", "16")}{" "} <StateGroupIcon stateGroup={key as TStateGroups} />
{GROUP_CHOICES[key as keyof typeof GROUP_CHOICES]} {GROUP_CHOICES[key as keyof typeof GROUP_CHOICES]}
</div> </div>
), ),

View File

@ -18,7 +18,7 @@ import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
// helpers // helpers
import { orderArrayBy } from "helpers/array.helper"; import { orderArrayBy } from "helpers/array.helper";
// types // types
import { IIssue, IIssueFilterOptions } from "types"; import { IIssue, IIssueFilterOptions, TIssuePriorities } from "types";
// fetch-keys // fetch-keys
import { USER_ISSUES, WORKSPACE_LABELS } from "constants/fetch-keys"; import { USER_ISSUES, WORKSPACE_LABELS } from "constants/fetch-keys";
import { PlusIcon } from "@heroicons/react/24/outline"; import { PlusIcon } from "@heroicons/react/24/outline";
@ -96,7 +96,7 @@ export const MyIssuesView: React.FC<Props> = ({
const sourceGroup = source.droppableId; const sourceGroup = source.droppableId;
const destinationGroup = destination.droppableId; const destinationGroup = destination.droppableId;
draggedItem[groupBy] = destinationGroup; draggedItem[groupBy] = destinationGroup as TIssuePriorities;
mutate<{ mutate<{
[key: string]: IIssue[]; [key: string]: IIssue[];

View File

@ -136,7 +136,7 @@ export const ParentIssuesListModal: React.FC<Props> = ({
onClick={() => setIsWorkspaceLevel((prevData) => !prevData)} onClick={() => setIsWorkspaceLevel((prevData) => !prevData)}
className="flex-shrink-0" className="flex-shrink-0"
> >
workspace level Workspace Level
</button> </button>
</div> </div>
</Tooltip> </Tooltip>

View File

@ -5,6 +5,7 @@ import issuesService from "services/issues.service";
// hooks // hooks
import useUser from "hooks/use-user"; import useUser from "hooks/use-user";
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
import useProjectDetails from "hooks/use-project-details";
// components // components
import { AddComment, IssueActivitySection } from "components/issues"; import { AddComment, IssueActivitySection } from "components/issues";
// types // types
@ -22,6 +23,7 @@ export const PeekOverviewIssueActivity: React.FC<Props> = ({ workspaceSlug, issu
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
const { user } = useUser(); const { user } = useUser();
const { projectDetails } = useProjectDetails();
const { data: issueActivity, mutate: mutateIssueActivity } = useSWR( const { data: issueActivity, mutate: mutateIssueActivity } = useSWR(
workspaceSlug && issue ? PROJECT_ISSUES_ACTIVITY(issue.id) : null, workspaceSlug && issue ? PROJECT_ISSUES_ACTIVITY(issue.id) : null,
@ -30,18 +32,11 @@ export const PeekOverviewIssueActivity: React.FC<Props> = ({ workspaceSlug, issu
: null : null
); );
const handleCommentUpdate = async (comment: IIssueComment) => { const handleCommentUpdate = async (commentId: string, data: Partial<IIssueComment>) => {
if (!workspaceSlug || !issue) return; if (!workspaceSlug || !issue) return;
await issuesService await issuesService
.patchIssueComment( .patchIssueComment(workspaceSlug as string, issue.project, issue.id, commentId, data, user)
workspaceSlug as string,
issue.project,
issue.id,
comment.id,
comment,
user
)
.then(() => mutateIssueActivity()); .then(() => mutateIssueActivity());
}; };
@ -80,9 +75,13 @@ export const PeekOverviewIssueActivity: React.FC<Props> = ({ workspaceSlug, issu
activity={issueActivity} activity={issueActivity}
handleCommentUpdate={handleCommentUpdate} handleCommentUpdate={handleCommentUpdate}
handleCommentDelete={handleCommentDelete} handleCommentDelete={handleCommentDelete}
showAccessSpecifier={projectDetails && projectDetails.is_deployed}
/> />
<div className="mt-4"> <div className="mt-4">
<AddComment onSubmit={handleAddComment} /> <AddComment
onSubmit={handleAddComment}
showAccessSpecifier={projectDetails && projectDetails.is_deployed}
/>
</div> </div>
</div> </div>
</div> </div>

View File

@ -2,7 +2,7 @@
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// headless ui // headless ui
import { Disclosure } from "@headlessui/react"; import { Disclosure } from "@headlessui/react";
import { getStateGroupIcon } from "components/icons"; import { StateGroupIcon } from "components/icons";
// hooks // hooks
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
import useUser from "hooks/use-user"; import useUser from "hooks/use-user";
@ -19,7 +19,7 @@ import { CustomDatePicker, Icon } from "components/ui";
// helpers // helpers
import { copyTextToClipboard } from "helpers/string.helper"; import { copyTextToClipboard } from "helpers/string.helper";
// types // types
import { IIssue } from "types"; import { IIssue, TIssuePriorities } from "types";
type Props = { type Props = {
handleDeleteIssue: () => void; handleDeleteIssue: () => void;
@ -66,7 +66,10 @@ export const PeekOverviewIssueProperties: React.FC<Props> = ({
{mode === "full" && ( {mode === "full" && (
<div className="flex justify-between gap-2 pb-3"> <div className="flex justify-between gap-2 pb-3">
<h6 className="flex items-center gap-2 font-medium"> <h6 className="flex items-center gap-2 font-medium">
{getStateGroupIcon(issue.state_detail.group, "16", "16", issue.state_detail.color)} <StateGroupIcon
stateGroup={issue.state_detail.group}
color={issue.state_detail.color}
/>
{issue.project_detail.identifier}-{issue.sequence_id} {issue.project_detail.identifier}-{issue.sequence_id}
</h6> </h6>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@ -114,7 +117,7 @@ export const PeekOverviewIssueProperties: React.FC<Props> = ({
<div className="w-3/4"> <div className="w-3/4">
<SidebarPrioritySelect <SidebarPrioritySelect
value={issue.priority} value={issue.priority}
onChange={(val: string) => handleUpdateIssue({ priority: val })} onChange={(val) => handleUpdateIssue({ priority: val })}
disabled={readOnly} disabled={readOnly}
/> />
</div> </div>

View File

@ -3,12 +3,14 @@ import React from "react";
// ui // ui
import { CustomSelect } from "components/ui"; import { CustomSelect } from "components/ui";
// icons // icons
import { getPriorityIcon } from "components/icons/priority-icon"; import { PriorityIcon } from "components/icons/priority-icon";
// types
import { TIssuePriorities } from "types";
// constants // constants
import { PRIORITIES } from "constants/project"; import { PRIORITIES } from "constants/project";
type Props = { type Props = {
value: string | null; value: TIssuePriorities;
onChange: (value: string) => void; onChange: (value: string) => void;
}; };
@ -18,7 +20,10 @@ export const IssuePrioritySelect: React.FC<Props> = ({ value, onChange }) => (
label={ label={
<div className="flex items-center justify-center gap-2 text-xs"> <div className="flex items-center justify-center gap-2 text-xs">
<span className="flex items-center"> <span className="flex items-center">
{getPriorityIcon(value, `text-xs ${value ? "" : "text-custom-text-200"}`)} <PriorityIcon
priority={value}
className={`text-xs ${value ? "" : "text-custom-text-200"}`}
/>
</span> </span>
<span className={`${value ? "" : "text-custom-text-200"} capitalize`}> <span className={`${value ? "" : "text-custom-text-200"} capitalize`}>
{value ?? "Priority"} {value ?? "Priority"}
@ -32,7 +37,9 @@ export const IssuePrioritySelect: React.FC<Props> = ({ value, onChange }) => (
<CustomSelect.Option key={priority} value={priority}> <CustomSelect.Option key={priority} value={priority}>
<div className="flex w-full justify-between gap-2 rounded"> <div className="flex w-full justify-between gap-2 rounded">
<div className="flex items-center justify-start gap-2"> <div className="flex items-center justify-start gap-2">
<span>{getPriorityIcon(priority)}</span> <span>
<PriorityIcon priority={priority} />
</span>
<span className="capitalize">{priority ?? "None"}</span> <span className="capitalize">{priority ?? "None"}</span>
</div> </div>
</div> </div>

View File

@ -10,7 +10,7 @@ import stateService from "services/state.service";
import { CustomSearchSelect } from "components/ui"; import { CustomSearchSelect } from "components/ui";
// icons // icons
import { PlusIcon, Squares2X2Icon } from "@heroicons/react/24/outline"; import { PlusIcon, Squares2X2Icon } from "@heroicons/react/24/outline";
import { getStateGroupIcon } from "components/icons"; import { StateGroupIcon } from "components/icons";
// helpers // helpers
import { getStatesList } from "helpers/state.helper"; import { getStatesList } from "helpers/state.helper";
// fetch keys // fetch keys
@ -41,7 +41,7 @@ export const IssueStateSelect: React.FC<Props> = ({ setIsOpen, value, onChange,
query: state.name, query: state.name,
content: ( content: (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{getStateGroupIcon(state.group, "16", "16", state.color)} <StateGroupIcon stateGroup={state.group} color={state.color} />
{state.name} {state.name}
</div> </div>
), ),
@ -58,9 +58,12 @@ export const IssueStateSelect: React.FC<Props> = ({ setIsOpen, value, onChange,
label={ label={
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{selectedOption ? ( {selectedOption ? (
getStateGroupIcon(selectedOption.group, "16", "16", selectedOption.color) <StateGroupIcon stateGroup={selectedOption.group} color={selectedOption.color} />
) : currentDefaultState ? ( ) : currentDefaultState ? (
getStateGroupIcon(currentDefaultState.group, "16", "16", currentDefaultState.color) <StateGroupIcon
stateGroup={currentDefaultState.group}
color={currentDefaultState.color}
/>
) : ( ) : (
<Squares2X2Icon className="h-3.5 w-3.5 text-custom-text-200" /> <Squares2X2Icon className="h-3.5 w-3.5 text-custom-text-200" />
)} )}

View File

@ -3,13 +3,15 @@ import React from "react";
// ui // ui
import { CustomSelect } from "components/ui"; import { CustomSelect } from "components/ui";
// icons // icons
import { getPriorityIcon } from "components/icons/priority-icon"; import { PriorityIcon } from "components/icons/priority-icon";
// types
import { TIssuePriorities } from "types";
// constants // constants
import { PRIORITIES } from "constants/project"; import { PRIORITIES } from "constants/project";
type Props = { type Props = {
value: string | null; value: TIssuePriorities;
onChange: (val: string) => void; onChange: (val: TIssuePriorities) => void;
disabled?: boolean; disabled?: boolean;
}; };
@ -31,7 +33,7 @@ export const SidebarPrioritySelect: React.FC<Props> = ({ value, onChange, disabl
}`} }`}
> >
<span className="grid place-items-center -my-1"> <span className="grid place-items-center -my-1">
{getPriorityIcon(value ?? "None", "!text-sm")} <PriorityIcon priority={value} className="!text-sm" />
</span> </span>
<span>{value ?? "None"}</span> <span>{value ?? "None"}</span>
</button> </button>
@ -44,7 +46,7 @@ export const SidebarPrioritySelect: React.FC<Props> = ({ value, onChange, disabl
{PRIORITIES.map((option) => ( {PRIORITIES.map((option) => (
<CustomSelect.Option key={option} value={option} className="capitalize"> <CustomSelect.Option key={option} value={option} className="capitalize">
<> <>
{getPriorityIcon(option, "text-sm")} <PriorityIcon priority={option} className="text-sm" />
{option ?? "None"} {option ?? "None"}
</> </>
</CustomSelect.Option> </CustomSelect.Option>

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