forked from github/plane
Merge branch 'develop' into comment-editor
This commit is contained in:
commit
a754300116
@ -8,8 +8,8 @@ Before submitting a new issue, please search the [issues](https://github.com/mak
|
||||
|
||||
While we want to fix all the [issues](https://github.com/makeplane/plane/issues), before fixing a bug we need to be able to reproduce and confirm it. Please provide us with a minimal reproduction scenario using a repository or [Gist](https://gist.github.com/). Having a live, reproducible scenario gives us the information without asking questions back & forth with additional questions like:
|
||||
|
||||
- 3rd-party libraries being used and their versions
|
||||
- a use-case that fails
|
||||
- 3rd-party libraries being used and their versions
|
||||
- a use-case that fails
|
||||
|
||||
Without said minimal reproduction, we won't be able to investigate all [issues](https://github.com/makeplane/plane/issues), and the issue might not be resolved.
|
||||
|
||||
@ -19,10 +19,10 @@ You can open a new issue with this [issue form](https://github.com/makeplane/pla
|
||||
|
||||
### Requirements
|
||||
|
||||
- Node.js version v16.18.0
|
||||
- Python version 3.8+
|
||||
- Postgres version v14
|
||||
- Redis version v6.2.7
|
||||
- Node.js version v16.18.0
|
||||
- Python version 3.8+
|
||||
- Postgres version v14
|
||||
- Redis version v6.2.7
|
||||
|
||||
### Setup the project
|
||||
|
||||
@ -30,6 +30,48 @@ The project is a monorepo, with backend api and frontend in a single repo.
|
||||
|
||||
The backend is a django project which is kept inside apiserver
|
||||
|
||||
1. Clone the repo
|
||||
|
||||
```bash
|
||||
git clone https://github.com/makeplane/plane
|
||||
cd plane
|
||||
chmod +x setup.sh
|
||||
```
|
||||
|
||||
2. Run setup.sh
|
||||
|
||||
```bash
|
||||
./setup.sh
|
||||
```
|
||||
|
||||
3. Define `NEXT_PUBLIC_API_BASE_URL=http://localhost` in **web/.env** and **space/.env** file
|
||||
|
||||
```bash
|
||||
echo "\nNEXT_PUBLIC_API_BASE_URL=http://localhost\n" >> ./web/.env
|
||||
```
|
||||
|
||||
```bash
|
||||
echo "\nNEXT_PUBLIC_API_BASE_URL=http://localhost\n" >> ./space/.env
|
||||
```
|
||||
|
||||
4. Run Docker compose up
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
5. Install dependencies
|
||||
|
||||
```bash
|
||||
yarn install
|
||||
```
|
||||
|
||||
6. Run the web app in development mode
|
||||
|
||||
```bash
|
||||
yarn dev
|
||||
```
|
||||
|
||||
## Missing a Feature?
|
||||
|
||||
If a feature is missing, you can directly _request_ a new one [here](https://github.com/makeplane/plane/issues/new?assignees=&labels=feature&template=feature_request.yml&title=%F0%9F%9A%80+Feature%3A+). You also can do the same by choosing "🚀 Feature" when raising a [New Issue](https://github.com/makeplane/plane/issues/new/choose) on our GitHub Repository.
|
||||
@ -39,8 +81,8 @@ If you would like to _implement_ it, an issue with your proposal must be submitt
|
||||
|
||||
To ensure consistency throughout the source code, please keep these rules in mind as you are working:
|
||||
|
||||
- All features or bug fixes must be tested by one or more specs (unit-tests).
|
||||
- We use [Eslint default rule guide](https://eslint.org/docs/rules/), with minor changes. An automated formatter is available using prettier.
|
||||
- All features or bug fixes must be tested by one or more specs (unit-tests).
|
||||
- We use [Eslint default rule guide](https://eslint.org/docs/rules/), with minor changes. An automated formatter is available using prettier.
|
||||
|
||||
## Need help? Questions and suggestions
|
||||
|
||||
@ -48,11 +90,11 @@ Questions, suggestions, and thoughts are most welcome. We can also be reached in
|
||||
|
||||
## Ways to contribute
|
||||
|
||||
- Try Plane Cloud and the self hosting platform and give feedback
|
||||
- Add new integrations
|
||||
- Help with open [issues](https://github.com/makeplane/plane/issues) or [create your own](https://github.com/makeplane/plane/issues/new/choose)
|
||||
- Share your thoughts and suggestions with us
|
||||
- Help create tutorials and blog posts
|
||||
- Request a feature by submitting a proposal
|
||||
- Report a bug
|
||||
- **Improve documentation** - fix incomplete or missing [docs](https://docs.plane.so/), bad wording, examples or explanations.
|
||||
- Try Plane Cloud and the self hosting platform and give feedback
|
||||
- Add new integrations
|
||||
- Help with open [issues](https://github.com/makeplane/plane/issues) or [create your own](https://github.com/makeplane/plane/issues/new/choose)
|
||||
- Share your thoughts and suggestions with us
|
||||
- Help create tutorials and blog posts
|
||||
- Request a feature by submitting a proposal
|
||||
- Report a bug
|
||||
- **Improve documentation** - fix incomplete or missing [docs](https://docs.plane.so/), bad wording, examples or explanations.
|
||||
|
@ -58,8 +58,17 @@ class WorkspaceEntityPermission(BasePermission):
|
||||
if request.user.is_anonymous:
|
||||
return False
|
||||
|
||||
## Safe Methods -> Handle the filtering logic in queryset
|
||||
if request.method in SAFE_METHODS:
|
||||
return WorkspaceMember.objects.filter(
|
||||
workspace__slug=view.workspace_slug,
|
||||
member=request.user,
|
||||
).exists()
|
||||
|
||||
return WorkspaceMember.objects.filter(
|
||||
member=request.user, workspace__slug=view.workspace_slug
|
||||
member=request.user,
|
||||
workspace__slug=view.workspace_slug,
|
||||
role__in=[Owner, Admin],
|
||||
).exists()
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
@ -370,6 +370,11 @@ class InboxIssueViewSet(BaseViewSet):
|
||||
if project_member.role <= 10 and str(inbox_issue.created_by_id) != str(request.user.id):
|
||||
return Response({"error": "You cannot delete inbox issue"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Check the issue status
|
||||
if inbox_issue.status in [-2, -1, 0, 2]:
|
||||
# Delete the issue also
|
||||
Issue.objects.filter(workspace__slug=slug, project_id=project_id, pk=inbox_issue.issue_id).delete()
|
||||
|
||||
inbox_issue.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
except InboxIssue.DoesNotExist:
|
||||
|
@ -24,7 +24,6 @@ from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.gzip import gzip_page
|
||||
from django.db import IntegrityError
|
||||
from django.conf import settings
|
||||
from django.db import IntegrityError
|
||||
|
||||
# Third Party imports
|
||||
@ -58,7 +57,6 @@ from plane.api.serializers import (
|
||||
IssuePublicSerializer,
|
||||
)
|
||||
from plane.api.permissions import (
|
||||
WorkspaceEntityPermission,
|
||||
ProjectEntityPermission,
|
||||
WorkSpaceAdminPermission,
|
||||
ProjectMemberPermission,
|
||||
@ -2400,27 +2398,6 @@ class IssueDraftViewSet(BaseViewSet):
|
||||
]
|
||||
serializer_class = IssueFlatSerializer
|
||||
model = Issue
|
||||
|
||||
|
||||
def perform_update(self, serializer):
|
||||
requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder)
|
||||
current_instance = (
|
||||
self.get_queryset().filter(pk=self.kwargs.get("pk", None)).first()
|
||||
)
|
||||
if current_instance is not None:
|
||||
issue_activity.delay(
|
||||
type="issue_draft.activity.updated",
|
||||
requested_data=requested_data,
|
||||
actor_id=str(self.request.user.id),
|
||||
issue_id=str(self.kwargs.get("pk", None)),
|
||||
project_id=str(self.kwargs.get("project_id", None)),
|
||||
current_instance=json.dumps(
|
||||
IssueSerializer(current_instance).data, cls=DjangoJSONEncoder
|
||||
),
|
||||
epoch=int(timezone.now().timestamp())
|
||||
)
|
||||
|
||||
return super().perform_update(serializer)
|
||||
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
@ -2614,6 +2591,47 @@ class IssueDraftViewSet(BaseViewSet):
|
||||
)
|
||||
|
||||
|
||||
def partial_update(self, request, slug, project_id, pk):
|
||||
try:
|
||||
issue = Issue.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, pk=pk
|
||||
)
|
||||
serializer = IssueSerializer(
|
||||
issue, data=request.data, partial=True
|
||||
)
|
||||
|
||||
if serializer.is_valid():
|
||||
if(request.data.get("is_draft") is not None and not request.data.get("is_draft")):
|
||||
serializer.save(created_at=timezone.now(), updated_at=timezone.now())
|
||||
else:
|
||||
serializer.save()
|
||||
issue_activity.delay(
|
||||
type="issue_draft.activity.updated",
|
||||
requested_data=json.dumps(request.data, cls=DjangoJSONEncoder),
|
||||
actor_id=str(self.request.user.id),
|
||||
issue_id=str(self.kwargs.get("pk", None)),
|
||||
project_id=str(self.kwargs.get("project_id", None)),
|
||||
current_instance=json.dumps(
|
||||
IssueSerializer(issue).data,
|
||||
cls=DjangoJSONEncoder,
|
||||
),
|
||||
epoch=int(timezone.now().timestamp())
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
except Issue.DoesNotExist:
|
||||
return Response(
|
||||
{"error": "Issue does not exists"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
def retrieve(self, request, slug, project_id, pk=None):
|
||||
try:
|
||||
issue = Issue.objects.get(
|
||||
|
@ -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:
|
||||
|
@ -61,7 +61,7 @@ class GlobalViewViewSet(BaseViewSet):
|
||||
.get_queryset()
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.select_related("workspace")
|
||||
.order_by("-created_at")
|
||||
.order_by(self.request.GET.get("order_by", "-created_at"))
|
||||
.distinct()
|
||||
)
|
||||
|
||||
|
@ -1389,7 +1389,7 @@ def issue_activity(
|
||||
):
|
||||
issue_subscribers = issue_subscribers + [issue.created_by_id]
|
||||
|
||||
for subscriber in issue_subscribers:
|
||||
for subscriber in list(set(issue_subscribers)):
|
||||
for issue_activity in issue_activities_created:
|
||||
bulk_notifications.append(
|
||||
Notification(
|
||||
|
@ -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,24 +1,42 @@
|
||||
# Generated by Django 4.2.3 on 2023-09-15 06:55
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
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)
|
||||
from django.db import migrations, models
|
||||
from django.conf import settings
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('db', '0044_auto_20230913_0709'),
|
||||
("db", "0044_auto_20230913_0709"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(update_issue_activity),
|
||||
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),
|
||||
),
|
||||
]
|
||||
|
@ -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),
|
||||
]
|
@ -0,0 +1,24 @@
|
||||
# Generated by Django 4.2.5 on 2023-09-27 11:18
|
||||
|
||||
from django.db import migrations, models
|
||||
import plane.db.models.workspace
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('db', '0047_auto_20230926_1029'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='globalview',
|
||||
name='sort_order',
|
||||
field=models.FloatField(default=65535),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='workspacemember',
|
||||
name='issue_props',
|
||||
field=models.JSONField(default=plane.db.models.workspace.get_issue_props),
|
||||
),
|
||||
]
|
@ -26,7 +26,7 @@ ROLE_CHOICES = (
|
||||
def get_default_props():
|
||||
return {
|
||||
"filters": {
|
||||
"priority": "none",
|
||||
"priority": None,
|
||||
"state": None,
|
||||
"state_group": None,
|
||||
"assignees": None,
|
||||
|
@ -17,12 +17,23 @@ class GlobalView(BaseModel):
|
||||
default=1, choices=((0, "Private"), (1, "Public"))
|
||||
)
|
||||
query_data = models.JSONField(default=dict)
|
||||
sort_order = models.FloatField(default=65535)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Global View"
|
||||
verbose_name_plural = "Global Views"
|
||||
db_table = "global_views"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self._state.adding:
|
||||
largest_sort_order = GlobalView.objects.filter(
|
||||
workspace=self.workspace
|
||||
).aggregate(largest=models.Max("sort_order"))["largest"]
|
||||
if largest_sort_order is not None:
|
||||
self.sort_order = largest_sort_order + 10000
|
||||
|
||||
super(GlobalView, self).save(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
"""Return name of the View"""
|
||||
|
@ -17,7 +17,7 @@ ROLE_CHOICES = (
|
||||
def get_default_props():
|
||||
return {
|
||||
"filters": {
|
||||
"priority": "none",
|
||||
"priority": None,
|
||||
"state": None,
|
||||
"state_group": None,
|
||||
"assignees": None,
|
||||
@ -29,7 +29,7 @@ def get_default_props():
|
||||
},
|
||||
"display_filters": {
|
||||
"group_by": None,
|
||||
"order_by": '-created_at',
|
||||
"order_by": "-created_at",
|
||||
"type": None,
|
||||
"sub_issue": True,
|
||||
"show_empty_groups": True,
|
||||
@ -54,6 +54,15 @@ def get_default_props():
|
||||
}
|
||||
|
||||
|
||||
def get_issue_props():
|
||||
return {
|
||||
"subscribed": True,
|
||||
"assigned": True,
|
||||
"created": True,
|
||||
"all_issues": True,
|
||||
}
|
||||
|
||||
|
||||
class Workspace(BaseModel):
|
||||
name = models.CharField(max_length=80, verbose_name="Workspace Name")
|
||||
logo = models.URLField(verbose_name="Logo", blank=True, null=True)
|
||||
@ -89,6 +98,7 @@ class WorkspaceMember(BaseModel):
|
||||
company_role = models.TextField(null=True, blank=True)
|
||||
view_props = models.JSONField(default=get_default_props)
|
||||
default_props = models.JSONField(default=get_default_props)
|
||||
issue_props = models.JSONField(default=get_issue_props)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["workspace", "member"]
|
||||
|
@ -163,17 +163,17 @@ def filter_target_date(params, filter, method):
|
||||
for query in target_dates:
|
||||
target_date_query = query.split(";")
|
||||
if len(target_date_query) == 2 and "after" in target_date_query:
|
||||
filter["target_date__gt"] = target_date_query[0]
|
||||
filter["target_date__gte"] = target_date_query[0]
|
||||
else:
|
||||
filter["target_date__lt"] = target_date_query[0]
|
||||
filter["target_date__lte"] = target_date_query[0]
|
||||
else:
|
||||
if params.get("target_date", None) and len(params.get("target_date")):
|
||||
for query in params.get("target_date"):
|
||||
target_date_query = query.split(";")
|
||||
if len(target_date_query) == 2 and "after" in target_date_query:
|
||||
filter["target_date__gt"] = target_date_query[0]
|
||||
filter["target_date__gte"] = target_date_query[0]
|
||||
else:
|
||||
filter["target_date__lt"] = target_date_query[0]
|
||||
filter["target_date__lte"] = target_date_query[0]
|
||||
|
||||
return filter
|
||||
|
||||
|
@ -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;
|
@ -15,17 +15,19 @@ export const AnalyticsScope: React.FC<Props> = ({ defaultAnalytics }) => (
|
||||
<div className="divide-y divide-custom-border-200">
|
||||
<div>
|
||||
<h6 className="px-3 text-base font-medium">Pending issues</h6>
|
||||
{defaultAnalytics.pending_issue_user.length > 0 ? (
|
||||
{defaultAnalytics.pending_issue_user && defaultAnalytics.pending_issue_user.length > 0 ? (
|
||||
<BarGraph
|
||||
data={defaultAnalytics.pending_issue_user}
|
||||
indexBy="assignees__display_name"
|
||||
indexBy="assignees__id"
|
||||
keys={["count"]}
|
||||
height="250px"
|
||||
colors={() => `#f97316`}
|
||||
customYAxisTickValues={defaultAnalytics.pending_issue_user.map((d) => d.count)}
|
||||
customYAxisTickValues={defaultAnalytics.pending_issue_user.map((d) =>
|
||||
d.count > 0 ? d.count : 50
|
||||
)}
|
||||
tooltip={(datum) => {
|
||||
const assignee = defaultAnalytics.pending_issue_user.find(
|
||||
(a) => a.assignees__display_name === `${datum.indexValue}`
|
||||
(a) => a.assignees__id === `${datum.indexValue}`
|
||||
);
|
||||
|
||||
return (
|
||||
@ -39,10 +41,9 @@ export const AnalyticsScope: React.FC<Props> = ({ defaultAnalytics }) => (
|
||||
}}
|
||||
axisBottom={{
|
||||
renderTick: (datum) => {
|
||||
const avatar =
|
||||
defaultAnalytics.pending_issue_user[datum.tickIndex]?.assignees__avatar ?? "";
|
||||
const assignee = defaultAnalytics.pending_issue_user[datum.tickIndex] ?? "";
|
||||
|
||||
if (avatar && avatar !== "")
|
||||
if (assignee && assignee?.assignees__avatar && assignee?.assignees__avatar !== "")
|
||||
return (
|
||||
<g transform={`translate(${datum.x},${datum.y})`}>
|
||||
<image
|
||||
@ -50,7 +51,7 @@ export const AnalyticsScope: React.FC<Props> = ({ defaultAnalytics }) => (
|
||||
y={10}
|
||||
width={16}
|
||||
height={16}
|
||||
xlinkHref={avatar}
|
||||
xlinkHref={assignee?.assignees__avatar}
|
||||
style={{ clipPath: "circle(50%)" }}
|
||||
/>
|
||||
</g>
|
||||
@ -60,7 +61,7 @@ export const AnalyticsScope: React.FC<Props> = ({ defaultAnalytics }) => (
|
||||
<g transform={`translate(${datum.x},${datum.y})`}>
|
||||
<circle cy={18} r={8} fill="#374151" />
|
||||
<text x={0} y={21} textAnchor="middle" fontSize={9} fill="#ffffff">
|
||||
{datum.value ? `${datum.value}`.toUpperCase()[0] : "?"}
|
||||
{datum.value ? `${assignee.assignees__display_name}`.toUpperCase()[0] : "?"}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
|
@ -161,6 +161,7 @@ export const CommandPalette: React.FC = observer(() => {
|
||||
/>
|
||||
<CreateUpdateViewModal
|
||||
handleClose={() => setIsCreateViewModalOpen(false)}
|
||||
viewType="project"
|
||||
isOpen={isCreateViewModalOpen}
|
||||
user={user}
|
||||
/>
|
||||
|
@ -2,13 +2,33 @@ import { useRouter } from "next/router";
|
||||
|
||||
import useSWR from "swr";
|
||||
|
||||
// hook
|
||||
import useEstimateOption from "hooks/use-estimate-option";
|
||||
// services
|
||||
import issuesService from "services/issues.service";
|
||||
// icons
|
||||
import { Icon, Tooltip } from "components/ui";
|
||||
import { CopyPlus } from "lucide-react";
|
||||
import { Squares2X2Icon } from "@heroicons/react/24/outline";
|
||||
import { BlockedIcon, BlockerIcon, RelatedIcon } from "components/icons";
|
||||
import {
|
||||
TagIcon,
|
||||
CopyPlus,
|
||||
Calendar,
|
||||
Link2Icon,
|
||||
RocketIcon,
|
||||
Users2Icon,
|
||||
ArchiveIcon,
|
||||
PaperclipIcon,
|
||||
ContrastIcon,
|
||||
TriangleIcon,
|
||||
LayoutGridIcon,
|
||||
SignalMediumIcon,
|
||||
MessageSquareIcon,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
BlockedIcon,
|
||||
BlockerIcon,
|
||||
RelatedIcon,
|
||||
StackedLayersHorizontalIcon,
|
||||
} from "components/icons";
|
||||
// helpers
|
||||
import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
|
||||
import { capitalizeFirstLetter } from "helpers/string.helper";
|
||||
@ -36,7 +56,7 @@ const IssueLink = ({ activity }: { activity: IIssueActivity }) => {
|
||||
{activity.issue_detail
|
||||
? `${activity.project_detail.identifier}-${activity.issue_detail.sequence_id}`
|
||||
: "Issue"}
|
||||
<Icon iconName="launch" className="!text-xs" />
|
||||
<RocketIcon size={12} color="#6b7280" />
|
||||
</a>
|
||||
</Tooltip>
|
||||
);
|
||||
@ -77,6 +97,18 @@ const LabelPill = ({ labelId }: { labelId: string }) => {
|
||||
/>
|
||||
);
|
||||
};
|
||||
const EstimatePoint = ({ point }: { point: string }) => {
|
||||
const { estimateValue, isEstimateActive } = useEstimateOption(Number(point));
|
||||
const currentPoint = Number(point) + 1;
|
||||
|
||||
return (
|
||||
<span className="font-medium text-custom-text-100">
|
||||
{isEstimateActive
|
||||
? estimateValue
|
||||
: `${currentPoint} ${currentPoint > 1 ? "points" : "point"}`}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const activityDetails: {
|
||||
[key: string]: {
|
||||
@ -117,14 +149,14 @@ const activityDetails: {
|
||||
</>
|
||||
);
|
||||
},
|
||||
icon: <Icon iconName="group" className="!text-2xl" aria-hidden="true" />,
|
||||
icon: <Users2Icon size={12} color="#6b7280" aria-hidden="true" />,
|
||||
},
|
||||
archived_at: {
|
||||
message: (activity) => {
|
||||
if (activity.new_value === "restore") return "restored the issue.";
|
||||
else return "archived the issue.";
|
||||
},
|
||||
icon: <Icon iconName="archive" className="!text-2xl" aria-hidden="true" />,
|
||||
icon: <ArchiveIcon size={12} color="#6b7280" aria-hidden="true" />,
|
||||
},
|
||||
attachment: {
|
||||
message: (activity, showIssue) => {
|
||||
@ -139,7 +171,7 @@ const activityDetails: {
|
||||
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
||||
>
|
||||
attachment
|
||||
<Icon iconName="launch" className="!text-xs" />
|
||||
<RocketIcon size={12} color="#6b7280" />
|
||||
</a>
|
||||
{showIssue && (
|
||||
<>
|
||||
@ -163,7 +195,7 @@ const activityDetails: {
|
||||
</>
|
||||
);
|
||||
},
|
||||
icon: <Icon iconName="attach_file" className="!text-2xl" aria-hidden="true" />,
|
||||
icon: <PaperclipIcon size={12} color="#6b7280" aria-hidden="true" />,
|
||||
},
|
||||
blocking: {
|
||||
message: (activity) => {
|
||||
@ -254,7 +286,7 @@ const activityDetails: {
|
||||
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
||||
>
|
||||
{activity.new_value}
|
||||
<Icon iconName="launch" className="!text-xs" />
|
||||
<RocketIcon size={12} color="#6b7280" />
|
||||
</a>
|
||||
</>
|
||||
);
|
||||
@ -269,7 +301,7 @@ const activityDetails: {
|
||||
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
||||
>
|
||||
{activity.new_value}
|
||||
<Icon iconName="launch" className="!text-xs" />
|
||||
<RocketIcon size={12} color="#6b7280" />
|
||||
</a>
|
||||
</>
|
||||
);
|
||||
@ -284,12 +316,12 @@ const activityDetails: {
|
||||
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
||||
>
|
||||
{activity.old_value}
|
||||
<Icon iconName="launch" className="!text-xs" />
|
||||
<RocketIcon size={12} color="#6b7280" />
|
||||
</a>
|
||||
</>
|
||||
);
|
||||
},
|
||||
icon: <Icon iconName="contrast" className="!text-2xl" aria-hidden="true" />,
|
||||
icon: <ContrastIcon size={12} color="#6b7280" aria-hidden="true" />,
|
||||
},
|
||||
description: {
|
||||
message: (activity, showIssue) => (
|
||||
@ -304,7 +336,7 @@ const activityDetails: {
|
||||
.
|
||||
</>
|
||||
),
|
||||
icon: <Icon iconName="chat" className="!text-2xl" aria-hidden="true" />,
|
||||
icon: <MessageSquareIcon size={12} color="#6b7280" aria-hidden="true" />,
|
||||
},
|
||||
estimate_point: {
|
||||
message: (activity, showIssue) => {
|
||||
@ -324,8 +356,7 @@ const activityDetails: {
|
||||
else
|
||||
return (
|
||||
<>
|
||||
set the estimate point to{" "}
|
||||
<span className="font-medium text-custom-text-100">{activity.new_value}</span>
|
||||
set the estimate point to <EstimatePoint point={activity.new_value} />
|
||||
{showIssue && (
|
||||
<>
|
||||
{" "}
|
||||
@ -336,14 +367,14 @@ const activityDetails: {
|
||||
</>
|
||||
);
|
||||
},
|
||||
icon: <Icon iconName="change_history" className="!text-2xl" aria-hidden="true" />,
|
||||
icon: <TriangleIcon size={12} color="#6b7280" aria-hidden="true" />,
|
||||
},
|
||||
issue: {
|
||||
message: (activity) => {
|
||||
if (activity.verb === "created") return "created the issue.";
|
||||
else return "deleted an issue.";
|
||||
},
|
||||
icon: <Icon iconName="stack" className="!text-2xl" aria-hidden="true" />,
|
||||
icon: <StackedLayersHorizontalIcon width={12} height={12} color="#6b7280" aria-hidden="true" />,
|
||||
},
|
||||
labels: {
|
||||
message: (activity, showIssue) => {
|
||||
@ -380,7 +411,7 @@ const activityDetails: {
|
||||
</>
|
||||
);
|
||||
},
|
||||
icon: <Icon iconName="sell" className="!text-2xl" aria-hidden="true" />,
|
||||
icon: <TagIcon size={12} color="#6b7280" aria-hidden="true" />,
|
||||
},
|
||||
link: {
|
||||
message: (activity, showIssue) => {
|
||||
@ -395,7 +426,7 @@ const activityDetails: {
|
||||
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
||||
>
|
||||
link
|
||||
<Icon iconName="launch" className="!text-xs" />
|
||||
<RocketIcon size={12} color="#6b7280" />
|
||||
</a>
|
||||
{showIssue && (
|
||||
<>
|
||||
@ -417,7 +448,7 @@ const activityDetails: {
|
||||
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
||||
>
|
||||
link
|
||||
<Icon iconName="launch" className="!text-xs" />
|
||||
<RocketIcon size={12} color="#6b7280" />
|
||||
</a>
|
||||
{showIssue && (
|
||||
<>
|
||||
@ -439,7 +470,7 @@ const activityDetails: {
|
||||
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
||||
>
|
||||
link
|
||||
<Icon iconName="launch" className="!text-xs" />
|
||||
<RocketIcon size={12} color="#6b7280" />
|
||||
</a>
|
||||
{showIssue && (
|
||||
<>
|
||||
@ -451,7 +482,7 @@ const activityDetails: {
|
||||
</>
|
||||
);
|
||||
},
|
||||
icon: <Icon iconName="link" className="!text-2xl" aria-hidden="true" />,
|
||||
icon: <Link2Icon size={12} color="#6b7280" aria-hidden="true" />,
|
||||
},
|
||||
modules: {
|
||||
message: (activity, showIssue, workspaceSlug) => {
|
||||
@ -466,7 +497,7 @@ const activityDetails: {
|
||||
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
||||
>
|
||||
{activity.new_value}
|
||||
<Icon iconName="launch" className="!text-xs" />
|
||||
<RocketIcon size={12} color="#6b7280" />
|
||||
</a>
|
||||
</>
|
||||
);
|
||||
@ -481,7 +512,7 @@ const activityDetails: {
|
||||
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
||||
>
|
||||
{activity.new_value}
|
||||
<Icon iconName="launch" className="!text-xs" />
|
||||
<RocketIcon size={12} color="#6b7280" />
|
||||
</a>
|
||||
</>
|
||||
);
|
||||
@ -496,12 +527,12 @@ const activityDetails: {
|
||||
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
||||
>
|
||||
{activity.old_value}
|
||||
<Icon iconName="launch" className="!text-xs" />
|
||||
<RocketIcon size={12} color="#6b7280" />
|
||||
</a>
|
||||
</>
|
||||
);
|
||||
},
|
||||
icon: <Icon iconName="dataset" className="!text-2xl" aria-hidden="true" />,
|
||||
icon: <Icon iconName="dataset" className="!text-xs !text-[#6b7280]" aria-hidden="true" />,
|
||||
},
|
||||
name: {
|
||||
message: (activity, showIssue) => (
|
||||
@ -516,7 +547,7 @@ const activityDetails: {
|
||||
.
|
||||
</>
|
||||
),
|
||||
icon: <Icon iconName="chat" className="!text-2xl" aria-hidden="true" />,
|
||||
icon: <MessageSquareIcon size={12} color="#6b7280" aria-hidden="true" />,
|
||||
},
|
||||
parent: {
|
||||
message: (activity, showIssue) => {
|
||||
@ -549,7 +580,13 @@ const activityDetails: {
|
||||
</>
|
||||
);
|
||||
},
|
||||
icon: <Icon iconName="supervised_user_circle" className="!text-2xl" aria-hidden="true" />,
|
||||
icon: (
|
||||
<Icon
|
||||
iconName="supervised_user_circle"
|
||||
className="!text-xs !text-[#6b7280]"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
),
|
||||
},
|
||||
priority: {
|
||||
message: (activity, showIssue) => (
|
||||
@ -567,7 +604,7 @@ const activityDetails: {
|
||||
.
|
||||
</>
|
||||
),
|
||||
icon: <Icon iconName="signal_cellular_alt" className="!text-2xl" aria-hidden="true" />,
|
||||
icon: <SignalMediumIcon size={12} color="#6b7280" aria-hidden="true" />,
|
||||
},
|
||||
start_date: {
|
||||
message: (activity, showIssue) => {
|
||||
@ -601,7 +638,7 @@ const activityDetails: {
|
||||
</>
|
||||
);
|
||||
},
|
||||
icon: <Icon iconName="calendar_today" className="!text-2xl" aria-hidden="true" />,
|
||||
icon: <Calendar size={12} color="#6b7280" aria-hidden="true" />,
|
||||
},
|
||||
state: {
|
||||
message: (activity, showIssue) => (
|
||||
@ -617,7 +654,7 @@ const activityDetails: {
|
||||
.
|
||||
</>
|
||||
),
|
||||
icon: <Squares2X2Icon className="h-6 w-6 text-custom-sidebar-200" aria-hidden="true" />,
|
||||
icon: <LayoutGridIcon size={12} color="#6b7280" aria-hidden="true" />,
|
||||
},
|
||||
target_date: {
|
||||
message: (activity, showIssue) => {
|
||||
@ -651,7 +688,7 @@ const activityDetails: {
|
||||
</>
|
||||
);
|
||||
},
|
||||
icon: <Icon iconName="calendar_today" className="!text-2xl" aria-hidden="true" />,
|
||||
icon: <Calendar size={12} color="#6b7280" aria-hidden="true" />,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -10,7 +10,14 @@ import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
||||
// helpers
|
||||
import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
|
||||
// types
|
||||
import { IIssueFilterOptions, IIssueLabels, IState, IUserLite, TStateGroups } from "types";
|
||||
import {
|
||||
IIssueFilterOptions,
|
||||
IIssueLabels,
|
||||
IProject,
|
||||
IState,
|
||||
IUserLite,
|
||||
TStateGroups,
|
||||
} from "types";
|
||||
// constants
|
||||
import { STATE_GROUP_COLORS } from "constants/state";
|
||||
|
||||
@ -20,7 +27,9 @@ type Props = {
|
||||
clearAllFilters: (...args: any) => void;
|
||||
labels: IIssueLabels[] | undefined;
|
||||
members: IUserLite[] | undefined;
|
||||
states: IState[] | undefined;
|
||||
states?: IState[] | undefined;
|
||||
stateGroup?: string[] | undefined;
|
||||
project?: IProject[] | undefined;
|
||||
};
|
||||
|
||||
export const FiltersList: React.FC<Props> = ({
|
||||
@ -30,6 +39,7 @@ export const FiltersList: React.FC<Props> = ({
|
||||
labels,
|
||||
members,
|
||||
states,
|
||||
project,
|
||||
}) => {
|
||||
if (!filters) return <></>;
|
||||
|
||||
@ -155,7 +165,6 @@ export const FiltersList: React.FC<Props> = ({
|
||||
: key === "assignees"
|
||||
? filters.assignees?.map((memberId: string) => {
|
||||
const member = members?.find((m) => m.id === memberId);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={memberId}
|
||||
@ -176,6 +185,32 @@ export const FiltersList: React.FC<Props> = ({
|
||||
</div>
|
||||
);
|
||||
})
|
||||
: key === "subscriber"
|
||||
? filters.subscriber?.map((memberId: string) => {
|
||||
const member = members?.find((m) => m.id === memberId);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={memberId}
|
||||
className="inline-flex items-center gap-x-1 rounded-full bg-custom-background-90 px-1"
|
||||
>
|
||||
<Avatar user={member} />
|
||||
<span>{member?.display_name}</span>
|
||||
<span
|
||||
className="cursor-pointer"
|
||||
onClick={() =>
|
||||
setFilters({
|
||||
subscriber: filters.subscriber?.filter(
|
||||
(p: any) => p !== memberId
|
||||
),
|
||||
})
|
||||
}
|
||||
>
|
||||
<XMarkIcon className="h-3 w-3" />
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
: key === "created_by"
|
||||
? filters.created_by?.map((memberId: string) => {
|
||||
const member = members?.find((m) => m.id === memberId);
|
||||
@ -298,6 +333,30 @@ export const FiltersList: React.FC<Props> = ({
|
||||
</div>
|
||||
);
|
||||
})
|
||||
: key === "project"
|
||||
? filters.project?.map((projectId) => {
|
||||
const currentProject = project?.find((p) => p.id === projectId);
|
||||
console.log("currentProject", currentProject);
|
||||
console.log("currentProject", projectId);
|
||||
return (
|
||||
<p
|
||||
key={currentProject?.id}
|
||||
className="inline-flex items-center gap-x-1 rounded-full px-2 py-0.5 capitalize"
|
||||
>
|
||||
<span>{currentProject?.name}</span>
|
||||
<span
|
||||
className="cursor-pointer"
|
||||
onClick={() =>
|
||||
setFilters({
|
||||
project: filters.project?.filter((p) => p !== projectId),
|
||||
})
|
||||
}
|
||||
>
|
||||
<XMarkIcon className="h-3 w-3" />
|
||||
</span>
|
||||
</p>
|
||||
);
|
||||
})
|
||||
: (filters[key] as any)?.join(", ")}
|
||||
<button
|
||||
type="button"
|
||||
|
@ -67,7 +67,7 @@ export const IssuesFilterView: React.FC = () => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, viewId } = router.query;
|
||||
const isArchivedIssues = router.pathname.includes("archived-issues");
|
||||
const isDraftIssues = router.pathname.includes("draft-issues");
|
||||
const isDraftIssues = router.pathname?.split("/")?.[4] === "draft-issues";
|
||||
|
||||
const {
|
||||
displayFilters,
|
||||
@ -93,7 +93,9 @@ 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"
|
||||
>
|
||||
@ -228,6 +230,9 @@ export const IssuesFilterView: React.FC = () => {
|
||||
return null;
|
||||
if (option.key === "project") return null;
|
||||
|
||||
if (isDraftIssues && option.key === "state_detail.group")
|
||||
return null;
|
||||
|
||||
return (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
@ -272,33 +277,35 @@ export const IssuesFilterView: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-custom-text-200">Issue type</h4>
|
||||
<div className="w-28">
|
||||
<CustomMenu
|
||||
label={
|
||||
FILTER_ISSUE_OPTIONS.find(
|
||||
(option) => option.key === displayFilters.type
|
||||
)?.name ?? "Select"
|
||||
}
|
||||
className="!w-full"
|
||||
buttonClassName="w-full"
|
||||
>
|
||||
{FILTER_ISSUE_OPTIONS.map((option) => (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
onClick={() =>
|
||||
setDisplayFilters({
|
||||
type: option.key,
|
||||
})
|
||||
}
|
||||
>
|
||||
{option.name}
|
||||
</CustomMenu.MenuItem>
|
||||
))}
|
||||
</CustomMenu>
|
||||
{!isArchivedIssues && (
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-custom-text-200">Issue type</h4>
|
||||
<div className="w-28">
|
||||
<CustomMenu
|
||||
label={
|
||||
FILTER_ISSUE_OPTIONS.find(
|
||||
(option) => option.key === displayFilters.type
|
||||
)?.name ?? "Select"
|
||||
}
|
||||
className="!w-full"
|
||||
buttonClassName="w-full"
|
||||
>
|
||||
{FILTER_ISSUE_OPTIONS.map((option) => (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
onClick={() =>
|
||||
setDisplayFilters({
|
||||
type: option.key,
|
||||
})
|
||||
}
|
||||
>
|
||||
{option.name}
|
||||
</CustomMenu.MenuItem>
|
||||
))}
|
||||
</CustomMenu>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{displayFilters.layout !== "calendar" &&
|
||||
displayFilters.layout !== "spreadsheet" && (
|
||||
@ -318,7 +325,7 @@ export const IssuesFilterView: React.FC = () => {
|
||||
displayFilters.layout !== "spreadsheet" &&
|
||||
displayFilters.layout !== "gantt_chart" && (
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-custom-text-200">Show empty states</h4>
|
||||
<h4 className="text-custom-text-200">Show empty groups</h4>
|
||||
<div className="w-28">
|
||||
<ToggleSwitch
|
||||
value={displayFilters.show_empty_groups ?? true}
|
||||
|
@ -12,6 +12,7 @@ import stateService from "services/state.service";
|
||||
// hooks
|
||||
import useUser from "hooks/use-user";
|
||||
import { useProjectMyMembership } from "contexts/project-member.context";
|
||||
import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view";
|
||||
// components
|
||||
import {
|
||||
AllLists,
|
||||
@ -86,6 +87,8 @@ export const AllViews: React.FC<Props> = ({
|
||||
|
||||
const { groupedIssues, isEmpty, displayFilters } = viewProps;
|
||||
|
||||
const { spreadsheetIssues, mutateIssues } = useSpreadsheetIssuesView();
|
||||
|
||||
const { data: stateGroups } = useSWR(
|
||||
workspaceSlug && projectId ? STATES_LIST(projectId as string) : null,
|
||||
workspaceSlug
|
||||
@ -174,6 +177,8 @@ export const AllViews: React.FC<Props> = ({
|
||||
) : displayFilters?.layout === "spreadsheet" ? (
|
||||
<SpreadsheetView
|
||||
handleIssueAction={handleIssueAction}
|
||||
spreadsheetIssues={spreadsheetIssues}
|
||||
mutateIssues={mutateIssues}
|
||||
openIssuesListModal={cycleId || moduleId ? openIssuesListModal : null}
|
||||
disableUserActions={disableUserActions}
|
||||
user={user}
|
||||
|
@ -20,7 +20,7 @@ import { renderEmoji } from "helpers/emoji.helper";
|
||||
// types
|
||||
import { IIssueViewProps, IState, TIssuePriorities, TStateGroups } 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";
|
||||
|
||||
@ -59,6 +59,15 @@ export const BoardHeader: React.FC<Props> = ({
|
||||
: 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 &&
|
||||
@ -82,7 +91,10 @@ export const BoardHeader: 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";
|
||||
@ -137,7 +149,9 @@ export const BoardHeader: 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.5 w-3.5 flex-shrink-0 rounded-full"
|
||||
|
@ -48,7 +48,7 @@ const InlineInput = () => {
|
||||
export const BoardInlineCreateIssueForm: React.FC<Props> = (props) => (
|
||||
<>
|
||||
<InlineCreateIssueFormWrapper
|
||||
className="flex flex-col justify-between gap-1.5 group/card relative select-none px-3.5 py-3 h-[118px] mb-3 rounded bg-custom-background-100 shadow"
|
||||
className="flex flex-col border-[0.5px] border-custom-border-100 justify-between gap-1.5 group/card relative select-none px-3.5 py-3 h-[118px] mb-3 rounded bg-custom-background-100 shadow-custom-shadow-sm"
|
||||
{...props}
|
||||
>
|
||||
<InlineInput />
|
||||
|
@ -63,6 +63,10 @@ export const SingleBoard: React.FC<Props> = (props) => {
|
||||
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
|
||||
@ -71,9 +75,7 @@ export const SingleBoard: React.FC<Props> = (props) => {
|
||||
|
||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disableUserActions;
|
||||
|
||||
const onCreateClick = () => {
|
||||
setIsInlineCreateIssueFormOpen(true);
|
||||
|
||||
const scrollToBottom = () => {
|
||||
const boardListElement = document.getElementById(`board-list-${groupTitle}`);
|
||||
|
||||
// timeout is needed because the animation
|
||||
@ -89,6 +91,11 @@ export const SingleBoard: React.FC<Props> = (props) => {
|
||||
}, 10);
|
||||
};
|
||||
|
||||
const onCreateClick = () => {
|
||||
setIsInlineCreateIssueFormOpen(true);
|
||||
scrollToBottom();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`flex-shrink-0 ${!isCollapsed ? "" : "flex h-full flex-col w-96"}`}>
|
||||
<BoardHeader
|
||||
@ -157,6 +164,7 @@ export const SingleBoard: React.FC<Props> = (props) => {
|
||||
type={type}
|
||||
index={index}
|
||||
issue={issue}
|
||||
projectId={issue.project_detail.id}
|
||||
groupTitle={groupTitle}
|
||||
editIssue={() => handleIssueAction(issue, "edit")}
|
||||
makeIssueCopy={() => handleIssueAction(issue, "copy")}
|
||||
@ -196,6 +204,7 @@ export const SingleBoard: React.FC<Props> = (props) => {
|
||||
<BoardInlineCreateIssueForm
|
||||
isOpen={isInlineCreateIssueFormOpen}
|
||||
handleClose={() => setIsInlineCreateIssueFormOpen(false)}
|
||||
onSuccess={() => scrollToBottom()}
|
||||
prePopulatedData={{
|
||||
...(cycleId && { cycle: cycleId.toString() }),
|
||||
...(moduleId && { module: moduleId.toString() }),
|
||||
@ -213,7 +222,11 @@ export const SingleBoard: React.FC<Props> = (props) => {
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-2 font-medium text-custom-primary outline-none p-1"
|
||||
onClick={() => onCreateClick()}
|
||||
onClick={() => {
|
||||
if (isDraftIssuesPage || isMyIssuesPage || isProfileIssuesPage) {
|
||||
addIssueToGroup();
|
||||
} else onCreateClick();
|
||||
}}
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
Add Issue
|
||||
|
@ -56,6 +56,7 @@ type Props = {
|
||||
provided: DraggableProvided;
|
||||
snapshot: DraggableStateSnapshot;
|
||||
issue: IIssue;
|
||||
projectId: string;
|
||||
groupTitle?: string;
|
||||
index: number;
|
||||
editIssue: () => void;
|
||||
@ -77,6 +78,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
provided,
|
||||
snapshot,
|
||||
issue,
|
||||
projectId,
|
||||
index,
|
||||
editIssue,
|
||||
makeIssueCopy,
|
||||
@ -104,7 +106,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
const { displayFilters, properties, mutateIssues } = viewProps;
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
||||
const { workspaceSlug, cycleId, moduleId } = router.query;
|
||||
|
||||
const isDraftIssue = router.pathname.includes("draft-issues");
|
||||
|
||||
@ -452,6 +454,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
<StateSelect
|
||||
value={issue.state_detail}
|
||||
onChange={handleStateChange}
|
||||
projectId={projectId}
|
||||
hideDropdownArrow
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
@ -479,6 +482,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
{properties.labels && issue.labels.length > 0 && (
|
||||
<LabelSelect
|
||||
value={issue.labels}
|
||||
projectId={projectId}
|
||||
onChange={handleLabelChange}
|
||||
labelsDetails={issue.label_details}
|
||||
hideDropdownArrow
|
||||
@ -489,6 +493,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
{properties.assignee && (
|
||||
<MembersSelect
|
||||
value={issue.assignees}
|
||||
projectId={projectId}
|
||||
onChange={handleAssigneeChange}
|
||||
membersDetails={issue.assignee_details}
|
||||
hideDropdownArrow
|
||||
|
@ -5,25 +5,12 @@ import { Popover, Transition } from "@headlessui/react";
|
||||
// ui
|
||||
import { CustomMenu, ToggleSwitch } from "components/ui";
|
||||
// icons
|
||||
import {
|
||||
CheckIcon,
|
||||
ChevronDownIcon,
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/24/outline";
|
||||
// helpers
|
||||
import {
|
||||
addMonths,
|
||||
addSevenDaysToDate,
|
||||
formatDate,
|
||||
getCurrentWeekEndDate,
|
||||
getCurrentWeekStartDate,
|
||||
isSameMonth,
|
||||
isSameYear,
|
||||
lastDayOfWeek,
|
||||
startOfWeek,
|
||||
subtract7DaysToDate,
|
||||
subtractMonths,
|
||||
updateDateWithMonth,
|
||||
updateDateWithYear,
|
||||
} from "helpers/calendar.helper";
|
||||
@ -31,190 +18,136 @@ import {
|
||||
import { MONTHS_LIST, YEARS_LIST } from "constants/calendar";
|
||||
|
||||
type Props = {
|
||||
isMonthlyView: boolean;
|
||||
setIsMonthlyView: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
currentDate: Date;
|
||||
setCurrentDate: React.Dispatch<React.SetStateAction<Date>>;
|
||||
showWeekEnds: boolean;
|
||||
setShowWeekEnds: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
changeDateRange: (startDate: Date, endDate: Date) => void;
|
||||
};
|
||||
|
||||
export const CalendarHeader: React.FC<Props> = ({
|
||||
setIsMonthlyView,
|
||||
isMonthlyView,
|
||||
currentDate,
|
||||
setCurrentDate,
|
||||
showWeekEnds,
|
||||
setShowWeekEnds,
|
||||
changeDateRange,
|
||||
}) => {
|
||||
const updateDate = (date: Date) => {
|
||||
setCurrentDate(date);
|
||||
}) => (
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="relative flex h-full w-full items-center justify-start gap-2 text-sm ">
|
||||
<Popover className="flex h-full items-center justify-start rounded-lg">
|
||||
{({ open }) => (
|
||||
<>
|
||||
<Popover.Button>
|
||||
<div className="flex items-center justify-center gap-2 text-2xl font-semibold text-custom-text-100">
|
||||
<span>{formatDate(currentDate, "Month")}</span>{" "}
|
||||
<span>{formatDate(currentDate, "yyyy")}</span>
|
||||
</div>
|
||||
</Popover.Button>
|
||||
|
||||
changeDateRange(startOfWeek(date), lastDayOfWeek(date));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="relative flex h-full w-full items-center justify-start gap-2 text-sm ">
|
||||
<Popover className="flex h-full items-center justify-start rounded-lg">
|
||||
{({ open }) => (
|
||||
<>
|
||||
<Popover.Button>
|
||||
<div className="flex items-center justify-center gap-2 text-2xl font-semibold text-custom-text-100">
|
||||
<span>{formatDate(currentDate, "Month")}</span>{" "}
|
||||
<span>{formatDate(currentDate, "yyyy")}</span>
|
||||
<Transition
|
||||
as={React.Fragment}
|
||||
enter="transition ease-out duration-200"
|
||||
enterFrom="opacity-0 translate-y-1"
|
||||
enterTo="opacity-100 translate-y-0"
|
||||
leave="transition ease-in duration-150"
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 translate-y-1"
|
||||
>
|
||||
<Popover.Panel className="absolute top-10 left-0 z-20 flex w-full max-w-xs transform flex-col overflow-hidden rounded-[10px] bg-custom-background-80 shadow-lg">
|
||||
<div className="flex items-center justify-center gap-5 px-2 py-2 text-sm">
|
||||
{YEARS_LIST.map((year) => (
|
||||
<button
|
||||
onClick={() => setCurrentDate(updateDateWithYear(year.label, currentDate))}
|
||||
className={` ${
|
||||
isSameYear(year.value, currentDate)
|
||||
? "text-sm font-medium text-custom-text-100"
|
||||
: "text-xs text-custom-text-200 "
|
||||
} hover:text-sm hover:font-medium hover:text-custom-text-100`}
|
||||
>
|
||||
{year.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</Popover.Button>
|
||||
<div className="grid grid-cols-4 border-t border-custom-border-200 px-2">
|
||||
{MONTHS_LIST.map((month) => (
|
||||
<button
|
||||
onClick={() =>
|
||||
setCurrentDate(updateDateWithMonth(`${month.value}`, currentDate))
|
||||
}
|
||||
className={`px-2 py-2 text-xs text-custom-text-200 hover:font-medium hover:text-custom-text-100 ${
|
||||
isSameMonth(`${month.value}`, currentDate)
|
||||
? "font-medium text-custom-text-100"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{month.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</>
|
||||
)}
|
||||
</Popover>
|
||||
|
||||
<Transition
|
||||
as={React.Fragment}
|
||||
enter="transition ease-out duration-200"
|
||||
enterFrom="opacity-0 translate-y-1"
|
||||
enterTo="opacity-100 translate-y-0"
|
||||
leave="transition ease-in duration-150"
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 translate-y-1"
|
||||
>
|
||||
<Popover.Panel className="absolute top-10 left-0 z-20 flex w-full max-w-xs transform flex-col overflow-hidden rounded-[10px] bg-custom-background-80 shadow-lg">
|
||||
<div className="flex items-center justify-center gap-5 px-2 py-2 text-sm">
|
||||
{YEARS_LIST.map((year) => (
|
||||
<button
|
||||
onClick={() => updateDate(updateDateWithYear(year.label, currentDate))}
|
||||
className={` ${
|
||||
isSameYear(year.value, currentDate)
|
||||
? "text-sm font-medium text-custom-text-100"
|
||||
: "text-xs text-custom-text-200 "
|
||||
} hover:text-sm hover:font-medium hover:text-custom-text-100`}
|
||||
>
|
||||
{year.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="grid grid-cols-4 border-t border-custom-border-200 px-2">
|
||||
{MONTHS_LIST.map((month) => (
|
||||
<button
|
||||
onClick={() =>
|
||||
updateDate(updateDateWithMonth(`${month.value}`, currentDate))
|
||||
}
|
||||
className={`px-2 py-2 text-xs text-custom-text-200 hover:font-medium hover:text-custom-text-100 ${
|
||||
isSameMonth(`${month.value}`, currentDate)
|
||||
? "font-medium text-custom-text-100"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{month.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</>
|
||||
)}
|
||||
</Popover>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
if (isMonthlyView) {
|
||||
updateDate(subtractMonths(currentDate, 1));
|
||||
} else {
|
||||
setCurrentDate(subtract7DaysToDate(currentDate));
|
||||
changeDateRange(
|
||||
getCurrentWeekStartDate(subtract7DaysToDate(currentDate)),
|
||||
getCurrentWeekEndDate(subtract7DaysToDate(currentDate))
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ChevronLeftIcon className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
if (isMonthlyView) {
|
||||
updateDate(addMonths(currentDate, 1));
|
||||
} else {
|
||||
setCurrentDate(addSevenDaysToDate(currentDate));
|
||||
changeDateRange(
|
||||
getCurrentWeekStartDate(addSevenDaysToDate(currentDate)),
|
||||
getCurrentWeekEndDate(addSevenDaysToDate(currentDate))
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ChevronRightIcon className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex w-full items-center justify-end gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
className="group flex cursor-pointer items-center gap-2 rounded-md border border-custom-border-200 px-3 py-1 text-sm hover:bg-custom-background-80 hover:text-custom-text-100 focus:outline-none"
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
if (isMonthlyView) {
|
||||
updateDate(new Date());
|
||||
} else {
|
||||
setCurrentDate(new Date());
|
||||
changeDateRange(
|
||||
getCurrentWeekStartDate(new Date()),
|
||||
getCurrentWeekEndDate(new Date())
|
||||
);
|
||||
}
|
||||
const previousMonthYear =
|
||||
currentDate.getMonth() === 0
|
||||
? currentDate.getFullYear() - 1
|
||||
: currentDate.getFullYear();
|
||||
const previousMonthMonth =
|
||||
currentDate.getMonth() === 0 ? 11 : currentDate.getMonth() - 1;
|
||||
|
||||
const previousMonthFirstDate = new Date(previousMonthYear, previousMonthMonth, 1);
|
||||
|
||||
setCurrentDate(previousMonthFirstDate);
|
||||
}}
|
||||
>
|
||||
Today
|
||||
<ChevronLeftIcon className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
const nextMonthYear =
|
||||
currentDate.getMonth() === 11
|
||||
? currentDate.getFullYear() + 1
|
||||
: currentDate.getFullYear();
|
||||
const nextMonthMonth = (currentDate.getMonth() + 1) % 12;
|
||||
|
||||
<CustomMenu
|
||||
customButton={
|
||||
<div className="group flex cursor-pointer items-center gap-2 rounded-md border border-custom-border-200 px-3 py-1 text-sm hover:bg-custom-background-80 hover:text-custom-text-100 focus:outline-none ">
|
||||
{isMonthlyView ? "Monthly" : "Weekly"}
|
||||
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
|
||||
</div>
|
||||
}
|
||||
const nextMonthFirstDate = new Date(nextMonthYear, nextMonthMonth, 1);
|
||||
|
||||
setCurrentDate(nextMonthFirstDate);
|
||||
}}
|
||||
>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
setIsMonthlyView(true);
|
||||
changeDateRange(startOfWeek(currentDate), lastDayOfWeek(currentDate));
|
||||
}}
|
||||
className="w-52 text-sm text-custom-text-200"
|
||||
>
|
||||
<div className="flex w-full max-w-[260px] items-center justify-between gap-2">
|
||||
<span className="flex items-center gap-2">Monthly View</span>
|
||||
<CheckIcon
|
||||
className={`h-4 w-4 flex-shrink-0 ${isMonthlyView ? "opacity-100" : "opacity-0"}`}
|
||||
/>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
setIsMonthlyView(false);
|
||||
changeDateRange(
|
||||
getCurrentWeekStartDate(currentDate),
|
||||
getCurrentWeekEndDate(currentDate)
|
||||
);
|
||||
}}
|
||||
className="w-52 text-sm text-custom-text-200"
|
||||
>
|
||||
<div className="flex w-full items-center justify-between gap-2">
|
||||
<span className="flex items-center gap-2">Weekly View</span>
|
||||
<CheckIcon
|
||||
className={`h-4 w-4 flex-shrink-0 ${isMonthlyView ? "opacity-0" : "opacity-100"}`}
|
||||
/>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
<div className="mt-1 flex w-52 items-center justify-between border-t border-custom-border-200 py-2 px-1 text-sm text-custom-text-200">
|
||||
<h4>Show weekends</h4>
|
||||
<ToggleSwitch value={showWeekEnds} onChange={() => setShowWeekEnds(!showWeekEnds)} />
|
||||
</div>
|
||||
</CustomMenu>
|
||||
<ChevronRightIcon className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
<div className="flex w-full items-center justify-end gap-2">
|
||||
<button
|
||||
className="group flex cursor-pointer items-center gap-2 rounded-md border border-custom-border-200 px-3 py-1 text-sm hover:bg-custom-background-80 hover:text-custom-text-100 focus:outline-none"
|
||||
onClick={() => setCurrentDate(new Date())}
|
||||
>
|
||||
Today
|
||||
</button>
|
||||
|
||||
<CustomMenu
|
||||
customButton={
|
||||
<div className="group flex cursor-pointer items-center gap-2 rounded-md border border-custom-border-200 px-3 py-1 text-sm hover:bg-custom-background-80 hover:text-custom-text-100 focus:outline-none">
|
||||
Options
|
||||
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="flex w-52 items-center justify-between px-1 text-sm text-custom-text-200">
|
||||
<h4>Show weekends</h4>
|
||||
<ToggleSwitch value={showWeekEnds} onChange={() => setShowWeekEnds(!showWeekEnds)} />
|
||||
</div>
|
||||
</CustomMenu>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default CalendarHeader;
|
||||
|
@ -1,10 +1,6 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { mutate } from "swr";
|
||||
|
||||
// react-beautiful-dnd
|
||||
import { DragDropContext, DropResult } from "react-beautiful-dnd";
|
||||
// services
|
||||
import issuesService from "services/issues.service";
|
||||
@ -50,31 +46,27 @@ export const CalendarView: React.FC<Props> = ({
|
||||
userAuth,
|
||||
}) => {
|
||||
const [showWeekEnds, setShowWeekEnds] = useState(false);
|
||||
const [currentDate, setCurrentDate] = useState(new Date());
|
||||
const [isMonthlyView, setIsMonthlyView] = useState(true);
|
||||
|
||||
const { calendarIssues, mutateIssues, params, activeMonthDate, setActiveMonthDate } =
|
||||
useCalendarIssuesView();
|
||||
|
||||
const [calendarDates, setCalendarDates] = useState<ICalendarRange>({
|
||||
startDate: startOfWeek(currentDate),
|
||||
endDate: lastDayOfWeek(currentDate),
|
||||
startDate: startOfWeek(activeMonthDate),
|
||||
endDate: lastDayOfWeek(activeMonthDate),
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
|
||||
|
||||
const { calendarIssues, mutateIssues, params, displayFilters, setDisplayFilters } =
|
||||
useCalendarIssuesView();
|
||||
|
||||
const totalDate = eachDayOfInterval({
|
||||
start: calendarDates.startDate,
|
||||
end: calendarDates.endDate,
|
||||
});
|
||||
|
||||
const onlyWeekDays = weekDayInterval({
|
||||
start: calendarDates.startDate,
|
||||
end: calendarDates.endDate,
|
||||
});
|
||||
|
||||
const currentViewDays = showWeekEnds ? totalDate : onlyWeekDays;
|
||||
const currentViewDays = showWeekEnds
|
||||
? eachDayOfInterval({
|
||||
start: calendarDates.startDate,
|
||||
end: calendarDates.endDate,
|
||||
})
|
||||
: weekDayInterval({
|
||||
start: calendarDates.startDate,
|
||||
end: calendarDates.endDate,
|
||||
});
|
||||
|
||||
const currentViewDaysData = currentViewDays.map((date: Date) => {
|
||||
const filterIssue =
|
||||
@ -148,27 +140,12 @@ export const CalendarView: React.FC<Props> = ({
|
||||
.then(() => mutate(fetchKey));
|
||||
};
|
||||
|
||||
const changeDateRange = (startDate: Date, endDate: Date) => {
|
||||
setCalendarDates({
|
||||
startDate,
|
||||
endDate,
|
||||
});
|
||||
|
||||
setDisplayFilters({
|
||||
calendar_date_range: `${renderDateFormat(startDate)};after,${renderDateFormat(
|
||||
endDate
|
||||
)};before`,
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!displayFilters || displayFilters.calendar_date_range === "")
|
||||
setDisplayFilters({
|
||||
calendar_date_range: `${renderDateFormat(
|
||||
startOfWeek(currentDate)
|
||||
)};after,${renderDateFormat(lastDayOfWeek(currentDate))};before`,
|
||||
});
|
||||
}, [currentDate, displayFilters, setDisplayFilters]);
|
||||
setCalendarDates({
|
||||
startDate: startOfWeek(activeMonthDate),
|
||||
endDate: lastDayOfWeek(activeMonthDate),
|
||||
});
|
||||
}, [activeMonthDate]);
|
||||
|
||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disableUserActions;
|
||||
|
||||
@ -183,15 +160,15 @@ 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}
|
||||
showWeekEnds={showWeekEnds}
|
||||
setShowWeekEnds={setShowWeekEnds}
|
||||
currentDate={currentDate}
|
||||
setCurrentDate={setCurrentDate}
|
||||
changeDateRange={changeDateRange}
|
||||
currentDate={activeMonthDate}
|
||||
setCurrentDate={setActiveMonthDate}
|
||||
/>
|
||||
|
||||
<div
|
||||
@ -202,30 +179,15 @@ export const CalendarView: React.FC<Props> = ({
|
||||
{weeks.map((date, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`flex items-center justify-start gap-2 border-custom-border-200 bg-custom-background-90 p-1.5 text-base font-medium text-custom-text-200 ${
|
||||
!isMonthlyView
|
||||
? showWeekEnds
|
||||
? (index + 1) % 7 === 0
|
||||
? ""
|
||||
: "border-r"
|
||||
: (index + 1) % 5 === 0
|
||||
? ""
|
||||
: "border-r"
|
||||
: ""
|
||||
}`}
|
||||
className={`flex items-center justify-start gap-2 border-custom-border-200 bg-custom-background-90 p-1.5 text-base font-medium text-custom-text-200`}
|
||||
>
|
||||
<span>
|
||||
{isMonthlyView
|
||||
? formatDate(date, "eee").substring(0, 3)
|
||||
: formatDate(date, "eee")}
|
||||
</span>
|
||||
{!isMonthlyView && <span>{formatDate(date, "d")}</span>}
|
||||
<span>{formatDate(date, "eee").substring(0, 3)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`grid h-full ${isMonthlyView ? "auto-rows-min" : ""} ${
|
||||
className={`grid h-full auto-rows-min ${
|
||||
showWeekEnds ? "grid-cols-7" : "grid-cols-5"
|
||||
} `}
|
||||
>
|
||||
@ -236,7 +198,6 @@ export const CalendarView: React.FC<Props> = ({
|
||||
date={date}
|
||||
handleIssueAction={handleIssueAction}
|
||||
addIssueToDate={addIssueToDate}
|
||||
isMonthlyView={isMonthlyView}
|
||||
showWeekEnds={showWeekEnds}
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
|
@ -1,5 +1,8 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
// next
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// react hook form
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
@ -16,21 +19,29 @@ type Props = {
|
||||
handleClose: () => void;
|
||||
onSuccess?: (data: IIssue) => Promise<void> | void;
|
||||
prePopulatedData?: Partial<IIssue>;
|
||||
dependencies: any[];
|
||||
};
|
||||
|
||||
const useCheckIfThereIsSpaceOnRight = (ref: React.RefObject<HTMLDivElement>) => {
|
||||
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 + 250;
|
||||
const width = right;
|
||||
|
||||
if (width > window.innerWidth) setIsThereSpaceOnRight(false);
|
||||
const innerWidth = container?.getBoundingClientRect().width ?? window.innerWidth;
|
||||
|
||||
if (width > innerWidth) setIsThereSpaceOnRight(false);
|
||||
else setIsThereSpaceOnRight(true);
|
||||
}, [ref]);
|
||||
}, [ref, deps, container]);
|
||||
|
||||
return isThereSpaceOnRight;
|
||||
};
|
||||
@ -56,30 +67,30 @@ const InlineInput = () => {
|
||||
{...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"
|
||||
className="w-full pr-2 py-2.5 rounded-md bg-transparent text-sm font-medium leading-5 text-custom-text-200 outline-none"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const CalendarInlineCreateIssueForm: React.FC<Props> = (props) => {
|
||||
const { isOpen } = props;
|
||||
const { isOpen, dependencies } = props;
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const isSpaceOnRight = useCheckIfThereIsSpaceOnRight(ref);
|
||||
const isSpaceOnRight = useCheckIfThereIsSpaceOnRight(ref, dependencies);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
ref={ref}
|
||||
className={`absolute -translate-x-1 top-5 transition-all z-20 ${
|
||||
className={`absolute top-10 transition-all z-20 w-full max-w-[calc(100%-1.25rem)] ${
|
||||
isOpen ? "opacity-100 scale-100" : "opacity-0 pointer-events-none scale-95"
|
||||
} ${isSpaceOnRight ? "left-full" : "right-0"}`}
|
||||
} right-2.5`}
|
||||
>
|
||||
<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"
|
||||
className="flex w-full px-1.5 border-[0.5px] border-custom-border-100 rounded z-50 items-center gap-x-2 bg-custom-background-100 shadow-custom-shadow-sm transition-opacity"
|
||||
>
|
||||
<InlineInput />
|
||||
</InlineCreateIssueFormWrapper>
|
||||
|
@ -24,14 +24,13 @@ type Props = {
|
||||
issues: IIssue[];
|
||||
};
|
||||
addIssueToDate: (date: string) => void;
|
||||
isMonthlyView: boolean;
|
||||
showWeekEnds: boolean;
|
||||
user: ICurrentUserResponse | undefined;
|
||||
isNotAllowed: boolean;
|
||||
};
|
||||
|
||||
export const SingleCalendarDate: React.FC<Props> = (props) => {
|
||||
const { handleIssueAction, date, index, isMonthlyView, showWeekEnds, user, isNotAllowed } = props;
|
||||
const { handleIssueAction, date, index, showWeekEnds, user, isNotAllowed } = props;
|
||||
|
||||
const router = useRouter();
|
||||
const { cycleId, moduleId } = router.query;
|
||||
@ -39,6 +38,8 @@ export const SingleCalendarDate: React.FC<Props> = (props) => {
|
||||
const [showAllIssues, setShowAllIssues] = useState(false);
|
||||
const [isCreateIssueFormOpen, setIsCreateIssueFormOpen] = useState(false);
|
||||
|
||||
const [formPosition, setFormPosition] = useState({ x: 0, y: 0 });
|
||||
|
||||
const totalIssues = date.issues.length;
|
||||
|
||||
return (
|
||||
@ -49,8 +50,6 @@ export const SingleCalendarDate: React.FC<Props> = (props) => {
|
||||
ref={provided.innerRef}
|
||||
{...provided.droppableProps}
|
||||
className={`group relative flex min-h-[150px] flex-col gap-1.5 border-t border-custom-border-200 p-2.5 text-left text-sm font-medium hover:bg-custom-background-90 ${
|
||||
isMonthlyView ? "" : "pt-9"
|
||||
} ${
|
||||
showWeekEnds
|
||||
? (index + 1) % 7 === 0
|
||||
? ""
|
||||
@ -60,59 +59,66 @@ export const SingleCalendarDate: React.FC<Props> = (props) => {
|
||||
: "border-r"
|
||||
}`}
|
||||
>
|
||||
{isMonthlyView && <span>{formatDate(new Date(date.date), "d")}</span>}
|
||||
{totalIssues > 0 &&
|
||||
date.issues.slice(0, showAllIssues ? totalIssues : 4).map((issue: IIssue, index) => (
|
||||
<Draggable key={issue.id} draggableId={issue.id} index={index}>
|
||||
{(provided, snapshot) => (
|
||||
<SingleCalendarIssue
|
||||
key={index}
|
||||
index={index}
|
||||
provided={provided}
|
||||
snapshot={snapshot}
|
||||
issue={issue}
|
||||
handleEditIssue={() => handleIssueAction(issue, "edit")}
|
||||
handleDeleteIssue={() => handleIssueAction(issue, "delete")}
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
<>
|
||||
<span>{formatDate(new Date(date.date), "d")}</span>
|
||||
{totalIssues > 0 &&
|
||||
date.issues.slice(0, showAllIssues ? totalIssues : 4).map((issue: IIssue, index) => (
|
||||
<Draggable key={issue.id} draggableId={issue.id} index={index}>
|
||||
{(provided, snapshot) => (
|
||||
<SingleCalendarIssue
|
||||
key={index}
|
||||
index={index}
|
||||
provided={provided}
|
||||
snapshot={snapshot}
|
||||
issue={issue}
|
||||
projectId={issue.project_detail.id}
|
||||
handleEditIssue={() => handleIssueAction(issue, "edit")}
|
||||
handleDeleteIssue={() => handleIssueAction(issue, "delete")}
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
|
||||
<CalendarInlineCreateIssueForm
|
||||
isOpen={isCreateIssueFormOpen}
|
||||
handleClose={() => setIsCreateIssueFormOpen(false)}
|
||||
prePopulatedData={{
|
||||
target_date: date.date,
|
||||
...(cycleId && { cycle: cycleId.toString() }),
|
||||
...(moduleId && { module: moduleId.toString() }),
|
||||
}}
|
||||
/>
|
||||
<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"
|
||||
className="w-min whitespace-nowrap rounded-md border border-custom-border-200 bg-custom-background-80 px-1.5 py-1 text-xs"
|
||||
onClick={() => setShowAllIssues((prevData) => !prevData)}
|
||||
{totalIssues > 4 && (
|
||||
<button
|
||||
type="button"
|
||||
className="w-min whitespace-nowrap rounded-md border border-custom-border-200 bg-custom-background-80 px-1.5 py-1 text-xs"
|
||||
onClick={() => setShowAllIssues((prevData) => !prevData)}
|
||||
>
|
||||
{showAllIssues ? "Hide" : totalIssues - 4 + " more"}
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={`absolute top-2 right-2 flex items-center justify-center rounded-md bg-custom-background-80 p-1 text-xs text-custom-text-200 opacity-0 group-hover:opacity-100`}
|
||||
>
|
||||
{showAllIssues ? "Hide" : totalIssues - 4 + " more"}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={(e) => {
|
||||
setIsCreateIssueFormOpen(true);
|
||||
setFormPosition({ x: e.clientX, y: e.clientY });
|
||||
}}
|
||||
className="flex items-center justify-center gap-1 text-center"
|
||||
>
|
||||
<PlusSmallIcon className="h-4 w-4 text-custom-text-200" />
|
||||
Add issue
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`absolute top-2 right-2 flex items-center justify-center rounded-md bg-custom-background-80 p-1 text-xs text-custom-text-200 opacity-0 group-hover:opacity-100`}
|
||||
>
|
||||
<button
|
||||
className="flex items-center justify-center gap-1 text-center"
|
||||
onClick={() => setIsCreateIssueFormOpen(true)}
|
||||
>
|
||||
<PlusSmallIcon className="h-4 w-4 text-custom-text-200" />
|
||||
Add issue
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{provided.placeholder}
|
||||
{provided.placeholder}
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
</StrictModeDroppable>
|
||||
|
@ -41,6 +41,7 @@ type Props = {
|
||||
provided: DraggableProvided;
|
||||
snapshot: DraggableStateSnapshot;
|
||||
issue: IIssue;
|
||||
projectId: string;
|
||||
user: ICurrentUserResponse | undefined;
|
||||
isNotAllowed: boolean;
|
||||
};
|
||||
@ -52,11 +53,12 @@ export const SingleCalendarIssue: React.FC<Props> = ({
|
||||
provided,
|
||||
snapshot,
|
||||
issue,
|
||||
projectId,
|
||||
user,
|
||||
isNotAllowed,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
|
||||
const { workspaceSlug, cycleId, moduleId, viewId } = router.query;
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
@ -310,6 +312,7 @@ export const SingleCalendarIssue: React.FC<Props> = ({
|
||||
{properties.state && (
|
||||
<StateSelect
|
||||
value={issue.state_detail}
|
||||
projectId={projectId}
|
||||
onChange={handleStateChange}
|
||||
hideDropdownArrow
|
||||
disabled={isNotAllowed}
|
||||
@ -334,6 +337,7 @@ export const SingleCalendarIssue: React.FC<Props> = ({
|
||||
{properties.labels && issue.labels.length > 0 && (
|
||||
<LabelSelect
|
||||
value={issue.labels}
|
||||
projectId={projectId}
|
||||
onChange={handleLabelChange}
|
||||
labelsDetails={issue.label_details}
|
||||
hideDropdownArrow
|
||||
@ -345,6 +349,7 @@ export const SingleCalendarIssue: React.FC<Props> = ({
|
||||
{properties.assignee && (
|
||||
<MembersSelect
|
||||
value={issue.assignees}
|
||||
projectId={projectId}
|
||||
onChange={handleAssigneeChange}
|
||||
membersDetails={issue.assignee_details}
|
||||
hideDropdownArrow
|
||||
|
@ -39,7 +39,7 @@ const InlineInput = () => {
|
||||
{...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"
|
||||
className="w-full px-2 rounded-md bg-transparent text-sm font-medium leading-5 text-custom-text-200 outline-none"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
@ -48,7 +48,7 @@ const InlineInput = () => {
|
||||
export const GanttInlineCreateIssueForm: React.FC<Props> = (props) => (
|
||||
<>
|
||||
<InlineCreateIssueFormWrapper
|
||||
className="flex py-3 px-4 mr-2.5 items-center rounded gap-x-2 border bg-custom-background-100 shadow-custom-shadow-sm"
|
||||
className="flex py-3 px-4 border-[0.5px] border-custom-border-100 mr-2.5 items-center rounded gap-x-2 bg-custom-background-100 shadow-custom-shadow-sm"
|
||||
{...props}
|
||||
>
|
||||
<InlineInput />
|
||||
|
@ -39,6 +39,7 @@ import {
|
||||
CYCLE_DETAILS,
|
||||
MODULE_DETAILS,
|
||||
PROJECT_ISSUES_LIST_WITH_PARAMS,
|
||||
PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS,
|
||||
} from "constants/fetch-keys";
|
||||
|
||||
// types
|
||||
@ -119,6 +120,8 @@ export const InlineCreateIssueFormWrapper: React.FC<Props> = (props) => {
|
||||
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();
|
||||
@ -154,15 +157,6 @@ export const InlineCreateIssueFormWrapper: React.FC<Props> = (props) => {
|
||||
if (!isOpen) reset({ ...defaultValues });
|
||||
}, [isOpen, reset]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isSubmitting)
|
||||
setToastAlert({
|
||||
type: "info",
|
||||
title: "Creating issue...",
|
||||
message: "Please wait while we create your issue.",
|
||||
});
|
||||
}, [isSubmitting, setToastAlert]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!errors) return;
|
||||
|
||||
@ -193,10 +187,17 @@ export const InlineCreateIssueFormWrapper: React.FC<Props> = (props) => {
|
||||
|
||||
reset({ ...defaultValues });
|
||||
|
||||
await issuesService
|
||||
.createIssues(workspaceSlug.toString(), projectId.toString(), formData, user)
|
||||
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));
|
||||
await mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(projectId.toString(), params));
|
||||
if (formData.cycle && formData.cycle !== "")
|
||||
await addIssueToCycle(
|
||||
workspaceSlug.toString(),
|
||||
@ -216,10 +217,12 @@ export const InlineCreateIssueFormWrapper: React.FC<Props> = (props) => {
|
||||
params
|
||||
);
|
||||
|
||||
if (displayFilters.layout === "calendar") mutate(calendarFetchKey);
|
||||
if (displayFilters.layout === "gantt_chart") mutate(ganttFetchKey);
|
||||
if (displayFilters.layout === "spreadsheet") mutate(spreadsheetFetchKey);
|
||||
if (groupedIssues) mutateMyIssues();
|
||||
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",
|
||||
|
@ -81,7 +81,9 @@ export const IssuesView: React.FC<Props> = ({
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
|
||||
const isDraftIssues = router.asPath.includes("draft-issues");
|
||||
|
||||
const isDraftIssues = router.pathname?.split("/")?.[4] === "draft-issues";
|
||||
const isArchivedIssues = router.pathname?.split("/")?.[4] === "archived-issues";
|
||||
|
||||
const { user } = useUserAuth();
|
||||
|
||||
@ -481,6 +483,7 @@ export const IssuesView: React.FC<Props> = ({
|
||||
<CreateUpdateViewModal
|
||||
isOpen={createViewModal !== null}
|
||||
handleClose={() => setCreateViewModal(null)}
|
||||
viewType="project"
|
||||
preLoadedData={createViewModal}
|
||||
user={user}
|
||||
/>
|
||||
@ -624,6 +627,7 @@ export const IssuesView: React.FC<Props> = ({
|
||||
params,
|
||||
properties,
|
||||
}}
|
||||
disableAddIssueOption={isArchivedIssues}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
@ -39,17 +39,24 @@ const InlineInput = () => {
|
||||
{...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"
|
||||
className="w-full px-2 py-3 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>
|
||||
<>
|
||||
<InlineCreateIssueFormWrapper
|
||||
className="flex border-[0.5px] border-t-0 border-custom-border-100 px-4 items-center gap-x-5 bg-custom-background-100 shadow-custom-shadow-sm z-10"
|
||||
{...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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
@ -51,6 +51,7 @@ import {
|
||||
type Props = {
|
||||
type?: string;
|
||||
issue: IIssue;
|
||||
projectId: string;
|
||||
groupTitle?: string;
|
||||
editIssue: () => void;
|
||||
index: number;
|
||||
@ -69,6 +70,7 @@ type Props = {
|
||||
export const SingleListIssue: React.FC<Props> = ({
|
||||
type,
|
||||
issue,
|
||||
projectId,
|
||||
editIssue,
|
||||
index,
|
||||
makeIssueCopy,
|
||||
@ -88,7 +90,7 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
const [contextMenuPosition, setContextMenuPosition] = useState<React.MouseEvent | null>(null);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, cycleId, moduleId, userId } = router.query;
|
||||
const { workspaceSlug, cycleId, moduleId, userId } = router.query;
|
||||
const isArchivedIssues = router.pathname.includes("archived-issues");
|
||||
const isDraftIssues = router.pathname?.split("/")?.[4] === "draft-issues";
|
||||
|
||||
@ -326,7 +328,7 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
</ContextMenu>
|
||||
|
||||
<div
|
||||
className="flex items-center justify-between px-4 py-2.5 gap-10 border-b border-custom-border-200 bg-custom-background-100 last:border-b-0"
|
||||
className="flex items-center justify-between px-4 py-2.5 gap-10 border-b-[0.5px] border-custom-border-100 bg-custom-background-100 last:border-b-0"
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault();
|
||||
setContextMenu(true);
|
||||
@ -350,6 +352,7 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
type="button"
|
||||
className="truncate text-[0.825rem] text-custom-text-100"
|
||||
onClick={() => {
|
||||
if (isArchivedIssues) return router.push(issuePath);
|
||||
if (!isDraftIssues) openPeekOverview(issue);
|
||||
if (isDraftIssues && handleDraftIssueSelect) handleDraftIssueSelect(issue);
|
||||
}}
|
||||
@ -376,6 +379,7 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
{properties.state && (
|
||||
<StateSelect
|
||||
value={issue.state_detail}
|
||||
projectId={projectId}
|
||||
onChange={handleStateChange}
|
||||
hideDropdownArrow
|
||||
disabled={isNotAllowed}
|
||||
@ -400,6 +404,7 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
{properties.labels && (
|
||||
<LabelSelect
|
||||
value={issue.labels}
|
||||
projectId={projectId}
|
||||
onChange={handleLabelChange}
|
||||
labelsDetails={issue.label_details}
|
||||
hideDropdownArrow
|
||||
@ -411,6 +416,7 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
{properties.assignee && (
|
||||
<MembersSelect
|
||||
value={issue.assignees}
|
||||
projectId={projectId}
|
||||
onChange={handleAssigneeChange}
|
||||
membersDetails={issue.assignee_details}
|
||||
hideDropdownArrow
|
||||
|
@ -34,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";
|
||||
|
||||
@ -62,6 +62,7 @@ export const SingleList: React.FC<Props> = (props) => {
|
||||
openIssuesListModal,
|
||||
handleDraftIssueAction,
|
||||
handleMyIssueOpen,
|
||||
addIssueToGroup,
|
||||
removeIssue,
|
||||
disableUserActions,
|
||||
disableAddIssueOption = false,
|
||||
@ -75,22 +76,43 @@ export const SingleList: React.FC<Props> = (props) => {
|
||||
|
||||
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
|
||||
);
|
||||
@ -105,7 +127,10 @@ export const SingleList: React.FC<Props> = (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";
|
||||
@ -159,7 +184,9 @@ export const SingleList: React.FC<Props> = (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"
|
||||
@ -212,8 +239,12 @@ export const SingleList: React.FC<Props> = (props) => {
|
||||
!disableAddIssueOption && (
|
||||
<button
|
||||
type="button"
|
||||
className="p-1 text-custom-text-200 hover:bg-custom-background-80"
|
||||
onClick={() => setIsCreateIssueFormOpen(true)}
|
||||
className="p-1 text-custom-text-200 hover:bg-custom-background-80"
|
||||
onClick={() => {
|
||||
if (isDraftIssuesPage || isMyIssuesPage || isProfileIssuesPage) {
|
||||
addIssueToGroup();
|
||||
} else setIsCreateIssueFormOpen(true);
|
||||
}}
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
</button>
|
||||
@ -258,6 +289,7 @@ export const SingleList: React.FC<Props> = (props) => {
|
||||
key={issue.id}
|
||||
type={type}
|
||||
issue={issue}
|
||||
projectId={issue.project_detail.id}
|
||||
groupTitle={groupTitle}
|
||||
index={index}
|
||||
editIssue={() => handleIssueAction(issue, "edit")}
|
||||
@ -294,7 +326,7 @@ export const SingleList: React.FC<Props> = (props) => {
|
||||
)}
|
||||
|
||||
<ListInlineCreateIssueForm
|
||||
isOpen={isCreateIssueFormOpen}
|
||||
isOpen={isCreateIssueFormOpen && !disableAddIssueOption}
|
||||
handleClose={() => setIsCreateIssueFormOpen(false)}
|
||||
prePopulatedData={{
|
||||
...(cycleId && { cycle: cycleId.toString() }),
|
||||
@ -303,11 +335,16 @@ export const SingleList: React.FC<Props> = (props) => {
|
||||
}}
|
||||
/>
|
||||
|
||||
{!isCreateIssueFormOpen && (
|
||||
<div className="w-full bg-custom-background-100 px-6 py-3">
|
||||
{!disableAddIssueOption && !isCreateIssueFormOpen && (
|
||||
// TODO: add border here
|
||||
<div className="w-full bg-custom-background-100 px-6 py-3 border-b border-custom-border-100">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsCreateIssueFormOpen(true)}
|
||||
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" />
|
||||
|
@ -0,0 +1,72 @@
|
||||
import React from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// components
|
||||
import { MembersSelect } from "components/project";
|
||||
// services
|
||||
import trackEventServices from "services/track-event.service";
|
||||
// types
|
||||
import { ICurrentUserResponse, IIssue, Properties } from "types";
|
||||
|
||||
type Props = {
|
||||
issue: IIssue;
|
||||
projectId: string;
|
||||
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
|
||||
properties: Properties;
|
||||
user: ICurrentUserResponse | undefined;
|
||||
isNotAllowed: boolean;
|
||||
};
|
||||
|
||||
export const AssigneeColumn: React.FC<Props> = ({
|
||||
issue,
|
||||
projectId,
|
||||
partialUpdateIssue,
|
||||
properties,
|
||||
user,
|
||||
isNotAllowed,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const handleAssigneeChange = (data: any) => {
|
||||
const newData = issue.assignees ?? [];
|
||||
|
||||
if (newData.includes(data)) newData.splice(newData.indexOf(data), 1);
|
||||
else newData.push(data);
|
||||
|
||||
partialUpdateIssue({ assignees_list: data }, issue);
|
||||
|
||||
trackEventServices.trackIssuePartialPropertyUpdateEvent(
|
||||
{
|
||||
workspaceSlug,
|
||||
workspaceId: issue.workspace,
|
||||
projectId: issue.project_detail.id,
|
||||
projectIdentifier: issue.project_detail.identifier,
|
||||
projectName: issue.project_detail.name,
|
||||
issueId: issue.id,
|
||||
},
|
||||
"ISSUE_PROPERTY_UPDATE_ASSIGNEE",
|
||||
user
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center text-sm h-11 w-full bg-custom-background-100">
|
||||
<span className="flex items-center px-4 py-2.5 h-full w-full flex-shrink-0 border-r border-b border-custom-border-100">
|
||||
{properties.assignee && (
|
||||
<MembersSelect
|
||||
value={issue.assignees}
|
||||
projectId={projectId}
|
||||
onChange={handleAssigneeChange}
|
||||
membersDetails={issue.assignee_details}
|
||||
buttonClassName="!p-0 !rounded-none !shadow-none !border-0"
|
||||
hideDropdownArrow
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,2 @@
|
||||
export * from "./spreadsheet-assignee-column";
|
||||
export * from "./assignee-column";
|
@ -0,0 +1,62 @@
|
||||
import React from "react";
|
||||
|
||||
// components
|
||||
import { AssigneeColumn } from "components/core";
|
||||
// hooks
|
||||
import useSubIssue from "hooks/use-sub-issue";
|
||||
// types
|
||||
import { ICurrentUserResponse, IIssue, Properties } from "types";
|
||||
|
||||
type Props = {
|
||||
issue: IIssue;
|
||||
projectId: string;
|
||||
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
|
||||
expandedIssues: string[];
|
||||
properties: Properties;
|
||||
user: ICurrentUserResponse | undefined;
|
||||
isNotAllowed: boolean;
|
||||
};
|
||||
|
||||
export const SpreadsheetAssigneeColumn: React.FC<Props> = ({
|
||||
issue,
|
||||
projectId,
|
||||
partialUpdateIssue,
|
||||
expandedIssues,
|
||||
properties,
|
||||
user,
|
||||
isNotAllowed,
|
||||
}) => {
|
||||
const isExpanded = expandedIssues.indexOf(issue.id) > -1;
|
||||
|
||||
const { subIssues, isLoading } = useSubIssue(issue.project_detail.id, issue.id, isExpanded);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<AssigneeColumn
|
||||
issue={issue}
|
||||
projectId={projectId}
|
||||
properties={properties}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
|
||||
{isExpanded &&
|
||||
!isLoading &&
|
||||
subIssues &&
|
||||
subIssues.length > 0 &&
|
||||
subIssues.map((subIssue: IIssue) => (
|
||||
<SpreadsheetAssigneeColumn
|
||||
key={subIssue.id}
|
||||
issue={subIssue}
|
||||
projectId={subIssue.project_detail.id}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
expandedIssues={expandedIssues}
|
||||
properties={properties}
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,34 @@
|
||||
import React from "react";
|
||||
|
||||
// types
|
||||
import { ICurrentUserResponse, IIssue, Properties } from "types";
|
||||
// helper
|
||||
import { renderLongDetailDateFormat } from "helpers/date-time.helper";
|
||||
|
||||
type Props = {
|
||||
issue: IIssue;
|
||||
projectId: string;
|
||||
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
|
||||
properties: Properties;
|
||||
user: ICurrentUserResponse | undefined;
|
||||
isNotAllowed: boolean;
|
||||
};
|
||||
|
||||
export const CreatedOnColumn: React.FC<Props> = ({
|
||||
issue,
|
||||
projectId,
|
||||
partialUpdateIssue,
|
||||
properties,
|
||||
user,
|
||||
isNotAllowed,
|
||||
}) => (
|
||||
<div className="flex items-center text-sm h-11 w-full bg-custom-background-100">
|
||||
<span className="flex items-center px-4 py-2.5 h-full w-full flex-shrink-0 border-r border-b border-custom-border-100">
|
||||
{properties.created_on && (
|
||||
<div className="flex items-center text-xs cursor-default text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
|
||||
{renderLongDetailDateFormat(issue.created_at)}
|
||||
</div>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
@ -0,0 +1,2 @@
|
||||
export * from "./spreadsheet-created-on-column";
|
||||
export * from "./created-on-column";
|
@ -0,0 +1,62 @@
|
||||
import React from "react";
|
||||
|
||||
// components
|
||||
import { CreatedOnColumn } from "components/core";
|
||||
// hooks
|
||||
import useSubIssue from "hooks/use-sub-issue";
|
||||
// types
|
||||
import { ICurrentUserResponse, IIssue, Properties } from "types";
|
||||
|
||||
type Props = {
|
||||
issue: IIssue;
|
||||
projectId: string;
|
||||
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
|
||||
expandedIssues: string[];
|
||||
properties: Properties;
|
||||
user: ICurrentUserResponse | undefined;
|
||||
isNotAllowed: boolean;
|
||||
};
|
||||
|
||||
export const SpreadsheetCreatedOnColumn: React.FC<Props> = ({
|
||||
issue,
|
||||
projectId,
|
||||
partialUpdateIssue,
|
||||
expandedIssues,
|
||||
properties,
|
||||
user,
|
||||
isNotAllowed,
|
||||
}) => {
|
||||
const isExpanded = expandedIssues.indexOf(issue.id) > -1;
|
||||
|
||||
const { subIssues, isLoading } = useSubIssue(issue.project_detail.id, issue.id, isExpanded);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<CreatedOnColumn
|
||||
issue={issue}
|
||||
projectId={projectId}
|
||||
properties={properties}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
|
||||
{isExpanded &&
|
||||
!isLoading &&
|
||||
subIssues &&
|
||||
subIssues.length > 0 &&
|
||||
subIssues.map((subIssue: IIssue) => (
|
||||
<SpreadsheetCreatedOnColumn
|
||||
key={subIssue.id}
|
||||
issue={subIssue}
|
||||
projectId={subIssue.project_detail.id}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
expandedIssues={expandedIssues}
|
||||
properties={properties}
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,38 @@
|
||||
import React from "react";
|
||||
|
||||
// components
|
||||
import { ViewDueDateSelect } from "components/issues";
|
||||
// types
|
||||
import { ICurrentUserResponse, IIssue, Properties } from "types";
|
||||
|
||||
type Props = {
|
||||
issue: IIssue;
|
||||
projectId: string;
|
||||
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
|
||||
properties: Properties;
|
||||
user: ICurrentUserResponse | undefined;
|
||||
isNotAllowed: boolean;
|
||||
};
|
||||
|
||||
export const DueDateColumn: React.FC<Props> = ({
|
||||
issue,
|
||||
projectId,
|
||||
partialUpdateIssue,
|
||||
properties,
|
||||
user,
|
||||
isNotAllowed,
|
||||
}) => (
|
||||
<div className="flex items-center text-sm h-11 w-full bg-custom-background-100">
|
||||
<span className="flex items-center px-4 py-2.5 h-full w-full flex-shrink-0 border-r border-b border-custom-border-100">
|
||||
{properties.due_date && (
|
||||
<ViewDueDateSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
noBorder
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
@ -0,0 +1,2 @@
|
||||
export * from "./spreadsheet-due-date-column";
|
||||
export * from "./due-date-column";
|
@ -0,0 +1,62 @@
|
||||
import React from "react";
|
||||
|
||||
// components
|
||||
import { DueDateColumn } from "components/core";
|
||||
// hooks
|
||||
import useSubIssue from "hooks/use-sub-issue";
|
||||
// types
|
||||
import { ICurrentUserResponse, IIssue, Properties } from "types";
|
||||
|
||||
type Props = {
|
||||
issue: IIssue;
|
||||
projectId: string;
|
||||
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
|
||||
expandedIssues: string[];
|
||||
properties: Properties;
|
||||
user: ICurrentUserResponse | undefined;
|
||||
isNotAllowed: boolean;
|
||||
};
|
||||
|
||||
export const SpreadsheetDueDateColumn: React.FC<Props> = ({
|
||||
issue,
|
||||
projectId,
|
||||
partialUpdateIssue,
|
||||
expandedIssues,
|
||||
properties,
|
||||
user,
|
||||
isNotAllowed,
|
||||
}) => {
|
||||
const isExpanded = expandedIssues.indexOf(issue.id) > -1;
|
||||
|
||||
const { subIssues, isLoading } = useSubIssue(issue.project_detail.id, issue.id, isExpanded);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<DueDateColumn
|
||||
issue={issue}
|
||||
projectId={projectId}
|
||||
properties={properties}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
|
||||
{isExpanded &&
|
||||
!isLoading &&
|
||||
subIssues &&
|
||||
subIssues.length > 0 &&
|
||||
subIssues.map((subIssue: IIssue) => (
|
||||
<SpreadsheetDueDateColumn
|
||||
key={subIssue.id}
|
||||
issue={subIssue}
|
||||
projectId={subIssue.project_detail.id}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
expandedIssues={expandedIssues}
|
||||
properties={properties}
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,38 @@
|
||||
import React from "react";
|
||||
|
||||
// components
|
||||
import { ViewEstimateSelect } from "components/issues";
|
||||
// types
|
||||
import { ICurrentUserResponse, IIssue, Properties } from "types";
|
||||
|
||||
type Props = {
|
||||
issue: IIssue;
|
||||
projectId: string;
|
||||
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
|
||||
properties: Properties;
|
||||
user: ICurrentUserResponse | undefined;
|
||||
isNotAllowed: boolean;
|
||||
};
|
||||
|
||||
export const EstimateColumn: React.FC<Props> = ({
|
||||
issue,
|
||||
projectId,
|
||||
partialUpdateIssue,
|
||||
properties,
|
||||
user,
|
||||
isNotAllowed,
|
||||
}) => (
|
||||
<div className="flex items-center text-sm h-11 w-full bg-custom-background-100">
|
||||
<span className="flex items-center px-4 py-2.5 h-full w-full flex-shrink-0 border-r border-b border-custom-border-100">
|
||||
{properties.estimate && (
|
||||
<ViewEstimateSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
position="left"
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
@ -0,0 +1,2 @@
|
||||
export * from "./spreadsheet-estimate-column";
|
||||
export * from "./estimate-column";
|
@ -0,0 +1,62 @@
|
||||
import React from "react";
|
||||
|
||||
// components
|
||||
import { EstimateColumn } from "components/core";
|
||||
// hooks
|
||||
import useSubIssue from "hooks/use-sub-issue";
|
||||
// types
|
||||
import { ICurrentUserResponse, IIssue, Properties } from "types";
|
||||
|
||||
type Props = {
|
||||
issue: IIssue;
|
||||
projectId: string;
|
||||
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
|
||||
expandedIssues: string[];
|
||||
properties: Properties;
|
||||
user: ICurrentUserResponse | undefined;
|
||||
isNotAllowed: boolean;
|
||||
};
|
||||
|
||||
export const SpreadsheetEstimateColumn: React.FC<Props> = ({
|
||||
issue,
|
||||
projectId,
|
||||
partialUpdateIssue,
|
||||
expandedIssues,
|
||||
properties,
|
||||
user,
|
||||
isNotAllowed,
|
||||
}) => {
|
||||
const isExpanded = expandedIssues.indexOf(issue.id) > -1;
|
||||
|
||||
const { subIssues, isLoading } = useSubIssue(issue.project_detail.id, issue.id, isExpanded);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<EstimateColumn
|
||||
issue={issue}
|
||||
projectId={projectId}
|
||||
properties={properties}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
|
||||
{isExpanded &&
|
||||
!isLoading &&
|
||||
subIssues &&
|
||||
subIssues.length > 0 &&
|
||||
subIssues.map((subIssue: IIssue) => (
|
||||
<SpreadsheetEstimateColumn
|
||||
key={subIssue.id}
|
||||
issue={subIssue}
|
||||
projectId={subIssue.project_detail.id}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
expandedIssues={expandedIssues}
|
||||
properties={properties}
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,4 +1,14 @@
|
||||
export * from "./assignee-column";
|
||||
export * from "./created-on-column";
|
||||
export * from "./due-date-column";
|
||||
export * from "./estimate-column";
|
||||
export * from "./issue-column";
|
||||
export * from "./label-column";
|
||||
export * from "./priority-column";
|
||||
export * from "./start-date-column";
|
||||
export * from "./state-column";
|
||||
export * from "./updated-on-column";
|
||||
export * from "./spreadsheet-view";
|
||||
export * from "./single-issue";
|
||||
export * from "./issue-column/issue-column";
|
||||
export * from "./spreadsheet-columns";
|
||||
export * from "./spreadsheet-issues";
|
||||
export * from "./issue-column/spreadsheet-issue-column";
|
||||
|
@ -0,0 +1,2 @@
|
||||
export * from "./spreadsheet-issue-column";
|
||||
export * from "./issue-column";
|
@ -0,0 +1,179 @@
|
||||
import React, { useState } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// components
|
||||
import { Popover2 } from "@blueprintjs/popover2";
|
||||
// icons
|
||||
import { Icon } from "components/ui";
|
||||
import {
|
||||
EllipsisHorizontalIcon,
|
||||
LinkIcon,
|
||||
PencilIcon,
|
||||
TrashIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// types
|
||||
import { IIssue, Properties, UserAuth } from "types";
|
||||
// helper
|
||||
import { copyTextToClipboard } from "helpers/string.helper";
|
||||
|
||||
type Props = {
|
||||
issue: IIssue;
|
||||
projectId: string;
|
||||
expanded: boolean;
|
||||
handleToggleExpand: (issueId: string) => void;
|
||||
properties: Properties;
|
||||
handleEditIssue: (issue: IIssue) => void;
|
||||
handleDeleteIssue: (issue: IIssue) => void;
|
||||
setCurrentProjectId: React.Dispatch<React.SetStateAction<string | null>>;
|
||||
disableUserActions: boolean;
|
||||
userAuth: UserAuth;
|
||||
nestingLevel: number;
|
||||
};
|
||||
|
||||
export const IssueColumn: React.FC<Props> = ({
|
||||
issue,
|
||||
projectId,
|
||||
expanded,
|
||||
handleToggleExpand,
|
||||
properties,
|
||||
handleEditIssue,
|
||||
handleDeleteIssue,
|
||||
setCurrentProjectId,
|
||||
disableUserActions,
|
||||
userAuth,
|
||||
nestingLevel,
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const openPeekOverview = () => {
|
||||
const { query } = router;
|
||||
setCurrentProjectId(issue.project_detail.id);
|
||||
router.push({
|
||||
pathname: router.pathname,
|
||||
query: { ...query, peekIssue: issue.id },
|
||||
});
|
||||
};
|
||||
|
||||
const handleCopyText = () => {
|
||||
const originURL =
|
||||
typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
|
||||
copyTextToClipboard(
|
||||
`${originURL}/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`
|
||||
).then(() => {
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Link Copied!",
|
||||
message: "Issue link copied to clipboard.",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const paddingLeft = `${nestingLevel * 54}px`;
|
||||
|
||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
|
||||
|
||||
return (
|
||||
<div className="group flex items-center w-[28rem] text-sm h-11 sticky top-0 bg-custom-background-100 truncate border-b border-r border-custom-border-100">
|
||||
<div
|
||||
className="flex gap-1.5 px-4 pr-0 py-2.5 items-center"
|
||||
style={issue.parent ? { paddingLeft } : {}}
|
||||
>
|
||||
<div className="relative flex items-center cursor-pointer text-xs text-center hover:text-custom-text-100">
|
||||
{properties.key && (
|
||||
<span className="flex items-center justify-center font-medium opacity-100 group-hover:opacity-0">
|
||||
{issue.project_detail?.identifier}-{issue.sequence_id}
|
||||
</span>
|
||||
)}
|
||||
{!isNotAllowed && !disableUserActions && (
|
||||
<div className="absolute top-0 left-2.5 opacity-0 group-hover:opacity-100">
|
||||
<Popover2
|
||||
isOpen={isOpen}
|
||||
canEscapeKeyClose
|
||||
onInteraction={(nextOpenState) => setIsOpen(nextOpenState)}
|
||||
content={
|
||||
<div
|
||||
className={`flex flex-col gap-1.5 overflow-y-scroll whitespace-nowrap rounded-md border p-1 text-xs shadow-lg focus:outline-none max-h-44 min-w-full border-custom-border-200 bg-custom-background-90`}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="hover:text-custom-text-200 w-full select-none gap-2 truncate rounded px-1 py-1.5 text-left text-custom-text-200 hover:bg-custom-background-80"
|
||||
onClick={() => {
|
||||
handleEditIssue(issue);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
<PencilIcon className="h-4 w-4" />
|
||||
<span>Edit issue</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="hover:text-custom-text-200 w-full select-none gap-2 truncate rounded px-1 py-1.5 text-left text-custom-text-200 hover:bg-custom-background-80"
|
||||
onClick={() => {
|
||||
handleDeleteIssue(issue);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
<span>Delete issue</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="hover:text-custom-text-200 w-full select-none gap-2 truncate rounded px-1 py-1.5 text-left text-custom-text-200 hover:bg-custom-background-80"
|
||||
onClick={() => {
|
||||
handleCopyText();
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
<LinkIcon className="h-4 w-4" />
|
||||
<span>Copy issue link</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
placement="bottom-start"
|
||||
>
|
||||
<EllipsisHorizontalIcon className="h-5 w-5 text-custom-text-200" />
|
||||
</Popover2>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{issue.sub_issues_count > 0 && (
|
||||
<div className="h-6 w-6 flex justify-center items-center">
|
||||
<button
|
||||
className="h-5 w-5 hover:bg-custom-background-90 hover:text-custom-text-100 rounded-sm cursor-pointer"
|
||||
onClick={() => handleToggleExpand(issue.id)}
|
||||
>
|
||||
<Icon iconName="chevron_right" className={`${expanded ? "rotate-90" : ""}`} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<span className="flex items-center px-4 py-2.5 h-full truncate flex-grow">
|
||||
<button
|
||||
type="button"
|
||||
className="truncate text-custom-text-100 text-left cursor-pointer w-full text-[0.825rem]"
|
||||
onClick={openPeekOverview}
|
||||
>
|
||||
{issue.name}
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,36 +1,34 @@
|
||||
import React, { useState } from "react";
|
||||
import React from "react";
|
||||
|
||||
// components
|
||||
import { SingleSpreadsheetIssue } from "components/core";
|
||||
import { IssueColumn } from "components/core";
|
||||
// hooks
|
||||
import useSubIssue from "hooks/use-sub-issue";
|
||||
// types
|
||||
import { ICurrentUserResponse, IIssue, Properties, UserAuth } from "types";
|
||||
import { IIssue, Properties, UserAuth } from "types";
|
||||
|
||||
type Props = {
|
||||
issue: IIssue;
|
||||
index: number;
|
||||
projectId: string;
|
||||
expandedIssues: string[];
|
||||
setExpandedIssues: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
properties: Properties;
|
||||
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
|
||||
gridTemplateColumns: string;
|
||||
setCurrentProjectId: React.Dispatch<React.SetStateAction<string | null>>;
|
||||
disableUserActions: boolean;
|
||||
user: ICurrentUserResponse | undefined;
|
||||
userAuth: UserAuth;
|
||||
nestingLevel?: number;
|
||||
};
|
||||
|
||||
export const SpreadsheetIssues: React.FC<Props> = ({
|
||||
index,
|
||||
export const SpreadsheetIssuesColumn: React.FC<Props> = ({
|
||||
issue,
|
||||
projectId,
|
||||
expandedIssues,
|
||||
setExpandedIssues,
|
||||
gridTemplateColumns,
|
||||
properties,
|
||||
handleIssueAction,
|
||||
setCurrentProjectId,
|
||||
disableUserActions,
|
||||
user,
|
||||
userAuth,
|
||||
nestingLevel = 0,
|
||||
}) => {
|
||||
@ -49,21 +47,20 @@ export const SpreadsheetIssues: React.FC<Props> = ({
|
||||
|
||||
const isExpanded = expandedIssues.indexOf(issue.id) > -1;
|
||||
|
||||
const { subIssues, isLoading } = useSubIssue(issue.id, isExpanded);
|
||||
const { subIssues, isLoading } = useSubIssue(issue.project_detail.id, issue.id, isExpanded);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SingleSpreadsheetIssue
|
||||
<IssueColumn
|
||||
issue={issue}
|
||||
index={index}
|
||||
projectId={projectId}
|
||||
expanded={isExpanded}
|
||||
handleToggleExpand={handleToggleExpand}
|
||||
gridTemplateColumns={gridTemplateColumns}
|
||||
properties={properties}
|
||||
handleEditIssue={() => handleIssueAction(issue, "edit")}
|
||||
handleDeleteIssue={() => handleIssueAction(issue, "delete")}
|
||||
setCurrentProjectId={setCurrentProjectId}
|
||||
disableUserActions={disableUserActions}
|
||||
user={user}
|
||||
userAuth={userAuth}
|
||||
nestingLevel={nestingLevel}
|
||||
/>
|
||||
@ -73,17 +70,16 @@ export const SpreadsheetIssues: React.FC<Props> = ({
|
||||
subIssues &&
|
||||
subIssues.length > 0 &&
|
||||
subIssues.map((subIssue: IIssue) => (
|
||||
<SpreadsheetIssues
|
||||
<SpreadsheetIssuesColumn
|
||||
key={subIssue.id}
|
||||
issue={subIssue}
|
||||
index={index}
|
||||
projectId={subIssue.project_detail.id}
|
||||
expandedIssues={expandedIssues}
|
||||
setExpandedIssues={setExpandedIssues}
|
||||
gridTemplateColumns={gridTemplateColumns}
|
||||
properties={properties}
|
||||
handleIssueAction={handleIssueAction}
|
||||
setCurrentProjectId={setCurrentProjectId}
|
||||
disableUserActions={disableUserActions}
|
||||
user={user}
|
||||
userAuth={userAuth}
|
||||
nestingLevel={nestingLevel + 1}
|
||||
/>
|
@ -0,0 +1,2 @@
|
||||
export * from "./spreadsheet-label-column";
|
||||
export * from "./label-column";
|
@ -0,0 +1,47 @@
|
||||
import React from "react";
|
||||
|
||||
// components
|
||||
import { LabelSelect } from "components/project";
|
||||
// types
|
||||
import { ICurrentUserResponse, IIssue, Properties } from "types";
|
||||
|
||||
type Props = {
|
||||
issue: IIssue;
|
||||
projectId: string;
|
||||
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
|
||||
properties: Properties;
|
||||
user: ICurrentUserResponse | undefined;
|
||||
isNotAllowed: boolean;
|
||||
};
|
||||
|
||||
export const LabelColumn: React.FC<Props> = ({
|
||||
issue,
|
||||
projectId,
|
||||
partialUpdateIssue,
|
||||
properties,
|
||||
user,
|
||||
isNotAllowed,
|
||||
}) => {
|
||||
const handleLabelChange = (data: any) => {
|
||||
partialUpdateIssue({ labels_list: data }, issue);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center text-sm h-11 w-full bg-custom-background-100">
|
||||
<span className="flex items-center px-4 py-2.5 h-full w-full flex-shrink-0 border-r border-b border-custom-border-100">
|
||||
{properties.labels && (
|
||||
<LabelSelect
|
||||
value={issue.labels}
|
||||
projectId={projectId}
|
||||
onChange={handleLabelChange}
|
||||
labelsDetails={issue.label_details}
|
||||
hideDropdownArrow
|
||||
maxRender={1}
|
||||
user={user}
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,62 @@
|
||||
import React from "react";
|
||||
|
||||
// components
|
||||
import { LabelColumn } from "components/core";
|
||||
// hooks
|
||||
import useSubIssue from "hooks/use-sub-issue";
|
||||
// types
|
||||
import { ICurrentUserResponse, IIssue, Properties } from "types";
|
||||
|
||||
type Props = {
|
||||
issue: IIssue;
|
||||
projectId: string;
|
||||
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
|
||||
expandedIssues: string[];
|
||||
properties: Properties;
|
||||
user: ICurrentUserResponse | undefined;
|
||||
isNotAllowed: boolean;
|
||||
};
|
||||
|
||||
export const SpreadsheetLabelColumn: React.FC<Props> = ({
|
||||
issue,
|
||||
projectId,
|
||||
partialUpdateIssue,
|
||||
expandedIssues,
|
||||
properties,
|
||||
user,
|
||||
isNotAllowed,
|
||||
}) => {
|
||||
const isExpanded = expandedIssues.indexOf(issue.id) > -1;
|
||||
|
||||
const { subIssues, isLoading } = useSubIssue(issue.project_detail.id, issue.id, isExpanded);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<LabelColumn
|
||||
issue={issue}
|
||||
projectId={projectId}
|
||||
properties={properties}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
|
||||
{isExpanded &&
|
||||
!isLoading &&
|
||||
subIssues &&
|
||||
subIssues.length > 0 &&
|
||||
subIssues.map((subIssue: IIssue) => (
|
||||
<SpreadsheetLabelColumn
|
||||
key={subIssue.id}
|
||||
issue={subIssue}
|
||||
projectId={subIssue.project_detail.id}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
expandedIssues={expandedIssues}
|
||||
properties={properties}
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,2 @@
|
||||
export * from "./spreadsheet-priority-column";
|
||||
export * from "./priority-column";
|
@ -0,0 +1,64 @@
|
||||
import React from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// components
|
||||
import { PrioritySelect } from "components/project";
|
||||
// services
|
||||
import trackEventServices from "services/track-event.service";
|
||||
// types
|
||||
import { ICurrentUserResponse, IIssue, Properties, TIssuePriorities } from "types";
|
||||
|
||||
type Props = {
|
||||
issue: IIssue;
|
||||
projectId: string;
|
||||
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
|
||||
properties: Properties;
|
||||
user: ICurrentUserResponse | undefined;
|
||||
isNotAllowed: boolean;
|
||||
};
|
||||
|
||||
export const PriorityColumn: React.FC<Props> = ({
|
||||
issue,
|
||||
projectId,
|
||||
partialUpdateIssue,
|
||||
properties,
|
||||
user,
|
||||
isNotAllowed,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const handlePriorityChange = (data: TIssuePriorities) => {
|
||||
partialUpdateIssue({ priority: data }, issue);
|
||||
trackEventServices.trackIssuePartialPropertyUpdateEvent(
|
||||
{
|
||||
workspaceSlug,
|
||||
workspaceId: issue.workspace,
|
||||
projectId: issue.project_detail.id,
|
||||
projectIdentifier: issue.project_detail.identifier,
|
||||
projectName: issue.project_detail.name,
|
||||
issueId: issue.id,
|
||||
},
|
||||
"ISSUE_PROPERTY_UPDATE_PRIORITY",
|
||||
user
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center text-sm h-11 w-full bg-custom-background-100">
|
||||
<span className="flex items-center px-4 py-2.5 h-full w-full flex-shrink-0 border-r border-b border-custom-border-100">
|
||||
{properties.priority && (
|
||||
<PrioritySelect
|
||||
value={issue.priority}
|
||||
onChange={handlePriorityChange}
|
||||
buttonClassName="!p-0 !rounded-none !shadow-none !border-0"
|
||||
hideDropdownArrow
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,62 @@
|
||||
import React from "react";
|
||||
|
||||
// components
|
||||
import { PriorityColumn } from "components/core";
|
||||
// hooks
|
||||
import useSubIssue from "hooks/use-sub-issue";
|
||||
// types
|
||||
import { ICurrentUserResponse, IIssue, Properties } from "types";
|
||||
|
||||
type Props = {
|
||||
issue: IIssue;
|
||||
projectId: string;
|
||||
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
|
||||
expandedIssues: string[];
|
||||
properties: Properties;
|
||||
user: ICurrentUserResponse | undefined;
|
||||
isNotAllowed: boolean;
|
||||
};
|
||||
|
||||
export const SpreadsheetPriorityColumn: React.FC<Props> = ({
|
||||
issue,
|
||||
projectId,
|
||||
partialUpdateIssue,
|
||||
expandedIssues,
|
||||
properties,
|
||||
user,
|
||||
isNotAllowed,
|
||||
}) => {
|
||||
const isExpanded = expandedIssues.indexOf(issue.id) > -1;
|
||||
|
||||
const { subIssues, isLoading } = useSubIssue(issue.project_detail.id, issue.id, isExpanded);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PriorityColumn
|
||||
issue={issue}
|
||||
projectId={projectId}
|
||||
properties={properties}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
|
||||
{isExpanded &&
|
||||
!isLoading &&
|
||||
subIssues &&
|
||||
subIssues.length > 0 &&
|
||||
subIssues.map((subIssue: IIssue) => (
|
||||
<SpreadsheetPriorityColumn
|
||||
key={subIssue.id}
|
||||
issue={subIssue}
|
||||
projectId={subIssue.project_detail.id}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
expandedIssues={expandedIssues}
|
||||
properties={properties}
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -49,6 +49,7 @@ import { renderLongDetailDateFormat } from "helpers/date-time.helper";
|
||||
|
||||
type Props = {
|
||||
issue: IIssue;
|
||||
projectId: string;
|
||||
index: number;
|
||||
expanded: boolean;
|
||||
handleToggleExpand: (issueId: string) => void;
|
||||
@ -64,6 +65,7 @@ type Props = {
|
||||
|
||||
export const SingleSpreadsheetIssue: React.FC<Props> = ({
|
||||
issue,
|
||||
projectId,
|
||||
index,
|
||||
expanded,
|
||||
handleToggleExpand,
|
||||
@ -80,7 +82,7 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
|
||||
const { workspaceSlug, cycleId, moduleId, viewId } = router.query;
|
||||
|
||||
const { params } = useSpreadsheetIssuesView();
|
||||
|
||||
@ -96,7 +98,7 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
|
||||
? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), params)
|
||||
: viewId
|
||||
? VIEW_ISSUES(viewId.toString(), params)
|
||||
: PROJECT_ISSUES_LIST_WITH_PARAMS(projectId.toString(), params);
|
||||
: PROJECT_ISSUES_LIST_WITH_PARAMS(projectId, params);
|
||||
|
||||
if (issue.parent)
|
||||
mutate<ISubIssueResponse>(
|
||||
@ -136,13 +138,7 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
|
||||
);
|
||||
|
||||
issuesService
|
||||
.patchIssue(
|
||||
workspaceSlug as string,
|
||||
projectId as string,
|
||||
issue.id as string,
|
||||
formData,
|
||||
user
|
||||
)
|
||||
.patchIssue(workspaceSlug as string, projectId, issue.id as string, formData, user)
|
||||
.then(() => {
|
||||
if (issue.parent) {
|
||||
mutate(SUB_ISSUES(issue.parent as string));
|
||||
@ -368,6 +364,7 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
|
||||
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
|
||||
<StateSelect
|
||||
value={issue.state_detail}
|
||||
projectId={projectId}
|
||||
onChange={handleStateChange}
|
||||
buttonClassName="!p-0 !rounded-none !shadow-none !border-0"
|
||||
hideDropdownArrow
|
||||
@ -390,6 +387,7 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
|
||||
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
|
||||
<MembersSelect
|
||||
value={issue.assignees}
|
||||
projectId={projectId}
|
||||
onChange={handleAssigneeChange}
|
||||
membersDetails={issue.assignee_details}
|
||||
buttonClassName="!p-0 !rounded-none !shadow-none !border-0"
|
||||
@ -402,6 +400,7 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
|
||||
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
|
||||
<LabelSelect
|
||||
value={issue.labels}
|
||||
projectId={projectId}
|
||||
onChange={handleLabelChange}
|
||||
labelsDetails={issue.label_details}
|
||||
hideDropdownArrow
|
||||
|
@ -1,23 +1,54 @@
|
||||
import React, { useState } from "react";
|
||||
import React, { useCallback, useState } from "react";
|
||||
|
||||
// next
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { KeyedMutator, mutate } from "swr";
|
||||
|
||||
// components
|
||||
import { SpreadsheetColumns, SpreadsheetIssues, ListInlineCreateIssueForm } from "components/core";
|
||||
import {
|
||||
ListInlineCreateIssueForm,
|
||||
SpreadsheetAssigneeColumn,
|
||||
SpreadsheetCreatedOnColumn,
|
||||
SpreadsheetDueDateColumn,
|
||||
SpreadsheetEstimateColumn,
|
||||
SpreadsheetIssuesColumn,
|
||||
SpreadsheetLabelColumn,
|
||||
SpreadsheetPriorityColumn,
|
||||
SpreadsheetStartDateColumn,
|
||||
SpreadsheetStateColumn,
|
||||
SpreadsheetUpdatedOnColumn,
|
||||
} from "components/core";
|
||||
import { CustomMenu, Spinner } from "components/ui";
|
||||
import { IssuePeekOverview } from "components/issues";
|
||||
// hooks
|
||||
import useIssuesProperties from "hooks/use-issue-properties";
|
||||
import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view";
|
||||
// types
|
||||
import { ICurrentUserResponse, IIssue, Properties, UserAuth } from "types";
|
||||
// constants
|
||||
import { SPREADSHEET_COLUMN } from "constants/spreadsheet";
|
||||
import { ICurrentUserResponse, IIssue, ISubIssueResponse, UserAuth } from "types";
|
||||
import useWorkspaceIssuesFilters from "hooks/use-worskpace-issue-filter";
|
||||
import {
|
||||
CYCLE_DETAILS,
|
||||
CYCLE_ISSUES_WITH_PARAMS,
|
||||
MODULE_DETAILS,
|
||||
MODULE_ISSUES_WITH_PARAMS,
|
||||
PROJECT_ISSUES_LIST_WITH_PARAMS,
|
||||
SUB_ISSUES,
|
||||
VIEW_ISSUES,
|
||||
WORKSPACE_VIEW_ISSUES,
|
||||
} from "constants/fetch-keys";
|
||||
import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view";
|
||||
import projectIssuesServices from "services/issues.service";
|
||||
// icon
|
||||
import { PlusIcon } from "@heroicons/react/24/outline";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
|
||||
type Props = {
|
||||
spreadsheetIssues: IIssue[];
|
||||
mutateIssues: KeyedMutator<
|
||||
| IIssue[]
|
||||
| {
|
||||
[key: string]: IIssue[];
|
||||
}
|
||||
>;
|
||||
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
|
||||
openIssuesListModal?: (() => void) | null;
|
||||
disableUserActions: boolean;
|
||||
@ -26,6 +57,8 @@ type Props = {
|
||||
};
|
||||
|
||||
export const SpreadsheetView: React.FC<Props> = ({
|
||||
spreadsheetIssues,
|
||||
mutateIssues,
|
||||
handleIssueAction,
|
||||
openIssuesListModal,
|
||||
disableUserActions,
|
||||
@ -33,121 +66,280 @@ export const SpreadsheetView: React.FC<Props> = ({
|
||||
userAuth,
|
||||
}) => {
|
||||
const [expandedIssues, setExpandedIssues] = useState<string[]>([]);
|
||||
const [currentProjectId, setCurrentProjectId] = useState<string | null>(null);
|
||||
|
||||
const [isInlineCreateIssueFormOpen, setIsInlineCreateIssueFormOpen] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
||||
const { workspaceSlug, projectId, cycleId, moduleId, viewId, workspaceViewId } = router.query;
|
||||
|
||||
const type = cycleId ? "cycle" : moduleId ? "module" : "issue";
|
||||
|
||||
const { spreadsheetIssues, mutateIssues } = useSpreadsheetIssuesView();
|
||||
|
||||
const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string);
|
||||
|
||||
const columnData = SPREADSHEET_COLUMN.map((column) => ({
|
||||
...column,
|
||||
isActive: properties
|
||||
? column.propertyName === "labels"
|
||||
? properties[column.propertyName as keyof Properties]
|
||||
: column.propertyName === "title"
|
||||
? true
|
||||
: properties[column.propertyName as keyof Properties]
|
||||
: false,
|
||||
}));
|
||||
const workspaceIssuesPath = [
|
||||
{
|
||||
params: {
|
||||
sub_issue: false,
|
||||
},
|
||||
path: "workspace-views/all-issues",
|
||||
},
|
||||
{
|
||||
params: {
|
||||
assignees: user?.id ?? undefined,
|
||||
sub_issue: false,
|
||||
},
|
||||
path: "workspace-views/assigned",
|
||||
},
|
||||
{
|
||||
params: {
|
||||
created_by: user?.id ?? undefined,
|
||||
sub_issue: false,
|
||||
},
|
||||
path: "workspace-views/created",
|
||||
},
|
||||
{
|
||||
params: {
|
||||
subscriber: user?.id ?? undefined,
|
||||
sub_issue: false,
|
||||
},
|
||||
path: "workspace-views/subscribed",
|
||||
},
|
||||
];
|
||||
|
||||
const gridTemplateColumns = columnData
|
||||
.filter((column) => column.isActive)
|
||||
.map((column) => column.colSize)
|
||||
.join(" ");
|
||||
const currentWorkspaceIssuePath = workspaceIssuesPath.find((path) =>
|
||||
router.pathname.includes(path.path)
|
||||
);
|
||||
|
||||
const { params: workspaceViewParams } = useWorkspaceIssuesFilters(
|
||||
workspaceSlug?.toString(),
|
||||
workspaceViewId?.toString()
|
||||
);
|
||||
|
||||
const { params } = useSpreadsheetIssuesView();
|
||||
|
||||
const partialUpdateIssue = useCallback(
|
||||
(formData: Partial<IIssue>, issue: IIssue) => {
|
||||
if (!workspaceSlug || !issue) return;
|
||||
|
||||
const fetchKey = cycleId
|
||||
? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), params)
|
||||
: moduleId
|
||||
? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), params)
|
||||
: viewId
|
||||
? VIEW_ISSUES(viewId.toString(), params)
|
||||
: workspaceViewId
|
||||
? WORKSPACE_VIEW_ISSUES(workspaceSlug.toString(), workspaceViewParams)
|
||||
: currentWorkspaceIssuePath
|
||||
? WORKSPACE_VIEW_ISSUES(workspaceSlug.toString(), currentWorkspaceIssuePath?.params)
|
||||
: PROJECT_ISSUES_LIST_WITH_PARAMS(issue.project_detail.id, params);
|
||||
|
||||
if (issue.parent)
|
||||
mutate<ISubIssueResponse>(
|
||||
SUB_ISSUES(issue.parent.toString()),
|
||||
(prevData) => {
|
||||
if (!prevData) return prevData;
|
||||
|
||||
return {
|
||||
...prevData,
|
||||
sub_issues: (prevData.sub_issues ?? []).map((i) => {
|
||||
if (i.id === issue.id) {
|
||||
return {
|
||||
...i,
|
||||
...formData,
|
||||
};
|
||||
}
|
||||
return i;
|
||||
}),
|
||||
};
|
||||
},
|
||||
false
|
||||
);
|
||||
else
|
||||
mutate<IIssue[]>(
|
||||
fetchKey,
|
||||
(prevData) =>
|
||||
(prevData ?? []).map((p) => {
|
||||
if (p.id === issue.id) {
|
||||
return {
|
||||
...p,
|
||||
...formData,
|
||||
};
|
||||
}
|
||||
return p;
|
||||
}),
|
||||
false
|
||||
);
|
||||
|
||||
projectIssuesServices
|
||||
.patchIssue(
|
||||
workspaceSlug as string,
|
||||
issue.project_detail.id,
|
||||
issue.id as string,
|
||||
formData,
|
||||
user
|
||||
)
|
||||
.then(() => {
|
||||
if (issue.parent) {
|
||||
mutate(SUB_ISSUES(issue.parent as string));
|
||||
} else {
|
||||
mutate(fetchKey);
|
||||
|
||||
if (cycleId) mutate(CYCLE_DETAILS(cycleId as string));
|
||||
if (moduleId) mutate(MODULE_DETAILS(moduleId as string));
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
},
|
||||
[
|
||||
workspaceSlug,
|
||||
cycleId,
|
||||
moduleId,
|
||||
viewId,
|
||||
workspaceViewId,
|
||||
currentWorkspaceIssuePath,
|
||||
workspaceViewParams,
|
||||
params,
|
||||
user,
|
||||
]
|
||||
);
|
||||
|
||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
|
||||
|
||||
const renderColumn = (header: string, Component: React.ComponentType<any>) => (
|
||||
<div className="relative flex flex-col h-max w-full bg-custom-background-100 rounded-sm">
|
||||
<div className="flex items-center min-w-[9rem] px-4 py-2.5 text-sm font-medium z-[1] h-11 w-full sticky top-0 bg-custom-background-90 border border-l-0 border-custom-border-200">
|
||||
{header}
|
||||
</div>
|
||||
<div className="h-full min-w-[9rem] w-full">
|
||||
{spreadsheetIssues.map((issue: IIssue, index) => (
|
||||
<Component
|
||||
key={`${issue.id}_${index}`}
|
||||
issue={issue}
|
||||
projectId={issue.project_detail.id}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
expandedIssues={expandedIssues}
|
||||
properties={properties}
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<IssuePeekOverview
|
||||
handleMutation={() => mutateIssues()}
|
||||
projectId={projectId?.toString() ?? ""}
|
||||
projectId={currentProjectId ?? ""}
|
||||
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
||||
readOnly={disableUserActions}
|
||||
/>
|
||||
<div className="h-full rounded-lg text-custom-text-200 overflow-x-auto whitespace-nowrap bg-custom-background-100">
|
||||
<div className="sticky z-[2] top-0 border-b border-custom-border-200 bg-custom-background-90 w-full min-w-max">
|
||||
<SpreadsheetColumns columnData={columnData} gridTemplateColumns={gridTemplateColumns} />
|
||||
</div>
|
||||
{spreadsheetIssues ? (
|
||||
<div className="flex flex-col h-full w-full bg-custom-background-100 rounded-sm ">
|
||||
{spreadsheetIssues.map((issue: IIssue, index) => (
|
||||
<SpreadsheetIssues
|
||||
key={`${issue.id}_${index}`}
|
||||
index={index}
|
||||
issue={issue}
|
||||
expandedIssues={expandedIssues}
|
||||
setExpandedIssues={setExpandedIssues}
|
||||
gridTemplateColumns={gridTemplateColumns}
|
||||
properties={properties}
|
||||
handleIssueAction={handleIssueAction}
|
||||
disableUserActions={disableUserActions}
|
||||
user={user}
|
||||
userAuth={userAuth}
|
||||
/>
|
||||
))}
|
||||
<div className="relative flex h-full w-full rounded-lg text-custom-text-200 overflow-x-auto whitespace-nowrap bg-custom-background-100">
|
||||
<div className="h-full w-full flex flex-col">
|
||||
<div className="flex max-h-full overflow-y-auto">
|
||||
{spreadsheetIssues ? (
|
||||
<>
|
||||
<div className="sticky left-0 w-[28rem] z-[2]">
|
||||
<div className="relative flex flex-col h-max w-full bg-custom-background-100 rounded-sm z-[2]">
|
||||
<div className="flex items-center text-sm font-medium z-[2] h-11 w-full sticky top-0 bg-custom-background-90 border border-l-0 border-custom-border-200">
|
||||
<span className="flex items-center px-4 py-2.5 h-full w-20 flex-shrink-0">
|
||||
ID
|
||||
</span>
|
||||
<span className="flex items-center px-4 py-2.5 h-full w-full flex-grow">
|
||||
Issue
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<ListInlineCreateIssueForm
|
||||
isOpen={isInlineCreateIssueFormOpen}
|
||||
handleClose={() => setIsInlineCreateIssueFormOpen(false)}
|
||||
prePopulatedData={{
|
||||
...(cycleId && { cycle: cycleId.toString() }),
|
||||
...(moduleId && { module: moduleId.toString() }),
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
className="relative group grid auto-rows-[minmax(44px,1fr)] hover:rounded-sm hover:bg-custom-background-80 border-b border-custom-border-200 w-full min-w-max"
|
||||
style={{ gridTemplateColumns }}
|
||||
>
|
||||
{!isInlineCreateIssueFormOpen && (
|
||||
<>
|
||||
{type === "issue" ? (
|
||||
<button
|
||||
className="flex gap-1.5 items-center pl-7 py-2.5 text-sm sticky left-0 z-[1] text-custom-text-200 bg-custom-background-100 group-hover:text-custom-text-100 group-hover:bg-custom-background-80 border-custom-border-200 w-full"
|
||||
onClick={() => setIsInlineCreateIssueFormOpen(true)}
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
Add Issue
|
||||
</button>
|
||||
) : (
|
||||
!disableUserActions && (
|
||||
<CustomMenu
|
||||
className="sticky left-0 z-[1]"
|
||||
customButton={
|
||||
<button
|
||||
className="flex gap-1.5 items-center pl-7 py-2.5 text-sm sticky left-0 z-[1] text-custom-text-200 bg-custom-background-100 group-hover:text-custom-text-100 group-hover:bg-custom-background-80 border-custom-border-200 w-full"
|
||||
type="button"
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
Add Issue
|
||||
</button>
|
||||
}
|
||||
position="left"
|
||||
optionsClassName="left-5 !w-36"
|
||||
noBorder
|
||||
>
|
||||
<CustomMenu.MenuItem onClick={() => setIsInlineCreateIssueFormOpen(true)}>
|
||||
Create new
|
||||
</CustomMenu.MenuItem>
|
||||
{openIssuesListModal && (
|
||||
<CustomMenu.MenuItem onClick={openIssuesListModal}>
|
||||
Add an existing issue
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
</CustomMenu>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{spreadsheetIssues.map((issue: IIssue, index) => (
|
||||
<SpreadsheetIssuesColumn
|
||||
key={`${issue.id}_${index}`}
|
||||
issue={issue}
|
||||
projectId={issue.project_detail.id}
|
||||
expandedIssues={expandedIssues}
|
||||
setExpandedIssues={setExpandedIssues}
|
||||
setCurrentProjectId={setCurrentProjectId}
|
||||
properties={properties}
|
||||
handleIssueAction={handleIssueAction}
|
||||
disableUserActions={disableUserActions}
|
||||
userAuth={userAuth}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{renderColumn("State", SpreadsheetStateColumn)}
|
||||
{renderColumn("Priority", SpreadsheetPriorityColumn)}
|
||||
{renderColumn("Assignees", SpreadsheetAssigneeColumn)}
|
||||
{renderColumn("Label", SpreadsheetLabelColumn)}
|
||||
{renderColumn("Start Date", SpreadsheetStartDateColumn)}
|
||||
{renderColumn("Due Date", SpreadsheetDueDateColumn)}
|
||||
{renderColumn("Estimate", SpreadsheetEstimateColumn)}
|
||||
{renderColumn("Created On", SpreadsheetCreatedOnColumn)}
|
||||
{renderColumn("Updated On", SpreadsheetUpdatedOnColumn)}
|
||||
</>
|
||||
) : (
|
||||
<div className="flex flex-col justify-center items-center h-full w-full">
|
||||
<Spinner />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<Spinner />
|
||||
)}
|
||||
|
||||
<div className="border-t border-custom-border-100">
|
||||
<div className="mb-3 z-50 sticky bottom-0 left-0">
|
||||
<ListInlineCreateIssueForm
|
||||
isOpen={isInlineCreateIssueFormOpen}
|
||||
handleClose={() => setIsInlineCreateIssueFormOpen(false)}
|
||||
prePopulatedData={{
|
||||
...(cycleId && { cycle: cycleId.toString() }),
|
||||
...(moduleId && { module: moduleId.toString() }),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{type === "issue"
|
||||
? !disableUserActions &&
|
||||
!isInlineCreateIssueFormOpen && (
|
||||
<button
|
||||
className="flex gap-1.5 items-center text-custom-primary-100 pl-4 py-2.5 text-sm sticky left-0 z-[1] w-full"
|
||||
onClick={() => setIsInlineCreateIssueFormOpen(true)}
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
New Issue
|
||||
</button>
|
||||
)
|
||||
: !disableUserActions &&
|
||||
!isInlineCreateIssueFormOpen && (
|
||||
<CustomMenu
|
||||
className="sticky left-0 z-10"
|
||||
customButton={
|
||||
<button
|
||||
className="flex gap-1.5 items-center text-custom-primary-100 pl-4 py-2.5 text-sm sticky left-0 z-[1] border-custom-border-200 w-full"
|
||||
type="button"
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
New Issue
|
||||
</button>
|
||||
}
|
||||
position="left"
|
||||
verticalPosition="top"
|
||||
optionsClassName="left-5 !w-36"
|
||||
noBorder
|
||||
>
|
||||
<CustomMenu.MenuItem onClick={() => setIsInlineCreateIssueFormOpen(true)}>
|
||||
Create new
|
||||
</CustomMenu.MenuItem>
|
||||
{openIssuesListModal && (
|
||||
<CustomMenu.MenuItem onClick={openIssuesListModal}>
|
||||
Add an existing issue
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
</CustomMenu>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
@ -0,0 +1,2 @@
|
||||
export * from "./spreadsheet-start-date-column";
|
||||
export * from "./start-date-column";
|
@ -0,0 +1,62 @@
|
||||
import React from "react";
|
||||
|
||||
// components
|
||||
import { StartDateColumn } from "components/core";
|
||||
// hooks
|
||||
import useSubIssue from "hooks/use-sub-issue";
|
||||
// types
|
||||
import { ICurrentUserResponse, IIssue, Properties } from "types";
|
||||
|
||||
type Props = {
|
||||
issue: IIssue;
|
||||
projectId: string;
|
||||
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
|
||||
expandedIssues: string[];
|
||||
properties: Properties;
|
||||
user: ICurrentUserResponse | undefined;
|
||||
isNotAllowed: boolean;
|
||||
};
|
||||
|
||||
export const SpreadsheetStartDateColumn: React.FC<Props> = ({
|
||||
issue,
|
||||
projectId,
|
||||
partialUpdateIssue,
|
||||
expandedIssues,
|
||||
properties,
|
||||
user,
|
||||
isNotAllowed,
|
||||
}) => {
|
||||
const isExpanded = expandedIssues.indexOf(issue.id) > -1;
|
||||
|
||||
const { subIssues, isLoading } = useSubIssue(issue.project_detail.id, issue.id, isExpanded);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<StartDateColumn
|
||||
issue={issue}
|
||||
projectId={projectId}
|
||||
properties={properties}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
|
||||
{isExpanded &&
|
||||
!isLoading &&
|
||||
subIssues &&
|
||||
subIssues.length > 0 &&
|
||||
subIssues.map((subIssue: IIssue) => (
|
||||
<SpreadsheetStartDateColumn
|
||||
key={subIssue.id}
|
||||
issue={subIssue}
|
||||
projectId={subIssue.project_detail.id}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
expandedIssues={expandedIssues}
|
||||
properties={properties}
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,38 @@
|
||||
import React from "react";
|
||||
|
||||
// components
|
||||
import { ViewStartDateSelect } from "components/issues";
|
||||
// types
|
||||
import { ICurrentUserResponse, IIssue, Properties } from "types";
|
||||
|
||||
type Props = {
|
||||
issue: IIssue;
|
||||
projectId: string;
|
||||
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
|
||||
properties: Properties;
|
||||
user: ICurrentUserResponse | undefined;
|
||||
isNotAllowed: boolean;
|
||||
};
|
||||
|
||||
export const StartDateColumn: React.FC<Props> = ({
|
||||
issue,
|
||||
projectId,
|
||||
partialUpdateIssue,
|
||||
properties,
|
||||
user,
|
||||
isNotAllowed,
|
||||
}) => (
|
||||
<div className="flex items-center text-sm h-11 w-full bg-custom-background-100">
|
||||
<span className="flex items-center px-4 py-2.5 h-full w-full flex-shrink-0 border-r border-b border-custom-border-100">
|
||||
{properties.due_date && (
|
||||
<ViewStartDateSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
noBorder
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
@ -0,0 +1,2 @@
|
||||
export * from "./spreadsheet-state-column";
|
||||
export * from "./state-column";
|
@ -0,0 +1,62 @@
|
||||
import React from "react";
|
||||
|
||||
// components
|
||||
import { StateColumn } from "components/core";
|
||||
// hooks
|
||||
import useSubIssue from "hooks/use-sub-issue";
|
||||
// types
|
||||
import { ICurrentUserResponse, IIssue, Properties } from "types";
|
||||
|
||||
type Props = {
|
||||
issue: IIssue;
|
||||
projectId: string;
|
||||
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
|
||||
expandedIssues: string[];
|
||||
properties: Properties;
|
||||
user: ICurrentUserResponse | undefined;
|
||||
isNotAllowed: boolean;
|
||||
};
|
||||
|
||||
export const SpreadsheetStateColumn: React.FC<Props> = ({
|
||||
issue,
|
||||
projectId,
|
||||
partialUpdateIssue,
|
||||
expandedIssues,
|
||||
properties,
|
||||
user,
|
||||
isNotAllowed,
|
||||
}) => {
|
||||
const isExpanded = expandedIssues.indexOf(issue.id) > -1;
|
||||
|
||||
const { subIssues, isLoading } = useSubIssue(issue.project_detail.id, issue.id, isExpanded);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<StateColumn
|
||||
issue={issue}
|
||||
projectId={projectId}
|
||||
properties={properties}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
|
||||
{isExpanded &&
|
||||
!isLoading &&
|
||||
subIssues &&
|
||||
subIssues.length > 0 &&
|
||||
subIssues.map((subIssue: IIssue) => (
|
||||
<SpreadsheetStateColumn
|
||||
key={subIssue.id}
|
||||
issue={subIssue}
|
||||
projectId={subIssue.project_detail.id}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
expandedIssues={expandedIssues}
|
||||
properties={properties}
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,87 @@
|
||||
import React from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// components
|
||||
import { StateSelect } from "components/states";
|
||||
// services
|
||||
import trackEventServices from "services/track-event.service";
|
||||
// types
|
||||
import { ICurrentUserResponse, IIssue, IState, Properties } from "types";
|
||||
|
||||
type Props = {
|
||||
issue: IIssue;
|
||||
projectId: string;
|
||||
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
|
||||
properties: Properties;
|
||||
user: ICurrentUserResponse | undefined;
|
||||
isNotAllowed: boolean;
|
||||
};
|
||||
|
||||
export const StateColumn: React.FC<Props> = ({
|
||||
issue,
|
||||
projectId,
|
||||
partialUpdateIssue,
|
||||
properties,
|
||||
user,
|
||||
isNotAllowed,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const handleStateChange = (data: string, states: IState[] | undefined) => {
|
||||
const oldState = states?.find((s) => s.id === issue.state);
|
||||
const newState = states?.find((s) => s.id === data);
|
||||
|
||||
partialUpdateIssue(
|
||||
{
|
||||
state: data,
|
||||
state_detail: newState,
|
||||
},
|
||||
issue
|
||||
);
|
||||
trackEventServices.trackIssuePartialPropertyUpdateEvent(
|
||||
{
|
||||
workspaceSlug,
|
||||
workspaceId: issue.workspace,
|
||||
projectId: issue.project_detail.id,
|
||||
projectIdentifier: issue.project_detail.identifier,
|
||||
projectName: issue.project_detail.name,
|
||||
issueId: issue.id,
|
||||
},
|
||||
"ISSUE_PROPERTY_UPDATE_STATE",
|
||||
user
|
||||
);
|
||||
if (oldState?.group !== "completed" && newState?.group !== "completed") {
|
||||
trackEventServices.trackIssueMarkedAsDoneEvent(
|
||||
{
|
||||
workspaceSlug: issue.workspace_detail.slug,
|
||||
workspaceId: issue.workspace_detail.id,
|
||||
projectId: issue.project_detail.id,
|
||||
projectIdentifier: issue.project_detail.identifier,
|
||||
projectName: issue.project_detail.name,
|
||||
issueId: issue.id,
|
||||
},
|
||||
user
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center text-sm h-11 w-full bg-custom-background-100">
|
||||
<span className="flex items-center px-4 py-2.5 h-full w-full flex-shrink-0 border-r border-b border-custom-border-100">
|
||||
{properties.state && (
|
||||
<StateSelect
|
||||
value={issue.state_detail}
|
||||
projectId={projectId}
|
||||
onChange={handleStateChange}
|
||||
buttonClassName="!p-0 !rounded-none !shadow-none !border-0"
|
||||
hideDropdownArrow
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,2 @@
|
||||
export * from "./spreadsheet-updated-on-column";
|
||||
export * from "./updated-on-column";
|
@ -0,0 +1,62 @@
|
||||
import React from "react";
|
||||
|
||||
// components
|
||||
import { UpdatedOnColumn } from "components/core";
|
||||
// hooks
|
||||
import useSubIssue from "hooks/use-sub-issue";
|
||||
// types
|
||||
import { ICurrentUserResponse, IIssue, Properties } from "types";
|
||||
|
||||
type Props = {
|
||||
issue: IIssue;
|
||||
projectId: string;
|
||||
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
|
||||
expandedIssues: string[];
|
||||
properties: Properties;
|
||||
user: ICurrentUserResponse | undefined;
|
||||
isNotAllowed: boolean;
|
||||
};
|
||||
|
||||
export const SpreadsheetUpdatedOnColumn: React.FC<Props> = ({
|
||||
issue,
|
||||
projectId,
|
||||
partialUpdateIssue,
|
||||
expandedIssues,
|
||||
properties,
|
||||
user,
|
||||
isNotAllowed,
|
||||
}) => {
|
||||
const isExpanded = expandedIssues.indexOf(issue.id) > -1;
|
||||
|
||||
const { subIssues, isLoading } = useSubIssue(issue.project_detail.id, issue.id, isExpanded);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<UpdatedOnColumn
|
||||
issue={issue}
|
||||
projectId={projectId}
|
||||
properties={properties}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
|
||||
{isExpanded &&
|
||||
!isLoading &&
|
||||
subIssues &&
|
||||
subIssues.length > 0 &&
|
||||
subIssues.map((subIssue: IIssue) => (
|
||||
<SpreadsheetUpdatedOnColumn
|
||||
key={subIssue.id}
|
||||
issue={subIssue}
|
||||
projectId={subIssue.project_detail.id}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
expandedIssues={expandedIssues}
|
||||
properties={properties}
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,34 @@
|
||||
import React from "react";
|
||||
|
||||
// types
|
||||
import { ICurrentUserResponse, IIssue, Properties } from "types";
|
||||
// helper
|
||||
import { renderLongDetailDateFormat } from "helpers/date-time.helper";
|
||||
|
||||
type Props = {
|
||||
issue: IIssue;
|
||||
projectId: string;
|
||||
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
|
||||
properties: Properties;
|
||||
user: ICurrentUserResponse | undefined;
|
||||
isNotAllowed: boolean;
|
||||
};
|
||||
|
||||
export const UpdatedOnColumn: React.FC<Props> = ({
|
||||
issue,
|
||||
projectId,
|
||||
partialUpdateIssue,
|
||||
properties,
|
||||
user,
|
||||
isNotAllowed,
|
||||
}) => (
|
||||
<div className="flex items-center text-sm h-11 w-full bg-custom-background-100">
|
||||
<span className="flex items-center px-4 py-2.5 h-full w-full flex-shrink-0 border-r border-b border-custom-border-100">
|
||||
{properties.updated_on && (
|
||||
<div className="flex items-center text-xs cursor-default text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
|
||||
{renderLongDetailDateFormat(issue.updated_at)}
|
||||
</div>
|
||||
)}
|
||||
</span>
|
||||
</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">
|
||||
|
@ -1,4 +1,6 @@
|
||||
import { FC, useEffect, useState } from "react";
|
||||
// next
|
||||
import { useRouter } from "next/router";
|
||||
// icons
|
||||
import { ArrowsPointingInIcon, ArrowsPointingOutIcon } from "@heroicons/react/20/solid";
|
||||
// components
|
||||
@ -11,6 +13,8 @@ import { GanttSidebar } from "../sidebar";
|
||||
import { MonthChartView } from "./month";
|
||||
// import { QuarterChartView } from "./quarter";
|
||||
// import { YearChartView } from "./year";
|
||||
// icons
|
||||
import { PlusIcon } from "lucide-react";
|
||||
// views
|
||||
import {
|
||||
// generateHourChart,
|
||||
@ -25,6 +29,7 @@ import {
|
||||
getNumberOfDaysBetweenTwoDatesInYear,
|
||||
getMonthChartItemPositionWidthInMonth,
|
||||
} from "../views";
|
||||
import { GanttInlineCreateIssueForm } from "components/core/views/gantt-chart-view/inline-create-issue-form";
|
||||
// types
|
||||
import { ChartDataType, IBlockUpdateData, IGanttBlock, TGanttViews } from "../types";
|
||||
// data
|
||||
@ -64,12 +69,17 @@ export const ChartViewRoot: FC<ChartViewRootProps> = ({
|
||||
const [itemsContainerWidth, setItemsContainerWidth] = useState<number>(0);
|
||||
const [fullScreenMode, setFullScreenMode] = useState<boolean>(false);
|
||||
|
||||
const [isCreateIssueFormOpen, setIsCreateIssueFormOpen] = useState(false);
|
||||
|
||||
// blocks state management starts
|
||||
const [chartBlocks, setChartBlocks] = useState<IGanttBlock[] | null>(null);
|
||||
|
||||
const { currentView, currentViewData, renderView, dispatch, allViews, updateScrollLeft } =
|
||||
useChart();
|
||||
|
||||
const router = useRouter();
|
||||
const { cycleId, moduleId } = router.query;
|
||||
|
||||
const renderBlockStructure = (view: any, blocks: IGanttBlock[] | null) =>
|
||||
blocks && blocks.length > 0
|
||||
? blocks.map((block: any) => ({
|
||||
@ -294,9 +304,12 @@ export const ChartViewRoot: FC<ChartViewRootProps> = ({
|
||||
>
|
||||
<div
|
||||
id="gantt-sidebar"
|
||||
className="h-full w-1/4 flex flex-col border-r border-custom-border-200 space-y-3"
|
||||
className="h-full w-1/4 flex flex-col border-r border-custom-border-200"
|
||||
>
|
||||
<div className="h-[60px] border-b border-custom-border-200 box-border flex-shrink-0" />
|
||||
<div className="h-[60px] border-b border-custom-border-200 box-border flex-shrink-0 flex items-end justify-between gap-2 text-sm text-custom-text-300 font-medium pb-2 pl-10 pr-4">
|
||||
<h6>{title}</h6>
|
||||
<h6>Duration</h6>
|
||||
</div>
|
||||
<GanttSidebar
|
||||
title={title}
|
||||
blockUpdateHandler={blockUpdateHandler}
|
||||
@ -304,6 +317,44 @@ export const ChartViewRoot: FC<ChartViewRootProps> = ({
|
||||
SidebarBlockRender={SidebarBlockRender}
|
||||
enableReorder={enableReorder}
|
||||
/>
|
||||
{chartBlocks && (
|
||||
<div className="pl-2.5 py-3">
|
||||
<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 pl-[1.875rem] 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>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className="relative flex h-full w-full flex-1 flex-col overflow-hidden overflow-x-auto horizontal-scroll-enable"
|
||||
|
@ -179,11 +179,6 @@ export const ChartDraggable: React.FC<Props> = ({
|
||||
|
||||
if (e.button !== 0) return;
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
setIsMoving(true);
|
||||
|
||||
const resizableDiv = resizableRef.current;
|
||||
|
||||
const columnWidth = currentViewData.data.width;
|
||||
@ -193,6 +188,8 @@ export const ChartDraggable: React.FC<Props> = ({
|
||||
let initialMarginLeft = parseInt(resizableDiv.style.marginLeft);
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
setIsMoving(true);
|
||||
|
||||
let delWidth = 0;
|
||||
|
||||
delWidth = checkScrollEnd(e);
|
||||
@ -295,7 +292,9 @@ export const ChartDraggable: React.FC<Props> = ({
|
||||
</>
|
||||
)}
|
||||
<div
|
||||
className="relative z-[2] rounded h-8 w-full flex items-center"
|
||||
className={`relative z-[2] rounded h-8 w-full flex items-center ${
|
||||
isMoving ? "pointer-events-none" : ""
|
||||
}`}
|
||||
onMouseDown={handleBlockMove}
|
||||
>
|
||||
<BlockRender data={block.data} />
|
||||
|
@ -1,5 +1,3 @@
|
||||
import { useState } from "react";
|
||||
// next
|
||||
import { useRouter } from "next/router";
|
||||
// react-beautiful-dnd
|
||||
import { DragDropContext, Draggable, DropResult } from "react-beautiful-dnd";
|
||||
@ -10,9 +8,8 @@ import { useChart } from "./hooks";
|
||||
import { Loader } from "components/ui";
|
||||
// icons
|
||||
import { EllipsisVerticalIcon } from "@heroicons/react/24/outline";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
// components
|
||||
import { GanttInlineCreateIssueForm } from "components/core/views/gantt-chart-view/inline-create-issue-form";
|
||||
// helpers
|
||||
import { findTotalDaysInRange } from "helpers/date-time.helper";
|
||||
// types
|
||||
import { IBlockUpdateData, IGanttBlock } from "./types";
|
||||
|
||||
@ -28,12 +25,10 @@ export const GanttSidebar: React.FC<Props> = (props) => {
|
||||
const { title, blockUpdateHandler, blocks, SidebarBlockRender, enableReorder } = props;
|
||||
|
||||
const router = useRouter();
|
||||
const { cycleId, moduleId } = router.query;
|
||||
const { cycleId } = router.query;
|
||||
|
||||
const { activeBlock, dispatch } = useChart();
|
||||
|
||||
const [isCreateIssueFormOpen, setIsCreateIssueFormOpen] = useState(false);
|
||||
|
||||
// update the active block on hover
|
||||
const updateActiveBlock = (block: IGanttBlock | null) => {
|
||||
dispatch({
|
||||
@ -92,14 +87,21 @@ export const GanttSidebar: React.FC<Props> = (props) => {
|
||||
<StrictModeDroppable droppableId="gantt-sidebar">
|
||||
{(droppableProvided) => (
|
||||
<div
|
||||
className="h-full overflow-y-auto pl-2.5"
|
||||
id={`gantt-sidebar-${cycleId}`}
|
||||
className="max-h-full overflow-y-auto pl-2.5 mt-3"
|
||||
ref={droppableProvided.innerRef}
|
||||
{...droppableProvided.droppableProps}
|
||||
>
|
||||
<>
|
||||
{blocks ? (
|
||||
blocks.length > 0 ? (
|
||||
blocks.map((block, index) => (
|
||||
blocks.map((block, index) => {
|
||||
const duration = findTotalDaysInRange(
|
||||
block.start_date ?? "",
|
||||
block.target_date ?? "",
|
||||
true
|
||||
);
|
||||
|
||||
return (
|
||||
<Draggable
|
||||
key={`sidebar-block-${block.id}`}
|
||||
draggableId={`sidebar-block-${block.id}`}
|
||||
@ -132,19 +134,20 @@ export const GanttSidebar: React.FC<Props> = (props) => {
|
||||
<EllipsisVerticalIcon className="h-4 -ml-5" />
|
||||
</button>
|
||||
)}
|
||||
<div className="flex-grow truncate w-full h-full">
|
||||
<SidebarBlockRender data={block.data} />
|
||||
<div className="flex-grow truncate h-full flex items-center justify-between gap-2">
|
||||
<div className="flex-grow truncate">
|
||||
<SidebarBlockRender data={block.data} />
|
||||
</div>
|
||||
<div className="flex-shrink-0 text-sm text-custom-text-200">
|
||||
{duration} day{duration > 1 ? "s" : ""}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
))
|
||||
) : (
|
||||
<div className="text-custom-text-200 text-sm text-center mt-8">
|
||||
No <span className="lowercase">{title}</span> found
|
||||
</div>
|
||||
)
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<Loader className="pr-2 space-y-3">
|
||||
<Loader.Item height="34px" />
|
||||
@ -155,28 +158,6 @@ export const GanttSidebar: React.FC<Props> = (props) => {
|
||||
)}
|
||||
{droppableProvided.placeholder}
|
||||
</>
|
||||
|
||||
<GanttInlineCreateIssueForm
|
||||
isOpen={isCreateIssueFormOpen}
|
||||
handleClose={() => setIsCreateIssueFormOpen(false)}
|
||||
prePopulatedData={{
|
||||
start_date: new Date(Date.now()).toISOString().split("T")[0],
|
||||
target_date: new Date(Date.now() + 86400000).toISOString().split("T")[0],
|
||||
...(cycleId && { cycle: cycleId.toString() }),
|
||||
...(moduleId && { module: moduleId.toString() }),
|
||||
}}
|
||||
/>
|
||||
|
||||
{!isCreateIssueFormOpen && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsCreateIssueFormOpen(true)}
|
||||
className="flex items-center gap-x-[6px] text-custom-primary-100 px-2 py-1 rounded-md mt-3"
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
<span className="text-sm font-medium text-custom-primary-100">New Issue</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</StrictModeDroppable>
|
||||
|
@ -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]);
|
||||
|
@ -69,6 +69,7 @@ interface IssueFormProps {
|
||||
createMore: boolean;
|
||||
setCreateMore: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
handleClose: () => void;
|
||||
handleDiscard: () => void;
|
||||
status: boolean;
|
||||
user: ICurrentUserResponse | undefined;
|
||||
fieldsToShow: (
|
||||
@ -100,6 +101,7 @@ export const DraftIssueForm: FC<IssueFormProps> = (props) => {
|
||||
status,
|
||||
user,
|
||||
fieldsToShow,
|
||||
handleDiscard,
|
||||
} = props;
|
||||
|
||||
const [stateModal, setStateModal] = useState(false);
|
||||
@ -574,7 +576,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}
|
||||
|
@ -5,7 +5,7 @@ import { Tooltip } from "components/ui";
|
||||
// icons
|
||||
import { StateGroupIcon } from "components/icons";
|
||||
// helpers
|
||||
import { findTotalDaysInRange, renderShortDate } from "helpers/date-time.helper";
|
||||
import { renderShortDate } from "helpers/date-time.helper";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
|
||||
@ -52,8 +52,6 @@ export const IssueGanttBlock = ({ data }: { data: IIssue }) => {
|
||||
export const IssueGanttSidebarBlock = ({ data }: { data: IIssue }) => {
|
||||
const router = useRouter();
|
||||
|
||||
const duration = findTotalDaysInRange(data?.start_date ?? "", data?.target_date ?? "", true);
|
||||
|
||||
const openPeekOverview = () => {
|
||||
const { query } = router;
|
||||
|
||||
@ -72,12 +70,7 @@ export const IssueGanttSidebarBlock = ({ data }: { data: IIssue }) => {
|
||||
<div className="text-xs text-custom-text-300 flex-shrink-0">
|
||||
{data?.project_detail?.identifier} {data?.sequence_id}
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-2 w-full flex-grow truncate">
|
||||
<h6 className="text-sm font-medium flex-grow truncate">{data?.name}</h6>
|
||||
<span className="flex-shrink-0 text-sm text-custom-text-200">
|
||||
{duration} day{duration > 1 ? "s" : ""}
|
||||
</span>
|
||||
</div>
|
||||
<h6 className="text-sm font-medium flex-grow truncate">{data?.name}</h6>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -4,18 +4,21 @@ import { useRouter } from "next/router";
|
||||
|
||||
import useSWR from "swr";
|
||||
|
||||
// hook
|
||||
import useProjects from "hooks/use-projects";
|
||||
import useWorkspaceMembers from "hooks/use-workspace-members";
|
||||
// services
|
||||
import issuesService from "services/issues.service";
|
||||
// components
|
||||
import { DateFilterModal } from "components/core";
|
||||
// ui
|
||||
import { MultiLevelDropdown } from "components/ui";
|
||||
import { Avatar, MultiLevelDropdown } from "components/ui";
|
||||
// icons
|
||||
import { PriorityIcon, StateGroupIcon } from "components/icons";
|
||||
// helpers
|
||||
import { checkIfArraysHaveSameElements } from "helpers/array.helper";
|
||||
// types
|
||||
import { IIssueFilterOptions, IQuery, TStateGroups } from "types";
|
||||
import { IIssueFilterOptions, TStateGroups } from "types";
|
||||
// fetch-keys
|
||||
import { WORKSPACE_LABELS } from "constants/fetch-keys";
|
||||
// constants
|
||||
@ -23,7 +26,7 @@ import { GROUP_CHOICES, PRIORITIES } from "constants/project";
|
||||
import { DATE_FILTER_OPTIONS } from "constants/filters";
|
||||
|
||||
type Props = {
|
||||
filters: Partial<IIssueFilterOptions> | IQuery;
|
||||
filters: Partial<IIssueFilterOptions>;
|
||||
onSelect: (option: any) => void;
|
||||
direction?: "left" | "right";
|
||||
height?: "sm" | "md" | "rg" | "lg";
|
||||
@ -55,6 +58,11 @@ export const MyIssuesSelectFilters: React.FC<Props> = ({
|
||||
: null
|
||||
);
|
||||
|
||||
const { projects: allProjects } = useProjects();
|
||||
const joinedProjects = allProjects?.filter((p) => p.is_member);
|
||||
|
||||
const { workspaceMembers } = useWorkspaceMembers(workspaceSlug?.toString() ?? "");
|
||||
|
||||
return (
|
||||
<>
|
||||
{isDateFilterModalOpen && (
|
||||
@ -74,25 +82,19 @@ export const MyIssuesSelectFilters: React.FC<Props> = ({
|
||||
height={height}
|
||||
options={[
|
||||
{
|
||||
id: "priority",
|
||||
label: "Priority",
|
||||
value: PRIORITIES,
|
||||
id: "project",
|
||||
label: "Project",
|
||||
value: joinedProjects,
|
||||
hasChildren: true,
|
||||
children: [
|
||||
...PRIORITIES.map((priority) => ({
|
||||
id: priority === null ? "null" : priority,
|
||||
label: (
|
||||
<div className="flex items-center gap-2 capitalize">
|
||||
<PriorityIcon priority={priority} /> {priority ?? "None"}
|
||||
</div>
|
||||
),
|
||||
value: {
|
||||
key: "priority",
|
||||
value: priority === null ? "null" : priority,
|
||||
},
|
||||
selected: filters?.priority?.includes(priority === null ? "null" : priority),
|
||||
})),
|
||||
],
|
||||
children: joinedProjects?.map((project) => ({
|
||||
id: project.id,
|
||||
label: <div className="flex items-center gap-2">{project.name}</div>,
|
||||
value: {
|
||||
key: "project",
|
||||
value: project.id,
|
||||
},
|
||||
selected: filters?.project?.includes(project.id),
|
||||
})),
|
||||
},
|
||||
{
|
||||
id: "state_group",
|
||||
@ -142,6 +144,87 @@ export const MyIssuesSelectFilters: React.FC<Props> = ({
|
||||
selected: filters?.labels?.includes(label.id),
|
||||
})),
|
||||
},
|
||||
{
|
||||
id: "priority",
|
||||
label: "Priority",
|
||||
value: PRIORITIES,
|
||||
hasChildren: true,
|
||||
children: [
|
||||
...PRIORITIES.map((priority) => ({
|
||||
id: priority === null ? "null" : priority,
|
||||
label: (
|
||||
<div className="flex items-center gap-2 capitalize">
|
||||
<PriorityIcon priority={priority} /> {priority ?? "None"}
|
||||
</div>
|
||||
),
|
||||
value: {
|
||||
key: "priority",
|
||||
value: priority === null ? "null" : priority,
|
||||
},
|
||||
selected: filters?.priority?.includes(priority === null ? "null" : priority),
|
||||
})),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "created_by",
|
||||
label: "Created by",
|
||||
value: workspaceMembers,
|
||||
hasChildren: true,
|
||||
children: workspaceMembers?.map((member) => ({
|
||||
id: member.member.id,
|
||||
label: (
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar user={member.member} />
|
||||
{member.member.display_name}
|
||||
</div>
|
||||
),
|
||||
value: {
|
||||
key: "created_by",
|
||||
value: member.member.id,
|
||||
},
|
||||
selected: filters?.created_by?.includes(member.member.id),
|
||||
})),
|
||||
},
|
||||
{
|
||||
id: "assignees",
|
||||
label: "Assignees",
|
||||
value: workspaceMembers,
|
||||
hasChildren: true,
|
||||
children: workspaceMembers?.map((member) => ({
|
||||
id: member.member.id,
|
||||
label: (
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar user={member.member} />
|
||||
{member.member.display_name}
|
||||
</div>
|
||||
),
|
||||
value: {
|
||||
key: "assignees",
|
||||
value: member.member.id,
|
||||
},
|
||||
selected: filters?.assignees?.includes(member.member.id),
|
||||
})),
|
||||
},
|
||||
{
|
||||
id: "subscriber",
|
||||
label: "Subscriber",
|
||||
value: workspaceMembers,
|
||||
hasChildren: true,
|
||||
children: workspaceMembers?.map((member) => ({
|
||||
id: member.member.id,
|
||||
label: (
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar user={member.member} />
|
||||
{member.member.display_name}
|
||||
</div>
|
||||
),
|
||||
value: {
|
||||
key: "subscriber",
|
||||
value: member.member.id,
|
||||
},
|
||||
selected: filters?.subscriber?.includes(member.member.id),
|
||||
})),
|
||||
},
|
||||
{
|
||||
id: "start_date",
|
||||
label: "Start date",
|
||||
|
@ -2,25 +2,20 @@ import React from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// headless ui
|
||||
import { Popover, Transition } from "@headlessui/react";
|
||||
// hooks
|
||||
import useMyIssuesFilters from "hooks/my-issues/use-my-issues-filter";
|
||||
import useEstimateOption from "hooks/use-estimate-option";
|
||||
// components
|
||||
import { MyIssuesSelectFilters } from "components/issues";
|
||||
// ui
|
||||
import { CustomMenu, ToggleSwitch, Tooltip } from "components/ui";
|
||||
import { Tooltip } from "components/ui";
|
||||
// icons
|
||||
import { ChevronDownIcon } from "@heroicons/react/24/outline";
|
||||
import { FormatListBulletedOutlined, GridViewOutlined } from "@mui/icons-material";
|
||||
import { FormatListBulletedOutlined } from "@mui/icons-material";
|
||||
import { CreditCard } from "lucide-react";
|
||||
// helpers
|
||||
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
||||
import { checkIfArraysHaveSameElements } from "helpers/array.helper";
|
||||
// types
|
||||
import { Properties, TIssueViewOptions } from "types";
|
||||
// constants
|
||||
import { GROUP_BY_OPTIONS, ORDER_BY_OPTIONS, FILTER_ISSUE_OPTIONS } from "constants/issue";
|
||||
import { TIssueViewOptions } from "types";
|
||||
|
||||
const issueViewOptions: { type: TIssueViewOptions; Icon: any }[] = [
|
||||
{
|
||||
@ -28,19 +23,26 @@ const issueViewOptions: { type: TIssueViewOptions; Icon: any }[] = [
|
||||
Icon: FormatListBulletedOutlined,
|
||||
},
|
||||
{
|
||||
type: "kanban",
|
||||
Icon: GridViewOutlined,
|
||||
type: "spreadsheet",
|
||||
Icon: CreditCard,
|
||||
},
|
||||
];
|
||||
|
||||
export const MyIssuesViewOptions: React.FC = () => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
const { workspaceSlug, workspaceViewId } = router.query;
|
||||
|
||||
const { displayFilters, setDisplayFilters, properties, setProperty, filters, setFilters } =
|
||||
useMyIssuesFilters(workspaceSlug?.toString());
|
||||
const { displayFilters, setDisplayFilters, filters, setFilters } = useMyIssuesFilters(
|
||||
workspaceSlug?.toString()
|
||||
);
|
||||
|
||||
const { isEstimateActive } = useEstimateOption();
|
||||
const workspaceViewPathName = ["workspace-views/all-issues"];
|
||||
|
||||
const isWorkspaceViewPath = workspaceViewPathName.some((pathname) =>
|
||||
router.pathname.includes(pathname)
|
||||
);
|
||||
|
||||
const showFilters = isWorkspaceViewPath || workspaceViewId;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
@ -55,244 +57,59 @@ export const MyIssuesViewOptions: React.FC = () => {
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none hover:bg-custom-sidebar-background-80 duration-300 ${
|
||||
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none hover:bg-custom-sidebar-background-100 duration-300 ${
|
||||
displayFilters?.layout === option.type
|
||||
? "bg-custom-sidebar-background-80"
|
||||
? "bg-custom-sidebar-background-100 shadow-sm"
|
||||
: "text-custom-sidebar-text-200"
|
||||
}`}
|
||||
onClick={() => setDisplayFilters({ layout: option.type })}
|
||||
onClick={() => {
|
||||
setDisplayFilters({ layout: option.type });
|
||||
if (option.type === "spreadsheet")
|
||||
router.push(`/${workspaceSlug}/workspace-views/all-issues`);
|
||||
else router.push(`/${workspaceSlug}/workspace-views`);
|
||||
}}
|
||||
>
|
||||
<option.Icon
|
||||
sx={{
|
||||
fontSize: 16,
|
||||
}}
|
||||
className={option.type === "gantt_chart" ? "rotate-90" : ""}
|
||||
className={option.type === "spreadsheet" ? "h-4 w-4" : ""}
|
||||
/>
|
||||
</button>
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
<MyIssuesSelectFilters
|
||||
filters={filters}
|
||||
onSelect={(option) => {
|
||||
const key = option.key as keyof typeof filters;
|
||||
{showFilters && (
|
||||
<MyIssuesSelectFilters
|
||||
filters={filters}
|
||||
onSelect={(option) => {
|
||||
const key = option.key as keyof typeof filters;
|
||||
|
||||
if (key === "start_date" || key === "target_date") {
|
||||
const valueExists = checkIfArraysHaveSameElements(filters?.[key] ?? [], option.value);
|
||||
if (key === "start_date" || key === "target_date") {
|
||||
const valueExists = checkIfArraysHaveSameElements(filters?.[key] ?? [], option.value);
|
||||
|
||||
setFilters({
|
||||
[key]: valueExists ? null : option.value,
|
||||
});
|
||||
} else {
|
||||
const valueExists = filters[key]?.includes(option.value);
|
||||
|
||||
if (valueExists)
|
||||
setFilters({
|
||||
[option.key]: ((filters[key] ?? []) as any[])?.filter(
|
||||
(val) => val !== option.value
|
||||
),
|
||||
[key]: valueExists ? null : option.value,
|
||||
});
|
||||
else
|
||||
setFilters({
|
||||
[option.key]: [...((filters[key] ?? []) as any[]), option.value],
|
||||
});
|
||||
}
|
||||
}}
|
||||
direction="left"
|
||||
height="rg"
|
||||
/>
|
||||
<Popover className="relative">
|
||||
{({ open }) => (
|
||||
<>
|
||||
<Popover.Button
|
||||
className={`group flex items-center gap-2 rounded-md border border-custom-border-200 bg-transparent px-3 py-1.5 text-xs hover:bg-custom-sidebar-background-90 hover:text-custom-sidebar-text-100 focus:outline-none duration-300 ${
|
||||
open
|
||||
? "bg-custom-sidebar-background-90 text-custom-sidebar-text-100"
|
||||
: "text-custom-sidebar-text-200"
|
||||
}`}
|
||||
>
|
||||
Display
|
||||
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
|
||||
</Popover.Button>
|
||||
} else {
|
||||
const valueExists = filters[key]?.includes(option.value);
|
||||
|
||||
<Transition
|
||||
as={React.Fragment}
|
||||
enter="transition ease-out duration-200"
|
||||
enterFrom="opacity-0 translate-y-1"
|
||||
enterTo="opacity-100 translate-y-0"
|
||||
leave="transition ease-in duration-150"
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 translate-y-1"
|
||||
>
|
||||
<Popover.Panel className="absolute right-0 z-30 mt-1 w-screen max-w-xs transform rounded-lg border border-custom-border-200 bg-custom-background-90 p-3 shadow-lg">
|
||||
<div className="relative divide-y-2 divide-custom-border-200">
|
||||
<div className="space-y-4 pb-3 text-xs">
|
||||
{displayFilters?.layout !== "calendar" &&
|
||||
displayFilters?.layout !== "spreadsheet" && (
|
||||
<>
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-custom-text-200">Group by</h4>
|
||||
<div className="w-28">
|
||||
<CustomMenu
|
||||
label={
|
||||
displayFilters?.group_by === "project"
|
||||
? "Project"
|
||||
: GROUP_BY_OPTIONS.find(
|
||||
(option) => option.key === displayFilters?.group_by
|
||||
)?.name ?? "Select"
|
||||
}
|
||||
className="!w-full"
|
||||
buttonClassName="w-full"
|
||||
>
|
||||
{GROUP_BY_OPTIONS.map((option) => {
|
||||
if (displayFilters?.layout === "kanban" && option.key === null)
|
||||
return null;
|
||||
if (
|
||||
option.key === "state" ||
|
||||
option.key === "created_by" ||
|
||||
option.key === "assignees"
|
||||
)
|
||||
return null;
|
||||
|
||||
return (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
onClick={() => setDisplayFilters({ group_by: option.key })}
|
||||
>
|
||||
{option.name}
|
||||
</CustomMenu.MenuItem>
|
||||
);
|
||||
})}
|
||||
</CustomMenu>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-custom-text-200">Order by</h4>
|
||||
<div className="w-28">
|
||||
<CustomMenu
|
||||
label={
|
||||
ORDER_BY_OPTIONS.find(
|
||||
(option) => option.key === displayFilters?.order_by
|
||||
)?.name ?? "Select"
|
||||
}
|
||||
className="!w-full"
|
||||
buttonClassName="w-full"
|
||||
>
|
||||
{ORDER_BY_OPTIONS.map((option) => {
|
||||
if (
|
||||
displayFilters?.group_by === "priority" &&
|
||||
option.key === "priority"
|
||||
)
|
||||
return null;
|
||||
if (option.key === "sort_order") return null;
|
||||
|
||||
return (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
onClick={() => {
|
||||
setDisplayFilters({ order_by: option.key });
|
||||
}}
|
||||
>
|
||||
{option.name}
|
||||
</CustomMenu.MenuItem>
|
||||
);
|
||||
})}
|
||||
</CustomMenu>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-custom-text-200">Issue type</h4>
|
||||
<div className="w-28">
|
||||
<CustomMenu
|
||||
label={
|
||||
FILTER_ISSUE_OPTIONS.find(
|
||||
(option) => option.key === displayFilters?.type
|
||||
)?.name ?? "Select"
|
||||
}
|
||||
className="!w-full"
|
||||
buttonClassName="w-full"
|
||||
>
|
||||
{FILTER_ISSUE_OPTIONS.map((option) => (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
onClick={() =>
|
||||
setDisplayFilters({
|
||||
type: option.key,
|
||||
})
|
||||
}
|
||||
>
|
||||
{option.name}
|
||||
</CustomMenu.MenuItem>
|
||||
))}
|
||||
</CustomMenu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{displayFilters?.layout !== "calendar" &&
|
||||
displayFilters?.layout !== "spreadsheet" && (
|
||||
<>
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-custom-text-200">Show empty states</h4>
|
||||
<div className="w-28">
|
||||
<ToggleSwitch
|
||||
value={displayFilters?.show_empty_groups ?? true}
|
||||
onChange={() =>
|
||||
setDisplayFilters({
|
||||
show_empty_groups: !displayFilters?.show_empty_groups,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 py-3">
|
||||
<h4 className="text-sm text-custom-text-200">Display Properties</h4>
|
||||
<div className="flex flex-wrap items-center gap-2 text-custom-text-200">
|
||||
{Object.keys(properties).map((key) => {
|
||||
if (key === "estimate" && !isEstimateActive) return null;
|
||||
|
||||
if (
|
||||
displayFilters?.layout === "spreadsheet" &&
|
||||
(key === "attachment_count" ||
|
||||
key === "link" ||
|
||||
key === "sub_issue_count")
|
||||
)
|
||||
return null;
|
||||
|
||||
if (
|
||||
displayFilters?.layout !== "spreadsheet" &&
|
||||
(key === "created_on" || key === "updated_on")
|
||||
)
|
||||
return null;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={key}
|
||||
type="button"
|
||||
className={`rounded border px-2 py-1 text-xs capitalize ${
|
||||
properties[key as keyof Properties]
|
||||
? "border-custom-primary bg-custom-primary text-white"
|
||||
: "border-custom-border-200"
|
||||
}`}
|
||||
onClick={() => setProperty(key as keyof Properties)}
|
||||
>
|
||||
{key === "key" ? "ID" : replaceUnderscoreIfSnakeCase(key)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</>
|
||||
)}
|
||||
</Popover>
|
||||
if (valueExists)
|
||||
setFilters({
|
||||
[option.key]: ((filters[key] ?? []) as any[])?.filter(
|
||||
(val) => val !== option.value
|
||||
),
|
||||
});
|
||||
else
|
||||
setFilters({
|
||||
[option.key]: [...((filters[key] ?? []) as any[]), option.value],
|
||||
});
|
||||
}
|
||||
}}
|
||||
direction="left"
|
||||
height="rg"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -1,6 +1,7 @@
|
||||
import React from "react";
|
||||
// next imports
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
// lucide icons
|
||||
import {
|
||||
ChevronDown,
|
||||
@ -38,6 +39,7 @@ export interface ISubIssues {
|
||||
issueId: string,
|
||||
issue?: IIssue | null
|
||||
) => void;
|
||||
setPeekParentId: (id: string) => void;
|
||||
}
|
||||
|
||||
export const SubIssues: React.FC<ISubIssues> = ({
|
||||
@ -53,38 +55,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={{
|
||||
@ -97,93 +115,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>
|
||||
);
|
||||
};
|
||||
|
@ -28,6 +28,7 @@ export interface ISubIssuesRootList {
|
||||
issueId: string,
|
||||
issue?: IIssue | null
|
||||
) => void;
|
||||
setPeekParentId: (id: string) => void;
|
||||
}
|
||||
|
||||
export const SubIssuesRootList: React.FC<ISubIssuesRootList> = ({
|
||||
@ -42,6 +43,7 @@ export const SubIssuesRootList: React.FC<ISubIssuesRootList> = ({
|
||||
handleIssuesLoader,
|
||||
copyText,
|
||||
handleIssueCrudOperation,
|
||||
setPeekParentId,
|
||||
}) => {
|
||||
const { data: issues, isLoading } = useSWR(
|
||||
workspaceSlug && projectId && parentIssue && parentIssue?.id
|
||||
@ -82,6 +84,7 @@ export const SubIssuesRootList: React.FC<ISubIssuesRootList> = ({
|
||||
handleIssuesLoader={handleIssuesLoader}
|
||||
copyText={copyText}
|
||||
handleIssueCrudOperation={handleIssueCrudOperation}
|
||||
setPeekParentId={setPeekParentId}
|
||||
/>
|
||||
))}
|
||||
|
||||
|
@ -161,6 +161,7 @@ export const IssueProperty: React.FC<IIssueProperty> = ({
|
||||
<div className="flex-shrink-0">
|
||||
<StateSelect
|
||||
value={issue.state_detail}
|
||||
projectId={issue.project_detail.id}
|
||||
onChange={handleStateChange}
|
||||
hideDropdownArrow
|
||||
disabled={!editable}
|
||||
@ -194,6 +195,7 @@ export const IssueProperty: React.FC<IIssueProperty> = ({
|
||||
<div className="flex-shrink-0">
|
||||
<MembersSelect
|
||||
value={issue.assignees}
|
||||
projectId={issue.project_detail.id}
|
||||
onChange={handleAssigneeChange}
|
||||
membersDetails={issue.assignee_details}
|
||||
hideDropdownArrow
|
||||
|
@ -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
|
||||
@ -43,7 +44,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();
|
||||
|
||||
@ -58,6 +63,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: [],
|
||||
@ -234,15 +241,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={
|
||||
<>
|
||||
@ -272,7 +312,7 @@ export const SubIssuesRoot: React.FC<ISubIssuesRoot> = ({ parentIssue, user }) =
|
||||
Add an existing issue
|
||||
</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
</>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
@ -327,6 +367,13 @@ export const SubIssuesRoot: React.FC<ISubIssuesRoot> = ({ parentIssue, user }) =
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<IssuePeekOverview
|
||||
handleMutation={() => peekParentId && peekIssue && mutateSubIssues(peekParentId)}
|
||||
projectId={projectId ?? ""}
|
||||
workspaceSlug={workspaceSlug ?? ""}
|
||||
readOnly={!isEditable}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -0,0 +1,121 @@
|
||||
import React from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// hooks
|
||||
import useMyIssuesFilters from "hooks/my-issues/use-my-issues-filter";
|
||||
import useWorkspaceIssuesFilters from "hooks/use-worskpace-issue-filter";
|
||||
// components
|
||||
import { MyIssuesSelectFilters } from "components/issues";
|
||||
// ui
|
||||
import { Tooltip } from "components/ui";
|
||||
// icons
|
||||
import { FormatListBulletedOutlined } from "@mui/icons-material";
|
||||
import { CreditCard } from "lucide-react";
|
||||
// helpers
|
||||
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
||||
import { checkIfArraysHaveSameElements } from "helpers/array.helper";
|
||||
// types
|
||||
import { TIssueViewOptions } from "types";
|
||||
|
||||
const issueViewOptions: { type: TIssueViewOptions; Icon: any }[] = [
|
||||
{
|
||||
type: "list",
|
||||
Icon: FormatListBulletedOutlined,
|
||||
},
|
||||
{
|
||||
type: "spreadsheet",
|
||||
Icon: CreditCard,
|
||||
},
|
||||
];
|
||||
|
||||
export const WorkspaceIssuesViewOptions: React.FC = () => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, workspaceViewId } = router.query;
|
||||
|
||||
const { displayFilters, setDisplayFilters } = useMyIssuesFilters(workspaceSlug?.toString());
|
||||
|
||||
const { filters, setFilters } = useWorkspaceIssuesFilters(
|
||||
workspaceSlug?.toString(),
|
||||
workspaceViewId?.toString()
|
||||
);
|
||||
|
||||
const isWorkspaceViewPath = router.pathname.includes("workspace-views/all-issues");
|
||||
|
||||
const showFilters = isWorkspaceViewPath || workspaceViewId;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-x- px-1 py-0.5 rounded bg-custom-sidebar-background-90 ">
|
||||
{issueViewOptions.map((option) => (
|
||||
<Tooltip
|
||||
key={option.type}
|
||||
tooltipContent={
|
||||
<span className="capitalize">{replaceUnderscoreIfSnakeCase(option.type)} View</span>
|
||||
}
|
||||
position="bottom"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none hover:bg-custom-sidebar-background-100 duration-300 ${
|
||||
displayFilters?.layout === option.type
|
||||
? "bg-custom-sidebar-background-100 shadow-sm"
|
||||
: "text-custom-sidebar-text-200"
|
||||
}`}
|
||||
onClick={() => {
|
||||
setDisplayFilters({ layout: option.type });
|
||||
if (option.type === "spreadsheet")
|
||||
router.push(`/${workspaceSlug}/workspace-views/all-issues`);
|
||||
else router.push(`/${workspaceSlug}/workspace-views`);
|
||||
}}
|
||||
>
|
||||
<option.Icon
|
||||
sx={{
|
||||
fontSize: 16,
|
||||
}}
|
||||
className={option.type === "spreadsheet" ? "h-4 w-4" : ""}
|
||||
/>
|
||||
</button>
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{showFilters && (
|
||||
<>
|
||||
<MyIssuesSelectFilters
|
||||
filters={filters}
|
||||
onSelect={(option) => {
|
||||
const key = option.key as keyof typeof filters;
|
||||
|
||||
if (key === "start_date" || key === "target_date") {
|
||||
const valueExists = checkIfArraysHaveSameElements(
|
||||
filters?.[key] ?? [],
|
||||
option.value
|
||||
);
|
||||
|
||||
setFilters({
|
||||
[key]: valueExists ? null : option.value,
|
||||
});
|
||||
} else {
|
||||
const valueExists = filters[key]?.includes(option.value);
|
||||
|
||||
if (valueExists)
|
||||
setFilters({
|
||||
[option.key]: ((filters[key] ?? []) as any[])?.filter(
|
||||
(val) => val !== option.value
|
||||
),
|
||||
});
|
||||
else
|
||||
setFilters({
|
||||
[option.key]: [...((filters[key] ?? []) as any[]), option.value],
|
||||
});
|
||||
}
|
||||
}}
|
||||
direction="left"
|
||||
height="rg"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</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>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
@ -1,15 +1,27 @@
|
||||
import React, { useEffect } from "react";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
|
||||
// headless ui
|
||||
import { Listbox, Transition } from "@headlessui/react";
|
||||
// react-hook-form
|
||||
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
||||
import {
|
||||
Control,
|
||||
Controller,
|
||||
FieldArrayWithId,
|
||||
UseFieldArrayRemove,
|
||||
useFieldArray,
|
||||
useForm,
|
||||
} from "react-hook-form";
|
||||
// services
|
||||
import workspaceService from "services/workspace.service";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// ui
|
||||
import { CustomSelect, Input, PrimaryButton, SecondaryButton } from "components/ui";
|
||||
import { Input, PrimaryButton, SecondaryButton } from "components/ui";
|
||||
// hooks
|
||||
import useDynamicDropdownPosition from "hooks/use-dynamic-dropdown";
|
||||
// icons
|
||||
import { PlusIcon, XMarkIcon } from "@heroicons/react/24/outline";
|
||||
import { ChevronDownIcon } from "@heroicons/react/20/solid";
|
||||
import { PlusIcon, XMarkIcon, CheckIcon } from "@heroicons/react/24/outline";
|
||||
// types
|
||||
import { ICurrentUserResponse, IWorkspace, TOnboardingSteps } from "types";
|
||||
// constants
|
||||
@ -31,12 +43,136 @@ type FormValues = {
|
||||
emails: EmailRole[];
|
||||
};
|
||||
|
||||
export const InviteMembers: React.FC<Props> = ({
|
||||
finishOnboarding,
|
||||
stepChange,
|
||||
user,
|
||||
workspace,
|
||||
}) => {
|
||||
type InviteMemberFormProps = {
|
||||
index: number;
|
||||
remove: UseFieldArrayRemove;
|
||||
control: Control<FormValues, any>;
|
||||
field: FieldArrayWithId<FormValues, "emails", "id">;
|
||||
fields: FieldArrayWithId<FormValues, "emails", "id">[];
|
||||
errors: any;
|
||||
};
|
||||
|
||||
const InviteMemberForm: React.FC<InviteMemberFormProps> = (props) => {
|
||||
const { control, index, fields, remove, errors } = props;
|
||||
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||
|
||||
useDynamicDropdownPosition(
|
||||
isDropdownOpen,
|
||||
() => setIsDropdownOpen(false),
|
||||
buttonRef,
|
||||
dropdownRef
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="group relative grid grid-cols-11 gap-4">
|
||||
<div className="col-span-7">
|
||||
<Controller
|
||||
control={control}
|
||||
name={`emails.${index}.email`}
|
||||
rules={{
|
||||
required: "Email ID is required",
|
||||
pattern: {
|
||||
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
|
||||
message: "Invalid Email ID",
|
||||
},
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<>
|
||||
<Input {...field} className="text-xs sm:text-sm" placeholder="Enter their email..." />
|
||||
{errors.emails?.[index]?.email && (
|
||||
<span className="text-red-500 text-xs">
|
||||
{errors.emails?.[index]?.email?.message}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-3">
|
||||
<Controller
|
||||
control={control}
|
||||
name={`emails.${index}.role`}
|
||||
rules={{ required: true }}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<Listbox
|
||||
as="div"
|
||||
value={value}
|
||||
onChange={(val) => {
|
||||
onChange(val);
|
||||
setIsDropdownOpen(false);
|
||||
}}
|
||||
className="flex-shrink-0 text-left w-full"
|
||||
>
|
||||
<Listbox.Button
|
||||
type="button"
|
||||
ref={buttonRef}
|
||||
onClick={() => setIsDropdownOpen((prev) => !prev)}
|
||||
className="flex items-center px-2.5 py-2 text-xs justify-between gap-1 w-full rounded-md border border-custom-border-300 shadow-sm duration-300 focus:outline-none"
|
||||
>
|
||||
<span className="text-xs sm:text-sm">{ROLE[value]}</span>
|
||||
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
|
||||
</Listbox.Button>
|
||||
|
||||
<Transition
|
||||
show={isDropdownOpen}
|
||||
as={React.Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Listbox.Options
|
||||
ref={dropdownRef}
|
||||
className="fixed w-36 z-10 border border-custom-border-300 mt-1 overflow-y-auto rounded-md bg-custom-background-90 text-xs shadow-lg focus:outline-none max-h-48"
|
||||
>
|
||||
<div className="space-y-1 p-2">
|
||||
{Object.entries(ROLE).map(([key, value]) => (
|
||||
<Listbox.Option
|
||||
key={key}
|
||||
value={parseInt(key)}
|
||||
className={({ active, selected }) =>
|
||||
`cursor-pointer select-none truncate rounded px-1 py-1.5 ${
|
||||
active || selected ? "bg-custom-background-80" : ""
|
||||
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
|
||||
}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2">{value}</div>
|
||||
{selected && <CheckIcon className="h-4 w-4 flex-shrink-0" />}
|
||||
</div>
|
||||
)}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</div>
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
</Listbox>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{fields.length > 1 && (
|
||||
<button
|
||||
type="button"
|
||||
className="hidden group-hover:grid self-center place-items-center rounded -ml-3"
|
||||
onClick={() => remove(index)}
|
||||
>
|
||||
<XMarkIcon className="h-3.5 w-3.5 text-custom-text-200" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const InviteMembers: React.FC<Props> = (props) => {
|
||||
const { finishOnboarding, stepChange, user, workspace } = props;
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const {
|
||||
@ -109,66 +245,15 @@ export const InviteMembers: React.FC<Props> = ({
|
||||
</div>
|
||||
<div className="space-y-3 sm:space-y-4 mb-3 h-full overflow-y-auto">
|
||||
{fields.map((field, index) => (
|
||||
<div key={field.id} className="group relative grid grid-cols-11 gap-4">
|
||||
<div className="col-span-7">
|
||||
<Controller
|
||||
control={control}
|
||||
name={`emails.${index}.email`}
|
||||
rules={{
|
||||
required: "Email ID is required",
|
||||
pattern: {
|
||||
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
|
||||
message: "Invalid Email ID",
|
||||
},
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<>
|
||||
<Input
|
||||
{...field}
|
||||
className="text-xs sm:text-sm"
|
||||
placeholder="Enter their email..."
|
||||
/>
|
||||
{errors.emails?.[index]?.email && (
|
||||
<span className="text-red-500 text-xs">
|
||||
{errors.emails?.[index]?.email?.message}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-3">
|
||||
<Controller
|
||||
control={control}
|
||||
name={`emails.${index}.role`}
|
||||
rules={{ required: true }}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<CustomSelect
|
||||
value={value}
|
||||
label={<span className="text-xs sm:text-sm">{ROLE[value]}</span>}
|
||||
onChange={onChange}
|
||||
width="w-full"
|
||||
input
|
||||
>
|
||||
{Object.entries(ROLE).map(([key, value]) => (
|
||||
<CustomSelect.Option key={key} value={parseInt(key)}>
|
||||
{value}
|
||||
</CustomSelect.Option>
|
||||
))}
|
||||
</CustomSelect>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{fields.length > 1 && (
|
||||
<button
|
||||
type="button"
|
||||
className="hidden group-hover:grid self-center place-items-center rounded -ml-3"
|
||||
onClick={() => remove(index)}
|
||||
>
|
||||
<XMarkIcon className="h-3.5 w-3.5 text-custom-text-200" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<InviteMemberForm
|
||||
control={control}
|
||||
errors={errors}
|
||||
field={field}
|
||||
fields={fields}
|
||||
index={index}
|
||||
remove={remove}
|
||||
key={field.id}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
|
@ -121,6 +121,10 @@ export const UserDetails: React.FC<Props> = ({ user }) => {
|
||||
register={register}
|
||||
validations={{
|
||||
required: "First name is required",
|
||||
maxLength: {
|
||||
value: 24,
|
||||
message: "First name cannot exceed the limit of 24 characters",
|
||||
},
|
||||
}}
|
||||
error={errors.first_name}
|
||||
/>
|
||||
@ -135,6 +139,10 @@ export const UserDetails: React.FC<Props> = ({ user }) => {
|
||||
placeholder="Enter your last name..."
|
||||
validations={{
|
||||
required: "Last name is required",
|
||||
maxLength: {
|
||||
value: 24,
|
||||
message: "Last name cannot exceed the limit of 24 characters",
|
||||
},
|
||||
}}
|
||||
error={errors.last_name}
|
||||
/>
|
||||
|
@ -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"
|
||||
>
|
||||
@ -239,9 +239,11 @@ export const ProfileIssuesViewOptions: React.FC = () => {
|
||||
<div className="w-28">
|
||||
<CustomMenu
|
||||
label={
|
||||
FILTER_ISSUE_OPTIONS.find(
|
||||
(option) => option.key === displayFilters?.type
|
||||
)?.name ?? "Select"
|
||||
<span className="truncate">
|
||||
{FILTER_ISSUE_OPTIONS.find(
|
||||
(option) => option.key === displayFilters?.type
|
||||
)?.name ?? "Select"}
|
||||
</span>
|
||||
}
|
||||
className="!w-full"
|
||||
buttonClassName="w-full"
|
||||
@ -266,7 +268,7 @@ export const ProfileIssuesViewOptions: React.FC = () => {
|
||||
displayFilters?.layout !== "spreadsheet" && (
|
||||
<>
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-custom-text-200">Show empty states</h4>
|
||||
<h4 className="text-custom-text-200">Show empty groups</h4>
|
||||
<div className="w-28">
|
||||
<ToggleSwitch
|
||||
value={displayFilters?.show_empty_groups ?? true}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user