Merge branch 'develop' of github.com:makeplane/plane into dev/private_bucket_for_attachments

This commit is contained in:
pablohashescobar 2023-09-25 12:29:59 +05:30
commit ad9ff684e9
43 changed files with 1219 additions and 267 deletions

View File

@ -102,48 +102,84 @@ class CycleViewSet(BaseViewSet):
.select_related("workspace") .select_related("workspace")
.select_related("owned_by") .select_related("owned_by")
.annotate(is_favorite=Exists(subquery)) .annotate(is_favorite=Exists(subquery))
.annotate(total_issues=Count("issue_cycle")) .annotate(
total_issues=Count(
"issue_cycle",
filter=Q(
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
)
)
.annotate( .annotate(
completed_issues=Count( completed_issues=Count(
"issue_cycle__issue__state__group", "issue_cycle__issue__state__group",
filter=Q(issue_cycle__issue__state__group="completed"), filter=Q(
issue_cycle__issue__state__group="completed",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
) )
) )
.annotate( .annotate(
cancelled_issues=Count( cancelled_issues=Count(
"issue_cycle__issue__state__group", "issue_cycle__issue__state__group",
filter=Q(issue_cycle__issue__state__group="cancelled"), filter=Q(
issue_cycle__issue__state__group="cancelled",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
) )
) )
.annotate( .annotate(
started_issues=Count( started_issues=Count(
"issue_cycle__issue__state__group", "issue_cycle__issue__state__group",
filter=Q(issue_cycle__issue__state__group="started"), filter=Q(
issue_cycle__issue__state__group="started",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
) )
) )
.annotate( .annotate(
unstarted_issues=Count( unstarted_issues=Count(
"issue_cycle__issue__state__group", "issue_cycle__issue__state__group",
filter=Q(issue_cycle__issue__state__group="unstarted"), filter=Q(
issue_cycle__issue__state__group="unstarted",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
) )
) )
.annotate( .annotate(
backlog_issues=Count( backlog_issues=Count(
"issue_cycle__issue__state__group", "issue_cycle__issue__state__group",
filter=Q(issue_cycle__issue__state__group="backlog"), filter=Q(
issue_cycle__issue__state__group="backlog",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
) )
) )
.annotate(total_estimates=Sum("issue_cycle__issue__estimate_point")) .annotate(total_estimates=Sum("issue_cycle__issue__estimate_point"))
.annotate( .annotate(
completed_estimates=Sum( completed_estimates=Sum(
"issue_cycle__issue__estimate_point", "issue_cycle__issue__estimate_point",
filter=Q(issue_cycle__issue__state__group="completed"), filter=Q(
issue_cycle__issue__state__group="completed",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
) )
) )
.annotate( .annotate(
started_estimates=Sum( started_estimates=Sum(
"issue_cycle__issue__estimate_point", "issue_cycle__issue__estimate_point",
filter=Q(issue_cycle__issue__state__group="started"), filter=Q(
issue_cycle__issue__state__group="started",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
) )
) )
.prefetch_related( .prefetch_related(
@ -196,17 +232,30 @@ class CycleViewSet(BaseViewSet):
.annotate(assignee_id=F("assignees__id")) .annotate(assignee_id=F("assignees__id"))
.annotate(avatar=F("assignees__avatar")) .annotate(avatar=F("assignees__avatar"))
.values("display_name", "assignee_id", "avatar") .values("display_name", "assignee_id", "avatar")
.annotate(total_issues=Count("assignee_id")) .annotate(
total_issues=Count(
"assignee_id",
filter=Q(archived_at__isnull=True, is_draft=False),
),
)
.annotate( .annotate(
completed_issues=Count( completed_issues=Count(
"assignee_id", "assignee_id",
filter=Q(completed_at__isnull=False), filter=Q(
completed_at__isnull=False,
archived_at__isnull=True,
is_draft=False,
),
) )
) )
.annotate( .annotate(
pending_issues=Count( pending_issues=Count(
"assignee_id", "assignee_id",
filter=Q(completed_at__isnull=True), filter=Q(
completed_at__isnull=True,
archived_at__isnull=True,
is_draft=False,
),
) )
) )
.order_by("display_name") .order_by("display_name")
@ -222,17 +271,30 @@ class CycleViewSet(BaseViewSet):
.annotate(color=F("labels__color")) .annotate(color=F("labels__color"))
.annotate(label_id=F("labels__id")) .annotate(label_id=F("labels__id"))
.values("label_name", "color", "label_id") .values("label_name", "color", "label_id")
.annotate(total_issues=Count("label_id")) .annotate(
total_issues=Count(
"label_id",
filter=Q(archived_at__isnull=True, is_draft=False),
)
)
.annotate( .annotate(
completed_issues=Count( completed_issues=Count(
"label_id", "label_id",
filter=Q(completed_at__isnull=False), filter=Q(
completed_at__isnull=False,
archived_at__isnull=True,
is_draft=False,
),
) )
) )
.annotate( .annotate(
pending_issues=Count( pending_issues=Count(
"label_id", "label_id",
filter=Q(completed_at__isnull=True), filter=Q(
completed_at__isnull=True,
archived_at__isnull=True,
is_draft=False,
),
) )
) )
.order_by("label_name") .order_by("label_name")
@ -385,17 +447,30 @@ class CycleViewSet(BaseViewSet):
.values( .values(
"first_name", "last_name", "assignee_id", "avatar", "display_name" "first_name", "last_name", "assignee_id", "avatar", "display_name"
) )
.annotate(total_issues=Count("assignee_id")) .annotate(
total_issues=Count(
"assignee_id",
filter=Q(archived_at__isnull=True, is_draft=False),
),
)
.annotate( .annotate(
completed_issues=Count( completed_issues=Count(
"assignee_id", "assignee_id",
filter=Q(completed_at__isnull=False), filter=Q(
completed_at__isnull=False,
archived_at__isnull=True,
is_draft=False,
),
) )
) )
.annotate( .annotate(
pending_issues=Count( pending_issues=Count(
"assignee_id", "assignee_id",
filter=Q(completed_at__isnull=True), filter=Q(
completed_at__isnull=True,
archived_at__isnull=True,
is_draft=False,
),
) )
) )
.order_by("first_name", "last_name") .order_by("first_name", "last_name")
@ -412,17 +487,30 @@ class CycleViewSet(BaseViewSet):
.annotate(color=F("labels__color")) .annotate(color=F("labels__color"))
.annotate(label_id=F("labels__id")) .annotate(label_id=F("labels__id"))
.values("label_name", "color", "label_id") .values("label_name", "color", "label_id")
.annotate(total_issues=Count("label_id")) .annotate(
total_issues=Count(
"label_id",
filter=Q(archived_at__isnull=True, is_draft=False),
),
)
.annotate( .annotate(
completed_issues=Count( completed_issues=Count(
"label_id", "label_id",
filter=Q(completed_at__isnull=False), filter=Q(
completed_at__isnull=False,
archived_at__isnull=True,
is_draft=False,
),
) )
) )
.annotate( .annotate(
pending_issues=Count( pending_issues=Count(
"label_id", "label_id",
filter=Q(completed_at__isnull=True), filter=Q(
completed_at__isnull=True,
archived_at__isnull=True,
is_draft=False,
),
) )
) )
.order_by("label_name") .order_by("label_name")

View File

@ -384,7 +384,7 @@ class BulkImportIssuesEndpoint(BaseAPIView):
sort_order=largest_sort_order, sort_order=largest_sort_order,
start_date=issue_data.get("start_date", None), start_date=issue_data.get("start_date", None),
target_date=issue_data.get("target_date", None), target_date=issue_data.get("target_date", None),
priority=issue_data.get("priority", None), priority=issue_data.get("priority", "none"),
created_by=request.user, created_by=request.user,
) )
) )

View File

@ -173,12 +173,12 @@ class InboxIssueViewSet(BaseViewSet):
) )
# Check for valid priority # Check for valid priority
if not request.data.get("issue", {}).get("priority", None) in [ if not request.data.get("issue", {}).get("priority", "none") in [
"low", "low",
"medium", "medium",
"high", "high",
"urgent", "urgent",
None, "none",
]: ]:
return Response( return Response(
{"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST {"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST
@ -480,12 +480,12 @@ class InboxIssuePublicViewSet(BaseViewSet):
) )
# Check for valid priority # Check for valid priority
if not request.data.get("issue", {}).get("priority", None) in [ if not request.data.get("issue", {}).get("priority", "none") in [
"low", "low",
"medium", "medium",
"high", "high",
"urgent", "urgent",
None, "none",
]: ]:
return Response( return Response(
{"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST {"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST

View File

@ -2439,6 +2439,7 @@ class IssueDraftViewSet(BaseViewSet):
current_instance=json.dumps( current_instance=json.dumps(
IssueSerializer(current_instance).data, cls=DjangoJSONEncoder IssueSerializer(current_instance).data, cls=DjangoJSONEncoder
), ),
epoch=int(timezone.now().timestamp())
) )
return super().perform_destroy(instance) return super().perform_destroy(instance)

View File

@ -40,6 +40,7 @@ from plane.utils.grouper import group_results
from plane.utils.issue_filters import issue_filters from plane.utils.issue_filters import issue_filters
from plane.utils.analytics_plot import burndown_plot from plane.utils.analytics_plot import burndown_plot
class ModuleViewSet(BaseViewSet): class ModuleViewSet(BaseViewSet):
model = Module model = Module
permission_classes = [ permission_classes = [
@ -78,35 +79,63 @@ class ModuleViewSet(BaseViewSet):
queryset=ModuleLink.objects.select_related("module", "created_by"), queryset=ModuleLink.objects.select_related("module", "created_by"),
) )
) )
.annotate(total_issues=Count("issue_module")) .annotate(
total_issues=Count(
"issue_module",
filter=Q(
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
),
)
.annotate( .annotate(
completed_issues=Count( completed_issues=Count(
"issue_module__issue__state__group", "issue_module__issue__state__group",
filter=Q(issue_module__issue__state__group="completed"), filter=Q(
issue_module__issue__state__group="completed",
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
) )
) )
.annotate( .annotate(
cancelled_issues=Count( cancelled_issues=Count(
"issue_module__issue__state__group", "issue_module__issue__state__group",
filter=Q(issue_module__issue__state__group="cancelled"), filter=Q(
issue_module__issue__state__group="cancelled",
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
) )
) )
.annotate( .annotate(
started_issues=Count( started_issues=Count(
"issue_module__issue__state__group", "issue_module__issue__state__group",
filter=Q(issue_module__issue__state__group="started"), filter=Q(
issue_module__issue__state__group="started",
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
) )
) )
.annotate( .annotate(
unstarted_issues=Count( unstarted_issues=Count(
"issue_module__issue__state__group", "issue_module__issue__state__group",
filter=Q(issue_module__issue__state__group="unstarted"), filter=Q(
issue_module__issue__state__group="unstarted",
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
) )
) )
.annotate( .annotate(
backlog_issues=Count( backlog_issues=Count(
"issue_module__issue__state__group", "issue_module__issue__state__group",
filter=Q(issue_module__issue__state__group="backlog"), filter=Q(
issue_module__issue__state__group="backlog",
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
),
) )
) )
.order_by(order_by, "name") .order_by(order_by, "name")
@ -179,18 +208,36 @@ class ModuleViewSet(BaseViewSet):
.annotate(assignee_id=F("assignees__id")) .annotate(assignee_id=F("assignees__id"))
.annotate(display_name=F("assignees__display_name")) .annotate(display_name=F("assignees__display_name"))
.annotate(avatar=F("assignees__avatar")) .annotate(avatar=F("assignees__avatar"))
.values("first_name", "last_name", "assignee_id", "avatar", "display_name") .values(
.annotate(total_issues=Count("assignee_id")) "first_name", "last_name", "assignee_id", "avatar", "display_name"
)
.annotate(
total_issues=Count(
"assignee_id",
filter=Q(
archived_at__isnull=True,
is_draft=False,
),
)
)
.annotate( .annotate(
completed_issues=Count( completed_issues=Count(
"assignee_id", "assignee_id",
filter=Q(completed_at__isnull=False), filter=Q(
completed_at__isnull=False,
archived_at__isnull=True,
is_draft=False,
),
) )
) )
.annotate( .annotate(
pending_issues=Count( pending_issues=Count(
"assignee_id", "assignee_id",
filter=Q(completed_at__isnull=True), filter=Q(
completed_at__isnull=True,
archived_at__isnull=True,
is_draft=False,
),
) )
) )
.order_by("first_name", "last_name") .order_by("first_name", "last_name")
@ -206,17 +253,33 @@ class ModuleViewSet(BaseViewSet):
.annotate(color=F("labels__color")) .annotate(color=F("labels__color"))
.annotate(label_id=F("labels__id")) .annotate(label_id=F("labels__id"))
.values("label_name", "color", "label_id") .values("label_name", "color", "label_id")
.annotate(total_issues=Count("label_id")) .annotate(
total_issues=Count(
"label_id",
filter=Q(
archived_at__isnull=True,
is_draft=False,
),
),
)
.annotate( .annotate(
completed_issues=Count( completed_issues=Count(
"label_id", "label_id",
filter=Q(completed_at__isnull=False), filter=Q(
completed_at__isnull=False,
archived_at__isnull=True,
is_draft=False,
),
) )
) )
.annotate( .annotate(
pending_issues=Count( pending_issues=Count(
"label_id", "label_id",
filter=Q(completed_at__isnull=True), filter=Q(
completed_at__isnull=True,
archived_at__isnull=True,
is_draft=False,
),
) )
) )
.order_by("label_name") .order_by("label_name")
@ -494,7 +557,6 @@ class ModuleLinkViewSet(BaseViewSet):
class ModuleFavoriteViewSet(BaseViewSet): class ModuleFavoriteViewSet(BaseViewSet):
serializer_class = ModuleFavoriteSerializer serializer_class = ModuleFavoriteSerializer
model = ModuleFavorite model = ModuleFavorite

View File

@ -1239,13 +1239,21 @@ class WorkspaceUserProfileEndpoint(BaseAPIView):
.annotate( .annotate(
created_issues=Count( created_issues=Count(
"project_issue", "project_issue",
filter=Q(project_issue__created_by_id=user_id), filter=Q(
project_issue__created_by_id=user_id,
project_issue__archived_at__isnull=True,
project_issue__is_draft=False,
),
) )
) )
.annotate( .annotate(
assigned_issues=Count( assigned_issues=Count(
"project_issue", "project_issue",
filter=Q(project_issue__assignees__in=[user_id]), filter=Q(
project_issue__assignees__in=[user_id],
project_issue__archived_at__isnull=True,
project_issue__is_draft=False,
),
) )
) )
.annotate( .annotate(
@ -1254,6 +1262,8 @@ class WorkspaceUserProfileEndpoint(BaseAPIView):
filter=Q( filter=Q(
project_issue__completed_at__isnull=False, project_issue__completed_at__isnull=False,
project_issue__assignees__in=[user_id], project_issue__assignees__in=[user_id],
project_issue__archived_at__isnull=True,
project_issue__is_draft=False,
), ),
) )
) )
@ -1267,6 +1277,8 @@ class WorkspaceUserProfileEndpoint(BaseAPIView):
"started", "started",
], ],
project_issue__assignees__in=[user_id], project_issue__assignees__in=[user_id],
project_issue__archived_at__isnull=True,
project_issue__is_draft=False,
), ),
) )
) )
@ -1317,6 +1329,11 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
def get(self, request, slug, user_id): def get(self, request, slug, user_id):
try: try:
filters = issue_filters(request.query_params, "GET") filters = issue_filters(request.query_params, "GET")
# Custom ordering for priority and state
priority_order = ["urgent", "high", "medium", "low", "none"]
state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
order_by_param = request.GET.get("order_by", "-created_at") order_by_param = request.GET.get("order_by", "-created_at")
issue_queryset = ( issue_queryset = (
Issue.issue_objects.filter( Issue.issue_objects.filter(

View File

@ -121,22 +121,6 @@ def track_priority(
epoch epoch
): ):
if current_instance.get("priority") != requested_data.get("priority"): if current_instance.get("priority") != requested_data.get("priority"):
if requested_data.get("priority") == None:
issue_activities.append(
IssueActivity(
issue_id=issue_id,
actor=actor,
verb="updated",
old_value=current_instance.get("priority"),
new_value=None,
field="priority",
project=project,
workspace=project.workspace,
comment=f"updated the priority to None",
epoch=epoch,
)
)
else:
issue_activities.append( issue_activities.append(
IssueActivity( IssueActivity(
issue_id=issue_id, issue_id=issue_id,

View File

@ -0,0 +1,27 @@
# Generated by Django 4.2.3 on 2023-09-21 07:58
from django.db import migrations
def update_priority_history(apps, schema_editor):
IssueActivity = apps.get_model("db", "IssueActivity")
updated_issue_activity = []
for obj in IssueActivity.objects.all():
if obj.field == "priority":
obj.new_value = obj.new_value or "none"
obj.old_value = obj.old_value or "none"
updated_issue_activity.append(obj)
IssueActivity.objects.bulk_update(
updated_issue_activity, ["new_value", "old_value"], batch_size=100
)
class Migration(migrations.Migration):
dependencies = [
("db", "0046_auto_20230919_1421"),
]
operations = [
migrations.RunPython(update_priority_history),
]

View File

@ -74,10 +74,10 @@ def build_graph_plot(queryset, x_axis, y_axis, segment=None):
sorted_data = grouped_data sorted_data = grouped_data
if temp_axis == "priority": if temp_axis == "priority":
order = ["low", "medium", "high", "urgent", "None"] order = ["low", "medium", "high", "urgent", "none"]
sorted_data = {key: grouped_data[key] for key in order if key in grouped_data} sorted_data = {key: grouped_data[key] for key in order if key in grouped_data}
else: else:
sorted_data = dict(sorted(grouped_data.items(), key=lambda x: (x[0] == "None", x[0]))) sorted_data = dict(sorted(grouped_data.items(), key=lambda x: (x[0] == "none", x[0])))
return sorted_data return sorted_data

View File

@ -40,9 +40,6 @@ def filter_priority(params, filter, method):
priorities = params.get("priority").split(",") priorities = params.get("priority").split(",")
if len(priorities) and "" not in priorities: if len(priorities) and "" not in priorities:
filter["priority__in"] = priorities filter["priority__in"] = priorities
else:
if params.get("priority", None) and len(params.get("priority")):
filter["priority__in"] = params.get("priority")
return filter return filter

View File

@ -15,17 +15,19 @@ export const AnalyticsScope: React.FC<Props> = ({ defaultAnalytics }) => (
<div className="divide-y divide-custom-border-200"> <div className="divide-y divide-custom-border-200">
<div> <div>
<h6 className="px-3 text-base font-medium">Pending issues</h6> <h6 className="px-3 text-base font-medium">Pending issues</h6>
{defaultAnalytics.pending_issue_user.length > 0 ? ( {defaultAnalytics.pending_issue_user && defaultAnalytics.pending_issue_user.length > 0 ? (
<BarGraph <BarGraph
data={defaultAnalytics.pending_issue_user} data={defaultAnalytics.pending_issue_user}
indexBy="assignees__display_name" indexBy="assignees__id"
keys={["count"]} keys={["count"]}
height="250px" height="250px"
colors={() => `#f97316`} colors={() => `#f97316`}
customYAxisTickValues={defaultAnalytics.pending_issue_user.map((d) => d.count)} customYAxisTickValues={defaultAnalytics.pending_issue_user.map((d) =>
d.count > 0 ? d.count : 50
)}
tooltip={(datum) => { tooltip={(datum) => {
const assignee = defaultAnalytics.pending_issue_user.find( const assignee = defaultAnalytics.pending_issue_user.find(
(a) => a.assignees__display_name === `${datum.indexValue}` (a) => a.assignees__id === `${datum.indexValue}`
); );
return ( return (
@ -39,10 +41,9 @@ export const AnalyticsScope: React.FC<Props> = ({ defaultAnalytics }) => (
}} }}
axisBottom={{ axisBottom={{
renderTick: (datum) => { renderTick: (datum) => {
const avatar = const assignee = defaultAnalytics.pending_issue_user[datum.tickIndex] ?? "";
defaultAnalytics.pending_issue_user[datum.tickIndex]?.assignees__avatar ?? "";
if (avatar && avatar !== "") if (assignee && assignee?.assignees__avatar && assignee?.assignees__avatar !== "")
return ( return (
<g transform={`translate(${datum.x},${datum.y})`}> <g transform={`translate(${datum.x},${datum.y})`}>
<image <image
@ -50,7 +51,7 @@ export const AnalyticsScope: React.FC<Props> = ({ defaultAnalytics }) => (
y={10} y={10}
width={16} width={16}
height={16} height={16}
xlinkHref={avatar} xlinkHref={assignee?.assignees__avatar}
style={{ clipPath: "circle(50%)" }} style={{ clipPath: "circle(50%)" }}
/> />
</g> </g>
@ -60,7 +61,7 @@ export const AnalyticsScope: React.FC<Props> = ({ defaultAnalytics }) => (
<g transform={`translate(${datum.x},${datum.y})`}> <g transform={`translate(${datum.x},${datum.y})`}>
<circle cy={18} r={8} fill="#374151" /> <circle cy={18} r={8} fill="#374151" />
<text x={0} y={21} textAnchor="middle" fontSize={9} fill="#ffffff"> <text x={0} y={21} textAnchor="middle" fontSize={9} fill="#ffffff">
{datum.value ? `${datum.value}`.toUpperCase()[0] : "?"} {datum.value ? `${assignee.assignees__display_name}`.toUpperCase()[0] : "?"}
</text> </text>
</g> </g>
); );

View File

@ -2,3 +2,4 @@ export * from "./all-boards";
export * from "./board-header"; export * from "./board-header";
export * from "./single-board"; export * from "./single-board";
export * from "./single-issue"; export * from "./single-issue";
export * from "./inline-create-issue-form";

View File

@ -0,0 +1,62 @@
import { useEffect } from "react";
// react hook form
import { useFormContext } from "react-hook-form";
// components
import { InlineCreateIssueFormWrapper } from "components/core";
// hooks
import useProjectDetails from "hooks/use-project-details";
// types
import { IIssue } from "types";
type Props = {
isOpen: boolean;
handleClose: () => void;
onSuccess?: (data: IIssue) => Promise<void> | void;
prePopulatedData?: Partial<IIssue>;
};
const InlineInput = () => {
const { projectDetails } = useProjectDetails();
const { register, setFocus } = useFormContext();
useEffect(() => {
setFocus("name");
}, [setFocus]);
return (
<div>
<h4 className="text-sm font-medium leading-5 text-custom-text-300">
{projectDetails?.identifier ?? "..."}
</h4>
<input
autoComplete="off"
placeholder="Issue Title"
{...register("name", {
required: "Issue title is required.",
})}
className="w-full px-2 pl-0 py-1.5 rounded-md bg-transparent text-sm font-medium leading-5 text-custom-text-200 outline-none"
/>
</div>
);
};
export const BoardInlineCreateIssueForm: React.FC<Props> = (props) => (
<>
<InlineCreateIssueFormWrapper
className="flex flex-col justify-between gap-1.5 group/card relative select-none px-3.5 py-3 h-[118px] mb-3 rounded bg-custom-background-100 shadow"
{...props}
>
<InlineInput />
</InlineCreateIssueFormWrapper>
{props.isOpen && (
<p className="text-xs ml-3 italic text-custom-text-200">
Press {"'"}Enter{"'"} to add another issue
</p>
)}
</>
);

View File

@ -6,7 +6,7 @@ import { useRouter } from "next/router";
import StrictModeDroppable from "components/dnd/StrictModeDroppable"; import StrictModeDroppable from "components/dnd/StrictModeDroppable";
import { Draggable } from "react-beautiful-dnd"; import { Draggable } from "react-beautiful-dnd";
// components // components
import { BoardHeader, SingleBoardIssue } from "components/core"; import { BoardHeader, SingleBoardIssue, BoardInlineCreateIssueForm } from "components/core";
// ui // ui
import { CustomMenu } from "components/ui"; import { CustomMenu } from "components/ui";
// icons // icons
@ -34,7 +34,8 @@ type Props = {
viewProps: IIssueViewProps; viewProps: IIssueViewProps;
}; };
export const SingleBoard: React.FC<Props> = ({ export const SingleBoard: React.FC<Props> = (props) => {
const {
addIssueToGroup, addIssueToGroup,
currentState, currentState,
groupTitle, groupTitle,
@ -50,10 +51,13 @@ export const SingleBoard: React.FC<Props> = ({
user, user,
userAuth, userAuth,
viewProps, viewProps,
}) => { } = props;
// collapse/expand // collapse/expand
const [isCollapsed, setIsCollapsed] = useState(true); const [isCollapsed, setIsCollapsed] = useState(true);
const [isInlineCreateIssueFormOpen, setIsInlineCreateIssueFormOpen] = useState(false);
const { displayFilters, groupedIssues } = viewProps; const { displayFilters, groupedIssues } = viewProps;
const router = useRouter(); const router = useRouter();
@ -67,6 +71,24 @@ export const SingleBoard: React.FC<Props> = ({
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disableUserActions; const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disableUserActions;
const onCreateClick = () => {
setIsInlineCreateIssueFormOpen(true);
const boardListElement = document.getElementById(`board-list-${groupTitle}`);
// timeout is needed because the animation
// takes time to complete & we can scroll only after that
const timeoutId = setTimeout(() => {
if (boardListElement)
boardListElement.scrollBy({
top: boardListElement.scrollHeight,
left: 0,
behavior: "smooth",
});
clearTimeout(timeoutId);
}, 10);
};
return ( return (
<div className={`flex-shrink-0 ${!isCollapsed ? "" : "flex h-full flex-col w-96"}`}> <div className={`flex-shrink-0 ${!isCollapsed ? "" : "flex h-full flex-col w-96"}`}>
<BoardHeader <BoardHeader
@ -115,6 +137,7 @@ export const SingleBoard: React.FC<Props> = ({
</> </>
)} )}
<div <div
id={`board-list-${groupTitle}`}
className={`pt-3 ${ className={`pt-3 ${
hasMinimumNumberOfCards ? "overflow-hidden overflow-y-scroll" : "" hasMinimumNumberOfCards ? "overflow-hidden overflow-y-scroll" : ""
} `} } `}
@ -134,6 +157,7 @@ export const SingleBoard: React.FC<Props> = ({
type={type} type={type}
index={index} index={index}
issue={issue} issue={issue}
projectId={issue.project_detail.id}
groupTitle={groupTitle} groupTitle={groupTitle}
editIssue={() => handleIssueAction(issue, "edit")} editIssue={() => handleIssueAction(issue, "edit")}
makeIssueCopy={() => handleIssueAction(issue, "copy")} makeIssueCopy={() => handleIssueAction(issue, "copy")}
@ -169,6 +193,19 @@ export const SingleBoard: React.FC<Props> = ({
> >
<>{provided.placeholder}</> <>{provided.placeholder}</>
</span> </span>
<BoardInlineCreateIssueForm
isOpen={isInlineCreateIssueFormOpen}
handleClose={() => setIsInlineCreateIssueFormOpen(false)}
prePopulatedData={{
...(cycleId && { cycle: cycleId.toString() }),
...(moduleId && { module: moduleId.toString() }),
[displayFilters?.group_by! === "labels"
? "labels_list"
: displayFilters?.group_by!]:
displayFilters?.group_by === "labels" ? [groupTitle] : groupTitle,
}}
/>
</div> </div>
{displayFilters?.group_by !== "created_by" && ( {displayFilters?.group_by !== "created_by" && (
<div> <div>
@ -177,7 +214,7 @@ export const SingleBoard: React.FC<Props> = ({
<button <button
type="button" type="button"
className="flex items-center gap-2 font-medium text-custom-primary outline-none p-1" className="flex items-center gap-2 font-medium text-custom-primary outline-none p-1"
onClick={addIssueToGroup} onClick={() => onCreateClick()}
> >
<PlusIcon className="h-4 w-4" /> <PlusIcon className="h-4 w-4" />
Add Issue Add Issue
@ -197,7 +234,7 @@ export const SingleBoard: React.FC<Props> = ({
position="left" position="left"
noBorder noBorder
> >
<CustomMenu.MenuItem onClick={addIssueToGroup}> <CustomMenu.MenuItem onClick={() => onCreateClick()}>
Create new Create new
</CustomMenu.MenuItem> </CustomMenu.MenuItem>
{openIssuesListModal && ( {openIssuesListModal && (

View File

@ -56,6 +56,7 @@ type Props = {
provided: DraggableProvided; provided: DraggableProvided;
snapshot: DraggableStateSnapshot; snapshot: DraggableStateSnapshot;
issue: IIssue; issue: IIssue;
projectId: string;
groupTitle?: string; groupTitle?: string;
index: number; index: number;
editIssue: () => void; editIssue: () => void;
@ -77,6 +78,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
provided, provided,
snapshot, snapshot,
issue, issue,
projectId,
index, index,
editIssue, editIssue,
makeIssueCopy, makeIssueCopy,
@ -104,7 +106,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
const { displayFilters, properties, mutateIssues } = viewProps; const { displayFilters, properties, mutateIssues } = viewProps;
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId } = router.query; const { workspaceSlug, cycleId, moduleId } = router.query;
const isDraftIssue = router.pathname.includes("draft-issues"); const isDraftIssue = router.pathname.includes("draft-issues");
@ -452,6 +454,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
<StateSelect <StateSelect
value={issue.state_detail} value={issue.state_detail}
onChange={handleStateChange} onChange={handleStateChange}
projectId={projectId}
hideDropdownArrow hideDropdownArrow
disabled={isNotAllowed} disabled={isNotAllowed}
/> />
@ -479,6 +482,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
{properties.labels && issue.labels.length > 0 && ( {properties.labels && issue.labels.length > 0 && (
<LabelSelect <LabelSelect
value={issue.labels} value={issue.labels}
projectId={projectId}
onChange={handleLabelChange} onChange={handleLabelChange}
labelsDetails={issue.label_details} labelsDetails={issue.label_details}
hideDropdownArrow hideDropdownArrow
@ -489,6 +493,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
{properties.assignee && ( {properties.assignee && (
<MembersSelect <MembersSelect
value={issue.assignees} value={issue.assignees}
projectId={projectId}
onChange={handleAssigneeChange} onChange={handleAssigneeChange}
membersDetails={issue.assignee_details} membersDetails={issue.assignee_details}
hideDropdownArrow hideDropdownArrow

View File

@ -2,3 +2,4 @@ export * from "./calendar-header";
export * from "./calendar"; export * from "./calendar";
export * from "./single-date"; export * from "./single-date";
export * from "./single-issue"; export * from "./single-issue";
export * from "./inline-create-issue-form";

View File

@ -0,0 +1,91 @@
import { useEffect, useRef, useState } from "react";
// react hook form
import { useFormContext } from "react-hook-form";
import { InlineCreateIssueFormWrapper } from "components/core";
// hooks
import useProjectDetails from "hooks/use-project-details";
// types
import { IIssue } from "types";
type Props = {
isOpen: boolean;
handleClose: () => void;
onSuccess?: (data: IIssue) => Promise<void> | void;
prePopulatedData?: Partial<IIssue>;
};
const useCheckIfThereIsSpaceOnRight = (ref: React.RefObject<HTMLDivElement>) => {
const [isThereSpaceOnRight, setIsThereSpaceOnRight] = useState(true);
useEffect(() => {
if (!ref.current) return;
const { right } = ref.current.getBoundingClientRect();
const width = right + 250;
if (width > window.innerWidth) setIsThereSpaceOnRight(false);
else setIsThereSpaceOnRight(true);
}, [ref]);
return isThereSpaceOnRight;
};
const InlineInput = () => {
const { projectDetails } = useProjectDetails();
const { register, setFocus } = useFormContext();
useEffect(() => {
setFocus("name");
}, [setFocus]);
return (
<>
<h4 className="text-sm font-medium leading-5 text-custom-text-400">
{projectDetails?.identifier ?? "..."}
</h4>
<input
type="text"
autoComplete="off"
placeholder="Issue Title"
{...register("name", {
required: "Issue title is required.",
})}
className="w-full px-2 py-1.5 rounded-md bg-transparent text-sm font-medium leading-5 text-custom-text-200 outline-none"
/>
</>
);
};
export const CalendarInlineCreateIssueForm: React.FC<Props> = (props) => {
const { isOpen } = props;
const ref = useRef<HTMLDivElement>(null);
const isSpaceOnRight = useCheckIfThereIsSpaceOnRight(ref);
return (
<>
<div
ref={ref}
className={`absolute -translate-x-1 top-5 transition-all z-20 ${
isOpen ? "opacity-100 scale-100" : "opacity-0 pointer-events-none scale-95"
} ${isSpaceOnRight ? "left-full" : "right-0"}`}
>
<InlineCreateIssueFormWrapper
{...props}
className="flex w-60 p-1 px-1.5 rounded items-center gap-x-3 bg-custom-background-100 shadow-custom-shadow-md transition-opacity"
>
<InlineInput />
</InlineCreateIssueFormWrapper>
</div>
{/* Added to make any other element as outside click. This will make input also to be outside. */}
{isOpen && <div className="w-screen h-screen fixed inset-0 z-10" />}
</>
);
};

View File

@ -1,10 +1,14 @@
import React, { useState } from "react"; import React, { useState } from "react";
// next
import { useRouter } from "next/router";
// react-beautiful-dnd // react-beautiful-dnd
import { Draggable } from "react-beautiful-dnd"; import { Draggable } from "react-beautiful-dnd";
// component // component
import StrictModeDroppable from "components/dnd/StrictModeDroppable"; import StrictModeDroppable from "components/dnd/StrictModeDroppable";
import { SingleCalendarIssue } from "./single-issue"; import { SingleCalendarIssue } from "./single-issue";
import { CalendarInlineCreateIssueForm } from "./inline-create-issue-form";
// icons // icons
import { PlusSmallIcon } from "@heroicons/react/24/outline"; import { PlusSmallIcon } from "@heroicons/react/24/outline";
// helper // helper
@ -26,17 +30,14 @@ type Props = {
isNotAllowed: boolean; isNotAllowed: boolean;
}; };
export const SingleCalendarDate: React.FC<Props> = ({ export const SingleCalendarDate: React.FC<Props> = (props) => {
handleIssueAction, const { handleIssueAction, date, index, isMonthlyView, showWeekEnds, user, isNotAllowed } = props;
date,
index, const router = useRouter();
addIssueToDate, const { cycleId, moduleId } = router.query;
isMonthlyView,
showWeekEnds,
user,
isNotAllowed,
}) => {
const [showAllIssues, setShowAllIssues] = useState(false); const [showAllIssues, setShowAllIssues] = useState(false);
const [isCreateIssueFormOpen, setIsCreateIssueFormOpen] = useState(false);
const totalIssues = date.issues.length; const totalIssues = date.issues.length;
@ -70,6 +71,7 @@ export const SingleCalendarDate: React.FC<Props> = ({
provided={provided} provided={provided}
snapshot={snapshot} snapshot={snapshot}
issue={issue} issue={issue}
projectId={issue.project_detail.id}
handleEditIssue={() => handleIssueAction(issue, "edit")} handleEditIssue={() => handleIssueAction(issue, "edit")}
handleDeleteIssue={() => handleIssueAction(issue, "delete")} handleDeleteIssue={() => handleIssueAction(issue, "delete")}
user={user} user={user}
@ -78,6 +80,17 @@ export const SingleCalendarDate: React.FC<Props> = ({
)} )}
</Draggable> </Draggable>
))} ))}
<CalendarInlineCreateIssueForm
isOpen={isCreateIssueFormOpen}
handleClose={() => setIsCreateIssueFormOpen(false)}
prePopulatedData={{
target_date: date.date,
...(cycleId && { cycle: cycleId.toString() }),
...(moduleId && { module: moduleId.toString() }),
}}
/>
{totalIssues > 4 && ( {totalIssues > 4 && (
<button <button
type="button" type="button"
@ -93,7 +106,7 @@ export const SingleCalendarDate: React.FC<Props> = ({
> >
<button <button
className="flex items-center justify-center gap-1 text-center" className="flex items-center justify-center gap-1 text-center"
onClick={() => addIssueToDate(date.date)} onClick={() => setIsCreateIssueFormOpen(true)}
> >
<PlusSmallIcon className="h-4 w-4 text-custom-text-200" /> <PlusSmallIcon className="h-4 w-4 text-custom-text-200" />
Add issue Add issue

View File

@ -41,6 +41,7 @@ type Props = {
provided: DraggableProvided; provided: DraggableProvided;
snapshot: DraggableStateSnapshot; snapshot: DraggableStateSnapshot;
issue: IIssue; issue: IIssue;
projectId: string;
user: ICurrentUserResponse | undefined; user: ICurrentUserResponse | undefined;
isNotAllowed: boolean; isNotAllowed: boolean;
}; };
@ -52,11 +53,12 @@ export const SingleCalendarIssue: React.FC<Props> = ({
provided, provided,
snapshot, snapshot,
issue, issue,
projectId,
user, user,
isNotAllowed, isNotAllowed,
}) => { }) => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query; const { workspaceSlug, cycleId, moduleId, viewId } = router.query;
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
@ -310,6 +312,7 @@ export const SingleCalendarIssue: React.FC<Props> = ({
{properties.state && ( {properties.state && (
<StateSelect <StateSelect
value={issue.state_detail} value={issue.state_detail}
projectId={projectId}
onChange={handleStateChange} onChange={handleStateChange}
hideDropdownArrow hideDropdownArrow
disabled={isNotAllowed} disabled={isNotAllowed}
@ -334,6 +337,7 @@ export const SingleCalendarIssue: React.FC<Props> = ({
{properties.labels && issue.labels.length > 0 && ( {properties.labels && issue.labels.length > 0 && (
<LabelSelect <LabelSelect
value={issue.labels} value={issue.labels}
projectId={projectId}
onChange={handleLabelChange} onChange={handleLabelChange}
labelsDetails={issue.label_details} labelsDetails={issue.label_details}
hideDropdownArrow hideDropdownArrow
@ -345,6 +349,7 @@ export const SingleCalendarIssue: React.FC<Props> = ({
{properties.assignee && ( {properties.assignee && (
<MembersSelect <MembersSelect
value={issue.assignees} value={issue.assignees}
projectId={projectId}
onChange={handleAssigneeChange} onChange={handleAssigneeChange}
membersDetails={issue.assignee_details} membersDetails={issue.assignee_details}
hideDropdownArrow hideDropdownArrow

View File

@ -0,0 +1,62 @@
import { useEffect } from "react";
// react hook form
import { useFormContext } from "react-hook-form";
// hooks
import useProjectDetails from "hooks/use-project-details";
// components
import { InlineCreateIssueFormWrapper } from "components/core";
// types
import { IIssue } from "types";
type Props = {
isOpen: boolean;
handleClose: () => void;
onSuccess?: (data: IIssue) => Promise<void> | void;
prePopulatedData?: Partial<IIssue>;
};
const InlineInput = () => {
const { projectDetails } = useProjectDetails();
const { register, setFocus } = useFormContext();
useEffect(() => {
setFocus("name");
}, [setFocus]);
return (
<>
<div className="w-[14px] h-[14px] rounded-full border border-custom-border-1000 flex-shrink-0" />
<h4 className="text-sm text-custom-text-400">{projectDetails?.identifier ?? "..."}</h4>
<input
type="text"
autoComplete="off"
placeholder="Issue Title"
{...register("name", {
required: "Issue title is required.",
})}
className="w-full px-2 py-1.5 rounded-md bg-transparent text-sm font-medium leading-5 text-custom-text-200 outline-none"
/>
</>
);
};
export const GanttInlineCreateIssueForm: React.FC<Props> = (props) => (
<>
<InlineCreateIssueFormWrapper
className="flex py-3 px-4 mr-2.5 items-center rounded gap-x-2 border bg-custom-background-100 shadow-custom-shadow-sm"
{...props}
>
<InlineInput />
</InlineCreateIssueFormWrapper>
{props.isOpen && (
<p className="text-xs ml-3 mt-3 italic text-custom-text-200">
Press {"'"}Enter{"'"} to add another issue
</p>
)}
</>
);

View File

@ -5,3 +5,4 @@ export * from "./list-view";
export * from "./spreadsheet-view"; export * from "./spreadsheet-view";
export * from "./all-views"; export * from "./all-views";
export * from "./issues-view"; export * from "./issues-view";
export * from "./inline-issue-create-wrapper";

View File

@ -0,0 +1,270 @@
import { useEffect, useRef } from "react";
// next
import { useRouter } from "next/router";
// swr
import { mutate } from "swr";
// react hook form
import { useForm, FormProvider } from "react-hook-form";
// headless ui
import { Transition } from "@headlessui/react";
// services
import modulesService from "services/modules.service";
import issuesService from "services/issues.service";
// hooks
import useToast from "hooks/use-toast";
import useUser from "hooks/use-user";
import useKeypress from "hooks/use-keypress";
import useIssuesView from "hooks/use-issues-view";
import useMyIssues from "hooks/my-issues/use-my-issues";
import useGanttChartIssues from "hooks/gantt-chart/issue-view";
import useCalendarIssuesView from "hooks/use-calendar-issues-view";
import useOutsideClickDetector from "hooks/use-outside-click-detector";
import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view";
// helpers
import { getFetchKeysForIssueMutation } from "helpers/string.helper";
// fetch-keys
import {
USER_ISSUE,
SUB_ISSUES,
CYCLE_ISSUES_WITH_PARAMS,
MODULE_ISSUES_WITH_PARAMS,
CYCLE_DETAILS,
MODULE_DETAILS,
PROJECT_ISSUES_LIST_WITH_PARAMS,
} from "constants/fetch-keys";
// types
import { IIssue } from "types";
const defaultValues: Partial<IIssue> = {
name: "",
};
type Props = {
isOpen: boolean;
handleClose: () => void;
onSuccess?: (data: IIssue) => Promise<void> | void;
prePopulatedData?: Partial<IIssue>;
className?: string;
children?: React.ReactNode;
};
export const addIssueToCycle = async (
workspaceSlug: string,
projectId: string,
issueId: string,
cycleId: string,
user: any,
params: any
) => {
if (!workspaceSlug || !projectId) return;
await issuesService
.addIssueToCycle(
workspaceSlug as string,
projectId.toString(),
cycleId,
{
issues: [issueId],
},
user
)
.then(() => {
if (cycleId) {
mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId, params));
mutate(CYCLE_DETAILS(cycleId as string));
}
});
};
export const addIssueToModule = async (
workspaceSlug: string,
projectId: string,
issueId: string,
moduleId: string,
user: any,
params: any
) => {
await modulesService
.addIssuesToModule(
workspaceSlug as string,
projectId.toString(),
moduleId as string,
{
issues: [issueId],
},
user
)
.then(() => {
if (moduleId) {
mutate(MODULE_ISSUES_WITH_PARAMS(moduleId as string, params));
mutate(MODULE_DETAILS(moduleId as string));
}
});
};
export const InlineCreateIssueFormWrapper: React.FC<Props> = (props) => {
const { isOpen, handleClose, onSuccess, prePopulatedData, children, className } = props;
const ref = useRef<HTMLFormElement>(null);
const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
const { user } = useUser();
const { setToastAlert } = useToast();
const { displayFilters, params } = useIssuesView();
const { params: calendarParams } = useCalendarIssuesView();
const { ...viewGanttParams } = params;
const { params: spreadsheetParams } = useSpreadsheetIssuesView();
const { groupedIssues, mutateMyIssues } = useMyIssues(workspaceSlug?.toString());
const { params: ganttParams } = useGanttChartIssues(
workspaceSlug?.toString(),
projectId?.toString()
);
const method = useForm<IIssue>({ defaultValues });
const {
reset,
handleSubmit,
getValues,
formState: { errors, isSubmitting },
} = method;
useOutsideClickDetector(ref, handleClose);
useKeypress("Escape", handleClose);
useEffect(() => {
const values = getValues();
if (prePopulatedData) reset({ ...defaultValues, ...values, ...prePopulatedData });
}, [reset, prePopulatedData, getValues]);
useEffect(() => {
if (!isOpen) reset({ ...defaultValues });
}, [isOpen, reset]);
useEffect(() => {
if (isSubmitting)
setToastAlert({
type: "info",
title: "Creating issue...",
message: "Please wait while we create your issue.",
});
}, [isSubmitting, setToastAlert]);
useEffect(() => {
if (!errors) return;
Object.keys(errors).forEach((key) => {
const error = errors[key as keyof IIssue];
setToastAlert({
type: "error",
title: "Error!",
message: error?.message?.toString() || "Some error occurred. Please try again.",
});
});
}, [errors, setToastAlert]);
const { calendarFetchKey, ganttFetchKey, spreadsheetFetchKey } = getFetchKeysForIssueMutation({
cycleId: cycleId,
moduleId: moduleId,
viewId: viewId,
projectId: projectId?.toString() ?? "",
calendarParams,
spreadsheetParams,
viewGanttParams,
ganttParams,
});
const onSubmitHandler = async (formData: IIssue) => {
if (!workspaceSlug || !projectId || !user || isSubmitting) return;
reset({ ...defaultValues });
await issuesService
.createIssues(workspaceSlug.toString(), projectId.toString(), formData, user)
.then(async (res) => {
mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(projectId.toString(), params));
if (formData.cycle && formData.cycle !== "")
await addIssueToCycle(
workspaceSlug.toString(),
projectId.toString(),
res.id,
formData.cycle,
user,
params
);
if (formData.module && formData.module !== "")
await addIssueToModule(
workspaceSlug.toString(),
projectId.toString(),
res.id,
formData.module,
user,
params
);
if (displayFilters.layout === "calendar") mutate(calendarFetchKey);
if (displayFilters.layout === "gantt_chart") mutate(ganttFetchKey);
if (displayFilters.layout === "spreadsheet") mutate(spreadsheetFetchKey);
if (groupedIssues) mutateMyIssues();
setToastAlert({
type: "success",
title: "Success!",
message: "Issue created successfully.",
});
if (onSuccess) await onSuccess(res);
if (formData.assignees_list?.some((assignee) => assignee === user?.id))
mutate(USER_ISSUE(workspaceSlug as string));
if (formData.parent && formData.parent !== "") mutate(SUB_ISSUES(formData.parent));
})
.catch((err) => {
Object.keys(err || {}).forEach((key) => {
const error = err?.[key];
const errorTitle = error ? (Array.isArray(error) ? error.join(", ") : error) : null;
setToastAlert({
type: "error",
title: "Error!",
message: errorTitle || "Some error occurred. Please try again.",
});
});
});
};
return (
<>
<Transition
show={isOpen}
enter="transition ease-in-out duration-200 transform"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="transition ease-in-out duration-200 transform"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<FormProvider {...method}>
<form ref={ref} className={className} onSubmit={handleSubmit(onSubmitHandler)}>
{children}
</form>
</FormProvider>
</Transition>
</>
);
};

View File

@ -1,3 +1,4 @@
export * from "./all-lists"; export * from "./all-lists";
export * from "./single-issue"; export * from "./single-issue";
export * from "./single-list"; export * from "./single-list";
export * from "./inline-create-issue-form";

View File

@ -0,0 +1,55 @@
import { useEffect } from "react";
// react hook form
import { useFormContext } from "react-hook-form";
// hooks
import useProjectDetails from "hooks/use-project-details";
// components
import { InlineCreateIssueFormWrapper } from "../inline-issue-create-wrapper";
// types
import { IIssue } from "types";
type Props = {
isOpen: boolean;
handleClose: () => void;
onSuccess?: (data: IIssue) => Promise<void> | void;
prePopulatedData?: Partial<IIssue>;
};
const InlineInput = () => {
const { projectDetails } = useProjectDetails();
const { register, setFocus } = useFormContext();
useEffect(() => {
setFocus("name");
}, [setFocus]);
return (
<>
<h4 className="text-sm font-medium leading-5 text-custom-text-400">
{projectDetails?.identifier ?? "..."}
</h4>
<input
type="text"
autoComplete="off"
placeholder="Issue Title"
{...register("name", {
required: "Issue title is required.",
})}
className="w-full px-2 py-1.5 rounded-md bg-transparent text-sm font-medium leading-5 text-custom-text-200 outline-none"
/>
</>
);
};
export const ListInlineCreateIssueForm: React.FC<Props> = (props) => (
<InlineCreateIssueFormWrapper
className="flex py-3 px-4 items-center gap-x-5 bg-custom-background-100 shadow-custom-shadow-md"
{...props}
>
<InlineInput />
</InlineCreateIssueFormWrapper>
);

View File

@ -51,6 +51,7 @@ import {
type Props = { type Props = {
type?: string; type?: string;
issue: IIssue; issue: IIssue;
projectId: string;
groupTitle?: string; groupTitle?: string;
editIssue: () => void; editIssue: () => void;
index: number; index: number;
@ -69,6 +70,7 @@ type Props = {
export const SingleListIssue: React.FC<Props> = ({ export const SingleListIssue: React.FC<Props> = ({
type, type,
issue, issue,
projectId,
editIssue, editIssue,
index, index,
makeIssueCopy, makeIssueCopy,
@ -88,7 +90,7 @@ export const SingleListIssue: React.FC<Props> = ({
const [contextMenuPosition, setContextMenuPosition] = useState<React.MouseEvent | null>(null); const [contextMenuPosition, setContextMenuPosition] = useState<React.MouseEvent | null>(null);
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId, userId } = router.query; const { workspaceSlug, cycleId, moduleId, userId } = router.query;
const isArchivedIssues = router.pathname.includes("archived-issues"); const isArchivedIssues = router.pathname.includes("archived-issues");
const isDraftIssues = router.pathname?.split("/")?.[4] === "draft-issues"; const isDraftIssues = router.pathname?.split("/")?.[4] === "draft-issues";
@ -376,6 +378,7 @@ export const SingleListIssue: React.FC<Props> = ({
{properties.state && ( {properties.state && (
<StateSelect <StateSelect
value={issue.state_detail} value={issue.state_detail}
projectId={projectId}
onChange={handleStateChange} onChange={handleStateChange}
hideDropdownArrow hideDropdownArrow
disabled={isNotAllowed} disabled={isNotAllowed}
@ -400,6 +403,7 @@ export const SingleListIssue: React.FC<Props> = ({
{properties.labels && ( {properties.labels && (
<LabelSelect <LabelSelect
value={issue.labels} value={issue.labels}
projectId={projectId}
onChange={handleLabelChange} onChange={handleLabelChange}
labelsDetails={issue.label_details} labelsDetails={issue.label_details}
hideDropdownArrow hideDropdownArrow
@ -411,6 +415,7 @@ export const SingleListIssue: React.FC<Props> = ({
{properties.assignee && ( {properties.assignee && (
<MembersSelect <MembersSelect
value={issue.assignees} value={issue.assignees}
projectId={projectId}
onChange={handleAssigneeChange} onChange={handleAssigneeChange}
membersDetails={issue.assignee_details} membersDetails={issue.assignee_details}
hideDropdownArrow hideDropdownArrow

View File

@ -1,3 +1,6 @@
import { useState } from "react";
// next
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR from "swr"; import useSWR from "swr";
@ -10,7 +13,7 @@ import projectService from "services/project.service";
// hooks // hooks
import useProjects from "hooks/use-projects"; import useProjects from "hooks/use-projects";
// components // components
import { SingleListIssue } from "components/core"; import { SingleListIssue, ListInlineCreateIssueForm } from "components/core";
// ui // ui
import { Avatar, CustomMenu } from "components/ui"; import { Avatar, CustomMenu } from "components/ui";
// icons // icons
@ -51,10 +54,10 @@ type Props = {
viewProps: IIssueViewProps; viewProps: IIssueViewProps;
}; };
export const SingleList: React.FC<Props> = ({ export const SingleList: React.FC<Props> = (props) => {
const {
currentState, currentState,
groupTitle, groupTitle,
addIssueToGroup,
handleIssueAction, handleIssueAction,
openIssuesListModal, openIssuesListModal,
handleDraftIssueAction, handleDraftIssueAction,
@ -65,10 +68,13 @@ export const SingleList: React.FC<Props> = ({
user, user,
userAuth, userAuth,
viewProps, viewProps,
}) => { } = props;
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId } = router.query; const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
const [isCreateIssueFormOpen, setIsCreateIssueFormOpen] = useState(false);
const isArchivedIssues = router.pathname.includes("archived-issues"); const isArchivedIssues = router.pathname.includes("archived-issues");
const type = cycleId ? "cycle" : moduleId ? "module" : "issue"; const type = cycleId ? "cycle" : moduleId ? "module" : "issue";
@ -207,7 +213,7 @@ export const SingleList: React.FC<Props> = ({
<button <button
type="button" type="button"
className="p-1 text-custom-text-200 hover:bg-custom-background-80" className="p-1 text-custom-text-200 hover:bg-custom-background-80"
onClick={addIssueToGroup} onClick={() => setIsCreateIssueFormOpen(true)}
> >
<PlusIcon className="h-4 w-4" /> <PlusIcon className="h-4 w-4" />
</button> </button>
@ -224,7 +230,9 @@ export const SingleList: React.FC<Props> = ({
position="right" position="right"
noBorder noBorder
> >
<CustomMenu.MenuItem onClick={addIssueToGroup}>Create new</CustomMenu.MenuItem> <CustomMenu.MenuItem onClick={() => setIsCreateIssueFormOpen(true)}>
Create new
</CustomMenu.MenuItem>
{openIssuesListModal && ( {openIssuesListModal && (
<CustomMenu.MenuItem onClick={openIssuesListModal}> <CustomMenu.MenuItem onClick={openIssuesListModal}>
Add an existing issue Add an existing issue
@ -250,6 +258,7 @@ export const SingleList: React.FC<Props> = ({
key={issue.id} key={issue.id}
type={type} type={type}
issue={issue} issue={issue}
projectId={issue.project_detail.id}
groupTitle={groupTitle} groupTitle={groupTitle}
index={index} index={index}
editIssue={() => handleIssueAction(issue, "edit")} editIssue={() => handleIssueAction(issue, "edit")}
@ -284,6 +293,29 @@ export const SingleList: React.FC<Props> = ({
) : ( ) : (
<div className="flex h-full w-full items-center justify-center">Loading...</div> <div className="flex h-full w-full items-center justify-center">Loading...</div>
)} )}
<ListInlineCreateIssueForm
isOpen={isCreateIssueFormOpen}
handleClose={() => setIsCreateIssueFormOpen(false)}
prePopulatedData={{
...(cycleId && { cycle: cycleId.toString() }),
...(moduleId && { module: moduleId.toString() }),
[displayFilters?.group_by!]: groupTitle,
}}
/>
{!isCreateIssueFormOpen && (
<div className="w-full bg-custom-background-100 px-6 py-3">
<button
type="button"
onClick={() => setIsCreateIssueFormOpen(true)}
className="flex items-center gap-x-[6px] text-custom-primary-100 px-2 py-1 rounded-md"
>
<PlusIcon className="h-4 w-4" />
<span className="text-sm font-medium text-custom-primary-100">New Issue</span>
</button>
</div>
)}
</Disclosure.Panel> </Disclosure.Panel>
</Transition> </Transition>
</div> </div>

View File

@ -49,6 +49,7 @@ import { renderLongDetailDateFormat } from "helpers/date-time.helper";
type Props = { type Props = {
issue: IIssue; issue: IIssue;
projectId: string;
index: number; index: number;
expanded: boolean; expanded: boolean;
handleToggleExpand: (issueId: string) => void; handleToggleExpand: (issueId: string) => void;
@ -64,6 +65,7 @@ type Props = {
export const SingleSpreadsheetIssue: React.FC<Props> = ({ export const SingleSpreadsheetIssue: React.FC<Props> = ({
issue, issue,
projectId,
index, index,
expanded, expanded,
handleToggleExpand, handleToggleExpand,
@ -80,7 +82,7 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query; const { workspaceSlug, cycleId, moduleId, viewId } = router.query;
const { params } = useSpreadsheetIssuesView(); const { params } = useSpreadsheetIssuesView();
@ -96,7 +98,7 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), params) ? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), params)
: viewId : viewId
? VIEW_ISSUES(viewId.toString(), params) ? VIEW_ISSUES(viewId.toString(), params)
: PROJECT_ISSUES_LIST_WITH_PARAMS(projectId.toString(), params); : PROJECT_ISSUES_LIST_WITH_PARAMS(projectId, params);
if (issue.parent) if (issue.parent)
mutate<ISubIssueResponse>( mutate<ISubIssueResponse>(
@ -136,13 +138,7 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
); );
issuesService issuesService
.patchIssue( .patchIssue(workspaceSlug as string, projectId, issue.id as string, formData, user)
workspaceSlug as string,
projectId as string,
issue.id as string,
formData,
user
)
.then(() => { .then(() => {
if (issue.parent) { if (issue.parent) {
mutate(SUB_ISSUES(issue.parent as string)); mutate(SUB_ISSUES(issue.parent as string));
@ -368,6 +364,7 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200"> <div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
<StateSelect <StateSelect
value={issue.state_detail} value={issue.state_detail}
projectId={projectId}
onChange={handleStateChange} onChange={handleStateChange}
buttonClassName="!p-0 !rounded-none !shadow-none !border-0" buttonClassName="!p-0 !rounded-none !shadow-none !border-0"
hideDropdownArrow hideDropdownArrow
@ -390,6 +387,7 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200"> <div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
<MembersSelect <MembersSelect
value={issue.assignees} value={issue.assignees}
projectId={projectId}
onChange={handleAssigneeChange} onChange={handleAssigneeChange}
membersDetails={issue.assignee_details} membersDetails={issue.assignee_details}
buttonClassName="!p-0 !rounded-none !shadow-none !border-0" buttonClassName="!p-0 !rounded-none !shadow-none !border-0"
@ -402,6 +400,7 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200"> <div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
<LabelSelect <LabelSelect
value={issue.labels} value={issue.labels}
projectId={projectId}
onChange={handleLabelChange} onChange={handleLabelChange}
labelsDetails={issue.label_details} labelsDetails={issue.label_details}
hideDropdownArrow hideDropdownArrow

View File

@ -55,6 +55,7 @@ export const SpreadsheetIssues: React.FC<Props> = ({
<div> <div>
<SingleSpreadsheetIssue <SingleSpreadsheetIssue
issue={issue} issue={issue}
projectId={issue.project_detail.id}
index={index} index={index}
expanded={isExpanded} expanded={isExpanded}
handleToggleExpand={handleToggleExpand} handleToggleExpand={handleToggleExpand}

View File

@ -4,7 +4,7 @@ import React, { useState } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
// components // components
import { SpreadsheetColumns, SpreadsheetIssues } from "components/core"; import { SpreadsheetColumns, SpreadsheetIssues, ListInlineCreateIssueForm } from "components/core";
import { CustomMenu, Spinner } from "components/ui"; import { CustomMenu, Spinner } from "components/ui";
import { IssuePeekOverview } from "components/issues"; import { IssuePeekOverview } from "components/issues";
// hooks // hooks
@ -33,6 +33,7 @@ export const SpreadsheetView: React.FC<Props> = ({
userAuth, userAuth,
}) => { }) => {
const [expandedIssues, setExpandedIssues] = useState<string[]>([]); const [expandedIssues, setExpandedIssues] = useState<string[]>([]);
const [isInlineCreateIssueFormOpen, setIsInlineCreateIssueFormOpen] = useState(false);
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId } = router.query; const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
@ -88,17 +89,26 @@ export const SpreadsheetView: React.FC<Props> = ({
userAuth={userAuth} userAuth={userAuth}
/> />
))} ))}
<ListInlineCreateIssueForm
isOpen={isInlineCreateIssueFormOpen}
handleClose={() => setIsInlineCreateIssueFormOpen(false)}
prePopulatedData={{
...(cycleId && { cycle: cycleId.toString() }),
...(moduleId && { module: moduleId.toString() }),
}}
/>
<div <div
className="relative group grid auto-rows-[minmax(44px,1fr)] hover:rounded-sm hover:bg-custom-background-80 border-b border-custom-border-200 w-full min-w-max" className="relative group grid auto-rows-[minmax(44px,1fr)] hover:rounded-sm hover:bg-custom-background-80 border-b border-custom-border-200 w-full min-w-max"
style={{ gridTemplateColumns }} style={{ gridTemplateColumns }}
> >
{!isInlineCreateIssueFormOpen && (
<>
{type === "issue" ? ( {type === "issue" ? (
<button <button
className="flex gap-1.5 items-center pl-7 py-2.5 text-sm sticky left-0 z-[1] text-custom-text-200 bg-custom-background-100 group-hover:text-custom-text-100 group-hover:bg-custom-background-80 border-custom-border-200 w-full" className="flex gap-1.5 items-center pl-7 py-2.5 text-sm sticky left-0 z-[1] text-custom-text-200 bg-custom-background-100 group-hover:text-custom-text-100 group-hover:bg-custom-background-80 border-custom-border-200 w-full"
onClick={() => { onClick={() => setIsInlineCreateIssueFormOpen(true)}
const e = new KeyboardEvent("keydown", { key: "c" });
document.dispatchEvent(e);
}}
> >
<PlusIcon className="h-4 w-4" /> <PlusIcon className="h-4 w-4" />
Add Issue Add Issue
@ -120,12 +130,7 @@ export const SpreadsheetView: React.FC<Props> = ({
optionsClassName="left-5 !w-36" optionsClassName="left-5 !w-36"
noBorder noBorder
> >
<CustomMenu.MenuItem <CustomMenu.MenuItem onClick={() => setIsInlineCreateIssueFormOpen(true)}>
onClick={() => {
const e = new KeyboardEvent("keydown", { key: "c" });
document.dispatchEvent(e);
}}
>
Create new Create new
</CustomMenu.MenuItem> </CustomMenu.MenuItem>
{openIssuesListModal && ( {openIssuesListModal && (
@ -136,6 +141,8 @@ export const SpreadsheetView: React.FC<Props> = ({
</CustomMenu> </CustomMenu>
) )
)} )}
</>
)}
</div> </div>
</div> </div>
) : ( ) : (

View File

@ -1,3 +1,6 @@
import { useState } from "react";
// next
import { useRouter } from "next/router";
// react-beautiful-dnd // react-beautiful-dnd
import { DragDropContext, Draggable, DropResult } from "react-beautiful-dnd"; import { DragDropContext, Draggable, DropResult } from "react-beautiful-dnd";
import StrictModeDroppable from "components/dnd/StrictModeDroppable"; import StrictModeDroppable from "components/dnd/StrictModeDroppable";
@ -7,6 +10,9 @@ import { useChart } from "./hooks";
import { Loader } from "components/ui"; import { Loader } from "components/ui";
// icons // icons
import { EllipsisVerticalIcon } from "@heroicons/react/24/outline"; import { EllipsisVerticalIcon } from "@heroicons/react/24/outline";
import { PlusIcon } from "lucide-react";
// components
import { GanttInlineCreateIssueForm } from "components/core/views/gantt-chart-view/inline-create-issue-form";
// types // types
import { IBlockUpdateData, IGanttBlock } from "./types"; import { IBlockUpdateData, IGanttBlock } from "./types";
@ -18,15 +24,16 @@ type Props = {
enableReorder: boolean; enableReorder: boolean;
}; };
export const GanttSidebar: React.FC<Props> = ({ export const GanttSidebar: React.FC<Props> = (props) => {
title, const { title, blockUpdateHandler, blocks, SidebarBlockRender, enableReorder } = props;
blockUpdateHandler,
blocks, const router = useRouter();
SidebarBlockRender, const { cycleId, moduleId } = router.query;
enableReorder,
}) => {
const { activeBlock, dispatch } = useChart(); const { activeBlock, dispatch } = useChart();
const [isCreateIssueFormOpen, setIsCreateIssueFormOpen] = useState(false);
// update the active block on hover // update the active block on hover
const updateActiveBlock = (block: IGanttBlock | null) => { const updateActiveBlock = (block: IGanttBlock | null) => {
dispatch({ dispatch({
@ -148,6 +155,28 @@ export const GanttSidebar: React.FC<Props> = ({
)} )}
{droppableProvided.placeholder} {droppableProvided.placeholder}
</> </>
<GanttInlineCreateIssueForm
isOpen={isCreateIssueFormOpen}
handleClose={() => setIsCreateIssueFormOpen(false)}
prePopulatedData={{
start_date: new Date(Date.now()).toISOString().split("T")[0],
target_date: new Date(Date.now() + 86400000).toISOString().split("T")[0],
...(cycleId && { cycle: cycleId.toString() }),
...(moduleId && { module: moduleId.toString() }),
}}
/>
{!isCreateIssueFormOpen && (
<button
type="button"
onClick={() => setIsCreateIssueFormOpen(true)}
className="flex items-center gap-x-[6px] text-custom-primary-100 px-2 py-1 rounded-md mt-3"
>
<PlusIcon className="h-4 w-4" />
<span className="text-sm font-medium text-custom-primary-100">New Issue</span>
</button>
)}
</div> </div>
)} )}
</StrictModeDroppable> </StrictModeDroppable>

View File

@ -24,6 +24,7 @@ import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys";
type Props = { type Props = {
value: string[]; value: string[];
projectId: string;
onChange: (data: any) => void; onChange: (data: any) => void;
labelsDetails: any[]; labelsDetails: any[];
className?: string; className?: string;
@ -37,6 +38,7 @@ type Props = {
export const LabelSelect: React.FC<Props> = ({ export const LabelSelect: React.FC<Props> = ({
value, value,
projectId,
onChange, onChange,
labelsDetails, labelsDetails,
className = "", className = "",
@ -54,15 +56,15 @@ export const LabelSelect: React.FC<Props> = ({
const [labelModal, setLabelModal] = useState(false); const [labelModal, setLabelModal] = useState(false);
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug } = router.query;
const dropdownBtn = useRef<any>(null); const dropdownBtn = useRef<any>(null);
const dropdownOptions = useRef<any>(null); const dropdownOptions = useRef<any>(null);
const { data: issueLabels } = useSWR<IIssueLabels[]>( const { data: issueLabels } = useSWR<IIssueLabels[]>(
projectId && fetchStates ? PROJECT_ISSUE_LABELS(projectId.toString()) : null, projectId && fetchStates ? PROJECT_ISSUE_LABELS(projectId) : null,
workspaceSlug && projectId && fetchStates workspaceSlug && projectId && fetchStates
? () => issuesService.getIssueLabels(workspaceSlug as string, projectId as string) ? () => issuesService.getIssueLabels(workspaceSlug as string, projectId)
: null : null
); );
@ -150,7 +152,7 @@ export const LabelSelect: React.FC<Props> = ({
<CreateLabelModal <CreateLabelModal
isOpen={labelModal} isOpen={labelModal}
handleClose={() => setLabelModal(false)} handleClose={() => setLabelModal(false)}
projectId={projectId.toString()} projectId={projectId}
user={user} user={user}
/> />
)} )}

View File

@ -18,6 +18,7 @@ import { IUser } from "types";
type Props = { type Props = {
value: string | string[]; value: string | string[];
projectId: string;
onChange: (data: any) => void; onChange: (data: any) => void;
membersDetails: IUser[]; membersDetails: IUser[];
renderWorkspaceMembers?: boolean; renderWorkspaceMembers?: boolean;
@ -30,6 +31,7 @@ type Props = {
export const MembersSelect: React.FC<Props> = ({ export const MembersSelect: React.FC<Props> = ({
value, value,
projectId,
onChange, onChange,
membersDetails, membersDetails,
renderWorkspaceMembers = false, renderWorkspaceMembers = false,
@ -44,14 +46,14 @@ export const MembersSelect: React.FC<Props> = ({
const [fetchStates, setFetchStates] = useState(false); const [fetchStates, setFetchStates] = useState(false);
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug } = router.query;
const dropdownBtn = useRef<any>(null); const dropdownBtn = useRef<any>(null);
const dropdownOptions = useRef<any>(null); const dropdownOptions = useRef<any>(null);
const { members } = useProjectMembers( const { members } = useProjectMembers(
workspaceSlug?.toString(), workspaceSlug?.toString(),
projectId?.toString(), projectId,
fetchStates && !renderWorkspaceMembers fetchStates && !renderWorkspaceMembers
); );

View File

@ -149,7 +149,7 @@ export const SingleProjectCard: React.FC<ProjectCardProps> = ({
</button> </button>
) : ( ) : (
<span className="cursor-default rounded bg-green-600 px-2 py-1 text-xs"> <span className="cursor-default rounded bg-green-600 px-2 py-1 text-xs">
Member Joined
</span> </span>
)} )}
{project.is_favorite && ( {project.is_favorite && (

View File

@ -25,6 +25,7 @@ import { getStatesList } from "helpers/state.helper";
type Props = { type Props = {
value: IState; value: IState;
onChange: (data: any, states: IState[] | undefined) => void; onChange: (data: any, states: IState[] | undefined) => void;
projectId: string;
className?: string; className?: string;
buttonClassName?: string; buttonClassName?: string;
optionsClassName?: string; optionsClassName?: string;
@ -35,6 +36,7 @@ type Props = {
export const StateSelect: React.FC<Props> = ({ export const StateSelect: React.FC<Props> = ({
value, value,
onChange, onChange,
projectId,
className = "", className = "",
buttonClassName = "", buttonClassName = "",
optionsClassName = "", optionsClassName = "",
@ -50,12 +52,12 @@ export const StateSelect: React.FC<Props> = ({
const [fetchStates, setFetchStates] = useState<boolean>(false); const [fetchStates, setFetchStates] = useState<boolean>(false);
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug } = router.query;
const { data: stateGroups } = useSWR( const { data: stateGroups } = useSWR(
workspaceSlug && projectId && fetchStates ? STATES_LIST(projectId as string) : null, workspaceSlug && projectId && fetchStates ? STATES_LIST(projectId) : null,
workspaceSlug && projectId && fetchStates workspaceSlug && projectId && fetchStates
? () => stateService.getStates(workspaceSlug as string, projectId as string) ? () => stateService.getStates(workspaceSlug as string, projectId)
: null : null
); );

View File

@ -35,7 +35,7 @@ export const ToggleSwitch: React.FC<Props> = (props) => {
: size === "md" : size === "md"
? "translate-x-4" ? "translate-x-4"
: "translate-x-5") + " bg-white" : "translate-x-5") + " bg-white"
: "translate-x-1 bg-custom-background-90" : "translate-x-0.5 bg-custom-background-90"
}`} }`}
/> />
</Switch> </Switch>

View File

@ -1,3 +1,10 @@
import {
CYCLE_ISSUES_WITH_PARAMS,
MODULE_ISSUES_WITH_PARAMS,
PROJECT_ISSUES_LIST_WITH_PARAMS,
VIEW_ISSUES,
} from "constants/fetch-keys";
export const addSpaceIfCamelCase = (str: string) => str.replace(/([a-z])([A-Z])/g, "$1 $2"); export const addSpaceIfCamelCase = (str: string) => str.replace(/([a-z])([A-Z])/g, "$1 $2");
export const replaceUnderscoreIfSnakeCase = (str: string) => str.replace(/_/g, " "); export const replaceUnderscoreIfSnakeCase = (str: string) => str.replace(/_/g, " ");
@ -122,3 +129,65 @@ export const objToQueryParams = (obj: any) => {
return params.toString(); return params.toString();
}; };
export const getFetchKeysForIssueMutation = (options: {
cycleId?: string | string[];
moduleId?: string | string[];
viewId?: string | string[];
projectId: string;
calendarParams: any;
spreadsheetParams: any;
viewGanttParams: any;
ganttParams: any;
}) => {
const {
cycleId,
moduleId,
viewId,
projectId,
calendarParams,
spreadsheetParams,
viewGanttParams,
ganttParams,
} = options;
const calendarFetchKey = cycleId
? { calendarFetchKey: CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), calendarParams) }
: moduleId
? { calendarFetchKey: MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), calendarParams) }
: viewId
? { calendarFetchKey: VIEW_ISSUES(viewId.toString(), calendarParams) }
: {
calendarFetchKey: PROJECT_ISSUES_LIST_WITH_PARAMS(
projectId?.toString() ?? "",
calendarParams
),
};
const spreadsheetFetchKey = cycleId
? { spreadsheetFetchKey: CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), spreadsheetParams) }
: moduleId
? { spreadsheetFetchKey: MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), spreadsheetParams) }
: viewId
? { spreadsheetFetchKey: VIEW_ISSUES(viewId.toString(), spreadsheetParams) }
: {
spreadsheetFetchKey: PROJECT_ISSUES_LIST_WITH_PARAMS(
projectId?.toString() ?? "",
spreadsheetParams
),
};
const ganttFetchKey = cycleId
? { ganttFetchKey: CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), ganttParams) }
: moduleId
? { ganttFetchKey: MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), ganttParams) }
: viewId
? { ganttFetchKey: VIEW_ISSUES(viewId.toString(), viewGanttParams) }
: { ganttFetchKey: PROJECT_ISSUES_LIST_WITH_PARAMS(projectId?.toString() ?? "", ganttParams) };
return {
...calendarFetchKey,
...spreadsheetFetchKey,
...ganttFetchKey,
};
};

View File

@ -36,6 +36,7 @@ const useGanttChartIssues = (workspaceSlug: string | undefined, projectId: strin
return { return {
ganttIssues, ganttIssues,
mutateGanttIssues, mutateGanttIssues,
params,
}; };
}; };

View File

@ -0,0 +1,19 @@
import { useEffect } from "react";
const useKeypress = (key: string, callback: () => void) => {
useEffect(() => {
const handleKeydown = (event: KeyboardEvent) => {
if (event.key === key) {
callback();
}
};
document.addEventListener("keydown", handleKeydown);
return () => {
document.removeEventListener("keydown", handleKeydown);
};
});
};
export default useKeypress;

View File

@ -127,9 +127,21 @@ const WorkspacePage: NextPage = () => {
/> />
</div> </div>
)} )}
<div className="p-8 space-y-8">
<div>
<h3 className="text-2xl font-semibold">
Good {greeting}, {user?.first_name} {user?.last_name}
</h3>
<h6 className="text-custom-text-400 font-medium flex items-center gap-2">
<div>{greeting === "morning" ? "🌤️" : greeting === "afternoon" ? "🌥️" : "🌙️"}</div>
<div>
{DAYS[today.getDay()]}, {renderShortDate(today)} {render12HourFormatTime(today)}
</div>
</h6>
</div>
{projects ? ( {projects ? (
projects.length > 0 ? ( projects.length > 0 ? (
<div className="p-8">
<div className="flex flex-col gap-8"> <div className="flex flex-col gap-8">
<IssuesStats data={workspaceDashboardData} /> <IssuesStats data={workspaceDashboardData} />
<div className="grid grid-cols-1 gap-8 lg:grid-cols-2"> <div className="grid grid-cols-1 gap-8 lg:grid-cols-2">
@ -143,17 +155,8 @@ const WorkspacePage: NextPage = () => {
/> />
</div> </div>
</div> </div>
</div>
) : ( ) : (
<div className="p-8"> <div className="bg-custom-primary-100/5 flex justify-between gap-5 md:gap-8">
<h3 className="text-2xl font-semibold">
Good {greeting}, {user?.first_name} {user?.last_name}
</h3>
<h6 className="text-custom-text-400 font-medium">
{greeting === "morning" ? "🌤️" : greeting === "afternoon" ? "🌥️" : "🌙️"}
{DAYS[today.getDay()]}, {renderShortDate(today)} {render12HourFormatTime(today)}
</h6>
<div className="mt-7 bg-custom-primary-100/5 flex justify-between gap-5 md:gap-8">
<div className="p-5 md:p-8 pr-0"> <div className="p-5 md:p-8 pr-0">
<h5 className="text-xl font-semibold">Create a project</h5> <h5 className="text-xl font-semibold">Create a project</h5>
<p className="mt-2 mb-5"> <p className="mt-2 mb-5">
@ -174,9 +177,9 @@ const WorkspacePage: NextPage = () => {
<Image src={emptyDashboard} alt="Empty Dashboard" /> <Image src={emptyDashboard} alt="Empty Dashboard" />
</div> </div>
</div> </div>
</div>
) )
) : null} ) : null}
</div>
</WorkspaceAuthorizationLayout> </WorkspaceAuthorizationLayout>
); );
}; };

View File

@ -46,7 +46,7 @@ const ProfileActivity = () => {
{userActivity ? ( {userActivity ? (
<section className="pr-9 py-8 w-full overflow-y-auto"> <section className="pr-9 py-8 w-full overflow-y-auto">
<div className="flex items-center py-3.5 border-b border-custom-border-200"> <div className="flex items-center py-3.5 border-b border-custom-border-200">
<h3 className="text-xl font-medium">Acitivity</h3> <h3 className="text-xl font-medium">Activity</h3>
</div> </div>
<div className={`flex flex-col gap-2 py-4 w-full`}> <div className={`flex flex-col gap-2 py-4 w-full`}>
<ul role="list" className="-mb-4"> <ul role="list" className="-mb-4">

View File

@ -66,7 +66,7 @@ const ProfilePreferences = observer(() => {
<div className="pr-9 py-8 w-full overflow-y-auto"> <div className="pr-9 py-8 w-full overflow-y-auto">
<div className="flex items-center py-3.5 border-b border-custom-border-200"> <div className="flex items-center py-3.5 border-b border-custom-border-200">
<h3 className="text-xl font-medium">Acitivity</h3> <h3 className="text-xl font-medium">Preferences</h3>
</div> </div>
<div className="grid grid-cols-12 gap-4 sm:gap-16 py-6"> <div className="grid grid-cols-12 gap-4 sm:gap-16 py-6">
<div className="col-span-12 sm:col-span-6"> <div className="col-span-12 sm:col-span-6">

View File

@ -50,7 +50,7 @@ const BillingSettings: NextPage = () => {
<section className="pr-9 py-8 w-full overflow-y-auto"> <section className="pr-9 py-8 w-full overflow-y-auto">
<div> <div>
<div className="flex items-center py-3.5 border-b border-custom-border-200"> <div className="flex items-center py-3.5 border-b border-custom-border-200">
<h3 className="text-xl font-medium">Billing & Plan</h3> <h3 className="text-xl font-medium">Billing & Plans</h3>
</div> </div>
</div> </div>
<div className="px-4 py-6"> <div className="px-4 py-6">