merge conflicts resolved

This commit is contained in:
Anmol Singh Bhatia 2023-09-26 18:14:18 +05:30
commit d3e4eb5753
43 changed files with 1102 additions and 415 deletions

View File

@ -34,7 +34,6 @@ class CycleSerializer(BaseSerializer):
unstarted_issues = serializers.IntegerField(read_only=True) unstarted_issues = serializers.IntegerField(read_only=True)
backlog_issues = serializers.IntegerField(read_only=True) backlog_issues = serializers.IntegerField(read_only=True)
assignees = serializers.SerializerMethodField(read_only=True) assignees = serializers.SerializerMethodField(read_only=True)
labels = serializers.SerializerMethodField(read_only=True)
total_estimates = serializers.IntegerField(read_only=True) total_estimates = serializers.IntegerField(read_only=True)
completed_estimates = serializers.IntegerField(read_only=True) completed_estimates = serializers.IntegerField(read_only=True)
started_estimates = serializers.IntegerField(read_only=True) started_estimates = serializers.IntegerField(read_only=True)
@ -50,11 +49,10 @@ class CycleSerializer(BaseSerializer):
members = [ members = [
{ {
"avatar": assignee.avatar, "avatar": assignee.avatar,
"first_name": assignee.first_name,
"display_name": assignee.display_name, "display_name": assignee.display_name,
"id": assignee.id, "id": assignee.id,
} }
for issue_cycle in obj.issue_cycle.all() for issue_cycle in obj.issue_cycle.prefetch_related("issue__assignees").all()
for assignee in issue_cycle.issue.assignees.all() for assignee in issue_cycle.issue.assignees.all()
] ]
# Use a set comprehension to return only the unique objects # Use a set comprehension to return only the unique objects
@ -64,24 +62,6 @@ class CycleSerializer(BaseSerializer):
unique_list = [dict(item) for item in unique_objects] unique_list = [dict(item) for item in unique_objects]
return unique_list return unique_list
def get_labels(self, obj):
labels = [
{
"name": label.name,
"color": label.color,
"id": label.id,
}
for issue_cycle in obj.issue_cycle.all()
for label in issue_cycle.issue.labels.all()
]
# Use a set comprehension to return only the unique objects
unique_objects = {frozenset(item.items()) for item in labels}
# Convert the set back to a list of dictionaries
unique_list = [dict(item) for item in unique_objects]
return unique_list
class Meta: class Meta:
model = Cycle model = Cycle

View File

@ -1094,7 +1094,7 @@ class ProjectMemberEndpoint(BaseAPIView):
project_id=project_id, project_id=project_id,
workspace__slug=slug, workspace__slug=slug,
member__is_bot=False, member__is_bot=False,
).select_related("project", "member") ).select_related("project", "member", "workspace")
serializer = ProjectMemberSerializer(project_members, many=True) serializer = ProjectMemberSerializer(project_members, many=True)
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
except Exception as e: except Exception as e:

View File

@ -58,20 +58,23 @@ def archive_old_issues():
# Check if Issues # Check if Issues
if issues: if issues:
# Set the archive time to current time
archive_at = timezone.now()
issues_to_update = [] issues_to_update = []
for issue in issues: for issue in issues:
issue.archived_at = timezone.now() issue.archived_at = archive_at
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
if issues_to_update: if issues_to_update:
updated_issues = Issue.objects.bulk_update( Issue.objects.bulk_update(
issues_to_update, ["archived_at"], batch_size=100 issues_to_update, ["archived_at"], batch_size=100
) )
[ [
issue_activity.delay( issue_activity.delay(
type="issue.activity.updated", type="issue.activity.updated",
requested_data=json.dumps({"archived_at": str(issue.archived_at)}), requested_data=json.dumps({"archived_at": str(archive_at)}),
actor_id=str(project.created_by_id), actor_id=str(project.created_by_id),
issue_id=issue.id, issue_id=issue.id,
project_id=project_id, project_id=project_id,
@ -79,7 +82,7 @@ def archive_old_issues():
subscriber=False, subscriber=False,
epoch = int(timezone.now().timestamp()) epoch = int(timezone.now().timestamp())
) )
for issue in updated_issues for issue in issues_to_update
] ]
return return
except Exception as e: except Exception as e:
@ -139,7 +142,7 @@ def close_old_issues():
# Bulk Update the issues and log the activity # Bulk Update the issues and log the activity
if issues_to_update: if issues_to_update:
updated_issues = Issue.objects.bulk_update(issues_to_update, ["state"], batch_size=100) Issue.objects.bulk_update(issues_to_update, ["state"], batch_size=100)
[ [
issue_activity.delay( issue_activity.delay(
type="issue.activity.updated", type="issue.activity.updated",
@ -151,7 +154,7 @@ def close_old_issues():
subscriber=False, subscriber=False,
epoch = int(timezone.now().timestamp()) epoch = int(timezone.now().timestamp())
) )
for issue in updated_issues for issue in issues_to_update
] ]
return return
except Exception as e: except Exception as e:

View File

@ -26,19 +26,19 @@ def workspace_member_props(old_props):
"calendar_date_range": old_props.get("calendarDateRange", ""), "calendar_date_range": old_props.get("calendarDateRange", ""),
}, },
"display_properties": { "display_properties": {
"assignee": old_props.get("properties", {}).get("assignee",None), "assignee": old_props.get("properties", {}).get("assignee", True),
"attachment_count": old_props.get("properties", {}).get("attachment_count", None), "attachment_count": old_props.get("properties", {}).get("attachment_count", True),
"created_on": old_props.get("properties", {}).get("created_on", None), "created_on": old_props.get("properties", {}).get("created_on", True),
"due_date": old_props.get("properties", {}).get("due_date", None), "due_date": old_props.get("properties", {}).get("due_date", True),
"estimate": old_props.get("properties", {}).get("estimate", None), "estimate": old_props.get("properties", {}).get("estimate", True),
"key": old_props.get("properties", {}).get("key", None), "key": old_props.get("properties", {}).get("key", True),
"labels": old_props.get("properties", {}).get("labels", None), "labels": old_props.get("properties", {}).get("labels", True),
"link": old_props.get("properties", {}).get("link", None), "link": old_props.get("properties", {}).get("link", True),
"priority": old_props.get("properties", {}).get("priority", None), "priority": old_props.get("properties", {}).get("priority", True),
"start_date": old_props.get("properties", {}).get("start_date", None), "start_date": old_props.get("properties", {}).get("start_date", True),
"state": old_props.get("properties", {}).get("state", None), "state": old_props.get("properties", {}).get("state", True),
"sub_issue_count": old_props.get("properties", {}).get("sub_issue_count", None), "sub_issue_count": old_props.get("properties", {}).get("sub_issue_count", True),
"updated_on": old_props.get("properties", {}).get("updated_on", None), "updated_on": old_props.get("properties", {}).get("updated_on", True),
}, },
} }
return new_props return new_props

View File

@ -1,50 +1,42 @@
# Generated by Django 4.2.3 on 2023-09-15 06:55 # Generated by Django 4.2.3 on 2023-09-15 06:55
from django.conf import settings
from django.db import migrations, models from django.db import migrations, models
from django.conf import settings
import django.db.models.deletion import django.db.models.deletion
import uuid import uuid
def update_issue_activity(apps, schema_editor):
IssueActivityModel = apps.get_model("db", "IssueActivity")
updated_issue_activity = []
for obj in IssueActivityModel.objects.all():
if obj.field == "blocks":
obj.field = "blocked_by"
updated_issue_activity.append(obj)
IssueActivityModel.objects.bulk_update(updated_issue_activity, ["field"], batch_size=100)
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('db', '0044_auto_20230913_0709'), ("db", "0044_auto_20230913_0709"),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='GlobalView', name="GlobalView",
fields=[ fields=[
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), ("created_at", models.DateTimeField(auto_now_add=True, verbose_name="Created At"),),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), ("updated_at", models.DateTimeField(auto_now=True, verbose_name="Last Modified At"),),
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), ("id", models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True,),),
('name', models.CharField(max_length=255, verbose_name='View Name')), ("name", models.CharField(max_length=255, verbose_name="View Name")),
('description', models.TextField(blank=True, verbose_name='View Description')), ("description", models.TextField(blank=True, verbose_name="View Description"),),
('query', models.JSONField(verbose_name='View Query')), ("query", models.JSONField(verbose_name="View Query")),
('access', models.PositiveSmallIntegerField(choices=[(0, 'Private'), (1, 'Public')], default=1)), ("access", models.PositiveSmallIntegerField(choices=[(0, "Private"), (1, "Public")], default=1),),
('query_data', models.JSONField(default=dict)), ("query_data", models.JSONField(default=dict)),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), ("created_by", models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="%(class)s_created_by", to=settings.AUTH_USER_MODEL, verbose_name="Created By",),),
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')), ("updated_by", models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="%(class)s_updated_by", to=settings.AUTH_USER_MODEL, verbose_name="Last Modified By",),),
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='global_views', to='db.workspace')), ("workspace", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name="global_views", to="db.workspace",),),
], ],
options={ options={
'verbose_name': 'Global View', "verbose_name": "Global View",
'verbose_name_plural': 'Global Views', "verbose_name_plural": "Global Views",
'db_table': 'global_views', "db_table": "global_views",
'ordering': ('-created_at',), "ordering": ("-created_at",),
}, },
), ),
migrations.RunPython(update_issue_activity), migrations.AddField(
model_name="issueactivity",
name="epoch",
field=models.FloatField(null=True),
),
] ]

View File

@ -1,53 +0,0 @@
# Generated by Django 4.2.3 on 2023-09-19 14:21
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import uuid
def update_epoch(apps, schema_editor):
IssueActivity = apps.get_model('db', 'IssueActivity')
updated_issue_activity = []
for obj in IssueActivity.objects.all():
obj.epoch = int(obj.created_at.timestamp())
updated_issue_activity.append(obj)
IssueActivity.objects.bulk_update(updated_issue_activity, ["epoch"], batch_size=100)
class Migration(migrations.Migration):
dependencies = [
('db', '0045_auto_20230915_0655'),
]
operations = [
migrations.CreateModel(
name='GlobalView',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('name', models.CharField(max_length=255, verbose_name='View Name')),
('description', models.TextField(blank=True, verbose_name='View Description')),
('query', models.JSONField(verbose_name='View Query')),
('access', models.PositiveSmallIntegerField(choices=[(0, 'Private'), (1, 'Public')], default=1)),
('query_data', models.JSONField(default=dict)),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='global_views', to='db.workspace')),
],
options={
'verbose_name': 'Global View',
'verbose_name_plural': 'Global Views',
'db_table': 'global_views',
'ordering': ('-created_at',),
},
),
migrations.AddField(
model_name='issueactivity',
name='epoch',
field=models.FloatField(null=True),
),
migrations.RunPython(update_epoch),
]

View File

@ -0,0 +1,26 @@
# Generated by Django 4.2.5 on 2023-09-26 10:15
from django.db import migrations
def update_issue_activity(apps, schema_editor):
IssueActivity = apps.get_model("db", "IssueActivity")
updated_issue_activity = []
for obj in IssueActivity.objects.all():
obj.epoch = int(obj.created_at.timestamp())
updated_issue_activity.append(obj)
IssueActivity.objects.bulk_update(
updated_issue_activity,
["epoch"],
batch_size=5000,
)
class Migration(migrations.Migration):
dependencies = [
('db', '0045_auto_20230915_0655'),
]
operations = [
migrations.RunPython(update_issue_activity),
]

View File

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

@ -0,0 +1,44 @@
# Generated by Django 4.2.5 on 2023-09-26 10:29
from django.db import migrations
def update_issue_activity_priority(apps, schema_editor):
IssueActivity = apps.get_model("db", "IssueActivity")
updated_issue_activity = []
for obj in IssueActivity.objects.filter(field="priority"):
# Set the old and new value to none if it is empty for 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=1000,
)
def update_issue_activity_blocked(apps, schema_editor):
IssueActivity = apps.get_model("db", "IssueActivity")
updated_issue_activity = []
for obj in IssueActivity.objects.filter(field="blocks"):
# Set the field to blocked_by
obj.field = "blocked_by"
updated_issue_activity.append(obj)
IssueActivity.objects.bulk_update(
updated_issue_activity,
["field"],
batch_size=1000,
)
class Migration(migrations.Migration):
dependencies = [
('db', '0046_auto_20230926_1015'),
]
operations = [
migrations.RunPython(update_issue_activity_priority),
migrations.RunPython(update_issue_activity_blocked),
]

View File

@ -11,6 +11,11 @@ http {
client_max_body_size ${FILE_SIZE_LIMIT}; client_max_body_size ${FILE_SIZE_LIMIT};
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Permissions-Policy "interest-cohort=()" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
location / { location / {
proxy_pass http://web:3000/; proxy_pass http://web:3000/;
} }
@ -20,6 +25,7 @@ http {
} }
location /spaces/ { location /spaces/ {
rewrite ^/spaces/?$ /spaces/login break;
proxy_pass http://space:3000/spaces/; proxy_pass http://space:3000/spaces/;
} }
@ -27,4 +33,4 @@ http {
proxy_pass http://plane-minio:9000/uploads/; proxy_pass http://plane-minio:9000/uploads/;
} }
} }
} }

View File

@ -33,7 +33,7 @@ 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] : "/"; const nextPath = router.asPath.includes("next_path") ? router.asPath.split("/?next_path=")[1] : "/login";
userStore.setCurrentUser(response?.user); userStore.setCurrentUser(response?.user);
@ -41,7 +41,7 @@ export const SignInView = observer(() => {
router.push(`/onboarding?next_path=${nextPath}`); router.push(`/onboarding?next_path=${nextPath}`);
return; return;
} }
router.push((nextPath ?? "/").toString()); router.push((nextPath ?? "/login").toString());
}; };
const handleGoogleSignIn = async ({ clientId, credential }: any) => { const handleGoogleSignIn = async ({ clientId, credential }: any) => {

View File

@ -1 +1 @@
export * from "./home"; export * from "./login";

View File

@ -4,7 +4,7 @@ import { useMobxStore } from "lib/mobx/store-provider";
// components // components
import { SignInView, UserLoggedIn } from "components/accounts"; import { SignInView, UserLoggedIn } from "components/accounts";
export const HomeView = observer(() => { export const LoginView = observer(() => {
const { user: userStore } = useMobxStore(); const { user: userStore } = useMobxStore();
if (!userStore.currentUser) return <SignInView />; if (!userStore.currentUser) return <SignInView />;

View File

@ -1,8 +0,0 @@
import React from "react";
// components
import { HomeView } from "components/views";
const HomePage = () => <HomeView />;
export default HomePage;

View File

@ -0,0 +1,8 @@
import React from "react";
// components
import { LoginView } from "components/views";
const LoginPage = () => <LoginView />;
export default LoginPage;

View File

@ -93,7 +93,7 @@ export const IssuesFilterView: React.FC = () => {
<Tooltip <Tooltip
key={option.type} key={option.type}
tooltipContent={ tooltipContent={
<span className="capitalize">{replaceUnderscoreIfSnakeCase(option.type)} View</span> <span className="capitalize">{replaceUnderscoreIfSnakeCase(option.type)} Layout</span>
} }
position="bottom" position="bottom"
> >

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,31 +34,39 @@ type Props = {
viewProps: IIssueViewProps; viewProps: IIssueViewProps;
}; };
export const SingleBoard: React.FC<Props> = ({ export const SingleBoard: React.FC<Props> = (props) => {
addIssueToGroup, const {
currentState, addIssueToGroup,
groupTitle, currentState,
disableUserActions, groupTitle,
disableAddIssueOption = false, disableUserActions,
dragDisabled, disableAddIssueOption = false,
handleIssueAction, dragDisabled,
handleDraftIssueAction, handleIssueAction,
handleTrashBox, handleDraftIssueAction,
openIssuesListModal, handleTrashBox,
handleMyIssueOpen, openIssuesListModal,
removeIssue, handleMyIssueOpen,
user, removeIssue,
userAuth, user,
viewProps, userAuth,
}) => { 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();
const { cycleId, moduleId } = router.query; const { cycleId, moduleId } = router.query;
const isMyIssuesPage = router.pathname.split("/")[3] === "my-issues";
const isProfileIssuesPage = router.pathname.split("/")[2] === "profile";
const isDraftIssuesPage = router.pathname.split("/")[4] === "draft-issues";
const type = cycleId ? "cycle" : moduleId ? "module" : "issue"; const type = cycleId ? "cycle" : moduleId ? "module" : "issue";
// Check if it has at least 4 tickets since it is enough to accommodate the Calendar height // Check if it has at least 4 tickets since it is enough to accommodate the Calendar height
@ -67,6 +75,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 +141,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" : ""
} `} } `}
@ -170,6 +197,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>
@ -178,7 +218,11 @@ 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={() => {
if (isDraftIssuesPage || isMyIssuesPage || isProfileIssuesPage) {
addIssueToGroup();
} else onCreateClick();
}}
> >
<PlusIcon className="h-4 w-4" /> <PlusIcon className="h-4 w-4" />
Add Issue Add Issue
@ -198,7 +242,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

@ -183,7 +183,10 @@ export const CalendarView: React.FC<Props> = ({
{calendarIssues ? ( {calendarIssues ? (
<div className="h-full overflow-y-auto"> <div className="h-full overflow-y-auto">
<DragDropContext onDragEnd={onDragEnd}> <DragDropContext onDragEnd={onDragEnd}>
<div className="h-full rounded-lg p-8 text-custom-text-200"> <div
id={`calendar-view-${cycleId ?? moduleId ?? viewId}`}
className="h-full rounded-lg p-8 text-custom-text-200"
>
<CalendarHeader <CalendarHeader
isMonthlyView={isMonthlyView} isMonthlyView={isMonthlyView}
setIsMonthlyView={setIsMonthlyView} setIsMonthlyView={setIsMonthlyView}

View File

@ -0,0 +1,102 @@
import { useEffect, useRef, useState } from "react";
// next
import { useRouter } from "next/router";
// 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>;
dependencies: any[];
};
const useCheckIfThereIsSpaceOnRight = (ref: React.RefObject<HTMLDivElement>, deps: any[]) => {
const [isThereSpaceOnRight, setIsThereSpaceOnRight] = useState(true);
const router = useRouter();
const { moduleId, cycleId, viewId } = router.query;
const container = document.getElementById(`calendar-view-${cycleId ?? moduleId ?? viewId}`);
useEffect(() => {
if (!ref.current) return;
const { right } = ref.current.getBoundingClientRect();
const width = right;
const innerWidth = container?.getBoundingClientRect().width ?? window.innerWidth;
if (width > innerWidth) setIsThereSpaceOnRight(false);
else setIsThereSpaceOnRight(true);
}, [ref, deps, container]);
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, dependencies } = props;
const ref = useRef<HTMLDivElement>(null);
const isSpaceOnRight = useCheckIfThereIsSpaceOnRight(ref, dependencies);
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;
@ -79,6 +80,18 @@ export const SingleCalendarDate: React.FC<Props> = ({
)} )}
</Draggable> </Draggable>
))} ))}
<CalendarInlineCreateIssueForm
isOpen={isCreateIssueFormOpen}
dependencies={[showWeekEnds]}
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"
@ -94,7 +107,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

@ -0,0 +1,273 @@
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,
PROJECT_DRAFT_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 isDraftIssues = router.pathname?.split("/")?.[4] === "draft-issues";
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 (!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 (!isDraftIssues
? issuesService.createIssues(workspaceSlug.toString(), projectId.toString(), formData, user)
: issuesService.createDraftIssue(
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 (isDraftIssues)
await mutate(PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS(projectId.toString() ?? "", params));
if (displayFilters.layout === "calendar") await mutate(calendarFetchKey);
if (displayFilters.layout === "gantt_chart") await mutate(ganttFetchKey);
if (displayFilters.layout === "spreadsheet") await mutate(spreadsheetFetchKey);
if (groupedIssues) await 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

@ -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 "../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>
{props.isOpen && (
<p className="text-xs ml-3 mt-3 italic text-custom-text-200">
Press {"'"}Enter{"'"} to add another issue
</p>
)}
</>
);

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
@ -31,7 +34,7 @@ import {
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, WORKSPACE_LABELS } from "constants/fetch-keys";
// constants // constants
import { STATE_GROUP_COLORS } from "constants/state"; import { STATE_GROUP_COLORS } from "constants/state";
@ -51,40 +54,65 @@ type Props = {
viewProps: IIssueViewProps; viewProps: IIssueViewProps;
}; };
export const SingleList: React.FC<Props> = ({ export const SingleList: React.FC<Props> = (props) => {
currentState, const {
groupTitle, currentState,
addIssueToGroup, groupTitle,
handleIssueAction, handleIssueAction,
openIssuesListModal, openIssuesListModal,
handleDraftIssueAction, handleDraftIssueAction,
handleMyIssueOpen, handleMyIssueOpen,
removeIssue, addIssueToGroup,
disableUserActions, removeIssue,
disableAddIssueOption = false, disableUserActions,
user, disableAddIssueOption = false,
userAuth, user,
viewProps, userAuth,
}) => { 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 isMyIssuesPage = router.pathname.split("/")[3] === "my-issues";
const isProfileIssuesPage = router.pathname.split("/")[2] === "profile";
const isDraftIssuesPage = router.pathname.split("/")[4] === "draft-issues";
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";
const { displayFilters, groupedIssues } = viewProps; const { displayFilters, groupedIssues } = viewProps;
const { data: issueLabels } = useSWR<IIssueLabels[]>( const { data: issueLabels } = useSWR(
workspaceSlug && projectId ? PROJECT_ISSUE_LABELS(projectId as string) : null, workspaceSlug && projectId && displayFilters?.group_by === "labels"
workspaceSlug && projectId ? PROJECT_ISSUE_LABELS(projectId.toString())
? () => issuesService.getIssueLabels(workspaceSlug as string, projectId as string) : null,
workspaceSlug && projectId && displayFilters?.group_by === "labels"
? () => issuesService.getIssueLabels(workspaceSlug.toString(), projectId.toString())
: null
);
const { data: workspaceLabels } = useSWR(
workspaceSlug && displayFilters?.group_by === "labels"
? WORKSPACE_LABELS(workspaceSlug.toString())
: null,
workspaceSlug && displayFilters?.group_by === "labels"
? () => issuesService.getWorkspaceLabels(workspaceSlug.toString())
: null : null
); );
const { data: members } = useSWR( const { data: members } = useSWR(
workspaceSlug && projectId ? PROJECT_MEMBERS(projectId as string) : null, workspaceSlug &&
workspaceSlug && projectId projectId &&
(displayFilters?.group_by === "created_by" || displayFilters?.group_by === "assignees")
? PROJECT_MEMBERS(projectId as string)
: null,
workspaceSlug &&
projectId &&
(displayFilters?.group_by === "created_by" || displayFilters?.group_by === "assignees")
? () => projectService.projectMembers(workspaceSlug as string, projectId as string) ? () => projectService.projectMembers(workspaceSlug as string, projectId as string)
: null : null
); );
@ -99,7 +127,10 @@ export const SingleList: React.FC<Props> = ({
title = addSpaceIfCamelCase(currentState?.name ?? ""); title = addSpaceIfCamelCase(currentState?.name ?? "");
break; break;
case "labels": case "labels":
title = issueLabels?.find((label) => label.id === groupTitle)?.name ?? "None"; title =
[...(issueLabels ?? []), ...(workspaceLabels ?? [])]?.find(
(label) => label.id === groupTitle
)?.name ?? "None";
break; break;
case "project": case "project":
title = projects?.find((p) => p.id === groupTitle)?.name ?? "None"; title = projects?.find((p) => p.id === groupTitle)?.name ?? "None";
@ -153,7 +184,9 @@ export const SingleList: React.FC<Props> = ({
break; break;
case "labels": case "labels":
const labelColor = const labelColor =
issueLabels?.find((label) => label.id === groupTitle)?.color ?? "#000000"; [...(issueLabels ?? []), ...(workspaceLabels ?? [])]?.find(
(label) => label.id === groupTitle
)?.color ?? "#000000";
icon = ( icon = (
<span <span
className="h-3 w-3 flex-shrink-0 rounded-full" className="h-3 w-3 flex-shrink-0 rounded-full"
@ -207,7 +240,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 +257,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
@ -285,6 +320,33 @@ 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 && !disableAddIssueOption}
handleClose={() => setIsCreateIssueFormOpen(false)}
prePopulatedData={{
...(cycleId && { cycle: cycleId.toString() }),
...(moduleId && { module: moduleId.toString() }),
[displayFilters?.group_by!]: groupTitle,
}}
/>
{!disableAddIssueOption && !isCreateIssueFormOpen && (
<div className="w-full bg-custom-background-100 px-6 py-3">
<button
type="button"
onClick={() => {
if (isDraftIssuesPage || isMyIssuesPage || isProfileIssuesPage) {
addIssueToGroup();
} else 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

@ -149,6 +149,10 @@ export const SingleCycleList: React.FC<TSingleStatProps> = ({
color: group.color, color: group.color,
})); }));
const completedIssues = cycle.completed_issues + cycle.cancelled_issues;
const percentage = cycle.total_issues > 0 ? (completedIssues / cycle.total_issues) * 100 : 0;
return ( return (
<div> <div>
<div className="flex flex-col text-xs hover:bg-custom-background-80"> <div className="flex flex-col text-xs hover:bg-custom-background-80">
@ -307,7 +311,7 @@ export const SingleCycleList: React.FC<TSingleStatProps> = ({
) : cycleStatus === "completed" ? ( ) : cycleStatus === "completed" ? (
<span className="flex gap-1"> <span className="flex gap-1">
<RadialProgressBar progress={100} /> <RadialProgressBar progress={100} />
<span>{100} %</span> <span>{Math.round(percentage)} %</span>
</span> </span>
) : ( ) : (
<span className="flex gap-1"> <span className="flex gap-1">

View File

@ -85,6 +85,7 @@ export const GanttSidebar: React.FC<Props> = ({
<StrictModeDroppable droppableId="gantt-sidebar"> <StrictModeDroppable droppableId="gantt-sidebar">
{(droppableProvided) => ( {(droppableProvided) => (
<div <div
id={`gantt-sidebar-${cycleId}`}
className="h-full overflow-y-auto pl-2.5" className="h-full overflow-y-auto pl-2.5"
ref={droppableProvided.innerRef} ref={droppableProvided.innerRef}
{...droppableProvided.droppableProps} {...droppableProvided.droppableProps}
@ -151,6 +152,42 @@ export const GanttSidebar: React.FC<Props> = ({
</div> </div>
)} )}
</StrictModeDroppable> </StrictModeDroppable>
<div className="pl-2.5">
<GanttInlineCreateIssueForm
isOpen={isCreateIssueFormOpen}
handleClose={() => setIsCreateIssueFormOpen(false)}
onSuccess={() => {
const ganttSidebar = document.getElementById(`gantt-sidebar-${cycleId}`);
const timeoutId = setTimeout(() => {
if (ganttSidebar)
ganttSidebar.scrollBy({
top: ganttSidebar.scrollHeight,
left: 0,
behavior: "smooth",
});
clearTimeout(timeoutId);
}, 10);
}}
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>
</DragDropContext> </DragDropContext>
); );
}; };

View File

@ -7,9 +7,9 @@ import { useRouter } from "next/router";
import { ActivityIcon, ActivityMessage } from "components/core"; import { ActivityIcon, ActivityMessage } from "components/core";
import { CommentCard } from "components/issues/comment"; import { CommentCard } from "components/issues/comment";
// ui // ui
import { Icon, Loader } from "components/ui"; import { Icon, Loader, Tooltip } from "components/ui";
// helpers // helpers
import { timeAgo } from "helpers/date-time.helper"; import { render24HourFormatTime, renderLongDateFormat, timeAgo } from "helpers/date-time.helper";
// types // types
import { IIssueActivity, IIssueComment } from "types"; import { IIssueActivity, IIssueComment } from "types";
@ -120,9 +120,15 @@ export const IssueActivitySection: React.FC<Props> = ({
</Link> </Link>
)}{" "} )}{" "}
{message}{" "} {message}{" "}
<span className="whitespace-nowrap"> <Tooltip
{timeAgo(activityItem.created_at)} tooltipContent={`${renderLongDateFormat(
</span> activityItem.created_at
)}, ${render24HourFormatTime(activityItem.created_at)}`}
>
<span className="whitespace-nowrap">
{timeAgo(activityItem.created_at)}
</span>
</Tooltip>
</div> </div>
</div> </div>
</div> </div>

View File

@ -4,6 +4,8 @@ import { useRouter } from "next/router";
import { mutate } from "swr"; import { mutate } from "swr";
import useUser from "hooks/use-user";
// headless ui // headless ui
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
// services // services
@ -16,7 +18,7 @@ import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
// ui // ui
import { SecondaryButton, DangerButton } from "components/ui"; import { SecondaryButton, DangerButton } from "components/ui";
// types // types
import type { IIssue, ICurrentUserResponse } from "types"; import type { IIssue } from "types";
// fetch-keys // fetch-keys
import { PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS } from "constants/fetch-keys"; import { PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS } from "constants/fetch-keys";
@ -24,12 +26,11 @@ type Props = {
isOpen: boolean; isOpen: boolean;
handleClose: () => void; handleClose: () => void;
data: IIssue | null; data: IIssue | null;
user?: ICurrentUserResponse;
onSubmit?: () => Promise<void> | void; onSubmit?: () => Promise<void> | void;
}; };
export const DeleteDraftIssueModal: React.FC<Props> = (props) => { export const DeleteDraftIssueModal: React.FC<Props> = (props) => {
const { isOpen, handleClose, data, user, onSubmit } = props; const { isOpen, handleClose, data, onSubmit } = props;
const [isDeleteLoading, setIsDeleteLoading] = useState(false); const [isDeleteLoading, setIsDeleteLoading] = useState(false);
@ -40,6 +41,8 @@ export const DeleteDraftIssueModal: React.FC<Props> = (props) => {
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
const { user } = useUser();
useEffect(() => { useEffect(() => {
setIsDeleteLoading(false); setIsDeleteLoading(false);
}, [isOpen]); }, [isOpen]);

View File

@ -66,6 +66,7 @@ interface IssueFormProps {
createMore: boolean; createMore: boolean;
setCreateMore: React.Dispatch<React.SetStateAction<boolean>>; setCreateMore: React.Dispatch<React.SetStateAction<boolean>>;
handleClose: () => void; handleClose: () => void;
handleDiscard: () => void;
status: boolean; status: boolean;
user: ICurrentUserResponse | undefined; user: ICurrentUserResponse | undefined;
fieldsToShow: ( fieldsToShow: (
@ -97,6 +98,7 @@ export const DraftIssueForm: FC<IssueFormProps> = (props) => {
status, status,
user, user,
fieldsToShow, fieldsToShow,
handleDiscard,
} = props; } = props;
const [stateModal, setStateModal] = useState(false); const [stateModal, setStateModal] = useState(false);
@ -569,7 +571,7 @@ export const DraftIssueForm: FC<IssueFormProps> = (props) => {
<ToggleSwitch value={createMore} onChange={() => {}} size="md" /> <ToggleSwitch value={createMore} onChange={() => {}} size="md" />
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<SecondaryButton onClick={onClose}>Discard</SecondaryButton> <SecondaryButton onClick={handleDiscard}>Discard</SecondaryButton>
<SecondaryButton <SecondaryButton
loading={isSubmitting} loading={isSubmitting}
onClick={handleSubmit((formData) => onClick={handleSubmit((formData) =>

View File

@ -97,6 +97,11 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = (props) =
setActiveProject(null); setActiveProject(null);
}; };
const onDiscard = () => {
clearDraftIssueLocalStorage();
onClose();
};
useEffect(() => { useEffect(() => {
setPreloadedData(prePopulateDataProps ?? {}); setPreloadedData(prePopulateDataProps ?? {});
@ -141,7 +146,7 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = (props) =
if (prePopulateData && prePopulateData.project && !activeProject) if (prePopulateData && prePopulateData.project && !activeProject)
return setActiveProject(prePopulateData.project); return setActiveProject(prePopulateData.project);
if (prePopulateData && prePopulateData.project) if (prePopulateData && prePopulateData.project && !activeProject)
return setActiveProject(prePopulateData.project); return setActiveProject(prePopulateData.project);
// if data is not present, set active project to the project // if data is not present, set active project to the project
@ -180,16 +185,8 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = (props) =
await issuesService await issuesService
.createDraftIssue(workspaceSlug as string, activeProject ?? "", payload, user) .createDraftIssue(workspaceSlug as string, activeProject ?? "", payload, user)
.then(async () => { .then(async () => {
mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params));
mutate(PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params)); mutate(PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params));
if (displayFilters.layout === "calendar") mutate(calendarFetchKey);
if (displayFilters.layout === "gantt_chart")
mutate(ganttFetchKey, {
start_target_date: true,
order_by: "sort_order",
});
if (displayFilters.layout === "spreadsheet") mutate(spreadsheetFetchKey);
if (groupedIssues) mutateMyIssues(); if (groupedIssues) mutateMyIssues();
setToastAlert({ setToastAlert({
@ -200,8 +197,6 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = (props) =
if (payload.assignees_list?.some((assignee) => assignee === user?.id)) if (payload.assignees_list?.some((assignee) => assignee === user?.id))
mutate(USER_ISSUE(workspaceSlug as string)); mutate(USER_ISSUE(workspaceSlug as string));
if (payload.parent && payload.parent !== "") mutate(SUB_ISSUES(payload.parent));
}) })
.catch(() => { .catch(() => {
setToastAlert({ setToastAlert({
@ -396,6 +391,7 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = (props) =
createMore={createMore} createMore={createMore}
setCreateMore={setCreateMore} setCreateMore={setCreateMore}
handleClose={onClose} handleClose={onClose}
handleDiscard={onDiscard}
projectId={activeProject ?? ""} projectId={activeProject ?? ""}
setActiveProject={setActiveProject} setActiveProject={setActiveProject}
status={data ? true : false} status={data ? true : false}

View File

@ -51,7 +51,7 @@ export const MyIssuesViewOptions: React.FC = () => {
<Tooltip <Tooltip
key={option.type} key={option.type}
tooltipContent={ tooltipContent={
<span className="capitalize">{replaceUnderscoreIfSnakeCase(option.type)} View</span> <span className="capitalize">{replaceUnderscoreIfSnakeCase(option.type)} Layout</span>
} }
position="bottom" position="bottom"
> >

View File

@ -1,6 +1,7 @@
import React from "react"; import React from "react";
// next imports // next imports
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router";
// lucide icons // lucide icons
import { import {
ChevronDown, ChevronDown,
@ -37,6 +38,7 @@ export interface ISubIssues {
issueId: string, issueId: string,
issue?: IIssue | null issue?: IIssue | null
) => void; ) => void;
setPeekParentId: (id: string) => void;
} }
export const SubIssues: React.FC<ISubIssues> = ({ export const SubIssues: React.FC<ISubIssues> = ({
@ -52,38 +54,54 @@ export const SubIssues: React.FC<ISubIssues> = ({
handleIssuesLoader, handleIssuesLoader,
copyText, copyText,
handleIssueCrudOperation, handleIssueCrudOperation,
}) => ( setPeekParentId,
<div> }) => {
{issue && ( const router = useRouter();
<div
className="relative flex items-center gap-2 py-1 px-2 w-full h-full hover:bg-custom-background-90 group transition-all border-b border-custom-border-100"
style={{ paddingLeft: `${spacingLeft}px` }}
>
<div className="flex-shrink-0 w-[22px] h-[22px]">
{issue?.sub_issues_count > 0 && (
<>
{issuesLoader.sub_issues.includes(issue?.id) ? (
<div className="w-full h-full flex justify-center items-center rounded-sm bg-custom-background-80 transition-all cursor-not-allowed">
<Loader width={14} strokeWidth={2} className="animate-spin" />
</div>
) : (
<div
className="w-full h-full flex justify-center items-center rounded-sm hover:bg-custom-background-80 transition-all cursor-pointer"
onClick={() => handleIssuesLoader({ key: "visibility", issueId: issue?.id })}
>
{issuesLoader && issuesLoader.visibility.includes(issue?.id) ? (
<ChevronDown width={14} strokeWidth={2} />
) : (
<ChevronRight width={14} strokeWidth={2} />
)}
</div>
)}
</>
)}
</div>
<Link href={`/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`}> const openPeekOverview = (issue_id: string) => {
<a className="w-full flex items-center gap-2"> const { query } = router;
setPeekParentId(parentIssue?.id);
router.push({
pathname: router.pathname,
query: { ...query, peekIssue: issue_id },
});
};
return (
<div>
{issue && (
<div
className="relative flex items-center gap-2 py-1 px-2 w-full h-full hover:bg-custom-background-90 group transition-all border-b border-custom-border-100"
style={{ paddingLeft: `${spacingLeft}px` }}
>
<div className="flex-shrink-0 w-[22px] h-[22px]">
{issue?.sub_issues_count > 0 && (
<>
{issuesLoader.sub_issues.includes(issue?.id) ? (
<div className="w-full h-full flex justify-center items-center rounded-sm bg-custom-background-80 transition-all cursor-not-allowed">
<Loader width={14} strokeWidth={2} className="animate-spin" />
</div>
) : (
<div
className="w-full h-full flex justify-center items-center rounded-sm hover:bg-custom-background-80 transition-all cursor-pointer"
onClick={() => handleIssuesLoader({ key: "visibility", issueId: issue?.id })}
>
{issuesLoader && issuesLoader.visibility.includes(issue?.id) ? (
<ChevronDown width={14} strokeWidth={2} />
) : (
<ChevronRight width={14} strokeWidth={2} />
)}
</div>
)}
</>
)}
</div>
<div
className="w-full flex items-center gap-2 cursor-pointer"
onClick={() => openPeekOverview(issue?.id)}
>
<div <div
className="flex-shrink-0 w-[6px] h-[6px] rounded-full" className="flex-shrink-0 w-[6px] h-[6px] rounded-full"
style={{ style={{
@ -96,93 +114,94 @@ export const SubIssues: React.FC<ISubIssues> = ({
<Tooltip tooltipHeading="Title" tooltipContent={`${issue?.name}`}> <Tooltip tooltipHeading="Title" tooltipContent={`${issue?.name}`}>
<div className="line-clamp-1 text-xs text-custom-text-100 pr-2">{issue?.name}</div> <div className="line-clamp-1 text-xs text-custom-text-100 pr-2">{issue?.name}</div>
</Tooltip> </Tooltip>
</a> </div>
</Link>
<div className="flex-shrink-0 text-sm"> <div className="flex-shrink-0 text-sm">
<IssueProperty <IssueProperty
workspaceSlug={workspaceSlug} workspaceSlug={workspaceSlug}
projectId={projectId} projectId={projectId}
parentIssue={parentIssue} parentIssue={parentIssue}
issue={issue} issue={issue}
user={user} user={user}
editable={editable} editable={editable}
/> />
</div> </div>
<div className="flex-shrink-0 text-sm">
<CustomMenu width="auto" ellipsis>
{editable && (
<CustomMenu.MenuItem
onClick={() => handleIssueCrudOperation("edit", parentIssue?.id, issue)}
>
<div className="flex items-center justify-start gap-2">
<Pencil width={14} strokeWidth={2} />
<span>Edit issue</span>
</div>
</CustomMenu.MenuItem>
)}
{editable && (
<CustomMenu.MenuItem
onClick={() => handleIssueCrudOperation("delete", parentIssue?.id, issue)}
>
<div className="flex items-center justify-start gap-2">
<Trash width={14} strokeWidth={2} />
<span>Delete issue</span>
</div>
</CustomMenu.MenuItem>
)}
<div className="flex-shrink-0 text-sm">
<CustomMenu width="auto" ellipsis>
{editable && (
<CustomMenu.MenuItem <CustomMenu.MenuItem
onClick={() => handleIssueCrudOperation("edit", parentIssue?.id, issue)} onClick={() =>
copyText(`${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`)
}
> >
<div className="flex items-center justify-start gap-2"> <div className="flex items-center justify-start gap-2">
<Pencil width={14} strokeWidth={2} /> <LinkIcon width={14} strokeWidth={2} />
<span>Edit issue</span> <span>Copy issue link</span>
</div> </div>
</CustomMenu.MenuItem> </CustomMenu.MenuItem>
)} </CustomMenu>
</div>
{editable && ( {editable && (
<CustomMenu.MenuItem <>
onClick={() => handleIssueCrudOperation("delete", parentIssue?.id, issue)} {issuesLoader.delete.includes(issue?.id) ? (
> <div className="flex-shrink-0 w-[22px] h-[22px] rounded-sm bg-red-200/10 text-red-500 transition-all cursor-not-allowed overflow-hidden flex justify-center items-center">
<div className="flex items-center justify-start gap-2"> <Loader width={14} strokeWidth={2} className="animate-spin" />
<Trash width={14} strokeWidth={2} />
<span>Delete issue</span>
</div> </div>
</CustomMenu.MenuItem> ) : (
)} <div
className="flex-shrink-0 invisible group-hover:visible w-[22px] h-[22px] rounded-sm hover:bg-custom-background-80 transition-all cursor-pointer overflow-hidden flex justify-center items-center"
<CustomMenu.MenuItem onClick={() => {
onClick={() => handleIssuesLoader({ key: "delete", issueId: issue?.id });
copyText(`${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`) removeIssueFromSubIssues(parentIssue?.id, issue);
} }}
> >
<div className="flex items-center justify-start gap-2"> <X width={14} strokeWidth={2} />
<LinkIcon width={14} strokeWidth={2} /> </div>
<span>Copy issue link</span> )}
</div> </>
</CustomMenu.MenuItem> )}
</CustomMenu>
</div> </div>
)}
{editable && ( {issuesLoader.visibility.includes(issue?.id) && issue?.sub_issues_count > 0 && (
<> <SubIssuesRootList
{issuesLoader.delete.includes(issue?.id) ? ( workspaceSlug={workspaceSlug}
<div className="flex-shrink-0 w-[22px] h-[22px] rounded-sm bg-red-200/10 text-red-500 transition-all cursor-not-allowed overflow-hidden flex justify-center items-center"> projectId={projectId}
<Loader width={14} strokeWidth={2} className="animate-spin" /> parentIssue={issue}
</div> spacingLeft={spacingLeft + 22}
) : ( user={user}
<div editable={editable}
className="flex-shrink-0 invisible group-hover:visible w-[22px] h-[22px] rounded-sm hover:bg-custom-background-80 transition-all cursor-pointer overflow-hidden flex justify-center items-center" removeIssueFromSubIssues={removeIssueFromSubIssues}
onClick={() => { issuesLoader={issuesLoader}
handleIssuesLoader({ key: "delete", issueId: issue?.id }); handleIssuesLoader={handleIssuesLoader}
removeIssueFromSubIssues(parentIssue?.id, issue); copyText={copyText}
}} handleIssueCrudOperation={handleIssueCrudOperation}
> setPeekParentId={setPeekParentId}
<X width={14} strokeWidth={2} /> />
</div> )}
)} </div>
</> );
)} };
</div>
)}
{issuesLoader.visibility.includes(issue?.id) && issue?.sub_issues_count > 0 && (
<SubIssuesRootList
workspaceSlug={workspaceSlug}
projectId={projectId}
parentIssue={issue}
spacingLeft={spacingLeft + 22}
user={user}
editable={editable}
removeIssueFromSubIssues={removeIssueFromSubIssues}
issuesLoader={issuesLoader}
handleIssuesLoader={handleIssuesLoader}
copyText={copyText}
handleIssueCrudOperation={handleIssueCrudOperation}
/>
)}
</div>
);

View File

@ -27,6 +27,7 @@ export interface ISubIssuesRootList {
issueId: string, issueId: string,
issue?: IIssue | null issue?: IIssue | null
) => void; ) => void;
setPeekParentId: (id: string) => void;
} }
export const SubIssuesRootList: React.FC<ISubIssuesRootList> = ({ export const SubIssuesRootList: React.FC<ISubIssuesRootList> = ({
@ -41,6 +42,7 @@ export const SubIssuesRootList: React.FC<ISubIssuesRootList> = ({
handleIssuesLoader, handleIssuesLoader,
copyText, copyText,
handleIssueCrudOperation, handleIssueCrudOperation,
setPeekParentId,
}) => { }) => {
const { data: issues, isLoading } = useSWR( const { data: issues, isLoading } = useSWR(
workspaceSlug && projectId && parentIssue && parentIssue?.id workspaceSlug && projectId && parentIssue && parentIssue?.id
@ -81,6 +83,7 @@ export const SubIssuesRootList: React.FC<ISubIssuesRootList> = ({
handleIssuesLoader={handleIssuesLoader} handleIssuesLoader={handleIssuesLoader}
copyText={copyText} copyText={copyText}
handleIssueCrudOperation={handleIssueCrudOperation} handleIssueCrudOperation={handleIssueCrudOperation}
setPeekParentId={setPeekParentId}
/> />
))} ))}

View File

@ -10,6 +10,7 @@ import { ExistingIssuesListModal } from "components/core";
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
import { SubIssuesRootList } from "./issues-list"; import { SubIssuesRootList } from "./issues-list";
import { ProgressBar } from "./progressbar"; import { ProgressBar } from "./progressbar";
import { IssuePeekOverview } from "components/issues/peek-overview";
// ui // ui
import { CustomMenu } from "components/ui"; import { CustomMenu } from "components/ui";
// hooks // hooks
@ -41,7 +42,11 @@ export interface ISubIssuesRootLoadersHandler {
export const SubIssuesRoot: React.FC<ISubIssuesRoot> = ({ parentIssue, user }) => { export const SubIssuesRoot: React.FC<ISubIssuesRoot> = ({ parentIssue, user }) => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string }; const { workspaceSlug, projectId, peekIssue } = router.query as {
workspaceSlug: string;
projectId: string;
peekIssue: string;
};
const { memberRole } = useProjectMyMembership(); const { memberRole } = useProjectMyMembership();
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
@ -55,6 +60,8 @@ export const SubIssuesRoot: React.FC<ISubIssuesRoot> = ({ parentIssue, user }) =
: null : null
); );
const [peekParentId, setPeekParentId] = React.useState<string | null>("");
const [issuesLoader, setIssuesLoader] = React.useState<ISubIssuesRootLoaders>({ const [issuesLoader, setIssuesLoader] = React.useState<ISubIssuesRootLoaders>({
visibility: [parentIssue?.id], visibility: [parentIssue?.id],
delete: [], delete: [],
@ -230,15 +237,48 @@ export const SubIssuesRoot: React.FC<ISubIssuesRoot> = ({ parentIssue, user }) =
handleIssuesLoader={handleIssuesLoader} handleIssuesLoader={handleIssuesLoader}
copyText={copyText} copyText={copyText}
handleIssueCrudOperation={handleIssueCrudOperation} handleIssueCrudOperation={handleIssueCrudOperation}
setPeekParentId={setPeekParentId}
/> />
</div> </div>
)} )}
<div>
<CustomMenu
label={
<>
<Plus className="h-3 w-3" />
Add sub-issue
</>
}
buttonClassName="whitespace-nowrap"
position="left"
noBorder
noChevron
>
<CustomMenu.MenuItem
onClick={() => {
mutateSubIssues(parentIssue?.id);
handleIssueCrudOperation("create", parentIssue?.id);
}}
>
Create new
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
onClick={() => {
mutateSubIssues(parentIssue?.id);
handleIssueCrudOperation("existing", parentIssue?.id);
}}
>
Add an existing issue
</CustomMenu.MenuItem>
</CustomMenu>
</div>
</> </>
) : ( ) : (
isEditable && ( isEditable && (
<div className="text-xs py-2 text-custom-text-300 font-medium"> <div className="flex justify-between items-center">
<div className="py-3 text-center">No sub issues are available</div> <div className="text-xs py-2 text-custom-text-300 italic">No Sub-Issues yet</div>
<> <div>
<CustomMenu <CustomMenu
label={ label={
<> <>
@ -268,7 +308,7 @@ export const SubIssuesRoot: React.FC<ISubIssuesRoot> = ({ parentIssue, user }) =
Add an existing issue Add an existing issue
</CustomMenu.MenuItem> </CustomMenu.MenuItem>
</CustomMenu> </CustomMenu>
</> </div>
</div> </div>
) )
)} )}
@ -323,6 +363,13 @@ export const SubIssuesRoot: React.FC<ISubIssuesRoot> = ({ parentIssue, user }) =
)} )}
</> </>
)} )}
<IssuePeekOverview
handleMutation={() => peekParentId && peekIssue && mutateSubIssues(peekParentId)}
projectId={projectId ?? ""}
workspaceSlug={workspaceSlug ?? ""}
readOnly={!isEditable}
/>
</div> </div>
); );
}; };

View File

@ -1,11 +1,13 @@
import React from "react"; import React, { useRef, useState } from "react";
//hook
import useOutsideClickDetector from "hooks/use-outside-click-detector";
// ui // ui
import { CustomMenu } from "components/ui"; import { CustomMenu } from "components/ui";
// types // types
import { IIssueLabels } from "types"; import { IIssueLabels } from "types";
//icons //icons
import { RectangleGroupIcon, PencilIcon } from "@heroicons/react/24/outline"; import { PencilIcon } from "@heroicons/react/24/outline";
import { Component, X } from "lucide-react"; import { Component, X } from "lucide-react";
type Props = { type Props = {
@ -20,9 +22,14 @@ export const SingleLabel: React.FC<Props> = ({
addLabelToGroup, addLabelToGroup,
editLabel, editLabel,
handleLabelDelete, handleLabelDelete,
}) => ( }) => {
<div className="gap-2 space-y-3 divide-y divide-custom-border-200 rounded border border-custom-border-200 bg-custom-background-100 px-4 py-2.5"> const [isMenuActive, setIsMenuActive] = useState(false);
<div className="group flex items-center justify-between"> const actionSectionRef = useRef<HTMLDivElement | null>(null);
useOutsideClickDetector(actionSectionRef, () => setIsMenuActive(false));
return (
<div className="relative group flex items-center justify-between gap-2 space-y-3 rounded border border-custom-border-200 bg-custom-background-100 px-4 py-2.5">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<span <span
className="h-3.5 w-3.5 flex-shrink-0 rounded-full" className="h-3.5 w-3.5 flex-shrink-0 rounded-full"
@ -32,36 +39,41 @@ export const SingleLabel: React.FC<Props> = ({
/> />
<h6 className="text-sm">{label.name}</h6> <h6 className="text-sm">{label.name}</h6>
</div> </div>
<div className="flex items-center gap-3.5 pointer-events-none opacity-0 group-hover:pointer-events-auto group-hover:opacity-100"> <div
<div className="h-4 w-4"> ref={actionSectionRef}
<CustomMenu className={`absolute -top-0.5 right-3 flex items-start gap-3.5 pointer-events-none opacity-0 group-hover:pointer-events-auto group-hover:opacity-100 ${
customButton={ isMenuActive ? "opacity-100" : ""
<div className="h-4 w-4"> }`}
<Component className="h-4 w-4 leading-4 text-custom-sidebar-text-400 flex-shrink-0" /> >
</div> <CustomMenu
} customButton={
<div className="h-4 w-4" onClick={() => setIsMenuActive(!isMenuActive)}>
<Component className="h-4 w-4 leading-4 text-custom-sidebar-text-400 flex-shrink-0" />
</div>
}
>
<CustomMenu.MenuItem onClick={() => addLabelToGroup(label)}>
<span className="flex items-center justify-start gap-2">
<Component className="h-4 w-4 leading-4 text-custom-sidebar-text-400 flex-shrink-0" />
<span>Convert to group</span>
</span>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem onClick={() => editLabel(label)}>
<span className="flex items-center justify-start gap-2">
<PencilIcon className="h-4 w-4" />
<span>Edit label</span>
</span>
</CustomMenu.MenuItem>
</CustomMenu>
<div className="py-0.5">
<button
className="flex h-4 w-4 items-center justify-start gap-2"
onClick={handleLabelDelete}
> >
<CustomMenu.MenuItem onClick={() => addLabelToGroup(label)}> <X className="h-4 w-4 text-custom-sidebar-text-400 flex-shrink-0" />
<span className="flex items-center justify-start gap-2">
<Component className="h-4 w-4 leading-4 text-custom-sidebar-text-400 flex-shrink-0" />
<span>Convert to group</span>
</span>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem onClick={() => editLabel(label)}>
<span className="flex items-center justify-start gap-2">
<PencilIcon className="h-4 w-4" />
<span>Edit label</span>
</span>
</CustomMenu.MenuItem>
</CustomMenu>
</div>
<div className="flex items-center">
<button className="flex items-center justify-start gap-2" onClick={handleLabelDelete}>
<X className="h-[18px] w-[18px] text-custom-sidebar-text-400 flex-shrink-0" />
</button> </button>
</div> </div>
</div> </div>
</div> </div>
</div> );
); };

View File

@ -81,7 +81,7 @@ export const ProfileIssuesViewOptions: React.FC = () => {
<Tooltip <Tooltip
key={option.type} key={option.type}
tooltipContent={ tooltipContent={
<span className="capitalize">{replaceUnderscoreIfSnakeCase(option.type)} View</span> <span className="capitalize">{replaceUnderscoreIfSnakeCase(option.type)} Layout</span>
} }
position="bottom" position="bottom"
> >

View File

@ -227,7 +227,8 @@ export const ProfileIssuesView = () => {
router.pathname.includes("my-issues")) ?? router.pathname.includes("my-issues")) ??
false; false;
const disableAddIssueOption = isSubscribedIssuesRoute || isMySubscribedIssues; const disableAddIssueOption =
isSubscribedIssuesRoute || isMySubscribedIssues || user?.id !== userId;
return ( return (
<> <>

View File

@ -44,7 +44,9 @@ export const WorkspaceSidebarQuickAction = () => {
> >
<button <button
type="button" type="button"
className="relative flex items-center gap-2 flex-grow rounded flex-shrink-0 py-1.5" className={`relative flex items-center gap-2 flex-grow rounded flex-shrink-0 py-1.5 ${
store?.theme?.sidebarCollapsed ? "justify-center" : ""
}`}
onClick={() => { onClick={() => {
const e = new KeyboardEvent("keydown", { key: "c" }); const e = new KeyboardEvent("keydown", { key: "c" });
document.dispatchEvent(e); document.dispatchEvent(e);
@ -61,11 +63,17 @@ export const WorkspaceSidebarQuickAction = () => {
{storedValue && Object.keys(JSON.parse(storedValue)).length > 0 && ( {storedValue && Object.keys(JSON.parse(storedValue)).length > 0 && (
<> <>
<div className="h-8 w-0.5 bg-custom-sidebar-background-80" /> <div
className={`h-8 w-0.5 bg-custom-sidebar-background-80 ${
store?.theme?.sidebarCollapsed ? "hidden" : "block"
}`}
/>
<button <button
type="button" type="button"
className="flex items-center justify-center rounded flex-shrink-0 py-1.5 ml-1.5" className={`flex items-center justify-center rounded flex-shrink-0 py-1.5 ml-1.5 ${
store?.theme?.sidebarCollapsed ? "hidden" : "block"
}`}
> >
<ChevronDown <ChevronDown
size={16} size={16}
@ -73,7 +81,11 @@ export const WorkspaceSidebarQuickAction = () => {
/> />
</button> </button>
<div className="absolute w-full h-10 pt-2 top-full left-0 opacity-0 group-hover:opacity-100 mt-0 pointer-events-none group-hover:pointer-events-auto"> <div
className={`fixed h-10 pt-2 w-[203px] left-4 opacity-0 group-hover:opacity-100 mt-0 pointer-events-none group-hover:pointer-events-auto ${
store?.theme?.sidebarCollapsed ? "top-[5.5rem]" : "top-24"
}`}
>
<div className="w-full h-full"> <div className="w-full h-full">
<button <button
onClick={() => setIsDraftIssueModalOpen(true)} onClick={() => setIsDraftIssueModalOpen(true)}

View File

@ -1,3 +1,4 @@
import dynamic from "next/dynamic";
// hooks // hooks
import useTheme from "hooks/use-theme"; import useTheme from "hooks/use-theme";
// components // components
@ -5,8 +6,18 @@ import {
WorkspaceHelpSection, WorkspaceHelpSection,
WorkspaceSidebarDropdown, WorkspaceSidebarDropdown,
WorkspaceSidebarMenu, WorkspaceSidebarMenu,
WorkspaceSidebarQuickAction,
} from "components/workspace"; } from "components/workspace";
const WorkspaceSidebarQuickAction = dynamic<{}>(
() =>
import("components/workspace/sidebar-quick-action").then(
(mod) => mod.WorkspaceSidebarQuickAction
),
{
ssr: false,
}
);
import { ProjectSidebarList } from "components/project"; import { ProjectSidebarList } from "components/project";
import { PublishProjectModal } from "components/project/publish-project/modal"; import { PublishProjectModal } from "components/project/publish-project/modal";
import { ConfirmProjectLeaveModal } from "components/project/confirm-project-leave-modal"; import { ConfirmProjectLeaveModal } from "components/project/confirm-project-leave-modal";

View File

@ -253,6 +253,7 @@ const Profile: NextPage = () => {
placeholder="Enter your first name" placeholder="Enter your first name"
className="!px-3 !py-2 rounded-md font-medium" className="!px-3 !py-2 rounded-md font-medium"
autoComplete="off" autoComplete="off"
maxLength={24}
/> />
</div> </div>
@ -266,6 +267,7 @@ const Profile: NextPage = () => {
placeholder="Enter your last name" placeholder="Enter your last name"
autoComplete="off" autoComplete="off"
className="!px-3 !py-2 rounded-md font-medium" className="!px-3 !py-2 rounded-md font-medium"
maxLength={24}
/> />
</div> </div>

View File

@ -189,10 +189,12 @@ const SingleCycle: React.FC = () => {
{cycleStatus === "completed" && ( {cycleStatus === "completed" && (
<TransferIssues handleClick={() => setTransferIssuesModal(true)} /> <TransferIssues handleClick={() => setTransferIssuesModal(true)} />
)} )}
<IssuesView <div className="relative overflow-y-auto w-full h-full">
openIssuesListModal={openIssuesListModal} <IssuesView
disableUserActions={cycleStatus === "completed" ?? false} openIssuesListModal={openIssuesListModal}
/> disableUserActions={cycleStatus === "completed" ?? false}
/>
</div>
</div> </div>
<CycleDetailsSidebar <CycleDetailsSidebar
cycleStatus={cycleStatus} cycleStatus={cycleStatus}

View File

@ -181,9 +181,9 @@ const SingleModule: React.FC = () => {
onClose={() => setAnalyticsModal(false)} onClose={() => setAnalyticsModal(false)}
/> />
<div <div
className={`h-full flex flex-col ${moduleSidebar ? "mr-[24rem]" : ""} ${ className={`relative overflow-y-auto h-full flex flex-col ${
analyticsModal ? "mr-[50%]" : "" moduleSidebar ? "mr-[24rem]" : ""
} duration-300`} } ${analyticsModal ? "mr-[50%]" : ""} duration-300`}
> >
<IssuesView openIssuesListModal={openIssuesListModal} /> <IssuesView openIssuesListModal={openIssuesListModal} />
</div> </div>

View File

@ -96,7 +96,7 @@ const ProjectModules: NextPage = () => {
<Tooltip <Tooltip
key={option.type} key={option.type}
tooltipContent={ tooltipContent={
<span className="capitalize">{replaceUnderscoreIfSnakeCase(option.type)} View</span> <span className="capitalize">{replaceUnderscoreIfSnakeCase(option.type)} Layout</span>
} }
position="bottom" position="bottom"
> >

View File

@ -337,10 +337,10 @@ const WorkspaceSettings: NextPage = () => {
<Disclosure.Panel> <Disclosure.Panel>
<div className="flex flex-col gap-8"> <div className="flex flex-col gap-8">
<span className="text-sm tracking-tight"> <span className="text-sm tracking-tight">
The danger zone of the project delete page is a critical area that The danger zone of the workspace delete page is a critical area that
requires careful consideration and attention. When deleting a project, all requires careful consideration and attention. When deleting a workspace,
of the data and resources within that project will be permanently removed all of the data and resources within that workspace will be permanently
and cannot be recovered. removed and cannot be recovered.
</span> </span>
<div> <div>
<DangerButton <DangerButton
@ -348,7 +348,7 @@ const WorkspaceSettings: NextPage = () => {
className="!text-sm" className="!text-sm"
outline outline
> >
Delete my project Delete my workspace
</DangerButton> </DangerButton>
</div> </div>
</div> </div>