diff --git a/apiserver/plane/app/serializers/__init__.py b/apiserver/plane/app/serializers/__init__.py index c35737cb545..0d72f919241 100644 --- a/apiserver/plane/app/serializers/__init__.py +++ b/apiserver/plane/app/serializers/__init__.py @@ -111,6 +111,7 @@ InboxSerializer, InboxIssueSerializer, IssueStateInboxSerializer, + InboxIssueLiteSerializer, ) from .analytic import AnalyticViewSerializer diff --git a/apiserver/plane/app/serializers/base.py b/apiserver/plane/app/serializers/base.py index 89683ffe520..446fdb6d537 100644 --- a/apiserver/plane/app/serializers/base.py +++ b/apiserver/plane/app/serializers/base.py @@ -60,6 +60,7 @@ def _filter_fields(self, fields): CycleIssueSerializer, IssueFlatSerializer, IssueRelationSerializer, + InboxIssueLiteSerializer ) # Expansion mapper @@ -80,9 +81,10 @@ def _filter_fields(self, fields): "issue_cycle": CycleIssueSerializer, "parent": IssueSerializer, "issue_relation": IssueRelationSerializer, + "issue_inbox" : InboxIssueLiteSerializer, } - self.fields[field] = expansion[field](many=True if field in ["members", "assignees", "labels", "issue_cycle", "issue_relation"] else False) + self.fields[field] = expansion[field](many=True if field in ["members", "assignees", "labels", "issue_cycle", "issue_relation", "issue_inbox"] else False) return self.fields @@ -103,6 +105,7 @@ def to_representation(self, instance): LabelSerializer, CycleIssueSerializer, IssueRelationSerializer, + InboxIssueLiteSerializer ) # Expansion mapper @@ -122,7 +125,8 @@ def to_representation(self, instance): "labels": LabelSerializer, "issue_cycle": CycleIssueSerializer, "parent": IssueSerializer, - "issue_relation": IssueRelationSerializer + "issue_relation": IssueRelationSerializer, + "issue_inbox" : InboxIssueLiteSerializer, } # Check if field in expansion then expand the field if expand in expansion: diff --git a/apiserver/plane/app/views/inbox.py b/apiserver/plane/app/views/inbox.py index 0f8e68656ae..3bacdae4cf8 100644 --- a/apiserver/plane/app/views/inbox.py +++ b/apiserver/plane/app/views/inbox.py @@ -88,39 +88,24 @@ class InboxIssueViewSet(BaseViewSet): ] def get_queryset(self): - return self.filter_queryset( - super() - .get_queryset() - .filter( - Q(snoozed_till__gte=timezone.now()) - | Q(snoozed_till__isnull=True), - workspace__slug=self.kwargs.get("slug"), - project_id=self.kwargs.get("project_id"), - inbox_id=self.kwargs.get("inbox_id"), - ) - .select_related("issue", "workspace", "project") - ) - - def list(self, request, slug, project_id, inbox_id): - filters = issue_filters(request.query_params, "GET") - issues = ( + return ( Issue.objects.filter( - issue_inbox__inbox_id=inbox_id, - workspace__slug=slug, - project_id=project_id, + project_id=self.kwargs.get("project_id"), + workspace__slug=self.kwargs.get("slug"), + issue_inbox__inbox_id=self.kwargs.get("inbox_id") ) - .filter(**filters) .select_related("workspace", "project", "state", "parent") - .prefetch_related("assignees", "labels") - .order_by("issue_inbox__snoozed_till", "issue_inbox__status") - .annotate( - sub_issues_count=Issue.issue_objects.filter( - parent=OuterRef("id") + .prefetch_related("labels", "assignees") + .prefetch_related( + Prefetch( + "issue_inbox", + queryset=InboxIssue.objects.only( + "status", "duplicate_to", "snoozed_till", "source" + ), ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") ) + .annotate(cycle_id=F("issue_cycle__cycle_id")) + .annotate(module_id=F("issue_module__module_id")) .annotate( link_count=IssueLink.objects.filter(issue=OuterRef("id")) .order_by() @@ -135,16 +120,20 @@ def list(self, request, slug, project_id, inbox_id): .annotate(count=Func(F("id"), function="Count")) .values("count") ) - .prefetch_related( - Prefetch( - "issue_inbox", - queryset=InboxIssue.objects.only( - "status", "duplicate_to", "snoozed_till", "source" - ), + .annotate( + sub_issues_count=Issue.issue_objects.filter( + parent=OuterRef("id") ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") ) - ) - issues_data = IssueStateInboxSerializer(issues, many=True).data + ).distinct() + + def list(self, request, slug, project_id, inbox_id): + filters = issue_filters(request.query_params, "GET") + issue_queryset = self.get_queryset().filter(**filters).order_by("issue_inbox__snoozed_till", "issue_inbox__status") + issues_data = IssueSerializer(issue_queryset, expand=self.expand, many=True).data return Response( issues_data, status=status.HTTP_200_OK, @@ -211,7 +200,8 @@ def create(self, request, slug, project_id, inbox_id): source=request.data.get("source", "in-app"), ) - serializer = IssueStateInboxSerializer(issue) + issue = (self.get_queryset().filter(pk=issue.id).first()) + serializer = IssueSerializer(issue ,expand=self.expand) return Response(serializer.data, status=status.HTTP_200_OK) def partial_update(self, request, slug, project_id, inbox_id, issue_id): @@ -331,22 +321,20 @@ def partial_update(self, request, slug, project_id, inbox_id, issue_id): if state is not None: issue.state = state issue.save() - + issue = (self.get_queryset().filter(pk=issue_id).first()) + serializer = IssueSerializer(issue, expand=self.expand) return Response(serializer.data, status=status.HTTP_200_OK) return Response( serializer.errors, status=status.HTTP_400_BAD_REQUEST ) else: - return Response( - InboxIssueSerializer(inbox_issue).data, - status=status.HTTP_200_OK, - ) + issue = (self.get_queryset().filter(pk=issue_id).first()) + serializer = IssueSerializer(issue ,expand=self.expand) + return Response(serializer.data, status=status.HTTP_200_OK) def retrieve(self, request, slug, project_id, inbox_id, issue_id): - issue = Issue.objects.get( - pk=issue_id, workspace__slug=slug, project_id=project_id - ) - serializer = IssueStateInboxSerializer(issue) + issue = self.get_queryset().filter(pk=issue_id).first() + serializer = IssueSerializer(issue, expand=self.expand,) return Response(serializer.data, status=status.HTTP_200_OK) def destroy(self, request, slug, project_id, inbox_id, issue_id): diff --git a/apiserver/plane/app/views/page.py b/apiserver/plane/app/views/page.py index d7bff43d6dc..1d8ff1fbb15 100644 --- a/apiserver/plane/app/views/page.py +++ b/apiserver/plane/app/views/page.py @@ -158,18 +158,18 @@ def archive(self, request, slug, project_id, page_id): pk=page_id, workspace__slug=slug, project_id=project_id ) - # only the owner and admin can archive the page + # only the owner or admin can archive the page if ( ProjectMember.objects.filter( project_id=project_id, member=request.user, is_active=True, - role__gte=20, + role__lte=15, ).exists() - or request.user.id != page.owned_by_id + and request.user.id != page.owned_by_id ): return Response( - {"error": "Only the owner and admin can archive the page"}, + {"error": "Only the owner or admin can archive the page"}, status=status.HTTP_400_BAD_REQUEST, ) @@ -182,18 +182,18 @@ def unarchive(self, request, slug, project_id, page_id): pk=page_id, workspace__slug=slug, project_id=project_id ) - # only the owner and admin can un archive the page + # only the owner or admin can un archive the page if ( ProjectMember.objects.filter( project_id=project_id, member=request.user, is_active=True, - role__gt=20, + role__lte=15, ).exists() - or request.user.id != page.owned_by_id + and request.user.id != page.owned_by_id ): return Response( - {"error": "Only the owner and admin can un archive the page"}, + {"error": "Only the owner or admin can un archive the page"}, status=status.HTTP_400_BAD_REQUEST, ) diff --git a/apiserver/plane/app/views/project.py b/apiserver/plane/app/views/project.py index 2895661f841..5d2f9567305 100644 --- a/apiserver/plane/app/views/project.py +++ b/apiserver/plane/app/views/project.py @@ -68,7 +68,7 @@ class ProjectViewSet(WebhookMixin, BaseViewSet): - serializer_class = ProjectSerializer + serializer_class = ProjectListSerializer model = Project webhook_event = "project" @@ -76,11 +76,6 @@ class ProjectViewSet(WebhookMixin, BaseViewSet): ProjectBasePermission, ] - def get_serializer_class(self, *args, **kwargs): - if self.action in ["update", "partial_update"]: - return ProjectSerializer - return ProjectDetailSerializer - def get_queryset(self): return self.filter_queryset( super() @@ -690,6 +685,19 @@ def create(self, request, slug, project_id): .order_by("sort_order") ) + bulk_project_members = [] + member_roles = {member.get("member_id"): member.get("role") for member in members} + # Update roles in the members array based on the member_roles dictionary + for project_member in ProjectMember.objects.filter(project_id=project_id, member_id__in=[member.get("member_id") for member in members]): + project_member.role = member_roles[str(project_member.member_id)] + project_member.is_active = True + bulk_project_members.append(project_member) + + # Update the roles of the existing members + ProjectMember.objects.bulk_update( + bulk_project_members, ["is_active", "role"], batch_size=100 + ) + for member in members: sort_order = [ project_member.get("sort_order") @@ -716,25 +724,6 @@ def create(self, request, slug, project_id): ) ) - # Check if the user is already a member of the project and is inactive - if ProjectMember.objects.filter( - workspace__slug=slug, - project_id=project_id, - member_id=member.get("member_id"), - is_active=False, - ).exists(): - member_detail = ProjectMember.objects.get( - workspace__slug=slug, - project_id=project_id, - member_id=member.get("member_id"), - is_active=False, - ) - # Check if the user has not deactivated the account - user = User.objects.filter(pk=member.get("member_id")).first() - if user.is_active: - member_detail.is_active = True - member_detail.save(update_fields=["is_active"]) - project_members = ProjectMember.objects.bulk_create( bulk_project_members, batch_size=10, @@ -745,8 +734,8 @@ def create(self, request, slug, project_id): bulk_issue_props, batch_size=10, ignore_conflicts=True ) - serializer = ProjectMemberSerializer(project_members, many=True) - + project_members = ProjectMember.objects.filter(project_id=project_id, member_id__in=[member.get("member_id") for member in members]) + serializer = ProjectMemberRoleSerializer(project_members, many=True) return Response(serializer.data, status=status.HTTP_201_CREATED) def list(self, request, slug, project_id): diff --git a/apiserver/plane/bgtasks/email_notification_task.py b/apiserver/plane/bgtasks/email_notification_task.py index 86b6d938e48..cf72555851e 100644 --- a/apiserver/plane/bgtasks/email_notification_task.py +++ b/apiserver/plane/bgtasks/email_notification_task.py @@ -164,30 +164,25 @@ def send_email_notification( } ) activity_time = changes.pop("activity_time") - template_data.append( - { - "actor_detail": { - "avatar_url": actor.avatar, - "first_name": actor.first_name, - "last_name": actor.last_name, - }, - "changes": changes, - "issue_details": { - "name": issue.name, - "identifier": f"{issue.project.identifier}-{issue.sequence_id}", - }, - "activity_time": str(activity_time), - } - ) + # Parse the input string into a datetime object + formatted_time = datetime.strptime(activity_time, "%Y-%m-%d %H:%M:%S").strftime("%H:%M %p") - span = f"""""" + if changes: + template_data.append( + { + "actor_detail": { + "avatar_url": actor.avatar, + "first_name": actor.first_name, + "last_name": actor.last_name, + }, + "changes": changes, + "issue_details": { + "name": issue.name, + "identifier": f"{issue.project.identifier}-{issue.sequence_id}", + }, + "activity_time": str(formatted_time), + } + ) summary = "updates were made to the issue by" @@ -204,11 +199,10 @@ def send_email_notification( "receiver": { "email": receiver.email, }, - "issue_unsubscribe": f"{base_api}/{str(issue.project.workspace.slug)}/projects/{str(issue.project.id)}/issues/{str(issue.id)}", + "issue_url": f"{base_api}/{str(issue.project.workspace.slug)}/projects/{str(issue.project.id)}/issues/{str(issue.id)}", "user_preference": f"{base_api}/profile/preferences/email", "comments": comments, } - print(json.dumps(context)) html_content = render_to_string( "emails/notifications/issue-updates.html", context ) @@ -236,7 +230,6 @@ def send_email_notification( EmailNotificationLog.objects.filter( pk__in=email_notification_ids ).update(sent_at=timezone.now()) - print("Email Sent") return except Exception as e: print(e) diff --git a/apiserver/plane/bgtasks/export_task.py b/apiserver/plane/bgtasks/export_task.py index f9e6c1ac835..b99e4b1d944 100644 --- a/apiserver/plane/bgtasks/export_task.py +++ b/apiserver/plane/bgtasks/export_task.py @@ -97,7 +97,7 @@ def upload_to_s3(zip_file, workspace_id, token_id, slug): ) # Create the new url with updated domain and protocol presigned_url = presigned_url.replace( - "http://plane-minio:9000/uploads/", + f"{settings.AWS_S3_ENDPOINT_URL}/{settings.AWS_STORAGE_BUCKET_NAME}/", f"{settings.AWS_S3_URL_PROTOCOL}//{settings.AWS_S3_CUSTOM_DOMAIN}/", ) else: diff --git a/apiserver/plane/bgtasks/notification_task.py b/apiserver/plane/bgtasks/notification_task.py index 3cb54a12237..9bb73ab2d2b 100644 --- a/apiserver/plane/bgtasks/notification_task.py +++ b/apiserver/plane/bgtasks/notification_task.py @@ -335,7 +335,11 @@ def notifications( user_id=subscriber ) - for issue_activity in issue_activities_created: + for issue_activity in issue_activities_created: + # If activity done in blocking then blocked by email should not go + if issue_activity.get("issue_detail").get("id") != issue_id: + continue; + # Do not send notification for description update if issue_activity.get("field") == "description": continue diff --git a/apiserver/plane/celery.py b/apiserver/plane/celery.py index 11a88de57a4..0912e276af4 100644 --- a/apiserver/plane/celery.py +++ b/apiserver/plane/celery.py @@ -31,7 +31,7 @@ }, "check-every-five-minutes-to-send-email-notifications": { "task": "plane.bgtasks.email_notification_task.stack_email_notification", - "schedule": crontab(minute='*/1') + "schedule": crontab(minute='*/5') }, } diff --git a/apiserver/runtime.txt b/apiserver/runtime.txt index dfe813b8606..d45f665dee8 100644 --- a/apiserver/runtime.txt +++ b/apiserver/runtime.txt @@ -1 +1 @@ -python-3.11.6 \ No newline at end of file +python-3.11.7 \ No newline at end of file diff --git a/apiserver/templates/emails/notifications/issue-updates.html b/apiserver/templates/emails/notifications/issue-updates.html index 2bf5b69eca5..4374846df7d 100644 --- a/apiserver/templates/emails/notifications/issue-updates.html +++ b/apiserver/templates/emails/notifications/issue-updates.html @@ -6,10 +6,8 @@ > - - - Updates on Issue + Updates on issue - - - - - - -
- +
+
+ + + +
+
+ +
+
+ + +
+
+
-
- - - - Plane - -
+

+ {{ issue.issue_identifier }} updates +

+

+ {{ issue.name }}: {{ issue.issue_identifier }} +

-
- - - - - -
-
- - - + + + +
-

- {{ issue.identifier }} updates -

-

+ +

+ {% if data.1 %}{{ data|length }}{% endif %} {{ summary }} + + {{ data.0.actor_detail.first_name}} + {{data.0.actor_detail.last_name }} + +

+ {% if comments.0 %} +

+ {{ comments|length }} {% if comments|length == 1 %}comment was{% else %}comments were{% endif %} left by + + {% if comments|length == 1 %} + {{ data.0.actor_detail.first_name }} + {{ data.0.actor_detail.last_name }} + {% else %} + {{ data.0.actor_detail.first_name }} + {{ data.0.actor_detail.last_name }} and others + {% endif %} + +

+ {% endif %} + {% if mentions and comments.0 and data.0 %} +

+ There are 3 new updates, added 1 new comment and, you were + + @{{ data.0.actor_detail.first_name}} + {{data.0.actor_detail.last_name }} + + mentioned a comment of this issue. +

+ {% endif %} + {% for update in data %} {% if update.changes.name %} + +

+ The issue title has been updated from “{{update.changes.user.old_value.0}}“ to "{{update.changes.user.new_value|last}}" +

+ {% endif %} + + {% if data %} +
+ +
+

+ Updates +

+
+ +
+ + + + + +
+ {% if update.actor_detail.avatar_url %} + + {% else %} + + + - -
- {{ issue.name }} -

-
-
-

- {{ summary }} + + {{ update.actor_detail.first_name.0 }} + +

+ {% endif %} +
+

+ {{ update.actor_detail.first_name }} {{ update.actor_detail.last_name }} +

+
+

+ {{ update.activity_time }} +

+
+ {% if update.changes.target_date %} + + + + + + + +
+ + +
+

+ Due Date: +

+
+
+

+ {{ update.changes.target_date.new_value.0 }} +

+
+ {% endif %} {% if update.changes.duplicate %} + + + + + + - + + +
+ + + Duplicate: + + + {% for duplicate in update.changes.duplicate.new_value %} - {{ data.0.actor_detail.first_name}} {{data.0.actor_detail.last_name }} + {{ duplicate }} -

- - + + +
+ {% endif %} + + {% if update.changes.assignees %} + + + + + {% if update.changes.assignees.new_value.0 %} + + {% endif %} {% if update.changes.assignees.new_value.1 %} + + {% endif %} {% if update.changes.assignees.old_value.0 %} + + {% endif %} {% if update.changes.assignees.old_value.1 %} + + {% endif %} + +
+ + +

+ Assignees: +

+
+

+ {{ update.changes.assignees.new_value.0 }} +

+
+

+ +{{ update.changes.assignees.new_value|length|add:"-1"}} + more +

+
+

+ {{update.changes.assignees.old_value.0}} +

+
+

+ +{{ update.changes.assignees.old_value|length|add:"-1"}} + more +

+
+ {% endif %} {% if update.changes.labels %} + + + + + + {% if update.changes.labels.new_value.0 %} + + {% endif %} + {% if update.changes.labels.new_value.1 %} + + {% endif %} + {% if update.changes.labels.old_value.0 %} + + {% endif %} + {% if update.changes.labels.old_value.1 %} + + {% endif %} + +
+ + +

+ Labels: +

+
+

+ {{update.changes.labels.new_value.0}} +

+
+

+ +{{ update.changes.labels.new_value|length|add:"-1"}} more +

+
+

+ {{update.changes.labels.old_value.0}} +

+
+

+ +{{ update.changes.labels.old_value|length|add:"-1"}} more +

+
+ {% endif %} + + {% if update.changes.state %} + + + + + + + + + + +
+ + +

+ State: +

+
+ + +

+ {{ update.changes.state.old_value.0 }} +

+
+ + + + +

+ {{update.changes.state.new_value|last }} +

+
+ {% endif %} {% if update.changes.link %} + + + + + + + +
+ + +

+ Links: +

+
+ {% for link in update.changes.link.new_value %} + + {{ link }} + + {% endfor %} + {% if update.changes.link.old_value|length > 0 %} + {% if update.changes.link.old_value.0 != "None" %} +

- + 2 Links were removed +

+ {% endif %} + {% endif %} +
+ {% endif %} + {% if update.changes.priority %} + + + + + + + + + + +
+ + +

+ Priority: +

+
+

+ {{ update.changes.priority.old_value.0 }} +

+
+ + +

+ {{ update.changes.priority.new_value|last }} +

+
+ {% endif %} + {% if update.changes.blocking.new_value %} + + + + + + + + +
+ + + Blocking: + + + {% for blocking in update.changes.blocking.new_value %} + + {{blocking}} + + {% endfor %} +
+ {% endif %} + + + {% endif %} + + {% endfor %} {% if comments.0 %} + +
+ +

+ Comments +

+ + {% for comment in comments %} + + + + - -
+ {% if comment.actor_detail.avatar_url %} + + {% else %} + + + + +
+ + {{ comment.actor_detail.first_name.0 }} + +
+ {% endif %} +
+ - - - {% for update in data %} - - - - {% endfor %} -
+

- Updates + {{ comment.actor_detail.first_name }} {{ comment.actor_detail.last_name }}

- - - - - - - {% if update.changes.assignees %} - - - - {% endif %} - - {% if update.changes.target_date %} - - - - {% endif %} --> - - {% if update.changes.duplicate %} - - - - {% endif %} - - {% if update.changes.labels %} - - - - {% endif %} - - {% if update.changes.state %} - - - - - - - {% endif %} - - {% if update.changes.link %} - - - - {% endif %} - - {% if update.changes.priority %} - - - - {% endif %} - - {% if update.changes.blocking %} - - - - {% endif %} -
- - - - - - -
- - -

- {{ update.actor_detail.first_name }} {{ update.actor_detail.last_name }} -

-
-

- {{ update.activity_time }} -

-
-
- - - - - {% for assignee in update.changes.assignees.old_value %} - - {% endfor %} - {% if update.changes.assignees.old_value and update.changes.assignee.new_value %} - - {% endif %} - {% for assignee in update.changes.assignees.new_value %} - - {% endfor %} - -
-

- Assignee: -

-
-

- {{ assignee }} -

-
- - -

- {{ assginee }} -

-
-
- - - - - - -
- - -

- Due Date: -

-
-

- {{ update.changes.target_date.new_value.0 }} -

-
-
- - - - - {% for dup in update.changes.duplicate.new_value %} - - {% endfor %} - -
- - -

- Duplicate: -

-
-

- {{ dup }} -

-
-
- - - - - - - -
- - -

- Labels: -

-
- - - {% for label in update.changes.labels.new_value %} - - {% endfor %} - -
-

- {{ label }} -

-
-
-
-
- - - - - - - - - -
- - -

- State: -

-
-

- {{ update.changes.state.old_value.0 }} -

-
-> -

- {{ update.changes.state.new_value.0 }} -

-
-
- - - - - - -
- - -

- Link: -

-
- - {{ update.changes.link.new_value.0 }} - -
-
- - - - - - - - -
- - -

- Priority: -

-
-

- {{ update.changes.priority.old_value.0 }} -

-
- -> - -

- {{ update.changes.priority.new_value.0 }} -

-
-
- - - - - {% for bl in update.changes.blocking.new_value %} - - {% endfor %} - -
- - -

- Blocking: -

-
- - {{ bl }} - -
-
-
- - {% if comments %} - - + {% for actor_comment in comment.actor_comments.new_value %} - - - - - - - - + {% endfor %}
-

- Comments +

+ {{ actor_comment|safe }}

+
-
-
- - - - {% for comment in comments %} - - {% endfor %} - -
-
- S -
- -
- - - - - {% for actor_comment in comment.actor_comments %} - - - - {% endfor %} -
-

- {{ comment.actor_detail.first_name }} {{ comment.actor_detail.last_name }} -

-
-
- {{ actor_comment.new_value.0 }} -
-
-
-
- {% endif %} - - -
-
+ {% endfor %} +
+ {% endif %} + + +
+ View issue +
+
+ -
- - - - -
-
- This email was sent to - {{ receiver.email }}. - If you'd rather not receive this kind of email, - you can unsubscribe to the issue - or - manage your email preferences. - - -
-
-
- - - + + + + +
+
+ This email was sent to + {{ receiver.email }}. + If you'd rather not receive this kind of email, + you can unsubscribe to the issue + or + manage your email preferences. + + +
+
+ diff --git a/deploy/1-click/install.sh b/deploy/1-click/install.sh new file mode 100644 index 00000000000..f32be504d0f --- /dev/null +++ b/deploy/1-click/install.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +if command -v curl &> /dev/null; then + sudo curl -sSL \ + -o /usr/local/bin/plane-app \ + https://raw.githubusercontent.com/makeplane/plane/${BRANCH:-master}/deploy/1-click/plane-app?token=$(date +%s) +else + sudo wget -q \ + -O /usr/local/bin/plane-app \ + https://raw.githubusercontent.com/makeplane/plane/${BRANCH:-master}/deploy/1-click/plane-app?token=$(date +%s) +fi + +sudo chmod +x /usr/local/bin/plane-app +sudo sed -i 's/export BRANCH=${BRANCH:-master}/export BRANCH='${BRANCH:-master}'/' /usr/local/bin/plane-app + +sudo plane-app --help \ No newline at end of file diff --git a/deploy/1-click/plane-app b/deploy/1-click/plane-app new file mode 100644 index 00000000000..445f39d697e --- /dev/null +++ b/deploy/1-click/plane-app @@ -0,0 +1,713 @@ +#!/bin/bash + +function print_header() { +clear + +cat <<"EOF" +--------------------------------------- + ____ _ +| _ \| | __ _ _ __ ___ +| |_) | |/ _` | '_ \ / _ \ +| __/| | (_| | | | | __/ +|_| |_|\__,_|_| |_|\___| + +--------------------------------------- +Project management tool from the future +--------------------------------------- + +EOF +} +function update_env_files() { + config_file=$1 + key=$2 + value=$3 + + # Check if the config file exists + if [ ! -f "$config_file" ]; then + echo "Config file not found. Creating a new one..." >&2 + touch "$config_file" + fi + + # Check if the key already exists in the config file + if grep -q "^$key=" "$config_file"; then + awk -v key="$key" -v value="$value" -F '=' '{if ($1 == key) $2 = value} 1' OFS='=' "$config_file" > "$config_file.tmp" && mv "$config_file.tmp" "$config_file" + else + echo "$key=$value" >> "$config_file" + fi +} +function read_env_file() { + config_file=$1 + key=$2 + + # Check if the config file exists + if [ ! -f "$config_file" ]; then + echo "Config file not found. Creating a new one..." >&2 + touch "$config_file" + fi + + # Check if the key already exists in the config file + if grep -q "^$key=" "$config_file"; then + value=$(awk -v key="$key" -F '=' '{if ($1 == key) print $2}' "$config_file") + echo "$value" + else + echo "" + fi +} +function update_config() { + config_file="$PLANE_INSTALL_DIR/config.env" + update_env_files "$config_file" "$1" "$2" +} +function read_config() { + config_file="$PLANE_INSTALL_DIR/config.env" + read_env_file "$config_file" "$1" +} +function update_env() { + config_file="$PLANE_INSTALL_DIR/.env" + update_env_files "$config_file" "$1" "$2" +} +function read_env() { + config_file="$PLANE_INSTALL_DIR/.env" + read_env_file "$config_file" "$1" +} +function show_message() { + print_header + + if [ "$2" == "replace_last_line" ]; then + PROGRESS_MSG[-1]="$1" + else + PROGRESS_MSG+=("$1") + fi + + for statement in "${PROGRESS_MSG[@]}"; do + echo "$statement" + done + +} +function prepare_environment() { + show_message "Prepare Environment..." >&2 + + show_message "- Updating OS with required tools ✋" >&2 + sudo apt-get update -y &> /dev/null + sudo apt-get upgrade -y &> /dev/null + + required_tools=("curl" "awk" "wget" "nano" "dialog" "git") + + for tool in "${required_tools[@]}"; do + if ! command -v $tool &> /dev/null; then + sudo apt install -y $tool &> /dev/null + fi + done + + show_message "- OS Updated ✅" "replace_last_line" >&2 + + # Install Docker if not installed + if ! command -v docker &> /dev/null; then + show_message "- Installing Docker ✋" >&2 + sudo curl -o- https://get.docker.com | bash - + + if [ "$EUID" -ne 0 ]; then + dockerd-rootless-setuptool.sh install &> /dev/null + fi + show_message "- Docker Installed ✅" "replace_last_line" >&2 + else + show_message "- Docker is already installed ✅" >&2 + fi + + update_config "PLANE_ARCH" "$CPU_ARCH" + update_config "DOCKER_VERSION" "$(docker -v | awk '{print $3}' | sed 's/,//g')" + update_config "PLANE_DATA_DIR" "$DATA_DIR" + update_config "PLANE_LOG_DIR" "$LOG_DIR" + + # echo "TRUE" + echo "Environment prepared successfully ✅" + show_message "Environment prepared successfully ✅" >&2 + show_message "" >&2 + return 0 +} +function download_plane() { + # Download Docker Compose File from github url + show_message "Downloading Plane Setup Files ✋" >&2 + curl -H 'Cache-Control: no-cache, no-store' \ + -s -o $PLANE_INSTALL_DIR/docker-compose.yaml \ + https://raw.githubusercontent.com/makeplane/plane/$BRANCH/deploy/selfhost/docker-compose.yml?$(date +%s) + + curl -H 'Cache-Control: no-cache, no-store' \ + -s -o $PLANE_INSTALL_DIR/variables-upgrade.env \ + https://raw.githubusercontent.com/makeplane/plane/$BRANCH/deploy/selfhost/variables.env?$(date +%s) + + # if .env does not exists rename variables-upgrade.env to .env + if [ ! -f "$PLANE_INSTALL_DIR/.env" ]; then + mv $PLANE_INSTALL_DIR/variables-upgrade.env $PLANE_INSTALL_DIR/.env + fi + + show_message "Plane Setup Files Downloaded ✅" "replace_last_line" >&2 + show_message "" >&2 + + echo "PLANE_DOWNLOADED" + return 0 +} +function printUsageInstructions() { + show_message "" >&2 + show_message "----------------------------------" >&2 + show_message "Usage Instructions" >&2 + show_message "----------------------------------" >&2 + show_message "" >&2 + show_message "To use the Plane Setup utility, use below commands" >&2 + show_message "" >&2 + + show_message "Usage: plane-app [OPTION]" >&2 + show_message "" >&2 + show_message " start Start Server" >&2 + show_message " stop Stop Server" >&2 + show_message " restart Restart Server" >&2 + show_message "" >&2 + show_message "other options" >&2 + show_message " -i, --install Install Plane" >&2 + show_message " -c, --configure Configure Plane" >&2 + show_message " -up, --upgrade Upgrade Plane" >&2 + show_message " -un, --uninstall Uninstall Plane" >&2 + show_message " -ui, --update-installer Update Plane Installer" >&2 + show_message " -h, --help Show help" >&2 + show_message "" >&2 + show_message "" >&2 + show_message "Application Data is stored in mentioned folders" >&2 + show_message " - DB Data: $DATA_DIR/postgres" >&2 + show_message " - Redis Data: $DATA_DIR/redis" >&2 + show_message " - Minio Data: $DATA_DIR/minio" >&2 + show_message "" >&2 + show_message "" >&2 + show_message "----------------------------------" >&2 + show_message "" >&2 +} +function build_local_image() { + show_message "- Downloading Plane Source Code ✋" >&2 + REPO=https://github.com/makeplane/plane.git + CURR_DIR=$PWD + PLANE_TEMP_CODE_DIR=$PLANE_INSTALL_DIR/temp + sudo rm -rf $PLANE_TEMP_CODE_DIR > /dev/null + + sudo git clone $REPO $PLANE_TEMP_CODE_DIR --branch $BRANCH --single-branch -q > /dev/null + + sudo cp $PLANE_TEMP_CODE_DIR/deploy/selfhost/build.yml $PLANE_TEMP_CODE_DIR/build.yml + + show_message "- Plane Source Code Downloaded ✅" "replace_last_line" >&2 + + show_message "- Building Docker Images ✋" >&2 + sudo docker compose --env-file=$PLANE_INSTALL_DIR/.env -f $PLANE_TEMP_CODE_DIR/build.yml build --no-cache +} +function check_for_docker_images() { + show_message "" >&2 + # show_message "Building Plane Images" >&2 + + update_env "DOCKERHUB_USER" "makeplane" + update_env "PULL_POLICY" "always" + CURR_DIR=$(pwd) + + if [ "$BRANCH" == "master" ]; then + update_env "APP_RELEASE" "latest" + export APP_RELEASE=latest + else + update_env "APP_RELEASE" "$BRANCH" + export APP_RELEASE=$BRANCH + fi + + if [ $CPU_ARCH == "amd64" ] || [ $CPU_ARCH == "x86_64" ]; then + # show_message "Building Plane Images for $CPU_ARCH is not required. Skipping... ✅" "replace_last_line" >&2 + echo "Building Plane Images for $CPU_ARCH is not required. Skipping..." + else + export DOCKERHUB_USER=myplane + show_message "Building Plane Images for $CPU_ARCH " >&2 + update_env "DOCKERHUB_USER" "myplane" + update_env "PULL_POLICY" "never" + + build_local_image + + sudo rm -rf $PLANE_INSTALL_DIR/temp > /dev/null + + show_message "- Docker Images Built ✅" "replace_last_line" >&2 + sudo cd $CURR_DIR + fi + + sudo sed -i "s|- pgdata:|- $DATA_DIR/postgres:|g" $PLANE_INSTALL_DIR/docker-compose.yaml + sudo sed -i "s|- redisdata:|- $DATA_DIR/redis:|g" $PLANE_INSTALL_DIR/docker-compose.yaml + sudo sed -i "s|- uploads:|- $DATA_DIR/minio:|g" $PLANE_INSTALL_DIR/docker-compose.yaml + + show_message "Downloading Plane Images for $CPU_ARCH ✋" >&2 + docker compose -f $PLANE_INSTALL_DIR/docker-compose.yaml --env-file=$PLANE_INSTALL_DIR/.env pull + show_message "Plane Images Downloaded ✅" "replace_last_line" >&2 +} +function configure_plane() { + show_message "" >&2 + show_message "Configuring Plane" >&2 + show_message "" >&2 + + exec 3>&1 + + nginx_port=$(read_env "NGINX_PORT") + domain_name=$(read_env "DOMAIN_NAME") + upload_limit=$(read_env "FILE_SIZE_LIMIT") + + NGINX_SETTINGS=$(dialog \ + --ok-label "Next" \ + --cancel-label "Skip" \ + --backtitle "Plane Configuration" \ + --title "Nginx Settings" \ + --form "" \ + 0 0 0 \ + "Port:" 1 1 "${nginx_port:-80}" 1 10 50 0 \ + "Domain:" 2 1 "${domain_name:-localhost}" 2 10 50 0 \ + "Upload Limit:" 3 1 "${upload_limit:-5242880}" 3 10 15 0 \ + 2>&1 1>&3) + + save_nginx_settings=0 + if [ $? -eq 0 ]; then + save_nginx_settings=1 + nginx_port=$(echo "$NGINX_SETTINGS" | sed -n 1p) + domain_name=$(echo "$NGINX_SETTINGS" | sed -n 2p) + upload_limit=$(echo "$NGINX_SETTINGS" | sed -n 3p) + fi + + + smtp_host=$(read_env "EMAIL_HOST") + smtp_user=$(read_env "EMAIL_HOST_USER") + smtp_password=$(read_env "EMAIL_HOST_PASSWORD") + smtp_port=$(read_env "EMAIL_PORT") + smtp_from=$(read_env "EMAIL_FROM") + smtp_tls=$(read_env "EMAIL_USE_TLS") + smtp_ssl=$(read_env "EMAIL_USE_SSL") + + SMTP_SETTINGS=$(dialog \ + --ok-label "Next" \ + --cancel-label "Skip" \ + --backtitle "Plane Configuration" \ + --title "SMTP Settings" \ + --form "" \ + 0 0 0 \ + "Host:" 1 1 "$smtp_host" 1 10 80 0 \ + "User:" 2 1 "$smtp_user" 2 10 80 0 \ + "Password:" 3 1 "$smtp_password" 3 10 80 0 \ + "Port:" 4 1 "${smtp_port:-587}" 4 10 5 0 \ + "From:" 5 1 "${smtp_from:-Mailer }" 5 10 80 0 \ + "TLS:" 6 1 "${smtp_tls:-1}" 6 10 1 1 \ + "SSL:" 7 1 "${smtp_ssl:-0}" 7 10 1 1 \ + 2>&1 1>&3) + + save_smtp_settings=0 + if [ $? -eq 0 ]; then + save_smtp_settings=1 + smtp_host=$(echo "$SMTP_SETTINGS" | sed -n 1p) + smtp_user=$(echo "$SMTP_SETTINGS" | sed -n 2p) + smtp_password=$(echo "$SMTP_SETTINGS" | sed -n 3p) + smtp_port=$(echo "$SMTP_SETTINGS" | sed -n 4p) + smtp_from=$(echo "$SMTP_SETTINGS" | sed -n 5p) + smtp_tls=$(echo "$SMTP_SETTINGS" | sed -n 6p) + fi + external_pgdb_url=$(dialog \ + --backtitle "Plane Configuration" \ + --title "Using External Postgres Database ?" \ + --ok-label "Next" \ + --cancel-label "Skip" \ + --inputbox "Enter your external database url" \ + 8 60 3>&1 1>&2 2>&3) + + + external_redis_url=$(dialog \ + --backtitle "Plane Configuration" \ + --title "Using External Redis Database ?" \ + --ok-label "Next" \ + --cancel-label "Skip" \ + --inputbox "Enter your external redis url" \ + 8 60 3>&1 1>&2 2>&3) + + + aws_region=$(read_env "AWS_REGION") + aws_access_key=$(read_env "AWS_ACCESS_KEY_ID") + aws_secret_key=$(read_env "AWS_SECRET_ACCESS_KEY") + aws_bucket=$(read_env "AWS_S3_BUCKET_NAME") + + + AWS_S3_SETTINGS=$(dialog \ + --ok-label "Next" \ + --cancel-label "Skip" \ + --backtitle "Plane Configuration" \ + --title "AWS S3 Bucket Configuration" \ + --form "" \ + 0 0 0 \ + "Region:" 1 1 "$aws_region" 1 10 50 0 \ + "Access Key:" 2 1 "$aws_access_key" 2 10 50 0 \ + "Secret Key:" 3 1 "$aws_secret_key" 3 10 50 0 \ + "Bucket:" 4 1 "$aws_bucket" 4 10 50 0 \ + 2>&1 1>&3) + + save_aws_settings=0 + if [ $? -eq 0 ]; then + save_aws_settings=1 + aws_region=$(echo "$AWS_S3_SETTINGS" | sed -n 1p) + aws_access_key=$(echo "$AWS_S3_SETTINGS" | sed -n 2p) + aws_secret_key=$(echo "$AWS_S3_SETTINGS" | sed -n 3p) + aws_bucket=$(echo "$AWS_S3_SETTINGS" | sed -n 4p) + fi + + # display dialogbox asking for confirmation to continue + CONFIRM_CONFIG=$(dialog \ + --title "Confirm Configuration" \ + --backtitle "Plane Configuration" \ + --yes-label "Confirm" \ + --no-label "Cancel" \ + --yesno \ + " + save_ngnix_settings: $save_nginx_settings + nginx_port: $nginx_port + domain_name: $domain_name + upload_limit: $upload_limit + + save_smtp_settings: $save_smtp_settings + smtp_host: $smtp_host + smtp_user: $smtp_user + smtp_password: $smtp_password + smtp_port: $smtp_port + smtp_from: $smtp_from + smtp_tls: $smtp_tls + smtp_ssl: $smtp_ssl + + save_aws_settings: $save_aws_settings + aws_region: $aws_region + aws_access_key: $aws_access_key + aws_secret_key: $aws_secret_key + aws_bucket: $aws_bucket + + pdgb_url: $external_pgdb_url + redis_url: $external_redis_url + " \ + 0 0 3>&1 1>&2 2>&3) + + if [ $? -eq 0 ]; then + if [ $save_nginx_settings == 1 ]; then + update_env "NGINX_PORT" "$nginx_port" + update_env "DOMAIN_NAME" "$domain_name" + update_env "WEB_URL" "http://$domain_name" + update_env "CORS_ALLOWED_ORIGINS" "http://$domain_name" + update_env "FILE_SIZE_LIMIT" "$upload_limit" + fi + + # check enable smpt settings value + if [ $save_smtp_settings == 1 ]; then + update_env "EMAIL_HOST" "$smtp_host" + update_env "EMAIL_HOST_USER" "$smtp_user" + update_env "EMAIL_HOST_PASSWORD" "$smtp_password" + update_env "EMAIL_PORT" "$smtp_port" + update_env "EMAIL_FROM" "$smtp_from" + update_env "EMAIL_USE_TLS" "$smtp_tls" + update_env "EMAIL_USE_SSL" "$smtp_ssl" + fi + + # check enable aws settings value + if [[ $save_aws_settings == 1 && $aws_access_key != "" && $aws_secret_key != "" ]] ; then + update_env "USE_MINIO" "0" + update_env "AWS_REGION" "$aws_region" + update_env "AWS_ACCESS_KEY_ID" "$aws_access_key" + update_env "AWS_SECRET_ACCESS_KEY" "$aws_secret_key" + update_env "AWS_S3_BUCKET_NAME" "$aws_bucket" + elif [[ -z $aws_access_key || -z $aws_secret_key ]] ; then + update_env "USE_MINIO" "1" + update_env "AWS_REGION" "" + update_env "AWS_ACCESS_KEY_ID" "" + update_env "AWS_SECRET_ACCESS_KEY" "" + update_env "AWS_S3_BUCKET_NAME" "uploads" + fi + + if [ "$external_pgdb_url" != "" ]; then + update_env "DATABASE_URL" "$external_pgdb_url" + fi + if [ "$external_redis_url" != "" ]; then + update_env "REDIS_URL" "$external_redis_url" + fi + fi + + exec 3>&- +} +function upgrade_configuration() { + upg_env_file="$PLANE_INSTALL_DIR/variables-upgrade.env" + # Check if the file exists + if [ -f "$upg_env_file" ]; then + # Read each line from the file + while IFS= read -r line; do + # Skip comments and empty lines + if [[ "$line" =~ ^\s*#.*$ ]] || [[ -z "$line" ]]; then + continue + fi + + # Split the line into key and value + key=$(echo "$line" | cut -d'=' -f1) + value=$(echo "$line" | cut -d'=' -f2-) + + current_value=$(read_env "$key") + + if [ -z "$current_value" ]; then + update_env "$key" "$value" + fi + done < "$upg_env_file" + fi +} +function install() { + show_message "" + if [ "$(uname)" == "Linux" ]; then + OS="linux" + OS_NAME=$(awk -F= '/^ID=/{print $2}' /etc/os-release) + # check the OS + if [ "$OS_NAME" == "ubuntu" ]; then + OS_SUPPORTED=true + show_message "******** Installing Plane ********" + show_message "" + + prepare_environment + + if [ $? -eq 0 ]; then + download_plane + if [ $? -eq 0 ]; then + # create_service + check_for_docker_images + + last_installed_on=$(read_config "INSTALLATION_DATE") + if [ "$last_installed_on" == "" ]; then + configure_plane + fi + printUsageInstructions + + update_config "INSTALLATION_DATE" "$(date)" + + show_message "Plane Installed Successfully ✅" + show_message "" + else + show_message "Download Failed ❌" + exit 1 + fi + else + show_message "Initialization Failed ❌" + exit 1 + fi + + else + PROGRESS_MSG="❌❌❌ Unsupported OS Detected ❌❌❌" + show_message "" + exit 1 + fi + else + PROGRESS_MSG="❌❌❌ Unsupported OS Detected : $(uname) ❌❌❌" + show_message "" + exit 1 + fi +} +function upgrade() { + if [ "$(uname)" == "Linux" ]; then + OS="linux" + OS_NAME=$(awk -F= '/^ID=/{print $2}' /etc/os-release) + # check the OS + if [ "$OS_NAME" == "ubuntu" ]; then + OS_SUPPORTED=true + + prepare_environment + + if [ $? -eq 0 ]; then + download_plane + if [ $? -eq 0 ]; then + check_for_docker_images + upgrade_configuration + update_config "UPGRADE_DATE" "$(date)" + + show_message "" + show_message "Plane Upgraded Successfully ✅" + show_message "" + printUsageInstructions + else + show_message "Download Failed ❌" + exit 1 + fi + else + show_message "Initialization Failed ❌" + exit 1 + fi + else + PROGRESS_MSG="Unsupported OS Detected" + show_message "" + exit 1 + fi + else + PROGRESS_MSG="Unsupported OS Detected : $(uname)" + show_message "" + exit 1 + fi +} +function uninstall() { + if [ "$(uname)" == "Linux" ]; then + OS="linux" + OS_NAME=$(awk -F= '/^ID=/{print $2}' /etc/os-release) + # check the OS + if [ "$OS_NAME" == "ubuntu" ]; then + OS_SUPPORTED=true + show_message "******** Uninstalling Plane ********" + show_message "" + + stop_server + # CHECK IF PLANE SERVICE EXISTS + # if [ -f "/etc/systemd/system/plane.service" ]; then + # sudo systemctl stop plane.service &> /dev/null + # sudo systemctl disable plane.service &> /dev/null + # sudo rm /etc/systemd/system/plane.service &> /dev/null + # sudo systemctl daemon-reload &> /dev/null + # fi + # show_message "- Plane Service removed ✅" + + if ! [ -x "$(command -v docker)" ]; then + echo "DOCKER_NOT_INSTALLED" &> /dev/null + else + # Ask of user input to confirm uninstall docker ? + CONFIRM_DOCKER_PURGE=$(dialog --title "Uninstall Docker" --yesno "Are you sure you want to uninstall docker ?" 8 60 3>&1 1>&2 2>&3) + if [ $? -eq 0 ]; then + show_message "- Uninstalling Docker ✋" + sudo apt-get purge -y docker-engine docker docker.io docker-ce docker-ce-cli docker-compose-plugin &> /dev/null + sudo apt-get autoremove -y --purge docker-engine docker docker.io docker-ce docker-compose-plugin &> /dev/null + show_message "- Docker Uninstalled ✅" "replace_last_line" >&2 + fi + fi + + rm $PLANE_INSTALL_DIR/.env &> /dev/null + rm $PLANE_INSTALL_DIR/variables-upgrade.env &> /dev/null + rm $PLANE_INSTALL_DIR/config.env &> /dev/null + rm $PLANE_INSTALL_DIR/docker-compose.yaml &> /dev/null + + # rm -rf $PLANE_INSTALL_DIR &> /dev/null + show_message "- Configuration Cleaned ✅" + + show_message "" + show_message "******** Plane Uninstalled ********" + show_message "" + show_message "" + show_message "Plane Configuration Cleaned with some exceptions" + show_message "- DB Data: $DATA_DIR/postgres" + show_message "- Redis Data: $DATA_DIR/redis" + show_message "- Minio Data: $DATA_DIR/minio" + show_message "" + show_message "" + show_message "Thank you for using Plane. We hope to see you again soon." + show_message "" + show_message "" + else + PROGRESS_MSG="Unsupported OS Detected : $(uname) ❌" + show_message "" + exit 1 + fi + else + PROGRESS_MSG="Unsupported OS Detected : $(uname) ❌" + show_message "" + exit 1 + fi +} +function start_server() { + docker_compose_file="$PLANE_INSTALL_DIR/docker-compose.yaml" + env_file="$PLANE_INSTALL_DIR/.env" + # check if both the files exits + if [ -f "$docker_compose_file" ] && [ -f "$env_file" ]; then + show_message "Starting Plane Server ✋" + docker compose -f $docker_compose_file --env-file=$env_file up -d + + # Wait for containers to be running + echo "Waiting for containers to start..." + while ! docker compose -f "$docker_compose_file" --env-file="$env_file" ps --services --filter "status=running" --quiet | grep -q "."; do + sleep 1 + done + show_message "Plane Server Started ✅" "replace_last_line" >&2 + else + show_message "Plane Server not installed. Please install Plane first ❌" "replace_last_line" >&2 + fi +} +function stop_server() { + docker_compose_file="$PLANE_INSTALL_DIR/docker-compose.yaml" + env_file="$PLANE_INSTALL_DIR/.env" + # check if both the files exits + if [ -f "$docker_compose_file" ] && [ -f "$env_file" ]; then + show_message "Stopping Plane Server ✋" + docker compose -f $docker_compose_file --env-file=$env_file down + show_message "Plane Server Stopped ✅" "replace_last_line" >&2 + else + show_message "Plane Server not installed. Please install Plane first ❌" "replace_last_line" >&2 + fi +} +function restart_server() { + docker_compose_file="$PLANE_INSTALL_DIR/docker-compose.yaml" + env_file="$PLANE_INSTALL_DIR/.env" + # check if both the files exits + if [ -f "$docker_compose_file" ] && [ -f "$env_file" ]; then + show_message "Restarting Plane Server ✋" + docker compose -f $docker_compose_file --env-file=$env_file restart + show_message "Plane Server Restarted ✅" "replace_last_line" >&2 + else + show_message "Plane Server not installed. Please install Plane first ❌" "replace_last_line" >&2 + fi +} +function show_help() { + # print_header + show_message "Usage: plane-app [OPTION]" >&2 + show_message "" >&2 + show_message " start Start Server" >&2 + show_message " stop Stop Server" >&2 + show_message " restart Restart Server" >&2 + show_message "" >&2 + show_message "other options" >&2 + show_message " -i, --install Install Plane" >&2 + show_message " -c, --configure Configure Plane" >&2 + show_message " -up, --upgrade Upgrade Plane" >&2 + show_message " -un, --uninstall Uninstall Plane" >&2 + show_message " -ui, --update-installer Update Plane Installer" >&2 + show_message " -h, --help Show help" >&2 + show_message "" >&2 + exit 1 + +} +function update_installer() { + show_message "Updating Plane Installer ✋" >&2 + curl -H 'Cache-Control: no-cache, no-store' \ + -s -o /usr/local/bin/plane-app \ + https://raw.githubusercontent.com/makeplane/plane/$BRANCH/deploy/1-click/install.sh?token=$(date +%s) + + chmod +x /usr/local/bin/plane-app > /dev/null&> /dev/null + show_message "Plane Installer Updated ✅" "replace_last_line" >&2 +} + +export BRANCH=${BRANCH:-master} +export APP_RELEASE=$BRANCH +export DOCKERHUB_USER=makeplane +export PULL_POLICY=always + +PLANE_INSTALL_DIR=/opt/plane +DATA_DIR=$PLANE_INSTALL_DIR/data +LOG_DIR=$PLANE_INSTALL_DIR/log +OS_SUPPORTED=false +CPU_ARCH=$(uname -m) +PROGRESS_MSG="" +USE_GLOBAL_IMAGES=1 + +mkdir -p $PLANE_INSTALL_DIR/{data,log} + +if [ "$1" == "start" ]; then + start_server +elif [ "$1" == "stop" ]; then + stop_server +elif [ "$1" == "restart" ]; then + restart_server +elif [ "$1" == "--install" ] || [ "$1" == "-i" ]; then + install +elif [ "$1" == "--configure" ] || [ "$1" == "-c" ]; then + configure_plane + printUsageInstructions +elif [ "$1" == "--upgrade" ] || [ "$1" == "-up" ]; then + upgrade +elif [ "$1" == "--uninstall" ] || [ "$1" == "-un" ]; then + uninstall +elif [ "$1" == "--update-installer" ] || [ "$1" == "-ui" ] ; then + update_installer +elif [ "$1" == "--help" ] || [ "$1" == "-h" ]; then + show_help +else + show_help +fi diff --git a/package.json b/package.json index 97b793a5b9c..64bd220589a 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ ], "scripts": { "build": "turbo run build", - "dev": "turbo run dev", + "dev": "turbo run dev --concurrency=13", "start": "turbo run start", "lint": "turbo run lint", "clean": "turbo run clean", @@ -34,4 +34,4 @@ "@types/react": "18.2.42" }, "packageManager": "yarn@1.22.19" -} +} \ No newline at end of file diff --git a/packages/editor/core/package.json b/packages/editor/core/package.json index 2a9ec776e19..8b31acdaf6d 100644 --- a/packages/editor/core/package.json +++ b/packages/editor/core/package.json @@ -30,7 +30,6 @@ "dependencies": { "@tiptap/core": "^2.1.13", "@tiptap/extension-blockquote": "^2.1.13", - "@tiptap/extension-code": "^2.1.13", "@tiptap/extension-code-block-lowlight": "^2.1.13", "@tiptap/extension-color": "^2.1.13", "@tiptap/extension-image": "^2.1.13", diff --git a/packages/editor/core/src/lib/editor-commands.ts b/packages/editor/core/src/lib/editor-commands.ts index 147797e2d16..4a56f07c2dc 100644 --- a/packages/editor/core/src/lib/editor-commands.ts +++ b/packages/editor/core/src/lib/editor-commands.ts @@ -34,8 +34,32 @@ export const toggleUnderline = (editor: Editor, range?: Range) => { }; export const toggleCodeBlock = (editor: Editor, range?: Range) => { - if (range) editor.chain().focus().deleteRange(range).toggleCodeBlock().run(); - else editor.chain().focus().toggleCodeBlock().run(); + // Check if code block is active then toggle code block + if (editor.isActive("codeBlock")) { + if (range) { + editor.chain().focus().deleteRange(range).toggleCodeBlock().run(); + return; + } + editor.chain().focus().toggleCodeBlock().run(); + return; + } + + // Check if user hasn't selected any text + const isSelectionEmpty = editor.state.selection.empty; + + if (isSelectionEmpty) { + if (range) { + editor.chain().focus().deleteRange(range).toggleCodeBlock().run(); + return; + } + editor.chain().focus().toggleCodeBlock().run(); + } else { + if (range) { + editor.chain().focus().deleteRange(range).toggleCode().run(); + return; + } + editor.chain().focus().toggleCode().run(); + } }; export const toggleOrderedList = (editor: Editor, range?: Range) => { @@ -59,8 +83,8 @@ export const toggleStrike = (editor: Editor, range?: Range) => { }; export const toggleBlockquote = (editor: Editor, range?: Range) => { - if (range) editor.chain().focus().deleteRange(range).toggleNode("paragraph", "paragraph").toggleBlockquote().run(); - else editor.chain().focus().toggleNode("paragraph", "paragraph").toggleBlockquote().run(); + if (range) editor.chain().focus().deleteRange(range).toggleBlockquote().run(); + else editor.chain().focus().toggleBlockquote().run(); }; export const insertTableCommand = (editor: Editor, range?: Range) => { diff --git a/packages/editor/core/src/ui/components/editor-container.tsx b/packages/editor/core/src/ui/components/editor-container.tsx index 4be5c984349..5480a51e931 100644 --- a/packages/editor/core/src/ui/components/editor-container.tsx +++ b/packages/editor/core/src/ui/components/editor-container.tsx @@ -12,7 +12,7 @@ export const EditorContainer = ({ editor, editorClassNames, hideDragHandle, chil
{ - editor?.chain().focus().run(); + editor?.chain().focus(undefined, { scrollIntoView: false }).run(); }} onMouseLeave={() => { hideDragHandle?.(); diff --git a/packages/editor/core/src/ui/extensions/code-inline/index.tsx b/packages/editor/core/src/ui/extensions/code-inline/index.tsx index 539dc9346ce..1c5d341090b 100644 --- a/packages/editor/core/src/ui/extensions/code-inline/index.tsx +++ b/packages/editor/core/src/ui/extensions/code-inline/index.tsx @@ -1,12 +1,80 @@ -import { markInputRule, markPasteRule } from "@tiptap/core"; -import Code from "@tiptap/extension-code"; +import { Mark, markInputRule, markPasteRule, mergeAttributes } from "@tiptap/core"; -export const inputRegex = /(?; +} + +declare module "@tiptap/core" { + interface Commands { + code: { + /** + * Set a code mark + */ + setCode: () => ReturnType; + /** + * Toggle inline code + */ + toggleCode: () => ReturnType; + /** + * Unset a code mark + */ + unsetCode: () => ReturnType; + }; + } +} + +export const inputRegex = /(?:^|\s)((?:`)((?:[^`]+))(?:`))$/; +export const pasteRegex = /(?:^|\s)((?:`)((?:[^`]+))(?:`))/g; + +export const CustomCodeInlineExtension = Mark.create({ + name: "code", + + addOptions() { + return { + HTMLAttributes: { + class: "rounded-md bg-custom-primary-30 mx-1 px-1 py-[2px] font-mono font-medium text-custom-text-1000", + spellcheck: "false", + }, + }; + }, + + excludes: "_", + + code: true, -export const CustomCodeInlineExtension = Code.extend({ exitable: true, - inclusive: false, + + parseHTML() { + return [{ tag: "code" }]; + }, + + renderHTML({ HTMLAttributes }) { + return ["code", mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]; + }, + + addCommands() { + return { + setCode: + () => + ({ commands }) => + commands.setMark(this.name), + toggleCode: + () => + ({ commands }) => + commands.toggleMark(this.name), + unsetCode: + () => + ({ commands }) => + commands.unsetMark(this.name), + }; + }, + + addKeyboardShortcuts() { + return { + "Mod-e": () => this.editor.commands.toggleCode(), + }; + }, + addInputRules() { return [ markInputRule({ @@ -15,6 +83,7 @@ export const CustomCodeInlineExtension = Code.extend({ }), ]; }, + addPasteRules() { return [ markPasteRule({ @@ -23,9 +92,4 @@ export const CustomCodeInlineExtension = Code.extend({ }), ]; }, -}).configure({ - HTMLAttributes: { - class: "rounded-md bg-custom-primary-30 mx-1 px-1 py-[2px] font-mono font-medium text-custom-text-1000", - spellcheck: "false", - }, }); diff --git a/packages/editor/core/src/ui/menus/menu-items/index.tsx b/packages/editor/core/src/ui/menus/menu-items/index.tsx index 610d677f825..f60febc59d6 100644 --- a/packages/editor/core/src/ui/menus/menu-items/index.tsx +++ b/packages/editor/core/src/ui/menus/menu-items/index.tsx @@ -106,7 +106,7 @@ export const TodoListItem = (editor: Editor): EditorMenuItem => ({ export const CodeItem = (editor: Editor): EditorMenuItem => ({ name: "code", - isActive: () => editor?.isActive("code"), + isActive: () => editor?.isActive("code") || editor?.isActive("codeBlock"), command: () => toggleCodeBlock(editor), icon: CodeIcon, }); @@ -120,7 +120,7 @@ export const NumberedListItem = (editor: Editor): EditorMenuItem => ({ export const QuoteItem = (editor: Editor): EditorMenuItem => ({ name: "quote", - isActive: () => editor?.isActive("quote"), + isActive: () => editor?.isActive("blockquote"), command: () => toggleBlockquote(editor), icon: QuoteIcon, }); diff --git a/packages/editor/document-editor/src/ui/extensions/index.tsx b/packages/editor/document-editor/src/ui/extensions/index.tsx index 7de1e292230..2576d0d74e7 100644 --- a/packages/editor/document-editor/src/ui/extensions/index.tsx +++ b/packages/editor/document-editor/src/ui/extensions/index.tsx @@ -1,56 +1,29 @@ import Placeholder from "@tiptap/extension-placeholder"; -import { IssueWidgetExtension } from "src/ui/extensions/widgets/issue-embed-widget"; - -import { IIssueEmbedConfig } from "src/ui/extensions/widgets/issue-embed-widget/types"; +import { IssueWidgetPlaceholder } from "src/ui/extensions/widgets/issue-embed-widget"; import { SlashCommand, DragAndDrop } from "@plane/editor-extensions"; -import { ISlashCommandItem, UploadImage } from "@plane/editor-core"; -import { IssueSuggestions } from "src/ui/extensions/widgets/issue-embed-suggestion-list"; -import { LayersIcon } from "@plane/ui"; +import { UploadImage } from "@plane/editor-core"; export const DocumentEditorExtensions = ( uploadFile: UploadImage, - issueEmbedConfig?: IIssueEmbedConfig, - setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void, - setHideDragHandle?: (hideDragHandlerFromDragDrop: () => void) => void -) => { - const additionalOptions: ISlashCommandItem[] = [ - { - key: "issue_embed", - title: "Issue embed", - description: "Embed an issue from the project.", - searchTerms: ["issue", "link", "embed"], - icon: , - command: ({ editor, range }) => { - editor - .chain() - .focus() - .insertContentAt( - range, - "

#issue_

\n" - ) - .run(); - }, - }, - ]; + setHideDragHandle?: (hideDragHandlerFromDragDrop: () => void) => void, + setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void +) => [ + SlashCommand(uploadFile, setIsSubmitting), + DragAndDrop(setHideDragHandle), + Placeholder.configure({ + placeholder: ({ node }) => { + if (node.type.name === "heading") { + return `Heading ${node.attrs.level}`; + } + if (node.type.name === "image" || node.type.name === "table") { + return ""; + } - return [ - SlashCommand(uploadFile, setIsSubmitting, additionalOptions), - DragAndDrop(setHideDragHandle), - Placeholder.configure({ - placeholder: ({ node }) => { - if (node.type.name === "heading") { - return `Heading ${node.attrs.level}`; - } - if (node.type.name === "image" || node.type.name === "table") { - return ""; - } + return "Press '/' for commands..."; + }, + includeChildren: true, + }), + IssueWidgetPlaceholder(), +]; - return "Press '/' for commands..."; - }, - includeChildren: true, - }), - IssueWidgetExtension({ issueEmbedConfig }), - IssueSuggestions(issueEmbedConfig ? issueEmbedConfig.issues : []), - ]; -}; diff --git a/packages/editor/document-editor/src/ui/extensions/widgets/issue-embed-suggestion-list/issue-suggestion-renderer.tsx b/packages/editor/document-editor/src/ui/extensions/widgets/issue-embed-suggestion-list/issue-suggestion-renderer.tsx index 637afe29c2a..869c7a8c6f3 100644 --- a/packages/editor/document-editor/src/ui/extensions/widgets/issue-embed-suggestion-list/issue-suggestion-renderer.tsx +++ b/packages/editor/document-editor/src/ui/extensions/widgets/issue-embed-suggestion-list/issue-suggestion-renderer.tsx @@ -78,7 +78,6 @@ const IssueSuggestionList = ({ const navigationKeys = ["ArrowUp", "ArrowDown", "Enter", "Tab"]; const onKeyDown = (e: KeyboardEvent) => { if (navigationKeys.includes(e.key)) { - e.preventDefault(); // if (editor.isFocused) { // editor.chain().blur(); // commandListContainer.current?.focus(); @@ -87,7 +86,6 @@ const IssueSuggestionList = ({ setSelectedIndex( (selectedIndex + displayedItems[currentSection].length - 1) % displayedItems[currentSection].length ); - e.stopPropagation(); return true; } if (e.key === "ArrowDown") { @@ -102,12 +100,10 @@ const IssueSuggestionList = ({ [currentSection]: [...prevItems[currentSection], ...nextItems], })); } - e.stopPropagation(); return true; } if (e.key === "Enter") { selectItem(currentSection, selectedIndex); - e.stopPropagation(); return true; } if (e.key === "Tab") { @@ -115,7 +111,6 @@ const IssueSuggestionList = ({ const nextSectionIndex = (currentSectionIndex + 1) % sections.length; setCurrentSection(sections[nextSectionIndex]); setSelectedIndex(0); - e.stopPropagation(); return true; } return false; @@ -150,7 +145,7 @@ const IssueSuggestionList = ({
{sections.map((section) => { const sectionItems = displayedItems[section]; @@ -193,29 +188,35 @@ const IssueSuggestionList = ({
) : null; }; - export const IssueListRenderer = () => { let component: ReactRenderer | null = null; let popup: any | null = null; return { onStart: (props: { editor: Editor; clientRect?: (() => DOMRect | null) | null }) => { + const container = document.querySelector(".frame-renderer") as HTMLElement; component = new ReactRenderer(IssueSuggestionList, { props, // @ts-ignore editor: props.editor, }); - // @ts-ignore - popup = tippy("body", { + popup = tippy(".frame-renderer", { + flipbehavior: ["bottom", "top"], + appendTo: () => document.querySelector(".frame-renderer") as HTMLElement, + flip: true, + flipOnUpdate: true, getReferenceClientRect: props.clientRect, - appendTo: () => document.querySelector("#editor-container"), content: component.element, showOnCreate: true, interactive: true, trigger: "manual", placement: "bottom-start", }); + + container.addEventListener("scroll", () => { + popup?.[0].destroy(); + }); }, onUpdate: (props: { editor: Editor; clientRect?: (() => DOMRect | null) | null }) => { component?.updateProps(props); @@ -230,10 +231,20 @@ export const IssueListRenderer = () => { popup?.[0].hide(); return true; } - // @ts-ignore - return component?.ref?.onKeyDown(props); + + const navigationKeys = ["ArrowUp", "ArrowDown", "Enter", "Tab"]; + if (navigationKeys.includes(props.event.key)) { + // @ts-ignore + component?.ref?.onKeyDown(props); + return true; + } + return false; }, onExit: (e) => { + const container = document.querySelector(".frame-renderer") as HTMLElement; + if (container) { + container.removeEventListener("scroll", () => {}); + } popup?.[0].destroy(); setTimeout(() => { component?.destroy(); diff --git a/packages/editor/document-editor/src/ui/extensions/widgets/issue-embed-widget/index.tsx b/packages/editor/document-editor/src/ui/extensions/widgets/issue-embed-widget/index.tsx index 9bbb34aa523..264a701521e 100644 --- a/packages/editor/document-editor/src/ui/extensions/widgets/issue-embed-widget/index.tsx +++ b/packages/editor/document-editor/src/ui/extensions/widgets/issue-embed-widget/index.tsx @@ -1,11 +1,3 @@ import { IssueWidget } from "src/ui/extensions/widgets/issue-embed-widget/issue-widget-node"; -import { IIssueEmbedConfig } from "src/ui/extensions/widgets/issue-embed-widget/types"; -interface IssueWidgetExtensionProps { - issueEmbedConfig?: IIssueEmbedConfig; -} - -export const IssueWidgetExtension = ({ issueEmbedConfig }: IssueWidgetExtensionProps) => - IssueWidget.configure({ - issueEmbedConfig, - }); +export const IssueWidgetPlaceholder = () => IssueWidget.configure({}); diff --git a/packages/editor/document-editor/src/ui/extensions/widgets/issue-embed-widget/issue-widget-card.tsx b/packages/editor/document-editor/src/ui/extensions/widgets/issue-embed-widget/issue-widget-card.tsx index caca2ded7bd..d3b6fd04f7d 100644 --- a/packages/editor/document-editor/src/ui/extensions/widgets/issue-embed-widget/issue-widget-card.tsx +++ b/packages/editor/document-editor/src/ui/extensions/widgets/issue-embed-widget/issue-widget-card.tsx @@ -1,77 +1,33 @@ // @ts-nocheck -import { useState, useEffect } from "react"; +import { Button } from "@plane/ui"; import { NodeViewWrapper } from "@tiptap/react"; -import { Avatar, AvatarGroup, Loader, PriorityIcon } from "@plane/ui"; -import { Calendar, AlertTriangle } from "lucide-react"; +import { Crown } from "lucide-react"; -export const IssueWidgetCard = (props) => { - const [loading, setLoading] = useState(1); - const [issueDetails, setIssueDetails] = useState(); - - useEffect(() => { - props.issueEmbedConfig - .fetchIssue(props.node.attrs.entity_identifier) - .then((issue) => { - setIssueDetails(issue); - setLoading(0); - }) - .catch(() => { - setLoading(-1); - }); - }, []); - - const completeIssueEmbedAction = () => { - props.issueEmbedConfig.clickAction(issueDetails.id, props.node.attrs.title); - }; - - return ( - - {loading == 0 ? ( -
-
- {issueDetails.project_detail.identifier}-{issueDetails.sequence_id} -
-

{issueDetails.name}

-
-
- +export const IssueWidgetCard = (props) => ( + +
+
+ {props.node.attrs.project_identifier}-{props.node.attrs.sequence_id} +
+
+
+
+
+
-
- - {issueDetails.assignee_details.map((assignee) => ( - - ))} - +
+ Embed and access issues in pages seamlessly, upgrade to plane pro now.
- {issueDetails.target_date && ( -
- - {new Date(issueDetails.target_date).toLocaleDateString()} -
- )}
+ + +
- ) : loading == -1 ? ( -
- - {"This Issue embed is not found in any project. It can no longer be updated or accessed from here."} -
- ) : ( -
- - -
- - -
-
-
- )} - - ); -}; +
+
+ +); diff --git a/packages/editor/document-editor/src/ui/extensions/widgets/issue-embed-widget/issue-widget-node.tsx b/packages/editor/document-editor/src/ui/extensions/widgets/issue-embed-widget/issue-widget-node.tsx index c13637bd916..6c744927adc 100644 --- a/packages/editor/document-editor/src/ui/extensions/widgets/issue-embed-widget/issue-widget-node.tsx +++ b/packages/editor/document-editor/src/ui/extensions/widgets/issue-embed-widget/issue-widget-node.tsx @@ -34,9 +34,7 @@ export const IssueWidget = Node.create({ }, addNodeView() { - return ReactNodeViewRenderer((props: Object) => ( - - )); + return ReactNodeViewRenderer((props: Object) => ); }, parseHTML() { diff --git a/packages/editor/document-editor/src/ui/extensions/widgets/issue-embed-widget/types.ts b/packages/editor/document-editor/src/ui/extensions/widgets/issue-embed-widget/types.ts deleted file mode 100644 index 615b55dee57..00000000000 --- a/packages/editor/document-editor/src/ui/extensions/widgets/issue-embed-widget/types.ts +++ /dev/null @@ -1,9 +0,0 @@ -export interface IEmbedConfig { - issueEmbedConfig: IIssueEmbedConfig; -} - -export interface IIssueEmbedConfig { - fetchIssue: (issueId: string) => Promise; - clickAction: (issueId: string, issueTitle: string) => void; - issues: Array; -} diff --git a/packages/editor/document-editor/src/ui/index.tsx b/packages/editor/document-editor/src/ui/index.tsx index e88632c3b40..d1bdbc93545 100644 --- a/packages/editor/document-editor/src/ui/index.tsx +++ b/packages/editor/document-editor/src/ui/index.tsx @@ -10,7 +10,6 @@ import { DocumentDetails } from "src/types/editor-types"; import { PageRenderer } from "src/ui/components/page-renderer"; import { getMenuOptions } from "src/utils/menu-options"; import { useRouter } from "next/router"; -import { IEmbedConfig } from "src/ui/extensions/widgets/issue-embed-widget/types"; interface IDocumentEditor { // document info @@ -47,7 +46,6 @@ interface IDocumentEditor { duplicationConfig?: IDuplicationConfig; pageLockConfig?: IPageLockConfig; pageArchiveConfig?: IPageArchiveConfig; - embedConfig?: IEmbedConfig; } interface DocumentEditorProps extends IDocumentEditor { forwardedRef?: React.Ref; @@ -75,13 +73,11 @@ const DocumentEditor = ({ duplicationConfig, pageLockConfig, pageArchiveConfig, - embedConfig, updatePageTitle, cancelUploadImage, onActionCompleteHandler, rerenderOnPropsChange, }: IDocumentEditor) => { - // const [alert, setAlert] = useState("") const { markings, updateMarkings } = useEditorMarkings(); const [sidePeekVisible, setSidePeekVisible] = useState(true); const router = useRouter(); @@ -112,12 +108,7 @@ const DocumentEditor = ({ cancelUploadImage, rerenderOnPropsChange, forwardedRef, - extensions: DocumentEditorExtensions( - uploadFile, - embedConfig?.issueEmbedConfig, - setIsSubmitting, - setHideDragHandleFunction - ), + extensions: DocumentEditorExtensions(uploadFile, setHideDragHandleFunction, setIsSubmitting), }); if (!editor) { @@ -158,11 +149,11 @@ const DocumentEditor = ({ documentDetails={documentDetails} isSubmitting={isSubmitting} /> -
+
-
+
void; - embedConfig?: IEmbedConfig; } interface DocumentReadOnlyEditorProps extends IDocumentReadOnlyEditor { @@ -51,7 +49,6 @@ const DocumentReadOnlyEditor = ({ pageDuplicationConfig, pageLockConfig, pageArchiveConfig, - embedConfig, rerenderOnPropsChange, onActionCompleteHandler, }: DocumentReadOnlyEditorProps) => { @@ -63,7 +60,7 @@ const DocumentReadOnlyEditor = ({ value, forwardedRef, rerenderOnPropsChange, - extensions: [IssueWidgetExtension({ issueEmbedConfig: embedConfig?.issueEmbedConfig })], + extensions: [IssueWidgetPlaceholder()], }); useEffect(() => { @@ -105,11 +102,11 @@ const DocumentReadOnlyEditor = ({ documentDetails={documentDetails} archivedAt={pageArchiveConfig && pageArchiveConfig.archived_at} /> -
+
-
+
Promise.resolve()} diff --git a/packages/types/src/inbox/inbox-issue.d.ts b/packages/types/src/inbox/inbox-issue.d.ts new file mode 100644 index 00000000000..c7d33f75b6a --- /dev/null +++ b/packages/types/src/inbox/inbox-issue.d.ts @@ -0,0 +1,65 @@ +import { TIssue } from "../issues/base"; + +export enum EInboxStatus { + PENDING = -2, + REJECT = -1, + SNOOZED = 0, + ACCEPTED = 1, + DUPLICATE = 2, +} + +export type TInboxStatus = + | EInboxStatus.PENDING + | EInboxStatus.REJECT + | EInboxStatus.SNOOZED + | EInboxStatus.ACCEPTED + | EInboxStatus.DUPLICATE; + +export type TInboxIssueDetail = { + id?: string; + source: "in-app"; + status: TInboxStatus; + duplicate_to: string | undefined; + snoozed_till: Date | undefined; +}; + +export type TInboxIssueDetailMap = Record< + string, + Record +>; // inbox_id -> issue_id -> TInboxIssueDetail + +export type TInboxIssueDetailIdMap = Record; // inbox_id -> issue_id[] + +export type TInboxIssueExtendedDetail = TIssue & { + issue_inbox: TInboxIssueDetail[]; +}; + +// property type checks +export type TInboxPendingStatus = { + status: EInboxStatus.PENDING; +}; + +export type TInboxRejectStatus = { + status: EInboxStatus.REJECT; +}; + +export type TInboxSnoozedStatus = { + status: EInboxStatus.SNOOZED; + snoozed_till: Date; +}; + +export type TInboxAcceptedStatus = { + status: EInboxStatus.ACCEPTED; +}; + +export type TInboxDuplicateStatus = { + status: EInboxStatus.DUPLICATE; + duplicate_to: string; // issue_id +}; + +export type TInboxDetailedStatus = + | TInboxPendingStatus + | TInboxRejectStatus + | TInboxSnoozedStatus + | TInboxAcceptedStatus + | TInboxDuplicateStatus; diff --git a/packages/types/src/inbox/inbox.d.ts b/packages/types/src/inbox/inbox.d.ts new file mode 100644 index 00000000000..1b4e23e0fc4 --- /dev/null +++ b/packages/types/src/inbox/inbox.d.ts @@ -0,0 +1,27 @@ +export type TInboxIssueFilterOptions = { + priority: string[]; + inbox_status: number[]; +}; + +export type TInboxIssueQueryParams = "priority" | "inbox_status"; + +export type TInboxIssueFilters = { filters: TInboxIssueFilterOptions }; + +export type TInbox = { + id: string; + name: string; + description: string; + workspace: string; + project: string; + is_default: boolean; + view_props: TInboxIssueFilters; + created_by: string; + updated_by: string; + created_at: Date; + updated_at: Date; + pending_issue_count: number; +}; + +export type TInboxDetailMap = Record; // inbox_id -> TInbox + +export type TInboxDetailIdMap = Record; // project_id -> inbox_id[] diff --git a/packages/types/src/inbox/root.d.ts b/packages/types/src/inbox/root.d.ts new file mode 100644 index 00000000000..2f10c088def --- /dev/null +++ b/packages/types/src/inbox/root.d.ts @@ -0,0 +1,2 @@ +export * from "./inbox"; +export * from "./inbox-issue"; diff --git a/packages/types/src/index.d.ts b/packages/types/src/index.d.ts index 209aa679424..6e8ded94296 100644 --- a/packages/types/src/index.d.ts +++ b/packages/types/src/index.d.ts @@ -13,7 +13,11 @@ export * from "./pages"; export * from "./ai"; export * from "./estimate"; export * from "./importer"; + +// FIXME: Remove this after development and the refactor/mobx-store-issue branch is stable export * from "./inbox"; +export * from "./inbox/root"; + export * from "./analytics"; export * from "./calendar"; export * from "./notifications"; diff --git a/packages/ui/src/dropdowns/custom-search-select.tsx b/packages/ui/src/dropdowns/custom-search-select.tsx index 9695eb93194..d1778e31aaa 100644 --- a/packages/ui/src/dropdowns/custom-search-select.tsx +++ b/packages/ui/src/dropdowns/custom-search-select.tsx @@ -27,6 +27,7 @@ export const CustomSearchSelect = (props: ICustomSearchSelectProps) => { onChange, options, onOpen, + onClose, optionsClassName = "", value, tabIndex, @@ -58,7 +59,10 @@ export const CustomSearchSelect = (props: ICustomSearchSelectProps) => { setIsOpen(true); if (referenceElement) referenceElement.focus(); }; - const closeDropdown = () => setIsOpen(false); + const closeDropdown = () => { + setIsOpen(false); + onClose && onClose(); + }; const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen); useOutsideClickDetector(dropdownRef, closeDropdown); @@ -114,7 +118,7 @@ export const CustomSearchSelect = (props: ICustomSearchSelectProps) => {
void; options: | { value: any; diff --git a/space/components/issues/peek-overview/comment/add-comment.tsx b/space/components/issues/peek-overview/comment/add-comment.tsx index d6c3ce4e6e4..ef1a115d282 100644 --- a/space/components/issues/peek-overview/comment/add-comment.tsx +++ b/space/components/issues/peek-overview/comment/add-comment.tsx @@ -14,6 +14,7 @@ import { Comment } from "types/issue"; import { LiteTextEditorWithRef } from "@plane/lite-text-editor"; // service import fileService from "services/file.service"; +import { RootStore } from "store/root"; const defaultValues: Partial = { comment_html: "", @@ -35,6 +36,9 @@ export const AddComment: React.FC = observer((props) => { } = useForm({ defaultValues }); const router = useRouter(); + const { project }: RootStore = useMobxStore(); + const workspaceId = project.workspace?.id; + const { workspace_slug, project_slug } = router.query as { workspace_slug: string; project_slug: string }; const { user: userStore, issueDetails: issueDetailStore } = useMobxStore(); @@ -78,8 +82,8 @@ export const AddComment: React.FC = observer((props) => { }} cancelUploadImage={fileService.cancelUpload} uploadFile={fileService.getUploadFileFunction(workspace_slug as string)} - deleteFile={fileService.deleteImage} - restoreFile={fileService.restoreImage} + deleteFile={fileService.getDeleteImageFunction(workspaceId as string)} + restoreFile={fileService.getRestoreImageFunction(workspaceId as string)} ref={editorRef} value={ !value || value === "" || (typeof value === "object" && Object.keys(value).length === 0) diff --git a/space/components/issues/peek-overview/comment/comment-detail-card.tsx b/space/components/issues/peek-overview/comment/comment-detail-card.tsx index a8216514076..7c6abe19956 100644 --- a/space/components/issues/peek-overview/comment/comment-detail-card.tsx +++ b/space/components/issues/peek-overview/comment/comment-detail-card.tsx @@ -17,6 +17,7 @@ import { Comment } from "types/issue"; import fileService from "services/file.service"; import useEditorSuggestions from "hooks/use-editor-suggestions"; +import { RootStore } from "store/root"; type Props = { workspaceSlug: string; comment: Comment; @@ -24,6 +25,9 @@ type Props = { export const CommentCard: React.FC = observer((props) => { const { comment, workspaceSlug } = props; + const { project }: RootStore = useMobxStore(); + const workspaceId = project.workspace?.id; + // store const { user: userStore, issueDetails: issueDetailStore } = useMobxStore(); // states @@ -105,8 +109,8 @@ export const CommentCard: React.FC = observer((props) => { onEnterKeyPress={handleSubmit(handleCommentUpdate)} cancelUploadImage={fileService.cancelUpload} uploadFile={fileService.getUploadFileFunction(workspaceSlug)} - deleteFile={fileService.deleteImage} - restoreFile={fileService.restoreImage} + deleteFile={fileService.getDeleteImageFunction(workspaceId as string)} + restoreFile={fileService.getRestoreImageFunction(workspaceId as string)} ref={editorRef} value={value} debouncedUpdatesEnabled={false} diff --git a/space/services/file.service.ts b/space/services/file.service.ts index b2d1f6ccd7e..ecebf92b7d3 100644 --- a/space/services/file.service.ts +++ b/space/services/file.service.ts @@ -74,6 +74,39 @@ class FileService extends APIService { }; } + getDeleteImageFunction(workspaceId: string) { + return async (src: string) => { + try { + const assetUrlWithWorkspaceId = `${workspaceId}/${this.extractAssetIdFromUrl(src, workspaceId)}`; + const data = await this.deleteImage(assetUrlWithWorkspaceId); + return data; + } catch (e) { + console.error(e); + } + }; + } + + getRestoreImageFunction(workspaceId: string) { + return async (src: string) => { + try { + const assetUrlWithWorkspaceId = `${workspaceId}/${this.extractAssetIdFromUrl(src, workspaceId)}`; + const data = await this.restoreImage(assetUrlWithWorkspaceId); + return data; + } catch (e) { + console.error(e); + } + }; + } + + extractAssetIdFromUrl(src: string, workspaceId: string): string { + const indexWhereAssetIdStarts = src.indexOf(workspaceId) + workspaceId.length + 1; + if (indexWhereAssetIdStarts === -1) { + throw new Error("Workspace ID not found in source string"); + } + const assetUrl = src.substring(indexWhereAssetIdStarts); + return assetUrl; + } + async deleteImage(assetUrlWithWorkspaceId: string): Promise { return this.delete(`/api/workspaces/file-assets/${assetUrlWithWorkspaceId}/`) .then((response) => response?.status) diff --git a/web/components/core/activity.tsx b/web/components/core/activity.tsx index b281b8c3651..b5a666774af 100644 --- a/web/components/core/activity.tsx +++ b/web/components/core/activity.tsx @@ -26,28 +26,30 @@ import { capitalizeFirstLetter } from "helpers/string.helper"; // types import { IIssueActivity } from "@plane/types"; -const IssueLink = ({ activity }: { activity: IIssueActivity }) => { +export const IssueLink = ({ activity }: { activity: IIssueActivity }) => { const router = useRouter(); const { workspaceSlug } = router.query; return ( - - - {activity.issue_detail ? `${activity.project_detail.identifier}-${activity.issue_detail.sequence_id}` : "Issue"}{" "} - {activity.issue_detail?.name} - + + {activity?.issue_detail ? ( + + {`${activity.project_detail.identifier}-${activity.issue_detail.sequence_id}`}{" "} + {activity.issue_detail?.name} + + ) : ( + + {" an Issue"}{" "} + + )} ); }; @@ -94,7 +96,7 @@ const EstimatePoint = observer((props: { point: string }) => { const { areEstimatesEnabledForCurrentProject, getEstimatePointValue } = useEstimate(); const currentPoint = Number(point) + 1; - const estimateValue = getEstimatePointValue(Number(point)); + const estimateValue = getEstimatePointValue(Number(point), null); return ( @@ -142,8 +144,18 @@ const activityDetails: { }, archived_at: { message: (activity) => { - if (activity.new_value === "restore") return "restored the issue."; - else return "archived the issue."; + if (activity.new_value === "restore") + return ( + <> + restored + + ); + else + return ( + <> + archived + + ); }, icon:

+ this issue + + . +

+ ), + textColor: (snoozeDatePassed: boolean = false) => (snoozeDatePassed ? "" : "text-custom-text-200"), + bgColor: (snoozeDatePassed: boolean = false) => (snoozeDatePassed ? "" : "bg-gray-500/10"), + borderColor: (snoozeDatePassed: boolean = false) => (snoozeDatePassed ? "" : "border-gray-500"), + }, +]; + +export const INBOX_ISSUE_SOURCE = "in-app"; diff --git a/web/constants/page.ts b/web/constants/page.ts index 4b303ae73b6..4183d46d18b 100644 --- a/web/constants/page.ts +++ b/web/constants/page.ts @@ -52,3 +52,38 @@ export const PAGE_ACCESS_SPECIFIERS: { key: number; label: string; icon: any }[] icon: Lock, }, ]; + +export const PAGE_EMPTY_STATE_DETAILS = { + All: { + key: "all", + title: "Write a note, a doc, or a full knowledge base", + description: + "Pages help you organise your thoughts to create wikis, discussions or even document heated takes for your project. Use it wisely!", + }, + Favorites: { + key: "favorites", + title: "No favorite pages yet", + description: "Favorites for quick access? mark them and find them right here.", + }, + Private: { + key: "private", + title: "No private pages yet", + description: "Keep your private thoughts here. When you're ready to share, the team's just a click away.", + }, + Shared: { + key: "shared", + title: "No shared pages yet", + description: "See pages shared with everyone in your project right here.", + }, + Archived: { + key: "archived", + title: "No archived pages yet", + description: "Archive pages not on your radar. Access them here when needed.", + }, + Recent: { + key: "recent", + title: "Write a note, a doc, or a full knowledge base", + description: + "Pages help you organise your thoughts to create wikis, discussions or even document heated takes for your project. Use it wisely! Pages will be sorted and grouped by last updated", + }, +}; diff --git a/web/constants/profile.ts b/web/constants/profile.ts index 063bb7e440f..0fffdbc9b2c 100644 --- a/web/constants/profile.ts +++ b/web/constants/profile.ts @@ -38,3 +38,47 @@ export const PROFILE_ACTION_LINKS: { Icon: Settings2, }, ]; + +export const PROFILE_VIEWER_TAB = [ + { + route: "", + label: "Summary", + selected: "/[workspaceSlug]/profile/[userId]", + }, +]; + +export const PROFILE_ADMINS_TAB = [ + { + route: "assigned", + label: "Assigned", + selected: "/[workspaceSlug]/profile/[userId]/assigned", + }, + { + route: "created", + label: "Created", + selected: "/[workspaceSlug]/profile/[userId]/created", + }, + { + route: "subscribed", + label: "Subscribed", + selected: "/[workspaceSlug]/profile/[userId]/subscribed", + }, +]; + +export const PROFILE_EMPTY_STATE_DETAILS = { + assigned: { + key: "assigned", + title: "No issues are assigned to you", + description: "Issues assigned to you can be tracked from here.", + }, + subscribed: { + key: "created", + title: "No issues yet", + description: "All issues created by you come here, track them here directly.", + }, + created: { + key: "subscribed", + title: "No issues yet", + description: "Subscribe to issues you are interested in, track all of them here.", + }, +}; diff --git a/web/constants/workspace.ts b/web/constants/workspace.ts index 1471de3958c..4e7a0d6ae03 100644 --- a/web/constants/workspace.ts +++ b/web/constants/workspace.ts @@ -190,3 +190,31 @@ export const WORKSPACE_SETTINGS_LINKS: { Icon: SettingIcon, }, ]; + +export const ALL_ISSUES_EMPTY_STATE_DETAILS = { + "all-issues": { + key: "all-issues", + title: "No issues in the project", + description: "First project done! Now, slice your work into trackable pieces with issues. Let's go!", + }, + assigned: { + key: "assigned", + title: "No issues yet", + description: "Issues assigned to you can be tracked from here.", + }, + created: { + key: "created", + title: "No issues yet", + description: "All issues created by you come here, track them here directly.", + }, + subscribed: { + key: "subscribed", + title: "No issues yet", + description: "Subscribe to issues you are interested in, track all of them here.", + }, + "custom-view": { + key: "custom-view", + title: "No issues yet", + description: "Issues that applies to the filters, track all of them here.", + }, +}; diff --git a/web/hooks/store/index.ts b/web/hooks/store/index.ts index 8727a3c94da..a0ce6b0e213 100644 --- a/web/hooks/store/index.ts +++ b/web/hooks/store/index.ts @@ -4,9 +4,6 @@ export * from "./use-cycle"; export * from "./use-dashboard"; export * from "./use-estimate"; export * from "./use-global-view"; -export * from "./use-inbox"; -export * from "./use-inbox-filters"; -export * from "./use-inbox-issues"; export * from "./use-label"; export * from "./use-member"; export * from "./use-mention"; @@ -22,3 +19,5 @@ export * from "./use-workspace"; export * from "./use-issues"; export * from "./use-kanban-view"; export * from "./use-issue-detail"; +export * from "./use-inbox"; +export * from "./use-inbox-issues"; diff --git a/web/hooks/store/use-inbox-filters.ts b/web/hooks/store/use-inbox-filters.ts deleted file mode 100644 index 1c0e1db8fff..00000000000 --- a/web/hooks/store/use-inbox-filters.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { useContext } from "react"; -// mobx store -import { StoreContext } from "contexts/store-context"; -// types -import { IInboxFiltersStore } from "store/inbox/inbox_filter.store"; - -export const useInboxFilters = (): IInboxFiltersStore => { - const context = useContext(StoreContext); - if (context === undefined) throw new Error("useInboxFilters must be used within StoreProvider"); - return context.inboxRoot.inboxFilters; -}; diff --git a/web/hooks/store/use-inbox-issues.ts b/web/hooks/store/use-inbox-issues.ts index 842019eeb51..2b2941f84ee 100644 --- a/web/hooks/store/use-inbox-issues.ts +++ b/web/hooks/store/use-inbox-issues.ts @@ -2,10 +2,14 @@ import { useContext } from "react"; // mobx store import { StoreContext } from "contexts/store-context"; // types -import { IInboxIssuesStore } from "store/inbox/inbox_issue.store"; +import { IInboxIssue } from "store/inbox/inbox_issue.store"; +import { IInboxFilter } from "store/inbox/inbox_filter.store"; -export const useInboxIssues = (): IInboxIssuesStore => { +export const useInboxIssues = (): { + issues: IInboxIssue; + filters: IInboxFilter; +} => { const context = useContext(StoreContext); if (context === undefined) throw new Error("useInboxIssues must be used within StoreProvider"); - return context.inboxRoot.inboxIssues; + return { issues: context.inbox.inboxIssue, filters: context.inbox.inboxFilter }; }; diff --git a/web/hooks/store/use-inbox.ts b/web/hooks/store/use-inbox.ts index 1c7d19039bc..598801d7472 100644 --- a/web/hooks/store/use-inbox.ts +++ b/web/hooks/store/use-inbox.ts @@ -2,10 +2,10 @@ import { useContext } from "react"; // mobx store import { StoreContext } from "contexts/store-context"; // types -import { IInboxStore } from "store/inbox/inbox.store"; +import { IInbox } from "store/inbox/inbox.store"; -export const useInbox = (): IInboxStore => { +export const useInbox = (): IInbox => { const context = useContext(StoreContext); if (context === undefined) throw new Error("useInbox must be used within StoreProvider"); - return context.inboxRoot.inbox; + return context.inbox.inbox; }; diff --git a/web/hooks/use-issue-embeds.tsx b/web/hooks/use-issue-embeds.tsx deleted file mode 100644 index 2c8f7700bcc..00000000000 --- a/web/hooks/use-issue-embeds.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { TIssue } from "@plane/types"; -import { PROJECT_ISSUES_LIST, STATES_LIST } from "constants/fetch-keys"; -import { EIssuesStoreType } from "constants/issue"; -import { StoreContext } from "contexts/store-context"; -import { autorun, toJS } from "mobx"; -import { useContext } from "react"; -import { IssueService } from "services/issue"; -import useSWR from "swr"; -import { useIssueDetail, useIssues, useMember, useProject, useProjectState } from "./store"; - -const issueService = new IssueService(); - -export const useIssueEmbeds = () => { - const workspaceSlug = useContext(StoreContext).app.router.workspaceSlug; - const projectId = useContext(StoreContext).app.router.projectId; - - const { getProjectById } = useProject(); - const { setPeekIssue } = useIssueDetail(); - const { getStateById } = useProjectState(); - const { getUserDetails } = useMember(); - - const { data: issuesResponse } = useSWR( - workspaceSlug && projectId ? PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string) : null, - workspaceSlug && projectId ? () => issueService.getIssues(workspaceSlug as string, projectId as string) : null - ); - - const issues = Object.values(issuesResponse ?? {}); - const issuesWithStateAndProject = issues.map((issue) => ({ - ...issue, - state_detail: toJS(getStateById(issue.state_id)), - project_detail: toJS(getProjectById(issue.project_id)), - assignee_details: issue.assignee_ids.map((assigneeid) => toJS(getUserDetails(assigneeid))), - })); - - const fetchIssue = async (issueId: string) => issuesWithStateAndProject.find((issue) => issue.id === issueId); - - const issueWidgetClickAction = (issueId: string) => { - if (!workspaceSlug || !projectId) return; - - setPeekIssue({ workspaceSlug, projectId: projectId, issueId }); - }; - - return { - issues: issuesWithStateAndProject, - fetchIssue, - issueWidgetClickAction, - }; -}; diff --git a/web/hooks/use-worskspace-issue-properties.ts b/web/hooks/use-workspace-issue-properties.ts similarity index 76% rename from web/hooks/use-worskspace-issue-properties.ts rename to web/hooks/use-workspace-issue-properties.ts index 0accc3ec2a2..f6c1c6c2fe1 100644 --- a/web/hooks/use-worskspace-issue-properties.ts +++ b/web/hooks/use-workspace-issue-properties.ts @@ -1,12 +1,12 @@ import useSWR from "swr"; import { useEstimate, useLabel, useProjectState } from "./store"; -export const useWorskspaceIssueProperties = (workspaceSlug: string | string[] | undefined) => { +export const useWorkspaceIssueProperties = (workspaceSlug: string | string[] | undefined) => { const { fetchWorkspaceLabels } = useLabel(); const { fetchWorkspaceStates } = useProjectState(); - const { fetchWorskpaceEstimates } = useEstimate(); + const { fetchWorkspaceEstimates } = useEstimate(); // fetch workspace labels useSWR( @@ -23,6 +23,6 @@ export const useWorskspaceIssueProperties = (workspaceSlug: string | string[] | // fetch workspace estimates useSWR( workspaceSlug ? `WORKSPACE_ESTIMATES_${workspaceSlug}` : null, - workspaceSlug ? () => fetchWorskpaceEstimates(workspaceSlug.toString()) : null + workspaceSlug ? () => fetchWorkspaceEstimates(workspaceSlug.toString()) : null ); }; diff --git a/web/layouts/auth-layout/project-wrapper.tsx b/web/layouts/auth-layout/project-wrapper.tsx index 4f123f537f8..d7d33fd0ebb 100644 --- a/web/layouts/auth-layout/project-wrapper.tsx +++ b/web/layouts/auth-layout/project-wrapper.tsx @@ -30,14 +30,14 @@ interface IProjectAuthWrapper { export const ProjectAuthWrapper: FC = observer((props) => { const { children } = props; // store - const { fetchInboxesList, isInboxEnabled } = useInbox(); + const { fetchInboxes } = useInbox(); const { commandPalette: { toggleCreateProjectModal }, } = useApplication(); const { membership: { fetchUserProjectInfo, projectMemberInfo, hasPermissionToProject }, } = useUser(); - const { getProjectById, fetchProjectDetails } = useProject(); + const { getProjectById, fetchProjectDetails, currentProjectDetails } = useProject(); const { fetchAllCycles } = useCycle(); const { fetchModules } = useModule(); const { fetchViews } = useProjectView(); @@ -96,11 +96,13 @@ export const ProjectAuthWrapper: FC = observer((props) => { workspaceSlug && projectId ? `PROJECT_VIEWS_${workspaceSlug}_${projectId}` : null, workspaceSlug && projectId ? () => fetchViews(workspaceSlug.toString(), projectId.toString()) : null ); - // fetching project inboxes if inbox is enabled + // fetching project inboxes if inbox is enabled in project settings useSWR( - workspaceSlug && projectId && isInboxEnabled ? `PROJECT_INBOXES_${workspaceSlug}_${projectId}` : null, - workspaceSlug && projectId && isInboxEnabled - ? () => fetchInboxesList(workspaceSlug.toString(), projectId.toString()) + workspaceSlug && projectId && currentProjectDetails && currentProjectDetails.inbox_view + ? `PROJECT_INBOXES_${workspaceSlug}_${projectId}` + : null, + workspaceSlug && projectId && currentProjectDetails && currentProjectDetails.inbox_view + ? () => fetchInboxes(workspaceSlug.toString(), projectId.toString()) : null, { revalidateOnFocus: false, diff --git a/web/pages/[workspaceSlug]/active-cycles.tsx b/web/pages/[workspaceSlug]/active-cycles.tsx index 61d57e2e695..c0478981526 100644 --- a/web/pages/[workspaceSlug]/active-cycles.tsx +++ b/web/pages/[workspaceSlug]/active-cycles.tsx @@ -1,13 +1,13 @@ import { ReactElement } from "react"; // components -import { WorkspaceActiveCyclesList } from "components/workspace"; import { WorkspaceActiveCycleHeader } from "components/headers"; +import { WorkspaceActiveCyclesUpgrade } from "components/workspace"; // layouts import { AppLayout } from "layouts/app-layout"; // types import { NextPageWithLayout } from "lib/types"; -const WorkspaceActiveCyclesPage: NextPageWithLayout = () => ; +const WorkspaceActiveCyclesPage: NextPageWithLayout = () => ; WorkspaceActiveCyclesPage.getLayout = function getLayout(page: ReactElement) { return }>{page}; diff --git a/web/pages/[workspaceSlug]/analytics.tsx b/web/pages/[workspaceSlug]/analytics.tsx index 4bd7b83d0c6..fcaef9f70bf 100644 --- a/web/pages/[workspaceSlug]/analytics.tsx +++ b/web/pages/[workspaceSlug]/analytics.tsx @@ -8,11 +8,7 @@ import { AppLayout } from "layouts/app-layout"; // components import { CustomAnalytics, ScopeAndDemand } from "components/analytics"; import { WorkspaceAnalyticsHeader } from "components/headers"; -import { NewEmptyState } from "components/common/new-empty-state"; -// icons -import { Plus } from "lucide-react"; -// assets -import emptyAnalytics from "public/empty-state/empty_analytics.webp"; +import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; // constants import { ANALYTICS_TABS } from "constants/analytics"; import { EUserWorkspaceRoles } from "constants/workspace"; @@ -26,11 +22,13 @@ const AnalyticsPage: NextPageWithLayout = observer(() => { eventTracker: { setTrackElement }, } = useApplication(); const { - membership: { currentProjectRole }, + membership: { currentWorkspaceRole }, + currentUser, } = useUser(); const { workspaceProjectIds } = useProject(); - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER; + const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "analytics", currentUser?.theme.theme === "light"); + const isEditingAllowed = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER; return ( <> @@ -63,29 +61,25 @@ const AnalyticsPage: NextPageWithLayout = observer(() => {
) : ( - <> - , - text: "Create Cycles and Modules first", - onClick: () => { - setTrackElement("ANALYTICS_EMPTY_STATE"); - toggleCreateProjectModal(true); - }, - }} - disabled={!isEditingAllowed} - /> - + { + setTrackElement("ANALYTICS_EMPTY_STATE"); + toggleCreateProjectModal(true); + }, + }} + comicBox={{ + title: "Analytics works best with Cycles + Modules", + description: + "First, timebox your issues into Cycles and, if you can, group issues that span more than a cycle into Modules. Check out both on the left nav.", + }} + size="lg" + disabled={!isEditingAllowed} + /> )} ); diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/archived-issues/[archivedIssueId].tsx b/web/pages/[workspaceSlug]/projects/[projectId]/archived-issues/[archivedIssueId].tsx index d8238d37235..046fdc0c458 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/archived-issues/[archivedIssueId].tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/archived-issues/[archivedIssueId].tsx @@ -1,43 +1,22 @@ -import { useCallback, useEffect, useState, ReactElement } from "react"; +import { useState, ReactElement } from "react"; import { useRouter } from "next/router"; -import useSWR, { mutate } from "swr"; -import { useForm } from "react-hook-form"; -// services -import { IssueService, IssueArchiveService } from "services/issue"; +import useSWR from "swr"; // hooks import useToast from "hooks/use-toast"; +import { useIssueDetail, useIssues, useProject } from "hooks/store"; // layouts import { AppLayout } from "layouts/app-layout"; // components -// FIXME: have to replace this once the issue details page is ready --issue-detail-- -// import { IssueDetailsSidebar, IssueMainContent } from "components/issues"; +import { IssueDetailRoot } from "components/issues"; import { ProjectArchivedIssueDetailsHeader } from "components/headers"; // ui import { ArchiveIcon, Loader } from "@plane/ui"; // icons import { History } from "lucide-react"; // types -import { TIssue } from "@plane/types"; import { NextPageWithLayout } from "lib/types"; -// fetch-keys -import { PROJECT_ISSUES_ACTIVITY, ISSUE_DETAILS } from "constants/fetch-keys"; -import { useProject } from "hooks/store"; - -const defaultValues: Partial = { - name: "", - // description: "", - description_html: "", - estimate_point: null, - state_id: "", - priority: "low", - target_date: new Date().toString(), - cycle_id: null, - module_id: null, -}; - -// services -const issueService = new IssueService(); -const issueArchiveService = new IssueArchiveService(); +// constants +import { EIssuesStoreType } from "constants/issue"; const ArchivedIssueDetailsPage: NextPageWithLayout = () => { // router @@ -46,84 +25,43 @@ const ArchivedIssueDetailsPage: NextPageWithLayout = () => { // states const [isRestoring, setIsRestoring] = useState(false); // hooks + const { + fetchIssue, + issue: { getIssueById }, + } = useIssueDetail(); + const { + issues: { removeIssueFromArchived }, + } = useIssues(EIssuesStoreType.ARCHIVED); const { setToastAlert } = useToast(); const { getProjectById } = useProject(); - const { data: issueDetails, mutate: mutateIssueDetails } = useSWR( - workspaceSlug && projectId && archivedIssueId ? ISSUE_DETAILS(archivedIssueId as string) : null, + const { isLoading } = useSWR( workspaceSlug && projectId && archivedIssueId - ? () => - issueArchiveService.retrieveArchivedIssue( - workspaceSlug as string, - projectId as string, - archivedIssueId as string - ) + ? `ARCHIVED_ISSUE_DETAIL_${workspaceSlug}_${projectId}_${archivedIssueId}` + : null, + workspaceSlug && projectId && archivedIssueId + ? () => fetchIssue(workspaceSlug.toString(), projectId.toString(), archivedIssueId.toString(), true) : null ); - const { reset, control, watch } = useForm({ - defaultValues, - }); - - const submitChanges = useCallback( - async (formData: Partial) => { - if (!workspaceSlug || !projectId || !archivedIssueId) return; - - mutate( - ISSUE_DETAILS(archivedIssueId as string), - (prevData) => { - if (!prevData) return prevData; - - return { - ...prevData, - ...formData, - }; - }, - false - ); - - const payload: Partial = { - ...formData, - }; - - await issueService - .patchIssue(workspaceSlug as string, projectId as string, archivedIssueId as string, payload) - .then(() => { - mutateIssueDetails(); - mutate(PROJECT_ISSUES_ACTIVITY(archivedIssueId as string)); - }) - .catch((e) => { - console.error(e); - }); - }, - [workspaceSlug, archivedIssueId, projectId, mutateIssueDetails] - ); - - useEffect(() => { - if (!issueDetails) return; - - mutate(PROJECT_ISSUES_ACTIVITY(archivedIssueId as string)); - reset({ - ...issueDetails, - }); - }, [issueDetails, reset, archivedIssueId]); + const issue = getIssueById(archivedIssueId?.toString() || "") || undefined; + if (!issue) return <>; const handleUnArchive = async () => { if (!workspaceSlug || !projectId || !archivedIssueId) return; setIsRestoring(true); - await issueArchiveService - .unarchiveIssue(workspaceSlug as string, projectId as string, archivedIssueId as string) + await removeIssueFromArchived(workspaceSlug as string, projectId as string, archivedIssueId as string) .then(() => { setToastAlert({ type: "success", title: "Success", message: - issueDetails && - `${getProjectById(issueDetails.project_id)?.identifier}-${ - issueDetails?.sequence_id - } is restored successfully under the project ${getProjectById(issueDetails.project_id)?.name}`, + issue && + `${getProjectById(issue.project_id)?.identifier}-${ + issue?.sequence_id + } is restored successfully under the project ${getProjectById(issue.project_id)?.name}`, }); router.push(`/${workspaceSlug}/projects/${projectId}/issues/${archivedIssueId}`); }) @@ -137,12 +75,29 @@ const ArchivedIssueDetailsPage: NextPageWithLayout = () => { .finally(() => setIsRestoring(false)); }; + const issueLoader = !issue || isLoading ? true : false; + return ( <> - {issueDetails && projectId ? ( + {issueLoader ? ( + +
+ + + + +
+
+ + + + +
+
+ ) : (
-
- {issueDetails.archived_at && ( +
+ {issue?.archived_at && (
@@ -154,42 +109,20 @@ const ArchivedIssueDetailsPage: NextPageWithLayout = () => { disabled={isRestoring} > - {isRestoring ? "Restoring..." : "Restore Issue"}
)} - {/* FIXME: have to replace this once the issue details page is ready --issue-detail-- */} - {/*
- -
*/} + {workspaceSlug && projectId && archivedIssueId && ( + + )}
- {/* FIXME: have to replace this once the issue details page is ready --issue-detail-- */} - {/*
- -
*/}
- ) : ( - -
- - - - -
-
- - - - -
-
)} ); diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx index e35f0c34128..c0077731926 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx @@ -2,7 +2,6 @@ import { Fragment, useCallback, useState, ReactElement } from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import { Tab } from "@headlessui/react"; -import { Plus } from "lucide-react"; // hooks import { useCycle, useUser } from "hooks/store"; import useLocalStorage from "hooks/use-local-storage"; @@ -11,11 +10,9 @@ import { AppLayout } from "layouts/app-layout"; // components import { CyclesHeader } from "components/headers"; import { CyclesView, ActiveCycleDetails, CycleCreateUpdateModal } from "components/cycles"; -import { NewEmptyState } from "components/common/new-empty-state"; +import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; // ui -import { Tooltip } from "@plane/ui"; -// images -import emptyCycle from "public/empty-state/empty_cycles.webp"; +import { Spinner, Tooltip } from "@plane/ui"; // types import { TCycleView, TCycleLayout } from "@plane/types"; import { NextPageWithLayout } from "lib/types"; @@ -28,8 +25,9 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => { // store hooks const { membership: { currentProjectRole }, + currentUser, } = useUser(); - const { currentProjectCycleIds } = useCycle(); + const { currentProjectCycleIds, loader } = useCycle(); // router const router = useRouter(); const { workspaceSlug, projectId, peekCycle } = router.query; @@ -51,6 +49,7 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => { }, [handleCurrentLayout, setCycleTab] ); + const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "cycles", currentUser?.theme.theme === "light"); const totalCycles = currentProjectCycleIds?.length ?? 0; @@ -58,6 +57,13 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => { if (!workspaceSlug || !projectId) return null; + if (loader) + return ( +
+ +
+ ); + return (
{ /> {totalCycles === 0 ? (
- , text: "Set your first cycle", onClick: () => { setCreateModal(true); }, }} + size="lg" disabled={!isEditingAllowed} />
diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx b/web/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx index 203cd347435..5cd1e6c2c60 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx @@ -1,41 +1,61 @@ import { ReactElement } from "react"; import { useRouter } from "next/router"; import useSWR from "swr"; +import { observer } from "mobx-react"; // hooks -import { useInboxFilters } from "hooks/store/"; +import { useProject, useInboxIssues } from "hooks/store"; // layouts import { AppLayout } from "layouts/app-layout"; // components -import { InboxActionsHeader, InboxMainContent, InboxIssuesListSidebar } from "components/inbox"; import { ProjectInboxHeader } from "components/headers"; +import { InboxSidebarRoot, InboxContentRoot } from "components/inbox"; + // types import { NextPageWithLayout } from "lib/types"; -const ProjectInboxPage: NextPageWithLayout = () => { +const ProjectInboxPage: NextPageWithLayout = observer(() => { const router = useRouter(); - const { workspaceSlug, projectId, inboxId } = router.query; - - const { fetchInboxFilters } = useInboxFilters(); + const { workspaceSlug, projectId, inboxId, inboxIssueId } = router.query; + // store hooks + const { currentProjectDetails } = useProject(); + const { + filters: { fetchInboxFilters }, + issues: { fetchInboxIssues }, + } = useInboxIssues(); useSWR( - workspaceSlug && projectId && inboxId ? `INBOX_FILTERS_${inboxId.toString()}` : null, - workspaceSlug && projectId && inboxId - ? () => fetchInboxFilters(workspaceSlug.toString(), projectId.toString(), inboxId.toString()) - : null + workspaceSlug && projectId && currentProjectDetails && currentProjectDetails?.inbox_view + ? `INBOX_ISSUES_${workspaceSlug.toString()}_${projectId.toString()}` + : null, + async () => { + if (workspaceSlug && projectId && inboxId && currentProjectDetails && currentProjectDetails?.inbox_view) { + await fetchInboxFilters(workspaceSlug.toString(), projectId.toString(), inboxId.toString()); + await fetchInboxIssues(workspaceSlug.toString(), projectId.toString(), inboxId.toString()); + } + } ); + if (!workspaceSlug || !projectId || !inboxId || !currentProjectDetails?.inbox_view) return <>; return ( -
- -
- -
- -
+
+
+ +
+
+
); -}; +}); ProjectInboxPage.getLayout = function getLayout(page: ReactElement) { return ( diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/inbox/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/inbox/index.tsx new file mode 100644 index 00000000000..e75aa3a5409 --- /dev/null +++ b/web/pages/[workspaceSlug]/projects/[projectId]/inbox/index.tsx @@ -0,0 +1,49 @@ +import { ReactElement } from "react"; +import { useRouter } from "next/router"; +import useSWR from "swr"; +import { observer } from "mobx-react"; +// hooks +import { useInbox, useProject } from "hooks/store"; +// layouts +import { AppLayout } from "layouts/app-layout"; +// components +import { ProjectInboxHeader } from "components/headers"; +// types +import { NextPageWithLayout } from "lib/types"; + +const ProjectInboxPage: NextPageWithLayout = observer(() => { + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + + const { currentProjectDetails } = useProject(); + const { fetchInboxes } = useInbox(); + + useSWR( + workspaceSlug && projectId && currentProjectDetails && currentProjectDetails?.inbox_view + ? `INBOX_${workspaceSlug.toString()}_${projectId.toString()}` + : null, + async () => { + if (workspaceSlug && projectId && currentProjectDetails && currentProjectDetails?.inbox_view) { + const inboxes = await fetchInboxes(workspaceSlug.toString(), projectId.toString()); + if (inboxes && inboxes.length > 0) + router.push(`/${workspaceSlug}/projects/${projectId}/inbox/${inboxes[0].id}`); + } + } + ); + + return ( +
+ {currentProjectDetails?.inbox_view ?
Loading...
:
You don{"'"}t have access to inbox
} +
+ ); +}); + +ProjectInboxPage.getLayout = function getLayout(page: ReactElement) { + return ( + } withProjectWrapper> + {page} + + ); +}; + +export default ProjectInboxPage; diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx b/web/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx index a141455b793..be512dda031 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx @@ -5,7 +5,8 @@ import { useRouter } from "next/router"; import { ReactElement, useEffect, useRef, useState } from "react"; import { Controller, useForm } from "react-hook-form"; // hooks -import { useApplication, useIssues, usePage, useUser } from "hooks/store"; + +import { useApplication, usePage, useUser, useWorkspace } from "hooks/store"; import useReloadConfirmations from "hooks/use-reload-confirmation"; import useToast from "hooks/use-toast"; // services @@ -27,15 +28,10 @@ import { NextPageWithLayout } from "lib/types"; // constants import { EUserProjectRoles } from "constants/project"; import { useProjectPages } from "hooks/store/use-project-specific-pages"; -import { useIssueEmbeds } from "hooks/use-issue-embeds"; import { IssuePeekOverview } from "components/issues"; -import { PROJECT_ISSUES_LIST } from "constants/fetch-keys"; -import { IssueService } from "services/issue"; -import { EIssuesStoreType } from "constants/issue"; // services const fileService = new FileService(); -const issueService = new IssueService(); const PageDetailsPage: NextPageWithLayout = observer(() => { // states @@ -46,6 +42,9 @@ const PageDetailsPage: NextPageWithLayout = observer(() => { const router = useRouter(); const { workspaceSlug, projectId, pageId } = router.query; + const workspaceStore = useWorkspace(); + const workspaceId = workspaceStore.getWorkspaceBySlug(workspaceSlug as string)?.id as string; + // store hooks const { config: { envConfig }, @@ -88,8 +87,6 @@ const PageDetailsPage: NextPageWithLayout = observer(() => { : null ); - const { issues, fetchIssue, issueWidgetClickAction } = useIssueEmbeds(); - const pageStore = usePage(pageId as string); useEffect( @@ -259,7 +256,7 @@ const PageDetailsPage: NextPageWithLayout = observer(() => { const userCanLock = currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole); - return pageIdMobx && issues ? ( + return pageIdMobx ? (
{isPageReadOnly ? ( @@ -288,13 +285,6 @@ const PageDetailsPage: NextPageWithLayout = observer(() => { } : undefined } - embedConfig={{ - issueEmbedConfig: { - issues: issues, - fetchIssue: fetchIssue, - clickAction: issueWidgetClickAction, - }, - }} /> ) : (
@@ -312,10 +302,10 @@ const PageDetailsPage: NextPageWithLayout = observer(() => { last_updated_by: updated_by, }} uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)} + deleteFile={fileService.getDeleteImageFunction(workspaceId)} + restoreFile={fileService.getRestoreImageFunction(workspaceId)} value={pageDescription} setShouldShowAlert={setShowAlert} - deleteFile={fileService.deleteImage} - restoreFile={fileService.restoreImage} cancelUploadImage={fileService.cancelUpload} ref={editorRef} debouncedUpdatesEnabled={false} @@ -338,13 +328,6 @@ const PageDetailsPage: NextPageWithLayout = observer(() => { : undefined } pageLockConfig={userCanLock ? { is_locked: false, action: lockPage } : undefined} - embedConfig={{ - issueEmbedConfig: { - issues: issues, - fetchIssue: fetchIssue, - clickAction: issueWidgetClickAction, - }, - }} /> )} /> diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx index cd9699b34eb..32299747fca 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx @@ -5,19 +5,22 @@ import { Tab } from "@headlessui/react"; import useSWR from "swr"; import { observer } from "mobx-react-lite"; // hooks -import { useUser } from "hooks/store"; +import { useApplication, useUser } from "hooks/store"; import useLocalStorage from "hooks/use-local-storage"; import useUserAuth from "hooks/use-user-auth"; // layouts import { AppLayout } from "layouts/app-layout"; // components import { RecentPagesList, CreateUpdatePageModal } from "components/pages"; +import { EmptyState, getEmptyStateImagePath } from "components/empty-state"; import { PagesHeader } from "components/headers"; +import { Spinner } from "@plane/ui"; // types import { NextPageWithLayout } from "lib/types"; // constants import { PAGE_TABS_LIST } from "constants/page"; import { useProjectPages } from "hooks/store/use-project-page"; +import { EUserWorkspaceRoles } from "constants/workspace"; const AllPagesList = dynamic(() => import("components/pages").then((a) => a.AllPagesList), { ssr: false, @@ -46,9 +49,17 @@ const ProjectPagesPage: NextPageWithLayout = observer(() => { // states const [createUpdatePageModal, setCreateUpdatePageModal] = useState(false); // store - const { currentUser, currentUserLoader } = useUser(); + const { + currentUser, + currentUserLoader, + membership: { currentProjectRole }, + } = useUser(); + const { + commandPalette: { toggleCreatePageModal }, + } = useApplication(); - const { fetchProjectPages, fetchArchivedProjectPages } = useProjectPages(); + const { fetchProjectPages, fetchArchivedProjectPages, loader, archivedPageLoader, projectPageIds, archivedPageIds } = + useProjectPages(); // hooks const {} = useUserAuth({ user: currentUser, isLoading: currentUserLoader }); // local storage @@ -83,81 +94,113 @@ const ProjectPagesPage: NextPageWithLayout = observer(() => { } }; + const EmptyStateImagePath = getEmptyStateImagePath("onboarding", "pages", currentUser?.theme.theme === "light"); + + const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER; + + if (loader || archivedPageLoader) + return ( +
+ +
+ ); + return ( <> - {workspaceSlug && projectId && ( - setCreateUpdatePageModal(false)} - projectId={projectId.toString()} + {projectPageIds && archivedPageIds && projectPageIds.length + archivedPageIds.length > 0 ? ( + <> + {workspaceSlug && projectId && ( + setCreateUpdatePageModal(false)} + projectId={projectId.toString()} + /> + )} +
+
+

Pages

+
+ { + switch (i) { + case 0: + return setPageTab("Recent"); + case 1: + return setPageTab("All"); + case 2: + return setPageTab("Favorites"); + case 3: + return setPageTab("Private"); + case 4: + return setPageTab("Shared"); + case 5: + return setPageTab("Archived"); + default: + return setPageTab("All"); + } + }} + > + +
+ {PAGE_TABS_LIST.map((tab) => ( + + `rounded-full border px-5 py-1.5 text-sm outline-none ${ + selected + ? "border-custom-primary bg-custom-primary text-white" + : "border-custom-border-200 bg-custom-background-100 hover:bg-custom-background-90" + }` + } + > + {tab.title} + + ))} +
+
+ + + + + + + + + + + + + + + + + + + + +
+
+ + ) : ( + toggleCreatePageModal(true), + }} + comicBox={{ + title: "A page can be a doc or a doc of docs.", + description: + "We wrote Nikhil and Meera’s love story. You could write your project’s mission, goals, and eventual vision.", + }} + size="lg" + disabled={!isEditingAllowed} /> )} -
-
-

Pages

-
- { - switch (i) { - case 0: - return setPageTab("Recent"); - case 1: - return setPageTab("All"); - case 2: - return setPageTab("Favorites"); - case 3: - return setPageTab("Private"); - case 4: - return setPageTab("Shared"); - case 5: - return setPageTab("Archived"); - default: - return setPageTab("All"); - } - }} - > - -
- {PAGE_TABS_LIST.map((tab) => ( - - `rounded-full border px-5 py-1.5 text-sm outline-none ${ - selected - ? "border-custom-primary bg-custom-primary text-white" - : "border-custom-border-200 bg-custom-background-100 hover:bg-custom-background-90" - }` - } - > - {tab.title} - - ))} -
-
- - - - - - - - - - - - - - - - - - - - -
-
); }); diff --git a/web/pages/profile/activity.tsx b/web/pages/profile/activity.tsx index 0cd01090507..da3a55f9d8e 100644 --- a/web/pages/profile/activity.tsx +++ b/web/pages/profile/activity.tsx @@ -1,12 +1,15 @@ import { ReactElement } from "react"; import useSWR from "swr"; import Link from "next/link"; +import { observer } from "mobx-react"; +//hooks +import { useUser } from "hooks/store"; // services import { UserService } from "services/user.service"; // layouts import { ProfileSettingsLayout } from "layouts/settings-layout"; // components -import { ActivityIcon, ActivityMessage } from "components/core"; +import { ActivityIcon, ActivityMessage, IssueLink } from "components/core"; import { RichReadOnlyEditor } from "@plane/rich-text-editor"; // icons import { History, MessageSquare } from "lucide-react"; @@ -21,8 +24,10 @@ import { NextPageWithLayout } from "lib/types"; const userService = new UserService(); -const ProfileActivityPage: NextPageWithLayout = () => { +const ProfileActivityPage: NextPageWithLayout = observer(() => { const { data: userActivity } = useSWR(USER_ACTIVITY, () => userService.getUserActivity()); + // store hooks + const { currentUser } = useUser(); return (
@@ -93,21 +98,13 @@ const ProfileActivityPage: NextPageWithLayout = () => { activityItem.field !== "modules" && activityItem.field !== "attachment" && activityItem.field !== "link" && - activityItem.field !== "estimate" ? ( - - created{" "} - - - this issue. - - + activityItem.field !== "estimate" && + !activityItem.field ? ( + + created - ) : activityItem.field ? ( - ) : ( - "created the issue." + ); if ("field" in activityItem && activityItem.field !== "updated_by") { @@ -158,7 +155,9 @@ const ProfileActivityPage: NextPageWithLayout = () => { href={`/${activityItem.workspace_detail.slug}/profile/${activityItem.actor_detail.id}`} > - {activityItem.actor_detail.display_name} + {currentUser?.id === activityItem.actor_detail.id + ? "You" + : activityItem.actor_detail.display_name} )}{" "} @@ -189,7 +188,7 @@ const ProfileActivityPage: NextPageWithLayout = () => { )}
); -}; +}); ProfileActivityPage.getLayout = function getLayout(page: ReactElement) { return {page}; diff --git a/web/public/empty-state/all-issues/all-issues-dark.webp b/web/public/empty-state/all-issues/all-issues-dark.webp new file mode 100644 index 00000000000..2e7da76b332 Binary files /dev/null and b/web/public/empty-state/all-issues/all-issues-dark.webp differ diff --git a/web/public/empty-state/all-issues/all-issues-light.webp b/web/public/empty-state/all-issues/all-issues-light.webp new file mode 100644 index 00000000000..6b5897bf97e Binary files /dev/null and b/web/public/empty-state/all-issues/all-issues-light.webp differ diff --git a/web/public/empty-state/all-issues/assigned-dark.webp b/web/public/empty-state/all-issues/assigned-dark.webp new file mode 100644 index 00000000000..5e8e3916f51 Binary files /dev/null and b/web/public/empty-state/all-issues/assigned-dark.webp differ diff --git a/web/public/empty-state/all-issues/assigned-light.webp b/web/public/empty-state/all-issues/assigned-light.webp new file mode 100644 index 00000000000..6b04f3b30b1 Binary files /dev/null and b/web/public/empty-state/all-issues/assigned-light.webp differ diff --git a/web/public/empty-state/all-issues/created-dark.webp b/web/public/empty-state/all-issues/created-dark.webp new file mode 100644 index 00000000000..6394e63f706 Binary files /dev/null and b/web/public/empty-state/all-issues/created-dark.webp differ diff --git a/web/public/empty-state/all-issues/created-light.webp b/web/public/empty-state/all-issues/created-light.webp new file mode 100644 index 00000000000..cf2b55dbb2d Binary files /dev/null and b/web/public/empty-state/all-issues/created-light.webp differ diff --git a/web/public/empty-state/all-issues/custom-view-dark.webp b/web/public/empty-state/all-issues/custom-view-dark.webp new file mode 100644 index 00000000000..aba847d79cb Binary files /dev/null and b/web/public/empty-state/all-issues/custom-view-dark.webp differ diff --git a/web/public/empty-state/all-issues/custom-view-light.webp b/web/public/empty-state/all-issues/custom-view-light.webp new file mode 100644 index 00000000000..a531babb32c Binary files /dev/null and b/web/public/empty-state/all-issues/custom-view-light.webp differ diff --git a/web/public/empty-state/all-issues/no-project-dark.webp b/web/public/empty-state/all-issues/no-project-dark.webp new file mode 100644 index 00000000000..50c1ccf2177 Binary files /dev/null and b/web/public/empty-state/all-issues/no-project-dark.webp differ diff --git a/web/public/empty-state/all-issues/no-project-light.webp b/web/public/empty-state/all-issues/no-project-light.webp new file mode 100644 index 00000000000..564c74ee5f6 Binary files /dev/null and b/web/public/empty-state/all-issues/no-project-light.webp differ diff --git a/web/public/empty-state/all-issues/subscribed-dark.webp b/web/public/empty-state/all-issues/subscribed-dark.webp new file mode 100644 index 00000000000..6923b65e107 Binary files /dev/null and b/web/public/empty-state/all-issues/subscribed-dark.webp differ diff --git a/web/public/empty-state/all-issues/subscribed-light.webp b/web/public/empty-state/all-issues/subscribed-light.webp new file mode 100644 index 00000000000..d0411895b20 Binary files /dev/null and b/web/public/empty-state/all-issues/subscribed-light.webp differ diff --git a/web/public/empty-state/archived/empty-issues-dark.webp b/web/public/empty-state/archived/empty-issues-dark.webp new file mode 100644 index 00000000000..09d522d286e Binary files /dev/null and b/web/public/empty-state/archived/empty-issues-dark.webp differ diff --git a/web/public/empty-state/archived/empty-issues-light.webp b/web/public/empty-state/archived/empty-issues-light.webp new file mode 100644 index 00000000000..7aa422a4f80 Binary files /dev/null and b/web/public/empty-state/archived/empty-issues-light.webp differ diff --git a/web/public/empty-state/cycle/active-dark.webp b/web/public/empty-state/cycle/active-dark.webp new file mode 100644 index 00000000000..76de471941f Binary files /dev/null and b/web/public/empty-state/cycle/active-dark.webp differ diff --git a/web/public/empty-state/cycle/active-light.webp b/web/public/empty-state/cycle/active-light.webp new file mode 100644 index 00000000000..d5508c069ca Binary files /dev/null and b/web/public/empty-state/cycle/active-light.webp differ diff --git a/web/public/empty-state/cycle/completed-dark.webp b/web/public/empty-state/cycle/completed-dark.webp new file mode 100644 index 00000000000..9121d1f4d2f Binary files /dev/null and b/web/public/empty-state/cycle/completed-dark.webp differ diff --git a/web/public/empty-state/cycle/completed-light.webp b/web/public/empty-state/cycle/completed-light.webp new file mode 100644 index 00000000000..c1799c34a3d Binary files /dev/null and b/web/public/empty-state/cycle/completed-light.webp differ diff --git a/web/public/empty-state/cycle/draft-dark.webp b/web/public/empty-state/cycle/draft-dark.webp new file mode 100644 index 00000000000..251016532a8 Binary files /dev/null and b/web/public/empty-state/cycle/draft-dark.webp differ diff --git a/web/public/empty-state/cycle/draft-light.webp b/web/public/empty-state/cycle/draft-light.webp new file mode 100644 index 00000000000..7e809f36256 Binary files /dev/null and b/web/public/empty-state/cycle/draft-light.webp differ diff --git a/web/public/empty-state/cycle/upcoming-dark.webp b/web/public/empty-state/cycle/upcoming-dark.webp new file mode 100644 index 00000000000..d412702c0ac Binary files /dev/null and b/web/public/empty-state/cycle/upcoming-dark.webp differ diff --git a/web/public/empty-state/cycle/upcoming-light.webp b/web/public/empty-state/cycle/upcoming-light.webp new file mode 100644 index 00000000000..febf6ec897f Binary files /dev/null and b/web/public/empty-state/cycle/upcoming-light.webp differ diff --git a/web/public/empty-state/dashboard/dark/completed-assigned-issues.svg b/web/public/empty-state/dashboard/dark/completed-assigned-issues.svg deleted file mode 100644 index 6f730cbb340..00000000000 --- a/web/public/empty-state/dashboard/dark/completed-assigned-issues.svg +++ /dev/null @@ -1,96 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/web/public/empty-state/dashboard/dark/completed-created-issues.svg b/web/public/empty-state/dashboard/dark/completed-created-issues.svg deleted file mode 100644 index 605b6874dd8..00000000000 --- a/web/public/empty-state/dashboard/dark/completed-created-issues.svg +++ /dev/null @@ -1,165 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/web/public/empty-state/dashboard/dark/completed-issues.svg b/web/public/empty-state/dashboard/dark/completed-issues.svg new file mode 100644 index 00000000000..8c4d083fb4e --- /dev/null +++ b/web/public/empty-state/dashboard/dark/completed-issues.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/web/public/empty-state/dashboard/dark/issues-by-priority.svg b/web/public/empty-state/dashboard/dark/issues-by-priority.svg index f7a3536da64..803f3a72779 100644 --- a/web/public/empty-state/dashboard/dark/issues-by-priority.svg +++ b/web/public/empty-state/dashboard/dark/issues-by-priority.svg @@ -1,105 +1,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + diff --git a/web/public/empty-state/dashboard/dark/issues-by-state-group.svg b/web/public/empty-state/dashboard/dark/issues-by-state-group.svg index bf91b7df345..b615c313faf 100644 --- a/web/public/empty-state/dashboard/dark/issues-by-state-group.svg +++ b/web/public/empty-state/dashboard/dark/issues-by-state-group.svg @@ -1,74 +1,24 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web/public/empty-state/dashboard/dark/overdue-assigned-issues.svg b/web/public/empty-state/dashboard/dark/overdue-assigned-issues.svg deleted file mode 100644 index 8cf2c168279..00000000000 --- a/web/public/empty-state/dashboard/dark/overdue-assigned-issues.svg +++ /dev/null @@ -1,108 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/web/public/empty-state/dashboard/dark/overdue-created-issues.svg b/web/public/empty-state/dashboard/dark/overdue-created-issues.svg deleted file mode 100644 index 3457e2ae3d7..00000000000 --- a/web/public/empty-state/dashboard/dark/overdue-created-issues.svg +++ /dev/null @@ -1,171 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/web/public/empty-state/dashboard/dark/overdue-issues.svg b/web/public/empty-state/dashboard/dark/overdue-issues.svg new file mode 100644 index 00000000000..cceb3480d04 --- /dev/null +++ b/web/public/empty-state/dashboard/dark/overdue-issues.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/web/public/empty-state/dashboard/dark/recent-activity.svg b/web/public/empty-state/dashboard/dark/recent-activity.svg index e1dfca53b5c..bf7234828f6 100644 --- a/web/public/empty-state/dashboard/dark/recent-activity.svg +++ b/web/public/empty-state/dashboard/dark/recent-activity.svg @@ -1,102 +1,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + diff --git a/web/public/empty-state/dashboard/dark/recent-collaborators-1.svg b/web/public/empty-state/dashboard/dark/recent-collaborators-1.svg new file mode 100644 index 00000000000..341364f59a7 --- /dev/null +++ b/web/public/empty-state/dashboard/dark/recent-collaborators-1.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/web/public/empty-state/dashboard/dark/recent-collaborators-2.svg b/web/public/empty-state/dashboard/dark/recent-collaborators-2.svg new file mode 100644 index 00000000000..6889f750e8f --- /dev/null +++ b/web/public/empty-state/dashboard/dark/recent-collaborators-2.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/web/public/empty-state/dashboard/dark/recent-collaborators-3.svg b/web/public/empty-state/dashboard/dark/recent-collaborators-3.svg new file mode 100644 index 00000000000..deb0e211ca6 --- /dev/null +++ b/web/public/empty-state/dashboard/dark/recent-collaborators-3.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/web/public/empty-state/dashboard/dark/recent-collaborators.svg b/web/public/empty-state/dashboard/dark/recent-collaborators.svg deleted file mode 100644 index 4ae975c4e32..00000000000 --- a/web/public/empty-state/dashboard/dark/recent-collaborators.svg +++ /dev/null @@ -1,82 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/web/public/empty-state/dashboard/dark/upcoming-assigned-issues.svg b/web/public/empty-state/dashboard/dark/upcoming-assigned-issues.svg deleted file mode 100644 index 96a63224c8a..00000000000 --- a/web/public/empty-state/dashboard/dark/upcoming-assigned-issues.svg +++ /dev/null @@ -1,108 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/web/public/empty-state/dashboard/dark/upcoming-created-issues.svg b/web/public/empty-state/dashboard/dark/upcoming-created-issues.svg deleted file mode 100644 index 6202c318149..00000000000 --- a/web/public/empty-state/dashboard/dark/upcoming-created-issues.svg +++ /dev/null @@ -1,171 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/web/public/empty-state/dashboard/dark/upcoming-issues.svg b/web/public/empty-state/dashboard/dark/upcoming-issues.svg new file mode 100644 index 00000000000..25599450ae5 --- /dev/null +++ b/web/public/empty-state/dashboard/dark/upcoming-issues.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/web/public/empty-state/dashboard/light/completed-assigned-issues.svg b/web/public/empty-state/dashboard/light/completed-assigned-issues.svg deleted file mode 100644 index ce427654a00..00000000000 --- a/web/public/empty-state/dashboard/light/completed-assigned-issues.svg +++ /dev/null @@ -1,72 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/web/public/empty-state/dashboard/light/completed-created-issues.svg b/web/public/empty-state/dashboard/light/completed-created-issues.svg deleted file mode 100644 index 59960c7c174..00000000000 --- a/web/public/empty-state/dashboard/light/completed-created-issues.svg +++ /dev/null @@ -1,166 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/web/public/empty-state/dashboard/light/completed-issues.svg b/web/public/empty-state/dashboard/light/completed-issues.svg new file mode 100644 index 00000000000..6f9c4c430db --- /dev/null +++ b/web/public/empty-state/dashboard/light/completed-issues.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/web/public/empty-state/dashboard/light/issues-by-priority.svg b/web/public/empty-state/dashboard/light/issues-by-priority.svg index 6cc385a5b8c..23c35d92f81 100644 --- a/web/public/empty-state/dashboard/light/issues-by-priority.svg +++ b/web/public/empty-state/dashboard/light/issues-by-priority.svg @@ -1,90 +1,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + diff --git a/web/public/empty-state/dashboard/light/issues-by-state-group.svg b/web/public/empty-state/dashboard/light/issues-by-state-group.svg index 5d75cd9f0cb..eb3275372be 100644 --- a/web/public/empty-state/dashboard/light/issues-by-state-group.svg +++ b/web/public/empty-state/dashboard/light/issues-by-state-group.svg @@ -1,60 +1,27 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web/public/empty-state/dashboard/light/overdue-assigned-issues.svg b/web/public/empty-state/dashboard/light/overdue-assigned-issues.svg deleted file mode 100644 index b8e475b4b37..00000000000 --- a/web/public/empty-state/dashboard/light/overdue-assigned-issues.svg +++ /dev/null @@ -1,104 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/web/public/empty-state/dashboard/light/overdue-created-issues.svg b/web/public/empty-state/dashboard/light/overdue-created-issues.svg deleted file mode 100644 index 4340c8c4dab..00000000000 --- a/web/public/empty-state/dashboard/light/overdue-created-issues.svg +++ /dev/null @@ -1,172 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/web/public/empty-state/dashboard/light/overdue-issues.svg b/web/public/empty-state/dashboard/light/overdue-issues.svg new file mode 100644 index 00000000000..1c66ddd0bb3 --- /dev/null +++ b/web/public/empty-state/dashboard/light/overdue-issues.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/web/public/empty-state/dashboard/light/recent-activity.svg b/web/public/empty-state/dashboard/light/recent-activity.svg index 028ff78b645..3bb67491d0d 100644 --- a/web/public/empty-state/dashboard/light/recent-activity.svg +++ b/web/public/empty-state/dashboard/light/recent-activity.svg @@ -1,108 +1,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + diff --git a/web/public/empty-state/dashboard/light/recent-collaborators-1.svg b/web/public/empty-state/dashboard/light/recent-collaborators-1.svg new file mode 100644 index 00000000000..fb77e418d99 --- /dev/null +++ b/web/public/empty-state/dashboard/light/recent-collaborators-1.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/web/public/empty-state/dashboard/light/recent-collaborators-2.svg b/web/public/empty-state/dashboard/light/recent-collaborators-2.svg new file mode 100644 index 00000000000..e25fc9532c5 --- /dev/null +++ b/web/public/empty-state/dashboard/light/recent-collaborators-2.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/web/public/empty-state/dashboard/light/recent-collaborators-3.svg b/web/public/empty-state/dashboard/light/recent-collaborators-3.svg new file mode 100644 index 00000000000..e46473e84a4 --- /dev/null +++ b/web/public/empty-state/dashboard/light/recent-collaborators-3.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/web/public/empty-state/dashboard/light/recent-collaborators.svg b/web/public/empty-state/dashboard/light/recent-collaborators.svg deleted file mode 100644 index beaa9cb1345..00000000000 --- a/web/public/empty-state/dashboard/light/recent-collaborators.svg +++ /dev/null @@ -1,78 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/web/public/empty-state/dashboard/light/upcoming-assigned-issues.svg b/web/public/empty-state/dashboard/light/upcoming-assigned-issues.svg deleted file mode 100644 index 8a021a476b7..00000000000 --- a/web/public/empty-state/dashboard/light/upcoming-assigned-issues.svg +++ /dev/null @@ -1,104 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/web/public/empty-state/dashboard/light/upcoming-created-issues.svg b/web/public/empty-state/dashboard/light/upcoming-created-issues.svg deleted file mode 100644 index 0d9ee81951a..00000000000 --- a/web/public/empty-state/dashboard/light/upcoming-created-issues.svg +++ /dev/null @@ -1,172 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/web/public/empty-state/dashboard/light/upcoming-issues.svg b/web/public/empty-state/dashboard/light/upcoming-issues.svg new file mode 100644 index 00000000000..ad303581966 --- /dev/null +++ b/web/public/empty-state/dashboard/light/upcoming-issues.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/web/public/empty-state/draft/empty-issues-dark.webp b/web/public/empty-state/draft/empty-issues-dark.webp new file mode 100644 index 00000000000..0973e529011 Binary files /dev/null and b/web/public/empty-state/draft/empty-issues-dark.webp differ diff --git a/web/public/empty-state/draft/empty-issues-light.webp b/web/public/empty-state/draft/empty-issues-light.webp new file mode 100644 index 00000000000..0ce0bb9f353 Binary files /dev/null and b/web/public/empty-state/draft/empty-issues-light.webp differ diff --git a/web/public/empty-state/empty-filters/calendar-dark.webp b/web/public/empty-state/empty-filters/calendar-dark.webp new file mode 100644 index 00000000000..160e1d533ec Binary files /dev/null and b/web/public/empty-state/empty-filters/calendar-dark.webp differ diff --git a/web/public/empty-state/empty-filters/calendar-light.webp b/web/public/empty-state/empty-filters/calendar-light.webp new file mode 100644 index 00000000000..0ab8f52c618 Binary files /dev/null and b/web/public/empty-state/empty-filters/calendar-light.webp differ diff --git a/web/public/empty-state/empty-filters/gantt_chart-dark.webp b/web/public/empty-state/empty-filters/gantt_chart-dark.webp new file mode 100644 index 00000000000..861439ba4e6 Binary files /dev/null and b/web/public/empty-state/empty-filters/gantt_chart-dark.webp differ diff --git a/web/public/empty-state/empty-filters/gantt_chart-light.webp b/web/public/empty-state/empty-filters/gantt_chart-light.webp new file mode 100644 index 00000000000..22537f8c576 Binary files /dev/null and b/web/public/empty-state/empty-filters/gantt_chart-light.webp differ diff --git a/web/public/empty-state/empty-filters/kanban-dark.webp b/web/public/empty-state/empty-filters/kanban-dark.webp new file mode 100644 index 00000000000..7845a126fb3 Binary files /dev/null and b/web/public/empty-state/empty-filters/kanban-dark.webp differ diff --git a/web/public/empty-state/empty-filters/kanban-light.webp b/web/public/empty-state/empty-filters/kanban-light.webp new file mode 100644 index 00000000000..e071a09a484 Binary files /dev/null and b/web/public/empty-state/empty-filters/kanban-light.webp differ diff --git a/web/public/empty-state/empty-filters/list-dark.webp b/web/public/empty-state/empty-filters/list-dark.webp new file mode 100644 index 00000000000..8d9f160b8b7 Binary files /dev/null and b/web/public/empty-state/empty-filters/list-dark.webp differ diff --git a/web/public/empty-state/empty-filters/list-light.webp b/web/public/empty-state/empty-filters/list-light.webp new file mode 100644 index 00000000000..00bacf12e83 Binary files /dev/null and b/web/public/empty-state/empty-filters/list-light.webp differ diff --git a/web/public/empty-state/empty-filters/spreadsheet-dark.webp b/web/public/empty-state/empty-filters/spreadsheet-dark.webp new file mode 100644 index 00000000000..3e86982e212 Binary files /dev/null and b/web/public/empty-state/empty-filters/spreadsheet-dark.webp differ diff --git a/web/public/empty-state/empty-filters/spreadsheet-light.webp b/web/public/empty-state/empty-filters/spreadsheet-light.webp new file mode 100644 index 00000000000..e9c6e3a02c2 Binary files /dev/null and b/web/public/empty-state/empty-filters/spreadsheet-light.webp differ diff --git a/web/public/empty-state/onboarding/analytics-dark.webp b/web/public/empty-state/onboarding/analytics-dark.webp new file mode 100644 index 00000000000..f9ed54ae638 Binary files /dev/null and b/web/public/empty-state/onboarding/analytics-dark.webp differ diff --git a/web/public/empty-state/onboarding/analytics-light.webp b/web/public/empty-state/onboarding/analytics-light.webp new file mode 100644 index 00000000000..ca0f5f5516a Binary files /dev/null and b/web/public/empty-state/onboarding/analytics-light.webp differ diff --git a/web/public/empty-state/onboarding/cycles-dark.webp b/web/public/empty-state/onboarding/cycles-dark.webp new file mode 100644 index 00000000000..d655b5226ac Binary files /dev/null and b/web/public/empty-state/onboarding/cycles-dark.webp differ diff --git a/web/public/empty-state/onboarding/cycles-light.webp b/web/public/empty-state/onboarding/cycles-light.webp new file mode 100644 index 00000000000..ca069f50a6b Binary files /dev/null and b/web/public/empty-state/onboarding/cycles-light.webp differ diff --git a/web/public/empty-state/onboarding/dashboard-dark.webp b/web/public/empty-state/onboarding/dashboard-dark.webp new file mode 100644 index 00000000000..486060c09e4 Binary files /dev/null and b/web/public/empty-state/onboarding/dashboard-dark.webp differ diff --git a/web/public/empty-state/onboarding/dashboard-light.webp b/web/public/empty-state/onboarding/dashboard-light.webp new file mode 100644 index 00000000000..89d97bf0851 Binary files /dev/null and b/web/public/empty-state/onboarding/dashboard-light.webp differ diff --git a/web/public/empty-state/onboarding/issues-dark.webp b/web/public/empty-state/onboarding/issues-dark.webp new file mode 100644 index 00000000000..d1b1338a175 Binary files /dev/null and b/web/public/empty-state/onboarding/issues-dark.webp differ diff --git a/web/public/empty-state/onboarding/issues-light.webp b/web/public/empty-state/onboarding/issues-light.webp new file mode 100644 index 00000000000..b875a5eb0fa Binary files /dev/null and b/web/public/empty-state/onboarding/issues-light.webp differ diff --git a/web/public/empty-state/onboarding/modules-dark.webp b/web/public/empty-state/onboarding/modules-dark.webp new file mode 100644 index 00000000000..ee86e78806e Binary files /dev/null and b/web/public/empty-state/onboarding/modules-dark.webp differ diff --git a/web/public/empty-state/onboarding/modules-light.webp b/web/public/empty-state/onboarding/modules-light.webp new file mode 100644 index 00000000000..1eedadb6ef8 Binary files /dev/null and b/web/public/empty-state/onboarding/modules-light.webp differ diff --git a/web/public/empty-state/onboarding/pages-dark.webp b/web/public/empty-state/onboarding/pages-dark.webp new file mode 100644 index 00000000000..278d228df93 Binary files /dev/null and b/web/public/empty-state/onboarding/pages-dark.webp differ diff --git a/web/public/empty-state/onboarding/pages-light.webp b/web/public/empty-state/onboarding/pages-light.webp new file mode 100644 index 00000000000..b7826e24477 Binary files /dev/null and b/web/public/empty-state/onboarding/pages-light.webp differ diff --git a/web/public/empty-state/onboarding/projects-dark.webp b/web/public/empty-state/onboarding/projects-dark.webp new file mode 100644 index 00000000000..2f34a02dee9 Binary files /dev/null and b/web/public/empty-state/onboarding/projects-dark.webp differ diff --git a/web/public/empty-state/onboarding/projects-light.webp b/web/public/empty-state/onboarding/projects-light.webp new file mode 100644 index 00000000000..54894cbde26 Binary files /dev/null and b/web/public/empty-state/onboarding/projects-light.webp differ diff --git a/web/public/empty-state/onboarding/views-dark.webp b/web/public/empty-state/onboarding/views-dark.webp new file mode 100644 index 00000000000..bca0f383ce7 Binary files /dev/null and b/web/public/empty-state/onboarding/views-dark.webp differ diff --git a/web/public/empty-state/onboarding/views-light.webp b/web/public/empty-state/onboarding/views-light.webp new file mode 100644 index 00000000000..cd6900834af Binary files /dev/null and b/web/public/empty-state/onboarding/views-light.webp differ diff --git a/web/public/empty-state/pages/all-dark.webp b/web/public/empty-state/pages/all-dark.webp new file mode 100644 index 00000000000..3c9ea167aff Binary files /dev/null and b/web/public/empty-state/pages/all-dark.webp differ diff --git a/web/public/empty-state/pages/all-light.webp b/web/public/empty-state/pages/all-light.webp new file mode 100644 index 00000000000..54b1b47d997 Binary files /dev/null and b/web/public/empty-state/pages/all-light.webp differ diff --git a/web/public/empty-state/pages/archived-dark.webp b/web/public/empty-state/pages/archived-dark.webp new file mode 100644 index 00000000000..3a9543b54fa Binary files /dev/null and b/web/public/empty-state/pages/archived-dark.webp differ diff --git a/web/public/empty-state/pages/archived-light.webp b/web/public/empty-state/pages/archived-light.webp new file mode 100644 index 00000000000..54cc928c99d Binary files /dev/null and b/web/public/empty-state/pages/archived-light.webp differ diff --git a/web/public/empty-state/pages/favorites-dark.webp b/web/public/empty-state/pages/favorites-dark.webp new file mode 100644 index 00000000000..9ae67887085 Binary files /dev/null and b/web/public/empty-state/pages/favorites-dark.webp differ diff --git a/web/public/empty-state/pages/favorites-light.webp b/web/public/empty-state/pages/favorites-light.webp new file mode 100644 index 00000000000..88b62af4d71 Binary files /dev/null and b/web/public/empty-state/pages/favorites-light.webp differ diff --git a/web/public/empty-state/pages/private-dark.webp b/web/public/empty-state/pages/private-dark.webp new file mode 100644 index 00000000000..ac3e836b4d3 Binary files /dev/null and b/web/public/empty-state/pages/private-dark.webp differ diff --git a/web/public/empty-state/pages/private-light.webp b/web/public/empty-state/pages/private-light.webp new file mode 100644 index 00000000000..760e1365726 Binary files /dev/null and b/web/public/empty-state/pages/private-light.webp differ diff --git a/web/public/empty-state/pages/recent-dark.webp b/web/public/empty-state/pages/recent-dark.webp new file mode 100644 index 00000000000..4a103354e71 Binary files /dev/null and b/web/public/empty-state/pages/recent-dark.webp differ diff --git a/web/public/empty-state/pages/recent-light.webp b/web/public/empty-state/pages/recent-light.webp new file mode 100644 index 00000000000..4b908d39819 Binary files /dev/null and b/web/public/empty-state/pages/recent-light.webp differ diff --git a/web/public/empty-state/pages/shared-dark.webp b/web/public/empty-state/pages/shared-dark.webp new file mode 100644 index 00000000000..941960280d0 Binary files /dev/null and b/web/public/empty-state/pages/shared-dark.webp differ diff --git a/web/public/empty-state/pages/shared-light.webp b/web/public/empty-state/pages/shared-light.webp new file mode 100644 index 00000000000..a3ead55f305 Binary files /dev/null and b/web/public/empty-state/pages/shared-light.webp differ diff --git a/web/public/empty-state/profile/activities-dark.webp b/web/public/empty-state/profile/activities-dark.webp new file mode 100644 index 00000000000..1b693947a0f Binary files /dev/null and b/web/public/empty-state/profile/activities-dark.webp differ diff --git a/web/public/empty-state/profile/activities-light.webp b/web/public/empty-state/profile/activities-light.webp new file mode 100644 index 00000000000..c287e0cd5e1 Binary files /dev/null and b/web/public/empty-state/profile/activities-light.webp differ diff --git a/web/public/empty-state/profile/assigned-dark.webp b/web/public/empty-state/profile/assigned-dark.webp new file mode 100644 index 00000000000..eaec74dde25 Binary files /dev/null and b/web/public/empty-state/profile/assigned-dark.webp differ diff --git a/web/public/empty-state/profile/assigned-light.webp b/web/public/empty-state/profile/assigned-light.webp new file mode 100644 index 00000000000..59a7b06e36e Binary files /dev/null and b/web/public/empty-state/profile/assigned-light.webp differ diff --git a/web/public/empty-state/profile/created-dark.webp b/web/public/empty-state/profile/created-dark.webp new file mode 100644 index 00000000000..12f153519ab Binary files /dev/null and b/web/public/empty-state/profile/created-dark.webp differ diff --git a/web/public/empty-state/profile/created-light.webp b/web/public/empty-state/profile/created-light.webp new file mode 100644 index 00000000000..f95679f112c Binary files /dev/null and b/web/public/empty-state/profile/created-light.webp differ diff --git a/web/public/empty-state/profile/issues-by-priority-dark.webp b/web/public/empty-state/profile/issues-by-priority-dark.webp new file mode 100644 index 00000000000..e1a71802e2d Binary files /dev/null and b/web/public/empty-state/profile/issues-by-priority-dark.webp differ diff --git a/web/public/empty-state/profile/issues-by-priority-light.webp b/web/public/empty-state/profile/issues-by-priority-light.webp new file mode 100644 index 00000000000..16abada6170 Binary files /dev/null and b/web/public/empty-state/profile/issues-by-priority-light.webp differ diff --git a/web/public/empty-state/profile/issues-by-state-dark.webp b/web/public/empty-state/profile/issues-by-state-dark.webp new file mode 100644 index 00000000000..82210aa864c Binary files /dev/null and b/web/public/empty-state/profile/issues-by-state-dark.webp differ diff --git a/web/public/empty-state/profile/issues-by-state-light.webp b/web/public/empty-state/profile/issues-by-state-light.webp new file mode 100644 index 00000000000..73788bb8694 Binary files /dev/null and b/web/public/empty-state/profile/issues-by-state-light.webp differ diff --git a/web/public/empty-state/profile/subscribed-dark.webp b/web/public/empty-state/profile/subscribed-dark.webp new file mode 100644 index 00000000000..ae30d3d5db5 Binary files /dev/null and b/web/public/empty-state/profile/subscribed-dark.webp differ diff --git a/web/public/empty-state/profile/subscribed-light.webp b/web/public/empty-state/profile/subscribed-light.webp new file mode 100644 index 00000000000..d24f58f245b Binary files /dev/null and b/web/public/empty-state/profile/subscribed-light.webp differ diff --git a/web/public/workspace-active-cycles/cta-l-1-dark.webp b/web/public/workspace-active-cycles/cta-l-1-dark.webp new file mode 100644 index 00000000000..901f4b38dd2 Binary files /dev/null and b/web/public/workspace-active-cycles/cta-l-1-dark.webp differ diff --git a/web/public/workspace-active-cycles/cta-l-1-light.webp b/web/public/workspace-active-cycles/cta-l-1-light.webp new file mode 100644 index 00000000000..7cce304c183 Binary files /dev/null and b/web/public/workspace-active-cycles/cta-l-1-light.webp differ diff --git a/web/public/workspace-active-cycles/cta-r-1-dark.webp b/web/public/workspace-active-cycles/cta-r-1-dark.webp new file mode 100644 index 00000000000..3c3a32acf51 Binary files /dev/null and b/web/public/workspace-active-cycles/cta-r-1-dark.webp differ diff --git a/web/public/workspace-active-cycles/cta-r-1-light.webp b/web/public/workspace-active-cycles/cta-r-1-light.webp new file mode 100644 index 00000000000..4911a27b4f2 Binary files /dev/null and b/web/public/workspace-active-cycles/cta-r-1-light.webp differ diff --git a/web/public/workspace-active-cycles/cta-r-2-dark.webp b/web/public/workspace-active-cycles/cta-r-2-dark.webp new file mode 100644 index 00000000000..012b6ecee84 Binary files /dev/null and b/web/public/workspace-active-cycles/cta-r-2-dark.webp differ diff --git a/web/public/workspace-active-cycles/cta-r-2-light.webp b/web/public/workspace-active-cycles/cta-r-2-light.webp new file mode 100644 index 00000000000..971d28fcbaf Binary files /dev/null and b/web/public/workspace-active-cycles/cta-r-2-light.webp differ diff --git a/web/services/file.service.ts b/web/services/file.service.ts index 4085a7309d9..d5e80dd536b 100644 --- a/web/services/file.service.ts +++ b/web/services/file.service.ts @@ -78,6 +78,39 @@ export class FileService extends APIService { }; } + getDeleteImageFunction(workspaceId: string) { + return async (src: string) => { + try { + const assetUrlWithWorkspaceId = `${workspaceId}/${this.extractAssetIdFromUrl(src, workspaceId)}`; + const data = await this.deleteImage(assetUrlWithWorkspaceId); + return data; + } catch (e) { + console.error(e); + } + }; + } + + getRestoreImageFunction(workspaceId: string) { + return async (src: string) => { + try { + const assetUrlWithWorkspaceId = `${workspaceId}/${this.extractAssetIdFromUrl(src, workspaceId)}`; + const data = await this.restoreImage(assetUrlWithWorkspaceId); + return data; + } catch (e) { + console.error(e); + } + }; + } + + extractAssetIdFromUrl(src: string, workspaceId: string): string { + const indexWhereAssetIdStarts = src.indexOf(workspaceId) + workspaceId.length + 1; + if (indexWhereAssetIdStarts === -1) { + throw new Error("Workspace ID not found in source string"); + } + const assetUrl = src.substring(indexWhereAssetIdStarts); + return assetUrl; + } + async deleteImage(assetUrlWithWorkspaceId: string): Promise { return this.delete(`/api/workspaces/file-assets/${assetUrlWithWorkspaceId}/`) .then((response) => response?.status) diff --git a/web/services/inbox/inbox-issue.service.ts b/web/services/inbox/inbox-issue.service.ts new file mode 100644 index 00000000000..6b2099059fe --- /dev/null +++ b/web/services/inbox/inbox-issue.service.ts @@ -0,0 +1,112 @@ +import { APIService } from "services/api.service"; +// helpers +import { API_BASE_URL } from "helpers/common.helper"; +// types +import type { TInboxIssueFilterOptions, TInboxIssueExtendedDetail, TIssue, TInboxDetailedStatus } from "@plane/types"; + +export class InboxIssueService extends APIService { + constructor() { + super(API_BASE_URL); + } + + async fetchInboxIssues( + workspaceSlug: string, + projectId: string, + inboxId: string, + params?: TInboxIssueFilterOptions | {} + ): Promise { + return this.get( + `/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/inbox-issues/?expand=issue_inbox`, + { + params, + } + ) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async fetchInboxIssueById( + workspaceSlug: string, + projectId: string, + inboxId: string, + inboxIssueId: string + ): Promise { + return this.get( + `/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/inbox-issues/${inboxIssueId}/?expand=issue_inbox` + ) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async createInboxIssue( + workspaceSlug: string, + projectId: string, + inboxId: string, + data: { + source: string; + issue: Partial; + } + ): Promise { + return this.post( + `/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/inbox-issues/?expand=issue_inbox`, + data + ) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async updateInboxIssue( + workspaceSlug: string, + projectId: string, + inboxId: string, + inboxIssueId: string, + data: { issue: Partial } + ): Promise { + return this.patch( + `/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/inbox-issues/${inboxIssueId}/?expand=issue_inbox`, + data + ) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async removeInboxIssue( + workspaceSlug: string, + projectId: string, + inboxId: string, + inboxIssueId: string + ): Promise { + return this.delete( + `/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/inbox-issues/${inboxIssueId}/` + ) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async updateInboxIssueStatus( + workspaceSlug: string, + projectId: string, + inboxId: string, + inboxIssueId: string, + data: TInboxDetailedStatus + ): Promise { + return this.patch( + `/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/inbox-issues/${inboxIssueId}/?expand=issue_inbox`, + data + ) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } +} diff --git a/web/services/inbox/inbox.service.ts b/web/services/inbox/inbox.service.ts new file mode 100644 index 00000000000..8ee6ee51456 --- /dev/null +++ b/web/services/inbox/inbox.service.ts @@ -0,0 +1,35 @@ +import { APIService } from "services/api.service"; +// helpers +import { API_BASE_URL } from "helpers/common.helper"; +// types +import type { TInbox } from "@plane/types"; + +export class InboxService extends APIService { + constructor() { + super(API_BASE_URL); + } + + async fetchInboxes(workspaceSlug: string, projectId: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async fetchInboxById(workspaceSlug: string, projectId: string, inboxId: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async updateInbox(workspaceSlug: string, projectId: string, inboxId: string, data: Partial): Promise { + return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/`, data) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } +} diff --git a/web/services/inbox/index.ts b/web/services/inbox/index.ts new file mode 100644 index 00000000000..fe3f30fce4d --- /dev/null +++ b/web/services/inbox/index.ts @@ -0,0 +1,2 @@ +export * from "./inbox.service"; +export * from "./inbox-issue.service"; diff --git a/web/store/cycle.store.ts b/web/store/cycle.store.ts index cb0e76f33eb..ed5077385c5 100644 --- a/web/store/cycle.store.ts +++ b/web/store/cycle.store.ts @@ -14,8 +14,9 @@ import { CycleService } from "services/cycle.service"; export interface ICycleStore { //Loaders - fetchedMap: Record; + loader: boolean; // observables + fetchedMap: Record; cycleMap: Record; activeCycleIdMap: Record; // computed @@ -32,8 +33,8 @@ export interface ICycleStore { // actions validateDate: (workspaceSlug: string, projectId: string, payload: CycleDateCheckData) => Promise; // fetch - fetchAllCycles: (workspaceSlug: string, projectId: string) => Promise; - fetchActiveCycle: (workspaceSlug: string, projectId: string) => Promise; + fetchAllCycles: (workspaceSlug: string, projectId: string) => Promise; + fetchActiveCycle: (workspaceSlug: string, projectId: string) => Promise; fetchCycleDetails: (workspaceSlug: string, projectId: string, cycleId: string) => Promise; // crud createCycle: (workspaceSlug: string, projectId: string, data: Partial) => Promise; @@ -51,6 +52,7 @@ export interface ICycleStore { export class CycleStore implements ICycleStore { // observables + loader: boolean = false; cycleMap: Record = {}; activeCycleIdMap: Record = {}; //loaders @@ -65,6 +67,7 @@ export class CycleStore implements ICycleStore { constructor(_rootStore: RootStore) { makeObservable(this, { // observables + loader: observable.ref, cycleMap: observable, activeCycleIdMap: observable, fetchedMap: observable, @@ -221,16 +224,24 @@ export class CycleStore implements ICycleStore { * @param projectId * @returns */ - fetchAllCycles = async (workspaceSlug: string, projectId: string) => - await this.cycleService.getCyclesWithParams(workspaceSlug, projectId).then((response) => { - runInAction(() => { - response.forEach((cycle) => { - set(this.cycleMap, [cycle.id], cycle); + fetchAllCycles = async (workspaceSlug: string, projectId: string) => { + try { + this.loader = true; + await this.cycleService.getCyclesWithParams(workspaceSlug, projectId).then((response) => { + runInAction(() => { + response.forEach((cycle) => { + set(this.cycleMap, [cycle.id], cycle); + }); + set(this.fetchedMap, projectId, true); + this.loader = false; }); - set(this.fetchedMap, projectId, true); + return response; }); - return response; - }); + } catch (error) { + this.loader = false; + return undefined; + } + }; /** * @description fetches active cycle for a project diff --git a/web/store/estimate.store.ts b/web/store/estimate.store.ts index f34d2203258..beddd52aba6 100644 --- a/web/store/estimate.store.ts +++ b/web/store/estimate.store.ts @@ -18,12 +18,12 @@ export interface IEstimateStore { activeEstimateDetails: IEstimate | null; // computed actions areEstimatesEnabledForProject: (projectId: string) => boolean; - getEstimatePointValue: (estimateKey: number | null, projectId?: string) => string; + getEstimatePointValue: (estimateKey: number | null, projectId: string | null) => string; getProjectEstimateById: (estimateId: string) => IEstimate | null; getProjectActiveEstimateDetails: (projectId: string) => IEstimate | null; // fetch actions fetchProjectEstimates: (workspaceSlug: string, projectId: string) => Promise; - fetchWorskpaceEstimates: (workspaceSlug: string) => Promise; + fetchWorkspaceEstimates: (workspaceSlug: string) => Promise; // crud actions createEstimate: (workspaceSlug: string, projectId: string, data: IEstimateFormData) => Promise; updateEstimate: ( @@ -56,7 +56,7 @@ export class EstimateStore implements IEstimateStore { activeEstimateDetails: computed, // actions fetchProjectEstimates: action, - fetchWorskpaceEstimates: action, + fetchWorkspaceEstimates: action, createEstimate: action, updateEstimate: action, deleteEstimate: action, @@ -109,7 +109,7 @@ export class EstimateStore implements IEstimateStore { /** * @description returns the point value for the given estimate key to display in the UI */ - getEstimatePointValue = computedFn((estimateKey: number | null, projectId?: string) => { + getEstimatePointValue = computedFn((estimateKey: number | null, projectId: string | null) => { if (estimateKey === null) return "None"; const activeEstimate = projectId ? this.getProjectActiveEstimateDetails(projectId) : this.activeEstimateDetails; return activeEstimate?.points?.find((point) => point.key === estimateKey)?.value || "None"; @@ -158,7 +158,7 @@ export class EstimateStore implements IEstimateStore { * @param workspaceSlug * @param projectId */ - fetchWorskpaceEstimates = async (workspaceSlug: string) => + fetchWorkspaceEstimates = async (workspaceSlug: string) => await this.estimateService.getWorkspaceEstimatesList(workspaceSlug).then((response) => { runInAction(() => { response.forEach((estimate) => { diff --git a/web/store/inbox/inbox.store.ts b/web/store/inbox/inbox.store.ts index b4358c23676..8d8f2bec546 100644 --- a/web/store/inbox/inbox.store.ts +++ b/web/store/inbox/inbox.store.ts @@ -1,37 +1,32 @@ -import { observable, action, makeObservable, runInAction, computed } from "mobx"; +import { observable, action, makeObservable, runInAction } from "mobx"; import { computedFn } from "mobx-utils"; -import { set } from "lodash"; +import set from "lodash/set"; +import update from "lodash/update"; +import concat from "lodash/concat"; +import uniq from "lodash/uniq"; // services -import { InboxService } from "services/inbox.service"; +import { InboxService } from "services/inbox/inbox.service"; // types import { RootStore } from "store/root.store"; -import { IInbox } from "@plane/types"; +import { TInboxDetailMap, TInboxDetailIdMap, TInbox } from "@plane/types"; -export interface IInboxStore { +export interface IInbox { // observables - inboxesList: { - [projectId: string]: IInbox[]; - }; - inboxDetails: { - [inboxId: string]: IInbox; - }; - // computed - isInboxEnabled: boolean; - // computed actions - getInboxId: (projectId: string) => string | null; + inboxes: TInboxDetailIdMap; + inboxMap: TInboxDetailMap; + // helper methods + getInboxesByProjectId: (projectId: string) => string[] | undefined; + getInboxById: (inboxId: string) => TInbox | undefined; // fetch actions - fetchInboxesList: (workspaceSlug: string, projectId: string) => Promise; - fetchInboxDetails: (workspaceSlug: string, projectId: string, inboxId: string) => Promise; + fetchInboxes: (workspaceSlug: string, projectId: string) => Promise; + fetchInboxById: (workspaceSlug: string, projectId: string, inboxId: string) => Promise; + updateInbox: (workspaceSlug: string, projectId: string, inboxId: string, data: Partial) => Promise; } -export class InboxStore implements IInboxStore { +export class Inbox implements IInbox { // observables - inboxesList: { - [projectId: string]: IInbox[]; - } = {}; - inboxDetails: { - [inboxId: string]: IInbox; - } = {}; + inboxes: TInboxDetailIdMap = {}; + inboxMap: TInboxDetailMap = {}; // root store rootStore; // services @@ -40,68 +35,80 @@ export class InboxStore implements IInboxStore { constructor(_rootStore: RootStore) { makeObservable(this, { // observables - inboxesList: observable, - inboxDetails: observable, - // computed - isInboxEnabled: computed, + inboxMap: observable, + inboxes: observable, // actions - fetchInboxesList: action, + fetchInboxes: action, + fetchInboxById: action, + updateInbox: action, }); - // root store this.rootStore = _rootStore; // services this.inboxService = new InboxService(); } - /** - * Returns true if inbox is enabled for current project - */ - get isInboxEnabled() { - const projectId = this.rootStore.app.router.projectId; - if (!projectId) return false; - const projectDetails = this.rootStore.projectRoot.project.currentProjectDetails; - if (!projectDetails) return false; - return projectDetails.inbox_view; - } + // helper methods + getInboxesByProjectId = computedFn((projectId: string) => { + if (!projectId) return undefined; + return this.inboxes?.[projectId] ?? undefined; + }); - /** - * Returns the inbox Id belongs to a specific project - */ - getInboxId = computedFn((projectId: string) => { - const projectDetails = this.rootStore.projectRoot.project.getProjectById(projectId); - if (!projectDetails || !projectDetails.inbox_view) return null; - return this.inboxesList[projectId]?.[0]?.id ?? null; + getInboxById = computedFn((inboxId: string) => { + if (!inboxId) return undefined; + return this.inboxMap[inboxId] ?? undefined; }); - /** - * Fetches the inboxes list belongs to a specific project - * @param workspaceSlug - * @param projectId - * @returns Promise - */ - fetchInboxesList = async (workspaceSlug: string, projectId: string) => { - return await this.inboxService.getInboxes(workspaceSlug, projectId).then((inboxes) => { + // actions + fetchInboxes = async (workspaceSlug: string, projectId: string) => { + try { + const response = await this.inboxService.fetchInboxes(workspaceSlug, projectId); + + const _inboxIds = response.map((inbox) => inbox.id); runInAction(() => { - set(this.inboxesList, projectId, inboxes); + response.forEach((inbox) => { + set(this.inboxMap, inbox.id, inbox); + }); + set(this.inboxes, projectId, _inboxIds); }); - return inboxes; - }); + + return response; + } catch (error) { + throw error; + } }; - /** - * Fetches the inbox details belongs to a specific inbox - * @param workspaceSlug - * @param projectId - * @param inboxId - * @returns Promise - */ - fetchInboxDetails = async (workspaceSlug: string, projectId: string, inboxId: string) => { - return await this.inboxService.getInboxById(workspaceSlug, projectId, inboxId).then((inboxDetailsResponse) => { + fetchInboxById = async (workspaceSlug: string, projectId: string, inboxId: string) => { + try { + const response = await this.inboxService.fetchInboxById(workspaceSlug, projectId, inboxId); + runInAction(() => { - set(this.inboxDetails, inboxId, inboxDetailsResponse); + set(this.inboxMap, inboxId, response); + update(this.inboxes, projectId, (inboxIds: string[] = []) => { + if (inboxIds.includes(inboxId)) return inboxIds; + return uniq(concat(inboxIds, inboxId)); + }); }); - return inboxDetailsResponse; - }); + + return response; + } catch (error) { + throw error; + } + }; + + updateInbox = async (workspaceSlug: string, projectId: string, inboxId: string, data: Partial) => { + try { + const response = await this.inboxService.updateInbox(workspaceSlug, projectId, inboxId, data); + + runInAction(() => { + Object.keys(response).forEach((key) => { + set(this.inboxMap, [inboxId, key], response[key as keyof TInbox]); + }); + }); + + return response; + } catch (error) { + throw error; + } }; } diff --git a/web/store/inbox/inbox_filter.store.ts b/web/store/inbox/inbox_filter.store.ts index 33209c6c339..c4566acbe26 100644 --- a/web/store/inbox/inbox_filter.store.ts +++ b/web/store/inbox/inbox_filter.store.ts @@ -1,34 +1,31 @@ import { observable, action, makeObservable, runInAction, computed } from "mobx"; -import { set } from "lodash"; +import set from "lodash/set"; +import isEmpty from "lodash/isEmpty"; // services import { InboxService } from "services/inbox.service"; // types import { RootStore } from "store/root.store"; -import { IInbox, IInboxFilterOptions, IInboxQueryParams } from "@plane/types"; -import { EUserWorkspaceRoles } from "constants/workspace"; -import { EUserProjectRoles } from "constants/project"; +import { TInboxIssueFilterOptions, TInboxIssueFilters, TInboxIssueQueryParams, TInbox } from "@plane/types"; -export interface IInboxFiltersStore { +export interface IInboxFilter { // observables - inboxFilters: Record; + filters: Record; // inbox_id -> TInboxIssueFilters // computed - appliedFilters: IInboxQueryParams | null; - // fetch action - fetchInboxFilters: (workspaceSlug: string, projectId: string, inboxId: string) => Promise; - // update action + inboxFilters: TInboxIssueFilters | undefined; + inboxAppliedFilters: Partial> | undefined; + // actions + fetchInboxFilters: (workspaceSlug: string, projectId: string, inboxId: string) => Promise; updateInboxFilters: ( workspaceSlug: string, projectId: string, inboxId: string, - filters: Partial - ) => Promise; + filters: Partial + ) => Promise; } -export class InboxFiltersStore implements IInboxFiltersStore { +export class InboxFilter implements IInboxFilter { // observables - inboxFilters: { - [inboxId: string]: { filters: IInboxFilterOptions }; - } = {}; + filters: Record = {}; // root store rootStore; // services @@ -37,12 +34,12 @@ export class InboxFiltersStore implements IInboxFiltersStore { constructor(_rootStore: RootStore) { makeObservable(this, { // observables - inboxFilters: observable, + filters: observable, // computed - appliedFilters: computed, - // fetch action + inboxFilters: computed, + inboxAppliedFilters: computed, + // actions fetchInboxFilters: action, - // update action updateInboxFilters: action, }); // root store @@ -51,69 +48,82 @@ export class InboxFiltersStore implements IInboxFiltersStore { this.inboxService = new InboxService(); } - /** - * Returns applied filters to specific inbox - */ - get appliedFilters(): IInboxQueryParams | null { + get inboxFilters() { const inboxId = this.rootStore.app.router.inboxId; - if (!inboxId) return null; - const filtersList = this.inboxFilters[inboxId]?.filters; - if (!filtersList) return null; - const filteredRouteParams: IInboxQueryParams = { - priority: filtersList.priority ? filtersList.priority.join(",") : null, - inbox_status: filtersList.inbox_status ? filtersList.inbox_status.join(",") : null, + if (!inboxId) return undefined; + + const displayFilters = this.filters[inboxId] || undefined; + if (isEmpty(displayFilters)) return undefined; + + const _filters: TInboxIssueFilters = { + filters: { + priority: isEmpty(displayFilters?.filters?.priority) ? [] : displayFilters?.filters?.priority, + inbox_status: isEmpty(displayFilters?.filters?.inbox_status) ? [] : displayFilters?.filters?.inbox_status, + }, }; - return filteredRouteParams; + return _filters; + } + + get inboxAppliedFilters() { + const userFilters = this.inboxFilters; + if (!userFilters) return undefined; + + const filteredParams = { + priority: userFilters?.filters?.priority?.join(",") || undefined, + inbox_status: userFilters?.filters?.inbox_status?.join(",") || undefined, + }; + return filteredParams; } - /** - * Fetches filters of a specific inbox and adds it to the store - * @param workspaceSlug - * @param projectId - * @param inboxId - * @returns Promise - * - */ fetchInboxFilters = async (workspaceSlug: string, projectId: string, inboxId: string) => { - return await this.inboxService.getInboxById(workspaceSlug, projectId, inboxId).then((issuesResponse) => { + try { + const response = await this.rootStore.inbox.inbox.fetchInboxById(workspaceSlug, projectId, inboxId); + + const filters: TInboxIssueFilterOptions = { + priority: response?.view_props?.filters?.priority || [], + inbox_status: response?.view_props?.filters?.inbox_status || [], + }; + runInAction(() => { - set(this.inboxFilters, [inboxId], issuesResponse.view_props); + set(this.filters, [inboxId], { filters: filters }); }); - return issuesResponse; - }); + + return response; + } catch (error) { + throw error; + } }; - /** - * Updates filters of a specific inbox and updates it in the store - * @param workspaceSlug - * @param projectId - * @param inboxId - * @param filters - * @returns Promise - * - */ updateInboxFilters = async ( workspaceSlug: string, projectId: string, inboxId: string, - filters: Partial + filters: Partial ) => { - const newViewProps = { - ...this.inboxFilters[inboxId], - filters: { - ...this.inboxFilters[inboxId]?.filters, - ...filters, - }, - }; - const userRole = this.rootStore.user.membership?.currentProjectRole || EUserProjectRoles.GUEST; - if (userRole > EUserWorkspaceRoles.VIEWER) - await this.inboxService - .patchInbox(workspaceSlug, projectId, inboxId, { view_props: newViewProps }) - .then((response) => { - runInAction(() => { - set(this.inboxFilters, [inboxId], newViewProps); - }); - return response; + try { + runInAction(() => { + Object.keys(filters).forEach((_key) => { + const _filterKey = _key as keyof TInboxIssueFilterOptions; + set(this.filters, [inboxId, "filters", _key], filters[_filterKey]); }); + }); + + const inboxFilters = this.inboxFilters; + let _filters: TInboxIssueFilterOptions = { + priority: inboxFilters?.filters?.priority || [], + inbox_status: inboxFilters?.filters?.inbox_status || [], + }; + _filters = { ..._filters, ...filters }; + + this.rootStore.inbox.inboxIssue.fetchInboxIssues(workspaceSlug, projectId, inboxId, "mutation"); + + const response = await this.rootStore.inbox.inbox.updateInbox(workspaceSlug, projectId, inboxId, { + view_props: { filters: _filters }, + }); + + return response; + } catch (error) { + throw error; + } }; } diff --git a/web/store/inbox/inbox_issue.store.ts b/web/store/inbox/inbox_issue.store.ts index 54dd2fbf3ec..2fedb73dc1b 100644 --- a/web/store/inbox/inbox_issue.store.ts +++ b/web/store/inbox/inbox_issue.store.ts @@ -1,244 +1,297 @@ -import { observable, action, makeObservable, runInAction, autorun, computed } from "mobx"; +import { observable, action, makeObservable, runInAction } from "mobx"; import { computedFn } from "mobx-utils"; -import { set } from "lodash"; +import set from "lodash/set"; +import update from "lodash/update"; +import concat from "lodash/concat"; +import uniq from "lodash/uniq"; +import pull from "lodash/pull"; // services -import { InboxService } from "services/inbox.service"; +import { InboxIssueService } from "services/inbox/inbox-issue.service"; // types import { RootStore } from "store/root.store"; -import { IInboxIssue, TIssue, TInboxStatus } from "@plane/types"; -// constants -import { INBOX_ISSUE_SOURCE } from "constants/inbox"; +import type { + TInboxIssueDetailIdMap, + TInboxIssueDetailMap, + TInboxIssueDetail, + TInboxIssueExtendedDetail, + TInboxDetailedStatus, + TIssue, +} from "@plane/types"; -export interface IInboxIssuesStore { +type TLoader = "init-loader" | "mutation" | undefined; + +export interface IInboxIssue { // observables - issueMap: Record>; // {inboxId: {issueId: IInboxIssue}} - // computed - currentInboxIssueIds: string[] | null; - // computed actions - getIssueById: (inboxId: string, issueId: string) => IInboxIssue | null; - // fetch actions - fetchIssues: (workspaceSlug: string, projectId: string, inboxId: string) => Promise; - fetchIssueDetails: ( + loader: TLoader; + inboxIssues: TInboxIssueDetailIdMap; + inboxIssueMap: TInboxIssueDetailMap; + // helper methods + getInboxIssuesByInboxId: (inboxId: string) => string[] | undefined; + getInboxIssueByIssueId: (inboxId: string, issueId: string) => TInboxIssueDetail | undefined; + // actions + fetchInboxIssues: ( workspaceSlug: string, projectId: string, inboxId: string, - issueId: string - ) => Promise; - // CRUD actions - createIssue: ( + loaderType?: TLoader + ) => Promise; + fetchInboxIssueById: ( workspaceSlug: string, projectId: string, inboxId: string, - data: Partial - ) => Promise; - updateIssue: ( + inboxIssueId: string + ) => Promise; + createInboxIssue: ( workspaceSlug: string, projectId: string, inboxId: string, - issueId: string, - data: Partial - ) => Promise; - updateIssueStatus: ( + data: Partial + ) => Promise; + updateInboxIssue: ( workspaceSlug: string, projectId: string, inboxId: string, - issueId: string, - data: TInboxStatus - ) => Promise; - deleteIssue: (workspaceSlug: string, projectId: string, inboxId: string, issueId: string) => Promise; + inboxIssueId: string, + data: Partial + ) => Promise; + removeInboxIssue: (workspaceSlug: string, projectId: string, inboxId: string, issueId: string) => Promise; + updateInboxIssueStatus: ( + workspaceSlug: string, + projectId: string, + inboxId: string, + inboxIssueId: string, + data: TInboxDetailedStatus + ) => Promise; } -export class InboxIssuesStore implements IInboxIssuesStore { +export class InboxIssue implements IInboxIssue { // observables - issueMap: { [inboxId: string]: Record } = {}; + loader: TLoader = "init-loader"; + inboxIssues: TInboxIssueDetailIdMap = {}; + inboxIssueMap: TInboxIssueDetailMap = {}; // root store rootStore; // services - inboxService; + inboxIssueService; constructor(_rootStore: RootStore) { makeObservable(this, { // observables - issueMap: observable, - // computed - currentInboxIssueIds: computed, - // fetch actions - fetchIssues: action, - fetchIssueDetails: action, - // CRUD actions - createIssue: action, - updateIssue: action, - updateIssueStatus: action, - deleteIssue: action, + loader: observable.ref, + inboxIssues: observable, + inboxIssueMap: observable, + // actions + fetchInboxIssues: action, + fetchInboxIssueById: action, + createInboxIssue: action, + updateInboxIssue: action, + removeInboxIssue: action, + updateInboxIssueStatus: action, }); // root store this.rootStore = _rootStore; // services - this.inboxService = new InboxService(); - autorun(() => { - const routerStore = this.rootStore.app.router; - const workspaceSlug = routerStore?.workspaceSlug; - const projectId = routerStore?.projectId; - const inboxId = routerStore?.inboxId; - if (workspaceSlug && projectId && inboxId && this.rootStore.inboxRoot.inboxFilters.inboxFilters[inboxId]) - this.fetchIssues(workspaceSlug, projectId, inboxId); - }); + this.inboxIssueService = new InboxIssueService(); } - /** - * Returns the issue IDs belong to a specific inbox issues list - */ - get currentInboxIssueIds() { - const inboxId = this.rootStore.app.router.inboxId; - if (!inboxId) return null; - return Object.keys(this.issueMap?.[inboxId] ?? {}) ?? null; - } + // helper methods + getInboxIssuesByInboxId = computedFn((inboxId: string) => { + if (!inboxId) return undefined; + return this.inboxIssues?.[inboxId] ?? undefined; + }); + + getInboxIssueByIssueId = computedFn((inboxId: string, issueId: string) => { + if (!inboxId) return undefined; + return this.inboxIssueMap?.[inboxId]?.[issueId] ?? undefined; + }); + + // actions + fetchInboxIssues = async ( + workspaceSlug: string, + projectId: string, + inboxId: string, + loaderType: TLoader = "init-loader" + ) => { + try { + this.loader = loaderType; + const queryParams = this.rootStore.inbox.inboxFilter.inboxAppliedFilters ?? {}; + + const response = await this.inboxIssueService.fetchInboxIssues(workspaceSlug, projectId, inboxId, queryParams); - /** - * Returns the issue details belongs to a specific inbox issue - */ - getIssueById = computedFn( - (inboxId: string, issueId: string): IInboxIssue | null => this.issueMap?.[inboxId]?.[issueId] ?? null - ); - - /** - * Fetches issues of a specific inbox and adds it to the store - * @param workspaceSlug - * @param projectId - * @param inboxId - * @returns Promise - */ - fetchIssues = async (workspaceSlug: string, projectId: string, inboxId: string) => { - const queryParams = this.rootStore.inboxRoot.inboxFilters.appliedFilters ?? undefined; - return await this.inboxService - .getInboxIssues(workspaceSlug, projectId, inboxId, queryParams) - .then((issuesResponse) => { - runInAction(() => { - issuesResponse.forEach((issue) => { - set(this.issueMap, [inboxId, issue.issue_inbox?.[0].id], issue); - }); + runInAction(() => { + response.forEach((_inboxIssue) => { + const { ["issue_inbox"]: issueInboxDetail, ...issue } = _inboxIssue; + this.rootStore.inbox.rootStore.issue.issues.addIssue([issue]); + const { ["id"]: omittedId, ...inboxIssue } = issueInboxDetail[0]; + set(this.inboxIssueMap, [inboxId, _inboxIssue.id], inboxIssue); }); - return issuesResponse; }); + + const _inboxIssueIds = response.map((inboxIssue) => inboxIssue.id); + runInAction(() => { + set(this.inboxIssues, inboxId, _inboxIssueIds); + this.loader = undefined; + }); + + return response; + } catch (error) { + this.loader = undefined; + throw error; + } }; - /** - * Fetches issue details of a specific inbox issue and updates it to the store - * @param workspaceSlug - * @param projectId - * @param inboxId - * @param issueId - * returns Promise - */ - fetchIssueDetails = async (workspaceSlug: string, projectId: string, inboxId: string, issueId: string) => { - return await this.inboxService - .getInboxIssueById(workspaceSlug, projectId, inboxId, issueId) - .then((issueResponse) => { - runInAction(() => { - set(this.issueMap, [inboxId, issueId], issueResponse); + fetchInboxIssueById = async (workspaceSlug: string, projectId: string, inboxId: string, inboxIssueId: string) => { + try { + const response = await this.inboxIssueService.fetchInboxIssueById( + workspaceSlug, + projectId, + inboxId, + inboxIssueId + ); + + runInAction(() => { + const { ["issue_inbox"]: issueInboxDetail, ...issue } = response; + this.rootStore.inbox.rootStore.issue.issues.updateIssue(issue.id, issue); + const { ["id"]: omittedId, ...inboxIssue } = issueInboxDetail[0]; + set(this.inboxIssueMap, [inboxId, response.id], inboxIssue); + }); + + runInAction(() => { + update(this.inboxIssues, inboxId, (inboxIssueIds: string[] = []) => { + if (inboxIssueIds.includes(response.id)) return inboxIssueIds; + return uniq(concat(inboxIssueIds, response.id)); }); - return issueResponse; }); + + // fetching issue activity + await this.rootStore.issue.issueDetail.fetchActivities(workspaceSlug, projectId, inboxIssueId); + // fetching issue reaction + await this.rootStore.issue.issueDetail.fetchReactions(workspaceSlug, projectId, inboxIssueId); + return response as any; + } catch (error) { + throw error; + } }; - /** - * Creates a new issue for a specific inbox and add it to the store - * @param workspaceSlug - * @param projectId - * @param inboxId - * @param data - * @returns Promise - */ - createIssue = async (workspaceSlug: string, projectId: string, inboxId: string, data: Partial) => { - const payload = { - issue: { - name: data.name, - // description: data.description, - description_html: data.description_html, - priority: data.priority, - }, - source: INBOX_ISSUE_SOURCE, - }; - return await this.inboxService.createInboxIssue(workspaceSlug, projectId, inboxId, payload).then((response) => { + createInboxIssue = async (workspaceSlug: string, projectId: string, inboxId: string, data: Partial) => { + try { + const response = await this.inboxIssueService.createInboxIssue(workspaceSlug, projectId, inboxId, { + source: "in-app", + issue: data, + }); + runInAction(() => { - set(this.issueMap, [inboxId, response.issue_inbox?.[0].id], response); + const { ["issue_inbox"]: issueInboxDetail, ...issue } = response; + this.rootStore.inbox.rootStore.issue.issues.addIssue([issue]); + const { ["id"]: omittedId, ...inboxIssue } = issueInboxDetail[0]; + set(this.inboxIssueMap, [inboxId, response.id], inboxIssue); + update(this.rootStore.inbox.inbox.inboxMap, [inboxId, "pending_issue_count"], (count: number = 0) => count + 1); }); + + runInAction(() => { + update(this.inboxIssues, inboxId, (inboxIssueIds: string[] = []) => { + if (inboxIssueIds.includes(response.id)) return inboxIssueIds; + return uniq(concat(inboxIssueIds, response.id)); + }); + }); + + await this.rootStore.issue.issueDetail.fetchActivities(workspaceSlug, projectId, response.id); return response; - }); + } catch (error) { + throw error; + } }; - /** - * Updates an issue for a specific inbox and update it in the store - * @param workspaceSlug - * @param projectId - * @param inboxId - * @param issueId - * @param data - * @returns Promise - */ - updateIssue = async ( + updateInboxIssue = async ( workspaceSlug: string, projectId: string, inboxId: string, - issueId: string, - data: Partial + inboxIssueId: string, + data: Partial ) => { - const issueDetails = this.rootStore.inboxRoot.inboxIssues.getIssueById(inboxId, issueId); - return await this.inboxService - .patchInboxIssue(workspaceSlug, projectId, inboxId, issueId, { issue: data }) - .then((issueResponse) => { - runInAction(() => { - set(this.issueMap, [inboxId, issueId], { - ...issueDetails, - ...issueResponse, - }); + try { + const response = await this.inboxIssueService.updateInboxIssue(workspaceSlug, projectId, inboxId, inboxIssueId, { + issue: data, + }); + + runInAction(() => { + const { ["issue_inbox"]: issueInboxDetail, ...issue } = response; + this.rootStore.inbox.rootStore.issue.issues.updateIssue(issue.id, issue); + const { ["id"]: omittedId, ...inboxIssue } = issueInboxDetail[0]; + set(this.inboxIssueMap, [inboxId, response.id], inboxIssue); + }); + + runInAction(() => { + update(this.inboxIssues, inboxId, (inboxIssueIds: string[] = []) => { + if (inboxIssueIds.includes(response.id)) return inboxIssueIds; + return uniq(concat(inboxIssueIds, response.id)); }); - return issueResponse; }); + + await this.rootStore.issue.issueDetail.fetchActivities(workspaceSlug, projectId, inboxIssueId); + return response as any; + } catch (error) { + throw error; + } + }; + + removeInboxIssue = async (workspaceSlug: string, projectId: string, inboxId: string, inboxIssueId: string) => { + try { + const response = await this.inboxIssueService.removeInboxIssue(workspaceSlug, projectId, inboxId, inboxIssueId); + + runInAction(() => { + pull(this.inboxIssues[inboxId], inboxIssueId); + delete this.inboxIssueMap[inboxId][inboxIssueId]; + this.rootStore.inbox.rootStore.issue.issues.removeIssue(inboxIssueId); + update(this.rootStore.inbox.inbox.inboxMap, [inboxId, "pending_issue_count"], (count: number = 0) => count - 1); + }); + + await this.rootStore.issue.issueDetail.fetchActivities(workspaceSlug, projectId, inboxIssueId); + return response as any; + } catch (error) { + throw error; + } }; - /** - * Updates an issue status for a specific inbox issue and update it in the store - * @param workspaceSlug - * @param projectId - * @param inboxId - * @param issueId - * @param data - * @returns Promise - */ - updateIssueStatus = async ( + updateInboxIssueStatus = async ( workspaceSlug: string, projectId: string, inboxId: string, - issueId: string, - data: TInboxStatus + inboxIssueId: string, + data: TInboxDetailedStatus ) => { - const issueDetails = this.rootStore.inboxRoot.inboxIssues.getIssueById(inboxId, issueId); - await this.inboxService.markInboxStatus(workspaceSlug, projectId, inboxId, issueId, data).then((response) => { + try { + const response = await this.inboxIssueService.updateInboxIssueStatus( + workspaceSlug, + projectId, + inboxId, + inboxIssueId, + data + ); + + const pendingStatus = -2; runInAction(() => { - set(this.issueMap, [inboxId, issueId, "issue_inbox", 0], { - ...issueDetails?.issue_inbox?.[0], - ...response?.issue_inbox?.[0], - }); + const { ["issue_inbox"]: issueInboxDetail, ...issue } = response; + this.rootStore.inbox.rootStore.issue.issues.addIssue([issue]); + const { ["id"]: omittedId, ...inboxIssue } = issueInboxDetail[0]; + set(this.inboxIssueMap, [inboxId, response.id], inboxIssue); + update(this.rootStore.inbox.inbox.inboxMap, [inboxId, "pending_issue_count"], (count: number = 0) => + data.status === pendingStatus ? count + 1 : count - 1 + ); }); - return response; - }); - }; - /** - * Deletes an issue for a specific inbox and removes it from the store - * @param workspaceSlug - * @param projectId - * @param inboxId - * @param issueId - * @returns Promise - */ - deleteIssue = async (workspaceSlug: string, projectId: string, inboxId: string, issueId: string) => { - await this.inboxService.deleteInboxIssue(workspaceSlug, projectId, inboxId, issueId).then((_) => { runInAction(() => { - delete this.issueMap?.[inboxId]?.[issueId]; + update(this.inboxIssues, inboxId, (inboxIssueIds: string[] = []) => { + if (inboxIssueIds.includes(response.id)) return inboxIssueIds; + return uniq(concat(inboxIssueIds, response.id)); + }); }); - }); + + await this.rootStore.issue.issueDetail.fetchActivities(workspaceSlug, projectId, inboxIssueId); + return response as any; + } catch (error) { + throw error; + } }; } diff --git a/web/store/inbox/index.ts b/web/store/inbox/index.ts deleted file mode 100644 index 07fd86a9a90..00000000000 --- a/web/store/inbox/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { makeAutoObservable } from "mobx"; -// types -import { RootStore } from "store/root.store"; -import { IInboxIssuesStore, InboxIssuesStore } from "./inbox_issue.store"; -import { IInboxFiltersStore, InboxFiltersStore } from "./inbox_filter.store"; -import { IInboxStore, InboxStore } from "./inbox.store"; - -export interface IInboxRootStore { - inbox: IInboxStore; - inboxFilters: IInboxFiltersStore; - inboxIssues: IInboxIssuesStore; -} - -export class InboxRootStore implements IInboxRootStore { - inbox: IInboxStore; - inboxFilters: IInboxFiltersStore; - inboxIssues: IInboxIssuesStore; - - constructor(_rootStore: RootStore) { - makeAutoObservable(this, {}); - this.inbox = new InboxStore(_rootStore); - this.inboxFilters = new InboxFiltersStore(_rootStore); - this.inboxIssues = new InboxIssuesStore(_rootStore); - } -} diff --git a/web/store/inbox/root.store.ts b/web/store/inbox/root.store.ts new file mode 100644 index 00000000000..b0706cca710 --- /dev/null +++ b/web/store/inbox/root.store.ts @@ -0,0 +1,26 @@ +// types +import { RootStore } from "store/root.store"; +import { IInbox, Inbox } from "./inbox.store"; +import { IInboxIssue, InboxIssue } from "./inbox_issue.store"; +import { IInboxFilter, InboxFilter } from "./inbox_filter.store"; + +export interface IInboxRootStore { + rootStore: RootStore; + inbox: IInbox; + inboxIssue: IInboxIssue; + inboxFilter: IInboxFilter; +} + +export class InboxRootStore implements IInboxRootStore { + rootStore: RootStore; + inbox: IInbox; + inboxIssue: IInboxIssue; + inboxFilter: IInboxFilter; + + constructor(_rootStore: RootStore) { + this.rootStore = _rootStore; + this.inbox = new Inbox(_rootStore); + this.inboxIssue = new InboxIssue(_rootStore); + this.inboxFilter = new InboxFilter(_rootStore); + } +} diff --git a/web/store/issue/archived/filter.store.ts b/web/store/issue/archived/filter.store.ts index fe4d686427d..9a9c91a3780 100644 --- a/web/store/issue/archived/filter.store.ts +++ b/web/store/issue/archived/filter.store.ts @@ -90,6 +90,7 @@ export class ArchivedIssuesFilter extends IssueFilterHelperStore implements IArc ); if (userFilters?.displayFilters?.layout === "gantt_chart") filteredRouteParams.start_target_date = true; + if (userFilters?.displayFilters?.layout === "spreadsheet") filteredRouteParams.sub_issue = false; return filteredRouteParams; } @@ -183,6 +184,11 @@ export class ArchivedIssuesFilter extends IssueFilterHelperStore implements IArc _filters.displayFilters.group_by = "state"; updatedDisplayFilters.group_by = "state"; } + // set sub_issue to false if layout is switched to spreadsheet and sub_issue is true + if (_filters.displayFilters.layout === "spreadsheet" && _filters.displayFilters.sub_issue === true) { + _filters.displayFilters.sub_issue = false; + updatedDisplayFilters.sub_issue = false; + } runInAction(() => { Object.keys(updatedDisplayFilters).forEach((_key) => { @@ -194,6 +200,9 @@ export class ArchivedIssuesFilter extends IssueFilterHelperStore implements IArc }); }); + if (this.requiresServerUpdate(updatedDisplayFilters)) + this.rootIssueStore.archivedIssues.fetchIssues(workspaceSlug, projectId, "mutation"); + this.handleIssuesLocalFilters.set(EIssuesStoreType.ARCHIVED, type, workspaceSlug, projectId, undefined, { display_filters: _filters.displayFilters, }); diff --git a/web/store/issue/archived/issue.store.ts b/web/store/issue/archived/issue.store.ts index 55eb2c20a49..a31cdfef106 100644 --- a/web/store/issue/archived/issue.store.ts +++ b/web/store/issue/archived/issue.store.ts @@ -90,11 +90,15 @@ export class ArchivedIssues extends IssueHelperStore implements IArchivedIssues const response = await this.archivedIssueService.getArchivedIssues(workspaceSlug, projectId, params); runInAction(() => { - set(this.issues, [projectId], Object.keys(response)); + set( + this.issues, + [projectId], + response.map((issue: TIssue) => issue.id) + ); this.loader = undefined; }); - this.rootIssueStore.issues.addIssue(Object.values(response)); + this.rootIssueStore.issues.addIssue(response); return response; } catch (error) { @@ -124,8 +128,8 @@ export class ArchivedIssues extends IssueHelperStore implements IArchivedIssues try { const response = await this.archivedIssueService.unarchiveIssue(workspaceSlug, projectId, issueId); - const issueIndex = this.issues[projectId].findIndex((_issueId) => _issueId === issueId); - if (issueIndex >= 0) + const issueIndex = this.issues[projectId]?.findIndex((_issueId) => _issueId === issueId); + if (issueIndex && issueIndex >= 0) runInAction(() => { this.issues[projectId].splice(issueIndex, 1); }); diff --git a/web/store/issue/cycle/filter.store.ts b/web/store/issue/cycle/filter.store.ts index e6ea3dde1a9..27347536bb7 100644 --- a/web/store/issue/cycle/filter.store.ts +++ b/web/store/issue/cycle/filter.store.ts @@ -91,6 +91,7 @@ export class CycleIssuesFilter extends IssueFilterHelperStore implements ICycleI ); if (userFilters?.displayFilters?.layout === "gantt_chart") filteredRouteParams.start_target_date = true; + if (userFilters?.displayFilters?.layout === "spreadsheet") filteredRouteParams.sub_issue = false; return filteredRouteParams; } @@ -195,6 +196,12 @@ export class CycleIssuesFilter extends IssueFilterHelperStore implements ICycleI updatedDisplayFilters.group_by = "state"; } + // set sub_issue to false if layout is switched to spreadsheet and sub_issue is true + if (_filters.displayFilters.layout === "spreadsheet" && _filters.displayFilters.sub_issue === true) { + _filters.displayFilters.sub_issue = false; + updatedDisplayFilters.sub_issue = false; + } + runInAction(() => { Object.keys(updatedDisplayFilters).forEach((_key) => { set( @@ -205,6 +212,9 @@ export class CycleIssuesFilter extends IssueFilterHelperStore implements ICycleI }); }); + if (this.requiresServerUpdate(updatedDisplayFilters)) + this.rootIssueStore.cycleIssues.fetchIssues(workspaceSlug, projectId, "mutation", cycleId); + await this.issueFilterService.patchCycleIssueFilters(workspaceSlug, projectId, cycleId, { display_filters: _filters.displayFilters, }); diff --git a/web/store/issue/cycle/issue.store.ts b/web/store/issue/cycle/issue.store.ts index 5519aa045b4..33cd06d4d64 100644 --- a/web/store/issue/cycle/issue.store.ts +++ b/web/store/issue/cycle/issue.store.ts @@ -3,6 +3,7 @@ import set from "lodash/set"; import update from "lodash/update"; import concat from "lodash/concat"; import pull from "lodash/pull"; +import uniq from "lodash/uniq"; // base class import { IssueHelperStore } from "../helpers/issue-helper.store"; // services @@ -117,12 +118,13 @@ export class CycleIssues extends IssueHelperStore implements ICycleIssues { const orderBy = displayFilters?.order_by; const layout = displayFilters?.layout; - const cycleIssueIds = this.issues[cycleId] ?? []; + const cycleIssueIds = this.issues[cycleId]; + if (!cycleIssueIds) return; const _issues = this.rootIssueStore.issues.getIssuesByIds(cycleIssueIds); - if (!_issues) return undefined; + if (!_issues) return []; - let issues: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues | undefined = undefined; + let issues: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues = []; if (layout === "list" && orderBy) { if (groupBy) issues = this.groupedIssues(groupBy, orderBy, _issues); @@ -164,7 +166,6 @@ export class CycleIssues extends IssueHelperStore implements ICycleIssues { return response; } catch (error) { - console.log(error); this.loader = undefined; throw error; } @@ -266,11 +267,13 @@ export class CycleIssues extends IssueHelperStore implements ICycleIssues { }); runInAction(() => { - update(this.issues, cycleId, (cycleIssueIds) => { - if (!cycleIssueIds) return [...issueIds]; - else return concat(cycleIssueIds, [...issueIds]); + update(this.issues, cycleId, (cycleIssueIds = []) => { + uniq(concat(cycleIssueIds, issueIds)); }); }); + issueIds.forEach((issueId) => { + this.rootStore.issues.updateIssue(issueId, { cycle_id: cycleId }); + }); return issueToCycle; } catch (error) { @@ -331,7 +334,6 @@ export class CycleIssues extends IssueHelperStore implements ICycleIssues { return response; } catch (error) { - console.log(error); this.loader = undefined; throw error; } diff --git a/web/store/issue/draft/filter.store.ts b/web/store/issue/draft/filter.store.ts index 658a2dd244d..7096040d559 100644 --- a/web/store/issue/draft/filter.store.ts +++ b/web/store/issue/draft/filter.store.ts @@ -90,6 +90,7 @@ export class DraftIssuesFilter extends IssueFilterHelperStore implements IDraftI ); if (userFilters?.displayFilters?.layout === "gantt_chart") filteredRouteParams.start_target_date = true; + if (userFilters?.displayFilters?.layout === "spreadsheet") filteredRouteParams.sub_issue = false; return filteredRouteParams; } @@ -179,6 +180,12 @@ export class DraftIssuesFilter extends IssueFilterHelperStore implements IDraftI updatedDisplayFilters.group_by = "state"; } + // set sub_issue to false if layout is switched to spreadsheet and sub_issue is true + if (_filters.displayFilters.layout === "spreadsheet" && _filters.displayFilters.sub_issue === true) { + _filters.displayFilters.sub_issue = false; + updatedDisplayFilters.sub_issue = false; + } + runInAction(() => { Object.keys(updatedDisplayFilters).forEach((_key) => { set( @@ -189,6 +196,9 @@ export class DraftIssuesFilter extends IssueFilterHelperStore implements IDraftI }); }); + if (this.requiresServerUpdate(updatedDisplayFilters)) + this.rootIssueStore.draftIssues.fetchIssues(workspaceSlug, projectId, "mutation"); + this.handleIssuesLocalFilters.set(EIssuesStoreType.DRAFT, type, workspaceSlug, projectId, undefined, { display_filters: _filters.displayFilters, }); diff --git a/web/store/issue/helpers/issue-filter-helper.store.ts b/web/store/issue/helpers/issue-filter-helper.store.ts index 2474d60197e..03bb1bc30d9 100644 --- a/web/store/issue/helpers/issue-filter-helper.store.ts +++ b/web/store/issue/helpers/issue-filter-helper.store.ts @@ -183,6 +183,20 @@ export class IssueFilterHelperStore implements IIssueFilterHelperStore { updated_on: displayProperties?.updated_on ?? true, }); + /** + * This Method returns true if the display properties changed requires a server side update + * @param displayFilters + * @returns + */ + requiresServerUpdate = (displayFilters: IIssueDisplayFilterOptions) => { + const SERVER_DISPLAY_FILTERS = ["sub_issue", "type"]; + const displayFilterKeys = Object.keys(displayFilters); + + return SERVER_DISPLAY_FILTERS.some((serverDisplayfilter: string) => + displayFilterKeys.includes(serverDisplayfilter) + ); + }; + handleIssuesLocalFilters = { fetchFiltersFromStorage: () => { const _filters = storage.get("issue_local_filters"); diff --git a/web/store/issue/issue-details/issue.store.ts b/web/store/issue/issue-details/issue.store.ts index bc34af0f610..be687eab824 100644 --- a/web/store/issue/issue-details/issue.store.ts +++ b/web/store/issue/issue-details/issue.store.ts @@ -1,13 +1,13 @@ import { makeObservable } from "mobx"; // services -import { IssueService } from "services/issue"; +import { IssueArchiveService, IssueService } from "services/issue"; // types import { IIssueDetail } from "./root.store"; import { TIssue } from "@plane/types"; export interface IIssueStoreActions { // actions - fetchIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise; + fetchIssue: (workspaceSlug: string, projectId: string, issueId: string, isArchived?: boolean) => Promise; updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial) => Promise; removeIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise; addIssueToCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => Promise; @@ -31,6 +31,7 @@ export class IssueStore implements IIssueStore { rootIssueDetailStore: IIssueDetail; // services issueService; + issueArchiveService; constructor(rootStore: IIssueDetail) { makeObservable(this, {}); @@ -38,6 +39,7 @@ export class IssueStore implements IIssueStore { this.rootIssueDetailStore = rootStore; // services this.issueService = new IssueService(); + this.issueArchiveService = new IssueArchiveService(); } // helper methods @@ -47,12 +49,17 @@ export class IssueStore implements IIssueStore { }; // actions - fetchIssue = async (workspaceSlug: string, projectId: string, issueId: string) => { + fetchIssue = async (workspaceSlug: string, projectId: string, issueId: string, isArchived = false) => { try { const query = { expand: "state,assignees,labels,parent", }; - const issue = (await this.issueService.retrieve(workspaceSlug, projectId, issueId, query)) as any; + + let issue: any; + + if (isArchived) issue = await this.issueArchiveService.retrieveArchivedIssue(workspaceSlug, projectId, issueId); + else issue = await this.issueService.retrieve(workspaceSlug, projectId, issueId, query); + if (!issue) throw new Error("Issue not found"); this.rootIssueDetailStore.rootIssueStore.issues.addIssue([issue]); @@ -90,6 +97,7 @@ export class IssueStore implements IIssueStore { this.rootIssueDetailStore.relation.fetchRelations(workspaceSlug, projectId, issueId); // fetching states + // TODO: check if this function is required this.rootIssueDetailStore.rootIssueStore.state.fetchProjectStates(workspaceSlug, projectId); return issue; diff --git a/web/store/issue/issue-details/root.store.ts b/web/store/issue/issue-details/root.store.ts index 0fedd99bbe7..9feb728c714 100644 --- a/web/store/issue/issue-details/root.store.ts +++ b/web/store/issue/issue-details/root.store.ts @@ -40,6 +40,7 @@ export interface IIssueDetail isIssueLinkModalOpen: boolean; isParentIssueModalOpen: boolean; isDeleteIssueModalOpen: boolean; + isRelationModalOpen: TIssueRelationTypes | null; // computed isAnyModalOpen: boolean; // actions @@ -47,6 +48,7 @@ export interface IIssueDetail toggleIssueLinkModal: (value: boolean) => void; toggleParentIssueModal: (value: boolean) => void; toggleDeleteIssueModal: (value: boolean) => void; + toggleRelationModal: (value: TIssueRelationTypes | null) => void; // store rootIssueStore: IIssueRootStore; issue: IIssueStore; @@ -67,6 +69,7 @@ export class IssueDetail implements IIssueDetail { isIssueLinkModalOpen: boolean = false; isParentIssueModalOpen: boolean = false; isDeleteIssueModalOpen: boolean = false; + isRelationModalOpen: TIssueRelationTypes | null = null; // store rootIssueStore: IIssueRootStore; issue: IIssueStore; @@ -87,6 +90,7 @@ export class IssueDetail implements IIssueDetail { isIssueLinkModalOpen: observable.ref, isParentIssueModalOpen: observable.ref, isDeleteIssueModalOpen: observable.ref, + isRelationModalOpen: observable.ref, // computed isAnyModalOpen: computed, // action @@ -94,6 +98,7 @@ export class IssueDetail implements IIssueDetail { toggleIssueLinkModal: action, toggleParentIssueModal: action, toggleDeleteIssueModal: action, + toggleRelationModal: action, }); // store @@ -112,7 +117,12 @@ export class IssueDetail implements IIssueDetail { // computed get isAnyModalOpen() { - return this.isIssueLinkModalOpen || this.isParentIssueModalOpen || this.isDeleteIssueModalOpen; + return ( + this.isIssueLinkModalOpen || + this.isParentIssueModalOpen || + this.isDeleteIssueModalOpen || + Boolean(this.isRelationModalOpen) + ); } // actions @@ -120,10 +130,11 @@ export class IssueDetail implements IIssueDetail { toggleIssueLinkModal = (value: boolean) => (this.isIssueLinkModalOpen = value); toggleParentIssueModal = (value: boolean) => (this.isParentIssueModalOpen = value); toggleDeleteIssueModal = (value: boolean) => (this.isDeleteIssueModalOpen = value); + toggleRelationModal = (value: TIssueRelationTypes | null) => (this.isRelationModalOpen = value); // issue - fetchIssue = async (workspaceSlug: string, projectId: string, issueId: string) => - this.issue.fetchIssue(workspaceSlug, projectId, issueId); + fetchIssue = async (workspaceSlug: string, projectId: string, issueId: string, isArchived = false) => + this.issue.fetchIssue(workspaceSlug, projectId, issueId, isArchived); updateIssue = async (workspaceSlug: string, projectId: string, issueId: string, data: Partial) => this.issue.updateIssue(workspaceSlug, projectId, issueId, data); removeIssue = async (workspaceSlug: string, projectId: string, issueId: string) => diff --git a/web/store/issue/issue.store.ts b/web/store/issue/issue.store.ts index f31c8f2f660..8ee689daf33 100644 --- a/web/store/issue/issue.store.ts +++ b/web/store/issue/issue.store.ts @@ -43,7 +43,7 @@ export class IssueStore implements IIssueStore { if (issues && issues.length <= 0) return; runInAction(() => { issues.forEach((issue) => { - set(this.issuesMap, issue.id, issue); + if (!this.issuesMap[issue.id]) set(this.issuesMap, issue.id, issue); }); }); }; diff --git a/web/store/issue/issue_kanban_view.store.ts b/web/store/issue/issue_kanban_view.store.ts index 9476a129d34..fa25038387d 100644 --- a/web/store/issue/issue_kanban_view.store.ts +++ b/web/store/issue/issue_kanban_view.store.ts @@ -9,7 +9,7 @@ export interface IIssueKanBanViewStore { subgroupByIssuesVisibility: string[]; }; // computed - getCanUserDragDrop: (order_by: string | null, group_by: string | null, sub_group_by?: string | null) => boolean; + getCanUserDragDrop: (group_by: string | null, sub_group_by: string | null) => boolean; canUserDragDropVertically: boolean; canUserDragDropHorizontally: boolean; // actions @@ -38,7 +38,7 @@ export class IssueKanBanViewStore implements IIssueKanBanViewStore { this.rootStore = _rootStore; } - getCanUserDragDrop = computedFn((group_by: string | null, sub_group_by?: string | null) => { + getCanUserDragDrop = computedFn((group_by: string | null, sub_group_by: string | null) => { if (group_by && ["state", "priority"].includes(group_by)) { if (!sub_group_by) return true; if (sub_group_by && ["state", "priority"].includes(sub_group_by)) return true; diff --git a/web/store/issue/module/filter.store.ts b/web/store/issue/module/filter.store.ts index 1e00dc8c0b8..3c309cecda0 100644 --- a/web/store/issue/module/filter.store.ts +++ b/web/store/issue/module/filter.store.ts @@ -91,6 +91,7 @@ export class ModuleIssuesFilter extends IssueFilterHelperStore implements IModul ); if (userFilters?.displayFilters?.layout === "gantt_chart") filteredRouteParams.start_target_date = true; + if (userFilters?.displayFilters?.layout === "spreadsheet") filteredRouteParams.sub_issue = false; return filteredRouteParams; } @@ -194,6 +195,12 @@ export class ModuleIssuesFilter extends IssueFilterHelperStore implements IModul updatedDisplayFilters.group_by = "state"; } + // set sub_issue to false if layout is switched to spreadsheet and sub_issue is true + if (_filters.displayFilters.layout === "spreadsheet" && _filters.displayFilters.sub_issue === true) { + _filters.displayFilters.sub_issue = false; + updatedDisplayFilters.sub_issue = false; + } + runInAction(() => { Object.keys(updatedDisplayFilters).forEach((_key) => { set( @@ -204,6 +211,9 @@ export class ModuleIssuesFilter extends IssueFilterHelperStore implements IModul }); }); + if (this.requiresServerUpdate(updatedDisplayFilters)) + this.rootIssueStore.moduleIssues.fetchIssues(workspaceSlug, projectId, "mutation", moduleId); + await this.issueFilterService.patchModuleIssueFilters(workspaceSlug, projectId, moduleId, { display_filters: _filters.displayFilters, }); diff --git a/web/store/issue/module/issue.store.ts b/web/store/issue/module/issue.store.ts index 4a51bdc0a21..e24f03fb6a5 100644 --- a/web/store/issue/module/issue.store.ts +++ b/web/store/issue/module/issue.store.ts @@ -3,6 +3,7 @@ import set from "lodash/set"; import update from "lodash/update"; import concat from "lodash/concat"; import pull from "lodash/pull"; +import uniq from "lodash/uniq"; // base class import { IssueHelperStore } from "../helpers/issue-helper.store"; // services @@ -110,12 +111,13 @@ export class ModuleIssues extends IssueHelperStore implements IModuleIssues { const orderBy = displayFilters?.order_by; const layout = displayFilters?.layout; - const moduleIssueIds = this.issues[moduleId] ?? []; + const moduleIssueIds = this.issues[moduleId]; + if (!moduleIssueIds) return; const _issues = this.rootIssueStore.issues.getIssuesByIds(moduleIssueIds); - if (!_issues) return undefined; + if (!_issues) return []; - let issues: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues | undefined = undefined; + let issues: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues = []; if (layout === "list" && orderBy) { if (groupBy) issues = this.groupedIssues(groupBy, orderBy, _issues); @@ -258,11 +260,13 @@ export class ModuleIssues extends IssueHelperStore implements IModuleIssues { }); runInAction(() => { - update(this.issues, moduleId, (moduleIssueIds) => { - if (!moduleIssueIds) return [...issueIds]; - else return concat(moduleIssueIds, [...issueIds]); + update(this.issues, moduleId, (moduleIssueIds = []) => { + uniq(concat(moduleIssueIds, issueIds)); }); }); + issueIds.forEach((issueId) => { + this.rootStore.issues.updateIssue(issueId, { module_id: moduleId }); + }); return issueToModule; } catch (error) { diff --git a/web/store/issue/profile/filter.store.ts b/web/store/issue/profile/filter.store.ts index 3ec5aed6f5d..a0f8028f844 100644 --- a/web/store/issue/profile/filter.store.ts +++ b/web/store/issue/profile/filter.store.ts @@ -94,6 +94,7 @@ export class ProfileIssuesFilter extends IssueFilterHelperStore implements IProf ); if (userFilters?.displayFilters?.layout === "gantt_chart") filteredRouteParams.start_target_date = true; + if (userFilters?.displayFilters?.layout === "spreadsheet") filteredRouteParams.sub_issue = false; return filteredRouteParams; } @@ -188,6 +189,11 @@ export class ProfileIssuesFilter extends IssueFilterHelperStore implements IProf _filters.displayFilters.group_by = "priority"; updatedDisplayFilters.group_by = "priority"; } + // set sub_issue to false if layout is switched to spreadsheet and sub_issue is true + if (_filters.displayFilters.layout === "spreadsheet" && _filters.displayFilters.sub_issue === true) { + _filters.displayFilters.sub_issue = false; + updatedDisplayFilters.sub_issue = false; + } runInAction(() => { Object.keys(updatedDisplayFilters).forEach((_key) => { @@ -199,6 +205,15 @@ export class ProfileIssuesFilter extends IssueFilterHelperStore implements IProf }); }); + if (this.requiresServerUpdate(updatedDisplayFilters)) + this.rootIssueStore.profileIssues.fetchIssues( + workspaceSlug, + undefined, + "mutation", + userId, + this.rootIssueStore.profileIssues.currentView + ); + this.handleIssuesLocalFilters.set(EIssuesStoreType.PROFILE, type, workspaceSlug, userId, undefined, { display_filters: _filters.displayFilters, }); diff --git a/web/store/issue/project-views/filter.store.ts b/web/store/issue/project-views/filter.store.ts index 828eca29a16..e0dae761ce8 100644 --- a/web/store/issue/project-views/filter.store.ts +++ b/web/store/issue/project-views/filter.store.ts @@ -91,6 +91,7 @@ export class ProjectViewIssuesFilter extends IssueFilterHelperStore implements I ); if (userFilters?.displayFilters?.layout === "gantt_chart") filteredRouteParams.start_target_date = true; + if (userFilters?.displayFilters?.layout === "spreadsheet") filteredRouteParams.sub_issue = false; return filteredRouteParams; } @@ -192,6 +193,11 @@ export class ProjectViewIssuesFilter extends IssueFilterHelperStore implements I _filters.displayFilters.group_by = "state"; updatedDisplayFilters.group_by = "state"; } + // set sub_issue to false if layout is switched to spreadsheet and sub_issue is true + if (_filters.displayFilters.layout === "spreadsheet" && _filters.displayFilters.sub_issue === true) { + _filters.displayFilters.sub_issue = false; + updatedDisplayFilters.sub_issue = false; + } runInAction(() => { Object.keys(updatedDisplayFilters).forEach((_key) => { @@ -203,6 +209,9 @@ export class ProjectViewIssuesFilter extends IssueFilterHelperStore implements I }); }); + if (this.requiresServerUpdate(updatedDisplayFilters)) + this.rootIssueStore.projectViewIssues.fetchIssues(workspaceSlug, projectId, "mutation", viewId); + await this.issueFilterService.patchView(workspaceSlug, projectId, viewId, { display_filters: _filters.displayFilters, }); diff --git a/web/store/issue/project-views/issue.store.ts b/web/store/issue/project-views/issue.store.ts index 1908b8a10e9..d643999c931 100644 --- a/web/store/issue/project-views/issue.store.ts +++ b/web/store/issue/project-views/issue.store.ts @@ -95,12 +95,13 @@ export class ProjectViewIssues extends IssueHelperStore implements IProjectViewI const orderBy = displayFilters?.order_by; const layout = displayFilters?.layout; - const viewIssueIds = this.issues[viewId] ?? []; + const viewIssueIds = this.issues[viewId]; + if (!viewIssueIds) return; const _issues = this.rootStore.issues.getIssuesByIds(viewIssueIds); - if (!_issues) return undefined; + if (!_issues) return []; - let issues: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues | undefined = undefined; + let issues: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues = []; if (layout === "list" && orderBy) { if (groupBy) issues = this.groupedIssues(groupBy, orderBy, _issues); diff --git a/web/store/issue/project/filter.store.ts b/web/store/issue/project/filter.store.ts index aaa8d81d94b..392b7203f55 100644 --- a/web/store/issue/project/filter.store.ts +++ b/web/store/issue/project/filter.store.ts @@ -90,6 +90,7 @@ export class ProjectIssuesFilter extends IssueFilterHelperStore implements IProj ); if (userFilters?.displayFilters?.layout === "gantt_chart") filteredRouteParams.start_target_date = true; + if (userFilters?.displayFilters?.layout === "spreadsheet") filteredRouteParams.sub_issue = false; return filteredRouteParams; } @@ -191,6 +192,12 @@ export class ProjectIssuesFilter extends IssueFilterHelperStore implements IProj updatedDisplayFilters.group_by = "state"; } + // set sub_issue to false if layout is switched to spreadsheet and sub_issue is true + if (_filters.displayFilters.layout === "spreadsheet" && _filters.displayFilters.sub_issue === true) { + _filters.displayFilters.sub_issue = false; + updatedDisplayFilters.sub_issue = false; + } + runInAction(() => { Object.keys(updatedDisplayFilters).forEach((_key) => { set( @@ -201,6 +208,9 @@ export class ProjectIssuesFilter extends IssueFilterHelperStore implements IProj }); }); + if (this.requiresServerUpdate(updatedDisplayFilters)) + this.rootIssueStore.projectIssues.fetchIssues(workspaceSlug, projectId, "mutation"); + await this.issueFilterService.patchProjectIssueFilters(workspaceSlug, projectId, { display_filters: _filters.displayFilters, }); diff --git a/web/store/issue/project/issue.store.ts b/web/store/issue/project/issue.store.ts index 8982789437e..2000a440a10 100644 --- a/web/store/issue/project/issue.store.ts +++ b/web/store/issue/project/issue.store.ts @@ -24,6 +24,7 @@ export interface IProjectIssues { updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial) => Promise; removeIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise; quickAddIssue: (workspaceSlug: string, projectId: string, data: TIssue) => Promise; + removeBulkIssues: (workspaceSlug: string, projectId: string, issueIds: string[]) => Promise; } export class ProjectIssues extends IssueHelperStore implements IProjectIssues { @@ -53,6 +54,7 @@ export class ProjectIssues extends IssueHelperStore implements IProjectIssues { createIssue: action, updateIssue: action, removeIssue: action, + removeBulkIssues: action, quickAddIssue: action, }); // root store @@ -73,12 +75,13 @@ export class ProjectIssues extends IssueHelperStore implements IProjectIssues { const orderBy = displayFilters?.order_by; const layout = displayFilters?.layout; - const projectIssueIds = this.issues[projectId] ?? []; + const projectIssueIds = this.issues[projectId]; + if (!projectIssueIds) return; const _issues = this.rootStore.issues.getIssuesByIds(projectIssueIds); - if (!_issues) return undefined; + if (!_issues) return []; - let issues: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues | undefined = undefined; + let issues: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues = []; if (layout === "list" && orderBy) { if (groupBy) issues = this.groupedIssues(groupBy, orderBy, _issues); @@ -185,4 +188,22 @@ export class ProjectIssues extends IssueHelperStore implements IProjectIssues { throw error; } }; + + removeBulkIssues = async (workspaceSlug: string, projectId: string, issueIds: string[]) => { + try { + runInAction(() => { + issueIds.forEach((issueId) => { + pull(this.issues[projectId], issueId); + this.rootStore.issues.removeIssue(issueId); + }); + }); + + const response = await this.issueService.bulkDeleteIssues(workspaceSlug, projectId, { issue_ids: issueIds }); + + return response; + } catch (error) { + this.fetchIssues(workspaceSlug, projectId, "mutation"); + throw error; + } + }; } diff --git a/web/store/issue/workspace/filter.store.ts b/web/store/issue/workspace/filter.store.ts index 95a9a88310f..34907bd9ba4 100644 --- a/web/store/issue/workspace/filter.store.ts +++ b/web/store/issue/workspace/filter.store.ts @@ -100,6 +100,7 @@ export class WorkspaceIssuesFilter extends IssueFilterHelperStore implements IWo ); if (userFilters?.displayFilters?.layout === "gantt_chart") filteredRouteParams.start_target_date = true; + if (userFilters?.displayFilters?.layout === "spreadsheet") filteredRouteParams.sub_issue = false; return filteredRouteParams; }; @@ -162,14 +163,15 @@ export class WorkspaceIssuesFilter extends IssueFilterHelperStore implements IWo ) => { try { if (!viewId) throw new Error("View id is required"); + const issueFilters = this.getIssueFilters(viewId); - if (isEmpty(this.filters) || isEmpty(this.filters[viewId]) || isEmpty(filters)) return; + if (!issueFilters || isEmpty(filters)) return; const _filters = { - filters: this.filters[viewId].filters as IIssueFilterOptions, - displayFilters: this.filters[viewId].displayFilters as IIssueDisplayFilterOptions, - displayProperties: this.filters[viewId].displayProperties as IIssueDisplayProperties, - kanbanFilters: this.filters[viewId].kanbanFilters as TIssueKanbanFilters, + filters: issueFilters.filters as IIssueFilterOptions, + displayFilters: issueFilters.displayFilters as IIssueDisplayFilterOptions, + displayProperties: issueFilters.displayProperties as IIssueDisplayProperties, + kanbanFilters: issueFilters.kanbanFilters as TIssueKanbanFilters, }; switch (type) { @@ -212,6 +214,11 @@ export class WorkspaceIssuesFilter extends IssueFilterHelperStore implements IWo _filters.displayFilters.group_by = "state"; updatedDisplayFilters.group_by = "state"; } + // set sub_issue to false if layout is switched to spreadsheet and sub_issue is true + if (_filters.displayFilters.layout === "spreadsheet" && _filters.displayFilters.sub_issue === true) { + _filters.displayFilters.sub_issue = false; + updatedDisplayFilters.sub_issue = false; + } runInAction(() => { Object.keys(updatedDisplayFilters).forEach((_key) => { @@ -222,6 +229,10 @@ export class WorkspaceIssuesFilter extends IssueFilterHelperStore implements IWo ); }); }); + + if (this.requiresServerUpdate(updatedDisplayFilters)) + this.rootIssueStore.workspaceIssues.fetchIssues(workspaceSlug, viewId, "mutation"); + if (["all-issues", "assigned", "created", "subscribed"].includes(viewId)) this.handleIssuesLocalFilters.set(EIssuesStoreType.GLOBAL, type, workspaceSlug, undefined, viewId, { display_filters: _filters.displayFilters, diff --git a/web/store/module.store.ts b/web/store/module.store.ts index ff864310a25..f0f576cbcf0 100644 --- a/web/store/module.store.ts +++ b/web/store/module.store.ts @@ -11,6 +11,7 @@ import { RootStore } from "store/root.store"; export interface IModuleStore { //Loaders + loader: boolean; fetchedMap: Record; // observables moduleMap: Record; @@ -21,7 +22,7 @@ export interface IModuleStore { getProjectModuleIds: (projectId: string) => string[] | null; // actions // fetch - fetchModules: (workspaceSlug: string, projectId: string) => Promise; + fetchModules: (workspaceSlug: string, projectId: string) => Promise; fetchModuleDetails: (workspaceSlug: string, projectId: string, moduleId: string) => Promise; // crud createModule: (workspaceSlug: string, projectId: string, data: Partial) => Promise; @@ -53,6 +54,7 @@ export interface IModuleStore { export class ModulesStore implements IModuleStore { // observables + loader: boolean = false; moduleMap: Record = {}; //loaders fetchedMap: Record = {}; @@ -65,6 +67,7 @@ export class ModulesStore implements IModuleStore { constructor(_rootStore: RootStore) { makeObservable(this, { // observables + loader: observable.ref, moduleMap: observable, fetchedMap: observable, // computed @@ -128,16 +131,24 @@ export class ModulesStore implements IModuleStore { * @param projectId * @returns IModule[] */ - fetchModules = async (workspaceSlug: string, projectId: string) => - await this.moduleService.getModules(workspaceSlug, projectId).then((response) => { - runInAction(() => { - response.forEach((module) => { - set(this.moduleMap, [module.id], { ...this.moduleMap[module.id], ...module }); + fetchModules = async (workspaceSlug: string, projectId: string) => { + try { + this.loader = true; + await this.moduleService.getModules(workspaceSlug, projectId).then((response) => { + runInAction(() => { + response.forEach((module) => { + set(this.moduleMap, [module.id], { ...this.moduleMap[module.id], ...module }); + }); + set(this.fetchedMap, projectId, true); + this.loader = false; }); - set(this.fetchedMap, projectId, true); + return response; }); - return response; - }); + } catch (error) { + this.loader = false; + return undefined; + } + }; /** * @description fetch module details diff --git a/web/store/project-page.store.ts b/web/store/project-page.store.ts index f2e3f92279d..072605bc34f 100644 --- a/web/store/project-page.store.ts +++ b/web/store/project-page.store.ts @@ -10,6 +10,8 @@ import { RootStore } from "./root.store"; import { isThisWeek, isToday, isYesterday } from "date-fns"; export interface IProjectPageStore { + loader: boolean; + archivedPageLoader: boolean; projectPageMap: Record>; projectArchivedPageMap: Record>; @@ -30,6 +32,8 @@ export interface IProjectPageStore { } export class ProjectPageStore implements IProjectPageStore { + loader: boolean = false; + archivedPageLoader: boolean = false; projectPageMap: Record> = {}; // { projectId: [page1, page2] } projectArchivedPageMap: Record> = {}; // { projectId: [page1, page2] } @@ -39,6 +43,8 @@ export class ProjectPageStore implements IProjectPageStore { pageService; constructor(_rootStore: RootStore) { makeObservable(this, { + loader: observable.ref, + archivedPageLoader: observable.ref, projectPageMap: observable, projectArchivedPageMap: observable, @@ -152,15 +158,19 @@ export class ProjectPageStore implements IProjectPageStore { */ fetchProjectPages = async (workspaceSlug: string, projectId: string) => { try { + this.loader = true; await this.pageService.getProjectPages(workspaceSlug, projectId).then((response) => { runInAction(() => { for (const page of response) { set(this.projectPageMap, [projectId, page.id], new PageStore(page, this.rootStore)); } + this.loader = false; }); return response; }); } catch (e) { + this.loader = false; + throw e; } }; @@ -173,15 +183,18 @@ export class ProjectPageStore implements IProjectPageStore { */ fetchArchivedProjectPages = async (workspaceSlug: string, projectId: string) => { try { + this.archivedPageLoader = true; await this.pageService.getArchivedPages(workspaceSlug, projectId).then((response) => { runInAction(() => { for (const page of response) { set(this.projectArchivedPageMap, [projectId, page.id], new PageStore(page, this.rootStore)); } + this.archivedPageLoader = false; }); return response; }); } catch (e) { + this.archivedPageLoader = false; throw e; } }; diff --git a/web/store/project-view.store.ts b/web/store/project-view.store.ts index 39778f9b781..dd5c4a034ac 100644 --- a/web/store/project-view.store.ts +++ b/web/store/project-view.store.ts @@ -9,6 +9,7 @@ import { IProjectView } from "@plane/types"; export interface IProjectViewStore { //Loaders + loader: boolean; fetchedMap: Record; // observables viewMap: Record; @@ -17,7 +18,7 @@ export interface IProjectViewStore { // computed actions getViewById: (viewId: string) => IProjectView; // fetch actions - fetchViews: (workspaceSlug: string, projectId: string) => Promise; + fetchViews: (workspaceSlug: string, projectId: string) => Promise; fetchViewDetails: (workspaceSlug: string, projectId: string, viewId: string) => Promise; // CRUD actions createView: (workspaceSlug: string, projectId: string, data: Partial) => Promise; @@ -35,6 +36,7 @@ export interface IProjectViewStore { export class ProjectViewStore implements IProjectViewStore { // observables + loader: boolean = false; viewMap: Record = {}; //loaders fetchedMap: Record = {}; @@ -46,6 +48,7 @@ export class ProjectViewStore implements IProjectViewStore { constructor(_rootStore: RootStore) { makeObservable(this, { // observables + loader: observable.ref, viewMap: observable, fetchedMap: observable, // computed @@ -88,16 +91,24 @@ export class ProjectViewStore implements IProjectViewStore { * @param projectId * @returns Promise */ - fetchViews = async (workspaceSlug: string, projectId: string) => - await this.viewService.getViews(workspaceSlug, projectId).then((response) => { - runInAction(() => { - response.forEach((view) => { - set(this.viewMap, [view.id], view); + fetchViews = async (workspaceSlug: string, projectId: string) => { + try { + this.loader = true; + await this.viewService.getViews(workspaceSlug, projectId).then((response) => { + runInAction(() => { + response.forEach((view) => { + set(this.viewMap, [view.id], view); + }); + set(this.fetchedMap, projectId, true); + this.loader = false; }); - set(this.fetchedMap, projectId, true); + return response; }); - return response; - }); + } catch (error) { + this.loader = false; + return undefined; + } + }; /** * Fetches view details for a specific view diff --git a/web/store/root.store.ts b/web/store/root.store.ts index 138041c62ad..b3aeeea0407 100644 --- a/web/store/root.store.ts +++ b/web/store/root.store.ts @@ -8,9 +8,9 @@ import { IModuleStore, ModulesStore } from "./module.store"; import { IUserRootStore, UserRootStore } from "./user"; import { IWorkspaceRootStore, WorkspaceRootStore } from "./workspace"; import { IssueRootStore, IIssueRootStore } from "./issue/root.store"; +import { IInboxRootStore, InboxRootStore } from "./inbox/root.store"; import { IStateStore, StateStore } from "./state.store"; import { IMemberRootStore, MemberRootStore } from "./member"; -import { IInboxRootStore, InboxRootStore } from "./inbox"; import { IEstimateStore, EstimateStore } from "./estimate.store"; import { GlobalViewStore, IGlobalViewStore } from "./global-view.store"; import { IMentionStore, MentionStore } from "./mention.store"; @@ -26,12 +26,12 @@ export class RootStore { workspaceRoot: IWorkspaceRootStore; projectRoot: IProjectRootStore; memberRoot: IMemberRootStore; - inboxRoot: IInboxRootStore; cycle: ICycleStore; module: IModuleStore; projectView: IProjectViewStore; globalView: IGlobalViewStore; issue: IIssueRootStore; + inbox: IInboxRootStore; state: IStateStore; label: ILabelStore; estimate: IEstimateStore; @@ -45,13 +45,32 @@ export class RootStore { this.workspaceRoot = new WorkspaceRootStore(this); this.projectRoot = new ProjectRootStore(this); this.memberRoot = new MemberRootStore(this); - this.inboxRoot = new InboxRootStore(this); // independent stores this.cycle = new CycleStore(this); this.module = new ModulesStore(this); this.projectView = new ProjectViewStore(this); this.globalView = new GlobalViewStore(this); this.issue = new IssueRootStore(this); + this.inbox = new InboxRootStore(this); + this.state = new StateStore(this); + this.label = new LabelStore(this); + this.estimate = new EstimateStore(this); + this.mention = new MentionStore(this); + this.projectPages = new ProjectPageStore(this); + this.dashboard = new DashboardStore(this); + } + + resetOnSignout() { + this.workspaceRoot = new WorkspaceRootStore(this); + this.projectRoot = new ProjectRootStore(this); + this.memberRoot = new MemberRootStore(this); + // independent stores + this.cycle = new CycleStore(this); + this.module = new ModulesStore(this); + this.projectView = new ProjectViewStore(this); + this.globalView = new GlobalViewStore(this); + this.issue = new IssueRootStore(this); + this.inbox = new InboxRootStore(this); this.state = new StateStore(this); this.label = new LabelStore(this); this.estimate = new EstimateStore(this); diff --git a/web/store/user/index.ts b/web/store/user/index.ts index d3a16c3fc9d..b07764a05ca 100644 --- a/web/store/user/index.ts +++ b/web/store/user/index.ts @@ -249,6 +249,8 @@ export class UserRootStore implements IUserRootStore { this.currentUserError = null; this.isUserLoggedIn = false; }); + this.membership = new UserMembershipStore(this.rootStore); + this.rootStore.resetOnSignout(); }); /** @@ -261,5 +263,7 @@ export class UserRootStore implements IUserRootStore { this.currentUser = null; this.isUserLoggedIn = false; }); + this.membership = new UserMembershipStore(this.rootStore); + this.rootStore.resetOnSignout(); }); }