forked from github/plane
merge conflicts resolved
This commit is contained in:
commit
d3e4eb5753
@ -34,7 +34,6 @@ class CycleSerializer(BaseSerializer):
|
||||
unstarted_issues = serializers.IntegerField(read_only=True)
|
||||
backlog_issues = serializers.IntegerField(read_only=True)
|
||||
assignees = serializers.SerializerMethodField(read_only=True)
|
||||
labels = serializers.SerializerMethodField(read_only=True)
|
||||
total_estimates = serializers.IntegerField(read_only=True)
|
||||
completed_estimates = serializers.IntegerField(read_only=True)
|
||||
started_estimates = serializers.IntegerField(read_only=True)
|
||||
@ -50,11 +49,10 @@ class CycleSerializer(BaseSerializer):
|
||||
members = [
|
||||
{
|
||||
"avatar": assignee.avatar,
|
||||
"first_name": assignee.first_name,
|
||||
"display_name": assignee.display_name,
|
||||
"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()
|
||||
]
|
||||
# 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]
|
||||
|
||||
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:
|
||||
model = Cycle
|
||||
|
@ -1094,7 +1094,7 @@ class ProjectMemberEndpoint(BaseAPIView):
|
||||
project_id=project_id,
|
||||
workspace__slug=slug,
|
||||
member__is_bot=False,
|
||||
).select_related("project", "member")
|
||||
).select_related("project", "member", "workspace")
|
||||
serializer = ProjectMemberSerializer(project_members, many=True)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
except Exception as e:
|
||||
|
@ -58,20 +58,23 @@ def archive_old_issues():
|
||||
|
||||
# Check if Issues
|
||||
if issues:
|
||||
# Set the archive time to current time
|
||||
archive_at = timezone.now()
|
||||
|
||||
issues_to_update = []
|
||||
for issue in issues:
|
||||
issue.archived_at = timezone.now()
|
||||
issue.archived_at = archive_at
|
||||
issues_to_update.append(issue)
|
||||
|
||||
# Bulk Update the issues and log the activity
|
||||
if issues_to_update:
|
||||
updated_issues = Issue.objects.bulk_update(
|
||||
Issue.objects.bulk_update(
|
||||
issues_to_update, ["archived_at"], batch_size=100
|
||||
)
|
||||
[
|
||||
issue_activity.delay(
|
||||
type="issue.activity.updated",
|
||||
requested_data=json.dumps({"archived_at": str(issue.archived_at)}),
|
||||
requested_data=json.dumps({"archived_at": str(archive_at)}),
|
||||
actor_id=str(project.created_by_id),
|
||||
issue_id=issue.id,
|
||||
project_id=project_id,
|
||||
@ -79,7 +82,7 @@ def archive_old_issues():
|
||||
subscriber=False,
|
||||
epoch = int(timezone.now().timestamp())
|
||||
)
|
||||
for issue in updated_issues
|
||||
for issue in issues_to_update
|
||||
]
|
||||
return
|
||||
except Exception as e:
|
||||
@ -139,7 +142,7 @@ def close_old_issues():
|
||||
|
||||
# Bulk Update the issues and log the activity
|
||||
if issues_to_update:
|
||||
updated_issues = Issue.objects.bulk_update(issues_to_update, ["state"], batch_size=100)
|
||||
Issue.objects.bulk_update(issues_to_update, ["state"], batch_size=100)
|
||||
[
|
||||
issue_activity.delay(
|
||||
type="issue.activity.updated",
|
||||
@ -151,7 +154,7 @@ def close_old_issues():
|
||||
subscriber=False,
|
||||
epoch = int(timezone.now().timestamp())
|
||||
)
|
||||
for issue in updated_issues
|
||||
for issue in issues_to_update
|
||||
]
|
||||
return
|
||||
except Exception as e:
|
||||
|
@ -26,19 +26,19 @@ def workspace_member_props(old_props):
|
||||
"calendar_date_range": old_props.get("calendarDateRange", ""),
|
||||
},
|
||||
"display_properties": {
|
||||
"assignee": old_props.get("properties", {}).get("assignee",None),
|
||||
"attachment_count": old_props.get("properties", {}).get("attachment_count", None),
|
||||
"created_on": old_props.get("properties", {}).get("created_on", None),
|
||||
"due_date": old_props.get("properties", {}).get("due_date", None),
|
||||
"estimate": old_props.get("properties", {}).get("estimate", None),
|
||||
"key": old_props.get("properties", {}).get("key", None),
|
||||
"labels": old_props.get("properties", {}).get("labels", None),
|
||||
"link": old_props.get("properties", {}).get("link", None),
|
||||
"priority": old_props.get("properties", {}).get("priority", None),
|
||||
"start_date": old_props.get("properties", {}).get("start_date", None),
|
||||
"state": old_props.get("properties", {}).get("state", None),
|
||||
"sub_issue_count": old_props.get("properties", {}).get("sub_issue_count", None),
|
||||
"updated_on": old_props.get("properties", {}).get("updated_on", None),
|
||||
"assignee": old_props.get("properties", {}).get("assignee", True),
|
||||
"attachment_count": old_props.get("properties", {}).get("attachment_count", True),
|
||||
"created_on": old_props.get("properties", {}).get("created_on", True),
|
||||
"due_date": old_props.get("properties", {}).get("due_date", True),
|
||||
"estimate": old_props.get("properties", {}).get("estimate", True),
|
||||
"key": old_props.get("properties", {}).get("key", True),
|
||||
"labels": old_props.get("properties", {}).get("labels", True),
|
||||
"link": old_props.get("properties", {}).get("link", True),
|
||||
"priority": old_props.get("properties", {}).get("priority", True),
|
||||
"start_date": old_props.get("properties", {}).get("start_date", True),
|
||||
"state": old_props.get("properties", {}).get("state", True),
|
||||
"sub_issue_count": old_props.get("properties", {}).get("sub_issue_count", True),
|
||||
"updated_on": old_props.get("properties", {}).get("updated_on", True),
|
||||
},
|
||||
}
|
||||
return new_props
|
||||
|
@ -1,50 +1,42 @@
|
||||
# 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.conf import settings
|
||||
import django.db.models.deletion
|
||||
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):
|
||||
|
||||
dependencies = [
|
||||
('db', '0044_auto_20230913_0709'),
|
||||
("db", "0044_auto_20230913_0709"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='GlobalView',
|
||||
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')),
|
||||
("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',),
|
||||
"verbose_name": "Global View",
|
||||
"verbose_name_plural": "Global Views",
|
||||
"db_table": "global_views",
|
||||
"ordering": ("-created_at",),
|
||||
},
|
||||
),
|
||||
migrations.RunPython(update_issue_activity),
|
||||
migrations.AddField(
|
||||
model_name="issueactivity",
|
||||
name="epoch",
|
||||
field=models.FloatField(null=True),
|
||||
),
|
||||
]
|
||||
|
@ -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),
|
||||
]
|
26
apiserver/plane/db/migrations/0046_auto_20230926_1015.py
Normal file
26
apiserver/plane/db/migrations/0046_auto_20230926_1015.py
Normal 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),
|
||||
]
|
@ -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),
|
||||
]
|
44
apiserver/plane/db/migrations/0047_auto_20230926_1029.py
Normal file
44
apiserver/plane/db/migrations/0047_auto_20230926_1029.py
Normal 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),
|
||||
]
|
@ -11,6 +11,11 @@ http {
|
||||
|
||||
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 / {
|
||||
proxy_pass http://web:3000/;
|
||||
}
|
||||
@ -20,6 +25,7 @@ http {
|
||||
}
|
||||
|
||||
location /spaces/ {
|
||||
rewrite ^/spaces/?$ /spaces/login break;
|
||||
proxy_pass http://space:3000/spaces/;
|
||||
}
|
||||
|
||||
@ -27,4 +33,4 @@ http {
|
||||
proxy_pass http://plane-minio:9000/uploads/;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -33,7 +33,7 @@ export const SignInView = observer(() => {
|
||||
const onSignInSuccess = (response: any) => {
|
||||
const isOnboarded = response?.user?.onboarding_step?.profile_complete || false;
|
||||
|
||||
const nextPath = router.asPath.includes("next_path") ? router.asPath.split("/?next_path=")[1] : "/";
|
||||
const nextPath = router.asPath.includes("next_path") ? router.asPath.split("/?next_path=")[1] : "/login";
|
||||
|
||||
userStore.setCurrentUser(response?.user);
|
||||
|
||||
@ -41,7 +41,7 @@ export const SignInView = observer(() => {
|
||||
router.push(`/onboarding?next_path=${nextPath}`);
|
||||
return;
|
||||
}
|
||||
router.push((nextPath ?? "/").toString());
|
||||
router.push((nextPath ?? "/login").toString());
|
||||
};
|
||||
|
||||
const handleGoogleSignIn = async ({ clientId, credential }: any) => {
|
||||
|
@ -1 +1 @@
|
||||
export * from "./home";
|
||||
export * from "./login";
|
||||
|
@ -4,7 +4,7 @@ import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
import { SignInView, UserLoggedIn } from "components/accounts";
|
||||
|
||||
export const HomeView = observer(() => {
|
||||
export const LoginView = observer(() => {
|
||||
const { user: userStore } = useMobxStore();
|
||||
|
||||
if (!userStore.currentUser) return <SignInView />;
|
@ -1,8 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
// components
|
||||
import { HomeView } from "components/views";
|
||||
|
||||
const HomePage = () => <HomeView />;
|
||||
|
||||
export default HomePage;
|
8
space/pages/login/index.tsx
Normal file
8
space/pages/login/index.tsx
Normal file
@ -0,0 +1,8 @@
|
||||
import React from "react";
|
||||
|
||||
// components
|
||||
import { LoginView } from "components/views";
|
||||
|
||||
const LoginPage = () => <LoginView />;
|
||||
|
||||
export default LoginPage;
|
@ -93,7 +93,7 @@ export const IssuesFilterView: React.FC = () => {
|
||||
<Tooltip
|
||||
key={option.type}
|
||||
tooltipContent={
|
||||
<span className="capitalize">{replaceUnderscoreIfSnakeCase(option.type)} View</span>
|
||||
<span className="capitalize">{replaceUnderscoreIfSnakeCase(option.type)} Layout</span>
|
||||
}
|
||||
position="bottom"
|
||||
>
|
||||
|
@ -6,7 +6,7 @@ import { useRouter } from "next/router";
|
||||
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
|
||||
import { Draggable } from "react-beautiful-dnd";
|
||||
// components
|
||||
import { BoardHeader, SingleBoardIssue } from "components/core";
|
||||
import { BoardHeader, SingleBoardIssue, BoardInlineCreateIssueForm } from "components/core";
|
||||
// ui
|
||||
import { CustomMenu } from "components/ui";
|
||||
// icons
|
||||
@ -34,31 +34,39 @@ type Props = {
|
||||
viewProps: IIssueViewProps;
|
||||
};
|
||||
|
||||
export const SingleBoard: React.FC<Props> = ({
|
||||
addIssueToGroup,
|
||||
currentState,
|
||||
groupTitle,
|
||||
disableUserActions,
|
||||
disableAddIssueOption = false,
|
||||
dragDisabled,
|
||||
handleIssueAction,
|
||||
handleDraftIssueAction,
|
||||
handleTrashBox,
|
||||
openIssuesListModal,
|
||||
handleMyIssueOpen,
|
||||
removeIssue,
|
||||
user,
|
||||
userAuth,
|
||||
viewProps,
|
||||
}) => {
|
||||
export const SingleBoard: React.FC<Props> = (props) => {
|
||||
const {
|
||||
addIssueToGroup,
|
||||
currentState,
|
||||
groupTitle,
|
||||
disableUserActions,
|
||||
disableAddIssueOption = false,
|
||||
dragDisabled,
|
||||
handleIssueAction,
|
||||
handleDraftIssueAction,
|
||||
handleTrashBox,
|
||||
openIssuesListModal,
|
||||
handleMyIssueOpen,
|
||||
removeIssue,
|
||||
user,
|
||||
userAuth,
|
||||
viewProps,
|
||||
} = props;
|
||||
|
||||
// collapse/expand
|
||||
const [isCollapsed, setIsCollapsed] = useState(true);
|
||||
|
||||
const [isInlineCreateIssueFormOpen, setIsInlineCreateIssueFormOpen] = useState(false);
|
||||
|
||||
const { displayFilters, groupedIssues } = viewProps;
|
||||
|
||||
const router = useRouter();
|
||||
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";
|
||||
|
||||
// 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 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 (
|
||||
<div className={`flex-shrink-0 ${!isCollapsed ? "" : "flex h-full flex-col w-96"}`}>
|
||||
<BoardHeader
|
||||
@ -115,6 +141,7 @@ export const SingleBoard: React.FC<Props> = ({
|
||||
</>
|
||||
)}
|
||||
<div
|
||||
id={`board-list-${groupTitle}`}
|
||||
className={`pt-3 ${
|
||||
hasMinimumNumberOfCards ? "overflow-hidden overflow-y-scroll" : ""
|
||||
} `}
|
||||
@ -170,6 +197,19 @@ export const SingleBoard: React.FC<Props> = ({
|
||||
>
|
||||
<>{provided.placeholder}</>
|
||||
</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>
|
||||
{displayFilters?.group_by !== "created_by" && (
|
||||
<div>
|
||||
@ -178,7 +218,11 @@ export const SingleBoard: React.FC<Props> = ({
|
||||
<button
|
||||
type="button"
|
||||
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" />
|
||||
Add Issue
|
||||
@ -198,7 +242,7 @@ export const SingleBoard: React.FC<Props> = ({
|
||||
position="left"
|
||||
noBorder
|
||||
>
|
||||
<CustomMenu.MenuItem onClick={addIssueToGroup}>
|
||||
<CustomMenu.MenuItem onClick={() => onCreateClick()}>
|
||||
Create new
|
||||
</CustomMenu.MenuItem>
|
||||
{openIssuesListModal && (
|
||||
|
@ -183,7 +183,10 @@ export const CalendarView: React.FC<Props> = ({
|
||||
{calendarIssues ? (
|
||||
<div className="h-full overflow-y-auto">
|
||||
<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
|
||||
isMonthlyView={isMonthlyView}
|
||||
setIsMonthlyView={setIsMonthlyView}
|
||||
|
@ -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" />}
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,10 +1,14 @@
|
||||
import React, { useState } from "react";
|
||||
|
||||
// next
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// react-beautiful-dnd
|
||||
import { Draggable } from "react-beautiful-dnd";
|
||||
// component
|
||||
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
|
||||
import { SingleCalendarIssue } from "./single-issue";
|
||||
import { CalendarInlineCreateIssueForm } from "./inline-create-issue-form";
|
||||
// icons
|
||||
import { PlusSmallIcon } from "@heroicons/react/24/outline";
|
||||
// helper
|
||||
@ -26,17 +30,14 @@ type Props = {
|
||||
isNotAllowed: boolean;
|
||||
};
|
||||
|
||||
export const SingleCalendarDate: React.FC<Props> = ({
|
||||
handleIssueAction,
|
||||
date,
|
||||
index,
|
||||
addIssueToDate,
|
||||
isMonthlyView,
|
||||
showWeekEnds,
|
||||
user,
|
||||
isNotAllowed,
|
||||
}) => {
|
||||
export const SingleCalendarDate: React.FC<Props> = (props) => {
|
||||
const { handleIssueAction, date, index, isMonthlyView, showWeekEnds, user, isNotAllowed } = props;
|
||||
|
||||
const router = useRouter();
|
||||
const { cycleId, moduleId } = router.query;
|
||||
|
||||
const [showAllIssues, setShowAllIssues] = useState(false);
|
||||
const [isCreateIssueFormOpen, setIsCreateIssueFormOpen] = useState(false);
|
||||
|
||||
const totalIssues = date.issues.length;
|
||||
|
||||
@ -79,6 +80,18 @@ export const SingleCalendarDate: React.FC<Props> = ({
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
|
||||
<CalendarInlineCreateIssueForm
|
||||
isOpen={isCreateIssueFormOpen}
|
||||
dependencies={[showWeekEnds]}
|
||||
handleClose={() => setIsCreateIssueFormOpen(false)}
|
||||
prePopulatedData={{
|
||||
target_date: date.date,
|
||||
...(cycleId && { cycle: cycleId.toString() }),
|
||||
...(moduleId && { module: moduleId.toString() }),
|
||||
}}
|
||||
/>
|
||||
|
||||
{totalIssues > 4 && (
|
||||
<button
|
||||
type="button"
|
||||
@ -94,7 +107,7 @@ export const SingleCalendarDate: React.FC<Props> = ({
|
||||
>
|
||||
<button
|
||||
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" />
|
||||
Add issue
|
||||
|
273
web/components/core/views/inline-issue-create-wrapper.tsx
Normal file
273
web/components/core/views/inline-issue-create-wrapper.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
};
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
@ -1,3 +1,6 @@
|
||||
import { useState } from "react";
|
||||
|
||||
// next
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR from "swr";
|
||||
@ -10,7 +13,7 @@ import projectService from "services/project.service";
|
||||
// hooks
|
||||
import useProjects from "hooks/use-projects";
|
||||
// components
|
||||
import { SingleListIssue } from "components/core";
|
||||
import { SingleListIssue, ListInlineCreateIssueForm } from "components/core";
|
||||
// ui
|
||||
import { Avatar, CustomMenu } from "components/ui";
|
||||
// icons
|
||||
@ -31,7 +34,7 @@ import {
|
||||
UserAuth,
|
||||
} from "types";
|
||||
// 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
|
||||
import { STATE_GROUP_COLORS } from "constants/state";
|
||||
|
||||
@ -51,40 +54,65 @@ type Props = {
|
||||
viewProps: IIssueViewProps;
|
||||
};
|
||||
|
||||
export const SingleList: React.FC<Props> = ({
|
||||
currentState,
|
||||
groupTitle,
|
||||
addIssueToGroup,
|
||||
handleIssueAction,
|
||||
openIssuesListModal,
|
||||
handleDraftIssueAction,
|
||||
handleMyIssueOpen,
|
||||
removeIssue,
|
||||
disableUserActions,
|
||||
disableAddIssueOption = false,
|
||||
user,
|
||||
userAuth,
|
||||
viewProps,
|
||||
}) => {
|
||||
export const SingleList: React.FC<Props> = (props) => {
|
||||
const {
|
||||
currentState,
|
||||
groupTitle,
|
||||
handleIssueAction,
|
||||
openIssuesListModal,
|
||||
handleDraftIssueAction,
|
||||
handleMyIssueOpen,
|
||||
addIssueToGroup,
|
||||
removeIssue,
|
||||
disableUserActions,
|
||||
disableAddIssueOption = false,
|
||||
user,
|
||||
userAuth,
|
||||
viewProps,
|
||||
} = props;
|
||||
|
||||
const router = useRouter();
|
||||
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 type = cycleId ? "cycle" : moduleId ? "module" : "issue";
|
||||
|
||||
const { displayFilters, groupedIssues } = viewProps;
|
||||
|
||||
const { data: issueLabels } = useSWR<IIssueLabels[]>(
|
||||
workspaceSlug && projectId ? PROJECT_ISSUE_LABELS(projectId as string) : null,
|
||||
workspaceSlug && projectId
|
||||
? () => issuesService.getIssueLabels(workspaceSlug as string, projectId as string)
|
||||
const { data: issueLabels } = useSWR(
|
||||
workspaceSlug && projectId && displayFilters?.group_by === "labels"
|
||||
? PROJECT_ISSUE_LABELS(projectId.toString())
|
||||
: 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
|
||||
);
|
||||
|
||||
const { data: members } = useSWR(
|
||||
workspaceSlug && projectId ? PROJECT_MEMBERS(projectId as string) : null,
|
||||
workspaceSlug && projectId
|
||||
workspaceSlug &&
|
||||
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)
|
||||
: null
|
||||
);
|
||||
@ -99,7 +127,10 @@ export const SingleList: React.FC<Props> = ({
|
||||
title = addSpaceIfCamelCase(currentState?.name ?? "");
|
||||
break;
|
||||
case "labels":
|
||||
title = issueLabels?.find((label) => label.id === groupTitle)?.name ?? "None";
|
||||
title =
|
||||
[...(issueLabels ?? []), ...(workspaceLabels ?? [])]?.find(
|
||||
(label) => label.id === groupTitle
|
||||
)?.name ?? "None";
|
||||
break;
|
||||
case "project":
|
||||
title = projects?.find((p) => p.id === groupTitle)?.name ?? "None";
|
||||
@ -153,7 +184,9 @@ export const SingleList: React.FC<Props> = ({
|
||||
break;
|
||||
case "labels":
|
||||
const labelColor =
|
||||
issueLabels?.find((label) => label.id === groupTitle)?.color ?? "#000000";
|
||||
[...(issueLabels ?? []), ...(workspaceLabels ?? [])]?.find(
|
||||
(label) => label.id === groupTitle
|
||||
)?.color ?? "#000000";
|
||||
icon = (
|
||||
<span
|
||||
className="h-3 w-3 flex-shrink-0 rounded-full"
|
||||
@ -207,7 +240,7 @@ export const SingleList: React.FC<Props> = ({
|
||||
<button
|
||||
type="button"
|
||||
className="p-1 text-custom-text-200 hover:bg-custom-background-80"
|
||||
onClick={addIssueToGroup}
|
||||
onClick={() => setIsCreateIssueFormOpen(true)}
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
</button>
|
||||
@ -224,7 +257,9 @@ export const SingleList: React.FC<Props> = ({
|
||||
position="right"
|
||||
noBorder
|
||||
>
|
||||
<CustomMenu.MenuItem onClick={addIssueToGroup}>Create new</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem onClick={() => setIsCreateIssueFormOpen(true)}>
|
||||
Create new
|
||||
</CustomMenu.MenuItem>
|
||||
{openIssuesListModal && (
|
||||
<CustomMenu.MenuItem onClick={openIssuesListModal}>
|
||||
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>
|
||||
)}
|
||||
|
||||
<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>
|
||||
</Transition>
|
||||
</div>
|
||||
|
@ -149,6 +149,10 @@ export const SingleCycleList: React.FC<TSingleStatProps> = ({
|
||||
color: group.color,
|
||||
}));
|
||||
|
||||
const completedIssues = cycle.completed_issues + cycle.cancelled_issues;
|
||||
|
||||
const percentage = cycle.total_issues > 0 ? (completedIssues / cycle.total_issues) * 100 : 0;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex flex-col text-xs hover:bg-custom-background-80">
|
||||
@ -307,7 +311,7 @@ export const SingleCycleList: React.FC<TSingleStatProps> = ({
|
||||
) : cycleStatus === "completed" ? (
|
||||
<span className="flex gap-1">
|
||||
<RadialProgressBar progress={100} />
|
||||
<span>{100} %</span>
|
||||
<span>{Math.round(percentage)} %</span>
|
||||
</span>
|
||||
) : (
|
||||
<span className="flex gap-1">
|
||||
|
@ -85,6 +85,7 @@ export const GanttSidebar: React.FC<Props> = ({
|
||||
<StrictModeDroppable droppableId="gantt-sidebar">
|
||||
{(droppableProvided) => (
|
||||
<div
|
||||
id={`gantt-sidebar-${cycleId}`}
|
||||
className="h-full overflow-y-auto pl-2.5"
|
||||
ref={droppableProvided.innerRef}
|
||||
{...droppableProvided.droppableProps}
|
||||
@ -151,6 +152,42 @@ export const GanttSidebar: React.FC<Props> = ({
|
||||
</div>
|
||||
)}
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
@ -7,9 +7,9 @@ import { useRouter } from "next/router";
|
||||
import { ActivityIcon, ActivityMessage } from "components/core";
|
||||
import { CommentCard } from "components/issues/comment";
|
||||
// ui
|
||||
import { Icon, Loader } from "components/ui";
|
||||
import { Icon, Loader, Tooltip } from "components/ui";
|
||||
// helpers
|
||||
import { timeAgo } from "helpers/date-time.helper";
|
||||
import { render24HourFormatTime, renderLongDateFormat, timeAgo } from "helpers/date-time.helper";
|
||||
// types
|
||||
import { IIssueActivity, IIssueComment } from "types";
|
||||
|
||||
@ -120,9 +120,15 @@ export const IssueActivitySection: React.FC<Props> = ({
|
||||
</Link>
|
||||
)}{" "}
|
||||
{message}{" "}
|
||||
<span className="whitespace-nowrap">
|
||||
{timeAgo(activityItem.created_at)}
|
||||
</span>
|
||||
<Tooltip
|
||||
tooltipContent={`${renderLongDateFormat(
|
||||
activityItem.created_at
|
||||
)}, ${render24HourFormatTime(activityItem.created_at)}`}
|
||||
>
|
||||
<span className="whitespace-nowrap">
|
||||
{timeAgo(activityItem.created_at)}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -4,6 +4,8 @@ import { useRouter } from "next/router";
|
||||
|
||||
import { mutate } from "swr";
|
||||
|
||||
import useUser from "hooks/use-user";
|
||||
|
||||
// headless ui
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// services
|
||||
@ -16,7 +18,7 @@ import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
|
||||
// ui
|
||||
import { SecondaryButton, DangerButton } from "components/ui";
|
||||
// types
|
||||
import type { IIssue, ICurrentUserResponse } from "types";
|
||||
import type { IIssue } from "types";
|
||||
// fetch-keys
|
||||
import { PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS } from "constants/fetch-keys";
|
||||
|
||||
@ -24,12 +26,11 @@ type Props = {
|
||||
isOpen: boolean;
|
||||
handleClose: () => void;
|
||||
data: IIssue | null;
|
||||
user?: ICurrentUserResponse;
|
||||
onSubmit?: () => Promise<void> | void;
|
||||
};
|
||||
|
||||
export const DeleteDraftIssueModal: React.FC<Props> = (props) => {
|
||||
const { isOpen, handleClose, data, user, onSubmit } = props;
|
||||
const { isOpen, handleClose, data, onSubmit } = props;
|
||||
|
||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||
|
||||
@ -40,6 +41,8 @@ export const DeleteDraftIssueModal: React.FC<Props> = (props) => {
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const { user } = useUser();
|
||||
|
||||
useEffect(() => {
|
||||
setIsDeleteLoading(false);
|
||||
}, [isOpen]);
|
||||
|
@ -66,6 +66,7 @@ interface IssueFormProps {
|
||||
createMore: boolean;
|
||||
setCreateMore: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
handleClose: () => void;
|
||||
handleDiscard: () => void;
|
||||
status: boolean;
|
||||
user: ICurrentUserResponse | undefined;
|
||||
fieldsToShow: (
|
||||
@ -97,6 +98,7 @@ export const DraftIssueForm: FC<IssueFormProps> = (props) => {
|
||||
status,
|
||||
user,
|
||||
fieldsToShow,
|
||||
handleDiscard,
|
||||
} = props;
|
||||
|
||||
const [stateModal, setStateModal] = useState(false);
|
||||
@ -569,7 +571,7 @@ export const DraftIssueForm: FC<IssueFormProps> = (props) => {
|
||||
<ToggleSwitch value={createMore} onChange={() => {}} size="md" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<SecondaryButton onClick={onClose}>Discard</SecondaryButton>
|
||||
<SecondaryButton onClick={handleDiscard}>Discard</SecondaryButton>
|
||||
<SecondaryButton
|
||||
loading={isSubmitting}
|
||||
onClick={handleSubmit((formData) =>
|
||||
|
@ -97,6 +97,11 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = (props) =
|
||||
setActiveProject(null);
|
||||
};
|
||||
|
||||
const onDiscard = () => {
|
||||
clearDraftIssueLocalStorage();
|
||||
onClose();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setPreloadedData(prePopulateDataProps ?? {});
|
||||
|
||||
@ -141,7 +146,7 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = (props) =
|
||||
if (prePopulateData && prePopulateData.project && !activeProject)
|
||||
return setActiveProject(prePopulateData.project);
|
||||
|
||||
if (prePopulateData && prePopulateData.project)
|
||||
if (prePopulateData && prePopulateData.project && !activeProject)
|
||||
return setActiveProject(prePopulateData.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
|
||||
.createDraftIssue(workspaceSlug as string, activeProject ?? "", payload, user)
|
||||
.then(async () => {
|
||||
mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params));
|
||||
mutate(PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params));
|
||||
|
||||
if (displayFilters.layout === "calendar") mutate(calendarFetchKey);
|
||||
if (displayFilters.layout === "gantt_chart")
|
||||
mutate(ganttFetchKey, {
|
||||
start_target_date: true,
|
||||
order_by: "sort_order",
|
||||
});
|
||||
if (displayFilters.layout === "spreadsheet") mutate(spreadsheetFetchKey);
|
||||
if (groupedIssues) mutateMyIssues();
|
||||
|
||||
setToastAlert({
|
||||
@ -200,8 +197,6 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = (props) =
|
||||
|
||||
if (payload.assignees_list?.some((assignee) => assignee === user?.id))
|
||||
mutate(USER_ISSUE(workspaceSlug as string));
|
||||
|
||||
if (payload.parent && payload.parent !== "") mutate(SUB_ISSUES(payload.parent));
|
||||
})
|
||||
.catch(() => {
|
||||
setToastAlert({
|
||||
@ -396,6 +391,7 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = (props) =
|
||||
createMore={createMore}
|
||||
setCreateMore={setCreateMore}
|
||||
handleClose={onClose}
|
||||
handleDiscard={onDiscard}
|
||||
projectId={activeProject ?? ""}
|
||||
setActiveProject={setActiveProject}
|
||||
status={data ? true : false}
|
||||
|
@ -51,7 +51,7 @@ export const MyIssuesViewOptions: React.FC = () => {
|
||||
<Tooltip
|
||||
key={option.type}
|
||||
tooltipContent={
|
||||
<span className="capitalize">{replaceUnderscoreIfSnakeCase(option.type)} View</span>
|
||||
<span className="capitalize">{replaceUnderscoreIfSnakeCase(option.type)} Layout</span>
|
||||
}
|
||||
position="bottom"
|
||||
>
|
||||
|
@ -1,6 +1,7 @@
|
||||
import React from "react";
|
||||
// next imports
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
// lucide icons
|
||||
import {
|
||||
ChevronDown,
|
||||
@ -37,6 +38,7 @@ export interface ISubIssues {
|
||||
issueId: string,
|
||||
issue?: IIssue | null
|
||||
) => void;
|
||||
setPeekParentId: (id: string) => void;
|
||||
}
|
||||
|
||||
export const SubIssues: React.FC<ISubIssues> = ({
|
||||
@ -52,38 +54,54 @@ export const SubIssues: React.FC<ISubIssues> = ({
|
||||
handleIssuesLoader,
|
||||
copyText,
|
||||
handleIssueCrudOperation,
|
||||
}) => (
|
||||
<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>
|
||||
setPeekParentId,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
|
||||
<Link href={`/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`}>
|
||||
<a className="w-full flex items-center gap-2">
|
||||
const openPeekOverview = (issue_id: string) => {
|
||||
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
|
||||
className="flex-shrink-0 w-[6px] h-[6px] rounded-full"
|
||||
style={{
|
||||
@ -96,93 +114,94 @@ export const SubIssues: React.FC<ISubIssues> = ({
|
||||
<Tooltip tooltipHeading="Title" tooltipContent={`${issue?.name}`}>
|
||||
<div className="line-clamp-1 text-xs text-custom-text-100 pr-2">{issue?.name}</div>
|
||||
</Tooltip>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="flex-shrink-0 text-sm">
|
||||
<IssueProperty
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
parentIssue={parentIssue}
|
||||
issue={issue}
|
||||
user={user}
|
||||
editable={editable}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-shrink-0 text-sm">
|
||||
<IssueProperty
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
parentIssue={parentIssue}
|
||||
issue={issue}
|
||||
user={user}
|
||||
editable={editable}
|
||||
/>
|
||||
</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
|
||||
onClick={() => handleIssueCrudOperation("edit", parentIssue?.id, issue)}
|
||||
onClick={() =>
|
||||
copyText(`${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`)
|
||||
}
|
||||
>
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
<Pencil width={14} strokeWidth={2} />
|
||||
<span>Edit issue</span>
|
||||
<LinkIcon width={14} strokeWidth={2} />
|
||||
<span>Copy issue link</span>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
</CustomMenu>
|
||||
</div>
|
||||
|
||||
{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>
|
||||
{editable && (
|
||||
<>
|
||||
{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">
|
||||
<Loader width={14} strokeWidth={2} className="animate-spin" />
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() =>
|
||||
copyText(`${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`)
|
||||
}
|
||||
>
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
<LinkIcon width={14} strokeWidth={2} />
|
||||
<span>Copy issue link</span>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
) : (
|
||||
<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"
|
||||
onClick={() => {
|
||||
handleIssuesLoader({ key: "delete", issueId: issue?.id });
|
||||
removeIssueFromSubIssues(parentIssue?.id, issue);
|
||||
}}
|
||||
>
|
||||
<X width={14} strokeWidth={2} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{editable && (
|
||||
<>
|
||||
{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">
|
||||
<Loader width={14} strokeWidth={2} className="animate-spin" />
|
||||
</div>
|
||||
) : (
|
||||
<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"
|
||||
onClick={() => {
|
||||
handleIssuesLoader({ key: "delete", issueId: issue?.id });
|
||||
removeIssueFromSubIssues(parentIssue?.id, issue);
|
||||
}}
|
||||
>
|
||||
<X width={14} strokeWidth={2} />
|
||||
</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>
|
||||
);
|
||||
{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}
|
||||
setPeekParentId={setPeekParentId}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -27,6 +27,7 @@ export interface ISubIssuesRootList {
|
||||
issueId: string,
|
||||
issue?: IIssue | null
|
||||
) => void;
|
||||
setPeekParentId: (id: string) => void;
|
||||
}
|
||||
|
||||
export const SubIssuesRootList: React.FC<ISubIssuesRootList> = ({
|
||||
@ -41,6 +42,7 @@ export const SubIssuesRootList: React.FC<ISubIssuesRootList> = ({
|
||||
handleIssuesLoader,
|
||||
copyText,
|
||||
handleIssueCrudOperation,
|
||||
setPeekParentId,
|
||||
}) => {
|
||||
const { data: issues, isLoading } = useSWR(
|
||||
workspaceSlug && projectId && parentIssue && parentIssue?.id
|
||||
@ -81,6 +83,7 @@ export const SubIssuesRootList: React.FC<ISubIssuesRootList> = ({
|
||||
handleIssuesLoader={handleIssuesLoader}
|
||||
copyText={copyText}
|
||||
handleIssueCrudOperation={handleIssueCrudOperation}
|
||||
setPeekParentId={setPeekParentId}
|
||||
/>
|
||||
))}
|
||||
|
||||
|
@ -10,6 +10,7 @@ import { ExistingIssuesListModal } from "components/core";
|
||||
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
||||
import { SubIssuesRootList } from "./issues-list";
|
||||
import { ProgressBar } from "./progressbar";
|
||||
import { IssuePeekOverview } from "components/issues/peek-overview";
|
||||
// ui
|
||||
import { CustomMenu } from "components/ui";
|
||||
// hooks
|
||||
@ -41,7 +42,11 @@ export interface ISubIssuesRootLoadersHandler {
|
||||
|
||||
export const SubIssuesRoot: React.FC<ISubIssuesRoot> = ({ parentIssue, user }) => {
|
||||
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 { setToastAlert } = useToast();
|
||||
@ -55,6 +60,8 @@ export const SubIssuesRoot: React.FC<ISubIssuesRoot> = ({ parentIssue, user }) =
|
||||
: null
|
||||
);
|
||||
|
||||
const [peekParentId, setPeekParentId] = React.useState<string | null>("");
|
||||
|
||||
const [issuesLoader, setIssuesLoader] = React.useState<ISubIssuesRootLoaders>({
|
||||
visibility: [parentIssue?.id],
|
||||
delete: [],
|
||||
@ -230,15 +237,48 @@ export const SubIssuesRoot: React.FC<ISubIssuesRoot> = ({ parentIssue, user }) =
|
||||
handleIssuesLoader={handleIssuesLoader}
|
||||
copyText={copyText}
|
||||
handleIssueCrudOperation={handleIssueCrudOperation}
|
||||
setPeekParentId={setPeekParentId}
|
||||
/>
|
||||
</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 && (
|
||||
<div className="text-xs py-2 text-custom-text-300 font-medium">
|
||||
<div className="py-3 text-center">No sub issues are available</div>
|
||||
<>
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="text-xs py-2 text-custom-text-300 italic">No Sub-Issues yet</div>
|
||||
<div>
|
||||
<CustomMenu
|
||||
label={
|
||||
<>
|
||||
@ -268,7 +308,7 @@ export const SubIssuesRoot: React.FC<ISubIssuesRoot> = ({ parentIssue, user }) =
|
||||
Add an existing issue
|
||||
</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
</>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
@ -1,11 +1,13 @@
|
||||
import React from "react";
|
||||
import React, { useRef, useState } from "react";
|
||||
|
||||
//hook
|
||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||
// ui
|
||||
import { CustomMenu } from "components/ui";
|
||||
// types
|
||||
import { IIssueLabels } from "types";
|
||||
//icons
|
||||
import { RectangleGroupIcon, PencilIcon } from "@heroicons/react/24/outline";
|
||||
import { PencilIcon } from "@heroicons/react/24/outline";
|
||||
import { Component, X } from "lucide-react";
|
||||
|
||||
type Props = {
|
||||
@ -20,9 +22,14 @@ export const SingleLabel: React.FC<Props> = ({
|
||||
addLabelToGroup,
|
||||
editLabel,
|
||||
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">
|
||||
<div className="group flex items-center justify-between">
|
||||
}) => {
|
||||
const [isMenuActive, setIsMenuActive] = useState(false);
|
||||
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">
|
||||
<span
|
||||
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>
|
||||
</div>
|
||||
<div className="flex items-center gap-3.5 pointer-events-none opacity-0 group-hover:pointer-events-auto group-hover:opacity-100">
|
||||
<div className="h-4 w-4">
|
||||
<CustomMenu
|
||||
customButton={
|
||||
<div className="h-4 w-4">
|
||||
<Component className="h-4 w-4 leading-4 text-custom-sidebar-text-400 flex-shrink-0" />
|
||||
</div>
|
||||
}
|
||||
<div
|
||||
ref={actionSectionRef}
|
||||
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 ${
|
||||
isMenuActive ? "opacity-100" : ""
|
||||
}`}
|
||||
>
|
||||
<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)}>
|
||||
<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" />
|
||||
<X className="h-4 w-4 text-custom-sidebar-text-400 flex-shrink-0" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
@ -81,7 +81,7 @@ export const ProfileIssuesViewOptions: React.FC = () => {
|
||||
<Tooltip
|
||||
key={option.type}
|
||||
tooltipContent={
|
||||
<span className="capitalize">{replaceUnderscoreIfSnakeCase(option.type)} View</span>
|
||||
<span className="capitalize">{replaceUnderscoreIfSnakeCase(option.type)} Layout</span>
|
||||
}
|
||||
position="bottom"
|
||||
>
|
||||
|
@ -227,7 +227,8 @@ export const ProfileIssuesView = () => {
|
||||
router.pathname.includes("my-issues")) ??
|
||||
false;
|
||||
|
||||
const disableAddIssueOption = isSubscribedIssuesRoute || isMySubscribedIssues;
|
||||
const disableAddIssueOption =
|
||||
isSubscribedIssuesRoute || isMySubscribedIssues || user?.id !== userId;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -44,7 +44,9 @@ export const WorkspaceSidebarQuickAction = () => {
|
||||
>
|
||||
<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={() => {
|
||||
const e = new KeyboardEvent("keydown", { key: "c" });
|
||||
document.dispatchEvent(e);
|
||||
@ -61,11 +63,17 @@ export const WorkspaceSidebarQuickAction = () => {
|
||||
|
||||
{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
|
||||
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
|
||||
size={16}
|
||||
@ -73,7 +81,11 @@ export const WorkspaceSidebarQuickAction = () => {
|
||||
/>
|
||||
</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">
|
||||
<button
|
||||
onClick={() => setIsDraftIssueModalOpen(true)}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import dynamic from "next/dynamic";
|
||||
// hooks
|
||||
import useTheme from "hooks/use-theme";
|
||||
// components
|
||||
@ -5,8 +6,18 @@ import {
|
||||
WorkspaceHelpSection,
|
||||
WorkspaceSidebarDropdown,
|
||||
WorkspaceSidebarMenu,
|
||||
WorkspaceSidebarQuickAction,
|
||||
} from "components/workspace";
|
||||
|
||||
const WorkspaceSidebarQuickAction = dynamic<{}>(
|
||||
() =>
|
||||
import("components/workspace/sidebar-quick-action").then(
|
||||
(mod) => mod.WorkspaceSidebarQuickAction
|
||||
),
|
||||
{
|
||||
ssr: false,
|
||||
}
|
||||
);
|
||||
|
||||
import { ProjectSidebarList } from "components/project";
|
||||
import { PublishProjectModal } from "components/project/publish-project/modal";
|
||||
import { ConfirmProjectLeaveModal } from "components/project/confirm-project-leave-modal";
|
||||
|
@ -253,6 +253,7 @@ const Profile: NextPage = () => {
|
||||
placeholder="Enter your first name"
|
||||
className="!px-3 !py-2 rounded-md font-medium"
|
||||
autoComplete="off"
|
||||
maxLength={24}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -266,6 +267,7 @@ const Profile: NextPage = () => {
|
||||
placeholder="Enter your last name"
|
||||
autoComplete="off"
|
||||
className="!px-3 !py-2 rounded-md font-medium"
|
||||
maxLength={24}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -189,10 +189,12 @@ const SingleCycle: React.FC = () => {
|
||||
{cycleStatus === "completed" && (
|
||||
<TransferIssues handleClick={() => setTransferIssuesModal(true)} />
|
||||
)}
|
||||
<IssuesView
|
||||
openIssuesListModal={openIssuesListModal}
|
||||
disableUserActions={cycleStatus === "completed" ?? false}
|
||||
/>
|
||||
<div className="relative overflow-y-auto w-full h-full">
|
||||
<IssuesView
|
||||
openIssuesListModal={openIssuesListModal}
|
||||
disableUserActions={cycleStatus === "completed" ?? false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<CycleDetailsSidebar
|
||||
cycleStatus={cycleStatus}
|
||||
|
@ -181,9 +181,9 @@ const SingleModule: React.FC = () => {
|
||||
onClose={() => setAnalyticsModal(false)}
|
||||
/>
|
||||
<div
|
||||
className={`h-full flex flex-col ${moduleSidebar ? "mr-[24rem]" : ""} ${
|
||||
analyticsModal ? "mr-[50%]" : ""
|
||||
} duration-300`}
|
||||
className={`relative overflow-y-auto h-full flex flex-col ${
|
||||
moduleSidebar ? "mr-[24rem]" : ""
|
||||
} ${analyticsModal ? "mr-[50%]" : ""} duration-300`}
|
||||
>
|
||||
<IssuesView openIssuesListModal={openIssuesListModal} />
|
||||
</div>
|
||||
|
@ -96,7 +96,7 @@ const ProjectModules: NextPage = () => {
|
||||
<Tooltip
|
||||
key={option.type}
|
||||
tooltipContent={
|
||||
<span className="capitalize">{replaceUnderscoreIfSnakeCase(option.type)} View</span>
|
||||
<span className="capitalize">{replaceUnderscoreIfSnakeCase(option.type)} Layout</span>
|
||||
}
|
||||
position="bottom"
|
||||
>
|
||||
|
@ -337,10 +337,10 @@ const WorkspaceSettings: NextPage = () => {
|
||||
<Disclosure.Panel>
|
||||
<div className="flex flex-col gap-8">
|
||||
<span className="text-sm tracking-tight">
|
||||
The danger zone of the project delete page is a critical area that
|
||||
requires careful consideration and attention. When deleting a project, all
|
||||
of the data and resources within that project will be permanently removed
|
||||
and cannot be recovered.
|
||||
The danger zone of the workspace delete page is a critical area that
|
||||
requires careful consideration and attention. When deleting a workspace,
|
||||
all of the data and resources within that workspace will be permanently
|
||||
removed and cannot be recovered.
|
||||
</span>
|
||||
<div>
|
||||
<DangerButton
|
||||
@ -348,7 +348,7 @@ const WorkspaceSettings: NextPage = () => {
|
||||
className="!text-sm"
|
||||
outline
|
||||
>
|
||||
Delete my project
|
||||
Delete my workspace
|
||||
</DangerButton>
|
||||
</div>
|
||||
</div>
|
||||
|
Loading…
Reference in New Issue
Block a user