From 550fe547e25d2247aba7e95d4c842dcce45ada00 Mon Sep 17 00:00:00 2001 From: Dheeraj Kumar Ketireddy Date: Tue, 29 Apr 2025 13:51:46 +0530 Subject: [PATCH 001/201] [WEB-3967] feat: Optimized module patch endpoint to reduce duplicate db calls (#6983) --- apiserver/plane/app/views/module/base.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/apiserver/plane/app/views/module/base.py b/apiserver/plane/app/views/module/base.py index 62840f555..829f7a6b6 100644 --- a/apiserver/plane/app/views/module/base.py +++ b/apiserver/plane/app/views/module/base.py @@ -710,23 +710,31 @@ class ModuleViewSet(BaseViewSet): @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) def partial_update(self, request, slug, project_id, pk): - module = self.get_queryset().filter(pk=pk) + module_queryset = self.get_queryset().filter(pk=pk) - if module.first().archived_at: + current_module = module_queryset.first() + + if not current_module: + return Response( + {"error": "Module not found"}, + status=status.HTTP_404_NOT_FOUND, + ) + + if current_module.archived_at: return Response( {"error": "Archived module cannot be updated"}, status=status.HTTP_400_BAD_REQUEST, ) current_instance = json.dumps( - ModuleSerializer(module.first()).data, cls=DjangoJSONEncoder + ModuleSerializer(current_module).data, cls=DjangoJSONEncoder ) serializer = ModuleWriteSerializer( - module.first(), data=request.data, partial=True + current_module, data=request.data, partial=True ) if serializer.is_valid(): serializer.save() - module = module.values( + module = module_queryset.values( # Required fields "id", "workspace_id", From 190300bc6c572da38a432315415101f066bb2f25 Mon Sep 17 00:00:00 2001 From: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com> Date: Tue, 29 Apr 2025 14:00:54 +0530 Subject: [PATCH 002/201] [WEB-3877] chore: changed the logic to end cycle (#6971) * chore: changed the logic to end cycle * chore: added issue deleted filter * chore: added check for progress snapshot --- apiserver/plane/api/serializers/cycle.py | 6 - apiserver/plane/api/views/cycle.py | 19 ++- apiserver/plane/app/serializers/cycle.py | 6 - apiserver/plane/app/views/cycle/base.py | 151 +++++++++++-------- apiserver/plane/app/views/workspace/cycle.py | 1 + apiserver/plane/utils/timezone_converter.py | 8 +- 6 files changed, 106 insertions(+), 85 deletions(-) diff --git a/apiserver/plane/api/serializers/cycle.py b/apiserver/plane/api/serializers/cycle.py index ba22e25f9..7a78b6664 100644 --- a/apiserver/plane/api/serializers/cycle.py +++ b/apiserver/plane/api/serializers/cycle.py @@ -48,11 +48,6 @@ class CycleSerializer(BaseSerializer): if not project_id: raise serializers.ValidationError("Project ID is required") - is_start_date_end_date_equal = ( - True - if str(data.get("start_date")) == str(data.get("end_date")) - else False - ) data["start_date"] = convert_to_utc( date=str(data.get("start_date").date()), project_id=project_id, @@ -61,7 +56,6 @@ class CycleSerializer(BaseSerializer): data["end_date"] = convert_to_utc( date=str(data.get("end_date", None).date()), project_id=project_id, - is_start_date_end_date_equal=is_start_date_end_date_equal, ) return data diff --git a/apiserver/plane/api/views/cycle.py b/apiserver/plane/api/views/cycle.py index 3e27ffdc4..9005821f3 100644 --- a/apiserver/plane/api/views/cycle.py +++ b/apiserver/plane/api/views/cycle.py @@ -788,6 +788,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView): issue_cycle__issue__archived_at__isnull=True, issue_cycle__issue__is_draft=False, issue_cycle__deleted_at__isnull=True, + issue_cycle__issue__deleted_at__isnull=True, ), ) ) @@ -799,6 +800,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView): issue_cycle__issue__archived_at__isnull=True, issue_cycle__issue__is_draft=False, issue_cycle__deleted_at__isnull=True, + issue_cycle__issue__deleted_at__isnull=True, ), ) ) @@ -847,6 +849,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView): ) ) ) + old_cycle = old_cycle.first() estimate_type = Project.objects.filter( workspace__slug=slug, @@ -966,7 +969,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView): ) estimate_completion_chart = burndown_plot( - queryset=old_cycle.first(), + queryset=old_cycle, slug=slug, project_id=project_id, plot_type="points", @@ -1114,7 +1117,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView): # Pass the new_cycle queryset to burndown_plot completion_chart = burndown_plot( - queryset=old_cycle.first(), + queryset=old_cycle, slug=slug, project_id=project_id, plot_type="issues", @@ -1126,12 +1129,12 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView): ).first() current_cycle.progress_snapshot = { - "total_issues": old_cycle.first().total_issues, - "completed_issues": old_cycle.first().completed_issues, - "cancelled_issues": old_cycle.first().cancelled_issues, - "started_issues": old_cycle.first().started_issues, - "unstarted_issues": old_cycle.first().unstarted_issues, - "backlog_issues": old_cycle.first().backlog_issues, + "total_issues": old_cycle.total_issues, + "completed_issues": old_cycle.completed_issues, + "cancelled_issues": old_cycle.cancelled_issues, + "started_issues": old_cycle.started_issues, + "unstarted_issues": old_cycle.unstarted_issues, + "backlog_issues": old_cycle.backlog_issues, "distribution": { "labels": label_distribution_data, "assignees": assignee_distribution_data, diff --git a/apiserver/plane/app/serializers/cycle.py b/apiserver/plane/app/serializers/cycle.py index b56b08350..b3b69e375 100644 --- a/apiserver/plane/app/serializers/cycle.py +++ b/apiserver/plane/app/serializers/cycle.py @@ -25,11 +25,6 @@ class CycleWriteSerializer(BaseSerializer): or (self.instance and self.instance.project_id) or self.context.get("project_id", None) ) - is_start_date_end_date_equal = ( - True - if str(data.get("start_date")) == str(data.get("end_date")) - else False - ) data["start_date"] = convert_to_utc( date=str(data.get("start_date").date()), project_id=project_id, @@ -38,7 +33,6 @@ class CycleWriteSerializer(BaseSerializer): data["end_date"] = convert_to_utc( date=str(data.get("end_date", None).date()), project_id=project_id, - is_start_date_end_date_equal=is_start_date_end_date_equal, ) return data diff --git a/apiserver/plane/app/views/cycle/base.py b/apiserver/plane/app/views/cycle/base.py index e88acaf82..60b051b40 100644 --- a/apiserver/plane/app/views/cycle/base.py +++ b/apiserver/plane/app/views/cycle/base.py @@ -117,6 +117,7 @@ class CycleViewSet(BaseViewSet): issue_cycle__issue__archived_at__isnull=True, issue_cycle__issue__is_draft=False, issue_cycle__deleted_at__isnull=True, + issue_cycle__issue__deleted_at__isnull=True, ), ) ) @@ -129,6 +130,7 @@ class CycleViewSet(BaseViewSet): issue_cycle__issue__archived_at__isnull=True, issue_cycle__issue__is_draft=False, issue_cycle__deleted_at__isnull=True, + issue_cycle__issue__deleted_at__isnull=True, ), ) ) @@ -141,6 +143,7 @@ class CycleViewSet(BaseViewSet): issue_cycle__issue__archived_at__isnull=True, issue_cycle__issue__is_draft=False, issue_cycle__deleted_at__isnull=True, + issue_cycle__issue__deleted_at__isnull=True, ), ) ) @@ -266,9 +269,7 @@ class CycleViewSet(BaseViewSet): "created_by", ) datetime_fields = ["start_date", "end_date"] - data = user_timezone_converter( - data, datetime_fields, project_timezone - ) + data = user_timezone_converter(data, datetime_fields, project_timezone) return Response(data, status=status.HTTP_200_OK) @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) @@ -415,9 +416,7 @@ class CycleViewSet(BaseViewSet): project_timezone = project.timezone datetime_fields = ["start_date", "end_date"] - cycle = user_timezone_converter( - cycle, datetime_fields, project_timezone - ) + cycle = user_timezone_converter(cycle, datetime_fields, project_timezone) # Send the model activity model_activity.delay( @@ -574,16 +573,12 @@ class CycleDateCheckEndpoint(BaseAPIView): status=status.HTTP_400_BAD_REQUEST, ) - is_start_date_end_date_equal = ( - True if str(start_date) == str(end_date) else False - ) start_date = convert_to_utc( date=str(start_date), project_id=project_id, is_start_date=True ) end_date = convert_to_utc( date=str(end_date), project_id=project_id, - is_start_date_end_date_equal=is_start_date_end_date_equal, ) # Check if any cycle intersects in the given interval @@ -668,6 +663,7 @@ class TransferCycleIssueEndpoint(BaseAPIView): issue_cycle__issue__archived_at__isnull=True, issue_cycle__issue__is_draft=False, issue_cycle__deleted_at__isnull=True, + issue_cycle__issue__deleted_at__isnull=True, ), ) ) @@ -732,6 +728,7 @@ class TransferCycleIssueEndpoint(BaseAPIView): ) ) ) + old_cycle = old_cycle.first() estimate_type = Project.objects.filter( workspace__slug=slug, @@ -850,7 +847,7 @@ class TransferCycleIssueEndpoint(BaseAPIView): ) estimate_completion_chart = burndown_plot( - queryset=old_cycle.first(), + queryset=old_cycle, slug=slug, project_id=project_id, plot_type="points", @@ -997,7 +994,7 @@ class TransferCycleIssueEndpoint(BaseAPIView): # Pass the new_cycle queryset to burndown_plot completion_chart = burndown_plot( - queryset=old_cycle.first(), + queryset=old_cycle, slug=slug, project_id=project_id, plot_type="issues", @@ -1009,12 +1006,12 @@ class TransferCycleIssueEndpoint(BaseAPIView): ).first() current_cycle.progress_snapshot = { - "total_issues": old_cycle.first().total_issues, - "completed_issues": old_cycle.first().completed_issues, - "cancelled_issues": old_cycle.first().cancelled_issues, - "started_issues": old_cycle.first().started_issues, - "unstarted_issues": old_cycle.first().unstarted_issues, - "backlog_issues": old_cycle.first().backlog_issues, + "total_issues": old_cycle.total_issues, + "completed_issues": old_cycle.completed_issues, + "cancelled_issues": old_cycle.cancelled_issues, + "started_issues": old_cycle.started_issues, + "unstarted_issues": old_cycle.unstarted_issues, + "backlog_issues": old_cycle.backlog_issues, "distribution": { "labels": label_distribution_data, "assignees": assignee_distribution_data, @@ -1122,6 +1119,14 @@ class CycleUserPropertiesEndpoint(BaseAPIView): class CycleProgressEndpoint(BaseAPIView): @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) def get(self, request, slug, project_id, cycle_id): + + cycle = Cycle.objects.filter( + workspace__slug=slug, project_id=project_id, id=cycle_id + ).first() + if not cycle: + return Response( + {"error": "Cycle not found"}, status=status.HTTP_404_NOT_FOUND + ) aggregate_estimates = ( Issue.issue_objects.filter( estimate_point__estimate__type="points", @@ -1172,53 +1177,60 @@ class CycleProgressEndpoint(BaseAPIView): ), ) ) + if cycle.progress_snapshot: + backlog_issues = cycle.progress_snapshot.get("backlog_issues", 0) + unstarted_issues = cycle.progress_snapshot.get("unstarted_issues", 0) + started_issues = cycle.progress_snapshot.get("started_issues", 0) + cancelled_issues = cycle.progress_snapshot.get("cancelled_issues", 0) + completed_issues = cycle.progress_snapshot.get("completed_issues", 0) + total_issues = cycle.progress_snapshot.get("total_issues", 0) + else: + backlog_issues = Issue.issue_objects.filter( + issue_cycle__cycle_id=cycle_id, + issue_cycle__deleted_at__isnull=True, + workspace__slug=slug, + project_id=project_id, + state__group="backlog", + ).count() - backlog_issues = Issue.issue_objects.filter( - issue_cycle__cycle_id=cycle_id, - issue_cycle__deleted_at__isnull=True, - workspace__slug=slug, - project_id=project_id, - state__group="backlog", - ).count() + unstarted_issues = Issue.issue_objects.filter( + issue_cycle__cycle_id=cycle_id, + issue_cycle__deleted_at__isnull=True, + workspace__slug=slug, + project_id=project_id, + state__group="unstarted", + ).count() - unstarted_issues = Issue.issue_objects.filter( - issue_cycle__cycle_id=cycle_id, - issue_cycle__deleted_at__isnull=True, - workspace__slug=slug, - project_id=project_id, - state__group="unstarted", - ).count() + started_issues = Issue.issue_objects.filter( + issue_cycle__cycle_id=cycle_id, + issue_cycle__deleted_at__isnull=True, + workspace__slug=slug, + project_id=project_id, + state__group="started", + ).count() - started_issues = Issue.issue_objects.filter( - issue_cycle__cycle_id=cycle_id, - issue_cycle__deleted_at__isnull=True, - workspace__slug=slug, - project_id=project_id, - state__group="started", - ).count() + cancelled_issues = Issue.issue_objects.filter( + issue_cycle__cycle_id=cycle_id, + issue_cycle__deleted_at__isnull=True, + workspace__slug=slug, + project_id=project_id, + state__group="cancelled", + ).count() - cancelled_issues = Issue.issue_objects.filter( - issue_cycle__cycle_id=cycle_id, - issue_cycle__deleted_at__isnull=True, - workspace__slug=slug, - project_id=project_id, - state__group="cancelled", - ).count() + completed_issues = Issue.issue_objects.filter( + issue_cycle__cycle_id=cycle_id, + issue_cycle__deleted_at__isnull=True, + workspace__slug=slug, + project_id=project_id, + state__group="completed", + ).count() - completed_issues = Issue.issue_objects.filter( - issue_cycle__cycle_id=cycle_id, - issue_cycle__deleted_at__isnull=True, - workspace__slug=slug, - project_id=project_id, - state__group="completed", - ).count() - - total_issues = Issue.issue_objects.filter( - issue_cycle__cycle_id=cycle_id, - issue_cycle__deleted_at__isnull=True, - workspace__slug=slug, - project_id=project_id, - ).count() + total_issues = Issue.issue_objects.filter( + issue_cycle__cycle_id=cycle_id, + issue_cycle__deleted_at__isnull=True, + workspace__slug=slug, + project_id=project_id, + ).count() return Response( { @@ -1279,6 +1291,25 @@ class CycleAnalyticsEndpoint(BaseAPIView): status=status.HTTP_400_BAD_REQUEST, ) + # this will tell whether the issues were transferred to the new cycle + """ + if the issues were transferred to the new cycle, then the progress_snapshot will be present + return the progress_snapshot data in the analytics for each date + + else issues were not transferred to the new cycle then generate the stats from the cycle isssue bridge tables + """ + + if cycle.progress_snapshot: + distribution = cycle.progress_snapshot.get("distribution", {}) + return Response( + { + "labels": distribution.get("labels", []), + "assignees": distribution.get("assignees", []), + "completion_chart": distribution.get("completion_chart", {}), + }, + status=status.HTTP_200_OK, + ) + estimate_type = Project.objects.filter( workspace__slug=slug, pk=project_id, diff --git a/apiserver/plane/app/views/workspace/cycle.py b/apiserver/plane/app/views/workspace/cycle.py index a9398a91d..3dce746ea 100644 --- a/apiserver/plane/app/views/workspace/cycle.py +++ b/apiserver/plane/app/views/workspace/cycle.py @@ -29,6 +29,7 @@ class WorkspaceCyclesEndpoint(BaseAPIView): issue_cycle__issue__archived_at__isnull=True, issue_cycle__issue__is_draft=False, issue_cycle__deleted_at__isnull=True, + issue_cycle__issue__deleted_at__isnull=True, ), ) ) diff --git a/apiserver/plane/utils/timezone_converter.py b/apiserver/plane/utils/timezone_converter.py index 40480b4f6..e4252422a 100644 --- a/apiserver/plane/utils/timezone_converter.py +++ b/apiserver/plane/utils/timezone_converter.py @@ -36,7 +36,7 @@ def user_timezone_converter(queryset, datetime_fields, user_timezone): def convert_to_utc( - date, project_id, is_start_date=False, is_start_date_end_date_equal=False + date, project_id, is_start_date=False ): """ Converts a start date string to the project's local timezone at 12:00 AM @@ -82,10 +82,8 @@ def convert_to_utc( return utc_datetime else: - # If it's start an end date are equal, add 23 hours, 59 minutes, and 59 seconds - # to make it the end of the day - if is_start_date_end_date_equal: - localized_datetime += timedelta(hours=23, minutes=59, seconds=59) + # the cycle end date is the last minute of the day + localized_datetime += timedelta(hours=23, minutes=59, seconds=0) # Convert the localized datetime to UTC utc_datetime = localized_datetime.astimezone(pytz.utc) From 298e3dc9cab15d9486ca7b48de4eaf11dcbdbbc2 Mon Sep 17 00:00:00 2001 From: Nikhil <118773738+pablohashescobar@users.noreply.github.com> Date: Tue, 29 Apr 2025 14:01:22 +0530 Subject: [PATCH 003/201] [WEB-3945] chore: update workspace onboarding to add default project (#6964) * chore: add json files and initial job to push data to workspace * chore: update seed data location * chore: update seed data to use assets from static urls * chore: update seed data to use updated labels * chore: add logging and update label name * chore: add created_by for project member * chore: add created_by_id for issue user property * chore: add workspace seed task logs * chore: update log message to return task name * chore: add warning log for workspace seed task * chore: add validation for issue seed data --- apiserver/plane/app/views/workspace/base.py | 3 + .../plane/bgtasks/workspace_seed_task.py | 319 ++++++++++++++++++ apiserver/plane/seeds/data/issues.json | 85 +++++ apiserver/plane/seeds/data/labels.json | 16 + apiserver/plane/seeds/data/projects.json | 17 + apiserver/plane/seeds/data/states.json | 47 +++ apiserver/plane/settings/common.py | 3 + 7 files changed, 490 insertions(+) create mode 100644 apiserver/plane/bgtasks/workspace_seed_task.py create mode 100644 apiserver/plane/seeds/data/issues.json create mode 100644 apiserver/plane/seeds/data/labels.json create mode 100644 apiserver/plane/seeds/data/projects.json create mode 100644 apiserver/plane/seeds/data/states.json diff --git a/apiserver/plane/app/views/workspace/base.py b/apiserver/plane/app/views/workspace/base.py index c627f19b6..e92e61e51 100644 --- a/apiserver/plane/app/views/workspace/base.py +++ b/apiserver/plane/app/views/workspace/base.py @@ -42,6 +42,7 @@ from django.views.decorators.cache import cache_control from django.views.decorators.vary import vary_on_cookie from plane.utils.constants import RESTRICTED_WORKSPACE_SLUGS from plane.license.utils.instance_value import get_configuration_value +from plane.bgtasks.workspace_seed_task import workspace_seed class WorkSpaceViewSet(BaseViewSet): @@ -126,6 +127,8 @@ class WorkSpaceViewSet(BaseViewSet): data["total_members"] = total_members data["role"] = 20 + workspace_seed.delay(serializer.data["id"]) + return Response(data, status=status.HTTP_201_CREATED) return Response( [serializer.errors[error][0] for error in serializer.errors], diff --git a/apiserver/plane/bgtasks/workspace_seed_task.py b/apiserver/plane/bgtasks/workspace_seed_task.py new file mode 100644 index 000000000..c2fbfb065 --- /dev/null +++ b/apiserver/plane/bgtasks/workspace_seed_task.py @@ -0,0 +1,319 @@ +# Python imports +import os +import json +import time +import uuid +from typing import Dict +import logging + +# Django imports +from django.conf import settings + +# Third party imports +from celery import shared_task + +# Module imports +from plane.db.models import ( + Workspace, + WorkspaceMember, + Project, + ProjectMember, + IssueUserProperty, + State, + Label, + Issue, + IssueLabel, + IssueSequence, + IssueActivity, +) + +logger = logging.getLogger("plane.worker") + + +def read_seed_file(filename): + """ + Read a JSON file from the seed directory. + + Args: + filename (str): Name of the JSON file to read + + Returns: + dict: Contents of the JSON file + """ + file_path = os.path.join(settings.SEED_DIR, "data", filename) + try: + with open(file_path, "r") as file: + return json.load(file) + except FileNotFoundError: + logger.error(f"Seed file {filename} not found in {settings.SEED_DIR}/data") + return None + except json.JSONDecodeError: + logger.error(f"Error decoding JSON from {filename}") + return None + + +def create_project_and_member(workspace: Workspace) -> Dict[int, uuid.UUID]: + """Creates a project and associated members for a workspace. + + Creates a new project using the workspace name and sets up all necessary + member associations and user properties. + + Args: + workspace: The workspace to create the project in + + Returns: + A mapping of seed project IDs to actual project IDs + """ + project_seeds = read_seed_file("projects.json") + project_identifier = "".join(ch for ch in workspace.name if ch.isalnum())[:5] + + # Create members + workspace_members = WorkspaceMember.objects.filter(workspace=workspace).values( + "member_id", "role" + ) + + projects_map: Dict[int, uuid.UUID] = {} + + if not project_seeds: + logger.warning( + "Task: workspace_seed_task -> No project seeds found. Skipping project creation." + ) + return projects_map + + for project_seed in project_seeds: + project_id = project_seed.pop("id") + # Remove the name from seed data since we want to use workspace name + project_seed.pop("name", None) + project_seed.pop("identifier", None) + + project = Project.objects.create( + **project_seed, + workspace=workspace, + name=workspace.name, # Use workspace name + identifier=project_identifier, + created_by_id=workspace.created_by_id, + ) + + # Create project members + ProjectMember.objects.bulk_create( + [ + ProjectMember( + project=project, + member_id=workspace_member["member_id"], + role=workspace_member["role"], + workspace_id=workspace.id, + created_by_id=workspace.created_by_id, + ) + for workspace_member in workspace_members + ] + ) + + # Create issue user properties + IssueUserProperty.objects.bulk_create( + [ + IssueUserProperty( + project=project, + user_id=workspace_member["member_id"], + workspace_id=workspace.id, + display_filters={ + "group_by": None, + "order_by": "sort_order", + "type": None, + "sub_issue": True, + "show_empty_groups": True, + "layout": "list", + "calendar_date_range": "", + }, + created_by_id=workspace.created_by_id, + ) + for workspace_member in workspace_members + ] + ) + # update map + projects_map[project_id] = project.id + logger.info(f"Task: workspace_seed_task -> Project {project_id} created") + + return projects_map + + +def create_project_states( + workspace: Workspace, project_map: Dict[int, uuid.UUID] +) -> Dict[int, uuid.UUID]: + """Creates states for each project in the workspace. + + Args: + workspace: The workspace containing the projects + project_map: Mapping of seed project IDs to actual project IDs + + Returns: + A mapping of seed state IDs to actual state IDs + """ + + state_seeds = read_seed_file("states.json") + state_map: Dict[int, uuid.UUID] = {} + + if not state_seeds: + return state_map + + for state_seed in state_seeds: + state_id = state_seed.pop("id") + project_id = state_seed.pop("project_id") + + state = State.objects.create( + **state_seed, + project_id=project_map[project_id], + workspace=workspace, + created_by_id=workspace.created_by_id, + ) + + state_map[state_id] = state.id + logger.info(f"Task: workspace_seed_task -> State {state_id} created") + return state_map + + +def create_project_labels( + workspace: Workspace, project_map: Dict[int, uuid.UUID] +) -> Dict[int, uuid.UUID]: + """Creates labels for each project in the workspace. + + Args: + workspace: The workspace containing the projects + project_map: Mapping of seed project IDs to actual project IDs + + Returns: + A mapping of seed label IDs to actual label IDs + """ + label_seeds = read_seed_file("labels.json") + label_map: Dict[int, uuid.UUID] = {} + + if not label_seeds: + return label_map + + for label_seed in label_seeds: + label_id = label_seed.pop("id") + project_id = label_seed.pop("project_id") + label = Label.objects.create( + **label_seed, + project_id=project_map[project_id], + workspace=workspace, + created_by_id=workspace.created_by_id, + ) + label_map[label_id] = label.id + + logger.info(f"Task: workspace_seed_task -> Label {label_id} created") + return label_map + + +def create_project_issues( + workspace: Workspace, + project_map: Dict[int, uuid.UUID], + states_map: Dict[int, uuid.UUID], + labels_map: Dict[int, uuid.UUID], +) -> None: + """Creates issues and their associated records for each project. + + Creates issues along with their sequences, activities, and label associations. + + Args: + workspace: The workspace containing the projects + project_map: Mapping of seed project IDs to actual project IDs + states_map: Mapping of seed state IDs to actual state IDs + labels_map: Mapping of seed label IDs to actual label IDs + """ + issue_seeds = read_seed_file("issues.json") + + if not issue_seeds: + return + + for issue_seed in issue_seeds: + required_fields = ["id", "labels", "project_id", "state_id"] + # get the values + for field in required_fields: + if field not in issue_seed: + logger.error( + f"Task: workspace_seed_task -> Required field '{field}' missing in issue seed" + ) + continue + + # get the values + issue_id = issue_seed.pop("id") + labels = issue_seed.pop("labels") + project_id = issue_seed.pop("project_id") + state_id = issue_seed.pop("state_id") + + issue = Issue.objects.create( + **issue_seed, + state_id=states_map[state_id], + project_id=project_map[project_id], + workspace=workspace, + created_by_id=workspace.created_by_id, + ) + IssueSequence.objects.create( + issue=issue, + project_id=project_map[project_id], + workspace_id=workspace.id, + created_by_id=workspace.created_by_id, + ) + + IssueActivity.objects.create( + issue=issue, + project_id=project_map[project_id], + workspace_id=workspace.id, + comment="created the issue", + verb="created", + actor_id=workspace.created_by_id, + epoch=time.time(), + ) + + for label_id in labels: + IssueLabel.objects.create( + issue=issue, + label_id=labels_map[label_id], + project_id=project_map[project_id], + workspace_id=workspace.id, + created_by_id=workspace.created_by_id, + ) + + logger.info(f"Task: workspace_seed_task -> Issue {issue_id} created") + return + + +@shared_task +def workspace_seed(workspace_id: uuid.UUID) -> None: + """Seeds a new workspace with initial project data. + + Creates a complete workspace setup including: + - Projects and project members + - Project states + - Project labels + - Issues and their associations + + Args: + workspace_id: ID of the workspace to seed + """ + try: + logger.info(f"Task: workspace_seed_task -> Seeding workspace {workspace_id}") + # Get the workspace + workspace = Workspace.objects.get(id=workspace_id) + + # Create a project with the same name as workspace + project_map = create_project_and_member(workspace) + + # Create project states + state_map = create_project_states(workspace, project_map) + + # Create project labels + label_map = create_project_labels(workspace, project_map) + + # create project issues + create_project_issues(workspace, project_map, state_map, label_map) + + logger.info( + f"Task: workspace_seed_task -> Workspace {workspace_id} seeded successfully" + ) + return + except Exception as e: + logger.error( + f"Task: workspace_seed_task -> Failed to seed workspace {workspace_id}: {str(e)}" + ) + raise e diff --git a/apiserver/plane/seeds/data/issues.json b/apiserver/plane/seeds/data/issues.json new file mode 100644 index 000000000..ca341304b --- /dev/null +++ b/apiserver/plane/seeds/data/issues.json @@ -0,0 +1,85 @@ +[ + { + "id": 1, + "name": "Welcome to Plane 👋", + "sequence_id": 1, + "description_html": "

Hey there! This demo project is your playground to get hands-on with Plane. We've set this up so you can click around and see how everything works without worrying about breaking anything.

Each work item is designed to make you familiar with the basics of using Plane. Just follow along card by card at your own pace.

First thing to try

  1. Look in the Properties section below where it says State: Todo.

  2. Click on it and change it to Done from the dropdown. Alternatively, you can drag and drop the card to the Done column.

", + "description_stripped": "Hey there! This demo project is your playground to get hands-on with Plane. We've set this up so you can click around and see how everything works without worrying about breaking anything.Each work item is designed to make you familiar with the basics of using Plane. Just follow along card by card at your own pace.First thing to tryLook in the Properties section below where it says State: Todo.Click on it and change it to Done from the dropdown. Alternatively, you can drag and drop the card to the Done column.", + "sort_order": 1000, + "state_id": 3, + "labels": [], + "priority": "none", + "project_id": 1 + }, + { + "id": 2, + "name": "1. Create Projects 🎯", + "sequence_id": 2, + "description_html": "


A Project in Plane is where all your work comes together. Think of it as a base that organizes your work items and everything else your team needs to get things done.

Note: This tutorial is already set up as a Project, and these cards you're reading are work items within it!

We're showing you how to create a new project just so you'll know exactly what to do when you're ready to start your own real one.

  1. Look over at the left sidebar and find where it says Projects.

  2. Hover your mouse there and you'll see a little + icon pop up - go ahead and click it!

  3. A modal opens where you can give your project a name and other details.

  4. Notice the Access type options? Public means anyone (except Guest users) can see and join it, while Private keeps it just for those you invite.

    Tip: You can also quickly create a new project by using the keyboard shortcut P from anywhere in Plane!

", + "sort_order": 2000, + "state_id": 2, + "labels": [2], + "priority": "none", + "project_id": 1 + }, + { + "id": 3, + "name": "2. Invite your team 🤜🤛", + "sequence_id": 3, + "description_html": "

Let's get your teammates on board!

First, you'll need to invite them to your workspace before they can join specific projects:

  1. Click on your workspace name in the top-left corner, then select Settings from the dropdown.

  2. Head over to the Members tab - this is your user management hub. Click Add member on the top right.

  3. Enter your teammate's email address. Select a role for them (Admin, Member or Guest) that determines what they can do in the workspace.

  4. Your team member will get an email invite. Once they've joined your workspace, you can add them to specific projects.

  5. To do this, go to your project's Settings page.

  6. Find the Members section, select your teammate, and assign them a project role - this controls what they can do within just this project.


That's it!

To learn more about user management, see Manage users and roles.

", + "description_stripped": "Let's get your teammates on board!First, you'll need to invite them to your workspace before they can join specific projects:Click on your workspace name in the top-left corner, then select Settings from the dropdown.Head over to the Members tab - this is your user management hub. Click Add member on the top right.Enter your teammate's email address. Select a role for them (Admin, Member or Guest) that determines what they can do in the workspace.Your team member will get an email invite. Once they've joined your workspace, you can add them to specific projects.To do this, go to your project's Settings page.Find the Members section, select your teammate, and assign them a project role - this controls what they can do within just this project.That's it!To learn more about user management, see Manage users and roles.", + "sort_order": 3000, + "state_id": 1, + "labels": [], + "priority": "none", + "project_id": 1 + }, + { + "id": 4, + "name": "3. Create and assign Work Items ✏️", + "sequence_id": 4, + "description_html": "

A work item is the fundamental building block of your project. Think of these as the actionable tasks that move your project forward.

Ready to add something to your project's to-do list? Here's how:

  1. Click the Add work item button in the top-right corner of the Work Items page.

  2. Give your task a clear title and add any details in the description.

  3. Set up the essentials:

    • Assign it to a team member (or yourself!)

    • Choose a priority level

    • Add start and due dates if there's a timeline

Tip: Save time by using the keyboard shortcut C from anywhere in your project to quickly create a new work item!

Want to dive deeper into all the things you can do with work items? Check out our documentation.

", + "description_stripped": "A work item is the fundamental building block of your project. Think of these as the actionable tasks that move your project forward.Ready to add something to your project's to-do list? Here's how:Click the Add work item button in the top-right corner of the Work Items page.Give your task a clear title and add any details in the description.Set up the essentials:Assign it to a team member (or yourself!)Choose a priority levelAdd start and due dates if there's a timelineTip: Save time by using the keyboard shortcut C from anywhere in your project to quickly create a new work item!Want to dive deeper into all the things you can do with work items? Check out our documentation.", + "sort_order": 4000, + "state_id": 1, + "labels": [2], + "priority": "none", + "project_id": 1 + }, + { + "id": 5, + "name": "4. Visualize your work 🔮", + "sequence_id": 5, + "description_html": "

Plane offers multiple ways to look at your work items depending on what you need to see. Let's explore how to change views and customize them!

Switch between layouts

  1. Look at the top toolbar in your project. You'll see several layout icons.

  2. Click any of these icons to instantly switch between layouts.

Tip: Different layouts work best for different needs. Try Board view for tracking progress, Calendar for deadline management, and Gantt for timeline planning! See Layouts for more info.

Filter and display options

Need to focus on specific work?

  1. Click the Filters dropdown in the toolbar. Select criteria and choose which items to show.

  2. Click the Display dropdown to tailor how the information appears in your layout

  3. Created the perfect setup? Save it for later by clicking the the Save View button.

  4. Access saved views anytime from the Views section in your sidebar.

", + "description_stripped": "Plane offers multiple ways to look at your work items depending on what you need to see. Let's explore how to change views and customize them!Switch between layoutsLook at the top toolbar in your project. You'll see several layout icons.Click any of these icons to instantly switch between layouts.Tip: Different layouts work best for different needs. Try Board view for tracking progress, Calendar for deadline management, and Gantt for timeline planning! See Layouts for more info.Filter and display optionsNeed to focus on specific work?Click the Filters dropdown in the toolbar. Select criteria and choose which items to show.Click the Display dropdown to tailor how the information appears in your layoutCreated the perfect setup? Save it for later by clicking the the Save View button.Access saved views anytime from the Views section in your sidebar.", + "sort_order": 5000, + "state_id": 1, + "labels": [], + "priority": "none", + "project_id": 1 + }, + { + "id": 6, + "name": "5. Use Cycles to time box tasks 🗓️", + "sequence_id": 6, + "description_html": "

A Cycle in Plane is like a sprint - a dedicated timeframe where your team focuses on completing specific work items. It helps you break down your project into manageable chunks with clear start and end dates so everyone knows what to work on and when it needs to be done.

Setup Cycles

  1. Go to the Cycles section in your project (you can find it in the left sidebar)

  2. Click the Add cycle button in the top-right corner

  3. Enter details and set the start and end dates for your cycle.

  4. Click Create cycle and you're ready to go!

  5. Add existing work items to the Cycle or create new ones.

Tip: To create a new Cycle quickly, just press Q from anywhere in your project!

Want to learn more?

Check out our detailed documentation for everything you need to know!

", + "description_stripped": "A Cycle in Plane is like a sprint - a dedicated timeframe where your team focuses on completing specific work items. It helps you break down your project into manageable chunks with clear start and end dates so everyone knows what to work on and when it needs to be done.Setup CyclesGo to the Cycles section in your project (you can find it in the left sidebar)Click the Add cycle button in the top-right cornerEnter details and set the start and end dates for your cycle.Click Create cycle and you're ready to go!Add existing work items to the Cycle or create new ones.Tip: To create a new Cycle quickly, just press Q from anywhere in your project!Want to learn more?Starting and stopping cyclesTransferring work items between cyclesTracking progress with chartsCheck out our detailed documentation for everything you need to know!", + "sort_order": 6000, + "state_id": 1, + "labels": [2], + "priority": "none", + "project_id": 1 + }, + { + "id": 7, + "name": "6. Customize your settings ⚙️", + "sequence_id": 7, + "description_html": "

Now that you're getting familiar with Plane, let's explore how you can customize settings to make it work just right for you and your team!

Workspace settings

Remember those workspace settings we mentioned when inviting team members? There's a lot more you can do there:

Project Settings

Each project has its own settings where you can:

Your Profile Settings

You can also customize your own personal experience! Click on your profile icon in the top-right corner to find:

Taking a few minutes to set things up just the way you like will make your everyday Plane experience much smoother!

Note: Some settings are only available to workspace or project admins. If you don't see certain options, you might need admin access.

", + "description_stripped": "Now that you're getting familiar with Plane, let's explore how you can customize settings to make it work just right for you and your team!Workspace settingsRemember those workspace settings we mentioned when inviting team members? There's a lot more you can do there:Invite and manage workspace membersUpgrade plans and manage billingImport data from other toolsExport your dataManage integrationsProject SettingsEach project has its own settings where you can:Change project details and visibilityInvite specific members to just this projectCustomize your workflow States (like adding a \"Testing\" state)Create and organize LabelsEnable or disable features you need (or don't need)Your Profile SettingsYou can also customize your own personal experience! Click on your profile icon in the top-right corner to find:Profile settings (update your name, photo, etc.)Choose your timezone and preferred language for the interfaceEmail notification preferences (what you want to be alerted about)Appearance settings (light/dark mode)Taking a few minutes to set things up just the way you like will make your everyday Plane experience much smoother!Note: Some settings are only available to workspace or project admins. If you don't see certain options, you might need admin access.", + "sort_order": 7000, + "state_id": 1, + "labels": [], + "priority": "none", + "project_id": 1 + } +] diff --git a/apiserver/plane/seeds/data/labels.json b/apiserver/plane/seeds/data/labels.json new file mode 100644 index 000000000..f7286a69c --- /dev/null +++ b/apiserver/plane/seeds/data/labels.json @@ -0,0 +1,16 @@ +[ + { + "id": 1, + "name": "admin", + "color": "#0693e3", + "sort_order": 85535, + "project_id": 1 + }, + { + "id": 2, + "name": "concepts", + "color": "#9900ef", + "sort_order": 95535, + "project_id": 1 + } +] diff --git a/apiserver/plane/seeds/data/projects.json b/apiserver/plane/seeds/data/projects.json new file mode 100644 index 000000000..1b24b8642 --- /dev/null +++ b/apiserver/plane/seeds/data/projects.json @@ -0,0 +1,17 @@ +[ + { + "id": 1, + "name": "Plane Demo Project", + "identifier": "PDP", + "description": "Welcome to the Plane Demo Project! This project throws you into the driver’s seat of Plane, work management software. Through curated work items, you’ll uncover key features, pick up best practices, and see how Plane can streamline your team’s workflow. Whether you’re a startup hungry to scale or an enterprise sharpening efficiency, this demo is your launchpad to mastering Plane. Jump in and see what it can do!", + "network": 2, + "cover_image": "https://images.unsplash.com/photo-1691230995681-480d86cbc135?auto=format&fit=crop&q=80&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&w=870&q=80", + "logo_props": { + "emoji": { + "url": "https://cdn.jsdelivr.net/npm/emoji-datasource-apple/img/apple/64/1f447.png", + "value": "128071" + }, + "in_use": "emoji" + } + } +] diff --git a/apiserver/plane/seeds/data/states.json b/apiserver/plane/seeds/data/states.json new file mode 100644 index 000000000..5eff65b9d --- /dev/null +++ b/apiserver/plane/seeds/data/states.json @@ -0,0 +1,47 @@ +[ + { + "id": 1, + "name": "Backlog", + "color": "#A3A3A3", + "sequence": 15000, + "group": "backlog", + "default": true, + "project_id": 1 + }, + { + "id": 2, + "name": "Todo", + "color": "#3A3A3A", + "sequence": 25000, + "group": "unstarted", + "default": false, + "project_id": 1 + }, + { + "id": 3, + "name": "In Progress", + "color": "#F59E0B", + "sequence": 35000, + "group": "started", + "default": false, + "project_id": 1 + }, + { + "id": 4, + "name": "Done", + "color": "#16A34A", + "sequence": 45000, + "group": "completed", + "default": false, + "project_id": 1 + }, + { + "id": 5, + "name": "Cancelled", + "color": "#EF4444", + "sequence": 55000, + "group": "cancelled", + "default": false, + "project_id": 1 + } +] diff --git a/apiserver/plane/settings/common.py b/apiserver/plane/settings/common.py index 444ec0a4f..76db7a928 100644 --- a/apiserver/plane/settings/common.py +++ b/apiserver/plane/settings/common.py @@ -396,3 +396,6 @@ ATTACHMENT_MIME_TYPES = [ # Gzip "application/x-gzip", ] + +# Seed directory path +SEED_DIR = os.path.join(BASE_DIR, "seeds") From baabb8266958d0ab61b6c6fd271dcf4831417a98 Mon Sep 17 00:00:00 2001 From: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com> Date: Tue, 29 Apr 2025 14:04:00 +0530 Subject: [PATCH 004/201] [WEB-3926] chore: removed the duplicated webhook task and updated the webhook task to handle exceptions correctly (#6951) * chore: removed the duplicated webhook function * chore: update webhook send task to handle errors --------- Co-authored-by: pablohashescobar --- .../plane/bgtasks/issue_activities_task.py | 34 -- apiserver/plane/bgtasks/webhook_task.py | 303 +++++++++--------- 2 files changed, 146 insertions(+), 191 deletions(-) diff --git a/apiserver/plane/bgtasks/issue_activities_task.py b/apiserver/plane/bgtasks/issue_activities_task.py index fcd75f8e3..4def8e8ca 100644 --- a/apiserver/plane/bgtasks/issue_activities_task.py +++ b/apiserver/plane/bgtasks/issue_activities_task.py @@ -1650,40 +1650,6 @@ def issue_activity( # Save all the values to database issue_activities_created = IssueActivity.objects.bulk_create(issue_activities) - # Post the updates to segway for integrations and webhooks - if len(issue_activities_created): - for activity in issue_activities_created: - webhook_activity.delay( - event=( - "issue_comment" - if activity.field == "comment" - else "intake_issue" - if intake - else "issue" - ), - event_id=( - activity.issue_comment_id - if activity.field == "comment" - else intake - if intake - else activity.issue_id - ), - verb=activity.verb, - field=( - "description" if activity.field == "comment" else activity.field - ), - old_value=( - activity.old_value if activity.old_value != "" else None - ), - new_value=( - activity.new_value if activity.new_value != "" else None - ), - actor_id=activity.actor_id, - current_site=origin, - slug=activity.workspace.slug, - old_identifier=activity.old_identifier, - new_identifier=activity.new_identifier, - ) if notification: notifications.delay( diff --git a/apiserver/plane/bgtasks/webhook_task.py b/apiserver/plane/bgtasks/webhook_task.py index c1ea01a4d..0bcfd2693 100644 --- a/apiserver/plane/bgtasks/webhook_task.py +++ b/apiserver/plane/bgtasks/webhook_task.py @@ -5,6 +5,7 @@ import logging import uuid import requests +from typing import Any, Dict, List, Optional, Union # Third party imports from celery import shared_task @@ -70,150 +71,89 @@ MODEL_MAPPER = { } -def get_model_data(event, event_id, many=False): +logger = logging.getLogger("plane.worker") + + +def get_model_data( + event: str, event_id: Union[str, List[str]], many: bool = False +) -> Dict[str, Any]: + """ + Retrieve and serialize model data based on the event type. + + Args: + event (str): The type of event/model to retrieve data for + event_id (Union[str, List[str]]): The ID or list of IDs of the model instance(s) + many (bool): Whether to retrieve multiple instances + + Returns: + Dict[str, Any]: Serialized model data + + Raises: + ValueError: If serializer is not found for the event + ObjectDoesNotExist: If model instance is not found + """ model = MODEL_MAPPER.get(event) - if many: - queryset = model.objects.filter(pk__in=event_id) - else: - queryset = model.objects.get(pk=event_id) - serializer = SERIALIZER_MAPPER.get(event) - return serializer(queryset, many=many).data + if model is None: + raise ValueError(f"Model not found for event: {event}") - -@shared_task( - bind=True, - autoretry_for=(requests.RequestException,), - retry_backoff=600, - max_retries=5, - retry_jitter=True, -) -def webhook_task(self, webhook, slug, event, event_data, action, current_site): try: - webhook = Webhook.objects.get(id=webhook, workspace__slug=slug) + if many: + queryset = model.objects.filter(pk__in=event_id) + else: + queryset = model.objects.get(pk=event_id) - headers = { - "Content-Type": "application/json", - "User-Agent": "Autopilot", - "X-Plane-Delivery": str(uuid.uuid4()), - "X-Plane-Event": event, - } + serializer = SERIALIZER_MAPPER.get(event) + if serializer is None: + raise ValueError(f"Serializer not found for event: {event}") - # # Your secret key - event_data = ( - json.loads(json.dumps(event_data, cls=DjangoJSONEncoder)) - if event_data is not None - else None - ) - - action = { - "POST": "create", - "PATCH": "update", - "PUT": "update", - "DELETE": "delete", - }.get(action, action) - - payload = { - "event": event, - "action": action, - "webhook_id": str(webhook.id), - "workspace_id": str(webhook.workspace_id), - "data": event_data, - } - - # Use HMAC for generating signature - if webhook.secret_key: - hmac_signature = hmac.new( - webhook.secret_key.encode("utf-8"), - json.dumps(payload).encode("utf-8"), - hashlib.sha256, - ) - signature = hmac_signature.hexdigest() - headers["X-Plane-Signature"] = signature - - # Send the webhook event - response = requests.post(webhook.url, headers=headers, json=payload, timeout=30) - - # Log the webhook request - WebhookLog.objects.create( - workspace_id=str(webhook.workspace_id), - webhook=str(webhook.id), - event_type=str(event), - request_method=str(action), - request_headers=str(headers), - request_body=str(payload), - response_status=str(response.status_code), - response_headers=str(response.headers), - response_body=str(response.text), - retry_count=str(self.request.retries), - ) - - except Webhook.DoesNotExist: - return - except requests.RequestException as e: - # Log the failed webhook request - WebhookLog.objects.create( - workspace_id=str(webhook.workspace_id), - webhook=str(webhook.id), - event_type=str(event), - request_method=str(action), - request_headers=str(headers), - request_body=str(payload), - response_status=500, - response_headers="", - response_body=str(e), - retry_count=str(self.request.retries), - ) - # Retry logic - if self.request.retries >= self.max_retries: - Webhook.objects.filter(pk=webhook.id).update(is_active=False) - if webhook: - # send email for the deactivation of the webhook - send_webhook_deactivation_email( - webhook_id=webhook.id, - receiver_id=webhook.created_by_id, - reason=str(e), - current_site=current_site, - ) - return - raise requests.RequestException() - - except Exception as e: - if settings.DEBUG: - print(e) - log_exception(e) - return + return serializer(queryset, many=many).data + except ObjectDoesNotExist: + raise ObjectDoesNotExist(f"No {event} found with id: {event_id}") @shared_task -def send_webhook_deactivation_email(webhook_id, receiver_id, current_site, reason): - # Get email configurations - ( - EMAIL_HOST, - EMAIL_HOST_USER, - EMAIL_HOST_PASSWORD, - EMAIL_PORT, - EMAIL_USE_TLS, - EMAIL_USE_SSL, - EMAIL_FROM, - ) = get_email_configuration() - - receiver = User.objects.get(pk=receiver_id) - webhook = Webhook.objects.get(pk=webhook_id) - subject = "Webhook Deactivated" - message = f"Webhook {webhook.url} has been deactivated due to failed requests." - - # Send the mail - context = { - "email": receiver.email, - "message": message, - "webhook_url": f"{current_site}/{str(webhook.workspace.slug)}/settings/webhooks/{str(webhook.id)}", - } - html_content = render_to_string( - "emails/notifications/webhook-deactivate.html", context - ) - text_content = strip_tags(html_content) +def send_webhook_deactivation_email( + webhook_id: str, receiver_id: str, current_site: str, reason: str +) -> None: + """ + Send an email notification when a webhook is deactivated. + Args: + webhook_id (str): ID of the deactivated webhook + receiver_id (str): ID of the user to receive the notification + current_site (str): Current site URL + reason (str): Reason for webhook deactivation + """ try: + ( + EMAIL_HOST, + EMAIL_HOST_USER, + EMAIL_HOST_PASSWORD, + EMAIL_PORT, + EMAIL_USE_TLS, + EMAIL_USE_SSL, + EMAIL_FROM, + ) = get_email_configuration() + + receiver = User.objects.get(pk=receiver_id) + webhook = Webhook.objects.get(pk=webhook_id) + + # Get the webhook payload + subject = "Webhook Deactivated" + message = f"Webhook {webhook.url} has been deactivated due to failed requests." + + # Send the mail + context = { + "email": receiver.email, + "message": message, + "webhook_url": f"{current_site}/{str(webhook.workspace.slug)}/settings/webhooks/{str(webhook.id)}", + } + html_content = render_to_string( + "emails/notifications/webhook-deactivate.html", context + ) + text_content = strip_tags(html_content) + + # Set the email connection connection = get_connection( host=EMAIL_HOST, port=int(EMAIL_PORT), @@ -223,6 +163,7 @@ def send_webhook_deactivation_email(webhook_id, receiver_id, current_site, reaso use_ssl=EMAIL_USE_SSL == "1", ) + # Create the email message msg = EmailMultiAlternatives( subject=subject, body=text_content, @@ -232,11 +173,10 @@ def send_webhook_deactivation_email(webhook_id, receiver_id, current_site, reaso ) msg.attach_alternative(html_content, "text/html") msg.send() - logging.getLogger("plane").info("Email sent successfully.") - return + logger.info("Email sent successfully.") except Exception as e: log_exception(e) - return + logger.error(f"Failed to send email: {e}") @shared_task( @@ -247,10 +187,29 @@ def send_webhook_deactivation_email(webhook_id, receiver_id, current_site, reaso retry_jitter=True, ) def webhook_send_task( - self, webhook, slug, event, event_data, action, current_site, activity -): + self, + webhook_id: str, + slug: str, + event: str, + event_data: Optional[Dict[str, Any]], + action: str, + current_site: str, + activity: Optional[Dict[str, Any]], +) -> None: + """ + Send webhook notifications to configured endpoints. + + Args: + webhook (str): Webhook ID + slug (str): Workspace slug + event (str): Event type + event_data (Optional[Dict[str, Any]]): Event data to be sent + action (str): HTTP method/action + current_site (str): Current site URL + activity (Optional[Dict[str, Any]]): Activity data + """ try: - webhook = Webhook.objects.get(id=webhook, workspace__slug=slug) + webhook = Webhook.objects.get(id=webhook_id, workspace__slug=slug) headers = { "Content-Type": "application/json", @@ -297,7 +256,12 @@ def webhook_send_task( ) signature = hmac_signature.hexdigest() headers["X-Plane-Signature"] = signature + except Exception as e: + log_exception(e) + logger.error(f"Failed to send webhook: {e}") + return + try: # Send the webhook event response = requests.post(webhook.url, headers=headers, json=payload, timeout=30) @@ -314,7 +278,7 @@ def webhook_send_task( response_body=str(response.text), retry_count=str(self.request.retries), ) - + logger.info(f"Webhook {webhook.id} sent successfully") except requests.RequestException as e: # Log the failed webhook request WebhookLog.objects.create( @@ -329,12 +293,13 @@ def webhook_send_task( response_body=str(e), retry_count=str(self.request.retries), ) + logger.error(f"Webhook {webhook.id} failed with error: {e}") # Retry logic if self.request.retries >= self.max_retries: Webhook.objects.filter(pk=webhook.id).update(is_active=False) if webhook: # send email for the deactivation of the webhook - send_webhook_deactivation_email( + send_webhook_deactivation_email.delay( webhook_id=webhook.id, receiver_id=webhook.created_by_id, reason=str(e), @@ -344,26 +309,50 @@ def webhook_send_task( raise requests.RequestException() except Exception as e: - if settings.DEBUG: - print(e) log_exception(e) return @shared_task def webhook_activity( - event, - verb, - field, - old_value, - new_value, - actor_id, - slug, - current_site, - event_id, - old_identifier, - new_identifier, -): + event: str, + verb: str, + field: Optional[str], + old_value: Any, + new_value: Any, + actor_id: str | uuid.UUID, + slug: str, + current_site: str, + event_id: str | uuid.UUID, + old_identifier: Optional[str], + new_identifier: Optional[str], +) -> None: + """ + Process and send webhook notifications for various activities in the system. + + This task filters relevant webhooks based on the event type and sends notifications + to all active webhooks for the workspace. + + Args: + event (str): Type of event (project, issue, module, cycle, issue_comment) + verb (str): Action performed (created, updated, deleted) + field (Optional[str]): Name of the field that was changed + old_value (Any): Previous value of the field + new_value (Any): New value of the field + actor_id (str | uuid.UUID): ID of the user who performed the action + slug (str): Workspace slug + current_site (str): Current site URL + event_id (str | uuid.UUID): ID of the event object + old_identifier (Optional[str]): Previous identifier if any + new_identifier (Optional[str]): New identifier if any + + Returns: + None + + Note: + The function silently returns on ObjectDoesNotExist exceptions to handle + race conditions where objects might have been deleted. + """ try: webhooks = Webhook.objects.filter(workspace__slug=slug, is_active=True) @@ -384,7 +373,7 @@ def webhook_activity( for webhook in webhooks: webhook_send_task.delay( - webhook=webhook.id, + webhook_id=webhook.id, slug=slug, event=event, event_data=( From f5449c8f933b201b5ca90ce6e490cad2b2bd89c8 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Tue, 29 Apr 2025 14:33:53 +0530 Subject: [PATCH 005/201] [WEB-3751] chore: work item state icon improvement (#6960) * chore: return order based on group * chore: order for workspace stats endpoint * chore: state response updated * chore: state icon types updated * chore: state icon updated * chore: state settings new icon implementation * chore: icon implementation * chore: code refactor * chore: code refactor * chore: code refactor * fix: order field type --------- Co-authored-by: sangeethailango --- apiserver/plane/api/views/project.py | 8 +- apiserver/plane/app/serializers/state.py | 5 +- apiserver/plane/app/views/project/base.py | 8 +- apiserver/plane/app/views/state/base.py | 14 +++ apiserver/plane/app/views/workspace/state.py | 12 +++ packages/constants/src/icon.ts | 7 ++ packages/constants/src/index.ts | 1 + packages/types/src/state.d.ts | 1 + .../ui/src/icons/state/backlog-group-icon.tsx | 50 ++++------ .../src/icons/state/cancelled-group-icon.tsx | 19 ++-- .../src/icons/state/completed-group-icon.tsx | 6 +- packages/ui/src/icons/state/dashed-circle.tsx | 40 ++++++++ packages/ui/src/icons/state/helper.tsx | 26 ++++-- .../ui/src/icons/state/progress-circle.tsx | 32 +++++++ .../ui/src/icons/state/started-group-icon.tsx | 92 ++++++++++++------- .../ui/src/icons/state/state-group-icon.tsx | 12 ++- .../src/icons/state/unstarted-group-icon.tsx | 56 ++++++++--- packages/ui/src/icons/type.ts | 1 + .../issues/filters/applied-filters/state.tsx | 5 +- .../components/issues/issue-layouts/utils.tsx | 4 +- .../work-item-actions/change-state-list.tsx | 8 +- .../automation/auto-close-automation.tsx | 16 +--- web/core/components/dropdowns/state.tsx | 10 +- .../components/home/widgets/recents/issue.tsx | 7 +- .../inbox-filter/applied-filters/state.tsx | 3 +- .../inbox/inbox-filter/filters/state.tsx | 10 +- .../filters/applied-filters/state-group.tsx | 3 +- .../filters/applied-filters/state.tsx | 8 +- .../filters/header/filters/state.tsx | 10 +- .../components/issues/issue-layouts/utils.tsx | 10 +- .../components/project-states/group-item.tsx | 3 +- .../project-states/state-item-title.tsx | 9 +- web/core/store/state.store.ts | 25 +++++ 33 files changed, 376 insertions(+), 145 deletions(-) create mode 100644 packages/constants/src/icon.ts create mode 100644 packages/ui/src/icons/state/dashed-circle.tsx create mode 100644 packages/ui/src/icons/state/progress-circle.tsx diff --git a/apiserver/plane/api/views/project.py b/apiserver/plane/api/views/project.py index 5ceb06a63..038d4faec 100644 --- a/apiserver/plane/api/views/project.py +++ b/apiserver/plane/api/views/project.py @@ -172,14 +172,14 @@ class ProjectAPIEndpoint(BaseAPIView): states = [ { "name": "Backlog", - "color": "#A3A3A3", + "color": "#60646C", "sequence": 15000, "group": "backlog", "default": True, }, { "name": "Todo", - "color": "#3A3A3A", + "color": "#60646C", "sequence": 25000, "group": "unstarted", }, @@ -191,13 +191,13 @@ class ProjectAPIEndpoint(BaseAPIView): }, { "name": "Done", - "color": "#16A34A", + "color": "#46A758", "sequence": 45000, "group": "completed", }, { "name": "Cancelled", - "color": "#EF4444", + "color": "#9AA4BC", "sequence": 55000, "group": "cancelled", }, diff --git a/apiserver/plane/app/serializers/state.py b/apiserver/plane/app/serializers/state.py index 61af5cab7..29d8cf302 100644 --- a/apiserver/plane/app/serializers/state.py +++ b/apiserver/plane/app/serializers/state.py @@ -1,11 +1,13 @@ # Module imports from .base import BaseSerializer - +from rest_framework import serializers from plane.db.models import State class StateSerializer(BaseSerializer): + order = serializers.FloatField(required=False) + class Meta: model = State fields = [ @@ -18,6 +20,7 @@ class StateSerializer(BaseSerializer): "default", "description", "sequence", + "order", ] read_only_fields = ["workspace", "project"] diff --git a/apiserver/plane/app/views/project/base.py b/apiserver/plane/app/views/project/base.py index 46290d7a5..31cbd8330 100644 --- a/apiserver/plane/app/views/project/base.py +++ b/apiserver/plane/app/views/project/base.py @@ -275,14 +275,14 @@ class ProjectViewSet(BaseViewSet): states = [ { "name": "Backlog", - "color": "#A3A3A3", + "color": "#60646C", "sequence": 15000, "group": "backlog", "default": True, }, { "name": "Todo", - "color": "#3A3A3A", + "color": "#60646C", "sequence": 25000, "group": "unstarted", }, @@ -294,13 +294,13 @@ class ProjectViewSet(BaseViewSet): }, { "name": "Done", - "color": "#16A34A", + "color": "#46A758", "sequence": 45000, "group": "completed", }, { "name": "Cancelled", - "color": "#EF4444", + "color": "#9AA4BC", "sequence": 55000, "group": "cancelled", }, diff --git a/apiserver/plane/app/views/state/base.py b/apiserver/plane/app/views/state/base.py index 419cd5a35..b735659c5 100644 --- a/apiserver/plane/app/views/state/base.py +++ b/apiserver/plane/app/views/state/base.py @@ -1,5 +1,6 @@ # Python imports from itertools import groupby +from collections import defaultdict # Django imports from django.db.utils import IntegrityError @@ -74,7 +75,19 @@ class StateViewSet(BaseViewSet): @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) def list(self, request, slug, project_id): states = StateSerializer(self.get_queryset(), many=True).data + + grouped_states = defaultdict(list) + for state in states: + grouped_states[state["group"]].append(state) + + for group, group_states in grouped_states.items(): + count = len(group_states) + + for index, state in enumerate(group_states, start=1): + state["order"] = index / count + grouped = request.GET.get("grouped", False) + if grouped == "true": state_dict = {} for key, value in groupby( @@ -83,6 +96,7 @@ class StateViewSet(BaseViewSet): ): state_dict[str(key)] = list(value) return Response(state_dict, status=status.HTTP_200_OK) + return Response(states, status=status.HTTP_200_OK) @invalidate_cache(path="workspaces/:slug/states/", url_params=True, user=False) diff --git a/apiserver/plane/app/views/workspace/state.py b/apiserver/plane/app/views/workspace/state.py index c00044cff..08bc2be28 100644 --- a/apiserver/plane/app/views/workspace/state.py +++ b/apiserver/plane/app/views/workspace/state.py @@ -8,6 +8,7 @@ from plane.app.views.base import BaseAPIView from plane.db.models import State from plane.app.permissions import WorkspaceEntityPermission from plane.utils.cache import cache_response +from collections import defaultdict class WorkspaceStatesEndpoint(BaseAPIView): @@ -22,5 +23,16 @@ class WorkspaceStatesEndpoint(BaseAPIView): project__archived_at__isnull=True, is_triage=False, ) + + grouped_states = defaultdict(list) + for state in states: + grouped_states[state.group].append(state) + + for group, group_states in grouped_states.items(): + count = len(group_states) + + for index, state in enumerate(group_states, start=1): + state.order = index / count + serializer = StateSerializer(states, many=True).data return Response(serializer, status=status.HTTP_200_OK) diff --git a/packages/constants/src/icon.ts b/packages/constants/src/icon.ts new file mode 100644 index 000000000..3ee66e31e --- /dev/null +++ b/packages/constants/src/icon.ts @@ -0,0 +1,7 @@ +export enum EIconSize { + XS = "xs", + SM = "sm", + MD = "md", + LG = "lg", + XL = "xl", +} diff --git a/packages/constants/src/index.ts b/packages/constants/src/index.ts index f974dd64b..057627fcd 100644 --- a/packages/constants/src/index.ts +++ b/packages/constants/src/index.ts @@ -32,3 +32,4 @@ export * from "./dashboard"; export * from "./page"; export * from "./emoji"; export * from "./subscription"; +export * from "./icon"; diff --git a/packages/types/src/state.d.ts b/packages/types/src/state.d.ts index d28194dc9..38d0abe12 100644 --- a/packages/types/src/state.d.ts +++ b/packages/types/src/state.d.ts @@ -12,6 +12,7 @@ export interface IState { project_id: string; sequence: number; workspace_id: string; + order: number; } export interface IStateLite { diff --git a/packages/ui/src/icons/state/backlog-group-icon.tsx b/packages/ui/src/icons/state/backlog-group-icon.tsx index fce4f0024..ebd0d05c4 100644 --- a/packages/ui/src/icons/state/backlog-group-icon.tsx +++ b/packages/ui/src/icons/state/backlog-group-icon.tsx @@ -1,39 +1,27 @@ import * as React from "react"; import { ISvgIcons } from "../type"; +import { DashedCircle } from "./dashed-circle"; export const BacklogGroupIcon: React.FC = ({ width = "20", height = "20", className, - color = "#a3a3a3", -}) => ( - - - - - - - - - - -); + color = "#60646C", +}) => { + // SVG parameters + const viewBoxSize = 16; + const center = viewBoxSize / 2; + const radius = 6; + return ( + + + + ); +}; diff --git a/packages/ui/src/icons/state/cancelled-group-icon.tsx b/packages/ui/src/icons/state/cancelled-group-icon.tsx index c18a2570a..fb802523e 100644 --- a/packages/ui/src/icons/state/cancelled-group-icon.tsx +++ b/packages/ui/src/icons/state/cancelled-group-icon.tsx @@ -4,7 +4,7 @@ import { ISvgIcons } from "../type"; export const CancelledGroupIcon: React.FC = ({ className = "", - color = "#ef4444", + color = "#9AA4BC", height = "20", width = "20", ...rest @@ -17,16 +17,11 @@ export const CancelledGroupIcon: React.FC = ({ xmlns="http://www.w3.org/2000/svg" {...rest} > - - - - - - - - + ); diff --git a/packages/ui/src/icons/state/completed-group-icon.tsx b/packages/ui/src/icons/state/completed-group-icon.tsx index b53292687..c4a15f15f 100644 --- a/packages/ui/src/icons/state/completed-group-icon.tsx +++ b/packages/ui/src/icons/state/completed-group-icon.tsx @@ -4,7 +4,7 @@ import { ISvgIcons } from "../type"; export const CompletedGroupIcon: React.FC = ({ className = "", - color = "#16a34a", + color = "#46A758", height = "20", width = "20", ...rest @@ -18,7 +18,9 @@ export const CompletedGroupIcon: React.FC = ({ {...rest} > diff --git a/packages/ui/src/icons/state/dashed-circle.tsx b/packages/ui/src/icons/state/dashed-circle.tsx new file mode 100644 index 000000000..4ee64cdf0 --- /dev/null +++ b/packages/ui/src/icons/state/dashed-circle.tsx @@ -0,0 +1,40 @@ +import * as React from "react"; + +interface DashedCircleProps { + center: number; + radius: number; + color: string; + percentage: number; + totalSegments?: number; +} + +export const DashedCircle: React.FC = ({ center, color, percentage, totalSegments = 15 }) => { + // Ensure percentage is between 0 and 100 + const validPercentage = Math.max(0, Math.min(100, percentage)); + + // Generate dashed segments for the circle + const generateDashedCircle = () => { + const segments = []; + const angleIncrement = 360 / totalSegments; + + for (let i = 0; i < totalSegments; i++) { + // Calculate the angle for this segment (starting from top/12 o'clock position) + const angle = i * angleIncrement - 90; // -90 adjusts to start from top center + + // Calculate if this segment should be hidden based on percentage + const segmentStartPercentage = (i / totalSegments) * 100; + const isSegmentVisible = segmentStartPercentage >= validPercentage; + + if (isSegmentVisible) { + segments.push( + + + + ); + } + } + return segments; + }; + + return {generateDashedCircle()}; +}; diff --git a/packages/ui/src/icons/state/helper.tsx b/packages/ui/src/icons/state/helper.tsx index 0f1fa1231..fda3eff34 100644 --- a/packages/ui/src/icons/state/helper.tsx +++ b/packages/ui/src/icons/state/helper.tsx @@ -1,9 +1,11 @@ +import { EIconSize } from "@plane/constants"; + export interface IStateGroupIcon { className?: string; color?: string; stateGroup: TStateGroups; - height?: string; - width?: string; + size?: EIconSize; + percentage?: number; } export type TStateGroups = "backlog" | "unstarted" | "started" | "completed" | "cancelled"; @@ -11,9 +13,19 @@ export type TStateGroups = "backlog" | "unstarted" | "started" | "completed" | " export const STATE_GROUP_COLORS: { [key in TStateGroups]: string; } = { - backlog: "#d9d9d9", - unstarted: "#3f76ff", - started: "#f59e0b", - completed: "#16a34a", - cancelled: "#dc2626", + backlog: "#60646C", + unstarted: "#60646C", + started: "#F59E0B", + completed: "#46A758", + cancelled: "#9AA4BC", +}; + +export const STATE_GROUP_SIZES: { + [key in EIconSize]: string; +} = { + [EIconSize.XS]: "10px", + [EIconSize.SM]: "12px", + [EIconSize.MD]: "14px", + [EIconSize.LG]: "16px", + [EIconSize.XL]: "18px", }; diff --git a/packages/ui/src/icons/state/progress-circle.tsx b/packages/ui/src/icons/state/progress-circle.tsx new file mode 100644 index 000000000..470a560fc --- /dev/null +++ b/packages/ui/src/icons/state/progress-circle.tsx @@ -0,0 +1,32 @@ +import * as React from "react"; + +interface ProgressCircleProps { + center: number; + radius: number; + color: string; + strokeWidth: number; + circumference: number; + dashOffset: number; +} + +export const ProgressCircle: React.FC = ({ + center, + radius, + color, + strokeWidth, + circumference, + dashOffset, +}) => ( + +); diff --git a/packages/ui/src/icons/state/started-group-icon.tsx b/packages/ui/src/icons/state/started-group-icon.tsx index 77f1d1609..d924c0d6c 100644 --- a/packages/ui/src/icons/state/started-group-icon.tsx +++ b/packages/ui/src/icons/state/started-group-icon.tsx @@ -1,43 +1,65 @@ import * as React from "react"; import { ISvgIcons } from "../type"; +import { DashedCircle } from "./dashed-circle"; +import { ProgressCircle } from "./progress-circle"; +// StateIcon component implementation export const StartedGroupIcon: React.FC = ({ width = "20", height = "20", className, - color = "#f39e1f", -}) => ( - - - - - - - - - - - -); + color = "#F59E0B", + percentage = 100, +}) => { + // Ensure percentage is between 0 and 100 + const normalized = + typeof percentage === "number" + ? percentage <= 1 + ? percentage * 100 // treat 0-1 as fraction + : percentage // already 0-100 + : 100; // fallback + const validPercentage = Math.max(0, Math.min(100, normalized)); + + // SVG parameters + const viewBoxSize = 16; + const center = viewBoxSize / 2; + const radius = 6; + const strokeWidth = 1.5; + + // Calculate the circumference of the circle + const circumference = 2 * Math.PI * radius; + const dashOffset = circumference * (1 - validPercentage / 100); + const dashOffsetSmall = circumference * (1 - 100 / 100); + + return ( + + {/* Dashed background circle with segments that disappear with progress */} + + + {/* render smaller circle in the middle */} + + + {/* Solid progress circle */} + + + ); +}; diff --git a/packages/ui/src/icons/state/state-group-icon.tsx b/packages/ui/src/icons/state/state-group-icon.tsx index e771fff5e..b5647ae05 100644 --- a/packages/ui/src/icons/state/state-group-icon.tsx +++ b/packages/ui/src/icons/state/state-group-icon.tsx @@ -1,11 +1,12 @@ import * as React from "react"; +import { EIconSize } from "@plane/constants"; import { BacklogGroupIcon } from "./backlog-group-icon"; import { CancelledGroupIcon } from "./cancelled-group-icon"; import { CompletedGroupIcon } from "./completed-group-icon"; +import { IStateGroupIcon, STATE_GROUP_COLORS, STATE_GROUP_SIZES } from "./helper"; import { StartedGroupIcon } from "./started-group-icon"; import { UnstartedGroupIcon } from "./unstarted-group-icon"; -import { IStateGroupIcon, STATE_GROUP_COLORS } from "./helper"; const iconComponents = { backlog: BacklogGroupIcon, @@ -19,17 +20,18 @@ export const StateGroupIcon: React.FC = ({ className = "", color, stateGroup, - height = "12px", - width = "12px", + size = EIconSize.SM, + percentage, }) => { const StateIconComponent = iconComponents[stateGroup] || UnstartedGroupIcon; return ( ); }; diff --git a/packages/ui/src/icons/state/unstarted-group-icon.tsx b/packages/ui/src/icons/state/unstarted-group-icon.tsx index 5c62b1f12..9f57b698f 100644 --- a/packages/ui/src/icons/state/unstarted-group-icon.tsx +++ b/packages/ui/src/icons/state/unstarted-group-icon.tsx @@ -1,21 +1,51 @@ import * as React from "react"; import { ISvgIcons } from "../type"; +import { DashedCircle } from "./dashed-circle"; +import { ProgressCircle } from "./progress-circle"; +// StateIcon component implementation export const UnstartedGroupIcon: React.FC = ({ width = "20", height = "20", className, - color = "#3a3a3a", -}) => ( - - - -); + color = "#F59E0B", + percentage = 100, +}) => { + // Ensure percentage is between 0 and 100 + const normalized = + typeof percentage === "number" + ? percentage <= 1 + ? percentage * 100 // treat 0-1 as fraction + : percentage // already 0-100 + : 100; // fallback + const validPercentage = Math.max(0, Math.min(100, normalized)); + + // SVG parameters + const viewBoxSize = 16; + const center = viewBoxSize / 2; + const radius = 6; + const strokeWidth = 1.5; + + // Calculate the circumference of the circle + const circumference = 2 * Math.PI * radius; + + // Calculate the dash offset based on percentage + const dashOffset = circumference * (1 - validPercentage / 100); + + return ( + + + + {/* Solid progress circle */} + + + ); +}; diff --git a/packages/ui/src/icons/type.ts b/packages/ui/src/icons/type.ts index 4a04c948b..09e605629 100644 --- a/packages/ui/src/icons/type.ts +++ b/packages/ui/src/icons/type.ts @@ -1,3 +1,4 @@ export interface ISvgIcons extends React.SVGAttributes { className?: string | undefined; + percentage?: number; } diff --git a/space/core/components/issues/filters/applied-filters/state.tsx b/space/core/components/issues/filters/applied-filters/state.tsx index 23bfc87e6..4166dabfb 100644 --- a/space/core/components/issues/filters/applied-filters/state.tsx +++ b/space/core/components/issues/filters/applied-filters/state.tsx @@ -2,7 +2,8 @@ import { observer } from "mobx-react"; import { X } from "lucide-react"; -// ui +// plane imports +import { EIconSize } from "@plane/constants"; import { StateGroupIcon } from "@plane/ui"; // hooks import { useStates } from "@/hooks/store"; @@ -26,7 +27,7 @@ export const AppliedStateFilters: React.FC = observer((props) => { return (
- + {stateDetails.name}
), payload: { state_id: state.id }, diff --git a/web/ce/components/command-palette/actions/work-item-actions/change-state-list.tsx b/web/ce/components/command-palette/actions/work-item-actions/change-state-list.tsx index 9f59d226b..1ec015bbd 100644 --- a/web/ce/components/command-palette/actions/work-item-actions/change-state-list.tsx +++ b/web/ce/components/command-palette/actions/work-item-actions/change-state-list.tsx @@ -2,6 +2,7 @@ import { Command } from "cmdk"; import { observer } from "mobx-react"; import { Check } from "lucide-react"; // plane imports +import { EIconSize } from "@plane/constants"; import { Spinner, StateGroupIcon } from "@plane/ui"; // store hooks import { useProjectState } from "@/hooks/store"; @@ -26,7 +27,12 @@ export const ChangeWorkItemStateList = observer((props: TChangeWorkItemStateList projectStates.map((state) => ( handleStateChange(state.id)} className="focus:outline-none">
- +

{state.name}

{state.id === currentStateId && }
diff --git a/web/core/components/automation/auto-close-automation.tsx b/web/core/components/automation/auto-close-automation.tsx index 833e3bfde..0adf1387d 100644 --- a/web/core/components/automation/auto-close-automation.tsx +++ b/web/core/components/automation/auto-close-automation.tsx @@ -6,7 +6,7 @@ import { useParams } from "next/navigation"; // icons import { ArchiveX } from "lucide-react"; // types -import { PROJECT_AUTOMATION_MONTHS, EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; +import { PROJECT_AUTOMATION_MONTHS, EUserPermissions, EUserPermissionsLevel, EIconSize } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { IProject } from "@plane/types"; // ui @@ -42,7 +42,7 @@ export const AutoCloseAutomation: React.FC = observer((props) => { query: state.name, content: (
- + {state.name}
), @@ -139,7 +139,7 @@ export const AutoCloseAutomation: React.FC = observer((props) => { -
+
{t("project_settings.automations.auto-close.auto_close_status")}
@@ -149,18 +149,12 @@ export const AutoCloseAutomation: React.FC = observer((props) => { label={
{selectedOption ? ( - + ) : currentDefaultState ? ( ) : ( diff --git a/web/core/components/dropdowns/state.tsx b/web/core/components/dropdowns/state.tsx index 9af77dd7e..bdc754260 100644 --- a/web/core/components/dropdowns/state.tsx +++ b/web/core/components/dropdowns/state.tsx @@ -98,7 +98,12 @@ export const StateDropdown: React.FC = observer((props) => { query: `${state?.name}`, content: (
- + {state?.name}
), @@ -179,7 +184,8 @@ export const StateDropdown: React.FC = observer((props) => { )} {BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && ( diff --git a/web/core/components/home/widgets/recents/issue.tsx b/web/core/components/home/widgets/recents/issue.tsx index 63bd732b3..d4be2f7f9 100644 --- a/web/core/components/home/widgets/recents/issue.tsx +++ b/web/core/components/home/widgets/recents/issue.tsx @@ -95,7 +95,12 @@ export const RecentIssue = observer((props: BlockProps) => {
- +
diff --git a/web/core/components/inbox/inbox-filter/applied-filters/state.tsx b/web/core/components/inbox/inbox-filter/applied-filters/state.tsx index 8a1f0d0ad..a237c910f 100644 --- a/web/core/components/inbox/inbox-filter/applied-filters/state.tsx +++ b/web/core/components/inbox/inbox-filter/applied-filters/state.tsx @@ -3,6 +3,7 @@ import { FC } from "react"; import { observer } from "mobx-react"; import { X } from "lucide-react"; +import { EIconSize } from "@plane/constants"; import { StateGroupIcon, Tag } from "@plane/ui"; // hooks import { useProjectInbox, useProjectState } from "@/hooks/store"; @@ -30,7 +31,7 @@ export const InboxIssueAppliedFiltersState: FC = observer(() => { return (
- +
{optionDetail?.name}
= observer((props) => { key={state?.id} isChecked={filterValue?.includes(state?.id) ? true : false} onClick={() => handleInboxIssueFilters("state", handleFilterValue(state.id))} - icon={} + icon={ + + } title={state.name} /> ))} diff --git a/web/core/components/issues/issue-layouts/filters/applied-filters/state-group.tsx b/web/core/components/issues/issue-layouts/filters/applied-filters/state-group.tsx index 521408cd9..807a5b233 100644 --- a/web/core/components/issues/issue-layouts/filters/applied-filters/state-group.tsx +++ b/web/core/components/issues/issue-layouts/filters/applied-filters/state-group.tsx @@ -4,6 +4,7 @@ import { observer } from "mobx-react"; // icons import { X } from "lucide-react"; +import { EIconSize } from "@plane/constants"; import { TStateGroups } from "@plane/types"; import { StateGroupIcon } from "@plane/ui"; @@ -19,7 +20,7 @@ export const AppliedStateGroupFilters: React.FC = observer((props) => { <> {values.map((stateGroup) => (
- + {stateGroup} -
- - - {subIssuesDistribution?.completed?.length ?? 0}/{subIssues.length} Done - -
-
- - {!disabled && ( - - - Add sub-work item - - } - buttonClassName="whitespace-nowrap" - placement="bottom-end" - noBorder - noChevron - > - { - setTrackElement("Issue detail nested sub-issue"); - handleIssueCrudState("create", parentIssueId, null); - toggleCreateIssueModal(true); - }} - > -
- - Create new -
-
- { - setTrackElement("Issue detail nested sub-issue"); - handleIssueCrudState("existing", parentIssueId, null); - toggleSubIssuesModal(issue.id); - }} - > -
- - Add existing -
-
-
- )} -
- - {subIssueHelpers.issue_visibility.includes(parentIssueId) && ( - - )} - - ) : ( - !disabled && ( -
-
No sub-work items yet
- - - Add sub-work item - - } - buttonClassName="whitespace-nowrap" - placement="bottom-end" - noBorder - noChevron - > - { - setTrackElement("Issue detail nested sub-issue"); - handleIssueCrudState("create", parentIssueId, null); - toggleCreateIssueModal(true); - }} - > -
- - Create new -
-
- { - setTrackElement("Issue detail nested sub-issue"); - handleIssueCrudState("existing", parentIssueId, null); - toggleSubIssuesModal(issue.id); - }} - > -
- - Add existing -
-
-
-
- ) - )} - - {/* issue create, add from existing , update and delete modals */} - {issueCrudState?.create?.toggle && issueCrudState?.create?.parentIssueId && isCreateIssueModalOpen && ( - { - handleIssueCrudState("create", null, null); - toggleCreateIssueModal(false); - }} - onSubmit={async (_issue: TIssue) => { - if (_issue.parent_id) { - await subIssueOperations.addSubIssue(workspaceSlug, projectId, _issue.parent_id, [_issue.id]); - } - }} - /> - )} - - {issueCrudState?.existing?.toggle && issueCrudState?.existing?.parentIssueId && isSubIssuesModalOpen && ( - { - handleIssueCrudState("existing", null, null); - toggleSubIssuesModal(null); - }} - searchParams={{ sub_issue: true, issue_id: issueCrudState?.existing?.parentIssueId }} - handleOnSubmit={(_issue) => - subIssueOperations.addSubIssue( - workspaceSlug, - projectId, - parentIssueId, - _issue.map((issue) => issue.id) - ) - } - workspaceLevelToggle - /> - )} - - {issueCrudState?.update?.toggle && issueCrudState?.update?.issue && ( - <> - { - handleIssueCrudState("update", null, null); - toggleCreateIssueModal(false); - }} - data={issueCrudState?.update?.issue ?? undefined} - onSubmit={async (_issue: TIssue) => { - await subIssueOperations.updateSubIssue( - workspaceSlug, - projectId, - parentIssueId, - _issue.id, - _issue, - issueCrudState?.update?.issue, - true - ); - }} - /> - - )} - - {issueCrudState?.delete?.toggle && - issueCrudState?.delete?.issue && - issueCrudState.delete.parentIssueId && - issueCrudState.delete.issue.id && ( - { - handleIssueCrudState("delete", null, null); - toggleDeleteIssueModal(null); - }} - data={issueCrudState?.delete?.issue as TIssue} - onSubmit={async () => - await subIssueOperations.deleteSubIssue( - workspaceSlug, - projectId, - issueCrudState?.delete?.parentIssueId as string, - issueCrudState?.delete?.issue?.id as string - ) - } - isSubIssue - /> - )} - - )} -
- ); -}); diff --git a/web/core/services/issue/issue.service.ts b/web/core/services/issue/issue.service.ts index ccd240bfa..e53492b81 100644 --- a/web/core/services/issue/issue.service.ts +++ b/web/core/services/issue/issue.service.ts @@ -267,9 +267,15 @@ export class IssueService extends APIService { }); } - async subIssues(workspaceSlug: string, projectId: string, issueId: string): Promise { + async subIssues( + workspaceSlug: string, + projectId: string, + issueId: string, + queries?: Partial> + ): Promise { return this.get( - `/api/workspaces/${workspaceSlug}/projects/${projectId}/${this.serviceType}/${issueId}/${this.serviceType === EIssueServiceType.EPICS ? "issues" : "sub-issues"}/` + `/api/workspaces/${workspaceSlug}/projects/${projectId}/${this.serviceType}/${issueId}/${this.serviceType === EIssueServiceType.EPICS ? "issues" : "sub-issues"}/`, + { params: queries } ) .then((response) => response?.data) .catch((error) => { diff --git a/web/core/store/issue/issue-details/sub_issues.store.ts b/web/core/store/issue/issue-details/sub_issues.store.ts index c2c160c39..d77c42d30 100644 --- a/web/core/store/issue/issue-details/sub_issues.store.ts +++ b/web/core/store/issue/issue-details/sub_issues.store.ts @@ -13,12 +13,16 @@ import { TIssueSubIssuesIdMap, TSubIssuesStateDistribution, TIssueServiceType, + TLoader, + TGroupedIssues, + TGroupedIssueCount, } from "@plane/types"; // services import { updatePersistentLayer } from "@/local-db/utils/utils"; import { IssueService } from "@/services/issue"; // store import { IIssueDetail } from "./root.store"; +import { IWorkItemSubIssueFiltersStore, WorkItemSubIssueFiltersStore } from "./sub_issues_filter.store"; export interface IIssueSubIssuesStoreActions { fetchSubIssues: (workspaceSlug: string, projectId: string, parentIssueId: string) => Promise; @@ -47,11 +51,16 @@ export interface IIssueSubIssuesStore extends IIssueSubIssuesStoreActions { // observables subIssuesStateDistribution: TIssueSubIssuesStateDistributionMap; subIssues: TIssueSubIssuesIdMap; + groupedSubIssuesMap: Record; + groupedSubIssuesCount: TGroupedIssueCount; subIssueHelpers: Record; // parent_issue_id -> TSubIssueHelpers + loader: TLoader; + filters: IWorkItemSubIssueFiltersStore; // helper methods stateDistributionByIssueId: (issueId: string) => TSubIssuesStateDistribution | undefined; subIssuesByIssueId: (issueId: string) => string[] | undefined; subIssueHelpersByIssueId: (issueId: string) => TSubIssueHelpers; + groupedSubIssuesByIssueId: (issueId: string) => TGroupedIssues | undefined; // actions fetchOtherProjectProperties: (workspaceSlug: string, projectIds: string[]) => Promise; setSubIssueHelpers: (parentIssueId: string, key: TSubIssueHelpersKeys, value: string) => void; @@ -61,7 +70,12 @@ export class IssueSubIssuesStore implements IIssueSubIssuesStore { // observables subIssuesStateDistribution: TIssueSubIssuesStateDistributionMap = {}; subIssues: TIssueSubIssuesIdMap = {}; + groupedSubIssuesMap: Record = {}; + groupedSubIssuesCount: TGroupedIssueCount = {}; subIssueHelpers: Record = {}; + loader: TLoader = undefined; + + filters: IWorkItemSubIssueFiltersStore; // root store rootIssueDetailStore: IIssueDetail; // services @@ -74,6 +88,8 @@ export class IssueSubIssuesStore implements IIssueSubIssuesStore { subIssuesStateDistribution: observable, subIssues: observable, subIssueHelpers: observable, + groupedSubIssuesMap: observable, + loader: observable.ref, // actions setSubIssueHelpers: action, fetchSubIssues: action, @@ -82,7 +98,9 @@ export class IssueSubIssuesStore implements IIssueSubIssuesStore { removeSubIssue: action, deleteSubIssue: action, fetchOtherProjectProperties: action, + groupedSubIssuesByIssueId: action, }); + this.filters = new WorkItemSubIssueFiltersStore(this); // root store this.rootIssueDetailStore = rootStore; // services @@ -101,6 +119,8 @@ export class IssueSubIssuesStore implements IIssueSubIssuesStore { return this.subIssues[issueId] ?? undefined; }; + groupedSubIssuesByIssueId = (issueId: string) => this.groupedSubIssuesMap[issueId] ?? undefined; + subIssueHelpersByIssueId = (issueId: string) => ({ preview_loader: this.subIssueHelpers?.[issueId]?.preview_loader || [], issue_visibility: this.subIssueHelpers?.[issueId]?.issue_visibility || [], @@ -118,20 +138,29 @@ export class IssueSubIssuesStore implements IIssueSubIssuesStore { }; fetchSubIssues = async (workspaceSlug: string, projectId: string, parentIssueId: string) => { - const response = await this.issueService.subIssues(workspaceSlug, projectId, parentIssueId); + // get filter params + const filterParams = this.filters.computedFilterParams(parentIssueId); + const response = await this.issueService.subIssues(workspaceSlug, projectId, parentIssueId, filterParams); + const subIssuesStateDistribution = response?.state_distribution ?? {}; - const subIssues = (response.sub_issues ?? []) as TIssue[]; - this.rootIssueDetailStore.rootIssueStore.issues.addIssue(subIssues); - // fetch other issues states and members when sub-issues are from different project - if (subIssues && subIssues.length > 0) { + + // process sub issues response + const { issueList, groupedIssues } = this.filters.processSubIssueResponse(response.sub_issues); + + // set grouped issues count + set(this.groupedSubIssuesMap, [parentIssueId], groupedIssues); + + this.rootIssueDetailStore.rootIssueStore.issues.addIssue(issueList); + + if (issueList && issueList.length > 0) { const otherProjectIds = uniq( - subIssues.map((issue) => issue.project_id).filter((id) => !!id && id !== projectId) + issueList.map((issue) => issue.project_id).filter((id) => !!id && id !== projectId) ) as string[]; this.fetchOtherProjectProperties(workspaceSlug, otherProjectIds); } - if (subIssues) { + if (issueList) { this.rootIssueDetailStore.rootIssueStore.issues.updateIssue(parentIssueId, { - sub_issues_count: subIssues.length, + sub_issues_count: issueList.length, }); } runInAction(() => { @@ -139,7 +168,7 @@ export class IssueSubIssuesStore implements IIssueSubIssuesStore { set( this.subIssues, parentIssueId, - subIssues.map((issue) => issue.id) + issueList.map((issue) => issue.id) ); }); return response; @@ -282,7 +311,7 @@ export class IssueSubIssuesStore implements IIssueSubIssuesStore { set( this.rootIssueDetailStore.rootIssueStore.issues.issuesMap, [parentIssueId, "sub_issues_count"], - this.subIssues[parentIssueId].length + this.subIssues[parentIssueId]?.length ); }); @@ -319,7 +348,7 @@ export class IssueSubIssuesStore implements IIssueSubIssuesStore { set( this.rootIssueDetailStore.rootIssueStore.issues.issuesMap, [parentIssueId, "sub_issues_count"], - this.subIssues[parentIssueId].length + this.subIssues[parentIssueId]?.length ); }); diff --git a/web/core/store/issue/issue-details/sub_issues_filter.store.ts b/web/core/store/issue/issue-details/sub_issues_filter.store.ts new file mode 100644 index 000000000..47edf767c --- /dev/null +++ b/web/core/store/issue/issue-details/sub_issues_filter.store.ts @@ -0,0 +1,202 @@ +import set from "lodash/set"; +import { action, makeObservable, observable } from "mobx"; +import { ALL_ISSUES, EIssueFilterType, EIssueGroupByToServerOptions } from "@plane/constants"; +import { + IIssueDisplayFilterOptions, + IIssueDisplayProperties, + IIssueFilters, + TGroupedIssueCount, + TGroupedIssues, + TIssue, + TIssueParams, + TIssues, + TSubGroupedIssues, + TSubIssueResponse, +} from "@plane/types"; +import { IIssueSubIssuesStore } from "./sub_issues.store"; + +export interface IWorkItemSubIssueFiltersStore { + subIssueFiltersMap: Record>; + // helpers methods + updateSubIssueFilters: ( + workspaceSlug: string, + projectId: string, + filterType: EIssueFilterType, + filters: IIssueDisplayFilterOptions | IIssueDisplayProperties, + parentId: string + ) => Promise; + getSubIssueFilters: (parentId: string) => Partial; + computedFilterParams: (parentId: string) => Partial>; + processSubIssueResponse: (issueResponse: TSubIssueResponse) => { + issueList: TIssue[]; + groupedIssues: TIssues; + groupedIssueCount: TGroupedIssueCount; + }; +} + +export class WorkItemSubIssueFiltersStore implements IWorkItemSubIssueFiltersStore { + // observables + subIssueFiltersMap: Record> = {}; + + subIssueStore: IIssueSubIssuesStore; + + constructor(subIssueStore: IIssueSubIssuesStore) { + makeObservable(this, { + subIssueFiltersMap: observable, + updateSubIssueFilters: action, + getSubIssueFilters: action, + }); + // sub issue store + this.subIssueStore = subIssueStore; + } + + /** + * @description This method is used to initialize the sub issue filters + * @param parentId + */ + initSubIssueFilters = (parentId: string) => { + set(this.subIssueFiltersMap, [parentId], { + displayFilters: {}, + displayProperties: { + key: true, + issue_type: true, + assignee: true, + start_date: true, + due_date: true, + labels: true, + priority: true, + state: true, + }, + }); + }; + + /** + * @description This method is used to process the sub issue response to provide the data to update the store + * @param issueResponse + * @returns issueList, list of issues data + * @returns groupedIssues, grouped issue ids + * @returns groupedIssueCount, object containing issue counts of individual groups + */ + processSubIssueResponse = ( + issueResponse: TSubIssueResponse + ): { + issueList: TIssue[]; + groupedIssues: TIssues; + groupedIssueCount: TGroupedIssueCount; + } => { + const issueResult = issueResponse; + + if (!issueResult) { + return { + issueList: [], + groupedIssues: {}, + groupedIssueCount: {}, + }; + } + + //if is an array then it's an ungrouped response. return values with groupId as ALL_ISSUES + if (Array.isArray(issueResult)) { + return { + issueList: issueResult, + groupedIssues: { + [ALL_ISSUES]: issueResult.map((issue) => issue.id), + }, + groupedIssueCount: { + [ALL_ISSUES]: issueResult.length, + }, + }; + } + + const issueList: TIssue[] = []; + const groupedIssues: TGroupedIssues | TSubGroupedIssues = {}; + const groupedIssueCount: TGroupedIssueCount = {}; + + // update total issue count to ALL_ISSUES + set(groupedIssueCount, [ALL_ISSUES], issueResult.length); + + // loop through all the groupIds from issue Result + for (const groupId in issueResult) { + const groupIssueResult = issueResult[groupId]; + + // if groupIssueResult is undefined then continue the loop + if (!groupIssueResult) continue; + + // set grouped Issue count of the current groupId + set(groupedIssueCount, [groupId], groupIssueResult.length); + + // add the result to issueList + issueList.push(...groupIssueResult); + // set the issue Ids to the groupId path + set( + groupedIssues, + [groupId], + groupIssueResult.map((issue) => issue.id) + ); + } + + return { issueList, groupedIssues, groupedIssueCount }; + }; + + /** + * @description This method is used to get the sub issue filters + * @param parentId + * @returns IIssueFilters + */ + getSubIssueFilters = (parentId: string) => { + if (!this.subIssueFiltersMap[parentId]) { + this.initSubIssueFilters(parentId); + } + return this.subIssueFiltersMap[parentId]; + }; + + computedFilterParams = (parentId: string) => { + const displayFilters = this.getSubIssueFilters(parentId).displayFilters; + + const computedFilters: Partial> = { + order_by: displayFilters?.order_by || undefined, + group_by: displayFilters?.group_by ? EIssueGroupByToServerOptions[displayFilters.group_by] : undefined, + }; + + const issueFiltersParams: Partial> = {}; + Object.keys(computedFilters).forEach((key) => { + const _key = key as TIssueParams; + const _value: string | boolean | string[] | undefined = computedFilters[_key]; + const nonEmptyArrayValue = Array.isArray(_value) && _value.length === 0 ? undefined : _value; + if (nonEmptyArrayValue != undefined) + issueFiltersParams[_key] = Array.isArray(nonEmptyArrayValue) + ? nonEmptyArrayValue.join(",") + : nonEmptyArrayValue; + }); + + return issueFiltersParams; + }; + + /** + * @description This method is used to update the sub issue filters + * @param projectId + * @param filterType + * @param filters + */ + updateSubIssueFilters = async ( + workspaceSlug: string, + projectId: string, + filterType: EIssueFilterType, + filters: IIssueDisplayFilterOptions | IIssueDisplayProperties, + parentId: string + ) => { + const _filters = this.getSubIssueFilters(parentId); + switch (filterType) { + case EIssueFilterType.DISPLAY_FILTERS: { + set(this.subIssueFiltersMap, [parentId, "displayFilters"], { ..._filters.displayFilters, ...filters }); + this.subIssueStore.fetchSubIssues(workspaceSlug, projectId, parentId); + break; + } + case EIssueFilterType.DISPLAY_PROPERTIES: + set(this.subIssueFiltersMap, [parentId, "displayProperties"], { + ..._filters.displayProperties, + ...filters, + }); + break; + } + }; +} From cdca5a4126d149fdb533768732d6543fd76f0138 Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Tue, 29 Apr 2025 15:33:03 +0530 Subject: [PATCH 009/201] chore: build fixes --- web/core/components/automation/auto-close-automation.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/web/core/components/automation/auto-close-automation.tsx b/web/core/components/automation/auto-close-automation.tsx index 0adf1387d..7aef61d13 100644 --- a/web/core/components/automation/auto-close-automation.tsx +++ b/web/core/components/automation/auto-close-automation.tsx @@ -149,7 +149,11 @@ export const AutoCloseAutomation: React.FC = observer((props) => { label={
{selectedOption ? ( - + ) : currentDefaultState ? ( Date: Tue, 29 Apr 2025 15:34:12 +0530 Subject: [PATCH 010/201] fix: turbo repo upgrade --- package.json | 2 +- yarn.lock | 68 ++++++++++++++++++++++++++-------------------------- 2 files changed, 35 insertions(+), 35 deletions(-) diff --git a/package.json b/package.json index cfcc346bb..152ab6a23 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "devDependencies": { "prettier": "latest", "prettier-plugin-tailwindcss": "^0.5.4", - "turbo": "^2.5.0" + "turbo": "^2.5.2" }, "resolutions": { "nanoid": "3.3.8", diff --git a/yarn.lock b/yarn.lock index 4063af1c4..78b07a567 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11166,47 +11166,47 @@ tunnel-agent@^0.6.0: dependencies: safe-buffer "^5.0.1" -turbo-darwin-64@2.5.0: - version "2.5.0" - resolved "https://registry.npmjs.org/turbo-darwin-64/-/turbo-darwin-64-2.5.0.tgz#3d966d03b98b9b41dd5abf80bc792e251195a905" - integrity sha512-fP1hhI9zY8hv0idym3hAaXdPi80TLovmGmgZFocVAykFtOxF+GlfIgM/l4iLAV9ObIO4SUXPVWHeBZQQ+Hpjag== +turbo-darwin-64@2.5.2: + version "2.5.2" + resolved "https://registry.npmjs.org/turbo-darwin-64/-/turbo-darwin-64-2.5.2.tgz#892927344ad37679143555ebdf41ad704f4d72d8" + integrity sha512-2aIl0Sx230nLk+Cg2qSVxvPOBWCZpwKNuAMKoROTvWKif6VMpkWWiR9XEPoz7sHeLmCOed4GYGMjL1bqAiIS/g== -turbo-darwin-arm64@2.5.0: - version "2.5.0" - resolved "https://registry.npmjs.org/turbo-darwin-arm64/-/turbo-darwin-arm64-2.5.0.tgz#94bf89c6f2d942eadad08741a11ef36601980b58" - integrity sha512-p9sYq7kXH7qeJwIQE86cOWv/xNqvow846l6c/qWc26Ib1ci5W7V0sI5thsrP3eH+VA0d+SHalTKg5SQXgNQBWA== +turbo-darwin-arm64@2.5.2: + version "2.5.2" + resolved "https://registry.npmjs.org/turbo-darwin-arm64/-/turbo-darwin-arm64-2.5.2.tgz#c3a00bcba481f5baa24ce4b1302f09c2c50d4e30" + integrity sha512-MrFYhK/jYu8N6QlqZtqSHi3e4QVxlzqU3ANHTKn3/tThuwTLbNHEvzBPWSj5W7nZcM58dCqi6gYrfRz6bJZyAA== -turbo-linux-64@2.5.0: - version "2.5.0" - resolved "https://registry.npmjs.org/turbo-linux-64/-/turbo-linux-64-2.5.0.tgz#878dae794436581d781c4573ff7975deab5b9d67" - integrity sha512-1iEln2GWiF3iPPPS1HQJT6ZCFXynJPd89gs9SkggH2EJsj3eRUSVMmMC8y6d7bBbhBFsiGGazwFIYrI12zs6uQ== +turbo-linux-64@2.5.2: + version "2.5.2" + resolved "https://registry.npmjs.org/turbo-linux-64/-/turbo-linux-64-2.5.2.tgz#1f366e0208b4da79d13d57df202dc5f49fb0367c" + integrity sha512-LxNqUE2HmAJQ/8deoLgMUDzKxd5bKxqH0UBogWa+DF+JcXhtze3UTMr6lEr0dEofdsEUYK1zg8FRjglmwlN5YA== -turbo-linux-arm64@2.5.0: - version "2.5.0" - resolved "https://registry.npmjs.org/turbo-linux-arm64/-/turbo-linux-arm64-2.5.0.tgz#c2d84b11a340d480fd0a44bfd7c8cece2383d6fb" - integrity sha512-bKBcbvuQHmsX116KcxHJuAcppiiBOfivOObh2O5aXNER6mce7YDDQJy00xQQNp1DhEfcSV2uOsvb3O3nN2cbcA== +turbo-linux-arm64@2.5.2: + version "2.5.2" + resolved "https://registry.npmjs.org/turbo-linux-arm64/-/turbo-linux-arm64-2.5.2.tgz#a48d9f1eddd279d523682eb74911b5c045ae1092" + integrity sha512-0MI1Ao1q8zhd+UUbIEsrM+yLq1BsrcJQRGZkxIsHFlGp7WQQH1oR3laBgfnUCNdCotCMD6w4moc9pUbXdOR3bg== -turbo-windows-64@2.5.0: - version "2.5.0" - resolved "https://registry.npmjs.org/turbo-windows-64/-/turbo-windows-64-2.5.0.tgz#104dbe9466e98196dd2c9f5b0fcad223c3c05fb6" - integrity sha512-9BCo8oQ7BO7J0K913Czbc3tw8QwLqn2nTe4E47k6aVYkM12ASTScweXPTuaPFP5iYXAT6z5Dsniw704Ixa5eGg== +turbo-windows-64@2.5.2: + version "2.5.2" + resolved "https://registry.npmjs.org/turbo-windows-64/-/turbo-windows-64-2.5.2.tgz#6ad0604fd7b1e53c54feca037de74e4303e23fa1" + integrity sha512-hOLcbgZzE5ttACHHyc1ajmWYq4zKT42IC3G6XqgiXxMbS+4eyVYTL+7UvCZBd3Kca1u4TLQdLQjeO76zyDJc2A== -turbo-windows-arm64@2.5.0: - version "2.5.0" - resolved "https://registry.npmjs.org/turbo-windows-arm64/-/turbo-windows-arm64-2.5.0.tgz#a7b4b5efea74001253ccf08f0edf004d9620e73e" - integrity sha512-OUHCV+ueXa3UzfZ4co/ueIHgeq9B2K48pZwIxKSm5VaLVuv8M13MhM7unukW09g++dpdrrE1w4IOVgxKZ0/exg== +turbo-windows-arm64@2.5.2: + version "2.5.2" + resolved "https://registry.npmjs.org/turbo-windows-arm64/-/turbo-windows-arm64-2.5.2.tgz#8bf9e79d2f3adf92371ef79da89c04090f3ab914" + integrity sha512-fMU41ABhSLa18H8V3Z7BMCGynQ8x+wj9WyBMvWm1jeyRKgkvUYJsO2vkIsy8m0vrwnIeVXKOIn6eSe1ddlBVqw== -turbo@^2.5.0: - version "2.5.0" - resolved "https://registry.npmjs.org/turbo/-/turbo-2.5.0.tgz#6ea89f4511e7fb68b866c51e709bfbace7efc62a" - integrity sha512-PvSRruOsitjy6qdqwIIyolv99+fEn57gP6gn4zhsHTEcCYgXPhv6BAxzAjleS8XKpo+Y582vTTA9nuqYDmbRuA== +turbo@^2.5.2: + version "2.5.2" + resolved "https://registry.npmjs.org/turbo/-/turbo-2.5.2.tgz#c6be6379f7495166fb0cc87d5362da8c7e4093c6" + integrity sha512-Qo5lfuStr6LQh3sPQl7kIi243bGU4aHGDQJUf6ylAdGwks30jJFloc9NYHP7Y373+gGU9OS0faA4Mb5Sy8X9Xw== optionalDependencies: - turbo-darwin-64 "2.5.0" - turbo-darwin-arm64 "2.5.0" - turbo-linux-64 "2.5.0" - turbo-linux-arm64 "2.5.0" - turbo-windows-64 "2.5.0" - turbo-windows-arm64 "2.5.0" + turbo-darwin-64 "2.5.2" + turbo-darwin-arm64 "2.5.2" + turbo-linux-64 "2.5.2" + turbo-linux-arm64 "2.5.2" + turbo-windows-64 "2.5.2" + turbo-windows-arm64 "2.5.2" tween-functions@^1.2.0: version "1.2.0" From c4ddff54199f39867a430c2ab26c3435fb8b568a Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Tue, 29 Apr 2025 15:48:52 +0530 Subject: [PATCH 011/201] chore: nextjs dependencies upgrade --- admin/package.json | 2 +- space/package.json | 2 +- web/package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/admin/package.json b/admin/package.json index 3b6cf3e6b..a809ef1b1 100644 --- a/admin/package.json +++ b/admin/package.json @@ -29,7 +29,7 @@ "lucide-react": "^0.469.0", "mobx": "^6.12.0", "mobx-react": "^9.1.1", - "next": "^14.2.26", + "next": "^14.2.28", "next-themes": "^0.2.1", "postcss": "^8.4.38", "react": "^18.3.1", diff --git a/space/package.json b/space/package.json index c5a302664..25db45895 100644 --- a/space/package.json +++ b/space/package.json @@ -36,7 +36,7 @@ "mobx": "^6.10.0", "mobx-react": "^9.1.1", "mobx-utils": "^6.0.8", - "next": "^14.2.26", + "next": "^14.2.28", "next-themes": "^0.2.1", "nprogress": "^0.2.0", "react": "^18.3.1", diff --git a/web/package.json b/web/package.json index c04564b30..868fa5742 100644 --- a/web/package.json +++ b/web/package.json @@ -51,7 +51,7 @@ "mobx": "^6.10.0", "mobx-react": "^9.1.1", "mobx-utils": "^6.0.8", - "next": "^14.2.26", + "next": "^14.2.28", "next-themes": "^0.2.1", "nprogress": "^0.2.0", "posthog-js": "^1.131.3", From d10bb0b638d5e4b72db0fbd0b37d33da76aff6c2 Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Tue, 29 Apr 2025 15:49:14 +0530 Subject: [PATCH 012/201] chore: yarn lock updates --- yarn.lock | 108 +++++++++++++++++++++++++++--------------------------- 1 file changed, 54 insertions(+), 54 deletions(-) diff --git a/yarn.lock b/yarn.lock index 78b07a567..6f35385eb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1613,10 +1613,10 @@ prop-types "^15.8.1" react-is "^19.0.0" -"@next/env@14.2.26": - version "14.2.26" - resolved "https://registry.yarnpkg.com/@next/env/-/env-14.2.26.tgz#5d55f72d2edb7246607c78f61e7d3ff21516bc2e" - integrity sha512-vO//GJ/YBco+H7xdQhzJxF7ub3SUwft76jwaeOyVVQFHCi5DCnkP16WHB+JBylo4vOKPoZBlR94Z8xBxNBdNJA== +"@next/env@14.2.28": + version "14.2.28" + resolved "https://registry.npmjs.org/@next/env/-/env-14.2.28.tgz#4bfeac21949743bfc8d09cfc223439112bcd2538" + integrity sha512-PAmWhJfJQlP+kxZwCjrVd9QnR5x0R3u0mTXTiZDgSd4h5LdXmjxCCWbN9kq6hkZBOax8Rm3xDW5HagWyJuT37g== "@next/eslint-plugin-next@14.2.24": version "14.2.24" @@ -1625,50 +1625,50 @@ dependencies: glob "10.3.10" -"@next/swc-darwin-arm64@14.2.26": - version "14.2.26" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.26.tgz#84b31a22149b2c49f5c5b29cddd7acb3a84d7e1c" - integrity sha512-zDJY8gsKEseGAxG+C2hTMT0w9Nk9N1Sk1qV7vXYz9MEiyRoF5ogQX2+vplyUMIfygnjn9/A04I6yrUTRTuRiyQ== +"@next/swc-darwin-arm64@14.2.28": + version "14.2.28" + resolved "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.28.tgz#b65bdd4f95eb883ca621d96563baa54ac7df6e3c" + integrity sha512-kzGChl9setxYWpk3H6fTZXXPFFjg7urptLq5o5ZgYezCrqlemKttwMT5iFyx/p1e/JeglTwDFRtb923gTJ3R1w== -"@next/swc-darwin-x64@14.2.26": - version "14.2.26" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.26.tgz#50a5eb37972d313951f76f36f1f0b7100d063ebd" - integrity sha512-U0adH5ryLfmTDkahLwG9sUQG2L0a9rYux8crQeC92rPhi3jGQEY47nByQHrVrt3prZigadwj/2HZ1LUUimuSbg== +"@next/swc-darwin-x64@14.2.28": + version "14.2.28" + resolved "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.28.tgz#1dc7d4a27927043ec3259b88044f11cce1219be4" + integrity sha512-z6FXYHDJlFOzVEOiiJ/4NG8aLCeayZdcRSMjPDysW297Up6r22xw6Ea9AOwQqbNsth8JNgIK8EkWz2IDwaLQcw== -"@next/swc-linux-arm64-gnu@14.2.26": - version "14.2.26" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.26.tgz#c4278c157623b05886e37ff17194811aca1c2d00" - integrity sha512-SINMl1I7UhfHGM7SoRiw0AbwnLEMUnJ/3XXVmhyptzriHbWvPPbbm0OEVG24uUKhuS1t0nvN/DBvm5kz6ZIqpg== +"@next/swc-linux-arm64-gnu@14.2.28": + version "14.2.28" + resolved "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.28.tgz#a4c6a805a821bb59fc66baa18236ddcfdb62e515" + integrity sha512-9ARHLEQXhAilNJ7rgQX8xs9aH3yJSj888ssSjJLeldiZKR4D7N08MfMqljk77fAwZsWwsrp8ohHsMvurvv9liQ== -"@next/swc-linux-arm64-musl@14.2.26": - version "14.2.26" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.26.tgz#5751132764b7a1f13a5a3fe447b03d564eb29705" - integrity sha512-s6JaezoyJK2DxrwHWxLWtJKlqKqTdi/zaYigDXUJ/gmx/72CrzdVZfMvUc6VqnZ7YEvRijvYo+0o4Z9DencduA== +"@next/swc-linux-arm64-musl@14.2.28": + version "14.2.28" + resolved "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.28.tgz#1b8cd8c9acdba9e591661f36dc3e04ef805c6701" + integrity sha512-p6gvatI1nX41KCizEe6JkF0FS/cEEF0u23vKDpl+WhPe/fCTBeGkEBh7iW2cUM0rvquPVwPWdiUR6Ebr/kQWxQ== -"@next/swc-linux-x64-gnu@14.2.26": - version "14.2.26" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.26.tgz#74312cac45704762faa73e0880be6549027303af" - integrity sha512-FEXeUQi8/pLr/XI0hKbe0tgbLmHFRhgXOUiPScz2hk0hSmbGiU8aUqVslj/6C6KA38RzXnWoJXo4FMo6aBxjzg== +"@next/swc-linux-x64-gnu@14.2.28": + version "14.2.28" + resolved "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.28.tgz#ba796651b1214b3e8a8aa34398c432b8defbe325" + integrity sha512-nsiSnz2wO6GwMAX2o0iucONlVL7dNgKUqt/mDTATGO2NY59EO/ZKnKEr80BJFhuA5UC1KZOMblJHWZoqIJddpA== -"@next/swc-linux-x64-musl@14.2.26": - version "14.2.26" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.26.tgz#5d96464d71d2000ec704e650a1a86bb9d73f760d" - integrity sha512-BUsomaO4d2DuXhXhgQCVt2jjX4B4/Thts8nDoIruEJkhE5ifeQFtvW5c9JkdOtYvE5p2G0hcwQ0UbRaQmQwaVg== +"@next/swc-linux-x64-musl@14.2.28": + version "14.2.28" + resolved "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.28.tgz#d1127560ca2aec303daded021b51d9cd49f9f5ca" + integrity sha512-+IuGQKoI3abrXFqx7GtlvNOpeExUH1mTIqCrh1LGFf8DnlUcTmOOCApEnPJUSLrSbzOdsF2ho2KhnQoO0I1RDw== -"@next/swc-win32-arm64-msvc@14.2.26": - version "14.2.26" - resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.26.tgz#859472b532b11499b8f5c2237f54401456286913" - integrity sha512-5auwsMVzT7wbB2CZXQxDctpWbdEnEW/e66DyXO1DcgHxIyhP06awu+rHKshZE+lPLIGiwtjo7bsyeuubewwxMw== +"@next/swc-win32-arm64-msvc@14.2.28": + version "14.2.28" + resolved "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.28.tgz#de2d115304adc5a576816a25fba872ceef270676" + integrity sha512-l61WZ3nevt4BAnGksUVFKy2uJP5DPz2E0Ma/Oklvo3sGj9sw3q7vBWONFRgz+ICiHpW5mV+mBrkB3XEubMrKaA== -"@next/swc-win32-ia32-msvc@14.2.26": - version "14.2.26" - resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.26.tgz#e52e9bd0c43b7a469b03eda6d7a07c3d0c28f549" - integrity sha512-GQWg/Vbz9zUGi9X80lOeGsz1rMH/MtFO/XqigDznhhhTfDlDoynCM6982mPCbSlxJ/aveZcKtTlwfAjwhyxDpg== +"@next/swc-win32-ia32-msvc@14.2.28": + version "14.2.28" + resolved "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.28.tgz#bd15266c1e24965e71faa31a2596693b5dd61944" + integrity sha512-+Kcp1T3jHZnJ9v9VTJ/yf1t/xmtFAc/Sge4v7mVc1z+NYfYzisi8kJ9AsY8itbgq+WgEwMtOpiLLJsUy2qnXZw== -"@next/swc-win32-x64-msvc@14.2.26": - version "14.2.26" - resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.26.tgz#6f42a3ae16ae15c5c5e36efa9b7e291c86ab1275" - integrity sha512-2rdB3T1/Gp7bv1eQTTm9d1Y1sv9UuJ2LAwOE0Pe2prHKe32UNscj7YS13fRB37d0GAiGNR+Y7ZcW8YjDI8Ns0w== +"@next/swc-win32-x64-msvc@14.2.28": + version "14.2.28" + resolved "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.28.tgz#e9de0aec5cda06bfa0e639ad2799829ae6617bf7" + integrity sha512-1gCmpvyhz7DkB1srRItJTnmR2UwQPAUXXIg9r0/56g3O8etGmwlX68skKXJOp9EejW3hhv7nSQUJ2raFiz4MoA== "@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents.3": version "2.1.8-no-fsevents.3" @@ -8419,12 +8419,12 @@ next-themes@^0.2.1: resolved "https://registry.npmjs.org/next-themes/-/next-themes-0.2.1.tgz#0c9f128e847979daf6c67f70b38e6b6567856e45" integrity sha512-B+AKNfYNIzh0vqQQKqQItTS8evEouKD7H5Hj3kmuPERwddR2TxvDSFZuTj6T7Jfn1oyeUyJMydPl1Bkxkh0W7A== -next@^14.2.26: - version "14.2.26" - resolved "https://registry.yarnpkg.com/next/-/next-14.2.26.tgz#b918b3fc5c55e1a67aada1347907675713687721" - integrity sha512-b81XSLihMwCfwiUVRRja3LphLo4uBBMZEzBBWMaISbKTwOmq3wPknIETy/8000tr7Gq4WmbuFYPS7jOYIf+ZJw== +next@^14.2.28: + version "14.2.28" + resolved "https://registry.npmjs.org/next/-/next-14.2.28.tgz#fdc2af93544d90a3915e544b73208c18668af6f9" + integrity sha512-QLEIP/kYXynIxtcKB6vNjtWLVs3Y4Sb+EClTC/CSVzdLD1gIuItccpu/n1lhmduffI32iPGEK2cLLxxt28qgYA== dependencies: - "@next/env" "14.2.26" + "@next/env" "14.2.28" "@swc/helpers" "0.5.5" busboy "1.6.0" caniuse-lite "^1.0.30001579" @@ -8432,15 +8432,15 @@ next@^14.2.26: postcss "8.4.31" styled-jsx "5.1.1" optionalDependencies: - "@next/swc-darwin-arm64" "14.2.26" - "@next/swc-darwin-x64" "14.2.26" - "@next/swc-linux-arm64-gnu" "14.2.26" - "@next/swc-linux-arm64-musl" "14.2.26" - "@next/swc-linux-x64-gnu" "14.2.26" - "@next/swc-linux-x64-musl" "14.2.26" - "@next/swc-win32-arm64-msvc" "14.2.26" - "@next/swc-win32-ia32-msvc" "14.2.26" - "@next/swc-win32-x64-msvc" "14.2.26" + "@next/swc-darwin-arm64" "14.2.28" + "@next/swc-darwin-x64" "14.2.28" + "@next/swc-linux-arm64-gnu" "14.2.28" + "@next/swc-linux-arm64-musl" "14.2.28" + "@next/swc-linux-x64-gnu" "14.2.28" + "@next/swc-linux-x64-musl" "14.2.28" + "@next/swc-win32-arm64-msvc" "14.2.28" + "@next/swc-win32-ia32-msvc" "14.2.28" + "@next/swc-win32-x64-msvc" "14.2.28" no-case@^3.0.4: version "3.0.4" From f23a2f0780969719e57599d69564f60a8833cce9 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Tue, 29 Apr 2025 20:13:55 +0530 Subject: [PATCH 013/201] [WEB-3973] chore: space app state icon size #6995 --- space/core/components/issues/filters/state.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/space/core/components/issues/filters/state.tsx b/space/core/components/issues/filters/state.tsx index 00cf4c0ff..48e308171 100644 --- a/space/core/components/issues/filters/state.tsx +++ b/space/core/components/issues/filters/state.tsx @@ -3,6 +3,7 @@ import React, { useState } from "react"; import { observer } from "mobx-react"; // ui +import { EIconSize } from "@plane/constants"; import { Loader, StateGroupIcon } from "@plane/ui"; // components import { FilterHeader, FilterOption } from "@/components/issues/filters/helpers"; @@ -51,7 +52,7 @@ export const FilterState: React.FC = observer((props) => { key={state.id} isChecked={appliedFilters?.includes(state.id) ? true : false} onClick={() => handleUpdate(state.id)} - icon={} + icon={} title={state.name} /> ))} From 5a1df8b4960ccfe1973fe425619b628fb7a2fe34 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Wed, 30 Apr 2025 14:56:38 +0530 Subject: [PATCH 014/201] [WEB-3560] chore: work item modal code refactor #6996 --- web/ce/components/issues/issue-modal/provider.tsx | 1 + .../issues/issue-modal/context/issue-modal-context.tsx | 1 + 2 files changed, 2 insertions(+) diff --git a/web/ce/components/issues/issue-modal/provider.tsx b/web/ce/components/issues/issue-modal/provider.tsx index 18e122d97..348ab8486 100644 --- a/web/ce/components/issues/issue-modal/provider.tsx +++ b/web/ce/components/issues/issue-modal/provider.tsx @@ -35,6 +35,7 @@ export const IssueModalProvider = observer((props: TIssueModalProviderProps) => handleCreateUpdatePropertyValues: () => Promise.resolve(), handleProjectEntitiesFetch: () => Promise.resolve(), handleTemplateChange: () => Promise.resolve(), + handleConvert: () => Promise.resolve(), }} > {children} diff --git a/web/core/components/issues/issue-modal/context/issue-modal-context.tsx b/web/core/components/issues/issue-modal/context/issue-modal-context.tsx index b73330a93..59d6558a6 100644 --- a/web/core/components/issues/issue-modal/context/issue-modal-context.tsx +++ b/web/core/components/issues/issue-modal/context/issue-modal-context.tsx @@ -63,6 +63,7 @@ export type TIssueModalContext = { handleCreateUpdatePropertyValues: (props: TCreateUpdatePropertyValuesProps) => Promise; handleProjectEntitiesFetch: (props: THandleProjectEntitiesFetchProps) => Promise; handleTemplateChange: (props: THandleTemplateChangeProps) => Promise; + handleConvert: (workspaceSlug: string, data: Partial) => Promise; }; export const IssueModalContext = createContext(undefined); From 1e46290727ec02ed82acf4436a3fe9f0e3ca416e Mon Sep 17 00:00:00 2001 From: Sangeetha Date: Wed, 30 Apr 2025 19:51:04 +0530 Subject: [PATCH 015/201] [WEB-3958] chore: allow members and admins to create api tokens (#6979) * chore: allow members and admins to create api tokens * chore: change permission for service api token --- apiserver/plane/app/views/api.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apiserver/plane/app/views/api.py b/apiserver/plane/app/views/api.py index 732d96832..98a2588a1 100644 --- a/apiserver/plane/app/views/api.py +++ b/apiserver/plane/app/views/api.py @@ -9,11 +9,11 @@ from rest_framework import status from .base import BaseAPIView from plane.db.models import APIToken, Workspace from plane.app.serializers import APITokenSerializer, APITokenReadSerializer -from plane.app.permissions import WorkspaceOwnerPermission +from plane.app.permissions import WorkspaceEntityPermission class ApiTokenEndpoint(BaseAPIView): - permission_classes = [WorkspaceOwnerPermission] + permission_classes = [WorkspaceEntityPermission] def post(self, request, slug): label = request.data.get("label", str(uuid4().hex)) @@ -68,7 +68,7 @@ class ApiTokenEndpoint(BaseAPIView): class ServiceApiTokenEndpoint(BaseAPIView): - permission_classes = [WorkspaceOwnerPermission] + permission_classes = [WorkspaceEntityPermission] def post(self, request, slug): workspace = Workspace.objects.get(slug=slug) From 28f9733d1b026a8dc048f7dbb7eba5934c8b11bc Mon Sep 17 00:00:00 2001 From: Aaron Heckmann Date: Wed, 30 Apr 2025 09:16:59 -0700 Subject: [PATCH 016/201] [WEB-3991] chore: local dev improvements (#6991) * chore: local dev improvements * chore: pr feedback * chore: fix setup * fix: env variables updated in .env.example files * fix(local): sign in to admin and web * chore: update minio deployment to create an bucket automatically on startup. * chore: resolve merge conflict * chore: updated api env with live base path --------- Co-authored-by: sriram veeraghanta Co-authored-by: pablohashescobar --- .gitignore | 1 + .yarnrc.yml | 1 + CONTRIBUTING.md | 34 ++-- README.md | 55 ++----- admin/.env.example | 13 +- admin/package.json | 2 +- apiserver/.env.example | 23 ++- apiserver/plane/authentication/utils/host.py | 20 ++- apiserver/plane/settings/common.py | 4 + apiserver/plane/utils/host.py | 20 ++- docker-compose-local.yml | 164 ++++++++++--------- live/.env.example | 11 +- packages/decorators/package.json | 8 +- setup.sh | 15 +- space/.env.example | 15 +- space/package.json | 2 +- web/.env.example | 12 +- 17 files changed, 234 insertions(+), 166 deletions(-) create mode 100644 .yarnrc.yml diff --git a/.gitignore b/.gitignore index 36f85dc78..0c8956423 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ node_modules .next +.yarn ### NextJS ### # Dependencies diff --git a/.yarnrc.yml b/.yarnrc.yml new file mode 100644 index 000000000..3186f3f07 --- /dev/null +++ b/.yarnrc.yml @@ -0,0 +1 @@ +nodeLinker: node-modules diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 68ef89085..03d667f4a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -35,11 +35,12 @@ This helps us triage and manage issues more efficiently. ### Requirements -- Node.js version v16.18.0 +- Docker Engine installed and running +- Node.js version 20+ [LTS version](https://nodejs.org/en/about/previous-releases) - Python version 3.8+ - Postgres version v14 - Redis version v6.2.7 -- **Memory**: Minimum **12 GB RAM** recommended +- **Memory**: Minimum **12 GB RAM** recommended > ⚠️ Running the project on a system with only 8 GB RAM may lead to setup failures or memory crashes (especially during Docker container build/start or dependency install). Use cloud environments like GitHub Codespaces or upgrade local RAM if possible. ### Setup the project @@ -68,6 +69,17 @@ chmod +x setup.sh docker compose -f docker-compose-local.yml up ``` +5. Start web apps: + +```bash +yarn dev +``` + +6. Open your browser to http://localhost:3001/god-mode/ and register yourself as instance admin +7. Open up your browser to http://localhost:3000 then log in using the same credentials from the previous step + +That’s it! You’re all set to begin coding. Remember to refresh your browser if changes don’t auto-reload. Happy contributing! 🎉 + ## Missing a Feature? If a feature is missing, you can directly _request_ a new one [here](https://github.com/makeplane/plane/issues/new?assignees=&labels=feature&template=feature_request.yml&title=%F0%9F%9A%80+Feature%3A+). You also can do the same by choosing "🚀 Feature" when raising a [New Issue](https://github.com/makeplane/plane/issues/new/choose) on our GitHub Repository. @@ -93,7 +105,7 @@ To ensure consistency throughout the source code, please keep these rules in min - **Improve documentation** - fix incomplete or missing [docs](https://docs.plane.so/), bad wording, examples or explanations. ## Contributing to language support -This guide is designed to help contributors understand how to add or update translations in the application. +This guide is designed to help contributors understand how to add or update translations in the application. ### Understanding translation structure @@ -108,7 +120,7 @@ packages/i18n/src/locales/ ├── fr/ │ └── translations.json └── [language]/ - └── translations.json + └── translations.json ``` #### Nested structure To keep translations organized, we use a nested structure for keys. This makes it easier to manage and locate specific translations. For example: @@ -128,14 +140,14 @@ To keep translations organized, we use a nested structure for keys. This makes i We use [IntlMessageFormat](https://formatjs.github.io/docs/intl-messageformat/) to handle dynamic content, such as variables and pluralization. Here's how to format your translations: #### Examples -- **Simple variables** +- **Simple variables** ```json { "greeting": "Hello, {name}!" } ``` -- **Pluralization** +- **Pluralization** ```json { "items": "{count, plural, one {Work item} other {Work items}}" @@ -160,15 +172,15 @@ We use [IntlMessageFormat](https://formatjs.github.io/docs/intl-messageformat/) ### Adding new languages Adding a new language involves several steps to ensure it integrates seamlessly with the project. Follow these instructions carefully: -1. **Update type definitions** +1. **Update type definitions** Add the new language to the TLanguage type in the language definitions file: ```typescript // types/language.ts export type TLanguage = "en" | "fr" | "your-lang"; - ``` + ``` -2. **Add language configuration** +2. **Add language configuration** Include the new language in the list of supported languages: ```typescript @@ -179,14 +191,14 @@ Include the new language in the list of supported languages: ]; ``` -3. **Create translation files** +3. **Create translation files** 1. Create a new folder for your language under locales (e.g., `locales/your-lang/`). 2. Add a `translations.json` file inside the folder. 3. Copy the structure from an existing translation file and translate all keys. -4. **Update import logic** +4. **Update import logic** Modify the language import logic to include your new language: ```typescript diff --git a/README.md b/README.md index dad8b4558..da57f38d8 100644 --- a/README.md +++ b/README.md @@ -47,10 +47,10 @@ Meet [Plane](https://plane.so/), an open-source project management tool to track Getting started with Plane is simple. Choose the setup that works best for you: -- **Plane Cloud** +- **Plane Cloud** Sign up for a free account on [Plane Cloud](https://app.plane.so)—it's the fastest way to get up and running without worrying about infrastructure. -- **Self-host Plane** +- **Self-host Plane** Prefer full control over your data and infrastructure? Install and run Plane on your own servers. Follow our detailed [deployment guides](https://developers.plane.so/self-hosting/overview) to get started. | Installation methods | Docs link | @@ -62,22 +62,22 @@ Prefer full control over your data and infrastructure? Install and run Plane on ## 🌟 Features -- **Issues** +- **Issues** Efficiently create and manage tasks with a robust rich text editor that supports file uploads. Enhance organization and tracking by adding sub-properties and referencing related issues. -- **Cycles** +- **Cycles** Maintain your team’s momentum with Cycles. Track progress effortlessly using burn-down charts and other insightful tools. -- **Modules** -Simplify complex projects by dividing them into smaller, manageable modules. +- **Modules** +Simplify complex projects by dividing them into smaller, manageable modules. -- **Views** +- **Views** Customize your workflow by creating filters to display only the most relevant issues. Save and share these views with ease. -- **Pages** +- **Pages** Capture and organize ideas using Plane Pages, complete with AI capabilities and a rich text editor. Format text, insert images, add hyperlinks, or convert your notes into actionable items. -- **Analytics** +- **Analytics** Access real-time insights across all your Plane data. Visualize trends, remove blockers, and keep your projects moving forward. - **Drive** (_coming soon_): The drive helps you share documents, images, videos, or any other files that make sense to you or your team and align on the problem/solution. @@ -85,38 +85,7 @@ Access real-time insights across all your Plane data. Visualize trends, remove b ## 🛠️ Local development -### Pre-requisites -- Ensure Docker Engine is installed and running. - -### Development setup -Setting up your local environment is simple and straightforward. Follow these steps to get started: - -1. Clone the repository: - ``` - git clone https://github.com/makeplane/plane.git - ``` -2. Navigate to the project folder: - ``` - cd plane - ``` -3. Create a new branch for your feature or fix: - ``` - git checkout -b - ``` -4. Run the setup script in the terminal: - ``` - ./setup.sh - ``` -5. Open the project in an IDE such as VS Code. - -6. Review the `.env` files in the relevant folders. Refer to [Environment Setup](./ENV_SETUP.md) for details on the environment variables used. - -7. Start the services using Docker: - ``` - docker compose -f docker-compose-local.yml up -d - ``` - -That’s it! You’re all set to begin coding. Remember to refresh your browser if changes don’t auto-reload. Happy contributing! 🎉 +See [CONTRIBUTING](./CONTRIBUTING.md) ## ⚙️ Built with [![Next JS](https://img.shields.io/badge/next.js-000000?style=for-the-badge&logo=nextdotjs&logoColor=white)](https://nextjs.org/) @@ -194,7 +163,7 @@ Feel free to ask questions, report bugs, participate in discussions, share ideas If you discover a security vulnerability in Plane, please report it responsibly instead of opening a public issue. We take all legitimate reports seriously and will investigate them promptly. See [Security policy](https://github.com/makeplane/plane/blob/master/SECURITY.md) for more info. -To disclose any security issues, please email us at security@plane.so. +To disclose any security issues, please email us at security@plane.so. ## 🤝 Contributing @@ -219,4 +188,4 @@ Please read [CONTRIBUTING.md](https://github.com/makeplane/plane/blob/master/CON ## License -This project is licensed under the [GNU Affero General Public License v3.0](https://github.com/makeplane/plane/blob/master/LICENSE.txt). \ No newline at end of file +This project is licensed under the [GNU Affero General Public License v3.0](https://github.com/makeplane/plane/blob/master/LICENSE.txt). diff --git a/admin/.env.example b/admin/.env.example index fdeb05c4d..15d7a36a9 100644 --- a/admin/.env.example +++ b/admin/.env.example @@ -1,3 +1,12 @@ -NEXT_PUBLIC_API_BASE_URL="" +NEXT_PUBLIC_API_BASE_URL="http://localhost:8000" + +NEXT_PUBLIC_WEB_BASE_URL="http://localhost:3000" + +NEXT_PUBLIC_ADMIN_BASE_URL="http://localhost:3001" NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode" -NEXT_PUBLIC_WEB_BASE_URL="" \ No newline at end of file + +NEXT_PUBLIC_SPACE_BASE_URL="http://localhost:3002" +NEXT_PUBLIC_SPACE_BASE_PATH="/spaces" + +NEXT_PUBLIC_LIVE_BASE_URL="http://localhost:3100" +NEXT_PUBLIC_LIVE_BASE_PATH="/live" diff --git a/admin/package.json b/admin/package.json index a809ef1b1..9a7bff005 100644 --- a/admin/package.json +++ b/admin/package.json @@ -17,10 +17,10 @@ "@headlessui/react": "^1.7.19", "@plane/constants": "*", "@plane/hooks": "*", + "@plane/services": "*", "@plane/types": "*", "@plane/ui": "*", "@plane/utils": "*", - "@plane/services": "*", "@tailwindcss/typography": "^0.5.9", "@types/lodash": "^4.17.0", "autoprefixer": "10.4.14", diff --git a/apiserver/.env.example b/apiserver/.env.example index b56494c35..7fdffd179 100644 --- a/apiserver/.env.example +++ b/apiserver/.env.example @@ -1,7 +1,7 @@ # Backend # Debug value for api server use it as 0 for production use DEBUG=0 -CORS_ALLOWED_ORIGINS="http://localhost" +CORS_ALLOWED_ORIGINS="http://localhost:3000,http://localhost:3001,http://localhost:3002,http://localhost:3100" # Database Settings POSTGRES_USER="plane" @@ -27,7 +27,7 @@ RABBITMQ_VHOST="plane" AWS_REGION="" AWS_ACCESS_KEY_ID="access-key" AWS_SECRET_ACCESS_KEY="secret-key" -AWS_S3_ENDPOINT_URL="http://plane-minio:9000" +AWS_S3_ENDPOINT_URL="http://localhost:9000" # Changing this requires change in the nginx.conf for uploads if using minio setup AWS_S3_BUCKET_NAME="uploads" # Maximum file upload limit @@ -37,22 +37,31 @@ FILE_SIZE_LIMIT=5242880 DOCKERIZED=1 # deprecated # set to 1 If using the pre-configured minio setup -USE_MINIO=1 +USE_MINIO=0 # Nginx Configuration NGINX_PORT=80 # Email redirections and minio domain settings -WEB_URL="http://localhost" +WEB_URL="http://localhost:8000" # Gunicorn Workers GUNICORN_WORKERS=2 # Base URLs -ADMIN_BASE_URL= -SPACE_BASE_URL= -APP_BASE_URL= +ADMIN_BASE_URL="http://localhost:3001" +ADMIN_BASE_PATH="/god-mode" +SPACE_BASE_URL="http://localhost:3002" +SPACE_BASE_PATH="/spaces" + +APP_BASE_URL="http://localhost:3000" +APP_BASE_PATH="" + +LIVE_BASE_URL="http://localhost:3100" +LIVE_BASE_PATH="/live" + +LIVE_SERVER_SECRET_KEY="secret-key" # Hard delete files after days HARD_DELETE_AFTER_DAYS=60 diff --git a/apiserver/plane/authentication/utils/host.py b/apiserver/plane/authentication/utils/host.py index c4625279c..64f191685 100644 --- a/apiserver/plane/authentication/utils/host.py +++ b/apiserver/plane/authentication/utils/host.py @@ -14,17 +14,29 @@ def base_host(request: Request | HttpRequest, is_admin: bool = False, is_space: # Admin redirections if is_admin: + admin_base_path = getattr(settings, "ADMIN_BASE_PATH", "/god-mode/") + if not admin_base_path.startswith("/"): + admin_base_path = "/" + admin_base_path + if not admin_base_path.endswith("/"): + admin_base_path += "/" + if settings.ADMIN_BASE_URL: - return settings.ADMIN_BASE_URL + return settings.ADMIN_BASE_URL + admin_base_path else: - return base_origin + "/god-mode/" + return base_origin + admin_base_path # Space redirections if is_space: + space_base_path = getattr(settings, "SPACE_BASE_PATH", "/spaces/") + if not space_base_path.startswith("/"): + space_base_path = "/" + space_base_path + if not space_base_path.endswith("/"): + space_base_path += "/" + if settings.SPACE_BASE_URL: - return settings.SPACE_BASE_URL + return settings.SPACE_BASE_URL + space_base_path else: - return base_origin + "/spaces/" + return base_origin + space_base_path # App Redirection if is_app: diff --git a/apiserver/plane/settings/common.py b/apiserver/plane/settings/common.py index 76db7a928..86a592128 100644 --- a/apiserver/plane/settings/common.py +++ b/apiserver/plane/settings/common.py @@ -312,9 +312,13 @@ CSRF_FAILURE_VIEW = "plane.authentication.views.common.csrf_failure" # Base URLs ADMIN_BASE_URL = os.environ.get("ADMIN_BASE_URL", None) +ADMIN_BASE_PATH = os.environ.get("ADMIN_BASE_PATH", None) SPACE_BASE_URL = os.environ.get("SPACE_BASE_URL", None) +SPACE_BASE_PATH = os.environ.get("SPACE_BASE_PATH", None) APP_BASE_URL = os.environ.get("APP_BASE_URL") +APP_BASE_PATH = os.environ.get("APP_BASE_PATH", None) LIVE_BASE_URL = os.environ.get("LIVE_BASE_URL") +LIVE_BASE_PATH = os.environ.get("LIVE_BASE_PATH") WEB_URL = os.environ.get("WEB_URL") HARD_DELETE_AFTER_DAYS = int(os.environ.get("HARD_DELETE_AFTER_DAYS", 60)) diff --git a/apiserver/plane/utils/host.py b/apiserver/plane/utils/host.py index c4914d7ff..7c8635836 100644 --- a/apiserver/plane/utils/host.py +++ b/apiserver/plane/utils/host.py @@ -19,17 +19,29 @@ def base_host(request: Request | HttpRequest, is_admin: bool = False, is_space: # Admin redirections if is_admin: + admin_base_path = getattr(settings, "ADMIN_BASE_PATH", "/god-mode/") + if not admin_base_path.startswith("/"): + admin_base_path = "/" + admin_base_path + if not admin_base_path.endswith("/"): + admin_base_path += "/" + if settings.ADMIN_BASE_URL: - return settings.ADMIN_BASE_URL + return settings.ADMIN_BASE_URL + admin_base_path else: - return base_origin + "/god-mode/" + return base_origin + admin_base_path # Space redirections if is_space: + space_base_path = getattr(settings, "SPACE_BASE_PATH", "/spaces/") + if not space_base_path.startswith("/"): + space_base_path = "/" + space_base_path + if not space_base_path.endswith("/"): + space_base_path += "/" + if settings.SPACE_BASE_URL: - return settings.SPACE_BASE_URL + return settings.SPACE_BASE_URL + space_base_path else: - return base_origin + "/spaces/" + return base_origin + space_base_path # App Redirection if is_app: diff --git a/docker-compose-local.yml b/docker-compose-local.yml index 392c55a11..86457615a 100644 --- a/docker-compose-local.yml +++ b/docker-compose-local.yml @@ -6,6 +6,8 @@ services: - dev_env volumes: - redisdata:/data + ports: + - "6379:6379" plane-mq: image: rabbitmq:3.13.6-management-alpine @@ -26,7 +28,15 @@ services: restart: unless-stopped networks: - dev_env - command: server /export --console-address ":9090" + entrypoint: > + /bin/sh -c " + mkdir -p /export/${AWS_S3_BUCKET_NAME} && + minio server /export --console-address ':9090' & + sleep 5 && + mc alias set myminio http://localhost:9000 ${AWS_ACCESS_KEY_ID} ${AWS_SECRET_ACCESS_KEY} && + mc mb myminio/${AWS_S3_BUCKET_NAME} -p || true + && tail -f /dev/null + " volumes: - uploads:/export env_file: @@ -34,6 +44,9 @@ services: environment: MINIO_ROOT_USER: ${AWS_ACCESS_KEY_ID} MINIO_ROOT_PASSWORD: ${AWS_SECRET_ACCESS_KEY} + ports: + - "9000:9000" + - "9090:9090" plane-db: image: postgres:15.7-alpine @@ -47,63 +60,65 @@ services: - .env environment: PGDATA: /var/lib/postgresql/data + ports: + - "5432:5432" - web: - build: - context: . - dockerfile: ./web/Dockerfile.dev - restart: unless-stopped - networks: - - dev_env - volumes: - - ./web:/app/web - env_file: - - ./web/.env - depends_on: - - api - - worker + # web: + # build: + # context: . + # dockerfile: ./web/Dockerfile.dev + # restart: unless-stopped + # networks: + # - dev_env + # volumes: + # - ./web:/app/web + # env_file: + # - ./web/.env + # depends_on: + # - api + # - worker - space: - build: - context: . - dockerfile: ./space/Dockerfile.dev - restart: unless-stopped - networks: - - dev_env - volumes: - - ./space:/app/space - depends_on: - - api - - worker - - web + # space: + # build: + # context: . + # dockerfile: ./space/Dockerfile.dev + # restart: unless-stopped + # networks: + # - dev_env + # volumes: + # - ./space:/app/space + # depends_on: + # - api + # - worker + # - web - admin: - build: - context: . - dockerfile: ./admin/Dockerfile.dev - restart: unless-stopped - networks: - - dev_env - volumes: - - ./admin:/app/admin - depends_on: - - api - - worker - - web + # admin: + # build: + # context: . + # dockerfile: ./admin/Dockerfile.dev + # restart: unless-stopped + # networks: + # - dev_env + # volumes: + # - ./admin:/app/admin + # depends_on: + # - api + # - worker + # - web - live: - build: - context: . - dockerfile: ./live/Dockerfile.dev - restart: unless-stopped - networks: - - dev_env - volumes: - - ./live:/app/live - depends_on: - - api - - worker - - web + # live: + # build: + # context: . + # dockerfile: ./live/Dockerfile.dev + # restart: unless-stopped + # networks: + # - dev_env + # volumes: + # - ./live:/app/live + # depends_on: + # - api + # - worker + # - web api: build: @@ -122,6 +137,9 @@ services: depends_on: - plane-db - plane-redis + - plane-mq + ports: + - "8000:8000" worker: build: @@ -179,25 +197,25 @@ services: - plane-db - plane-redis - proxy: - build: - context: ./nginx - dockerfile: Dockerfile.dev - restart: unless-stopped - networks: - - dev_env - ports: - - ${NGINX_PORT}:80 - env_file: - - .env - environment: - FILE_SIZE_LIMIT: ${FILE_SIZE_LIMIT:-5242880} - BUCKET_NAME: ${AWS_S3_BUCKET_NAME:-uploads} - depends_on: - - web - - api - - space - - admin + # proxy: + # build: + # context: ./nginx + # dockerfile: Dockerfile.dev + # restart: unless-stopped + # networks: + # - dev_env + # ports: + # - ${NGINX_PORT}:80 + # env_file: + # - .env + # environment: + # FILE_SIZE_LIMIT: ${FILE_SIZE_LIMIT:-5242880} + # BUCKET_NAME: ${AWS_S3_BUCKET_NAME:-uploads} + # depends_on: + # - api + # - web + # - space + # - admin volumes: redisdata: diff --git a/live/.env.example b/live/.env.example index 3203db847..064258253 100644 --- a/live/.env.example +++ b/live/.env.example @@ -1,8 +1,13 @@ -API_BASE_URL="http://api:8000" +API_BASE_URL="http://localhost:8000" + +WEB_BASE_URL="http://localhost:3000" + +LIVE_BASE_URL="http://localhost:3100" LIVE_BASE_PATH="/live" -REDIS_URL="redis://plane-redis:6379/" +LIVE_SERVER_SECRET_KEY="secret-key" # If you prefer not to provide a Redis URL, you can set the REDIS_HOST and REDIS_PORT environment variables instead. REDIS_PORT=6379 -REDIS_HOST=plane-redis \ No newline at end of file +REDIS_HOST=localhost +REDIS_URL="redis://localhost:6379/" diff --git a/packages/decorators/package.json b/packages/decorators/package.json index 92c49b969..433b5c11a 100644 --- a/packages/decorators/package.json +++ b/packages/decorators/package.json @@ -17,15 +17,15 @@ "lint:errors": "eslint src --ext .ts,.tsx --quiet" }, "dependencies": { - "reflect-metadata": "^0.2.2", - "express": "^4.21.2" + "express": "^4.21.2", + "reflect-metadata": "^0.2.2" }, "devDependencies": { "@plane/eslint-config": "*", - "@types/express": "^4.17.21", - "@types/reflect-metadata": "^0.1.0", "@plane/typescript-config": "*", + "@types/express": "^4.17.21", "@types/node": "^20.14.9", + "@types/reflect-metadata": "^0.1.0", "@types/ws": "^8.5.10", "tsup": "8.4.0", "typescript": "^5.3.3" diff --git a/setup.sh b/setup.sh index 2dcaba80f..c19a29abf 100755 --- a/setup.sh +++ b/setup.sh @@ -22,14 +22,14 @@ echo -e "${BOLD}Setting up your development environment...${NC}\n" copy_env_file() { local source=$1 local destination=$2 - + if [ ! -f "$source" ]; then echo -e "${RED}Error: Source file $source does not exist.${NC}" return 1 fi - + cp "$source" "$destination" - + if [ $? -eq 0 ]; then echo -e "${GREEN}✓${NC} Copied $destination" else @@ -52,7 +52,7 @@ for service in "${services[@]}"; do if [ "$service" != "" ]; then prefix="./$service/" fi - + copy_env_file "${prefix}.env.example" "${prefix}.env" || success=false done @@ -60,7 +60,7 @@ done if [ -f "./apiserver/.env" ]; then echo -e "\n${YELLOW}Generating Django SECRET_KEY...${NC}" SECRET_KEY=$(tr -dc 'a-z0-9' < /dev/urandom | head -c50) - + if [ -z "$SECRET_KEY" ]; then echo -e "${RED}Error: Failed to generate SECRET_KEY.${NC}" echo -e "${RED}Ensure 'tr' and 'head' commands are available on your system.${NC}" @@ -74,6 +74,11 @@ else success=false fi +# Activate Yarn (version set in package.json) +corepack enable yarn || success=false +# Install Node dependencies +yarn install || success=false + # Summary echo -e "\n${YELLOW}Setup status:${NC}" if [ "$success" = true ]; then diff --git a/space/.env.example b/space/.env.example index 33939c7e2..15d7a36a9 100644 --- a/space/.env.example +++ b/space/.env.example @@ -1,3 +1,12 @@ -NEXT_PUBLIC_API_BASE_URL="" -NEXT_PUBLIC_WEB_BASE_URL="" -NEXT_PUBLIC_SPACE_BASE_PATH="/spaces" \ No newline at end of file +NEXT_PUBLIC_API_BASE_URL="http://localhost:8000" + +NEXT_PUBLIC_WEB_BASE_URL="http://localhost:3000" + +NEXT_PUBLIC_ADMIN_BASE_URL="http://localhost:3001" +NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode" + +NEXT_PUBLIC_SPACE_BASE_URL="http://localhost:3002" +NEXT_PUBLIC_SPACE_BASE_PATH="/spaces" + +NEXT_PUBLIC_LIVE_BASE_URL="http://localhost:3100" +NEXT_PUBLIC_LIVE_BASE_PATH="/live" diff --git a/space/package.json b/space/package.json index 25db45895..4dd54ca6e 100644 --- a/space/package.json +++ b/space/package.json @@ -22,9 +22,9 @@ "@plane/constants": "*", "@plane/editor": "*", "@plane/i18n": "*", + "@plane/services": "*", "@plane/types": "*", "@plane/ui": "*", - "@plane/services": "*", "axios": "^1.8.3", "clsx": "^2.0.0", "date-fns": "^4.1.0", diff --git a/web/.env.example b/web/.env.example index ad5ac4173..15d7a36a9 100644 --- a/web/.env.example +++ b/web/.env.example @@ -1,10 +1,12 @@ -NEXT_PUBLIC_API_BASE_URL="" +NEXT_PUBLIC_API_BASE_URL="http://localhost:8000" -NEXT_PUBLIC_ADMIN_BASE_URL="" +NEXT_PUBLIC_WEB_BASE_URL="http://localhost:3000" + +NEXT_PUBLIC_ADMIN_BASE_URL="http://localhost:3001" NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode" -NEXT_PUBLIC_SPACE_BASE_URL="" +NEXT_PUBLIC_SPACE_BASE_URL="http://localhost:3002" NEXT_PUBLIC_SPACE_BASE_PATH="/spaces" -NEXT_PUBLIC_LIVE_BASE_URL="" -NEXT_PUBLIC_LIVE_BASE_PATH="/live" \ No newline at end of file +NEXT_PUBLIC_LIVE_BASE_URL="http://localhost:3100" +NEXT_PUBLIC_LIVE_BASE_PATH="/live" From dbc00e4add98eac6ba88ec372a04c6797e8b9632 Mon Sep 17 00:00:00 2001 From: Sangeetha Date: Thu, 1 May 2025 19:22:00 +0530 Subject: [PATCH 017/201] [WEB-3992] chore: support for x-zip-compressed type #7001 --- apiserver/plane/settings/common.py | 1 + 1 file changed, 1 insertion(+) diff --git a/apiserver/plane/settings/common.py b/apiserver/plane/settings/common.py index 86a592128..67f51f7bf 100644 --- a/apiserver/plane/settings/common.py +++ b/apiserver/plane/settings/common.py @@ -377,6 +377,7 @@ ATTACHMENT_MIME_TYPES = [ # Archives "application/zip", "application/x-rar-compressed", + "application/x-zip-compressed", "application/x-tar", "application/gzip", # 3D Models From fbca9d9a7a85ffa27ccc53234ddf35dd8e95c147 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Fri, 2 May 2025 16:53:06 +0530 Subject: [PATCH 018/201] [WEB-3996] fix: attachment icon rendering and added support for rar and zip icons (#7007) * chore: zip and rar file icon * chore: zip and rar file icon * fix: attachment icon * chore: application/x-rar type added * fix: compressed file extensions * chore: updated file upload extensions --------- Co-authored-by: sriram veeraghanta --- apiserver/plane/settings/common.py | 13 ++++++++++++- .../icons/attachment/attachment-icon.tsx | 6 ++++++ web/core/components/icons/attachment/index.ts | 2 ++ .../icons/attachment/rar-file-icon.tsx | 10 ++++++++++ .../icons/attachment/zip-file-icon.tsx | 10 ++++++++++ .../issues/attachment/attachment-list-item.tsx | 2 +- web/public/attachment/rar-icon.png | Bin 0 -> 10465 bytes web/public/attachment/zip-icon.png | Bin 0 -> 7217 bytes 8 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 web/core/components/icons/attachment/rar-file-icon.tsx create mode 100644 web/core/components/icons/attachment/zip-file-icon.tsx create mode 100644 web/public/attachment/rar-icon.png create mode 100644 web/public/attachment/zip-icon.png diff --git a/apiserver/plane/settings/common.py b/apiserver/plane/settings/common.py index 67f51f7bf..15d7a21b3 100644 --- a/apiserver/plane/settings/common.py +++ b/apiserver/plane/settings/common.py @@ -376,10 +376,21 @@ ATTACHMENT_MIME_TYPES = [ "video/x-ms-wmv", # Archives "application/zip", + "application/x-rar", "application/x-rar-compressed", - "application/x-zip-compressed", "application/x-tar", "application/gzip", + "application/x-zip", + "application/x-zip-compressed", + "application/x-7z-compressed", + "application/x-compressed", + "application/x-compressed-tar", + "application/x-compressed-tar-gz", + "application/x-compressed-tar-bz2", + "application/x-compressed-tar-zip", + "application/x-compressed-tar-7z", + "application/x-compressed-tar-rar", + "application/x-compressed-tar-zip", # 3D Models "model/gltf-binary", "model/gltf+json", diff --git a/web/core/components/icons/attachment/attachment-icon.tsx b/web/core/components/icons/attachment/attachment-icon.tsx index 61835abab..e21ae8893 100644 --- a/web/core/components/icons/attachment/attachment-icon.tsx +++ b/web/core/components/icons/attachment/attachment-icon.tsx @@ -10,10 +10,12 @@ import { JpgIcon, PdfIcon, PngIcon, + RarIcon, SheetIcon, SvgIcon, TxtIcon, VideoIcon, + ZipIcon, } from "@/components/icons/attachment"; export const getFileIcon = (fileType: string, size: number = 28) => { @@ -52,6 +54,10 @@ export const getFileIcon = (fileType: string, size: number = 28) => { return ; case "mkv": return ; + case "zip": + return ; + case "rar": + return ; default: return ; diff --git a/web/core/components/icons/attachment/index.ts b/web/core/components/icons/attachment/index.ts index f7f1e6ed3..a2c65ceef 100644 --- a/web/core/components/icons/attachment/index.ts +++ b/web/core/components/icons/attachment/index.ts @@ -18,3 +18,5 @@ export * from "./svg-file-icon"; export * from "./tune-icon"; export * from "./txt-file-icon"; export * from "./video-file-icon"; +export * from "./zip-file-icon"; +export * from "./rar-file-icon"; diff --git a/web/core/components/icons/attachment/rar-file-icon.tsx b/web/core/components/icons/attachment/rar-file-icon.tsx new file mode 100644 index 000000000..0c3dd88e1 --- /dev/null +++ b/web/core/components/icons/attachment/rar-file-icon.tsx @@ -0,0 +1,10 @@ +import React from "react"; +import Image from "next/image"; +// image +import RarFileIcon from "@/public/attachment/rar-icon.png"; +// type +import type { ImageIconPros } from "../types"; + +export const RarIcon: React.FC = ({ width, height }) => ( + RarFileIcon +); diff --git a/web/core/components/icons/attachment/zip-file-icon.tsx b/web/core/components/icons/attachment/zip-file-icon.tsx new file mode 100644 index 000000000..8f5d0388b --- /dev/null +++ b/web/core/components/icons/attachment/zip-file-icon.tsx @@ -0,0 +1,10 @@ +import React from "react"; +import Image from "next/image"; +// image +import ZipFileIcon from "@/public/attachment/zip-icon.png"; +// type +import type { ImageIconPros } from "../types"; + +export const ZipIcon: React.FC = ({ width, height }) => ( + ZipFileIcon +); diff --git a/web/core/components/issues/attachment/attachment-list-item.tsx b/web/core/components/issues/attachment/attachment-list-item.tsx index 027369405..0458f4485 100644 --- a/web/core/components/issues/attachment/attachment-list-item.tsx +++ b/web/core/components/issues/attachment/attachment-list-item.tsx @@ -38,7 +38,7 @@ export const IssueAttachmentsListItem: FC = observer( // derived values const attachment = attachmentId ? getAttachmentById(attachmentId) : undefined; const fileName = getFileName(attachment?.attributes.name ?? ""); - const fileExtension = getFileExtension(attachment?.asset_url ?? ""); + const fileExtension = getFileExtension(attachment?.attributes.name ?? ""); const fileIcon = getFileIcon(fileExtension, 18); const fileURL = getFileURL(attachment?.asset_url ?? ""); // hooks diff --git a/web/public/attachment/rar-icon.png b/web/public/attachment/rar-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..7305455bd9a56e904db4677238d9f343042af8b4 GIT binary patch literal 10465 zcmcI}bySq!xA!x{fP{35!T^G#ARwUB00SzW(jg@x-7v&Z0!m7Qgdm;LAq`RjA|)l= z-3|A_@4MD}*Sqc?zx%uIU2E3NS?BD%&yLTI=b2z7g=hG-K*9hN46;$wvS=rf^qqfcMgIOc92j(c04p&tNjRpo)(SW(5 zJ5O#Z;h{KqU{hB&&vSONrK8u_NX@Mz8~MAhuW$V9tUEvN{VmAPj7lpFf}`Ww9l36cVhc6Sl9 zt5A6QBo4aK;VuFf2Lo;mLlB+`5)eTyB7v1r6P^YtVhyMv0dZ5hEIA-6+cSzWb6rE# z*&;&@B+Ei<`LMVTLTH*n zX7nOJCFlahh5!*!^8gG$s4xKFIvN`R0QZ@oL;&z2_0A-)Lidv z#=k#*53O{gHIWPq4geB=^<^PO0|54413n`W{NSM+Y=Hy-W|0VH2!O`Mz7K#=@__*W z13Ur%5HJ{c7=%WE_$4NK4ghc{6bI0Rl&zL$&37dWr1s(>5m*p_eP)!BYyD(qr%&ko zv>h`p~>?*(OfVNOoD+g#ZL-k|Bjukpe)0F!7EJ z6m$c_Oe%Q#2-!3Z{wO1x-h!Kg3~4f;b6&u(|2h)_AXtb&zk<#LX+9%Czrv%jL16fk zKtO6G1_aPlUv+(=9EJa`)#8=$+p;bZ6d@f&5Zam)C_zr3>g4$~w>9uuLqnO5EjKMG z{~iF7Iw0ds$o}P9{uN?(QpozcUq4#}fM&X*r^`R)__UaUIm#{yRa7h;Q!u)&?p~fcGzPMw4*7<9#f`{CMHsFudj<)F<=dohpW^0Migu}z_~(iT@RtZSai zuR!C~Jl);h%XP%TB}Yg{eK>m%0H6c(x0haE5(7X9cWPWC=p03xGGEXrO-&tk2mrW! z1JU*A)PwdgiR$nb1OT_bg8VmdyTREFY5<@AyU|4AxziQJJTdtvkHYznXLgs!w)ep+ z$P_au7{uvcfP%;bZt(l>ZJ?Q+x;ampjB1Pyx3r(SMWMXhc2yx$BV2}Wq$%TunaaA# zr9Vs!8w#?dP2A}I|0wuxY&4NrZEy0A`H44bQ{4`f-x_mAW&Z1a|hqL zL>}~~QvS38a~^alT5RlVTr`$1H*Q7@Rl6_|>=rQ&G3>aJwb>_a24iB2qWK;Sw{`_Z zFzI$#7VdjfDgK9nz_~|DmuFc&6@ABfR73qwnHVkJ&q{R&b6Kq**JF;@4=Sx=_J`NZ zhF2H=f5H%aR>aPw*e_prz9oZiK$qG(#ZO=ifdO(w*VNtT>pH45=bv9r^0`cGvjAXj z0-%LpnxnxLh)+29N%-#oF#e}|Rr2CPfpuuPJkaESbR4L)wX{_USb_ED2Kn9_lT!*4 z+*Exw#V0-#7x zA0jxY8yWzU4uF9<2*44bh5&>C{K6&z@q<8v3HYy*rUmR>>UTL7or_i7xjxBkoGwbz zn)bHvxYW6}k=E{`Gv#FmriMt-BP<_O6r*xSKD>UiJS4cRrm0itSf|Q^`q{oL>sfu4 z&O(AFcm)N@k&!x6(;?+DvG#qd_-f}IOW}!=VfiP(&wDW7C)8BSf~PiL`nn^+x-xO< zGT#?K2oM1Xy!(SSrH;kMQ$hn2hd?~0O%)odzkv0gA#qj2%s^fBJa$9T07ys61(`kI27G! z3xR+VXt}>CtLkdyDNL?+J*f9GaO(b*%ldsM5sD2oVSN8)1>AlodAeXCsFYmF0&Y2c z;+R2vZ?kw`QM2|bCAEuEnQZmoGGXoaB!4BH3lRnkmu{%B-GYAITPn#l%8c(L6XSXJGREYx@EaIJGRWFQ-o}Sj4;B zxSS|+p!1vMT#msAg7|{pXet?yBzin_%Mx!jz>Vr1VTNHMvsucI9`AFz!7W4p+R86T z6CiriTFRMYJ6ID-d3n%^qiA@vsv+skUU~Ocf(_;QX2@bP=9IODGNg$ZU{Fa@>GC!x zq=2=0y!lgqsrSv}$nWDq__XNOKD(P;brnTfKyF zO6>$#;>b2)KsqT}M(Z@TV9lydx7%W3q~nXGpwiLN^~73f<)+Cbb#W+Qiw_JebG}#Q z$A$YhElS<9wsrG@sQZ4#AtJ^D7}U!*((Y8$>fRRYbJw|}6Uv@5=dT42eFfmZl(@-! z8SDwJOPzh}UKWo;>+3G~OTZZ@0OYt`3;l_lnA+*im62ufc)hjLPYV{^i)EM__FthQ z)L2AeQfcprTykQ%sRZT}E{_R1(*I#qnNoI2wBoZ^rh2fv$ad9GqpqzPU=;PwVQDg) zV?sqZ?n<8^H{37ZT~eCq!QIF@j-Vh+AaaBum^J4VNcXi}y8LxO^-7LL5yO4JeUI% z@qik%f7yH9} zsdpG3i{UXa*y5t?RP6(8+$AS}Ty{EuQCIt^o0CO<3O8*&U*uGhvV*9Tb$Sfmx`gIc&ZGOSTOD%_#fWt|`=~EPZgRXAj0qMK1 zsa7~~4=%(7KCj#-Nq9q~Ob1G+@#|l1{W0gOOF1J|UqQ5bszvnX-HDclfErRsE?BSIESh_(8v_sVLyY zd%2{vlb3Lsq=-hwT@Xd4-01pFmrU?@YSgx6Yn=Y)`T_4WM+@#aoWT=Jr#tk67lHM4 zZ>$)CFa(9`#^kjL$w_XP(;LZb9E(l(k+Yf)`D$mTo9D-Q1$^UU6mmUITd`hpn^7!E zk}PfXOxAZ`h9|Rep{7J>l!l%5MSewRHd2@iHL95VEakYl^WYsnYwerj&>*9?I$qfQ z!RoyNHn`1*mj^VoVXd;nkUqw&R0vDOhje5`rjm% z&x<{u5J*vw@cv1NIK~FdbC;NFcMn+Pmcm<_&EsPaFH`es6%@JT6vL7>OXqLZw%Rvk z^*Er;IrY73LbeT3JKD#hyh|;&^IvE0wzhw8-5>r{Z+85kY7t$=0Q@MDBgL35E}qNo z5!l`AS+?6f*oHOFZSXbdeCp_@N++av-NE_aLx!OHa=vbCkU)Mq8k4(dcQe^Tl{?mj16wtM%)CmrI?(wQUdmMq0Wx zRGIO%f^m9n3pdcHM0_`^$&mcLENhUFuC1(IH&tZwQTTo4iZ{lEkBut6k66r)ATx-y z>sUuJ`NySnXezS4e|acYz?>L=^sEOc%wW&~pZ&4BOB@ZC_$J$8`I?&&dTis4Uo=cd zp?DJ9wne5-x9%vP%;6>KF&4(0AG{gZ-TAB*Q=pSSw!S7mOe@0X+qJzPeDn@$zIubsNleb;#;YJYSavx zr8}i~tYST!4dq>ijSTEy{FLDE80G1H8_;v&dyI3un%Wln$uv<QnMtljspZ)P;>BKAA4{{A$H`aI(ue<(aqr zIA2aXzW^zj$4FPyqoo~Qpe@YuXrRQfN=M?v2cJSCk5wB3PsBj>`X%EEJQa07_$cG0XVO z*;$*$SqN^qV~EVk6u06}!HP(;#zr|md&tlcev23bSd~V3idZjktlLfLpFfllD0tR> zWR-KZ2Vd(?k$S7e3Wp6DQuAU3PgK&_Ti0<84m_BeAIVjH)Z~xztNu>a zXra{U2xrcCJ>Hh8So&)C^z5;_`db3Ivl+?TAs;Y^?T8{CVnHtTR13S8DPQ+fx;SsY zw1O)S|EW*Zdpl;);H7sEBUC3kxZ!j>0Ea)8mS-O%lNBhSB=kC0$8I!$<#nx+VU}$s z>e+ATx`#^~?u5qu%F6xyT?Dt(Q z&)ULQ$%4cf^Yh|++OpQ47dD4;e)I%17UGxA85sEP5Hlj$07TZpTkDr+FpKcJI5~9i zkw{F&5>vCIQ@McnCk_aoI`3Ji%(PCnN1$ozdWm}eUyhd!Cq!#ubb$r_G3?QFY9chz z(IG)!?mPNP`tWtCs`;0O(!ifSg)=^#B6vG~AB0att~%WXlr~${{&%-hTVK*GktnK5 z_I4iU0v@A;p;=arwkeEr`WFfIFK!fK_7GKDT5KNu1*MiWa8n=cfEx4g>;!F9-I4q5 zxfX4&t7PAjxwjA)1VPL7>gf2@(uIjjBaIjeNC(rNMb3Z6JS>`wH4RC@=_P`ypPmwV zHOd*h+rza4;lxv48}%TQCO>`i%wt`zB>O;0II6QwA*LcGE@OAkv@^&{RBu$Vc*Twi z2y*{1J^uc?Y~yo&^;O$Hp62EP$)}cBLrG_4Vv@f0d=5Wz$Zkwq6{&W{=V=MVS2bH* z4|e)>e=*cf#ov$){m%810#j3WpE=wJfDgWX`0!mp(1OXz3rpa9H&mmz1+`6kpGD+W z?2uZ7I}#%yDlk(Ii2Np)3%)KDy5t{bff8cez3Eo@0S3mPv{kJnQLhoNcfmT zlZE6tT1f|k2JR@Mb0#hxl@vi`R3akTBDd`6)tcOe+7?;XeeSQnBl8Y46KgO;(-ZSn$8B%s4wRi6v1zNg+PRU zuK)cTZJNjrZ8GW)QQm@$avm~xH4ixy_4UDLKS<9|I6KcQ9y*ajE$Zs>JXM||SQF)Z zKb2T#+}I4Y)yqXl5Y{mQE6wnbF|Fq@_G1$gh+!{ga$|uezuKag?d#(6Z*GI-1tQSP z4Ab3bMnB@k5FsN8%=pmT+t`BWlNOp`;7cVus9vW?%(9ROq5?Yi53ams2@0SBCA{a; z+Iq_5%T*7LkA4b2_4OB{kaV;sIp97(AST6a2Q7R+ga|8=UOl(5h_iiaaNpObYA}M$ z)SC)BdirnQ6QK*Etwoj2ZNmoMy{cLUF zE({Hdj&ABKDZyVSQ(&%k*mljfDUA1^nX$_qkqI7IrT%Ns#XI_|NPb)?!Lg`JrZ>a& zcg}St#%cuaKBrCyv5;a!wd@MsPi%+&rIiSV-S?`RFu$^Q1xl1J4Z6*?J%Jo$j`Ya> zo@J6RgK!$hd!K`Go(~c3--Mu_=2N0?+u~KjTB~zjQzte<@$~PzR}PII6a3UdrhrO+ zg9cw4L&OO%LU@DaW&EsP5jj>?&0RxV27~Z4=i@^^zc2-yFCl8U4cO0q$xyG7!G>6t zN4jusv>9aRy6-zbAHm{%34O5s?aaWdaaveZ`qI!qSZW}?2VI-J`ZT2hMHsyI`gO66 zJwFz(M%_v$`u=?W?Zkv_-pp?*iCw`j9D^Io93?aeInfDv?x(moZ>{jb-mvgy7AbN0 zoT)D^Za2GLC_F5$b{@D&PT+_T@%Q0KBNu+ppiQAc5sa4)BZ85&Fl$q2!3HeyKWVea zxC_sSt-UhHOoJ;esKLd@knsBvC0qf9%sBK31rc&Q#hz8|7!-Wl4X{5yF1`9rFLS9o zT=p7jj$OPp_T68pGbdgnZCXt+^yZ}8HGlGTVten1!;sJXEaX3}1)=P2-wz`0Grd(< z?<6(y9YBaGHezR<9*kxlbyg(aAuBMr#U7=3758Vf*uF-|?jY!(ha*pxQfc$?J^tIx zRa02hmFQr@fk`cuB=8|FMLl5BQ@}S#AP}~??)5>%RiLStXrz+U z=vkO=h(#3*=6VAZ(s7#gMK^pGuvt}cE#C*{`+X~~yY-Tm#Nd8T8Q>BY@I z#=LRnG_~AbW*s1sR@#t1Lmw;1v?P};chV-2iQJL;EU|J7nIvDX%MGS}K4^l-kA$as&Fw>G=Cb79rA~Z{N&Vnmjdue}uBN+yo&KTgP1TK8u?lSavb!d!sy-|Iv^(T*wGebjhFm-k^kj|o% zEsXB?6fmSWT4c{a3G|2-U#a9_0!Kgu&t5E7;M*ZU}LjA;@YNn8IU-6kA0eS*fzE^mn>w5aTj+%@}ae6xawe5 zi;gmc!H5t*0k^l3%io)uR*ByVxd6GZn;SrTb~qvk@ba${D{g-~)mvv+uWi zL{qPHHz4pmyEB8!qSJj_j|)NYZ=ITEbVkrKy8QV?Z@0&pnJn_MykBvf5`c=VH*?O7 zj9snm*U!?=C;$er&u+ri)o-F)Pc3jw`HqVr?b$}&=gM!u73Yr(wY1px3BH8?@*01es=93aAjwwXDm@> z^uMg+^ayQuU~^LAxi3$)J&u_H<~$OiMivw#0(cE#7|!v)zY4I;w+(4nWWY%m;!d z%Tlc9(fJC+3Z_1ITlm!M!wG&x7)%T`Fr6 zEB-e;L2zy$Vh(qt$Y)tXG~~VaDiaJT-9ot{Z%ALLE&g)Fv(T=i&b#Qi_OeiH#S^%T zuZyQ%^YBqrOHPUNCec_8Zx8?q82_fO-lp1Wqy4)vqwv9PACF9hv~X>UjdqWHxVVp@T;q;ljA; zuDG~u0ix2F9UU264t_WDZ1-SZeq@!&quV5*Xq@@)&E0n@EmukD7hemK<;3pAK8#sQ zGJk3WgBlzCrDmTj$B9|)d!bu+9v^RPqkcB#9fNe-zh#)b%^&658{JUkrm$ZS~yh zGkR$qO7iBMG-mmkg|CR4Sx?4!TS7}33)Y(P^1~{bjKfMrZ>3 z(eA@EWyYL^#*D%^Noh;_(qBH2;Cxh+P|D2s&GVKW-wp+d>YwylVFE2o8vZoH%QGE) z%9q8`ZZ9cd!Q2$dIe#dgH{Yol7&&@}Pkg=Cb5r~lW45NlOUwD56XgR{wnKUGvNJ(J z5!U{WHnIWgWSN$WZvelH!u?oIzYysu6nr5CC4Lj|o&+IE9^a(Ler4BX)%19De(dtH zq>N^TE%i?M@dI#|z-CYE_v+hJUmDz17VTfq;(cl}_5m5yQrxharo9QNLI2Ui*>D9C zR+hr}9${yj_n2u0#XD-Gq&yVCW|e8S0zAu|wm!E1`pkNUb9jd(7=m88u3qYmu|PWb z2Lz7+0y-ug3PxfYzSRbQ>OF<-Yr4`PdgHWaM-4s3ArW=k(zg3cN}&k@BR+l1<~%0Y;9&OL&4|2dJ!TE5 z7$HW%Qnr>R0G##iU8`qUA7;rFL7#_J#tt}Y^7tn5W8TaIn|leoEHd&wAv?~S#ACU< z*6(tP^fDFchSb#3OpdKJ)dxljzY*^;hgDYIy2TjQ0R^qWw(U*2RhT7xRf*U&s4xV=Yhc~BIoPNn}B7ir-+58+g#yLH}CG+ z5_k|CvcjLGcq6*p~(9}SvRGDajeOHPY-A8cHszL@E%`qCSn zk=O-|1LboovAweq)5gRU& zXvFNz3yk)@nn5^J)oGV^?i2_TwK?MW<+|b$eA>wu>EV($yehW_{!uv*R@yVmy$5c zp+#7*oy70j)$J>&wi8 zTf9!A&DD9$uX$!Abt@xoIuA?Q1m2`G`i=-|Q{5}^p9$TIrT|4pL{dGMR7!^1?wk*iw zQGKx7xiY*T>*@Ueiqi{ZlqhuZSAjIq-qRysU`SuL<$M3oPa+x@fb=HSh$E4X_k5}zFAKF8)OZPB z+PU}BYHMeQaZrHzU8=3tqb$yZpt#Bwm`zJ1$<`-^LN_uC#RHvfVNVq#=)sGUj3v!g zwk^Q64BC*~YyAwFnnGKwF#+ruoSwcmqf6S2HI4hM2}*QwSpS{+XJl{h>X4W$V+a@B zxB5*`#B!%;`<}uB&}OSJW(9vjFK3$TdP-P_-9?X%V`W2|>k>=FHRqjp}ZjCWR?^U?a_i9;92bNR6(0R=HZ z;hOP0Le?9nuT}CqV?y<1T7JS!Z&5vG2=Dca2pD8FJ?R0A{y679n3uKt~w*B zi1av7!+GzrmwqIAIG*LBc%uR-ULVNr-F?1cF&3C`qthtYrAyV7kTCCQz+A>)_J!?5 jhE~+!^ln<}1x;T5FYVa78xO&M5r7v`3dkY}L*M@Zx{$c3 literal 0 HcmV?d00001 diff --git a/web/public/attachment/zip-icon.png b/web/public/attachment/zip-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..0db1da136dfc502e25795ee67c7c9893c1644a65 GIT binary patch literal 7217 zcmbVQcU)9ivOY~lvXTbLwjepEfFKei=OCa2L6i)VgQV^u3#`=%_zD1ThA_bj>+bK5^1``2Kd`KUsdQ`kB&)JXG837Izd;~t3Hx$w zl$(#3nHYMET^d#9BUS)#Z-UsLn+T*7EY=u=P#3;tcHst=;E)DLP|AWcydWG9007Yd zNRt0d#EF9f6aWbTG*G$EcGNIv2jPW8NbpyY_u53C{RLgxmRTt$t_$ZMUKFU(5$BXA z6yGrYDLl7&$XhnyQ9vf%B>N)&`cUENivljqObxCG!GeNV^>mLWs@_u{dY93smfu}K{Eq6Q_ko8ga{6`_Ga&K1reyFo}niAUo7mulZFug zC7FN*5?TCuDd)bnIg||JD5BOY`m|Qz4{rA6t+@w^oij*+tG+DoL;+N#X~{zKPR*07 zDvV;?r(?`UdR|^*vB<6{Tg~)j*Ae=`ctB5(=0TMZ06I51`5=bFyC;Y5>UjakvD~@& zUtzVcUZfA8k6G6LZ8^z^<^S4MVb7$Z?pk?#JH z*VZT32ff2LVG|ks7L1FXk4!`Qvr`Sy3ZluH&LI5l-G4%dQ?Wxv{}8HWxJs%-*ZM|j zDB}QsWQF$XM#Ov&0D=|OwKP^0N>Ih8>=Z|fb}f2b<^JmsbVm|h8r^o`5A^7td+{0ex?YE9)Dz|Pz81NT05xRI|@Y96bKNYFr zTW$Sh;R8V4YMycW#?JB0TYKW$6IcwA5wC6RX@9np-Pb~=^W1XX6ma6l_FX?!45`dU z4R+h~@Sx7c!8&Vq9L2NZ;QHt64{G}}?6|Y>_hv|zjyQ$>M}=Y8-b?|QGV^TbiDyF7 z*XoX5$kf~xu-2|9wtD+3{_q1*BtrQ)gDJ0BrSjX_=@tOY*P*-`X5B zHxbQ5uyhy%d+fZXwOY;7eLGY{RRp~w5zaQaM^qqtX-Do_K!OAz?%>ulH1jv!@a-!d z=DLsKfp}Sbt=?8*<-i?h*^4{;@!F1QZ*lSyM38K6f)z@6yJ6tR&jh#2!VO4VKB!W@ zbRxx>BY@jYIvrbJRvw~OxKVLBcOXu)r*ya+IPmyYtihm0~g`$%z_q>L1KVft1~ zKXZQY!VxD49!s;dE=&47o84_nOGw>7&3MUZz-erTU`ErkfA9VBC%;tTJ1ps36BvBx z)ZEpueKjkeRA@hUVz1LvtDSk@$R?RVv$btF<75WHyro?9HJMDc!SKRlpmuP~*K>Gz z0VJ(DM%iU0eOz&Si$Z?hUk4SaLtRf=M!b>*f@Cex9~kEu_Q$UT(F#&i+bJmUBqc>S z86C^3JIJ9}=HYE7uZkohvF1CbV(Vhuqp0}NtGTm=T({rCt^uUfKx(D!J7&{w!GUF* z9a^1@%2Z{TVhI)kH|%z6$<(v;R6JUL52ca(ak=EgTaC$XgLSNUjH z=gQ6UWQ_-i_U~Wa_jl(z79(vId)m@?Sl7lje;Q#m-@Z|AeD2WQ=NEFKtXE0c@?#)S zsS-Vw*)MjasnPNIBtw(A-Agok(ZTyZuhsM(c2JtQ5mjc}d3fs+5t0tfX}ucG4Nx2L z9V;`46rV#BC&;{%#xxt7<&Kc>fi)PIn0t~n#)}kDsw=IYAegjPuRNTk4JG;lfPF@T zoF|i0{V&4r*Z86^y|uQa71y9kDwtuHAy@1cjxHFtRgJ64`)%dDq8y{%Bb+9S{bE;s z@_7N9qSBA7Ret?!D$RNCj)b;;!6Se?Zm^PNWMF+mZ}bZB}A0;cm{#OCq5FegOF>H6zSb3qT3Cxu6hPo2S~x&F;_2-92OH=SN|;u#*kI~TNI0W`~5H3&{M3$LfPxe zhG84ad}9=Hcy97NgoW;X?cRVDPCuiFWO6g>7hI*d2>EVcNy-w(;B2f4Muo=?19Rj33tbbA;w9fl;O0Er(9IvPPuXX2r}`%mizEsq=dX?U zP(!q-5lNTpuDDE6)pgR7>FtTx;e>B)IgTf_8Jc`0e9d7-Hd~!$d5=B{26-Kc@Cw{Z zDr%i^5L+sD;0Wnu!wGMYzC5DfaUMZ0y)%!!<9t1JmskaWi7Uenl9w%*l`U1MICiQx z)ywXDx59@5VErRk7EzPee$W=~bvzX;p!zu9rxS{blo4qQxdfBE^Z25&VQRdJt#qZJ z7@Gi2gE$_Um|9hNYkcqRCpTkLftu#=2OYTBE+*9?dUkaKYZE3>cChXNr>Lo3ue*<% z_Hcnt+m*OV2k}m|lq)`cU|ByTGrm=`t}dGRy#NMj7Zn)#Yn`-DG6i_<@L4jW)@}q& zHh!5Jr|ed2r$VkGNjY;%)Pf&uYP#&q*=&GMMOEZNG z4i2`Es^kn(;^*b1lY4z%F}+gRBqKx7=?Uv$N7m&{>Rz7XO!M06gO$p3Rw4#F^xVpQ zcNSs1mNoN_E#s@g(v#*z(chIbOHlNfA@V^sJAY zo14*9iwvO>0@`E;educQG2^+xFv|vhv9|dz@4OdHVsY^8hruQGE*{n;XdCvb?yA$- z+G~+biQ1=0C95XOuBES?vc|j{-n>i8s%dP*c=*|fn;B23gtaXW?l<2KMEqj1@XIU( zkZrqTWTZIT-)QUL6y3C8E+RF2q0vl+a@zmCgYB zQ>at>vO_dh#qvXL5WH15_WU5f(6VjHAoY_Vl$}Gis{0XkhM$Ujv;A|!%_Zq+2DupG z2(q0!`T;6@aml*W&~TN7->VW;ts1858gGcUL_)tFaX#&)D}A^Sx*yAKLV6~Ml!o8E zXP9$^Sf(Tg`b-ax%T6!ITL^{2aJx^7>1n#ymgFa9Y+*{*B{xu;qy&NXB95J%cU1eu z=!xWF)=R0;{)1Onw!Cf()q)Wb->ds~D`0p8H8Ba$!<>b0J_vlY^o6 z$`d;TT93QWbYPM=^Z2?rxBdoWkrp#gT%MOp=Q6+>l2}3t8BEs#R1kIpyR)iTGeP`m33mMtFSjVp3RI%83$3@W=WlIfJj) z>>d5p3e_U2d8aO@i^$=Fkdn2%<^3<&#aUBe%3L0Hm=^%l8_OP=c=Pm62BJ4l=7X?-ac8W?d9#7*DY%4Wu6Xo zUiNh!FEi}r-^jPAODG1xi-WINkP}=9zw@!2@O%i;iX(kgogs4vb!`{3?f?0ZszG{d zZ0YpSnom0R1I>E?RImg6UHVMfDRZ%uZq;wixcIyEP8Urnph|zdr6vKKdSCF&{y7k&#>kv_Rhi zDW~4jM~utUtwdq%R-Du`46w7HF0YrD@1=(C9wnsAEFmob$VW)@e_m_QxXjJ%CMyhp za{)GRpjmxFR9j+*?VHj~Zg6U*wj(WxR(maT*y@a_51nQc(%;=BW2UABEvhny@iZFo z#XDWb1I>|euEVHqIUkM>xL6r+r`f#E1Ta{a4bd=<=V;l8?GmiXBX%gpfJ_F@CC1pN z{ckTaQ;`ECnySb+@t1_B{nYs2Y^xj*xyvKvP@wTFj0pT0wM}PcYI4w0c5Ww`1CxCjQ$Ji?8pC)?)>dQis{5I$dbv3S9Wy>2 zn_l2~P@eQOQo!s?-n85tj*gMyoie(jzmzU1!OjSf0fUGv4a|h*u{VnNYx(egkg`>7 z&89a+%nk*B7#I~|V{UO{>Jw#d2_7@rc2_*gJ^rZ_jVoRo+FaKFDjZLD9Az>+AGcqk z203Nas@^IcCF$>K>_fgV5031<0SBSYuUVN#q6T}K_{~@xh~^MR;8UolWHpAM(hHL@6kg)<^uw5sdpF5`lqijF@`}+<8@Ms zfSTk{Y3H+3RAh8nV1V(d09It?HWEp}wkUtxj-9jLL{4Gf=soY?YzGholVh1XJN(OR z-DO8bCBB=%57VfGT*$ZR!A!L_COQ7pHNE}B@awgek_&tO(`>jpE+hJSPf`Z3b^|i# zn77-iEId-h`@O5o_zi?0_H@=uT^&(exRE;*c<}J_Fc=Rf3!RuA8YnH-9&&(x9K=AU zKn3ekRh*7cY92?_y-4K1l_wQ#u5^5mdoliM-NaPhNHuGfr|IC_+<9dfKG@PYmbqWj zPhEaE@z~8?>EQ+a6|Cpo0L<-`RkooPi||=u$iXOj`pXB7b%Eo`#=;~)j1kIx_D9&E z_0r;XafXq@j|rR&b;S699TR_=X=P!n!H`xjLD2FQzy@TX#*sa4R?nPljy;7zO;lch z)$V$wiUm;ilLrul%WP1lT$0*%Ew!u$45xaA2oM^!w({@pQcd z+1WEL5DW1p(+)uK4er8*Kg#&{R z#hOyb`a$8LL@j5dsQ@{$dAz>n3-dg5Y3f52v@JT`P$*;RF8CN4&g@o2#-T2+ch{6% zk&-5f*?BP|LB(y>n)Bf|B{$oT)`AT?DjHf<3;pUe0?TxJxB8a!r@IdppHTIcBmAak ze@yqwAYrn4&lr!Bm9bRJ7<@+XeX<`rcT~%UQ5Kpe_(_%JiKemuvD%Bh840`1M^65S z*Y2X;@=D>EssMbXzle{{bOvG-iwVmTyCkHsnFd z?!zK&R+i5xqFmEEAMv444rK%U)4~YdRH3`*?+dnjb;Mh4uOPj6)%=}%GmnJ1S+c(O zhBz@+&0U@A>uN&4YjM&ozttMncV>o7f=Te@uN4O*+<))lFP{EvLBOM>7r;B^d`Cdb zzz~4OBhI;G78!Yq36KsqIxHRBp93EGCN+YaGJlALasnkG{;=~AUxl9|FxCT z(VT$my(3B0xF&Ju&Lj!H7#F-)ozPI%-HsRd0vsSv)W0JqN55$6Ei6{&!CjxnC+}w0 zLkXKOBrW2Egb#!R*WOh3WzV~pDFrF^$7d$`#YpYM!2_I@PA_anG6*CF60^^$shgK( zNV*Q~-IrE>&4El4VbEkKz2)xWw&~yI{Pnm=5 zyY$XS$M3Eds?aK4W)K`PO|AJ@g9qn{LhP9b6c=(uaJIj<^Q_%(>x#JUH`d4j=V2+m zTVJ%kQsqFmfh5q>q?kM~$h=wSGU|0(HtzgbZp8XJadhUb;cwr*jc3MW4~>j?l}UP8 zFE4hr?sXe&J~n&jXfpo!=wgE_oTslV(91@qHMx9hxb8{&i@H~rY?7>H<|mf}5Ak~P WT8Yo+4`QM6164&Wg)(`Ip#K4@{jZV$ literal 0 HcmV?d00001 From 42e2b787f0ab36cad33ca9a7d08e2826d74fcf8d Mon Sep 17 00:00:00 2001 From: Nikhil <118773738+pablohashescobar@users.noreply.github.com> Date: Mon, 5 May 2025 18:58:24 +0530 Subject: [PATCH 019/201] [WEB-4013]chore: publish login and standardize urls in common settings (#7013) * chore: handling base path and urls * chore: uniformize urls in common settings * correct live url * chore: use url join to correctly join urls --------- Co-authored-by: sriram veeraghanta --- apiserver/plane/authentication/utils/host.py | 13 +++-- apiserver/plane/bgtasks/copy_s3_object.py | 20 ++++++-- apiserver/plane/settings/common.py | 40 ++++++++++++--- apiserver/plane/utils/host.py | 12 +++-- apiserver/plane/utils/url.py | 54 ++++++++++++++++++++ 5 files changed, 120 insertions(+), 19 deletions(-) create mode 100644 apiserver/plane/utils/url.py diff --git a/apiserver/plane/authentication/utils/host.py b/apiserver/plane/authentication/utils/host.py index 64f191685..67c8a4f72 100644 --- a/apiserver/plane/authentication/utils/host.py +++ b/apiserver/plane/authentication/utils/host.py @@ -1,18 +1,25 @@ # Django imports from django.conf import settings from django.http import HttpRequest + # Third party imports from rest_framework.request import Request # Module imports from plane.utils.ip_address import get_client_ip -def base_host(request: Request | HttpRequest, is_admin: bool = False, is_space: bool = False, is_app: bool = False) -> str: + +def base_host( + request: Request | HttpRequest, + is_admin: bool = False, + is_space: bool = False, + is_app: bool = False, +) -> str: """Utility function to return host / origin from the request""" # Calculate the base origin from request base_origin = settings.WEB_URL or settings.APP_BASE_URL - # Admin redirections + # Admin redirection if is_admin: admin_base_path = getattr(settings, "ADMIN_BASE_PATH", "/god-mode/") if not admin_base_path.startswith("/"): @@ -25,7 +32,7 @@ def base_host(request: Request | HttpRequest, is_admin: bool = False, is_space: else: return base_origin + admin_base_path - # Space redirections + # Space redirection if is_space: space_base_path = getattr(settings, "SPACE_BASE_PATH", "/spaces/") if not space_base_path.startswith("/"): diff --git a/apiserver/plane/bgtasks/copy_s3_object.py b/apiserver/plane/bgtasks/copy_s3_object.py index d73b96454..972873396 100644 --- a/apiserver/plane/bgtasks/copy_s3_object.py +++ b/apiserver/plane/bgtasks/copy_s3_object.py @@ -3,7 +3,7 @@ import uuid import base64 import requests from bs4 import BeautifulSoup - +from urllib.parse import urljoin # Django imports from django.conf import settings @@ -12,6 +12,7 @@ from plane.db.models import FileAsset, Page, Issue from plane.utils.exception_logger import log_exception from plane.settings.storage import S3Storage from celery import shared_task +from plane.utils.url import get_url_components def get_entity_id_field(entity_type, entity_id): @@ -67,11 +68,20 @@ def sync_with_external_service(entity_name, description_html): "description_html": description_html, "variant": "rich" if entity_name == "PAGE" else "document", } - response = requests.post( - f"{settings.LIVE_BASE_URL}/convert-document/", - json=data, - headers=None, + + if not settings.LIVE_URL: + return {} + + live_url = get_url_components(settings.LIVE_URL) + if not live_url: + return {} + + base_url = ( + f"{live_url.get('scheme')}://{live_url.get('netloc')}{live_url.get('path')}" ) + url = urljoin(base_url, "convert-document/") + + response = requests.post(url, json=data, headers=None) if response.status_code == 200: return response.json() except requests.RequestException as e: diff --git a/apiserver/plane/settings/common.py b/apiserver/plane/settings/common.py index 15d7a21b3..38d2ac6e0 100644 --- a/apiserver/plane/settings/common.py +++ b/apiserver/plane/settings/common.py @@ -3,7 +3,7 @@ # Python imports import os from urllib.parse import urlparse - +from urllib.parse import urljoin # Third party imports import dj_database_url @@ -13,6 +13,10 @@ from django.core.management.utils import get_random_secret_key from corsheaders.defaults import default_headers +# Module imports +from plane.utils.url import is_valid_url + + BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # Secret Key @@ -310,15 +314,35 @@ CSRF_TRUSTED_ORIGINS = cors_allowed_origins CSRF_COOKIE_DOMAIN = os.environ.get("COOKIE_DOMAIN", None) CSRF_FAILURE_VIEW = "plane.authentication.views.common.csrf_failure" -# Base URLs +###### Base URLs ###### + +# Admin Base URL ADMIN_BASE_URL = os.environ.get("ADMIN_BASE_URL", None) -ADMIN_BASE_PATH = os.environ.get("ADMIN_BASE_PATH", None) +if ADMIN_BASE_URL and not is_valid_url(ADMIN_BASE_URL): + ADMIN_BASE_URL = None +ADMIN_BASE_PATH = os.environ.get("ADMIN_BASE_PATH", "/god-mode/") + +# Space Base URL SPACE_BASE_URL = os.environ.get("SPACE_BASE_URL", None) -SPACE_BASE_PATH = os.environ.get("SPACE_BASE_PATH", None) -APP_BASE_URL = os.environ.get("APP_BASE_URL") -APP_BASE_PATH = os.environ.get("APP_BASE_PATH", None) -LIVE_BASE_URL = os.environ.get("LIVE_BASE_URL") -LIVE_BASE_PATH = os.environ.get("LIVE_BASE_PATH") +if SPACE_BASE_URL and not is_valid_url(SPACE_BASE_URL): + SPACE_BASE_URL = None +SPACE_BASE_PATH = os.environ.get("SPACE_BASE_PATH", "/spaces/") + +# App Base URL +APP_BASE_URL = os.environ.get("APP_BASE_URL", None) +if APP_BASE_URL and not is_valid_url(APP_BASE_URL): + APP_BASE_URL = None +APP_BASE_PATH = os.environ.get("APP_BASE_PATH", "/") + +# Live Base URL +LIVE_BASE_URL = os.environ.get("LIVE_BASE_URL", None) +if LIVE_BASE_URL and not is_valid_url(LIVE_BASE_URL): + LIVE_BASE_URL = None +LIVE_BASE_PATH = os.environ.get("LIVE_BASE_PATH", "/live/") + +LIVE_URL = urljoin(LIVE_BASE_URL, LIVE_BASE_PATH) if LIVE_BASE_URL else None + +# WEB URL WEB_URL = os.environ.get("WEB_URL") HARD_DELETE_AFTER_DAYS = int(os.environ.get("HARD_DELETE_AFTER_DAYS", 60)) diff --git a/apiserver/plane/utils/host.py b/apiserver/plane/utils/host.py index 7c8635836..d74a86ffd 100644 --- a/apiserver/plane/utils/host.py +++ b/apiserver/plane/utils/host.py @@ -9,7 +9,13 @@ from rest_framework.request import Request # Module imports from plane.utils.ip_address import get_client_ip -def base_host(request: Request | HttpRequest, is_admin: bool = False, is_space: bool = False, is_app: bool = False) -> str: + +def base_host( + request: Request | HttpRequest, + is_admin: bool = False, + is_space: bool = False, + is_app: bool = False, +) -> str: """Utility function to return host / origin from the request""" # Calculate the base origin from request base_origin = settings.WEB_URL or settings.APP_BASE_URL @@ -17,7 +23,7 @@ def base_host(request: Request | HttpRequest, is_admin: bool = False, is_space: if not base_origin: raise ImproperlyConfigured("APP_BASE_URL or WEB_URL is not set") - # Admin redirections + # Admin redirection if is_admin: admin_base_path = getattr(settings, "ADMIN_BASE_PATH", "/god-mode/") if not admin_base_path.startswith("/"): @@ -30,7 +36,7 @@ def base_host(request: Request | HttpRequest, is_admin: bool = False, is_space: else: return base_origin + admin_base_path - # Space redirections + # Space redirection if is_space: space_base_path = getattr(settings, "SPACE_BASE_PATH", "/spaces/") if not space_base_path.startswith("/"): diff --git a/apiserver/plane/utils/url.py b/apiserver/plane/utils/url.py new file mode 100644 index 000000000..0658572bf --- /dev/null +++ b/apiserver/plane/utils/url.py @@ -0,0 +1,54 @@ +# Python imports +from typing import Optional +from urllib.parse import urlparse + + +def is_valid_url(url: str) -> bool: + """ + Validates whether the given string is a well-formed URL. + + Args: + url (str): The URL string to validate. + + Returns: + bool: True if the URL is valid, False otherwise. + + Example: + >>> is_valid_url("https://example.com") + True + >>> is_valid_url("not a url") + False + """ + try: + result = urlparse(url) + # A valid URL should have at least scheme and netloc + return all([result.scheme, result.netloc]) + except TypeError: + return False + + +def get_url_components(url: str) -> Optional[dict]: + """ + Parses the URL and returns its components if valid. + + Args: + url (str): The URL string to parse. + + Returns: + Optional[dict]: A dictionary with URL components if valid, None otherwise. + + Example: + >>> get_url_components("https://example.com/path?query=1") + {'scheme': 'https', 'netloc': 'example.com', 'path': '/path', 'params': '', 'query': 'query=1', 'fragment': ''} + """ + if not is_valid_url(url): + return None + result = urlparse(url) + return { + "scheme": result.scheme, + "netloc": result.netloc, + "path": result.path, + "params": result.params, + "query": result.query, + "fragment": result.fragment, + } From b4cc2d83fecef3ebf4b0b3498255ae653b8ebb77 Mon Sep 17 00:00:00 2001 From: Nikhil <118773738+pablohashescobar@users.noreply.github.com> Date: Tue, 6 May 2025 01:20:33 +0530 Subject: [PATCH 020/201] [WEB-4014] fix: check access when duplicating pages #7015 --- apiserver/plane/app/views/page/base.py | 9 ++++++++- apiserver/plane/db/models/page.py | 9 ++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/apiserver/plane/app/views/page/base.py b/apiserver/plane/app/views/page/base.py index e8a3c3ffd..26e9223b8 100644 --- a/apiserver/plane/app/views/page/base.py +++ b/apiserver/plane/app/views/page/base.py @@ -42,6 +42,7 @@ from plane.bgtasks.page_version_task import page_version from plane.bgtasks.recent_visited_task import recent_visited_task from plane.bgtasks.copy_s3_object import copy_s3_objects + def unarchive_archive_page_and_descendants(page_id, archived_at): # Your SQL query sql = """ @@ -198,7 +199,7 @@ class PageViewSet(BaseViewSet): project = Project.objects.get(pk=project_id) """ - if the role is guest and guest_view_all_features is false and owned by is not + if the role is guest and guest_view_all_features is false and owned by is not the requesting user then dont show the page """ @@ -572,6 +573,12 @@ class PageDuplicateEndpoint(BaseAPIView): pk=page_id, workspace__slug=slug, projects__id=project_id ).first() + # check for permission + if page.access == Page.PRIVATE_ACCESS and page.owned_by_id != request.user.id: + return Response( + {"error": "Permission denied"}, status=status.HTTP_403_FORBIDDEN + ) + # get all the project ids where page is present project_ids = ProjectPage.objects.filter(page_id=page_id).values_list( "project_id", flat=True diff --git a/apiserver/plane/db/models/page.py b/apiserver/plane/db/models/page.py index 5f4fb2744..5be9c6164 100644 --- a/apiserver/plane/db/models/page.py +++ b/apiserver/plane/db/models/page.py @@ -17,6 +17,11 @@ def get_view_props(): class Page(BaseModel): + PRIVATE_ACCESS = 1 + PUBLIC_ACCESS = 0 + + ACCESS_CHOICES = ((PRIVATE_ACCESS, "Private"), (PUBLIC_ACCESS, "Public")) + workspace = models.ForeignKey( "db.Workspace", on_delete=models.CASCADE, related_name="pages" ) @@ -91,9 +96,7 @@ class PageLog(BaseModel): transaction = models.UUIDField(default=uuid.uuid4) page = models.ForeignKey(Page, related_name="page_log", on_delete=models.CASCADE) entity_identifier = models.UUIDField(null=True) - entity_name = models.CharField( - max_length=30, verbose_name="Transaction Type" - ) + entity_name = models.CharField(max_length=30, verbose_name="Transaction Type") workspace = models.ForeignKey( "db.Workspace", on_delete=models.CASCADE, related_name="workspace_page_log" ) From 0a01e0eb4165b6ae332957d8ed79037091fe128a Mon Sep 17 00:00:00 2001 From: Nikhil <118773738+pablohashescobar@users.noreply.github.com> Date: Tue, 6 May 2025 01:21:53 +0530 Subject: [PATCH 021/201] [WEB-4013] chore: correct live url #7014 --- apiserver/plane/bgtasks/copy_s3_object.py | 14 ++++-------- apiserver/plane/utils/url.py | 27 ++++++++++++++++++++++- 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/apiserver/plane/bgtasks/copy_s3_object.py b/apiserver/plane/bgtasks/copy_s3_object.py index 972873396..a92d7fe4e 100644 --- a/apiserver/plane/bgtasks/copy_s3_object.py +++ b/apiserver/plane/bgtasks/copy_s3_object.py @@ -3,7 +3,7 @@ import uuid import base64 import requests from bs4 import BeautifulSoup -from urllib.parse import urljoin + # Django imports from django.conf import settings @@ -12,7 +12,7 @@ from plane.db.models import FileAsset, Page, Issue from plane.utils.exception_logger import log_exception from plane.settings.storage import S3Storage from celery import shared_task -from plane.utils.url import get_url_components +from plane.utils.url import normalize_url_path def get_entity_id_field(entity_type, entity_id): @@ -69,17 +69,11 @@ def sync_with_external_service(entity_name, description_html): "variant": "rich" if entity_name == "PAGE" else "document", } - if not settings.LIVE_URL: - return {} - - live_url = get_url_components(settings.LIVE_URL) + live_url = settings.LIVE_URL if not live_url: return {} - base_url = ( - f"{live_url.get('scheme')}://{live_url.get('netloc')}{live_url.get('path')}" - ) - url = urljoin(base_url, "convert-document/") + url = normalize_url_path(f"{live_url}/convert-document/") response = requests.post(url, json=data, headers=None) if response.status_code == 200: diff --git a/apiserver/plane/utils/url.py b/apiserver/plane/utils/url.py index 0658572bf..e485f93df 100644 --- a/apiserver/plane/utils/url.py +++ b/apiserver/plane/utils/url.py @@ -1,6 +1,7 @@ # Python imports +import re from typing import Optional -from urllib.parse import urlparse +from urllib.parse import urlparse, urlunparse def is_valid_url(url: str) -> bool: @@ -52,3 +53,27 @@ def get_url_components(url: str) -> Optional[dict]: "query": result.query, "fragment": result.fragment, } + + +def normalize_url_path(url: str) -> str: + """ + Normalize the path component of a URL by replacing multiple consecutive slashes with a single slash. + + This function preserves the protocol, domain, query parameters, and fragments of the URL, + only modifying the path portion to ensure there are no duplicate slashes. + + Args: + url (str): The input URL string to normalize. + + Returns: + str: The normalized URL with redundant slashes in the path removed. + + Example: + >>> normalize_url_path('https://example.com//foo///bar//baz?x=1#frag') + 'https://example.com/foo/bar/baz?x=1#frag' + """ + parts = urlparse(url) + # Normalize the path + normalized_path = re.sub(r"/+", "/", parts.path) + # Reconstruct the URL + return urlunparse(parts._replace(path=normalized_path)) From d366ac158135b6fed7095ddfba85f45c4efae302 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Wed, 7 May 2025 00:28:43 +0530 Subject: [PATCH 022/201] [WEB-2508] fix: page favorite item title mutation (#7020) * fix: remove page favorite item title fallback value * refactor: use nullish coalescing operator --- web/core/hooks/use-favorite-item-details.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/web/core/hooks/use-favorite-item-details.tsx b/web/core/hooks/use-favorite-item-details.tsx index 343e143ed..eadc64f7c 100644 --- a/web/core/hooks/use-favorite-item-details.tsx +++ b/web/core/hooks/use-favorite-item-details.tsx @@ -43,23 +43,23 @@ export const useFavoriteItemDetails = (workspaceSlug: string, favorite: IFavorit switch (favoriteItemEntityType) { case "project": - itemTitle = currentProjectDetails?.name || favoriteItemName; + itemTitle = currentProjectDetails?.name ?? favoriteItemName; itemIcon = getFavoriteItemIcon("project", currentProjectDetails?.logo_props || favoriteItemLogoProps); break; case "page": - itemTitle = getPageName(pageDetail?.name || favoriteItemName); - itemIcon = getFavoriteItemIcon("page", pageDetail?.logo_props || favoriteItemLogoProps); + itemTitle = getPageName(pageDetail?.name ?? favoriteItemName); + itemIcon = getFavoriteItemIcon("page", pageDetail?.logo_props ?? favoriteItemLogoProps); break; case "view": - itemTitle = viewDetails?.name || favoriteItemName; + itemTitle = viewDetails?.name ?? favoriteItemName; itemIcon = getFavoriteItemIcon("view", viewDetails?.logo_props || favoriteItemLogoProps); break; case "cycle": - itemTitle = cycleDetail?.name || favoriteItemName; + itemTitle = cycleDetail?.name ?? favoriteItemName; itemIcon = getFavoriteItemIcon("cycle"); break; case "module": - itemTitle = moduleDetail?.name || favoriteItemName; + itemTitle = moduleDetail?.name ?? favoriteItemName; itemIcon = getFavoriteItemIcon("module"); break; default: { From bc2936dcd3eec6277f2f4e87d91063ca68b8a437 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Wed, 7 May 2025 00:51:51 +0530 Subject: [PATCH 023/201] [WEB-3906] fix: page table of content overlap with the page content #7018 --- packages/editor/src/styles/variables.css | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/editor/src/styles/variables.css b/packages/editor/src/styles/variables.css index 44690cf52..6d6e2d9b6 100644 --- a/packages/editor/src/styles/variables.css +++ b/packages/editor/src/styles/variables.css @@ -233,7 +233,9 @@ padding-left: var(--normal-content-margin); padding-right: var(--normal-content-margin); } +} +@container page-content-container (max-width: 930px) { .page-summary-container { display: none; } From 6faff1d556f06a8b7c7aa7b7cbd92828e25c12dd Mon Sep 17 00:00:00 2001 From: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com> Date: Wed, 7 May 2025 18:40:37 +0530 Subject: [PATCH 024/201] [WEB-3877] fix: changed logic to calculate cycle duration (#7024) * chore: cycle running days * chore: removed the module filter --- apiserver/plane/utils/analytics_plot.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/apiserver/plane/utils/analytics_plot.py b/apiserver/plane/utils/analytics_plot.py index 7527a3524..9e2f8c59d 100644 --- a/apiserver/plane/utils/analytics_plot.py +++ b/apiserver/plane/utils/analytics_plot.py @@ -140,7 +140,9 @@ def burndown_plot(queryset, slug, project_id, plot_type, cycle_id=None, module_i # Get all dates between the two dates date_range = [ (queryset.start_date + timedelta(days=x)).date() - for x in range((queryset.end_date - queryset.start_date).days + 1) + for x in range( + (queryset.end_date.date() - queryset.start_date.date()).days + 1 + ) ] else: date_range = [] @@ -180,7 +182,9 @@ def burndown_plot(queryset, slug, project_id, plot_type, cycle_id=None, module_i # Get all dates between the two dates date_range = [ (queryset.start_date + timedelta(days=x)) - for x in range((queryset.target_date - queryset.start_date).days + 1) + for x in range( + (queryset.target_date - queryset.start_date).days + 1 + ) ] chart_data = {str(date): 0 for date in date_range} From d45676749248cad1b761cae6747945e354addbba Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Wed, 7 May 2025 18:41:28 +0530 Subject: [PATCH 025/201] [WEB-3955] chore: work item parent select modal params #7021 --- web/core/components/issues/issue-detail/parent-select.tsx | 1 - .../issues/issue-modal/components/default-properties.tsx | 1 - 2 files changed, 2 deletions(-) diff --git a/web/core/components/issues/issue-detail/parent-select.tsx b/web/core/components/issues/issue-detail/parent-select.tsx index 59cb3808a..dc50864a9 100644 --- a/web/core/components/issues/issue-detail/parent-select.tsx +++ b/web/core/components/issues/issue-detail/parent-select.tsx @@ -70,7 +70,6 @@ export const IssueParentSelect: React.FC = observer((props) isOpen={isParentIssueModalOpen === issueId} handleClose={() => toggleParentIssueModal(null)} onChange={(issue: any) => handleParentIssue(issue?.id)} - searchEpic /> + )} +
+
+
+ {/* Priority */} + {isFilterEnabled("priority") && ( +
+ handleFiltersUpdate("priority", val)} + searchQuery={filtersSearchQuery} + /> +
+ )} + + {/* state group */} + {isFilterEnabled("state_group") && ( +
+ handleFiltersUpdate("state_group", val)} + searchQuery={filtersSearchQuery} + /> +
+ )} + + {/* State */} + {isFilterEnabled("state") && ( +
+ handleFiltersUpdate("state", val)} + searchQuery={filtersSearchQuery} + states={states} + /> +
+ )} + + {/* Projects */} + {isFilterEnabled("project") && ( +
+ handleFiltersUpdate("project", val)} + searchQuery={filtersSearchQuery} + /> +
+ )} + + {/* work item types */} + {isFilterEnabled("issue_type") && ( +
+ handleFiltersUpdate("issue_type", val)} + searchQuery={filtersSearchQuery} + /> +
+ )} + + {/* Assignees */} + {isFilterEnabled("assignees") && ( +
+ handleFiltersUpdate("assignees", val)} + memberIds={memberIds} + searchQuery={filtersSearchQuery} + /> +
+ )} + + {/* Start Date */} + {isFilterEnabled("start_date") && ( +
+ handleFiltersUpdate("start_date", val)} + searchQuery={filtersSearchQuery} + /> +
+ )} + + {/* Target Date */} + {isFilterEnabled("target_date") && ( +
+ handleFiltersUpdate("target_date", val)} + searchQuery={filtersSearchQuery} + /> +
+ )} +
+
+ + + ); +}); diff --git a/web/core/components/issues/issue-detail-widgets/sub-issues/issues-list/list-group.tsx b/web/core/components/issues/issue-detail-widgets/sub-issues/issues-list/list-group.tsx new file mode 100644 index 000000000..3b84cd732 --- /dev/null +++ b/web/core/components/issues/issue-detail-widgets/sub-issues/issues-list/list-group.tsx @@ -0,0 +1,96 @@ +import { FC, useState } from "react"; +import { observer } from "mobx-react"; +import { ChevronRight, CircleDashed } from "lucide-react"; +import { ALL_ISSUES, EIssuesStoreType } from "@plane/constants"; +import { IGroupByColumn, TIssue, TIssueServiceType, TSubIssueOperations } from "@plane/types"; +import { Collapsible } from "@plane/ui"; +import { cn } from "@plane/utils"; +import { SubIssuesListItem } from "./list-item"; + +interface TSubIssuesListGroupProps { + workItemIds: string[]; + projectId: string; + workspaceSlug: string; + group: IGroupByColumn; + serviceType: TIssueServiceType; + disabled: boolean; + parentIssueId: string; + handleIssueCrudState: ( + key: "create" | "existing" | "update" | "delete", + issueId: string, + issue?: TIssue | null + ) => void; + subIssueOperations: TSubIssueOperations; + storeType?: EIssuesStoreType; + spacingLeft?: number; +} + +export const SubIssuesListGroup: FC = observer((props) => { + const { + group, + serviceType, + disabled, + parentIssueId, + projectId, + workspaceSlug, + handleIssueCrudState, + subIssueOperations, + workItemIds, + storeType = EIssuesStoreType.PROJECT, + spacingLeft = 0, + } = props; + + const isAllIssues = group.id === ALL_ISSUES; + + // states + const [isCollapsibleOpen, setIsCollapsibleOpen] = useState(isAllIssues); + + if (!workItemIds.length) return null; + + return ( + <> + setIsCollapsibleOpen(!isCollapsibleOpen)} + title={ + !isAllIssues && ( +
+ +
+ {group.icon ?? } +
+ {group.name} + {workItemIds.length} +
+ ) + } + buttonClassName={cn("hidden", !isAllIssues && "block")} + > + {/* Work items list */} +
+ {workItemIds?.map((workItemId) => ( + + ))} +
+
+ + ); +}); diff --git a/web/core/components/issues/issue-detail-widgets/sub-issues/issues-list/list-item.tsx b/web/core/components/issues/issue-detail-widgets/sub-issues/issues-list/list-item.tsx index abb2aaca0..1d581929c 100644 --- a/web/core/components/issues/issue-detail-widgets/sub-issues/issues-list/list-item.tsx +++ b/web/core/components/issues/issue-detail-widgets/sub-issues/issues-list/list-item.tsx @@ -3,7 +3,7 @@ import { observer } from "mobx-react"; import { ChevronRight, X, Pencil, Trash, Link as LinkIcon, Loader } from "lucide-react"; // plane imports -import { EIssueServiceType } from "@plane/constants"; +import { EIssueServiceType, EIssuesStoreType } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { TIssue, TIssueServiceType, TSubIssueOperations } from "@plane/types"; import { ControlLink, CustomMenu, Tooltip } from "@plane/ui"; @@ -37,6 +37,7 @@ type Props = { subIssueOperations: TSubIssueOperations; issueId: string; issueServiceType?: TIssueServiceType; + storeType?: EIssuesStoreType; }; export const SubIssuesListItem: React.FC = observer((props) => { @@ -51,6 +52,7 @@ export const SubIssuesListItem: React.FC = observer((props) => { handleIssueCrudState, subIssueOperations, issueServiceType = EIssueServiceType.ISSUES, + storeType = EIssuesStoreType.PROJECT, } = props; const { t } = useTranslation(); const { @@ -81,7 +83,7 @@ export const SubIssuesListItem: React.FC = observer((props) => { // derived values const subIssueFilters = getSubIssueFilters(parentIssueId); - const displayProperties = subIssueFilters.displayProperties ?? {}; + const displayProperties = subIssueFilters?.displayProperties ?? {}; // const handleIssuePeekOverview = (issue: TIssue) => handleRedirection(workspaceSlug, issue, isMobile); @@ -265,6 +267,7 @@ export const SubIssuesListItem: React.FC = observer((props) => { subIssueCount > 0 && !isCurrentIssueRoot && ( = observer((props) => maxDate?.setDate(maxDate.getDate()); return (
+ +
+ + issue.project_id && + updateSubIssue( + workspaceSlug, + issue.project_id, + parentIssueId, + issueId, + { + state_id: val, + }, + { ...issue } + ) + } + disabled={!disabled} + buttonVariant="border-with-text" + /> +
+
+ + +
+ + issue.project_id && + updateSubIssue(workspaceSlug, issue.project_id, parentIssueId, issueId, { + priority: val, + }) + } + disabled={!disabled} + buttonVariant="border-without-text" + buttonClassName="border" + /> +
+
+
= observer((props) =>
- -
- - issue.project_id && - updateSubIssue( - workspaceSlug, - issue.project_id, - parentIssueId, - issueId, - { - state_id: val, - }, - { ...issue } - ) - } - disabled={!disabled} - buttonVariant="border-with-text" - /> -
-
- - -
- - issue.project_id && - updateSubIssue(workspaceSlug, issue.project_id, parentIssueId, issueId, { - priority: val, - }) - } - disabled={!disabled} - buttonVariant="border-without-text" - buttonClassName="border" - /> -
-
-
void; subIssueOperations: TSubIssueOperations; issueServiceType?: TIssueServiceType; + storeType: EIssuesStoreType; }; export const SubIssuesListRoot: React.FC = observer((props) => { @@ -29,36 +34,89 @@ export const SubIssuesListRoot: React.FC = observer((props) => { projectId, parentIssueId, rootIssueId, - spacingLeft = 10, disabled, handleIssueCrudState, subIssueOperations, issueServiceType = EIssueServiceType.ISSUES, + storeType = EIssuesStoreType.PROJECT, + spacingLeft = 0, } = props; // store hooks const { - subIssues: { subIssuesByIssueId }, + subIssues: { + subIssuesByIssueId, + filters: { getSubIssueFilters, getGroupedSubWorkItems, getFilteredSubWorkItems, resetFilters }, + }, } = useIssueDetail(issueServiceType); + // derived values - const subIssueIds = subIssuesByIssueId(parentIssueId); + const filters = getSubIssueFilters(parentIssueId); + const isRootLevel = useMemo(() => rootIssueId === parentIssueId, [rootIssueId, parentIssueId]); + const group_by = isRootLevel ? (filters?.displayFilters?.group_by ?? null) : null; + const filteredSubWorkItemsCount = (getFilteredSubWorkItems(rootIssueId, filters.filters ?? {}) ?? []).length; + + const groups = getGroupByColumns({ + groupBy: group_by as GroupByColumnTypes, + includeNone: true, + isWorkspaceLevel: isWorkspaceLevel(storeType), + isEpic: issueServiceType === EIssueServiceType.EPICS, + projectId, + }); + + const getWorkItemIds = useCallback( + (groupId: string) => { + if (isRootLevel) { + const groupedSubIssues = getGroupedSubWorkItems(parentIssueId); + return groupedSubIssues?.[groupId] ?? []; + } + const subIssueIds = subIssuesByIssueId(parentIssueId); + return subIssueIds ?? []; + }, + [isRootLevel, subIssuesByIssueId, parentIssueId, getGroupedSubWorkItems] + ); + + const isSubWorkItems = issueServiceType === EIssueServiceType.ISSUES; return (
- {subIssueIds?.map((issueId) => ( - 0 ? ( + groups?.map((group) => ( + + )) + ) : ( + } + customClassName={storeType !== EIssuesStoreType.EPIC ? "border-none" : ""} + actionElement={ + + } /> - ))} + )}
); }); diff --git a/web/core/components/issues/issue-detail-widgets/sub-issues/title-actions.tsx b/web/core/components/issues/issue-detail-widgets/sub-issues/title-actions.tsx index 80f9af6fe..e9e4393ed 100644 --- a/web/core/components/issues/issue-detail-widgets/sub-issues/title-actions.tsx +++ b/web/core/components/issues/issue-detail-widgets/sub-issues/title-actions.tsx @@ -1,58 +1,89 @@ import { FC, useCallback } from "react"; +import cloneDeep from "lodash/cloneDeep"; import { observer } from "mobx-react"; import { EIssueFilterType, EIssueServiceType, ISSUE_DISPLAY_FILTERS_BY_PAGE } from "@plane/constants"; -import { IIssueDisplayFilterOptions, IIssueDisplayProperties, TIssueServiceType } from "@plane/types"; -import { useIssueDetail } from "@/hooks/store"; +import { + IIssueDisplayFilterOptions, + IIssueDisplayProperties, + IIssueFilterOptions, + TIssueServiceType, +} from "@plane/types"; +import { useIssueDetail, useMember, useProjectState } from "@/hooks/store"; import { SubIssueDisplayFilters } from "./display-filters"; +import { SubIssueFilters } from "./filters"; import { SubIssuesActionButton } from "./quick-action-button"; type TSubWorkItemTitleActionsProps = { disabled: boolean; issueServiceType?: TIssueServiceType; parentId: string; - workspaceSlug: string; projectId: string; }; export const SubWorkItemTitleActions: FC = observer((props) => { - const { disabled, issueServiceType = EIssueServiceType.ISSUES, parentId, workspaceSlug, projectId } = props; + const { disabled, issueServiceType = EIssueServiceType.ISSUES, parentId, projectId } = props; // store hooks const { subIssues: { - filters: { getSubIssueFilters, updateSubIssueFilters }, + filters: { getSubIssueFilters, updateSubWorkItemFilters }, }, } = useIssueDetail(issueServiceType); + const { getProjectStates } = useProjectState(); + const { + project: { getProjectMemberIds }, + } = useMember(); // derived values const subIssueFilters = getSubIssueFilters(parentId); + const projectStates = getProjectStates(projectId); + const projectMemberIds = getProjectMemberIds(projectId, false); const layoutDisplayFiltersOptions = ISSUE_DISPLAY_FILTERS_BY_PAGE["sub_work_items"].list; const handleDisplayFilters = useCallback( (updatedDisplayFilter: Partial) => { - if (!workspaceSlug || !projectId) return; - updateSubIssueFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, updatedDisplayFilter, parentId); + updateSubWorkItemFilters(EIssueFilterType.DISPLAY_FILTERS, updatedDisplayFilter, parentId); }, - [workspaceSlug, projectId, parentId, updateSubIssueFilters] + [updateSubWorkItemFilters, parentId] ); const handleDisplayPropertiesUpdate = useCallback( (updatedDisplayProperties: Partial) => { - if (!workspaceSlug || !projectId) return; - updateSubIssueFilters( - workspaceSlug, - projectId, - EIssueFilterType.DISPLAY_PROPERTIES, - updatedDisplayProperties, - parentId - ); + updateSubWorkItemFilters(EIssueFilterType.DISPLAY_PROPERTIES, updatedDisplayProperties, parentId); }, - [workspaceSlug, projectId, parentId, updateSubIssueFilters] + [updateSubWorkItemFilters, parentId] + ); + + const handleFiltersUpdate = useCallback( + (key: keyof IIssueFilterOptions, value: string | string[]) => { + const newValues = cloneDeep(subIssueFilters?.filters?.[key]) ?? []; + + if (Array.isArray(value)) { + // this validation is majorly for the filter start_date, target_date custom + value.forEach((val) => { + if (!newValues.includes(val)) newValues.push(val); + else newValues.splice(newValues.indexOf(val), 1); + }); + } else { + if (subIssueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1); + else newValues.push(value); + } + + updateSubWorkItemFilters(EIssueFilterType.FILTERS, { [key]: newValues }, parentId); + }, + [subIssueFilters?.filters, updateSubWorkItemFilters, parentId] ); return ( -
+ // prevent click everywhere +
{ + e.stopPropagation(); + e.preventDefault(); + }} + > = observ handleDisplayPropertiesUpdate={handleDisplayPropertiesUpdate} handleDisplayFiltersUpdate={handleDisplayFilters} /> + {!disabled && ( )} diff --git a/web/core/components/issues/issue-detail-widgets/sub-issues/title.tsx b/web/core/components/issues/issue-detail-widgets/sub-issues/title.tsx index ceccf95dd..041f0ca23 100644 --- a/web/core/components/issues/issue-detail-widgets/sub-issues/title.tsx +++ b/web/core/components/issues/issue-detail-widgets/sub-issues/title.tsx @@ -33,10 +33,7 @@ export const SubIssuesCollapsibleTitle: FC = observer((props) => { const { t } = useTranslation(); // store hooks const { - subIssues: { - subIssuesByIssueId, - stateDistributionByIssueId, - }, + subIssues: { subIssuesByIssueId, stateDistributionByIssueId }, } = useIssueDetail(issueServiceType); // derived values const subIssuesDistribution = stateDistributionByIssueId(parentIssueId); @@ -63,7 +60,6 @@ export const SubIssuesCollapsibleTitle: FC = observer((props) => { } actionItemElement={ [EIssuesStoreType.PROFILE, EIssuesStoreType.GLOBAL].includes(type) ? true : false; @@ -66,6 +74,7 @@ type TGetGroupByColumns = { includeNone: boolean; isWorkspaceLevel: boolean; isEpic?: boolean; + projectId?: string; }; // NOTE: Type of groupBy is different compared to what's being passed from the components. @@ -76,6 +85,7 @@ export const getGroupByColumns = ({ includeNone, isWorkspaceLevel, isEpic = false, + projectId, }: TGetGroupByColumns): IGroupByColumn[] | undefined => { // If no groupBy is specified and includeNone is true, return "All Issues" group if (!groupBy && includeNone) { @@ -93,21 +103,24 @@ export const getGroupByColumns = ({ if (!groupBy) return undefined; // Map of group by options to their corresponding column getter functions - const groupByColumnMap: Record IGroupByColumn[] | undefined> = { + const groupByColumnMap: Record< + GroupByColumnTypes, + ({ isWorkspaceLevel, projectId }: TGetColumns) => IGroupByColumn[] | undefined + > = { project: getProjectColumns, cycle: getCycleColumns, module: getModuleColumns, state: getStateColumns, "state_detail.group": getStateGroupColumns, priority: getPriorityColumns, - labels: () => getLabelsColumns(isWorkspaceLevel), + labels: getLabelsColumns, assignees: getAssigneeColumns, created_by: getCreatedByColumns, team_project: getTeamProjectColumns, }; // Get and return the columns for the specified group by option - return groupByColumnMap[groupBy]?.(); + return groupByColumnMap[groupBy]?.({ isWorkspaceLevel, projectId }); }; const getProjectColumns = (): IGroupByColumn[] | undefined => { @@ -190,11 +203,12 @@ const getModuleColumns = (): IGroupByColumn[] | undefined => { return modules; }; -const getStateColumns = (): IGroupByColumn[] | undefined => { - const { projectStates } = store.state; - if (!projectStates) return; +const getStateColumns = ({ projectId }: TGetColumns): IGroupByColumn[] | undefined => { + const { getProjectStates, projectStates } = store.state; + const _states = projectId ? getProjectStates(projectId) : projectStates; + if (!_states) return; // map project states to group by columns - return projectStates.map((state) => ({ + return _states.map((state) => ({ id: state.id, name: state.name, icon: ( @@ -232,7 +246,7 @@ const getPriorityColumns = (): IGroupByColumn[] => { })); }; -const getLabelsColumns = (isWorkspaceLevel: boolean = false): IGroupByColumn[] => { +const getLabelsColumns = ({ isWorkspaceLevel }: TGetColumns): IGroupByColumn[] => { const { workspaceLabels, projectLabels } = store.label; // map labels to group by columns const labels = [ @@ -250,22 +264,40 @@ const getLabelsColumns = (isWorkspaceLevel: boolean = false): IGroupByColumn[] = })); }; -const getAssigneeColumns = (): IGroupByColumn[] | undefined => { +const getAssigneeColumns = ({ isWorkspaceLevel, projectId }: TGetColumns): IGroupByColumn[] | undefined => { + const assigneeColumns: IGroupByColumn[] = []; const { - project: { projectMemberIds }, + project: { projectMemberIds, getProjectMemberIds }, getUserDetails, } = store.memberRoot; - if (!projectMemberIds) return; - // Map project member ids to group by assignee columns - const assigneeColumns: IGroupByColumn[] = projectMemberIds.map((memberId) => { - const member = getUserDetails(memberId); - return { - id: memberId, - name: member?.display_name || "", - icon: , - payload: { assignee_ids: [memberId] }, - }; - }); + // if workspace level + if (isWorkspaceLevel) { + const { workspaceMemberIds } = store.memberRoot.workspace; + if (!workspaceMemberIds) return; + workspaceMemberIds.forEach((memberId) => { + const member = getUserDetails(memberId); + assigneeColumns.push({ + id: memberId, + name: member?.display_name || "", + icon: , + payload: { assignee_ids: [memberId] }, + }); + }); + } else { + // if project level + const _projectMemberIds = projectId ? getProjectMemberIds(projectId, false) : projectMemberIds; + if (!_projectMemberIds) return; + // Map project member ids to group by assignee columns + _projectMemberIds.forEach((memberId) => { + const member = getUserDetails(memberId); + assigneeColumns.push({ + id: memberId, + name: member?.display_name || "", + icon: , + payload: { assignee_ids: [memberId] }, + }); + }); + } assigneeColumns.push({ id: "None", name: "None", icon: , payload: {} }); return assigneeColumns; }; @@ -719,3 +751,37 @@ export const SpreadSheetPropertyIcon: FC = (pro if (!Icon) return null; return ; }; + +/** + * This method returns if the filters are applied + * @param filters + * @returns + */ +export const isDisplayFiltersApplied = (filters: Partial): boolean => { + const isDisplayPropertiesApplied = Object.keys(DEFAULT_DISPLAY_PROPERTIES).some( + (key) => !filters.displayProperties?.[key as keyof IIssueDisplayProperties] + ); + + const isDisplayFiltersApplied = Object.keys(filters.displayFilters ?? {}).some((key) => { + const value = filters.displayFilters?.[key as keyof IIssueDisplayFilterOptions]; + if (!value) return false; + // -create_at is the default order + if (key === "order_by") { + return value !== "-created_at"; + } + return true; + }); + + return isDisplayPropertiesApplied || isDisplayFiltersApplied; +}; + +/** + * This method returns if the filters are applied + * @param filters + * @returns + */ +export const isFiltersApplied = (filters: IIssueFilterOptions): boolean => + Object.values(filters).some((value) => { + if (Array.isArray(value)) return value.length > 0; + return value !== undefined && value !== null && value !== ""; + }); diff --git a/web/core/store/issue/helpers/base-issues-utils.ts b/web/core/store/issue/helpers/base-issues-utils.ts index a9fc639b4..dcb4e86ad 100644 --- a/web/core/store/issue/helpers/base-issues-utils.ts +++ b/web/core/store/issue/helpers/base-issues-utils.ts @@ -1,8 +1,24 @@ +import cloneDeep from "lodash/cloneDeep"; +import groupBy from "lodash/groupBy"; +import indexOf from "lodash/indexOf"; import isEmpty from "lodash/isEmpty"; +import orderBy from "lodash/orderBy"; +import set from "lodash/set"; import uniq from "lodash/uniq"; -import { ALL_ISSUES } from "@plane/constants"; -import { TIssue } from "@plane/types"; -import { EIssueGroupedAction } from "./base-issues.store"; +import { runInAction } from "mobx"; +import { ALL_ISSUES, EIssueFilterType, FILTER_TO_ISSUE_MAP, ISSUE_PRIORITIES } from "@plane/constants"; +import { + IIssueDisplayFilterOptions, + IIssueDisplayProperties, + IIssueFilterOptions, + IIssueFilters, + TIssue, + TIssueGroupByOptions, + TIssueOrderByOptions, +} from "@plane/types"; +import { checkDateCriteria, convertToISODateString, parseDateFilter } from "@/helpers/date-time.helper"; +import { store } from "@/lib/store-context"; +import { EIssueGroupedAction, ISSUE_GROUP_BY_KEY } from "./base-issues.store"; /** * returns, @@ -173,3 +189,192 @@ export const getSortOrderToFilterEmptyValues = (key: string, object: any) => { // get IssueIds from Issue data List export const getIssueIds = (issues: TIssue[]) => issues.map((issue) => issue?.id); + +/** + * Checks if an issue meets the date filter criteria + * @param issue The issue to check + * @param filterKey The date field to check ('start_date' or 'target_date') + * @param dateFilters Array of date filter strings + * @returns boolean indicating if the issue meets the date criteria + */ +export const checkIssueDateFilter = ( + issue: TIssue, + filterKey: "start_date" | "target_date", + dateFilters: string[] +): boolean => { + if (!dateFilters || dateFilters.length === 0) return true; + + const issueDate = issue[filterKey]; + if (!issueDate) return false; + + // Issue should match all the date filters (AND operation) + return dateFilters.every((filterValue) => { + const parsed = parseDateFilter(filterValue); + if (!parsed?.date || !parsed?.type) { + // ignore invalid filter instead of failing the whole evaluation + console.warn(`[filters] Ignoring unparsable date filter "${filterValue}"`); + return true; + } + return checkDateCriteria(new Date(issueDate), parsed.date, parsed.type); + }); +}; + +/** + * Helper method to get previous issues state + * @param issues - The array of issues to get the previous state for. + * @returns The previous state of the issues. + */ +export const getPreviousIssuesState = (issues: TIssue[]) => { + const issueIds = issues.map((issue) => issue.id); + const issuesPreviousState: Record = {}; + issueIds.forEach((issueId) => { + if (store.issue.issues.issuesMap[issueId]) { + issuesPreviousState[issueId] = cloneDeep(store.issue.issues.issuesMap[issueId]); + } + }); + return issuesPreviousState; +}; + +/** + * Filters the given work items based on the provided filters and display filters. + * @param work items - The array of work items to be filtered. + * @param filters - The filters to be applied to the issues. + * @param displayFilters - The display filters to be applied to the issues. + * @returns The filtered array of issues. + */ +export const getFilteredWorkItems = (workItems: TIssue[], filters: IIssueFilterOptions | undefined): TIssue[] => { + if (!filters) return workItems; + // Get all active filters + const activeFilters = Object.entries(filters).filter(([, value]) => value && value.length > 0); + // If no active filters, return all issues + if (activeFilters.length === 0) { + return workItems; + } + + return workItems.filter((workItem) => + // Check all filter conditions (AND operation between different filters) + activeFilters.every(([filterKey, filterValues]) => { + // Handle date filters separately + if (filterKey === "start_date" || filterKey === "target_date") { + return checkIssueDateFilter(workItem, filterKey as "start_date" | "target_date", filterValues as string[]); + } + // Handle regular filters + const issueKey = FILTER_TO_ISSUE_MAP[filterKey as keyof IIssueFilterOptions]; + if (!issueKey) return true; // Skip if no mapping exists + const issueValue = workItem[issueKey as keyof TIssue]; + // Handle array-based properties vs single value properties + if (Array.isArray(issueValue)) { + return filterValues!.some((filterValue: any) => issueValue.includes(filterValue)); + } else { + return filterValues!.includes(issueValue as string); + } + }) + ); +}; + +/** + * Orders the given work items based on the provided order by key. + * @param workItems - The array of work items to be ordered. + * @param orderByKey - The key to order the issues by. + * @returns The ordered array of work items. + */ +export const getOrderedWorkItems = (workItems: TIssue[], orderByKey: TIssueOrderByOptions): string[] => { + switch (orderByKey) { + case "-updated_at": + return getIssueIds(orderBy(workItems, (item) => convertToISODateString(item["updated_at"]), ["desc"])); + + case "-created_at": + return getIssueIds(orderBy(workItems, (item) => convertToISODateString(item["created_at"]), ["desc"])); + + case "-start_date": + return getIssueIds( + orderBy( + workItems, + [getSortOrderToFilterEmptyValues.bind(null, "start_date"), "start_date"], //preferring sorting based on empty values to always keep the empty values below + ["asc", "desc"] + ) + ); + + case "-priority": { + const sortArray = ISSUE_PRIORITIES.map((i) => i.key); + return getIssueIds( + orderBy(workItems, (currentIssue: TIssue) => indexOf(sortArray, currentIssue?.priority), ["asc"]) + ); + } + default: + return getIssueIds(workItems); + } +}; + +export const getGroupedWorkItemIds = ( + workItems: TIssue[], + groupByKey?: TIssueGroupByOptions, + orderByKey: TIssueOrderByOptions = "-created_at" +): Record => { + // If group by is not set set default as ALL ISSUES + if (!groupByKey) { + return { + [ALL_ISSUES]: getOrderedWorkItems(workItems, orderByKey), + }; + } + + // Group work items + const groupKey = ISSUE_GROUP_BY_KEY[groupByKey]; + const groupedWorkItems = groupBy(workItems, (item) => { + const value = item[groupKey]; + if (Array.isArray(value)) { + if (value.length === 0) return "None"; + // Sort & join to build deterministic set-like key + return value.slice().sort().join(","); + } + return value ?? "None"; + }); + + // Convert to Record type + const groupedWorkItemsRecord: Record = {}; + Object.entries(groupedWorkItems).forEach(([key, items]) => { + groupedWorkItemsRecord[key] = getOrderedWorkItems(items as TIssue[], orderByKey); + }); + + return groupedWorkItemsRecord; +}; + +/** + * Updates the filters for a given work item. + * @param filtersMap - The map of filters for the work item. + * @param filterType - The type of filter to update. + * @param filters - The filters to update. + * @param workItemId - The ID of the work item to update. + */ +export const updateFilters = ( + filtersMap: Record>, + filterType: EIssueFilterType, + filters: IIssueDisplayFilterOptions | IIssueDisplayProperties | IIssueFilterOptions, + workItemId: string +) => { + const existingFilters = filtersMap[workItemId] ?? {}; + const _filters = { + filters: existingFilters.filters, + displayFilters: existingFilters.displayFilters, + displayProperties: existingFilters.displayProperties, + }; + + switch (filterType) { + case EIssueFilterType.FILTERS: { + const updatedFilters = filters as IIssueFilterOptions; + _filters.filters = { ..._filters.filters, ...updatedFilters }; + set(filtersMap, [workItemId, "filters"], { ..._filters.filters, ...updatedFilters }); + break; + } + case EIssueFilterType.DISPLAY_FILTERS: { + set(filtersMap, [workItemId, "displayFilters"], { ..._filters.displayFilters, ...filters }); + break; + } + case EIssueFilterType.DISPLAY_PROPERTIES: + set(filtersMap, [workItemId, "displayProperties"], { + ..._filters.displayProperties, + ...filters, + }); + break; + } +}; diff --git a/web/core/store/issue/helpers/base-issues.store.ts b/web/core/store/issue/helpers/base-issues.store.ts index d8a7dfa18..b28ff13fa 100644 --- a/web/core/store/issue/helpers/base-issues.store.ts +++ b/web/core/store/issue/helpers/base-issues.store.ts @@ -118,10 +118,10 @@ export interface IBaseIssuesStore { } // This constant maps the group by keys to the respective issue property that the key relies on -const ISSUE_GROUP_BY_KEY: Record = { +export const ISSUE_GROUP_BY_KEY: Record = { project: "project_id", state: "state_id", - "state_detail.group": "state_id" as keyof TIssue, // state_detail.group is only being used for state_group display, + "state_detail.group": "state__group" as keyof TIssue, // state_detail.group is only being used for state_group display, priority: "priority", labels: "label_ids", created_by: "created_by", diff --git a/web/core/store/issue/issue-details/sub_issues.store.ts b/web/core/store/issue/issue-details/sub_issues.store.ts index d77c42d30..a8be50f88 100644 --- a/web/core/store/issue/issue-details/sub_issues.store.ts +++ b/web/core/store/issue/issue-details/sub_issues.store.ts @@ -4,6 +4,7 @@ import set from "lodash/set"; import uniq from "lodash/uniq"; import update from "lodash/update"; import { action, makeObservable, observable, runInAction } from "mobx"; +import { computedFn } from "mobx-utils"; import { EIssueServiceType } from "@plane/constants"; // types import { @@ -14,8 +15,6 @@ import { TSubIssuesStateDistribution, TIssueServiceType, TLoader, - TGroupedIssues, - TGroupedIssueCount, } from "@plane/types"; // services import { updatePersistentLayer } from "@/local-db/utils/utils"; @@ -51,8 +50,6 @@ export interface IIssueSubIssuesStore extends IIssueSubIssuesStoreActions { // observables subIssuesStateDistribution: TIssueSubIssuesStateDistributionMap; subIssues: TIssueSubIssuesIdMap; - groupedSubIssuesMap: Record; - groupedSubIssuesCount: TGroupedIssueCount; subIssueHelpers: Record; // parent_issue_id -> TSubIssueHelpers loader: TLoader; filters: IWorkItemSubIssueFiltersStore; @@ -60,7 +57,6 @@ export interface IIssueSubIssuesStore extends IIssueSubIssuesStoreActions { stateDistributionByIssueId: (issueId: string) => TSubIssuesStateDistribution | undefined; subIssuesByIssueId: (issueId: string) => string[] | undefined; subIssueHelpersByIssueId: (issueId: string) => TSubIssueHelpers; - groupedSubIssuesByIssueId: (issueId: string) => TGroupedIssues | undefined; // actions fetchOtherProjectProperties: (workspaceSlug: string, projectIds: string[]) => Promise; setSubIssueHelpers: (parentIssueId: string, key: TSubIssueHelpersKeys, value: string) => void; @@ -70,8 +66,6 @@ export class IssueSubIssuesStore implements IIssueSubIssuesStore { // observables subIssuesStateDistribution: TIssueSubIssuesStateDistributionMap = {}; subIssues: TIssueSubIssuesIdMap = {}; - groupedSubIssuesMap: Record = {}; - groupedSubIssuesCount: TGroupedIssueCount = {}; subIssueHelpers: Record = {}; loader: TLoader = undefined; @@ -88,7 +82,6 @@ export class IssueSubIssuesStore implements IIssueSubIssuesStore { subIssuesStateDistribution: observable, subIssues: observable, subIssueHelpers: observable, - groupedSubIssuesMap: observable, loader: observable.ref, // actions setSubIssueHelpers: action, @@ -98,7 +91,6 @@ export class IssueSubIssuesStore implements IIssueSubIssuesStore { removeSubIssue: action, deleteSubIssue: action, fetchOtherProjectProperties: action, - groupedSubIssuesByIssueId: action, }); this.filters = new WorkItemSubIssueFiltersStore(this); // root store @@ -114,12 +106,7 @@ export class IssueSubIssuesStore implements IIssueSubIssuesStore { return this.subIssuesStateDistribution[issueId] ?? undefined; }; - subIssuesByIssueId = (issueId: string) => { - if (!issueId) return undefined; - return this.subIssues[issueId] ?? undefined; - }; - - groupedSubIssuesByIssueId = (issueId: string) => this.groupedSubIssuesMap[issueId] ?? undefined; + subIssuesByIssueId = computedFn((issueId: string) => this.subIssues[issueId]); subIssueHelpersByIssueId = (issueId: string) => ({ preview_loader: this.subIssueHelpers?.[issueId]?.preview_loader || [], @@ -138,20 +125,17 @@ export class IssueSubIssuesStore implements IIssueSubIssuesStore { }; fetchSubIssues = async (workspaceSlug: string, projectId: string, parentIssueId: string) => { - // get filter params - const filterParams = this.filters.computedFilterParams(parentIssueId); - const response = await this.issueService.subIssues(workspaceSlug, projectId, parentIssueId, filterParams); + this.loader = "init-loader"; + + const response = await this.issueService.subIssues(workspaceSlug, projectId, parentIssueId); const subIssuesStateDistribution = response?.state_distribution ?? {}; - // process sub issues response - const { issueList, groupedIssues } = this.filters.processSubIssueResponse(response.sub_issues); - - // set grouped issues count - set(this.groupedSubIssuesMap, [parentIssueId], groupedIssues); + const issueList = (response.sub_issues ?? []) as TIssue[]; this.rootIssueDetailStore.rootIssueStore.issues.addIssue(issueList); + // fetch other issues states and members when sub-issues are from different project if (issueList && issueList.length > 0) { const otherProjectIds = uniq( issueList.map((issue) => issue.project_id).filter((id) => !!id && id !== projectId) @@ -163,6 +147,7 @@ export class IssueSubIssuesStore implements IIssueSubIssuesStore { sub_issues_count: issueList.length, }); } + runInAction(() => { set(this.subIssuesStateDistribution, parentIssueId, subIssuesStateDistribution); set( @@ -171,6 +156,8 @@ export class IssueSubIssuesStore implements IIssueSubIssuesStore { issueList.map((issue) => issue.id) ); }); + + this.loader = undefined; return response; }; diff --git a/web/core/store/issue/issue-details/sub_issues_filter.store.ts b/web/core/store/issue/issue-details/sub_issues_filter.store.ts index 47edf767c..d68583b68 100644 --- a/web/core/store/issue/issue-details/sub_issues_filter.store.ts +++ b/web/core/store/issue/issue-details/sub_issues_filter.store.ts @@ -1,202 +1,139 @@ import set from "lodash/set"; -import { action, makeObservable, observable } from "mobx"; -import { ALL_ISSUES, EIssueFilterType, EIssueGroupByToServerOptions } from "@plane/constants"; +import { action, makeObservable, observable, runInAction } from "mobx"; +import { computedFn } from "mobx-utils"; +import { EIssueFilterType } from "@plane/constants"; import { IIssueDisplayFilterOptions, IIssueDisplayProperties, + IIssueFilterOptions, IIssueFilters, - TGroupedIssueCount, TGroupedIssues, TIssue, - TIssueParams, - TIssues, - TSubGroupedIssues, - TSubIssueResponse, } from "@plane/types"; -import { IIssueSubIssuesStore } from "./sub_issues.store"; +import { getFilteredWorkItems, getGroupedWorkItemIds, updateFilters } from "../helpers/base-issues-utils"; +import { IssueSubIssuesStore } from "./sub_issues.store"; + +export const DEFAULT_DISPLAY_PROPERTIES = { + key: true, + issue_type: true, + assignee: true, + start_date: true, + due_date: true, + labels: true, + priority: true, + state: true, +}; export interface IWorkItemSubIssueFiltersStore { - subIssueFiltersMap: Record>; + subIssueFilters: Record>; // helpers methods - updateSubIssueFilters: ( - workspaceSlug: string, - projectId: string, + updateSubWorkItemFilters: ( filterType: EIssueFilterType, - filters: IIssueDisplayFilterOptions | IIssueDisplayProperties, - parentId: string - ) => Promise; - getSubIssueFilters: (parentId: string) => Partial; - computedFilterParams: (parentId: string) => Partial>; - processSubIssueResponse: (issueResponse: TSubIssueResponse) => { - issueList: TIssue[]; - groupedIssues: TIssues; - groupedIssueCount: TGroupedIssueCount; - }; + filters: IIssueDisplayFilterOptions | IIssueDisplayProperties | IIssueFilterOptions, + workItemId: string + ) => void; + getGroupedSubWorkItems: (workItemId: string) => TGroupedIssues; + getFilteredSubWorkItems: (workItemId: string, filters: IIssueFilterOptions) => TIssue[]; + getSubIssueFilters: (workItemId: string) => Partial; + resetFilters: (workItemId: string) => void; } export class WorkItemSubIssueFiltersStore implements IWorkItemSubIssueFiltersStore { // observables - subIssueFiltersMap: Record> = {}; + subIssueFilters: Record> = {}; - subIssueStore: IIssueSubIssuesStore; + // root store + subIssueStore: IssueSubIssuesStore; - constructor(subIssueStore: IIssueSubIssuesStore) { + constructor(subIssueStore: IssueSubIssuesStore) { makeObservable(this, { - subIssueFiltersMap: observable, - updateSubIssueFilters: action, + subIssueFilters: observable, + updateSubWorkItemFilters: action, getSubIssueFilters: action, }); - // sub issue store + + // root store this.subIssueStore = subIssueStore; } - /** - * @description This method is used to initialize the sub issue filters - * @param parentId - */ - initSubIssueFilters = (parentId: string) => { - set(this.subIssueFiltersMap, [parentId], { - displayFilters: {}, - displayProperties: { - key: true, - issue_type: true, - assignee: true, - start_date: true, - due_date: true, - labels: true, - priority: true, - state: true, - }, - }); - }; - - /** - * @description This method is used to process the sub issue response to provide the data to update the store - * @param issueResponse - * @returns issueList, list of issues data - * @returns groupedIssues, grouped issue ids - * @returns groupedIssueCount, object containing issue counts of individual groups - */ - processSubIssueResponse = ( - issueResponse: TSubIssueResponse - ): { - issueList: TIssue[]; - groupedIssues: TIssues; - groupedIssueCount: TGroupedIssueCount; - } => { - const issueResult = issueResponse; - - if (!issueResult) { - return { - issueList: [], - groupedIssues: {}, - groupedIssueCount: {}, - }; - } - - //if is an array then it's an ungrouped response. return values with groupId as ALL_ISSUES - if (Array.isArray(issueResult)) { - return { - issueList: issueResult, - groupedIssues: { - [ALL_ISSUES]: issueResult.map((issue) => issue.id), - }, - groupedIssueCount: { - [ALL_ISSUES]: issueResult.length, - }, - }; - } - - const issueList: TIssue[] = []; - const groupedIssues: TGroupedIssues | TSubGroupedIssues = {}; - const groupedIssueCount: TGroupedIssueCount = {}; - - // update total issue count to ALL_ISSUES - set(groupedIssueCount, [ALL_ISSUES], issueResult.length); - - // loop through all the groupIds from issue Result - for (const groupId in issueResult) { - const groupIssueResult = issueResult[groupId]; - - // if groupIssueResult is undefined then continue the loop - if (!groupIssueResult) continue; - - // set grouped Issue count of the current groupId - set(groupedIssueCount, [groupId], groupIssueResult.length); - - // add the result to issueList - issueList.push(...groupIssueResult); - // set the issue Ids to the groupId path - set( - groupedIssues, - [groupId], - groupIssueResult.map((issue) => issue.id) - ); - } - - return { issueList, groupedIssues, groupedIssueCount }; - }; - /** * @description This method is used to get the sub issue filters - * @param parentId - * @returns IIssueFilters + * @param workItemId + * @returns */ - getSubIssueFilters = (parentId: string) => { - if (!this.subIssueFiltersMap[parentId]) { - this.initSubIssueFilters(parentId); + getSubIssueFilters = (workItemId: string) => { + if (!this.subIssueFilters[workItemId]) { + this.initializeFilters(workItemId); } - return this.subIssueFiltersMap[parentId]; - }; - - computedFilterParams = (parentId: string) => { - const displayFilters = this.getSubIssueFilters(parentId).displayFilters; - - const computedFilters: Partial> = { - order_by: displayFilters?.order_by || undefined, - group_by: displayFilters?.group_by ? EIssueGroupByToServerOptions[displayFilters.group_by] : undefined, - }; - - const issueFiltersParams: Partial> = {}; - Object.keys(computedFilters).forEach((key) => { - const _key = key as TIssueParams; - const _value: string | boolean | string[] | undefined = computedFilters[_key]; - const nonEmptyArrayValue = Array.isArray(_value) && _value.length === 0 ? undefined : _value; - if (nonEmptyArrayValue != undefined) - issueFiltersParams[_key] = Array.isArray(nonEmptyArrayValue) - ? nonEmptyArrayValue.join(",") - : nonEmptyArrayValue; - }); - - return issueFiltersParams; + return this.subIssueFilters[workItemId]; }; /** - * @description This method is used to update the sub issue filters - * @param projectId + * @description This method is used to initialize the sub issue filters + * @param workItemId + */ + initializeFilters = (workItemId: string) => { + set(this.subIssueFilters, [workItemId, "displayProperties"], DEFAULT_DISPLAY_PROPERTIES); + set(this.subIssueFilters, [workItemId, "filters"], {}); + set(this.subIssueFilters, [workItemId, "displayFilters"], {}); + }; + + /** + * @description This method updates filters for sub issues. * @param filterType * @param filters */ - updateSubIssueFilters = async ( - workspaceSlug: string, - projectId: string, + updateSubWorkItemFilters = ( filterType: EIssueFilterType, - filters: IIssueDisplayFilterOptions | IIssueDisplayProperties, - parentId: string + filters: IIssueDisplayFilterOptions | IIssueDisplayProperties | IIssueFilterOptions, + workItemId: string ) => { - const _filters = this.getSubIssueFilters(parentId); - switch (filterType) { - case EIssueFilterType.DISPLAY_FILTERS: { - set(this.subIssueFiltersMap, [parentId, "displayFilters"], { ..._filters.displayFilters, ...filters }); - this.subIssueStore.fetchSubIssues(workspaceSlug, projectId, parentId); - break; - } - case EIssueFilterType.DISPLAY_PROPERTIES: - set(this.subIssueFiltersMap, [parentId, "displayProperties"], { - ..._filters.displayProperties, - ...filters, - }); - break; - } + runInAction(() => { + updateFilters(this.subIssueFilters, filterType, filters, workItemId); + }); + }; + + /** + * @description This method is used to get the grouped sub work items + * @param parentWorkItemId + * @returns + */ + getGroupedSubWorkItems = computedFn((parentWorkItemId: string) => { + const subIssueFilters = this.getSubIssueFilters(parentWorkItemId); + + const filteredWorkItems = this.getFilteredSubWorkItems(parentWorkItemId, subIssueFilters.filters ?? {}); + + // get group by and order by + const groupByKey = subIssueFilters.displayFilters?.group_by; + const orderByKey = subIssueFilters.displayFilters?.order_by; + + const groupedWorkItemIds = getGroupedWorkItemIds(filteredWorkItems, groupByKey, orderByKey); + + return groupedWorkItemIds; + }); + + /** + * @description This method is used to get the filtered sub work items + * @param workItemId + * @returns + */ + getFilteredSubWorkItems = computedFn((workItemId: string, filters: IIssueFilterOptions) => { + const subIssueIds = this.subIssueStore.subIssuesByIssueId(workItemId); + const workItems = this.subIssueStore.rootIssueDetailStore.rootIssueStore.issues.getIssuesByIds( + subIssueIds, + "un-archived" + ); + + const filteredWorkItems = getFilteredWorkItems(workItems, filters); + + return filteredWorkItems; + }); + + /** + * @description This method is used to reset the filters + * @param workItemId + */ + resetFilters = (workItemId: string) => { + this.initializeFilters(workItemId); }; } diff --git a/web/helpers/date-time.helper.ts b/web/helpers/date-time.helper.ts index c26addd7c..cff0da2c2 100644 --- a/web/helpers/date-time.helper.ts +++ b/web/helpers/date-time.helper.ts @@ -405,3 +405,73 @@ export const generateDateArray = (startDate: string | Date, endDate: string | Da return dateArray; }; + +/** + * Processes relative date strings like "1_weeks", "2_months" etc and returns a Date + * @param value The relative date string (e.g., "1_weeks", "2_months") + * @returns Date object representing the calculated date + */ +export const processRelativeDate = (value: string): Date => { + const [amountStr, unit] = value.split("_"); + const amount = parseInt(amountStr, 10); + if (isNaN(amount)) { + throw new Error(`Invalid relative amount: ${amountStr}`); + } + const date = new Date(); + + switch (unit) { + case "days": + date.setDate(date.getDate() + amount); + break; + case "weeks": + date.setDate(date.getDate() + amount * 7); + break; + case "months": + date.setMonth(date.getMonth() + amount); + break; + default: + throw new Error(`Unsupported time unit: ${unit}`); + } + + return date; +}; + +/** + * Parses a date filter string and returns the comparison type and date + * @param filterValue The date filter string (e.g., "1_weeks;after;fromnow" or "2024-12-01;after") + * @returns Object containing the comparison type and target date + */ +export const parseDateFilter = (filterValue: string): { type: "after" | "before"; date: Date } => { + const parts = filterValue.split(";"); + const dateStr = parts[0]; + const type = parts[1] as "after" | "before"; + + let date: Date; + if (dateStr.includes("_")) { + // Handle relative dates (e.g., "1_weeks;after;fromnow") + date = processRelativeDate(dateStr); + } else { + // Handle absolute dates (e.g., "2024-12-01;after") + date = new Date(dateStr); + } + + return { type, date }; +}; + +/** + * Checks if a date meets the filter criteria + * @param dateToCheck The date to check + * @param filterDate The filter date to compare against + * @param type The type of comparison ('after' or 'before') + * @returns boolean indicating if the date meets the criteria + */ +export const checkDateCriteria = (dateToCheck: Date | null, filterDate: Date, type: "after" | "before"): boolean => { + if (!dateToCheck) return false; + + const checkDate = new Date(dateToCheck); + const normalizedCheck = new Date(checkDate.setHours(0, 0, 0, 0)); + const normalizedFilter = new Date(filterDate.getTime()); + normalizedFilter.setHours(0, 0, 0, 0); + + return type === "after" ? normalizedCheck >= normalizedFilter : normalizedCheck <= normalizedFilter; +}; From 30db59534dca5c0f891f7bcf77a4039d8b2363fb Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Fri, 9 May 2025 14:26:29 +0530 Subject: [PATCH 033/201] [WEB-3985] feat: common postcss config and local fonts across all plane applications (#6998) * [WEB-3985] feat: common postcss config and local fonts across all plane applications * improvement: split fonts into a separate exports --- admin/app/layout.tsx | 6 ++-- admin/package.json | 1 + admin/postcss.config.js | 10 ++---- admin/{app => styles}/globals.css | 31 +++++++++++------- admin/tsconfig.json | 3 +- packages/propel/package.json | 3 +- packages/propel/postcss.config.js | 8 ++--- packages/propel/src/globals.css | 12 ------- .../propel/src}/styles/fonts/Inter/LICENSE | 0 .../fonts/Inter/inter-v13-latin-200.woff2 | Bin .../fonts/Inter/inter-v13-latin-300.woff2 | Bin .../fonts/Inter/inter-v13-latin-500.woff2 | Bin .../fonts/Inter/inter-v13-latin-600.woff2 | Bin .../fonts/Inter/inter-v13-latin-700.woff2 | Bin .../fonts/Inter/inter-v13-latin-800.woff2 | Bin .../fonts/Inter/inter-v13-latin-regular.woff2 | Bin .../fonts/Material-Symbols-Rounded/LICENSE | 0 ...l-symbols-rounded-v168-latin-regular.woff2 | Bin .../propel/src/styles/fonts/index.css | 23 +++++++++---- space/package.json | 1 + space/styles/globals.css | 3 +- web/postcss.config.js | 10 ++---- web/styles/globals.css | 2 +- 23 files changed, 51 insertions(+), 62 deletions(-) rename admin/{app => styles}/globals.css (92%) delete mode 100644 packages/propel/src/globals.css rename {web => packages/propel/src}/styles/fonts/Inter/LICENSE (100%) rename {web => packages/propel/src}/styles/fonts/Inter/inter-v13-latin-200.woff2 (100%) rename {web => packages/propel/src}/styles/fonts/Inter/inter-v13-latin-300.woff2 (100%) rename {web => packages/propel/src}/styles/fonts/Inter/inter-v13-latin-500.woff2 (100%) rename {web => packages/propel/src}/styles/fonts/Inter/inter-v13-latin-600.woff2 (100%) rename {web => packages/propel/src}/styles/fonts/Inter/inter-v13-latin-700.woff2 (100%) rename {web => packages/propel/src}/styles/fonts/Inter/inter-v13-latin-800.woff2 (100%) rename {web => packages/propel/src}/styles/fonts/Inter/inter-v13-latin-regular.woff2 (100%) rename {web => packages/propel/src}/styles/fonts/Material-Symbols-Rounded/LICENSE (100%) rename {web => packages/propel/src}/styles/fonts/Material-Symbols-Rounded/material-symbols-rounded-v168-latin-regular.woff2 (100%) rename web/styles/fonts/main.css => packages/propel/src/styles/fonts/index.css (63%) diff --git a/admin/app/layout.tsx b/admin/app/layout.tsx index ef9559af8..b10e9186c 100644 --- a/admin/app/layout.tsx +++ b/admin/app/layout.tsx @@ -3,18 +3,16 @@ import { ReactNode } from "react"; import { ThemeProvider, useTheme } from "next-themes"; import { SWRConfig } from "swr"; -// ui +// plane imports import { ADMIN_BASE_PATH, DEFAULT_SWR_CONFIG } from "@plane/constants"; import { Toast } from "@plane/ui"; import { resolveGeneralTheme } from "@plane/utils"; -// constants -// helpers // lib import { InstanceProvider } from "@/lib/instance-provider"; import { StoreProvider } from "@/lib/store-provider"; import { UserProvider } from "@/lib/user-provider"; // styles -import "./globals.css"; +import "@/styles/globals.css"; const ToastWithTheme = () => { const { resolvedTheme } = useTheme(); diff --git a/admin/package.json b/admin/package.json index 9a7bff005..0ee292f03 100644 --- a/admin/package.json +++ b/admin/package.json @@ -17,6 +17,7 @@ "@headlessui/react": "^1.7.19", "@plane/constants": "*", "@plane/hooks": "*", + "@plane/propel": "*", "@plane/services": "*", "@plane/types": "*", "@plane/ui": "*", diff --git a/admin/postcss.config.js b/admin/postcss.config.js index 6887c8262..9b1e55fc4 100644 --- a/admin/postcss.config.js +++ b/admin/postcss.config.js @@ -1,8 +1,2 @@ -module.exports = { - plugins: { - "postcss-import": {}, - "tailwindcss/nesting": {}, - tailwindcss: {}, - autoprefixer: {}, - }, -}; +// eslint-disable-next-line @typescript-eslint/no-require-imports +module.exports = require("@plane/tailwind-config/postcss.config.js"); diff --git a/admin/app/globals.css b/admin/styles/globals.css similarity index 92% rename from admin/app/globals.css rename to admin/styles/globals.css index 0a2218c21..d5554ce2f 100644 --- a/admin/app/globals.css +++ b/admin/styles/globals.css @@ -1,5 +1,4 @@ -@import url("https://fonts.googleapis.com/css2?family=Inter:wght@200;300;400;500;600;700;800&display=swap"); -@import url("https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@48,400,0,0&display=swap"); +@import "@plane/propel/styles/fonts"; @tailwind base; @tailwind components; @@ -60,23 +59,31 @@ --color-border-300: 212, 212, 212; /* strong border- 1 */ --color-border-400: 185, 185, 185; /* strong border- 2 */ - --color-shadow-2xs: 0px 0px 1px 0px rgba(23, 23, 23, 0.06), 0px 1px 2px 0px rgba(23, 23, 23, 0.06), + --color-shadow-2xs: + 0px 0px 1px 0px rgba(23, 23, 23, 0.06), 0px 1px 2px 0px rgba(23, 23, 23, 0.06), 0px 1px 2px 0px rgba(23, 23, 23, 0.14); - --color-shadow-xs: 0px 1px 2px 0px rgba(0, 0, 0, 0.16), 0px 2px 4px 0px rgba(16, 24, 40, 0.12), + --color-shadow-xs: + 0px 1px 2px 0px rgba(0, 0, 0, 0.16), 0px 2px 4px 0px rgba(16, 24, 40, 0.12), 0px 1px 8px -1px rgba(16, 24, 40, 0.1); - --color-shadow-sm: 0px 1px 4px 0px rgba(0, 0, 0, 0.01), 0px 4px 8px 0px rgba(0, 0, 0, 0.02), - 0px 1px 12px 0px rgba(0, 0, 0, 0.12); - --color-shadow-rg: 0px 3px 6px 0px rgba(0, 0, 0, 0.1), 0px 4px 4px 0px rgba(16, 24, 40, 0.08), + --color-shadow-sm: + 0px 1px 4px 0px rgba(0, 0, 0, 0.01), 0px 4px 8px 0px rgba(0, 0, 0, 0.02), 0px 1px 12px 0px rgba(0, 0, 0, 0.12); + --color-shadow-rg: + 0px 3px 6px 0px rgba(0, 0, 0, 0.1), 0px 4px 4px 0px rgba(16, 24, 40, 0.08), 0px 1px 12px 0px rgba(16, 24, 40, 0.04); - --color-shadow-md: 0px 4px 8px 0px rgba(0, 0, 0, 0.12), 0px 6px 12px 0px rgba(16, 24, 40, 0.12), + --color-shadow-md: + 0px 4px 8px 0px rgba(0, 0, 0, 0.12), 0px 6px 12px 0px rgba(16, 24, 40, 0.12), 0px 1px 16px 0px rgba(16, 24, 40, 0.12); - --color-shadow-lg: 0px 6px 12px 0px rgba(0, 0, 0, 0.12), 0px 8px 16px 0px rgba(0, 0, 0, 0.12), + --color-shadow-lg: + 0px 6px 12px 0px rgba(0, 0, 0, 0.12), 0px 8px 16px 0px rgba(0, 0, 0, 0.12), 0px 1px 24px 0px rgba(16, 24, 40, 0.12); - --color-shadow-xl: 0px 0px 18px 0px rgba(0, 0, 0, 0.16), 0px 0px 24px 0px rgba(16, 24, 40, 0.16), + --color-shadow-xl: + 0px 0px 18px 0px rgba(0, 0, 0, 0.16), 0px 0px 24px 0px rgba(16, 24, 40, 0.16), 0px 0px 52px 0px rgba(16, 24, 40, 0.16); - --color-shadow-2xl: 0px 8px 16px 0px rgba(0, 0, 0, 0.12), 0px 12px 24px 0px rgba(16, 24, 40, 0.12), + --color-shadow-2xl: + 0px 8px 16px 0px rgba(0, 0, 0, 0.12), 0px 12px 24px 0px rgba(16, 24, 40, 0.12), 0px 1px 32px 0px rgba(16, 24, 40, 0.12); - --color-shadow-3xl: 0px 12px 24px 0px rgba(0, 0, 0, 0.12), 0px 16px 32px 0px rgba(0, 0, 0, 0.12), + --color-shadow-3xl: + 0px 12px 24px 0px rgba(0, 0, 0, 0.12), 0px 16px 32px 0px rgba(0, 0, 0, 0.12), 0px 1px 48px 0px rgba(16, 24, 40, 0.12); --color-shadow-4xl: 0px 8px 40px 0px rgba(0, 0, 61, 0.05), 0px 12px 32px -16px rgba(0, 0, 0, 0.05); diff --git a/admin/tsconfig.json b/admin/tsconfig.json index f9bb7cf10..e32f01e6a 100644 --- a/admin/tsconfig.json +++ b/admin/tsconfig.json @@ -6,7 +6,8 @@ "paths": { "@/*": ["core/*"], "@/public/*": ["public/*"], - "@/plane-admin/*": ["ce/*"] + "@/plane-admin/*": ["ce/*"], + "@/styles/*": ["styles/*"] } }, "include": ["next-env.d.ts", "next.config.js", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], diff --git a/packages/propel/package.json b/packages/propel/package.json index 382739d03..3522c2f64 100644 --- a/packages/propel/package.json +++ b/packages/propel/package.json @@ -9,7 +9,8 @@ }, "exports": { "./ui/*": "./src/ui/*.tsx", - "./charts/*": "./src/charts/*/index.ts" + "./charts/*": "./src/charts/*/index.ts", + "./styles/fonts": "./src/styles/fonts/index.css" }, "dependencies": { "@radix-ui/react-slot": "^1.1.1", diff --git a/packages/propel/postcss.config.js b/packages/propel/postcss.config.js index 12a703d90..9b1e55fc4 100644 --- a/packages/propel/postcss.config.js +++ b/packages/propel/postcss.config.js @@ -1,6 +1,2 @@ -module.exports = { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -}; +// eslint-disable-next-line @typescript-eslint/no-require-imports +module.exports = require("@plane/tailwind-config/postcss.config.js"); diff --git a/packages/propel/src/globals.css b/packages/propel/src/globals.css deleted file mode 100644 index ee2896808..000000000 --- a/packages/propel/src/globals.css +++ /dev/null @@ -1,12 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - -@layer base { - * { - @apply border-border; - } - body { - @apply font-sans antialiased bg-background text-foreground; - } -} diff --git a/web/styles/fonts/Inter/LICENSE b/packages/propel/src/styles/fonts/Inter/LICENSE similarity index 100% rename from web/styles/fonts/Inter/LICENSE rename to packages/propel/src/styles/fonts/Inter/LICENSE diff --git a/web/styles/fonts/Inter/inter-v13-latin-200.woff2 b/packages/propel/src/styles/fonts/Inter/inter-v13-latin-200.woff2 similarity index 100% rename from web/styles/fonts/Inter/inter-v13-latin-200.woff2 rename to packages/propel/src/styles/fonts/Inter/inter-v13-latin-200.woff2 diff --git a/web/styles/fonts/Inter/inter-v13-latin-300.woff2 b/packages/propel/src/styles/fonts/Inter/inter-v13-latin-300.woff2 similarity index 100% rename from web/styles/fonts/Inter/inter-v13-latin-300.woff2 rename to packages/propel/src/styles/fonts/Inter/inter-v13-latin-300.woff2 diff --git a/web/styles/fonts/Inter/inter-v13-latin-500.woff2 b/packages/propel/src/styles/fonts/Inter/inter-v13-latin-500.woff2 similarity index 100% rename from web/styles/fonts/Inter/inter-v13-latin-500.woff2 rename to packages/propel/src/styles/fonts/Inter/inter-v13-latin-500.woff2 diff --git a/web/styles/fonts/Inter/inter-v13-latin-600.woff2 b/packages/propel/src/styles/fonts/Inter/inter-v13-latin-600.woff2 similarity index 100% rename from web/styles/fonts/Inter/inter-v13-latin-600.woff2 rename to packages/propel/src/styles/fonts/Inter/inter-v13-latin-600.woff2 diff --git a/web/styles/fonts/Inter/inter-v13-latin-700.woff2 b/packages/propel/src/styles/fonts/Inter/inter-v13-latin-700.woff2 similarity index 100% rename from web/styles/fonts/Inter/inter-v13-latin-700.woff2 rename to packages/propel/src/styles/fonts/Inter/inter-v13-latin-700.woff2 diff --git a/web/styles/fonts/Inter/inter-v13-latin-800.woff2 b/packages/propel/src/styles/fonts/Inter/inter-v13-latin-800.woff2 similarity index 100% rename from web/styles/fonts/Inter/inter-v13-latin-800.woff2 rename to packages/propel/src/styles/fonts/Inter/inter-v13-latin-800.woff2 diff --git a/web/styles/fonts/Inter/inter-v13-latin-regular.woff2 b/packages/propel/src/styles/fonts/Inter/inter-v13-latin-regular.woff2 similarity index 100% rename from web/styles/fonts/Inter/inter-v13-latin-regular.woff2 rename to packages/propel/src/styles/fonts/Inter/inter-v13-latin-regular.woff2 diff --git a/web/styles/fonts/Material-Symbols-Rounded/LICENSE b/packages/propel/src/styles/fonts/Material-Symbols-Rounded/LICENSE similarity index 100% rename from web/styles/fonts/Material-Symbols-Rounded/LICENSE rename to packages/propel/src/styles/fonts/Material-Symbols-Rounded/LICENSE diff --git a/web/styles/fonts/Material-Symbols-Rounded/material-symbols-rounded-v168-latin-regular.woff2 b/packages/propel/src/styles/fonts/Material-Symbols-Rounded/material-symbols-rounded-v168-latin-regular.woff2 similarity index 100% rename from web/styles/fonts/Material-Symbols-Rounded/material-symbols-rounded-v168-latin-regular.woff2 rename to packages/propel/src/styles/fonts/Material-Symbols-Rounded/material-symbols-rounded-v168-latin-regular.woff2 diff --git a/web/styles/fonts/main.css b/packages/propel/src/styles/fonts/index.css similarity index 63% rename from web/styles/fonts/main.css rename to packages/propel/src/styles/fonts/index.css index 7263a01a9..7d6779d22 100644 --- a/web/styles/fonts/main.css +++ b/packages/propel/src/styles/fonts/index.css @@ -4,7 +4,7 @@ font-family: Inter; font-style: normal; font-weight: 200; - src: url("fonts/Inter/inter-v13-latin-200.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ + src: url("./Inter/inter-v13-latin-200.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ } /* inter-300 - latin */ @@ -13,7 +13,7 @@ font-family: Inter; font-style: normal; font-weight: 300; - src: url("fonts/Inter/inter-v13-latin-300.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ + src: url("./Inter/inter-v13-latin-300.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ } /* inter-regular - latin */ @@ -22,7 +22,7 @@ font-family: Inter; font-style: normal; font-weight: 405; - src: url("fonts/Inter/inter-v13-latin-regular.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ + src: url("./Inter/inter-v13-latin-regular.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ } /* inter-500 - latin */ @@ -31,7 +31,16 @@ font-family: Inter; font-style: normal; font-weight: 500; - src: url("fonts/Inter/inter-v13-latin-500.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ + src: url("./Inter/inter-v13-latin-500.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} + +/* inter-600 - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: Inter; + font-style: normal; + font-weight: 600; + src: url("./Inter/inter-v13-latin-600.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ } /* inter-700 - latin */ @@ -40,7 +49,7 @@ font-family: Inter; font-style: normal; font-weight: 700; - src: url("fonts/Inter/inter-v13-latin-700.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ + src: url("./Inter/inter-v13-latin-700.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ } /* inter-800 - latin */ @@ -49,7 +58,7 @@ font-family: Inter; font-style: normal; font-weight: 800; - src: url("fonts/Inter/inter-v13-latin-800.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ + src: url("./Inter/inter-v13-latin-800.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ } /* material-symbols-rounded-regular - latin */ @@ -58,7 +67,7 @@ font-family: "Material Symbols Rounded"; font-style: normal; font-weight: 400; - src: url("fonts/Material-Symbols-Rounded/material-symbols-rounded-v168-latin-regular.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ + src: url("./Material-Symbols-Rounded/material-symbols-rounded-v168-latin-regular.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ } .material-symbols-rounded { diff --git a/space/package.json b/space/package.json index 4dd54ca6e..4cccd65d1 100644 --- a/space/package.json +++ b/space/package.json @@ -22,6 +22,7 @@ "@plane/constants": "*", "@plane/editor": "*", "@plane/i18n": "*", + "@plane/propel": "*", "@plane/services": "*", "@plane/types": "*", "@plane/ui": "*", diff --git a/space/styles/globals.css b/space/styles/globals.css index 8976e83c2..5d27de674 100644 --- a/space/styles/globals.css +++ b/space/styles/globals.css @@ -1,5 +1,4 @@ -@import url("https://fonts.googleapis.com/css2?family=Inter:wght@200;300;400;500;600;700;800&display=swap"); -@import url("https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@48,400,0,0&display=swap"); +@import "@plane/propel/styles/fonts"; @tailwind base; @tailwind components; diff --git a/web/postcss.config.js b/web/postcss.config.js index 6887c8262..9b1e55fc4 100644 --- a/web/postcss.config.js +++ b/web/postcss.config.js @@ -1,8 +1,2 @@ -module.exports = { - plugins: { - "postcss-import": {}, - "tailwindcss/nesting": {}, - tailwindcss: {}, - autoprefixer: {}, - }, -}; +// eslint-disable-next-line @typescript-eslint/no-require-imports +module.exports = require("@plane/tailwind-config/postcss.config.js"); diff --git a/web/styles/globals.css b/web/styles/globals.css index 55c8c869e..ff71ba5ac 100644 --- a/web/styles/globals.css +++ b/web/styles/globals.css @@ -1,4 +1,4 @@ -@import url("fonts/main.css"); +@import "@plane/propel/styles/fonts"; @tailwind base; @tailwind components; From 50082f08436884435d90faf6898def0407af52c0 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Fri, 9 May 2025 16:53:51 +0530 Subject: [PATCH 034/201] [WEB-4002] fix: sidebar tab highlight (#7011) * fix: work item tab highlight * chore: code refactor * chore: code refactor * chore: code refactor --- .../workspace/sidebar/project-navigation.tsx | 42 +++++++++++++++---- web/core/store/issue/issue.store.ts | 7 ++++ 2 files changed, 42 insertions(+), 7 deletions(-) diff --git a/web/core/components/workspace/sidebar/project-navigation.tsx b/web/core/components/workspace/sidebar/project-navigation.tsx index 8a94fbcf6..f4add66e6 100644 --- a/web/core/components/workspace/sidebar/project-navigation.tsx +++ b/web/core/components/workspace/sidebar/project-navigation.tsx @@ -3,7 +3,7 @@ import React, { FC, useCallback, useMemo } from "react"; import { observer } from "mobx-react"; import Link from "next/link"; -import { usePathname } from "next/navigation"; +import { useParams, usePathname } from "next/navigation"; import { FileText, Layers } from "lucide-react"; import { EUserPermissionsLevel, EUserPermissions, EUserProjectRoles } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; @@ -12,7 +12,7 @@ import { Tooltip, DiceIcon, ContrastIcon, LayersIcon, Intake } from "@plane/ui"; // components import { SidebarNavItem } from "@/components/sidebar"; // hooks -import { useAppTheme, useProject, useUserPermissions } from "@/hooks/store"; +import { useAppTheme, useIssueDetail, useProject, useUserPermissions } from "@/hooks/store"; import { usePlatformOS } from "@/hooks/use-platform-os"; // plane-web constants @@ -24,6 +24,7 @@ export type TNavigationItem = { shouldRender: boolean; sortOrder: number; i18n_key: string; + key: string; }; type TProjectItemsProps = { @@ -35,15 +36,23 @@ type TProjectItemsProps = { export const ProjectNavigation: FC = observer((props) => { const { workspaceSlug, projectId, additionalNavigationItems, isSidebarCollapsed } = props; + const { workItem: workItemIdentifierFromRoute } = useParams(); // store hooks const { t } = useTranslation(); const { toggleSidebar } = useAppTheme(); const { getPartialProjectById } = useProject(); const { isMobile } = usePlatformOS(); const { allowPermissions } = useUserPermissions(); + const { + issue: { getIssueIdByIdentifier, getIssueById }, + } = useIssueDetail(); // pathname const pathname = usePathname(); // derived values + const workItemId = workItemIdentifierFromRoute + ? getIssueIdByIdentifier(workItemIdentifierFromRoute?.toString()) + : undefined; + const workItem = workItemId ? getIssueById(workItemId) : undefined; const project = getPartialProjectById(projectId); // handlers const handleProjectClick = () => { @@ -58,6 +67,7 @@ export const ProjectNavigation: FC = observer((props) => { (workspaceSlug: string, projectId: string): TNavigationItem[] => [ { i18n_key: "sidebar.work_items", + key: "work_items", name: "Work items", href: `/${workspaceSlug}/projects/${projectId}/issues`, icon: LayersIcon, @@ -67,6 +77,7 @@ export const ProjectNavigation: FC = observer((props) => { }, { i18n_key: "sidebar.cycles", + key: "cycles", name: "Cycles", href: `/${workspaceSlug}/projects/${projectId}/cycles`, icon: ContrastIcon, @@ -76,6 +87,7 @@ export const ProjectNavigation: FC = observer((props) => { }, { i18n_key: "sidebar.modules", + key: "modules", name: "Modules", href: `/${workspaceSlug}/projects/${projectId}/modules`, icon: DiceIcon, @@ -85,6 +97,7 @@ export const ProjectNavigation: FC = observer((props) => { }, { i18n_key: "sidebar.views", + key: "views", name: "Views", href: `/${workspaceSlug}/projects/${projectId}/views`, icon: Layers, @@ -94,6 +107,7 @@ export const ProjectNavigation: FC = observer((props) => { }, { i18n_key: "sidebar.pages", + key: "pages", name: "Pages", href: `/${workspaceSlug}/projects/${projectId}/pages`, icon: FileText, @@ -103,6 +117,7 @@ export const ProjectNavigation: FC = observer((props) => { }, { i18n_key: "sidebar.intake", + key: "intake", name: "Intake", href: `/${workspaceSlug}/projects/${projectId}/intake`, icon: Intake, @@ -134,6 +149,23 @@ export const ProjectNavigation: FC = observer((props) => { return sortedNavigationItems; }, [workspaceSlug, projectId, baseNavigation, additionalNavigationItems]); + const isActive = useCallback( + (item: TNavigationItem) => { + // work item condition + const workItemCondition = workItemId && workItem && !workItem?.is_epic && workItem?.project_id === projectId; + // epic condition + const epicCondition = workItemId && workItem && workItem?.is_epic && workItem?.project_id === projectId; + // is active + const isWorkItemActive = item.key === "work_items" && workItemCondition; + const isEpicActive = item.key === "epics" && epicCondition; + // pathname condition + const isPathnameActive = pathname.includes(item.href); + // return + return isWorkItemActive || isEpicActive || isPathnameActive; + }, + [pathname, workItem, workItemId, projectId] + ); + return ( <> {navigationItemsMemo.map((item) => { @@ -154,11 +186,7 @@ export const ProjectNavigation: FC = observer((props) => {
{ issues.forEach((issue) => { + // add issue identifier to the issuesIdentifierMap + const projectIdentifier = rootStore.projectRoot.project.getProjectIdentifierById(issue?.project_id); + const workItemSequenceId = issue?.sequence_id; + const issueIdentifier = `${projectIdentifier}-${workItemSequenceId}`; + set(this.issuesIdentifierMap, issueIdentifier, issue.id); + if (!this.issuesMap[issue.id]) set(this.issuesMap, issue.id, issue); else update(this.issuesMap, issue.id, (prevIssue) => ({ ...prevIssue, ...issue })); }); From a263bfc01f05f52dd0ecc8c6a052533b9f1b1d77 Mon Sep 17 00:00:00 2001 From: Henit Chobisa Date: Fri, 9 May 2025 17:23:49 +0530 Subject: [PATCH 035/201] chore: added external id and source to page model (#7040) * chore: added external id and source to page model * chore: added migration * fix: added blank field --- ...5_page_external_id_page_external_source.py | 23 +++++++++++++++++++ apiserver/plane/db/models/page.py | 3 +++ 2 files changed, 26 insertions(+) create mode 100644 apiserver/plane/db/migrations/0095_page_external_id_page_external_source.py diff --git a/apiserver/plane/db/migrations/0095_page_external_id_page_external_source.py b/apiserver/plane/db/migrations/0095_page_external_id_page_external_source.py new file mode 100644 index 000000000..eed8acf87 --- /dev/null +++ b/apiserver/plane/db/migrations/0095_page_external_id_page_external_source.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.14 on 2025-05-09 11:31 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('db', '0094_auto_20250425_0902'), + ] + + operations = [ + migrations.AddField( + model_name='page', + name='external_id', + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AddField( + model_name='page', + name='external_source', + field=models.CharField(blank=True, max_length=255, null=True), + ), + ] diff --git a/apiserver/plane/db/models/page.py b/apiserver/plane/db/models/page.py index 5be9c6164..30a641ef8 100644 --- a/apiserver/plane/db/models/page.py +++ b/apiserver/plane/db/models/page.py @@ -58,6 +58,9 @@ class Page(BaseModel): moved_to_page = models.UUIDField(null=True, blank=True) moved_to_project = models.UUIDField(null=True, blank=True) + external_id = models.CharField(max_length=255, null=True, blank=True) + external_source = models.CharField(max_length=255, null=True, blank=True) + class Meta: verbose_name = "Page" verbose_name_plural = "Pages" From 64aae0a2ac0a4ee45e8448cff813c2d6965b1485 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Fri, 9 May 2025 18:49:43 +0530 Subject: [PATCH 036/201] [WEB-4051] fix: comment editor list items font size #7034 --- web/core/components/comments/comment-card.tsx | 4 +--- web/core/components/comments/comment-create.tsx | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/web/core/components/comments/comment-card.tsx b/web/core/components/comments/comment-card.tsx index dd9e61e03..698e1ddf3 100644 --- a/web/core/components/comments/comment-card.tsx +++ b/web/core/components/comments/comment-card.tsx @@ -161,9 +161,8 @@ export const CommentCard: FC = observer((props) => { return asset_id; }} projectId={projectId?.toString() ?? ""} - editorClassName="[&>*]:!py-0 [&>*]:!text-sm" parentClassName="p-2" - /> + />
{!isEmpty && ( @@ -208,7 +207,6 @@ export const CommentCard: FC = observer((props) => { initialValue={comment.comment_html ?? ""} workspaceId={workspaceId} workspaceSlug={workspaceSlug} - editorClassName="[&>*]:!py-0 [&>*]:!text-sm" containerClassName="!py-1" projectId={(projectId as string) ?? ""} /> diff --git a/web/core/components/comments/comment-create.tsx b/web/core/components/comments/comment-create.tsx index 1321a385f..dc74a9a12 100644 --- a/web/core/components/comments/comment-create.tsx +++ b/web/core/components/comments/comment-create.tsx @@ -125,7 +125,7 @@ export const CommentCreate: FC = observer((props) => { }} ref={editorRef} initialValue={value ?? "

"} - containerClassName="min-h-min [&_p]:!p-0 [&_p]:!text-sm" + containerClassName="min-h-min" onChange={(comment_json, comment_html) => onChange(comment_html)} accessSpecifier={accessValue ?? EIssueCommentAccessSpecifier.INTERNAL} handleAccessChange={onAccessChange} From b5634f5fa1a659de68dc822da10f8f3a206bd905 Mon Sep 17 00:00:00 2001 From: Surya Prashanth Date: Fri, 9 May 2025 21:05:05 +0530 Subject: [PATCH 037/201] chore: add disable_auto_set_user flag on base model save method (#7041) - when disable_auto_set_user flag is set, user fields like created_by are derived from payload instead of crum --- apiserver/plane/db/models/base.py | 36 ++++++++++++++++++------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/apiserver/plane/db/models/base.py b/apiserver/plane/db/models/base.py index d0531e881..558c25a40 100644 --- a/apiserver/plane/db/models/base.py +++ b/apiserver/plane/db/models/base.py @@ -18,22 +18,28 @@ class BaseModel(AuditModel): class Meta: abstract = True - def save(self, *args, **kwargs): - user = get_current_user() + def save(self, *args, created_by_id=None, disable_auto_set_user=False, **kwargs): + if not disable_auto_set_user: + # Check if created_by_id is provided + if created_by_id: + self.created_by_id = created_by_id + else: + user = get_current_user() - if user is None or user.is_anonymous: - self.created_by = None - self.updated_by = None - super(BaseModel, self).save(*args, **kwargs) - else: - # Check if the model is being created or updated - if self._state.adding: - # If created only set created_by value: set updated_by to None - self.created_by = user - self.updated_by = None - # If updated only set updated_by value don't touch created_by - self.updated_by = user - super(BaseModel, self).save(*args, **kwargs) + if user is None or user.is_anonymous: + self.created_by = None + self.updated_by = None + else: + # Check if the model is being created or updated + if self._state.adding: + # If creating, set created_by and leave updated_by as None + self.created_by = user + self.updated_by = None + else: + # If updating, set updated_by only + self.updated_by = user + + super(BaseModel, self).save(*args, **kwargs) def __str__(self): return str(self.id) From 02bccb44d6ab4c5c4bac9f1417d8add8de9c53c5 Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Fri, 9 May 2025 21:07:24 +0530 Subject: [PATCH 038/201] chore: adding robots txt file for not indexing the server --- apiserver/plane/urls.py | 2 -- apiserver/plane/web/urls.py | 4 ++-- apiserver/plane/web/views.py | 10 +++++++++- apiserver/templates/about.html | 9 --------- apiserver/templates/index.html | 5 ----- 5 files changed, 11 insertions(+), 19 deletions(-) delete mode 100644 apiserver/templates/about.html delete mode 100644 apiserver/templates/index.html diff --git a/apiserver/plane/urls.py b/apiserver/plane/urls.py index e3870a393..b692306a7 100644 --- a/apiserver/plane/urls.py +++ b/apiserver/plane/urls.py @@ -2,12 +2,10 @@ from django.conf import settings from django.urls import include, path, re_path -from django.views.generic import TemplateView handler404 = "plane.app.views.error_404.custom_404_view" urlpatterns = [ - path("", TemplateView.as_view(template_name="index.html")), path("api/", include("plane.app.urls")), path("api/public/", include("plane.space.urls")), path("api/instances/", include("plane.license.urls")), diff --git a/apiserver/plane/web/urls.py b/apiserver/plane/web/urls.py index 512d4a258..28734ad91 100644 --- a/apiserver/plane/web/urls.py +++ b/apiserver/plane/web/urls.py @@ -1,4 +1,4 @@ from django.urls import path -from django.views.generic import TemplateView +from plane.web.views import robots_txt, health_check -urlpatterns = [path("about/", TemplateView.as_view(template_name="about.html"))] +urlpatterns = [path("robots.txt", robots_txt), path("", health_check)] diff --git a/apiserver/plane/web/views.py b/apiserver/plane/web/views.py index 60f00ef0e..8acb70a77 100644 --- a/apiserver/plane/web/views.py +++ b/apiserver/plane/web/views.py @@ -1 +1,9 @@ -# Create your views here. +from django.http import HttpResponse, JsonResponse + + +def health_check(request): + return JsonResponse({"status": "OK"}) + + +def robots_txt(request): + return HttpResponse("User-agent: *\nDisallow: /", content_type="text/plain") diff --git a/apiserver/templates/about.html b/apiserver/templates/about.html deleted file mode 100644 index 91804cda4..000000000 --- a/apiserver/templates/about.html +++ /dev/null @@ -1,9 +0,0 @@ - -{% extends 'base.html' %} -{% load static %} - - -{% block content %} -

Hello from plane!

-

Made with Django

-{% endblock content %} \ No newline at end of file diff --git a/apiserver/templates/index.html b/apiserver/templates/index.html deleted file mode 100644 index 630ca66b6..000000000 --- a/apiserver/templates/index.html +++ /dev/null @@ -1,5 +0,0 @@ - {% extends 'base.html' %} {% load static %} {% block content %} -
-

Hello from plane!

-
-{% endblock content %} \ No newline at end of file From 13c46e0fdf660cc0439a4f9610baa2f0aba3a7a0 Mon Sep 17 00:00:00 2001 From: Sangeetha Date: Fri, 9 May 2025 21:09:13 +0530 Subject: [PATCH 039/201] [WEB-3987] chore: project export funtionality enhancement (#7002) * chore: comment details of work item * chore: attachment count and attachment name * chore: issue link and subscriber count * chore: list of assignees * chore: asset_url as attachment_links * chore: code refactor * fix: cannot export Excel * chore: remove print statements * fix: filtering in list * chore: optimize attachment_count and attachment_link query * chore: optimize fetching issue details for multiple select * chore: use Prefetch to avoid duplicates --- apiserver/plane/bgtasks/export_task.py | 257 ++++++++++++++++--------- 1 file changed, 166 insertions(+), 91 deletions(-) diff --git a/apiserver/plane/bgtasks/export_task.py b/apiserver/plane/bgtasks/export_task.py index 33e382f44..061167122 100644 --- a/apiserver/plane/bgtasks/export_task.py +++ b/apiserver/plane/bgtasks/export_task.py @@ -10,13 +10,17 @@ from botocore.client import Config # Third party imports from celery import shared_task + # Django imports from django.conf import settings from django.utils import timezone from openpyxl import Workbook +from django.db.models import F, Prefetch + +from collections import defaultdict # Module imports -from plane.db.models import ExporterHistory, Issue +from plane.db.models import ExporterHistory, Issue, FileAsset, Label, User from plane.utils.exception_logger import log_exception @@ -152,69 +156,68 @@ def upload_to_s3(zip_file, workspace_id, token_id, slug): def generate_table_row(issue): return [ - f"""{issue["project__identifier"]}-{issue["sequence_id"]}""", - issue["project__name"], + f"""{issue["project_identifier"]}-{issue["sequence_id"]}""", + issue["project_name"], issue["name"], - issue["description_stripped"], - issue["state__name"], + issue["description"], + issue["state_name"], dateConverter(issue["start_date"]), dateConverter(issue["target_date"]), issue["priority"], - ( - f"{issue['created_by__first_name']} {issue['created_by__last_name']}" - if issue["created_by__first_name"] and issue["created_by__last_name"] - else "" - ), - ( - f"{issue['assignees__first_name']} {issue['assignees__last_name']}" - if issue["assignees__first_name"] and issue["assignees__last_name"] - else "" - ), - issue["labels__name"] if issue["labels__name"] else "", - issue["issue_cycle__cycle__name"], - dateConverter(issue["issue_cycle__cycle__start_date"]), - dateConverter(issue["issue_cycle__cycle__end_date"]), - issue["issue_module__module__name"], - dateConverter(issue["issue_module__module__start_date"]), - dateConverter(issue["issue_module__module__target_date"]), + issue["created_by"], + ", ".join(issue["labels"]) if issue["labels"] else "", + issue.get("cycle_name", ""), + issue.get("cycle_start_date", ""), + issue.get("cycle_end_date", ""), + ", ".join(issue.get("module_name", "")) if issue.get("module_name") else "", dateTimeConverter(issue["created_at"]), dateTimeConverter(issue["updated_at"]), dateTimeConverter(issue["completed_at"]), dateTimeConverter(issue["archived_at"]), + ", ".join( + [ + f"{comment['comment']} ({comment['created_at']} by {comment['created_by']})" + for comment in issue["comments"] + ] + ) + if issue["comments"] + else "", + issue["estimate"] if issue["estimate"] else "", + ", ".join(issue["link"]) if issue["link"] else "", + ", ".join(issue["assignees"]) if issue["assignees"] else "", + issue["subscribers_count"] if issue["subscribers_count"] else "", + issue["attachment_count"] if issue["attachment_count"] else "", + ", ".join(issue["attachment_links"]) if issue["attachment_links"] else "", ] def generate_json_row(issue): return { - "ID": f"""{issue["project__identifier"]}-{issue["sequence_id"]}""", - "Project": issue["project__name"], + "ID": f"""{issue["project_identifier"]}-{issue["sequence_id"]}""", + "Project": issue["project_name"], "Name": issue["name"], - "Description": issue["description_stripped"], - "State": issue["state__name"], + "Description": issue["description"], + "State": issue["state_name"], "Start Date": dateConverter(issue["start_date"]), "Target Date": dateConverter(issue["target_date"]), "Priority": issue["priority"], - "Created By": ( - f"{issue['created_by__first_name']} {issue['created_by__last_name']}" - if issue["created_by__first_name"] and issue["created_by__last_name"] - else "" - ), - "Assignee": ( - f"{issue['assignees__first_name']} {issue['assignees__last_name']}" - if issue["assignees__first_name"] and issue["assignees__last_name"] - else "" - ), - "Labels": issue["labels__name"] if issue["labels__name"] else "", - "Cycle Name": issue["issue_cycle__cycle__name"], - "Cycle Start Date": dateConverter(issue["issue_cycle__cycle__start_date"]), - "Cycle End Date": dateConverter(issue["issue_cycle__cycle__end_date"]), - "Module Name": issue["issue_module__module__name"], - "Module Start Date": dateConverter(issue["issue_module__module__start_date"]), - "Module Target Date": dateConverter(issue["issue_module__module__target_date"]), + "Created By": (f"{issue['created_by']}" if issue["created_by"] else ""), + "Assignee": issue["assignees"], + "Labels": issue["labels"], + "Cycle Name": issue["cycle_name"], + "Cycle Start Date": issue["cycle_start_date"], + "Cycle End Date": issue["cycle_end_date"], + "Module Name": issue["module_name"], "Created At": dateTimeConverter(issue["created_at"]), "Updated At": dateTimeConverter(issue["updated_at"]), "Completed At": dateTimeConverter(issue["completed_at"]), "Archived At": dateTimeConverter(issue["archived_at"]), + "Comments": issue["comments"], + "Estimate": issue["estimate"], + "Link": issue["link"], + "Subscribers Count": issue["subscribers_count"], + "Attachment Count": issue["attachment_count"], + "Attachment Links": issue["attachment_links"], } @@ -276,6 +279,7 @@ def generate_csv(header, project_id, issues, files): rows = [header] for issue in issues: row = generate_table_row(issue) + update_table_row(rows, row) csv_file = create_csv_file(rows) files.append((f"{project_id}.csv", csv_file)) @@ -294,6 +298,7 @@ def generate_xlsx(header, project_id, issues, files): rows = [header] for issue in issues: row = generate_table_row(issue) + update_table_row(rows, row) xlsx_file = create_xlsx_file(rows) files.append((f"{project_id}.xlsx", xlsx_file)) @@ -307,51 +312,112 @@ def issue_export_task(provider, workspace_id, project_ids, token_id, multiple, s exporter_instance.save(update_fields=["status"]) workspace_issues = ( - ( - Issue.objects.filter( - workspace__id=workspace_id, - project_id__in=project_ids, - project__project_projectmember__member=exporter_instance.initiated_by_id, - project__project_projectmember__is_active=True, - project__archived_at__isnull=True, - ) - .select_related("project", "workspace", "state", "parent", "created_by") - .prefetch_related( - "assignees", "labels", "issue_cycle__cycle", "issue_module__module" - ) - .values( - "id", - "project__identifier", - "project__name", - "project__id", - "sequence_id", - "name", - "description_stripped", - "priority", - "start_date", - "target_date", - "state__name", - "created_at", - "updated_at", - "completed_at", - "archived_at", - "issue_cycle__cycle__name", - "issue_cycle__cycle__start_date", - "issue_cycle__cycle__end_date", - "issue_module__module__name", - "issue_module__module__start_date", - "issue_module__module__target_date", - "created_by__first_name", - "created_by__last_name", - "assignees__first_name", - "assignees__last_name", - "labels__name", - ) + Issue.objects.filter( + workspace__id=workspace_id, + project_id__in=project_ids, + project__project_projectmember__member=exporter_instance.initiated_by_id, + project__project_projectmember__is_active=True, + project__archived_at__isnull=True, + ) + .select_related( + "project", + "workspace", + "state", + "parent", + "created_by", + "estimate_point", + ) + .prefetch_related( + "labels", + "issue_cycle__cycle", + "issue_module__module", + "issue_comments", + "assignees", + Prefetch( + "assignees", + queryset=User.objects.only("first_name", "last_name").distinct(), + to_attr="assignee_details", + ), + Prefetch( + "labels", + queryset=Label.objects.only("name").distinct(), + to_attr="label_details", + ), + "issue_subscribers", + "issue_link", ) - .order_by("project__identifier", "sequence_id") - .distinct() ) - # CSV header + + file_assets = FileAsset.objects.filter( + issue_id__in=workspace_issues.values_list("id", flat=True) + ).annotate(work_item_id=F("issue_id"), asset_id=F("id")) + + attachment_dict = defaultdict(list) + for asset in file_assets: + attachment_dict[asset.work_item_id].append(asset.asset_id) + + issues_data = [] + + for issue in workspace_issues: + attachments = attachment_dict.get(issue.id, []) + + issue_data = { + "id": issue.id, + "project_identifier": issue.project.identifier, + "project_name": issue.project.name, + "project_id": issue.project.id, + "sequence_id": issue.sequence_id, + "name": issue.name, + "description": issue.description_stripped, + "priority": issue.priority, + "start_date": issue.start_date, + "target_date": issue.target_date, + "state_name": issue.state.name if issue.state else None, + "created_at": issue.created_at, + "updated_at": issue.updated_at, + "completed_at": issue.completed_at, + "archived_at": issue.archived_at, + "module_name": [ + module.module.name for module in issue.issue_module.all() + ], + "created_by": f"{issue.created_by.first_name} {issue.created_by.last_name}", + "labels": [label.name for label in issue.label_details], + "comments": [ + { + "comment": comment.comment_stripped, + "created_at": dateConverter(comment.created_at), + "created_by": f"{comment.created_by.first_name} {comment.created_by.last_name}", + } + for comment in issue.issue_comments.all() + ], + "estimate": issue.estimate_point.estimate.name + if issue.estimate_point + else "", + "link": [link.url for link in issue.issue_link.all()], + "assignees": [ + f"{assignee.first_name} {assignee.last_name}" + for assignee in issue.assignee_details + ], + "subscribers_count": issue.issue_subscribers.count(), + "attachment_count": len(attachments), + "attachment_links": [ + f"/api/assets/v2/workspaces/{issue.workspace.slug}/projects/{issue.project_id}/issues/{issue.id}/attachments/{asset}/" + for asset in attachments + ], + } + + # Get prefetched cycles and modules + cycles = list(issue.issue_cycle.all()) + + # Update cycle data + for cycle in cycles: + issue_data["cycle_name"] = cycle.cycle.name + issue_data["cycle_start_date"] = dateConverter(cycle.cycle.start_date) + issue_data["cycle_end_date"] = dateConverter(cycle.cycle.end_date) + + issues_data.append(issue_data) + + # CSV header header = [ "ID", "Project", @@ -362,18 +428,22 @@ def issue_export_task(provider, workspace_id, project_ids, token_id, multiple, s "Target Date", "Priority", "Created By", - "Assignee", "Labels", "Cycle Name", "Cycle Start Date", "Cycle End Date", "Module Name", - "Module Start Date", - "Module Target Date", "Created At", "Updated At", "Completed At", "Archived At", + "Comments", + "Estimate", + "Link", + "Assignees", + "Subscribers Count", + "Attachment Count", + "Attachment Links", ] EXPORTER_MAPPER = { @@ -384,8 +454,13 @@ def issue_export_task(provider, workspace_id, project_ids, token_id, multiple, s files = [] if multiple: + project_dict = defaultdict(list) + for issue in issues_data: + project_dict[str(issue["project_id"])].append(issue) + for project_id in project_ids: - issues = workspace_issues.filter(project__id=project_id) + issues = project_dict.get(str(project_id), []) + exporter = EXPORTER_MAPPER.get(provider) if exporter is not None: exporter(header, project_id, issues, files) @@ -393,7 +468,7 @@ def issue_export_task(provider, workspace_id, project_ids, token_id, multiple, s else: exporter = EXPORTER_MAPPER.get(provider) if exporter is not None: - exporter(header, workspace_id, workspace_issues, files) + exporter(header, workspace_id, issues_data, files) zip_buffer = create_zip_file(files) upload_to_s3(zip_buffer, workspace_id, token_id, slug) From b435ceedfc0b028576d65f358d28592540ab66b9 Mon Sep 17 00:00:00 2001 From: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com> Date: Mon, 12 May 2025 13:15:17 +0530 Subject: [PATCH 040/201] [WEB-3782] chore: analytics endpoints (#6973) * chore: analytics endpoint * chore: created analytics chart * chore: validation errors * chore: added a new graph in advance analytics * chore: added csv exporter * chore: updated the filtering logic for analytics * chore: opitmised the analytics endpoint * chore: updated the base function for viewsets * chore: updated the export logic * chore: added type hints * chore: added type hints --- apiserver/plane/app/urls/analytic.py | 24 ++ apiserver/plane/app/views/__init__.py | 7 + apiserver/plane/app/views/analytic/advance.py | 397 ++++++++++++++++++ .../plane/bgtasks/analytic_plot_export.py | 29 ++ apiserver/plane/utils/build_chart.py | 205 +++++++++ apiserver/plane/utils/date_utils.py | 197 +++++++++ 6 files changed, 859 insertions(+) create mode 100644 apiserver/plane/app/views/analytic/advance.py create mode 100644 apiserver/plane/utils/build_chart.py create mode 100644 apiserver/plane/utils/date_utils.py diff --git a/apiserver/plane/app/urls/analytic.py b/apiserver/plane/app/urls/analytic.py index abe18f2ad..c6f024f75 100644 --- a/apiserver/plane/app/urls/analytic.py +++ b/apiserver/plane/app/urls/analytic.py @@ -6,8 +6,12 @@ from plane.app.views import ( AnalyticViewViewset, SavedAnalyticEndpoint, ExportAnalyticsEndpoint, + AdvanceAnalyticsEndpoint, + AdvanceAnalyticsStatsEndpoint, + AdvanceAnalyticsChartEndpoint, DefaultAnalyticsEndpoint, ProjectStatsEndpoint, + AdvanceAnalyticsExportEndpoint, ) @@ -49,4 +53,24 @@ urlpatterns = [ ProjectStatsEndpoint.as_view(), name="project-analytics", ), + path( + "workspaces//advance-analytics/", + AdvanceAnalyticsEndpoint.as_view(), + name="advance-analytics", + ), + path( + "workspaces//advance-analytics-stats/", + AdvanceAnalyticsStatsEndpoint.as_view(), + name="advance-analytics-stats", + ), + path( + "workspaces//advance-analytics-charts/", + AdvanceAnalyticsChartEndpoint.as_view(), + name="advance-analytics-chart", + ), + path( + "workspaces//advance-analytics-export/", + AdvanceAnalyticsExportEndpoint.as_view(), + name="advance-analytics-export", + ), ] diff --git a/apiserver/plane/app/views/__init__.py b/apiserver/plane/app/views/__init__.py index 7baba9bb0..a3c72f370 100644 --- a/apiserver/plane/app/views/__init__.py +++ b/apiserver/plane/app/views/__init__.py @@ -199,6 +199,13 @@ from .analytic.base import ( ProjectStatsEndpoint, ) +from .analytic.advance import ( + AdvanceAnalyticsEndpoint, + AdvanceAnalyticsStatsEndpoint, + AdvanceAnalyticsChartEndpoint, + AdvanceAnalyticsExportEndpoint, +) + from .notification.base import ( NotificationViewSet, UnreadNotificationEndpoint, diff --git a/apiserver/plane/app/views/analytic/advance.py b/apiserver/plane/app/views/analytic/advance.py new file mode 100644 index 000000000..8b5832772 --- /dev/null +++ b/apiserver/plane/app/views/analytic/advance.py @@ -0,0 +1,397 @@ +from rest_framework.response import Response +from rest_framework import status +from typing import Dict, List, Any +from datetime import timedelta +from django.db.models import QuerySet, Q, Count +from django.http import HttpRequest + +from plane.app.views.base import BaseAPIView +from plane.app.permissions import ROLE, allow_permission +from plane.db.models import ( + WorkspaceMember, + Project, + Issue, + Cycle, + Module, + IssueView, + ProjectPage, +) + +from plane.utils.build_chart import build_analytics_chart +from plane.bgtasks.analytic_plot_export import export_analytics_to_csv_email +from plane.utils.date_utils import get_analytics_filters + + +class AdvanceAnalyticsBaseView(BaseAPIView): + def initialize_workspace(self, slug: str, type: str) -> None: + self._workspace_slug = slug + self.filters = get_analytics_filters( + slug=slug, + type=type, + user=self.request.user, + date_filter=self.request.GET.get("date_filter", None), + project_ids=self.request.GET.get("project_ids", None), + ) + + +class AdvanceAnalyticsEndpoint(AdvanceAnalyticsBaseView): + def get_filtered_counts(self, queryset: QuerySet) -> Dict[str, int]: + def get_filtered_count() -> int: + if self.filters["analytics_date_range"]: + return queryset.filter( + created_at__gte=self.filters["analytics_date_range"]["current"][ + "gte" + ], + created_at__lte=self.filters["analytics_date_range"]["current"][ + "lte" + ], + ).count() + return queryset.count() + + def get_previous_count() -> int: + if self.filters["analytics_date_range"] and self.filters[ + "analytics_date_range" + ].get("previous"): + return queryset.filter( + created_at__gte=self.filters["analytics_date_range"]["previous"][ + "gte" + ], + created_at__lte=self.filters["analytics_date_range"]["previous"][ + "lte" + ], + ).count() + return 0 + + return { + "count": get_filtered_count(), + "filter_count": get_previous_count(), + } + + def get_overview_data(self) -> Dict[str, Dict[str, int]]: + return { + "total_users": self.get_filtered_counts( + WorkspaceMember.objects.filter( + workspace__slug=self._workspace_slug, is_active=True + ) + ), + "total_admins": self.get_filtered_counts( + WorkspaceMember.objects.filter( + workspace__slug=self._workspace_slug, + role=ROLE.ADMIN.value, + is_active=True, + ) + ), + "total_members": self.get_filtered_counts( + WorkspaceMember.objects.filter( + workspace__slug=self._workspace_slug, + role=ROLE.MEMBER.value, + is_active=True, + ) + ), + "total_guests": self.get_filtered_counts( + WorkspaceMember.objects.filter( + workspace__slug=self._workspace_slug, + role=ROLE.GUEST.value, + is_active=True, + ) + ), + "total_projects": self.get_filtered_counts( + Project.objects.filter(**self.filters["project_filters"]) + ), + "total_work_items": self.get_filtered_counts( + Issue.issue_objects.filter(**self.filters["base_filters"]) + ), + "total_cycles": self.get_filtered_counts( + Cycle.objects.filter(**self.filters["base_filters"]) + ), + "total_intake": self.get_filtered_counts( + Issue.objects.filter(**self.filters["base_filters"]).filter( + issue_intake__isnull=False + ) + ), + } + + def get_work_items_stats(self) -> Dict[str, Dict[str, int]]: + base_queryset = Issue.objects.filter(**self.filters["base_filters"]) + + return { + "total_work_items": self.get_filtered_counts(base_queryset), + "started_work_items": self.get_filtered_counts( + base_queryset.filter(state__group="started") + ), + "backlog_work_items": self.get_filtered_counts( + base_queryset.filter(state__group="backlog") + ), + "un_started_work_items": self.get_filtered_counts( + base_queryset.filter(state__group="unstarted") + ), + "completed_work_items": self.get_filtered_counts( + base_queryset.filter(state__group="completed") + ), + } + + @allow_permission([ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE") + def get(self, request: HttpRequest, slug: str) -> Response: + self.initialize_workspace(slug, type="analytics") + tab = request.GET.get("tab", "overview") + + if tab == "overview": + return Response( + self.get_overview_data(), + status=status.HTTP_200_OK, + ) + + elif tab == "work-items": + return Response( + self.get_work_items_stats(), + status=status.HTTP_200_OK, + ) + + return Response({"message": "Invalid tab"}, status=status.HTTP_400_BAD_REQUEST) + + +class AdvanceAnalyticsStatsEndpoint(AdvanceAnalyticsBaseView): + def get_project_issues_stats(self) -> QuerySet: + # Get the base queryset with workspace and project filters + base_queryset = Issue.issue_objects.filter(**self.filters["base_filters"]) + + # Apply date range filter if available + if self.filters["chart_period_range"]: + start_date, end_date = self.filters["chart_period_range"] + base_queryset = base_queryset.filter( + created_at__date__gte=start_date, created_at__date__lte=end_date + ) + + return ( + base_queryset.values("project_id", "project__name") + .annotate( + cancelled_work_items=Count("id", filter=Q(state__group="cancelled")), + completed_work_items=Count("id", filter=Q(state__group="completed")), + backlog_work_items=Count("id", filter=Q(state__group="backlog")), + un_started_work_items=Count("id", filter=Q(state__group="unstarted")), + started_work_items=Count("id", filter=Q(state__group="started")), + ) + .order_by("project_id") + ) + + @allow_permission([ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE") + def get(self, request: HttpRequest, slug: str) -> Response: + self.initialize_workspace(slug, type="chart") + type = request.GET.get("type", "work-items") + + if type == "work-items": + return Response( + self.get_project_issues_stats(), + status=status.HTTP_200_OK, + ) + + return Response({"message": "Invalid type"}, status=status.HTTP_400_BAD_REQUEST) + + +class AdvanceAnalyticsChartEndpoint(AdvanceAnalyticsBaseView): + def project_chart(self) -> List[Dict[str, Any]]: + # Get the base queryset with workspace and project filters + base_queryset = Issue.issue_objects.filter(**self.filters["base_filters"]) + date_filter = {} + # Apply date range filter if available + if self.filters["chart_period_range"]: + start_date, end_date = self.filters["chart_period_range"] + date_filter = { + "created_at__date__gte": start_date, + "created_at__date__lte": end_date, + } + + total_work_items = base_queryset.filter(**date_filter).count() + total_cycles = Cycle.objects.filter( + **self.filters["base_filters"], **date_filter + ).count() + total_modules = Module.objects.filter( + **self.filters["base_filters"], **date_filter + ).count() + total_intake = Issue.objects.filter( + issue_intake__isnull=False, **self.filters["base_filters"], **date_filter + ).count() + total_members = WorkspaceMember.objects.filter( + workspace__slug=self._workspace_slug, is_active=True, **date_filter + ).count() + total_pages = ProjectPage.objects.filter( + **self.filters["base_filters"], **date_filter + ).count() + total_views = IssueView.objects.filter( + **self.filters["base_filters"], **date_filter + ).count() + + data = { + "work_items": total_work_items, + "cycles": total_cycles, + "modules": total_modules, + "intake": total_intake, + "members": total_members, + "pages": total_pages, + "views": total_views, + } + + return [ + { + "key": key, + "name": key.replace("_", " ").title(), + "count": value or 0, + } + for key, value in data.items() + ] + + def work_item_completion_chart(self) -> Dict[str, Any]: + # Get the base queryset + queryset = ( + Issue.issue_objects.filter(**self.filters["base_filters"]) + .select_related("workspace", "state", "parent") + .prefetch_related( + "assignees", "labels", "issue_module__module", "issue_cycle__cycle" + ) + ) + # Apply date range filter if available + if self.filters["chart_period_range"]: + start_date, end_date = self.filters["chart_period_range"] + queryset = queryset.filter( + created_at__date__gte=start_date, created_at__date__lte=end_date + ) + + # Get daily stats with optimized query + daily_stats = ( + queryset.values("created_at__date") + .annotate( + created_count=Count("id"), + completed_count=Count("id", filter=Q(completed_at__isnull=False)), + ) + .order_by("created_at__date") + ) + + # Create a dictionary of existing stats with summed counts + stats_dict = { + stat["created_at__date"].strftime("%Y-%m-%d"): { + "created_count": stat["created_count"], + "completed_count": stat["completed_count"], + } + for stat in daily_stats + } + + # Generate data for all days in the range + data = [] + current_date = start_date + while current_date <= end_date: + date_str = current_date.strftime("%Y-%m-%d") + stats = stats_dict.get(date_str, {"created_count": 0, "completed_count": 0}) + data.append( + { + "key": date_str, + "name": date_str, + "count": stats["created_count"] + stats["completed_count"], + "completed_issues": stats["completed_count"], + "created_issues": stats["created_count"], + } + ) + current_date += timedelta(days=1) + + schema = { + "completed_issues": "completed_issues", + "created_issues": "created_issues", + } + + return {"data": data, "schema": schema} + + @allow_permission([ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE") + def get(self, request: HttpRequest, slug: str) -> Response: + self.initialize_workspace(slug, type="chart") + type = request.GET.get("type", "projects") + group_by = request.GET.get("group_by", None) + x_axis = request.GET.get("x_axis", "PRIORITY") + + if type == "projects": + return Response(self.project_chart(), status=status.HTTP_200_OK) + + elif type == "custom-work-items": + # Get the base queryset + queryset = ( + Issue.issue_objects.filter(**self.filters["base_filters"]) + .select_related("workspace", "state", "parent") + .prefetch_related( + "assignees", "labels", "issue_module__module", "issue_cycle__cycle" + ) + ) + + # Apply date range filter if available + if self.filters["chart_period_range"]: + start_date, end_date = self.filters["chart_period_range"] + queryset = queryset.filter( + created_at__date__gte=start_date, created_at__date__lte=end_date + ) + + return Response( + build_analytics_chart(queryset, x_axis, group_by), + status=status.HTTP_200_OK, + ) + + elif type == "work-items": + return Response( + self.work_item_completion_chart(), + status=status.HTTP_200_OK, + ) + + return Response({"message": "Invalid type"}, status=status.HTTP_400_BAD_REQUEST) + + +class AdvanceAnalyticsExportEndpoint(AdvanceAnalyticsBaseView): + @allow_permission([ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE") + def post(self, request: HttpRequest, slug: str) -> Response: + self.initialize_workspace(slug, type="chart") + queryset = Issue.issue_objects.filter(**self.filters["base_filters"]) + + # Apply date range filter if available + if self.filters["chart_period_range"]: + start_date, end_date = self.filters["chart_period_range"] + queryset = queryset.filter( + created_at__date__gte=start_date, created_at__date__lte=end_date + ) + + queryset = ( + queryset.values("project_id", "project__name") + .annotate( + cancelled_work_items=Count("id", filter=Q(state__group="cancelled")), + completed_work_items=Count("id", filter=Q(state__group="completed")), + backlog_work_items=Count("id", filter=Q(state__group="backlog")), + un_started_work_items=Count("id", filter=Q(state__group="unstarted")), + started_work_items=Count("id", filter=Q(state__group="started")), + ) + .order_by("project_id") + ) + + # Convert QuerySet to list of dictionaries for serialization + serialized_data = list(queryset) + + headers = [ + "Projects", + "Completed Issues", + "Backlog Issues", + "Unstarted Issues", + "Started Issues", + ] + + keys = [ + "project__name", + "completed_work_items", + "backlog_work_items", + "un_started_work_items", + "started_work_items", + ] + + email = request.user.email + + # Send serialized data to background task + export_analytics_to_csv_email.delay(serialized_data, headers, keys, email, slug) + + return Response( + { + "message": f"Once the export is ready it will be emailed to you at {str(email)}" + }, + status=status.HTTP_200_OK, + ) diff --git a/apiserver/plane/bgtasks/analytic_plot_export.py b/apiserver/plane/bgtasks/analytic_plot_export.py index 2ba10fe2d..0f07ccc85 100644 --- a/apiserver/plane/bgtasks/analytic_plot_export.py +++ b/apiserver/plane/bgtasks/analytic_plot_export.py @@ -464,3 +464,32 @@ def analytic_export_task(email, data, slug): except Exception as e: log_exception(e) return + + +@shared_task +def export_analytics_to_csv_email(data, headers, keys, email, slug): + try: + """ + Prepares a CSV from data and sends it as an email attachment. + + Parameters: + - data: List of dictionaries (e.g. from .values()) + - headers: List of CSV column headers + - keys: Keys to extract from each data item (dict) + - email: Email address to send to + - slug: Used for the filename + """ + # Prepare rows: header + data rows + rows = [headers] + for item in data: + row = [item.get(key, "") for key in keys] + rows.append(row) + + # Generate CSV buffer + csv_buffer = generate_csv_from_rows(rows) + + # Send email with CSV attachment + send_export_email(email=email, slug=slug, csv_buffer=csv_buffer, rows=rows) + except Exception as e: + log_exception(e) + return diff --git a/apiserver/plane/utils/build_chart.py b/apiserver/plane/utils/build_chart.py new file mode 100644 index 000000000..4ae3397f8 --- /dev/null +++ b/apiserver/plane/utils/build_chart.py @@ -0,0 +1,205 @@ +from typing import Dict, Any, Tuple, Optional, List, Union + + +# Django imports +from django.db.models import ( + Count, + F, + QuerySet, + Aggregate, +) + +from plane.db.models import Issue +from rest_framework.exceptions import ValidationError + + +x_axis_mapper = { + "STATES": "STATES", + "STATE_GROUPS": "STATE_GROUPS", + "LABELS": "LABELS", + "ASSIGNEES": "ASSIGNEES", + "ESTIMATE_POINTS": "ESTIMATE_POINTS", + "CYCLES": "CYCLES", + "MODULES": "MODULES", + "PRIORITY": "PRIORITY", + "START_DATE": "START_DATE", + "TARGET_DATE": "TARGET_DATE", + "CREATED_AT": "CREATED_AT", + "COMPLETED_AT": "COMPLETED_AT", + "CREATED_BY": "CREATED_BY", +} + + +def get_y_axis_filter(y_axis: str) -> Dict[str, Any]: + filter_mapping = { + "WORK_ITEM_COUNT": {"id": F("id")}, + } + return filter_mapping.get(y_axis, {}) + + +def get_x_axis_field() -> Dict[str, Tuple[str, str, Optional[Dict[str, Any]]]]: + return { + "STATES": ("state__id", "state__name", None), + "STATE_GROUPS": ("state__group", "state__group", None), + "LABELS": ( + "labels__id", + "labels__name", + {"label_issue__deleted_at__isnull": True}, + ), + "ASSIGNEES": ( + "assignees__id", + "assignees__display_name", + {"issue_assignee__deleted_at__isnull": True}, + ), + "ESTIMATE_POINTS": ("estimate_point__value", "estimate_point__key", None), + "CYCLES": ( + "issue_cycle__cycle_id", + "issue_cycle__cycle__name", + {"issue_cycle__deleted_at__isnull": True}, + ), + "MODULES": ( + "issue_module__module_id", + "issue_module__module__name", + {"issue_module__deleted_at__isnull": True}, + ), + "PRIORITY": ("priority", "priority", None), + "START_DATE": ("start_date", "start_date", None), + "TARGET_DATE": ("target_date", "target_date", None), + "CREATED_AT": ("created_at__date", "created_at__date", None), + "COMPLETED_AT": ("completed_at__date", "completed_at__date", None), + "CREATED_BY": ("created_by_id", "created_by__display_name", None), + } + + +def process_grouped_data( + data: List[Dict[str, Any]], +) -> Tuple[List[Dict[str, Any]], Dict[str, str]]: + response = {} + schema = {} + + for item in data: + key = item["key"] + if key not in response: + response[key] = { + "key": key if key else "none", + "name": ( + item.get("display_name", key) + if item.get("display_name", key) + else "None" + ), + "count": 0, + } + group_key = str(item["group_key"]) if item["group_key"] else "none" + schema[group_key] = item.get("group_name", item["group_key"]) + schema[group_key] = schema[group_key] if schema[group_key] else "None" + response[key][group_key] = response[key].get(group_key, 0) + item["count"] + response[key]["count"] += item["count"] + + return list(response.values()), schema + + +def build_number_chart_response( + queryset: QuerySet[Issue], + y_axis_filter: Dict[str, Any], + y_axis: str, + aggregate_func: Aggregate, +) -> List[Dict[str, Any]]: + count = ( + queryset.filter(**y_axis_filter).aggregate(total=aggregate_func).get("total", 0) + ) + return [{"key": y_axis, "name": y_axis, "count": count}] + + +def build_grouped_chart_response( + queryset: QuerySet[Issue], + id_field: str, + name_field: str, + group_field: str, + group_name_field: str, + aggregate_func: Aggregate, +) -> Tuple[List[Dict[str, Any]], Dict[str, str]]: + data = ( + queryset.annotate( + key=F(id_field), + group_key=F(group_field), + group_name=F(group_name_field), + display_name=F(name_field) if name_field else F(id_field), + ) + .values("key", "group_key", "group_name", "display_name") + .annotate(count=aggregate_func) + .order_by("-count") + ) + return process_grouped_data(data) + + +def build_simple_chart_response( + queryset: QuerySet, id_field: str, name_field: str, aggregate_func: Aggregate +) -> List[Dict[str, Any]]: + data = ( + queryset.annotate( + key=F(id_field), display_name=F(name_field) if name_field else F(id_field) + ) + .values("key", "display_name") + .annotate(count=aggregate_func) + .order_by("key") + ) + + return [ + { + "key": item["key"] if item["key"] else "None", + "name": item["display_name"] if item["display_name"] else "None", + "count": item["count"], + } + for item in data + ] + + +def build_analytics_chart( + queryset: QuerySet[Issue], + x_axis: str, + group_by: Optional[str] = None, + date_filter: Optional[str] = None, +) -> Dict[str, Union[List[Dict[str, Any]], Dict[str, str]]]: + + # Validate x_axis + if x_axis not in x_axis_mapper: + raise ValidationError(f"Invalid x_axis field: {x_axis}") + + # Validate group_by + if group_by and group_by not in x_axis_mapper: + raise ValidationError(f"Invalid group_by field: {group_by}") + + field_mapping = get_x_axis_field() + + id_field, name_field, additional_filter = field_mapping.get( + x_axis, (None, None, {}) + ) + group_field, group_name_field, group_additional_filter = field_mapping.get( + group_by, (None, None, {}) + ) + + # Apply additional filters if they exist + if additional_filter or {}: + queryset = queryset.filter(**additional_filter) + + if group_additional_filter or {}: + queryset = queryset.filter(**group_additional_filter) + + aggregate_func = Count("id", distinct=True) + + if group_field: + response, schema = build_grouped_chart_response( + queryset, + id_field, + name_field, + group_field, + group_name_field, + aggregate_func, + ) + else: + response = build_simple_chart_response( + queryset, id_field, name_field, aggregate_func + ) + schema = {} + + return {"data": response, "schema": schema} diff --git a/apiserver/plane/utils/date_utils.py b/apiserver/plane/utils/date_utils.py new file mode 100644 index 000000000..86e6b9a3e --- /dev/null +++ b/apiserver/plane/utils/date_utils.py @@ -0,0 +1,197 @@ +from datetime import datetime, timedelta, date +from django.utils import timezone +from typing import Dict, Optional, List, Union, Tuple, Any + +from plane.db.models import User + + +def get_analytics_date_range( + date_filter: Optional[str] = None, + start_date: Optional[str] = None, + end_date: Optional[str] = None, +) -> Optional[Dict[str, Dict[str, datetime]]]: + """ + Get date range for analytics with current and previous periods for comparison. + Returns a dictionary with current and previous date ranges. + + Args: + date_filter (str): The type of date filter to apply + start_date (str): Start date for custom range (format: YYYY-MM-DD) + end_date (str): End date for custom range (format: YYYY-MM-DD) + + Returns: + dict: Dictionary containing current and previous date ranges + """ + if not date_filter: + return None + + today = timezone.now().date() + + if date_filter == "yesterday": + yesterday = today - timedelta(days=1) + return { + "current": { + "gte": datetime.combine(yesterday, datetime.min.time()), + "lte": datetime.combine(yesterday, datetime.max.time()), + } + } + elif date_filter == "last_7_days": + return { + "current": { + "gte": datetime.combine(today - timedelta(days=7), datetime.min.time()), + "lte": datetime.combine(today, datetime.max.time()), + }, + "previous": { + "gte": datetime.combine( + today - timedelta(days=14), datetime.min.time() + ), + "lte": datetime.combine(today - timedelta(days=8), datetime.max.time()), + }, + } + elif date_filter == "last_30_days": + return { + "current": { + "gte": datetime.combine( + today - timedelta(days=30), datetime.min.time() + ), + "lte": datetime.combine(today, datetime.max.time()), + }, + "previous": { + "gte": datetime.combine( + today - timedelta(days=60), datetime.min.time() + ), + "lte": datetime.combine( + today - timedelta(days=31), datetime.max.time() + ), + }, + } + elif date_filter == "last_3_months": + return { + "current": { + "gte": datetime.combine( + today - timedelta(days=90), datetime.min.time() + ), + "lte": datetime.combine(today, datetime.max.time()), + }, + "previous": { + "gte": datetime.combine( + today - timedelta(days=180), datetime.min.time() + ), + "lte": datetime.combine( + today - timedelta(days=91), datetime.max.time() + ), + }, + } + elif date_filter == "custom" and start_date and end_date: + try: + start = datetime.strptime(start_date, "%Y-%m-%d").date() + end = datetime.strptime(end_date, "%Y-%m-%d").date() + return { + "current": { + "gte": datetime.combine(start, datetime.min.time()), + "lte": datetime.combine(end, datetime.max.time()), + } + } + except (ValueError, TypeError): + return None + return None + + +def get_chart_period_range( + date_filter: Optional[str] = None, +) -> Optional[Tuple[date, date]]: + """ + Get date range for chart visualization. + Returns a tuple of (start_date, end_date) for the specified period. + + Args: + date_filter (str): The type of date filter to apply. Options are: + - "yesterday": Yesterday's date + - "last_7_days": Last 7 days + - "last_30_days": Last 30 days + - "last_3_months": Last 90 days + Defaults to "last_7_days" if not specified or invalid. + + Returns: + tuple: A tuple containing (start_date, end_date) as date objects + """ + if not date_filter: + return None + + today = timezone.now().date() + period_ranges = { + "yesterday": ( + today - timedelta(days=1), + today - timedelta(days=1), + ), + "last_7_days": (today - timedelta(days=7), today), + "last_30_days": (today - timedelta(days=30), today), + "last_3_months": (today - timedelta(days=90), today), + } + + return period_ranges.get(date_filter, period_ranges["last_7_days"]) + + +def get_analytics_filters( + slug: str, + user: User, + type: str, + date_filter: Optional[str] = None, + project_ids: Optional[Union[str, List[str]]] = None, +) -> Dict[str, Any]: + """ + Get combined project and date filters for analytics endpoints + + Args: + slug: The workspace slug + user: The current user + type: The type of filter ("analytics" or "chart") + date_filter: Optional date filter string + project_ids: Optional list of project IDs or comma-separated string of project IDs + + Returns: + dict: A dictionary containing: + - base_filters: Base filters for the workspace and user + - project_filters: Project-specific filters + - analytics_date_range: Date range filters for analytics comparison + - chart_period_range: Date range for chart visualization + """ + # Get project IDs from request + if project_ids and isinstance(project_ids, str): + project_ids = [str(project_id) for project_id in project_ids.split(",")] + + # Base filters for workspace and user + base_filters = { + "workspace__slug": slug, + "project__project_projectmember__member": user, + "project__project_projectmember__is_active": True, + } + + # Project filters + project_filters = { + "workspace__slug": slug, + "project_projectmember__member": user, + "project_projectmember__is_active": True, + } + + # Add project IDs to filters if provided + if project_ids: + base_filters["project_id__in"] = project_ids + project_filters["id__in"] = project_ids + + # Initialize date range variables + analytics_date_range = None + chart_period_range = None + + # Get date range filters based on type + if type == "analytics": + analytics_date_range = get_analytics_date_range(date_filter) + elif type == "chart": + chart_period_range = get_chart_period_range(date_filter) + + return { + "base_filters": base_filters, + "project_filters": project_filters, + "analytics_date_range": analytics_date_range, + "chart_period_range": chart_period_range, + } From 26c8cba322d10a6175f3aa733230ec95db0de8ac Mon Sep 17 00:00:00 2001 From: Aaron Heckmann Date: Mon, 12 May 2025 00:46:30 -0700 Subject: [PATCH 041/201] [WEB-4008] fix: handle when settings are None #7016 https://app.plane.so/plane/browse/WEB-4008/ --- apiserver/plane/authentication/utils/host.py | 8 ++++++-- apiserver/plane/utils/host.py | 8 ++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/apiserver/plane/authentication/utils/host.py b/apiserver/plane/authentication/utils/host.py index 67c8a4f72..415791a87 100644 --- a/apiserver/plane/authentication/utils/host.py +++ b/apiserver/plane/authentication/utils/host.py @@ -21,7 +21,9 @@ def base_host( # Admin redirection if is_admin: - admin_base_path = getattr(settings, "ADMIN_BASE_PATH", "/god-mode/") + admin_base_path = getattr(settings, "ADMIN_BASE_PATH", None) + if not isinstance(admin_base_path, str): + admin_base_path = "/god-mode/" if not admin_base_path.startswith("/"): admin_base_path = "/" + admin_base_path if not admin_base_path.endswith("/"): @@ -34,7 +36,9 @@ def base_host( # Space redirection if is_space: - space_base_path = getattr(settings, "SPACE_BASE_PATH", "/spaces/") + space_base_path = getattr(settings, "SPACE_BASE_PATH", None) + if not isinstance(space_base_path, str): + space_base_path = "/spaces/" if not space_base_path.startswith("/"): space_base_path = "/" + space_base_path if not space_base_path.endswith("/"): diff --git a/apiserver/plane/utils/host.py b/apiserver/plane/utils/host.py index d74a86ffd..860e19e0e 100644 --- a/apiserver/plane/utils/host.py +++ b/apiserver/plane/utils/host.py @@ -25,7 +25,9 @@ def base_host( # Admin redirection if is_admin: - admin_base_path = getattr(settings, "ADMIN_BASE_PATH", "/god-mode/") + admin_base_path = getattr(settings, "ADMIN_BASE_PATH", None) + if not isinstance(admin_base_path, str): + admin_base_path = "/god-mode/" if not admin_base_path.startswith("/"): admin_base_path = "/" + admin_base_path if not admin_base_path.endswith("/"): @@ -38,7 +40,9 @@ def base_host( # Space redirection if is_space: - space_base_path = getattr(settings, "SPACE_BASE_PATH", "/spaces/") + space_base_path = getattr(settings, "SPACE_BASE_PATH", None) + if not isinstance(space_base_path, str): + space_base_path = "/spaces/" if not space_base_path.startswith("/"): space_base_path = "/" + space_base_path if not space_base_path.endswith("/"): From e68d3444103ccd7768129816ef12175b606c1940 Mon Sep 17 00:00:00 2001 From: Vamsi Krishna <46787868+vamsikrishnamathala@users.noreply.github.com> Date: Mon, 12 May 2025 18:21:05 +0530 Subject: [PATCH 042/201] [WEB-4074]fix: removed sub-work item filters at nested levels #7047 --- .../sub-issues/issues-list/list-group.tsx | 4 +- .../sub-issues/issues-list/root.tsx | 43 ++++++++++--------- 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/web/core/components/issues/issue-detail-widgets/sub-issues/issues-list/list-group.tsx b/web/core/components/issues/issue-detail-widgets/sub-issues/issues-list/list-group.tsx index 3b84cd732..adc6cb23a 100644 --- a/web/core/components/issues/issue-detail-widgets/sub-issues/issues-list/list-group.tsx +++ b/web/core/components/issues/issue-detail-widgets/sub-issues/issues-list/list-group.tsx @@ -15,6 +15,7 @@ interface TSubIssuesListGroupProps { serviceType: TIssueServiceType; disabled: boolean; parentIssueId: string; + rootIssueId: string; handleIssueCrudState: ( key: "create" | "existing" | "update" | "delete", issueId: string, @@ -31,6 +32,7 @@ export const SubIssuesListGroup: FC = observer((props) serviceType, disabled, parentIssueId, + rootIssueId, projectId, workspaceSlug, handleIssueCrudState, @@ -79,7 +81,7 @@ export const SubIssuesListGroup: FC = observer((props) workspaceSlug={workspaceSlug} projectId={projectId} parentIssueId={parentIssueId} - rootIssueId={parentIssueId} + rootIssueId={rootIssueId} issueId={workItemId} disabled={disabled} handleIssueCrudState={handleIssueCrudState} diff --git a/web/core/components/issues/issue-detail-widgets/sub-issues/issues-list/root.tsx b/web/core/components/issues/issue-detail-widgets/sub-issues/issues-list/root.tsx index f86765167..2e88e0bc5 100644 --- a/web/core/components/issues/issue-detail-widgets/sub-issues/issues-list/root.tsx +++ b/web/core/components/issues/issue-detail-widgets/sub-issues/issues-list/root.tsx @@ -50,7 +50,7 @@ export const SubIssuesListRoot: React.FC = observer((props) => { } = useIssueDetail(issueServiceType); // derived values - const filters = getSubIssueFilters(parentIssueId); + const filters = getSubIssueFilters(rootIssueId); const isRootLevel = useMemo(() => rootIssueId === parentIssueId, [rootIssueId, parentIssueId]); const group_by = isRootLevel ? (filters?.displayFilters?.group_by ?? null) : null; const filteredSubWorkItemsCount = (getFilteredSubWorkItems(rootIssueId, filters.filters ?? {}) ?? []).length; @@ -66,37 +66,20 @@ export const SubIssuesListRoot: React.FC = observer((props) => { const getWorkItemIds = useCallback( (groupId: string) => { if (isRootLevel) { - const groupedSubIssues = getGroupedSubWorkItems(parentIssueId); + const groupedSubIssues = getGroupedSubWorkItems(rootIssueId); return groupedSubIssues?.[groupId] ?? []; } const subIssueIds = subIssuesByIssueId(parentIssueId); return subIssueIds ?? []; }, - [isRootLevel, subIssuesByIssueId, parentIssueId, getGroupedSubWorkItems] + [isRootLevel, subIssuesByIssueId, rootIssueId, getGroupedSubWorkItems, parentIssueId] ); const isSubWorkItems = issueServiceType === EIssueServiceType.ISSUES; return (
- {filteredSubWorkItemsCount > 0 ? ( - groups?.map((group) => ( - - )) - ) : ( + {isRootLevel && filteredSubWorkItemsCount === 0 ? ( = observer((props) => { } /> + ) : ( + groups?.map((group) => ( + + )) )}
); From dc16f2862e749373e4ffa1604d8eb5cb11755957 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Mon, 12 May 2025 18:37:36 +0530 Subject: [PATCH 043/201] [WIKI-181] refactor: make file handling generic in editor (#7046) * refactor: make file handling generic * fix: useeffect dependency array * chore: remove mime type to extension conversion --- .../src/ce/extensions/core/extensions.ts | 3 +- packages/editor/src/core/constants/config.ts | 54 +++++- .../components/image-uploader.tsx | 30 ++-- .../extensions/custom-image/custom-image.ts | 6 +- packages/editor/src/core/extensions/drop.ts | 127 ++++++++++++++ packages/editor/src/core/extensions/drop.tsx | 94 ---------- .../editor/src/core/extensions/extensions.tsx | 1 + .../src/core/helpers/editor-commands.ts | 4 +- .../validate-file.ts => helpers/file.ts} | 8 +- .../editor/src/core/hooks/use-file-upload.ts | 164 +++++++++--------- .../editor/src/core/plugins/drag-handle.ts | 4 +- .../src/core/plugins/image/constants.ts | 7 - .../src/core/plugins/image/delete-image.ts | 18 +- .../editor/src/core/plugins/image/index.ts | 2 - .../src/core/plugins/image/utils/index.ts | 1 - packages/utils/src/attachment.ts | 32 ++++ packages/utils/src/index.ts | 2 +- 17 files changed, 334 insertions(+), 223 deletions(-) create mode 100644 packages/editor/src/core/extensions/drop.ts delete mode 100644 packages/editor/src/core/extensions/drop.tsx rename packages/editor/src/core/{plugins/image/utils/validate-file.ts => helpers/file.ts} (74%) delete mode 100644 packages/editor/src/core/plugins/image/constants.ts delete mode 100644 packages/editor/src/core/plugins/image/utils/index.ts create mode 100644 packages/utils/src/attachment.ts diff --git a/packages/editor/src/ce/extensions/core/extensions.ts b/packages/editor/src/ce/extensions/core/extensions.ts index d03229133..cecfb38b4 100644 --- a/packages/editor/src/ce/extensions/core/extensions.ts +++ b/packages/editor/src/ce/extensions/core/extensions.ts @@ -1,9 +1,10 @@ import { Extensions } from "@tiptap/core"; // types -import { TExtensions } from "@/types"; +import { TExtensions, TFileHandler } from "@/types"; type Props = { disabledExtensions: TExtensions[]; + fileHandler: TFileHandler; }; export const CoreEditorAdditionalExtensions = (props: Props): Extensions => { diff --git a/packages/editor/src/core/constants/config.ts b/packages/editor/src/core/constants/config.ts index ac6d63dd1..922be9ef9 100644 --- a/packages/editor/src/core/constants/config.ts +++ b/packages/editor/src/core/constants/config.ts @@ -8,5 +8,55 @@ export const DEFAULT_DISPLAY_CONFIG: TDisplayConfig = { wideLayout: false, }; -export const ACCEPTED_FILE_MIME_TYPES = ["image/jpeg", "image/jpg", "image/png", "image/webp", "image/gif"]; -export const ACCEPTED_FILE_EXTENSIONS = ACCEPTED_FILE_MIME_TYPES.map((type) => `.${type.split("/")[1]}`); +export const ACCEPTED_IMAGE_MIME_TYPES = ["image/jpeg", "image/jpg", "image/png", "image/webp", "image/gif"]; + +export const ACCEPTED_ATTACHMENT_MIME_TYPES = [ + "image/jpeg", + "image/png", + "image/gif", + "image/svg+xml", + "image/webp", + "image/tiff", + "image/bmp", + "application/pdf", + "application/msword", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "application/vnd.ms-excel", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "application/vnd.ms-powerpoint", + "application/vnd.openxmlformats-officedocument.presentationml.presentation", + "text/plain", + "application/rtf", + "audio/mpeg", + "audio/wav", + "audio/ogg", + "audio/midi", + "audio/x-midi", + "audio/aac", + "audio/flac", + "audio/x-m4a", + "video/mp4", + "video/mpeg", + "video/ogg", + "video/webm", + "video/quicktime", + "video/x-msvideo", + "video/x-ms-wmv", + "application/zip", + "application/x-rar-compressed", + "application/x-tar", + "application/gzip", + "model/gltf-binary", + "model/gltf+json", + "application/octet-stream", + "font/ttf", + "font/otf", + "font/woff", + "font/woff2", + "text/css", + "text/javascript", + "application/json", + "text/xml", + "text/csv", + "application/xml", +]; diff --git a/packages/editor/src/core/extensions/custom-image/components/image-uploader.tsx b/packages/editor/src/core/extensions/custom-image/components/image-uploader.tsx index 0fd0e6dd4..0a3ee1a1c 100644 --- a/packages/editor/src/core/extensions/custom-image/components/image-uploader.tsx +++ b/packages/editor/src/core/extensions/custom-image/components/image-uploader.tsx @@ -3,11 +3,11 @@ import { ChangeEvent, useCallback, useEffect, useMemo, useRef } from "react"; // plane utils import { cn } from "@plane/utils"; // constants -import { ACCEPTED_FILE_EXTENSIONS } from "@/constants/config"; +import { ACCEPTED_IMAGE_MIME_TYPES } from "@/constants/config"; // extensions import { CustoBaseImageNodeViewProps, getImageComponentImageFileMap } from "@/extensions/custom-image"; // hooks -import { useUploader, useDropZone, uploadFirstImageAndInsertRemaining } from "@/hooks/use-file-upload"; +import { useUploader, useDropZone, uploadFirstFileAndInsertRemaining } from "@/hooks/use-file-upload"; type CustomImageUploaderProps = CustoBaseImageNodeViewProps & { maxFileSize: number; @@ -41,7 +41,9 @@ export const CustomImageUploader = (props: CustomImageUploaderProps) => { if (!imageEntityId) return; setIsUploaded(true); // Update the node view's src attribute post upload - updateAttributes({ src: url }); + updateAttributes({ + src: url, + }); imageComponentImageFileMap?.delete(imageEntityId); const pos = getPos(); @@ -51,7 +53,7 @@ export const CustomImageUploader = (props: CustomImageUploaderProps) => { // only if the cursor is at the current image component, manipulate // the cursor position - if (currentNode && currentNode.type.name === "imageComponent" && currentNode.attrs.src === url) { + if (currentNode && currentNode.type.name === node.type.name && currentNode.attrs.src === url) { // control cursor position after upload const nextNode = editor.state.doc.nodeAt(pos + 1); @@ -68,17 +70,23 @@ export const CustomImageUploader = (props: CustomImageUploaderProps) => { [imageComponentImageFileMap, imageEntityId, updateAttributes, getPos] ); // hooks - const { uploading: isImageBeingUploaded, uploadFile } = useUploader({ - blockId: imageEntityId ?? "", - editor, - loadImageFromFileSystem, + const { isUploading: isImageBeingUploaded, uploadFile } = useUploader({ + acceptedMimeTypes: ACCEPTED_IMAGE_MIME_TYPES, + // @ts-expect-error - TODO: fix typings, and don't remove await from here for now + editorCommand: async (file) => await editor?.commands.uploadImage(imageEntityId, file), + handleProgressStatus: (isUploading) => { + editor.storage.imageComponent.uploadInProgress = isUploading; + }, + loadFileFromFileSystem: loadImageFromFileSystem, maxFileSize, onUpload, }); const { draggedInside, onDrop, onDragEnter, onDragLeave } = useDropZone({ + acceptedMimeTypes: ACCEPTED_IMAGE_MIME_TYPES, editor, maxFileSize, pos: getPos(), + type: "image", uploader: uploadFile, }); @@ -110,11 +118,13 @@ export const CustomImageUploader = (props: CustomImageUploaderProps) => { if (!filesList) { return; } - await uploadFirstImageAndInsertRemaining({ + await uploadFirstFileAndInsertRemaining({ + acceptedMimeTypes: ACCEPTED_IMAGE_MIME_TYPES, editor, filesList, maxFileSize, pos: getPos(), + type: "image", uploader: uploadFile, }); }, @@ -170,7 +180,7 @@ export const CustomImageUploader = (props: CustomImageUploaderProps) => { ref={fileInputRef} hidden type="file" - accept={ACCEPTED_FILE_EXTENSIONS.join(",")} + accept={ACCEPTED_IMAGE_MIME_TYPES.join(",")} onChange={onFileChange} multiple /> diff --git a/packages/editor/src/core/extensions/custom-image/custom-image.ts b/packages/editor/src/core/extensions/custom-image/custom-image.ts index 4f1b3c8db..a9a69fa60 100644 --- a/packages/editor/src/core/extensions/custom-image/custom-image.ts +++ b/packages/editor/src/core/extensions/custom-image/custom-image.ts @@ -2,12 +2,15 @@ import { Editor, mergeAttributes } from "@tiptap/core"; import { Image } from "@tiptap/extension-image"; import { ReactNodeViewRenderer } from "@tiptap/react"; import { v4 as uuidv4 } from "uuid"; +// constants +import { ACCEPTED_IMAGE_MIME_TYPES } from "@/constants/config"; // extensions import { CustomImageNode } from "@/extensions/custom-image"; // helpers +import { isFileValid } from "@/helpers/file"; import { insertEmptyParagraphAtNodeBoundaries } from "@/helpers/insert-empty-paragraph-at-node-boundary"; // plugins -import { TrackImageDeletionPlugin, TrackImageRestorationPlugin, isFileValid } from "@/plugins/image"; +import { TrackImageDeletionPlugin, TrackImageRestorationPlugin } from "@/plugins/image"; // types import { TFileHandler } from "@/types"; @@ -146,6 +149,7 @@ export const CustomImageExtension = (props: TFileHandler) => { if ( props?.file && !isFileValid({ + acceptedMimeTypes: ACCEPTED_IMAGE_MIME_TYPES, file: props.file, maxFileSize, }) diff --git a/packages/editor/src/core/extensions/drop.ts b/packages/editor/src/core/extensions/drop.ts new file mode 100644 index 000000000..2a5a994f8 --- /dev/null +++ b/packages/editor/src/core/extensions/drop.ts @@ -0,0 +1,127 @@ +import { Extension, Editor } from "@tiptap/core"; +import { Plugin, PluginKey } from "@tiptap/pm/state"; +// constants +import { ACCEPTED_ATTACHMENT_MIME_TYPES, ACCEPTED_IMAGE_MIME_TYPES } from "@/constants/config"; +// types +import { TEditorCommands } from "@/types"; + +export const DropHandlerExtension = Extension.create({ + name: "dropHandler", + priority: 1000, + + addProseMirrorPlugins() { + const editor = this.editor; + return [ + new Plugin({ + key: new PluginKey("drop-handler-plugin"), + props: { + handlePaste: (view, event) => { + if ( + editor.isEditable && + event.clipboardData && + event.clipboardData.files && + event.clipboardData.files.length > 0 + ) { + event.preventDefault(); + const files = Array.from(event.clipboardData.files); + const acceptedFiles = files.filter( + (f) => ACCEPTED_IMAGE_MIME_TYPES.includes(f.type) || ACCEPTED_ATTACHMENT_MIME_TYPES.includes(f.type) + ); + + if (acceptedFiles.length) { + const pos = view.state.selection.from; + insertFilesSafely({ + editor, + files: acceptedFiles, + initialPos: pos, + event: "drop", + }); + } + return true; + } + return false; + }, + handleDrop: (view, event, _slice, moved) => { + if ( + editor.isEditable && + !moved && + event.dataTransfer && + event.dataTransfer.files && + event.dataTransfer.files.length > 0 + ) { + event.preventDefault(); + const files = Array.from(event.dataTransfer.files); + const acceptedFiles = files.filter( + (f) => ACCEPTED_IMAGE_MIME_TYPES.includes(f.type) || ACCEPTED_ATTACHMENT_MIME_TYPES.includes(f.type) + ); + + if (acceptedFiles.length) { + const coordinates = view.posAtCoords({ + left: event.clientX, + top: event.clientY, + }); + + if (coordinates) { + const pos = coordinates.pos; + insertFilesSafely({ + editor, + files: acceptedFiles, + initialPos: pos, + event: "drop", + }); + } + return true; + } + } + return false; + }, + }, + }), + ]; + }, +}); + +type InsertFilesSafelyArgs = { + editor: Editor; + event: "insert" | "drop"; + files: File[]; + initialPos: number; + type?: Extract; +}; + +export const insertFilesSafely = async (args: InsertFilesSafelyArgs) => { + const { editor, event, files, initialPos, type } = args; + let pos = initialPos; + + for (const file of files) { + // safe insertion + const docSize = editor.state.doc.content.size; + pos = Math.min(pos, docSize); + + let fileType: "image" | "attachment" | null = null; + + try { + if (type) { + if (["image", "attachment"].includes(type)) fileType = type; + else throw new Error("Wrong file type passed"); + } else { + if (ACCEPTED_IMAGE_MIME_TYPES.includes(file.type)) fileType = "image"; + else if (ACCEPTED_ATTACHMENT_MIME_TYPES.includes(file.type)) fileType = "attachment"; + } + // insert file depending on the type at the current position + if (fileType === "image") { + editor.commands.insertImageComponent({ + file, + pos, + event, + }); + } else if (fileType === "attachment") { + } + } catch (error) { + console.error(`Error while ${event}ing file:`, error); + } + + // Move to the next position + pos += 1; + } +}; diff --git a/packages/editor/src/core/extensions/drop.tsx b/packages/editor/src/core/extensions/drop.tsx deleted file mode 100644 index 0d578770a..000000000 --- a/packages/editor/src/core/extensions/drop.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { Extension, Editor } from "@tiptap/core"; -import { Plugin, PluginKey } from "@tiptap/pm/state"; -import { EditorView } from "@tiptap/pm/view"; - -export const DropHandlerExtension = Extension.create({ - name: "dropHandler", - priority: 1000, - - addProseMirrorPlugins() { - const editor = this.editor; - return [ - new Plugin({ - key: new PluginKey("drop-handler-plugin"), - props: { - handlePaste: (view: EditorView, event: ClipboardEvent) => { - if ( - editor.isEditable && - event.clipboardData && - event.clipboardData.files && - event.clipboardData.files.length > 0 - ) { - event.preventDefault(); - const files = Array.from(event.clipboardData.files); - const imageFiles = files.filter((file) => file.type.startsWith("image")); - - if (imageFiles.length > 0) { - const pos = view.state.selection.from; - insertImagesSafely({ editor, files: imageFiles, initialPos: pos, event: "drop" }); - } - return true; - } - return false; - }, - handleDrop: (view: EditorView, event: DragEvent, _slice: any, moved: boolean) => { - if ( - editor.isEditable && - !moved && - event.dataTransfer && - event.dataTransfer.files && - event.dataTransfer.files.length > 0 - ) { - event.preventDefault(); - const files = Array.from(event.dataTransfer.files); - const imageFiles = files.filter((file) => file.type.startsWith("image")); - - if (imageFiles.length > 0) { - const coordinates = view.posAtCoords({ - left: event.clientX, - top: event.clientY, - }); - - if (coordinates) { - const pos = coordinates.pos; - insertImagesSafely({ editor, files: imageFiles, initialPos: pos, event: "drop" }); - } - return true; - } - } - return false; - }, - }, - }), - ]; - }, -}); -export const insertImagesSafely = async ({ - editor, - files, - initialPos, - event, -}: { - editor: Editor; - files: File[]; - initialPos: number; - event: "insert" | "drop"; -}) => { - let pos = initialPos; - - for (const file of files) { - // safe insertion - const docSize = editor.state.doc.content.size; - pos = Math.min(pos, docSize); - - try { - // Insert the image at the current position - editor.commands.insertImageComponent({ file, pos, event }); - } catch (error) { - console.error(`Error while ${event}ing image:`, error); - } - - // Move to the next position - pos += 1; - } -}; diff --git a/packages/editor/src/core/extensions/extensions.tsx b/packages/editor/src/core/extensions/extensions.tsx index ff200cd32..1ef0a3b15 100644 --- a/packages/editor/src/core/extensions/extensions.tsx +++ b/packages/editor/src/core/extensions/extensions.tsx @@ -172,6 +172,7 @@ export const CoreEditorExtensions = (args: TArguments): Extensions => { CustomColorExtension, ...CoreEditorAdditionalExtensions({ disabledExtensions, + fileHandler, }), ]; diff --git a/packages/editor/src/core/helpers/editor-commands.ts b/packages/editor/src/core/helpers/editor-commands.ts index 39796ac24..e8c98ada5 100644 --- a/packages/editor/src/core/helpers/editor-commands.ts +++ b/packages/editor/src/core/helpers/editor-commands.ts @@ -1,7 +1,6 @@ import { Editor, Range } from "@tiptap/core"; -// types -import { InsertImageComponentProps } from "@/extensions"; // extensions +import { InsertImageComponentProps } from "@/extensions"; import { replaceCodeWithText } from "@/extensions/code/utils/replace-code-block-with-text"; // helpers import { findTableAncestor } from "@/helpers/common"; @@ -206,6 +205,7 @@ export const insertHorizontalRule = (editor: Editor, range?: Range) => { if (range) editor.chain().focus().deleteRange(range).setHorizontalRule().run(); else editor.chain().focus().setHorizontalRule().run(); }; + export const insertCallout = (editor: Editor, range?: Range) => { if (range) editor.chain().focus().deleteRange(range).insertCallout().run(); else editor.chain().focus().insertCallout().run(); diff --git a/packages/editor/src/core/plugins/image/utils/validate-file.ts b/packages/editor/src/core/helpers/file.ts similarity index 74% rename from packages/editor/src/core/plugins/image/utils/validate-file.ts rename to packages/editor/src/core/helpers/file.ts index 703bb2bf0..f2c9968f0 100644 --- a/packages/editor/src/core/plugins/image/utils/validate-file.ts +++ b/packages/editor/src/core/helpers/file.ts @@ -1,20 +1,18 @@ -// constants -import { ACCEPTED_FILE_MIME_TYPES } from "@/constants/config"; - type TArgs = { + acceptedMimeTypes: string[]; file: File; maxFileSize: number; }; export const isFileValid = (args: TArgs): boolean => { - const { file, maxFileSize } = args; + const { acceptedMimeTypes, file, maxFileSize } = args; if (!file) { alert("No file selected. Please select a file to upload."); return false; } - if (!ACCEPTED_FILE_MIME_TYPES.includes(file.type)) { + if (!acceptedMimeTypes.includes(file.type)) { alert("Invalid file type. Please select a JPEG, JPG, PNG, WEBP or GIF file."); return false; } diff --git a/packages/editor/src/core/hooks/use-file-upload.ts b/packages/editor/src/core/hooks/use-file-upload.ts index e57c811a0..b707824f2 100644 --- a/packages/editor/src/core/hooks/use-file-upload.ts +++ b/packages/editor/src/core/hooks/use-file-upload.ts @@ -1,87 +1,87 @@ -import { DragEvent, useCallback, useEffect, useState } from "react"; import { Editor } from "@tiptap/core"; +import { DragEvent, useCallback, useEffect, useState } from "react"; // extensions -import { insertImagesSafely } from "@/extensions/drop"; +import { insertFilesSafely } from "@/extensions/drop"; // plugins -import { isFileValid } from "@/plugins/image"; +import { isFileValid } from "@/helpers/file"; +// types +import { TEditorCommands } from "@/types"; type TUploaderArgs = { - blockId: string; - editor: Editor; - loadImageFromFileSystem: (file: string) => void; + acceptedMimeTypes: string[]; + editorCommand: (file: File) => Promise; + handleProgressStatus?: (isUploading: boolean) => void; + loadFileFromFileSystem?: (file: string) => void; maxFileSize: number; - onUpload: (url: string) => void; + onUpload: (url: string, file: File) => void; }; export const useUploader = (args: TUploaderArgs) => { - const { blockId, editor, loadImageFromFileSystem, maxFileSize, onUpload } = args; + const { acceptedMimeTypes, editorCommand, handleProgressStatus, loadFileFromFileSystem, maxFileSize, onUpload } = + args; // states - const [uploading, setUploading] = useState(false); + const [isUploading, setIsUploading] = useState(false); const uploadFile = useCallback( async (file: File) => { - const setImageUploadInProgress = (isUploading: boolean) => { - if (editor.storage.imageComponent) { - editor.storage.imageComponent.uploadInProgress = isUploading; - } - }; - setImageUploadInProgress(true); - setUploading(true); - const fileNameTrimmed = trimFileName(file.name); - const fileWithTrimmedName = new File([file], fileNameTrimmed, { type: file.type }); + handleProgressStatus?.(true); + setIsUploading(true); const isValid = isFileValid({ - file: fileWithTrimmedName, + acceptedMimeTypes, + file, maxFileSize, }); if (!isValid) { - setImageUploadInProgress(false); + handleProgressStatus?.(false); + setIsUploading(false); return; } try { - const reader = new FileReader(); - reader.onload = () => { - if (reader.result) { - loadImageFromFileSystem(reader.result as string); - } else { - console.error("Failed to read the file: reader.result is null"); - } - }; - reader.onerror = () => { - console.error("Error reading file"); - }; - reader.readAsDataURL(fileWithTrimmedName); - // @ts-expect-error - TODO: fix typings, and don't remove await from - // here for now - const url: string = await editor?.commands.uploadImage(blockId, fileWithTrimmedName); + if (loadFileFromFileSystem) { + const reader = new FileReader(); + reader.onload = () => { + if (reader.result) { + loadFileFromFileSystem(reader.result as string); + } else { + console.error("Failed to read the file: reader.result is null"); + } + }; + reader.onerror = () => { + console.error("Error reading file"); + }; + reader.readAsDataURL(file); + } + const url: string = await editorCommand(file); if (!url) { - throw new Error("Something went wrong while uploading the image"); + throw new Error("Something went wrong while uploading the file."); } - onUpload(url); - } catch (errPayload: any) { - console.log(errPayload); + onUpload(url, file); + } catch (errPayload) { const error = errPayload?.response?.data?.error || "Something went wrong"; console.error(error); } finally { - setImageUploadInProgress(false); - setUploading(false); + handleProgressStatus?.(false); + setIsUploading(false); } }, - [onUpload] + [acceptedMimeTypes, editorCommand, handleProgressStatus, loadFileFromFileSystem, maxFileSize, onUpload] ); - return { uploading, uploadFile }; + return { isUploading, uploadFile }; }; type TDropzoneArgs = { + acceptedMimeTypes: string[]; editor: Editor; maxFileSize: number; pos: number; + type: Extract; uploader: (file: File) => Promise; }; export const useDropZone = (args: TDropzoneArgs) => { - const { editor, maxFileSize, pos, uploader } = args; + const { acceptedMimeTypes, editor, maxFileSize, pos, type, uploader } = args; // states const [isDragging, setIsDragging] = useState(false); const [draggedInside, setDraggedInside] = useState(false); @@ -112,83 +112,79 @@ export const useDropZone = (args: TDropzoneArgs) => { return; } const filesList = e.dataTransfer.files; - await uploadFirstImageAndInsertRemaining({ + await uploadFirstFileAndInsertRemaining({ + acceptedMimeTypes, editor, filesList, maxFileSize, pos, + type, uploader, }); }, - [uploader, editor, pos] + [acceptedMimeTypes, editor, maxFileSize, pos, type, uploader] ); + const onDragEnter = useCallback(() => setDraggedInside(true), []); + const onDragLeave = useCallback(() => setDraggedInside(false), []); - const onDragEnter = () => { - setDraggedInside(true); + return { + isDragging, + draggedInside, + onDragEnter, + onDragLeave, + onDrop, }; - - const onDragLeave = () => { - setDraggedInside(false); - }; - - return { isDragging, draggedInside, onDragEnter, onDragLeave, onDrop }; }; -function trimFileName(fileName: string, maxLength = 100) { - if (fileName.length > maxLength) { - const extension = fileName.split(".").pop(); - const nameWithoutExtension = fileName.slice(0, -(extension?.length ?? 0 + 1)); - const allowedNameLength = maxLength - (extension?.length ?? 0) - 1; // -1 for the dot - return `${nameWithoutExtension.slice(0, allowedNameLength)}.${extension}`; - } - - return fileName; -} - -type TMultipleImagesArgs = { +type TMultipleFileArgs = { + acceptedMimeTypes: string[]; editor: Editor; filesList: FileList; maxFileSize: number; pos: number; + type: Extract; uploader: (file: File) => Promise; }; -// Upload the first image and insert the remaining images for uploading multiple image -// post insertion of image-component -export async function uploadFirstImageAndInsertRemaining(args: TMultipleImagesArgs) { - const { editor, filesList, maxFileSize, pos, uploader } = args; +// Upload the first file and insert the remaining ones for uploading multiple files +export const uploadFirstFileAndInsertRemaining = async (args: TMultipleFileArgs) => { + const { acceptedMimeTypes, editor, filesList, maxFileSize, pos, type, uploader } = args; const filteredFiles: File[] = []; for (let i = 0; i < filesList.length; i += 1) { - const item = filesList.item(i); + const file = filesList.item(i); if ( - item && - item.type.indexOf("image") !== -1 && + file && isFileValid({ - file: item, + acceptedMimeTypes, + file, maxFileSize, }) ) { - filteredFiles.push(item); + filteredFiles.push(file); } } if (filteredFiles.length !== filesList.length) { - console.warn("Some files were not images and have been ignored."); + console.warn("Some files were invalid and have been ignored."); } if (filteredFiles.length === 0) { - console.error("No image files found to upload"); + console.error("No files found to upload."); return; } - // Upload the first image + // Upload the first file const firstFile = filteredFiles[0]; uploader(firstFile); - - // Insert the remaining images + // Insert the remaining files const remainingFiles = filteredFiles.slice(1); - if (remainingFiles.length > 0) { const docSize = editor.state.doc.content.size; - const posOfNextImageToBeInserted = Math.min(pos + 1, docSize); - insertImagesSafely({ editor, files: remainingFiles, initialPos: posOfNextImageToBeInserted, event: "drop" }); + const posOfNextFileToBeInserted = Math.min(pos + 1, docSize); + insertFilesSafely({ + editor, + files: remainingFiles, + initialPos: posOfNextFileToBeInserted, + event: "drop", + type, + }); } -} +}; diff --git a/packages/editor/src/core/plugins/drag-handle.ts b/packages/editor/src/core/plugins/drag-handle.ts index e71c38b30..f9a60a48c 100644 --- a/packages/editor/src/core/plugins/drag-handle.ts +++ b/packages/editor/src/core/plugins/drag-handle.ts @@ -10,10 +10,10 @@ const verticalEllipsisIcon = const generalSelectors = [ "li", - "p:not(:first-child)", + "p.editor-paragraph-block:not(:first-child)", ".code-block", "blockquote", - "h1, h2, h3, h4, h5, h6", + "h1.editor-heading-block, h2.editor-heading-block, h3.editor-heading-block, h4.editor-heading-block, h5.editor-heading-block, h6.editor-heading-block", "[data-type=horizontalRule]", ".table-wrapper", ".issue-embed", diff --git a/packages/editor/src/core/plugins/image/constants.ts b/packages/editor/src/core/plugins/image/constants.ts deleted file mode 100644 index 72fae6710..000000000 --- a/packages/editor/src/core/plugins/image/constants.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { PluginKey } from "@tiptap/pm/state"; - -export const uploadKey = new PluginKey("upload-image"); -export const deleteKey = new PluginKey("delete-image"); -export const restoreKey = new PluginKey("restore-image"); - -export const IMAGE_NODE_TYPE = "image"; diff --git a/packages/editor/src/core/plugins/image/delete-image.ts b/packages/editor/src/core/plugins/image/delete-image.ts index bcede7707..459d9fd70 100644 --- a/packages/editor/src/core/plugins/image/delete-image.ts +++ b/packages/editor/src/core/plugins/image/delete-image.ts @@ -37,20 +37,16 @@ export const TrackImageDeletionPlugin = (editor: Editor, deleteImage: DeleteImag removedImages.forEach(async (node) => { const src = node.attrs.src; - editor.storage[nodeType].deletedImageSet.set(src, true); - await onNodeDeleted(src, deleteImage); + editor.storage[nodeType].deletedImageSet?.set(src, true); + if (!src) return; + try { + await deleteImage(src); + } catch (error) { + console.error("Error deleting image:", error); + } }); }); return null; }, }); - -async function onNodeDeleted(src: string, deleteImage: DeleteImage): Promise { - if (!src) return; - try { - await deleteImage(src); - } catch (error) { - console.error("Error deleting image: ", error); - } -} diff --git a/packages/editor/src/core/plugins/image/index.ts b/packages/editor/src/core/plugins/image/index.ts index dfb787873..c0dc631c5 100644 --- a/packages/editor/src/core/plugins/image/index.ts +++ b/packages/editor/src/core/plugins/image/index.ts @@ -1,5 +1,3 @@ export * from "./types"; -export * from "./utils"; -export * from "./constants"; export * from "./delete-image"; export * from "./restore-image"; diff --git a/packages/editor/src/core/plugins/image/utils/index.ts b/packages/editor/src/core/plugins/image/utils/index.ts deleted file mode 100644 index 08d377a83..000000000 --- a/packages/editor/src/core/plugins/image/utils/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./validate-file"; diff --git a/packages/utils/src/attachment.ts b/packages/utils/src/attachment.ts new file mode 100644 index 000000000..1f9f4f5a3 --- /dev/null +++ b/packages/utils/src/attachment.ts @@ -0,0 +1,32 @@ +export const generateFileName = (fileName: string) => { + const date = new Date(); + const timestamp = date.getTime(); + + const _fileName = getFileName(fileName); + const nameWithoutExtension = _fileName.length > 80 ? _fileName.substring(0, 80) : _fileName; + const extension = getFileExtension(fileName); + + return `${nameWithoutExtension}-${timestamp}.${extension}`; +}; + +export const getFileExtension = (filename: string) => filename.slice(((filename.lastIndexOf(".") - 1) >>> 0) + 2); + +export const getFileName = (fileName: string) => { + const dotIndex = fileName.lastIndexOf("."); + + const nameWithoutExtension = fileName.substring(0, dotIndex); + + return nameWithoutExtension; +}; + +export const convertBytesToSize = (bytes: number) => { + let size; + + if (bytes < 1024 * 1024) { + size = Math.round(bytes / 1024) + " KB"; + } else { + size = Math.round(bytes / (1024 * 1024)) + " MB"; + } + + return size; +}; diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 765dce49d..495d065df 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -1,4 +1,5 @@ export * from "./array"; +export * from "./attachment"; export * from "./auth"; export * from "./datetime"; export * from "./color"; @@ -16,4 +17,3 @@ export * from "./work-item"; export * from "./get-icon-for-link"; export * from "./subscription"; - From 8613a80b16f8b82a76622cde79971ec7f3767d44 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Mon, 12 May 2025 19:13:39 +0530 Subject: [PATCH 044/201] [WEB-3523] feat: start of week preference (#7033) * chore: startOfWeek constant and types updated * chore: startOfWeek updated in profile store * chore: StartOfWeekPreference added to profile appearance settings * chore: calendar layout startOfWeek implementation * chore: date picker startOfWeek implementation * chore: gantt layout startOfWeek implementation * chore: code refactor * chore: code refactor * chore: code refactor --- packages/constants/src/profile.ts | 50 +++++++++++++++++ packages/types/src/users.d.ts | 11 +--- packages/ui/src/calendar.tsx | 1 + space/core/store/profile.store.ts | 2 + web/app/profile/appearance/page.tsx | 7 ++- web/core/components/dropdowns/date.tsx | 11 +++- .../components/gantt-chart/chart/root.tsx | 7 ++- web/core/components/gantt-chart/data/index.ts | 6 ++ .../components/gantt-chart/views/week-view.ts | 33 +++++++---- .../issue-layouts/calendar/week-days.tsx | 28 ++++++++-- .../issue-layouts/calendar/week-header.tsx | 18 ++++-- web/core/components/profile/index.ts | 1 + .../profile/start-of-week-preference.tsx | 55 +++++++++++++++++++ web/core/constants/calendar.ts | 9 +++ .../store/issue/issue_calendar_view.store.ts | 24 +++++++- web/core/store/user/profile.store.ts | 4 +- web/helpers/calendar.helper.ts | 20 +++++++ 17 files changed, 251 insertions(+), 36 deletions(-) create mode 100644 web/core/components/profile/start-of-week-preference.tsx diff --git a/packages/constants/src/profile.ts b/packages/constants/src/profile.ts index f7765a0cf..032e4526a 100644 --- a/packages/constants/src/profile.ts +++ b/packages/constants/src/profile.ts @@ -71,3 +71,53 @@ export const PROFILE_ADMINS_TAB = [ selected: "/activity/", }, ]; + +/** + * @description The start of the week for the user + * @enum {number} + */ +export enum EStartOfTheWeek { + SUNDAY = 0, + MONDAY = 1, + TUESDAY = 2, + WEDNESDAY = 3, + THURSDAY = 4, + FRIDAY = 5, + SATURDAY = 6, +} + +/** + * @description The options for the start of the week + * @type {Array<{value: EStartOfTheWeek, label: string}>} + * @constant + */ +export const START_OF_THE_WEEK_OPTIONS = [ + { + value: EStartOfTheWeek.SUNDAY, + label: "Sunday", + }, + { + value: EStartOfTheWeek.MONDAY, + label: "Monday", + }, + { + value: EStartOfTheWeek.TUESDAY, + label: "Tuesday", + }, + { + value: EStartOfTheWeek.WEDNESDAY, + label: "Wednesday", + }, + { + value: EStartOfTheWeek.THURSDAY, + label: "Thursday", + }, + { + value: EStartOfTheWeek.FRIDAY, + label: "Friday", + }, + { + value: EStartOfTheWeek.SATURDAY, + label: "Saturday", + }, +]; diff --git a/packages/types/src/users.d.ts b/packages/types/src/users.d.ts index e5140fdef..9f6ac4905 100644 --- a/packages/types/src/users.d.ts +++ b/packages/types/src/users.d.ts @@ -1,3 +1,4 @@ +import { EStartOfTheWeek } from "@plane/constants"; import { IIssueActivity, TIssuePriorities, TStateGroups } from "."; import { TUserPermissions } from "./enums"; @@ -64,6 +65,7 @@ export type TUserProfile = { language: string; created_at: Date | string; updated_at: Date | string; + start_of_the_week: EStartOfTheWeek; }; export interface IInstanceAdminStatus { @@ -155,14 +157,7 @@ export interface IUserProfileProjectSegregation { id: string; pending_issues: number; }[]; - user_data: Pick< - IUser, - | "avatar_url" - | "cover_image_url" - | "display_name" - | "first_name" - | "last_name" - > & { + user_data: Pick & { date_joined: Date; user_timezone: string; }; diff --git a/packages/ui/src/calendar.tsx b/packages/ui/src/calendar.tsx index 9fdbab176..80b160cc1 100644 --- a/packages/ui/src/calendar.tsx +++ b/packages/ui/src/calendar.tsx @@ -17,6 +17,7 @@ export const Calendar = ({ className, classNames, showOutsideDays = true, ...pro { const { t } = useTranslation(); const { setTheme } = useTheme(); @@ -75,6 +75,7 @@ const ProfileAppearancePage = observer(() => {
{userProfile?.theme?.theme === "custom" && } + ) : (
diff --git a/web/core/components/dropdowns/date.tsx b/web/core/components/dropdowns/date.tsx index 83a5e7b36..684d6f3ef 100644 --- a/web/core/components/dropdowns/date.tsx +++ b/web/core/components/dropdowns/date.tsx @@ -1,15 +1,18 @@ import React, { useRef, useState } from "react"; +import { observer } from "mobx-react"; import { Matcher } from "react-day-picker"; import { createPortal } from "react-dom"; import { usePopper } from "react-popper"; import { CalendarDays, X } from "lucide-react"; import { Combobox } from "@headlessui/react"; // ui +import { EStartOfTheWeek } from "@plane/constants"; import { ComboDropDown, Calendar } from "@plane/ui"; // helpers import { cn } from "@/helpers/common.helper"; import { renderFormattedDate, getDate } from "@/helpers/date-time.helper"; // hooks +import { useUserProfile } from "@/hooks/store"; import { useDropdown } from "@/hooks/use-dropdown"; // components import { DropdownButton } from "./buttons"; @@ -33,7 +36,7 @@ type Props = TDropdownProps & { renderByDefault?: boolean; }; -export const DateDropdown: React.FC = (props) => { +export const DateDropdown: React.FC = observer((props) => { const { buttonClassName = "", buttonContainerClassName, @@ -62,6 +65,9 @@ export const DateDropdown: React.FC = (props) => { const [isOpen, setIsOpen] = useState(false); // refs const dropdownRef = useRef(null); + // hooks + const { data } = useUserProfile(); + const startOfWeek = data?.start_of_the_week; // popper-js refs const [referenceElement, setReferenceElement] = useState(null); const [popperElement, setPopperElement] = useState(null); @@ -186,6 +192,7 @@ export const DateDropdown: React.FC = (props) => { disabled={disabledDays} mode="single" fixedWeeks + weekStartsOn={startOfWeek} />
, @@ -193,4 +200,4 @@ export const DateDropdown: React.FC = (props) => { )} ); -}; +}); diff --git a/web/core/components/gantt-chart/chart/root.tsx b/web/core/components/gantt-chart/chart/root.tsx index 03e63e7a5..16604d881 100644 --- a/web/core/components/gantt-chart/chart/root.tsx +++ b/web/core/components/gantt-chart/chart/root.tsx @@ -1,10 +1,13 @@ import { FC, useEffect, useState } from "react"; import { observer } from "mobx-react"; +// plane imports +import { EStartOfTheWeek } from "@plane/constants"; // components import { GanttChartHeader, GanttChartMainContent } from "@/components/gantt-chart"; // helpers import { cn } from "@/helpers/common.helper"; // hooks +import { useUserProfile } from "@/hooks/store"; import { useTimeLineChartStore } from "@/hooks/use-timeline-chart"; // import { SIDEBAR_WIDTH } from "../constants"; @@ -87,6 +90,8 @@ export const ChartViewRoot: FC = observer((props) => { updateRenderView, updateAllBlocksOnChartChangeWhileDragging, } = useTimeLineChartStore(); + const { data } = useUserProfile(); + const startOfWeek = data?.start_of_the_week; const updateCurrentViewRenderPayload = (side: null | "left" | "right", view: TGanttViews, targetDate?: Date) => { const selectedCurrentView: TGanttViews = view; @@ -98,7 +103,7 @@ export const ChartViewRoot: FC = observer((props) => { if (selectedCurrentViewData === undefined) return; const currentViewHelpers = timelineViewHelpers[selectedCurrentView]; - const currentRender = currentViewHelpers.generateChart(selectedCurrentViewData, side, targetDate); + const currentRender = currentViewHelpers.generateChart(selectedCurrentViewData, side, targetDate, startOfWeek); const mergeRenderPayloads = currentViewHelpers.mergeRenderPayloads as ( a: IWeekBlock[] | IMonthView | IMonthBlock[], b: IWeekBlock[] | IMonthView | IMonthBlock[] diff --git a/web/core/components/gantt-chart/data/index.ts b/web/core/components/gantt-chart/data/index.ts index 6db8dda65..2e72810d8 100644 --- a/web/core/components/gantt-chart/data/index.ts +++ b/web/core/components/gantt-chart/data/index.ts @@ -1,7 +1,13 @@ // types +import { EStartOfTheWeek } from "@plane/constants"; import { WeekMonthDataType, ChartDataType, TGanttViews } from "../types"; // constants +export const generateWeeks = (startOfWeek: EStartOfTheWeek = EStartOfTheWeek.SUNDAY): WeekMonthDataType[] => [ + ...weeks.slice(startOfWeek), + ...weeks.slice(0, startOfWeek), +]; + export const weeks: WeekMonthDataType[] = [ { key: 0, shortTitle: "sun", title: "sunday", abbreviation: "Su" }, { key: 1, shortTitle: "mon", title: "monday", abbreviation: "M" }, diff --git a/web/core/components/gantt-chart/views/week-view.ts b/web/core/components/gantt-chart/views/week-view.ts index 65915274c..ea3d75f91 100644 --- a/web/core/components/gantt-chart/views/week-view.ts +++ b/web/core/components/gantt-chart/views/week-view.ts @@ -1,5 +1,6 @@ // -import { weeks, months } from "../data"; +import { EStartOfTheWeek } from "@plane/constants"; +import { months, generateWeeks } from "../data"; import { ChartDataType } from "../types"; import { getNumberOfDaysBetweenTwoDates, getWeekNumberByDate } from "./helpers"; export interface IDayBlock { @@ -38,7 +39,12 @@ export interface IWeekBlock { * @param side * @returns */ -const generateWeekChart = (weekPayload: ChartDataType, side: null | "left" | "right", targetDate?: Date) => { +const generateWeekChart = ( + weekPayload: ChartDataType, + side: null | "left" | "right", + targetDate?: Date, + startOfWeek: EStartOfTheWeek = EStartOfTheWeek.SUNDAY +) => { let renderState = weekPayload; const range: number = renderState.data.approxFilterRange || 6; @@ -56,7 +62,7 @@ const generateWeekChart = (weekPayload: ChartDataType, side: null | "left" | "ri minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - range, currentDate.getDate()); plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + range, currentDate.getDate()); - if (minusDate && plusDate) filteredDates = getWeeksBetweenTwoDates(minusDate, plusDate); + if (minusDate && plusDate) filteredDates = getWeeksBetweenTwoDates(minusDate, plusDate, true, startOfWeek); startDate = filteredDates[0].startDate; endDate = filteredDates[filteredDates.length - 1].endDate; @@ -77,7 +83,7 @@ const generateWeekChart = (weekPayload: ChartDataType, side: null | "left" | "ri minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - range, 1); plusDate = new Date(chartStartDate.getFullYear(), chartStartDate.getMonth(), chartStartDate.getDate() - 1); - if (minusDate && plusDate) filteredDates = getWeeksBetweenTwoDates(minusDate, plusDate); + if (minusDate && plusDate) filteredDates = getWeeksBetweenTwoDates(minusDate, plusDate, true, startOfWeek); startDate = filteredDates[0].startDate; endDate = new Date(chartStartDate.getFullYear(), chartStartDate.getMonth(), chartStartDate.getDate() - 1); @@ -94,7 +100,7 @@ const generateWeekChart = (weekPayload: ChartDataType, side: null | "left" | "ri minusDate = new Date(chartEndDate.getFullYear(), chartEndDate.getMonth(), chartEndDate.getDate() + 1); plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + range, 1); - if (minusDate && plusDate) filteredDates = getWeeksBetweenTwoDates(minusDate, plusDate); + if (minusDate && plusDate) filteredDates = getWeeksBetweenTwoDates(minusDate, plusDate, true, startOfWeek); startDate = new Date(chartEndDate.getFullYear(), chartEndDate.getMonth(), chartEndDate.getDate() + 1); endDate = filteredDates[filteredDates.length - 1].endDate; @@ -120,14 +126,18 @@ const generateWeekChart = (weekPayload: ChartDataType, side: null | "left" | "ri export const getWeeksBetweenTwoDates = ( startDate: Date, endDate: Date, - shouldPopulateDaysForWeek: boolean = true + shouldPopulateDaysForWeek: boolean = true, + startOfWeek: EStartOfTheWeek = EStartOfTheWeek.SUNDAY ): IWeekBlock[] => { const weeks: IWeekBlock[] = []; const currentDate = new Date(startDate.getTime()); const today = new Date(); - currentDate.setDate(currentDate.getDate() - currentDate.getDay()); + // Adjust the current date to the start of the week + const day = currentDate.getDay(); + const diff = (day + 7 - startOfWeek) % 7; // Calculate days to subtract to get to startOfWeek + currentDate.setDate(currentDate.getDate() - diff); while (currentDate <= endDate) { const weekStartDate = new Date(currentDate.getTime()); @@ -141,7 +151,7 @@ export const getWeeksBetweenTwoDates = ( const weekNumber = getWeekNumberByDate(currentDate); weeks.push({ - children: shouldPopulateDaysForWeek ? populateDaysForWeek(weekStartDate) : undefined, + children: shouldPopulateDaysForWeek ? populateDaysForWeek(weekStartDate, startOfWeek) : undefined, weekNumber, weekData: { shortTitle: `w${weekNumber}`, @@ -171,17 +181,18 @@ export const getWeeksBetweenTwoDates = ( * @param startDate * @returns */ -const populateDaysForWeek = (startDate: Date): IDayBlock[] => { +const populateDaysForWeek = (startDate: Date, startOfWeek: EStartOfTheWeek = EStartOfTheWeek.SUNDAY): IDayBlock[] => { const currentDate = new Date(startDate); const days: IDayBlock[] = []; const today = new Date(); + const weekDays = generateWeeks(startOfWeek); for (let i = 0; i < 7; i++) { days.push({ date: new Date(currentDate), day: currentDate.getDay(), - dayData: weeks[currentDate.getDay()], - title: `${weeks[currentDate.getDay()].abbreviation} ${currentDate.getDate()}`, + dayData: weekDays[i], + title: `${weekDays[i].abbreviation} ${currentDate.getDate()}`, today: today.setHours(0, 0, 0, 0) == currentDate.setHours(0, 0, 0, 0), }); currentDate.setDate(currentDate.getDate() + 1); diff --git a/web/core/components/issues/issue-layouts/calendar/week-days.tsx b/web/core/components/issues/issue-layouts/calendar/week-days.tsx index 9aaa13229..c5ba104ee 100644 --- a/web/core/components/issues/issue-layouts/calendar/week-days.tsx +++ b/web/core/components/issues/issue-layouts/calendar/week-days.tsx @@ -1,9 +1,14 @@ import { observer } from "mobx-react"; +import { EStartOfTheWeek } from "@plane/constants"; import { TGroupedIssues, TIssue, TIssueMap, TPaginationData } from "@plane/types"; +import { cn } from "@plane/utils"; // components import { CalendarDayTile } from "@/components/issues"; // helpers +import { getOrderedDays } from "@/helpers/calendar.helper"; import { renderFormattedPayloadDate } from "@/helpers/date-time.helper"; +// hooks +import { useUserProfile } from "@/hooks/store"; // types import { IProjectEpicsFilter } from "@/plane-web/store/issue/epic"; import { ICycleIssuesFilter } from "@/store/issue/cycle"; @@ -65,20 +70,33 @@ export const CalendarWeekDays: React.FC = observer((props) => { canEditProperties, isEpic = false, } = props; + // hooks + const { data } = useUserProfile(); + const startOfWeek = data?.start_of_the_week; const calendarLayout = issuesFilterStore?.issueFilters?.displayFilters?.calendar?.layout ?? "month"; const showWeekends = issuesFilterStore?.issueFilters?.displayFilters?.calendar?.show_weekends ?? false; if (!week) return null; + const shouldShowDay = (dayDate: Date) => { + if (showWeekends) return true; + const day = dayDate.getDay(); + return !(day === 0 || day === 6); + }; + + const sortedWeekDays = getOrderedDays(Object.values(week), (item) => item.date.getDay(), startOfWeek); + return (
- {Object.values(week).map((date: ICalendarDate) => { - if (!showWeekends && (date.date.getDay() === 0 || date.date.getDay() === 6)) return null; + {sortedWeekDays.map((date: ICalendarDate) => { + if (!shouldShowDay(date.date)) return null; return ( = observer((props) => { const { isLoading, showWeekends } = props; + // hooks + const { data } = useUserProfile(); + const startOfWeek = data?.start_of_the_week; + + // derived + const orderedDays = getOrderedDays(Object.values(DAYS_LIST), (item) => item.value, startOfWeek); return (
= observer((props) => { {isLoading && (
)} - {Object.values(DAYS_LIST).map((day) => { - if (!showWeekends && (day.shortTitle === "Sat" || day.shortTitle === "Sun")) return null; + {orderedDays.map((day) => { + if (!showWeekends && (day.value === EStartOfTheWeek.SUNDAY || day.value === EStartOfTheWeek.SATURDAY)) + return null; return (
+ START_OF_THE_WEEK_OPTIONS.find((option) => option.value === startOfWeek)?.label; + +export const StartOfWeekPreference = observer(() => { + // hooks + const { data: userProfile, updateUserProfile } = useUserProfile(); + + return ( +
+
+

First day of the week

+

This will change how all calendars in your app look.

+
+
+ { + updateUserProfile({ start_of_the_week: val }) + .then(() => { + setToast({ + type: TOAST_TYPE.SUCCESS, + title: "Success", + message: "First day of the week updated successfully", + }); + }) + .catch(() => { + setToast({ type: TOAST_TYPE.ERROR, title: "Update failed", message: "Please try again later." }); + }); + }} + input + maxHeight="lg" + > + <> + {START_OF_THE_WEEK_OPTIONS.map((day) => ( + + {day.label} + + ))} + + +
+
+ ); +}); diff --git a/web/core/constants/calendar.ts b/web/core/constants/calendar.ts index b2d0624c5..79db73ef7 100644 --- a/web/core/constants/calendar.ts +++ b/web/core/constants/calendar.ts @@ -1,3 +1,4 @@ +import { EStartOfTheWeek } from "@plane/constants"; import { TCalendarLayouts } from "@plane/types"; export const MONTHS_LIST: { @@ -60,35 +61,43 @@ export const DAYS_LIST: { [dayIndex: number]: { shortTitle: string; title: string; + value: EStartOfTheWeek; }; } = { 1: { shortTitle: "Sun", title: "Sunday", + value: EStartOfTheWeek.SUNDAY, }, 2: { shortTitle: "Mon", title: "Monday", + value: EStartOfTheWeek.MONDAY, }, 3: { shortTitle: "Tue", title: "Tuesday", + value: EStartOfTheWeek.TUESDAY, }, 4: { shortTitle: "Wed", title: "Wednesday", + value: EStartOfTheWeek.WEDNESDAY, }, 5: { shortTitle: "Thu", title: "Thursday", + value: EStartOfTheWeek.THURSDAY, }, 6: { shortTitle: "Fri", title: "Friday", + value: EStartOfTheWeek.FRIDAY, }, 7: { shortTitle: "Sat", title: "Saturday", + value: EStartOfTheWeek.SATURDAY, }, }; diff --git a/web/core/store/issue/issue_calendar_view.store.ts b/web/core/store/issue/issue_calendar_view.store.ts index c84fe956b..4757fb5b3 100644 --- a/web/core/store/issue/issue_calendar_view.store.ts +++ b/web/core/store/issue/issue_calendar_view.store.ts @@ -68,7 +68,29 @@ export class CalendarStore implements ICalendarStore { const { activeMonthDate } = this.calendarFilters; - return this.calendarPayload[`y-${activeMonthDate.getFullYear()}`][`m-${activeMonthDate.getMonth()}`]; + const year = activeMonthDate.getFullYear(); + const month = activeMonthDate.getMonth(); + + // Get the weeks for the current month + const weeks = this.calendarPayload[`y-${year}`][`m-${month}`]; + + // If no weeks exist, return undefined + if (!weeks) return undefined; + + // Create a new object to store the reordered weeks + const reorderedWeeks: { [weekNumber: string]: ICalendarWeek } = {}; + + // Get all week numbers and sort them + const weekNumbers = Object.keys(weeks).map((key) => parseInt(key.replace("w-", ""))); + weekNumbers.sort((a, b) => a - b); + + // Reorder weeks based on start_of_week + weekNumbers.forEach((weekNumber) => { + const weekKey = `w-${weekNumber}`; + reorderedWeeks[weekKey] = weeks[weekKey]; + }); + + return reorderedWeeks; } get activeWeekNumber() { diff --git a/web/core/store/user/profile.store.ts b/web/core/store/user/profile.store.ts index 08cbe1fc7..d5e796be6 100644 --- a/web/core/store/user/profile.store.ts +++ b/web/core/store/user/profile.store.ts @@ -2,6 +2,7 @@ import cloneDeep from "lodash/cloneDeep"; import set from "lodash/set"; import { action, makeObservable, observable, runInAction } from "mobx"; // types +import { EStartOfTheWeek } from "@plane/constants"; import { IUserTheme, TUserProfile } from "@plane/types"; // services import { UserService } from "@/services/user.service"; @@ -58,7 +59,8 @@ export class ProfileStore implements IUserProfileStore { has_billing_address: false, created_at: "", updated_at: "", - language: "" + language: "", + start_of_the_week: EStartOfTheWeek.SUNDAY, }; // services diff --git a/web/helpers/calendar.helper.ts b/web/helpers/calendar.helper.ts index 5b89d8625..709cf9c96 100644 --- a/web/helpers/calendar.helper.ts +++ b/web/helpers/calendar.helper.ts @@ -1,5 +1,7 @@ +import { EStartOfTheWeek } from "@plane/constants"; // helpers import { ICalendarDate, ICalendarPayload } from "@/components/issues"; +import { DAYS_LIST } from "@/constants/calendar"; import { getWeekNumberOfDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper"; // types @@ -92,3 +94,21 @@ export const generateCalendarData = (currentStructure: ICalendarPayload | null, return calendarData; }; + +/** + * Returns a new array sorted by the startOfWeek. + * @param items Array of items to sort. + * @param getDayIndex Function to get the day index (0-6) from an item. + * @param startOfWeek The day to start the week on. + */ +export function getOrderedDays( + items: T[], + getDayIndex: (item: T) => number, + startOfWeek: EStartOfTheWeek = EStartOfTheWeek.SUNDAY +): T[] { + return [...items].sort((a, b) => { + const dayA = (7 + getDayIndex(a) - startOfWeek) % 7; + const dayB = (7 + getDayIndex(b) - startOfWeek) % 7; + return dayA - dayB; + }); +} From 5f8d5ea388a6ee1897247f3e34368aafb61f88e3 Mon Sep 17 00:00:00 2001 From: Sangeetha Date: Mon, 12 May 2025 19:14:10 +0530 Subject: [PATCH 045/201] [WEB-4054] chore: search-issues endpoint code refactoring (#7029) * chore: moved some code to seperate function * fix: function name typo --- apiserver/plane/app/views/search/issue.py | 134 ++++++++++++++++------ 1 file changed, 99 insertions(+), 35 deletions(-) diff --git a/apiserver/plane/app/views/search/issue.py b/apiserver/plane/app/views/search/issue.py index 3db9e1cba..ed826782a 100644 --- a/apiserver/plane/app/views/search/issue.py +++ b/apiserver/plane/app/views/search/issue.py @@ -1,5 +1,5 @@ # Django imports -from django.db.models import Q +from django.db.models import Q, QuerySet # Third party imports from rest_framework import status @@ -12,6 +12,95 @@ from plane.utils.issue_search import search_issues class IssueSearchEndpoint(BaseAPIView): + def filter_issues_by_project(self, project_id: int, issues: QuerySet) -> QuerySet: + """ + Filter issues by project + """ + + issues = issues.filter(project_id=project_id) + + return issues + + def search_issues_by_query(self, query: str, issues: QuerySet) -> QuerySet: + """ + Search issues by query + """ + + issues = search_issues(query, issues) + + return issues + + def search_issues_and_excluding_parent( + self, issues: QuerySet, issue_id: str + ) -> QuerySet: + """ + Search issues and epics by query excluding the parent + """ + + issue = Issue.issue_objects.filter(pk=issue_id).first() + if issue: + issues = issues.filter( + ~Q(pk=issue_id), ~Q(pk=issue.parent_id), ~Q(parent_id=issue_id) + ) + return issues + + def filter_issues_excluding_related_issues( + self, issue_id: str, issues: QuerySet + ) -> QuerySet: + """ + Filter issues excluding related issues + """ + + issue = Issue.issue_objects.filter(pk=issue_id).first() + related_issue_ids = ( + IssueRelation.objects.filter(Q(related_issue=issue) | Q(issue=issue)) + .values_list("issue_id", "related_issue_id") + .distinct() + ) + + related_issue_ids = [item for sublist in related_issue_ids for item in sublist] + + if issue: + issues = issues.filter(~Q(pk=issue_id), ~Q(pk__in=related_issue_ids)) + + return issues + + def filter_root_issues_only(self, issue_id: str, issues: QuerySet) -> QuerySet: + """ + Filter root issues only + """ + issue = Issue.issue_objects.filter(pk=issue_id).first() + if issue: + issues = issues.filter(~Q(pk=issue_id), parent__isnull=True) + if issue.parent: + issues = issues.filter(~Q(pk=issue.parent_id)) + return issues + + def exclude_issues_in_cycles(self, issues: QuerySet) -> QuerySet: + """ + Exclude issues in cycles + """ + issues = issues.exclude( + Q(issue_cycle__isnull=False) & Q(issue_cycle__deleted_at__isnull=True) + ) + return issues + + def exclude_issues_in_module(self, issues: QuerySet, module: str) -> QuerySet: + """ + Exclude issues in a module + """ + issues = issues.exclude( + Q(issue_module__module=module) & Q(issue_module__deleted_at__isnull=True) + ) + return issues + + def filter_issues_without_target_date(self, issues: QuerySet) -> QuerySet: + """ + Filter issues without a target date + """ + issues = issues.filter(target_date__isnull=True) + return issues + def get(self, request, slug, project_id): query = request.query_params.get("search", False) workspace_search = request.query_params.get("workspace_search", "false") @@ -21,7 +110,6 @@ class IssueSearchEndpoint(BaseAPIView): module = request.query_params.get("module", False) sub_issue = request.query_params.get("sub_issue", "false") target_date = request.query_params.get("target_date", True) - issue_id = request.query_params.get("issue_id", False) issues = Issue.issue_objects.filter( @@ -32,52 +120,28 @@ class IssueSearchEndpoint(BaseAPIView): ) if workspace_search == "false": - issues = issues.filter(project_id=project_id) + issues = self.filter_issues_by_project(project_id, issues) if query: - issues = search_issues(query, issues) + issues = self.search_issues_by_query(query, issues) if parent == "true" and issue_id: - issue = Issue.issue_objects.filter(pk=issue_id).first() - if issue: - issues = issues.filter( - ~Q(pk=issue_id), ~Q(pk=issue.parent_id), ~Q(parent_id=issue_id) - ) + issues = self.search_issues_and_excluding_parent(issues, issue_id) + if issue_relation == "true" and issue_id: - issue = Issue.issue_objects.filter(pk=issue_id).first() - related_issue_ids = IssueRelation.objects.filter( - Q(related_issue=issue) | Q(issue=issue) - ).values_list( - "issue_id", "related_issue_id" - ).distinct() + issues = self.filter_issues_excluding_related_issues(issue_id, issues) - related_issue_ids = [item for sublist in related_issue_ids for item in sublist] - - if issue: - issues = issues.filter( - ~Q(pk=issue_id), - ~Q(pk__in=related_issue_ids), - ) if sub_issue == "true" and issue_id: - issue = Issue.issue_objects.filter(pk=issue_id).first() - if issue: - issues = issues.filter(~Q(pk=issue_id), parent__isnull=True) - if issue.parent: - issues = issues.filter(~Q(pk=issue.parent_id)) + issues = self.filter_root_issues_only(issue_id, issues) if cycle == "true": - issues = issues.exclude( - Q(issue_cycle__isnull=False) & Q(issue_cycle__deleted_at__isnull=True) - ) + issues = self.exclude_issues_in_cycles(issues) if module: - issues = issues.exclude( - Q(issue_module__module=module) - & Q(issue_module__deleted_at__isnull=True) - ) + issues = self.exclude_issues_in_module(issues, module) if target_date == "none": - issues = issues.filter(target_date__isnull=True) + issues = self.filter_issues_without_target_date(issues) if ProjectMember.objects.filter( project_id=project_id, member=self.request.user, is_active=True, role=5 From 079c3a3a990cda682f1f4b63f43dc3ed3192aca2 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Mon, 12 May 2025 19:15:39 +0530 Subject: [PATCH 046/201] [WEB-3978] chore: cmd k search result redirection improvements (#7012) * fix: work item tab highlight * chore: projectListOpen state and toggle method added to command palette store * chore: openProjectAndScrollToSidebar helper function and highlight keyframes added * chore: SidebarProjectsListItem updated * chore: openProjectAndScrollToSidebar implementation * chore: code refactor * chore: code refactor * chore: code refactor * chore: code refactor * chore: code refactor * chore: code refactor * chore: code refactor --- .../command-palette/actions/helper.tsx | 25 +++++++++++++++++++ .../actions/search-results.tsx | 13 ++++++++-- .../workspace/sidebar/projects-list-item.tsx | 9 ++++--- web/core/store/base-command-palette.store.ts | 19 ++++++++++++++ web/styles/globals.css | 11 ++++++++ 5 files changed, 72 insertions(+), 5 deletions(-) create mode 100644 web/core/components/command-palette/actions/helper.tsx diff --git a/web/core/components/command-palette/actions/helper.tsx b/web/core/components/command-palette/actions/helper.tsx new file mode 100644 index 000000000..5156cadf7 --- /dev/null +++ b/web/core/components/command-palette/actions/helper.tsx @@ -0,0 +1,25 @@ +import { store } from "@/lib/store-context"; + +export const openProjectAndScrollToSidebar = (itemProjectId: string | undefined) => { + if (!itemProjectId) { + console.warn("No project id provided. Cannot open project and scroll to sidebar."); + return; + } + // open the project list + store.commandPalette.toggleProjectListOpen(itemProjectId, true); + // scroll to the element + const scrollElementId = `sidebar-${itemProjectId}-JOINED`; + const scrollElement = document.getElementById(scrollElementId); + // if the element exists, scroll to it + if (scrollElement) { + setTimeout(() => { + scrollElement.scrollIntoView({ behavior: "smooth", block: "start" }); + // Restart the highlight animation every time + scrollElement.style.animation = "none"; + // Trigger a reflow to ensure the animation is restarted + void scrollElement.offsetWidth; + // Restart the highlight animation + scrollElement.style.animation = "highlight 2s ease-in-out"; + }); + } +}; diff --git a/web/core/components/command-palette/actions/search-results.tsx b/web/core/components/command-palette/actions/search-results.tsx index 18a8a75be..a33d85ff7 100644 --- a/web/core/components/command-palette/actions/search-results.tsx +++ b/web/core/components/command-palette/actions/search-results.tsx @@ -1,6 +1,7 @@ "use client"; import { Command } from "cmdk"; +import { observer } from "mobx-react"; import { useParams } from "next/navigation"; // plane imports import { IWorkspaceSearchResults } from "@plane/types"; @@ -8,13 +9,15 @@ import { IWorkspaceSearchResults } from "@plane/types"; import { useAppRouter } from "@/hooks/use-app-router"; // plane web imports import { commandGroups } from "@/plane-web/components/command-palette"; +// helpers +import { openProjectAndScrollToSidebar } from "./helper"; type Props = { closePalette: () => void; results: IWorkspaceSearchResults; }; -export const CommandPaletteSearchResults: React.FC = (props) => { +export const CommandPaletteSearchResults: React.FC = observer((props) => { const { closePalette, results } = props; // router const router = useAppRouter(); @@ -38,6 +41,12 @@ export const CommandPaletteSearchResults: React.FC = (props) => { onSelect={() => { closePalette(); router.push(currentSection.path(item, projectId)); + const itemProjectId = + item?.project_id || + (Array.isArray(item?.project_ids) && item?.project_ids?.length > 0 + ? item?.project_ids[0] + : undefined); + if (itemProjectId) openProjectAndScrollToSidebar(itemProjectId); }} value={`${key}-${item?.id}-${item.name}-${item.project__identifier ?? ""}-${item.sequence_id ?? ""}`} className="focus:outline-none" @@ -54,4 +63,4 @@ export const CommandPaletteSearchResults: React.FC = (props) => { })} ); -}; +}); diff --git a/web/core/components/workspace/sidebar/projects-list-item.tsx b/web/core/components/workspace/sidebar/projects-list-item.tsx index 978fe0ba0..75b10aa4c 100644 --- a/web/core/components/workspace/sidebar/projects-list-item.tsx +++ b/web/core/components/workspace/sidebar/projects-list-item.tsx @@ -24,7 +24,7 @@ import { LeaveProjectModal, PublishProjectModal } from "@/components/project"; // helpers import { cn } from "@/helpers/common.helper"; // hooks -import { useAppTheme, useEventTracker, useProject, useUserPermissions } from "@/hooks/store"; +import { useAppTheme, useCommandPalette, useEventTracker, useProject, useUserPermissions } from "@/hooks/store"; import { usePlatformOS } from "@/hooks/use-platform-os"; // plane-web components import { ProjectNavigationRoot } from "@/plane-web/components/sidebar"; @@ -64,12 +64,13 @@ export const SidebarProjectsListItem: React.FC = observer((props) => { const { getPartialProjectById } = useProject(); const { isMobile } = usePlatformOS(); const { allowPermissions } = useUserPermissions(); + const { getIsProjectListOpen, toggleProjectListOpen } = useCommandPalette(); // states const [leaveProjectModalOpen, setLeaveProjectModal] = useState(false); const [publishModalOpen, setPublishModal] = useState(false); const [isMenuActive, setIsMenuActive] = useState(false); const [isDragging, setIsDragging] = useState(false); - const [isProjectListOpen, setIsProjectListOpen] = useState(false); + const isProjectListOpen = getIsProjectListOpen(projectId); const [instruction, setInstruction] = useState<"DRAG_OVER" | "DRAG_BELOW" | undefined>(undefined); // refs const actionSectionRef = useRef(null); @@ -79,6 +80,8 @@ export const SidebarProjectsListItem: React.FC = observer((props) => { const { workspaceSlug, projectId: URLProjectId } = useParams(); // derived values const project = getPartialProjectById(projectId); + // toggle project list open + const setIsProjectListOpen = (value: boolean) => toggleProjectListOpen(projectId, value); // auth const isAdmin = allowPermissions( [EUserPermissions.ADMIN], @@ -198,7 +201,7 @@ export const SidebarProjectsListItem: React.FC = observer((props) => { if (URLProjectId === project.id) setIsProjectListOpen(true); }, [URLProjectId]); - const handleItemClick = () => setIsProjectListOpen((prev) => !prev); + const handleItemClick = () => setIsProjectListOpen(!isProjectListOpen); return ( <> setPublishModal(false)} /> diff --git a/web/core/store/base-command-palette.store.ts b/web/core/store/base-command-palette.store.ts index aaf1170c0..7024daf4d 100644 --- a/web/core/store/base-command-palette.store.ts +++ b/web/core/store/base-command-palette.store.ts @@ -1,4 +1,5 @@ import { observable, action, makeObservable } from "mobx"; +import { computedFn } from "mobx-utils"; import { EIssuesStoreType, TCreateModalStoreTypes, @@ -26,6 +27,8 @@ export interface IBaseCommandPaletteStore { isBulkDeleteIssueModalOpen: boolean; createIssueStoreType: TCreateModalStoreTypes; allStickiesModal: boolean; + projectListOpenMap: Record; + getIsProjectListOpen: (projectId: string) => boolean; // toggle actions toggleCommandPaletteModal: (value?: boolean) => void; toggleShortcutModal: (value?: boolean) => void; @@ -38,6 +41,7 @@ export interface IBaseCommandPaletteStore { toggleDeleteIssueModal: (value?: boolean) => void; toggleBulkDeleteIssueModal: (value?: boolean) => void; toggleAllStickiesModal: (value?: boolean) => void; + toggleProjectListOpen: (projectId: string, value?: boolean) => void; } export abstract class BaseCommandPaletteStore implements IBaseCommandPaletteStore { @@ -54,6 +58,7 @@ export abstract class BaseCommandPaletteStore implements IBaseCommandPaletteStor createPageModal: TCreatePageModal = DEFAULT_CREATE_PAGE_MODAL_DATA; createIssueStoreType: TCreateModalStoreTypes = EIssuesStoreType.PROJECT; allStickiesModal: boolean = false; + projectListOpenMap: Record = {}; constructor() { makeObservable(this, { @@ -70,6 +75,7 @@ export abstract class BaseCommandPaletteStore implements IBaseCommandPaletteStor createPageModal: observable, createIssueStoreType: observable, allStickiesModal: observable, + projectListOpenMap: observable, // projectPages: computed, // toggle actions toggleCommandPaletteModal: action, @@ -83,6 +89,7 @@ export abstract class BaseCommandPaletteStore implements IBaseCommandPaletteStor toggleDeleteIssueModal: action, toggleBulkDeleteIssueModal: action, toggleAllStickiesModal: action, + toggleProjectListOpen: action, }); } @@ -104,6 +111,18 @@ export abstract class BaseCommandPaletteStore implements IBaseCommandPaletteStor this.allStickiesModal ); } + // computedFn + getIsProjectListOpen = computedFn((projectId: string) => this.projectListOpenMap[projectId]); + + /** + * Toggles the project list open state + * @param projectId + * @param value + */ + toggleProjectListOpen = (projectId: string, value?: boolean) => { + if (value !== undefined) this.projectListOpenMap[projectId] = value; + else this.projectListOpenMap[projectId] = !this.projectListOpenMap[projectId]; + }; /** * Toggles the command palette modal diff --git a/web/styles/globals.css b/web/styles/globals.css index ff71ba5ac..c6e4654d0 100644 --- a/web/styles/globals.css +++ b/web/styles/globals.css @@ -942,3 +942,14 @@ div.web-view-spinner div.bar12 { .animate-fade-out { animation: fadeOut 500ms ease-in 100ms forwards; } + +@keyframes highlight { + 0% { + background-color: rgba(var(--color-background-90), 1); + border-radius: 4px; + } + 100% { + background-color: transparent; + border-radius: 4px; + } +} From 0d5c7c66531f59116fcc59115f7a3b953abbdc3c Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Mon, 12 May 2025 19:47:44 +0530 Subject: [PATCH 047/201] [WEB-4051] regression: update font size of comment editor #7048 --- .../issues/peek-overview/comment/comment-detail-card.tsx | 3 +++ web/core/components/comments/comment-card.tsx | 6 ++++++ web/core/components/comments/comment-create.tsx | 3 +++ .../sidebar/notification-card/content.tsx | 3 +++ 4 files changed, 15 insertions(+) diff --git a/space/core/components/issues/peek-overview/comment/comment-detail-card.tsx b/space/core/components/issues/peek-overview/comment/comment-detail-card.tsx index 70fcedd0a..56a71be5a 100644 --- a/space/core/components/issues/peek-overview/comment/comment-detail-card.tsx +++ b/space/core/components/issues/peek-overview/comment/comment-detail-card.tsx @@ -144,6 +144,9 @@ export const CommentCard: React.FC = observer((props) => { ref={showEditorRef} id={comment.id} initialValue={comment.comment_html} + displayConfig={{ + fontSize: "small-font", + }} />
diff --git a/web/core/components/comments/comment-card.tsx b/web/core/components/comments/comment-card.tsx index 698e1ddf3..1831af4a0 100644 --- a/web/core/components/comments/comment-card.tsx +++ b/web/core/components/comments/comment-card.tsx @@ -162,6 +162,9 @@ export const CommentCard: FC = observer((props) => { }} projectId={projectId?.toString() ?? ""} parentClassName="p-2" + displayConfig={{ + fontSize: "small-font", + }} />
@@ -209,6 +212,9 @@ export const CommentCard: FC = observer((props) => { workspaceSlug={workspaceSlug} containerClassName="!py-1" projectId={(projectId as string) ?? ""} + displayConfig={{ + fontSize: "small-font", + }} />
diff --git a/web/core/components/comments/comment-create.tsx b/web/core/components/comments/comment-create.tsx index dc74a9a12..aae88c6c1 100644 --- a/web/core/components/comments/comment-create.tsx +++ b/web/core/components/comments/comment-create.tsx @@ -137,6 +137,9 @@ export const CommentCreate: FC = observer((props) => { }} showToolbarInitially={showToolbarInitially} parentClassName="p-2" + displayConfig={{ + fontSize: "small-font", + }} /> )} /> diff --git a/web/core/components/workspace-notifications/sidebar/notification-card/content.tsx b/web/core/components/workspace-notifications/sidebar/notification-card/content.tsx index feb8c373c..7d58b3a8f 100644 --- a/web/core/components/workspace-notifications/sidebar/notification-card/content.tsx +++ b/web/core/components/workspace-notifications/sidebar/notification-card/content.tsx @@ -102,6 +102,9 @@ export const NotificationContent: FC<{ workspaceId={workspaceId} workspaceSlug={workspaceSlug} projectId={projectId} + displayConfig={{ + fontSize: "small-font", + }} />
)} From 75d81f9e957f14c1048e6f06e98c4199a94a316d Mon Sep 17 00:00:00 2001 From: JayashTripathy <76092296+JayashTripathy@users.noreply.github.com> Date: Mon, 12 May 2025 20:50:33 +0530 Subject: [PATCH 048/201] [WEB-3781] Analytics page enhancements (#7005) * chore: analytics endpoint * added anlytics v2 * updated status icons * added area chart in workitems and en translations * active projects * chore: created analytics chart * chore: validation errors * improved radar-chart , added empty states , added projects summary * chore: added a new graph in advance analytics * integrated priority chart * chore: added csv exporter * added priority dropdown * integrated created vs resolved chart * custom x and y axis label in bar and area chart * added wrapper styles to legends * added filter components * fixed temp data imports * integrated filters in priority charts * added label to priority chart and updated duration filter * refactor * reverted to void onchange * fixed some contant exports * fixed type issues * fixed some type and build issues * chore: updated the filtering logic for analytics * updated default value to last_30_days * percentage value whole number and added some rules for axis options * fixed some translations * added - custom tick for radar, calc of insight cards, filter labels * chore: opitmised the analytics endpoint * replace old analytics path with new , updated labels of insight card, done some store fixes * chore: updated the export request * Enhanced ProjectSelect to support multi-select, improved state management, and optimized data fetching and component structure. * fix: round completion percentage calculation in ActiveProjectItem * added empty states in project insights * Added loader and empty state in created/resolved chart * added loaders * added icons in filters * added custom colors in customised charts * cleaned up some code * added some responsiveness * updated translations * updated serrchbar for the table * added work item modal in project analytics * fixed some of the layput issues in the peek view * chore: updated the base function for viewsets * synced tab to url * code cleanup * chore: updated the export logic * fixed project_ids filter * added icon in projectdropdown * updated export button position * export csv and emptystates icons * refactor * code refactor * updated loaders, moved color pallete to contants, added nullish collasece operator in neccessary places * removed uneccessary cn * fixed formatting issues * fixed empty project_ids in payload * improved null checks * optimized charts * modified relevant variables to observable.ref * fixed the duration type * optimized some code * updated query key in project-insight * updated query key in project-insight * updated formatting * chore: replaced analytics route with new one and done some optimizations * removed the old analytics --------- Co-authored-by: NarayanBavisetti --- apiserver/plane/app/views/analytic/advance.py | 14 ++ packages/constants/src/analytics-v2/common.ts | 105 ++++++++ packages/constants/src/analytics-v2/index.ts | 1 + packages/constants/src/chart.ts | 155 ++++++++++++ packages/constants/src/index.ts | 1 + packages/constants/src/state.ts | 2 + .../i18n/src/locales/cs/translations.json | 33 ++- .../i18n/src/locales/de/translations.json | 33 ++- .../i18n/src/locales/en/translations.json | 34 ++- .../i18n/src/locales/es/translations.json | 33 ++- .../i18n/src/locales/fr/translations.json | 33 ++- .../i18n/src/locales/id/translations.json | 33 ++- .../i18n/src/locales/it/translations.json | 33 ++- .../i18n/src/locales/ja/translations.json | 33 ++- .../i18n/src/locales/ko/translations.json | 33 ++- .../i18n/src/locales/pl/translations.json | 33 ++- .../i18n/src/locales/pt-BR/translations.json | 33 ++- .../i18n/src/locales/ro/translations.json | 33 ++- .../i18n/src/locales/ru/translations.json | 33 ++- .../i18n/src/locales/sk/translations.json | 33 ++- .../i18n/src/locales/tr-TR/translations.json | 33 ++- .../i18n/src/locales/ua/translations.json | 33 ++- .../i18n/src/locales/vi-VN/translations.json | 33 ++- .../i18n/src/locales/zh-CN/translations.json | 33 ++- .../i18n/src/locales/zh-TW/translations.json | 33 ++- packages/propel/package.json | 4 +- .../propel/src/charts/area-chart/root.tsx | 27 +- packages/propel/src/charts/bar-chart/root.tsx | 23 +- .../propel/src/charts/components/legend.tsx | 23 +- .../propel/src/charts/components/tick.tsx | 28 ++- .../propel/src/charts/line-chart/root.tsx | 20 +- .../propel/src/charts/radar-chart/index.ts | 1 + .../propel/src/charts/radar-chart/root.tsx | 95 ++++++++ .../propel/src/charts/scatter-chart/index.ts | 1 + .../propel/src/charts/scatter-chart/root.tsx | 155 ++++++++++++ packages/propel/src/table/core.tsx | 120 +++++++++ packages/propel/src/table/index.ts | 1 + packages/types/src/analytics-v2.d.ts | 52 ++++ packages/types/src/charts/common.d.ts | 16 ++ .../src/{charts.d.ts => charts/index.d.ts} | 80 +++++- packages/types/src/enums.ts | 6 + packages/types/src/index.d.ts | 1 + packages/ui/src/icons/at-risk-icon.tsx | 21 +- packages/ui/src/icons/off-track-icon.tsx | 21 +- packages/ui/src/icons/on-track-icon.tsx | 36 ++- .../(projects)/analytics/layout.tsx | 3 +- .../(projects)/analytics/page.tsx | 72 +++--- .../issues/(list)/mobile-header.tsx | 4 +- web/app/page.tsx | 1 - web/ce/components/analytics-v2/tabs.ts | 11 + .../analytics-v2/analytics-filter-actions.tsx | 34 +++ .../analytics-section-wrapper.tsx | 30 +++ .../analytics-v2/analytics-wrapper.tsx | 22 ++ .../components/analytics-v2/empty-state.tsx | 48 ++++ web/core/components/analytics-v2/index.ts | 1 + .../components/analytics-v2/insight-card.tsx | 47 ++++ .../analytics-v2/insight-table/data-table.tsx | 177 ++++++++++++++ .../analytics-v2/insight-table/index.ts | 1 + .../analytics-v2/insight-table/loader.tsx | 34 +++ .../analytics-v2/insight-table/root.tsx | 74 ++++++ web/core/components/analytics-v2/loaders.tsx | 23 ++ .../overview/active-project-item.tsx | 57 +++++ .../analytics-v2/overview/active-projects.tsx | 44 ++++ .../components/analytics-v2/overview/index.ts | 1 + .../overview/project-insights.tsx | 109 +++++++++ .../components/analytics-v2/overview/root.tsx | 19 ++ .../analytics-v2/select/analytics-params.tsx | 98 ++++++++ .../analytics-v2/select/duration.tsx | 50 ++++ .../analytics-v2/select/project.tsx | 60 +++++ .../analytics-v2/select/select-x-axis.tsx | 31 +++ .../analytics-v2/select/select-y-axis.tsx | 67 +++++ .../analytics-v2/total-insights.tsx | 58 +++++ .../components/analytics-v2/trend-piece.tsx | 47 ++++ .../work-items/created-vs-resolved.tsx | 119 +++++++++ .../work-items/customized-insights.tsx | 53 ++++ .../analytics-v2/work-items/index.ts | 1 + .../analytics-v2/work-items/modal/content.tsx | 48 ++++ .../analytics-v2/work-items/modal/header.tsx | 37 +++ .../analytics-v2/work-items/modal/index.tsx | 64 +++++ .../work-items/priority-chart.tsx | 230 ++++++++++++++++++ .../analytics-v2/work-items/root.tsx | 19 ++ .../analytics-v2/work-items/utils.ts | 47 ++++ .../work-items/workitems-insight-table.tsx | 102 ++++++++ web/core/components/chart/utils.ts | 166 +++++++++++++ .../empty-state/detailed-empty-state-root.tsx | 4 +- web/core/components/issues/filters.tsx | 3 +- web/core/hooks/store/index.ts | 1 + web/core/hooks/store/use-analytics-v2.ts | 11 + web/core/services/analytics-v2.service.ts | 60 +++++ web/core/store/analytics-v2.store.ts | 68 ++++++ web/core/store/root.store.ts | 3 + web/package.json | 4 +- .../analytics-v2/empty-chart-area-dark.webp | Bin 0 -> 2720 bytes .../analytics-v2/empty-chart-area-light.webp | Bin 0 -> 694 bytes .../analytics-v2/empty-chart-bar-dark.webp | Bin 0 -> 2508 bytes .../analytics-v2/empty-chart-bar-light.webp | Bin 0 -> 512 bytes .../analytics-v2/empty-chart-radar-dark.webp | Bin 0 -> 3076 bytes .../analytics-v2/empty-chart-radar-light.webp | Bin 0 -> 716 bytes .../empty-grid-background-dark.webp | Bin 0 -> 35070 bytes .../empty-grid-background-light.webp | Bin 0 -> 2576 bytes .../analytics-v2/empty-table-dark.webp | Bin 0 -> 3280 bytes .../analytics-v2/empty-table-light.webp | Bin 0 -> 862 bytes yarn.lock | 181 +++++++++++++- 103 files changed, 3919 insertions(+), 162 deletions(-) create mode 100644 packages/constants/src/analytics-v2/common.ts create mode 100644 packages/constants/src/analytics-v2/index.ts create mode 100644 packages/propel/src/charts/radar-chart/index.ts create mode 100644 packages/propel/src/charts/radar-chart/root.tsx create mode 100644 packages/propel/src/charts/scatter-chart/index.ts create mode 100644 packages/propel/src/charts/scatter-chart/root.tsx create mode 100644 packages/propel/src/table/core.tsx create mode 100644 packages/propel/src/table/index.ts create mode 100644 packages/types/src/analytics-v2.d.ts create mode 100644 packages/types/src/charts/common.d.ts rename packages/types/src/{charts.d.ts => charts/index.d.ts} (63%) create mode 100644 web/ce/components/analytics-v2/tabs.ts create mode 100644 web/core/components/analytics-v2/analytics-filter-actions.tsx create mode 100644 web/core/components/analytics-v2/analytics-section-wrapper.tsx create mode 100644 web/core/components/analytics-v2/analytics-wrapper.tsx create mode 100644 web/core/components/analytics-v2/empty-state.tsx create mode 100644 web/core/components/analytics-v2/index.ts create mode 100644 web/core/components/analytics-v2/insight-card.tsx create mode 100644 web/core/components/analytics-v2/insight-table/data-table.tsx create mode 100644 web/core/components/analytics-v2/insight-table/index.ts create mode 100644 web/core/components/analytics-v2/insight-table/loader.tsx create mode 100644 web/core/components/analytics-v2/insight-table/root.tsx create mode 100644 web/core/components/analytics-v2/loaders.tsx create mode 100644 web/core/components/analytics-v2/overview/active-project-item.tsx create mode 100644 web/core/components/analytics-v2/overview/active-projects.tsx create mode 100644 web/core/components/analytics-v2/overview/index.ts create mode 100644 web/core/components/analytics-v2/overview/project-insights.tsx create mode 100644 web/core/components/analytics-v2/overview/root.tsx create mode 100644 web/core/components/analytics-v2/select/analytics-params.tsx create mode 100644 web/core/components/analytics-v2/select/duration.tsx create mode 100644 web/core/components/analytics-v2/select/project.tsx create mode 100644 web/core/components/analytics-v2/select/select-x-axis.tsx create mode 100644 web/core/components/analytics-v2/select/select-y-axis.tsx create mode 100644 web/core/components/analytics-v2/total-insights.tsx create mode 100644 web/core/components/analytics-v2/trend-piece.tsx create mode 100644 web/core/components/analytics-v2/work-items/created-vs-resolved.tsx create mode 100644 web/core/components/analytics-v2/work-items/customized-insights.tsx create mode 100644 web/core/components/analytics-v2/work-items/index.ts create mode 100644 web/core/components/analytics-v2/work-items/modal/content.tsx create mode 100644 web/core/components/analytics-v2/work-items/modal/header.tsx create mode 100644 web/core/components/analytics-v2/work-items/modal/index.tsx create mode 100644 web/core/components/analytics-v2/work-items/priority-chart.tsx create mode 100644 web/core/components/analytics-v2/work-items/root.tsx create mode 100644 web/core/components/analytics-v2/work-items/utils.ts create mode 100644 web/core/components/analytics-v2/work-items/workitems-insight-table.tsx create mode 100644 web/core/components/chart/utils.ts create mode 100644 web/core/hooks/store/use-analytics-v2.ts create mode 100644 web/core/services/analytics-v2.service.ts create mode 100644 web/core/store/analytics-v2.store.ts create mode 100644 web/public/empty-state/analytics-v2/empty-chart-area-dark.webp create mode 100644 web/public/empty-state/analytics-v2/empty-chart-area-light.webp create mode 100644 web/public/empty-state/analytics-v2/empty-chart-bar-dark.webp create mode 100644 web/public/empty-state/analytics-v2/empty-chart-bar-light.webp create mode 100644 web/public/empty-state/analytics-v2/empty-chart-radar-dark.webp create mode 100644 web/public/empty-state/analytics-v2/empty-chart-radar-light.webp create mode 100644 web/public/empty-state/analytics-v2/empty-grid-background-dark.webp create mode 100644 web/public/empty-state/analytics-v2/empty-grid-background-light.webp create mode 100644 web/public/empty-state/analytics-v2/empty-table-dark.webp create mode 100644 web/public/empty-state/analytics-v2/empty-table-light.webp diff --git a/apiserver/plane/app/views/analytic/advance.py b/apiserver/plane/app/views/analytic/advance.py index 8b5832772..9b258eca0 100644 --- a/apiserver/plane/app/views/analytic/advance.py +++ b/apiserver/plane/app/views/analytic/advance.py @@ -17,6 +17,17 @@ from plane.db.models import ( ProjectPage, ) +from django.db.models import ( + Q, + Count, +) +from plane.utils.build_chart import build_analytics_chart +from datetime import timedelta +from plane.bgtasks.analytic_plot_export import export_analytics_to_csv_email +from plane.utils.date_utils import ( + get_analytics_filters, +) + from plane.utils.build_chart import build_analytics_chart from plane.bgtasks.analytic_plot_export import export_analytics_to_csv_email from plane.utils.date_utils import get_analytics_filters @@ -35,6 +46,7 @@ class AdvanceAnalyticsBaseView(BaseAPIView): class AdvanceAnalyticsEndpoint(AdvanceAnalyticsBaseView): + def get_filtered_counts(self, queryset: QuerySet) -> Dict[str, int]: def get_filtered_count() -> int: if self.filters["analytics_date_range"]: @@ -111,6 +123,7 @@ class AdvanceAnalyticsEndpoint(AdvanceAnalyticsBaseView): ), } + def get_work_items_stats(self) -> Dict[str, Dict[str, int]]: base_queryset = Issue.objects.filter(**self.filters["base_filters"]) @@ -193,6 +206,7 @@ class AdvanceAnalyticsChartEndpoint(AdvanceAnalyticsBaseView): # Get the base queryset with workspace and project filters base_queryset = Issue.issue_objects.filter(**self.filters["base_filters"]) date_filter = {} + # Apply date range filter if available if self.filters["chart_period_range"]: start_date, end_date = self.filters["chart_period_range"] diff --git a/packages/constants/src/analytics-v2/common.ts b/packages/constants/src/analytics-v2/common.ts new file mode 100644 index 000000000..6eab3ab29 --- /dev/null +++ b/packages/constants/src/analytics-v2/common.ts @@ -0,0 +1,105 @@ +import { TAnalyticsTabsV2Base } from "@plane/types"; +import { ChartXAxisProperty, ChartYAxisMetric } from "../chart"; + +export const insightsFields: Record = { + overview: [ + "total_users", + "total_admins", + "total_members", + "total_guests", + "total_projects", + "total_work_items", + "total_cycles", + "total_intake", + ], + "work-items": [ + "total_work_items", + "started_work_items", + "backlog_work_items", + "un_started_work_items", + "completed_work_items", + ], +}; + +export const ANALYTICS_V2_DURATION_FILTER_OPTIONS = [ + { + name: "Yesterday", + value: "yesterday", + }, + { + name: "Last 7 days", + value: "last_7_days", + }, + { + name: "Last 30 days", + value: "last_30_days", + }, + { + name: "Last 3 months", + value: "last_3_months", + }, +]; + +export const ANALYTICS_V2_X_AXIS_VALUES: { value: ChartXAxisProperty; label: string }[] = [ + { + value: ChartXAxisProperty.STATES, + label: "State name", + }, + { + value: ChartXAxisProperty.STATE_GROUPS, + label: "State group", + }, + { + value: ChartXAxisProperty.PRIORITY, + label: "Priority", + }, + { + value: ChartXAxisProperty.LABELS, + label: "Label", + }, + { + value: ChartXAxisProperty.ASSIGNEES, + label: "Assignee", + }, + { + value: ChartXAxisProperty.ESTIMATE_POINTS, + label: "Estimate point", + }, + { + value: ChartXAxisProperty.CYCLES, + label: "Cycle", + }, + { + value: ChartXAxisProperty.MODULES, + label: "Module", + }, + { + value: ChartXAxisProperty.COMPLETED_AT, + label: "Completed date", + }, + { + value: ChartXAxisProperty.TARGET_DATE, + label: "Due date", + }, + { + value: ChartXAxisProperty.START_DATE, + label: "Start date", + }, + { + value: ChartXAxisProperty.CREATED_AT, + label: "Created date", + }, +]; + +export const ANALYTICS_V2_Y_AXIS_VALUES: { value: ChartYAxisMetric; label: string }[] = [ + { + value: ChartYAxisMetric.WORK_ITEM_COUNT, + label: "Work item", + }, + { + value: ChartYAxisMetric.ESTIMATE_POINT_COUNT, + label: "Estimate", + }, +]; + +export const ANALYTICS_V2_DATE_KEYS = ["completed_at", "target_date", "start_date", "created_at"]; diff --git a/packages/constants/src/analytics-v2/index.ts b/packages/constants/src/analytics-v2/index.ts new file mode 100644 index 000000000..3e2372da3 --- /dev/null +++ b/packages/constants/src/analytics-v2/index.ts @@ -0,0 +1 @@ +export * from "./common" \ No newline at end of file diff --git a/packages/constants/src/chart.ts b/packages/constants/src/chart.ts index bddd0fd38..be736d807 100644 --- a/packages/constants/src/chart.ts +++ b/packages/constants/src/chart.ts @@ -1,2 +1,157 @@ +import { TChartColorScheme } from "@plane/types"; + export const LABEL_CLASSNAME = "uppercase text-custom-text-300/60 text-sm tracking-wide"; export const AXIS_LABEL_CLASSNAME = "uppercase text-custom-text-300/60 text-sm tracking-wide"; + + +export enum ChartXAxisProperty { + STATES = "STATES", + STATE_GROUPS = "STATE_GROUPS", + LABELS = "LABELS", + ASSIGNEES = "ASSIGNEES", + ESTIMATE_POINTS = "ESTIMATE_POINTS", + CYCLES = "CYCLES", + MODULES = "MODULES", + PRIORITY = "PRIORITY", + START_DATE = "START_DATE", + TARGET_DATE = "TARGET_DATE", + CREATED_AT = "CREATED_AT", + COMPLETED_AT = "COMPLETED_AT", + CREATED_BY = "CREATED_BY", + WORK_ITEM_TYPES = "WORK_ITEM_TYPES", + PROJECTS = "PROJECTS", + EPICS = "EPICS", +} + +export enum ChartYAxisMetric { + WORK_ITEM_COUNT = "WORK_ITEM_COUNT", + ESTIMATE_POINT_COUNT = "ESTIMATE_POINT_COUNT", + PENDING_WORK_ITEM_COUNT = "PENDING_WORK_ITEM_COUNT", + COMPLETED_WORK_ITEM_COUNT = "COMPLETED_WORK_ITEM_COUNT", + IN_PROGRESS_WORK_ITEM_COUNT = "IN_PROGRESS_WORK_ITEM_COUNT", + WORK_ITEM_DUE_THIS_WEEK_COUNT = "WORK_ITEM_DUE_THIS_WEEK_COUNT", + WORK_ITEM_DUE_TODAY_COUNT = "WORK_ITEM_DUE_TODAY_COUNT", + BLOCKED_WORK_ITEM_COUNT = "BLOCKED_WORK_ITEM_COUNT", +} + + +export enum ChartXAxisDateGrouping { + DAY = "DAY", + WEEK = "WEEK", + MONTH = "MONTH", + YEAR = "YEAR", +} + +export const TO_CAPITALIZE_PROPERTIES: ChartXAxisProperty[] = [ + ChartXAxisProperty.PRIORITY, + ChartXAxisProperty.STATE_GROUPS, +]; + +export const CHART_X_AXIS_DATE_PROPERTIES: ChartXAxisProperty[] = [ + ChartXAxisProperty.START_DATE, + ChartXAxisProperty.TARGET_DATE, + ChartXAxisProperty.CREATED_AT, + ChartXAxisProperty.COMPLETED_AT, +]; + + +export enum EChartModels { + BASIC = "BASIC", + STACKED = "STACKED", + GROUPED = "GROUPED", + MULTI_LINE = "MULTI_LINE", + COMPARISON = "COMPARISON", + PROGRESS = "PROGRESS", +} + +export const CHART_COLOR_PALETTES: { + key: TChartColorScheme; + i18n_label: string; + light: string[]; + dark: string[]; +}[] = [ + { + key: "modern", + i18n_label: "dashboards.widget.color_palettes.modern", + light: [ + "#6172E8", + "#8B6EDB", + "#E05F99", + "#29A383", + "#CB8A37", + "#3AA7C1", + "#F1B24A", + "#E84855", + "#50C799", + "#B35F9E", + ], + dark: [ + "#6B7CDE", + "#8E9DE6", + "#D45D9E", + "#2EAF85", + "#D4A246", + "#29A7C1", + "#B89F6A", + "#D15D64", + "#4ED079", + "#A169A4", + ], + }, + { + key: "horizon", + i18n_label: "dashboards.widget.color_palettes.horizon", + light: [ + "#E76E50", + "#289D90", + "#F3A362", + "#E9C368", + "#264753", + "#8A6FA0", + "#5B9EE5", + "#7CC474", + "#BA7DB5", + "#CF8640", + ], + dark: [ + "#E05A3A", + "#1D8A7E", + "#D98B4D", + "#D1AC50", + "#3A6B7C", + "#7D6297", + "#4D8ACD", + "#569C64", + "#C16A8C", + "#B77436", + ], + }, + { + key: "earthen", + i18n_label: "dashboards.widget.color_palettes.earthen", + light: [ + "#386641", + "#6A994E", + "#A7C957", + "#E97F4E", + "#BC4749", + "#9E2A2B", + "#80CED1", + "#5C3E79", + "#526EAB", + "#6B5B95", + ], + dark: [ + "#497752", + "#7BAA5F", + "#B8DA68", + "#FA905F", + "#CD585A", + "#AF3B3C", + "#91DFE2", + "#6D4F8A", + "#637FBC", + "#7C6CA6", + ], + }, + ]; diff --git a/packages/constants/src/index.ts b/packages/constants/src/index.ts index 057627fcd..49e10c3d1 100644 --- a/packages/constants/src/index.ts +++ b/packages/constants/src/index.ts @@ -33,3 +33,4 @@ export * from "./page"; export * from "./emoji"; export * from "./subscription"; export * from "./icon"; +export * from "./analytics-v2"; diff --git a/packages/constants/src/state.ts b/packages/constants/src/state.ts index fa0f5d277..3b6de4c8f 100644 --- a/packages/constants/src/state.ts +++ b/packages/constants/src/state.ts @@ -1,3 +1,4 @@ +"use client" export type TStateGroups = "backlog" | "unstarted" | "started" | "completed" | "cancelled"; export type TDraggableData = { @@ -77,4 +78,5 @@ export const PROGRESS_STATE_GROUPS_DETAILS = [ }, ]; + export const DISPLAY_WORKFLOW_PRO_CTA = false; diff --git a/packages/i18n/src/locales/cs/translations.json b/packages/i18n/src/locales/cs/translations.json index b4a7f2480..4aa64f40d 100644 --- a/packages/i18n/src/locales/cs/translations.json +++ b/packages/i18n/src/locales/cs/translations.json @@ -1311,7 +1311,38 @@ } } } - } + }, + "empty_state_v2": { + "customized_insights": { + "description": "Pracovní položky přiřazené vám, rozdělené podle stavu, se zde zobrazí.", + "title": "Zatím žádná data" + }, + "created_vs_resolved": { + "description": "Pracovní položky vytvořené a vyřešené v průběhu času se zde zobrazí.", + "title": "Zatím žádná data" + }, + "project_insights": { + "title": "Zatím žádná data", + "description": "Pracovní položky přiřazené vám, rozdělené podle stavu, se zde zobrazí." + } + }, + "created_vs_resolved": "Vytvořeno vs Vyřešeno", + "customized_insights": "Přizpůsobené přehledy", + "backlog_work_items": "Pracovní položky v backlogu", + "active_projects": "Aktivní projekty", + "trend_on_charts": "Trend na grafech", + "all_projects": "Všechny projekty", + "summary_of_projects": "Souhrn projektů", + "project_insights": "Přehled projektu", + "started_work_items": "Zahájené pracovní položky", + "total_work_items": "Celkový počet pracovních položek", + "total_projects": "Celkový počet projektů", + "total_admins": "Celkový počet administrátorů", + "total_users": "Celkový počet uživatelů", + "total_intake": "Celkový příjem", + "un_started_work_items": "Nezahájené pracovní položky", + "total_guests": "Celkový počet hostů", + "completed_work_items": "Dokončené pracovní položky" }, "workspace_projects": { "label": "{count, plural, one {Projekt} few {Projekty} other {Projektů}}", diff --git a/packages/i18n/src/locales/de/translations.json b/packages/i18n/src/locales/de/translations.json index 6032432d1..1fa8eaa0e 100644 --- a/packages/i18n/src/locales/de/translations.json +++ b/packages/i18n/src/locales/de/translations.json @@ -1311,7 +1311,38 @@ } } } - } + }, + "empty_state_v2": { + "customized_insights": { + "description": "Ihnen zugewiesene Arbeitselemente, aufgeschlüsselt nach Status, werden hier angezeigt.", + "title": "Noch keine Daten" + }, + "created_vs_resolved": { + "description": "Im Laufe der Zeit erstellte und gelöste Arbeitselemente werden hier angezeigt.", + "title": "Noch keine Daten" + }, + "project_insights": { + "title": "Noch keine Daten", + "description": "Ihnen zugewiesene Arbeitselemente, aufgeschlüsselt nach Status, werden hier angezeigt." + } + }, + "created_vs_resolved": "Erstellt vs Gelöst", + "customized_insights": "Individuelle Einblicke", + "backlog_work_items": "Backlog-Arbeitselemente", + "active_projects": "Aktive Projekte", + "trend_on_charts": "Trend in Diagrammen", + "all_projects": "Alle Projekte", + "summary_of_projects": "Projektübersicht", + "project_insights": "Projekteinblicke", + "started_work_items": "Begonnene Arbeitselemente", + "total_work_items": "Gesamte Arbeitselemente", + "total_projects": "Gesamtprojekte", + "total_admins": "Gesamtanzahl der Admins", + "total_users": "Gesamtanzahl der Benutzer", + "total_intake": "Gesamteinnahmen", + "un_started_work_items": "Nicht begonnene Arbeitselemente", + "total_guests": "Gesamtanzahl der Gäste", + "completed_work_items": "Abgeschlossene Arbeitselemente" }, "workspace_projects": { "label": "{count, plural, one {Projekt} few {Projekte} other {Projekte}}", diff --git a/packages/i18n/src/locales/en/translations.json b/packages/i18n/src/locales/en/translations.json index 4e233ace3..ef16944ef 100644 --- a/packages/i18n/src/locales/en/translations.json +++ b/packages/i18n/src/locales/en/translations.json @@ -699,7 +699,8 @@ "view": "View", "deactivated_user": "Deactivated user", "apply": "Apply", - "applying": "Applying" + "applying": "Applying", + "overview": "Overview" }, "chart": { "x_axis": "X-axis", @@ -1146,6 +1147,37 @@ } } } + }, + "total_work_items": "Total work items", + "started_work_items": "Started work items", + "backlog_work_items": "Backlog work items", + "un_started_work_items": "Unstarted work items", + "completed_work_items": "Completed work items", + "total_guests": "Total Guests", + "total_intake": "Total Intake", + "total_users": "Total Users", + "total_admins": "Total Admins", + "total_projects": "Total Projects", + "project_insights": "Project Insights", + "summary_of_projects": "Summary of Projects", + "all_projects": "All Projects", + "trend_on_charts": "Trend on charts", + "active_projects": "Active Projects", + "customized_insights": "Customized Insights", + "created_vs_resolved": "Created vs Resolved", + "empty_state_v2": { + "project_insights": { + "title": "No data yet", + "description": "Work items assigned to you, broken down by state, will show up here." + }, + "created_vs_resolved": { + "title": "No data yet", + "description": "Work items created and resolved over time will show up here." + }, + "customized_insights": { + "title": "No data yet", + "description": "Work items assigned to you, broken down by state, will show up here." + } } }, "workspace_projects": { diff --git a/packages/i18n/src/locales/es/translations.json b/packages/i18n/src/locales/es/translations.json index bd0402cab..966e3178d 100644 --- a/packages/i18n/src/locales/es/translations.json +++ b/packages/i18n/src/locales/es/translations.json @@ -1314,7 +1314,38 @@ } } } - } + }, + "empty_state_v2": { + "customized_insights": { + "description": "Los elementos de trabajo asignados a ti, desglosados por estado, aparecerán aquí.", + "title": "Aún no hay datos" + }, + "created_vs_resolved": { + "description": "Los elementos de trabajo creados y resueltos con el tiempo aparecerán aquí.", + "title": "Aún no hay datos" + }, + "project_insights": { + "title": "Aún no hay datos", + "description": "Los elementos de trabajo asignados a ti, desglosados por estado, aparecerán aquí." + } + }, + "created_vs_resolved": "Creado vs Resuelto", + "customized_insights": "Información personalizada", + "backlog_work_items": "Elementos de trabajo en backlog", + "active_projects": "Proyectos activos", + "trend_on_charts": "Tendencia en gráficos", + "all_projects": "Todos los proyectos", + "summary_of_projects": "Resumen de proyectos", + "project_insights": "Información del proyecto", + "started_work_items": "Elementos de trabajo iniciados", + "total_work_items": "Total de elementos de trabajo", + "total_projects": "Total de proyectos", + "total_admins": "Total de administradores", + "total_users": "Total de usuarios", + "total_intake": "Ingreso total", + "un_started_work_items": "Elementos de trabajo no iniciados", + "total_guests": "Total de invitados", + "completed_work_items": "Elementos de trabajo completados" }, "workspace_projects": { "label": "{count, plural, one {Proyecto} other {Proyectos}}", diff --git a/packages/i18n/src/locales/fr/translations.json b/packages/i18n/src/locales/fr/translations.json index 9d3e25b4d..5188b3334 100644 --- a/packages/i18n/src/locales/fr/translations.json +++ b/packages/i18n/src/locales/fr/translations.json @@ -1312,7 +1312,38 @@ } } } - } + }, + "empty_state_v2": { + "customized_insights": { + "description": "Les éléments de travail qui vous sont assignés, répartis par état, s'afficheront ici.", + "title": "Pas encore de données" + }, + "created_vs_resolved": { + "description": "Les éléments de travail créés et résolus au fil du temps s'afficheront ici.", + "title": "Pas encore de données" + }, + "project_insights": { + "title": "Pas encore de données", + "description": "Les éléments de travail qui vous sont assignés, répartis par état, s'afficheront ici." + } + }, + "created_vs_resolved": "Créé vs Résolu", + "customized_insights": "Informations personnalisées", + "backlog_work_items": "Éléments de travail en backlog", + "active_projects": "Projets actifs", + "trend_on_charts": "Tendance sur les graphiques", + "all_projects": "Tous les projets", + "summary_of_projects": "Résumé des projets", + "project_insights": "Aperçus du projet", + "started_work_items": "Éléments de travail commencés", + "total_work_items": "Total des éléments de travail", + "total_projects": "Total des projets", + "total_admins": "Total des administrateurs", + "total_users": "Nombre total d'utilisateurs", + "total_intake": "Revenu total", + "un_started_work_items": "Éléments de travail non commencés", + "total_guests": "Nombre total d'invités", + "completed_work_items": "Éléments de travail terminés" }, "workspace_projects": { "label": "{count, plural, one {Projet} other {Projets}}", diff --git a/packages/i18n/src/locales/id/translations.json b/packages/i18n/src/locales/id/translations.json index b48df1890..3a6c92873 100644 --- a/packages/i18n/src/locales/id/translations.json +++ b/packages/i18n/src/locales/id/translations.json @@ -1311,7 +1311,38 @@ } } } - } + }, + "empty_state_v2": { + "customized_insights": { + "description": "Item pekerjaan yang ditugaskan kepada Anda, dipecah berdasarkan status, akan muncul di sini.", + "title": "Belum ada data" + }, + "created_vs_resolved": { + "description": "Item pekerjaan yang dibuat dan diselesaikan dari waktu ke waktu akan muncul di sini.", + "title": "Belum ada data" + }, + "project_insights": { + "title": "Belum ada data", + "description": "Item pekerjaan yang ditugaskan kepada Anda, dipecah berdasarkan status, akan muncul di sini." + } + }, + "created_vs_resolved": "Dibuat vs Diselesaikan", + "customized_insights": "Wawasan yang Disesuaikan", + "backlog_work_items": "Item pekerjaan backlog", + "active_projects": "Proyek Aktif", + "trend_on_charts": "Tren pada grafik", + "all_projects": "Semua Proyek", + "summary_of_projects": "Ringkasan Proyek", + "project_insights": "Wawasan Proyek", + "started_work_items": "Item pekerjaan yang telah dimulai", + "total_work_items": "Total item pekerjaan", + "total_projects": "Total Proyek", + "total_admins": "Total Admin", + "total_users": "Total Pengguna", + "total_intake": "Total Pemasukan", + "un_started_work_items": "Item pekerjaan yang belum dimulai", + "total_guests": "Total Tamu", + "completed_work_items": "Item pekerjaan yang telah selesai" }, "workspace_projects": { "label": "{count, plural, one {Proyek} other {Proyek}}", diff --git a/packages/i18n/src/locales/it/translations.json b/packages/i18n/src/locales/it/translations.json index b222ed68b..ff58fee31 100644 --- a/packages/i18n/src/locales/it/translations.json +++ b/packages/i18n/src/locales/it/translations.json @@ -1310,7 +1310,38 @@ } } } - } + }, + "empty_state_v2": { + "customized_insights": { + "description": "Gli elementi di lavoro assegnati a te, suddivisi per stato, verranno visualizzati qui.", + "title": "Nessun dato disponibile" + }, + "created_vs_resolved": { + "description": "Gli elementi di lavoro creati e risolti nel tempo verranno visualizzati qui.", + "title": "Nessun dato disponibile" + }, + "project_insights": { + "title": "Nessun dato disponibile", + "description": "Gli elementi di lavoro assegnati a te, suddivisi per stato, verranno visualizzati qui." + } + }, + "created_vs_resolved": "Creato vs Risolto", + "customized_insights": "Approfondimenti personalizzati", + "backlog_work_items": "Elementi di lavoro nel backlog", + "active_projects": "Progetti attivi", + "trend_on_charts": "Tendenza nei grafici", + "all_projects": "Tutti i progetti", + "summary_of_projects": "Riepilogo dei progetti", + "project_insights": "Approfondimenti sul progetto", + "started_work_items": "Elementi di lavoro iniziati", + "total_work_items": "Totale elementi di lavoro", + "total_projects": "Progetti totali", + "total_admins": "Totale amministratori", + "total_users": "Totale utenti", + "total_intake": "Entrate totali", + "un_started_work_items": "Elementi di lavoro non avviati", + "total_guests": "Totale ospiti", + "completed_work_items": "Elementi di lavoro completati" }, "workspace_projects": { "label": "{count, plural, one {Progetto} other {Progetti}}", diff --git a/packages/i18n/src/locales/ja/translations.json b/packages/i18n/src/locales/ja/translations.json index fdb1f2fd0..9656f0439 100644 --- a/packages/i18n/src/locales/ja/translations.json +++ b/packages/i18n/src/locales/ja/translations.json @@ -1312,7 +1312,38 @@ } } } - } + }, + "empty_state_v2": { + "customized_insights": { + "description": "あなたに割り当てられた作業項目は、ステータスごとに分類されてここに表示されます。", + "title": "まだデータがありません" + }, + "created_vs_resolved": { + "description": "時間の経過とともに作成および解決された作業項目がここに表示されます。", + "title": "まだデータがありません" + }, + "project_insights": { + "title": "まだデータがありません", + "description": "あなたに割り当てられた作業項目は、ステータスごとに分類されてここに表示されます。" + } + }, + "created_vs_resolved": "作成 vs 解決", + "customized_insights": "カスタマイズされたインサイト", + "backlog_work_items": "バックログの作業項目", + "active_projects": "アクティブなプロジェクト", + "trend_on_charts": "グラフの傾向", + "all_projects": "すべてのプロジェクト", + "summary_of_projects": "プロジェクトの概要", + "project_insights": "プロジェクトのインサイト", + "started_work_items": "開始された作業項目", + "total_work_items": "作業項目の合計", + "total_projects": "プロジェクト合計", + "total_admins": "管理者の合計", + "total_users": "ユーザー総数", + "total_intake": "総収入", + "un_started_work_items": "未開始の作業項目", + "total_guests": "ゲストの合計", + "completed_work_items": "完了した作業項目" }, "workspace_projects": { "label": "{count, plural, one {プロジェクト} other {プロジェクト}}", diff --git a/packages/i18n/src/locales/ko/translations.json b/packages/i18n/src/locales/ko/translations.json index 4d53d1e8c..eb9b97bf4 100644 --- a/packages/i18n/src/locales/ko/translations.json +++ b/packages/i18n/src/locales/ko/translations.json @@ -1313,7 +1313,38 @@ } } } - } + }, + "empty_state_v2": { + "customized_insights": { + "description": "귀하에게 할당된 작업 항목이 상태별로 나누어 여기에 표시됩니다.", + "title": "아직 데이터가 없습니다" + }, + "created_vs_resolved": { + "description": "시간이 지나면서 생성되고 해결된 작업 항목이 여기에 표시됩니다.", + "title": "아직 데이터가 없습니다" + }, + "project_insights": { + "title": "아직 데이터가 없습니다", + "description": "귀하에게 할당된 작업 항목이 상태별로 나누어 여기에 표시됩니다." + } + }, + "created_vs_resolved": "생성됨 vs 해결됨", + "customized_insights": "맞춤형 인사이트", + "backlog_work_items": "백로그 작업 항목", + "active_projects": "활성 프로젝트", + "trend_on_charts": "차트의 추세", + "all_projects": "모든 프로젝트", + "summary_of_projects": "프로젝트 요약", + "project_insights": "프로젝트 인사이트", + "started_work_items": "시작된 작업 항목", + "total_work_items": "총 작업 항목", + "total_projects": "총 프로젝트 수", + "total_admins": "총 관리자 수", + "total_users": "총 사용자 수", + "total_intake": "총 수입", + "un_started_work_items": "시작되지 않은 작업 항목", + "total_guests": "총 게스트 수", + "completed_work_items": "완료된 작업 항목" }, "workspace_projects": { "label": "{count, plural, one {프로젝트} other {프로젝트}}", diff --git a/packages/i18n/src/locales/pl/translations.json b/packages/i18n/src/locales/pl/translations.json index 49caa7350..28290e3d0 100644 --- a/packages/i18n/src/locales/pl/translations.json +++ b/packages/i18n/src/locales/pl/translations.json @@ -1313,7 +1313,38 @@ } } } - } + }, + "empty_state_v2": { + "customized_insights": { + "description": "Przypisane do Ciebie elementy pracy, podzielone według stanu, pojawią się tutaj.", + "title": "Brak danych" + }, + "created_vs_resolved": { + "description": "Elementy pracy utworzone i rozwiązane w czasie pojawią się tutaj.", + "title": "Brak danych" + }, + "project_insights": { + "title": "Brak danych", + "description": "Przypisane do Ciebie elementy pracy, podzielone według stanu, pojawią się tutaj." + } + }, + "created_vs_resolved": "Utworzone vs Rozwiązane", + "customized_insights": "Dostosowane informacje", + "backlog_work_items": "Elementy pracy w backlogu", + "active_projects": "Aktywne projekty", + "trend_on_charts": "Trend na wykresach", + "all_projects": "Wszystkie projekty", + "summary_of_projects": "Podsumowanie projektów", + "project_insights": "Wgląd w projekt", + "started_work_items": "Rozpoczęte elementy pracy", + "total_work_items": "Łączna liczba elementów pracy", + "total_projects": "Łączna liczba projektów", + "total_admins": "Łączna liczba administratorów", + "total_users": "Łączna liczba użytkowników", + "total_intake": "Całkowity dochód", + "un_started_work_items": "Nierozpoczęte elementy pracy", + "total_guests": "Łączna liczba gości", + "completed_work_items": "Ukończone elementy pracy" }, "workspace_projects": { "label": "{count, plural, one {Projekt} few {Projekty} other {Projektów}}", diff --git a/packages/i18n/src/locales/pt-BR/translations.json b/packages/i18n/src/locales/pt-BR/translations.json index 9c15dcb6d..6b31fcbf4 100644 --- a/packages/i18n/src/locales/pt-BR/translations.json +++ b/packages/i18n/src/locales/pt-BR/translations.json @@ -1313,7 +1313,38 @@ } } } - } + }, + "empty_state_v2": { + "customized_insights": { + "description": "Os itens de trabalho atribuídos a você, divididos por estado, aparecerão aqui.", + "title": "Ainda não há dados" + }, + "created_vs_resolved": { + "description": "Os itens de trabalho criados e resolvidos ao longo do tempo aparecerão aqui.", + "title": "Ainda não há dados" + }, + "project_insights": { + "title": "Ainda não há dados", + "description": "Os itens de trabalho atribuídos a você, divididos por estado, aparecerão aqui." + } + }, + "created_vs_resolved": "Criado vs Resolvido", + "customized_insights": "Insights personalizados", + "backlog_work_items": "Itens de trabalho no backlog", + "active_projects": "Projetos ativos", + "trend_on_charts": "Tendência nos gráficos", + "all_projects": "Todos os projetos", + "summary_of_projects": "Resumo dos projetos", + "project_insights": "Insights do projeto", + "started_work_items": "Itens de trabalho iniciados", + "total_work_items": "Total de itens de trabalho", + "total_projects": "Total de projetos", + "total_admins": "Total de administradores", + "total_users": "Total de usuários", + "total_intake": "Receita total", + "un_started_work_items": "Itens de trabalho não iniciados", + "total_guests": "Total de convidados", + "completed_work_items": "Itens de trabalho concluídos" }, "workspace_projects": { "label": "{count, plural, one {Projeto} other {Projetos}}", diff --git a/packages/i18n/src/locales/ro/translations.json b/packages/i18n/src/locales/ro/translations.json index f3d2053a3..704ee840f 100644 --- a/packages/i18n/src/locales/ro/translations.json +++ b/packages/i18n/src/locales/ro/translations.json @@ -1311,7 +1311,38 @@ } } } - } + }, + "empty_state_v2": { + "customized_insights": { + "description": "Elementele de lucru atribuite ție, împărțite pe stări, vor apărea aici.", + "title": "Nu există date încă" + }, + "created_vs_resolved": { + "description": "Elementele de lucru create și rezolvate în timp vor apărea aici.", + "title": "Nu există date încă" + }, + "project_insights": { + "title": "Nu există date încă", + "description": "Elementele de lucru atribuite ție, împărțite pe stări, vor apărea aici." + } + }, + "created_vs_resolved": "Creat vs Rezolvat", + "customized_insights": "Perspective personalizate", + "backlog_work_items": "Elemente de lucru din backlog", + "active_projects": "Proiecte active", + "trend_on_charts": "Tendință în grafice", + "all_projects": "Toate proiectele", + "summary_of_projects": "Sumarul proiectelor", + "project_insights": "Informații despre proiect", + "started_work_items": "Elemente de lucru începute", + "total_work_items": "Totalul elementelor de lucru", + "total_projects": "Total proiecte", + "total_admins": "Total administratori", + "total_users": "Total utilizatori", + "total_intake": "Venit total", + "un_started_work_items": "Elemente de lucru neîncepute", + "total_guests": "Total invitați", + "completed_work_items": "Elemente de lucru finalizate" }, "workspace_projects": { "label": "{count, plural, one {Proiect} other {Proiecte}}", diff --git a/packages/i18n/src/locales/ru/translations.json b/packages/i18n/src/locales/ru/translations.json index 751f91613..f1a9659e3 100644 --- a/packages/i18n/src/locales/ru/translations.json +++ b/packages/i18n/src/locales/ru/translations.json @@ -1313,7 +1313,38 @@ } } } - } + }, + "empty_state_v2": { + "customized_insights": { + "description": "Назначенные вам рабочие элементы, разбитые по статусам, появятся здесь.", + "title": "Данных пока нет" + }, + "created_vs_resolved": { + "description": "Созданные и решённые со временем рабочие элементы появятся здесь.", + "title": "Данных пока нет" + }, + "project_insights": { + "title": "Данных пока нет", + "description": "Назначенные вам рабочие элементы, разбитые по статусам, появятся здесь." + } + }, + "created_vs_resolved": "Создано vs Решено", + "customized_insights": "Индивидуальные аналитические данные", + "backlog_work_items": "Элементы работы в бэклоге", + "active_projects": "Активные проекты", + "trend_on_charts": "Тренд на графиках", + "all_projects": "Все проекты", + "summary_of_projects": "Сводка по проектам", + "project_insights": "Аналитика проекта", + "started_work_items": "Начатые рабочие элементы", + "total_work_items": "Общее количество рабочих элементов", + "total_projects": "Всего проектов", + "total_admins": "Всего администраторов", + "total_users": "Всего пользователей", + "total_intake": "Общий доход", + "un_started_work_items": "Не начатые рабочие элементы", + "total_guests": "Всего гостей", + "completed_work_items": "Завершённые рабочие элементы" }, "workspace_projects": { "label": "{count, plural, one {Проект} other {Проекты}}", diff --git a/packages/i18n/src/locales/sk/translations.json b/packages/i18n/src/locales/sk/translations.json index e02cad8cc..0aa8f4f84 100644 --- a/packages/i18n/src/locales/sk/translations.json +++ b/packages/i18n/src/locales/sk/translations.json @@ -1313,7 +1313,38 @@ } } } - } + }, + "empty_state_v2": { + "customized_insights": { + "description": "Pracovné položky priradené vám, rozdelené podľa stavu, sa zobrazia tu.", + "title": "Zatiaľ žiadne údaje" + }, + "created_vs_resolved": { + "description": "Pracovné položky vytvorené a vyriešené v priebehu času sa zobrazia tu.", + "title": "Zatiaľ žiadne údaje" + }, + "project_insights": { + "title": "Zatiaľ žiadne údaje", + "description": "Pracovné položky priradené vám, rozdelené podľa stavu, sa zobrazia tu." + } + }, + "created_vs_resolved": "Vytvorené vs Vyriešené", + "customized_insights": "Prispôsobené prehľady", + "backlog_work_items": "Pracovné položky v backlogu", + "active_projects": "Aktívne projekty", + "trend_on_charts": "Trend na grafoch", + "all_projects": "Všetky projekty", + "summary_of_projects": "Súhrn projektov", + "project_insights": "Prehľad projektu", + "started_work_items": "Spustené pracovné položky", + "total_work_items": "Celkový počet pracovných položiek", + "total_projects": "Celkový počet projektov", + "total_admins": "Celkový počet administrátorov", + "total_users": "Celkový počet používateľov", + "total_intake": "Celkový príjem", + "un_started_work_items": "Nespustené pracovné položky", + "total_guests": "Celkový počet hostí", + "completed_work_items": "Dokončené pracovné položky" }, "workspace_projects": { "label": "{count, plural, one {Projekt} few {Projekty} other {Projektov}}", diff --git a/packages/i18n/src/locales/tr-TR/translations.json b/packages/i18n/src/locales/tr-TR/translations.json index b3df9a081..7d4cde25d 100644 --- a/packages/i18n/src/locales/tr-TR/translations.json +++ b/packages/i18n/src/locales/tr-TR/translations.json @@ -1314,7 +1314,38 @@ } } } - } + }, + "empty_state_v2": { + "customized_insights": { + "description": "Size atanan iş öğeleri, duruma göre ayrılarak burada gösterilecektir.", + "title": "Henüz veri yok" + }, + "created_vs_resolved": { + "description": "Zaman içinde oluşturulan ve çözümlenen iş öğeleri burada gösterilecektir.", + "title": "Henüz veri yok" + }, + "project_insights": { + "title": "Henüz veri yok", + "description": "Size atanan iş öğeleri, duruma göre ayrılarak burada gösterilecektir." + } + }, + "created_vs_resolved": "Oluşturulan vs Çözülen", + "customized_insights": "Özelleştirilmiş İçgörüler", + "backlog_work_items": "Backlog iş öğeleri", + "active_projects": "Aktif Projeler", + "trend_on_charts": "Grafiklerdeki eğilim", + "all_projects": "Tüm Projeler", + "summary_of_projects": "Projelerin Özeti", + "project_insights": "Proje İçgörüleri", + "started_work_items": "Başlatılan iş öğeleri", + "total_work_items": "Toplam iş öğesi", + "total_projects": "Toplam Proje", + "total_admins": "Toplam Yönetici", + "total_users": "Toplam Kullanıcı", + "total_intake": "Toplam Gelir", + "un_started_work_items": "Başlanmamış iş öğeleri", + "total_guests": "Toplam Misafir", + "completed_work_items": "Tamamlanmış iş öğeleri" }, "workspace_projects": { "label": "{count, plural, one {Proje} other {Projeler}}", diff --git a/packages/i18n/src/locales/ua/translations.json b/packages/i18n/src/locales/ua/translations.json index 7aa384890..841dbf803 100644 --- a/packages/i18n/src/locales/ua/translations.json +++ b/packages/i18n/src/locales/ua/translations.json @@ -1313,7 +1313,38 @@ } } } - } + }, + "empty_state_v2": { + "customized_insights": { + "description": "Призначені вам робочі елементи, розбиті за станом, з’являться тут.", + "title": "Ще немає даних" + }, + "created_vs_resolved": { + "description": "Створені та вирішені з часом робочі елементи з’являться тут.", + "title": "Ще немає даних" + }, + "project_insights": { + "title": "Ще немає даних", + "description": "Призначені вам робочі елементи, розбиті за станом, з’являться тут." + } + }, + "created_vs_resolved": "Створено vs Вирішено", + "customized_insights": "Персоналізовані аналітичні дані", + "backlog_work_items": "Робочі елементи у беклозі", + "active_projects": "Активні проєкти", + "trend_on_charts": "Тенденція на графіках", + "all_projects": "Усі проєкти", + "summary_of_projects": "Зведення проєктів", + "project_insights": "Аналітика проєкту", + "started_work_items": "Розпочаті робочі елементи", + "total_work_items": "Усього робочих елементів", + "total_projects": "Усього проєктів", + "total_admins": "Усього адміністраторів", + "total_users": "Усього користувачів", + "total_intake": "Загальний дохід", + "un_started_work_items": "Нерозпочаті робочі елементи", + "total_guests": "Усього гостей", + "completed_work_items": "Завершені робочі елементи" }, "workspace_projects": { "label": "{count, plural, one {Проєкт} few {Проєкти} other {Проєктів}}", diff --git a/packages/i18n/src/locales/vi-VN/translations.json b/packages/i18n/src/locales/vi-VN/translations.json index f9f334efb..de2c722eb 100644 --- a/packages/i18n/src/locales/vi-VN/translations.json +++ b/packages/i18n/src/locales/vi-VN/translations.json @@ -1312,7 +1312,38 @@ } } } - } + }, + "empty_state_v2": { + "customized_insights": { + "description": "Các hạng mục công việc được giao cho bạn, phân loại theo trạng thái, sẽ hiển thị tại đây.", + "title": "Chưa có dữ liệu" + }, + "created_vs_resolved": { + "description": "Các hạng mục công việc được tạo và giải quyết theo thời gian sẽ hiển thị tại đây.", + "title": "Chưa có dữ liệu" + }, + "project_insights": { + "title": "Chưa có dữ liệu", + "description": "Các hạng mục công việc được giao cho bạn, phân loại theo trạng thái, sẽ hiển thị tại đây." + } + }, + "created_vs_resolved": "Đã tạo vs Đã giải quyết", + "customized_insights": "Thông tin chi tiết tùy chỉnh", + "backlog_work_items": "Các hạng mục công việc tồn đọng", + "active_projects": "Dự án đang hoạt động", + "trend_on_charts": "Xu hướng trên biểu đồ", + "all_projects": "Tất cả dự án", + "summary_of_projects": "Tóm tắt dự án", + "project_insights": "Thông tin chi tiết dự án", + "started_work_items": "Hạng mục công việc đã bắt đầu", + "total_work_items": "Tổng số hạng mục công việc", + "total_projects": "Tổng số dự án", + "total_admins": "Tổng số quản trị viên", + "total_users": "Tổng số người dùng", + "total_intake": "Tổng thu", + "un_started_work_items": "Hạng mục công việc chưa bắt đầu", + "total_guests": "Tổng số khách", + "completed_work_items": "Hạng mục công việc đã hoàn thành" }, "workspace_projects": { "label": "{count, plural, one {dự án} other {dự án}}", diff --git a/packages/i18n/src/locales/zh-CN/translations.json b/packages/i18n/src/locales/zh-CN/translations.json index 019f13795..d3e3e5998 100644 --- a/packages/i18n/src/locales/zh-CN/translations.json +++ b/packages/i18n/src/locales/zh-CN/translations.json @@ -1312,7 +1312,38 @@ } } } - } + }, + "empty_state_v2": { + "customized_insights": { + "description": "分配给您的工作项将按状态分类显示在此处。", + "title": "暂无数据" + }, + "created_vs_resolved": { + "description": "随着时间推移创建和解决的工作项将显示在此处。", + "title": "暂无数据" + }, + "project_insights": { + "title": "暂无数据", + "description": "分配给您的工作项将按状态分类显示在此处。" + } + }, + "created_vs_resolved": "已创建 vs 已解决", + "customized_insights": "自定义洞察", + "backlog_work_items": "待办工作项", + "active_projects": "活跃项目", + "trend_on_charts": "图表趋势", + "all_projects": "所有项目", + "summary_of_projects": "项目概览", + "project_insights": "项目洞察", + "started_work_items": "已开始的工作项", + "total_work_items": "工作项总数", + "total_projects": "项目总数", + "total_admins": "管理员总数", + "total_users": "用户总数", + "total_intake": "总收入", + "un_started_work_items": "未开始的工作项", + "total_guests": "访客总数", + "completed_work_items": "已完成的工作项" }, "workspace_projects": { "label": "{count, plural, one {项目} other {项目}}", diff --git a/packages/i18n/src/locales/zh-TW/translations.json b/packages/i18n/src/locales/zh-TW/translations.json index 4246e5aed..ed49e1fe3 100644 --- a/packages/i18n/src/locales/zh-TW/translations.json +++ b/packages/i18n/src/locales/zh-TW/translations.json @@ -1313,7 +1313,38 @@ } } } - } + }, + "empty_state_v2": { + "customized_insights": { + "description": "指派給您的工作項目將依狀態分類顯示在此處。", + "title": "尚無資料" + }, + "created_vs_resolved": { + "description": "隨著時間推移所建立與解決的工作項目將顯示在此處。", + "title": "尚無資料" + }, + "project_insights": { + "title": "尚無資料", + "description": "指派給您的工作項目將依狀態分類顯示在此處。" + } + }, + "created_vs_resolved": "已建立 vs 已解決", + "customized_insights": "自訂化洞察", + "backlog_work_items": "待辦工作項目", + "active_projects": "啟用中的專案", + "trend_on_charts": "圖表趨勢", + "all_projects": "所有專案", + "summary_of_projects": "專案摘要", + "project_insights": "專案洞察", + "started_work_items": "已開始的工作項目", + "total_work_items": "工作項目總數", + "total_projects": "專案總數", + "total_admins": "管理員總數", + "total_users": "使用者總數", + "total_intake": "總收入", + "un_started_work_items": "未開始的工作項目", + "total_guests": "訪客總數", + "completed_work_items": "已完成的工作項目" }, "workspace_projects": { "label": "{count, plural, one {專案} other {專案}}", diff --git a/packages/propel/package.json b/packages/propel/package.json index 3522c2f64..7c1e96684 100644 --- a/packages/propel/package.json +++ b/packages/propel/package.json @@ -10,10 +10,12 @@ "exports": { "./ui/*": "./src/ui/*.tsx", "./charts/*": "./src/charts/*/index.ts", + "./table": "./src/table/index.ts", "./styles/fonts": "./src/styles/fonts/index.css" }, "dependencies": { "@radix-ui/react-slot": "^1.1.1", + "@tanstack/react-table": "^8.21.3", "class-variance-authority": "^0.7.1", "lucide-react": "^0.469.0", "react": "^18.3.1", @@ -29,4 +31,4 @@ "@types/react-dom": "18.3.0", "typescript": "^5.3.3" } -} +} \ No newline at end of file diff --git a/packages/propel/src/charts/area-chart/root.tsx b/packages/propel/src/charts/area-chart/root.tsx index 7d4e9e6ba..b90de27cf 100644 --- a/packages/propel/src/charts/area-chart/root.tsx +++ b/packages/propel/src/charts/area-chart/root.tsx @@ -29,13 +29,21 @@ export const AreaChart = React.memo((props: // states const [activeArea, setActiveArea] = useState(null); const [activeLegend, setActiveLegend] = useState(null); + // derived values - const itemKeys = useMemo(() => areas.map((area) => area.key), [areas]); - const itemLabels: Record = useMemo( - () => areas.reduce((acc, area) => ({ ...acc, [area.key]: area.label }), {}), - [areas] - ); - const itemDotColors = useMemo(() => areas.reduce((acc, area) => ({ ...acc, [area.key]: area.fill }), {}), [areas]); + const { itemKeys, itemLabels, itemDotColors } = useMemo(() => { + const keys: string[] = []; + const labels: Record = {}; + const colors: Record = {}; + + for (const area of areas) { + keys.push(area.key); + labels[area.key] = area.label; + colors[area.key] = area.fill; + } + + return { itemKeys: keys, itemLabels: labels, itemDotColors: colors }; + }, [areas]); const renderAreas = useMemo( () => @@ -77,7 +85,7 @@ export const AreaChart = React.memo((props: // get the last data point const lastPoint = data[data.length - 1]; // for the y-value in the last point, use its yAxis key value - const lastYValue = lastPoint[yAxis.key] || 0; + const lastYValue = lastPoint[yAxis.key] ?? 0; // create data for a straight line that has points at each x-axis position return data.map((item, index) => { // calculate the y value for this point on the straight line @@ -91,7 +99,6 @@ export const AreaChart = React.memo((props: }; }); }, [data, xAxis.key]); - return (
@@ -128,8 +135,8 @@ export const AreaChart = React.memo((props: value: yAxis.label, angle: -90, position: "bottom", - offset: -24, - dx: -16, + offset: yAxis.offset ?? -24, + dx: yAxis.dx ?? -16, className: AXIS_LABEL_CLASSNAME, } } diff --git a/packages/propel/src/charts/bar-chart/root.tsx b/packages/propel/src/charts/bar-chart/root.tsx index abe936d5c..e3dbe1d8c 100644 --- a/packages/propel/src/charts/bar-chart/root.tsx +++ b/packages/propel/src/charts/bar-chart/root.tsx @@ -40,13 +40,22 @@ export const BarChart = React.memo((props: T // states const [activeBar, setActiveBar] = useState(null); const [activeLegend, setActiveLegend] = useState(null); + // derived values - const stackKeys = useMemo(() => bars.map((bar) => bar.key), [bars]); - const stackLabels: Record = useMemo( - () => bars.reduce((acc, bar) => ({ ...acc, [bar.key]: bar.label }), {}), - [bars] - ); - const stackDotColors = useMemo(() => bars.reduce((acc, bar) => ({ ...acc, [bar.key]: bar.fill }), {}), [bars]); + const { stackKeys, stackLabels, stackDotColors } = useMemo(() => { + const keys: string[] = []; + const labels: Record = {}; + const colors: Record = {}; + + for (const bar of bars) { + keys.push(bar.key); + labels[bar.key] = bar.label; + // For tooltip, we need a string color. If fill is a function, use a default color + colors[bar.key] = typeof bar.fill === "function" ? "#000000" : bar.fill; + } + + return { stackKeys: keys, stackLabels: labels, stackDotColors: colors }; + }, [bars]); const renderBars = useMemo( () => @@ -102,7 +111,7 @@ export const BarChart = React.memo((props: T axisLine={false} label={{ value: xAxis.label, - dy: 28, + dy: xAxis.dy ?? 28, className: AXIS_LABEL_CLASSNAME, }} tickCount={tickCount.x} diff --git a/packages/propel/src/charts/components/legend.tsx b/packages/propel/src/charts/components/legend.tsx index 2be69c5cb..3c4558120 100644 --- a/packages/propel/src/charts/components/legend.tsx +++ b/packages/propel/src/charts/components/legend.tsx @@ -15,16 +15,17 @@ export const getLegendProps = (args: TChartLegend): LegendProps => { overflow: "hidden", ...(layout === "vertical" ? { - top: 0, - alignItems: "center", - height: "100%", - } + top: 0, + alignItems: "center", + height: "100%", + } : { - left: 0, - bottom: 0, - width: "100%", - justifyContent: "center", - }), + left: 0, + bottom: 0, + width: "100%", + justifyContent: "center", + }), + ...args.wrapperStyles, }, content: , }; @@ -33,8 +34,8 @@ export const getLegendProps = (args: TChartLegend): LegendProps => { const CustomLegend = React.forwardRef< HTMLDivElement, React.ComponentProps<"div"> & - Pick & - TChartLegend + Pick & + TChartLegend >((props, ref) => { const { formatter, layout, onClick, onMouseEnter, onMouseLeave, payload } = props; diff --git a/packages/propel/src/charts/components/tick.tsx b/packages/propel/src/charts/components/tick.tsx index e26e25ef3..4b64e8373 100644 --- a/packages/propel/src/charts/components/tick.tsx +++ b/packages/propel/src/charts/components/tick.tsx @@ -4,10 +4,10 @@ import React from "react"; // Common classnames const AXIS_TICK_CLASSNAME = "fill-custom-text-300 text-sm"; -export const CustomXAxisTick = React.memo(({ x, y, payload }: any) => ( +export const CustomXAxisTick = React.memo(({ x, y, payload, getLabel }: any) => ( - {payload.value} + {getLabel ? getLabel(payload.value) : payload.value} )); @@ -20,4 +20,28 @@ export const CustomYAxisTick = React.memo(({ x, y, payload }: any) => ( )); + CustomYAxisTick.displayName = "CustomYAxisTick"; + +export const CustomRadarAxisTick = React.memo( + ({ x, y, payload, getLabel, cx, cy, offset = 16 }: any) => { + // Calculate direction vector from center to tick + const dx = x - cx; + const dy = y - cy; + // Normalize and apply offset + const length = Math.sqrt(dx * dx + dy * dy); + const normX = dx / length; + const normY = dy / length; + const labelX = x + normX * offset; + const labelY = y + normY * offset; + + return ( + + + {getLabel ? getLabel(payload.value) : payload.value} + + + ); + } +); +CustomRadarAxisTick.displayName = "CustomRadarAxisTick"; diff --git a/packages/propel/src/charts/line-chart/root.tsx b/packages/propel/src/charts/line-chart/root.tsx index 6812797b7..f3b2ef72c 100644 --- a/packages/propel/src/charts/line-chart/root.tsx +++ b/packages/propel/src/charts/line-chart/root.tsx @@ -38,13 +38,21 @@ export const LineChart = React.memo((props: // states const [activeLine, setActiveLine] = useState(null); const [activeLegend, setActiveLegend] = useState(null); + // derived values - const itemKeys = useMemo(() => lines.map((line) => line.key), [lines]); - const itemLabels: Record = useMemo( - () => lines.reduce((acc, line) => ({ ...acc, [line.key]: line.label }), {}), - [lines] - ); - const itemDotColors = useMemo(() => lines.reduce((acc, line) => ({ ...acc, [line.key]: line.stroke }), {}), [lines]); + const { itemKeys, itemLabels, itemDotColors } = useMemo(() => { + const keys: string[] = []; + const labels: Record = {}; + const colors: Record = {}; + + for (const line of lines) { + keys.push(line.key); + labels[line.key] = line.label; + colors[line.key] = line.stroke; + } + + return { itemKeys: keys, itemLabels: labels, itemDotColors: colors }; + }, [lines]); const renderLines = useMemo( () => diff --git a/packages/propel/src/charts/radar-chart/index.ts b/packages/propel/src/charts/radar-chart/index.ts new file mode 100644 index 000000000..50a9c47c0 --- /dev/null +++ b/packages/propel/src/charts/radar-chart/index.ts @@ -0,0 +1 @@ +export * from "./root"; \ No newline at end of file diff --git a/packages/propel/src/charts/radar-chart/root.tsx b/packages/propel/src/charts/radar-chart/root.tsx new file mode 100644 index 000000000..b8a1b95d7 --- /dev/null +++ b/packages/propel/src/charts/radar-chart/root.tsx @@ -0,0 +1,95 @@ +import { useMemo, useState } from "react"; +import { + PolarGrid, + Radar, + RadarChart as CoreRadarChart, + ResponsiveContainer, + PolarAngleAxis, + Tooltip, + Legend, +} from "recharts"; +import { TRadarChartProps } from "@plane/types"; +import { getLegendProps } from "../components/legend"; +import { CustomRadarAxisTick } from "../components/tick"; +import { CustomTooltip } from "../components/tooltip"; + +const RadarChart = (props: TRadarChartProps) => { + const { data, radars, margin, showTooltip, legend, className, angleAxis } = props; + + // states + const [, setActiveIndex] = useState(null); + const [activeLegend, setActiveLegend] = useState(null); + + const { itemKeys, itemLabels, itemDotColors } = useMemo(() => { + const keys: string[] = []; + const labels: Record = {}; + const colors: Record = {}; + + for (const radar of radars) { + keys.push(radar.key); + labels[radar.key] = radar.name; + colors[radar.key] = radar.stroke ?? radar.fill ?? "#000000"; + } + return { itemKeys: keys, itemLabels: labels, itemDotColors: colors }; + }, [radars]); + + return ( +
+ + + + } /> + {showTooltip && ( + ( + + )} + /> + )} + {legend && ( + // @ts-expect-error recharts types are not up to date + { + // @ts-expect-error recharts types are not up to date + const key: string | undefined = payload.payload?.key; + if (!key) return; + setActiveLegend(key); + setActiveIndex(null); + }} + onMouseLeave={() => setActiveLegend(null)} + {...getLegendProps(legend)} + /> + )} + {radars.map((radar) => ( + + ))} + + +
+ ); +}; + +export { RadarChart }; diff --git a/packages/propel/src/charts/scatter-chart/index.ts b/packages/propel/src/charts/scatter-chart/index.ts new file mode 100644 index 000000000..50a9c47c0 --- /dev/null +++ b/packages/propel/src/charts/scatter-chart/index.ts @@ -0,0 +1 @@ +export * from "./root"; \ No newline at end of file diff --git a/packages/propel/src/charts/scatter-chart/root.tsx b/packages/propel/src/charts/scatter-chart/root.tsx new file mode 100644 index 000000000..d7996c990 --- /dev/null +++ b/packages/propel/src/charts/scatter-chart/root.tsx @@ -0,0 +1,155 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +"use client"; + +import React, { useMemo, useState } from "react"; +import { + CartesianGrid, + ScatterChart as CoreScatterChart, + Legend, + Scatter, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from "recharts"; +// plane imports +import { AXIS_LABEL_CLASSNAME } from "@plane/constants"; +import { TScatterChartProps } from "@plane/types"; +// local components +import { getLegendProps } from "../components/legend"; +import { CustomXAxisTick, CustomYAxisTick } from "../components/tick"; +import { CustomTooltip } from "../components/tooltip"; + +export const ScatterChart = React.memo((props: TScatterChartProps) => { + const { + data, + scatterPoints, + margin, + xAxis, + yAxis, + + className, + tickCount = { + x: undefined, + y: 10, + }, + legend, + showTooltip = true, + } = props; + // states + const [activePoint, setActivePoint] = useState(null); + const [activeLegend, setActiveLegend] = useState(null); + + //derived values + const { itemKeys, itemLabels, itemDotColors } = useMemo(() => { + const keys: string[] = []; + const labels: Record = {}; + const colors: Record = {}; + + for (const point of scatterPoints) { + keys.push(point.key); + labels[point.key] = point.label; + colors[point.key] = point.fill; + } + return { itemKeys: keys, itemLabels: labels, itemDotColors: colors }; + }, [scatterPoints]); + + const renderPoints = useMemo( + () => + scatterPoints.map((point) => ( + setActivePoint(point.key)} + onMouseLeave={() => setActivePoint(null)} + /> + )), + [activeLegend, scatterPoints] + ); + + return ( +
+ + + + } + tickLine={false} + axisLine={false} + label={ + xAxis.label && { + value: xAxis.label, + dy: 28, + className: AXIS_LABEL_CLASSNAME, + } + } + tickCount={tickCount.x} + /> + } + tickCount={tickCount.y} + allowDecimals={!!yAxis.allowDecimals} + /> + {legend && ( + // @ts-expect-error recharts types are not up to date + setActiveLegend(payload.value)} + onMouseLeave={() => setActiveLegend(null)} + formatter={(value) => itemLabels[value]} + {...getLegendProps(legend)} + /> + )} + {showTooltip && ( + ( + + )} + /> + )} + {renderPoints} + + +
+ ); +}); +ScatterChart.displayName = "ScatterChart"; diff --git a/packages/propel/src/table/core.tsx b/packages/propel/src/table/core.tsx new file mode 100644 index 000000000..e6e7ad59c --- /dev/null +++ b/packages/propel/src/table/core.tsx @@ -0,0 +1,120 @@ +import * as React from "react" + +import { cn } from "@plane/utils" + +const Table = React.forwardRef< + HTMLTableElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+ + +)) +Table.displayName = "Table" + +const TableHeader = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableHeader.displayName = "TableHeader" + +const TableBody = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableBody.displayName = "TableBody" + +const TableFooter = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableFooter.displayName = "TableFooter" + +const TableRow = React.forwardRef< + HTMLTableRowElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableRow.displayName = "TableRow" + +const TableHead = React.forwardRef< + HTMLTableHeaderCellElement, + React.ThHTMLAttributes +>(({ className, ...props }, ref) => ( + From 5226b17f90e25db01f387a41fa0c067e4af75c93 Mon Sep 17 00:00:00 2001 From: JayashTripathy <76092296+JayashTripathy@users.noreply.github.com> Date: Fri, 23 May 2025 15:05:37 +0530 Subject: [PATCH 071/201] [WEB-4159] feat: add 'restricted_entity' translation key across multiple languages #7106 --- packages/i18n/src/locales/cs/translations.json | 3 ++- packages/i18n/src/locales/de/translations.json | 3 ++- packages/i18n/src/locales/en/translations.json | 3 ++- packages/i18n/src/locales/es/translations.json | 3 ++- packages/i18n/src/locales/fr/translations.json | 3 ++- packages/i18n/src/locales/id/translations.json | 3 ++- packages/i18n/src/locales/it/translations.json | 3 ++- packages/i18n/src/locales/ja/translations.json | 3 ++- packages/i18n/src/locales/ko/translations.json | 3 ++- packages/i18n/src/locales/pl/translations.json | 3 ++- packages/i18n/src/locales/pt-BR/translations.json | 3 ++- packages/i18n/src/locales/ro/translations.json | 3 ++- packages/i18n/src/locales/ru/translations.json | 3 ++- packages/i18n/src/locales/sk/translations.json | 3 ++- packages/i18n/src/locales/tr-TR/translations.json | 3 ++- packages/i18n/src/locales/ua/translations.json | 5 +++-- packages/i18n/src/locales/vi-VN/translations.json | 3 ++- packages/i18n/src/locales/zh-CN/translations.json | 3 ++- packages/i18n/src/locales/zh-TW/translations.json | 3 ++- 19 files changed, 39 insertions(+), 20 deletions(-) diff --git a/packages/i18n/src/locales/cs/translations.json b/packages/i18n/src/locales/cs/translations.json index 4aa64f40d..f41ae9021 100644 --- a/packages/i18n/src/locales/cs/translations.json +++ b/packages/i18n/src/locales/cs/translations.json @@ -746,7 +746,8 @@ "message": "Něco se pokazilo. Zkuste to prosím znovu." }, "required": "Toto pole je povinné", - "entity_required": "{entity} je povinná" + "entity_required": "{entity} je povinná", + "restricted_entity": "{entity} je omezen" }, "update_link": "Aktualizovat odkaz", "attach": "Připojit", diff --git a/packages/i18n/src/locales/de/translations.json b/packages/i18n/src/locales/de/translations.json index 1fa8eaa0e..857a08a8a 100644 --- a/packages/i18n/src/locales/de/translations.json +++ b/packages/i18n/src/locales/de/translations.json @@ -746,7 +746,8 @@ "message": "Etwas ist schiefgelaufen. Bitte versuchen Sie es erneut." }, "required": "Dieses Feld ist erforderlich", - "entity_required": "{entity} ist erforderlich" + "entity_required": "{entity} ist erforderlich", + "restricted_entity": "{entity} ist eingeschränkt" }, "update_link": "Link aktualisieren", "attach": "Anhängen", diff --git a/packages/i18n/src/locales/en/translations.json b/packages/i18n/src/locales/en/translations.json index ef16944ef..e6202deb6 100644 --- a/packages/i18n/src/locales/en/translations.json +++ b/packages/i18n/src/locales/en/translations.json @@ -580,7 +580,8 @@ "message": "Something went wrong. Please try again." }, "required": "This field is required", - "entity_required": "{entity} is required" + "entity_required": "{entity} is required", + "restricted_entity": "{entity} is restricted" }, "update_link": "Update link", "attach": "Attach", diff --git a/packages/i18n/src/locales/es/translations.json b/packages/i18n/src/locales/es/translations.json index 966e3178d..c8f258385 100644 --- a/packages/i18n/src/locales/es/translations.json +++ b/packages/i18n/src/locales/es/translations.json @@ -750,7 +750,8 @@ "message": "Algo salió mal. Por favor, inténtalo de nuevo." }, "required": "Este campo es obligatorio", - "entity_required": "{entity} es obligatorio" + "entity_required": "{entity} es obligatorio", + "restricted_entity": "{entity} está restringido" }, "update_link": "Actualizar enlace", "attach": "Adjuntar", diff --git a/packages/i18n/src/locales/fr/translations.json b/packages/i18n/src/locales/fr/translations.json index 5188b3334..a92d64daa 100644 --- a/packages/i18n/src/locales/fr/translations.json +++ b/packages/i18n/src/locales/fr/translations.json @@ -748,7 +748,8 @@ "message": "Quelque chose s'est mal passé. Veuillez réessayer." }, "required": "Ce champ est obligatoire", - "entity_required": "{entity} est requis" + "entity_required": "{entity} est requis", + "restricted_entity": "{entity} est restreint" }, "update_link": "Mettre à jour le lien", "attach": "Joindre", diff --git a/packages/i18n/src/locales/id/translations.json b/packages/i18n/src/locales/id/translations.json index 3a6c92873..f1fb2a827 100644 --- a/packages/i18n/src/locales/id/translations.json +++ b/packages/i18n/src/locales/id/translations.json @@ -746,7 +746,8 @@ "message": "Sesuatu telah salah. Silakan coba lagi." }, "required": "Bidang ini diperlukan", - "entity_required": "{entity} diperlukan" + "entity_required": "{entity} diperlukan", + "restricted_entity": "{entity} dibatasi" }, "update_link": "Perbarui tautan", "attach": "Lampirkan", diff --git a/packages/i18n/src/locales/it/translations.json b/packages/i18n/src/locales/it/translations.json index ff58fee31..1311c625e 100644 --- a/packages/i18n/src/locales/it/translations.json +++ b/packages/i18n/src/locales/it/translations.json @@ -743,7 +743,8 @@ "message": "Qualcosa è andato storto. Per favore, riprova." }, "required": "Questo campo è obbligatorio", - "entity_required": "{entity} è obbligatorio" + "entity_required": "{entity} è obbligatorio", + "restricted_entity": "{entity} è limitato" }, "update_link": "Aggiorna link", "attach": "Allega", diff --git a/packages/i18n/src/locales/ja/translations.json b/packages/i18n/src/locales/ja/translations.json index 9656f0439..70bcd1c8f 100644 --- a/packages/i18n/src/locales/ja/translations.json +++ b/packages/i18n/src/locales/ja/translations.json @@ -748,7 +748,8 @@ "message": "問題が発生しました。もう一度お試しください。" }, "required": "この項目は必須です", - "entity_required": "{entity}は必須です" + "entity_required": "{entity}は必須です", + "restricted_entity": "{entity} は制限されています" }, "update_link": "リンクを更新", "attach": "添付", diff --git a/packages/i18n/src/locales/ko/translations.json b/packages/i18n/src/locales/ko/translations.json index eb9b97bf4..eee2ffbf0 100644 --- a/packages/i18n/src/locales/ko/translations.json +++ b/packages/i18n/src/locales/ko/translations.json @@ -748,7 +748,8 @@ "message": "문제가 발생했습니다. 다시 시도해주세요." }, "required": "이 필드는 필수입니다", - "entity_required": "{entity}가 필요합니다" + "entity_required": "{entity}가 필요합니다", + "restricted_entity": "{entity}은(는) 제한되어 있습니다" }, "update_link": "링크 업데이트", "attach": "첨부", diff --git a/packages/i18n/src/locales/pl/translations.json b/packages/i18n/src/locales/pl/translations.json index 28290e3d0..7ff792a26 100644 --- a/packages/i18n/src/locales/pl/translations.json +++ b/packages/i18n/src/locales/pl/translations.json @@ -748,7 +748,8 @@ "message": "Coś poszło nie tak. Spróbuj ponownie." }, "required": "To pole jest wymagane", - "entity_required": "{entity} jest wymagane" + "entity_required": "{entity} jest wymagane", + "restricted_entity": "{entity} jest ograniczony" }, "update_link": "Zaktualizuj link", "attach": "Dołącz", diff --git a/packages/i18n/src/locales/pt-BR/translations.json b/packages/i18n/src/locales/pt-BR/translations.json index 6b31fcbf4..a5eb08a86 100644 --- a/packages/i18n/src/locales/pt-BR/translations.json +++ b/packages/i18n/src/locales/pt-BR/translations.json @@ -748,7 +748,8 @@ "message": "Algo deu errado. Por favor, tente novamente." }, "required": "Este campo é obrigatório", - "entity_required": "{entity} é obrigatório" + "entity_required": "{entity} é obrigatório", + "restricted_entity": "{entity} está restrito" }, "update_link": "Atualizar link", "attach": "Anexar", diff --git a/packages/i18n/src/locales/ro/translations.json b/packages/i18n/src/locales/ro/translations.json index 704ee840f..a88ef0609 100644 --- a/packages/i18n/src/locales/ro/translations.json +++ b/packages/i18n/src/locales/ro/translations.json @@ -746,7 +746,8 @@ "message": "Ceva a funcționat greșit. Te rugăm să încerci din nou." }, "required": "Acest câmp este obligatoriu", - "entity_required": "{entity} este obligatoriu" + "entity_required": "{entity} este obligatoriu", + "restricted_entity": "{entity} este restricționat" }, "update_link": "Actualizează link-ul", "attach": "Atașează", diff --git a/packages/i18n/src/locales/ru/translations.json b/packages/i18n/src/locales/ru/translations.json index f1a9659e3..4159cccd3 100644 --- a/packages/i18n/src/locales/ru/translations.json +++ b/packages/i18n/src/locales/ru/translations.json @@ -748,7 +748,8 @@ "message": "Что-то пошло не так. Попробуйте позже." }, "required": "Это поле обязательно", - "entity_required": "{entity} обязательно" + "entity_required": "{entity} обязательно", + "restricted_entity": "{entity} ограничен" }, "update_link": "обновить ссылку", "attach": "Прикрепить", diff --git a/packages/i18n/src/locales/sk/translations.json b/packages/i18n/src/locales/sk/translations.json index 0aa8f4f84..f7093e59c 100644 --- a/packages/i18n/src/locales/sk/translations.json +++ b/packages/i18n/src/locales/sk/translations.json @@ -748,7 +748,8 @@ "message": "Niečo sa pokazilo. Skúste to prosím znova." }, "required": "Toto pole je povinné", - "entity_required": "{entity} je povinná" + "entity_required": "{entity} je povinná", + "restricted_entity": "{entity} je obmedzený" }, "update_link": "Aktualizovať odkaz", "attach": "Pripojiť", diff --git a/packages/i18n/src/locales/tr-TR/translations.json b/packages/i18n/src/locales/tr-TR/translations.json index 7d4cde25d..de2cb9d7f 100644 --- a/packages/i18n/src/locales/tr-TR/translations.json +++ b/packages/i18n/src/locales/tr-TR/translations.json @@ -748,7 +748,8 @@ "message": "Bir hata oluştu. Lütfen tekrar deneyin." }, "required": "Bu alan gereklidir", - "entity_required": "{entity} gereklidir" + "entity_required": "{entity} gereklidir", + "restricted_entity": "{entity} kısıtlanmıştır" }, "update_link": "Bağlantıyı güncelle", "attach": "Ekle", diff --git a/packages/i18n/src/locales/ua/translations.json b/packages/i18n/src/locales/ua/translations.json index 841dbf803..9be16b11d 100644 --- a/packages/i18n/src/locales/ua/translations.json +++ b/packages/i18n/src/locales/ua/translations.json @@ -747,8 +747,9 @@ "title": "Помилка!", "message": "Щось пішло не так. Будь ласка, спробуйте ще раз." }, - "required": "Це поле є обов’язковим", - "entity_required": "{entity} є обов’язковим" + "required": "Це поле є обов'язковим", + "entity_required": "{entity} є обов'язковим", + "restricted_entity": "{entity} обмежено" }, "update_link": "Оновити посилання", "attach": "Прикріпити", diff --git a/packages/i18n/src/locales/vi-VN/translations.json b/packages/i18n/src/locales/vi-VN/translations.json index de2c722eb..756eea3d9 100644 --- a/packages/i18n/src/locales/vi-VN/translations.json +++ b/packages/i18n/src/locales/vi-VN/translations.json @@ -748,7 +748,8 @@ "message": "Đã xảy ra lỗi. Vui lòng thử lại." }, "required": "Trường này là bắt buộc", - "entity_required": "{entity} là bắt buộc" + "entity_required": "{entity} là bắt buộc", + "restricted_entity": "{entity} bị hạn chế" }, "update_link": "Cập nhật liên kết", "attach": "Đính kèm", diff --git a/packages/i18n/src/locales/zh-CN/translations.json b/packages/i18n/src/locales/zh-CN/translations.json index d3e3e5998..8dbdb5523 100644 --- a/packages/i18n/src/locales/zh-CN/translations.json +++ b/packages/i18n/src/locales/zh-CN/translations.json @@ -748,7 +748,8 @@ "message": "发生错误。请重试。" }, "required": "此字段为必填项", - "entity_required": "{entity}为必填项" + "entity_required": "{entity}为必填项", + "restricted_entity": "{entity}已被限制" }, "update_link": "更新链接", "attach": "附加", diff --git a/packages/i18n/src/locales/zh-TW/translations.json b/packages/i18n/src/locales/zh-TW/translations.json index ed49e1fe3..53888c516 100644 --- a/packages/i18n/src/locales/zh-TW/translations.json +++ b/packages/i18n/src/locales/zh-TW/translations.json @@ -748,7 +748,8 @@ "message": "發生錯誤。請再試一次。" }, "required": "此欄位為必填", - "entity_required": "{entity} 為必填" + "entity_required": "{entity} 為必填", + "restricted_entity": "{entity}已被限制" }, "update_link": "更新連結", "attach": "附加", From 9812129ad3cb4ac972794786e1c9f3147d1929da Mon Sep 17 00:00:00 2001 From: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com> Date: Fri, 23 May 2025 15:05:57 +0530 Subject: [PATCH 072/201] [WEB-4133] chore: optimised the analytics endpoints (#7105) * chore: optimised the analytics endpoints * chore: segregated peek view endpoints * chore: added analytics values validation * chore: added project validation * chore: reverted the changes --------- Co-authored-by: JayashTripathy --- apiserver/plane/app/urls/analytic.py | 18 + apiserver/plane/app/views/__init__.py | 6 + apiserver/plane/app/views/analytic/advance.py | 338 +++----------- .../app/views/analytic/project_analytics.py | 421 ++++++++++++++++++ .../overview/project-insights.tsx | 5 +- .../analytics-v2/total-insights.tsx | 18 +- .../work-items/created-vs-resolved.tsx | 5 +- .../work-items/priority-chart.tsx | 20 +- .../work-items/workitems-insight-table.tsx | 18 +- web/core/services/analytics-v2.service.ts | 57 ++- 10 files changed, 599 insertions(+), 307 deletions(-) create mode 100644 apiserver/plane/app/views/analytic/project_analytics.py diff --git a/apiserver/plane/app/urls/analytic.py b/apiserver/plane/app/urls/analytic.py index 0eebd3108..3e4172771 100644 --- a/apiserver/plane/app/urls/analytic.py +++ b/apiserver/plane/app/urls/analytic.py @@ -11,6 +11,9 @@ from plane.app.views import ( AdvanceAnalyticsChartEndpoint, DefaultAnalyticsEndpoint, ProjectStatsEndpoint, + ProjectAdvanceAnalyticsEndpoint, + ProjectAdvanceAnalyticsStatsEndpoint, + ProjectAdvanceAnalyticsChartEndpoint, ) @@ -67,4 +70,19 @@ urlpatterns = [ AdvanceAnalyticsChartEndpoint.as_view(), name="advance-analytics-chart", ), + path( + "workspaces//projects//advance-analytics/", + ProjectAdvanceAnalyticsEndpoint.as_view(), + name="project-advance-analytics", + ), + path( + "workspaces//projects//advance-analytics-stats/", + ProjectAdvanceAnalyticsStatsEndpoint.as_view(), + name="project-advance-analytics-stats", + ), + path( + "workspaces//projects//advance-analytics-charts/", + ProjectAdvanceAnalyticsChartEndpoint.as_view(), + name="project-advance-analytics-chart", + ), ] diff --git a/apiserver/plane/app/views/__init__.py b/apiserver/plane/app/views/__init__.py index 2034c5548..98dcab84f 100644 --- a/apiserver/plane/app/views/__init__.py +++ b/apiserver/plane/app/views/__init__.py @@ -205,6 +205,12 @@ from .analytic.advance import ( AdvanceAnalyticsChartEndpoint, ) +from .analytic.project_analytics import ( + ProjectAdvanceAnalyticsEndpoint, + ProjectAdvanceAnalyticsStatsEndpoint, + ProjectAdvanceAnalyticsChartEndpoint, +) + from .notification.base import ( NotificationViewSet, UnreadNotificationEndpoint, diff --git a/apiserver/plane/app/views/analytic/advance.py b/apiserver/plane/app/views/analytic/advance.py index c55f5566b..a79ab98c5 100644 --- a/apiserver/plane/app/views/analytic/advance.py +++ b/apiserver/plane/app/views/analytic/advance.py @@ -5,7 +5,6 @@ from django.db.models import QuerySet, Q, Count from django.http import HttpRequest from django.db.models.functions import TruncMonth from django.utils import timezone -from datetime import timedelta from plane.app.views.base import BaseAPIView from plane.app.permissions import ROLE, allow_permission from plane.db.models import ( @@ -17,12 +16,7 @@ from plane.db.models import ( IssueView, ProjectPage, Workspace, - CycleIssue, - ModuleIssue, ) -from django.db import models -from django.db.models import F, Case, When, Value -from django.db.models.functions import Concat from plane.utils.build_chart import build_analytics_chart from plane.utils.date_utils import ( get_analytics_filters, @@ -118,25 +112,8 @@ class AdvanceAnalyticsEndpoint(AdvanceAnalyticsBaseView): ), } - def get_work_items_stats( - self, cycle_id=None, module_id=None - ) -> Dict[str, Dict[str, int]]: - """ - Returns work item stats for the workspace, or filtered by cycle_id or module_id if provided. - """ - base_queryset = None - if cycle_id is not None: - cycle_issues = CycleIssue.objects.filter( - **self.filters["base_filters"], cycle_id=cycle_id - ).values_list("issue_id", flat=True) - base_queryset = Issue.issue_objects.filter(id__in=cycle_issues) - elif module_id is not None: - module_issues = ModuleIssue.objects.filter( - **self.filters["base_filters"], module_id=module_id - ).values_list("issue_id", flat=True) - base_queryset = Issue.issue_objects.filter(id__in=module_issues) - else: - base_queryset = Issue.issue_objects.filter(**self.filters["base_filters"]) + def get_work_items_stats(self) -> Dict[str, Dict[str, int]]: + base_queryset = Issue.issue_objects.filter(**self.filters["base_filters"]) return { "total_work_items": self.get_filtered_counts(base_queryset), @@ -165,11 +142,8 @@ class AdvanceAnalyticsEndpoint(AdvanceAnalyticsBaseView): status=status.HTTP_200_OK, ) elif tab == "work-items": - # Optionally accept cycle_id or module_id as query params - cycle_id = request.GET.get("cycle_id", None) - module_id = request.GET.get("module_id", None) return Response( - self.get_work_items_stats(cycle_id=cycle_id, module_id=module_id), + self.get_work_items_stats(), status=status.HTTP_200_OK, ) return Response({"message": "Invalid tab"}, status=status.HTTP_400_BAD_REQUEST) @@ -188,7 +162,21 @@ class AdvanceAnalyticsStatsEndpoint(AdvanceAnalyticsBaseView): ) return ( - base_queryset.values("project_id", "project__name") + base_queryset.values("project_id", "project__name").annotate( + cancelled_work_items=Count("id", filter=Q(state__group="cancelled")), + completed_work_items=Count("id", filter=Q(state__group="completed")), + backlog_work_items=Count("id", filter=Q(state__group="backlog")), + un_started_work_items=Count("id", filter=Q(state__group="unstarted")), + started_work_items=Count("id", filter=Q(state__group="started")), + ) + .order_by("project_id") + ) + + def get_work_items_stats(self) -> Dict[str, Dict[str, int]]: + base_queryset = Issue.issue_objects.filter(**self.filters["base_filters"]) + return ( + base_queryset + .values("project_id", "project__name") .annotate( cancelled_work_items=Count("id", filter=Q(state__group="cancelled")), completed_work_items=Count("id", filter=Q(state__group="completed")), @@ -199,100 +187,14 @@ class AdvanceAnalyticsStatsEndpoint(AdvanceAnalyticsBaseView): .order_by("project_id") ) - def get_work_items_stats( - self, cycle_id=None, module_id=None, peek_view=False - ) -> Dict[str, Dict[str, int]]: - base_queryset = None - if cycle_id is not None: - cycle_issues = CycleIssue.objects.filter( - **self.filters["base_filters"], cycle_id=cycle_id - ).values_list("issue_id", flat=True) - base_queryset = Issue.issue_objects.filter(id__in=cycle_issues) - elif module_id is not None: - module_issues = ModuleIssue.objects.filter( - **self.filters["base_filters"], module_id=module_id - ).values_list("issue_id", flat=True) - base_queryset = Issue.issue_objects.filter(id__in=module_issues) - elif peek_view: - base_queryset = Issue.issue_objects.filter(**self.filters["base_filters"]) - else: - base_queryset = Issue.issue_objects.filter(**self.filters["base_filters"]) - return ( - base_queryset.values("project_id", "project__name") - .annotate( - cancelled_work_items=Count( - "id", filter=Q(state__group="cancelled") - ), - completed_work_items=Count( - "id", filter=Q(state__group="completed") - ), - backlog_work_items=Count("id", filter=Q(state__group="backlog")), - un_started_work_items=Count( - "id", filter=Q(state__group="unstarted") - ), - started_work_items=Count("id", filter=Q(state__group="started")), - ) - .order_by("project_id") - ) - - return ( - base_queryset.annotate(display_name=F("assignees__display_name")) - .annotate(assignee_id=F("assignees__id")) - .annotate(avatar=F("assignees__avatar")) - .annotate( - avatar_url=Case( - # If `avatar_asset` exists, use it to generate the asset URL - When( - assignees__avatar_asset__isnull=False, - then=Concat( - Value("/api/assets/v2/static/"), - "assignees__avatar_asset", # Assuming avatar_asset has an id or relevant field - Value("/"), - ), - ), - # If `avatar_asset` is None, fall back to using `avatar` field directly - When( - assignees__avatar_asset__isnull=True, then="assignees__avatar" - ), - default=Value(None), - output_field=models.CharField(), - ) - ) - .values("display_name", "assignee_id", "avatar_url") - .annotate( - cancelled_work_items=Count( - "id", filter=Q(state__group="cancelled"), distinct=True - ), - completed_work_items=Count( - "id", filter=Q(state__group="completed"), distinct=True - ), - backlog_work_items=Count( - "id", filter=Q(state__group="backlog"), distinct=True - ), - un_started_work_items=Count( - "id", filter=Q(state__group="unstarted"), distinct=True - ), - started_work_items=Count( - "id", filter=Q(state__group="started"), distinct=True - ), - ) - .order_by("display_name") - ) - @allow_permission([ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE") def get(self, request: HttpRequest, slug: str) -> Response: self.initialize_workspace(slug, type="chart") type = request.GET.get("type", "work-items") if type == "work-items": - # Optionally accept cycle_id or module_id as query params - cycle_id = request.GET.get("cycle_id", None) - module_id = request.GET.get("module_id", None) - peek_view = request.GET.get("peek_view", False) return Response( - self.get_work_items_stats( - cycle_id=cycle_id, module_id=module_id, peek_view=peek_view - ), + self.get_work_items_stats(), status=status.HTTP_200_OK, ) @@ -352,9 +254,7 @@ class AdvanceAnalyticsChartEndpoint(AdvanceAnalyticsBaseView): for key, value in data.items() ] - def work_item_completion_chart( - self, cycle_id=None, module_id=None, peek_view=False - ) -> Dict[str, Any]: + def work_item_completion_chart(self) -> Dict[str, Any]: # Get the base queryset queryset = ( Issue.issue_objects.filter(**self.filters["base_filters"]) @@ -364,143 +264,62 @@ class AdvanceAnalyticsChartEndpoint(AdvanceAnalyticsBaseView): ) ) - if cycle_id is not None and peek_view: - cycle_issues = CycleIssue.objects.filter( - **self.filters["base_filters"], cycle_id=cycle_id - ).values_list("issue_id", flat=True) - cycle = Cycle.objects.filter(id=cycle_id).first() - if cycle and cycle.start_date: - start_date = cycle.start_date.date() - end_date = cycle.end_date.date() - else: - return {"data": [], "schema": {}} - queryset = cycle_issues - elif module_id is not None and peek_view: - module_issues = ModuleIssue.objects.filter( - **self.filters["base_filters"], module_id=module_id - ).values_list("issue_id", flat=True) - module = Module.objects.filter(id=module_id).first() - if module and module.start_date: - start_date = module.start_date - end_date = module.target_date - else: - return {"data": [], "schema": {}} - queryset = module_issues - elif peek_view: - project_ids_str = self.request.GET.get("project_ids") - if project_ids_str: - project_id_list = [ - pid.strip() for pid in project_ids_str.split(",") if pid.strip() - ] - else: - project_id_list = [] - return {"data": [], "schema": {}} - project_id = project_id_list[0] - project = Project.objects.filter(id=project_id).first() - if project.created_at: - start_date = project.created_at.date().replace(day=1) - else: - return {"data": [], "schema": {}} - else: - workspace = Workspace.objects.get(slug=self._workspace_slug) - start_date = workspace.created_at.date().replace(day=1) + workspace = Workspace.objects.get(slug=self._workspace_slug) + start_date = workspace.created_at.date().replace(day=1) - if cycle_id or module_id: - # Get daily stats with optimized query - daily_stats = ( - queryset.values("created_at__date") - .annotate( - created_count=Count("id"), - completed_count=Count( - "id", filter=Q(issue__state__group="completed") - ), - ) - .order_by("created_at__date") + # Apply date range filter if available + if self.filters["chart_period_range"]: + start_date, end_date = self.filters["chart_period_range"] + queryset = queryset.filter( + created_at__date__gte=start_date, created_at__date__lte=end_date ) - # Create a dictionary of existing stats with summed counts - stats_dict = { - stat["created_at__date"].strftime("%Y-%m-%d"): { - "created_count": stat["created_count"], - "completed_count": stat["completed_count"], - } - for stat in daily_stats - } - - # Generate data for all days in the range - data = [] - current_date = start_date - while current_date <= end_date: - date_str = current_date.strftime("%Y-%m-%d") - stats = stats_dict.get( - date_str, {"created_count": 0, "completed_count": 0} - ) - data.append( - { - "key": date_str, - "name": date_str, - "count": stats["created_count"] + stats["completed_count"], - "completed_issues": stats["completed_count"], - "created_issues": stats["created_count"], - } - ) - current_date += timedelta(days=1) - else: - # Apply date range filter if available - if self.filters["chart_period_range"]: - start_date, end_date = self.filters["chart_period_range"] - queryset = queryset.filter( - created_at__date__gte=start_date, created_at__date__lte=end_date - ) - - # Annotate by month and count - monthly_stats = ( - queryset.annotate(month=TruncMonth("created_at")) - .values("month") - .annotate( - created_count=Count("id"), - completed_count=Count("id", filter=Q(state__group="completed")), - ) - .order_by("month") + # Annotate by month and count + monthly_stats = ( + queryset.annotate(month=TruncMonth("created_at")) + .values("month") + .annotate( + created_count=Count("id"), + completed_count=Count("id", filter=Q(state__group="completed")), ) + .order_by("month") + ) - # Create dictionary of month -> counts - stats_dict = { - stat["month"].strftime("%Y-%m-%d"): { - "created_count": stat["created_count"], - "completed_count": stat["completed_count"], - } - for stat in monthly_stats + # Create dictionary of month -> counts + stats_dict = { + stat["month"].strftime("%Y-%m-%d"): { + "created_count": stat["created_count"], + "completed_count": stat["completed_count"], } + for stat in monthly_stats + } - # Generate monthly data (ensure months with 0 count are included) - data = [] - # include the current date at the end - end_date = timezone.now().date() - last_month = end_date.replace(day=1) - current_month = start_date + # Generate monthly data (ensure months with 0 count are included) + data = [] + # include the current date at the end + end_date = timezone.now().date() + last_month = end_date.replace(day=1) + current_month = start_date - while current_month <= last_month: - date_str = current_month.strftime("%Y-%m-%d") - stats = stats_dict.get( - date_str, {"created_count": 0, "completed_count": 0} + while current_month <= last_month: + date_str = current_month.strftime("%Y-%m-%d") + stats = stats_dict.get(date_str, {"created_count": 0, "completed_count": 0}) + data.append( + { + "key": date_str, + "name": date_str, + "count": stats["created_count"], + "completed_issues": stats["completed_count"], + "created_issues": stats["created_count"], + } + ) + # Move to next month + if current_month.month == 12: + current_month = current_month.replace( + year=current_month.year + 1, month=1 ) - data.append( - { - "key": date_str, - "name": date_str, - "count": stats["created_count"], - "completed_issues": stats["completed_count"], - "created_issues": stats["created_count"], - } - ) - # Move to next month - if current_month.month == 12: - current_month = current_month.replace( - year=current_month.year + 1, month=1 - ) - else: - current_month = current_month.replace(month=current_month.month + 1) + else: + current_month = current_month.replace(month=current_month.month + 1) schema = { "completed_issues": "completed_issues", @@ -515,8 +334,6 @@ class AdvanceAnalyticsChartEndpoint(AdvanceAnalyticsBaseView): type = request.GET.get("type", "projects") group_by = request.GET.get("group_by", None) x_axis = request.GET.get("x_axis", "PRIORITY") - cycle_id = request.GET.get("cycle_id", None) - module_id = request.GET.get("module_id", None) if type == "projects": return Response(self.project_chart(), status=status.HTTP_200_OK) @@ -530,19 +347,6 @@ class AdvanceAnalyticsChartEndpoint(AdvanceAnalyticsBaseView): ) ) - # Apply cycle/module filters if present - if cycle_id is not None: - cycle_issues = CycleIssue.objects.filter( - **self.filters["base_filters"], cycle_id=cycle_id - ).values_list("issue_id", flat=True) - queryset = queryset.filter(id__in=cycle_issues) - - elif module_id is not None: - module_issues = ModuleIssue.objects.filter( - **self.filters["base_filters"], module_id=module_id - ).values_list("issue_id", flat=True) - queryset = queryset.filter(id__in=module_issues) - # Apply date range filter if available if self.filters["chart_period_range"]: start_date, end_date = self.filters["chart_period_range"] @@ -556,14 +360,8 @@ class AdvanceAnalyticsChartEndpoint(AdvanceAnalyticsBaseView): ) elif type == "work-items": - # Optionally accept cycle_id or module_id as query params - cycle_id = request.GET.get("cycle_id", None) - module_id = request.GET.get("module_id", None) - peek_view = request.GET.get("peek_view", False) return Response( - self.work_item_completion_chart( - cycle_id=cycle_id, module_id=module_id, peek_view=peek_view - ), + self.work_item_completion_chart(), status=status.HTTP_200_OK, ) diff --git a/apiserver/plane/app/views/analytic/project_analytics.py b/apiserver/plane/app/views/analytic/project_analytics.py new file mode 100644 index 000000000..655f8e989 --- /dev/null +++ b/apiserver/plane/app/views/analytic/project_analytics.py @@ -0,0 +1,421 @@ +from rest_framework.response import Response +from rest_framework import status +from typing import Dict, Any +from django.db.models import QuerySet, Q, Count +from django.http import HttpRequest +from django.db.models.functions import TruncMonth +from django.utils import timezone +from datetime import timedelta +from plane.app.views.base import BaseAPIView +from plane.app.permissions import ROLE, allow_permission +from plane.db.models import ( + Project, + Issue, + Cycle, + Module, + CycleIssue, + ModuleIssue, +) +from django.db import models +from django.db.models import F, Case, When, Value +from django.db.models.functions import Concat +from plane.utils.build_chart import build_analytics_chart +from plane.utils.date_utils import ( + get_analytics_filters, +) + + +class ProjectAdvanceAnalyticsBaseView(BaseAPIView): + def initialize_workspace(self, slug: str, type: str) -> None: + self._workspace_slug = slug + self.filters = get_analytics_filters( + slug=slug, + type=type, + user=self.request.user, + date_filter=self.request.GET.get("date_filter", None), + project_ids=self.request.GET.get("project_ids", None), + ) + + +class ProjectAdvanceAnalyticsEndpoint(ProjectAdvanceAnalyticsBaseView): + def get_filtered_counts(self, queryset: QuerySet) -> Dict[str, int]: + def get_filtered_count() -> int: + if self.filters["analytics_date_range"]: + return queryset.filter( + created_at__gte=self.filters["analytics_date_range"]["current"][ + "gte" + ], + created_at__lte=self.filters["analytics_date_range"]["current"][ + "lte" + ], + ).count() + return queryset.count() + + return { + "count": get_filtered_count(), + } + + def get_work_items_stats( + self, project_id, cycle_id=None, module_id=None + ) -> Dict[str, Dict[str, int]]: + """ + Returns work item stats for the workspace, or filtered by cycle_id or module_id if provided. + """ + base_queryset = None + if cycle_id is not None: + cycle_issues = CycleIssue.objects.filter( + **self.filters["base_filters"], cycle_id=cycle_id + ).values_list("issue_id", flat=True) + base_queryset = Issue.issue_objects.filter(id__in=cycle_issues) + elif module_id is not None: + module_issues = ModuleIssue.objects.filter( + **self.filters["base_filters"], module_id=module_id + ).values_list("issue_id", flat=True) + base_queryset = Issue.issue_objects.filter(id__in=module_issues) + else: + base_queryset = Issue.issue_objects.filter( + **self.filters["base_filters"], project_id=project_id + ) + + return { + "total_work_items": self.get_filtered_counts(base_queryset), + "started_work_items": self.get_filtered_counts( + base_queryset.filter(state__group="started") + ), + "backlog_work_items": self.get_filtered_counts( + base_queryset.filter(state__group="backlog") + ), + "un_started_work_items": self.get_filtered_counts( + base_queryset.filter(state__group="unstarted") + ), + "completed_work_items": self.get_filtered_counts( + base_queryset.filter(state__group="completed") + ), + } + + @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) + def get(self, request: HttpRequest, slug: str, project_id: str) -> Response: + self.initialize_workspace(slug, type="analytics") + + # Optionally accept cycle_id or module_id as query params + cycle_id = request.GET.get("cycle_id", None) + module_id = request.GET.get("module_id", None) + return Response( + self.get_work_items_stats( + cycle_id=cycle_id, module_id=module_id, project_id=project_id + ), + status=status.HTTP_200_OK, + ) + + +class ProjectAdvanceAnalyticsStatsEndpoint(ProjectAdvanceAnalyticsBaseView): + def get_project_issues_stats(self) -> QuerySet: + # Get the base queryset with workspace and project filters + base_queryset = Issue.issue_objects.filter(**self.filters["base_filters"]) + + # Apply date range filter if available + if self.filters["chart_period_range"]: + start_date, end_date = self.filters["chart_period_range"] + base_queryset = base_queryset.filter( + created_at__date__gte=start_date, created_at__date__lte=end_date + ) + + return ( + base_queryset.values("project_id", "project__name") + .annotate( + cancelled_work_items=Count("id", filter=Q(state__group="cancelled")), + completed_work_items=Count("id", filter=Q(state__group="completed")), + backlog_work_items=Count("id", filter=Q(state__group="backlog")), + un_started_work_items=Count("id", filter=Q(state__group="unstarted")), + started_work_items=Count("id", filter=Q(state__group="started")), + ) + .order_by("project_id") + ) + + def get_work_items_stats( + self, project_id, cycle_id=None, module_id=None + ) -> Dict[str, Dict[str, int]]: + base_queryset = None + if cycle_id is not None: + cycle_issues = CycleIssue.objects.filter( + **self.filters["base_filters"], cycle_id=cycle_id + ).values_list("issue_id", flat=True) + base_queryset = Issue.issue_objects.filter(id__in=cycle_issues) + elif module_id is not None: + module_issues = ModuleIssue.objects.filter( + **self.filters["base_filters"], module_id=module_id + ).values_list("issue_id", flat=True) + base_queryset = Issue.issue_objects.filter(id__in=module_issues) + else: + base_queryset = Issue.issue_objects.filter( + **self.filters["base_filters"], project_id=project_id + ) + return ( + base_queryset.annotate(display_name=F("assignees__display_name")) + .annotate(assignee_id=F("assignees__id")) + .annotate(avatar=F("assignees__avatar")) + .annotate( + avatar_url=Case( + # If `avatar_asset` exists, use it to generate the asset URL + When( + assignees__avatar_asset__isnull=False, + then=Concat( + Value("/api/assets/v2/static/"), + "assignees__avatar_asset", # Assuming avatar_asset has an id or relevant field + Value("/"), + ), + ), + # If `avatar_asset` is None, fall back to using `avatar` field directly + When( + assignees__avatar_asset__isnull=True, then="assignees__avatar" + ), + default=Value(None), + output_field=models.CharField(), + ) + ) + .values("display_name", "assignee_id", "avatar_url") + .annotate( + cancelled_work_items=Count( + "id", filter=Q(state__group="cancelled"), distinct=True + ), + completed_work_items=Count( + "id", filter=Q(state__group="completed"), distinct=True + ), + backlog_work_items=Count( + "id", filter=Q(state__group="backlog"), distinct=True + ), + un_started_work_items=Count( + "id", filter=Q(state__group="unstarted"), distinct=True + ), + started_work_items=Count( + "id", filter=Q(state__group="started"), distinct=True + ), + ) + .order_by("display_name") + ) + + @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) + def get(self, request: HttpRequest, slug: str, project_id: str) -> Response: + self.initialize_workspace(slug, type="chart") + type = request.GET.get("type", "work-items") + + if type == "work-items": + # Optionally accept cycle_id or module_id as query params + cycle_id = request.GET.get("cycle_id", None) + module_id = request.GET.get("module_id", None) + return Response( + self.get_work_items_stats( + project_id=project_id, cycle_id=cycle_id, module_id=module_id + ), + status=status.HTTP_200_OK, + ) + + return Response({"message": "Invalid type"}, status=status.HTTP_400_BAD_REQUEST) + + +class ProjectAdvanceAnalyticsChartEndpoint(ProjectAdvanceAnalyticsBaseView): + def work_item_completion_chart( + self, project_id, cycle_id=None, module_id=None + ) -> Dict[str, Any]: + # Get the base queryset + queryset = ( + Issue.issue_objects.filter(**self.filters["base_filters"]) + .filter(project_id=project_id) + .select_related("workspace", "state", "parent") + .prefetch_related( + "assignees", "labels", "issue_module__module", "issue_cycle__cycle" + ) + ) + + if cycle_id is not None: + cycle_issues = CycleIssue.objects.filter( + **self.filters["base_filters"], cycle_id=cycle_id + ).values_list("issue_id", flat=True) + cycle = Cycle.objects.filter(id=cycle_id).first() + if cycle and cycle.start_date: + start_date = cycle.start_date.date() + end_date = cycle.end_date.date() + else: + return {"data": [], "schema": {}} + queryset = cycle_issues + + elif module_id is not None: + module_issues = ModuleIssue.objects.filter( + **self.filters["base_filters"], module_id=module_id + ).values_list("issue_id", flat=True) + module = Module.objects.filter(id=module_id).first() + if module and module.start_date: + start_date = module.start_date + end_date = module.target_date + else: + return {"data": [], "schema": {}} + queryset = module_issues + + else: + project = Project.objects.filter(id=project_id).first() + if project.created_at: + start_date = project.created_at.date().replace(day=1) + else: + return {"data": [], "schema": {}} + + if cycle_id or module_id: + # Get daily stats with optimized query + daily_stats = ( + queryset.values("created_at__date") + .annotate( + created_count=Count("id"), + completed_count=Count( + "id", filter=Q(issue__state__group="completed") + ), + ) + .order_by("created_at__date") + ) + + # Create a dictionary of existing stats with summed counts + stats_dict = { + stat["created_at__date"].strftime("%Y-%m-%d"): { + "created_count": stat["created_count"], + "completed_count": stat["completed_count"], + } + for stat in daily_stats + } + + # Generate data for all days in the range + data = [] + current_date = start_date + while current_date <= end_date: + date_str = current_date.strftime("%Y-%m-%d") + stats = stats_dict.get( + date_str, {"created_count": 0, "completed_count": 0} + ) + data.append( + { + "key": date_str, + "name": date_str, + "count": stats["created_count"] + stats["completed_count"], + "completed_issues": stats["completed_count"], + "created_issues": stats["created_count"], + } + ) + current_date += timedelta(days=1) + else: + # Apply date range filter if available + if self.filters["chart_period_range"]: + start_date, end_date = self.filters["chart_period_range"] + queryset = queryset.filter( + created_at__date__gte=start_date, created_at__date__lte=end_date + ) + + # Annotate by month and count + monthly_stats = ( + queryset.annotate(month=TruncMonth("created_at")) + .values("month") + .annotate( + created_count=Count("id"), + completed_count=Count("id", filter=Q(state__group="completed")), + ) + .order_by("month") + ) + + # Create dictionary of month -> counts + stats_dict = { + stat["month"].strftime("%Y-%m-%d"): { + "created_count": stat["created_count"], + "completed_count": stat["completed_count"], + } + for stat in monthly_stats + } + + # Generate monthly data (ensure months with 0 count are included) + data = [] + # include the current date at the end + end_date = timezone.now().date() + last_month = end_date.replace(day=1) + current_month = start_date + + while current_month <= last_month: + date_str = current_month.strftime("%Y-%m-%d") + stats = stats_dict.get( + date_str, {"created_count": 0, "completed_count": 0} + ) + data.append( + { + "key": date_str, + "name": date_str, + "count": stats["created_count"], + "completed_issues": stats["completed_count"], + "created_issues": stats["created_count"], + } + ) + # Move to next month + if current_month.month == 12: + current_month = current_month.replace( + year=current_month.year + 1, month=1 + ) + else: + current_month = current_month.replace(month=current_month.month + 1) + + schema = { + "completed_issues": "completed_issues", + "created_issues": "created_issues", + } + + return {"data": data, "schema": schema} + + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) + def get(self, request: HttpRequest, slug: str, project_id: str) -> Response: + self.initialize_workspace(slug, type="chart") + type = request.GET.get("type", "projects") + group_by = request.GET.get("group_by", None) + x_axis = request.GET.get("x_axis", "PRIORITY") + cycle_id = request.GET.get("cycle_id", None) + module_id = request.GET.get("module_id", None) + + if type == "custom-work-items": + queryset = ( + Issue.issue_objects.filter(**self.filters["base_filters"]) + .filter(project_id=project_id) + .select_related("workspace", "state", "parent") + .prefetch_related( + "assignees", "labels", "issue_module__module", "issue_cycle__cycle" + ) + ) + + # Apply cycle/module filters if present + if cycle_id is not None: + cycle_issues = CycleIssue.objects.filter( + **self.filters["base_filters"], cycle_id=cycle_id + ).values_list("issue_id", flat=True) + queryset = queryset.filter(id__in=cycle_issues) + + elif module_id is not None: + module_issues = ModuleIssue.objects.filter( + **self.filters["base_filters"], module_id=module_id + ).values_list("issue_id", flat=True) + queryset = queryset.filter(id__in=module_issues) + + # Apply date range filter if available + if self.filters["chart_period_range"]: + start_date, end_date = self.filters["chart_period_range"] + queryset = queryset.filter( + created_at__date__gte=start_date, created_at__date__lte=end_date + ) + + return Response( + build_analytics_chart(queryset, x_axis, group_by), + status=status.HTTP_200_OK, + ) + + elif type == "work-items": + # Optionally accept cycle_id or module_id as query params + cycle_id = request.GET.get("cycle_id", None) + module_id = request.GET.get("module_id", None) + + return Response( + self.work_item_completion_chart( + project_id=project_id, cycle_id=cycle_id, module_id=module_id + ), + status=status.HTTP_200_OK, + ) + + return Response({"message": "Invalid type"}, status=status.HTTP_400_BAD_REQUEST) diff --git a/web/core/components/analytics-v2/overview/project-insights.tsx b/web/core/components/analytics-v2/overview/project-insights.tsx index 83054a885..9c8f829a1 100644 --- a/web/core/components/analytics-v2/overview/project-insights.tsx +++ b/web/core/components/analytics-v2/overview/project-insights.tsx @@ -39,8 +39,9 @@ const ProjectInsights = observer(() => { ...(selectedProjects?.length > 0 && { project_ids: selectedProjects?.join(",") }), ...(selectedCycle ? { cycle_id: selectedCycle } : {}), ...(selectedModule ? { module_id: selectedModule } : {}), - ...(isPeekView ? { peek_view: true } : {}), - }) + }, + isPeekView + ) ); return ( diff --git a/web/core/components/analytics-v2/total-insights.tsx b/web/core/components/analytics-v2/total-insights.tsx index 8a3ffe8b7..5fc8b2239 100644 --- a/web/core/components/analytics-v2/total-insights.tsx +++ b/web/core/components/analytics-v2/total-insights.tsx @@ -26,13 +26,17 @@ const TotalInsights: React.FC<{ analyticsType: TAnalyticsTabsV2Base; peekView?: const { data: totalInsightsData, isLoading } = useSWR( `total-insights-${analyticsType}-${selectedDuration}-${selectedProjects}-${selectedCycle}-${selectedModule}-${isPeekView}`, () => - analyticsV2Service.getAdvanceAnalytics(workspaceSlug, analyticsType, { - // date_filter: selectedDuration, - ...(selectedProjects?.length > 0 ? { project_ids: selectedProjects.join(",") } : {}), - ...(selectedCycle ? { cycle_id: selectedCycle } : {}), - ...(selectedModule ? { module_id: selectedModule } : {}), - ...(isPeekView ? { peek_view: true } : {}), - }) + analyticsV2Service.getAdvanceAnalytics( + workspaceSlug, + analyticsType, + { + // date_filter: selectedDuration, + ...(selectedProjects?.length > 0 ? { project_ids: selectedProjects.join(",") } : {}), + ...(selectedCycle ? { cycle_id: selectedCycle } : {}), + ...(selectedModule ? { module_id: selectedModule } : {}), + }, + isPeekView + ) ); return (
{ ...(selectedProjects?.length > 0 && { project_ids: selectedProjects?.join(",") }), ...(selectedCycle ? { cycle_id: selectedCycle } : {}), ...(selectedModule ? { module_id: selectedModule } : {}), - ...(isPeekView ? { peek_view: true } : {}), - }) + }, + isPeekView + ) ); const parsedData: TChartData[] = useMemo(() => { if (!createdVsResolvedData?.data) return []; diff --git a/web/core/components/analytics-v2/work-items/priority-chart.tsx b/web/core/components/analytics-v2/work-items/priority-chart.tsx index 664824851..c5b31bb16 100644 --- a/web/core/components/analytics-v2/work-items/priority-chart.tsx +++ b/web/core/components/analytics-v2/work-items/priority-chart.tsx @@ -57,14 +57,18 @@ const PriorityChart = observer((props: Props) => { `customized-insights-chart-${workspaceSlug}-${selectedDuration}- ${selectedProjects}-${selectedCycle}-${selectedModule}-${props.x_axis}-${props.y_axis}-${props.group_by}-${isPeekView}`, () => - analyticsV2Service.getAdvanceAnalyticsCharts(workspaceSlug, "custom-work-items", { - // date_filter: selectedDuration, - ...(selectedProjects?.length > 0 && { project_ids: selectedProjects?.join(",") }), - ...(selectedCycle ? { cycle_id: selectedCycle } : {}), - ...(selectedModule ? { module_id: selectedModule } : {}), - ...(isPeekView ? { peek_view: true } : {}), - ...props, - }) + analyticsV2Service.getAdvanceAnalyticsCharts( + workspaceSlug, + "custom-work-items", + { + // date_filter: selectedDuration, + ...(selectedProjects?.length > 0 && { project_ids: selectedProjects?.join(",") }), + ...(selectedCycle ? { cycle_id: selectedCycle } : {}), + ...(selectedModule ? { module_id: selectedModule } : {}), + ...props, + }, + isPeekView + ) ); const parsedData = useMemo( () => diff --git a/web/core/components/analytics-v2/work-items/workitems-insight-table.tsx b/web/core/components/analytics-v2/work-items/workitems-insight-table.tsx index e42824492..1fd834329 100644 --- a/web/core/components/analytics-v2/work-items/workitems-insight-table.tsx +++ b/web/core/components/analytics-v2/work-items/workitems-insight-table.tsx @@ -31,13 +31,17 @@ const WorkItemsInsightTable = observer(() => { const { data: workItemsData, isLoading } = useSWR( `insights-table-work-items-${workspaceSlug}-${selectedDuration}-${selectedProjects}-${selectedCycle}-${selectedModule}-${isPeekView}`, () => - analyticsV2Service.getAdvanceAnalyticsStats(workspaceSlug, "work-items", { - // date_filter: selectedDuration, - ...(selectedProjects?.length > 0 ? { project_ids: selectedProjects.join(",") } : {}), - ...(selectedCycle ? { cycle_id: selectedCycle } : {}), - ...(selectedModule ? { module_id: selectedModule } : {}), - ...(isPeekView ? { peek_view: true } : {}), - }) + analyticsV2Service.getAdvanceAnalyticsStats( + workspaceSlug, + "work-items", + { + // date_filter: selectedDuration, + ...(selectedProjects?.length > 0 ? { project_ids: selectedProjects.join(",") } : {}), + ...(selectedCycle ? { cycle_id: selectedCycle } : {}), + ...(selectedModule ? { module_id: selectedModule } : {}), + }, + isPeekView + ) ); // derived values const columnsLabels = useMemo( diff --git a/web/core/services/analytics-v2.service.ts b/web/core/services/analytics-v2.service.ts index 87257cbc6..05e1b78b7 100644 --- a/web/core/services/analytics-v2.service.ts +++ b/web/core/services/analytics-v2.service.ts @@ -10,14 +10,18 @@ export class AnalyticsV2Service extends APIService { async getAdvanceAnalytics( workspaceSlug: string, tab: TAnalyticsTabsV2Base, - params?: Record + params?: Record, + isPeekView?: boolean ): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/advance-analytics/`, { - params: { - tab, - ...params, - }, - }) + return this.get( + this.processUrl("advance-analytics", workspaceSlug, tab, params, isPeekView), + { + params: { + tab, + ...params, + }, + } + ) .then((res) => res?.data) .catch((err) => { throw err?.response?.data; @@ -27,9 +31,17 @@ export class AnalyticsV2Service extends APIService { async getAdvanceAnalyticsStats( workspaceSlug: string, tab: Exclude, - params?: Record + params?: Record, + isPeekView?: boolean ): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/advance-analytics-stats/`, { + const processedUrl = this.processUrl>( + "advance-analytics-stats", + workspaceSlug, + tab, + params, + isPeekView + ); + return this.get(processedUrl, { params: { type: tab, ...params, @@ -44,9 +56,17 @@ export class AnalyticsV2Service extends APIService { async getAdvanceAnalyticsCharts( workspaceSlug: string, tab: TAnalyticsGraphsV2Base, - params?: Record + params?: Record, + isPeekView?: boolean ): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/advance-analytics-charts/`, { + const processedUrl = this.processUrl( + "advance-analytics-charts", + workspaceSlug, + tab, + params, + isPeekView + ); + return this.get(processedUrl, { params: { type: tab, ...params, @@ -57,4 +77,19 @@ export class AnalyticsV2Service extends APIService { throw err?.response?.data; }); } + + processUrl( + endpoint: string, + workspaceSlug: string, + tab: T, + params?: Record, + isPeekView?: boolean + ) { + let processedUrl = `/api/workspaces/${workspaceSlug}`; + if (isPeekView && tab === "work-items") { + const projectId = params?.project_ids.split(",")[0]; + processedUrl += `/projects/${projectId}`; + } + return `${processedUrl}/${endpoint}`; + } } From 6216ad77f443793ec08bb80eb287b83ba28f1546 Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Fri, 23 May 2025 15:06:58 +0530 Subject: [PATCH 073/201] [WEB-4146] fix: AI environment variables configuration in GodMode (#7104) * [WEB-4146] fix: artificial intelligence environment variables configuration * chore: update llm configuration keys --- admin/app/ai/form.tsx | 16 ++++++++-------- apiserver/plane/license/api/views/instance.py | 8 ++++---- packages/types/src/instance/ai.d.ts | 2 +- packages/types/src/instance/base.d.ts | 2 +- .../components/description-editor.tsx | 4 ++-- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/admin/app/ai/form.tsx b/admin/app/ai/form.tsx index 4258a99fb..47ab9480e 100644 --- a/admin/app/ai/form.tsx +++ b/admin/app/ai/form.tsx @@ -26,16 +26,16 @@ export const InstanceAIForm: FC = (props) => { formState: { errors, isSubmitting }, } = useForm({ defaultValues: { - OPENAI_API_KEY: config["OPENAI_API_KEY"], - GPT_ENGINE: config["GPT_ENGINE"], + LLM_API_KEY: config["LLM_API_KEY"], + LLM_MODEL: config["LLM_MODEL"], }, }); const aiFormFields: TControllerInputFormField[] = [ { - key: "GPT_ENGINE", + key: "LLM_MODEL", type: "text", - label: "GPT_ENGINE", + label: "LLM Model", description: ( <> Choose an OpenAI engine.{" "} @@ -49,12 +49,12 @@ export const InstanceAIForm: FC = (props) => { ), - placeholder: "gpt-3.5-turbo", - error: Boolean(errors.GPT_ENGINE), + placeholder: "gpt-4o-mini", + error: Boolean(errors.LLM_MODEL), required: false, }, { - key: "OPENAI_API_KEY", + key: "LLM_API_KEY", type: "password", label: "API key", description: ( @@ -71,7 +71,7 @@ export const InstanceAIForm: FC = (props) => { ), placeholder: "sk-asddassdfasdefqsdfasd23das3dasdcasd", - error: Boolean(errors.OPENAI_API_KEY), + error: Boolean(errors.LLM_API_KEY), required: false, }, ]; diff --git a/apiserver/plane/license/api/views/instance.py b/apiserver/plane/license/api/views/instance.py index 0e2b64fc9..c598acfef 100644 --- a/apiserver/plane/license/api/views/instance.py +++ b/apiserver/plane/license/api/views/instance.py @@ -57,7 +57,7 @@ class InstanceEndpoint(BaseAPIView): POSTHOG_API_KEY, POSTHOG_HOST, UNSPLASH_ACCESS_KEY, - OPENAI_API_KEY, + LLM_API_KEY, IS_INTERCOM_ENABLED, INTERCOM_APP_ID, ) = get_configuration_value( @@ -112,8 +112,8 @@ class InstanceEndpoint(BaseAPIView): "default": os.environ.get("UNSPLASH_ACCESS_KEY", ""), }, { - "key": "OPENAI_API_KEY", - "default": os.environ.get("OPENAI_API_KEY", ""), + "key": "LLM_API_KEY", + "default": os.environ.get("LLM_API_KEY", ""), }, # Intercom settings { @@ -151,7 +151,7 @@ class InstanceEndpoint(BaseAPIView): data["has_unsplash_configured"] = bool(UNSPLASH_ACCESS_KEY) # Open AI settings - data["has_openai_configured"] = bool(OPENAI_API_KEY) + data["has_llm_configured"] = bool(LLM_API_KEY) # File size settings data["file_size_limit"] = float(os.environ.get("FILE_SIZE_LIMIT", 5242880)) diff --git a/packages/types/src/instance/ai.d.ts b/packages/types/src/instance/ai.d.ts index 0ac34557a..5bfd1a6ba 100644 --- a/packages/types/src/instance/ai.d.ts +++ b/packages/types/src/instance/ai.d.ts @@ -1 +1 @@ -export type TInstanceAIConfigurationKeys = "OPENAI_API_KEY" | "GPT_ENGINE"; +export type TInstanceAIConfigurationKeys = "LLM_API_KEY" | "LLM_MODEL"; diff --git a/packages/types/src/instance/base.d.ts b/packages/types/src/instance/base.d.ts index dc5ee5fc7..79b1e642f 100644 --- a/packages/types/src/instance/base.d.ts +++ b/packages/types/src/instance/base.d.ts @@ -49,7 +49,7 @@ export interface IInstanceConfig { posthog_api_key: string | undefined; posthog_host: string | undefined; has_unsplash_configured: boolean; - has_openai_configured: boolean; + has_llm_configured: boolean; file_size_limit: number | undefined; is_smtp_configured: boolean; app_base_url: string | undefined; diff --git a/web/core/components/issues/issue-modal/components/description-editor.tsx b/web/core/components/issues/issue-modal/components/description-editor.tsx index b19fc62db..9cae8d840 100644 --- a/web/core/components/issues/issue-modal/components/description-editor.tsx +++ b/web/core/components/issues/issue-modal/components/description-editor.tsx @@ -225,7 +225,7 @@ export const IssueDescriptionEditor: React.FC = ob )} />
- {issueName && issueName.trim() !== "" && config?.has_openai_configured && ( + {issueName && issueName.trim() !== "" && config?.has_llm_configured && ( )} - {config?.has_openai_configured && projectId && ( + {config?.has_llm_configured && projectId && ( { From 731c4e8fcdc950e3ac98138037e093333edc43b4 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Fri, 23 May 2025 15:29:37 +0530 Subject: [PATCH 074/201] [WEB-4161] fix: eslint config for library config file #7103 --- packages/eslint-config/library.js | 4 +- packages/eslint-config/package.json | 1 + yarn.lock | 200 +++++++++++++++++++++++----- 3 files changed, 171 insertions(+), 34 deletions(-) diff --git a/packages/eslint-config/library.js b/packages/eslint-config/library.js index 790364230..b868b35a4 100644 --- a/packages/eslint-config/library.js +++ b/packages/eslint-config/library.js @@ -5,7 +5,7 @@ const project = resolve(process.cwd(), "tsconfig.json"); /** @type {import("eslint").Linter.Config} */ module.exports = { extends: ["prettier", "plugin:@typescript-eslint/recommended"], - plugins: ["react", "@typescript-eslint", "import"], + plugins: ["react", "react-hooks", "@typescript-eslint", "import"], globals: { React: true, JSX: true, @@ -38,7 +38,7 @@ module.exports = { "react/self-closing-comp": ["error", { component: true, html: true }], "react/jsx-boolean-value": "error", "react/jsx-no-duplicate-props": "error", - // "react-hooks/exhaustive-deps": "warn", + "react-hooks/exhaustive-deps": "warn", "@typescript-eslint/no-unused-expressions": "warn", "@typescript-eslint/no-unused-vars": ["warn"], "@typescript-eslint/no-explicit-any": "warn", diff --git a/packages/eslint-config/package.json b/packages/eslint-config/package.json index 9ec353684..4e2ef3b57 100644 --- a/packages/eslint-config/package.json +++ b/packages/eslint-config/package.json @@ -17,6 +17,7 @@ "eslint-config-turbo": "^1.12.4", "eslint-plugin-import": "^2.29.1", "eslint-plugin-react": "^7.33.2", + "eslint-plugin-react-hooks": "^5.2.0", "typescript": "5.3.3" } } diff --git a/yarn.lock b/yarn.lock index 6976451fa..ef27eff52 100644 --- a/yarn.lock +++ b/yarn.lock @@ -257,7 +257,7 @@ "@babel/traverse" "^7.25.9" "@babel/types" "^7.25.9" -"@babel/helpers@7.26.10", "@babel/helpers@^7.26.7": +"@babel/helpers@^7.26.7": version "7.26.10" resolved "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.10.tgz#6baea3cd62ec2d0c1068778d63cb1314f6637384" integrity sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g== @@ -852,7 +852,7 @@ "@babel/plugin-transform-modules-commonjs" "^7.25.9" "@babel/plugin-transform-typescript" "^7.25.9" -"@babel/runtime@7.26.10", "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.13", "@babel/runtime@^7.23.9", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.13", "@babel/runtime@^7.23.9", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": version "7.26.10" resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz#a07b4d8fa27af131a633d7b3524db803eb4764c2" integrity sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw== @@ -1124,126 +1124,251 @@ resolved "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz#5e13fac887f08c44f76b0ccaf3370eb00fec9bb6" integrity sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg== +"@esbuild/aix-ppc64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz#38848d3e25afe842a7943643cbcd387cc6e13461" + integrity sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA== + "@esbuild/aix-ppc64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz#499600c5e1757a524990d5d92601f0ac3ce87f64" integrity sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ== +"@esbuild/android-arm64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz#f592957ae8b5643129fa889c79e69cd8669bb894" + integrity sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg== + "@esbuild/android-arm64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz#b9b8231561a1dfb94eb31f4ee056b92a985c324f" integrity sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g== +"@esbuild/android-arm@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.24.2.tgz#72d8a2063aa630308af486a7e5cbcd1e134335b3" + integrity sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q== + "@esbuild/android-arm@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.0.tgz#ca6e7888942505f13e88ac9f5f7d2a72f9facd2b" integrity sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g== +"@esbuild/android-x64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.24.2.tgz#9a7713504d5f04792f33be9c197a882b2d88febb" + integrity sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw== + "@esbuild/android-x64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.0.tgz#e765ea753bac442dfc9cb53652ce8bd39d33e163" integrity sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg== +"@esbuild/darwin-arm64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz#02ae04ad8ebffd6e2ea096181b3366816b2b5936" + integrity sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA== + "@esbuild/darwin-arm64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz#fa394164b0d89d4fdc3a8a21989af70ef579fa2c" integrity sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw== +"@esbuild/darwin-x64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz#9ec312bc29c60e1b6cecadc82bd504d8adaa19e9" + integrity sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA== + "@esbuild/darwin-x64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz#91979d98d30ba6e7d69b22c617cc82bdad60e47a" integrity sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg== +"@esbuild/freebsd-arm64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz#5e82f44cb4906d6aebf24497d6a068cfc152fa00" + integrity sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg== + "@esbuild/freebsd-arm64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz#b97e97073310736b430a07b099d837084b85e9ce" integrity sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w== +"@esbuild/freebsd-x64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz#3fb1ce92f276168b75074b4e51aa0d8141ecce7f" + integrity sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q== + "@esbuild/freebsd-x64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz#f3b694d0da61d9910ec7deff794d444cfbf3b6e7" integrity sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A== +"@esbuild/linux-arm64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz#856b632d79eb80aec0864381efd29de8fd0b1f43" + integrity sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg== + "@esbuild/linux-arm64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz#f921f699f162f332036d5657cad9036f7a993f73" integrity sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg== +"@esbuild/linux-arm@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz#c846b4694dc5a75d1444f52257ccc5659021b736" + integrity sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA== + "@esbuild/linux-arm@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz#cc49305b3c6da317c900688995a4050e6cc91ca3" integrity sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg== +"@esbuild/linux-ia32@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz#f8a16615a78826ccbb6566fab9a9606cfd4a37d5" + integrity sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw== + "@esbuild/linux-ia32@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz#3e0736fcfab16cff042dec806247e2c76e109e19" integrity sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg== +"@esbuild/linux-loong64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz#1c451538c765bf14913512c76ed8a351e18b09fc" + integrity sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ== + "@esbuild/linux-loong64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz#ea2bf730883cddb9dfb85124232b5a875b8020c7" integrity sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw== +"@esbuild/linux-mips64el@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz#0846edeefbc3d8d50645c51869cc64401d9239cb" + integrity sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw== + "@esbuild/linux-mips64el@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz#4cababb14eede09248980a2d2d8b966464294ff1" integrity sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ== +"@esbuild/linux-ppc64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz#8e3fc54505671d193337a36dfd4c1a23b8a41412" + integrity sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw== + "@esbuild/linux-ppc64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz#8860a4609914c065373a77242e985179658e1951" integrity sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw== +"@esbuild/linux-riscv64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz#6a1e92096d5e68f7bb10a0d64bb5b6d1daf9a694" + integrity sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q== + "@esbuild/linux-riscv64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz#baf26e20bb2d38cfb86ee282dff840c04f4ed987" integrity sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA== +"@esbuild/linux-s390x@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz#ab18e56e66f7a3c49cb97d337cd0a6fea28a8577" + integrity sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw== + "@esbuild/linux-s390x@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz#8323afc0d6cb1b6dc6e9fd21efd9e1542c3640a4" integrity sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA== +"@esbuild/linux-x64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz#8140c9b40da634d380b0b29c837a0b4267aff38f" + integrity sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q== + "@esbuild/linux-x64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz#08fcf60cb400ed2382e9f8e0f5590bac8810469a" integrity sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw== +"@esbuild/netbsd-arm64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz#65f19161432bafb3981f5f20a7ff45abb2e708e6" + integrity sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw== + "@esbuild/netbsd-arm64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz#935c6c74e20f7224918fbe2e6c6fe865b6c6ea5b" integrity sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw== +"@esbuild/netbsd-x64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz#7a3a97d77abfd11765a72f1c6f9b18f5396bcc40" + integrity sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw== + "@esbuild/netbsd-x64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz#414677cef66d16c5a4d210751eb2881bb9c1b62b" integrity sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA== +"@esbuild/openbsd-arm64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz#58b00238dd8f123bfff68d3acc53a6ee369af89f" + integrity sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A== + "@esbuild/openbsd-arm64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz#8fd55a4d08d25cdc572844f13c88d678c84d13f7" integrity sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw== +"@esbuild/openbsd-x64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz#0ac843fda0feb85a93e288842936c21a00a8a205" + integrity sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA== + "@esbuild/openbsd-x64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz#0c48ddb1494bbc2d6bcbaa1429a7f465fa1dedde" integrity sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg== +"@esbuild/sunos-x64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz#8b7aa895e07828d36c422a4404cc2ecf27fb15c6" + integrity sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig== + "@esbuild/sunos-x64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz#86ff9075d77962b60dd26203d7352f92684c8c92" integrity sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg== +"@esbuild/win32-arm64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz#c023afb647cabf0c3ed13f0eddfc4f1d61c66a85" + integrity sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ== + "@esbuild/win32-arm64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz#849c62327c3229467f5b5cd681bf50588442e96c" integrity sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw== +"@esbuild/win32-ia32@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz#96c356132d2dda990098c8b8b951209c3cd743c2" + integrity sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA== + "@esbuild/win32-ia32@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz#f62eb480cd7cca088cb65bb46a6db25b725dc079" integrity sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA== +"@esbuild/win32-x64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz#34aa0b52d0fbb1a654b596acfa595f0c7b77a77b" + integrity sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg== + "@esbuild/win32-x64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz#c8e119a30a7c8d60b9d2e22d2073722dde3b710b" @@ -5930,7 +6055,38 @@ esbuild-register@^3.5.0: dependencies: debug "^4.3.4" -esbuild@0.25.0, "esbuild@^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0", esbuild@^0.25.0: +"esbuild@^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0": + version "0.24.2" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.24.2.tgz#b5b55bee7de017bff5fb8a4e3e44f2ebe2c3567d" + integrity sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA== + optionalDependencies: + "@esbuild/aix-ppc64" "0.24.2" + "@esbuild/android-arm" "0.24.2" + "@esbuild/android-arm64" "0.24.2" + "@esbuild/android-x64" "0.24.2" + "@esbuild/darwin-arm64" "0.24.2" + "@esbuild/darwin-x64" "0.24.2" + "@esbuild/freebsd-arm64" "0.24.2" + "@esbuild/freebsd-x64" "0.24.2" + "@esbuild/linux-arm" "0.24.2" + "@esbuild/linux-arm64" "0.24.2" + "@esbuild/linux-ia32" "0.24.2" + "@esbuild/linux-loong64" "0.24.2" + "@esbuild/linux-mips64el" "0.24.2" + "@esbuild/linux-ppc64" "0.24.2" + "@esbuild/linux-riscv64" "0.24.2" + "@esbuild/linux-s390x" "0.24.2" + "@esbuild/linux-x64" "0.24.2" + "@esbuild/netbsd-arm64" "0.24.2" + "@esbuild/netbsd-x64" "0.24.2" + "@esbuild/openbsd-arm64" "0.24.2" + "@esbuild/openbsd-x64" "0.24.2" + "@esbuild/sunos-x64" "0.24.2" + "@esbuild/win32-arm64" "0.24.2" + "@esbuild/win32-ia32" "0.24.2" + "@esbuild/win32-x64" "0.24.2" + +esbuild@^0.25.0: version "0.25.0" resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz#0de1787a77206c5a79eeb634a623d39b5006ce92" integrity sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw== @@ -6085,6 +6241,11 @@ eslint-plugin-jsx-a11y@^6.7.1: resolved "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.0.0-canary-7118f5dd7-20230705.tgz#4d55c50e186f1a2b0636433d2b0b2f592ddbccfd" integrity sha512-AZYbMo/NW9chdL7vk6HQzQhT+PvTAEVqWk9ziruUoW2kAOcN5qNyelv70e0F1VNQAbvutOC9oc+xfWycI9FxDw== +eslint-plugin-react-hooks@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz#1be0080901e6ac31ce7971beed3d3ec0a423d9e3" + integrity sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg== + eslint-plugin-react@^7.33.2: version "7.37.4" resolved "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.4.tgz#1b6c80b6175b6ae4b26055ae4d55d04c414c7181" @@ -8396,7 +8557,7 @@ mz@^2.7.0: object-assign "^4.0.1" thenify-all "^1.0.0" -nanoid@3.3.8, nanoid@^3.3.6, nanoid@^3.3.8: +nanoid@^3.3.6, nanoid@^3.3.8: version "3.3.8" resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf" integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w== @@ -10556,16 +10717,7 @@ streamx@^2.15.0, streamx@^2.21.0: optionalDependencies: bare-events "^2.2.0" -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -10658,14 +10810,7 @@ string_decoder@^1.1.1, string_decoder@^1.3.0: dependencies: safe-buffer "~5.2.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": - version "6.0.1" - resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -11883,16 +12028,7 @@ word-wrap@^1.2.5: resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": - version "7.0.0" - resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - -wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== From 643390e723cd9426d87ecb360380944ad50a16a3 Mon Sep 17 00:00:00 2001 From: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com> Date: Fri, 23 May 2025 15:30:42 +0530 Subject: [PATCH 075/201] [WEB-4145] chore: added validation for project deletion #7101 --- apiserver/plane/app/views/project/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apiserver/plane/app/views/project/base.py b/apiserver/plane/app/views/project/base.py index 31cbd8330..8e4ea5246 100644 --- a/apiserver/plane/app/views/project/base.py +++ b/apiserver/plane/app/views/project/base.py @@ -445,7 +445,7 @@ class ProjectViewSet(BaseViewSet): is_active=True, ).exists() ): - project = Project.objects.get(pk=pk) + project = Project.objects.get(pk=pk, workspace__slug=slug) project.delete() webhook_activity.delay( event="project", From 037bb88b53e2333ecbe0acf9a17f6d10a5b05bb4 Mon Sep 17 00:00:00 2001 From: Nikhil <118773738+pablohashescobar@users.noreply.github.com> Date: Fri, 23 May 2025 15:31:40 +0530 Subject: [PATCH 076/201] [WEB-4144] fix: api logger to handle content decode errors #7099 --- apiserver/plane/middleware/logger.py | 34 ++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/apiserver/plane/middleware/logger.py b/apiserver/plane/middleware/logger.py index 166de17c2..7481c3992 100644 --- a/apiserver/plane/middleware/logger.py +++ b/apiserver/plane/middleware/logger.py @@ -83,6 +83,32 @@ class APITokenLogMiddleware: self.process_request(request, response, request_body) return response + def _safe_decode_body(self, content): + """ + Safely decodes request/response body content, handling binary data. + Returns None if content is None, or a string representation of the content. + """ + # If the content is None, return None + if content is None: + return None + + # If the content is an empty bytes object, return None + if content == b"": + return None + + # Check if content is binary by looking for common binary file signatures + if ( + content.startswith(b"\x89PNG") + or content.startswith(b"\xff\xd8\xff") + or content.startswith(b"%PDF") + ): + return "[Binary Content]" + + try: + return content.decode("utf-8") + except UnicodeDecodeError: + return "[Could not decode content]" + def process_request(self, request, response, request_body): api_key_header = "X-Api-Key" api_key = request.headers.get(api_key_header) @@ -95,9 +121,13 @@ class APITokenLogMiddleware: method=request.method, query_params=request.META.get("QUERY_STRING", ""), headers=str(request.headers), - body=(request_body.decode("utf-8") if request_body else None), + body=( + self._safe_decode_body(request_body) if request_body else None + ), response_body=( - response.content.decode("utf-8") if response.content else None + self._safe_decode_body(response.content) + if response.content + else None ), response_code=response.status_code, ip_address=get_client_ip(request=request), From cd200169b6b9d66890084a30ddff3f0ebc29990d Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Fri, 23 May 2025 15:32:41 +0530 Subject: [PATCH 077/201] [WEB-4107] chore: redirect user to the newly created project view after creation #7098 --- web/core/components/views/modal.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/web/core/components/views/modal.tsx b/web/core/components/views/modal.tsx index 0ec329ac1..8e708eb2b 100644 --- a/web/core/components/views/modal.tsx +++ b/web/core/components/views/modal.tsx @@ -10,6 +10,7 @@ import { EModalPosition, EModalWidth, ModalCore, TOAST_TYPE, setToast } from "@p import { ProjectViewForm } from "@/components/views"; // hooks import { useProjectView } from "@/hooks/store"; +import { useAppRouter } from "@/hooks/use-app-router"; import useKeypress from "@/hooks/use-keypress"; type Props = { @@ -23,6 +24,8 @@ type Props = { export const CreateUpdateProjectViewModal: FC = observer((props) => { const { data, isOpen, onClose, preLoadedData, workspaceSlug, projectId } = props; + // router + const router = useAppRouter(); // store hooks const { createView, updateView } = useProjectView(); @@ -32,8 +35,9 @@ export const CreateUpdateProjectViewModal: FC = observer((props) => { const handleCreateView = async (payload: IProjectView) => { await createView(workspaceSlug, projectId, payload) - .then(() => { + .then((res) => { handleClose(); + router.push(`/${workspaceSlug}/projects/${projectId}/views/${res.id}`); setToast({ type: TOAST_TYPE.SUCCESS, title: "Success!", From 6eb0b5ddb0513d529723977b46963802dbc4a1c8 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Fri, 23 May 2025 15:33:56 +0530 Subject: [PATCH 078/201] [WEB-4137] chore: restrict SVG file selection (#7095) * chore: update accepted file mime types * chore: update accepted file mime types --- packages/constants/src/file.ts | 13 +++++++++++++ web/core/components/core/image-picker-popover.tsx | 6 ++---- .../core/modals/user-image-upload-modal.tsx | 6 ++---- .../core/modals/workspace-image-upload-modal.tsx | 6 ++---- 4 files changed, 19 insertions(+), 12 deletions(-) diff --git a/packages/constants/src/file.ts b/packages/constants/src/file.ts index 3fac821fa..9de3b0356 100644 --- a/packages/constants/src/file.ts +++ b/packages/constants/src/file.ts @@ -1 +1,14 @@ export const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB + +export const ACCEPTED_AVATAR_IMAGE_MIME_TYPES_FOR_REACT_DROPZONE = { + "image/jpeg": [], + "image/jpg": [], + "image/png": [], + "image/webp": [], +}; +export const ACCEPTED_COVER_IMAGE_MIME_TYPES_FOR_REACT_DROPZONE = { + "image/jpeg": [], + "image/jpg": [], + "image/png": [], + "image/webp": [], +}; diff --git a/web/core/components/core/image-picker-popover.tsx b/web/core/components/core/image-picker-popover.tsx index e54805d0f..bade076d9 100644 --- a/web/core/components/core/image-picker-popover.tsx +++ b/web/core/components/core/image-picker-popover.tsx @@ -9,7 +9,7 @@ import { Control, Controller } from "react-hook-form"; import useSWR from "swr"; import { Tab, Popover } from "@headlessui/react"; // plane imports -import { MAX_FILE_SIZE } from "@plane/constants"; +import { ACCEPTED_COVER_IMAGE_MIME_TYPES_FOR_REACT_DROPZONE, MAX_FILE_SIZE } from "@plane/constants"; import { useOutsideClickDetector } from "@plane/hooks"; // plane types import { EFileAssetType } from "@plane/types/src/enums"; @@ -88,9 +88,7 @@ export const ImagePickerPopover: React.FC = observer((props) => { const { getRootProps, getInputProps, isDragActive, fileRejections } = useDropzone({ onDrop, - accept: { - "image/*": [".png", ".jpg", ".jpeg", ".webp"], - }, + accept: ACCEPTED_COVER_IMAGE_MIME_TYPES_FOR_REACT_DROPZONE, maxSize: MAX_FILE_SIZE, }); diff --git a/web/core/components/core/modals/user-image-upload-modal.tsx b/web/core/components/core/modals/user-image-upload-modal.tsx index 286f36415..c768ff265 100644 --- a/web/core/components/core/modals/user-image-upload-modal.tsx +++ b/web/core/components/core/modals/user-image-upload-modal.tsx @@ -6,7 +6,7 @@ import { useDropzone } from "react-dropzone"; import { UserCircle2 } from "lucide-react"; import { Transition, Dialog } from "@headlessui/react"; // plane imports -import { MAX_FILE_SIZE } from "@plane/constants"; +import { ACCEPTED_AVATAR_IMAGE_MIME_TYPES_FOR_REACT_DROPZONE, MAX_FILE_SIZE } from "@plane/constants"; import { EFileAssetType } from "@plane/types/src/enums"; import { Button, TOAST_TYPE, setToast } from "@plane/ui"; // helpers @@ -35,9 +35,7 @@ export const UserImageUploadModal: React.FC = observer((props) => { const { getRootProps, getInputProps, isDragActive, fileRejections } = useDropzone({ onDrop, - accept: { - "image/*": [".png", ".jpg", ".jpeg", ".webp"], - }, + accept: ACCEPTED_AVATAR_IMAGE_MIME_TYPES_FOR_REACT_DROPZONE, maxSize: MAX_FILE_SIZE, multiple: false, }); diff --git a/web/core/components/core/modals/workspace-image-upload-modal.tsx b/web/core/components/core/modals/workspace-image-upload-modal.tsx index df0248c84..6b4bf3609 100644 --- a/web/core/components/core/modals/workspace-image-upload-modal.tsx +++ b/web/core/components/core/modals/workspace-image-upload-modal.tsx @@ -6,7 +6,7 @@ import { useDropzone } from "react-dropzone"; import { UserCircle2 } from "lucide-react"; import { Transition, Dialog } from "@headlessui/react"; // plane imports -import { MAX_FILE_SIZE } from "@plane/constants"; +import { ACCEPTED_AVATAR_IMAGE_MIME_TYPES_FOR_REACT_DROPZONE, MAX_FILE_SIZE } from "@plane/constants"; import { EFileAssetType } from "@plane/types/src/enums"; import { Button } from "@plane/ui"; // helpers @@ -43,9 +43,7 @@ export const WorkspaceImageUploadModal: React.FC = observer((props) => { const { getRootProps, getInputProps, isDragActive, fileRejections } = useDropzone({ onDrop, - accept: { - "image/*": [".png", ".jpg", ".jpeg", ".webp"], - }, + accept: ACCEPTED_AVATAR_IMAGE_MIME_TYPES_FOR_REACT_DROPZONE, maxSize: MAX_FILE_SIZE, multiple: false, }); From 5223bd01e82acd00581323abee872e053ea32f39 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Fri, 23 May 2025 15:35:47 +0530 Subject: [PATCH 079/201] [WEB-4153] chore: extend custom font family in tailwind config (#7093) * chore: remove unwanted font family * chore: add font family to extend object --- packages/tailwind-config/tailwind.config.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/tailwind-config/tailwind.config.js b/packages/tailwind-config/tailwind.config.js index 700831d12..5beff4bf8 100644 --- a/packages/tailwind-config/tailwind.config.js +++ b/packages/tailwind-config/tailwind.config.js @@ -461,9 +461,9 @@ module.exports = { "onboarding-gradient-200": "var( --gradient-onboarding-200)", "onboarding-gradient-300": "var( --gradient-onboarding-300)", }, - }, - fontFamily: { - custom: ["Inter", "sans-serif"], + fontFamily: { + custom: ["Inter", "sans-serif"], + }, }, }, plugins: [ From a3b9152a9bb741504b3ee456b0f995c0c5aaf90d Mon Sep 17 00:00:00 2001 From: Vamsi Krishna <46787868+vamsikrishnamathala@users.noreply.github.com> Date: Fri, 23 May 2025 15:36:47 +0530 Subject: [PATCH 080/201] [WEB-4123]feat: language support for sub-work item empty states #7092 --- .../i18n/src/locales/cs/translations.json | 12 +++++++ .../i18n/src/locales/de/translations.json | 12 +++++++ .../i18n/src/locales/en/translations.json | 14 +++++++- .../i18n/src/locales/es/translations.json | 12 +++++++ .../i18n/src/locales/fr/translations.json | 12 +++++++ .../i18n/src/locales/id/translations.json | 12 +++++++ .../i18n/src/locales/it/translations.json | 12 +++++++ .../i18n/src/locales/ja/translations.json | 12 +++++++ .../i18n/src/locales/ko/translations.json | 12 +++++++ .../i18n/src/locales/pl/translations.json | 12 +++++++ .../i18n/src/locales/pt-BR/translations.json | 12 +++++++ .../i18n/src/locales/ro/translations.json | 12 +++++++ .../i18n/src/locales/ru/translations.json | 12 +++++++ .../i18n/src/locales/sk/translations.json | 12 +++++++ .../i18n/src/locales/tr-TR/translations.json | 12 +++++++ .../i18n/src/locales/ua/translations.json | 12 +++++++ .../i18n/src/locales/vi-VN/translations.json | 12 +++++++ .../i18n/src/locales/zh-CN/translations.json | 12 +++++++ .../i18n/src/locales/zh-TW/translations.json | 12 +++++++ .../sub-issues/issues-list/root.tsx | 33 ++++++++++--------- 20 files changed, 247 insertions(+), 16 deletions(-) diff --git a/packages/i18n/src/locales/cs/translations.json b/packages/i18n/src/locales/cs/translations.json index f41ae9021..28109cd9d 100644 --- a/packages/i18n/src/locales/cs/translations.json +++ b/packages/i18n/src/locales/cs/translations.json @@ -1108,6 +1108,18 @@ "remove": { "success": "Podřízená pracovní položka úspěšně odebrána", "error": "Chyba při odebírání podřízené položky" + }, + "empty_state": { + "sub_list_filters": { + "title": "Nemáte podřízené pracovní položky, které odpovídají použitým filtrům.", + "description": "Chcete-li zobrazit všechny podřízené pracovní položky, odstraňte všechny použité filtry.", + "action": "Odstranit filtry" + }, + "list_filters": { + "title": "Nemáte pracovní položky, které odpovídají použitým filtrům.", + "description": "Chcete-li zobrazit všechny pracovní položky, odstraňte všechny použité filtry.", + "action": "Odstranit filtry" + } } }, "view": { diff --git a/packages/i18n/src/locales/de/translations.json b/packages/i18n/src/locales/de/translations.json index 857a08a8a..1cc370728 100644 --- a/packages/i18n/src/locales/de/translations.json +++ b/packages/i18n/src/locales/de/translations.json @@ -1108,6 +1108,18 @@ "remove": { "success": "Untergeordnetes Arbeitselement erfolgreich entfernt", "error": "Fehler beim Entfernen des untergeordneten Elements" + }, + "empty_state": { + "sub_list_filters": { + "title": "Sie haben keine untergeordneten Arbeitselemente, die den von Ihnen angewendeten Filtern entsprechen.", + "description": "Um alle untergeordneten Arbeitselemente anzuzeigen, entfernen Sie alle angewendeten Filter.", + "action": "Filter entfernen" + }, + "list_filters": { + "title": "Sie haben keine Arbeitselemente, die den von Ihnen angewendeten Filtern entsprechen.", + "description": "Um alle Arbeitselemente anzuzeigen, entfernen Sie alle angewendeten Filter.", + "action": "Filter entfernen" + } } }, "view": { diff --git a/packages/i18n/src/locales/en/translations.json b/packages/i18n/src/locales/en/translations.json index e6202deb6..c959108e0 100644 --- a/packages/i18n/src/locales/en/translations.json +++ b/packages/i18n/src/locales/en/translations.json @@ -944,6 +944,18 @@ "remove": { "success": "Sub-work item removed successfully", "error": "Error removing sub-work item" + }, + "empty_state": { + "sub_list_filters": { + "title": "You don't have sub-work items that match the filters you've applied.", + "description": "To see all sub-work items, clear all applied filters.", + "action": "Clear filters" + }, + "list_filters": { + "title": "You don't have work items that match the filters you've applied.", + "description": "To see all work items, clear all applied filters.", + "action": "Clear filters" + } } }, "view": { @@ -2284,4 +2296,4 @@ "previously_edited_by": "Previously edited by", "edited_by": "Edited by" } -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/es/translations.json b/packages/i18n/src/locales/es/translations.json index c8f258385..94f1819a4 100644 --- a/packages/i18n/src/locales/es/translations.json +++ b/packages/i18n/src/locales/es/translations.json @@ -1111,6 +1111,18 @@ "remove": { "success": "Sub-elemento eliminado correctamente", "error": "Error al eliminar el sub-elemento" + }, + "empty_state": { + "sub_list_filters": { + "title": "No tienes sub-elementos de trabajo que coincidan con los filtros que has aplicado.", + "description": "Para ver todos los sub-elementos de trabajo, elimina todos los filtros aplicados.", + "action": "Eliminar filtros" + }, + "list_filters": { + "title": "No tienes elementos de trabajo que coincidan con los filtros que has aplicado.", + "description": "Para ver todos los elementos de trabajo, elimina todos los filtros aplicados.", + "action": "Eliminar filtros" + } } }, "view": { diff --git a/packages/i18n/src/locales/fr/translations.json b/packages/i18n/src/locales/fr/translations.json index a92d64daa..4f356f149 100644 --- a/packages/i18n/src/locales/fr/translations.json +++ b/packages/i18n/src/locales/fr/translations.json @@ -1109,6 +1109,18 @@ "remove": { "success": "Sous-élément de travail supprimé avec succès", "error": "Erreur lors de la suppression du sous-élément de travail" + }, + "empty_state": { + "sub_list_filters": { + "title": "Vous n'avez pas de sous-éléments de travail qui correspondent aux filtres que vous avez appliqués.", + "description": "Pour voir tous les sous-éléments de travail, effacer tous les filtres appliqués.", + "action": "Effacer les filtres" + }, + "list_filters": { + "title": "Vous n'avez pas d'éléments de travail qui correspondent aux filtres que vous avez appliqués.", + "description": "Pour voir tous les éléments de travail, effacer tous les filtres appliqués.", + "action": "Effacer les filtres" + } } }, "view": { diff --git a/packages/i18n/src/locales/id/translations.json b/packages/i18n/src/locales/id/translations.json index f1fb2a827..20b683c65 100644 --- a/packages/i18n/src/locales/id/translations.json +++ b/packages/i18n/src/locales/id/translations.json @@ -1108,6 +1108,18 @@ "remove": { "success": "Sub-item kerja berhasil dihapus", "error": "Kesalahan saat menghapus sub-item kerja" + }, + "empty_state": { + "sub_list_filters": { + "title": "Anda tidak memiliki sub-item kerja yang cocok dengan filter yang Anda terapkan.", + "description": "Untuk melihat semua sub-item kerja, hapus semua filter yang diterapkan.", + "action": "Hapus filter" + }, + "list_filters": { + "title": "Anda tidak memiliki item kerja yang cocok dengan filter yang Anda terapkan.", + "description": "Untuk melihat semua item kerja, hapus semua filter yang diterapkan.", + "action": "Hapus filter" + } } }, "view": { diff --git a/packages/i18n/src/locales/it/translations.json b/packages/i18n/src/locales/it/translations.json index 1311c625e..5534d885c 100644 --- a/packages/i18n/src/locales/it/translations.json +++ b/packages/i18n/src/locales/it/translations.json @@ -1107,6 +1107,18 @@ "remove": { "success": "Sotto-elemento di lavoro rimosso con successo", "error": "Errore nella rimozione del sotto-elemento di lavoro" + }, + "empty_state": { + "sub_list_filters": { + "title": "Non hai sotto-elementi di lavoro che corrispondono ai filtri che hai applicato.", + "description": "Per vedere tutti i sotto-elementi di lavoro, cancella tutti i filtri applicati.", + "action": "Cancella filtri" + }, + "list_filters": { + "title": "Non hai elementi di lavoro che corrispondono ai filtri che hai applicato.", + "description": "Per vedere tutti gli elementi di lavoro, cancella tutti i filtri applicati.", + "action": "Cancella filtri" + } } }, "view": { diff --git a/packages/i18n/src/locales/ja/translations.json b/packages/i18n/src/locales/ja/translations.json index 70bcd1c8f..a6f36a65b 100644 --- a/packages/i18n/src/locales/ja/translations.json +++ b/packages/i18n/src/locales/ja/translations.json @@ -1109,6 +1109,18 @@ "remove": { "success": "サブ作業項目を削除しました", "error": "サブ作業項目の削除中にエラーが発生しました" + }, + "empty_state": { + "sub_list_filters": { + "title": "適用されたフィルターに一致するサブ作業項目がありません。", + "description": "すべてのサブ作業項目を表示するには、すべての適用されたフィルターをクリアしてください。", + "action": "フィルターをクリア" + }, + "list_filters": { + "title": "適用されたフィルターに一致する作業項目がありません。", + "description": "すべての作業項目を表示するには、すべての適用されたフィルターをクリアしてください。", + "action": "フィルターをクリア" + } } }, "view": { diff --git a/packages/i18n/src/locales/ko/translations.json b/packages/i18n/src/locales/ko/translations.json index eee2ffbf0..2858d729c 100644 --- a/packages/i18n/src/locales/ko/translations.json +++ b/packages/i18n/src/locales/ko/translations.json @@ -1110,6 +1110,18 @@ "remove": { "success": "하위 작업 항목이 성공적으로 제거되었습니다", "error": "하위 작업 항목 제거 중 오류 발생" + }, + "empty_state": { + "sub_list_filters": { + "title": "적용된 필터에 일치하는 하위 작업 항목이 없습니다.", + "description": "모든 하위 작업 항목을 보려면 모든 적용된 필터를 지우세요.", + "action": "필터 지우기" + }, + "list_filters": { + "title": "적용된 필터에 일치하는 작업 항목이 없습니다.", + "description": "모든 작업 항목을 보려면 모든 적용된 필터를 지우세요.", + "action": "필터 지우기" + } } }, "view": { diff --git a/packages/i18n/src/locales/pl/translations.json b/packages/i18n/src/locales/pl/translations.json index 7ff792a26..d11005833 100644 --- a/packages/i18n/src/locales/pl/translations.json +++ b/packages/i18n/src/locales/pl/translations.json @@ -1110,6 +1110,18 @@ "remove": { "success": "Podrzędny element pracy usunięto pomyślnie", "error": "Błąd podczas usuwania elementu podrzędnego" + }, + "empty_state": { + "sub_list_filters": { + "title": "Nie masz elementów podrzędnych, które pasują do filtrów, które zastosowałeś.", + "description": "Aby zobaczyć wszystkie elementy podrzędne, wyczyść wszystkie zastosowane filtry.", + "action": "Wyczyść filtry" + }, + "list_filters": { + "title": "Nie masz elementów pracy, które pasują do filtrów, które zastosowałeś.", + "description": "Aby zobaczyć wszystkie elementy pracy, wyczyść wszystkie zastosowane filtry.", + "action": "Wyczyść filtry" + } } }, "view": { diff --git a/packages/i18n/src/locales/pt-BR/translations.json b/packages/i18n/src/locales/pt-BR/translations.json index a5eb08a86..de630da97 100644 --- a/packages/i18n/src/locales/pt-BR/translations.json +++ b/packages/i18n/src/locales/pt-BR/translations.json @@ -1110,6 +1110,18 @@ "remove": { "success": "Sub-item de trabalho removido com sucesso", "error": "Erro ao remover sub-item de trabalho" + }, + "empty_state": { + "sub_list_filters": { + "title": "Você não tem sub-itens de trabalho que correspondem aos filtros que você aplicou.", + "description": "Para ver todos os sub-itens de trabalho, limpe todos os filtros aplicados.", + "action": "Limpar filtros" + }, + "list_filters": { + "title": "Você não tem itens de trabalho que correspondem aos filtros que você aplicou.", + "description": "Para ver todos os itens de trabalho, limpe todos os filtros aplicados.", + "action": "Limpar filtros" + } } }, "view": { diff --git a/packages/i18n/src/locales/ro/translations.json b/packages/i18n/src/locales/ro/translations.json index a88ef0609..f60a4881b 100644 --- a/packages/i18n/src/locales/ro/translations.json +++ b/packages/i18n/src/locales/ro/translations.json @@ -1108,6 +1108,18 @@ "remove": { "success": "Sub-activitatea a fost eliminată cu succes", "error": "Eroare la eliminarea sub-activității" + }, + "empty_state": { + "sub_list_filters": { + "title": "Nu ai sub-elemente de lucru care corespund filtrelor pe care le-ai aplicat.", + "description": "Pentru a vedea toate sub-elementele de lucru, șterge toate filtrele aplicate.", + "action": "Șterge filtrele" + }, + "list_filters": { + "title": "Nu ai elemente de lucru care corespund filtrelor pe care le-ai aplicat.", + "description": "Pentru a vedea toate elementele de lucru, șterge toate filtrele aplicate.", + "action": "Șterge filtrele" + } } }, "view": { diff --git a/packages/i18n/src/locales/ru/translations.json b/packages/i18n/src/locales/ru/translations.json index 4159cccd3..564716529 100644 --- a/packages/i18n/src/locales/ru/translations.json +++ b/packages/i18n/src/locales/ru/translations.json @@ -1110,6 +1110,18 @@ "remove": { "success": "Подэлемент успешно удален", "error": "Ошибка удаления подэлемента" + }, + "empty_state": { + "sub_list_filters": { + "title": "У вас нет подэлементов, которые соответствуют примененным фильтрам.", + "description": "Чтобы увидеть все подэлементы, очистите все примененные фильтры.", + "action": "Очистить фильтры" + }, + "list_filters": { + "title": "У вас нет рабочих элементов, которые соответствуют примененным фильтрам.", + "description": "Чтобы увидеть все рабочие элементы, очистите все примененные фильтры.", + "action": "Очистить фильтры" + } } }, "view": { diff --git a/packages/i18n/src/locales/sk/translations.json b/packages/i18n/src/locales/sk/translations.json index f7093e59c..60f2c21ca 100644 --- a/packages/i18n/src/locales/sk/translations.json +++ b/packages/i18n/src/locales/sk/translations.json @@ -1110,6 +1110,18 @@ "remove": { "success": "Podriadená pracovná položka bola úspešne odstránená", "error": "Chyba pri odstraňovaní podriadenej položky" + }, + "empty_state": { + "sub_list_filters": { + "title": "Nemáte podriadené pracovné položky, ktoré zodpovedajú použitým filtrom.", + "description": "Pre zobrazenie všetkých podriadených pracovných položiek vymažte všetky použité filtre.", + "action": "Vymazať filtre" + }, + "list_filters": { + "title": "Nemáte pracovné položky, ktoré zodpovedajú použitým filtrom.", + "description": "Pre zobrazenie všetkých pracovných položiek vymažte všetky použité filtre.", + "action": "Vymazať filtre" + } } }, "view": { diff --git a/packages/i18n/src/locales/tr-TR/translations.json b/packages/i18n/src/locales/tr-TR/translations.json index de2cb9d7f..cec11a992 100644 --- a/packages/i18n/src/locales/tr-TR/translations.json +++ b/packages/i18n/src/locales/tr-TR/translations.json @@ -1111,6 +1111,18 @@ "remove": { "success": "Alt iş öğesi başarıyla kaldırıldı", "error": "Alt iş öğesi kaldırılırken hata oluştu" + }, + "empty_state": { + "sub_list_filters": { + "title": "Alt iş öğelerinizin filtreleriyle eşleşmiyor.", + "description": "Tüm alt iş öğelerini görmek için tüm uygulanan filtreleri temizleyin.", + "action": "Filtreleri temizle" + }, + "list_filters": { + "title": "İş öğelerinizin filtreleriyle eşleşmiyor.", + "description": "Tüm iş öğelerini görmek için tüm uygulanan filtreleri temizleyin.", + "action": "Filtreleri temizle" + } } }, "view": { diff --git a/packages/i18n/src/locales/ua/translations.json b/packages/i18n/src/locales/ua/translations.json index 9be16b11d..2a82df68f 100644 --- a/packages/i18n/src/locales/ua/translations.json +++ b/packages/i18n/src/locales/ua/translations.json @@ -1110,6 +1110,18 @@ "remove": { "success": "Похідну робочу одиницю успішно вилучено", "error": "Помилка під час вилучення похідної одиниці" + }, + "empty_state": { + "sub_list_filters": { + "title": "Ви не маєте похідних робочих одиниць, які відповідають застосованим фільтрам.", + "description": "Щоб побачити всі похідні робочі одиниці, очистіть всі застосовані фільтри.", + "action": "Очистити фільтри" + }, + "list_filters": { + "title": "Ви не маєте робочих одиниць, які відповідають застосованим фільтрам.", + "description": "Щоб побачити всі робочі одиниці, очистіть всі застосовані фільтри.", + "action": "Очистити фільтри" + } } }, "view": { diff --git a/packages/i18n/src/locales/vi-VN/translations.json b/packages/i18n/src/locales/vi-VN/translations.json index 756eea3d9..418d96ac4 100644 --- a/packages/i18n/src/locales/vi-VN/translations.json +++ b/packages/i18n/src/locales/vi-VN/translations.json @@ -1109,6 +1109,18 @@ "remove": { "success": "Đã xóa mục công việc con thành công", "error": "Đã xảy ra lỗi khi xóa mục công việc con" + }, + "empty_state": { + "sub_list_filters": { + "title": "Bạn không có mục công việc con nào phù hợp với các bộ lọc mà bạn đã áp dụng.", + "description": "Để xem tất cả các mục công việc con, hãy xóa tất cả các bộ lọc đã áp dụng.", + "action": "Xóa bộ lọc" + }, + "list_filters": { + "title": "Bạn không có mục công việc nào phù hợp với các bộ lọc mà bạn đã áp dụng.", + "description": "Để xem tất cả các mục công việc, hãy xóa tất cả các bộ lọc đã áp dụng.", + "action": "Xóa bộ lọc" + } } }, "view": { diff --git a/packages/i18n/src/locales/zh-CN/translations.json b/packages/i18n/src/locales/zh-CN/translations.json index 8dbdb5523..8f8ca2d26 100644 --- a/packages/i18n/src/locales/zh-CN/translations.json +++ b/packages/i18n/src/locales/zh-CN/translations.json @@ -1109,6 +1109,18 @@ "remove": { "success": "子工作项移除成功", "error": "移除子工作项时出错" + }, + "empty_state": { + "sub_list_filters": { + "title": "您没有符合您应用的过滤器的子工作项。", + "description": "要查看所有子工作项,请清除所有应用的过滤器。", + "action": "清除过滤器" + }, + "list_filters": { + "title": "您没有符合您应用的过滤器的工作项。", + "description": "要查看所有工作项,请清除所有应用的过滤器。", + "action": "清除过滤器" + } } }, "view": { diff --git a/packages/i18n/src/locales/zh-TW/translations.json b/packages/i18n/src/locales/zh-TW/translations.json index 53888c516..472ba631c 100644 --- a/packages/i18n/src/locales/zh-TW/translations.json +++ b/packages/i18n/src/locales/zh-TW/translations.json @@ -1110,6 +1110,18 @@ "remove": { "success": "子工作事項移除成功", "error": "移除子工作事項時發生錯誤" + }, + "empty_state": { + "sub_list_filters": { + "title": "您沒有符合您應用過的過濾器的子工作事項。", + "description": "要查看所有子工作事項,請清除所有應用過的過濾器。", + "action": "清除過濾器" + }, + "list_filters": { + "title": "您沒有符合您應用過的過濾器的工作事項。", + "description": "要查看所有工作事項,請清除所有應用過的過濾器。", + "action": "清除過濾器" + } } }, "view": { diff --git a/web/core/components/issues/issue-detail-widgets/sub-issues/issues-list/root.tsx b/web/core/components/issues/issue-detail-widgets/sub-issues/issues-list/root.tsx index 6b101829e..c99f99086 100644 --- a/web/core/components/issues/issue-detail-widgets/sub-issues/issues-list/root.tsx +++ b/web/core/components/issues/issue-detail-widgets/sub-issues/issues-list/root.tsx @@ -3,6 +3,7 @@ import { observer } from "mobx-react"; // plane imports import { ListFilter } from "lucide-react"; import { EIssueServiceType, EIssuesStoreType } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; import { GroupByColumnTypes, TIssue, TIssueServiceType, TSubIssueOperations } from "@plane/types"; // hooks import { Button, Loader } from "@plane/ui"; @@ -41,10 +42,12 @@ export const SubIssuesListRoot: React.FC = observer((props) => { storeType = EIssuesStoreType.PROJECT, spacingLeft = 0, } = props; + const { t } = useTranslation(); // store hooks const { subIssues: { - subIssuesByIssueId, loader, + subIssuesByIssueId, + loader, filters: { getSubIssueFilters, getGroupedSubWorkItems, getFilteredSubWorkItems, resetFilters }, }, } = useIssueDetail(issueServiceType); @@ -77,15 +80,15 @@ export const SubIssuesListRoot: React.FC = observer((props) => { const isSubWorkItems = issueServiceType === EIssueServiceType.ISSUES; - if (loader === "init-loader") { - return ( - - {Array.from({ length: 5 }).map((_, index) => ( - - ))} - - ); - } + if (loader === "init-loader") { + return ( + + {Array.from({ length: 5 }).map((_, index) => ( + + ))} + + ); + } return (
@@ -93,19 +96,19 @@ export const SubIssuesListRoot: React.FC = observer((props) => { } customClassName={storeType !== EIssuesStoreType.EPIC ? "border-none" : ""} actionElement={ } /> From f8ca1e46b15684e281aa36e86b39c58e30b8d24b Mon Sep 17 00:00:00 2001 From: Aaron Heckmann Date: Fri, 23 May 2025 03:42:04 -0700 Subject: [PATCH 081/201] [WEB-4098] feat: noindex/nofollow (#7088) * feat: noindex/nofollow - On login: nofollow - On app pages: noindex, nofollow https://app.plane.so/plane/browse/WEB-4098/ - https://nextjs.org/docs/app/api-reference/file-conventions/layout - https://nextjs.org/docs/app/building-your-application/routing/route-groups#creating-multiple-root-layouts - https://nextjs.org/docs/app/api-reference/functions/generate-metadata#link-relpreload * chore: address PR feedback --- .../(projects)/active-cycles/header.tsx | 0 .../(projects)/active-cycles/layout.tsx | 0 .../(projects)/active-cycles/page.tsx | 0 .../(projects)/analytics/header.tsx | 0 .../(projects)/analytics/layout.tsx | 0 .../(projects)/analytics/page.tsx | 0 .../(projects)/browse/[workItem]/header.tsx | 0 .../(projects)/browse/[workItem]/layout.tsx | 0 .../(projects)/browse/[workItem]/page.tsx | 0 .../(projects)/drafts/header.tsx | 0 .../(projects)/drafts/layout.tsx | 0 .../(projects)/drafts/page.tsx | 0 .../(projects)/extended-project-sidebar.tsx | 0 .../(projects)/extended-sidebar.tsx | 0 .../[workspaceSlug]/(projects)/header.tsx | 0 .../[workspaceSlug]/(projects)/layout.tsx | 0 .../(projects)/notifications/layout.tsx | 0 .../(projects)/notifications/page.tsx | 0 .../[workspaceSlug]/(projects)/page.tsx | 0 .../profile/[userId]/[profileViewId]/page.tsx | 0 .../profile/[userId]/activity/page.tsx | 0 .../(projects)/profile/[userId]/header.tsx | 0 .../(projects)/profile/[userId]/layout.tsx | 0 .../profile/[userId]/mobile-header.tsx | 0 .../(projects)/profile/[userId]/navbar.tsx | 0 .../(projects)/profile/[userId]/page.tsx | 0 .../[projectId]/archives/cycles/layout.tsx | 0 .../[projectId]/archives/cycles/page.tsx | 0 .../(detail)/[projectId]/archives/header.tsx | 0 .../(detail)/[archivedIssueId]/page.tsx | 0 .../archives/issues/(detail)/header.tsx | 0 .../archives/issues/(detail)/layout.tsx | 0 .../archives/issues/(list)/layout.tsx | 0 .../archives/issues/(list)/page.tsx | 0 .../[projectId]/archives/modules/layout.tsx | 0 .../[projectId]/archives/modules/page.tsx | 0 .../cycles/(detail)/[cycleId]/page.tsx | 0 .../[projectId]/cycles/(detail)/header.tsx | 0 .../[projectId]/cycles/(detail)/layout.tsx | 0 .../cycles/(detail)/mobile-header.tsx | 0 .../[projectId]/cycles/(list)/header.tsx | 0 .../[projectId]/cycles/(list)/layout.tsx | 0 .../cycles/(list)/mobile-header.tsx | 0 .../[projectId]/cycles/(list)/page.tsx | 0 .../[projectId]/draft-issues/header.tsx | 0 .../[projectId]/draft-issues/layout.tsx | 0 .../[projectId]/draft-issues/page.tsx | 0 .../(detail)/[projectId]/intake/layout.tsx | 0 .../(detail)/[projectId]/intake/page.tsx | 0 .../issues/(detail)/[issueId]/page.tsx | 0 .../[projectId]/issues/(list)/header.tsx | 0 .../[projectId]/issues/(list)/layout.tsx | 0 .../issues/(list)/mobile-header.tsx | 0 .../[projectId]/issues/(list)/page.tsx | 0 .../modules/(detail)/[moduleId]/page.tsx | 0 .../[projectId]/modules/(detail)/header.tsx | 0 .../[projectId]/modules/(detail)/layout.tsx | 0 .../modules/(detail)/mobile-header.tsx | 0 .../[projectId]/modules/(list)/header.tsx | 0 .../[projectId]/modules/(list)/layout.tsx | 0 .../modules/(list)/mobile-header.tsx | 0 .../[projectId]/modules/(list)/page.tsx | 0 .../pages/(detail)/[pageId]/page.tsx | 0 .../[projectId]/pages/(detail)/header.tsx | 0 .../[projectId]/pages/(detail)/layout.tsx | 0 .../[projectId]/pages/(list)/header.tsx | 0 .../[projectId]/pages/(list)/layout.tsx | 0 .../[projectId]/pages/(list)/page.tsx | 0 .../(with-sidebar)/automations/page.tsx | 0 .../(with-sidebar)/estimates/page.tsx | 0 .../settings/(with-sidebar)/features/page.tsx | 0 .../settings/(with-sidebar)/labels/page.tsx | 0 .../settings/(with-sidebar)/layout.tsx | 0 .../settings/(with-sidebar)/members/page.tsx | 0 .../settings/(with-sidebar)/page.tsx | 0 .../settings/(with-sidebar)/sidebar.tsx | 0 .../settings/(with-sidebar)/states/page.tsx | 0 .../(detail)/[projectId]/settings/header.tsx | 0 .../views/(detail)/[viewId]/header.tsx | 0 .../views/(detail)/[viewId]/page.tsx | 0 .../[projectId]/views/(detail)/layout.tsx | 0 .../[projectId]/views/(list)/header.tsx | 0 .../[projectId]/views/(list)/layout.tsx | 0 .../views/(list)/mobile-header.tsx | 0 .../[projectId]/views/(list)/page.tsx | 0 .../projects/(detail)/archives/layout.tsx | 0 .../projects/(detail)/archives/page.tsx | 0 .../(projects)/projects/(detail)/layout.tsx | 0 .../(projects)/projects/(list)/layout.tsx | 0 .../(projects)/projects/(list)/page.tsx | 0 .../(with-sidebar)/api-tokens/page.tsx | 0 .../settings/(with-sidebar)/billing/page.tsx | 0 .../settings/(with-sidebar)/exports/page.tsx | 0 .../settings/(with-sidebar)/imports/page.tsx | 0 .../(with-sidebar)/integrations/page.tsx | 0 .../settings/(with-sidebar)/layout.tsx | 0 .../settings/(with-sidebar)/members/page.tsx | 0 .../(with-sidebar)/mobile-header-tabs.tsx | 0 .../settings/(with-sidebar)/page.tsx | 0 .../settings/(with-sidebar)/sidebar.tsx | 0 .../webhooks/[webhookId]/page.tsx | 0 .../settings/(with-sidebar)/webhooks/page.tsx | 0 .../(projects)/settings/header.tsx | 0 .../[workspaceSlug]/(projects)/sidebar.tsx | 0 .../(projects)/stickies/header.tsx | 0 .../(projects)/stickies/layout.tsx | 0 .../(projects)/stickies/page.tsx | 0 .../workspace-views/[globalViewId]/page.tsx | 0 .../(projects)/workspace-views/header.tsx | 0 .../(projects)/workspace-views/layout.tsx | 0 .../(projects)/workspace-views/page.tsx | 0 .../accounts/forgot-password/layout.tsx | 0 .../accounts/forgot-password/page.tsx | 0 .../accounts/reset-password/layout.tsx | 0 .../accounts/reset-password/page.tsx | 0 .../accounts/set-password/layout.tsx | 0 .../accounts/set-password/page.tsx | 0 .../{ => (all)}/create-workspace/layout.tsx | 0 web/app/{ => (all)}/create-workspace/page.tsx | 0 .../installations/[provider]/layout.tsx | 0 .../installations/[provider]/page.tsx | 0 web/app/{ => (all)}/invitations/layout.tsx | 0 web/app/{ => (all)}/invitations/page.tsx | 0 web/app/(all)/layout.preload.tsx | 28 +++++++++++++++++ web/app/(all)/layout.tsx | 31 +++++++++++++++++++ web/app/{ => (all)}/onboarding/layout.tsx | 0 web/app/{ => (all)}/onboarding/page.tsx | 0 web/app/{ => (all)}/profile/activity/page.tsx | 0 .../{ => (all)}/profile/appearance/page.tsx | 0 web/app/{ => (all)}/profile/layout.tsx | 0 .../profile/notifications/page.tsx | 0 web/app/{ => (all)}/profile/page.tsx | 0 web/app/{ => (all)}/profile/security/page.tsx | 0 web/app/{ => (all)}/profile/sidebar.tsx | 0 web/app/{ => (all)}/sign-up/layout.tsx | 4 +++ web/app/{ => (all)}/sign-up/page.tsx | 0 .../workspace-invitations/layout.tsx | 0 .../workspace-invitations/page.tsx | 0 web/app/(home)/layout.tsx | 21 +++++++++++++ web/app/{ => (home)}/page.tsx | 0 web/app/layout.tsx | 20 +++--------- web/app/not-found.tsx | 4 +++ 142 files changed, 92 insertions(+), 16 deletions(-) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/active-cycles/header.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/active-cycles/layout.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/active-cycles/page.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/analytics/header.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/analytics/layout.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/analytics/page.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/browse/[workItem]/header.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/browse/[workItem]/layout.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/browse/[workItem]/page.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/drafts/header.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/drafts/layout.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/drafts/page.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/extended-project-sidebar.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/extended-sidebar.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/header.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/layout.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/notifications/layout.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/notifications/page.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/page.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/profile/[userId]/[profileViewId]/page.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/profile/[userId]/activity/page.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/profile/[userId]/header.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/profile/[userId]/layout.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/profile/[userId]/mobile-header.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/profile/[userId]/navbar.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/profile/[userId]/page.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/cycles/layout.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/cycles/page.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/header.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(detail)/[archivedIssueId]/page.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(detail)/header.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(detail)/layout.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(list)/layout.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(list)/page.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/modules/layout.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/modules/page.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/[cycleId]/page.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/header.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/layout.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/mobile-header.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(list)/header.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(list)/layout.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(list)/mobile-header.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(list)/page.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/draft-issues/header.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/draft-issues/layout.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/draft-issues/page.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/intake/layout.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/intake/page.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(detail)/[issueId]/page.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(list)/header.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(list)/layout.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(list)/mobile-header.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(list)/page.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/[moduleId]/page.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/header.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/layout.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/mobile-header.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(list)/header.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(list)/layout.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(list)/mobile-header.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(list)/page.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/[pageId]/page.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/header.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/layout.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(list)/header.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(list)/layout.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(list)/page.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/automations/page.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/estimates/page.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/features/page.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/labels/page.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/layout.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/members/page.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/page.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/sidebar.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/states/page.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/header.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(detail)/[viewId]/header.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(detail)/[viewId]/page.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(detail)/layout.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(list)/header.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(list)/layout.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(list)/mobile-header.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(list)/page.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/projects/(detail)/archives/layout.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/projects/(detail)/archives/page.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/projects/(detail)/layout.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/projects/(list)/layout.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/projects/(list)/page.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/settings/(with-sidebar)/api-tokens/page.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/settings/(with-sidebar)/billing/page.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/settings/(with-sidebar)/exports/page.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/settings/(with-sidebar)/imports/page.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/settings/(with-sidebar)/integrations/page.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/settings/(with-sidebar)/layout.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/settings/(with-sidebar)/members/page.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/settings/(with-sidebar)/mobile-header-tabs.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/settings/(with-sidebar)/page.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/settings/(with-sidebar)/sidebar.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/settings/(with-sidebar)/webhooks/[webhookId]/page.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/settings/(with-sidebar)/webhooks/page.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/settings/header.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/sidebar.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/stickies/header.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/stickies/layout.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/stickies/page.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/workspace-views/[globalViewId]/page.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/workspace-views/header.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/workspace-views/layout.tsx (100%) rename web/app/{ => (all)}/[workspaceSlug]/(projects)/workspace-views/page.tsx (100%) rename web/app/{ => (all)}/accounts/forgot-password/layout.tsx (100%) rename web/app/{ => (all)}/accounts/forgot-password/page.tsx (100%) rename web/app/{ => (all)}/accounts/reset-password/layout.tsx (100%) rename web/app/{ => (all)}/accounts/reset-password/page.tsx (100%) rename web/app/{ => (all)}/accounts/set-password/layout.tsx (100%) rename web/app/{ => (all)}/accounts/set-password/page.tsx (100%) rename web/app/{ => (all)}/create-workspace/layout.tsx (100%) rename web/app/{ => (all)}/create-workspace/page.tsx (100%) rename web/app/{ => (all)}/installations/[provider]/layout.tsx (100%) rename web/app/{ => (all)}/installations/[provider]/page.tsx (100%) rename web/app/{ => (all)}/invitations/layout.tsx (100%) rename web/app/{ => (all)}/invitations/page.tsx (100%) create mode 100644 web/app/(all)/layout.preload.tsx create mode 100644 web/app/(all)/layout.tsx rename web/app/{ => (all)}/onboarding/layout.tsx (100%) rename web/app/{ => (all)}/onboarding/page.tsx (100%) rename web/app/{ => (all)}/profile/activity/page.tsx (100%) rename web/app/{ => (all)}/profile/appearance/page.tsx (100%) rename web/app/{ => (all)}/profile/layout.tsx (100%) rename web/app/{ => (all)}/profile/notifications/page.tsx (100%) rename web/app/{ => (all)}/profile/page.tsx (100%) rename web/app/{ => (all)}/profile/security/page.tsx (100%) rename web/app/{ => (all)}/profile/sidebar.tsx (100%) rename web/app/{ => (all)}/sign-up/layout.tsx (79%) rename web/app/{ => (all)}/sign-up/page.tsx (100%) rename web/app/{ => (all)}/workspace-invitations/layout.tsx (100%) rename web/app/{ => (all)}/workspace-invitations/page.tsx (100%) create mode 100644 web/app/(home)/layout.tsx rename web/app/{ => (home)}/page.tsx (100%) diff --git a/web/app/[workspaceSlug]/(projects)/active-cycles/header.tsx b/web/app/(all)/[workspaceSlug]/(projects)/active-cycles/header.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/active-cycles/header.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/active-cycles/header.tsx diff --git a/web/app/[workspaceSlug]/(projects)/active-cycles/layout.tsx b/web/app/(all)/[workspaceSlug]/(projects)/active-cycles/layout.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/active-cycles/layout.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/active-cycles/layout.tsx diff --git a/web/app/[workspaceSlug]/(projects)/active-cycles/page.tsx b/web/app/(all)/[workspaceSlug]/(projects)/active-cycles/page.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/active-cycles/page.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/active-cycles/page.tsx diff --git a/web/app/[workspaceSlug]/(projects)/analytics/header.tsx b/web/app/(all)/[workspaceSlug]/(projects)/analytics/header.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/analytics/header.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/analytics/header.tsx diff --git a/web/app/[workspaceSlug]/(projects)/analytics/layout.tsx b/web/app/(all)/[workspaceSlug]/(projects)/analytics/layout.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/analytics/layout.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/analytics/layout.tsx diff --git a/web/app/[workspaceSlug]/(projects)/analytics/page.tsx b/web/app/(all)/[workspaceSlug]/(projects)/analytics/page.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/analytics/page.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/analytics/page.tsx diff --git a/web/app/[workspaceSlug]/(projects)/browse/[workItem]/header.tsx b/web/app/(all)/[workspaceSlug]/(projects)/browse/[workItem]/header.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/browse/[workItem]/header.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/browse/[workItem]/header.tsx diff --git a/web/app/[workspaceSlug]/(projects)/browse/[workItem]/layout.tsx b/web/app/(all)/[workspaceSlug]/(projects)/browse/[workItem]/layout.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/browse/[workItem]/layout.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/browse/[workItem]/layout.tsx diff --git a/web/app/[workspaceSlug]/(projects)/browse/[workItem]/page.tsx b/web/app/(all)/[workspaceSlug]/(projects)/browse/[workItem]/page.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/browse/[workItem]/page.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/browse/[workItem]/page.tsx diff --git a/web/app/[workspaceSlug]/(projects)/drafts/header.tsx b/web/app/(all)/[workspaceSlug]/(projects)/drafts/header.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/drafts/header.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/drafts/header.tsx diff --git a/web/app/[workspaceSlug]/(projects)/drafts/layout.tsx b/web/app/(all)/[workspaceSlug]/(projects)/drafts/layout.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/drafts/layout.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/drafts/layout.tsx diff --git a/web/app/[workspaceSlug]/(projects)/drafts/page.tsx b/web/app/(all)/[workspaceSlug]/(projects)/drafts/page.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/drafts/page.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/drafts/page.tsx diff --git a/web/app/[workspaceSlug]/(projects)/extended-project-sidebar.tsx b/web/app/(all)/[workspaceSlug]/(projects)/extended-project-sidebar.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/extended-project-sidebar.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/extended-project-sidebar.tsx diff --git a/web/app/[workspaceSlug]/(projects)/extended-sidebar.tsx b/web/app/(all)/[workspaceSlug]/(projects)/extended-sidebar.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/extended-sidebar.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/extended-sidebar.tsx diff --git a/web/app/[workspaceSlug]/(projects)/header.tsx b/web/app/(all)/[workspaceSlug]/(projects)/header.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/header.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/header.tsx diff --git a/web/app/[workspaceSlug]/(projects)/layout.tsx b/web/app/(all)/[workspaceSlug]/(projects)/layout.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/layout.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/layout.tsx diff --git a/web/app/[workspaceSlug]/(projects)/notifications/layout.tsx b/web/app/(all)/[workspaceSlug]/(projects)/notifications/layout.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/notifications/layout.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/notifications/layout.tsx diff --git a/web/app/[workspaceSlug]/(projects)/notifications/page.tsx b/web/app/(all)/[workspaceSlug]/(projects)/notifications/page.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/notifications/page.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/notifications/page.tsx diff --git a/web/app/[workspaceSlug]/(projects)/page.tsx b/web/app/(all)/[workspaceSlug]/(projects)/page.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/page.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/page.tsx diff --git a/web/app/[workspaceSlug]/(projects)/profile/[userId]/[profileViewId]/page.tsx b/web/app/(all)/[workspaceSlug]/(projects)/profile/[userId]/[profileViewId]/page.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/profile/[userId]/[profileViewId]/page.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/profile/[userId]/[profileViewId]/page.tsx diff --git a/web/app/[workspaceSlug]/(projects)/profile/[userId]/activity/page.tsx b/web/app/(all)/[workspaceSlug]/(projects)/profile/[userId]/activity/page.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/profile/[userId]/activity/page.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/profile/[userId]/activity/page.tsx diff --git a/web/app/[workspaceSlug]/(projects)/profile/[userId]/header.tsx b/web/app/(all)/[workspaceSlug]/(projects)/profile/[userId]/header.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/profile/[userId]/header.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/profile/[userId]/header.tsx diff --git a/web/app/[workspaceSlug]/(projects)/profile/[userId]/layout.tsx b/web/app/(all)/[workspaceSlug]/(projects)/profile/[userId]/layout.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/profile/[userId]/layout.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/profile/[userId]/layout.tsx diff --git a/web/app/[workspaceSlug]/(projects)/profile/[userId]/mobile-header.tsx b/web/app/(all)/[workspaceSlug]/(projects)/profile/[userId]/mobile-header.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/profile/[userId]/mobile-header.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/profile/[userId]/mobile-header.tsx diff --git a/web/app/[workspaceSlug]/(projects)/profile/[userId]/navbar.tsx b/web/app/(all)/[workspaceSlug]/(projects)/profile/[userId]/navbar.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/profile/[userId]/navbar.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/profile/[userId]/navbar.tsx diff --git a/web/app/[workspaceSlug]/(projects)/profile/[userId]/page.tsx b/web/app/(all)/[workspaceSlug]/(projects)/profile/[userId]/page.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/profile/[userId]/page.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/profile/[userId]/page.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/cycles/layout.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/cycles/layout.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/cycles/layout.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/cycles/layout.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/cycles/page.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/cycles/page.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/cycles/page.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/cycles/page.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/header.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/header.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/header.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/header.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(detail)/[archivedIssueId]/page.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(detail)/[archivedIssueId]/page.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(detail)/[archivedIssueId]/page.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(detail)/[archivedIssueId]/page.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(detail)/header.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(detail)/header.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(detail)/header.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(detail)/header.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(detail)/layout.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(detail)/layout.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(detail)/layout.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(detail)/layout.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(list)/layout.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(list)/layout.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(list)/layout.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(list)/layout.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(list)/page.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(list)/page.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(list)/page.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(list)/page.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/modules/layout.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/modules/layout.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/modules/layout.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/modules/layout.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/modules/page.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/modules/page.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/modules/page.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/modules/page.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/[cycleId]/page.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/[cycleId]/page.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/[cycleId]/page.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/[cycleId]/page.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/header.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/header.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/header.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/header.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/layout.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/layout.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/layout.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/layout.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/mobile-header.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/mobile-header.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/mobile-header.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/mobile-header.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(list)/header.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(list)/header.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(list)/header.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(list)/header.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(list)/layout.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(list)/layout.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(list)/layout.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(list)/layout.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(list)/mobile-header.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(list)/mobile-header.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(list)/mobile-header.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(list)/mobile-header.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(list)/page.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(list)/page.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(list)/page.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(list)/page.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/draft-issues/header.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/draft-issues/header.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/draft-issues/header.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/draft-issues/header.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/draft-issues/layout.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/draft-issues/layout.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/draft-issues/layout.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/draft-issues/layout.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/draft-issues/page.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/draft-issues/page.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/draft-issues/page.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/draft-issues/page.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/intake/layout.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/intake/layout.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/intake/layout.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/intake/layout.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/intake/page.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/intake/page.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/intake/page.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/intake/page.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(detail)/[issueId]/page.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(detail)/[issueId]/page.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(detail)/[issueId]/page.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(detail)/[issueId]/page.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(list)/header.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(list)/header.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(list)/header.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(list)/header.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(list)/layout.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(list)/layout.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(list)/layout.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(list)/layout.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(list)/mobile-header.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(list)/mobile-header.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(list)/mobile-header.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(list)/mobile-header.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(list)/page.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(list)/page.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(list)/page.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(list)/page.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/[moduleId]/page.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/[moduleId]/page.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/[moduleId]/page.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/[moduleId]/page.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/header.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/header.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/header.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/header.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/layout.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/layout.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/layout.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/layout.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/mobile-header.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/mobile-header.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/mobile-header.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/mobile-header.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(list)/header.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(list)/header.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(list)/header.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(list)/header.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(list)/layout.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(list)/layout.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(list)/layout.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(list)/layout.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(list)/mobile-header.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(list)/mobile-header.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(list)/mobile-header.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(list)/mobile-header.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(list)/page.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(list)/page.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(list)/page.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(list)/page.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/[pageId]/page.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/[pageId]/page.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/[pageId]/page.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/[pageId]/page.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/header.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/header.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/header.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/header.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/layout.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/layout.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/layout.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/layout.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(list)/header.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(list)/header.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(list)/header.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(list)/header.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(list)/layout.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(list)/layout.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(list)/layout.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(list)/layout.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(list)/page.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(list)/page.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(list)/page.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(list)/page.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/automations/page.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/automations/page.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/automations/page.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/automations/page.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/estimates/page.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/estimates/page.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/estimates/page.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/estimates/page.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/features/page.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/features/page.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/features/page.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/features/page.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/labels/page.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/labels/page.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/labels/page.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/labels/page.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/layout.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/layout.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/layout.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/layout.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/members/page.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/members/page.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/members/page.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/members/page.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/page.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/page.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/page.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/page.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/sidebar.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/sidebar.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/sidebar.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/sidebar.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/states/page.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/states/page.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/states/page.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/states/page.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/header.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/header.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/header.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/header.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(detail)/[viewId]/header.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(detail)/[viewId]/header.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(detail)/[viewId]/header.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(detail)/[viewId]/header.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(detail)/[viewId]/page.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(detail)/[viewId]/page.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(detail)/[viewId]/page.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(detail)/[viewId]/page.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(detail)/layout.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(detail)/layout.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(detail)/layout.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(detail)/layout.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(list)/header.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(list)/header.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(list)/header.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(list)/header.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(list)/layout.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(list)/layout.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(list)/layout.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(list)/layout.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(list)/mobile-header.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(list)/mobile-header.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(list)/mobile-header.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(list)/mobile-header.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(list)/page.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(list)/page.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(list)/page.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(list)/page.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/archives/layout.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/archives/layout.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(detail)/archives/layout.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/archives/layout.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/archives/page.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/archives/page.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(detail)/archives/page.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/archives/page.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/layout.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/layout.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(detail)/layout.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/layout.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(list)/layout.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(list)/layout.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(list)/layout.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/projects/(list)/layout.tsx diff --git a/web/app/[workspaceSlug]/(projects)/projects/(list)/page.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(list)/page.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/projects/(list)/page.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/projects/(list)/page.tsx diff --git a/web/app/[workspaceSlug]/(projects)/settings/(with-sidebar)/api-tokens/page.tsx b/web/app/(all)/[workspaceSlug]/(projects)/settings/(with-sidebar)/api-tokens/page.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/settings/(with-sidebar)/api-tokens/page.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/settings/(with-sidebar)/api-tokens/page.tsx diff --git a/web/app/[workspaceSlug]/(projects)/settings/(with-sidebar)/billing/page.tsx b/web/app/(all)/[workspaceSlug]/(projects)/settings/(with-sidebar)/billing/page.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/settings/(with-sidebar)/billing/page.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/settings/(with-sidebar)/billing/page.tsx diff --git a/web/app/[workspaceSlug]/(projects)/settings/(with-sidebar)/exports/page.tsx b/web/app/(all)/[workspaceSlug]/(projects)/settings/(with-sidebar)/exports/page.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/settings/(with-sidebar)/exports/page.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/settings/(with-sidebar)/exports/page.tsx diff --git a/web/app/[workspaceSlug]/(projects)/settings/(with-sidebar)/imports/page.tsx b/web/app/(all)/[workspaceSlug]/(projects)/settings/(with-sidebar)/imports/page.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/settings/(with-sidebar)/imports/page.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/settings/(with-sidebar)/imports/page.tsx diff --git a/web/app/[workspaceSlug]/(projects)/settings/(with-sidebar)/integrations/page.tsx b/web/app/(all)/[workspaceSlug]/(projects)/settings/(with-sidebar)/integrations/page.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/settings/(with-sidebar)/integrations/page.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/settings/(with-sidebar)/integrations/page.tsx diff --git a/web/app/[workspaceSlug]/(projects)/settings/(with-sidebar)/layout.tsx b/web/app/(all)/[workspaceSlug]/(projects)/settings/(with-sidebar)/layout.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/settings/(with-sidebar)/layout.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/settings/(with-sidebar)/layout.tsx diff --git a/web/app/[workspaceSlug]/(projects)/settings/(with-sidebar)/members/page.tsx b/web/app/(all)/[workspaceSlug]/(projects)/settings/(with-sidebar)/members/page.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/settings/(with-sidebar)/members/page.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/settings/(with-sidebar)/members/page.tsx diff --git a/web/app/[workspaceSlug]/(projects)/settings/(with-sidebar)/mobile-header-tabs.tsx b/web/app/(all)/[workspaceSlug]/(projects)/settings/(with-sidebar)/mobile-header-tabs.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/settings/(with-sidebar)/mobile-header-tabs.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/settings/(with-sidebar)/mobile-header-tabs.tsx diff --git a/web/app/[workspaceSlug]/(projects)/settings/(with-sidebar)/page.tsx b/web/app/(all)/[workspaceSlug]/(projects)/settings/(with-sidebar)/page.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/settings/(with-sidebar)/page.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/settings/(with-sidebar)/page.tsx diff --git a/web/app/[workspaceSlug]/(projects)/settings/(with-sidebar)/sidebar.tsx b/web/app/(all)/[workspaceSlug]/(projects)/settings/(with-sidebar)/sidebar.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/settings/(with-sidebar)/sidebar.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/settings/(with-sidebar)/sidebar.tsx diff --git a/web/app/[workspaceSlug]/(projects)/settings/(with-sidebar)/webhooks/[webhookId]/page.tsx b/web/app/(all)/[workspaceSlug]/(projects)/settings/(with-sidebar)/webhooks/[webhookId]/page.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/settings/(with-sidebar)/webhooks/[webhookId]/page.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/settings/(with-sidebar)/webhooks/[webhookId]/page.tsx diff --git a/web/app/[workspaceSlug]/(projects)/settings/(with-sidebar)/webhooks/page.tsx b/web/app/(all)/[workspaceSlug]/(projects)/settings/(with-sidebar)/webhooks/page.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/settings/(with-sidebar)/webhooks/page.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/settings/(with-sidebar)/webhooks/page.tsx diff --git a/web/app/[workspaceSlug]/(projects)/settings/header.tsx b/web/app/(all)/[workspaceSlug]/(projects)/settings/header.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/settings/header.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/settings/header.tsx diff --git a/web/app/[workspaceSlug]/(projects)/sidebar.tsx b/web/app/(all)/[workspaceSlug]/(projects)/sidebar.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/sidebar.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/sidebar.tsx diff --git a/web/app/[workspaceSlug]/(projects)/stickies/header.tsx b/web/app/(all)/[workspaceSlug]/(projects)/stickies/header.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/stickies/header.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/stickies/header.tsx diff --git a/web/app/[workspaceSlug]/(projects)/stickies/layout.tsx b/web/app/(all)/[workspaceSlug]/(projects)/stickies/layout.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/stickies/layout.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/stickies/layout.tsx diff --git a/web/app/[workspaceSlug]/(projects)/stickies/page.tsx b/web/app/(all)/[workspaceSlug]/(projects)/stickies/page.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/stickies/page.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/stickies/page.tsx diff --git a/web/app/[workspaceSlug]/(projects)/workspace-views/[globalViewId]/page.tsx b/web/app/(all)/[workspaceSlug]/(projects)/workspace-views/[globalViewId]/page.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/workspace-views/[globalViewId]/page.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/workspace-views/[globalViewId]/page.tsx diff --git a/web/app/[workspaceSlug]/(projects)/workspace-views/header.tsx b/web/app/(all)/[workspaceSlug]/(projects)/workspace-views/header.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/workspace-views/header.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/workspace-views/header.tsx diff --git a/web/app/[workspaceSlug]/(projects)/workspace-views/layout.tsx b/web/app/(all)/[workspaceSlug]/(projects)/workspace-views/layout.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/workspace-views/layout.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/workspace-views/layout.tsx diff --git a/web/app/[workspaceSlug]/(projects)/workspace-views/page.tsx b/web/app/(all)/[workspaceSlug]/(projects)/workspace-views/page.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/workspace-views/page.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/workspace-views/page.tsx diff --git a/web/app/accounts/forgot-password/layout.tsx b/web/app/(all)/accounts/forgot-password/layout.tsx similarity index 100% rename from web/app/accounts/forgot-password/layout.tsx rename to web/app/(all)/accounts/forgot-password/layout.tsx diff --git a/web/app/accounts/forgot-password/page.tsx b/web/app/(all)/accounts/forgot-password/page.tsx similarity index 100% rename from web/app/accounts/forgot-password/page.tsx rename to web/app/(all)/accounts/forgot-password/page.tsx diff --git a/web/app/accounts/reset-password/layout.tsx b/web/app/(all)/accounts/reset-password/layout.tsx similarity index 100% rename from web/app/accounts/reset-password/layout.tsx rename to web/app/(all)/accounts/reset-password/layout.tsx diff --git a/web/app/accounts/reset-password/page.tsx b/web/app/(all)/accounts/reset-password/page.tsx similarity index 100% rename from web/app/accounts/reset-password/page.tsx rename to web/app/(all)/accounts/reset-password/page.tsx diff --git a/web/app/accounts/set-password/layout.tsx b/web/app/(all)/accounts/set-password/layout.tsx similarity index 100% rename from web/app/accounts/set-password/layout.tsx rename to web/app/(all)/accounts/set-password/layout.tsx diff --git a/web/app/accounts/set-password/page.tsx b/web/app/(all)/accounts/set-password/page.tsx similarity index 100% rename from web/app/accounts/set-password/page.tsx rename to web/app/(all)/accounts/set-password/page.tsx diff --git a/web/app/create-workspace/layout.tsx b/web/app/(all)/create-workspace/layout.tsx similarity index 100% rename from web/app/create-workspace/layout.tsx rename to web/app/(all)/create-workspace/layout.tsx diff --git a/web/app/create-workspace/page.tsx b/web/app/(all)/create-workspace/page.tsx similarity index 100% rename from web/app/create-workspace/page.tsx rename to web/app/(all)/create-workspace/page.tsx diff --git a/web/app/installations/[provider]/layout.tsx b/web/app/(all)/installations/[provider]/layout.tsx similarity index 100% rename from web/app/installations/[provider]/layout.tsx rename to web/app/(all)/installations/[provider]/layout.tsx diff --git a/web/app/installations/[provider]/page.tsx b/web/app/(all)/installations/[provider]/page.tsx similarity index 100% rename from web/app/installations/[provider]/page.tsx rename to web/app/(all)/installations/[provider]/page.tsx diff --git a/web/app/invitations/layout.tsx b/web/app/(all)/invitations/layout.tsx similarity index 100% rename from web/app/invitations/layout.tsx rename to web/app/(all)/invitations/layout.tsx diff --git a/web/app/invitations/page.tsx b/web/app/(all)/invitations/page.tsx similarity index 100% rename from web/app/invitations/page.tsx rename to web/app/(all)/invitations/page.tsx diff --git a/web/app/(all)/layout.preload.tsx b/web/app/(all)/layout.preload.tsx new file mode 100644 index 000000000..18ca3b4b3 --- /dev/null +++ b/web/app/(all)/layout.preload.tsx @@ -0,0 +1,28 @@ +"use client"; + +import { useEffect } from "react"; +import ReactDOM from "react-dom"; + +// https://nextjs.org/docs/app/api-reference/functions/generate-metadata#link-relpreload +export const usePreloadResources = () => { + useEffect(() => { + const preloadItem = (url: string) => { + ReactDOM.preload(url, { as: "fetch", crossOrigin: "use-credentials" }); + }; + + const urls = [ + `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/instances/`, + `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/users/me/`, + `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/users/me/profile/`, + `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/users/me/settings/`, + `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/users/me/workspaces/?v=${Date.now()}`, + ]; + + urls.forEach(url => preloadItem(url)); + }, []); +}; + +export const PreloadResources = () => { + usePreloadResources(); + return null; +}; diff --git a/web/app/(all)/layout.tsx b/web/app/(all)/layout.tsx new file mode 100644 index 000000000..023852648 --- /dev/null +++ b/web/app/(all)/layout.tsx @@ -0,0 +1,31 @@ +import { Metadata, Viewport } from "next"; + +import { PreloadResources } from "./layout.preload"; + +// styles +import "@/styles/command-pallette.css"; +import "@/styles/emoji.css"; +import "@/styles/react-day-picker.css"; + +export const metadata: Metadata = { + robots: { + index: false, + follow: false, + }, +}; + +export const viewport: Viewport = { + minimumScale: 1, + initialScale: 1, + width: "device-width", + viewportFit: "cover", +}; + +export default function AppLayout({ children }: { children: React.ReactNode }) { + return ( +
+ + {children} +
+ ); +} diff --git a/web/app/onboarding/layout.tsx b/web/app/(all)/onboarding/layout.tsx similarity index 100% rename from web/app/onboarding/layout.tsx rename to web/app/(all)/onboarding/layout.tsx diff --git a/web/app/onboarding/page.tsx b/web/app/(all)/onboarding/page.tsx similarity index 100% rename from web/app/onboarding/page.tsx rename to web/app/(all)/onboarding/page.tsx diff --git a/web/app/profile/activity/page.tsx b/web/app/(all)/profile/activity/page.tsx similarity index 100% rename from web/app/profile/activity/page.tsx rename to web/app/(all)/profile/activity/page.tsx diff --git a/web/app/profile/appearance/page.tsx b/web/app/(all)/profile/appearance/page.tsx similarity index 100% rename from web/app/profile/appearance/page.tsx rename to web/app/(all)/profile/appearance/page.tsx diff --git a/web/app/profile/layout.tsx b/web/app/(all)/profile/layout.tsx similarity index 100% rename from web/app/profile/layout.tsx rename to web/app/(all)/profile/layout.tsx diff --git a/web/app/profile/notifications/page.tsx b/web/app/(all)/profile/notifications/page.tsx similarity index 100% rename from web/app/profile/notifications/page.tsx rename to web/app/(all)/profile/notifications/page.tsx diff --git a/web/app/profile/page.tsx b/web/app/(all)/profile/page.tsx similarity index 100% rename from web/app/profile/page.tsx rename to web/app/(all)/profile/page.tsx diff --git a/web/app/profile/security/page.tsx b/web/app/(all)/profile/security/page.tsx similarity index 100% rename from web/app/profile/security/page.tsx rename to web/app/(all)/profile/security/page.tsx diff --git a/web/app/profile/sidebar.tsx b/web/app/(all)/profile/sidebar.tsx similarity index 100% rename from web/app/profile/sidebar.tsx rename to web/app/(all)/profile/sidebar.tsx diff --git a/web/app/sign-up/layout.tsx b/web/app/(all)/sign-up/layout.tsx similarity index 79% rename from web/app/sign-up/layout.tsx rename to web/app/(all)/sign-up/layout.tsx index f7f405c27..3ae097721 100644 --- a/web/app/sign-up/layout.tsx +++ b/web/app/(all)/sign-up/layout.tsx @@ -2,6 +2,10 @@ import { Metadata } from "next"; export const metadata: Metadata = { title: "Sign up - Plane", + robots: { + index: true, + follow: false, + } }; export default function SignUpLayout({ children }: { children: React.ReactNode }) { diff --git a/web/app/sign-up/page.tsx b/web/app/(all)/sign-up/page.tsx similarity index 100% rename from web/app/sign-up/page.tsx rename to web/app/(all)/sign-up/page.tsx diff --git a/web/app/workspace-invitations/layout.tsx b/web/app/(all)/workspace-invitations/layout.tsx similarity index 100% rename from web/app/workspace-invitations/layout.tsx rename to web/app/(all)/workspace-invitations/layout.tsx diff --git a/web/app/workspace-invitations/page.tsx b/web/app/(all)/workspace-invitations/page.tsx similarity index 100% rename from web/app/workspace-invitations/page.tsx rename to web/app/(all)/workspace-invitations/page.tsx diff --git a/web/app/(home)/layout.tsx b/web/app/(home)/layout.tsx new file mode 100644 index 000000000..56380fb66 --- /dev/null +++ b/web/app/(home)/layout.tsx @@ -0,0 +1,21 @@ +import { Metadata, Viewport } from "next"; + +export const metadata: Metadata = { + robots: { + index: true, + follow: false, + }, +}; + +export const viewport: Viewport = { + minimumScale: 1, + initialScale: 1, + width: "device-width", + viewportFit: "cover", +}; + +export default function HomeLayout({ children }: { children: React.ReactNode }) { + return ( +
{children}
+ ); +} diff --git a/web/app/page.tsx b/web/app/(home)/page.tsx similarity index 100% rename from web/app/page.tsx rename to web/app/(home)/page.tsx diff --git a/web/app/layout.tsx b/web/app/layout.tsx index 6024753df..a36c75c49 100644 --- a/web/app/layout.tsx +++ b/web/app/layout.tsx @@ -1,15 +1,14 @@ import { Metadata, Viewport } from "next"; import Script from "next/script"; + // styles import "@/styles/globals.css"; -import "@/styles/command-pallette.css"; -import "@/styles/emoji.css"; -import "@/styles/react-day-picker.css"; -// meta data info import { SITE_DESCRIPTION, SITE_NAME } from "@plane/constants"; + // helpers -import { API_BASE_URL, cn } from "@/helpers/common.helper"; +import { cn } from "@/helpers/common.helper"; + // local import { AppProvider } from "./provider"; @@ -60,17 +59,6 @@ export default function RootLayout({ children }: { children: React.ReactNode }) - {/* preloading */} - - - - -
diff --git a/web/app/not-found.tsx b/web/app/not-found.tsx index ecc01b500..1f1ec0e2c 100644 --- a/web/app/not-found.tsx +++ b/web/app/not-found.tsx @@ -11,6 +11,10 @@ import Image404 from "@/public/404.svg"; export const metadata: Metadata = { title: "404 - Page Not Found", + robots: { + index: false, + follow: false, + }, }; const PageNotFound = () => ( From 5c9bdb1cea351e92ad0e1c98bbc3da2cf1ff4ee5 Mon Sep 17 00:00:00 2001 From: JayashTripathy <76092296+JayashTripathy@users.noreply.github.com> Date: Fri, 23 May 2025 16:13:09 +0530 Subject: [PATCH 082/201] [WEB-4133] fix: analytics release bugs (#7086) * fix: header text of insight table search * fix: made the active project list scrollable * chore: added xAxis label to table header * chore: removed the intake issues * fix: made the headerText necessary --------- Co-authored-by: NarayanBavisetti Co-authored-by: sriram veeraghanta --- apiserver/plane/app/views/analytic/advance.py | 40 +++++++++---------- .../analytics-v2/insight-table/root.tsx | 5 ++- .../analytics-v2/overview/active-projects.tsx | 2 +- .../work-items/priority-chart.tsx | 24 +++++------ .../work-items/workitems-insight-table.tsx | 1 + 5 files changed, 36 insertions(+), 36 deletions(-) diff --git a/apiserver/plane/app/views/analytic/advance.py b/apiserver/plane/app/views/analytic/advance.py index a79ab98c5..8a2aea90b 100644 --- a/apiserver/plane/app/views/analytic/advance.py +++ b/apiserver/plane/app/views/analytic/advance.py @@ -16,6 +16,9 @@ from plane.db.models import ( IssueView, ProjectPage, Workspace, + CycleIssue, + ModuleIssue, + ProjectMember, ) from plane.utils.build_chart import build_analytics_chart from plane.utils.date_utils import ( @@ -69,32 +72,27 @@ class AdvanceAnalyticsEndpoint(AdvanceAnalyticsBaseView): } def get_overview_data(self) -> Dict[str, Dict[str, int]]: + members_query = WorkspaceMember.objects.filter( + workspace__slug=self._workspace_slug, is_active=True + ) + + if self.request.GET.get("project_ids", None): + project_ids = self.request.GET.get("project_ids", None) + project_ids = [str(project_id) for project_id in project_ids.split(",")] + members_query = ProjectMember.objects.filter( + project_id__in=project_ids, is_active=True + ) + return { - "total_users": self.get_filtered_counts( - WorkspaceMember.objects.filter( - workspace__slug=self._workspace_slug, is_active=True - ) - ), + "total_users": self.get_filtered_counts(members_query), "total_admins": self.get_filtered_counts( - WorkspaceMember.objects.filter( - workspace__slug=self._workspace_slug, - role=ROLE.ADMIN.value, - is_active=True, - ) + members_query.filter(role=ROLE.ADMIN.value) ), "total_members": self.get_filtered_counts( - WorkspaceMember.objects.filter( - workspace__slug=self._workspace_slug, - role=ROLE.MEMBER.value, - is_active=True, - ) + members_query.filter(role=ROLE.MEMBER.value) ), "total_guests": self.get_filtered_counts( - WorkspaceMember.objects.filter( - workspace__slug=self._workspace_slug, - role=ROLE.GUEST.value, - is_active=True, - ) + members_query.filter(role=ROLE.GUEST.value) ), "total_projects": self.get_filtered_counts( Project.objects.filter(**self.filters["project_filters"]) @@ -107,7 +105,7 @@ class AdvanceAnalyticsEndpoint(AdvanceAnalyticsBaseView): ), "total_intake": self.get_filtered_counts( Issue.objects.filter(**self.filters["base_filters"]).filter( - issue_intake__isnull=False + issue_intake__status__in=["-2", "0"] ) ), } diff --git a/web/core/components/analytics-v2/insight-table/root.tsx b/web/core/components/analytics-v2/insight-table/root.tsx index 8e6e8422e..1ee90c726 100644 --- a/web/core/components/analytics-v2/insight-table/root.tsx +++ b/web/core/components/analytics-v2/insight-table/root.tsx @@ -13,12 +13,13 @@ interface InsightTableProps> isLoading?: boolean; columns: ColumnDef[]; columnsLabels?: Record; + headerText: string; } export const InsightTable = >( props: InsightTableProps ): React.ReactElement => { - const { data, isLoading, columns, columnsLabels } = props; + const { data, isLoading, columns, columnsLabels, headerText } = props; const params = useParams(); const { t } = useTranslation(); const workspaceSlug = params.workspaceSlug.toString(); @@ -55,7 +56,7 @@ export const InsightTable = ) => (
@@ -80,33 +83,7 @@ export const SubIssuesListItemProperties: React.FC = observer((props) => disabled={!disabled} buttonVariant="border-without-text" buttonClassName="border" - /> -
- - - -
- - issue.project_id && - updateSubIssue( - workspaceSlug, - issue.project_id, - parentIssueId, - issueId, - { - start_date: val ? renderFormattedPayloadDate(val) : null, - }, - { ...issue } - ) - } - maxDate={maxDate} - placeholder={t("common.order_by.start_date")} - icon={} - buttonVariant={issue.start_date ? "border-with-text" : "border-without-text"} - optionsClassName="z-30" - disabled={!disabled} + showTooltip />
From 7cb5a9120a62471e7815e035516e56995610847a Mon Sep 17 00:00:00 2001 From: Vamsi Krishna <46787868+vamsikrishnamathala@users.noreply.github.com> Date: Mon, 26 May 2025 14:28:56 +0530 Subject: [PATCH 085/201] [WEB-4173]fix: fixed layout overflow issue #7119 --- web/app/(all)/layout.tsx | 4 ++-- web/app/(home)/layout.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/web/app/(all)/layout.tsx b/web/app/(all)/layout.tsx index 023852648..32589c4bf 100644 --- a/web/app/(all)/layout.tsx +++ b/web/app/(all)/layout.tsx @@ -23,9 +23,9 @@ export const viewport: Viewport = { export default function AppLayout({ children }: { children: React.ReactNode }) { return ( -
+ <> {children} -
+ ); } diff --git a/web/app/(home)/layout.tsx b/web/app/(home)/layout.tsx index 56380fb66..0ed40f86b 100644 --- a/web/app/(home)/layout.tsx +++ b/web/app/(home)/layout.tsx @@ -16,6 +16,6 @@ export const viewport: Viewport = { export default function HomeLayout({ children }: { children: React.ReactNode }) { return ( -
{children}
+ <>{children} ); } From 193ae9bfc842278acfba9fdccb8c7f62af5c96f5 Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Mon, 26 May 2025 14:58:26 +0530 Subject: [PATCH 086/201] fix: yarn lock file --- yarn.lock | 195 +++++++++--------------------------------------------- 1 file changed, 32 insertions(+), 163 deletions(-) diff --git a/yarn.lock b/yarn.lock index ef27eff52..22d8754dc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -257,7 +257,7 @@ "@babel/traverse" "^7.25.9" "@babel/types" "^7.25.9" -"@babel/helpers@^7.26.7": +"@babel/helpers@7.26.10", "@babel/helpers@^7.26.7": version "7.26.10" resolved "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.10.tgz#6baea3cd62ec2d0c1068778d63cb1314f6637384" integrity sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g== @@ -852,7 +852,7 @@ "@babel/plugin-transform-modules-commonjs" "^7.25.9" "@babel/plugin-transform-typescript" "^7.25.9" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.13", "@babel/runtime@^7.23.9", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": +"@babel/runtime@7.26.10", "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.13", "@babel/runtime@^7.23.9", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": version "7.26.10" resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz#a07b4d8fa27af131a633d7b3524db803eb4764c2" integrity sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw== @@ -1124,251 +1124,126 @@ resolved "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz#5e13fac887f08c44f76b0ccaf3370eb00fec9bb6" integrity sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg== -"@esbuild/aix-ppc64@0.24.2": - version "0.24.2" - resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz#38848d3e25afe842a7943643cbcd387cc6e13461" - integrity sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA== - "@esbuild/aix-ppc64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz#499600c5e1757a524990d5d92601f0ac3ce87f64" integrity sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ== -"@esbuild/android-arm64@0.24.2": - version "0.24.2" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz#f592957ae8b5643129fa889c79e69cd8669bb894" - integrity sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg== - "@esbuild/android-arm64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz#b9b8231561a1dfb94eb31f4ee056b92a985c324f" integrity sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g== -"@esbuild/android-arm@0.24.2": - version "0.24.2" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.24.2.tgz#72d8a2063aa630308af486a7e5cbcd1e134335b3" - integrity sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q== - "@esbuild/android-arm@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.0.tgz#ca6e7888942505f13e88ac9f5f7d2a72f9facd2b" integrity sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g== -"@esbuild/android-x64@0.24.2": - version "0.24.2" - resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.24.2.tgz#9a7713504d5f04792f33be9c197a882b2d88febb" - integrity sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw== - "@esbuild/android-x64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.0.tgz#e765ea753bac442dfc9cb53652ce8bd39d33e163" integrity sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg== -"@esbuild/darwin-arm64@0.24.2": - version "0.24.2" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz#02ae04ad8ebffd6e2ea096181b3366816b2b5936" - integrity sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA== - "@esbuild/darwin-arm64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz#fa394164b0d89d4fdc3a8a21989af70ef579fa2c" integrity sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw== -"@esbuild/darwin-x64@0.24.2": - version "0.24.2" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz#9ec312bc29c60e1b6cecadc82bd504d8adaa19e9" - integrity sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA== - "@esbuild/darwin-x64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz#91979d98d30ba6e7d69b22c617cc82bdad60e47a" integrity sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg== -"@esbuild/freebsd-arm64@0.24.2": - version "0.24.2" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz#5e82f44cb4906d6aebf24497d6a068cfc152fa00" - integrity sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg== - "@esbuild/freebsd-arm64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz#b97e97073310736b430a07b099d837084b85e9ce" integrity sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w== -"@esbuild/freebsd-x64@0.24.2": - version "0.24.2" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz#3fb1ce92f276168b75074b4e51aa0d8141ecce7f" - integrity sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q== - "@esbuild/freebsd-x64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz#f3b694d0da61d9910ec7deff794d444cfbf3b6e7" integrity sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A== -"@esbuild/linux-arm64@0.24.2": - version "0.24.2" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz#856b632d79eb80aec0864381efd29de8fd0b1f43" - integrity sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg== - "@esbuild/linux-arm64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz#f921f699f162f332036d5657cad9036f7a993f73" integrity sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg== -"@esbuild/linux-arm@0.24.2": - version "0.24.2" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz#c846b4694dc5a75d1444f52257ccc5659021b736" - integrity sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA== - "@esbuild/linux-arm@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz#cc49305b3c6da317c900688995a4050e6cc91ca3" integrity sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg== -"@esbuild/linux-ia32@0.24.2": - version "0.24.2" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz#f8a16615a78826ccbb6566fab9a9606cfd4a37d5" - integrity sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw== - "@esbuild/linux-ia32@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz#3e0736fcfab16cff042dec806247e2c76e109e19" integrity sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg== -"@esbuild/linux-loong64@0.24.2": - version "0.24.2" - resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz#1c451538c765bf14913512c76ed8a351e18b09fc" - integrity sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ== - "@esbuild/linux-loong64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz#ea2bf730883cddb9dfb85124232b5a875b8020c7" integrity sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw== -"@esbuild/linux-mips64el@0.24.2": - version "0.24.2" - resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz#0846edeefbc3d8d50645c51869cc64401d9239cb" - integrity sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw== - "@esbuild/linux-mips64el@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz#4cababb14eede09248980a2d2d8b966464294ff1" integrity sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ== -"@esbuild/linux-ppc64@0.24.2": - version "0.24.2" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz#8e3fc54505671d193337a36dfd4c1a23b8a41412" - integrity sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw== - "@esbuild/linux-ppc64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz#8860a4609914c065373a77242e985179658e1951" integrity sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw== -"@esbuild/linux-riscv64@0.24.2": - version "0.24.2" - resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz#6a1e92096d5e68f7bb10a0d64bb5b6d1daf9a694" - integrity sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q== - "@esbuild/linux-riscv64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz#baf26e20bb2d38cfb86ee282dff840c04f4ed987" integrity sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA== -"@esbuild/linux-s390x@0.24.2": - version "0.24.2" - resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz#ab18e56e66f7a3c49cb97d337cd0a6fea28a8577" - integrity sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw== - "@esbuild/linux-s390x@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz#8323afc0d6cb1b6dc6e9fd21efd9e1542c3640a4" integrity sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA== -"@esbuild/linux-x64@0.24.2": - version "0.24.2" - resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz#8140c9b40da634d380b0b29c837a0b4267aff38f" - integrity sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q== - "@esbuild/linux-x64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz#08fcf60cb400ed2382e9f8e0f5590bac8810469a" integrity sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw== -"@esbuild/netbsd-arm64@0.24.2": - version "0.24.2" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz#65f19161432bafb3981f5f20a7ff45abb2e708e6" - integrity sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw== - "@esbuild/netbsd-arm64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz#935c6c74e20f7224918fbe2e6c6fe865b6c6ea5b" integrity sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw== -"@esbuild/netbsd-x64@0.24.2": - version "0.24.2" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz#7a3a97d77abfd11765a72f1c6f9b18f5396bcc40" - integrity sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw== - "@esbuild/netbsd-x64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz#414677cef66d16c5a4d210751eb2881bb9c1b62b" integrity sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA== -"@esbuild/openbsd-arm64@0.24.2": - version "0.24.2" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz#58b00238dd8f123bfff68d3acc53a6ee369af89f" - integrity sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A== - "@esbuild/openbsd-arm64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz#8fd55a4d08d25cdc572844f13c88d678c84d13f7" integrity sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw== -"@esbuild/openbsd-x64@0.24.2": - version "0.24.2" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz#0ac843fda0feb85a93e288842936c21a00a8a205" - integrity sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA== - "@esbuild/openbsd-x64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz#0c48ddb1494bbc2d6bcbaa1429a7f465fa1dedde" integrity sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg== -"@esbuild/sunos-x64@0.24.2": - version "0.24.2" - resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz#8b7aa895e07828d36c422a4404cc2ecf27fb15c6" - integrity sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig== - "@esbuild/sunos-x64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz#86ff9075d77962b60dd26203d7352f92684c8c92" integrity sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg== -"@esbuild/win32-arm64@0.24.2": - version "0.24.2" - resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz#c023afb647cabf0c3ed13f0eddfc4f1d61c66a85" - integrity sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ== - "@esbuild/win32-arm64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz#849c62327c3229467f5b5cd681bf50588442e96c" integrity sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw== -"@esbuild/win32-ia32@0.24.2": - version "0.24.2" - resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz#96c356132d2dda990098c8b8b951209c3cd743c2" - integrity sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA== - "@esbuild/win32-ia32@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz#f62eb480cd7cca088cb65bb46a6db25b725dc079" integrity sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA== -"@esbuild/win32-x64@0.24.2": - version "0.24.2" - resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz#34aa0b52d0fbb1a654b596acfa595f0c7b77a77b" - integrity sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg== - "@esbuild/win32-x64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz#c8e119a30a7c8d60b9d2e22d2073722dde3b710b" @@ -6055,38 +5930,7 @@ esbuild-register@^3.5.0: dependencies: debug "^4.3.4" -"esbuild@^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0": - version "0.24.2" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.24.2.tgz#b5b55bee7de017bff5fb8a4e3e44f2ebe2c3567d" - integrity sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA== - optionalDependencies: - "@esbuild/aix-ppc64" "0.24.2" - "@esbuild/android-arm" "0.24.2" - "@esbuild/android-arm64" "0.24.2" - "@esbuild/android-x64" "0.24.2" - "@esbuild/darwin-arm64" "0.24.2" - "@esbuild/darwin-x64" "0.24.2" - "@esbuild/freebsd-arm64" "0.24.2" - "@esbuild/freebsd-x64" "0.24.2" - "@esbuild/linux-arm" "0.24.2" - "@esbuild/linux-arm64" "0.24.2" - "@esbuild/linux-ia32" "0.24.2" - "@esbuild/linux-loong64" "0.24.2" - "@esbuild/linux-mips64el" "0.24.2" - "@esbuild/linux-ppc64" "0.24.2" - "@esbuild/linux-riscv64" "0.24.2" - "@esbuild/linux-s390x" "0.24.2" - "@esbuild/linux-x64" "0.24.2" - "@esbuild/netbsd-arm64" "0.24.2" - "@esbuild/netbsd-x64" "0.24.2" - "@esbuild/openbsd-arm64" "0.24.2" - "@esbuild/openbsd-x64" "0.24.2" - "@esbuild/sunos-x64" "0.24.2" - "@esbuild/win32-arm64" "0.24.2" - "@esbuild/win32-ia32" "0.24.2" - "@esbuild/win32-x64" "0.24.2" - -esbuild@^0.25.0: +esbuild@0.25.0, "esbuild@^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0", esbuild@^0.25.0: version "0.25.0" resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz#0de1787a77206c5a79eeb634a623d39b5006ce92" integrity sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw== @@ -8557,7 +8401,7 @@ mz@^2.7.0: object-assign "^4.0.1" thenify-all "^1.0.0" -nanoid@^3.3.6, nanoid@^3.3.8: +nanoid@3.3.8, nanoid@^3.3.6, nanoid@^3.3.8: version "3.3.8" resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf" integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w== @@ -10717,7 +10561,16 @@ streamx@^2.15.0, streamx@^2.21.0: optionalDependencies: bare-events "^2.2.0" -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -10810,7 +10663,14 @@ string_decoder@^1.1.1, string_decoder@^1.3.0: dependencies: safe-buffer "~5.2.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -12028,7 +11888,16 @@ word-wrap@^1.2.5: resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version "7.0.0" + resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== From 0eafbb698a317e453422241bfc69a27788807479 Mon Sep 17 00:00:00 2001 From: JayashTripathy <76092296+JayashTripathy@users.noreply.github.com> Date: Mon, 26 May 2025 15:22:16 +0530 Subject: [PATCH 087/201] [WEB-3494] fix: size of created at value #7112 --- web/core/components/issues/peek-overview/properties.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/core/components/issues/peek-overview/properties.tsx b/web/core/components/issues/peek-overview/properties.tsx index 8fd7fd58f..93516297f 100644 --- a/web/core/components/issues/peek-overview/properties.tsx +++ b/web/core/components/issues/peek-overview/properties.tsx @@ -135,7 +135,7 @@ export const PeekOverviewProperties: FC = observer((pro showTooltip userIds={createdByDetails?.display_name.includes("-intake") ? null : createdByDetails?.id} /> - + {createdByDetails?.display_name.includes("-intake") ? "Plane" : createdByDetails?.display_name}
From 5a208cb1b9360d727951dbd0e99634fec9021069 Mon Sep 17 00:00:00 2001 From: JayashTripathy <76092296+JayashTripathy@users.noreply.github.com> Date: Mon, 26 May 2025 15:23:39 +0530 Subject: [PATCH 088/201] [WEB-2403] fix: alignment of project states in collapsed view #7114 --- web/core/components/issues/issue-layouts/kanban/default.tsx | 2 +- .../issues/issue-layouts/kanban/headers/group-by-card.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/web/core/components/issues/issue-layouts/kanban/default.tsx b/web/core/components/issues/issue-layouts/kanban/default.tsx index d13615404..32cddc5a2 100644 --- a/web/core/components/issues/issue-layouts/kanban/default.tsx +++ b/web/core/components/issues/issue-layouts/kanban/default.tsx @@ -162,7 +162,7 @@ export const KanBan: React.FC = observer((props) => { } `} > {sub_group_by === null && ( -
+
= observer((props) => { verticalAlignPosition ? `w-[44px] flex-col items-center` : `w-full flex-row items-center` }`} > -
+
{icon ? icon : }
From 4e485d6402d71ee263ddc507b7029d0b6b73e106 Mon Sep 17 00:00:00 2001 From: JayashTripathy <76092296+JayashTripathy@users.noreply.github.com> Date: Mon, 26 May 2025 15:24:13 +0530 Subject: [PATCH 089/201] [WEB-4160] fix: close the context menu after select #7113 --- web/core/components/workspace/sidebar/projects-list-item.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/web/core/components/workspace/sidebar/projects-list-item.tsx b/web/core/components/workspace/sidebar/projects-list-item.tsx index 75b10aa4c..715d02cb1 100644 --- a/web/core/components/workspace/sidebar/projects-list-item.tsx +++ b/web/core/components/workspace/sidebar/projects-list-item.tsx @@ -311,6 +311,7 @@ export const SidebarProjectsListItem: React.FC = observer((props) => { customButtonClassName="grid place-items-center" placement="bottom-start" useCaptureForOutsideClick + closeOnSelect > {/* TODO: Removed is_favorite logic due to the optimization in projects API */} {/* {isAuthorized && ( From 78cc32765bfb9ef4b5d7a6901d26332e2cd072eb Mon Sep 17 00:00:00 2001 From: Dheeraj Kumar Ketireddy Date: Mon, 26 May 2025 15:26:26 +0530 Subject: [PATCH 090/201] [WEB-3707] pytest based test suite for apiserver (#7010) * pytest bases tests for apiserver * Trimmed spaces * Updated .gitignore for pytest local files --- .gitignore | 2 + apiserver/.coveragerc | 25 + apiserver/plane/authentication/urls.py | 30 +- apiserver/plane/tests/README.md | 143 ++++++ apiserver/plane/tests/TESTING_GUIDE.md | 151 ++++++ apiserver/plane/tests/__init__.py | 2 +- apiserver/plane/tests/api/base.py | 34 -- apiserver/plane/tests/api/test_asset.py | 1 - .../plane/tests/api/test_auth_extended.py | 1 - .../plane/tests/api/test_authentication.py | 183 ------- apiserver/plane/tests/api/test_cycle.py | 1 - apiserver/plane/tests/api/test_issue.py | 1 - apiserver/plane/tests/api/test_oauth.py | 1 - apiserver/plane/tests/api/test_people.py | 1 - apiserver/plane/tests/api/test_project.py | 1 - apiserver/plane/tests/api/test_shortcut.py | 1 - apiserver/plane/tests/api/test_state.py | 1 - apiserver/plane/tests/api/test_view.py | 1 - apiserver/plane/tests/api/test_workspace.py | 44 -- apiserver/plane/tests/conftest.py | 78 +++ apiserver/plane/tests/conftest_external.py | 117 +++++ .../plane/tests/{api => contract}/__init__.py | 0 .../plane/tests/contract/api/__init__.py | 0 .../plane/tests/contract/app/__init__.py | 1 + .../tests/contract/app/test_authentication.py | 459 ++++++++++++++++++ .../tests/contract/app/test_workspace_app.py | 79 +++ apiserver/plane/tests/factories.py | 82 ++++ apiserver/plane/tests/smoke/__init__.py | 0 .../plane/tests/smoke/test_auth_smoke.py | 100 ++++ apiserver/plane/tests/unit/__init__.py | 0 apiserver/plane/tests/unit/models/__init__.py | 0 .../tests/unit/models/test_workspace_model.py | 50 ++ .../plane/tests/unit/serializers/__init__.py | 0 .../tests/unit/serializers/test_workspace.py | 71 +++ apiserver/plane/tests/unit/utils/__init__.py | 0 apiserver/plane/tests/unit/utils/test_uuid.py | 49 ++ apiserver/pytest.ini | 17 + apiserver/requirements/test.txt | 14 +- apiserver/run_tests.py | 91 ++++ apiserver/run_tests.sh | 4 + 40 files changed, 1546 insertions(+), 290 deletions(-) create mode 100644 apiserver/.coveragerc create mode 100644 apiserver/plane/tests/README.md create mode 100644 apiserver/plane/tests/TESTING_GUIDE.md delete mode 100644 apiserver/plane/tests/api/base.py delete mode 100644 apiserver/plane/tests/api/test_asset.py delete mode 100644 apiserver/plane/tests/api/test_auth_extended.py delete mode 100644 apiserver/plane/tests/api/test_authentication.py delete mode 100644 apiserver/plane/tests/api/test_cycle.py delete mode 100644 apiserver/plane/tests/api/test_issue.py delete mode 100644 apiserver/plane/tests/api/test_oauth.py delete mode 100644 apiserver/plane/tests/api/test_people.py delete mode 100644 apiserver/plane/tests/api/test_project.py delete mode 100644 apiserver/plane/tests/api/test_shortcut.py delete mode 100644 apiserver/plane/tests/api/test_state.py delete mode 100644 apiserver/plane/tests/api/test_view.py delete mode 100644 apiserver/plane/tests/api/test_workspace.py create mode 100644 apiserver/plane/tests/conftest.py create mode 100644 apiserver/plane/tests/conftest_external.py rename apiserver/plane/tests/{api => contract}/__init__.py (100%) create mode 100644 apiserver/plane/tests/contract/api/__init__.py create mode 100644 apiserver/plane/tests/contract/app/__init__.py create mode 100644 apiserver/plane/tests/contract/app/test_authentication.py create mode 100644 apiserver/plane/tests/contract/app/test_workspace_app.py create mode 100644 apiserver/plane/tests/factories.py create mode 100644 apiserver/plane/tests/smoke/__init__.py create mode 100644 apiserver/plane/tests/smoke/test_auth_smoke.py create mode 100644 apiserver/plane/tests/unit/__init__.py create mode 100644 apiserver/plane/tests/unit/models/__init__.py create mode 100644 apiserver/plane/tests/unit/models/test_workspace_model.py create mode 100644 apiserver/plane/tests/unit/serializers/__init__.py create mode 100644 apiserver/plane/tests/unit/serializers/test_workspace.py create mode 100644 apiserver/plane/tests/unit/utils/__init__.py create mode 100644 apiserver/plane/tests/unit/utils/test_uuid.py create mode 100644 apiserver/pytest.ini create mode 100755 apiserver/run_tests.py create mode 100755 apiserver/run_tests.sh diff --git a/.gitignore b/.gitignore index 0c8956423..a6a407ba9 100644 --- a/.gitignore +++ b/.gitignore @@ -53,6 +53,8 @@ mediafiles .env .DS_Store logs/ +htmlcov/ +.coverage node_modules/ assets/dist/ diff --git a/apiserver/.coveragerc b/apiserver/.coveragerc new file mode 100644 index 000000000..bd829d141 --- /dev/null +++ b/apiserver/.coveragerc @@ -0,0 +1,25 @@ +[run] +source = plane +omit = + */tests/* + */migrations/* + */settings/* + */wsgi.py + */asgi.py + */urls.py + manage.py + */admin.py + */apps.py + +[report] +exclude_lines = + pragma: no cover + def __repr__ + if self.debug: + raise NotImplementedError + if __name__ == .__main__. + pass + raise ImportError + +[html] +directory = htmlcov \ No newline at end of file diff --git a/apiserver/plane/authentication/urls.py b/apiserver/plane/authentication/urls.py index d474fe4df..d8b5799de 100644 --- a/apiserver/plane/authentication/urls.py +++ b/apiserver/plane/authentication/urls.py @@ -42,11 +42,11 @@ urlpatterns = [ # credentials path("sign-in/", SignInAuthEndpoint.as_view(), name="sign-in"), path("sign-up/", SignUpAuthEndpoint.as_view(), name="sign-up"), - path("spaces/sign-in/", SignInAuthSpaceEndpoint.as_view(), name="sign-in"), - path("spaces/sign-up/", SignUpAuthSpaceEndpoint.as_view(), name="sign-in"), + path("spaces/sign-in/", SignInAuthSpaceEndpoint.as_view(), name="space-sign-in"), + path("spaces/sign-up/", SignUpAuthSpaceEndpoint.as_view(), name="space-sign-up"), # signout path("sign-out/", SignOutAuthEndpoint.as_view(), name="sign-out"), - path("spaces/sign-out/", SignOutAuthSpaceEndpoint.as_view(), name="sign-out"), + path("spaces/sign-out/", SignOutAuthSpaceEndpoint.as_view(), name="space-sign-out"), # csrf token path("get-csrf-token/", CSRFTokenEndpoint.as_view(), name="get_csrf_token"), # Magic sign in @@ -56,17 +56,17 @@ urlpatterns = [ path( "spaces/magic-generate/", MagicGenerateSpaceEndpoint.as_view(), - name="magic-generate", + name="space-magic-generate", ), path( "spaces/magic-sign-in/", MagicSignInSpaceEndpoint.as_view(), - name="magic-sign-in", + name="space-magic-sign-in", ), path( "spaces/magic-sign-up/", MagicSignUpSpaceEndpoint.as_view(), - name="magic-sign-up", + name="space-magic-sign-up", ), ## Google Oauth path("google/", GoogleOauthInitiateEndpoint.as_view(), name="google-initiate"), @@ -74,12 +74,12 @@ urlpatterns = [ path( "spaces/google/", GoogleOauthInitiateSpaceEndpoint.as_view(), - name="google-initiate", + name="space-google-initiate", ), path( - "google/callback/", + "spaces/google/callback/", GoogleCallbackSpaceEndpoint.as_view(), - name="google-callback", + name="space-google-callback", ), ## Github Oauth path("github/", GitHubOauthInitiateEndpoint.as_view(), name="github-initiate"), @@ -87,12 +87,12 @@ urlpatterns = [ path( "spaces/github/", GitHubOauthInitiateSpaceEndpoint.as_view(), - name="github-initiate", + name="space-github-initiate", ), path( "spaces/github/callback/", GitHubCallbackSpaceEndpoint.as_view(), - name="github-callback", + name="space-github-callback", ), ## Gitlab Oauth path("gitlab/", GitLabOauthInitiateEndpoint.as_view(), name="gitlab-initiate"), @@ -100,12 +100,12 @@ urlpatterns = [ path( "spaces/gitlab/", GitLabOauthInitiateSpaceEndpoint.as_view(), - name="gitlab-initiate", + name="space-gitlab-initiate", ), path( "spaces/gitlab/callback/", GitLabCallbackSpaceEndpoint.as_view(), - name="gitlab-callback", + name="space-gitlab-callback", ), # Email Check path("email-check/", EmailCheckEndpoint.as_view(), name="email-check"), @@ -120,12 +120,12 @@ urlpatterns = [ path( "spaces/forgot-password/", ForgotPasswordSpaceEndpoint.as_view(), - name="forgot-password", + name="space-forgot-password", ), path( "spaces/reset-password///", ResetPasswordSpaceEndpoint.as_view(), - name="forgot-password", + name="space-forgot-password", ), path("change-password/", ChangePasswordEndpoint.as_view(), name="forgot-password"), path("set-password/", SetUserPasswordEndpoint.as_view(), name="set-password"), diff --git a/apiserver/plane/tests/README.md b/apiserver/plane/tests/README.md new file mode 100644 index 000000000..df9aba6da --- /dev/null +++ b/apiserver/plane/tests/README.md @@ -0,0 +1,143 @@ +# Plane Tests + +This directory contains tests for the Plane application. The tests are organized using pytest. + +## Test Structure + +Tests are organized into the following categories: + +- **Unit tests**: Test individual functions or classes in isolation. +- **Contract tests**: Test interactions between components and verify API contracts are fulfilled. + - **API tests**: Test the external API endpoints (under `/api/v1/`). + - **App tests**: Test the web application API endpoints (under `/api/`). +- **Smoke tests**: Basic tests to verify that the application runs correctly. + +## API vs App Endpoints + +Plane has two types of API endpoints: + +1. **External API** (`plane.api`): + - Available at `/api/v1/` endpoint + - Uses API key authentication (X-Api-Key header) + - Designed for external API contracts and third-party access + - Tests use the `api_key_client` fixture for authentication + - Test files are in `contract/api/` + +2. **Web App API** (`plane.app`): + - Available at `/api/` endpoint + - Uses session-based authentication (CSRF disabled) + - Designed for the web application frontend + - Tests use the `session_client` fixture for authentication + - Test files are in `contract/app/` + +## Running Tests + +To run all tests: + +```bash +python -m pytest +``` + +To run specific test categories: + +```bash +# Run unit tests +python -m pytest plane/tests/unit/ + +# Run API contract tests +python -m pytest plane/tests/contract/api/ + +# Run App contract tests +python -m pytest plane/tests/contract/app/ + +# Run smoke tests +python -m pytest plane/tests/smoke/ +``` + +For convenience, we also provide a helper script: + +```bash +# Run all tests +./run_tests.py + +# Run only unit tests +./run_tests.py -u + +# Run contract tests with coverage report +./run_tests.py -c -o + +# Run tests in parallel +./run_tests.py -p +``` + +## Fixtures + +The following fixtures are available for testing: + +- `api_client`: Unauthenticated API client +- `create_user`: Creates a test user +- `api_token`: API token for the test user +- `api_key_client`: API client with API key authentication (for external API tests) +- `session_client`: API client with session authentication (for app API tests) +- `plane_server`: Live Django test server for HTTP-based smoke tests + +## Writing Tests + +When writing tests, follow these guidelines: + +1. Place tests in the appropriate directory based on their type. +2. Use the correct client fixture based on the API being tested: + - For external API (`/api/v1/`), use `api_key_client` + - For web app API (`/api/`), use `session_client` + - For smoke tests with real HTTP, use `plane_server` +3. Use the correct URL namespace when reverse-resolving URLs: + - For external API, use `reverse("api:endpoint_name")` + - For web app API, use `reverse("endpoint_name")` +4. Add the `@pytest.mark.django_db` decorator to tests that interact with the database. +5. Add the appropriate markers (`@pytest.mark.contract`, etc.) to categorize tests. + +## Test Fixtures + +Common fixtures are defined in: + +- `conftest.py`: General fixtures for authentication, database access, etc. +- `conftest_external.py`: Fixtures for external services (Redis, Elasticsearch, Celery, MongoDB) +- `factories.py`: Test factories for easy model instance creation + +## Best Practices + +When writing tests, follow these guidelines: + +1. **Use pytest's assert syntax** instead of Django's `self.assert*` methods. +2. **Add markers to categorize tests**: + ```python + @pytest.mark.unit + @pytest.mark.contract + @pytest.mark.smoke + ``` +3. **Use fixtures instead of setUp/tearDown methods** for cleaner, more reusable test code. +4. **Mock external dependencies** with the provided fixtures to avoid external service dependencies. +5. **Write focused tests** that verify one specific behavior or edge case. +6. **Keep test files small and organized** by logical components or endpoints. +7. **Target 90% code coverage** for models, serializers, and business logic. + +## External Dependencies + +Tests for components that interact with external services should: + +1. Use the `mock_redis`, `mock_elasticsearch`, `mock_mongodb`, and `mock_celery` fixtures for unit and most contract tests. +2. For more comprehensive contract tests, use Docker-based test containers (optional). + +## Coverage Reports + +Generate a coverage report with: + +```bash +python -m pytest --cov=plane --cov-report=term --cov-report=html +``` + +This creates an HTML report in the `htmlcov/` directory. + +## Migration from Old Tests + +Some tests are still in the old format in the `api/` directory. These need to be migrated to the new contract test structure in the appropriate directories. \ No newline at end of file diff --git a/apiserver/plane/tests/TESTING_GUIDE.md b/apiserver/plane/tests/TESTING_GUIDE.md new file mode 100644 index 000000000..98f4a1dba --- /dev/null +++ b/apiserver/plane/tests/TESTING_GUIDE.md @@ -0,0 +1,151 @@ +# Testing Guide for Plane + +This guide explains how to write tests for Plane using our pytest-based testing strategy. + +## Test Categories + +We divide tests into three categories: + +1. **Unit Tests**: Testing individual components in isolation. +2. **Contract Tests**: Testing API endpoints and verifying contracts between components. +3. **Smoke Tests**: Basic end-to-end tests for critical flows. + +## Writing Unit Tests + +Unit tests should be placed in the appropriate directory under `tests/unit/` depending on what you're testing: + +- `tests/unit/models/` - For model tests +- `tests/unit/serializers/` - For serializer tests +- `tests/unit/utils/` - For utility function tests + +### Example Unit Test: + +```python +import pytest +from plane.api.serializers import MySerializer + +@pytest.mark.unit +class TestMySerializer: + def test_serializer_valid_data(self): + # Create input data + data = {"field1": "value1", "field2": 42} + + # Initialize the serializer + serializer = MySerializer(data=data) + + # Validate + assert serializer.is_valid() + + # Check validated data + assert serializer.validated_data["field1"] == "value1" + assert serializer.validated_data["field2"] == 42 +``` + +## Writing Contract Tests + +Contract tests should be placed in `tests/contract/api/` or `tests/contract/app/` directories and should test the API endpoints. + +### Example Contract Test: + +```python +import pytest +from django.urls import reverse +from rest_framework import status + +@pytest.mark.contract +class TestMyEndpoint: + @pytest.mark.django_db + def test_my_endpoint_get(self, auth_client): + # Get the URL + url = reverse("my-endpoint") + + # Make request + response = auth_client.get(url) + + # Check response + assert response.status_code == status.HTTP_200_OK + assert "data" in response.data +``` + +## Writing Smoke Tests + +Smoke tests should be placed in `tests/smoke/` directory and use the `plane_server` fixture to test against a real HTTP server. + +### Example Smoke Test: + +```python +import pytest +import requests + +@pytest.mark.smoke +class TestCriticalFlow: + @pytest.mark.django_db + def test_login_flow(self, plane_server, create_user, user_data): + # Get login URL + url = f"{plane_server.url}/api/auth/signin/" + + # Test login + response = requests.post( + url, + json={ + "email": user_data["email"], + "password": user_data["password"] + } + ) + + # Verify + assert response.status_code == 200 + data = response.json() + assert "access_token" in data +``` + +## Useful Fixtures + +Our test setup provides several useful fixtures: + +1. `api_client`: An unauthenticated DRF APIClient +2. `api_key_client`: API client with API key authentication (for external API tests) +3. `session_client`: API client with session authentication (for web app API tests) +4. `create_user`: Creates and returns a test user +5. `mock_redis`: Mocks Redis interactions +6. `mock_elasticsearch`: Mocks Elasticsearch interactions +7. `mock_celery`: Mocks Celery task execution + +## Using Factory Boy + +For more complex test data setup, use the provided factories: + +```python +from plane.tests.factories import UserFactory, WorkspaceFactory + +# Create a user +user = UserFactory() + +# Create a workspace with a specific owner +workspace = WorkspaceFactory(owner=user) + +# Create multiple objects +users = UserFactory.create_batch(5) +``` + +## Running Tests + +Use pytest to run tests: + +```bash +# Run all tests +python -m pytest + +# Run only unit tests with coverage +python -m pytest -m unit --cov=plane +``` + +## Best Practices + +1. **Keep tests small and focused** - Each test should verify one specific behavior. +2. **Use markers** - Always add appropriate markers (`@pytest.mark.unit`, etc.). +3. **Mock external dependencies** - Use the provided mock fixtures. +4. **Use factories** - For complex data setup, use factories. +5. **Don't test the framework** - Focus on testing your business logic, not Django/DRF itself. +6. **Write readable assertions** - Use plain `assert` statements with clear messaging. +7. **Focus on coverage** - Aim for ≥90% code coverage for critical components. \ No newline at end of file diff --git a/apiserver/plane/tests/__init__.py b/apiserver/plane/tests/__init__.py index 0a0e47b0b..73d90cd21 100644 --- a/apiserver/plane/tests/__init__.py +++ b/apiserver/plane/tests/__init__.py @@ -1 +1 @@ -from .api import * +# Test package initialization diff --git a/apiserver/plane/tests/api/base.py b/apiserver/plane/tests/api/base.py deleted file mode 100644 index e3209a281..000000000 --- a/apiserver/plane/tests/api/base.py +++ /dev/null @@ -1,34 +0,0 @@ -# Third party imports -from rest_framework.test import APITestCase, APIClient - -# Module imports -from plane.db.models import User -from plane.app.views.authentication import get_tokens_for_user - - -class BaseAPITest(APITestCase): - def setUp(self): - self.client = APIClient(HTTP_USER_AGENT="plane/test", REMOTE_ADDR="10.10.10.10") - - -class AuthenticatedAPITest(BaseAPITest): - def setUp(self): - super().setUp() - - ## Create Dummy User - self.email = "user@plane.so" - user = User.objects.create(email=self.email) - user.set_password("user@123") - user.save() - - # Set user - self.user = user - - # Set Up User ID - self.user_id = user.id - - access_token, _ = get_tokens_for_user(user) - self.access_token = access_token - - # Set Up Authentication Token - self.client.credentials(HTTP_AUTHORIZATION="Bearer " + access_token) diff --git a/apiserver/plane/tests/api/test_asset.py b/apiserver/plane/tests/api/test_asset.py deleted file mode 100644 index b15d32e40..000000000 --- a/apiserver/plane/tests/api/test_asset.py +++ /dev/null @@ -1 +0,0 @@ -# TODO: Tests for File Asset Uploads diff --git a/apiserver/plane/tests/api/test_auth_extended.py b/apiserver/plane/tests/api/test_auth_extended.py deleted file mode 100644 index af6450ef4..000000000 --- a/apiserver/plane/tests/api/test_auth_extended.py +++ /dev/null @@ -1 +0,0 @@ -# TODO: Tests for ChangePassword and other Endpoints diff --git a/apiserver/plane/tests/api/test_authentication.py b/apiserver/plane/tests/api/test_authentication.py deleted file mode 100644 index 5d7beabdf..000000000 --- a/apiserver/plane/tests/api/test_authentication.py +++ /dev/null @@ -1,183 +0,0 @@ -# Python import -import json - -# Django imports -from django.urls import reverse - -# Third Party imports -from rest_framework import status -from .base import BaseAPITest - -# Module imports -from plane.db.models import User -from plane.settings.redis import redis_instance - - -class SignInEndpointTests(BaseAPITest): - def setUp(self): - super().setUp() - user = User.objects.create(email="user@plane.so") - user.set_password("user@123") - user.save() - - def test_without_data(self): - url = reverse("sign-in") - response = self.client.post(url, {}, format="json") - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - - def test_email_validity(self): - url = reverse("sign-in") - response = self.client.post( - url, {"email": "useremail.com", "password": "user@123"}, format="json" - ) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual( - response.data, {"error": "Please provide a valid email address."} - ) - - def test_password_validity(self): - url = reverse("sign-in") - response = self.client.post( - url, {"email": "user@plane.so", "password": "user123"}, format="json" - ) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - self.assertEqual( - response.data, - { - "error": "Sorry, we could not find a user with the provided credentials. Please try again." - }, - ) - - def test_user_exists(self): - url = reverse("sign-in") - response = self.client.post( - url, {"email": "user@email.so", "password": "user123"}, format="json" - ) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - self.assertEqual( - response.data, - { - "error": "Sorry, we could not find a user with the provided credentials. Please try again." - }, - ) - - def test_user_login(self): - url = reverse("sign-in") - - response = self.client.post( - url, {"email": "user@plane.so", "password": "user@123"}, format="json" - ) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data.get("user").get("email"), "user@plane.so") - - -class MagicLinkGenerateEndpointTests(BaseAPITest): - def setUp(self): - super().setUp() - user = User.objects.create(email="user@plane.so") - user.set_password("user@123") - user.save() - - def test_without_data(self): - url = reverse("magic-generate") - response = self.client.post(url, {}, format="json") - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - - def test_email_validity(self): - url = reverse("magic-generate") - response = self.client.post(url, {"email": "useremail.com"}, format="json") - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual( - response.data, {"error": "Please provide a valid email address."} - ) - - def test_magic_generate(self): - url = reverse("magic-generate") - - ri = redis_instance() - ri.delete("magic_user@plane.so") - - response = self.client.post(url, {"email": "user@plane.so"}, format="json") - self.assertEqual(response.status_code, status.HTTP_200_OK) - - def test_max_generate_attempt(self): - url = reverse("magic-generate") - - ri = redis_instance() - ri.delete("magic_user@plane.so") - - for _ in range(4): - response = self.client.post(url, {"email": "user@plane.so"}, format="json") - - response = self.client.post(url, {"email": "user@plane.so"}, format="json") - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual( - response.data, {"error": "Max attempts exhausted. Please try again later."} - ) - - -class MagicSignInEndpointTests(BaseAPITest): - def setUp(self): - super().setUp() - user = User.objects.create(email="user@plane.so") - user.set_password("user@123") - user.save() - - def test_without_data(self): - url = reverse("magic-sign-in") - response = self.client.post(url, {}, format="json") - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(response.data, {"error": "User token and key are required"}) - - def test_expired_invalid_magic_link(self): - ri = redis_instance() - ri.delete("magic_user@plane.so") - - url = reverse("magic-sign-in") - response = self.client.post( - url, - {"key": "magic_user@plane.so", "token": "xxxx-xxxxx-xxxx"}, - format="json", - ) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual( - response.data, {"error": "The magic code/link has expired please try again"} - ) - - def test_invalid_magic_code(self): - ri = redis_instance() - ri.delete("magic_user@plane.so") - ## Create Token - url = reverse("magic-generate") - self.client.post(url, {"email": "user@plane.so"}, format="json") - - url = reverse("magic-sign-in") - response = self.client.post( - url, - {"key": "magic_user@plane.so", "token": "xxxx-xxxxx-xxxx"}, - format="json", - ) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual( - response.data, {"error": "Your login code was incorrect. Please try again."} - ) - - def test_magic_code_sign_in(self): - ri = redis_instance() - ri.delete("magic_user@plane.so") - ## Create Token - url = reverse("magic-generate") - self.client.post(url, {"email": "user@plane.so"}, format="json") - - # Get the token - user_data = json.loads(ri.get("magic_user@plane.so")) - token = user_data["token"] - - url = reverse("magic-sign-in") - response = self.client.post( - url, {"key": "magic_user@plane.so", "token": token}, format="json" - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data.get("user").get("email"), "user@plane.so") diff --git a/apiserver/plane/tests/api/test_cycle.py b/apiserver/plane/tests/api/test_cycle.py deleted file mode 100644 index 72b580c99..000000000 --- a/apiserver/plane/tests/api/test_cycle.py +++ /dev/null @@ -1 +0,0 @@ -# TODO: Write Test for Cycle Endpoints diff --git a/apiserver/plane/tests/api/test_issue.py b/apiserver/plane/tests/api/test_issue.py deleted file mode 100644 index a45ff36b1..000000000 --- a/apiserver/plane/tests/api/test_issue.py +++ /dev/null @@ -1 +0,0 @@ -# TODO: Write Test for Issue Endpoints diff --git a/apiserver/plane/tests/api/test_oauth.py b/apiserver/plane/tests/api/test_oauth.py deleted file mode 100644 index 1e7dac0ef..000000000 --- a/apiserver/plane/tests/api/test_oauth.py +++ /dev/null @@ -1 +0,0 @@ -# TODO: Tests for OAuth Authentication Endpoint diff --git a/apiserver/plane/tests/api/test_people.py b/apiserver/plane/tests/api/test_people.py deleted file mode 100644 index 624281a2f..000000000 --- a/apiserver/plane/tests/api/test_people.py +++ /dev/null @@ -1 +0,0 @@ -# TODO: Write Test for people Endpoint diff --git a/apiserver/plane/tests/api/test_project.py b/apiserver/plane/tests/api/test_project.py deleted file mode 100644 index 9a7c50f19..000000000 --- a/apiserver/plane/tests/api/test_project.py +++ /dev/null @@ -1 +0,0 @@ -# TODO: Write Tests for project endpoints diff --git a/apiserver/plane/tests/api/test_shortcut.py b/apiserver/plane/tests/api/test_shortcut.py deleted file mode 100644 index 5103b5059..000000000 --- a/apiserver/plane/tests/api/test_shortcut.py +++ /dev/null @@ -1 +0,0 @@ -# TODO: Write Test for shortcuts diff --git a/apiserver/plane/tests/api/test_state.py b/apiserver/plane/tests/api/test_state.py deleted file mode 100644 index a336d955a..000000000 --- a/apiserver/plane/tests/api/test_state.py +++ /dev/null @@ -1 +0,0 @@ -# TODO: Wrote test for state endpoints diff --git a/apiserver/plane/tests/api/test_view.py b/apiserver/plane/tests/api/test_view.py deleted file mode 100644 index c8864f28a..000000000 --- a/apiserver/plane/tests/api/test_view.py +++ /dev/null @@ -1 +0,0 @@ -# TODO: Write test for view endpoints diff --git a/apiserver/plane/tests/api/test_workspace.py b/apiserver/plane/tests/api/test_workspace.py deleted file mode 100644 index d63eab2e0..000000000 --- a/apiserver/plane/tests/api/test_workspace.py +++ /dev/null @@ -1,44 +0,0 @@ -# Django imports -from django.urls import reverse - -# Third party import -from rest_framework import status - -# Module imports -from .base import AuthenticatedAPITest -from plane.db.models import Workspace, WorkspaceMember - - -class WorkSpaceCreateReadUpdateDelete(AuthenticatedAPITest): - def setUp(self): - super().setUp() - - def test_create_workspace(self): - url = reverse("workspace") - - # Test with empty data - response = self.client.post(url, {}, format="json") - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - - # Test with valid data - response = self.client.post( - url, {"name": "Plane", "slug": "pla-ne"}, format="json" - ) - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertEqual(Workspace.objects.count(), 1) - # Check if the member is created - self.assertEqual(WorkspaceMember.objects.count(), 1) - - # Check other values - workspace = Workspace.objects.get(pk=response.data["id"]) - workspace_member = WorkspaceMember.objects.get( - workspace=workspace, member_id=self.user_id - ) - self.assertEqual(workspace.owner_id, self.user_id) - self.assertEqual(workspace_member.role, 20) - - # Create a already existing workspace - response = self.client.post( - url, {"name": "Plane", "slug": "pla-ne"}, format="json" - ) - self.assertEqual(response.status_code, status.HTTP_409_CONFLICT) diff --git a/apiserver/plane/tests/conftest.py b/apiserver/plane/tests/conftest.py new file mode 100644 index 000000000..ce0d3be2b --- /dev/null +++ b/apiserver/plane/tests/conftest.py @@ -0,0 +1,78 @@ +import pytest +from django.conf import settings +from rest_framework.test import APIClient +from pytest_django.fixtures import django_db_setup +from unittest.mock import patch, MagicMock + +from plane.db.models import User +from plane.db.models.api import APIToken + + +@pytest.fixture(scope="session") +def django_db_setup(django_db_setup): + """Set up the Django database for the test session""" + pass + + +@pytest.fixture +def api_client(): + """Return an unauthenticated API client""" + return APIClient() + + +@pytest.fixture +def user_data(): + """Return standard user data for tests""" + return { + "email": "test@plane.so", + "password": "test-password", + "first_name": "Test", + "last_name": "User" + } + + +@pytest.fixture +def create_user(db, user_data): + """Create and return a user instance""" + user = User.objects.create( + email=user_data["email"], + first_name=user_data["first_name"], + last_name=user_data["last_name"] + ) + user.set_password(user_data["password"]) + user.save() + return user + + +@pytest.fixture +def api_token(db, create_user): + """Create and return an API token for testing the external API""" + token = APIToken.objects.create( + user=create_user, + label="Test API Token", + token="test-api-token-12345", + ) + return token + + +@pytest.fixture +def api_key_client(api_client, api_token): + """Return an API key authenticated client for external API testing""" + api_client.credentials(HTTP_X_API_KEY=api_token.token) + return api_client + + +@pytest.fixture +def session_client(api_client, create_user): + """Return a session authenticated API client for app API testing, which is what plane.app uses""" + api_client.force_authenticate(user=create_user) + return api_client + + +@pytest.fixture +def plane_server(live_server): + """ + Renamed version of live_server fixture to avoid name clashes. + Returns a live Django server for testing HTTP requests. + """ + return live_server \ No newline at end of file diff --git a/apiserver/plane/tests/conftest_external.py b/apiserver/plane/tests/conftest_external.py new file mode 100644 index 000000000..d2d6a2df5 --- /dev/null +++ b/apiserver/plane/tests/conftest_external.py @@ -0,0 +1,117 @@ +import pytest +from unittest.mock import MagicMock, patch +from django.conf import settings + + +@pytest.fixture +def mock_redis(): + """ + Mock Redis for testing without actual Redis connection. + + This fixture patches the redis_instance function to return a MagicMock + that behaves like a Redis client. + """ + mock_redis_client = MagicMock() + + # Configure the mock to handle common Redis operations + mock_redis_client.get.return_value = None + mock_redis_client.set.return_value = True + mock_redis_client.delete.return_value = True + mock_redis_client.exists.return_value = 0 + mock_redis_client.ttl.return_value = -1 + + # Start the patch + with patch('plane.settings.redis.redis_instance', return_value=mock_redis_client): + yield mock_redis_client + + +@pytest.fixture +def mock_elasticsearch(): + """ + Mock Elasticsearch for testing without actual ES connection. + + This fixture patches Elasticsearch to return a MagicMock + that behaves like an Elasticsearch client. + """ + mock_es_client = MagicMock() + + # Configure the mock to handle common ES operations + mock_es_client.indices.exists.return_value = True + mock_es_client.indices.create.return_value = {"acknowledged": True} + mock_es_client.search.return_value = {"hits": {"total": {"value": 0}, "hits": []}} + mock_es_client.index.return_value = {"_id": "test_id", "result": "created"} + mock_es_client.update.return_value = {"_id": "test_id", "result": "updated"} + mock_es_client.delete.return_value = {"_id": "test_id", "result": "deleted"} + + # Start the patch + with patch('elasticsearch.Elasticsearch', return_value=mock_es_client): + yield mock_es_client + + +@pytest.fixture +def mock_mongodb(): + """ + Mock MongoDB for testing without actual MongoDB connection. + + This fixture patches PyMongo to return a MagicMock that behaves like a MongoDB client. + """ + # Create mock MongoDB clients and collections + mock_mongo_client = MagicMock() + mock_mongo_db = MagicMock() + mock_mongo_collection = MagicMock() + + # Set up the chain: client -> database -> collection + mock_mongo_client.__getitem__.return_value = mock_mongo_db + mock_mongo_client.get_database.return_value = mock_mongo_db + mock_mongo_db.__getitem__.return_value = mock_mongo_collection + + # Configure common MongoDB collection operations + mock_mongo_collection.find_one.return_value = None + mock_mongo_collection.find.return_value = MagicMock( + __iter__=lambda x: iter([]), + count=lambda: 0 + ) + mock_mongo_collection.insert_one.return_value = MagicMock( + inserted_id="mock_id_123", + acknowledged=True + ) + mock_mongo_collection.insert_many.return_value = MagicMock( + inserted_ids=["mock_id_123", "mock_id_456"], + acknowledged=True + ) + mock_mongo_collection.update_one.return_value = MagicMock( + modified_count=1, + matched_count=1, + acknowledged=True + ) + mock_mongo_collection.update_many.return_value = MagicMock( + modified_count=2, + matched_count=2, + acknowledged=True + ) + mock_mongo_collection.delete_one.return_value = MagicMock( + deleted_count=1, + acknowledged=True + ) + mock_mongo_collection.delete_many.return_value = MagicMock( + deleted_count=2, + acknowledged=True + ) + mock_mongo_collection.count_documents.return_value = 0 + + # Start the patch + with patch('pymongo.MongoClient', return_value=mock_mongo_client): + yield mock_mongo_client + + +@pytest.fixture +def mock_celery(): + """ + Mock Celery for testing without actual task execution. + + This fixture patches Celery's task.delay() to prevent actual task execution. + """ + # Start the patch + with patch('celery.app.task.Task.delay') as mock_delay: + mock_delay.return_value = MagicMock(id="mock-task-id") + yield mock_delay \ No newline at end of file diff --git a/apiserver/plane/tests/api/__init__.py b/apiserver/plane/tests/contract/__init__.py similarity index 100% rename from apiserver/plane/tests/api/__init__.py rename to apiserver/plane/tests/contract/__init__.py diff --git a/apiserver/plane/tests/contract/api/__init__.py b/apiserver/plane/tests/contract/api/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apiserver/plane/tests/contract/app/__init__.py b/apiserver/plane/tests/contract/app/__init__.py new file mode 100644 index 000000000..0519ecba6 --- /dev/null +++ b/apiserver/plane/tests/contract/app/__init__.py @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apiserver/plane/tests/contract/app/test_authentication.py b/apiserver/plane/tests/contract/app/test_authentication.py new file mode 100644 index 000000000..0dc548710 --- /dev/null +++ b/apiserver/plane/tests/contract/app/test_authentication.py @@ -0,0 +1,459 @@ +import json +import uuid +import pytest +from django.urls import reverse +from django.utils import timezone +from rest_framework import status +from django.test import Client +from django.core.exceptions import ValidationError +from unittest.mock import patch, MagicMock + +from plane.db.models import User +from plane.settings.redis import redis_instance +from plane.license.models import Instance + + +@pytest.fixture +def setup_instance(db): + """Create and configure an instance for authentication tests""" + instance_id = uuid.uuid4() if not Instance.objects.exists() else Instance.objects.first().id + + # Create or update instance with all required fields + instance, _ = Instance.objects.update_or_create( + id=instance_id, + defaults={ + "instance_name": "Test Instance", + "instance_id": str(uuid.uuid4()), + "current_version": "1.0.0", + "domain": "http://localhost:8000", + "last_checked_at": timezone.now(), + "is_setup_done": True, + } + ) + return instance + + +@pytest.fixture +def django_client(): + """Return a Django test client with User-Agent header for handling redirects""" + client = Client(HTTP_USER_AGENT="Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:15.0) Gecko/20100101 Firefox/15.0.1") + return client + + +@pytest.mark.contract +class TestMagicLinkGenerate: + """Test magic link generation functionality""" + + @pytest.fixture + def setup_user(self, db): + """Create a test user for magic link tests""" + user = User.objects.create(email="user@plane.so") + user.set_password("user@123") + user.save() + return user + + @pytest.mark.django_db + def test_without_data(self, api_client, setup_user, setup_instance): + """Test magic link generation with empty data""" + url = reverse("magic-generate") + try: + response = api_client.post(url, {}, format="json") + assert response.status_code == status.HTTP_400_BAD_REQUEST + except ValidationError: + # If a ValidationError is raised directly, that's also acceptable + # as it indicates the empty email was rejected + assert True + + @pytest.mark.django_db + def test_email_validity(self, api_client, setup_user, setup_instance): + """Test magic link generation with invalid email format""" + url = reverse("magic-generate") + try: + response = api_client.post(url, {"email": "useremail.com"}, format="json") + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert "error_code" in response.data # Check for error code in response + except ValidationError: + # If a ValidationError is raised directly, that's also acceptable + # as it indicates the invalid email was rejected + assert True + + @pytest.mark.django_db + @patch("plane.bgtasks.magic_link_code_task.magic_link.delay") + def test_magic_generate(self, mock_magic_link, api_client, setup_user, setup_instance): + """Test successful magic link generation""" + url = reverse("magic-generate") + + ri = redis_instance() + ri.delete("magic_user@plane.so") + + response = api_client.post(url, {"email": "user@plane.so"}, format="json") + assert response.status_code == status.HTTP_200_OK + assert "key" in response.data # Check for key in response + + # Verify the mock was called with the expected arguments + mock_magic_link.assert_called_once() + args = mock_magic_link.call_args[0] + assert args[0] == "user@plane.so" # First arg should be the email + + @pytest.mark.django_db + @patch("plane.bgtasks.magic_link_code_task.magic_link.delay") + def test_max_generate_attempt(self, mock_magic_link, api_client, setup_user, setup_instance): + """Test exceeding maximum magic link generation attempts""" + url = reverse("magic-generate") + + ri = redis_instance() + ri.delete("magic_user@plane.so") + + for _ in range(4): + api_client.post(url, {"email": "user@plane.so"}, format="json") + + response = api_client.post(url, {"email": "user@plane.so"}, format="json") + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert "error_code" in response.data # Check for error code in response + + +@pytest.mark.contract +class TestSignInEndpoint: + """Test sign-in functionality""" + + @pytest.fixture + def setup_user(self, db): + """Create a test user for authentication tests""" + user = User.objects.create(email="user@plane.so") + user.set_password("user@123") + user.save() + return user + + @pytest.mark.django_db + def test_without_data(self, django_client, setup_user, setup_instance): + """Test sign-in with empty data""" + url = reverse("sign-in") + response = django_client.post(url, {}, follow=True) + + # Check redirect contains error code + assert "REQUIRED_EMAIL_PASSWORD_SIGN_IN" in response.redirect_chain[-1][0] + + @pytest.mark.django_db + def test_email_validity(self, django_client, setup_user, setup_instance): + """Test sign-in with invalid email format""" + url = reverse("sign-in") + response = django_client.post( + url, {"email": "useremail.com", "password": "user@123"}, follow=True + ) + + # Check redirect contains error code + assert "INVALID_EMAIL_SIGN_IN" in response.redirect_chain[-1][0] + + @pytest.mark.django_db + def test_user_exists(self, django_client, setup_user, setup_instance): + """Test sign-in with non-existent user""" + url = reverse("sign-in") + response = django_client.post( + url, {"email": "user@email.so", "password": "user123"}, follow=True + ) + + # Check redirect contains error code + assert "USER_DOES_NOT_EXIST" in response.redirect_chain[-1][0] + + @pytest.mark.django_db + def test_password_validity(self, django_client, setup_user, setup_instance): + """Test sign-in with incorrect password""" + url = reverse("sign-in") + response = django_client.post( + url, {"email": "user@plane.so", "password": "user123"}, follow=True + ) + + + # Check for the specific authentication error in the URL + redirect_urls = [url for url, _ in response.redirect_chain] + redirect_contents = ' '.join(redirect_urls) + + # The actual error code for invalid password is AUTHENTICATION_FAILED_SIGN_IN + assert "AUTHENTICATION_FAILED_SIGN_IN" in redirect_contents + + @pytest.mark.django_db + def test_user_login(self, django_client, setup_user, setup_instance): + """Test successful sign-in""" + url = reverse("sign-in") + + # First make the request without following redirects + response = django_client.post( + url, {"email": "user@plane.so", "password": "user@123"}, follow=False + ) + + # Check that the initial response is a redirect (302) without error code + assert response.status_code == 302 + assert "error_code" not in response.url + + # Now follow just the first redirect to avoid 404s + response = django_client.get(response.url, follow=False) + + # The user should be authenticated regardless of the final page + assert "_auth_user_id" in django_client.session + + @pytest.mark.django_db + def test_next_path_redirection(self, django_client, setup_user, setup_instance): + """Test sign-in with next_path parameter""" + url = reverse("sign-in") + next_path = "workspaces" + + # First make the request without following redirects + response = django_client.post( + url, + {"email": "user@plane.so", "password": "user@123", "next_path": next_path}, + follow=False + ) + + # Check that the initial response is a redirect (302) without error code + assert response.status_code == 302 + assert "error_code" not in response.url + + + # In a real browser, the next_path would be used to build the absolute URL + # Since we're just testing the authentication logic, we won't check for the exact URL structure + # Instead, just verify that we're authenticated + assert "_auth_user_id" in django_client.session + + +@pytest.mark.contract +class TestMagicSignIn: + """Test magic link sign-in functionality""" + + @pytest.fixture + def setup_user(self, db): + """Create a test user for magic sign-in tests""" + user = User.objects.create(email="user@plane.so") + user.set_password("user@123") + user.save() + return user + + @pytest.mark.django_db + def test_without_data(self, django_client, setup_user, setup_instance): + """Test magic link sign-in with empty data""" + url = reverse("magic-sign-in") + response = django_client.post(url, {}, follow=True) + + # Check redirect contains error code + assert "MAGIC_SIGN_IN_EMAIL_CODE_REQUIRED" in response.redirect_chain[-1][0] + + @pytest.mark.django_db + def test_expired_invalid_magic_link(self, django_client, setup_user, setup_instance): + """Test magic link sign-in with expired/invalid link""" + ri = redis_instance() + ri.delete("magic_user@plane.so") + + url = reverse("magic-sign-in") + response = django_client.post( + url, + {"email": "user@plane.so", "code": "xxxx-xxxxx-xxxx"}, + follow=False + ) + + # Check that we get a redirect + assert response.status_code == 302 + + # The actual error code is EXPIRED_MAGIC_CODE_SIGN_IN (when key doesn't exist) + # or INVALID_MAGIC_CODE_SIGN_IN (when key exists but code doesn't match) + assert "EXPIRED_MAGIC_CODE_SIGN_IN" in response.url or "INVALID_MAGIC_CODE_SIGN_IN" in response.url + + @pytest.mark.django_db + def test_user_does_not_exist(self, django_client, setup_instance): + """Test magic sign-in with non-existent user""" + url = reverse("magic-sign-in") + response = django_client.post( + url, + {"email": "nonexistent@plane.so", "code": "xxxx-xxxxx-xxxx"}, + follow=True + ) + + # Check redirect contains error code + assert "USER_DOES_NOT_EXIST" in response.redirect_chain[-1][0] + + @pytest.mark.django_db + @patch("plane.bgtasks.magic_link_code_task.magic_link.delay") + def test_magic_code_sign_in(self, mock_magic_link, django_client, api_client, setup_user, setup_instance): + """Test successful magic link sign-in process""" + # First generate a magic link token + gen_url = reverse("magic-generate") + response = api_client.post(gen_url, {"email": "user@plane.so"}, format="json") + + # Check that the token generation was successful + assert response.status_code == status.HTTP_200_OK + + # Since we're mocking the magic_link task, we need to manually get the token from Redis + ri = redis_instance() + user_data = json.loads(ri.get("magic_user@plane.so")) + token = user_data["token"] + + # Use Django client to test the redirect flow without following redirects + url = reverse("magic-sign-in") + response = django_client.post( + url, + {"email": "user@plane.so", "code": token}, + follow=False + ) + + # Check that the initial response is a redirect without error code + assert response.status_code == 302 + assert "error_code" not in response.url + + # The user should now be authenticated + assert "_auth_user_id" in django_client.session + + @pytest.mark.django_db + @patch("plane.bgtasks.magic_link_code_task.magic_link.delay") + def test_magic_sign_in_with_next_path(self, mock_magic_link, django_client, api_client, setup_user, setup_instance): + """Test magic sign-in with next_path parameter""" + # First generate a magic link token + gen_url = reverse("magic-generate") + response = api_client.post(gen_url, {"email": "user@plane.so"}, format="json") + + # Check that the token generation was successful + assert response.status_code == status.HTTP_200_OK + + # Since we're mocking the magic_link task, we need to manually get the token from Redis + ri = redis_instance() + user_data = json.loads(ri.get("magic_user@plane.so")) + token = user_data["token"] + + # Use Django client to test the redirect flow without following redirects + url = reverse("magic-sign-in") + next_path = "workspaces" + response = django_client.post( + url, + {"email": "user@plane.so", "code": token, "next_path": next_path}, + follow=False + ) + + # Check that the initial response is a redirect without error code + assert response.status_code == 302 + assert "error_code" not in response.url + + # Check that the redirect URL contains the next_path + assert next_path in response.url + + # The user should now be authenticated + assert "_auth_user_id" in django_client.session + + +@pytest.mark.contract +class TestMagicSignUp: + """Test magic link sign-up functionality""" + + @pytest.mark.django_db + def test_without_data(self, django_client, setup_instance): + """Test magic link sign-up with empty data""" + url = reverse("magic-sign-up") + response = django_client.post(url, {}, follow=True) + + # Check redirect contains error code + assert "MAGIC_SIGN_UP_EMAIL_CODE_REQUIRED" in response.redirect_chain[-1][0] + + @pytest.mark.django_db + def test_user_already_exists(self, django_client, db, setup_instance): + """Test magic sign-up with existing user""" + # Create a user that already exists + User.objects.create(email="existing@plane.so") + + url = reverse("magic-sign-up") + response = django_client.post( + url, + {"email": "existing@plane.so", "code": "xxxx-xxxxx-xxxx"}, + follow=True + ) + + # Check redirect contains error code + assert "USER_ALREADY_EXIST" in response.redirect_chain[-1][0] + + @pytest.mark.django_db + def test_expired_invalid_magic_link(self, django_client, setup_instance): + """Test magic link sign-up with expired/invalid link""" + url = reverse("magic-sign-up") + response = django_client.post( + url, + {"email": "new@plane.so", "code": "xxxx-xxxxx-xxxx"}, + follow=False + ) + + # Check that we get a redirect + assert response.status_code == 302 + + # The actual error code is EXPIRED_MAGIC_CODE_SIGN_UP (when key doesn't exist) + # or INVALID_MAGIC_CODE_SIGN_UP (when key exists but code doesn't match) + assert "EXPIRED_MAGIC_CODE_SIGN_UP" in response.url or "INVALID_MAGIC_CODE_SIGN_UP" in response.url + + @pytest.mark.django_db + @patch("plane.bgtasks.magic_link_code_task.magic_link.delay") + def test_magic_code_sign_up(self, mock_magic_link, django_client, api_client, setup_instance): + """Test successful magic link sign-up process""" + email = "newuser@plane.so" + + # First generate a magic link token + gen_url = reverse("magic-generate") + response = api_client.post(gen_url, {"email": email}, format="json") + + # Check that the token generation was successful + assert response.status_code == status.HTTP_200_OK + + # Since we're mocking the magic_link task, we need to manually get the token from Redis + ri = redis_instance() + user_data = json.loads(ri.get(f"magic_{email}")) + token = user_data["token"] + + # Use Django client to test the redirect flow without following redirects + url = reverse("magic-sign-up") + response = django_client.post( + url, + {"email": email, "code": token}, + follow=False + ) + + # Check that the initial response is a redirect without error code + assert response.status_code == 302 + assert "error_code" not in response.url + + # Check if user was created + assert User.objects.filter(email=email).exists() + + # Check if user is authenticated + assert "_auth_user_id" in django_client.session + + @pytest.mark.django_db + @patch("plane.bgtasks.magic_link_code_task.magic_link.delay") + def test_magic_sign_up_with_next_path(self, mock_magic_link, django_client, api_client, setup_instance): + """Test magic sign-up with next_path parameter""" + email = "newuser2@plane.so" + + # First generate a magic link token + gen_url = reverse("magic-generate") + response = api_client.post(gen_url, {"email": email}, format="json") + + # Check that the token generation was successful + assert response.status_code == status.HTTP_200_OK + + # Since we're mocking the magic_link task, we need to manually get the token from Redis + ri = redis_instance() + user_data = json.loads(ri.get(f"magic_{email}")) + token = user_data["token"] + + # Use Django client to test the redirect flow without following redirects + url = reverse("magic-sign-up") + next_path = "onboarding" + response = django_client.post( + url, + {"email": email, "code": token, "next_path": next_path}, + follow=False + ) + + # Check that the initial response is a redirect without error code + assert response.status_code == 302 + assert "error_code" not in response.url + + # In a real browser, the next_path would be used to build the absolute URL + # Since we're just testing the authentication logic, we won't check for the exact URL structure + + # Check if user was created + assert User.objects.filter(email=email).exists() + + # Check if user is authenticated + assert "_auth_user_id" in django_client.session \ No newline at end of file diff --git a/apiserver/plane/tests/contract/app/test_workspace_app.py b/apiserver/plane/tests/contract/app/test_workspace_app.py new file mode 100644 index 000000000..71ad1d412 --- /dev/null +++ b/apiserver/plane/tests/contract/app/test_workspace_app.py @@ -0,0 +1,79 @@ +import pytest +from django.urls import reverse +from rest_framework import status +from unittest.mock import patch + +from plane.db.models import Workspace, WorkspaceMember + + +@pytest.mark.contract +class TestWorkspaceAPI: + """Test workspace CRUD operations""" + + @pytest.mark.django_db + def test_create_workspace_empty_data(self, session_client): + """Test creating a workspace with empty data""" + url = reverse("workspace") + + # Test with empty data + response = session_client.post(url, {}, format="json") + assert response.status_code == status.HTTP_400_BAD_REQUEST + + @pytest.mark.django_db + @patch("plane.bgtasks.workspace_seed_task.workspace_seed.delay") + def test_create_workspace_valid_data(self, mock_workspace_seed, session_client, create_user): + """Test creating a workspace with valid data""" + url = reverse("workspace") + user = create_user # Use the create_user fixture directly as it returns a user object + + # Test with valid data - include all required fields + workspace_data = { + "name": "Plane", + "slug": "pla-ne-test", + "company_name": "Plane Inc." + } + + # Make the request + response = session_client.post(url, workspace_data, format="json") + + # Check response status + assert response.status_code == status.HTTP_201_CREATED + + # Verify workspace was created + assert Workspace.objects.count() == 1 + + # Check if the member is created + assert WorkspaceMember.objects.count() == 1 + + # Check other values + workspace = Workspace.objects.get(slug=workspace_data["slug"]) + workspace_member = WorkspaceMember.objects.filter( + workspace=workspace, member=user + ).first() + assert workspace.owner == user + assert workspace_member.role == 20 + + # Verify the workspace_seed task was called + mock_workspace_seed.assert_called_once_with(response.data["id"]) + + @pytest.mark.django_db + @patch('plane.bgtasks.workspace_seed_task.workspace_seed.delay') + def test_create_duplicate_workspace(self, mock_workspace_seed, session_client): + """Test creating a duplicate workspace""" + url = reverse("workspace") + + # Create first workspace + session_client.post( + url, {"name": "Plane", "slug": "pla-ne"}, format="json" + ) + + # Try to create a workspace with the same slug + response = session_client.post( + url, {"name": "Plane", "slug": "pla-ne"}, format="json" + ) + + # The API returns 400 BAD REQUEST for duplicate slugs, not 409 CONFLICT + assert response.status_code == status.HTTP_400_BAD_REQUEST + + # Optionally check the error message to confirm it's related to the duplicate slug + assert "slug" in response.data \ No newline at end of file diff --git a/apiserver/plane/tests/factories.py b/apiserver/plane/tests/factories.py new file mode 100644 index 000000000..8d95773de --- /dev/null +++ b/apiserver/plane/tests/factories.py @@ -0,0 +1,82 @@ +import factory +from uuid import uuid4 +from django.utils import timezone + +from plane.db.models import ( + User, + Workspace, + WorkspaceMember, + Project, + ProjectMember +) + + +class UserFactory(factory.django.DjangoModelFactory): + """Factory for creating User instances""" + class Meta: + model = User + django_get_or_create = ('email',) + + id = factory.LazyFunction(uuid4) + email = factory.Sequence(lambda n: f'user{n}@plane.so') + password = factory.PostGenerationMethodCall('set_password', 'password') + first_name = factory.Sequence(lambda n: f'First{n}') + last_name = factory.Sequence(lambda n: f'Last{n}') + is_active = True + is_superuser = False + is_staff = False + + +class WorkspaceFactory(factory.django.DjangoModelFactory): + """Factory for creating Workspace instances""" + class Meta: + model = Workspace + django_get_or_create = ('slug',) + + id = factory.LazyFunction(uuid4) + name = factory.Sequence(lambda n: f'Workspace {n}') + slug = factory.Sequence(lambda n: f'workspace-{n}') + owner = factory.SubFactory(UserFactory) + created_at = factory.LazyFunction(timezone.now) + updated_at = factory.LazyFunction(timezone.now) + + +class WorkspaceMemberFactory(factory.django.DjangoModelFactory): + """Factory for creating WorkspaceMember instances""" + class Meta: + model = WorkspaceMember + + id = factory.LazyFunction(uuid4) + workspace = factory.SubFactory(WorkspaceFactory) + member = factory.SubFactory(UserFactory) + role = 20 # Admin role by default + created_at = factory.LazyFunction(timezone.now) + updated_at = factory.LazyFunction(timezone.now) + + +class ProjectFactory(factory.django.DjangoModelFactory): + """Factory for creating Project instances""" + class Meta: + model = Project + django_get_or_create = ('name', 'workspace') + + id = factory.LazyFunction(uuid4) + name = factory.Sequence(lambda n: f'Project {n}') + workspace = factory.SubFactory(WorkspaceFactory) + created_by = factory.SelfAttribute('workspace.owner') + updated_by = factory.SelfAttribute('workspace.owner') + created_at = factory.LazyFunction(timezone.now) + updated_at = factory.LazyFunction(timezone.now) + + +class ProjectMemberFactory(factory.django.DjangoModelFactory): + """Factory for creating ProjectMember instances""" + class Meta: + model = ProjectMember + + id = factory.LazyFunction(uuid4) + project = factory.SubFactory(ProjectFactory) + member = factory.SubFactory(UserFactory) + role = 20 # Admin role by default + created_at = factory.LazyFunction(timezone.now) + updated_at = factory.LazyFunction(timezone.now) \ No newline at end of file diff --git a/apiserver/plane/tests/smoke/__init__.py b/apiserver/plane/tests/smoke/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apiserver/plane/tests/smoke/test_auth_smoke.py b/apiserver/plane/tests/smoke/test_auth_smoke.py new file mode 100644 index 000000000..4d6de6c35 --- /dev/null +++ b/apiserver/plane/tests/smoke/test_auth_smoke.py @@ -0,0 +1,100 @@ +import pytest +import requests +from django.urls import reverse + + +@pytest.mark.smoke +class TestAuthSmoke: + """Smoke tests for authentication endpoints""" + + @pytest.mark.django_db + def test_login_endpoint_available(self, plane_server, create_user, user_data): + """Test that the login endpoint is available and responds correctly""" + # Get the sign-in URL + relative_url = reverse("sign-in") + url = f"{plane_server.url}{relative_url}" + + # 1. Test bad login - test with wrong password + response = requests.post( + url, + data={ + "email": user_data["email"], + "password": "wrong-password" + } + ) + + # For bad credentials, any of these status codes would be valid + # The test shouldn't be brittle to minor implementation changes + assert response.status_code != 500, "Authentication should not cause server errors" + assert response.status_code != 404, "Authentication endpoint should exist" + + if response.status_code == 200: + # If API returns 200 for failures, check the response body for error indication + if hasattr(response, 'json'): + try: + data = response.json() + # JSON response might indicate error in its structure + assert "error" in data or "error_code" in data or "detail" in data or response.url.endswith("sign-in"), \ + "Error response should contain error details" + except ValueError: + # It's ok if response isn't JSON format + pass + elif response.status_code in [302, 303]: + # If it's a redirect, it should redirect to a login page or error page + redirect_url = response.headers.get('Location', '') + assert "error" in redirect_url or "sign-in" in redirect_url, \ + "Failed login should redirect to login page or error page" + + # 2. Test good login with correct credentials + response = requests.post( + url, + data={ + "email": user_data["email"], + "password": user_data["password"] + }, + allow_redirects=False # Don't follow redirects + ) + + # Successful auth should not be a client error or server error + assert response.status_code not in range(400, 600), \ + f"Authentication with valid credentials failed with status {response.status_code}" + + # Specific validation based on response type + if response.status_code in [302, 303]: + # Redirect-based auth: check that redirect URL doesn't contain error + redirect_url = response.headers.get('Location', '') + assert "error" not in redirect_url and "error_code" not in redirect_url, \ + "Successful login redirect should not contain error parameters" + + elif response.status_code == 200: + # API token-based auth: check for tokens or user session + if hasattr(response, 'json'): + try: + data = response.json() + # If it's a token response + if "access_token" in data: + assert "refresh_token" in data, "JWT auth should return both access and refresh tokens" + # If it's a user session response + elif "user" in data: + assert "is_authenticated" in data and data["is_authenticated"], \ + "User session response should indicate authentication" + # Otherwise it should at least indicate success + else: + assert not any(error_key in data for error_key in ["error", "error_code", "detail"]), \ + "Success response should not contain error keys" + except ValueError: + # Non-JSON is acceptable if it's a redirect or HTML response + pass + + +@pytest.mark.smoke +class TestHealthCheckSmoke: + """Smoke test for health check endpoint""" + + def test_healthcheck_endpoint(self, plane_server): + """Test that the health check endpoint is available and responds correctly""" + # Make a request to the health check endpoint + response = requests.get(f"{plane_server.url}/") + + # Should be OK + assert response.status_code == 200, "Health check endpoint should return 200 OK" \ No newline at end of file diff --git a/apiserver/plane/tests/unit/__init__.py b/apiserver/plane/tests/unit/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apiserver/plane/tests/unit/models/__init__.py b/apiserver/plane/tests/unit/models/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apiserver/plane/tests/unit/models/test_workspace_model.py b/apiserver/plane/tests/unit/models/test_workspace_model.py new file mode 100644 index 000000000..40380fa0f --- /dev/null +++ b/apiserver/plane/tests/unit/models/test_workspace_model.py @@ -0,0 +1,50 @@ +import pytest +from uuid import uuid4 + +from plane.db.models import Workspace, WorkspaceMember, User + + +@pytest.mark.unit +class TestWorkspaceModel: + """Test the Workspace model""" + + @pytest.mark.django_db + def test_workspace_creation(self, create_user): + """Test creating a workspace""" + # Create a workspace + workspace = Workspace.objects.create( + name="Test Workspace", + slug="test-workspace", + id=uuid4(), + owner=create_user + ) + + # Verify it was created + assert workspace.id is not None + assert workspace.name == "Test Workspace" + assert workspace.slug == "test-workspace" + assert workspace.owner == create_user + + @pytest.mark.django_db + def test_workspace_member_creation(self, create_user): + """Test creating a workspace member""" + # Create a workspace + workspace = Workspace.objects.create( + name="Test Workspace", + slug="test-workspace", + id=uuid4(), + owner=create_user + ) + + # Create a workspace member + workspace_member = WorkspaceMember.objects.create( + workspace=workspace, + member=create_user, + role=20 # Admin role + ) + + # Verify it was created + assert workspace_member.id is not None + assert workspace_member.workspace == workspace + assert workspace_member.member == create_user + assert workspace_member.role == 20 \ No newline at end of file diff --git a/apiserver/plane/tests/unit/serializers/__init__.py b/apiserver/plane/tests/unit/serializers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apiserver/plane/tests/unit/serializers/test_workspace.py b/apiserver/plane/tests/unit/serializers/test_workspace.py new file mode 100644 index 000000000..19767a7c6 --- /dev/null +++ b/apiserver/plane/tests/unit/serializers/test_workspace.py @@ -0,0 +1,71 @@ +import pytest +from uuid import uuid4 + +from plane.api.serializers import WorkspaceLiteSerializer +from plane.db.models import Workspace, User + + +@pytest.mark.unit +class TestWorkspaceLiteSerializer: + """Test the WorkspaceLiteSerializer""" + + def test_workspace_lite_serializer_fields(self, db): + """Test that the serializer includes the correct fields""" + # Create a user to be the owner + owner = User.objects.create( + email="test@example.com", + first_name="Test", + last_name="User" + ) + + # Create a workspace with explicit ID to test serialization + workspace_id = uuid4() + workspace = Workspace.objects.create( + name="Test Workspace", + slug="test-workspace", + id=workspace_id, + owner=owner + ) + + # Serialize the workspace + serialized_data = WorkspaceLiteSerializer(workspace).data + + # Check fields are present and correct + assert "name" in serialized_data + assert "slug" in serialized_data + assert "id" in serialized_data + + assert serialized_data["name"] == "Test Workspace" + assert serialized_data["slug"] == "test-workspace" + assert str(serialized_data["id"]) == str(workspace_id) + + def test_workspace_lite_serializer_read_only(self, db): + """Test that the serializer fields are read-only""" + # Create a user to be the owner + owner = User.objects.create( + email="test2@example.com", + first_name="Test", + last_name="User" + ) + + # Create a workspace + workspace = Workspace.objects.create( + name="Test Workspace", + slug="test-workspace", + id=uuid4(), + owner=owner + ) + + # Try to update via serializer + serializer = WorkspaceLiteSerializer( + workspace, + data={"name": "Updated Name", "slug": "updated-slug"} + ) + + # Serializer should be valid (since read-only fields are ignored) + assert serializer.is_valid() + + # Save should not update the read-only fields + updated_workspace = serializer.save() + assert updated_workspace.name == "Test Workspace" + assert updated_workspace.slug == "test-workspace" \ No newline at end of file diff --git a/apiserver/plane/tests/unit/utils/__init__.py b/apiserver/plane/tests/unit/utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apiserver/plane/tests/unit/utils/test_uuid.py b/apiserver/plane/tests/unit/utils/test_uuid.py new file mode 100644 index 000000000..81403c5be --- /dev/null +++ b/apiserver/plane/tests/unit/utils/test_uuid.py @@ -0,0 +1,49 @@ +import uuid +import pytest +from plane.utils.uuid import is_valid_uuid, convert_uuid_to_integer + + +@pytest.mark.unit +class TestUUIDUtils: + """Test the UUID utilities""" + + def test_is_valid_uuid_with_valid_uuid(self): + """Test is_valid_uuid with a valid UUID""" + # Generate a valid UUID + valid_uuid = str(uuid.uuid4()) + assert is_valid_uuid(valid_uuid) is True + + def test_is_valid_uuid_with_invalid_uuid(self): + """Test is_valid_uuid with invalid UUID strings""" + # Test with different invalid formats + assert is_valid_uuid("not-a-uuid") is False + assert is_valid_uuid("123456789") is False + assert is_valid_uuid("") is False + assert is_valid_uuid("00000000-0000-0000-0000-000000000000") is False # This is a valid UUID but version 1 + + def test_convert_uuid_to_integer(self): + """Test convert_uuid_to_integer function""" + # Create a known UUID + test_uuid = uuid.UUID("f47ac10b-58cc-4372-a567-0e02b2c3d479") + + # Convert to integer + result = convert_uuid_to_integer(test_uuid) + + # Check that the result is an integer + assert isinstance(result, int) + + # Ensure consistent results with the same input + assert convert_uuid_to_integer(test_uuid) == result + + # Different UUIDs should produce different integers + different_uuid = uuid.UUID("550e8400-e29b-41d4-a716-446655440000") + assert convert_uuid_to_integer(different_uuid) != result + + def test_convert_uuid_to_integer_string_input(self): + """Test convert_uuid_to_integer handles string UUID""" + # Test with a UUID string + test_uuid_str = "f47ac10b-58cc-4372-a567-0e02b2c3d479" + test_uuid = uuid.UUID(test_uuid_str) + + # Should get the same result whether passing UUID or string + assert convert_uuid_to_integer(test_uuid) == convert_uuid_to_integer(test_uuid_str) \ No newline at end of file diff --git a/apiserver/pytest.ini b/apiserver/pytest.ini new file mode 100644 index 000000000..e2f194456 --- /dev/null +++ b/apiserver/pytest.ini @@ -0,0 +1,17 @@ +[pytest] +DJANGO_SETTINGS_MODULE = plane.settings.test +python_files = test_*.py +python_classes = Test* +python_functions = test_* + +markers = + unit: Unit tests for models, serializers, and utility functions + contract: Contract tests for API endpoints + smoke: Smoke tests for critical functionality + slow: Tests that are slow and might be skipped in some contexts + +addopts = + --strict-markers + --reuse-db + --nomigrations + -vs \ No newline at end of file diff --git a/apiserver/requirements/test.txt b/apiserver/requirements/test.txt index 1ffc82d00..85978128b 100644 --- a/apiserver/requirements/test.txt +++ b/apiserver/requirements/test.txt @@ -1,4 +1,12 @@ -r base.txt -# test checker -pytest==7.1.2 -coverage==6.5.0 \ No newline at end of file +# test framework +pytest==7.4.0 +pytest-django==4.5.2 +pytest-cov==4.1.0 +pytest-xdist==3.3.1 +pytest-mock==3.11.1 +factory-boy==3.3.0 +freezegun==1.2.2 +coverage==7.2.7 +httpx==0.24.1 +requests==2.31.0 \ No newline at end of file diff --git a/apiserver/run_tests.py b/apiserver/run_tests.py new file mode 100755 index 000000000..f4f0951b1 --- /dev/null +++ b/apiserver/run_tests.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python +import argparse +import subprocess +import sys + + +def main(): + parser = argparse.ArgumentParser(description="Run Plane tests") + parser.add_argument( + "-u", "--unit", + action="store_true", + help="Run unit tests only" + ) + parser.add_argument( + "-c", "--contract", + action="store_true", + help="Run contract tests only" + ) + parser.add_argument( + "-s", "--smoke", + action="store_true", + help="Run smoke tests only" + ) + parser.add_argument( + "-o", "--coverage", + action="store_true", + help="Generate coverage report" + ) + parser.add_argument( + "-p", "--parallel", + action="store_true", + help="Run tests in parallel" + ) + parser.add_argument( + "-v", "--verbose", + action="store_true", + help="Verbose output" + ) + args = parser.parse_args() + + # Build command + cmd = ["python", "-m", "pytest"] + markers = [] + + # Add test markers + if args.unit: + markers.append("unit") + if args.contract: + markers.append("contract") + if args.smoke: + markers.append("smoke") + + # Add markers filter + if markers: + cmd.extend(["-m", " or ".join(markers)]) + + # Add coverage + if args.coverage: + cmd.extend(["--cov=plane", "--cov-report=term", "--cov-report=html"]) + + # Add parallel + if args.parallel: + cmd.extend(["-n", "auto"]) + + # Add verbose + if args.verbose: + cmd.append("-v") + + # Add common flags + cmd.extend(["--reuse-db", "--nomigrations"]) + + # Print command + print(f"Running: {' '.join(cmd)}") + + # Execute command + result = subprocess.run(cmd) + + # Check coverage thresholds if coverage is enabled + if args.coverage: + print("Checking coverage thresholds...") + coverage_cmd = ["python", "-m", "coverage", "report", "--fail-under=90"] + coverage_result = subprocess.run(coverage_cmd) + if coverage_result.returncode != 0: + print("Coverage below threshold (90%)") + sys.exit(coverage_result.returncode) + + sys.exit(result.returncode) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/apiserver/run_tests.sh b/apiserver/run_tests.sh new file mode 100755 index 000000000..7e22479b5 --- /dev/null +++ b/apiserver/run_tests.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +# This is a simple wrapper script that calls the main test runner in the tests directory +exec tests/run_tests.sh "$@" \ No newline at end of file From 04c7c53e0926910aa2dae2b68348794a12ba742f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 May 2025 19:45:15 +0530 Subject: [PATCH 091/201] chore(deps): bump requests (#7120) Bumps the pip group with 1 update in the /apiserver/requirements directory: [requests](https://github.com/psf/requests). Updates `requests` from 2.31.0 to 2.32.2 - [Release notes](https://github.com/psf/requests/releases) - [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md) - [Commits](https://github.com/psf/requests/compare/v2.31.0...v2.32.2) --- updated-dependencies: - dependency-name: requests dependency-version: 2.32.2 dependency-type: direct:production dependency-group: pip ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apiserver/requirements/test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apiserver/requirements/test.txt b/apiserver/requirements/test.txt index 85978128b..9536ab1e2 100644 --- a/apiserver/requirements/test.txt +++ b/apiserver/requirements/test.txt @@ -9,4 +9,4 @@ factory-boy==3.3.0 freezegun==1.2.2 coverage==7.2.7 httpx==0.24.1 -requests==2.31.0 \ No newline at end of file +requests==2.32.2 \ No newline at end of file From b4bc49971c6a5078757612113f51a3155a4d52b0 Mon Sep 17 00:00:00 2001 From: Akshita Goyal <36129505+gakshita@users.noreply.github.com> Date: Wed, 28 May 2025 00:54:21 +0530 Subject: [PATCH 092/201] [WEB-4130] fix: cycle charts minor optimizations (#7123) --- .../cycles/analytics-sidebar/issue-progress.tsx | 2 +- .../cycles/dropdowns/estimate-type-dropdown.tsx | 11 +++++++---- web/core/store/estimates/project-estimate.store.ts | 7 +++++++ 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/web/core/components/cycles/analytics-sidebar/issue-progress.tsx b/web/core/components/cycles/analytics-sidebar/issue-progress.tsx index c0ece831c..f911b4418 100644 --- a/web/core/components/cycles/analytics-sidebar/issue-progress.tsx +++ b/web/core/components/cycles/analytics-sidebar/issue-progress.tsx @@ -32,7 +32,7 @@ type Options = { export const cycleEstimateOptions: Options[] = [ { value: "issues", label: "Work items" }, - { value: "points", label: "Points" }, + { value: "points", label: "Estimates" }, ]; export const cycleChartOptions: Options[] = [ { value: "burndown", label: "Burn-down" }, diff --git a/web/core/components/cycles/dropdowns/estimate-type-dropdown.tsx b/web/core/components/cycles/dropdowns/estimate-type-dropdown.tsx index 7eba6418d..2899b7ca3 100644 --- a/web/core/components/cycles/dropdowns/estimate-type-dropdown.tsx +++ b/web/core/components/cycles/dropdowns/estimate-type-dropdown.tsx @@ -1,5 +1,7 @@ import React from "react"; +import { observer } from "mobx-react-lite"; import { TCycleEstimateType } from "@plane/types"; +import { EEstimateSystem } from "@plane/types/src/enums"; import { CustomSelect } from "@plane/ui"; import { useCycle, useProjectEstimates } from "@/hooks/store"; import { cycleEstimateOptions } from "../analytics-sidebar"; @@ -12,12 +14,13 @@ type TProps = { cycleId: string; }; -export const EstimateTypeDropdown = (props: TProps) => { +export const EstimateTypeDropdown = observer((props: TProps) => { const { value, onChange, projectId, cycleId, showDefault = false } = props; const { getIsPointsDataAvailable } = useCycle(); - const { areEstimateEnabledByProjectId } = useProjectEstimates(); + const { areEstimateEnabledByProjectId, currentProjectEstimateType } = useProjectEstimates(); const isCurrentProjectEstimateEnabled = projectId && areEstimateEnabledByProjectId(projectId) ? true : false; - return getIsPointsDataAvailable(cycleId) || isCurrentProjectEstimateEnabled ? ( + return (getIsPointsDataAvailable(cycleId) || isCurrentProjectEstimateEnabled) && + currentProjectEstimateType !== EEstimateSystem.CATEGORIES ? (
{ ) : showDefault ? ( {value} ) : null; -}; +}); diff --git a/web/core/store/estimates/project-estimate.store.ts b/web/core/store/estimates/project-estimate.store.ts index 773a7d6dc..c8c3a17aa 100644 --- a/web/core/store/estimates/project-estimate.store.ts +++ b/web/core/store/estimates/project-estimate.store.ts @@ -27,6 +27,7 @@ export interface IProjectEstimateStore { currentActiveEstimateId: string | undefined; currentActiveEstimate: IEstimate | undefined; archivedEstimateIds: string[] | undefined; + currentProjectEstimateType: TEstimateSystemKeys | undefined; areEstimateEnabledByProjectId: (projectId: string) => boolean; estimateIdsByProjectId: (projectId: string) => string[] | undefined; currentActiveEstimateIdByProjectId: (projectId: string) => string | undefined; @@ -63,6 +64,7 @@ export class ProjectEstimateStore implements IProjectEstimateStore { currentActiveEstimateId: computed, currentActiveEstimate: computed, archivedEstimateIds: computed, + currentProjectEstimateType: computed, // actions getWorkspaceEstimates: action, getProjectEstimates: action, @@ -73,6 +75,11 @@ export class ProjectEstimateStore implements IProjectEstimateStore { } // computed + + get currentProjectEstimateType(): TEstimateSystemKeys | undefined { + return this.currentActiveEstimateId ? this.estimates[this.currentActiveEstimateId]?.type : undefined; + } + /** * @description get current active estimate id for a project * @returns { string | undefined } From a3a580923c43c7550efeb623886911f15dd7560a Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Wed, 28 May 2025 00:58:22 +0530 Subject: [PATCH 093/201] [WEB-4166] chore: projects app sidebar accessibility (#7115) * chore: add ARIA attributes * chore: add missing translations * chore: add accessibility translations for multiple languages and configured store according to it * chore: refactor translation file handling and introduce TranslationFiles enum * fix: accessibility issues in workspace sidebar --------- Co-authored-by: JayashTripathy Co-authored-by: Prateek Shourya --- packages/i18n/src/constants/language.ts | 9 ++ .../i18n/src/locales/cs/accessibility.json | 27 ++++++ .../i18n/src/locales/de/accessibility.json | 27 ++++++ .../i18n/src/locales/en/accessibility.json | 27 ++++++ .../i18n/src/locales/en/translations.json | 2 +- .../i18n/src/locales/es/accessibility.json | 27 ++++++ .../i18n/src/locales/fr/accessibility.json | 27 ++++++ .../i18n/src/locales/id/accessibility.json | 27 ++++++ .../i18n/src/locales/it/accessibility.json | 27 ++++++ .../i18n/src/locales/ja/accessibility.json | 27 ++++++ .../i18n/src/locales/ko/accessibility.json | 27 ++++++ .../i18n/src/locales/pl/accessibility.json | 27 ++++++ .../i18n/src/locales/pt-BR/accessibility.json | 27 ++++++ .../i18n/src/locales/ro/accessibility.json | 27 ++++++ .../i18n/src/locales/ru/accessibility.json | 27 ++++++ .../i18n/src/locales/sk/accessibility.json | 27 ++++++ .../i18n/src/locales/tr-TR/accessibility.json | 27 ++++++ .../i18n/src/locales/ua/accessibility.json | 27 ++++++ .../i18n/src/locales/vi-VN/accessibility.json | 27 ++++++ .../i18n/src/locales/zh-CN/accessibility.json | 27 ++++++ .../i18n/src/locales/zh-TW/accessibility.json | 27 ++++++ packages/i18n/src/store/index.ts | 66 +++++--------- packages/ui/src/avatar/avatar.tsx | 2 +- packages/ui/src/dropdowns/custom-menu.tsx | 6 +- packages/ui/src/dropdowns/helper.tsx | 1 + .../(projects)/extended-sidebar.tsx | 6 +- web/ce/components/workspace/edition-badge.tsx | 10 ++- .../workspace/sidebar/app-search.tsx | 6 ++ .../sidebar/extended-sidebar-item.tsx | 2 +- .../workspace/sidebar/sidebar-item.tsx | 2 +- web/ce/constants/sidebar-favorites.ts | 4 +- web/core/components/workspace/logo.tsx | 47 +++++----- .../components/workspace/sidebar/dropdown.tsx | 23 ++--- .../sidebar/favorites/favorite-folder.tsx | 20 +++-- .../common/favorite-item-quick-action.tsx | 14 ++- .../sidebar/favorites/favorites-menu.tsx | 87 +++++++++++++------ .../sidebar/favorites/new-fav-folder.tsx | 9 +- .../workspace/sidebar/help-section.tsx | 7 +- .../workspace/sidebar/projects-list-item.tsx | 15 +++- .../workspace/sidebar/projects-list.tsx | 15 +++- .../workspace/sidebar/quick-actions.tsx | 4 +- .../workspace/sidebar/sidebar-menu-items.tsx | 62 +++++++------ web/core/store/theme.store.ts | 15 ++-- 43 files changed, 777 insertions(+), 170 deletions(-) create mode 100644 packages/i18n/src/locales/cs/accessibility.json create mode 100644 packages/i18n/src/locales/de/accessibility.json create mode 100644 packages/i18n/src/locales/en/accessibility.json create mode 100644 packages/i18n/src/locales/es/accessibility.json create mode 100644 packages/i18n/src/locales/fr/accessibility.json create mode 100644 packages/i18n/src/locales/id/accessibility.json create mode 100644 packages/i18n/src/locales/it/accessibility.json create mode 100644 packages/i18n/src/locales/ja/accessibility.json create mode 100644 packages/i18n/src/locales/ko/accessibility.json create mode 100644 packages/i18n/src/locales/pl/accessibility.json create mode 100644 packages/i18n/src/locales/pt-BR/accessibility.json create mode 100644 packages/i18n/src/locales/ro/accessibility.json create mode 100644 packages/i18n/src/locales/ru/accessibility.json create mode 100644 packages/i18n/src/locales/sk/accessibility.json create mode 100644 packages/i18n/src/locales/tr-TR/accessibility.json create mode 100644 packages/i18n/src/locales/ua/accessibility.json create mode 100644 packages/i18n/src/locales/vi-VN/accessibility.json create mode 100644 packages/i18n/src/locales/zh-CN/accessibility.json create mode 100644 packages/i18n/src/locales/zh-TW/accessibility.json diff --git a/packages/i18n/src/constants/language.ts b/packages/i18n/src/constants/language.ts index d3d3a887a..4969178a5 100644 --- a/packages/i18n/src/constants/language.ts +++ b/packages/i18n/src/constants/language.ts @@ -24,4 +24,13 @@ export const SUPPORTED_LANGUAGES: ILanguageOption[] = [ { label: "Türkçe", value: "tr-TR" }, ]; +/** + * Enum for translation file names + * These are the JSON files that contain translations each category + */ +export enum ETranslationFiles { + TRANSLATIONS = "translations", + ACCESSIBILITY = "accessibility", +} + export const LANGUAGE_STORAGE_KEY = "userLanguage"; diff --git a/packages/i18n/src/locales/cs/accessibility.json b/packages/i18n/src/locales/cs/accessibility.json new file mode 100644 index 000000000..4a715f75b --- /dev/null +++ b/packages/i18n/src/locales/cs/accessibility.json @@ -0,0 +1,27 @@ +{ + "aria_labels": { + "projects_sidebar": { + "workspace_logo": "Logo pracovního prostoru", + "open_workspace_switcher": "Otevřít přepínač pracovního prostoru", + "open_user_menu": "Otevřít uživatelské menu", + "open_command_palette": "Otevřít paletu příkazů", + "open_extended_sidebar": "Otevřít rozšířený postranní panel", + "close_extended_sidebar": "Zavřít rozšířený postranní panel", + "create_favorites_folder": "Vytvořit složku oblíbených", + "open_folder": "Otevřít složku", + "close_folder": "Zavřít složku", + "open_favorites_menu": "Otevřít menu oblíbených", + "close_favorites_menu": "Zavřít menu oblíbených", + "enter_folder_name": "Zadejte název složky", + "create_new_project": "Vytvořit nový projekt", + "open_projects_menu": "Otevřít menu projektů", + "close_projects_menu": "Zavřít menu projektů", + "toggle_quick_actions_menu": "Přepnout menu rychlých akcí", + "open_project_menu": "Otevřít menu projektu", + "close_project_menu": "Zavřít menu projektu", + "collapse_sidebar": "Sbalit postranní panel", + "expand_sidebar": "Rozbalit postranní panel", + "edition_badge": "Otevřít modal placených plánů" + } + } +} \ No newline at end of file diff --git a/packages/i18n/src/locales/de/accessibility.json b/packages/i18n/src/locales/de/accessibility.json new file mode 100644 index 000000000..0faf00916 --- /dev/null +++ b/packages/i18n/src/locales/de/accessibility.json @@ -0,0 +1,27 @@ +{ + "aria_labels": { + "projects_sidebar": { + "workspace_logo": "Arbeitsbereich-Logo", + "open_workspace_switcher": "Arbeitsbereich-Umschalter öffnen", + "open_user_menu": "Benutzermenü öffnen", + "open_command_palette": "Befehlspalette öffnen", + "open_extended_sidebar": "Erweiterte Seitenleiste öffnen", + "close_extended_sidebar": "Erweiterte Seitenleiste schließen", + "create_favorites_folder": "Favoriten-Ordner erstellen", + "open_folder": "Ordner öffnen", + "close_folder": "Ordner schließen", + "open_favorites_menu": "Favoriten-Menü öffnen", + "close_favorites_menu": "Favoriten-Menü schließen", + "enter_folder_name": "Ordnername eingeben", + "create_new_project": "Neues Projekt erstellen", + "open_projects_menu": "Projekt-Menü öffnen", + "close_projects_menu": "Projekt-Menü schließen", + "toggle_quick_actions_menu": "Schnellaktionen-Menü umschalten", + "open_project_menu": "Projekt-Menü öffnen", + "close_project_menu": "Projekt-Menü schließen", + "collapse_sidebar": "Seitenleiste einklappen", + "expand_sidebar": "Seitenleiste ausklappen", + "edition_badge": "Modal für kostenpflichtige Pläne öffnen" + } + } +} \ No newline at end of file diff --git a/packages/i18n/src/locales/en/accessibility.json b/packages/i18n/src/locales/en/accessibility.json new file mode 100644 index 000000000..35759d266 --- /dev/null +++ b/packages/i18n/src/locales/en/accessibility.json @@ -0,0 +1,27 @@ +{ + "aria_labels": { + "projects_sidebar": { + "workspace_logo": "Workspace logo", + "open_workspace_switcher": "Open workspace switcher", + "open_user_menu": "Open user menu", + "open_command_palette": "Open command palette", + "open_extended_sidebar": "Open extended sidebar", + "close_extended_sidebar": "Close extended sidebar", + "create_favorites_folder": "Create favorites folder", + "open_folder": "Open folder", + "close_folder": "Close folder", + "open_favorites_menu": "Open favorites menu", + "close_favorites_menu": "Close favorites menu", + "enter_folder_name": "Enter folder name", + "create_new_project": "Create new project", + "open_projects_menu": "Open projects menu", + "close_projects_menu": "Close projects menu", + "toggle_quick_actions_menu": "Toggle quick actions menu", + "open_project_menu": "Open project menu", + "close_project_menu": "Close project menu", + "collapse_sidebar": "Collapse sidebar", + "expand_sidebar": "Expand sidebar", + "edition_badge": "Open paid plans' modal" + } + } +} \ No newline at end of file diff --git a/packages/i18n/src/locales/en/translations.json b/packages/i18n/src/locales/en/translations.json index c959108e0..ead40fd1e 100644 --- a/packages/i18n/src/locales/en/translations.json +++ b/packages/i18n/src/locales/en/translations.json @@ -2296,4 +2296,4 @@ "previously_edited_by": "Previously edited by", "edited_by": "Edited by" } -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/es/accessibility.json b/packages/i18n/src/locales/es/accessibility.json new file mode 100644 index 000000000..41bf0b777 --- /dev/null +++ b/packages/i18n/src/locales/es/accessibility.json @@ -0,0 +1,27 @@ +{ + "aria_labels": { + "projects_sidebar": { + "workspace_logo": "Logo del espacio de trabajo", + "open_workspace_switcher": "Abrir cambiador de espacio de trabajo", + "open_user_menu": "Abrir menú de usuario", + "open_command_palette": "Abrir paleta de comandos", + "open_extended_sidebar": "Abrir barra lateral extendida", + "close_extended_sidebar": "Cerrar barra lateral extendida", + "create_favorites_folder": "Crear carpeta de favoritos", + "open_folder": "Abrir carpeta", + "close_folder": "Cerrar carpeta", + "open_favorites_menu": "Abrir menú de favoritos", + "close_favorites_menu": "Cerrar menú de favoritos", + "enter_folder_name": "Ingresar nombre de carpeta", + "create_new_project": "Crear nuevo proyecto", + "open_projects_menu": "Abrir menú de proyectos", + "close_projects_menu": "Cerrar menú de proyectos", + "toggle_quick_actions_menu": "Alternar menú de acciones rápidas", + "open_project_menu": "Abrir menú de proyecto", + "close_project_menu": "Cerrar menú de proyecto", + "collapse_sidebar": "Colapsar barra lateral", + "expand_sidebar": "Expandir barra lateral", + "edition_badge": "Abrir modal de planes de pago" + } + } +} \ No newline at end of file diff --git a/packages/i18n/src/locales/fr/accessibility.json b/packages/i18n/src/locales/fr/accessibility.json new file mode 100644 index 000000000..ba42a4f41 --- /dev/null +++ b/packages/i18n/src/locales/fr/accessibility.json @@ -0,0 +1,27 @@ +{ + "aria_labels": { + "projects_sidebar": { + "workspace_logo": "Logo de l'espace de travail", + "open_workspace_switcher": "Ouvrir le sélecteur d'espace de travail", + "open_user_menu": "Ouvrir le menu utilisateur", + "open_command_palette": "Ouvrir la palette de commandes", + "open_extended_sidebar": "Ouvrir la barre latérale étendue", + "close_extended_sidebar": "Fermer la barre latérale étendue", + "create_favorites_folder": "Créer un dossier de favoris", + "open_folder": "Ouvrir le dossier", + "close_folder": "Fermer le dossier", + "open_favorites_menu": "Ouvrir le menu des favoris", + "close_favorites_menu": "Fermer le menu des favoris", + "enter_folder_name": "Saisir le nom du dossier", + "create_new_project": "Créer un nouveau projet", + "open_projects_menu": "Ouvrir le menu des projets", + "close_projects_menu": "Fermer le menu des projets", + "toggle_quick_actions_menu": "Basculer le menu d'actions rapides", + "open_project_menu": "Ouvrir le menu du projet", + "close_project_menu": "Fermer le menu du projet", + "collapse_sidebar": "Réduire la barre latérale", + "expand_sidebar": "Étendre la barre latérale", + "edition_badge": "Ouvrir le modal des plans payants" + } + } +} \ No newline at end of file diff --git a/packages/i18n/src/locales/id/accessibility.json b/packages/i18n/src/locales/id/accessibility.json new file mode 100644 index 000000000..2aca032cc --- /dev/null +++ b/packages/i18n/src/locales/id/accessibility.json @@ -0,0 +1,27 @@ +{ + "aria_labels": { + "projects_sidebar": { + "workspace_logo": "Logo ruang kerja", + "open_workspace_switcher": "Buka penukar ruang kerja", + "open_user_menu": "Buka menu pengguna", + "open_command_palette": "Buka palet perintah", + "open_extended_sidebar": "Buka sidebar diperluas", + "close_extended_sidebar": "Tutup sidebar diperluas", + "create_favorites_folder": "Buat folder favorit", + "open_folder": "Buka folder", + "close_folder": "Tutup folder", + "open_favorites_menu": "Buka menu favorit", + "close_favorites_menu": "Tutup menu favorit", + "enter_folder_name": "Masukkan nama folder", + "create_new_project": "Buat proyek baru", + "open_projects_menu": "Buka menu proyek", + "close_projects_menu": "Tutup menu proyek", + "toggle_quick_actions_menu": "Alihkan menu tindakan cepat", + "open_project_menu": "Buka menu proyek", + "close_project_menu": "Tutup menu proyek", + "collapse_sidebar": "Tutup sidebar", + "expand_sidebar": "Perluas sidebar", + "edition_badge": "Buka modal paket berbayar" + } + } +} \ No newline at end of file diff --git a/packages/i18n/src/locales/it/accessibility.json b/packages/i18n/src/locales/it/accessibility.json new file mode 100644 index 000000000..8f22d3b8e --- /dev/null +++ b/packages/i18n/src/locales/it/accessibility.json @@ -0,0 +1,27 @@ +{ + "aria_labels": { + "projects_sidebar": { + "workspace_logo": "Logo dell'area di lavoro", + "open_workspace_switcher": "Apri selettore area di lavoro", + "open_user_menu": "Apri menu utente", + "open_command_palette": "Apri tavolozza comandi", + "open_extended_sidebar": "Apri barra laterale estesa", + "close_extended_sidebar": "Chiudi barra laterale estesa", + "create_favorites_folder": "Crea cartella preferiti", + "open_folder": "Apri cartella", + "close_folder": "Chiudi cartella", + "open_favorites_menu": "Apri menu preferiti", + "close_favorites_menu": "Chiudi menu preferiti", + "enter_folder_name": "Inserisci nome cartella", + "create_new_project": "Crea nuovo progetto", + "open_projects_menu": "Apri menu progetti", + "close_projects_menu": "Chiudi menu progetti", + "toggle_quick_actions_menu": "Attiva/disattiva menu azioni rapide", + "open_project_menu": "Apri menu progetto", + "close_project_menu": "Chiudi menu progetto", + "collapse_sidebar": "Comprimi barra laterale", + "expand_sidebar": "Espandi barra laterale", + "edition_badge": "Apri modal piani a pagamento" + } + } +} \ No newline at end of file diff --git a/packages/i18n/src/locales/ja/accessibility.json b/packages/i18n/src/locales/ja/accessibility.json new file mode 100644 index 000000000..a598c435a --- /dev/null +++ b/packages/i18n/src/locales/ja/accessibility.json @@ -0,0 +1,27 @@ +{ + "aria_labels": { + "projects_sidebar": { + "workspace_logo": "ワークスペースロゴ", + "open_workspace_switcher": "ワークスペーススイッチャーを開く", + "open_user_menu": "ユーザーメニューを開く", + "open_command_palette": "コマンドパレットを開く", + "open_extended_sidebar": "拡張サイドバーを開く", + "close_extended_sidebar": "拡張サイドバーを閉じる", + "create_favorites_folder": "お気に入りフォルダを作成", + "open_folder": "フォルダを開く", + "close_folder": "フォルダを閉じる", + "open_favorites_menu": "お気に入りメニューを開く", + "close_favorites_menu": "お気に入りメニューを閉じる", + "enter_folder_name": "フォルダ名を入力", + "create_new_project": "新しいプロジェクトを作成", + "open_projects_menu": "プロジェクトメニューを開く", + "close_projects_menu": "プロジェクトメニューを閉じる", + "toggle_quick_actions_menu": "クイックアクションメニューの切り替え", + "open_project_menu": "プロジェクトメニューを開く", + "close_project_menu": "プロジェクトメニューを閉じる", + "collapse_sidebar": "サイドバーを折りたたむ", + "expand_sidebar": "サイドバーを展開", + "edition_badge": "有料プランのモーダルを開く" + } + } +} \ No newline at end of file diff --git a/packages/i18n/src/locales/ko/accessibility.json b/packages/i18n/src/locales/ko/accessibility.json new file mode 100644 index 000000000..491b8c35c --- /dev/null +++ b/packages/i18n/src/locales/ko/accessibility.json @@ -0,0 +1,27 @@ +{ + "aria_labels": { + "projects_sidebar": { + "workspace_logo": "워크스페이스 로고", + "open_workspace_switcher": "워크스페이스 전환기 열기", + "open_user_menu": "사용자 메뉴 열기", + "open_command_palette": "명령 팔레트 열기", + "open_extended_sidebar": "확장된 사이드바 열기", + "close_extended_sidebar": "확장된 사이드바 닫기", + "create_favorites_folder": "즐겨찾기 폴더 생성", + "open_folder": "폴더 열기", + "close_folder": "폴더 닫기", + "open_favorites_menu": "즐겨찾기 메뉴 열기", + "close_favorites_menu": "즐겨찾기 메뉴 닫기", + "enter_folder_name": "폴더 이름 입력", + "create_new_project": "새 프로젝트 생성", + "open_projects_menu": "프로젝트 메뉴 열기", + "close_projects_menu": "프로젝트 메뉴 닫기", + "toggle_quick_actions_menu": "빠른 작업 메뉴 토글", + "open_project_menu": "프로젝트 메뉴 열기", + "close_project_menu": "프로젝트 메뉴 닫기", + "collapse_sidebar": "사이드바 축소", + "expand_sidebar": "사이드바 확장", + "edition_badge": "유료 플랜 모달 열기" + } + } +} \ No newline at end of file diff --git a/packages/i18n/src/locales/pl/accessibility.json b/packages/i18n/src/locales/pl/accessibility.json new file mode 100644 index 000000000..5ff936d47 --- /dev/null +++ b/packages/i18n/src/locales/pl/accessibility.json @@ -0,0 +1,27 @@ +{ + "aria_labels": { + "projects_sidebar": { + "workspace_logo": "Logo obszaru roboczego", + "open_workspace_switcher": "Otwórz przełącznik obszaru roboczego", + "open_user_menu": "Otwórz menu użytkownika", + "open_command_palette": "Otwórz paletę poleceń", + "open_extended_sidebar": "Otwórz rozszerzoną pasek boczny", + "close_extended_sidebar": "Zamknij rozszerzoną pasek boczny", + "create_favorites_folder": "Utwórz folder ulubionych", + "open_folder": "Otwórz folder", + "close_folder": "Zamknij folder", + "open_favorites_menu": "Otwórz menu ulubionych", + "close_favorites_menu": "Zamknij menu ulubionych", + "enter_folder_name": "Wprowadź nazwę folderu", + "create_new_project": "Utwórz nowy projekt", + "open_projects_menu": "Otwórz menu projektów", + "close_projects_menu": "Zamknij menu projektów", + "toggle_quick_actions_menu": "Przełącz menu szybkich akcji", + "open_project_menu": "Otwórz menu projektu", + "close_project_menu": "Zamknij menu projektu", + "collapse_sidebar": "Zwiń pasek boczny", + "expand_sidebar": "Rozwiń pasek boczny", + "edition_badge": "Otwórz modal płatnych planów" + } + } +} \ No newline at end of file diff --git a/packages/i18n/src/locales/pt-BR/accessibility.json b/packages/i18n/src/locales/pt-BR/accessibility.json new file mode 100644 index 000000000..333b55a7f --- /dev/null +++ b/packages/i18n/src/locales/pt-BR/accessibility.json @@ -0,0 +1,27 @@ +{ + "aria_labels": { + "projects_sidebar": { + "workspace_logo": "Logo do espaço de trabalho", + "open_workspace_switcher": "Abrir seletor de espaço de trabalho", + "open_user_menu": "Abrir menu do usuário", + "open_command_palette": "Abrir paleta de comandos", + "open_extended_sidebar": "Abrir barra lateral estendida", + "close_extended_sidebar": "Fechar barra lateral estendida", + "create_favorites_folder": "Criar pasta de favoritos", + "open_folder": "Abrir pasta", + "close_folder": "Fechar pasta", + "open_favorites_menu": "Abrir menu de favoritos", + "close_favorites_menu": "Fechar menu de favoritos", + "enter_folder_name": "Digite o nome da pasta", + "create_new_project": "Criar novo projeto", + "open_projects_menu": "Abrir menu de projetos", + "close_projects_menu": "Fechar menu de projetos", + "toggle_quick_actions_menu": "Alternar menu de ações rápidas", + "open_project_menu": "Abrir menu do projeto", + "close_project_menu": "Fechar menu do projeto", + "collapse_sidebar": "Recolher barra lateral", + "expand_sidebar": "Expandir barra lateral", + "edition_badge": "Abrir modal de planos pagos" + } + } +} \ No newline at end of file diff --git a/packages/i18n/src/locales/ro/accessibility.json b/packages/i18n/src/locales/ro/accessibility.json new file mode 100644 index 000000000..1a201a48c --- /dev/null +++ b/packages/i18n/src/locales/ro/accessibility.json @@ -0,0 +1,27 @@ +{ + "aria_labels": { + "projects_sidebar": { + "workspace_logo": "Logo spațiu de lucru", + "open_workspace_switcher": "Deschide comutator spațiu de lucru", + "open_user_menu": "Deschide meniul utilizatorului", + "open_command_palette": "Deschide paleta de comenzi", + "open_extended_sidebar": "Deschide bara laterală extinsă", + "close_extended_sidebar": "Închide bara laterală extinsă", + "create_favorites_folder": "Creează folder de favorite", + "open_folder": "Deschide folderul", + "close_folder": "Închide folderul", + "open_favorites_menu": "Deschide meniul de favorite", + "close_favorites_menu": "Închide meniul de favorite", + "enter_folder_name": "Introduceți numele folderului", + "create_new_project": "Creează proiect nou", + "open_projects_menu": "Deschide meniul de proiecte", + "close_projects_menu": "Închide meniul de proiecte", + "toggle_quick_actions_menu": "Comută meniul de acțiuni rapide", + "open_project_menu": "Deschide meniul proiectului", + "close_project_menu": "Închide meniul proiectului", + "collapse_sidebar": "Restrânge bara laterală", + "expand_sidebar": "Extinde bara laterală", + "edition_badge": "Deschide modalul planurilor plătite" + } + } +} \ No newline at end of file diff --git a/packages/i18n/src/locales/ru/accessibility.json b/packages/i18n/src/locales/ru/accessibility.json new file mode 100644 index 000000000..ebec8dc2f --- /dev/null +++ b/packages/i18n/src/locales/ru/accessibility.json @@ -0,0 +1,27 @@ +{ + "aria_labels": { + "projects_sidebar": { + "workspace_logo": "Логотип рабочей области", + "open_workspace_switcher": "Открыть переключатель рабочей области", + "open_user_menu": "Открыть пользовательское меню", + "open_command_palette": "Открыть палитру команд", + "open_extended_sidebar": "Открыть расширенную боковую панель", + "close_extended_sidebar": "Закрыть расширенную боковую панель", + "create_favorites_folder": "Создать папку избранного", + "open_folder": "Открыть папку", + "close_folder": "Закрыть папку", + "open_favorites_menu": "Открыть меню избранного", + "close_favorites_menu": "Закрыть меню избранного", + "enter_folder_name": "Введите имя папки", + "create_new_project": "Создать новый проект", + "open_projects_menu": "Открыть меню проектов", + "close_projects_menu": "Закрыть меню проектов", + "toggle_quick_actions_menu": "Переключить меню быстрых действий", + "open_project_menu": "Открыть меню проекта", + "close_project_menu": "Закрыть меню проекта", + "collapse_sidebar": "Свернуть боковую панель", + "expand_sidebar": "Развернуть боковую панель", + "edition_badge": "Открыть модал платных планов" + } + } +} \ No newline at end of file diff --git a/packages/i18n/src/locales/sk/accessibility.json b/packages/i18n/src/locales/sk/accessibility.json new file mode 100644 index 000000000..59a309f60 --- /dev/null +++ b/packages/i18n/src/locales/sk/accessibility.json @@ -0,0 +1,27 @@ +{ + "aria_labels": { + "projects_sidebar": { + "workspace_logo": "Logo pracovného priestoru", + "open_workspace_switcher": "Otvoriť prepínač pracovného priestoru", + "open_user_menu": "Otvoriť používateľské menu", + "open_command_palette": "Otvoriť paletu príkazov", + "open_extended_sidebar": "Otvoriť rozšírený bočný panel", + "close_extended_sidebar": "Zavrieť rozšírený bočný panel", + "create_favorites_folder": "Vytvoriť priečinok obľúbených", + "open_folder": "Otvoriť priečinok", + "close_folder": "Zavrieť priečinok", + "open_favorites_menu": "Otvoriť menu obľúbených", + "close_favorites_menu": "Zavrieť menu obľúbených", + "enter_folder_name": "Zadajte názov priečinka", + "create_new_project": "Vytvoriť nový projekt", + "open_projects_menu": "Otvoriť menu projektov", + "close_projects_menu": "Zavrieť menu projektov", + "toggle_quick_actions_menu": "Prepnúť menu rýchlych akcií", + "open_project_menu": "Otvoriť menu projektu", + "close_project_menu": "Zavrieť menu projektu", + "collapse_sidebar": "Zbaliť bočný panel", + "expand_sidebar": "Rozbaliť bočný panel", + "edition_badge": "Otvoriť modal platených plánov" + } + } +} \ No newline at end of file diff --git a/packages/i18n/src/locales/tr-TR/accessibility.json b/packages/i18n/src/locales/tr-TR/accessibility.json new file mode 100644 index 000000000..35b8f340e --- /dev/null +++ b/packages/i18n/src/locales/tr-TR/accessibility.json @@ -0,0 +1,27 @@ +{ + "aria_labels": { + "projects_sidebar": { + "workspace_logo": "Çalışma alanı logosu", + "open_workspace_switcher": "Çalışma alanı değiştiricisini aç", + "open_user_menu": "Kullanıcı menüsünü aç", + "open_command_palette": "Komut paletini aç", + "open_extended_sidebar": "Genişletilmiş kenar çubuğunu aç", + "close_extended_sidebar": "Genişletilmiş kenar çubuğunu kapat", + "create_favorites_folder": "Favoriler klasörü oluştur", + "open_folder": "Klasörü aç", + "close_folder": "Klasörü kapat", + "open_favorites_menu": "Favoriler menüsünü aç", + "close_favorites_menu": "Favoriler menüsünü kapat", + "enter_folder_name": "Klasör adını girin", + "create_new_project": "Yeni proje oluştur", + "open_projects_menu": "Projeler menüsünü aç", + "close_projects_menu": "Projeler menüsünü kapat", + "toggle_quick_actions_menu": "Hızlı eylemler menüsünü aç/kapat", + "open_project_menu": "Proje menüsünü aç", + "close_project_menu": "Proje menüsünü kapat", + "collapse_sidebar": "Kenar çubuğunu daralt", + "expand_sidebar": "Kenar çubuğunu genişlet", + "edition_badge": "Ücretli planlar modalını aç" + } + } +} \ No newline at end of file diff --git a/packages/i18n/src/locales/ua/accessibility.json b/packages/i18n/src/locales/ua/accessibility.json new file mode 100644 index 000000000..b6bdc7d52 --- /dev/null +++ b/packages/i18n/src/locales/ua/accessibility.json @@ -0,0 +1,27 @@ +{ + "aria_labels": { + "projects_sidebar": { + "workspace_logo": "Логотип робочого простору", + "open_workspace_switcher": "Відкрити перемикач робочого простору", + "open_user_menu": "Відкрити меню користувача", + "open_command_palette": "Відкрити палітру команд", + "open_extended_sidebar": "Відкрити розширену бічну панель", + "close_extended_sidebar": "Закрити розширену бічну панель", + "create_favorites_folder": "Створити папку улюблених", + "open_folder": "Відкрити папку", + "close_folder": "Закрити папку", + "open_favorites_menu": "Відкрити меню улюблених", + "close_favorites_menu": "Закрити меню улюблених", + "enter_folder_name": "Введіть назву папки", + "create_new_project": "Створити новий проект", + "open_projects_menu": "Відкрити меню проектів", + "close_projects_menu": "Закрити меню проектів", + "toggle_quick_actions_menu": "Перемкнути меню швидких дій", + "open_project_menu": "Відкрити меню проекту", + "close_project_menu": "Закрити меню проекту", + "collapse_sidebar": "Згорнути бічну панель", + "expand_sidebar": "Розгорнути бічну панель", + "edition_badge": "Відкрити модал платних планів" + } + } +} \ No newline at end of file diff --git a/packages/i18n/src/locales/vi-VN/accessibility.json b/packages/i18n/src/locales/vi-VN/accessibility.json new file mode 100644 index 000000000..8071da9e3 --- /dev/null +++ b/packages/i18n/src/locales/vi-VN/accessibility.json @@ -0,0 +1,27 @@ +{ + "aria_labels": { + "projects_sidebar": { + "workspace_logo": "Logo không gian làm việc", + "open_workspace_switcher": "Mở trình chuyển đổi không gian làm việc", + "open_user_menu": "Mở menu người dùng", + "open_command_palette": "Mở bảng lệnh", + "open_extended_sidebar": "Mở thanh bên mở rộng", + "close_extended_sidebar": "Đóng thanh bên mở rộng", + "create_favorites_folder": "Tạo thư mục yêu thích", + "open_folder": "Mở thư mục", + "close_folder": "Đóng thư mục", + "open_favorites_menu": "Mở menu yêu thích", + "close_favorites_menu": "Đóng menu yêu thích", + "enter_folder_name": "Nhập tên thư mục", + "create_new_project": "Tạo dự án mới", + "open_projects_menu": "Mở menu dự án", + "close_projects_menu": "Đóng menu dự án", + "toggle_quick_actions_menu": "Bật/tắt menu hành động nhanh", + "open_project_menu": "Mở menu dự án", + "close_project_menu": "Đóng menu dự án", + "collapse_sidebar": "Thu gọn thanh bên", + "expand_sidebar": "Mở rộng thanh bên", + "edition_badge": "Mở modal gói trả phí" + } + } +} \ No newline at end of file diff --git a/packages/i18n/src/locales/zh-CN/accessibility.json b/packages/i18n/src/locales/zh-CN/accessibility.json new file mode 100644 index 000000000..b19f68676 --- /dev/null +++ b/packages/i18n/src/locales/zh-CN/accessibility.json @@ -0,0 +1,27 @@ +{ + "aria_labels": { + "projects_sidebar": { + "workspace_logo": "工作空间徽标", + "open_workspace_switcher": "打开工作空间切换器", + "open_user_menu": "打开用户菜单", + "open_command_palette": "打开命令面板", + "open_extended_sidebar": "打开扩展侧边栏", + "close_extended_sidebar": "关闭扩展侧边栏", + "create_favorites_folder": "创建收藏夹文件夹", + "open_folder": "打开文件夹", + "close_folder": "关闭文件夹", + "open_favorites_menu": "打开收藏夹菜单", + "close_favorites_menu": "关闭收藏夹菜单", + "enter_folder_name": "输入文件夹名称", + "create_new_project": "创建新项目", + "open_projects_menu": "打开项目菜单", + "close_projects_menu": "关闭项目菜单", + "toggle_quick_actions_menu": "切换快速操作菜单", + "open_project_menu": "打开项目菜单", + "close_project_menu": "关闭项目菜单", + "collapse_sidebar": "折叠侧边栏", + "expand_sidebar": "展开侧边栏", + "edition_badge": "打开付费计划模态框" + } + } +} \ No newline at end of file diff --git a/packages/i18n/src/locales/zh-TW/accessibility.json b/packages/i18n/src/locales/zh-TW/accessibility.json new file mode 100644 index 000000000..97e07ae73 --- /dev/null +++ b/packages/i18n/src/locales/zh-TW/accessibility.json @@ -0,0 +1,27 @@ +{ + "aria_labels": { + "projects_sidebar": { + "workspace_logo": "工作空間標誌", + "open_workspace_switcher": "打開工作空間切換器", + "open_user_menu": "打開用戶選單", + "open_command_palette": "打開命令面板", + "open_extended_sidebar": "打開擴展側邊欄", + "close_extended_sidebar": "關閉擴展側邊欄", + "create_favorites_folder": "創建收藏夾文件夾", + "open_folder": "打開文件夾", + "close_folder": "關閉文件夾", + "open_favorites_menu": "打開收藏夾選單", + "close_favorites_menu": "關閉收藏夾選單", + "enter_folder_name": "輸入文件夾名稱", + "create_new_project": "創建新項目", + "open_projects_menu": "打開項目選單", + "close_projects_menu": "關閉項目選單", + "toggle_quick_actions_menu": "切換快速操作選單", + "open_project_menu": "打開項目選單", + "close_project_menu": "關閉項目選單", + "collapse_sidebar": "摺疊側邊欄", + "expand_sidebar": "展開側邊欄", + "edition_badge": "打開付費計劃模態框" + } + } +} \ No newline at end of file diff --git a/packages/i18n/src/store/index.ts b/packages/i18n/src/store/index.ts index ff4cee107..c75d7b8a3 100644 --- a/packages/i18n/src/store/index.ts +++ b/packages/i18n/src/store/index.ts @@ -3,7 +3,7 @@ import get from "lodash/get"; import merge from "lodash/merge"; import { makeAutoObservable, runInAction } from "mobx"; // constants -import { FALLBACK_LANGUAGE, SUPPORTED_LANGUAGES, LANGUAGE_STORAGE_KEY } from "../constants"; +import { FALLBACK_LANGUAGE, SUPPORTED_LANGUAGES, LANGUAGE_STORAGE_KEY, ETranslationFiles } from "../constants"; // core translations imports import coreEn from "../locales/en/core.json"; // types @@ -130,54 +130,32 @@ export class TranslationStore { } } + /** + * Helper function to import and merge multiple translation files for a language + * @param language - The language code + * @param files - Array of file names to import (without .json extension) + * @returns Promise that resolves to merged translations + */ + private async importAndMergeFiles(language: TLanguage, files: string[]): Promise { + try { + const importPromises = files.map((file) => import(`../locales/${language}/${file}.json`)); + + const modules = await Promise.all(importPromises); + const merged = modules.reduce((acc, module) => merge(acc, module.default), {}); + return { default: merged }; + } catch (error) { + throw new Error(`Failed to import and merge files for ${language}: ${error}`); + } + } + /** * Imports the translations for the given language * @param language - The language to import the translations for * @returns {Promise} */ - private importLanguageFile(language: TLanguage): Promise { - switch (language) { - case "en": - return import("../locales/en/translations.json"); - case "fr": - return import("../locales/fr/translations.json"); - case "es": - return import("../locales/es/translations.json"); - case "ja": - return import("../locales/ja/translations.json"); - case "zh-CN": - return import("../locales/zh-CN/translations.json"); - case "zh-TW": - return import("../locales/zh-TW/translations.json"); - case "ru": - return import("../locales/ru/translations.json"); - case "it": - return import("../locales/it/translations.json"); - case "cs": - return import("../locales/cs/translations.json"); - case "sk": - return import("../locales/sk/translations.json"); - case "de": - return import("../locales/de/translations.json"); - case "ua": - return import("../locales/ua/translations.json"); - case "pl": - return import("../locales/pl/translations.json"); - case "ko": - return import("../locales/ko/translations.json"); - case "pt-BR": - return import("../locales/pt-BR/translations.json"); - case "id": - return import("../locales/id/translations.json"); - case "ro": - return import("../locales/ro/translations.json"); - case "vi-VN": - return import("../locales/vi-VN/translations.json"); - case "tr-TR": - return import("../locales/tr-TR/translations.json"); - default: - throw new Error(`Unsupported language: ${language}`); - } + private async importLanguageFile(language: TLanguage): Promise { + const files = Object.values(ETranslationFiles); + return this.importAndMergeFiles(language, files); } /** Checks if the language is valid based on the supported languages */ diff --git a/packages/ui/src/avatar/avatar.tsx b/packages/ui/src/avatar/avatar.tsx index 0c57cceba..84a8ab895 100644 --- a/packages/ui/src/avatar/avatar.tsx +++ b/packages/ui/src/avatar/avatar.tsx @@ -160,7 +160,7 @@ export const Avatar: React.FC = (props) => { color: fallbackTextColor ?? "#ffffff", }} > - {name ? name[0].toUpperCase() : fallbackText ?? "?"} + {name?.[0]?.toUpperCase() ?? fallbackText ?? "?"}
)}
diff --git a/packages/ui/src/dropdowns/custom-menu.tsx b/packages/ui/src/dropdowns/custom-menu.tsx index 24c8a106a..688f14897 100644 --- a/packages/ui/src/dropdowns/custom-menu.tsx +++ b/packages/ui/src/dropdowns/custom-menu.tsx @@ -14,6 +14,7 @@ import { ICustomMenuDropdownProps, ICustomMenuItemProps } from "./helper"; const CustomMenu = (props: ICustomMenuDropdownProps) => { const { + ariaLabel, buttonClassName = "", customButtonClassName = "", customButtonTabIndex = 0, @@ -75,7 +76,7 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => { e.stopPropagation(); e.preventDefault(); isOpen ? closeDropdown() : openDropdown(); - if (menuButtonOnClick) menuButtonOnClick(); + menuButtonOnClick?.(); }; const handleMouseEnter = () => { @@ -147,6 +148,7 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => { className={customButtonClassName} tabIndex={customButtonTabIndex} disabled={disabled} + aria-label={ariaLabel} > {customButton} @@ -164,6 +166,7 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => { disabled ? "cursor-not-allowed" : "cursor-pointer hover:bg-custom-background-80" } ${buttonClassName}`} tabIndex={customButtonTabIndex} + aria-label={ariaLabel} > @@ -183,6 +186,7 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => { onClick={handleMenuButtonClick} tabIndex={customButtonTabIndex} disabled={disabled} + aria-label={ariaLabel} > {label} {!noChevron && } diff --git a/packages/ui/src/dropdowns/helper.tsx b/packages/ui/src/dropdowns/helper.tsx index 0e7587051..1d40acef7 100644 --- a/packages/ui/src/dropdowns/helper.tsx +++ b/packages/ui/src/dropdowns/helper.tsx @@ -32,6 +32,7 @@ export interface ICustomMenuDropdownProps extends IDropdownProps { closeOnSelect?: boolean; portalElement?: Element | null; openOnHover?: boolean; + ariaLabel?: string; } export interface ICustomSelectProps extends IDropdownProps { diff --git a/web/app/(all)/[workspaceSlug]/(projects)/extended-sidebar.tsx b/web/app/(all)/[workspaceSlug]/(projects)/extended-sidebar.tsx index e0003eeba..baa41eb9f 100644 --- a/web/app/(all)/[workspaceSlug]/(projects)/extended-sidebar.tsx +++ b/web/app/(all)/[workspaceSlug]/(projects)/extended-sidebar.tsx @@ -96,7 +96,7 @@ export const ExtendedAppSidebar = observer(() => { useExtendedSidebarOutsideClickDetector( extendedSidebarRef, - () => toggleExtendedSidebar(false), + () => toggleExtendedSidebar(true), "extended-sidebar-toggle" ); @@ -106,8 +106,8 @@ export const ExtendedAppSidebar = observer(() => { className={cn( "absolute top-0 h-full z-[19] flex flex-col gap-0.5 w-[300px] transform transition-all duration-300 ease-in-out bg-custom-sidebar-background-100 border-r border-custom-sidebar-border-200 p-4 shadow-md pb-6", { - "translate-x-0 opacity-100 pointer-events-auto": extendedSidebarCollapsed, - "-translate-x-full opacity-0 pointer-events-none": !extendedSidebarCollapsed, + "-translate-x-full opacity-0 pointer-events-none": extendedSidebarCollapsed, + "translate-x-0 opacity-100 pointer-events-auto": !extendedSidebarCollapsed, "left-[70px]": sidebarCollapsed, "left-[250px]": !sidebarCollapsed, } diff --git a/web/ce/components/workspace/edition-badge.tsx b/web/ce/components/workspace/edition-badge.tsx index b32ce9e61..e0fefc356 100644 --- a/web/ce/components/workspace/edition-badge.tsx +++ b/web/ce/components/workspace/edition-badge.tsx @@ -1,7 +1,8 @@ import { useState } from "react"; import { observer } from "mobx-react"; import packageJson from "package.json"; -// ui +// plane imports +import { useTranslation } from "@plane/i18n"; import { Button, Tooltip } from "@plane/ui"; // hooks import { usePlatformOS } from "@/hooks/use-platform-os"; @@ -9,9 +10,12 @@ import { usePlatformOS } from "@/hooks/use-platform-os"; import { PaidPlanUpgradeModal } from "../license"; export const WorkspaceEditionBadge = observer(() => { - const { isMobile } = usePlatformOS(); // states const [isPaidPlanPurchaseModalOpen, setIsPaidPlanPurchaseModalOpen] = useState(false); + // translation + const { t } = useTranslation(); + // platform + const { isMobile } = usePlatformOS(); return ( <> @@ -25,6 +29,8 @@ export const WorkspaceEditionBadge = observer(() => { variant="accent-primary" className="w-fit min-w-24 cursor-pointer rounded-2xl px-2 py-1 text-center text-sm font-medium outline-none" onClick={() => setIsPaidPlanPurchaseModalOpen(true)} + aria-haspopup="dialog" + aria-label={t("aria_labels.projects_sidebar.edition_badge")} > Community diff --git a/web/ce/components/workspace/sidebar/app-search.tsx b/web/ce/components/workspace/sidebar/app-search.tsx index 6ab92b996..1ba0ea72c 100644 --- a/web/ce/components/workspace/sidebar/app-search.tsx +++ b/web/ce/components/workspace/sidebar/app-search.tsx @@ -1,5 +1,7 @@ import { observer } from "mobx-react"; import { Search } from "lucide-react"; +// plane imports +import { useTranslation } from "@plane/i18n"; // helpers import { cn } from "@/helpers/common.helper"; // hooks @@ -9,9 +11,12 @@ export const AppSearch = observer(() => { // store hooks const { sidebarCollapsed } = useAppTheme(); const { toggleCommandPaletteModal } = useCommandPalette(); + // translation + const { t } = useTranslation(); return ( diff --git a/web/ce/components/workspace/sidebar/extended-sidebar-item.tsx b/web/ce/components/workspace/sidebar/extended-sidebar-item.tsx index 5bb2fe885..5e7343e0a 100644 --- a/web/ce/components/workspace/sidebar/extended-sidebar-item.tsx +++ b/web/ce/components/workspace/sidebar/extended-sidebar-item.tsx @@ -55,7 +55,7 @@ export const ExtendedSidebarItem: FC = observer((prop const sidebarPreference = getNavigationPreferences(workspaceSlug.toString()); const isPinned = sidebarPreference?.[item.key]?.is_pinned; - const handleLinkClick = () => toggleExtendedSidebar(); + const handleLinkClick = () => toggleExtendedSidebar(true); if (!allowPermissions(item.access as any, EUserPermissionsLevel.WORKSPACE, workspaceSlug.toString())) { return null; diff --git a/web/ce/components/workspace/sidebar/sidebar-item.tsx b/web/ce/components/workspace/sidebar/sidebar-item.tsx index 51a5735de..3645bde3d 100644 --- a/web/ce/components/workspace/sidebar/sidebar-item.tsx +++ b/web/ce/components/workspace/sidebar/sidebar-item.tsx @@ -38,7 +38,7 @@ export const SidebarItem: FC = observer((props) => { if (window.innerWidth < 768) { toggleSidebar(); } - if (extendedSidebarCollapsed) toggleExtendedSidebar(); + if (!extendedSidebarCollapsed) toggleExtendedSidebar(); }; const staticItems = ["home", "inbox", "pi-chat", "projects"]; diff --git a/web/ce/constants/sidebar-favorites.ts b/web/ce/constants/sidebar-favorites.ts index 9fa80e05f..a6f49f8aa 100644 --- a/web/ce/constants/sidebar-favorites.ts +++ b/web/ce/constants/sidebar-favorites.ts @@ -1,7 +1,7 @@ -import { Briefcase, ContrastIcon, FileText, Layers, LucideIcon } from "lucide-react"; +import { Briefcase, FileText, Layers, LucideIcon } from "lucide-react"; // plane imports import { IFavorite } from "@plane/types"; -import { DiceIcon, FavoriteFolderIcon, ISvgIcons } from "@plane/ui"; +import { ContrastIcon, DiceIcon, FavoriteFolderIcon, ISvgIcons } from "@plane/ui"; export const FAVORITE_ITEM_ICONS: Record | LucideIcon> = { page: FileText, diff --git a/web/core/components/workspace/logo.tsx b/web/core/components/workspace/logo.tsx index 460e076c5..f25615dfc 100644 --- a/web/core/components/workspace/logo.tsx +++ b/web/core/components/workspace/logo.tsx @@ -1,4 +1,6 @@ -// plane utils +import { observer } from "mobx-react"; +// plane imports +import { useTranslation } from "@plane/i18n"; import { cn } from "@plane/utils"; // helpers import { getFileURL } from "@/helpers/file.helper"; @@ -9,22 +11,27 @@ type Props = { classNames?: string; }; -export const WorkspaceLogo = (props: Props) => ( -
- {props.logo && props.logo !== "" ? ( - Workspace Logo - ) : ( - (props.name?.charAt(0) ?? "...") - )} -
-); +export const WorkspaceLogo = observer((props: Props) => { + // translation + const { t } = useTranslation(); + + return ( +
+ {props.logo && props.logo !== "" ? ( + {t("aria_labels.projects_sidebar.workspace_logo")} + ) : ( + (props.name?.[0] ?? "...") + )} +
+ ); +}); diff --git a/web/core/components/workspace/sidebar/dropdown.tsx b/web/core/components/workspace/sidebar/dropdown.tsx index f9e2aca59..b4e6b93d7 100644 --- a/web/core/components/workspace/sidebar/dropdown.tsx +++ b/web/core/components/workspace/sidebar/dropdown.tsx @@ -25,21 +25,17 @@ import { WorkspaceLogo } from "../logo"; import SidebarDropdownItem from "./dropdown-item"; export const SidebarDropdown = observer(() => { - const { t } = useTranslation(); - // store hooks const { sidebarCollapsed, toggleSidebar } = useAppTheme(); const { data: currentUser } = useUser(); - const { - // updateCurrentUser, - // isUserInstanceAdmin, - signOut, - } = useUser(); + const { signOut } = useUser(); const { updateUserProfile } = useUserProfile(); - const isWorkspaceCreationEnabled = getIsWorkspaceCreationDisabled() === false; - - const isUserInstanceAdmin = false; const { currentWorkspace: activeWorkspace, workspaces } = useWorkspace(); + // derived values + const isWorkspaceCreationEnabled = getIsWorkspaceCreationDisabled() === false; + const isUserInstanceAdmin = false; + // translation + const { t } = useTranslation(); // popper-js refs const [referenceElement, setReferenceElement] = useState(null); const [popperElement, setPopperElement] = useState(null); @@ -87,6 +83,7 @@ export const SidebarDropdown = observer(() => { "group/menu-button flex items-center justify-between gap-1 p-1 truncate rounded text-sm font-medium text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80 focus:outline-none", { "flex-grow": !sidebarCollapsed } )} + aria-label={t("aria_labels.projects_sidebar.open_workspace_switcher")} >
@@ -190,7 +187,11 @@ export const SidebarDropdown = observer(() => { )} - + = (props) => { const [isDragging, setIsDragging] = useState(false); const [folderToRename, setFolderToRename] = useState(null); const [instruction, setInstruction] = useState(undefined); - // refs const actionSectionRef = useRef(null); const elementRef = useRef(null); + // translation + const { t } = useTranslation(); useEffect(() => { if (favorite.children === undefined && workspaceSlug) { @@ -231,11 +231,11 @@ export const FavoriteFolder: React.FC = (props) => { setIsMenuActive(!isMenuActive)} > - + } + menuButtonOnClick={() => setIsMenuActive(!isMenuActive)} className={cn( "opacity-0 pointer-events-none flex-shrink-0 group-hover/project-item:opacity-100 group-hover/project-item:pointer-events-auto", { @@ -244,6 +244,7 @@ export const FavoriteFolder: React.FC = (props) => { )} customButtonClassName="grid place-items-center" placement="bottom-start" + ariaLabel={t("aria_labels.projects_sidebar.toggle_quick_actions_menu")} > handleRemoveFromFavorites(favorite)}> @@ -267,9 +268,12 @@ export const FavoriteFolder: React.FC = (props) => { "inline-block": isMenuActive, } )} + aria-label={t( + open ? "aria_labels.projects_sidebar.close_folder" : "aria_labels.projects_sidebar.open_folder" + )} > diff --git a/web/core/components/workspace/sidebar/favorites/favorite-items/common/favorite-item-quick-action.tsx b/web/core/components/workspace/sidebar/favorites/favorite-items/common/favorite-item-quick-action.tsx index eadaedb34..cf6436733 100644 --- a/web/core/components/workspace/sidebar/favorites/favorite-items/common/favorite-item-quick-action.tsx +++ b/web/core/components/workspace/sidebar/favorites/favorite-items/common/favorite-item-quick-action.tsx @@ -1,8 +1,10 @@ "use client"; import React, { FC } from "react"; +import { observer } from "mobx-react"; import { MoreHorizontal, Star } from "lucide-react"; +// plane imports +import { useTranslation } from "@plane/i18n"; import { IFavorite } from "@plane/types"; -// ui import { CustomMenu } from "@plane/ui"; // helpers import { cn } from "@/helpers/common.helper"; @@ -15,19 +17,22 @@ type Props = { handleRemoveFromFavorites: (favorite: IFavorite) => void; }; -export const FavoriteItemQuickAction: FC = (props) => { +export const FavoriteItemQuickAction: FC = observer((props) => { const { ref, isMenuActive, onChange, handleRemoveFromFavorites, favorite } = props; + // translation + const { t } = useTranslation(); + return ( onChange(!isMenuActive)} > } + menuButtonOnClick={() => onChange(!isMenuActive)} className={cn( "opacity-0 pointer-events-none flex-shrink-0 group-hover/project-item:opacity-100 group-hover/project-item:pointer-events-auto", { @@ -36,6 +41,7 @@ export const FavoriteItemQuickAction: FC = (props) => { )} customButtonClassName="grid place-items-center" placement="bottom-start" + ariaLabel={t("aria_labels.projects_sidebar.toggle_quick_actions_menu")} > handleRemoveFromFavorites(favorite)}> @@ -45,4 +51,4 @@ export const FavoriteItemQuickAction: FC = (props) => { ); -}; +}); diff --git a/web/core/components/workspace/sidebar/favorites/favorites-menu.tsx b/web/core/components/workspace/sidebar/favorites/favorites-menu.tsx index 2beed74f6..f83f0ed06 100644 --- a/web/core/components/workspace/sidebar/favorites/favorites-menu.tsx +++ b/web/core/components/workspace/sidebar/favorites/favorites-menu.tsx @@ -34,13 +34,12 @@ import { getInstructionFromPayload, TargetData } from "./favorites.helpers"; import { NewFavoriteFolder } from "./new-fav-folder"; export const SidebarFavoritesMenu = observer(() => { - //state + // states const [createNewFolder, setCreateNewFolder] = useState(null); - const [isDragging, setIsDragging] = useState(false); - + // navigation + const { workspaceSlug } = useParams(); // store hooks - const { t } = useTranslation(); const { sidebarCollapsed } = useAppTheme(); const { favoriteIds, @@ -50,17 +49,17 @@ export const SidebarFavoritesMenu = observer(() => { reOrderFavorite, moveFavoriteToFolder, } = useFavorite(); - const { workspaceSlug } = useParams(); - + // translation + const { t } = useTranslation(); + // platform hooks const { isMobile } = usePlatformOS(); - // local storage const { setValue: toggleFavoriteMenu, storedValue } = useLocalStorage(IS_FAVORITE_MENU_OPEN, false); // derived values const isFavoriteMenuOpen = !!storedValue; // refs - const containerRef = useRef(null); - const elementRef = useRef(null); + const containerRef = useRef(null); + const elementRef = useRef(null); const handleMoveToFolder = (sourceId: string, destinationId: string) => { moveFavoriteToFolder(workspaceSlug.toString(), sourceId, { @@ -131,6 +130,7 @@ export const SidebarFavoritesMenu = observer(() => { }); }); }; + const handleRemoveFromFavoritesFolder = (favoriteId: string) => { removeFromFavoriteFolder(workspaceSlug.toString(), favoriteId).catch(() => { setToast({ @@ -151,7 +151,7 @@ export const SidebarFavoritesMenu = observer(() => { }); }); }, - [workspaceSlug, reOrderFavorite] + [workspaceSlug, reOrderFavorite, t] ); useEffect(() => { @@ -190,37 +190,68 @@ export const SidebarFavoritesMenu = observer(() => { <> {!sidebarCollapsed && ( - - toggleFavoriteMenu(!isFavoriteMenuOpen)} className="flex-1 text-start"> - {t("favorites")} - - + toggleFavoriteMenu(!isFavoriteMenuOpen)} + aria-label={t( + isFavoriteMenuOpen + ? "aria_labels.projects_sidebar.close_favorites_menu" + : "aria_labels.projects_sidebar.open_favorites_menu" + )} + > + {t("favorites")} + +
- { setCreateNewFolder(true); if (!isFavoriteMenuOpen) toggleFavoriteMenu(!isFavoriteMenuOpen); }} - className={cn("size-4 flex-shrink-0 text-custom-sidebar-text-400 transition-transform")} - /> + aria-label={t("aria_labels.projects_sidebar.create_favorites_folder")} + > + + - toggleFavoriteMenu(!isFavoriteMenuOpen)} - className={cn("size-4 flex-shrink-0 text-custom-sidebar-text-400 transition-transform", { - "rotate-90": isFavoriteMenuOpen, - })} - /> - - + aria-label={t( + isFavoriteMenuOpen + ? "aria_labels.projects_sidebar.close_favorites_menu" + : "aria_labels.projects_sidebar.open_favorites_menu" + )} + > + + +
+
)} { name="name" control={control} rules={{ required: true }} - render={({ field }) => } + render={({ field }) => ( + + )} />
diff --git a/web/core/components/workspace/sidebar/help-section.tsx b/web/core/components/workspace/sidebar/help-section.tsx index 04ef02fea..9c1b98fe8 100644 --- a/web/core/components/workspace/sidebar/help-section.tsx +++ b/web/core/components/workspace/sidebar/help-section.tsx @@ -175,8 +175,13 @@ export const SidebarHelpSection: React.FC = observer( isCollapsed ? "w-full" : "" }`} onClick={() => toggleSidebar()} + aria-label={t( + isCollapsed + ? "aria_labels.projects_sidebar.expand_sidebar" + : "aria_labels.projects_sidebar.collapse_sidebar" + )} > - +
diff --git a/web/core/components/workspace/sidebar/projects-list-item.tsx b/web/core/components/workspace/sidebar/projects-list-item.tsx index 715d02cb1..40b1f2be1 100644 --- a/web/core/components/workspace/sidebar/projects-list-item.tsx +++ b/web/core/components/workspace/sidebar/projects-list-item.tsx @@ -184,13 +184,13 @@ export const SidebarProjectsListItem: React.FC = observer((props) => { const sourceId = source?.data?.id as string | undefined; const destinationId = self?.data?.id as string | undefined; - handleOnProjectDrop && handleOnProjectDrop(sourceId, destinationId, currentInstruction === "DRAG_BELOW"); + handleOnProjectDrop?.(sourceId, destinationId, currentInstruction === "DRAG_BELOW"); highlightIssueOnDrop(`sidebar-${sourceId}-${projectListType}`); }, }) ); - }, [projectRef?.current, dragHandleRef?.current, projectId, isLastChild, projectListType, handleOnProjectDrop]); + }, [projectId, isLastChild, projectListType, handleOnProjectDrop]); useOutsideClickDetector(actionSectionRef, () => setIsMenuActive(false)); useOutsideClickDetector(projectRef, () => projectRef?.current?.classList?.remove(HIGHLIGHT_CLASS)); @@ -284,6 +284,11 @@ export const SidebarProjectsListItem: React.FC = observer((props) => { className={cn("flex-grow flex items-center gap-1.5 text-left select-none w-full", { "justify-center": isSidebarCollapsed, })} + aria-label={ + isProjectListOpen + ? t("aria_labels.projects_sidebar.close_project_menu") + : t("aria_labels.projects_sidebar.open_project_menu") + } >
@@ -310,6 +315,7 @@ export const SidebarProjectsListItem: React.FC = observer((props) => { )} customButtonClassName="grid place-items-center" placement="bottom-start" + ariaLabel={t("aria_labels.projects_sidebar.toggle_quick_actions_menu")} useCaptureForOutsideClick closeOnSelect > @@ -384,6 +390,11 @@ export const SidebarProjectsListItem: React.FC = observer((props) => { } )} onClick={() => setIsProjectListOpen(!isProjectListOpen)} + aria-label={t( + isProjectListOpen + ? "aria_labels.projects_sidebar.close_project_menu" + : "aria_labels.projects_sidebar.open_project_menu" + )} > { as="button" type="button" className={cn( - "group w-full flex items-center gap-1 whitespace-nowrap text-left text-sm font-semibold text-custom-sidebar-text-400", + "w-full flex items-center gap-1 whitespace-nowrap text-left text-sm font-semibold text-custom-sidebar-text-400", { "!text-center w-8 px-2 py-1.5 justify-center": isCollapsed, } )} onClick={() => toggleListDisclosure(!isAllProjectsListOpen)} + aria-label={t( + isAllProjectsListOpen + ? "aria_labels.projects_sidebar.close_projects_menu" + : "aria_labels.projects_sidebar.open_projects_menu" + )} > <> @@ -195,6 +200,7 @@ export const SidebarProjectsList: FC = observer(() => { setTrackElement(`APP_SIDEBAR_JOINED_BLOCK`); setIsProjectModalOpen(true); }} + aria-label={t("aria_labels.projects_sidebar.create_new_project")} > @@ -205,9 +211,14 @@ export const SidebarProjectsList: FC = observer(() => { type="button" className="p-0.5 rounded hover:bg-custom-sidebar-background-80 flex-shrink-0" onClick={() => toggleListDisclosure(!isAllProjectsListOpen)} + aria-label={t( + isAllProjectsListOpen + ? "aria_labels.projects_sidebar.close_projects_menu" + : "aria_labels.projects_sidebar.open_projects_menu" + )} > diff --git a/web/core/components/workspace/sidebar/quick-actions.tsx b/web/core/components/workspace/sidebar/quick-actions.tsx index 150c08d68..86ab89603 100644 --- a/web/core/components/workspace/sidebar/quick-actions.tsx +++ b/web/core/components/workspace/sidebar/quick-actions.tsx @@ -46,7 +46,9 @@ export const SidebarQuickActions = observer(() => { const handleMouseEnter = () => { // if enter before time out clear the timeout - timeoutRef?.current && clearTimeout(timeoutRef.current); + if (timeoutRef?.current) { + clearTimeout(timeoutRef.current); + } setIsDraftButtonOpen(true); }; diff --git a/web/core/components/workspace/sidebar/sidebar-menu-items.tsx b/web/core/components/workspace/sidebar/sidebar-menu-items.tsx index bedfa02a3..89c0f150e 100644 --- a/web/core/components/workspace/sidebar/sidebar-menu-items.tsx +++ b/web/core/components/workspace/sidebar/sidebar-menu-items.tsx @@ -8,6 +8,7 @@ import { WORKSPACE_SIDEBAR_DYNAMIC_NAVIGATION_ITEMS_LINKS, WORKSPACE_SIDEBAR_STATIC_NAVIGATION_ITEMS_LINKS, } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; import { cn } from "@plane/utils"; // components import { SidebarNavItem } from "@/components/sidebar"; @@ -20,9 +21,10 @@ export const SidebarMenuItems = observer(() => { // routers const { workspaceSlug } = useParams(); // store hooks - const { sidebarCollapsed, toggleExtendedSidebar } = useAppTheme(); + const { sidebarCollapsed, extendedSidebarCollapsed, toggleExtendedSidebar } = useAppTheme(); const { getNavigationPreferences } = useWorkspace(); - + // translation + const { t } = useTranslation(); // derived values const currentWorkspaceNavigationPreferences = getNavigationPreferences(workspaceSlug.toString()); @@ -39,31 +41,35 @@ export const SidebarMenuItems = observer(() => { ); return ( - <> -
- {WORKSPACE_SIDEBAR_STATIC_NAVIGATION_ITEMS_LINKS.map((item, _index) => ( - - ))} - {sortedNavigationItems.map((item, _index) => ( - - ))} - - - -
- +
+ {WORKSPACE_SIDEBAR_STATIC_NAVIGATION_ITEMS_LINKS.map((item, _index) => ( + + ))} + {sortedNavigationItems.map((item, _index) => ( + + ))} + + + +
); }); diff --git a/web/core/store/theme.store.ts b/web/core/store/theme.store.ts index a5d961e2d..b37fb0ef5 100644 --- a/web/core/store/theme.store.ts +++ b/web/core/store/theme.store.ts @@ -1,4 +1,4 @@ -import { action, observable, makeObservable } from "mobx"; +import { action, observable, makeObservable, runInAction } from "mobx"; export interface IThemeStore { // observables @@ -26,7 +26,7 @@ export interface IThemeStore { export class ThemeStore implements IThemeStore { // observables sidebarCollapsed: boolean | undefined = undefined; - extendedSidebarCollapsed: boolean | undefined = undefined; + extendedSidebarCollapsed: boolean | undefined = true; extendedProjectSidebarCollapsed: boolean | undefined = undefined; profileSidebarCollapsed: boolean | undefined = undefined; workspaceAnalyticsSidebarCollapsed: boolean | undefined = undefined; @@ -78,12 +78,11 @@ export class ThemeStore implements IThemeStore { * @param collapsed */ toggleExtendedSidebar = (collapsed?: boolean) => { - if (collapsed === undefined) { - this.extendedSidebarCollapsed = !this.extendedSidebarCollapsed; - } else { - this.extendedSidebarCollapsed = collapsed; - } - localStorage.setItem("extended_sidebar_collapsed", this.extendedSidebarCollapsed.toString()); + const updatedState = collapsed ?? !this.extendedSidebarCollapsed; + runInAction(() => { + this.extendedSidebarCollapsed = updatedState; + }); + localStorage.setItem("extended_sidebar_collapsed", updatedState.toString()); }; /** From e388a9a2797f8e65e201c4d3d38df2f53fce0cf7 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Wed, 28 May 2025 01:43:01 +0530 Subject: [PATCH 094/201] [WIKI-181] refactor: file plugins and types (#7074) * refactor: file plugins and types * refactor: image extension storage types * chore: update meta tag name * chore: extension fileset storage key * fix: build errors * refactor: utility extension * refactor: file plugins * chore: remove standalone plugin extensions * chore: refactoring out onCreate into a common utility * refactor: work item embed extension * chore: use extension enums * fix: errors and warnings * refactor: rename extension files * fix: tsup reloading issue * fix: image upload types and heading types * fix: file plugin object reference * fix: iseditable is hard coded * fix: image extension names * fix: collaborative editor editable value * chore: add constants for editor meta as well --------- Co-authored-by: Palanikannan M --- live/package.json | 2 +- packages/decorators/package.json | 2 +- packages/editor/package.json | 2 +- packages/editor/src/ce/constants/utility.ts | 14 + .../src/ce/extensions/document-extensions.tsx | 1 - packages/editor/src/ce/types/storage.ts | 27 +- .../editors/document/collaborative-editor.tsx | 5 +- .../editors/document/read-only-editor.tsx | 4 +- .../components/editors/editor-container.tsx | 17 +- .../editors/link-view-container.tsx | 2 +- .../core/components/links/link-edit-view.tsx | 4 +- .../src/core/components/menus/block-menu.tsx | 9 +- .../menus/bubble-menu/link-selector.tsx | 6 +- .../menus/bubble-menu/node-selector.tsx | 2 +- .../components/menus/bubble-menu/root.tsx | 5 +- .../src/core/components/menus/menu-items.ts | 107 +++--- .../editor/src/core/constants/extension.ts | 44 +++ packages/editor/src/core/constants/meta.ts | 3 + .../src/core/extensions/callout/block.tsx | 2 +- .../extensions/callout/extension-config.ts | 8 +- .../core/extensions/callout/logo-selector.tsx | 7 +- .../src/core/extensions/callout/types.ts | 2 +- .../src/core/extensions/callout/utils.ts | 15 +- .../editor/src/core/extensions/clipboard.ts | 89 ----- .../src/core/extensions/code-inline/index.tsx | 6 +- .../extensions/code/code-block-node-view.tsx | 4 +- .../src/core/extensions/code/code-block.ts | 12 +- .../core/extensions/code/lowlight-plugin.ts | 2 +- .../src/core/extensions/core-without-props.ts | 18 +- .../src/core/extensions/custom-code-inline.ts | 9 - .../src/core/extensions/custom-color.ts | 5 +- .../custom-image/components/image-node.tsx | 5 +- .../components/image-uploader.tsx | 8 +- .../custom-image/components/upload-status.tsx | 6 +- .../extensions/custom-image/custom-image.ts | 49 +-- .../custom-image/read-only-custom-image.ts | 10 +- .../core/extensions/custom-link/extension.tsx | 7 +- .../custom-link/helpers/clickHandler.ts | 2 +- .../custom-list-keymap/list-helpers.ts | 16 +- .../custom-list-keymap/list-keymap.ts | 12 +- packages/editor/src/core/extensions/drop.ts | 127 ------- .../{enter-key-extension.tsx => enter-key.ts} | 17 +- .../{extensions.tsx => extensions.ts} | 30 +- .../{headers.ts => headings-list.ts} | 6 +- .../src/core/extensions/horizontal-rule.ts | 6 +- .../src/core/extensions/image/extension.tsx | 38 +- .../image/image-component-without-props.tsx | 102 +++-- .../image/image-extension-without-props.tsx | 35 +- .../core/extensions/image/read-only-image.tsx | 4 +- packages/editor/src/core/extensions/index.ts | 10 +- .../src/core/extensions/issue-embed/index.ts | 2 - .../issue-embed/issue-embed-without-props.ts | 41 -- .../extensions/issue-embed/widget-node.tsx | 66 ---- .../core/extensions/{keymap.tsx => keymap.ts} | 12 +- .../extensions/mentions/mention-node-view.tsx | 2 +- .../mentions/mentions-list-dropdown.tsx | 10 +- .../src/core/extensions/mentions/utils.ts | 6 +- .../core/extensions/{quote.tsx => quote.ts} | 4 +- ...extensions.tsx => read-only-extensions.ts} | 7 +- .../{side-menu.tsx => side-menu.ts} | 4 +- .../slash-commands/command-items-list.tsx | 23 +- .../slash-commands/command-menu.tsx | 9 +- .../core/extensions/slash-commands/root.tsx | 12 +- .../table/{table-cell => }/table-cell.ts | 5 +- .../core/extensions/table/table-cell/index.ts | 1 - .../table/{table-header => }/table-header.ts | 5 +- .../extensions/table/table-header/index.ts | 1 - .../table/{table-row => }/table-row.ts | 4 +- .../core/extensions/table/table-row/index.ts | 1 - .../extensions/table/table/table-controls.ts | 20 +- .../extensions/table/table/table-view.tsx | 22 +- .../src/core/extensions/table/table/table.ts | 23 +- .../delete-table-when-all-cells-selected.ts | 11 +- .../insert-line-above-table-action.ts | 8 +- .../insert-line-below-table-action.ts | 10 +- .../src/core/extensions/typography/index.ts | 4 +- .../editor/src/core/extensions/utility.ts | 71 ++++ .../work-item-embed/extension-config.ts | 43 +++ .../extensions/work-item-embed/extension.tsx | 30 ++ .../core/extensions/work-item-embed/index.ts | 1 + packages/editor/src/core/helpers/common.ts | 10 +- .../src/core/helpers/editor-commands.ts | 48 +-- packages/editor/src/core/helpers/file.ts | 18 +- .../editor/src/core/helpers/image-helpers.ts | 32 ++ ...insert-empty-paragraph-at-node-boundary.ts | 10 +- .../core/hooks/use-collaborative-editor.ts | 2 +- packages/editor/src/core/hooks/use-editor.ts | 39 +- .../editor/src/core/hooks/use-file-upload.ts | 29 +- .../src/core/hooks/use-read-only-editor.ts | 3 +- .../editor/src/core/plugins/drag-handle.ts | 31 +- packages/editor/src/core/plugins/drop.ts | 118 ++++++ .../editor/src/core/plugins/file/delete.ts | 67 ++++ .../editor/src/core/plugins/file/restore.ts | 72 ++++ packages/editor/src/core/plugins/file/root.ts | 22 ++ .../editor/src/core/plugins/file/types.ts | 8 + .../src/core/plugins/image/delete-image.ts | 52 --- .../editor/src/core/plugins/image/index.ts | 3 - .../src/core/plugins/image/restore-image.ts | 61 --- .../core/plugins/image/types/image-node.ts | 13 - .../src/core/plugins/image/types/index.ts | 1 - .../src/core/plugins/markdown-clipboard.ts | 80 ++++ .../editor/src/core/types/collaboration.ts | 2 +- packages/editor/src/core/types/config.ts | 10 +- packages/editor/src/core/types/image.ts | 5 - packages/editor/src/core/types/index.ts | 1 - packages/hooks/package.json | 2 +- packages/ui/package.json | 2 +- packages/utils/package.json | 2 +- web/core/store/pages/base-page.ts | 1 - yarn.lock | 355 ++++++++++-------- 110 files changed, 1344 insertions(+), 1138 deletions(-) create mode 100644 packages/editor/src/ce/constants/utility.ts create mode 100644 packages/editor/src/core/constants/extension.ts create mode 100644 packages/editor/src/core/constants/meta.ts delete mode 100644 packages/editor/src/core/extensions/clipboard.ts delete mode 100644 packages/editor/src/core/extensions/custom-code-inline.ts delete mode 100644 packages/editor/src/core/extensions/drop.ts rename packages/editor/src/core/extensions/{enter-key-extension.tsx => enter-key.ts} (53%) rename packages/editor/src/core/extensions/{extensions.tsx => extensions.ts} (86%) rename packages/editor/src/core/extensions/{headers.ts => headings-list.ts} (86%) delete mode 100644 packages/editor/src/core/extensions/issue-embed/index.ts delete mode 100644 packages/editor/src/core/extensions/issue-embed/issue-embed-without-props.ts delete mode 100644 packages/editor/src/core/extensions/issue-embed/widget-node.tsx rename packages/editor/src/core/extensions/{keymap.tsx => keymap.ts} (92%) rename packages/editor/src/core/extensions/{quote.tsx => quote.ts} (85%) rename packages/editor/src/core/extensions/{read-only-extensions.tsx => read-only-extensions.ts} (97%) rename packages/editor/src/core/extensions/{side-menu.tsx => side-menu.ts} (97%) rename packages/editor/src/core/extensions/table/{table-cell => }/table-cell.ts (91%) delete mode 100644 packages/editor/src/core/extensions/table/table-cell/index.ts rename packages/editor/src/core/extensions/table/{table-header => }/table-header.ts (90%) delete mode 100644 packages/editor/src/core/extensions/table/table-header/index.ts rename packages/editor/src/core/extensions/table/{table-row => }/table-row.ts (88%) delete mode 100644 packages/editor/src/core/extensions/table/table-row/index.ts create mode 100644 packages/editor/src/core/extensions/utility.ts create mode 100644 packages/editor/src/core/extensions/work-item-embed/extension-config.ts create mode 100644 packages/editor/src/core/extensions/work-item-embed/extension.tsx create mode 100644 packages/editor/src/core/extensions/work-item-embed/index.ts create mode 100644 packages/editor/src/core/helpers/image-helpers.ts create mode 100644 packages/editor/src/core/plugins/drop.ts create mode 100644 packages/editor/src/core/plugins/file/delete.ts create mode 100644 packages/editor/src/core/plugins/file/restore.ts create mode 100644 packages/editor/src/core/plugins/file/root.ts create mode 100644 packages/editor/src/core/plugins/file/types.ts delete mode 100644 packages/editor/src/core/plugins/image/delete-image.ts delete mode 100644 packages/editor/src/core/plugins/image/index.ts delete mode 100644 packages/editor/src/core/plugins/image/restore-image.ts delete mode 100644 packages/editor/src/core/plugins/image/types/image-node.ts delete mode 100644 packages/editor/src/core/plugins/image/types/index.ts create mode 100644 packages/editor/src/core/plugins/markdown-clipboard.ts delete mode 100644 packages/editor/src/core/types/image.ts diff --git a/live/package.json b/live/package.json index 9616b1513..3dcb8b35e 100644 --- a/live/package.json +++ b/live/package.json @@ -57,7 +57,7 @@ "concurrently": "^9.0.1", "nodemon": "^3.1.7", "ts-node": "^10.9.2", - "tsup": "^8.4.0", + "tsup": "8.3.0", "typescript": "5.3.3" } } diff --git a/packages/decorators/package.json b/packages/decorators/package.json index 433b5c11a..198fdc698 100644 --- a/packages/decorators/package.json +++ b/packages/decorators/package.json @@ -27,7 +27,7 @@ "@types/node": "^20.14.9", "@types/reflect-metadata": "^0.1.0", "@types/ws": "^8.5.10", - "tsup": "8.4.0", + "tsup": "8.3.0", "typescript": "^5.3.3" }, "peerDependencies": { diff --git a/packages/editor/package.json b/packages/editor/package.json index cfbd0861e..5a899f738 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -81,7 +81,7 @@ "@types/react": "^18.3.11", "@types/react-dom": "^18.2.18", "postcss": "^8.4.38", - "tsup": "^8.4.0", + "tsup": "8.3.0", "typescript": "5.3.3" }, "keywords": [ diff --git a/packages/editor/src/ce/constants/utility.ts b/packages/editor/src/ce/constants/utility.ts new file mode 100644 index 000000000..616838a62 --- /dev/null +++ b/packages/editor/src/ce/constants/utility.ts @@ -0,0 +1,14 @@ +import { ExtensionFileSetStorageKey } from "@/plane-editor/types/storage"; + +export const NODE_FILE_MAP: { + [key: string]: { + fileSetName: ExtensionFileSetStorageKey; + }; +} = { + image: { + fileSetName: "deletedImageSet", + }, + imageComponent: { + fileSetName: "deletedImageSet", + }, +}; diff --git a/packages/editor/src/ce/extensions/document-extensions.tsx b/packages/editor/src/ce/extensions/document-extensions.tsx index 445f5e0f8..29072b41c 100644 --- a/packages/editor/src/ce/extensions/document-extensions.tsx +++ b/packages/editor/src/ce/extensions/document-extensions.tsx @@ -1,5 +1,4 @@ import { HocuspocusProvider } from "@hocuspocus/provider"; -import { Extensions } from "@tiptap/core"; import { AnyExtension } from "@tiptap/core"; import { SlashCommands } from "@/extensions"; // plane editor types diff --git a/packages/editor/src/ce/types/storage.ts b/packages/editor/src/ce/types/storage.ts index 4e106738b..5f576df50 100644 --- a/packages/editor/src/ce/types/storage.ts +++ b/packages/editor/src/ce/types/storage.ts @@ -1,13 +1,20 @@ -import { HeadingExtensionStorage } from "@/extensions"; -import { CustomImageExtensionStorage } from "@/extensions/custom-image"; -import { CustomLinkStorage } from "@/extensions/custom-link"; -import { MentionExtensionStorage } from "@/extensions/mentions"; -import { ImageExtensionStorage } from "@/plugins/image"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; +// extensions +import { type HeadingExtensionStorage } from "@/extensions"; +import { type CustomImageExtensionStorage } from "@/extensions/custom-image"; +import { type CustomLinkStorage } from "@/extensions/custom-link"; +import { type ImageExtensionStorage } from "@/extensions/image"; +import { type MentionExtensionStorage } from "@/extensions/mentions"; +import { type UtilityExtensionStorage } from "@/extensions/utility"; export type ExtensionStorageMap = { - imageComponent: CustomImageExtensionStorage; - image: ImageExtensionStorage; - link: CustomLinkStorage; - headingList: HeadingExtensionStorage; - mention: MentionExtensionStorage; + [CORE_EXTENSIONS.CUSTOM_IMAGE]: CustomImageExtensionStorage; + [CORE_EXTENSIONS.IMAGE]: ImageExtensionStorage; + [CORE_EXTENSIONS.CUSTOM_LINK]: CustomLinkStorage; + [CORE_EXTENSIONS.HEADINGS_LIST]: HeadingExtensionStorage; + [CORE_EXTENSIONS.MENTION]: MentionExtensionStorage; + [CORE_EXTENSIONS.UTILITY]: UtilityExtensionStorage; }; + +export type ExtensionFileSetStorageKey = Extract; diff --git a/packages/editor/src/core/components/editors/document/collaborative-editor.tsx b/packages/editor/src/core/components/editors/document/collaborative-editor.tsx index 623ec9508..d1398ff5a 100644 --- a/packages/editor/src/core/components/editors/document/collaborative-editor.tsx +++ b/packages/editor/src/core/components/editors/document/collaborative-editor.tsx @@ -7,7 +7,7 @@ import { DocumentContentLoader, PageRenderer } from "@/components/editors"; // constants import { DEFAULT_DISPLAY_CONFIG } from "@/constants/config"; // extensions -import { IssueWidget } from "@/extensions"; +import { WorkItemEmbedExtension } from "@/extensions"; // helpers import { getEditorClassNames } from "@/helpers/common"; // hooks @@ -39,9 +39,10 @@ const CollaborativeDocumentEditor = (props: ICollaborativeDocumentEditor) => { } = props; const extensions: Extensions = []; + if (embedHandler?.issue) { extensions.push( - IssueWidget({ + WorkItemEmbedExtension({ widgetCallback: embedHandler.issue.widgetCallback, }) ); diff --git a/packages/editor/src/core/components/editors/document/read-only-editor.tsx b/packages/editor/src/core/components/editors/document/read-only-editor.tsx index 54a1f96e2..2d2e30830 100644 --- a/packages/editor/src/core/components/editors/document/read-only-editor.tsx +++ b/packages/editor/src/core/components/editors/document/read-only-editor.tsx @@ -7,7 +7,7 @@ import { PageRenderer } from "@/components/editors"; // constants import { DEFAULT_DISPLAY_CONFIG } from "@/constants/config"; // extensions -import { IssueWidget } from "@/extensions"; +import { WorkItemEmbedExtension } from "@/extensions"; // helpers import { getEditorClassNames } from "@/helpers/common"; // hooks @@ -53,7 +53,7 @@ const DocumentReadOnlyEditor = (props: IDocumentReadOnlyEditor) => { const extensions: Extensions = []; if (embedHandler?.issue) { extensions.push( - IssueWidget({ + WorkItemEmbedExtension({ widgetCallback: embedHandler.issue.widgetCallback, }) ); diff --git a/packages/editor/src/core/components/editors/editor-container.tsx b/packages/editor/src/core/components/editors/editor-container.tsx index d0811cd41..6daa0719a 100644 --- a/packages/editor/src/core/components/editors/editor-container.tsx +++ b/packages/editor/src/core/components/editors/editor-container.tsx @@ -4,6 +4,7 @@ import { FC, ReactNode, useRef } from "react"; import { cn } from "@plane/utils"; // constants import { DEFAULT_DISPLAY_CONFIG } from "@/constants/config"; +import { CORE_EXTENSIONS } from "@/constants/extension"; // types import { TDisplayConfig } from "@/types"; // components @@ -36,12 +37,12 @@ export const EditorContainer: FC = (props) => { if ( currentNode.content.size === 0 && // Check if the current node is empty !( - editor.isActive("orderedList") || - editor.isActive("bulletList") || - editor.isActive("taskItem") || - editor.isActive("table") || - editor.isActive("blockquote") || - editor.isActive("codeBlock") + editor.isActive(CORE_EXTENSIONS.ORDERED_LIST) || + editor.isActive(CORE_EXTENSIONS.BULLET_LIST) || + editor.isActive(CORE_EXTENSIONS.TASK_ITEM) || + editor.isActive(CORE_EXTENSIONS.TABLE) || + editor.isActive(CORE_EXTENSIONS.BLOCKQUOTE) || + editor.isActive(CORE_EXTENSIONS.CODE_BLOCK) ) // Check if it's an empty node within an orderedList, bulletList, taskItem, table, quote or code block ) { return; @@ -53,10 +54,10 @@ export const EditorContainer: FC = (props) => { const lastNode = lastNodePos.node(); // Check if the last node is a not paragraph - if (lastNode && lastNode.type.name !== "paragraph") { + if (lastNode && lastNode.type.name !== CORE_EXTENSIONS.PARAGRAPH) { // If last node is not a paragraph, insert a new paragraph at the end const endPosition = editor?.state.doc.content.size; - editor?.chain().insertContentAt(endPosition, { type: "paragraph" }).run(); + editor?.chain().insertContentAt(endPosition, { type: CORE_EXTENSIONS.PARAGRAPH }).run(); // Focus the newly added paragraph for immediate editing editor diff --git a/packages/editor/src/core/components/editors/link-view-container.tsx b/packages/editor/src/core/components/editors/link-view-container.tsx index 41263a996..68fa33dde 100644 --- a/packages/editor/src/core/components/editors/link-view-container.tsx +++ b/packages/editor/src/core/components/editors/link-view-container.tsx @@ -12,7 +12,7 @@ interface LinkViewContainerProps { export const LinkViewContainer: FC = ({ editor, containerRef }) => { const [linkViewProps, setLinkViewProps] = useState(); const [isOpen, setIsOpen] = useState(false); - const [virtualElement, setVirtualElement] = useState(null); + const [virtualElement, setVirtualElement] = useState(null); const editorState = useEditorState({ editor, diff --git a/packages/editor/src/core/components/links/link-edit-view.tsx b/packages/editor/src/core/components/links/link-edit-view.tsx index ad66ce4b4..1e9a62b0e 100644 --- a/packages/editor/src/core/components/links/link-edit-view.tsx +++ b/packages/editor/src/core/components/links/link-edit-view.tsx @@ -51,7 +51,9 @@ export const LinkEditView = ({ viewProps }: LinkEditViewProps) => { if (!hasSubmitted.current && !linkRemoved && initialUrl === "") { try { removeLink(); - } catch (e) {} + } catch (e) { + console.error("Error removing link", e); + } } }, [linkRemoved, initialUrl] diff --git a/packages/editor/src/core/components/menus/block-menu.tsx b/packages/editor/src/core/components/menus/block-menu.tsx index c143abd00..bd86628cb 100644 --- a/packages/editor/src/core/components/menus/block-menu.tsx +++ b/packages/editor/src/core/components/menus/block-menu.tsx @@ -1,7 +1,9 @@ -import { useCallback, useEffect, useRef } from "react"; import { Editor } from "@tiptap/react"; -import tippy, { Instance } from "tippy.js"; import { Copy, LucideIcon, Trash2 } from "lucide-react"; +import { useCallback, useEffect, useRef } from "react"; +import tippy, { Instance } from "tippy.js"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; interface BlockMenuProps { editor: Editor; @@ -102,7 +104,8 @@ export const BlockMenu = (props: BlockMenuProps) => { key: "duplicate", label: "Duplicate", isDisabled: - editor.state.selection.content().content.firstChild?.type.name === "image" || editor.isActive("imageComponent"), + editor.state.selection.content().content.firstChild?.type.name === CORE_EXTENSIONS.IMAGE || + editor.isActive(CORE_EXTENSIONS.CUSTOM_IMAGE), onClick: (e) => { e.preventDefault(); e.stopPropagation(); diff --git a/packages/editor/src/core/components/menus/bubble-menu/link-selector.tsx b/packages/editor/src/core/components/menus/bubble-menu/link-selector.tsx index 1dd47c5bb..6f582f89c 100644 --- a/packages/editor/src/core/components/menus/bubble-menu/link-selector.tsx +++ b/packages/editor/src/core/components/menus/bubble-menu/link-selector.tsx @@ -1,8 +1,10 @@ import { Editor } from "@tiptap/core"; import { Check, Link, Trash2 } from "lucide-react"; import { Dispatch, FC, SetStateAction, useCallback, useRef, useState } from "react"; -// plane utils +// plane imports import { cn } from "@plane/utils"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; // helpers import { isValidHttpUrl } from "@/helpers/common"; import { setLinkEditor, unsetLinkEditor } from "@/helpers/editor-commands"; @@ -43,7 +45,7 @@ export const BubbleMenuLinkSelector: FC = (props) => { "h-full flex items-center gap-1 px-3 text-sm font-medium text-custom-text-300 hover:bg-custom-background-80 active:bg-custom-background-80 rounded transition-colors", { "bg-custom-background-80": isOpen, - "text-custom-text-100": editor.isActive("link"), + "text-custom-text-100": editor.isActive(CORE_EXTENSIONS.CUSTOM_LINK), } )} onClick={(e) => { diff --git a/packages/editor/src/core/components/menus/bubble-menu/node-selector.tsx b/packages/editor/src/core/components/menus/bubble-menu/node-selector.tsx index 7d1378800..564f7d97c 100644 --- a/packages/editor/src/core/components/menus/bubble-menu/node-selector.tsx +++ b/packages/editor/src/core/components/menus/bubble-menu/node-selector.tsx @@ -1,6 +1,6 @@ -import { Dispatch, FC, SetStateAction } from "react"; import { Editor } from "@tiptap/react"; import { Check, ChevronDown } from "lucide-react"; +import { Dispatch, FC, SetStateAction } from "react"; // plane utils import { cn } from "@plane/utils"; // components diff --git a/packages/editor/src/core/components/menus/bubble-menu/root.tsx b/packages/editor/src/core/components/menus/bubble-menu/root.tsx index 02eb8d486..30a7c5620 100644 --- a/packages/editor/src/core/components/menus/bubble-menu/root.tsx +++ b/packages/editor/src/core/components/menus/bubble-menu/root.tsx @@ -18,6 +18,7 @@ import { } from "@/components/menus"; // constants import { COLORS_LIST } from "@/constants/common"; +import { CORE_EXTENSIONS } from "@/constants/extension"; // extensions import { isCellSelection } from "@/extensions/table/table/utilities/is-cell-selection"; // local components @@ -90,8 +91,8 @@ export const EditorBubbleMenu: FC = (props: { editor: Edi if ( empty || !editor.isEditable || - editor.isActive("image") || - editor.isActive("imageComponent") || + editor.isActive(CORE_EXTENSIONS.IMAGE) || + editor.isActive(CORE_EXTENSIONS.CUSTOM_IMAGE) || isNodeSelection(selection) || isCellSelection(selection) || isSelecting diff --git a/packages/editor/src/core/components/menus/menu-items.ts b/packages/editor/src/core/components/menus/menu-items.ts index 4268ccb6c..c3aa4d414 100644 --- a/packages/editor/src/core/components/menus/menu-items.ts +++ b/packages/editor/src/core/components/menus/menu-items.ts @@ -23,6 +23,8 @@ import { Palette, AlignCenter, } from "lucide-react"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; // helpers import { insertHorizontalRule, @@ -35,12 +37,7 @@ import { toggleBold, toggleBulletList, toggleCodeBlock, - toggleHeadingFive, - toggleHeadingFour, - toggleHeadingOne, - toggleHeadingSix, - toggleHeadingThree, - toggleHeadingTwo, + toggleHeading, toggleItalic, toggleOrderedList, toggleStrike, @@ -65,63 +62,49 @@ export type EditorMenuItem = { export const TextItem = (editor: Editor): EditorMenuItem<"text"> => ({ key: "text", name: "Text", - isActive: () => editor.isActive("paragraph"), + isActive: () => editor.isActive(CORE_EXTENSIONS.PARAGRAPH), command: () => setText(editor), icon: CaseSensitive, }); -export const HeadingOneItem = (editor: Editor): EditorMenuItem<"h1"> => ({ - key: "h1", - name: "Heading 1", - isActive: () => editor.isActive("heading", { level: 1 }), - command: () => toggleHeadingOne(editor), - icon: Heading1, +type SupportedHeadingLevels = "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; + +const HeadingItem = ( + editor: Editor, + level: 1 | 2 | 3 | 4 | 5 | 6, + key: T, + name: string, + icon: LucideIcon +): EditorMenuItem => ({ + key, + name, + isActive: () => editor.isActive(CORE_EXTENSIONS.HEADING, { level }), + command: () => toggleHeading(editor, level), + icon, }); -export const HeadingTwoItem = (editor: Editor): EditorMenuItem<"h2"> => ({ - key: "h2", - name: "Heading 2", - isActive: () => editor.isActive("heading", { level: 2 }), - command: () => toggleHeadingTwo(editor), - icon: Heading2, -}); +export const HeadingOneItem = (editor: Editor): EditorMenuItem<"h1"> => + HeadingItem(editor, 1, "h1", "Heading 1", Heading1); -export const HeadingThreeItem = (editor: Editor): EditorMenuItem<"h3"> => ({ - key: "h3", - name: "Heading 3", - isActive: () => editor.isActive("heading", { level: 3 }), - command: () => toggleHeadingThree(editor), - icon: Heading3, -}); +export const HeadingTwoItem = (editor: Editor): EditorMenuItem<"h2"> => + HeadingItem(editor, 2, "h2", "Heading 2", Heading2); -export const HeadingFourItem = (editor: Editor): EditorMenuItem<"h4"> => ({ - key: "h4", - name: "Heading 4", - isActive: () => editor.isActive("heading", { level: 4 }), - command: () => toggleHeadingFour(editor), - icon: Heading4, -}); +export const HeadingThreeItem = (editor: Editor): EditorMenuItem<"h3"> => + HeadingItem(editor, 3, "h3", "Heading 3", Heading3); -export const HeadingFiveItem = (editor: Editor): EditorMenuItem<"h5"> => ({ - key: "h5", - name: "Heading 5", - isActive: () => editor.isActive("heading", { level: 5 }), - command: () => toggleHeadingFive(editor), - icon: Heading5, -}); +export const HeadingFourItem = (editor: Editor): EditorMenuItem<"h4"> => + HeadingItem(editor, 4, "h4", "Heading 4", Heading4); -export const HeadingSixItem = (editor: Editor): EditorMenuItem<"h6"> => ({ - key: "h6", - name: "Heading 6", - isActive: () => editor.isActive("heading", { level: 6 }), - command: () => toggleHeadingSix(editor), - icon: Heading6, -}); +export const HeadingFiveItem = (editor: Editor): EditorMenuItem<"h5"> => + HeadingItem(editor, 5, "h5", "Heading 5", Heading5); + +export const HeadingSixItem = (editor: Editor): EditorMenuItem<"h6"> => + HeadingItem(editor, 6, "h6", "Heading 6", Heading6); export const BoldItem = (editor: Editor): EditorMenuItem<"bold"> => ({ key: "bold", name: "Bold", - isActive: () => editor?.isActive("bold"), + isActive: () => editor?.isActive(CORE_EXTENSIONS.BOLD), command: () => toggleBold(editor), icon: BoldIcon, }); @@ -129,7 +112,7 @@ export const BoldItem = (editor: Editor): EditorMenuItem<"bold"> => ({ export const ItalicItem = (editor: Editor): EditorMenuItem<"italic"> => ({ key: "italic", name: "Italic", - isActive: () => editor?.isActive("italic"), + isActive: () => editor?.isActive(CORE_EXTENSIONS.ITALIC), command: () => toggleItalic(editor), icon: ItalicIcon, }); @@ -137,7 +120,7 @@ export const ItalicItem = (editor: Editor): EditorMenuItem<"italic"> => ({ export const UnderLineItem = (editor: Editor): EditorMenuItem<"underline"> => ({ key: "underline", name: "Underline", - isActive: () => editor?.isActive("underline"), + isActive: () => editor?.isActive(CORE_EXTENSIONS.UNDERLINE), command: () => toggleUnderline(editor), icon: UnderlineIcon, }); @@ -145,7 +128,7 @@ export const UnderLineItem = (editor: Editor): EditorMenuItem<"underline"> => ({ export const StrikeThroughItem = (editor: Editor): EditorMenuItem<"strikethrough"> => ({ key: "strikethrough", name: "Strikethrough", - isActive: () => editor?.isActive("strike"), + isActive: () => editor?.isActive(CORE_EXTENSIONS.STRIKETHROUGH), command: () => toggleStrike(editor), icon: StrikethroughIcon, }); @@ -153,7 +136,7 @@ export const StrikeThroughItem = (editor: Editor): EditorMenuItem<"strikethrough export const BulletListItem = (editor: Editor): EditorMenuItem<"bulleted-list"> => ({ key: "bulleted-list", name: "Bulleted list", - isActive: () => editor?.isActive("bulletList"), + isActive: () => editor?.isActive(CORE_EXTENSIONS.BULLET_LIST), command: () => toggleBulletList(editor), icon: ListIcon, }); @@ -161,7 +144,7 @@ export const BulletListItem = (editor: Editor): EditorMenuItem<"bulleted-list"> export const NumberedListItem = (editor: Editor): EditorMenuItem<"numbered-list"> => ({ key: "numbered-list", name: "Numbered list", - isActive: () => editor?.isActive("orderedList"), + isActive: () => editor?.isActive(CORE_EXTENSIONS.ORDERED_LIST), command: () => toggleOrderedList(editor), icon: ListOrderedIcon, }); @@ -169,7 +152,7 @@ export const NumberedListItem = (editor: Editor): EditorMenuItem<"numbered-list" export const TodoListItem = (editor: Editor): EditorMenuItem<"to-do-list"> => ({ key: "to-do-list", name: "To-do list", - isActive: () => editor.isActive("taskItem"), + isActive: () => editor.isActive(CORE_EXTENSIONS.TASK_ITEM), command: () => toggleTaskList(editor), icon: CheckSquare, }); @@ -177,7 +160,7 @@ export const TodoListItem = (editor: Editor): EditorMenuItem<"to-do-list"> => ({ export const QuoteItem = (editor: Editor): EditorMenuItem<"quote"> => ({ key: "quote", name: "Quote", - isActive: () => editor?.isActive("blockquote"), + isActive: () => editor?.isActive(CORE_EXTENSIONS.BLOCKQUOTE), command: () => toggleBlockquote(editor), icon: TextQuote, }); @@ -185,7 +168,7 @@ export const QuoteItem = (editor: Editor): EditorMenuItem<"quote"> => ({ export const CodeItem = (editor: Editor): EditorMenuItem<"code"> => ({ key: "code", name: "Code", - isActive: () => editor?.isActive("code") || editor?.isActive("codeBlock"), + isActive: () => editor?.isActive(CORE_EXTENSIONS.CODE_INLINE) || editor?.isActive(CORE_EXTENSIONS.CODE_BLOCK), command: () => toggleCodeBlock(editor), icon: CodeIcon, }); @@ -193,7 +176,7 @@ export const CodeItem = (editor: Editor): EditorMenuItem<"code"> => ({ export const TableItem = (editor: Editor): EditorMenuItem<"table"> => ({ key: "table", name: "Table", - isActive: () => editor?.isActive("table"), + isActive: () => editor?.isActive(CORE_EXTENSIONS.TABLE), command: () => insertTableCommand(editor), icon: TableIcon, }); @@ -201,7 +184,7 @@ export const TableItem = (editor: Editor): EditorMenuItem<"table"> => ({ export const ImageItem = (editor: Editor): EditorMenuItem<"image"> => ({ key: "image", name: "Image", - isActive: () => editor?.isActive("image") || editor?.isActive("imageComponent"), + isActive: () => editor?.isActive(CORE_EXTENSIONS.IMAGE) || editor?.isActive(CORE_EXTENSIONS.CUSTOM_IMAGE), command: () => insertImage({ editor, event: "insert", pos: editor.state.selection.from }), icon: ImageIcon, }); @@ -210,7 +193,7 @@ export const HorizontalRuleItem = (editor: Editor) => ({ key: "divider", name: "Divider", - isActive: () => editor?.isActive("horizontalRule"), + isActive: () => editor?.isActive(CORE_EXTENSIONS.HORIZONTAL_RULE), command: () => insertHorizontalRule(editor), icon: MinusSquare, }) as const; @@ -218,7 +201,7 @@ export const HorizontalRuleItem = (editor: Editor) => export const TextColorItem = (editor: Editor): EditorMenuItem<"text-color"> => ({ key: "text-color", name: "Color", - isActive: (props) => editor.isActive("customColor", { color: props?.color }), + isActive: (props) => editor.isActive(CORE_EXTENSIONS.CUSTOM_COLOR, { color: props?.color }), command: (props) => { if (!props) return; toggleTextColor(props.color, editor); @@ -229,7 +212,7 @@ export const TextColorItem = (editor: Editor): EditorMenuItem<"text-color"> => ( export const BackgroundColorItem = (editor: Editor): EditorMenuItem<"background-color"> => ({ key: "background-color", name: "Background color", - isActive: (props) => editor.isActive("customColor", { backgroundColor: props?.color }), + isActive: (props) => editor.isActive(CORE_EXTENSIONS.CUSTOM_COLOR, { backgroundColor: props?.color }), command: (props) => { if (!props) return; toggleBackgroundColor(props.color, editor); diff --git a/packages/editor/src/core/constants/extension.ts b/packages/editor/src/core/constants/extension.ts new file mode 100644 index 000000000..db070cb7b --- /dev/null +++ b/packages/editor/src/core/constants/extension.ts @@ -0,0 +1,44 @@ +export enum CORE_EXTENSIONS { + BLOCKQUOTE = "blockquote", + BOLD = "bold", + BULLET_LIST = "bulletList", + CALLOUT = "calloutComponent", + CHARACTER_COUNT = "characterCount", + CODE_BLOCK = "codeBlock", + CODE_INLINE = "code", + CUSTOM_COLOR = "customColor", + CUSTOM_IMAGE = "imageComponent", + CUSTOM_LINK = "link", + DOCUMENT = "doc", + DROP_CURSOR = "dropCursor", + ENTER_KEY = "enterKey", + GAP_CURSOR = "gapCursor", + HARD_BREAK = "hardBreak", + HEADING = "heading", + HEADINGS_LIST = "headingsList", + HISTORY = "history", + HORIZONTAL_RULE = "horizontalRule", + IMAGE = "image", + ITALIC = "italic", + LIST_ITEM = "listItem", + MARKDOWN_CLIPBOARD = "markdownClipboard", + MENTION = "mention", + ORDERED_LIST = "orderedList", + PARAGRAPH = "paragraph", + PLACEHOLDER = "placeholder", + SIDE_MENU = "editorSideMenu", + SLASH_COMMANDS = "slash-command", + STRIKETHROUGH = "strike", + TABLE = "table", + TABLE_CELL = "tableCell", + TABLE_HEADER = "tableHeader", + TABLE_ROW = "tableRow", + TASK_ITEM = "taskItem", + TASK_LIST = "taskList", + TEXT_ALIGN = "textAlign", + TEXT_STYLE = "textStyle", + TYPOGRAPHY = "typography", + UNDERLINE = "underline", + UTILITY = "utility", + WORK_ITEM_EMBED = "issue-embed-component", +} diff --git a/packages/editor/src/core/constants/meta.ts b/packages/editor/src/core/constants/meta.ts new file mode 100644 index 000000000..66769bb82 --- /dev/null +++ b/packages/editor/src/core/constants/meta.ts @@ -0,0 +1,3 @@ +export enum CORE_EDITOR_META { + SKIP_FILE_DELETION = "skipFileDeletion", +} diff --git a/packages/editor/src/core/extensions/callout/block.tsx b/packages/editor/src/core/extensions/callout/block.tsx index b6c6d7991..662a5ad39 100644 --- a/packages/editor/src/core/extensions/callout/block.tsx +++ b/packages/editor/src/core/extensions/callout/block.tsx @@ -1,5 +1,5 @@ -import React, { useState } from "react"; import { NodeViewContent, NodeViewProps, NodeViewWrapper } from "@tiptap/react"; +import React, { useState } from "react"; // constants import { COLORS_LIST } from "@/constants/common"; // local components diff --git a/packages/editor/src/core/extensions/callout/extension-config.ts b/packages/editor/src/core/extensions/callout/extension-config.ts index 546311509..e52be72d6 100644 --- a/packages/editor/src/core/extensions/callout/extension-config.ts +++ b/packages/editor/src/core/extensions/callout/extension-config.ts @@ -1,6 +1,8 @@ import { Node, mergeAttributes } from "@tiptap/core"; -import { Node as NodeType } from "@tiptap/pm/model"; import { MarkdownSerializerState } from "@tiptap/pm/markdown"; +import { Node as NodeType } from "@tiptap/pm/model"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; // types import { EAttributeNames, TCalloutBlockAttributes } from "./types"; // utils @@ -9,14 +11,14 @@ import { DEFAULT_CALLOUT_BLOCK_ATTRIBUTES } from "./utils"; // Extend Tiptap's Commands interface declare module "@tiptap/core" { interface Commands { - calloutComponent: { + [CORE_EXTENSIONS.CALLOUT]: { insertCallout: () => ReturnType; }; } } export const CustomCalloutExtensionConfig = Node.create({ - name: "calloutComponent", + name: CORE_EXTENSIONS.CALLOUT, group: "block", content: "block+", diff --git a/packages/editor/src/core/extensions/callout/logo-selector.tsx b/packages/editor/src/core/extensions/callout/logo-selector.tsx index 8ea47d50d..7a552cd16 100644 --- a/packages/editor/src/core/extensions/callout/logo-selector.tsx +++ b/packages/editor/src/core/extensions/callout/logo-selector.tsx @@ -1,9 +1,6 @@ -// plane helpers -import { convertHexEmojiToDecimal } from "@plane/utils"; -// plane ui +// plane imports import { EmojiIconPicker, EmojiIconPickerTypes, Logo, TEmojiLogoProps } from "@plane/ui"; -// plane utils -import { cn } from "@plane/utils"; +import { cn, convertHexEmojiToDecimal } from "@plane/utils"; // types import { TCalloutBlockAttributes } from "./types"; // utils diff --git a/packages/editor/src/core/extensions/callout/types.ts b/packages/editor/src/core/extensions/callout/types.ts index 17c55d9e5..8e650d873 100644 --- a/packages/editor/src/core/extensions/callout/types.ts +++ b/packages/editor/src/core/extensions/callout/types.ts @@ -20,7 +20,7 @@ export type TCalloutBlockEmojiAttributes = { export type TCalloutBlockAttributes = { [EAttributeNames.LOGO_IN_USE]: "emoji" | "icon"; - [EAttributeNames.BACKGROUND]: string; + [EAttributeNames.BACKGROUND]: string | undefined; [EAttributeNames.BLOCK_TYPE]: "callout-component"; } & TCalloutBlockIconAttributes & TCalloutBlockEmojiAttributes; diff --git a/packages/editor/src/core/extensions/callout/utils.ts b/packages/editor/src/core/extensions/callout/utils.ts index 6568a40e3..3bf07f0a9 100644 --- a/packages/editor/src/core/extensions/callout/utils.ts +++ b/packages/editor/src/core/extensions/callout/utils.ts @@ -1,7 +1,6 @@ -// plane helpers -import { sanitizeHTML } from "@plane/utils"; -// plane ui +// plane imports import { TEmojiLogoProps } from "@plane/ui"; +import { sanitizeHTML } from "@plane/utils"; // types import { EAttributeNames, @@ -12,11 +11,11 @@ import { export const DEFAULT_CALLOUT_BLOCK_ATTRIBUTES: TCalloutBlockAttributes = { "data-logo-in-use": "emoji", - "data-icon-color": null, - "data-icon-name": null, + "data-icon-color": undefined, + "data-icon-name": undefined, "data-emoji-unicode": "128161", "data-emoji-url": "https://cdn.jsdelivr.net/npm/emoji-datasource-apple/img/apple/64/1f4a1.png", - "data-background": null, + "data-background": undefined, "data-block-type": "callout-component", }; @@ -32,7 +31,7 @@ export const getStoredLogo = (): TStoredLogoValue => { }; if (typeof window !== "undefined") { - const storedData = sanitizeHTML(localStorage.getItem("editor-calloutComponent-logo")); + const storedData = sanitizeHTML(localStorage.getItem("editor-calloutComponent-logo") ?? ""); if (storedData) { let parsedData: TEmojiLogoProps; try { @@ -69,7 +68,7 @@ export const updateStoredLogo = (value: TEmojiLogoProps): void => { // function to get the stored background color from local storage export const getStoredBackgroundColor = (): string | null => { if (typeof window !== "undefined") { - return sanitizeHTML(localStorage.getItem("editor-calloutComponent-background")); + return sanitizeHTML(localStorage.getItem("editor-calloutComponent-background") ?? ""); } return null; }; diff --git a/packages/editor/src/core/extensions/clipboard.ts b/packages/editor/src/core/extensions/clipboard.ts deleted file mode 100644 index 252f0a113..000000000 --- a/packages/editor/src/core/extensions/clipboard.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { Extension } from "@tiptap/core"; -import { Fragment, Node } from "@tiptap/pm/model"; -import { Plugin, PluginKey } from "@tiptap/pm/state"; - -export const MarkdownClipboard = Extension.create({ - name: "markdownClipboard", - - addProseMirrorPlugins() { - return [ - new Plugin({ - key: new PluginKey("markdownClipboard"), - props: { - clipboardTextSerializer: (slice) => { - const markdownSerializer = this.editor.storage.markdown.serializer; - const isTableRow = slice.content.firstChild?.type?.name === "tableRow"; - const nodeSelect = slice.openStart === 0 && slice.openEnd === 0; - - if (nodeSelect) { - return markdownSerializer.serialize(slice.content); - } - - const processTableContent = (tableNode: Node | Fragment) => { - let result = ""; - tableNode.content?.forEach?.((tableRowNode: Node | Fragment) => { - tableRowNode.content?.forEach?.((cell: Node) => { - const cellContent = cell.content ? markdownSerializer.serialize(cell.content) : ""; - result += cellContent + "\n"; - }); - }); - return result; - }; - - if (isTableRow) { - const rowsCount = slice.content?.childCount || 0; - const cellsCount = slice.content?.firstChild?.content?.childCount || 0; - if (rowsCount === 1 || cellsCount === 1) { - return processTableContent(slice.content); - } else { - return markdownSerializer.serialize(slice.content); - } - } - - const traverseToParentOfLeaf = ( - node: Node | null, - parent: Fragment | Node, - depth: number - ): Node | Fragment => { - let currentNode = node; - let currentParent = parent; - let currentDepth = depth; - - while (currentNode && currentDepth > 1 && currentNode.content?.firstChild) { - if (currentNode.content?.childCount > 1) { - if (currentNode.content.firstChild?.type?.name === "listItem") { - return currentParent; - } else { - return currentNode.content; - } - } - - currentParent = currentNode; - currentNode = currentNode.content?.firstChild || null; - currentDepth--; - } - - return currentParent; - }; - - if (slice.content.childCount > 1) { - return markdownSerializer.serialize(slice.content); - } else { - const targetNode = traverseToParentOfLeaf(slice.content.firstChild, slice.content, slice.openStart); - - let currentNode = targetNode; - while (currentNode && currentNode.content && currentNode.childCount === 1 && currentNode.firstChild) { - currentNode = currentNode.firstChild; - } - if (currentNode instanceof Node && currentNode.isText) { - return currentNode.text; - } - - return markdownSerializer.serialize(targetNode); - } - }, - }, - }), - ]; - }, -}); diff --git a/packages/editor/src/core/extensions/code-inline/index.tsx b/packages/editor/src/core/extensions/code-inline/index.tsx index 6e023b6ed..ae320cf6a 100644 --- a/packages/editor/src/core/extensions/code-inline/index.tsx +++ b/packages/editor/src/core/extensions/code-inline/index.tsx @@ -1,4 +1,6 @@ import { Mark, markInputRule, markPasteRule, mergeAttributes } from "@tiptap/core"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; export interface CodeOptions { HTMLAttributes: Record; @@ -6,7 +8,7 @@ export interface CodeOptions { declare module "@tiptap/core" { interface Commands { - code: { + [CORE_EXTENSIONS.CODE_INLINE]: { /** * Set a code mark */ @@ -27,7 +29,7 @@ export const inputRegex = /(?:^|\s)((?:`)((?:[^`]+))(?:`))$/; const pasteRegex = /(?:^|\s)((?:`)((?:[^`]+))(?:`))/g; export const CustomCodeInlineExtension = Mark.create({ - name: "code", + name: CORE_EXTENSIONS.CODE_INLINE, addOptions() { return { diff --git a/packages/editor/src/core/extensions/code/code-block-node-view.tsx b/packages/editor/src/core/extensions/code/code-block-node-view.tsx index a06d83990..7626031bc 100644 --- a/packages/editor/src/core/extensions/code/code-block-node-view.tsx +++ b/packages/editor/src/core/extensions/code/code-block-node-view.tsx @@ -1,11 +1,11 @@ "use client"; -import { useState } from "react"; import { Node as ProseMirrorNode } from "@tiptap/pm/model"; import { NodeViewWrapper, NodeViewContent } from "@tiptap/react"; import ts from "highlight.js/lib/languages/typescript"; import { common, createLowlight } from "lowlight"; import { CopyIcon, CheckIcon } from "lucide-react"; +import { useState } from "react"; // ui import { Tooltip } from "@plane/ui"; // plane utils @@ -27,7 +27,7 @@ export const CodeBlockComponent: React.FC = ({ node }) await navigator.clipboard.writeText(node.textContent); setCopied(true); setTimeout(() => setCopied(false), 1000); - } catch (error) { + } catch { setCopied(false); } e.preventDefault(); diff --git a/packages/editor/src/core/extensions/code/code-block.ts b/packages/editor/src/core/extensions/code/code-block.ts index b2218ee45..3b07617ca 100644 --- a/packages/editor/src/core/extensions/code/code-block.ts +++ b/packages/editor/src/core/extensions/code/code-block.ts @@ -1,5 +1,7 @@ import { mergeAttributes, Node, textblockTypeInputRule } from "@tiptap/core"; import { Plugin, PluginKey } from "@tiptap/pm/state"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; export interface CodeBlockOptions { /** @@ -25,7 +27,7 @@ export interface CodeBlockOptions { declare module "@tiptap/core" { interface Commands { - codeBlock: { + [CORE_EXTENSIONS.CODE_BLOCK]: { /** * Set a code block */ @@ -42,7 +44,7 @@ export const backtickInputRegex = /^```([a-z]+)?[\s\n]$/; export const tildeInputRegex = /^~~~([a-z]+)?[\s\n]$/; export const CodeBlock = Node.create({ - name: "codeBlock", + name: CORE_EXTENSIONS.CODE_BLOCK, addOptions() { return { @@ -118,7 +120,7 @@ export const CodeBlock = Node.create({ toggleCodeBlock: (attributes) => ({ commands }) => - commands.toggleNode(this.name, "paragraph", attributes), + commands.toggleNode(this.name, CORE_EXTENSIONS.PARAGRAPH, attributes), }; }, @@ -126,7 +128,7 @@ export const CodeBlock = Node.create({ return { "Mod-Alt-c": () => this.editor.commands.toggleCodeBlock(), - // remove code block when at start of document or code block is empty + // remove codeBlock when at start of document or codeBlock is empty Backspace: () => { try { const { empty, $anchor } = this.editor.state.selection; @@ -259,7 +261,7 @@ export const CodeBlock = Node.create({ return false; } - if (this.editor.isActive("code")) { + if (this.editor.isActive(CORE_EXTENSIONS.CODE_INLINE)) { // Check if it's an inline code block event.preventDefault(); const text = event.clipboardData.getData("text/plain"); diff --git a/packages/editor/src/core/extensions/code/lowlight-plugin.ts b/packages/editor/src/core/extensions/code/lowlight-plugin.ts index 5ac30c27e..0b8ed71ad 100644 --- a/packages/editor/src/core/extensions/code/lowlight-plugin.ts +++ b/packages/editor/src/core/extensions/code/lowlight-plugin.ts @@ -88,7 +88,7 @@ export function LowlightPlugin({ throw Error("You should provide an instance of lowlight to use the code-block-lowlight extension"); } - const lowlightPlugin: Plugin = new Plugin({ + const lowlightPlugin: Plugin = new Plugin({ key: new PluginKey("lowlight"), state: { diff --git a/packages/editor/src/core/extensions/core-without-props.ts b/packages/editor/src/core/extensions/core-without-props.ts index ed9f5c1a4..a309c2013 100644 --- a/packages/editor/src/core/extensions/core-without-props.ts +++ b/packages/editor/src/core/extensions/core-without-props.ts @@ -3,24 +3,24 @@ import TaskList from "@tiptap/extension-task-list"; import TextStyle from "@tiptap/extension-text-style"; import TiptapUnderline from "@tiptap/extension-underline"; import StarterKit from "@tiptap/starter-kit"; -// extensions // helpers import { isValidHttpUrl } from "@/helpers/common"; +// plane editor imports +import { CoreEditorAdditionalExtensionsWithoutProps } from "@/plane-editor/extensions/core/without-props"; +// extensions +import { CustomCalloutExtensionConfig } from "./callout/extension-config"; import { CustomCodeBlockExtensionWithoutProps } from "./code/without-props"; import { CustomCodeInlineExtension } from "./code-inline"; +import { CustomColorExtension } from "./custom-color"; import { CustomLinkExtension } from "./custom-link"; import { CustomHorizontalRule } from "./horizontal-rule"; import { ImageExtensionWithoutProps } from "./image"; import { CustomImageComponentWithoutProps } from "./image/image-component-without-props"; -import { IssueWidgetWithoutProps } from "./issue-embed/issue-embed-without-props"; import { CustomMentionExtensionConfig } from "./mentions/extension-config"; import { CustomQuoteExtension } from "./quote"; import { TableHeader, TableCell, TableRow, Table } from "./table"; import { CustomTextAlignExtension } from "./text-align"; -import { CustomCalloutExtensionConfig } from "./callout/extension-config"; -import { CustomColorExtension } from "./custom-color"; -// plane editor extensions -import { CoreEditorAdditionalExtensionsWithoutProps } from "@/plane-editor/extensions/core/without-props"; +import { WorkItemEmbedExtensionConfig } from "./work-item-embed/extension-config"; export const CoreEditorExtensionsWithoutProps = [ StarterKit.configure({ @@ -72,12 +72,12 @@ export const CoreEditorExtensionsWithoutProps = [ "text-custom-primary-300 underline underline-offset-[3px] hover:text-custom-primary-500 transition-colors cursor-pointer", }, }), - ImageExtensionWithoutProps().configure({ + ImageExtensionWithoutProps.configure({ HTMLAttributes: { class: "rounded-md", }, }), - CustomImageComponentWithoutProps(), + CustomImageComponentWithoutProps, TiptapUnderline, TextStyle, TaskList.configure({ @@ -104,4 +104,4 @@ export const CoreEditorExtensionsWithoutProps = [ ...CoreEditorAdditionalExtensionsWithoutProps, ]; -export const DocumentEditorExtensionsWithoutProps = [IssueWidgetWithoutProps()]; +export const DocumentEditorExtensionsWithoutProps = [WorkItemEmbedExtensionConfig]; diff --git a/packages/editor/src/core/extensions/custom-code-inline.ts b/packages/editor/src/core/extensions/custom-code-inline.ts deleted file mode 100644 index 3b3cfaab1..000000000 --- a/packages/editor/src/core/extensions/custom-code-inline.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Extension } from "@tiptap/core"; -import codemark from "prosemirror-codemark"; - -export const CustomCodeMarkPlugin = Extension.create({ - name: "codemarkPlugin", - addProseMirrorPlugins() { - return codemark({ markType: this.editor.schema.marks.code }); - }, -}); diff --git a/packages/editor/src/core/extensions/custom-color.ts b/packages/editor/src/core/extensions/custom-color.ts index b377099fb..8b516e8ec 100644 --- a/packages/editor/src/core/extensions/custom-color.ts +++ b/packages/editor/src/core/extensions/custom-color.ts @@ -1,10 +1,11 @@ import { Mark, mergeAttributes } from "@tiptap/core"; // constants import { COLORS_LIST } from "@/constants/common"; +import { CORE_EXTENSIONS } from "@/constants/extension"; declare module "@tiptap/core" { interface Commands { - color: { + [CORE_EXTENSIONS.CUSTOM_COLOR]: { /** * Set the text color * @param {string} color The color to set @@ -34,7 +35,7 @@ declare module "@tiptap/core" { } export const CustomColorExtension = Mark.create({ - name: "customColor", + name: CORE_EXTENSIONS.CUSTOM_COLOR, addOptions() { return { diff --git a/packages/editor/src/core/extensions/custom-image/components/image-node.tsx b/packages/editor/src/core/extensions/custom-image/components/image-node.tsx index e525bc6da..f8bfcf4a1 100644 --- a/packages/editor/src/core/extensions/custom-image/components/image-node.tsx +++ b/packages/editor/src/core/extensions/custom-image/components/image-node.tsx @@ -1,7 +1,10 @@ import { Editor, NodeViewProps, NodeViewWrapper } from "@tiptap/react"; import { useEffect, useRef, useState } from "react"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; // extensions import { CustomImageBlock, CustomImageUploader, ImageAttributes } from "@/extensions/custom-image"; +// helpers import { getExtensionStorage } from "@/helpers/get-extension-storage"; export type CustoBaseImageNodeViewProps = { @@ -77,7 +80,7 @@ export const CustomImageNode = (props: CustomImageNodeProps) => { failedToLoadImage={failedToLoadImage} getPos={getPos} loadImageFromFileSystem={setImageFromFileSystem} - maxFileSize={getExtensionStorage(editor, "imageComponent").maxFileSize} + maxFileSize={getExtensionStorage(editor, CORE_EXTENSIONS.CUSTOM_IMAGE).maxFileSize} node={node} setIsUploaded={setIsUploaded} selected={selected} diff --git a/packages/editor/src/core/extensions/custom-image/components/image-uploader.tsx b/packages/editor/src/core/extensions/custom-image/components/image-uploader.tsx index 0a3ee1a1c..5af4f556d 100644 --- a/packages/editor/src/core/extensions/custom-image/components/image-uploader.tsx +++ b/packages/editor/src/core/extensions/custom-image/components/image-uploader.tsx @@ -4,10 +4,12 @@ import { ChangeEvent, useCallback, useEffect, useMemo, useRef } from "react"; import { cn } from "@plane/utils"; // constants import { ACCEPTED_IMAGE_MIME_TYPES } from "@/constants/config"; +import { CORE_EXTENSIONS } from "@/constants/extension"; // extensions import { CustoBaseImageNodeViewProps, getImageComponentImageFileMap } from "@/extensions/custom-image"; // hooks import { useUploader, useDropZone, uploadFirstFileAndInsertRemaining } from "@/hooks/use-file-upload"; +import { getExtensionStorage } from "@/helpers/get-extension-storage"; type CustomImageUploaderProps = CustoBaseImageNodeViewProps & { maxFileSize: number; @@ -57,7 +59,7 @@ export const CustomImageUploader = (props: CustomImageUploaderProps) => { // control cursor position after upload const nextNode = editor.state.doc.nodeAt(pos + 1); - if (nextNode && nextNode.type.name === "paragraph") { + if (nextNode && nextNode.type.name === CORE_EXTENSIONS.PARAGRAPH) { // If there is a paragraph node after the image component, move the focus to the next node editor.commands.setTextSelection(pos + 1); } else { @@ -75,7 +77,7 @@ export const CustomImageUploader = (props: CustomImageUploaderProps) => { // @ts-expect-error - TODO: fix typings, and don't remove await from here for now editorCommand: async (file) => await editor?.commands.uploadImage(imageEntityId, file), handleProgressStatus: (isUploading) => { - editor.storage.imageComponent.uploadInProgress = isUploading; + getExtensionStorage(editor, CORE_EXTENSIONS.UTILITY).uploadInProgress = isUploading; }, loadFileFromFileSystem: loadImageFromFileSystem, maxFileSize, @@ -85,6 +87,7 @@ export const CustomImageUploader = (props: CustomImageUploaderProps) => { acceptedMimeTypes: ACCEPTED_IMAGE_MIME_TYPES, editor, maxFileSize, + onInvalidFile: (_error, message) => alert(message), pos: getPos(), type: "image", uploader: uploadFile, @@ -123,6 +126,7 @@ export const CustomImageUploader = (props: CustomImageUploaderProps) => { editor, filesList, maxFileSize, + onInvalidFile: (_error, message) => alert(message), pos: getPos(), type: "image", uploader: uploadFile, diff --git a/packages/editor/src/core/extensions/custom-image/components/upload-status.tsx b/packages/editor/src/core/extensions/custom-image/components/upload-status.tsx index 8b71713d2..f88c69c6f 100644 --- a/packages/editor/src/core/extensions/custom-image/components/upload-status.tsx +++ b/packages/editor/src/core/extensions/custom-image/components/upload-status.tsx @@ -1,6 +1,10 @@ import { Editor } from "@tiptap/core"; import { useEditorState } from "@tiptap/react"; import { useEffect, useRef, useState } from "react"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; +// helpers +import { getExtensionStorage } from "@/helpers/get-extension-storage"; type Props = { editor: Editor; @@ -16,7 +20,7 @@ export const ImageUploadStatus: React.FC = (props) => { // subscribe to image upload status const uploadStatus: number | undefined = useEditorState({ editor, - selector: ({ editor }) => editor.storage.imageComponent?.assetsUploadStatus[nodeId], + selector: ({ editor }) => getExtensionStorage(editor, CORE_EXTENSIONS.UTILITY)?.assetsUploadStatus?.[nodeId], }); useEffect(() => { diff --git a/packages/editor/src/core/extensions/custom-image/custom-image.ts b/packages/editor/src/core/extensions/custom-image/custom-image.ts index 11586bf86..afd02fd09 100644 --- a/packages/editor/src/core/extensions/custom-image/custom-image.ts +++ b/packages/editor/src/core/extensions/custom-image/custom-image.ts @@ -1,17 +1,16 @@ import { Editor, mergeAttributes } from "@tiptap/core"; -import { Image } from "@tiptap/extension-image"; +import { Image as BaseImageExtension } from "@tiptap/extension-image"; import { ReactNodeViewRenderer } from "@tiptap/react"; import { v4 as uuidv4 } from "uuid"; // constants import { ACCEPTED_IMAGE_MIME_TYPES } from "@/constants/config"; +import { CORE_EXTENSIONS } from "@/constants/extension"; // extensions import { CustomImageNode } from "@/extensions/custom-image"; // helpers import { isFileValid } from "@/helpers/file"; import { getExtensionStorage } from "@/helpers/get-extension-storage"; import { insertEmptyParagraphAtNodeBoundaries } from "@/helpers/insert-empty-paragraph-at-node-boundary"; -// plugins -import { TrackImageDeletionPlugin, TrackImageRestorationPlugin } from "@/plugins/image"; // types import { TFileHandler } from "@/types"; @@ -23,23 +22,21 @@ export type InsertImageComponentProps = { declare module "@tiptap/core" { interface Commands { - imageComponent: { + [CORE_EXTENSIONS.CUSTOM_IMAGE]: { insertImageComponent: ({ file, pos, event }: InsertImageComponentProps) => ReturnType; uploadImage: (blockId: string, file: File) => () => Promise | undefined; - updateAssetsUploadStatus?: (updatedStatus: TFileHandler["assetsUploadStatus"]) => () => void; getImageSource?: (path: string) => () => Promise; restoreImage: (src: string) => () => Promise; }; } } -export const getImageComponentImageFileMap = (editor: Editor) => getExtensionStorage(editor, "imageComponent")?.fileMap; +export const getImageComponentImageFileMap = (editor: Editor) => + getExtensionStorage(editor, CORE_EXTENSIONS.CUSTOM_IMAGE)?.fileMap; export interface CustomImageExtensionStorage { - assetsUploadStatus: TFileHandler["assetsUploadStatus"]; fileMap: Map; deletedImageSet: Map; - uploadInProgress: boolean; maxFileSize: number; } @@ -47,16 +44,14 @@ export type UploadEntity = ({ event: "insert" } | { event: "drop"; file: File }) export const CustomImageExtension = (props: TFileHandler) => { const { - assetsUploadStatus, getAssetSrc, upload, - delete: deleteImageFn, restore: restoreImageFn, validation: { maxFileSize }, } = props; - return Image.extend, CustomImageExtensionStorage>({ - name: "imageComponent", + return BaseImageExtension.extend, CustomImageExtensionStorage>({ + name: CORE_EXTENSIONS.CUSTOM_IMAGE, selectable: true, group: "block", atom: true, @@ -102,41 +97,15 @@ export const CustomImageExtension = (props: TFileHandler) => { }; }, - addProseMirrorPlugins() { - return [ - TrackImageDeletionPlugin(this.editor, deleteImageFn, this.name), - TrackImageRestorationPlugin(this.editor, restoreImageFn, this.name), - ]; - }, - - onCreate(this) { - const imageSources = new Set(); - this.editor.state.doc.descendants((node) => { - if (node.type.name === this.name) { - if (!node.attrs.src?.startsWith("http")) return; - imageSources.add(node.attrs.src); - } - }); - imageSources.forEach(async (src) => { - try { - await restoreImageFn(src); - } catch (error) { - console.error("Error restoring image: ", error); - } - }); - }, - addStorage() { return { fileMap: new Map(), deletedImageSet: new Map(), - uploadInProgress: false, maxFileSize, // escape markdown for images markdown: { serialize() {}, }, - assetsUploadStatus, }; }, @@ -152,6 +121,7 @@ export const CustomImageExtension = (props: TFileHandler) => { acceptedMimeTypes: ACCEPTED_IMAGE_MIME_TYPES, file: props.file, maxFileSize, + onError: (_error, message) => alert(message), }) ) { return false; @@ -196,9 +166,6 @@ export const CustomImageExtension = (props: TFileHandler) => { const fileUrl = await upload(blockId, file); return fileUrl; }, - updateAssetsUploadStatus: (updatedStatus) => () => { - this.storage.assetsUploadStatus = updatedStatus; - }, getImageSource: (path) => async () => await getAssetSrc(path), restoreImage: (src) => async () => { await restoreImageFn(src); diff --git a/packages/editor/src/core/extensions/custom-image/read-only-custom-image.ts b/packages/editor/src/core/extensions/custom-image/read-only-custom-image.ts index 51b758898..4a85ffd94 100644 --- a/packages/editor/src/core/extensions/custom-image/read-only-custom-image.ts +++ b/packages/editor/src/core/extensions/custom-image/read-only-custom-image.ts @@ -1,6 +1,8 @@ import { mergeAttributes } from "@tiptap/core"; -import { Image } from "@tiptap/extension-image"; +import { Image as BaseImageExtension } from "@tiptap/extension-image"; import { ReactNodeViewRenderer } from "@tiptap/react"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; // components import { CustomImageNode, CustomImageExtensionStorage } from "@/extensions/custom-image"; // types @@ -9,8 +11,8 @@ import { TReadOnlyFileHandler } from "@/types"; export const CustomReadOnlyImageExtension = (props: TReadOnlyFileHandler) => { const { getAssetSrc, restore: restoreImageFn } = props; - return Image.extend, CustomImageExtensionStorage>({ - name: "imageComponent", + return BaseImageExtension.extend, CustomImageExtensionStorage>({ + name: CORE_EXTENSIONS.CUSTOM_IMAGE, selectable: false, group: "block", atom: true, @@ -53,13 +55,11 @@ export const CustomReadOnlyImageExtension = (props: TReadOnlyFileHandler) => { return { fileMap: new Map(), deletedImageSet: new Map(), - uploadInProgress: false, maxFileSize: 0, // escape markdown for images markdown: { serialize() {}, }, - assetsUploadStatus: {}, }; }, diff --git a/packages/editor/src/core/extensions/custom-link/extension.tsx b/packages/editor/src/core/extensions/custom-link/extension.tsx index 27c1bb598..182afc9f8 100644 --- a/packages/editor/src/core/extensions/custom-link/extension.tsx +++ b/packages/editor/src/core/extensions/custom-link/extension.tsx @@ -1,6 +1,9 @@ import { Mark, markPasteRule, mergeAttributes, PasteRuleMatch } from "@tiptap/core"; import { Plugin } from "@tiptap/pm/state"; import { find, registerCustomProtocol, reset } from "linkifyjs"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; +// local imports import { autolink } from "./helpers/autolink"; import { clickHandler } from "./helpers/clickHandler"; import { pasteHandler } from "./helpers/pasteHandler"; @@ -46,7 +49,7 @@ export interface LinkOptions { declare module "@tiptap/core" { interface Commands { - link: { + [CORE_EXTENSIONS.CUSTOM_LINK]: { /** * Set a link mark */ @@ -79,7 +82,7 @@ export type CustomLinkStorage = { }; export const CustomLinkExtension = Mark.create({ - name: "link", + name: CORE_EXTENSIONS.CUSTOM_LINK, priority: 1000, diff --git a/packages/editor/src/core/extensions/custom-link/helpers/clickHandler.ts b/packages/editor/src/core/extensions/custom-link/helpers/clickHandler.ts index 1b084d1ac..72906bc94 100644 --- a/packages/editor/src/core/extensions/custom-link/helpers/clickHandler.ts +++ b/packages/editor/src/core/extensions/custom-link/helpers/clickHandler.ts @@ -16,7 +16,7 @@ export function clickHandler(options: ClickHandlerOptions): Plugin { } let a = event.target as HTMLElement; - const els = []; + const els: HTMLElement[] = []; while (a?.nodeName !== "DIV") { els.push(a); diff --git a/packages/editor/src/core/extensions/custom-list-keymap/list-helpers.ts b/packages/editor/src/core/extensions/custom-list-keymap/list-helpers.ts index 7d4cad17e..547f9f17e 100644 --- a/packages/editor/src/core/extensions/custom-list-keymap/list-helpers.ts +++ b/packages/editor/src/core/extensions/custom-list-keymap/list-helpers.ts @@ -1,12 +1,14 @@ import { Editor, getNodeType, getNodeAtPosition, isAtEndOfNode, isAtStartOfNode, isNodeActive } from "@tiptap/core"; import { Node, NodeType } from "@tiptap/pm/model"; import { EditorState } from "@tiptap/pm/state"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; const findListItemPos = (typeOrName: string | NodeType, state: EditorState) => { const { $from } = state.selection; const nodeType = getNodeType(typeOrName, state.schema); - let currentNode = null; + let currentNode: Node | null = null; let currentDepth = $from.depth; let currentPos = $from.pos; let targetDepth: number | null = null; @@ -72,7 +74,11 @@ const getPrevListDepth = (typeOrName: string, state: EditorState) => { // Traverse up the document structure from the adjusted position for (let d = resolvedPos.depth; d > 0; d--) { const node = resolvedPos.node(d); - if (node.type.name === "bulletList" || node.type.name === "orderedList" || node.type.name === "taskList") { + if ( + [CORE_EXTENSIONS.BULLET_LIST, CORE_EXTENSIONS.ORDERED_LIST, CORE_EXTENSIONS.TASK_LIST].includes( + node.type.name as CORE_EXTENSIONS + ) + ) { // Increment depth for each list ancestor found depth++; } @@ -309,12 +315,12 @@ const isCurrentParagraphASibling = (state: EditorState): boolean => { // Ensure we're in a paragraph and the parent is a list item. if ( - currentParagraphNode.type.name === "paragraph" && - (listItemNode.type.name === "listItem" || listItemNode.type.name === "taskItem") + currentParagraphNode.type.name === CORE_EXTENSIONS.PARAGRAPH && + [CORE_EXTENSIONS.LIST_ITEM, CORE_EXTENSIONS.TASK_ITEM].includes(listItemNode.type.name as CORE_EXTENSIONS) ) { let paragraphNodesCount = 0; listItemNode.forEach((child) => { - if (child.type.name === "paragraph") { + if (child.type.name === CORE_EXTENSIONS.PARAGRAPH) { paragraphNodesCount++; } }); diff --git a/packages/editor/src/core/extensions/custom-list-keymap/list-keymap.ts b/packages/editor/src/core/extensions/custom-list-keymap/list-keymap.ts index 2a17838fd..576888f55 100644 --- a/packages/editor/src/core/extensions/custom-list-keymap/list-keymap.ts +++ b/packages/editor/src/core/extensions/custom-list-keymap/list-keymap.ts @@ -1,4 +1,6 @@ import { Extension } from "@tiptap/core"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; // extensions import { handleBackspace, handleDelete } from "@/extensions/custom-list-keymap/list-helpers"; @@ -31,10 +33,10 @@ export const ListKeymap = ({ tabIndex }: { tabIndex?: number }) => addKeyboardShortcuts() { return { Tab: () => { - if (this.editor.isActive("listItem") || this.editor.isActive("taskItem")) { - if (this.editor.commands.sinkListItem("listItem")) { + if (this.editor.isActive(CORE_EXTENSIONS.LIST_ITEM) || this.editor.isActive(CORE_EXTENSIONS.TASK_ITEM)) { + if (this.editor.commands.sinkListItem(CORE_EXTENSIONS.LIST_ITEM)) { return true; - } else if (this.editor.commands.sinkListItem("taskItem")) { + } else if (this.editor.commands.sinkListItem(CORE_EXTENSIONS.TASK_ITEM)) { return true; } return true; @@ -46,9 +48,9 @@ export const ListKeymap = ({ tabIndex }: { tabIndex?: number }) => return true; }, "Shift-Tab": () => { - if (this.editor.commands.liftListItem("listItem")) { + if (this.editor.commands.liftListItem(CORE_EXTENSIONS.LIST_ITEM)) { return true; - } else if (this.editor.commands.liftListItem("taskItem")) { + } else if (this.editor.commands.liftListItem(CORE_EXTENSIONS.TASK_ITEM)) { return true; } // if tabIndex is set, we don't want to handle Tab key diff --git a/packages/editor/src/core/extensions/drop.ts b/packages/editor/src/core/extensions/drop.ts deleted file mode 100644 index 2a5a994f8..000000000 --- a/packages/editor/src/core/extensions/drop.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { Extension, Editor } from "@tiptap/core"; -import { Plugin, PluginKey } from "@tiptap/pm/state"; -// constants -import { ACCEPTED_ATTACHMENT_MIME_TYPES, ACCEPTED_IMAGE_MIME_TYPES } from "@/constants/config"; -// types -import { TEditorCommands } from "@/types"; - -export const DropHandlerExtension = Extension.create({ - name: "dropHandler", - priority: 1000, - - addProseMirrorPlugins() { - const editor = this.editor; - return [ - new Plugin({ - key: new PluginKey("drop-handler-plugin"), - props: { - handlePaste: (view, event) => { - if ( - editor.isEditable && - event.clipboardData && - event.clipboardData.files && - event.clipboardData.files.length > 0 - ) { - event.preventDefault(); - const files = Array.from(event.clipboardData.files); - const acceptedFiles = files.filter( - (f) => ACCEPTED_IMAGE_MIME_TYPES.includes(f.type) || ACCEPTED_ATTACHMENT_MIME_TYPES.includes(f.type) - ); - - if (acceptedFiles.length) { - const pos = view.state.selection.from; - insertFilesSafely({ - editor, - files: acceptedFiles, - initialPos: pos, - event: "drop", - }); - } - return true; - } - return false; - }, - handleDrop: (view, event, _slice, moved) => { - if ( - editor.isEditable && - !moved && - event.dataTransfer && - event.dataTransfer.files && - event.dataTransfer.files.length > 0 - ) { - event.preventDefault(); - const files = Array.from(event.dataTransfer.files); - const acceptedFiles = files.filter( - (f) => ACCEPTED_IMAGE_MIME_TYPES.includes(f.type) || ACCEPTED_ATTACHMENT_MIME_TYPES.includes(f.type) - ); - - if (acceptedFiles.length) { - const coordinates = view.posAtCoords({ - left: event.clientX, - top: event.clientY, - }); - - if (coordinates) { - const pos = coordinates.pos; - insertFilesSafely({ - editor, - files: acceptedFiles, - initialPos: pos, - event: "drop", - }); - } - return true; - } - } - return false; - }, - }, - }), - ]; - }, -}); - -type InsertFilesSafelyArgs = { - editor: Editor; - event: "insert" | "drop"; - files: File[]; - initialPos: number; - type?: Extract; -}; - -export const insertFilesSafely = async (args: InsertFilesSafelyArgs) => { - const { editor, event, files, initialPos, type } = args; - let pos = initialPos; - - for (const file of files) { - // safe insertion - const docSize = editor.state.doc.content.size; - pos = Math.min(pos, docSize); - - let fileType: "image" | "attachment" | null = null; - - try { - if (type) { - if (["image", "attachment"].includes(type)) fileType = type; - else throw new Error("Wrong file type passed"); - } else { - if (ACCEPTED_IMAGE_MIME_TYPES.includes(file.type)) fileType = "image"; - else if (ACCEPTED_ATTACHMENT_MIME_TYPES.includes(file.type)) fileType = "attachment"; - } - // insert file depending on the type at the current position - if (fileType === "image") { - editor.commands.insertImageComponent({ - file, - pos, - event, - }); - } else if (fileType === "attachment") { - } - } catch (error) { - console.error(`Error while ${event}ing file:`, error); - } - - // Move to the next position - pos += 1; - } -}; diff --git a/packages/editor/src/core/extensions/enter-key-extension.tsx b/packages/editor/src/core/extensions/enter-key.ts similarity index 53% rename from packages/editor/src/core/extensions/enter-key-extension.tsx rename to packages/editor/src/core/extensions/enter-key.ts index d67ceb78b..65119425f 100644 --- a/packages/editor/src/core/extensions/enter-key-extension.tsx +++ b/packages/editor/src/core/extensions/enter-key.ts @@ -1,16 +1,19 @@ import { Extension } from "@tiptap/core"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; +// helpers +import { getExtensionStorage } from "@/helpers/get-extension-storage"; export const EnterKeyExtension = (onEnterKeyPress?: () => void) => Extension.create({ - name: "enterKey", + name: CORE_EXTENSIONS.ENTER_KEY, addKeyboardShortcuts(this) { return { Enter: () => { - if (!this.editor.storage.mentionsOpen) { - if (onEnterKeyPress) { - onEnterKeyPress(); - } + const isMentionOpen = getExtensionStorage(this.editor, CORE_EXTENSIONS.MENTION)?.mentionsOpen; + if (!isMentionOpen) { + onEnterKeyPress?.(); return true; } return false; @@ -18,8 +21,8 @@ export const EnterKeyExtension = (onEnterKeyPress?: () => void) => "Shift-Enter": ({ editor }) => editor.commands.first(({ commands }) => [ () => commands.newlineInCode(), - () => commands.splitListItem("listItem"), - () => commands.splitListItem("taskItem"), + () => commands.splitListItem(CORE_EXTENSIONS.LIST_ITEM), + () => commands.splitListItem(CORE_EXTENSIONS.TASK_ITEM), () => commands.createParagraphNear(), () => commands.liftEmptyBlock(), () => commands.splitBlock(), diff --git a/packages/editor/src/core/extensions/extensions.tsx b/packages/editor/src/core/extensions/extensions.ts similarity index 86% rename from packages/editor/src/core/extensions/extensions.tsx rename to packages/editor/src/core/extensions/extensions.ts index 1ef0a3b15..51969cd5c 100644 --- a/packages/editor/src/core/extensions/extensions.tsx +++ b/packages/editor/src/core/extensions/extensions.ts @@ -7,12 +7,13 @@ import TextStyle from "@tiptap/extension-text-style"; import TiptapUnderline from "@tiptap/extension-underline"; import StarterKit from "@tiptap/starter-kit"; import { Markdown } from "tiptap-markdown"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; // extensions import { CustomCalloutExtension, CustomCodeBlockExtension, CustomCodeInlineExtension, - CustomCodeMarkPlugin, CustomColorExtension, CustomHorizontalRule, CustomImageExtension, @@ -22,17 +23,17 @@ import { CustomQuoteExtension, CustomTextAlignExtension, CustomTypographyExtension, - DropHandlerExtension, ImageExtension, ListKeymap, Table, TableCell, TableHeader, TableRow, - MarkdownClipboard, + UtilityExtension, } from "@/extensions"; // helpers import { isValidHttpUrl } from "@/helpers/common"; +import { getExtensionStorage } from "@/helpers/get-extension-storage"; // plane editor extensions import { CoreEditorAdditionalExtensions } from "@/plane-editor/extensions"; // types @@ -49,7 +50,7 @@ type TArguments = { }; export const CoreEditorExtensions = (args: TArguments): Extensions => { - const { disabledExtensions, enableHistory, fileHandler, mentionHandler, placeholder, tabIndex } = args; + const { disabledExtensions, enableHistory, fileHandler, mentionHandler, placeholder, tabIndex, editable } = args; const extensions = [ StarterKit.configure({ @@ -89,7 +90,6 @@ export const CoreEditorExtensions = (args: TArguments): Extensions => { ...(enableHistory ? {} : { history: false }), }), CustomQuoteExtension, - DropHandlerExtension, CustomHorizontalRule.configure({ HTMLAttributes: { class: "py-4 border-custom-border-400", @@ -127,7 +127,6 @@ export const CoreEditorExtensions = (args: TArguments): Extensions => { class: "", }, }), - CustomCodeMarkPlugin, CustomCodeInlineExtension, Markdown.configure({ html: true, @@ -135,7 +134,6 @@ export const CoreEditorExtensions = (args: TArguments): Extensions => { transformPastedText: true, breaks: true, }), - MarkdownClipboard, Table, TableHeader, TableCell, @@ -145,15 +143,17 @@ export const CoreEditorExtensions = (args: TArguments): Extensions => { placeholder: ({ editor, node }) => { if (!editor.isEditable) return ""; - if (node.type.name === "heading") return `Heading ${node.attrs.level}`; + if (node.type.name === CORE_EXTENSIONS.HEADING) return `Heading ${node.attrs.level}`; - if (editor.storage.imageComponent?.uploadInProgress) return ""; + const isUploadInProgress = getExtensionStorage(editor, CORE_EXTENSIONS.UTILITY)?.uploadInProgress; + + if (isUploadInProgress) return ""; const shouldHidePlaceholder = - editor.isActive("table") || - editor.isActive("codeBlock") || - editor.isActive("image") || - editor.isActive("imageComponent"); + editor.isActive(CORE_EXTENSIONS.TABLE) || + editor.isActive(CORE_EXTENSIONS.CODE_BLOCK) || + editor.isActive(CORE_EXTENSIONS.IMAGE) || + editor.isActive(CORE_EXTENSIONS.CUSTOM_IMAGE); if (shouldHidePlaceholder) return ""; @@ -169,6 +169,10 @@ export const CoreEditorExtensions = (args: TArguments): Extensions => { CharacterCount, CustomTextAlignExtension, CustomCalloutExtension, + UtilityExtension({ + isEditable: editable, + fileHandler, + }), CustomColorExtension, ...CoreEditorAdditionalExtensions({ disabledExtensions, diff --git a/packages/editor/src/core/extensions/headers.ts b/packages/editor/src/core/extensions/headings-list.ts similarity index 86% rename from packages/editor/src/core/extensions/headers.ts rename to packages/editor/src/core/extensions/headings-list.ts index 958cf6ca3..51a9aeedc 100644 --- a/packages/editor/src/core/extensions/headers.ts +++ b/packages/editor/src/core/extensions/headings-list.ts @@ -1,5 +1,7 @@ import { Extension } from "@tiptap/core"; import { Plugin, PluginKey } from "@tiptap/pm/state"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; export interface IMarking { type: "heading"; @@ -12,8 +14,8 @@ export type HeadingExtensionStorage = { headings: IMarking[]; }; -export const HeadingListExtension = Extension.create({ - name: "headingList", +export const HeadingListExtension = Extension.create({ + name: CORE_EXTENSIONS.HEADINGS_LIST, addStorage() { return { diff --git a/packages/editor/src/core/extensions/horizontal-rule.ts b/packages/editor/src/core/extensions/horizontal-rule.ts index b9be1a314..99a5dacc3 100644 --- a/packages/editor/src/core/extensions/horizontal-rule.ts +++ b/packages/editor/src/core/extensions/horizontal-rule.ts @@ -1,5 +1,7 @@ import { isNodeSelection, mergeAttributes, Node, nodeInputRule } from "@tiptap/core"; import { NodeSelection, TextSelection } from "@tiptap/pm/state"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; export interface HorizontalRuleOptions { HTMLAttributes: Record; @@ -7,7 +9,7 @@ export interface HorizontalRuleOptions { declare module "@tiptap/core" { interface Commands { - horizontalRule: { + [CORE_EXTENSIONS.HORIZONTAL_RULE]: { /** * Add a horizontal rule */ @@ -17,7 +19,7 @@ declare module "@tiptap/core" { } export const CustomHorizontalRule = Node.create({ - name: "horizontalRule", + name: CORE_EXTENSIONS.HORIZONTAL_RULE, addOptions() { return { diff --git a/packages/editor/src/core/extensions/image/extension.tsx b/packages/editor/src/core/extensions/image/extension.tsx index 6766b4d0c..12844149c 100644 --- a/packages/editor/src/core/extensions/image/extension.tsx +++ b/packages/editor/src/core/extensions/image/extension.tsx @@ -1,23 +1,23 @@ -import ImageExt from "@tiptap/extension-image"; +import { Image as BaseImageExtension } from "@tiptap/extension-image"; import { ReactNodeViewRenderer } from "@tiptap/react"; // extensions import { CustomImageNode } from "@/extensions"; // helpers import { insertEmptyParagraphAtNodeBoundaries } from "@/helpers/insert-empty-paragraph-at-node-boundary"; -// plugins -import { ImageExtensionStorage, TrackImageDeletionPlugin, TrackImageRestorationPlugin } from "@/plugins/image"; // types import { TFileHandler } from "@/types"; +export type ImageExtensionStorage = { + deletedImageSet: Map; +}; + export const ImageExtension = (fileHandler: TFileHandler) => { const { getAssetSrc, - delete: deleteImageFn, - restore: restoreImageFn, validation: { maxFileSize }, } = fileHandler; - return ImageExt.extend({ + return BaseImageExtension.extend({ addKeyboardShortcuts() { return { ArrowDown: insertEmptyParagraphAtNodeBoundaries("down", this.name), @@ -25,36 +25,10 @@ export const ImageExtension = (fileHandler: TFileHandler) => { }; }, - addProseMirrorPlugins() { - return [ - TrackImageDeletionPlugin(this.editor, deleteImageFn, this.name), - TrackImageRestorationPlugin(this.editor, restoreImageFn, this.name), - ]; - }, - - onCreate(this) { - const imageSources = new Set(); - this.editor.state.doc.descendants((node) => { - if (node.type.name === this.name) { - if (!node.attrs.src?.startsWith("http")) return; - - imageSources.add(node.attrs.src); - } - }); - imageSources.forEach(async (src) => { - try { - await restoreImageFn(src); - } catch (error) { - console.error("Error restoring image: ", error); - } - }); - }, - // storage to keep track of image states Map addStorage() { return { deletedImageSet: new Map(), - uploadInProgress: false, maxFileSize, }; }, diff --git a/packages/editor/src/core/extensions/image/image-component-without-props.tsx b/packages/editor/src/core/extensions/image/image-component-without-props.tsx index c17bcc559..bd2c3f16b 100644 --- a/packages/editor/src/core/extensions/image/image-component-without-props.tsx +++ b/packages/editor/src/core/extensions/image/image-component-without-props.tsx @@ -1,58 +1,56 @@ import { mergeAttributes } from "@tiptap/core"; -import { Image } from "@tiptap/extension-image"; -// extensions -import { ImageExtensionStorage } from "@/plugins/image"; +import { Image as BaseImageExtension } from "@tiptap/extension-image"; +// local imports +import { ImageExtensionStorage } from "./extension"; -export const CustomImageComponentWithoutProps = () => - Image.extend, ImageExtensionStorage>({ - name: "imageComponent", - selectable: true, - group: "block", - atom: true, - draggable: true, +export const CustomImageComponentWithoutProps = BaseImageExtension.extend< + Record, + ImageExtensionStorage +>({ + name: "imageComponent", + selectable: true, + group: "block", + atom: true, + draggable: true, - addAttributes() { - return { - ...this.parent?.(), - width: { - default: "35%", - }, - src: { - default: null, - }, - height: { - default: "auto", - }, - ["id"]: { - default: null, - }, - aspectRatio: { - default: null, - }, - }; - }, + addAttributes() { + return { + ...this.parent?.(), + width: { + default: "35%", + }, + src: { + default: null, + }, + height: { + default: "auto", + }, + ["id"]: { + default: null, + }, + aspectRatio: { + default: null, + }, + }; + }, - parseHTML() { - return [ - { - tag: "image-component", - }, - ]; - }, + parseHTML() { + return [ + { + tag: "image-component", + }, + ]; + }, - renderHTML({ HTMLAttributes }) { - return ["image-component", mergeAttributes(HTMLAttributes)]; - }, + renderHTML({ HTMLAttributes }) { + return ["image-component", mergeAttributes(HTMLAttributes)]; + }, - addStorage() { - return { - fileMap: new Map(), - deletedImageSet: new Map(), - uploadInProgress: false, - maxFileSize: 0, - assetsUploadStatus: {}, - }; - }, - }); - -export default CustomImageComponentWithoutProps; + addStorage() { + return { + fileMap: new Map(), + deletedImageSet: new Map(), + maxFileSize: 0, + }; + }, +}); diff --git a/packages/editor/src/core/extensions/image/image-extension-without-props.tsx b/packages/editor/src/core/extensions/image/image-extension-without-props.tsx index bb6c5b4ad..ba064bef4 100644 --- a/packages/editor/src/core/extensions/image/image-extension-without-props.tsx +++ b/packages/editor/src/core/extensions/image/image-extension-without-props.tsx @@ -1,19 +1,18 @@ -import ImageExt from "@tiptap/extension-image"; +import { Image as BaseImageExtension } from "@tiptap/extension-image"; -export const ImageExtensionWithoutProps = () => - ImageExt.extend({ - addAttributes() { - return { - ...this.parent?.(), - width: { - default: "35%", - }, - height: { - default: null, - }, - aspectRatio: { - default: null, - }, - }; - }, - }); +export const ImageExtensionWithoutProps = BaseImageExtension.extend({ + addAttributes() { + return { + ...this.parent?.(), + width: { + default: "35%", + }, + height: { + default: null, + }, + aspectRatio: { + default: null, + }, + }; + }, +}); diff --git a/packages/editor/src/core/extensions/image/read-only-image.tsx b/packages/editor/src/core/extensions/image/read-only-image.tsx index a65607803..271c39fd8 100644 --- a/packages/editor/src/core/extensions/image/read-only-image.tsx +++ b/packages/editor/src/core/extensions/image/read-only-image.tsx @@ -1,4 +1,4 @@ -import Image from "@tiptap/extension-image"; +import { Image as BaseImageExtension } from "@tiptap/extension-image"; import { ReactNodeViewRenderer } from "@tiptap/react"; // extensions import { CustomImageNode } from "@/extensions"; @@ -8,7 +8,7 @@ import { TReadOnlyFileHandler } from "@/types"; export const ReadOnlyImageExtension = (props: TReadOnlyFileHandler) => { const { getAssetSrc } = props; - return Image.extend({ + return BaseImageExtension.extend({ addAttributes() { return { ...this.parent?.(), diff --git a/packages/editor/src/core/extensions/index.ts b/packages/editor/src/core/extensions/index.ts index e98607585..3c3232885 100644 --- a/packages/editor/src/core/extensions/index.ts +++ b/packages/editor/src/core/extensions/index.ts @@ -5,22 +5,20 @@ export * from "./custom-image"; export * from "./custom-link"; export * from "./custom-list-keymap"; export * from "./image"; -export * from "./issue-embed"; export * from "./mentions"; export * from "./slash-commands"; export * from "./table"; export * from "./typography"; +export * from "./work-item-embed"; export * from "./core-without-props"; -export * from "./custom-code-inline"; export * from "./custom-color"; -export * from "./drop"; -export * from "./enter-key-extension"; +export * from "./enter-key"; export * from "./extensions"; -export * from "./headers"; +export * from "./headings-list"; export * from "./horizontal-rule"; export * from "./keymap"; export * from "./quote"; export * from "./read-only-extensions"; export * from "./side-menu"; export * from "./text-align"; -export * from "./clipboard"; +export * from "./utility"; diff --git a/packages/editor/src/core/extensions/issue-embed/index.ts b/packages/editor/src/core/extensions/issue-embed/index.ts deleted file mode 100644 index f47619a03..000000000 --- a/packages/editor/src/core/extensions/issue-embed/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./widget-node"; -export * from "./issue-embed-without-props"; diff --git a/packages/editor/src/core/extensions/issue-embed/issue-embed-without-props.ts b/packages/editor/src/core/extensions/issue-embed/issue-embed-without-props.ts deleted file mode 100644 index bef366cba..000000000 --- a/packages/editor/src/core/extensions/issue-embed/issue-embed-without-props.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { mergeAttributes, Node } from "@tiptap/core"; - -export const IssueWidgetWithoutProps = () => - Node.create({ - name: "issue-embed-component", - group: "block", - atom: true, - selectable: true, - draggable: true, - - addAttributes() { - return { - entity_identifier: { - default: undefined, - }, - project_identifier: { - default: undefined, - }, - workspace_identifier: { - default: undefined, - }, - id: { - default: undefined, - }, - entity_name: { - default: undefined, - }, - }; - }, - - parseHTML() { - return [ - { - tag: "issue-embed-component", - }, - ]; - }, - renderHTML({ HTMLAttributes }) { - return ["issue-embed-component", mergeAttributes(HTMLAttributes)]; - }, - }); diff --git a/packages/editor/src/core/extensions/issue-embed/widget-node.tsx b/packages/editor/src/core/extensions/issue-embed/widget-node.tsx deleted file mode 100644 index a216ab6d9..000000000 --- a/packages/editor/src/core/extensions/issue-embed/widget-node.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { mergeAttributes, Node } from "@tiptap/core"; -import { ReactNodeViewRenderer, NodeViewWrapper } from "@tiptap/react"; - -type Props = { - widgetCallback: ({ - issueId, - projectId, - workspaceSlug, - }: { - issueId: string; - projectId: string | undefined; - workspaceSlug: string | undefined; - }) => React.ReactNode; -}; - -export const IssueWidget = (props: Props) => - Node.create({ - name: "issue-embed-component", - group: "block", - atom: true, - selectable: true, - draggable: true, - - addAttributes() { - return { - entity_identifier: { - default: undefined, - }, - project_identifier: { - default: undefined, - }, - workspace_identifier: { - default: undefined, - }, - id: { - default: undefined, - }, - entity_name: { - default: undefined, - }, - }; - }, - - addNodeView() { - return ReactNodeViewRenderer((issueProps: any) => ( - - {props.widgetCallback({ - issueId: issueProps.node.attrs.entity_identifier, - projectId: issueProps.node.attrs.project_identifier, - workspaceSlug: issueProps.node.attrs.workspace_identifier, - })} - - )); - }, - - parseHTML() { - return [ - { - tag: "issue-embed-component", - }, - ]; - }, - renderHTML({ HTMLAttributes }) { - return ["issue-embed-component", mergeAttributes(HTMLAttributes)]; - }, - }); diff --git a/packages/editor/src/core/extensions/keymap.tsx b/packages/editor/src/core/extensions/keymap.ts similarity index 92% rename from packages/editor/src/core/extensions/keymap.tsx rename to packages/editor/src/core/extensions/keymap.ts index 81d60e34f..a4961bb96 100644 --- a/packages/editor/src/core/extensions/keymap.tsx +++ b/packages/editor/src/core/extensions/keymap.ts @@ -2,11 +2,13 @@ import { Extension } from "@tiptap/core"; import { NodeType } from "@tiptap/pm/model"; import { Plugin, PluginKey, Transaction } from "@tiptap/pm/state"; import { canJoin } from "@tiptap/pm/transform"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; declare module "@tiptap/core" { // eslint-disable-next-line no-unused-vars interface Commands { - customkeymap: { + customKeymap: { /** * Select text between node boundaries */ @@ -59,7 +61,7 @@ function autoJoin(tr: Transaction, newTr: Transaction, nodeTypes: NodeType[]) { } export const CustomKeymap = Extension.create({ - name: "CustomKeymap", + name: "customKeymap", addCommands() { return { @@ -87,9 +89,9 @@ export const CustomKeymap = Extension.create({ const newTr = newState.tr; const joinableNodes = [ - newState.schema.nodes["orderedList"], - newState.schema.nodes["taskList"], - newState.schema.nodes["bulletList"], + newState.schema.nodes[CORE_EXTENSIONS.ORDERED_LIST], + newState.schema.nodes[CORE_EXTENSIONS.TASK_LIST], + newState.schema.nodes[CORE_EXTENSIONS.BULLET_LIST], ]; let joined = false; diff --git a/packages/editor/src/core/extensions/mentions/mention-node-view.tsx b/packages/editor/src/core/extensions/mentions/mention-node-view.tsx index 006336fbb..aac00de88 100644 --- a/packages/editor/src/core/extensions/mentions/mention-node-view.tsx +++ b/packages/editor/src/core/extensions/mentions/mention-node-view.tsx @@ -18,7 +18,7 @@ export const MentionNodeView = (props: Props) => { return ( {(extension.options as TMentionExtensionOptions).renderComponent({ - entity_identifier: attrs[EMentionComponentAttributeNames.ENTITY_IDENTIFIER], + entity_identifier: attrs[EMentionComponentAttributeNames.ENTITY_IDENTIFIER] ?? "", entity_name: attrs[EMentionComponentAttributeNames.ENTITY_NAME] ?? "user_mention", })} diff --git a/packages/editor/src/core/extensions/mentions/mentions-list-dropdown.tsx b/packages/editor/src/core/extensions/mentions/mentions-list-dropdown.tsx index 4f09ed2ae..da11d0f99 100644 --- a/packages/editor/src/core/extensions/mentions/mentions-list-dropdown.tsx +++ b/packages/editor/src/core/extensions/mentions/mentions-list-dropdown.tsx @@ -1,7 +1,7 @@ "use client"; -import { forwardRef, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useRef, useState } from "react"; import { Editor } from "@tiptap/react"; +import { forwardRef, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useRef, useState } from "react"; import { v4 as uuidv4 } from "uuid"; // plane utils import { cn } from "@plane/utils"; @@ -61,7 +61,9 @@ export const MentionsListDropdown = forwardRef((props: MentionsListDropdownProps sections, selectedIndex, }); - setSelectedIndex(newIndex); + if (newIndex) { + setSelectedIndex(newIndex); + } }, })); @@ -79,7 +81,9 @@ export const MentionsListDropdown = forwardRef((props: MentionsListDropdownProps setIsLoading(true); try { const sectionsResponse = await searchCallback?.(query); - setSections(sectionsResponse); + if (sectionsResponse) { + setSections(sectionsResponse); + } } catch (error) { console.error("Failed to fetch suggestions:", error); } finally { diff --git a/packages/editor/src/core/extensions/mentions/utils.ts b/packages/editor/src/core/extensions/mentions/utils.ts index e8e7ed4b7..5a7550c83 100644 --- a/packages/editor/src/core/extensions/mentions/utils.ts +++ b/packages/editor/src/core/extensions/mentions/utils.ts @@ -1,7 +1,7 @@ import { Editor } from "@tiptap/core"; -import { SuggestionOptions } from "@tiptap/suggestion"; import { ReactRenderer } from "@tiptap/react"; -import tippy from "tippy.js"; +import { SuggestionOptions } from "@tiptap/suggestion"; +import tippy, { Instance } from "tippy.js"; // helpers import { CommandListInstance } from "@/helpers/tippy"; // types @@ -15,7 +15,7 @@ export const renderMentionsDropdown = () => { const { searchCallback } = props; let component: ReactRenderer | null = null; - let popup: any | null = null; + let popup: Instance | null = null; return { onStart: (props: { editor: Editor; clientRect: DOMRect }) => { diff --git a/packages/editor/src/core/extensions/quote.tsx b/packages/editor/src/core/extensions/quote.ts similarity index 85% rename from packages/editor/src/core/extensions/quote.tsx rename to packages/editor/src/core/extensions/quote.ts index 4ae81ffe4..99a6c10f0 100644 --- a/packages/editor/src/core/extensions/quote.tsx +++ b/packages/editor/src/core/extensions/quote.ts @@ -1,4 +1,6 @@ import Blockquote from "@tiptap/extension-blockquote"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; export const CustomQuoteExtension = Blockquote.extend({ addKeyboardShortcuts() { @@ -10,7 +12,7 @@ export const CustomQuoteExtension = Blockquote.extend({ if (!parent) return false; - if (parent.type.name !== "blockquote") { + if (parent.type.name !== CORE_EXTENSIONS.BLOCKQUOTE) { return false; } if ($from.pos !== $to.pos) return false; diff --git a/packages/editor/src/core/extensions/read-only-extensions.tsx b/packages/editor/src/core/extensions/read-only-extensions.ts similarity index 97% rename from packages/editor/src/core/extensions/read-only-extensions.tsx rename to packages/editor/src/core/extensions/read-only-extensions.ts index 3881c548b..bcfc76411 100644 --- a/packages/editor/src/core/extensions/read-only-extensions.tsx +++ b/packages/editor/src/core/extensions/read-only-extensions.ts @@ -24,7 +24,7 @@ import { CustomTextAlignExtension, CustomCalloutReadOnlyExtension, CustomColorExtension, - MarkdownClipboard, + UtilityExtension, } from "@/extensions"; // helpers import { isValidHttpUrl } from "@/helpers/common"; @@ -117,7 +117,6 @@ export const CoreReadOnlyEditorExtensions = (props: Props): Extensions => { html: true, transformCopiedText: false, }), - MarkdownClipboard, Table, TableHeader, TableCell, @@ -127,6 +126,10 @@ export const CoreReadOnlyEditorExtensions = (props: Props): Extensions => { CustomColorExtension, CustomTextAlignExtension, CustomCalloutReadOnlyExtension, + UtilityExtension({ + isEditable: false, + fileHandler, + }), ...CoreReadOnlyEditorAdditionalExtensions({ disabledExtensions, }), diff --git a/packages/editor/src/core/extensions/side-menu.tsx b/packages/editor/src/core/extensions/side-menu.ts similarity index 97% rename from packages/editor/src/core/extensions/side-menu.tsx rename to packages/editor/src/core/extensions/side-menu.ts index 5f11286b5..34e3c45e5 100644 --- a/packages/editor/src/core/extensions/side-menu.tsx +++ b/packages/editor/src/core/extensions/side-menu.ts @@ -1,6 +1,8 @@ import { Extension } from "@tiptap/core"; import { Plugin, PluginKey } from "@tiptap/pm/state"; import { EditorView } from "@tiptap/pm/view"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; // plugins import { AIHandlePlugin } from "@/plugins/ai-handle"; import { DragHandlePlugin, nodeDOMAtCoords } from "@/plugins/drag-handle"; @@ -33,7 +35,7 @@ export const SideMenuExtension = (props: Props) => { const { aiEnabled, dragDropEnabled } = props; return Extension.create({ - name: "editorSideMenu", + name: CORE_EXTENSIONS.SIDE_MENU, addProseMirrorPlugins() { return [ SideMenu({ diff --git a/packages/editor/src/core/extensions/slash-commands/command-items-list.tsx b/packages/editor/src/core/extensions/slash-commands/command-items-list.tsx index 9fcc733ae..fe9ec06a6 100644 --- a/packages/editor/src/core/extensions/slash-commands/command-items-list.tsx +++ b/packages/editor/src/core/extensions/slash-commands/command-items-list.tsx @@ -26,22 +26,17 @@ import { toggleBulletList, toggleOrderedList, toggleTaskList, - toggleHeadingOne, - toggleHeadingTwo, - toggleHeadingThree, - toggleHeadingFour, - toggleHeadingFive, - toggleHeadingSix, + toggleHeading, toggleTextColor, toggleBackgroundColor, insertImage, insertCallout, setText, } from "@/helpers/editor-commands"; -// types -import { CommandProps, ISlashCommandItem, TSlashCommandSectionKeys } from "@/types"; // plane editor extensions import { coreEditorAdditionalSlashCommandOptions } from "@/plane-editor/extensions"; +// types +import { CommandProps, ISlashCommandItem, TSlashCommandSectionKeys } from "@/types"; // local types import { TExtensionProps, TSlashCommandAdditionalOption } from "./root"; @@ -75,7 +70,7 @@ export const getSlashCommandFilteredSections = description: "Big section heading.", searchTerms: ["title", "big", "large"], icon: , - command: ({ editor, range }) => toggleHeadingOne(editor, range), + command: ({ editor, range }) => toggleHeading(editor, 1, range), }, { commandKey: "h2", @@ -84,7 +79,7 @@ export const getSlashCommandFilteredSections = description: "Medium section heading.", searchTerms: ["subtitle", "medium"], icon: , - command: ({ editor, range }) => toggleHeadingTwo(editor, range), + command: ({ editor, range }) => toggleHeading(editor, 2, range), }, { commandKey: "h3", @@ -93,7 +88,7 @@ export const getSlashCommandFilteredSections = description: "Small section heading.", searchTerms: ["subtitle", "small"], icon: , - command: ({ editor, range }) => toggleHeadingThree(editor, range), + command: ({ editor, range }) => toggleHeading(editor, 3, range), }, { commandKey: "h4", @@ -102,7 +97,7 @@ export const getSlashCommandFilteredSections = description: "Small section heading.", searchTerms: ["subtitle", "small"], icon: , - command: ({ editor, range }) => toggleHeadingFour(editor, range), + command: ({ editor, range }) => toggleHeading(editor, 4, range), }, { commandKey: "h5", @@ -111,7 +106,7 @@ export const getSlashCommandFilteredSections = description: "Small section heading.", searchTerms: ["subtitle", "small"], icon: , - command: ({ editor, range }) => toggleHeadingFive(editor, range), + command: ({ editor, range }) => toggleHeading(editor, 5, range), }, { commandKey: "h6", @@ -120,7 +115,7 @@ export const getSlashCommandFilteredSections = description: "Small section heading.", searchTerms: ["subtitle", "small"], icon: , - command: ({ editor, range }) => toggleHeadingSix(editor, range), + command: ({ editor, range }) => toggleHeading(editor, 6, range), }, { commandKey: "to-do-list", diff --git a/packages/editor/src/core/extensions/slash-commands/command-menu.tsx b/packages/editor/src/core/extensions/slash-commands/command-menu.tsx index 4ecd3f8fa..9d85266f2 100644 --- a/packages/editor/src/core/extensions/slash-commands/command-menu.tsx +++ b/packages/editor/src/core/extensions/slash-commands/command-menu.tsx @@ -1,15 +1,16 @@ -import { forwardRef, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useRef, useState } from "react"; import { Editor } from "@tiptap/core"; +import { forwardRef, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useRef, useState } from "react"; // helpers import { DROPDOWN_NAVIGATION_KEYS, getNextValidIndex } from "@/helpers/tippy"; // components +import { ISlashCommandItem } from "@/types"; import { TSlashCommandSection } from "./command-items-list"; import { CommandMenuItem } from "./command-menu-item"; export type SlashCommandsMenuProps = { editor: Editor; items: TSlashCommandSection[]; - command: any; + command: (item: ISlashCommandItem) => void; }; export const SlashCommandsMenu = forwardRef((props: SlashCommandsMenuProps, ref) => { @@ -103,7 +104,9 @@ export const SlashCommandsMenu = forwardRef((props: SlashCommandsMenuProps, ref) sections, selectedIndex, }); - setSelectedIndex(newIndex); + if (newIndex) { + setSelectedIndex(newIndex); + } }, })); diff --git a/packages/editor/src/core/extensions/slash-commands/root.tsx b/packages/editor/src/core/extensions/slash-commands/root.tsx index c0c078a2d..828149d50 100644 --- a/packages/editor/src/core/extensions/slash-commands/root.tsx +++ b/packages/editor/src/core/extensions/slash-commands/root.tsx @@ -1,7 +1,9 @@ import { Editor, Range, Extension } from "@tiptap/core"; import { ReactRenderer } from "@tiptap/react"; import Suggestion, { SuggestionOptions } from "@tiptap/suggestion"; -import tippy from "tippy.js"; +import tippy, { Instance } from "tippy.js"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; // helpers import { CommandListInstance } from "@/helpers/tippy"; // types @@ -20,7 +22,7 @@ export type TSlashCommandAdditionalOption = ISlashCommandItem & { }; const Command = Extension.create({ - name: "slash-command", + name: CORE_EXTENSIONS.SLASH_COMMANDS, addOptions() { return { suggestion: { @@ -34,11 +36,11 @@ const Command = Extension.create({ const parentNode = selection.$from.node(selection.$from.depth); const blockType = parentNode.type.name; - if (blockType === "codeBlock") { + if (blockType === CORE_EXTENSIONS.CODE_BLOCK) { return false; } - if (editor.isActive("table")) { + if (editor.isActive(CORE_EXTENSIONS.TABLE)) { return false; } @@ -59,7 +61,7 @@ const Command = Extension.create({ const renderItems = () => { let component: ReactRenderer | null = null; - let popup: any | null = null; + let popup: Instance | null = null; return { onStart: (props: { editor: Editor; clientRect?: (() => DOMRect | null) | null }) => { component = new ReactRenderer(SlashCommandsMenu, { diff --git a/packages/editor/src/core/extensions/table/table-cell/table-cell.ts b/packages/editor/src/core/extensions/table/table-cell.ts similarity index 91% rename from packages/editor/src/core/extensions/table/table-cell/table-cell.ts rename to packages/editor/src/core/extensions/table/table-cell.ts index 403bd3f02..2ba06845a 100644 --- a/packages/editor/src/core/extensions/table/table-cell/table-cell.ts +++ b/packages/editor/src/core/extensions/table/table-cell.ts @@ -1,11 +1,12 @@ import { mergeAttributes, Node } from "@tiptap/core"; - +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; export interface TableCellOptions { HTMLAttributes: Record; } export const TableCell = Node.create({ - name: "tableCell", + name: CORE_EXTENSIONS.TABLE_CELL, addOptions() { return { diff --git a/packages/editor/src/core/extensions/table/table-cell/index.ts b/packages/editor/src/core/extensions/table/table-cell/index.ts deleted file mode 100644 index 68a25a9c3..000000000 --- a/packages/editor/src/core/extensions/table/table-cell/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { TableCell } from "./table-cell"; diff --git a/packages/editor/src/core/extensions/table/table-header/table-header.ts b/packages/editor/src/core/extensions/table/table-header.ts similarity index 90% rename from packages/editor/src/core/extensions/table/table-header/table-header.ts rename to packages/editor/src/core/extensions/table/table-header.ts index bd994f467..491889eef 100644 --- a/packages/editor/src/core/extensions/table/table-header/table-header.ts +++ b/packages/editor/src/core/extensions/table/table-header.ts @@ -1,11 +1,12 @@ import { mergeAttributes, Node } from "@tiptap/core"; - +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; export interface TableHeaderOptions { HTMLAttributes: Record; } export const TableHeader = Node.create({ - name: "tableHeader", + name: CORE_EXTENSIONS.TABLE_HEADER, addOptions() { return { diff --git a/packages/editor/src/core/extensions/table/table-header/index.ts b/packages/editor/src/core/extensions/table/table-header/index.ts deleted file mode 100644 index 290f37d0b..000000000 --- a/packages/editor/src/core/extensions/table/table-header/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { TableHeader } from "./table-header"; diff --git a/packages/editor/src/core/extensions/table/table-row/table-row.ts b/packages/editor/src/core/extensions/table/table-row.ts similarity index 88% rename from packages/editor/src/core/extensions/table/table-row/table-row.ts rename to packages/editor/src/core/extensions/table/table-row.ts index f961c0582..48f95a41c 100644 --- a/packages/editor/src/core/extensions/table/table-row/table-row.ts +++ b/packages/editor/src/core/extensions/table/table-row.ts @@ -1,11 +1,13 @@ import { mergeAttributes, Node } from "@tiptap/core"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; export interface TableRowOptions { HTMLAttributes: Record; } export const TableRow = Node.create({ - name: "tableRow", + name: CORE_EXTENSIONS.TABLE_ROW, addOptions() { return { diff --git a/packages/editor/src/core/extensions/table/table-row/index.ts b/packages/editor/src/core/extensions/table/table-row/index.ts deleted file mode 100644 index 24dafb7e0..000000000 --- a/packages/editor/src/core/extensions/table/table-row/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { TableRow } from "./table-row"; diff --git a/packages/editor/src/core/extensions/table/table/table-controls.ts b/packages/editor/src/core/extensions/table/table/table-controls.ts index 052922579..d499b1b6a 100644 --- a/packages/editor/src/core/extensions/table/table/table-controls.ts +++ b/packages/editor/src/core/extensions/table/table/table-controls.ts @@ -1,6 +1,8 @@ import { findParentNode } from "@tiptap/core"; -import { Plugin, PluginKey, TextSelection } from "@tiptap/pm/state"; +import { Plugin, PluginKey, TextSelection, Transaction } from "@tiptap/pm/state"; import { DecorationSet, Decoration } from "@tiptap/pm/view"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; const key = new PluginKey("tableControls"); @@ -17,16 +19,14 @@ export function tableControls() { }, props: { handleTripleClickOn(view, pos, node, nodePos, event, direct) { - if (node.type.name === 'tableCell') { + if (node.type.name === CORE_EXTENSIONS.TABLE_CELL) { event.preventDefault(); const $pos = view.state.doc.resolve(pos); const line = $pos.parent; const linePos = $pos.start(); const start = linePos; const end = linePos + line.nodeSize - 1; - const tr = view.state.tr.setSelection( - TextSelection.create(view.state.doc, start, end) - ); + const tr = view.state.tr.setSelection(TextSelection.create(view.state.doc, start, end)); view.dispatch(tr); return true; } @@ -52,12 +52,12 @@ export function tableControls() { if (!pos || pos.pos < 0 || pos.pos > view.state.doc.content.size) return; - const table = findParentNode((node) => node.type.name === "table")( - TextSelection.create(view.state.doc, pos.pos) - ); - const cell = findParentNode((node) => node.type.name === "tableCell" || node.type.name === "tableHeader")( + const table = findParentNode((node) => node.type.name === CORE_EXTENSIONS.TABLE)( TextSelection.create(view.state.doc, pos.pos) ); + const cell = findParentNode((node) => + [CORE_EXTENSIONS.TABLE_CELL, CORE_EXTENSIONS.TABLE_HEADER].includes(node.type.name as CORE_EXTENSIONS) + )(TextSelection.create(view.state.doc, pos.pos)); if (!table || !cell) return; @@ -112,7 +112,7 @@ class TableControlsState { }; } - apply(tr: any) { + apply(tr: Transaction) { const actions = tr.getMeta(key); if (actions?.setHoveredTable !== undefined) { diff --git a/packages/editor/src/core/extensions/table/table/table-view.tsx b/packages/editor/src/core/extensions/table/table/table-view.tsx index 2a4802126..f78d964ed 100644 --- a/packages/editor/src/core/extensions/table/table/table-view.tsx +++ b/packages/editor/src/core/extensions/table/table/table-view.tsx @@ -1,12 +1,12 @@ -import { h } from "jsx-dom-cjs"; -import { Node as ProseMirrorNode, ResolvedPos } from "@tiptap/pm/model"; -import { Decoration, NodeView } from "@tiptap/pm/view"; -import tippy, { Instance, Props } from "tippy.js"; - import { Editor } from "@tiptap/core"; +import { Node as ProseMirrorNode, ResolvedPos } from "@tiptap/pm/model"; import { CellSelection, TableMap, updateColumnsOnResize } from "@tiptap/pm/tables"; - +import { Decoration, NodeView } from "@tiptap/pm/view"; +import { h } from "jsx-dom-cjs"; import { icons } from "src/core/extensions/table/table/icons"; +import tippy, { Instance, Props } from "tippy.js"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; type ToolboxItem = { label: string; @@ -30,10 +30,10 @@ export function updateColumns( if (!row) return; for (let i = 0, col = 0; i < row.childCount; i += 1) { - const { colspan, colwidth } = row.child(i).attrs; + const { colspan, colWidth } = row.child(i).attrs; for (let j = 0; j < colspan; j += 1, col += 1) { - const hasWidth = overrideCol === col ? overrideValue : colwidth && colwidth[j]; + const hasWidth = overrideCol === col ? overrideValue : colWidth && colWidth[j]; const cssWidth = hasWidth ? `${hasWidth}px` : ""; totalWidth += hasWidth || cellMinWidth; @@ -85,7 +85,7 @@ function setCellsBackgroundColor(editor: Editor, color: { backgroundColor: strin return editor .chain() .focus() - .updateAttributes("tableCell", { + .updateAttributes(CORE_EXTENSIONS.TABLE_CELL, { background: color.backgroundColor, textColor: color.textColor, }) @@ -104,12 +104,12 @@ function setTableRowBackgroundColor(editor: Editor, color: { backgroundColor: st // Find the depth of the table row node let rowDepth = hoveredCell.depth; - while (rowDepth > 0 && hoveredCell.node(rowDepth).type.name !== "tableRow") { + while (rowDepth > 0 && hoveredCell.node(rowDepth).type.name !== CORE_EXTENSIONS.TABLE_ROW) { rowDepth--; } // If we couldn't find a tableRow node, we can't set the background color - if (hoveredCell.node(rowDepth).type.name !== "tableRow") { + if (hoveredCell.node(rowDepth).type.name !== CORE_EXTENSIONS.TABLE_ROW) { return false; } diff --git a/packages/editor/src/core/extensions/table/table/table.ts b/packages/editor/src/core/extensions/table/table/table.ts index fd775d211..4810706b3 100644 --- a/packages/editor/src/core/extensions/table/table/table.ts +++ b/packages/editor/src/core/extensions/table/table/table.ts @@ -19,11 +19,14 @@ import { toggleHeader, toggleHeaderCell, } from "@tiptap/pm/tables"; - -import { tableControls } from "@/extensions/table/table/table-controls"; -import { TableView } from "@/extensions/table/table/table-view"; -import { createTable } from "@/extensions/table/table/utilities/create-table"; -import { deleteTableWhenAllCellsSelected } from "@/extensions/table/table/utilities/delete-table-when-all-cells-selected"; +import { Decoration } from "@tiptap/pm/view"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; +// local imports +import { tableControls } from "./table-controls"; +import { TableView } from "./table-view"; +import { createTable } from "./utilities/create-table"; +import { deleteTableWhenAllCellsSelected } from "./utilities/delete-table-when-all-cells-selected"; import { insertLineAboveTableAction } from "./utilities/insert-line-above-table-action"; import { insertLineBelowTableAction } from "./utilities/insert-line-below-table-action"; @@ -38,7 +41,7 @@ export interface TableOptions { declare module "@tiptap/core" { interface Commands { - table: { + [CORE_EXTENSIONS.TABLE]: { insertTable: (options?: { rows?: number; cols?: number; @@ -79,7 +82,7 @@ declare module "@tiptap/core" { } export const Table = Node.create({ - name: "table", + name: CORE_EXTENSIONS.TABLE, addOptions() { return { @@ -219,8 +222,8 @@ export const Table = Node.create({ addKeyboardShortcuts() { return { Tab: () => { - if (this.editor.isActive("table")) { - if (this.editor.isActive("listItem") || this.editor.isActive("taskItem")) { + if (this.editor.isActive(CORE_EXTENSIONS.TABLE)) { + if (this.editor.isActive(CORE_EXTENSIONS.LIST_ITEM) || this.editor.isActive(CORE_EXTENSIONS.TASK_ITEM)) { return false; } if (this.editor.commands.goToNextCell()) { @@ -249,7 +252,7 @@ export const Table = Node.create({ return ({ editor, getPos, node, decorations }) => { const { cellMinWidth } = this.options; - return new TableView(node, cellMinWidth, decorations as any, editor, getPos as () => number); + return new TableView(node, cellMinWidth, decorations as Decoration[], editor, getPos as () => number); }; }, diff --git a/packages/editor/src/core/extensions/table/table/utilities/delete-table-when-all-cells-selected.ts b/packages/editor/src/core/extensions/table/table/utilities/delete-table-when-all-cells-selected.ts index 53388fbf2..5c84b8617 100644 --- a/packages/editor/src/core/extensions/table/table/utilities/delete-table-when-all-cells-selected.ts +++ b/packages/editor/src/core/extensions/table/table/utilities/delete-table-when-all-cells-selected.ts @@ -1,4 +1,6 @@ import { findParentNodeClosestToPos, KeyboardShortcutCommand } from "@tiptap/core"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; // extensions import { isCellSelection } from "@/extensions/table/table/utilities/is-cell-selection"; @@ -10,14 +12,17 @@ export const deleteTableWhenAllCellsSelected: KeyboardShortcutCommand = ({ edito } let cellCount = 0; - const table = findParentNodeClosestToPos(selection.ranges[0].$from, (node) => node.type.name === "table"); + const table = findParentNodeClosestToPos( + selection.ranges[0].$from, + (node) => node.type.name === CORE_EXTENSIONS.TABLE + ); table?.node.descendants((node) => { - if (node.type.name === "table") { + if (node.type.name === CORE_EXTENSIONS.TABLE) { return false; } - if (["tableCell", "tableHeader"].includes(node.type.name)) { + if ([CORE_EXTENSIONS.TABLE_CELL, CORE_EXTENSIONS.TABLE_HEADER].includes(node.type.name as CORE_EXTENSIONS)) { cellCount += 1; } }); diff --git a/packages/editor/src/core/extensions/table/table/utilities/insert-line-above-table-action.ts b/packages/editor/src/core/extensions/table/table/utilities/insert-line-above-table-action.ts index ca5ed3d7e..35c2ee3c7 100644 --- a/packages/editor/src/core/extensions/table/table/utilities/insert-line-above-table-action.ts +++ b/packages/editor/src/core/extensions/table/table/utilities/insert-line-above-table-action.ts @@ -1,17 +1,19 @@ import { KeyboardShortcutCommand } from "@tiptap/core"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; // helpers import { findParentNodeOfType } from "@/helpers/common"; export const insertLineAboveTableAction: KeyboardShortcutCommand = ({ editor }) => { // Check if the current selection or the closest node is a table - if (!editor.isActive("table")) return false; + if (!editor.isActive(CORE_EXTENSIONS.TABLE)) return false; try { // Get the current selection const { selection } = editor.state; // Find the table node and its position - const tableNode = findParentNodeOfType(selection, "table"); + const tableNode = findParentNodeOfType(selection, CORE_EXTENSIONS.TABLE); if (!tableNode) return false; const tablePos = tableNode.pos; @@ -39,7 +41,7 @@ export const insertLineAboveTableAction: KeyboardShortcutCommand = ({ editor }) const prevNode = editor.state.doc.nodeAt(prevNodePos - 1); - if (prevNode && prevNode.type.name === "paragraph") { + if (prevNode && prevNode.type.name === CORE_EXTENSIONS.PARAGRAPH) { // If there's a paragraph before the table, move the cursor to the end of that paragraph const endOfParagraphPos = tablePos - prevNode.nodeSize; editor.chain().setTextSelection(endOfParagraphPos).run(); diff --git a/packages/editor/src/core/extensions/table/table/utilities/insert-line-below-table-action.ts b/packages/editor/src/core/extensions/table/table/utilities/insert-line-below-table-action.ts index 7edca9f30..6c26e22a2 100644 --- a/packages/editor/src/core/extensions/table/table/utilities/insert-line-below-table-action.ts +++ b/packages/editor/src/core/extensions/table/table/utilities/insert-line-below-table-action.ts @@ -1,17 +1,19 @@ import { KeyboardShortcutCommand } from "@tiptap/core"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; // helpers import { findParentNodeOfType } from "@/helpers/common"; export const insertLineBelowTableAction: KeyboardShortcutCommand = ({ editor }) => { // Check if the current selection or the closest node is a table - if (!editor.isActive("table")) return false; + if (!editor.isActive(CORE_EXTENSIONS.TABLE)) return false; try { // Get the current selection const { selection } = editor.state; // Find the table node and its position - const tableNode = findParentNodeOfType(selection, "table"); + const tableNode = findParentNodeOfType(selection, CORE_EXTENSIONS.TABLE); if (!tableNode) return false; const tablePos = tableNode.pos; @@ -31,13 +33,13 @@ export const insertLineBelowTableAction: KeyboardShortcutCommand = ({ editor }) // Check for an existing node immediately after the table const nextNode = editor.state.doc.nodeAt(nextNodePos); - if (nextNode && nextNode.type.name === "paragraph") { + if (nextNode && nextNode.type.name === CORE_EXTENSIONS.PARAGRAPH) { // If the next node is an paragraph, move the cursor there const endOfParagraphPos = nextNodePos + nextNode.nodeSize - 1; editor.chain().setTextSelection(endOfParagraphPos).run(); } else if (!nextNode) { // If the next node doesn't exist i.e. we're at the end of the document, create and insert a new empty node there - editor.chain().insertContentAt(nextNodePos, { type: "paragraph" }).run(); + editor.chain().insertContentAt(nextNodePos, { type: CORE_EXTENSIONS.PARAGRAPH }).run(); editor .chain() .setTextSelection(nextNodePos + 1) diff --git a/packages/editor/src/core/extensions/typography/index.ts b/packages/editor/src/core/extensions/typography/index.ts index 6b736953b..32ffea6a2 100644 --- a/packages/editor/src/core/extensions/typography/index.ts +++ b/packages/editor/src/core/extensions/typography/index.ts @@ -1,4 +1,6 @@ import { Extension, InputRule } from "@tiptap/core"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; import { TypographyOptions, emDash, @@ -23,7 +25,7 @@ import { } from "./inputRules"; export const CustomTypographyExtension = Extension.create({ - name: "typography", + name: CORE_EXTENSIONS.TYPOGRAPHY, addInputRules() { const rules: InputRule[] = []; diff --git a/packages/editor/src/core/extensions/utility.ts b/packages/editor/src/core/extensions/utility.ts new file mode 100644 index 000000000..1d656de5a --- /dev/null +++ b/packages/editor/src/core/extensions/utility.ts @@ -0,0 +1,71 @@ +import { Extension } from "@tiptap/core"; +// prosemirror plugins +import codemark from "prosemirror-codemark"; +// helpers +import { restorePublicImages } from "@/helpers/image-helpers"; +// plugins +import { DropHandlerPlugin } from "@/plugins/drop"; +import { FilePlugins } from "@/plugins/file/root"; +import { MarkdownClipboardPlugin } from "@/plugins/markdown-clipboard"; +// types +import { TFileHandler, TReadOnlyFileHandler } from "@/types"; + +declare module "@tiptap/core" { + interface Commands { + utility: { + updateAssetsUploadStatus: (updatedStatus: TFileHandler["assetsUploadStatus"]) => () => void; + }; + } +} + +export interface UtilityExtensionStorage { + assetsUploadStatus: TFileHandler["assetsUploadStatus"]; + uploadInProgress: boolean; +} + +type Props = { + fileHandler: TFileHandler | TReadOnlyFileHandler; + isEditable: boolean; +}; + +export const UtilityExtension = (props: Props) => { + const { fileHandler, isEditable } = props; + const { restore: restoreImageFn } = fileHandler; + + return Extension.create, UtilityExtensionStorage>({ + name: "utility", + priority: 1000, + + addProseMirrorPlugins() { + return [ + ...FilePlugins({ + editor: this.editor, + isEditable, + fileHandler, + }), + ...codemark({ markType: this.editor.schema.marks.code }), + MarkdownClipboardPlugin(this.editor), + DropHandlerPlugin(this.editor), + ]; + }, + + onCreate() { + restorePublicImages(this.editor, restoreImageFn); + }, + + addStorage() { + return { + assetsUploadStatus: isEditable && "assetsUploadStatus" in fileHandler ? fileHandler.assetsUploadStatus : {}, + uploadInProgress: false, + }; + }, + + addCommands() { + return { + updateAssetsUploadStatus: (updatedStatus) => () => { + this.storage.assetsUploadStatus = updatedStatus; + }, + }; + }, + }); +}; diff --git a/packages/editor/src/core/extensions/work-item-embed/extension-config.ts b/packages/editor/src/core/extensions/work-item-embed/extension-config.ts new file mode 100644 index 000000000..0ea25c770 --- /dev/null +++ b/packages/editor/src/core/extensions/work-item-embed/extension-config.ts @@ -0,0 +1,43 @@ +import { mergeAttributes, Node } from "@tiptap/core"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; + +export const WorkItemEmbedExtensionConfig = Node.create({ + name: CORE_EXTENSIONS.WORK_ITEM_EMBED, + group: "block", + atom: true, + selectable: true, + draggable: true, + + addAttributes() { + return { + entity_identifier: { + default: undefined, + }, + project_identifier: { + default: undefined, + }, + workspace_identifier: { + default: undefined, + }, + id: { + default: undefined, + }, + entity_name: { + default: undefined, + }, + }; + }, + + parseHTML() { + return [ + { + tag: "issue-embed-component", + }, + ]; + }, + + renderHTML({ HTMLAttributes }) { + return ["issue-embed-component", mergeAttributes(HTMLAttributes)]; + }, +}); diff --git a/packages/editor/src/core/extensions/work-item-embed/extension.tsx b/packages/editor/src/core/extensions/work-item-embed/extension.tsx new file mode 100644 index 000000000..64e655a40 --- /dev/null +++ b/packages/editor/src/core/extensions/work-item-embed/extension.tsx @@ -0,0 +1,30 @@ +import { ReactNodeViewRenderer, NodeViewWrapper } from "@tiptap/react"; +// local imports +import { WorkItemEmbedExtensionConfig } from "./extension-config"; + +type Props = { + widgetCallback: ({ + issueId, + projectId, + workspaceSlug, + }: { + issueId: string; + projectId: string | undefined; + workspaceSlug: string | undefined; + }) => React.ReactNode; +}; + +export const WorkItemEmbedExtension = (props: Props) => + WorkItemEmbedExtensionConfig.extend({ + addNodeView() { + return ReactNodeViewRenderer((issueProps: any) => ( + + {props.widgetCallback({ + issueId: issueProps.node.attrs.entity_identifier, + projectId: issueProps.node.attrs.project_identifier, + workspaceSlug: issueProps.node.attrs.workspace_identifier, + })} + + )); + }, + }); diff --git a/packages/editor/src/core/extensions/work-item-embed/index.ts b/packages/editor/src/core/extensions/work-item-embed/index.ts new file mode 100644 index 000000000..2ce32da8b --- /dev/null +++ b/packages/editor/src/core/extensions/work-item-embed/index.ts @@ -0,0 +1 @@ +export * from "./extension"; diff --git a/packages/editor/src/core/helpers/common.ts b/packages/editor/src/core/helpers/common.ts index 974b111d0..e694e1e85 100644 --- a/packages/editor/src/core/helpers/common.ts +++ b/packages/editor/src/core/helpers/common.ts @@ -1,6 +1,8 @@ import { EditorState, Selection } from "@tiptap/pm/state"; -// plane utils +// plane imports import { cn } from "@plane/utils"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; interface EditorClassNames { noBorder?: boolean; @@ -67,7 +69,7 @@ export const isValidHttpUrl = (string: string): { isValid: boolean; url: string url: string, }; } - } catch (_) { + } catch { // Original string wasn't a valid URL - that's okay, we'll try with https } @@ -79,7 +81,7 @@ export const isValidHttpUrl = (string: string): { isValid: boolean; url: string isValid: true, url: urlWithHttps, }; - } catch (_) { + } catch { return { isValid: false, url: string, @@ -91,7 +93,7 @@ export const getParagraphCount = (editorState: EditorState | undefined) => { if (!editorState) return 0; let paragraphCount = 0; editorState.doc.descendants((node) => { - if (node.type.name === "paragraph" && node.content.size > 0) paragraphCount++; + if (node.type.name === CORE_EXTENSIONS.PARAGRAPH && node.content.size > 0) paragraphCount++; }); return paragraphCount; }; diff --git a/packages/editor/src/core/helpers/editor-commands.ts b/packages/editor/src/core/helpers/editor-commands.ts index e8c98ada5..5fa15cb08 100644 --- a/packages/editor/src/core/helpers/editor-commands.ts +++ b/packages/editor/src/core/helpers/editor-commands.ts @@ -1,4 +1,6 @@ import { Editor, Range } from "@tiptap/core"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; // extensions import { InsertImageComponentProps } from "@/extensions"; import { replaceCodeWithText } from "@/extensions/code/utils/replace-code-block-with-text"; @@ -6,44 +8,14 @@ import { replaceCodeWithText } from "@/extensions/code/utils/replace-code-block- import { findTableAncestor } from "@/helpers/common"; export const setText = (editor: Editor, range?: Range) => { - if (range) editor.chain().focus().deleteRange(range).setNode("paragraph").run(); - else editor.chain().focus().setNode("paragraph").run(); + if (range) editor.chain().focus().deleteRange(range).setNode(CORE_EXTENSIONS.PARAGRAPH).run(); + else editor.chain().focus().setNode(CORE_EXTENSIONS.PARAGRAPH).run(); }; -export const toggleHeadingOne = (editor: Editor, range?: Range) => { - if (range) editor.chain().focus().deleteRange(range).setNode("heading", { level: 1 }).run(); +export const toggleHeading = (editor: Editor, level: 1 | 2 | 3 | 4 | 5 | 6, range?: Range) => { + if (range) editor.chain().focus().deleteRange(range).setNode(CORE_EXTENSIONS.HEADING, { level }).run(); // @ts-expect-error tiptap types are incorrect - else editor.chain().focus().toggleHeading({ level: 1 }).run(); -}; - -export const toggleHeadingTwo = (editor: Editor, range?: Range) => { - if (range) editor.chain().focus().deleteRange(range).setNode("heading", { level: 2 }).run(); - // @ts-expect-error tiptap types are incorrect - else editor.chain().focus().toggleHeading({ level: 2 }).run(); -}; - -export const toggleHeadingThree = (editor: Editor, range?: Range) => { - if (range) editor.chain().focus().deleteRange(range).setNode("heading", { level: 3 }).run(); - // @ts-expect-error tiptap types are incorrect - else editor.chain().focus().toggleHeading({ level: 3 }).run(); -}; - -export const toggleHeadingFour = (editor: Editor, range?: Range) => { - if (range) editor.chain().focus().deleteRange(range).setNode("heading", { level: 4 }).run(); - // @ts-expect-error tiptap types are incorrect - else editor.chain().focus().toggleHeading({ level: 4 }).run(); -}; - -export const toggleHeadingFive = (editor: Editor, range?: Range) => { - if (range) editor.chain().focus().deleteRange(range).setNode("heading", { level: 5 }).run(); - // @ts-expect-error tiptap types are incorrect - else editor.chain().focus().toggleHeading({ level: 5 }).run(); -}; - -export const toggleHeadingSix = (editor: Editor, range?: Range) => { - if (range) editor.chain().focus().deleteRange(range).setNode("heading", { level: 6 }).run(); - // @ts-expect-error tiptap types are incorrect - else editor.chain().focus().toggleHeading({ level: 6 }).run(); + else editor.chain().focus().toggleHeading({ level }).run(); }; export const toggleBold = (editor: Editor, range?: Range) => { @@ -68,7 +40,7 @@ export const toggleUnderline = (editor: Editor, range?: Range) => { export const toggleCodeBlock = (editor: Editor, range?: Range) => { try { // if it's a code block, replace it with the code with paragraphs - if (editor.isActive("codeBlock")) { + if (editor.isActive(CORE_EXTENSIONS.CODE_BLOCK)) { replaceCodeWithText(editor); return; } @@ -77,12 +49,12 @@ export const toggleCodeBlock = (editor: Editor, range?: Range) => { const text = editor.state.doc.textBetween(from, to, "\n"); const isMultiline = text.includes("\n"); - // if the selection is not a range i.e. empty, then simply convert it into a code block + // if the selection is not a range i.e. empty, then simply convert it into a codeBlock if (editor.state.selection.empty) { editor.chain().focus().toggleCodeBlock().run(); } else if (isMultiline) { // if the selection is multiline, then also replace the text content with - // a code block + // a codeBlock editor.chain().focus().deleteRange({ from, to }).insertContentAt(from, `\`\`\`\n${text}\n\`\`\``).run(); } else { // if the selection is single line, then simply convert it into inline diff --git a/packages/editor/src/core/helpers/file.ts b/packages/editor/src/core/helpers/file.ts index f2c9968f0..33d3c7d78 100644 --- a/packages/editor/src/core/helpers/file.ts +++ b/packages/editor/src/core/helpers/file.ts @@ -1,24 +1,34 @@ +export enum EFileError { + INVALID_FILE_TYPE = "INVALID_FILE_TYPE", + FILE_SIZE_TOO_LARGE = "FILE_SIZE_TOO_LARGE", + NO_FILE_SELECTED = "NO_FILE_SELECTED", +} + type TArgs = { acceptedMimeTypes: string[]; file: File; maxFileSize: number; + onError: (error: EFileError, message: string) => void; }; export const isFileValid = (args: TArgs): boolean => { - const { acceptedMimeTypes, file, maxFileSize } = args; + const { acceptedMimeTypes, file, maxFileSize, onError } = args; if (!file) { - alert("No file selected. Please select a file to upload."); + onError(EFileError.NO_FILE_SELECTED, "No file selected. Please select a file to upload."); return false; } if (!acceptedMimeTypes.includes(file.type)) { - alert("Invalid file type. Please select a JPEG, JPG, PNG, WEBP or GIF file."); + onError(EFileError.INVALID_FILE_TYPE, "Invalid file type."); return false; } if (file.size > maxFileSize) { - alert(`File size too large. Please select a file smaller than ${maxFileSize / 1024 / 1024}MB.`); + onError( + EFileError.FILE_SIZE_TOO_LARGE, + `File size too large. Please select a file smaller than ${maxFileSize / 1024 / 1024}MB.` + ); return false; } diff --git a/packages/editor/src/core/helpers/image-helpers.ts b/packages/editor/src/core/helpers/image-helpers.ts new file mode 100644 index 000000000..9fcb877f9 --- /dev/null +++ b/packages/editor/src/core/helpers/image-helpers.ts @@ -0,0 +1,32 @@ +import { Editor } from "@tiptap/core"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; +// types +import { TFileHandler } from "@/types"; + +/** + * Finds all public image nodes in the document and restores them using the provided restore function + * + * Never remove this onCreate hook, it's a hack to restore old public + * images, since they don't give error if they've been deleted as they are + * rendered directly from image source instead of going through the + * apiserver + */ +export const restorePublicImages = (editor: Editor, restoreImageFn: TFileHandler["restore"]) => { + const imageSources = new Set(); + editor.state.doc.descendants((node) => { + if ([CORE_EXTENSIONS.IMAGE, CORE_EXTENSIONS.CUSTOM_IMAGE].includes(node.type.name as CORE_EXTENSIONS)) { + if (!node.attrs.src?.startsWith("http")) return; + + imageSources.add(node.attrs.src); + } + }); + + imageSources.forEach(async (src) => { + try { + await restoreImageFn(src); + } catch (error) { + console.error("Error restoring image: ", error); + } + }); +}; diff --git a/packages/editor/src/core/helpers/insert-empty-paragraph-at-node-boundary.ts b/packages/editor/src/core/helpers/insert-empty-paragraph-at-node-boundary.ts index ffad88d4e..b9449b494 100644 --- a/packages/editor/src/core/helpers/insert-empty-paragraph-at-node-boundary.ts +++ b/packages/editor/src/core/helpers/insert-empty-paragraph-at-node-boundary.ts @@ -1,5 +1,7 @@ import { KeyboardShortcutCommand } from "@tiptap/core"; import { Node as ProseMirrorNode } from "@tiptap/pm/model"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; type Direction = "up" | "down"; @@ -39,13 +41,13 @@ export const insertEmptyParagraphAtNodeBoundaries: ( if (insertPosUp === 0) { // If at the very start of the document, insert a new paragraph at the start - editor.chain().insertContentAt(insertPosUp, { type: "paragraph" }).run(); + editor.chain().insertContentAt(insertPosUp, { type: CORE_EXTENSIONS.PARAGRAPH }).run(); editor.chain().setTextSelection(insertPosUp).run(); // Set the cursor to the new paragraph } else { // Otherwise, check the node immediately before the target node const prevNode = doc.nodeAt(insertPosUp - 1); - if (prevNode && prevNode.type.name === "paragraph") { + if (prevNode && prevNode.type.name === CORE_EXTENSIONS.PARAGRAPH) { // If the previous node is a paragraph, move the cursor there editor .chain() @@ -67,13 +69,13 @@ export const insertEmptyParagraphAtNodeBoundaries: ( // Check the node immediately after the target node const nextNode = doc.nodeAt(insertPosDown); - if (nextNode && nextNode.type.name === "paragraph") { + if (nextNode && nextNode.type.name === CORE_EXTENSIONS.PARAGRAPH) { // If the next node is a paragraph, move the cursor to the end of it const endOfParagraphPos = insertPosDown + nextNode.nodeSize - 1; editor.chain().setTextSelection(endOfParagraphPos).run(); } else if (!nextNode) { // If there is no next node (end of document), insert a new paragraph - editor.chain().insertContentAt(insertPosDown, { type: "paragraph" }).run(); + editor.chain().insertContentAt(insertPosDown, { type: CORE_EXTENSIONS.PARAGRAPH }).run(); editor .chain() .setTextSelection(insertPosDown + 1) diff --git a/packages/editor/src/core/hooks/use-collaborative-editor.ts b/packages/editor/src/core/hooks/use-collaborative-editor.ts index 4abf7d6d1..8677b29ed 100644 --- a/packages/editor/src/core/hooks/use-collaborative-editor.ts +++ b/packages/editor/src/core/hooks/use-collaborative-editor.ts @@ -1,6 +1,6 @@ -import { useEffect, useMemo, useState } from "react"; import { HocuspocusProvider } from "@hocuspocus/provider"; import Collaboration from "@tiptap/extension-collaboration"; +import { useEffect, useMemo, useState } from "react"; import { IndexeddbPersistence } from "y-indexeddb"; // extensions import { HeadingListExtension, SideMenuExtension } from "@/extensions"; diff --git a/packages/editor/src/core/hooks/use-editor.ts b/packages/editor/src/core/hooks/use-editor.ts index cf9d04d83..a0cd73915 100644 --- a/packages/editor/src/core/hooks/use-editor.ts +++ b/packages/editor/src/core/hooks/use-editor.ts @@ -6,10 +6,13 @@ import { useImperativeHandle, MutableRefObject, useEffect } from "react"; import * as Y from "yjs"; // components import { getEditorMenuItems } from "@/components/menus"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; // extensions import { CoreEditorExtensions } from "@/extensions"; // helpers import { getParagraphCount } from "@/helpers/common"; +import { getExtensionStorage } from "@/helpers/get-extension-storage"; import { insertContentAtSavedSelection } from "@/helpers/insert-content-at-cursor-position"; import { IMarking, scrollSummary, scrollToNodeViaDOMCoordinates } from "@/helpers/scroll-to-node"; // props @@ -23,6 +26,7 @@ import type { TExtensions, TMentionHandler, } from "@/types"; +import { CORE_EDITOR_META } from "@/constants/meta"; export interface CustomEditorProps { editable: boolean; @@ -111,16 +115,19 @@ export const useEditor = (props: CustomEditorProps) => { // value is null when intentionally passed where syncing is not yet // supported and value is undefined when the data from swr is not populated if (value == null) return; - if (editor && !editor.isDestroyed && !editor.storage.imageComponent?.uploadInProgress) { - try { - editor.commands.setContent(value, false, { preserveWhitespace: "full" }); - if (editor.state.selection) { - const docLength = editor.state.doc.content.size; - const relativePosition = Math.min(editor.state.selection.from, docLength - 1); - editor.commands.setTextSelection(relativePosition); + if (editor) { + const isUploadInProgress = getExtensionStorage(editor, CORE_EXTENSIONS.UTILITY)?.uploadInProgress; + if (!editor.isDestroyed && !isUploadInProgress) { + try { + editor.commands.setContent(value, false, { preserveWhitespace: "full" }); + if (editor.state.selection) { + const docLength = editor.state.doc.content.size; + const relativePosition = Math.min(editor.state.selection.from, docLength - 1); + editor.commands.setTextSelection(relativePosition); + } + } catch (error) { + console.error("Error syncing editor content with external value:", error); } - } catch (error) { - console.error("Error syncing editor content with external value:", error); } } }, [editor, value, id]); @@ -143,7 +150,7 @@ export const useEditor = (props: CustomEditorProps) => { }, getCurrentCursorPosition: () => editor?.state.selection.from, clearEditor: (emitUpdate = false) => { - editor?.chain().setMeta("skipImageDeletion", true).clearContent(emitUpdate).run(); + editor?.chain().setMeta(CORE_EDITOR_META.SKIP_FILE_DELETION, true).clearContent(emitUpdate).run(); }, setEditorValue: (content: string, emitUpdate = false) => { editor?.commands.setContent(content, emitUpdate, { preserveWhitespace: "full" }); @@ -179,7 +186,10 @@ export const useEditor = (props: CustomEditorProps) => { onHeadingChange: (callback: (headings: IMarking[]) => void) => { // Subscribe to update event emitted from headers extension editor?.on("update", () => { - callback(editor?.storage.headingList.headings); + const headings = getExtensionStorage(editor, CORE_EXTENSIONS.HEADINGS_LIST)?.headings; + if (headings) { + callback(headings); + } }); // Return a function to unsubscribe to the continuous transactions of // the editor on unmounting the component that has subscribed to this @@ -188,7 +198,7 @@ export const useEditor = (props: CustomEditorProps) => { editor?.off("update"); }; }, - getHeadings: () => editor?.storage.headingList.headings, + getHeadings: () => (editor ? getExtensionStorage(editor, CORE_EXTENSIONS.HEADINGS_LIST)?.headings : []), onStateChange: (callback: () => void) => { // Subscribe to editor state changes editor?.on("transaction", () => { @@ -221,7 +231,8 @@ export const useEditor = (props: CustomEditorProps) => { if (!editor) return; scrollSummary(editor, marking); }, - isEditorReadyToDiscard: () => editor?.storage.imageComponent?.uploadInProgress === false, + isEditorReadyToDiscard: () => + !!editor && getExtensionStorage(editor, CORE_EXTENSIONS.UTILITY)?.uploadInProgress === false, setFocusAtPosition: (position: number) => { if (!editor || editor.isDestroyed) { console.error("Editor reference is not available or has been destroyed."); @@ -232,7 +243,7 @@ export const useEditor = (props: CustomEditorProps) => { const safePosition = Math.max(0, Math.min(position, docSize)); editor .chain() - .insertContentAt(safePosition, [{ type: "paragraph" }]) + .insertContentAt(safePosition, [{ type: CORE_EXTENSIONS.PARAGRAPH }]) .focus() .run(); } catch (error) { diff --git a/packages/editor/src/core/hooks/use-file-upload.ts b/packages/editor/src/core/hooks/use-file-upload.ts index b707824f2..e40c15913 100644 --- a/packages/editor/src/core/hooks/use-file-upload.ts +++ b/packages/editor/src/core/hooks/use-file-upload.ts @@ -1,9 +1,9 @@ import { Editor } from "@tiptap/core"; import { DragEvent, useCallback, useEffect, useState } from "react"; -// extensions -import { insertFilesSafely } from "@/extensions/drop"; +// helpers +import { EFileError, isFileValid } from "@/helpers/file"; // plugins -import { isFileValid } from "@/helpers/file"; +import { insertFilesSafely } from "@/plugins/drop"; // types import { TEditorCommands } from "@/types"; @@ -13,12 +13,20 @@ type TUploaderArgs = { handleProgressStatus?: (isUploading: boolean) => void; loadFileFromFileSystem?: (file: string) => void; maxFileSize: number; + onInvalidFile: (error: EFileError, message: string) => void; onUpload: (url: string, file: File) => void; }; export const useUploader = (args: TUploaderArgs) => { - const { acceptedMimeTypes, editorCommand, handleProgressStatus, loadFileFromFileSystem, maxFileSize, onUpload } = - args; + const { + acceptedMimeTypes, + editorCommand, + handleProgressStatus, + loadFileFromFileSystem, + maxFileSize, + onInvalidFile, + onUpload, + } = args; // states const [isUploading, setIsUploading] = useState(false); @@ -30,6 +38,7 @@ export const useUploader = (args: TUploaderArgs) => { acceptedMimeTypes, file, maxFileSize, + onError: onInvalidFile, }); if (!isValid) { handleProgressStatus?.(false); @@ -75,13 +84,14 @@ type TDropzoneArgs = { acceptedMimeTypes: string[]; editor: Editor; maxFileSize: number; + onInvalidFile: (error: EFileError, message: string) => void; pos: number; type: Extract; uploader: (file: File) => Promise; }; export const useDropZone = (args: TDropzoneArgs) => { - const { acceptedMimeTypes, editor, maxFileSize, pos, type, uploader } = args; + const { acceptedMimeTypes, editor, maxFileSize, onInvalidFile, pos, type, uploader } = args; // states const [isDragging, setIsDragging] = useState(false); const [draggedInside, setDraggedInside] = useState(false); @@ -117,12 +127,13 @@ export const useDropZone = (args: TDropzoneArgs) => { editor, filesList, maxFileSize, + onInvalidFile, pos, type, uploader, }); }, - [acceptedMimeTypes, editor, maxFileSize, pos, type, uploader] + [acceptedMimeTypes, editor, maxFileSize, onInvalidFile, pos, type, uploader] ); const onDragEnter = useCallback(() => setDraggedInside(true), []); const onDragLeave = useCallback(() => setDraggedInside(false), []); @@ -141,6 +152,7 @@ type TMultipleFileArgs = { editor: Editor; filesList: FileList; maxFileSize: number; + onInvalidFile: (error: EFileError, message: string) => void; pos: number; type: Extract; uploader: (file: File) => Promise; @@ -148,7 +160,7 @@ type TMultipleFileArgs = { // Upload the first file and insert the remaining ones for uploading multiple files export const uploadFirstFileAndInsertRemaining = async (args: TMultipleFileArgs) => { - const { acceptedMimeTypes, editor, filesList, maxFileSize, pos, type, uploader } = args; + const { acceptedMimeTypes, editor, filesList, maxFileSize, onInvalidFile, pos, type, uploader } = args; const filteredFiles: File[] = []; for (let i = 0; i < filesList.length; i += 1) { const file = filesList.item(i); @@ -158,6 +170,7 @@ export const uploadFirstFileAndInsertRemaining = async (args: TMultipleFileArgs) acceptedMimeTypes, file, maxFileSize, + onError: onInvalidFile, }) ) { filteredFiles.push(file); diff --git a/packages/editor/src/core/hooks/use-read-only-editor.ts b/packages/editor/src/core/hooks/use-read-only-editor.ts index b50b56b02..5bd731d5f 100644 --- a/packages/editor/src/core/hooks/use-read-only-editor.ts +++ b/packages/editor/src/core/hooks/use-read-only-editor.ts @@ -12,6 +12,7 @@ import { IMarking, scrollSummary } from "@/helpers/scroll-to-node"; import { CoreReadOnlyEditorProps } from "@/props"; // types import type { EditorReadOnlyRefApi, TExtensions, TReadOnlyFileHandler, TReadOnlyMentionHandler } from "@/types"; +import { CORE_EDITOR_META } from "@/constants/meta"; interface CustomReadOnlyEditorProps { disabledExtensions: TExtensions[]; @@ -75,7 +76,7 @@ export const useReadOnlyEditor = (props: CustomReadOnlyEditorProps) => { useImperativeHandle(forwardedRef, () => ({ clearEditor: (emitUpdate = false) => { - editor?.chain().setMeta("skipImageDeletion", true).clearContent(emitUpdate).run(); + editor?.chain().setMeta(CORE_EDITOR_META.SKIP_FILE_DELETION, true).clearContent(emitUpdate).run(); }, setEditorValue: (content: string, emitUpdate = false) => { editor?.commands.setContent(content, emitUpdate, { preserveWhitespace: "full" }); diff --git a/packages/editor/src/core/plugins/drag-handle.ts b/packages/editor/src/core/plugins/drag-handle.ts index f9a60a48c..aa00fa32d 100644 --- a/packages/editor/src/core/plugins/drag-handle.ts +++ b/packages/editor/src/core/plugins/drag-handle.ts @@ -2,6 +2,8 @@ import { Fragment, Slice, Node, Schema } from "@tiptap/pm/model"; import { NodeSelection } from "@tiptap/pm/state"; // @ts-expect-error __serializeForClipboard's is not exported import { __serializeForClipboard, EditorView } from "@tiptap/pm/view"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; // extensions import { SideMenuHandleOptions, SideMenuPluginProps } from "@/extensions"; @@ -132,7 +134,7 @@ export const DragHandlePlugin = (options: SideMenuPluginProps): SideMenuHandleOp let listType = ""; let isDragging = false; let lastClientY = 0; - let scrollAnimationFrame = null; + let scrollAnimationFrame: number | null = null; let isDraggedOutsideWindow: "top" | "bottom" | boolean = false; let isMouseInsideWhileDragging = false; let currentScrollSpeed = 0; @@ -142,8 +144,10 @@ export const DragHandlePlugin = (options: SideMenuPluginProps): SideMenuHandleOp }; const handleDragStart = (event: DragEvent, view: EditorView) => { - const { listType: listTypeFromDragStart } = handleNodeSelection(event, view, true, options); - listType = listTypeFromDragStart; + const { listType: listTypeFromDragStart } = handleNodeSelection(event, view, true, options) ?? {}; + if (listTypeFromDragStart) { + listType = listTypeFromDragStart; + } isDragging = true; lastClientY = event.clientY; scroll(); @@ -297,7 +301,7 @@ export const DragHandlePlugin = (options: SideMenuPluginProps): SideMenuHandleOp // Traverse up the document tree to find if we're inside a list item for (let i = resolvedPos.depth; i > 0; i--) { - if (resolvedPos.node(i).type.name === "listItem") { + if (resolvedPos.node(i).type.name === CORE_EXTENSIONS.LIST_ITEM) { isDroppedInsideList = true; dropDepth = i; break; @@ -305,7 +309,7 @@ export const DragHandlePlugin = (options: SideMenuPluginProps): SideMenuHandleOp } // Handle nested list items and task items - if (droppedNode.type.name === "listItem") { + if (droppedNode.type.name === CORE_EXTENSIONS.LIST_ITEM) { let slice = view.state.selection.content(); let newFragment = slice.content; @@ -348,8 +352,8 @@ function flattenListStructure(fragment: Fragment, schema: Schema): Fragment { (node.content.firstChild.type === schema.nodes.bulletList || node.content.firstChild.type === schema.nodes.orderedList) ) { - const sublist = node.content.firstChild; - const flattened = flattenListStructure(sublist.content, schema); + const subList = node.content.firstChild; + const flattened = flattenListStructure(subList.content, schema); flattened.forEach((subNode) => result.push(subNode)); } } @@ -376,7 +380,7 @@ const handleNodeSelection = ( let draggedNodePos = nodePosAtDOM(node, view, options); if (draggedNodePos == null || draggedNodePos < 0) return; - // Handle blockquotes separately + // Handle blockquote separately if (node.matches("blockquote")) { draggedNodePos = nodePosAtDOMForBlockQuotes(node, view); if (draggedNodePos === null || draggedNodePos === undefined) return; @@ -385,7 +389,10 @@ const handleNodeSelection = ( const $pos = view.state.doc.resolve(draggedNodePos); // If it's a nested list item or task item, move up to the item level - if (($pos.parent.type.name === "listItem" || $pos.parent.type.name === "taskItem") && $pos.depth > 1) { + if ( + [CORE_EXTENSIONS.LIST_ITEM, CORE_EXTENSIONS.TASK_ITEM].includes($pos.parent.type.name as CORE_EXTENSIONS) && + $pos.depth > 1 + ) { draggedNodePos = $pos.before($pos.depth); } } @@ -403,14 +410,16 @@ const handleNodeSelection = ( // Additional logic for drag start if (event instanceof DragEvent && !event.dataTransfer) return; - if (nodeSelection.node.type.name === "listItem" || nodeSelection.node.type.name === "taskItem") { + if ( + [CORE_EXTENSIONS.LIST_ITEM, CORE_EXTENSIONS.TASK_ITEM].includes(nodeSelection.node.type.name as CORE_EXTENSIONS) + ) { listType = node.closest("ol, ul")?.tagName || ""; } const slice = view.state.selection.content(); const { dom, text } = __serializeForClipboard(view, slice); - if (event instanceof DragEvent) { + if (event instanceof DragEvent && event.dataTransfer) { event.dataTransfer.clearData(); event.dataTransfer.setData("text/html", dom.innerHTML); event.dataTransfer.setData("text/plain", text); diff --git a/packages/editor/src/core/plugins/drop.ts b/packages/editor/src/core/plugins/drop.ts new file mode 100644 index 000000000..a0bb65779 --- /dev/null +++ b/packages/editor/src/core/plugins/drop.ts @@ -0,0 +1,118 @@ +import { Editor } from "@tiptap/core"; +import { Plugin, PluginKey } from "@tiptap/pm/state"; +// constants +import { ACCEPTED_ATTACHMENT_MIME_TYPES, ACCEPTED_IMAGE_MIME_TYPES } from "@/constants/config"; +// types +import { TEditorCommands } from "@/types"; + +export const DropHandlerPlugin = (editor: Editor): Plugin => + new Plugin({ + key: new PluginKey("drop-handler-plugin"), + props: { + handlePaste: (view, event) => { + if ( + editor.isEditable && + event.clipboardData && + event.clipboardData.files && + event.clipboardData.files.length > 0 + ) { + event.preventDefault(); + const files = Array.from(event.clipboardData.files); + const acceptedFiles = files.filter( + (f) => ACCEPTED_IMAGE_MIME_TYPES.includes(f.type) || ACCEPTED_ATTACHMENT_MIME_TYPES.includes(f.type) + ); + + if (acceptedFiles.length) { + const pos = view.state.selection.from; + insertFilesSafely({ + editor, + files: acceptedFiles, + initialPos: pos, + event: "drop", + }); + } + return true; + } + return false; + }, + handleDrop: (view, event, _slice, moved) => { + if ( + editor.isEditable && + !moved && + event.dataTransfer && + event.dataTransfer.files && + event.dataTransfer.files.length > 0 + ) { + event.preventDefault(); + const files = Array.from(event.dataTransfer.files); + const acceptedFiles = files.filter( + (f) => ACCEPTED_IMAGE_MIME_TYPES.includes(f.type) || ACCEPTED_ATTACHMENT_MIME_TYPES.includes(f.type) + ); + + if (acceptedFiles.length) { + const coordinates = view.posAtCoords({ + left: event.clientX, + top: event.clientY, + }); + + if (coordinates) { + const pos = coordinates.pos; + insertFilesSafely({ + editor, + files: acceptedFiles, + initialPos: pos, + event: "drop", + }); + } + return true; + } + } + return false; + }, + }, + }); + +type InsertFilesSafelyArgs = { + editor: Editor; + event: "insert" | "drop"; + files: File[]; + initialPos: number; + type?: Extract; +}; + +export const insertFilesSafely = async (args: InsertFilesSafelyArgs) => { + const { editor, event, files, initialPos, type } = args; + let pos = initialPos; + + for (const file of files) { + // safe insertion + const docSize = editor.state.doc.content.size; + pos = Math.min(pos, docSize); + + let fileType: "image" | "attachment" | null = null; + + try { + if (type) { + if (["image", "attachment"].includes(type)) fileType = type; + else throw new Error("Wrong file type passed"); + } else { + if (ACCEPTED_IMAGE_MIME_TYPES.includes(file.type)) fileType = "image"; + else if (ACCEPTED_ATTACHMENT_MIME_TYPES.includes(file.type)) fileType = "attachment"; + } + // insert file depending on the type at the current position + if (fileType === "image") { + editor.commands.insertImageComponent({ + file, + pos, + event, + }); + } else if (fileType === "attachment") { + } + } catch (error) { + console.error(`Error while ${event}ing file:`, error); + } + + // Move to the next position + pos += 1; + } +}; diff --git a/packages/editor/src/core/plugins/file/delete.ts b/packages/editor/src/core/plugins/file/delete.ts new file mode 100644 index 000000000..b77841c22 --- /dev/null +++ b/packages/editor/src/core/plugins/file/delete.ts @@ -0,0 +1,67 @@ +import { Editor } from "@tiptap/core"; +import { EditorState, Plugin, PluginKey, Transaction } from "@tiptap/pm/state"; +// plane editor imports +import { NODE_FILE_MAP } from "@/plane-editor/constants/utility"; +// types +import { TFileHandler } from "@/types"; +// local imports +import { TFileNode } from "./types"; + +const DELETE_PLUGIN_KEY = new PluginKey("delete-utility"); + +export const TrackFileDeletionPlugin = (editor: Editor, deleteHandler: TFileHandler["delete"]): Plugin => + new Plugin({ + key: DELETE_PLUGIN_KEY, + appendTransaction: (transactions: readonly Transaction[], oldState: EditorState, newState: EditorState) => { + const newFileSources: { + [nodeType: string]: Set | undefined; + } = {}; + if (!transactions.some((tr) => tr.docChanged)) return null; + + newState.doc.descendants((node) => { + const nodeType = node.type.name; + const nodeFileSetDetails = NODE_FILE_MAP[nodeType]; + if (nodeFileSetDetails) { + if (newFileSources[nodeType]) { + newFileSources[nodeType].add(node.attrs.src); + } else { + newFileSources[nodeType] = new Set([node.attrs.src]); + } + } + }); + + transactions.forEach((transaction) => { + // if the transaction has meta of skipFileDeletion set to true, then return (like while clearing the editor content programmatically) + if (transaction.getMeta("skipFileDeletion")) return; + + const removedFiles: TFileNode[] = []; + + // iterate through all the nodes in the old state + oldState.doc.descendants((node) => { + const nodeType = node.type.name; + const isAValidNode = NODE_FILE_MAP[nodeType]; + // if the node doesn't match, then return as no point in checking + if (!isAValidNode) return; + // Check if the node has been deleted or replaced + if (!newFileSources[nodeType]?.has(node.attrs.src)) { + removedFiles.push(node as TFileNode); + } + }); + + removedFiles.forEach(async (node) => { + const nodeType = node.type.name; + const src = node.attrs.src; + const nodeFileSetDetails = NODE_FILE_MAP[nodeType]; + if (!nodeFileSetDetails || !src) return; + try { + editor.storage[nodeType][nodeFileSetDetails.fileSetName]?.set(src, true); + await deleteHandler(src); + } catch (error) { + console.error("Error deleting file via delete utility plugin:", error); + } + }); + }); + + return null; + }, + }); diff --git a/packages/editor/src/core/plugins/file/restore.ts b/packages/editor/src/core/plugins/file/restore.ts new file mode 100644 index 000000000..04a4c295c --- /dev/null +++ b/packages/editor/src/core/plugins/file/restore.ts @@ -0,0 +1,72 @@ +import { Editor } from "@tiptap/core"; +import { EditorState, Plugin, PluginKey, Transaction } from "@tiptap/pm/state"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; +// plane editor imports +import { NODE_FILE_MAP } from "@/plane-editor/constants/utility"; +// types +import { TFileHandler } from "@/types"; +// local imports +import { TFileNode } from "./types"; + +const RESTORE_PLUGIN_KEY = new PluginKey("restore-utility"); + +export const TrackFileRestorationPlugin = (editor: Editor, restoreHandler: TFileHandler["restore"]): Plugin => + new Plugin({ + key: RESTORE_PLUGIN_KEY, + appendTransaction: (transactions: readonly Transaction[], oldState: EditorState, newState: EditorState) => { + if (!transactions.some((tr) => tr.docChanged)) return null; + + const oldFileSources: { + [key: string]: Set | undefined; + } = {}; + oldState.doc.descendants((node) => { + const nodeType = node.type.name; + const nodeFileSetDetails = NODE_FILE_MAP[nodeType]; + if (nodeFileSetDetails) { + if (oldFileSources[nodeType]) { + oldFileSources[nodeType].add(node.attrs.src); + } else { + oldFileSources[nodeType] = new Set([node.attrs.src]); + } + } + }); + + transactions.forEach(() => { + const addedFiles: TFileNode[] = []; + + newState.doc.descendants((node, pos) => { + const nodeType = node.type.name; + const isAValidNode = NODE_FILE_MAP[nodeType]; + // if the node doesn't match, then return as no point in checking + if (!isAValidNode) return; + if (pos < 0 || pos > newState.doc.content.size) return; + if (oldFileSources[nodeType]?.has(node.attrs.src)) return; + // if the src is just a id (private bucket), then we don't need to handle restore from here but + // only while it fails to load + if (nodeType === CORE_EXTENSIONS.CUSTOM_IMAGE && !node.attrs.src?.startsWith("http")) return; + addedFiles.push(node as TFileNode); + }); + + addedFiles.forEach(async (node) => { + const nodeType = node.type.name; + const src = node.attrs.src; + const nodeFileSetDetails = NODE_FILE_MAP[nodeType]; + const extensionFileSetStorage = editor.storage[nodeType]?.[nodeFileSetDetails.fileSetName]; + const wasDeleted = extensionFileSetStorage?.get(src); + if (!nodeFileSetDetails || !src) return; + if (wasDeleted === undefined) { + extensionFileSetStorage?.set(src, false); + } else if (wasDeleted === true) { + try { + await restoreHandler(src); + extensionFileSetStorage?.set(src, false); + } catch (error) { + console.error("Error restoring file via restore utility plugin:", error); + } + } + }); + }); + return null; + }, + }); diff --git a/packages/editor/src/core/plugins/file/root.ts b/packages/editor/src/core/plugins/file/root.ts new file mode 100644 index 000000000..693ac6964 --- /dev/null +++ b/packages/editor/src/core/plugins/file/root.ts @@ -0,0 +1,22 @@ +import { Editor } from "@tiptap/core"; +import { Plugin } from "@tiptap/pm/state"; +// types +import { TFileHandler, TReadOnlyFileHandler } from "@/types"; +// local imports +import { TrackFileDeletionPlugin } from "./delete"; +import { TrackFileRestorationPlugin } from "./restore"; + +type TArgs = { + editor: Editor; + fileHandler: TFileHandler | TReadOnlyFileHandler; + isEditable: boolean; +}; + +export const FilePlugins = (args: TArgs): Plugin[] => { + const { editor, fileHandler, isEditable } = args; + + return [ + ...(isEditable && "delete" in fileHandler ? [TrackFileDeletionPlugin(editor, fileHandler.delete)] : []), + TrackFileRestorationPlugin(editor, fileHandler.restore), + ]; +}; diff --git a/packages/editor/src/core/plugins/file/types.ts b/packages/editor/src/core/plugins/file/types.ts new file mode 100644 index 000000000..164d12ae7 --- /dev/null +++ b/packages/editor/src/core/plugins/file/types.ts @@ -0,0 +1,8 @@ +import { Node as ProseMirrorNode } from "@tiptap/pm/model"; + +export type TFileNode = ProseMirrorNode & { + attrs: { + src: string; + id: string; + }; +}; diff --git a/packages/editor/src/core/plugins/image/delete-image.ts b/packages/editor/src/core/plugins/image/delete-image.ts deleted file mode 100644 index 459d9fd70..000000000 --- a/packages/editor/src/core/plugins/image/delete-image.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { Editor } from "@tiptap/core"; -import { EditorState, Plugin, PluginKey, Transaction } from "@tiptap/pm/state"; -// plugins -import { type ImageNode } from "@/plugins/image"; -// types -import { DeleteImage } from "@/types"; - -export const TrackImageDeletionPlugin = (editor: Editor, deleteImage: DeleteImage, nodeType: string): Plugin => - new Plugin({ - key: new PluginKey(`delete-${nodeType}`), - appendTransaction: (transactions: readonly Transaction[], oldState: EditorState, newState: EditorState) => { - const newImageSources = new Set(); - newState.doc.descendants((node) => { - if (node.type.name === nodeType) { - newImageSources.add(node.attrs.src); - } - }); - - transactions.forEach((transaction) => { - // if the transaction has meta of skipImageDeletion get to true, then return (like while clearing the editor content programatically) - if (transaction.getMeta("skipImageDeletion")) return; - // transaction could be a selection - if (!transaction.docChanged) return; - - const removedImages: ImageNode[] = []; - - // iterate through all the nodes in the old state - oldState.doc.descendants((oldNode) => { - // if the node is not an image, then return as no point in checking - if (oldNode.type.name !== nodeType) return; - - // Check if the node has been deleted or replaced - if (!newImageSources.has(oldNode.attrs.src)) { - removedImages.push(oldNode as ImageNode); - } - }); - - removedImages.forEach(async (node) => { - const src = node.attrs.src; - editor.storage[nodeType].deletedImageSet?.set(src, true); - if (!src) return; - try { - await deleteImage(src); - } catch (error) { - console.error("Error deleting image:", error); - } - }); - }); - - return null; - }, - }); diff --git a/packages/editor/src/core/plugins/image/index.ts b/packages/editor/src/core/plugins/image/index.ts deleted file mode 100644 index c0dc631c5..000000000 --- a/packages/editor/src/core/plugins/image/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./types"; -export * from "./delete-image"; -export * from "./restore-image"; diff --git a/packages/editor/src/core/plugins/image/restore-image.ts b/packages/editor/src/core/plugins/image/restore-image.ts deleted file mode 100644 index 4eecf01d7..000000000 --- a/packages/editor/src/core/plugins/image/restore-image.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { Editor } from "@tiptap/core"; -import { EditorState, Plugin, PluginKey, Transaction } from "@tiptap/pm/state"; -// plugins -import { ImageNode } from "@/plugins/image"; -// types -import { RestoreImage } from "@/types"; - -export const TrackImageRestorationPlugin = (editor: Editor, restoreImage: RestoreImage, nodeType: string): Plugin => - new Plugin({ - key: new PluginKey(`restore-${nodeType}`), - appendTransaction: (transactions: readonly Transaction[], oldState: EditorState, newState: EditorState) => { - const oldImageSources = new Set(); - oldState.doc.descendants((node) => { - if (node.type.name === nodeType) { - oldImageSources.add(node.attrs.src); - } - }); - - transactions.forEach((transaction) => { - if (!transaction.docChanged) return; - - const addedImages: ImageNode[] = []; - - newState.doc.descendants((node, pos) => { - if (node.type.name !== nodeType) return; - if (pos < 0 || pos > newState.doc.content.size) return; - if (oldImageSources.has(node.attrs.src)) return; - // if the src is just a id (private bucket), then we don't need to handle restore from here but - // only while it fails to load - if (!node.attrs.src?.startsWith("http")) return; - addedImages.push(node as ImageNode); - }); - - addedImages.forEach(async (image) => { - const src = image.attrs.src; - const wasDeleted = editor.storage[nodeType].deletedImageSet.get(src); - if (wasDeleted === undefined) { - editor.storage[nodeType].deletedImageSet.set(src, false); - } else if (wasDeleted === true) { - try { - await onNodeRestored(src, restoreImage); - editor.storage[nodeType].deletedImageSet.set(src, false); - } catch (error) { - console.error("Error restoring image: ", error); - } - } - }); - }); - return null; - }, - }); - -async function onNodeRestored(src: string, restoreImage: RestoreImage): Promise { - if (!src) return; - try { - await restoreImage(src); - } catch (error) { - console.error("Error restoring image: ", error); - throw error; - } -} diff --git a/packages/editor/src/core/plugins/image/types/image-node.ts b/packages/editor/src/core/plugins/image/types/image-node.ts deleted file mode 100644 index 67afc8315..000000000 --- a/packages/editor/src/core/plugins/image/types/image-node.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Node as ProseMirrorNode } from "@tiptap/pm/model"; - -export interface ImageNode extends ProseMirrorNode { - attrs: { - src: string; - id: string; - }; -} - -export type ImageExtensionStorage = { - deletedImageSet: Map; - uploadInProgress: boolean; -}; diff --git a/packages/editor/src/core/plugins/image/types/index.ts b/packages/editor/src/core/plugins/image/types/index.ts deleted file mode 100644 index 2fddf3bf6..000000000 --- a/packages/editor/src/core/plugins/image/types/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./image-node"; diff --git a/packages/editor/src/core/plugins/markdown-clipboard.ts b/packages/editor/src/core/plugins/markdown-clipboard.ts new file mode 100644 index 000000000..78f649b23 --- /dev/null +++ b/packages/editor/src/core/plugins/markdown-clipboard.ts @@ -0,0 +1,80 @@ +import { Editor } from "@tiptap/core"; +import { Fragment, Node } from "@tiptap/pm/model"; +import { Plugin, PluginKey } from "@tiptap/pm/state"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; + +export const MarkdownClipboardPlugin = (editor: Editor): Plugin => + new Plugin({ + key: new PluginKey("markdownClipboard"), + props: { + clipboardTextSerializer: (slice) => { + const markdownSerializer = editor.storage.markdown.serializer; + const isTableRow = slice.content.firstChild?.type?.name === CORE_EXTENSIONS.TABLE_ROW; + const nodeSelect = slice.openStart === 0 && slice.openEnd === 0; + + if (nodeSelect) { + return markdownSerializer.serialize(slice.content); + } + + const processTableContent = (tableNode: Node | Fragment) => { + let result = ""; + tableNode.content?.forEach?.((tableRowNode: Node | Fragment) => { + tableRowNode.content?.forEach?.((cell: Node) => { + const cellContent = cell.content ? markdownSerializer.serialize(cell.content) : ""; + result += cellContent + "\n"; + }); + }); + return result; + }; + + if (isTableRow) { + const rowsCount = slice.content?.childCount || 0; + const cellsCount = slice.content?.firstChild?.content?.childCount || 0; + if (rowsCount === 1 || cellsCount === 1) { + return processTableContent(slice.content); + } else { + return markdownSerializer.serialize(slice.content); + } + } + + const traverseToParentOfLeaf = (node: Node | null, parent: Fragment | Node, depth: number): Node | Fragment => { + let currentNode = node; + let currentParent = parent; + let currentDepth = depth; + + while (currentNode && currentDepth > 1 && currentNode.content?.firstChild) { + if (currentNode.content?.childCount > 1) { + if (currentNode.content.firstChild?.type?.name === CORE_EXTENSIONS.LIST_ITEM) { + return currentParent; + } else { + return currentNode.content; + } + } + + currentParent = currentNode; + currentNode = currentNode.content?.firstChild || null; + currentDepth--; + } + + return currentParent; + }; + + if (slice.content.childCount > 1) { + return markdownSerializer.serialize(slice.content); + } else { + const targetNode = traverseToParentOfLeaf(slice.content.firstChild, slice.content, slice.openStart); + + let currentNode = targetNode; + while (currentNode && currentNode.content && currentNode.childCount === 1 && currentNode.firstChild) { + currentNode = currentNode.firstChild; + } + if (currentNode instanceof Node && currentNode.isText) { + return currentNode.text; + } + + return markdownSerializer.serialize(targetNode); + } + }, + }, + }); diff --git a/packages/editor/src/core/types/collaboration.ts b/packages/editor/src/core/types/collaboration.ts index 82e2f81f9..556086232 100644 --- a/packages/editor/src/core/types/collaboration.ts +++ b/packages/editor/src/core/types/collaboration.ts @@ -22,7 +22,7 @@ export type TServerHandler = { type TCollaborativeEditorHookProps = { disabledExtensions: TExtensions[]; - editable?: boolean; + editable: boolean; editorClassName: string; editorProps?: EditorProps; extensions?: Extensions; diff --git a/packages/editor/src/core/types/config.ts b/packages/editor/src/core/types/config.ts index 4c91fec5d..b72e3dcf6 100644 --- a/packages/editor/src/core/types/config.ts +++ b/packages/editor/src/core/types/config.ts @@ -1,19 +1,17 @@ -import { DeleteImage, RestoreImage, UploadImage } from "@/types"; - export type TReadOnlyFileHandler = { getAssetSrc: (path: string) => Promise; - restore: RestoreImage; + restore: (assetSrc: string) => Promise; }; export type TFileHandler = TReadOnlyFileHandler & { assetsUploadStatus: Record; // blockId => progress percentage cancel: () => void; - delete: DeleteImage; - upload: UploadImage; + delete: (assetSrc: string) => Promise; + upload: (blockId: string, file: File) => Promise; validation: { /** * @description max file size in bytes - * @example enter 5242880( 5* 1024 * 1024) for 5MB + * @example enter 5242880(5 * 1024 * 1024) for 5MB */ maxFileSize: number; }; diff --git a/packages/editor/src/core/types/image.ts b/packages/editor/src/core/types/image.ts deleted file mode 100644 index ca6f76fb1..000000000 --- a/packages/editor/src/core/types/image.ts +++ /dev/null @@ -1,5 +0,0 @@ -export type DeleteImage = (assetUrlWithWorkspaceId: string) => Promise; - -export type RestoreImage = (assetUrlWithWorkspaceId: string) => Promise; - -export type UploadImage = (blockId: string, file: File) => Promise; diff --git a/packages/editor/src/core/types/index.ts b/packages/editor/src/core/types/index.ts index e99a74b28..66cb24942 100644 --- a/packages/editor/src/core/types/index.ts +++ b/packages/editor/src/core/types/index.ts @@ -4,7 +4,6 @@ export * from "./config"; export * from "./editor"; export * from "./embed"; export * from "./extensions"; -export * from "./image"; export * from "./mention"; export * from "./slash-commands-suggestion"; export * from "@/plane-editor/types"; diff --git a/packages/hooks/package.json b/packages/hooks/package.json index d444bedda..320203e2d 100644 --- a/packages/hooks/package.json +++ b/packages/hooks/package.json @@ -22,7 +22,7 @@ "@plane/eslint-config": "*", "@types/node": "^22.5.4", "@types/react": "^18.3.11", - "tsup": "^8.4.0", + "tsup": "8.3.0", "typescript": "^5.3.3" } } diff --git a/packages/ui/package.json b/packages/ui/package.json index 45550f369..e0920d49e 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -71,7 +71,7 @@ "postcss-cli": "^11.0.0", "postcss-nested": "^6.0.1", "storybook": "^8.1.1", - "tsup": "^8.4.0", + "tsup": "8.3.0", "typescript": "5.3.3" } } diff --git a/packages/utils/package.json b/packages/utils/package.json index fc8600077..07504d9a4 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -29,7 +29,7 @@ "@types/node": "^22.5.4", "@types/react": "^18.3.11", "@types/zxcvbn": "^4.4.5", - "tsup": "^8.4.0", + "tsup": "8.3.0", "typescript": "^5.3.3" } } diff --git a/web/core/store/pages/base-page.ts b/web/core/store/pages/base-page.ts index 32a087c95..6639e8e84 100644 --- a/web/core/store/pages/base-page.ts +++ b/web/core/store/pages/base-page.ts @@ -529,7 +529,6 @@ export class BasePage implements TBasePage { }; setEditorRef = (editorRef: EditorRefApi | null) => { - console.log("store editorRef", editorRef); runInAction(() => { this.editorRef = editorRef; }); diff --git a/yarn.lock b/yarn.lock index 22d8754dc..6fca25f4d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2236,100 +2236,105 @@ resolved "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-3.0.0.tgz#96fdb89d25c62e7b6a5d08caf0ce5114370e3b8f" integrity sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg== -"@rollup/rollup-android-arm-eabi@4.35.0": - version "4.35.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.35.0.tgz#e1d7700735f7e8de561ef7d1fa0362082a180c43" - integrity sha512-uYQ2WfPaqz5QtVgMxfN6NpLD+no0MYHDBywl7itPYd3K5TjjSghNKmX8ic9S8NU8w81NVhJv/XojcHptRly7qQ== +"@rollup/rollup-android-arm-eabi@4.41.1": + version "4.41.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.41.1.tgz#f39f09f60d4a562de727c960d7b202a2cf797424" + integrity sha512-NELNvyEWZ6R9QMkiytB4/L4zSEaBC03KIXEghptLGLZWJ6VPrL63ooZQCOnlx36aQPGhzuOMwDerC1Eb2VmrLw== -"@rollup/rollup-android-arm64@4.35.0": - version "4.35.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.35.0.tgz#fa6cdfb1fc9e2c8e227a7f35d524d8f7f90cf4db" - integrity sha512-FtKddj9XZudurLhdJnBl9fl6BwCJ3ky8riCXjEw3/UIbjmIY58ppWwPEvU3fNu+W7FUsAsB1CdH+7EQE6CXAPA== +"@rollup/rollup-android-arm64@4.41.1": + version "4.41.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.41.1.tgz#d19af7e23760717f1d879d4ca3d2cd247742dff2" + integrity sha512-DXdQe1BJ6TK47ukAoZLehRHhfKnKg9BjnQYUu9gzhI8Mwa1d2fzxA1aw2JixHVl403bwp1+/o/NhhHtxWJBgEA== -"@rollup/rollup-darwin-arm64@4.35.0": - version "4.35.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.35.0.tgz#6da5a1ddc4f11d4a7ae85ab443824cb6bf614e30" - integrity sha512-Uk+GjOJR6CY844/q6r5DR/6lkPFOw0hjfOIzVx22THJXMxktXG6CbejseJFznU8vHcEBLpiXKY3/6xc+cBm65Q== +"@rollup/rollup-darwin-arm64@4.41.1": + version "4.41.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.41.1.tgz#1c3a2fbf205d80641728e05f4a56c909e95218b7" + integrity sha512-5afxvwszzdulsU2w8JKWwY8/sJOLPzf0e1bFuvcW5h9zsEg+RQAojdW0ux2zyYAz7R8HvvzKCjLNJhVq965U7w== -"@rollup/rollup-darwin-x64@4.35.0": - version "4.35.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.35.0.tgz#25b74ce2d8d3f9ea8e119b01384d44a1c0a0d3ae" - integrity sha512-3IrHjfAS6Vkp+5bISNQnPogRAW5GAV1n+bNCrDwXmfMHbPl5EhTmWtfmwlJxFRUCBZ+tZ/OxDyU08aF6NI/N5Q== +"@rollup/rollup-darwin-x64@4.41.1": + version "4.41.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.41.1.tgz#aa66d2ba1a25e609500e13bef06dc0e71cc0c0d4" + integrity sha512-egpJACny8QOdHNNMZKf8xY0Is6gIMz+tuqXlusxquWu3F833DcMwmGM7WlvCO9sB3OsPjdC4U0wHw5FabzCGZg== -"@rollup/rollup-freebsd-arm64@4.35.0": - version "4.35.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.35.0.tgz#be3d39e3441df5d6e187c83d158c60656c82e203" - integrity sha512-sxjoD/6F9cDLSELuLNnY0fOrM9WA0KrM0vWm57XhrIMf5FGiN8D0l7fn+bpUeBSU7dCgPV2oX4zHAsAXyHFGcQ== +"@rollup/rollup-freebsd-arm64@4.41.1": + version "4.41.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.41.1.tgz#df10a7b6316a0ef1028c6ca71a081124c537e30d" + integrity sha512-DBVMZH5vbjgRk3r0OzgjS38z+atlupJ7xfKIDJdZZL6sM6wjfDNo64aowcLPKIx7LMQi8vybB56uh1Ftck/Atg== -"@rollup/rollup-freebsd-x64@4.35.0": - version "4.35.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.35.0.tgz#cd932d3ec679711efd65ca25821fb318e25b7ce4" - integrity sha512-2mpHCeRuD1u/2kruUiHSsnjWtHjqVbzhBkNVQ1aVD63CcexKVcQGwJ2g5VphOd84GvxfSvnnlEyBtQCE5hxVVw== +"@rollup/rollup-freebsd-x64@4.41.1": + version "4.41.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.41.1.tgz#a3fdce8a05e95b068cbcb46e4df5185e407d0c35" + integrity sha512-3FkydeohozEskBxNWEIbPfOE0aqQgB6ttTkJ159uWOFn42VLyfAiyD9UK5mhu+ItWzft60DycIN1Xdgiy8o/SA== -"@rollup/rollup-linux-arm-gnueabihf@4.35.0": - version "4.35.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.35.0.tgz#d300b74c6f805474225632f185daaeae760ac2bb" - integrity sha512-mrA0v3QMy6ZSvEuLs0dMxcO2LnaCONs1Z73GUDBHWbY8tFFocM6yl7YyMu7rz4zS81NDSqhrUuolyZXGi8TEqg== +"@rollup/rollup-linux-arm-gnueabihf@4.41.1": + version "4.41.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.41.1.tgz#49f766c55383bd0498014a9d76924348c2f3890c" + integrity sha512-wC53ZNDgt0pqx5xCAgNunkTzFE8GTgdZ9EwYGVcg+jEjJdZGtq9xPjDnFgfFozQI/Xm1mh+D9YlYtl+ueswNEg== -"@rollup/rollup-linux-arm-musleabihf@4.35.0": - version "4.35.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.35.0.tgz#2caac622380f314c41934ed1e68ceaf6cc380cc3" - integrity sha512-DnYhhzcvTAKNexIql8pFajr0PiDGrIsBYPRvCKlA5ixSS3uwo/CWNZxB09jhIapEIg945KOzcYEAGGSmTSpk7A== +"@rollup/rollup-linux-arm-musleabihf@4.41.1": + version "4.41.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.41.1.tgz#1d4d7d32fc557e17d52e1857817381ea365e2959" + integrity sha512-jwKCca1gbZkZLhLRtsrka5N8sFAaxrGz/7wRJ8Wwvq3jug7toO21vWlViihG85ei7uJTpzbXZRcORotE+xyrLA== -"@rollup/rollup-linux-arm64-gnu@4.35.0": - version "4.35.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.35.0.tgz#1ec841650b038cc15c194c26326483fd7ebff3e3" - integrity sha512-uagpnH2M2g2b5iLsCTZ35CL1FgyuzzJQ8L9VtlJ+FckBXroTwNOaD0z0/UF+k5K3aNQjbm8LIVpxykUOQt1m/A== +"@rollup/rollup-linux-arm64-gnu@4.41.1": + version "4.41.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.41.1.tgz#f4fc317268441e9589edad3be8f62b6c03009bc1" + integrity sha512-g0UBcNknsmmNQ8V2d/zD2P7WWfJKU0F1nu0k5pW4rvdb+BIqMm8ToluW/eeRmxCared5dD76lS04uL4UaNgpNA== -"@rollup/rollup-linux-arm64-musl@4.35.0": - version "4.35.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.35.0.tgz#2fc70a446d986e27f6101ea74e81746987f69150" - integrity sha512-XQxVOCd6VJeHQA/7YcqyV0/88N6ysSVzRjJ9I9UA/xXpEsjvAgDTgH3wQYz5bmr7SPtVK2TsP2fQ2N9L4ukoUg== +"@rollup/rollup-linux-arm64-musl@4.41.1": + version "4.41.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.41.1.tgz#63a1f1b0671cb17822dabae827fef0e443aebeb7" + integrity sha512-XZpeGB5TKEZWzIrj7sXr+BEaSgo/ma/kCgrZgL0oo5qdB1JlTzIYQKel/RmhT6vMAvOdM2teYlAaOGJpJ9lahg== -"@rollup/rollup-linux-loongarch64-gnu@4.35.0": - version "4.35.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.35.0.tgz#561bd045cd9ce9e08c95f42e7a8688af8c93d764" - integrity sha512-5pMT5PzfgwcXEwOaSrqVsz/LvjDZt+vQ8RT/70yhPU06PTuq8WaHhfT1LW+cdD7mW6i/J5/XIkX/1tCAkh1W6g== +"@rollup/rollup-linux-loongarch64-gnu@4.41.1": + version "4.41.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.41.1.tgz#c659b01cc6c0730b547571fc3973e1e955369f98" + integrity sha512-bkCfDJ4qzWfFRCNt5RVV4DOw6KEgFTUZi2r2RuYhGWC8WhCA8lCAJhDeAmrM/fdiAH54m0mA0Vk2FGRPyzI+tw== -"@rollup/rollup-linux-powerpc64le-gnu@4.35.0": - version "4.35.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.35.0.tgz#45d849a0b33813f33fe5eba9f99e0ff15ab5caad" - integrity sha512-c+zkcvbhbXF98f4CtEIP1EBA/lCic5xB0lToneZYvMeKu5Kamq3O8gqrxiYYLzlZH6E3Aq+TSW86E4ay8iD8EA== +"@rollup/rollup-linux-powerpc64le-gnu@4.41.1": + version "4.41.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.41.1.tgz#612e746f9ad7e58480f964d65e0d6c3f4aae69a8" + integrity sha512-3mr3Xm+gvMX+/8EKogIZSIEF0WUu0HL9di+YWlJpO8CQBnoLAEL/roTCxuLncEdgcfJcvA4UMOf+2dnjl4Ut1A== -"@rollup/rollup-linux-riscv64-gnu@4.35.0": - version "4.35.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.35.0.tgz#78dde3e6fcf5b5733a97d0a67482d768aa1e83a5" - integrity sha512-s91fuAHdOwH/Tad2tzTtPX7UZyytHIRR6V4+2IGlV0Cej5rkG0R61SX4l4y9sh0JBibMiploZx3oHKPnQBKe4g== +"@rollup/rollup-linux-riscv64-gnu@4.41.1": + version "4.41.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.41.1.tgz#4610dbd1dcfbbae32fbc10c20ae7387acb31110c" + integrity sha512-3rwCIh6MQ1LGrvKJitQjZFuQnT2wxfU+ivhNBzmxXTXPllewOF7JR1s2vMX/tWtUYFgphygxjqMl76q4aMotGw== -"@rollup/rollup-linux-s390x-gnu@4.35.0": - version "4.35.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.35.0.tgz#2e34835020f9e03dfb411473a5c2a0e8a9c5037b" - integrity sha512-hQRkPQPLYJZYGP+Hj4fR9dDBMIM7zrzJDWFEMPdTnTy95Ljnv0/4w/ixFw3pTBMEuuEuoqtBINYND4M7ujcuQw== +"@rollup/rollup-linux-riscv64-musl@4.41.1": + version "4.41.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.41.1.tgz#054911fab40dc83fafc21e470193c058108f19d8" + integrity sha512-LdIUOb3gvfmpkgFZuccNa2uYiqtgZAz3PTzjuM5bH3nvuy9ty6RGc/Q0+HDFrHrizJGVpjnTZ1yS5TNNjFlklw== -"@rollup/rollup-linux-x64-gnu@4.35.0": - version "4.35.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.35.0.tgz#4f9774beddc6f4274df57ac99862eb23040de461" - integrity sha512-Pim1T8rXOri+0HmV4CdKSGrqcBWX0d1HoPnQ0uw0bdp1aP5SdQVNBy8LjYncvnLgu3fnnCt17xjWGd4cqh8/hA== +"@rollup/rollup-linux-s390x-gnu@4.41.1": + version "4.41.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.41.1.tgz#98896eca8012547c7f04bd07eaa6896825f9e1a5" + integrity sha512-oIE6M8WC9ma6xYqjvPhzZYk6NbobIURvP/lEbh7FWplcMO6gn7MM2yHKA1eC/GvYwzNKK/1LYgqzdkZ8YFxR8g== -"@rollup/rollup-linux-x64-musl@4.35.0": - version "4.35.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.35.0.tgz#dfcff2c1aed518b3d23ccffb49afb349d74fb608" - integrity sha512-QysqXzYiDvQWfUiTm8XmJNO2zm9yC9P/2Gkrwg2dH9cxotQzunBHYr6jk4SujCTqnfGxduOmQcI7c2ryuW8XVg== +"@rollup/rollup-linux-x64-gnu@4.41.1": + version "4.41.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.41.1.tgz#01cf56844a1e636ee80dfb364e72c2b7142ad896" + integrity sha512-cWBOvayNvA+SyeQMp79BHPK8ws6sHSsYnK5zDcsC3Hsxr1dgTABKjMnMslPq1DvZIp6uO7kIWhiGwaTdR4Og9A== -"@rollup/rollup-win32-arm64-msvc@4.35.0": - version "4.35.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.35.0.tgz#b0b37e2d77041e3aa772f519291309abf4c03a84" - integrity sha512-OUOlGqPkVJCdJETKOCEf1mw848ZyJ5w50/rZ/3IBQVdLfR5jk/6Sr5m3iO2tdPgwo0x7VcncYuOvMhBWZq8ayg== +"@rollup/rollup-linux-x64-musl@4.41.1": + version "4.41.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.41.1.tgz#e67c7531df6dff0b4c241101d4096617fbca87c3" + integrity sha512-y5CbN44M+pUCdGDlZFzGGBSKCA4A/J2ZH4edTYSSxFg7ce1Xt3GtydbVKWLlzL+INfFIZAEg1ZV6hh9+QQf9YQ== -"@rollup/rollup-win32-ia32-msvc@4.35.0": - version "4.35.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.35.0.tgz#5b5a40e44a743ddc0e06b8e1b3982f856dc9ce0a" - integrity sha512-2/lsgejMrtwQe44glq7AFFHLfJBPafpsTa6JvP2NGef/ifOa4KBoglVf7AKN7EV9o32evBPRqfg96fEHzWo5kw== +"@rollup/rollup-win32-arm64-msvc@4.41.1": + version "4.41.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.41.1.tgz#7eeada98444e580674de6989284e4baacd48ea65" + integrity sha512-lZkCxIrjlJlMt1dLO/FbpZbzt6J/A8p4DnqzSa4PWqPEUUUnzXLeki/iyPLfV0BmHItlYgHUqJe+3KiyydmiNQ== -"@rollup/rollup-win32-x64-msvc@4.35.0": - version "4.35.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.35.0.tgz#05f25dbc9981bee1ae6e713daab10397044a46ca" - integrity sha512-PIQeY5XDkrOysbQblSW7v3l1MDZzkTEzAfTPkj5VAu3FW8fS4ynyLg2sINp0fp3SjZ8xkRYpLqoKcYqAkhU1dw== +"@rollup/rollup-win32-ia32-msvc@4.41.1": + version "4.41.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.41.1.tgz#516c4b54f80587b4a390aaf4940b40870271d35d" + integrity sha512-+psFT9+pIh2iuGsxFYYa/LhS5MFKmuivRsx9iPJWNSGbh2XVEjk90fmpUEjCnILPEPJnikAU6SFDiEUyOv90Pg== + +"@rollup/rollup-win32-x64-msvc@4.41.1": + version "4.41.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.41.1.tgz#848f99b0d9936d92221bb6070baeff4db6947a30" + integrity sha512-Wq2zpapRYLfi4aKxf2Xff0tN+7slj2d4R87WEzqw7ZLsVvO5zwYCIuEGSZYiK41+GlwUo1HiR+GdkLEJnCKTCw== "@rtsao/scc@^1.1.0": version "1.1.0" @@ -3323,11 +3328,16 @@ "@types/estree" "*" "@types/json-schema" "*" -"@types/estree@*", "@types/estree@1.0.6", "@types/estree@^1.0.0", "@types/estree@^1.0.6": +"@types/estree@*", "@types/estree@^1.0.0", "@types/estree@^1.0.6": version "1.0.6" resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50" integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw== +"@types/estree@1.0.7": + version "1.0.7" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.7.tgz#4158d3105276773d5b7695cd4834b1722e4f37a8" + integrity sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ== + "@types/express-serve-static-core@*", "@types/express-serve-static-core@^5.0.0": version "5.0.6" resolved "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz#41fec4ea20e9c7b22f024ab88a95c6bb288f51b8" @@ -4604,9 +4614,9 @@ buffer@^6.0.3: base64-js "^1.3.1" ieee754 "^1.2.1" -bundle-require@^5.1.0: +bundle-require@^5.0.0: version "5.1.0" - resolved "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz#8db66f41950da3d77af1ef3322f4c3e04009faee" + resolved "https://registry.yarnpkg.com/bundle-require/-/bundle-require-5.1.0.tgz#8db66f41950da3d77af1ef3322f4c3e04009faee" integrity sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA== dependencies: load-tsconfig "^0.2.3" @@ -4761,13 +4771,6 @@ chokidar@^3.3.0, chokidar@^3.5.2, chokidar@^3.5.3, chokidar@^3.6.0: optionalDependencies: fsevents "~2.3.2" -chokidar@^4.0.3: - version "4.0.3" - resolved "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz#7be37a4c03c9aee1ecfe862a4a23b2c70c205d30" - integrity sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA== - dependencies: - readdirp "^4.0.1" - chownr@^1.1.1: version "1.1.4" resolved "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" @@ -4992,10 +4995,10 @@ concurrently@^9.0.1: tree-kill "^1.2.2" yargs "^17.7.2" -consola@^3.4.0: - version "3.4.0" - resolved "https://registry.npmjs.org/consola/-/consola-3.4.0.tgz#4cfc9348fd85ed16a17940b3032765e31061ab88" - integrity sha512-EiPU8G6dQG0GFHNR8ljnZFki/8a+cQwEQ+7wpxdChl02Q8HXlwEZWD5lqAF8vC2sEC3Tehr8hy7vErz88LHyUA== +consola@^3.2.3: + version "3.4.2" + resolved "https://registry.yarnpkg.com/consola/-/consola-3.4.2.tgz#5af110145397bb67afdab77013fdc34cae590ea7" + integrity sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA== constant-case@^3.0.4: version "3.0.4" @@ -5099,7 +5102,7 @@ cross-fetch@^3.1.5: dependencies: node-fetch "^2.7.0" -cross-spawn@^7.0.0, cross-spawn@^7.0.2: +cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.6" resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== @@ -5364,7 +5367,7 @@ debug@2.6.9: dependencies: ms "2.0.0" -debug@4, debug@^4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.7, debug@^4.4.0: +debug@4, debug@^4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.7: version "4.4.0" resolved "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a" integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA== @@ -5378,6 +5381,13 @@ debug@^3.2.7: dependencies: ms "^2.1.1" +debug@^4.3.5: + version "4.4.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.1.tgz#e5a8bc6cbc4c6cd3e64308b0693a3d4fa550189b" + integrity sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ== + dependencies: + ms "^2.1.3" + decimal.js-light@^2.4.1: version "2.5.1" resolved "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz#134fd32508f19e208f4fb2f8dac0d2626a867934" @@ -5930,7 +5940,7 @@ esbuild-register@^3.5.0: dependencies: debug "^4.3.4" -esbuild@0.25.0, "esbuild@^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0", esbuild@^0.25.0: +esbuild@0.25.0, "esbuild@^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0", esbuild@^0.23.0: version "0.25.0" resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz#0de1787a77206c5a79eeb634a623d39b5006ce92" integrity sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw== @@ -6266,6 +6276,21 @@ events@^3.2.0, events@^3.3.0: resolved "https://registry.npmjs.org/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== +execa@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" + integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== + dependencies: + cross-spawn "^7.0.3" + get-stream "^6.0.0" + human-signals "^2.1.0" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.1" + onetime "^5.1.2" + signal-exit "^3.0.3" + strip-final-newline "^2.0.0" + expand-template@^2.0.3: version "2.0.3" resolved "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" @@ -6400,10 +6425,10 @@ fault@^2.0.0: dependencies: format "^0.2.0" -fdir@^6.4.3: - version "6.4.3" - resolved "https://registry.npmjs.org/fdir/-/fdir-6.4.3.tgz#011cdacf837eca9b811c89dbb902df714273db72" - integrity sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw== +fdir@^6.4.4: + version "6.4.4" + resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.4.4.tgz#1cfcf86f875a883e19a8fab53622cfe992e8d2f9" + integrity sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg== fecha@^4.2.0: version "4.2.3" @@ -6737,6 +6762,11 @@ get-stdin@^9.0.0: resolved "https://registry.npmjs.org/get-stdin/-/get-stdin-9.0.0.tgz#3983ff82e03d56f1b2ea0d3e60325f39d703a575" integrity sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA== +get-stream@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" + integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== + get-symbol-description@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz#7bdd54e0befe8ffc9f3b4e203220d9f1e881b6ee" @@ -7064,6 +7094,11 @@ https-proxy-agent@^7.0.6: agent-base "^7.1.2" debug "4" +human-signals@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" + integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== + hyphen@^1.6.4: version "1.10.6" resolved "https://registry.npmjs.org/hyphen/-/hyphen-1.10.6.tgz#0e779d280e696102b97d7e42f5ca5de2cc97e274" @@ -8286,6 +8321,11 @@ mime@1.6.0: resolved "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== +mimic-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + mimic-response@^3.1.0: version "3.1.0" resolved "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9" @@ -8547,6 +8587,13 @@ normalize.css@^8.0.1: resolved "https://registry.npmjs.org/normalize.css/-/normalize.css-8.0.1.tgz#9b98a208738b9cc2634caacbc42d131c97487bf3" integrity sha512-qizSNPO93t1YUuUhP22btGOo3chcvDFqFaj2TRybP0DMxkHOCTYwp3n34fel4a31ORXy4m1Xq0Gyqpb5m33qIg== +npm-run-path@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" + integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== + dependencies: + path-key "^3.0.0" + nprogress@^0.2.0: version "0.2.0" resolved "https://registry.npmjs.org/nprogress/-/nprogress-0.2.0.tgz#cb8f34c53213d895723fcbab907e9422adbcafb1" @@ -8685,6 +8732,13 @@ one-time@^1.0.0: dependencies: fn.name "1.x.x" +onetime@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== + dependencies: + mimic-fn "^2.1.0" + open@^8.0.4: version "8.4.2" resolved "https://registry.npmjs.org/open/-/open-8.4.2.tgz#5b5ffe2a8f793dcd2aad73e550cb87b59cb084f9" @@ -8867,7 +8921,7 @@ path-is-absolute@^1.0.0: resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== -path-key@^3.1.0: +path-key@^3.0.0, path-key@^3.1.0: version "3.1.1" resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== @@ -9807,11 +9861,6 @@ readable-stream@^4.0.0: process "^0.11.10" string_decoder "^1.3.0" -readdirp@^4.0.1: - version "4.1.2" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-4.1.2.tgz#eb85801435fbf2a7ee58f19e0921b068fc69948d" - integrity sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg== - readdirp@~3.6.0: version "3.6.0" resolved "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" @@ -10072,32 +10121,33 @@ robust-predicates@^3.0.2: resolved "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz#d5b28528c4824d20fc48df1928d41d9efa1ad771" integrity sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg== -rollup@^4.34.8: - version "4.35.0" - resolved "https://registry.npmjs.org/rollup/-/rollup-4.35.0.tgz#76c95dba17a579df4c00c3955aed32aa5d4dc66d" - integrity sha512-kg6oI4g+vc41vePJyO6dHt/yl0Rz3Thv0kJeVQ3D1kS3E5XSuKbPc29G4IpT/Kv1KQwgHVcN+HtyS+HYLNSvQg== +rollup@^4.19.0: + version "4.41.1" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.41.1.tgz#46ddc1b33cf1b0baa99320d3b0b4973dc2253b6a" + integrity sha512-cPmwD3FnFv8rKMBc1MxWCwVQFxwf1JEmSX3iQXrRVVG15zerAIXRjMFVWnd5Q5QvgKF7Aj+5ykXFhUl+QGnyOw== dependencies: - "@types/estree" "1.0.6" + "@types/estree" "1.0.7" optionalDependencies: - "@rollup/rollup-android-arm-eabi" "4.35.0" - "@rollup/rollup-android-arm64" "4.35.0" - "@rollup/rollup-darwin-arm64" "4.35.0" - "@rollup/rollup-darwin-x64" "4.35.0" - "@rollup/rollup-freebsd-arm64" "4.35.0" - "@rollup/rollup-freebsd-x64" "4.35.0" - "@rollup/rollup-linux-arm-gnueabihf" "4.35.0" - "@rollup/rollup-linux-arm-musleabihf" "4.35.0" - "@rollup/rollup-linux-arm64-gnu" "4.35.0" - "@rollup/rollup-linux-arm64-musl" "4.35.0" - "@rollup/rollup-linux-loongarch64-gnu" "4.35.0" - "@rollup/rollup-linux-powerpc64le-gnu" "4.35.0" - "@rollup/rollup-linux-riscv64-gnu" "4.35.0" - "@rollup/rollup-linux-s390x-gnu" "4.35.0" - "@rollup/rollup-linux-x64-gnu" "4.35.0" - "@rollup/rollup-linux-x64-musl" "4.35.0" - "@rollup/rollup-win32-arm64-msvc" "4.35.0" - "@rollup/rollup-win32-ia32-msvc" "4.35.0" - "@rollup/rollup-win32-x64-msvc" "4.35.0" + "@rollup/rollup-android-arm-eabi" "4.41.1" + "@rollup/rollup-android-arm64" "4.41.1" + "@rollup/rollup-darwin-arm64" "4.41.1" + "@rollup/rollup-darwin-x64" "4.41.1" + "@rollup/rollup-freebsd-arm64" "4.41.1" + "@rollup/rollup-freebsd-x64" "4.41.1" + "@rollup/rollup-linux-arm-gnueabihf" "4.41.1" + "@rollup/rollup-linux-arm-musleabihf" "4.41.1" + "@rollup/rollup-linux-arm64-gnu" "4.41.1" + "@rollup/rollup-linux-arm64-musl" "4.41.1" + "@rollup/rollup-linux-loongarch64-gnu" "4.41.1" + "@rollup/rollup-linux-powerpc64le-gnu" "4.41.1" + "@rollup/rollup-linux-riscv64-gnu" "4.41.1" + "@rollup/rollup-linux-riscv64-musl" "4.41.1" + "@rollup/rollup-linux-s390x-gnu" "4.41.1" + "@rollup/rollup-linux-x64-gnu" "4.41.1" + "@rollup/rollup-linux-x64-musl" "4.41.1" + "@rollup/rollup-win32-arm64-msvc" "4.41.1" + "@rollup/rollup-win32-ia32-msvc" "4.41.1" + "@rollup/rollup-win32-x64-msvc" "4.41.1" fsevents "~2.3.2" rope-sequence@^1.3.0: @@ -10404,6 +10454,11 @@ side-channel@^1.0.6, side-channel@^1.1.0: side-channel-map "^1.0.1" side-channel-weakmap "^1.0.2" +signal-exit@^3.0.3: + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + signal-exit@^4.0.1: version "4.1.0" resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" @@ -10689,6 +10744,11 @@ strip-bom@^3.0.0: resolved "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== +strip-final-newline@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" + integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== + strip-indent@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001" @@ -10969,17 +11029,12 @@ tinycolor2@^1.4.1: resolved "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz#f98007460169b0263b97072c5ae92484ce02d09e" integrity sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw== -tinyexec@^0.3.2: - version "0.3.2" - resolved "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz#941794e657a85e496577995c6eef66f53f42b3d2" - integrity sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA== - -tinyglobby@^0.2.11: - version "0.2.12" - resolved "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.12.tgz#ac941a42e0c5773bd0b5d08f32de82e74a1a61b5" - integrity sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww== +tinyglobby@^0.2.1: + version "0.2.14" + resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.14.tgz#5280b0cf3f972b050e74ae88406c0a6a58f4079d" + integrity sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ== dependencies: - fdir "^6.4.3" + fdir "^6.4.4" picomatch "^4.0.2" tinyrainbow@^1.2.0: @@ -11152,26 +11207,26 @@ tslib@~2.5.0: resolved "https://registry.npmjs.org/tslib/-/tslib-2.5.3.tgz#24944ba2d990940e6e982c4bea147aba80209913" integrity sha512-mSxlJJwl3BMEQCUNnxXBU9jP4JBktcEGhURcPR6VQVlnP0FdDEsIaz0C35dXNGLyRfrATNofF0F5p2KPxQgB+w== -tsup@8.4.0, tsup@^8.4.0: - version "8.4.0" - resolved "https://registry.npmjs.org/tsup/-/tsup-8.4.0.tgz#2fdf537e7abc8f1ccbbbfe4228f16831457d4395" - integrity sha512-b+eZbPCjz10fRryaAA7C8xlIHnf8VnsaRqydheLIqwG/Mcpfk8Z5zp3HayX7GaTygkigHl5cBUs+IhcySiIexQ== +tsup@8.3.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/tsup/-/tsup-8.3.0.tgz#c7dae40b13d11d81fb144c0f90077a99102a572a" + integrity sha512-ALscEeyS03IomcuNdFdc0YWGVIkwH1Ws7nfTbAPuoILvEV2hpGQAY72LIOjglGo4ShWpZfpBqP/jpQVCzqYQag== dependencies: - bundle-require "^5.1.0" + bundle-require "^5.0.0" cac "^6.7.14" - chokidar "^4.0.3" - consola "^3.4.0" - debug "^4.4.0" - esbuild "^0.25.0" + chokidar "^3.6.0" + consola "^3.2.3" + debug "^4.3.5" + esbuild "^0.23.0" + execa "^5.1.1" joycon "^3.1.1" - picocolors "^1.1.1" + picocolors "^1.0.1" postcss-load-config "^6.0.1" resolve-from "^5.0.0" - rollup "^4.34.8" + rollup "^4.19.0" source-map "0.8.0-beta.0" sucrase "^3.35.0" - tinyexec "^0.3.2" - tinyglobby "^0.2.11" + tinyglobby "^0.2.1" tree-kill "^1.2.2" tsutils@^3.21.0: From 26b62c4a70ee7aee780504fd2bace8c023e66482 Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Wed, 28 May 2025 02:17:23 +0530 Subject: [PATCH 095/201] fix: tsup version 8.4.0 --- live/package.json | 2 +- packages/decorators/package.json | 2 +- packages/editor/package.json | 2 +- packages/hooks/package.json | 2 +- packages/ui/package.json | 2 +- packages/utils/package.json | 2 +- yarn.lock | 113 +++++++++++-------------------- 7 files changed, 44 insertions(+), 81 deletions(-) diff --git a/live/package.json b/live/package.json index 3dcb8b35e..f020fb16e 100644 --- a/live/package.json +++ b/live/package.json @@ -57,7 +57,7 @@ "concurrently": "^9.0.1", "nodemon": "^3.1.7", "ts-node": "^10.9.2", - "tsup": "8.3.0", + "tsup": "8.4.0", "typescript": "5.3.3" } } diff --git a/packages/decorators/package.json b/packages/decorators/package.json index 198fdc698..433b5c11a 100644 --- a/packages/decorators/package.json +++ b/packages/decorators/package.json @@ -27,7 +27,7 @@ "@types/node": "^20.14.9", "@types/reflect-metadata": "^0.1.0", "@types/ws": "^8.5.10", - "tsup": "8.3.0", + "tsup": "8.4.0", "typescript": "^5.3.3" }, "peerDependencies": { diff --git a/packages/editor/package.json b/packages/editor/package.json index 5a899f738..f2da418c7 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -81,7 +81,7 @@ "@types/react": "^18.3.11", "@types/react-dom": "^18.2.18", "postcss": "^8.4.38", - "tsup": "8.3.0", + "tsup": "8.4.0", "typescript": "5.3.3" }, "keywords": [ diff --git a/packages/hooks/package.json b/packages/hooks/package.json index 320203e2d..e477c6446 100644 --- a/packages/hooks/package.json +++ b/packages/hooks/package.json @@ -22,7 +22,7 @@ "@plane/eslint-config": "*", "@types/node": "^22.5.4", "@types/react": "^18.3.11", - "tsup": "8.3.0", + "tsup": "8.4.0", "typescript": "^5.3.3" } } diff --git a/packages/ui/package.json b/packages/ui/package.json index e0920d49e..2581999f3 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -71,7 +71,7 @@ "postcss-cli": "^11.0.0", "postcss-nested": "^6.0.1", "storybook": "^8.1.1", - "tsup": "8.3.0", + "tsup": "8.4.0", "typescript": "5.3.3" } } diff --git a/packages/utils/package.json b/packages/utils/package.json index 07504d9a4..75df0d263 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -29,7 +29,7 @@ "@types/node": "^22.5.4", "@types/react": "^18.3.11", "@types/zxcvbn": "^4.4.5", - "tsup": "8.3.0", + "tsup": "8.4.0", "typescript": "^5.3.3" } } diff --git a/yarn.lock b/yarn.lock index 6fca25f4d..27e8f5165 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4614,7 +4614,7 @@ buffer@^6.0.3: base64-js "^1.3.1" ieee754 "^1.2.1" -bundle-require@^5.0.0: +bundle-require@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/bundle-require/-/bundle-require-5.1.0.tgz#8db66f41950da3d77af1ef3322f4c3e04009faee" integrity sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA== @@ -4771,6 +4771,13 @@ chokidar@^3.3.0, chokidar@^3.5.2, chokidar@^3.5.3, chokidar@^3.6.0: optionalDependencies: fsevents "~2.3.2" +chokidar@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-4.0.3.tgz#7be37a4c03c9aee1ecfe862a4a23b2c70c205d30" + integrity sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA== + dependencies: + readdirp "^4.0.1" + chownr@^1.1.1: version "1.1.4" resolved "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" @@ -4995,7 +5002,7 @@ concurrently@^9.0.1: tree-kill "^1.2.2" yargs "^17.7.2" -consola@^3.2.3: +consola@^3.4.0: version "3.4.2" resolved "https://registry.yarnpkg.com/consola/-/consola-3.4.2.tgz#5af110145397bb67afdab77013fdc34cae590ea7" integrity sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA== @@ -5102,7 +5109,7 @@ cross-fetch@^3.1.5: dependencies: node-fetch "^2.7.0" -cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: +cross-spawn@^7.0.0, cross-spawn@^7.0.2: version "7.0.6" resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== @@ -5381,7 +5388,7 @@ debug@^3.2.7: dependencies: ms "^2.1.1" -debug@^4.3.5: +debug@^4.4.0: version "4.4.1" resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.1.tgz#e5a8bc6cbc4c6cd3e64308b0693a3d4fa550189b" integrity sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ== @@ -5940,7 +5947,7 @@ esbuild-register@^3.5.0: dependencies: debug "^4.3.4" -esbuild@0.25.0, "esbuild@^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0", esbuild@^0.23.0: +esbuild@0.25.0, "esbuild@^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0", esbuild@^0.25.0: version "0.25.0" resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz#0de1787a77206c5a79eeb634a623d39b5006ce92" integrity sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw== @@ -6276,21 +6283,6 @@ events@^3.2.0, events@^3.3.0: resolved "https://registry.npmjs.org/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== -execa@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" - integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== - dependencies: - cross-spawn "^7.0.3" - get-stream "^6.0.0" - human-signals "^2.1.0" - is-stream "^2.0.0" - merge-stream "^2.0.0" - npm-run-path "^4.0.1" - onetime "^5.1.2" - signal-exit "^3.0.3" - strip-final-newline "^2.0.0" - expand-template@^2.0.3: version "2.0.3" resolved "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" @@ -6762,11 +6754,6 @@ get-stdin@^9.0.0: resolved "https://registry.npmjs.org/get-stdin/-/get-stdin-9.0.0.tgz#3983ff82e03d56f1b2ea0d3e60325f39d703a575" integrity sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA== -get-stream@^6.0.0: - version "6.0.1" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" - integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== - get-symbol-description@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz#7bdd54e0befe8ffc9f3b4e203220d9f1e881b6ee" @@ -7094,11 +7081,6 @@ https-proxy-agent@^7.0.6: agent-base "^7.1.2" debug "4" -human-signals@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" - integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== - hyphen@^1.6.4: version "1.10.6" resolved "https://registry.npmjs.org/hyphen/-/hyphen-1.10.6.tgz#0e779d280e696102b97d7e42f5ca5de2cc97e274" @@ -8321,11 +8303,6 @@ mime@1.6.0: resolved "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== -mimic-fn@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" - integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== - mimic-response@^3.1.0: version "3.1.0" resolved "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9" @@ -8587,13 +8564,6 @@ normalize.css@^8.0.1: resolved "https://registry.npmjs.org/normalize.css/-/normalize.css-8.0.1.tgz#9b98a208738b9cc2634caacbc42d131c97487bf3" integrity sha512-qizSNPO93t1YUuUhP22btGOo3chcvDFqFaj2TRybP0DMxkHOCTYwp3n34fel4a31ORXy4m1Xq0Gyqpb5m33qIg== -npm-run-path@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" - integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== - dependencies: - path-key "^3.0.0" - nprogress@^0.2.0: version "0.2.0" resolved "https://registry.npmjs.org/nprogress/-/nprogress-0.2.0.tgz#cb8f34c53213d895723fcbab907e9422adbcafb1" @@ -8732,13 +8702,6 @@ one-time@^1.0.0: dependencies: fn.name "1.x.x" -onetime@^5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" - integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== - dependencies: - mimic-fn "^2.1.0" - open@^8.0.4: version "8.4.2" resolved "https://registry.npmjs.org/open/-/open-8.4.2.tgz#5b5ffe2a8f793dcd2aad73e550cb87b59cb084f9" @@ -8921,7 +8884,7 @@ path-is-absolute@^1.0.0: resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== -path-key@^3.0.0, path-key@^3.1.0: +path-key@^3.1.0: version "3.1.1" resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== @@ -9861,6 +9824,11 @@ readable-stream@^4.0.0: process "^0.11.10" string_decoder "^1.3.0" +readdirp@^4.0.1: + version "4.1.2" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-4.1.2.tgz#eb85801435fbf2a7ee58f19e0921b068fc69948d" + integrity sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg== + readdirp@~3.6.0: version "3.6.0" resolved "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" @@ -10121,7 +10089,7 @@ robust-predicates@^3.0.2: resolved "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz#d5b28528c4824d20fc48df1928d41d9efa1ad771" integrity sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg== -rollup@^4.19.0: +rollup@^4.34.8: version "4.41.1" resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.41.1.tgz#46ddc1b33cf1b0baa99320d3b0b4973dc2253b6a" integrity sha512-cPmwD3FnFv8rKMBc1MxWCwVQFxwf1JEmSX3iQXrRVVG15zerAIXRjMFVWnd5Q5QvgKF7Aj+5ykXFhUl+QGnyOw== @@ -10454,11 +10422,6 @@ side-channel@^1.0.6, side-channel@^1.1.0: side-channel-map "^1.0.1" side-channel-weakmap "^1.0.2" -signal-exit@^3.0.3: - version "3.0.7" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" - integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== - signal-exit@^4.0.1: version "4.1.0" resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" @@ -10744,11 +10707,6 @@ strip-bom@^3.0.0: resolved "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== -strip-final-newline@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" - integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== - strip-indent@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001" @@ -11029,7 +10987,12 @@ tinycolor2@^1.4.1: resolved "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz#f98007460169b0263b97072c5ae92484ce02d09e" integrity sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw== -tinyglobby@^0.2.1: +tinyexec@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/tinyexec/-/tinyexec-0.3.2.tgz#941794e657a85e496577995c6eef66f53f42b3d2" + integrity sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA== + +tinyglobby@^0.2.11: version "0.2.14" resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.14.tgz#5280b0cf3f972b050e74ae88406c0a6a58f4079d" integrity sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ== @@ -11207,26 +11170,26 @@ tslib@~2.5.0: resolved "https://registry.npmjs.org/tslib/-/tslib-2.5.3.tgz#24944ba2d990940e6e982c4bea147aba80209913" integrity sha512-mSxlJJwl3BMEQCUNnxXBU9jP4JBktcEGhURcPR6VQVlnP0FdDEsIaz0C35dXNGLyRfrATNofF0F5p2KPxQgB+w== -tsup@8.3.0: - version "8.3.0" - resolved "https://registry.yarnpkg.com/tsup/-/tsup-8.3.0.tgz#c7dae40b13d11d81fb144c0f90077a99102a572a" - integrity sha512-ALscEeyS03IomcuNdFdc0YWGVIkwH1Ws7nfTbAPuoILvEV2hpGQAY72LIOjglGo4ShWpZfpBqP/jpQVCzqYQag== +tsup@8.4.0: + version "8.4.0" + resolved "https://registry.yarnpkg.com/tsup/-/tsup-8.4.0.tgz#2fdf537e7abc8f1ccbbbfe4228f16831457d4395" + integrity sha512-b+eZbPCjz10fRryaAA7C8xlIHnf8VnsaRqydheLIqwG/Mcpfk8Z5zp3HayX7GaTygkigHl5cBUs+IhcySiIexQ== dependencies: - bundle-require "^5.0.0" + bundle-require "^5.1.0" cac "^6.7.14" - chokidar "^3.6.0" - consola "^3.2.3" - debug "^4.3.5" - esbuild "^0.23.0" - execa "^5.1.1" + chokidar "^4.0.3" + consola "^3.4.0" + debug "^4.4.0" + esbuild "^0.25.0" joycon "^3.1.1" - picocolors "^1.0.1" + picocolors "^1.1.1" postcss-load-config "^6.0.1" resolve-from "^5.0.0" - rollup "^4.19.0" + rollup "^4.34.8" source-map "0.8.0-beta.0" sucrase "^3.35.0" - tinyglobby "^0.2.1" + tinyexec "^0.3.2" + tinyglobby "^0.2.11" tree-kill "^1.2.2" tsutils@^3.21.0: From 141cb17e8a9d6bc26dacb1c7173356d698b93529 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Wed, 28 May 2025 19:03:14 +0530 Subject: [PATCH 096/201] fix: Optimize image uploads in Editor (#7129) * fix: memoize file upload functions * chore: update extension name * chore: update notation * chore: resolve chokidar package * fix: spelling mistakes --- package.json | 3 +- .../custom-image/components/image-block.tsx | 4 +-- .../custom-image/components/image-node.tsx | 4 +-- .../components/image-uploader.tsx | 34 ++++++++++++++----- .../editor/src/core/hooks/use-file-upload.ts | 10 +++++- 5 files changed, 41 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index 593d84459..050615878 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,8 @@ "nanoid": "3.3.8", "esbuild": "0.25.0", "@babel/helpers": "7.26.10", - "@babel/runtime": "7.26.10" + "@babel/runtime": "7.26.10", + "chokidar": "3.6.0" }, "packageManager": "yarn@1.22.22" } diff --git a/packages/editor/src/core/extensions/custom-image/components/image-block.tsx b/packages/editor/src/core/extensions/custom-image/components/image-block.tsx index 0cc38f5a4..5dfbad012 100644 --- a/packages/editor/src/core/extensions/custom-image/components/image-block.tsx +++ b/packages/editor/src/core/extensions/custom-image/components/image-block.tsx @@ -3,7 +3,7 @@ import React, { useRef, useState, useCallback, useLayoutEffect, useEffect } from // plane utils import { cn } from "@plane/utils"; // extensions -import { CustoBaseImageNodeViewProps, ImageToolbarRoot } from "@/extensions/custom-image"; +import { CustomBaseImageNodeViewProps, ImageToolbarRoot } from "@/extensions/custom-image"; import { ImageUploadStatus } from "./upload-status"; const MIN_SIZE = 100; @@ -38,7 +38,7 @@ const ensurePixelString = (value: Pixel | TDefault | number | undefin return value; }; -type CustomImageBlockProps = CustoBaseImageNodeViewProps & { +type CustomImageBlockProps = CustomBaseImageNodeViewProps & { imageFromFileSystem: string | undefined; setFailedToLoadImage: (isError: boolean) => void; editorContainer: HTMLDivElement | null; diff --git a/packages/editor/src/core/extensions/custom-image/components/image-node.tsx b/packages/editor/src/core/extensions/custom-image/components/image-node.tsx index f8bfcf4a1..8dfe6974b 100644 --- a/packages/editor/src/core/extensions/custom-image/components/image-node.tsx +++ b/packages/editor/src/core/extensions/custom-image/components/image-node.tsx @@ -7,7 +7,7 @@ import { CustomImageBlock, CustomImageUploader, ImageAttributes } from "@/extens // helpers import { getExtensionStorage } from "@/helpers/get-extension-storage"; -export type CustoBaseImageNodeViewProps = { +export type CustomBaseImageNodeViewProps = { getPos: () => number; editor: Editor; node: NodeViewProps["node"] & { @@ -17,7 +17,7 @@ export type CustoBaseImageNodeViewProps = { selected: boolean; }; -export type CustomImageNodeProps = NodeViewProps & CustoBaseImageNodeViewProps; +export type CustomImageNodeProps = NodeViewProps & CustomBaseImageNodeViewProps; export const CustomImageNode = (props: CustomImageNodeProps) => { const { getPos, editor, node, updateAttributes, selected } = props; diff --git a/packages/editor/src/core/extensions/custom-image/components/image-uploader.tsx b/packages/editor/src/core/extensions/custom-image/components/image-uploader.tsx index 5af4f556d..7a7f71f00 100644 --- a/packages/editor/src/core/extensions/custom-image/components/image-uploader.tsx +++ b/packages/editor/src/core/extensions/custom-image/components/image-uploader.tsx @@ -6,12 +6,14 @@ import { cn } from "@plane/utils"; import { ACCEPTED_IMAGE_MIME_TYPES } from "@/constants/config"; import { CORE_EXTENSIONS } from "@/constants/extension"; // extensions -import { CustoBaseImageNodeViewProps, getImageComponentImageFileMap } from "@/extensions/custom-image"; +import { CustomBaseImageNodeViewProps, getImageComponentImageFileMap } from "@/extensions/custom-image"; +// helpers +import { EFileError } from "@/helpers/file"; +import { getExtensionStorage } from "@/helpers/get-extension-storage"; // hooks import { useUploader, useDropZone, uploadFirstFileAndInsertRemaining } from "@/hooks/use-file-upload"; -import { getExtensionStorage } from "@/helpers/get-extension-storage"; -type CustomImageUploaderProps = CustoBaseImageNodeViewProps & { +type CustomImageUploaderProps = CustomBaseImageNodeViewProps & { maxFileSize: number; loadImageFromFileSystem: (file: string) => void; failedToLoadImage: boolean; @@ -71,23 +73,39 @@ export const CustomImageUploader = (props: CustomImageUploaderProps) => { }, [imageComponentImageFileMap, imageEntityId, updateAttributes, getPos] ); + + const uploadImageEditorCommand = useCallback( + async (file: File) => await editor?.commands.uploadImage(imageEntityId ?? "", file), + [editor, imageEntityId] + ); + + const handleProgressStatus = useCallback( + (isUploading: boolean) => { + getExtensionStorage(editor, CORE_EXTENSIONS.UTILITY).uploadInProgress = isUploading; + }, + [editor] + ); + // hooks const { isUploading: isImageBeingUploaded, uploadFile } = useUploader({ acceptedMimeTypes: ACCEPTED_IMAGE_MIME_TYPES, // @ts-expect-error - TODO: fix typings, and don't remove await from here for now - editorCommand: async (file) => await editor?.commands.uploadImage(imageEntityId, file), - handleProgressStatus: (isUploading) => { - getExtensionStorage(editor, CORE_EXTENSIONS.UTILITY).uploadInProgress = isUploading; - }, + editorCommand: uploadImageEditorCommand, + handleProgressStatus, loadFileFromFileSystem: loadImageFromFileSystem, maxFileSize, onUpload, }); + + const handleInvalidFile = useCallback((_error: EFileError, message: string) => { + alert(message); + }, []); + const { draggedInside, onDrop, onDragEnter, onDragLeave } = useDropZone({ acceptedMimeTypes: ACCEPTED_IMAGE_MIME_TYPES, editor, maxFileSize, - onInvalidFile: (_error, message) => alert(message), + onInvalidFile: handleInvalidFile, pos: getPos(), type: "image", uploader: uploadFile, diff --git a/packages/editor/src/core/hooks/use-file-upload.ts b/packages/editor/src/core/hooks/use-file-upload.ts index e40c15913..51116fe52 100644 --- a/packages/editor/src/core/hooks/use-file-upload.ts +++ b/packages/editor/src/core/hooks/use-file-upload.ts @@ -74,7 +74,15 @@ export const useUploader = (args: TUploaderArgs) => { setIsUploading(false); } }, - [acceptedMimeTypes, editorCommand, handleProgressStatus, loadFileFromFileSystem, maxFileSize, onUpload] + [ + acceptedMimeTypes, + editorCommand, + handleProgressStatus, + loadFileFromFileSystem, + maxFileSize, + onInvalidFile, + onUpload, + ] ); return { isUploading, uploadFile }; From 4a97d7c28c26b6bddc0ea2450d48d4ee3185008a Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Thu, 29 May 2025 17:53:48 +0530 Subject: [PATCH 097/201] fix: adding url validations for workspace name and user name --- apiserver/plane/app/serializers/user.py | 11 +++++++++++ apiserver/plane/app/serializers/workspace.py | 13 +++++++++++++ apiserver/plane/app/views/workspace/base.py | 7 +++++++ apiserver/plane/utils/url.py | 8 ++++++++ 4 files changed, 39 insertions(+) diff --git a/apiserver/plane/app/serializers/user.py b/apiserver/plane/app/serializers/user.py index ebc002c9c..c0e106178 100644 --- a/apiserver/plane/app/serializers/user.py +++ b/apiserver/plane/app/serializers/user.py @@ -3,11 +3,22 @@ from rest_framework import serializers # Module import from plane.db.models import Account, Profile, User, Workspace, WorkspaceMemberInvite +from plane.utils.url import contains_url from .base import BaseSerializer class UserSerializer(BaseSerializer): + def validate_first_name(self, value): + if contains_url(value): + raise serializers.ValidationError("First name cannot contain a URL.") + return value + + def validate_last_name(self, value): + if contains_url(value): + raise serializers.ValidationError("Last name cannot contain a URL.") + return value + class Meta: model = User # Exclude password field from the serializer diff --git a/apiserver/plane/app/serializers/workspace.py b/apiserver/plane/app/serializers/workspace.py index 9fba7256e..7a9289266 100644 --- a/apiserver/plane/app/serializers/workspace.py +++ b/apiserver/plane/app/serializers/workspace.py @@ -25,10 +25,12 @@ from plane.db.models import ( WorkspaceUserPreference, ) from plane.utils.constants import RESTRICTED_WORKSPACE_SLUGS +from plane.utils.url import contains_url # Django imports from django.core.validators import URLValidator from django.core.exceptions import ValidationError +import re class WorkSpaceSerializer(DynamicBaseSerializer): @@ -36,10 +38,21 @@ class WorkSpaceSerializer(DynamicBaseSerializer): logo_url = serializers.CharField(read_only=True) role = serializers.IntegerField(read_only=True) + def validate_name(self, value): + # Check if the name contains a URL + if contains_url(value): + raise serializers.ValidationError("Name must not contain URLs") + return value + def validate_slug(self, value): # Check if the slug is restricted if value in RESTRICTED_WORKSPACE_SLUGS: raise serializers.ValidationError("Slug is not valid") + # Slug should only contain alphanumeric characters, hyphens, and underscores + if not re.match(r"^[a-zA-Z0-9_-]+$", value): + raise serializers.ValidationError( + "Slug can only contain letters, numbers, hyphens (-), and underscores (_)" + ) return value class Meta: diff --git a/apiserver/plane/app/views/workspace/base.py b/apiserver/plane/app/views/workspace/base.py index e92e61e51..8ca29526d 100644 --- a/apiserver/plane/app/views/workspace/base.py +++ b/apiserver/plane/app/views/workspace/base.py @@ -43,6 +43,7 @@ from django.views.decorators.vary import vary_on_cookie from plane.utils.constants import RESTRICTED_WORKSPACE_SLUGS from plane.license.utils.instance_value import get_configuration_value from plane.bgtasks.workspace_seed_task import workspace_seed +from plane.utils.url import contains_url class WorkSpaceViewSet(BaseViewSet): @@ -109,6 +110,12 @@ class WorkSpaceViewSet(BaseViewSet): status=status.HTTP_400_BAD_REQUEST, ) + if contains_url(name): + return Response( + {"error": "Name cannot contain a URL"}, + status=status.HTTP_400_BAD_REQUEST, + ) + if serializer.is_valid(raise_exception=True): serializer.save(owner=request.user) # Create Workspace member diff --git a/apiserver/plane/utils/url.py b/apiserver/plane/utils/url.py index e485f93df..1b4a229a8 100644 --- a/apiserver/plane/utils/url.py +++ b/apiserver/plane/utils/url.py @@ -4,6 +4,14 @@ from typing import Optional from urllib.parse import urlparse, urlunparse +def contains_url(value: str) -> bool: + """ + Check if the value contains a URL. + """ + url_pattern = re.compile(r"https?://|www\\.") + return bool(url_pattern.search(value)) + + def is_valid_url(url: str) -> bool: """ Validates whether the given string is a well-formed URL. From b16a5851021ee3880d735785c661a8a95d964e29 Mon Sep 17 00:00:00 2001 From: Vipin Chaudhary Date: Fri, 30 May 2025 18:17:03 +0530 Subject: [PATCH 098/201] [WIKI-343] [WIKI-312] Fix: html characters (#7049) * fix: handle symbols and space * chore: refactor --- packages/editor/src/core/hooks/use-editor.ts | 5 +++-- packages/editor/src/core/hooks/use-read-only-editor.ts | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/editor/src/core/hooks/use-editor.ts b/packages/editor/src/core/hooks/use-editor.ts index a0cd73915..ce3cdbe5f 100644 --- a/packages/editor/src/core/hooks/use-editor.ts +++ b/packages/editor/src/core/hooks/use-editor.ts @@ -81,6 +81,7 @@ export const useEditor = (props: CustomEditorProps) => { immediatelyRender: false, shouldRerenderOnTransaction: false, autofocus, + parseOptions: { preserveWhitespace: true }, editorProps: { ...CoreEditorProps({ editorClassName, @@ -119,7 +120,7 @@ export const useEditor = (props: CustomEditorProps) => { const isUploadInProgress = getExtensionStorage(editor, CORE_EXTENSIONS.UTILITY)?.uploadInProgress; if (!editor.isDestroyed && !isUploadInProgress) { try { - editor.commands.setContent(value, false, { preserveWhitespace: "full" }); + editor.commands.setContent(value, false, { preserveWhitespace: true }); if (editor.state.selection) { const docLength = editor.state.doc.content.size; const relativePosition = Math.min(editor.state.selection.from, docLength - 1); @@ -153,7 +154,7 @@ export const useEditor = (props: CustomEditorProps) => { editor?.chain().setMeta(CORE_EDITOR_META.SKIP_FILE_DELETION, true).clearContent(emitUpdate).run(); }, setEditorValue: (content: string, emitUpdate = false) => { - editor?.commands.setContent(content, emitUpdate, { preserveWhitespace: "full" }); + editor?.commands.setContent(content, emitUpdate, { preserveWhitespace: true }); }, setEditorValueAtCursorPosition: (content: string) => { if (editor?.state.selection) { diff --git a/packages/editor/src/core/hooks/use-read-only-editor.ts b/packages/editor/src/core/hooks/use-read-only-editor.ts index 5bd731d5f..6a6e25d9f 100644 --- a/packages/editor/src/core/hooks/use-read-only-editor.ts +++ b/packages/editor/src/core/hooks/use-read-only-editor.ts @@ -46,6 +46,7 @@ export const useReadOnlyEditor = (props: CustomReadOnlyEditorProps) => { immediatelyRender: true, shouldRerenderOnTransaction: false, content: typeof initialValue === "string" && initialValue.trim() !== "" ? initialValue : "

", + parseOptions: { preserveWhitespace: true }, editorProps: { ...CoreReadOnlyEditorProps({ editorClassName, @@ -71,7 +72,7 @@ export const useReadOnlyEditor = (props: CustomReadOnlyEditorProps) => { // for syncing swr data on tab refocus etc useEffect(() => { if (initialValue === null || initialValue === undefined) return; - if (editor && !editor.isDestroyed) editor?.commands.setContent(initialValue, false, { preserveWhitespace: "full" }); + if (editor && !editor.isDestroyed) editor?.commands.setContent(initialValue, false, { preserveWhitespace: true }); }, [editor, initialValue]); useImperativeHandle(forwardedRef, () => ({ @@ -79,7 +80,7 @@ export const useReadOnlyEditor = (props: CustomReadOnlyEditorProps) => { editor?.chain().setMeta(CORE_EDITOR_META.SKIP_FILE_DELETION, true).clearContent(emitUpdate).run(); }, setEditorValue: (content: string, emitUpdate = false) => { - editor?.commands.setContent(content, emitUpdate, { preserveWhitespace: "full" }); + editor?.commands.setContent(content, emitUpdate, { preserveWhitespace: true }); }, getMarkDown: (): string => { const markdownOutput = editor?.storage.markdown.getMarkdown(); From 01b685ea5726f8c9e6f994d3ca04de939b36537e Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Fri, 30 May 2025 18:18:05 +0530 Subject: [PATCH 099/201] [WIKI-181] refactor: invalid file handling #7139 --- .../components/image-uploader.tsx | 15 ++---- .../editor/src/core/hooks/use-file-upload.ts | 54 ++++++------------- .../editor/src/core/plugins/file/delete.ts | 4 +- 3 files changed, 23 insertions(+), 50 deletions(-) diff --git a/packages/editor/src/core/extensions/custom-image/components/image-uploader.tsx b/packages/editor/src/core/extensions/custom-image/components/image-uploader.tsx index 7a7f71f00..17c9f8177 100644 --- a/packages/editor/src/core/extensions/custom-image/components/image-uploader.tsx +++ b/packages/editor/src/core/extensions/custom-image/components/image-uploader.tsx @@ -86,6 +86,10 @@ export const CustomImageUploader = (props: CustomImageUploaderProps) => { [editor] ); + const handleInvalidFile = useCallback((_error: EFileError, _file: File, message: string) => { + alert(message); + }, []); + // hooks const { isUploading: isImageBeingUploaded, uploadFile } = useUploader({ acceptedMimeTypes: ACCEPTED_IMAGE_MIME_TYPES, @@ -94,18 +98,12 @@ export const CustomImageUploader = (props: CustomImageUploaderProps) => { handleProgressStatus, loadFileFromFileSystem: loadImageFromFileSystem, maxFileSize, + onInvalidFile: handleInvalidFile, onUpload, }); - const handleInvalidFile = useCallback((_error: EFileError, message: string) => { - alert(message); - }, []); - const { draggedInside, onDrop, onDragEnter, onDragLeave } = useDropZone({ - acceptedMimeTypes: ACCEPTED_IMAGE_MIME_TYPES, editor, - maxFileSize, - onInvalidFile: handleInvalidFile, pos: getPos(), type: "image", uploader: uploadFile, @@ -140,11 +138,8 @@ export const CustomImageUploader = (props: CustomImageUploaderProps) => { return; } await uploadFirstFileAndInsertRemaining({ - acceptedMimeTypes: ACCEPTED_IMAGE_MIME_TYPES, editor, filesList, - maxFileSize, - onInvalidFile: (_error, message) => alert(message), pos: getPos(), type: "image", uploader: uploadFile, diff --git a/packages/editor/src/core/hooks/use-file-upload.ts b/packages/editor/src/core/hooks/use-file-upload.ts index 51116fe52..dce48cca5 100644 --- a/packages/editor/src/core/hooks/use-file-upload.ts +++ b/packages/editor/src/core/hooks/use-file-upload.ts @@ -9,11 +9,11 @@ import { TEditorCommands } from "@/types"; type TUploaderArgs = { acceptedMimeTypes: string[]; - editorCommand: (file: File) => Promise; + editorCommand: (file: File) => Promise; handleProgressStatus?: (isUploading: boolean) => void; loadFileFromFileSystem?: (file: string) => void; maxFileSize: number; - onInvalidFile: (error: EFileError, message: string) => void; + onInvalidFile: (error: EFileError, file: File, message: string) => void; onUpload: (url: string, file: File) => void; }; @@ -38,7 +38,7 @@ export const useUploader = (args: TUploaderArgs) => { acceptedMimeTypes, file, maxFileSize, - onError: onInvalidFile, + onError: (error, message) => onInvalidFile(error, file, message), }); if (!isValid) { handleProgressStatus?.(false); @@ -60,7 +60,7 @@ export const useUploader = (args: TUploaderArgs) => { }; reader.readAsDataURL(file); } - const url: string = await editorCommand(file); + const url = await editorCommand(file); if (!url) { throw new Error("Something went wrong while uploading the file."); @@ -89,17 +89,14 @@ export const useUploader = (args: TUploaderArgs) => { }; type TDropzoneArgs = { - acceptedMimeTypes: string[]; editor: Editor; - maxFileSize: number; - onInvalidFile: (error: EFileError, message: string) => void; pos: number; type: Extract; uploader: (file: File) => Promise; }; export const useDropZone = (args: TDropzoneArgs) => { - const { acceptedMimeTypes, editor, maxFileSize, onInvalidFile, pos, type, uploader } = args; + const { editor, pos, type, uploader } = args; // states const [isDragging, setIsDragging] = useState(false); const [draggedInside, setDraggedInside] = useState(false); @@ -126,22 +123,21 @@ export const useDropZone = (args: TDropzoneArgs) => { async (e: DragEvent) => { e.preventDefault(); setDraggedInside(false); - if (e.dataTransfer.files.length === 0 || !editor.isEditable) { + const filesList = e.dataTransfer.files; + + if (filesList.length === 0 || !editor.isEditable) { return; } - const filesList = e.dataTransfer.files; + await uploadFirstFileAndInsertRemaining({ - acceptedMimeTypes, editor, filesList, - maxFileSize, - onInvalidFile, pos, type, uploader, }); }, - [acceptedMimeTypes, editor, maxFileSize, onInvalidFile, pos, type, uploader] + [editor, pos, type, uploader] ); const onDragEnter = useCallback(() => setDraggedInside(true), []); const onDragLeave = useCallback(() => setDraggedInside(false), []); @@ -156,11 +152,8 @@ export const useDropZone = (args: TDropzoneArgs) => { }; type TMultipleFileArgs = { - acceptedMimeTypes: string[]; editor: Editor; filesList: FileList; - maxFileSize: number; - onInvalidFile: (error: EFileError, message: string) => void; pos: number; type: Extract; uploader: (file: File) => Promise; @@ -168,35 +161,18 @@ type TMultipleFileArgs = { // Upload the first file and insert the remaining ones for uploading multiple files export const uploadFirstFileAndInsertRemaining = async (args: TMultipleFileArgs) => { - const { acceptedMimeTypes, editor, filesList, maxFileSize, onInvalidFile, pos, type, uploader } = args; - const filteredFiles: File[] = []; - for (let i = 0; i < filesList.length; i += 1) { - const file = filesList.item(i); - if ( - file && - isFileValid({ - acceptedMimeTypes, - file, - maxFileSize, - onError: onInvalidFile, - }) - ) { - filteredFiles.push(file); - } - } - if (filteredFiles.length !== filesList.length) { - console.warn("Some files were invalid and have been ignored."); - } - if (filteredFiles.length === 0) { + const { editor, filesList, pos, type, uploader } = args; + const filesArray = Array.from(filesList); + if (filesArray.length === 0) { console.error("No files found to upload."); return; } // Upload the first file - const firstFile = filteredFiles[0]; + const firstFile = filesArray[0]; uploader(firstFile); // Insert the remaining files - const remainingFiles = filteredFiles.slice(1); + const remainingFiles = filesArray.slice(1); if (remainingFiles.length > 0) { const docSize = editor.state.doc.content.size; const posOfNextFileToBeInserted = Math.min(pos + 1, docSize); diff --git a/packages/editor/src/core/plugins/file/delete.ts b/packages/editor/src/core/plugins/file/delete.ts index b77841c22..ac69b1819 100644 --- a/packages/editor/src/core/plugins/file/delete.ts +++ b/packages/editor/src/core/plugins/file/delete.ts @@ -1,5 +1,7 @@ import { Editor } from "@tiptap/core"; import { EditorState, Plugin, PluginKey, Transaction } from "@tiptap/pm/state"; +// constants +import { CORE_EDITOR_META } from "@/constants/meta"; // plane editor imports import { NODE_FILE_MAP } from "@/plane-editor/constants/utility"; // types @@ -32,7 +34,7 @@ export const TrackFileDeletionPlugin = (editor: Editor, deleteHandler: TFileHand transactions.forEach((transaction) => { // if the transaction has meta of skipFileDeletion set to true, then return (like while clearing the editor content programmatically) - if (transaction.getMeta("skipFileDeletion")) return; + if (transaction.getMeta(CORE_EDITOR_META.SKIP_FILE_DELETION)) return; const removedFiles: TFileNode[] = []; From cb92108bf4764f4dbe6bfe33b666837bc352e00d Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Fri, 30 May 2025 18:22:20 +0530 Subject: [PATCH 100/201] [WEB-4197] chore: auth forms semantics and accessibility #7128 --- .../i18n/src/locales/cs/accessibility.json | 9 ++- .../i18n/src/locales/de/accessibility.json | 9 ++- .../i18n/src/locales/en/accessibility.json | 9 ++- .../i18n/src/locales/es/accessibility.json | 9 ++- .../i18n/src/locales/fr/accessibility.json | 9 ++- .../i18n/src/locales/id/accessibility.json | 9 ++- .../i18n/src/locales/it/accessibility.json | 9 ++- .../i18n/src/locales/ja/accessibility.json | 9 ++- .../i18n/src/locales/ko/accessibility.json | 9 ++- .../i18n/src/locales/pl/accessibility.json | 9 ++- .../i18n/src/locales/pt-BR/accessibility.json | 9 ++- .../i18n/src/locales/ro/accessibility.json | 9 ++- .../i18n/src/locales/ru/accessibility.json | 9 ++- .../i18n/src/locales/sk/accessibility.json | 9 ++- .../i18n/src/locales/tr-TR/accessibility.json | 9 ++- .../i18n/src/locales/ua/accessibility.json | 9 ++- .../i18n/src/locales/vi-VN/accessibility.json | 9 ++- .../i18n/src/locales/zh-CN/accessibility.json | 9 ++- .../i18n/src/locales/zh-TW/accessibility.json | 9 ++- web/app/layout.tsx | 2 +- .../account/auth-forms/auth-banner.tsx | 26 ++++--- .../account/auth-forms/auth-header.tsx | 4 +- .../components/account/auth-forms/email.tsx | 12 ++-- .../auth-forms/forgot-password-popover.tsx | 9 ++- .../account/auth-forms/password.tsx | 70 +++++++++++-------- .../account/auth-forms/unique-code.tsx | 29 +++++--- .../account/terms-and-conditions.tsx | 4 +- 27 files changed, 252 insertions(+), 75 deletions(-) diff --git a/packages/i18n/src/locales/cs/accessibility.json b/packages/i18n/src/locales/cs/accessibility.json index 4a715f75b..676c2d442 100644 --- a/packages/i18n/src/locales/cs/accessibility.json +++ b/packages/i18n/src/locales/cs/accessibility.json @@ -22,6 +22,13 @@ "collapse_sidebar": "Sbalit postranní panel", "expand_sidebar": "Rozbalit postranní panel", "edition_badge": "Otevřít modal placených plánů" + }, + "auth_forms": { + "clear_email": "Vymazat e-mail", + "show_password": "Zobrazit heslo", + "hide_password": "Skrýt heslo", + "close_alert": "Zavřít upozornění", + "close_popover": "Zavřít vyskakovací okno" } } -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/de/accessibility.json b/packages/i18n/src/locales/de/accessibility.json index 0faf00916..edf90970f 100644 --- a/packages/i18n/src/locales/de/accessibility.json +++ b/packages/i18n/src/locales/de/accessibility.json @@ -22,6 +22,13 @@ "collapse_sidebar": "Seitenleiste einklappen", "expand_sidebar": "Seitenleiste ausklappen", "edition_badge": "Modal für kostenpflichtige Pläne öffnen" + }, + "auth_forms": { + "clear_email": "E-Mail löschen", + "show_password": "Passwort anzeigen", + "hide_password": "Passwort verbergen", + "close_alert": "Warnung schließen", + "close_popover": "Popover schließen" } } -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/en/accessibility.json b/packages/i18n/src/locales/en/accessibility.json index 35759d266..86660d640 100644 --- a/packages/i18n/src/locales/en/accessibility.json +++ b/packages/i18n/src/locales/en/accessibility.json @@ -22,6 +22,13 @@ "collapse_sidebar": "Collapse sidebar", "expand_sidebar": "Expand sidebar", "edition_badge": "Open paid plans' modal" + }, + "auth_forms": { + "clear_email": "Clear email", + "show_password": "Show password", + "hide_password": "Hide password", + "close_alert": "Close alert", + "close_popover": "Close popover" } } -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/es/accessibility.json b/packages/i18n/src/locales/es/accessibility.json index 41bf0b777..4d957f5a9 100644 --- a/packages/i18n/src/locales/es/accessibility.json +++ b/packages/i18n/src/locales/es/accessibility.json @@ -22,6 +22,13 @@ "collapse_sidebar": "Colapsar barra lateral", "expand_sidebar": "Expandir barra lateral", "edition_badge": "Abrir modal de planes de pago" + }, + "auth_forms": { + "clear_email": "Limpiar correo electrónico", + "show_password": "Mostrar contraseña", + "hide_password": "Ocultar contraseña", + "close_alert": "Cerrar alerta", + "close_popover": "Cerrar ventana emergente" } } -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/fr/accessibility.json b/packages/i18n/src/locales/fr/accessibility.json index ba42a4f41..435247c58 100644 --- a/packages/i18n/src/locales/fr/accessibility.json +++ b/packages/i18n/src/locales/fr/accessibility.json @@ -22,6 +22,13 @@ "collapse_sidebar": "Réduire la barre latérale", "expand_sidebar": "Étendre la barre latérale", "edition_badge": "Ouvrir le modal des plans payants" + }, + "auth_forms": { + "clear_email": "Effacer l'e-mail", + "show_password": "Afficher le mot de passe", + "hide_password": "Masquer le mot de passe", + "close_alert": "Fermer l'alerte", + "close_popover": "Fermer la fenêtre contextuelle" } } -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/id/accessibility.json b/packages/i18n/src/locales/id/accessibility.json index 2aca032cc..732073401 100644 --- a/packages/i18n/src/locales/id/accessibility.json +++ b/packages/i18n/src/locales/id/accessibility.json @@ -22,6 +22,13 @@ "collapse_sidebar": "Tutup sidebar", "expand_sidebar": "Perluas sidebar", "edition_badge": "Buka modal paket berbayar" + }, + "auth_forms": { + "clear_email": "Hapus email", + "show_password": "Tampilkan kata sandi", + "hide_password": "Sembunyikan kata sandi", + "close_alert": "Tutup peringatan", + "close_popover": "Tutup popover" } } -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/it/accessibility.json b/packages/i18n/src/locales/it/accessibility.json index 8f22d3b8e..16d22bcbc 100644 --- a/packages/i18n/src/locales/it/accessibility.json +++ b/packages/i18n/src/locales/it/accessibility.json @@ -22,6 +22,13 @@ "collapse_sidebar": "Comprimi barra laterale", "expand_sidebar": "Espandi barra laterale", "edition_badge": "Apri modal piani a pagamento" + }, + "auth_forms": { + "clear_email": "Cancella email", + "show_password": "Mostra password", + "hide_password": "Nascondi password", + "close_alert": "Chiudi avviso", + "close_popover": "Chiudi popover" } } -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/ja/accessibility.json b/packages/i18n/src/locales/ja/accessibility.json index a598c435a..b983500ff 100644 --- a/packages/i18n/src/locales/ja/accessibility.json +++ b/packages/i18n/src/locales/ja/accessibility.json @@ -22,6 +22,13 @@ "collapse_sidebar": "サイドバーを折りたたむ", "expand_sidebar": "サイドバーを展開", "edition_badge": "有料プランのモーダルを開く" + }, + "auth_forms": { + "clear_email": "メールをクリア", + "show_password": "パスワードを表示", + "hide_password": "パスワードを非表示", + "close_alert": "アラートを閉じる", + "close_popover": "ポップオーバーを閉じる" } } -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/ko/accessibility.json b/packages/i18n/src/locales/ko/accessibility.json index 491b8c35c..298a7e122 100644 --- a/packages/i18n/src/locales/ko/accessibility.json +++ b/packages/i18n/src/locales/ko/accessibility.json @@ -22,6 +22,13 @@ "collapse_sidebar": "사이드바 축소", "expand_sidebar": "사이드바 확장", "edition_badge": "유료 플랜 모달 열기" + }, + "auth_forms": { + "clear_email": "이메일 지우기", + "show_password": "비밀번호 표시", + "hide_password": "비밀번호 숨기기", + "close_alert": "알림 닫기", + "close_popover": "팝오버 닫기" } } -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/pl/accessibility.json b/packages/i18n/src/locales/pl/accessibility.json index 5ff936d47..c1407911a 100644 --- a/packages/i18n/src/locales/pl/accessibility.json +++ b/packages/i18n/src/locales/pl/accessibility.json @@ -22,6 +22,13 @@ "collapse_sidebar": "Zwiń pasek boczny", "expand_sidebar": "Rozwiń pasek boczny", "edition_badge": "Otwórz modal płatnych planów" + }, + "auth_forms": { + "clear_email": "Wyczyść e-mail", + "show_password": "Pokaż hasło", + "hide_password": "Ukryj hasło", + "close_alert": "Zamknij alert", + "close_popover": "Zamknij popover" } } -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/pt-BR/accessibility.json b/packages/i18n/src/locales/pt-BR/accessibility.json index 333b55a7f..de90eeb36 100644 --- a/packages/i18n/src/locales/pt-BR/accessibility.json +++ b/packages/i18n/src/locales/pt-BR/accessibility.json @@ -22,6 +22,13 @@ "collapse_sidebar": "Recolher barra lateral", "expand_sidebar": "Expandir barra lateral", "edition_badge": "Abrir modal de planos pagos" + }, + "auth_forms": { + "clear_email": "Limpar e-mail", + "show_password": "Mostrar senha", + "hide_password": "Ocultar senha", + "close_alert": "Fechar alerta", + "close_popover": "Fechar popover" } } -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/ro/accessibility.json b/packages/i18n/src/locales/ro/accessibility.json index 1a201a48c..52f555481 100644 --- a/packages/i18n/src/locales/ro/accessibility.json +++ b/packages/i18n/src/locales/ro/accessibility.json @@ -22,6 +22,13 @@ "collapse_sidebar": "Restrânge bara laterală", "expand_sidebar": "Extinde bara laterală", "edition_badge": "Deschide modalul planurilor plătite" + }, + "auth_forms": { + "clear_email": "Șterge e-mailul", + "show_password": "Afișează parola", + "hide_password": "Ascunde parola", + "close_alert": "Închide alerta", + "close_popover": "Închide popover-ul" } } -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/ru/accessibility.json b/packages/i18n/src/locales/ru/accessibility.json index ebec8dc2f..dd4dde76b 100644 --- a/packages/i18n/src/locales/ru/accessibility.json +++ b/packages/i18n/src/locales/ru/accessibility.json @@ -22,6 +22,13 @@ "collapse_sidebar": "Свернуть боковую панель", "expand_sidebar": "Развернуть боковую панель", "edition_badge": "Открыть модал платных планов" + }, + "auth_forms": { + "clear_email": "Очистить email", + "show_password": "Показать пароль", + "hide_password": "Скрыть пароль", + "close_alert": "Закрыть уведомление", + "close_popover": "Закрыть всплывающее окно" } } -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/sk/accessibility.json b/packages/i18n/src/locales/sk/accessibility.json index 59a309f60..26c5c8be6 100644 --- a/packages/i18n/src/locales/sk/accessibility.json +++ b/packages/i18n/src/locales/sk/accessibility.json @@ -22,6 +22,13 @@ "collapse_sidebar": "Zbaliť bočný panel", "expand_sidebar": "Rozbaliť bočný panel", "edition_badge": "Otvoriť modal platených plánov" + }, + "auth_forms": { + "clear_email": "Vymazať e-mail", + "show_password": "Zobraziť heslo", + "hide_password": "Skryť heslo", + "close_alert": "Zavrieť upozornenie", + "close_popover": "Zavrieť vyskakovacie okno" } } -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/tr-TR/accessibility.json b/packages/i18n/src/locales/tr-TR/accessibility.json index 35b8f340e..80a35611c 100644 --- a/packages/i18n/src/locales/tr-TR/accessibility.json +++ b/packages/i18n/src/locales/tr-TR/accessibility.json @@ -22,6 +22,13 @@ "collapse_sidebar": "Kenar çubuğunu daralt", "expand_sidebar": "Kenar çubuğunu genişlet", "edition_badge": "Ücretli planlar modalını aç" + }, + "auth_forms": { + "clear_email": "E-postayı temizle", + "show_password": "Şifreyi göster", + "hide_password": "Şifreyi gizle", + "close_alert": "Uyarıyı kapat", + "close_popover": "Açılır pencereyi kapat" } } -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/ua/accessibility.json b/packages/i18n/src/locales/ua/accessibility.json index b6bdc7d52..427667312 100644 --- a/packages/i18n/src/locales/ua/accessibility.json +++ b/packages/i18n/src/locales/ua/accessibility.json @@ -22,6 +22,13 @@ "collapse_sidebar": "Згорнути бічну панель", "expand_sidebar": "Розгорнути бічну панель", "edition_badge": "Відкрити модал платних планів" + }, + "auth_forms": { + "clear_email": "Очистити email", + "show_password": "Показати пароль", + "hide_password": "Приховати пароль", + "close_alert": "Закрити сповіщення", + "close_popover": "Закрити спливаюче вікно" } } -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/vi-VN/accessibility.json b/packages/i18n/src/locales/vi-VN/accessibility.json index 8071da9e3..b3ab93530 100644 --- a/packages/i18n/src/locales/vi-VN/accessibility.json +++ b/packages/i18n/src/locales/vi-VN/accessibility.json @@ -22,6 +22,13 @@ "collapse_sidebar": "Thu gọn thanh bên", "expand_sidebar": "Mở rộng thanh bên", "edition_badge": "Mở modal gói trả phí" + }, + "auth_forms": { + "clear_email": "Xóa email", + "show_password": "Hiển thị mật khẩu", + "hide_password": "Ẩn mật khẩu", + "close_alert": "Đóng cảnh báo", + "close_popover": "Đóng popover" } } -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/zh-CN/accessibility.json b/packages/i18n/src/locales/zh-CN/accessibility.json index b19f68676..fea84d063 100644 --- a/packages/i18n/src/locales/zh-CN/accessibility.json +++ b/packages/i18n/src/locales/zh-CN/accessibility.json @@ -22,6 +22,13 @@ "collapse_sidebar": "折叠侧边栏", "expand_sidebar": "展开侧边栏", "edition_badge": "打开付费计划模态框" + }, + "auth_forms": { + "clear_email": "清除邮箱", + "show_password": "显示密码", + "hide_password": "隐藏密码", + "close_alert": "关闭警告", + "close_popover": "关闭弹出框" } } -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/zh-TW/accessibility.json b/packages/i18n/src/locales/zh-TW/accessibility.json index 97e07ae73..75747f861 100644 --- a/packages/i18n/src/locales/zh-TW/accessibility.json +++ b/packages/i18n/src/locales/zh-TW/accessibility.json @@ -22,6 +22,13 @@ "collapse_sidebar": "摺疊側邊欄", "expand_sidebar": "展開側邊欄", "edition_badge": "打開付費計劃模態框" + }, + "auth_forms": { + "clear_email": "清除電子郵件", + "show_password": "顯示密碼", + "hide_password": "隱藏密碼", + "close_alert": "關閉警告", + "close_popover": "關閉彈出框" } } -} \ No newline at end of file +} diff --git a/web/app/layout.tsx b/web/app/layout.tsx index a36c75c49..d368a70d7 100644 --- a/web/app/layout.tsx +++ b/web/app/layout.tsx @@ -69,7 +69,7 @@ export default function RootLayout({ children }: { children: React.ReactNode }) "app-container" )} > -
{children}
+
{children}
diff --git a/web/core/components/account/auth-forms/auth-banner.tsx b/web/core/components/account/auth-forms/auth-banner.tsx index 191d7a0a7..da1c57c4a 100644 --- a/web/core/components/account/auth-forms/auth-banner.tsx +++ b/web/core/components/account/auth-forms/auth-banner.tsx @@ -1,5 +1,7 @@ import { FC } from "react"; import { Info, X } from "lucide-react"; +// plane imports +import { useTranslation } from "@plane/i18n"; // helpers import { TAuthErrorInfo } from "@/helpers/authentication.helper"; @@ -10,20 +12,28 @@ type TAuthBanner = { export const AuthBanner: FC = (props) => { const { bannerData, handleBannerData } = props; + // translation + const { t } = useTranslation(); if (!bannerData) return <>; + return ( -
-
+
+
-
{bannerData?.message}
-
handleBannerData && handleBannerData(undefined)} +

{bannerData?.message}

+
+ +
); }; diff --git a/web/core/components/account/auth-forms/auth-header.tsx b/web/core/components/account/auth-forms/auth-header.tsx index b33c694ba..c705c7edd 100644 --- a/web/core/components/account/auth-forms/auth-header.tsx +++ b/web/core/components/account/auth-forms/auth-header.tsx @@ -102,9 +102,9 @@ export const AuthHeader: FC = observer((props) => { return ( <>
-

+

{typeof header === "string" ? t(header) : header} -

+

{t(subHeader)}

{children} diff --git a/web/core/components/account/auth-forms/email.tsx b/web/core/components/account/auth-forms/email.tsx index 724f52442..9f3129364 100644 --- a/web/core/components/account/auth-forms/email.tsx +++ b/web/core/components/account/auth-forms/email.tsx @@ -47,7 +47,7 @@ export const AuthEmailForm: FC = observer((props) => { return (
-
)} diff --git a/web/core/components/account/auth-forms/password.tsx b/web/core/components/account/auth-forms/password.tsx index 979899679..0692eb86d 100644 --- a/web/core/components/account/auth-forms/password.tsx +++ b/web/core/components/account/auth-forms/password.tsx @@ -167,7 +167,7 @@ export const AuthPasswordForm: React.FC = observer((props: Props) => { {nextPath && }
-
-
{mode === EAuthModes.SIGN_UP && (
-
- + ); }); diff --git a/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/layout.tsx b/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/layout.tsx new file mode 100644 index 000000000..fb9677994 --- /dev/null +++ b/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/layout.tsx @@ -0,0 +1,58 @@ +"use client"; + +import { FC, ReactNode } from "react"; +import { observer } from "mobx-react"; +// components +import { usePathname } from "next/navigation"; +import { EUserWorkspaceRoles, WORKSPACE_SETTINGS_ACCESS } from "@plane/constants"; +// hooks +import { NotAuthorizedView } from "@/components/auth-screens"; +import { CommandPalette } from "@/components/command-palette"; +import { SettingsMobileNav } from "@/components/settings"; +import { getWorkspaceActivePath, pathnameToAccessKey } from "@/components/settings/helper"; +import { useUserPermissions } from "@/hooks/store"; +// local components +import { WorkspaceSettingsSidebar } from "./sidebar"; + +export interface IWorkspaceSettingLayout { + children: ReactNode; +} + +const WorkspaceSettingLayout: FC = observer((props) => { + const { children } = props; + // store hooks + const { workspaceUserInfo } = useUserPermissions(); + // next hooks + const pathname = usePathname(); + // derived values + const { workspaceSlug, accessKey } = pathnameToAccessKey(pathname); + const userWorkspaceRole = workspaceUserInfo?.[workspaceSlug.toString()]?.role; + + const isAuthorized: boolean | string = + pathname && + workspaceSlug && + userWorkspaceRole && + WORKSPACE_SETTINGS_ACCESS[accessKey]?.includes(userWorkspaceRole as EUserWorkspaceRoles); + + return ( + <> + + +
+ {workspaceUserInfo && !isAuthorized ? ( + + ) : ( +
+
{}
+ {children} +
+ )} +
+ + ); +}); + +export default WorkspaceSettingLayout; diff --git a/web/app/(all)/[workspaceSlug]/(projects)/settings/(with-sidebar)/members/page.tsx b/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/members/page.tsx similarity index 95% rename from web/app/(all)/[workspaceSlug]/(projects)/settings/(with-sidebar)/members/page.tsx rename to web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/members/page.tsx index 8be7a9d22..250b5bc02 100644 --- a/web/app/(all)/[workspaceSlug]/(projects)/settings/(with-sidebar)/members/page.tsx +++ b/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/members/page.tsx @@ -14,6 +14,7 @@ import { Button, TOAST_TYPE, setToast } from "@plane/ui"; import { NotAuthorizedView } from "@/components/auth-screens"; import { CountChip } from "@/components/common"; import { PageHead } from "@/components/core"; +import { SettingsContentWrapper } from "@/components/settings"; import { WorkspaceMembersList } from "@/components/workspace"; // helpers import { cn } from "@/helpers/common.helper"; @@ -95,11 +96,11 @@ const WorkspaceMembersSettingsPage = observer(() => { // if user is not authorized to view this page if (workspaceUserInfo && !canPerformWorkspaceMemberActions) { - return ; + return ; } return ( - <> + { onSubmit={handleWorkspaceInvite} />
@@ -137,7 +138,7 @@ const WorkspaceMembersSettingsPage = observer(() => {
- + ); }); diff --git a/web/app/(all)/[workspaceSlug]/(projects)/settings/(with-sidebar)/mobile-header-tabs.tsx b/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/mobile-header-tabs.tsx similarity index 100% rename from web/app/(all)/[workspaceSlug]/(projects)/settings/(with-sidebar)/mobile-header-tabs.tsx rename to web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/mobile-header-tabs.tsx diff --git a/web/app/(all)/[workspaceSlug]/(projects)/settings/(with-sidebar)/page.tsx b/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/page.tsx similarity index 85% rename from web/app/(all)/[workspaceSlug]/(projects)/settings/(with-sidebar)/page.tsx rename to web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/page.tsx index 6088cf0a5..736c34810 100644 --- a/web/app/(all)/[workspaceSlug]/(projects)/settings/(with-sidebar)/page.tsx +++ b/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/page.tsx @@ -4,6 +4,7 @@ import { observer } from "mobx-react"; // components import { useTranslation } from "@plane/i18n"; import { PageHead } from "@/components/core"; +import { SettingsContentWrapper } from "@/components/settings"; import { WorkspaceDetails } from "@/components/workspace"; // hooks import { useWorkspace } from "@/hooks/store"; @@ -18,10 +19,10 @@ const WorkspaceSettingsPage = observer(() => { : undefined; return ( - <> + - + ); }); diff --git a/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/sidebar.tsx b/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/sidebar.tsx new file mode 100644 index 000000000..8a97c8b05 --- /dev/null +++ b/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/sidebar.tsx @@ -0,0 +1,73 @@ +import { useParams, usePathname } from "next/navigation"; +import { ArrowUpToLine, Building, CreditCard, Users, Webhook } from "lucide-react"; +import { + EUserPermissionsLevel, + GROUPED_WORKSPACE_SETTINGS, + WORKSPACE_SETTINGS_CATEGORIES, + EUserWorkspaceRoles, + EUserPermissions, + WORKSPACE_SETTINGS_CATEGORY, +} from "@plane/constants"; +import { SettingsSidebar } from "@/components/settings"; +import { useUserPermissions } from "@/hooks/store/user"; +import { shouldRenderSettingLink } from "@/plane-web/helpers/workspace.helper"; + +const ICONS = { + general: Building, + members: Users, + export: ArrowUpToLine, + "billing-and-plans": CreditCard, + webhooks: Webhook, +}; + +export const WorkspaceActionIcons = ({ + type, + size, + className, +}: { + type: string; + size?: number; + className?: string; +}) => { + if (type === undefined) return null; + const Icon = ICONS[type as keyof typeof ICONS]; + if (!Icon) return null; + return ; +}; + +type TWorkspaceSettingsSidebarProps = { + isMobile?: boolean; +}; + +export const WorkspaceSettingsSidebar = (props: TWorkspaceSettingsSidebarProps) => { + const { isMobile = false } = props; + // router + const pathname = usePathname(); + const { workspaceSlug } = useParams(); // store hooks + const { allowPermissions } = useUserPermissions(); + const isAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE); + + return ( + + isAdmin || ![WORKSPACE_SETTINGS_CATEGORY.FEATURES, WORKSPACE_SETTINGS_CATEGORY.DEVELOPER].includes(category) + )} + groupedSettings={GROUPED_WORKSPACE_SETTINGS} + workspaceSlug={workspaceSlug.toString()} + isActive={(data: { href: string }) => + data.href === "/settings" + ? pathname === `/${workspaceSlug}${data.href}/` + : new RegExp(`^/${workspaceSlug}${data.href}/`).test(pathname) + } + shouldRender={(data: { key: string; access?: EUserWorkspaceRoles[] | undefined }) => + data.access + ? shouldRenderSettingLink(workspaceSlug.toString(), data.key) && + allowPermissions(data.access, EUserPermissionsLevel.WORKSPACE, workspaceSlug.toString()) + : false + } + actionIcons={WorkspaceActionIcons} + /> + ); +}; diff --git a/web/app/(all)/[workspaceSlug]/(projects)/settings/(with-sidebar)/webhooks/[webhookId]/page.tsx b/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/webhooks/[webhookId]/page.tsx similarity index 96% rename from web/app/(all)/[workspaceSlug]/(projects)/settings/(with-sidebar)/webhooks/[webhookId]/page.tsx rename to web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/webhooks/[webhookId]/page.tsx index 5edc914e9..a775ff3b1 100644 --- a/web/app/(all)/[workspaceSlug]/(projects)/settings/(with-sidebar)/webhooks/[webhookId]/page.tsx +++ b/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/webhooks/[webhookId]/page.tsx @@ -11,6 +11,7 @@ import { TOAST_TYPE, setToast } from "@plane/ui"; // components import { LogoSpinner } from "@/components/common"; import { PageHead } from "@/components/core"; +import { SettingsContentWrapper } from "@/components/settings"; import { DeleteWebhookModal, WebhookDeleteSection, WebhookForm } from "@/components/web-hooks"; // hooks import { useUserPermissions, useWebhook, useWorkspace } from "@/hooks/store"; @@ -87,7 +88,7 @@ const WebhookDetailsPage = observer(() => { ); return ( - <> + setDeleteWebhookModal(false)} />
@@ -96,7 +97,7 @@ const WebhookDetailsPage = observer(() => {
{currentWebhook && setDeleteWebhookModal(true)} />}
- + ); }); diff --git a/web/app/(all)/[workspaceSlug]/(projects)/settings/(with-sidebar)/webhooks/page.tsx b/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/webhooks/page.tsx similarity index 75% rename from web/app/(all)/[workspaceSlug]/(projects)/settings/(with-sidebar)/webhooks/page.tsx rename to web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/webhooks/page.tsx index 2623660da..d1692168e 100644 --- a/web/app/(all)/[workspaceSlug]/(projects)/settings/(with-sidebar)/webhooks/page.tsx +++ b/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/webhooks/page.tsx @@ -7,11 +7,11 @@ import useSWR from "swr"; // plane imports import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; -import { Button } from "@plane/ui"; // components import { NotAuthorizedView } from "@/components/auth-screens"; import { PageHead } from "@/components/core"; import { DetailedEmptyState } from "@/components/empty-state"; +import { SettingsContentWrapper, SettingsHeading } from "@/components/settings"; import { WebhookSettingsLoader } from "@/components/ui"; import { WebhooksList, CreateWebhookModal } from "@/components/web-hooks"; // hooks @@ -48,15 +48,15 @@ const WebhooksListPage = observer(() => { }, [showCreateWebhookModal, webhookSecretKey, clearSecretKey]); if (workspaceUserInfo && !canPerformWorkspaceAdminActions) { - return ; + return ; } if (!webhooks) return ; return ( - <> + -
+
{ setShowCreateWebhookModal(false); }} /> + setShowCreateWebhookModal(true), + }} + /> {Object.keys(webhooks).length > 0 ? (
-
-
{t("workspace_settings.settings.webhooks.title")}
- -
) : (
-
-
{t("workspace_settings.settings.webhooks.title")}
- -
setShowCreateWebhookModal(true), + }} />
)}
- + ); }); diff --git a/web/app/(all)/[workspaceSlug]/(settings)/settings/account/activity/page.tsx b/web/app/(all)/[workspaceSlug]/(settings)/settings/account/activity/page.tsx new file mode 100644 index 000000000..05777e648 --- /dev/null +++ b/web/app/(all)/[workspaceSlug]/(settings)/settings/account/activity/page.tsx @@ -0,0 +1,89 @@ +"use client"; + +import { useState } from "react"; +import { observer } from "mobx-react"; +import { useTranslation } from "@plane/i18n"; +// ui +import { Button } from "@plane/ui"; +// components +import { PageHead } from "@/components/core"; +import { DetailedEmptyState } from "@/components/empty-state"; +import { ProfileActivityListPage } from "@/components/profile"; +// hooks +import { SettingsHeading } from "@/components/settings"; +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; + +const PER_PAGE = 100; + +const ProfileActivityPage = observer(() => { + // states + const [pageCount, setPageCount] = useState(1); + const [totalPages, setTotalPages] = useState(0); + const [resultsCount, setResultsCount] = useState(0); + const [isEmpty, setIsEmpty] = useState(false); + // plane hooks + const { t } = useTranslation(); + // derived values + const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/profile/activity" }); + + const updateTotalPages = (count: number) => setTotalPages(count); + + const updateResultsCount = (count: number) => setResultsCount(count); + + const updateEmptyState = (isEmpty: boolean) => setIsEmpty(isEmpty); + + const handleLoadMore = () => setPageCount((prev) => prev + 1); + + const activityPages: JSX.Element[] = []; + for (let i = 0; i < pageCount; i++) + activityPages.push( + + ); + + const isLoadMoreVisible = pageCount < totalPages && resultsCount !== 0; + + if (isEmpty) { + return ( +
+ + +
+ ); + } + + return ( + <> + + +
{activityPages}
+ {isLoadMoreVisible && ( +
+ +
+ )} + + ); +}); + +export default ProfileActivityPage; diff --git a/web/app/(all)/[workspaceSlug]/(projects)/settings/(with-sidebar)/api-tokens/page.tsx b/web/app/(all)/[workspaceSlug]/(settings)/settings/account/api-tokens/page.tsx similarity index 71% rename from web/app/(all)/[workspaceSlug]/(projects)/settings/(with-sidebar)/api-tokens/page.tsx rename to web/app/(all)/[workspaceSlug]/(settings)/settings/account/api-tokens/page.tsx index 21334ff23..10461db07 100644 --- a/web/app/(all)/[workspaceSlug]/(projects)/settings/(with-sidebar)/api-tokens/page.tsx +++ b/web/app/(all)/[workspaceSlug]/(settings)/settings/account/api-tokens/page.tsx @@ -7,12 +7,12 @@ import useSWR from "swr"; // plane imports import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; -import { Button } from "@plane/ui"; // component import { ApiTokenListItem, CreateApiTokenModal } from "@/components/api-token"; import { NotAuthorizedView } from "@/components/auth-screens"; import { PageHead } from "@/components/core"; import { DetailedEmptyState } from "@/components/empty-state"; +import { SettingsHeading } from "@/components/settings"; import { APITokenSettingsLoader } from "@/components/ui"; import { API_TOKENS_LIST } from "@/constants/fetch-keys"; // store hooks @@ -48,7 +48,7 @@ const ApiTokensPage = observer(() => { : undefined; if (workspaceUserInfo && !canPerformWorkspaceAdminActions) { - return ; + return ; } if (!tokens) { @@ -56,18 +56,20 @@ const ApiTokensPage = observer(() => { } return ( - <> +
setIsCreateTokenModalOpen(false)} /> -
+
{tokens.length > 0 ? ( <> -
-

{t("workspace_settings.settings.api_tokens.title")}

- -
+ setIsCreateTokenModalOpen(true), + }} + />
{tokens.map((token) => ( @@ -76,23 +78,31 @@ const ApiTokensPage = observer(() => { ) : (
-
-

{t("workspace_settings.settings.api_tokens.title")}

- -
+ setIsCreateTokenModalOpen(true), + }} + />
setIsCreateTokenModalOpen(true), + }} />
)}
- +
); }); diff --git a/web/app/(all)/[workspaceSlug]/(settings)/settings/account/layout.tsx b/web/app/(all)/[workspaceSlug]/(settings)/settings/account/layout.tsx new file mode 100644 index 000000000..9dcffd57c --- /dev/null +++ b/web/app/(all)/[workspaceSlug]/(settings)/settings/account/layout.tsx @@ -0,0 +1,33 @@ +"use client"; + +import { ReactNode } from "react"; +// components +import { observer } from "mobx-react"; +import { usePathname } from "next/navigation"; +import { SettingsContentWrapper, SettingsMobileNav } from "@/components/settings"; +import { getProfileActivePath } from "@/components/settings/helper"; +import { ProfileSidebar } from "./sidebar"; + +type Props = { + children: ReactNode; +}; + +const ProfileSettingsLayout = observer((props: Props) => { + const { children } = props; + // router + const pathname = usePathname(); + + return ( + <> + +
+
+ +
+ {children} +
+ + ); +}); + +export default ProfileSettingsLayout; diff --git a/web/app/(all)/[workspaceSlug]/(settings)/settings/account/notifications/page.tsx b/web/app/(all)/[workspaceSlug]/(settings)/settings/account/notifications/page.tsx new file mode 100644 index 000000000..cc71877af --- /dev/null +++ b/web/app/(all)/[workspaceSlug]/(settings)/settings/account/notifications/page.tsx @@ -0,0 +1,37 @@ +"use client"; + +import useSWR from "swr"; +// components +import { useTranslation } from "@plane/i18n"; +import { PageHead } from "@/components/core"; +import { EmailNotificationForm } from "@/components/profile/notification"; +import { SettingsHeading } from "@/components/settings"; +import { EmailSettingsLoader } from "@/components/ui"; +// services +import { UserService } from "@/services/user.service"; + +const userService = new UserService(); + +export default function ProfileNotificationPage() { + const { t } = useTranslation(); + // fetching user email notification settings + const { data, isLoading } = useSWR("CURRENT_USER_EMAIL_NOTIFICATION_SETTINGS", () => + userService.currentUserEmailNotificationSettings() + ); + + if (!data || isLoading) { + return ; + } + + return ( + <> + + + + + + ); +} diff --git a/web/app/(all)/[workspaceSlug]/(settings)/settings/account/page.tsx b/web/app/(all)/[workspaceSlug]/(settings)/settings/account/page.tsx new file mode 100644 index 000000000..f37178c2a --- /dev/null +++ b/web/app/(all)/[workspaceSlug]/(settings)/settings/account/page.tsx @@ -0,0 +1,32 @@ +"use client"; + +import { observer } from "mobx-react"; +import { useTranslation } from "@plane/i18n"; +// components +import { LogoSpinner } from "@/components/common"; +import { PageHead } from "@/components/core"; +import { ProfileForm } from "@/components/profile"; +// hooks +import { useUser } from "@/hooks/store"; + +const ProfileSettingsPage = observer(() => { + const { t } = useTranslation(); + // store hooks + const { data: currentUser, userProfile } = useUser(); + + if (!currentUser) + return ( +
+ +
+ ); + + return ( + <> + + + + ); +}); + +export default ProfileSettingsPage; diff --git a/web/app/(all)/[workspaceSlug]/(settings)/settings/account/preferences/page.tsx b/web/app/(all)/[workspaceSlug]/(settings)/settings/account/preferences/page.tsx new file mode 100644 index 000000000..81c37ae4c --- /dev/null +++ b/web/app/(all)/[workspaceSlug]/(settings)/settings/account/preferences/page.tsx @@ -0,0 +1,46 @@ +"use client"; + +import { observer } from "mobx-react"; +import { useTranslation } from "@plane/i18n"; +// components +import { LogoSpinner } from "@/components/common"; +import { PageHead } from "@/components/core"; +import { PreferencesList } from "@/components/preferences/list"; +import { LanguageTimezone, ProfileSettingContentHeader } from "@/components/profile"; +// hooks +import { SettingsHeading } from "@/components/settings"; +import { useUserProfile } from "@/hooks/store"; +const ProfileAppearancePage = observer(() => { + const { t } = useTranslation(); + // hooks + const { data: userProfile } = useUserProfile(); + + return ( + <> + + {userProfile ? ( + <> +
+
+ + +
+
+ + +
+
+ + ) : ( +
+ +
+ )} + + ); +}); + +export default ProfileAppearancePage; diff --git a/web/app/(all)/[workspaceSlug]/(settings)/settings/account/security/page.tsx b/web/app/(all)/[workspaceSlug]/(settings)/settings/account/security/page.tsx new file mode 100644 index 000000000..b9cdf9d26 --- /dev/null +++ b/web/app/(all)/[workspaceSlug]/(settings)/settings/account/security/page.tsx @@ -0,0 +1,251 @@ +"use client"; + +import { useState } from "react"; +import { observer } from "mobx-react"; +import { Controller, useForm } from "react-hook-form"; +import { Eye, EyeOff } from "lucide-react"; +import { useTranslation } from "@plane/i18n"; +// ui +import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; +// components +import { PasswordStrengthMeter } from "@/components/account"; +import { PageHead } from "@/components/core"; +import { ProfileSettingContentHeader } from "@/components/profile"; +// helpers +import { authErrorHandler } from "@/helpers/authentication.helper"; +import { E_PASSWORD_STRENGTH, getPasswordStrength } from "@/helpers/password.helper"; +// hooks +import { useUser } from "@/hooks/store"; +// services +import { AuthService } from "@/services/auth.service"; + +export interface FormValues { + old_password: string; + new_password: string; + confirm_password: string; +} + +const defaultValues: FormValues = { + old_password: "", + new_password: "", + confirm_password: "", +}; + +const authService = new AuthService(); + +const defaultShowPassword = { + oldPassword: false, + password: false, + confirmPassword: false, +}; + +const SecurityPage = observer(() => { + // store + const { data: currentUser, changePassword } = useUser(); + // states + const [showPassword, setShowPassword] = useState(defaultShowPassword); + const [isPasswordInputFocused, setIsPasswordInputFocused] = useState(false); + const [isRetryPasswordInputFocused, setIsRetryPasswordInputFocused] = useState(false); + + // use form + const { + control, + handleSubmit, + watch, + formState: { errors, isSubmitting }, + reset, + } = useForm({ defaultValues }); + // derived values + const oldPassword = watch("old_password"); + const password = watch("new_password"); + const confirmPassword = watch("confirm_password"); + const oldPasswordRequired = !currentUser?.is_password_autoset; + // i18n + const { t } = useTranslation(); + + const isNewPasswordSameAsOldPassword = oldPassword !== "" && password !== "" && password === oldPassword; + + const handleShowPassword = (key: keyof typeof showPassword) => + setShowPassword((prev) => ({ ...prev, [key]: !prev[key] })); + + const handleChangePassword = async (formData: FormValues) => { + const { old_password, new_password } = formData; + try { + const csrfToken = await authService.requestCSRFToken().then((data) => data?.csrf_token); + if (!csrfToken) throw new Error("csrf token not found"); + + await changePassword(csrfToken, { + ...(oldPasswordRequired && { old_password }), + new_password, + }); + + reset(defaultValues); + setShowPassword(defaultShowPassword); + setToast({ + type: TOAST_TYPE.SUCCESS, + title: t("auth.common.password.toast.change_password.success.title"), + message: t("auth.common.password.toast.change_password.success.message"), + }); + } catch (err: any) { + const errorInfo = authErrorHandler(err.error_code?.toString()); + setToast({ + type: TOAST_TYPE.ERROR, + title: errorInfo?.title ?? t("auth.common.password.toast.error.title"), + message: + typeof errorInfo?.message === "string" ? errorInfo.message : t("auth.common.password.toast.error.message"), + }); + } + }; + + const isButtonDisabled = + getPasswordStrength(password) != E_PASSWORD_STRENGTH.STRENGTH_VALID || + (oldPasswordRequired && oldPassword.trim() === "") || + password.trim() === "" || + confirmPassword.trim() === "" || + password !== confirmPassword || + password === oldPassword; + + const passwordSupport = password.length > 0 && + getPasswordStrength(password) != E_PASSWORD_STRENGTH.STRENGTH_VALID && ( + + ); + + const renderPasswordMatchError = !isRetryPasswordInputFocused || confirmPassword.length >= password.length; + + return ( + <> + + +
+
+ {oldPasswordRequired && ( +
+

{t("auth.common.password.current_password.label")}

+
+ ( + + )} + /> + {showPassword?.oldPassword ? ( + handleShowPassword("oldPassword")} + /> + ) : ( + handleShowPassword("oldPassword")} + /> + )} +
+ {errors.old_password && {errors.old_password.message}} +
+ )} +
+

{t("auth.common.password.new_password.label")}

+
+ ( + setIsPasswordInputFocused(true)} + onBlur={() => setIsPasswordInputFocused(false)} + /> + )} + /> + {showPassword?.password ? ( + handleShowPassword("password")} + /> + ) : ( + handleShowPassword("password")} + /> + )} +
+ {passwordSupport} + {isNewPasswordSameAsOldPassword && !isPasswordInputFocused && ( + {t("new_password_must_be_different_from_old_password")} + )} +
+
+

{t("auth.common.password.confirm_password.label")}

+
+ ( + setIsRetryPasswordInputFocused(true)} + onBlur={() => setIsRetryPasswordInputFocused(false)} + /> + )} + /> + {showPassword?.confirmPassword ? ( + handleShowPassword("confirmPassword")} + /> + ) : ( + handleShowPassword("confirmPassword")} + /> + )} +
+ {!!confirmPassword && password !== confirmPassword && renderPasswordMatchError && ( + {t("auth.common.password.errors.match")} + )} +
+
+ +
+ +
+ + + ); +}); + +export default SecurityPage; diff --git a/web/app/(all)/[workspaceSlug]/(settings)/settings/account/sidebar.tsx b/web/app/(all)/[workspaceSlug]/(settings)/settings/account/sidebar.tsx new file mode 100644 index 000000000..6e495daff --- /dev/null +++ b/web/app/(all)/[workspaceSlug]/(settings)/settings/account/sidebar.tsx @@ -0,0 +1,82 @@ +import { observer } from "mobx-react"; +import { useParams, usePathname } from "next/navigation"; +import { CircleUser, Activity, Bell, CircleUserRound, KeyRound, Settings2, Blocks, Lock } from "lucide-react"; +import { + EUserPermissions, + EUserPermissionsLevel, + GROUPED_PROFILE_SETTINGS, + PROFILE_SETTINGS_CATEGORIES, + PROFILE_SETTINGS_CATEGORY, +} from "@plane/constants"; +import { SettingsSidebar } from "@/components/settings"; +import { getFileURL } from "@/helpers/file.helper"; +import { useUser, useUserPermissions } from "@/hooks/store/user"; + +const ICONS = { + profile: CircleUser, + security: Lock, + activity: Activity, + preferences: Settings2, + notifications: Bell, + "api-tokens": KeyRound, + connections: Blocks, +}; + +export const ProjectActionIcons = ({ type, size, className }: { type: string; size?: number; className?: string }) => { + if (type === undefined) return null; + const Icon = ICONS[type as keyof typeof ICONS]; + if (!Icon) return null; + return ; +}; + +type TProfileSidebarProps = { + isMobile?: boolean; +}; + +export const ProfileSidebar = observer((props: TProfileSidebarProps) => { + const { isMobile = false } = props; + // router + const pathname = usePathname(); + const { workspaceSlug } = useParams(); + // store hooks + const { data: currentUser } = useUser(); + + const { allowPermissions } = useUserPermissions(); + const isAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE); + return ( + isAdmin || category !== PROFILE_SETTINGS_CATEGORY.DEVELOPER + )} + groupedSettings={GROUPED_PROFILE_SETTINGS} + workspaceSlug={workspaceSlug?.toString() ?? ""} + isActive={(data: { href: string }) => pathname === `/${workspaceSlug}${data.href}/`} + customHeader={ +
+
+ {!currentUser?.avatar_url || currentUser?.avatar_url === "" ? ( +
+ +
+ ) : ( +
+ {currentUser?.display_name} +
+ )} +
+
+
{currentUser?.display_name}
+
{currentUser?.email}
+
+
+ } + actionIcons={ProjectActionIcons} + shouldRender + /> + ); +}); diff --git a/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/automations/page.tsx b/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/automations/page.tsx similarity index 79% rename from web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/automations/page.tsx rename to web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/automations/page.tsx index 5fc536d91..c7542b4f0 100644 --- a/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/automations/page.tsx +++ b/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/automations/page.tsx @@ -13,6 +13,7 @@ import { NotAuthorizedView } from "@/components/auth-screens"; import { AutoArchiveAutomation, AutoCloseAutomation } from "@/components/automation"; import { PageHead } from "@/components/core"; // hooks +import { SettingsContentWrapper, SettingsHeading } from "@/components/settings"; import { useProject, useUserPermissions } from "@/hooks/store"; const AutomationSettingsPage = observer(() => { @@ -43,20 +44,21 @@ const AutomationSettingsPage = observer(() => { const pageTitle = projectDetails?.name ? `${projectDetails?.name} - Automations` : undefined; if (workspaceUserInfo && !canPerformProjectAdminActions) { - return ; + return ; } return ( - <> + -
-
-

{t("project_settings.automations.label")}

-
+
+
- + ); }); diff --git a/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/estimates/page.tsx b/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/estimates/page.tsx similarity index 80% rename from web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/estimates/page.tsx rename to web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/estimates/page.tsx index 0a19713e8..db9d17e89 100644 --- a/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/estimates/page.tsx +++ b/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/estimates/page.tsx @@ -8,6 +8,7 @@ import { NotAuthorizedView } from "@/components/auth-screens"; import { PageHead } from "@/components/core"; import { EstimateRoot } from "@/components/estimates"; // hooks +import { SettingsContentWrapper } from "@/components/settings"; import { useProject, useUserPermissions } from "@/hooks/store"; const EstimatesSettingsPage = observer(() => { @@ -23,22 +24,20 @@ const EstimatesSettingsPage = observer(() => { if (!workspaceSlug || !projectId) return <>; if (workspaceUserInfo && !canPerformProjectAdminActions) { - return ; + return ; } return ( - <> + -
+
- + ); }); diff --git a/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/features/page.tsx b/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/page.tsx similarity index 81% rename from web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/features/page.tsx rename to web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/page.tsx index 23aa8ad45..d84ba10c4 100644 --- a/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/features/page.tsx +++ b/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/page.tsx @@ -8,6 +8,7 @@ import { NotAuthorizedView } from "@/components/auth-screens"; import { PageHead } from "@/components/core"; import { ProjectFeaturesList } from "@/components/project"; // hooks +import { SettingsContentWrapper } from "@/components/settings"; import { useProject, useUserPermissions } from "@/hooks/store"; const FeaturesSettingsPage = observer(() => { @@ -23,20 +24,20 @@ const FeaturesSettingsPage = observer(() => { if (!workspaceSlug || !projectId) return null; if (workspaceUserInfo && !canPerformProjectAdminActions) { - return ; + return ; } return ( - <> + -
+
- + ); }); diff --git a/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/labels/page.tsx b/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/labels/page.tsx similarity index 87% rename from web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/labels/page.tsx rename to web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/labels/page.tsx index 17a466a80..317e76929 100644 --- a/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/labels/page.tsx +++ b/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/labels/page.tsx @@ -10,6 +10,7 @@ import { NotAuthorizedView } from "@/components/auth-screens"; import { PageHead } from "@/components/core"; import { ProjectSettingsLabelList } from "@/components/labels"; // hooks +import { SettingsContentWrapper } from "@/components/settings"; import { useProject, useUserPermissions } from "@/hooks/store"; const LabelsSettingsPage = observer(() => { @@ -38,19 +39,19 @@ const LabelsSettingsPage = observer(() => { element, }) ); - }, [scrollableContainerRef?.current]); + }, []); if (workspaceUserInfo && !canPerformProjectMemberActions) { - return ; + return ; } return ( - <> + -
+
- + ); }); diff --git a/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/members/page.tsx b/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/members/page.tsx similarity index 83% rename from web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/members/page.tsx rename to web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/members/page.tsx index 9deaef126..06990217f 100644 --- a/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/members/page.tsx +++ b/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/members/page.tsx @@ -7,6 +7,7 @@ import { NotAuthorizedView } from "@/components/auth-screens"; import { PageHead } from "@/components/core"; import { ProjectMemberList, ProjectSettingsMemberDefaults } from "@/components/project"; // hooks +import { SettingsContentWrapper } from "@/components/settings"; import { useProject, useUserPermissions } from "@/hooks/store"; const MembersSettingsPage = observer(() => { @@ -23,17 +24,17 @@ const MembersSettingsPage = observer(() => { const canPerformProjectMemberActions = isProjectMemberOrAdmin || isWorkspaceAdmin; if (workspaceUserInfo && !canPerformProjectMemberActions) { - return ; + return ; } return ( - <> + -
+
- + ); }); diff --git a/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/page.tsx b/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/page.tsx similarity index 91% rename from web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/page.tsx rename to web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/page.tsx index 96ff1bcc3..cf79fa127 100644 --- a/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/page.tsx +++ b/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/page.tsx @@ -16,9 +16,9 @@ import { ProjectDetailsFormLoader, } from "@/components/project"; // hooks +import { SettingsContentWrapper } from "@/components/settings"; import { useProject, useUserPermissions } from "@/hooks/store"; - -const GeneralSettingsPage = observer(() => { +const ProjectSettingsPage = observer(() => { // states const [selectProject, setSelectedProject] = useState(null); const [archiveProject, setArchiveProject] = useState(false); @@ -45,7 +45,7 @@ const GeneralSettingsPage = observer(() => { const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - General Settings` : undefined; return ( - <> + {currentProjectDetails && workspaceSlug && projectId && ( <> @@ -64,7 +64,7 @@ const GeneralSettingsPage = observer(() => { )} -
+
{currentProjectDetails && workspaceSlug && projectId && !isLoading ? ( { )}
- + ); }); -export default GeneralSettingsPage; +export default ProjectSettingsPage; diff --git a/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/states/page.tsx b/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/states/page.tsx similarity index 68% rename from web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/states/page.tsx rename to web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/states/page.tsx index 54fca1c08..30f6c3da6 100644 --- a/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/(with-sidebar)/states/page.tsx +++ b/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/states/page.tsx @@ -9,6 +9,7 @@ import { NotAuthorizedView } from "@/components/auth-screens"; import { PageHead } from "@/components/core"; import { ProjectStateRoot } from "@/components/project-states"; // hook +import { SettingsContentWrapper, SettingsHeading } from "@/components/settings"; import { useProject, useUserPermissions } from "@/hooks/store"; const StatesSettingsPage = observer(() => { @@ -28,19 +29,22 @@ const StatesSettingsPage = observer(() => { ); if (workspaceUserInfo && !canPerformProjectMemberActions) { - return ; + return ; } return ( - <> + -
-

{t("common.states")}

+
+ + {workspaceSlug && projectId && ( + + )}
- {workspaceSlug && projectId && ( - - )} - + ); }); diff --git a/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/layout.tsx b/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/layout.tsx new file mode 100644 index 000000000..4701775b4 --- /dev/null +++ b/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/layout.tsx @@ -0,0 +1,46 @@ +"use client"; + +import { ReactNode, useEffect } from "react"; +import { observer } from "mobx-react"; +import { useParams, usePathname } from "next/navigation"; +// components +import { SettingsMobileNav } from "@/components/settings"; +import { getProjectActivePath } from "@/components/settings/helper"; +import { ProjectSettingsSidebar } from "@/components/settings/project/sidebar"; +import { useProject } from "@/hooks/store"; +import { useAppRouter } from "@/hooks/use-app-router"; +import { ProjectAuthWrapper } from "@/plane-web/layouts/project-wrapper"; + +type Props = { + children: ReactNode; +}; + +const ProjectSettingsLayout = observer((props: Props) => { + const { children } = props; + // router + const router = useAppRouter(); + const pathname = usePathname(); + const { workspaceSlug, projectId } = useParams(); + const { joinedProjectIds } = useProject(); + + useEffect(() => { + if (projectId) return; + if (joinedProjectIds.length > 0) { + router.push(`/${workspaceSlug}/settings/projects/${joinedProjectIds[0]}`); + } + }, [joinedProjectIds, router, workspaceSlug, projectId]); + + return ( + <> + + +
+
{projectId && }
+ {children} +
+
+ + ); +}); + +export default ProjectSettingsLayout; diff --git a/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/page.tsx b/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/page.tsx new file mode 100644 index 000000000..65ea62701 --- /dev/null +++ b/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/page.tsx @@ -0,0 +1,38 @@ +"use client"; +import Image from "next/image"; +import Link from "next/link"; +import { useTheme } from "next-themes"; +import { Button, getButtonStyling } from "@plane/ui"; +import { cn } from "@plane/utils"; +import { useCommandPalette } from "@/hooks/store"; + +const ProjectSettingsPage = () => { + // store hooks + const { resolvedTheme } = useTheme(); + const { toggleCreateProjectModal } = useCommandPalette(); + // derived values + const resolvedPath = + resolvedTheme === "dark" + ? "/empty-state/project-settings/no-projects-dark.png" + : "/empty-state/project-settings/no-projects-light.png"; + return ( +
+ +
No projects yet
+
+ Projects act as the foundation for goal-driven work. They let you manage your teams, tasks, and everything you + need to get things done. +
+
+ + Learn more about projects + + +
+
+ ); +}; + +export default ProjectSettingsPage; diff --git a/web/app/(all)/profile/appearance/page.tsx b/web/app/(all)/profile/appearance/page.tsx index db367e49a..ac5beec37 100644 --- a/web/app/(all)/profile/appearance/page.tsx +++ b/web/app/(all)/profile/appearance/page.tsx @@ -11,7 +11,7 @@ import { setPromiseToast } from "@plane/ui"; // components import { LogoSpinner } from "@/components/common"; import { ThemeSwitch, PageHead, CustomThemeSelector } from "@/components/core"; -import { ProfileSettingContentHeader, ProfileSettingContentWrapper, StartOfWeekPreference } from "@/components/profile"; +import { ProfileSettingContentHeader, ProfileSettingContentWrapper } from "@/components/profile"; // helpers import { applyTheme, unsetCustomCssVariables } from "@/helpers/theme.helper"; // hooks @@ -75,7 +75,6 @@ const ProfileAppearancePage = observer(() => {
{userProfile?.theme?.theme === "custom" && } - ) : (
diff --git a/web/ce/components/preferences/config.ts b/web/ce/components/preferences/config.ts new file mode 100644 index 000000000..1a67ab7d3 --- /dev/null +++ b/web/ce/components/preferences/config.ts @@ -0,0 +1,7 @@ +import { StartOfWeekPreference } from "@/components/profile/start-of-week-preference"; +import { ThemeSwitcher } from "./theme-switcher"; + +export const PREFERENCE_COMPONENTS = { + theme: ThemeSwitcher, + start_of_week: StartOfWeekPreference, +}; diff --git a/web/ce/components/preferences/theme-switcher.tsx b/web/ce/components/preferences/theme-switcher.tsx new file mode 100644 index 000000000..6fd397231 --- /dev/null +++ b/web/ce/components/preferences/theme-switcher.tsx @@ -0,0 +1,105 @@ +"use client"; + +import { useEffect, useState, useCallback } from "react"; +import { observer } from "mobx-react"; +import { useTheme } from "next-themes"; +import { I_THEME_OPTION, THEME_OPTIONS } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; +import { IUserTheme } from "@plane/types"; +import { setPromiseToast } from "@plane/ui"; + +// components +import { CustomThemeSelector, ThemeSwitch } from "@/components/core"; +// helpers +import { PreferencesSection } from "@/components/preferences/section"; +import { applyTheme, unsetCustomCssVariables } from "@/helpers/theme.helper"; +// hooks +import { useUserProfile } from "@/hooks/store"; + +export const ThemeSwitcher = observer( + (props: { + option: { + id: string; + title: string; + description: string; + }; + }) => { + // hooks + const { setTheme } = useTheme(); + const { data: userProfile, updateUserTheme } = useUserProfile(); + + // states + const [currentTheme, setCurrentTheme] = useState(null); + + const { t } = useTranslation(); + + // initialize theme + useEffect(() => { + if (!userProfile?.theme?.theme) return; + + const userThemeOption = THEME_OPTIONS.find((t) => t.value === userProfile.theme.theme); + + if (userThemeOption) { + setCurrentTheme(userThemeOption); + } + }, [userProfile?.theme?.theme]); + + // handlers + const applyThemeChange = useCallback( + (theme: Partial) => { + const themeValue = theme?.theme || "system"; + setTheme(themeValue); + + if (theme?.theme === "custom" && theme?.palette) { + const defaultPalette = "#0d101b,#c5c5c5,#3f76ff,#0d101b,#c5c5c5"; + const palette = theme.palette !== ",,,," ? theme.palette : defaultPalette; + applyTheme(palette, false); + } else { + unsetCustomCssVariables(); + } + }, + [setTheme] + ); + + const handleThemeChange = useCallback( + async (themeOption: I_THEME_OPTION) => { + try { + applyThemeChange({ theme: themeOption.value }); + + const updatePromise = updateUserTheme({ theme: themeOption.value }); + setPromiseToast(updatePromise, { + loading: "Updating theme...", + success: { + title: "Success!", + message: () => "Theme updated successfully!", + }, + error: { + title: "Error!", + message: () => "Failed to update the theme", + }, + }); + } catch (error) { + console.error("Error updating theme:", error); + } + }, + [applyThemeChange, updateUserTheme] + ); + + if (!userProfile) return null; + + return ( + <> + + +
+ } + /> + {userProfile.theme?.theme === "custom" && } + + ); + } +); diff --git a/web/ce/components/workspace/billing/root.tsx b/web/ce/components/workspace/billing/root.tsx index f76052584..6dd452134 100644 --- a/web/ce/components/workspace/billing/root.tsx +++ b/web/ce/components/workspace/billing/root.tsx @@ -6,9 +6,11 @@ import { EProductSubscriptionEnum, SUBSCRIPTION_WITH_BILLING_FREQUENCY, } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; import { TBillingFrequency, TProductBillingFrequency } from "@plane/types"; import { cn } from "@plane/utils"; // components +import { SettingsHeading } from "@/components/settings"; import { getSubscriptionTextColor } from "@/components/workspace/billing/subscription"; // local imports import { PlansComparison } from "./comparison/root"; @@ -20,6 +22,7 @@ export const BillingRoot = observer(() => { const [productBillingFrequency, setProductBillingFrequency] = useState( DEFAULT_PRODUCT_BILLING_FREQUENCY ); + const { t } = useTranslation(); /** * Retrieves the billing frequency for a given subscription type @@ -56,11 +59,10 @@ export const BillingRoot = observer(() => { return (
-
-
-

Billing and plans

-
-
+
pathname === `${baseUrl}/settings/`, + highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/`, Icon: SettingIcon, }, members: { key: "members", i18n_label: "members", - href: `/settings/members`, + href: `/members`, access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST], - highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/members/`, + highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/members/`, Icon: SettingIcon, }, features: { key: "features", i18n_label: "common.features", - href: `/settings/features`, + href: `/features`, access: [EUserPermissions.ADMIN], - highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/features/`, + highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/features/`, Icon: SettingIcon, }, states: { key: "states", i18n_label: "common.states", - href: `/settings/states`, + href: `/states`, access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/states/`, + highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/states/`, Icon: SettingIcon, }, labels: { key: "labels", i18n_label: "common.labels", - href: `/settings/labels`, + href: `/labels`, access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/labels/`, + highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/labels/`, Icon: SettingIcon, }, estimates: { key: "estimates", i18n_label: "common.estimates", - href: `/settings/estimates`, + href: `/estimates`, access: [EUserPermissions.ADMIN], - highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/estimates/`, + highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/estimates/`, Icon: SettingIcon, }, automations: { key: "automations", i18n_label: "project_settings.automations.label", - href: `/settings/automations`, + href: `/automations`, access: [EUserPermissions.ADMIN], - highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/automations/`, + highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/automations/`, Icon: SettingIcon, }, }; diff --git a/web/core/components/auth-screens/not-authorized-view.tsx b/web/core/components/auth-screens/not-authorized-view.tsx index fe344f468..58265a41f 100644 --- a/web/core/components/auth-screens/not-authorized-view.tsx +++ b/web/core/components/auth-screens/not-authorized-view.tsx @@ -12,17 +12,18 @@ type Props = { actionButton?: React.ReactNode; section?: "settings" | "general"; isProjectView?: boolean; + className?: string; }; export const NotAuthorizedView: React.FC = observer((props) => { - const { actionButton, section = "general", isProjectView = false } = props; + const { actionButton, section = "general", isProjectView = false, className } = props; // assets const settingAsset = isProjectView ? ProjectNotAuthorizedImg : WorkspaceNotAuthorizedImg; const asset = section === "settings" ? settingAsset : Unauthorized; return ( - +
diff --git a/web/core/components/core/modals/workspace-image-upload-modal.tsx b/web/core/components/core/modals/workspace-image-upload-modal.tsx index 6b4bf3609..163e7ff29 100644 --- a/web/core/components/core/modals/workspace-image-upload-modal.tsx +++ b/web/core/components/core/modals/workspace-image-upload-modal.tsx @@ -8,7 +8,7 @@ import { Transition, Dialog } from "@headlessui/react"; // plane imports import { ACCEPTED_AVATAR_IMAGE_MIME_TYPES_FOR_REACT_DROPZONE, MAX_FILE_SIZE } from "@plane/constants"; import { EFileAssetType } from "@plane/types/src/enums"; -import { Button } from "@plane/ui"; +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; // helpers import { getAssetIdFromUrl, getFileURL } from "@/helpers/file.helper"; import { checkURLValidity } from "@/helpers/string.helper"; @@ -71,9 +71,13 @@ export const WorkspaceImageUploadModal: React.FC = observer((props) => { ); updateWorkspaceLogo(workspaceSlug.toString(), asset_url); onSuccess(asset_url); - } catch (error) { + } catch (error: any) { console.log("error", error); - throw new Error("Error in uploading file."); + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error", + message: error.error || "Something went wrong", + }); } finally { setIsImageUploading(false); } diff --git a/web/core/components/estimates/empty-screen.tsx b/web/core/components/estimates/empty-screen.tsx index 0155e2519..73acce27f 100644 --- a/web/core/components/estimates/empty-screen.tsx +++ b/web/core/components/estimates/empty-screen.tsx @@ -1,13 +1,10 @@ "use client"; import { FC } from "react"; -import Image from "next/image"; import { useTheme } from "next-themes"; import { useTranslation } from "@plane/i18n"; -import { Button } from "@plane/ui"; // public images -import EstimateEmptyDarkImage from "@/public/empty-state/estimates/dark.svg"; -import EstimateEmptyLightImage from "@/public/empty-state/estimates/light.svg"; +import { DetailedEmptyState } from "../empty-state"; type TEstimateEmptyScreen = { onButtonClick: () => void; @@ -20,28 +17,17 @@ export const EstimateEmptyScreen: FC = (props) => { const { t } = useTranslation(); - const emptyScreenImage = resolvedTheme === "light" ? EstimateEmptyLightImage : EstimateEmptyDarkImage; - + const resolvedPath = `/empty-state/project-settings/estimates-${resolvedTheme === "light" ? "light" : "dark"}.png`; return ( -
-
- -
-
-

- {t("project_settings.empty_state.estimates.title")} -

-

{t("project_settings.empty_state.estimates.description")}

-
-
- -
-
+ ); }; diff --git a/web/core/components/estimates/root.tsx b/web/core/components/estimates/root.tsx index 56a8f5efa..5d0294f72 100644 --- a/web/core/components/estimates/root.tsx +++ b/web/core/components/estimates/root.tsx @@ -15,6 +15,7 @@ import { import { useProject, useProjectEstimates } from "@/hooks/store"; // plane web components import { UpdateEstimateModal } from "@/plane-web/components/estimates"; +import { SettingsHeading } from "../settings"; type TEstimateRoot = { workspaceSlug: string; @@ -46,9 +47,11 @@ export const EstimateRoot: FC = observer((props) => { ) : (
{/* header */} -
-

{t("common.estimates")}

-
+ + {/* current active estimate section */} {currentActiveEstimateId ? ( @@ -57,7 +60,7 @@ export const EstimateRoot: FC = observer((props) => {

{t("project_settings.estimates.title")}

-

{t("project_settings.estimates.description")}

+

{t("project_settings.estimates.enable_description")}

diff --git a/web/core/components/exporter/column.tsx b/web/core/components/exporter/column.tsx new file mode 100644 index 000000000..113478c9d --- /dev/null +++ b/web/core/components/exporter/column.tsx @@ -0,0 +1,112 @@ +import { Download } from "lucide-react"; +import { IExportData } from "@plane/types"; +import { getDate, getFileURL, renderFormattedDate } from "@plane/utils"; + +type RowData = IExportData; +const checkExpiry = (inputDateString: string) => { + const currentDate = new Date(); + const expiryDate = getDate(inputDateString); + if (!expiryDate) return false; + expiryDate.setDate(expiryDate.getDate() + 7); + return expiryDate > currentDate; +}; +export const useExportColumns = () => { + const columns = [ + { + key: "Exported By", + content: "Exported By", + tdRender: (rowData: RowData) => { + const { avatar_url, display_name, email } = rowData.initiated_by_detail; + return ( +
+
+ {avatar_url && avatar_url.trim() !== "" ? ( + + {display_name + + ) : ( + + {(email ?? display_name ?? "?")[0]} + + )} +
+
{display_name}
+
+ ); + }, + }, + { + key: "Exported On", + content: "Exported On", + tdRender: (rowData: RowData) => {renderFormattedDate(rowData.created_at)}, + }, + + { + key: "Exported projects", + content: "Exported projects", + tdRender: (rowData: RowData) =>
{rowData.project.length} project(s)
, + }, + { + key: "Format", + content: "Format", + tdRender: (rowData: RowData) => ( + + {rowData.provider === "csv" + ? "CSV" + : rowData.provider === "xlsx" + ? "Excel" + : rowData.provider === "json" + ? "JSON" + : ""} + + ), + }, + { + key: "Status", + content: "Status", + tdRender: (rowData: RowData) => ( + + {rowData.status} + + ), + }, + { + key: "Download", + content: "Download", + tdRender: (rowData: RowData) => + checkExpiry(rowData.created_at) ? ( + <> + {rowData.status == "completed" ? ( + + + + ) : ( + "-" + )} + + ) : ( +
Expired
+ ), + }, + ]; + return columns; +}; diff --git a/web/core/components/exporter/export-form.tsx b/web/core/components/exporter/export-form.tsx new file mode 100644 index 000000000..b108d2e4c --- /dev/null +++ b/web/core/components/exporter/export-form.tsx @@ -0,0 +1,172 @@ +import { useState } from "react"; +import { intersection } from "lodash"; +import { Controller, useForm } from "react-hook-form"; +import { EUserPermissions, EUserPermissionsLevel, EXPORTERS_LIST } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; +import { Button, CustomSearchSelect, CustomSelect, TOAST_TYPE, setToast } from "@plane/ui"; +import { useProject, useUser, useUserPermissions } from "@/hooks/store"; +import { ProjectExportService } from "@/services/project/project-export.service"; + +type Props = { + workspaceSlug: string; + provider: string | null; + mutateServices: () => void; +}; +type FormData = { + provider: (typeof EXPORTERS_LIST)[0]; + project: string[]; + multiple: boolean; +}; +const projectExportService = new ProjectExportService(); + +export const ExportForm = (props: Props) => { + // props + const { workspaceSlug, mutateServices } = props; + // states + const [exportLoading, setExportLoading] = useState(false); + // store hooks + const { allowPermissions } = useUserPermissions(); + const { data: user, canPerformAnyCreateAction, projectsWithCreatePermissions } = useUser(); + const { workspaceProjectIds, getProjectById } = useProject(); + const { t } = useTranslation(); + // form + const { handleSubmit, control } = useForm({ + defaultValues: { + provider: EXPORTERS_LIST[0], + project: [], + multiple: false, + }, + }); + // derived values + const hasProjects = workspaceProjectIds && workspaceProjectIds.length > 0; + const isAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE); + const wsProjectIdsWithCreatePermisisons = projectsWithCreatePermissions + ? intersection(workspaceProjectIds, Object.keys(projectsWithCreatePermissions)) + : []; + const options = wsProjectIdsWithCreatePermisisons?.map((projectId) => { + const projectDetails = getProjectById(projectId); + + return { + value: projectDetails?.id, + query: `${projectDetails?.name} ${projectDetails?.identifier}`, + content: ( +
+ {projectDetails?.identifier} + {projectDetails?.name} +
+ ), + }; + }); + + // handlers + const ExportCSVToMail = async (formData: FormData) => { + console.log(formData); + setExportLoading(true); + if (workspaceSlug && user) { + const payload = { + provider: formData.provider.provider, + project: formData.project, + multiple: formData.project.length > 1, + }; + await projectExportService + .csvExport(workspaceSlug as string, payload) + .then(() => { + mutateServices(); + setExportLoading(false); + setToast({ + type: TOAST_TYPE.SUCCESS, + title: t("workspace_settings.settings.exports.modal.toasts.success.title"), + message: t("workspace_settings.settings.exports.modal.toasts.success.message", { + entity: + formData.provider.provider === "csv" + ? "CSV" + : formData.provider.provider === "xlsx" + ? "Excel" + : formData.provider.provider === "json" + ? "JSON" + : "", + }), + }); + }) + .catch(() => { + setExportLoading(false); + setToast({ + type: TOAST_TYPE.ERROR, + title: t("error"), + message: t("workspace_settings.settings.exports.modal.toasts.error.message"), + }); + }); + } + }; + return ( +
+
+ {/* Project Selector */} +
+
+ {t("workspace_settings.settings.exports.exporting_projects")} +
+ ( + onChange(val)} + options={options} + input + label={ + value && value.length > 0 + ? value + .map((projectId) => { + const projectDetails = getProjectById(projectId); + + return projectDetails?.identifier; + }) + .join(", ") + : "All projects" + } + optionsClassName="max-w-48 sm:max-w-[532px]" + placement="bottom-end" + multiple + /> + )} + /> +
+ {/* Format Selector */} +
+
+ {t("workspace_settings.settings.exports.format")} +
+ ( + + {EXPORTERS_LIST.map((service) => ( + + {t(service.i18n_title)} + + ))} + + )} + /> +
+
+
+ +
+ + ); +}; diff --git a/web/core/components/exporter/guide.tsx b/web/core/components/exporter/guide.tsx index 1dfd90010..024cd059e 100644 --- a/web/core/components/exporter/guide.tsx +++ b/web/core/components/exporter/guide.tsx @@ -1,221 +1,38 @@ "use client"; -import { useEffect, useState } from "react"; +import { useState } from "react"; import { observer } from "mobx-react"; -import Image from "next/image"; -import Link from "next/link"; import { useParams, useSearchParams } from "next/navigation"; -import useSWR, { mutate } from "swr"; -// icons -import { MoveLeft, MoveRight, RefreshCw } from "lucide-react"; -// plane imports -import { EXPORTERS_LIST, EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; -import { useTranslation } from "@plane/i18n"; -import { Button } from "@plane/ui"; -// components -import { DetailedEmptyState } from "@/components/empty-state"; -import { Exporter, SingleExport } from "@/components/exporter"; -import { ImportExportSettingsLoader } from "@/components/ui"; -// constants +import { mutate } from "swr"; import { EXPORT_SERVICES_LIST } from "@/constants/fetch-keys"; -// hooks -import { useProject, useUser, useUserPermissions } from "@/hooks/store"; -import { useAppRouter } from "@/hooks/use-app-router"; -import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; -// services images -import CSVLogo from "@/public/services/csv.svg"; -import ExcelLogo from "@/public/services/excel.svg"; -import JSONLogo from "@/public/services/json.svg"; -// services -import { IntegrationService } from "@/services/integrations"; - -const integrationService = new IntegrationService(); - -const getExporterLogo = (provider: string) => { - switch (provider) { - case "csv": - return CSVLogo; - case "excel": - return ExcelLogo; - case "xlsx": - return ExcelLogo; - case "json": - return JSONLogo; - default: - return ""; - } -}; +import { ExportForm } from "./export-form"; +import { PrevExports } from "./prev-exports"; const IntegrationGuide = observer(() => { - // states - const [refreshing, setRefreshing] = useState(false); - const per_page = 10; - const [cursor, setCursor] = useState(`10:0:0`); // router - const router = useAppRouter(); const { workspaceSlug } = useParams(); const searchParams = useSearchParams(); const provider = searchParams.get("provider"); - // plane hooks - const { t } = useTranslation(); - // store hooks - const { data: currentUser, canPerformAnyCreateAction } = useUser(); - const { allowPermissions } = useUserPermissions(); - const { workspaceProjectIds } = useProject(); - // derived values - const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/workspace-settings/exports" }); - - const { data: exporterServices } = useSWR( - workspaceSlug && cursor ? EXPORT_SERVICES_LIST(workspaceSlug as string, cursor, `${per_page}`) : null, - workspaceSlug && cursor - ? () => integrationService.getExportsServicesList(workspaceSlug as string, cursor, per_page) - : null - ); - - const handleRefresh = () => { - setRefreshing(true); - mutate(EXPORT_SERVICES_LIST(workspaceSlug as string, `${cursor}`, `${per_page}`)).then(() => setRefreshing(false)); - }; - - const handleCsvClose = () => { - router.replace(`/${workspaceSlug?.toString()}/settings/exports`); - }; - - const hasProjects = workspaceProjectIds && workspaceProjectIds.length > 0; - const isAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE); - - useEffect(() => { - const interval = setInterval(() => { - if (exporterServices?.results?.some((service) => service.status === "processing")) { - handleRefresh(); - } else { - clearInterval(interval); - } - }, 3000); - - return () => clearInterval(interval); - }, [exporterServices]); + // state + const per_page = 10; + const [cursor, setCursor] = useState(`10:0:0`); return ( <>
<> -
- {EXPORTERS_LIST.map((service) => ( -
-
-
-
- -
-
-

{t(service.i18n_title)}

-

{t(service.i18n_description)}

-
-
-
- - - - - -
-
-
- ))} -
-
-
-
-

- {t("workspace_settings.settings.exports.previous_exports")} -

- - -
-
- - -
-
-
- {exporterServices && exporterServices?.results ? ( - exporterServices?.results?.length > 0 ? ( -
-
- {exporterServices?.results.map((service) => ( - - ))} -
-
- ) : ( -
- -
- ) - ) : ( - - )} -
-
- - {provider && ( - handleCsvClose()} - data={null} - user={currentUser || null} + mutate(EXPORT_SERVICES_LIST(workspaceSlug as string, `${cursor}`, `${per_page}`))} /> - )} + +
); diff --git a/web/core/components/exporter/prev-exports.tsx b/web/core/components/exporter/prev-exports.tsx new file mode 100644 index 000000000..b07d580b3 --- /dev/null +++ b/web/core/components/exporter/prev-exports.tsx @@ -0,0 +1,137 @@ +import { useEffect, useState } from "react"; +import { observer } from "mobx-react"; +import useSWR, { mutate } from "swr"; +import { MoveLeft, MoveRight, RefreshCw } from "lucide-react"; +import { useTranslation } from "@plane/i18n"; +import { IExportData } from "@plane/types"; +import { Table } from "@plane/ui"; +import { EXPORT_SERVICES_LIST } from "@/constants/fetch-keys"; +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; +import { IntegrationService } from "@/services/integrations"; +import { DetailedEmptyState } from "../empty-state"; +import { ImportExportSettingsLoader } from "../ui"; +import { useExportColumns } from "./column"; + +const integrationService = new IntegrationService(); + +type Props = { + workspaceSlug: string; + cursor: string | undefined; + per_page: number; + setCursor: (cursor: string) => void; +}; +type RowData = IExportData; +export const PrevExports = observer((props: Props) => { + // props + const { workspaceSlug, cursor, per_page, setCursor } = props; + // state + const [refreshing, setRefreshing] = useState(false); + // hooks + const { t } = useTranslation(); + const columns = useExportColumns(); + const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/workspace-settings/exports" }); + + const { data: exporterServices } = useSWR( + workspaceSlug && cursor ? EXPORT_SERVICES_LIST(workspaceSlug as string, cursor, `${per_page}`) : null, + workspaceSlug && cursor + ? () => integrationService.getExportsServicesList(workspaceSlug as string, cursor, per_page) + : null + ); + + const handleRefresh = () => { + setRefreshing(true); + mutate(EXPORT_SERVICES_LIST(workspaceSlug as string, `${cursor}`, `${per_page}`)).then(() => setRefreshing(false)); + }; + + useEffect(() => { + const interval = setInterval(() => { + if (exporterServices?.results?.some((service) => service.status === "processing")) { + handleRefresh(); + } else { + clearInterval(interval); + } + }, 3000); + + return () => clearInterval(interval); + }, [exporterServices]); + + return ( +
+
+
+

+ {t("workspace_settings.settings.exports.previous_exports")} +

+ + +
+
+ + +
+
+ +
+ {exporterServices && exporterServices?.results ? ( + exporterServices?.results?.length > 0 ? ( +
+
+
[role=checkbox]]:translate-y-[2px]", + className + )} + {...props} + /> +)) +TableHead.displayName = "TableHead" + +const TableCell = React.forwardRef< + HTMLTableDataCellElement, + React.TdHTMLAttributes +>(({ className, ...props }, ref) => ( + [role=checkbox]]:translate-y-[2px]", + className + )} + {...props} + /> +)) +TableCell.displayName = "TableCell" + +const TableCaption = React.forwardRef< + HTMLTableDataCellElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +TableCaption.displayName = "TableCaption" + +export { + Table, + TableHeader, + TableBody, + TableFooter, + TableHead, + TableRow, + TableCell, + TableCaption, +} \ No newline at end of file diff --git a/packages/propel/src/table/index.ts b/packages/propel/src/table/index.ts new file mode 100644 index 000000000..8b83d73fe --- /dev/null +++ b/packages/propel/src/table/index.ts @@ -0,0 +1 @@ +export * from "./core"; \ No newline at end of file diff --git a/packages/types/src/analytics-v2.d.ts b/packages/types/src/analytics-v2.d.ts new file mode 100644 index 000000000..176cd1191 --- /dev/null +++ b/packages/types/src/analytics-v2.d.ts @@ -0,0 +1,52 @@ +import { ChartXAxisProperty, ChartYAxisMetric } from "@plane/constants"; +import { TChartData } from "./charts"; + +export type TAnalyticsTabsV2Base = "overview" | "work-items" +export type TAnalyticsGraphsV2Base = "projects" | "work-items" | "custom-work-items" + + +// service types + +export interface IAnalyticsResponseV2 { + [key: string]: any; +} + +export interface IAnalyticsResponseFieldsV2 { + count: number; + filter_count: number; +} + +export interface IAnalyticsRadarEntityV2 { + key: string, + name: string, + count: number +} + +// chart types + +export interface IChartResponseV2 { + schema: Record; + data: TChartData[]; +} + +// table types + +export interface WorkItemInsightColumns { + project_id: string; + project__name: string; + cancelled_work_items: number; + completed_work_items: number; + backlog_work_items: number; + un_started_work_items: number; + started_work_items: number; +} + +export type AnalyticsTableDataMap = { + "work-items": WorkItemInsightColumns, +} + +export interface IAnalyticsV2Params { + x_axis: ChartXAxisProperty; + y_axis: ChartYAxisMetric; + group_by?: ChartXAxisProperty; +} \ No newline at end of file diff --git a/packages/types/src/charts/common.d.ts b/packages/types/src/charts/common.d.ts new file mode 100644 index 000000000..85034c2fe --- /dev/null +++ b/packages/types/src/charts/common.d.ts @@ -0,0 +1,16 @@ + + +export type TChartColorScheme = "modern" | "horizon" | "earthen"; + +export type TChartDatum = { + key: string; + name: string; + count: number; +} & Record; + +export type TChart = { + data: TChartDatum[]; + schema: Record; +}; + + diff --git a/packages/types/src/charts.d.ts b/packages/types/src/charts/index.d.ts similarity index 63% rename from packages/types/src/charts.d.ts rename to packages/types/src/charts/index.d.ts index b1fc2997d..2747973aa 100644 --- a/packages/types/src/charts.d.ts +++ b/packages/types/src/charts/index.d.ts @@ -1,7 +1,14 @@ + + +// ============================================================ +// Chart Base +// ============================================================ +export * from "./common"; export type TChartLegend = { align: "left" | "center" | "right"; verticalAlign: "top" | "middle" | "bottom"; layout: "horizontal" | "vertical"; + wrapperStyles?: React.CSSProperties; }; export type TChartMargin = { @@ -22,6 +29,7 @@ type TChartProps = { key: keyof TChartData; label?: string; strokeColor?: string; + dy?: number; }; yAxis: { allowDecimals?: boolean; @@ -29,6 +37,8 @@ type TChartProps = { key: keyof TChartData; label?: string; strokeColor?: string; + offset?: number; + dx?: number; }; className?: string; legend?: TChartLegend; @@ -40,6 +50,10 @@ type TChartProps = { showTooltip?: boolean; }; +// ============================================================ +// Bar Chart +// ============================================================ + export type TBarItem = { key: T; label: string; @@ -56,6 +70,10 @@ export type TBarChartProps = TChartProps = { key: T; label: string; @@ -71,6 +89,25 @@ export type TLineChartProps = TChartProps[]; }; +// ============================================================ +// Scatter Chart +// ============================================================ + +export type TScatterPointItem = { + key: T; + label: string; + fill: string; + stroke: string; +}; + +export type TScatterChartProps = TChartProps & { + scatterPoints: TScatterPointItem[]; +}; + +// ============================================================ +// Area Chart +// ============================================================ + export type TAreaItem = { key: T; label: string; @@ -92,6 +129,10 @@ export type TAreaChartProps = TChartProps = { key: T; fill: string; @@ -119,6 +160,10 @@ export type TPieChartProps = Pick< customLegend?: (props: any) => React.ReactNode; }; +// ============================================================ +// Tree Map +// ============================================================ + export type TreeMapItem = { name: string; value: number; @@ -126,13 +171,13 @@ export type TreeMapItem = { textClassName?: string; icon?: React.ReactElement; } & ( - | { + | { fillColor: string; } - | { + | { fillClassName: string; } -); + ); export type TreeMapChartProps = { data: TreeMapItem[]; @@ -158,3 +203,32 @@ export type TContentVisibility = { top: TTopSectionConfig; bottom: TBottomSectionConfig; }; + +// ============================================================ +// Radar Chart +// ============================================================ + +export type TRadarItem = { + key: T; + name: string; + fill?: string; + stroke?: string; + fillOpacity?: number; + dot?: { + r: number; + fillOpacity: number; + } +} + +export type TRadarChartProps = Pick< + TChartProps, + "className" | "showTooltip" | "margin" | "data" | "legend" +> & { + dataKey: T; + radars: TRadarItem[]; + angleAxis: { + key: keyof TChartData; + label?: string; + strokeColor?: string; + }; +} diff --git a/packages/types/src/enums.ts b/packages/types/src/enums.ts index 53138a1d7..a49bec7ab 100644 --- a/packages/types/src/enums.ts +++ b/packages/types/src/enums.ts @@ -67,3 +67,9 @@ export enum EFileAssetType { PROJECT_DESCRIPTION = "PROJECT_DESCRIPTION", TEAM_SPACE_COMMENT_DESCRIPTION = "TEAM_SPACE_COMMENT_DESCRIPTION", } + +export enum EUpdateStatus { + OFF_TRACK = "OFF-TRACK", + ON_TRACK = "ON-TRACK", + AT_RISK = "AT-RISK", +} \ No newline at end of file diff --git a/packages/types/src/index.d.ts b/packages/types/src/index.d.ts index b6af3b562..0ac656fc6 100644 --- a/packages/types/src/index.d.ts +++ b/packages/types/src/index.d.ts @@ -43,3 +43,4 @@ export * from "./home"; export * from "./stickies"; export * from "./utils"; export * from "./payment"; +export * from "./analytics-v2"; \ No newline at end of file diff --git a/packages/ui/src/icons/at-risk-icon.tsx b/packages/ui/src/icons/at-risk-icon.tsx index bb4437e6d..65e5ae63d 100644 --- a/packages/ui/src/icons/at-risk-icon.tsx +++ b/packages/ui/src/icons/at-risk-icon.tsx @@ -3,27 +3,18 @@ import * as React from "react"; import { ISvgIcons } from "./type"; export const AtRiskIcon: React.FC = ({ width = "16", height = "16" }) => ( - - - - + + - - + + diff --git a/packages/ui/src/icons/off-track-icon.tsx b/packages/ui/src/icons/off-track-icon.tsx index 0d93d1b60..cbb8ba1f8 100644 --- a/packages/ui/src/icons/off-track-icon.tsx +++ b/packages/ui/src/icons/off-track-icon.tsx @@ -3,27 +3,18 @@ import * as React from "react"; import { ISvgIcons } from "./type"; export const OffTrackIcon: React.FC = ({ width = "16", height = "16" }) => ( - - - - + + - - + + diff --git a/packages/ui/src/icons/on-track-icon.tsx b/packages/ui/src/icons/on-track-icon.tsx index c384d4c8d..5dcabcec9 100644 --- a/packages/ui/src/icons/on-track-icon.tsx +++ b/packages/ui/src/icons/on-track-icon.tsx @@ -3,45 +3,39 @@ import * as React from "react"; import { ISvgIcons } from "./type"; export const OnTrackIcon: React.FC = ({ width = "16", height = "16" }) => ( - - - - + + - - + + diff --git a/web/app/[workspaceSlug]/(projects)/analytics/layout.tsx b/web/app/[workspaceSlug]/(projects)/analytics/layout.tsx index 8dfc8b3b0..6f087aa56 100644 --- a/web/app/[workspaceSlug]/(projects)/analytics/layout.tsx +++ b/web/app/[workspaceSlug]/(projects)/analytics/layout.tsx @@ -1,6 +1,7 @@ "use client"; - +// components import { AppHeader, ContentWrapper } from "@/components/core"; +// plane web components import { WorkspaceAnalyticsHeader } from "./header"; export default function WorkspaceAnalyticsLayout({ children }: { children: React.ReactNode }) { diff --git a/web/app/[workspaceSlug]/(projects)/analytics/page.tsx b/web/app/[workspaceSlug]/(projects)/analytics/page.tsx index 8875e1465..b49723633 100644 --- a/web/app/[workspaceSlug]/(projects)/analytics/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/analytics/page.tsx @@ -1,24 +1,24 @@ "use client"; -import React, { Fragment } from "react"; +import { useMemo } from "react"; import { observer } from "mobx-react"; -import { useSearchParams } from "next/navigation"; -import { Tab } from "@headlessui/react"; +import { useRouter, useSearchParams } from "next/navigation"; // plane package imports -import { ANALYTICS_TABS, EUserPermissionsLevel, EUserPermissions } from "@plane/constants"; +import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; -import { Header, EHeaderVariant } from "@plane/ui"; +import { Tabs } from "@plane/ui"; // components -import { CustomAnalytics, ScopeAndDemand } from "@/components/analytics"; +import AnalyticsFilterActions from "@/components/analytics-v2/analytics-filter-actions"; import { PageHead } from "@/components/core"; import { ComicBoxButton, DetailedEmptyState } from "@/components/empty-state"; // hooks import { useCommandPalette, useEventTracker, useProject, useUserPermissions, useWorkspace } from "@/hooks/store"; import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; +import { ANALYTICS_TABS } from "@/plane-web/components/analytics-v2/tabs"; const AnalyticsPage = observer(() => { + const router = useRouter(); const searchParams = useSearchParams(); - const analytics_tab = searchParams.get("analytics_tab"); // plane imports const { t } = useTranslation(); // store hooks @@ -40,44 +40,38 @@ const AnalyticsPage = observer(() => { EUserPermissionsLevel.WORKSPACE ); - // TODO: refactor loader implementation + const tabs = useMemo( + () => + ANALYTICS_TABS.map((tab) => ({ + key: tab.key, + label: t(tab.i18nKey), + content: , + onClick: () => { + router.push(`?tab=${tab.key}`); + }, + })), + [router, t] + ); + const defaultTab = searchParams.get("tab") || ANALYTICS_TABS[0].key; + return ( <> {workspaceProjectIds && ( <> {workspaceProjectIds.length > 0 || loader === "init-loader" ? ( -
- -
- - {ANALYTICS_TABS.map((tab) => ( - - {({ selected }) => ( - - )} - - ))} - -
- - - - - - - - -
+
+ } + />
) : ( { return ( <> - setAnalyticsModal(false)} projectDetails={currentProjectDetails ?? undefined} diff --git a/web/app/page.tsx b/web/app/page.tsx index 8d52af80c..aac36f0a1 100644 --- a/web/app/page.tsx +++ b/web/app/page.tsx @@ -1,5 +1,4 @@ "use client"; - import React from "react"; import { observer } from "mobx-react"; import Image from "next/image"; diff --git a/web/ce/components/analytics-v2/tabs.ts b/web/ce/components/analytics-v2/tabs.ts new file mode 100644 index 000000000..8390601eb --- /dev/null +++ b/web/ce/components/analytics-v2/tabs.ts @@ -0,0 +1,11 @@ +import { TAnalyticsTabsV2Base } from "@plane/types"; +import { Overview } from "@/components/analytics-v2/overview"; +import { WorkItems } from "@/components/analytics-v2/work-items"; +export const ANALYTICS_TABS: { + key: TAnalyticsTabsV2Base; + i18nKey: string; + content: React.FC; +}[] = [ + { key: "overview", i18nKey: "common.overview", content: Overview }, + { key: "work-items", i18nKey: "sidebar.work_items", content: WorkItems }, + ]; diff --git a/web/core/components/analytics-v2/analytics-filter-actions.tsx b/web/core/components/analytics-v2/analytics-filter-actions.tsx new file mode 100644 index 000000000..b9b69bed9 --- /dev/null +++ b/web/core/components/analytics-v2/analytics-filter-actions.tsx @@ -0,0 +1,34 @@ +// plane web components +import { observer } from "mobx-react-lite"; +// hooks +import { useProject } from "@/hooks/store"; +import { useAnalyticsV2 } from "@/hooks/store/use-analytics-v2"; +// components +import DurationDropdown from "./select/duration"; +import { ProjectSelect } from "./select/project"; + +const AnalyticsFilterActions = observer(() => { + const { selectedProjects, selectedDuration, updateSelectedProjects, updateSelectedDuration } = useAnalyticsV2(); + const { workspaceProjectIds } = useProject(); + return ( +
+ { + updateSelectedProjects(val ?? []); + }} + projectIds={workspaceProjectIds} + /> + { + updateSelectedDuration(val); + }} + dropdownArrow + /> +
+ ); +}); + +export default AnalyticsFilterActions; diff --git a/web/core/components/analytics-v2/analytics-section-wrapper.tsx b/web/core/components/analytics-v2/analytics-section-wrapper.tsx new file mode 100644 index 000000000..deb691644 --- /dev/null +++ b/web/core/components/analytics-v2/analytics-section-wrapper.tsx @@ -0,0 +1,30 @@ +import { cn } from "@plane/utils"; + +type Props = { + title?: string; + children: React.ReactNode; + className?: string; + subtitle?: string | null; + actions?: React.ReactNode; + headerClassName?: string; +}; + +const AnalyticsSectionWrapper: React.FC = (props) => { + const { title, children, className, subtitle, actions, headerClassName } = props; + return ( +
+
+ {title && ( +
+

{title}

+ {subtitle &&

• {subtitle}

} +
+ )} + {actions} +
+ {children} +
+ ); +}; + +export default AnalyticsSectionWrapper; diff --git a/web/core/components/analytics-v2/analytics-wrapper.tsx b/web/core/components/analytics-v2/analytics-wrapper.tsx new file mode 100644 index 000000000..d6193a2b3 --- /dev/null +++ b/web/core/components/analytics-v2/analytics-wrapper.tsx @@ -0,0 +1,22 @@ +import React from "react"; +// plane package imports +import { cn } from "@plane/utils"; + +type Props = { + title: string; + children: React.ReactNode; + className?: string; +}; + +const AnalyticsWrapper: React.FC = (props) => { + const { title, children, className } = props; + + return ( +
+

{title}

+ {children} +
+ ); +}; + +export default AnalyticsWrapper; diff --git a/web/core/components/analytics-v2/empty-state.tsx b/web/core/components/analytics-v2/empty-state.tsx new file mode 100644 index 000000000..1a1ee86e8 --- /dev/null +++ b/web/core/components/analytics-v2/empty-state.tsx @@ -0,0 +1,48 @@ +import React from "react"; +import Image from "next/image"; +// plane package imports +import { cn } from "@plane/utils"; +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; + +type Props = { + title: string; + description?: string; + assetPath?: string; + className?: string; +}; + +const AnalyticsV2EmptyState = ({ title, description, assetPath, className }: Props) => { + const backgroundReolvedPath = useResolvedAssetPath({ basePath: "/empty-state/analytics-v2/empty-grid-background" }); + + return ( +
+
+ {assetPath && ( +
+ {title} +
+ {title} +
+
+ )} +
+

{title}

+ {description &&

{description}

} +
+
+
+ ); +}; +export default AnalyticsV2EmptyState; diff --git a/web/core/components/analytics-v2/index.ts b/web/core/components/analytics-v2/index.ts new file mode 100644 index 000000000..8ac82df5d --- /dev/null +++ b/web/core/components/analytics-v2/index.ts @@ -0,0 +1 @@ +export * from "./overview/root"; diff --git a/web/core/components/analytics-v2/insight-card.tsx b/web/core/components/analytics-v2/insight-card.tsx new file mode 100644 index 000000000..cd22b7e92 --- /dev/null +++ b/web/core/components/analytics-v2/insight-card.tsx @@ -0,0 +1,47 @@ +// plane package imports +import React, { useMemo } from "react"; +import { IAnalyticsResponseFieldsV2 } from "@plane/types"; +import { Loader } from "@plane/ui"; +// components +import TrendPiece from "./trend-piece"; + +export type InsightCardProps = { + data?: IAnalyticsResponseFieldsV2; + label: string; + isLoading?: boolean; + versus?: string | null; +}; + +const InsightCard = (props: InsightCardProps) => { + const { data, label, isLoading, versus } = props; + const { count, filter_count } = data || {}; + const percentage = useMemo(() => { + if (count != null && filter_count != null) { + const result = ((count - filter_count) / count) * 100; + const isFiniteAndNotNaNOrZero = Number.isFinite(result) && !Number.isNaN(result) && result !== 0; + return isFiniteAndNotNaNOrZero ? result : null; + } + return null; + }, [count, filter_count]); + + return ( +
+
{label}
+ {!isLoading ? ( +
+
{count}
+ {percentage && ( +
+ + {versus &&
vs {versus}
} +
+ )} +
+ ) : ( + + )} +
+ ); +}; + +export default InsightCard; diff --git a/web/core/components/analytics-v2/insight-table/data-table.tsx b/web/core/components/analytics-v2/insight-table/data-table.tsx new file mode 100644 index 000000000..c811c9265 --- /dev/null +++ b/web/core/components/analytics-v2/insight-table/data-table.tsx @@ -0,0 +1,177 @@ +"use client"; + +import * as React from "react"; +import { + ColumnDef, + ColumnFiltersState, + SortingState, + VisibilityState, + Table as TanstackTable, + flexRender, + getCoreRowModel, + getFacetedRowModel, + getFacetedUniqueValues, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, +} from "@tanstack/react-table"; +import { Search, X } from "lucide-react"; +// plane package imports +import { useTranslation } from "@plane/i18n"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@plane/propel/table"; +import { cn } from "@plane/utils"; +// plane web components +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; +import AnalyticsV2EmptyState from "../empty-state"; + +interface DataTableProps { + columns: ColumnDef[]; + data: TData[]; + searchPlaceholder: string; + actions?: (table: TanstackTable) => React.ReactNode; +} + +export function DataTable({ columns, data, searchPlaceholder, actions }: DataTableProps) { + const [rowSelection, setRowSelection] = React.useState({}); + const [columnVisibility, setColumnVisibility] = React.useState({}); + const [columnFilters, setColumnFilters] = React.useState([]); + const [sorting, setSorting] = React.useState([]); + const { t } = useTranslation(); + const inputRef = React.useRef(null); + const [isSearchOpen, setIsSearchOpen] = React.useState(false); + const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/analytics-v2/empty-table" }); + + const table = useReactTable({ + data, + columns, + state: { + sorting, + columnVisibility, + rowSelection, + columnFilters, + }, + enableRowSelection: true, + onRowSelectionChange: setRowSelection, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + onColumnVisibilityChange: setColumnVisibility, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFacetedRowModel: getFacetedRowModel(), + getFacetedUniqueValues: getFacetedUniqueValues(), + }); + + return ( +
+
+
+ {table.getHeaderGroups()?.[0]?.headers?.[0]?.id && ( +
+ {searchPlaceholder} +
+ )} + {!isSearchOpen && ( + + )} +
+ + { + const columnId = table.getHeaderGroups()?.[0]?.headers?.[0]?.id; + if (columnId) table.getColumn(columnId)?.setFilterValue(e.target.value); + }} + onKeyDown={(e) => { + if (e.key === "Enter") { + setIsSearchOpen(true); + } + }} + /> + {isSearchOpen && ( + + )} +
+
+ {actions &&
{actions(table)}
} +
+ +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder + ? null + : (flexRender(header.column.columnDef.header, header.getContext()) as any)} + + ))} + + ))} + + + {table.getRowModel().rows?.length > 0 ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext()) as any} + + ))} + + )) + ) : ( + + +
+ +
+
+
+ )} +
+
+
+
+ ); +} diff --git a/web/core/components/analytics-v2/insight-table/index.ts b/web/core/components/analytics-v2/insight-table/index.ts new file mode 100644 index 000000000..1efe34c51 --- /dev/null +++ b/web/core/components/analytics-v2/insight-table/index.ts @@ -0,0 +1 @@ +export * from "./root"; diff --git a/web/core/components/analytics-v2/insight-table/loader.tsx b/web/core/components/analytics-v2/insight-table/loader.tsx new file mode 100644 index 000000000..0f7f9dc35 --- /dev/null +++ b/web/core/components/analytics-v2/insight-table/loader.tsx @@ -0,0 +1,34 @@ +import * as React from "react"; +import { ColumnDef } from "@tanstack/react-table"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@plane/propel/table"; +import { Loader } from "@plane/ui"; + +interface TableSkeletonProps { + columns: ColumnDef[]; + rows: number; +} + +export const TableLoader: React.FC = ({ columns, rows }) => ( + + + + {columns.map((column, index) => ( + + {typeof column.header === "string" ? column.header : ""} + + ))} + + + + {Array.from({ length: rows }).map((_, rowIndex) => ( + + {columns.map((_, colIndex) => ( + + + + ))} + + ))} + +
+); diff --git a/web/core/components/analytics-v2/insight-table/root.tsx b/web/core/components/analytics-v2/insight-table/root.tsx new file mode 100644 index 000000000..0e482a40a --- /dev/null +++ b/web/core/components/analytics-v2/insight-table/root.tsx @@ -0,0 +1,74 @@ +import { ColumnDef, Row, Table } from "@tanstack/react-table"; +import { download, generateCsv, mkConfig } from "export-to-csv"; +import { useParams } from "next/navigation"; +import { Download } from "lucide-react"; +import { useTranslation } from "@plane/i18n"; +import { AnalyticsTableDataMap, TAnalyticsTabsV2Base } from "@plane/types"; +import { Button } from "@plane/ui"; +import { DataTable } from "./data-table"; +import { TableLoader } from "./loader"; +interface InsightTableProps> { + analyticsType: T; + data?: AnalyticsTableDataMap[T][]; + isLoading?: boolean; + columns: ColumnDef[]; + columnsLabels?: Record; +} + +export const InsightTable = >( + props: InsightTableProps +): React.ReactElement => { + const { data, isLoading, columns, columnsLabels } = props; + const params = useParams(); + const { t } = useTranslation(); + const workspaceSlug = params.workspaceSlug as string; + if (isLoading) { + return ; + } + + const csvConfig = mkConfig({ + fieldSeparator: ",", + filename: `${workspaceSlug}-analytics`, + decimalSeparator: ".", + useKeysAsHeaders: true, + }); + + const exportCSV = (rows: Row[]) => { + const rowData: any = rows.map((row) => { + const { project_id, ...exportableData } = row.original; + return Object.fromEntries( + Object.entries(exportableData).map(([key, value]) => { + if (columnsLabels?.[key]) { + return [columnsLabels[key], value]; + } + return [key, value]; + }) + ); + }); + const csv = generateCsv(csvConfig)(rowData); + download(csvConfig)(csv); + }; + + return ( +
+ {data ? ( + ) => ( + + )} + /> + ) : ( +
No data
+ )} +
+ ); +}; diff --git a/web/core/components/analytics-v2/loaders.tsx b/web/core/components/analytics-v2/loaders.tsx new file mode 100644 index 000000000..e35d235ce --- /dev/null +++ b/web/core/components/analytics-v2/loaders.tsx @@ -0,0 +1,23 @@ +import { Loader } from "@plane/ui"; + +export const ProjectInsightsLoader = () => ( +
+ + + +
+ + + + + + +
+
+); + +export const ChartLoader = () => ( + + + +); diff --git a/web/core/components/analytics-v2/overview/active-project-item.tsx b/web/core/components/analytics-v2/overview/active-project-item.tsx new file mode 100644 index 000000000..088bf6185 --- /dev/null +++ b/web/core/components/analytics-v2/overview/active-project-item.tsx @@ -0,0 +1,57 @@ +import { Briefcase } from "lucide-react"; +// plane package imports +import { Logo } from "@plane/ui"; +import { cn } from "@plane/utils"; +// plane web hooks +import { useProject } from "@/hooks/store"; + +type Props = { + project: { + id: string; + completed_issues?: number; + total_issues?: number; + }; + isLoading?: boolean; +}; +const CompletionPercentage = ({ percentage }: { percentage: number }) => { + const percentageColor = percentage > 50 ? "bg-green-500/30 text-green-500" : "bg-red-500/30 text-red-500"; + return ( +
+ {percentage}% +
+ ); +}; + +const ActiveProjectItem = (props: Props) => { + const { project } = props; + const { getProjectById } = useProject(); + const { id, completed_issues, total_issues } = project; + + const projectDetails = getProjectById(id); + + if (!projectDetails) return null; + + return ( +
+
+
+ + {projectDetails?.logo_props ? ( + + ) : ( + + + + )} + +
+

{projectDetails?.name}

+
+ +
+ ); +}; + +export default ActiveProjectItem; diff --git a/web/core/components/analytics-v2/overview/active-projects.tsx b/web/core/components/analytics-v2/overview/active-projects.tsx new file mode 100644 index 000000000..2d5d71116 --- /dev/null +++ b/web/core/components/analytics-v2/overview/active-projects.tsx @@ -0,0 +1,44 @@ +import React from "react"; +import { observer } from "mobx-react"; +import { useParams } from "next/navigation"; +import useSWR from "swr"; +// plane package imports +import { useTranslation } from "@plane/i18n"; +import { Loader } from "@plane/ui"; +// plane web hooks +import { useAnalyticsV2, useProject } from "@/hooks/store"; +// plane web components +import AnalyticsSectionWrapper from "../analytics-section-wrapper"; +import ActiveProjectItem from "./active-project-item"; + +const ActiveProjects = observer(() => { + const { t } = useTranslation(); + const { fetchProjectAnalyticsCount } = useProject(); + const { workspaceSlug } = useParams(); + const { selectedDurationLabel } = useAnalyticsV2(); + const { data: projectAnalyticsCount, isLoading: isProjectAnalyticsCountLoading } = useSWR( + workspaceSlug ? ["projectAnalyticsCount", workspaceSlug] : null, + workspaceSlug + ? () => + fetchProjectAnalyticsCount(workspaceSlug.toString(), { + fields: "total_work_items,total_completed_work_items", + }) + : null + ); + return ( + +
+ {isProjectAnalyticsCountLoading && + Array.from({ length: 5 }).map((_, index) => )} + {!isProjectAnalyticsCountLoading && + projectAnalyticsCount?.map((project) => )} +
+
+ ); +}); + +export default ActiveProjects; diff --git a/web/core/components/analytics-v2/overview/index.ts b/web/core/components/analytics-v2/overview/index.ts new file mode 100644 index 000000000..1efe34c51 --- /dev/null +++ b/web/core/components/analytics-v2/overview/index.ts @@ -0,0 +1 @@ +export * from "./root"; diff --git a/web/core/components/analytics-v2/overview/project-insights.tsx b/web/core/components/analytics-v2/overview/project-insights.tsx new file mode 100644 index 000000000..a767cf476 --- /dev/null +++ b/web/core/components/analytics-v2/overview/project-insights.tsx @@ -0,0 +1,109 @@ +import { observer } from "mobx-react"; +import dynamic from "next/dynamic"; +import { useParams } from "next/navigation"; +import useSWR from "swr"; +// plane package imports +import { useTranslation } from "@plane/i18n"; +import { TChartData } from "@plane/types"; +// hooks +import { useAnalyticsV2 } from "@/hooks/store/use-analytics-v2"; +// services +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; +import { AnalyticsV2Service } from "@/services/analytics-v2.service"; +// plane web components +import AnalyticsSectionWrapper from "../analytics-section-wrapper"; +import AnalyticsV2EmptyState from "../empty-state"; +import { ProjectInsightsLoader } from "../loaders"; + +const RadarChart = dynamic(() => + import("@plane/propel/charts/radar-chart").then((mod) => ({ + default: mod.RadarChart, + })) +); + +const analyticsV2Service = new AnalyticsV2Service(); + +const ProjectInsights = observer(() => { + const params = useParams(); + const { t } = useTranslation(); + const workspaceSlug = params.workspaceSlug as string; + const { selectedDuration, selectedDurationLabel, selectedProjects } = useAnalyticsV2(); + const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/analytics-v2/empty-chart-radar" }); + + const { data: projectInsightsData, isLoading: isLoadingProjectInsight } = useSWR( + `radar-chart-${workspaceSlug}-${selectedDuration}-${selectedProjects}`, + () => + analyticsV2Service.getAdvanceAnalyticsCharts[]>(workspaceSlug, "projects", { + date_filter: selectedDuration, + ...(selectedProjects?.length > 0 && { project_ids: selectedProjects?.join(",") }), + }) + ); + + return ( + + {isLoadingProjectInsight ? ( + + ) : projectInsightsData && projectInsightsData?.length == 0 ? ( + + ) : ( +
+ {projectInsightsData && ( + + )} +
+
{t("workspace_analytics.summary_of_projects")}
+
{t("workspace_analytics.all_projects")}
+
+
+
{t("workspace_analytics.trend_on_charts")}
+
{t("common.work_items")}
+
+ {projectInsightsData?.map((item) => ( +
+
{item.name}
+
+ {/* */} +
{item.count}
+
+
+ ))} +
+
+
+ )} +
+ ); +}); + +export default ProjectInsights; diff --git a/web/core/components/analytics-v2/overview/root.tsx b/web/core/components/analytics-v2/overview/root.tsx new file mode 100644 index 000000000..3856353aa --- /dev/null +++ b/web/core/components/analytics-v2/overview/root.tsx @@ -0,0 +1,19 @@ +import React from "react"; +import AnalyticsWrapper from "../analytics-wrapper"; +import TotalInsights from "../total-insights"; +import ActiveProjects from "./active-projects"; +import ProjectInsights from "./project-insights"; + +const Overview: React.FC = () => ( + +
+ +
+ + +
+
+
+); + +export { Overview }; diff --git a/web/core/components/analytics-v2/select/analytics-params.tsx b/web/core/components/analytics-v2/select/analytics-params.tsx new file mode 100644 index 000000000..61a9d1b1f --- /dev/null +++ b/web/core/components/analytics-v2/select/analytics-params.tsx @@ -0,0 +1,98 @@ +import { useMemo } from "react"; +import { observer } from "mobx-react"; +import { Control, Controller, UseFormSetValue } from "react-hook-form"; +import { Calendar, SlidersHorizontal } from "lucide-react"; +// plane package imports +import { ANALYTICS_V2_X_AXIS_VALUES, ANALYTICS_V2_Y_AXIS_VALUES, ChartYAxisMetric } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; +import { IAnalyticsV2Params } from "@plane/types"; +import { cn } from "@plane/utils"; +// plane web components +import { AnalyticsV2Service } from "@/services/analytics-v2.service"; +import { SelectXAxis } from "./select-x-axis"; +import { SelectYAxis } from "./select-y-axis"; + +type Props = { + control: Control; + setValue: UseFormSetValue; + params: IAnalyticsV2Params; + workspaceSlug: string; + classNames?: string; +}; + +export const AnalyticsV2SelectParams: React.FC = observer((props) => { + const { control, params, classNames } = props; + const xAxisOptions = useMemo( + () => ANALYTICS_V2_X_AXIS_VALUES.filter((option) => option.value !== params.group_by), + [params.group_by] + ); + const groupByOptions = useMemo( + () => ANALYTICS_V2_X_AXIS_VALUES.filter((option) => option.value !== params.x_axis), + [params.x_axis] + ); + + return ( +
+
+ ( + { + onChange(val); + }} + options={ANALYTICS_V2_Y_AXIS_VALUES} + hiddenOptions={[ChartYAxisMetric.ESTIMATE_POINT_COUNT]} + /> + )} + /> + ( + { + onChange(val); + }} + label={ +
+ + + {xAxisOptions.find((v) => v.value === value)?.label || "Add Property"} + +
+ } + options={xAxisOptions} + /> + )} + /> + ( + { + onChange(val); + }} + label={ +
+ + + {groupByOptions.find((v) => v.value === value)?.label || "Add Property"} + +
+ } + options={groupByOptions} + placeholder="Group By" + allowNoValue + /> + )} + /> +
+
+ ); +}); diff --git a/web/core/components/analytics-v2/select/duration.tsx b/web/core/components/analytics-v2/select/duration.tsx new file mode 100644 index 000000000..de18ab202 --- /dev/null +++ b/web/core/components/analytics-v2/select/duration.tsx @@ -0,0 +1,50 @@ +// plane package imports +import React, { ReactNode } from "react"; +import { Calendar } from "lucide-react"; +// plane package imports +import { ANALYTICS_V2_DURATION_FILTER_OPTIONS } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; +import { CustomSearchSelect } from "@plane/ui"; +// types +import { TDropdownProps } from "@/components/dropdowns/types"; + +type Props = TDropdownProps & { + value: string | null; + onChange: (val: (typeof ANALYTICS_V2_DURATION_FILTER_OPTIONS)[number]["value"]) => void; + //optional + button?: ReactNode; + dropdownArrow?: boolean; + dropdownArrowClassName?: string; + onClose?: () => void; + renderByDefault?: boolean; + tabIndex?: number; +}; + +function DurationDropdown({ placeholder = "Duration", onChange, value }: Props) { + useTranslation(); + + const options = ANALYTICS_V2_DURATION_FILTER_OPTIONS.map((option) => ({ + value: option.value, + query: option.name, + content: ( +
+ {option.name} +
+ ), + })); + return ( + + + {value ? ANALYTICS_V2_DURATION_FILTER_OPTIONS.find((opt) => opt.value === value)?.name : placeholder} +
+ } + /> + ); +} + +export default DurationDropdown; diff --git a/web/core/components/analytics-v2/select/project.tsx b/web/core/components/analytics-v2/select/project.tsx new file mode 100644 index 000000000..61a994208 --- /dev/null +++ b/web/core/components/analytics-v2/select/project.tsx @@ -0,0 +1,60 @@ +"use client"; + +import { observer } from "mobx-react"; +import { Briefcase } from "lucide-react"; +// plane package imports +import { CustomSearchSelect, Logo } from "@plane/ui"; +// hooks +import { useProject } from "@/hooks/store"; + +type Props = { + value: string[] | undefined; + onChange: (val: string[] | null) => void; + projectIds: string[] | undefined; +}; + +export const ProjectSelect: React.FC = observer((props) => { + const { value, onChange, projectIds } = props; + const { getProjectById } = useProject(); + + const options = projectIds?.map((projectId) => { + const projectDetails = getProjectById(projectId); + + return { + value: projectDetails?.id, + query: `${projectDetails?.name} ${projectDetails?.identifier}`, + content: ( +
+ {projectDetails?.logo_props ? ( + + ) : ( + + )} + {projectDetails?.name} +
+ ), + }; + }); + + return ( + onChange(val)} + options={options} + label={ +
+ + {value && value.length > 3 + ? `3+ projects` + : value && value.length > 0 + ? projectIds + ?.filter((p) => value.includes(p)) + .map((p) => getProjectById(p)?.name) + .join(", ") + : "All projects"} +
+ } + multiple + /> + ); +}); diff --git a/web/core/components/analytics-v2/select/select-x-axis.tsx b/web/core/components/analytics-v2/select/select-x-axis.tsx new file mode 100644 index 000000000..a655c9a13 --- /dev/null +++ b/web/core/components/analytics-v2/select/select-x-axis.tsx @@ -0,0 +1,31 @@ +"use client"; +// plane package imports +import { ChartXAxisProperty } from "@plane/constants"; +import { CustomSelect } from "@plane/ui"; + +type Props = { + value?: ChartXAxisProperty; + onChange: (val: ChartXAxisProperty | null) => void; + options: { value: ChartXAxisProperty; label: string }[]; + placeholder?: string; + hiddenOptions?: ChartXAxisProperty[]; + allowNoValue?: boolean; + label?: string | JSX.Element; +}; + +export const SelectXAxis: React.FC = (props) => { + const { value, onChange, options, hiddenOptions, allowNoValue, label } = props; + return ( + + {allowNoValue && No value} + {options.map((item) => { + if (hiddenOptions?.includes(item.value)) return null; + return ( + + {item.label} + + ); + })} + + ); +}; diff --git a/web/core/components/analytics-v2/select/select-y-axis.tsx b/web/core/components/analytics-v2/select/select-y-axis.tsx new file mode 100644 index 000000000..c80e2a1e4 --- /dev/null +++ b/web/core/components/analytics-v2/select/select-y-axis.tsx @@ -0,0 +1,67 @@ +"use client"; + +import { observer } from "mobx-react"; +import { useParams } from "next/navigation"; +import { Briefcase } from "lucide-react"; +// plane package imports +import { ChartYAxisMetric } from "@plane/constants"; +import { CustomSelect } from "@plane/ui"; +// hooks +import { useProjectEstimates } from "@/hooks/store"; +// plane web constants +import { EEstimateSystem } from "@/plane-web/constants/estimates"; + +type Props = { + value: ChartYAxisMetric; + onChange: (val: ChartYAxisMetric | null) => void; + hiddenOptions?: ChartYAxisMetric[]; + options: { value: ChartYAxisMetric; label: string }[]; +}; + +export const SelectYAxis: React.FC = observer(({ value, onChange, hiddenOptions, options }) => { + // hooks + const { projectId } = useParams(); + const { areEstimateEnabledByProjectId, currentActiveEstimateId, estimateById } = useProjectEstimates(); + + const isEstimateEnabled = (analyticsOption: string) => { + if (analyticsOption === "estimate") { + if ( + projectId && + currentActiveEstimateId && + areEstimateEnabledByProjectId(projectId.toString()) && + estimateById(currentActiveEstimateId)?.type === EEstimateSystem.POINTS + ) { + return true; + } else { + return false; + } + } + + return true; + }; + + return ( + + + {options.find((v) => v.value === value)?.label ?? "Add Metric"} + + } + onChange={onChange} + maxHeight="lg" + > + {options.map((item) => { + if (hiddenOptions?.includes(item.value)) return null; + return ( + isEstimateEnabled(item.value) && ( + + {item.label} + + ) + ); + })} + + ); +}); diff --git a/web/core/components/analytics-v2/total-insights.tsx b/web/core/components/analytics-v2/total-insights.tsx new file mode 100644 index 000000000..ac8914e11 --- /dev/null +++ b/web/core/components/analytics-v2/total-insights.tsx @@ -0,0 +1,58 @@ +// plane package imports +import { observer } from "mobx-react-lite"; +import { useParams } from "next/navigation"; +import useSWR from "swr"; +import { insightsFields } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; +import { IAnalyticsResponseV2, TAnalyticsTabsV2Base } from "@plane/types"; +//hooks +import { cn } from "@/helpers/common.helper"; +import { useAnalyticsV2 } from "@/hooks/store/use-analytics-v2"; +//services +import { AnalyticsV2Service } from "@/services/analytics-v2.service"; +// plane web components +import InsightCard from "./insight-card"; + +const analyticsV2Service = new AnalyticsV2Service(); + +const TotalInsights: React.FC<{ analyticsType: TAnalyticsTabsV2Base; peekView?: boolean }> = observer( + ({ analyticsType, peekView }) => { + const params = useParams(); + const workspaceSlug = params.workspaceSlug as string; + const { t } = useTranslation(); + const { selectedDuration, selectedProjects, selectedDurationLabel } = useAnalyticsV2(); + + const { data: totalInsightsData, isLoading } = useSWR( + `total-insights-${analyticsType}-${selectedDuration}-${selectedProjects}`, + () => + analyticsV2Service.getAdvanceAnalytics(workspaceSlug, analyticsType, { + date_filter: selectedDuration, + ...(selectedProjects?.length > 0 ? { project_ids: selectedProjects.join(",") } : {}), + }) + ); + return ( +
+ {insightsFields[analyticsType]?.map((item: string) => ( + + ))} +
+ ); + } +); + +export default TotalInsights; diff --git a/web/core/components/analytics-v2/trend-piece.tsx b/web/core/components/analytics-v2/trend-piece.tsx new file mode 100644 index 000000000..23daa89be --- /dev/null +++ b/web/core/components/analytics-v2/trend-piece.tsx @@ -0,0 +1,47 @@ +// plane package imports +import React from "react"; +import { TrendingDown, TrendingUp } from "lucide-react"; +import { cn } from "@plane/utils"; +// plane web components + +type Props = { + percentage: number; + className?: string; + size?: "xs" | "sm" | "md" | "lg"; +}; + +const sizeConfig = { + xs: { + text: "text-xs", + icon: "w-3 h-3", + }, + sm: { + text: "text-sm", + icon: "w-4 h-4", + }, + md: { + text: "text-base", + icon: "w-5 h-5", + }, + lg: { + text: "text-lg", + icon: "w-6 h-6", + }, +} as const; + +const TrendPiece = (props: Props) => { + const { percentage, className, size = "sm" } = props; + const isPositive = percentage > 0; + const config = sizeConfig[size]; + + return ( +
+ {isPositive ? : } + {Math.round(Math.abs(percentage))}% +
+ ); +}; + +export default TrendPiece; diff --git a/web/core/components/analytics-v2/work-items/created-vs-resolved.tsx b/web/core/components/analytics-v2/work-items/created-vs-resolved.tsx new file mode 100644 index 000000000..bd4673f46 --- /dev/null +++ b/web/core/components/analytics-v2/work-items/created-vs-resolved.tsx @@ -0,0 +1,119 @@ +import { useMemo } from "react"; +import { observer } from "mobx-react"; +import { useParams } from "next/navigation"; +import useSWR from "swr"; +// plane package imports +import { useTranslation } from "@plane/i18n"; +import { AreaChart } from "@plane/propel/charts/area-chart"; +import { IChartResponseV2, TChartData } from "@plane/types"; +import { renderFormattedDate } from "@plane/utils"; +// hooks +import { useAnalyticsV2 } from "@/hooks/store/use-analytics-v2"; +// services +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; +import { AnalyticsV2Service } from "@/services/analytics-v2.service"; +// plane web components +import AnalyticsSectionWrapper from "../analytics-section-wrapper"; +import AnalyticsV2EmptyState from "../empty-state"; +import { ChartLoader } from "../loaders"; + +const analyticsV2Service = new AnalyticsV2Service(); +const CreatedVsResolved = observer(() => { + const { selectedDuration, selectedDurationLabel, selectedProjects } = useAnalyticsV2(); + const params = useParams(); + const { t } = useTranslation(); + const workspaceSlug = params.workspaceSlug as string; + const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/analytics-v2/empty-chart-area" }); + const { data: createdVsResolvedData, isLoading: isCreatedVsResolvedLoading } = useSWR( + `created-vs-resolved-${workspaceSlug}-${selectedDuration}-${selectedProjects}`, + () => + analyticsV2Service.getAdvanceAnalyticsCharts(workspaceSlug, "work-items", { + date_filter: selectedDuration, + ...(selectedProjects?.length > 0 && { project_ids: selectedProjects?.join(",") }), + }) + ); + const parsedData: TChartData[] = useMemo(() => { + if (!createdVsResolvedData?.data) return []; + return createdVsResolvedData.data.map((datum) => ({ + ...datum, + [datum.key]: datum.count, + name: renderFormattedDate(datum.key) ?? datum.key, + })); + }, [createdVsResolvedData]); + + const areas = useMemo( + () => [ + { + key: "completed_issues", + label: "Resolved", + fill: "#19803833", + fillOpacity: 1, + stackId: "bar-one", + showDot: false, + smoothCurves: true, + strokeColor: "#198038", + strokeOpacity: 1, + }, + { + key: "created_issues", + label: "Created", + fill: "#1192E833", + fillOpacity: 1, + stackId: "bar-one", + showDot: false, + smoothCurves: true, + strokeColor: "#1192E8", + strokeOpacity: 1, + }, + ], + [] + ); + + return ( + + {isCreatedVsResolvedLoading ? ( + + ) : parsedData && parsedData.length > 0 ? ( + + ) : ( + + )} + + ); +}); + +export default CreatedVsResolved; diff --git a/web/core/components/analytics-v2/work-items/customized-insights.tsx b/web/core/components/analytics-v2/work-items/customized-insights.tsx new file mode 100644 index 000000000..86fea0c83 --- /dev/null +++ b/web/core/components/analytics-v2/work-items/customized-insights.tsx @@ -0,0 +1,53 @@ +import { observer } from "mobx-react"; +import { useParams } from "next/navigation"; +import { useForm } from "react-hook-form"; +// plane package imports +import { ChartXAxisProperty, ChartYAxisMetric } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; +import { IAnalyticsV2Params } from "@plane/types"; +import { cn } from "@plane/utils"; +// plane web components +import AnalyticsSectionWrapper from "../analytics-section-wrapper"; +import { AnalyticsV2SelectParams } from "../select/analytics-params"; +import PriorityChart from "./priority-chart"; + +const defaultValues: IAnalyticsV2Params = { + x_axis: ChartXAxisProperty.PRIORITY, + y_axis: ChartYAxisMetric.WORK_ITEM_COUNT, +}; + +const CustomizedInsights = observer(({ peekView }: { peekView?: boolean }) => { + const { t } = useTranslation(); + const { workspaceSlug } = useParams(); + const { control, watch, setValue } = useForm({ + defaultValues: { + ...defaultValues, + }, + }); + + const params = { + x_axis: watch("x_axis"), + y_axis: watch("y_axis"), + group_by: watch("group_by"), + }; + + return ( + + } + > + + + ); +}); + +export default CustomizedInsights; diff --git a/web/core/components/analytics-v2/work-items/index.ts b/web/core/components/analytics-v2/work-items/index.ts new file mode 100644 index 000000000..1efe34c51 --- /dev/null +++ b/web/core/components/analytics-v2/work-items/index.ts @@ -0,0 +1 @@ +export * from "./root"; diff --git a/web/core/components/analytics-v2/work-items/modal/content.tsx b/web/core/components/analytics-v2/work-items/modal/content.tsx new file mode 100644 index 000000000..85004d9af --- /dev/null +++ b/web/core/components/analytics-v2/work-items/modal/content.tsx @@ -0,0 +1,48 @@ +import React, { useEffect, useState } from "react"; +import { observer } from "mobx-react"; +import { Tab } from "@headlessui/react"; +// plane package imports +import { IProject } from "@plane/types"; +import { Spinner } from "@plane/ui"; +// hooks +import { useAnalyticsV2 } from "@/hooks/store"; +// plane web components +import TotalInsights from "../../total-insights"; +import CreatedVsResolved from "../created-vs-resolved"; +import CustomizedInsights from "../customized-insights"; +import WorkItemsInsightTable from "../workitems-insight-table"; + +type Props = { + fullScreen: boolean; + projectDetails: IProject | undefined; +}; + +export const WorkItemsModalMainContent: React.FC = observer((props) => { + const { projectDetails, fullScreen } = props; + const { updateSelectedProjects } = useAnalyticsV2(); + const [isProjectConfigured, setIsProjectConfigured] = useState(false); + + useEffect(() => { + if (!projectDetails?.id) return; + updateSelectedProjects([projectDetails?.id ?? ""]); + setIsProjectConfigured(true); + }, [projectDetails?.id, updateSelectedProjects]); + + if (!isProjectConfigured) + return ( +
+ +
+ ); + + return ( + +
+ + + + +
+
+ ); +}); diff --git a/web/core/components/analytics-v2/work-items/modal/header.tsx b/web/core/components/analytics-v2/work-items/modal/header.tsx new file mode 100644 index 000000000..f4bcdee38 --- /dev/null +++ b/web/core/components/analytics-v2/work-items/modal/header.tsx @@ -0,0 +1,37 @@ +import { observer } from "mobx-react"; + +// icons +import { Expand, Shrink, X } from "lucide-react"; + +type Props = { + fullScreen: boolean; + handleClose: () => void; + setFullScreen: React.Dispatch>; + title: string; +}; + +export const WorkItemsModalHeader: React.FC = observer((props) => { + const { fullScreen, handleClose, setFullScreen, title } = props; + + return ( +
+

Analytics for {title}

+
+ + +
+
+ ); +}); diff --git a/web/core/components/analytics-v2/work-items/modal/index.tsx b/web/core/components/analytics-v2/work-items/modal/index.tsx new file mode 100644 index 000000000..1404f862c --- /dev/null +++ b/web/core/components/analytics-v2/work-items/modal/index.tsx @@ -0,0 +1,64 @@ +import React, { useState } from "react"; +import { observer } from "mobx-react"; +import { Dialog, Transition } from "@headlessui/react"; +// plane package imports +import { IProject } from "@plane/types"; +// plane web components +import { WorkItemsModalMainContent } from "./content"; +import { WorkItemsModalHeader } from "./header"; + +type Props = { + isOpen: boolean; + onClose: () => void; + projectDetails?: IProject | undefined; +}; + +export const WorkItemsModal: React.FC = observer((props) => { + const { isOpen, onClose, projectDetails } = props; + + const [fullScreen, setFullScreen] = useState(false); + + const handleClose = () => { + onClose(); + }; + + return ( + + + +
+ +
+
+ + +
+
+
+
+
+
+
+ ); +}); diff --git a/web/core/components/analytics-v2/work-items/priority-chart.tsx b/web/core/components/analytics-v2/work-items/priority-chart.tsx new file mode 100644 index 000000000..acf0b6cf9 --- /dev/null +++ b/web/core/components/analytics-v2/work-items/priority-chart.tsx @@ -0,0 +1,230 @@ +import { useMemo } from "react"; +import { ColumnDef, Row, Table } from "@tanstack/react-table"; +import { mkConfig, generateCsv, download } from "export-to-csv"; +import { observer } from "mobx-react"; +import { useParams } from "next/navigation"; +import { useTheme } from "next-themes"; +import useSWR from "swr"; +// plane package imports +import { Download } from "lucide-react"; +import { + ANALYTICS_V2_X_AXIS_VALUES, + ANALYTICS_V2_Y_AXIS_VALUES, + CHART_COLOR_PALETTES, + ChartXAxisDateGrouping, + ChartXAxisProperty, + ChartYAxisMetric, + EChartModels, +} from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; +import { BarChart } from "@plane/propel/charts/bar-chart"; +import { IChartResponseV2 } from "@plane/types"; +import { TBarItem, TChart, TChartData, TChartDatum } from "@plane/types/src/charts"; +// plane web components +import { Button } from "@plane/ui"; +import { generateExtendedColors, parseChartData } from "@/components/chart/utils"; +// hooks +import { useProjectState } from "@/hooks/store"; +import { useAnalyticsV2 } from "@/hooks/store/use-analytics-v2"; +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; +import { AnalyticsV2Service } from "@/services/analytics-v2.service"; +import AnalyticsV2EmptyState from "../empty-state"; +import { DataTable } from "../insight-table/data-table"; +import { ChartLoader } from "../loaders"; +import { generateBarColor } from "./utils"; + +interface Props { + x_axis: ChartXAxisProperty; + y_axis: ChartYAxisMetric; + group_by?: ChartXAxisProperty; + x_axis_date_grouping?: ChartXAxisDateGrouping; +} + +const analyticsV2Service = new AnalyticsV2Service(); +const PriorityChart = observer((props: Props) => { + const { x_axis, y_axis, group_by } = props; + const { t } = useTranslation(); + const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/analytics-v2/empty-chart-bar" }); + // store hooks + const { selectedDuration, selectedProjects } = useAnalyticsV2(); + const { workspaceStates } = useProjectState(); + const { resolvedTheme } = useTheme(); + // router + const params = useParams(); + const workspaceSlug = params.workspaceSlug as string; + + const { data: priorityChartData, isLoading: priorityChartLoading } = useSWR( + `customized-insights-chart-${workspaceSlug}-${selectedDuration}-${selectedProjects}-${props.x_axis}-${props.y_axis}-${props.group_by}`, + () => + analyticsV2Service.getAdvanceAnalyticsCharts(workspaceSlug, "custom-work-items", { + date_filter: selectedDuration, + ...(selectedProjects?.length > 0 && { project_ids: selectedProjects?.join(",") }), + ...props, + }) + ); + const parsedData = useMemo( + () => + priorityChartData && parseChartData(priorityChartData, props.x_axis, props.group_by, props.x_axis_date_grouping), + [priorityChartData, props.x_axis, props.group_by, props.x_axis_date_grouping] + ); + const chart_model = props.group_by ? EChartModels.STACKED : EChartModels.BASIC; + + const bars: TBarItem[] = useMemo(() => { + if (!parsedData) return []; + let parsedBars: TBarItem[]; + const schemaKeys = Object.keys(parsedData.schema); + const baseColors = CHART_COLOR_PALETTES[0]?.[resolvedTheme === "dark" ? "dark" : "light"]; + const extendedColors = generateExtendedColors(baseColors ?? [], schemaKeys.length); + if (chart_model === EChartModels.BASIC) { + parsedBars = [ + { + key: "count", + label: "Count", + stackId: "bar-one", + fill: (payload) => generateBarColor(payload.key, { x_axis, y_axis, group_by }, baseColors, workspaceStates), + textClassName: "", + showPercentage: false, + showTopBorderRadius: () => true, + showBottomBorderRadius: () => true, + }, + ]; + } else if (chart_model === EChartModels.STACKED && parsedData.schema) { + const parsedExtremes: { + [key: string]: { + top: string | null; + bottom: string | null; + }; + } = {}; + parsedData.data.forEach((datum) => { + let top = null; + let bottom = null; + for (let i = 0; i < schemaKeys.length; i++) { + const key = schemaKeys[i]; + if (datum[key] === 0) continue; + if (!bottom) bottom = key; + top = key; + } + parsedExtremes[datum.key] = { top, bottom }; + }); + + parsedBars = schemaKeys.map((key, index) => ({ + key: key, + label: parsedData.schema[key], + stackId: "bar-one", + fill: extendedColors[index], + textClassName: "", + showPercentage: false, + showTopBorderRadius: (value, payload: TChartDatum) => parsedExtremes[payload.key].top === value, + showBottomBorderRadius: (value, payload: TChartDatum) => parsedExtremes[payload.key].bottom === value, + })); + } else { + parsedBars = []; + } + return parsedBars; + }, [chart_model, group_by, parsedData, resolvedTheme, workspaceStates, x_axis, y_axis]); + + const defaultColumns: ColumnDef[] = useMemo( + () => [ + { + accessorKey: "name", + header: () => "Name", + }, + { + accessorKey: "count", + header: () =>
Count
, + cell: ({ row }) =>
{row.original.count}
, + }, + ], + [] + ); + + const columns: ColumnDef[] = useMemo( + () => + parsedData + ? Object.keys(parsedData?.schema ?? {}).map((key) => ({ + accessorKey: key, + header: () =>
{parsedData.schema[key]}
, + cell: ({ row }) =>
{row.original[key]}
, + })) + : [], + [parsedData] + ); + + const csvConfig = mkConfig({ + fieldSeparator: ",", + filename: `${workspaceSlug}-analytics`, + decimalSeparator: ".", + useKeysAsHeaders: true, + }); + + const exportCSV = (rows: Row[]) => { + const rowData = rows.map((row) => ({ + name: row.original.name, + count: row.original.count, + })); + const csv = generateCsv(csvConfig)(rowData); + download(csvConfig)(csv); + }; + + const yAxisLabel = useMemo( + () => ANALYTICS_V2_Y_AXIS_VALUES.find((item) => item.value === props.y_axis)?.label ?? props.y_axis, + [props.y_axis] + ); + const xAxisLabel = useMemo( + () => ANALYTICS_V2_X_AXIS_VALUES.find((item) => item.value === props.x_axis)?.label ?? props.x_axis, + [props.x_axis] + ); + + return ( +
+ {priorityChartLoading ? ( + + ) : parsedData?.data && parsedData.data.length > 0 ? ( + <> + + ) => ( + + )} + /> + + ) : ( + + )} +
+ ); +}); + +export default PriorityChart; diff --git a/web/core/components/analytics-v2/work-items/root.tsx b/web/core/components/analytics-v2/work-items/root.tsx new file mode 100644 index 000000000..80e8aef62 --- /dev/null +++ b/web/core/components/analytics-v2/work-items/root.tsx @@ -0,0 +1,19 @@ +import React from "react"; +import AnalyticsWrapper from "../analytics-wrapper"; +import TotalInsights from "../total-insights"; +import CreatedVsResolved from "./created-vs-resolved"; +import CustomizedInsights from "./customized-insights"; +import WorkItemsInsightTable from "./workitems-insight-table"; + +const WorkItems: React.FC = () => ( + +
+ + + + +
+
+); + +export { WorkItems }; diff --git a/web/core/components/analytics-v2/work-items/utils.ts b/web/core/components/analytics-v2/work-items/utils.ts new file mode 100644 index 000000000..6e0b47a44 --- /dev/null +++ b/web/core/components/analytics-v2/work-items/utils.ts @@ -0,0 +1,47 @@ +// plane package imports +import { ChartXAxisProperty, ChartYAxisMetric } from "@plane/constants"; +import { IState } from "@plane/types"; + +interface ParamsProps { + x_axis: ChartXAxisProperty; + y_axis: ChartYAxisMetric; + group_by?: ChartXAxisProperty; +} + +export const generateBarColor = ( + value: string | null | undefined, + params: ParamsProps, + baseColors: string[], + workspaceStates?: IState[] +): string => { + if (!value) return baseColors[0]; + let color = baseColors[0]; + // Priority + if (params.x_axis === ChartXAxisProperty.PRIORITY) { + color = + value === "urgent" + ? "#ef4444" + : value === "high" + ? "#f97316" + : value === "medium" + ? "#eab308" + : value === "low" + ? "#22c55e" + : "#ced4da"; + } + + // State + if (params.x_axis === ChartXAxisProperty.STATES) { + if (workspaceStates && workspaceStates.length > 0) { + const state = workspaceStates.find((s) => s.id === value); + if (state) { + color = state.color; + } else { + const index = Math.abs(value.split("").reduce((acc, char) => acc + char.charCodeAt(0), 0)) % baseColors.length; + color = baseColors[index]; + } + } + } + + return color; +}; diff --git a/web/core/components/analytics-v2/work-items/workitems-insight-table.tsx b/web/core/components/analytics-v2/work-items/workitems-insight-table.tsx new file mode 100644 index 000000000..cd3e7ae4e --- /dev/null +++ b/web/core/components/analytics-v2/work-items/workitems-insight-table.tsx @@ -0,0 +1,102 @@ +import { useMemo } from "react"; +import { ColumnDef } from "@tanstack/react-table"; +import { observer } from "mobx-react"; +import { useParams } from "next/navigation"; +import useSWR from "swr"; +import { Briefcase } from "lucide-react"; +// plane package imports +import { useTranslation } from "@plane/i18n"; +import { WorkItemInsightColumns, AnalyticsTableDataMap } from "@plane/types"; +// plane web components +import { Logo } from "@/components/common/logo"; +// hooks +import { useAnalyticsV2 } from "@/hooks/store/use-analytics-v2"; +import { useProject } from "@/hooks/store/use-project"; +import { AnalyticsV2Service } from "@/services/analytics-v2.service"; +// plane web components +import { InsightTable } from "../insight-table"; + +const analyticsV2Service = new AnalyticsV2Service(); + +const WorkItemsInsightTable = observer(() => { + // router + const params = useParams(); + const workspaceSlug = params.workspaceSlug as string; + const { t } = useTranslation(); + // store hooks + const { getProjectById } = useProject(); + const { selectedDuration, selectedProjects } = useAnalyticsV2(); + const { data: workItemsData, isLoading } = useSWR( + `insights-table-work-items-${workspaceSlug}-${selectedDuration}-${selectedProjects}`, + () => + analyticsV2Service.getAdvanceAnalyticsStats(workspaceSlug, "work-items", { + date_filter: selectedDuration, + ...(selectedProjects?.length > 0 ? { project_ids: selectedProjects.join(",") } : {}), + }) + ); + // derived values + const columnsLabels: Record = { + backlog_work_items: t("workspace_projects.state.backlog"), + started_work_items: t("workspace_projects.state.started"), + un_started_work_items: t("workspace_projects.state.unstarted"), + completed_work_items: t("workspace_projects.state.completed"), + cancelled_work_items: t("workspace_projects.state.cancelled"), + project__name: t("common.project"), + }; + const columns = useMemo( + () => + [ + { + accessorKey: "project__name", + header: () =>
{columnsLabels["project__name"]}
, + cell: ({ row }) => { + const project = getProjectById(row.original.project_id); + return ( +
+ {project?.logo_props ? : } + {project?.name} +
+ ); + }, + }, + { + accessorKey: "backlog_work_items", + header: () =>
{columnsLabels["backlog_work_items"]}
, + cell: ({ row }) =>
{row.original.backlog_work_items}
, + }, + { + accessorKey: "started_work_items", + header: () =>
{columnsLabels["started_work_items"]}
, + cell: ({ row }) =>
{row.original.started_work_items}
, + }, + { + accessorKey: "un_started_work_items", + header: () =>
{columnsLabels["un_started_work_items"]}
, + cell: ({ row }) =>
{row.original.un_started_work_items}
, + }, + { + accessorKey: "completed_work_items", + header: () =>
{columnsLabels["completed_work_items"]}
, + cell: ({ row }) =>
{row.original.completed_work_items}
, + }, + { + accessorKey: "cancelled_work_items", + header: () =>
{columnsLabels["cancelled_work_items"]}
, + cell: ({ row }) =>
{row.original.cancelled_work_items}
, + }, + ] as ColumnDef[], + [getProjectById] + ); + + return ( + + analyticsType="work-items" + data={workItemsData} + isLoading={isLoading} + columns={columns} + columnsLabels={columnsLabels} + /> + ); +}); + +export default WorkItemsInsightTable; diff --git a/web/core/components/chart/utils.ts b/web/core/components/chart/utils.ts new file mode 100644 index 000000000..9e1d779bf --- /dev/null +++ b/web/core/components/chart/utils.ts @@ -0,0 +1,166 @@ +import { getWeekOfMonth, isValid } from "date-fns"; +import { CHART_X_AXIS_DATE_PROPERTIES, ChartXAxisDateGrouping, ChartXAxisProperty, TO_CAPITALIZE_PROPERTIES } from "@plane/constants"; +import { TChart, TChartDatum } from "@plane/types"; +import { capitalizeFirstLetter, hexToHsl, hslToHex, renderFormattedDate } from "@plane/utils"; +import { renderFormattedDateWithoutYear } from "@/helpers/date-time.helper"; + +const getDateGroupingName = (date: string, dateGrouping: ChartXAxisDateGrouping): string => { + if (!date || ["none", "null"].includes(date.toLowerCase())) return "None"; + + const formattedData = new Date(date); + const isValidDate = isValid(formattedData); + + if (!isValidDate) return date; + + const year = formattedData.getFullYear(); + const currentYear = new Date().getFullYear(); + + const isCurrentYear = year === currentYear; + + let parsedName: string | undefined; + + switch (dateGrouping) { + case ChartXAxisDateGrouping.DAY: + if (isCurrentYear) parsedName = renderFormattedDateWithoutYear(formattedData); + else parsedName = renderFormattedDate(formattedData); + break; + case ChartXAxisDateGrouping.WEEK: { + const month = renderFormattedDate(formattedData, "MMM"); + parsedName = `${month}, Week ${getWeekOfMonth(formattedData)}`; + break; + } + case ChartXAxisDateGrouping.MONTH: + if (isCurrentYear) parsedName = renderFormattedDate(formattedData, "MMM"); + else parsedName = renderFormattedDate(formattedData, "MMM, yyyy"); + break; + case ChartXAxisDateGrouping.YEAR: + parsedName = `${year}`; + break; + default: + parsedName = date; + } + + return parsedName ?? date; +}; + +export const parseChartData = ( + data: TChart | null | undefined, + xAxisProperty: ChartXAxisProperty | null | undefined, + groupByProperty: ChartXAxisProperty | null | undefined, + xAxisDateGrouping: ChartXAxisDateGrouping | null | undefined +): TChart => { + if (!data) { + return { + data: [], + schema: {}, + }; + } + const widgetData = structuredClone(data.data); + const schema = structuredClone(data.schema); + const allKeys = Object.keys(schema); + const updatedWidgetData: TChartDatum[] = widgetData.map((datum) => { + const keys = Object.keys(datum); + const missingKeys = allKeys.filter((key) => !keys.includes(key)); + const missingValues: Record = Object.fromEntries(missingKeys.map(key => [key, 0])); + + if (xAxisProperty) { + // capitalize first letter if xAxisProperty is in TO_CAPITALIZE_PROPERTIES and no groupByProperty is set + if (TO_CAPITALIZE_PROPERTIES.includes(xAxisProperty)) { + datum.name = capitalizeFirstLetter(datum.name); + } + + // parse timestamp to visual date if xAxisProperty is in WIDGET_X_AXIS_DATE_PROPERTIES + if (CHART_X_AXIS_DATE_PROPERTIES.includes(xAxisProperty)) { + datum.name = getDateGroupingName(datum.name, xAxisDateGrouping ?? ChartXAxisDateGrouping.DAY); + } + } + + return { + ...datum, + ...missingValues, + }; + }); + + // capitalize first letter if groupByProperty is in TO_CAPITALIZE_PROPERTIES + const updatedSchema = schema; + if (groupByProperty) { + if (TO_CAPITALIZE_PROPERTIES.includes(groupByProperty)) { + Object.keys(updatedSchema).forEach((key) => { + updatedSchema[key] = capitalizeFirstLetter(updatedSchema[key]); + }); + } + + if (CHART_X_AXIS_DATE_PROPERTIES.includes(groupByProperty)) { + Object.keys(updatedSchema).forEach((key) => { + updatedSchema[key] = getDateGroupingName(updatedSchema[key], xAxisDateGrouping ?? ChartXAxisDateGrouping.DAY); + }); + } + } + + return { + data: updatedWidgetData, + schema: updatedSchema, + }; +}; + +export const generateExtendedColors = (baseColorSet: string[], targetCount: number) => { + const colors = [...baseColorSet]; + const baseCount = baseColorSet.length; + + if (targetCount <= baseCount) { + return colors.slice(0, targetCount); + } + + // Convert base colors to HSL + const baseHSL = baseColorSet.map(hexToHsl); + + // Calculate average saturation and lightness from base colors + const avgSat = baseHSL.reduce((sum, hsl) => sum + hsl.s, 0) / baseHSL.length; + const avgLight = baseHSL.reduce((sum, hsl) => sum + hsl.l, 0) / baseHSL.length; + + // Sort base colors by hue for better distribution + const sortedBaseHSL = [...baseHSL].sort((a, b) => a.h - b.h); + + // Generate additional colors for each base color + const colorsNeeded = targetCount - baseCount; + const colorsPerBase = Math.ceil(colorsNeeded / baseCount); + + for (let i = 0; i < baseCount; i++) { + const baseColor = sortedBaseHSL[i]; + const nextBaseColor = sortedBaseHSL[(i + 1) % baseCount]; + + // Calculate hue distance to next base color + const hueDistance = (nextBaseColor.h - baseColor.h + 360) % 360; + const hueParts = colorsPerBase + 1; + + // Narrower ranges for more consistency + const satRange = [Math.max(40, avgSat - 5), Math.min(60, avgSat + 5)]; + const lightRange = [Math.max(40, avgLight - 5), Math.min(60, avgLight + 5)]; + + for (let j = 1; j <= colorsPerBase; j++) { + if (colors.length >= targetCount) break; + + // Create evenly spaced hue variations between base colors + const hueStep = (hueDistance / hueParts) * j; + const newHue = (baseColor.h + hueStep) % 360; + + // Keep saturation and lightness closer to base color + const newSat = baseColor.s * 0.8 + avgSat * 0.2; + const newLight = baseColor.l * 0.8 + avgLight * 0.2; + + // Ensure values stay within desired ranges + const finalSat = Math.max(satRange[0], Math.min(satRange[1], newSat)); + const finalLight = Math.max(lightRange[0], Math.min(lightRange[1], newLight)); + + colors.push( + hslToHex({ + h: newHue, + s: finalSat, + l: finalLight, + }) + ); + } + } + + return colors.slice(0, targetCount); +}; \ No newline at end of file diff --git a/web/core/components/empty-state/detailed-empty-state-root.tsx b/web/core/components/empty-state/detailed-empty-state-root.tsx index 4ae97e839..887a1eb42 100644 --- a/web/core/components/empty-state/detailed-empty-state-root.tsx +++ b/web/core/components/empty-state/detailed-empty-state-root.tsx @@ -85,9 +85,7 @@ export const DetailedEmptyState: React.FC = observer((props) => { {description &&

{description}

} - {assetPath && ( - {title} - )} + {assetPath && {title}} {hasButtons && (
diff --git a/web/core/components/issues/filters.tsx b/web/core/components/issues/filters.tsx index 346c960b7..a50afe50e 100644 --- a/web/core/components/issues/filters.tsx +++ b/web/core/components/issues/filters.tsx @@ -18,6 +18,7 @@ import { useLabel, useProjectState, useMember, useIssues } from "@/hooks/store"; // plane web types import { TProject } from "@/plane-web/types"; import { ProjectAnalyticsModal } from "../analytics"; +import { WorkItemsModal } from "../analytics-v2/work-items/modal"; type Props = { currentProjectDetails: TProject | undefined; @@ -97,7 +98,7 @@ const HeaderFilters = observer((props: Props) => { return ( <> - setAnalyticsModal(false)} projectDetails={currentProjectDetails ?? undefined} diff --git a/web/core/hooks/store/index.ts b/web/core/hooks/store/index.ts index 7f2d3b89d..6a1b91e5d 100644 --- a/web/core/hooks/store/index.ts +++ b/web/core/hooks/store/index.ts @@ -31,3 +31,4 @@ export * from "./use-workspace"; export * from "./user"; export * from "./use-transient"; export * from "./workspace-draft"; +export * from "./use-analytics-v2"; diff --git a/web/core/hooks/store/use-analytics-v2.ts b/web/core/hooks/store/use-analytics-v2.ts new file mode 100644 index 000000000..c8c13ba61 --- /dev/null +++ b/web/core/hooks/store/use-analytics-v2.ts @@ -0,0 +1,11 @@ +import { useContext } from "react"; +// mobx store +import { StoreContext } from "@/lib/store-context"; +// types +import { IAnalyticsStoreV2 } from "@/store/analytics-v2.store"; + +export const useAnalyticsV2 = (): IAnalyticsStoreV2 => { + const context = useContext(StoreContext); + if (context === undefined) throw new Error("useAnalyticsV2 must be used within StoreProvider"); + return context.analyticsV2; +}; diff --git a/web/core/services/analytics-v2.service.ts b/web/core/services/analytics-v2.service.ts new file mode 100644 index 000000000..87257cbc6 --- /dev/null +++ b/web/core/services/analytics-v2.service.ts @@ -0,0 +1,60 @@ +import { API_BASE_URL } from "@plane/constants"; +import { IAnalyticsResponseV2, TAnalyticsTabsV2Base, TAnalyticsGraphsV2Base } from "@plane/types"; +import { APIService } from "./api.service"; + +export class AnalyticsV2Service extends APIService { + constructor() { + super(API_BASE_URL); + } + + async getAdvanceAnalytics( + workspaceSlug: string, + tab: TAnalyticsTabsV2Base, + params?: Record + ): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/advance-analytics/`, { + params: { + tab, + ...params, + }, + }) + .then((res) => res?.data) + .catch((err) => { + throw err?.response?.data; + }); + } + + async getAdvanceAnalyticsStats( + workspaceSlug: string, + tab: Exclude, + params?: Record + ): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/advance-analytics-stats/`, { + params: { + type: tab, + ...params, + }, + }) + .then((res) => res?.data) + .catch((err) => { + throw err?.response?.data; + }); + } + + async getAdvanceAnalyticsCharts( + workspaceSlug: string, + tab: TAnalyticsGraphsV2Base, + params?: Record + ): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/advance-analytics-charts/`, { + params: { + type: tab, + ...params, + }, + }) + .then((res) => res?.data) + .catch((err) => { + throw err?.response?.data; + }); + } +} diff --git a/web/core/store/analytics-v2.store.ts b/web/core/store/analytics-v2.store.ts new file mode 100644 index 000000000..bf8f91a72 --- /dev/null +++ b/web/core/store/analytics-v2.store.ts @@ -0,0 +1,68 @@ +import { action, computed, makeObservable, observable, runInAction } from "mobx"; +import { ANALYTICS_V2_DURATION_FILTER_OPTIONS } from "@plane/constants"; +import { TAnalyticsTabsV2Base } from "@plane/types"; +import { CoreRootStore } from "./root.store"; + +type DurationType = (typeof ANALYTICS_V2_DURATION_FILTER_OPTIONS)[number]["value"]; + +export interface IAnalyticsStoreV2 { + //observables + currentTab: TAnalyticsTabsV2Base; + selectedProjects: string[]; + selectedDuration: DurationType; + + //computed + selectedDurationLabel: DurationType | null; + + //actions + updateSelectedProjects: (projects: string[]) => void; + updateSelectedDuration: (duration: DurationType) => void; +} + +export class AnalyticsStoreV2 implements IAnalyticsStoreV2 { + //observables + currentTab: TAnalyticsTabsV2Base = "overview"; + selectedProjects: DurationType[] = []; + selectedDuration: DurationType = "last_30_days"; + + constructor(_rootStore: CoreRootStore) { + makeObservable(this, { + // observables + currentTab: observable.ref, + selectedDuration: observable.ref, + selectedProjects: observable.ref, + // computed + selectedDurationLabel: computed, + // actions + updateSelectedProjects: action, + updateSelectedDuration: action, + }); + } + + get selectedDurationLabel() { + return ANALYTICS_V2_DURATION_FILTER_OPTIONS.find((item) => item.value === this.selectedDuration)?.name ?? null; + } + + updateSelectedProjects = (projects: string[]) => { + const initialState = this.selectedProjects; + try { + runInAction(() => { + this.selectedProjects = projects; + }); + } catch (error) { + console.error("Failed to update selected project"); + throw error; + } + }; + + updateSelectedDuration = (duration: DurationType) => { + try { + runInAction(() => { + this.selectedDuration = duration; + }); + } catch (error) { + console.error("Failed to update selected duration"); + throw error; + } + }; +} diff --git a/web/core/store/root.store.ts b/web/core/store/root.store.ts index d06ed2418..d2355de78 100644 --- a/web/core/store/root.store.ts +++ b/web/core/store/root.store.ts @@ -6,6 +6,7 @@ import { CommandPaletteStore, ICommandPaletteStore } from "@/plane-web/store/com import { RootStore } from "@/plane-web/store/root.store"; import { IStateStore, StateStore } from "@/plane-web/store/state.store"; // stores +import { IAnalyticsStoreV2, AnalyticsStoreV2 } from "./analytics-v2.store"; import { CycleStore, ICycleStore } from "./cycle.store"; import { CycleFilterStore, ICycleFilterStore } from "./cycle_filter.store"; import { DashboardStore, IDashboardStore } from "./dashboard.store"; @@ -49,6 +50,7 @@ export class CoreRootStore { state: IStateStore; label: ILabelStore; dashboard: IDashboardStore; + analyticsV2: IAnalyticsStoreV2; projectPages: IProjectPageStore; router: IRouterStore; commandPalette: ICommandPaletteStore; @@ -94,6 +96,7 @@ export class CoreRootStore { this.transient = new TransientStore(); this.stickyStore = new StickyStore(); this.editorAssetStore = new EditorAssetStore(); + this.analyticsV2 = new AnalyticsStoreV2(this); } resetOnSignOut() { diff --git a/web/package.json b/web/package.json index 868fa5742..b5e6b80eb 100644 --- a/web/package.json +++ b/web/package.json @@ -39,12 +39,14 @@ "@plane/utils": "*", "@popperjs/core": "^2.11.8", "@react-pdf/renderer": "^3.4.5", + "@tanstack/react-table": "^8.21.3", "axios": "^1.8.3", "clsx": "^2.0.0", "cmdk": "^1.0.0", "comlink": "^4.4.1", "date-fns": "^4.1.0", "dotenv": "^16.0.3", + "export-to-csv": "^1.4.0", "isomorphic-dompurify": "^2.12.0", "lodash": "^4.17.21", "lucide-react": "^0.469.0", @@ -91,4 +93,4 @@ "prettier": "^3.2.5", "typescript": "5.3.3" } -} +} \ No newline at end of file diff --git a/web/public/empty-state/analytics-v2/empty-chart-area-dark.webp b/web/public/empty-state/analytics-v2/empty-chart-area-dark.webp new file mode 100644 index 0000000000000000000000000000000000000000..509f66ccc559cb781f306cde52e6abad566a26bb GIT binary patch literal 2720 zcmV;R3Sae7Nk&GP3IG6CMM6+kP&il$0000G0002N006-N06|PpNR0yk01co-+qS7a z+UMto*tXHKZQHhO+qP}BoV0A)w(aB;uD;-;s`W(#1OaH{Hb|PNVzrE(Z$N-SkRcd9 zAwn}HjUQjpEClFZJbHBJ{I?&5GPTM;qKF?qX}oQ#xBU>G^NhiV!}q<>J>uZwtM^HI zH}Us6$bmBISL)TS0=Xk4`s0c2!~0zwXKfwH98iTV!QpI0AXoTuK0m%SO{1^wN#JSN z-4(J$tW>*Z=Icmc$6~a@t^trPLJI6%7BUOOj+LO(nN|=ma-G>c@v%!PN(S<0NQm%| zu&%@KPr6jhs{5OW2&CJ;VEqeQYAVVj3_^wn#katPg37@f0po`s5uukqf0bh$4I1Cy9w3jfN+sV0FSXMqe}v9U9x z46{|%E-i!J%w6&(at8qn`NOtNEWKmIAQt$D;+EA&67@QW@h1urcjNy;*mqcDtNuho z{P#W_^dCY05%eEH{}J>b!DsP3M1GUre~&IFLf?i&!YLjllM|jB9yovrH}WwF2~M)t z12>PhHocTiNL(zm*yAz<)8?{I;|hsqv9@}c;4-{@q0fVaBH~fmtzHitW8j#}$wkDC z^u4{V?@Ky&VE%zT0%CvHSw1h5Yxb$o@o0AOaJ22d%|4~vR=zYHQe$6U*&d4bb-FOr zA7<#7vL@=vfrk%YlQfSOEq1J!F`zOsnWZYwUnF)j zfAb352CG|4&Z|OSD4AQ#o5)c7OJt|`vB`i zPg6Nu)6<~Cc$9D!b{ErxPS7$;1k{1h+_49}@!;$LE*kb*Gu zv#KxJi_$Gk1gt6*0;HM%69Wfiv5`TBGCAPDT!)%4()GzfbUOko{e)2*32@TM1rA)K z!7oAR&v1h6`*N)R9F-*D{Q&@0P&gpu1^@uCD*&AVD#8H506r-ah(e*EArrX_<+|9i&I1b2n3B)u{7z4QP6A8@bc|JHxpdd~X>^f&#j4|DFU4MdPjX);H^=?Vz8 zCUT@axUy119(*Yfb#I9&)AT^YB!A^ibxj6i+WC!}`0Ou@tsZ-%OO`C-D)$DL^iyMw zIpi@Bu96|;`g}V-*Uxx}IdIZ{aUKJNUyT1H&E+J}Vk;st<&ju=|L3X!AHL3{t!ay; zr0wNMsojaQ7Ove28h(*5w%bj5imgyjFYa`Al@`n4fh-kFEKC#?m@whwrq=P(`X#XA zV?b>%Io~b@$jGAsHjKFgXzBQ;8*2Hy;xmi(^xOlywHYhe>IUH-qNg+Ux(LI`Ewby==d~amG)u~UIr`P0GOY5L7 z&pcFHZd=tM!cSfA`s)BYEvN%aYN_wrn-HTjO5t)%e38ivKAt&wSI|t}()ubc{$%k< zcQBRwdXEcK)90}``gD)!nb$b1Yw1tSqnI7O z3ocVbbb#p%G!QOkEWi{RYn&@1d(Y#CkN?-P>e&vtfWMYB0xktO^S>W-mBDno$7O;c zCu~3Cjigo);d!({Iar2TVxuwt_O`cs&$?QgMg({SPT9{JG~GxuCc@aJWbCY(O^8FOD}ZbJJq-GQ zm6+}S8lE5)P9XTb?>{dIwKO#IzCUAs92pb2IDagSmcC-U2uY!KCzrA{ z>p1UY;RWU-ma+&+=wE);z!Uv7m4uCEdXM1;!n}L~vEg^_A3eNf^6+-U3We1tEBrT^ zggrsM>g0;2+Trw$rzbjfCoB+U`@?=Fu5t;?Q?Wb0$ z0qM5va6!hszyGgFu-c(1vH!h_SNooyw!k%OeIM86bv`;_$o#w7Dm~3Mxe;{irPS=K z)OkN>waLBa`s+!52D>uv0iI$@3zw!|`r7bm|%q?ZIB7>I2VBljB-ScXvI zweW!FWT)0zEP75hR>`*)-sZix%i1~O&W7S>-ZJV4sMwY`k=DqLV+Xvb2j6^VGtrb` zs)n}(MJkbJ+32tUX>e`bc)JIa6&ztS>P*td2gTlGOaEOstcgM|{!SAiRztL1t$&@y zy6Ld*&K&0O8)sGobN(9FeiXZvH>+A**Y{)7ki5w@M1qh?f+Y?MT{2cPbg`!K?#4m4 zPcCn6sED>VJF_#sakt9k`CfwDl+yw}CS%(U(&q=7&?9574Xe!fQqm^PtW|4!Wtf;H zd5OpdF5gFyclFAV1>?e*^Z;$X@s`8TdnxZ^zA$J%X4i4;sSUxs4oIrvJzvqAniXBm zC>DOzSd6*-7Zc9D64q=JbbFv2s$-4@{ca4JNg@=L6ZQynuIyJkVY7Pc&)audgKjSc z(%%wPoOLH(n(g$_VAuL0ZQ`O_>cj6_c?l&#<&e0BlQal_e(Uk*`g5WMN>iT7sds@^ zyR(W4@_X9{|h41VRHGeO~@Y1+y;sHBCG+!i=` z=$-K4MR@fbrSlvXO$Mr{Tfr^8cTitQ@FO`9Sei>oEt8`SpUK8xCL0hJrG@@UM_i6$Jdh7 a#D6Sj!!p0?0ssIo9RQsHD!~B606vjOn@c66qamX8TVSvf z32AQKU{0v-uXcd_5a3+Lo}BX2T!1#<3li)dl#~M0>AnrR!2F`+pr$0I$aHz4`Ng{Z zJ{80XJX|YE0%&60H6bz8iJ^;h8zEDIK_6$D98N$(Rl2NIsza~vRkutkV!CQ_bsAmdy>=*SBfWL7VLFG20=+MJHjkF5;CDs$lY0iQsPm0%Q^&Q!;Y&jgf z3lb`J0kh9E0092|?`^C^AtKhfUF%eME_MH_?U~bf!8}cie^S#?*4RIrZ!P0+Byf&0*SFCKL9*e8{sQAUDjyyh=#-jGWEzBSI_KiH#_rHY$oJ zY>umu_dew0mfs#q!)c^_sxll8HI1744|deZ+Uz)25?Rv_1V$>nhyoGIkhvZN^47Qm zP5U*fJ5T3Bd4K5MboYk@B3SA6ghjuGGD#tJUGSjw1R_}To(&h{DuzinuDEg$wIprC zjkU}yNE$1#U&3)8d(8UMc-l!d^g0$~E_jn7wD4fB0qPaLxc~+Uhkr2Vefq)hxXuQa zsFgO7Haps=eT~YV&ZmN@?CN+bo_#`N$x5zZHsL&fov?}JQ~`Vgp0}c-i~WrWOgV`G cN#<|BMM6+kP&il$0000G0002N006-N06|PpNR0yk01co-+qS7a z+UMto*tXHKZQHhO+qP}BoV0A)w(aB;uD;-;s`W(#1OaH{Hb|PNVzrE(Z$N-SkRcd9 zAwn}HjUQjpEClFZJbHBJ{I?&5GPTM;qKF?qX}oQ#xBU>G^NhiV!}q<>J>uZwtM^HI zH}Us6$bmBISL)TS0=Xk4`s0c2!~0zwXKfwH98iTV!QpI0AXoTuK0m%SO{1^wN#JSN z-4(J$tW>*Z=Icmc$6~a@t^trPLJI6%7BUOOj+LO(nN|=ma-G>c@v%!PN(S<0NQm%| zu&%@KPr6jhs{5OW2&CJ;VEqeQYAVVj3_^wn#katPg37@f0po`s5uukqf0bh$4I1Cy9w3jfN+sV0FSXMqe}v9U9x z46{|%E-i!J%w6&(at8qn`NOtNEWKmIAQt$D;+EA&67@QW@h1urcjNy;*mqcDtNuho z{P#W_^dCY05%eEH{}J>b!DsP3M1GUre~&IFLf?i&!YLjllM|jB9yovrH}WwF2~M)t z12>PhHocTiNL(zm*yAz<)8?{I;|hsqv9@}c;4-{@q0fVaBH~fmtzHitW8j#}$wkDC z^u4{V?@Ky&VE%zT0%CvHSw1h5Yxb$o@o0AOaJ22d%|4~vR=zYHQe$6U*&d4bb-FOr zA7<#7vL@=vfrk%YlQfSOEq1J!F`zOsnWZYwUnF)j zfAb352CG|4&Z|OSD4AQ#o5)c7OJt|`vB`i zPg6Nu)6<~Cc$9D!b{ErxPS7$;1k{1h+_49}@!;$LE*kb*Gu zv#KxJi_$Gk1gt6*0;HM%69Wfiv5`TBGCAPDT!)%4()GzfbUOko{e)2*32@TM1rA)K z!7oAR&v1h6`*N)R9F-*D{Q&@0P&gnE1^@uCDgd1UD#8H506r-Yg+igB40JdQ0YX|^ zzaJ(<&=J6suzUk5`bX-2_b#)1tp2z1Nowz z{eP$L`#Lt9a+6piH2w-S5`Xy-QzN2r0ZFVg%YjTTcIjWb_7jN_#l4A;F42ywijQz6#ul8sX z@8e`1x#)r?CBL8O+|OKxmzOz++>i@xTY`GKG43w-J`c7;IWV|0V<8_nO$yf&|38&O zTV5FQ&bP#5-Me<}+qWCL!W}x4N;}Wy8lSTdchW z*`We6aU;zIA<|M6WQNzcjT`Tt6mq8%v|K;I(@mFV<&ruCC`ULs&wi@sz zM;Kw9KeiuWshu^`m3SY)ub=4aTSfZ^X7W$_V`Zy=eGJ)3+udNN{9no zBT&CJ@<@NmT5IJK`BHx(Sff{iFH-*j6HTR!QP@zZK-w_}_-1 zK@48m1Xe-F7x?ooAFtjN!bj4}zC@Az9I{nGmJ0ZR4;S`ALLMZR`0s>zKH5O2+|V&c zYWJV`uDIXhuB#(LK@-IMsyDLY{i!-cT+tujifX?`xTIY*d$d{XF=DrBxzIKbtAy*) zmTXkLN5?yU`qA&f9fNvSYimOFVQXM3ee74<*pQ-=yNN72eo5V_BjEljq!->iN-L4@ z*m`AoI3@3x`dK?xRhVxp2v&J3dl|UR16rrh?iD7gJw+o%J<&3*clQ{&^PwUW2XDnb zv1Y?|V0(q33LPK>kK2hMVH%7_VeQV?4W|QWftc7DE`+n8me);(638{;BeC%Lr#Mj- z7Qy3sPxtCBs0jtP;e>#dip%vD8X1X&?B5=8^Ge0(6M;MnvEh2eouIb$Clj?Q-d04W z3PWH$4Q)sDYJ_XN;K~q<3|GL}A@#EMSGd|oYoRmZn{GSWYY*Q4goS7Pe(7C?3J#tA z7U@%wnZ=xXy96Njr9@o<7#ywuXX@II>eUf@xf%BQwPvG-ESU~E&JVryV6K11F`e>W zWvlH2T-*K{R-33mvatVgI%i)naKQ}Vls|0gqyyvX;rN)?%8K;XZ)&q>qwy?HW)P5f zy8te)*Z&(JK)Tz1)s;)#ssjN3rg}?IsWA#;PJKClWCfMA)9M~YKfmg0#90{G;VV8N zR!|OV+G2GV_;2S*&lc&Jd_j6}PVq+S&;Zs0ThB3f!$b5VHAWByhgGmYGfm+qAs-s1 zZ~(jLg_N`P22c3*cFU6cCADr3*h(9&1>Bl+@PTCw<4j3PTeXq99!PbAsLQw4L6+_Z z-^)Zgw7m}gZz>>wXO!|!Pv))FWe?t87=e2QzpLoPpYs&D4WXO&b<0X_znpOUZ0iS< zE@eu21g7422^K9SoC?RO($dCO`8GYu6K|6In|j+-v|7?t5utw`Z!QTBE6i}tHo&r# z;mhnT#bYM literal 0 HcmV?d00001 diff --git a/web/public/empty-state/analytics-v2/empty-chart-bar-light.webp b/web/public/empty-state/analytics-v2/empty-chart-bar-light.webp new file mode 100644 index 0000000000000000000000000000000000000000..e6eeb09dbc25f79894682611568dca5b2bcb0035 GIT binary patch literal 512 zcmV+b0{{I|Nk&Ha0RRA3MM6+kP&gp$0RRBd8~~jGD!~B606vjGnMoz1rJ*Sp3s|rc z32AQKU*xHAr@)7Q0WklLlAmSpE=?!u4|6CE$86ll*~FlB%}UdR2;pP$WERiX+zP!b zYu921#r~*TpD3^slpS6q*Bfuoua)P6#2Gr}TjkTapRc$V@}TY2=b> z2QbTw539Kr;GtO1WV6UK0XFIkW>*I6_!XdrQK0ZkBM)wyI3ZUXF zTedAPT>t?7{#IhZHXfhr;xZ-jf}cw_%mp5a%D&2PeSa!lE?D?+AXJ;s$#7up6!zz@L*HB7%c&FhSp0E%2>gKM#s^0Ihda6 zZU$Cx+{lXWsdRazP~C$p@IRd+J7$=BtWt~p3MRyg;8dN12I=dgKuJERSr<9(7K^No zytht@G3(9<7_s}Ye{d5UF1$p5J%b$k(_@Q4zeg-R#xPra=*1#%s@C1C+Ki=`U;qI7 CC-ZUu literal 0 HcmV?d00001 diff --git a/web/public/empty-state/analytics-v2/empty-chart-radar-dark.webp b/web/public/empty-state/analytics-v2/empty-chart-radar-dark.webp new file mode 100644 index 0000000000000000000000000000000000000000..94e2f514b1572e08549a11dc058f3642dee6f9d2 GIT binary patch literal 3076 zcmV+f4Eys^Nk&He3jhFDMM6+kP&il$0000G0002N006-N06|PpNR0yk01co-+qS7a z+UMto*tXHKZQHhO+qP}BoV0A)w(aB;uD;-;s`W(#1OaH{Hb|PNVzrE(Z$N-SkRcd9 zAwn}HjUQjpEClFZJbHBJ{I?&5GPTM;qKF?qX}oQ#xBU>G^NhiV!}q<>J>uZwtM^HI zH}Us6$bmBISL)TS0=Xk4`s0c2!~0zwXKfwH98iTV!QpI0AXoTuK0m%SO{1^wN#JSN z-4(J$tW>*Z=Icmc$6~a@t^trPLJI6%7BUOOj+LO(nN|=ma-G>c@v%!PN(S<0NQm%| zu&%@KPr6jhs{5OW2&CJ;VEqeQYAVVj3_^wn#katPg37@f0po`s5uukqf0bh$4I1Cy9w3jfN+sV0FSXMqe}v9U9x z46{|%E-i!J%w6&(at8qn`NOtNEWKmIAQt$D;+EA&67@QW@h1urcjNy;*mqcDtNuho z{P#W_^dCY05%eEH{}J>b!DsP3M1GUre~&IFLf?i&!YLjllM|jB9yovrH}WwF2~M)t z12>PhHocTiNL(zm*yAz<)8?{I;|hsqv9@}c;4-{@q0fVaBH~fmtzHitW8j#}$wkDC z^u4{V?@Ky&VE%zT0%CvHSw1h5Yxb$o@o0AOaJ22d%|4~vR=zYHQe$6U*&d4bb-FOr zA7<#7vL@=vfrk%YlQfSOEq1J!F`zOsnWZYwUnF)j zfAb352CG|4&Z|OSD4AQ#o5)c7OJt|`vB`i zPg6Nu)6<~Cc$9D!b{ErxPS7$;1k{1h+_49}@!;$LE*kb*Gu zv#KxJi_$Gk1gt6*0;HM%69Wfiv5`TBGCAPDT!)%4()GzfbUOko{e)2*32@TM1rA)K z!7oAR&v1h6`*N)R9F-*D{Q&@0P&gn+2mk<(H2|FfD#8H506r-ah(e*EAro08SQr9? zw6|{7laQi9*{E@xj<2IzUM+lbPd!BPcAqRTaTug`rhGR z%Rj1rvHN%L^ZRG)LHgVrnDn^2#cwkqqF-${H#avoH#5V%sh`UT&&szh8U-$ZxiI~I z;^}tg2Hv)U)ASkf*kJ-He6^~CKfHUz(>9Z@O&%N_cQ3QK>q zsUjtDqSyxjY*ys)aCj-r^oS|pJ>u2pII$_}bAFQW2=8_cCbE|1kJxt4F{QLU4w$t^ z4O)Y6BD(XxkPgQyCA6#6fdL66RHYX5xBPhdvQkX_|JYch2DV#^<(=K%g97Zn~hZw^*(9` zlu!5||MOMC-6snPW~={8AZYyKfu~sJnkV&NU*{ib?yY}D&AGfD zh%i>b`NxOCMi!(GxuGWw{O=Ry|Hc^-AOGTQqRpi7)S3$AntT_J`l`+{_`iLUE6K|q z@-RbomL2f1n>2@qQ@Orxd97Qta|&uP4H#A4LQNn4$0j_34R@>a!Jo-sgl?y9>k_aJ z@L3Uq$|?gSCQ=qZ`uCgIk^_laifd&K+nsmPww0Nx1$tYa_bZ7*$N!D>Vv0|%5da6S zkm@J1aQzeV?Y6|s9sl1TSiu4*Jq+J}n}DAF@yxObOfO+76!0BCYu*5lP_&QySe=An zIz0XuKcZ*s(iKJgA~JH-ZTv2Dc^UvUjgD%4?o2*xP7V-R^-2|Juc?I6-aJw;s8^K{0g0R{br$ z2nzTCH7+^nWvOJGwXGQ%UZ|}z$@2$fi22b~?C22qEpqaQqXgg@bVNz_iCG)*B8^_c z&e<@VIuO03-bcw>0mb?bmLJBJ;priWS&$n9Hy3SY4<`-5b`LkC5vPU@ai`3)`gAiw z>r{hZxQeUJ)<1G4s`{a9#OCE$Zj;H&r*Ovrq6J0$59D1QpIi#W+yb}Vl9Ptm4dg>F4L1s0oCybnox>ZhyP9}9euM$uc*(Q+BM|7;KC3$S%)Jvrk8-&??@^9iCzc_CVzB9lBL z3B`8PC^kmMh_DafsentNwjP*%0f0!~b-JU6{6-LnW>@@1Jn9I5PtyEWE3dl0aVat9 zCAM_AX{{ysgJ|;zkNtdeKqw4b6Z79Y$-rGEg_id%(lBvubX{Ol?BEhS$(zwh+tqm$ zVVy|*SYEqcA|*Ge^55$e3rB|8b`I_6;gtP?qRg`NCMaNP#3a4JJ*JXaz<&>k-BkgBKD5vWm0Q64 z#AjhiqdD&PmpyxHc9Cf@%pY$PFgX)0DX*-DiSuy z2mixREN62utUugz_v_}#EMBO21k~Z736%7+)PoNII!)-An zeEgA_@SEUe>tkcC1wgMz40_;B00A35HE^9$_-*hz0CsHWAuwxwG}ak+YVFdw%w3^x z*SRYFck#C>D5FCFEu;r*aeR2y6=-I4e$>>eb!!=7UHk3g>!(Ukm8!knudgi_z%@wM z$d_LnCuh}r2Bk*t5z>3s&|uhz2q^Z^>GsCZ z4x7e5eAW|Bq0`Z!esCb1HWp_kiB|}ppF1H%bnp}F&JqMeE%C!n!?@j9JMc7DH~(lgVVMUC9`FB%!@kj!O9jJ|{L~KA z7e#<-k@&}N_a7p$bqY&R&7n8UI_(W@<+kr+r(Cf(2%F*o1ZipgoJDCXk8^q@l70}x S0s?R4hyV_t`JfIk$N&I#U-Q5K literal 0 HcmV?d00001 diff --git a/web/public/empty-state/analytics-v2/empty-chart-radar-light.webp b/web/public/empty-state/analytics-v2/empty-chart-radar-light.webp new file mode 100644 index 0000000000000000000000000000000000000000..29820169bbc372b4ae0b61a43efc5a53cc41d594 GIT binary patch literal 716 zcmV;-0yF(mNk&G*0ssJ4MM6+kP&gpC0ssK;CjgxRD!~B606vjKno1?4C84Fb3_!3F z32AQKU{0vJjzWQJ?wR(`JP@-ppj^rWvGkVc{pk%d_kOab@vhON)!=FW{eUYH&6S1> zQ^>=-$5wy1=-vBLX&d*I8-NVIX0wI4p8Lp&!;(^JM;scebiV{+%G>(4dIt>;Sv*B1 z^hwYcCIPB?R;5hqo5u9T;6cKuU{q9q+wxp8zcNO_B;3k=@&nHsPCz8BnF{8u zYL~v75WK;E{pRCW-#cL%d2=$$hVV}|)lcf@Mj(koq=~AY%`*3Xvh#od{`kvqWr3wt z@Gy?xLXgsraqautwkMuR0|1;L6G^7`k?WMakkO8ANJ4CSL}PIP5oq}pH=35P!D%PB zf=3lU`bLt6T1TWrc3_o7Pydi<8&iRb2HQt{S66%r#iMNVPAQ3q2Clb;JpL@%f88~n z221$JGWJsgusv*h9uhu0BI%aLxyh)%mc#K0X4K_~jUX^ed8PhZKJA{#7Dz|C%)JMd z>Yvd-3i|s|)-5tygSRc8iWQAmWfyj0+~L=;YwBu1%)Xp21!st8RnSZ|Z%yB)#ln5j z4#Erm`-h`_i7nCDQ*AG#=hfc?A|0e$Q%Jf3Y5WseTN$CtR`u~SkqJ3S)ruZ|Zq19y zik>ZjIT3nMjJv91Q}$~%Ib?XXMq#p6l{?^2#>%fdsnV$|%>W&@XGTQmZ>Si7p&0=1 y9S7h=J!qCXnB&N1>!OShAQ8~*>mrQ`22aiMwOo=0>9Yrx5)iL_W&-Rm0001fdtw~` literal 0 HcmV?d00001 diff --git a/web/public/empty-state/analytics-v2/empty-grid-background-dark.webp b/web/public/empty-state/analytics-v2/empty-grid-background-dark.webp new file mode 100644 index 0000000000000000000000000000000000000000..5e420f2852caa36e45aa85094c4f569037a769e9 GIT binary patch literal 35070 zcmV)BK*PUMNk&HYhyVarMM6+kP&il$0000G0002B0RXiD06|PpNaxNW2;!?VYHU*LvqyK75{ zw)-A)?O3~NSE8yYGDT7l5L-c#IiTYEnzh>;yUl)Yo0ZPdPUhSJbEeJN?p82?C`vMr zp`dc!agO^q_8j9G^O!)zZrC=E%@_>r9v(ni7L)X!c+=$HfByaFXBY%j z#Q0xIbrk9$1Yq8@04V-%!~$Vg0~ZWMD{>sF7fE8nq!$?=^%bFDEE+=DDHYeNnuh+*oB;(~4nqs)(AYvi zIO=u11zk>6!SCR8e7}ypr<3#*^bHB9Tu!IcM@JwpVx#;9S(VZsz*5t~=yhUo&c{Qz zc`fH@4N+>GRv~zusuYEI$oAo#Dj7@0Qu7^z(8E=!1R?DYLA?Cod*I<*>)0H=WAArdJ^;vJS@AZ+{c z8ztg2pggcKVF44$NUEMNm0%IKww-Yc*IqL{@5@DSmFlq5(~`MiR(Dq#P930Yny z){}RbsGG)7|IXyNZ@`wV7RLIcf$L4X&nWiZU}Hje?XfU6tX90u#5$_doEb%!1ooQ+ zQt?n?03_GxVg>rnHY*mmLIrhvB@MN6%blnXQ9gkL0MTO%C9499`K0@EVhIx}U$rh; zvABwxh&p62(rtwA`e#+dYSEN11IpKALW?EAB?1`_;!cRJrc&AHyEGPvP-r^7k`~HN zC6+6>yrF*(pVZOU5V6! z5GhAh0go|lm{@ZW1VD)wfU}~Iw^gN~3lgIA;%kpub-#unJ?4K49MhAOq3oPiSR*NB zT+>2egK-d>*I7ohoLL-EKrd&aFqfThDytrXquKXJB<)!G59HnmV8?~=Ld@!o!@LQj zg4qgVrLCdf2$UWG=7EYSZ>~{*As=J}r-P9ZiHT(%8DJJcjzIH9u-K6XV|m#)u=G9a zc{03YT-qk%3@`1Md2gkQP&&jkDjlRwdju$HIRs4S7=XqqXfL2VqpOGmQw>2{8Y7eB z76Qx)WbF{=w}HrC?+9S`eyd8BPXhLa(uN8%+w!0Q91d)sHe$xaGR18YptM~bG`l;i zqHKp~Zv=rL0vzcQ(@8`r8pYHwpdR3`(Rlp4B{Q4&y*N+ZD zjcp7neopki5Q^7ioS*IkqVGytff-;Kt1*`d1meNcNEDmSjm}GBBugBzi0S-vU$yyk zU$ls#c_byJgGnP6C64HF-V`kaxSim{TrmG{z;Y5QpVg7MfQ45A2uTbkNfbgb>E>1VNIo2hQCiju=#2rO z<&2C27AC?tOw39>n$a1rIlU)IP&EHWOW;=3cY7E07$RDerXDapCeJ}G4nm)f#V56Y_NW_4ZSm> z5T(TXUBvR$ii&8)X>KpDEFoxEmRbu&y&G9$_DbF*oRG-?Ww*RP^FShRQ_s4$2~7Do z8D8EBpfWG(H$t|xXe(1YO-q6=tvAxrOHA7i-hEr9f@psO38Vy;myWak0Tmt?USd%4 z5463jy(*5QjmQarw1k>~3YvW^jB zD3TYCNyx7VHDU82pP&)FWZgHKqiUlDHsXfAIP0%STRuBO$!10Q;rN1VO!~aVt!j` z$UYNu*%;#v6KfSQx6hzbeW?lsbAuy`XV;Lat3@xmar;7(epWxfuUSIzUl}F&QJH~TBEWF zvawo8x^x;_D2`Z?R2rn+*Z_8YC5`1PXqZ)Nw30?tA!s7XRl01UndFfWVM%SkZY2#s$K3%=CwQ-pzpN{2{( zNsIHQ)2r57Q;nIpDX?UuG%mz7SI4Y6q6-B_O!%1%HD6M{eJvIsuR zvV>BZV`=T~R`Gs!R~-y-IRTW{Sa+%s*=7k?I=_m}$`2@Fb?~(Eoc8_5Uh(}ixSgP) z2t?OUL-b(^OIj0(Q!cU5UB5vVXbPmUYeCU!=S ztRV7sZYywP05U4veE_}Vz!U`J^}=E6QVeD_brwlj{mae<%@!vB1i<`T=jE1WX!2Y@ zsh6~;Kv@gjaSy_oVgo@5xNZ(Bj*-DR{~O@AAsC=r_8}aVLNzzs9Wk~dg@_d9_7Dk1 z;vvpOaYzkjq~g-^T@Xn>0kPmA9S4Y3Yb2AcPN8Hl7)IpJ1M?EJ+SJSY%ZKv*)U({UyD2UZUN()J}y_5q<{}OW}2&I}R(Irr<6Rni!R}rz!a+hC_m!=sV7VbQ}YQy66Qz` z*qzf*ighTPHp0@m2v1zf;zw$ijKWizb9v*%K_I+Fp1d`{yvWLrmAmeQ0^k);7m|?!* zINbGTCda=8eCm4^#zoIW{ZbR_A2#5ozqB!h4_;$oJnli9eXEJ}|E<7{pS3W?UkrR~ zm&I}6ahS0;6(2r?J8!TsCQgB_HL=!?V1Cz9c9j|}&e;0>>AuC&3LM@YA_TZN0tD9! zRRUs36RD_Cr@uOZB8QfmZ)ku&-LoobT$Nv_LeVP!U(6^6>Ev)$Hd&Ok&6TvJ#`=$$ ztFqZD1Mcc^H5HuKKP&H_b_yjUy&_aX>~C?EvEowGI5iT==2V=~<|tp2>B4augn|*Z zJN>@pODsz)-_VanYdoDN%DN$Y-xsLWB+$c2R#8Bn=YwRANo z7JeutJW2O}?V-CCZnkpDLHjtoSY(SyT`;t0q@}ByC>Xs)&}kJ~beEle{w@z$vIDl+z736IQkgEOV4F>W*Isev^EdV)Q5u0tF-D zVxQNcDT2@(*deSNX*dF#ad+%Qb-#om;ou0vaZgC78W>g!_M#LaVfpOMSn-O$6blUR zp2s6D3dSbGP$2)!pdiKqkNr%8fV3R~UN+ApF=#LXhQ3<*S@P4rB0xyTEjVwfC8Qoc z_f?F1P}XtjZk+K#Nip4-+y8dp6>FoEdiv^h7<*w zyKvmP54~d)lLHI=yM&=C;aJ4)p#TXaNnkp42ns}uUYan2k#?ntQtk zc}vk9U10uP^OcZ#cAI$sGxSnwQqOKv&ngQdrGVf)#I(&Il5qtu?FW;7$Ox&JDJ{~k zQ3A3(|8ZhpopV4dMxc8nrE?TKj$z-{KxE$734zxpOhSWs!b;3u-9TsUxdFG`j0Plt z;QG_aZ!Z|q3c-0Rx%US^N&@0>cm9oyDSYln z=0@G`VeUO9)^o;j>o+Wn(Wi=E+G}y#|0v9wSU7(en}1{eKG(^_FHNk&$25Pp$uUXI z%`F`t3d>TP2&zmCVV!L&&k!Mb0!CK4{XH%x8 zkAo;zPch1}gub8=%2;I+RGQP?X3>99Nffy>3q_}hLAv+QOGft|deH#D>2zYQOJyu# z3MFc_NzjsU2Z0vrk<00#8`0Ta0HIqWf+fdo-qIzU*FVWpak0hqPc80!ykaRk*P~)} zpiMVoKfB4!OT9=EuPIOjCorBgJjlzx_1Viq~zEG9tCgo*Ql2sj%Crvt`P0-X&&c3#{Ch*nnS za4of=1*We-a8Cqrftetzz>ET#1x86QLz$WdSAxjNsu0FEp&cFx@8-QU}9hx7&kct;8+VAcW+~LiZUVrznjCv z5g7x7`CEXahH@xj+kWuKj6s0zsX4a7;+WeFtp=bBYibT$1{qw_Kou-3NI8t8j45H5&O4)2re@yoyB*dN6Oh7>~|nbnC8j0r*mv*Y{VBeB4dfJ9xt8zc9Q4MRHa zHeq67fgwX_8UW&ucLb)4KxER>)J5se?2ZJJS=%D2klhQ9Sd@Scb<|ps41oD731sVY zl6Jy~1uyMS@ImCyGedPBVqb_C@jSBi z*fK})ky2`84cCSzKp&O$mh^zbF#W?o>>m+^`k zfw49*Ln-JAaQ^l}KuBiDbs7$qGzo#QF&R*zMs!KqX4Pb0&jMIv5&&1wp{$8m|8_nOSddak;9`n*`|6Hn{`>LWI5l(CXn8Q zNPV5tEsBYy806+uQNvhHf4A4~OITp>?gd!Zz$ro?eGWnAv?9E_oO0M z;a*a>IOjts8_OMobb*3yupmkuHlzR>6M}qiLK<{qxjMS8o(PY`3-_Y2V1&Az#6*dj z>O=LEH7iznvD=*y`4U7eE^}%`I5|j^ol=MkHdvCis6FQcmaWZ6k@pNPcAKC<|D4D+ zQM!z9pVkzea^s)HEK;zD(Q);t)vR}vHrc8iiWp^Tcl9hPV}!G^1(!yl+^}Q>2kHAH zywyR%!$r1`l+KC3E}BJT1bPgt$k4RgrLnac!-5yD15O=FC=!@!@5b=M8JLrNZU!fw zmq4I--j+{-ujw^}Y5&O#tNt!nm#Bxp$GjKJLtYDn510ZR2ORmKgdtBDpS{_#BZ2B) zQ&VH>ffGMm((qL~EUe>z;SX4tS_z)cjy)vU%@MiV_6RI=nL9}eD4SBgn->So~7Kq@Jh2lwksJDjgPfMS$_}Ed?H`M!4Y6akTR~(9<v-b!FA8tCR<3t{z0z?Ou7%^e%a zeS5(yUXHE;Hv>clvUZ5`Tdd4*Bd~kFrO(6)XtQbSdYav4mhMJy$F!B170aZ%VQIQP zaC(O&88;AHYlS5DgjS3pZX+S2<5m+h`;&+ht}p@Y0drM2T0v7Fv)XAARM%7v76K46 z1`g=$KvW&0;zECYL=XG!usEu-i7WOlFk~EOAB&k!#AtxLVgx_`vO%SzUIn~*wjn@r z{N34M%ahF@e=>@XzQ@9N)IUQHGqFB%I&QkyjUhqhgVjytVk%mTh1?Z|2GH$TGZnF%)PY3TUtE)zEMv(X z2Hio**|aE+4ie`2*{}PJ4k<*V1(R`yIUQnpXxY4gLmsXXml%S7pI$@Ru9<% zl0c`Rq$_5k`wTFav0YIN&OKBigt<9zO+t{4yKxSyPD}_=tZx7pO&V0;TQ^|*m4P8Y z-aU)>)r4ULmA99Uy3T(fY`rNTOhvH9uiWOgo*`XLezaI~J zPX22bG2Z$v;1VW8b%XN7+cELv1Tp3Kdc?8EG>9qLuDhUP8{!OXM$n+Vt_VN`O&S3R zvPfc*Qwu{>1?J?KSGxK#qppFW1U~Hm!4ZJD2ck+iERf+GBV!pR04uT~044}VU?1Xe zPLNtJVX(#x1A@`7FbpB{I0N=bA@Ruh8tehg!@pBqz?Cy*jk8b&LlRCX&J`}hhgptNfHv& zM-1WCpIca~9|e50mLw+k#~j6dAElMpmsGs+CpiRg;-i6gSr~Zg;n;5fj_NDP?_OhN z>#PeA-(lLbSK-djm>jQyzIbykDsaJlHUD170D0axZuzP*2x9bk)F*c#c?3`oJ4w?Y zqjfxS7&rgS!dUr0@Y~2B0(q}V%>FW`0C_U8bEkzhItlDZ2#99f(7;$)AiW;qKC3#k zft=c66;C(?xM500Ih}kM=D#l)AdgCmCiM#&Ev|GV1e9yvo(+fHKFDBLx*6Aj**v6k< z`X9s|eiCN>!^C{r5N`UZg|Yfv;M-=tPhE+bUznJu5ZiZJ7-N$Q9K~P%8t@kq+#FqF z7cl%!qgy0)u4*wVXVheyD`%Z^_%K8b^v8QqomK-8eUdp7)&`0lmj^p2hK{ksqGs;& z7p)z2nQNeKEi@^|*y$B^qPPIL6AJn`*pDU48MM3{V9^Ahxj9}wVXi~sRg!J-8|Pri4HR0i-N#F`(Qt+!^hFZ`<-{`?H0^mie21Vq@<4- zAO^j=q&QLJAX&!na(D}ZMTi!d&e|wrbzMwdr;Bi#mjmtcIY_sz)8B!)_sq*d%SlPU zV(cJBADG-G6wQexcoQ~gkLcq-2L{hRoBK}%8ltWu#8c}ZgFoR;b7m&nw7EGZE>ND?VN_)3*0KIey;)WZjwXJ zVuD#xAk^%lo4iKF*73@OSguy?wJfgy7JxdsJ~T(F1LR@&Bp zqmMGvSOF(oWF_`20*^J-SOu(joGCwnGcLC5G$LGV+A?6Z%>m#61~#;b=%rC9u~Bf{ zMLCGC+<1h93z5f*S9Nd2Ki$AxJ>>-T<6IC?~mXABT>$Fm#77vE1TlaycoB zBQ!Z}0Qzd#K3%ook=)QAyRWm%Qja1 zGeFb3=p6-&@a~Kt>p$XsAOK_BA7l)9xqYxC;7K3M^;a_P6#!)QkGv;C$hWr zzhiCzo%-*A_n)&kK5!m3Kg`7Z)d=2gZV5g1-OzC+)|b!Z4L0VT6<&X-h4GTV!2Cwj zuGoYh{U4KKGx6;2SQu}8F{Vy7v3|4;-+76JvE~Zku{T*9fBRVMy3oY@+62C4ZZK`S z1bCUuKexCEyWekOKE1-VH=0qMK-e2dt4Qs~ARR1Glrac57k!vuw^7qe*&?R?pR2YI zsSKeKhe}bfgjMEP1fT9b2`Bwn^l3tO#woWb9Tywq4cZ$sTTlx{E8U$OV9^GkLgWjI zzKpTV(YYoa6qB2pUPyKuW+xJv5UB`3XVm_OUQxVX3DElm z@GZ5XymzPK9IWzaRf;XRd>sCU-M_{8f+E;$6qG+mcYaiaRGG@RQnO@(-n0NA(4hzf zyNUB-5&zvEL|_oPL) zbDm&X(gAOZ=1mwoC|&mVZNtq-d5cSEkuYy{bv=jWb;`})m4|Q6cl-ea?Eb5|2JJHz z<;HA5A>T<+F%F{N8cux(y&MG5?G1EG)>jQcu-m1z3lZE<#W=cf2D@QdQXdE5!~*+V zxJB$PIEtFE<2mieV%fYL0z4orN6~-33V{_v^1NX(Y+?vG$^yrl2M6XHJ&+g21S07_ zj2wGILYelD6X(6#z{EI!j|j}gIu=;_S%A5GL&Vem6F^FaRUp1%P!K19$9*H$mOvn0 zwH?fDJ<OjOWgNm<;Or!5C0+&K9WyC}nkcB{pUfd3 zT(w^hdRC0eej)4D&p?-i7&DCc#aprb>9t};7QYmA!lo2L3pfG9X$fP>7Q||WGR{8k z>KPuuF%u&I!>cTgK;W>3AO|7Cu@*(B$jMVe&VZ)9H#5r$3NnuRICvq}<9G?-96PMgwKGAiOs2&OlAW>O5o@RkGho@}9A@Oo>&U2%aRLt8z;cvr9%@?*e#J8}$g^tn@i!*aL&v%G#}>xei-^zfusELe z08agx{Wl4h;|Jz1McwCPz*FWdjt`yBEsrxXuOGq3{@KEK-1|y@;qdEc;idX)+%?IsHH&}Tkc7m|;2VN?7Q_Szc zB}nYLk$++g@)x$ipf6&w|0yX)<>g>GZ095`Yt{n0V^QR8$~O+ULob*6JByRY*O zqUhG4+>~9%#XdI!cj5u>-3!Kkl!p_$YwGT!RObnycQs?D7#wuRFG2@Rf2*;B;$he2 z(f3{JZWU(>NeA5N_xo#~)6X(iE0k`FNp58J*Ckli$f@a|m2MY74OGL<6Gk@%%N(7i zpBV6uYF;i5czFY_JD|RR)WCfc@?~xcuKNicpb{{Xqjjq%Ck&@=6BB5jTR!Q3CMFTL6O$;9Vw0Anv^lMD7LPpON6D zjNs6|%mg4n=k7@ml#o#PR0ENd0ONNH21yVec`Ix%ejOGUfR(=jl%o)M!d2<^j+*C* z_(Zj6qa}%PB6}T`%Og#>5uqgU4Ynba_q+lSEq$7}INIk$}UL3CNw;&+%EDj5U!Nd>{Oa=j{ zWMh#FlGY1eY(%a*$N*`QmGWDr5N1le6ig_}9^v!AR%=;%2G=+OY**Yiy;20FIiq;L?WatBM$+-zn>%%mT>MS z&U}j2@zQZz^G%YO9q|I-uWA@dJ^KXg{s&rF{_`Px?^_^{+8KWUy&ffhjBdY38-IfNr<$m{8{nm&aWBEqlHwkmfG8s_s zu_(L00j%fE{U$(LO|F{Q|62=V9N@LvEsp0AbKf&D=YiWUvoKBv@Yy+wV>7t_lXg_V zH6ODuE(T@qjcp@#{(C_rUjAXrUIFMsCf06Xi-`pa-~OhB@#ob1f0$SYfZIQ6VXOi0 zmETz$e-F%l%EX)nZvL!=aWcSfnF-qk%>LNch}>pw6>lK0*;agr@xUNI5ulHv1qi#3 zxxbubi9+SYw68(45BcgEYp_r2*$1$)C0U>WLrbu9X?i)k7X=ynLz_c`5X%#NwB$QNoZvUhK=Q>;aqpFvL9pa)4T zEeT`UmKar*bW!MBTf45*{;$LXK8*${T_k}aAuu~e39cstn4kcvSZ4&YfekaHXXEPEaeu~-3Ty~q;pG=MLmCFOcx^-E3606g>+ zFhDK@t5;_x04u2@UJ1w|!Q*bs34}9&-XF>07zd7dk%6dX0G_lB2KOdl!2%5vWu4C&hJ|or?i{rx=;;su#%wLb<9p*1=UHn04qlxwH`{DX?O^&@4UuA9y zz4{+8KVo8CcLFYdn8~q^dfGQEj6Z)F4jgS_UA-RPe7?CubQSPubBE|{kH@ZaOw7xd z<1+J?%})7e;Av*UUUm|8{v}Kf;Adt64?lzWFB9v?Vb0x+gY>~xB!{F252`^E|9{`1 zgKE$^geg8AN~A~!5lytTNv5TxI<%NGphKy*L@nlWP#cm(R!%x(Tc>&O&X3Ab_nUgr zO?oY1l`S6JPMSK-x7cyv4?@g?6s86$T@?Y0F;uxX4209Fz=~C9yb);q#lbjwfPP;V z$0}g_9}?h@fR76g{Y=FGChahQ%ZQ8uTo0`OmO+7r>pB0OQxFpXuP`XUBY{m>`zZ-QtpzUtcz|l96!iiYdg~J46nu{72MZf> z!sr4=?h)uMPmoeiz)F)c2w}ZN6#_7EbQJ=OoZ?v10940WMu@8j3k<-f66Wv;AS?_W zmJtwX{|SXjqp=eO$jb4?5=PjRFyts$cAQBe;7G^>Lv?IIVQ@?wgJ|qE)WF6RA@dN{ znp6=OoiwRxXxyZV0D7^mVjUu>{w|Gjp_CBHC?%Tma@z8h+^erxptD&r4}yiILiokl zODyuFpyeyIV3P}>Qz{q;eR>q_hCjtm(lH%_X) zc>ycOK6Mz|HZyOq>=fYVkw7+4PgsfBUxLOYydMIOz&ndf#^pyMJY+Mh_IO-~SV6>qaqmTe9Vua;yjT?J+oFZ%o+TK<6BQ zW^)ep;r|XBfoAOAz*>iRg9-NNacup8g)#ne;IeHN$Mes{fxj>@zqA6E|7~GkPtUdY z^)BGHr`!8_Blz?`754Sw+oZHU++rbHTJ&V#n|&q6Z7&3{Exk__fNuG&4m5Y$=LT^m>k57SDE{z zX8>25SVsfI&VUbT9bsoiluQ2jav;gT?1ND97mYgxGzd@_t`EpmX2C%EIm8?Na zk=XYoq5l~gBn^eZRzdv^S_6LKQ7(rLGU69$uqwN2%{2fSaCa}Ro4gq8dv^Pa?N+FR z1ZbeI-nRhpbCxJOA1FF8+;NHS40eqL^!I^UC@L?d`g-esb*O_OeNB<*=5mVy_4kS0 z$1G>U`W$3CXs_eXhznB#e%hfvOoTzJ9d(K!K#OZ+2O7DXfnG}mxjB4c2!sCC--2t9 zf({TyLlq29;C+J$0&RS%i6P;|CCp|2YGMQ+{R@ZLFNOfR8U&G#!^Yph1OZ-kYXO6s zZAjo=+X0&1KNAU2_PpM{QUcE1tBgR2_bEW8H-1lskd1rf?XofO&6FSqnY_+o5VF5; zXn4@a4UB|i@~`YIpc-dBCz_nV`O)T5Z@op`eWAAUguZ%8J$pKr8NB0iIt$h&N&Ar? zCPPazM)^3IFaSy<@{Vgc6^p8tJ((VSUTZD`^=g$90I(~dWk~?7i%2XYh$#fpeyI3d zlR6H-5%xG6gZ5Y%_=#sZ+?!sIoiPT%U0=kJXIL;c;jvFkD(Z>j1-%A7R;iqWo zL$EmBGOnL|o*w+HDsF5ASD^J6Y*AoWF2YUSC7l54P0k43b2_Pr=%T2odb+9-; z74ZIV!#E!IT-HxBv;2;Yxb6}&?Vkev_G&8`&%6jT&$BTn@w2xZR9gNoz*{%N;C$ZM z+V{tR0rIv{T>eR`N{)D*@V0paLytaO`#)nA(W8W$ersV&9t~ZaAf_BEE7iMm2my06 zVr@apLE2+N%?s0;EsQZiw{I`}UBCJonApOWADRDy7=RzlSsXio>Hjt{YvAVZndyh{ z9TcM4XI2NIc>OmmyAg`tFtH8@JHJ`d^y+U}47ULDc2v{A*6&&vYXJSk+}FDYnExL; z1JIVsEsT={{e0fy*ayu1+SZ8LbEk!|9>UIogwX6Doq-ieI=~RhAbmbrg`3(fVFy9> zH$jq5ajAot2TKLHgH%zoBDA`dKpZRy<*puoO#RNURAf?7VY#pX4cPV14ocU@rM`IF)6!L$ zL;fK9JLkIyN={6bDb(MnT7=PQF%4khwvez}O$7D({nITmyN;@upgsq!;=E{~c#(@m z4wNwZKcvr%AfMBD{zP~sm0L3>WIlLI^>l-HI7o|L{`YN&dkPtbm17$3C79s(IUBd4l9n%Ap+-rO5J~g2G-AS$MD4r z%t<~sgHs+v<|KDt1HGmqkSTi{wEXn|a*`f&{+j?WMLAyNTXQTB8^!g1!p1T-@!~(X z>`Z}wVJ2n`aBPO9Y}_B*YNhQQVCWLa;F^F=e6tM7Q9v)+opT8H2FCu_3PD%Q;M9ji zNlm}v)~^XqV@@Iccq>LPOt4axpG0ohU}5dO8$3Enxwn4~)kumJ`*mx!;R4hAPR|0X z1K4;3!yPrp4-X(ymijv3v{4#ZH{65a3yHw&`05;vyC02X+b@V`1u-p$B<~Z?%OO&Z zvt9_o@WdC}7|`lJ0+Ar591kLZ90Uk2ElIc`;jl{JFyHr-zfhNJ>^{D68C715*|ioe>0;rk*sSP9v+5r?fwAtMwI?0`0;NN7Oz z2^dL;)aMnEhKF_p_A6yD?fb291fcS`@;_jBh7cx+h%8)~;{YW?Tdsq$#+CYI(%B>e z%Kt#yafYhx`;AwEO0U$0z~JcLBQ;(g2dQWUg-Sr35TqbIs)h_%s!E!pkduu7^*pGI zB#Z#lNk{|;4pr1N03an96X)lcQ=l3_gGC_a7^x6v5{eXcOqi>!ue~8)3f3_aP|uhh zW5DcuMj@crgQfuG-Uw08$^sb!rla{ASpk~Pp#+Rl^&F`z$f^g-37}FHdIaVLQYlIW z)}dJf0SuI+Jv8641149&o#G%E_sO#*irp>y}tWm3uDa>f&Y7x#qqa~!>$W# z5tid?@31gV_!HsfW~!gPNxMH_V*WwE4cA#1YfmD61~bYY;{JYLD)yg&mAJa)5YhGd zqZ348d2#AC3b}*qAPN>C=}Q2eQHvV9ze(X0p{S9Z31Rs`8B^UR403X*MO02r$|&+@ zK+X(+=(2hQ4pw~#bf#Nixm10cv4i!+hbf@@>hR{j+ps!drZ+}!^*{smnDUF{*56y{ z{mq}$b>a8nU7>5mz4M0hF4P?G-*%+8vfahwuWbMOXUFMP=kGxs1mcFGcj*t;% zx>Uy#gZ?l(bsz{`z5xsVnEgT0ZJjRE7`)eFIZC%iZY*}Y zbV5E1b&=3cqmn<$E+lng(#Ih}{l0hp=IzBo3A=3aCa?vhyfat>cGlz7L`0{O%3n+a z@3J_=FM`X|LQ{H!X2&ZvAYT;dGjyak{hgPlfB~>jF*NoE5J63T`<@X{%q_uPac8TY%(fiI4Pkm<}xas7V*%)Mdgd6(x< zz$kzpBZmT4Lz}Jw46q7%^OR-dK=0?K`fG%X$F+?0j0C_*qOE^Ge$8wuOz`BxrL+fQR0hmk>jDD_57z~u* z!yuU6Y8yicYoM53k|%ZuBg-;mK=nRgg%O}IRfFRO1u#YRdNSU;>Kd#7#;m_iWgdvP z(U$!H42aS=PY^7xK%|%edA*bj213e!1R#H&6^mA(-7 zGKq;Z)?oSyfFKf(hXmbnQ-YLu>raQi4S-qy*^}7&L$fj-NnCroL4jjVfxcpypXn!$+ zubO*h7rm2uiHY@}XX1_*njE)Oy!4|M#*6+G_47^p{_(oe=C~bt`*$sjH@^@wFE+6* zUx%N+)WTTvec;b;wmAO!u{!W-6Y~ut`1#k({}Sv8z`vPT=dH)o_e_p+0yb~9FecUm zcbZtEfl-6}LTy$*7SG7K#cYEmbQwkq$`Xf!j)Q=@)gYE66xU*+S$_8+4B) zl`%Ut$cqIH+}&Tj)oqlD9W+kK+!)kAU;m^~woWA~8_@r_V9BRJEIa&{#_T{eb*!;BpDmoy5kVHssd$U(8LYcID2Xg`xSoax1m`3!*Or`$ z3i4sdU-(_GAZ;2aM<0S5xTe$6Z6R?P3xxOPq*`o$Ql917>$)4xVro%agKl(M*Tuy? zWU!P#KcoO3s1vKjn@sl^C`I8<2nWcRRB}iV(sA-}(L=24sIZ1s0m@uG(nOb2K7RU_H3kC*a4OB8Y2I)}C z^O)R}ZqijDaQ=JH!&iiSA20mZZ!r2|1_R{n(|EvB1DS!`av5;x@B%>!!YhA<4WA%D zB>lX0hR=Ng2xTB5ugIV>kWD{9ijW;|vP%c(T~(U0XI=?dDFgo5%pZWU9|Dlof5G=N zgy#M$@js4f2%s3Fet0=n{JUbp5EqV5PT{<#CmadMZ65+&%N$Da>vwS3(bTu(O*~?D=&p! zaR(@dfDpnkjVQ$Ei@6G z8B$jnSsYJM*VQDoD1188EBfM;U-51m$geTwzDQU$_Y$zU?k$3LEjUu=O)(*ay469D zyEOkrYlP5gaS3IK*{vgXelFm5+*l<^)q$klW?<1Z=v1Q!=R{DO zIOvR1f^$W2Vxa>p=R{B`-LdNow2)}gx%6Vmi-qE9=Y7OMld$9JiA9CQvZQpq=}7MU z4Zd}p{<6faB<=PF_%a~6MNwY>WI&t0t0w|ZM_>uL%+X@SSi}~=G8KK8Ez+sN5VjDN zMa<5Nx4&<%P*F3L9UpdN(cBqWT&|$t-0ycw3evUOMN)&bQcI9CLn><80xFcKN*V#| z@A=^1TSYQ3u<>?946g}fh7o@Yois+~B){H*-i0|71HL+kEMpqf8y5^jAjm19HwGpQAhiuRtd}4pB-{3(IxZz@NW%3s zRvndCLci}?@a%wq#v^gf4vd|fGAA5AjXd!r3e4*Pcx(b-j9Y$jN|dvlVKM*+k1uJq z`eI8y2spmMs1%`Rpx2hN?45)ug4-@~B?01;})sH224B-@3tQ7IzjsAr+!glXVivA{wvCv83f%*LhtLedWkERF&R5ote) z<3Ka$1Uwg~Bk3UfmDFaO07ZM=C9)VAfikqq#@XQ~v~-1UQ;Cb<+e+gT0-%`EBrHR~ zyezg}0A_Pc2;;!~ta42i#)y5n`~t@+#(hwl6Gp5cW~PCVf@63Yw2MXxxV*yrUINHa zRs#EHVWePm6xe0j0%pBe7RTfQ<{Xd(rLhsn+Sm|Q1kD{l^C+=ug4$mvK_xqK5Ys@y1Q5MI%`ulq=m8NhL?Tl9LAGqqF2-7Mb%m*|(b(v#?gL!C zzkx_O&N>P+U&t^U)n7P_Uwy-%&=G$KykRbbM2xRF4clHNDaC?5KZK87xpgza6uN{l4p9#yZ1ODItSQvl&TuohIVtsopE`N1l zUk|S`w}f8w033LoiTS26{OoHM#)c;dmzr4T9H!~ZO^*8nY~OBS3{L=4SUiVB7B~Qw zFyy_o>pY8(<*FCp;}F#GX}BLFmNY>qXhOlInNa5FK*7ai5X$D0P)@8!wcr>eQB@R4 z9dHVz@?cSM7ZzfTBi4g#*6QhED|nUFZ%J{`LQ>~Mh;B(^F(GZDR$o;Ux^}1{H9LLN z5kM^FTRCWja{9f?St!nVpOac2MV1?*XtkoV+dpX)+|{v&P5$@SqKPa!oi=J!sXnd_ za?k?X`Q}b17TaXNg{3ydsPoV&u#iV@4Wp5|&RaAY~}ck>6y@6Q-n8tCWENQA-#ipp0xz2txuXG+vyg zMgc0rpNZh8nf@tZxM!_^^E1FQ8e#K5L|A4a3#2Oj`lcop~zF}>6FeQaRq9~Q5Gp1mwT}K^Ksep zfP@fjvbETbCma~%+(SmN`WHyZ+GiettsiPQvg4`>U;Zo%d)6OAe`fzd z#A9%)&2bg5|M&0<+fy?$<91l1evtNUWJ*3SyS{TdkC0rwm z>ZnV_R06LdxuR&ZpdfU-BorlO3(UoB>bRl2^M;)^HG!fNOBPF} z5=YUba(U2O>CS~3kh>ukwOi7pJTY2u6eaH9!i6^q6rBQAU2JNUOx`E}c%5$FbR-sQ z4@*gx)S`O4XATJ6zNMA1#G)f-1C<@8$(#PN5w0O>lOQf`^J#?G2E+TNBX3i}66tzc ztl6y-i|MXgXoRvxPDkGNC@gY=Tur1COHj9sTGSrods-|xwck-WD!Q98mTw}U#2FX8 zIoBf<6{q0M0L$iF47+fO9GyBU7n9$Cck`=+B`hh2Aa7-}SX}CJH@pt~4hq&m&~#!_ zMa6@wJS2q0Zv;~<=fGha7@V0SCqU%hu86U5g9>vyfb}6~CbmtZ+GtP$w?(X4l`#<5 zcQ>#pr2reO0~|Tdpa2xP?qnLaE+U>{T2*7T;gIEQJjqJsmYoPEnV=4;Qc(S~ZmH(KQ_0 zvIo^E2~g_&wAO}uA*CR>`}@EXGKN5eZ{LZDr_(y#RpXrV@?AXg`pB9sliG}KDZAjKGC=os{=v^O z37|bL?Qa-ldC1o=Drqgr3@{WHc!5^RX;6u&mjEYiI?m`%fdJ*yM!(W=#v>z=Jh)=S zMJY0=3gTHnl;NciNc#)p04)vgqez4Tl#de%a%G++Db-GV0Il*PEB9^gqKLEw!5oie zY4UM`0F`dcV`&*DR1TsjnI&X4F83f%2~Em?d>p_f!D3wSqLzWXX4Nnx{fDnmeQnB+ zEKuk7aOW?hQVzw{j|4tCr>Jy=i~r{s?0LVWnDt-M!*{-={JKnXE8c2QIIbxq3g;9%hlsmd`W94XZp(-Xkn%5C@EgkyNB++l2$|NH2pQ5 zl`XM^0cQq;CFW~UQfJ{qBD-zMQFiRe?L<0DRbCNd7pvWpb|uaI11RjcxX^;5ly1LM z`yE$zjlRE&u*eaxq#WLIpx~3Z>~!7$UMChExf}^ajf&R6@)camStPWCnV=SxUF7KV zypi)KA0kjP`5kxAotAEdADc^CQ!oh56S}VV8fp-}wX{O?J*|e%iK0LKq2u?@(hGsY zl6LcH6ajSF)bvg(kwphSC%PT6XzcTQf4O>qCGA!&o-#%`11`lC=`=var?vRW2iGT% zV)01DVxl+wDmN@*sE8SAQz9@RkPF^{IdLr$gywm;wZ`P(2|+@3;CkTfh6!Q81Anm{ zqYomHZwY-d>fWa!04c_8-v^(WFd)g~7q@E7lT!pVEDLtQE+FSv9if+*wxY&{mnush z)HsjYFDRi9e*8=Jo&`xr#_6*CIO2R5KQ}+~b>KnE1ZBr{_n>zUWE{V!vFgaE^oW?g z3)qO5LPW_y6T%jfBuG`WP(%yR-5Xda(z$pJ)+o&-Rt*Ek!6q!od!J5?Po zX46qkWsk3ya_IU_=?M({Q?8)&#VcHDR#4(pnN5 z4-kVm4+^Pl8bW@#KjGpqFbybAP#3jErSVH_7c9`P?td+lMk<$Wf{UqPSw zwTXFO&b{Bndd4!`_FW5O^zp>E_gWmMZN%K=CgvGE?6}j!+yLBT7RT0_AfZZ;s3J%4 z)YgA;$`Xg1S1cps`kdI5WH}1o3kks`f&{|DA+^vfpis(^0WL`++L$Y!HGxil4zqTD zx(5U=N9eS&C9-svLV;RH+UdNhixvsztgKnEn$F$W8j^c~>DJ2k7Lpb!vCKi0tn;Rb zC6+Q2osG&3RHA0Ll^wf~xP$?q)V4eQ>^Nb?C`uf4a9SN9mm{^PWOUj{aAFH;4J@0x zddgVNd=@ulYVQ!Q69Kta5UJyCX-U6R#fi|S@fA}CO8~lUx_8vq%7#QNp@)CDqIVK^ zkRWG%Yx*qu9XM+-LRF`gEfkN+gVf^fw&OF47Wrc-NdM==+W9mEU!~VzC~M-Zo~8W; zbUZQS`hYE|-%c^AGDg35h+34cmh_uCej#syB1(7aG*>OkJ^^hXwQh$&kZ+8mg zD-wjno4yUWcL>JD-wiu4a(19`d_CgiQ%DjC#%)&s&j6WwU)qY*PXa-t;}mhB(gI6Z zBJi@(@68?lQV4(mCC;NHB(0#I{z^l?pCCXD#}}ux@mv|l%y)qYjvJS1_1kUeot|OD z1@ESa<*TA%Ml!z-7)dae@P}g3X2oM86)=)18N@sid7^Uw zs|AsIRzHgA0|4dysb{q;1q5Ui5LgtY{eo83_F!+*T0VvDiS-|bvV5E2bk;)2GG0{4S4IgiyqEYIHEUNnk%sY7s@rSWaMst9ok4_d-(VSFKs1UeX(X`deJ! zbXqYZHH#bpDqlJCaj2MXh0Z_SyRd+Rjd{x<6b;^4=yOz;O}V-g%?1!URXle5 zc8gfMn--={MW}(Qw7_&=2h)|>J#=VCQK5`#nk5^fGetq&Oav}6dNYqJT;Uv5S*?N#%Y1<0!GIYh@^ia zf@@NU#Hxs}E@J=_Td~2Q!tw<>+zvIOceo1apIi^5B*NYU;96uH0(L|UFBhN`Bklnv zf`H7Mslj1DDTgqRgQAHfH4luE2m}dY{~W>^khynXh{*ao(4-XG!L)PE`UNaa3?Xi77?5%tOKjhs ztb7UQHO2Z*O7CZ%L_tnR!8ZgrRB}{)A-~hY>Z0Qg8U>D$;gG`GaJU@Rj75&lhp05C*};Ny zsB*}+IAXzkO$5s3!>r+TR9sPX2xEZn19pVhVaaN*6^ls%oTZ9GpCh|OJxvS^gH20- zp(aSVtJ?Xa;iK+p}ya;dz0GKbm9M0m%mI(B63MEeHK~PFE zAE99}4O+w;K~hLiphKDOwfd|rCjA6cFGH}}WSr&YfH?byroZn?~91iS0U|~#50-H^&^*zjPwK;%Yvlhna2xLb!MAX8e+M(i= zH1C!VOPDBO)~J0?i&Trxg@=Ym1fADEfwFabB~1#&>N%7%=an?d`#BNbD`^FbElTr{ z?&#EsUZ<|1P95P8@>ZVnMJr>KsiV`uA@UK~!immqMVBLqhsxIGB*|xf=a9Jpi-zbM zLa&2E=R+tu&IV8m=YIhQB09E+|0NtMzkjx}{|y}~W4vkd??3o{AMpb=vtR!J@^Rn*^UsNL&-Zts zkKi2}Pq$0>GVm8rea3zObp7Rj%KwS}1ODEB1mt~~H}Oy5KY$;$U%~#wF!Z1|{vDgF z+dsSiA^ZgV0QG?UCCO428WokSgZCfWU;O`3u*yvHy4f|LsHS-Tqh9Z}q>{Tk`|qzx^ z>7Rptq2I^8&~#(*tMp&`M_^}W|IPmRsfFpc*#!aim;0yget~Z<@h)PX(tYdAhw^;W z@c{DAz582i;6uUqbyQgw%ejCdZujqVfDJLJiDRSMa-(p;;698}H^D8L4IfCbe z@A&}wDkI#5+#4{@R&fv^zNY}69JugJKPOV-0<<1Yhx+xaxbJt&iGUVa+?l!{sK^BO zzse{9+)y$RS#|1!;Tre8&g6W#vET5ysj&AmdXeR~p5hcpY_@Cd6>xl>t+kP|b#5u4tf8t=R6;qQLkW&j&z zBw5>lH2Cj#%sS+FTJ4)FOc@LdNBvl!K^Wrg)V*>pq|sgj{kqw=*$t7=xbxh~fn_j# znNi;|B~ZChMA7#}V`_ zqAiE+{vvYd>cUGCQs3n>QoLQ9^^$&CE{yV*2OZ;p6;!{%uAlItDRr_sTgIl8^b#go z5tn{w9~*dl=7bC3ij#VY4cg@4xQbI6QkK{K#NLAjDzljrD6fpoQOPIljVa&ky2YLs zy%qCk!ygooz?<{RR~C(gZZ;ZbqBYY<;uLRq2v0wfxyDZpVHYx0g89w`r^bimAoul6 z`_AHyJK1IObyWy~=buCWo$CS98PGg9lEkXMSa;&~$rGZjGYw3Ik&xtDS;ne5!9Ij8 zPD5LbAB<{ZBXfLQEdV9fV|P@rBi|`JjfW53yUjH*K0IORNpm3_i>dGsDIy_bmJfX$ z;NPBLHr$9|i4tU+^^H*N*Y!7EFcHNMbOHf3ysNUV{T$n?!16%>BRu`(i3!<}$+Czk z>{oi&kmqL>mfZPWY2ovdjOidBGC2u7ffZ*qgq_FXWw1FZmE`4bY&+uj=O&UfHIn%{ z6e=8TdkMz6fjKgWA!1UD^{qJ4*us%%#+f^{QfX~*VDCPr#@Sd_gSaJ?psa1Ec$(3% zWl}IA&^;Q}Xc{(rCUat{OwT-RcUI8T4k5|7M@($OW-RYvDOsv0aUuJ3q*?Xa=ZsUq zpbm(ctN^6^Aa?ik?mLy8VLb5^RAFpk+~cKVY-BhfP{)gzXV>ZSinmyHUl&={-;RzP~1Zaz+fGc6H_l-6F+kRA1)r z_MdXItS6o#ijdR~C^~go80@ceBX3PwH0ojDF;kA;L_4F_Kj4Z2nkjp@Ar!m=0k^-u$}WEkasO`r&nmK zy&Im&z`K&$ilbi_wcFTh8M0$2-E@g!zSGLH(=#&m1eqG_$N1cd(x)dj2_2 z21BEiHzEUIu>^tAgy%zq?lr_b$qmDs=%cY;?n|WohX1)%AQH7}x5Q;Ad^2L9@VD{# zi@Xl&dSu5v9I*of^mFPWt{MKgot8u=qb)d12LU0FnZYNop{r))oNldf!m}$Vyg>C3 zb$Ld_vW5kFzxTXPZL(z0x~nPZeO5{>r)jj%2%Vg3)NynM|HrIGX&AU@+LPn*5_>QP z{y#FZi@XiTv_W+*n4l4A7Hsb9RH^28c89j({zW-GhH0GyO zQ5Sw*MAY7tP|QbnOFL#G03u{91nx5RA@?-vF6E&3IkSQv5H(k8vfIh&b=c!`LLH=)bkfHH& zl2-k58ZcHW4aZ%4qgF;;zNpFO#_*-ojEN?2|5XG4005T!`v(uDw=m3)jVf|(9KdCB zbOu?uO%aNM>&8&ta{h+6`Enwo+!%qVi7i zZ92bVMIn#FT^Y)$NNMABTFlkX$v7NSTya60YP%Z+*ExYN9T1mT|tR*PNq$*L_>~sH)}I zdxmn4LB#L^4CF?PT&9bofAhP;*Op8Q2L^9<{D3qHAO>e0SG$s*L@*0tePD)U2S@qn zU({|hs(YNcJ|+L`CtP)5tilVwOVgkL0}J(12l>sBu8_I;3Cg#Wp#!m3NfuE(6NxW! z=8f(-7_47pX6rlV>wy+QRMN8HV;dO!cFd;D`G$Y%(jlvvEB!BJM+VKJI?}2=OR)L@ zH~a6ZXljA9o#I0pOuIVqf(1F0UG#YzZj<4q_gc%b9TPHROw3l#x=Qv@Oxfs}g8A2s z68M!r!%~%3gK3Wn4t-x%%(d3ZYFAPeo6Q$~EL)lN^;R>8#D>642Q*J$M#u%v)OaI8 z5@G<1GW>76`nAx+dlt$3i1Oy z@-w+cz4_acBd?ezNf=mx7C~aJ{vr?^q-iG2-1!dw{WsmW(3Vh5Ghtvtc-@9e62Tr% zd^UA0o-SdTZkb{tYPhb*v_>H#6RIrYHeQd1&dtrO5Od|h;zL#-h;lmsm#ik;7(lDl z`u@jkA^WnMSuj`j01%(eyYA`kI4M8#&Gx-dztvW02}i=sBNhsj8|(>FNG48{DURMS zs5E;_Y;y|gqeG6voYEe=M>wT3{X?%&Cm@+m$y@;K(v+&_I-MO_0$D%#5)UHB2M9bF zAJpJz1}bvPHlc<&@RqBdd$C?Cd;b}-D5P+C&iwJ%>Zf`8Dl7~)@mi{XwmkAc|611X z2p2izBH=aO)^_!twSa^`qYQH92_nDI)UppOfd8hW>zIz9vsbq=t=cIXin58+xT(4A zWgViJe8u=}4bpU}=jikEIfIs`?W7t|%uZvmnd{Z_>evAkt|@2tS`%>UmjU9VjUf4Q zGzRuCR1fG%Z#LUQ?x{d4qu6Is6&7W6?H#p%WTw^;OT`s;5L#&w7KP<&U*emfgnS>y z@@g3@m9rnEcEZv!%PFB)Z6KJ4ig`c!GQBh9{jKiiR31~u4T(;U5$d&e=of>esA;o7 zdzHQ@v*W5hfJ`xHfx#=42~3?iE!ZCd6;5ju@cuvjnOD{I-Nxlgn=*H$yy7oXfMG!c zq^l1$P!txWF?0Mrd(wa=JcPxJRmVzPyBVid+E=Gq^Z-ErIYpv3(7$hyr`X|E;kz1| ziww&iw+(U1JL>&d8;YM1gJaJGD2=f{JrZjF7eZ7X-HKs=Wd0%@aWnsGs1X}(>Xh(z z)DEhwoGeLU=!PFNHnFZW?7&{CR5eCUWSq}&!ekemxiCAc;6C(4@vF>*mBI0#X=Eoz+H~<94l`IAD33hAkPByTK3>ExEbuUgCX*a&Ai#pmyrl}0(6j?xRGD3v zEeR-)$P-ito1{3N>1bDgUW5XVC?zBxNZwg^wF0xL`UU#|W`uy!;#3eraJst4({WNI{K1_@-z}{MD~%;WN60Qan2(B=gN0 zjnGc=5@}zSmjrR~EjuG{@4t}$ZAe@EwF8OG?=6YYNRVT_7|Y(`zcw&2ZjUvT zoO<76+Kun<3r|Nd@~%Q%y-h1jr2zZAn??6YBtv>6nTe;GqPwX(zQ)W4UDHoBY>IkN zIhWbGJV<=f)j}Y}`wfrbcK24d_Cv;4up+y`d~jh(E8OS^ z!iL!%?Q%{LLX$9OFn7srXtfjK$$2vsArt1{pXj;y%ev54UqpFEC7d z==XyjKJ!s}N0+_p`!RmMY)Mwf#angBpl2(He)=)inD^;asoS;v`6KsOKN*F2n9Z9k z`@=M@n2bA0Qy;ZyN(&%A?+}ETcNmsw;e!DZL+7?~9uBU9{kVAHp)6CF9_>F-R4Qt5 zJk%(!!&zXzTkIY`#IehZWjklyKMx-_Y}H5pthlhTi-xL<^MlYTbZ~Amakv!>U*-VJCR8(Y0*SC0j~KWPuKf#Hnxzew{v?^M^S`$ z++*1suXK%pyX>WDAXj`Ai-p0e5ze~aAUz|+`FR{8xVHS@jjCt*>&8W1roo{nPJ~O7 z3g|ia_z6VL*djZ zlo-YIZJe*9j}9{qBBN`B1(q>b=Yr{B(nNy`5RdWNGOGICzZKqDICM4Y&`Wn2KW{SX zpsHqh?+8-Ghl>oKm4*XSIWPO#Mv?gmh)CX0Iwu>?-G+GwP%&&|)R(z=$zI8}$EwJ8 zIPZ8*p4GcCo+o;WYNOD1>JF)y-jZTW8xrC%DhQtdceSuPdP1WE7OdC`P$f2THu#WT zN{A{11~9bKwXz+SgX==`v(!o)bfe~{JVR=nYoRo^Rb)GeycHFZIz7x@Hl5@jdC>rg zJy-wk-R=>zagt)&8=Ip%-3Wq|m?*p`jvXDOZlJKTPi|;Gx8aPV zGjHkGogu1D&QSN;xnRA1ZAzOJOV*fv?uvP4GC)-edT@!31D5sxyes9!`*RqkCzHSYJu#9?G8Sq#9o=UN6P6@iU`}`hs+t(g<0y;m|rderrnxd4Sb= zht_8#b?bX}5x^WT48!pz9Nl%-IOlhirU4uQf&{2pUpxYW!c}OQC-qcOKe^}AN_EEp zub&2;8Tm!Cz*|kil+QO%#IVQcvzPvU9Jzl`$?g5nQZ#V1r%i~>8uPEiorz&s+@tGYQcPi1?~jBli^$S3w<5NZAlMtXZp!^m>ADq|oz2Dho6KOd7{K8t~{a97yhFIiTvyv@98(!<)7?$mgzC0vAtsO&YyInU()_!%3qC^b;sV(oPo0_ zfulYElC-`6XI5nogWJ2K4Q$-+c7$dF?*lL6bE6_iNWu7j|FeatX2MmfxHS`0EH00& zFNN#wpZ(u1FpTES7~B31gwY!zf9Y{q-)sIseL{F;Z*&Wv!LHOH)tnNwuuG5Or=PFa zEUB9-^OmUpp`h3*zeVbLQ61o;>M4*o>U7YoKZnMG9aQENDmIiW_X+X}oSSvRyMR!- zI0sUMKqP(|56~07D4+fP(dA(oAn9&u6_?q9V0t3)Uu0-~(x8p)sWiPV00#rG44Lj_ z%QW;{h_PQuy;Hj0L%vn40cuS3qel3^-A0c6|Na}5Ku`zYlbX-S8D(hy#VT&RLHn8p zuQ;A1(FNf|d)+N=(rQ&zTg+;n%Sra~Mtoto?D9%26tPJQ87h}%m`>gJ6(hfCQf z?ehr_Tk7VGJ2d(J3CZX`bRd@azRua4zuv%A7fRXX2RRg+7YOp4Dhej;rPaoqU0?>m za(L{*EpODjb~_Du+~ZHm-zW!|t=>w41r+Ex4pg5GScoX9VDGz*^%1Sjj=>VpEr2Op z(3MHP%Ai`niK!Z>NPt4IkuWnW_N z(w#B}{lr{=1cCJt0*1$^8`jJInF70cpC*J-d7roxNf%cp+KWd%Sm*$y!R`A2o{mc=|#2O=tmHYx1-c@A1(m=@7y9jK88PL>=e}E;_i1PYAI`s zU1-0Tpn~$nku*==QlA;kUZ8;KTmC^^vznP*`*f9Be^m4E6wAbL0N^x9=A1s+YOml>jN3xD{ zQ0BvhdvY=%#3vWFmSCU@-m)_k>6Ip_P03Osa1@VL`b1d$vXZ?UvX8Vtd2jzP5w!j$pnuEk zPyWt9>J zH`F~+o~y|lcsJo(9mC_{2l*)i68sPxo&4MJOFj?{z9Njefm$Y2Mj}1mdhTjYGac*# zf%ILtPjJf5{&YMhkiY^ym>POTqQhz;5P7;k`IvD|BA&u+Us}{cBSC4ea8V1)U^Vpt8dDFdPCrsa>v3ai9%0_Y_aqhbK9Y7`>u^ z|0td%p|+t%J_(dR6`-v?<0t&ADelzL;S3kpD`nHdT9c*TwUO+thN{>W(Q`Nppxy$IE`93_lYP8E9bh%VJNK!9}NU5fR zdLIwvN&MiUmZx3oWBOqLfC~E>%7k^alOg9HHZ68!^xoaHMioNx2W|6qbsiH^56Zw7*kBD0cCwM_a5Q>KPN{=J$(8T#Nq zWHxEQv?gM}u>ngEl5``DpVTP*Yb6q+2hFy`-9`MYKgTO@Qx$zuqid`8VMQvYgn-aH zT#DxbgPPRHe<#VrfBwXj$xB6s2zu`j*=c7X;8Xrj!7C8r%kq9Af?2_{`97}l2q_o{ zZ{+!D;srnC`GKCK!U|T3$%yL=Zd&`|;>YNNB3JT!hATA`?LG`KNOA! zhs@DSPn-s2D7ZDQn(`5^nP1P2fp?^X+c4gPUt{365;8cU!E|;wJYXXDcBc^Uze*`D@%Cax3AHPvrX~ zjbu>A{GUUrN-xRuY}|)xQ+c2Fw9wcvc)jz-qMR)M1H2J3`r>O2vyuFtTJkcYzBQsU zwNmax#~S~W>B=W8*~t`TB2m z6jNc3dd|rOX#4-tSL+sy*Q}yI1DR2Y)IqA- zn3pXCut6oIgHgJ>qX6!*bvfVG)F!TC*xI@kudxd5dL0P@Cbu_(6k(pQCE@J;VqVf@ zG$VT7>KLjR34ORv?KuCt>@BFlrf7BPcJXe^QYkWa%cEphj?Hp3aAl*`&^x4g!Q&22 zzR)k{F{cStdN=o{xT}Wzd$zT<~-XO?IZ{KbY4Uq?w zVGc*9DS%?{zTu_#1)G=XXKMi+o1!y`nrY2uf;`zSO9fWuS8Y>9V*e}SWsw%w3c^9? zK#gR-P3ePCI*qAT%>Hh9!vy=A9#wiq(Yjt-9o|AU87!w0zB$Pu+VWLPSSn#1Qz(lGyk4x8cO0+zXGDb?~U|nS(asJg1qN?GE%WJ z#NyYnNaol1tNA{S7cBz5cY9#_8f7@=#{E^9l(G5K>oy$Hxay^I3Ua6`Wj~Ygba2Wa z$@077UZXf=_DZE{wam;ft-u7Fw&+-@tW$}&L!)TntfGLR3w${K8A>zC< zo}_syNvw)&IB#qp&XVlIMQ$7{=nJ`$ioHPsHf>KetX~vJI?D)fDuB1;xR~BGa9mu4 z;*PjX8T+_cy_XiJ#X6=i^x2=9161Lt#JuxbSqE0iBi!&4TaQO$yCar$g5^SXJPq7# zI};>&BI&X>#VitPhz$0uSd`-5?IG*6MugTUq%3UE*+O6@n@v@VeYUWEpFt`XO~+%k>j!=PNa= z^&gl-iW}dL3SA{LlZ%TNRjGeAh)+q&#X)}s@r!az#LbFFS(LkG(^(4fsz-S-$fDL- za!NB$SkSC{eN6y()6V=y@QdwMpEAG(;~B4>sqV!!EGd=__=IeZWlA}EJApM*ZK%uYBzaI$D>Nb(0x>d;U* zRU?km>HB->++RkE3>t!*r;e0oQe5oqw)IDR;lVfYkmJT;&}PW^%nH2v%DZDtjq7?U z`;$Y(<#=&)$cx?;5~^{N^Q)>DLcNiKSp|j%(h8!Fy~X}S{GWu61(ItWM#+^v@03GE zNaA<%5UM`-54?iixDAh%usOtB|0)&f7zg3)8YSoOj2i-zr4go6k8N-eC`ZKLZ7d9q zdFqlL;PVUR`Ay~90msP^B`YAi260UZSnIbz=jspj8`HEXLrOHi(pgjg%#erQ2)cF2 z3P>vgnm5X(10dd}HP&Pqfe%dx5p7+5OkuC&dfb^PN;pvGkbW2Sk}TlRlhHv_!1A}> z{-QJP-c@fMT33D_K31tt)MHgODea}{^y=ecE^b7+EG&c|Ud^(|);GbD(Mn z&7=6fo~_Fa)U7>pK$9NB9(wD5(qhY8EBs_H;tRLE4X_}d%F!`?6I=?c4Lj(|&T>7a zlbWA)Q(j*+k<)wvLH3Oc;g|#6NXQbzh8sPCd|M>eJ(3Qe(dd+;Wj8Pb=o~wlz+qb(=6OWSu{+fn-ZQHn?i2sx2 z95CqA^{WqgDOTdG^Q@`Hi3*Ky_fn^h6r1o2nj=$JqJ7ad`US*2Lvjm25Uuew;K=T_AK zN0zxNZ_4DR=Ns^!-=o`63E^CkKGS+RHf8h{s? zX8X#a)WHqd_9$Y(b#z;6vJopo-?Z%T+)v4^(* zM547TW!PsRIzkHOK4`gCF(fRd|oqRrC^iYC)W=c#@? z?SzDyUB0{+o{FS!2yxsK72;#&%b-K?no8alL}cqeM)r$W7FtlJq zgCmT=%n_xU>9IbE!cb?C8#70viM-afa1l)O1dao*m%b^q^eY^Kvq%TDX$F7pqJuEP z8Yu^ddulifcB{$wyzR}kpm=MZ>VDzUV=6_y)x#2W=U<>qN0nL6dy)zia<{vB)9-V7 z!@EVP;YD>S?G9jm%)wg4A>GGp@)bI6uuomoOl7PM#Yn&h7*uwuY9~AsaU|t8EkeX^ z?uY+^NdgS&4FuC`T+w3pvml65=xsC1aSn)+W{L6>kQ?t*>$fJGOt!?==+r$ucr@O; zUu9<`yr~&`&78AQn%ymvqoAjB)~bW|P!eUC696`u%_Cv!==1WUOIHlqmPk)+j&{Y?R8COCQ* zz;o}PJ!G-d>8{L`+a@syBs`v_(*6qch1sSMOerIVZU?%1*N4P%Nlr$SQ-}`tBFwbm zTP;!@sr$ofR_`|winV3G2%Vig(lZbB|8oIQ$ilk* zxlHg_Ie*2y7A2g$vG9mk<^Oi|^b-f*C!%AAVu_|Hd>mRcRCwk{&S3MpOVOzZAv za^Vr9fvQZdFVF$y^$`MnM7K7w5RQOdn_M=O*0?f{mR}X%MN10xyJuIHX8n_NZn*@* z+wC7XU%}wZCL|}DYaf4EXla8Y*z$HO)WVR{E8Y@4% zdNKR3o8?R|P}oXK8h=opJ!zc5BIASPWByPniTr>Yh_(ML6GhGg3XO1AW$~3b4I3&$ zi4Zx&Et`~!cUqQ2)2`0}PY?T-yGE;Eh6>=>PYh_Ow6ciOUQ>9{{8n*Hrc?{$`lwYv zWtOB0N^8JU)e1IPIpyDqv`E70uMh5!jo_!BfK7i3OkTP-*mYO(_FqX2zay<>%M_ z4PsW`Q9k(6ZbR8g`+7##==!>OyX5Wbm)rTbbo)NOjMAS*r+*CDLhG%7U&K%{u=`)g zEBTYopV54GCIMLFC*)u0xrKDW_%b-!G{uTo50idEL6e(}}vGY*fWYX3ds7MH3g z$iB3Q9%71Qz?z>Z9FTh#NsJ?gSWM}Cb}L>RH4~R!?5HUJ@_n*ZOpoxt{GU_7R!~_E zyVw<OuV~8`o zbGGBQZTgq^8_+59a2^~64_3OOsaKnifv_Nexfq8b`M366sFd!Q`o)d7>_&4>(-A?m*(Id|!o3{dd-CY|q?{8+x~Nto#g^UyHpun6Np zF8d!r?ma>qD*)uoFtD`Jd7*huo4gL;$$VS-kHWJ(LWl<6eBK+O(n+laRJFY807hTj zl)@P}neqtPu+{tVdCbdek?-La>h!&N;JkhjAN|VaoRB6cpDl12VMd#f>?KjIqcu7wz+MR{d;J>XE#q5g-vhz{GjGN^Z09GYF;Wz@}VPA`MF`*fhZFm_N%Fm zw^>qqPa+-$Pk+lQX(*$_gUzc5fX`Bmj46XCP3pv(N}`0m2g&tp^mF+!CSzS2x?9LB q34+sPfB*mh0HaU<0000009AMqq7A2-_zmAn{0M|=;5U6Q00009b~F9} literal 0 HcmV?d00001 diff --git a/web/public/empty-state/analytics-v2/empty-grid-background-light.webp b/web/public/empty-state/analytics-v2/empty-grid-background-light.webp new file mode 100644 index 0000000000000000000000000000000000000000..592a1281668b28a05bc2ba3f3c57f7ce575ae1d5 GIT binary patch literal 2576 zcmV+r3h(t&Nk&Ep3IG6CMM6+kP&gp`2><|4W&oW5D!2i)0X~sPolGU9CL*8;V89Xy zX>IkFt#niWo`kCLM>u`^&-*v)XXxi=Ue;|pIhh0HNhXwm_y_;}-LcTB|38PH&=fGA zj>?WoOA}aXVcAj1Nn&daOgkz$DJ)H4sfT4pB_w<+`m2-D#MT;^c2sgw=J8m$B*3_G zQdpY9Quhb$@~7aTL;d2VAY>pUu{DGC1>nVG8Q3+b!%|D`og_uT#Ho`kD#KF_%5s*p zi*y0fwgE~)kY{-{8%;D)Eimk;^n*Vm7FTh3<;9Scot>gK8&dSwTYXxA_R))0N&il9d)rI#w~kLeKGI za2#@c9NMfXLpQnhoAQsKD~qq04$Hjz29+WbkR3BQ(jsp7i9d=bRUcLzmF*Gz5xVm& zyUtb`I9slaIwiu|r&Tj}Sir^*j+TF27m2MJly$RiFgmkmq6xXZ$u34~%aQg=gxoRa zyZowdtXEXov%HcLwBCn(y1nqxY}a0;9dwCqPf^Q^pYa(fo7aNN8ldx_&?^4!HKPDp z<(iVH83;@s>GN`a>h~hj4DTj>kY{HlP3&^D7qm25dw66g=J!^&1o1QUgFDAxBWCd} z{Aho+A;bM5{dl>#%CoKt|TYS5}PNMZ2%`t9W2l7b@C7rSem<83BLnHj$d}uk9;)~Xmi`) zHHM}hP>5SENKe>)g5%P})*6_0RLw^|U`4vyoH7&f*-^< z>B#M^A1CA=U5y6^>vZ8>SURSn4*u{&623(|! zmv`D!$0yFjT95$)_k%CK4tdxOg9y##ZNTYL&I;f5t8}_tC7h#E5396Z2fMh`Vz!Z@ z5lTg<9$>>}$%{c08k6>GD`;r5-#l+Vo8a$24nuAm+%JPO&MafIQ{{C}bZVBsB~-*h zc|^Q8GT63Cs)cS@oj{SPGDbQ6&VLFtPIOaF2g6O=UJCM@h&Z0_1)to{h-Q5GK819o zXr3Zs8^6NzC4mvVl+ZoD$>SOaDUN@%R$r9{#^f>sKE4DM*wA`q865M!slvy#b;lxH2l zS@E0e9lJ1R(!xj!by?@gV;jP+w;__JNEJfLJPW=MHwUC4I?-45BiwDz&V-OHH~6Qn zvZ!qnRgr0}Tqju-mnd=6WBc4hmPb?9?a(bO>{$R>NdxY9u zeTxclj_3*FP*Zb6(gVmc{Arc`#t|5dQ!od3AP_5XuQ|&?I|B(W!H^=Gb8pN;@Q^2I zYh;&(iG>GO+*3O+K3Ai67739cy_+tf>a&X49;X*;eU=n$w{@A4g+gA2&jo6AH88cE zO&D-I8W`Hab>o1l$C*~Ccbhn(SP4U>^$n9bU!W`Ld<)M@0{v6QQ_32gehjtYW`4g3 z{&{=Gu-YOg*|TZSP&cDgnPYJr?o`tq+CPne`Z@<(yRuu*2vMvTqJO|2RwkU`Kn3F` zOSdp;xr{QnYd96oS@+CxyU3_i~Yte|W)sV@87pa(jEY1!xoMj7-6N zJ>o_Ug)kW@*|OiIWN@)Ws+}?vmwCnGYy|(`S)1zgl+u7bWpGTrCvG9)iUlkD7OR9{ z4Zva5TAUk@p@3oh&W;9+_m0rgpOHDG_Y zqJBOjaRE+p1BUsM}c2FwQP{#;SHh58LWRQ(voycuap znaDB2Id%}}Ny$s$wmVu{>gt+Om<}P-M2!d7K(BD>yxz-iLw4v)0WDgoW3-XPmwWIe zUG}QQlM!dwz`p~Gn@^qhvn<2>4j1$)?*F3@?~$HDrwjF3) z&P=>`Z!^=YY%vlGgl_^0c|UxL8FZJlg9W=m(kkdvZ;4}pgC*PkmB+r#K;KgA$XEVY zDRjC?3QBy@MOOcr@Q|b`9$7W?G4O?-uvlz(z|jQ%0COO+SK-ABD-tq<&=c^@a}R{8 zXz^HreEI)?8;TR`_%#luVIt69M&5pW102~`hPJD**6e<5ecV_zZ}_QUTCFi!4#9w zauo5h5zf8_Y;_HHK#XU2!eVjYKgwZx`ZZ^TTj#ZLC{Y6KMSRhS+~1Mag{f m$#@&23jhEJ7(vVD$?G7r`<4Iz0000000000000000002O)YriP literal 0 HcmV?d00001 diff --git a/web/public/empty-state/analytics-v2/empty-table-dark.webp b/web/public/empty-state/analytics-v2/empty-table-dark.webp new file mode 100644 index 0000000000000000000000000000000000000000..f5fcedd51013fd03aa14665e9e0f9c8fb2a819d9 GIT binary patch literal 3280 zcmV;>3@`IiNk&G<3;+OEMM6+kP&il$0000G0002N006-N06|PpNR0yk01co-+qS7a z+UMto*tXHKZQHhO+qP}BoV0A)w(aB;uD;-;s`W(#1OaH{Hb|PNVzrE(Z$N-SkRcd9 zAwn}HjUQjpEClFZJbHBJ{I?&5GPTM;qKF?qX}oQ#xBU>G^NhiV!}q<>J>uZwtM^HI zH}Us6$bmBISL)TS0=Xk4`s0c2!~0zwXKfwH98iTV!QpI0AXoTuK0m%SO{1^wN#JSN z-4(J$tW>*Z=Icmc$6~a@t^trPLJI6%7BUOOj+LO(nN|=ma-G>c@v%!PN(S<0NQm%| zu&%@KPr6jhs{5OW2&CJ;VEqeQYAVVj3_^wn#katPg37@f0po`s5uukqf0bh$4I1Cy9w3jfN+sV0FSXMqe}v9U9x z46{|%E-i!J%w6&(at8qn`NOtNEWKmIAQt$D;+EA&67@QW@h1urcjNy;*mqcDtNuho z{P#W_^dCY05%eEH{}J>b!DsP3M1GUre~&IFLf?i&!YLjllM|jB9yovrH}WwF2~M)t z12>PhHocTiNL(zm*yAz<)8?{I;|hsqv9@}c;4-{@q0fVaBH~fmtzHitW8j#}$wkDC z^u4{V?@Ky&VE%zT0%CvHSw1h5Yxb$o@o0AOaJ22d%|4~vR=zYHQe$6U*&d4bb-FOr zA7<#7vL@=vfrk%YlQfSOEq1J!F`zOsnWZYwUnF)j zfAb352CG|4&Z|OSD4AQ#o5)c7OJt|`vB`i zPg6Nu)6<~Cc$9D!b{ErxPS7$;1k{1h+_49}@!;$LE*kb*Gu zv#KxJi_$Gk1gt6*0;HM%69Wfiv5`TBGCAPDT!)%4()GzfbUOko{e)2*32@TM1rA)K z!7oAR&v1h6`*N)R9F-*D{Q&@0P&gnI2><|aJOG^mD#8H506r-Yhe9Et3OsKR3;{w~ zTeu6xxBXqCKjJI`?exw>>QN)JV1L@Bw+m(YkfG)u^L@O3spXpXGu8_zb9#N(ub#?w zxc@o6xyk#jeoggTwztZU!n@49NdK*W(ffe;InZ;}pZWd6f13YY|8e%y^MUKX_Vd`M z^|(mqTl|6Y;BQfoy2ru~DD|M(-{N>DYE7ux28$WYlw&jXIuTTC=Q+-EijylojesTe zKs;A&0A@@;MhXPa;lMBTl^qxV*N*uKN-jR_q~iB`clTgX-qec{7Cy3O2nOJC_x_;I zb|!^7>>{cErVJ9oaYicd=OIkN9({6?vhq>{Yy%b4VoR|J%>#k_Pr?EVTcauVLYlB| z5ApG134FzcRbK^ICN>Zab&!!}>{yzcQAv2TeH)sK>Ft5~9?T_8^?>z!)I(b&6dWxl z93B~1f$9;zerU>lkeFyjGWMd+cBAKVw-hAIJB)VPa;+-@xXjUY>%LL8&XNx3ksg=R zDM{9_!4o`TaDH1d+-Uo;KJ1Uxy@N!2qM6RMY*cD+-uJyevlC?sV5V+=8QKwfx!>?P zm*!-hd}%fsD^~TYx>A!60m@d0#M9BX`k=J?+MbG!~Yh>T^5-a;(7Tsf@w^iI$fB!iD6BTFv zCO&s+BzOexry|_TP&!$ez8%hQBgY~fYrjTZ*OPjS)lfP2&6!G11hqI!6o<{iq_$2L zIzOwoIa$^2St(4&aEa$IYX~h#o_^Y+A*&U9!6XD>Ib)2GsaonW$lf=mHF{1Z|Bh4! zn1Gm4P-H*=2C-t?fwte?5(iIw2VK|Fa0C81g1Y@5JH>^Loc{({1G(C19#s|H(TWG2 zQW}fG;_v>H|4=KDG%;&qHnwypf2q3!#ohOQ;P}7#Vm})xc=oBkP-(9Lerwlgw1Es* zb%&kCD1^8~?#E#O+cyo$y&#A&L8wb(8?kLQs`Jc!>7_K6ma^G8>B}jZ)p3?MpdMf? zJF#%*M-+Ci)%>Er=zUMKe(oH#D%}MUONNsuC1`(|V=vtpdn(vbDM}Ec#C5-Ru9gvI zv_QPVY8G5u1SW}e?btP9SS^ps8R>(i1RE|VTxkBZJ5xNb6L$C`DyUuPTY^qj?PdPE zL1Q@-4Bd`K8z!UwY*N>RA@i{41V_4uGHvePCGrVhiPWX@`%>L?61s7uZ8!NQg$=QMNK08!^499*0bQWZ2iQuhn@MUxbU48l)DnkrOKpU%M!)^GNTR8kQ{Lm13(o(O}Udt)IAgg z>o#j67T>L1C`(*`U|umH=eu{@-NJ>TGwz|W0~UL{#&7`rWAru&6JYwXj3sPA=5z1; zVyt%`Ect`14 zPZlvC<56UHix#)M>$8WD0y>Ms%6?=)<^hiM8GN?pG;W&h+|rGxCDa;l|NkzpezM7p z%%|L3RbZYbgS`dRK(sTT&;)b=L4ScSNZje|WYFw6D8lYv9xbqwjyM)4%-Ignqf^f) zD$0u(sj5F!cSSFdf6}@R$Gsnh^je*l5nEKHz#>~`_D;0Dj6yf_ov;x%;m{m;=SRW! zS=)5ZV{C)8_VHPC_d*X7`LZ)wd>@F<1__+%UJppBrWDX*=-%`NngmsRCX<_kDZ^5y zy!VR!fberm$D`)_0->A@MPi6NOtavoV4kmPXDiFc<)MLW)*s^y)w+CU`X&!wR=62) zq)q@oX*6b1v~0Vl;=%_408+ifE)bG{tTMUzkXS%GYzY9Z8~JV3AP1AyaTvD+rQZbc zhRU{Ef2Q^okb#~M7;6~-XS9eY4HZpy5&Av2DGg`=v)LLY8OOV)l*a z7pO4|fU+BjTD5DVd0=#fa9H2st5u=JsLOLPqRK@`=8}+D!*`t?h5Y`&rh>e zCH87VCD|&8vA?B-Jsuk7@zzmVDQP=-buqA75v5263P8E-hc0OWZ`t;^V`vRP#wTY z)~P^%^WrCnI&0lhAf+Om6wXr(>Z5zBkpY{MhCgfQR!cnBsVTSA@+ZDAP*6iJ6sbVJ zhuvis21F6oqf2qI23A!UI%<0N+NViKv0xep^N6t;2<oZ zhm{iuFErH~qQgQ?C>+IL2ESU_qchFo9!+}r#2!1u(0{-DzJFxN2_yAsU2e;|mIWxPWb4Kis1k6C#x|ElPI3*`b^N?z;M5vBqkzfEWW O64I+n2Ul-E@0b9}W>mod literal 0 HcmV?d00001 diff --git a/web/public/empty-state/analytics-v2/empty-table-light.webp b/web/public/empty-state/analytics-v2/empty-table-light.webp new file mode 100644 index 0000000000000000000000000000000000000000..c9e8fec9515186717b828acbde0ca388e849a5c2 GIT binary patch literal 862 zcmV-k1EKs8g*kE3qfy8>oQrA%$8)F6G&Fg`$oj?gV?c;l`xZD!&zNT z$hdGyyu&-Ty}vEn#H+kYn3+S4RSh1ixziaB_INI?A6!GHS;wFZPNuZ+L>TE>+&5j( zc3;U$e|c=FY>9H{KcJZD0>Vf5?Qv=O0PlOH0cxpfT)150%C|t;F^F_0+3M;}$IA&|lN`z4OT>2!G3@ygLY70*m({$~KRcKs@fVC7CSAN-&F+p!wRdsvb6BmD4le8gQ!%{PTP_3rArg{{{d6 z{{5us>Y#*C>??xWO-s|$PcAG33Dz5si2ieDQ9AH4V&B`E#DMOyt!i;ZWM zPHHUpn&)O#7h>>KYWi*_E{HG7YrI6+w@9oaJ;{O>ZwwdB*=49)O2{3SVB%x{h|%_B z_gchyrX*mh+Lp7!pwD+sRYbwuPj>xVfy5h?k9U|%5A|vque>dvfSKji&!4<;agEtA z(v@lr%C9v{ME$XIDipici8@tgzPKn}lV!?Zub5qs`Vk9S6Yw$P^~JE_Kti8nbX_!6 zZ<=UGkf_69UP`zAoxRjBqd+hdn%l?fl4dW}Y!6@_W|O!R z#k_E6X`Y1)k|U)c%XUZBz-+#UeMc0ye7AGpGF~yfTY_dc<|oAVSfjr@-U$-=`~kYz zEOx9h;g|tmAKmY(1iue6RrHvA$#`9n~ww4;vW1&z3gD^({{pdAWB^FIy oT#|Fs=VdS0yTn{dqC^2@3${CQ4hu8ebVB%{QRIhU3V;9r0Qne~5&!@I literal 0 HcmV?d00001 diff --git a/yarn.lock b/yarn.lock index 6f35385eb..47ce955c8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -257,7 +257,7 @@ "@babel/traverse" "^7.25.9" "@babel/types" "^7.25.9" -"@babel/helpers@7.26.10", "@babel/helpers@^7.26.7": +"@babel/helpers@^7.26.7": version "7.26.10" resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.26.10.tgz#6baea3cd62ec2d0c1068778d63cb1314f6637384" integrity sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g== @@ -852,7 +852,7 @@ "@babel/plugin-transform-modules-commonjs" "^7.25.9" "@babel/plugin-transform-typescript" "^7.25.9" -"@babel/runtime@7.26.10", "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.13", "@babel/runtime@^7.23.9", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.13", "@babel/runtime@^7.23.9", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": version "7.26.10" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.10.tgz#a07b4d8fa27af131a633d7b3524db803eb4764c2" integrity sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw== @@ -1124,126 +1124,251 @@ resolved "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz#5e13fac887f08c44f76b0ccaf3370eb00fec9bb6" integrity sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg== +"@esbuild/aix-ppc64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz#38848d3e25afe842a7943643cbcd387cc6e13461" + integrity sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA== + "@esbuild/aix-ppc64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz#499600c5e1757a524990d5d92601f0ac3ce87f64" integrity sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ== +"@esbuild/android-arm64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz#f592957ae8b5643129fa889c79e69cd8669bb894" + integrity sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg== + "@esbuild/android-arm64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz#b9b8231561a1dfb94eb31f4ee056b92a985c324f" integrity sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g== +"@esbuild/android-arm@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.24.2.tgz#72d8a2063aa630308af486a7e5cbcd1e134335b3" + integrity sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q== + "@esbuild/android-arm@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.0.tgz#ca6e7888942505f13e88ac9f5f7d2a72f9facd2b" integrity sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g== +"@esbuild/android-x64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.24.2.tgz#9a7713504d5f04792f33be9c197a882b2d88febb" + integrity sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw== + "@esbuild/android-x64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.0.tgz#e765ea753bac442dfc9cb53652ce8bd39d33e163" integrity sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg== +"@esbuild/darwin-arm64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz#02ae04ad8ebffd6e2ea096181b3366816b2b5936" + integrity sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA== + "@esbuild/darwin-arm64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz#fa394164b0d89d4fdc3a8a21989af70ef579fa2c" integrity sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw== +"@esbuild/darwin-x64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz#9ec312bc29c60e1b6cecadc82bd504d8adaa19e9" + integrity sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA== + "@esbuild/darwin-x64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz#91979d98d30ba6e7d69b22c617cc82bdad60e47a" integrity sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg== +"@esbuild/freebsd-arm64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz#5e82f44cb4906d6aebf24497d6a068cfc152fa00" + integrity sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg== + "@esbuild/freebsd-arm64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz#b97e97073310736b430a07b099d837084b85e9ce" integrity sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w== +"@esbuild/freebsd-x64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz#3fb1ce92f276168b75074b4e51aa0d8141ecce7f" + integrity sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q== + "@esbuild/freebsd-x64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz#f3b694d0da61d9910ec7deff794d444cfbf3b6e7" integrity sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A== +"@esbuild/linux-arm64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz#856b632d79eb80aec0864381efd29de8fd0b1f43" + integrity sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg== + "@esbuild/linux-arm64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz#f921f699f162f332036d5657cad9036f7a993f73" integrity sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg== +"@esbuild/linux-arm@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz#c846b4694dc5a75d1444f52257ccc5659021b736" + integrity sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA== + "@esbuild/linux-arm@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz#cc49305b3c6da317c900688995a4050e6cc91ca3" integrity sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg== +"@esbuild/linux-ia32@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz#f8a16615a78826ccbb6566fab9a9606cfd4a37d5" + integrity sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw== + "@esbuild/linux-ia32@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz#3e0736fcfab16cff042dec806247e2c76e109e19" integrity sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg== +"@esbuild/linux-loong64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz#1c451538c765bf14913512c76ed8a351e18b09fc" + integrity sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ== + "@esbuild/linux-loong64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz#ea2bf730883cddb9dfb85124232b5a875b8020c7" integrity sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw== +"@esbuild/linux-mips64el@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz#0846edeefbc3d8d50645c51869cc64401d9239cb" + integrity sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw== + "@esbuild/linux-mips64el@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz#4cababb14eede09248980a2d2d8b966464294ff1" integrity sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ== +"@esbuild/linux-ppc64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz#8e3fc54505671d193337a36dfd4c1a23b8a41412" + integrity sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw== + "@esbuild/linux-ppc64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz#8860a4609914c065373a77242e985179658e1951" integrity sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw== +"@esbuild/linux-riscv64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz#6a1e92096d5e68f7bb10a0d64bb5b6d1daf9a694" + integrity sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q== + "@esbuild/linux-riscv64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz#baf26e20bb2d38cfb86ee282dff840c04f4ed987" integrity sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA== +"@esbuild/linux-s390x@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz#ab18e56e66f7a3c49cb97d337cd0a6fea28a8577" + integrity sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw== + "@esbuild/linux-s390x@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz#8323afc0d6cb1b6dc6e9fd21efd9e1542c3640a4" integrity sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA== +"@esbuild/linux-x64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz#8140c9b40da634d380b0b29c837a0b4267aff38f" + integrity sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q== + "@esbuild/linux-x64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz#08fcf60cb400ed2382e9f8e0f5590bac8810469a" integrity sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw== +"@esbuild/netbsd-arm64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz#65f19161432bafb3981f5f20a7ff45abb2e708e6" + integrity sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw== + "@esbuild/netbsd-arm64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz#935c6c74e20f7224918fbe2e6c6fe865b6c6ea5b" integrity sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw== +"@esbuild/netbsd-x64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz#7a3a97d77abfd11765a72f1c6f9b18f5396bcc40" + integrity sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw== + "@esbuild/netbsd-x64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz#414677cef66d16c5a4d210751eb2881bb9c1b62b" integrity sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA== +"@esbuild/openbsd-arm64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz#58b00238dd8f123bfff68d3acc53a6ee369af89f" + integrity sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A== + "@esbuild/openbsd-arm64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz#8fd55a4d08d25cdc572844f13c88d678c84d13f7" integrity sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw== +"@esbuild/openbsd-x64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz#0ac843fda0feb85a93e288842936c21a00a8a205" + integrity sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA== + "@esbuild/openbsd-x64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz#0c48ddb1494bbc2d6bcbaa1429a7f465fa1dedde" integrity sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg== +"@esbuild/sunos-x64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz#8b7aa895e07828d36c422a4404cc2ecf27fb15c6" + integrity sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig== + "@esbuild/sunos-x64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz#86ff9075d77962b60dd26203d7352f92684c8c92" integrity sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg== +"@esbuild/win32-arm64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz#c023afb647cabf0c3ed13f0eddfc4f1d61c66a85" + integrity sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ== + "@esbuild/win32-arm64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz#849c62327c3229467f5b5cd681bf50588442e96c" integrity sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw== +"@esbuild/win32-ia32@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz#96c356132d2dda990098c8b8b951209c3cd743c2" + integrity sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA== + "@esbuild/win32-ia32@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz#f62eb480cd7cca088cb65bb46a6db25b725dc079" integrity sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA== +"@esbuild/win32-x64@0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz#34aa0b52d0fbb1a654b596acfa595f0c7b77a77b" + integrity sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg== + "@esbuild/win32-x64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz#c8e119a30a7c8d60b9d2e22d2073722dde3b710b" @@ -2775,6 +2900,13 @@ lodash.merge "^4.6.2" postcss-selector-parser "6.0.10" +"@tanstack/react-table@^8.21.3": + version "8.21.3" + resolved "https://registry.yarnpkg.com/@tanstack/react-table/-/react-table-8.21.3.tgz#2c38c747a5731c1a07174fda764b9c2b1fb5e91b" + integrity sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww== + dependencies: + "@tanstack/table-core" "8.21.3" + "@tanstack/react-virtual@^3.0.0-beta.60": version "3.13.0" resolved "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.0.tgz#f50bccdfbb792cb11fdc0342fd3ec6945c730389" @@ -2782,6 +2914,11 @@ dependencies: "@tanstack/virtual-core" "3.13.0" +"@tanstack/table-core@8.21.3": + version "8.21.3" + resolved "https://registry.yarnpkg.com/@tanstack/table-core/-/table-core-8.21.3.tgz#2977727d8fc8dfa079112d9f4d4c019110f1732c" + integrity sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg== + "@tanstack/virtual-core@3.13.0": version "3.13.0" resolved "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.0.tgz#8db0ccc9d6c32b6393551a6d19c87dbb259a8828" @@ -5918,7 +6055,38 @@ esbuild-register@^3.5.0: dependencies: debug "^4.3.4" -esbuild@0.25.0, "esbuild@^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0", esbuild@^0.25.0: +"esbuild@^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0": + version "0.24.2" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.24.2.tgz#b5b55bee7de017bff5fb8a4e3e44f2ebe2c3567d" + integrity sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA== + optionalDependencies: + "@esbuild/aix-ppc64" "0.24.2" + "@esbuild/android-arm" "0.24.2" + "@esbuild/android-arm64" "0.24.2" + "@esbuild/android-x64" "0.24.2" + "@esbuild/darwin-arm64" "0.24.2" + "@esbuild/darwin-x64" "0.24.2" + "@esbuild/freebsd-arm64" "0.24.2" + "@esbuild/freebsd-x64" "0.24.2" + "@esbuild/linux-arm" "0.24.2" + "@esbuild/linux-arm64" "0.24.2" + "@esbuild/linux-ia32" "0.24.2" + "@esbuild/linux-loong64" "0.24.2" + "@esbuild/linux-mips64el" "0.24.2" + "@esbuild/linux-ppc64" "0.24.2" + "@esbuild/linux-riscv64" "0.24.2" + "@esbuild/linux-s390x" "0.24.2" + "@esbuild/linux-x64" "0.24.2" + "@esbuild/netbsd-arm64" "0.24.2" + "@esbuild/netbsd-x64" "0.24.2" + "@esbuild/openbsd-arm64" "0.24.2" + "@esbuild/openbsd-x64" "0.24.2" + "@esbuild/sunos-x64" "0.24.2" + "@esbuild/win32-arm64" "0.24.2" + "@esbuild/win32-ia32" "0.24.2" + "@esbuild/win32-x64" "0.24.2" + +esbuild@^0.25.0: version "0.25.0" resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.25.0.tgz#0de1787a77206c5a79eeb634a623d39b5006ce92" integrity sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw== @@ -6254,6 +6422,11 @@ expand-template@^2.0.3: resolved "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== +export-to-csv@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/export-to-csv/-/export-to-csv-1.4.0.tgz#03fb42a4a4262cd03bde57a7b9bcad115149cf4b" + integrity sha512-6CX17Cu+rC2Fi2CyZ4CkgVG3hLl6BFsdAxfXiZkmDFIDY4mRx2y2spdeH6dqPHI9rP+AsHEfGeKz84Uuw7+Pmg== + express-ws@^5.0.2: version "5.0.2" resolved "https://registry.npmjs.org/express-ws/-/express-ws-5.0.2.tgz#5b02d41b937d05199c6c266d7cc931c823bda8eb" @@ -8379,7 +8552,7 @@ mz@^2.7.0: object-assign "^4.0.1" thenify-all "^1.0.0" -nanoid@3.3.8, nanoid@^3.3.6, nanoid@^3.3.8: +nanoid@^3.3.6, nanoid@^3.3.8: version "3.3.8" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf" integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w== From 3a6d0c11fbf750f4b5d4d24b61f0e33e98ca33a5 Mon Sep 17 00:00:00 2001 From: Vamsi Krishna <46787868+vamsikrishnamathala@users.noreply.github.com> Date: Tue, 13 May 2025 16:18:13 +0530 Subject: [PATCH 049/201] fix: set accordion to expand by default (#7053) --- .../sub-issues/issues-list/list-group.tsx | 2 +- .../sub-issues/issues-list/root.tsx | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/web/core/components/issues/issue-detail-widgets/sub-issues/issues-list/list-group.tsx b/web/core/components/issues/issue-detail-widgets/sub-issues/issues-list/list-group.tsx index adc6cb23a..b860646ba 100644 --- a/web/core/components/issues/issue-detail-widgets/sub-issues/issues-list/list-group.tsx +++ b/web/core/components/issues/issue-detail-widgets/sub-issues/issues-list/list-group.tsx @@ -45,7 +45,7 @@ export const SubIssuesListGroup: FC = observer((props) const isAllIssues = group.id === ALL_ISSUES; // states - const [isCollapsibleOpen, setIsCollapsibleOpen] = useState(isAllIssues); + const [isCollapsibleOpen, setIsCollapsibleOpen] = useState(true); if (!workItemIds.length) return null; diff --git a/web/core/components/issues/issue-detail-widgets/sub-issues/issues-list/root.tsx b/web/core/components/issues/issue-detail-widgets/sub-issues/issues-list/root.tsx index 2e88e0bc5..6b101829e 100644 --- a/web/core/components/issues/issue-detail-widgets/sub-issues/issues-list/root.tsx +++ b/web/core/components/issues/issue-detail-widgets/sub-issues/issues-list/root.tsx @@ -5,7 +5,7 @@ import { ListFilter } from "lucide-react"; import { EIssueServiceType, EIssuesStoreType } from "@plane/constants"; import { GroupByColumnTypes, TIssue, TIssueServiceType, TSubIssueOperations } from "@plane/types"; // hooks -import { Button } from "@plane/ui"; +import { Button, Loader } from "@plane/ui"; import { SectionEmptyState } from "@/components/empty-state"; import { getGroupByColumns, isWorkspaceLevel } from "@/components/issues/issue-layouts/utils"; import { useIssueDetail } from "@/hooks/store"; @@ -44,7 +44,7 @@ export const SubIssuesListRoot: React.FC = observer((props) => { // store hooks const { subIssues: { - subIssuesByIssueId, + subIssuesByIssueId, loader, filters: { getSubIssueFilters, getGroupedSubWorkItems, getFilteredSubWorkItems, resetFilters }, }, } = useIssueDetail(issueServiceType); @@ -77,6 +77,16 @@ export const SubIssuesListRoot: React.FC = observer((props) => { const isSubWorkItems = issueServiceType === EIssueServiceType.ISSUES; + if (loader === "init-loader") { + return ( + + {Array.from({ length: 5 }).map((_, index) => ( + + ))} + + ); + } + return (
{isRootLevel && filteredSubWorkItemsCount === 0 ? ( From 803f6cc62a40fda4023e489e024857fa12a327ce Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Tue, 13 May 2025 16:20:08 +0530 Subject: [PATCH 050/201] chore: yarn lock file updates --- yarn.lock | 172 +++--------------------------------------------------- 1 file changed, 8 insertions(+), 164 deletions(-) diff --git a/yarn.lock b/yarn.lock index 47ce955c8..d3211c449 100644 --- a/yarn.lock +++ b/yarn.lock @@ -257,9 +257,9 @@ "@babel/traverse" "^7.25.9" "@babel/types" "^7.25.9" -"@babel/helpers@^7.26.7": +"@babel/helpers@7.26.10", "@babel/helpers@^7.26.7": version "7.26.10" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.26.10.tgz#6baea3cd62ec2d0c1068778d63cb1314f6637384" + resolved "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.10.tgz#6baea3cd62ec2d0c1068778d63cb1314f6637384" integrity sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g== dependencies: "@babel/template" "^7.26.9" @@ -852,9 +852,9 @@ "@babel/plugin-transform-modules-commonjs" "^7.25.9" "@babel/plugin-transform-typescript" "^7.25.9" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.13", "@babel/runtime@^7.23.9", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": +"@babel/runtime@7.26.10", "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.13", "@babel/runtime@^7.23.9", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": version "7.26.10" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.10.tgz#a07b4d8fa27af131a633d7b3524db803eb4764c2" + resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz#a07b4d8fa27af131a633d7b3524db803eb4764c2" integrity sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw== dependencies: regenerator-runtime "^0.14.0" @@ -1124,251 +1124,126 @@ resolved "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz#5e13fac887f08c44f76b0ccaf3370eb00fec9bb6" integrity sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg== -"@esbuild/aix-ppc64@0.24.2": - version "0.24.2" - resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz#38848d3e25afe842a7943643cbcd387cc6e13461" - integrity sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA== - "@esbuild/aix-ppc64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz#499600c5e1757a524990d5d92601f0ac3ce87f64" integrity sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ== -"@esbuild/android-arm64@0.24.2": - version "0.24.2" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz#f592957ae8b5643129fa889c79e69cd8669bb894" - integrity sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg== - "@esbuild/android-arm64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz#b9b8231561a1dfb94eb31f4ee056b92a985c324f" integrity sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g== -"@esbuild/android-arm@0.24.2": - version "0.24.2" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.24.2.tgz#72d8a2063aa630308af486a7e5cbcd1e134335b3" - integrity sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q== - "@esbuild/android-arm@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.0.tgz#ca6e7888942505f13e88ac9f5f7d2a72f9facd2b" integrity sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g== -"@esbuild/android-x64@0.24.2": - version "0.24.2" - resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.24.2.tgz#9a7713504d5f04792f33be9c197a882b2d88febb" - integrity sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw== - "@esbuild/android-x64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.0.tgz#e765ea753bac442dfc9cb53652ce8bd39d33e163" integrity sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg== -"@esbuild/darwin-arm64@0.24.2": - version "0.24.2" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz#02ae04ad8ebffd6e2ea096181b3366816b2b5936" - integrity sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA== - "@esbuild/darwin-arm64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz#fa394164b0d89d4fdc3a8a21989af70ef579fa2c" integrity sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw== -"@esbuild/darwin-x64@0.24.2": - version "0.24.2" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz#9ec312bc29c60e1b6cecadc82bd504d8adaa19e9" - integrity sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA== - "@esbuild/darwin-x64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz#91979d98d30ba6e7d69b22c617cc82bdad60e47a" integrity sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg== -"@esbuild/freebsd-arm64@0.24.2": - version "0.24.2" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz#5e82f44cb4906d6aebf24497d6a068cfc152fa00" - integrity sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg== - "@esbuild/freebsd-arm64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz#b97e97073310736b430a07b099d837084b85e9ce" integrity sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w== -"@esbuild/freebsd-x64@0.24.2": - version "0.24.2" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz#3fb1ce92f276168b75074b4e51aa0d8141ecce7f" - integrity sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q== - "@esbuild/freebsd-x64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz#f3b694d0da61d9910ec7deff794d444cfbf3b6e7" integrity sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A== -"@esbuild/linux-arm64@0.24.2": - version "0.24.2" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz#856b632d79eb80aec0864381efd29de8fd0b1f43" - integrity sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg== - "@esbuild/linux-arm64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz#f921f699f162f332036d5657cad9036f7a993f73" integrity sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg== -"@esbuild/linux-arm@0.24.2": - version "0.24.2" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz#c846b4694dc5a75d1444f52257ccc5659021b736" - integrity sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA== - "@esbuild/linux-arm@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz#cc49305b3c6da317c900688995a4050e6cc91ca3" integrity sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg== -"@esbuild/linux-ia32@0.24.2": - version "0.24.2" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz#f8a16615a78826ccbb6566fab9a9606cfd4a37d5" - integrity sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw== - "@esbuild/linux-ia32@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz#3e0736fcfab16cff042dec806247e2c76e109e19" integrity sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg== -"@esbuild/linux-loong64@0.24.2": - version "0.24.2" - resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz#1c451538c765bf14913512c76ed8a351e18b09fc" - integrity sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ== - "@esbuild/linux-loong64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz#ea2bf730883cddb9dfb85124232b5a875b8020c7" integrity sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw== -"@esbuild/linux-mips64el@0.24.2": - version "0.24.2" - resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz#0846edeefbc3d8d50645c51869cc64401d9239cb" - integrity sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw== - "@esbuild/linux-mips64el@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz#4cababb14eede09248980a2d2d8b966464294ff1" integrity sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ== -"@esbuild/linux-ppc64@0.24.2": - version "0.24.2" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz#8e3fc54505671d193337a36dfd4c1a23b8a41412" - integrity sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw== - "@esbuild/linux-ppc64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz#8860a4609914c065373a77242e985179658e1951" integrity sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw== -"@esbuild/linux-riscv64@0.24.2": - version "0.24.2" - resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz#6a1e92096d5e68f7bb10a0d64bb5b6d1daf9a694" - integrity sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q== - "@esbuild/linux-riscv64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz#baf26e20bb2d38cfb86ee282dff840c04f4ed987" integrity sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA== -"@esbuild/linux-s390x@0.24.2": - version "0.24.2" - resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz#ab18e56e66f7a3c49cb97d337cd0a6fea28a8577" - integrity sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw== - "@esbuild/linux-s390x@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz#8323afc0d6cb1b6dc6e9fd21efd9e1542c3640a4" integrity sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA== -"@esbuild/linux-x64@0.24.2": - version "0.24.2" - resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz#8140c9b40da634d380b0b29c837a0b4267aff38f" - integrity sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q== - "@esbuild/linux-x64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz#08fcf60cb400ed2382e9f8e0f5590bac8810469a" integrity sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw== -"@esbuild/netbsd-arm64@0.24.2": - version "0.24.2" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz#65f19161432bafb3981f5f20a7ff45abb2e708e6" - integrity sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw== - "@esbuild/netbsd-arm64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz#935c6c74e20f7224918fbe2e6c6fe865b6c6ea5b" integrity sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw== -"@esbuild/netbsd-x64@0.24.2": - version "0.24.2" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz#7a3a97d77abfd11765a72f1c6f9b18f5396bcc40" - integrity sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw== - "@esbuild/netbsd-x64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz#414677cef66d16c5a4d210751eb2881bb9c1b62b" integrity sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA== -"@esbuild/openbsd-arm64@0.24.2": - version "0.24.2" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz#58b00238dd8f123bfff68d3acc53a6ee369af89f" - integrity sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A== - "@esbuild/openbsd-arm64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz#8fd55a4d08d25cdc572844f13c88d678c84d13f7" integrity sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw== -"@esbuild/openbsd-x64@0.24.2": - version "0.24.2" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz#0ac843fda0feb85a93e288842936c21a00a8a205" - integrity sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA== - "@esbuild/openbsd-x64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz#0c48ddb1494bbc2d6bcbaa1429a7f465fa1dedde" integrity sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg== -"@esbuild/sunos-x64@0.24.2": - version "0.24.2" - resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz#8b7aa895e07828d36c422a4404cc2ecf27fb15c6" - integrity sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig== - "@esbuild/sunos-x64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz#86ff9075d77962b60dd26203d7352f92684c8c92" integrity sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg== -"@esbuild/win32-arm64@0.24.2": - version "0.24.2" - resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz#c023afb647cabf0c3ed13f0eddfc4f1d61c66a85" - integrity sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ== - "@esbuild/win32-arm64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz#849c62327c3229467f5b5cd681bf50588442e96c" integrity sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw== -"@esbuild/win32-ia32@0.24.2": - version "0.24.2" - resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz#96c356132d2dda990098c8b8b951209c3cd743c2" - integrity sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA== - "@esbuild/win32-ia32@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz#f62eb480cd7cca088cb65bb46a6db25b725dc079" integrity sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA== -"@esbuild/win32-x64@0.24.2": - version "0.24.2" - resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz#34aa0b52d0fbb1a654b596acfa595f0c7b77a77b" - integrity sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg== - "@esbuild/win32-x64@0.25.0": version "0.25.0" resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz#c8e119a30a7c8d60b9d2e22d2073722dde3b710b" @@ -6055,40 +5930,9 @@ esbuild-register@^3.5.0: dependencies: debug "^4.3.4" -"esbuild@^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0": - version "0.24.2" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.24.2.tgz#b5b55bee7de017bff5fb8a4e3e44f2ebe2c3567d" - integrity sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA== - optionalDependencies: - "@esbuild/aix-ppc64" "0.24.2" - "@esbuild/android-arm" "0.24.2" - "@esbuild/android-arm64" "0.24.2" - "@esbuild/android-x64" "0.24.2" - "@esbuild/darwin-arm64" "0.24.2" - "@esbuild/darwin-x64" "0.24.2" - "@esbuild/freebsd-arm64" "0.24.2" - "@esbuild/freebsd-x64" "0.24.2" - "@esbuild/linux-arm" "0.24.2" - "@esbuild/linux-arm64" "0.24.2" - "@esbuild/linux-ia32" "0.24.2" - "@esbuild/linux-loong64" "0.24.2" - "@esbuild/linux-mips64el" "0.24.2" - "@esbuild/linux-ppc64" "0.24.2" - "@esbuild/linux-riscv64" "0.24.2" - "@esbuild/linux-s390x" "0.24.2" - "@esbuild/linux-x64" "0.24.2" - "@esbuild/netbsd-arm64" "0.24.2" - "@esbuild/netbsd-x64" "0.24.2" - "@esbuild/openbsd-arm64" "0.24.2" - "@esbuild/openbsd-x64" "0.24.2" - "@esbuild/sunos-x64" "0.24.2" - "@esbuild/win32-arm64" "0.24.2" - "@esbuild/win32-ia32" "0.24.2" - "@esbuild/win32-x64" "0.24.2" - -esbuild@^0.25.0: +esbuild@0.25.0, "esbuild@^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0", esbuild@^0.25.0: version "0.25.0" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.25.0.tgz#0de1787a77206c5a79eeb634a623d39b5006ce92" + resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz#0de1787a77206c5a79eeb634a623d39b5006ce92" integrity sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw== optionalDependencies: "@esbuild/aix-ppc64" "0.25.0" @@ -8552,9 +8396,9 @@ mz@^2.7.0: object-assign "^4.0.1" thenify-all "^1.0.0" -nanoid@^3.3.6, nanoid@^3.3.8: +nanoid@3.3.8, nanoid@^3.3.6, nanoid@^3.3.8: version "3.3.8" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf" + resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf" integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w== napi-build-utils@^2.0.0: From 4c3f7f27a508dcd3976748f2efd773918b4cc34d Mon Sep 17 00:00:00 2001 From: Manish Gupta <59428681+mguptahub@users.noreply.github.com> Date: Wed, 14 May 2025 10:02:21 +0530 Subject: [PATCH 051/201] fix: update API service startup check to use HTTP request instead of logs (#7054) --- deploy/selfhost/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/selfhost/install.sh b/deploy/selfhost/install.sh index 5ce2d5964..6e37b0aff 100755 --- a/deploy/selfhost/install.sh +++ b/deploy/selfhost/install.sh @@ -366,7 +366,7 @@ function startServices() { local api_container_id=$(docker container ls -q -f "name=$SERVICE_FOLDER-api") local idx2=0 - while ! docker logs $api_container_id 2>&1 | grep -m 1 -i "Application startup complete" | grep -q "."; + while ! docker exec $api_container_id python3 -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/')" > /dev/null 2>&1; do local message=">> Waiting for API Service to Start" local dots=$(printf '%*s' $idx2 | tr ' ' '.') From 080cf70e3f6f7359023129c23e2dd2a3724bd765 Mon Sep 17 00:00:00 2001 From: Manish Gupta <59428681+mguptahub@users.noreply.github.com> Date: Wed, 14 May 2025 12:33:53 +0530 Subject: [PATCH 052/201] refactor: Enhance backup and restore scripts for container data (#7055) * refactor: enhance backup and restore scripts for container data management * fix: ensure proper quoting in backup script to handle paths with spaces * fix: ensure backup directory is only removed if tar command succeeds * CodeRabbit fixes --- deploy/selfhost/install.sh | 70 ++++++++++++++++++++++++++------------ deploy/selfhost/restore.sh | 6 ++-- 2 files changed, 52 insertions(+), 24 deletions(-) diff --git a/deploy/selfhost/install.sh b/deploy/selfhost/install.sh index 6e37b0aff..9f0065f66 100755 --- a/deploy/selfhost/install.sh +++ b/deploy/selfhost/install.sh @@ -508,43 +508,69 @@ function viewLogs(){ echo "INVALID SERVICE NAME SUPPLIED" fi } -function backupSingleVolume() { - backupFolder=$1 - selectedVolume=$2 - # Backup data from Docker volume to the backup folder - # docker run --rm -v "$selectedVolume":/source -v "$backupFolder":/backup busybox sh -c 'cp -r /source/* /backup/' - local tobereplaced="plane-app_" - local replacewith="" +function backup_container_dir() { + local BACKUP_FOLDER=$1 + local CONTAINER_NAME=$2 + local CONTAINER_DATA_DIR=$3 + local SERVICE_FOLDER=$4 - local svcName="${selectedVolume//$tobereplaced/$replacewith}" + echo "Backing up $CONTAINER_NAME data..." + local CONTAINER_ID=$(/bin/bash -c "$COMPOSE_CMD -f $DOCKER_FILE_PATH ps -q $CONTAINER_NAME") + if [ -z "$CONTAINER_ID" ]; then + echo "Error: $CONTAINER_NAME container not found. Make sure the services are running." + return 1 + fi - docker run --rm \ - -e TAR_NAME="$svcName" \ - -v "$selectedVolume":/"$svcName" \ - -v "$backupFolder":/backup \ - busybox sh -c 'tar -czf "/backup/${TAR_NAME}.tar.gz" /${TAR_NAME}' + # Create a temporary directory for the backup + mkdir -p "$BACKUP_FOLDER/$SERVICE_FOLDER" + + # Copy the data directory from the running container + echo "Copying $CONTAINER_NAME data directory..." + docker cp -q "$CONTAINER_ID:$CONTAINER_DATA_DIR/." "$BACKUP_FOLDER/$SERVICE_FOLDER/" + local cp_status=$? + + if [ $cp_status -ne 0 ]; then + echo "Error: Failed to copy $SERVICE_FOLDER data" + rm -rf $BACKUP_FOLDER/$SERVICE_FOLDER + return 1 + fi + + # Create tar.gz of the data + cd "$BACKUP_FOLDER" + tar -czf "${SERVICE_FOLDER}.tar.gz" "$SERVICE_FOLDER/" + local tar_status=$? + if [ $tar_status -eq 0 ]; then + rm -rf "$SERVICE_FOLDER/" + fi + cd - > /dev/null + + if [ $tar_status -ne 0 ]; then + echo "Error: Failed to create tar archive" + return 1 + fi + + echo "Successfully backed up $SERVICE_FOLDER data" } + function backupData() { local datetime=$(date +"%Y%m%d-%H%M") local BACKUP_FOLDER=$PLANE_INSTALL_DIR/backup/$datetime mkdir -p "$BACKUP_FOLDER" - volumes=$(docker volume ls -f "name=$SERVICE_FOLDER" --format "{{.Name}}" | grep -E "_pgdata|_redisdata|_uploads") - # Check if there are any matching volumes - if [ -z "$volumes" ]; then - echo "No volumes found starting with '$SERVICE_FOLDER'" + # Check if docker-compose.yml exists + if [ ! -f "$DOCKER_FILE_PATH" ]; then + echo "Error: docker-compose.yml not found at $DOCKER_FILE_PATH" exit 1 fi - for vol in $volumes; do - echo "Backing Up $vol" - backupSingleVolume "$BACKUP_FOLDER" "$vol" - done + backup_container_dir "$BACKUP_FOLDER" "plane-db" "/var/lib/postgresql/data" "pgdata" || exit 1 + backup_container_dir "$BACKUP_FOLDER" "plane-minio" "/export" "uploads" || exit 1 + backup_container_dir "$BACKUP_FOLDER" "plane-mq" "/var/lib/rabbitmq" "rabbitmq_data" || exit 1 + backup_container_dir "$BACKUP_FOLDER" "plane-redis" "/data" "redisdata" || exit 1 echo "" echo "Backup completed successfully. Backup files are stored in $BACKUP_FOLDER" echo "" - } function askForAction() { local DEFAULT_ACTION=$1 diff --git a/deploy/selfhost/restore.sh b/deploy/selfhost/restore.sh index cd453b2a7..23b8de6cf 100755 --- a/deploy/selfhost/restore.sh +++ b/deploy/selfhost/restore.sh @@ -66,8 +66,10 @@ function restoreData() { exit 1 fi + local volume_suffix + volume_suffix="_pgdata|_redisdata|_uploads|_rabbitmq_data" local volumes - volumes=$(docker volume ls -f "name=plane-app" --format "{{.Name}}" | grep -E "_pgdata|_redisdata|_uploads") + volumes=$(docker volume ls -f "name=plane-app" --format "{{.Name}}" | grep -E "$volume_suffix") # Check if there are any matching volumes if [ -z "$volumes" ]; then echo ".....No volumes found starting with 'plane-app'" @@ -87,7 +89,7 @@ function restoreData() { echo "Found $BACKUP_FILE" local docVol - docVol=$(docker volume ls -f "name=$restoreVolName" --format "{{.Name}}" | grep -E "_pgdata|_redisdata|_uploads") + docVol=$(docker volume ls -f "name=$restoreVolName" --format "{{.Name}}" | grep -E "$volume_suffix") if [ -z "$docVol" ]; then echo "Skipping: No volume found with name $restoreVolName" From 534f5c7dd0c31bd801526414c395ade6f2aff144 Mon Sep 17 00:00:00 2001 From: Nikhil <118773738+pablohashescobar@users.noreply.github.com> Date: Wed, 14 May 2025 18:00:49 +0530 Subject: [PATCH 053/201] [WEB-4088] fix: issue exports when cycles are not present (#7057) * fix: issue exports when cycles are not present * fix: type check --- apiserver/plane/bgtasks/export_task.py | 146 ++++++++++++++++++------- 1 file changed, 106 insertions(+), 40 deletions(-) diff --git a/apiserver/plane/bgtasks/export_task.py b/apiserver/plane/bgtasks/export_task.py index 061167122..78210db64 100644 --- a/apiserver/plane/bgtasks/export_task.py +++ b/apiserver/plane/bgtasks/export_task.py @@ -3,10 +3,11 @@ import csv import io import json import zipfile - +from typing import List import boto3 from botocore.client import Config - +from uuid import UUID +from datetime import datetime, date # Third party imports from celery import shared_task @@ -20,21 +21,30 @@ from django.db.models import F, Prefetch from collections import defaultdict # Module imports -from plane.db.models import ExporterHistory, Issue, FileAsset, Label, User +from plane.db.models import ExporterHistory, Issue, FileAsset, Label, User, IssueComment from plane.utils.exception_logger import log_exception -def dateTimeConverter(time): +def dateTimeConverter(time: datetime) -> str | None: + """ + Convert a datetime object to a formatted string. + """ if time: return time.strftime("%a, %d %b %Y %I:%M:%S %Z%z") -def dateConverter(time): +def dateConverter(time: date) -> str | None: + """ + Convert a date object to a formatted string. + """ if time: return time.strftime("%a, %d %b %Y") -def create_csv_file(data): +def create_csv_file(data: List[List[str]]) -> str: + """ + Create a CSV file from the provided data. + """ csv_buffer = io.StringIO() csv_writer = csv.writer(csv_buffer, delimiter=",", quoting=csv.QUOTE_ALL) @@ -45,11 +55,17 @@ def create_csv_file(data): return csv_buffer.getvalue() -def create_json_file(data): +def create_json_file(data: List[dict]) -> str: + """ + Create a JSON file from the provided data. + """ return json.dumps(data) -def create_xlsx_file(data): +def create_xlsx_file(data: List[List[str]]) -> bytes: + """ + Create an XLSX file from the provided data. + """ workbook = Workbook() sheet = workbook.active @@ -62,7 +78,10 @@ def create_xlsx_file(data): return xlsx_buffer.getvalue() -def create_zip_file(files): +def create_zip_file(files: List[tuple[str, str | bytes]]) -> io.BytesIO: + """ + Create a ZIP file from the provided files. + """ zip_buffer = io.BytesIO() with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zipf: for filename, file_content in files: @@ -71,8 +90,11 @@ def create_zip_file(files): zip_buffer.seek(0) return zip_buffer - -def upload_to_s3(zip_file, workspace_id, token_id, slug): +# TODO: Change the upload_to_s3 function to use the new storage method with entry in file asset table +def upload_to_s3(zip_file: io.BytesIO, workspace_id: UUID, token_id: str, slug: str) -> None: + """ + Upload a ZIP file to S3 and generate a presigned URL. + """ file_name = ( f"{workspace_id}/export-{slug}-{token_id[:6]}-{str(timezone.now().date())}.zip" ) @@ -154,7 +176,10 @@ def upload_to_s3(zip_file, workspace_id, token_id, slug): exporter_instance.save(update_fields=["status", "url", "key"]) -def generate_table_row(issue): +def generate_table_row(issue: dict) -> List[str]: + """ + Generate a table row from an issue dictionary. + """ return [ f"""{issue["project_identifier"]}-{issue["sequence_id"]}""", issue["project_name"], @@ -166,22 +191,24 @@ def generate_table_row(issue): issue["priority"], issue["created_by"], ", ".join(issue["labels"]) if issue["labels"] else "", - issue.get("cycle_name", ""), - issue.get("cycle_start_date", ""), - issue.get("cycle_end_date", ""), + issue["cycle_name"], + issue["cycle_start_date"], + issue["cycle_end_date"], ", ".join(issue.get("module_name", "")) if issue.get("module_name") else "", dateTimeConverter(issue["created_at"]), dateTimeConverter(issue["updated_at"]), dateTimeConverter(issue["completed_at"]), dateTimeConverter(issue["archived_at"]), - ", ".join( - [ - f"{comment['comment']} ({comment['created_at']} by {comment['created_by']})" - for comment in issue["comments"] - ] - ) - if issue["comments"] - else "", + ( + ", ".join( + [ + f"{comment['comment']} ({comment['created_at']} by {comment['created_by']})" + for comment in issue["comments"] + ] + ) + if issue["comments"] + else "" + ), issue["estimate"] if issue["estimate"] else "", ", ".join(issue["link"]) if issue["link"] else "", ", ".join(issue["assignees"]) if issue["assignees"] else "", @@ -191,7 +218,10 @@ def generate_table_row(issue): ] -def generate_json_row(issue): +def generate_json_row(issue: dict) -> dict: + """ + Generate a JSON row from an issue dictionary. + """ return { "ID": f"""{issue["project_identifier"]}-{issue["sequence_id"]}""", "Project": issue["project_name"], @@ -221,7 +251,10 @@ def generate_json_row(issue): } -def update_json_row(rows, row): +def update_json_row(rows: List[dict], row: dict) -> None: + """ + Update the json row with the new assignee and label. + """ matched_index = next( ( index @@ -250,7 +283,10 @@ def update_json_row(rows, row): rows.append(row) -def update_table_row(rows, row): +def update_table_row(rows: List[List[str]], row: List[str]) -> None: + """ + Update the table row with the new assignee and label. + """ matched_index = next( (index for index, existing_row in enumerate(rows) if existing_row[0] == row[0]), None, @@ -272,20 +308,22 @@ def update_table_row(rows, row): rows.append(row) -def generate_csv(header, project_id, issues, files): +def generate_csv(header: List[str], project_id: str, issues: List[dict], files: List[tuple[str, str | bytes]]) -> None: """ Generate CSV export for all the passed issues. """ rows = [header] for issue in issues: row = generate_table_row(issue) - update_table_row(rows, row) csv_file = create_csv_file(rows) files.append((f"{project_id}.csv", csv_file)) -def generate_json(header, project_id, issues, files): +def generate_json(header: List[str], project_id: str, issues: List[dict], files: List[tuple[str, str | bytes]]) -> None: + """ + Generate JSON export for all the passed issues. + """ rows = [] for issue in issues: row = generate_json_row(issue) @@ -294,7 +332,10 @@ def generate_json(header, project_id, issues, files): files.append((f"{project_id}.json", json_file)) -def generate_xlsx(header, project_id, issues, files): +def generate_xlsx(header: List[str], project_id: str, issues: List[dict], files: List[tuple[str, str | bytes]]) -> None: + """ + Generate XLSX export for all the passed issues. + """ rows = [header] for issue in issues: row = generate_table_row(issue) @@ -304,13 +345,29 @@ def generate_xlsx(header, project_id, issues, files): files.append((f"{project_id}.xlsx", xlsx_file)) +def get_created_by(obj: Issue | IssueComment) -> str: + """ + Get the created by user for the given object. + """ + if obj.created_by: + return f"{obj.created_by.first_name} {obj.created_by.last_name}" + return "" + + @shared_task -def issue_export_task(provider, workspace_id, project_ids, token_id, multiple, slug): +def issue_export_task(provider: str, workspace_id: UUID, project_ids: List[str], token_id: str, multiple: bool, slug: str): + """ + Export issues from the workspace. + provider (str): The provider to export the issues to csv | json | xlsx. + token_id (str): The export object token id. + multiple (bool): Whether to export the issues to multiple files per project. + """ try: exporter_instance = ExporterHistory.objects.get(token=token_id) exporter_instance.status = "processing" exporter_instance.save(update_fields=["status"]) + # Base query to get the issues workspace_issues = ( Issue.objects.filter( workspace__id=workspace_id, @@ -348,16 +405,21 @@ def issue_export_task(provider, workspace_id, project_ids, token_id, multiple, s ) ) + # Get the attachments for the issues file_assets = FileAsset.objects.filter( - issue_id__in=workspace_issues.values_list("id", flat=True) + issue_id__in=workspace_issues.values_list("id", flat=True), + entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT ).annotate(work_item_id=F("issue_id"), asset_id=F("id")) + # Create a dictionary to store the attachments for the issues attachment_dict = defaultdict(list) for asset in file_assets: attachment_dict[asset.work_item_id].append(asset.asset_id) + # Create a list to store the issues data issues_data = [] + # Iterate over the issues for issue in workspace_issues: attachments = attachment_dict.get(issue.id, []) @@ -380,18 +442,18 @@ def issue_export_task(provider, workspace_id, project_ids, token_id, multiple, s "module_name": [ module.module.name for module in issue.issue_module.all() ], - "created_by": f"{issue.created_by.first_name} {issue.created_by.last_name}", + "created_by": get_created_by(issue), "labels": [label.name for label in issue.label_details], "comments": [ { "comment": comment.comment_stripped, "created_at": dateConverter(comment.created_at), - "created_by": f"{comment.created_by.first_name} {comment.created_by.last_name}", + "created_by": get_created_by(comment), } for comment in issue.issue_comments.all() ], "estimate": issue.estimate_point.estimate.name - if issue.estimate_point + if issue.estimate_point and issue.estimate_point.estimate else "", "link": [link.url for link in issue.issue_link.all()], "assignees": [ @@ -406,14 +468,17 @@ def issue_export_task(provider, workspace_id, project_ids, token_id, multiple, s ], } - # Get prefetched cycles and modules - cycles = list(issue.issue_cycle.all()) - - # Update cycle data - for cycle in cycles: + # Get Cycles data for the issue + cycle = issue.issue_cycle.last() + if cycle: + # Update cycle data issue_data["cycle_name"] = cycle.cycle.name issue_data["cycle_start_date"] = dateConverter(cycle.cycle.start_date) issue_data["cycle_end_date"] = dateConverter(cycle.cycle.end_date) + else: + issue_data["cycle_name"] = "" + issue_data["cycle_start_date"] = "" + issue_data["cycle_end_date"] = "" issues_data.append(issue_data) @@ -446,6 +511,7 @@ def issue_export_task(provider, workspace_id, project_ids, token_id, multiple, s "Attachment Links", ] + # Map the provider to the function EXPORTER_MAPPER = { "csv": generate_csv, "json": generate_json, From 084cc75726da3bcbc18e00c6697736730eb1461e Mon Sep 17 00:00:00 2001 From: JayashTripathy <76092296+JayashTripathy@users.noreply.github.com> Date: Wed, 14 May 2025 18:01:36 +0530 Subject: [PATCH 054/201] [WEB-4092] fix:broken detailed empty state layout #7056 --- web/core/components/empty-state/detailed-empty-state-root.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/web/core/components/empty-state/detailed-empty-state-root.tsx b/web/core/components/empty-state/detailed-empty-state-root.tsx index 887a1eb42..4ae97e839 100644 --- a/web/core/components/empty-state/detailed-empty-state-root.tsx +++ b/web/core/components/empty-state/detailed-empty-state-root.tsx @@ -85,7 +85,9 @@ export const DetailedEmptyState: React.FC = observer((props) => { {description &&

{description}

}
- {assetPath && {title}} + {assetPath && ( + {title} + )} {hasButtons && (
From ba158d5d6eec0fad036411d19b02d5732d640a91 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Fri, 16 May 2025 19:16:30 +0530 Subject: [PATCH 055/201] [WEB-4109] chore: remove analytics duration filter (#7073) * chore: remove analytics duration filter * removed subtitle from title and date_filter from service call * chore: removed the date filter * bottom text of insight trend card * chore: changed issue manager * fix: limited items in table * fix: removed unnecessary props from data-table --------- Co-authored-by: JayashTripathy Co-authored-by: NarayanBavisetti --- apiserver/plane/app/views/analytic/advance.py | 51 +++++++++++-------- apiserver/plane/utils/date_utils.py | 6 ++- .../analytics-v2/analytics-filter-actions.tsx | 4 +- .../analytics-section-wrapper.tsx | 2 +- .../components/analytics-v2/insight-card.tsx | 4 +- .../analytics-v2/insight-table/data-table.tsx | 4 -- .../overview/project-insights.tsx | 2 +- .../analytics-v2/total-insights.tsx | 2 +- .../work-items/created-vs-resolved.tsx | 2 +- .../work-items/priority-chart.tsx | 2 +- .../work-items/workitems-insight-table.tsx | 2 +- 11 files changed, 46 insertions(+), 35 deletions(-) diff --git a/apiserver/plane/app/views/analytic/advance.py b/apiserver/plane/app/views/analytic/advance.py index 9b258eca0..127d22acb 100644 --- a/apiserver/plane/app/views/analytic/advance.py +++ b/apiserver/plane/app/views/analytic/advance.py @@ -1,9 +1,10 @@ from rest_framework.response import Response from rest_framework import status from typing import Dict, List, Any -from datetime import timedelta from django.db.models import QuerySet, Q, Count from django.http import HttpRequest +from django.db.models.functions import TruncMonth +from django.utils import timezone from plane.app.views.base import BaseAPIView from plane.app.permissions import ROLE, allow_permission @@ -15,14 +16,10 @@ from plane.db.models import ( Module, IssueView, ProjectPage, + Workspace ) -from django.db.models import ( - Q, - Count, -) from plane.utils.build_chart import build_analytics_chart -from datetime import timedelta from plane.bgtasks.analytic_plot_export import export_analytics_to_csv_email from plane.utils.date_utils import ( get_analytics_filters, @@ -125,7 +122,7 @@ class AdvanceAnalyticsEndpoint(AdvanceAnalyticsBaseView): def get_work_items_stats(self) -> Dict[str, Dict[str, int]]: - base_queryset = Issue.objects.filter(**self.filters["base_filters"]) + base_queryset = Issue.issue_objects.filter(**self.filters["base_filters"]) return { "total_work_items": self.get_filtered_counts(base_queryset), @@ -263,6 +260,7 @@ class AdvanceAnalyticsChartEndpoint(AdvanceAnalyticsBaseView): "assignees", "labels", "issue_module__module", "issue_cycle__cycle" ) ) + # Apply date range filter if available if self.filters["chart_period_range"]: start_date, end_date = self.filters["chart_period_range"] @@ -270,41 +268,54 @@ class AdvanceAnalyticsChartEndpoint(AdvanceAnalyticsBaseView): created_at__date__gte=start_date, created_at__date__lte=end_date ) - # Get daily stats with optimized query - daily_stats = ( - queryset.values("created_at__date") + # Annotate by month and count + monthly_stats = ( + queryset.annotate(month=TruncMonth("created_at")) + .values("month") .annotate( created_count=Count("id"), completed_count=Count("id", filter=Q(completed_at__isnull=False)), ) - .order_by("created_at__date") + .order_by("month") ) - # Create a dictionary of existing stats with summed counts + # Create dictionary of month -> counts stats_dict = { - stat["created_at__date"].strftime("%Y-%m-%d"): { + stat["month"].strftime("%Y-%m-%d"): { "created_count": stat["created_count"], "completed_count": stat["completed_count"], } - for stat in daily_stats + for stat in monthly_stats } - # Generate data for all days in the range + # Generate monthly data (ensure months with 0 count are included) data = [] - current_date = start_date - while current_date <= end_date: - date_str = current_date.strftime("%Y-%m-%d") + workspace = Workspace.objects.get(slug=self._workspace_slug) + start_date = workspace.created_at.date().replace(day=1) + # include the current date at the end + end_date = timezone.now().date() + last_month = end_date.replace(day=1) + current_month = start_date + + while current_month <= last_month: + date_str = current_month.strftime("%Y-%m-%d") stats = stats_dict.get(date_str, {"created_count": 0, "completed_count": 0}) data.append( { "key": date_str, "name": date_str, - "count": stats["created_count"] + stats["completed_count"], + "count": stats[ + "created_count" + ], # <- Total created issues in that month "completed_issues": stats["completed_count"], "created_issues": stats["created_count"], } ) - current_date += timedelta(days=1) + # Move to next month + if current_month.month == 12: + current_month = current_month.replace(year=current_month.year + 1, month=1) + else: + current_month = current_month.replace(month=current_month.month + 1) schema = { "completed_issues": "completed_issues", diff --git a/apiserver/plane/utils/date_utils.py b/apiserver/plane/utils/date_utils.py index 86e6b9a3e..5726fbfd1 100644 --- a/apiserver/plane/utils/date_utils.py +++ b/apiserver/plane/utils/date_utils.py @@ -129,7 +129,7 @@ def get_chart_period_range( "last_3_months": (today - timedelta(days=90), today), } - return period_ranges.get(date_filter, period_ranges["last_7_days"]) + return period_ranges.get(date_filter, None) def get_analytics_filters( @@ -165,6 +165,8 @@ def get_analytics_filters( "workspace__slug": slug, "project__project_projectmember__member": user, "project__project_projectmember__is_active": True, + "project__deleted_at__isnull": True, + "project__archived_at__isnull": True, } # Project filters @@ -172,6 +174,8 @@ def get_analytics_filters( "workspace__slug": slug, "project_projectmember__member": user, "project_projectmember__is_active": True, + "project__deleted_at__isnull": True, + "project__archived_at__isnull": True, } # Add project IDs to filters if provided diff --git a/web/core/components/analytics-v2/analytics-filter-actions.tsx b/web/core/components/analytics-v2/analytics-filter-actions.tsx index b9b69bed9..aae166bf4 100644 --- a/web/core/components/analytics-v2/analytics-filter-actions.tsx +++ b/web/core/components/analytics-v2/analytics-filter-actions.tsx @@ -19,14 +19,14 @@ const AnalyticsFilterActions = observer(() => { }} projectIds={workspaceProjectIds} /> - { updateSelectedDuration(val); }} dropdownArrow - /> + /> */}
); }); diff --git a/web/core/components/analytics-v2/analytics-section-wrapper.tsx b/web/core/components/analytics-v2/analytics-section-wrapper.tsx index deb691644..2a3a17f6a 100644 --- a/web/core/components/analytics-v2/analytics-section-wrapper.tsx +++ b/web/core/components/analytics-v2/analytics-section-wrapper.tsx @@ -17,7 +17,7 @@ const AnalyticsSectionWrapper: React.FC = (props) => { {title && (

{title}

- {subtitle &&

• {subtitle}

} + {/* {subtitle &&

• {subtitle}

} */}
)} {actions} diff --git a/web/core/components/analytics-v2/insight-card.tsx b/web/core/components/analytics-v2/insight-card.tsx index cd22b7e92..2ce4c2fdc 100644 --- a/web/core/components/analytics-v2/insight-card.tsx +++ b/web/core/components/analytics-v2/insight-card.tsx @@ -30,12 +30,12 @@ const InsightCard = (props: InsightCardProps) => { {!isLoading ? (
{count}
- {percentage && ( + {/* {percentage && (
{versus &&
vs {versus}
}
- )} + )} */}
) : ( diff --git a/web/core/components/analytics-v2/insight-table/data-table.tsx b/web/core/components/analytics-v2/insight-table/data-table.tsx index c811c9265..61e02cb33 100644 --- a/web/core/components/analytics-v2/insight-table/data-table.tsx +++ b/web/core/components/analytics-v2/insight-table/data-table.tsx @@ -51,14 +51,10 @@ export function DataTable({ columns, data, searchPlaceholder, act rowSelection, columnFilters, }, - enableRowSelection: true, - onRowSelectionChange: setRowSelection, - onSortingChange: setSorting, onColumnFiltersChange: setColumnFilters, onColumnVisibilityChange: setColumnVisibility, getCoreRowModel: getCoreRowModel(), getFilteredRowModel: getFilteredRowModel(), - getPaginationRowModel: getPaginationRowModel(), getSortedRowModel: getSortedRowModel(), getFacetedRowModel: getFacetedRowModel(), getFacetedUniqueValues: getFacetedUniqueValues(), diff --git a/web/core/components/analytics-v2/overview/project-insights.tsx b/web/core/components/analytics-v2/overview/project-insights.tsx index a767cf476..fa156be6a 100644 --- a/web/core/components/analytics-v2/overview/project-insights.tsx +++ b/web/core/components/analytics-v2/overview/project-insights.tsx @@ -34,7 +34,7 @@ const ProjectInsights = observer(() => { `radar-chart-${workspaceSlug}-${selectedDuration}-${selectedProjects}`, () => analyticsV2Service.getAdvanceAnalyticsCharts[]>(workspaceSlug, "projects", { - date_filter: selectedDuration, + // date_filter: selectedDuration, ...(selectedProjects?.length > 0 && { project_ids: selectedProjects?.join(",") }), }) ); diff --git a/web/core/components/analytics-v2/total-insights.tsx b/web/core/components/analytics-v2/total-insights.tsx index ac8914e11..09998feaf 100644 --- a/web/core/components/analytics-v2/total-insights.tsx +++ b/web/core/components/analytics-v2/total-insights.tsx @@ -26,7 +26,7 @@ const TotalInsights: React.FC<{ analyticsType: TAnalyticsTabsV2Base; peekView?: `total-insights-${analyticsType}-${selectedDuration}-${selectedProjects}`, () => analyticsV2Service.getAdvanceAnalytics(workspaceSlug, analyticsType, { - date_filter: selectedDuration, + // date_filter: selectedDuration, ...(selectedProjects?.length > 0 ? { project_ids: selectedProjects.join(",") } : {}), }) ); diff --git a/web/core/components/analytics-v2/work-items/created-vs-resolved.tsx b/web/core/components/analytics-v2/work-items/created-vs-resolved.tsx index bd4673f46..2c95b916d 100644 --- a/web/core/components/analytics-v2/work-items/created-vs-resolved.tsx +++ b/web/core/components/analytics-v2/work-items/created-vs-resolved.tsx @@ -28,7 +28,7 @@ const CreatedVsResolved = observer(() => { `created-vs-resolved-${workspaceSlug}-${selectedDuration}-${selectedProjects}`, () => analyticsV2Service.getAdvanceAnalyticsCharts(workspaceSlug, "work-items", { - date_filter: selectedDuration, + // date_filter: selectedDuration, ...(selectedProjects?.length > 0 && { project_ids: selectedProjects?.join(",") }), }) ); diff --git a/web/core/components/analytics-v2/work-items/priority-chart.tsx b/web/core/components/analytics-v2/work-items/priority-chart.tsx index acf0b6cf9..5e81c7efa 100644 --- a/web/core/components/analytics-v2/work-items/priority-chart.tsx +++ b/web/core/components/analytics-v2/work-items/priority-chart.tsx @@ -57,7 +57,7 @@ const PriorityChart = observer((props: Props) => { `customized-insights-chart-${workspaceSlug}-${selectedDuration}-${selectedProjects}-${props.x_axis}-${props.y_axis}-${props.group_by}`, () => analyticsV2Service.getAdvanceAnalyticsCharts(workspaceSlug, "custom-work-items", { - date_filter: selectedDuration, + // date_filter: selectedDuration, ...(selectedProjects?.length > 0 && { project_ids: selectedProjects?.join(",") }), ...props, }) diff --git a/web/core/components/analytics-v2/work-items/workitems-insight-table.tsx b/web/core/components/analytics-v2/work-items/workitems-insight-table.tsx index cd3e7ae4e..85c93676b 100644 --- a/web/core/components/analytics-v2/work-items/workitems-insight-table.tsx +++ b/web/core/components/analytics-v2/work-items/workitems-insight-table.tsx @@ -30,7 +30,7 @@ const WorkItemsInsightTable = observer(() => { `insights-table-work-items-${workspaceSlug}-${selectedDuration}-${selectedProjects}`, () => analyticsV2Service.getAdvanceAnalyticsStats(workspaceSlug, "work-items", { - date_filter: selectedDuration, + // date_filter: selectedDuration, ...(selectedProjects?.length > 0 ? { project_ids: selectedProjects.join(",") } : {}), }) ); From 5b776392bd60c01f224b73e3254b25103460ec27 Mon Sep 17 00:00:00 2001 From: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com> Date: Sat, 17 May 2025 17:11:26 +0530 Subject: [PATCH 056/201] chore: revamped the analytics for cycle and module in peek view. (#7075) * chore: added cycles and modules in analytics peek view * chore: added cycles and modules analytics * chore: added project filter for work items * chore: added a peekview flag and based on that table columns * chore: added peek view * chore: added check for display name * chore: cleaned up some code * chore: fixed export csv data * chore: added distinct work items * chore: assignee in peek view * updated csv fields * chore: updated workitems peek with assignee * fix: removed type assersions for workspaceslug * chore: added day wise filter in cycles and modules * chore: added extra validations --------- Co-authored-by: JayashTripathy --- apiserver/plane/app/urls/analytic.py | 6 - apiserver/plane/app/views/__init__.py | 1 - apiserver/plane/app/views/analytic/advance.py | 409 ++++++++++++------ apiserver/plane/utils/date_utils.py | 4 +- packages/types/src/analytics-v2.d.ts | 51 ++- .../[projectId]/cycles/(detail)/header.tsx | 5 +- .../cycles/(detail)/mobile-header.tsx | 5 +- .../[projectId]/modules/(detail)/header.tsx | 5 +- .../modules/(detail)/mobile-header.tsx | 4 +- .../analytics-v2/insight-table/root.tsx | 4 +- .../overview/project-insights.tsx | 10 +- .../analytics-v2/total-insights.tsx | 10 +- .../work-items/created-vs-resolved.tsx | 10 +- .../analytics-v2/work-items/modal/content.tsx | 50 ++- .../analytics-v2/work-items/modal/header.tsx | 13 +- .../analytics-v2/work-items/modal/index.tsx | 15 +- .../work-items/priority-chart.tsx | 31 +- .../work-items/workitems-insight-table.tsx | 97 +++-- web/core/components/analytics/old-page.tsx | 107 +++++ web/core/store/analytics-v2.store.ts | 39 +- web/core/store/root.store.ts | 2 +- 21 files changed, 642 insertions(+), 236 deletions(-) create mode 100644 web/core/components/analytics/old-page.tsx diff --git a/apiserver/plane/app/urls/analytic.py b/apiserver/plane/app/urls/analytic.py index c6f024f75..0eebd3108 100644 --- a/apiserver/plane/app/urls/analytic.py +++ b/apiserver/plane/app/urls/analytic.py @@ -11,7 +11,6 @@ from plane.app.views import ( AdvanceAnalyticsChartEndpoint, DefaultAnalyticsEndpoint, ProjectStatsEndpoint, - AdvanceAnalyticsExportEndpoint, ) @@ -68,9 +67,4 @@ urlpatterns = [ AdvanceAnalyticsChartEndpoint.as_view(), name="advance-analytics-chart", ), - path( - "workspaces//advance-analytics-export/", - AdvanceAnalyticsExportEndpoint.as_view(), - name="advance-analytics-export", - ), ] diff --git a/apiserver/plane/app/views/__init__.py b/apiserver/plane/app/views/__init__.py index a3c72f370..2034c5548 100644 --- a/apiserver/plane/app/views/__init__.py +++ b/apiserver/plane/app/views/__init__.py @@ -203,7 +203,6 @@ from .analytic.advance import ( AdvanceAnalyticsEndpoint, AdvanceAnalyticsStatsEndpoint, AdvanceAnalyticsChartEndpoint, - AdvanceAnalyticsExportEndpoint, ) from .notification.base import ( diff --git a/apiserver/plane/app/views/analytic/advance.py b/apiserver/plane/app/views/analytic/advance.py index 127d22acb..b6c5f1e0b 100644 --- a/apiserver/plane/app/views/analytic/advance.py +++ b/apiserver/plane/app/views/analytic/advance.py @@ -5,7 +5,7 @@ from django.db.models import QuerySet, Q, Count from django.http import HttpRequest from django.db.models.functions import TruncMonth from django.utils import timezone - +from datetime import timedelta from plane.app.views.base import BaseAPIView from plane.app.permissions import ROLE, allow_permission from plane.db.models import ( @@ -16,19 +16,18 @@ from plane.db.models import ( Module, IssueView, ProjectPage, - Workspace + Workspace, + CycleIssue, + ModuleIssue, ) - +from django.db import models +from django.db.models import F, Case, When, Value +from django.db.models.functions import Concat from plane.utils.build_chart import build_analytics_chart -from plane.bgtasks.analytic_plot_export import export_analytics_to_csv_email from plane.utils.date_utils import ( get_analytics_filters, ) -from plane.utils.build_chart import build_analytics_chart -from plane.bgtasks.analytic_plot_export import export_analytics_to_csv_email -from plane.utils.date_utils import get_analytics_filters - class AdvanceAnalyticsBaseView(BaseAPIView): def initialize_workspace(self, slug: str, type: str) -> None: @@ -73,7 +72,7 @@ class AdvanceAnalyticsEndpoint(AdvanceAnalyticsBaseView): return { "count": get_filtered_count(), - "filter_count": get_previous_count(), + # "filter_count": get_previous_count(), } def get_overview_data(self) -> Dict[str, Dict[str, int]]: @@ -120,9 +119,25 @@ class AdvanceAnalyticsEndpoint(AdvanceAnalyticsBaseView): ), } - - def get_work_items_stats(self) -> Dict[str, Dict[str, int]]: - base_queryset = Issue.issue_objects.filter(**self.filters["base_filters"]) + def get_work_items_stats( + self, cycle_id=None, module_id=None + ) -> Dict[str, Dict[str, int]]: + """ + Returns work item stats for the workspace, or filtered by cycle_id or module_id if provided. + """ + base_queryset = None + if cycle_id is not None: + cycle_issues = CycleIssue.objects.filter( + **self.filters["base_filters"], cycle_id=cycle_id + ).values_list("issue_id", flat=True) + base_queryset = Issue.issue_objects.filter(id__in=cycle_issues) + elif module_id is not None: + module_issues = ModuleIssue.objects.filter( + **self.filters["base_filters"], module_id=module_id + ).values_list("issue_id", flat=True) + base_queryset = Issue.issue_objects.filter(id__in=module_issues) + else: + base_queryset = Issue.issue_objects.filter(**self.filters["base_filters"]) return { "total_work_items": self.get_filtered_counts(base_queryset), @@ -150,13 +165,14 @@ class AdvanceAnalyticsEndpoint(AdvanceAnalyticsBaseView): self.get_overview_data(), status=status.HTTP_200_OK, ) - elif tab == "work-items": + # Optionally accept cycle_id or module_id as query params + cycle_id = request.GET.get("cycle_id", None) + module_id = request.GET.get("module_id", None) return Response( - self.get_work_items_stats(), + self.get_work_items_stats(cycle_id=cycle_id, module_id=module_id), status=status.HTTP_200_OK, ) - return Response({"message": "Invalid tab"}, status=status.HTTP_400_BAD_REQUEST) @@ -184,14 +200,100 @@ class AdvanceAnalyticsStatsEndpoint(AdvanceAnalyticsBaseView): .order_by("project_id") ) + def get_work_items_stats( + self, cycle_id=None, module_id=None, peek_view=False + ) -> Dict[str, Dict[str, int]]: + base_queryset = None + if cycle_id is not None: + cycle_issues = CycleIssue.objects.filter( + **self.filters["base_filters"], cycle_id=cycle_id + ).values_list("issue_id", flat=True) + base_queryset = Issue.issue_objects.filter(id__in=cycle_issues) + elif module_id is not None: + module_issues = ModuleIssue.objects.filter( + **self.filters["base_filters"], module_id=module_id + ).values_list("issue_id", flat=True) + base_queryset = Issue.issue_objects.filter(id__in=module_issues) + elif peek_view: + base_queryset = Issue.issue_objects.filter(**self.filters["base_filters"]) + else: + base_queryset = Issue.issue_objects.filter(**self.filters["base_filters"]) + return ( + base_queryset.values("project_id", "project__name") + .annotate( + cancelled_work_items=Count( + "id", filter=Q(state__group="cancelled") + ), + completed_work_items=Count( + "id", filter=Q(state__group="completed") + ), + backlog_work_items=Count("id", filter=Q(state__group="backlog")), + un_started_work_items=Count( + "id", filter=Q(state__group="unstarted") + ), + started_work_items=Count("id", filter=Q(state__group="started")), + ) + .order_by("project_id") + ) + + return ( + base_queryset.annotate(display_name=F("assignees__display_name")) + .annotate(assignee_id=F("assignees__id")) + .annotate(avatar=F("assignees__avatar")) + .annotate( + avatar_url=Case( + # If `avatar_asset` exists, use it to generate the asset URL + When( + assignees__avatar_asset__isnull=False, + then=Concat( + Value("/api/assets/v2/static/"), + "assignees__avatar_asset", # Assuming avatar_asset has an id or relevant field + Value("/"), + ), + ), + # If `avatar_asset` is None, fall back to using `avatar` field directly + When( + assignees__avatar_asset__isnull=True, then="assignees__avatar" + ), + default=Value(None), + output_field=models.CharField(), + ) + ) + .values("display_name", "assignee_id", "avatar_url") + .annotate( + cancelled_work_items=Count( + "id", filter=Q(state__group="cancelled"), distinct=True + ), + completed_work_items=Count( + "id", filter=Q(state__group="completed"), distinct=True + ), + backlog_work_items=Count( + "id", filter=Q(state__group="backlog"), distinct=True + ), + un_started_work_items=Count( + "id", filter=Q(state__group="unstarted"), distinct=True + ), + started_work_items=Count( + "id", filter=Q(state__group="started"), distinct=True + ), + ) + .order_by("display_name") + ) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE") def get(self, request: HttpRequest, slug: str) -> Response: self.initialize_workspace(slug, type="chart") type = request.GET.get("type", "work-items") if type == "work-items": + # Optionally accept cycle_id or module_id as query params + cycle_id = request.GET.get("cycle_id", None) + module_id = request.GET.get("module_id", None) + peek_view = request.GET.get("peek_view", False) return Response( - self.get_project_issues_stats(), + self.get_work_items_stats( + cycle_id=cycle_id, module_id=module_id, peek_view=peek_view + ), status=status.HTTP_200_OK, ) @@ -251,7 +353,9 @@ class AdvanceAnalyticsChartEndpoint(AdvanceAnalyticsBaseView): for key, value in data.items() ] - def work_item_completion_chart(self) -> Dict[str, Any]: + def work_item_completion_chart( + self, cycle_id=None, module_id=None, peek_view=False + ) -> Dict[str, Any]: # Get the base queryset queryset = ( Issue.issue_objects.filter(**self.filters["base_filters"]) @@ -261,61 +365,143 @@ class AdvanceAnalyticsChartEndpoint(AdvanceAnalyticsBaseView): ) ) - # Apply date range filter if available - if self.filters["chart_period_range"]: - start_date, end_date = self.filters["chart_period_range"] - queryset = queryset.filter( - created_at__date__gte=start_date, created_at__date__lte=end_date - ) - - # Annotate by month and count - monthly_stats = ( - queryset.annotate(month=TruncMonth("created_at")) - .values("month") - .annotate( - created_count=Count("id"), - completed_count=Count("id", filter=Q(completed_at__isnull=False)), - ) - .order_by("month") - ) - - # Create dictionary of month -> counts - stats_dict = { - stat["month"].strftime("%Y-%m-%d"): { - "created_count": stat["created_count"], - "completed_count": stat["completed_count"], - } - for stat in monthly_stats - } - - # Generate monthly data (ensure months with 0 count are included) - data = [] - workspace = Workspace.objects.get(slug=self._workspace_slug) - start_date = workspace.created_at.date().replace(day=1) - # include the current date at the end - end_date = timezone.now().date() - last_month = end_date.replace(day=1) - current_month = start_date - - while current_month <= last_month: - date_str = current_month.strftime("%Y-%m-%d") - stats = stats_dict.get(date_str, {"created_count": 0, "completed_count": 0}) - data.append( - { - "key": date_str, - "name": date_str, - "count": stats[ - "created_count" - ], # <- Total created issues in that month - "completed_issues": stats["completed_count"], - "created_issues": stats["created_count"], - } - ) - # Move to next month - if current_month.month == 12: - current_month = current_month.replace(year=current_month.year + 1, month=1) + if cycle_id is not None and peek_view: + cycle_issues = CycleIssue.objects.filter( + **self.filters["base_filters"], cycle_id=cycle_id + ).values_list("issue_id", flat=True) + cycle = Cycle.objects.filter(id=cycle_id).first() + if cycle and cycle.start_date: + start_date = cycle.start_date.date() + end_date = cycle.end_date.date() else: - current_month = current_month.replace(month=current_month.month + 1) + return {"data": [], "schema": {}} + queryset = cycle_issues + elif module_id is not None and peek_view: + module_issues = ModuleIssue.objects.filter( + **self.filters["base_filters"], module_id=module_id + ).values_list("issue_id", flat=True) + module = Module.objects.filter(id=module_id).first() + if module and module.start_date: + start_date = module.start_date + end_date = module.target_date + else: + return {"data": [], "schema": {}} + queryset = module_issues + elif peek_view: + project_ids_str = self.request.GET.get("project_ids") + if project_ids_str: + project_id_list = [ + pid.strip() for pid in project_ids_str.split(",") if pid.strip() + ] + else: + project_id_list = [] + return {"data": [], "schema": {}} + project_id = project_id_list[0] + project = Project.objects.filter(id=project_id).first() + if project.created_at: + start_date = project.created_at.date().replace(day=1) + else: + return {"data": [], "schema": {}} + else: + workspace = Workspace.objects.get(slug=self._workspace_slug) + start_date = workspace.created_at.date().replace(day=1) + + if cycle_id or module_id: + # Get daily stats with optimized query + daily_stats = ( + queryset.values("created_at__date") + .annotate( + created_count=Count("id"), + completed_count=Count( + "id", filter=Q(issue__state__group="completed") + ), + ) + .order_by("created_at__date") + ) + + # Create a dictionary of existing stats with summed counts + stats_dict = { + stat["created_at__date"].strftime("%Y-%m-%d"): { + "created_count": stat["created_count"], + "completed_count": stat["completed_count"], + } + for stat in daily_stats + } + + # Generate data for all days in the range + data = [] + current_date = start_date + while current_date <= end_date: + date_str = current_date.strftime("%Y-%m-%d") + stats = stats_dict.get( + date_str, {"created_count": 0, "completed_count": 0} + ) + data.append( + { + "key": date_str, + "name": date_str, + "count": stats["created_count"] + stats["completed_count"], + "completed_issues": stats["completed_count"], + "created_issues": stats["created_count"], + } + ) + current_date += timedelta(days=1) + else: + # Apply date range filter if available + if self.filters["chart_period_range"]: + start_date, end_date = self.filters["chart_period_range"] + queryset = queryset.filter( + created_at__date__gte=start_date, created_at__date__lte=end_date + ) + + # Annotate by month and count + monthly_stats = ( + queryset.annotate(month=TruncMonth("created_at")) + .values("month") + .annotate( + created_count=Count("id"), + completed_count=Count("id", filter=Q(state__group="completed")), + ) + .order_by("month") + ) + + # Create dictionary of month -> counts + stats_dict = { + stat["month"].strftime("%Y-%m-%d"): { + "created_count": stat["created_count"], + "completed_count": stat["completed_count"], + } + for stat in monthly_stats + } + + # Generate monthly data (ensure months with 0 count are included) + data = [] + # include the current date at the end + end_date = timezone.now().date() + last_month = end_date.replace(day=1) + current_month = start_date + + while current_month <= last_month: + date_str = current_month.strftime("%Y-%m-%d") + stats = stats_dict.get( + date_str, {"created_count": 0, "completed_count": 0} + ) + data.append( + { + "key": date_str, + "name": date_str, + "count": stats["created_count"], + "completed_issues": stats["completed_count"], + "created_issues": stats["created_count"], + } + ) + # Move to next month + if current_month.month == 12: + current_month = current_month.replace( + year=current_month.year + 1, month=1 + ) + else: + current_month = current_month.replace(month=current_month.month + 1) schema = { "completed_issues": "completed_issues", @@ -330,12 +516,13 @@ class AdvanceAnalyticsChartEndpoint(AdvanceAnalyticsBaseView): type = request.GET.get("type", "projects") group_by = request.GET.get("group_by", None) x_axis = request.GET.get("x_axis", "PRIORITY") + cycle_id = request.GET.get("cycle_id", None) + module_id = request.GET.get("module_id", None) if type == "projects": return Response(self.project_chart(), status=status.HTTP_200_OK) elif type == "custom-work-items": - # Get the base queryset queryset = ( Issue.issue_objects.filter(**self.filters["base_filters"]) .select_related("workspace", "state", "parent") @@ -344,6 +531,19 @@ class AdvanceAnalyticsChartEndpoint(AdvanceAnalyticsBaseView): ) ) + # Apply cycle/module filters if present + if cycle_id is not None: + cycle_issues = CycleIssue.objects.filter( + **self.filters["base_filters"], cycle_id=cycle_id + ).values_list("issue_id", flat=True) + queryset = queryset.filter(id__in=cycle_issues) + + elif module_id is not None: + module_issues = ModuleIssue.objects.filter( + **self.filters["base_filters"], module_id=module_id + ).values_list("issue_id", flat=True) + queryset = queryset.filter(id__in=module_issues) + # Apply date range filter if available if self.filters["chart_period_range"]: start_date, end_date = self.filters["chart_period_range"] @@ -357,66 +557,15 @@ class AdvanceAnalyticsChartEndpoint(AdvanceAnalyticsBaseView): ) elif type == "work-items": + # Optionally accept cycle_id or module_id as query params + cycle_id = request.GET.get("cycle_id", None) + module_id = request.GET.get("module_id", None) + peek_view = request.GET.get("peek_view", False) return Response( - self.work_item_completion_chart(), + self.work_item_completion_chart( + cycle_id=cycle_id, module_id=module_id, peek_view=peek_view + ), status=status.HTTP_200_OK, ) return Response({"message": "Invalid type"}, status=status.HTTP_400_BAD_REQUEST) - - -class AdvanceAnalyticsExportEndpoint(AdvanceAnalyticsBaseView): - @allow_permission([ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE") - def post(self, request: HttpRequest, slug: str) -> Response: - self.initialize_workspace(slug, type="chart") - queryset = Issue.issue_objects.filter(**self.filters["base_filters"]) - - # Apply date range filter if available - if self.filters["chart_period_range"]: - start_date, end_date = self.filters["chart_period_range"] - queryset = queryset.filter( - created_at__date__gte=start_date, created_at__date__lte=end_date - ) - - queryset = ( - queryset.values("project_id", "project__name") - .annotate( - cancelled_work_items=Count("id", filter=Q(state__group="cancelled")), - completed_work_items=Count("id", filter=Q(state__group="completed")), - backlog_work_items=Count("id", filter=Q(state__group="backlog")), - un_started_work_items=Count("id", filter=Q(state__group="unstarted")), - started_work_items=Count("id", filter=Q(state__group="started")), - ) - .order_by("project_id") - ) - - # Convert QuerySet to list of dictionaries for serialization - serialized_data = list(queryset) - - headers = [ - "Projects", - "Completed Issues", - "Backlog Issues", - "Unstarted Issues", - "Started Issues", - ] - - keys = [ - "project__name", - "completed_work_items", - "backlog_work_items", - "un_started_work_items", - "started_work_items", - ] - - email = request.user.email - - # Send serialized data to background task - export_analytics_to_csv_email.delay(serialized_data, headers, keys, email, slug) - - return Response( - { - "message": f"Once the export is ready it will be emailed to you at {str(email)}" - }, - status=status.HTTP_200_OK, - ) diff --git a/apiserver/plane/utils/date_utils.py b/apiserver/plane/utils/date_utils.py index 5726fbfd1..4225e70b5 100644 --- a/apiserver/plane/utils/date_utils.py +++ b/apiserver/plane/utils/date_utils.py @@ -174,8 +174,8 @@ def get_analytics_filters( "workspace__slug": slug, "project_projectmember__member": user, "project_projectmember__is_active": True, - "project__deleted_at__isnull": True, - "project__archived_at__isnull": True, + "deleted_at__isnull": True, + "archived_at__isnull": True, } # Add project IDs to filters if provided diff --git a/packages/types/src/analytics-v2.d.ts b/packages/types/src/analytics-v2.d.ts index 176cd1191..1a8652b70 100644 --- a/packages/types/src/analytics-v2.d.ts +++ b/packages/types/src/analytics-v2.d.ts @@ -1,52 +1,55 @@ import { ChartXAxisProperty, ChartYAxisMetric } from "@plane/constants"; import { TChartData } from "./charts"; -export type TAnalyticsTabsV2Base = "overview" | "work-items" -export type TAnalyticsGraphsV2Base = "projects" | "work-items" | "custom-work-items" - +export type TAnalyticsTabsV2Base = "overview" | "work-items"; +export type TAnalyticsGraphsV2Base = "projects" | "work-items" | "custom-work-items"; // service types export interface IAnalyticsResponseV2 { - [key: string]: any; + [key: string]: any; } export interface IAnalyticsResponseFieldsV2 { - count: number; - filter_count: number; + count: number; + filter_count: number; } export interface IAnalyticsRadarEntityV2 { - key: string, - name: string, - count: number + key: string; + name: string; + count: number; } // chart types export interface IChartResponseV2 { - schema: Record; - data: TChartData[]; + schema: Record; + data: TChartData[]; } // table types export interface WorkItemInsightColumns { - project_id: string; - project__name: string; - cancelled_work_items: number; - completed_work_items: number; - backlog_work_items: number; - un_started_work_items: number; - started_work_items: number; + project_id?: string; + project__name?: string; + cancelled_work_items: number; + completed_work_items: number; + backlog_work_items: number; + un_started_work_items: number; + started_work_items: number; + // because of the peek view, we will display the name of the project instead of project__name + display_name?: string; + avatar_url?: string; + assignee_id?: string; } export type AnalyticsTableDataMap = { - "work-items": WorkItemInsightColumns, -} + "work-items": WorkItemInsightColumns; +}; export interface IAnalyticsV2Params { - x_axis: ChartXAxisProperty; - y_axis: ChartYAxisMetric; - group_by?: ChartXAxisProperty; -} \ No newline at end of file + x_axis: ChartXAxisProperty; + y_axis: ChartYAxisMetric; + group_by?: ChartXAxisProperty; +} diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/header.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/header.tsx index ebe492584..07dd0fc4d 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/header.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/header.tsx @@ -27,7 +27,7 @@ import { // ui import { Breadcrumbs, Button, ContrastIcon, Tooltip, Header, CustomSearchSelect } from "@plane/ui"; // components -import { ProjectAnalyticsModal } from "@/components/analytics"; +import { WorkItemsModal } from "@/components/analytics-v2/work-items/modal"; import { BreadcrumbLink, SwitcherLabel } from "@/components/common"; import { CycleQuickActions } from "@/components/cycles"; import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues"; @@ -161,7 +161,8 @@ export const CycleIssuesHeader: React.FC = observer(() => { return ( <> - setAnalyticsModal(false)} cycleDetails={cycleDetails ?? undefined} diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/mobile-header.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/mobile-header.tsx index 31eb5b249..4efa986b4 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/mobile-header.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/mobile-header.tsx @@ -19,7 +19,7 @@ import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOption // ui import { CustomMenu } from "@plane/ui"; // components -import { ProjectAnalyticsModal } from "@/components/analytics"; +import { WorkItemsModal } from "@/components/analytics-v2/work-items/modal"; import { DisplayFiltersSelection, FilterSelection, FiltersDropdown, IssueLayoutIcon } from "@/components/issues"; // helpers import { isIssueFilterActive } from "@/helpers/filter.helper"; @@ -123,7 +123,8 @@ export const CycleIssuesMobileHeader = () => { return ( <> - setAnalyticsModal(false)} cycleDetails={cycleDetails ?? undefined} diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/header.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/header.tsx index 6bbbb29a0..6830b5532 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/header.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/header.tsx @@ -25,7 +25,7 @@ import { // ui import { Breadcrumbs, Button, DiceIcon, Tooltip, Header, CustomSearchSelect } from "@plane/ui"; // components -import { ProjectAnalyticsModal } from "@/components/analytics"; +import { WorkItemsModal } from "@/components/analytics-v2/work-items/modal"; import { BreadcrumbLink, SwitcherLabel } from "@/components/common"; import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues"; // helpers @@ -155,10 +155,11 @@ export const ModuleIssuesHeader: React.FC = observer(() => { return ( <> - setAnalyticsModal(false)} moduleDetails={moduleDetails ?? undefined} + projectDetails={currentProjectDetails} />
diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/mobile-header.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/mobile-header.tsx index 1f000bae2..741fe3b53 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/mobile-header.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/mobile-header.tsx @@ -21,6 +21,7 @@ import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOption import { CustomMenu } from "@plane/ui"; // components import { ProjectAnalyticsModal } from "@/components/analytics"; +import { WorkItemsModal } from "@/components/analytics-v2/work-items/modal"; import { DisplayFiltersSelection, FilterSelection, @@ -106,10 +107,11 @@ export const ModuleIssuesMobileHeader = observer(() => { return (
- setAnalyticsModal(false)} moduleDetails={moduleDetails ?? undefined} + projectDetails={currentProjectDetails} />
const { data, isLoading, columns, columnsLabels } = props; const params = useParams(); const { t } = useTranslation(); - const workspaceSlug = params.workspaceSlug as string; + const workspaceSlug = params.workspaceSlug.toString(); if (isLoading) { return ; } @@ -35,7 +35,7 @@ export const InsightTable = const exportCSV = (rows: Row[]) => { const rowData: any = rows.map((row) => { - const { project_id, ...exportableData } = row.original; + const { project_id, avatar_url, assignee_id, ...exportableData } = row.original; return Object.fromEntries( Object.entries(exportableData).map(([key, value]) => { if (columnsLabels?.[key]) { diff --git a/web/core/components/analytics-v2/overview/project-insights.tsx b/web/core/components/analytics-v2/overview/project-insights.tsx index fa156be6a..83054a885 100644 --- a/web/core/components/analytics-v2/overview/project-insights.tsx +++ b/web/core/components/analytics-v2/overview/project-insights.tsx @@ -26,16 +26,20 @@ const analyticsV2Service = new AnalyticsV2Service(); const ProjectInsights = observer(() => { const params = useParams(); const { t } = useTranslation(); - const workspaceSlug = params.workspaceSlug as string; - const { selectedDuration, selectedDurationLabel, selectedProjects } = useAnalyticsV2(); + const workspaceSlug = params.workspaceSlug.toString(); + const { selectedDuration, selectedDurationLabel, selectedProjects, selectedCycle, selectedModule, isPeekView } = + useAnalyticsV2(); const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/analytics-v2/empty-chart-radar" }); const { data: projectInsightsData, isLoading: isLoadingProjectInsight } = useSWR( - `radar-chart-${workspaceSlug}-${selectedDuration}-${selectedProjects}`, + `radar-chart-${workspaceSlug}-${selectedDuration}-${selectedProjects}-${selectedCycle}-${selectedModule}-${isPeekView}`, () => analyticsV2Service.getAdvanceAnalyticsCharts[]>(workspaceSlug, "projects", { // date_filter: selectedDuration, ...(selectedProjects?.length > 0 && { project_ids: selectedProjects?.join(",") }), + ...(selectedCycle ? { cycle_id: selectedCycle } : {}), + ...(selectedModule ? { module_id: selectedModule } : {}), + ...(isPeekView ? { peek_view: true } : {}), }) ); diff --git a/web/core/components/analytics-v2/total-insights.tsx b/web/core/components/analytics-v2/total-insights.tsx index 09998feaf..8a3ffe8b7 100644 --- a/web/core/components/analytics-v2/total-insights.tsx +++ b/web/core/components/analytics-v2/total-insights.tsx @@ -18,16 +18,20 @@ const analyticsV2Service = new AnalyticsV2Service(); const TotalInsights: React.FC<{ analyticsType: TAnalyticsTabsV2Base; peekView?: boolean }> = observer( ({ analyticsType, peekView }) => { const params = useParams(); - const workspaceSlug = params.workspaceSlug as string; + const workspaceSlug = params.workspaceSlug.toString(); const { t } = useTranslation(); - const { selectedDuration, selectedProjects, selectedDurationLabel } = useAnalyticsV2(); + const { selectedDuration, selectedProjects, selectedDurationLabel, selectedCycle, selectedModule, isPeekView } = + useAnalyticsV2(); const { data: totalInsightsData, isLoading } = useSWR( - `total-insights-${analyticsType}-${selectedDuration}-${selectedProjects}`, + `total-insights-${analyticsType}-${selectedDuration}-${selectedProjects}-${selectedCycle}-${selectedModule}-${isPeekView}`, () => analyticsV2Service.getAdvanceAnalytics(workspaceSlug, analyticsType, { // date_filter: selectedDuration, ...(selectedProjects?.length > 0 ? { project_ids: selectedProjects.join(",") } : {}), + ...(selectedCycle ? { cycle_id: selectedCycle } : {}), + ...(selectedModule ? { module_id: selectedModule } : {}), + ...(isPeekView ? { peek_view: true } : {}), }) ); return ( diff --git a/web/core/components/analytics-v2/work-items/created-vs-resolved.tsx b/web/core/components/analytics-v2/work-items/created-vs-resolved.tsx index 2c95b916d..873ac2ed7 100644 --- a/web/core/components/analytics-v2/work-items/created-vs-resolved.tsx +++ b/web/core/components/analytics-v2/work-items/created-vs-resolved.tsx @@ -19,17 +19,21 @@ import { ChartLoader } from "../loaders"; const analyticsV2Service = new AnalyticsV2Service(); const CreatedVsResolved = observer(() => { - const { selectedDuration, selectedDurationLabel, selectedProjects } = useAnalyticsV2(); + const { selectedDuration, selectedDurationLabel, selectedProjects, selectedCycle, selectedModule, isPeekView } = + useAnalyticsV2(); const params = useParams(); const { t } = useTranslation(); - const workspaceSlug = params.workspaceSlug as string; + const workspaceSlug = params.workspaceSlug.toString(); const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/analytics-v2/empty-chart-area" }); const { data: createdVsResolvedData, isLoading: isCreatedVsResolvedLoading } = useSWR( - `created-vs-resolved-${workspaceSlug}-${selectedDuration}-${selectedProjects}`, + `created-vs-resolved-${workspaceSlug}-${selectedDuration}-${selectedProjects}-${selectedCycle}-${selectedModule}-${isPeekView}`, () => analyticsV2Service.getAdvanceAnalyticsCharts(workspaceSlug, "work-items", { // date_filter: selectedDuration, ...(selectedProjects?.length > 0 && { project_ids: selectedProjects?.join(",") }), + ...(selectedCycle ? { cycle_id: selectedCycle } : {}), + ...(selectedModule ? { module_id: selectedModule } : {}), + ...(isPeekView ? { peek_view: true } : {}), }) ); const parsedData: TChartData[] = useMemo(() => { diff --git a/web/core/components/analytics-v2/work-items/modal/content.tsx b/web/core/components/analytics-v2/work-items/modal/content.tsx index 85004d9af..62b3c24ba 100644 --- a/web/core/components/analytics-v2/work-items/modal/content.tsx +++ b/web/core/components/analytics-v2/work-items/modal/content.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useState } from "react"; import { observer } from "mobx-react"; import { Tab } from "@headlessui/react"; // plane package imports -import { IProject } from "@plane/types"; +import { ICycle, IModule, IProject } from "@plane/types"; import { Spinner } from "@plane/ui"; // hooks import { useAnalyticsV2 } from "@/hooks/store"; @@ -15,20 +15,52 @@ import WorkItemsInsightTable from "../workitems-insight-table"; type Props = { fullScreen: boolean; projectDetails: IProject | undefined; + cycleDetails: ICycle | undefined; + moduleDetails: IModule | undefined; }; export const WorkItemsModalMainContent: React.FC = observer((props) => { - const { projectDetails, fullScreen } = props; - const { updateSelectedProjects } = useAnalyticsV2(); - const [isProjectConfigured, setIsProjectConfigured] = useState(false); + const { projectDetails, cycleDetails, moduleDetails, fullScreen } = props; + const { updateSelectedProjects, updateSelectedCycle, updateSelectedModule, updateIsPeekView } = useAnalyticsV2(); + const [isModalConfigured, setIsModalConfigured] = useState(false); useEffect(() => { - if (!projectDetails?.id) return; - updateSelectedProjects([projectDetails?.id ?? ""]); - setIsProjectConfigured(true); - }, [projectDetails?.id, updateSelectedProjects]); + updateIsPeekView(true); - if (!isProjectConfigured) + // Handle project selection + if (projectDetails?.id) { + updateSelectedProjects([projectDetails.id]); + } + + // Handle cycle selection + if (cycleDetails?.id) { + updateSelectedCycle(cycleDetails.id); + } + + // Handle module selection + if (moduleDetails?.id) { + updateSelectedModule(moduleDetails.id); + } + setIsModalConfigured(true); + + // Cleanup fields + return () => { + updateSelectedProjects([]); + updateSelectedCycle(""); + updateSelectedModule(""); + updateIsPeekView(false); + }; + }, [ + projectDetails?.id, + cycleDetails?.id, + moduleDetails?.id, + updateSelectedProjects, + updateSelectedCycle, + updateSelectedModule, + updateIsPeekView, + ]); + + if (!isModalConfigured) return (
diff --git a/web/core/components/analytics-v2/work-items/modal/header.tsx b/web/core/components/analytics-v2/work-items/modal/header.tsx index f4bcdee38..1aa2c1b66 100644 --- a/web/core/components/analytics-v2/work-items/modal/header.tsx +++ b/web/core/components/analytics-v2/work-items/modal/header.tsx @@ -1,21 +1,26 @@ import { observer } from "mobx-react"; - -// icons +// plane package imports import { Expand, Shrink, X } from "lucide-react"; +import { ICycle, IModule } from "@plane/types"; +// icons type Props = { fullScreen: boolean; handleClose: () => void; setFullScreen: React.Dispatch>; title: string; + cycle?: ICycle; + module?: IModule; }; export const WorkItemsModalHeader: React.FC = observer((props) => { - const { fullScreen, handleClose, setFullScreen, title } = props; + const { fullScreen, handleClose, setFullScreen, title, cycle, module } = props; return (
-

Analytics for {title}

+

+ Analytics for {title} {cycle && `in ${cycle.name}`} {module && `in ${module.name}`} +

diff --git a/web/core/components/analytics-v2/work-items/priority-chart.tsx b/web/core/components/analytics-v2/work-items/priority-chart.tsx index 5e81c7efa..664824851 100644 --- a/web/core/components/analytics-v2/work-items/priority-chart.tsx +++ b/web/core/components/analytics-v2/work-items/priority-chart.tsx @@ -46,19 +46,23 @@ const PriorityChart = observer((props: Props) => { const { t } = useTranslation(); const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/analytics-v2/empty-chart-bar" }); // store hooks - const { selectedDuration, selectedProjects } = useAnalyticsV2(); + const { selectedDuration, selectedProjects, selectedCycle, selectedModule, isPeekView } = useAnalyticsV2(); const { workspaceStates } = useProjectState(); const { resolvedTheme } = useTheme(); // router const params = useParams(); - const workspaceSlug = params.workspaceSlug as string; + const workspaceSlug = params.workspaceSlug.toString(); const { data: priorityChartData, isLoading: priorityChartLoading } = useSWR( - `customized-insights-chart-${workspaceSlug}-${selectedDuration}-${selectedProjects}-${props.x_axis}-${props.y_axis}-${props.group_by}`, + `customized-insights-chart-${workspaceSlug}-${selectedDuration}- + ${selectedProjects}-${selectedCycle}-${selectedModule}-${props.x_axis}-${props.y_axis}-${props.group_by}-${isPeekView}`, () => analyticsV2Service.getAdvanceAnalyticsCharts(workspaceSlug, "custom-work-items", { // date_filter: selectedDuration, ...(selectedProjects?.length > 0 && { project_ids: selectedProjects?.join(",") }), + ...(selectedCycle ? { cycle_id: selectedCycle } : {}), + ...(selectedModule ? { module_id: selectedModule } : {}), + ...(isPeekView ? { peek_view: true } : {}), ...props, }) ); @@ -158,10 +162,23 @@ const PriorityChart = observer((props: Props) => { }); const exportCSV = (rows: Row[]) => { - const rowData = rows.map((row) => ({ - name: row.original.name, - count: row.original.count, - })); + const rowData = rows.map((row) => { + const hiddenFields = ["key", "avatar_url", "assignee_id", "project_id"]; + const otherFields = Object.keys(row.original).filter( + (key) => key !== "name" && key !== "count" && !hiddenFields.includes(key) && !key.includes("id") + ); + return { + name: row.original.name, + count: row.original.count, + ...otherFields.reduce( + (acc, key) => { + acc[parsedData?.schema[key] ?? key] = row.original[key]; + return acc; + }, + {} as Record + ), + }; + }); const csv = generateCsv(csvConfig)(rowData); download(csvConfig)(csv); }; diff --git a/web/core/components/analytics-v2/work-items/workitems-insight-table.tsx b/web/core/components/analytics-v2/work-items/workitems-insight-table.tsx index 85c93676b..e42824492 100644 --- a/web/core/components/analytics-v2/work-items/workitems-insight-table.tsx +++ b/web/core/components/analytics-v2/work-items/workitems-insight-table.tsx @@ -1,13 +1,15 @@ import { useMemo } from "react"; -import { ColumnDef } from "@tanstack/react-table"; +import { ColumnDef, Row } from "@tanstack/react-table"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; import useSWR from "swr"; -import { Briefcase } from "lucide-react"; +import { Briefcase, UserRound } from "lucide-react"; // plane package imports import { useTranslation } from "@plane/i18n"; import { WorkItemInsightColumns, AnalyticsTableDataMap } from "@plane/types"; // plane web components +import { Avatar } from "@plane/ui"; +import { getFileURL } from "@plane/utils"; import { Logo } from "@/components/common/logo"; // hooks import { useAnalyticsV2 } from "@/hooks/store/use-analytics-v2"; @@ -21,44 +23,85 @@ const analyticsV2Service = new AnalyticsV2Service(); const WorkItemsInsightTable = observer(() => { // router const params = useParams(); - const workspaceSlug = params.workspaceSlug as string; + const workspaceSlug = params.workspaceSlug.toString(); const { t } = useTranslation(); // store hooks const { getProjectById } = useProject(); - const { selectedDuration, selectedProjects } = useAnalyticsV2(); + const { selectedDuration, selectedProjects, selectedCycle, selectedModule, isPeekView } = useAnalyticsV2(); const { data: workItemsData, isLoading } = useSWR( - `insights-table-work-items-${workspaceSlug}-${selectedDuration}-${selectedProjects}`, + `insights-table-work-items-${workspaceSlug}-${selectedDuration}-${selectedProjects}-${selectedCycle}-${selectedModule}-${isPeekView}`, () => analyticsV2Service.getAdvanceAnalyticsStats(workspaceSlug, "work-items", { // date_filter: selectedDuration, ...(selectedProjects?.length > 0 ? { project_ids: selectedProjects.join(",") } : {}), + ...(selectedCycle ? { cycle_id: selectedCycle } : {}), + ...(selectedModule ? { module_id: selectedModule } : {}), + ...(isPeekView ? { peek_view: true } : {}), }) ); // derived values - const columnsLabels: Record = { - backlog_work_items: t("workspace_projects.state.backlog"), - started_work_items: t("workspace_projects.state.started"), - un_started_work_items: t("workspace_projects.state.unstarted"), - completed_work_items: t("workspace_projects.state.completed"), - cancelled_work_items: t("workspace_projects.state.cancelled"), - project__name: t("common.project"), - }; + const columnsLabels = useMemo( + () => ({ + backlog_work_items: t("workspace_projects.state.backlog"), + started_work_items: t("workspace_projects.state.started"), + un_started_work_items: t("workspace_projects.state.unstarted"), + completed_work_items: t("workspace_projects.state.completed"), + cancelled_work_items: t("workspace_projects.state.cancelled"), + project__name: t("common.project"), + display_name: t("common.assignee"), + }), + [t] + ); const columns = useMemo( () => [ - { - accessorKey: "project__name", - header: () =>
{columnsLabels["project__name"]}
, - cell: ({ row }) => { - const project = getProjectById(row.original.project_id); - return ( -
- {project?.logo_props ? : } - {project?.name} -
- ); - }, - }, + !isPeekView + ? { + accessorKey: "project__name", + header: () =>
{columnsLabels["project__name"]}
, + cell: ({ row }) => { + const project = getProjectById(row.original.project_id); + return ( +
+ {project?.logo_props ? ( + + ) : ( + + )} + {project?.name} +
+ ); + }, + } + : { + accessorKey: "display_name", + header: () =>
{columnsLabels["display_name"]}
, + cell: ({ row }: { row: Row }) => ( +
+
+ {row.original.avatar_url && row.original.avatar_url !== "" ? ( + + ) : ( +
+ {row.original.display_name ? ( + row.original.display_name?.[0] + ) : ( + + )} +
+ )} + + {row.original.display_name ?? t(`Unassigned`)} + +
+
+ ), + }, { accessorKey: "backlog_work_items", header: () =>
{columnsLabels["backlog_work_items"]}
, @@ -85,7 +128,7 @@ const WorkItemsInsightTable = observer(() => { cell: ({ row }) =>
{row.original.cancelled_work_items}
, }, ] as ColumnDef[], - [getProjectById] + [columnsLabels, getProjectById, isPeekView, t] ); return ( diff --git a/web/core/components/analytics/old-page.tsx b/web/core/components/analytics/old-page.tsx new file mode 100644 index 000000000..719d66214 --- /dev/null +++ b/web/core/components/analytics/old-page.tsx @@ -0,0 +1,107 @@ +"use client"; + +import React, { Fragment } from "react"; +import { observer } from "mobx-react"; +import { useSearchParams } from "next/navigation"; +import { Tab } from "@headlessui/react"; +// plane package imports +import { ANALYTICS_TABS, EUserPermissionsLevel, EUserPermissions } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; +import { Header, EHeaderVariant } from "@plane/ui"; +// components +import { CustomAnalytics, ScopeAndDemand } from "@/components/analytics"; +import { PageHead } from "@/components/core"; +import { ComicBoxButton, DetailedEmptyState } from "@/components/empty-state"; +// hooks +import { useCommandPalette, useEventTracker, useProject, useUserPermissions, useWorkspace } from "@/hooks/store"; +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; + +const OldAnalyticsPage = observer(() => { + const searchParams = useSearchParams(); + const analytics_tab = searchParams.get("analytics_tab"); + // plane imports + const { t } = useTranslation(); + // store hooks + const { toggleCreateProjectModal } = useCommandPalette(); + const { setTrackElement } = useEventTracker(); + const { workspaceProjectIds, loader } = useProject(); + const { currentWorkspace } = useWorkspace(); + const { allowPermissions } = useUserPermissions(); + // helper hooks + const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/onboarding/analytics" }); + // derived values + const pageTitle = currentWorkspace?.name + ? t(`workspace_analytics.page_label`, { workspace: currentWorkspace?.name }) + : undefined; + + // permissions + const canPerformEmptyStateActions = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.WORKSPACE + ); + + // TODO: refactor loader implementation + return ( + <> + + {workspaceProjectIds && ( + <> + {workspaceProjectIds.length > 0 || loader === "init-loader" ? ( +
+ +
+ + {ANALYTICS_TABS.map((tab) => ( + + {({ selected }) => ( + + )} + + ))} + +
+ + + + + + + + +
+
+ ) : ( + { + setTrackElement("Analytics empty state"); + toggleCreateProjectModal(true); + }} + disabled={!canPerformEmptyStateActions} + /> + } + /> + )} + + )} + + ); +}); + +export default OldAnalyticsPage; diff --git a/web/core/store/analytics-v2.store.ts b/web/core/store/analytics-v2.store.ts index bf8f91a72..97582577a 100644 --- a/web/core/store/analytics-v2.store.ts +++ b/web/core/store/analytics-v2.store.ts @@ -10,6 +10,9 @@ export interface IAnalyticsStoreV2 { currentTab: TAnalyticsTabsV2Base; selectedProjects: string[]; selectedDuration: DurationType; + selectedCycle: string; + selectedModule: string; + isPeekView?: boolean; //computed selectedDurationLabel: DurationType | null; @@ -17,25 +20,36 @@ export interface IAnalyticsStoreV2 { //actions updateSelectedProjects: (projects: string[]) => void; updateSelectedDuration: (duration: DurationType) => void; + updateSelectedCycle: (cycle: string) => void; + updateSelectedModule: (module: string) => void; + updateIsPeekView: (isPeekView: boolean) => void; } export class AnalyticsStoreV2 implements IAnalyticsStoreV2 { //observables currentTab: TAnalyticsTabsV2Base = "overview"; - selectedProjects: DurationType[] = []; + selectedProjects: string[] = []; selectedDuration: DurationType = "last_30_days"; - - constructor(_rootStore: CoreRootStore) { + selectedCycle: string = ""; + selectedModule: string = ""; + isPeekView: boolean = false; + constructor() { makeObservable(this, { // observables currentTab: observable.ref, selectedDuration: observable.ref, selectedProjects: observable.ref, + selectedCycle: observable.ref, + selectedModule: observable.ref, + isPeekView: observable.ref, // computed selectedDurationLabel: computed, // actions updateSelectedProjects: action, updateSelectedDuration: action, + updateSelectedCycle: action, + updateSelectedModule: action, + updateIsPeekView: action, }); } @@ -44,7 +58,6 @@ export class AnalyticsStoreV2 implements IAnalyticsStoreV2 { } updateSelectedProjects = (projects: string[]) => { - const initialState = this.selectedProjects; try { runInAction(() => { this.selectedProjects = projects; @@ -65,4 +78,22 @@ export class AnalyticsStoreV2 implements IAnalyticsStoreV2 { throw error; } }; + + updateSelectedCycle = (cycle: string) => { + runInAction(() => { + this.selectedCycle = cycle; + }); + }; + + updateSelectedModule = (module: string) => { + runInAction(() => { + this.selectedModule = module; + }); + }; + + updateIsPeekView = (isPeekView: boolean) => { + runInAction(() => { + this.isPeekView = isPeekView; + }); + }; } diff --git a/web/core/store/root.store.ts b/web/core/store/root.store.ts index d2355de78..2aef8d030 100644 --- a/web/core/store/root.store.ts +++ b/web/core/store/root.store.ts @@ -96,7 +96,7 @@ export class CoreRootStore { this.transient = new TransientStore(); this.stickyStore = new StickyStore(); this.editorAssetStore = new EditorAssetStore(); - this.analyticsV2 = new AnalyticsStoreV2(this); + this.analyticsV2 = new AnalyticsStoreV2(); } resetOnSignOut() { From 6c483fad2fa1f4404420b30c7cde648f0a68e604 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Sun, 18 May 2025 15:18:09 +0530 Subject: [PATCH 057/201] [WEB-4041] chore: modal outside click behaviour #7072 --- web/core/components/cycles/modal.tsx | 7 ++++++- .../inbox/modals/create-modal/modal.tsx | 13 +++++++++---- web/core/components/issues/issue-modal/base.tsx | 1 - web/core/components/modules/modal.tsx | 7 ++++++- .../components/project/create-project-modal.tsx | 8 +++++++- web/core/components/views/modal.tsx | 7 ++++++- .../web-hooks/create-webhook-modal.tsx | 16 +++++++--------- 7 files changed, 41 insertions(+), 18 deletions(-) diff --git a/web/core/components/cycles/modal.tsx b/web/core/components/cycles/modal.tsx index a12193559..15fa4a5b9 100644 --- a/web/core/components/cycles/modal.tsx +++ b/web/core/components/cycles/modal.tsx @@ -13,6 +13,7 @@ import { CycleForm } from "@/components/cycles"; // constants // hooks import { useEventTracker, useCycle, useProject } from "@/hooks/store"; +import useKeypress from "@/hooks/use-keypress"; import useLocalStorage from "@/hooks/use-local-storage"; import { usePlatformOS } from "@/hooks/use-platform-os"; // services @@ -180,8 +181,12 @@ export const CycleCreateUpdateModal: React.FC = (props) => { setActiveProject(projectId ?? workspaceProjectIds?.[0] ?? null); }, [activeProject, data, projectId, workspaceProjectIds, isOpen]); + useKeypress("Escape", () => { + if (isOpen) handleClose(); + }); + return ( - + = (props) // handlers const handleDuplicateIssueModal = (value: boolean) => setIsDuplicateModalOpen(value); + useKeypress("Escape", () => { + if (modalState) { + handleModalClose(); + setIsDuplicateModalOpen(false); + } + }); + return ( { - handleModalClose(); - setIsDuplicateModalOpen(false); - }} position={EModalPosition.TOP} width={isDuplicateModalOpen ? EModalWidth.VIXL : EModalWidth.XXXXL} className="!bg-transparent rounded-lg shadow-none transition-[width] ease-linear" diff --git a/web/core/components/issues/issue-modal/base.tsx b/web/core/components/issues/issue-modal/base.tsx index b30c1a85c..d27223239 100644 --- a/web/core/components/issues/issue-modal/base.tsx +++ b/web/core/components/issues/issue-modal/base.tsx @@ -375,7 +375,6 @@ export const CreateUpdateIssueModalBase: React.FC = observer(( return ( handleClose(true)} position={EModalPosition.TOP} width={isDuplicateModalOpen ? EModalWidth.VIXL : EModalWidth.XXXXL} className="!bg-transparent rounded-lg shadow-none transition-[width] ease-linear" diff --git a/web/core/components/modules/modal.tsx b/web/core/components/modules/modal.tsx index 8cfc8820c..e1bc7c7ef 100644 --- a/web/core/components/modules/modal.tsx +++ b/web/core/components/modules/modal.tsx @@ -13,6 +13,7 @@ import { ModuleForm } from "@/components/modules"; // constants // hooks import { useEventTracker, useModule, useProject } from "@/hooks/store"; +import useKeypress from "@/hooks/use-keypress"; import { usePlatformOS } from "@/hooks/use-platform-os"; type Props = { @@ -142,8 +143,12 @@ export const CreateUpdateModuleModal: React.FC = observer((props) => { setActiveProject(projectId ?? workspaceProjectIds?.[0] ?? null); }, [activeProject, data, projectId, workspaceProjectIds, isOpen]); + useKeypress("Escape", () => { + if (isOpen) handleClose(); + }); + return ( - + = (props) => { } }; + useKeypress("Escape", () => { + if (isOpen) onClose(); + }); + return ( - + {currentStep === EProjectCreationSteps.CREATE_PROJECT && ( = observer((props) => { else await handleUpdateView(formData); }; + useKeypress("Escape", () => { + if (isOpen) handleClose(); + }); + return ( - + = (props) => { }, 350); }; + useKeypress("Escape", () => { + if (isOpen && !generatedWebhook) handleClose(); + }); + return ( - { - if (!generatedWebhook) handleClose(); - }} - position={EModalPosition.TOP} - width={EModalWidth.XXL} - className="p-4 pb-0" - > + {!generatedWebhook ? ( ) : ( From 906ce8b500d1adc25059c59093398661923434a3 Mon Sep 17 00:00:00 2001 From: Akshita Goyal <36129505+gakshita@users.noreply.github.com> Date: Sun, 18 May 2025 15:19:05 +0530 Subject: [PATCH 058/201] [WEB-4104] fix: project loading state #7065 --- web/core/store/project/project.store.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/core/store/project/project.store.ts b/web/core/store/project/project.store.ts index 45ab85edf..e460b1087 100644 --- a/web/core/store/project/project.store.ts +++ b/web/core/store/project/project.store.ts @@ -293,7 +293,7 @@ export class ProjectStore implements IProjectStore { update(this.projectMap, [project.id], (p) => ({ ...p, ...project })); }); this.loader = "loaded"; - this.fetchStatus = "partial"; + if (!this.fetchStatus) this.fetchStatus = "partial"; }); return projectsResponse; } catch (error) { From 9c9952a8231925c7446daf77fda6d2b30929b1de Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Sun, 18 May 2025 15:28:00 +0530 Subject: [PATCH 059/201] [WEB-3866] fix: work item attachment activity #7062 --- web/core/components/core/activity.tsx | 10 +--------- .../issue-activity/activity/actions/attachment.tsx | 12 +----------- 2 files changed, 2 insertions(+), 20 deletions(-) diff --git a/web/core/components/core/activity.tsx b/web/core/components/core/activity.tsx index 218697795..3febacd2d 100644 --- a/web/core/components/core/activity.tsx +++ b/web/core/components/core/activity.tsx @@ -196,15 +196,7 @@ const activityDetails: { if (activity.verb === "created") return ( <> - uploaded a new{" "} - - attachment - + uploaded a new attachment {showIssue && ( <> {" "} diff --git a/web/core/components/issues/issue-detail/issue-activity/activity/actions/attachment.tsx b/web/core/components/issues/issue-detail/issue-activity/activity/actions/attachment.tsx index 2e827dd02..3ba87df9c 100644 --- a/web/core/components/issues/issue-detail/issue-activity/activity/actions/attachment.tsx +++ b/web/core/components/issues/issue-detail/issue-activity/activity/actions/attachment.tsx @@ -25,17 +25,7 @@ export const IssueAttachmentActivity: FC = observer((p ends={ends} > <> - {activity.verb === "created" ? `uploaded a new ` : `removed an attachment`} - {activity.verb === "created" && ( - - attachment - - )} + {activity.verb === "created" ? `uploaded a new attachment` : `removed an attachment`} {showIssue && (activity.verb === "created" ? ` to ` : ` from `)} {showIssue && }. From e48b2da6232a94691746484e876a0dafba4294e8 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Sun, 18 May 2025 15:28:47 +0530 Subject: [PATCH 060/201] [WEB-4056] fix: archived work item validation #7060 --- web/app/[workspaceSlug]/(projects)/browse/[workItem]/page.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/web/app/[workspaceSlug]/(projects)/browse/[workItem]/page.tsx b/web/app/[workspaceSlug]/(projects)/browse/[workItem]/page.tsx index e315f3549..e117d0ab2 100644 --- a/web/app/[workspaceSlug]/(projects)/browse/[workItem]/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/browse/[workItem]/page.tsx @@ -104,6 +104,7 @@ const IssueDetailsPage = observer(() => { workspaceSlug={workspaceSlug.toString()} projectId={projectId.toString()} issueId={issueId.toString()} + is_archived={!!issue?.archived_at} /> ) From 2a2feaf88efd3e650900690c53733db569a2b67b Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Mon, 19 May 2025 13:12:52 +0530 Subject: [PATCH 061/201] [WIKI-181] chore: editor extension storage utility code split (#7071) * chore: storage extension code split * chore: use storage extension utility --- packages/editor/src/ce/types/storage.ts | 13 ++++++++++++ .../extensions/custom-image/custom-image.ts | 8 +++---- .../custom-image/read-only-custom-image.ts | 4 ++-- .../src/core/helpers/get-extension-storage.ts | 21 +++---------------- 4 files changed, 22 insertions(+), 24 deletions(-) create mode 100644 packages/editor/src/ce/types/storage.ts diff --git a/packages/editor/src/ce/types/storage.ts b/packages/editor/src/ce/types/storage.ts new file mode 100644 index 000000000..4e106738b --- /dev/null +++ b/packages/editor/src/ce/types/storage.ts @@ -0,0 +1,13 @@ +import { HeadingExtensionStorage } from "@/extensions"; +import { CustomImageExtensionStorage } from "@/extensions/custom-image"; +import { CustomLinkStorage } from "@/extensions/custom-link"; +import { MentionExtensionStorage } from "@/extensions/mentions"; +import { ImageExtensionStorage } from "@/plugins/image"; + +export type ExtensionStorageMap = { + imageComponent: CustomImageExtensionStorage; + image: ImageExtensionStorage; + link: CustomLinkStorage; + headingList: HeadingExtensionStorage; + mention: MentionExtensionStorage; +}; diff --git a/packages/editor/src/core/extensions/custom-image/custom-image.ts b/packages/editor/src/core/extensions/custom-image/custom-image.ts index a9a69fa60..11586bf86 100644 --- a/packages/editor/src/core/extensions/custom-image/custom-image.ts +++ b/packages/editor/src/core/extensions/custom-image/custom-image.ts @@ -8,6 +8,7 @@ import { ACCEPTED_IMAGE_MIME_TYPES } from "@/constants/config"; import { CustomImageNode } from "@/extensions/custom-image"; // helpers import { isFileValid } from "@/helpers/file"; +import { getExtensionStorage } from "@/helpers/get-extension-storage"; import { insertEmptyParagraphAtNodeBoundaries } from "@/helpers/insert-empty-paragraph-at-node-boundary"; // plugins import { TrackImageDeletionPlugin, TrackImageRestorationPlugin } from "@/plugins/image"; @@ -32,10 +33,9 @@ declare module "@tiptap/core" { } } -export const getImageComponentImageFileMap = (editor: Editor) => - (editor.storage.imageComponent as UploadImageExtensionStorage | undefined)?.fileMap; +export const getImageComponentImageFileMap = (editor: Editor) => getExtensionStorage(editor, "imageComponent")?.fileMap; -export interface UploadImageExtensionStorage { +export interface CustomImageExtensionStorage { assetsUploadStatus: TFileHandler["assetsUploadStatus"]; fileMap: Map; deletedImageSet: Map; @@ -55,7 +55,7 @@ export const CustomImageExtension = (props: TFileHandler) => { validation: { maxFileSize }, } = props; - return Image.extend, UploadImageExtensionStorage>({ + return Image.extend, CustomImageExtensionStorage>({ name: "imageComponent", selectable: true, group: "block", diff --git a/packages/editor/src/core/extensions/custom-image/read-only-custom-image.ts b/packages/editor/src/core/extensions/custom-image/read-only-custom-image.ts index 0d8a7cc55..51b758898 100644 --- a/packages/editor/src/core/extensions/custom-image/read-only-custom-image.ts +++ b/packages/editor/src/core/extensions/custom-image/read-only-custom-image.ts @@ -2,14 +2,14 @@ import { mergeAttributes } from "@tiptap/core"; import { Image } from "@tiptap/extension-image"; import { ReactNodeViewRenderer } from "@tiptap/react"; // components -import { CustomImageNode, UploadImageExtensionStorage } from "@/extensions/custom-image"; +import { CustomImageNode, CustomImageExtensionStorage } from "@/extensions/custom-image"; // types import { TReadOnlyFileHandler } from "@/types"; export const CustomReadOnlyImageExtension = (props: TReadOnlyFileHandler) => { const { getAssetSrc, restore: restoreImageFn } = props; - return Image.extend, UploadImageExtensionStorage>({ + return Image.extend, CustomImageExtensionStorage>({ name: "imageComponent", selectable: false, group: "block", diff --git a/packages/editor/src/core/helpers/get-extension-storage.ts b/packages/editor/src/core/helpers/get-extension-storage.ts index 0107f8425..86db93e18 100644 --- a/packages/editor/src/core/helpers/get-extension-storage.ts +++ b/packages/editor/src/core/helpers/get-extension-storage.ts @@ -1,23 +1,8 @@ import { Editor } from "@tiptap/core"; -import { - CustomLinkStorage, - HeadingExtensionStorage, - MentionExtensionStorage, - UploadImageExtensionStorage, -} from "@/extensions"; -import { ImageExtensionStorage } from "@/plugins/image"; +// plane editor types +import { ExtensionStorageMap } from "@/plane-editor/types/storage"; -type ExtensionNames = "imageComponent" | "image" | "link" | "headingList" | "mention"; - -interface ExtensionStorageMap { - imageComponent: UploadImageExtensionStorage; - image: ImageExtensionStorage; - link: CustomLinkStorage; - headingList: HeadingExtensionStorage; - mention: MentionExtensionStorage; -} - -export const getExtensionStorage = ( +export const getExtensionStorage = ( editor: Editor, extensionName: K ): ExtensionStorageMap[K] => editor.storage[extensionName]; From 2d475491e9b134600a83bd5a5064dfbcf0b683a1 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Mon, 19 May 2025 15:20:40 +0530 Subject: [PATCH 062/201] [WEB-4117] refactor: work item widgets code split (#7078) * refactor: work item widget code split * fix: types --- packages/types/src/issues/issue.d.ts | 2 +- .../issue-detail-widgets/action-buttons.tsx | 14 ++++++++++++ .../additional-widgets.tsx | 10 --------- .../issue-detail-widgets/collapsibles.tsx | 14 ++++++++++++ .../issues/issue-detail-widgets/index.ts | 1 - .../issues/issue-detail-widgets/modals.tsx | 13 +++++++++++ .../issue-detail-widgets/action-buttons.tsx | 13 +++++++++-- .../issue-detail-widget-collapsibles.tsx | 13 ++++++----- .../issue-detail-widget-modals.tsx | 19 +++++++++++----- .../issues/issue-detail-widgets/root.tsx | 4 +--- .../issue-detail-widgets/sub-issues/root.tsx | 4 ++-- .../store/issue/issue-details/root.store.ts | 22 +++++++++---------- 12 files changed, 88 insertions(+), 41 deletions(-) create mode 100644 web/ce/components/issues/issue-detail-widgets/action-buttons.tsx delete mode 100644 web/ce/components/issues/issue-detail-widgets/additional-widgets.tsx create mode 100644 web/ce/components/issues/issue-detail-widgets/collapsibles.tsx delete mode 100644 web/ce/components/issues/issue-detail-widgets/index.ts create mode 100644 web/ce/components/issues/issue-detail-widgets/modals.tsx diff --git a/packages/types/src/issues/issue.d.ts b/packages/types/src/issues/issue.d.ts index e51bfa36d..a9d8970f9 100644 --- a/packages/types/src/issues/issue.d.ts +++ b/packages/types/src/issues/issue.d.ts @@ -119,7 +119,7 @@ export type TBulkOperationsPayload = { properties: Partial; }; -export type TIssueDetailWidget = "sub-issues" | "relations" | "links" | "attachments"; +export type TWorkItemWidgets = "sub-work-items" | "relations" | "links" | "attachments"; export type TIssueServiceType = EIssueServiceType.ISSUES | EIssueServiceType.EPICS | EIssueServiceType.WORK_ITEMS; diff --git a/web/ce/components/issues/issue-detail-widgets/action-buttons.tsx b/web/ce/components/issues/issue-detail-widgets/action-buttons.tsx new file mode 100644 index 000000000..1312c0839 --- /dev/null +++ b/web/ce/components/issues/issue-detail-widgets/action-buttons.tsx @@ -0,0 +1,14 @@ +import { FC } from "react"; +// plane types +import { TIssueServiceType, TWorkItemWidgets } from "@plane/types"; + +export type TWorkItemAdditionalWidgetActionButtonsProps = { + disabled: boolean; + hideWidgets: TWorkItemWidgets[]; + issueServiceType: TIssueServiceType; + projectId: string; + workItemId: string; + workspaceSlug: string; +}; + +export const WorkItemAdditionalWidgetActionButtons: FC = () => null; diff --git a/web/ce/components/issues/issue-detail-widgets/additional-widgets.tsx b/web/ce/components/issues/issue-detail-widgets/additional-widgets.tsx deleted file mode 100644 index 04288603a..000000000 --- a/web/ce/components/issues/issue-detail-widgets/additional-widgets.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { FC } from "react"; - -export type TWorkItemAdditionalWidgets = { - workspaceSlug: string; - projectId: string; - workItemId: string; - disabled: boolean; -}; - -export const WorkItemAdditionalWidgets: FC = (props) => <>; diff --git a/web/ce/components/issues/issue-detail-widgets/collapsibles.tsx b/web/ce/components/issues/issue-detail-widgets/collapsibles.tsx new file mode 100644 index 000000000..a9a6a1b29 --- /dev/null +++ b/web/ce/components/issues/issue-detail-widgets/collapsibles.tsx @@ -0,0 +1,14 @@ +import { FC } from "react"; +// plane types +import { TIssueServiceType, TWorkItemWidgets } from "@plane/types"; + +export type TWorkItemAdditionalWidgetCollapsiblesProps = { + disabled: boolean; + hideWidgets: TWorkItemWidgets[]; + issueServiceType: TIssueServiceType; + projectId: string; + workItemId: string; + workspaceSlug: string; +}; + +export const WorkItemAdditionalWidgetCollapsibles: FC = () => null; diff --git a/web/ce/components/issues/issue-detail-widgets/index.ts b/web/ce/components/issues/issue-detail-widgets/index.ts deleted file mode 100644 index a972c5053..000000000 --- a/web/ce/components/issues/issue-detail-widgets/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./additional-widgets"; diff --git a/web/ce/components/issues/issue-detail-widgets/modals.tsx b/web/ce/components/issues/issue-detail-widgets/modals.tsx new file mode 100644 index 000000000..2e9dfe40d --- /dev/null +++ b/web/ce/components/issues/issue-detail-widgets/modals.tsx @@ -0,0 +1,13 @@ +import { FC } from "react"; +// plane types +import { TIssueServiceType, TWorkItemWidgets } from "@plane/types"; + +export type TWorkItemAdditionalWidgetModalsProps = { + hideWidgets: TWorkItemWidgets[]; + issueServiceType: TIssueServiceType; + projectId: string; + workItemId: string; + workspaceSlug: string; +}; + +export const WorkItemAdditionalWidgetModals: FC = () => null; diff --git a/web/core/components/issues/issue-detail-widgets/action-buttons.tsx b/web/core/components/issues/issue-detail-widgets/action-buttons.tsx index 29d1ff00d..27b6b4265 100644 --- a/web/core/components/issues/issue-detail-widgets/action-buttons.tsx +++ b/web/core/components/issues/issue-detail-widgets/action-buttons.tsx @@ -4,7 +4,7 @@ import React, { FC } from "react"; import { Layers, Link, Paperclip, Waypoints } from "lucide-react"; // plane imports import { useTranslation } from "@plane/i18n"; -import { TIssueServiceType } from "@plane/types"; +import { TIssueServiceType, TWorkItemWidgets } from "@plane/types"; // components import { IssueAttachmentActionButton, @@ -12,8 +12,9 @@ import { RelationActionButton, SubIssuesActionButton, IssueDetailWidgetButton, - TWorkItemWidgets, } from "@/components/issues/issue-detail-widgets"; +// plane web imports +import { WorkItemAdditionalWidgetActionButtons } from "@/plane-web/components/issues/issue-detail-widgets/action-buttons"; type Props = { workspaceSlug: string; @@ -88,6 +89,14 @@ export const IssueDetailWidgetActionButtons: FC = (props) => { issueServiceType={issueServiceType} /> )} +
); }; diff --git a/web/core/components/issues/issue-detail-widgets/issue-detail-widget-collapsibles.tsx b/web/core/components/issues/issue-detail-widgets/issue-detail-widget-collapsibles.tsx index f73114eff..ec3302d61 100644 --- a/web/core/components/issues/issue-detail-widgets/issue-detail-widget-collapsibles.tsx +++ b/web/core/components/issues/issue-detail-widgets/issue-detail-widget-collapsibles.tsx @@ -2,19 +2,18 @@ import React, { FC } from "react"; import { observer } from "mobx-react"; // plane imports -import { TIssueServiceType } from "@plane/types"; +import { TIssueServiceType, TWorkItemWidgets } from "@plane/types"; // components import { AttachmentsCollapsible, LinksCollapsible, RelationsCollapsible, SubIssuesCollapsible, - TWorkItemWidgets, } from "@/components/issues/issue-detail-widgets"; // hooks import { useIssueDetail } from "@/hooks/store"; // Plane-web -import { WorkItemAdditionalWidgets } from "@/plane-web/components/issues/issue-detail-widgets"; +import { WorkItemAdditionalWidgetCollapsibles } from "@/plane-web/components/issues/issue-detail-widgets/collapsibles"; import { useTimeLineRelationOptions } from "@/plane-web/components/relations"; type Props = { @@ -87,11 +86,13 @@ export const IssueDetailWidgetCollapsibles: FC = observer((props) => { issueServiceType={issueServiceType} /> )} -
); diff --git a/web/core/components/issues/issue-detail-widgets/issue-detail-widget-modals.tsx b/web/core/components/issues/issue-detail-widgets/issue-detail-widget-modals.tsx index f68d3dad6..d79879872 100644 --- a/web/core/components/issues/issue-detail-widgets/issue-detail-widget-modals.tsx +++ b/web/core/components/issues/issue-detail-widgets/issue-detail-widget-modals.tsx @@ -1,18 +1,19 @@ import React, { FC } from "react"; import { observer } from "mobx-react"; -import { ISearchIssueResponse, TIssue, TIssueServiceType } from "@plane/types"; +import { ISearchIssueResponse, TIssue, TIssueServiceType, TWorkItemWidgets } from "@plane/types"; import { setToast, TOAST_TYPE } from "@plane/ui"; // components import { ExistingIssuesListModal } from "@/components/core"; import { CreateUpdateIssueModal } from "@/components/issues/issue-modal"; // hooks import { useIssueDetail } from "@/hooks/store"; - +// plane web imports +import { WorkItemAdditionalWidgetModals } from "@/plane-web/components/issues/issue-detail-widgets/modals"; +// local imports import { IssueLinkCreateUpdateModal } from "../issue-detail/links/create-update-link-modal"; // helpers import { useLinkOperations } from "./links/helper"; import { useSubIssueOperations } from "./sub-issues/helper"; -import { TWorkItemWidgets } from "."; type Props = { workspaceSlug: string; @@ -65,7 +66,7 @@ export const IssueDetailWidgetModals: FC = observer((props) => { const handleExistingIssuesModalClose = () => { handleIssueCrudState("existing", null, null); - setLastWidgetAction("sub-issues"); + setLastWidgetAction("sub-work-items"); toggleSubIssuesModal(null); }; @@ -80,7 +81,7 @@ export const IssueDetailWidgetModals: FC = observer((props) => { const handleCreateUpdateModalClose = () => { handleIssueCrudState("create", null, null); toggleCreateIssueModal(false); - setLastWidgetAction("sub-issues"); + setLastWidgetAction("sub-work-items"); }; const handleCreateUpdateModalOnSubmit = async (_issue: TIssue) => { @@ -190,6 +191,14 @@ export const IssueDetailWidgetModals: FC = observer((props) => { workspaceLevelToggle /> )} + + ); }); diff --git a/web/core/components/issues/issue-detail-widgets/root.tsx b/web/core/components/issues/issue-detail-widgets/root.tsx index b1cebf9b1..ef8b8c285 100644 --- a/web/core/components/issues/issue-detail-widgets/root.tsx +++ b/web/core/components/issues/issue-detail-widgets/root.tsx @@ -2,7 +2,7 @@ import React, { FC } from "react"; // plane imports -import { TIssueServiceType } from "@plane/types"; +import { TIssueServiceType, TWorkItemWidgets } from "@plane/types"; // components import { IssueDetailWidgetActionButtons, @@ -10,8 +10,6 @@ import { IssueDetailWidgetModals, } from "@/components/issues/issue-detail-widgets"; -export type TWorkItemWidgets = "sub-work-items" | "relations" | "links" | "attachments"; - type Props = { workspaceSlug: string; projectId: string; diff --git a/web/core/components/issues/issue-detail-widgets/sub-issues/root.tsx b/web/core/components/issues/issue-detail-widgets/sub-issues/root.tsx index adea10b7d..a48d2ef5c 100644 --- a/web/core/components/issues/issue-detail-widgets/sub-issues/root.tsx +++ b/web/core/components/issues/issue-detail-widgets/sub-issues/root.tsx @@ -22,12 +22,12 @@ export const SubIssuesCollapsible: FC = observer((props) => { // store hooks const { openWidgets, toggleOpenWidget } = useIssueDetail(issueServiceType); // derived values - const isCollapsibleOpen = openWidgets.includes("sub-issues"); + const isCollapsibleOpen = openWidgets.includes("sub-work-items"); return ( toggleOpenWidget("sub-issues")} + onToggle={() => toggleOpenWidget("sub-work-items")} title={ void; toggleSubIssuesModal: (value: string | null) => void; toggleDeleteAttachmentModal: (attachmentId: string | null) => void; - setOpenWidgets: (state: TIssueDetailWidget[]) => void; - setLastWidgetAction: (action: TIssueDetailWidget) => void; - toggleOpenWidget: (state: TIssueDetailWidget) => void; + setOpenWidgets: (state: TWorkItemWidgets[]) => void; + setLastWidgetAction: (action: TWorkItemWidgets) => void; + toggleOpenWidget: (state: TWorkItemWidgets) => void; setRelationKey: (relationKey: TIssueRelationTypes | null) => void; setIssueCrudOperationState: (state: TIssueCrudOperationState) => void; // store @@ -131,8 +131,8 @@ export class IssueDetail implements IIssueDetail { issue: undefined, }, }; - openWidgets: TIssueDetailWidget[] = ["sub-issues", "links", "attachments"]; - lastWidgetAction: TIssueDetailWidget | null = null; + openWidgets: TWorkItemWidgets[] = ["sub-work-items", "links", "attachments"]; + lastWidgetAction: TWorkItemWidgets | null = null; isCreateIssueModalOpen: boolean = false; isIssueLinkModalOpen: boolean = false; isParentIssueModalOpen: string | null = null; @@ -238,14 +238,14 @@ export class IssueDetail implements IIssueDetail { (this.isRelationModalOpen = { issueId, relationType }); toggleSubIssuesModal = (issueId: string | null) => (this.isSubIssuesModalOpen = issueId); toggleDeleteAttachmentModal = (attachmentId: string | null) => (this.attachmentDeleteModalId = attachmentId); - setOpenWidgets = (state: TIssueDetailWidget[]) => { + setOpenWidgets = (state: TWorkItemWidgets[]) => { this.openWidgets = state; if (this.lastWidgetAction) this.lastWidgetAction = null; }; - setLastWidgetAction = (action: TIssueDetailWidget) => { + setLastWidgetAction = (action: TWorkItemWidgets) => { this.openWidgets = [action]; }; - toggleOpenWidget = (state: TIssueDetailWidget) => { + toggleOpenWidget = (state: TWorkItemWidgets) => { if (this.openWidgets && this.openWidgets.includes(state)) this.openWidgets = this.openWidgets.filter((s) => s !== state); else this.openWidgets = [state, ...this.openWidgets]; From 7e21618762acebcdd2f3b8c6cb71b1981fc81fea Mon Sep 17 00:00:00 2001 From: Akshita Goyal <36129505+gakshita@users.noreply.github.com> Date: Mon, 19 May 2025 15:20:57 +0530 Subject: [PATCH 063/201] [WEB-3461] fix: profile activity rendering issue (#7059) * fix: profile activity * fix: icon * fix: handled conversion case * fix: handled conversion case --- web/core/components/core/activity.tsx | 34 +++++++++++++++++-- .../activity/profile-activity-list.tsx | 11 +----- 2 files changed, 33 insertions(+), 12 deletions(-) diff --git a/web/core/components/core/activity.tsx b/web/core/components/core/activity.tsx index 3febacd2d..0a1245725 100644 --- a/web/core/components/core/activity.tsx +++ b/web/core/components/core/activity.tsx @@ -21,7 +21,7 @@ import { UsersIcon, } from "lucide-react"; import { IIssueActivity } from "@plane/types"; -import { Tooltip, BlockedIcon, BlockerIcon, RelatedIcon, LayersIcon, DiceIcon, Intake } from "@plane/ui"; +import { Tooltip, BlockedIcon, BlockerIcon, RelatedIcon, LayersIcon, DiceIcon, Intake, EpicIcon } from "@plane/ui"; // helpers import { renderFormattedDate } from "@/helpers/date-time.helper"; import { generateWorkItemLink } from "@/helpers/issue.helper"; @@ -271,6 +271,12 @@ const activityDetails: { created ); + else if (activity.verb === "converted") + return ( + <> + converted to an epic + + ); else return ( <> @@ -280,6 +286,29 @@ const activityDetails: { }, icon:
); - const message = - activityItem.verb === "created" && - !["cycles", "modules", "attachment", "link", "estimate"].includes(activityItem.field) && - !activityItem.field ? ( - - created - - ) : ( - - ); + const message = ; if ("field" in activityItem && activityItem.field !== "updated_by") return ( From 1fc370973106e7445a1eadf7ffbf2a40b7928f51 Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Mon, 19 May 2025 16:25:46 +0530 Subject: [PATCH 064/201] chore: Strict Null Check in Admin app (#7081) * chore: upgrade to latest version of turbo repo * fix: tsconfig changes * chore: adding format script to package json * fix: formatting of files --- admin/app/authentication/github/form.tsx | 6 +- .../admin-sidebar/sidebar-dropdown.tsx | 2 +- admin/core/store/instance.store.ts | 2 +- .../authentication/authentication-modes.tsx | 2 +- admin/package.json | 1 + admin/tsconfig.json | 9 ++- package.json | 2 +- yarn.lock | 68 +++++++++---------- 8 files changed, 47 insertions(+), 45 deletions(-) diff --git a/admin/app/authentication/github/form.tsx b/admin/app/authentication/github/form.tsx index 91795ea70..0c6d81ae6 100644 --- a/admin/app/authentication/github/form.tsx +++ b/admin/app/authentication/github/form.tsx @@ -98,11 +98,7 @@ export const InstanceGithubConfigForm: FC = (props) => { key: "GITHUB_ORGANIZATION_ID", type: "text", label: "Organization ID", - description: ( - <> - The organization github ID. - - ), + description: <>The organization github ID., placeholder: "123456789", error: Boolean(errors.GITHUB_ORGANIZATION_ID), required: false, diff --git a/admin/core/components/admin-sidebar/sidebar-dropdown.tsx b/admin/core/components/admin-sidebar/sidebar-dropdown.tsx index 501d501d8..0cde7f551 100644 --- a/admin/core/components/admin-sidebar/sidebar-dropdown.tsx +++ b/admin/core/components/admin-sidebar/sidebar-dropdown.tsx @@ -7,7 +7,7 @@ import { LogOut, UserCog2, Palette } from "lucide-react"; import { Menu, Transition } from "@headlessui/react"; // plane internal packages import { API_BASE_URL } from "@plane/constants"; -import {AuthService } from "@plane/services"; +import { AuthService } from "@plane/services"; import { Avatar } from "@plane/ui"; import { getFileURL, cn } from "@plane/utils"; // hooks diff --git a/admin/core/store/instance.store.ts b/admin/core/store/instance.store.ts index 9b25a2469..33954fe73 100644 --- a/admin/core/store/instance.store.ts +++ b/admin/core/store/instance.store.ts @@ -2,7 +2,7 @@ import set from "lodash/set"; import { observable, action, computed, makeObservable, runInAction } from "mobx"; // plane internal packages import { EInstanceStatus, TInstanceStatus } from "@plane/constants"; -import {InstanceService} from "@plane/services"; +import { InstanceService } from "@plane/services"; import { IInstance, IInstanceAdmin, diff --git a/admin/ee/components/authentication/authentication-modes.tsx b/admin/ee/components/authentication/authentication-modes.tsx index 3a8ab7d1d..4e3b05a52 100644 --- a/admin/ee/components/authentication/authentication-modes.tsx +++ b/admin/ee/components/authentication/authentication-modes.tsx @@ -1 +1 @@ -export * from "ce/components/authentication/authentication-modes"; \ No newline at end of file +export * from "ce/components/authentication/authentication-modes"; diff --git a/admin/package.json b/admin/package.json index 0ee292f03..15de77757 100644 --- a/admin/package.json +++ b/admin/package.json @@ -10,6 +10,7 @@ "build": "next build", "preview": "next build && next start", "start": "next start", + "format": "prettier --write .", "lint": "eslint . --ext .ts,.tsx", "lint:errors": "eslint . --ext .ts,.tsx --quiet" }, diff --git a/admin/tsconfig.json b/admin/tsconfig.json index e32f01e6a..df72d07b4 100644 --- a/admin/tsconfig.json +++ b/admin/tsconfig.json @@ -1,14 +1,19 @@ { "extends": "@plane/typescript-config/nextjs.json", "compilerOptions": { - "plugins": [{ "name": "next" }], + "plugins": [ + { + "name": "next" + } + ], "baseUrl": ".", "paths": { "@/*": ["core/*"], "@/public/*": ["public/*"], "@/plane-admin/*": ["ce/*"], "@/styles/*": ["styles/*"] - } + }, + "strictNullChecks": true }, "include": ["next-env.d.ts", "next.config.js", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "exclude": ["node_modules"] diff --git a/package.json b/package.json index 152ab6a23..593d84459 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "devDependencies": { "prettier": "latest", "prettier-plugin-tailwindcss": "^0.5.4", - "turbo": "^2.5.2" + "turbo": "^2.5.3" }, "resolutions": { "nanoid": "3.3.8", diff --git a/yarn.lock b/yarn.lock index d3211c449..6976451fa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11183,47 +11183,47 @@ tunnel-agent@^0.6.0: dependencies: safe-buffer "^5.0.1" -turbo-darwin-64@2.5.2: - version "2.5.2" - resolved "https://registry.npmjs.org/turbo-darwin-64/-/turbo-darwin-64-2.5.2.tgz#892927344ad37679143555ebdf41ad704f4d72d8" - integrity sha512-2aIl0Sx230nLk+Cg2qSVxvPOBWCZpwKNuAMKoROTvWKif6VMpkWWiR9XEPoz7sHeLmCOed4GYGMjL1bqAiIS/g== +turbo-darwin-64@2.5.3: + version "2.5.3" + resolved "https://registry.yarnpkg.com/turbo-darwin-64/-/turbo-darwin-64-2.5.3.tgz#e1f19e816f76e0d636e31e66f8238c43bf870f45" + integrity sha512-YSItEVBUIvAGPUDpAB9etEmSqZI3T6BHrkBkeSErvICXn3dfqXUfeLx35LfptLDEbrzFUdwYFNmt8QXOwe9yaw== -turbo-darwin-arm64@2.5.2: - version "2.5.2" - resolved "https://registry.npmjs.org/turbo-darwin-arm64/-/turbo-darwin-arm64-2.5.2.tgz#c3a00bcba481f5baa24ce4b1302f09c2c50d4e30" - integrity sha512-MrFYhK/jYu8N6QlqZtqSHi3e4QVxlzqU3ANHTKn3/tThuwTLbNHEvzBPWSj5W7nZcM58dCqi6gYrfRz6bJZyAA== +turbo-darwin-arm64@2.5.3: + version "2.5.3" + resolved "https://registry.yarnpkg.com/turbo-darwin-arm64/-/turbo-darwin-arm64-2.5.3.tgz#f80074fd786f703bcb0415e13df225ba781950fc" + integrity sha512-5PefrwHd42UiZX7YA9m1LPW6x9YJBDErXmsegCkVp+GjmWrADfEOxpFrGQNonH3ZMj77WZB2PVE5Aw3gA+IOhg== -turbo-linux-64@2.5.2: - version "2.5.2" - resolved "https://registry.npmjs.org/turbo-linux-64/-/turbo-linux-64-2.5.2.tgz#1f366e0208b4da79d13d57df202dc5f49fb0367c" - integrity sha512-LxNqUE2HmAJQ/8deoLgMUDzKxd5bKxqH0UBogWa+DF+JcXhtze3UTMr6lEr0dEofdsEUYK1zg8FRjglmwlN5YA== +turbo-linux-64@2.5.3: + version "2.5.3" + resolved "https://registry.yarnpkg.com/turbo-linux-64/-/turbo-linux-64-2.5.3.tgz#93bfe009a24a76295c8164896845b5098c293293" + integrity sha512-M9xigFgawn5ofTmRzvjjLj3Lqc05O8VHKuOlWNUlnHPUltFquyEeSkpQNkE/vpPdOR14AzxqHbhhxtfS4qvb1w== -turbo-linux-arm64@2.5.2: - version "2.5.2" - resolved "https://registry.npmjs.org/turbo-linux-arm64/-/turbo-linux-arm64-2.5.2.tgz#a48d9f1eddd279d523682eb74911b5c045ae1092" - integrity sha512-0MI1Ao1q8zhd+UUbIEsrM+yLq1BsrcJQRGZkxIsHFlGp7WQQH1oR3laBgfnUCNdCotCMD6w4moc9pUbXdOR3bg== +turbo-linux-arm64@2.5.3: + version "2.5.3" + resolved "https://registry.yarnpkg.com/turbo-linux-arm64/-/turbo-linux-arm64-2.5.3.tgz#bf4664561094711aa289d92b9443de2aefca5d6e" + integrity sha512-auJRbYZ8SGJVqvzTikpg1bsRAsiI9Tk0/SDkA5Xgg0GdiHDH/BOzv1ZjDE2mjmlrO/obr19Dw+39OlMhwLffrw== -turbo-windows-64@2.5.2: - version "2.5.2" - resolved "https://registry.npmjs.org/turbo-windows-64/-/turbo-windows-64-2.5.2.tgz#6ad0604fd7b1e53c54feca037de74e4303e23fa1" - integrity sha512-hOLcbgZzE5ttACHHyc1ajmWYq4zKT42IC3G6XqgiXxMbS+4eyVYTL+7UvCZBd3Kca1u4TLQdLQjeO76zyDJc2A== +turbo-windows-64@2.5.3: + version "2.5.3" + resolved "https://registry.yarnpkg.com/turbo-windows-64/-/turbo-windows-64-2.5.3.tgz#acbc2db093c7a74f0e692b899e649284285a2e7b" + integrity sha512-arLQYohuHtIEKkmQSCU9vtrKUg+/1TTstWB9VYRSsz+khvg81eX6LYHtXJfH/dK7Ho6ck+JaEh5G+QrE1jEmCQ== -turbo-windows-arm64@2.5.2: - version "2.5.2" - resolved "https://registry.npmjs.org/turbo-windows-arm64/-/turbo-windows-arm64-2.5.2.tgz#8bf9e79d2f3adf92371ef79da89c04090f3ab914" - integrity sha512-fMU41ABhSLa18H8V3Z7BMCGynQ8x+wj9WyBMvWm1jeyRKgkvUYJsO2vkIsy8m0vrwnIeVXKOIn6eSe1ddlBVqw== +turbo-windows-arm64@2.5.3: + version "2.5.3" + resolved "https://registry.yarnpkg.com/turbo-windows-arm64/-/turbo-windows-arm64-2.5.3.tgz#78e8cfdb49b69fbadf03031532f1524be3661729" + integrity sha512-3JPn66HAynJ0gtr6H+hjY4VHpu1RPKcEwGATvGUTmLmYSYBQieVlnGDRMMoYN066YfyPqnNGCfhYbXfH92Cm0g== -turbo@^2.5.2: - version "2.5.2" - resolved "https://registry.npmjs.org/turbo/-/turbo-2.5.2.tgz#c6be6379f7495166fb0cc87d5362da8c7e4093c6" - integrity sha512-Qo5lfuStr6LQh3sPQl7kIi243bGU4aHGDQJUf6ylAdGwks30jJFloc9NYHP7Y373+gGU9OS0faA4Mb5Sy8X9Xw== +turbo@^2.5.3: + version "2.5.3" + resolved "https://registry.yarnpkg.com/turbo/-/turbo-2.5.3.tgz#657dcae430552d9bb237e9e1d91f711c465c9f28" + integrity sha512-iHuaNcq5GZZnr3XDZNuu2LSyCzAOPwDuo5Qt+q64DfsTP1i3T2bKfxJhni2ZQxsvAoxRbuUK5QetJki4qc5aYA== optionalDependencies: - turbo-darwin-64 "2.5.2" - turbo-darwin-arm64 "2.5.2" - turbo-linux-64 "2.5.2" - turbo-linux-arm64 "2.5.2" - turbo-windows-64 "2.5.2" - turbo-windows-arm64 "2.5.2" + turbo-darwin-64 "2.5.3" + turbo-darwin-arm64 "2.5.3" + turbo-linux-64 "2.5.3" + turbo-linux-arm64 "2.5.3" + turbo-windows-64 "2.5.3" + turbo-windows-arm64 "2.5.3" tween-functions@^1.2.0: version "1.2.0" From 75a11ba31adc9e40abef483192e41df3e4a5accc Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Mon, 19 May 2025 17:14:26 +0530 Subject: [PATCH 065/201] fix: polynomial regular expression used on uncontrolled data (#7083) * fix: polynomial regular expression used on uncontrolled data * fix: optimize the function to handle both operations --- packages/editor/src/core/helpers/common.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/editor/src/core/helpers/common.ts b/packages/editor/src/core/helpers/common.ts index 36075caf2..974b111d0 100644 --- a/packages/editor/src/core/helpers/common.ts +++ b/packages/editor/src/core/helpers/common.ts @@ -38,11 +38,10 @@ export const findTableAncestor = (node: Node | null): HTMLTableElement | null => return node as HTMLTableElement; }; -export const getTrimmedHTML = (html: string) => { - html = html.replace(/^(

<\/p>)+/, ""); - html = html.replace(/(

<\/p>)+$/, ""); - return html; -}; +export const getTrimmedHTML = (html: string) => + html + .replace(/^(?:

<\/p>)+/g, "") // Remove from beginning + .replace(/(?:

<\/p>)+$/g, ""); // Remove from end export const isValidHttpUrl = (string: string): { isValid: boolean; url: string } => { // List of potentially dangerous protocols to block From cfac8ce350b966f2bdc9b2ba5aa71f5b803a75d3 Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Mon, 19 May 2025 17:34:46 +0530 Subject: [PATCH 066/201] fix: ruff file formatting based on config file pyproject (#7082) --- apiserver/plane/api/serializers/__init__.py | 2 +- apiserver/plane/api/serializers/issue.py | 15 +- apiserver/plane/app/serializers/favorite.py | 1 + apiserver/plane/app/serializers/workspace.py | 10 +- apiserver/plane/app/views/analytic/advance.py | 1 - apiserver/plane/app/views/cycle/base.py | 5 +- apiserver/plane/app/views/cycle/issue.py | 1 + apiserver/plane/app/views/external/base.py | 69 +++-- apiserver/plane/app/views/issue/archive.py | 1 + apiserver/plane/app/views/issue/attachment.py | 1 + apiserver/plane/app/views/issue/comment.py | 1 + apiserver/plane/app/views/issue/link.py | 1 + apiserver/plane/app/views/issue/reaction.py | 1 + apiserver/plane/app/views/issue/relation.py | 1 + apiserver/plane/app/views/issue/sub_issue.py | 1 + apiserver/plane/app/views/module/base.py | 1 + apiserver/plane/app/views/module/issue.py | 7 +- apiserver/plane/app/views/project/invite.py | 1 + apiserver/plane/app/views/timezone/base.py | 266 ++++++++++-------- apiserver/plane/app/views/workspace/cycle.py | 1 + apiserver/plane/app/views/workspace/draft.py | 1 + .../app/views/workspace/user_preference.py | 23 +- .../plane/authentication/adapter/base.py | 1 + .../plane/authentication/adapter/error.py | 1 - .../authentication/provider/oauth/github.py | 39 +-- apiserver/plane/authentication/utils/login.py | 1 + .../plane/authentication/views/app/email.py | 1 + .../plane/authentication/views/app/github.py | 1 + .../plane/authentication/views/app/gitlab.py | 1 + .../plane/authentication/views/app/google.py | 5 +- .../plane/authentication/views/common.py | 7 +- .../plane/authentication/views/space/magic.py | 2 +- apiserver/plane/bgtasks/export_task.py | 38 ++- .../commands/update_deleted_workspace_slug.py | 6 +- apiserver/plane/db/models/__init__.py | 2 +- apiserver/plane/db/models/workspace.py | 10 +- .../management/commands/configure_instance.py | 2 +- apiserver/plane/settings/storage.py | 1 - apiserver/plane/space/utils/grouper.py | 6 +- apiserver/plane/space/views/meta.py | 4 +- apiserver/plane/utils/analytics_plot.py | 4 +- apiserver/plane/utils/build_chart.py | 1 - apiserver/plane/utils/timezone_converter.py | 4 +- apiserver/pyproject.toml | 2 +- 44 files changed, 319 insertions(+), 231 deletions(-) diff --git a/apiserver/plane/api/serializers/__init__.py b/apiserver/plane/api/serializers/__init__.py index 4eb1457ce..8c84b2328 100644 --- a/apiserver/plane/api/serializers/__init__.py +++ b/apiserver/plane/api/serializers/__init__.py @@ -15,4 +15,4 @@ from .state import StateLiteSerializer, StateSerializer from .cycle import CycleSerializer, CycleIssueSerializer, CycleLiteSerializer from .module import ModuleSerializer, ModuleIssueSerializer, ModuleLiteSerializer from .intake import IntakeIssueSerializer -from .estimate import EstimatePointSerializer \ No newline at end of file +from .estimate import EstimatePointSerializer diff --git a/apiserver/plane/api/serializers/issue.py b/apiserver/plane/api/serializers/issue.py index 82969efe7..10738b97f 100644 --- a/apiserver/plane/api/serializers/issue.py +++ b/apiserver/plane/api/serializers/issue.py @@ -160,12 +160,15 @@ class IssueSerializer(BaseSerializer): else: try: # Then assign it to default assignee, if it is a valid assignee - if default_assignee_id is not None and ProjectMember.objects.filter( - member_id=default_assignee_id, - project_id=project_id, - role__gte=15, - is_active=True - ).exists(): + if ( + default_assignee_id is not None + and ProjectMember.objects.filter( + member_id=default_assignee_id, + project_id=project_id, + role__gte=15, + is_active=True, + ).exists() + ): IssueAssignee.objects.create( assignee_id=default_assignee_id, issue=issue, diff --git a/apiserver/plane/app/serializers/favorite.py b/apiserver/plane/app/serializers/favorite.py index 18f92f3ea..940b8ee82 100644 --- a/apiserver/plane/app/serializers/favorite.py +++ b/apiserver/plane/app/serializers/favorite.py @@ -53,6 +53,7 @@ def get_entity_model_and_serializer(entity_type): } return entity_map.get(entity_type, (None, None)) + class UserFavoriteSerializer(serializers.ModelSerializer): entity_data = serializers.SerializerMethodField() diff --git a/apiserver/plane/app/serializers/workspace.py b/apiserver/plane/app/serializers/workspace.py index 52333c246..9fba7256e 100644 --- a/apiserver/plane/app/serializers/workspace.py +++ b/apiserver/plane/app/serializers/workspace.py @@ -148,7 +148,6 @@ class WorkspaceUserLinkSerializer(BaseSerializer): return value - def create(self, validated_data): # Filtering the WorkspaceUserLink with the given url to check if the link already exists. @@ -157,7 +156,7 @@ class WorkspaceUserLinkSerializer(BaseSerializer): workspace_user_link = WorkspaceUserLink.objects.filter( url=url, workspace_id=validated_data.get("workspace_id"), - owner_id=validated_data.get("owner_id") + owner_id=validated_data.get("owner_id"), ) if workspace_user_link.exists(): @@ -173,10 +172,8 @@ class WorkspaceUserLinkSerializer(BaseSerializer): url = validated_data.get("url") workspace_user_link = WorkspaceUserLink.objects.filter( - url=url, - workspace_id=instance.workspace_id, - owner=instance.owner - ) + url=url, workspace_id=instance.workspace_id, owner=instance.owner + ) if workspace_user_link.exclude(pk=instance.id).exists(): raise serializers.ValidationError( @@ -185,6 +182,7 @@ class WorkspaceUserLinkSerializer(BaseSerializer): return super().update(instance, validated_data) + class IssueRecentVisitSerializer(serializers.ModelSerializer): project_identifier = serializers.SerializerMethodField() diff --git a/apiserver/plane/app/views/analytic/advance.py b/apiserver/plane/app/views/analytic/advance.py index b6c5f1e0b..c55f5566b 100644 --- a/apiserver/plane/app/views/analytic/advance.py +++ b/apiserver/plane/app/views/analytic/advance.py @@ -42,7 +42,6 @@ class AdvanceAnalyticsBaseView(BaseAPIView): class AdvanceAnalyticsEndpoint(AdvanceAnalyticsBaseView): - def get_filtered_counts(self, queryset: QuerySet) -> Dict[str, int]: def get_filtered_count() -> int: if self.filters["analytics_date_range"]: diff --git a/apiserver/plane/app/views/cycle/base.py b/apiserver/plane/app/views/cycle/base.py index 60b051b40..bcce69bf8 100644 --- a/apiserver/plane/app/views/cycle/base.py +++ b/apiserver/plane/app/views/cycle/base.py @@ -1119,14 +1119,13 @@ class CycleUserPropertiesEndpoint(BaseAPIView): class CycleProgressEndpoint(BaseAPIView): @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) def get(self, request, slug, project_id, cycle_id): - cycle = Cycle.objects.filter( workspace__slug=slug, project_id=project_id, id=cycle_id ).first() if not cycle: return Response( {"error": "Cycle not found"}, status=status.HTTP_404_NOT_FOUND - ) + ) aggregate_estimates = ( Issue.issue_objects.filter( estimate_point__estimate__type="points", @@ -1177,7 +1176,7 @@ class CycleProgressEndpoint(BaseAPIView): ), ) ) - if cycle.progress_snapshot: + if cycle.progress_snapshot: backlog_issues = cycle.progress_snapshot.get("backlog_issues", 0) unstarted_issues = cycle.progress_snapshot.get("unstarted_issues", 0) started_issues = cycle.progress_snapshot.get("started_issues", 0) diff --git a/apiserver/plane/app/views/cycle/issue.py b/apiserver/plane/app/views/cycle/issue.py index 9b9e1ad30..ad7762629 100644 --- a/apiserver/plane/app/views/cycle/issue.py +++ b/apiserver/plane/app/views/cycle/issue.py @@ -29,6 +29,7 @@ from plane.utils.paginator import GroupedOffsetPaginator, SubGroupedOffsetPagina from plane.app.permissions import allow_permission, ROLE from plane.utils.host import base_host + class CycleIssueViewSet(BaseViewSet): serializer_class = CycleIssueSerializer model = CycleIssue diff --git a/apiserver/plane/app/views/external/base.py b/apiserver/plane/app/views/external/base.py index 5643da226..864d0ff8c 100644 --- a/apiserver/plane/app/views/external/base.py +++ b/apiserver/plane/app/views/external/base.py @@ -11,8 +11,7 @@ from rest_framework.response import Response # Module import from plane.app.permissions import ROLE, allow_permission -from plane.app.serializers import (ProjectLiteSerializer, - WorkspaceLiteSerializer) +from plane.app.serializers import ProjectLiteSerializer, WorkspaceLiteSerializer from plane.db.models import Project, Workspace from plane.license.utils.instance_value import get_configuration_value from plane.utils.exception_logger import log_exception @@ -22,6 +21,7 @@ from ..base import BaseAPIView class LLMProvider: """Base class for LLM provider configurations""" + name: str = "" models: List[str] = [] default_model: str = "" @@ -34,11 +34,13 @@ class LLMProvider: "default_model": cls.default_model, } + class OpenAIProvider(LLMProvider): name = "OpenAI" models = ["gpt-3.5-turbo", "gpt-4o-mini", "gpt-4o", "o1-mini", "o1-preview"] default_model = "gpt-4o-mini" + class AnthropicProvider(LLMProvider): name = "Anthropic" models = [ @@ -49,40 +51,45 @@ class AnthropicProvider(LLMProvider): "claude-2.1", "claude-2", "claude-instant-1.2", - "claude-instant-1" + "claude-instant-1", ] default_model = "claude-3-sonnet-20240229" + class GeminiProvider(LLMProvider): name = "Gemini" models = ["gemini-pro", "gemini-1.5-pro-latest", "gemini-pro-vision"] default_model = "gemini-pro" + SUPPORTED_PROVIDERS = { "openai": OpenAIProvider, "anthropic": AnthropicProvider, "gemini": GeminiProvider, } + def get_llm_config() -> Tuple[str | None, str | None, str | None]: """ Helper to get LLM configuration values, returns: - api_key, model, provider """ - api_key, provider_key, model = get_configuration_value([ - { - "key": "LLM_API_KEY", - "default": os.environ.get("LLM_API_KEY", None), - }, - { - "key": "LLM_PROVIDER", - "default": os.environ.get("LLM_PROVIDER", "openai"), - }, - { - "key": "LLM_MODEL", - "default": os.environ.get("LLM_MODEL", None), - }, - ]) + api_key, provider_key, model = get_configuration_value( + [ + { + "key": "LLM_API_KEY", + "default": os.environ.get("LLM_API_KEY", None), + }, + { + "key": "LLM_PROVIDER", + "default": os.environ.get("LLM_PROVIDER", "openai"), + }, + { + "key": "LLM_MODEL", + "default": os.environ.get("LLM_MODEL", None), + }, + ] + ) provider = SUPPORTED_PROVIDERS.get(provider_key.lower()) if not provider: @@ -99,16 +106,20 @@ def get_llm_config() -> Tuple[str | None, str | None, str | None]: # Validate model is supported by provider if model not in provider.models: - log_exception(ValueError( - f"Model {model} not supported by {provider.name}. " - f"Supported models: {', '.join(provider.models)}" - )) + log_exception( + ValueError( + f"Model {model} not supported by {provider.name}. " + f"Supported models: {', '.join(provider.models)}" + ) + ) return None, None, None return api_key, model, provider_key -def get_llm_response(task, prompt, api_key: str, model: str, provider: str) -> Tuple[str | None, str | None]: +def get_llm_response( + task, prompt, api_key: str, model: str, provider: str +) -> Tuple[str | None, str | None]: """Helper to get LLM completion response""" final_text = task + "\n" + prompt try: @@ -118,10 +129,7 @@ def get_llm_response(task, prompt, api_key: str, model: str, provider: str) -> T client = OpenAI(api_key=api_key) chat_completion = client.chat.completions.create( - model=model, - messages=[ - {"role": "user", "content": final_text} - ] + model=model, messages=[{"role": "user", "content": final_text}] ) text = chat_completion.choices[0].message.content return text, None @@ -135,6 +143,7 @@ def get_llm_response(task, prompt, api_key: str, model: str, provider: str) -> T else: return None, f"Error occurred while generating response from {provider}" + class GPTIntegrationEndpoint(BaseAPIView): @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) def post(self, request, slug, project_id): @@ -152,7 +161,9 @@ class GPTIntegrationEndpoint(BaseAPIView): {"error": "Task is required"}, status=status.HTTP_400_BAD_REQUEST ) - text, error = get_llm_response(task, request.data.get("prompt", False), api_key, model, provider) + text, error = get_llm_response( + task, request.data.get("prompt", False), api_key, model, provider + ) if not text and error: return Response( {"error": "An internal error has occurred."}, @@ -190,7 +201,9 @@ class WorkspaceGPTIntegrationEndpoint(BaseAPIView): {"error": "Task is required"}, status=status.HTTP_400_BAD_REQUEST ) - text, error = get_llm_response(task, request.data.get("prompt", False), api_key, model, provider) + text, error = get_llm_response( + task, request.data.get("prompt", False), api_key, model, provider + ) if not text and error: return Response( {"error": "An internal error has occurred."}, diff --git a/apiserver/plane/app/views/issue/archive.py b/apiserver/plane/app/views/issue/archive.py index 48b317c84..118d1e7f9 100644 --- a/apiserver/plane/app/views/issue/archive.py +++ b/apiserver/plane/app/views/issue/archive.py @@ -38,6 +38,7 @@ from plane.utils.paginator import GroupedOffsetPaginator, SubGroupedOffsetPagina from plane.app.permissions import allow_permission, ROLE from plane.utils.error_codes import ERROR_CODES from plane.utils.host import base_host + # Module imports from .. import BaseViewSet, BaseAPIView diff --git a/apiserver/plane/app/views/issue/attachment.py b/apiserver/plane/app/views/issue/attachment.py index 0ff85572f..423710e4a 100644 --- a/apiserver/plane/app/views/issue/attachment.py +++ b/apiserver/plane/app/views/issue/attachment.py @@ -23,6 +23,7 @@ from plane.settings.storage import S3Storage from plane.bgtasks.storage_metadata_task import get_asset_object_metadata from plane.utils.host import base_host + class IssueAttachmentEndpoint(BaseAPIView): serializer_class = IssueAttachmentSerializer model = FileAsset diff --git a/apiserver/plane/app/views/issue/comment.py b/apiserver/plane/app/views/issue/comment.py index 2d81201c9..c848b6fcf 100644 --- a/apiserver/plane/app/views/issue/comment.py +++ b/apiserver/plane/app/views/issue/comment.py @@ -19,6 +19,7 @@ from plane.db.models import IssueComment, ProjectMember, CommentReaction, Projec from plane.bgtasks.issue_activities_task import issue_activity from plane.utils.host import base_host + class IssueCommentViewSet(BaseViewSet): serializer_class = IssueCommentSerializer model = IssueComment diff --git a/apiserver/plane/app/views/issue/link.py b/apiserver/plane/app/views/issue/link.py index 45cab8479..d2641e0a4 100644 --- a/apiserver/plane/app/views/issue/link.py +++ b/apiserver/plane/app/views/issue/link.py @@ -17,6 +17,7 @@ from plane.db.models import IssueLink from plane.bgtasks.issue_activities_task import issue_activity from plane.utils.host import base_host + class IssueLinkViewSet(BaseViewSet): permission_classes = [ProjectEntityPermission] diff --git a/apiserver/plane/app/views/issue/reaction.py b/apiserver/plane/app/views/issue/reaction.py index b92970382..8700b6345 100644 --- a/apiserver/plane/app/views/issue/reaction.py +++ b/apiserver/plane/app/views/issue/reaction.py @@ -17,6 +17,7 @@ from plane.db.models import IssueReaction from plane.bgtasks.issue_activities_task import issue_activity from plane.utils.host import base_host + class IssueReactionViewSet(BaseViewSet): serializer_class = IssueReactionSerializer model = IssueReaction diff --git a/apiserver/plane/app/views/issue/relation.py b/apiserver/plane/app/views/issue/relation.py index 0a8ffa2f9..50d319a88 100644 --- a/apiserver/plane/app/views/issue/relation.py +++ b/apiserver/plane/app/views/issue/relation.py @@ -29,6 +29,7 @@ from plane.bgtasks.issue_activities_task import issue_activity from plane.utils.issue_relation_mapper import get_actual_relation from plane.utils.host import base_host + class IssueRelationViewSet(BaseViewSet): serializer_class = IssueRelationSerializer model = IssueRelation diff --git a/apiserver/plane/app/views/issue/sub_issue.py b/apiserver/plane/app/views/issue/sub_issue.py index 5791281f0..0843a9a51 100644 --- a/apiserver/plane/app/views/issue/sub_issue.py +++ b/apiserver/plane/app/views/issue/sub_issue.py @@ -25,6 +25,7 @@ from collections import defaultdict from plane.utils.host import base_host from plane.utils.order_queryset import order_issue_queryset + class SubIssuesEndpoint(BaseAPIView): permission_classes = [ProjectEntityPermission] diff --git a/apiserver/plane/app/views/module/base.py b/apiserver/plane/app/views/module/base.py index 829f7a6b6..69d48ae59 100644 --- a/apiserver/plane/app/views/module/base.py +++ b/apiserver/plane/app/views/module/base.py @@ -63,6 +63,7 @@ from .. import BaseAPIView, BaseViewSet from plane.bgtasks.recent_visited_task import recent_visited_task from plane.utils.host import base_host + class ModuleViewSet(BaseViewSet): model = Module webhook_event = "module" diff --git a/apiserver/plane/app/views/module/issue.py b/apiserver/plane/app/views/module/issue.py index 089d73ef9..96d1f550a 100644 --- a/apiserver/plane/app/views/module/issue.py +++ b/apiserver/plane/app/views/module/issue.py @@ -36,6 +36,7 @@ from plane.utils.paginator import GroupedOffsetPaginator, SubGroupedOffsetPagina from .. import BaseViewSet from plane.utils.host import base_host + class ModuleIssueViewSet(BaseViewSet): serializer_class = ModuleIssueSerializer model = ModuleIssue @@ -280,7 +281,11 @@ class ModuleIssueViewSet(BaseViewSet): issue_id=str(issue_id), project_id=str(project_id), current_instance=json.dumps( - {"module_name": module_issue.first().module.name if (module_issue.first() and module_issue.first().module) else None} + { + "module_name": module_issue.first().module.name + if (module_issue.first() and module_issue.first().module) + else None + } ), epoch=int(timezone.now().timestamp()), notification=True, diff --git a/apiserver/plane/app/views/project/invite.py b/apiserver/plane/app/views/project/invite.py index 72c0bae06..c7ae8b19c 100644 --- a/apiserver/plane/app/views/project/invite.py +++ b/apiserver/plane/app/views/project/invite.py @@ -29,6 +29,7 @@ from plane.db.models import ( from plane.db.models.project import ProjectNetwork from plane.utils.host import base_host + class ProjectInvitationsViewset(BaseViewSet): serializer_class = ProjectMemberInviteSerializer model = ProjectMemberInvite diff --git a/apiserver/plane/app/views/timezone/base.py b/apiserver/plane/app/views/timezone/base.py index 840fdbdbc..21d1d3560 100644 --- a/apiserver/plane/app/views/timezone/base.py +++ b/apiserver/plane/app/views/timezone/base.py @@ -24,125 +24,152 @@ class TimezoneEndpoint(APIView): @method_decorator(cache_page(60 * 60 * 2)) def get(self, request): timezone_locations = [ - ('Midway Island', 'Pacific/Midway'), # UTC-11:00 - ('American Samoa', 'Pacific/Pago_Pago'), # UTC-11:00 - ('Hawaii', 'Pacific/Honolulu'), # UTC-10:00 - ('Aleutian Islands', 'America/Adak'), # UTC-10:00 (DST: UTC-09:00) - ('Marquesas Islands', 'Pacific/Marquesas'), # UTC-09:30 - ('Alaska', 'America/Anchorage'), # UTC-09:00 (DST: UTC-08:00) - ('Gambier Islands', 'Pacific/Gambier'), # UTC-09:00 - ('Pacific Time (US and Canada)', 'America/Los_Angeles'), # UTC-08:00 (DST: UTC-07:00) - ('Baja California', 'America/Tijuana'), # UTC-08:00 (DST: UTC-07:00) - ('Mountain Time (US and Canada)', 'America/Denver'), # UTC-07:00 (DST: UTC-06:00) - ('Arizona', 'America/Phoenix'), # UTC-07:00 - ('Chihuahua, Mazatlan', 'America/Chihuahua'), # UTC-07:00 (DST: UTC-06:00) - ('Central Time (US and Canada)', 'America/Chicago'), # UTC-06:00 (DST: UTC-05:00) - ('Saskatchewan', 'America/Regina'), # UTC-06:00 - ('Guadalajara, Mexico City, Monterrey', 'America/Mexico_City'), # UTC-06:00 (DST: UTC-05:00) - ('Tegucigalpa, Honduras', 'America/Tegucigalpa'), # UTC-06:00 - ('Costa Rica', 'America/Costa_Rica'), # UTC-06:00 - ('Eastern Time (US and Canada)', 'America/New_York'), # UTC-05:00 (DST: UTC-04:00) - ('Lima', 'America/Lima'), # UTC-05:00 - ('Bogota', 'America/Bogota'), # UTC-05:00 - ('Quito', 'America/Guayaquil'), # UTC-05:00 - ('Chetumal', 'America/Cancun'), # UTC-05:00 (DST: UTC-04:00) - ('Caracas (Old Venezuela Time)', 'America/Caracas'), # UTC-04:30 - ('Atlantic Time (Canada)', 'America/Halifax'), # UTC-04:00 (DST: UTC-03:00) - ('Caracas', 'America/Caracas'), # UTC-04:00 - ('Santiago', 'America/Santiago'), # UTC-04:00 (DST: UTC-03:00) - ('La Paz', 'America/La_Paz'), # UTC-04:00 - ('Manaus', 'America/Manaus'), # UTC-04:00 - ('Georgetown', 'America/Guyana'), # UTC-04:00 - ('Bermuda', 'Atlantic/Bermuda'), # UTC-04:00 (DST: UTC-03:00) - ('Newfoundland Time (Canada)', 'America/St_Johns'), # UTC-03:30 (DST: UTC-02:30) - ('Buenos Aires', 'America/Argentina/Buenos_Aires'), # UTC-03:00 - ('Brasilia', 'America/Sao_Paulo'), # UTC-03:00 - ('Greenland', 'America/Godthab'), # UTC-03:00 (DST: UTC-02:00) - ('Montevideo', 'America/Montevideo'), # UTC-03:00 - ('Falkland Islands', 'Atlantic/Stanley'), # UTC-03:00 - ('South Georgia and the South Sandwich Islands', 'Atlantic/South_Georgia'), # UTC-02:00 - ('Azores', 'Atlantic/Azores'), # UTC-01:00 (DST: UTC+00:00) - ('Cape Verde Islands', 'Atlantic/Cape_Verde'), # UTC-01:00 - ('Dublin', 'Europe/Dublin'), # UTC+00:00 (DST: UTC+01:00) - ('Reykjavik', 'Atlantic/Reykjavik'), # UTC+00:00 - ('Lisbon', 'Europe/Lisbon'), # UTC+00:00 (DST: UTC+01:00) - ('Monrovia', 'Africa/Monrovia'), # UTC+00:00 - ('Casablanca', 'Africa/Casablanca'), # UTC+00:00 (DST: UTC+01:00) - ('Central European Time (Berlin, Rome, Paris)', 'Europe/Paris'), # UTC+01:00 (DST: UTC+02:00) - ('West Central Africa', 'Africa/Lagos'), # UTC+01:00 - ('Algiers', 'Africa/Algiers'), # UTC+01:00 - ('Lagos', 'Africa/Lagos'), # UTC+01:00 - ('Tunis', 'Africa/Tunis'), # UTC+01:00 - ('Eastern European Time (Cairo, Helsinki, Kyiv)', 'Europe/Kiev'), # UTC+02:00 (DST: UTC+03:00) - ('Athens', 'Europe/Athens'), # UTC+02:00 (DST: UTC+03:00) - ('Jerusalem', 'Asia/Jerusalem'), # UTC+02:00 (DST: UTC+03:00) - ('Johannesburg', 'Africa/Johannesburg'), # UTC+02:00 - ('Harare, Pretoria', 'Africa/Harare'), # UTC+02:00 - ('Moscow Time', 'Europe/Moscow'), # UTC+03:00 - ('Baghdad', 'Asia/Baghdad'), # UTC+03:00 - ('Nairobi', 'Africa/Nairobi'), # UTC+03:00 - ('Kuwait, Riyadh', 'Asia/Riyadh'), # UTC+03:00 - ('Tehran', 'Asia/Tehran'), # UTC+03:30 (DST: UTC+04:30) - ('Abu Dhabi', 'Asia/Dubai'), # UTC+04:00 - ('Baku', 'Asia/Baku'), # UTC+04:00 (DST: UTC+05:00) - ('Yerevan', 'Asia/Yerevan'), # UTC+04:00 (DST: UTC+05:00) - ('Astrakhan', 'Europe/Astrakhan'), # UTC+04:00 - ('Tbilisi', 'Asia/Tbilisi'), # UTC+04:00 - ('Mauritius', 'Indian/Mauritius'), # UTC+04:00 - ('Islamabad', 'Asia/Karachi'), # UTC+05:00 - ('Karachi', 'Asia/Karachi'), # UTC+05:00 - ('Tashkent', 'Asia/Tashkent'), # UTC+05:00 - ('Yekaterinburg', 'Asia/Yekaterinburg'), # UTC+05:00 - ('Maldives', 'Indian/Maldives'), # UTC+05:00 - ('Chagos', 'Indian/Chagos'), # UTC+05:00 - ('Chennai', 'Asia/Kolkata'), # UTC+05:30 - ('Kolkata', 'Asia/Kolkata'), # UTC+05:30 - ('Mumbai', 'Asia/Kolkata'), # UTC+05:30 - ('New Delhi', 'Asia/Kolkata'), # UTC+05:30 - ('Sri Jayawardenepura', 'Asia/Colombo'), # UTC+05:30 - ('Kathmandu', 'Asia/Kathmandu'), # UTC+05:45 - ('Dhaka', 'Asia/Dhaka'), # UTC+06:00 - ('Almaty', 'Asia/Almaty'), # UTC+06:00 - ('Bishkek', 'Asia/Bishkek'), # UTC+06:00 - ('Thimphu', 'Asia/Thimphu'), # UTC+06:00 - ('Yangon (Rangoon)', 'Asia/Yangon'), # UTC+06:30 - ('Cocos Islands', 'Indian/Cocos'), # UTC+06:30 - ('Bangkok', 'Asia/Bangkok'), # UTC+07:00 - ('Hanoi', 'Asia/Ho_Chi_Minh'), # UTC+07:00 - ('Jakarta', 'Asia/Jakarta'), # UTC+07:00 - ('Novosibirsk', 'Asia/Novosibirsk'), # UTC+07:00 - ('Krasnoyarsk', 'Asia/Krasnoyarsk'), # UTC+07:00 - ('Beijing', 'Asia/Shanghai'), # UTC+08:00 - ('Singapore', 'Asia/Singapore'), # UTC+08:00 - ('Perth', 'Australia/Perth'), # UTC+08:00 - ('Hong Kong', 'Asia/Hong_Kong'), # UTC+08:00 - ('Ulaanbaatar', 'Asia/Ulaanbaatar'), # UTC+08:00 - ('Palau', 'Pacific/Palau'), # UTC+08:00 - ('Eucla', 'Australia/Eucla'), # UTC+08:45 - ('Tokyo', 'Asia/Tokyo'), # UTC+09:00 - ('Seoul', 'Asia/Seoul'), # UTC+09:00 - ('Yakutsk', 'Asia/Yakutsk'), # UTC+09:00 - ('Adelaide', 'Australia/Adelaide'), # UTC+09:30 (DST: UTC+10:30) - ('Darwin', 'Australia/Darwin'), # UTC+09:30 - ('Sydney', 'Australia/Sydney'), # UTC+10:00 (DST: UTC+11:00) - ('Brisbane', 'Australia/Brisbane'), # UTC+10:00 - ('Guam', 'Pacific/Guam'), # UTC+10:00 - ('Vladivostok', 'Asia/Vladivostok'), # UTC+10:00 - ('Tahiti', 'Pacific/Tahiti'), # UTC+10:00 - ('Lord Howe Island', 'Australia/Lord_Howe'), # UTC+10:30 (DST: UTC+11:00) - ('Solomon Islands', 'Pacific/Guadalcanal'), # UTC+11:00 - ('Magadan', 'Asia/Magadan'), # UTC+11:00 - ('Norfolk Island', 'Pacific/Norfolk'), # UTC+11:00 - ('Bougainville Island', 'Pacific/Bougainville'), # UTC+11:00 - ('Chokurdakh', 'Asia/Srednekolymsk'), # UTC+11:00 - ('Auckland', 'Pacific/Auckland'), # UTC+12:00 (DST: UTC+13:00) - ('Wellington', 'Pacific/Auckland'), # UTC+12:00 (DST: UTC+13:00) - ('Fiji Islands', 'Pacific/Fiji'), # UTC+12:00 (DST: UTC+13:00) - ('Anadyr', 'Asia/Anadyr'), # UTC+12:00 - ('Chatham Islands', 'Pacific/Chatham'), # UTC+12:45 (DST: UTC+13:45) - ("Nuku'alofa", 'Pacific/Tongatapu'), # UTC+13:00 - ('Samoa', 'Pacific/Apia'), # UTC+13:00 (DST: UTC+14:00) - ('Kiritimati Island', 'Pacific/Kiritimati') # UTC+14:00 + ("Midway Island", "Pacific/Midway"), # UTC-11:00 + ("American Samoa", "Pacific/Pago_Pago"), # UTC-11:00 + ("Hawaii", "Pacific/Honolulu"), # UTC-10:00 + ("Aleutian Islands", "America/Adak"), # UTC-10:00 (DST: UTC-09:00) + ("Marquesas Islands", "Pacific/Marquesas"), # UTC-09:30 + ("Alaska", "America/Anchorage"), # UTC-09:00 (DST: UTC-08:00) + ("Gambier Islands", "Pacific/Gambier"), # UTC-09:00 + ( + "Pacific Time (US and Canada)", + "America/Los_Angeles", + ), # UTC-08:00 (DST: UTC-07:00) + ("Baja California", "America/Tijuana"), # UTC-08:00 (DST: UTC-07:00) + ( + "Mountain Time (US and Canada)", + "America/Denver", + ), # UTC-07:00 (DST: UTC-06:00) + ("Arizona", "America/Phoenix"), # UTC-07:00 + ("Chihuahua, Mazatlan", "America/Chihuahua"), # UTC-07:00 (DST: UTC-06:00) + ( + "Central Time (US and Canada)", + "America/Chicago", + ), # UTC-06:00 (DST: UTC-05:00) + ("Saskatchewan", "America/Regina"), # UTC-06:00 + ( + "Guadalajara, Mexico City, Monterrey", + "America/Mexico_City", + ), # UTC-06:00 (DST: UTC-05:00) + ("Tegucigalpa, Honduras", "America/Tegucigalpa"), # UTC-06:00 + ("Costa Rica", "America/Costa_Rica"), # UTC-06:00 + ( + "Eastern Time (US and Canada)", + "America/New_York", + ), # UTC-05:00 (DST: UTC-04:00) + ("Lima", "America/Lima"), # UTC-05:00 + ("Bogota", "America/Bogota"), # UTC-05:00 + ("Quito", "America/Guayaquil"), # UTC-05:00 + ("Chetumal", "America/Cancun"), # UTC-05:00 (DST: UTC-04:00) + ("Caracas (Old Venezuela Time)", "America/Caracas"), # UTC-04:30 + ("Atlantic Time (Canada)", "America/Halifax"), # UTC-04:00 (DST: UTC-03:00) + ("Caracas", "America/Caracas"), # UTC-04:00 + ("Santiago", "America/Santiago"), # UTC-04:00 (DST: UTC-03:00) + ("La Paz", "America/La_Paz"), # UTC-04:00 + ("Manaus", "America/Manaus"), # UTC-04:00 + ("Georgetown", "America/Guyana"), # UTC-04:00 + ("Bermuda", "Atlantic/Bermuda"), # UTC-04:00 (DST: UTC-03:00) + ( + "Newfoundland Time (Canada)", + "America/St_Johns", + ), # UTC-03:30 (DST: UTC-02:30) + ("Buenos Aires", "America/Argentina/Buenos_Aires"), # UTC-03:00 + ("Brasilia", "America/Sao_Paulo"), # UTC-03:00 + ("Greenland", "America/Godthab"), # UTC-03:00 (DST: UTC-02:00) + ("Montevideo", "America/Montevideo"), # UTC-03:00 + ("Falkland Islands", "Atlantic/Stanley"), # UTC-03:00 + ( + "South Georgia and the South Sandwich Islands", + "Atlantic/South_Georgia", + ), # UTC-02:00 + ("Azores", "Atlantic/Azores"), # UTC-01:00 (DST: UTC+00:00) + ("Cape Verde Islands", "Atlantic/Cape_Verde"), # UTC-01:00 + ("Dublin", "Europe/Dublin"), # UTC+00:00 (DST: UTC+01:00) + ("Reykjavik", "Atlantic/Reykjavik"), # UTC+00:00 + ("Lisbon", "Europe/Lisbon"), # UTC+00:00 (DST: UTC+01:00) + ("Monrovia", "Africa/Monrovia"), # UTC+00:00 + ("Casablanca", "Africa/Casablanca"), # UTC+00:00 (DST: UTC+01:00) + ( + "Central European Time (Berlin, Rome, Paris)", + "Europe/Paris", + ), # UTC+01:00 (DST: UTC+02:00) + ("West Central Africa", "Africa/Lagos"), # UTC+01:00 + ("Algiers", "Africa/Algiers"), # UTC+01:00 + ("Lagos", "Africa/Lagos"), # UTC+01:00 + ("Tunis", "Africa/Tunis"), # UTC+01:00 + ( + "Eastern European Time (Cairo, Helsinki, Kyiv)", + "Europe/Kiev", + ), # UTC+02:00 (DST: UTC+03:00) + ("Athens", "Europe/Athens"), # UTC+02:00 (DST: UTC+03:00) + ("Jerusalem", "Asia/Jerusalem"), # UTC+02:00 (DST: UTC+03:00) + ("Johannesburg", "Africa/Johannesburg"), # UTC+02:00 + ("Harare, Pretoria", "Africa/Harare"), # UTC+02:00 + ("Moscow Time", "Europe/Moscow"), # UTC+03:00 + ("Baghdad", "Asia/Baghdad"), # UTC+03:00 + ("Nairobi", "Africa/Nairobi"), # UTC+03:00 + ("Kuwait, Riyadh", "Asia/Riyadh"), # UTC+03:00 + ("Tehran", "Asia/Tehran"), # UTC+03:30 (DST: UTC+04:30) + ("Abu Dhabi", "Asia/Dubai"), # UTC+04:00 + ("Baku", "Asia/Baku"), # UTC+04:00 (DST: UTC+05:00) + ("Yerevan", "Asia/Yerevan"), # UTC+04:00 (DST: UTC+05:00) + ("Astrakhan", "Europe/Astrakhan"), # UTC+04:00 + ("Tbilisi", "Asia/Tbilisi"), # UTC+04:00 + ("Mauritius", "Indian/Mauritius"), # UTC+04:00 + ("Islamabad", "Asia/Karachi"), # UTC+05:00 + ("Karachi", "Asia/Karachi"), # UTC+05:00 + ("Tashkent", "Asia/Tashkent"), # UTC+05:00 + ("Yekaterinburg", "Asia/Yekaterinburg"), # UTC+05:00 + ("Maldives", "Indian/Maldives"), # UTC+05:00 + ("Chagos", "Indian/Chagos"), # UTC+05:00 + ("Chennai", "Asia/Kolkata"), # UTC+05:30 + ("Kolkata", "Asia/Kolkata"), # UTC+05:30 + ("Mumbai", "Asia/Kolkata"), # UTC+05:30 + ("New Delhi", "Asia/Kolkata"), # UTC+05:30 + ("Sri Jayawardenepura", "Asia/Colombo"), # UTC+05:30 + ("Kathmandu", "Asia/Kathmandu"), # UTC+05:45 + ("Dhaka", "Asia/Dhaka"), # UTC+06:00 + ("Almaty", "Asia/Almaty"), # UTC+06:00 + ("Bishkek", "Asia/Bishkek"), # UTC+06:00 + ("Thimphu", "Asia/Thimphu"), # UTC+06:00 + ("Yangon (Rangoon)", "Asia/Yangon"), # UTC+06:30 + ("Cocos Islands", "Indian/Cocos"), # UTC+06:30 + ("Bangkok", "Asia/Bangkok"), # UTC+07:00 + ("Hanoi", "Asia/Ho_Chi_Minh"), # UTC+07:00 + ("Jakarta", "Asia/Jakarta"), # UTC+07:00 + ("Novosibirsk", "Asia/Novosibirsk"), # UTC+07:00 + ("Krasnoyarsk", "Asia/Krasnoyarsk"), # UTC+07:00 + ("Beijing", "Asia/Shanghai"), # UTC+08:00 + ("Singapore", "Asia/Singapore"), # UTC+08:00 + ("Perth", "Australia/Perth"), # UTC+08:00 + ("Hong Kong", "Asia/Hong_Kong"), # UTC+08:00 + ("Ulaanbaatar", "Asia/Ulaanbaatar"), # UTC+08:00 + ("Palau", "Pacific/Palau"), # UTC+08:00 + ("Eucla", "Australia/Eucla"), # UTC+08:45 + ("Tokyo", "Asia/Tokyo"), # UTC+09:00 + ("Seoul", "Asia/Seoul"), # UTC+09:00 + ("Yakutsk", "Asia/Yakutsk"), # UTC+09:00 + ("Adelaide", "Australia/Adelaide"), # UTC+09:30 (DST: UTC+10:30) + ("Darwin", "Australia/Darwin"), # UTC+09:30 + ("Sydney", "Australia/Sydney"), # UTC+10:00 (DST: UTC+11:00) + ("Brisbane", "Australia/Brisbane"), # UTC+10:00 + ("Guam", "Pacific/Guam"), # UTC+10:00 + ("Vladivostok", "Asia/Vladivostok"), # UTC+10:00 + ("Tahiti", "Pacific/Tahiti"), # UTC+10:00 + ("Lord Howe Island", "Australia/Lord_Howe"), # UTC+10:30 (DST: UTC+11:00) + ("Solomon Islands", "Pacific/Guadalcanal"), # UTC+11:00 + ("Magadan", "Asia/Magadan"), # UTC+11:00 + ("Norfolk Island", "Pacific/Norfolk"), # UTC+11:00 + ("Bougainville Island", "Pacific/Bougainville"), # UTC+11:00 + ("Chokurdakh", "Asia/Srednekolymsk"), # UTC+11:00 + ("Auckland", "Pacific/Auckland"), # UTC+12:00 (DST: UTC+13:00) + ("Wellington", "Pacific/Auckland"), # UTC+12:00 (DST: UTC+13:00) + ("Fiji Islands", "Pacific/Fiji"), # UTC+12:00 (DST: UTC+13:00) + ("Anadyr", "Asia/Anadyr"), # UTC+12:00 + ("Chatham Islands", "Pacific/Chatham"), # UTC+12:45 (DST: UTC+13:45) + ("Nuku'alofa", "Pacific/Tongatapu"), # UTC+13:00 + ("Samoa", "Pacific/Apia"), # UTC+13:00 (DST: UTC+14:00) + ("Kiritimati Island", "Pacific/Kiritimati"), # UTC+14:00 ] timezone_list = [] @@ -150,7 +177,6 @@ class TimezoneEndpoint(APIView): # Process timezone mapping for friendly_name, tz_identifier in timezone_locations: - try: tz = pytz.timezone(tz_identifier) current_offset = now.astimezone(tz).strftime("%z") diff --git a/apiserver/plane/app/views/workspace/cycle.py b/apiserver/plane/app/views/workspace/cycle.py index 3dce746ea..eb899553d 100644 --- a/apiserver/plane/app/views/workspace/cycle.py +++ b/apiserver/plane/app/views/workspace/cycle.py @@ -12,6 +12,7 @@ from plane.app.permissions import WorkspaceViewerPermission from plane.app.serializers.cycle import CycleSerializer from plane.utils.timezone_converter import user_timezone_converter + class WorkspaceCyclesEndpoint(BaseAPIView): permission_classes = [WorkspaceViewerPermission] diff --git a/apiserver/plane/app/views/workspace/draft.py b/apiserver/plane/app/views/workspace/draft.py index 9503781f1..a5e61d6b4 100644 --- a/apiserver/plane/app/views/workspace/draft.py +++ b/apiserver/plane/app/views/workspace/draft.py @@ -38,6 +38,7 @@ from plane.bgtasks.issue_activities_task import issue_activity from plane.utils.issue_filters import issue_filters from plane.utils.host import base_host + class WorkspaceDraftIssueViewSet(BaseViewSet): model = DraftIssue diff --git a/apiserver/plane/app/views/workspace/user_preference.py b/apiserver/plane/app/views/workspace/user_preference.py index 07ae70ac0..7cfa740e8 100644 --- a/apiserver/plane/app/views/workspace/user_preference.py +++ b/apiserver/plane/app/views/workspace/user_preference.py @@ -27,10 +27,7 @@ class WorkspaceUserPreferenceViewSet(BaseAPIView): create_preference_keys = [] - keys = [ - key - for key, _ in WorkspaceUserPreference.UserPreferenceKeys.choices - ] + keys = [key for key, _ in WorkspaceUserPreference.UserPreferenceKeys.choices] for preference in keys: if preference not in get_preference.values_list("key", flat=True): @@ -39,7 +36,10 @@ class WorkspaceUserPreferenceViewSet(BaseAPIView): preference = WorkspaceUserPreference.objects.bulk_create( [ WorkspaceUserPreference( - key=key, user=request.user, workspace=workspace, sort_order=(65535 + (i*10000)) + key=key, + user=request.user, + workspace=workspace, + sort_order=(65535 + (i * 10000)), ) for i, key in enumerate(create_preference_keys) ], @@ -47,10 +47,13 @@ class WorkspaceUserPreferenceViewSet(BaseAPIView): ignore_conflicts=True, ) - preferences = WorkspaceUserPreference.objects.filter( - user=request.user, workspace_id=workspace.id - ).order_by("sort_order").values("key", "is_pinned", "sort_order") - + preferences = ( + WorkspaceUserPreference.objects.filter( + user=request.user, workspace_id=workspace.id + ) + .order_by("sort_order") + .values("key", "is_pinned", "sort_order") + ) user_preferences = {} @@ -58,7 +61,7 @@ class WorkspaceUserPreferenceViewSet(BaseAPIView): user_preferences[(str(preference["key"]))] = { "is_pinned": preference["is_pinned"], "sort_order": preference["sort_order"], - } + } return Response( user_preferences, status=status.HTTP_200_OK, diff --git a/apiserver/plane/authentication/adapter/base.py b/apiserver/plane/authentication/adapter/base.py index f788dcb41..b28735120 100644 --- a/apiserver/plane/authentication/adapter/base.py +++ b/apiserver/plane/authentication/adapter/base.py @@ -18,6 +18,7 @@ from plane.bgtasks.user_activation_email_task import user_activation_email from plane.utils.host import base_host from plane.utils.ip_address import get_client_ip + class Adapter: """Common interface for all auth providers""" diff --git a/apiserver/plane/authentication/adapter/error.py b/apiserver/plane/authentication/adapter/error.py index dcbe039fb..7c629b441 100644 --- a/apiserver/plane/authentication/adapter/error.py +++ b/apiserver/plane/authentication/adapter/error.py @@ -41,7 +41,6 @@ AUTHENTICATION_ERROR_CODES = { "GOOGLE_OAUTH_PROVIDER_ERROR": 5115, "GITHUB_OAUTH_PROVIDER_ERROR": 5120, "GITLAB_OAUTH_PROVIDER_ERROR": 5121, - # Reset Password "INVALID_PASSWORD_TOKEN": 5125, "EXPIRED_PASSWORD_TOKEN": 5130, diff --git a/apiserver/plane/authentication/provider/oauth/github.py b/apiserver/plane/authentication/provider/oauth/github.py index 4a7808c8a..d8116cec3 100644 --- a/apiserver/plane/authentication/provider/oauth/github.py +++ b/apiserver/plane/authentication/provider/oauth/github.py @@ -25,23 +25,24 @@ class GitHubOAuthProvider(OauthAdapter): organization_scope = "read:org" - def __init__(self, request, code=None, state=None, callback=None): - GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET, GITHUB_ORGANIZATION_ID = get_configuration_value( - [ - { - "key": "GITHUB_CLIENT_ID", - "default": os.environ.get("GITHUB_CLIENT_ID"), - }, - { - "key": "GITHUB_CLIENT_SECRET", - "default": os.environ.get("GITHUB_CLIENT_SECRET"), - }, - { - "key": "GITHUB_ORGANIZATION_ID", - "default": os.environ.get("GITHUB_ORGANIZATION_ID"), - }, - ] + GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET, GITHUB_ORGANIZATION_ID = ( + get_configuration_value( + [ + { + "key": "GITHUB_CLIENT_ID", + "default": os.environ.get("GITHUB_CLIENT_ID"), + }, + { + "key": "GITHUB_CLIENT_SECRET", + "default": os.environ.get("GITHUB_CLIENT_SECRET"), + }, + { + "key": "GITHUB_ORGANIZATION_ID", + "default": os.environ.get("GITHUB_ORGANIZATION_ID"), + }, + ] + ) ) if not (GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET): @@ -128,7 +129,10 @@ class GitHubOAuthProvider(OauthAdapter): def is_user_in_organization(self, github_username): headers = {"Authorization": f"Bearer {self.token_data.get('access_token')}"} - response = requests.get(f"{self.org_membership_url}/{self.organization_id}/memberships/{github_username}", headers=headers) + response = requests.get( + f"{self.org_membership_url}/{self.organization_id}/memberships/{github_username}", + headers=headers, + ) return response.status_code == 200 # 200 means the user is a member def set_user_data(self): @@ -145,7 +149,6 @@ class GitHubOAuthProvider(OauthAdapter): error_message="GITHUB_USER_NOT_IN_ORG", ) - email = self.__get_email(headers=headers) super().set_user_data( { diff --git a/apiserver/plane/authentication/utils/login.py b/apiserver/plane/authentication/utils/login.py index f8c0ed842..e9437ae44 100644 --- a/apiserver/plane/authentication/utils/login.py +++ b/apiserver/plane/authentication/utils/login.py @@ -6,6 +6,7 @@ from django.conf import settings from plane.utils.host import base_host from plane.utils.ip_address import get_client_ip + def user_login(request, user, is_app=False, is_admin=False, is_space=False): login(request=request, user=user) diff --git a/apiserver/plane/authentication/views/app/email.py b/apiserver/plane/authentication/views/app/email.py index 7e91b21c1..0ac51265e 100644 --- a/apiserver/plane/authentication/views/app/email.py +++ b/apiserver/plane/authentication/views/app/email.py @@ -21,6 +21,7 @@ from plane.authentication.adapter.error import ( ) from plane.utils.path_validator import validate_next_path + class SignInAuthEndpoint(View): def post(self, request): next_path = request.POST.get("next_path") diff --git a/apiserver/plane/authentication/views/app/github.py b/apiserver/plane/authentication/views/app/github.py index f558bcd4b..18cbe7b6c 100644 --- a/apiserver/plane/authentication/views/app/github.py +++ b/apiserver/plane/authentication/views/app/github.py @@ -18,6 +18,7 @@ from plane.authentication.adapter.error import ( ) from plane.utils.path_validator import validate_next_path + class GitHubOauthInitiateEndpoint(View): def get(self, request): # Get host and next path diff --git a/apiserver/plane/authentication/views/app/gitlab.py b/apiserver/plane/authentication/views/app/gitlab.py index c3a0f5876..d6479e954 100644 --- a/apiserver/plane/authentication/views/app/gitlab.py +++ b/apiserver/plane/authentication/views/app/gitlab.py @@ -18,6 +18,7 @@ from plane.authentication.adapter.error import ( ) from plane.utils.path_validator import validate_next_path + class GitLabOauthInitiateEndpoint(View): def get(self, request): # Get host and next path diff --git a/apiserver/plane/authentication/views/app/google.py b/apiserver/plane/authentication/views/app/google.py index 2caf9f51b..66b6f7662 100644 --- a/apiserver/plane/authentication/views/app/google.py +++ b/apiserver/plane/authentication/views/app/google.py @@ -20,6 +20,7 @@ from plane.authentication.adapter.error import ( ) from plane.utils.path_validator import validate_next_path + class GoogleOauthInitiateEndpoint(View): def get(self, request): request.session["host"] = base_host(request=request, is_app=True) @@ -95,7 +96,9 @@ class GoogleCallbackEndpoint(View): # Get the redirection path path = get_redirection_path(user=user) # redirect to referer path - url = urljoin(base_host, str(validate_next_path(next_path)) if next_path else path) + url = urljoin( + base_host, str(validate_next_path(next_path)) if next_path else path + ) return HttpResponseRedirect(url) except AuthenticationException as e: params = e.get_error_dict() diff --git a/apiserver/plane/authentication/views/common.py b/apiserver/plane/authentication/views/common.py index 7a18072ae..ab60e6d04 100644 --- a/apiserver/plane/authentication/views/common.py +++ b/apiserver/plane/authentication/views/common.py @@ -53,12 +53,14 @@ class ChangePasswordEndpoint(APIView): error_message="MISSING_PASSWORD", payload={"error": "Old password is missing"}, ) - return Response(exc.get_error_dict(), status=status.HTTP_400_BAD_REQUEST) + return Response( + exc.get_error_dict(), status=status.HTTP_400_BAD_REQUEST + ) # Get the new password new_password = request.data.get("new_password", False) - if not new_password: + if not new_password: exc = AuthenticationException( error_code=AUTHENTICATION_ERROR_CODES["MISSING_PASSWORD"], error_message="MISSING_PASSWORD", @@ -66,7 +68,6 @@ class ChangePasswordEndpoint(APIView): ) return Response(exc.get_error_dict(), status=status.HTTP_400_BAD_REQUEST) - # If the user password is not autoset then we need to check the old passwords if not user.is_password_autoset and not user.check_password(old_password): exc = AuthenticationException( diff --git a/apiserver/plane/authentication/views/space/magic.py b/apiserver/plane/authentication/views/space/magic.py index cb682137c..d230af7ed 100644 --- a/apiserver/plane/authentication/views/space/magic.py +++ b/apiserver/plane/authentication/views/space/magic.py @@ -25,6 +25,7 @@ from plane.authentication.adapter.error import ( ) from plane.utils.path_validator import validate_next_path + class MagicGenerateSpaceEndpoint(APIView): permission_classes = [AllowAny] @@ -38,7 +39,6 @@ class MagicGenerateSpaceEndpoint(APIView): ) return Response(exc.get_error_dict(), status=status.HTTP_400_BAD_REQUEST) - email = request.data.get("email", "").strip().lower() try: validate_email(email) diff --git a/apiserver/plane/bgtasks/export_task.py b/apiserver/plane/bgtasks/export_task.py index 78210db64..7eba31476 100644 --- a/apiserver/plane/bgtasks/export_task.py +++ b/apiserver/plane/bgtasks/export_task.py @@ -8,6 +8,7 @@ import boto3 from botocore.client import Config from uuid import UUID from datetime import datetime, date + # Third party imports from celery import shared_task @@ -90,8 +91,11 @@ def create_zip_file(files: List[tuple[str, str | bytes]]) -> io.BytesIO: zip_buffer.seek(0) return zip_buffer + # TODO: Change the upload_to_s3 function to use the new storage method with entry in file asset table -def upload_to_s3(zip_file: io.BytesIO, workspace_id: UUID, token_id: str, slug: str) -> None: +def upload_to_s3( + zip_file: io.BytesIO, workspace_id: UUID, token_id: str, slug: str +) -> None: """ Upload a ZIP file to S3 and generate a presigned URL. """ @@ -308,7 +312,12 @@ def update_table_row(rows: List[List[str]], row: List[str]) -> None: rows.append(row) -def generate_csv(header: List[str], project_id: str, issues: List[dict], files: List[tuple[str, str | bytes]]) -> None: +def generate_csv( + header: List[str], + project_id: str, + issues: List[dict], + files: List[tuple[str, str | bytes]], +) -> None: """ Generate CSV export for all the passed issues. """ @@ -320,7 +329,12 @@ def generate_csv(header: List[str], project_id: str, issues: List[dict], files: files.append((f"{project_id}.csv", csv_file)) -def generate_json(header: List[str], project_id: str, issues: List[dict], files: List[tuple[str, str | bytes]]) -> None: +def generate_json( + header: List[str], + project_id: str, + issues: List[dict], + files: List[tuple[str, str | bytes]], +) -> None: """ Generate JSON export for all the passed issues. """ @@ -332,7 +346,12 @@ def generate_json(header: List[str], project_id: str, issues: List[dict], files: files.append((f"{project_id}.json", json_file)) -def generate_xlsx(header: List[str], project_id: str, issues: List[dict], files: List[tuple[str, str | bytes]]) -> None: +def generate_xlsx( + header: List[str], + project_id: str, + issues: List[dict], + files: List[tuple[str, str | bytes]], +) -> None: """ Generate XLSX export for all the passed issues. """ @@ -355,7 +374,14 @@ def get_created_by(obj: Issue | IssueComment) -> str: @shared_task -def issue_export_task(provider: str, workspace_id: UUID, project_ids: List[str], token_id: str, multiple: bool, slug: str): +def issue_export_task( + provider: str, + workspace_id: UUID, + project_ids: List[str], + token_id: str, + multiple: bool, + slug: str, +): """ Export issues from the workspace. provider (str): The provider to export the issues to csv | json | xlsx. @@ -408,7 +434,7 @@ def issue_export_task(provider: str, workspace_id: UUID, project_ids: List[str], # Get the attachments for the issues file_assets = FileAsset.objects.filter( issue_id__in=workspace_issues.values_list("id", flat=True), - entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT + entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT, ).annotate(work_item_id=F("issue_id"), asset_id=F("id")) # Create a dictionary to store the attachments for the issues diff --git a/apiserver/plane/db/management/commands/update_deleted_workspace_slug.py b/apiserver/plane/db/management/commands/update_deleted_workspace_slug.py index 3d07e8e34..48600e662 100644 --- a/apiserver/plane/db/management/commands/update_deleted_workspace_slug.py +++ b/apiserver/plane/db/management/commands/update_deleted_workspace_slug.py @@ -5,7 +5,9 @@ from plane.db.models import Workspace class Command(BaseCommand): - help = "Updates the slug of a soft-deleted workspace by appending the epoch timestamp" + help = ( + "Updates the slug of a soft-deleted workspace by appending the epoch timestamp" + ) def add_arguments(self, parser): parser.add_argument( @@ -75,4 +77,4 @@ class Command(BaseCommand): self.style.ERROR( f"Error updating workspace '{workspace.name}': {str(e)}" ) - ) \ No newline at end of file + ) diff --git a/apiserver/plane/db/models/__init__.py b/apiserver/plane/db/models/__init__.py index 04e5a27f6..3cf46c919 100644 --- a/apiserver/plane/db/models/__init__.py +++ b/apiserver/plane/db/models/__init__.py @@ -82,4 +82,4 @@ from .label import Label from .device import Device, DeviceSession -from .sticky import Sticky \ No newline at end of file +from .sticky import Sticky diff --git a/apiserver/plane/db/models/workspace.py b/apiserver/plane/db/models/workspace.py index e1af103f3..7e5103a70 100644 --- a/apiserver/plane/db/models/workspace.py +++ b/apiserver/plane/db/models/workspace.py @@ -153,12 +153,8 @@ class Workspace(BaseModel): return None def delete( - self, - using: Optional[str] = None, - soft: bool = True, - *args: Any, - **kwargs: Any - ): + self, using: Optional[str] = None, soft: bool = True, *args: Any, **kwargs: Any + ): """ Override the delete method to append epoch timestamp to the slug when soft deleting. @@ -172,7 +168,7 @@ class Workspace(BaseModel): result = super().delete(using=using, soft=soft, *args, **kwargs) # If it's a soft delete and the model still exists (not hard deleted) - if soft and hasattr(self, 'deleted_at') and self.deleted_at: + if soft and hasattr(self, "deleted_at") and self.deleted_at: # Use the deleted_at timestamp to update the slug deletion_timestamp: int = int(self.deleted_at.timestamp()) self.slug = f"{self.slug}__{deletion_timestamp}" diff --git a/apiserver/plane/license/management/commands/configure_instance.py b/apiserver/plane/license/management/commands/configure_instance.py index ce6bbf7a0..2e1b6a123 100644 --- a/apiserver/plane/license/management/commands/configure_instance.py +++ b/apiserver/plane/license/management/commands/configure_instance.py @@ -157,7 +157,7 @@ class Command(BaseCommand): }, # Deprecated, use LLM_MODEL { - "key": "GPT_ENGINE", + "key": "GPT_ENGINE", "value": os.environ.get("GPT_ENGINE", "gpt-3.5-turbo"), "category": "SMTP", "is_encrypted": False, diff --git a/apiserver/plane/settings/storage.py b/apiserver/plane/settings/storage.py index a757d12f3..f2be261ad 100644 --- a/apiserver/plane/settings/storage.py +++ b/apiserver/plane/settings/storage.py @@ -32,7 +32,6 @@ class S3Storage(S3Boto3Storage): ) or os.environ.get("MINIO_ENDPOINT_URL") if os.environ.get("USE_MINIO") == "1": - # Determine protocol based on environment variable if os.environ.get("MINIO_ENDPOINT_SSL") == "1": endpoint_protocol = "https" diff --git a/apiserver/plane/space/utils/grouper.py b/apiserver/plane/space/utils/grouper.py index b334999de..4dd956b9f 100644 --- a/apiserver/plane/space/utils/grouper.py +++ b/apiserver/plane/space/utils/grouper.py @@ -135,7 +135,7 @@ def issue_on_results( default=None, output_field=JSONField(), ), - filter=Q(votes__isnull=False,votes__deleted_at__isnull=True), + filter=Q(votes__isnull=False, votes__deleted_at__isnull=True), distinct=True, ), reaction_items=ArrayAgg( @@ -169,7 +169,9 @@ def issue_on_results( default=None, output_field=JSONField(), ), - filter=Q(issue_reactions__isnull=False, issue_reactions__deleted_at__isnull=True), + filter=Q( + issue_reactions__isnull=False, issue_reactions__deleted_at__isnull=True + ), distinct=True, ), ).values(*required_fields, "vote_items", "reaction_items") diff --git a/apiserver/plane/space/views/meta.py b/apiserver/plane/space/views/meta.py index d092e7e58..dc7ecb648 100644 --- a/apiserver/plane/space/views/meta.py +++ b/apiserver/plane/space/views/meta.py @@ -14,9 +14,7 @@ class ProjectMetaDataEndpoint(BaseAPIView): def get(self, request, anchor): try: - deploy_board = DeployBoard.objects.get( - anchor=anchor, entity_name="project" - ) + deploy_board = DeployBoard.objects.get(anchor=anchor, entity_name="project") except DeployBoard.DoesNotExist: return Response( {"error": "Project is not published"}, status=status.HTTP_404_NOT_FOUND diff --git a/apiserver/plane/utils/analytics_plot.py b/apiserver/plane/utils/analytics_plot.py index 9e2f8c59d..43c465e7c 100644 --- a/apiserver/plane/utils/analytics_plot.py +++ b/apiserver/plane/utils/analytics_plot.py @@ -182,9 +182,7 @@ def burndown_plot(queryset, slug, project_id, plot_type, cycle_id=None, module_i # Get all dates between the two dates date_range = [ (queryset.start_date + timedelta(days=x)) - for x in range( - (queryset.target_date - queryset.start_date).days + 1 - ) + for x in range((queryset.target_date - queryset.start_date).days + 1) ] chart_data = {str(date): 0 for date in date_range} diff --git a/apiserver/plane/utils/build_chart.py b/apiserver/plane/utils/build_chart.py index 4ae3397f8..be5bb7753 100644 --- a/apiserver/plane/utils/build_chart.py +++ b/apiserver/plane/utils/build_chart.py @@ -160,7 +160,6 @@ def build_analytics_chart( group_by: Optional[str] = None, date_filter: Optional[str] = None, ) -> Dict[str, Union[List[Dict[str, Any]], Dict[str, str]]]: - # Validate x_axis if x_axis not in x_axis_mapper: raise ValidationError(f"Invalid x_axis field: {x_axis}") diff --git a/apiserver/plane/utils/timezone_converter.py b/apiserver/plane/utils/timezone_converter.py index e4252422a..9a66742ed 100644 --- a/apiserver/plane/utils/timezone_converter.py +++ b/apiserver/plane/utils/timezone_converter.py @@ -35,9 +35,7 @@ def user_timezone_converter(queryset, datetime_fields, user_timezone): return queryset_values -def convert_to_utc( - date, project_id, is_start_date=False -): +def convert_to_utc(date, project_id, is_start_date=False): """ Converts a start date string to the project's local timezone at 12:00 AM and then converts it to UTC for storage. diff --git a/apiserver/pyproject.toml b/apiserver/pyproject.toml index 4292580a8..099d5e36e 100644 --- a/apiserver/pyproject.toml +++ b/apiserver/pyproject.toml @@ -42,7 +42,7 @@ quote-style = "double" indent-style = "space" # Respect magic trailing commas. -skip-magic-trailing-comma = true +# skip-magic-trailing-comma = true # Automatically detect the appropriate line ending. line-ending = "auto" From 2f4aa843fc2e3e15c92a8351d220e48f7b19a0e8 Mon Sep 17 00:00:00 2001 From: Sangeetha Date: Tue, 20 May 2025 12:56:30 +0530 Subject: [PATCH 067/201] [WEB-4122] fix: estimate in project export #7091 --- apiserver/plane/bgtasks/export_task.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apiserver/plane/bgtasks/export_task.py b/apiserver/plane/bgtasks/export_task.py index 7eba31476..4d7fcd5ff 100644 --- a/apiserver/plane/bgtasks/export_task.py +++ b/apiserver/plane/bgtasks/export_task.py @@ -478,8 +478,8 @@ def issue_export_task( } for comment in issue.issue_comments.all() ], - "estimate": issue.estimate_point.estimate.name - if issue.estimate_point and issue.estimate_point.estimate + "estimate": issue.estimate_point.value + if issue.estimate_point and issue.estimate_point.value else "", "link": [link.url for link in issue.issue_link.all()], "assignees": [ From 0a8cc24da505fd519fcc3c9d6b5e15bc7ce21b29 Mon Sep 17 00:00:00 2001 From: Nikhil <118773738+pablohashescobar@users.noreply.github.com> Date: Wed, 21 May 2025 20:34:52 +0530 Subject: [PATCH 068/201] chore: add validation fields in users (#7102) * chore: add validation fields in users * chore: make is email valid default value False --- ...0096_user_is_email_valid_user_masked_at.py | 23 +++++++++++++++++++ apiserver/plane/db/models/user.py | 6 +++++ 2 files changed, 29 insertions(+) create mode 100644 apiserver/plane/db/migrations/0096_user_is_email_valid_user_masked_at.py diff --git a/apiserver/plane/db/migrations/0096_user_is_email_valid_user_masked_at.py b/apiserver/plane/db/migrations/0096_user_is_email_valid_user_masked_at.py new file mode 100644 index 000000000..66635d89d --- /dev/null +++ b/apiserver/plane/db/migrations/0096_user_is_email_valid_user_masked_at.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.20 on 2025-05-21 13:43 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("db", "0095_page_external_id_page_external_source"), + ] + + operations = [ + migrations.AddField( + model_name="user", + name="is_email_valid", + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name="user", + name="masked_at", + field=models.DateTimeField(null=True), + ), + ] diff --git a/apiserver/plane/db/models/user.py b/apiserver/plane/db/models/user.py index c6bf37d37..ad6e858ad 100644 --- a/apiserver/plane/db/models/user.py +++ b/apiserver/plane/db/models/user.py @@ -106,6 +106,12 @@ class User(AbstractBaseUser, PermissionsMixin): max_length=255, default="UTC", choices=USER_TIMEZONE_CHOICES ) + # email validation + is_email_valid = models.BooleanField(default=False) + + # masking + masked_at = models.DateTimeField(null=True) + USERNAME_FIELD = "email" REQUIRED_FIELDS = ["username"] From 4460529b37a7aaf314e3f3bbaa075592f6d7e814 Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Fri, 23 May 2025 13:53:16 +0530 Subject: [PATCH 069/201] [WEB-4154] fix: dropdown container classname (#7085) * fix: dropdown container classname * improvement: update string utils for joinWithConjunction * improvement: add more string utils --- packages/ui/src/dropdown/dropdown.d.ts | 2 +- packages/ui/src/dropdown/multi-select.tsx | 22 ++++----- packages/ui/src/dropdown/single-select.tsx | 22 ++++----- packages/utils/src/string.ts | 57 ++++++++++------------ 4 files changed, 48 insertions(+), 55 deletions(-) diff --git a/packages/ui/src/dropdown/dropdown.d.ts b/packages/ui/src/dropdown/dropdown.d.ts index dd441d0a8..8d1159e7c 100644 --- a/packages/ui/src/dropdown/dropdown.d.ts +++ b/packages/ui/src/dropdown/dropdown.d.ts @@ -4,7 +4,7 @@ export interface IDropdown { // root props onOpen?: () => void; onClose?: () => void; - containerClassName?: (isOpen: boolean) => string; + containerClassName?: string | ((isOpen: boolean) => string); tabIndex?: number; placement?: Placement; disabled?: boolean; diff --git a/packages/ui/src/dropdown/multi-select.tsx b/packages/ui/src/dropdown/multi-select.tsx index 25f22c6be..400e2c728 100644 --- a/packages/ui/src/dropdown/multi-select.tsx +++ b/packages/ui/src/dropdown/multi-select.tsx @@ -1,19 +1,14 @@ -import React, { FC, useMemo, useRef, useState } from "react"; -import sortBy from "lodash/sortBy"; -// headless ui import { Combobox } from "@headlessui/react"; -// popper-js +import sortBy from "lodash/sortBy"; +import React, { FC, useMemo, useRef, useState } from "react"; import { usePopper } from "react-popper"; -// plane helpers +// plane imports import { useOutsideClickDetector } from "@plane/hooks"; -// components +// local imports +import { cn } from "../../helpers"; +import { useDropdownKeyPressed } from "../hooks/use-dropdown-key-pressed"; import { DropdownButton } from "./common"; import { DropdownOptions } from "./common/options"; -// hooks -import { useDropdownKeyPressed } from "../hooks/use-dropdown-key-pressed"; -// helper -import { cn } from "../../helpers"; -// types import { IMultiSelectDropdown } from "./dropdown"; export const MultiSelectDropdown: FC = (props) => { @@ -118,7 +113,10 @@ export const MultiSelectDropdown: FC = (props) => { ref={dropdownRef} value={value} onChange={onChange} - className={cn("h-full", containerClassName)} + className={cn( + "h-full", + typeof containerClassName === "function" ? containerClassName(isOpen) : containerClassName + )} tabIndex={tabIndex} multiple onKeyDown={handleKeyDown} diff --git a/packages/ui/src/dropdown/single-select.tsx b/packages/ui/src/dropdown/single-select.tsx index bcdff40c1..9614feb51 100644 --- a/packages/ui/src/dropdown/single-select.tsx +++ b/packages/ui/src/dropdown/single-select.tsx @@ -1,19 +1,14 @@ -import React, { FC, useMemo, useRef, useState } from "react"; -import sortBy from "lodash/sortBy"; -// headless ui import { Combobox } from "@headlessui/react"; -// popper-js +import sortBy from "lodash/sortBy"; +import React, { FC, useMemo, useRef, useState } from "react"; import { usePopper } from "react-popper"; -// plane helpers +// plane imports import { useOutsideClickDetector } from "@plane/hooks"; -// components +// local imports +import { cn } from "../../helpers"; +import { useDropdownKeyPressed } from "../hooks/use-dropdown-key-pressed"; import { DropdownButton } from "./common"; import { DropdownOptions } from "./common/options"; -// hooks -import { useDropdownKeyPressed } from "../hooks/use-dropdown-key-pressed"; -// helper -import { cn } from "../../helpers"; -// types import { ISingleSelectDropdown } from "./dropdown"; export const Dropdown: FC = (props) => { @@ -118,7 +113,10 @@ export const Dropdown: FC = (props) => { ref={dropdownRef} value={value} onChange={onChange} - className={cn("h-full", containerClassName)} + className={cn( + "h-full", + typeof containerClassName === "function" ? containerClassName(isOpen) : containerClassName + )} tabIndex={tabIndex} onKeyDown={handleKeyDown} disabled={disabled} diff --git a/packages/utils/src/string.ts b/packages/utils/src/string.ts index 19840df4d..d663c49c9 100644 --- a/packages/utils/src/string.ts +++ b/packages/utils/src/string.ts @@ -86,36 +86,6 @@ export const copyUrlToClipboard = async (path: string) => { await copyTextToClipboard(url.toString()); }; -/** - * @description Generates a deterministic HSL color based on input string - * @param {string} string - Input string to generate color from - * @returns {string} HSL color string - * @example - * generateRandomColor("hello") // returns consistent HSL color for "hello" - * generateRandomColor("") // returns "rgb(var(--color-primary-100))" - */ -export const generateRandomColor = (string: string): string => { - if (!string) return "rgb(var(--color-primary-100))"; - - string = `${string}`; - - const uniqueId = string.length.toString() + string; - const combinedString = uniqueId + string; - - const hash = Array.from(combinedString).reduce((acc, char) => { - const charCode = char.charCodeAt(0); - return (acc << 5) - acc + charCode; - }, 0); - - const hue = hash % 360; - const saturation = 70; - const lightness = 60; - - const randomColor = `hsl(${hue}, ${saturation}%, ${lightness}%)`; - - return randomColor; -}; - /** * @description Gets first character of first word or first characters of first two words * @param {string} str - Input string @@ -275,6 +245,33 @@ export const checkURLValidity = (url: string): boolean => { return urlPattern.test(url); }; +/** + * Combines array elements with a separator and adds a conjunction before the last element + * @param array Array of strings to combine + * @param separator Separator to use between elements (default: ", ") + * @param conjunction Conjunction to use before last element (default: "and") + * @returns Combined string with conjunction before the last element + */ +export const joinWithConjunction = (array: string[], separator: string = ", ", conjunction: string = "and"): string => { + if (!array || array.length === 0) return ""; + if (array.length === 1) return array[0]; + if (array.length === 2) return `${array[0]} ${conjunction} ${array[1]}`; + + const lastElement = array[array.length - 1]; + const elementsExceptLast = array.slice(0, -1); + + return `${elementsExceptLast.join(separator)}${separator}${conjunction} ${lastElement}`; +}; + +/** + * @description Ensures a URL has a protocol + * @param {string} url + * @returns {string} + * @example + * ensureUrlHasProtocol("example.com") => "http://example.com" + */ +export const ensureUrlHasProtocol = (url: string): string => (url.startsWith("http") ? url : `http://${url}`); + // Browser-only clipboard functions // let copyTextToClipboard: (text: string) => Promise; From b376e5300a5c2a9c4ef2d0ef067815334b05e7f7 Mon Sep 17 00:00:00 2001 From: Vamsi Krishna <46787868+vamsikrishnamathala@users.noreply.github.com> Date: Fri, 23 May 2025 15:04:50 +0530 Subject: [PATCH 070/201] [WEB-3155]fix: email notification comments overflow #7110 --- apiserver/templates/emails/notifications/issue-updates.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apiserver/templates/emails/notifications/issue-updates.html b/apiserver/templates/emails/notifications/issue-updates.html index c1a48752f..e17f0e9e6 100644 --- a/apiserver/templates/emails/notifications/issue-updates.html +++ b/apiserver/templates/emails/notifications/issue-updates.html @@ -209,7 +209,7 @@ {% for actor_comment in comment.actor_comments.new_value %}

-
+

{{ actor_comment|safe }}

rowData?.id ?? ""} + tHeadClassName="border-b border-custom-border-100" + thClassName="text-left font-medium divide-x-0 text-custom-text-400" + tBodyClassName="divide-y-0" + tBodyTrClassName="divide-x-0 p-4 h-[40px] text-custom-text-200" + tHeadTrClassName="divide-x-0" + /> + + + ) : ( +
+ +
+ ) + ) : ( + + )} + + + ); +}); diff --git a/web/core/components/home/widgets/empty-states/no-projects.tsx b/web/core/components/home/widgets/empty-states/no-projects.tsx index 44496aa81..f3f35d5a1 100644 --- a/web/core/components/home/widgets/empty-states/no-projects.tsx +++ b/web/core/components/home/widgets/empty-states/no-projects.tsx @@ -115,7 +115,7 @@ export const NoProjectsEmptyState = observer(() => { flag: "visited_profile", cta: { text: "home.empty.personalize_account.cta", - link: "/profile", + link: `/${workspaceSlug}/settings/account`, disabled: false, }, }, diff --git a/web/core/components/issues/issue-layouts/empty-states/archived-issues.tsx b/web/core/components/issues/issue-layouts/empty-states/archived-issues.tsx index b4c3f7ae5..f9aca06f2 100644 --- a/web/core/components/issues/issue-layouts/empty-states/archived-issues.tsx +++ b/web/core/components/issues/issue-layouts/empty-states/archived-issues.tsx @@ -72,7 +72,7 @@ export const ProjectArchivedEmptyState: React.FC = observer(() => { assetPath={archivedIssuesResolvedPath} primaryButton={{ text: t("project_issues.empty_state.no_archived_issues.primary_button.text"), - onClick: () => router.push(`/${workspaceSlug}/projects/${projectId}/settings/automations`), + onClick: () => router.push(`/${workspaceSlug}/settings/projects/${projectId}/automations`), disabled: !canPerformEmptyStateActions, }} /> diff --git a/web/core/components/labels/project-setting-label-list.tsx b/web/core/components/labels/project-setting-label-list.tsx index 8150eac5f..2df43aa97 100644 --- a/web/core/components/labels/project-setting-label-list.tsx +++ b/web/core/components/labels/project-setting-label-list.tsx @@ -19,6 +19,7 @@ import { // hooks import { useLabel, useUserPermissions } from "@/hooks/store"; import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; +import { SettingsHeading } from "../settings"; // plane web imports export const ProjectSettingsLabelList: React.FC = observer(() => { @@ -75,14 +76,16 @@ export const ProjectSettingsLabelList: React.FC = observer(() => { data={selectDeleteLabel ?? null} onClose={() => setSelectDeleteLabel(null)} /> -
-

Labels

- {isEditable && ( - - )} -
+ +
{showLabelForm && (
@@ -106,6 +109,7 @@ export const ProjectSettingsLabelList: React.FC = observer(() => { title={t("project_settings.empty_state.labels.title")} description={t("project_settings.empty_state.labels.description")} assetPath={resolvedPath} + className="w-full !px-0 !py-4" />
) : ( diff --git a/web/core/components/preferences/list.tsx b/web/core/components/preferences/list.tsx new file mode 100644 index 000000000..beb08fd1a --- /dev/null +++ b/web/core/components/preferences/list.tsx @@ -0,0 +1,11 @@ +import { PREFERENCE_OPTIONS } from "@plane/constants"; +import { PREFERENCE_COMPONENTS } from "@/plane-web/components/preferences/config"; + +export const PreferencesList = () => ( +
+ {PREFERENCE_OPTIONS.map((option) => { + const Component = PREFERENCE_COMPONENTS[option.id as keyof typeof PREFERENCE_COMPONENTS]; + return ; + })} +
+); diff --git a/web/core/components/preferences/section.tsx b/web/core/components/preferences/section.tsx new file mode 100644 index 000000000..5ba35bb55 --- /dev/null +++ b/web/core/components/preferences/section.tsx @@ -0,0 +1,15 @@ +interface SettingsSectionProps { + title: string; + description: string; + control: React.ReactNode; +} + +export const PreferencesSection = ({ title, description, control }: SettingsSectionProps) => ( +
+
+

{title}

+

{description}

+
+
{control}
+
+); diff --git a/web/core/components/profile/activity/profile-activity-list.tsx b/web/core/components/profile/activity/profile-activity-list.tsx index efcd83e73..36eca920f 100644 --- a/web/core/components/profile/activity/profile-activity-list.tsx +++ b/web/core/components/profile/activity/profile-activity-list.tsx @@ -81,8 +81,8 @@ export const ProfileActivityListPage: React.FC = observer((props) => {
)} - -
- - - - {params.segment ? ( - barGraphData.xAxisKeys.map((key) => ( - - )) - ) : ( - - )} - - - - {barGraphData.data.map((item, index) => ( - - - {params.segment ? ( - barGraphData.xAxisKeys.map((key, index) => ( - - )) - ) : ( - - )} - - ))} - -
- {ANALYTICS_X_AXIS_VALUES.find((v) => v.value === params.x_axis)?.label} - -
- {params.segment === "priority" ? ( - - ) : ( - - )} - {renderChartDynamicLabel(generateDisplayName(key, analytics, params, "segment"))?.label} -
-
- {ANALYTICS_Y_AXIS_VALUES.find((v) => v.value === params.y_axis)?.label} -
-
-
- {params.x_axis === "priority" ? ( - - ) : ( -
- )} -
-
- -
- {generateDisplayName(`${item.name}`, analytics, params, "x_axis")} -
-
-
-
-
- {item[key] ?? 0} - {item[yAxisKey]}
-
-); diff --git a/web/core/components/analytics-v2/empty-state.tsx b/web/core/components/analytics/empty-state.tsx similarity index 89% rename from web/core/components/analytics-v2/empty-state.tsx rename to web/core/components/analytics/empty-state.tsx index 1a1ee86e8..5243e6c88 100644 --- a/web/core/components/analytics-v2/empty-state.tsx +++ b/web/core/components/analytics/empty-state.tsx @@ -11,8 +11,8 @@ type Props = { className?: string; }; -const AnalyticsV2EmptyState = ({ title, description, assetPath, className }: Props) => { - const backgroundReolvedPath = useResolvedAssetPath({ basePath: "/empty-state/analytics-v2/empty-grid-background" }); +const AnalyticsEmptyState = ({ title, description, assetPath, className }: Props) => { + const backgroundReolvedPath = useResolvedAssetPath({ basePath: "/empty-state/analytics/empty-grid-background" }); return (
); }; -export default AnalyticsV2EmptyState; +export default AnalyticsEmptyState; diff --git a/web/core/components/analytics/index.ts b/web/core/components/analytics/index.ts index c5f769890..8ac82df5d 100644 --- a/web/core/components/analytics/index.ts +++ b/web/core/components/analytics/index.ts @@ -1,3 +1 @@ -export * from "./custom-analytics"; -export * from "./scope-and-demand"; -export * from "./project-modal"; +export * from "./overview/root"; diff --git a/web/core/components/analytics-v2/insight-card.tsx b/web/core/components/analytics/insight-card.tsx similarity index 93% rename from web/core/components/analytics-v2/insight-card.tsx rename to web/core/components/analytics/insight-card.tsx index 2ce4c2fdc..739de6645 100644 --- a/web/core/components/analytics-v2/insight-card.tsx +++ b/web/core/components/analytics/insight-card.tsx @@ -1,12 +1,12 @@ // plane package imports import React, { useMemo } from "react"; -import { IAnalyticsResponseFieldsV2 } from "@plane/types"; +import { IAnalyticsResponseFields } from "@plane/types"; import { Loader } from "@plane/ui"; // components import TrendPiece from "./trend-piece"; export type InsightCardProps = { - data?: IAnalyticsResponseFieldsV2; + data?: IAnalyticsResponseFields; label: string; isLoading?: boolean; versus?: string | null; diff --git a/web/core/components/analytics-v2/insight-table/data-table.tsx b/web/core/components/analytics/insight-table/data-table.tsx similarity index 95% rename from web/core/components/analytics-v2/insight-table/data-table.tsx rename to web/core/components/analytics/insight-table/data-table.tsx index 61e02cb33..8a66c3caf 100644 --- a/web/core/components/analytics-v2/insight-table/data-table.tsx +++ b/web/core/components/analytics/insight-table/data-table.tsx @@ -23,7 +23,7 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@ import { cn } from "@plane/utils"; // plane web components import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; -import AnalyticsV2EmptyState from "../empty-state"; +import AnalyticsEmptyState from "../empty-state"; interface DataTableProps { columns: ColumnDef[]; @@ -40,7 +40,7 @@ export function DataTable({ columns, data, searchPlaceholder, act const { t } = useTranslation(); const inputRef = React.useRef(null); const [isSearchOpen, setIsSearchOpen] = React.useState(false); - const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/analytics-v2/empty-table" }); + const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/analytics/empty-table" }); const table = useReactTable({ data, @@ -155,9 +155,9 @@ export function DataTable({ columns, data, searchPlaceholder, act
- diff --git a/web/core/components/analytics-v2/insight-table/index.ts b/web/core/components/analytics/insight-table/index.ts similarity index 100% rename from web/core/components/analytics-v2/insight-table/index.ts rename to web/core/components/analytics/insight-table/index.ts diff --git a/web/core/components/analytics-v2/insight-table/loader.tsx b/web/core/components/analytics/insight-table/loader.tsx similarity index 100% rename from web/core/components/analytics-v2/insight-table/loader.tsx rename to web/core/components/analytics/insight-table/loader.tsx diff --git a/web/core/components/analytics/insight-table/root.tsx b/web/core/components/analytics/insight-table/root.tsx new file mode 100644 index 000000000..583db1b7a --- /dev/null +++ b/web/core/components/analytics/insight-table/root.tsx @@ -0,0 +1,49 @@ +import { ColumnDef, Row, Table } from "@tanstack/react-table"; +import { Download } from "lucide-react"; +import { useTranslation } from "@plane/i18n"; +import { AnalyticsTableDataMap, TAnalyticsTabsBase } from "@plane/types"; +import { Button } from "@plane/ui"; +import { DataTable } from "./data-table"; +import { TableLoader } from "./loader"; +interface InsightTableProps> { + analyticsType: T; + data?: AnalyticsTableDataMap[T][]; + isLoading?: boolean; + columns: ColumnDef[]; + columnsLabels?: Record; + headerText: string; + onExport?: (rows: Row[]) => void; +} + +export const InsightTable = >( + props: InsightTableProps +): React.ReactElement => { + const { data, isLoading, columns, headerText, onExport } = props; + const { t } = useTranslation(); + if (isLoading) { + return ; + } + + return ( +
+ {data ? ( + ) => ( + + )} + /> + ) : ( +
{t("common.no_data_yet")}
+ )} +
+ ); +}; diff --git a/web/core/components/analytics-v2/loaders.tsx b/web/core/components/analytics/loaders.tsx similarity index 100% rename from web/core/components/analytics-v2/loaders.tsx rename to web/core/components/analytics/loaders.tsx diff --git a/web/core/components/analytics/old-page.tsx b/web/core/components/analytics/old-page.tsx deleted file mode 100644 index 719d66214..000000000 --- a/web/core/components/analytics/old-page.tsx +++ /dev/null @@ -1,107 +0,0 @@ -"use client"; - -import React, { Fragment } from "react"; -import { observer } from "mobx-react"; -import { useSearchParams } from "next/navigation"; -import { Tab } from "@headlessui/react"; -// plane package imports -import { ANALYTICS_TABS, EUserPermissionsLevel, EUserPermissions } from "@plane/constants"; -import { useTranslation } from "@plane/i18n"; -import { Header, EHeaderVariant } from "@plane/ui"; -// components -import { CustomAnalytics, ScopeAndDemand } from "@/components/analytics"; -import { PageHead } from "@/components/core"; -import { ComicBoxButton, DetailedEmptyState } from "@/components/empty-state"; -// hooks -import { useCommandPalette, useEventTracker, useProject, useUserPermissions, useWorkspace } from "@/hooks/store"; -import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; - -const OldAnalyticsPage = observer(() => { - const searchParams = useSearchParams(); - const analytics_tab = searchParams.get("analytics_tab"); - // plane imports - const { t } = useTranslation(); - // store hooks - const { toggleCreateProjectModal } = useCommandPalette(); - const { setTrackElement } = useEventTracker(); - const { workspaceProjectIds, loader } = useProject(); - const { currentWorkspace } = useWorkspace(); - const { allowPermissions } = useUserPermissions(); - // helper hooks - const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/onboarding/analytics" }); - // derived values - const pageTitle = currentWorkspace?.name - ? t(`workspace_analytics.page_label`, { workspace: currentWorkspace?.name }) - : undefined; - - // permissions - const canPerformEmptyStateActions = allowPermissions( - [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - EUserPermissionsLevel.WORKSPACE - ); - - // TODO: refactor loader implementation - return ( - <> - - {workspaceProjectIds && ( - <> - {workspaceProjectIds.length > 0 || loader === "init-loader" ? ( -
- -
- - {ANALYTICS_TABS.map((tab) => ( - - {({ selected }) => ( - - )} - - ))} - -
- - - - - - - - -
-
- ) : ( - { - setTrackElement("Analytics empty state"); - toggleCreateProjectModal(true); - }} - disabled={!canPerformEmptyStateActions} - /> - } - /> - )} - - )} - - ); -}); - -export default OldAnalyticsPage; diff --git a/web/core/components/analytics-v2/overview/active-project-item.tsx b/web/core/components/analytics/overview/active-project-item.tsx similarity index 100% rename from web/core/components/analytics-v2/overview/active-project-item.tsx rename to web/core/components/analytics/overview/active-project-item.tsx diff --git a/web/core/components/analytics-v2/overview/active-projects.tsx b/web/core/components/analytics/overview/active-projects.tsx similarity index 93% rename from web/core/components/analytics-v2/overview/active-projects.tsx rename to web/core/components/analytics/overview/active-projects.tsx index cb1e2ef69..2bcc8e831 100644 --- a/web/core/components/analytics-v2/overview/active-projects.tsx +++ b/web/core/components/analytics/overview/active-projects.tsx @@ -6,7 +6,7 @@ import useSWR from "swr"; import { useTranslation } from "@plane/i18n"; import { Loader } from "@plane/ui"; // plane web hooks -import { useAnalyticsV2, useProject } from "@/hooks/store"; +import { useAnalytics, useProject } from "@/hooks/store"; // plane web components import AnalyticsSectionWrapper from "../analytics-section-wrapper"; import ActiveProjectItem from "./active-project-item"; @@ -15,7 +15,7 @@ const ActiveProjects = observer(() => { const { t } = useTranslation(); const { fetchProjectAnalyticsCount } = useProject(); const { workspaceSlug } = useParams(); - const { selectedDurationLabel } = useAnalyticsV2(); + const { selectedDurationLabel } = useAnalytics(); const { data: projectAnalyticsCount, isLoading: isProjectAnalyticsCountLoading } = useSWR( workspaceSlug ? ["projectAnalyticsCount", workspaceSlug] : null, workspaceSlug diff --git a/web/core/components/analytics-v2/overview/index.ts b/web/core/components/analytics/overview/index.ts similarity index 100% rename from web/core/components/analytics-v2/overview/index.ts rename to web/core/components/analytics/overview/index.ts diff --git a/web/core/components/analytics-v2/overview/project-insights.tsx b/web/core/components/analytics/overview/project-insights.tsx similarity index 80% rename from web/core/components/analytics-v2/overview/project-insights.tsx rename to web/core/components/analytics/overview/project-insights.tsx index 9c8f829a1..9844a4b4f 100644 --- a/web/core/components/analytics-v2/overview/project-insights.tsx +++ b/web/core/components/analytics/overview/project-insights.tsx @@ -6,13 +6,13 @@ import useSWR from "swr"; import { useTranslation } from "@plane/i18n"; import { TChartData } from "@plane/types"; // hooks -import { useAnalyticsV2 } from "@/hooks/store/use-analytics-v2"; +import { useAnalytics } from "@/hooks/store/use-analytics"; // services import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; -import { AnalyticsV2Service } from "@/services/analytics-v2.service"; +import { AnalyticsService } from "@/services/analytics.service"; // plane web components import AnalyticsSectionWrapper from "../analytics-section-wrapper"; -import AnalyticsV2EmptyState from "../empty-state"; +import AnalyticsEmptyState from "../empty-state"; import { ProjectInsightsLoader } from "../loaders"; const RadarChart = dynamic(() => @@ -21,25 +21,28 @@ const RadarChart = dynamic(() => })) ); -const analyticsV2Service = new AnalyticsV2Service(); +const analyticsService = new AnalyticsService(); const ProjectInsights = observer(() => { const params = useParams(); const { t } = useTranslation(); const workspaceSlug = params.workspaceSlug.toString(); const { selectedDuration, selectedDurationLabel, selectedProjects, selectedCycle, selectedModule, isPeekView } = - useAnalyticsV2(); - const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/analytics-v2/empty-chart-radar" }); + useAnalytics(); + const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/analytics/empty-chart-radar" }); const { data: projectInsightsData, isLoading: isLoadingProjectInsight } = useSWR( `radar-chart-${workspaceSlug}-${selectedDuration}-${selectedProjects}-${selectedCycle}-${selectedModule}-${isPeekView}`, () => - analyticsV2Service.getAdvanceAnalyticsCharts[]>(workspaceSlug, "projects", { - // date_filter: selectedDuration, - ...(selectedProjects?.length > 0 && { project_ids: selectedProjects?.join(",") }), - ...(selectedCycle ? { cycle_id: selectedCycle } : {}), - ...(selectedModule ? { module_id: selectedModule } : {}), - }, + analyticsService.getAdvanceAnalyticsCharts[]>( + workspaceSlug, + "projects", + { + // date_filter: selectedDuration, + ...(selectedProjects?.length > 0 && { project_ids: selectedProjects?.join(",") }), + ...(selectedCycle ? { cycle_id: selectedCycle } : {}), + ...(selectedModule ? { module_id: selectedModule } : {}), + }, isPeekView ) ); @@ -53,9 +56,9 @@ const ProjectInsights = observer(() => { {isLoadingProjectInsight ? ( ) : projectInsightsData && projectInsightsData?.length == 0 ? ( - diff --git a/web/core/components/analytics-v2/overview/root.tsx b/web/core/components/analytics/overview/root.tsx similarity index 100% rename from web/core/components/analytics-v2/overview/root.tsx rename to web/core/components/analytics/overview/root.tsx diff --git a/web/core/components/analytics/project-modal/header.tsx b/web/core/components/analytics/project-modal/header.tsx deleted file mode 100644 index 79d63ce3c..000000000 --- a/web/core/components/analytics/project-modal/header.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { observer } from "mobx-react"; - -// icons -import { Expand, Shrink, X } from "lucide-react"; - -type Props = { - fullScreen: boolean; - handleClose: () => void; - setFullScreen: React.Dispatch>; - title: string; -}; - -export const ProjectAnalyticsModalHeader: React.FC = observer((props) => { - const { fullScreen, handleClose, setFullScreen, title } = props; - - return ( -
-

Analytics for {title}

-
- - -
-
- ); -}); diff --git a/web/core/components/analytics/project-modal/index.ts b/web/core/components/analytics/project-modal/index.ts deleted file mode 100644 index 70ca0260a..000000000 --- a/web/core/components/analytics/project-modal/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./header"; -export * from "./main-content"; -export * from "./modal"; diff --git a/web/core/components/analytics/project-modal/main-content.tsx b/web/core/components/analytics/project-modal/main-content.tsx deleted file mode 100644 index cd3813f80..000000000 --- a/web/core/components/analytics/project-modal/main-content.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import React, { Fragment } from "react"; -import { observer } from "mobx-react"; -import { Tab } from "@headlessui/react"; -// plane package imports -import { ANALYTICS_TABS } from "@plane/constants"; -import { useTranslation } from "@plane/i18n"; -import { ICycle, IModule, IProject } from "@plane/types"; -// components -import { CustomAnalytics, ScopeAndDemand } from "@/components/analytics"; - -type Props = { - fullScreen: boolean; - cycleDetails: ICycle | undefined; - moduleDetails: IModule | undefined; - projectDetails: IProject | undefined; -}; - -export const ProjectAnalyticsModalMainContent: React.FC = observer((props) => { - const { fullScreen, cycleDetails, moduleDetails } = props; - const { t } = useTranslation(); - return ( - - - {ANALYTICS_TABS.map((tab) => ( - - {({ selected }) => ( - - )} - - ))} - - - - - - - - - - - ); -}); diff --git a/web/core/components/analytics/project-modal/modal.tsx b/web/core/components/analytics/project-modal/modal.tsx deleted file mode 100644 index fb45d6aa9..000000000 --- a/web/core/components/analytics/project-modal/modal.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import React, { useState } from "react"; -import { observer } from "mobx-react"; -import { Dialog, Transition } from "@headlessui/react"; -import { ICycle, IModule, IProject } from "@plane/types"; - -// components -import { ProjectAnalyticsModalHeader, ProjectAnalyticsModalMainContent } from "@/components/analytics"; -// types - -type Props = { - isOpen: boolean; - onClose: () => void; - cycleDetails?: ICycle | undefined; - moduleDetails?: IModule | undefined; - projectDetails?: IProject | undefined; -}; - -export const ProjectAnalyticsModal: React.FC = observer((props) => { - const { isOpen, onClose, cycleDetails, moduleDetails, projectDetails } = props; - - const [fullScreen, setFullScreen] = useState(false); - - const handleClose = () => { - onClose(); - }; - - return ( - - - -
- -
-
- - -
-
-
-
-
-
-
- ); -}); diff --git a/web/core/components/analytics/scope-and-demand/demand.tsx b/web/core/components/analytics/scope-and-demand/demand.tsx deleted file mode 100644 index fefdc8fc6..000000000 --- a/web/core/components/analytics/scope-and-demand/demand.tsx +++ /dev/null @@ -1,58 +0,0 @@ -// plane imports -import { STATE_GROUPS } from "@plane/constants"; -import { useTranslation } from "@plane/i18n"; -// types -import { IDefaultAnalyticsResponse, TStateGroups } from "@plane/types"; -// constants -import { Card } from "@plane/ui"; - -type Props = { - defaultAnalytics: IDefaultAnalyticsResponse; -}; - -export const AnalyticsDemand: React.FC = ({ defaultAnalytics }) => { - const { t } = useTranslation(); - - return ( - -
-

{t("workspace_analytics.open_tasks")}

-

{defaultAnalytics.open_issues}

-
-
- {defaultAnalytics?.open_issues_classified.map((group) => { - const percentage = ((group.state_count / defaultAnalytics.total_issues) * 100).toFixed(0); - - return ( -
-
-
- -
{group.state_group}
- - {group.state_count} - -
-

{percentage}%

-
-
-
-
-
- ); - })} -
- - ); -}; diff --git a/web/core/components/analytics/scope-and-demand/index.ts b/web/core/components/analytics/scope-and-demand/index.ts deleted file mode 100644 index ae756a961..000000000 --- a/web/core/components/analytics/scope-and-demand/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from "./demand"; -export * from "./leaderboard"; -export * from "./scope-and-demand"; -export * from "./scope"; -export * from "./year-wise-issues"; diff --git a/web/core/components/analytics/scope-and-demand/leaderboard.tsx b/web/core/components/analytics/scope-and-demand/leaderboard.tsx deleted file mode 100644 index 76f96be4a..000000000 --- a/web/core/components/analytics/scope-and-demand/leaderboard.tsx +++ /dev/null @@ -1,69 +0,0 @@ -// plane ui -import { useTranslation } from "@plane/i18n"; -import { Card } from "@plane/ui"; -// components -import { ProfileEmptyState } from "@/components/ui"; -// helpers -import { getFileURL } from "@/helpers/file.helper"; -// image -import emptyUsers from "@/public/empty-state/empty_users.svg"; - -type Props = { - users: { - avatar_url: string | null; - display_name: string | null; - firstName: string; - lastName: string; - count: number; - id: string; - }[]; - title: string; - emptyStateMessage: string; - workspaceSlug: string; -}; - -export const AnalyticsLeaderBoard: React.FC = ({ users, title, emptyStateMessage, workspaceSlug }) => { - const { t } = useTranslation(); - return ( - -
{title}
- {users.length > 0 ? ( -
- ) : ( -
- -
- )} - - ); -}; diff --git a/web/core/components/analytics/scope-and-demand/scope-and-demand.tsx b/web/core/components/analytics/scope-and-demand/scope-and-demand.tsx deleted file mode 100644 index ae51727aa..000000000 --- a/web/core/components/analytics/scope-and-demand/scope-and-demand.tsx +++ /dev/null @@ -1,115 +0,0 @@ -"use client"; -import { useParams } from "next/navigation"; -import useSWR from "swr"; -// ui -import { useTranslation } from "@plane/i18n"; -import { Button, ContentWrapper, Loader } from "@plane/ui"; -// components -import { AnalyticsDemand, AnalyticsLeaderBoard, AnalyticsScope, AnalyticsYearWiseIssues } from "@/components/analytics"; -// fetch-keys -import { DEFAULT_ANALYTICS } from "@/constants/fetch-keys"; -// services -import { AnalyticsService } from "@/services/analytics.service"; - -type Props = { - fullScreen?: boolean; -}; - -// services -const analyticsService = new AnalyticsService(); - -export const ScopeAndDemand: React.FC = (props) => { - const { fullScreen = true } = props; - - const { workspaceSlug, projectId, cycleId, moduleId } = useParams(); - const { t } = useTranslation(); - - const isProjectLevel = projectId ? true : false; - - const params = isProjectLevel - ? { - project: projectId ? [projectId.toString()] : null, - cycle: cycleId ? cycleId.toString() : null, - module: moduleId ? moduleId.toString() : null, - } - : undefined; - - const { - data: defaultAnalytics, - error: defaultAnalyticsError, - mutate: mutateDefaultAnalytics, - } = useSWR( - workspaceSlug ? DEFAULT_ANALYTICS(workspaceSlug.toString(), params) : null, - workspaceSlug ? () => analyticsService.getDefaultAnalytics(workspaceSlug.toString(), params) : null - ); - - // scope data - const pendingIssues = defaultAnalytics?.pending_issue_user ?? []; - const pendingUnAssignedIssuesUser = pendingIssues?.find((issue) => issue.assignees__id === null); - const pendingAssignedIssues = pendingIssues?.filter((issue) => issue.assignees__id !== null); - - return ( - <> - {!defaultAnalyticsError ? ( - defaultAnalytics ? ( - -
- - - ({ - avatar_url: user?.created_by__avatar_url, - firstName: user?.created_by__first_name, - lastName: user?.created_by__last_name, - display_name: user?.created_by__display_name, - count: user?.count, - id: user?.created_by__id, - }))} - title={t("workspace_analytics.most_work_items_created.title")} - emptyStateMessage={t("workspace_analytics.most_work_items_created.empty_state")} - workspaceSlug={workspaceSlug?.toString() ?? ""} - /> - ({ - avatar_url: user?.assignees__avatar_url, - firstName: user?.assignees__first_name, - lastName: user?.assignees__last_name, - display_name: user?.assignees__display_name, - count: user?.count, - id: user?.assignees__id, - }))} - title={t("workspace_analytics.most_work_items_closed.title")} - emptyStateMessage={t("workspace_analytics.most_work_items_closed.empty_state")} - workspaceSlug={workspaceSlug?.toString() ?? ""} - /> -
- -
-
-
- ) : ( - - - - - - - ) - ) : ( -
-
-

{t("workspace_analytics.error")}

-
- -
-
-
- )} - - ); -}; diff --git a/web/core/components/analytics/scope-and-demand/scope.tsx b/web/core/components/analytics/scope-and-demand/scope.tsx deleted file mode 100644 index 13cd60d56..000000000 --- a/web/core/components/analytics/scope-and-demand/scope.tsx +++ /dev/null @@ -1,99 +0,0 @@ -// plane types -import { useTranslation } from "@plane/i18n"; -import { IDefaultAnalyticsUser } from "@plane/types"; -// plane ui -import { Card } from "@plane/ui"; -// components -import { BarGraph, ProfileEmptyState } from "@/components/ui"; -// helpers -import { getFileURL } from "@/helpers/file.helper"; -// image -import emptyBarGraph from "@/public/empty-state/empty_bar_graph.svg"; - -type Props = { - pendingUnAssignedIssuesUser: IDefaultAnalyticsUser | undefined; - pendingAssignedIssues: IDefaultAnalyticsUser[]; -}; - -export const AnalyticsScope: React.FC = ({ pendingUnAssignedIssuesUser, pendingAssignedIssues }) => { - const { t } = useTranslation(); - return ( - -
-
-
-
{t("workspace_analytics.pending_work_items.title")}
- {pendingUnAssignedIssuesUser && ( -
- {t("unassigned")}: {pendingUnAssignedIssuesUser.count} -
- )} -
- - {pendingAssignedIssues && pendingAssignedIssues.length > 0 ? ( - `#f97316`} - customYAxisTickValues={pendingAssignedIssues.map((d) => (d.count > 0 ? d.count : 50))} - tooltip={(datum) => { - const assignee = pendingAssignedIssues.find((a) => a.assignees__id === `${datum.indexValue}`); - - return ( -
- - {assignee ? assignee.assignees__display_name : "No assignee"}:{" "} - - {datum.value} -
- ); - }} - axisBottom={{ - renderTick: (datum) => { - const assignee = pendingAssignedIssues[datum.tickIndex] ?? ""; - - if (assignee && assignee?.assignees__avatar_url && assignee?.assignees__avatar_url !== "") - return ( - - - - ); - else - return ( - - - - {datum.value ? `${assignee.assignees__display_name}`.toUpperCase()[0] : "?"} - - - ); - }, - }} - margin={{ top: 20 }} - theme={{ - axis: {}, - }} - /> - ) : ( -
- -
- )} -
-
-
- ); -}; diff --git a/web/core/components/analytics/scope-and-demand/year-wise-issues.tsx b/web/core/components/analytics/scope-and-demand/year-wise-issues.tsx deleted file mode 100644 index 0f469db70..000000000 --- a/web/core/components/analytics/scope-and-demand/year-wise-issues.tsx +++ /dev/null @@ -1,64 +0,0 @@ -// ui -import { useTranslation } from "@plane/i18n"; -import { IDefaultAnalyticsResponse } from "@plane/types"; -import { Card } from "@plane/ui"; -import { LineGraph, ProfileEmptyState } from "@/components/ui"; -// image -import { MONTHS_LIST } from "@/constants/calendar"; -import emptyGraph from "@/public/empty-state/empty_graph.svg"; -// types -// constants - -type Props = { - defaultAnalytics: IDefaultAnalyticsResponse; -}; - -export const AnalyticsYearWiseIssues: React.FC = ({ defaultAnalytics }) => { - const { t } = useTranslation(); - return ( - -

{t("workspace_analytics.work_items_closed_in_a_year.title")}

- {defaultAnalytics.issue_completed_month_wise.length > 0 ? ( - ({ - x: t(month.shortTitle), - y: - defaultAnalytics.issue_completed_month_wise.find((data) => data.month === parseInt(index, 10)) - ?.count || 0, - })), - }, - ]} - customYAxisTickValues={defaultAnalytics.issue_completed_month_wise.map((data) => data.count)} - height="300px" - colors={(datum) => datum.color} - curve="monotoneX" - margin={{ top: 20 }} - enableSlices="x" - sliceTooltip={(datum) => ( -
- {datum.slice.points[0].data.yFormatted} - {t("workspace_analytics.work_items_closed_in")} - {datum.slice.points[0].data.xFormatted} -
- )} - theme={{ - background: "rgb(var(--color-background-100))", - }} - enableArea - /> - ) : ( -
- -
- )} -
- ); -}; diff --git a/web/core/components/analytics-v2/select/analytics-params.tsx b/web/core/components/analytics/select/analytics-params.tsx similarity index 80% rename from web/core/components/analytics-v2/select/analytics-params.tsx rename to web/core/components/analytics/select/analytics-params.tsx index 61a9d1b1f..f4ef0d9eb 100644 --- a/web/core/components/analytics-v2/select/analytics-params.tsx +++ b/web/core/components/analytics/select/analytics-params.tsx @@ -3,31 +3,30 @@ import { observer } from "mobx-react"; import { Control, Controller, UseFormSetValue } from "react-hook-form"; import { Calendar, SlidersHorizontal } from "lucide-react"; // plane package imports -import { ANALYTICS_V2_X_AXIS_VALUES, ANALYTICS_V2_Y_AXIS_VALUES, ChartYAxisMetric } from "@plane/constants"; +import { ANALYTICS_X_AXIS_VALUES, ANALYTICS_Y_AXIS_VALUES, ChartYAxisMetric } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; -import { IAnalyticsV2Params } from "@plane/types"; +import { IAnalyticsParams } from "@plane/types"; import { cn } from "@plane/utils"; // plane web components -import { AnalyticsV2Service } from "@/services/analytics-v2.service"; import { SelectXAxis } from "./select-x-axis"; import { SelectYAxis } from "./select-y-axis"; type Props = { - control: Control; - setValue: UseFormSetValue; - params: IAnalyticsV2Params; + control: Control; + setValue: UseFormSetValue; + params: IAnalyticsParams; workspaceSlug: string; classNames?: string; }; -export const AnalyticsV2SelectParams: React.FC = observer((props) => { +export const AnalyticsSelectParams: React.FC = observer((props) => { const { control, params, classNames } = props; const xAxisOptions = useMemo( - () => ANALYTICS_V2_X_AXIS_VALUES.filter((option) => option.value !== params.group_by), + () => ANALYTICS_X_AXIS_VALUES.filter((option) => option.value !== params.group_by), [params.group_by] ); const groupByOptions = useMemo( - () => ANALYTICS_V2_X_AXIS_VALUES.filter((option) => option.value !== params.x_axis), + () => ANALYTICS_X_AXIS_VALUES.filter((option) => option.value !== params.x_axis), [params.x_axis] ); @@ -43,7 +42,7 @@ export const AnalyticsV2SelectParams: React.FC = observer((props) => { onChange={(val: ChartYAxisMetric | null) => { onChange(val); }} - options={ANALYTICS_V2_Y_AXIS_VALUES} + options={ANALYTICS_Y_AXIS_VALUES} hiddenOptions={[ChartYAxisMetric.ESTIMATE_POINT_COUNT]} /> )} diff --git a/web/core/components/analytics-v2/select/duration.tsx b/web/core/components/analytics/select/duration.tsx similarity index 76% rename from web/core/components/analytics-v2/select/duration.tsx rename to web/core/components/analytics/select/duration.tsx index de18ab202..5c99a61b0 100644 --- a/web/core/components/analytics-v2/select/duration.tsx +++ b/web/core/components/analytics/select/duration.tsx @@ -2,7 +2,7 @@ import React, { ReactNode } from "react"; import { Calendar } from "lucide-react"; // plane package imports -import { ANALYTICS_V2_DURATION_FILTER_OPTIONS } from "@plane/constants"; +import { ANALYTICS_DURATION_FILTER_OPTIONS } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { CustomSearchSelect } from "@plane/ui"; // types @@ -10,7 +10,7 @@ import { TDropdownProps } from "@/components/dropdowns/types"; type Props = TDropdownProps & { value: string | null; - onChange: (val: (typeof ANALYTICS_V2_DURATION_FILTER_OPTIONS)[number]["value"]) => void; + onChange: (val: (typeof ANALYTICS_DURATION_FILTER_OPTIONS)[number]["value"]) => void; //optional button?: ReactNode; dropdownArrow?: boolean; @@ -23,7 +23,7 @@ type Props = TDropdownProps & { function DurationDropdown({ placeholder = "Duration", onChange, value }: Props) { useTranslation(); - const options = ANALYTICS_V2_DURATION_FILTER_OPTIONS.map((option) => ({ + const options = ANALYTICS_DURATION_FILTER_OPTIONS.map((option) => ({ value: option.value, query: option.name, content: ( @@ -40,7 +40,7 @@ function DurationDropdown({ placeholder = "Duration", onChange, value }: Props) label={
- {value ? ANALYTICS_V2_DURATION_FILTER_OPTIONS.find((opt) => opt.value === value)?.name : placeholder} + {value ? ANALYTICS_DURATION_FILTER_OPTIONS.find((opt) => opt.value === value)?.name : placeholder}
} /> diff --git a/web/core/components/analytics-v2/select/project.tsx b/web/core/components/analytics/select/project.tsx similarity index 100% rename from web/core/components/analytics-v2/select/project.tsx rename to web/core/components/analytics/select/project.tsx diff --git a/web/core/components/analytics-v2/select/select-x-axis.tsx b/web/core/components/analytics/select/select-x-axis.tsx similarity index 100% rename from web/core/components/analytics-v2/select/select-x-axis.tsx rename to web/core/components/analytics/select/select-x-axis.tsx diff --git a/web/core/components/analytics-v2/select/select-y-axis.tsx b/web/core/components/analytics/select/select-y-axis.tsx similarity index 100% rename from web/core/components/analytics-v2/select/select-y-axis.tsx rename to web/core/components/analytics/select/select-y-axis.tsx diff --git a/web/core/components/analytics/total-insights.tsx b/web/core/components/analytics/total-insights.tsx new file mode 100644 index 000000000..61f3e7205 --- /dev/null +++ b/web/core/components/analytics/total-insights.tsx @@ -0,0 +1,102 @@ +// plane package imports +import { observer } from "mobx-react-lite"; +import { useParams } from "next/navigation"; +import useSWR from "swr"; +import { IInsightField, insightsFields } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; +import { IAnalyticsResponse, TAnalyticsTabsBase } from "@plane/types"; +//hooks +import { cn } from "@/helpers/common.helper"; +import { useAnalytics } from "@/hooks/store/use-analytics"; +//services +import { AnalyticsService } from "@/services/analytics.service"; +// plane web components +import InsightCard from "./insight-card"; + +const analyticsService = new AnalyticsService(); + +const getInsightLabel = ( + analyticsType: TAnalyticsTabsBase, + item: IInsightField, + isEpic: boolean | undefined, + t: (key: string, options?: any) => string +) => { + if (analyticsType === "work-items") { + return isEpic + ? t(item.i18nKey, { entity: t("common.epics") }) + : t(item.i18nKey, { entity: t("common.work_items") }); + } + + // Get the base translation with entity + const baseTranslation = t(item.i18nKey, { + ...item.i18nProps, + entity: item.i18nProps?.entity && t(item.i18nProps?.entity), + }); + + // Add prefix if available + const prefix = item.i18nProps?.prefix ? `${t(item.i18nProps.prefix)} ` : ""; + + // Add suffix if available + const suffix = item.i18nProps?.suffix ? ` ${t(item.i18nProps.suffix)}` : ""; + + // Combine prefix, base translation, and suffix + return `${prefix}${baseTranslation}${suffix}`; +}; + +const TotalInsights: React.FC<{ + analyticsType: TAnalyticsTabsBase; + peekView?: boolean; +}> = observer(({ analyticsType, peekView }) => { + const params = useParams(); + const workspaceSlug = params.workspaceSlug.toString(); + const { t } = useTranslation(); + const { + selectedDuration, + selectedProjects, + selectedDurationLabel, + selectedCycle, + selectedModule, + isPeekView, + isEpic, + } = useAnalytics(); + const { data: totalInsightsData, isLoading } = useSWR( + `total-insights-${analyticsType}-${selectedDuration}-${selectedProjects}-${selectedCycle}-${selectedModule}-${isEpic}`, + () => + analyticsService.getAdvanceAnalytics( + workspaceSlug, + analyticsType, + { + // date_filter: selectedDuration, + ...(selectedProjects?.length > 0 ? { project_ids: selectedProjects.join(",") } : {}), + ...(selectedCycle ? { cycle_id: selectedCycle } : {}), + ...(selectedModule ? { module_id: selectedModule } : {}), + ...(isEpic ? { epic: true } : {}), + }, + isPeekView + ) + ); + return ( +
+ {insightsFields[analyticsType]?.map((item) => ( + + ))} +
+ ); +}); + +export default TotalInsights; diff --git a/web/core/components/analytics-v2/trend-piece.tsx b/web/core/components/analytics/trend-piece.tsx similarity index 100% rename from web/core/components/analytics-v2/trend-piece.tsx rename to web/core/components/analytics/trend-piece.tsx diff --git a/web/core/components/analytics-v2/work-items/created-vs-resolved.tsx b/web/core/components/analytics/work-items/created-vs-resolved.tsx similarity index 70% rename from web/core/components/analytics-v2/work-items/created-vs-resolved.tsx rename to web/core/components/analytics/work-items/created-vs-resolved.tsx index 46f3faf54..76215238d 100644 --- a/web/core/components/analytics-v2/work-items/created-vs-resolved.tsx +++ b/web/core/components/analytics/work-items/created-vs-resolved.tsx @@ -5,35 +5,46 @@ import useSWR from "swr"; // plane package imports import { useTranslation } from "@plane/i18n"; import { AreaChart } from "@plane/propel/charts/area-chart"; -import { IChartResponseV2, TChartData } from "@plane/types"; +import { IChartResponse, TChartData } from "@plane/types"; import { renderFormattedDate } from "@plane/utils"; // hooks -import { useAnalyticsV2 } from "@/hooks/store/use-analytics-v2"; +import { useAnalytics } from "@/hooks/store/use-analytics"; // services import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; -import { AnalyticsV2Service } from "@/services/analytics-v2.service"; +import { AnalyticsService } from "@/services/analytics.service"; // plane web components import AnalyticsSectionWrapper from "../analytics-section-wrapper"; -import AnalyticsV2EmptyState from "../empty-state"; +import AnalyticsEmptyState from "../empty-state"; import { ChartLoader } from "../loaders"; -const analyticsV2Service = new AnalyticsV2Service(); +const analyticsService = new AnalyticsService(); const CreatedVsResolved = observer(() => { - const { selectedDuration, selectedDurationLabel, selectedProjects, selectedCycle, selectedModule, isPeekView } = - useAnalyticsV2(); + const { + selectedDuration, + selectedDurationLabel, + selectedProjects, + selectedCycle, + selectedModule, + isPeekView, + isEpic, + } = useAnalytics(); const params = useParams(); const { t } = useTranslation(); const workspaceSlug = params.workspaceSlug.toString(); - const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/analytics-v2/empty-chart-area" }); + const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/analytics/empty-chart-area" }); const { data: createdVsResolvedData, isLoading: isCreatedVsResolvedLoading } = useSWR( - `created-vs-resolved-${workspaceSlug}-${selectedDuration}-${selectedProjects}-${selectedCycle}-${selectedModule}-${isPeekView}`, + `created-vs-resolved-${workspaceSlug}-${selectedDuration}-${selectedProjects}-${selectedCycle}-${selectedModule}-${isPeekView}-${isEpic}`, () => - analyticsV2Service.getAdvanceAnalyticsCharts(workspaceSlug, "work-items", { - // date_filter: selectedDuration, - ...(selectedProjects?.length > 0 && { project_ids: selectedProjects?.join(",") }), - ...(selectedCycle ? { cycle_id: selectedCycle } : {}), - ...(selectedModule ? { module_id: selectedModule } : {}), - }, + analyticsService.getAdvanceAnalyticsCharts( + workspaceSlug, + "work-items", + { + // date_filter: selectedDuration, + ...(selectedProjects?.length > 0 && { project_ids: selectedProjects?.join(",") }), + ...(selectedCycle ? { cycle_id: selectedCycle } : {}), + ...(selectedModule ? { module_id: selectedModule } : {}), + ...(isEpic ? { epic: true } : {}), + }, isPeekView ) ); @@ -89,11 +100,11 @@ const CreatedVsResolved = observer(() => { areas={areas} xAxis={{ key: "name", - label: "Date", + label: t("date"), }} yAxis={{ key: "count", - label: "Number of Issues", + label: t("no_of", { entity: t("work_items") }), offset: -30, dx: -22, }} @@ -110,9 +121,9 @@ const CreatedVsResolved = observer(() => { }} /> ) : ( - diff --git a/web/core/components/analytics-v2/work-items/customized-insights.tsx b/web/core/components/analytics/work-items/customized-insights.tsx similarity index 84% rename from web/core/components/analytics-v2/work-items/customized-insights.tsx rename to web/core/components/analytics/work-items/customized-insights.tsx index 86fea0c83..657465822 100644 --- a/web/core/components/analytics-v2/work-items/customized-insights.tsx +++ b/web/core/components/analytics/work-items/customized-insights.tsx @@ -4,14 +4,14 @@ import { useForm } from "react-hook-form"; // plane package imports import { ChartXAxisProperty, ChartYAxisMetric } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; -import { IAnalyticsV2Params } from "@plane/types"; +import { IAnalyticsParams } from "@plane/types"; import { cn } from "@plane/utils"; // plane web components import AnalyticsSectionWrapper from "../analytics-section-wrapper"; -import { AnalyticsV2SelectParams } from "../select/analytics-params"; +import { AnalyticsSelectParams } from "../select/analytics-params"; import PriorityChart from "./priority-chart"; -const defaultValues: IAnalyticsV2Params = { +const defaultValues: IAnalyticsParams = { x_axis: ChartXAxisProperty.PRIORITY, y_axis: ChartYAxisMetric.WORK_ITEM_COUNT, }; @@ -19,7 +19,7 @@ const defaultValues: IAnalyticsV2Params = { const CustomizedInsights = observer(({ peekView }: { peekView?: boolean }) => { const { t } = useTranslation(); const { workspaceSlug } = useParams(); - const { control, watch, setValue } = useForm({ + const { control, watch, setValue } = useForm({ defaultValues: { ...defaultValues, }, @@ -37,7 +37,7 @@ const CustomizedInsights = observer(({ peekView }: { peekView?: boolean }) => { className="col-span-1" headerClassName={cn(peekView ? "flex-col items-start" : "")} actions={ - = observer((props) => { const { projectDetails, cycleDetails, moduleDetails, fullScreen } = props; - const { updateSelectedProjects, updateSelectedCycle, updateSelectedModule, updateIsPeekView } = useAnalyticsV2(); + const { updateSelectedProjects, updateSelectedCycle, updateSelectedModule, updateIsPeekView } = useAnalytics(); const [isModalConfigured, setIsModalConfigured] = useState(false); useEffect(() => { diff --git a/web/core/components/analytics-v2/work-items/modal/header.tsx b/web/core/components/analytics/work-items/modal/header.tsx similarity index 100% rename from web/core/components/analytics-v2/work-items/modal/header.tsx rename to web/core/components/analytics/work-items/modal/header.tsx diff --git a/web/core/components/analytics-v2/work-items/modal/index.tsx b/web/core/components/analytics/work-items/modal/index.tsx similarity index 90% rename from web/core/components/analytics-v2/work-items/modal/index.tsx rename to web/core/components/analytics/work-items/modal/index.tsx index c30c2687d..292dc1be5 100644 --- a/web/core/components/analytics-v2/work-items/modal/index.tsx +++ b/web/core/components/analytics/work-items/modal/index.tsx @@ -1,8 +1,9 @@ -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import { observer } from "mobx-react"; import { Dialog, Transition } from "@headlessui/react"; // plane package imports import { ICycle, IModule, IProject } from "@plane/types"; +import { useAnalytics } from "@/hooks/store"; // plane web components import { WorkItemsModalMainContent } from "./content"; import { WorkItemsModalHeader } from "./header"; @@ -13,17 +14,22 @@ type Props = { projectDetails?: IProject | undefined; cycleDetails?: ICycle | undefined; moduleDetails?: IModule | undefined; + isEpic?: boolean; }; export const WorkItemsModal: React.FC = observer((props) => { - const { isOpen, onClose, projectDetails, moduleDetails, cycleDetails } = props; - + const { isOpen, onClose, projectDetails, moduleDetails, cycleDetails, isEpic } = props; + const { updateIsEpic } = useAnalytics(); const [fullScreen, setFullScreen] = useState(false); const handleClose = () => { onClose(); }; + useEffect(() => { + updateIsEpic(isEpic ?? false); + }, [isEpic, updateIsEpic]); + return ( diff --git a/web/core/components/analytics-v2/work-items/priority-chart.tsx b/web/core/components/analytics/work-items/priority-chart.tsx similarity index 88% rename from web/core/components/analytics-v2/work-items/priority-chart.tsx rename to web/core/components/analytics/work-items/priority-chart.tsx index 9d10c2b6a..78a05c2c7 100644 --- a/web/core/components/analytics-v2/work-items/priority-chart.tsx +++ b/web/core/components/analytics/work-items/priority-chart.tsx @@ -8,8 +8,8 @@ import useSWR from "swr"; // plane package imports import { Download } from "lucide-react"; import { - ANALYTICS_V2_X_AXIS_VALUES, - ANALYTICS_V2_Y_AXIS_VALUES, + ANALYTICS_X_AXIS_VALUES, + ANALYTICS_Y_AXIS_VALUES, CHART_COLOR_PALETTES, ChartXAxisDateGrouping, ChartXAxisProperty, @@ -18,17 +18,17 @@ import { } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { BarChart } from "@plane/propel/charts/bar-chart"; -import { IChartResponseV2 } from "@plane/types"; +import { IChartResponse } from "@plane/types"; import { TBarItem, TChart, TChartData, TChartDatum } from "@plane/types/src/charts"; // plane web components import { Button } from "@plane/ui"; import { generateExtendedColors, parseChartData } from "@/components/chart/utils"; // hooks import { useProjectState } from "@/hooks/store"; -import { useAnalyticsV2 } from "@/hooks/store/use-analytics-v2"; +import { useAnalytics } from "@/hooks/store/use-analytics"; import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; -import { AnalyticsV2Service } from "@/services/analytics-v2.service"; -import AnalyticsV2EmptyState from "../empty-state"; +import { AnalyticsService } from "@/services/analytics.service"; +import AnalyticsEmptyState from "../empty-state"; import { DataTable } from "../insight-table/data-table"; import { ChartLoader } from "../loaders"; import { generateBarColor } from "./utils"; @@ -40,13 +40,13 @@ interface Props { x_axis_date_grouping?: ChartXAxisDateGrouping; } -const analyticsV2Service = new AnalyticsV2Service(); +const analyticsService = new AnalyticsService(); const PriorityChart = observer((props: Props) => { const { x_axis, y_axis, group_by } = props; const { t } = useTranslation(); - const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/analytics-v2/empty-chart-bar" }); + const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/analytics/empty-chart-bar" }); // store hooks - const { selectedDuration, selectedProjects, selectedCycle, selectedModule, isPeekView } = useAnalyticsV2(); + const { selectedDuration, selectedProjects, selectedCycle, selectedModule, isPeekView, isEpic } = useAnalytics(); const { workspaceStates } = useProjectState(); const { resolvedTheme } = useTheme(); // router @@ -55,9 +55,9 @@ const PriorityChart = observer((props: Props) => { const { data: priorityChartData, isLoading: priorityChartLoading } = useSWR( `customized-insights-chart-${workspaceSlug}-${selectedDuration}- - ${selectedProjects}-${selectedCycle}-${selectedModule}-${props.x_axis}-${props.y_axis}-${props.group_by}-${isPeekView}`, + ${selectedProjects}-${selectedCycle}-${selectedModule}-${props.x_axis}-${props.y_axis}-${props.group_by}-${isPeekView}-${isEpic}`, () => - analyticsV2Service.getAdvanceAnalyticsCharts( + analyticsService.getAdvanceAnalyticsCharts( workspaceSlug, "custom-work-items", { @@ -65,6 +65,7 @@ const PriorityChart = observer((props: Props) => { ...(selectedProjects?.length > 0 && { project_ids: selectedProjects?.join(",") }), ...(selectedCycle ? { cycle_id: selectedCycle } : {}), ...(selectedModule ? { module_id: selectedModule } : {}), + ...(isEpic ? { epic: true } : {}), ...props, }, isPeekView @@ -132,11 +133,11 @@ const PriorityChart = observer((props: Props) => { }, [chart_model, group_by, parsedData, resolvedTheme, workspaceStates, x_axis, y_axis]); const yAxisLabel = useMemo( - () => ANALYTICS_V2_Y_AXIS_VALUES.find((item) => item.value === props.y_axis)?.label ?? props.y_axis, + () => ANALYTICS_Y_AXIS_VALUES.find((item) => item.value === props.y_axis)?.label ?? props.y_axis, [props.y_axis] ); const xAxisLabel = useMemo( - () => ANALYTICS_V2_X_AXIS_VALUES.find((item) => item.value === props.x_axis)?.label ?? props.x_axis, + () => ANALYTICS_X_AXIS_VALUES.find((item) => item.value === props.x_axis)?.label ?? props.x_axis, [props.x_axis] ); @@ -237,9 +238,9 @@ const PriorityChart = observer((props: Props) => { /> ) : ( - diff --git a/web/core/components/analytics-v2/work-items/root.tsx b/web/core/components/analytics/work-items/root.tsx similarity index 100% rename from web/core/components/analytics-v2/work-items/root.tsx rename to web/core/components/analytics/work-items/root.tsx diff --git a/web/core/components/analytics-v2/work-items/utils.ts b/web/core/components/analytics/work-items/utils.ts similarity index 100% rename from web/core/components/analytics-v2/work-items/utils.ts rename to web/core/components/analytics/work-items/utils.ts diff --git a/web/core/components/analytics-v2/work-items/workitems-insight-table.tsx b/web/core/components/analytics/work-items/workitems-insight-table.tsx similarity index 73% rename from web/core/components/analytics-v2/work-items/workitems-insight-table.tsx rename to web/core/components/analytics/work-items/workitems-insight-table.tsx index 6dca7d929..45e12b1e3 100644 --- a/web/core/components/analytics-v2/work-items/workitems-insight-table.tsx +++ b/web/core/components/analytics/work-items/workitems-insight-table.tsx @@ -1,5 +1,6 @@ -import { useMemo } from "react"; +import { useMemo, useCallback } from "react"; import { ColumnDef, Row } from "@tanstack/react-table"; +import { download, generateCsv } from "export-to-csv"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; import useSWR from "swr"; @@ -12,13 +13,14 @@ import { Avatar } from "@plane/ui"; import { getFileURL } from "@plane/utils"; import { Logo } from "@/components/common/logo"; // hooks -import { useAnalyticsV2 } from "@/hooks/store/use-analytics-v2"; +import { useAnalytics } from "@/hooks/store/use-analytics"; import { useProject } from "@/hooks/store/use-project"; -import { AnalyticsV2Service } from "@/services/analytics-v2.service"; +import { AnalyticsService } from "@/services/analytics.service"; // plane web components +import { csvConfig } from "../config"; import { InsightTable } from "../insight-table"; -const analyticsV2Service = new AnalyticsV2Service(); +const analyticsService = new AnalyticsService(); const WorkItemsInsightTable = observer(() => { // router @@ -27,11 +29,11 @@ const WorkItemsInsightTable = observer(() => { const { t } = useTranslation(); // store hooks const { getProjectById } = useProject(); - const { selectedDuration, selectedProjects, selectedCycle, selectedModule, isPeekView } = useAnalyticsV2(); + const { selectedDuration, selectedProjects, selectedCycle, selectedModule, isPeekView, isEpic } = useAnalytics(); const { data: workItemsData, isLoading } = useSWR( - `insights-table-work-items-${workspaceSlug}-${selectedDuration}-${selectedProjects}-${selectedCycle}-${selectedModule}-${isPeekView}`, + `insights-table-work-items-${workspaceSlug}-${selectedDuration}-${selectedProjects}-${selectedCycle}-${selectedModule}-${isPeekView}-${isEpic}`, () => - analyticsV2Service.getAdvanceAnalyticsStats( + analyticsService.getAdvanceAnalyticsStats( workspaceSlug, "work-items", { @@ -39,23 +41,25 @@ const WorkItemsInsightTable = observer(() => { ...(selectedProjects?.length > 0 ? { project_ids: selectedProjects.join(",") } : {}), ...(selectedCycle ? { cycle_id: selectedCycle } : {}), ...(selectedModule ? { module_id: selectedModule } : {}), + ...(isEpic ? { epic: true } : {}), }, isPeekView ) ); // derived values - const columnsLabels = useMemo( - () => ({ - backlog_work_items: t("workspace_projects.state.backlog"), - started_work_items: t("workspace_projects.state.started"), - un_started_work_items: t("workspace_projects.state.unstarted"), - completed_work_items: t("workspace_projects.state.completed"), - cancelled_work_items: t("workspace_projects.state.cancelled"), - project__name: t("common.project"), - display_name: t("common.assignee"), - }), - [t] - ); + const columnsLabels: Record, string> = + useMemo( + () => ({ + backlog_work_items: t("workspace_projects.state.backlog"), + started_work_items: t("workspace_projects.state.started"), + un_started_work_items: t("workspace_projects.state.unstarted"), + completed_work_items: t("workspace_projects.state.completed"), + cancelled_work_items: t("workspace_projects.state.cancelled"), + project__name: t("common.project"), + display_name: t("common.assignee"), + }), + [t] + ); const columns = useMemo( () => [ @@ -135,6 +139,25 @@ const WorkItemsInsightTable = observer(() => { [columnsLabels, getProjectById, isPeekView, t] ); + const exportCSV = useCallback( + (rows: Row[]) => { + const rowData: any = rows.map((row) => { + const { project_id, avatar_url, assignee_id, ...exportableData } = row.original; + return Object.fromEntries( + Object.entries(exportableData).map(([key, value]) => { + if (columnsLabels?.[key as keyof typeof columnsLabels]) { + return [columnsLabels[key as keyof typeof columnsLabels], value]; + } + return [key, value]; + }) + ); + }); + const csv = generateCsv(csvConfig(workspaceSlug))(rowData); + download(csvConfig(workspaceSlug))(csv); + }, + [columnsLabels, workspaceSlug] + ); + return ( analyticsType="work-items" @@ -142,7 +165,8 @@ const WorkItemsInsightTable = observer(() => { isLoading={isLoading} columns={columns} columnsLabels={columnsLabels} - headerText={isPeekView ? columnsLabels["display_name"] : columnsLabels["project__name"]} + headerText={isPeekView ? t("common.assignee") : t("common.projects")} + onExport={exportCSV} /> ); }); diff --git a/web/core/components/core/sidebar/progress-chart.tsx b/web/core/components/core/sidebar/progress-chart.tsx index bb4eeb493..e6317eeb9 100644 --- a/web/core/components/core/sidebar/progress-chart.tsx +++ b/web/core/components/core/sidebar/progress-chart.tsx @@ -1,155 +1,64 @@ import React from "react"; -import { eachDayOfInterval, isValid } from "date-fns"; -import { TModuleCompletionChartDistribution } from "@plane/types"; -// ui -import { LineGraph } from "@/components/ui"; -// helpers -import { getDate, renderFormattedDateWithoutYear } from "@/helpers/date-time.helper"; -//types +import { AreaChart } from "@plane/propel/charts/area-chart"; +import { TChartData, TModuleCompletionChartDistribution } from "@plane/types"; +import { renderFormattedDateWithoutYear } from "@/helpers/date-time.helper"; type Props = { distribution: TModuleCompletionChartDistribution; - startDate: string | Date; - endDate: string | Date; totalIssues: number; className?: string; plotTitle?: string; }; -const styleById = { - ideal: { - strokeDasharray: "6, 3", - strokeWidth: 1, - }, - default: { - strokeWidth: 1, - }, -}; - -const DashedLine = ({ series, lineGenerator, xScale, yScale }: any) => - series.map(({ id, data, color }: any) => ( - ({ - x: xScale(d.data.x), - y: yScale(d.data.y), - })) - )} - fill="none" - stroke={color ?? "#ddd"} - style={styleById[id as keyof typeof styleById] || styleById.default} - /> - )); - -const ProgressChart: React.FC = ({ - distribution, - startDate, - endDate, - totalIssues, - className = "", - plotTitle = "work items", -}) => { - const chartData = Object.keys(distribution ?? []).map((key) => ({ - currentDate: renderFormattedDateWithoutYear(key), - pending: distribution[key], +const ProgressChart: React.FC = ({ distribution, totalIssues, className = "", plotTitle = "work items" }) => { + const chartData: TChartData[] = Object.keys(distribution ?? []).map((key, index) => ({ + name: renderFormattedDateWithoutYear(key), + current: distribution[key] ?? 0, + ideal: totalIssues * (1 - index / (Object.keys(distribution ?? []).length - 1)), })); - const generateXAxisTickValues = () => { - const start = getDate(startDate); - const end = getDate(endDate); - - let dates: Date[] = []; - if (start && end && isValid(start) && isValid(end)) { - dates = eachDayOfInterval({ start, end }); - } - - if (dates.length === 0) return []; - - const formattedDates = dates.map((d) => renderFormattedDateWithoutYear(d)); - const firstDate = formattedDates[0]; - const lastDate = formattedDates[formattedDates.length - 1]; - - if (formattedDates.length <= 2) return [firstDate, lastDate]; - - const middleDateIndex = Math.floor(formattedDates.length / 2); - const middleDate = formattedDates[middleDateIndex]; - - return [firstDate, middleDate, lastDate]; - }; - return (
- 0 - ? chartData.map((item, index) => ({ - index, - x: item.currentDate, - y: item.pending, - color: "#3F76FF", - })) - : [], - enableArea: true, + key: "current", + label: `Current ${plotTitle}`, + strokeColor: "#3F76FF", + fill: "#3F76FF33", + fillOpacity: 1, + showDot: true, + smoothCurves: true, + strokeOpacity: 1, + stackId: "bar-one", }, { - id: "ideal", - color: "#a9bbd0", - fill: "transparent", - data: - chartData.length > 0 - ? [ - { - x: chartData[0].currentDate, - y: totalIssues, - }, - { - x: chartData[chartData.length - 1].currentDate, - y: 0, - }, - ] - : [], + key: "ideal", + label: `Ideal ${plotTitle}`, + strokeColor: "#A9BBD0", + fill: "#A9BBD0", + fillOpacity: 0, + showDot: true, + smoothCurves: true, + strokeOpacity: 1, + stackId: "bar-two", + style: { + strokeDasharray: "6, 3", + strokeWidth: 1, + }, }, ]} - layers={["grid", "markers", "areas", DashedLine, "slices", "points", "axes", "legends"]} - axisBottom={{ - tickValues: generateXAxisTickValues(), - }} - enablePoints={false} - enableArea - colors={(datum) => datum.color ?? "#3F76FF"} - customYAxisTickValues={[0, totalIssues]} - gridXValues={ - chartData.length > 0 ? chartData.map((item, index) => (index % 2 === 0 ? item.currentDate : "")) : undefined - } - enableSlices="x" - sliceTooltip={(datum) => ( -
- {datum.slice.points?.[1]?.data?.yFormatted ?? datum.slice.points[0].data.yFormatted} - {plotTitle} pending on - {datum.slice.points[0].data.xFormatted} -
- )} - theme={{ - background: "transparent", - axis: { - domain: { - line: { - stroke: "rgb(var(--color-border))", - strokeWidth: 1, - }, - }, + xAxis={{ key: "name", label: "Date" }} + yAxis={{ key: "current", label: "Completion" }} + margin={{ bottom: 30 }} + className="h-[370px] w-full" + legend={{ + align: "center", + verticalAlign: "bottom", + layout: "horizontal", + wrapperStyles: { + marginTop: 20, }, }} /> diff --git a/web/core/components/cycles/active-cycle/productivity.tsx b/web/core/components/cycles/active-cycle/productivity.tsx index 613355c7e..f53e2ef87 100644 --- a/web/core/components/cycles/active-cycle/productivity.tsx +++ b/web/core/components/cycles/active-cycle/productivity.tsx @@ -54,17 +54,7 @@ export const ActiveCycleProductivity: FC = observe {cycle.total_issues > 0 ? ( <>
-
-
-
- - {t("project_cycles.active_cycle.ideal")} -
-
- - {t("project_cycles.active_cycle.current")} -
-
+
{estimateType === "points" ? ( {`Pending points - ${cycle.backlog_estimate_points + cycle.unstarted_estimate_points + cycle.started_estimate_points}`} ) : ( @@ -78,16 +68,12 @@ export const ActiveCycleProductivity: FC = observe {estimateType === "points" ? ( ) : ( diff --git a/web/core/components/dashboard/home-dashboard-widgets.tsx b/web/core/components/dashboard/home-dashboard-widgets.tsx deleted file mode 100644 index fe64278dc..000000000 --- a/web/core/components/dashboard/home-dashboard-widgets.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { observer } from "mobx-react"; -import { useParams } from "next/navigation"; -// types -import { TWidgetKeys } from "@plane/types"; -// components -import { - AssignedIssuesWidget, - CreatedIssuesWidget, - IssuesByPriorityWidget, - IssuesByStateGroupWidget, - OverviewStatsWidget, - RecentActivityWidget, - RecentCollaboratorsWidget, - RecentProjectsWidget, - WidgetProps, -} from "@/components/dashboard"; -// hooks -import { useDashboard } from "@/hooks/store"; - -const WIDGETS_LIST: { - [key in TWidgetKeys]: { component: React.FC; fullWidth: boolean }; -} = { - overview_stats: { component: OverviewStatsWidget, fullWidth: true }, - assigned_issues: { component: AssignedIssuesWidget, fullWidth: false }, - created_issues: { component: CreatedIssuesWidget, fullWidth: false }, - issues_by_state_groups: { component: IssuesByStateGroupWidget, fullWidth: false }, - issues_by_priority: { component: IssuesByPriorityWidget, fullWidth: false }, - recent_activity: { component: RecentActivityWidget, fullWidth: false }, - recent_projects: { component: RecentProjectsWidget, fullWidth: false }, - recent_collaborators: { component: RecentCollaboratorsWidget, fullWidth: true }, -}; - -export const DashboardWidgets = observer(() => { - // router - const { workspaceSlug } = useParams(); - // store hooks - const { homeDashboardId, homeDashboardWidgets } = useDashboard(); - - const doesWidgetExist = (widgetKey: TWidgetKeys) => - Boolean(homeDashboardWidgets?.find((widget) => widget.key === widgetKey)); - - if (!workspaceSlug || !homeDashboardId) return null; - - return ( -
- {Object.entries(WIDGETS_LIST).map(([key, widget]) => { - const WidgetComponent = widget.component; - // if the widget doesn't exist, return null - if (!doesWidgetExist(key as TWidgetKeys)) return null; - // if the widget is full width, return it in a 2 column grid - if (widget.fullWidth) - return ( -
- -
- ); - else - return ; - })} -
- ); -}); diff --git a/web/core/components/dashboard/index.ts b/web/core/components/dashboard/index.ts index 129cdb69e..e88b1c701 100644 --- a/web/core/components/dashboard/index.ts +++ b/web/core/components/dashboard/index.ts @@ -1,3 +1,2 @@ export * from "./widgets"; -export * from "./home-dashboard-widgets"; export * from "./project-empty-state"; diff --git a/web/core/components/dashboard/widgets/index.ts b/web/core/components/dashboard/widgets/index.ts index 31fc645d4..e622d708f 100644 --- a/web/core/components/dashboard/widgets/index.ts +++ b/web/core/components/dashboard/widgets/index.ts @@ -5,8 +5,6 @@ export * from "./issue-panels"; export * from "./loaders"; export * from "./assigned-issues"; export * from "./created-issues"; -export * from "./issues-by-priority"; -export * from "./issues-by-state-group"; export * from "./overview-stats"; export * from "./recent-activity"; export * from "./recent-collaborators"; diff --git a/web/core/components/dashboard/widgets/issues-by-priority.tsx b/web/core/components/dashboard/widgets/issues-by-priority.tsx deleted file mode 100644 index ac6788120..000000000 --- a/web/core/components/dashboard/widgets/issues-by-priority.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import { useEffect } from "react"; -import { observer } from "mobx-react"; -import Link from "next/link"; -// types -import { EDurationFilters } from "@plane/constants"; -import { TIssuesByPriorityWidgetFilters, TIssuesByPriorityWidgetResponse } from "@plane/types"; -// components -import { Card } from "@plane/ui"; -import { - DurationFilterDropdown, - IssuesByPriorityEmptyState, - WidgetLoader, - WidgetProps, -} from "@/components/dashboard/widgets"; -import { IssuesByPriorityGraph } from "@/components/graphs"; -// constants -// helpers -import { getCustomDates } from "@/helpers/dashboard.helper"; -// hooks -import { useDashboard } from "@/hooks/store"; -import { useAppRouter } from "@/hooks/use-app-router"; - -const WIDGET_KEY = "issues_by_priority"; - -export const IssuesByPriorityWidget: React.FC = observer((props) => { - const { dashboardId, workspaceSlug } = props; - // router - const router = useAppRouter(); - // store hooks - const { fetchWidgetStats, getWidgetDetails, getWidgetStats, updateDashboardWidgetFilters } = useDashboard(); - // derived values - const widgetDetails = getWidgetDetails(workspaceSlug, dashboardId, WIDGET_KEY); - const widgetStats = getWidgetStats(workspaceSlug, dashboardId, WIDGET_KEY); - const selectedDuration = widgetDetails?.widget_filters.duration ?? EDurationFilters.NONE; - const selectedCustomDates = widgetDetails?.widget_filters.custom_dates ?? []; - - const handleUpdateFilters = async (filters: Partial) => { - if (!widgetDetails) return; - - await updateDashboardWidgetFilters(workspaceSlug, dashboardId, widgetDetails.id, { - widgetKey: WIDGET_KEY, - filters, - }); - - const filterDates = getCustomDates( - filters.duration ?? selectedDuration, - filters.custom_dates ?? selectedCustomDates - ); - fetchWidgetStats(workspaceSlug, dashboardId, { - widget_key: WIDGET_KEY, - ...(filterDates.trim() !== "" ? { target_date: filterDates } : {}), - }); - }; - - useEffect(() => { - const filterDates = getCustomDates(selectedDuration, selectedCustomDates); - fetchWidgetStats(workspaceSlug, dashboardId, { - widget_key: WIDGET_KEY, - ...(filterDates.trim() !== "" ? { target_date: filterDates } : {}), - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - if (!widgetDetails || !widgetStats) return ; - - const totalCount = widgetStats.reduce((acc, item) => acc + item?.count, 0); - const chartData = widgetStats.map((item) => ({ - priority: item?.priority, - priority_count: item?.count, - })); - - return ( - -
- - Assigned by priority - - - handleUpdateFilters({ - duration: val, - ...(val === "custom" ? { custom_dates: customDates } : {}), - }) - } - /> -
- {totalCount > 0 ? ( -
-
- { - router.push( - `/${workspaceSlug}/workspace-views/assigned?priority=${`${datum.data.priority}`.toLowerCase()}` - ); - }} - /> -
-
- ) : ( -
- -
- )} -
- ); -}); diff --git a/web/core/components/dashboard/widgets/issues-by-state-group.tsx b/web/core/components/dashboard/widgets/issues-by-state-group.tsx deleted file mode 100644 index fec4204cf..000000000 --- a/web/core/components/dashboard/widgets/issues-by-state-group.tsx +++ /dev/null @@ -1,251 +0,0 @@ -import { useEffect, useState } from "react"; -import { linearGradientDef } from "@nivo/core"; -import { observer } from "mobx-react"; -import Link from "next/link"; -// types -import { EDurationFilters, STATE_GROUPS } from "@plane/constants"; -import { TIssuesByStateGroupsWidgetFilters, TIssuesByStateGroupsWidgetResponse, TStateGroups } from "@plane/types"; -// components -import { Card } from "@plane/ui"; -import { - DurationFilterDropdown, - IssuesByStateGroupEmptyState, - WidgetLoader, - WidgetProps, -} from "@/components/dashboard/widgets"; -import { PieGraph } from "@/components/ui"; -// helpers -import { getCustomDates } from "@/helpers/dashboard.helper"; -// hooks -import { useDashboard } from "@/hooks/store"; -import { useAppRouter } from "@/hooks/use-app-router"; - -const WIDGET_KEY = "issues_by_state_groups"; - -export const STATE_GROUP_GRAPH_COLORS: Record = { - backlog: "#CDCED6", - unstarted: "#80838D", - started: "#FFC53D", - completed: "#3E9B4F", - cancelled: "#E5484D", -}; -// colors for work items by state group widget graph arcs -export const STATE_GROUP_GRAPH_GRADIENTS = [ - linearGradientDef("gradientBacklog", [ - { offset: 0, color: "#DEDEDE" }, - { offset: 100, color: "#BABABE" }, - ]), - linearGradientDef("gradientUnstarted", [ - { offset: 0, color: "#D4D4D4" }, - { offset: 100, color: "#878796" }, - ]), - linearGradientDef("gradientStarted", [ - { offset: 0, color: "#FFD300" }, - { offset: 100, color: "#FAE270" }, - ]), - linearGradientDef("gradientCompleted", [ - { offset: 0, color: "#0E8B1B" }, - { offset: 100, color: "#37CB46" }, - ]), - linearGradientDef("gradientCanceled", [ - { offset: 0, color: "#C90004" }, - { offset: 100, color: "#FF7679" }, - ]), -]; - -export const IssuesByStateGroupWidget: React.FC = observer((props) => { - const { dashboardId, workspaceSlug } = props; - // states - const [defaultStateGroup, setDefaultStateGroup] = useState(null); - const [activeStateGroup, setActiveStateGroup] = useState(null); - // router - const router = useAppRouter(); - // store hooks - const { fetchWidgetStats, getWidgetDetails, getWidgetStats, updateDashboardWidgetFilters } = useDashboard(); - // derived values - const widgetDetails = getWidgetDetails(workspaceSlug, dashboardId, WIDGET_KEY); - const widgetStats = getWidgetStats(workspaceSlug, dashboardId, WIDGET_KEY); - const selectedDuration = widgetDetails?.widget_filters.duration ?? EDurationFilters.NONE; - const selectedCustomDates = widgetDetails?.widget_filters.custom_dates ?? []; - - const handleUpdateFilters = async (filters: Partial) => { - if (!widgetDetails) return; - - await updateDashboardWidgetFilters(workspaceSlug, dashboardId, widgetDetails.id, { - widgetKey: WIDGET_KEY, - filters, - }); - - const filterDates = getCustomDates( - filters.duration ?? selectedDuration, - filters.custom_dates ?? selectedCustomDates - ); - fetchWidgetStats(workspaceSlug, dashboardId, { - widget_key: WIDGET_KEY, - ...(filterDates.trim() !== "" ? { target_date: filterDates } : {}), - }); - }; - - // fetch widget stats - useEffect(() => { - const filterDates = getCustomDates(selectedDuration, selectedCustomDates); - fetchWidgetStats(workspaceSlug, dashboardId, { - widget_key: WIDGET_KEY, - ...(filterDates.trim() !== "" ? { target_date: filterDates } : {}), - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - // set active group for center metric - useEffect(() => { - if (!widgetStats) return; - - const startedCount = widgetStats?.find((item) => item?.state === "started")?.count ?? 0; - const unStartedCount = widgetStats?.find((item) => item?.state === "unstarted")?.count ?? 0; - const backlogCount = widgetStats?.find((item) => item?.state === "backlog")?.count ?? 0; - const completedCount = widgetStats?.find((item) => item?.state === "completed")?.count ?? 0; - const canceledCount = widgetStats?.find((item) => item?.state === "cancelled")?.count ?? 0; - - const stateGroup = - startedCount > 0 - ? "started" - : unStartedCount > 0 - ? "unstarted" - : backlogCount > 0 - ? "backlog" - : completedCount > 0 - ? "completed" - : canceledCount > 0 - ? "cancelled" - : null; - - setActiveStateGroup(stateGroup); - setDefaultStateGroup(stateGroup); - }, [widgetStats]); - - if (!widgetDetails || !widgetStats) return ; - - const totalCount = widgetStats?.reduce((acc, item) => acc + item?.count, 0); - const chartData = widgetStats?.map((item) => ({ - color: STATE_GROUP_GRAPH_COLORS[item?.state as keyof typeof STATE_GROUP_GRAPH_COLORS], - id: item?.state, - label: item?.state, - value: (item?.count / totalCount) * 100, - })); - - const CenteredMetric = ({ dataWithArc, centerX, centerY }: any) => { - const data = dataWithArc?.find((datum: any) => datum?.id === activeStateGroup); - const percentage = chartData?.find((item) => item.id === activeStateGroup)?.value?.toFixed(0); - - return ( - - - {percentage}% - - - {data?.id} - - - ); - }; - - return ( - -
- - Assigned by state - - - handleUpdateFilters({ - duration: val, - ...(val === "custom" ? { custom_dates: customDates } : {}), - }) - } - /> -
- {totalCount > 0 ? ( -
-
-
- datum.data.color} - padAngle={1} - enableArcLinkLabels={false} - enableArcLabels={false} - activeOuterRadiusOffset={5} - tooltip={() => <>} - margin={{ - top: 0, - right: 5, - bottom: 0, - left: 5, - }} - defs={STATE_GROUP_GRAPH_GRADIENTS} - fill={Object.values(STATE_GROUPS).map((p) => ({ - match: { - id: p.key, - }, - id: `gradient${p.label}`, - }))} - onClick={(datum, e) => { - e.preventDefault(); - e.stopPropagation(); - router.push(`/${workspaceSlug}/workspace-views/assigned/?state_group=${datum.id}`); - }} - onMouseEnter={(datum) => setActiveStateGroup(datum.id as TStateGroups)} - onMouseLeave={() => setActiveStateGroup(defaultStateGroup)} - layers={["arcs", CenteredMetric]} - /> -
-
- {chartData.map((item) => ( -
-
-
- {item.label} -
- {item.value.toFixed(0)}% -
- ))} -
-
-
- ) : ( -
- -
- )} - - ); -}); diff --git a/web/core/components/graphs/index.ts b/web/core/components/graphs/index.ts deleted file mode 100644 index 305c3944e..000000000 --- a/web/core/components/graphs/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./issues-by-priority"; diff --git a/web/core/components/graphs/issues-by-priority.tsx b/web/core/components/graphs/issues-by-priority.tsx deleted file mode 100644 index faed3709e..000000000 --- a/web/core/components/graphs/issues-by-priority.tsx +++ /dev/null @@ -1,169 +0,0 @@ -import { ComputedDatum } from "@nivo/bar"; -import { Theme, linearGradientDef } from "@nivo/core"; -import { ISSUE_PRIORITIES } from "@plane/constants"; -// components -import { TIssuePriorities } from "@plane/types"; -import { BarGraph } from "@/components/ui"; -// helpers -import { capitalizeFirstLetter } from "@/helpers/string.helper"; - -// gradients for work items by priority widget graph bars -export const PRIORITY_GRAPH_GRADIENTS = [ - linearGradientDef( - "gradient_urgent", - [ - { offset: 0, color: "#A90408" }, - { offset: 100, color: "#DF4D51" }, - ], - { - x1: 1, - y1: 0, - x2: 0, - y2: 0, - } - ), - linearGradientDef( - "gradient_high", - [ - { offset: 0, color: "#FE6B00" }, - { offset: 100, color: "#FFAC88" }, - ], - { - x1: 1, - y1: 0, - x2: 0, - y2: 0, - } - ), - linearGradientDef( - "gradient_medium", - [ - { offset: 0, color: "#F5AC00" }, - { offset: 100, color: "#FFD675" }, - ], - { - x1: 1, - y1: 0, - x2: 0, - y2: 0, - } - ), - linearGradientDef( - "gradient_low", - [ - { offset: 0, color: "#1B46DE" }, - { offset: 100, color: "#4F9BF4" }, - ], - { - x1: 1, - y1: 0, - x2: 0, - y2: 0, - } - ), - linearGradientDef( - "gradient_none", - [ - { offset: 0, color: "#A0A1A9" }, - { offset: 100, color: "#B9BBC6" }, - ], - { - x1: 1, - y1: 0, - x2: 0, - y2: 0, - } - ), -]; - -type Props = { - borderRadius?: number; - data: { - priority: TIssuePriorities; - priority_count: number; - }[]; - height?: number; - onBarClick?: ( - datum: ComputedDatum & { - color: string; - } - ) => void; - padding?: number; - theme?: Theme; -}; - -const PRIORITY_TEXT_COLORS = { - urgent: "#CE2C31", - high: "#AB4800", - medium: "#AB6400", - low: "#1F2D5C", - none: "#60646C", -}; - -export const IssuesByPriorityGraph: React.FC = (props) => { - const { borderRadius = 8, data, height = 300, onBarClick, padding = 0.05, theme } = props; - - const chartData = data.map((priority) => ({ - priority: capitalizeFirstLetter(priority.priority), - value: priority.priority_count, - })); - - return ( - p.priority_count)} - axisBottom={{ - tickPadding: 8, - tickSize: 0, - }} - tooltip={(datum) => ( -
- - {datum.data.priority}: - {datum.value} -
- )} - colors={({ data }) => `url(#gradient${data.priority})`} - defs={PRIORITY_GRAPH_GRADIENTS} - fill={ISSUE_PRIORITIES.map((p) => ({ - match: { - id: p.key, - }, - id: `gradient_${p.key}`, - }))} - onClick={(datum) => { - if (onBarClick) onBarClick(datum); - }} - theme={{ - axis: { - domain: { - line: { - stroke: "transparent", - }, - }, - ticks: { - text: { - fontSize: 13, - }, - }, - }, - grid: { - line: { - stroke: "transparent", - }, - }, - ...theme, - }} - /> - ); -}; diff --git a/web/core/components/issues/filters.tsx b/web/core/components/issues/filters.tsx index a50afe50e..9ef8706ea 100644 --- a/web/core/components/issues/filters.tsx +++ b/web/core/components/issues/filters.tsx @@ -17,8 +17,7 @@ import { isIssueFilterActive } from "@/helpers/filter.helper"; import { useLabel, useProjectState, useMember, useIssues } from "@/hooks/store"; // plane web types import { TProject } from "@/plane-web/types"; -import { ProjectAnalyticsModal } from "../analytics"; -import { WorkItemsModal } from "../analytics-v2/work-items/modal"; +import { WorkItemsModal } from "../analytics/work-items/modal"; type Props = { currentProjectDetails: TProject | undefined; @@ -102,6 +101,7 @@ const HeaderFilters = observer((props: Props) => { isOpen={analyticsModal} onClose={() => setAnalyticsModal(false)} projectDetails={currentProjectDetails ?? undefined} + isEpic={storeType === EIssuesStoreType.EPIC} /> = observer((p {/* progress burndown chart */}
-
-
- - {t("ideal")} -
-
- - {t("current")} -
-
{moduleStartDate && moduleEndDate && completionChartDistributionData && ( {plotType === "points" ? ( ) : ( diff --git a/web/core/components/page-views/index.ts b/web/core/components/page-views/index.ts deleted file mode 100644 index 0491e8caf..000000000 --- a/web/core/components/page-views/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./workspace-dashboard"; diff --git a/web/core/components/page-views/workspace-dashboard.tsx b/web/core/components/page-views/workspace-dashboard.tsx deleted file mode 100644 index 19d3590b0..000000000 --- a/web/core/components/page-views/workspace-dashboard.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import { useEffect } from "react"; -import { observer } from "mobx-react"; -import { useParams } from "next/navigation"; -// plane imports -import { EUserPermissionsLevel, PRODUCT_TOUR_COMPLETED, EUserPermissions } from "@plane/constants"; -import { useTranslation } from "@plane/i18n"; -import { ContentWrapper } from "@plane/ui"; -// components -import { DashboardWidgets } from "@/components/dashboard"; -import { ComicBoxButton, DetailedEmptyState } from "@/components/empty-state"; -import { IssuePeekOverview } from "@/components/issues"; -import { TourRoot } from "@/components/onboarding"; -import { UserGreetingsView } from "@/components/user"; -// constants -// helpers -import { cn } from "@/helpers/common.helper"; -// hooks -import { - useCommandPalette, - useUserProfile, - useEventTracker, - useDashboard, - useProject, - useUser, - useUserPermissions, -} from "@/hooks/store"; -import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; -import useSize from "@/hooks/use-window-size"; - -export const WorkspaceDashboardView = observer(() => { - // plane hooks - const { t } = useTranslation(); - // store hooks - const { captureEvent, setTrackElement } = useEventTracker(); - const { toggleCreateProjectModal } = useCommandPalette(); - const { workspaceSlug } = useParams(); - const { data: currentUser } = useUser(); - const { data: currentUserProfile, updateTourCompleted } = useUserProfile(); - const { homeDashboardId, fetchHomeDashboardWidgets } = useDashboard(); - const { joinedProjectIds, loader } = useProject(); - const { allowPermissions } = useUserPermissions(); - - // helper hooks - const [windowWidth] = useSize(); - const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/onboarding/dashboard" }); - - const handleTourCompleted = () => { - updateTourCompleted() - .then(() => { - captureEvent(PRODUCT_TOUR_COMPLETED, { - user_id: currentUser?.id, - state: "SUCCESS", - }); - }) - .catch((error) => { - console.error(error); - }); - }; - - // fetch home dashboard widgets on workspace change - useEffect(() => { - if (!workspaceSlug) return; - - fetchHomeDashboardWidgets(workspaceSlug?.toString()); - }, [fetchHomeDashboardWidgets, workspaceSlug]); - - const canPerformEmptyStateActions = allowPermissions( - [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - EUserPermissionsLevel.WORKSPACE - ); - - // TODO: refactor loader implementation - return ( - <> - {currentUserProfile && !currentUserProfile.is_tour_completed && ( -
- -
- )} - {homeDashboardId && joinedProjectIds && ( - <> - {joinedProjectIds.length > 0 || loader === "init-loader" ? ( - <> - - = 768, - })} - > - {currentUser && } - - - - - ) : ( - { - setTrackElement("Dashboard empty state"); - toggleCreateProjectModal(true); - }} - disabled={!canPerformEmptyStateActions} - /> - } - /> - )} - - )} - - ); -}); diff --git a/web/core/components/profile/overview/priority-distribution/index.ts b/web/core/components/profile/overview/priority-distribution/index.ts deleted file mode 100644 index 64d81eb12..000000000 --- a/web/core/components/profile/overview/priority-distribution/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./priority-distribution"; diff --git a/web/core/components/profile/overview/priority-distribution/main-content.tsx b/web/core/components/profile/overview/priority-distribution/main-content.tsx deleted file mode 100644 index 5a4f64bce..000000000 --- a/web/core/components/profile/overview/priority-distribution/main-content.tsx +++ /dev/null @@ -1,31 +0,0 @@ -// components -import { IUserPriorityDistribution } from "@plane/types"; -import { IssuesByPriorityGraph } from "@/components/graphs"; -import { ProfileEmptyState } from "@/components/ui"; -// assets -import emptyBarGraph from "@/public/empty-state/empty_bar_graph.svg"; -// types - -type Props = { - priorityDistribution: IUserPriorityDistribution[]; -}; - -export const PriorityDistributionContent: React.FC = (props) => { - const { priorityDistribution } = props; - - return ( -
- {priorityDistribution.length > 0 ? ( - - ) : ( -
- -
- )} -
- ); -}; diff --git a/web/core/components/profile/overview/priority-distribution/priority-distribution.tsx b/web/core/components/profile/overview/priority-distribution/priority-distribution.tsx deleted file mode 100644 index a7bca192b..000000000 --- a/web/core/components/profile/overview/priority-distribution/priority-distribution.tsx +++ /dev/null @@ -1,35 +0,0 @@ -"use client"; - -// components -// ui -import { IUserPriorityDistribution } from "@plane/types"; -import { Loader } from "@plane/ui"; -// types -import { PriorityDistributionContent } from "./main-content"; - -type Props = { - priorityDistribution: IUserPriorityDistribution[] | undefined; -}; - -export const ProfilePriorityDistribution: React.FC = (props) => { - const { priorityDistribution } = props; - - return ( -
-

Work items by priority

- {priorityDistribution ? ( - - ) : ( -
- - - - - - - -
- )} -
- ); -}; diff --git a/web/core/components/ui/graphs/bar-graph.tsx b/web/core/components/ui/graphs/bar-graph.tsx deleted file mode 100644 index 1a724322e..000000000 --- a/web/core/components/ui/graphs/bar-graph.tsx +++ /dev/null @@ -1,49 +0,0 @@ -// nivo -import { ResponsiveBar, BarSvgProps } from "@nivo/bar"; -import { CHARTS_THEME, CHART_DEFAULT_MARGIN } from "@plane/constants"; -// helpers -import { generateYAxisTickValues } from "@/helpers/graph.helper"; -// types -import { TGraph } from "./types"; -// constants - -type Props = { - indexBy: string; - keys: string[]; - customYAxisTickValues?: number[]; -}; - -export const BarGraph: React.FC, "height" | "width">> = ({ - indexBy, - keys, - customYAxisTickValues, - height = "400px", - width = "100%", - margin, - theme, - ...rest -}) => ( -
- 7) ? 0.8 : 0.9} - axisLeft={{ - tickSize: 0, - tickPadding: 10, - tickValues: customYAxisTickValues ? generateYAxisTickValues(customYAxisTickValues) : undefined, - }} - axisBottom={{ - tickSize: 0, - tickPadding: 10, - tickRotation: rest.data.length > 7 ? -45 : 0, - }} - labelTextColor={{ from: "color", modifiers: [["darker", 1.6]] }} - theme={{ ...CHARTS_THEME, ...(theme ?? {}) }} - animate - enableLabel={rest.enableLabel ?? false} - {...rest} - /> -
-); diff --git a/web/core/components/ui/graphs/calendar-graph.tsx b/web/core/components/ui/graphs/calendar-graph.tsx deleted file mode 100644 index 5a63dd640..000000000 --- a/web/core/components/ui/graphs/calendar-graph.tsx +++ /dev/null @@ -1,34 +0,0 @@ -// nivo -import { ResponsiveCalendar, CalendarSvgProps } from "@nivo/calendar"; -// types -import { CHARTS_THEME, CHART_DEFAULT_MARGIN } from "@plane/constants"; -import { TGraph } from "./types"; -// constants - -export const CalendarGraph: React.FC> = ({ - height = "400px", - width = "100%", - margin, - theme, - ...rest -}) => ( -
- -
-); diff --git a/web/core/components/ui/graphs/index.ts b/web/core/components/ui/graphs/index.ts deleted file mode 100644 index 984bb642c..000000000 --- a/web/core/components/ui/graphs/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from "./bar-graph"; -export * from "./calendar-graph"; -export * from "./line-graph"; -export * from "./pie-graph"; -export * from "./scatter-plot-graph"; diff --git a/web/core/components/ui/graphs/line-graph.tsx b/web/core/components/ui/graphs/line-graph.tsx deleted file mode 100644 index e04d7ea6c..000000000 --- a/web/core/components/ui/graphs/line-graph.tsx +++ /dev/null @@ -1,35 +0,0 @@ -// nivo -import { ResponsiveLine, LineSvgProps } from "@nivo/line"; -// helpers -import { CHARTS_THEME, CHART_DEFAULT_MARGIN } from "@plane/constants"; -import { generateYAxisTickValues } from "@/helpers/graph.helper"; -// types -import { TGraph } from "./types"; -// constants - -type Props = { - customYAxisTickValues?: number[]; -}; - -export const LineGraph: React.FC = ({ - customYAxisTickValues, - height = "400px", - width = "100%", - margin, - theme, - ...rest -}) => ( -
- -
-); diff --git a/web/core/components/ui/graphs/pie-graph.tsx b/web/core/components/ui/graphs/pie-graph.tsx deleted file mode 100644 index 645859741..000000000 --- a/web/core/components/ui/graphs/pie-graph.tsx +++ /dev/null @@ -1,23 +0,0 @@ -// nivo -import { PieSvgProps, ResponsivePie } from "@nivo/pie"; -// types -import { CHARTS_THEME, CHART_DEFAULT_MARGIN } from "@plane/constants"; -import { TGraph } from "./types"; -// constants - -export const PieGraph: React.FC, "height" | "width">> = ({ - height = "400px", - width = "100%", - margin, - theme, - ...rest -}) => ( -
- -
-); diff --git a/web/core/components/ui/graphs/scatter-plot-graph.tsx b/web/core/components/ui/graphs/scatter-plot-graph.tsx deleted file mode 100644 index e4a7d7fcc..000000000 --- a/web/core/components/ui/graphs/scatter-plot-graph.tsx +++ /dev/null @@ -1,23 +0,0 @@ -// nivo -import { ResponsiveScatterPlot, ScatterPlotSvgProps } from "@nivo/scatterplot"; -// types -import { CHARTS_THEME, CHART_DEFAULT_MARGIN } from "@plane/constants"; -import { TGraph } from "./types"; -// constants - -export const ScatterPlotGraph: React.FC, "height" | "width">> = ({ - height = "400px", - width = "100%", - margin, - theme, - ...rest -}) => ( -
- -
-); diff --git a/web/core/components/ui/graphs/types.d.ts b/web/core/components/ui/graphs/types.d.ts deleted file mode 100644 index c8646405c..000000000 --- a/web/core/components/ui/graphs/types.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Theme, Margin } from "@nivo/core"; - -export type TGraph = { - height?: string; - width?: string; - margin?: Partial; - theme?: Theme; -}; diff --git a/web/core/components/ui/index.ts b/web/core/components/ui/index.ts index e441b896f..35367be7e 100644 --- a/web/core/components/ui/index.ts +++ b/web/core/components/ui/index.ts @@ -1,4 +1,3 @@ -export * from "./graphs"; export * from "./empty-space"; export * from "./labels-list"; export * from "./markdown-to-component"; diff --git a/web/core/constants/fetch-keys.ts b/web/core/constants/fetch-keys.ts index ec5de760f..d5bbf3ed6 100644 --- a/web/core/constants/fetch-keys.ts +++ b/web/core/constants/fetch-keys.ts @@ -1,4 +1,4 @@ -import { IAnalyticsParams, IJiraMetadata } from "@plane/types"; +import { IJiraMetadata } from "@plane/types"; const paramsToKey = (params: any) => { const { @@ -237,14 +237,6 @@ export const MY_PAGES_LIST = (pageId: string) => `MY_PAGE_LIST_${pageId}`; export const ESTIMATES_LIST = (projectId: string) => `ESTIMATES_LIST_${projectId.toUpperCase()}`; export const ESTIMATE_DETAILS = (estimateId: string) => `ESTIMATE_DETAILS_${estimateId.toUpperCase()}`; -// analytics -export const ANALYTICS = (workspaceSlug: string, params: IAnalyticsParams) => - `ANALYTICS${workspaceSlug.toUpperCase()}_${params.x_axis}_${params.y_axis}_${ - params.segment - }_${params.project?.toString()}`; -export const DEFAULT_ANALYTICS = (workspaceSlug: string, params?: Partial) => - `DEFAULT_ANALYTICS_${workspaceSlug.toUpperCase()}_${params?.project?.toString()}_${params?.cycle}_${params?.module}`; - // profile export const USER_PROFILE_DATA = (workspaceSlug: string, userId: string) => `USER_PROFILE_ACTIVITY_${workspaceSlug.toUpperCase()}_${userId.toUpperCase()}`; diff --git a/web/core/hooks/store/index.ts b/web/core/hooks/store/index.ts index 6a1b91e5d..dfc104542 100644 --- a/web/core/hooks/store/index.ts +++ b/web/core/hooks/store/index.ts @@ -31,4 +31,4 @@ export * from "./use-workspace"; export * from "./user"; export * from "./use-transient"; export * from "./workspace-draft"; -export * from "./use-analytics-v2"; +export * from "./use-analytics"; diff --git a/web/core/hooks/store/use-analytics-v2.ts b/web/core/hooks/store/use-analytics-v2.ts deleted file mode 100644 index c8c13ba61..000000000 --- a/web/core/hooks/store/use-analytics-v2.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { useContext } from "react"; -// mobx store -import { StoreContext } from "@/lib/store-context"; -// types -import { IAnalyticsStoreV2 } from "@/store/analytics-v2.store"; - -export const useAnalyticsV2 = (): IAnalyticsStoreV2 => { - const context = useContext(StoreContext); - if (context === undefined) throw new Error("useAnalyticsV2 must be used within StoreProvider"); - return context.analyticsV2; -}; diff --git a/web/core/hooks/store/use-analytics.ts b/web/core/hooks/store/use-analytics.ts new file mode 100644 index 000000000..a07af60ed --- /dev/null +++ b/web/core/hooks/store/use-analytics.ts @@ -0,0 +1,11 @@ +import { useContext } from "react"; +// mobx store +import { StoreContext } from "@/lib/store-context"; +// types +import { IAnalyticsStore } from "@/plane-web/store/analytics.store"; + +export const useAnalytics = (): IAnalyticsStore => { + const context = useContext(StoreContext); + if (context === undefined) throw new Error("useAnalytics must be used within StoreProvider"); + return context.analytics; +}; diff --git a/web/core/services/analytics-v2.service.ts b/web/core/services/analytics-v2.service.ts deleted file mode 100644 index 05e1b78b7..000000000 --- a/web/core/services/analytics-v2.service.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { API_BASE_URL } from "@plane/constants"; -import { IAnalyticsResponseV2, TAnalyticsTabsV2Base, TAnalyticsGraphsV2Base } from "@plane/types"; -import { APIService } from "./api.service"; - -export class AnalyticsV2Service extends APIService { - constructor() { - super(API_BASE_URL); - } - - async getAdvanceAnalytics( - workspaceSlug: string, - tab: TAnalyticsTabsV2Base, - params?: Record, - isPeekView?: boolean - ): Promise { - return this.get( - this.processUrl("advance-analytics", workspaceSlug, tab, params, isPeekView), - { - params: { - tab, - ...params, - }, - } - ) - .then((res) => res?.data) - .catch((err) => { - throw err?.response?.data; - }); - } - - async getAdvanceAnalyticsStats( - workspaceSlug: string, - tab: Exclude, - params?: Record, - isPeekView?: boolean - ): Promise { - const processedUrl = this.processUrl>( - "advance-analytics-stats", - workspaceSlug, - tab, - params, - isPeekView - ); - return this.get(processedUrl, { - params: { - type: tab, - ...params, - }, - }) - .then((res) => res?.data) - .catch((err) => { - throw err?.response?.data; - }); - } - - async getAdvanceAnalyticsCharts( - workspaceSlug: string, - tab: TAnalyticsGraphsV2Base, - params?: Record, - isPeekView?: boolean - ): Promise { - const processedUrl = this.processUrl( - "advance-analytics-charts", - workspaceSlug, - tab, - params, - isPeekView - ); - return this.get(processedUrl, { - params: { - type: tab, - ...params, - }, - }) - .then((res) => res?.data) - .catch((err) => { - throw err?.response?.data; - }); - } - - processUrl( - endpoint: string, - workspaceSlug: string, - tab: T, - params?: Record, - isPeekView?: boolean - ) { - let processedUrl = `/api/workspaces/${workspaceSlug}`; - if (isPeekView && tab === "work-items") { - const projectId = params?.project_ids.split(",")[0]; - processedUrl += `/projects/${projectId}`; - } - return `${processedUrl}/${endpoint}`; - } -} diff --git a/web/core/services/analytics.service.ts b/web/core/services/analytics.service.ts index 991407d16..de8e489d3 100644 --- a/web/core/services/analytics.service.ts +++ b/web/core/services/analytics.service.ts @@ -1,63 +1,99 @@ -// services -import { - IAnalyticsParams, - IAnalyticsResponse, - IDefaultAnalyticsResponse, - IExportAnalyticsFormData, - ISaveAnalyticsFormData, -} from "@plane/types"; -import { API_BASE_URL } from "@/helpers/common.helper"; -import { APIService } from "@/services/api.service"; -// types -// helpers +import { API_BASE_URL } from "@plane/constants"; +import { IAnalyticsResponse, TAnalyticsTabsBase, TAnalyticsGraphsBase, TAnalyticsFilterParams } from "@plane/types"; +import { APIService } from "./api.service"; export class AnalyticsService extends APIService { constructor() { super(API_BASE_URL); } - async getAnalytics(workspaceSlug: string, params: IAnalyticsParams): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/analytics/`, { - params: { - ...params, - project: params?.project ? params.project.toString() : null, - }, - }) - .then((response) => response?.data) - .catch((error) => { - throw error?.response?.data; - }); - } - - async getDefaultAnalytics( + async getAdvanceAnalytics( workspaceSlug: string, - params?: Partial - ): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/default-analytics/`, { + tab: TAnalyticsTabsBase, + params?: TAnalyticsFilterParams, + isPeekView?: boolean + ): Promise { + return this.get(this.processUrl("advance-analytics", workspaceSlug, tab, params, isPeekView), { params: { + tab, ...params, - project: params?.project ? params.project.toString() : null, }, }) - .then((response) => response?.data) - .catch((error) => { - throw error?.response?.data; + .then((res) => res?.data) + .catch((err) => { + throw err?.response?.data; }); } - async saveAnalytics(workspaceSlug: string, data: ISaveAnalyticsFormData): Promise { - return this.post(`/api/workspaces/${workspaceSlug}/analytic-view/`, data) - .then((response) => response?.data) - .catch((error) => { - throw error?.response?.data; + async getAdvanceAnalyticsStats( + workspaceSlug: string, + tab: Exclude, + params?: TAnalyticsFilterParams, + isPeekView?: boolean + ): Promise { + const processedUrl = this.processUrl>( + "advance-analytics-stats", + workspaceSlug, + tab, + params, + isPeekView + ); + return this.get(processedUrl, { + params: { + type: tab, + ...params, + }, + }) + .then((res) => res?.data) + .catch((err) => { + throw err?.response?.data; }); } - async exportAnalytics(workspaceSlug: string, data: IExportAnalyticsFormData): Promise { - return this.post(`/api/workspaces/${workspaceSlug}/export-analytics/`, data) - .then((response) => response?.data) - .catch((error) => { - throw error?.response?.data; + async getAdvanceAnalyticsCharts( + workspaceSlug: string, + tab: TAnalyticsGraphsBase, + params?: TAnalyticsFilterParams, + isPeekView?: boolean + ): Promise { + const processedUrl = this.processUrl( + "advance-analytics-charts", + workspaceSlug, + tab, + params, + isPeekView + ); + return this.get(processedUrl, { + params: { + type: tab, + ...params, + }, + }) + .then((res) => res?.data) + .catch((err) => { + throw err?.response?.data; }); } + + processUrl( + endpoint: string, + workspaceSlug: string, + tab: TAnalyticsGraphsBase | TAnalyticsTabsBase, + params?: TAnalyticsFilterParams, + isPeekView?: boolean + ) { + let processedUrl = `/api/workspaces/${workspaceSlug}`; + if (isPeekView && (tab === "work-items" || tab === "custom-work-items")) { + const projectIds = params?.project_ids; + if (typeof projectIds !== "string" || !projectIds.trim()) { + throw new Error("project_ids parameter is required for peek view of work items"); + } + const projectId = projectIds.split(",")[0]; + if (!projectId) { + throw new Error("Invalid project_ids format - no project ID found"); + } + processedUrl += `/projects/${projectId}`; + } + return `${processedUrl}/${endpoint}`; + } } diff --git a/web/core/store/analytics-v2.store.ts b/web/core/store/analytics.store.ts similarity index 73% rename from web/core/store/analytics-v2.store.ts rename to web/core/store/analytics.store.ts index 97582577a..a78c49710 100644 --- a/web/core/store/analytics-v2.store.ts +++ b/web/core/store/analytics.store.ts @@ -1,19 +1,19 @@ import { action, computed, makeObservable, observable, runInAction } from "mobx"; -import { ANALYTICS_V2_DURATION_FILTER_OPTIONS } from "@plane/constants"; -import { TAnalyticsTabsV2Base } from "@plane/types"; +import { ANALYTICS_DURATION_FILTER_OPTIONS, EIssuesStoreType } from "@plane/constants"; +import { TAnalyticsTabsBase } from "@plane/types"; import { CoreRootStore } from "./root.store"; -type DurationType = (typeof ANALYTICS_V2_DURATION_FILTER_OPTIONS)[number]["value"]; +type DurationType = (typeof ANALYTICS_DURATION_FILTER_OPTIONS)[number]["value"]; -export interface IAnalyticsStoreV2 { +export interface IBaseAnalyticsStore { //observables - currentTab: TAnalyticsTabsV2Base; + currentTab: TAnalyticsTabsBase; selectedProjects: string[]; selectedDuration: DurationType; selectedCycle: string; selectedModule: string; isPeekView?: boolean; - + isEpic?: boolean; //computed selectedDurationLabel: DurationType | null; @@ -23,25 +23,28 @@ export interface IAnalyticsStoreV2 { updateSelectedCycle: (cycle: string) => void; updateSelectedModule: (module: string) => void; updateIsPeekView: (isPeekView: boolean) => void; + updateIsEpic: (isEpic: boolean) => void; } -export class AnalyticsStoreV2 implements IAnalyticsStoreV2 { +export abstract class BaseAnalyticsStore implements IBaseAnalyticsStore { //observables - currentTab: TAnalyticsTabsV2Base = "overview"; + currentTab: TAnalyticsTabsBase = "overview"; selectedProjects: string[] = []; selectedDuration: DurationType = "last_30_days"; selectedCycle: string = ""; selectedModule: string = ""; isPeekView: boolean = false; + isEpic: boolean = false; constructor() { makeObservable(this, { // observables currentTab: observable.ref, selectedDuration: observable.ref, - selectedProjects: observable.ref, + selectedProjects: observable, selectedCycle: observable.ref, selectedModule: observable.ref, isPeekView: observable.ref, + isEpic: observable.ref, // computed selectedDurationLabel: computed, // actions @@ -50,11 +53,12 @@ export class AnalyticsStoreV2 implements IAnalyticsStoreV2 { updateSelectedCycle: action, updateSelectedModule: action, updateIsPeekView: action, + updateIsEpic: action, }); } get selectedDurationLabel() { - return ANALYTICS_V2_DURATION_FILTER_OPTIONS.find((item) => item.value === this.selectedDuration)?.name ?? null; + return ANALYTICS_DURATION_FILTER_OPTIONS.find((item) => item.value === this.selectedDuration)?.name ?? null; } updateSelectedProjects = (projects: string[]) => { @@ -96,4 +100,10 @@ export class AnalyticsStoreV2 implements IAnalyticsStoreV2 { this.isPeekView = isPeekView; }); }; + + updateIsEpic = (isEpic: boolean) => { + runInAction(() => { + this.isEpic = isEpic; + }); + }; } diff --git a/web/core/store/root.store.ts b/web/core/store/root.store.ts index b3e93afc9..e210754cc 100644 --- a/web/core/store/root.store.ts +++ b/web/core/store/root.store.ts @@ -2,11 +2,11 @@ import { enableStaticRendering } from "mobx-react"; // plane imports import { FALLBACK_LANGUAGE, LANGUAGE_STORAGE_KEY } from "@plane/i18n"; // plane web store +import { AnalyticsStore, IAnalyticsStore } from "@/plane-web/store/analytics.store"; import { CommandPaletteStore, ICommandPaletteStore } from "@/plane-web/store/command-palette.store"; import { RootStore } from "@/plane-web/store/root.store"; import { IStateStore, StateStore } from "@/plane-web/store/state.store"; // stores -import { IAnalyticsStoreV2, AnalyticsStoreV2 } from "./analytics-v2.store"; import { CycleStore, ICycleStore } from "./cycle.store"; import { CycleFilterStore, ICycleFilterStore } from "./cycle_filter.store"; import { DashboardStore, IDashboardStore } from "./dashboard.store"; @@ -50,7 +50,7 @@ export class CoreRootStore { state: IStateStore; label: ILabelStore; dashboard: IDashboardStore; - analyticsV2: IAnalyticsStoreV2; + analytics: IAnalyticsStore; projectPages: IProjectPageStore; router: IRouterStore; commandPalette: ICommandPaletteStore; @@ -96,7 +96,7 @@ export class CoreRootStore { this.transient = new TransientStore(); this.stickyStore = new StickyStore(); this.editorAssetStore = new EditorAssetStore(); - this.analyticsV2 = new AnalyticsStoreV2(); + this.analytics = new AnalyticsStore(); } resetOnSignOut() { diff --git a/web/ee/store/analytics.store.ts b/web/ee/store/analytics.store.ts new file mode 100644 index 000000000..ef866f65a --- /dev/null +++ b/web/ee/store/analytics.store.ts @@ -0,0 +1,8 @@ +import { BaseAnalyticsStore, IBaseAnalyticsStore } from "@/store/analytics.store"; + +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export interface IAnalyticsStore extends IBaseAnalyticsStore { + //observables +} + +export class AnalyticsStore extends BaseAnalyticsStore {} diff --git a/web/helpers/analytics.helper.ts b/web/helpers/analytics.helper.ts deleted file mode 100644 index cee4b40f3..000000000 --- a/web/helpers/analytics.helper.ts +++ /dev/null @@ -1,152 +0,0 @@ -// nivo -import { BarDatum } from "@nivo/bar"; -// plane imports -import { ANALYTICS_DATE_KEYS, STATE_GROUPS } from "@plane/constants"; -import { IAnalyticsData, IAnalyticsParams, IAnalyticsResponse, TStateGroups } from "@plane/types"; -// constants -import { MONTHS_LIST } from "@/constants/calendar"; -// helpers -import { addSpaceIfCamelCase, capitalizeFirstLetter, generateRandomColor } from "@/helpers/string.helper"; - -export const convertResponseToBarGraphData = ( - response: IAnalyticsData | undefined, - params: IAnalyticsParams -): { data: BarDatum[]; xAxisKeys: string[] } => { - if (!response || !(typeof response === "object") || Object.keys(response).length === 0) - return { data: [], xAxisKeys: [] }; - - const data: BarDatum[] = []; - - let xAxisKeys: string[] = []; - const yAxisKey = params.y_axis === "issue_count" ? "count" : "estimate"; - - Object.keys(response).forEach((key) => { - const segments: { [key: string]: number } = {}; - - if (params.segment) { - response[key].map((item: any) => { - segments[item.segment ?? "None"] = item[yAxisKey] ?? 0; - - // store the segment in the xAxisKeys array - if (!xAxisKeys.includes(item.segment ?? "None")) xAxisKeys.push(item.segment ?? "None"); - }); - - data.push({ - name: ANALYTICS_DATE_KEYS.includes(params.x_axis) - ? renderMonthAndYear(key) - : params.x_axis === "priority" || params.x_axis === "state__group" - ? capitalizeFirstLetter(key) - : key, - ...segments, - }); - } else { - xAxisKeys = [yAxisKey]; - - const item = response[key][0]; - - data.push({ - name: ANALYTICS_DATE_KEYS.includes(params.x_axis) - ? renderMonthAndYear(item.dimension) - : params.x_axis === "priority" || params.x_axis === "state__group" - ? capitalizeFirstLetter(item.dimension ?? "None") - : (item.dimension ?? "None"), - [yAxisKey]: item[yAxisKey] ?? 0, - }); - } - }); - - return { data, xAxisKeys }; -}; - -export const generateBarColor = ( - value: string, - analytics: IAnalyticsResponse, - params: IAnalyticsParams, - type: "x_axis" | "segment" -): string => { - let color: string | undefined = generateRandomColor(value); - - if (!analytics) return color; - - if (params[type] === "state_id") - color = analytics?.extras.state_details.find((s) => s.state_id === value)?.state__color; - - if (params[type] === "labels__id") - color = analytics?.extras.label_details.find((l) => l.labels__id === value)?.labels__color ?? undefined; - - if (params[type] === "state__group") color = STATE_GROUPS[value.toLowerCase() as TStateGroups]?.color ?? undefined; - - if (params[type] === "priority") { - const priority = value.toLowerCase(); - - color = - priority === "urgent" - ? "#ef4444" - : priority === "high" - ? "#f97316" - : priority === "medium" - ? "#eab308" - : priority === "low" - ? "#22c55e" - : "#ced4da"; - } - - return color ?? generateRandomColor(value); -}; - -export const generateDisplayName = ( - value: string, - analytics: IAnalyticsResponse, - params: IAnalyticsParams, - type: "x_axis" | "segment" -): string => { - let displayName = addSpaceIfCamelCase(value); - - if (!analytics) return displayName; - - if (params[type] === "assignees__id") - displayName = - analytics?.extras.assignee_details.find((a) => a.assignees__id === value)?.assignees__display_name ?? - "No assignee"; - - if (params[type] === "issue_cycle__cycle_id") - displayName = - analytics?.extras.cycle_details.find((c) => c.issue_cycle__cycle_id === value)?.issue_cycle__cycle__name ?? - "None"; - - if (params[type] === "issue_module__module_id") - displayName = - analytics?.extras.module_details.find((m) => m.issue_module__module_id === value)?.issue_module__module__name ?? - "None"; - - if (params[type] === "labels__id") - displayName = analytics?.extras.label_details.find((l) => l.labels__id === value)?.labels__name ?? "None"; - - if (params[type] === "state_id") - displayName = analytics?.extras.state_details.find((s) => s.state_id === value)?.state__name ?? "None"; - - if (ANALYTICS_DATE_KEYS.includes(params.segment ?? "")) displayName = renderMonthAndYear(value); - - return displayName; -}; - -export const renderMonthAndYear = (date: string | number | null): string => { - if (!date || date === "") return ""; - - const monthNumber = parseInt(`${date}`.split("-")[1], 10); - const year = `${date}`.split("-")[0]; - - return (MONTHS_LIST[monthNumber]?.shortTitle || "None") + ` ${year ? year : ""}`; -}; - -export const MAX_CHART_LABEL_LENGTH = 15; -export const renderChartDynamicLabel = ( - label: string, - length: number = MAX_CHART_LABEL_LENGTH -): { label: string; length: number } => { - const currentLabel = label.substring(0, length); - return { - label: `${label.length > MAX_CHART_LABEL_LENGTH ? `${currentLabel.substring(0, MAX_CHART_LABEL_LENGTH - 3)}...` : currentLabel}`, - length: currentLabel.length, - }; -}; diff --git a/web/next.config.js b/web/next.config.js index b71958854..41ed049c5 100644 --- a/web/next.config.js +++ b/web/next.config.js @@ -24,12 +24,6 @@ const nextConfig = { "lucide-react", "date-fns", "@headlessui/react", - "@nivo/core", - "@nivo/bar", - "@nivo/line", - "@nivo/pie", - "@nivo/calendar", - "@nivo/scatterplot", "react-color", "react-day-picker", "react-dropzone", diff --git a/web/package.json b/web/package.json index 7eb12c8ac..6312f9f90 100644 --- a/web/package.json +++ b/web/package.json @@ -22,13 +22,6 @@ "@blueprintjs/popover2": "^1.13.3", "@headlessui/react": "^1.7.3", "@intercom/messenger-js-sdk": "^0.0.12", - "@nivo/bar": "^0.88.0", - "@nivo/calendar": "^0.88.0", - "@nivo/core": "^0.88.0", - "@nivo/legends": "^0.88.0", - "@nivo/line": "^0.88.0", - "@nivo/pie": "^0.88.0", - "@nivo/scatterplot": "^0.88.0", "@plane/constants": "*", "@plane/editor": "*", "@plane/hooks": "*", diff --git a/web/public/empty-state/analytics-v2/empty-chart-area-dark.webp b/web/public/empty-state/analytics/empty-chart-area-dark.webp similarity index 100% rename from web/public/empty-state/analytics-v2/empty-chart-area-dark.webp rename to web/public/empty-state/analytics/empty-chart-area-dark.webp diff --git a/web/public/empty-state/analytics-v2/empty-chart-area-light.webp b/web/public/empty-state/analytics/empty-chart-area-light.webp similarity index 100% rename from web/public/empty-state/analytics-v2/empty-chart-area-light.webp rename to web/public/empty-state/analytics/empty-chart-area-light.webp diff --git a/web/public/empty-state/analytics-v2/empty-chart-bar-dark.webp b/web/public/empty-state/analytics/empty-chart-bar-dark.webp similarity index 100% rename from web/public/empty-state/analytics-v2/empty-chart-bar-dark.webp rename to web/public/empty-state/analytics/empty-chart-bar-dark.webp diff --git a/web/public/empty-state/analytics-v2/empty-chart-bar-light.webp b/web/public/empty-state/analytics/empty-chart-bar-light.webp similarity index 100% rename from web/public/empty-state/analytics-v2/empty-chart-bar-light.webp rename to web/public/empty-state/analytics/empty-chart-bar-light.webp diff --git a/web/public/empty-state/analytics-v2/empty-chart-radar-dark.webp b/web/public/empty-state/analytics/empty-chart-radar-dark.webp similarity index 100% rename from web/public/empty-state/analytics-v2/empty-chart-radar-dark.webp rename to web/public/empty-state/analytics/empty-chart-radar-dark.webp diff --git a/web/public/empty-state/analytics-v2/empty-chart-radar-light.webp b/web/public/empty-state/analytics/empty-chart-radar-light.webp similarity index 100% rename from web/public/empty-state/analytics-v2/empty-chart-radar-light.webp rename to web/public/empty-state/analytics/empty-chart-radar-light.webp diff --git a/web/public/empty-state/analytics-v2/empty-grid-background-dark.webp b/web/public/empty-state/analytics/empty-grid-background-dark.webp similarity index 100% rename from web/public/empty-state/analytics-v2/empty-grid-background-dark.webp rename to web/public/empty-state/analytics/empty-grid-background-dark.webp diff --git a/web/public/empty-state/analytics-v2/empty-grid-background-light.webp b/web/public/empty-state/analytics/empty-grid-background-light.webp similarity index 100% rename from web/public/empty-state/analytics-v2/empty-grid-background-light.webp rename to web/public/empty-state/analytics/empty-grid-background-light.webp diff --git a/web/public/empty-state/analytics-v2/empty-table-dark.webp b/web/public/empty-state/analytics/empty-table-dark.webp similarity index 100% rename from web/public/empty-state/analytics-v2/empty-table-dark.webp rename to web/public/empty-state/analytics/empty-table-dark.webp diff --git a/web/public/empty-state/analytics-v2/empty-table-light.webp b/web/public/empty-state/analytics/empty-table-light.webp similarity index 100% rename from web/public/empty-state/analytics-v2/empty-table-light.webp rename to web/public/empty-state/analytics/empty-table-light.webp diff --git a/yarn.lock b/yarn.lock index ea131bb2b..29dd63ada 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1675,201 +1675,6 @@ resolved "https://registry.npmjs.org/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.3.tgz#323d72dd25103d0c4fbdce89dadf574a787b1f9b" integrity sha512-s88O1aVtXftvp5bCPB7WnmXc5IwOZZ7YPuwNPt+GtOOXpPvad1LfbmjYv+qII7zP6RU2QGnqve27dnLycEnyEQ== -"@nivo/annotations@0.88.0": - version "0.88.0" - resolved "https://registry.npmjs.org/@nivo/annotations/-/annotations-0.88.0.tgz#96830d735331dea2b60e66ce3971ef43b45cca8c" - integrity sha512-NXE+1oIUn+EGWMQpnpeRMLgi2wyuzhGDoJQY4OUHissCUiNotid2oNQ/PXJwN0toiu+/j9SyhzI32xr70OPi7Q== - dependencies: - "@nivo/colors" "0.88.0" - "@nivo/core" "0.88.0" - "@react-spring/web" "9.4.5 || ^9.7.2" - lodash "^4.17.21" - -"@nivo/arcs@0.88.0": - version "0.88.0" - resolved "https://registry.npmjs.org/@nivo/arcs/-/arcs-0.88.0.tgz#0004aa612fd12eee5fd415ed257812b722bf5a10" - integrity sha512-q7MHxT71s/KKlDDtSJS4L9+/JIa5HPZZrDr3ZFECLnvp0TC1qzyFMtVevN2CsXopSTj8poN4uFXPWxYVXOq8vg== - dependencies: - "@nivo/colors" "0.88.0" - "@nivo/core" "0.88.0" - "@react-spring/web" "9.4.5 || ^9.7.2" - "@types/d3-shape" "^3.1.6" - d3-shape "^3.2.0" - -"@nivo/axes@0.88.0": - version "0.88.0" - resolved "https://registry.npmjs.org/@nivo/axes/-/axes-0.88.0.tgz#b773efb217fc0faedbb5f8bc458f4b78820e2761" - integrity sha512-jF7aIxzTNayV5cI1J/b9Q1FfpMBxTXGk3OwSigXMSfYWlliskDn2u0qGRLiYhuXFdQAWIp4oXsO1GcAQ0eRVdg== - dependencies: - "@nivo/core" "0.88.0" - "@nivo/scales" "0.88.0" - "@react-spring/web" "9.4.5 || ^9.7.2" - "@types/d3-format" "^1.4.1" - "@types/d3-time-format" "^2.3.1" - d3-format "^1.4.4" - d3-time-format "^3.0.0" - -"@nivo/bar@^0.88.0": - version "0.88.0" - resolved "https://registry.npmjs.org/@nivo/bar/-/bar-0.88.0.tgz#646f12233c0dc9f2507823bfe30a2f1d76ddae71" - integrity sha512-wckwuHWeCikxGvvdRfGL+dVFsUD9uHk1r9s7bWUfOD+p8BWhxtYqfXpHolEfgGg3UyPaHtpGA7P4zgE5vgo7gQ== - dependencies: - "@nivo/annotations" "0.88.0" - "@nivo/axes" "0.88.0" - "@nivo/colors" "0.88.0" - "@nivo/core" "0.88.0" - "@nivo/legends" "0.88.0" - "@nivo/scales" "0.88.0" - "@nivo/tooltip" "0.88.0" - "@react-spring/web" "9.4.5 || ^9.7.2" - "@types/d3-scale" "^4.0.8" - "@types/d3-shape" "^3.1.6" - d3-scale "^4.0.2" - d3-shape "^3.2.0" - lodash "^4.17.21" - -"@nivo/calendar@^0.88.0": - version "0.88.0" - resolved "https://registry.npmjs.org/@nivo/calendar/-/calendar-0.88.0.tgz#f09e8ab5332d8f9b18de87e95ca6a48013ac0606" - integrity sha512-sTpoaN5bNRwywRIVKAv7oo+/ZZjX0cjBcpbyFQZqXnEmFX8tEO55Rn/469zWDG776Gk7wHcuwmQfEIqwWM9PfQ== - dependencies: - "@nivo/core" "0.88.0" - "@nivo/legends" "0.88.0" - "@nivo/tooltip" "0.88.0" - "@types/d3-scale" "^4.0.8" - "@types/d3-time" "^1.0.10" - "@types/d3-time-format" "^3.0.0" - d3-scale "^4.0.2" - d3-time "^1.0.10" - d3-time-format "^3.0.0" - lodash "^4.17.21" - -"@nivo/colors@0.88.0": - version "0.88.0" - resolved "https://registry.npmjs.org/@nivo/colors/-/colors-0.88.0.tgz#2790ac0607381800270f2902e4d957e65e2cad70" - integrity sha512-IZ+leYIqAlo7dyLHmsQwujanfRgXyoQ5H7PU3RWLEn1PP0zxDKLgEjFEDADpDauuslh2Tx0L81GNkWR6QSP0Mw== - dependencies: - "@nivo/core" "0.88.0" - "@types/d3-color" "^3.0.0" - "@types/d3-scale" "^4.0.8" - "@types/d3-scale-chromatic" "^3.0.0" - "@types/prop-types" "^15.7.2" - d3-color "^3.1.0" - d3-scale "^4.0.2" - d3-scale-chromatic "^3.0.0" - lodash "^4.17.21" - prop-types "^15.7.2" - -"@nivo/core@0.88.0", "@nivo/core@^0.88.0": - version "0.88.0" - resolved "https://registry.npmjs.org/@nivo/core/-/core-0.88.0.tgz#ec79c7d63311473f15a463fd78274d9971a52848" - integrity sha512-XjUkA5MmwjLP38bdrJwn36Gj7T5SYMKD55LYQp/1nIJPdxqJ38dUfE4XyBDfIEgfP6yrHOihw3C63cUdnUBoiw== - dependencies: - "@nivo/tooltip" "0.88.0" - "@react-spring/web" "9.4.5 || ^9.7.2" - "@types/d3-shape" "^3.1.6" - d3-color "^3.1.0" - d3-format "^1.4.4" - d3-interpolate "^3.0.1" - d3-scale "^4.0.2" - d3-scale-chromatic "^3.0.0" - d3-shape "^3.2.0" - d3-time-format "^3.0.0" - lodash "^4.17.21" - prop-types "^15.7.2" - -"@nivo/legends@0.88.0", "@nivo/legends@^0.88.0": - version "0.88.0" - resolved "https://registry.npmjs.org/@nivo/legends/-/legends-0.88.0.tgz#a6007492048358a14bd061472a117ed34e7b0c81" - integrity sha512-d4DF9pHbD8LmGJlp/Gp1cF4e8y2wfQTcw3jVhbZj9zkb7ZWB7JfeF60VHRfbXNux9bjQ9U78/SssQqueVDPEmg== - dependencies: - "@nivo/colors" "0.88.0" - "@nivo/core" "0.88.0" - "@types/d3-scale" "^4.0.8" - d3-scale "^4.0.2" - -"@nivo/line@^0.88.0": - version "0.88.0" - resolved "https://registry.npmjs.org/@nivo/line/-/line-0.88.0.tgz#145b194f2c1bff2dd6071ab46fe114f9c4237811" - integrity sha512-hFTyZ3BdAZvq2HwdwMj2SJGUeodjEW+7DLtFMIIoVIxmjZlAs3z533HcJ9cJd3it928fDm8SF/rgHs0TztYf9Q== - dependencies: - "@nivo/annotations" "0.88.0" - "@nivo/axes" "0.88.0" - "@nivo/colors" "0.88.0" - "@nivo/core" "0.88.0" - "@nivo/legends" "0.88.0" - "@nivo/scales" "0.88.0" - "@nivo/tooltip" "0.88.0" - "@nivo/voronoi" "0.88.0" - "@react-spring/web" "9.4.5 || ^9.7.2" - d3-shape "^3.2.0" - -"@nivo/pie@^0.88.0": - version "0.88.0" - resolved "https://registry.npmjs.org/@nivo/pie/-/pie-0.88.0.tgz#628a608d077d9cfe850ffef1b16181482479e7c7" - integrity sha512-BE6dFWlGne1SnaEkFHNbg0sZBiwtcIqBFwmMRJ0F11SiKOzVeJyq3KiyY1I2ySSCx5VR1V8/MNBXzXFu3vJMAQ== - dependencies: - "@nivo/arcs" "0.88.0" - "@nivo/colors" "0.88.0" - "@nivo/core" "0.88.0" - "@nivo/legends" "0.88.0" - "@nivo/tooltip" "0.88.0" - "@types/d3-shape" "^3.1.6" - d3-shape "^3.2.0" - -"@nivo/scales@0.88.0": - version "0.88.0" - resolved "https://registry.npmjs.org/@nivo/scales/-/scales-0.88.0.tgz#c69110b5ab504debb80a5a01a7824780af241ffb" - integrity sha512-HbpxkQp6tHCltZ1yDGeqdLcaJl5ze54NPjurfGtx/Uq+H5IQoBd4Tln49bUar5CsFAMsXw8yF1HQvASr7I1SIA== - dependencies: - "@types/d3-scale" "^4.0.8" - "@types/d3-time" "^1.1.1" - "@types/d3-time-format" "^3.0.0" - d3-scale "^4.0.2" - d3-time "^1.0.11" - d3-time-format "^3.0.0" - lodash "^4.17.21" - -"@nivo/scatterplot@^0.88.0": - version "0.88.0" - resolved "https://registry.npmjs.org/@nivo/scatterplot/-/scatterplot-0.88.0.tgz#d6572986eee49e2da942c01be37f6fe189bf6506" - integrity sha512-kVUnBknVnX/+kAnOVl27wSrVrDAnYt7hyttss5jydBMzGidxK7vFBwRUFl5yrPGmEwpDSO8POvXjSF4M3XHzZA== - dependencies: - "@nivo/annotations" "0.88.0" - "@nivo/axes" "0.88.0" - "@nivo/colors" "0.88.0" - "@nivo/core" "0.88.0" - "@nivo/legends" "0.88.0" - "@nivo/scales" "0.88.0" - "@nivo/tooltip" "0.88.0" - "@nivo/voronoi" "0.88.0" - "@react-spring/web" "9.4.5 || ^9.7.2" - "@types/d3-scale" "^4.0.8" - "@types/d3-shape" "^3.1.6" - d3-scale "^4.0.2" - d3-shape "^3.2.0" - lodash "^4.17.21" - -"@nivo/tooltip@0.88.0": - version "0.88.0" - resolved "https://registry.npmjs.org/@nivo/tooltip/-/tooltip-0.88.0.tgz#b98348c0d617fea09c1bbddccc443c12ca697620" - integrity sha512-iEjVfQA8gumAzg/yUinjTwswygCkE5Iwuo8opwnrbpNIqMrleBV+EAKIgB0PrzepIoW8CFG/SJhoiRfbU8jhOw== - dependencies: - "@nivo/core" "0.88.0" - "@react-spring/web" "9.4.5 || ^9.7.2" - -"@nivo/voronoi@0.88.0": - version "0.88.0" - resolved "https://registry.npmjs.org/@nivo/voronoi/-/voronoi-0.88.0.tgz#029f984ccb6e72b415f398e1bd74c572960197ae" - integrity sha512-MyiNLvODthFoMjQ7Wjp693nogbTmVEx8Yn/7QkJhyPQbFyyA37TF/D1a/ox4h2OslXtP6K9QFN+42gB/zu7ixw== - dependencies: - "@nivo/core" "0.88.0" - "@nivo/tooltip" "0.88.0" - "@types/d3-delaunay" "^6.0.4" - "@types/d3-scale" "^4.0.8" - d3-delaunay "^6.0.4" - d3-scale "^4.0.2" - "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -2186,51 +1991,6 @@ resolved "https://registry.npmjs.org/@react-pdf/types/-/types-2.7.1.tgz#eb9f70be66b42c47f60c5afcc0af044ac48b98bf" integrity sha512-MyjR1u+6SclQ/Tx6NP3/yoYZw7reXgC4OHFOrdMh/zeZ+ezfdGyovB+jdmVQuMe7Fsh64v7PUkO5tnsXHyCFWQ== -"@react-spring/animated@~9.7.5": - version "9.7.5" - resolved "https://registry.npmjs.org/@react-spring/animated/-/animated-9.7.5.tgz#eb0373aaf99b879736b380c2829312dae3b05f28" - integrity sha512-Tqrwz7pIlsSDITzxoLS3n/v/YCUHQdOIKtOJf4yL6kYVSDTSmVK1LI1Q3M/uu2Sx4X3pIWF3xLUhlsA6SPNTNg== - dependencies: - "@react-spring/shared" "~9.7.5" - "@react-spring/types" "~9.7.5" - -"@react-spring/core@~9.7.5": - version "9.7.5" - resolved "https://registry.npmjs.org/@react-spring/core/-/core-9.7.5.tgz#72159079f52c1c12813d78b52d4f17c0bf6411f7" - integrity sha512-rmEqcxRcu7dWh7MnCcMXLvrf6/SDlSokLaLTxiPlAYi11nN3B5oiCUAblO72o+9z/87j2uzxa2Inm8UbLjXA+w== - dependencies: - "@react-spring/animated" "~9.7.5" - "@react-spring/shared" "~9.7.5" - "@react-spring/types" "~9.7.5" - -"@react-spring/rafz@~9.7.5": - version "9.7.5" - resolved "https://registry.npmjs.org/@react-spring/rafz/-/rafz-9.7.5.tgz#ee7959676e7b5d6a3813e8c17d5e50df98b95df9" - integrity sha512-5ZenDQMC48wjUzPAm1EtwQ5Ot3bLIAwwqP2w2owG5KoNdNHpEJV263nGhCeKKmuA3vG2zLLOdu3or6kuDjA6Aw== - -"@react-spring/shared@~9.7.5": - version "9.7.5" - resolved "https://registry.npmjs.org/@react-spring/shared/-/shared-9.7.5.tgz#6d513622df6ad750bbbd4dedb4ca0a653ec92073" - integrity sha512-wdtoJrhUeeyD/PP/zo+np2s1Z820Ohr/BbuVYv+3dVLW7WctoiN7std8rISoYoHpUXtbkpesSKuPIw/6U1w1Pw== - dependencies: - "@react-spring/rafz" "~9.7.5" - "@react-spring/types" "~9.7.5" - -"@react-spring/types@~9.7.5": - version "9.7.5" - resolved "https://registry.npmjs.org/@react-spring/types/-/types-9.7.5.tgz#e5dd180f3ed985b44fd2cd2f32aa9203752ef3e8" - integrity sha512-HVj7LrZ4ReHWBimBvu2SKND3cDVUPWKLqRTmWe/fNY6o1owGOX0cAHbdPDTMelgBlVbrTKrre6lFkhqGZErK/g== - -"@react-spring/web@9.4.5 || ^9.7.2": - version "9.7.5" - resolved "https://registry.npmjs.org/@react-spring/web/-/web-9.7.5.tgz#7d7782560b3a6fb9066b52824690da738605de80" - integrity sha512-lmvqGwpe+CSttsWNZVr+Dg62adtKhauGwLyGE/RRyZ8AAMLgb9x3NDMA5RMElXo+IMyTkPp7nxTB8ZQlmhb6JQ== - dependencies: - "@react-spring/animated" "~9.7.5" - "@react-spring/core" "~9.7.5" - "@react-spring/shared" "~9.7.5" - "@react-spring/types" "~9.7.5" - "@remirror/core-constants@3.0.0": version "3.0.0" resolved "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-3.0.0.tgz#96fdb89d25c62e7b6a5d08caf0ce5114370e3b8f" @@ -3210,26 +2970,16 @@ resolved "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz#1f6658e3d2006c4fceac53fde464166859f8b8c5" integrity sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg== -"@types/d3-color@*", "@types/d3-color@^3.0.0": +"@types/d3-color@*": version "3.1.3" resolved "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz#368c961a18de721da8200e80bf3943fb53136af2" integrity sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A== -"@types/d3-delaunay@^6.0.4": - version "6.0.4" - resolved "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz#185c1a80cc807fdda2a3fe960f7c11c4a27952e1" - integrity sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw== - "@types/d3-ease@^3.0.0": version "3.0.2" resolved "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz#e28db1bfbfa617076f7770dd1d9a48eaa3b6c51b" integrity sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA== -"@types/d3-format@^1.4.1": - version "1.4.5" - resolved "https://registry.npmjs.org/@types/d3-format/-/d3-format-1.4.5.tgz#6392303c2ca3c287c3a1a2046455cd0a0bd50bbe" - integrity sha512-mLxrC1MSWupOSncXN/HOlWUAAIffAEBaI4+PKy2uMPsKe4FNZlk7qrbTjmzJXITQQqBHivaks4Td18azgqnotA== - "@types/d3-interpolate@^3.0.1": version "3.0.4" resolved "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz#412b90e84870285f2ff8a846c6eb60344f12a41c" @@ -3242,45 +2992,25 @@ resolved "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz#f632b380c3aca1dba8e34aa049bcd6a4af23df8a" integrity sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg== -"@types/d3-scale-chromatic@^3.0.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz#dc6d4f9a98376f18ea50bad6c39537f1b5463c39" - integrity sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ== - -"@types/d3-scale@^4.0.2", "@types/d3-scale@^4.0.8": +"@types/d3-scale@^4.0.2": version "4.0.9" resolved "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz#57a2f707242e6fe1de81ad7bfcccaaf606179afb" integrity sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw== dependencies: "@types/d3-time" "*" -"@types/d3-shape@^3.1.0", "@types/d3-shape@^3.1.6": +"@types/d3-shape@^3.1.0": version "3.1.7" resolved "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz#2b7b423dc2dfe69c8c93596e673e37443348c555" integrity sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg== dependencies: "@types/d3-path" "*" -"@types/d3-time-format@^2.3.1": - version "2.3.4" - resolved "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-2.3.4.tgz#544af5184df8b3fc4d9b42b14058789acee2905e" - integrity sha512-xdDXbpVO74EvadI3UDxjxTdR6QIxm1FKzEA/+F8tL4GWWUg/hgvBqf6chql64U5A9ZUGWo7pEu4eNlyLwbKdhg== - -"@types/d3-time-format@^3.0.0": - version "3.0.4" - resolved "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-3.0.4.tgz#f972bdd7be1048184577cf235a44721a78c6bb4b" - integrity sha512-or9DiDnYI1h38J9hxKEsw513+KVuFbEVhl7qdxcaudoiqWWepapUen+2vAriFGexr6W5+P4l9+HJrB39GG+oRg== - "@types/d3-time@*", "@types/d3-time@^3.0.0": version "3.0.4" resolved "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz#8472feecd639691450dd8000eb33edd444e1323f" integrity sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g== -"@types/d3-time@^1.0.10", "@types/d3-time@^1.1.1": - version "1.1.4" - resolved "https://registry.npmjs.org/@types/d3-time/-/d3-time-1.1.4.tgz#20da4b75c537a940e7319b75717c67a2e499515a" - integrity sha512-JIvy2HjRInE+TXOmIGN5LCmeO0hkFZx5f9FZ7kiN+D+YTcc8pptsiLiuHsvwxwC7VVKmJ2ExHUgNlAiV7vQM9g== - "@types/d3-timer@^3.0.0": version "3.0.2" resolved "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz#70bbda77dc23aa727413e22e214afa3f0e852f70" @@ -3533,7 +3263,7 @@ resolved "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz#5950e50960793055845e956c427fc2b0d70c5239" integrity sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw== -"@types/prop-types@*", "@types/prop-types@^15.0.0", "@types/prop-types@^15.7.12", "@types/prop-types@^15.7.2": +"@types/prop-types@*", "@types/prop-types@^15.0.0", "@types/prop-types@^15.7.12": version "15.7.14" resolved "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz#1433419d73b2a7ebfc6918dcefd2ec0d5cd698f2" integrity sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ== @@ -5193,13 +4923,6 @@ csstype@^3.0.2, csstype@^3.1.3: resolved "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== -d3-array@2: - version "2.12.1" - resolved "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz#e20b41aafcdffdf5d50928004ececf815a465e81" - integrity sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ== - dependencies: - internmap "^1.0.0" - "d3-array@2 - 3", "d3-array@2.10.0 - 3", d3-array@^3.1.6: version "3.2.4" resolved "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz#15fec33b237f97ac5d7c986dc77da273a8ed0bb5" @@ -5207,18 +4930,11 @@ d3-array@2: dependencies: internmap "1 - 2" -"d3-color@1 - 3", d3-color@^3.1.0: +"d3-color@1 - 3": version "3.1.0" resolved "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz#395b2833dfac71507f12ac2f7af23bf819de24e2" integrity sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA== -d3-delaunay@^6.0.4: - version "6.0.4" - resolved "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz#98169038733a0a5babbeda55054f795bb9e4a58b" - integrity sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A== - dependencies: - delaunator "5" - d3-ease@^3.0.1: version "3.0.1" resolved "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz#9658ac38a2140d59d346160f1f6c30fda0bd12f4" @@ -5229,12 +4945,7 @@ d3-ease@^3.0.1: resolved "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz#9260e23a28ea5cb109e93b21a06e24e2ebd55641" integrity sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA== -d3-format@^1.4.4: - version "1.4.5" - resolved "https://registry.npmjs.org/d3-format/-/d3-format-1.4.5.tgz#374f2ba1320e3717eb74a9356c67daee17a7edb4" - integrity sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ== - -"d3-interpolate@1 - 3", "d3-interpolate@1.2.0 - 3", d3-interpolate@^3.0.1: +"d3-interpolate@1.2.0 - 3", d3-interpolate@^3.0.1: version "3.0.1" resolved "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d" integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g== @@ -5246,14 +4957,6 @@ d3-path@^3.1.0: resolved "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz#22df939032fb5a71ae8b1800d61ddb7851c42526" integrity sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ== -d3-scale-chromatic@^3.0.0: - version "3.1.0" - resolved "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz#34c39da298b23c20e02f1a4b239bd0f22e7f1314" - integrity sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ== - dependencies: - d3-color "1 - 3" - d3-interpolate "1 - 3" - d3-scale@^4.0.2: version "4.0.2" resolved "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz#82b38e8e8ff7080764f8dcec77bd4be393689396" @@ -5265,7 +4968,7 @@ d3-scale@^4.0.2: d3-time "2.1.1 - 3" d3-time-format "2 - 4" -d3-shape@^3.1.0, d3-shape@^3.2.0: +d3-shape@^3.1.0: version "3.2.0" resolved "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz#a1a839cbd9ba45f28674c69d7f855bcf91dfc6a5" integrity sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA== @@ -5279,20 +4982,6 @@ d3-shape@^3.1.0, d3-shape@^3.2.0: dependencies: d3-time "1 - 3" -d3-time-format@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/d3-time-format/-/d3-time-format-3.0.0.tgz#df8056c83659e01f20ac5da5fdeae7c08d5f1bb6" - integrity sha512-UXJh6EKsHBTjopVqZBhFysQcoXSv/5yLONZvkQ5Kk3qbwiUYkdX17Xa1PT6U1ZWXGGfB1ey5L8dKMlFq2DO0Ag== - dependencies: - d3-time "1 - 2" - -"d3-time@1 - 2": - version "2.1.1" - resolved "https://registry.npmjs.org/d3-time/-/d3-time-2.1.1.tgz#e9d8a8a88691f4548e68ca085e5ff956724a6682" - integrity sha512-/eIQe/eR4kCQwq7yxi7z4c6qEXf2IYGcjoWB5OOQy4Tq9Uv39/947qlDcN2TLkiTzQWzvnsuYPB9TrWaNfipKQ== - dependencies: - d3-array "2" - "d3-time@1 - 3", "d3-time@2.1.1 - 3", d3-time@^3.0.0: version "3.1.0" resolved "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz#9310db56e992e3c0175e1ef385e545e48a9bb5c7" @@ -5300,11 +4989,6 @@ d3-time-format@^3.0.0: dependencies: d3-array "2 - 3" -d3-time@^1.0.10, d3-time@^1.0.11: - version "1.1.0" - resolved "https://registry.npmjs.org/d3-time/-/d3-time-1.1.0.tgz#b1e19d307dae9c900b7e5b25ffc5dcc249a8a0f1" - integrity sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA== - d3-timer@^3.0.1: version "3.0.1" resolved "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0" @@ -5477,13 +5161,6 @@ define-properties@^1.1.3, define-properties@^1.2.1: has-property-descriptors "^1.0.0" object-keys "^1.1.1" -delaunator@5: - version "5.0.1" - resolved "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz#39032b08053923e924d6094fe2cde1a99cc51278" - integrity sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw== - dependencies: - robust-predicates "^3.0.2" - delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" @@ -7185,11 +6862,6 @@ internal-slot@^1.1.0: resolved "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz#6685f23755e43c524e251d29cbc97248e3061009" integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg== -internmap@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz#0017cc8a3b99605f0302f2b198d272e015e5df95" - integrity sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw== - intl-messageformat@^10.7.11: version "10.7.15" resolved "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.15.tgz#5cdc62139ef39ece1b083db32dae4d1c9fa5b627" @@ -9277,7 +8949,7 @@ process@^0.11.10: resolved "https://registry.npmjs.org/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== -prop-types@^15.0.0, prop-types@^15.5.10, prop-types@^15.5.8, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: +prop-types@^15.0.0, prop-types@^15.5.10, prop-types@^15.5.8, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -10077,11 +9749,6 @@ rimraf@^3.0.2: dependencies: glob "^7.1.3" -robust-predicates@^3.0.2: - version "3.0.2" - resolved "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz#d5b28528c4824d20fc48df1928d41d9efa1ad771" - integrity sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg== - rollup@^4.34.8: version "4.41.1" resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.41.1.tgz#46ddc1b33cf1b0baa99320d3b0b4973dc2253b6a" @@ -10572,7 +10239,16 @@ streamx@^2.15.0, streamx@^2.21.0: optionalDependencies: bare-events "^2.2.0" -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -10665,7 +10341,14 @@ string_decoder@^1.1.1, string_decoder@^1.3.0: dependencies: safe-buffer "~5.2.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -11883,7 +11566,16 @@ word-wrap@^1.2.5: resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version "7.0.0" + resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== From 6be3f0ea73b1b20c4273ed297d164ccb9dddbca6 Mon Sep 17 00:00:00 2001 From: Vamsi Krishna <46787868+vamsikrishnamathala@users.noreply.github.com> Date: Fri, 6 Jun 2025 13:21:00 +0530 Subject: [PATCH 135/201] [WEB-4208]chore: refactored work item quick actions (#7136) * chore: refactored work item quick actions * chore: update event handling for menu * chore: reverted unwanted changes * fix: update archive copy link * chore: handled undefined function implementation --- .../ui/src/dropdowns/context-menu/item.tsx | 259 ++++++++++-- .../ui/src/dropdowns/context-menu/root.tsx | 102 ++++- packages/ui/src/dropdowns/custom-menu.tsx | 311 ++++++++++++++- packages/ui/src/dropdowns/helper.tsx | 30 ++ .../copy-menu-helper.tsx | 22 ++ .../duplicate-modal.tsx | 11 + .../quick-action-dropdowns/index.ts | 2 + .../store/issue/issue-details/root.store.ts | 18 + web/core/components/dropdowns/project.tsx | 8 +- .../issues/issue-layouts/kanban/block.tsx | 21 + .../quick-action-dropdowns/all-issue.tsx | 187 +++++---- .../quick-action-dropdowns/archived-issue.tsx | 84 +--- .../quick-action-dropdowns/cycle-issue.tsx | 199 +++++----- .../quick-action-dropdowns/draft-issue.tsx | 89 ++--- .../quick-action-dropdowns/helper.tsx | 372 ++++++++++++++++++ .../quick-action-dropdowns/index.ts | 7 +- .../quick-action-dropdowns/module-issue.tsx | 197 ++++++---- .../quick-action-dropdowns/project-issue.tsx | 196 +++++---- web/core/store/issue/root.store.ts | 2 +- .../quick-action-dropdowns/index.ts | 1 + .../store/issue/issue-details/root.store.ts | 1 + 21 files changed, 1602 insertions(+), 517 deletions(-) create mode 100644 web/ce/components/issues/issue-layouts/quick-action-dropdowns/copy-menu-helper.tsx create mode 100644 web/ce/components/issues/issue-layouts/quick-action-dropdowns/duplicate-modal.tsx create mode 100644 web/ce/components/issues/issue-layouts/quick-action-dropdowns/index.ts create mode 100644 web/ce/store/issue/issue-details/root.store.ts create mode 100644 web/core/components/issues/issue-layouts/quick-action-dropdowns/helper.tsx create mode 100644 web/ee/components/issues/issue-layouts/quick-action-dropdowns/index.ts create mode 100644 web/ee/store/issue/issue-details/root.store.ts diff --git a/packages/ui/src/dropdowns/context-menu/item.tsx b/packages/ui/src/dropdowns/context-menu/item.tsx index 831243920..8e2050d9d 100644 --- a/packages/ui/src/dropdowns/context-menu/item.tsx +++ b/packages/ui/src/dropdowns/context-menu/item.tsx @@ -1,8 +1,10 @@ -import React from "react"; +import { ChevronRight } from "lucide-react"; +import React, { useState, useRef, useContext } from "react"; +import { usePopper } from "react-popper"; // helpers import { cn } from "../../../helpers"; // types -import { TContextMenuItem } from "./root"; +import { TContextMenuItem, ContextMenuContext, Portal } from "./root"; type ContextMenuItemProps = { handleActiveItem: () => void; @@ -14,45 +16,230 @@ type ContextMenuItemProps = { export const ContextMenuItem: React.FC = (props) => { const { handleActiveItem, handleClose, isActive, item } = props; + // Nested menu state + const [isNestedOpen, setIsNestedOpen] = useState(false); + const [referenceElement, setReferenceElement] = useState(null); + const [popperElement, setPopperElement] = useState(null); + const [activeNestedIndex, setActiveNestedIndex] = useState(0); + const nestedMenuRef = useRef(null); + + const contextMenuContext = useContext(ContextMenuContext); + const hasNestedItems = item.nestedMenuItems && item.nestedMenuItems.length > 0; + const renderedNestedItems = item.nestedMenuItems?.filter((nestedItem) => nestedItem.shouldRender !== false) || []; + + const { styles, attributes } = usePopper(referenceElement, popperElement, { + placement: "right-start", + strategy: "fixed", + modifiers: [ + { + name: "offset", + options: { + offset: [0, 4], + }, + }, + { + name: "flip", + options: { + fallbackPlacements: ["left-start", "right-end", "left-end", "top-start", "bottom-start"], + }, + }, + { + name: "preventOverflow", + options: { + padding: 8, + }, + }, + ], + }); + + const closeNestedMenu = React.useCallback(() => { + setIsNestedOpen(false); + setActiveNestedIndex(0); + }, []); + + // Register this nested menu with the main context + React.useEffect(() => { + if (contextMenuContext && hasNestedItems) { + return contextMenuContext.registerSubmenu(closeNestedMenu); + } + }, [contextMenuContext, hasNestedItems, closeNestedMenu]); + + const handleItemClick = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + + if (hasNestedItems) { + // Toggle nested menu + if (!isNestedOpen && contextMenuContext) { + contextMenuContext.closeAllSubmenus(); + } + setIsNestedOpen(!isNestedOpen); + } else { + // Execute action for regular items + item.action(); + if (item.closeOnClick !== false) handleClose(); + } + }; + + const handleMouseEnter = () => { + handleActiveItem(); + + if (hasNestedItems) { + // Close other submenus and open this one + if (contextMenuContext) { + contextMenuContext.closeAllSubmenus(); + } + setIsNestedOpen(true); + } + }; + + const handleNestedItemClick = (nestedItem: TContextMenuItem, e?: React.MouseEvent) => { + if (e) { + e.preventDefault(); + e.stopPropagation(); + } + + nestedItem.action(); + if (nestedItem.closeOnClick !== false) { + handleClose(); // Close the entire context menu + } + }; + + // Handle keyboard navigation for nested items + React.useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (!isNestedOpen || !hasNestedItems) return; + + if (e.key === "ArrowDown") { + e.preventDefault(); + setActiveNestedIndex((prev) => (prev + 1) % renderedNestedItems.length); + } + if (e.key === "ArrowUp") { + e.preventDefault(); + setActiveNestedIndex((prev) => (prev - 1 + renderedNestedItems.length) % renderedNestedItems.length); + } + if (e.key === "Enter") { + e.preventDefault(); + const nestedItem = renderedNestedItems[activeNestedIndex]; + if (!nestedItem.disabled) { + handleNestedItemClick(nestedItem); + } + } + if (e.key === "ArrowLeft") { + e.preventDefault(); + closeNestedMenu(); + } + }; + + if (isNestedOpen && nestedMenuRef.current) { + const menuElement = nestedMenuRef.current; + menuElement.addEventListener("keydown", handleKeyDown); + // Ensure the menu can receive keyboard events + menuElement.setAttribute("tabindex", "-1"); + menuElement.focus(); + return () => { + menuElement.removeEventListener("keydown", handleKeyDown); + }; + } + }, [isNestedOpen, activeNestedIndex, renderedNestedItems, hasNestedItems, closeNestedMenu]); + if (item.shouldRender === false) return null; return ( - + + {/* Nested Menu */} + {hasNestedItems && isNestedOpen && ( + +
+
+ {renderedNestedItems.map((nestedItem, index) => ( + + ))} +
- +
)} - + ); }; diff --git a/packages/ui/src/dropdowns/context-menu/root.tsx b/packages/ui/src/dropdowns/context-menu/root.tsx index 61554d7bd..480607dba 100644 --- a/packages/ui/src/dropdowns/context-menu/root.tsx +++ b/packages/ui/src/dropdowns/context-menu/root.tsx @@ -21,15 +21,46 @@ export type TContextMenuItem = { disabled?: boolean; className?: string; iconClassName?: string; + nestedMenuItems?: TContextMenuItem[]; }; +// Portal component for nested menus +interface PortalProps { + children: React.ReactNode; + container?: Element | null; +} + +export const Portal: React.FC = ({ children, container }) => { + const [mounted, setMounted] = React.useState(false); + + React.useEffect(() => { + setMounted(true); + return () => setMounted(false); + }, []); + + if (!mounted) { + return null; + } + + const targetContainer = container || document.body; + return ReactDOM.createPortal(children, targetContainer); +}; + +// Context for managing nested menus +export const ContextMenuContext = React.createContext<{ + closeAllSubmenus: () => void; + registerSubmenu: (closeSubmenu: () => void) => () => void; + portalContainer?: Element | null; +} | null>(null); + type ContextMenuProps = { parentRef: React.RefObject; items: TContextMenuItem[]; + portalContainer?: Element | null; }; const ContextMenuWithoutPortal: React.FC = (props) => { - const { parentRef, items } = props; + const { parentRef, items, portalContainer } = props; // states const [isOpen, setIsOpen] = useState(false); const [position, setPosition] = useState({ @@ -39,11 +70,24 @@ const ContextMenuWithoutPortal: React.FC = (props) => { const [activeItemIndex, setActiveItemIndex] = useState(0); // refs const contextMenuRef = useRef(null); + const submenuClosersRef = useRef void>>(new Set()); // derived values const renderedItems = items.filter((item) => item.shouldRender !== false); const { isMobile } = usePlatformOS(); + const closeAllSubmenus = React.useCallback(() => { + submenuClosersRef.current.forEach((closeSubmenu) => closeSubmenu()); + }, []); + + const registerSubmenu = React.useCallback((closeSubmenu: () => void) => { + submenuClosersRef.current.add(closeSubmenu); + return () => { + submenuClosersRef.current.delete(closeSubmenu); + }; + }, []); + const handleClose = () => { + closeAllSubmenus(); setIsOpen(false); setActiveItemIndex(0); }; @@ -121,13 +165,42 @@ const ContextMenuWithoutPortal: React.FC = (props) => { }; }, [activeItemIndex, isOpen, renderedItems, setIsOpen]); - // close on clicking outside - useOutsideClickDetector(contextMenuRef, handleClose); + // Custom handler for nested menu portal clicks + React.useEffect(() => { + const handleDocumentClick = (event: MouseEvent) => { + const target = event.target as HTMLElement; + + // Check if the click is on a nested menu element + const isNestedMenuClick = target.closest('[data-context-submenu="true"]'); + const isMainMenuClick = contextMenuRef.current?.contains(target); + + // Also check if the target itself has the data attribute + const isNestedMenuElement = target.hasAttribute("data-context-submenu"); + + // If it's a nested menu click, main menu click, or nested menu element, don't close + if (isNestedMenuClick || isMainMenuClick || isNestedMenuElement) { + return; + } + + // If menu is open and it's an outside click, close it + if (isOpen) { + handleClose(); + } + }; + + if (isOpen) { + // Use capture phase to ensure we handle the event before other handlers + document.addEventListener("mousedown", handleDocumentClick, true); + return () => { + document.removeEventListener("mousedown", handleDocumentClick, true); + }; + } + }, [isOpen, handleClose]); return (
= (props) => { top: position.y, left: position.x, }} + data-context-menu="true" > - {renderedItems.map((item, index) => ( - setActiveItemIndex(index)} - handleClose={handleClose} - isActive={index === activeItemIndex} - item={item} - /> - ))} + + {renderedItems.map((item, index) => ( + setActiveItemIndex(index)} + handleClose={handleClose} + isActive={index === activeItemIndex} + item={item} + /> + ))} +
); diff --git a/packages/ui/src/dropdowns/custom-menu.tsx b/packages/ui/src/dropdowns/custom-menu.tsx index 688f14897..d043ced70 100644 --- a/packages/ui/src/dropdowns/custom-menu.tsx +++ b/packages/ui/src/dropdowns/custom-menu.tsx @@ -1,5 +1,5 @@ import { Menu } from "@headlessui/react"; -import { ChevronDown, MoreHorizontal } from "lucide-react"; +import { ChevronDown, ChevronRight, MoreHorizontal } from "lucide-react"; import * as React from "react"; import ReactDOM from "react-dom"; import { usePopper } from "react-popper"; @@ -10,7 +10,46 @@ import { cn } from "../../helpers"; // hooks import { useDropdownKeyDown } from "../hooks/use-dropdown-key-down"; // types -import { ICustomMenuDropdownProps, ICustomMenuItemProps } from "./helper"; +import { + ICustomMenuDropdownProps, + ICustomMenuItemProps, + ICustomSubMenuProps, + ICustomSubMenuTriggerProps, + ICustomSubMenuContentProps, +} from "./helper"; + +interface PortalProps { + children: React.ReactNode; + container?: Element | null; + asChild?: boolean; +} + +const Portal: React.FC = ({ children, container, asChild = false }) => { + const [mounted, setMounted] = React.useState(false); + + React.useEffect(() => { + setMounted(true); + return () => setMounted(false); + }, []); + + if (!mounted) { + return null; + } + + const targetContainer = container || document.body; + + if (asChild) { + return ReactDOM.createPortal(children, targetContainer); + } + + return ReactDOM.createPortal(
{children}
, targetContainer); +}; + +// Context for main menu to communicate with submenus +const MenuContext = React.createContext<{ + closeAllSubmenus: () => void; + registerSubmenu: (closeSubmenu: () => void) => () => void; +} | null>(null); const CustomMenu = (props: ICustomMenuDropdownProps) => { const { @@ -45,19 +84,35 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => { const [isOpen, setIsOpen] = React.useState(false); // refs const dropdownRef = React.useRef(null); + const submenuClosersRef = React.useRef void>>(new Set()); const { styles, attributes } = usePopper(referenceElement, popperElement, { placement: placement ?? "auto", }); + const closeAllSubmenus = React.useCallback(() => { + submenuClosersRef.current.forEach((closeSubmenu) => closeSubmenu()); + }, []); + + const registerSubmenu = React.useCallback((closeSubmenu: () => void) => { + submenuClosersRef.current.add(closeSubmenu); + return () => { + submenuClosersRef.current.delete(closeSubmenu); + }; + }, []); + const openDropdown = () => { setIsOpen(true); if (referenceElement) referenceElement.focus(); }; - const closeDropdown = () => { - if (isOpen) onMenuClose?.(); + + const closeDropdown = React.useCallback(() => { + if (isOpen) { + closeAllSubmenus(); + onMenuClose?.(); + } setIsOpen(false); - }; + }, [isOpen, closeAllSubmenus, onMenuClose]); const selectActiveItem = () => { const activeItem: HTMLElement | undefined | null = dropdownRef.current?.querySelector( @@ -75,8 +130,12 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => { const handleMenuButtonClick = (e: React.MouseEvent) => { e.stopPropagation(); e.preventDefault(); - isOpen ? closeDropdown() : openDropdown(); - menuButtonOnClick?.(); + if (isOpen) { + closeDropdown(); + } else { + openDropdown(); + } + if (menuButtonOnClick) menuButtonOnClick(); }; const handleMouseEnter = () => { @@ -86,13 +145,43 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => { const handleMouseLeave = () => { if (openOnHover && isOpen) { setTimeout(() => { - closeDropdown(); - }, 500); + // Only close if menu is still open + if (isOpen) { + closeDropdown(); + } + }, 150); // Small delay to allow moving to submenu } }; useOutsideClickDetector(dropdownRef, closeDropdown, useCaptureForOutsideClick); + // Custom handler for submenu portal clicks + React.useEffect(() => { + const handleDocumentClick = (event: MouseEvent) => { + const target = event.target as HTMLElement; + const isSubmenuClick = target.closest('[data-prevent-outside-click="true"]'); + const isMainMenuClick = dropdownRef.current?.contains(target); + + // If it's a submenu click or main menu click, don't close + if (isSubmenuClick || isMainMenuClick) { + return; + } + + // If menu is open and it's an outside click, close it + if (isOpen) { + closeDropdown(); + } + }; + + if (isOpen) { + document.addEventListener("mousedown", handleDocumentClick, useCaptureForOutsideClick); + + return () => { + document.removeEventListener("mousedown", handleDocumentClick, useCaptureForOutsideClick); + }; + } + }, [isOpen, closeDropdown, useCaptureForOutsideClick]); + let menuItems = ( { style={styles.popper} {...attributes.popper} > - {children} + {children}
); @@ -136,6 +225,7 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => { onClick={handleOnClick} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} + data-main-menu="true" > {({ open }) => ( <> @@ -202,8 +292,161 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => { ); }; +// SubMenu context for closing submenu from nested items +const SubMenuContext = React.createContext<{ closeSubmenu: () => void } | null>(null); + +// Hook to use submenu context +const useSubMenu = () => React.useContext(SubMenuContext); + +// SubMenu implementation +const SubMenu: React.FC = (props) => { + const { + children, + trigger, + disabled = false, + className = "", + contentClassName = "", + placement = "right-start", + } = props; + + const [isOpen, setIsOpen] = React.useState(false); + const [referenceElement, setReferenceElement] = React.useState(null); + const [popperElement, setPopperElement] = React.useState(null); + const submenuRef = React.useRef(null); + + const menuContext = React.useContext(MenuContext); + + const { styles, attributes } = usePopper(referenceElement, popperElement, { + placement, + strategy: "fixed", // Use fixed positioning to escape overflow constraints + modifiers: [ + { + name: "offset", + options: { + offset: [0, 4], + }, + }, + { + name: "flip", + options: { + fallbackPlacements: ["left-start", "right-end", "left-end", "top-start", "bottom-start"], + }, + }, + { + name: "preventOverflow", + options: { + padding: 8, + }, + }, + ], + }); + + const closeSubmenu = React.useCallback(() => { + setIsOpen(false); + }, []); + + // Register this submenu with the main menu context + React.useEffect(() => { + if (menuContext) { + return menuContext.registerSubmenu(closeSubmenu); + } + }, [menuContext, closeSubmenu]); + + const toggleSubmenu = () => { + if (!disabled) { + // Close other submenus when opening this one + if (!isOpen && menuContext) { + menuContext.closeAllSubmenus(); + } + setIsOpen(!isOpen); + } + }; + + const handleClick = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + toggleSubmenu(); + }; + + // Close submenu when clicking on other menu items + React.useEffect(() => { + const handleMenuItemClick = (e: Event) => { + const target = e.target as HTMLElement; + // Check if the click is on a menu item that's not part of this submenu + if (target.closest('[role="menuitem"]') && !submenuRef.current?.contains(target)) { + closeSubmenu(); + } + }; + + document.addEventListener("click", handleMenuItemClick); + return () => { + document.removeEventListener("click", handleMenuItemClick); + }; + }, [closeSubmenu]); + + return ( +
+ + + {({ active }) => ( +
+ {trigger} + +
+ )} +
+
+ + {isOpen && ( + +
{ + // Notify parent menu that we're hovering over submenu + const mainMenuElement = document.querySelector('[data-main-menu="true"]'); + if (mainMenuElement) { + const mouseEnterEvent = new MouseEvent("mouseenter", { bubbles: true }); + mainMenuElement.dispatchEvent(mouseEnterEvent); + } + }} + onMouseLeave={() => { + // Notify parent menu that we're leaving submenu + const mainMenuElement = document.querySelector('[data-main-menu="true"]'); + if (mainMenuElement) { + const mouseLeaveEvent = new MouseEvent("mouseleave", { bubbles: true }); + mainMenuElement.dispatchEvent(mouseLeaveEvent); + } + }} + > + {children} +
+
+ )} +
+ ); +}; + const MenuItem: React.FC = (props) => { const { children, disabled = false, onClick, className } = props; + const submenuContext = useSubMenu(); return ( @@ -221,6 +464,8 @@ const MenuItem: React.FC = (props) => { onClick={(e) => { close(); onClick?.(e); + // Close submenu if this item is inside a submenu + submenuContext?.closeSubmenu(); }} disabled={disabled} > @@ -231,6 +476,52 @@ const MenuItem: React.FC = (props) => { ); }; +const SubMenuTrigger: React.FC = (props) => { + const { children, disabled = false, className } = props; + + return ( + + {({ active }) => ( +
+ {children} + +
+ )} +
+ ); +}; + +const SubMenuContent: React.FC = (props) => { + const { children, className } = props; + + return ( +
+ {children} +
+ ); +}; + +// Add all components as static properties for external use +CustomMenu.Portal = Portal; CustomMenu.MenuItem = MenuItem; +CustomMenu.SubMenu = SubMenu; +CustomMenu.SubMenuTrigger = SubMenuTrigger; +CustomMenu.SubMenuContent = SubMenuContent; export { CustomMenu }; diff --git a/packages/ui/src/dropdowns/helper.tsx b/packages/ui/src/dropdowns/helper.tsx index 1d40acef7..ad6cb4fd5 100644 --- a/packages/ui/src/dropdowns/helper.tsx +++ b/packages/ui/src/dropdowns/helper.tsx @@ -21,6 +21,12 @@ export interface IDropdownProps { useCaptureForOutsideClick?: boolean; } +export interface IPortalProps { + children: React.ReactNode; + container?: Element | null; + asChild?: boolean; +} + export interface ICustomMenuDropdownProps extends IDropdownProps { children: React.ReactNode; ellipsis?: boolean; @@ -75,3 +81,27 @@ export interface ICustomSelectItemProps { value: any; className?: string; } + +// Submenu interfaces +export interface ICustomSubMenuProps { + children: React.ReactNode; + trigger: React.ReactNode; + disabled?: boolean; + className?: string; + contentClassName?: string; + placement?: Placement; +} + +export interface ICustomSubMenuTriggerProps { + children: React.ReactNode; + disabled?: boolean; + className?: string; +} + +export interface ICustomSubMenuContentProps { + children: React.ReactNode; + className?: string; + placement?: Placement; + sideOffset?: number; + alignOffset?: number; +} diff --git a/web/ce/components/issues/issue-layouts/quick-action-dropdowns/copy-menu-helper.tsx b/web/ce/components/issues/issue-layouts/quick-action-dropdowns/copy-menu-helper.tsx new file mode 100644 index 000000000..f9aed4040 --- /dev/null +++ b/web/ce/components/issues/issue-layouts/quick-action-dropdowns/copy-menu-helper.tsx @@ -0,0 +1,22 @@ +import { Copy } from "lucide-react"; +import { TContextMenuItem } from "@plane/ui"; + +export interface CopyMenuHelperProps { + baseItem: { + key: string; + title: string; + icon: typeof Copy; + action: () => void; + shouldRender: boolean; + }; + activeLayout: string; + setTrackElement: (element: string) => void; + setCreateUpdateIssueModal: (open: boolean) => void; + setDuplicateWorkItemModal?: (open: boolean) => void; +} + +export const createCopyMenuWithDuplication = (props: CopyMenuHelperProps): TContextMenuItem => { + const { baseItem } = props; + + return baseItem; +}; diff --git a/web/ce/components/issues/issue-layouts/quick-action-dropdowns/duplicate-modal.tsx b/web/ce/components/issues/issue-layouts/quick-action-dropdowns/duplicate-modal.tsx new file mode 100644 index 000000000..1ea30e26e --- /dev/null +++ b/web/ce/components/issues/issue-layouts/quick-action-dropdowns/duplicate-modal.tsx @@ -0,0 +1,11 @@ +import { FC } from "react"; + +type TDuplicateWorkItemModalProps = { + workItemId: string; + onClose: () => void; + isOpen: boolean; + workspaceSlug: string; + projectId: string; +}; + +export const DuplicateWorkItemModal: FC = () => <>; diff --git a/web/ce/components/issues/issue-layouts/quick-action-dropdowns/index.ts b/web/ce/components/issues/issue-layouts/quick-action-dropdowns/index.ts new file mode 100644 index 000000000..470ae9181 --- /dev/null +++ b/web/ce/components/issues/issue-layouts/quick-action-dropdowns/index.ts @@ -0,0 +1,2 @@ +export * from "./duplicate-modal"; +export * from "./copy-menu-helper"; diff --git a/web/ce/store/issue/issue-details/root.store.ts b/web/ce/store/issue/issue-details/root.store.ts new file mode 100644 index 000000000..bbea3f46b --- /dev/null +++ b/web/ce/store/issue/issue-details/root.store.ts @@ -0,0 +1,18 @@ +import { makeObservable } from "mobx"; +import { TIssueServiceType } from "@plane/types"; +import { + IssueDetail as IssueDetailCore, + IIssueDetail as IIssueDetailCore, +} from "@/store/issue/issue-details/root.store"; +import { IIssueRootStore } from "@/store/issue/root.store"; + +export type IIssueDetail = IIssueDetailCore; + +export class IssueDetail extends IssueDetailCore { + constructor(rootStore: IIssueRootStore, serviceType: TIssueServiceType) { + super(rootStore, serviceType); + makeObservable(this, { + // observables + }); + } +} diff --git a/web/core/components/dropdowns/project.tsx b/web/core/components/dropdowns/project.tsx index 88c13bdb9..39ab86201 100644 --- a/web/core/components/dropdowns/project.tsx +++ b/web/core/components/dropdowns/project.tsx @@ -29,6 +29,7 @@ type Props = TDropdownProps & { onClose?: () => void; renderCondition?: (project: TProject) => boolean; renderByDefault?: boolean; + currentProjectId?: string; } & ( | { multiple: false; @@ -63,6 +64,7 @@ export const ProjectDropdown: React.FC = observer((props) => { tabIndex, value, renderByDefault = true, + currentProjectId, } = props; // states const [query, setQuery] = useState(""); @@ -108,7 +110,9 @@ export const ProjectDropdown: React.FC = observer((props) => { }); const filteredOptions = - query === "" ? options : options?.filter((o) => o?.query.toLowerCase().includes(query.toLowerCase())); + query === "" + ? options?.filter((o) => o?.value !== currentProjectId) + : options?.filter((o) => o?.value !== currentProjectId && o?.query.toLowerCase().includes(query.toLowerCase())); const { handleClose, handleKeyDown, handleOnClick, searchInputKeyDown } = useDropdown({ dropdownRef, @@ -198,7 +202,7 @@ export const ProjectDropdown: React.FC = observer((props) => { > {!hideIcon && getProjectIcon(value)} {BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && ( - {getDisplayName(value, placeholder)} + {getDisplayName(value, placeholder)} )} {dropdownArrow && (
+ } + disabled={item.disabled} + className={cn( + "flex items-center gap-2", + { + "text-custom-text-400": item.disabled, + }, + item.className + )} + > + {item.nestedMenuItems.map((nestedItem) => ( + { + e.preventDefault(); + e.stopPropagation(); + nestedItem.action(); + }} + className={cn( + "flex items-center gap-2", + { + "text-custom-text-400": nestedItem.disabled, + }, + nestedItem.className + )} + disabled={nestedItem.disabled} + > + {nestedItem.icon && } +
+
{nestedItem.title}
+ {nestedItem.description && ( +

+ {nestedItem.description} +

+ )} +
+
+ ))} + + ); + } + + // Render regular menu item return ( = observer((props) => { const { @@ -47,76 +45,34 @@ export const ArchivedIssueQuickActions: React.FC = observer(( const isRestoringAllowed = handleRestore && allowPermissions([EUserPermissions.ADMIN, EUserPermissions.MEMBER], EUserPermissionsLevel.PROJECT); - const issueLink = `${workspaceSlug}/projects/${issue.project_id}/archives/issues/${issue.id}`; - - const handleOpenInNewTab = () => window.open(`/${issueLink}`, "_blank"); - const handleCopyIssueLink = () => - copyUrlToClipboard(issueLink).then(() => - setToast({ - type: TOAST_TYPE.SUCCESS, - title: "Link copied", - message: "Work item link copied to clipboard", - }) - ); - const handleIssueRestore = async () => { - if (!handleRestore) return; - await handleRestore() - .then(() => { - setToast({ - type: TOAST_TYPE.SUCCESS, - title: "Restore success", - message: "Your work item can be found in project work items.", - }); - }) - .catch(() => { - setToast({ - type: TOAST_TYPE.ERROR, - title: "Error!", - message: "Work item could not be restored. Please try again.", - }); - }); + // Menu items and modals using helper + const menuItemProps: MenuItemFactoryProps = { + issue, + workspaceSlug: workspaceSlug?.toString(), + activeLayout, + isEditingAllowed, + isDeletingAllowed: isEditingAllowed, + isRestoringAllowed: !!isRestoringAllowed, + setTrackElement, + setIssueToEdit: () => {}, + setCreateUpdateIssueModal: () => {}, + setDeleteIssueModal, + handleRestore, + handleDelete, }; - const MENU_ITEMS: TContextMenuItem[] = [ - { - key: "restore", - title: "Restore", - icon: ArchiveRestoreIcon, - action: handleIssueRestore, - shouldRender: isRestoringAllowed, - }, - { - key: "open-in-new-tab", - title: "Open in new tab", - icon: ExternalLink, - action: handleOpenInNewTab, - }, - { - key: "copy-link", - title: "Copy link", - icon: Link, - action: handleCopyIssueLink, - }, - { - key: "delete", - title: "Delete", - icon: Trash2, - action: () => { - setTrackElement(activeLayout); - setDeleteIssueModal(true); - }, - shouldRender: isEditingAllowed, - }, - ]; + const MENU_ITEMS = useArchivedIssueMenuItems(menuItemProps); return ( <> + {/* Modals */} setDeleteIssueModal(false)} onSubmit={handleDelete} /> + = observer((props) => { const { @@ -38,6 +39,7 @@ export const CycleIssueQuickActions: React.FC = observer((pro const [issueToEdit, setIssueToEdit] = useState(undefined); const [deleteIssueModal, setDeleteIssueModal] = useState(false); const [archiveIssueModal, setArchiveIssueModal] = useState(false); + const [duplicateWorkItemModal, setDuplicateWorkItemModal] = useState(false); // router const { workspaceSlug, cycleId } = useParams(); // store hooks @@ -58,25 +60,6 @@ export const CycleIssueQuickActions: React.FC = observer((pro const activeLayout = `${issuesFilter.issueFilters?.displayFilters?.layout} layout`; - const workItemLink = generateWorkItemLink({ - workspaceSlug: workspaceSlug?.toString(), - projectId: issue?.project_id, - issueId: issue?.id, - projectIdentifier, - sequenceId: issue?.sequence_id, - }); - - const handleOpenInNewTab = () => window.open(workItemLink, "_blank"); - - const handleCopyIssueLink = () => - copyUrlToClipboard(workItemLink).then(() => - setToast({ - type: TOAST_TYPE.SUCCESS, - title: "Link copied", - message: "Work item link copied to clipboard", - }) - ); - const duplicateIssuePayload = omit( { ...issue, @@ -86,75 +69,35 @@ export const CycleIssueQuickActions: React.FC = observer((pro ["id"] ); - const MENU_ITEMS: TContextMenuItem[] = [ - { - key: "edit", - title: "Edit", - icon: Pencil, - action: () => { - setIssueToEdit({ - ...issue, - cycle_id: cycleId?.toString() ?? null, - }); - setTrackElement(activeLayout); - setCreateUpdateIssueModal(true); - }, - shouldRender: isEditingAllowed, - }, - { - key: "make-a-copy", - title: "Make a copy", - icon: Copy, - action: () => { - setTrackElement(activeLayout); - setCreateUpdateIssueModal(true); - }, - shouldRender: isEditingAllowed, - }, - { - key: "open-in-new-tab", - title: "Open in new tab", - icon: ExternalLink, - action: handleOpenInNewTab, - }, - { - key: "copy-link", - title: "Copy link", - icon: Link, - action: handleCopyIssueLink, - }, - { - key: "remove-from-cycle", - title: "Remove from cycle", - icon: XCircle, - action: () => handleRemoveFromView?.(), - shouldRender: isEditingAllowed, - }, - { - key: "archive", - title: "Archive", - description: isInArchivableGroup ? undefined : "Only completed or canceled\nwork items can be archived", - icon: ArchiveIcon, - className: "items-start", - iconClassName: "mt-1", - action: () => setArchiveIssueModal(true), - disabled: !isInArchivableGroup, - shouldRender: isArchivingAllowed, - }, - { - key: "delete", - title: "Delete", - icon: Trash2, - action: () => { - setTrackElement(activeLayout); - setDeleteIssueModal(true); - }, - shouldRender: isDeletingAllowed, - }, - ]; + // Menu items and modals using helper + const menuItemProps: MenuItemFactoryProps = { + issue, + workspaceSlug: workspaceSlug?.toString(), + projectIdentifier, + activeLayout, + isEditingAllowed, + isArchivingAllowed, + isDeletingAllowed, + isInArchivableGroup, + setTrackElement, + setIssueToEdit, + setCreateUpdateIssueModal, + setDeleteIssueModal, + setArchiveIssueModal, + setDuplicateWorkItemModal, + handleRemoveFromView, + cycleId: cycleId?.toString(), + handleDelete, + handleUpdate, + handleArchive, + storeType: EIssuesStoreType.CYCLE, + }; + + const MENU_ITEMS = useCycleIssueMenuItems(menuItemProps); return ( <> + {/* Modals */} = observer((pro if (issueToEdit && handleUpdate) await handleUpdate(data); }} storeType={EIssuesStoreType.CYCLE} + isDraft={false} /> + {issue.project_id && workspaceSlug && ( + setDuplicateWorkItemModal(false)} + workspaceSlug={workspaceSlug.toString()} + projectId={issue.project_id} + /> + )} + = observer((pro > {MENU_ITEMS.map((item) => { if (item.shouldRender === false) return null; + + // Render submenu if nestedMenuItems exist + if (item.nestedMenuItems && item.nestedMenuItems.length > 0) { + return ( + + {item.icon && } +
{item.title}
+ {item.description && ( +

+ {item.description} +

+ )} +
+ } + disabled={item.disabled} + className={cn( + "flex items-center gap-2", + { + "text-custom-text-400": item.disabled, + }, + item.className + )} + > + {item.nestedMenuItems.map((nestedItem) => ( + { + e.preventDefault(); + e.stopPropagation(); + nestedItem.action(); + }} + className={cn( + "flex items-center gap-2", + { + "text-custom-text-400": nestedItem.disabled, + }, + nestedItem.className + )} + disabled={nestedItem.disabled} + > + {nestedItem.icon && } +
+
{nestedItem.title}
+ {nestedItem.description && ( +

+ {nestedItem.description} +

+ )} +
+
+ ))} + + ); + } + + // Render regular menu item return ( = observer((props) => { const { @@ -32,6 +30,9 @@ export const DraftIssueQuickActions: React.FC = observer((pro placements = "bottom-end", parentRef, } = props; + // router + const { workspaceSlug } = useParams(); + const pathname = usePathname(); // states const [createUpdateIssueModal, setCreateUpdateIssueModal] = useState(false); const [issueToEdit, setIssueToEdit] = useState(undefined); @@ -39,56 +40,52 @@ export const DraftIssueQuickActions: React.FC = observer((pro // store hooks const { allowPermissions } = useUserPermissions(); const { setTrackElement } = useEventTracker(); - const { issuesFilter } = useIssues(EIssuesStoreType.PROJECT); - - const { t } = useTranslation(); // derived values - const activeLayout = `${issuesFilter.issueFilters?.displayFilters?.layout} layout`; + const activeLayout = "Draft Issues"; // auth const isEditingAllowed = - allowPermissions([EUserPermissions.ADMIN, EUserPermissions.MEMBER], EUserPermissionsLevel.PROJECT) && !readOnly; + allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.PROJECT, + workspaceSlug?.toString(), + issue.project_id ?? undefined + ) && !readOnly; const isDeletingAllowed = isEditingAllowed; + const isDraftIssue = pathname?.includes("draft-issues") || false; + const duplicateIssuePayload = omit( { ...issue, name: `${issue.name} (copy)`, - is_draft: true, + is_draft: isDraftIssue ? false : issue.is_draft, + sourceIssueId: issue.id, }, ["id"] ); - const MENU_ITEMS: TContextMenuItem[] = [ - { - key: "edit", - title: "edit", - icon: Pencil, - action: () => { - setTrackElement(activeLayout); - setIssueToEdit(issue); - setCreateUpdateIssueModal(true); - }, - shouldRender: isEditingAllowed, - }, - { - key: "delete", - title: "delete", - icon: Trash2, - action: () => { - setTrackElement(activeLayout); - setDeleteIssueModal(true); - }, - shouldRender: isDeletingAllowed, - }, - ]; + // Menu items and modals using helper + const menuItemProps: MenuItemFactoryProps = { + issue, + workspaceSlug: workspaceSlug?.toString(), + activeLayout, + isEditingAllowed, + isDeletingAllowed, + isDraftIssue, + setTrackElement, + setIssueToEdit, + setCreateUpdateIssueModal, + setDeleteIssueModal, + handleDelete, + handleUpdate, + storeType: EIssuesStoreType.DRAFT, + }; - // check if any of the menu items should render - const shouldRenderQuickAction = MENU_ITEMS.some((item) => item.shouldRender); - - if (!shouldRenderQuickAction) return <>; + const MENU_ITEMS = useDraftIssueMenuItems(menuItemProps); return ( <> + {/* Modals */} = observer((pro if (issueToEdit && handleUpdate) await handleUpdate(data); }} storeType={EIssuesStoreType.DRAFT} - isDraft + isDraft={isDraftIssue} /> + {MENU_ITEMS.map((item) => { @@ -140,7 +137,7 @@ export const DraftIssueQuickActions: React.FC = observer((pro > {item.icon && }
-
{t(item.title ?? "")}
+
{item.title}
{item.description && (

void) | (() => Promise) | undefined, + actionName: string +): void; + +// Overload for functions with one parameter +export function handleOptionalAction( + optionalFn: ((param: T) => void) | ((param: T) => Promise) | undefined, + actionName: string, + param: T +): void; + +// Implementation +export function handleOptionalAction( + optionalFn: (() => void) | (() => Promise) | ((param: T) => void) | ((param: T) => Promise) | undefined, + actionName: string, + param?: T +): void { + if (optionalFn) { + if (param !== undefined) { + (optionalFn as (param: T) => void | Promise)(param); + } else { + (optionalFn as () => void | Promise)(); + } + } else { + setToast({ + type: TOAST_TYPE.ERROR, + title: "Action not available", + message: `${actionName} action is not implemented.`, + }); + } +} + +export interface MenuItemFactoryProps { + issue: TIssue; + workspaceSlug?: string; + projectIdentifier?: string; + activeLayout?: string; + isEditingAllowed: boolean; + isArchivingAllowed?: boolean; + isDeletingAllowed: boolean; + isRestoringAllowed?: boolean; + isInArchivableGroup?: boolean; + issueTypeDetail?: { is_active?: boolean }; + isDraftIssue?: boolean; + // Action handlers + setTrackElement: (element: string) => void; + setIssueToEdit: (issue: TIssue | undefined) => void; + setCreateUpdateIssueModal: (open: boolean) => void; + setDeleteIssueModal: (open: boolean) => void; + setArchiveIssueModal?: (open: boolean) => void; + setDuplicateWorkItemModal?: (open: boolean) => void; + handleRemoveFromView?: () => void; + handleRestore?: () => Promise; + // External handlers + handleDelete?: () => Promise; + handleUpdate?: (data: TIssue) => Promise; + handleArchive?: () => Promise; + // Context-specific data + cycleId?: string; + moduleId?: string; + storeType?: EIssuesStoreType; +} + +// Common action handlers hook +export const useIssueActionHandlers = (props: MenuItemFactoryProps) => { + const { issue, workspaceSlug, projectIdentifier, handleRestore } = props; + + const workItemLink = useMemo( + () => + generateWorkItemLink({ + workspaceSlug, + projectId: issue?.project_id, + issueId: issue?.id, + projectIdentifier, + sequenceId: issue?.sequence_id, + }), + [workspaceSlug, projectIdentifier, issue] + ); + + const handleCopyIssueLink = () => + copyUrlToClipboard(workItemLink).then(() => + setToast({ + type: TOAST_TYPE.SUCCESS, + title: "Link copied", + message: "Work item link copied to clipboard", + }) + ); + + const handleOpenInNewTab = () => window.open(workItemLink, "_blank"); + + const handleIssueRestore = async () => { + if (!handleRestore) { + handleOptionalAction(handleRestore, "Restore"); + return; + } + await handleRestore() + .then(() => { + setToast({ + type: TOAST_TYPE.SUCCESS, + title: "Restore success", + message: "Your work item can be found in project work items.", + }); + }) + .catch(() => { + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: "Work item could not be restored. Please try again.", + }); + }); + }; + + return { + workItemLink, + handleCopyIssueLink, + handleOpenInNewTab, + handleIssueRestore, + }; +}; + +export const useMenuItemFactory = (props: MenuItemFactoryProps) => { + const { t } = useTranslation(); + const actionHandlers = useIssueActionHandlers(props); + + const { + issue, + activeLayout = "", + isEditingAllowed, + isArchivingAllowed = false, + isDeletingAllowed, + isRestoringAllowed = false, + isInArchivableGroup = false, + issueTypeDetail, + setTrackElement, + setIssueToEdit, + setCreateUpdateIssueModal, + setDeleteIssueModal, + setArchiveIssueModal, + setDuplicateWorkItemModal, + handleRemoveFromView, + } = props; + + const createEditMenuItem = (customEditAction?: () => void): TContextMenuItem => ({ + key: "edit", + title: t("common.actions.edit"), + icon: Pencil, + action: + customEditAction || + (() => { + setTrackElement(activeLayout); + setIssueToEdit(issue); + setCreateUpdateIssueModal(true); + }), + shouldRender: isEditingAllowed, + }); + + const createCopyMenuItem = (): TContextMenuItem => { + const baseItem = { + key: "make-a-copy", + title: t("common.actions.make_a_copy"), + icon: Copy, + action: () => { + setTrackElement(activeLayout); + setCreateUpdateIssueModal(true); + }, + shouldRender: isEditingAllowed && (issueTypeDetail?.is_active ?? true), + }; + + return createCopyMenuWithDuplication({ + baseItem, + activeLayout, + setTrackElement, + setCreateUpdateIssueModal, + setDuplicateWorkItemModal, + }); + }; + + const createOpenInNewTabMenuItem = (): TContextMenuItem => ({ + key: "open-in-new-tab", + title: t("common.actions.open_in_new_tab"), + icon: ExternalLink, + action: actionHandlers.handleOpenInNewTab, + }); + + const createCopyLinkMenuItem = (): TContextMenuItem => ({ + key: "copy-link", + title: t("common.actions.copy_link"), + icon: Link, + action: actionHandlers.handleCopyIssueLink, + }); + + const createRemoveFromCycleMenuItem = (): TContextMenuItem => ({ + key: "remove-from-cycle", + title: "Remove from cycle", + icon: XCircle, + action: () => handleOptionalAction(handleRemoveFromView, "Remove from cycle"), + shouldRender: isEditingAllowed, + }); + + const createRemoveFromModuleMenuItem = (): TContextMenuItem => ({ + key: "remove-from-module", + title: "Remove from module", + icon: XCircle, + action: () => handleOptionalAction(handleRemoveFromView, "Remove from module"), + shouldRender: isEditingAllowed, + }); + + const createArchiveMenuItem = (): TContextMenuItem => ({ + key: "archive", + title: t("common.actions.archive"), + description: isInArchivableGroup ? undefined : t("issue.archive.description"), + icon: ArchiveIcon, + className: "items-start", + iconClassName: "mt-1", + action: () => handleOptionalAction(setArchiveIssueModal, "Archive", true), + disabled: !isInArchivableGroup, + shouldRender: isArchivingAllowed, + }); + + const createRestoreMenuItem = (): TContextMenuItem => ({ + key: "restore", + title: "Restore", + icon: ArchiveRestoreIcon, + action: actionHandlers.handleIssueRestore, + shouldRender: isRestoringAllowed, + }); + + const createDeleteMenuItem = (): TContextMenuItem => ({ + key: "delete", + title: t("common.actions.delete"), + icon: Trash2, + action: () => { + setTrackElement(activeLayout); + setDeleteIssueModal(true); + }, + shouldRender: isDeletingAllowed, + }); + + return { + ...actionHandlers, + createEditMenuItem, + createCopyMenuItem, + createOpenInNewTabMenuItem, + createCopyLinkMenuItem, + createRemoveFromCycleMenuItem, + createRemoveFromModuleMenuItem, + createArchiveMenuItem, + createRestoreMenuItem, + createDeleteMenuItem, + }; +}; + +// Predefined menu item sets for different contexts +export const useProjectIssueMenuItems = (props: MenuItemFactoryProps): TContextMenuItem[] => { + const factory = useMenuItemFactory(props); + + return useMemo( + () => [ + factory.createEditMenuItem(), + factory.createCopyMenuItem(), + factory.createOpenInNewTabMenuItem(), + factory.createCopyLinkMenuItem(), + factory.createArchiveMenuItem(), + factory.createDeleteMenuItem(), + ], + [factory] + ); +}; + +export const useAllIssueMenuItems = (props: MenuItemFactoryProps): TContextMenuItem[] => { + const factory = useMenuItemFactory(props); + + return useMemo( + () => [ + factory.createEditMenuItem(), + factory.createCopyMenuItem(), + factory.createOpenInNewTabMenuItem(), + factory.createCopyLinkMenuItem(), + factory.createArchiveMenuItem(), + factory.createDeleteMenuItem(), + ], + [factory] + ); +}; + +export const useCycleIssueMenuItems = (props: MenuItemFactoryProps): TContextMenuItem[] => { + const factory = useMenuItemFactory(props); + + const customEditAction = () => { + props.setIssueToEdit({ + ...props.issue, + cycle_id: props.cycleId ?? null, + }); + props.setTrackElement(props.activeLayout || ""); + props.setCreateUpdateIssueModal(true); + }; + + return useMemo( + () => [ + factory.createEditMenuItem(customEditAction), + factory.createCopyMenuItem(), + factory.createOpenInNewTabMenuItem(), + factory.createCopyLinkMenuItem(), + factory.createRemoveFromCycleMenuItem(), + factory.createArchiveMenuItem(), + factory.createDeleteMenuItem(), + ], + [factory, props.cycleId] + ); +}; + +export const useModuleIssueMenuItems = (props: MenuItemFactoryProps): TContextMenuItem[] => { + const factory = useMenuItemFactory(props); + + const customEditAction = () => { + props.setIssueToEdit({ + ...props.issue, + module_ids: props.moduleId ? [props.moduleId] : [], + }); + props.setTrackElement(props.activeLayout || ""); + props.setCreateUpdateIssueModal(true); + }; + + return useMemo( + () => [ + factory.createEditMenuItem(customEditAction), + factory.createCopyMenuItem(), + factory.createOpenInNewTabMenuItem(), + factory.createCopyLinkMenuItem(), + factory.createRemoveFromModuleMenuItem(), + factory.createArchiveMenuItem(), + factory.createDeleteMenuItem(), + ], + [factory, props.moduleId] + ); +}; + +export const useArchivedIssueMenuItems = (props: MenuItemFactoryProps): TContextMenuItem[] => { + const factory = useMenuItemFactory(props); + + return useMemo( + () => [ + factory.createRestoreMenuItem(), + factory.createOpenInNewTabMenuItem(), + factory.createCopyLinkMenuItem(), + factory.createDeleteMenuItem(), + ], + [factory] + ); +}; + +export const useDraftIssueMenuItems = (props: MenuItemFactoryProps): TContextMenuItem[] => { + const factory = useMenuItemFactory(props); + + return useMemo(() => [factory.createEditMenuItem(), factory.createDeleteMenuItem()], [factory]); +}; diff --git a/web/core/components/issues/issue-layouts/quick-action-dropdowns/index.ts b/web/core/components/issues/issue-layouts/quick-action-dropdowns/index.ts index dbc1e9f5a..075f8ae0a 100644 --- a/web/core/components/issues/issue-layouts/quick-action-dropdowns/index.ts +++ b/web/core/components/issues/issue-layouts/quick-action-dropdowns/index.ts @@ -1,7 +1,8 @@ +export * from "./all-issue"; +export * from "./archived-issue"; export * from "./cycle-issue"; +export * from "./draft-issue"; export * from "./module-issue"; export * from "./project-issue"; -export * from "./archived-issue"; -export * from "./draft-issue"; -export * from "./all-issue"; +export * from "./helper"; export * from "../../workspace-draft/quick-action"; diff --git a/web/core/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx b/web/core/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx index c7f949fcf..6a8840ace 100644 --- a/web/core/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx +++ b/web/core/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx @@ -4,21 +4,21 @@ import { useState } from "react"; import omit from "lodash/omit"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; -import { Copy, ExternalLink, Link, Pencil, Trash2, XCircle } from "lucide-react"; // plane imports import { ARCHIVABLE_STATE_GROUPS, EIssuesStoreType, EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; import { TIssue } from "@plane/types"; -import { ArchiveIcon, ContextMenu, CustomMenu, TContextMenuItem, TOAST_TYPE, setToast } from "@plane/ui"; -import { copyUrlToClipboard } from "@plane/utils"; +import { ContextMenu, CustomMenu } from "@plane/ui"; // components import { ArchiveIssueModal, CreateUpdateIssueModal, DeleteIssueModal } from "@/components/issues"; // helpers import { cn } from "@/helpers/common.helper"; -import { generateWorkItemLink } from "@/helpers/issue.helper"; // hooks import { useIssues, useEventTracker, useProjectState, useUserPermissions, useProject } from "@/hooks/store"; -// types +// plane-web components +import { DuplicateWorkItemModal } from "@/plane-web/components/issues/issue-layouts/quick-action-dropdowns"; import { IQuickActionProps } from "../list/list-view-types"; +// helper +import { useModuleIssueMenuItems, MenuItemFactoryProps } from "./helper"; export const ModuleIssueQuickActions: React.FC = observer((props) => { const { @@ -38,6 +38,7 @@ export const ModuleIssueQuickActions: React.FC = observer((pr const [issueToEdit, setIssueToEdit] = useState(undefined); const [deleteIssueModal, setDeleteIssueModal] = useState(false); const [archiveIssueModal, setArchiveIssueModal] = useState(false); + const [duplicateWorkItemModal, setDuplicateWorkItemModal] = useState(false); // router const { workspaceSlug, moduleId } = useParams(); // store hooks @@ -58,25 +59,6 @@ export const ModuleIssueQuickActions: React.FC = observer((pr const activeLayout = `${issuesFilter.issueFilters?.displayFilters?.layout} layout`; - const workItemLink = generateWorkItemLink({ - workspaceSlug: workspaceSlug?.toString(), - projectId: issue?.project_id, - issueId: issue?.id, - projectIdentifier, - sequenceId: issue?.sequence_id, - }); - - const handleOpenInNewTab = () => window.open(workItemLink, "_blank"); - - const handleCopyIssueLink = () => - copyUrlToClipboard(workItemLink).then(() => - setToast({ - type: TOAST_TYPE.SUCCESS, - title: "Link copied", - message: "Work item link copied to clipboard", - }) - ); - const duplicateIssuePayload = omit( { ...issue, @@ -86,72 +68,35 @@ export const ModuleIssueQuickActions: React.FC = observer((pr ["id"] ); - const MENU_ITEMS: TContextMenuItem[] = [ - { - key: "edit", - title: "Edit", - icon: Pencil, - action: () => { - setIssueToEdit({ ...issue, module_ids: moduleId ? [moduleId.toString()] : [] }); - setTrackElement(activeLayout); - setCreateUpdateIssueModal(true); - }, - shouldRender: isEditingAllowed, - }, - { - key: "make-a-copy", - title: "Make a copy", - icon: Copy, - action: () => { - setTrackElement(activeLayout); - setCreateUpdateIssueModal(true); - }, - shouldRender: isEditingAllowed, - }, - { - key: "open-in-new-tab", - title: "Open in new tab", - icon: ExternalLink, - action: handleOpenInNewTab, - }, - { - key: "copy-link", - title: "Copy link", - icon: Link, - action: handleCopyIssueLink, - }, - { - key: "remove-from-module", - title: "Remove from module", - icon: XCircle, - action: () => handleRemoveFromView?.(), - shouldRender: isEditingAllowed, - }, - { - key: "archive", - title: "Archive", - description: isInArchivableGroup ? undefined : "Only completed or canceled\nwork items can be archived", - icon: ArchiveIcon, - className: "items-start", - iconClassName: "mt-1", - action: () => setArchiveIssueModal(true), - disabled: !isInArchivableGroup, - shouldRender: isArchivingAllowed, - }, - { - key: "delete", - title: "Delete", - icon: Trash2, - action: () => { - setTrackElement(activeLayout); - setDeleteIssueModal(true); - }, - shouldRender: isDeletingAllowed, - }, - ]; + // Menu items and modals using helper + const menuItemProps: MenuItemFactoryProps = { + issue, + workspaceSlug: workspaceSlug?.toString(), + projectIdentifier, + activeLayout, + isEditingAllowed, + isArchivingAllowed, + isDeletingAllowed, + isInArchivableGroup, + setTrackElement, + setIssueToEdit, + setCreateUpdateIssueModal, + setDeleteIssueModal, + setArchiveIssueModal, + setDuplicateWorkItemModal, + handleRemoveFromView, + moduleId: moduleId?.toString(), + handleDelete, + handleUpdate, + handleArchive, + storeType: EIssuesStoreType.MODULE, + }; + + const MENU_ITEMS = useModuleIssueMenuItems(menuItemProps); return ( <> + {/* Modals */} = observer((pr if (issueToEdit && handleUpdate) await handleUpdate(data); }} storeType={EIssuesStoreType.MODULE} + isDraft={false} /> + {issue.project_id && workspaceSlug && ( + setDuplicateWorkItemModal(false)} + workspaceSlug={workspaceSlug.toString()} + projectId={issue.project_id} + /> + )} + = observer((pr > {MENU_ITEMS.map((item) => { if (item.shouldRender === false) return null; + + // Render submenu if nestedMenuItems exist + if (item.nestedMenuItems && item.nestedMenuItems.length > 0) { + return ( + + {item.icon && } +

{item.title}
+ {item.description && ( +

+ {item.description} +

+ )} +
+ } + disabled={item.disabled} + className={cn( + "flex items-center gap-2", + { + "text-custom-text-400": item.disabled, + }, + item.className + )} + > + {item.nestedMenuItems.map((nestedItem) => ( + { + e.preventDefault(); + e.stopPropagation(); + nestedItem.action(); + }} + className={cn( + "flex items-center gap-2", + { + "text-custom-text-400": nestedItem.disabled, + }, + nestedItem.className + )} + disabled={nestedItem.disabled} + > + {nestedItem.icon && } +
+
{nestedItem.title}
+ {nestedItem.description && ( +

+ {nestedItem.description} +

+ )} +
+
+ ))} + + ); + } + + // Render regular menu item return ( = observer((props) => { const { @@ -33,8 +32,6 @@ export const ProjectIssueQuickActions: React.FC = observer((p placements = "bottom-end", parentRef, } = props; - // i18n - const { t } = useTranslation(); // router const { workspaceSlug } = useParams(); const pathname = usePathname(); @@ -43,6 +40,7 @@ export const ProjectIssueQuickActions: React.FC = observer((p const [issueToEdit, setIssueToEdit] = useState(undefined); const [deleteIssueModal, setDeleteIssueModal] = useState(false); const [archiveIssueModal, setArchiveIssueModal] = useState(false); + const [duplicateWorkItemModal, setDuplicateWorkItemModal] = useState(false); // store hooks const { allowPermissions } = useUserPermissions(); const { setTrackElement } = useEventTracker(); @@ -65,24 +63,6 @@ export const ProjectIssueQuickActions: React.FC = observer((p const isInArchivableGroup = !!stateDetails && ARCHIVABLE_STATE_GROUPS.includes(stateDetails?.group); const isDeletingAllowed = isEditingAllowed; - const workItemLink = generateWorkItemLink({ - workspaceSlug: workspaceSlug?.toString(), - projectId: issue?.project_id, - issueId: issue?.id, - projectIdentifier, - sequenceId: issue?.sequence_id, - }); - - const handleCopyIssueLink = () => - copyUrlToClipboard(workItemLink).then(() => - setToast({ - type: TOAST_TYPE.SUCCESS, - title: "Link copied", - message: "Work item link copied to clipboard", - }) - ); - const handleOpenInNewTab = () => window.open(workItemLink, "_blank"); - const isDraftIssue = pathname?.includes("draft-issues") || false; const duplicateIssuePayload = omit( @@ -95,68 +75,34 @@ export const ProjectIssueQuickActions: React.FC = observer((p ["id"] ); - const MENU_ITEMS: TContextMenuItem[] = useMemo( - () => [ - { - key: "edit", - title: t("common.actions.edit"), - icon: Pencil, - action: () => { - setTrackElement(activeLayout); - setIssueToEdit(issue); - setCreateUpdateIssueModal(true); - }, - shouldRender: isEditingAllowed, - }, - { - key: "make-a-copy", - title: t("common.actions.make_a_copy"), - icon: Copy, - action: () => { - setTrackElement(activeLayout); - setCreateUpdateIssueModal(true); - }, - shouldRender: isEditingAllowed, - }, - { - key: "open-in-new-tab", - title: t("common.actions.open_in_new_tab"), - icon: ExternalLink, - action: handleOpenInNewTab, - }, - { - key: "copy-link", - title: t("common.actions.copy_link"), - icon: Link, - action: handleCopyIssueLink, - }, - { - key: "archive", - title: t("common.actions.archive"), - description: isInArchivableGroup ? undefined : t("issue.archive.description"), - icon: ArchiveIcon, - className: "items-start", - iconClassName: "mt-1", - action: () => setArchiveIssueModal(true), - disabled: !isInArchivableGroup, - shouldRender: isArchivingAllowed, - }, - { - key: "delete", - title: t("common.actions.delete"), - icon: Trash2, - action: () => { - setTrackElement(activeLayout); - setDeleteIssueModal(true); - }, - shouldRender: isDeletingAllowed, - }, - ], - [t] - ); + // Menu items and modals using helper + const menuItemProps: MenuItemFactoryProps = { + issue, + workspaceSlug: workspaceSlug?.toString(), + projectIdentifier, + activeLayout, + isEditingAllowed, + isArchivingAllowed, + isDeletingAllowed, + isInArchivableGroup, + isDraftIssue, + setTrackElement, + setIssueToEdit, + setCreateUpdateIssueModal, + setDeleteIssueModal, + setArchiveIssueModal, + setDuplicateWorkItemModal, + handleDelete, + handleUpdate, + handleArchive, + storeType: EIssuesStoreType.PROJECT, + }; + + const MENU_ITEMS = useProjectIssueMenuItems(menuItemProps); return ( <> + {/* Modals */} = observer((p storeType={EIssuesStoreType.PROJECT} isDraft={isDraftIssue} /> + {issue.project_id && workspaceSlug && ( + setDuplicateWorkItemModal(false)} + workspaceSlug={workspaceSlug.toString()} + projectId={issue.project_id} + /> + )} + = observer((p portalElement={portalElement} menuItemsClassName="z-[14]" maxHeight="lg" - useCaptureForOutsideClick closeOnSelect > {MENU_ITEMS.map((item) => { if (item.shouldRender === false) return null; + + // Render submenu if nestedMenuItems exist + if (item.nestedMenuItems && item.nestedMenuItems.length > 0) { + return ( + + {item.icon && } +
{item.title}
+ {item.description && ( +

+ {item.description} +

+ )} +
+ } + disabled={item.disabled} + className={cn( + "flex items-center gap-2", + { + "text-custom-text-400": item.disabled, + }, + item.className + )} + > + {item.nestedMenuItems.map((nestedItem) => ( + { + e.preventDefault(); + e.stopPropagation(); + nestedItem.action(); + }} + className={cn( + "flex items-center gap-2", + { + "text-custom-text-400": nestedItem.disabled, + }, + nestedItem.className + )} + disabled={nestedItem.disabled} + > + {nestedItem.icon && } +
+
{nestedItem.title}
+ {nestedItem.description && ( +

+ {nestedItem.description} +

+ )} +
+
+ ))} + + ); + } + + // Render regular menu item return ( Date: Fri, 6 Jun 2025 14:09:56 +0530 Subject: [PATCH 136/201] refactor: unused components, hooks, constants (#7157) * refactor: remove unused dashboard components and fetch keys * refactor: remove unused hooks and wrappers * chore: remove unused function --- web/core/components/dashboard/index.ts | 2 - .../dashboard/project-empty-state.tsx | 46 -- .../dashboard/widgets/assigned-issues.tsx | 165 ------- .../dashboard/widgets/created-issues.tsx | 162 ------- .../widgets/dropdowns/duration-filter.tsx | 58 --- .../dashboard/widgets/dropdowns/index.ts | 1 - .../widgets/empty-states/assigned-issues.tsx | 55 --- .../widgets/empty-states/created-issues.tsx | 55 --- .../dashboard/widgets/empty-states/index.ts | 6 - .../empty-states/issues-by-priority.tsx | 25 -- .../empty-states/issues-by-state-group.tsx | 25 -- .../widgets/empty-states/recent-activity.tsx | 25 -- .../empty-states/recent-collaborators.tsx | 39 -- .../dashboard/widgets/error-states/index.ts | 1 - .../dashboard/widgets/error-states/issues.tsx | 34 -- .../components/dashboard/widgets/index.ts | 11 - .../dashboard/widgets/issue-panels/index.ts | 3 - .../widgets/issue-panels/issue-list-item.tsx | 401 ------------------ .../widgets/issue-panels/issues-list.tsx | 134 ------ .../widgets/issue-panels/tabs-list.tsx | 61 --- .../widgets/loaders/assigned-issues.tsx | 24 -- .../dashboard/widgets/loaders/index.ts | 1 - .../widgets/loaders/issues-by-priority.tsx | 17 - .../widgets/loaders/issues-by-state-group.tsx | 24 -- .../dashboard/widgets/loaders/loader.tsx | 31 -- .../widgets/loaders/overview-stats.tsx | 16 - .../widgets/loaders/recent-activity.tsx | 22 - .../widgets/loaders/recent-collaborators.tsx | 20 - .../widgets/loaders/recent-projects.tsx | 22 - .../dashboard/widgets/overview-stats.tsx | 100 ----- .../dashboard/widgets/recent-activity.tsx | 109 ----- .../collaborators-list.tsx | 154 ------- .../widgets/recent-collaborators/index.ts | 1 - .../widgets/recent-collaborators/root.tsx | 36 -- .../dashboard/widgets/recent-projects.tsx | 134 ------ web/core/constants/fetch-keys.ts | 152 +------ web/core/hooks/use-comment-reaction.tsx | 93 ---- web/core/hooks/use-dynamic-dropdown.tsx | 63 --- web/core/hooks/use-url-hash.tsx | 14 - web/core/lib/wrappers/crisp-wrapper.tsx | 41 -- web/helpers/string.helper.ts | 29 -- web/package.json | 2 - 42 files changed, 1 insertion(+), 2413 deletions(-) delete mode 100644 web/core/components/dashboard/index.ts delete mode 100644 web/core/components/dashboard/project-empty-state.tsx delete mode 100644 web/core/components/dashboard/widgets/assigned-issues.tsx delete mode 100644 web/core/components/dashboard/widgets/created-issues.tsx delete mode 100644 web/core/components/dashboard/widgets/dropdowns/duration-filter.tsx delete mode 100644 web/core/components/dashboard/widgets/dropdowns/index.ts delete mode 100644 web/core/components/dashboard/widgets/empty-states/assigned-issues.tsx delete mode 100644 web/core/components/dashboard/widgets/empty-states/created-issues.tsx delete mode 100644 web/core/components/dashboard/widgets/empty-states/index.ts delete mode 100644 web/core/components/dashboard/widgets/empty-states/issues-by-priority.tsx delete mode 100644 web/core/components/dashboard/widgets/empty-states/issues-by-state-group.tsx delete mode 100644 web/core/components/dashboard/widgets/empty-states/recent-activity.tsx delete mode 100644 web/core/components/dashboard/widgets/empty-states/recent-collaborators.tsx delete mode 100644 web/core/components/dashboard/widgets/error-states/index.ts delete mode 100644 web/core/components/dashboard/widgets/error-states/issues.tsx delete mode 100644 web/core/components/dashboard/widgets/index.ts delete mode 100644 web/core/components/dashboard/widgets/issue-panels/index.ts delete mode 100644 web/core/components/dashboard/widgets/issue-panels/issue-list-item.tsx delete mode 100644 web/core/components/dashboard/widgets/issue-panels/issues-list.tsx delete mode 100644 web/core/components/dashboard/widgets/issue-panels/tabs-list.tsx delete mode 100644 web/core/components/dashboard/widgets/loaders/assigned-issues.tsx delete mode 100644 web/core/components/dashboard/widgets/loaders/index.ts delete mode 100644 web/core/components/dashboard/widgets/loaders/issues-by-priority.tsx delete mode 100644 web/core/components/dashboard/widgets/loaders/issues-by-state-group.tsx delete mode 100644 web/core/components/dashboard/widgets/loaders/loader.tsx delete mode 100644 web/core/components/dashboard/widgets/loaders/overview-stats.tsx delete mode 100644 web/core/components/dashboard/widgets/loaders/recent-activity.tsx delete mode 100644 web/core/components/dashboard/widgets/loaders/recent-collaborators.tsx delete mode 100644 web/core/components/dashboard/widgets/loaders/recent-projects.tsx delete mode 100644 web/core/components/dashboard/widgets/overview-stats.tsx delete mode 100644 web/core/components/dashboard/widgets/recent-activity.tsx delete mode 100644 web/core/components/dashboard/widgets/recent-collaborators/collaborators-list.tsx delete mode 100644 web/core/components/dashboard/widgets/recent-collaborators/index.ts delete mode 100644 web/core/components/dashboard/widgets/recent-collaborators/root.tsx delete mode 100644 web/core/components/dashboard/widgets/recent-projects.tsx delete mode 100644 web/core/hooks/use-comment-reaction.tsx delete mode 100644 web/core/hooks/use-dynamic-dropdown.tsx delete mode 100644 web/core/hooks/use-url-hash.tsx delete mode 100644 web/core/lib/wrappers/crisp-wrapper.tsx diff --git a/web/core/components/dashboard/index.ts b/web/core/components/dashboard/index.ts deleted file mode 100644 index e88b1c701..000000000 --- a/web/core/components/dashboard/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./widgets"; -export * from "./project-empty-state"; diff --git a/web/core/components/dashboard/project-empty-state.tsx b/web/core/components/dashboard/project-empty-state.tsx deleted file mode 100644 index eb06d21be..000000000 --- a/web/core/components/dashboard/project-empty-state.tsx +++ /dev/null @@ -1,46 +0,0 @@ -"use client"; - -import { observer } from "mobx-react"; -import Image from "next/image"; -// ui -import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; -import { Button } from "@plane/ui"; -// hooks -import { useCommandPalette, useEventTracker, useUserPermissions } from "@/hooks/store"; -// assets -import ProjectEmptyStateImage from "@/public/empty-state/onboarding/dashboard-light.webp"; - -export const DashboardProjectEmptyState = observer(() => { - // store hooks - const { toggleCreateProjectModal } = useCommandPalette(); - const { setTrackElement } = useEventTracker(); - const { allowPermissions } = useUserPermissions(); - - // derived values - const canCreateProject = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE); - - return ( -
-

Overview of your projects, activity, and metrics

-

- Welcome to Plane, we are excited to have you here. Create your first project and track your work items, and this - page will transform into a space that helps you progress. Admins will also see items which help their team - progress. -

- Project empty state - {canCreateProject && ( -
- -
- )} -
- ); -}); diff --git a/web/core/components/dashboard/widgets/assigned-issues.tsx b/web/core/components/dashboard/widgets/assigned-issues.tsx deleted file mode 100644 index cc2b74314..000000000 --- a/web/core/components/dashboard/widgets/assigned-issues.tsx +++ /dev/null @@ -1,165 +0,0 @@ -import { useEffect, useState } from "react"; -import { observer } from "mobx-react"; -import Link from "next/link"; -import { Tab } from "@headlessui/react"; -import { EDurationFilters, FILTERED_ISSUES_TABS_LIST, UNFILTERED_ISSUES_TABS_LIST } from "@plane/constants"; -import { TAssignedIssuesWidgetFilters, TAssignedIssuesWidgetResponse } from "@plane/types"; -// hooks -import { Card } from "@plane/ui"; -import { - DurationFilterDropdown, - IssuesErrorState, - TabsList, - WidgetIssuesList, - WidgetLoader, - WidgetProps, -} from "@/components/dashboard/widgets"; -import { getCustomDates, getRedirectionFilters, getTabKey } from "@/helpers/dashboard.helper"; -import { useDashboard } from "@/hooks/store"; -// components -// helpers -// types -// constants - -const WIDGET_KEY = "assigned_issues"; - -export const AssignedIssuesWidget: React.FC = observer((props) => { - const { dashboardId, workspaceSlug } = props; - // states - const [fetching, setFetching] = useState(false); - // store hooks - const { fetchWidgetStats, getWidgetDetails, getWidgetStats, getWidgetStatsError, updateDashboardWidgetFilters } = - useDashboard(); - // derived values - const widgetDetails = getWidgetDetails(workspaceSlug, dashboardId, WIDGET_KEY); - const widgetStats = getWidgetStats(workspaceSlug, dashboardId, WIDGET_KEY); - const widgetStatsError = getWidgetStatsError(workspaceSlug, dashboardId, WIDGET_KEY); - const selectedDurationFilter = widgetDetails?.widget_filters.duration ?? EDurationFilters.NONE; - const selectedTab = getTabKey(selectedDurationFilter, widgetDetails?.widget_filters.tab); - const selectedCustomDates = widgetDetails?.widget_filters.custom_dates ?? []; - - const handleUpdateFilters = async (filters: Partial) => { - if (!widgetDetails) return; - - setFetching(true); - - await updateDashboardWidgetFilters(workspaceSlug, dashboardId, widgetDetails.id, { - widgetKey: WIDGET_KEY, - filters, - }); - - const filterDates = getCustomDates( - filters.duration ?? selectedDurationFilter, - filters.custom_dates ?? selectedCustomDates - ); - fetchWidgetStats(workspaceSlug, dashboardId, { - widget_key: WIDGET_KEY, - issue_type: filters.tab ?? selectedTab, - ...(filterDates.trim() !== "" ? { target_date: filterDates } : {}), - expand: "issue_relation", - }).finally(() => setFetching(false)); - }; - - useEffect(() => { - const filterDates = getCustomDates(selectedDurationFilter, selectedCustomDates); - - fetchWidgetStats(workspaceSlug, dashboardId, { - widget_key: WIDGET_KEY, - issue_type: selectedTab, - ...(filterDates.trim() !== "" ? { target_date: filterDates } : {}), - expand: "issue_relation", - }); - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const filterParams = getRedirectionFilters(selectedTab); - const tabsList = selectedDurationFilter === "none" ? UNFILTERED_ISSUES_TABS_LIST : FILTERED_ISSUES_TABS_LIST; - const selectedTabIndex = tabsList.findIndex((tab) => tab.key === selectedTab); - - if ((!widgetDetails || !widgetStats) && !widgetStatsError) return ; - - return ( - - {widgetStatsError ? ( - - handleUpdateFilters({ - duration: EDurationFilters.NONE, - tab: "pending", - }) - } - /> - ) : ( - widgetStats && ( - <> -
- - Assigned to you - - { - if (val === "custom" && customDates) { - handleUpdateFilters({ - duration: val, - custom_dates: customDates, - }); - return; - } - - if (val === selectedDurationFilter) return; - - let newTab = selectedTab; - // switch to pending tab if target date is changed to none - if (val === "none" && selectedTab !== "completed") newTab = "pending"; - // switch to upcoming tab if target date is changed to other than none - if (val !== "none" && selectedDurationFilter === "none" && selectedTab !== "completed") - newTab = "upcoming"; - - handleUpdateFilters({ - duration: val, - tab: newTab, - }); - }} - /> -
- { - const newSelectedTab = tabsList[i]; - handleUpdateFilters({ tab: newSelectedTab?.key ?? "completed" }); - }} - className="h-full flex flex-col" - > - - - {tabsList.map((tab) => { - if (tab.key !== selectedTab) return null; - - return ( - - - - ); - })} - - - - ) - )} -
- ); -}); diff --git a/web/core/components/dashboard/widgets/created-issues.tsx b/web/core/components/dashboard/widgets/created-issues.tsx deleted file mode 100644 index 47dc6c268..000000000 --- a/web/core/components/dashboard/widgets/created-issues.tsx +++ /dev/null @@ -1,162 +0,0 @@ -import { useEffect, useState } from "react"; -import { observer } from "mobx-react"; -import Link from "next/link"; -import { Tab } from "@headlessui/react"; -import { EDurationFilters, FILTERED_ISSUES_TABS_LIST, UNFILTERED_ISSUES_TABS_LIST } from "@plane/constants"; -import { TCreatedIssuesWidgetFilters, TCreatedIssuesWidgetResponse } from "@plane/types"; -// hooks -import { Card } from "@plane/ui"; -import { - DurationFilterDropdown, - IssuesErrorState, - TabsList, - WidgetIssuesList, - WidgetLoader, - WidgetProps, -} from "@/components/dashboard/widgets"; -import { getCustomDates, getRedirectionFilters, getTabKey } from "@/helpers/dashboard.helper"; -import { useDashboard } from "@/hooks/store"; -// components -// helpers -// types -// constants - -const WIDGET_KEY = "created_issues"; - -export const CreatedIssuesWidget: React.FC = observer((props) => { - const { dashboardId, workspaceSlug } = props; - // states - const [fetching, setFetching] = useState(false); - // store hooks - const { fetchWidgetStats, getWidgetDetails, getWidgetStats, getWidgetStatsError, updateDashboardWidgetFilters } = - useDashboard(); - // derived values - const widgetDetails = getWidgetDetails(workspaceSlug, dashboardId, WIDGET_KEY); - const widgetStats = getWidgetStats(workspaceSlug, dashboardId, WIDGET_KEY); - const widgetStatsError = getWidgetStatsError(workspaceSlug, dashboardId, WIDGET_KEY); - const selectedDurationFilter = widgetDetails?.widget_filters.duration ?? EDurationFilters.NONE; - const selectedTab = getTabKey(selectedDurationFilter, widgetDetails?.widget_filters.tab); - const selectedCustomDates = widgetDetails?.widget_filters.custom_dates ?? []; - - const handleUpdateFilters = async (filters: Partial) => { - if (!widgetDetails) return; - - setFetching(true); - - await updateDashboardWidgetFilters(workspaceSlug, dashboardId, widgetDetails.id, { - widgetKey: WIDGET_KEY, - filters, - }); - - const filterDates = getCustomDates( - filters.duration ?? selectedDurationFilter, - filters.custom_dates ?? selectedCustomDates - ); - fetchWidgetStats(workspaceSlug, dashboardId, { - widget_key: WIDGET_KEY, - issue_type: filters.tab ?? selectedTab, - ...(filterDates.trim() !== "" ? { target_date: filterDates } : {}), - }).finally(() => setFetching(false)); - }; - - useEffect(() => { - const filterDates = getCustomDates(selectedDurationFilter, selectedCustomDates); - - fetchWidgetStats(workspaceSlug, dashboardId, { - widget_key: WIDGET_KEY, - issue_type: selectedTab, - ...(filterDates.trim() !== "" ? { target_date: filterDates } : {}), - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const filterParams = getRedirectionFilters(selectedTab); - const tabsList = selectedDurationFilter === "none" ? UNFILTERED_ISSUES_TABS_LIST : FILTERED_ISSUES_TABS_LIST; - const selectedTabIndex = tabsList.findIndex((tab) => tab.key === selectedTab); - - if ((!widgetDetails || !widgetStats) && !widgetStatsError) return ; - - return ( - - {widgetStatsError ? ( - - handleUpdateFilters({ - duration: EDurationFilters.NONE, - tab: "pending", - }) - } - /> - ) : ( - widgetStats && ( - <> -
- - Created by you - - { - if (val === "custom" && customDates) { - handleUpdateFilters({ - duration: val, - custom_dates: customDates, - }); - return; - } - - if (val === selectedDurationFilter) return; - - let newTab = selectedTab; - // switch to pending tab if target date is changed to none - if (val === "none" && selectedTab !== "completed") newTab = "pending"; - // switch to upcoming tab if target date is changed to other than none - if (val !== "none" && selectedDurationFilter === "none" && selectedTab !== "completed") - newTab = "upcoming"; - - handleUpdateFilters({ - duration: val, - tab: newTab, - }); - }} - /> -
- { - const newSelectedTab = tabsList[i]; - handleUpdateFilters({ tab: newSelectedTab.key ?? "completed" }); - }} - className="h-full flex flex-col" - > - - - {tabsList.map((tab) => { - if (tab.key !== selectedTab) return null; - - return ( - - - - ); - })} - - - - ) - )} -
- ); -}); diff --git a/web/core/components/dashboard/widgets/dropdowns/duration-filter.tsx b/web/core/components/dashboard/widgets/dropdowns/duration-filter.tsx deleted file mode 100644 index 2a897de82..000000000 --- a/web/core/components/dashboard/widgets/dropdowns/duration-filter.tsx +++ /dev/null @@ -1,58 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { ChevronDown } from "lucide-react"; -// components -import { DURATION_FILTER_OPTIONS, EDurationFilters } from "@plane/constants"; -import { CustomMenu } from "@plane/ui"; -import { DateFilterModal } from "@/components/core"; -// ui -// helpers -import { getDurationFilterDropdownLabel } from "@/helpers/dashboard.helper"; -// constants - -type Props = { - customDates?: string[]; - onChange: (value: EDurationFilters, customDates?: string[]) => void; - value: EDurationFilters; -}; - -export const DurationFilterDropdown: React.FC = (props) => { - const { customDates, onChange, value } = props; - // states - const [isDateFilterModalOpen, setIsDateFilterModalOpen] = useState(false); - - return ( - <> - setIsDateFilterModalOpen(false)} - onSelect={(val) => onChange(EDurationFilters.CUSTOM, val)} - title="Due date" - /> - - {getDurationFilterDropdownLabel(value, customDates ?? [])} - -
- } - placement="bottom-end" - closeOnSelect - > - {DURATION_FILTER_OPTIONS.map((option) => ( - { - if (option.key === "custom") setIsDateFilterModalOpen(true); - else onChange(option.key); - }} - > - {option.label} - - ))} - - - ); -}; diff --git a/web/core/components/dashboard/widgets/dropdowns/index.ts b/web/core/components/dashboard/widgets/dropdowns/index.ts deleted file mode 100644 index cff4cdb44..000000000 --- a/web/core/components/dashboard/widgets/dropdowns/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./duration-filter"; diff --git a/web/core/components/dashboard/widgets/empty-states/assigned-issues.tsx b/web/core/components/dashboard/widgets/empty-states/assigned-issues.tsx deleted file mode 100644 index 169547846..000000000 --- a/web/core/components/dashboard/widgets/empty-states/assigned-issues.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import Image from "next/image"; -import { useTheme } from "next-themes"; -import { TIssuesListTypes } from "@plane/types"; -import CompletedIssuesDark from "@/public/empty-state/dashboard/dark/completed-issues.svg"; -import OverdueIssuesDark from "@/public/empty-state/dashboard/dark/overdue-issues.svg"; -import UpcomingIssuesDark from "@/public/empty-state/dashboard/dark/upcoming-issues.svg"; -import CompletedIssuesLight from "@/public/empty-state/dashboard/light/completed-issues.svg"; -import OverdueIssuesLight from "@/public/empty-state/dashboard/light/overdue-issues.svg"; -import UpcomingIssuesLight from "@/public/empty-state/dashboard/light/upcoming-issues.svg"; - -export const ASSIGNED_ISSUES_EMPTY_STATES = { - pending: { - title: "Work items assigned to you that are pending\nwill show up here.", - darkImage: UpcomingIssuesDark, - lightImage: UpcomingIssuesLight, - }, - upcoming: { - title: "Upcoming work items assigned to\nyou will show up here.", - darkImage: UpcomingIssuesDark, - lightImage: UpcomingIssuesLight, - }, - overdue: { - title: "Work items assigned to you that are past\ntheir due date will show up here.", - darkImage: OverdueIssuesDark, - lightImage: OverdueIssuesLight, - }, - completed: { - title: "Work items assigned to you that you have\nmarked Completed will show up here.", - darkImage: CompletedIssuesDark, - lightImage: CompletedIssuesLight, - }, -}; -type Props = { - type: TIssuesListTypes; -}; - -export const AssignedIssuesEmptyState: React.FC = (props) => { - const { type } = props; - // next-themes - const { resolvedTheme } = useTheme(); - - const typeDetails = ASSIGNED_ISSUES_EMPTY_STATES[type]; - - const image = resolvedTheme === "dark" ? typeDetails.darkImage : typeDetails.lightImage; - - // TODO: update empty state logic to use a general component - return ( -
-
- Assigned work items -
-

{typeDetails.title}

-
- ); -}; diff --git a/web/core/components/dashboard/widgets/empty-states/created-issues.tsx b/web/core/components/dashboard/widgets/empty-states/created-issues.tsx deleted file mode 100644 index 49a96f1fb..000000000 --- a/web/core/components/dashboard/widgets/empty-states/created-issues.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import Image from "next/image"; -import { useTheme } from "next-themes"; -import { TIssuesListTypes } from "@plane/types"; -import CompletedIssuesDark from "@/public/empty-state/dashboard/dark/completed-issues.svg"; -import OverdueIssuesDark from "@/public/empty-state/dashboard/dark/overdue-issues.svg"; -import UpcomingIssuesDark from "@/public/empty-state/dashboard/dark/upcoming-issues.svg"; -import CompletedIssuesLight from "@/public/empty-state/dashboard/light/completed-issues.svg"; -import OverdueIssuesLight from "@/public/empty-state/dashboard/light/overdue-issues.svg"; -import UpcomingIssuesLight from "@/public/empty-state/dashboard/light/upcoming-issues.svg"; - -export const CREATED_ISSUES_EMPTY_STATES = { - pending: { - title: "Work items created by you that are pending\nwill show up here.", - darkImage: UpcomingIssuesDark, - lightImage: UpcomingIssuesLight, - }, - upcoming: { - title: "Upcoming work items you created\nwill show up here.", - darkImage: UpcomingIssuesDark, - lightImage: UpcomingIssuesLight, - }, - overdue: { - title: "Work items created by you that are past their\ndue date will show up here.", - darkImage: OverdueIssuesDark, - lightImage: OverdueIssuesLight, - }, - completed: { - title: "Work items created by you that you have\nmarked completed will show up here.", - darkImage: CompletedIssuesDark, - lightImage: CompletedIssuesLight, - }, -}; - -type Props = { - type: TIssuesListTypes; -}; - -export const CreatedIssuesEmptyState: React.FC = (props) => { - const { type } = props; - // next-themes - const { resolvedTheme } = useTheme(); - - const typeDetails = CREATED_ISSUES_EMPTY_STATES[type]; - - const image = resolvedTheme === "dark" ? typeDetails.darkImage : typeDetails.lightImage; - - return ( -
-
- Assigned work items -
-

{typeDetails.title}

-
- ); -}; diff --git a/web/core/components/dashboard/widgets/empty-states/index.ts b/web/core/components/dashboard/widgets/empty-states/index.ts deleted file mode 100644 index 72ca1dbb2..000000000 --- a/web/core/components/dashboard/widgets/empty-states/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export * from "./assigned-issues"; -export * from "./created-issues"; -export * from "./issues-by-priority"; -export * from "./issues-by-state-group"; -export * from "./recent-activity"; -export * from "./recent-collaborators"; diff --git a/web/core/components/dashboard/widgets/empty-states/issues-by-priority.tsx b/web/core/components/dashboard/widgets/empty-states/issues-by-priority.tsx deleted file mode 100644 index 7f70d3bf3..000000000 --- a/web/core/components/dashboard/widgets/empty-states/issues-by-priority.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import Image from "next/image"; -import { useTheme } from "next-themes"; -// assets -import DarkImage from "@/public/empty-state/dashboard/dark/issues-by-priority.svg"; -import LightImage from "@/public/empty-state/dashboard/light/issues-by-priority.svg"; - -export const IssuesByPriorityEmptyState = () => { - // next-themes - const { resolvedTheme } = useTheme(); - - const image = resolvedTheme === "dark" ? DarkImage : LightImage; - - return ( -
-
- Work items by state group -
-

- Work items assigned to you, broken down by -
- priority will show up here. -

-
- ); -}; diff --git a/web/core/components/dashboard/widgets/empty-states/issues-by-state-group.tsx b/web/core/components/dashboard/widgets/empty-states/issues-by-state-group.tsx deleted file mode 100644 index 69d4761e5..000000000 --- a/web/core/components/dashboard/widgets/empty-states/issues-by-state-group.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import Image from "next/image"; -import { useTheme } from "next-themes"; -// assets -import DarkImage from "@/public/empty-state/dashboard/dark/issues-by-state-group.svg"; -import LightImage from "@/public/empty-state/dashboard/light/issues-by-state-group.svg"; - -export const IssuesByStateGroupEmptyState = () => { - // next-themes - const { resolvedTheme } = useTheme(); - - const image = resolvedTheme === "dark" ? DarkImage : LightImage; - - return ( -
-
- Work items by state group -
-

- Work items assigned to you, broken down by state, -
- will show up here. -

-
- ); -}; diff --git a/web/core/components/dashboard/widgets/empty-states/recent-activity.tsx b/web/core/components/dashboard/widgets/empty-states/recent-activity.tsx deleted file mode 100644 index f25f39920..000000000 --- a/web/core/components/dashboard/widgets/empty-states/recent-activity.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import Image from "next/image"; -import { useTheme } from "next-themes"; -// assets -import DarkImage from "@/public/empty-state/dashboard/dark/recent-activity.svg"; -import LightImage from "@/public/empty-state/dashboard/light/recent-activity.svg"; - -export const RecentActivityEmptyState = () => { - // next-themes - const { resolvedTheme } = useTheme(); - - const image = resolvedTheme === "dark" ? DarkImage : LightImage; - - return ( -
-
- Work items by state group -
-

- All your work items activities across -
- projects will show up here. -

-
- ); -}; diff --git a/web/core/components/dashboard/widgets/empty-states/recent-collaborators.tsx b/web/core/components/dashboard/widgets/empty-states/recent-collaborators.tsx deleted file mode 100644 index d1a1200aa..000000000 --- a/web/core/components/dashboard/widgets/empty-states/recent-collaborators.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import Image from "next/image"; -import { useTheme } from "next-themes"; -// assets -import DarkImage1 from "@/public/empty-state/dashboard/dark/recent-collaborators-1.svg"; -import DarkImage2 from "@/public/empty-state/dashboard/dark/recent-collaborators-2.svg"; -import DarkImage3 from "@/public/empty-state/dashboard/dark/recent-collaborators-3.svg"; -import LightImage1 from "@/public/empty-state/dashboard/light/recent-collaborators-1.svg"; -import LightImage2 from "@/public/empty-state/dashboard/light/recent-collaborators-2.svg"; -import LightImage3 from "@/public/empty-state/dashboard/light/recent-collaborators-3.svg"; - -export const RecentCollaboratorsEmptyState = () => { - // next-themes - const { resolvedTheme } = useTheme(); - - const image1 = resolvedTheme === "dark" ? DarkImage1 : LightImage1; - const image2 = resolvedTheme === "dark" ? DarkImage2 : LightImage2; - const image3 = resolvedTheme === "dark" ? DarkImage3 : LightImage3; - - return ( -
-

- Compare your activities with the top -
- seven in your project. -

-
-
- Recent collaborators -
-
- Recent collaborators -
-
- Recent collaborators -
-
-
- ); -}; diff --git a/web/core/components/dashboard/widgets/error-states/index.ts b/web/core/components/dashboard/widgets/error-states/index.ts deleted file mode 100644 index bd8854f37..000000000 --- a/web/core/components/dashboard/widgets/error-states/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./issues"; diff --git a/web/core/components/dashboard/widgets/error-states/issues.tsx b/web/core/components/dashboard/widgets/error-states/issues.tsx deleted file mode 100644 index fb3e3550b..000000000 --- a/web/core/components/dashboard/widgets/error-states/issues.tsx +++ /dev/null @@ -1,34 +0,0 @@ -"use client"; - -import { AlertTriangle, RefreshCcw } from "lucide-react"; -// ui -import { Button } from "@plane/ui"; - -type Props = { - isRefreshing: boolean; - onClick: () => void; -}; - -export const IssuesErrorState: React.FC = (props) => { - const { isRefreshing, onClick } = props; - - return ( -
-
-
- -
-

There was an error in fetching widget details

- -
-
- ); -}; diff --git a/web/core/components/dashboard/widgets/index.ts b/web/core/components/dashboard/widgets/index.ts deleted file mode 100644 index e622d708f..000000000 --- a/web/core/components/dashboard/widgets/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -export * from "./dropdowns"; -export * from "./empty-states"; -export * from "./error-states"; -export * from "./issue-panels"; -export * from "./loaders"; -export * from "./assigned-issues"; -export * from "./created-issues"; -export * from "./overview-stats"; -export * from "./recent-activity"; -export * from "./recent-collaborators"; -export * from "./recent-projects"; diff --git a/web/core/components/dashboard/widgets/issue-panels/index.ts b/web/core/components/dashboard/widgets/issue-panels/index.ts deleted file mode 100644 index f5b7d53d4..000000000 --- a/web/core/components/dashboard/widgets/issue-panels/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./issue-list-item"; -export * from "./issues-list"; -export * from "./tabs-list"; diff --git a/web/core/components/dashboard/widgets/issue-panels/issue-list-item.tsx b/web/core/components/dashboard/widgets/issue-panels/issue-list-item.tsx deleted file mode 100644 index 75471c6fd..000000000 --- a/web/core/components/dashboard/widgets/issue-panels/issue-list-item.tsx +++ /dev/null @@ -1,401 +0,0 @@ -"use client"; - -import { isToday } from "date-fns/isToday"; -import { observer } from "mobx-react"; -// types -import { TIssue, TWidgetIssue } from "@plane/types"; -// ui -import { Avatar, AvatarGroup, ControlLink, PriorityIcon } from "@plane/ui"; -// helpers -import { findTotalDaysInRange, getDate, renderFormattedDate } from "@/helpers/date-time.helper"; -import { getFileURL } from "@/helpers/file.helper"; -import { generateWorkItemLink } from "@/helpers/issue.helper"; -// hooks -import { useIssueDetail, useMember, useProject } from "@/hooks/store"; -// plane web components -import { IssueIdentifier } from "@/plane-web/components/issues"; - -export type IssueListItemProps = { - issueId: string; - onClick: (issue: TIssue) => void; - workspaceSlug: string; -}; - -export const AssignedUpcomingIssueListItem: React.FC = observer((props) => { - const { issueId, onClick, workspaceSlug } = props; - // store hooks - const { getProjectById } = useProject(); - const { - issue: { getIssueById }, - } = useIssueDetail(); - // derived values - const issueDetails = getIssueById(issueId) as TWidgetIssue | undefined; - - if (!issueDetails || !issueDetails.project_id) return null; - - const projectDetails = getProjectById(issueDetails.project_id); - - const blockedByIssues = issueDetails.issue_relation?.filter((issue) => issue.relation_type === "blocked_by") ?? []; - - const blockedByIssueProjectDetails = - blockedByIssues.length === 1 ? getProjectById(blockedByIssues[0]?.project_id ?? "") : null; - - const targetDate = getDate(issueDetails.target_date); - - const workItemLink = generateWorkItemLink({ - workspaceSlug, - projectId: issueDetails?.project_id, - issueId: issueDetails?.id, - projectIdentifier: projectDetails?.identifier, - sequenceId: issueDetails?.sequence_id, - }); - - return ( - onClick(issueDetails)} - className="grid grid-cols-12 gap-1 rounded px-3 py-2 hover:bg-custom-background-80" - > -
- {projectDetails && ( - - )} -
{issueDetails.name}
-
-
- -
-
- {targetDate ? (isToday(targetDate) ? "Today" : renderFormattedDate(targetDate)) : "-"} -
-
- {blockedByIssues.length > 0 - ? blockedByIssues.length > 1 - ? `${blockedByIssues.length} blockers` - : blockedByIssueProjectDetails && ( - - ) - : "-"} -
-
- ); -}); - -export const AssignedOverdueIssueListItem: React.FC = observer((props) => { - const { issueId, onClick, workspaceSlug } = props; - // store hooks - const { getProjectById } = useProject(); - const { - issue: { getIssueById }, - } = useIssueDetail(); - // derived values - const issueDetails = getIssueById(issueId) as TWidgetIssue | undefined; - - if (!issueDetails || !issueDetails.project_id) return null; - - const projectDetails = getProjectById(issueDetails.project_id); - const blockedByIssues = issueDetails.issue_relation?.filter((issue) => issue.relation_type === "blocked_by") ?? []; - - const blockedByIssueProjectDetails = - blockedByIssues.length === 1 ? getProjectById(blockedByIssues[0]?.project_id ?? "") : null; - - const dueBy = findTotalDaysInRange(getDate(issueDetails.target_date), new Date(), false) ?? 0; - - const workItemLink = generateWorkItemLink({ - workspaceSlug, - projectId: issueDetails?.project_id, - issueId: issueDetails?.id, - projectIdentifier: projectDetails?.identifier, - sequenceId: issueDetails?.sequence_id, - }); - - return ( - onClick(issueDetails)} - className="grid grid-cols-12 gap-1 rounded px-3 py-2 hover:bg-custom-background-80" - > -
- {projectDetails && ( - - )} -
{issueDetails.name}
-
-
- -
-
- {dueBy} {`day${dueBy > 1 ? "s" : ""}`} -
-
- {blockedByIssues.length > 0 - ? blockedByIssues.length > 1 - ? `${blockedByIssues.length} blockers` - : blockedByIssueProjectDetails && ( - - ) - : "-"} -
-
- ); -}); - -export const AssignedCompletedIssueListItem: React.FC = observer((props) => { - const { issueId, onClick, workspaceSlug } = props; - // store hooks - const { - issue: { getIssueById }, - } = useIssueDetail(); - const { getProjectById } = useProject(); - // derived values - const issueDetails = getIssueById(issueId); - - if (!issueDetails || !issueDetails.project_id) return null; - - const projectDetails = getProjectById(issueDetails.project_id); - - const workItemLink = generateWorkItemLink({ - workspaceSlug, - projectId: issueDetails?.project_id, - issueId: issueDetails?.id, - projectIdentifier: projectDetails?.identifier, - sequenceId: issueDetails?.sequence_id, - }); - - return ( - onClick(issueDetails)} - className="grid grid-cols-12 gap-1 rounded px-3 py-2 hover:bg-custom-background-80" - > -
- {projectDetails && ( - - )} -
{issueDetails.name}
-
-
- -
-
- ); -}); - -export const CreatedUpcomingIssueListItem: React.FC = observer((props) => { - const { issueId, onClick, workspaceSlug } = props; - // store hooks - const { getUserDetails } = useMember(); - const { - issue: { getIssueById }, - } = useIssueDetail(); - const { getProjectById } = useProject(); - // derived values - const issue = getIssueById(issueId); - - if (!issue || !issue.project_id) return null; - - const projectDetails = getProjectById(issue.project_id); - const targetDate = getDate(issue.target_date); - - const workItemLink = generateWorkItemLink({ - workspaceSlug, - projectId: issue?.project_id, - issueId: issue?.id, - projectIdentifier: projectDetails?.identifier, - sequenceId: issue?.sequence_id, - }); - - return ( - onClick(issue)} - className="grid grid-cols-12 gap-1 rounded px-3 py-2 hover:bg-custom-background-80" - > -
- {projectDetails && ( - - )} -
{issue.name}
-
-
- -
-
- {targetDate ? (isToday(targetDate) ? "Today" : renderFormattedDate(targetDate)) : "-"} -
-
- {issue.assignee_ids && issue.assignee_ids?.length > 0 ? ( - - {issue.assignee_ids?.map((assigneeId) => { - const userDetails = getUserDetails(assigneeId); - - if (!userDetails) return null; - - return ( - - ); - })} - - ) : ( - "-" - )} -
-
- ); -}); - -export const CreatedOverdueIssueListItem: React.FC = observer((props) => { - const { issueId, onClick, workspaceSlug } = props; - // store hooks - const { getUserDetails } = useMember(); - const { - issue: { getIssueById }, - } = useIssueDetail(); - const { getProjectById } = useProject(); - // derived values - const issue = getIssueById(issueId); - - if (!issue || !issue.project_id) return null; - - const projectDetails = getProjectById(issue.project_id); - - const dueBy: number = findTotalDaysInRange(getDate(issue.target_date), new Date(), false) ?? 0; - - const workItemLink = generateWorkItemLink({ - workspaceSlug, - projectId: issue?.project_id, - issueId: issue?.id, - projectIdentifier: projectDetails?.identifier, - sequenceId: issue?.sequence_id, - }); - - return ( - onClick(issue)} - className="grid grid-cols-12 gap-1 rounded px-3 py-2 hover:bg-custom-background-80" - > -
- {projectDetails && ( - - )} -
{issue.name}
-
-
- -
-
- {dueBy} {`day${dueBy > 1 ? "s" : ""}`} -
-
- {issue.assignee_ids.length > 0 ? ( - - {issue.assignee_ids?.map((assigneeId) => { - const userDetails = getUserDetails(assigneeId); - - if (!userDetails) return null; - - return ( - - ); - })} - - ) : ( - "-" - )} -
-
- ); -}); - -export const CreatedCompletedIssueListItem: React.FC = observer((props) => { - const { issueId, onClick, workspaceSlug } = props; - // store hooks - const { getUserDetails } = useMember(); - const { - issue: { getIssueById }, - } = useIssueDetail(); - const { getProjectById } = useProject(); - // derived values - const issue = getIssueById(issueId); - - if (!issue || !issue.project_id) return null; - - const projectDetails = getProjectById(issue.project_id); - - const workItemLink = generateWorkItemLink({ - workspaceSlug, - projectId: issue?.project_id, - issueId: issue?.id, - projectIdentifier: projectDetails?.identifier, - sequenceId: issue?.sequence_id, - }); - - return ( - onClick(issue)} - className="grid grid-cols-12 gap-1 rounded px-3 py-2 hover:bg-custom-background-80" - > -
- {projectDetails && ( - - )} -
{issue.name}
-
-
- -
-
- {issue.assignee_ids.length > 0 ? ( - - {issue.assignee_ids?.map((assigneeId) => { - const userDetails = getUserDetails(assigneeId); - - if (!userDetails) return null; - - return ( - - ); - })} - - ) : ( - "-" - )} -
-
- ); -}); diff --git a/web/core/components/dashboard/widgets/issue-panels/issues-list.tsx b/web/core/components/dashboard/widgets/issue-panels/issues-list.tsx deleted file mode 100644 index 925e2ee9a..000000000 --- a/web/core/components/dashboard/widgets/issue-panels/issues-list.tsx +++ /dev/null @@ -1,134 +0,0 @@ -"use client"; - -import Link from "next/link"; -import { TAssignedIssuesWidgetResponse, TCreatedIssuesWidgetResponse, TIssue, TIssuesListTypes } from "@plane/types"; -// hooks -// components -import { Loader, getButtonStyling } from "@plane/ui"; -import { - AssignedCompletedIssueListItem, - AssignedIssuesEmptyState, - AssignedOverdueIssueListItem, - AssignedUpcomingIssueListItem, - CreatedCompletedIssueListItem, - CreatedIssuesEmptyState, - CreatedOverdueIssueListItem, - CreatedUpcomingIssueListItem, - IssueListItemProps, -} from "@/components/dashboard/widgets"; -// ui -// helpers -import { cn } from "@/helpers/common.helper"; -import { getRedirectionFilters } from "@/helpers/dashboard.helper"; -// hooks -import useIssuePeekOverviewRedirection from "@/hooks/use-issue-peek-overview-redirection"; -import { usePlatformOS } from "@/hooks/use-platform-os"; - -export type WidgetIssuesListProps = { - isLoading: boolean; - tab: TIssuesListTypes; - type: "assigned" | "created"; - widgetStats: TAssignedIssuesWidgetResponse | TCreatedIssuesWidgetResponse; - workspaceSlug: string; -}; - -export const WidgetIssuesList: React.FC = (props) => { - const { isLoading, tab, type, widgetStats, workspaceSlug } = props; - // hooks - const { isMobile } = usePlatformOS(); - const { handleRedirection } = useIssuePeekOverviewRedirection(); - - // handlers - const handleIssuePeekOverview = (issue: TIssue) => handleRedirection(workspaceSlug, issue, isMobile); - - const filterParams = getRedirectionFilters(tab); - - const ISSUE_LIST_ITEM: { - [key: string]: { - [key in TIssuesListTypes]: React.FC; - }; - } = { - assigned: { - pending: AssignedUpcomingIssueListItem, - upcoming: AssignedUpcomingIssueListItem, - overdue: AssignedOverdueIssueListItem, - completed: AssignedCompletedIssueListItem, - }, - created: { - pending: CreatedUpcomingIssueListItem, - upcoming: CreatedUpcomingIssueListItem, - overdue: CreatedOverdueIssueListItem, - completed: CreatedCompletedIssueListItem, - }, - }; - - const issuesList = widgetStats.issues; - - return ( - <> -
- {isLoading ? ( - - - - - - - ) : issuesList.length > 0 ? ( - <> -
-
- Work items - - {widgetStats.count} - -
-
Priority
- {["upcoming", "pending"].includes(tab) &&
Due date
} - {tab === "overdue" &&
Due by
} - {type === "assigned" && tab !== "completed" &&
Blocked by
} - {type === "created" &&
Assigned to
} -
-
- {issuesList.map((issue) => { - const IssueListItem = ISSUE_LIST_ITEM[type][tab]; - - if (!IssueListItem) return null; - - return ( - - ); - })} -
- - ) : ( -
- {type === "assigned" && } - {type === "created" && } -
- )} -
- {!isLoading && issuesList.length > 0 && ( - - View all work items - - )} - - ); -}; diff --git a/web/core/components/dashboard/widgets/issue-panels/tabs-list.tsx b/web/core/components/dashboard/widgets/issue-panels/tabs-list.tsx deleted file mode 100644 index 9df044c37..000000000 --- a/web/core/components/dashboard/widgets/issue-panels/tabs-list.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { observer } from "mobx-react"; -import { Tab } from "@headlessui/react"; -// helpers -import { EDurationFilters, FILTERED_ISSUES_TABS_LIST, UNFILTERED_ISSUES_TABS_LIST } from "@plane/constants"; -import { TIssuesListTypes } from "@plane/types"; -import { cn } from "@/helpers/common.helper"; -// types -// constants - -type Props = { - durationFilter: EDurationFilters; - selectedTab: TIssuesListTypes; -}; - -export const TabsList: React.FC = observer((props) => { - const { durationFilter, selectedTab } = props; - - const tabsList = durationFilter === "none" ? UNFILTERED_ISSUES_TABS_LIST : FILTERED_ISSUES_TABS_LIST; - const selectedTabIndex = tabsList.findIndex((tab) => tab.key === selectedTab); - - return ( - -
- {tabsList.map((tab) => ( - - {tab.label} - - ))} - - ); -}); diff --git a/web/core/components/dashboard/widgets/loaders/assigned-issues.tsx b/web/core/components/dashboard/widgets/loaders/assigned-issues.tsx deleted file mode 100644 index 8a78fedf1..000000000 --- a/web/core/components/dashboard/widgets/loaders/assigned-issues.tsx +++ /dev/null @@ -1,24 +0,0 @@ -"use client"; - -// ui -import { Loader } from "@plane/ui"; - -export const AssignedIssuesWidgetLoader = () => ( - -
- - -
-
- - -
-
- - - - - -
-
-); diff --git a/web/core/components/dashboard/widgets/loaders/index.ts b/web/core/components/dashboard/widgets/loaders/index.ts deleted file mode 100644 index ee5286f0f..000000000 --- a/web/core/components/dashboard/widgets/loaders/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./loader"; diff --git a/web/core/components/dashboard/widgets/loaders/issues-by-priority.tsx b/web/core/components/dashboard/widgets/loaders/issues-by-priority.tsx deleted file mode 100644 index c6f075b58..000000000 --- a/web/core/components/dashboard/widgets/loaders/issues-by-priority.tsx +++ /dev/null @@ -1,17 +0,0 @@ -"use client"; - -// ui -import { Loader } from "@plane/ui"; - -export const IssuesByPriorityWidgetLoader = () => ( - - -
- - - - - -
-
-); diff --git a/web/core/components/dashboard/widgets/loaders/issues-by-state-group.tsx b/web/core/components/dashboard/widgets/loaders/issues-by-state-group.tsx deleted file mode 100644 index 099323cce..000000000 --- a/web/core/components/dashboard/widgets/loaders/issues-by-state-group.tsx +++ /dev/null @@ -1,24 +0,0 @@ -"use client"; - -import range from "lodash/range"; -// ui -import { Loader } from "@plane/ui"; - -export const IssuesByStateGroupWidgetLoader = () => ( - - -
-
-
- -
-
-
-
- {range(5).map((index) => ( - - ))} -
-
- -); diff --git a/web/core/components/dashboard/widgets/loaders/loader.tsx b/web/core/components/dashboard/widgets/loaders/loader.tsx deleted file mode 100644 index ae4038b38..000000000 --- a/web/core/components/dashboard/widgets/loaders/loader.tsx +++ /dev/null @@ -1,31 +0,0 @@ -// components -import { TWidgetKeys } from "@plane/types"; -import { AssignedIssuesWidgetLoader } from "./assigned-issues"; -import { IssuesByPriorityWidgetLoader } from "./issues-by-priority"; -import { IssuesByStateGroupWidgetLoader } from "./issues-by-state-group"; -import { OverviewStatsWidgetLoader } from "./overview-stats"; -import { RecentActivityWidgetLoader } from "./recent-activity"; -import { RecentCollaboratorsWidgetLoader } from "./recent-collaborators"; -import { RecentProjectsWidgetLoader } from "./recent-projects"; -// types - -type Props = { - widgetKey: TWidgetKeys; -}; - -export const WidgetLoader: React.FC = (props) => { - const { widgetKey } = props; - - const loaders = { - overview_stats: , - assigned_issues: , - created_issues: , - issues_by_state_groups: , - issues_by_priority: , - recent_activity: , - recent_projects: , - recent_collaborators: , - }; - - return loaders[widgetKey]; -}; diff --git a/web/core/components/dashboard/widgets/loaders/overview-stats.tsx b/web/core/components/dashboard/widgets/loaders/overview-stats.tsx deleted file mode 100644 index e780bb399..000000000 --- a/web/core/components/dashboard/widgets/loaders/overview-stats.tsx +++ /dev/null @@ -1,16 +0,0 @@ -"use client"; - -import range from "lodash/range"; -// ui -import { Loader } from "@plane/ui"; - -export const OverviewStatsWidgetLoader = () => ( - - {range(4).map((index) => ( -
- - -
- ))} -
-); diff --git a/web/core/components/dashboard/widgets/loaders/recent-activity.tsx b/web/core/components/dashboard/widgets/loaders/recent-activity.tsx deleted file mode 100644 index 2df78a15a..000000000 --- a/web/core/components/dashboard/widgets/loaders/recent-activity.tsx +++ /dev/null @@ -1,22 +0,0 @@ -"use client"; - -import range from "lodash/range"; -// ui -import { Loader } from "@plane/ui"; - -export const RecentActivityWidgetLoader = () => ( - - - {range(7).map((index) => ( -
-
- -
-
- - -
-
- ))} -
-); diff --git a/web/core/components/dashboard/widgets/loaders/recent-collaborators.tsx b/web/core/components/dashboard/widgets/loaders/recent-collaborators.tsx deleted file mode 100644 index 2dceaf132..000000000 --- a/web/core/components/dashboard/widgets/loaders/recent-collaborators.tsx +++ /dev/null @@ -1,20 +0,0 @@ -"use client"; - -import range from "lodash/range"; -// ui -import { Loader } from "@plane/ui"; - -export const RecentCollaboratorsWidgetLoader = () => ( - <> - {range(8).map((index) => ( - -
-
- -
- -
-
- ))} - -); diff --git a/web/core/components/dashboard/widgets/loaders/recent-projects.tsx b/web/core/components/dashboard/widgets/loaders/recent-projects.tsx deleted file mode 100644 index 38bc7e29a..000000000 --- a/web/core/components/dashboard/widgets/loaders/recent-projects.tsx +++ /dev/null @@ -1,22 +0,0 @@ -"use client"; - -import range from "lodash/range"; -// ui -import { Loader } from "@plane/ui"; - -export const RecentProjectsWidgetLoader = () => ( - - - {range(5).map((index) => ( -
-
- -
-
- - -
-
- ))} -
-); diff --git a/web/core/components/dashboard/widgets/overview-stats.tsx b/web/core/components/dashboard/widgets/overview-stats.tsx deleted file mode 100644 index 20421f33b..000000000 --- a/web/core/components/dashboard/widgets/overview-stats.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import { useEffect } from "react"; -import { observer } from "mobx-react"; -import Link from "next/link"; -import { TOverviewStatsWidgetResponse } from "@plane/types"; -// hooks -import { Card, ECardSpacing } from "@plane/ui"; -import { WidgetLoader } from "@/components/dashboard/widgets"; -import { cn } from "@/helpers/common.helper"; -import { renderFormattedPayloadDate } from "@/helpers/date-time.helper"; -import { useDashboard } from "@/hooks/store"; -// components -// helpers -// types - -export type WidgetProps = { - dashboardId: string; - workspaceSlug: string; -}; - -const WIDGET_KEY = "overview_stats"; - -export const OverviewStatsWidget: React.FC = observer((props) => { - const { dashboardId, workspaceSlug } = props; - // store hooks - const { fetchWidgetStats, getWidgetStats } = useDashboard(); - // derived values - const widgetStats = getWidgetStats(workspaceSlug, dashboardId, WIDGET_KEY); - - const today = renderFormattedPayloadDate(new Date()); - const STATS_LIST = [ - { - key: "assigned", - title: "Work items assigned", - count: widgetStats?.assigned_issues_count, - link: `/${workspaceSlug}/workspace-views/assigned`, - }, - { - key: "overdue", - title: "Work items overdue", - count: widgetStats?.pending_issues_count, - link: `/${workspaceSlug}/workspace-views/assigned/?state_group=backlog,unstarted,started&target_date=${today};before`, - }, - { - key: "created", - title: "Work items created", - count: widgetStats?.created_issues_count, - link: `/${workspaceSlug}/workspace-views/created`, - }, - { - key: "completed", - title: "Work items completed", - count: widgetStats?.completed_issues_count, - link: `/${workspaceSlug}/workspace-views/assigned?state_group=completed`, - }, - ]; - - useEffect(() => { - fetchWidgetStats(workspaceSlug, dashboardId, { - widget_key: WIDGET_KEY, - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - if (!widgetStats) return ; - - return ( - - {STATS_LIST.map((stat, index) => ( -
- -
-
-
{stat.count}
-

{stat.title}

-
-
- -
- ))} -
- ); -}); diff --git a/web/core/components/dashboard/widgets/recent-activity.tsx b/web/core/components/dashboard/widgets/recent-activity.tsx deleted file mode 100644 index 84ac985e1..000000000 --- a/web/core/components/dashboard/widgets/recent-activity.tsx +++ /dev/null @@ -1,109 +0,0 @@ -"use client"; - -import { useEffect } from "react"; -import { observer } from "mobx-react"; -import Link from "next/link"; -import { History } from "lucide-react"; -// types -import { TRecentActivityWidgetResponse } from "@plane/types"; -// components -import { Card, Avatar, getButtonStyling } from "@plane/ui"; -import { ActivityIcon, ActivityMessage, IssueLink } from "@/components/core"; -import { RecentActivityEmptyState, WidgetLoader, WidgetProps } from "@/components/dashboard/widgets"; -// helpers -import { cn } from "@/helpers/common.helper"; -import { calculateTimeAgo } from "@/helpers/date-time.helper"; -import { getFileURL } from "@/helpers/file.helper"; -// hooks -import { useDashboard, useUser } from "@/hooks/store"; - -const WIDGET_KEY = "recent_activity"; - -export const RecentActivityWidget: React.FC = observer((props) => { - const { dashboardId, workspaceSlug } = props; - // store hooks - const { data: currentUser } = useUser(); - // derived values - const { fetchWidgetStats, getWidgetStats } = useDashboard(); - const widgetStats = getWidgetStats(workspaceSlug, dashboardId, WIDGET_KEY); - const redirectionLink = `/${workspaceSlug}/profile/${currentUser?.id}/activity`; - - useEffect(() => { - fetchWidgetStats(workspaceSlug, dashboardId, { - widget_key: WIDGET_KEY, - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - if (!widgetStats) return ; - - return ( - - - Your work item activities - - {widgetStats.length > 0 ? ( -
- {widgetStats.map((activity) => ( -
-
- {activity.field ? ( - activity.new_value === "restore" ? ( - - ) : ( -
- -
- ) - ) : activity.actor_detail.avatar_url && activity.actor_detail.avatar_url !== "" ? ( - - ) : ( -
- {activity.actor_detail.is_bot - ? activity.actor_detail.first_name.charAt(0) - : activity.actor_detail.display_name.charAt(0)} -
- )} -
-
-

- - {currentUser?.id === activity.actor_detail.id ? "You" : activity.actor_detail?.display_name}{" "} - - {activity.field ? ( - - ) : ( - - created - - )} -

-

- {calculateTimeAgo(activity.created_at)} -

-
-
- ))} - - View all - -
- ) : ( -
- -
- )} -
- ); -}); diff --git a/web/core/components/dashboard/widgets/recent-collaborators/collaborators-list.tsx b/web/core/components/dashboard/widgets/recent-collaborators/collaborators-list.tsx deleted file mode 100644 index 415809e04..000000000 --- a/web/core/components/dashboard/widgets/recent-collaborators/collaborators-list.tsx +++ /dev/null @@ -1,154 +0,0 @@ -"use client"; -import { useState } from "react"; -import sortBy from "lodash/sortBy"; -import { observer } from "mobx-react"; -import Link from "next/link"; -import useSWR from "swr"; -// types -import { TRecentCollaboratorsWidgetResponse } from "@plane/types"; -// ui -import { Avatar } from "@plane/ui"; -// helpers -import { getFileURL } from "@/helpers/file.helper"; -// hooks -import { useDashboard, useMember, useUser } from "@/hooks/store"; -// components -import { WidgetLoader } from "../loaders"; - -type CollaboratorListItemProps = { - issueCount: number; - userId: string; - workspaceSlug: string; -}; - -const CollaboratorListItem: React.FC = observer((props) => { - const { issueCount, userId, workspaceSlug } = props; - // store hooks - const { data: currentUser } = useUser(); - const { getUserDetails } = useMember(); - // derived values - const userDetails = getUserDetails(userId); - const isCurrentUser = userId === currentUser?.id; - - if (!userDetails || userDetails.is_bot) return null; - - return ( - -
- -
-
- {isCurrentUser ? "You" : userDetails?.display_name} -
-

- {issueCount} active work items{issueCount > 1 ? "s" : ""} -

- - ); -}); - -type CollaboratorsListProps = { - dashboardId: string; - searchQuery?: string; - workspaceSlug: string; -}; - -const WIDGET_KEY = "recent_collaborators"; - -export const CollaboratorsList: React.FC = (props) => { - const { dashboardId, searchQuery = "", workspaceSlug } = props; - - // state - const [visibleItems, setVisibleItems] = useState(16); - const [isExpanded, setIsExpanded] = useState(false); - // store hooks - const { fetchWidgetStats } = useDashboard(); - const { getUserDetails } = useMember(); - const { data: currentUser } = useUser(); - - const { data: widgetStats } = useSWR( - workspaceSlug && dashboardId ? `WIDGET_STATS_${workspaceSlug}_${dashboardId}` : null, - workspaceSlug && dashboardId - ? () => - fetchWidgetStats(workspaceSlug, dashboardId, { - widget_key: WIDGET_KEY, - }) - : null - ) as { - data: TRecentCollaboratorsWidgetResponse[] | undefined; - }; - - if (!widgetStats) - return ( -
- -
- ); - - const sortedStats = sortBy(widgetStats, [(user) => user?.user_id !== currentUser?.id]); - - const filteredStats = sortedStats.filter((user) => { - if (!user) return false; - const userDetails = getUserDetails(user?.user_id); - if (!userDetails || userDetails.is_bot) return false; - const { display_name, first_name, last_name } = userDetails; - const searchLower = searchQuery.toLowerCase(); - return ( - display_name?.toLowerCase().includes(searchLower) || - first_name?.toLowerCase().includes(searchLower) || - last_name?.toLowerCase().includes(searchLower) - ); - }); - - // Update the displayedStats to always use the visibleItems limit - const handleLoadMore = () => { - setVisibleItems((prev) => { - const newValue = prev + 16; - if (newValue >= filteredStats.length) { - setIsExpanded(true); - return filteredStats.length; - } - return newValue; - }); - }; - - const handleHide = () => { - setVisibleItems(16); - setIsExpanded(false); - }; - - const displayedStats = filteredStats.slice(0, visibleItems); - - return ( - <> -
- {displayedStats?.map((user) => ( - - ))} -
- {filteredStats.length > visibleItems && !isExpanded && ( -
-
- Load more -
-
- )} - {isExpanded && ( -
-
Hide
-
- )} - - ); -}; diff --git a/web/core/components/dashboard/widgets/recent-collaborators/index.ts b/web/core/components/dashboard/widgets/recent-collaborators/index.ts deleted file mode 100644 index 1efe34c51..000000000 --- a/web/core/components/dashboard/widgets/recent-collaborators/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./root"; diff --git a/web/core/components/dashboard/widgets/recent-collaborators/root.tsx b/web/core/components/dashboard/widgets/recent-collaborators/root.tsx deleted file mode 100644 index 4d3064207..000000000 --- a/web/core/components/dashboard/widgets/recent-collaborators/root.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { useState } from "react"; -import { Search } from "lucide-react"; -// types -import { Card } from "@plane/ui"; -import { WidgetProps } from "@/components/dashboard/widgets"; -// components -import { CollaboratorsList } from "./collaborators-list"; - -export const RecentCollaboratorsWidget: React.FC = (props) => { - const { dashboardId, workspaceSlug } = props; - // states - const [searchQuery, setSearchQuery] = useState(""); - - return ( - -
-
-

Collaborators

-

- View and find all members you collaborate with across projects -

-
-
- - setSearchQuery(e.target.value)} - /> -
-
- -
- ); -}; diff --git a/web/core/components/dashboard/widgets/recent-projects.tsx b/web/core/components/dashboard/widgets/recent-projects.tsx deleted file mode 100644 index 3e9c4e257..000000000 --- a/web/core/components/dashboard/widgets/recent-projects.tsx +++ /dev/null @@ -1,134 +0,0 @@ -"use client"; - -import { useEffect } from "react"; -import { observer } from "mobx-react"; -import Link from "next/link"; -import { Plus } from "lucide-react"; -// plane types -import { PROJECT_BACKGROUND_COLORS, EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; -import { TRecentProjectsWidgetResponse } from "@plane/types"; -// plane ui -import { Avatar, AvatarGroup, Card } from "@plane/ui"; -// components -import { Logo } from "@/components/common"; -import { WidgetLoader, WidgetProps } from "@/components/dashboard/widgets"; -// constants -// helpers -import { getFileURL } from "@/helpers/file.helper"; -// hooks -import { - useEventTracker, - useDashboard, - useProject, - useCommandPalette, - useUserPermissions, - useMember, -} from "@/hooks/store"; -// plane web constants - -const WIDGET_KEY = "recent_projects"; - -type ProjectListItemProps = { - projectId: string; - workspaceSlug: string; -}; - -const ProjectListItem: React.FC = observer((props) => { - const { projectId, workspaceSlug } = props; - // store hooks - const { getProjectById } = useProject(); - const { getUserDetails } = useMember(); - // derived values - const projectDetails = getProjectById(projectId); - - const randomBgColor = PROJECT_BACKGROUND_COLORS[Math.floor(Math.random() * PROJECT_BACKGROUND_COLORS.length)]; - - if (!projectDetails) return null; - - return ( - -
-
- -
-
-
-
- {projectDetails.name} -
-
- - {projectDetails.members?.map((memberId) => { - const userDetails = getUserDetails(memberId); - if (!userDetails) return null; - return ( - - ); - })} - -
-
- - ); -}); - -export const RecentProjectsWidget: React.FC = observer((props) => { - const { dashboardId, workspaceSlug } = props; - // store hooks - const { toggleCreateProjectModal } = useCommandPalette(); - const { setTrackElement } = useEventTracker(); - const { allowPermissions } = useUserPermissions(); - const { fetchWidgetStats, getWidgetStats } = useDashboard(); - // derived values - const widgetStats = getWidgetStats(workspaceSlug, dashboardId, WIDGET_KEY); - const canCreateProject = allowPermissions( - [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - EUserPermissionsLevel.WORKSPACE - ); - - useEffect(() => { - fetchWidgetStats(workspaceSlug, dashboardId, { - widget_key: WIDGET_KEY, - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - if (!widgetStats) return ; - - return ( - - - Recent projects - -
- {canCreateProject && ( - - )} - {widgetStats.map((projectId) => ( - - ))} -
-
- ); -}); diff --git a/web/core/constants/fetch-keys.ts b/web/core/constants/fetch-keys.ts index d5bbf3ed6..f0c9551a4 100644 --- a/web/core/constants/fetch-keys.ts +++ b/web/core/constants/fetch-keys.ts @@ -47,105 +47,19 @@ const paramsToKey = (params: any) => { return `${layoutKey}_${projectKey}_${stateGroupKey}_${stateKey}_${priorityKey}_${assigneesKey}_${mentionsKey}_${createdByKey}_${type}_${groupBy}_${orderBy}_${labelsKey}_${startDateKey}_${targetDateKey}_${sub_issue}_${subscriberKey}`; }; -const myIssuesParamsToKey = (params: any) => { - const { assignees, created_by, labels, priority, state_group, subscriber, start_date, target_date } = params; - - let assigneesKey = assignees ? assignees.split(",") : []; - let createdByKey = created_by ? created_by.split(",") : []; - let stateGroupKey = state_group ? state_group.split(",") : []; - let subscriberKey = subscriber ? subscriber.split(",") : []; - let priorityKey = priority ? priority.split(",") : []; - let labelsKey = labels ? labels.split(",") : []; - const startDateKey = start_date ?? ""; - const targetDateKey = target_date ?? ""; - const type = params?.type ? params.type.toUpperCase() : "NULL"; - const groupBy = params?.group_by ? params.group_by.toUpperCase() : "NULL"; - const orderBy = params?.order_by ? params.order_by.toUpperCase() : "NULL"; - - // sorting each keys in ascending order - assigneesKey = assigneesKey.sort().join("_"); - createdByKey = createdByKey.sort().join("_"); - stateGroupKey = stateGroupKey.sort().join("_"); - subscriberKey = subscriberKey.sort().join("_"); - priorityKey = priorityKey.sort().join("_"); - labelsKey = labelsKey.sort().join("_"); - - return `${assigneesKey}_${createdByKey}_${stateGroupKey}_${subscriberKey}_${priorityKey}_${type}_${groupBy}_${orderBy}_${labelsKey}_${startDateKey}_${targetDateKey}`; -}; - -export const CURRENT_USER = "CURRENT_USER"; -export const USER_WORKSPACE_INVITATIONS = "USER_WORKSPACE_INVITATIONS"; export const USER_WORKSPACES_LIST = "USER_WORKSPACES_LIST"; -export const WORKSPACE_DETAILS = (workspaceSlug: string) => `WORKSPACE_DETAILS_${workspaceSlug.toUpperCase()}`; - export const WORKSPACE_MEMBERS = (workspaceSlug: string) => `WORKSPACE_MEMBERS_${workspaceSlug.toUpperCase()}`; -export const WORKSPACE_MEMBERS_ME = (workspaceSlug: string) => `WORKSPACE_MEMBERS_ME${workspaceSlug.toUpperCase()}`; -export const WORKSPACE_INVITATIONS = (workspaceSlug: string) => `WORKSPACE_INVITATIONS_${workspaceSlug.toString()}`; + export const WORKSPACE_INVITATION = (invitationId: string) => `WORKSPACE_INVITATION_${invitationId}`; -export const LAST_ACTIVE_WORKSPACE_AND_PROJECTS = "LAST_ACTIVE_WORKSPACE_AND_PROJECTS"; -export const PROJECTS_LIST = ( - workspaceSlug: string, - params: { - is_favorite: "all" | boolean; - } -) => { - if (!params) return `PROJECTS_LIST_${workspaceSlug.toUpperCase()}`; - - return `PROJECTS_LIST_${workspaceSlug.toUpperCase()}_${params.is_favorite.toString().toUpperCase()}`; -}; export const PROJECT_DETAILS = (projectId: string) => `PROJECT_DETAILS_${projectId.toUpperCase()}`; export const PROJECT_MEMBERS = (projectId: string) => `PROJECT_MEMBERS_${projectId.toUpperCase()}`; -export const PROJECT_INVITATIONS = (projectId: string) => `PROJECT_INVITATIONS_${projectId.toString()}`; -export const PROJECT_ISSUES_LIST = (workspaceSlug: string, projectId: string) => - `PROJECT_ISSUES_LIST_${workspaceSlug.toUpperCase()}_${projectId.toUpperCase()}`; -export const PROJECT_ISSUES_LIST_WITH_PARAMS = (projectId: string, params?: any) => { - if (!params) return `PROJECT_ISSUES_LIST_WITH_PARAMS_${projectId.toUpperCase()}`; - - const paramsKey = paramsToKey(params); - - return `PROJECT_ISSUES_LIST_WITH_PARAMS_${projectId.toUpperCase()}_${paramsKey}`; -}; -export const PROJECT_ARCHIVED_ISSUES_LIST_WITH_PARAMS = (projectId: string, params?: any) => { - if (!params) return `PROJECT_ARCHIVED_ISSUES_LIST_WITH_PARAMS_${projectId.toUpperCase()}`; - - const paramsKey = paramsToKey(params); - - return `PROJECT_ARCHIVED_ISSUES_LIST_WITH_PARAMS_${projectId.toUpperCase()}_${paramsKey}`; -}; - -export const PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS = (projectId: string, params?: any) => { - if (!params) return `PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS${projectId.toUpperCase()}`; - - const paramsKey = paramsToKey(params); - - return `PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS${projectId.toUpperCase()}_${paramsKey}`; -}; - -export const GLOBAL_VIEWS_LIST = (workspaceSlug: string) => `GLOBAL_VIEWS_LIST_${workspaceSlug.toUpperCase()}`; -export const GLOBAL_VIEW_DETAILS = (globalViewId: string) => `GLOBAL_VIEW_DETAILS_${globalViewId.toUpperCase()}`; -export const GLOBAL_VIEW_ISSUES = (globalViewId: string) => `GLOBAL_VIEW_ISSUES_${globalViewId.toUpperCase()}`; - -export const PROJECT_ISSUES_DETAILS = (issueId: string) => `PROJECT_ISSUES_DETAILS_${issueId.toUpperCase()}`; -export const PROJECT_ISSUES_PROPERTIES = (projectId: string) => `PROJECT_ISSUES_PROPERTIES_${projectId.toUpperCase()}`; -export const PROJECT_ISSUES_COMMENTS = (issueId: string) => `PROJECT_ISSUES_COMMENTS_${issueId.toUpperCase()}`; -export const PROJECT_ISSUES_ACTIVITY = (issueId: string) => `PROJECT_ISSUES_ACTIVITY_${issueId.toUpperCase()}`; -export const PROJECT_ISSUE_BY_STATE = (projectId: string) => `PROJECT_ISSUE_BY_STATE_${projectId.toUpperCase()}`; -export const PROJECT_ISSUE_LABELS = (projectId: string) => `PROJECT_ISSUE_LABELS_${projectId.toUpperCase()}`; -export const WORKSPACE_LABELS = (workspaceSlug: string) => `WORKSPACE_LABELS_${workspaceSlug.toUpperCase()}`; export const PROJECT_GITHUB_REPOSITORY = (projectId: string) => `PROJECT_GITHUB_REPOSITORY_${projectId.toUpperCase()}`; // cycles -export const CYCLES_LIST = (projectId: string) => `CYCLE_LIST_${projectId.toUpperCase()}`; -export const INCOMPLETE_CYCLES_LIST = (projectId: string) => `INCOMPLETE_CYCLES_LIST_${projectId.toUpperCase()}`; -export const CURRENT_CYCLE_LIST = (projectId: string) => `CURRENT_CYCLE_LIST_${projectId.toUpperCase()}`; -export const UPCOMING_CYCLES_LIST = (projectId: string) => `UPCOMING_CYCLES_LIST_${projectId.toUpperCase()}`; -export const DRAFT_CYCLES_LIST = (projectId: string) => `DRAFT_CYCLES_LIST_${projectId.toUpperCase()}`; -export const COMPLETED_CYCLES_LIST = (projectId: string) => `COMPLETED_CYCLES_LIST_${projectId.toUpperCase()}`; -export const CYCLE_ISSUES = (cycleId: string) => `CYCLE_ISSUES_${cycleId.toUpperCase()}`; export const CYCLE_ISSUES_WITH_PARAMS = (cycleId: string, params?: any) => { if (!params) return `CYCLE_ISSUES_WITH_PARAMS_${cycleId.toUpperCase()}`; @@ -153,47 +67,11 @@ export const CYCLE_ISSUES_WITH_PARAMS = (cycleId: string, params?: any) => { return `CYCLE_ISSUES_WITH_PARAMS_${cycleId.toUpperCase()}_${paramsKey.toUpperCase()}`; }; -export const CYCLE_DETAILS = (cycleId: string) => `CYCLE_DETAILS_${cycleId.toUpperCase()}`; -export const STATES_LIST = (projectId: string) => `STATES_LIST_${projectId.toUpperCase()}`; - -export const USER_ISSUE = (workspaceSlug: string) => `USER_ISSUE_${workspaceSlug.toUpperCase()}`; -export const USER_ISSUES = (workspaceSlug: string, params: any) => { - const paramsKey = myIssuesParamsToKey(params); - - return `USER_ISSUES_${workspaceSlug.toUpperCase()}_${paramsKey}`; -}; export const USER_ACTIVITY = (params: { cursor?: string }) => `USER_ACTIVITY_${params?.cursor}`; -export const USER_WORKSPACE_DASHBOARD = (workspaceSlug: string) => - `USER_WORKSPACE_DASHBOARD_${workspaceSlug.toUpperCase()}`; -export const USER_PROJECT_VIEW = (projectId: string) => `USER_PROJECT_VIEW_${projectId.toUpperCase()}`; - -export const MODULE_LIST = (projectId: string) => `MODULE_LIST_${projectId.toUpperCase()}`; -export const MODULE_ISSUES = (moduleId: string) => `MODULE_ISSUES_${moduleId.toUpperCase()}`; -export const MODULE_ISSUES_WITH_PARAMS = (moduleId: string, params?: any) => { - if (!params) return `MODULE_ISSUES_WITH_PARAMS_${moduleId.toUpperCase()}`; - - const paramsKey = paramsToKey(params); - - return `MODULE_ISSUES_WITH_PARAMS_${moduleId.toUpperCase()}_${paramsKey.toUpperCase()}`; -}; -export const MODULE_DETAILS = (moduleId: string) => `MODULE_DETAILS_${moduleId.toUpperCase()}`; - -export const VIEWS_LIST = (projectId: string) => `VIEWS_LIST_${projectId.toUpperCase()}`; -export const VIEW_DETAILS = (viewId: string) => `VIEW_DETAILS_${viewId.toUpperCase()}`; -export const VIEW_ISSUES = (viewId: string, params: any) => { - if (!params) return `VIEW_ISSUES_${viewId.toUpperCase()}`; - - const paramsKey = paramsToKey(params); - - return `VIEW_ISSUES_${viewId.toUpperCase()}_${paramsKey.toUpperCase()}`; -}; // Issues export const ISSUE_DETAILS = (issueId: string) => `ISSUE_DETAILS_${issueId.toUpperCase()}`; -export const SUB_ISSUES = (issueId: string) => `SUB_ISSUES_${issueId.toUpperCase()}`; -export const ISSUE_ATTACHMENTS = (issueId: string) => `ISSUE_ATTACHMENTS_${issueId.toUpperCase()}`; -export const ARCHIVED_ISSUE_DETAILS = (issueId: string) => `ARCHIVED_ISSUE_DETAILS_${issueId.toUpperCase()}`; // integrations export const APP_INTEGRATIONS = "APP_INTEGRATIONS"; @@ -222,21 +100,6 @@ export const GITHUB_REPOSITORY_INFO = (workspaceSlug: string, repoName: string) export const SLACK_CHANNEL_INFO = (workspaceSlug: string, projectId: string) => `SLACK_CHANNEL_INFO_${workspaceSlug.toString().toUpperCase()}_${projectId.toUpperCase()}`; -// Pages -export const RECENT_PAGES_LIST = (projectId: string) => `RECENT_PAGES_LIST_${projectId.toUpperCase()}`; -export const ALL_PAGES_LIST = (projectId: string) => `ALL_PAGES_LIST_${projectId.toUpperCase()}`; -export const ARCHIVED_PAGES_LIST = (projectId: string) => `ARCHIVED_PAGES_LIST_${projectId.toUpperCase}`; -export const FAVORITE_PAGES_LIST = (projectId: string) => `FAVORITE_PAGES_LIST_${projectId.toUpperCase()}`; -export const PRIVATE_PAGES_LIST = (projectId: string) => `PRIVATE_PAGES_LIST_${projectId.toUpperCase()}`; -export const SHARED_PAGES_LIST = (projectId: string) => `SHARED_PAGES_LIST_${projectId.toUpperCase()}`; -export const PAGE_DETAILS = (pageId: string) => `PAGE_DETAILS_${pageId.toUpperCase()}`; -export const PAGE_BLOCKS_LIST = (pageId: string) => `PAGE_BLOCK_LIST_${pageId.toUpperCase()}`; -export const PAGE_BLOCK_DETAILS = (pageId: string) => `PAGE_BLOCK_DETAILS_${pageId.toUpperCase()}`; -export const MY_PAGES_LIST = (pageId: string) => `MY_PAGE_LIST_${pageId}`; -// estimates -export const ESTIMATES_LIST = (projectId: string) => `ESTIMATES_LIST_${projectId.toUpperCase()}`; -export const ESTIMATE_DETAILS = (estimateId: string) => `ESTIMATE_DETAILS_${estimateId.toUpperCase()}`; - // profile export const USER_PROFILE_DATA = (workspaceSlug: string, userId: string) => `USER_PROFILE_ACTIVITY_${workspaceSlug.toUpperCase()}_${userId.toUpperCase()}`; @@ -249,19 +112,6 @@ export const USER_PROFILE_ACTIVITY = ( ) => `USER_WORKSPACE_PROFILE_ACTIVITY_${workspaceSlug.toUpperCase()}_${userId.toUpperCase()}_${params?.cursor}`; export const USER_PROFILE_PROJECT_SEGREGATION = (workspaceSlug: string, userId: string) => `USER_PROFILE_PROJECT_SEGREGATION_${workspaceSlug.toUpperCase()}_${userId.toUpperCase()}`; -export const USER_PROFILE_ISSUES = (workspaceSlug: string, userId: string, params: any) => { - const paramsKey = myIssuesParamsToKey(params); - - return `USER_PROFILE_ISSUES_${workspaceSlug.toUpperCase()}_${userId.toUpperCase()}_${paramsKey}`; -}; - -// reactions -export const ISSUE_REACTION_LIST = (workspaceSlug: string, projectId: string, issueId: string) => - `ISSUE_REACTION_LIST_${workspaceSlug.toUpperCase()}_${projectId.toUpperCase()}_${issueId.toUpperCase()}`; -export const COMMENT_REACTION_LIST = (workspaceSlug: string, projectId: string, commendId: string) => - `COMMENT_REACTION_LIST_${workspaceSlug.toUpperCase()}_${projectId.toUpperCase()}_${commendId.toUpperCase()}`; // api-tokens export const API_TOKENS_LIST = (workspaceSlug: string) => `API_TOKENS_LIST_${workspaceSlug.toUpperCase()}`; -export const API_TOKEN_DETAILS = (workspaceSlug: string, tokenId: string) => - `API_TOKEN_DETAILS_${workspaceSlug.toUpperCase()}_${tokenId.toUpperCase()}`; diff --git a/web/core/hooks/use-comment-reaction.tsx b/web/core/hooks/use-comment-reaction.tsx deleted file mode 100644 index 848c9b426..000000000 --- a/web/core/hooks/use-comment-reaction.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import useSWR from "swr"; -// fetch keys -import { COMMENT_REACTION_LIST } from "@/constants/fetch-keys"; -// services -import { groupReactions } from "@/helpers/emoji.helper"; -import { useUser } from "@/hooks/store"; -import { IssueReactionService } from "@/services/issue"; -// helpers -// hooks -// services -const issueReactionService = new IssueReactionService(); - -const useCommentReaction: any = ( - workspaceSlug?: string | string[] | null, - projectId?: string | string[] | null, - commendId?: string | string[] | null -) => { - const { - data: commentReactions, - mutate: mutateCommentReactions, - error, - } = useSWR( - workspaceSlug && projectId && commendId - ? COMMENT_REACTION_LIST(workspaceSlug.toString(), projectId.toString(), commendId.toString()) - : null, - workspaceSlug && projectId && commendId - ? () => - issueReactionService.listIssueCommentReactions( - workspaceSlug.toString(), - projectId.toString(), - commendId.toString() - ) - : null - ); - - const { data: currentUser } = useUser(); - - const groupedReactions = groupReactions(commentReactions || [], "reaction"); - - /** - * @description Use this function to create user's reaction to an issue. This function will mutate the reactions state. - * @param {string} reaction - * @example handleReactionDelete("123") // 123 -> is emoji hexa-code - */ - - const handleReactionCreate = async (reaction: string) => { - if (!workspaceSlug || !projectId || !commendId) return; - - const data = await issueReactionService.createIssueCommentReaction( - workspaceSlug.toString(), - projectId.toString(), - commendId.toString(), - { reaction } - ); - - mutateCommentReactions((prev: any) => [...(prev || []), data]); - }; - - /** - * @description Use this function to delete user's reaction from an issue. This function will mutate the reactions state. - * @param {string} reaction - * @example handleReactionDelete("123") // 123 -> is emoji hexa-code - */ - - const handleReactionDelete = async (reaction: string) => { - if (!workspaceSlug || !projectId || !commendId) return; - - mutateCommentReactions( - (prevData: any) => prevData?.filter((r: any) => r.actor !== currentUser?.id || r.reaction !== reaction) || [], - false - ); - - await issueReactionService.deleteIssueCommentReaction( - workspaceSlug.toString(), - projectId.toString(), - commendId.toString(), - reaction - ); - - mutateCommentReactions(); - }; - - return { - isLoading: !commentReactions && !error, - commentReactions, - groupedReactions, - handleReactionCreate, - handleReactionDelete, - mutateCommentReactions, - } as const; -}; - -export default useCommentReaction; diff --git a/web/core/hooks/use-dynamic-dropdown.tsx b/web/core/hooks/use-dynamic-dropdown.tsx deleted file mode 100644 index 771763e35..000000000 --- a/web/core/hooks/use-dynamic-dropdown.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import React, { useCallback, useEffect } from "react"; -// plane helpers -import { useOutsideClickDetector } from "@plane/hooks"; - -/** - * Custom hook for dynamic dropdown position calculation. - * @param isOpen - Indicates whether the dropdown is open. - * @param handleClose - Callback to handle closing the dropdown. - * @param buttonRef - Ref object for the button triggering the dropdown. - * @param dropdownRef - Ref object for the dropdown element. - */ - -const useDynamicDropdownPosition = ( - isOpen: boolean, - handleClose: () => void, - buttonRef: React.RefObject, - dropdownRef: React.RefObject -) => { - const handlePosition = useCallback(() => { - const button = buttonRef.current; - const dropdown = dropdownRef.current; - - if (!dropdown || !button) return; - - const buttonRect = button.getBoundingClientRect(); - const dropdownRect = dropdown.getBoundingClientRect(); - - const { innerHeight, innerWidth, scrollX, scrollY } = window; - - let top: number = buttonRect.bottom + scrollY; - if (top + dropdownRect.height > innerHeight) top = innerHeight - dropdownRect.height; - - let left: number = buttonRect.left + scrollX + (buttonRect.width - dropdownRect.width) / 2; - if (left + dropdownRect.width > innerWidth) left = innerWidth - dropdownRect.width; - - dropdown.style.top = `${Math.max(top, 5)}px`; - dropdown.style.left = `${Math.max(left, 5)}px`; - }, [buttonRef, dropdownRef]); - - useEffect(() => { - if (isOpen) handlePosition(); - }, [handlePosition, isOpen]); - - useOutsideClickDetector(dropdownRef, () => { - if (isOpen) handleClose(); - }); - - const handleResize = useCallback(() => { - if (isOpen) { - handlePosition(); - } - }, [handlePosition, isOpen]); - - useEffect(() => { - window.addEventListener("resize", handleResize); - - return () => { - window.removeEventListener("resize", handleResize); - }; - }, [isOpen, handleResize]); -}; - -export default useDynamicDropdownPosition; diff --git a/web/core/hooks/use-url-hash.tsx b/web/core/hooks/use-url-hash.tsx deleted file mode 100644 index 9dc6d1770..000000000 --- a/web/core/hooks/use-url-hash.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { useEffect, useState } from "react"; - -const useURLHash = () => { - const [hashValue, setHashValue] = useState(); - - useEffect(() => { - const hash = window.location.hash?.split("#")[1]; - setHashValue(hash); - }, []); - - return hashValue; -}; - -export default useURLHash; diff --git a/web/core/lib/wrappers/crisp-wrapper.tsx b/web/core/lib/wrappers/crisp-wrapper.tsx deleted file mode 100644 index 1debf8ea5..000000000 --- a/web/core/lib/wrappers/crisp-wrapper.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { useEffect, ReactNode, FC } from "react"; -import { observer } from "mobx-react"; -// hooks -import { useUser } from "@/hooks/store"; - -declare global { - interface Window { - $crisp: unknown[]; - CRISP_WEBSITE_ID: unknown; - } -} - -export interface ICrispWrapper { - children: ReactNode; -} - -const CrispWrapper: FC = observer((props) => { - const { children } = props; - const { data: user } = useUser(); - - useEffect(() => { - if (typeof window && user?.email && process.env.NEXT_PUBLIC_CRISP_ID) { - window.$crisp = []; - window.CRISP_WEBSITE_ID = process.env.NEXT_PUBLIC_CRISP_ID; - (function () { - const d = document; - const s = d.createElement("script"); - s.src = "https://client.crisp.chat/l.js"; - s.async = true; - d.getElementsByTagName("head")[0].appendChild(s); - window.$crisp.push(["set", "user:email", [user.email]]); - window.$crisp.push(["do", "chat:hide"]); - window.$crisp.push(["do", "chat:close"]); - })(); - } - }, [user?.email]); - - return <>{children}; -}); - -export default CrispWrapper; diff --git a/web/helpers/string.helper.ts b/web/helpers/string.helper.ts index d09731091..4c85e3ca8 100644 --- a/web/helpers/string.helper.ts +++ b/web/helpers/string.helper.ts @@ -1,10 +1,4 @@ import DOMPurify from "isomorphic-dompurify"; -import { - CYCLE_ISSUES_WITH_PARAMS, - MODULE_ISSUES_WITH_PARAMS, - PROJECT_ISSUES_LIST_WITH_PARAMS, - VIEW_ISSUES, -} from "@/constants/fetch-keys"; export const addSpaceIfCamelCase = (str: string) => { if (str === undefined || str === null) return ""; @@ -150,29 +144,6 @@ export const objToQueryParams = (obj: any) => { return params.toString(); }; -export const getFetchKeysForIssueMutation = (options: { - cycleId?: string | string[]; - moduleId?: string | string[]; - viewId?: string | string[]; - projectId: string; - viewGanttParams: any; - ganttParams: any; -}) => { - const { cycleId, moduleId, viewId, projectId, viewGanttParams, ganttParams } = options; - - const ganttFetchKey = cycleId - ? { ganttFetchKey: CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), ganttParams) } - : moduleId - ? { ganttFetchKey: MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), ganttParams) } - : viewId - ? { ganttFetchKey: VIEW_ISSUES(viewId.toString(), viewGanttParams) } - : { ganttFetchKey: PROJECT_ISSUES_LIST_WITH_PARAMS(projectId?.toString() ?? "", ganttParams) }; - - return { - ...ganttFetchKey, - }; -}; - /** * @returns {boolean} true if searchQuery is substring of text in the same order, false otherwise * @description Returns true if searchQuery is substring of text in the same order, false otherwise diff --git a/web/package.json b/web/package.json index 6312f9f90..2122586e5 100644 --- a/web/package.json +++ b/web/package.json @@ -19,7 +19,6 @@ "@atlaskit/pragmatic-drag-and-drop": "^1.1.3", "@atlaskit/pragmatic-drag-and-drop-auto-scroll": "^1.3.0", "@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3", - "@blueprintjs/popover2": "^1.13.3", "@headlessui/react": "^1.7.3", "@intercom/messenger-js-sdk": "^0.0.12", "@plane/constants": "*", @@ -65,7 +64,6 @@ "smooth-scroll-into-view-if-needed": "^2.0.2", "swr": "^2.1.3", "tailwind-merge": "^2.0.0", - "use-debounce": "^9.0.4", "use-font-face-observer": "^1.2.2", "uuid": "^9.0.0", "zxcvbn": "^4.4.2" From 053c895120c3045c4e6e320eeabf9a72e150fa69 Mon Sep 17 00:00:00 2001 From: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com> Date: Fri, 6 Jun 2025 15:02:00 +0530 Subject: [PATCH 137/201] [WEB 4252] chore: updated the favicon request for work item link (#7173) * chore: added the favicon to link * chore: added none validation for soup --- apiserver/plane/api/views/issue.py | 8 +- apiserver/plane/app/views/issue/link.py | 4 +- .../plane/bgtasks/work_item_link_task.py | 80 +++++++++---------- 3 files changed, 45 insertions(+), 47 deletions(-) diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index efbdf07f9..9e8579526 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -58,7 +58,7 @@ from plane.bgtasks.storage_metadata_task import get_asset_object_metadata from .base import BaseAPIView from plane.utils.host import base_host from plane.bgtasks.webhook_task import model_activity - +from plane.bgtasks.work_item_link_task import crawl_work_item_link_title class WorkspaceIssueAPIEndpoint(BaseAPIView): """ @@ -692,6 +692,9 @@ class IssueLinkAPIEndpoint(BaseAPIView): serializer = IssueLinkSerializer(data=request.data) if serializer.is_valid(): serializer.save(project_id=project_id, issue_id=issue_id) + crawl_work_item_link_title.delay( + serializer.data.get("id"), serializer.data.get("url") + ) link = IssueLink.objects.get(pk=serializer.data["id"]) link.created_by_id = request.data.get("created_by", request.user.id) @@ -719,6 +722,9 @@ class IssueLinkAPIEndpoint(BaseAPIView): serializer = IssueLinkSerializer(issue_link, data=request.data, partial=True) if serializer.is_valid(): serializer.save() + crawl_work_item_link_title.delay( + serializer.data.get("id"), serializer.data.get("url") + ) issue_activity.delay( type="link.activity.updated", requested_data=requested_data, diff --git a/apiserver/plane/app/views/issue/link.py b/apiserver/plane/app/views/issue/link.py index 099550b84..0a574dc19 100644 --- a/apiserver/plane/app/views/issue/link.py +++ b/apiserver/plane/app/views/issue/link.py @@ -45,7 +45,7 @@ class IssueLinkViewSet(BaseViewSet): serializer = IssueLinkSerializer(data=request.data) if serializer.is_valid(): serializer.save(project_id=project_id, issue_id=issue_id) - crawl_work_item_link_title( + crawl_work_item_link_title.delay( serializer.data.get("id"), serializer.data.get("url") ) issue_activity.delay( @@ -78,7 +78,7 @@ class IssueLinkViewSet(BaseViewSet): serializer = IssueLinkSerializer(issue_link, data=request.data, partial=True) if serializer.is_valid(): serializer.save() - crawl_work_item_link_title( + crawl_work_item_link_title.delay( serializer.data.get("id"), serializer.data.get("url") ) diff --git a/apiserver/plane/bgtasks/work_item_link_task.py b/apiserver/plane/bgtasks/work_item_link_task.py index 9a3ac265e..1ba48caf9 100644 --- a/apiserver/plane/bgtasks/work_item_link_task.py +++ b/apiserver/plane/bgtasks/work_item_link_task.py @@ -19,17 +19,6 @@ logger = logging.getLogger("plane.worker") DEFAULT_FAVICON = "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLWxpbmstaWNvbiBsdWNpZGUtbGluayI+PHBhdGggZD0iTTEwIDEzYTUgNSAwIDAgMCA3LjU0LjU0bDMtM2E1IDUgMCAwIDAtNy4wNy03LjA3bC0xLjcyIDEuNzEiLz48cGF0aCBkPSJNMTQgMTFhNSA1IDAgMCAwLTcuNTQtLjU0bC0zIDNhNSA1IDAgMCAwIDcuMDcgNy4wN2wxLjcxLTEuNzEiLz48L3N2Zz4=" # noqa: E501 - -@shared_task -def crawl_work_item_link_title(id: str, url: str) -> None: - meta_data = crawl_work_item_link_title_and_favicon(url) - issue_link = IssueLink.objects.get(id=id) - - issue_link.metadata = meta_data - - issue_link.save() - - def crawl_work_item_link_title_and_favicon(url: str) -> Dict[str, Any]: """ Crawls a URL to extract the title and favicon. @@ -57,17 +46,18 @@ def crawl_work_item_link_title_and_favicon(url: str) -> Dict[str, Any]: "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" # noqa: E501 } - # Fetch the main page - response = requests.get(url, headers=headers, timeout=2) + soup = None + title = None - response.raise_for_status() + try: + response = requests.get(url, headers=headers, timeout=1) - # Parse HTML - soup = BeautifulSoup(response.content, "html.parser") + soup = BeautifulSoup(response.content, "html.parser") + title_tag = soup.find("title") + title = title_tag.get_text().strip() if title_tag else None - # Extract title - title_tag = soup.find("title") - title = title_tag.get_text().strip() if title_tag else None + except requests.RequestException as e: + logger.warning(f"Failed to fetch HTML for title: {str(e)}") # Fetch and encode favicon favicon_base64 = fetch_and_encode_favicon(headers, soup, url) @@ -82,14 +72,6 @@ def crawl_work_item_link_title_and_favicon(url: str) -> Dict[str, Any]: return result - except requests.RequestException as e: - log_exception(e) - return { - "error": f"Request failed: {str(e)}", - "title": None, - "favicon": None, - "url": url, - } except Exception as e: log_exception(e) return { @@ -100,7 +82,7 @@ def crawl_work_item_link_title_and_favicon(url: str) -> Dict[str, Any]: } -def find_favicon_url(soup: BeautifulSoup, base_url: str) -> Optional[str]: +def find_favicon_url(soup: Optional[BeautifulSoup], base_url: str) -> Optional[str]: """ Find the favicon URL from HTML soup. @@ -111,18 +93,20 @@ def find_favicon_url(soup: BeautifulSoup, base_url: str) -> Optional[str]: Returns: str: Absolute URL to favicon or None """ - # Look for various favicon link tags - favicon_selectors = [ - 'link[rel="icon"]', - 'link[rel="shortcut icon"]', - 'link[rel="apple-touch-icon"]', - 'link[rel="apple-touch-icon-precomposed"]', - ] - for selector in favicon_selectors: - favicon_tag = soup.select_one(selector) - if favicon_tag and favicon_tag.get("href"): - return urljoin(base_url, favicon_tag["href"]) + if soup is not None: + # Look for various favicon link tags + favicon_selectors = [ + 'link[rel="icon"]', + 'link[rel="shortcut icon"]', + 'link[rel="apple-touch-icon"]', + 'link[rel="apple-touch-icon-precomposed"]', + ] + + for selector in favicon_selectors: + favicon_tag = soup.select_one(selector) + if favicon_tag and favicon_tag.get("href"): + return urljoin(base_url, favicon_tag["href"]) # Fallback to /favicon.ico parsed_url = urlparse(base_url) @@ -131,7 +115,6 @@ def find_favicon_url(soup: BeautifulSoup, base_url: str) -> Optional[str]: # Check if fallback exists try: response = requests.head(fallback_url, timeout=2) - response.raise_for_status() if response.status_code == 200: return fallback_url except requests.RequestException as e: @@ -142,8 +125,8 @@ def find_favicon_url(soup: BeautifulSoup, base_url: str) -> Optional[str]: def fetch_and_encode_favicon( - headers: Dict[str, str], soup: BeautifulSoup, url: str -) -> Optional[Dict[str, str]]: + headers: Dict[str, str], soup: Optional[BeautifulSoup], url: str +) -> Dict[str, Optional[str]]: """ Fetch favicon and encode it as base64. @@ -162,8 +145,7 @@ def fetch_and_encode_favicon( "favicon_base64": f"data:image/svg+xml;base64,{DEFAULT_FAVICON}", } - response = requests.get(favicon_url, headers=headers, timeout=2) - response.raise_for_status() + response = requests.get(favicon_url, headers=headers, timeout=1) # Get content type content_type = response.headers.get("content-type", "image/x-icon") @@ -183,3 +165,13 @@ def fetch_and_encode_favicon( "favicon_url": None, "favicon_base64": f"data:image/svg+xml;base64,{DEFAULT_FAVICON}", } + + +@shared_task +def crawl_work_item_link_title(id: str, url: str) -> None: + meta_data = crawl_work_item_link_title_and_favicon(url) + issue_link = IssueLink.objects.get(id=id) + + issue_link.metadata = meta_data + + issue_link.save() From 950fcfdb4046b705fa0bfecda96310bd9f073ffc Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Fri, 6 Jun 2025 15:04:00 +0530 Subject: [PATCH 138/201] [WIKI-391] chore: handle deactivated user display name in version history #7171 --- .../core/description-versions/dropdown-item.tsx | 2 +- .../components/core/description-versions/dropdown.tsx | 3 ++- web/core/components/pages/version/sidebar-list-item.tsx | 8 +++++--- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/web/core/components/core/description-versions/dropdown-item.tsx b/web/core/components/core/description-versions/dropdown-item.tsx index 11682c206..6e5b3b491 100644 --- a/web/core/components/core/description-versions/dropdown-item.tsx +++ b/web/core/components/core/description-versions/dropdown-item.tsx @@ -31,7 +31,7 @@ export const DescriptionVersionsDropdownItem: React.FC = observer((props) />

- {versionCreator?.display_name} + {versionCreator?.display_name ?? t("common.deactivated_user")} {calculateTimeAgo(version.last_saved_at)}

diff --git a/web/core/components/core/description-versions/dropdown.tsx b/web/core/components/core/description-versions/dropdown.tsx index 098498500..39e9f7033 100644 --- a/web/core/components/core/description-versions/dropdown.tsx +++ b/web/core/components/core/description-versions/dropdown.tsx @@ -40,7 +40,8 @@ export const DescriptionVersionsDropdown: React.FC = observer((props) =>

{t("description_versions.last_edited_by")}{" "} - {lastUpdatedByUserDisplayName} {calculateTimeAgo(lastUpdatedAt)} + {lastUpdatedByUserDisplayName ?? t("common.deactivated_user")}{" "} + {calculateTimeAgo(lastUpdatedAt)}

} diff --git a/web/core/components/pages/version/sidebar-list-item.tsx b/web/core/components/pages/version/sidebar-list-item.tsx index be11f5725..5459bc166 100644 --- a/web/core/components/pages/version/sidebar-list-item.tsx +++ b/web/core/components/pages/version/sidebar-list-item.tsx @@ -1,8 +1,8 @@ import { observer } from "mobx-react"; import Link from "next/link"; -// plane types +// plane imports +import { useTranslation } from "@plane/i18n"; import { TPageVersion } from "@plane/types"; -// plane ui import { Avatar } from "@plane/ui"; // helpers import { cn } from "@/helpers/common.helper"; @@ -23,6 +23,8 @@ export const PlaneVersionsSidebarListItem: React.FC = observer((props) => const { getUserDetails } = useMember(); // derived values const ownerDetails = getUserDetails(version.owned_by); + // translation + const { t } = useTranslation(); return ( = observer((props) => size="sm" className="flex-shrink-0" /> - {ownerDetails?.display_name} + {ownerDetails?.display_name ?? t("common.deactivated_user")}

); From edeeee1227be55b0febb9fb1ec27d368adb9284d Mon Sep 17 00:00:00 2001 From: Vamsi Krishna <46787868+vamsikrishnamathala@users.noreply.github.com> Date: Sun, 8 Jun 2025 23:46:12 +0530 Subject: [PATCH 139/201] [WEB-4063]chore: updated work item email template (#7044) * chore: updated work item email template * chore: passed dynamic value for email template --------- Co-authored-by: NarayanBavisetti --- apiserver/plane/bgtasks/email_notification_task.py | 1 + .../templates/emails/notifications/issue-updates.html | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/apiserver/plane/bgtasks/email_notification_task.py b/apiserver/plane/bgtasks/email_notification_task.py index e700b2624..dcc37796d 100644 --- a/apiserver/plane/bgtasks/email_notification_task.py +++ b/apiserver/plane/bgtasks/email_notification_task.py @@ -284,6 +284,7 @@ def send_email_notification( "project": str(issue.project.name), "user_preference": f"{base_api}/profile/preferences/email", "comments": comments, + "entity_type": "issue", } html_content = render_to_string( "emails/notifications/issue-updates.html", context diff --git a/apiserver/templates/emails/notifications/issue-updates.html b/apiserver/templates/emails/notifications/issue-updates.html index e17f0e9e6..8ba91c6fe 100644 --- a/apiserver/templates/emails/notifications/issue-updates.html +++ b/apiserver/templates/emails/notifications/issue-updates.html @@ -3,7 +3,7 @@ - Updates on issue + Updates on {{entity_type}} @@ -37,7 +37,7 @@ {% else %}

{{summary}} {% if data|length > 0 %} {{ data.0.actor_detail.first_name}} {{data.0.actor_detail.last_name}} {% else %} {{ comments.0.actor_detail.first_name}} {{comments.0.actor_detail.last_name}} {% endif %} and others.

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

The issue title has been updated to {{ issue.name}}

+

The {{entity_type}} title has been updated to {{ issue.name}}

{% endif %} {% if data %}
@@ -224,7 +224,7 @@ {% endif %}
-
View issue
+
View {{entity_type}}
@@ -232,7 +232,7 @@
- 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 {{entity_type}} or manage your email preferences.
From 1608e4f1223b81446e156b064ce0bc5ee2418ab0 Mon Sep 17 00:00:00 2001 From: Vamsi Krishna <46787868+vamsikrishnamathala@users.noreply.github.com> Date: Sun, 8 Jun 2025 23:47:08 +0530 Subject: [PATCH 140/201] [WEB-3374]feat: added merge date display (#7141) * feat: added merge date display * chore: moved formatter ti utils * chore: removed unwanted props --- packages/utils/src/datetime.ts | 56 +++++++++ .../analytics-sidebar/sidebar-header.tsx | 14 +-- .../cycles/list/cycle-list-item-action.tsx | 7 +- web/core/components/dropdowns/date-range.tsx | 110 +++++++++++++----- web/core/components/dropdowns/index.ts | 1 + web/core/components/dropdowns/merged-date.tsx | 34 ++++++ .../sub-issues/issues-list/properties.tsx | 66 +++++++---- .../properties/all-properties.tsx | 62 ++++------ .../modules/module-list-item-action.tsx | 1 + 9 files changed, 248 insertions(+), 103 deletions(-) create mode 100644 web/core/components/dropdowns/merged-date.tsx diff --git a/packages/utils/src/datetime.ts b/packages/utils/src/datetime.ts index 0a12a2270..8cec48f5f 100644 --- a/packages/utils/src/datetime.ts +++ b/packages/utils/src/datetime.ts @@ -333,3 +333,59 @@ export const generateDateArray = (startDate: string | Date, endDate: string | Da } return dateArray; }; + +/** + * Formats merged date range display with smart formatting + * - Single date: "Jan 24, 2025" + * - Same year, same month: "Jan 24 - 28, 2025" + * - Same year, different month: "Jan 24 - Feb 6, 2025" + * - Different year: "Dec 28, 2024 - Jan 4, 2025" + */ +export const formatDateRange = ( + parsedStartDate: Date | null | undefined, + parsedEndDate: Date | null | undefined +): string => { + // If no dates are provided + if (!parsedStartDate && !parsedEndDate) { + return ""; + } + + // If only start date is provided + if (parsedStartDate && !parsedEndDate) { + return format(parsedStartDate, "MMM dd, yyyy"); + } + + // If only end date is provided + if (!parsedStartDate && parsedEndDate) { + return format(parsedEndDate, "MMM dd, yyyy"); + } + + // If both dates are provided + if (parsedStartDate && parsedEndDate) { + const startYear = parsedStartDate.getFullYear(); + const startMonth = parsedStartDate.getMonth(); + const endYear = parsedEndDate.getFullYear(); + const endMonth = parsedEndDate.getMonth(); + + // Same year, same month + if (startYear === endYear && startMonth === endMonth) { + const startDay = format(parsedStartDate, "dd"); + const endDay = format(parsedEndDate, "dd"); + return `${format(parsedStartDate, "MMM")} ${startDay} - ${endDay}, ${startYear}`; + } + + // Same year, different month + if (startYear === endYear) { + const startFormatted = format(parsedStartDate, "MMM dd"); + const endFormatted = format(parsedEndDate, "MMM dd"); + return `${startFormatted} - ${endFormatted}, ${startYear}`; + } + + // Different year + const startFormatted = format(parsedStartDate, "MMM dd, yyyy"); + const endFormatted = format(parsedEndDate, "MMM dd, yyyy"); + return `${startFormatted} - ${endFormatted}`; + } + + return ""; +}; \ No newline at end of file diff --git a/web/core/components/cycles/analytics-sidebar/sidebar-header.tsx b/web/core/components/cycles/analytics-sidebar/sidebar-header.tsx index f441b907b..01c692837 100644 --- a/web/core/components/cycles/analytics-sidebar/sidebar-header.tsx +++ b/web/core/components/cycles/analytics-sidebar/sidebar-header.tsx @@ -3,21 +3,12 @@ import React, { FC, useEffect, useState } from "react"; import { observer } from "mobx-react"; import { Controller, useForm } from "react-hook-form"; -import { - ArchiveIcon, - ArchiveRestoreIcon, - ArrowRight, - ChevronRight, - EllipsisIcon, - LinkIcon, - Trash2, -} from "lucide-react"; +import { ArrowRight, ChevronRight } from "lucide-react"; // Plane Imports import { CYCLE_STATUS, CYCLE_UPDATED, EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { ICycle } from "@plane/types"; -import { CustomMenu, setToast, TOAST_TYPE } from "@plane/ui"; -import { copyUrlToClipboard } from "@plane/utils"; +import { setToast, TOAST_TYPE } from "@plane/ui"; // components import { DateRangeDropdown } from "@/components/dropdowns"; // helpers @@ -239,6 +230,7 @@ export const CycleSidebarHeader: FC = observer((props) => { {renderFormattedDateInUserTimezone(cycleDetails.end_date ?? "")} } + mergeDates showTooltip={!!cycleDetails.start_date && !!cycleDetails.end_date} // show tooltip only if both start and end date are present required={cycleDetails.status !== "draft"} disabled={!isEditingAllowed || isArchived || isCompleted} diff --git a/web/core/components/cycles/list/cycle-list-item-action.tsx b/web/core/components/cycles/list/cycle-list-item-action.tsx index c90ff653e..bc3553078 100644 --- a/web/core/components/cycles/list/cycle-list-item-action.tsx +++ b/web/core/components/cycles/list/cycle-list-item-action.tsx @@ -1,7 +1,6 @@ "use client"; import React, { FC, MouseEvent, useEffect, useMemo, useState } from "react"; -import { format, parseISO } from "date-fns"; import { observer } from "mobx-react"; import { useParams, usePathname, useSearchParams } from "next/navigation"; import { useForm } from "react-hook-form"; @@ -23,6 +22,7 @@ import { Avatar, AvatarGroup, FavoriteStar, LayersIcon, Tooltip, TransferIcon, s import { CycleQuickActions, TransferIssuesModal } from "@/components/cycles"; import { DateRangeDropdown } from "@/components/dropdowns"; import { ButtonAvatars } from "@/components/dropdowns/member/avatar"; +import { MergedDateDisplay } from "@/components/dropdowns/merged-date"; import { getDate } from "@/helpers/date-time.helper"; import { getFileURL } from "@/helpers/file.helper"; // hooks @@ -230,9 +230,7 @@ export const CycleListItemAction: FC = observer((props) => { >
- {cycleDetails.start_date && {format(parseISO(cycleDetails.start_date), "MMM dd, yyyy")}} - - {cycleDetails.end_date && {format(parseISO(cycleDetails.end_date), "MMM dd, yyyy")}} +
{projectUTCOffset && ( @@ -269,6 +267,7 @@ export const CycleListItemAction: FC = observer((props) => { {renderFormattedDateInUserTimezone(cycleDetails.end_date ?? "")} } + mergeDates required={cycleDetails.status !== "draft"} disabled hideIcon={{ diff --git a/web/core/components/dropdowns/date-range.tsx b/web/core/components/dropdowns/date-range.tsx index 0ab9fd44b..62c7d9409 100644 --- a/web/core/components/dropdowns/date-range.tsx +++ b/web/core/components/dropdowns/date-range.tsx @@ -4,7 +4,7 @@ import React, { useEffect, useRef, useState } from "react"; import { Placement } from "@popperjs/core"; import { DateRange, Matcher } from "react-day-picker"; import { usePopper } from "react-popper"; -import { ArrowRight, CalendarCheck2, CalendarDays } from "lucide-react"; +import { ArrowRight, CalendarCheck2, CalendarDays, X } from "lucide-react"; import { Combobox } from "@headlessui/react"; // plane imports import { useTranslation } from "@plane/i18n"; @@ -17,6 +17,7 @@ import { renderFormattedDate } from "@/helpers/date-time.helper"; import { useDropdown } from "@/hooks/use-dropdown"; // components import { DropdownButton } from "./buttons"; +import { MergedDateDisplay } from "./merged-date"; // types import { TButtonVariants } from "./types"; @@ -30,11 +31,14 @@ type Props = { buttonVariant: TButtonVariants; cancelButtonText?: string; className?: string; + clearIconClassName?: string; disabled?: boolean; hideIcon?: { from?: boolean; to?: boolean; }; + isClearable?: boolean; + mergeDates?: boolean; minDate?: Date; maxDate?: Date; onSelect?: (range: DateRange | undefined) => void; @@ -65,11 +69,14 @@ export const DateRangeDropdown: React.FC = (props) => { buttonToDateClassName, buttonVariant, className, + clearIconClassName = "", disabled = false, hideIcon = { from: true, to: true, }, + isClearable = false, + mergeDates, minDate, maxDate, onSelect, @@ -118,20 +125,18 @@ export const DateRangeDropdown: React.FC = (props) => { setIsOpen, }); - const handleClose = () => { - if (!isOpen) return; - setIsOpen(false); - setDateRange({ - from: value.from, - to: value.to, - }); - if (referenceElement) referenceElement.blur(); - }; - const disabledDays: Matcher[] = []; if (minDate) disabledDays.push({ before: minDate }); if (maxDate) disabledDays.push({ after: maxDate }); + const clearDates = () => { + const clearedRange = { from: undefined, to: undefined }; + setDateRange(clearedRange); + onSelect?.(clearedRange); + }; + + const hasDisplayedDates = dateRange.from || dateRange.to; + useEffect(() => { setDateRange(value); }, [value]); @@ -158,9 +163,9 @@ export const DateRangeDropdown: React.FC = (props) => { tooltipContent={ customTooltipContent ?? ( <> - {dateRange.from ? renderFormattedDate(dateRange.from) : "N/A"} - {" - "} - {dateRange.to ? renderFormattedDate(dateRange.to) : "N/A"} + {dateRange.from ? renderFormattedDate(dateRange.from) : ""} + {dateRange.from && dateRange.to ? " - " : ""} + {dateRange.to ? renderFormattedDate(dateRange.to) : ""} ) } @@ -168,19 +173,70 @@ export const DateRangeDropdown: React.FC = (props) => { variant={buttonVariant} renderToolTipByDefault={renderByDefault} > - - {!hideIcon.from && } - {dateRange.from ? renderFormattedDate(dateRange.from) : renderPlaceholder ? placeholder.from : ""} - - - - {!hideIcon.to && } - {dateRange.to ? renderFormattedDate(dateRange.to) : renderPlaceholder ? placeholder.to : ""} - + {mergeDates ? ( + // Merged date display +
+ {!hideIcon.from && } + {dateRange.from || dateRange.to ? ( + + ) : ( + renderPlaceholder && ( + <> + {placeholder.from} + + {placeholder.to} + + ) + )} + {isClearable && !disabled && hasDisplayedDates && ( + { + e.stopPropagation(); + e.preventDefault(); + clearDates(); + }} + /> + )} +
+ ) : ( + // Original separate date display + <> + + {!hideIcon.from && } + {dateRange.from ? renderFormattedDate(dateRange.from) : renderPlaceholder ? placeholder.from : ""} + + + + {!hideIcon.to && } + {dateRange.to ? renderFormattedDate(dateRange.to) : renderPlaceholder ? placeholder.to : ""} + + {isClearable && !disabled && hasDisplayedDates && ( + { + e.stopPropagation(); + e.preventDefault(); + clearDates(); + }} + /> + )} + + )} ); diff --git a/web/core/components/dropdowns/index.ts b/web/core/components/dropdowns/index.ts index 64b7efe80..0948f5e75 100644 --- a/web/core/components/dropdowns/index.ts +++ b/web/core/components/dropdowns/index.ts @@ -3,6 +3,7 @@ export * from "./cycle"; export * from "./date-range"; export * from "./date"; export * from "./estimate"; +export * from "./merged-date"; export * from "./module"; export * from "./priority"; export * from "./project"; diff --git a/web/core/components/dropdowns/merged-date.tsx b/web/core/components/dropdowns/merged-date.tsx new file mode 100644 index 000000000..ca2adc46e --- /dev/null +++ b/web/core/components/dropdowns/merged-date.tsx @@ -0,0 +1,34 @@ +import React from "react"; +import { observer } from "mobx-react"; +// helpers +import { formatDateRange } from "@plane/utils"; +import { getDate } from "@/helpers/date-time.helper"; + +type Props = { + startDate: Date | string | null | undefined; + endDate: Date | string | null | undefined; + className?: string; +}; + +/** + * Formats merged date range display with smart formatting + * - Single date: "Jan 24, 2025" + * - Same year, same month: "Jan 24 - 28, 2025" + * - Same year, different month: "Jan 24 - Feb 6, 2025" + * - Different year: "Dec 28, 2024 - Jan 4, 2025" + */ +export const MergedDateDisplay: React.FC = observer((props) => { + const { startDate, endDate, className = "" } = props; + + // Parse dates + const parsedStartDate = getDate(startDate); + const parsedEndDate = getDate(endDate); + + const displayText = formatDateRange(parsedStartDate, parsedEndDate); + + if (!displayText) { + return null; + } + + return {displayText}; +}); diff --git a/web/core/components/issues/issue-detail-widgets/sub-issues/issues-list/properties.tsx b/web/core/components/issues/issue-detail-widgets/sub-issues/issues-list/properties.tsx index a6a8fae3d..29b6e2770 100644 --- a/web/core/components/issues/issue-detail-widgets/sub-issues/issues-list/properties.tsx +++ b/web/core/components/issues/issue-detail-widgets/sub-issues/issues-list/properties.tsx @@ -1,11 +1,10 @@ // plane imports import { SyntheticEvent } from "react"; import { observer } from "mobx-react"; -import { CalendarClock } from "lucide-react"; import { useTranslation } from "@plane/i18n"; import { IIssueDisplayProperties, TIssue } from "@plane/types"; // components -import { PriorityDropdown, MemberDropdown, StateDropdown, DateDropdown } from "@/components/dropdowns"; +import { PriorityDropdown, MemberDropdown, StateDropdown, DateRangeDropdown } from "@/components/dropdowns"; // hooks import { WithDisplayPropertiesHOC } from "@/components/issues/issue-layouts/properties/with-display-properties-HOC"; import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper"; @@ -37,6 +36,22 @@ export const SubIssuesListItemProperties: React.FC = observer((props) => e.preventDefault(); }; + const handleStartDate = (date: Date | null) => { + if (issue.project_id) { + updateSubIssue(workspaceSlug, issue.project_id, parentIssueId, issueId, { + start_date: date ? renderFormattedPayloadDate(date) : null, + }); + } + }; + + const handleTargetDate = (date: Date | null) => { + if (issue.project_id) { + updateSubIssue(workspaceSlug, issue.project_id, parentIssueId, issueId, { + target_date: date ? renderFormattedPayloadDate(date) : null, + }); + } + }; + if (!displayProperties) return <>; const maxDate = getDate(issue.target_date); @@ -88,29 +103,32 @@ export const SubIssuesListItemProperties: React.FC = observer((props) =>
- -
- - issue.project_id && - updateSubIssue( - workspaceSlug, - issue.project_id, - parentIssueId, - issueId, - { - target_date: val ? renderFormattedPayloadDate(val) : null, - }, - { ...issue } - ) - } - maxDate={maxDate} - placeholder={t("common.order_by.due_date")} - icon={} - buttonVariant={issue.target_date ? "border-with-text" : "border-without-text"} - optionsClassName="z-30" + {/* merged dates */} + !!(properties.start_date || properties.due_date)} + > +
+ { + handleStartDate(range?.from ?? null); + handleTargetDate(range?.to ?? null); + }} + hideIcon={{ + from: false, + }} + isClearable + mergeDates + buttonVariant={issue.start_date || issue.target_date ? "border-with-text" : "border-without-text"} disabled={!disabled} + showTooltip + customTooltipHeading="Date Range" + renderPlaceholder={false} />
diff --git a/web/core/components/issues/issue-layouts/properties/all-properties.tsx b/web/core/components/issues/issue-layouts/properties/all-properties.tsx index c14922b04..aacabc448 100644 --- a/web/core/components/issues/issue-layouts/properties/all-properties.tsx +++ b/web/core/components/issues/issue-layouts/properties/all-properties.tsx @@ -5,7 +5,7 @@ import xor from "lodash/xor"; import { observer } from "mobx-react"; import { useParams, usePathname } from "next/navigation"; // icons -import { CalendarCheck2, CalendarClock, Layers, Link, Paperclip } from "lucide-react"; +import { Layers, Link, Paperclip } from "lucide-react"; // types import { ISSUE_UPDATED } from "@plane/constants"; // i18n @@ -15,13 +15,13 @@ import { TIssue, IIssueDisplayProperties, TIssuePriorities } from "@plane/types" import { Tooltip } from "@plane/ui"; // components import { - DateDropdown, EstimateDropdown, PriorityDropdown, MemberDropdown, ModuleDropdown, CycleDropdown, StateDropdown, + DateRangeDropdown, } from "@/components/dropdowns"; // constants // helpers @@ -265,12 +265,6 @@ export const IssueProperties: React.FC = observer((props) => { const defaultLabelOptions = issue?.label_ids?.map((id) => labelMap[id]) || []; - const minDate = getDate(issue.start_date); - minDate?.setDate(minDate.getDate()); - - const maxDate = getDate(issue.target_date); - maxDate?.setDate(maxDate.getDate()); - const handleEventPropagation = (e: SyntheticEvent) => { e.stopPropagation(); e.preventDefault(); @@ -310,40 +304,34 @@ export const IssueProperties: React.FC = observer((props) => {
- {/* start date */} - + {/* merged dates */} + !!(properties.start_date || properties.due_date)} + >
- } - buttonVariant={issue.start_date ? "border-with-text" : "border-without-text"} - optionsClassName="z-10" - disabled={isReadOnly} - renderByDefault={isMobile} - showTooltip - /> -
-
- - {/* target/due date */} - -
- } - buttonVariant={issue.target_date ? "border-with-text" : "border-without-text"} + { + handleStartDate(range?.from ?? null); + handleTargetDate(range?.to ?? null); + }} + hideIcon={{ + from: false, + }} + isClearable + mergeDates + buttonVariant={issue.start_date || issue.target_date ? "border-with-text" : "border-without-text"} buttonClassName={shouldHighlightIssueDueDate(issue.target_date, stateDetails?.group) ? "text-red-500" : ""} - clearIconClassName="!text-custom-text-100" - optionsClassName="z-10" disabled={isReadOnly} renderByDefault={isMobile} showTooltip + renderPlaceholder={false} + customTooltipHeading="Date Range" />
diff --git a/web/core/components/modules/module-list-item-action.tsx b/web/core/components/modules/module-list-item-action.tsx index 7a1b37a65..8235ad8b2 100644 --- a/web/core/components/modules/module-list-item-action.tsx +++ b/web/core/components/modules/module-list-item-action.tsx @@ -158,6 +158,7 @@ export const ModuleListItemAction: FC = observer((props) => { target_date: val?.to ? renderFormattedPayloadDate(val.to) : null, }); }} + mergeDates placeholder={{ from: t("start_date"), to: t("end_date"), From 11debee4028016a3347efc2942d14869bfbc5348 Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Mon, 9 Jun 2025 00:31:27 +0530 Subject: [PATCH 141/201] fix: build errors related to project member list (#7185) --- packages/types/src/project/projects.d.ts | 3 ++- web/core/store/member/base-project-member.store.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/types/src/project/projects.d.ts b/packages/types/src/project/projects.d.ts index ae1897d30..360a92c55 100644 --- a/packages/types/src/project/projects.d.ts +++ b/packages/types/src/project/projects.d.ts @@ -85,15 +85,16 @@ export interface IProjectMemberLite { export type TProjectMembership = { member: string; role: TUserPermissions | EUserProjectRoles; - created_at: string; } & ( | { id: string; original_role: EUserProjectRoles; + created_at: string; } | { id: null; original_role: null; + created_at: null; } ); diff --git a/web/core/store/member/base-project-member.store.ts b/web/core/store/member/base-project-member.store.ts index 705115306..40047b065 100644 --- a/web/core/store/member/base-project-member.store.ts +++ b/web/core/store/member/base-project-member.store.ts @@ -177,7 +177,7 @@ export abstract class BaseProjectMemberStore implements IBaseProjectMemberStore original_role: projectMember.original_role, member: { ...userDetails, - joining_date: projectMember.created_at, + joining_date: projectMember.created_at ?? undefined, }, created_at: projectMember.created_at, }; From b3b285b1e50bb2576e993aa53792e75cc1dc5630 Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Mon, 9 Jun 2025 12:49:26 +0530 Subject: [PATCH 142/201] chore: upgrade django version to 4.2.22 --- apiserver/requirements/base.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apiserver/requirements/base.txt b/apiserver/requirements/base.txt index 6cdb4d8b2..3a12b9bf6 100644 --- a/apiserver/requirements/base.txt +++ b/apiserver/requirements/base.txt @@ -1,7 +1,7 @@ # base requirements # django -Django==4.2.21 +Django==4.2.22 # rest framework djangorestframework==3.15.2 # postgres From d91d7a2f6092b4d3b5224dc07ed8c9f7b57bbc05 Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Mon, 9 Jun 2025 12:58:18 +0530 Subject: [PATCH 143/201] chore: tar-fs patch upgrade --- yarn.lock | 5 ----- 1 file changed, 5 deletions(-) diff --git a/yarn.lock b/yarn.lock index 29dd63ada..42bed480b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11190,11 +11190,6 @@ use-callback-ref@^1.3.3: dependencies: tslib "^2.0.0" -use-debounce@^9.0.4: - version "9.0.4" - resolved "https://registry.npmjs.org/use-debounce/-/use-debounce-9.0.4.tgz#51d25d856fbdfeb537553972ce3943b897f1ac85" - integrity sha512-6X8H/mikbrt0XE8e+JXRtZ8yYVvKkdYRfmIhWZYsP8rcNs9hk3APV8Ua2mFkKRLcJKVdnX2/Vwrmg2GWKUQEaQ== - use-font-face-observer@^1.2.2: version "1.2.2" resolved "https://registry.npmjs.org/use-font-face-observer/-/use-font-face-observer-1.2.2.tgz#ed230d907589c6b17e8c8b896c9f5913968ac5ed" From c86e7e02bcac9ea422d444c863010378dfad6654 Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Mon, 9 Jun 2025 13:19:14 +0530 Subject: [PATCH 144/201] chore: upgrade tar-fs package to fix vulnerabilities --- package.json | 3 ++- turbo.json | 1 - yarn.lock | 66 +++++++--------------------------------------------- 3 files changed, 11 insertions(+), 59 deletions(-) diff --git a/package.json b/package.json index d8d8128ad..dd7f3fd8a 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,8 @@ "esbuild": "0.25.0", "@babel/helpers": "7.26.10", "@babel/runtime": "7.26.10", - "chokidar": "3.6.0" + "chokidar": "3.6.0", + "tar-fs": "3.0.9" }, "packageManager": "yarn@1.22.22" } diff --git a/turbo.json b/turbo.json index 65f289b9c..36e95edbd 100644 --- a/turbo.json +++ b/turbo.json @@ -1,6 +1,5 @@ { "$schema": "https://turbo.build/schema.json", - "ui": "tui", "globalEnv": [ "NODE_ENV", "NEXT_PUBLIC_API_BASE_URL", diff --git a/yarn.lock b/yarn.lock index 42bed480b..66dfc896e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4240,15 +4240,6 @@ bind-event-listener@^3.0.0: resolved "https://registry.npmjs.org/bind-event-listener/-/bind-event-listener-3.0.0.tgz#c90f9a7fcb65cac21045f810c20ef7e647a74921" integrity sha512-PJvH288AWQhKs2v9zyfYdPzlPqf5bXbGMmhmUIY9x4dAUGIWgomO771oBQNwJnMQSnUIXhKu6sgzpBRXTlvb8Q== -bl@^4.0.3: - version "4.1.0" - resolved "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" - integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== - dependencies: - buffer "^5.5.0" - inherits "^2.0.4" - readable-stream "^3.4.0" - bluebird@^3.7.2: version "3.7.2" resolved "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" @@ -4333,14 +4324,6 @@ buffer-from@^1.0.0: resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== -buffer@^5.5.0: - version "5.7.1" - resolved "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" - integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== - dependencies: - base64-js "^1.3.1" - ieee754 "^1.1.13" - buffer@^6.0.3: version "6.0.3" resolved "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" @@ -4506,11 +4489,6 @@ chokidar@3.6.0, chokidar@^3.3.0, chokidar@^3.5.2, chokidar@^3.5.3, chokidar@^3.6 optionalDependencies: fsevents "~2.3.2" -chownr@^1.1.1: - version "1.1.4" - resolved "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" - integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== - chromatic@^11.4.0: version "11.25.2" resolved "https://registry.npmjs.org/chromatic/-/chromatic-11.25.2.tgz#cb93dc1332d8f6b70d97a3ef126bc6d03429d396" @@ -5442,7 +5420,7 @@ encodeurl@~2.0.0: resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58" integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== -end-of-stream@^1.1.0, end-of-stream@^1.4.1: +end-of-stream@^1.1.0: version "1.4.4" resolved "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== @@ -6315,11 +6293,6 @@ fresh@0.5.2: resolved "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== -fs-constants@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" - integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== - fs-extra@^10.0.0: version "10.1.0" resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf" @@ -6780,7 +6753,7 @@ icss-utils@^5.0.0, icss-utils@^5.1.0: resolved "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae" integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA== -ieee754@^1.1.13, ieee754@^1.2.1: +ieee754@^1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== @@ -6833,7 +6806,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: +inherits@2, inherits@2.0.4, inherits@^2.0.3, inherits@~2.0.3: version "2.0.4" resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -8019,7 +7992,7 @@ minipass@^4.2.4: resolved "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== -mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3: +mkdirp-classic@^0.5.3: version "0.5.3" resolved "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== @@ -9474,7 +9447,7 @@ read-cache@^1.0.0: dependencies: pify "^2.3.0" -readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.2: +readable-stream@^3.4.0, readable-stream@^3.6.2: version "3.6.2" resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== @@ -10527,20 +10500,10 @@ tapable@^2.0.0, tapable@^2.1.1, tapable@^2.2.0, tapable@^2.2.1: resolved "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== -tar-fs@^2.0.0: - version "2.1.2" - resolved "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.2.tgz#425f154f3404cb16cb8ff6e671d45ab2ed9596c5" - integrity sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA== - dependencies: - chownr "^1.1.1" - mkdirp-classic "^0.5.2" - pump "^3.0.0" - tar-stream "^2.1.4" - -tar-fs@^3.0.4: - version "3.0.8" - resolved "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.8.tgz#8f62012537d5ff89252d01e48690dc4ebed33ab7" - integrity sha512-ZoROL70jptorGAlgAYiLoBLItEKw/fUxg9BSYK/dF/GAGYFJOJJJMvjPAKDJraCXFwadD456FCuvLWgfhMsPwg== +tar-fs@3.0.9, tar-fs@^2.0.0, tar-fs@^3.0.4: + version "3.0.9" + resolved "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.9.tgz#d570793c6370d7078926c41fa422891566a0b617" + integrity sha512-XF4w9Xp+ZQgifKakjZYmFdkLoSWd34VGKcsTCwlNWM7QG3ZbaxnTsaBwnjFZqHRf/rROxaR8rXnbtwdvaDI+lA== dependencies: pump "^3.0.0" tar-stream "^3.1.5" @@ -10548,17 +10511,6 @@ tar-fs@^3.0.4: bare-fs "^4.0.1" bare-path "^3.0.0" -tar-stream@^2.1.4: - version "2.2.0" - resolved "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287" - integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ== - dependencies: - bl "^4.0.3" - end-of-stream "^1.4.1" - fs-constants "^1.0.0" - inherits "^2.0.3" - readable-stream "^3.1.1" - tar-stream@^3.1.5: version "3.1.7" resolved "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz#24b3fb5eabada19fe7338ed6d26e5f7c482e792b" From 5a43ec8411cc54a8b9a4307995b322f06f78cd73 Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Mon, 9 Jun 2025 13:20:07 +0530 Subject: [PATCH 145/201] chore: turbo repo version upgrade --- package.json | 2 +- yarn.lock | 68 ++++++++++++++++++++++++++-------------------------- 2 files changed, 35 insertions(+), 35 deletions(-) diff --git a/package.json b/package.json index dd7f3fd8a..e8a0b22d7 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "devDependencies": { "prettier": "latest", "prettier-plugin-tailwindcss": "^0.5.4", - "turbo": "^2.5.3" + "turbo": "^2.5.4" }, "resolutions": { "nanoid": "3.3.8", diff --git a/yarn.lock b/yarn.lock index 66dfc896e..dd601e5c6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10818,47 +10818,47 @@ tunnel-agent@^0.6.0: dependencies: safe-buffer "^5.0.1" -turbo-darwin-64@2.5.3: - version "2.5.3" - resolved "https://registry.yarnpkg.com/turbo-darwin-64/-/turbo-darwin-64-2.5.3.tgz#e1f19e816f76e0d636e31e66f8238c43bf870f45" - integrity sha512-YSItEVBUIvAGPUDpAB9etEmSqZI3T6BHrkBkeSErvICXn3dfqXUfeLx35LfptLDEbrzFUdwYFNmt8QXOwe9yaw== +turbo-darwin-64@2.5.4: + version "2.5.4" + resolved "https://registry.npmjs.org/turbo-darwin-64/-/turbo-darwin-64-2.5.4.tgz#f03b3f071365c626d05e84d0d4b9db96348f2951" + integrity sha512-ah6YnH2dErojhFooxEzmvsoZQTMImaruZhFPfMKPBq8sb+hALRdvBNLqfc8NWlZq576FkfRZ/MSi4SHvVFT9PQ== -turbo-darwin-arm64@2.5.3: - version "2.5.3" - resolved "https://registry.yarnpkg.com/turbo-darwin-arm64/-/turbo-darwin-arm64-2.5.3.tgz#f80074fd786f703bcb0415e13df225ba781950fc" - integrity sha512-5PefrwHd42UiZX7YA9m1LPW6x9YJBDErXmsegCkVp+GjmWrADfEOxpFrGQNonH3ZMj77WZB2PVE5Aw3gA+IOhg== +turbo-darwin-arm64@2.5.4: + version "2.5.4" + resolved "https://registry.npmjs.org/turbo-darwin-arm64/-/turbo-darwin-arm64-2.5.4.tgz#614becf281da75af5a01094373f380ac2b48190a" + integrity sha512-2+Nx6LAyuXw2MdXb7pxqle3MYignLvS7OwtsP9SgtSBaMlnNlxl9BovzqdYAgkUW3AsYiQMJ/wBRb7d+xemM5A== -turbo-linux-64@2.5.3: - version "2.5.3" - resolved "https://registry.yarnpkg.com/turbo-linux-64/-/turbo-linux-64-2.5.3.tgz#93bfe009a24a76295c8164896845b5098c293293" - integrity sha512-M9xigFgawn5ofTmRzvjjLj3Lqc05O8VHKuOlWNUlnHPUltFquyEeSkpQNkE/vpPdOR14AzxqHbhhxtfS4qvb1w== +turbo-linux-64@2.5.4: + version "2.5.4" + resolved "https://registry.npmjs.org/turbo-linux-64/-/turbo-linux-64-2.5.4.tgz#fc7425f35feb19a8373e7926eccdd4a3b2fb77a4" + integrity sha512-5May2kjWbc8w4XxswGAl74GZ5eM4Gr6IiroqdLhXeXyfvWEdm2mFYCSWOzz0/z5cAgqyGidF1jt1qzUR8hTmOA== -turbo-linux-arm64@2.5.3: - version "2.5.3" - resolved "https://registry.yarnpkg.com/turbo-linux-arm64/-/turbo-linux-arm64-2.5.3.tgz#bf4664561094711aa289d92b9443de2aefca5d6e" - integrity sha512-auJRbYZ8SGJVqvzTikpg1bsRAsiI9Tk0/SDkA5Xgg0GdiHDH/BOzv1ZjDE2mjmlrO/obr19Dw+39OlMhwLffrw== +turbo-linux-arm64@2.5.4: + version "2.5.4" + resolved "https://registry.npmjs.org/turbo-linux-arm64/-/turbo-linux-arm64-2.5.4.tgz#ab5418a88089e4ec20b2c28c3d3c6b8a99b07a81" + integrity sha512-/2yqFaS3TbfxV3P5yG2JUI79P7OUQKOUvAnx4MV9Bdz6jqHsHwc9WZPpO4QseQm+NvmgY6ICORnoVPODxGUiJg== -turbo-windows-64@2.5.3: - version "2.5.3" - resolved "https://registry.yarnpkg.com/turbo-windows-64/-/turbo-windows-64-2.5.3.tgz#acbc2db093c7a74f0e692b899e649284285a2e7b" - integrity sha512-arLQYohuHtIEKkmQSCU9vtrKUg+/1TTstWB9VYRSsz+khvg81eX6LYHtXJfH/dK7Ho6ck+JaEh5G+QrE1jEmCQ== +turbo-windows-64@2.5.4: + version "2.5.4" + resolved "https://registry.npmjs.org/turbo-windows-64/-/turbo-windows-64-2.5.4.tgz#fc78585d3950e5cd64bed8d6648f078e7f32f9a5" + integrity sha512-EQUO4SmaCDhO6zYohxIjJpOKRN3wlfU7jMAj3CgcyTPvQR/UFLEKAYHqJOnJtymbQmiiM/ihX6c6W6Uq0yC7mA== -turbo-windows-arm64@2.5.3: - version "2.5.3" - resolved "https://registry.yarnpkg.com/turbo-windows-arm64/-/turbo-windows-arm64-2.5.3.tgz#78e8cfdb49b69fbadf03031532f1524be3661729" - integrity sha512-3JPn66HAynJ0gtr6H+hjY4VHpu1RPKcEwGATvGUTmLmYSYBQieVlnGDRMMoYN066YfyPqnNGCfhYbXfH92Cm0g== +turbo-windows-arm64@2.5.4: + version "2.5.4" + resolved "https://registry.npmjs.org/turbo-windows-arm64/-/turbo-windows-arm64-2.5.4.tgz#5134537cc9fa27f4647f2f899b7ba2dacfc6ad22" + integrity sha512-oQ8RrK1VS8lrxkLriotFq+PiF7iiGgkZtfLKF4DDKsmdbPo0O9R2mQxm7jHLuXraRCuIQDWMIw6dpcr7Iykf4A== -turbo@^2.5.3: - version "2.5.3" - resolved "https://registry.yarnpkg.com/turbo/-/turbo-2.5.3.tgz#657dcae430552d9bb237e9e1d91f711c465c9f28" - integrity sha512-iHuaNcq5GZZnr3XDZNuu2LSyCzAOPwDuo5Qt+q64DfsTP1i3T2bKfxJhni2ZQxsvAoxRbuUK5QetJki4qc5aYA== +turbo@^2.5.4: + version "2.5.4" + resolved "https://registry.npmjs.org/turbo/-/turbo-2.5.4.tgz#e46213a4560b94e56c014e0fd56d06605de16753" + integrity sha512-kc8ZibdRcuWUG1pbYSBFWqmIjynlD8Lp7IB6U3vIzvOv9VG+6Sp8bzyeBWE3Oi8XV5KsQrznyRTBPvrf99E4mA== optionalDependencies: - turbo-darwin-64 "2.5.3" - turbo-darwin-arm64 "2.5.3" - turbo-linux-64 "2.5.3" - turbo-linux-arm64 "2.5.3" - turbo-windows-64 "2.5.3" - turbo-windows-arm64 "2.5.3" + turbo-darwin-64 "2.5.4" + turbo-darwin-arm64 "2.5.4" + turbo-linux-64 "2.5.4" + turbo-linux-arm64 "2.5.4" + turbo-windows-64 "2.5.4" + turbo-windows-arm64 "2.5.4" tween-functions@^1.2.0: version "1.2.0" From 1f1b4217352aeedd80320780efff315ec5a2f56c Mon Sep 17 00:00:00 2001 From: Farahat Abdrabouh <88924701+fasdjkherig@users.noreply.github.com> Date: Mon, 9 Jun 2025 10:52:07 +0300 Subject: [PATCH 146/201] Docs: Correct numeric values in contributing guide #7184 --- CONTRIBUTING.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 03d667f4a..4a1567520 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -69,14 +69,14 @@ chmod +x setup.sh docker compose -f docker-compose-local.yml up ``` -5. Start web apps: +4. Start web apps: ```bash yarn dev ``` -6. Open your browser to http://localhost:3001/god-mode/ and register yourself as instance admin -7. Open up your browser to http://localhost:3000 then log in using the same credentials from the previous step +5. Open your browser to http://localhost:3001/god-mode/ and register yourself as instance admin +6. Open up your browser to http://localhost:3000 then log in using the same credentials from the previous step That’s it! You’re all set to begin coding. Remember to refresh your browser if changes don’t auto-reload. Happy contributing! 🎉 From 07e937cd8ec16fc9ce84a8535ea2f32d1b445af2 Mon Sep 17 00:00:00 2001 From: Vamsi Krishna <46787868+vamsikrishnamathala@users.noreply.github.com> Date: Mon, 9 Jun 2025 15:33:57 +0530 Subject: [PATCH 147/201] [WEB-4094]chore: workspace notifications refactor (#7061) * chore: workspace notifications refactor * fix: url params * fix: added null checks to avoid run time errors * fix: notifications header color fix --- packages/types/src/issues/issue.d.ts | 9 +- .../(projects)/notifications/page.tsx | 97 +-------- .../workspace-notifications/index.ts | 1 + .../workspace-notifications/list-root.tsx | 8 + web/ce/hooks/use-notification-preview.tsx | 25 +++ .../issues/issue-detail/subscription.tsx | 13 +- .../components/issues/peek-overview/root.tsx | 10 +- .../workspace-notifications/root.tsx | 188 +++++++++--------- .../sidebar/header/root.tsx | 2 +- .../workspace-notifications/sidebar/index.ts | 2 + .../workspace-notifications/sidebar/root.tsx | 118 +++++++++++ .../store/issue/issue-details/root.store.ts | 2 +- .../issue/issue-details/subscription.store.ts | 6 +- 13 files changed, 273 insertions(+), 208 deletions(-) create mode 100644 web/ce/components/workspace-notifications/list-root.tsx create mode 100644 web/ce/hooks/use-notification-preview.tsx create mode 100644 web/core/components/workspace-notifications/sidebar/root.tsx diff --git a/packages/types/src/issues/issue.d.ts b/packages/types/src/issues/issue.d.ts index a9d8970f9..01c0b2f3a 100644 --- a/packages/types/src/issues/issue.d.ts +++ b/packages/types/src/issues/issue.d.ts @@ -1,4 +1,4 @@ -import { EIssueServiceType } from "@plane/constants"; +import { EIssueServiceType, EIssuesStoreType } from "@plane/constants"; import { TIssuePriorities } from "../issues"; import { TIssueAttachment } from "./issue_attachment"; import { TIssueLink } from "./issue_link"; @@ -181,3 +181,10 @@ export type TPublicIssuesResponse = { extra_stats: null; results: TPublicIssueResponseResults; }; + +export interface IWorkItemPeekOverview { + embedIssue?: boolean; + embedRemoveCurrentNotification?: () => void; + is_draft?: boolean; + storeType?: EIssuesStoreType; +} \ No newline at end of file diff --git a/web/app/(all)/[workspaceSlug]/(projects)/notifications/page.tsx b/web/app/(all)/[workspaceSlug]/(projects)/notifications/page.tsx index 8afe768d8..415ea8fbf 100644 --- a/web/app/(all)/[workspaceSlug]/(projects)/notifications/page.tsx +++ b/web/app/(all)/[workspaceSlug]/(projects)/notifications/page.tsx @@ -1,22 +1,14 @@ "use client"; -import { useCallback, useEffect } from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; -import useSWR from "swr"; // plane imports -import { ENotificationLoader, ENotificationQueryParamType } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; // components -import { LogoSpinner } from "@/components/common"; import { PageHead } from "@/components/core"; -import { SimpleEmptyState } from "@/components/empty-state"; -import { InboxContentRoot } from "@/components/inbox"; -import { IssuePeekOverview } from "@/components/issues"; +import { NotificationsRoot } from "@/components/workspace-notifications"; // hooks -import { useIssueDetail, useUserPermissions, useWorkspace, useWorkspaceNotifications } from "@/hooks/store"; -import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; -import { useWorkspaceIssueProperties } from "@/hooks/use-workspace-issue-properties"; +import { useWorkspace } from "@/hooks/store"; const WorkspaceDashboardPage = observer(() => { const { workspaceSlug } = useParams(); @@ -24,98 +16,15 @@ const WorkspaceDashboardPage = observer(() => { const { t } = useTranslation(); // hooks const { currentWorkspace } = useWorkspace(); - const { - currentSelectedNotificationId, - setCurrentSelectedNotificationId, - notificationLiteByNotificationId, - notificationIdsByWorkspaceId, - getNotifications, - } = useWorkspaceNotifications(); - const { fetchUserProjectInfo } = useUserPermissions(); - const { setPeekIssue } = useIssueDetail(); // derived values const pageTitle = currentWorkspace?.name ? t("notification.page_label", { workspace: currentWorkspace?.name }) : undefined; - const { workspace_slug, project_id, issue_id, is_inbox_issue } = - notificationLiteByNotificationId(currentSelectedNotificationId); - const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/intake/issue-detail" }); - - // fetching workspace work item properties - useWorkspaceIssueProperties(workspaceSlug); - - // fetch workspace notifications - const notificationMutation = - currentWorkspace && notificationIdsByWorkspaceId(currentWorkspace.id) - ? ENotificationLoader.MUTATION_LOADER - : ENotificationLoader.INIT_LOADER; - const notificationLoader = - currentWorkspace && notificationIdsByWorkspaceId(currentWorkspace.id) - ? ENotificationQueryParamType.CURRENT - : ENotificationQueryParamType.INIT; - useSWR( - currentWorkspace?.slug ? `WORKSPACE_NOTIFICATION` : null, - currentWorkspace?.slug - ? () => getNotifications(currentWorkspace?.slug, notificationMutation, notificationLoader) - : null - ); - - // fetching user project member info - const { isLoading: projectMemberInfoLoader } = useSWR( - workspace_slug && project_id && is_inbox_issue - ? `PROJECT_MEMBER_PERMISSION_INFO_${workspace_slug}_${project_id}` - : null, - workspace_slug && project_id && is_inbox_issue ? () => fetchUserProjectInfo(workspace_slug, project_id) : null - ); - - const embedRemoveCurrentNotification = useCallback( - () => setCurrentSelectedNotificationId(undefined), - [setCurrentSelectedNotificationId] - ); - - // clearing up the selected notifications when unmounting the page - useEffect( - () => () => { - setCurrentSelectedNotificationId(undefined); - setPeekIssue(undefined); - }, - [setCurrentSelectedNotificationId, setPeekIssue] - ); return ( <> -
- {!currentSelectedNotificationId ? ( -
- -
- ) : ( - <> - {is_inbox_issue === true && workspace_slug && project_id && issue_id ? ( - <> - {projectMemberInfoLoader ? ( -
- -
- ) : ( - {}} - isMobileSidebar={false} - workspaceSlug={workspace_slug} - projectId={project_id} - inboxIssueId={issue_id} - isNotificationEmbed - embedRemoveCurrentNotification={embedRemoveCurrentNotification} - /> - )} - - ) : ( - - )} - - )} -
+ ); }); diff --git a/web/ce/components/workspace-notifications/index.ts b/web/ce/components/workspace-notifications/index.ts index 18c4afa96..c12683ce6 100644 --- a/web/ce/components/workspace-notifications/index.ts +++ b/web/ce/components/workspace-notifications/index.ts @@ -1 +1,2 @@ export * from "./notification-card/root"; +export * from "./list-root"; diff --git a/web/ce/components/workspace-notifications/list-root.tsx b/web/ce/components/workspace-notifications/list-root.tsx new file mode 100644 index 000000000..55fd68c3f --- /dev/null +++ b/web/ce/components/workspace-notifications/list-root.tsx @@ -0,0 +1,8 @@ +import { NotificationCardListRoot } from "./notification-card/root"; + +export type TNotificationListRoot = { + workspaceSlug: string; + workspaceId: string; +}; + +export const NotificationListRoot = (props: TNotificationListRoot) => ; diff --git a/web/ce/hooks/use-notification-preview.tsx b/web/ce/hooks/use-notification-preview.tsx new file mode 100644 index 000000000..b0c18d554 --- /dev/null +++ b/web/ce/hooks/use-notification-preview.tsx @@ -0,0 +1,25 @@ +import { EIssueServiceType } from "@plane/constants"; +import { IWorkItemPeekOverview } from "@plane/types"; +import { IssuePeekOverview } from "@/components/issues"; +import { useIssueDetail } from "@/hooks/store"; +import { TPeekIssue } from "@/store/issue/issue-details/root.store"; + +export type TNotificationPreview = { + isWorkItem: boolean; + PeekOverviewComponent: React.ComponentType; + setPeekWorkItem: (peekIssue: TPeekIssue | undefined) => void; +}; + +/** + * This function returns if the current active notification is related to work item or an epic. + * @returns isWorkItem: boolean, peekOverviewComponent: IWorkItemPeekOverview, setPeekWorkItem + */ +export const useNotificationPreview = (): TNotificationPreview => { + const { peekIssue, setPeekIssue } = useIssueDetail(EIssueServiceType.ISSUES); + + return { + isWorkItem: Boolean(peekIssue), + PeekOverviewComponent: IssuePeekOverview, + setPeekWorkItem: setPeekIssue, + }; +}; diff --git a/web/core/components/issues/issue-detail/subscription.tsx b/web/core/components/issues/issue-detail/subscription.tsx index e19e787f3..8239d08ac 100644 --- a/web/core/components/issues/issue-detail/subscription.tsx +++ b/web/core/components/issues/issue-detail/subscription.tsx @@ -5,7 +5,7 @@ import isNil from "lodash/isNil"; import { observer } from "mobx-react"; import { Bell, BellOff } from "lucide-react"; // plane-i18n -import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; +import { EUserPermissions, EUserPermissionsLevel, EIssueServiceType } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; // UI import { Button, Loader, TOAST_TYPE, setToast } from "@plane/ui"; @@ -16,17 +16,18 @@ export type TIssueSubscription = { workspaceSlug: string; projectId: string; issueId: string; + serviceType?: EIssueServiceType; }; export const IssueSubscription: FC = observer((props) => { - const { workspaceSlug, projectId, issueId } = props; + const { workspaceSlug, projectId, issueId, serviceType = EIssueServiceType.ISSUES } = props; const { t } = useTranslation(); // hooks const { subscription: { getSubscriptionByIssueId }, createSubscription, removeSubscription, - } = useIssueDetail(); + } = useIssueDetail(serviceType); // state const [loading, setLoading] = useState(false); // hooks @@ -53,12 +54,12 @@ export const IssueSubscription: FC = observer((props) => { : t("issue.subscription.actions.subscribed"), }); setLoading(false); - } catch (error) { + } catch { setLoading(false); setToast({ type: TOAST_TYPE.ERROR, title: t("toast.error"), - message: t("commons.error.message"), + message: t("common.error.message"), }); } }; @@ -78,7 +79,7 @@ export const IssueSubscription: FC = observer((props) => { variant="outline-primary" className="hover:!bg-custom-primary-100/20" onClick={handleSubscription} - disabled={!isEditable} + disabled={!isEditable || loading} > {loading ? ( diff --git a/web/core/components/issues/peek-overview/root.tsx b/web/core/components/issues/peek-overview/root.tsx index 5ed6403bc..9d6653440 100644 --- a/web/core/components/issues/peek-overview/root.tsx +++ b/web/core/components/issues/peek-overview/root.tsx @@ -14,7 +14,7 @@ import { EUserPermissionsLevel, } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; -import { TIssue } from "@plane/types"; +import { TIssue, IWorkItemPeekOverview } from "@plane/types"; // plane ui import { TOAST_TYPE, setPromiseToast, setToast } from "@plane/ui"; // components @@ -24,14 +24,8 @@ import { IssueView, TIssueOperations } from "@/components/issues"; import { useEventTracker, useIssueDetail, useIssues, useUserPermissions } from "@/hooks/store"; import { useIssueStoreType } from "@/hooks/use-issue-layout-store"; -interface IIssuePeekOverview { - embedIssue?: boolean; - embedRemoveCurrentNotification?: () => void; - is_draft?: boolean; - storeType?: EIssuesStoreType; -} -export const IssuePeekOverview: FC = observer((props) => { +export const IssuePeekOverview: FC = observer((props) => { const { embedIssue = false, embedRemoveCurrentNotification, diff --git a/web/core/components/workspace-notifications/root.tsx b/web/core/components/workspace-notifications/root.tsx index 37623a613..6b41a9cea 100644 --- a/web/core/components/workspace-notifications/root.tsx +++ b/web/core/components/workspace-notifications/root.tsx @@ -1,116 +1,116 @@ "use client"; -import { FC, useCallback } from "react"; +import { useCallback, useEffect } from "react"; import { observer } from "mobx-react"; -import { useParams } from "next/navigation"; +import useSWR from "swr"; // plane imports -import { NOTIFICATION_TABS, TNotificationTab } from "@plane/constants"; +import { ENotificationLoader, ENotificationQueryParamType } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; // components -import { Header, Row, ERowVariant, EHeaderVariant, ContentWrapper } from "@plane/ui"; -import { CountChip } from "@/components/common"; -import { - NotificationsLoader, - NotificationEmptyState, - NotificationSidebarHeader, - AppliedFilters, -} from "@/components/workspace-notifications"; -// helpers -import { cn } from "@/helpers/common.helper"; -import { getNumberCount } from "@/helpers/string.helper"; +import { cn } from "@plane/utils"; +import { LogoSpinner } from "@/components/common"; +import { SimpleEmptyState } from "@/components/empty-state"; +import { InboxContentRoot } from "@/components/inbox"; // hooks -import { useWorkspace, useWorkspaceNotifications } from "@/hooks/store"; -import { NotificationCardListRoot } from "@/plane-web/components/workspace-notifications"; -export const NotificationsSidebarRoot: FC = observer(() => { - const { workspaceSlug } = useParams(); +import { useUserPermissions, useWorkspace, useWorkspaceNotifications } from "@/hooks/store"; +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; +import { useWorkspaceIssueProperties } from "@/hooks/use-workspace-issue-properties"; +import { useNotificationPreview } from "@/plane-web/hooks/use-notification-preview"; + +type NotificationsRootProps = { + workspaceSlug?: string; +}; + +export const NotificationsRoot = observer(({ workspaceSlug }: NotificationsRootProps) => { + // plane hooks + const { t } = useTranslation(); // hooks - const { getWorkspaceBySlug } = useWorkspace(); + const { currentWorkspace } = useWorkspace(); const { currentSelectedNotificationId, - unreadNotificationsCount, - loader, + setCurrentSelectedNotificationId, + notificationLiteByNotificationId, notificationIdsByWorkspaceId, - currentNotificationTab, - setCurrentNotificationTab, + getNotifications, } = useWorkspaceNotifications(); - - const { t } = useTranslation(); + const { fetchUserProjectInfo } = useUserPermissions(); + const { isWorkItem, PeekOverviewComponent, setPeekWorkItem } = useNotificationPreview(); // derived values - const workspace = workspaceSlug ? getWorkspaceBySlug(workspaceSlug.toString()) : undefined; - const notificationIds = workspace ? notificationIdsByWorkspaceId(workspace.id) : undefined; + const { workspace_slug, project_id, issue_id, is_inbox_issue } = + notificationLiteByNotificationId(currentSelectedNotificationId); + const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/intake/issue-detail" }); - const handleTabClick = useCallback( - (tabValue: TNotificationTab) => { - if (currentNotificationTab !== tabValue) { - setCurrentNotificationTab(tabValue); - } - }, - [currentNotificationTab, setCurrentNotificationTab] + // fetching workspace work item properties + useWorkspaceIssueProperties(workspaceSlug); + + // fetch workspace notifications + const notificationMutation = + currentWorkspace && notificationIdsByWorkspaceId(currentWorkspace.id) + ? ENotificationLoader.MUTATION_LOADER + : ENotificationLoader.INIT_LOADER; + const notificationLoader = + currentWorkspace && notificationIdsByWorkspaceId(currentWorkspace.id) + ? ENotificationQueryParamType.CURRENT + : ENotificationQueryParamType.INIT; + useSWR( + currentWorkspace?.slug ? `WORKSPACE_NOTIFICATION_${currentWorkspace?.slug}` : null, + currentWorkspace?.slug + ? () => getNotifications(currentWorkspace?.slug, notificationMutation, notificationLoader) + : null ); - if (!workspaceSlug || !workspace) return <>; + // fetching user project member info + const { isLoading: projectMemberInfoLoader } = useSWR( + workspace_slug && project_id && is_inbox_issue + ? `PROJECT_MEMBER_PERMISSION_INFO_${workspace_slug}_${project_id}` + : null, + workspace_slug && project_id && is_inbox_issue ? () => fetchUserProjectInfo(workspace_slug, project_id) : null + ); + + const embedRemoveCurrentNotification = useCallback( + () => setCurrentSelectedNotificationId(undefined), + [setCurrentSelectedNotificationId] + ); + + // clearing up the selected notifications when unmounting the page + useEffect( + () => () => { + setPeekWorkItem(undefined); + }, + [setCurrentSelectedNotificationId, setPeekWorkItem] + ); return ( -
-
- - - - -
- {NOTIFICATION_TABS.map((tab) => ( -
handleTabClick(tab.value)} - > -
-
{t(tab.i18n_label)}
- {tab.count(unreadNotificationsCount) > 0 && ( - - )} -
- {currentNotificationTab === tab.value && ( -
+
+ {!currentSelectedNotificationId ? ( +
+ +
+ ) : ( + <> + {is_inbox_issue === true && workspace_slug && project_id && issue_id ? ( + <> + {projectMemberInfoLoader ? ( +
+ +
+ ) : ( + {}} + isMobileSidebar={false} + workspaceSlug={workspace_slug} + projectId={project_id} + inboxIssueId={issue_id} + isNotificationEmbed + embedRemoveCurrentNotification={embedRemoveCurrentNotification} + /> )} -
- ))} -
- - {/* applied filters */} - - - {/* rendering notifications */} - {loader === "init-loader" ? ( -
- -
- ) : ( - <> - {notificationIds && notificationIds.length > 0 ? ( - - - - ) : ( -
- -
- )} - - )} -
+ + ) : ( + + )} + + )}
); }); diff --git a/web/core/components/workspace-notifications/sidebar/header/root.tsx b/web/core/components/workspace-notifications/sidebar/header/root.tsx index c641bde19..cdda51fbf 100644 --- a/web/core/components/workspace-notifications/sidebar/header/root.tsx +++ b/web/core/components/workspace-notifications/sidebar/header/root.tsx @@ -20,7 +20,7 @@ export const NotificationSidebarHeader: FC = observe if (!workspaceSlug) return <>; return ( -
+
diff --git a/web/core/components/workspace-notifications/sidebar/index.ts b/web/core/components/workspace-notifications/sidebar/index.ts index 9e46ed0e6..4713a9b3c 100644 --- a/web/core/components/workspace-notifications/sidebar/index.ts +++ b/web/core/components/workspace-notifications/sidebar/index.ts @@ -6,3 +6,5 @@ export * from "./header"; export * from "./filters"; export * from "./notification-card"; + +export * from "./root"; diff --git a/web/core/components/workspace-notifications/sidebar/root.tsx b/web/core/components/workspace-notifications/sidebar/root.tsx new file mode 100644 index 000000000..48b94bb1a --- /dev/null +++ b/web/core/components/workspace-notifications/sidebar/root.tsx @@ -0,0 +1,118 @@ +"use client"; +import { FC, useCallback } from "react"; + +import { observer } from "mobx-react"; +import { useParams } from "next/navigation"; +// plane imports +import { NOTIFICATION_TABS, TNotificationTab } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; +// components +import { Header, Row, ERowVariant, EHeaderVariant, ContentWrapper } from "@plane/ui"; +import { CountChip } from "@/components/common"; +import { + NotificationsLoader, + NotificationEmptyState, + NotificationSidebarHeader, + AppliedFilters, +} from "@/components/workspace-notifications"; +// helpers +import { cn } from "@/helpers/common.helper"; +import { getNumberCount } from "@/helpers/string.helper"; +// hooks +import { useWorkspace, useWorkspaceNotifications } from "@/hooks/store"; + +import { NotificationListRoot } from "@/plane-web/components/workspace-notifications/list-root"; + +export const NotificationsSidebarRoot: FC = observer(() => { + const { workspaceSlug } = useParams(); + // hooks + const { getWorkspaceBySlug } = useWorkspace(); + const { + currentSelectedNotificationId, + unreadNotificationsCount, + loader, + notificationIdsByWorkspaceId, + currentNotificationTab, + setCurrentNotificationTab, + } = useWorkspaceNotifications(); + + const { t } = useTranslation(); + // derived values + const workspace = workspaceSlug ? getWorkspaceBySlug(workspaceSlug.toString()) : undefined; + const notificationIds = workspace ? notificationIdsByWorkspaceId(workspace.id) : undefined; + + const handleTabClick = useCallback( + (tabValue: TNotificationTab) => { + if (currentNotificationTab !== tabValue) { + setCurrentNotificationTab(tabValue); + } + }, + [currentNotificationTab, setCurrentNotificationTab] + ); + + if (!workspaceSlug || !workspace) return <>; + + return ( +
+
+ + + + +
+ {NOTIFICATION_TABS.map((tab) => ( +
handleTabClick(tab.value)} + > +
+
{t(tab.i18n_label)}
+ {tab.count(unreadNotificationsCount) > 0 && ( + + )} +
+ {currentNotificationTab === tab.value && ( +
+ )} +
+ ))} +
+ + {/* applied filters */} + + + {/* rendering notifications */} + {loader === "init-loader" ? ( +
+ +
+ ) : ( + <> + {notificationIds && notificationIds.length > 0 ? ( + + + + ) : ( +
+ +
+ )} + + )} +
+
+ ); +}); diff --git a/web/core/store/issue/issue-details/root.store.ts b/web/core/store/issue/issue-details/root.store.ts index 5a3b394b0..1d795dd94 100644 --- a/web/core/store/issue/issue-details/root.store.ts +++ b/web/core/store/issue/issue-details/root.store.ts @@ -204,7 +204,7 @@ export class IssueDetail implements IIssueDetail { this.commentReaction = new IssueCommentReactionStore(this); this.subIssues = new IssueSubIssuesStore(this, serviceType); this.link = new IssueLinkStore(this, serviceType); - this.subscription = new IssueSubscriptionStore(this); + this.subscription = new IssueSubscriptionStore(this, serviceType); this.relation = new IssueRelationStore(this); } diff --git a/web/core/store/issue/issue-details/subscription.store.ts b/web/core/store/issue/issue-details/subscription.store.ts index 69b685c23..da3b2e6bf 100644 --- a/web/core/store/issue/issue-details/subscription.store.ts +++ b/web/core/store/issue/issue-details/subscription.store.ts @@ -1,10 +1,10 @@ import set from "lodash/set"; import { action, makeObservable, observable, runInAction } from "mobx"; // services +import { EIssueServiceType } from "@plane/constants"; import { IssueService } from "@/services/issue/issue.service"; // types import { IIssueDetail } from "./root.store"; - export interface IIssueSubscriptionStoreActions { addSubscription: (issueId: string, isSubscribed: boolean | undefined | null) => void; fetchSubscriptions: (workspaceSlug: string, projectId: string, issueId: string) => Promise; @@ -27,7 +27,7 @@ export class IssueSubscriptionStore implements IIssueSubscriptionStore { // services issueService; - constructor(rootStore: IIssueDetail) { + constructor(rootStore: IIssueDetail, serviceType: EIssueServiceType) { makeObservable(this, { // observables subscriptionMap: observable, @@ -40,7 +40,7 @@ export class IssueSubscriptionStore implements IIssueSubscriptionStore { // root store this.rootIssueDetail = rootStore; // services - this.issueService = new IssueService(); + this.issueService = new IssueService(serviceType); } // helper methods From 8fcffd233881b83a04191c0c5e34f17bfdeda0d6 Mon Sep 17 00:00:00 2001 From: Vamsi Krishna <46787868+vamsikrishnamathala@users.noreply.github.com> Date: Mon, 9 Jun 2025 15:46:57 +0530 Subject: [PATCH 148/201] [WEB-4196]fix: sub work item copy link message #7186 --- .../issues/issue-detail-widgets/sub-issues/helper.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/web/core/components/issues/issue-detail-widgets/sub-issues/helper.ts b/web/core/components/issues/issue-detail-widgets/sub-issues/helper.ts index 8c62e2c7d..564e72c05 100644 --- a/web/core/components/issues/issue-detail-widgets/sub-issues/helper.ts +++ b/web/core/components/issues/issue-detail-widgets/sub-issues/helper.ts @@ -46,10 +46,7 @@ export const useSubIssueOperations = (issueServiceType: TIssueServiceType): TSub type: TOAST_TYPE.SUCCESS, title: t("common.link_copied"), message: t("entity.link_copied_to_clipboard", { - entity: - issueServiceType === EIssueServiceType.ISSUES - ? t("issue.label", { count: 1 }) - : t("epic.label", { count: 1 }), + entity: t("epic.label", { count: 1 }), }), }); }); From d15d7549f72e7f6ecd3890147dae7db31bb981d0 Mon Sep 17 00:00:00 2001 From: Saurabh Kumar <70131915+Saurabhkmr98@users.noreply.github.com> Date: Mon, 9 Jun 2025 16:02:09 +0530 Subject: [PATCH 149/201] [SILO-303] Add external id and external source in project model #7182 --- ...ect_external_id_project_external_source.py | 23 +++++++++++++++++++ apiserver/plane/db/models/project.py | 3 +++ 2 files changed, 26 insertions(+) create mode 100644 apiserver/plane/db/migrations/0097_project_external_id_project_external_source.py diff --git a/apiserver/plane/db/migrations/0097_project_external_id_project_external_source.py b/apiserver/plane/db/migrations/0097_project_external_id_project_external_source.py new file mode 100644 index 000000000..5548f8afd --- /dev/null +++ b/apiserver/plane/db/migrations/0097_project_external_id_project_external_source.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.21 on 2025-06-06 12:33 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('db', '0096_user_is_email_valid_user_masked_at'), + ] + + operations = [ + migrations.AddField( + model_name='project', + name='external_id', + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AddField( + model_name='project', + name='external_source', + field=models.CharField(blank=True, max_length=255, null=True), + ), + ] diff --git a/apiserver/plane/db/models/project.py b/apiserver/plane/db/models/project.py index c4d097ac8..79a0707d3 100644 --- a/apiserver/plane/db/models/project.py +++ b/apiserver/plane/db/models/project.py @@ -122,6 +122,9 @@ class Project(BaseModel): # timezone TIMEZONE_CHOICES = tuple(zip(pytz.all_timezones, pytz.all_timezones)) timezone = models.CharField(max_length=255, default="UTC", choices=TIMEZONE_CHOICES) + # external_id for imports + external_source = models.CharField(max_length=255, null=True, blank=True) + external_id = models.CharField(max_length=255, blank=True, null=True) @property def cover_image_url(self): From 9965f48ba7da003a6400c1f4487278c4319094d5 Mon Sep 17 00:00:00 2001 From: Sangmin Ahn Date: Mon, 9 Jun 2025 19:37:42 +0900 Subject: [PATCH 150/201] fix: prevent prematurely triggered Japanese label creation (#7084) --- packages/ui/src/hooks/use-dropdown-key-down.tsx | 2 +- .../issues/issue-detail/label/select/label-select.tsx | 2 +- .../issues/issue-layouts/properties/label-dropdown.tsx | 2 +- web/core/hooks/use-dropdown-key-down.tsx | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/ui/src/hooks/use-dropdown-key-down.tsx b/packages/ui/src/hooks/use-dropdown-key-down.tsx index b93a4d551..2dfe6532e 100644 --- a/packages/ui/src/hooks/use-dropdown-key-down.tsx +++ b/packages/ui/src/hooks/use-dropdown-key-down.tsx @@ -12,7 +12,7 @@ type TUseDropdownKeyDown = { export const useDropdownKeyDown: TUseDropdownKeyDown = (onOpen, onClose, isOpen, selectActiveItem?) => { const handleKeyDown = useCallback( (event: React.KeyboardEvent) => { - if (event.key === "Enter") { + if (event.key === "Enter" && !event.nativeEvent.isComposing) { if (!isOpen) { event.stopPropagation(); onOpen(); diff --git a/web/core/components/issues/issue-detail/label/select/label-select.tsx b/web/core/components/issues/issue-detail/label/select/label-select.tsx index e89cd45fc..8fc4829bc 100644 --- a/web/core/components/issues/issue-detail/label/select/label-select.tsx +++ b/web/core/components/issues/issue-detail/label/select/label-select.tsx @@ -99,7 +99,7 @@ export const IssueLabelSelect: React.FC = observer((props) => setQuery(""); } - if (query !== "" && e.key === "Enter" && canCreateLabel) { + if (query !== "" && e.key === "Enter" && !e.nativeEvent.isComposing && canCreateLabel) { e.stopPropagation(); e.preventDefault(); await handleAddLabel(query); diff --git a/web/core/components/issues/issue-layouts/properties/label-dropdown.tsx b/web/core/components/issues/issue-layouts/properties/label-dropdown.tsx index 54edb6935..931d0f30c 100644 --- a/web/core/components/issues/issue-layouts/properties/label-dropdown.tsx +++ b/web/core/components/issues/issue-layouts/properties/label-dropdown.tsx @@ -158,7 +158,7 @@ export const LabelDropdown = (props: ILabelDropdownProps) => { setQuery(""); } - if (query !== "" && e.key === "Enter" && canCreateLabel) { + if (query !== "" && e.key === "Enter" && !e.nativeEvent.isComposing && canCreateLabel) { e.preventDefault(); await handleAddLabel(query); } diff --git a/web/core/hooks/use-dropdown-key-down.tsx b/web/core/hooks/use-dropdown-key-down.tsx index cc69906ce..fdb925dc2 100644 --- a/web/core/hooks/use-dropdown-key-down.tsx +++ b/web/core/hooks/use-dropdown-key-down.tsx @@ -21,7 +21,7 @@ export const useDropdownKeyDown: TUseDropdownKeyDown = (onEnterKeyDown, onEscKey const handleKeyDown = useCallback( (event: React.KeyboardEvent) => { - if (event.key === "Enter") { + if (event.key === "Enter" && !event.nativeEvent.isComposing) { stopEventPropagation(event); onEnterKeyDown(); } else if (event.key === "Escape") { From 531748dcc303523f69342767b5763c41692bb237 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Tue, 10 Jun 2025 01:47:59 +0530 Subject: [PATCH 151/201] [WEB-4288] fix: auth page tab index (#7189) * fix: auth page tab index * chore: code refactor --- .../components/account/auth-forms/email.tsx | 25 ++++++++++++------- .../components/account/auth-forms/email.tsx | 2 +- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/space/core/components/account/auth-forms/email.tsx b/space/core/components/account/auth-forms/email.tsx index f0b88407a..97f7c7b67 100644 --- a/space/core/components/account/auth-forms/email.tsx +++ b/space/core/components/account/auth-forms/email.tsx @@ -40,7 +40,7 @@ export const AuthEmailForm: FC = observer((props) => { const isButtonDisabled = email.length === 0 || Boolean(emailError?.email) || isSubmitting; - const [isFocused, setIsFocused] = useState(true) + const [isFocused, setIsFocused] = useState(true); const inputRef = useRef(null); return ( @@ -54,9 +54,12 @@ export const AuthEmailForm: FC = observer((props) => { `relative flex items-center rounded-md bg-onboarding-background-200 border`, !isFocused && Boolean(emailError?.email) ? `border-red-500` : `border-onboarding-border-100` )} - tabIndex={-1} - onFocus={() => {setIsFocused(true)}} - onBlur={() => {setIsFocused(false)}} + onFocus={() => { + setIsFocused(true); + }} + onBlur={() => { + setIsFocused(false); + }} > = observer((props) => { autoFocus ref={inputRef} /> - {email.length > 0 && ( - 0 && ( + )}
{emailError?.email && !isFocused && ( @@ -92,4 +99,4 @@ export const AuthEmailForm: FC = observer((props) => { ); -}); \ No newline at end of file +}); diff --git a/web/core/components/account/auth-forms/email.tsx b/web/core/components/account/auth-forms/email.tsx index 9f3129364..78b26b4c6 100644 --- a/web/core/components/account/auth-forms/email.tsx +++ b/web/core/components/account/auth-forms/email.tsx @@ -55,7 +55,6 @@ export const AuthEmailForm: FC = observer((props) => { `relative flex items-center rounded-md bg-onboarding-background-200 border`, !isFocused && Boolean(emailError?.email) ? `border-red-500` : `border-onboarding-border-100` )} - tabIndex={-1} onFocus={() => { setIsFocused(true); }} @@ -84,6 +83,7 @@ export const AuthEmailForm: FC = observer((props) => { }} className="absolute right-3 size-5 grid place-items-center" aria-label={t("aria_labels.auth_forms.clear_email")} + tabIndex={-1} > From 6adc721b3479649d0ab5bc8f9d53b4e29566436a Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Tue, 10 Jun 2025 13:56:42 +0530 Subject: [PATCH 152/201] [WEB-4283] fix: update group key handling in issue store utilities for state groups (#7191) * fix: update group key handling in issue store utilities for state groups - Introduced a new function to determine the default group key based on the provided groupByKey. - Updated references to use the new function for improved clarity and maintainability. - Adjusted the mapping for "state_detail.group" in the ISSUE_GROUP_BY_KEY to ensure consistency. - Enhanced the getArrayStringArray method to handle group values more effectively. * refactor: clean up filters constants --- packages/constants/src/issue/filter.ts | 40 +----------------- .../store/issue/helpers/base-issues-utils.ts | 17 ++++++-- .../store/issue/helpers/base-issues.store.ts | 41 +++++++++++++------ 3 files changed, 43 insertions(+), 55 deletions(-) diff --git a/packages/constants/src/issue/filter.ts b/packages/constants/src/issue/filter.ts index 2e29474eb..46275d751 100644 --- a/packages/constants/src/issue/filter.ts +++ b/packages/constants/src/issue/filter.ts @@ -136,45 +136,7 @@ export const ISSUE_DISPLAY_FILTERS_BY_PAGE: TIssueFiltersToDisplayByPageType = { ], display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS, display_filters: { - group_by: [ - "state", - "cycle", - "module", - "state_detail.group", - "priority", - "labels", - "assignees", - "created_by", - null, - ], - order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority"], - type: [null, "active", "backlog"], - }, - extra_options: { - access: true, - values: ["show_empty_groups"], - }, - }, - }, - draft_issues: { - list: { - filters: ["priority", "state_group", "cycle", "module", "labels", "start_date", "target_date", "issue_type"], - display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS, - display_filters: { - group_by: ["state_detail.group", "cycle", "module", "priority", "project", "labels", null], - order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority"], - type: [null, "active", "backlog"], - }, - extra_options: { - access: true, - values: ["show_empty_groups"], - }, - }, - kanban: { - filters: ["priority", "state_group", "cycle", "module", "labels", "start_date", "target_date", "issue_type"], - display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS, - display_filters: { - group_by: ["state_detail.group", "cycle", "module", "priority", "project", "labels"], + group_by: ["state", "cycle", "module", "priority", "labels", "assignees", "created_by", null], order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority"], type: [null, "active", "backlog"], }, diff --git a/web/core/store/issue/helpers/base-issues-utils.ts b/web/core/store/issue/helpers/base-issues-utils.ts index dcb4e86ad..331715949 100644 --- a/web/core/store/issue/helpers/base-issues-utils.ts +++ b/web/core/store/issue/helpers/base-issues-utils.ts @@ -5,7 +5,6 @@ import isEmpty from "lodash/isEmpty"; import orderBy from "lodash/orderBy"; import set from "lodash/set"; import uniq from "lodash/uniq"; -import { runInAction } from "mobx"; import { ALL_ISSUES, EIssueFilterType, FILTER_TO_ISSUE_MAP, ISSUE_PRIORITIES } from "@plane/constants"; import { IIssueDisplayFilterOptions, @@ -318,10 +317,22 @@ export const getGroupedWorkItemIds = ( }; } + // Get the default key for the group by key + const getDefaultGroupKey = (groupByKey: TIssueGroupByOptions) => { + switch (groupByKey) { + case "state_detail.group": + return "state__group"; + case null: + return null; + default: + return ISSUE_GROUP_BY_KEY[groupByKey]; + } + }; + // Group work items - const groupKey = ISSUE_GROUP_BY_KEY[groupByKey]; + const groupKey = getDefaultGroupKey(groupByKey); const groupedWorkItems = groupBy(workItems, (item) => { - const value = item[groupKey]; + const value = groupKey ? item[groupKey] : null; if (Array.isArray(value)) { if (value.length === 0) return "None"; // Sort & join to build deterministic set-like key diff --git a/web/core/store/issue/helpers/base-issues.store.ts b/web/core/store/issue/helpers/base-issues.store.ts index b28ff13fa..30ac6fb04 100644 --- a/web/core/store/issue/helpers/base-issues.store.ts +++ b/web/core/store/issue/helpers/base-issues.store.ts @@ -121,7 +121,7 @@ export interface IBaseIssuesStore { export const ISSUE_GROUP_BY_KEY: Record = { project: "project_id", state: "state_id", - "state_detail.group": "state__group" as keyof TIssue, // state_detail.group is only being used for state_group display, + "state_detail.group": "state_id", // state_detail.group is only being used for state_group display, priority: "priority", labels: "label_ids", created_by: "created_by", @@ -137,7 +137,7 @@ export const ISSUE_FILTER_DEFAULT_DATA: Record | undefined, value: string | string[] | undefined | null, @@ -1708,9 +1709,23 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore { // if array return the array if (Array.isArray(value)) return value; - // if the groupKey is state group then return the group based on state_id + return this.getDefaultGroupValue(issueObject, value, groupByKey); + }; + + // /** + // * Gets the default value for a group when the primary value is empty + // * @param issueObject - The issue object to extract fallback values from + // * @param groupByKey - The group by key to determine fallback logic + // * @returns Default group value as string array + // */ + private getDefaultGroupValue = ( + issueObject: Partial, + value: string, + groupByKey?: TIssueGroupByOptions + ): string[] => { + // Handle special case for state group if (groupByKey === "state_detail.group") { - return [this.rootIssueStore.rootStore.state.stateMap?.[value]?.group]; + return [this.rootIssueStore.rootStore.state.stateMap?.[value]?.group ?? issueObject.state__group]; } return [value]; From 32d5fea3d311b7fd2f78415f3f7042e7da5d9f95 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Jun 2025 17:39:48 +0530 Subject: [PATCH 153/201] chore(deps): bump requests (#7193) Bumps the pip group with 1 update in the /apiserver/requirements directory: [requests](https://github.com/psf/requests). Updates `requests` from 2.32.2 to 2.32.4 - [Release notes](https://github.com/psf/requests/releases) - [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md) - [Commits](https://github.com/psf/requests/compare/v2.32.2...v2.32.4) --- updated-dependencies: - dependency-name: requests dependency-version: 2.32.4 dependency-type: direct:production dependency-group: pip ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apiserver/requirements/test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apiserver/requirements/test.txt b/apiserver/requirements/test.txt index 9536ab1e2..66a1ff163 100644 --- a/apiserver/requirements/test.txt +++ b/apiserver/requirements/test.txt @@ -9,4 +9,4 @@ factory-boy==3.3.0 freezegun==1.2.2 coverage==7.2.7 httpx==0.24.1 -requests==2.32.2 \ No newline at end of file +requests==2.32.4 \ No newline at end of file From 9c28db8b7b58cafd200789948bfc3ceb6a93d9f5 Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Tue, 10 Jun 2025 20:32:39 +0530 Subject: [PATCH 154/201] [WEB-4300] improvement: add allowedProjectIds to create work item modal (#7195) --- .../command-palette/modals/issue-level.tsx | 2 + .../issues/issue-modal/provider.tsx | 10 ++++- web/core/components/cycles/form.tsx | 14 ++++-- .../components/issues/issue-modal/base.tsx | 20 +++++---- .../issue-modal/components/project-select.tsx | 44 +++++++++---------- .../context/issue-modal-context.tsx | 1 + .../components/issues/issue-modal/modal.tsx | 7 ++- web/core/components/modules/form.tsx | 8 ++-- web/core/store/base-command-palette.store.ts | 9 +++- web/helpers/project.helper.ts | 12 +---- 10 files changed, 72 insertions(+), 55 deletions(-) diff --git a/web/ce/components/command-palette/modals/issue-level.tsx b/web/ce/components/command-palette/modals/issue-level.tsx index 02eada571..f88908f25 100644 --- a/web/ce/components/command-palette/modals/issue-level.tsx +++ b/web/ce/components/command-palette/modals/issue-level.tsx @@ -39,6 +39,7 @@ export const IssueLevelModals: FC = observer((props) => toggleDeleteIssueModal, isBulkDeleteIssueModalOpen, toggleBulkDeleteIssueModal, + createWorkItemAllowedProjectIds, } = useCommandPalette(); // derived values const issueDetails = issueId ? getIssueById(issueId) : undefined; @@ -80,6 +81,7 @@ export const IssueLevelModals: FC = observer((props) => data={getCreateIssueModalData()} isDraft={isDraftIssue} onSubmit={handleCreateIssueSubmit} + allowedProjectIds={createWorkItemAllowedProjectIds} /> {workspaceSlug && projectId && issueId && issueDetails && ( ; + allowedProjectIds?: string[]; children: React.ReactNode; }; export const IssueModalProvider = observer((props: TIssueModalProviderProps) => { - const { children } = props; + const { children, allowedProjectIds } = props; // states const [selectedParentIssue, setSelectedParentIssue] = useState(null); + // store hooks + const { projectsWithCreatePermissions } = useUser(); + // derived values + const projectIdsWithCreatePermissions = Object.keys(projectsWithCreatePermissions ?? {}); return ( {}, isApplyingTemplate: false, diff --git a/web/core/components/cycles/form.tsx b/web/core/components/cycles/form.tsx index c5c9c9186..601ae557c 100644 --- a/web/core/components/cycles/form.tsx +++ b/web/core/components/cycles/form.tsx @@ -14,8 +14,9 @@ import { DateRangeDropdown, ProjectDropdown } from "@/components/dropdowns"; // constants // helpers import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper"; -import { shouldRenderProject } from "@/helpers/project.helper"; import { getTabIndex } from "@/helpers/tab-indices.helper"; +// hooks +import { useUser } from "@/hooks/store/user/user-user"; type Props = { handleFormSubmit: (values: Partial, dirtyFields: any) => Promise; @@ -36,7 +37,10 @@ const defaultValues: Partial = { export const CycleForm: React.FC = (props) => { const { handleFormSubmit, handleClose, status, projectId, setActiveProject, data, isMobile = false } = props; + // plane hooks const { t } = useTranslation(); + // store hooks + const { projectsWithCreatePermissions } = useUser(); // form data const { formState: { errors, isSubmitting, dirtyFields }, @@ -75,12 +79,14 @@ export const CycleForm: React.FC = (props) => { { - onChange(val); - setActiveProject(val); + if (!Array.isArray(val)) { + onChange(val); + setActiveProject(val); + } }} multiple={false} buttonVariant="border-with-text" - renderCondition={(project) => shouldRenderProject(project)} + renderCondition={(project) => !!projectsWithCreatePermissions?.[project.id]} tabIndex={getIndex("cover_image")} />
diff --git a/web/core/components/issues/issue-modal/base.tsx b/web/core/components/issues/issue-modal/base.tsx index d27223239..80370100f 100644 --- a/web/core/components/issues/issue-modal/base.tsx +++ b/web/core/components/issues/issue-modal/base.tsx @@ -13,7 +13,12 @@ import { CreateIssueToastActionItems, IssuesModalProps } from "@/components/issu // constants // hooks import { useIssueModal } from "@/hooks/context/use-issue-modal"; -import { useEventTracker, useCycle, useIssues, useModule, useIssueDetail, useUser, useProject } from "@/hooks/store"; +import { useCycle } from "@/hooks/store/use-cycle"; +import { useEventTracker } from "@/hooks/store/use-event-tracker"; +import { useIssueDetail } from "@/hooks/store/use-issue-detail"; +import { useIssues } from "@/hooks/store/use-issues"; +import { useModule } from "@/hooks/store/use-module"; +import { useProject } from "@/hooks/store/use-project"; import { useIssueStoreType } from "@/hooks/use-issue-layout-store"; import { useIssuesActions } from "@/hooks/use-issues-actions"; // services @@ -59,14 +64,13 @@ export const CreateUpdateIssueModalBase: React.FC = observer(( const { t } = useTranslation(); const { captureIssueEvent } = useEventTracker(); const { workspaceSlug, projectId: routerProjectId, cycleId, moduleId, workItem } = useParams(); - const { projectsWithCreatePermissions } = useUser(); const { fetchCycleDetails } = useCycle(); const { fetchModuleDetails } = useModule(); const { issues } = useIssues(storeType); const { issues: projectIssues } = useIssues(EIssuesStoreType.PROJECT); const { issues: draftIssues } = useIssues(EIssuesStoreType.WORKSPACE_DRAFT); const { fetchIssue } = useIssueDetail(); - const { handleCreateUpdatePropertyValues } = useIssueModal(); + const { allowedProjectIds, handleCreateUpdatePropertyValues } = useIssueModal(); const { getProjectByIdentifier } = useProject(); // pathname const pathname = usePathname(); @@ -76,7 +80,6 @@ export const CreateUpdateIssueModalBase: React.FC = observer(( const routerProjectIdentifier = workItem?.toString().split("-")[0]; const projectIdFromRouter = getProjectByIdentifier(routerProjectIdentifier)?.id; const projectId = data?.project_id ?? routerProjectId?.toString() ?? projectIdFromRouter; - const projectIdsWithCreatePermissions = Object.keys(projectsWithCreatePermissions ?? {}); const fetchIssueDetail = async (issueId: string | undefined) => { setDescription(undefined); @@ -114,10 +117,9 @@ export const CreateUpdateIssueModalBase: React.FC = observer(( return; } - // if data is not present, set active project to the project - // in the url. This has the least priority. - if (projectIdsWithCreatePermissions && projectIdsWithCreatePermissions.length > 0 && !activeProjectId) - setActiveProjectId(projectId?.toString() ?? projectIdsWithCreatePermissions?.[0]); + // if data is not present, set active project to the first project in the allowedProjectIds array + if (allowedProjectIds && allowedProjectIds.length > 0 && !activeProjectId) + setActiveProjectId(projectId?.toString() ?? allowedProjectIds?.[0]); // clearing up the description state when we leave the component return () => setDescription(undefined); @@ -346,7 +348,7 @@ export const CreateUpdateIssueModalBase: React.FC = observer(( const handleDuplicateIssueModal = (value: boolean) => setIsDuplicateModalOpen(value); // don't open the modal if there are no projects - if (!projectIdsWithCreatePermissions || projectIdsWithCreatePermissions.length === 0 || !activeProjectId) return null; + if (!allowedProjectIds || allowedProjectIds.length === 0 || !activeProjectId) return null; const commonIssueModalProps: IssueFormProps = { issueTitleRef: issueTitleRef, diff --git a/web/core/components/issues/issue-modal/components/project-select.tsx b/web/core/components/issues/issue-modal/components/project-select.tsx index 29268b4ad..3c2d235d0 100644 --- a/web/core/components/issues/issue-modal/components/project-select.tsx +++ b/web/core/components/issues/issue-modal/components/project-select.tsx @@ -10,10 +10,9 @@ import { TIssue } from "@plane/types"; // components import { ProjectDropdown } from "@/components/dropdowns"; // helpers -import { shouldRenderProject } from "@/helpers/project.helper"; import { getTabIndex } from "@/helpers/tab-indices.helper"; -// store hooks -import { useUser } from "@/hooks/store"; +// hooks +import { useIssueModal } from "@/hooks/context/use-issue-modal"; import { usePlatformOS } from "@/hooks/use-platform-os"; type TIssueProjectSelectProps = { @@ -25,8 +24,9 @@ type TIssueProjectSelectProps = { export const IssueProjectSelect: React.FC = observer((props) => { const { control, disabled = false, handleFormChange } = props; // store hooks - const { projectsWithCreatePermissions } = useUser(); const { isMobile } = usePlatformOS(); + // context hooks + const { allowedProjectIds } = useIssueModal(); const { getIndex } = getTabIndex(ETabIndices.ISSUE_FORM, isMobile); @@ -37,26 +37,22 @@ export const IssueProjectSelect: React.FC = observer(( rules={{ required: true, }} - render={({ field: { value, onChange } }) => - projectsWithCreatePermissions && projectsWithCreatePermissions[value!] ? ( -
- { - onChange(projectId); - handleFormChange(); - }} - multiple={false} - buttonVariant="border-with-text" - renderCondition={(project) => shouldRenderProject(project)} - tabIndex={getIndex("project_id")} - disabled={disabled} - /> -
- ) : ( - <> - ) - } + render={({ field: { value, onChange } }) => ( +
+ { + onChange(projectId); + handleFormChange(); + }} + multiple={false} + buttonVariant="border-with-text" + renderCondition={(project) => allowedProjectIds.includes(project.id)} + tabIndex={getIndex("project_id")} + disabled={disabled} + /> +
+ )} /> ); }); diff --git a/web/core/components/issues/issue-modal/context/issue-modal-context.tsx b/web/core/components/issues/issue-modal/context/issue-modal-context.tsx index 59d6558a6..064640885 100644 --- a/web/core/components/issues/issue-modal/context/issue-modal-context.tsx +++ b/web/core/components/issues/issue-modal/context/issue-modal-context.tsx @@ -47,6 +47,7 @@ export type THandleParentWorkItemDetailsProps = { }; export type TIssueModalContext = { + allowedProjectIds: string[]; workItemTemplateId: string | null; setWorkItemTemplateId: React.Dispatch>; isApplyingTemplate: boolean; diff --git a/web/core/components/issues/issue-modal/modal.tsx b/web/core/components/issues/issue-modal/modal.tsx index 0ba526e1d..b87573808 100644 --- a/web/core/components/issues/issue-modal/modal.tsx +++ b/web/core/components/issues/issue-modal/modal.tsx @@ -29,6 +29,7 @@ export interface IssuesModalProps { }; isProjectSelectionDisabled?: boolean; templateId?: string; + allowedProjectIds?: string[]; } export const CreateUpdateIssueModal: React.FC = observer((props) => { @@ -43,7 +44,11 @@ export const CreateUpdateIssueModal: React.FC = observer((prop if (!props.isOpen) return null; return ( - + ); diff --git a/web/core/components/modules/form.tsx b/web/core/components/modules/form.tsx index 4123c0bb1..675850e7e 100644 --- a/web/core/components/modules/form.tsx +++ b/web/core/components/modules/form.tsx @@ -13,9 +13,9 @@ import { DateRangeDropdown, ProjectDropdown, MemberDropdown } from "@/components import { ModuleStatusSelect } from "@/components/modules"; // helpers import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper"; -import { shouldRenderProject } from "@/helpers/project.helper"; import { getTabIndex } from "@/helpers/tab-indices.helper"; -// types +// hooks +import { useUser } from "@/hooks/store/user/user-user"; type Props = { handleFormSubmit: (values: Partial, dirtyFields: any) => Promise; @@ -37,6 +37,8 @@ const defaultValues: Partial = { export const ModuleForm: React.FC = (props) => { const { handleFormSubmit, handleClose, status, projectId, setActiveProject, data, isMobile = false } = props; + // store hooks + const { projectsWithCreatePermissions } = useUser(); // form info const { formState: { errors, isSubmitting, dirtyFields }, @@ -93,7 +95,7 @@ export const ModuleForm: React.FC = (props) => { }} multiple={false} buttonVariant="border-with-text" - renderCondition={(project) => shouldRenderProject(project)} + renderCondition={(project) => !!projectsWithCreatePermissions?.[project.id]} tabIndex={getIndex("cover_image")} />
diff --git a/web/core/store/base-command-palette.store.ts b/web/core/store/base-command-palette.store.ts index 7024daf4d..9c7273a5f 100644 --- a/web/core/store/base-command-palette.store.ts +++ b/web/core/store/base-command-palette.store.ts @@ -26,6 +26,7 @@ export interface IBaseCommandPaletteStore { isDeleteIssueModalOpen: boolean; isBulkDeleteIssueModalOpen: boolean; createIssueStoreType: TCreateModalStoreTypes; + createWorkItemAllowedProjectIds: string[] | undefined; allStickiesModal: boolean; projectListOpenMap: Record; getIsProjectListOpen: (projectId: string) => boolean; @@ -36,7 +37,7 @@ export interface IBaseCommandPaletteStore { toggleCreateCycleModal: (value?: boolean) => void; toggleCreateViewModal: (value?: boolean) => void; toggleCreatePageModal: (value?: TCreatePageModal) => void; - toggleCreateIssueModal: (value?: boolean, storeType?: TCreateModalStoreTypes) => void; + toggleCreateIssueModal: (value?: boolean, storeType?: TCreateModalStoreTypes, allowedProjectIds?: string[]) => void; toggleCreateModuleModal: (value?: boolean) => void; toggleDeleteIssueModal: (value?: boolean) => void; toggleBulkDeleteIssueModal: (value?: boolean) => void; @@ -57,6 +58,7 @@ export abstract class BaseCommandPaletteStore implements IBaseCommandPaletteStor isBulkDeleteIssueModalOpen: boolean = false; createPageModal: TCreatePageModal = DEFAULT_CREATE_PAGE_MODAL_DATA; createIssueStoreType: TCreateModalStoreTypes = EIssuesStoreType.PROJECT; + createWorkItemAllowedProjectIds: IBaseCommandPaletteStore["createWorkItemAllowedProjectIds"] = undefined; allStickiesModal: boolean = false; projectListOpenMap: Record = {}; @@ -74,6 +76,7 @@ export abstract class BaseCommandPaletteStore implements IBaseCommandPaletteStor isBulkDeleteIssueModalOpen: observable.ref, createPageModal: observable, createIssueStoreType: observable, + createWorkItemAllowedProjectIds: observable, allStickiesModal: observable, projectListOpenMap: observable, // projectPages: computed, @@ -214,13 +217,15 @@ export abstract class BaseCommandPaletteStore implements IBaseCommandPaletteStor * @param storeType * @returns */ - toggleCreateIssueModal = (value?: boolean, storeType?: TCreateModalStoreTypes) => { + toggleCreateIssueModal = (value?: boolean, storeType?: TCreateModalStoreTypes, allowedProjectIds?: string[]) => { if (value !== undefined) { this.isCreateIssueModalOpen = value; this.createIssueStoreType = storeType || EIssuesStoreType.PROJECT; + this.createWorkItemAllowedProjectIds = allowedProjectIds ?? undefined; } else { this.isCreateIssueModalOpen = !this.isCreateIssueModalOpen; this.createIssueStoreType = EIssuesStoreType.PROJECT; + this.createWorkItemAllowedProjectIds = undefined; } }; diff --git a/web/helpers/project.helper.ts b/web/helpers/project.helper.ts index e8bdb5514..b62054284 100644 --- a/web/helpers/project.helper.ts +++ b/web/helpers/project.helper.ts @@ -1,12 +1,10 @@ import sortBy from "lodash/sortBy"; // types -import { EUserPermissions } from "@plane/constants"; import { TProjectDisplayFilters, TProjectFilters, TProjectOrderByOptions } from "@plane/types"; // helpers import { getDate } from "@/helpers/date-time.helper"; import { satisfiesDateFilter } from "@/helpers/filter.helper"; -// plane web constants -// types +// plane web imports import { TProject } from "@/plane-web/types"; /** @@ -49,14 +47,6 @@ export const orderJoinedProjects = ( export const projectIdentifierSanitizer = (identifier: string): string => identifier.replace(/[^ÇŞĞIİÖÜA-Za-z0-9]/g, ""); -/** - * @description Checks if the project should be rendered or not based on the user role - * @param {TProject} project - * @returns {boolean} - */ -export const shouldRenderProject = (project: TProject): boolean => - !!project.member_role && project.member_role >= EUserPermissions.MEMBER; - /** * @description filters projects based on the filter * @param {TProject} project From ad11a34efc5891412863704c9889a014dd542e24 Mon Sep 17 00:00:00 2001 From: Akshita Goyal <36129505+gakshita@users.noreply.github.com> Date: Wed, 11 Jun 2025 16:11:40 +0530 Subject: [PATCH 155/201] [WEB-4236] fix: divided settings scroll for sidebar and main content (#7201) * fix: divided settings scroll for sidebar and main content * fix: handled icons * fix: mobile css --- web/app/(all)/[workspaceSlug]/(settings)/layout.tsx | 6 +++--- .../(settings)/settings/(workspace)/layout.tsx | 4 ++-- .../[workspaceSlug]/(settings)/settings/account/layout.tsx | 4 +++- .../[workspaceSlug]/(settings)/settings/projects/layout.tsx | 2 +- web/app/(all)/profile/sidebar.tsx | 6 ++++-- web/core/components/settings/content-wrapper.tsx | 2 +- web/core/components/settings/sidebar/root.tsx | 6 +++--- 7 files changed, 17 insertions(+), 13 deletions(-) diff --git a/web/app/(all)/[workspaceSlug]/(settings)/layout.tsx b/web/app/(all)/[workspaceSlug]/(settings)/layout.tsx index c081681b6..4bd4a5340 100644 --- a/web/app/(all)/[workspaceSlug]/(settings)/layout.tsx +++ b/web/app/(all)/[workspaceSlug]/(settings)/layout.tsx @@ -2,7 +2,7 @@ import { CommandPalette } from "@/components/command-palette"; import { ContentWrapper } from "@/components/core"; -import { SettingsContentLayout, SettingsHeader } from "@/components/settings"; +import { SettingsHeader } from "@/components/settings"; import { AuthenticationWrapper } from "@/lib/wrappers"; import { WorkspaceAuthWrapper } from "@/plane-web/layouts/workspace-wrapper"; @@ -15,8 +15,8 @@ export default function SettingsLayout({ children }: { children: React.ReactNode {/* Header */} {/* Content */} - - {children} + +
{children}
diff --git a/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/layout.tsx b/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/layout.tsx index c694165a6..bc3e69a7a 100644 --- a/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/layout.tsx +++ b/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/layout.tsx @@ -39,13 +39,13 @@ const WorkspaceSettingLayout: FC = observer((props) => hamburgerContent={WorkspaceSettingsSidebar} activePath={getWorkspaceActivePath(pathname) || ""} /> -
+
{workspaceUserInfo && !isAuthorized ? ( ) : (
{}
- {children} +
{children}
)}
diff --git a/web/app/(all)/[workspaceSlug]/(settings)/settings/account/layout.tsx b/web/app/(all)/[workspaceSlug]/(settings)/settings/account/layout.tsx index 9dcffd57c..43ff52032 100644 --- a/web/app/(all)/[workspaceSlug]/(settings)/settings/account/layout.tsx +++ b/web/app/(all)/[workspaceSlug]/(settings)/settings/account/layout.tsx @@ -24,7 +24,9 @@ const ProfileSettingsLayout = observer((props: Props) => {
- {children} +
+ {children} +
); diff --git a/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/layout.tsx b/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/layout.tsx index 4701775b4..011b240b9 100644 --- a/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/layout.tsx +++ b/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/layout.tsx @@ -36,7 +36,7 @@ const ProjectSettingsLayout = observer((props: Props) => {
{projectId && }
- {children} +
{children}
diff --git a/web/app/(all)/profile/sidebar.tsx b/web/app/(all)/profile/sidebar.tsx index 59e3daa48..0d8539c50 100644 --- a/web/app/(all)/profile/sidebar.tsx +++ b/web/app/(all)/profile/sidebar.tsx @@ -47,17 +47,19 @@ const WORKSPACE_ACTION_LINKS = [ }, ]; -export const ProjectActionIcons = ({ type, size, className }: { type: string; size?: number; className?: string }) => { +const ProjectActionIcons = ({ type, size, className = "" }: { type: string; size?: number; className?: string }) => { const icons = { profile: CircleUser, security: KeyRound, activity: Activity, - appearance: Settings2, + preferences: Settings2, notifications: Bell, + "api-tokens": KeyRound, }; if (type === undefined) return null; const Icon = icons[type as keyof typeof icons]; + if (!Icon) return null; return ; }; export const ProfileLayoutSidebar = observer(() => { diff --git a/web/core/components/settings/content-wrapper.tsx b/web/core/components/settings/content-wrapper.tsx index c65418d28..dce3d197c 100644 --- a/web/core/components/settings/content-wrapper.tsx +++ b/web/core/components/settings/content-wrapper.tsx @@ -12,7 +12,7 @@ export const SettingsContentWrapper = observer((props: TProps) => { return (
diff --git a/web/core/components/settings/sidebar/root.tsx b/web/core/components/settings/sidebar/root.tsx index 832561cfb..b29425f9e 100644 --- a/web/core/components/settings/sidebar/root.tsx +++ b/web/core/components/settings/sidebar/root.tsx @@ -37,7 +37,7 @@ export const SettingsSidebar = observer((props: SettingsSidebarProps) => { return (
{ {/* Header */} {/* Navigation */} -
+
{categories.map((category) => ( -
+
{t(category)} {groupedSettings[category].length > 0 && (
From c1a078ef3f86f1f5717896f2ebfd71bbfc30b8ca Mon Sep 17 00:00:00 2001 From: JayashTripathy <76092296+JayashTripathy@users.noreply.github.com> Date: Thu, 12 Jun 2025 21:15:09 +0530 Subject: [PATCH 156/201] [WEB-4246] Analytics minor improvements (#7194) * chore: updated label for epics * chore: improved export logic * refactor: move csvConfig to export.ts and clean up export logic * refactor: remove unused CSV export logic from WorkItemsInsightTable component * refactor: streamline data handling in InsightTable component for improved rendering * feat: add translation for "No. of {entity}" and update priority chart y-axis label to use new translation * refactor: cleaned up some component and added utilitites * feat: add "at_risk" translation to multiple languages in translations.json files * refactor: update TrendPiece component to use new status variants for analytics * fix: adjust TrendPiece component logic for on-track and off-track status * refactor: use nullish coalescing operator for yAxis.dx in line and scatter charts * feat: add "at_risk" translation to various languages in translations.json files * feat: add "no_of" translation to various languages in translations.json files * feat: update "at_risk" translation in Ukrainian, Vietnamese, and Chinese locales in translations.json files --- packages/constants/src/module.ts | 29 ++- .../i18n/src/locales/cs/translations.json | 6 +- .../i18n/src/locales/de/translations.json | 4 +- .../i18n/src/locales/en/translations.json | 6 +- .../i18n/src/locales/es/translations.json | 6 +- .../i18n/src/locales/fr/translations.json | 6 +- .../i18n/src/locales/id/translations.json | 9 +- .../i18n/src/locales/it/translations.json | 6 +- .../i18n/src/locales/ja/translations.json | 6 +- .../i18n/src/locales/ko/translations.json | 6 +- .../i18n/src/locales/pl/translations.json | 6 +- .../i18n/src/locales/pt-BR/translations.json | 6 +- .../i18n/src/locales/ro/translations.json | 6 +- .../i18n/src/locales/ru/translations.json | 6 +- .../i18n/src/locales/sk/translations.json | 6 +- .../i18n/src/locales/tr-TR/translations.json | 6 +- .../i18n/src/locales/ua/translations.json | 6 +- .../i18n/src/locales/vi-VN/translations.json | 6 +- .../i18n/src/locales/zh-CN/translations.json | 6 +- .../i18n/src/locales/zh-TW/translations.json | 7 +- .../propel/src/charts/line-chart/root.tsx | 2 +- .../propel/src/charts/scatter-chart/root.tsx | 32 +-- packages/types/src/analytics.d.ts | 15 +- packages/types/src/charts/index.d.ts | 29 ++- packages/ui/src/icons/cycle/helper.tsx | 9 + packages/ui/src/icons/cycle/index.ts | 1 + web/ce/components/analytics/tabs.ts | 1 + web/core/components/analytics/config.ts | 9 - web/core/components/analytics/empty-state.tsx | 2 +- web/core/components/analytics/export.ts | 26 +++ .../analytics/insight-table/data-table.tsx | 2 +- .../analytics/insight-table/root.tsx | 32 ++- .../analytics/overview/project-insights.tsx | 2 +- web/core/components/analytics/trend-piece.tsx | 41 +++- .../work-items/created-vs-resolved.tsx | 2 +- .../analytics/work-items/priority-chart.tsx | 69 +++--- .../work-items/workitems-insight-table.tsx | 216 ++++++++++-------- 37 files changed, 383 insertions(+), 252 deletions(-) delete mode 100644 web/core/components/analytics/config.ts create mode 100644 web/core/components/analytics/export.ts diff --git a/packages/constants/src/module.ts b/packages/constants/src/module.ts index 6ce30f0dc..16a332303 100644 --- a/packages/constants/src/module.ts +++ b/packages/constants/src/module.ts @@ -1,9 +1,16 @@ // types -import { - TModuleLayoutOptions, - TModuleOrderByOptions, - TModuleStatus, -} from "@plane/types"; +import { TModuleLayoutOptions, TModuleOrderByOptions, TModuleStatus } from "@plane/types"; + +export const MODULE_STATUS_COLORS: { + [key in TModuleStatus]: string; +} = { + backlog: "#a3a3a2", + planned: "#3f76ff", + paused: "#525252", + completed: "#16a34a", + cancelled: "#ef4444", + "in-progress": "#f39e1f", +}; export const MODULE_STATUS: { i18n_label: string; @@ -15,42 +22,42 @@ export const MODULE_STATUS: { { i18n_label: "project_modules.status.backlog", value: "backlog", - color: "#a3a3a2", + color: MODULE_STATUS_COLORS.backlog, textColor: "text-custom-text-400", bgColor: "bg-custom-background-80", }, { i18n_label: "project_modules.status.planned", value: "planned", - color: "#3f76ff", + color: MODULE_STATUS_COLORS.planned, textColor: "text-blue-500", bgColor: "bg-indigo-50", }, { i18n_label: "project_modules.status.in_progress", value: "in-progress", - color: "#f39e1f", + color: MODULE_STATUS_COLORS["in-progress"], textColor: "text-amber-500", bgColor: "bg-amber-50", }, { i18n_label: "project_modules.status.paused", value: "paused", - color: "#525252", + color: MODULE_STATUS_COLORS.paused, textColor: "text-custom-text-300", bgColor: "bg-custom-background-90", }, { i18n_label: "project_modules.status.completed", value: "completed", - color: "#16a34a", + color: MODULE_STATUS_COLORS.completed, textColor: "text-green-600", bgColor: "bg-green-100", }, { i18n_label: "project_modules.status.cancelled", value: "cancelled", - color: "#ef4444", + color: MODULE_STATUS_COLORS.cancelled, textColor: "text-red-500", bgColor: "bg-red-50", }, diff --git a/packages/i18n/src/locales/cs/translations.json b/packages/i18n/src/locales/cs/translations.json index b2ac82a65..599765916 100644 --- a/packages/i18n/src/locales/cs/translations.json +++ b/packages/i18n/src/locales/cs/translations.json @@ -872,13 +872,15 @@ "guests": "Hosté", "on_track": "Na správné cestě", "off_track": "Mimo plán", + "at_risk": "V ohrožení", "timeline": "Časová osa", "completion": "Dokončení", "upcoming": "Nadcházející", "completed": "Dokončeno", "in_progress": "Probíhá", "planned": "Plánováno", - "paused": "Pozastaveno" + "paused": "Pozastaveno", + "no_of": "Počet {entity}" }, "chart": { "x_axis": "Osa X", @@ -2467,4 +2469,4 @@ "plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Plane se nespustil. To může být způsobeno tím, že se jeden nebo více služeb Plane nepodařilo spustit.", "choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "Vyberte View Logs z setup.sh a Docker logů, abyste si byli jisti." } -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/de/translations.json b/packages/i18n/src/locales/de/translations.json index b895435ae..1cac1d99f 100644 --- a/packages/i18n/src/locales/de/translations.json +++ b/packages/i18n/src/locales/de/translations.json @@ -872,13 +872,15 @@ "guests": "Gäste", "on_track": "Im Plan", "off_track": "Außer Plan", + "at_risk": "Gefährdet", "timeline": "Zeitleiste", "completion": "Fertigstellung", "upcoming": "Bevorstehend", "completed": "Abgeschlossen", "in_progress": "In Bearbeitung", "planned": "Geplant", - "paused": "Pausiert" + "paused": "Pausiert", + "no_of": "Anzahl {entity}" }, "chart": { "x_axis": "X-Achse", diff --git a/packages/i18n/src/locales/en/translations.json b/packages/i18n/src/locales/en/translations.json index aaf83df2f..654ce3cc6 100644 --- a/packages/i18n/src/locales/en/translations.json +++ b/packages/i18n/src/locales/en/translations.json @@ -617,6 +617,7 @@ "click_to_add_description": "Click to add description", "on_track": "On-Track", "off_track": "Off-Track", + "at_risk": "At risk", "timeline": "Timeline", "completion": "Completion", "upcoming": "Upcoming", @@ -721,7 +722,8 @@ "deactivated_user": "Deactivated user", "apply": "Apply", "applying": "Applying", - "overview": "Overview" + "overview": "Overview", + "no_of": "No. of {entity}" }, "chart": { "x_axis": "X-axis", @@ -2343,4 +2345,4 @@ "plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Plane didn't start up. This could be because one or more Plane services failed to start.", "choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "Choose View Logs from setup.sh and Docker logs to be sure." } -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/es/translations.json b/packages/i18n/src/locales/es/translations.json index 31c8f75e3..97f0792f2 100644 --- a/packages/i18n/src/locales/es/translations.json +++ b/packages/i18n/src/locales/es/translations.json @@ -875,13 +875,15 @@ "guests": "Invitados", "on_track": "En camino", "off_track": "Fuera de camino", + "at_risk": "En riesgo", "timeline": "Cronograma", "completion": "Finalización", "upcoming": "Próximo", "completed": "Completado", "in_progress": "En progreso", "planned": "Planificado", - "paused": "Pausado" + "paused": "Pausado", + "no_of": "N.º de {entity}" }, "chart": { "x_axis": "Eje X", @@ -2469,4 +2471,4 @@ "plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Plane no se inició. Esto podría deberse a que uno o más servicios de Plane fallaron al iniciar.", "choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "Selecciona View Logs desde setup.sh y los logs de Docker para estar seguro." } -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/fr/translations.json b/packages/i18n/src/locales/fr/translations.json index afa6f1844..9fce5a002 100644 --- a/packages/i18n/src/locales/fr/translations.json +++ b/packages/i18n/src/locales/fr/translations.json @@ -873,13 +873,15 @@ "guests": "Invités", "on_track": "Sur la bonne voie", "off_track": "Hors de la bonne voie", + "at_risk": "À risque", "timeline": "Chronologie", "completion": "Achèvement", "upcoming": "À venir", "completed": "Terminé", "in_progress": "En cours", "planned": "Planifié", - "paused": "En pause" + "paused": "En pause", + "no_of": "Nº de {entity}" }, "chart": { "x_axis": "Axe X", @@ -2467,4 +2469,4 @@ "plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Plane n'a pas démarré. Cela pourrait être dû au fait qu'un ou plusieurs services Plane ont échoué à démarrer.", "choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "Choisissez View Logs depuis setup.sh et les logs Docker pour en être sûr." } -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/id/translations.json b/packages/i18n/src/locales/id/translations.json index dc59bdaf1..87c4a952f 100644 --- a/packages/i18n/src/locales/id/translations.json +++ b/packages/i18n/src/locales/id/translations.json @@ -872,13 +872,15 @@ "guests": "Tamu", "on_track": "Sesuai Jalur", "off_track": "Menyimpang", + "at_risk": "Dalam risiko", "timeline": "Linimasa", "completion": "Penyelesaian", "upcoming": "Mendatang", "completed": "Selesai", "in_progress": "Sedang berlangsung", "planned": "Direncanakan", - "paused": "Dijedaikan" + "paused": "Dijedaikan", + "no_of": "Jumlah {entity}" }, "chart": { "x_axis": "Sumbu-X", @@ -2460,5 +2462,6 @@ "self_hosted_maintenance_message": { "plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Plane tidak berhasil dimulai. Ini bisa karena satu atau lebih layanan Plane gagal untuk dimulai.", "choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "Pilih View Logs dari setup.sh dan log Docker untuk memastikan." - } -} + }, + "no_of": "Jumlah {entity}" +} \ No newline at end of file diff --git a/packages/i18n/src/locales/it/translations.json b/packages/i18n/src/locales/it/translations.json index 03a1160bf..75df8e9b5 100644 --- a/packages/i18n/src/locales/it/translations.json +++ b/packages/i18n/src/locales/it/translations.json @@ -871,13 +871,15 @@ "guests": "Ospiti", "on_track": "In linea", "off_track": "Fuori rotta", + "at_risk": "A rischio", "timeline": "Cronologia", "completion": "Completamento", "upcoming": "In arrivo", "completed": "Completato", "in_progress": "In corso", "planned": "Pianificato", - "paused": "In pausa" + "paused": "In pausa", + "no_of": "N. di {entity}" }, "chart": { "x_axis": "Asse X", @@ -2466,4 +2468,4 @@ "plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Plane non si è avviato. Questo potrebbe essere dovuto al fatto che uno o più servizi Plane non sono riusciti ad avviarsi.", "choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "Scegli View Logs da setup.sh e dai log Docker per essere sicuro." } -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/ja/translations.json b/packages/i18n/src/locales/ja/translations.json index 0c081974e..e7610eac3 100644 --- a/packages/i18n/src/locales/ja/translations.json +++ b/packages/i18n/src/locales/ja/translations.json @@ -873,13 +873,15 @@ "guests": "ゲスト", "on_track": "順調", "off_track": "遅れ", + "at_risk": "リスクあり", "timeline": "タイムライン", "completion": "完了", "upcoming": "今後の予定", "completed": "完了", "in_progress": "進行中", "planned": "計画済み", - "paused": "一時停止" + "paused": "一時停止", + "no_of": "{entity} の数" }, "chart": { "x_axis": "エックス アクシス", @@ -2467,4 +2469,4 @@ "plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Planeが起動しませんでした。これは1つまたは複数のPlaneサービスの起動に失敗したことが原因である可能性があります。", "choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "setup.shとDockerログからView Logsを選択して確認してください。" } -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/ko/translations.json b/packages/i18n/src/locales/ko/translations.json index 66be56308..6100f3fd6 100644 --- a/packages/i18n/src/locales/ko/translations.json +++ b/packages/i18n/src/locales/ko/translations.json @@ -874,13 +874,15 @@ "guests": "게스트", "on_track": "계획대로 진행 중", "off_track": "계획 이탈", + "at_risk": "위험", "timeline": "타임라인", "completion": "완료", "upcoming": "예정된", "completed": "완료됨", "in_progress": "진행 중", "planned": "계획된", - "paused": "일시 중지됨" + "paused": "일시 중지됨", + "no_of": "{entity} 수" }, "chart": { "x_axis": "X축", @@ -2469,4 +2471,4 @@ "plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Plane이 시작되지 않았습니다. 이는 하나 이상의 Plane 서비스가 시작에 실패했기 때문일 수 있습니다.", "choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "확실히 하려면 setup.sh와 Docker 로그에서 View Logs를 선택하세요." } -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/pl/translations.json b/packages/i18n/src/locales/pl/translations.json index fe74d5c4f..7cd8ba385 100644 --- a/packages/i18n/src/locales/pl/translations.json +++ b/packages/i18n/src/locales/pl/translations.json @@ -874,13 +874,15 @@ "guests": "Goście", "on_track": "Na dobrej drodze", "off_track": "Poza planem", + "at_risk": "W zagrożeniu", "timeline": "Oś czasu", "completion": "Zakończenie", "upcoming": "Nadchodzące", "completed": "Zakończone", "in_progress": "W trakcie", "planned": "Zaplanowane", - "paused": "Wstrzymane" + "paused": "Wstrzymane", + "no_of": "Liczba {entity}" }, "chart": { "x_axis": "Oś X", @@ -2468,4 +2470,4 @@ "plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Plane nie uruchomił się. Może to być spowodowane tym, że jedna lub więcej usług Plane nie mogła się uruchomić.", "choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "Wybierz View Logs z setup.sh i logów Docker, aby mieć pewność." } -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/pt-BR/translations.json b/packages/i18n/src/locales/pt-BR/translations.json index bb8d1da67..f640a9f01 100644 --- a/packages/i18n/src/locales/pt-BR/translations.json +++ b/packages/i18n/src/locales/pt-BR/translations.json @@ -874,13 +874,15 @@ "guests": "Convidados", "on_track": "No caminho certo", "off_track": "Fora do caminho", + "at_risk": "Em risco", "timeline": "Linha do tempo", "completion": "Conclusão", "upcoming": "Próximo", "completed": "Concluído", "in_progress": "Em andamento", "planned": "Planejado", - "paused": "Pausado" + "paused": "Pausado", + "no_of": "Nº de {entity}" }, "chart": { "x_axis": "Eixo X", @@ -2463,4 +2465,4 @@ "plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "O Plane não inicializou. Isso pode ser porque um ou mais serviços do Plane falharam ao iniciar.", "choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "Escolha View Logs do setup.sh e logs do Docker para ter certeza." } -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/ro/translations.json b/packages/i18n/src/locales/ro/translations.json index 3c8ab4502..fd59eb3e8 100644 --- a/packages/i18n/src/locales/ro/translations.json +++ b/packages/i18n/src/locales/ro/translations.json @@ -872,13 +872,15 @@ "guests": "Invitați", "on_track": "Pe drumul cel bun", "off_track": "În afara traiectoriei", + "at_risk": "În pericol", "timeline": "Cronologie", "completion": "Finalizare", "upcoming": "Viitor", "completed": "Finalizat", "in_progress": "În desfășurare", "planned": "Planificat", - "paused": "Pauzat" + "paused": "Pauzat", + "no_of": "Nr. de {entity}" }, "chart": { "x_axis": "axa-X", @@ -2461,4 +2463,4 @@ "plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Plane nu a pornit. Aceasta ar putea fi din cauza că unul sau mai multe servicii Plane au eșuat să pornească.", "choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "Alegeți View Logs din setup.sh și logurile Docker pentru a fi siguri." } -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/ru/translations.json b/packages/i18n/src/locales/ru/translations.json index 624179ef6..d4067bd72 100644 --- a/packages/i18n/src/locales/ru/translations.json +++ b/packages/i18n/src/locales/ru/translations.json @@ -874,6 +874,7 @@ "guests": "Гости", "on_track": "По плану", "off_track": "Отклонение от плана", + "at_risk": "Под угрозой", "timeline": "Хронология", "completion": "Завершение", "upcoming": "Предстоящие", @@ -2468,5 +2469,6 @@ "self_hosted_maintenance_message": { "plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Plane не запустился. Это может быть из-за того, что один или несколько сервисов Plane не смогли запуститься.", "choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "Выберите View Logs из setup.sh и логов Docker, чтобы убедиться." - } -} + }, + "no_of": "Количество {entity}" +} \ No newline at end of file diff --git a/packages/i18n/src/locales/sk/translations.json b/packages/i18n/src/locales/sk/translations.json index d87be6ce5..e3c3e864e 100644 --- a/packages/i18n/src/locales/sk/translations.json +++ b/packages/i18n/src/locales/sk/translations.json @@ -874,13 +874,15 @@ "guests": "Hostia", "on_track": "Na správnej ceste", "off_track": "Mimo plán", + "at_risk": "V ohrození", "timeline": "Časová os", "completion": "Dokončenie", "upcoming": "Nadchádzajúce", "completed": "Dokončené", "in_progress": "Prebieha", "planned": "Plánované", - "paused": "Pozastavené" + "paused": "Pozastavené", + "no_of": "Počet {entity}" }, "chart": { "x_axis": "Os X", @@ -2468,4 +2470,4 @@ "plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Plane sa nespustil. Toto môže byť spôsobené tým, že sa jedna alebo viac služieb Plane nepodarilo spustiť.", "choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "Vyberte View Logs z setup.sh a Docker logov, aby ste si boli istí." } -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/tr-TR/translations.json b/packages/i18n/src/locales/tr-TR/translations.json index 7c9219b85..4d1c4ab12 100644 --- a/packages/i18n/src/locales/tr-TR/translations.json +++ b/packages/i18n/src/locales/tr-TR/translations.json @@ -875,13 +875,15 @@ "guests": "Misafirler", "on_track": "Yolunda", "off_track": "Yolunda değil", + "at_risk": "Risk altında", "timeline": "Zaman çizelgesi", "completion": "Tamamlama", "upcoming": "Yaklaşan", "completed": "Tamamlandı", "in_progress": "Devam ediyor", "planned": "Planlandı", - "paused": "Durduruldu" + "paused": "Durduruldu", + "no_of": "{entity} sayısı" }, "chart": { "x_axis": "X ekseni", @@ -2447,4 +2449,4 @@ "plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Plane başlatılamadı. Bu, bir veya daha fazla Plane servisinin başlatılamaması nedeniyle olabilir.", "choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "Emin olmak için setup.sh ve Docker loglarından View Logs'u seçin." } -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/ua/translations.json b/packages/i18n/src/locales/ua/translations.json index 603f6c0c5..252a858d5 100644 --- a/packages/i18n/src/locales/ua/translations.json +++ b/packages/i18n/src/locales/ua/translations.json @@ -874,13 +874,15 @@ "guests": "Гості", "on_track": "У межах графіку", "off_track": "Поза графіком", + "at_risk": "Під загрозою", "timeline": "Хронологія", "completion": "Завершення", "upcoming": "Майбутнє", "completed": "Завершено", "in_progress": "В процесі", "planned": "Заплановано", - "paused": "Призупинено" + "paused": "Призупинено", + "no_of": "Кількість {entity}" }, "chart": { "x_axis": "Вісь X", @@ -2468,4 +2470,4 @@ "plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Plane не запустився. Це може бути через те, що один або декілька сервісів Plane не змогли запуститися.", "choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "Виберіть View Logs з setup.sh та логів Docker, щоб переконатися." } -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/vi-VN/translations.json b/packages/i18n/src/locales/vi-VN/translations.json index f78aea31a..d6e6d7999 100644 --- a/packages/i18n/src/locales/vi-VN/translations.json +++ b/packages/i18n/src/locales/vi-VN/translations.json @@ -873,13 +873,15 @@ "guests": "Khách", "on_track": "Đúng tiến độ", "off_track": "Chệch hướng", + "at_risk": "Có nguy cơ", "timeline": "Dòng thời gian", "completion": "Hoàn thành", "upcoming": "Sắp tới", "completed": "Đã hoàn thành", "in_progress": "Đang tiến hành", "planned": "Đã lên kế hoạch", - "paused": "Tạm dừng" + "paused": "Tạm dừng", + "no_of": "Số lượng {entity}" }, "chart": { "x_axis": "Trục X", @@ -2466,4 +2468,4 @@ "plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Plane không khởi động được. Điều này có thể do một hoặc nhiều dịch vụ Plane không khởi động được.", "choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "Chọn View Logs từ setup.sh và log Docker để chắc chắn." } -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/zh-CN/translations.json b/packages/i18n/src/locales/zh-CN/translations.json index b785d8649..d815a5ad7 100644 --- a/packages/i18n/src/locales/zh-CN/translations.json +++ b/packages/i18n/src/locales/zh-CN/translations.json @@ -873,13 +873,15 @@ "guests": "访客", "on_track": "进展顺利", "off_track": "偏离轨道", + "at_risk": "有风险", "timeline": "时间轴", "completion": "完成", "upcoming": "即将发生", "completed": "已完成", "in_progress": "进行中", "planned": "已计划", - "paused": "暂停" + "paused": "暂停", + "no_of": "{entity} 的数量" }, "chart": { "x_axis": "X轴", @@ -2448,4 +2450,4 @@ "plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Plane 未能启动。这可能是因为一个或多个 Plane 服务启动失败。", "choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "请选择“查看日志”来查看 setup.sh 和 Docker 日志,以确认问题。" } -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/zh-TW/translations.json b/packages/i18n/src/locales/zh-TW/translations.json index b75d720f5..bec40f2ea 100644 --- a/packages/i18n/src/locales/zh-TW/translations.json +++ b/packages/i18n/src/locales/zh-TW/translations.json @@ -880,7 +880,9 @@ "completed": "已完成", "in_progress": "進行中", "planned": "已計劃", - "paused": "暫停" + "paused": "暫停", + "at_risk": "有風險", + "no_of": "{entity} 的數量" }, "chart": { "x_axis": "X 軸", @@ -2465,9 +2467,8 @@ "previously_edited_by": "先前編輯者", "edited_by": "編輯者" }, - "self_hosted_maintenance_message": { "plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Plane 未能啟動。這可能是因為一個或多個 Plane 服務啟動失敗。", "choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "從 setup.sh 和 Docker 日誌中選擇 View Logs 來確認。" } -} +} \ No newline at end of file diff --git a/packages/propel/src/charts/line-chart/root.tsx b/packages/propel/src/charts/line-chart/root.tsx index f3b2ef72c..28a02fc30 100644 --- a/packages/propel/src/charts/line-chart/root.tsx +++ b/packages/propel/src/charts/line-chart/root.tsx @@ -122,7 +122,7 @@ export const LineChart = React.memo((props: angle: -90, position: "bottom", offset: -24, - dx: -16, + dx: yAxis.dx ?? -16, className: AXIS_LABEL_CLASSNAME, } } diff --git a/packages/propel/src/charts/scatter-chart/root.tsx b/packages/propel/src/charts/scatter-chart/root.tsx index d7996c990..5187d131b 100644 --- a/packages/propel/src/charts/scatter-chart/root.tsx +++ b/packages/propel/src/charts/scatter-chart/root.tsx @@ -27,7 +27,6 @@ export const ScatterChart = React.memo((prop margin, xAxis, yAxis, - className, tickCount = { x: undefined, @@ -35,6 +34,7 @@ export const ScatterChart = React.memo((prop }, legend, showTooltip = true, + customTooltipContent, } = props; // states const [activePoint, setActivePoint] = useState(null); @@ -107,7 +107,7 @@ export const ScatterChart = React.memo((prop angle: -90, position: "bottom", offset: -24, - dx: -16, + dx: yAxis.dx ?? -16, className: AXIS_LABEL_CLASSNAME, } } @@ -133,17 +133,21 @@ export const ScatterChart = React.memo((prop wrapperStyle={{ pointerEvents: "auto", }} - content={({ active, label, payload }) => ( - - )} + content={({ active, label, payload }) => + customTooltipContent ? ( + customTooltipContent({ active, label, payload }) + ) : ( + + ) + } /> )} {renderPoints} @@ -152,4 +156,4 @@ export const ScatterChart = React.memo((prop
); }); -ScatterChart.displayName = "ScatterChart"; +ScatterChart.displayName = "ScatterChart"; \ No newline at end of file diff --git a/packages/types/src/analytics.d.ts b/packages/types/src/analytics.d.ts index d55d74f4f..dbcf52f36 100644 --- a/packages/types/src/analytics.d.ts +++ b/packages/types/src/analytics.d.ts @@ -1,5 +1,6 @@ import { ChartXAxisProperty, ChartYAxisMetric } from "@plane/constants"; import { TChartData } from "./charts"; +import { Row } from "@tanstack/react-table"; export type TAnalyticsTabsBase = "overview" | "work-items"; export type TAnalyticsGraphsBase = "projects" | "work-items" | "custom-work-items"; @@ -20,12 +21,6 @@ export interface IAnalyticsResponseFields { filter_count: number; } -export interface IAnalyticsRadarEntity { - key: string; - name: string; - count: number; -} - // chart types export interface IChartResponse { @@ -43,7 +38,7 @@ export interface WorkItemInsightColumns { backlog_work_items: number; un_started_work_items: number; started_work_items: number; - // because of the peek view, we will display the name of the project instead of project__name + // incase of peek view, we will display the display_name instead of project__name display_name?: string; avatar_url?: string; assignee_id?: string; @@ -58,3 +53,9 @@ export interface IAnalyticsParams { y_axis: ChartYAxisMetric; group_by?: ChartXAxisProperty; } + +export type ExportConfig = { + key: string; + value: (row: Row) => string | number; + label?: string; +}; diff --git a/packages/types/src/charts/index.d.ts b/packages/types/src/charts/index.d.ts index 2747973aa..316cfd6b8 100644 --- a/packages/types/src/charts/index.d.ts +++ b/packages/types/src/charts/index.d.ts @@ -1,7 +1,5 @@ - - // ============================================================ -// Chart Base +// Chart Base // ============================================================ export * from "./common"; export type TChartLegend = { @@ -48,10 +46,11 @@ type TChartProps = { y?: number; }; showTooltip?: boolean; + customTooltipContent?: (props: { active?: boolean; label: string; payload: any }) => React.ReactNode; }; // ============================================================ -// Bar Chart +// Bar Chart // ============================================================ export type TBarItem = { @@ -71,7 +70,7 @@ export type TBarChartProps = TChartProps = { @@ -90,7 +89,7 @@ export type TLineChartProps = TChartProps = { @@ -105,7 +104,7 @@ export type TScatterChartProps = TChartProps }; // ============================================================ -// Area Chart +// Area Chart // ============================================================ export type TAreaItem = { @@ -130,7 +129,7 @@ export type TAreaChartProps = TChartProps = { @@ -161,7 +160,7 @@ export type TPieChartProps = Pick< }; // ============================================================ -// Tree Map +// Tree Map // ============================================================ export type TreeMapItem = { @@ -171,13 +170,13 @@ export type TreeMapItem = { textClassName?: string; icon?: React.ReactElement; } & ( - | { + | { fillColor: string; } - | { + | { fillClassName: string; } - ); +); export type TreeMapChartProps = { data: TreeMapItem[]; @@ -217,8 +216,8 @@ export type TRadarItem = { dot?: { r: number; fillOpacity: number; - } -} + }; +}; export type TRadarChartProps = Pick< TChartProps, @@ -231,4 +230,4 @@ export type TRadarChartProps = Pick< label?: string; strokeColor?: string; }; -} +}; diff --git a/packages/ui/src/icons/cycle/helper.tsx b/packages/ui/src/icons/cycle/helper.tsx index ec91cc2c2..c53069bc7 100644 --- a/packages/ui/src/icons/cycle/helper.tsx +++ b/packages/ui/src/icons/cycle/helper.tsx @@ -16,3 +16,12 @@ export const CYCLE_GROUP_COLORS: { completed: "#16A34A", draft: "#525252", }; + +export const CYCLE_GROUP_I18N_LABELS: { + [key in TCycleGroups]: string; +} = { + current: "current", + upcoming: "common.upcoming", + completed: "common.completed", + draft: "project_cycles.status.draft", +}; diff --git a/packages/ui/src/icons/cycle/index.ts b/packages/ui/src/icons/cycle/index.ts index e74c8ff8c..c3a791a2b 100644 --- a/packages/ui/src/icons/cycle/index.ts +++ b/packages/ui/src/icons/cycle/index.ts @@ -3,3 +3,4 @@ export * from "./circle-dot-full-icon"; export * from "./contrast-icon"; export * from "./circle-dot-full-icon"; export * from "./cycle-group-icon"; +export * from "./helper"; diff --git a/web/ce/components/analytics/tabs.ts b/web/ce/components/analytics/tabs.ts index 6ce6daf8e..6f978a3c6 100644 --- a/web/ce/components/analytics/tabs.ts +++ b/web/ce/components/analytics/tabs.ts @@ -5,6 +5,7 @@ export const ANALYTICS_TABS: { key: TAnalyticsTabsBase; i18nKey: string; content: React.FC; + isExtended?: boolean; }[] = [ { key: "overview", i18nKey: "common.overview", content: Overview }, { key: "work-items", i18nKey: "sidebar.work_items", content: WorkItems }, diff --git a/web/core/components/analytics/config.ts b/web/core/components/analytics/config.ts deleted file mode 100644 index 5e297e908..000000000 --- a/web/core/components/analytics/config.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { mkConfig } from "export-to-csv"; - -export const csvConfig = (workspaceSlug: string) => - mkConfig({ - fieldSeparator: ",", - filename: `${workspaceSlug}-analytics`, - decimalSeparator: ".", - useKeysAsHeaders: true, - }); diff --git a/web/core/components/analytics/empty-state.tsx b/web/core/components/analytics/empty-state.tsx index 5243e6c88..3704f3e84 100644 --- a/web/core/components/analytics/empty-state.tsx +++ b/web/core/components/analytics/empty-state.tsx @@ -39,7 +39,7 @@ const AnalyticsEmptyState = ({ title, description, assetPath, className }: Props )}

{title}

- {description &&

{description}

} + {description &&

{description}

}
diff --git a/web/core/components/analytics/export.ts b/web/core/components/analytics/export.ts new file mode 100644 index 000000000..f0a448979 --- /dev/null +++ b/web/core/components/analytics/export.ts @@ -0,0 +1,26 @@ +import { ColumnDef, Row } from "@tanstack/react-table"; +import { download, generateCsv, mkConfig } from "export-to-csv"; + +export const csvConfig = (workspaceSlug: string) => + mkConfig({ + fieldSeparator: ",", + filename: `${workspaceSlug}-analytics`, + decimalSeparator: ".", + useKeysAsHeaders: true, + }); + +export const exportCSV = (rows: Row[], columns: ColumnDef[], workspaceSlug: string) => { + const rowData = rows.map((row) => { + const exportColumns = columns.map((col) => col.meta?.export); + const cells = exportColumns.reduce((acc: Record, col) => { + if (col) { + const cell = col?.value(row) ?? "-"; + acc[col.label ?? col.key] = cell; + } + return acc; + }, {}); + return cells; + }); + const csv = generateCsv(csvConfig(workspaceSlug))(rowData); + download(csvConfig(workspaceSlug))(csv); +}; diff --git a/web/core/components/analytics/insight-table/data-table.tsx b/web/core/components/analytics/insight-table/data-table.tsx index 8a66c3caf..35dc3b365 100644 --- a/web/core/components/analytics/insight-table/data-table.tsx +++ b/web/core/components/analytics/insight-table/data-table.tsx @@ -94,7 +94,7 @@ export function DataTable({ columns, data, searchPlaceholder, act ref={inputRef} className="w-full max-w-[234px] border-none bg-transparent text-sm text-custom-text-100 placeholder:text-custom-text-400 focus:outline-none" placeholder="Search" - value={table.getColumn(table.getHeaderGroups()?.[0].headers[0].id)?.getFilterValue() as string} + value={table.getColumn(table.getHeaderGroups()?.[0]?.headers?.[0]?.id)?.getFilterValue() as string} onChange={(e) => { const columnId = table.getHeaderGroups()?.[0]?.headers?.[0]?.id; if (columnId) table.getColumn(columnId)?.setFilterValue(e.target.value); diff --git a/web/core/components/analytics/insight-table/root.tsx b/web/core/components/analytics/insight-table/root.tsx index 583db1b7a..2fd19393b 100644 --- a/web/core/components/analytics/insight-table/root.tsx +++ b/web/core/components/analytics/insight-table/root.tsx @@ -26,24 +26,20 @@ export const InsightTable = >( return (
- {data ? ( - ) => ( - - )} - /> - ) : ( -
{t("common.no_data_yet")}
- )} + ) => ( + + )} + />
); }; diff --git a/web/core/components/analytics/overview/project-insights.tsx b/web/core/components/analytics/overview/project-insights.tsx index 9844a4b4f..994e6e9b7 100644 --- a/web/core/components/analytics/overview/project-insights.tsx +++ b/web/core/components/analytics/overview/project-insights.tsx @@ -32,7 +32,7 @@ const ProjectInsights = observer(() => { const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/analytics/empty-chart-radar" }); const { data: projectInsightsData, isLoading: isLoadingProjectInsight } = useSWR( - `radar-chart-${workspaceSlug}-${selectedDuration}-${selectedProjects}-${selectedCycle}-${selectedModule}-${isPeekView}`, + `radar-chart-project-insights-${workspaceSlug}-${selectedDuration}-${selectedProjects}-${selectedCycle}-${selectedModule}-${isPeekView}`, () => analyticsService.getAdvanceAnalyticsCharts[]>( workspaceSlug, diff --git a/web/core/components/analytics/trend-piece.tsx b/web/core/components/analytics/trend-piece.tsx index 23daa89be..60062222a 100644 --- a/web/core/components/analytics/trend-piece.tsx +++ b/web/core/components/analytics/trend-piece.tsx @@ -8,6 +8,8 @@ type Props = { percentage: number; className?: string; size?: "xs" | "sm" | "md" | "lg"; + trendIconVisible?: boolean; + variant?: "simple" | "outlined" | "tinted"; }; const sizeConfig = { @@ -29,16 +31,47 @@ const sizeConfig = { }, } as const; +const variants: Record, Record<"ontrack" | "offtrack" | "atrisk", string>> = { + simple: { + ontrack: "text-green-500", + offtrack: "text-yellow-500", + atrisk: "text-red-500", + }, + outlined: { + ontrack: "text-green-500 border border-green-500", + offtrack: "text-yellow-500 border border-yellow-500", + atrisk: "text-red-500 border border-red-500", + }, + tinted: { + ontrack: "text-green-500 bg-green-500/10", + offtrack: "text-yellow-500 bg-yellow-500/10", + atrisk: "text-red-500 bg-red-500/10", + }, +} as const; + const TrendPiece = (props: Props) => { - const { percentage, className, size = "sm" } = props; - const isPositive = percentage > 0; + const { percentage, className, trendIconVisible = true, size = "sm", variant = "simple" } = props; + const isOnTrack = percentage >= 66; + const isOffTrack = percentage >= 33 && percentage < 66; const config = sizeConfig[size]; return (
- {isPositive ? : } + {trendIconVisible && + (isOnTrack ? ( + + ) : isOffTrack ? ( + + ) : ( + + ))} {Math.round(Math.abs(percentage))}%
); diff --git a/web/core/components/analytics/work-items/created-vs-resolved.tsx b/web/core/components/analytics/work-items/created-vs-resolved.tsx index 76215238d..d5deb80e7 100644 --- a/web/core/components/analytics/work-items/created-vs-resolved.tsx +++ b/web/core/components/analytics/work-items/created-vs-resolved.tsx @@ -104,7 +104,7 @@ const CreatedVsResolved = observer(() => { }} yAxis={{ key: "count", - label: t("no_of", { entity: t("work_items") }), + label: t("no_of", { entity: isEpic ? t("epics") : t("work_items") }), offset: -30, dx: -22, }} diff --git a/web/core/components/analytics/work-items/priority-chart.tsx b/web/core/components/analytics/work-items/priority-chart.tsx index 78a05c2c7..354044bda 100644 --- a/web/core/components/analytics/work-items/priority-chart.tsx +++ b/web/core/components/analytics/work-items/priority-chart.tsx @@ -1,6 +1,6 @@ import { useMemo } from "react"; -import { ColumnDef, Row, Table } from "@tanstack/react-table"; -import { mkConfig, generateCsv, download } from "export-to-csv"; +import { ColumnDef, RowData, Table } from "@tanstack/react-table"; +import { mkConfig } from "export-to-csv"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; import { useTheme } from "next-themes"; @@ -18,8 +18,8 @@ import { } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { BarChart } from "@plane/propel/charts/bar-chart"; -import { IChartResponse } from "@plane/types"; -import { TBarItem, TChart, TChartData, TChartDatum } from "@plane/types/src/charts"; +import { ExportConfig } from "@plane/types"; +import { TBarItem, TChart, TChartDatum } from "@plane/types/src/charts"; // plane web components import { Button } from "@plane/ui"; import { generateExtendedColors, parseChartData } from "@/components/chart/utils"; @@ -29,10 +29,17 @@ import { useAnalytics } from "@/hooks/store/use-analytics"; import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; import { AnalyticsService } from "@/services/analytics.service"; import AnalyticsEmptyState from "../empty-state"; +import { exportCSV } from "../export"; import { DataTable } from "../insight-table/data-table"; import { ChartLoader } from "../loaders"; import { generateBarColor } from "./utils"; +declare module "@tanstack/react-table" { + interface ColumnMeta { + export: ExportConfig; + } +} + interface Props { x_axis: ChartXAxisProperty; y_axis: ChartYAxisMetric; @@ -146,11 +153,25 @@ const PriorityChart = observer((props: Props) => { { accessorKey: "name", header: () => xAxisLabel, + meta: { + export: { + key: xAxisLabel, + value: (row) => row.original.name, + label: xAxisLabel, + }, + }, }, { accessorKey: "count", header: () =>
Count
, cell: ({ row }) =>
{row.original.count}
, + meta: { + export: { + key: "Count", + value: (row) => row.original.count, + label: "Count", + }, + }, }, ], [xAxisLabel] @@ -163,40 +184,18 @@ const PriorityChart = observer((props: Props) => { accessorKey: key, header: () =>
{parsedData.schema[key]}
, cell: ({ row }) =>
{row.original[key]}
, + meta: { + export: { + key, + value: (row) => row.original[key], + label: parsedData.schema[key], + }, + }, })) : [], [parsedData] ); - const csvConfig = mkConfig({ - fieldSeparator: ",", - filename: `${workspaceSlug}-analytics`, - decimalSeparator: ".", - useKeysAsHeaders: true, - }); - - const exportCSV = (rows: Row[]) => { - const rowData = rows.map((row) => { - const hiddenFields = ["key", "avatar_url", "assignee_id", "project_id"]; - const otherFields = Object.keys(row.original).filter( - (key) => key !== "name" && key !== "count" && !hiddenFields.includes(key) && !key.includes("id") - ); - return { - name: row.original.name, - count: row.original.count, - ...otherFields.reduce( - (acc, key) => { - acc[parsedData?.schema[key] ?? key] = row.original[key]; - return acc; - }, - {} as Record - ), - }; - }); - const csv = generateCsv(csvConfig)(rowData); - download(csvConfig)(csv); - }; - return (
{priorityChartLoading ? ( @@ -217,7 +216,7 @@ const PriorityChart = observer((props: Props) => { }} yAxis={{ key: "count", - label: yAxisLabel, + label: t("no_of", { entity: yAxisLabel.replace("_", " ") }), offset: -40, dx: -26, }} @@ -230,7 +229,7 @@ const PriorityChart = observer((props: Props) => { diff --git a/web/core/components/analytics/work-items/workitems-insight-table.tsx b/web/core/components/analytics/work-items/workitems-insight-table.tsx index 45e12b1e3..98935271f 100644 --- a/web/core/components/analytics/work-items/workitems-insight-table.tsx +++ b/web/core/components/analytics/work-items/workitems-insight-table.tsx @@ -1,13 +1,12 @@ -import { useMemo, useCallback } from "react"; -import { ColumnDef, Row } from "@tanstack/react-table"; -import { download, generateCsv } from "export-to-csv"; +import { useMemo } from "react"; +import { ColumnDef, Row, RowData } from "@tanstack/react-table"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; import useSWR from "swr"; import { Briefcase, UserRound } from "lucide-react"; // plane package imports import { useTranslation } from "@plane/i18n"; -import { WorkItemInsightColumns, AnalyticsTableDataMap } from "@plane/types"; +import { WorkItemInsightColumns, AnalyticsTableDataMap, ExportConfig } from "@plane/types"; // plane web components import { Avatar } from "@plane/ui"; import { getFileURL } from "@plane/utils"; @@ -17,11 +16,17 @@ import { useAnalytics } from "@/hooks/store/use-analytics"; import { useProject } from "@/hooks/store/use-project"; import { AnalyticsService } from "@/services/analytics.service"; // plane web components -import { csvConfig } from "../config"; +import { exportCSV } from "../export"; import { InsightTable } from "../insight-table"; const analyticsService = new AnalyticsService(); +declare module "@tanstack/react-table" { + interface ColumnMeta { + export: ExportConfig; + } +} + const WorkItemsInsightTable = observer(() => { // router const params = useParams(); @@ -60,104 +65,125 @@ const WorkItemsInsightTable = observer(() => { }), [t] ); - const columns = useMemo( - () => - [ - !isPeekView - ? { - accessorKey: "project__name", - header: () =>
{columnsLabels["project__name"]}
, - cell: ({ row }) => { - const project = getProjectById(row.original.project_id); - return ( -
- {project?.logo_props ? ( - - ) : ( - - )} - {project?.name} -
- ); - }, - } - : { - accessorKey: "display_name", - header: () =>
{columnsLabels["display_name"]}
, - cell: ({ row }: { row: Row }) => ( -
-
- {row.original.avatar_url && row.original.avatar_url !== "" ? ( - - ) : ( -
- {row.original.display_name ? ( - row.original.display_name?.[0] - ) : ( - - )} -
- )} - - {row.original.display_name ?? t(`Unassigned`)} - -
+ const columns: ColumnDef[] = useMemo( + () => [ + !isPeekView + ? { + accessorKey: "project__name", + header: () =>
{columnsLabels["project__name"]}
, + cell: ({ row }) => { + const project = getProjectById(row.original.project_id); + return ( +
+ {project?.logo_props ? ( + + ) : ( + + )} + {project?.name}
- ), + ); }, - { - accessorKey: "backlog_work_items", - header: () =>
{columnsLabels["backlog_work_items"]}
, - cell: ({ row }) =>
{row.original.backlog_work_items}
, + meta: { + export: { + key: columnsLabels["project__name"], + value: (row) => row.original.project__name?.toString() ?? "", + }, + }, + } + : { + accessorKey: "display_name", + header: () =>
{columnsLabels["display_name"]}
, + cell: ({ row }: { row: Row }) => ( +
+
+ {row.original.avatar_url && row.original.avatar_url !== "" ? ( + + ) : ( +
+ {row.original.display_name ? ( + row.original.display_name?.[0] + ) : ( + + )} +
+ )} + + {row.original.display_name ?? t(`Unassigned`)} + +
+
+ ), + meta: { + export: { + key: columnsLabels["display_name"], + value: (row) => row.original.display_name?.toString() ?? "", + }, + }, + }, + { + accessorKey: "backlog_work_items", + header: () =>
{columnsLabels["backlog_work_items"]}
, + cell: ({ row }) =>
{row.original.backlog_work_items}
, + meta: { + export: { + key: columnsLabels["backlog_work_items"], + value: (row) => row.original.backlog_work_items.toString(), + }, }, - { - accessorKey: "started_work_items", - header: () =>
{columnsLabels["started_work_items"]}
, - cell: ({ row }) =>
{row.original.started_work_items}
, + }, + { + accessorKey: "started_work_items", + header: () =>
{columnsLabels["started_work_items"]}
, + cell: ({ row }) =>
{row.original.started_work_items}
, + meta: { + export: { + key: columnsLabels["started_work_items"], + value: (row) => row.original.started_work_items.toString(), + }, }, - { - accessorKey: "un_started_work_items", - header: () =>
{columnsLabels["un_started_work_items"]}
, - cell: ({ row }) =>
{row.original.un_started_work_items}
, + }, + { + accessorKey: "un_started_work_items", + header: () =>
{columnsLabels["un_started_work_items"]}
, + cell: ({ row }) =>
{row.original.un_started_work_items}
, + meta: { + export: { + key: columnsLabels["un_started_work_items"], + value: (row) => row.original.un_started_work_items.toString(), + }, }, - { - accessorKey: "completed_work_items", - header: () =>
{columnsLabels["completed_work_items"]}
, - cell: ({ row }) =>
{row.original.completed_work_items}
, + }, + { + accessorKey: "completed_work_items", + header: () =>
{columnsLabels["completed_work_items"]}
, + cell: ({ row }) =>
{row.original.completed_work_items}
, + meta: { + export: { + key: columnsLabels["completed_work_items"], + value: (row) => row.original.completed_work_items.toString(), + }, }, - { - accessorKey: "cancelled_work_items", - header: () =>
{columnsLabels["cancelled_work_items"]}
, - cell: ({ row }) =>
{row.original.cancelled_work_items}
, + }, + { + accessorKey: "cancelled_work_items", + header: () =>
{columnsLabels["cancelled_work_items"]}
, + cell: ({ row }) =>
{row.original.cancelled_work_items}
, + meta: { + export: { + key: columnsLabels["cancelled_work_items"], + value: (row) => row.original.cancelled_work_items.toString(), + }, }, - ] as ColumnDef[], + }, + ], [columnsLabels, getProjectById, isPeekView, t] ); - - const exportCSV = useCallback( - (rows: Row[]) => { - const rowData: any = rows.map((row) => { - const { project_id, avatar_url, assignee_id, ...exportableData } = row.original; - return Object.fromEntries( - Object.entries(exportableData).map(([key, value]) => { - if (columnsLabels?.[key as keyof typeof columnsLabels]) { - return [columnsLabels[key as keyof typeof columnsLabels], value]; - } - return [key, value]; - }) - ); - }); - const csv = generateCsv(csvConfig(workspaceSlug))(rowData); - download(csvConfig(workspaceSlug))(csv); - }, - [columnsLabels, workspaceSlug] - ); - return ( analyticsType="work-items" @@ -166,7 +192,7 @@ const WorkItemsInsightTable = observer(() => { columns={columns} columnsLabels={columnsLabels} headerText={isPeekView ? t("common.assignee") : t("common.projects")} - onExport={exportCSV} + onExport={(rows) => workItemsData && exportCSV(rows, columns, workspaceSlug)} /> ); }); From 11b222ece816e782f3a567c586ba0418521f9d37 Mon Sep 17 00:00:00 2001 From: Aaron Date: Thu, 12 Jun 2025 13:10:27 -0700 Subject: [PATCH 157/201] chore(deps): update TypeScript version across multiple packages to 5.8.3 (#7209) --- admin/package.json | 2 +- live/package.json | 2 +- packages/decorators/package.json | 2 +- packages/editor/package.json | 2 +- packages/eslint-config/package.json | 2 +- packages/hooks/package.json | 2 +- packages/i18n/package.json | 2 +- packages/logger/package.json | 2 +- packages/propel/package.json | 2 +- packages/shared-state/package.json | 2 +- packages/ui/package.json | 2 +- packages/utils/package.json | 2 +- space/package.json | 2 +- web/package.json | 2 +- yarn.lock | 13 ++++--------- 15 files changed, 18 insertions(+), 23 deletions(-) diff --git a/admin/package.json b/admin/package.json index 011419f1d..8dae30c58 100644 --- a/admin/package.json +++ b/admin/package.json @@ -50,6 +50,6 @@ "@types/react-dom": "^18.2.18", "@types/uuid": "^9.0.8", "@types/zxcvbn": "^4.4.4", - "typescript": "5.3.3" + "typescript": "5.8.3" } } diff --git a/live/package.json b/live/package.json index ce1beb93c..0c1be213f 100644 --- a/live/package.json +++ b/live/package.json @@ -58,6 +58,6 @@ "nodemon": "^3.1.7", "ts-node": "^10.9.2", "tsup": "8.4.0", - "typescript": "5.3.3" + "typescript": "5.8.3" } } diff --git a/packages/decorators/package.json b/packages/decorators/package.json index 433b5c11a..566e34827 100644 --- a/packages/decorators/package.json +++ b/packages/decorators/package.json @@ -28,7 +28,7 @@ "@types/reflect-metadata": "^0.1.0", "@types/ws": "^8.5.10", "tsup": "8.4.0", - "typescript": "^5.3.3" + "typescript": "5.8.3" }, "peerDependencies": { "express": ">=4.21.2", diff --git a/packages/editor/package.json b/packages/editor/package.json index 4194c0932..52c5d8dba 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -82,7 +82,7 @@ "@types/react-dom": "^18.2.18", "postcss": "^8.4.38", "tsup": "8.4.0", - "typescript": "5.3.3" + "typescript": "5.8.3" }, "keywords": [ "editor", diff --git a/packages/eslint-config/package.json b/packages/eslint-config/package.json index 6c6689428..9b63b542a 100644 --- a/packages/eslint-config/package.json +++ b/packages/eslint-config/package.json @@ -18,6 +18,6 @@ "eslint-plugin-import": "^2.29.1", "eslint-plugin-react": "^7.33.2", "eslint-plugin-react-hooks": "^5.2.0", - "typescript": "5.3.3" + "typescript": "5.8.3" } } diff --git a/packages/hooks/package.json b/packages/hooks/package.json index e76e28320..82897a8b6 100644 --- a/packages/hooks/package.json +++ b/packages/hooks/package.json @@ -23,6 +23,6 @@ "@types/node": "^22.5.4", "@types/react": "^18.3.11", "tsup": "8.4.0", - "typescript": "^5.3.3" + "typescript": "5.8.3" } } diff --git a/packages/i18n/package.json b/packages/i18n/package.json index ce9073a65..1ccc02071 100644 --- a/packages/i18n/package.json +++ b/packages/i18n/package.json @@ -17,6 +17,6 @@ "devDependencies": { "@plane/eslint-config": "*", "@types/node": "^22.5.4", - "typescript": "^5.3.3" + "typescript": "5.8.3" } } diff --git a/packages/logger/package.json b/packages/logger/package.json index bc4a91c77..ed2f401b1 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -17,6 +17,6 @@ "devDependencies": { "@plane/eslint-config": "*", "@types/node": "^22.5.4", - "typescript": "^5.3.3" + "typescript": "5.8.3" } } diff --git a/packages/propel/package.json b/packages/propel/package.json index f57e96e73..f686d5e1c 100644 --- a/packages/propel/package.json +++ b/packages/propel/package.json @@ -29,6 +29,6 @@ "@plane/typescript-config": "*", "@types/react": "18.3.1", "@types/react-dom": "18.3.0", - "typescript": "^5.3.3" + "typescript": "5.8.3" } } \ No newline at end of file diff --git a/packages/shared-state/package.json b/packages/shared-state/package.json index eaf231f38..0f559de72 100644 --- a/packages/shared-state/package.json +++ b/packages/shared-state/package.json @@ -16,6 +16,6 @@ "devDependencies": { "@plane/eslint-config": "*", "@types/node": "^22.5.4", - "typescript": "^5.3.3" + "typescript": "5.8.3" } } diff --git a/packages/ui/package.json b/packages/ui/package.json index 555b98e23..213af997a 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -72,6 +72,6 @@ "postcss-nested": "^6.0.1", "storybook": "^8.1.1", "tsup": "8.4.0", - "typescript": "5.3.3" + "typescript": "5.8.3" } } diff --git a/packages/utils/package.json b/packages/utils/package.json index 6a3d5f973..5aebea9ac 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -30,6 +30,6 @@ "@types/react": "^18.3.11", "@types/zxcvbn": "^4.4.5", "tsup": "8.4.0", - "typescript": "^5.3.3" + "typescript": "5.8.3" } } diff --git a/space/package.json b/space/package.json index 95c0ffa72..f55b427f0 100644 --- a/space/package.json +++ b/space/package.json @@ -63,6 +63,6 @@ "@types/uuid": "^9.0.1", "@types/zxcvbn": "^4.4.4", "@typescript-eslint/eslint-plugin": "^5.48.2", - "typescript": "5.3.3" + "typescript": "5.8.3" } } diff --git a/web/package.json b/web/package.json index 57e1afafb..825921fc6 100644 --- a/web/package.json +++ b/web/package.json @@ -82,6 +82,6 @@ "@types/uuid": "^8.3.4", "@types/zxcvbn": "^4.4.4", "prettier": "^3.2.5", - "typescript": "5.3.3" + "typescript": "5.8.3" } } diff --git a/yarn.lock b/yarn.lock index dd601e5c6..f60eb834f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10940,15 +10940,10 @@ typed-styles@^0.0.7: resolved "https://registry.npmjs.org/typed-styles/-/typed-styles-0.0.7.tgz#93392a008794c4595119ff62dde6809dbc40a3d9" integrity sha512-pzP0PWoZUhsECYjABgCGQlRGL1n7tOHsgwYv3oIiEpJwGhFTuty/YNeduxQYzXXa3Ge5BdT6sHYIQYpl4uJ+5Q== -typescript@5.3.3: - version "5.3.3" - resolved "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37" - integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw== - -typescript@^5.3.3: - version "5.7.3" - resolved "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz#919b44a7dbb8583a9b856d162be24a54bf80073e" - integrity sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw== +typescript@5.8.3: + version "5.8.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.8.3.tgz#92f8a3e5e3cf497356f4178c34cd65a7f5e8440e" + integrity sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ== uc.micro@^2.0.0, uc.micro@^2.1.0: version "2.1.0" From ebc2bdcd3aef1cf51f224dd3f9d1ee1d5c719095 Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Fri, 13 Jun 2025 01:50:44 +0530 Subject: [PATCH 158/201] feat: adding build process to logger package using tsup #7210 --- packages/logger/.eslintrc.js | 3 --- packages/logger/package.json | 14 +++++++++++--- packages/logger/tsup.config.ts | 12 ++++++++++++ 3 files changed, 23 insertions(+), 6 deletions(-) create mode 100644 packages/logger/tsup.config.ts diff --git a/packages/logger/.eslintrc.js b/packages/logger/.eslintrc.js index 558b8f76e..b11b7bb6d 100644 --- a/packages/logger/.eslintrc.js +++ b/packages/logger/.eslintrc.js @@ -3,7 +3,4 @@ module.exports = { root: true, extends: ["@plane/eslint-config/library.js"], parser: "@typescript-eslint/parser", - parserOptions: { - project: true, - }, }; diff --git a/packages/logger/package.json b/packages/logger/package.json index ed2f401b1..83179ba2d 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -4,11 +4,18 @@ "license": "AGPL-3.0", "description": "Logger shared across multiple apps internally", "private": true, - "main": "./src/index.ts", - "types": "./src/index.ts", + "main": "./dist/index.js", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "files": [ + "dist/**" + ], "scripts": { + "build": "tsup", + "dev": "tsup --watch", "lint": "eslint src --ext .ts,.tsx", - "lint:errors": "eslint src --ext .ts,.tsx --quiet" + "lint:errors": "eslint src --ext .ts,.tsx --quiet", + "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist" }, "dependencies": { "winston": "^3.17.0", @@ -17,6 +24,7 @@ "devDependencies": { "@plane/eslint-config": "*", "@types/node": "^22.5.4", + "tsup": "8.4.0", "typescript": "5.8.3" } } diff --git a/packages/logger/tsup.config.ts b/packages/logger/tsup.config.ts new file mode 100644 index 000000000..85bf72fce --- /dev/null +++ b/packages/logger/tsup.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + format: ["esm", "cjs"], + dts: true, + splitting: false, + sourcemap: true, + clean: true, + external: ["winston", "winston-daily-rotate-file"], + treeshake: true, +}); From 6fe0415d667022dfcf94dd1e4825c15dc6e5c49f Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Fri, 13 Jun 2025 14:41:08 +0530 Subject: [PATCH 159/201] [WEB-4316] chore: new endpoints to download an asset (#7207) * chore: new endpoints to download an asset * chore: add exception handling --- apiserver/plane/app/urls/asset.py | 12 ++++++ apiserver/plane/app/views/__init__.py | 2 + apiserver/plane/app/views/asset/v2.py | 53 +++++++++++++++++++++++++++ 3 files changed, 67 insertions(+) diff --git a/apiserver/plane/app/urls/asset.py b/apiserver/plane/app/urls/asset.py index 77dd3d00e..93356b04c 100644 --- a/apiserver/plane/app/urls/asset.py +++ b/apiserver/plane/app/urls/asset.py @@ -13,6 +13,8 @@ from plane.app.views import ( ProjectAssetEndpoint, ProjectBulkAssetEndpoint, AssetCheckEndpoint, + WorkspaceAssetDownloadEndpoint, + ProjectAssetDownloadEndpoint, ) @@ -89,4 +91,14 @@ urlpatterns = [ AssetCheckEndpoint.as_view(), name="asset-check", ), + path( + "assets/v2/workspaces//download//", + WorkspaceAssetDownloadEndpoint.as_view(), + name="workspace-asset-download", + ), + path( + "assets/v2/workspaces//projects//download//", + ProjectAssetDownloadEndpoint.as_view(), + name="project-asset-download", + ), ] diff --git a/apiserver/plane/app/views/__init__.py b/apiserver/plane/app/views/__init__.py index 55642a533..6d56473e3 100644 --- a/apiserver/plane/app/views/__init__.py +++ b/apiserver/plane/app/views/__init__.py @@ -107,6 +107,8 @@ from .asset.v2 import ( ProjectAssetEndpoint, ProjectBulkAssetEndpoint, AssetCheckEndpoint, + WorkspaceAssetDownloadEndpoint, + ProjectAssetDownloadEndpoint, ) from .issue.base import ( IssueListEndpoint, diff --git a/apiserver/plane/app/views/asset/v2.py b/apiserver/plane/app/views/asset/v2.py index aecba04b8..5994ffd8c 100644 --- a/apiserver/plane/app/views/asset/v2.py +++ b/apiserver/plane/app/views/asset/v2.py @@ -718,3 +718,56 @@ class AssetCheckEndpoint(BaseAPIView): id=asset_id, workspace__slug=slug, deleted_at__isnull=True ).exists() return Response({"exists": asset}, status=status.HTTP_200_OK) + + +class WorkspaceAssetDownloadEndpoint(BaseAPIView): + """Endpoint to generate a download link for an asset with content-disposition=attachment.""" + + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE") + def get(self, request, slug, asset_id): + try: + asset = FileAsset.objects.get( + id=asset_id, + workspace__slug=slug, + is_uploaded=True, + ) + except FileAsset.DoesNotExist: + return Response( + {"error": "The requested asset could not be found."}, + status=status.HTTP_404_NOT_FOUND, + ) + + storage = S3Storage(request=request) + signed_url = storage.generate_presigned_url( + object_name=asset.asset.name, + disposition=f"attachment; filename={asset.asset.name}", + ) + + return HttpResponseRedirect(signed_url) + + +class ProjectAssetDownloadEndpoint(BaseAPIView): + """Endpoint to generate a download link for an asset with content-disposition=attachment.""" + + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="PROJECT") + def get(self, request, slug, project_id, asset_id): + try: + asset = FileAsset.objects.get( + id=asset_id, + workspace__slug=slug, + project_id=project_id, + is_uploaded=True, + ) + except FileAsset.DoesNotExist: + return Response( + {"error": "The requested asset could not be found."}, + status=status.HTTP_404_NOT_FOUND, + ) + + storage = S3Storage(request=request) + signed_url = storage.generate_presigned_url( + object_name=asset.asset.name, + disposition=f"attachment; filename={asset.asset.name}", + ) + + return HttpResponseRedirect(signed_url) From 0fa9c8b0156d31a03828112ac516d26f1c3e15ac Mon Sep 17 00:00:00 2001 From: JayashTripathy <76092296+JayashTripathy@users.noreply.github.com> Date: Mon, 16 Jun 2025 14:01:49 +0530 Subject: [PATCH 160/201] [WEB-4323] refactor: Analytics refactor (#7213) * chore: updated label for epics * chore: improved export logic * refactor: move csvConfig to export.ts and clean up export logic * refactor: remove unused CSV export logic from WorkItemsInsightTable component * refactor: streamline data handling in InsightTable component for improved rendering * feat: add translation for "No. of {entity}" and update priority chart y-axis label to use new translation * refactor: cleaned up some component and added utilitites * feat: add "at_risk" translation to multiple languages in translations.json files * refactor: update TrendPiece component to use new status variants for analytics * fix: adjust TrendPiece component logic for on-track and off-track status * refactor: use nullish coalescing operator for yAxis.dx in line and scatter charts * feat: add "at_risk" translation to various languages in translations.json files * feat: add "no_of" translation to various languages in translations.json files * feat: update "at_risk" translation in Ukrainian, Vietnamese, and Chinese locales in translations.json files * refactor: rename insightsFields to ANALYTICS_INSIGHTS_FIELDS and update analytics tab import to use getAnalyticsTabs function * feat: update AnalyticsWrapper to use i18n for titles and add new translation for "no_of" in Russian * fix: update yAxis labels and offsets in various charts to use new translation key and improve layout * feat: define AnalyticsTab interface and refactor getAnalyticsTabs function for improved type safety * fix: update AnalyticsTab interface to use TAnalyticsTabsBase for improved type safety * fix: add whitespace-nowrap class to TableHead for improved header layout in DataTable component --- packages/constants/src/analytics/common.ts | 2 +- packages/i18n/src/locales/ru/translations.json | 3 ++- packages/propel/src/charts/bar-chart/root.tsx | 4 ++-- packages/types/src/analytics.d.ts | 6 ++++++ .../[workspaceSlug]/(projects)/analytics/page.tsx | 9 ++++++--- web/ce/components/analytics/tabs.ts | 12 ------------ web/ce/components/analytics/tabs.tsx | 8 ++++++++ web/core/components/analytics/analytics-wrapper.tsx | 9 +++++---- .../analytics/insight-table/data-table.tsx | 2 +- web/core/components/analytics/overview/root.tsx | 2 +- web/core/components/analytics/total-insights.tsx | 6 +++--- .../analytics/work-items/created-vs-resolved.tsx | 6 +++--- .../analytics/work-items/priority-chart.tsx | 4 ++-- web/core/components/analytics/work-items/root.tsx | 2 +- 14 files changed, 41 insertions(+), 34 deletions(-) delete mode 100644 web/ce/components/analytics/tabs.ts create mode 100644 web/ce/components/analytics/tabs.tsx diff --git a/packages/constants/src/analytics/common.ts b/packages/constants/src/analytics/common.ts index cf0624e91..4ac899523 100644 --- a/packages/constants/src/analytics/common.ts +++ b/packages/constants/src/analytics/common.ts @@ -11,7 +11,7 @@ export interface IInsightField { }; } -export const insightsFields: Record = { +export const ANALYTICS_INSIGHTS_FIELDS: Record = { overview: [ { key: "total_users", diff --git a/packages/i18n/src/locales/ru/translations.json b/packages/i18n/src/locales/ru/translations.json index d4067bd72..4dcb5d0e6 100644 --- a/packages/i18n/src/locales/ru/translations.json +++ b/packages/i18n/src/locales/ru/translations.json @@ -881,7 +881,8 @@ "completed": "Завершено", "in_progress": "В процессе", "planned": "Запланировано", - "paused": "На паузе" + "paused": "На паузе", + "no_of": "Количество {entity}" }, "chart": { "x_axis": "Ось X", diff --git a/packages/propel/src/charts/bar-chart/root.tsx b/packages/propel/src/charts/bar-chart/root.tsx index e3dbe1d8c..8826a55cf 100644 --- a/packages/propel/src/charts/bar-chart/root.tsx +++ b/packages/propel/src/charts/bar-chart/root.tsx @@ -124,8 +124,8 @@ export const BarChart = React.memo((props: T value: yAxis.label, angle: -90, position: "bottom", - offset: -24, - dx: -16, + offset: yAxis.offset ?? -24, + dx: yAxis.dx ?? -16, className: AXIS_LABEL_CLASSNAME, }} tick={(props) => } diff --git a/packages/types/src/analytics.d.ts b/packages/types/src/analytics.d.ts index dbcf52f36..80c773fa2 100644 --- a/packages/types/src/analytics.d.ts +++ b/packages/types/src/analytics.d.ts @@ -4,6 +4,12 @@ import { Row } from "@tanstack/react-table"; export type TAnalyticsTabsBase = "overview" | "work-items"; export type TAnalyticsGraphsBase = "projects" | "work-items" | "custom-work-items"; +export interface AnalyticsTab { + key: TAnalyticsTabsBase; + label: string; + content: React.FC; + isDisabled: boolean; +} export type TAnalyticsFilterParams = { project_ids?: string; cycle_id?: string; diff --git a/web/app/(all)/[workspaceSlug]/(projects)/analytics/page.tsx b/web/app/(all)/[workspaceSlug]/(projects)/analytics/page.tsx index 389a80a90..7e9e0ac9e 100644 --- a/web/app/(all)/[workspaceSlug]/(projects)/analytics/page.tsx +++ b/web/app/(all)/[workspaceSlug]/(projects)/analytics/page.tsx @@ -14,7 +14,7 @@ import { ComicBoxButton, DetailedEmptyState } from "@/components/empty-state"; // hooks import { useCommandPalette, useEventTracker, useProject, useUserPermissions, useWorkspace } from "@/hooks/store"; import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; -import { ANALYTICS_TABS } from "@/plane-web/components/analytics/tabs"; +import { getAnalyticsTabs } from "@/plane-web/components/analytics/tabs"; const AnalyticsPage = observer(() => { const router = useRouter(); @@ -40,17 +40,20 @@ const AnalyticsPage = observer(() => { EUserPermissionsLevel.WORKSPACE ); + const ANALYTICS_TABS = useMemo(() => getAnalyticsTabs(t), [t]); + const tabs = useMemo( () => ANALYTICS_TABS.map((tab) => ({ key: tab.key, - label: t(tab.i18nKey), + label: tab.label, content: , onClick: () => { router.push(`?tab=${tab.key}`); }, + isDisabled: tab.isDisabled, })), - [router, t] + [ANALYTICS_TABS, router] ); const defaultTab = searchParams.get("tab") || ANALYTICS_TABS[0].key; diff --git a/web/ce/components/analytics/tabs.ts b/web/ce/components/analytics/tabs.ts deleted file mode 100644 index 6f978a3c6..000000000 --- a/web/ce/components/analytics/tabs.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { TAnalyticsTabsBase } from "@plane/types"; -import { Overview } from "@/components/analytics/overview"; -import { WorkItems } from "@/components/analytics/work-items"; -export const ANALYTICS_TABS: { - key: TAnalyticsTabsBase; - i18nKey: string; - content: React.FC; - isExtended?: boolean; -}[] = [ - { key: "overview", i18nKey: "common.overview", content: Overview }, - { key: "work-items", i18nKey: "sidebar.work_items", content: WorkItems }, -]; diff --git a/web/ce/components/analytics/tabs.tsx b/web/ce/components/analytics/tabs.tsx new file mode 100644 index 000000000..eb8344c05 --- /dev/null +++ b/web/ce/components/analytics/tabs.tsx @@ -0,0 +1,8 @@ +import { AnalyticsTab } from "@plane/types"; +import { Overview } from "@/components/analytics/overview"; +import { WorkItems } from "@/components/analytics/work-items"; + +export const getAnalyticsTabs = (t: (key: string, params?: Record) => string): AnalyticsTab[] => [ + { key: "overview", label: t("common.overview"), content: Overview, isDisabled: false }, + { key: "work-items", label: t("sidebar.work_items"), content: WorkItems, isDisabled: false }, +]; diff --git a/web/core/components/analytics/analytics-wrapper.tsx b/web/core/components/analytics/analytics-wrapper.tsx index d6193a2b3..c86edb950 100644 --- a/web/core/components/analytics/analytics-wrapper.tsx +++ b/web/core/components/analytics/analytics-wrapper.tsx @@ -1,19 +1,20 @@ import React from "react"; // plane package imports +import { useTranslation } from "@plane/i18n"; import { cn } from "@plane/utils"; type Props = { - title: string; + i18nTitle: string; children: React.ReactNode; className?: string; }; const AnalyticsWrapper: React.FC = (props) => { - const { title, children, className } = props; - + const { i18nTitle, children, className } = props; + const { t } = useTranslation(); return (
-

{title}

+

{t(i18nTitle)}

{children}
); diff --git a/web/core/components/analytics/insight-table/data-table.tsx b/web/core/components/analytics/insight-table/data-table.tsx index 35dc3b365..8b1c1bab2 100644 --- a/web/core/components/analytics/insight-table/data-table.tsx +++ b/web/core/components/analytics/insight-table/data-table.tsx @@ -131,7 +131,7 @@ export function DataTable({ columns, data, searchPlaceholder, act {table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => ( - + {header.isPlaceholder ? null : (flexRender(header.column.columnDef.header, header.getContext()) as any)} diff --git a/web/core/components/analytics/overview/root.tsx b/web/core/components/analytics/overview/root.tsx index 3856353aa..b10bf32de 100644 --- a/web/core/components/analytics/overview/root.tsx +++ b/web/core/components/analytics/overview/root.tsx @@ -5,7 +5,7 @@ import ActiveProjects from "./active-projects"; import ProjectInsights from "./project-insights"; const Overview: React.FC = () => ( - +
diff --git a/web/core/components/analytics/total-insights.tsx b/web/core/components/analytics/total-insights.tsx index 61f3e7205..a5412dca3 100644 --- a/web/core/components/analytics/total-insights.tsx +++ b/web/core/components/analytics/total-insights.tsx @@ -2,7 +2,7 @@ import { observer } from "mobx-react-lite"; import { useParams } from "next/navigation"; import useSWR from "swr"; -import { IInsightField, insightsFields } from "@plane/constants"; +import { IInsightField, ANALYTICS_INSIGHTS_FIELDS } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { IAnalyticsResponse, TAnalyticsTabsBase } from "@plane/types"; //hooks @@ -80,13 +80,13 @@ const TotalInsights: React.FC<{ className={cn( "grid grid-cols-1 gap-8 sm:grid-cols-2 md:gap-10", !peekView - ? insightsFields[analyticsType]?.length % 5 === 0 + ? ANALYTICS_INSIGHTS_FIELDS[analyticsType]?.length % 5 === 0 ? "gap-10 lg:grid-cols-5" : "gap-8 lg:grid-cols-4" : "grid-cols-2" )} > - {insightsFields[analyticsType]?.map((item) => ( + {ANALYTICS_INSIGHTS_FIELDS[analyticsType]?.map((item) => ( { }} yAxis={{ key: "count", - label: t("no_of", { entity: isEpic ? t("epics") : t("work_items") }), - offset: -30, - dx: -22, + label: t("common.no_of", { entity: isEpic ? t("epics") : t("work_items") }), + offset: -60, + dx: -24, }} legend={{ align: "left", diff --git a/web/core/components/analytics/work-items/priority-chart.tsx b/web/core/components/analytics/work-items/priority-chart.tsx index 354044bda..5c0069978 100644 --- a/web/core/components/analytics/work-items/priority-chart.tsx +++ b/web/core/components/analytics/work-items/priority-chart.tsx @@ -216,8 +216,8 @@ const PriorityChart = observer((props: Props) => { }} yAxis={{ key: "count", - label: t("no_of", { entity: yAxisLabel.replace("_", " ") }), - offset: -40, + label: t("common.no_of", { entity: yAxisLabel.replace("_", " ") }), + offset: -60, dx: -26, }} /> diff --git a/web/core/components/analytics/work-items/root.tsx b/web/core/components/analytics/work-items/root.tsx index 80e8aef62..c30a36d59 100644 --- a/web/core/components/analytics/work-items/root.tsx +++ b/web/core/components/analytics/work-items/root.tsx @@ -6,7 +6,7 @@ import CustomizedInsights from "./customized-insights"; import WorkItemsInsightTable from "./workitems-insight-table"; const WorkItems: React.FC = () => ( - +
From cf595de7c74b05240ea55f564fa4439c6c95fe52 Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Mon, 16 Jun 2025 14:02:47 +0530 Subject: [PATCH 161/201] [WEB-4311] fix: membership data handling and state reversal on error (#7205) --- .../store/member/base-project-member.store.ts | 2 +- .../store/member/workspace-member.store.ts | 24 ++++++++++++------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/web/core/store/member/base-project-member.store.ts b/web/core/store/member/base-project-member.store.ts index 40047b065..ceba44c9e 100644 --- a/web/core/store/member/base-project-member.store.ts +++ b/web/core/store/member/base-project-member.store.ts @@ -273,7 +273,7 @@ export abstract class BaseProjectMemberStore implements IBaseProjectMemberStore if (!memberDetails || !memberDetails?.id) throw new Error("Member not found"); // original data to revert back in case of error const isCurrentUser = this.rootStore.user.data?.id === userId; - const membershipBeforeUpdate = this.getProjectMembershipByUserId(userId, projectId); + const membershipBeforeUpdate = { ...this.getProjectMembershipByUserId(userId, projectId) }; const permissionBeforeUpdate = isCurrentUser ? this.rootStore.user.permission.getProjectRoleByWorkspaceSlugAndProjectId(workspaceSlug, projectId) : undefined; diff --git a/web/core/store/member/workspace-member.store.ts b/web/core/store/member/workspace-member.store.ts index e45c2d5c2..a4a5b1f1c 100644 --- a/web/core/store/member/workspace-member.store.ts +++ b/web/core/store/member/workspace-member.store.ts @@ -30,6 +30,7 @@ export interface IWorkspaceMemberStore { workspaceMemberInvitationIds: string[] | null; memberMap: Record | null; // computed actions + getWorkspaceMemberIds: (workspaceSlug: string) => string[]; getSearchedWorkspaceMemberIds: (searchQuery: string) => string[] | null; getSearchedWorkspaceInvitationIds: (searchQuery: string) => string[] | null; getWorkspaceMemberDetails: (workspaceMemberId: string) => IWorkspaceMember | null; @@ -95,14 +96,8 @@ export class WorkspaceMemberStore implements IWorkspaceMemberStore { get workspaceMemberIds() { const workspaceSlug = this.routerStore.workspaceSlug; if (!workspaceSlug) return null; - let members = Object.values(this.workspaceMemberMap?.[workspaceSlug] ?? {}); - members = sortBy(members, [ - (m) => m.member !== this.userStore?.data?.id, - (m) => this.memberRoot?.memberMap?.[m.member]?.display_name?.toLowerCase(), - ]); - //filter out bots - const memberIds = members.filter((m) => !this.memberRoot?.memberMap?.[m.member]?.is_bot).map((m) => m.member); - return memberIds; + + return this.getWorkspaceMemberIds(workspaceSlug); } get memberMap() { @@ -117,6 +112,17 @@ export class WorkspaceMemberStore implements IWorkspaceMemberStore { return this.workspaceMemberInvitations?.[workspaceSlug]?.map((inv) => inv.id); } + getWorkspaceMemberIds = computedFn((workspaceSlug: string) => { + let members = Object.values(this.workspaceMemberMap?.[workspaceSlug] ?? {}); + members = sortBy(members, [ + (m) => m.member !== this.userStore?.data?.id, + (m) => this.memberRoot?.memberMap?.[m.member]?.display_name?.toLowerCase(), + ]); + //filter out bots + const memberIds = members.filter((m) => !this.memberRoot?.memberMap?.[m.member]?.is_bot).map((m) => m.member); + return memberIds; + }); + /** * @description get the list of all the user ids that match the search query of all the members of the current workspace * @param searchQuery @@ -217,7 +223,7 @@ export class WorkspaceMemberStore implements IWorkspaceMemberStore { const memberDetails = this.getWorkspaceMemberDetails(userId); if (!memberDetails) throw new Error("Member not found"); // original data to revert back in case of error - const originalProjectMemberData = this.workspaceMemberMap?.[workspaceSlug]?.[userId]; + const originalProjectMemberData = { ...this.workspaceMemberMap?.[workspaceSlug]?.[userId] }; try { runInAction(() => { set(this.workspaceMemberMap, [workspaceSlug, userId, "role"], data.role); From e13d8aa4b37c3dda740319829b8fa59bb0644111 Mon Sep 17 00:00:00 2001 From: JayashTripathy <76092296+JayashTripathy@users.noreply.github.com> Date: Mon, 16 Jun 2025 14:03:07 +0530 Subject: [PATCH 162/201] [WEB-4231] Pie chart tooltip #7192 --- packages/propel/src/charts/pie-chart/root.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/propel/src/charts/pie-chart/root.tsx b/packages/propel/src/charts/pie-chart/root.tsx index 1110260b9..f49acfd7e 100644 --- a/packages/propel/src/charts/pie-chart/root.tsx +++ b/packages/propel/src/charts/pie-chart/root.tsx @@ -128,7 +128,7 @@ export const PieChart = React.memo((props: T className: "text-custom-background-90/80 cursor-pointer", }} wrapperStyle={{ - pointerEvents: "auto", + pointerEvents: "none", }} content={({ active, payload }) => { if (!active || !payload || !payload.length) return null; From 640b23fb1b37470b4e37c6a3efe76f0c5f0435a0 Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Mon, 16 Jun 2025 17:02:04 +0530 Subject: [PATCH 163/201] chore(deps): nextjs upgrade to 14.2.30 --- admin/package.json | 2 +- space/package.json | 2 +- web/package.json | 2 +- yarn.lock | 108 ++++++++++++++++++++++----------------------- 4 files changed, 57 insertions(+), 57 deletions(-) diff --git a/admin/package.json b/admin/package.json index 8dae30c58..7b1746726 100644 --- a/admin/package.json +++ b/admin/package.json @@ -31,7 +31,7 @@ "lucide-react": "^0.469.0", "mobx": "^6.12.0", "mobx-react": "^9.1.1", - "next": "^14.2.29", + "next": "14.2.30", "next-themes": "^0.2.1", "postcss": "^8.4.38", "react": "^18.3.1", diff --git a/space/package.json b/space/package.json index f55b427f0..9a74895b4 100644 --- a/space/package.json +++ b/space/package.json @@ -37,7 +37,7 @@ "mobx": "^6.10.0", "mobx-react": "^9.1.1", "mobx-utils": "^6.0.8", - "next": "^14.2.29", + "next": "14.2.30", "next-themes": "^0.2.1", "nprogress": "^0.2.0", "react": "^18.3.1", diff --git a/web/package.json b/web/package.json index 825921fc6..2be55d235 100644 --- a/web/package.json +++ b/web/package.json @@ -45,7 +45,7 @@ "mobx": "^6.10.0", "mobx-react": "^9.1.1", "mobx-utils": "^6.0.8", - "next": "^14.2.29", + "next": "14.2.30", "next-themes": "^0.2.1", "nprogress": "^0.2.0", "posthog-js": "^1.131.3", diff --git a/yarn.lock b/yarn.lock index f60eb834f..6b678566d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1613,10 +1613,10 @@ prop-types "^15.8.1" react-is "^19.0.0" -"@next/env@14.2.29": - version "14.2.29" - resolved "https://registry.npmjs.org/@next/env/-/env-14.2.29.tgz#febceb77ab90e44a683c10748a62d1bc10f20d19" - integrity sha512-UzgLR2eBfhKIQt0aJ7PWH7XRPYw7SXz0Fpzdl5THjUnvxy4kfBk9OU4RNPNiETewEEtaBcExNFNn1QWH8wQTjg== +"@next/env@14.2.30": + version "14.2.30" + resolved "https://registry.yarnpkg.com/@next/env/-/env-14.2.30.tgz#f955b57975751584722b6b0a2a8cf2bdcc4ffae3" + integrity sha512-KBiBKrDY6kxTQWGzKjQB7QirL3PiiOkV7KW98leHFjtVRKtft76Ra5qSA/SL75xT44dp6hOcqiiJ6iievLOYug== "@next/eslint-plugin-next@14.2.24": version "14.2.24" @@ -1625,50 +1625,50 @@ dependencies: glob "10.3.10" -"@next/swc-darwin-arm64@14.2.29": - version "14.2.29" - resolved "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.29.tgz#615cf42d1533272fcea468489387a089f1ab01c4" - integrity sha512-wWtrAaxCVMejxPHFb1SK/PVV1WDIrXGs9ki0C/kUM8ubKHQm+3hU9MouUywCw8Wbhj3pewfHT2wjunLEr/TaLA== +"@next/swc-darwin-arm64@14.2.30": + version "14.2.30" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.30.tgz#8179a35a068bc6f43a9ab6439875f6e330d02e52" + integrity sha512-EAqfOTb3bTGh9+ewpO/jC59uACadRHM6TSA9DdxJB/6gxOpyV+zrbqeXiFTDy9uV6bmipFDkfpAskeaDcO+7/g== -"@next/swc-darwin-x64@14.2.29": - version "14.2.29" - resolved "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.29.tgz#2bf21a5f25f784c456cc58f0f273ede6898a3c96" - integrity sha512-7Z/jk+6EVBj4pNLw/JQrvZVrAh9Bv8q81zCFSfvTMZ51WySyEHWVpwCEaJY910LyBftv2F37kuDPQm0w9CEXyg== +"@next/swc-darwin-x64@14.2.30": + version "14.2.30" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.30.tgz#87c08d805c0546a73c25a0538a81f8b5f43bd0e9" + integrity sha512-TyO7Wz1IKE2kGv8dwQ0bmPL3s44EKVencOqwIY69myoS3rdpO1NPg5xPM5ymKu7nfX4oYJrpMxv8G9iqLsnL4A== -"@next/swc-linux-arm64-gnu@14.2.29": - version "14.2.29" - resolved "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.29.tgz#408f86e4ae787342f93513a562a226545e971953" - integrity sha512-o6hrz5xRBwi+G7JFTHc+RUsXo2lVXEfwh4/qsuWBMQq6aut+0w98WEnoNwAwt7hkEqegzvazf81dNiwo7KjITw== +"@next/swc-linux-arm64-gnu@14.2.30": + version "14.2.30" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.30.tgz#eed26d87d96d9ef6fffbde98ceed2c75108a9911" + integrity sha512-I5lg1fgPJ7I5dk6mr3qCH1hJYKJu1FsfKSiTKoYwcuUf53HWTrEkwmMI0t5ojFKeA6Vu+SfT2zVy5NS0QLXV4Q== -"@next/swc-linux-arm64-musl@14.2.29": - version "14.2.29" - resolved "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.29.tgz#94071e41c222b68cbbf82fa3a3a33f5b5ca19a94" - integrity sha512-9i+JEHBOVgqxQ92HHRFlSW1EQXqa/89IVjtHgOqsShCcB/ZBjTtkWGi+SGCJaYyWkr/lzu51NTMCfKuBf7ULNw== +"@next/swc-linux-arm64-musl@14.2.30": + version "14.2.30" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.30.tgz#54b38b43c8acf3d3e0b71ae208a0bfca5a9b8563" + integrity sha512-8GkNA+sLclQyxgzCDs2/2GSwBc92QLMrmYAmoP2xehe5MUKBLB2cgo34Yu242L1siSkwQkiV4YLdCnjwc/Micw== -"@next/swc-linux-x64-gnu@14.2.29": - version "14.2.29" - resolved "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.29.tgz#757a87136f9dd40d7dd0b8624b20b275701c3dcc" - integrity sha512-B7JtMbkUwHijrGBOhgSQu2ncbCYq9E7PZ7MX58kxheiEOwdkM+jGx0cBb+rN5AeqF96JypEppK6i/bEL9T13lA== +"@next/swc-linux-x64-gnu@14.2.30": + version "14.2.30" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.30.tgz#0ee0419da4dc1211a4c925b0841419cd07aa6c59" + integrity sha512-8Ly7okjssLuBoe8qaRCcjGtcMsv79hwzn/63wNeIkzJVFVX06h5S737XNr7DZwlsbTBDOyI6qbL2BJB5n6TV/w== -"@next/swc-linux-x64-musl@14.2.29": - version "14.2.29" - resolved "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.29.tgz#84a6429f212a08c629a3bfca19842a1053653217" - integrity sha512-yCcZo1OrO3aQ38B5zctqKU1Z3klOohIxug6qdiKO3Q3qNye/1n6XIs01YJ+Uf+TdpZQ0fNrOQI2HrTLF3Zprnw== +"@next/swc-linux-x64-musl@14.2.30": + version "14.2.30" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.30.tgz#e88463d8c10dd600087b062f2dea59a515cd66f6" + integrity sha512-dBmV1lLNeX4mR7uI7KNVHsGQU+OgTG5RGFPi3tBJpsKPvOPtg9poyav/BYWrB3GPQL4dW5YGGgalwZ79WukbKQ== -"@next/swc-win32-arm64-msvc@14.2.29": - version "14.2.29" - resolved "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.29.tgz#e79ac6ef251d8e380f5bb406f39dcd144fc0408b" - integrity sha512-WnrfeOEtTVidI9Z6jDLy+gxrpDcEJtZva54LYC0bSKQqmyuHzl0ego+v0F/v2aXq0am67BRqo/ybmmt45Tzo4A== +"@next/swc-win32-arm64-msvc@14.2.30": + version "14.2.30" + resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.30.tgz#6975cbbab74d519b06d93210ed86cd4f3dbc1c4d" + integrity sha512-6MMHi2Qc1Gkq+4YLXAgbYslE1f9zMGBikKMdmQRHXjkGPot1JY3n5/Qrbg40Uvbi8//wYnydPnyvNhI1DMUW1g== -"@next/swc-win32-ia32-msvc@14.2.29": - version "14.2.29" - resolved "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.29.tgz#e8bfabeb2bf004f228063358c862a1159ff94b29" - integrity sha512-vkcriFROT4wsTdSeIzbxaZjTNTFKjSYmLd8q/GVH3Dn8JmYjUKOuKXHK8n+lovW/kdcpIvydO5GtN+It2CvKWA== +"@next/swc-win32-ia32-msvc@14.2.30": + version "14.2.30" + resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.30.tgz#08ad4de2e082bc6b07d41099b4310daec7885748" + integrity sha512-pVZMnFok5qEX4RT59mK2hEVtJX+XFfak+/rjHpyFh7juiT52r177bfFKhnlafm0UOSldhXjj32b+LZIOdswGTg== -"@next/swc-win32-x64-msvc@14.2.29": - version "14.2.29" - resolved "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.29.tgz#9097b85893a51ca9ba3b9d1733a4aab954edeab5" - integrity sha512-iPPwUEKnVs7pwR0EBLJlwxLD7TTHWS/AoVZx1l9ZQzfQciqaFEr5AlYzA2uB6Fyby1IF18t4PL0nTpB+k4Tzlw== +"@next/swc-win32-x64-msvc@14.2.30": + version "14.2.30" + resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.30.tgz#94d3ddcc1e97572a0514a6180c8e3bb415e1dc98" + integrity sha512-4KCo8hMZXMjpTzs3HOqOGYYwAXymXIy7PEPAXNEcEOyKqkjiDlECumrWziy+JEF0Oi4ILHGxzgQ3YiMGG2t/Lg== "@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents.3": version "2.1.8-no-fsevents.3" @@ -8101,12 +8101,12 @@ next-themes@^0.2.1: resolved "https://registry.npmjs.org/next-themes/-/next-themes-0.2.1.tgz#0c9f128e847979daf6c67f70b38e6b6567856e45" integrity sha512-B+AKNfYNIzh0vqQQKqQItTS8evEouKD7H5Hj3kmuPERwddR2TxvDSFZuTj6T7Jfn1oyeUyJMydPl1Bkxkh0W7A== -next@^14.2.29: - version "14.2.29" - resolved "https://registry.npmjs.org/next/-/next-14.2.29.tgz#f67610f8368ef863065b3b791e23b9198f0df615" - integrity sha512-s98mCOMOWLGGpGOfgKSnleXLuegvvH415qtRZXpSp00HeEgdmrxmwL9cgKU+h4XrhB16zEI5d/7BnkS3ATInsA== +next@14.2.30: + version "14.2.30" + resolved "https://registry.yarnpkg.com/next/-/next-14.2.30.tgz#7b7288859794574067f65d6e2ea98822f2173006" + integrity sha512-+COdu6HQrHHFQ1S/8BBsCag61jZacmvbuL2avHvQFbWa2Ox7bE+d8FyNgxRLjXQ5wtPyQwEmk85js/AuaG2Sbg== dependencies: - "@next/env" "14.2.29" + "@next/env" "14.2.30" "@swc/helpers" "0.5.5" busboy "1.6.0" caniuse-lite "^1.0.30001579" @@ -8114,15 +8114,15 @@ next@^14.2.29: postcss "8.4.31" styled-jsx "5.1.1" optionalDependencies: - "@next/swc-darwin-arm64" "14.2.29" - "@next/swc-darwin-x64" "14.2.29" - "@next/swc-linux-arm64-gnu" "14.2.29" - "@next/swc-linux-arm64-musl" "14.2.29" - "@next/swc-linux-x64-gnu" "14.2.29" - "@next/swc-linux-x64-musl" "14.2.29" - "@next/swc-win32-arm64-msvc" "14.2.29" - "@next/swc-win32-ia32-msvc" "14.2.29" - "@next/swc-win32-x64-msvc" "14.2.29" + "@next/swc-darwin-arm64" "14.2.30" + "@next/swc-darwin-x64" "14.2.30" + "@next/swc-linux-arm64-gnu" "14.2.30" + "@next/swc-linux-arm64-musl" "14.2.30" + "@next/swc-linux-x64-gnu" "14.2.30" + "@next/swc-linux-x64-musl" "14.2.30" + "@next/swc-win32-arm64-msvc" "14.2.30" + "@next/swc-win32-ia32-msvc" "14.2.30" + "@next/swc-win32-x64-msvc" "14.2.30" no-case@^3.0.4: version "3.0.4" From dffcc6dc10fe03ea751e6a21a3dd63bc8699fb8e Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Mon, 16 Jun 2025 17:10:08 +0530 Subject: [PATCH 164/201] chore(deps): brace-expansion upgraded to 2.0.2 --- package.json | 1 + yarn.lock | 21 ++++----------------- 2 files changed, 5 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index e8a0b22d7..d1711c577 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "turbo": "^2.5.4" }, "resolutions": { + "brace-expansion": "2.0.2", "nanoid": "3.3.8", "esbuild": "0.25.0", "@babel/helpers": "7.26.10", diff --git a/yarn.lock b/yarn.lock index 6b678566d..e8fcdc11c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4268,18 +4268,10 @@ boolbase@^1.0.0: resolved "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww== -brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - -brace-expansion@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" - integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== +brace-expansion@2.0.2, brace-expansion@^1.1.7, brace-expansion@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.2.tgz#54fc53237a613d854c7bd37463aad17df87214e7" + integrity sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ== dependencies: balanced-match "^1.0.0" @@ -4690,11 +4682,6 @@ compute-scroll-into-view@^3.0.2: resolved "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz#02c3386ec531fb6a9881967388e53e8564f3e9aa" integrity sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw== -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== - concurrently@^9.0.1: version "9.1.2" resolved "https://registry.npmjs.org/concurrently/-/concurrently-9.1.2.tgz#22d9109296961eaee773e12bfb1ce9a66bc9836c" From 2014400bede47a358947bcf9fdd2a280fff2adc2 Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Mon, 16 Jun 2025 17:18:41 +0530 Subject: [PATCH 165/201] refactor: move web utils to packages (#7145) * refactor: move web utils to packages * fix: build and lint errors * chore: update drag handle plugin * chore: update table cell type to fix build errors * fix: build errors * chore: sync few changes * fix: build errors * chore: minor fixes related to duplicate assets imports * fix: build errors * chore: minor changes --- .../components/authentication/auth-banner.tsx | 6 +- admin/core/components/login/sign-in-form.tsx | 4 +- admin/core/lib/auth-helpers.tsx | 4 +- packages/constants/src/auth.ts | 12 +- packages/constants/src/endpoints.ts | 16 +- .../constants/src}/estimates.ts | 2 +- .../{ui/src/emoji => constants/src}/icons.ts | 0 packages/constants/src/index.ts | 4 +- .../constants/src/{inbox.ts => intake.ts} | 29 ++ packages/constants/src/state.ts | 1 + packages/editor/src/core/helpers/parser.ts | 89 ++++ packages/editor/src/core/helpers/yjs-utils.ts | 48 ++ .../editor/src/core/plugins/drag-handle.ts | 5 +- packages/propel/src/table/core.tsx | 168 +++--- packages/propel/tsconfig.json | 3 +- packages/services/src/file/file.service.ts | 16 + packages/types/src/calendar.d.ts | 25 + packages/types/src/cycle/cycle.d.ts | 13 + packages/types/src/enums.ts | 12 +- packages/types/src/file.d.ts | 17 +- packages/types/src/index.d.ts | 1 + .../types/src/layout/gantt.d.ts | 0 packages/types/src/layout/index.ts | 1 + packages/types/src/project/projects.d.ts | 4 + packages/ui/src/emoji/icons-list.tsx | 10 +- packages/ui/src/emoji/index.ts | 1 - packages/ui/src/emoji/logo.tsx | 8 +- packages/ui/src/emoji/lucide-icons-list.tsx | 18 +- packages/utils/src/auth.ts | 74 +-- .../utils/src/calendar.ts | 54 +- packages/utils/src/color.ts | 169 ++++++- packages/utils/src/common.ts | 2 + .../utils/src/cycle.ts | 69 ++- packages/utils/src/datetime.ts | 436 ++++++++++------ .../utils/src/distribution-update.ts | 5 +- .../utils/src/editor.ts | 4 +- packages/utils/src/emoji.ts | 47 +- .../utils/src}/estimates.ts | 2 +- packages/utils/src/file.ts | 64 +++ .../utils/src/filter.ts | 7 +- packages/utils/src/index.ts | 19 +- .../utils/src/intake.ts | 47 +- packages/utils/src/issue.ts | 37 -- packages/utils/src/math.ts | 2 + .../utils/src/module.ts | 7 +- .../utils/src/notification.ts | 2 +- .../utils/src/page.ts | 8 +- packages/utils/src/permission/index.ts | 1 + .../src/{permission.ts => permission/role.ts} | 14 +- .../utils/src/project-views.ts | 10 +- .../utils/src/project.ts | 12 +- .../utils/src/router.ts | 0 packages/utils/src/state.ts | 13 - packages/utils/src/string.ts | 158 +++--- .../utils/src/tab-indices.ts | 1 + packages/utils/src/theme.ts | 122 +++++ .../utils/src/work-item/base.ts | 33 +- packages/utils/src/work-item/index.ts | 2 + packages/utils/src/work-item/modal.ts | 15 + .../utils/src/work-item/state.ts | 1 - .../components/account/auth-forms/email.tsx | 2 +- .../helpers/password-strength-meter.tsx | 2 +- space/core/components/common/project-logo.tsx | 2 +- .../editor/embeds/mentions/user.tsx | 2 +- .../components/editor/lite-text-editor.tsx | 2 +- .../editor/lite-text-read-only-editor.tsx | 2 +- .../editor/rich-text-read-only-editor.tsx | 2 +- space/core/components/editor/toolbar.tsx | 6 +- .../issue-layouts/kanban/kanban-group.tsx | 3 +- .../issue-layouts/properties/due-date.tsx | 2 +- .../components/issues/navbar/user-avatar.tsx | 2 +- .../comment/comment-detail-card.tsx | 2 +- .../comment/comment-reactions.tsx | 3 +- .../issues/peek-overview/issue-properties.tsx | 5 +- .../issues/reactions/issue-vote-reactions.tsx | 3 +- space/core/lib/toast-provider.tsx | 7 +- space/helpers/editor.helper.ts | 5 +- .../(projects)/analytics/header.tsx | 8 +- .../(projects)/extended-project-sidebar.tsx | 3 +- .../(projects)/profile/[userId]/header.tsx | 5 +- .../profile/[userId]/mobile-header.tsx | 2 +- .../cycles/(detail)/[cycleId]/page.tsx | 2 +- .../[projectId]/cycles/(detail)/header.tsx | 23 +- .../cycles/(detail)/mobile-header.tsx | 7 +- .../[projectId]/cycles/(list)/page.tsx | 4 +- .../[projectId]/draft-issues/header.tsx | 2 +- .../(detail)/[projectId]/intake/page.tsx | 5 +- .../issues/(list)/mobile-header.tsx | 7 +- .../modules/(detail)/[moduleId]/page.tsx | 2 +- .../[projectId]/modules/(detail)/header.tsx | 7 +- .../modules/(detail)/mobile-header.tsx | 7 +- .../[projectId]/modules/(list)/page.tsx | 4 +- .../[projectId]/pages/(detail)/header.tsx | 2 +- .../views/(detail)/[viewId]/header.tsx | 2 +- .../[projectId]/views/(list)/page.tsx | 4 +- .../[workspaceSlug]/(projects)/sidebar.tsx | 2 +- .../(projects)/workspace-views/header.tsx | 2 +- .../settings/(workspace)/exports/page.tsx | 5 +- .../settings/(workspace)/members/page.tsx | 3 +- .../settings/account/preferences/page.tsx | 4 +- .../settings/account/security/page.tsx | 5 +- .../(settings)/settings/account/sidebar.tsx | 5 +- .../(all)/accounts/forgot-password/page.tsx | 3 +- .../(all)/accounts/reset-password/page.tsx | 4 +- web/app/(all)/accounts/set-password/page.tsx | 5 +- web/app/(all)/invitations/page.tsx | 3 +- web/app/(all)/profile/appearance/page.tsx | 2 +- web/app/(all)/profile/security/page.tsx | 7 +- web/app/(all)/profile/sidebar.tsx | 3 +- web/app/error.tsx | 9 +- web/app/layout.tsx | 2 +- web/app/provider.tsx | 2 +- .../workspace-active-cycles-upgrade.tsx | 2 +- web/ce/components/command-palette/helpers.tsx | 2 +- web/ce/components/comments/comment-block.tsx | 4 +- .../cycles/analytics-sidebar/base.tsx | 2 +- .../blockDraggables/left-draggable.tsx | 2 +- .../blockDraggables/right-draggable.tsx | 2 +- .../global/product-updates-header.tsx | 2 +- web/ce/components/issues/header.tsx | 3 +- .../issues/issue-details/issue-identifier.tsx | 2 +- .../pages/editor/ai/ask-pi-menu.tsx | 2 +- web/ce/components/pages/editor/ai/menu.tsx | 2 +- .../editor/embed/issue-embed-upgrade-card.tsx | 2 +- .../components/preferences/theme-switcher.tsx | 4 +- .../components/projects/create/attributes.tsx | 2 +- web/ce/components/projects/mobile-header.tsx | 2 +- web/ce/components/workspace/edition-badge.tsx | 2 +- .../workspace/sidebar/app-search.tsx | 2 +- web/ce/components/workspace/upgrade-badge.tsx | 2 +- web/ce/constants/index.ts | 1 - web/ce/services/project/estimate.service.ts | 2 +- web/ce/services/project/view.service.ts | 3 +- web/ce/services/workspace.service.ts | 3 +- web/ce/store/timeline/base-timeline.store.ts | 4 +- .../components/account/auth-forms/email.tsx | 4 +- .../account/auth-forms/password.tsx | 5 +- .../account/auth-forms/unique-code.tsx | 3 +- .../account/oauth/github-button.tsx | 2 +- .../account/oauth/gitlab-button.tsx | 2 +- .../account/oauth/google-button.tsx | 2 +- .../account/password-strength-meter.tsx | 13 +- .../analytics/select/select-y-axis.tsx | 4 +- .../components/analytics/total-insights.tsx | 8 +- .../api-token/modal/create-token-modal.tsx | 3 +- web/core/components/api-token/modal/form.tsx | 4 +- .../modal/generated-token-details.tsx | 3 +- .../components/api-token/token-list-item.tsx | 2 +- .../automation/auto-archive-automation.tsx | 2 +- web/core/components/chart/utils.ts | 4 +- .../actions/issue-actions/actions-list.tsx | 2 +- .../actions/issue-actions/change-assignee.tsx | 2 +- .../command-palette/command-modal.tsx | 3 +- .../command-palette/command-palette.tsx | 2 +- .../shortcuts-modal/commands-list.tsx | 2 +- web/core/components/comments/comment-card.tsx | 2 +- .../components/comments/comment-create.tsx | 3 +- .../components/comments/comment-reaction.tsx | 2 +- .../common/activity/activity-block.tsx | 2 +- .../common/applied-filters/date.tsx | 4 +- .../common/applied-filters/members.tsx | 2 +- web/core/components/common/count-chip.tsx | 2 +- .../components/common/filters/created-at.tsx | 2 +- .../components/common/filters/created-by.tsx | 2 +- web/core/components/common/logo.tsx | 8 +- web/core/components/common/pro-icon.tsx | 2 +- web/core/components/common/switcher-label.tsx | 3 +- web/core/components/core/activity.tsx | 4 +- web/core/components/core/content-wrapper.tsx | 2 +- .../core/filters/date-filter-modal.tsx | 2 +- .../components/core/image-picker-popover.tsx | 2 +- web/core/components/core/list/list-item.tsx | 2 +- .../modals/existing-issues-list-modal.tsx | 3 +- .../core/modals/user-image-upload-modal.tsx | 3 +- .../modals/workspace-image-upload-modal.tsx | 3 +- .../multiple-select/entity-select-action.tsx | 2 +- .../multiple-select/group-select-action.tsx | 2 +- .../components/core/render-if-visible-HOC.tsx | 2 +- .../core/sidebar/progress-chart.tsx | 3 +- .../cycles/active-cycle/cycle-stats.tsx | 4 +- .../analytics-sidebar/issue-progress.tsx | 2 +- .../analytics-sidebar/progress-stats.tsx | 3 +- .../analytics-sidebar/sidebar-details.tsx | 4 +- .../analytics-sidebar/sidebar-header.tsx | 15 +- .../cycles/applied-filters/date.tsx | 3 +- .../cycles/applied-filters/root.tsx | 2 +- .../cycles/applied-filters/status.tsx | 2 +- .../cycles/archived-cycles/header.tsx | 3 +- .../cycles/archived-cycles/root.tsx | 2 +- .../components/cycles/cycle-peek-overview.tsx | 2 +- .../components/cycles/cycles-view-header.tsx | 3 +- .../cycles/dropdowns/filters/end-date.tsx | 2 +- .../cycles/dropdowns/filters/start-date.tsx | 2 +- web/core/components/cycles/form.tsx | 5 +- .../cycles/list/cycle-list-group-header.tsx | 2 +- .../cycles/list/cycle-list-item-action.tsx | 4 +- .../list/cycle-list-project-group-header.tsx | 2 +- .../cycles/list/cycles-list-item.tsx | 2 +- web/core/components/cycles/quick-actions.tsx | 3 +- web/core/components/dropdowns/buttons.tsx | 2 +- web/core/components/dropdowns/cycle/index.tsx | 2 +- web/core/components/dropdowns/date-range.tsx | 3 +- web/core/components/dropdowns/date.tsx | 3 +- web/core/components/dropdowns/estimate.tsx | 3 +- .../components/dropdowns/member/avatar.tsx | 3 +- .../components/dropdowns/member/index.tsx | 2 +- .../dropdowns/member/member-options.tsx | 3 +- web/core/components/dropdowns/merged-date.tsx | 3 +- .../components/dropdowns/module/index.tsx | 2 +- .../dropdowns/module/module-options.tsx | 2 +- web/core/components/dropdowns/priority.tsx | 2 +- web/core/components/dropdowns/project.tsx | 2 +- web/core/components/dropdowns/state.tsx | 2 +- .../editor/embeds/mentions/user.tsx | 3 +- .../lite-text-editor/lite-text-editor.tsx | 3 +- .../lite-text-read-only-editor.tsx | 2 +- .../editor/lite-text-editor/toolbar.tsx | 2 +- .../rich-text-editor/rich-text-editor.tsx | 2 +- .../rich-text-read-only-editor.tsx | 4 +- .../editor/sticky-editor/editor.tsx | 2 +- .../editor/sticky-editor/toolbar.tsx | 2 +- .../components/estimates/create/modal.tsx | 2 +- .../components/estimates/create/stage-one.tsx | 2 +- .../estimates/estimate-list-item.tsx | 5 +- .../estimates/points/create-root.tsx | 2 +- .../components/estimates/points/create.tsx | 5 +- .../components/estimates/points/preview.tsx | 2 +- .../components/estimates/points/update.tsx | 5 +- .../components/estimates/radio-select.tsx | 2 +- .../components/exporter/single-export.tsx | 2 +- .../gantt-chart/blocks/block-row-list.tsx | 2 +- .../gantt-chart/blocks/block-row.tsx | 4 +- .../components/gantt-chart/blocks/block.tsx | 4 +- .../gantt-chart/blocks/blocks-list.tsx | 2 +- .../components/gantt-chart/chart/header.tsx | 4 +- .../gantt-chart/chart/main-content.tsx | 9 +- .../components/gantt-chart/chart/root.tsx | 4 +- .../gantt-chart/chart/views/month.tsx | 2 +- web/core/components/gantt-chart/data/index.ts | 2 +- .../gantt-chart/helpers/add-block.tsx | 4 +- .../blockResizables/left-resizable.tsx | 4 +- .../blockResizables/right-resizable.tsx | 4 +- .../blockResizables/use-gantt-resizable.ts | 2 +- .../gantt-chart/helpers/draggable.tsx | 4 +- web/core/components/gantt-chart/index.ts | 1 - web/core/components/gantt-chart/root.tsx | 3 +- .../gantt-chart/sidebar/issues/block.tsx | 4 +- .../gantt-chart/sidebar/issues/sidebar.tsx | 2 +- .../gantt-chart/sidebar/modules/block.tsx | 2 +- .../gantt-chart/sidebar/modules/sidebar.tsx | 2 +- .../components/gantt-chart/sidebar/root.tsx | 4 +- .../components/gantt-chart/sidebar/utils.ts | 2 +- .../components/gantt-chart/views/helpers.ts | 4 +- .../gantt-chart/views/month-view.ts | 2 +- .../gantt-chart/views/quarter-view.ts | 2 +- .../components/gantt-chart/views/week-view.ts | 2 +- .../global/product-updates/footer.tsx | 2 +- web/core/components/home/root.tsx | 2 +- .../home/widgets/empty-states/no-projects.tsx | 3 +- .../manage/widget-item-drag-handle.tsx | 2 +- .../components/home/widgets/recents/issue.tsx | 3 +- .../components/home/widgets/recents/page.tsx | 5 +- .../home/widgets/recents/project.tsx | 2 +- .../inbox/content/inbox-issue-header.tsx | 8 +- .../content/inbox-issue-mobile-header.tsx | 4 +- .../inbox/content/issue-properties.tsx | 3 +- .../components/inbox/content/issue-root.tsx | 2 +- .../inbox-filter/applied-filters/date.tsx | 4 +- .../inbox-filter/applied-filters/member.tsx | 2 +- .../inbox/inbox-filter/filters/date.tsx | 2 +- .../inbox/inbox-filter/filters/members.tsx | 2 +- .../inbox/inbox-filter/sorting/order-by.tsx | 2 +- .../components/inbox/inbox-issue-status.tsx | 3 +- .../inbox/modals/create-modal/create-root.tsx | 3 +- .../modals/create-modal/issue-description.tsx | 3 +- .../modals/create-modal/issue-properties.tsx | 3 +- .../inbox/modals/create-modal/issue-title.tsx | 2 +- web/core/components/inbox/root.tsx | 4 +- .../inbox/sidebar/inbox-list-item.tsx | 4 +- web/core/components/inbox/sidebar/root.tsx | 5 +- .../components/instance/not-ready-view.tsx | 2 +- .../integration/github/import-data.tsx | 2 +- .../integration/github/select-repository.tsx | 2 +- .../integration/github/single-user-select.tsx | 2 +- .../integration/jira/give-details.tsx | 2 +- .../integration/jira/import-users.tsx | 2 +- .../components/integration/single-import.tsx | 2 +- .../issues/archived-issues-header.tsx | 2 +- .../issues/attachment/attachment-detail.tsx | 6 +- .../attachment/attachment-list-item.tsx | 5 +- .../attachment-list-upload-item.tsx | 2 +- .../attachment/attachment-upload-details.tsx | 5 +- .../attachment/delete-attachment-modal.tsx | 2 +- .../issues/bulk-operations/upgrade-banner.tsx | 2 +- .../create-issue-toast-action-items.tsx | 3 +- .../components/issues/description-input.tsx | 2 +- web/core/components/issues/filters.tsx | 2 +- .../sub-issues/issues-list/list-item.tsx | 3 +- .../sub-issues/issues-list/properties.tsx | 5 +- .../issue-detail-widgets/widget-button.tsx | 2 +- .../issues/issue-detail/cycle-select.tsx | 2 +- .../issue-activity/activity-filter.tsx | 2 +- .../actions/helpers/activity-block.tsx | 2 +- .../activity/actions/helpers/issue-link.tsx | 2 +- .../activity/actions/start_date.tsx | 2 +- .../activity/actions/target_date.tsx | 2 +- .../issue-activity/activity/activity-list.tsx | 2 +- .../issue-detail/issue-activity/helper.tsx | 2 +- .../issue-detail-quick-actions.tsx | 4 +- .../label/select/label-select.tsx | 2 +- .../issues/issue-detail/links/link-detail.tsx | 5 +- .../issues/issue-detail/links/link-item.tsx | 3 +- .../issues/issue-detail/main-content.tsx | 2 +- .../issues/issue-detail/module-select.tsx | 2 +- .../issues/issue-detail/parent-select.tsx | 2 +- .../issues/issue-detail/parent/root.tsx | 2 +- .../issue-detail/parent/sibling-item.tsx | 2 +- .../issue-detail/reactions/issue-comment.tsx | 3 +- .../issues/issue-detail/reactions/issue.tsx | 3 +- .../issues/issue-detail/relation-select.tsx | 3 +- .../issues/issue-detail/sidebar.tsx | 4 +- .../issue-layouts/calendar/calendar.tsx | 5 +- .../issue-layouts/calendar/day-tile.tsx | 7 +- .../calendar/dropdowns/months-dropdown.tsx | 2 +- .../issues/issue-layouts/calendar/index.ts | 1 - .../issue-layouts/calendar/issue-block.tsx | 3 +- .../issue-layouts/calendar/issue-blocks.tsx | 2 +- .../calendar/quick-add-issue-actions.tsx | 2 +- .../issues/issue-layouts/calendar/types.d.ts | 24 - .../issue-layouts/calendar/week-days.tsx | 9 +- .../issue-layouts/calendar/week-header.tsx | 2 +- .../filters/applied-filters/date.tsx | 3 +- .../filters/applied-filters/filters-list.tsx | 2 +- .../filters/applied-filters/members.tsx | 2 +- .../roots/global-view-root.tsx | 2 +- .../filters/applied-filters/state.tsx | 2 +- .../filters/header/filters/assignee.tsx | 2 +- .../filters/header/filters/created-by.tsx | 2 +- .../filters/header/filters/mentions.tsx | 2 +- .../issue-layouts/gantt/base-gantt-root.tsx | 7 +- .../issues/issue-layouts/gantt/blocks.tsx | 3 +- .../issue-layouts/group-drag-overlay.tsx | 2 +- .../issues/issue-layouts/kanban/block.tsx | 3 +- .../issue-layouts/kanban/kanban-group.tsx | 2 +- .../issues/issue-layouts/list/block.tsx | 3 +- .../list/headers/group-by-card.tsx | 2 +- .../properties/all-properties.tsx | 4 +- .../quick-action-dropdowns/all-issue.tsx | 3 +- .../quick-action-dropdowns/archived-issue.tsx | 2 +- .../quick-action-dropdowns/cycle-issue.tsx | 3 +- .../quick-action-dropdowns/draft-issue.tsx | 6 +- .../quick-action-dropdowns/helper.tsx | 4 +- .../quick-action-dropdowns/module-issue.tsx | 3 +- .../quick-action-dropdowns/project-issue.tsx | 3 +- .../issue-layouts/quick-add/form/gantt.tsx | 2 +- .../issues/issue-layouts/quick-add/root.tsx | 3 +- .../spreadsheet/columns/created-on-column.tsx | 2 +- .../spreadsheet/columns/due-date-column.tsx | 4 +- .../spreadsheet/columns/start-date-column.tsx | 2 +- .../spreadsheet/columns/sub-issue-column.tsx | 2 +- .../spreadsheet/columns/updated-on-column.tsx | 2 +- .../issue-layouts/spreadsheet/issue-row.tsx | 3 +- .../spreadsheet/spreadsheet-header.tsx | 2 +- .../components/issues/issue-layouts/utils.tsx | 3 +- .../components/default-properties.tsx | 3 +- .../components/description-editor.tsx | 3 +- .../issue-modal/components/parent-tag.tsx | 2 +- .../issue-modal/components/project-select.tsx | 3 +- .../issue-modal/components/title-input.tsx | 2 +- .../context/issue-modal-context.tsx | 2 +- .../issues/issue-modal/draft-issue-layout.tsx | 2 +- .../components/issues/issue-modal/form.tsx | 6 +- .../issues/parent-issues-list-modal.tsx | 3 +- .../issues/peek-overview/header.tsx | 4 +- .../issues/peek-overview/issue-detail.tsx | 2 +- .../issues/peek-overview/properties.tsx | 4 +- .../components/issues/peek-overview/view.tsx | 2 +- .../issues/relations/issue-list-item.tsx | 2 +- web/core/components/issues/select/label.tsx | 2 +- web/core/components/issues/title-input.tsx | 2 +- .../workspace-draft/draft-issue-block.tsx | 2 +- .../draft-issue-properties.tsx | 3 +- .../issues/workspace-draft/quick-action.tsx | 2 +- .../issues/workspace-draft/root.tsx | 2 +- .../components/labels/create-label-modal.tsx | 2 +- .../labels/label-block/label-item-block.tsx | 2 +- .../license/modal/card/free-plan.tsx | 2 +- .../analytics-sidebar/issue-progress.tsx | 10 +- .../analytics-sidebar/progress-stats.tsx | 3 +- .../modules/analytics-sidebar/root.tsx | 16 +- .../modules/applied-filters/date.tsx | 3 +- .../modules/applied-filters/members.tsx | 2 +- .../modules/applied-filters/root.tsx | 2 +- .../modules/archived-modules/header.tsx | 3 +- .../modules/archived-modules/root.tsx | 2 +- .../modules/dropdowns/filters/lead.tsx | 2 +- .../modules/dropdowns/filters/members.tsx | 2 +- .../modules/dropdowns/filters/start-date.tsx | 2 +- .../modules/dropdowns/filters/target-date.tsx | 2 +- .../components/modules/dropdowns/order-by.tsx | 2 +- web/core/components/modules/form.tsx | 4 +- .../gantt-chart/modules-list-layout.tsx | 6 +- .../components/modules/links/list-item.tsx | 5 +- .../components/modules/module-card-item.tsx | 3 +- .../modules/module-list-item-action.tsx | 2 +- .../components/modules/module-list-item.tsx | 2 +- .../modules/module-peek-overview.tsx | 2 +- .../components/modules/module-view-header.tsx | 3 +- web/core/components/modules/quick-actions.tsx | 3 +- .../components/onboarding/invitations.tsx | 3 +- .../components/onboarding/invite-members.tsx | 2 +- .../components/onboarding/profile-setup.tsx | 5 +- .../components/onboarding/step-indicator.tsx | 2 +- .../onboarding/switch-account-dropdown.tsx | 3 +- .../components/pages/dropdowns/actions.tsx | 2 +- .../components/pages/editor/editor-body.tsx | 9 +- web/core/components/pages/editor/title.tsx | 3 +- .../pages/editor/toolbar/color-dropdown.tsx | 2 +- .../pages/editor/toolbar/info-popover.tsx | 6 +- .../pages/editor/toolbar/options-dropdown.tsx | 2 +- .../components/pages/editor/toolbar/root.tsx | 2 +- .../pages/editor/toolbar/toolbar.tsx | 2 +- web/core/components/pages/header/root.tsx | 2 +- .../pages/list/applied-filters/root.tsx | 2 +- .../pages/list/block-item-action.tsx | 3 +- web/core/components/pages/list/block.tsx | 2 +- web/core/components/pages/list/order-by.tsx | 2 +- .../components/pages/list/search-input.tsx | 2 +- .../components/pages/list/tab-navigation.tsx | 2 +- .../components/pages/modals/page-form.tsx | 3 +- .../components/pages/version/main-content.tsx | 2 +- web/core/components/pages/version/root.tsx | 2 +- .../pages/version/sidebar-list-item.tsx | 4 +- .../components/pages/version/sidebar-list.tsx | 2 +- .../profile/activity/activity-list.tsx | 3 +- .../profile/activity/download-button.tsx | 2 +- .../activity/profile-activity-list.tsx | 3 +- web/core/components/profile/form.tsx | 3 +- .../components/profile/overview/activity.tsx | 3 +- .../overview/priority-distribution.tsx | 2 +- .../profile/overview/state-distribution.tsx | 2 +- .../profile/profile-issues-filter.tsx | 2 +- .../profile-setting-content-wrapper.tsx | 2 +- web/core/components/profile/sidebar.tsx | 4 +- .../components/project-states/state-item.tsx | 4 +- .../project/applied-filters/date.tsx | 3 +- .../project/applied-filters/members.tsx | 2 +- .../project/applied-filters/root.tsx | 2 +- web/core/components/project/card.tsx | 12 +- .../project/create-project-modal.tsx | 5 +- .../project/create/common-attributes.tsx | 4 +- web/core/components/project/create/header.tsx | 4 +- .../project/create/project-create-buttons.tsx | 2 +- .../project/dropdowns/filters/created-at.tsx | 2 +- .../project/dropdowns/filters/lead.tsx | 2 +- .../project/dropdowns/filters/members.tsx | 2 +- .../components/project/dropdowns/order-by.tsx | 2 +- web/core/components/project/filters.tsx | 3 +- web/core/components/project/form.tsx | 4 +- web/core/components/project/member-select.tsx | 2 +- .../components/project/multi-select-modal.tsx | 2 +- .../project/publish-project/modal.tsx | 4 +- web/core/components/project/root.tsx | 4 +- .../components/project/search-projects.tsx | 2 +- .../project/send-project-invitation-modal.tsx | 2 +- .../project/sidebar/nav-item-children.tsx | 2 +- .../settings/project/sidebar/root.tsx | 7 +- .../components/settings/sidebar/header.tsx | 6 +- .../components/settings/sidebar/nav-item.tsx | 4 +- .../components/sidebar/sidebar-navigation.tsx | 2 +- web/core/components/stickies/modal/search.tsx | 2 +- .../sticky/sticky-item-drag-handle.tsx | 2 +- .../components/views/applied-filters/root.tsx | 2 +- web/core/components/views/form.tsx | 4 +- web/core/components/views/quick-actions.tsx | 3 +- .../components/views/view-list-header.tsx | 2 +- .../views/view-list-item-action.tsx | 3 +- .../web-hooks/create-webhook-modal.tsx | 2 +- .../components/web-hooks/form/secret-key.tsx | 3 +- web/core/components/web-hooks/utils.ts | 2 +- .../notification-app-sidebar-option.tsx | 6 +- .../workspace-notifications/root.tsx | 2 +- .../sidebar/filters/menu/menu-option-item.tsx | 2 +- .../header/options/menu-option/menu-item.tsx | 2 +- .../sidebar/notification-card/content.tsx | 6 +- .../sidebar/notification-card/item.tsx | 4 +- .../notification-card/options/button.tsx | 2 +- .../notification-card/options/root.tsx | 2 +- .../options/snooze/modal.tsx | 2 +- .../notification-card/options/snooze/root.tsx | 2 +- .../workspace-notifications/sidebar/root.tsx | 4 +- web/core/components/workspace/logo.tsx | 5 +- .../workspace/settings/member-columns.tsx | 2 +- .../workspace/settings/workspace-details.tsx | 3 +- .../workspace/sidebar/dropdown-item.tsx | 3 +- .../components/workspace/sidebar/dropdown.tsx | 5 +- .../sidebar/favorites/favorite-folder.tsx | 2 +- .../common/favorite-item-drag-handle.tsx | 2 +- .../common/favorite-item-quick-action.tsx | 2 +- .../common/favorite-item-wrapper.tsx | 2 +- .../sidebar/favorites/favorites-menu.tsx | 2 +- .../workspace/sidebar/help-section.tsx | 2 +- .../workspace/sidebar/projects-list-item.tsx | 4 +- .../workspace/sidebar/projects-list.tsx | 4 +- .../workspace/sidebar/quick-actions.tsx | 2 +- .../workspace/sidebar/user-menu.tsx | 2 +- .../workspace/sidebar/workspace-menu.tsx | 2 +- .../views/default-view-list-item.tsx | 2 +- .../views/default-view-quick-action.tsx | 4 +- web/core/components/workspace/views/form.tsx | 2 +- .../workspace/views/quick-action.tsx | 4 +- .../workspace/views/view-list-item.tsx | 3 +- web/core/constants/editor.ts | 6 +- web/core/hooks/editor/use-editor-config.ts | 2 +- web/core/hooks/editor/use-editor-mention.tsx | 2 +- web/core/hooks/use-favorite-item-details.tsx | 2 +- .../use-issue-peek-overview-redirection.tsx | 2 +- web/core/hooks/use-parse-editor-content.ts | 2 +- web/core/lib/posthog-provider.tsx | 2 +- web/core/lib/wrappers/store-wrapper.tsx | 2 +- web/core/local-db/utils/load-workspace.ts | 2 +- web/core/services/ai.service.ts | 2 +- web/core/services/analytics.service.ts | 2 + web/core/services/api_token.service.ts | 2 +- web/core/services/app_config.service.ts | 2 +- web/core/services/app_installation.service.ts | 2 +- web/core/services/auth.service.ts | 2 +- web/core/services/cycle.service.ts | 2 +- web/core/services/cycle_archive.service.ts | 2 +- web/core/services/dashboard.service.ts | 2 +- .../services/favorite/favorite.service.ts | 2 +- web/core/services/file.service.ts | 4 +- .../services/inbox/inbox-issue.service.ts | 3 +- .../inbox/intake-work_item_version.service.ts | 2 +- web/core/services/instance.service.ts | 2 +- .../services/integrations/github.service.ts | 2 +- .../integrations/integration.service.ts | 2 +- .../services/integrations/jira.service.ts | 2 +- web/core/services/issue/issue.service.ts | 5 +- .../services/issue/issue_activity.service.ts | 3 +- .../services/issue/issue_archive.service.ts | 3 +- .../issue/issue_attachment.service.ts | 5 +- .../services/issue/issue_comment.service.ts | 5 +- .../services/issue/issue_draft.service.ts | 2 +- .../services/issue/issue_label.service.ts | 2 +- .../services/issue/issue_reaction.service.ts | 3 +- .../services/issue/issue_relation.service.ts | 2 +- .../issue/work_item_version.service.ts | 3 +- .../services/issue/workspace_draft.service.ts | 2 +- web/core/services/issue_filter.service.ts | 2 +- web/core/services/module.service.ts | 2 +- web/core/services/module_archive.service.ts | 2 +- .../page/project-page-version.service.ts | 2 +- .../services/page/project-page.service.ts | 2 +- .../project/project-archive.service.ts | 2 +- .../project/project-export.service.ts | 2 +- .../project/project-member.service.ts | 2 +- .../project/project-publish.service.ts | 2 +- .../services/project/project-state.service.ts | 2 +- web/core/services/project/project.service.ts | 2 +- web/core/services/sticky.service.ts | 3 +- web/core/services/timezone.service.ts | 2 +- web/core/services/user.service.ts | 2 +- web/core/services/webhook.service.ts | 2 +- .../workspace-notification.service.ts | 2 +- web/core/store/cycle.store.ts | 4 +- web/core/store/inbox/inbox-issue.store.ts | 3 +- web/core/store/inbox/project-inbox.store.ts | 4 +- web/core/store/issue/archived/filter.store.ts | 2 +- web/core/store/issue/cycle/filter.store.ts | 2 +- web/core/store/issue/cycle/issue.store.ts | 2 +- web/core/store/issue/draft/filter.store.ts | 2 +- .../store/issue/helpers/base-issues-utils.ts | 2 +- .../store/issue/helpers/base-issues.store.ts | 5 +- .../helpers/issue-filter-helper.store.ts | 2 +- .../issue-details/comment_reaction.store.ts | 2 +- .../issue/issue-details/reaction.store.ts | 2 +- web/core/store/issue/issue.store.ts | 2 +- .../store/issue/issue_calendar_view.store.ts | 6 +- .../store/issue/issue_gantt_view.store.ts | 2 +- web/core/store/issue/module/filter.store.ts | 2 +- web/core/store/issue/module/issue.store.ts | 2 +- web/core/store/issue/profile/filter.store.ts | 2 +- .../store/issue/project-views/filter.store.ts | 2 +- web/core/store/issue/project/filter.store.ts | 2 +- .../issue/workspace-draft/filter.store.ts | 2 +- .../issue/workspace-draft/issue.store.ts | 2 +- .../store/issue/workspace/filter.store.ts | 2 +- web/core/store/label.store.ts | 2 +- web/core/store/module.store.ts | 3 +- .../workspace-notifications.store.ts | 2 +- web/core/store/pages/project-page.store.ts | 2 +- web/core/store/project-view.store.ts | 2 +- web/core/store/project/project.store.ts | 2 +- web/core/store/state.store.ts | 2 +- web/core/store/user/index.ts | 7 +- web/ee/constants/estimates.ts | 1 - web/helpers/array.helper.ts | 119 ----- web/helpers/attachment.helper.ts | 32 -- web/helpers/authentication.helper.tsx | 4 +- web/helpers/color.helper.ts | 144 ------ web/helpers/command-palette.ts | 1 - web/helpers/common.helper.ts | 44 -- web/helpers/dashboard.helper.ts | 4 +- web/helpers/date-time.helper.ts | 477 ------------------ web/helpers/download.helper.ts | 14 - web/helpers/emoji.helper.tsx | 66 +-- web/helpers/file.helper.ts | 96 ---- web/helpers/graph.helper.ts | 2 + web/helpers/issue-modal.helper.ts | 16 - web/helpers/password.helper.ts | 67 --- web/helpers/string.helper.ts | 236 --------- web/helpers/theme.helper.ts | 123 ----- web/helpers/user.helper.ts | 12 - 614 files changed, 1999 insertions(+), 3030 deletions(-) rename {web/ce/constants => packages/constants/src}/estimates.ts (99%) rename packages/{ui/src/emoji => constants/src}/icons.ts (100%) rename packages/constants/src/{inbox.ts => intake.ts} (81%) create mode 100644 packages/editor/src/core/helpers/parser.ts rename web/core/components/gantt-chart/types/index.ts => packages/types/src/layout/gantt.d.ts (100%) create mode 100644 packages/types/src/layout/index.ts rename web/helpers/calendar.helper.ts => packages/utils/src/calendar.ts (66%) rename web/helpers/cycle.helper.ts => packages/utils/src/cycle.ts (77%) rename web/helpers/distribution-update.helper.ts => packages/utils/src/distribution-update.ts (99%) rename web/helpers/editor.helper.ts => packages/utils/src/editor.ts (94%) rename {web/helpers => packages/utils/src}/estimates.ts (94%) rename web/helpers/filter.helper.ts => packages/utils/src/filter.ts (95%) rename web/helpers/inbox.helper.ts => packages/utils/src/intake.ts (57%) delete mode 100644 packages/utils/src/issue.ts create mode 100644 packages/utils/src/math.ts rename web/helpers/module.helper.ts => packages/utils/src/module.ts (96%) rename web/helpers/notification.helper.ts => packages/utils/src/notification.ts (82%) rename web/helpers/page.helper.ts => packages/utils/src/page.ts (95%) create mode 100644 packages/utils/src/permission/index.ts rename packages/utils/src/{permission.ts => permission/role.ts} (57%) rename web/helpers/project-views.helpers.ts => packages/utils/src/project-views.ts (94%) rename web/helpers/project.helper.ts => packages/utils/src/project.ts (93%) rename web/helpers/router.helper.ts => packages/utils/src/router.ts (100%) delete mode 100644 packages/utils/src/state.ts rename web/helpers/tab-indices.helper.ts => packages/utils/src/tab-indices.ts (95%) rename web/helpers/issue.helper.ts => packages/utils/src/work-item/base.ts (92%) rename web/helpers/state.helper.ts => packages/utils/src/work-item/state.ts (99%) delete mode 100644 web/core/components/issues/issue-layouts/calendar/types.d.ts delete mode 100644 web/ee/constants/estimates.ts delete mode 100644 web/helpers/array.helper.ts delete mode 100644 web/helpers/attachment.helper.ts delete mode 100644 web/helpers/color.helper.ts delete mode 100644 web/helpers/command-palette.ts delete mode 100644 web/helpers/common.helper.ts delete mode 100644 web/helpers/date-time.helper.ts delete mode 100644 web/helpers/download.helper.ts delete mode 100644 web/helpers/file.helper.ts delete mode 100644 web/helpers/issue-modal.helper.ts delete mode 100644 web/helpers/password.helper.ts delete mode 100644 web/helpers/string.helper.ts delete mode 100644 web/helpers/theme.helper.ts delete mode 100644 web/helpers/user.helper.ts diff --git a/admin/core/components/authentication/auth-banner.tsx b/admin/core/components/authentication/auth-banner.tsx index 7c1e5ea29..5d63808f1 100644 --- a/admin/core/components/authentication/auth-banner.tsx +++ b/admin/core/components/authentication/auth-banner.tsx @@ -1,11 +1,11 @@ import { FC } from "react"; import { Info, X } from "lucide-react"; // plane constants -import { TAuthErrorInfo } from "@plane/constants"; +import { TAdminAuthErrorInfo } from "@plane/constants"; type TAuthBanner = { - bannerData: TAuthErrorInfo | undefined; - handleBannerData?: (bannerData: TAuthErrorInfo | undefined) => void; + bannerData: TAdminAuthErrorInfo | undefined; + handleBannerData?: (bannerData: TAdminAuthErrorInfo | undefined) => void; }; export const AuthBanner: FC = (props) => { diff --git a/admin/core/components/login/sign-in-form.tsx b/admin/core/components/login/sign-in-form.tsx index 986e5cebe..553ffe6c5 100644 --- a/admin/core/components/login/sign-in-form.tsx +++ b/admin/core/components/login/sign-in-form.tsx @@ -4,7 +4,7 @@ import { FC, useEffect, useMemo, useState } from "react"; import { useSearchParams } from "next/navigation"; import { Eye, EyeOff } from "lucide-react"; // plane internal packages -import { API_BASE_URL, EAdminAuthErrorCodes, TAuthErrorInfo } from "@plane/constants"; +import { API_BASE_URL, EAdminAuthErrorCodes, TAdminAuthErrorInfo } from "@plane/constants"; import { AuthService } from "@plane/services"; import { Button, Input, Spinner } from "@plane/ui"; // components @@ -54,7 +54,7 @@ export const InstanceSignInForm: FC = (props) => { const [csrfToken, setCsrfToken] = useState(undefined); const [formData, setFormData] = useState(defaultFromData); const [isSubmitting, setIsSubmitting] = useState(false); - const [errorInfo, setErrorInfo] = useState(undefined); + const [errorInfo, setErrorInfo] = useState(undefined); const handleFormChange = (key: keyof TFormData, value: string | boolean) => setFormData((prev) => ({ ...prev, [key]: value })); diff --git a/admin/core/lib/auth-helpers.tsx b/admin/core/lib/auth-helpers.tsx index 582b56e29..f9882b5e5 100644 --- a/admin/core/lib/auth-helpers.tsx +++ b/admin/core/lib/auth-helpers.tsx @@ -3,7 +3,7 @@ import Image from "next/image"; import Link from "next/link"; import { KeyRound, Mails } from "lucide-react"; // plane packages -import { SUPPORT_EMAIL, EAdminAuthErrorCodes, TAuthErrorInfo } from "@plane/constants"; +import { SUPPORT_EMAIL, EAdminAuthErrorCodes, TAdminAuthErrorInfo } from "@plane/constants"; import { TGetBaseAuthenticationModeProps, TInstanceAuthenticationModes } from "@plane/types"; import { resolveGeneralTheme } from "@plane/utils"; // components @@ -89,7 +89,7 @@ const errorCodeMessages: { export const authErrorHandler = ( errorCode: EAdminAuthErrorCodes, email?: string | undefined -): TAuthErrorInfo | undefined => { +): TAdminAuthErrorInfo | undefined => { const bannerAlertErrorCodes = [ EAdminAuthErrorCodes.ADMIN_ALREADY_EXIST, EAdminAuthErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME, diff --git a/packages/constants/src/auth.ts b/packages/constants/src/auth.ts index bcdda31b4..01ed762ff 100644 --- a/packages/constants/src/auth.ts +++ b/packages/constants/src/auth.ts @@ -69,11 +69,12 @@ export enum EErrorAlertType { export type TAuthErrorInfo = { type: EErrorAlertType; - code: EAdminAuthErrorCodes; + code: EAuthErrorCodes; title: string; - message: any; + message: React.ReactNode; }; + export enum EAdminAuthErrorCodes { // Admin ADMIN_ALREADY_EXIST = "5150", @@ -87,6 +88,13 @@ export enum EAdminAuthErrorCodes { ADMIN_USER_DEACTIVATED = "5190", } +export type TAdminAuthErrorInfo = { + type: EErrorAlertType; + code: EAdminAuthErrorCodes; + title: string; + message: React.ReactNode; +}; + export enum EAuthErrorCodes { // Global INSTANCE_NOT_CONFIGURED = "5000", diff --git a/packages/constants/src/endpoints.ts b/packages/constants/src/endpoints.ts index cd1c08d7a..3f7a4eeee 100644 --- a/packages/constants/src/endpoints.ts +++ b/packages/constants/src/endpoints.ts @@ -1,28 +1,26 @@ export const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || ""; -export const API_BASE_PATH = process.env.NEXT_PUBLIC_API_BASE_PATH || "/"; +export const API_BASE_PATH = process.env.NEXT_PUBLIC_API_BASE_PATH || ""; export const API_URL = encodeURI(`${API_BASE_URL}${API_BASE_PATH}`); // God Mode Admin App Base Url export const ADMIN_BASE_URL = process.env.NEXT_PUBLIC_ADMIN_BASE_URL || ""; -export const ADMIN_BASE_PATH = process.env.NEXT_PUBLIC_ADMIN_BASE_PATH || "/"; +export const ADMIN_BASE_PATH = process.env.NEXT_PUBLIC_ADMIN_BASE_PATH || ""; export const GOD_MODE_URL = encodeURI(`${ADMIN_BASE_URL}${ADMIN_BASE_PATH}`); // Publish App Base Url export const SPACE_BASE_URL = process.env.NEXT_PUBLIC_SPACE_BASE_URL || ""; -export const SPACE_BASE_PATH = process.env.NEXT_PUBLIC_SPACE_BASE_PATH || "/"; +export const SPACE_BASE_PATH = process.env.NEXT_PUBLIC_SPACE_BASE_PATH || ""; export const SITES_URL = encodeURI(`${SPACE_BASE_URL}${SPACE_BASE_PATH}`); // Live App Base Url export const LIVE_BASE_URL = process.env.NEXT_PUBLIC_LIVE_BASE_URL || ""; -export const LIVE_BASE_PATH = process.env.NEXT_PUBLIC_LIVE_BASE_PATH || "/"; +export const LIVE_BASE_PATH = process.env.NEXT_PUBLIC_LIVE_BASE_PATH || ""; export const LIVE_URL = encodeURI(`${LIVE_BASE_URL}${LIVE_BASE_PATH}`); // Web App Base Url export const WEB_BASE_URL = process.env.NEXT_PUBLIC_WEB_BASE_URL || ""; -export const WEB_BASE_PATH = process.env.NEXT_PUBLIC_WEB_BASE_PATH || "/"; +export const WEB_BASE_PATH = process.env.NEXT_PUBLIC_WEB_BASE_PATH || ""; export const WEB_URL = encodeURI(`${WEB_BASE_URL}${WEB_BASE_PATH}`); // plane website url -export const WEBSITE_URL = - process.env.NEXT_PUBLIC_WEBSITE_URL || "https://plane.so"; +export const WEBSITE_URL = process.env.NEXT_PUBLIC_WEBSITE_URL || "https://plane.so"; // support email -export const SUPPORT_EMAIL = - process.env.NEXT_PUBLIC_SUPPORT_EMAIL || "support@plane.so"; +export const SUPPORT_EMAIL = process.env.NEXT_PUBLIC_SUPPORT_EMAIL || "support@plane.so"; // marketing links export const MARKETING_PRICING_PAGE_LINK = "https://plane.so/pricing"; export const MARKETING_CONTACT_US_PAGE_LINK = "https://plane.so/contact"; diff --git a/web/ce/constants/estimates.ts b/packages/constants/src/estimates.ts similarity index 99% rename from web/ce/constants/estimates.ts rename to packages/constants/src/estimates.ts index 2cba8ac83..34e04e562 100644 --- a/web/ce/constants/estimates.ts +++ b/packages/constants/src/estimates.ts @@ -1,4 +1,4 @@ -// types +// plane imports import { TEstimateSystems } from "@plane/types"; export const MAX_ESTIMATE_POINT_INPUT_LENGTH = 20; diff --git a/packages/ui/src/emoji/icons.ts b/packages/constants/src/icons.ts similarity index 100% rename from packages/ui/src/emoji/icons.ts rename to packages/constants/src/icons.ts diff --git a/packages/constants/src/index.ts b/packages/constants/src/index.ts index 0f5610350..a7452ebe5 100644 --- a/packages/constants/src/index.ts +++ b/packages/constants/src/index.ts @@ -5,6 +5,7 @@ export * from "./endpoints"; export * from "./file"; export * from "./filter"; export * from "./graph"; +export * from "./icons"; export * from "./instance"; export * from "./issue"; export * from "./metadata"; @@ -21,7 +22,7 @@ export * from "./module"; export * from "./project"; export * from "./views"; export * from "./themes"; -export * from "./inbox"; +export * from "./intake"; export * from "./profile"; export * from "./workspace-drafts"; export * from "./label"; @@ -33,4 +34,5 @@ export * from "./emoji"; export * from "./subscription"; export * from "./settings"; export * from "./icon"; +export * from "./estimates"; export * from "./analytics"; diff --git a/packages/constants/src/inbox.ts b/packages/constants/src/intake.ts similarity index 81% rename from packages/constants/src/inbox.ts rename to packages/constants/src/intake.ts index 2d94c1f04..be8c6ffe3 100644 --- a/packages/constants/src/inbox.ts +++ b/packages/constants/src/intake.ts @@ -95,3 +95,32 @@ export const INBOX_ISSUE_SORT_BY_OPTIONS = [ i18n_label: "common.sort.desc", }, ]; + +export enum EPastDurationFilters { + TODAY = "today", + YESTERDAY = "yesterday", + LAST_7_DAYS = "last_7_days", + LAST_30_DAYS = "last_30_days", +} + +export const PAST_DURATION_FILTER_OPTIONS: { + name: string; + value: string; +}[] = [ + { + name: "Today", + value: EPastDurationFilters.TODAY, + }, + { + name: "Yesterday", + value: EPastDurationFilters.YESTERDAY, + }, + { + name: "Last 7 days", + value: EPastDurationFilters.LAST_7_DAYS, + }, + { + name: "Last 30 days", + value: EPastDurationFilters.LAST_30_DAYS, + }, +]; diff --git a/packages/constants/src/state.ts b/packages/constants/src/state.ts index 3b6de4c8f..af7971023 100644 --- a/packages/constants/src/state.ts +++ b/packages/constants/src/state.ts @@ -1,4 +1,5 @@ "use client" + export type TStateGroups = "backlog" | "unstarted" | "started" | "completed" | "cancelled"; export type TDraggableData = { diff --git a/packages/editor/src/core/helpers/parser.ts b/packages/editor/src/core/helpers/parser.ts new file mode 100644 index 000000000..844c7b6af --- /dev/null +++ b/packages/editor/src/core/helpers/parser.ts @@ -0,0 +1,89 @@ +// plane imports +import { TDocumentPayload, TDuplicateAssetData, TDuplicateAssetResponse } from "@plane/types"; +import { TEditorAssetType } from "@plane/types/src/enums"; +// local imports +import { convertHTMLDocumentToAllFormats } from "./yjs-utils"; + +/** + * @description function to extract all image assets from HTML content + * @param htmlContent + * @returns {string[]} array of image asset sources + */ +export const extractImageAssetsFromHTMLContent = (htmlContent: string): string[] => { + // create a DOM parser + const parser = new DOMParser(); + // parse the HTML string into a DOM document + const doc = parser.parseFromString(htmlContent, "text/html"); + // get all image components + const imageComponents = doc.querySelectorAll("image-component"); + // collect all unique image sources + const imageSources = new Set(); + // extract sources from image components + imageComponents.forEach((component) => { + const src = component.getAttribute("src"); + if (src) imageSources.add(src); + }); + return Array.from(imageSources); +}; + +/** + * @description function to replace image assets in HTML content with new IDs + * @param props + * @returns {string} HTML content with replaced image assets + */ +export const replaceImageAssetsInHTMLContent = (props: { + htmlContent: string; + assetMap: Record; +}): string => { + const { htmlContent, assetMap } = props; + // create a DOM parser + const parser = new DOMParser(); + // parse the HTML string into a DOM document + const doc = parser.parseFromString(htmlContent, "text/html"); + // replace sources in image components + const imageComponents = doc.querySelectorAll("image-component"); + imageComponents.forEach((component) => { + const oldSrc = component.getAttribute("src"); + if (oldSrc && assetMap[oldSrc]) { + component.setAttribute("src", assetMap[oldSrc]); + } + }); + // serialize the document back into a string + return doc.body.innerHTML; +}; + +export const getEditorContentWithReplacedImageAssets = async (props: { + descriptionHTML: string; + entityId: string; + entityType: TEditorAssetType; + projectId: string | undefined; + variant: "rich" | "document"; + duplicateAssetService: (params: TDuplicateAssetData) => Promise; +}): Promise => { + const { descriptionHTML, entityId, entityType, projectId, variant, duplicateAssetService } = props; + let replacedDescription = descriptionHTML; + // step 1: extract image assets from the description + const imageAssets = extractImageAssetsFromHTMLContent(descriptionHTML); + if (imageAssets.length !== 0) { + // step 2: duplicate the image assets + const duplicateAssetsResponse = await duplicateAssetService({ + entity_id: entityId, + entity_type: entityType, + project_id: projectId, + asset_ids: imageAssets, + }); + if (Object.keys(duplicateAssetsResponse ?? {}).length > 0) { + // step 3: replace the image assets in the description + replacedDescription = replaceImageAssetsInHTMLContent({ + htmlContent: descriptionHTML, + assetMap: duplicateAssetsResponse, + }); + } + } + // step 4: convert the description to the document payload + const documentPayload = convertHTMLDocumentToAllFormats({ + document_html: replacedDescription, + variant, + }); + return documentPayload; +}; diff --git a/packages/editor/src/core/helpers/yjs-utils.ts b/packages/editor/src/core/helpers/yjs-utils.ts index dce75fd1f..d61711127 100644 --- a/packages/editor/src/core/helpers/yjs-utils.ts +++ b/packages/editor/src/core/helpers/yjs-utils.ts @@ -3,6 +3,7 @@ import { generateHTML, generateJSON } from "@tiptap/html"; import { prosemirrorJSONToYDoc, yXmlFragmentToProseMirrorRootNode } from "y-prosemirror"; import * as Y from "yjs"; // extensions +import { TDocumentPayload } from "@plane/types"; import { CoreEditorExtensionsWithoutProps, DocumentEditorExtensionsWithoutProps, @@ -140,3 +141,50 @@ export const getAllDocumentFormatsFromDocumentEditorBinaryData = ( contentHTML, }; }; + +type TConvertHTMLDocumentToAllFormatsArgs = { + document_html: string; + variant: "rich" | "document"; +}; + +/** + * @description Converts HTML content to all supported document formats (JSON, HTML, and binary) + * @param {TConvertHTMLDocumentToAllFormatsArgs} args - Arguments containing HTML content and variant type + * @param {string} args.document_html - The HTML content to convert + * @param {"rich" | "document"} args.variant - The type of editor variant to use for conversion + * @returns {TDocumentPayload} Object containing the document in all supported formats + * @throws {Error} If an invalid variant is provided + */ +export const convertHTMLDocumentToAllFormats = (args: TConvertHTMLDocumentToAllFormatsArgs): TDocumentPayload => { + const { document_html, variant } = args; + + let allFormats: TDocumentPayload; + + if (variant === "rich") { + // Convert HTML to binary format for rich text editor + const contentBinary = getBinaryDataFromRichTextEditorHTMLString(document_html); + // Generate all document formats from the binary data + const { contentBinaryEncoded, contentHTML, contentJSON } = + getAllDocumentFormatsFromRichTextEditorBinaryData(contentBinary); + allFormats = { + description: contentJSON, + description_html: contentHTML, + description_binary: contentBinaryEncoded, + }; + } else if (variant === "document") { + // Convert HTML to binary format for document editor + const contentBinary = getBinaryDataFromDocumentEditorHTMLString(document_html); + // Generate all document formats from the binary data + const { contentBinaryEncoded, contentHTML, contentJSON } = + getAllDocumentFormatsFromDocumentEditorBinaryData(contentBinary); + allFormats = { + description: contentJSON, + description_html: contentHTML, + description_binary: contentBinaryEncoded, + }; + } else { + throw new Error(`Invalid variant provided: ${variant}`); + } + + return allFormats; +}; diff --git a/packages/editor/src/core/plugins/drag-handle.ts b/packages/editor/src/core/plugins/drag-handle.ts index aa00fa32d..4a534bc4c 100644 --- a/packages/editor/src/core/plugins/drag-handle.ts +++ b/packages/editor/src/core/plugins/drag-handle.ts @@ -1,7 +1,6 @@ import { Fragment, Slice, Node, Schema } from "@tiptap/pm/model"; import { NodeSelection } from "@tiptap/pm/state"; -// @ts-expect-error __serializeForClipboard's is not exported -import { __serializeForClipboard, EditorView } from "@tiptap/pm/view"; +import { EditorView } from "@tiptap/pm/view"; // constants import { CORE_EXTENSIONS } from "@/constants/extension"; // extensions @@ -417,7 +416,7 @@ const handleNodeSelection = ( } const slice = view.state.selection.content(); - const { dom, text } = __serializeForClipboard(view, slice); + const { dom, text } = view.serializeForClipboard(slice); if (event instanceof DragEvent && event.dataTransfer) { event.dataTransfer.clearData(); diff --git a/packages/propel/src/table/core.tsx b/packages/propel/src/table/core.tsx index e6e7ad59c..577b79b2e 100644 --- a/packages/propel/src/table/core.tsx +++ b/packages/propel/src/table/core.tsx @@ -1,120 +1,76 @@ -import * as React from "react" +import * as React from "react"; -import { cn } from "@plane/utils" +import { cn } from "@plane/utils"; -const Table = React.forwardRef< - HTMLTableElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( +const Table = React.forwardRef>( + ({ className, ...props }, ref) => (
- +
-)) -Table.displayName = "Table" + ) +); +Table.displayName = "Table"; -const TableHeader = React.forwardRef< - HTMLTableSectionElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( - -)) -TableHeader.displayName = "TableHeader" - -const TableBody = React.forwardRef< - HTMLTableSectionElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( - >( + ({ className, ...props }, ref) => ( + -)) -TableBody.displayName = "TableBody" + ) +); +TableHeader.displayName = "TableHeader"; -const TableFooter = React.forwardRef< - HTMLTableSectionElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( - -)) -TableFooter.displayName = "TableFooter" +const TableBody = React.forwardRef>( + ({ className, ...props }, ref) => +); +TableBody.displayName = "TableBody"; -const TableRow = React.forwardRef< - HTMLTableRowElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( +const TableFooter = React.forwardRef>( + ({ className, ...props }, ref) => ( + + ) +); +TableFooter.displayName = "TableFooter"; + +const TableRow = React.forwardRef>( + ({ className, ...props }, ref) => ( -)) -TableRow.displayName = "TableRow" + ) +); +TableRow.displayName = "TableRow"; -const TableHead = React.forwardRef< - HTMLTableHeaderCellElement, - React.ThHTMLAttributes ->(({ className, ...props }, ref) => ( -
[role=checkbox]]:translate-y-[2px]", - className - )} - {...props} - /> -)) -TableHead.displayName = "TableHead" +const TableHead = React.forwardRef>(({ className, ...props }, ref) => ( + [role=checkbox]]:translate-y-[2px]", + className + )} + {...props} + /> +)); +TableHead.displayName = "TableHead"; -const TableCell = React.forwardRef< - HTMLTableDataCellElement, - React.TdHTMLAttributes ->(({ className, ...props }, ref) => ( - [role=checkbox]]:translate-y-[2px]", - className - )} - {...props} - /> -)) -TableCell.displayName = "TableCell" +const TableCell = React.forwardRef>(({ className, ...props }, ref) => ( + [role=checkbox]]:translate-y-[2px]", className)} + {...props} + /> +)); +TableCell.displayName = "TableCell"; -const TableCaption = React.forwardRef< - HTMLTableDataCellElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)) -TableCaption.displayName = "TableCaption" +const TableCaption = React.forwardRef>( + ({ className, ...props }, ref) => ( + + ) +); +TableCaption.displayName = "TableCaption"; -export { - Table, - TableHeader, - TableBody, - TableFooter, - TableHead, - TableRow, - TableCell, - TableCaption, -} \ No newline at end of file +export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption }; diff --git a/packages/propel/tsconfig.json b/packages/propel/tsconfig.json index 1f695a242..f811f5e05 100644 --- a/packages/propel/tsconfig.json +++ b/packages/propel/tsconfig.json @@ -1,7 +1,8 @@ { "extends": "@plane/typescript-config/react-library.json", "compilerOptions": { - "outDir": "dist" + "jsx": "react", + "lib": ["esnext", "dom"] }, "include": ["src"], "exclude": ["node_modules", "dist"] diff --git a/packages/services/src/file/file.service.ts b/packages/services/src/file/file.service.ts index 59c054faf..32edd4eb4 100644 --- a/packages/services/src/file/file.service.ts +++ b/packages/services/src/file/file.service.ts @@ -1,6 +1,7 @@ // plane imports import { API_BASE_URL } from "@plane/constants"; // api service +import { TDuplicateAssetData, TDuplicateAssetResponse } from "@plane/types"; import { APIService } from "../api.service"; // helpers import { getAssetIdFromUrl } from "./helper"; @@ -64,4 +65,19 @@ export class FileService extends APIService { throw error?.response?.data; }); } + + /** + * Duplicates assets + * @param {string} workspaceSlug - The workspace slug + * @param {TDuplicateAssetData} data - The data for the duplicate assets + * @returns {Promise} Promise resolving to a record of asset IDs + * @throws {Error} If the request fails + */ + async duplicateAssets(workspaceSlug: string, data: TDuplicateAssetData): Promise { + return this.post(`/api/assets/v2/workspaces/${workspaceSlug}/duplicate-assets/`, data) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } } diff --git a/packages/types/src/calendar.d.ts b/packages/types/src/calendar.d.ts index cb27e2d10..348d93b1f 100644 --- a/packages/types/src/calendar.d.ts +++ b/packages/types/src/calendar.d.ts @@ -2,3 +2,28 @@ export interface ICalendarRange { startDate: Date; endDate: Date; } + +export interface ICalendarDate { + date: Date; + year: number; + month: number; + day: number; + week: number; // week number wrt year, eg- 51, 52 + is_current_month: boolean; + is_current_week: boolean; + is_today: boolean; +} + +export interface ICalendarWeek { + [date: string]: ICalendarDate; +} + +export interface ICalendarMonth { + [monthIndex: string]: { + [weekNumber: string]: ICalendarWeek; + }; +} + +export interface ICalendarPayload { + [year: string]: ICalendarMonth; +} diff --git a/packages/types/src/cycle/cycle.d.ts b/packages/types/src/cycle/cycle.d.ts index 638d974e6..218219914 100644 --- a/packages/types/src/cycle/cycle.d.ts +++ b/packages/types/src/cycle/cycle.d.ts @@ -136,3 +136,16 @@ export type TPublicCycle = { name: string; status: string; }; + +export type TProgressChartData = { + date: string; + scope: number; + completed: number; + backlog: number; + started: number; + unstarted: number; + cancelled: number; + pending: number; + ideal: number; + actual: number; +}[]; diff --git a/packages/types/src/enums.ts b/packages/types/src/enums.ts index a49bec7ab..7776e9f24 100644 --- a/packages/types/src/enums.ts +++ b/packages/types/src/enums.ts @@ -68,8 +68,18 @@ export enum EFileAssetType { TEAM_SPACE_COMMENT_DESCRIPTION = "TEAM_SPACE_COMMENT_DESCRIPTION", } +export type TEditorAssetType = + | EFileAssetType.COMMENT_DESCRIPTION + | EFileAssetType.ISSUE_DESCRIPTION + | EFileAssetType.DRAFT_ISSUE_DESCRIPTION + | EFileAssetType.PAGE_DESCRIPTION + | EFileAssetType.TEAM_SPACE_DESCRIPTION + | EFileAssetType.INITIATIVE_DESCRIPTION + | EFileAssetType.PROJECT_DESCRIPTION + | EFileAssetType.TEAM_SPACE_COMMENT_DESCRIPTION; + export enum EUpdateStatus { OFF_TRACK = "OFF-TRACK", ON_TRACK = "ON-TRACK", AT_RISK = "AT-RISK", -} \ No newline at end of file +} diff --git a/packages/types/src/file.d.ts b/packages/types/src/file.d.ts index 8bcaade6c..d26533221 100644 --- a/packages/types/src/file.d.ts +++ b/packages/types/src/file.d.ts @@ -1,16 +1,16 @@ -import { EFileAssetType } from "./enums" +import { EFileAssetType } from "./enums"; export type TFileMetaDataLite = { name: string; // file size in bytes size: number; type: string; -} +}; export type TFileEntityInfo = { entity_identifier: string; entity_type: EFileAssetType; -} +}; export type TFileMetaData = TFileMetaDataLite & TFileEntityInfo; @@ -29,4 +29,13 @@ export type TFileSignedURLResponse = { "x-amz-signature": string; }; }; -}; \ No newline at end of file +}; + +export type TDuplicateAssetData = { + entity_id: string; + entity_type: EFileAssetType; + project_id?: string; + asset_ids: string[]; +}; + +export type TDuplicateAssetResponse = Record; // asset_id -> new_asset_id diff --git a/packages/types/src/index.d.ts b/packages/types/src/index.d.ts index 9a6711421..d8b95fca4 100644 --- a/packages/types/src/index.d.ts +++ b/packages/types/src/index.d.ts @@ -43,4 +43,5 @@ export * from "./home"; export * from "./stickies"; export * from "./utils"; export * from "./payment"; +export * from "./layout"; export * from "./analytics"; diff --git a/web/core/components/gantt-chart/types/index.ts b/packages/types/src/layout/gantt.d.ts similarity index 100% rename from web/core/components/gantt-chart/types/index.ts rename to packages/types/src/layout/gantt.d.ts diff --git a/packages/types/src/layout/index.ts b/packages/types/src/layout/index.ts new file mode 100644 index 000000000..88de77a54 --- /dev/null +++ b/packages/types/src/layout/index.ts @@ -0,0 +1 @@ +export * from "./gantt"; diff --git a/packages/types/src/project/projects.d.ts b/packages/types/src/project/projects.d.ts index 360a92c55..9d6b03ab1 100644 --- a/packages/types/src/project/projects.d.ts +++ b/packages/types/src/project/projects.d.ts @@ -141,3 +141,7 @@ export interface ISearchIssueResponse { workspace__slug: string; type_id: string; } + +export type TPartialProject = IPartialProject; + +export type TProject = TPartialProject & IProject; diff --git a/packages/ui/src/emoji/icons-list.tsx b/packages/ui/src/emoji/icons-list.tsx index bb7607489..bae22ffb8 100644 --- a/packages/ui/src/emoji/icons-list.tsx +++ b/packages/ui/src/emoji/icons-list.tsx @@ -1,14 +1,14 @@ +import { Search } from "lucide-react"; import React, { useEffect, useState } from "react"; // icons -import { Search } from "lucide-react"; -import { MATERIAL_ICONS_LIST } from "./icons"; +import useFontFaceObserver from "use-font-face-observer"; +import { MATERIAL_ICONS_LIST } from "@plane/constants"; +import { cn } from "../../helpers"; +import { Input } from "../form-fields"; import { InfoIcon } from "../icons"; // components -import { Input } from "../form-fields"; // hooks -import useFontFaceObserver from "use-font-face-observer"; // helpers -import { cn } from "../../helpers"; import { DEFAULT_COLORS, TIconsListProps, adjustColorForContrast } from "./emoji-icon-helper"; export const IconsList: React.FC = (props) => { diff --git a/packages/ui/src/emoji/index.ts b/packages/ui/src/emoji/index.ts index c881d8897..c87b6cd23 100644 --- a/packages/ui/src/emoji/index.ts +++ b/packages/ui/src/emoji/index.ts @@ -1,5 +1,4 @@ export * from "./emoji-icon-picker-new"; export * from "./emoji-icon-picker"; export * from "./emoji-icon-helper"; -export * from "./icons"; export * from "./logo"; diff --git a/packages/ui/src/emoji/logo.tsx b/packages/ui/src/emoji/logo.tsx index 74d226dfa..0d598bac3 100644 --- a/packages/ui/src/emoji/logo.tsx +++ b/packages/ui/src/emoji/logo.tsx @@ -1,9 +1,9 @@ -import React, { FC } from "react"; import { Emoji } from "emoji-picker-react"; +import React, { FC } from "react"; import useFontFaceObserver from "use-font-face-observer"; -// icons -import { LUCIDE_ICONS_LIST } from "./icons"; -// helpers +// plane imports +import { LUCIDE_ICONS_LIST } from "@plane/constants"; +// local imports import { emojiCodeToUnicode } from "./helpers"; export type TEmojiLogoProps = { diff --git a/packages/ui/src/emoji/lucide-icons-list.tsx b/packages/ui/src/emoji/lucide-icons-list.tsx index d2fe95ecf..d569feb26 100644 --- a/packages/ui/src/emoji/lucide-icons-list.tsx +++ b/packages/ui/src/emoji/lucide-icons-list.tsx @@ -1,14 +1,12 @@ -import React, { useEffect, useState } from "react"; -// components -import { Input } from "../form-fields"; -// helpers -import { cn } from "../../helpers"; -import { DEFAULT_COLORS, TIconsListProps, adjustColorForContrast } from "./emoji-icon-helper"; -// icons -import { InfoIcon } from "../icons"; -// constants -import { LUCIDE_ICONS_LIST } from "./icons"; import { Search } from "lucide-react"; +import React, { useEffect, useState } from "react"; +// plane imports +import { LUCIDE_ICONS_LIST } from "@plane/constants"; +// local imports +import { cn } from "../../helpers"; +import { Input } from "../form-fields"; +import { InfoIcon } from "../icons"; +import { DEFAULT_COLORS, TIconsListProps, adjustColorForContrast } from "./emoji-icon-helper"; export const LucideIconsList: React.FC = (props) => { const { defaultColor, onChange, searchDisabled = false } = props; diff --git a/packages/utils/src/auth.ts b/packages/utils/src/auth.ts index 297b4c9ed..ddd1b3396 100644 --- a/packages/utils/src/auth.ts +++ b/packages/utils/src/auth.ts @@ -1,11 +1,14 @@ +"use client"; + import { ReactNode } from "react"; import zxcvbn from "zxcvbn"; +// plane imports import { E_PASSWORD_STRENGTH, - SPACE_PASSWORD_CRITERIA, PASSWORD_MIN_LENGTH, EErrorAlertType, EAuthErrorCodes, + TAuthErrorInfo, } from "@plane/constants"; /** @@ -30,50 +33,29 @@ export type PasswordCriterion = { /** * @description Password strength criteria */ -export const PASSWORD_CRITERIA: PasswordCriterion[] = [ - { regex: /[a-z]/, description: "lowercase" }, - { regex: /[A-Z]/, description: "uppercase" }, - { regex: /[0-9]/, description: "number" }, - { regex: /[^a-zA-Z0-9]/, description: "special character" }, +export const PASSWORD_CRITERIA = [ + { + key: "min_8_char", + label: "Min 8 characters", + isCriteriaValid: (password: string) => password.length >= PASSWORD_MIN_LENGTH, + }, + // { + // key: "min_1_upper_case", + // label: "Min 1 upper-case letter", + // isCriteriaValid: (password: string) => PASSWORD_NUMBER_REGEX.test(password), + // }, + // { + // key: "min_1_number", + // label: "Min 1 number", + // isCriteriaValid: (password: string) => PASSWORD_CHAR_CAPS_REGEX.test(password), + // }, + // { + // key: "min_1_special_char", + // label: "Min 1 special character", + // isCriteriaValid: (password: string) => PASSWORD_SPECIAL_CHAR_REGEX.test(password), + // }, ]; -/** - * @description Checks if password meets all criteria - * @param {string} password - Password to check - * @returns {boolean} Whether password meets all criteria - */ -export const checkPasswordCriteria = (password: string): boolean => - PASSWORD_CRITERIA.every((criterion) => criterion.regex.test(password)); - -/** - * @description Checks password strength against criteria - * @param {string} password - Password to check - * @returns {PasswordStrength} Password strength level - * @example - * checkPasswordStrength("abc") // returns PasswordStrength.WEAK - * checkPasswordStrength("Abc123!@#") // returns PasswordStrength.STRONG - */ -export const checkPasswordStrength = (password: string): PasswordStrength => { - if (!password || password.length === 0) return PasswordStrength.EMPTY; - if (password.length < PASSWORD_MIN_LENGTH) return PasswordStrength.WEAK; - - const criteriaCount = PASSWORD_CRITERIA.filter((criterion) => criterion.regex.test(password)).length; - - const zxcvbnScore = zxcvbn(password).score; - - if (criteriaCount <= 1 || zxcvbnScore <= 1) return PasswordStrength.WEAK; - if (criteriaCount === 2 || zxcvbnScore === 2) return PasswordStrength.FAIR; - if (criteriaCount === 3 || zxcvbnScore === 3) return PasswordStrength.GOOD; - return PasswordStrength.STRONG; -}; - -export type TAuthErrorInfo = { - type: EErrorAlertType; - code: EAuthErrorCodes; - title: string; - message: ReactNode; -}; - // Password strength check export const getPasswordStrength = (password: string): E_PASSWORD_STRENGTH => { let passwordStrength: E_PASSWORD_STRENGTH = E_PASSWORD_STRENGTH.EMPTY; @@ -89,9 +71,9 @@ export const getPasswordStrength = (password: string): E_PASSWORD_STRENGTH => { return passwordStrength; } - const passwordCriteriaValidation = SPACE_PASSWORD_CRITERIA.map((criteria) => - criteria.isCriteriaValid(password) - ).every((criterion) => criterion); + const passwordCriteriaValidation = PASSWORD_CRITERIA.map((criteria) => criteria.isCriteriaValid(password)).every( + (criterion) => criterion + ); const passwordStrengthScore = zxcvbn(password).score; if (passwordCriteriaValidation === false || passwordStrengthScore <= 2) { diff --git a/web/helpers/calendar.helper.ts b/packages/utils/src/calendar.ts similarity index 66% rename from web/helpers/calendar.helper.ts rename to packages/utils/src/calendar.ts index 709cf9c96..f4217ea41 100644 --- a/web/helpers/calendar.helper.ts +++ b/packages/utils/src/calendar.ts @@ -1,46 +1,8 @@ +// plane imports import { EStartOfTheWeek } from "@plane/constants"; -// helpers -import { ICalendarDate, ICalendarPayload } from "@/components/issues"; -import { DAYS_LIST } from "@/constants/calendar"; -import { getWeekNumberOfDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper"; -// types - -export const formatDate = (date: Date, format: string): string => { - const day = date.getDate(); - const month = date.getMonth() + 1; - const year = date.getFullYear(); - const hours = date.getHours(); - const minutes = date.getMinutes(); - const seconds = date.getSeconds(); - const daysOfWeek = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; - const monthsOfYear = [ - "January", - "February", - "March", - "April", - "May", - "June", - "July", - "August", - "September", - "October", - "November", - "December", - ]; - - const formattedDate = format - .replace("dd", day.toString().padStart(2, "0")) - .replace("d", day.toString()) - .replace("eee", daysOfWeek[date.getDay()]) - .replace("Month", monthsOfYear[month - 1]) - .replace("yyyy", year.toString()) - .replace("yyy", year.toString().slice(-3)) - .replace("hh", hours.toString().padStart(2, "0")) - .replace("mm", minutes.toString().padStart(2, "0")) - .replace("ss", seconds.toString().padStart(2, "0")); - - return formattedDate; -}; +import { ICalendarDate, ICalendarPayload } from "@plane/types"; +// local imports +import { getWeekNumberOfDate, renderFormattedPayloadDate } from "./datetime"; /** * @returns {ICalendarPayload} calendar payload to render the calendar @@ -101,14 +63,12 @@ export const generateCalendarData = (currentStructure: ICalendarPayload | null, * @param getDayIndex Function to get the day index (0-6) from an item. * @param startOfWeek The day to start the week on. */ -export function getOrderedDays( +export const getOrderedDays = ( items: T[], getDayIndex: (item: T) => number, startOfWeek: EStartOfTheWeek = EStartOfTheWeek.SUNDAY -): T[] { - return [...items].sort((a, b) => { +): T[] => [...items].sort((a, b) => { const dayA = (7 + getDayIndex(a) - startOfWeek) % 7; const dayB = (7 + getDayIndex(b) - startOfWeek) % 7; return dayA - dayB; - }); -} + }) diff --git a/packages/utils/src/color.ts b/packages/utils/src/color.ts index f97910efa..017c594b7 100644 --- a/packages/utils/src/color.ts +++ b/packages/utils/src/color.ts @@ -1,13 +1,13 @@ /** * Represents an RGB color with numeric values for red, green, and blue components - * @typedef {Object} RGB + * @typedef {Object} TRgb * @property {number} r - Red component (0-255) * @property {number} g - Green component (0-255) * @property {number} b - Blue component (0-255) */ -export type RGB = { r: number; g: number; b: number }; +export type TRgb = { r: number; g: number; b: number }; -export type HSL = { h: number; s: number; l: number }; +export type THsl = { h: number; s: number; l: number }; /** * @description Validates and clamps color values to RGB range (0-255) @@ -40,7 +40,7 @@ export const toHex = (value: number) => validateColor(value).toString(16).padSta * hexToRgb("#00ff00") // returns { r: 0, g: 255, b: 0 } * hexToRgb("#0000ff") // returns { r: 0, g: 0, b: 255 } */ -export const hexToRgb = (hex: string): RGB => { +export const hexToRgb = (hex: string): TRgb => { const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex.trim()); return result ? { @@ -63,7 +63,7 @@ export const hexToRgb = (hex: string): RGB => { * rgbToHex({ r: 0, g: 255, b: 0 }) // returns "#00ff00" * rgbToHex({ r: 0, g: 0, b: 255 }) // returns "#0000ff" */ -export const rgbToHex = ({ r, g, b }: RGB): string => `#${toHex(r)}${toHex(g)}${toHex(b)}`; +export const rgbToHex = ({ r, g, b }: TRgb): string => `#${toHex(r)}${toHex(g)}${toHex(b)}`; /** * Converts Hex values to HSL values @@ -74,7 +74,7 @@ export const rgbToHex = ({ r, g, b }: RGB): string => `#${toHex(r)}${toHex(g)}${ * hexToHsl("#00ff00") // returns { h: 120, s: 100, l: 50 } * hexToHsl("#0000ff") // returns { h: 240, s: 100, l: 50 } */ -export const hexToHsl = (hex: string): HSL => { +export const hexToHsl = (hex: string): THsl => { // return default value for invalid hex if (!/^#[0-9A-Fa-f]{6}$/.test(hex)) return { h: 0, s: 0, l: 0 }; @@ -124,7 +124,7 @@ export const hexToHsl = (hex: string): HSL => { * hslToHex({ h: 120, s: 100, l: 50 }) // returns "#00ff00" * hslToHex({ h: 240, s: 100, l: 50 }) // returns "#0000ff" */ -export const hslToHex = ({ h, s, l }: HSL): string => { +export const hslToHex = ({ h, s, l }: THsl): string => { if (h < 0 || h > 360) return "#000000"; if (s < 0 || s > 100) return "#000000"; if (l < 0 || l > 100) return "#000000"; @@ -142,3 +142,158 @@ export const hslToHex = ({ h, s, l }: HSL): string => { return `#${f(0)}${f(8)}${f(4)}`; }; + +/** + * Calculate relative luminance of a color according to WCAG + * @param {Object} rgb - RGB color object with r, g, b properties + * @returns {number} Relative luminance value + */ +export const getLuminance = ({ r, g, b }: TRgb) => { + // Convert RGB to sRGB + const sR = r / 255; + const sG = g / 255; + const sB = b / 255; + + // Convert sRGB to linear RGB with gamma correction + const R = sR <= 0.03928 ? sR / 12.92 : Math.pow((sR + 0.055) / 1.055, 2.4); + const G = sG <= 0.03928 ? sG / 12.92 : Math.pow((sG + 0.055) / 1.055, 2.4); + const B = sB <= 0.03928 ? sB / 12.92 : Math.pow((sB + 0.055) / 1.055, 2.4); + + // Calculate luminance + return 0.2126 * R + 0.7152 * G + 0.0722 * B; +}; + +/** + * Calculate contrast ratio between two colors + * @param {Object} rgb1 - First RGB color object + * @param {Object} rgb2 - Second RGB color object + * @returns {number} Contrast ratio between the colors + */ +export function getContrastRatio(rgb1: { r: number; g: number; b: number }, rgb2: { r: number; g: number; b: number }) { + const luminance1 = getLuminance(rgb1); + const luminance2 = getLuminance(rgb2); + + const lighter = Math.max(luminance1, luminance2); + const darker = Math.min(luminance1, luminance2); + + return (lighter + 0.05) / (darker + 0.05); +} + +/** + * Lighten a color by a specified amount + * @param {Object} rgb - RGB color object + * @param {number} amount - Amount to lighten (0-1) + * @returns {Object} Lightened RGB color + */ +export function lightenColor(rgb: { r: number; g: number; b: number }, amount: number) { + return { + r: rgb.r + (255 - rgb.r) * amount, + g: rgb.g + (255 - rgb.g) * amount, + b: rgb.b + (255 - rgb.b) * amount, + }; +} + +/** + * Darken a color by a specified amount + * @param {Object} rgb - RGB color object + * @param {number} amount - Amount to darken (0-1) + * @returns {Object} Darkened RGB color + */ +export function darkenColor(rgb: { r: number; g: number; b: number }, amount: number) { + return { + r: rgb.r * (1 - amount), + g: rgb.g * (1 - amount), + b: rgb.b * (1 - amount), + }; +} + +/** + * Generate appropriate foreground and background colors based on input color + * @param {string} color - Input color in hex format + * @returns {Object} Object containing foreground and background colors in hex format + */ +export function generateIconColors(color: string) { + // Parse input color + const rgbColor = hexToRgb(color); + const luminance = getLuminance(rgbColor); + + // Initialize output colors + let foregroundColor = rgbColor; + + // Constants for color adjustment + const MIN_CONTRAST_RATIO = 3.0; // Minimum acceptable contrast ratio + + // For light colors, use as foreground and darken for background + if (luminance > 0.5) { + // Make sure the foreground color is dark enough for visibility + let adjustedForeground = foregroundColor; + const whiteContrast = getContrastRatio(foregroundColor, { r: 255, g: 255, b: 255 }); + + if (whiteContrast < MIN_CONTRAST_RATIO) { + // Darken the foreground color until it has enough contrast + let darkenAmount = 0.1; + while (darkenAmount <= 0.9) { + adjustedForeground = darkenColor(foregroundColor, darkenAmount); + if (getContrastRatio(adjustedForeground, { r: 255, g: 255, b: 255 }) >= MIN_CONTRAST_RATIO) { + break; + } + darkenAmount += 0.1; + } + foregroundColor = adjustedForeground; + } + } + // For dark colors, use as foreground and lighten for background + else { + // Make sure the foreground color is light enough for visibility + let adjustedForeground = foregroundColor; + const blackContrast = getContrastRatio(foregroundColor, { r: 0, g: 0, b: 0 }); + + if (blackContrast < MIN_CONTRAST_RATIO) { + // Lighten the foreground color until it has enough contrast + let lightenAmount = 0.1; + while (lightenAmount <= 0.9) { + adjustedForeground = lightenColor(foregroundColor, lightenAmount); + if (getContrastRatio(adjustedForeground, { r: 0, g: 0, b: 0 }) >= MIN_CONTRAST_RATIO) { + break; + } + lightenAmount += 0.1; + } + foregroundColor = adjustedForeground; + } + } + + return { + foreground: rgbToHex({ r: foregroundColor.r, g: foregroundColor.g, b: foregroundColor.b }), + background: `rgba(${foregroundColor.r}, ${foregroundColor.g}, ${foregroundColor.b}, 0.25)`, + }; +} + +/** + * @description Generates a deterministic HSL color based on input string + * @param {string} input - Input string to generate color from + * @returns {THsl} An object containing the HSL values + * @example + * generateRandomColor("hello") // returns consistent HSL color for "hello" + * generateRandomColor("") // returns { h: 0, s: 0, l: 0 } + */ +export const generateRandomColor = (input: string): THsl => { + // If input is falsy, generate a random seed string. + // The random seed is created by converting a random number to base-36 and taking a substring. + const seed = input || Math.random().toString(36).substring(2, 8); + + const uniqueId = seed.length.toString() + seed; // Unique identifier based on string length + const combinedString = uniqueId + seed; + + // Create a hash value from the combined string. + const hash = Array.from(combinedString).reduce((acc, char) => { + const charCode = char.charCodeAt(0); + return (acc << 5) - acc + charCode; + }, 0); + + // Derive the HSL values from the hash. + const hue = Math.abs(hash % 360); + const saturation = 70; // Maintains a good amount of color + const lightness = 70; // Increased lightness for a pastel look + + return { h: hue, s: saturation, l: lightness }; +}; diff --git a/packages/utils/src/common.ts b/packages/utils/src/common.ts index d2d02c299..a65d62cfb 100644 --- a/packages/utils/src/common.ts +++ b/packages/utils/src/common.ts @@ -58,3 +58,5 @@ export const isComplete = (obj: CompleteOrEmpty): obj is T => { // Check if it has any own properties return Object.keys(obj).length > 0; }; + +export const convertRemToPixel = (rem: number): number => rem * 0.9 * 16; diff --git a/web/helpers/cycle.helper.ts b/packages/utils/src/cycle.ts similarity index 77% rename from web/helpers/cycle.helper.ts rename to packages/utils/src/cycle.ts index 6e62c3a04..66002c740 100644 --- a/web/helpers/cycle.helper.ts +++ b/packages/utils/src/cycle.ts @@ -1,28 +1,19 @@ import { startOfToday, format } from "date-fns"; -import { isEmpty, orderBy, uniqBy } from "lodash"; +import isEmpty from "lodash/isEmpty"; +import orderBy from "lodash/orderBy"; import sortBy from "lodash/sortBy"; +import uniqBy from "lodash/uniqBy"; +// plane imports import { ICycle, TCycleFilters } from "@plane/types"; -// helpers -import { findTotalDaysInRange, generateDateArray, getDate } from "@/helpers/date-time.helper"; -import { satisfiesDateFilter } from "@/helpers/filter.helper"; - -export type TProgressChartData = { - date: string; - scope: number; - completed: number; - backlog: number; - started: number; - unstarted: number; - cancelled: number; - pending: number; - ideal: number; - actual: number; -}[]; +// local imports +import { findTotalDaysInRange, generateDateArray, getDate } from "./datetime"; +import { satisfiesDateFilter } from "./filter"; /** - * @description orders cycles based on their status - * @param {ICycle[]} cycles - * @returns {ICycle[]} + * Orders cycles based on their status + * @param {ICycle[]} cycles - Array of cycles to be ordered + * @param {boolean} sortByManual - Whether to sort by manual order + * @returns {ICycle[]} Ordered array of cycles */ export const orderCycles = (cycles: ICycle[], sortByManual: boolean): ICycle[] => { if (cycles.length === 0) return []; @@ -48,10 +39,10 @@ export const orderCycles = (cycles: ICycle[], sortByManual: boolean): ICycle[] = }; /** - * @description filters cycles based on the filter - * @param {ICycle} cycle - * @param {TCycleFilters} filter - * @returns {boolean} + * Filters cycles based on provided filter criteria + * @param {ICycle} cycle - The cycle to be filtered + * @param {TCycleFilters} filter - Filter criteria to apply + * @returns {boolean} Whether the cycle passes the filter */ export const shouldFilterCycle = (cycle: ICycle, filter: TCycleFilters): boolean => { let fallsInFilters = true; @@ -76,7 +67,21 @@ export const shouldFilterCycle = (cycle: ICycle, filter: TCycleFilters): boolean return fallsInFilters; }; +/** + * Calculates the scope based on whether it's an issue or estimate points + * @param {any} p - Progress data + * @param {boolean} isTypeIssue - Whether the type is an issue + * @returns {number} Calculated scope + */ const scope = (p: any, isTypeIssue: boolean) => (isTypeIssue ? p.total_issues : p.total_estimate_points); + +/** + * Calculates the ideal progress value + * @param {string} date - Current date + * @param {number} scope - Total scope + * @param {ICycle} cycle - Cycle data + * @returns {number} Ideal progress value + */ const ideal = (date: string, scope: number, cycle: ICycle) => Math.floor( ((findTotalDaysInRange(date, cycle.end_date) || 0) / @@ -84,6 +89,14 @@ const ideal = (date: string, scope: number, cycle: ICycle) => scope ); +/** + * Formats cycle data for version 1 + * @param {boolean} isTypeIssue - Whether the type is an issue + * @param {ICycle} cycle - Cycle data + * @param {boolean} isBurnDown - Whether it's a burn down chart + * @param {Date|string} endDate - End date + * @returns {TProgressChartData} Formatted progress data + */ const formatV1Data = (isTypeIssue: boolean, cycle: ICycle, isBurnDown: boolean, endDate: Date | string) => { const today = format(startOfToday(), "yyyy-MM-dd"); const data = isTypeIssue ? cycle.distribution : cycle.estimate_distribution; @@ -117,6 +130,14 @@ const formatV1Data = (isTypeIssue: boolean, cycle: ICycle, isBurnDown: boolean, return progress; }; +/** + * Formats cycle data for version 2 + * @param {boolean} isTypeIssue - Whether the type is an issue + * @param {ICycle} cycle - Cycle data + * @param {boolean} isBurnDown - Whether it's a burn down chart + * @param {Date|string} endDate - End date + * @returns {TProgressChartData} Formatted progress data + */ const formatV2Data = (isTypeIssue: boolean, cycle: ICycle, isBurnDown: boolean, endDate: Date | string) => { if (!cycle.progress) return []; let today: Date | string = startOfToday(); diff --git a/packages/utils/src/datetime.ts b/packages/utils/src/datetime.ts index 8cec48f5f..22241e759 100644 --- a/packages/utils/src/datetime.ts +++ b/packages/utils/src/datetime.ts @@ -1,45 +1,15 @@ import { differenceInDays, format, formatDistanceToNow, isAfter, isEqual, isValid, parseISO } from "date-fns"; +import isNumber from "lodash/isNumber"; +// Format Date Helpers /** - * This method returns a date from string of type yyyy-mm-dd - * This method is recommended to use instead of new Date() as this does not introduce any timezone offsets - * @param date - * @returns date or undefined - */ -export const getDate = (date: string | Date | undefined | null): Date | undefined => { - try { - if (!date || date === "") return; - - if (typeof date !== "string" && !(date instanceof String)) return date; - - const [yearString, monthString, dayString] = date.substring(0, 10).split("-"); - const year = parseInt(yearString); - const month = parseInt(monthString); - const day = parseInt(dayString); - // Using Number.isInteger instead of lodash's isNumber for better specificity and no external dependency - if (!Number.isInteger(year) || !Number.isInteger(month) || !Number.isInteger(day)) return; - - return new Date(year, month - 1, day); - } catch (e) { - return undefined; - } -}; - -/** - * @returns {string | null} formatted date in the format of MMM dd, yyyy + * @returns {string | null} formatted date in the desired format or platform default format (MMM dd, yyyy) * @description Returns date in the formatted format * @param {Date | string} date + * @param {string} formatToken (optional) // default MMM dd, yyyy + * @example renderFormattedDate("2024-01-01", "MM-DD-YYYY") // Jan 01, 2024 * @example renderFormattedDate("2024-01-01") // Jan 01, 2024 */ -/** - * @description Returns date in the formatted format - * @param {Date | string} date Date to format - * @param {string} formatToken Format token (optional, default: MMM dd, yyyy) - * @returns {string | undefined} Formatted date in the desired format - * @example - * renderFormattedDate("2024-01-01") // returns "Jan 01, 2024" - * renderFormattedDate("2024-01-01", "MM-DD-YYYY") // returns "01-01-2024" - */ export const renderFormattedDate = ( date: string | Date | undefined | null, formatToken: string = "MMM dd, yyyy" @@ -49,7 +19,7 @@ export const renderFormattedDate = ( // return if undefined if (!parsedDate) return; // Check if the parsed date is valid before formatting - if (!isValid(parsedDate)) return; // Return undefined for invalid dates + if (!isValid(parsedDate)) return; // Return null for invalid dates let formattedDate; try { // Format the date in the format provided or default format (MMM dd, yyyy) @@ -62,13 +32,75 @@ export const renderFormattedDate = ( }; /** + * @returns {string} formatted date in the format of MMM dd + * @description Returns date in the formatted format + * @param {string | Date} date + * @example renderShortDateFormat("2024-01-01") // Jan 01 + */ +export const renderFormattedDateWithoutYear = (date: string | Date): string => { + // Parse the date to check if it is valid + const parsedDate = getDate(date); + // return if undefined + if (!parsedDate) return ""; + // Check if the parsed date is valid before formatting + if (!isValid(parsedDate)) return ""; // Return empty string for invalid dates + // Format the date in short format (MMM dd) + const formattedDate = format(parsedDate, "MMM dd"); + return formattedDate; +}; + +/** + * @returns {string | null} formatted date in the format of yyyy-mm-dd to be used in payload + * @description Returns date in the formatted format to be used in payload + * @param {Date | string} date + * @example renderFormattedPayloadDate("Jan 01, 20224") // "2024-01-01" + */ +export const renderFormattedPayloadDate = (date: Date | string | undefined | null): string | undefined => { + // Parse the date to check if it is valid + const parsedDate = getDate(date); + // return if undefined + if (!parsedDate) return; + // Check if the parsed date is valid before formatting + if (!isValid(parsedDate)) return; // Return null for invalid dates + // Format the date in payload format (yyyy-mm-dd) + const formattedDate = format(parsedDate, "yyyy-MM-dd"); + return formattedDate; +}; + +// Format Time Helpers +/** + * @returns {string} formatted date in the format of hh:mm a or HH:mm + * @description Returns date in 12 hour format if in12HourFormat is true else 24 hour format + * @param {string | Date} date + * @param {boolean} timeFormat (optional) // default 24 hour + * @example renderFormattedTime("2024-01-01 13:00:00") // 13:00 + * @example renderFormattedTime("2024-01-01 13:00:00", "12-hour") // 01:00 PM + */ +export const renderFormattedTime = (date: string | Date, timeFormat: "12-hour" | "24-hour" = "24-hour"): string => { + // Parse the date to check if it is valid + const parsedDate = new Date(date); + // return if undefined + if (!parsedDate) return ""; + // Check if the parsed date is valid + if (!isValid(parsedDate)) return ""; // Return empty string for invalid dates + // Format the date in 12 hour format if in12HourFormat is true + if (timeFormat === "12-hour") { + const formattedTime = format(parsedDate, "hh:mm a"); + return formattedTime; + } + // Format the date in 24 hour format + const formattedTime = format(parsedDate, "HH:mm"); + return formattedTime; +}; + +// Date Difference Helpers +/** + * @returns {number} total number of days in range * @description Returns total number of days in range - * @param {string | Date} startDate - Start date - * @param {string | Date} endDate - End date - * @param {boolean} inclusive - Include start and end dates (optional, default: true) - * @returns {number | undefined} Total number of days - * @example - * findTotalDaysInRange("2024-01-01", "2024-01-08") // returns 8 + * @param {string} startDate + * @param {string} endDate + * @param {boolean} inclusive + * @example checkIfStringIsDate("2021-01-01", "2021-01-08") // 8 */ export const findTotalDaysInRange = ( startDate: Date | string | undefined | null, @@ -89,118 +121,139 @@ export const findTotalDaysInRange = ( }; /** - * @description Add number of days to the provided date - * @param {string | Date} startDate - Start date - * @param {number} numberOfDays - Number of days to add - * @returns {Date | undefined} Resulting date - * @example - * addDaysToDate("2024-01-01", 7) // returns Date(2024-01-08) + * Add number of days to the provided date and return a resulting new date + * @param startDate + * @param numberOfDays + * @returns */ -export const addDaysToDate = (startDate: Date | string | undefined | null, numberOfDays: number): Date | undefined => { +export const addDaysToDate = (startDate: Date | string | undefined | null, numberOfDays: number) => { // Parse the dates to check if they are valid const parsedStartDate = getDate(startDate); + // return if undefined if (!parsedStartDate) return; + const newDate = new Date(parsedStartDate); newDate.setDate(newDate.getDate() + numberOfDays); + return newDate; }; /** + * @returns {number} number of days left from today * @description Returns number of days left from today - * @param {string | Date} date - Target date - * @param {boolean} inclusive - Include today (optional, default: true) - * @returns {number | undefined} Number of days left - * @example - * findHowManyDaysLeft("2024-01-08") // returns days between today and Jan 8, 2024 + * @param {string | Date} date + * @param {boolean} inclusive (optional) // default true + * @example findHowManyDaysLeft("2024-01-01") // 3 */ export const findHowManyDaysLeft = ( date: Date | string | undefined | null, inclusive: boolean = true ): number | undefined => { if (!date) return undefined; + // Pass the date to findTotalDaysInRange function to find the total number of days in range from today return findTotalDaysInRange(new Date(), date, inclusive); }; +// Time Difference Helpers /** + * @returns {string} formatted date in the form of amount of time passed since the event happened * @description Returns time passed since the event happened - * @param {string | number | Date} time - Time to calculate from - * @returns {string} Formatted time ago string - * @example - * calculateTimeAgo("2023-01-01") // returns "1 year ago" + * @param {string | Date} time + * @example calculateTimeAgo("2023-01-01") // 1 year ago */ export const calculateTimeAgo = (time: string | number | Date | null): string => { if (!time) return ""; + // Parse the time to check if it is valid const parsedTime = typeof time === "string" || typeof time === "number" ? parseISO(String(time)) : time; - if (!parsedTime) return ""; + // return if undefined + if (!parsedTime) return ""; // Return empty string for invalid dates + // Format the time in the form of amount of time passed since the event happened const distance = formatDistanceToNow(parsedTime, { addSuffix: true }); return distance; }; -/** - * @description Returns short form of time passed (e.g., 1y, 2mo, 3d) - * @param {string | number | Date} date - Date to calculate from - * @returns {string} Short form time ago - * @example - * calculateTimeAgoShort("2023-01-01") // returns "1y" - */ -export const calculateTimeAgoShort = (date: string | number | Date | null): string => { - if (!date) return ""; +export function calculateTimeAgoShort(date: string | number | Date | null): string { + if (!date) { + return ""; + } const parsedDate = typeof date === "string" ? parseISO(date) : new Date(date); const now = new Date(); const diffInSeconds = (now.getTime() - parsedDate.getTime()) / 1000; - if (diffInSeconds < 60) return `${Math.floor(diffInSeconds)}s`; + if (diffInSeconds < 60) { + return `${Math.floor(diffInSeconds)}s`; + } + const diffInMinutes = diffInSeconds / 60; - if (diffInMinutes < 60) return `${Math.floor(diffInMinutes)}m`; + if (diffInMinutes < 60) { + return `${Math.floor(diffInMinutes)}m`; + } + const diffInHours = diffInMinutes / 60; - if (diffInHours < 24) return `${Math.floor(diffInHours)}h`; + if (diffInHours < 24) { + return `${Math.floor(diffInHours)}h`; + } + const diffInDays = diffInHours / 24; - if (diffInDays < 30) return `${Math.floor(diffInDays)}d`; + if (diffInDays < 30) { + return `${Math.floor(diffInDays)}d`; + } + const diffInMonths = diffInDays / 30; - if (diffInMonths < 12) return `${Math.floor(diffInMonths)}mo`; + if (diffInMonths < 12) { + return `${Math.floor(diffInMonths)}mo`; + } + const diffInYears = diffInMonths / 12; return `${Math.floor(diffInYears)}y`; -}; +} +// Date Validation Helpers /** - * @description Checks if a date is greater than today - * @param {string} dateStr - Date string to check - * @returns {boolean} True if date is greater than today - * @example - * isDateGreaterThanToday("2024-12-31") // returns true + * @returns {string} boolean value depending on whether the date is greater than today + * @description Returns boolean value depending on whether the date is greater than today + * @param {string} dateStr + * @example isDateGreaterThanToday("2024-01-01") // true */ export const isDateGreaterThanToday = (dateStr: string): boolean => { + // Return false if dateStr is not present if (!dateStr) return false; + // Parse the date to check if it is valid const date = parseISO(dateStr); const today = new Date(); - if (!isValid(date)) return false; + // Check if the parsed date is valid + if (!isValid(date)) return false; // Return false for invalid dates + // Return true if the date is greater than today return isAfter(date, today); }; +// Week Related Helpers /** + * @returns {number} week number of date * @description Returns week number of date - * @param {Date} date - Date to get week number from - * @returns {number} Week number (1-52) - * @example - * getWeekNumberOfDate(new Date("2023-09-01")) // returns 35 + * @param {Date} date + * @example getWeekNumber(new Date("2023-09-01")) // 35 */ export const getWeekNumberOfDate = (date: Date): number => { const currentDate = date; + // Adjust the starting day to Sunday (0) instead of Monday (1) const startDate = new Date(currentDate.getFullYear(), 0, 1); + // Calculate the number of days between currentDate and startDate const days = Math.floor((currentDate.getTime() - startDate.getTime()) / (24 * 60 * 60 * 1000)); + // Adjust the calculation for weekNumber const weekNumber = Math.ceil((days + 1) / 7); return weekNumber; }; /** - * @description Checks if two dates are equal - * @param {Date | string} date1 - First date - * @param {Date | string} date2 - Second date - * @returns {boolean} True if dates are equal - * @example - * checkIfDatesAreEqual("2024-01-01", "2024-01-01") // returns true + * @returns {boolean} boolean value depending on whether the dates are equal + * @description Returns boolean value depending on whether the dates are equal + * @param date1 + * @param date2 + * @example checkIfDatesAreEqual("2024-01-01", "2024-01-01") // true + * @example checkIfDatesAreEqual("2024-01-01", "2024-01-02") // false */ export const checkIfDatesAreEqual = ( date1: Date | string | null | undefined, @@ -208,101 +261,115 @@ export const checkIfDatesAreEqual = ( ): boolean => { const parsedDate1 = getDate(date1); const parsedDate2 = getDate(date2); + // return if undefined if (!parsedDate1 && !parsedDate2) return true; if (!parsedDate1 || !parsedDate2) return false; + return isEqual(parsedDate1, parsedDate2); }; /** - * @description Checks if a string matches date format YYYY-MM-DD - * @param {string} date - Date string to check - * @returns {boolean} True if string matches date format - * @example - * isInDateFormat("2024-01-01") // returns true + * This method returns a date from string of type yyyy-mm-dd + * This method is recommended to use instead of new Date() as this does not introduce any timezone offsets + * @param date + * @returns date or undefined */ -export const isInDateFormat = (date: string): boolean => { +export const getDate = (date: string | Date | undefined | null): Date | undefined => { + try { + if (!date || date === "") return; + + if (typeof date !== "string" && !(date instanceof String)) return date; + + const [yearString, monthString, dayString] = date.substring(0, 10).split("-"); + const year = parseInt(yearString); + const month = parseInt(monthString); + const day = parseInt(dayString); + if (!isNumber(year) || !isNumber(month) || !isNumber(day)) return; + + return new Date(year, month - 1, day); + } catch (e) { + return undefined; + } +}; + +export const isInDateFormat = (date: string) => { const datePattern = /^\d{4}-\d{2}-\d{2}$/; return datePattern.test(date); }; /** - * @description Converts date string to ISO format - * @param {string} dateString - Date string to convert - * @returns {string | undefined} ISO date string - * @example - * convertToISODateString("2024-01-01") // returns "2024-01-01T00:00:00.000Z" + * returns the date string in ISO format regardless of the timezone in input date string + * @param dateString + * @returns */ -export const convertToISODateString = (dateString: string | undefined): string | undefined => { +export const convertToISODateString = (dateString: string | undefined) => { if (!dateString) return dateString; + const date = new Date(dateString); return date.toISOString(); }; /** - * @description Converts date string to epoch timestamp - * @param {string} dateString - Date string to convert - * @returns {number | undefined} Epoch timestamp - * @example - * convertToEpoch("2024-01-01") // returns 1704067200000 + * returns the date string in Epoch regardless of the timezone in input date string + * @param dateString + * @returns */ -export const convertToEpoch = (dateString: string | undefined): number | undefined => { - if (!dateString) return undefined; +export const convertToEpoch = (dateString: string | undefined) => { + if (!dateString) return dateString; + const date = new Date(dateString); return date.getTime(); }; /** - * @description Gets current date time in ISO format - * @returns {string} Current date time in ISO format - * @example - * getCurrentDateTimeInISO() // returns "2024-01-01T12:00:00.000Z" + * get current Date time in UTC ISO format + * @returns */ -export const getCurrentDateTimeInISO = (): string => { +export const getCurrentDateTimeInISO = () => { const date = new Date(); return date.toISOString(); }; /** - * @description Converts hours and minutes to total minutes - * @param {number} hours - Number of hours - * @param {number} minutes - Number of minutes - * @returns {number} Total minutes - * @example - * convertHoursMinutesToMinutes(2, 30) // returns 150 + * @description converts hours and minutes to minutes + * @param { number } hours + * @param { number } minutes + * @returns { number } minutes + * @example convertHoursMinutesToMinutes(2, 30) // Output: 150 */ export const convertHoursMinutesToMinutes = (hours: number, minutes: number): number => hours * 60 + minutes; /** - * @description Converts total minutes to hours and minutes - * @param {number} mins - Total minutes - * @returns {{ hours: number; minutes: number }} Hours and minutes - * @example - * convertMinutesToHoursAndMinutes(150) // returns { hours: 2, minutes: 30 } + * @description converts minutes to hours and minutes + * @param { number } mins + * @returns { number, number } hours and minutes + * @example convertMinutesToHoursAndMinutes(150) // Output: { hours: 2, minutes: 30 } */ export const convertMinutesToHoursAndMinutes = (mins: number): { hours: number; minutes: number } => { const hours = Math.floor(mins / 60); const minutes = Math.floor(mins % 60); - return { hours, minutes }; + + return { hours: hours, minutes: minutes }; }; /** - * @description Converts minutes to hours and minutes string - * @param {number} totalMinutes - Total minutes - * @returns {string} Formatted string (e.g., "2h 30m") - * @example - * convertMinutesToHoursMinutesString(150) // returns "2h 30m" + * @description converts minutes to hours and minutes string + * @param { number } totalMinutes + * @returns { string } 0h 0m + * @example convertMinutesToHoursAndMinutes(150) // Output: 2h 10m */ export const convertMinutesToHoursMinutesString = (totalMinutes: number): string => { const { hours, minutes } = convertMinutesToHoursAndMinutes(totalMinutes); + return `${hours ? `${hours}h ` : ``}${minutes ? `${minutes}m ` : ``}`; }; /** - * @description Calculates read time in seconds from word count - * @param {number} wordsCount - Number of words - * @returns {number} Read time in seconds - * @example - * getReadTimeFromWordsCount(400) // returns 120 + * @description calculates the read time for a document using the words count + * @param {number} wordsCount + * @returns {number} total number of seconds + * @example getReadTimeFromWordsCount(400) // Output: 120 + * @example getReadTimeFromWordsCount(100) // Output: 30s */ export const getReadTimeFromWordsCount = (wordsCount: number): number => { const wordsPerMinute = 200; @@ -311,29 +378,104 @@ export const getReadTimeFromWordsCount = (wordsCount: number): number => { }; /** - * @description Generates array of dates between start and end dates - * @param {string | Date} startDate - Start date - * @param {string | Date} endDate - End date - * @returns {Array<{ date: string }>} Array of dates - * @example - * generateDateArray("2024-01-01", "2024-01-03") - * // returns [{ date: "2024-01-02" }, { date: "2024-01-03" }] + * @description generates an array of dates between the start and end dates + * @param startDate + * @param endDate + * @returns */ -export const generateDateArray = (startDate: string | Date, endDate: string | Date): Array<{ date: string }> => { +export const generateDateArray = (startDate: string | Date, endDate: string | Date) => { + // Convert the start and end dates to Date objects if they aren't already const start = new Date(startDate); + // start.setDate(start.getDate() + 1); const end = new Date(endDate); - end.setDate(end.getDate() + 1); + end.setDate(end.getDate() + 2); + // Create an empty array to store the dates const dateArray = []; + + // Use a while loop to generate dates between the range while (start <= end) { - start.setDate(start.getDate() + 1); + // Push the current date (converted to ISO string for consistency) dateArray.push({ date: new Date(start).toISOString().split("T")[0], }); + // Increment the date by 1 day (86400000 milliseconds) + start.setDate(start.getDate() + 1); } + return dateArray; }; +/** + * Processes relative date strings like "1_weeks", "2_months" etc and returns a Date + * @param value The relative date string (e.g., "1_weeks", "2_months") + * @returns Date object representing the calculated date + */ +export const processRelativeDate = (value: string): Date => { + const [amountStr, unit] = value.split("_"); + const amount = parseInt(amountStr, 10); + if (isNaN(amount)) { + throw new Error(`Invalid relative amount: ${amountStr}`); + } + const date = new Date(); + + switch (unit) { + case "days": + date.setDate(date.getDate() + amount); + break; + case "weeks": + date.setDate(date.getDate() + amount * 7); + break; + case "months": + date.setMonth(date.getMonth() + amount); + break; + default: + throw new Error(`Unsupported time unit: ${unit}`); + } + + return date; +}; + +/** + * Parses a date filter string and returns the comparison type and date + * @param filterValue The date filter string (e.g., "1_weeks;after;fromnow" or "2024-12-01;after") + * @returns Object containing the comparison type and target date + */ +export const parseDateFilter = (filterValue: string): { type: "after" | "before"; date: Date } => { + const parts = filterValue.split(";"); + const dateStr = parts[0]; + const type = parts[1] as "after" | "before"; + + let date: Date; + if (dateStr.includes("_")) { + // Handle relative dates (e.g., "1_weeks;after;fromnow") + date = processRelativeDate(dateStr); + } else { + // Handle absolute dates (e.g., "2024-12-01;after") + date = new Date(dateStr); + } + + return { type, date }; +}; + +/** + * Checks if a date meets the filter criteria + * @param dateToCheck The date to check + * @param filterDate The filter date to compare against + * @param type The type of comparison ('after' or 'before') + * @returns boolean indicating if the date meets the criteria + */ +export const checkDateCriteria = (dateToCheck: Date | null, filterDate: Date, type: "after" | "before"): boolean => { + if (!dateToCheck) return false; + + const checkDate = new Date(dateToCheck); + const normalizedCheck = new Date(checkDate.setHours(0, 0, 0, 0)); + const normalizedFilter = new Date(filterDate.getTime()); + normalizedFilter.setHours(0, 0, 0, 0); + + return type === "after" ? normalizedCheck >= normalizedFilter : normalizedCheck <= normalizedFilter; +}; + /** * Formats merged date range display with smart formatting * - Single date: "Jan 24, 2025" @@ -388,4 +530,4 @@ export const formatDateRange = ( } return ""; -}; \ No newline at end of file +}; diff --git a/web/helpers/distribution-update.helper.ts b/packages/utils/src/distribution-update.ts similarity index 99% rename from web/helpers/distribution-update.helper.ts rename to packages/utils/src/distribution-update.ts index b640bf33c..d085aabdb 100644 --- a/web/helpers/distribution-update.helper.ts +++ b/packages/utils/src/distribution-update.ts @@ -1,12 +1,13 @@ +"use client"; + import { format } from "date-fns"; import get from "lodash/get"; import set from "lodash/set"; // plane imports import { STATE_GROUPS, COMPLETED_STATE_GROUPS } from "@plane/constants"; -// types import { ICycle, IEstimatePoint, IModule, IState, TIssue } from "@plane/types"; // helper -import { getDate } from "./date-time.helper"; +import { getDate } from "./datetime"; export type DistributionObjectUpdate = { id: string; diff --git a/web/helpers/editor.helper.ts b/packages/utils/src/editor.ts similarity index 94% rename from web/helpers/editor.helper.ts rename to packages/utils/src/editor.ts index 17e170493..1bdf3a504 100644 --- a/web/helpers/editor.helper.ts +++ b/packages/utils/src/editor.ts @@ -1,5 +1,5 @@ -// helpers -import { getFileURL } from "@/helpers/file.helper"; +// local imports +import { getFileURL } from "./file"; type TEditorSrcArgs = { assetId: string; diff --git a/packages/utils/src/emoji.ts b/packages/utils/src/emoji.ts index 9b796575a..00272f41a 100644 --- a/packages/utils/src/emoji.ts +++ b/packages/utils/src/emoji.ts @@ -1,3 +1,8 @@ +"use client"; + +// plane imports +import { LUCIDE_ICONS_LIST, RANDOM_EMOJI_CODES } from "@plane/constants"; + /** * Converts a hyphen-separated hexadecimal emoji code to its decimal representation * @param {string} emojiUnified - The unified emoji code in hexadecimal format (e.g., "1f600" or "1f1e6-1f1e8") @@ -41,24 +46,46 @@ export const emojiCodeToUnicode = (emoji: string): string => { /** * Groups reactions by a specified key - * @param {T[]} reactions - Array of reaction objects + * @param {any[]} reactions - Array of reaction objects * @param {string} key - Key to group reactions by * @returns {Object} Object with reactions grouped by the specified key - * @example - * const reactions = [{ reaction: "👍", id: 1 }, { reaction: "👍", id: 2 }, { reaction: "❤️", id: 3 }]; - * groupReactions(reactions, "reaction") // returns { "👍": [{ reaction: "👍", id: 1 }, { reaction: "👍", id: 2 }], "❤️": [{ reaction: "❤️", id: 3 }] } */ -export const groupReactions = (reactions: T[], key: string): { [key: string]: T[] } => { +export const groupReactions: (reactions: any[], key: string) => { [key: string]: any[] } = ( + reactions: any, + key: string +) => { + if (!Array.isArray(reactions)) { + console.error("Expected an array of reactions, but got:", reactions); + return {}; + } + const groupedReactions = reactions.reduce( - (acc: { [key: string]: T[] }, reaction: T) => { - if (!acc[reaction[key as keyof T] as string]) { - acc[reaction[key as keyof T] as string] = []; + (acc: any, reaction: any) => { + if (!reaction || typeof reaction !== "object" || !Object.prototype.hasOwnProperty.call(reaction, key)) { + console.warn("Skipping undefined reaction or missing key:", reaction); + return acc; // Skip undefined reactions or those without the specified key } - acc[reaction[key as keyof T] as string].push(reaction); + + if (!acc[reaction[key]]) { + acc[reaction[key]] = []; + } + acc[reaction[key]].push(reaction); return acc; }, - {} as { [key: string]: T[] } + {} as { [key: string]: any[] } ); return groupedReactions; }; + +/** + * Returns a random emoji code from the RANDOM_EMOJI_CODES array + * @returns {string} A random emoji code + */ +export const getRandomEmoji = (): string => RANDOM_EMOJI_CODES[Math.floor(Math.random() * RANDOM_EMOJI_CODES.length)]; + +/** + * Returns a random icon name from the LUCIDE_ICONS_LIST array + */ +export const getRandomIconName = (): string => + LUCIDE_ICONS_LIST[Math.floor(Math.random() * LUCIDE_ICONS_LIST.length)].name; diff --git a/web/helpers/estimates.ts b/packages/utils/src/estimates.ts similarity index 94% rename from web/helpers/estimates.ts rename to packages/utils/src/estimates.ts index 62676503d..0a9b37728 100644 --- a/web/helpers/estimates.ts +++ b/packages/utils/src/estimates.ts @@ -1,5 +1,5 @@ // plane web constants -import { EEstimateSystem } from "@/plane-web/constants/estimates"; +import { EEstimateSystem } from "@plane/constants"; export const isEstimatePointValuesRepeated = ( estimatePoints: string[], diff --git a/packages/utils/src/file.ts b/packages/utils/src/file.ts index 42b52bf48..d119ecc18 100644 --- a/packages/utils/src/file.ts +++ b/packages/utils/src/file.ts @@ -1,3 +1,4 @@ +// plane imports import { API_BASE_URL } from "@plane/constants"; import { TFileMetaDataLite, TFileSignedURLResponse } from "@plane/types"; @@ -47,3 +48,66 @@ export const getAssetIdFromUrl = (src: string): string => { const assetUrl = sourcePaths[sourcePaths.length - 1]; return assetUrl; }; + +/** + * @description encode image via URL to base64 + * @param {string} url + * @returns + */ +export const getBase64Image = async (url: string): Promise => { + if (!url || typeof url !== "string") { + throw new Error("Invalid URL provided"); + } + + // Try to create a URL object to validate the URL + try { + new URL(url); + } catch { + throw new Error("Invalid URL format"); + } + + const response = await fetch(url); + // check if the response is OK + if (!response.ok) { + throw new Error(`Failed to fetch image: ${response.statusText}`); + } + + const blob = await response.blob(); + return new Promise((resolve, reject) => { + const reader = new FileReader(); + + reader.onloadend = () => { + if (reader.result) { + resolve(reader.result as string); + } else { + reject(new Error("Failed to convert image to base64.")); + } + }; + + reader.onerror = () => { + reject(new Error("Failed to read the image file.")); + }; + + reader.readAsDataURL(blob); + }); +}; + +/** + * @description downloads a CSV file + * @param {Array> | { [key: string]: string }} data - The data to be exported to CSV + * @param {string} name - The name of the file to be downloaded + */ +export const csvDownload = (data: Array> | { [key: string]: string }, name: string) => { + const rows = Array.isArray(data) ? [...data] : [Object.keys(data), Object.values(data)]; + + const csvContent = "data:text/csv;charset=utf-8," + rows.map((e) => e.join(",")).join("\n"); + const encodedUri = encodeURI(csvContent); + + const link = document.createElement("a"); + link.href = encodedUri; + link.download = `${name}.csv`; + + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); +}; diff --git a/web/helpers/filter.helper.ts b/packages/utils/src/filter.ts similarity index 95% rename from web/helpers/filter.helper.ts rename to packages/utils/src/filter.ts index 403aea4bf..4052d3477 100644 --- a/web/helpers/filter.helper.ts +++ b/packages/utils/src/filter.ts @@ -1,8 +1,8 @@ import { differenceInCalendarDays } from "date-fns/differenceInCalendarDays"; -// helpers +// plane imports import { IIssueFilters } from "@plane/types"; -import { getDate } from "./date-time.helper"; -// import { IIssueFilterOptions } from "@plane/types"; +// local imports +import { getDate } from "./datetime"; /** * @description calculates the total number of filters applied @@ -21,6 +21,7 @@ export const calculateTotalFilters = (filters: T): number => }) .reduce((curr, prev) => curr + prev, 0) : 0; + /** * @description checks if the date satisfies the filter * @param {Date} date diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 30f06b8c4..1e6eb687e 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -1,17 +1,30 @@ export * from "./array"; export * from "./attachment"; export * from "./auth"; +export * from "./calendar"; export * from "./color"; export * from "./common"; +export * from "./cycle"; export * from "./datetime"; +export * from "./distribution-update"; +export * from "./editor"; export * from "./emoji"; +export * from "./estimates"; export * from "./file"; +export * from "./filter"; export * from "./get-icon-for-link"; -export * from "./issue"; +export * from "./intake"; +export * from "./math"; +export * from "./module"; +export * from "./notification"; +export * from "./page"; export * from "./permission"; -export * from "./state"; +export * from "./project"; +export * from "./project-views"; +export * from "./router"; export * from "./string"; export * from "./subscription"; +export * from "./tab-indices"; export * from "./theme"; export * from "./work-item"; -export * from "./workspace"; \ No newline at end of file +export * from "./workspace"; diff --git a/web/helpers/inbox.helper.ts b/packages/utils/src/intake.ts similarity index 57% rename from web/helpers/inbox.helper.ts rename to packages/utils/src/intake.ts index 52a85b0ba..12e59f9ac 100644 --- a/web/helpers/inbox.helper.ts +++ b/packages/utils/src/intake.ts @@ -1,25 +1,8 @@ import { subDays } from "date-fns"; -import { renderFormattedPayloadDate } from "./date-time.helper"; - -export enum EInboxIssueCurrentTab { - OPEN = "open", - CLOSED = "closed", -} - -export enum EInboxIssueStatus { - PENDING = -2, - DECLINED = -1, - SNOOZED = 0, - ACCEPTED = 1, - DUPLICATE = 2, -} - -export enum EPastDurationFilters { - TODAY = "today", - YESTERDAY = "yesterday", - LAST_7_DAYS = "last_7_days", - LAST_30_DAYS = "last_30_days", -} +// plane imports +import { EPastDurationFilters } from "@plane/constants"; +// local imports +import { renderFormattedPayloadDate } from "./datetime"; export const getCustomDates = (duration: EPastDurationFilters): string => { const today = new Date(); @@ -49,25 +32,3 @@ export const getCustomDates = (duration: EPastDurationFilters): string => { } } }; - -export const PAST_DURATION_FILTER_OPTIONS: { - name: string; - value: string; -}[] = [ - { - name: "Today", - value: EPastDurationFilters.TODAY, - }, - { - name: "Yesterday", - value: EPastDurationFilters.YESTERDAY, - }, - { - name: "Last 7 days", - value: EPastDurationFilters.LAST_7_DAYS, - }, - { - name: "Last 30 days", - value: EPastDurationFilters.LAST_30_DAYS, - }, -]; diff --git a/packages/utils/src/issue.ts b/packages/utils/src/issue.ts deleted file mode 100644 index 0fc5d5261..000000000 --- a/packages/utils/src/issue.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { differenceInCalendarDays } from "date-fns/differenceInCalendarDays"; -import { ISSUE_PRIORITY_FILTERS, STATE_GROUPS, TIssuePriorities, TIssueFilterPriorityObject } from "@plane/constants"; -import { TStateGroups } from "@plane/types"; -import { getDate } from "./datetime"; - -export const getIssuePriorityFilters = (priorityKey: TIssuePriorities): TIssueFilterPriorityObject | undefined => { - const currentIssuePriority: TIssueFilterPriorityObject | undefined = - ISSUE_PRIORITY_FILTERS && ISSUE_PRIORITY_FILTERS.length > 0 - ? ISSUE_PRIORITY_FILTERS.find((_priority) => _priority.key === priorityKey) - : undefined; - - if (currentIssuePriority) return currentIssuePriority; - return undefined; -}; - -/** - * @description check if the issue due date should be highlighted - * @param date - * @param stateGroup - * @returns boolean - */ -export const shouldHighlightIssueDueDate = ( - date: string | Date | null, - stateGroup: TStateGroups | undefined -): boolean => { - if (!date || !stateGroup) return false; - // if the issue is completed or cancelled, don't highlight the due date - if ([STATE_GROUPS.completed.key, STATE_GROUPS.cancelled.key].includes(stateGroup)) return false; - - const parsedDate = getDate(date); - if (!parsedDate) return false; - - const targetDateDistance = differenceInCalendarDays(parsedDate, new Date()); - - // if the issue is overdue, highlight the due date - return targetDateDistance <= 0; -}; diff --git a/packages/utils/src/math.ts b/packages/utils/src/math.ts new file mode 100644 index 000000000..88e4040a0 --- /dev/null +++ b/packages/utils/src/math.ts @@ -0,0 +1,2 @@ +export const getProgress = (completed: number | undefined, total: number | undefined) => + total && total > 0 ? Math.round(((completed ?? 0) / total) * 100) : 0; diff --git a/web/helpers/module.helper.ts b/packages/utils/src/module.ts similarity index 96% rename from web/helpers/module.helper.ts rename to packages/utils/src/module.ts index 456cdfc8b..b1e9f314d 100644 --- a/web/helpers/module.helper.ts +++ b/packages/utils/src/module.ts @@ -1,8 +1,9 @@ import sortBy from "lodash/sortBy"; +// plane imports import { IModule, TModuleDisplayFilters, TModuleFilters, TModuleOrderByOptions } from "@plane/types"; -// helpers -import { getDate } from "@/helpers/date-time.helper"; -import { satisfiesDateFilter } from "@/helpers/filter.helper"; +// local imports +import { getDate } from "./datetime"; +import { satisfiesDateFilter } from "./filter"; /** * @description orders modules based on their status diff --git a/web/helpers/notification.helper.ts b/packages/utils/src/notification.ts similarity index 82% rename from web/helpers/notification.helper.ts rename to packages/utils/src/notification.ts index fbc77d21b..b4bc9cbd9 100644 --- a/web/helpers/notification.helper.ts +++ b/packages/utils/src/notification.ts @@ -1,4 +1,4 @@ -import { stripAndTruncateHTML } from "./string.helper"; +import { stripAndTruncateHTML } from "./string"; export const sanitizeCommentForNotification = (mentionContent: string | undefined) => mentionContent diff --git a/web/helpers/page.helper.ts b/packages/utils/src/page.ts similarity index 95% rename from web/helpers/page.helper.ts rename to packages/utils/src/page.ts index d1b027104..2501bc522 100644 --- a/web/helpers/page.helper.ts +++ b/packages/utils/src/page.ts @@ -1,8 +1,9 @@ import sortBy from "lodash/sortBy"; +// plane imports import { TPage, TPageFilterProps, TPageFiltersSortBy, TPageFiltersSortKey, TPageNavigationTabs } from "@plane/types"; -// helpers -import { getDate } from "@/helpers/date-time.helper"; -import { satisfiesDateFilter } from "@/helpers/filter.helper"; +// local imports +import { getDate } from "./datetime"; +import { satisfiesDateFilter } from "./filter"; /** * @description filters pages based on the page type @@ -83,3 +84,4 @@ export const getPageName = (name: string | undefined) => { if (!name || name.trim() === "") return "Untitled"; return name; }; + diff --git a/packages/utils/src/permission/index.ts b/packages/utils/src/permission/index.ts new file mode 100644 index 000000000..807babea7 --- /dev/null +++ b/packages/utils/src/permission/index.ts @@ -0,0 +1 @@ +export * from "./role"; diff --git a/packages/utils/src/permission.ts b/packages/utils/src/permission/role.ts similarity index 57% rename from packages/utils/src/permission.ts rename to packages/utils/src/permission/role.ts index 781050005..5e6bf02e1 100644 --- a/packages/utils/src/permission.ts +++ b/packages/utils/src/permission/role.ts @@ -1,4 +1,16 @@ -import { EUserPermissions, EUserProjectRoles, EUserWorkspaceRoles } from "@plane/constants"; +// plane imports +import { EUserProjectRoles, EUserWorkspaceRoles, EUserPermissions } from "@plane/constants"; + +export const getUserRole = (role: EUserPermissions | EUserWorkspaceRoles | EUserProjectRoles) => { + switch (role) { + case EUserPermissions.GUEST: + return "GUEST"; + case EUserPermissions.MEMBER: + return "MEMBER"; + case EUserPermissions.ADMIN: + return "ADMIN"; + } +}; type TSupportedRole = EUserPermissions | EUserProjectRoles | EUserWorkspaceRoles; diff --git a/web/helpers/project-views.helpers.ts b/packages/utils/src/project-views.ts similarity index 94% rename from web/helpers/project-views.helpers.ts rename to packages/utils/src/project-views.ts index 0de90196d..5263d7098 100644 --- a/web/helpers/project-views.helpers.ts +++ b/packages/utils/src/project-views.ts @@ -1,9 +1,11 @@ import isNil from "lodash/isNil"; import orderBy from "lodash/orderBy"; +// plane imports +import { SPACE_BASE_PATH, SPACE_BASE_URL } from "@plane/constants"; import { IProjectView, TViewFilterProps, TViewFiltersSortBy, TViewFiltersSortKey } from "@plane/types"; -import { getDate } from "@/helpers/date-time.helper"; -import { SPACE_BASE_PATH, SPACE_BASE_URL } from "./common.helper"; -import { satisfiesDateFilter } from "./filter.helper"; +// local imports +import { getDate } from "./datetime"; +import { satisfiesDateFilter } from "./filter"; /** * order views base on TViewFiltersSortKey @@ -100,4 +102,4 @@ export const getPublishViewLink = (anchor: string | undefined) => { const SPACE_APP_URL = (SPACE_BASE_URL.trim() === "" ? window.location.origin : SPACE_BASE_URL) + SPACE_BASE_PATH; return `${SPACE_APP_URL}/views/${anchor}`; -}; \ No newline at end of file +}; diff --git a/web/helpers/project.helper.ts b/packages/utils/src/project.ts similarity index 93% rename from web/helpers/project.helper.ts rename to packages/utils/src/project.ts index b62054284..7ae60748c 100644 --- a/web/helpers/project.helper.ts +++ b/packages/utils/src/project.ts @@ -1,11 +1,9 @@ import sortBy from "lodash/sortBy"; -// types -import { TProjectDisplayFilters, TProjectFilters, TProjectOrderByOptions } from "@plane/types"; -// helpers -import { getDate } from "@/helpers/date-time.helper"; -import { satisfiesDateFilter } from "@/helpers/filter.helper"; -// plane web imports -import { TProject } from "@/plane-web/types"; +// plane imports +import { TProject, TProjectDisplayFilters, TProjectFilters, TProjectOrderByOptions } from "@plane/types"; +// local imports +import { getDate } from "./datetime"; +import { satisfiesDateFilter } from "./filter"; /** * Updates the sort order of the project. diff --git a/web/helpers/router.helper.ts b/packages/utils/src/router.ts similarity index 100% rename from web/helpers/router.helper.ts rename to packages/utils/src/router.ts diff --git a/packages/utils/src/state.ts b/packages/utils/src/state.ts deleted file mode 100644 index 8d97c39f6..000000000 --- a/packages/utils/src/state.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { STATE_GROUPS } from "@plane/constants"; -import { IState } from "@plane/types"; - -export const sortStates = (states: IState[]) => { - if (!states || states.length === 0) return; - - return states.sort((stateA, stateB) => { - if (stateA.group === stateB.group) { - return stateA.sequence - stateB.sequence; - } - return Object.keys(STATE_GROUPS).indexOf(stateA.group) - Object.keys(STATE_GROUPS).indexOf(stateB.group); - }); -}; diff --git a/packages/utils/src/string.ts b/packages/utils/src/string.ts index d663c49c9..1bb23c81d 100644 --- a/packages/utils/src/string.ts +++ b/packages/utils/src/string.ts @@ -55,22 +55,6 @@ export const createSimilarString = (str: string) => { return shuffled; }; -/** - * @description Copies text to clipboard - * @param {string} text - Text to copy - * @returns {Promise} Promise that resolves when copying is complete - * @example - * await copyTextToClipboard("Hello, World!") // copies "Hello, World!" to clipboard - */ -export const copyTextToClipboard = async (text: string): Promise => { - if (typeof navigator === "undefined") return; - try { - await navigator.clipboard.writeText(text); - } catch (err) { - console.error("Failed to copy text: ", err); - } -}; - /** * @description Copies full URL (origin + path) to clipboard * @param {string} path - URL path to copy @@ -146,39 +130,30 @@ export const objToQueryParams = (obj: any) => { export const capitalizeFirstLetter = (str: string) => str.charAt(0).toUpperCase() + str.slice(1); /** - * @description: This function will remove all the HTML tags from the string + * @description : This function will remove all the HTML tags from the string + * @param {string} htmlString + * @return {string} + * @example : + * const html = "

Some text

"; +const text = stripHTML(html); +console.log(text); // Some text + */ +export const sanitizeHTML = (htmlString: string) => { + const sanitizedText = DOMPurify.sanitize(htmlString, { ALLOWED_TAGS: [] }); // sanitize the string to remove all HTML tags + return sanitizedText.trim(); // trim the string to remove leading and trailing whitespaces +}; + +/** + * @description: This function will remove all the HTML tags from the string and truncate the string to the specified length * @param {string} html + * @param {number} length * @return {string} * @example: * const html = "

Some text

"; - * const text = stripHTML(html); + * const text = stripAndTruncateHTML(html); * console.log(text); // Some text */ -/** - * @description Sanitizes HTML string by removing tags and properly escaping entities - * @param {string} htmlString - HTML string to sanitize - * @returns {string} Sanitized string with escaped HTML entities - * @example - * sanitizeHTML("

Hello & 'world'

") // returns "Hello & 'world'" - */ -export const sanitizeHTML = (htmlString: string) => { - if (!htmlString) return ""; - - // First use DOMPurify to remove all HTML tags while preserving text content - const sanitizedText = DOMPurify.sanitize(htmlString, { - ALLOWED_TAGS: [], - ALLOWED_ATTR: [], - USE_PROFILES: { - html: false, - svg: false, - svgFilters: false, - mathMl: false, - }, - }); - - // Additional escaping for quotes and apostrophes - return sanitizedText.trim().replace(/'/g, "'").replace(/"/g, """); -}; +export const stripAndTruncateHTML = (html: string, length: number = 55) => truncateText(sanitizeHTML(html), length); /** * @returns {boolean} true if email is valid, false otherwise @@ -272,43 +247,74 @@ export const joinWithConjunction = (array: string[], separator: string = ", ", c */ export const ensureUrlHasProtocol = (url: string): string => (url.startsWith("http") ? url : `http://${url}`); -// Browser-only clipboard functions -// let copyTextToClipboard: (text: string) => Promise; +/** + * @returns {boolean} true if searchQuery is substring of text in the same order, false otherwise + * @description Returns true if searchQuery is substring of text in the same order, false otherwise + * @param {string} text string to compare from + * @param {string} searchQuery + * @example substringMatch("hello world", "hlo") => true + * @example substringMatch("hello world", "hoe") => false + */ +export const substringMatch = (text: string, searchQuery: string): boolean => { + try { + let searchIndex = 0; -// if (typeof window !== "undefined") { -// const fallbackCopyTextToClipboard = (text: string) => { -// const textArea = document.createElement("textarea"); -// textArea.value = text; + for (let i = 0; i < text.length; i++) { + if (text[i].toLowerCase() === searchQuery[searchIndex]?.toLowerCase()) searchIndex++; -// // Avoid scrolling to bottom -// textArea.style.top = "0"; -// textArea.style.left = "0"; -// textArea.style.position = "fixed"; + // All characters of searchQuery found in order + if (searchIndex === searchQuery.length) return true; + } -// document.body.appendChild(textArea); -// textArea.focus(); -// textArea.select(); + // Not all characters of searchQuery found in order + return false; + } catch (error) { + return false; + } +}; -// try { -// // FIXME: Even though we are using this as a fallback, execCommand is deprecated 👎. We should find a better way to do this. -// // https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand -// document.execCommand("copy"); -// } catch (err) {} +/** + * @description Copies text to clipboard + * @param {string} text - Text to copy + * @returns {Promise} Promise that resolves when copying is complete + * @example + * await copyTextToClipboard("Hello, World!") // copies "Hello, World!" to clipboard + */ +const fallbackCopyTextToClipboard = (text: string) => { + const textArea = document.createElement("textarea"); + textArea.value = text; -// document.body.removeChild(textArea); -// }; + // Avoid scrolling to bottom + textArea.style.top = "0"; + textArea.style.left = "0"; + textArea.style.position = "fixed"; -// copyTextToClipboard = async (text: string) => { -// if (!navigator.clipboard) { -// fallbackCopyTextToClipboard(text); -// return; -// } -// await navigator.clipboard.writeText(text); -// }; -// } else { -// copyTextToClipboard = async () => { -// throw new Error("copyTextToClipboard is only available in browser environments"); -// }; -// } + document.body.appendChild(textArea); + textArea.focus(); + textArea.select(); -// export { copyTextToClipboard }; + try { + // FIXME: Even though we are using this as a fallback, execCommand is deprecated 👎. We should find a better way to do this. + // https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand + document.execCommand("copy"); + } catch (err) { + // catch fallback error + } + + document.body.removeChild(textArea); +}; + +/** + * @description Copies text to clipboard + * @param {string} text - Text to copy + * @returns {Promise} Promise that resolves when copying is complete + * @example + * await copyTextToClipboard("Hello, World!") // copies "Hello, World!" to clipboard + */ +export const copyTextToClipboard = async (text: string): Promise => { + if (!navigator.clipboard) { + fallbackCopyTextToClipboard(text); + return; + } + await navigator.clipboard.writeText(text); +}; diff --git a/web/helpers/tab-indices.helper.ts b/packages/utils/src/tab-indices.ts similarity index 95% rename from web/helpers/tab-indices.helper.ts rename to packages/utils/src/tab-indices.ts index fcf7ad391..ea30903c9 100644 --- a/web/helpers/tab-indices.helper.ts +++ b/packages/utils/src/tab-indices.ts @@ -1,3 +1,4 @@ +// plane imports import { ETabIndices, TAB_INDEX_MAP } from "@plane/constants"; export const getTabIndex = (type?: ETabIndices, isMobile: boolean = false) => { diff --git a/packages/utils/src/theme.ts b/packages/utils/src/theme.ts index 1f2c22b02..487ceaccd 100644 --- a/packages/utils/src/theme.ts +++ b/packages/utils/src/theme.ts @@ -1,2 +1,124 @@ +// local imports +import { TRgb, hexToRgb } from "./color"; + +type TShades = { + 10: TRgb; + 20: TRgb; + 30: TRgb; + 40: TRgb; + 50: TRgb; + 60: TRgb; + 70: TRgb; + 80: TRgb; + 90: TRgb; + 100: TRgb; + 200: TRgb; + 300: TRgb; + 400: TRgb; + 500: TRgb; + 600: TRgb; + 700: TRgb; + 800: TRgb; + 900: TRgb; +}; + +const calculateShades = (hexValue: string): TShades => { + const shades: Partial = {}; + const { r, g, b } = hexToRgb(hexValue); + + const convertHexToSpecificShade = (shade: number): TRgb => { + if (shade <= 100) { + const decimalValue = (100 - shade) / 100; + + const newR = Math.floor(r + (255 - r) * decimalValue); + const newG = Math.floor(g + (255 - g) * decimalValue); + const newB = Math.floor(b + (255 - b) * decimalValue); + + return { + r: newR, + g: newG, + b: newB, + }; + } else { + const decimalValue = 1 - Math.ceil((shade - 100) / 100) / 10; + + const newR = Math.ceil(r * decimalValue); + const newG = Math.ceil(g * decimalValue); + const newB = Math.ceil(b * decimalValue); + + return { + r: newR, + g: newG, + b: newB, + }; + } + }; + + for (let i = 10; i <= 900; i >= 100 ? (i += 100) : (i += 10)) + shades[i as keyof TShades] = convertHexToSpecificShade(i); + + return shades as TShades; +}; + +export const applyTheme = (palette: string, isDarkPalette: boolean) => { + if (!palette) return; + const themeElement = document?.querySelector("html"); + // palette: [bg, text, primary, sidebarBg, sidebarText] + const values: string[] = palette.split(","); + values.push(isDarkPalette ? "dark" : "light"); + + const bgShades = calculateShades(values[0]); + const textShades = calculateShades(values[1]); + const primaryShades = calculateShades(values[2]); + const sidebarBackgroundShades = calculateShades(values[3]); + const sidebarTextShades = calculateShades(values[4]); + + for (let i = 10; i <= 900; i >= 100 ? (i += 100) : (i += 10)) { + const shade = i as keyof TShades; + + const bgRgbValues = `${bgShades[shade].r}, ${bgShades[shade].g}, ${bgShades[shade].b}`; + const textRgbValues = `${textShades[shade].r}, ${textShades[shade].g}, ${textShades[shade].b}`; + const primaryRgbValues = `${primaryShades[shade].r}, ${primaryShades[shade].g}, ${primaryShades[shade].b}`; + const sidebarBackgroundRgbValues = `${sidebarBackgroundShades[shade].r}, ${sidebarBackgroundShades[shade].g}, ${sidebarBackgroundShades[shade].b}`; + const sidebarTextRgbValues = `${sidebarTextShades[shade].r}, ${sidebarTextShades[shade].g}, ${sidebarTextShades[shade].b}`; + + themeElement?.style.setProperty(`--color-background-${shade}`, bgRgbValues); + themeElement?.style.setProperty(`--color-text-${shade}`, textRgbValues); + themeElement?.style.setProperty(`--color-primary-${shade}`, primaryRgbValues); + themeElement?.style.setProperty(`--color-sidebar-background-${shade}`, sidebarBackgroundRgbValues); + themeElement?.style.setProperty(`--color-sidebar-text-${shade}`, sidebarTextRgbValues); + + if (i >= 100 && i <= 400) { + const borderShade = i === 100 ? 70 : i === 200 ? 80 : i === 300 ? 90 : 100; + + themeElement?.style.setProperty( + `--color-border-${shade}`, + `${bgShades[borderShade].r}, ${bgShades[borderShade].g}, ${bgShades[borderShade].b}` + ); + themeElement?.style.setProperty( + `--color-sidebar-border-${shade}`, + `${sidebarBackgroundShades[borderShade].r}, ${sidebarBackgroundShades[borderShade].g}, ${sidebarBackgroundShades[borderShade].b}` + ); + } + } + + themeElement?.style.setProperty("--color-scheme", values[5]); +}; + +export const unsetCustomCssVariables = () => { + for (let i = 10; i <= 900; i >= 100 ? (i += 100) : (i += 10)) { + const dom = document.querySelector("[data-theme='custom']"); + + dom?.style.removeProperty(`--color-background-${i}`); + dom?.style.removeProperty(`--color-text-${i}`); + dom?.style.removeProperty(`--color-border-${i}`); + dom?.style.removeProperty(`--color-primary-${i}`); + dom?.style.removeProperty(`--color-sidebar-background-${i}`); + dom?.style.removeProperty(`--color-sidebar-text-${i}`); + dom?.style.removeProperty(`--color-sidebar-border-${i}`); + dom?.style.removeProperty("--color-scheme"); + } +}; + export const resolveGeneralTheme = (resolvedTheme: string | undefined) => resolvedTheme?.includes("light") ? "light" : resolvedTheme?.includes("dark") ? "dark" : "system"; diff --git a/web/helpers/issue.helper.ts b/packages/utils/src/work-item/base.ts similarity index 92% rename from web/helpers/issue.helper.ts rename to packages/utils/src/work-item/base.ts index 7c73cb599..9c37605e0 100644 --- a/web/helpers/issue.helper.ts +++ b/packages/utils/src/work-item/base.ts @@ -1,12 +1,19 @@ import { differenceInCalendarDays } from "date-fns/differenceInCalendarDays"; import isEmpty from "lodash/isEmpty"; import { v4 as uuidv4 } from "uuid"; -// plane constants -import { EIssueLayoutTypes, ISSUE_DISPLAY_FILTERS_BY_PAGE, STATE_GROUPS } from "@plane/constants"; -// types +// plane imports +import { + EIssueLayoutTypes, + ISSUE_DISPLAY_FILTERS_BY_PAGE, + STATE_GROUPS, + TIssuePriorities, + ISSUE_PRIORITY_FILTERS, + TIssueFilterPriorityObject, +} from "@plane/constants"; import { IIssueDisplayFilterOptions, IIssueDisplayProperties, + IGanttBlock, TGroupedIssues, TIssue, TIssueGroupByOptions, @@ -16,11 +23,10 @@ import { TSubGroupedIssues, TUnGroupedIssues, } from "@plane/types"; -import { IGanttBlock } from "@/components/gantt-chart"; -// helpers -import { orderArrayBy } from "@/helpers/array.helper"; -import { getDate } from "@/helpers/date-time.helper"; -import { isEditorEmpty } from "@/helpers/editor.helper"; +// local imports +import { orderArrayBy } from "../array"; +import { getDate } from "../datetime"; +import { isEditorEmpty } from "../editor"; type THandleIssuesMutation = ( formData: Partial, @@ -171,6 +177,7 @@ export const shouldHighlightIssueDueDate = ( // if the issue is overdue, highlight the due date return targetDateDistance <= 0; }; + export const getIssueBlocksStructure = (block: TIssue): IGanttBlock => ({ data: block, id: block?.id, @@ -333,3 +340,13 @@ export const generateWorkItemLink = ({ return isArchived ? archiveIssueLink : isEpic ? epicLink : workItemLink; }; + +export const getIssuePriorityFilters = (priorityKey: TIssuePriorities): TIssueFilterPriorityObject | undefined => { + const currentIssuePriority: TIssueFilterPriorityObject | undefined = + ISSUE_PRIORITY_FILTERS && ISSUE_PRIORITY_FILTERS.length > 0 + ? ISSUE_PRIORITY_FILTERS.find((_priority) => _priority.key === priorityKey) + : undefined; + + if (currentIssuePriority) return currentIssuePriority; + return undefined; +}; diff --git a/packages/utils/src/work-item/index.ts b/packages/utils/src/work-item/index.ts index 031608e25..002d393b1 100644 --- a/packages/utils/src/work-item/index.ts +++ b/packages/utils/src/work-item/index.ts @@ -1 +1,3 @@ +export * from "./base"; export * from "./modal"; +export * from "./state"; diff --git a/packages/utils/src/work-item/modal.ts b/packages/utils/src/work-item/modal.ts index c70d7591f..74bb1063b 100644 --- a/packages/utils/src/work-item/modal.ts +++ b/packages/utils/src/work-item/modal.ts @@ -1,3 +1,4 @@ +import set from "lodash/set"; // plane imports import { DEFAULT_WORK_ITEM_FORM_VALUES } from "@plane/constants"; import { IPartialProject, ISearchIssueResponse, IState, TIssue } from "@plane/types"; @@ -31,3 +32,17 @@ export const convertWorkItemDataToSearchResponse = ( state__name: state?.name ?? "", workspace__slug: workspaceSlug, }); + + +export function getChangedIssuefields(formData: Partial, dirtyFields: { [key: string]: boolean | undefined }) { + const changedFields = {}; + + const dirtyFieldKeys = Object.keys(dirtyFields) as (keyof TIssue)[]; + for (const dirtyField of dirtyFieldKeys) { + if (!!dirtyFields[dirtyField]) { + set(changedFields, [dirtyField], formData[dirtyField]); + } + } + + return changedFields as Partial; +} diff --git a/web/helpers/state.helper.ts b/packages/utils/src/work-item/state.ts similarity index 99% rename from web/helpers/state.helper.ts rename to packages/utils/src/work-item/state.ts index 7d0b9de38..8a123d7e9 100644 --- a/web/helpers/state.helper.ts +++ b/packages/utils/src/work-item/state.ts @@ -1,6 +1,5 @@ // plane imports import { STATE_GROUPS, TDraggableData } from "@plane/constants"; -// types import { IState, IStateResponse } from "@plane/types"; export const orderStateGroups = (unorderedStateGroups: IStateResponse | undefined): IStateResponse | undefined => { diff --git a/space/core/components/account/auth-forms/email.tsx b/space/core/components/account/auth-forms/email.tsx index 97f7c7b67..4815fef6a 100644 --- a/space/core/components/account/auth-forms/email.tsx +++ b/space/core/components/account/auth-forms/email.tsx @@ -9,7 +9,7 @@ import { IEmailCheckData } from "@plane/types"; // ui import { Button, Input, Spinner } from "@plane/ui"; // helpers -import { cn } from "@/helpers/common.helper"; +import { cn } from "@plane/utils"; import { checkEmailValidity } from "@/helpers/string.helper"; type TAuthEmailForm = { diff --git a/space/core/components/account/helpers/password-strength-meter.tsx b/space/core/components/account/helpers/password-strength-meter.tsx index 342f77efb..611067355 100644 --- a/space/core/components/account/helpers/password-strength-meter.tsx +++ b/space/core/components/account/helpers/password-strength-meter.tsx @@ -3,7 +3,7 @@ import { FC, useMemo } from "react"; // import { CircleCheck } from "lucide-react"; // helpers -import { cn } from "@/helpers/common.helper"; +import { cn } from "@plane/utils"; import { E_PASSWORD_STRENGTH, // PASSWORD_CRITERIA, diff --git a/space/core/components/common/project-logo.tsx b/space/core/components/common/project-logo.tsx index dfb3a4b80..2dfc04b38 100644 --- a/space/core/components/common/project-logo.tsx +++ b/space/core/components/common/project-logo.tsx @@ -1,7 +1,7 @@ // types import { TLogoProps } from "@plane/types"; // helpers -import { cn } from "@/helpers/common.helper"; +import { cn } from "@plane/utils"; type Props = { className?: string; diff --git a/space/core/components/editor/embeds/mentions/user.tsx b/space/core/components/editor/embeds/mentions/user.tsx index 5a178396b..eb3698a98 100644 --- a/space/core/components/editor/embeds/mentions/user.tsx +++ b/space/core/components/editor/embeds/mentions/user.tsx @@ -1,6 +1,6 @@ import { observer } from "mobx-react"; // helpers -import { cn } from "@/helpers/common.helper"; +import { cn } from "@plane/utils"; // hooks import { useMember, useUser } from "@/hooks/store"; diff --git a/space/core/components/editor/lite-text-editor.tsx b/space/core/components/editor/lite-text-editor.tsx index 6c6a19641..5c62d14f8 100644 --- a/space/core/components/editor/lite-text-editor.tsx +++ b/space/core/components/editor/lite-text-editor.tsx @@ -2,10 +2,10 @@ import React from "react"; // plane imports import { EditorRefApi, ILiteTextEditor, LiteTextEditorWithRef, TFileHandler } from "@plane/editor"; import { MakeOptional } from "@plane/types"; +import { cn } from "@plane/utils"; // components import { EditorMentionsRoot, IssueCommentToolbar } from "@/components/editor"; // helpers -import { cn } from "@/helpers/common.helper"; import { getEditorFileHandlers } from "@/helpers/editor.helper"; import { isCommentEmpty } from "@/helpers/string.helper"; diff --git a/space/core/components/editor/lite-text-read-only-editor.tsx b/space/core/components/editor/lite-text-read-only-editor.tsx index acb5cf14d..45c7fb612 100644 --- a/space/core/components/editor/lite-text-read-only-editor.tsx +++ b/space/core/components/editor/lite-text-read-only-editor.tsx @@ -2,10 +2,10 @@ import React from "react"; // plane imports import { EditorReadOnlyRefApi, ILiteTextReadOnlyEditor, LiteTextReadOnlyEditorWithRef } from "@plane/editor"; import { MakeOptional } from "@plane/types"; +import { cn } from "@plane/utils"; // components import { EditorMentionsRoot } from "@/components/editor"; // helpers -import { cn } from "@/helpers/common.helper"; import { getReadOnlyEditorFileHandlers } from "@/helpers/editor.helper"; // store hooks import { useMember } from "@/hooks/store"; diff --git a/space/core/components/editor/rich-text-read-only-editor.tsx b/space/core/components/editor/rich-text-read-only-editor.tsx index f2d386629..7a39760df 100644 --- a/space/core/components/editor/rich-text-read-only-editor.tsx +++ b/space/core/components/editor/rich-text-read-only-editor.tsx @@ -2,10 +2,10 @@ import React from "react"; // plane imports import { EditorReadOnlyRefApi, IRichTextReadOnlyEditor, RichTextReadOnlyEditorWithRef } from "@plane/editor"; import { MakeOptional } from "@plane/types"; +import { cn } from "@plane/utils"; // components import { EditorMentionsRoot } from "@/components/editor"; // helpers -import { cn } from "@/helpers/common.helper"; import { getReadOnlyEditorFileHandlers } from "@/helpers/editor.helper"; // store hooks import { useMember } from "@/hooks/store"; diff --git a/space/core/components/editor/toolbar.tsx b/space/core/components/editor/toolbar.tsx index 0d6931af5..e1c457c38 100644 --- a/space/core/components/editor/toolbar.tsx +++ b/space/core/components/editor/toolbar.tsx @@ -1,12 +1,10 @@ "use client"; import React, { useEffect, useState, useCallback } from "react"; -// editor +// plane imports import { TOOLBAR_ITEMS, ToolbarMenuItem, EditorRefApi } from "@plane/editor"; -// ui import { Button, Tooltip } from "@plane/ui"; -// helpers -import { cn } from "@/helpers/common.helper"; +import { cn } from "@plane/utils"; type Props = { executeCommand: (item: ToolbarMenuItem) => void; diff --git a/space/core/components/issues/issue-layouts/kanban/kanban-group.tsx b/space/core/components/issues/issue-layouts/kanban/kanban-group.tsx index fd00d20ba..50e6ba774 100644 --- a/space/core/components/issues/issue-layouts/kanban/kanban-group.tsx +++ b/space/core/components/issues/issue-layouts/kanban/kanban-group.tsx @@ -11,8 +11,7 @@ import { TPaginationData, TLoader, } from "@plane/types"; -// helpers -import { cn } from "@/helpers/common.helper"; +import { cn } from "@plane/utils"; // hooks import { useIntersectionObserver } from "@/hooks/use-intersection-observer"; // diff --git a/space/core/components/issues/issue-layouts/properties/due-date.tsx b/space/core/components/issues/issue-layouts/properties/due-date.tsx index fd4875154..1ab6293a3 100644 --- a/space/core/components/issues/issue-layouts/properties/due-date.tsx +++ b/space/core/components/issues/issue-layouts/properties/due-date.tsx @@ -3,8 +3,8 @@ import { observer } from "mobx-react"; import { CalendarCheck2 } from "lucide-react"; import { Tooltip } from "@plane/ui"; +import { cn } from "@plane/utils"; // helpers -import { cn } from "@/helpers/common.helper"; import { renderFormattedDate } from "@/helpers/date-time.helper"; import { shouldHighlightIssueDueDate } from "@/helpers/issue.helper"; // hooks diff --git a/space/core/components/issues/navbar/user-avatar.tsx b/space/core/components/issues/navbar/user-avatar.tsx index 0d0706478..d33eb2f77 100644 --- a/space/core/components/issues/navbar/user-avatar.tsx +++ b/space/core/components/issues/navbar/user-avatar.tsx @@ -11,8 +11,8 @@ import { Popover, Transition } from "@headlessui/react"; import { API_BASE_URL } from "@plane/constants"; import { AuthService } from "@plane/services"; import { Avatar, Button } from "@plane/ui"; +import { getFileURL } from "@plane/utils"; // helpers -import { getFileURL } from "@/helpers/file.helper"; import { queryParamGenerator } from "@/helpers/query-param-generator"; // hooks import { useUser } from "@/hooks/store"; diff --git a/space/core/components/issues/peek-overview/comment/comment-detail-card.tsx b/space/core/components/issues/peek-overview/comment/comment-detail-card.tsx index 56a71be5a..9adebeb2d 100644 --- a/space/core/components/issues/peek-overview/comment/comment-detail-card.tsx +++ b/space/core/components/issues/peek-overview/comment/comment-detail-card.tsx @@ -6,12 +6,12 @@ import { Menu, Transition } from "@headlessui/react"; // plane imports import { EditorRefApi } from "@plane/editor"; import { TIssuePublicComment } from "@plane/types"; +import { getFileURL } from "@plane/utils"; // components import { LiteTextEditor, LiteTextReadOnlyEditor } from "@/components/editor"; import { CommentReactions } from "@/components/issues/peek-overview"; // helpers import { timeAgo } from "@/helpers/date-time.helper"; -import { getFileURL } from "@/helpers/file.helper"; // hooks import { useIssueDetails, usePublish, useUser } from "@/hooks/store"; import useIsInIframe from "@/hooks/use-is-in-iframe"; diff --git a/space/core/components/issues/peek-overview/comment/comment-reactions.tsx b/space/core/components/issues/peek-overview/comment/comment-reactions.tsx index e285e5a8a..6ffb58952 100644 --- a/space/core/components/issues/peek-overview/comment/comment-reactions.tsx +++ b/space/core/components/issues/peek-overview/comment/comment-reactions.tsx @@ -4,10 +4,11 @@ import React from "react"; import { observer } from "mobx-react"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { Tooltip } from "@plane/ui"; +// plane imports +import { cn } from "@plane/utils"; // ui import { ReactionSelector } from "@/components/ui"; // helpers -import { cn } from "@/helpers/common.helper"; import { groupReactions, renderEmoji } from "@/helpers/emoji.helper"; import { queryParamGenerator } from "@/helpers/query-param-generator"; // hooks diff --git a/space/core/components/issues/peek-overview/issue-properties.tsx b/space/core/components/issues/peek-overview/issue-properties.tsx index 596993bc2..f9390a193 100644 --- a/space/core/components/issues/peek-overview/issue-properties.tsx +++ b/space/core/components/issues/peek-overview/issue-properties.tsx @@ -3,14 +3,13 @@ import { observer } from "mobx-react"; import { useParams } from "next/navigation"; import { CalendarCheck2, Signal } from "lucide-react"; +// plane imports import { useTranslation } from "@plane/i18n"; -// ui import { DoubleCircleIcon, StateGroupIcon, TOAST_TYPE, setToast } from "@plane/ui"; -import { getIssuePriorityFilters } from "@plane/utils"; +import { cn, getIssuePriorityFilters } from "@plane/utils"; // components import { Icon } from "@/components/ui"; // helpers -import { cn } from "@/helpers/common.helper"; import { renderFormattedDate } from "@/helpers/date-time.helper"; import { shouldHighlightIssueDueDate } from "@/helpers/issue.helper"; import { copyTextToClipboard, addSpaceIfCamelCase } from "@/helpers/string.helper"; diff --git a/space/core/components/issues/reactions/issue-vote-reactions.tsx b/space/core/components/issues/reactions/issue-vote-reactions.tsx index 7134c05cf..83f93e0b7 100644 --- a/space/core/components/issues/reactions/issue-vote-reactions.tsx +++ b/space/core/components/issues/reactions/issue-vote-reactions.tsx @@ -3,9 +3,10 @@ import { useState } from "react"; import { observer } from "mobx-react"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; +// plane imports import { Tooltip } from "@plane/ui"; +import { cn } from "@plane/utils"; // helpers -import { cn } from "@/helpers/common.helper"; import { queryParamGenerator } from "@/helpers/query-param-generator"; // hooks import { useIssueDetails, useUser } from "@/hooks/store"; diff --git a/space/core/lib/toast-provider.tsx b/space/core/lib/toast-provider.tsx index 1083cb6af..20a37c3e9 100644 --- a/space/core/lib/toast-provider.tsx +++ b/space/core/lib/toast-provider.tsx @@ -1,11 +1,10 @@ "use client"; import { ReactNode } from "react"; -import { useTheme } from "next-themes" -// ui +import { useTheme } from "next-themes"; +// plane imports import { Toast } from "@plane/ui"; -// helpers -import { resolveGeneralTheme } from "@/helpers/common.helper"; +import { resolveGeneralTheme } from "@plane/utils"; export const ToastProvider = ({ children }: { children: ReactNode }) => { // themes diff --git a/space/helpers/editor.helper.ts b/space/helpers/editor.helper.ts index cde842c72..e63ba8834 100644 --- a/space/helpers/editor.helper.ts +++ b/space/helpers/editor.helper.ts @@ -1,9 +1,8 @@ -// plane internal +// plane imports import { MAX_FILE_SIZE } from "@plane/constants"; import { TFileHandler, TReadOnlyFileHandler } from "@plane/editor"; import { SitesFileService } from "@plane/services"; -// helpers -import { getFileURL } from "@/helpers/file.helper"; +import { getFileURL } from "@plane/utils"; // services const sitesFileService = new SitesFileService(); diff --git a/web/app/(all)/[workspaceSlug]/(projects)/analytics/header.tsx b/web/app/(all)/[workspaceSlug]/(projects)/analytics/header.tsx index 2c3247bd7..b42b9d218 100644 --- a/web/app/(all)/[workspaceSlug]/(projects)/analytics/header.tsx +++ b/web/app/(all)/[workspaceSlug]/(projects)/analytics/header.tsx @@ -4,15 +4,15 @@ import { useEffect } from "react"; import { observer } from "mobx-react"; import { useSearchParams } from "next/navigation"; import { BarChart2, PanelRight } from "lucide-react"; +// plane imports import { useTranslation } from "@plane/i18n"; -// ui import { Breadcrumbs, Header } from "@plane/ui"; +import { cn } from "@plane/utils"; // components -import { BreadcrumbLink } from "@/components/common"; -// helpers -import { cn } from "@/helpers/common.helper"; +import { BreadcrumbLink } from "@/components/common/breadcrumb-link"; // hooks import { useAppTheme } from "@/hooks/store"; + export const WorkspaceAnalyticsHeader = observer(() => { const { t } = useTranslation(); const searchParams = useSearchParams(); diff --git a/web/app/(all)/[workspaceSlug]/(projects)/extended-project-sidebar.tsx b/web/app/(all)/[workspaceSlug]/(projects)/extended-project-sidebar.tsx index b96f008ab..0cd87200c 100644 --- a/web/app/(all)/[workspaceSlug]/(projects)/extended-project-sidebar.tsx +++ b/web/app/(all)/[workspaceSlug]/(projects)/extended-project-sidebar.tsx @@ -8,12 +8,11 @@ import { Plus, Search } from "lucide-react"; import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { setToast, TOAST_TYPE, Tooltip } from "@plane/ui"; -import { cn, copyUrlToClipboard } from "@plane/utils"; +import { cn, copyUrlToClipboard, orderJoinedProjects } from "@plane/utils"; // components import { CreateProjectModal } from "@/components/project"; import { SidebarProjectsListItem } from "@/components/workspace"; // hooks -import { orderJoinedProjects } from "@/helpers/project.helper"; import { useAppTheme, useProject, useUserPermissions } from "@/hooks/store"; import useExtendedSidebarOutsideClickDetector from "@/hooks/use-extended-sidebar-overview-outside-click"; import { TProject } from "@/plane-web/types"; diff --git a/web/app/(all)/[workspaceSlug]/(projects)/profile/[userId]/header.tsx b/web/app/(all)/[workspaceSlug]/(projects)/profile/[userId]/header.tsx index e97b4751f..a0af93738 100644 --- a/web/app/(all)/[workspaceSlug]/(projects)/profile/[userId]/header.tsx +++ b/web/app/(all)/[workspaceSlug]/(projects)/profile/[userId]/header.tsx @@ -10,10 +10,11 @@ import { PROFILE_VIEWER_TAB, PROFILE_ADMINS_TAB, EUserPermissions, EUserPermissi import { useTranslation } from "@plane/i18n"; import { IUserProfileProjectSegregation } from "@plane/types"; import { Breadcrumbs, Header, CustomMenu, UserActivityIcon } from "@plane/ui"; -import { BreadcrumbLink } from "@/components/common"; +import { cn } from "@plane/utils"; // components +import { BreadcrumbLink } from "@/components/common/breadcrumb-link"; import { ProfileIssuesFilter } from "@/components/profile"; -import { cn } from "@/helpers/common.helper"; +// hooks import { useAppTheme, useUser, useUserPermissions } from "@/hooks/store"; type TUserProfileHeader = { diff --git a/web/app/(all)/[workspaceSlug]/(projects)/profile/[userId]/mobile-header.tsx b/web/app/(all)/[workspaceSlug]/(projects)/profile/[userId]/mobile-header.tsx index 49fa5d2e5..60ee2c351 100644 --- a/web/app/(all)/[workspaceSlug]/(projects)/profile/[userId]/mobile-header.tsx +++ b/web/app/(all)/[workspaceSlug]/(projects)/profile/[userId]/mobile-header.tsx @@ -20,10 +20,10 @@ import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOption // ui import { CustomMenu } from "@plane/ui"; // components +import { isIssueFilterActive } from "@plane/utils"; import { DisplayFiltersSelection, FilterSelection, FiltersDropdown, IssueLayoutIcon } from "@/components/issues"; // helpers -import { isIssueFilterActive } from "@/helpers/filter.helper"; // hooks import { useIssues, useLabel } from "@/hooks/store"; diff --git a/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/[cycleId]/page.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/[cycleId]/page.tsx index fbe8ed85d..b26a063ec 100644 --- a/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/[cycleId]/page.tsx +++ b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/[cycleId]/page.tsx @@ -3,13 +3,13 @@ import { observer } from "mobx-react"; import { useParams } from "next/navigation"; // components +import { cn } from "@plane/utils"; import { EmptyState } from "@/components/common"; import { PageHead } from "@/components/core"; import { CycleDetailsSidebar } from "@/components/cycles"; import useCyclesDetails from "@/components/cycles/active-cycle/use-cycles-details"; import { CycleLayoutRoot } from "@/components/issues/issue-layouts"; // helpers -import { cn } from "@/helpers/common.helper"; // hooks import { useCycle, useProject } from "@/hooks/store"; import { useAppRouter } from "@/hooks/use-app-router"; diff --git a/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/header.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/header.tsx index 7d15cd22d..ddbac3f07 100644 --- a/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/header.tsx +++ b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/header.tsx @@ -6,50 +6,45 @@ import Link from "next/link"; import { useParams } from "next/navigation"; // icons import { PanelRight } from "lucide-react"; -// plane constants +// plane imports import { - EIssueLayoutTypes, EIssueFilterType, + EIssueLayoutTypes, EIssuesStoreType, - ISSUE_DISPLAY_FILTERS_BY_PAGE, EUserPermissions, EUserPermissionsLevel, + ISSUE_DISPLAY_FILTERS_BY_PAGE, } from "@plane/constants"; -// i18n import { useTranslation } from "@plane/i18n"; -// types import { ICustomSearchSelectOption, IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, } from "@plane/types"; -// ui -import { Breadcrumbs, Button, ContrastIcon, Tooltip, Header, CustomSearchSelect } from "@plane/ui"; +import { Breadcrumbs, Button, ContrastIcon, CustomSearchSelect, Header, Tooltip } from "@plane/ui"; +import { cn, isIssueFilterActive } from "@plane/utils"; // components import { WorkItemsModal } from "@/components/analytics/work-items/modal"; import { BreadcrumbLink, SwitcherLabel } from "@/components/common"; import { CycleQuickActions } from "@/components/cycles"; import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues"; -// helpers -import { cn } from "@/helpers/common.helper"; -import { isIssueFilterActive } from "@/helpers/filter.helper"; // hooks import { - useEventTracker, + useCommandPalette, useCycle, + useEventTracker, + useIssues, useLabel, useMember, useProject, useProjectState, - useIssues, - useCommandPalette, useUserPermissions, } from "@/hooks/store"; import { useAppRouter } from "@/hooks/use-app-router"; import useLocalStorage from "@/hooks/use-local-storage"; import { usePlatformOS } from "@/hooks/use-platform-os"; -// plane web +// plane web imports import { ProjectBreadcrumb } from "@/plane-web/components/breadcrumbs"; export const CycleIssuesHeader: React.FC = observer(() => { diff --git a/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/mobile-header.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/mobile-header.tsx index 0494f5f33..fc98e941b 100644 --- a/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/mobile-header.tsx +++ b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/mobile-header.tsx @@ -4,7 +4,7 @@ import { useCallback, useState } from "react"; import { useParams } from "next/navigation"; // icons import { Calendar, ChevronDown, Kanban, List } from "lucide-react"; -// plane constants +// plane imports import { EIssueLayoutTypes, EIssueFilterType, @@ -12,17 +12,14 @@ import { ISSUE_LAYOUTS, ISSUE_DISPLAY_FILTERS_BY_PAGE, } from "@plane/constants"; -// i18n import { useTranslation } from "@plane/i18n"; -// types import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types"; -// ui import { CustomMenu } from "@plane/ui"; +import { isIssueFilterActive } from "@plane/utils"; // components import { WorkItemsModal } from "@/components/analytics/work-items/modal"; import { DisplayFiltersSelection, FilterSelection, FiltersDropdown, IssueLayoutIcon } from "@/components/issues"; // helpers -import { isIssueFilterActive } from "@/helpers/filter.helper"; // hooks import { useIssues, useCycle, useProjectState, useLabel, useMember, useProject } from "@/hooks/store"; diff --git a/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(list)/page.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(list)/page.tsx index e23396f4d..edb4e1bcf 100644 --- a/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(list)/page.tsx +++ b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(list)/page.tsx @@ -9,12 +9,12 @@ import { useTranslation } from "@plane/i18n"; import { TCycleFilters } from "@plane/types"; // components import { Header, EHeaderVariant } from "@plane/ui"; -import { PageHead } from "@/components/core"; +import { calculateTotalFilters } from "@plane/utils"; +import { PageHead } from "@/components/core/page-title"; import { CyclesView, CycleCreateUpdateModal, CycleAppliedFiltersList } from "@/components/cycles"; import { ComicBoxButton, DetailedEmptyState } from "@/components/empty-state"; import { CycleModuleListLayout } from "@/components/ui"; // helpers -import { calculateTotalFilters } from "@/helpers/filter.helper"; // hooks import { useEventTracker, useCycle, useProject, useCycleFilter, useUserPermissions } from "@/hooks/store"; import { useAppRouter } from "@/hooks/use-app-router"; diff --git a/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/draft-issues/header.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/draft-issues/header.tsx index 2200b31f1..a7065a1f5 100644 --- a/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/draft-issues/header.tsx +++ b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/draft-issues/header.tsx @@ -12,10 +12,10 @@ import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOption // ui import { Breadcrumbs, LayersIcon, Tooltip } from "@plane/ui"; // components +import { isIssueFilterActive } from "@plane/utils"; import { BreadcrumbLink } from "@/components/common"; import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues"; // helpers -import { isIssueFilterActive } from "@/helpers/filter.helper"; // hooks import { useIssues, useLabel, useMember, useProject, useProjectState } from "@/hooks/store"; import { usePlatformOS } from "@/hooks/use-platform-os"; diff --git a/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/intake/page.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/intake/page.tsx index 5b6ad8b5f..48ab4d38b 100644 --- a/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/intake/page.tsx +++ b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/intake/page.tsx @@ -1,15 +1,14 @@ "use client"; import { observer } from "mobx-react"; -// components import { useParams, useSearchParams } from "next/navigation"; -import { EUserPermissionsLevel } from "@plane/constants"; +import { EUserPermissionsLevel, EInboxIssueCurrentTab } from "@plane/constants"; +// components import { EUserProjectRoles } from "@plane/constants/src/user"; import { useTranslation } from "@plane/i18n"; import { PageHead } from "@/components/core"; import { DetailedEmptyState } from "@/components/empty-state"; import { InboxIssueRoot } from "@/components/inbox"; // helpers -import { EInboxIssueCurrentTab } from "@/helpers/inbox.helper"; // hooks import { useProject, useUserPermissions } from "@/hooks/store"; import { useAppRouter } from "@/hooks/use-app-router"; diff --git a/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(list)/mobile-header.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(list)/mobile-header.tsx index def050462..27a78ed3a 100644 --- a/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(list)/mobile-header.tsx +++ b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(list)/mobile-header.tsx @@ -5,7 +5,7 @@ import { observer } from "mobx-react"; import { useParams } from "next/navigation"; // icons import { Calendar, ChevronDown, Kanban, List } from "lucide-react"; -// plane constants +// plane imports import { EIssueLayoutTypes, EIssueFilterType, @@ -13,12 +13,10 @@ import { ISSUE_LAYOUTS, ISSUE_DISPLAY_FILTERS_BY_PAGE, } from "@plane/constants"; -// i18n import { useTranslation } from "@plane/i18n"; -// types import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types"; -// ui import { CustomMenu } from "@plane/ui"; +import { isIssueFilterActive } from "@plane/utils"; // components import { WorkItemsModal } from "@/components/analytics/work-items/modal"; import { @@ -28,7 +26,6 @@ import { IssueLayoutIcon, } from "@/components/issues/issue-layouts"; // helpers -import { isIssueFilterActive } from "@/helpers/filter.helper"; // hooks import { useIssues, useLabel, useMember, useProject, useProjectState } from "@/hooks/store"; diff --git a/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/[moduleId]/page.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/[moduleId]/page.tsx index d35f31465..db3171307 100644 --- a/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/[moduleId]/page.tsx +++ b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/[moduleId]/page.tsx @@ -4,12 +4,12 @@ import { observer } from "mobx-react"; import { useParams } from "next/navigation"; import useSWR from "swr"; // components +import { cn } from "@plane/utils"; import { EmptyState } from "@/components/common"; import { PageHead } from "@/components/core"; import { ModuleLayoutRoot } from "@/components/issues"; import { ModuleAnalyticsSidebar } from "@/components/modules"; // helpers -import { cn } from "@/helpers/common.helper"; // hooks import { useModule, useProject } from "@/hooks/store"; import { useAppRouter } from "@/hooks/use-app-router"; diff --git a/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/header.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/header.tsx index 0fb2b138f..ee081917c 100644 --- a/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/header.tsx +++ b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/header.tsx @@ -6,7 +6,7 @@ import Link from "next/link"; import { useParams } from "next/navigation"; // icons import { PanelRight } from "lucide-react"; -// plane constants +// plane imports import { EIssueLayoutTypes, EIssuesStoreType, @@ -15,23 +15,20 @@ import { EUserPermissions, EUserPermissionsLevel, } from "@plane/constants"; -// types import { ICustomSearchSelectOption, IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, } from "@plane/types"; -// ui import { Breadcrumbs, Button, DiceIcon, Tooltip, Header, CustomSearchSelect } from "@plane/ui"; +import { cn, isIssueFilterActive } from "@plane/utils"; // components import { WorkItemsModal } from "@/components/analytics/work-items/modal"; import { BreadcrumbLink, SwitcherLabel } from "@/components/common"; import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues"; // helpers import { ModuleQuickActions } from "@/components/modules"; -import { cn } from "@/helpers/common.helper"; -import { isIssueFilterActive } from "@/helpers/filter.helper"; // hooks import { useEventTracker, diff --git a/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/mobile-header.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/mobile-header.tsx index 0501f5527..71d2b78d4 100644 --- a/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/mobile-header.tsx +++ b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/mobile-header.tsx @@ -5,7 +5,7 @@ import { observer } from "mobx-react"; import { useParams } from "next/navigation"; // icons import { Calendar, ChevronDown, Kanban, List } from "lucide-react"; -// plane constants +// plane imports import { EIssueLayoutTypes, EIssueFilterType, @@ -13,12 +13,10 @@ import { ISSUE_LAYOUTS, ISSUE_DISPLAY_FILTERS_BY_PAGE, } from "@plane/constants"; -// plane i18n import { useTranslation } from "@plane/i18n"; -// types import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types"; -// ui import { CustomMenu } from "@plane/ui"; +import { isIssueFilterActive } from "@plane/utils"; // components import { WorkItemsModal } from "@/components/analytics/work-items/modal"; import { @@ -28,7 +26,6 @@ import { IssueLayoutIcon, } from "@/components/issues/issue-layouts"; // helpers -import { isIssueFilterActive } from "@/helpers/filter.helper"; // hooks import { useIssues, useLabel, useMember, useModule, useProject, useProjectState } from "@/hooks/store"; diff --git a/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(list)/page.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(list)/page.tsx index 7c62a4d51..1a2980760 100644 --- a/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(list)/page.tsx +++ b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(list)/page.tsx @@ -8,11 +8,11 @@ import { EUserPermissionsLevel, EUserProjectRoles } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { TModuleFilters } from "@plane/types"; // components -import { PageHead } from "@/components/core"; +import { calculateTotalFilters } from "@plane/utils"; +import { PageHead } from "@/components/core/page-title"; import { DetailedEmptyState } from "@/components/empty-state"; import { ModuleAppliedFiltersList, ModulesListView } from "@/components/modules"; // helpers -import { calculateTotalFilters } from "@/helpers/filter.helper"; // hooks import { useModuleFilter, useProject, useUserPermissions } from "@/hooks/store"; import { useAppRouter } from "@/hooks/use-app-router"; diff --git a/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/header.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/header.tsx index d939c6fe5..da7b90291 100644 --- a/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/header.tsx +++ b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/header.tsx @@ -7,10 +7,10 @@ import { ICustomSearchSelectOption } from "@plane/types"; // ui import { Breadcrumbs, Header, CustomSearchSelect } from "@plane/ui"; // components +import { getPageName } from "@plane/utils"; import { BreadcrumbLink, PageAccessIcon, SwitcherLabel } from "@/components/common"; import { PageHeaderActions } from "@/components/pages/header/actions"; // helpers -import { getPageName } from "@/helpers/page.helper"; // hooks import { useProject } from "@/hooks/store"; // plane web components diff --git a/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(detail)/[viewId]/header.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(detail)/[viewId]/header.tsx index 3bc057479..6634f0efc 100644 --- a/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(detail)/[viewId]/header.tsx +++ b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(detail)/[viewId]/header.tsx @@ -24,12 +24,12 @@ import { // ui import { Breadcrumbs, Button, Tooltip, Header, CustomSearchSelect } from "@plane/ui"; // components +import { isIssueFilterActive } from "@plane/utils"; import { BreadcrumbLink, SwitcherLabel } from "@/components/common"; import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues"; // constants import { ViewQuickActions } from "@/components/views"; // helpers -import { isIssueFilterActive } from "@/helpers/filter.helper"; // hooks import { useCommandPalette, diff --git a/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(list)/page.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(list)/page.tsx index 4e21defc9..d9068966c 100644 --- a/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(list)/page.tsx +++ b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(list)/page.tsx @@ -8,13 +8,13 @@ import { EUserPermissionsLevel, EUserProjectRoles, EViewAccess } from "@plane/co import { useTranslation } from "@plane/i18n"; import { TViewFilterProps } from "@plane/types"; import { Header, EHeaderVariant } from "@plane/ui"; -import { PageHead } from "@/components/core"; +import { calculateTotalFilters } from "@plane/utils"; +import { PageHead } from "@/components/core/page-title"; import { DetailedEmptyState } from "@/components/empty-state"; import { ProjectViewsList } from "@/components/views"; import { ViewAppliedFiltersList } from "@/components/views/applied-filters"; // constants // helpers -import { calculateTotalFilters } from "@/helpers/filter.helper"; // hooks import { useProject, useProjectView, useUserPermissions } from "@/hooks/store"; import { useAppRouter } from "@/hooks/use-app-router"; diff --git a/web/app/(all)/[workspaceSlug]/(projects)/sidebar.tsx b/web/app/(all)/[workspaceSlug]/(projects)/sidebar.tsx index 27a73137e..39d6313ba 100644 --- a/web/app/(all)/[workspaceSlug]/(projects)/sidebar.tsx +++ b/web/app/(all)/[workspaceSlug]/(projects)/sidebar.tsx @@ -5,11 +5,11 @@ import { observer } from "mobx-react"; import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; import { useOutsideClickDetector } from "@plane/hooks"; // components +import { cn } from "@plane/utils"; import { SidebarDropdown, SidebarHelpSection, SidebarProjectsList, SidebarQuickActions } from "@/components/workspace"; import { SidebarFavoritesMenu } from "@/components/workspace/sidebar/favorites/favorites-menu"; import { SidebarMenuItems } from "@/components/workspace/sidebar/sidebar-menu-items"; // helpers -import { cn } from "@/helpers/common.helper"; // hooks import { useAppTheme, useUserPermissions } from "@/hooks/store"; import { useFavorite } from "@/hooks/store/use-favorite"; diff --git a/web/app/(all)/[workspaceSlug]/(projects)/workspace-views/header.tsx b/web/app/(all)/[workspaceSlug]/(projects)/workspace-views/header.tsx index 5a5ee0a6e..1819ac0ee 100644 --- a/web/app/(all)/[workspaceSlug]/(projects)/workspace-views/header.tsx +++ b/web/app/(all)/[workspaceSlug]/(projects)/workspace-views/header.tsx @@ -12,11 +12,11 @@ import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOption // ui import { Breadcrumbs, Button, Header } from "@plane/ui"; // components +import { isIssueFilterActive } from "@plane/utils"; import { BreadcrumbLink } from "@/components/common"; import { DisplayFiltersSelection, FiltersDropdown, FilterSelection } from "@/components/issues"; import { CreateUpdateWorkspaceViewModal } from "@/components/workspace"; // helpers -import { isIssueFilterActive } from "@/helpers/filter.helper"; // hooks import { useLabel, useMember, useIssues, useGlobalView } from "@/hooks/store"; diff --git a/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/exports/page.tsx b/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/exports/page.tsx index 9f08259c6..62aabb10e 100644 --- a/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/exports/page.tsx +++ b/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/exports/page.tsx @@ -4,13 +4,14 @@ import { observer } from "mobx-react"; // components import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; +import { cn } from "@plane/utils"; import { NotAuthorizedView } from "@/components/auth-screens"; import { PageHead } from "@/components/core"; import ExportGuide from "@/components/exporter/guide"; // helpers -import { SettingsContentWrapper, SettingsHeading } from "@/components/settings"; -import { cn } from "@/helpers/common.helper"; // hooks +import { SettingsContentWrapper } from "@/components/settings/content-wrapper"; +import SettingsHeading from "@/components/settings/heading"; import { useUserPermissions, useWorkspace } from "@/hooks/store"; const ExportsPage = observer(() => { diff --git a/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/members/page.tsx b/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/members/page.tsx index 250b5bc02..d2c65340e 100644 --- a/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/members/page.tsx +++ b/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/members/page.tsx @@ -10,6 +10,7 @@ import { useTranslation } from "@plane/i18n"; import { IWorkspaceBulkInviteFormData } from "@plane/types"; // ui import { Button, TOAST_TYPE, setToast } from "@plane/ui"; +import { cn, getUserRole } from "@plane/utils"; // components import { NotAuthorizedView } from "@/components/auth-screens"; import { CountChip } from "@/components/common"; @@ -17,8 +18,6 @@ import { PageHead } from "@/components/core"; import { SettingsContentWrapper } from "@/components/settings"; import { WorkspaceMembersList } from "@/components/workspace"; // helpers -import { cn } from "@/helpers/common.helper"; -import { getUserRole } from "@/helpers/user.helper"; // hooks import { useEventTracker, useMember, useUserPermissions, useWorkspace } from "@/hooks/store"; // plane web components diff --git a/web/app/(all)/[workspaceSlug]/(settings)/settings/account/preferences/page.tsx b/web/app/(all)/[workspaceSlug]/(settings)/settings/account/preferences/page.tsx index 81c37ae4c..87479a2c8 100644 --- a/web/app/(all)/[workspaceSlug]/(settings)/settings/account/preferences/page.tsx +++ b/web/app/(all)/[workspaceSlug]/(settings)/settings/account/preferences/page.tsx @@ -6,10 +6,12 @@ import { useTranslation } from "@plane/i18n"; import { LogoSpinner } from "@/components/common"; import { PageHead } from "@/components/core"; import { PreferencesList } from "@/components/preferences/list"; -import { LanguageTimezone, ProfileSettingContentHeader } from "@/components/profile"; +import { ProfileSettingContentHeader } from "@/components/profile"; // hooks +import { LanguageTimezone } from "@/components/profile/preferences/language-timezone"; import { SettingsHeading } from "@/components/settings"; import { useUserProfile } from "@/hooks/store"; + const ProfileAppearancePage = observer(() => { const { t } = useTranslation(); // hooks diff --git a/web/app/(all)/[workspaceSlug]/(settings)/settings/account/security/page.tsx b/web/app/(all)/[workspaceSlug]/(settings)/settings/account/security/page.tsx index b9cdf9d26..3200959c7 100644 --- a/web/app/(all)/[workspaceSlug]/(settings)/settings/account/security/page.tsx +++ b/web/app/(all)/[workspaceSlug]/(settings)/settings/account/security/page.tsx @@ -4,16 +4,17 @@ import { useState } from "react"; import { observer } from "mobx-react"; import { Controller, useForm } from "react-hook-form"; import { Eye, EyeOff } from "lucide-react"; +// plane imports +import { E_PASSWORD_STRENGTH } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; -// ui import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; +import { getPasswordStrength } from "@plane/utils"; // components import { PasswordStrengthMeter } from "@/components/account"; import { PageHead } from "@/components/core"; import { ProfileSettingContentHeader } from "@/components/profile"; // helpers import { authErrorHandler } from "@/helpers/authentication.helper"; -import { E_PASSWORD_STRENGTH, getPasswordStrength } from "@/helpers/password.helper"; // hooks import { useUser } from "@/hooks/store"; // services diff --git a/web/app/(all)/[workspaceSlug]/(settings)/settings/account/sidebar.tsx b/web/app/(all)/[workspaceSlug]/(settings)/settings/account/sidebar.tsx index 6e495daff..addc59596 100644 --- a/web/app/(all)/[workspaceSlug]/(settings)/settings/account/sidebar.tsx +++ b/web/app/(all)/[workspaceSlug]/(settings)/settings/account/sidebar.tsx @@ -1,6 +1,7 @@ import { observer } from "mobx-react"; import { useParams, usePathname } from "next/navigation"; import { CircleUser, Activity, Bell, CircleUserRound, KeyRound, Settings2, Blocks, Lock } from "lucide-react"; +// plane imports import { EUserPermissions, EUserPermissionsLevel, @@ -8,8 +9,10 @@ import { PROFILE_SETTINGS_CATEGORIES, PROFILE_SETTINGS_CATEGORY, } from "@plane/constants"; +import { getFileURL } from "@plane/utils"; +// components import { SettingsSidebar } from "@/components/settings"; -import { getFileURL } from "@/helpers/file.helper"; +// hooks import { useUser, useUserPermissions } from "@/hooks/store/user"; const ICONS = { diff --git a/web/app/(all)/accounts/forgot-password/page.tsx b/web/app/(all)/accounts/forgot-password/page.tsx index 9ee2cc482..8cf7eae4a 100644 --- a/web/app/(all)/accounts/forgot-password/page.tsx +++ b/web/app/(all)/accounts/forgot-password/page.tsx @@ -12,10 +12,9 @@ import { CircleCheck } from "lucide-react"; import { FORGOT_PASS_LINK, NAVIGATE_TO_SIGNUP } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { Button, Input, TOAST_TYPE, getButtonStyling, setToast } from "@plane/ui"; +import { cn, checkEmailValidity } from "@plane/utils"; // helpers import { EPageTypes } from "@/helpers/authentication.helper"; -import { cn } from "@/helpers/common.helper"; -import { checkEmailValidity } from "@/helpers/string.helper"; // hooks import { useEventTracker, useInstance } from "@/hooks/store"; import useTimer from "@/hooks/use-timer"; diff --git a/web/app/(all)/accounts/reset-password/page.tsx b/web/app/(all)/accounts/reset-password/page.tsx index e0230f205..388e7a02d 100644 --- a/web/app/(all)/accounts/reset-password/page.tsx +++ b/web/app/(all)/accounts/reset-password/page.tsx @@ -9,9 +9,11 @@ import { useSearchParams } from "next/navigation"; import { useTheme } from "next-themes"; import { Eye, EyeOff } from "lucide-react"; // ui +import { API_BASE_URL, E_PASSWORD_STRENGTH } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { Button, Input } from "@plane/ui"; // components +import { getPasswordStrength } from "@plane/utils"; import { AuthBanner, PasswordStrengthMeter } from "@/components/account"; // helpers import { @@ -21,8 +23,6 @@ import { TAuthErrorInfo, authErrorHandler, } from "@/helpers/authentication.helper"; -import { API_BASE_URL } from "@/helpers/common.helper"; -import { E_PASSWORD_STRENGTH, getPasswordStrength } from "@/helpers/password.helper"; // wrappers import { AuthenticationWrapper } from "@/lib/wrappers"; // services diff --git a/web/app/(all)/accounts/set-password/page.tsx b/web/app/(all)/accounts/set-password/page.tsx index 5bfa7c08f..872f965dd 100644 --- a/web/app/(all)/accounts/set-password/page.tsx +++ b/web/app/(all)/accounts/set-password/page.tsx @@ -9,13 +9,14 @@ import { useSearchParams } from "next/navigation"; import { useTheme } from "next-themes"; import { Eye, EyeOff } from "lucide-react"; // plane imports +import { E_PASSWORD_STRENGTH } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // components -import { PasswordStrengthMeter } from "@/components/account"; +import { getPasswordStrength } from "@plane/utils"; +import { PasswordStrengthMeter } from "@/components/account/password-strength-meter"; // helpers import { EPageTypes } from "@/helpers/authentication.helper"; -import { E_PASSWORD_STRENGTH, getPasswordStrength } from "@/helpers/password.helper"; // hooks import { useUser } from "@/hooks/store"; import { useAppRouter } from "@/hooks/use-app-router"; diff --git a/web/app/(all)/invitations/page.tsx b/web/app/(all)/invitations/page.tsx index df6befa68..38d50f2e6 100644 --- a/web/app/(all)/invitations/page.tsx +++ b/web/app/(all)/invitations/page.tsx @@ -15,13 +15,12 @@ import { useTranslation } from "@plane/i18n"; import type { IWorkspaceMemberInvitation } from "@plane/types"; // ui import { Button, TOAST_TYPE, setToast } from "@plane/ui"; +import { truncateText, getUserRole } from "@plane/utils"; // components import { EmptyState } from "@/components/common"; import { WorkspaceLogo } from "@/components/workspace/logo"; import { USER_WORKSPACES_LIST } from "@/constants/fetch-keys"; // helpers -import { truncateText } from "@/helpers/string.helper"; -import { getUserRole } from "@/helpers/user.helper"; // hooks import { useEventTracker, useUser, useUserProfile, useWorkspace } from "@/hooks/store"; import { useAppRouter } from "@/hooks/use-app-router"; diff --git a/web/app/(all)/profile/appearance/page.tsx b/web/app/(all)/profile/appearance/page.tsx index ac5beec37..877dfd511 100644 --- a/web/app/(all)/profile/appearance/page.tsx +++ b/web/app/(all)/profile/appearance/page.tsx @@ -9,11 +9,11 @@ import { useTranslation } from "@plane/i18n"; import { IUserTheme } from "@plane/types"; import { setPromiseToast } from "@plane/ui"; // components +import { applyTheme, unsetCustomCssVariables } from "@plane/utils"; import { LogoSpinner } from "@/components/common"; import { ThemeSwitch, PageHead, CustomThemeSelector } from "@/components/core"; import { ProfileSettingContentHeader, ProfileSettingContentWrapper } from "@/components/profile"; // helpers -import { applyTheme, unsetCustomCssVariables } from "@/helpers/theme.helper"; // hooks import { useUserProfile } from "@/hooks/store"; diff --git a/web/app/(all)/profile/security/page.tsx b/web/app/(all)/profile/security/page.tsx index 8477d70d9..eec52f994 100644 --- a/web/app/(all)/profile/security/page.tsx +++ b/web/app/(all)/profile/security/page.tsx @@ -4,16 +4,17 @@ import { useState } from "react"; import { observer } from "mobx-react"; import { Controller, useForm } from "react-hook-form"; import { Eye, EyeOff } from "lucide-react"; +import { E_PASSWORD_STRENGTH } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; // ui import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // components -import { PasswordStrengthMeter } from "@/components/account"; -import { PageHead } from "@/components/core"; +import { getPasswordStrength } from "@plane/utils"; +import { PasswordStrengthMeter } from "@/components/account/password-strength-meter"; +import { PageHead } from "@/components/core/page-title"; import { ProfileSettingContentHeader, ProfileSettingContentWrapper } from "@/components/profile"; // helpers import { authErrorHandler } from "@/helpers/authentication.helper"; -import { E_PASSWORD_STRENGTH, getPasswordStrength } from "@/helpers/password.helper"; // hooks import { useUser } from "@/hooks/store"; // services diff --git a/web/app/(all)/profile/sidebar.tsx b/web/app/(all)/profile/sidebar.tsx index 0d8539c50..be70e1e13 100644 --- a/web/app/(all)/profile/sidebar.tsx +++ b/web/app/(all)/profile/sidebar.tsx @@ -22,12 +22,11 @@ import { PROFILE_ACTION_LINKS } from "@plane/constants"; import { useOutsideClickDetector } from "@plane/hooks"; import { useTranslation } from "@plane/i18n"; import { TOAST_TYPE, Tooltip, setToast } from "@plane/ui"; +import { cn, getFileURL } from "@plane/utils"; // components import { SidebarNavItem } from "@/components/sidebar"; // constants // helpers -import { cn } from "@/helpers/common.helper"; -import { getFileURL } from "@/helpers/file.helper"; // hooks import { useAppTheme, useUser, useUserSettings, useWorkspace } from "@/hooks/store"; import { usePlatformOS } from "@/hooks/use-platform-os"; diff --git a/web/app/error.tsx b/web/app/error.tsx index 348bc6b64..565c362d5 100644 --- a/web/app/error.tsx +++ b/web/app/error.tsx @@ -1,11 +1,10 @@ "use client"; -// ui import Link from "next/link"; +// plane imports +import { API_BASE_URL } from "@plane/constants"; import { Button, TOAST_TYPE, getButtonStyling, setToast } from "@plane/ui"; -// helpers import { cn } from "@plane/utils"; -import { API_BASE_URL } from "@/helpers/common.helper"; // hooks import { useAppRouter } from "@/hooks/use-app-router"; // layouts @@ -19,10 +18,6 @@ const authService = new AuthService(); export default function CustomErrorComponent() { const router = useAppRouter(); - const handleRefresh = () => { - window.location.reload(); - }; - const handleSignOut = async () => { await authService .signOut(API_BASE_URL) diff --git a/web/app/layout.tsx b/web/app/layout.tsx index 56d6ba2ed..fc454e54b 100644 --- a/web/app/layout.tsx +++ b/web/app/layout.tsx @@ -7,7 +7,7 @@ import "@/styles/globals.css"; import { SITE_DESCRIPTION, SITE_NAME } from "@plane/constants"; // helpers -import { cn } from "@/helpers/common.helper"; +import { cn } from "@plane/utils"; // local import { AppProvider } from "./provider"; diff --git a/web/app/provider.tsx b/web/app/provider.tsx index b120882f7..554bf796d 100644 --- a/web/app/provider.tsx +++ b/web/app/provider.tsx @@ -9,7 +9,7 @@ import { WEB_SWR_CONFIG } from "@plane/constants"; import { TranslationProvider } from "@plane/i18n"; import { Toast } from "@plane/ui"; //helpers -import { resolveGeneralTheme } from "@/helpers/theme.helper"; +import { resolveGeneralTheme } from "@plane/utils"; // nprogress import { AppProgressBar } from "@/lib/n-progress"; // polyfills diff --git a/web/ce/components/active-cycles/workspace-active-cycles-upgrade.tsx b/web/ce/components/active-cycles/workspace-active-cycles-upgrade.tsx index e8a13f036..b82fb019e 100644 --- a/web/ce/components/active-cycles/workspace-active-cycles-upgrade.tsx +++ b/web/ce/components/active-cycles/workspace-active-cycles-upgrade.tsx @@ -9,9 +9,9 @@ import { MARKETING_PRICING_PAGE_LINK } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { ContentWrapper, getButtonStyling } from "@plane/ui"; // components +import { cn } from "@plane/utils"; import { ProIcon } from "@/components/common"; // helper -import { cn } from "@/helpers/common.helper"; // hooks import { useUser } from "@/hooks/store"; diff --git a/web/ce/components/command-palette/helpers.tsx b/web/ce/components/command-palette/helpers.tsx index d846ebfa0..141f65bdd 100644 --- a/web/ce/components/command-palette/helpers.tsx +++ b/web/ce/components/command-palette/helpers.tsx @@ -12,7 +12,7 @@ import { // ui import { ContrastIcon, DiceIcon } from "@plane/ui"; // helpers -import { generateWorkItemLink } from "@/helpers/issue.helper"; +import { generateWorkItemLink } from "@plane/utils"; // plane web components import { IssueIdentifier } from "@/plane-web/components/issues"; diff --git a/web/ce/components/comments/comment-block.tsx b/web/ce/components/comments/comment-block.tsx index 72b5b7bf5..11b98d6cb 100644 --- a/web/ce/components/comments/comment-block.tsx +++ b/web/ce/components/comments/comment-block.tsx @@ -4,9 +4,9 @@ import { observer } from "mobx-react"; import { useTranslation } from "@plane/i18n"; import { TIssueComment } from "@plane/types"; import { Avatar, Tooltip } from "@plane/ui"; -import { calculateTimeAgo, cn, getFileURL, renderFormattedDate } from "@plane/utils"; +import { calculateTimeAgo, cn, getFileURL, renderFormattedDate, renderFormattedTime } from "@plane/utils"; // hooks -import { renderFormattedTime } from "@/helpers/date-time.helper"; +// import { useMember } from "@/hooks/store"; type TCommentBlock = { diff --git a/web/ce/components/cycles/analytics-sidebar/base.tsx b/web/ce/components/cycles/analytics-sidebar/base.tsx index 881ab6ab3..518f98648 100644 --- a/web/ce/components/cycles/analytics-sidebar/base.tsx +++ b/web/ce/components/cycles/analytics-sidebar/base.tsx @@ -6,10 +6,10 @@ import { useTranslation } from "@plane/i18n"; import { TCycleEstimateType } from "@plane/types"; import { Loader } from "@plane/ui"; // components +import { getDate } from "@plane/utils"; import ProgressChart from "@/components/core/sidebar/progress-chart"; import { EstimateTypeDropdown, validateCycleSnapshot } from "@/components/cycles"; // helpers -import { getDate } from "@/helpers/date-time.helper"; // hooks import { useCycle } from "@/hooks/store"; diff --git a/web/ce/components/gantt-chart/dependency/blockDraggables/left-draggable.tsx b/web/ce/components/gantt-chart/dependency/blockDraggables/left-draggable.tsx index ccb3780c5..cb1f33f79 100644 --- a/web/ce/components/gantt-chart/dependency/blockDraggables/left-draggable.tsx +++ b/web/ce/components/gantt-chart/dependency/blockDraggables/left-draggable.tsx @@ -1,5 +1,5 @@ import { RefObject } from "react"; -import { IGanttBlock } from "@/components/gantt-chart"; +import type { IGanttBlock } from "@plane/types"; type LeftDependencyDraggableProps = { block: IGanttBlock; diff --git a/web/ce/components/gantt-chart/dependency/blockDraggables/right-draggable.tsx b/web/ce/components/gantt-chart/dependency/blockDraggables/right-draggable.tsx index 3d5ac24e0..29c731c9d 100644 --- a/web/ce/components/gantt-chart/dependency/blockDraggables/right-draggable.tsx +++ b/web/ce/components/gantt-chart/dependency/blockDraggables/right-draggable.tsx @@ -1,5 +1,5 @@ import { RefObject } from "react"; -import { IGanttBlock } from "@/components/gantt-chart"; +import type { IGanttBlock } from "@plane/types"; type RightDependencyDraggableProps = { block: IGanttBlock; diff --git a/web/ce/components/global/product-updates-header.tsx b/web/ce/components/global/product-updates-header.tsx index 5274ab5c1..8a2a94c5b 100644 --- a/web/ce/components/global/product-updates-header.tsx +++ b/web/ce/components/global/product-updates-header.tsx @@ -2,7 +2,7 @@ import { observer } from "mobx-react"; import Image from "next/image"; import { useTranslation } from "@plane/i18n"; // helpers -import { cn } from "@/helpers/common.helper"; +import { cn } from "@plane/utils"; // assets import PlaneLogo from "@/public/plane-logos/blue-without-text.png"; // package.json diff --git a/web/ce/components/issues/header.tsx b/web/ce/components/issues/header.tsx index abe44d506..7e7073cd9 100644 --- a/web/ce/components/issues/header.tsx +++ b/web/ce/components/issues/header.tsx @@ -4,8 +4,8 @@ import { observer } from "mobx-react"; import { useParams } from "next/navigation"; // icons import { Circle, ExternalLink } from "lucide-react"; +import { EIssuesStoreType, EUserPermissions, EUserPermissionsLevel, SPACE_BASE_PATH, SPACE_BASE_URL } from "@plane/constants"; // plane constants -import { EIssuesStoreType, EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; // ui import { Breadcrumbs, Button, LayersIcon, Tooltip, Header } from "@plane/ui"; @@ -14,7 +14,6 @@ import { BreadcrumbLink, CountChip } from "@/components/common"; // constants import HeaderFilters from "@/components/issues/filters"; // helpers -import { SPACE_BASE_PATH, SPACE_BASE_URL } from "@/helpers/common.helper"; // hooks import { useEventTracker, useProject, useCommandPalette, useUserPermissions } from "@/hooks/store"; import { useIssues } from "@/hooks/store/use-issues"; diff --git a/web/ce/components/issues/issue-details/issue-identifier.tsx b/web/ce/components/issues/issue-details/issue-identifier.tsx index 1e87d387e..b806803f4 100644 --- a/web/ce/components/issues/issue-details/issue-identifier.tsx +++ b/web/ce/components/issues/issue-details/issue-identifier.tsx @@ -5,7 +5,7 @@ import { IIssueDisplayProperties } from "@plane/types"; // ui import { setToast, TOAST_TYPE, Tooltip } from "@plane/ui"; // helpers -import { cn } from "@/helpers/common.helper"; +import { cn } from "@plane/utils"; // hooks import { useIssueDetail, useProject } from "@/hooks/store"; diff --git a/web/ce/components/pages/editor/ai/ask-pi-menu.tsx b/web/ce/components/pages/editor/ai/ask-pi-menu.tsx index bd49942ef..93d0c998a 100644 --- a/web/ce/components/pages/editor/ai/ask-pi-menu.tsx +++ b/web/ce/components/pages/editor/ai/ask-pi-menu.tsx @@ -3,9 +3,9 @@ import { CircleArrowUp, CornerDownRight, RefreshCcw, Sparkles } from "lucide-rea // ui import { Tooltip } from "@plane/ui"; // components +import { cn } from "@plane/utils"; import { RichTextReadOnlyEditor } from "@/components/editor"; // helpers -import { cn } from "@/helpers/common.helper"; // hooks import { useWorkspace } from "@/hooks/store"; diff --git a/web/ce/components/pages/editor/ai/menu.tsx b/web/ce/components/pages/editor/ai/menu.tsx index 3c33d3f55..e9a98279f 100644 --- a/web/ce/components/pages/editor/ai/menu.tsx +++ b/web/ce/components/pages/editor/ai/menu.tsx @@ -7,9 +7,9 @@ import { EditorRefApi } from "@plane/editor"; // plane ui import { Tooltip } from "@plane/ui"; // components +import { cn } from "@plane/utils"; import { RichTextReadOnlyEditor } from "@/components/editor"; // helpers -import { cn } from "@/helpers/common.helper"; // plane web constants import { AI_EDITOR_TASKS, LOADING_TEXTS } from "@/plane-web/constants/ai"; // plane web services diff --git a/web/ce/components/pages/editor/embed/issue-embed-upgrade-card.tsx b/web/ce/components/pages/editor/embed/issue-embed-upgrade-card.tsx index 1f698f0a7..4ade46da1 100644 --- a/web/ce/components/pages/editor/embed/issue-embed-upgrade-card.tsx +++ b/web/ce/components/pages/editor/embed/issue-embed-upgrade-card.tsx @@ -1,9 +1,9 @@ // plane ui import { getButtonStyling } from "@plane/ui"; // components +import { cn } from "@plane/utils"; import { ProIcon } from "@/components/common"; // helpers -import { cn } from "@/helpers/common.helper"; export const IssueEmbedUpgradeCard: React.FC = (props) => (
Promise; diff --git a/web/core/components/account/auth-forms/password.tsx b/web/core/components/account/auth-forms/password.tsx index 0692eb86d..b3740acea 100644 --- a/web/core/components/account/auth-forms/password.tsx +++ b/web/core/components/account/auth-forms/password.tsx @@ -6,16 +6,15 @@ import Link from "next/link"; // icons import { Eye, EyeOff, Info, X, XCircle } from "lucide-react"; // plane imports -import { FORGOT_PASSWORD, SIGN_IN_WITH_CODE, SIGN_IN_WITH_PASSWORD, SIGN_UP_WITH_PASSWORD } from "@plane/constants"; +import { FORGOT_PASSWORD, SIGN_IN_WITH_CODE, SIGN_IN_WITH_PASSWORD, SIGN_UP_WITH_PASSWORD, API_BASE_URL, E_PASSWORD_STRENGTH } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { Button, Input, Spinner } from "@plane/ui"; +import { getPasswordStrength } from "@plane/utils"; // components import { ForgotPasswordPopover, PasswordStrengthMeter } from "@/components/account"; // constants // helpers import { EAuthModes, EAuthSteps } from "@/helpers/authentication.helper"; -import { API_BASE_URL } from "@/helpers/common.helper"; -import { E_PASSWORD_STRENGTH, getPasswordStrength } from "@/helpers/password.helper"; // hooks import { useEventTracker } from "@/hooks/store"; // services diff --git a/web/core/components/account/auth-forms/unique-code.tsx b/web/core/components/account/auth-forms/unique-code.tsx index 9cd30b8cd..a6c06b70d 100644 --- a/web/core/components/account/auth-forms/unique-code.tsx +++ b/web/core/components/account/auth-forms/unique-code.tsx @@ -2,13 +2,12 @@ import React, { useEffect, useState } from "react"; import { CircleCheck, XCircle } from "lucide-react"; -import { CODE_VERIFIED } from "@plane/constants"; +import { CODE_VERIFIED, API_BASE_URL } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { Button, Input, Spinner } from "@plane/ui"; // constants // helpers import { EAuthModes } from "@/helpers/authentication.helper"; -import { API_BASE_URL } from "@/helpers/common.helper"; // hooks import { useEventTracker } from "@/hooks/store"; import useTimer from "@/hooks/use-timer"; diff --git a/web/core/components/account/oauth/github-button.tsx b/web/core/components/account/oauth/github-button.tsx index 02b5f55bd..c5b6fe8f5 100644 --- a/web/core/components/account/oauth/github-button.tsx +++ b/web/core/components/account/oauth/github-button.tsx @@ -3,7 +3,7 @@ import { useSearchParams } from "next/navigation"; import Image from "next/image"; import { useTheme } from "next-themes"; // helpers -import { API_BASE_URL } from "@/helpers/common.helper"; +import { API_BASE_URL } from "@plane/constants"; // images import githubLightModeImage from "/public/logos/github-black.png"; import githubDarkModeImage from "/public/logos/github-dark.svg"; diff --git a/web/core/components/account/oauth/gitlab-button.tsx b/web/core/components/account/oauth/gitlab-button.tsx index 33221a297..ac861af8b 100644 --- a/web/core/components/account/oauth/gitlab-button.tsx +++ b/web/core/components/account/oauth/gitlab-button.tsx @@ -3,7 +3,7 @@ import Image from "next/image"; import { useSearchParams } from "next/navigation"; import { useTheme } from "next-themes"; // helpers -import { API_BASE_URL } from "@/helpers/common.helper"; +import { API_BASE_URL } from "@plane/constants"; // images import GitlabLogo from "/public/logos/gitlab-logo.svg"; diff --git a/web/core/components/account/oauth/google-button.tsx b/web/core/components/account/oauth/google-button.tsx index c125589ac..6d4665c03 100644 --- a/web/core/components/account/oauth/google-button.tsx +++ b/web/core/components/account/oauth/google-button.tsx @@ -3,7 +3,7 @@ import { useSearchParams } from "next/navigation"; import Image from "next/image"; import { useTheme } from "next-themes"; // helpers -import { API_BASE_URL } from "@/helpers/common.helper"; +import { API_BASE_URL } from "@plane/constants"; // images import GoogleLogo from "/public/logos/google-logo.svg"; diff --git a/web/core/components/account/password-strength-meter.tsx b/web/core/components/account/password-strength-meter.tsx index 1f0abae9c..76b03f66a 100644 --- a/web/core/components/account/password-strength-meter.tsx +++ b/web/core/components/account/password-strength-meter.tsx @@ -1,15 +1,10 @@ "use client"; import { FC, useMemo } from "react"; +// plane imports +import { E_PASSWORD_STRENGTH } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; -// import { CircleCheck } from "lucide-react"; -// helpers -import { cn } from "@/helpers/common.helper"; -import { - E_PASSWORD_STRENGTH, - // PASSWORD_CRITERIA, - getPasswordStrength, -} from "@/helpers/password.helper"; +import { cn, getPasswordStrength } from "@plane/utils"; type TPasswordStrengthMeter = { password: string; @@ -59,7 +54,7 @@ export const PasswordStrengthMeter: FC = (props) => { }; } } - }, [strength,t]); + }, [strength, t]); const isPasswordMeterVisible = isFocused ? true : strength === E_PASSWORD_STRENGTH.STRENGTH_VALID ? false : true; diff --git a/web/core/components/analytics/select/select-y-axis.tsx b/web/core/components/analytics/select/select-y-axis.tsx index c80e2a1e4..931b1976d 100644 --- a/web/core/components/analytics/select/select-y-axis.tsx +++ b/web/core/components/analytics/select/select-y-axis.tsx @@ -3,14 +3,12 @@ import { observer } from "mobx-react"; import { useParams } from "next/navigation"; import { Briefcase } from "lucide-react"; +import { ChartYAxisMetric, EEstimateSystem } from "@plane/constants"; // plane package imports -import { ChartYAxisMetric } from "@plane/constants"; import { CustomSelect } from "@plane/ui"; // hooks import { useProjectEstimates } from "@/hooks/store"; // plane web constants -import { EEstimateSystem } from "@/plane-web/constants/estimates"; - type Props = { value: ChartYAxisMetric; onChange: (val: ChartYAxisMetric | null) => void; diff --git a/web/core/components/analytics/total-insights.tsx b/web/core/components/analytics/total-insights.tsx index a5412dca3..e85c9c68a 100644 --- a/web/core/components/analytics/total-insights.tsx +++ b/web/core/components/analytics/total-insights.tsx @@ -5,12 +5,12 @@ import useSWR from "swr"; import { IInsightField, ANALYTICS_INSIGHTS_FIELDS } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { IAnalyticsResponse, TAnalyticsTabsBase } from "@plane/types"; -//hooks -import { cn } from "@/helpers/common.helper"; +import { cn } from "@plane/utils"; +// hooks import { useAnalytics } from "@/hooks/store/use-analytics"; -//services +// services import { AnalyticsService } from "@/services/analytics.service"; -// plane web components +// local imports import InsightCard from "./insight-card"; const analyticsService = new AnalyticsService(); diff --git a/web/core/components/api-token/modal/create-token-modal.tsx b/web/core/components/api-token/modal/create-token-modal.tsx index eb6d8220f..a848520b1 100644 --- a/web/core/components/api-token/modal/create-token-modal.tsx +++ b/web/core/components/api-token/modal/create-token-modal.tsx @@ -7,13 +7,12 @@ import { mutate } from "swr"; import { IApiToken } from "@plane/types"; // ui import { EModalPosition, EModalWidth, ModalCore, TOAST_TYPE, setToast } from "@plane/ui"; +import { renderFormattedDate, csvDownload } from "@plane/utils"; // components import { CreateApiTokenForm, GeneratedTokenDetails } from "@/components/api-token"; // fetch-keys import { API_TOKENS_LIST } from "@/constants/fetch-keys"; // helpers -import { renderFormattedDate } from "@/helpers/date-time.helper"; -import { csvDownload } from "@/helpers/download.helper"; // services import { APITokenService } from "@/services/api_token.service"; diff --git a/web/core/components/api-token/modal/form.tsx b/web/core/components/api-token/modal/form.tsx index 8bee88d9f..eafd3e821 100644 --- a/web/core/components/api-token/modal/form.tsx +++ b/web/core/components/api-token/modal/form.tsx @@ -9,12 +9,10 @@ import { useTranslation } from "@plane/i18n"; import { IApiToken } from "@plane/types"; // ui import { Button, CustomSelect, Input, TextArea, ToggleSwitch, TOAST_TYPE, setToast } from "@plane/ui"; +import { cn, renderFormattedDate, renderFormattedTime } from "@plane/utils"; // components import { DateDropdown } from "@/components/dropdowns"; // helpers -import { cn } from "@/helpers/common.helper"; -import { renderFormattedDate, renderFormattedTime } from "@/helpers/date-time.helper"; - type Props = { handleClose: () => void; neverExpires: boolean; diff --git a/web/core/components/api-token/modal/generated-token-details.tsx b/web/core/components/api-token/modal/generated-token-details.tsx index e6e3d35fe..e80ba33bf 100644 --- a/web/core/components/api-token/modal/generated-token-details.tsx +++ b/web/core/components/api-token/modal/generated-token-details.tsx @@ -5,9 +5,8 @@ import { useTranslation } from "@plane/i18n"; import { IApiToken } from "@plane/types"; // ui import { Button, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; +import { renderFormattedDate, renderFormattedTime, copyTextToClipboard } from "@plane/utils"; // helpers -import { renderFormattedDate, renderFormattedTime } from "@/helpers/date-time.helper"; -import { copyTextToClipboard } from "@/helpers/string.helper"; // types import { usePlatformOS } from "@/hooks/use-platform-os"; // hooks diff --git a/web/core/components/api-token/token-list-item.tsx b/web/core/components/api-token/token-list-item.tsx index 6d07a79bd..37b354960 100644 --- a/web/core/components/api-token/token-list-item.tsx +++ b/web/core/components/api-token/token-list-item.tsx @@ -5,8 +5,8 @@ import { XCircle } from "lucide-react"; import { IApiToken } from "@plane/types"; // components import { Tooltip } from "@plane/ui"; +import { renderFormattedDate, calculateTimeAgo, renderFormattedTime } from "@plane/utils"; import { DeleteApiTokenModal } from "@/components/api-token"; -import { renderFormattedDate, calculateTimeAgo, renderFormattedTime } from "@/helpers/date-time.helper"; import { usePlatformOS } from "@/hooks/use-platform-os"; // ui // helpers diff --git a/web/core/components/automation/auto-archive-automation.tsx b/web/core/components/automation/auto-archive-automation.tsx index 6190c9326..c1a8c0b75 100644 --- a/web/core/components/automation/auto-archive-automation.tsx +++ b/web/core/components/automation/auto-archive-automation.tsx @@ -5,7 +5,7 @@ import { observer } from "mobx-react"; import { useParams } from "next/navigation"; import { ArchiveRestore } from "lucide-react"; // types -import { PROJECT_AUTOMATION_MONTHS,EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; +import { PROJECT_AUTOMATION_MONTHS, EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { IProject } from "@plane/types"; // ui diff --git a/web/core/components/chart/utils.ts b/web/core/components/chart/utils.ts index 9e1d779bf..092cc2c21 100644 --- a/web/core/components/chart/utils.ts +++ b/web/core/components/chart/utils.ts @@ -1,8 +1,8 @@ import { getWeekOfMonth, isValid } from "date-fns"; import { CHART_X_AXIS_DATE_PROPERTIES, ChartXAxisDateGrouping, ChartXAxisProperty, TO_CAPITALIZE_PROPERTIES } from "@plane/constants"; import { TChart, TChartDatum } from "@plane/types"; -import { capitalizeFirstLetter, hexToHsl, hslToHex, renderFormattedDate } from "@plane/utils"; -import { renderFormattedDateWithoutYear } from "@/helpers/date-time.helper"; +import { capitalizeFirstLetter, hexToHsl, hslToHex, renderFormattedDate, renderFormattedDateWithoutYear } from "@plane/utils"; +// const getDateGroupingName = (date: string, dateGrouping: ChartXAxisDateGrouping): string => { if (!date || ["none", "null"].includes(date.toLowerCase())) return "None"; diff --git a/web/core/components/command-palette/actions/issue-actions/actions-list.tsx b/web/core/components/command-palette/actions/issue-actions/actions-list.tsx index 938138793..812ffd984 100644 --- a/web/core/components/command-palette/actions/issue-actions/actions-list.tsx +++ b/web/core/components/command-palette/actions/issue-actions/actions-list.tsx @@ -9,7 +9,7 @@ import { TIssue } from "@plane/types"; // hooks import { DoubleCircleIcon, TOAST_TYPE, setToast } from "@plane/ui"; // helpers -import { copyTextToClipboard } from "@/helpers/string.helper"; +import { copyTextToClipboard } from "@plane/utils"; // hooks import { useCommandPalette, useIssueDetail, useUser } from "@/hooks/store"; diff --git a/web/core/components/command-palette/actions/issue-actions/change-assignee.tsx b/web/core/components/command-palette/actions/issue-actions/change-assignee.tsx index 66b71794d..f4a3b0951 100644 --- a/web/core/components/command-palette/actions/issue-actions/change-assignee.tsx +++ b/web/core/components/command-palette/actions/issue-actions/change-assignee.tsx @@ -10,7 +10,7 @@ import { TIssue } from "@plane/types"; // plane ui import { Avatar } from "@plane/ui"; // helpers -import { getFileURL } from "@/helpers/file.helper"; +import { getFileURL } from "@plane/utils"; // hooks import { useIssueDetail, useMember } from "@/hooks/store"; diff --git a/web/core/components/command-palette/command-modal.tsx b/web/core/components/command-palette/command-modal.tsx index 5cc080504..ce8d8528d 100644 --- a/web/core/components/command-palette/command-modal.tsx +++ b/web/core/components/command-palette/command-modal.tsx @@ -12,7 +12,7 @@ import { EUserPermissions, EUserPermissionsLevel, WORKSPACE_DEFAULT_SEARCH_RESUL import { useTranslation } from "@plane/i18n"; import { IWorkspaceSearchResults } from "@plane/types"; import { LayersIcon, Loader, ToggleSwitch } from "@plane/ui"; -import { cn } from "@plane/utils"; +import { cn, getTabIndex } from "@plane/utils"; // components import { ChangeIssueAssignee, @@ -27,7 +27,6 @@ import { } from "@/components/command-palette"; import { SimpleEmptyState } from "@/components/empty-state"; // helpers -import { getTabIndex } from "@/helpers/tab-indices.helper"; // hooks import { useCommandPalette, diff --git a/web/core/components/command-palette/command-palette.tsx b/web/core/components/command-palette/command-palette.tsx index 9399c52f5..e7557c2fd 100644 --- a/web/core/components/command-palette/command-palette.tsx +++ b/web/core/components/command-palette/command-palette.tsx @@ -8,9 +8,9 @@ import useSWR from "swr"; import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; import { TOAST_TYPE, setToast } from "@plane/ui"; // components +import { copyTextToClipboard } from "@plane/utils"; import { CommandModal, ShortcutsModal } from "@/components/command-palette"; // helpers -import { copyTextToClipboard } from "@/helpers/string.helper"; // hooks import { useEventTracker, diff --git a/web/core/components/command-palette/shortcuts-modal/commands-list.tsx b/web/core/components/command-palette/shortcuts-modal/commands-list.tsx index 570cb02fa..c01eff48f 100644 --- a/web/core/components/command-palette/shortcuts-modal/commands-list.tsx +++ b/web/core/components/command-palette/shortcuts-modal/commands-list.tsx @@ -1,6 +1,6 @@ import { Command } from "lucide-react"; // helpers -import { substringMatch } from "@/helpers/string.helper"; +import { substringMatch } from "@plane/utils"; // hooks import { usePlatformOS } from "@/hooks/use-platform-os"; // plane web helpers diff --git a/web/core/components/comments/comment-card.tsx b/web/core/components/comments/comment-card.tsx index 1831af4a0..66dd75427 100644 --- a/web/core/components/comments/comment-card.tsx +++ b/web/core/components/comments/comment-card.tsx @@ -11,9 +11,9 @@ import { useTranslation } from "@plane/i18n"; import { TIssueComment, TCommentsOperations } from "@plane/types"; import { CustomMenu } from "@plane/ui"; // components +import { isCommentEmpty } from "@plane/utils"; import { LiteTextEditor, LiteTextReadOnlyEditor } from "@/components/editor"; // helpers -import { isCommentEmpty } from "@/helpers/string.helper"; // hooks import { useUser } from "@/hooks/store"; // diff --git a/web/core/components/comments/comment-create.tsx b/web/core/components/comments/comment-create.tsx index aae88c6c1..450814f51 100644 --- a/web/core/components/comments/comment-create.tsx +++ b/web/core/components/comments/comment-create.tsx @@ -7,12 +7,11 @@ import { EIssueCommentAccessSpecifier } from "@plane/constants"; import { EditorRefApi } from "@plane/editor"; // plane types import { TIssueComment, TCommentsOperations } from "@plane/types"; +import { cn, isCommentEmpty } from "@plane/utils"; // components import { LiteTextEditor } from "@/components/editor"; // constants -import { cn } from "@/helpers/common.helper"; // helpers -import { isCommentEmpty } from "@/helpers/string.helper"; // hooks import { useWorkspace } from "@/hooks/store"; // services diff --git a/web/core/components/comments/comment-reaction.tsx b/web/core/components/comments/comment-reaction.tsx index b101c9402..395894140 100644 --- a/web/core/components/comments/comment-reaction.tsx +++ b/web/core/components/comments/comment-reaction.tsx @@ -6,9 +6,9 @@ import { observer } from "mobx-react"; import { TCommentsOperations, TIssueComment } from "@plane/types"; import { Tooltip } from "@plane/ui"; // components +import { cn } from "@plane/utils"; import { ReactionSelector } from "@/components/issues"; // helpers -import { cn } from "@/helpers/common.helper"; import { renderEmoji } from "@/helpers/emoji.helper"; export type TProps = { diff --git a/web/core/components/common/activity/activity-block.tsx b/web/core/components/common/activity/activity-block.tsx index 61c1a0b7b..b5bcf9616 100644 --- a/web/core/components/common/activity/activity-block.tsx +++ b/web/core/components/common/activity/activity-block.tsx @@ -7,7 +7,7 @@ import { TWorkspaceBaseActivity } from "@plane/types"; // ui import { Tooltip } from "@plane/ui"; // helpers -import { renderFormattedTime, renderFormattedDate, calculateTimeAgo } from "@/helpers/date-time.helper"; +import { renderFormattedTime, renderFormattedDate, calculateTimeAgo } from "@plane/utils"; // hooks import { usePlatformOS } from "@/hooks/use-platform-os"; // local components diff --git a/web/core/components/common/applied-filters/date.tsx b/web/core/components/common/applied-filters/date.tsx index 9dcde53a4..3ecf8a540 100644 --- a/web/core/components/common/applied-filters/date.tsx +++ b/web/core/components/common/applied-filters/date.tsx @@ -3,10 +3,8 @@ import { observer } from "mobx-react"; import { X } from "lucide-react"; // plane constants import { DATE_BEFORE_FILTER_OPTIONS } from "@plane/constants"; +import { renderFormattedDate, capitalizeFirstLetter } from "@plane/utils"; // helpers -import { renderFormattedDate } from "@/helpers/date-time.helper"; -import { capitalizeFirstLetter } from "@/helpers/string.helper"; - type Props = { editable: boolean | undefined; handleRemove: (val: string) => void; diff --git a/web/core/components/common/applied-filters/members.tsx b/web/core/components/common/applied-filters/members.tsx index 87d339366..508a47f98 100644 --- a/web/core/components/common/applied-filters/members.tsx +++ b/web/core/components/common/applied-filters/members.tsx @@ -5,7 +5,7 @@ import { X } from "lucide-react"; // plane ui import { Avatar } from "@plane/ui"; // helpers -import { getFileURL } from "@/helpers/file.helper"; +import { getFileURL } from "@plane/utils"; // types import { useMember } from "@/hooks/store"; diff --git a/web/core/components/common/count-chip.tsx b/web/core/components/common/count-chip.tsx index 0b5820d75..f44f349bf 100644 --- a/web/core/components/common/count-chip.tsx +++ b/web/core/components/common/count-chip.tsx @@ -2,7 +2,7 @@ import { FC } from "react"; // -import { cn } from "@/helpers/common.helper"; +import { cn } from "@plane/utils"; type TCountChip = { count: string | number; diff --git a/web/core/components/common/filters/created-at.tsx b/web/core/components/common/filters/created-at.tsx index b1c23f70a..3626531bd 100644 --- a/web/core/components/common/filters/created-at.tsx +++ b/web/core/components/common/filters/created-at.tsx @@ -2,10 +2,10 @@ import React, { useState } from "react"; import { observer } from "mobx-react"; // plane constants import { DATE_BEFORE_FILTER_OPTIONS } from "@plane/constants"; +import { isInDateFormat } from "@plane/utils"; import { DateFilterModal } from "@/components/core"; import { FilterHeader, FilterOption } from "@/components/issues"; // helpers -import { isInDateFormat } from "@/helpers/date-time.helper"; type Props = { appliedFilters: string[] | null; diff --git a/web/core/components/common/filters/created-by.tsx b/web/core/components/common/filters/created-by.tsx index 6bc75047b..513830e67 100644 --- a/web/core/components/common/filters/created-by.tsx +++ b/web/core/components/common/filters/created-by.tsx @@ -6,9 +6,9 @@ import { observer } from "mobx-react"; // ui import { Avatar, Loader } from "@plane/ui"; // components +import { getFileURL } from "@plane/utils"; import { FilterHeader, FilterOption } from "@/components/issues"; // helpers -import { getFileURL } from "@/helpers/file.helper"; // hooks import { useMember, useUser } from "@/hooks/store"; diff --git a/web/core/components/common/logo.tsx b/web/core/components/common/logo.tsx index 49d4d60a1..f02fd2896 100644 --- a/web/core/components/common/logo.tsx +++ b/web/core/components/common/logo.tsx @@ -6,12 +6,10 @@ import { Emoji } from "emoji-picker-react"; // should be after the imported here rather than some below helper functions as it is in the original file // eslint-disable-next-line import/order import useFontFaceObserver from "use-font-face-observer"; -// types +// plane imports +import { LUCIDE_ICONS_LIST } from "@plane/constants"; import { TLogoProps } from "@plane/types"; -// ui -import { LUCIDE_ICONS_LIST } from "@plane/ui"; -// helpers -import { emojiCodeToUnicode } from "@/helpers/emoji.helper"; +import { emojiCodeToUnicode } from "@plane/utils"; type Props = { logo: TLogoProps; diff --git a/web/core/components/common/pro-icon.tsx b/web/core/components/common/pro-icon.tsx index 39d4c2a90..47300b6d6 100644 --- a/web/core/components/common/pro-icon.tsx +++ b/web/core/components/common/pro-icon.tsx @@ -3,7 +3,7 @@ import { FC } from "react"; import { Crown } from "lucide-react"; // helpers -import { cn } from "@/helpers/common.helper"; +import { cn } from "@plane/utils"; type TProIcon = { className?: string; diff --git a/web/core/components/common/switcher-label.tsx b/web/core/components/common/switcher-label.tsx index e1426461c..6bb4c346f 100644 --- a/web/core/components/common/switcher-label.tsx +++ b/web/core/components/common/switcher-label.tsx @@ -1,8 +1,7 @@ import { FC } from "react"; import { TLogoProps } from "@plane/types"; import { ISvgIcons, Logo } from "@plane/ui"; -import { getFileURL } from "@plane/utils"; -import { truncateText } from "@/helpers/string.helper"; +import { getFileURL, truncateText } from "@plane/utils"; type TSwitcherLabelProps = { logo_props?: TLogoProps; logo_url?: string; diff --git a/web/core/components/core/activity.tsx b/web/core/components/core/activity.tsx index 0a1245725..37f041d6e 100644 --- a/web/core/components/core/activity.tsx +++ b/web/core/components/core/activity.tsx @@ -22,10 +22,8 @@ import { } from "lucide-react"; import { IIssueActivity } from "@plane/types"; import { Tooltip, BlockedIcon, BlockerIcon, RelatedIcon, LayersIcon, DiceIcon, Intake, EpicIcon } from "@plane/ui"; +import { renderFormattedDate, generateWorkItemLink, capitalizeFirstLetter } from "@plane/utils"; // helpers -import { renderFormattedDate } from "@/helpers/date-time.helper"; -import { generateWorkItemLink } from "@/helpers/issue.helper"; -import { capitalizeFirstLetter } from "@/helpers/string.helper"; import { useLabel } from "@/hooks/store"; import { usePlatformOS } from "@/hooks/use-platform-os"; // types diff --git a/web/core/components/core/content-wrapper.tsx b/web/core/components/core/content-wrapper.tsx index eefc96b1e..143ae085e 100644 --- a/web/core/components/core/content-wrapper.tsx +++ b/web/core/components/core/content-wrapper.tsx @@ -2,7 +2,7 @@ import { ReactNode } from "react"; // helpers -import { cn } from "@/helpers/common.helper"; +import { cn } from "@plane/utils"; export interface ContentWrapperProps { className?: string; diff --git a/web/core/components/core/filters/date-filter-modal.tsx b/web/core/components/core/filters/date-filter-modal.tsx index a1c76da4b..2da5b2f35 100644 --- a/web/core/components/core/filters/date-filter-modal.tsx +++ b/web/core/components/core/filters/date-filter-modal.tsx @@ -7,7 +7,7 @@ import { Dialog, Transition } from "@headlessui/react"; import { Button, Calendar } from "@plane/ui"; -import { renderFormattedPayloadDate, renderFormattedDate, getDate } from "@/helpers/date-time.helper"; +import { renderFormattedPayloadDate, renderFormattedDate, getDate } from "@plane/utils"; import { DateFilterSelect } from "./date-filter-select"; type Props = { title: string; diff --git a/web/core/components/core/image-picker-popover.tsx b/web/core/components/core/image-picker-popover.tsx index bade076d9..0634a2312 100644 --- a/web/core/components/core/image-picker-popover.tsx +++ b/web/core/components/core/image-picker-popover.tsx @@ -16,7 +16,7 @@ import { EFileAssetType } from "@plane/types/src/enums"; // ui import { Button, Input, Loader, TOAST_TYPE, setToast } from "@plane/ui"; // helpers -import { getFileURL } from "@/helpers/file.helper"; +import { getFileURL } from "@plane/utils"; // hooks import { useDropdownKeyDown } from "@/hooks/use-dropdown-key-down"; // services diff --git a/web/core/components/core/list/list-item.tsx b/web/core/components/core/list/list-item.tsx index 9a29f61c0..7fe1cad4f 100644 --- a/web/core/components/core/list/list-item.tsx +++ b/web/core/components/core/list/list-item.tsx @@ -3,7 +3,7 @@ import React, { FC } from "react"; // ui import { ControlLink, Row, Tooltip } from "@plane/ui"; // helpers -import { cn } from "@/helpers/common.helper"; +import { cn } from "@plane/utils"; // hooks import { useAppRouter } from "@/hooks/use-app-router"; diff --git a/web/core/components/core/modals/existing-issues-list-modal.tsx b/web/core/components/core/modals/existing-issues-list-modal.tsx index f9132133b..4ab6fe014 100644 --- a/web/core/components/core/modals/existing-issues-list-modal.tsx +++ b/web/core/components/core/modals/existing-issues-list-modal.tsx @@ -9,9 +9,8 @@ import { useTranslation } from "@plane/i18n"; import { ISearchIssueResponse, TProjectIssuesSearchParams } from "@plane/types"; // ui import { Button, Loader, ToggleSwitch, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; +import { generateWorkItemLink, getTabIndex } from "@plane/utils"; // helpers -import { generateWorkItemLink } from "@/helpers/issue.helper"; -import { getTabIndex } from "@/helpers/tab-indices.helper"; // hooks import useDebounce from "@/hooks/use-debounce"; import { usePlatformOS } from "@/hooks/use-platform-os"; diff --git a/web/core/components/core/modals/user-image-upload-modal.tsx b/web/core/components/core/modals/user-image-upload-modal.tsx index c768ff265..a7693b8da 100644 --- a/web/core/components/core/modals/user-image-upload-modal.tsx +++ b/web/core/components/core/modals/user-image-upload-modal.tsx @@ -9,9 +9,8 @@ import { Transition, Dialog } from "@headlessui/react"; import { ACCEPTED_AVATAR_IMAGE_MIME_TYPES_FOR_REACT_DROPZONE, MAX_FILE_SIZE } from "@plane/constants"; import { EFileAssetType } from "@plane/types/src/enums"; import { Button, TOAST_TYPE, setToast } from "@plane/ui"; +import { getAssetIdFromUrl, getFileURL, checkURLValidity } from "@plane/utils"; // helpers -import { getAssetIdFromUrl, getFileURL } from "@/helpers/file.helper"; -import { checkURLValidity } from "@/helpers/string.helper"; // services import { FileService } from "@/services/file.service"; const fileService = new FileService(); diff --git a/web/core/components/core/modals/workspace-image-upload-modal.tsx b/web/core/components/core/modals/workspace-image-upload-modal.tsx index 163e7ff29..0bebeb653 100644 --- a/web/core/components/core/modals/workspace-image-upload-modal.tsx +++ b/web/core/components/core/modals/workspace-image-upload-modal.tsx @@ -9,9 +9,8 @@ import { Transition, Dialog } from "@headlessui/react"; import { ACCEPTED_AVATAR_IMAGE_MIME_TYPES_FOR_REACT_DROPZONE, MAX_FILE_SIZE } from "@plane/constants"; import { EFileAssetType } from "@plane/types/src/enums"; import { Button, TOAST_TYPE, setToast } from "@plane/ui"; +import { getAssetIdFromUrl, getFileURL, checkURLValidity } from "@plane/utils"; // helpers -import { getAssetIdFromUrl, getFileURL } from "@/helpers/file.helper"; -import { checkURLValidity } from "@/helpers/string.helper"; // hooks import { useWorkspace } from "@/hooks/store"; // services diff --git a/web/core/components/core/multiple-select/entity-select-action.tsx b/web/core/components/core/multiple-select/entity-select-action.tsx index cbec2a34e..13795db93 100644 --- a/web/core/components/core/multiple-select/entity-select-action.tsx +++ b/web/core/components/core/multiple-select/entity-select-action.tsx @@ -4,7 +4,7 @@ import { observer } from "mobx-react"; // ui import { Checkbox } from "@plane/ui"; // helpers -import { cn } from "@/helpers/common.helper"; +import { cn } from "@plane/utils"; // hooks import { TSelectionHelper } from "@/hooks/use-multiple-select"; diff --git a/web/core/components/core/multiple-select/group-select-action.tsx b/web/core/components/core/multiple-select/group-select-action.tsx index 296e5bf3d..3040f2d1e 100644 --- a/web/core/components/core/multiple-select/group-select-action.tsx +++ b/web/core/components/core/multiple-select/group-select-action.tsx @@ -2,7 +2,7 @@ // ui import { Checkbox } from "@plane/ui"; // helpers -import { cn } from "@/helpers/common.helper"; +import { cn } from "@plane/utils"; // hooks import { TSelectionHelper } from "@/hooks/use-multiple-select"; diff --git a/web/core/components/core/render-if-visible-HOC.tsx b/web/core/components/core/render-if-visible-HOC.tsx index ea3fc9c2f..f00cf78ee 100644 --- a/web/core/components/core/render-if-visible-HOC.tsx +++ b/web/core/components/core/render-if-visible-HOC.tsx @@ -1,5 +1,5 @@ import React, { useState, useRef, useEffect, ReactNode, MutableRefObject } from "react"; -import { cn } from "@/helpers/common.helper"; +import { cn } from "@plane/utils"; type Props = { defaultHeight?: string; diff --git a/web/core/components/core/sidebar/progress-chart.tsx b/web/core/components/core/sidebar/progress-chart.tsx index e6317eeb9..ac1f1cb5a 100644 --- a/web/core/components/core/sidebar/progress-chart.tsx +++ b/web/core/components/core/sidebar/progress-chart.tsx @@ -1,7 +1,8 @@ import React from "react"; +// plane imports import { AreaChart } from "@plane/propel/charts/area-chart"; import { TChartData, TModuleCompletionChartDistribution } from "@plane/types"; -import { renderFormattedDateWithoutYear } from "@/helpers/date-time.helper"; +import { renderFormattedDateWithoutYear } from "@plane/utils"; type Props = { distribution: TModuleCompletionChartDistribution; diff --git a/web/core/components/cycles/active-cycle/cycle-stats.tsx b/web/core/components/cycles/active-cycle/cycle-stats.tsx index f932415c3..77e36f334 100644 --- a/web/core/components/cycles/active-cycle/cycle-stats.tsx +++ b/web/core/components/cycles/active-cycle/cycle-stats.tsx @@ -12,14 +12,12 @@ import { useTranslation } from "@plane/i18n"; import { ICycle, IIssueFilterOptions } from "@plane/types"; // ui import { Tooltip, Loader, PriorityIcon, Avatar } from "@plane/ui"; +import { cn, renderFormattedDate, renderFormattedDateWithoutYear, getFileURL } from "@plane/utils"; // components import { SingleProgressStats } from "@/components/core"; import { StateDropdown } from "@/components/dropdowns"; import { SimpleEmptyState } from "@/components/empty-state"; // helpers -import { cn } from "@/helpers/common.helper"; -import { renderFormattedDate, renderFormattedDateWithoutYear } from "@/helpers/date-time.helper"; -import { getFileURL } from "@/helpers/file.helper"; // hooks import { useIssueDetail, useIssues } from "@/hooks/store"; import { useIntersectionObserver } from "@/hooks/use-intersection-observer"; diff --git a/web/core/components/cycles/analytics-sidebar/issue-progress.tsx b/web/core/components/cycles/analytics-sidebar/issue-progress.tsx index f911b4418..3b2c29250 100644 --- a/web/core/components/cycles/analytics-sidebar/issue-progress.tsx +++ b/web/core/components/cycles/analytics-sidebar/issue-progress.tsx @@ -11,10 +11,10 @@ import { EIssueFilterType, EIssuesStoreType } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { ICycle, IIssueFilterOptions, TCyclePlotType, TProgressSnapshot } from "@plane/types"; // components +import { getDate } from "@plane/utils"; import { CycleProgressStats } from "@/components/cycles"; // constants // helpers -import { getDate } from "@/helpers/date-time.helper"; // hooks import { useIssues, useCycle } from "@/hooks/store"; // plane web components diff --git a/web/core/components/cycles/analytics-sidebar/progress-stats.tsx b/web/core/components/cycles/analytics-sidebar/progress-stats.tsx index 4f13b8612..7e1c37857 100644 --- a/web/core/components/cycles/analytics-sidebar/progress-stats.tsx +++ b/web/core/components/cycles/analytics-sidebar/progress-stats.tsx @@ -14,11 +14,10 @@ import { TStateGroups, } from "@plane/types"; import { Avatar, StateGroupIcon } from "@plane/ui"; +import { cn, getFileURL } from "@plane/utils"; // components import { SingleProgressStats } from "@/components/core"; // helpers -import { cn } from "@/helpers/common.helper"; -import { getFileURL } from "@/helpers/file.helper"; // hooks import { useProjectState } from "@/hooks/store"; import useLocalStorage from "@/hooks/use-local-storage"; diff --git a/web/core/components/cycles/analytics-sidebar/sidebar-details.tsx b/web/core/components/cycles/analytics-sidebar/sidebar-details.tsx index 992d7f3ca..b2d9db7d9 100644 --- a/web/core/components/cycles/analytics-sidebar/sidebar-details.tsx +++ b/web/core/components/cycles/analytics-sidebar/sidebar-details.tsx @@ -4,16 +4,16 @@ import isEmpty from "lodash/isEmpty"; import { observer } from "mobx-react"; import { LayersIcon, SquareUser, Users } from "lucide-react"; // plane types +import { EEstimateSystem } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { ICycle } from "@plane/types"; // plane ui import { Avatar, AvatarGroup, TextArea } from "@plane/ui"; // helpers -import { getFileURL } from "@/helpers/file.helper"; +import { getFileURL } from "@plane/utils"; // hooks import { useMember, useProjectEstimates } from "@/hooks/store"; // plane web constants -import { EEstimateSystem } from "@/plane-web/constants/estimates"; type Props = { projectId: string; diff --git a/web/core/components/cycles/analytics-sidebar/sidebar-header.tsx b/web/core/components/cycles/analytics-sidebar/sidebar-header.tsx index 01c692837..ebf94e0c6 100644 --- a/web/core/components/cycles/analytics-sidebar/sidebar-header.tsx +++ b/web/core/components/cycles/analytics-sidebar/sidebar-header.tsx @@ -9,18 +9,15 @@ import { CYCLE_STATUS, CYCLE_UPDATED, EUserPermissions, EUserPermissionsLevel } import { useTranslation } from "@plane/i18n"; import { ICycle } from "@plane/types"; import { setToast, TOAST_TYPE } from "@plane/ui"; +import { getDate, renderFormattedPayloadDate } from "@plane/utils"; // components import { DateRangeDropdown } from "@/components/dropdowns"; -// helpers -import { renderFormattedPayloadDate, getDate } from "@/helpers/date-time.helper"; // hooks import { useCycle, useEventTracker, useUserPermissions } from "@/hooks/store"; -import { useAppRouter } from "@/hooks/use-app-router"; -// plane web constants -// services import { useTimeZoneConverter } from "@/hooks/use-timezone-converter"; +// services import { CycleService } from "@/services/cycle.service"; -// local components +// local imports import { ArchiveCycleModal } from "../archived-cycles"; import { CycleDeleteModal } from "../delete-modal"; @@ -41,15 +38,13 @@ const cycleService = new CycleService(); export const CycleSidebarHeader: FC = observer((props) => { const { workspaceSlug, projectId, cycleDetails, handleClose, isArchived = false } = props; - // router - const router = useAppRouter(); // states const [archiveCycleModal, setArchiveCycleModal] = useState(false); const [cycleDeleteModal, setCycleDeleteModal] = useState(false); // hooks const { allowPermissions } = useUserPermissions(); - const { updateCycleDetails, restoreCycle } = useCycle(); - const { setTrackElement, captureCycleEvent } = useEventTracker(); + const { updateCycleDetails } = useCycle(); + const { captureCycleEvent } = useEventTracker(); const { t } = useTranslation(); const { renderFormattedDateInUserTimezone, getProjectUTCOffset } = useTimeZoneConverter(projectId); diff --git a/web/core/components/cycles/applied-filters/date.tsx b/web/core/components/cycles/applied-filters/date.tsx index c2ee47be5..18a5ece58 100644 --- a/web/core/components/cycles/applied-filters/date.tsx +++ b/web/core/components/cycles/applied-filters/date.tsx @@ -2,8 +2,7 @@ import { observer } from "mobx-react"; import { X } from "lucide-react"; // helpers import { DATE_AFTER_FILTER_OPTIONS } from "@plane/constants"; -import { renderFormattedDate } from "@/helpers/date-time.helper"; -import { capitalizeFirstLetter } from "@/helpers/string.helper"; +import { renderFormattedDate, capitalizeFirstLetter } from "@plane/utils"; // constants type Props = { diff --git a/web/core/components/cycles/applied-filters/root.tsx b/web/core/components/cycles/applied-filters/root.tsx index b53f0ab4d..78a62238c 100644 --- a/web/core/components/cycles/applied-filters/root.tsx +++ b/web/core/components/cycles/applied-filters/root.tsx @@ -5,8 +5,8 @@ import { useTranslation } from "@plane/i18n"; import { TCycleFilters } from "@plane/types"; // hooks import { Tag } from "@plane/ui"; +import { replaceUnderscoreIfSnakeCase } from "@plane/utils"; import { AppliedDateFilters, AppliedStatusFilters } from "@/components/cycles"; -import { replaceUnderscoreIfSnakeCase } from "@/helpers/string.helper"; import { useUserPermissions } from "@/hooks/store"; // components diff --git a/web/core/components/cycles/applied-filters/status.tsx b/web/core/components/cycles/applied-filters/status.tsx index 7bb219251..ef2d63b1a 100644 --- a/web/core/components/cycles/applied-filters/status.tsx +++ b/web/core/components/cycles/applied-filters/status.tsx @@ -2,7 +2,7 @@ import { observer } from "mobx-react"; import { X } from "lucide-react"; import { CYCLE_STATUS } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; -import { cn } from "@/helpers/common.helper"; +import { cn } from "@plane/utils"; type Props = { handleRemove: (val: string) => void; diff --git a/web/core/components/cycles/archived-cycles/header.tsx b/web/core/components/cycles/archived-cycles/header.tsx index cdc174b0e..7ec368cdd 100644 --- a/web/core/components/cycles/archived-cycles/header.tsx +++ b/web/core/components/cycles/archived-cycles/header.tsx @@ -7,13 +7,12 @@ import { ListFilter, Search, X } from "lucide-react"; import { useOutsideClickDetector } from "@plane/hooks"; // types import type { TCycleFilters } from "@plane/types"; +import { cn, calculateTotalFilters } from "@plane/utils"; // components import { ArchiveTabsList } from "@/components/archives"; import { CycleFiltersSelection } from "@/components/cycles"; import { FiltersDropdown } from "@/components/issues"; // helpers -import { cn } from "@/helpers/common.helper"; -import { calculateTotalFilters } from "@/helpers/filter.helper"; // hooks import { useCycleFilter } from "@/hooks/store"; diff --git a/web/core/components/cycles/archived-cycles/root.tsx b/web/core/components/cycles/archived-cycles/root.tsx index 165b5e4aa..6005dfb3c 100644 --- a/web/core/components/cycles/archived-cycles/root.tsx +++ b/web/core/components/cycles/archived-cycles/root.tsx @@ -6,11 +6,11 @@ import useSWR from "swr"; import { useTranslation } from "@plane/i18n"; import { TCycleFilters } from "@plane/types"; // components +import { calculateTotalFilters } from "@plane/utils"; import { ArchivedCyclesView, CycleAppliedFiltersList } from "@/components/cycles"; import { DetailedEmptyState } from "@/components/empty-state"; import { CycleModuleListLayout } from "@/components/ui"; // helpers -import { calculateTotalFilters } from "@/helpers/filter.helper"; // hooks import { useCycle, useCycleFilter } from "@/hooks/store"; import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; diff --git a/web/core/components/cycles/cycle-peek-overview.tsx b/web/core/components/cycles/cycle-peek-overview.tsx index 187425b8d..2baadf0c1 100644 --- a/web/core/components/cycles/cycle-peek-overview.tsx +++ b/web/core/components/cycles/cycle-peek-overview.tsx @@ -2,7 +2,7 @@ import React, { useEffect } from "react"; import { observer } from "mobx-react"; import { usePathname, useSearchParams } from "next/navigation"; // hooks -import { generateQueryParams } from "@/helpers/router.helper"; +import { generateQueryParams } from "@plane/utils"; import { useCycle } from "@/hooks/store"; import { useAppRouter } from "@/hooks/use-app-router"; // components diff --git a/web/core/components/cycles/cycles-view-header.tsx b/web/core/components/cycles/cycles-view-header.tsx index fe5b942db..0ec921754 100644 --- a/web/core/components/cycles/cycles-view-header.tsx +++ b/web/core/components/cycles/cycles-view-header.tsx @@ -7,12 +7,11 @@ import { useOutsideClickDetector } from "@plane/hooks"; // types import { useTranslation } from "@plane/i18n"; import { TCycleFilters } from "@plane/types"; +import { cn, calculateTotalFilters } from "@plane/utils"; // components import { CycleFiltersSelection } from "@/components/cycles"; import { FiltersDropdown } from "@/components/issues"; // helpers -import { cn } from "@/helpers/common.helper"; -import { calculateTotalFilters } from "@/helpers/filter.helper"; // hooks import { useCycleFilter } from "@/hooks/store"; diff --git a/web/core/components/cycles/dropdowns/filters/end-date.tsx b/web/core/components/cycles/dropdowns/filters/end-date.tsx index f9b7d2f37..b83699966 100644 --- a/web/core/components/cycles/dropdowns/filters/end-date.tsx +++ b/web/core/components/cycles/dropdowns/filters/end-date.tsx @@ -3,11 +3,11 @@ import { observer } from "mobx-react"; // constants import { DATE_AFTER_FILTER_OPTIONS } from "@plane/constants"; // components +import { isInDateFormat } from "@plane/utils"; import { DateFilterModal } from "@/components/core"; import { FilterHeader, FilterOption } from "@/components/issues"; // helpers -import { isInDateFormat } from "@/helpers/date-time.helper"; type Props = { appliedFilters: string[] | null; diff --git a/web/core/components/cycles/dropdowns/filters/start-date.tsx b/web/core/components/cycles/dropdowns/filters/start-date.tsx index eb2032edb..19ff38bc6 100644 --- a/web/core/components/cycles/dropdowns/filters/start-date.tsx +++ b/web/core/components/cycles/dropdowns/filters/start-date.tsx @@ -3,11 +3,11 @@ import { observer } from "mobx-react"; // constants import { DATE_AFTER_FILTER_OPTIONS } from "@plane/constants"; // components +import { isInDateFormat } from "@plane/utils"; import { DateFilterModal } from "@/components/core"; import { FilterHeader, FilterOption } from "@/components/issues"; // helpers -import { isInDateFormat } from "@/helpers/date-time.helper"; type Props = { appliedFilters: string[] | null; diff --git a/web/core/components/cycles/form.tsx b/web/core/components/cycles/form.tsx index 601ae557c..3c3c98575 100644 --- a/web/core/components/cycles/form.tsx +++ b/web/core/components/cycles/form.tsx @@ -9,12 +9,9 @@ import { useTranslation } from "@plane/i18n"; import { ICycle } from "@plane/types"; // ui import { Button, Input, TextArea } from "@plane/ui"; +import { getDate, renderFormattedPayloadDate, getTabIndex } from "@plane/utils"; // components import { DateRangeDropdown, ProjectDropdown } from "@/components/dropdowns"; -// constants -// helpers -import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper"; -import { getTabIndex } from "@/helpers/tab-indices.helper"; // hooks import { useUser } from "@/hooks/store/user/user-user"; diff --git a/web/core/components/cycles/list/cycle-list-group-header.tsx b/web/core/components/cycles/list/cycle-list-group-header.tsx index d132cc70d..0d51a28ee 100644 --- a/web/core/components/cycles/list/cycle-list-group-header.tsx +++ b/web/core/components/cycles/list/cycle-list-group-header.tsx @@ -7,7 +7,7 @@ import { TCycleGroups } from "@plane/types"; // icons import { Row, CycleGroupIcon } from "@plane/ui"; // helpers -import { cn } from "@/helpers/common.helper"; +import { cn } from "@plane/utils"; type Props = { type: TCycleGroups; diff --git a/web/core/components/cycles/list/cycle-list-item-action.tsx b/web/core/components/cycles/list/cycle-list-item-action.tsx index bc3553078..79b1650e1 100644 --- a/web/core/components/cycles/list/cycle-list-item-action.tsx +++ b/web/core/components/cycles/list/cycle-list-item-action.tsx @@ -18,15 +18,13 @@ import { useTranslation } from "@plane/i18n"; import { ICycle, TCycleGroups } from "@plane/types"; // ui import { Avatar, AvatarGroup, FavoriteStar, LayersIcon, Tooltip, TransferIcon, setPromiseToast } from "@plane/ui"; +import { getDate, getFileURL, generateQueryParams } from "@plane/utils"; // components import { CycleQuickActions, TransferIssuesModal } from "@/components/cycles"; import { DateRangeDropdown } from "@/components/dropdowns"; import { ButtonAvatars } from "@/components/dropdowns/member/avatar"; import { MergedDateDisplay } from "@/components/dropdowns/merged-date"; -import { getDate } from "@/helpers/date-time.helper"; -import { getFileURL } from "@/helpers/file.helper"; // hooks -import { generateQueryParams } from "@/helpers/router.helper"; import { useCycle, useEventTracker, useMember, useUserPermissions } from "@/hooks/store"; import { useAppRouter } from "@/hooks/use-app-router"; import { usePlatformOS } from "@/hooks/use-platform-os"; diff --git a/web/core/components/cycles/list/cycle-list-project-group-header.tsx b/web/core/components/cycles/list/cycle-list-project-group-header.tsx index d663eca0d..4699dba63 100644 --- a/web/core/components/cycles/list/cycle-list-project-group-header.tsx +++ b/web/core/components/cycles/list/cycle-list-project-group-header.tsx @@ -6,7 +6,7 @@ import { ChevronRight } from "lucide-react"; // icons import { Row, Logo } from "@plane/ui"; // helpers -import { cn } from "@/helpers/common.helper"; +import { cn } from "@plane/utils"; import { useProject } from "@/hooks/store/use-project"; type Props = { diff --git a/web/core/components/cycles/list/cycles-list-item.tsx b/web/core/components/cycles/list/cycles-list-item.tsx index 2de4a9076..6e0bca8de 100644 --- a/web/core/components/cycles/list/cycles-list-item.tsx +++ b/web/core/components/cycles/list/cycles-list-item.tsx @@ -10,11 +10,11 @@ import type { TCycleGroups } from "@plane/types"; // ui import { CircularProgressIndicator } from "@plane/ui"; // components +import { generateQueryParams } from "@plane/utils"; import { ListItem } from "@/components/core/list"; import { CycleQuickActions } from "@/components/cycles/"; import { CycleListItemAction } from "@/components/cycles/list"; // helpers -import { generateQueryParams } from "@/helpers/router.helper"; // hooks import { useCycle } from "@/hooks/store"; import { useAppRouter } from "@/hooks/use-app-router"; diff --git a/web/core/components/cycles/quick-actions.tsx b/web/core/components/cycles/quick-actions.tsx index 0faba55fa..bd971f625 100644 --- a/web/core/components/cycles/quick-actions.tsx +++ b/web/core/components/cycles/quick-actions.tsx @@ -9,11 +9,10 @@ import { ArchiveRestoreIcon, ExternalLink, LinkIcon, Pencil, Trash2 } from "luci import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { ArchiveIcon, ContextMenu, CustomMenu, TContextMenuItem, TOAST_TYPE, setToast } from "@plane/ui"; -import { copyUrlToClipboard } from "@plane/utils"; +import { copyUrlToClipboard, cn } from "@plane/utils"; // components import { ArchiveCycleModal, CycleCreateUpdateModal, CycleDeleteModal } from "@/components/cycles"; // helpers -import { cn } from "@/helpers/common.helper"; // hooks import { useCycle, useEventTracker, useUserPermissions } from "@/hooks/store"; import { useAppRouter } from "@/hooks/use-app-router"; diff --git a/web/core/components/dropdowns/buttons.tsx b/web/core/components/dropdowns/buttons.tsx index a2b7c590a..de14c11b8 100644 --- a/web/core/components/dropdowns/buttons.tsx +++ b/web/core/components/dropdowns/buttons.tsx @@ -2,7 +2,7 @@ // helpers import { Tooltip } from "@plane/ui"; -import { cn } from "@/helpers/common.helper"; +import { cn } from "@plane/utils"; // types import { usePlatformOS } from "@/hooks/use-platform-os"; import { BACKGROUND_BUTTON_VARIANTS, BORDER_BUTTON_VARIANTS } from "./constants"; diff --git a/web/core/components/dropdowns/cycle/index.tsx b/web/core/components/dropdowns/cycle/index.tsx index f06b1956d..7499c64e3 100644 --- a/web/core/components/dropdowns/cycle/index.tsx +++ b/web/core/components/dropdowns/cycle/index.tsx @@ -7,7 +7,7 @@ import { useTranslation } from "@plane/i18n"; // ui import { ComboDropDown, ContrastIcon } from "@plane/ui"; // helpers -import { cn } from "@/helpers/common.helper"; +import { cn } from "@plane/utils"; // hooks import { useCycle } from "@/hooks/store"; import { useDropdown } from "@/hooks/use-dropdown"; diff --git a/web/core/components/dropdowns/date-range.tsx b/web/core/components/dropdowns/date-range.tsx index 62c7d9409..d8c9bd4ba 100644 --- a/web/core/components/dropdowns/date-range.tsx +++ b/web/core/components/dropdowns/date-range.tsx @@ -10,9 +10,8 @@ import { Combobox } from "@headlessui/react"; import { useTranslation } from "@plane/i18n"; // ui import { ComboDropDown, Calendar } from "@plane/ui"; +import { cn, renderFormattedDate } from "@plane/utils"; // helpers -import { cn } from "@/helpers/common.helper"; -import { renderFormattedDate } from "@/helpers/date-time.helper"; // hooks import { useDropdown } from "@/hooks/use-dropdown"; // components diff --git a/web/core/components/dropdowns/date.tsx b/web/core/components/dropdowns/date.tsx index 684d6f3ef..bab9ff2f8 100644 --- a/web/core/components/dropdowns/date.tsx +++ b/web/core/components/dropdowns/date.tsx @@ -8,9 +8,8 @@ import { Combobox } from "@headlessui/react"; // ui import { EStartOfTheWeek } from "@plane/constants"; import { ComboDropDown, Calendar } from "@plane/ui"; +import { cn, renderFormattedDate, getDate } from "@plane/utils"; // helpers -import { cn } from "@/helpers/common.helper"; -import { renderFormattedDate, getDate } from "@/helpers/date-time.helper"; // hooks import { useUserProfile } from "@/hooks/store"; import { useDropdown } from "@/hooks/use-dropdown"; diff --git a/web/core/components/dropdowns/estimate.tsx b/web/core/components/dropdowns/estimate.tsx index 8c43f68b6..4f80d061b 100644 --- a/web/core/components/dropdowns/estimate.tsx +++ b/web/core/components/dropdowns/estimate.tsx @@ -8,9 +8,8 @@ import { Combobox } from "@headlessui/react"; import { useTranslation } from "@plane/i18n"; import { EEstimateSystem } from "@plane/types/src/enums"; import { ComboDropDown } from "@plane/ui"; +import { convertMinutesToHoursMinutesString, cn } from "@plane/utils"; // helpers -import { convertMinutesToHoursMinutesString } from "@plane/utils"; -import { cn } from "@/helpers/common.helper"; // hooks import { useEstimate, diff --git a/web/core/components/dropdowns/member/avatar.tsx b/web/core/components/dropdowns/member/avatar.tsx index 0b2189f65..980144a19 100644 --- a/web/core/components/dropdowns/member/avatar.tsx +++ b/web/core/components/dropdowns/member/avatar.tsx @@ -4,10 +4,9 @@ import { observer } from "mobx-react"; import { LucideIcon, Users } from "lucide-react"; // plane ui import { Avatar, AvatarGroup } from "@plane/ui"; +import { cn, getFileURL } from "@plane/utils"; // plane utils -import { cn } from "@plane/utils"; // helpers -import { getFileURL } from "@/helpers/file.helper"; // hooks import { useMember } from "@/hooks/store"; diff --git a/web/core/components/dropdowns/member/index.tsx b/web/core/components/dropdowns/member/index.tsx index d86d31534..acfa9f460 100644 --- a/web/core/components/dropdowns/member/index.tsx +++ b/web/core/components/dropdowns/member/index.tsx @@ -5,7 +5,7 @@ import { useTranslation } from "@plane/i18n"; // ui import { ComboDropDown } from "@plane/ui"; // helpers -import { cn } from "@/helpers/common.helper"; +import { cn } from "@plane/utils"; // hooks import { useMember } from "@/hooks/store"; import { useDropdown } from "@/hooks/use-dropdown"; diff --git a/web/core/components/dropdowns/member/member-options.tsx b/web/core/components/dropdowns/member/member-options.tsx index 28f84553f..f2cea10fa 100644 --- a/web/core/components/dropdowns/member/member-options.tsx +++ b/web/core/components/dropdowns/member/member-options.tsx @@ -12,9 +12,8 @@ import { EUserPermissions } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; // plane ui import { Avatar } from "@plane/ui"; +import { cn, getFileURL } from "@plane/utils"; // helpers -import { cn } from "@/helpers/common.helper"; -import { getFileURL } from "@/helpers/file.helper"; // hooks import { useUser, useMember } from "@/hooks/store"; import { usePlatformOS } from "@/hooks/use-platform-os"; diff --git a/web/core/components/dropdowns/merged-date.tsx b/web/core/components/dropdowns/merged-date.tsx index ca2adc46e..92a3d1e82 100644 --- a/web/core/components/dropdowns/merged-date.tsx +++ b/web/core/components/dropdowns/merged-date.tsx @@ -1,8 +1,7 @@ import React from "react"; import { observer } from "mobx-react"; // helpers -import { formatDateRange } from "@plane/utils"; -import { getDate } from "@/helpers/date-time.helper"; +import { formatDateRange, getDate } from "@plane/utils"; type Props = { startDate: Date | string | null | undefined; diff --git a/web/core/components/dropdowns/module/index.tsx b/web/core/components/dropdowns/module/index.tsx index f9233e0b2..9e789dbf4 100644 --- a/web/core/components/dropdowns/module/index.tsx +++ b/web/core/components/dropdowns/module/index.tsx @@ -8,7 +8,7 @@ import { useTranslation } from "@plane/i18n"; // ui import { ComboDropDown, DiceIcon, Tooltip } from "@plane/ui"; // helpers -import { cn } from "@/helpers/common.helper"; +import { cn } from "@plane/utils"; // hooks import { useModule } from "@/hooks/store"; import { useDropdown } from "@/hooks/use-dropdown"; diff --git a/web/core/components/dropdowns/module/module-options.tsx b/web/core/components/dropdowns/module/module-options.tsx index d2d3f909d..6b96220b4 100644 --- a/web/core/components/dropdowns/module/module-options.tsx +++ b/web/core/components/dropdowns/module/module-options.tsx @@ -12,7 +12,7 @@ import { useTranslation } from "@plane/i18n"; //components import { DiceIcon } from "@plane/ui"; //store -import { cn } from "@/helpers/common.helper"; +import { cn } from "@plane/utils"; import { useModule } from "@/hooks/store"; import { usePlatformOS } from "@/hooks/use-platform-os"; //hooks diff --git a/web/core/components/dropdowns/priority.tsx b/web/core/components/dropdowns/priority.tsx index dfc0ff071..e3bc44e73 100644 --- a/web/core/components/dropdowns/priority.tsx +++ b/web/core/components/dropdowns/priority.tsx @@ -12,7 +12,7 @@ import { TIssuePriorities } from "@plane/types"; // ui import { ComboDropDown, PriorityIcon, Tooltip } from "@plane/ui"; // helpers -import { cn } from "@/helpers/common.helper"; +import { cn } from "@plane/utils"; // hooks import { useDropdown } from "@/hooks/use-dropdown"; import { usePlatformOS } from "@/hooks/use-platform-os"; diff --git a/web/core/components/dropdowns/project.tsx b/web/core/components/dropdowns/project.tsx index 39ab86201..265122c4b 100644 --- a/web/core/components/dropdowns/project.tsx +++ b/web/core/components/dropdowns/project.tsx @@ -7,9 +7,9 @@ import { Combobox } from "@headlessui/react"; import { useTranslation } from "@plane/i18n"; import { ComboDropDown } from "@plane/ui"; // components +import { cn } from "@plane/utils"; import { Logo } from "@/components/common"; // helpers -import { cn } from "@/helpers/common.helper"; // hooks import { useProject } from "@/hooks/store"; import { useDropdown } from "@/hooks/use-dropdown"; diff --git a/web/core/components/dropdowns/state.tsx b/web/core/components/dropdowns/state.tsx index db8cbb5c7..312eb2db3 100644 --- a/web/core/components/dropdowns/state.tsx +++ b/web/core/components/dropdowns/state.tsx @@ -10,7 +10,7 @@ import { useTranslation } from "@plane/i18n"; // ui import { ComboDropDown, Spinner, StateGroupIcon } from "@plane/ui"; // helpers -import { cn } from "@/helpers/common.helper"; +import { cn } from "@plane/utils"; // hooks import { useProjectState } from "@/hooks/store"; import { useDropdown } from "@/hooks/use-dropdown"; diff --git a/web/core/components/editor/embeds/mentions/user.tsx b/web/core/components/editor/embeds/mentions/user.tsx index a8e4a4b0b..83a6ee79f 100644 --- a/web/core/components/editor/embeds/mentions/user.tsx +++ b/web/core/components/editor/embeds/mentions/user.tsx @@ -7,10 +7,9 @@ import { usePopper } from "react-popper"; import { ROLE } from "@plane/constants"; // plane ui import { Avatar } from "@plane/ui"; +import { cn, getFileURL } from "@plane/utils"; // constants // helpers -import { cn } from "@/helpers/common.helper"; -import { getFileURL } from "@/helpers/file.helper"; // hooks import { useMember, useUser } from "@/hooks/store"; diff --git a/web/core/components/editor/lite-text-editor/lite-text-editor.tsx b/web/core/components/editor/lite-text-editor/lite-text-editor.tsx index 5dda3b8db..a29f84a63 100644 --- a/web/core/components/editor/lite-text-editor/lite-text-editor.tsx +++ b/web/core/components/editor/lite-text-editor/lite-text-editor.tsx @@ -7,10 +7,9 @@ import { EditorRefApi, ILiteTextEditor, LiteTextEditorWithRef, TFileHandler } fr import { useTranslation } from "@plane/i18n"; // components import { MakeOptional } from "@plane/types"; +import { cn, isCommentEmpty } from "@plane/utils"; import { EditorMentionsRoot, IssueCommentToolbar } from "@/components/editor"; // helpers -import { cn } from "@/helpers/common.helper"; -import { isCommentEmpty } from "@/helpers/string.helper"; // hooks import { useEditorConfig, useEditorMention } from "@/hooks/editor"; // store hooks diff --git a/web/core/components/editor/lite-text-editor/lite-text-read-only-editor.tsx b/web/core/components/editor/lite-text-editor/lite-text-read-only-editor.tsx index 9ee48ba84..1f9a9ff49 100644 --- a/web/core/components/editor/lite-text-editor/lite-text-read-only-editor.tsx +++ b/web/core/components/editor/lite-text-editor/lite-text-read-only-editor.tsx @@ -3,9 +3,9 @@ import React from "react"; import { EditorReadOnlyRefApi, ILiteTextReadOnlyEditor, LiteTextReadOnlyEditorWithRef } from "@plane/editor"; import { MakeOptional } from "@plane/types"; // components +import { cn } from "@plane/utils"; import { EditorMentionsRoot } from "@/components/editor"; // helpers -import { cn } from "@/helpers/common.helper"; // hooks import { useEditorConfig } from "@/hooks/editor"; // store hooks diff --git a/web/core/components/editor/lite-text-editor/toolbar.tsx b/web/core/components/editor/lite-text-editor/toolbar.tsx index 818e35fae..13cd07c44 100644 --- a/web/core/components/editor/lite-text-editor/toolbar.tsx +++ b/web/core/components/editor/lite-text-editor/toolbar.tsx @@ -10,9 +10,9 @@ import { useTranslation } from "@plane/i18n"; // ui import { Button, Tooltip } from "@plane/ui"; // constants +import { cn } from "@plane/utils"; import { TOOLBAR_ITEMS, ToolbarMenuItem } from "@/constants/editor"; // helpers -import { cn } from "@/helpers/common.helper"; type Props = { accessSpecifier?: EIssueCommentAccessSpecifier; diff --git a/web/core/components/editor/rich-text-editor/rich-text-editor.tsx b/web/core/components/editor/rich-text-editor/rich-text-editor.tsx index ea27ec45d..e86742431 100644 --- a/web/core/components/editor/rich-text-editor/rich-text-editor.tsx +++ b/web/core/components/editor/rich-text-editor/rich-text-editor.tsx @@ -3,9 +3,9 @@ import React, { forwardRef } from "react"; import { EditorRefApi, IRichTextEditor, RichTextEditorWithRef, TFileHandler } from "@plane/editor"; import { MakeOptional, TSearchEntityRequestPayload, TSearchResponse } from "@plane/types"; // components +import { cn } from "@plane/utils"; import { EditorMentionsRoot } from "@/components/editor"; // helpers -import { cn } from "@/helpers/common.helper"; // hooks import { useEditorConfig, useEditorMention } from "@/hooks/editor"; // store hooks diff --git a/web/core/components/editor/rich-text-editor/rich-text-read-only-editor.tsx b/web/core/components/editor/rich-text-editor/rich-text-read-only-editor.tsx index 50eb65e78..7ac8a8a22 100644 --- a/web/core/components/editor/rich-text-editor/rich-text-read-only-editor.tsx +++ b/web/core/components/editor/rich-text-editor/rich-text-read-only-editor.tsx @@ -1,11 +1,13 @@ +"use client"; + import React from "react"; // plane imports import { EditorReadOnlyRefApi, IRichTextReadOnlyEditor, RichTextReadOnlyEditorWithRef } from "@plane/editor"; import { MakeOptional } from "@plane/types"; // components +import { cn } from "@plane/utils"; import { EditorMentionsRoot } from "@/components/editor"; // helpers -import { cn } from "@/helpers/common.helper"; // hooks import { useEditorConfig } from "@/hooks/editor"; // store hooks diff --git a/web/core/components/editor/sticky-editor/editor.tsx b/web/core/components/editor/sticky-editor/editor.tsx index de20e8902..251096122 100644 --- a/web/core/components/editor/sticky-editor/editor.tsx +++ b/web/core/components/editor/sticky-editor/editor.tsx @@ -6,7 +6,7 @@ import { EditorRefApi, ILiteTextEditor, LiteTextEditorWithRef, TFileHandler } fr // components import { TSticky } from "@plane/types"; // helpers -import { cn } from "@/helpers/common.helper"; +import { cn } from "@plane/utils"; // hooks import { useEditorConfig } from "@/hooks/editor"; // plane web hooks diff --git a/web/core/components/editor/sticky-editor/toolbar.tsx b/web/core/components/editor/sticky-editor/toolbar.tsx index c92f7448d..84b27226e 100644 --- a/web/core/components/editor/sticky-editor/toolbar.tsx +++ b/web/core/components/editor/sticky-editor/toolbar.tsx @@ -9,9 +9,9 @@ import { useOutsideClickDetector } from "@plane/hooks"; import { TSticky } from "@plane/types"; import { Tooltip } from "@plane/ui"; // constants +import { cn } from "@plane/utils"; import { TOOLBAR_ITEMS, ToolbarMenuItem } from "@/constants/editor"; // helpers -import { cn } from "@/helpers/common.helper"; import { ColorPalette } from "./color-palette"; type Props = { diff --git a/web/core/components/estimates/create/modal.tsx b/web/core/components/estimates/create/modal.tsx index c5760fb99..531a024ab 100644 --- a/web/core/components/estimates/create/modal.tsx +++ b/web/core/components/estimates/create/modal.tsx @@ -4,6 +4,7 @@ import { FC, useEffect, useMemo, useState } from "react"; import { observer } from "mobx-react"; import { ChevronLeft } from "lucide-react"; // types +import { EEstimateSystem, ESTIMATE_SYSTEMS } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { IEstimateFormData, TEstimateSystemKeys, TEstimatePointsObject, TEstimateTypeError } from "@plane/types"; // ui @@ -13,7 +14,6 @@ import { EstimateCreateStageOne, EstimatePointCreateRoot } from "@/components/es // hooks import { useProjectEstimates } from "@/hooks/store"; // plane web constants -import { EEstimateSystem, ESTIMATE_SYSTEMS } from "@/plane-web/constants/estimates"; type TCreateEstimateModal = { workspaceSlug: string; diff --git a/web/core/components/estimates/create/stage-one.tsx b/web/core/components/estimates/create/stage-one.tsx index 9fcd02bfd..d16827364 100644 --- a/web/core/components/estimates/create/stage-one.tsx +++ b/web/core/components/estimates/create/stage-one.tsx @@ -2,6 +2,7 @@ import { FC } from "react"; import { Info } from "lucide-react"; +import { EEstimateSystem, ESTIMATE_SYSTEMS } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { TEstimateSystemKeys } from "@plane/types"; import { Tooltip } from "@plane/ui"; @@ -11,7 +12,6 @@ import { RadioInput } from "@/components/estimates"; // plane web constants import { isEstimateSystemEnabled } from "@/plane-web/components/estimates/helper"; import { UpgradeBadge } from "@/plane-web/components/workspace"; -import { EEstimateSystem, ESTIMATE_SYSTEMS } from "@/plane-web/constants/estimates"; type TEstimateCreateStageOne = { estimateSystem: TEstimateSystemKeys; diff --git a/web/core/components/estimates/estimate-list-item.tsx b/web/core/components/estimates/estimate-list-item.tsx index e69ab0d46..14846c7de 100644 --- a/web/core/components/estimates/estimate-list-item.tsx +++ b/web/core/components/estimates/estimate-list-item.tsx @@ -1,13 +1,12 @@ import { FC } from "react"; import { observer } from "mobx-react"; +import { EEstimateSystem } from "@plane/constants"; +import { convertMinutesToHoursMinutesString, cn } from "@plane/utils"; // helpers -import { convertMinutesToHoursMinutesString } from "@plane/utils"; -import { cn } from "@/helpers/common.helper"; // hooks import { useEstimate, useProjectEstimates } from "@/hooks/store"; // plane web components import { EstimateListItemButtons } from "@/plane-web/components/estimates"; -import { EEstimateSystem } from "@/plane-web/constants/estimates"; type TEstimateListItem = { estimateId: string; diff --git a/web/core/components/estimates/points/create-root.tsx b/web/core/components/estimates/points/create-root.tsx index 33e220313..7c074245a 100644 --- a/web/core/components/estimates/points/create-root.tsx +++ b/web/core/components/estimates/points/create-root.tsx @@ -3,12 +3,12 @@ import { Dispatch, FC, SetStateAction, useCallback, useState } from "react"; import { observer } from "mobx-react"; import { Plus } from "lucide-react"; +import { estimateCount } from "@plane/constants"; import { TEstimatePointsObject, TEstimateSystemKeys, TEstimateTypeError } from "@plane/types"; import { Button, Sortable } from "@plane/ui"; // components import { EstimatePointCreate, EstimatePointItemPreview } from "@/components/estimates/points"; // plane web constants -import { estimateCount } from "@/plane-web/constants/estimates"; type TEstimatePointCreateRoot = { workspaceSlug: string; diff --git a/web/core/components/estimates/points/create.tsx b/web/core/components/estimates/points/create.tsx index b9742d9b0..5418830ac 100644 --- a/web/core/components/estimates/points/create.tsx +++ b/web/core/components/estimates/points/create.tsx @@ -3,17 +3,16 @@ import { FC, useState, FormEvent } from "react"; import { observer } from "mobx-react"; import { Check, Info, X } from "lucide-react"; +import { EEstimateSystem, MAX_ESTIMATE_POINT_INPUT_LENGTH } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { TEstimatePointsObject, TEstimateSystemKeys, TEstimateTypeErrorObject } from "@plane/types"; import { Spinner, TOAST_TYPE, Tooltip, setToast } from "@plane/ui"; +import { cn, isEstimatePointValuesRepeated } from "@plane/utils"; import { EstimateInputRoot } from "@/components/estimates/inputs/root"; // helpers -import { cn } from "@/helpers/common.helper"; -import { isEstimatePointValuesRepeated } from "@/helpers/estimates"; // hooks import { useEstimate } from "@/hooks/store"; // plane web constants -import { EEstimateSystem, MAX_ESTIMATE_POINT_INPUT_LENGTH } from "@/plane-web/constants/estimates"; type TEstimatePointCreate = { workspaceSlug: string; diff --git a/web/core/components/estimates/points/preview.tsx b/web/core/components/estimates/points/preview.tsx index a5d5235ba..eaac1ed99 100644 --- a/web/core/components/estimates/points/preview.tsx +++ b/web/core/components/estimates/points/preview.tsx @@ -1,6 +1,7 @@ import { FC, useEffect, useRef, useState } from "react"; import { observer } from "mobx-react"; import { GripVertical, Pencil, Trash2 } from "lucide-react"; +import { EEstimateSystem, estimateCount } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { TEstimatePointsObject, TEstimateSystemKeys, TEstimateTypeErrorObject } from "@plane/types"; // components @@ -9,7 +10,6 @@ import { EstimatePointUpdate } from "@/components/estimates/points"; // plane web components import { EstimatePointDelete } from "@/plane-web/components/estimates"; // plane web constants -import { EEstimateSystem, estimateCount } from "@/plane-web/constants/estimates"; type TEstimatePointItemPreview = { workspaceSlug: string; diff --git a/web/core/components/estimates/points/update.tsx b/web/core/components/estimates/points/update.tsx index 9d32ef2d9..9924cb08a 100644 --- a/web/core/components/estimates/points/update.tsx +++ b/web/core/components/estimates/points/update.tsx @@ -3,17 +3,16 @@ import { FC, useEffect, useState, FormEvent } from "react"; import { observer } from "mobx-react"; import { Check, Info, X } from "lucide-react"; +import { EEstimateSystem, MAX_ESTIMATE_POINT_INPUT_LENGTH } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { TEstimatePointsObject, TEstimateSystemKeys, TEstimateTypeErrorObject } from "@plane/types"; import { Spinner, TOAST_TYPE, Tooltip, setToast } from "@plane/ui"; +import { cn, isEstimatePointValuesRepeated } from "@plane/utils"; import { EstimateInputRoot } from "@/components/estimates/inputs/root"; // helpers -import { cn } from "@/helpers/common.helper"; -import { isEstimatePointValuesRepeated } from "@/helpers/estimates"; // hooks import { useEstimatePoint } from "@/hooks/store"; // plane web constants -import { EEstimateSystem, MAX_ESTIMATE_POINT_INPUT_LENGTH } from "@/plane-web/constants/estimates"; type TEstimatePointUpdate = { workspaceSlug: string; diff --git a/web/core/components/estimates/radio-select.tsx b/web/core/components/estimates/radio-select.tsx index 0515b2c8a..2fd691999 100644 --- a/web/core/components/estimates/radio-select.tsx +++ b/web/core/components/estimates/radio-select.tsx @@ -1,6 +1,6 @@ import React from "react"; // helpers -import { cn } from "@/helpers/common.helper"; +import { cn } from "@plane/utils"; type RadioInputProps = { name?: string; diff --git a/web/core/components/exporter/single-export.tsx b/web/core/components/exporter/single-export.tsx index 8bea2ed94..6e8dde3da 100644 --- a/web/core/components/exporter/single-export.tsx +++ b/web/core/components/exporter/single-export.tsx @@ -5,7 +5,7 @@ import { useState, FC } from "react"; import { IExportData } from "@plane/types"; import { Button } from "@plane/ui"; // helpers -import { getDate, renderFormattedDate } from "@/helpers/date-time.helper"; +import { getDate, renderFormattedDate } from "@plane/utils"; // types type Props = { diff --git a/web/core/components/gantt-chart/blocks/block-row-list.tsx b/web/core/components/gantt-chart/blocks/block-row-list.tsx index 183f62657..0be3bead7 100644 --- a/web/core/components/gantt-chart/blocks/block-row-list.tsx +++ b/web/core/components/gantt-chart/blocks/block-row-list.tsx @@ -1,11 +1,11 @@ import { FC } from "react"; // components +import type { IBlockUpdateData, IGanttBlock } from "@plane/types"; import RenderIfVisible from "@/components/core/render-if-visible-HOC"; // hooks import { TSelectionHelper } from "@/hooks/use-multiple-select"; // types import { BLOCK_HEIGHT } from "../constants"; -import { IBlockUpdateData, IGanttBlock } from "../types"; import { BlockRow } from "./block-row"; export type GanttChartBlocksProps = { diff --git a/web/core/components/gantt-chart/blocks/block-row.tsx b/web/core/components/gantt-chart/blocks/block-row.tsx index b215aee4b..f856d201f 100644 --- a/web/core/components/gantt-chart/blocks/block-row.tsx +++ b/web/core/components/gantt-chart/blocks/block-row.tsx @@ -2,7 +2,8 @@ import { useEffect, useState } from "react"; import { observer } from "mobx-react"; import { ArrowRight } from "lucide-react"; // helpers -import { cn } from "@/helpers/common.helper"; +import type { IBlockUpdateData, IGanttBlock } from "@plane/types"; +import { cn } from "@plane/utils"; // hooks import { useIssueDetail } from "@/hooks/store"; import { TSelectionHelper } from "@/hooks/use-multiple-select"; @@ -10,7 +11,6 @@ import { useTimeLineChartStore } from "@/hooks/use-timeline-chart"; // import { BLOCK_HEIGHT, SIDEBAR_WIDTH } from "../constants"; import { ChartAddBlock } from "../helpers"; -import { IBlockUpdateData, IGanttBlock } from "../types"; type Props = { blockId: string; diff --git a/web/core/components/gantt-chart/blocks/block.tsx b/web/core/components/gantt-chart/blocks/block.tsx index 8671993c7..f459a02af 100644 --- a/web/core/components/gantt-chart/blocks/block.tsx +++ b/web/core/components/gantt-chart/blocks/block.tsx @@ -1,9 +1,10 @@ import { RefObject, useRef } from "react"; import { observer } from "mobx-react"; // components +import type { IBlockUpdateDependencyData } from "@plane/types"; +import { cn } from "@plane/utils"; import RenderIfVisible from "@/components/core/render-if-visible-HOC"; // helpers -import { cn } from "@/helpers/common.helper"; // hooks import { useTimeLineChartStore } from "@/hooks/use-timeline-chart"; // constants @@ -11,7 +12,6 @@ import { BLOCK_HEIGHT } from "../constants"; // components import { ChartDraggable } from "../helpers"; import { useGanttResizable } from "../helpers/blockResizables/use-gantt-resizable"; -import { IBlockUpdateDependencyData } from "../types"; type Props = { blockId: string; diff --git a/web/core/components/gantt-chart/blocks/blocks-list.tsx b/web/core/components/gantt-chart/blocks/blocks-list.tsx index 8015819d7..c8644b465 100644 --- a/web/core/components/gantt-chart/blocks/blocks-list.tsx +++ b/web/core/components/gantt-chart/blocks/blocks-list.tsx @@ -1,6 +1,6 @@ import { FC } from "react"; // -import { IBlockUpdateDependencyData } from "../types"; +import type { IBlockUpdateDependencyData } from "@plane/types"; import { GanttChartBlock } from "./block"; export type GanttChartBlocksProps = { diff --git a/web/core/components/gantt-chart/chart/header.tsx b/web/core/components/gantt-chart/chart/header.tsx index 7eb858be3..cc5bd9a3b 100644 --- a/web/core/components/gantt-chart/chart/header.tsx +++ b/web/core/components/gantt-chart/chart/header.tsx @@ -2,16 +2,16 @@ import { observer } from "mobx-react"; import { Expand, Shrink } from "lucide-react"; import { useTranslation } from "@plane/i18n"; // plane +import type { TGanttViews } from "@plane/types"; import { Row } from "@plane/ui"; // components +import { cn } from "@plane/utils"; import { VIEWS_LIST } from "@/components/gantt-chart/data"; // helpers -import { cn } from "@/helpers/common.helper"; // hooks import { useTimeLineChartStore } from "@/hooks/use-timeline-chart"; // import { GANTT_BREADCRUMBS_HEIGHT } from "../constants"; -import { TGanttViews } from "../types"; type Props = { blockIds: string[]; diff --git a/web/core/components/gantt-chart/chart/main-content.tsx b/web/core/components/gantt-chart/chart/main-content.tsx index 63e01c54e..8297bfae5 100644 --- a/web/core/components/gantt-chart/chart/main-content.tsx +++ b/web/core/components/gantt-chart/chart/main-content.tsx @@ -2,23 +2,18 @@ import { useEffect, useRef } from "react"; import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine"; import { autoScrollForElements } from "@atlaskit/pragmatic-drag-and-drop-auto-scroll/element"; import { observer } from "mobx-react"; +import { ChartDataType, IBlockUpdateData, IBlockUpdateDependencyData, IGanttBlock, TGanttViews } from "@plane/types"; +import { cn, getDate } from "@plane/utils"; // components import { MultipleSelectGroup } from "@/components/core"; import { - ChartDataType, GanttChartBlocksList, GanttChartSidebar, - IBlockUpdateData, - IBlockUpdateDependencyData, - IGanttBlock, MonthChartView, QuarterChartView, - TGanttViews, WeekChartView, } from "@/components/gantt-chart"; // helpers -import { cn } from "@/helpers/common.helper"; -import { getDate } from "@/helpers/date-time.helper"; // hooks import { useTimeLineChartStore } from "@/hooks/use-timeline-chart"; // plane web components diff --git a/web/core/components/gantt-chart/chart/root.tsx b/web/core/components/gantt-chart/chart/root.tsx index 16604d881..2509e8d55 100644 --- a/web/core/components/gantt-chart/chart/root.tsx +++ b/web/core/components/gantt-chart/chart/root.tsx @@ -3,16 +3,16 @@ import { observer } from "mobx-react"; // plane imports import { EStartOfTheWeek } from "@plane/constants"; // components +import type { ChartDataType, IBlockUpdateData, IBlockUpdateDependencyData, TGanttViews } from "@plane/types"; +import { cn } from "@plane/utils"; import { GanttChartHeader, GanttChartMainContent } from "@/components/gantt-chart"; // helpers -import { cn } from "@/helpers/common.helper"; // hooks import { useUserProfile } from "@/hooks/store"; import { useTimeLineChartStore } from "@/hooks/use-timeline-chart"; // import { SIDEBAR_WIDTH } from "../constants"; import { currentViewDataWithView } from "../data"; -import { ChartDataType, IBlockUpdateData, IBlockUpdateDependencyData, TGanttViews } from "../types"; import { getNumberOfDaysBetweenTwoDates, IMonthBlock, diff --git a/web/core/components/gantt-chart/chart/views/month.tsx b/web/core/components/gantt-chart/chart/views/month.tsx index bfe29b90d..01f6aa801 100644 --- a/web/core/components/gantt-chart/chart/views/month.tsx +++ b/web/core/components/gantt-chart/chart/views/month.tsx @@ -1,9 +1,9 @@ import { FC } from "react"; import { observer } from "mobx-react"; // components +import { cn } from "@plane/utils"; import { HEADER_HEIGHT, SIDEBAR_WIDTH } from "@/components/gantt-chart/constants"; // helpers -import { cn } from "@/helpers/common.helper"; // hooks import { useTimeLineChartStore } from "@/hooks/use-timeline-chart"; // types diff --git a/web/core/components/gantt-chart/data/index.ts b/web/core/components/gantt-chart/data/index.ts index 2e72810d8..1871278f8 100644 --- a/web/core/components/gantt-chart/data/index.ts +++ b/web/core/components/gantt-chart/data/index.ts @@ -1,6 +1,6 @@ // types import { EStartOfTheWeek } from "@plane/constants"; -import { WeekMonthDataType, ChartDataType, TGanttViews } from "../types"; +import type { WeekMonthDataType, ChartDataType, TGanttViews } from "@plane/types"; // constants export const generateWeeks = (startOfWeek: EStartOfTheWeek = EStartOfTheWeek.SUNDAY): WeekMonthDataType[] => [ diff --git a/web/core/components/gantt-chart/helpers/add-block.tsx b/web/core/components/gantt-chart/helpers/add-block.tsx index 0c8ccdcb1..fed218f91 100644 --- a/web/core/components/gantt-chart/helpers/add-block.tsx +++ b/web/core/components/gantt-chart/helpers/add-block.tsx @@ -5,14 +5,14 @@ import { addDays } from "date-fns"; import { observer } from "mobx-react"; import { Plus } from "lucide-react"; // ui +import type { IBlockUpdateData, IGanttBlock } from "@plane/types"; import { Tooltip } from "@plane/ui"; // helpers -import { renderFormattedDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper"; +import { renderFormattedDate, renderFormattedPayloadDate } from "@plane/utils"; // hooks import { usePlatformOS } from "@/hooks/use-platform-os"; import { useTimeLineChartStore } from "@/hooks/use-timeline-chart"; // -import { IBlockUpdateData, IGanttBlock } from "../types"; type Props = { block: IGanttBlock; diff --git a/web/core/components/gantt-chart/helpers/blockResizables/left-resizable.tsx b/web/core/components/gantt-chart/helpers/blockResizables/left-resizable.tsx index 99b96e786..48e7277c3 100644 --- a/web/core/components/gantt-chart/helpers/blockResizables/left-resizable.tsx +++ b/web/core/components/gantt-chart/helpers/blockResizables/left-resizable.tsx @@ -1,9 +1,9 @@ import { useState } from "react"; import { observer } from "mobx-react"; // plane utils -import { cn } from "@plane/utils"; +import { cn, renderFormattedDate } from "@plane/utils"; //helpers -import { renderFormattedDate } from "@/helpers/date-time.helper"; +// //hooks import { useTimeLineChartStore } from "@/hooks/use-timeline-chart"; diff --git a/web/core/components/gantt-chart/helpers/blockResizables/right-resizable.tsx b/web/core/components/gantt-chart/helpers/blockResizables/right-resizable.tsx index 8370a7630..8b4f0ea49 100644 --- a/web/core/components/gantt-chart/helpers/blockResizables/right-resizable.tsx +++ b/web/core/components/gantt-chart/helpers/blockResizables/right-resizable.tsx @@ -1,9 +1,9 @@ import { useState } from "react"; import { observer } from "mobx-react"; // plane utils -import { cn } from "@plane/utils"; +import { cn, renderFormattedDate } from "@plane/utils"; //helpers -import { renderFormattedDate } from "@/helpers/date-time.helper"; +// //hooks import { useTimeLineChartStore } from "@/hooks/use-timeline-chart"; diff --git a/web/core/components/gantt-chart/helpers/blockResizables/use-gantt-resizable.ts b/web/core/components/gantt-chart/helpers/blockResizables/use-gantt-resizable.ts index c96a25725..b96e58390 100644 --- a/web/core/components/gantt-chart/helpers/blockResizables/use-gantt-resizable.ts +++ b/web/core/components/gantt-chart/helpers/blockResizables/use-gantt-resizable.ts @@ -1,11 +1,11 @@ import { useRef, useState } from "react"; // Plane +import type { IBlockUpdateDependencyData, IGanttBlock } from "@plane/types"; import { setToast, TOAST_TYPE } from "@plane/ui"; // hooks import { useTimeLineChartStore } from "@/hooks/use-timeline-chart"; // import { DEFAULT_BLOCK_WIDTH, SIDEBAR_WIDTH } from "../../constants"; -import { IBlockUpdateDependencyData, IGanttBlock } from "../../types"; export const useGanttResizable = ( block: IGanttBlock, diff --git a/web/core/components/gantt-chart/helpers/draggable.tsx b/web/core/components/gantt-chart/helpers/draggable.tsx index a19b19f63..35babf9c3 100644 --- a/web/core/components/gantt-chart/helpers/draggable.tsx +++ b/web/core/components/gantt-chart/helpers/draggable.tsx @@ -1,9 +1,9 @@ import React, { RefObject } from "react"; import { observer } from "mobx-react"; // hooks -import { IGanttBlock } from "@/components/gantt-chart"; +import type { IGanttBlock } from "@plane/types"; // helpers -import { cn } from "@/helpers/common.helper"; +import { cn } from "@plane/utils"; // Plane-web import { LeftDependencyDraggable, RightDependencyDraggable } from "@/plane-web/components/gantt-chart"; // diff --git a/web/core/components/gantt-chart/index.ts b/web/core/components/gantt-chart/index.ts index bb2cbc99c..2d3055c2c 100644 --- a/web/core/components/gantt-chart/index.ts +++ b/web/core/components/gantt-chart/index.ts @@ -3,4 +3,3 @@ export * from "./chart"; export * from "./helpers"; export * from "./root"; export * from "./sidebar"; -export * from "./types"; diff --git a/web/core/components/gantt-chart/root.tsx b/web/core/components/gantt-chart/root.tsx index 3ed21d144..3e761477c 100644 --- a/web/core/components/gantt-chart/root.tsx +++ b/web/core/components/gantt-chart/root.tsx @@ -1,9 +1,10 @@ import { FC, useEffect } from "react"; import { observer } from "mobx-react"; // components -import { ChartViewRoot, IBlockUpdateData, IBlockUpdateDependencyData } from "@/components/gantt-chart"; +import type { IBlockUpdateData, IBlockUpdateDependencyData } from "@plane/types"; // hooks import { useTimeLineChartStore } from "@/hooks/use-timeline-chart"; +import { ChartViewRoot } from "./chart/root"; type GanttChartRootProps = { border?: boolean; diff --git a/web/core/components/gantt-chart/sidebar/issues/block.tsx b/web/core/components/gantt-chart/sidebar/issues/block.tsx index c0218aceb..730e9c6c5 100644 --- a/web/core/components/gantt-chart/sidebar/issues/block.tsx +++ b/web/core/components/gantt-chart/sidebar/issues/block.tsx @@ -1,10 +1,11 @@ import { observer } from "mobx-react"; // components +import type { IGanttBlock } from "@plane/types"; import { Row } from "@plane/ui"; +import { cn } from "@plane/utils"; import { MultipleSelectEntityAction } from "@/components/core"; import { IssueGanttSidebarBlock } from "@/components/issues"; // helpers -import { cn } from "@/helpers/common.helper"; // hooks import { useIssueDetail } from "@/hooks/store"; import { TSelectionHelper } from "@/hooks/use-multiple-select"; @@ -12,7 +13,6 @@ import { useTimeLineChartStore } from "@/hooks/use-timeline-chart"; // constants import { BLOCK_HEIGHT, GANTT_SELECT_GROUP } from "../../constants"; // types -import { IGanttBlock } from "../../types"; type Props = { block: IGanttBlock; diff --git a/web/core/components/gantt-chart/sidebar/issues/sidebar.tsx b/web/core/components/gantt-chart/sidebar/issues/sidebar.tsx index d2e5557ff..30be6512b 100644 --- a/web/core/components/gantt-chart/sidebar/issues/sidebar.tsx +++ b/web/core/components/gantt-chart/sidebar/issues/sidebar.tsx @@ -3,10 +3,10 @@ import { RefObject, useState } from "react"; import { observer } from "mobx-react"; // ui +import type { IBlockUpdateData } from "@plane/types"; import { Loader } from "@plane/ui"; // components import RenderIfVisible from "@/components/core/render-if-visible-HOC"; -import { IBlockUpdateData } from "@/components/gantt-chart/types"; import { GanttLayoutLIstItem } from "@/components/ui"; //hooks import { useIntersectionObserver } from "@/hooks/use-intersection-observer"; diff --git a/web/core/components/gantt-chart/sidebar/modules/block.tsx b/web/core/components/gantt-chart/sidebar/modules/block.tsx index fb8eaf481..04563db61 100644 --- a/web/core/components/gantt-chart/sidebar/modules/block.tsx +++ b/web/core/components/gantt-chart/sidebar/modules/block.tsx @@ -2,10 +2,10 @@ import { observer } from "mobx-react"; // Plane import { Row } from "@plane/ui"; // components +import { cn } from "@plane/utils"; import { BLOCK_HEIGHT } from "@/components/gantt-chart/constants"; import { ModuleGanttSidebarBlock } from "@/components/modules"; // helpers -import { cn } from "@/helpers/common.helper"; // hooks import { useTimeLineChartStore } from "@/hooks/use-timeline-chart"; diff --git a/web/core/components/gantt-chart/sidebar/modules/sidebar.tsx b/web/core/components/gantt-chart/sidebar/modules/sidebar.tsx index 182fcefac..9c3715a76 100644 --- a/web/core/components/gantt-chart/sidebar/modules/sidebar.tsx +++ b/web/core/components/gantt-chart/sidebar/modules/sidebar.tsx @@ -2,9 +2,9 @@ import { observer } from "mobx-react"; // ui +import type { IBlockUpdateData } from "@plane/types"; import { Loader } from "@plane/ui"; // components -import { IBlockUpdateData } from "@/components/gantt-chart"; // hooks import { useTimeLineChart } from "@/hooks/use-timeline-chart"; // diff --git a/web/core/components/gantt-chart/sidebar/root.tsx b/web/core/components/gantt-chart/sidebar/root.tsx index 7f6866ad6..e4f2f8d0c 100644 --- a/web/core/components/gantt-chart/sidebar/root.tsx +++ b/web/core/components/gantt-chart/sidebar/root.tsx @@ -2,11 +2,11 @@ import { RefObject } from "react"; import { observer } from "mobx-react"; import { useTranslation } from "@plane/i18n"; // components +import type { ChartDataType, IBlockUpdateData, IGanttBlock } from "@plane/types"; import { Row, ERowVariant } from "@plane/ui"; +import { cn } from "@plane/utils"; import { MultipleSelectGroupAction } from "@/components/core"; -import { ChartDataType, IBlockUpdateData, IGanttBlock } from "@/components/gantt-chart"; // helpers -import { cn } from "@/helpers/common.helper"; // hooks import { TSelectionHelper } from "@/hooks/use-multiple-select"; // constants diff --git a/web/core/components/gantt-chart/sidebar/utils.ts b/web/core/components/gantt-chart/sidebar/utils.ts index 15b8e55fc..28590e348 100644 --- a/web/core/components/gantt-chart/sidebar/utils.ts +++ b/web/core/components/gantt-chart/sidebar/utils.ts @@ -1,4 +1,4 @@ -import { ChartDataType, IBlockUpdateData, IGanttBlock } from "../types"; +import type { ChartDataType, IBlockUpdateData, IGanttBlock } from "@plane/types"; export const handleOrderChange = ( draggingBlockId: string | undefined, diff --git a/web/core/components/gantt-chart/views/helpers.ts b/web/core/components/gantt-chart/views/helpers.ts index 81064bd46..3a603f70b 100644 --- a/web/core/components/gantt-chart/views/helpers.ts +++ b/web/core/components/gantt-chart/views/helpers.ts @@ -1,6 +1,6 @@ -import { addDaysToDate, findTotalDaysInRange, getDate } from "@/helpers/date-time.helper"; +import type { ChartDataType, IGanttBlock } from "@plane/types"; +import { addDaysToDate, findTotalDaysInRange, getDate } from "@plane/utils"; import { DEFAULT_BLOCK_WIDTH } from "../constants"; -import { ChartDataType, IGanttBlock } from "../types"; /** * Generates Date by using Day, month and Year diff --git a/web/core/components/gantt-chart/views/month-view.ts b/web/core/components/gantt-chart/views/month-view.ts index 41acf14d5..178a68d67 100644 --- a/web/core/components/gantt-chart/views/month-view.ts +++ b/web/core/components/gantt-chart/views/month-view.ts @@ -1,8 +1,8 @@ import cloneDeep from "lodash/cloneDeep"; import uniqBy from "lodash/uniqBy"; // +import type { ChartDataType } from "@plane/types"; import { months } from "../data"; -import { ChartDataType } from "../types"; import { getNumberOfDaysBetweenTwoDates, getNumberOfDaysInMonth } from "./helpers"; import { getWeeksBetweenTwoDates, IWeekBlock } from "./week-view"; diff --git a/web/core/components/gantt-chart/views/quarter-view.ts b/web/core/components/gantt-chart/views/quarter-view.ts index a584a26e3..b8541bf98 100644 --- a/web/core/components/gantt-chart/views/quarter-view.ts +++ b/web/core/components/gantt-chart/views/quarter-view.ts @@ -1,6 +1,6 @@ // +import type { ChartDataType } from "@plane/types"; import { quarters } from "../data"; -import { ChartDataType } from "../types"; import { getNumberOfDaysBetweenTwoDates } from "./helpers"; import { getMonthsBetweenTwoDates, IMonthBlock } from "./month-view"; diff --git a/web/core/components/gantt-chart/views/week-view.ts b/web/core/components/gantt-chart/views/week-view.ts index ea3d75f91..c7b4ce6f4 100644 --- a/web/core/components/gantt-chart/views/week-view.ts +++ b/web/core/components/gantt-chart/views/week-view.ts @@ -1,7 +1,7 @@ // import { EStartOfTheWeek } from "@plane/constants"; +import type { ChartDataType } from "@plane/types"; import { months, generateWeeks } from "../data"; -import { ChartDataType } from "../types"; import { getNumberOfDaysBetweenTwoDates, getWeekNumberByDate } from "./helpers"; export interface IDayBlock { date: Date; diff --git a/web/core/components/global/product-updates/footer.tsx b/web/core/components/global/product-updates/footer.tsx index 5d402e85e..84966e637 100644 --- a/web/core/components/global/product-updates/footer.tsx +++ b/web/core/components/global/product-updates/footer.tsx @@ -3,7 +3,7 @@ import { useTranslation } from "@plane/i18n"; // ui import { getButtonStyling } from "@plane/ui"; // helpers -import { cn } from "@/helpers/common.helper"; +import { cn } from "@plane/utils"; // assets import PlaneLogo from "@/public/plane-logos/blue-without-text.png"; diff --git a/web/core/components/home/root.tsx b/web/core/components/home/root.tsx index b4e75ce95..c350d3119 100644 --- a/web/core/components/home/root.tsx +++ b/web/core/components/home/root.tsx @@ -4,10 +4,10 @@ import { useParams } from "next/navigation"; import useSWR from "swr"; import { PRODUCT_TOUR_COMPLETED } from "@plane/constants"; import { ContentWrapper } from "@plane/ui"; +import { cn } from "@plane/utils"; import { TourRoot } from "@/components/onboarding"; // constants // helpers -import { cn } from "@/helpers/common.helper"; // hooks import { useUserProfile, useEventTracker, useUser } from "@/hooks/store"; import { useHome } from "@/hooks/store/use-home"; diff --git a/web/core/components/home/widgets/empty-states/no-projects.tsx b/web/core/components/home/widgets/empty-states/no-projects.tsx index f3f35d5a1..be4c8af64 100644 --- a/web/core/components/home/widgets/empty-states/no-projects.tsx +++ b/web/core/components/home/widgets/empty-states/no-projects.tsx @@ -8,9 +8,8 @@ import { Briefcase, Check, Hotel, Users, X } from "lucide-react"; import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; import { useLocalStorage } from "@plane/hooks"; import { useTranslation } from "@plane/i18n"; +import { cn, getFileURL } from "@plane/utils"; // helpers -import { cn } from "@plane/utils"; -import { getFileURL } from "@/helpers/file.helper"; // hooks import { useCommandPalette, diff --git a/web/core/components/home/widgets/manage/widget-item-drag-handle.tsx b/web/core/components/home/widgets/manage/widget-item-drag-handle.tsx index 2995ce086..b19f74d24 100644 --- a/web/core/components/home/widgets/manage/widget-item-drag-handle.tsx +++ b/web/core/components/home/widgets/manage/widget-item-drag-handle.tsx @@ -4,7 +4,7 @@ import { observer } from "mobx-react"; // ui import { DragHandle } from "@plane/ui"; // helper -import { cn } from "@/helpers/common.helper"; +import { cn } from "@plane/utils"; type Props = { sort_order: number | null; diff --git a/web/core/components/home/widgets/recents/issue.tsx b/web/core/components/home/widgets/recents/issue.tsx index d4be2f7f9..33778b641 100644 --- a/web/core/components/home/widgets/recents/issue.tsx +++ b/web/core/components/home/widgets/recents/issue.tsx @@ -5,12 +5,11 @@ import { EIssueServiceType } from "@plane/constants"; import { TActivityEntityData, TIssueEntityData } from "@plane/types"; // plane ui import { LayersIcon, PriorityIcon, StateGroupIcon, Tooltip } from "@plane/ui"; +import { calculateTimeAgo, generateWorkItemLink } from "@plane/utils"; // components import { ListItem } from "@/components/core/list"; import { MemberDropdown } from "@/components/dropdowns"; // helpers -import { calculateTimeAgo } from "@/helpers/date-time.helper"; -import { generateWorkItemLink } from "@/helpers/issue.helper"; // hooks import { useIssueDetail, useProject, useProjectState } from "@/hooks/store"; // plane web components diff --git a/web/core/components/home/widgets/recents/page.tsx b/web/core/components/home/widgets/recents/page.tsx index 532831d32..b1ded3f44 100644 --- a/web/core/components/home/widgets/recents/page.tsx +++ b/web/core/components/home/widgets/recents/page.tsx @@ -4,13 +4,12 @@ import { FileText } from "lucide-react"; import { TActivityEntityData, TPageEntityData } from "@plane/types"; // plane ui import { Avatar, Logo } from "@plane/ui"; +import { calculateTimeAgo, getFileURL, getPageName } from "@plane/utils"; // plane utils -import { getFileURL } from "@plane/utils"; // components import { ListItem } from "@/components/core/list"; // helpers -import { calculateTimeAgo } from "@/helpers/date-time.helper"; -import { getPageName } from "@/helpers/page.helper"; +// // hooks import { useMember } from "@/hooks/store"; diff --git a/web/core/components/home/widgets/recents/project.tsx b/web/core/components/home/widgets/recents/project.tsx index 9321a017f..4943dbde0 100644 --- a/web/core/components/home/widgets/recents/project.tsx +++ b/web/core/components/home/widgets/recents/project.tsx @@ -4,10 +4,10 @@ import { TActivityEntityData, TProjectEntityData } from "@plane/types"; // plane ui import { Logo } from "@plane/ui"; // components +import { calculateTimeAgo } from "@plane/utils"; import { ListItem } from "@/components/core/list"; import { MemberDropdown } from "@/components/dropdowns"; // helpers -import { calculateTimeAgo } from "@/helpers/date-time.helper"; type BlockProps = { activity: TActivityEntityData; diff --git a/web/core/components/inbox/content/inbox-issue-header.tsx b/web/core/components/inbox/content/inbox-issue-header.tsx index f1ae748eb..16e4835cf 100644 --- a/web/core/components/inbox/content/inbox-issue-header.tsx +++ b/web/core/components/inbox/content/inbox-issue-header.tsx @@ -15,11 +15,11 @@ import { MoveRight, Copy, } from "lucide-react"; -import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; +import { EUserPermissions, EUserPermissionsLevel, EInboxIssueStatus } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { TNameDescriptionLoader } from "@plane/types"; import { Button, ControlLink, CustomMenu, Row, TOAST_TYPE, setToast } from "@plane/ui"; -import { copyUrlToClipboard } from "@plane/utils"; +import { copyUrlToClipboard, findHowManyDaysLeft, generateWorkItemLink } from "@plane/utils"; // components import { DeclineIssueModal, @@ -31,9 +31,7 @@ import { } from "@/components/inbox"; import { CreateUpdateIssueModal, NameDescriptionUpdateStatus } from "@/components/issues"; // helpers -import { findHowManyDaysLeft } from "@/helpers/date-time.helper"; -import { EInboxIssueStatus } from "@/helpers/inbox.helper"; -import { generateWorkItemLink } from "@/helpers/issue.helper"; +// // hooks import { useUser, useProjectInbox, useProject, useUserPermissions } from "@/hooks/store"; import { useAppRouter } from "@/hooks/use-app-router"; diff --git a/web/core/components/inbox/content/inbox-issue-mobile-header.tsx b/web/core/components/inbox/content/inbox-issue-mobile-header.tsx index 93b823587..d5ade21fc 100644 --- a/web/core/components/inbox/content/inbox-issue-mobile-header.tsx +++ b/web/core/components/inbox/content/inbox-issue-mobile-header.tsx @@ -17,13 +17,11 @@ import { } from "lucide-react"; import { TNameDescriptionLoader } from "@plane/types"; import { Header, CustomMenu, EHeaderVariant } from "@plane/ui"; +import { cn, findHowManyDaysLeft, generateWorkItemLink } from "@plane/utils"; // components import { InboxIssueStatus } from "@/components/inbox"; import { NameDescriptionUpdateStatus } from "@/components/issues"; // helpers -import { cn } from "@/helpers/common.helper"; -import { findHowManyDaysLeft } from "@/helpers/date-time.helper"; -import { generateWorkItemLink } from "@/helpers/issue.helper"; // hooks import { useProject } from "@/hooks/store"; import { useAppRouter } from "@/hooks/use-app-router"; diff --git a/web/core/components/inbox/content/issue-properties.tsx b/web/core/components/inbox/content/issue-properties.tsx index e189df9fa..d5bfa4ebc 100644 --- a/web/core/components/inbox/content/issue-properties.tsx +++ b/web/core/components/inbox/content/issue-properties.tsx @@ -5,12 +5,11 @@ import { observer } from "mobx-react"; import { CalendarCheck2, CopyPlus, Signal, Tag, Users } from "lucide-react"; import { TInboxDuplicateIssueDetails, TIssue } from "@plane/types"; import { ControlLink, DoubleCircleIcon, Tooltip } from "@plane/ui"; +import { getDate, renderFormattedPayloadDate, generateWorkItemLink } from "@plane/utils"; // components import { DateDropdown, PriorityDropdown, MemberDropdown, StateDropdown } from "@/components/dropdowns"; import { IssueLabel, TIssueOperations } from "@/components/issues"; // helper -import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper"; -import { generateWorkItemLink } from "@/helpers/issue.helper"; // hooks import { useProject } from "@/hooks/store"; import { useAppRouter } from "@/hooks/use-app-router"; diff --git a/web/core/components/inbox/content/issue-root.tsx b/web/core/components/inbox/content/issue-root.tsx index 3aee29050..e04698343 100644 --- a/web/core/components/inbox/content/issue-root.tsx +++ b/web/core/components/inbox/content/issue-root.tsx @@ -9,6 +9,7 @@ import { EditorRefApi } from "@plane/editor"; import { TIssue, TNameDescriptionLoader } from "@plane/types"; import { Loader, TOAST_TYPE, setToast } from "@plane/ui"; // components +import { getTextContent } from "@plane/utils"; import { DescriptionVersionsRoot } from "@/components/core/description-versions"; import { InboxIssueContentProperties } from "@/components/inbox/content"; import { @@ -20,7 +21,6 @@ import { IssueAttachmentRoot, } from "@/components/issues"; // helpers -import { getTextContent } from "@/helpers/editor.helper"; // hooks import { useEventTracker, useIssueDetail, useMember, useProject, useProjectInbox, useUser } from "@/hooks/store"; import useReloadConfirmations from "@/hooks/use-reload-confirmation"; diff --git a/web/core/components/inbox/inbox-filter/applied-filters/date.tsx b/web/core/components/inbox/inbox-filter/applied-filters/date.tsx index b8673f205..956f75719 100644 --- a/web/core/components/inbox/inbox-filter/applied-filters/date.tsx +++ b/web/core/components/inbox/inbox-filter/applied-filters/date.tsx @@ -1,12 +1,12 @@ import { FC } from "react"; import { observer } from "mobx-react"; import { X } from "lucide-react"; +import { PAST_DURATION_FILTER_OPTIONS } from "@plane/constants"; import { TInboxIssueFilterDateKeys } from "@plane/types"; // helpers import { Tag } from "@plane/ui"; -import { renderFormattedDate } from "@/helpers/date-time.helper"; +import { renderFormattedDate } from "@plane/utils"; // constants -import { PAST_DURATION_FILTER_OPTIONS } from "@/helpers/inbox.helper"; // hooks import { useProjectInbox } from "@/hooks/store"; diff --git a/web/core/components/inbox/inbox-filter/applied-filters/member.tsx b/web/core/components/inbox/inbox-filter/applied-filters/member.tsx index aff556c62..a93095579 100644 --- a/web/core/components/inbox/inbox-filter/applied-filters/member.tsx +++ b/web/core/components/inbox/inbox-filter/applied-filters/member.tsx @@ -8,7 +8,7 @@ import { TInboxIssueFilterMemberKeys } from "@plane/types"; // plane ui import { Avatar, Tag } from "@plane/ui"; // helpers -import { getFileURL } from "@/helpers/file.helper"; +import { getFileURL } from "@plane/utils"; // hooks import { useMember, useProjectInbox } from "@/hooks/store"; diff --git a/web/core/components/inbox/inbox-filter/filters/date.tsx b/web/core/components/inbox/inbox-filter/filters/date.tsx index aee188be8..4eb8d9d31 100644 --- a/web/core/components/inbox/inbox-filter/filters/date.tsx +++ b/web/core/components/inbox/inbox-filter/filters/date.tsx @@ -2,12 +2,12 @@ import { FC, useState } from "react"; import concat from "lodash/concat"; import uniq from "lodash/uniq"; import { observer } from "mobx-react"; +import { PAST_DURATION_FILTER_OPTIONS } from "@plane/constants"; import { TInboxIssueFilterDateKeys } from "@plane/types"; // components import { DateFilterModal } from "@/components/core"; import { FilterHeader, FilterOption } from "@/components/issues"; // constants -import { PAST_DURATION_FILTER_OPTIONS } from "@/helpers/inbox.helper"; // hooks import { useProjectInbox } from "@/hooks/store"; diff --git a/web/core/components/inbox/inbox-filter/filters/members.tsx b/web/core/components/inbox/inbox-filter/filters/members.tsx index 1dcf2de68..77ce95c4e 100644 --- a/web/core/components/inbox/inbox-filter/filters/members.tsx +++ b/web/core/components/inbox/inbox-filter/filters/members.tsx @@ -8,9 +8,9 @@ import { TInboxIssueFilterMemberKeys } from "@plane/types"; // plane ui import { Avatar, Loader } from "@plane/ui"; // components +import { getFileURL } from "@plane/utils"; import { FilterHeader, FilterOption } from "@/components/issues"; // helpers -import { getFileURL } from "@/helpers/file.helper"; // hooks import { useMember, useProjectInbox, useUser } from "@/hooks/store"; diff --git a/web/core/components/inbox/inbox-filter/sorting/order-by.tsx b/web/core/components/inbox/inbox-filter/sorting/order-by.tsx index d3dabbf86..b65a20160 100644 --- a/web/core/components/inbox/inbox-filter/sorting/order-by.tsx +++ b/web/core/components/inbox/inbox-filter/sorting/order-by.tsx @@ -9,7 +9,7 @@ import { TInboxIssueSortingOrderByKeys, TInboxIssueSortingSortByKeys } from "@pl import { CustomMenu, getButtonStyling } from "@plane/ui"; // constants // helpers -import { cn } from "@/helpers/common.helper"; +import { cn } from "@plane/utils"; // hooks import { useProjectInbox } from "@/hooks/store"; import useSize from "@/hooks/use-window-size"; diff --git a/web/core/components/inbox/inbox-issue-status.tsx b/web/core/components/inbox/inbox-issue-status.tsx index 8e7867a75..00a7d0887 100644 --- a/web/core/components/inbox/inbox-issue-status.tsx +++ b/web/core/components/inbox/inbox-issue-status.tsx @@ -4,8 +4,7 @@ import { observer } from "mobx-react"; // helpers import { INBOX_STATUS } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; -import { cn } from "@/helpers/common.helper"; -import { findHowManyDaysLeft } from "@/helpers/date-time.helper"; +import { cn, findHowManyDaysLeft } from "@plane/utils"; // store import { IInboxIssueStore } from "@/store/inbox/inbox-issue.store"; import { ICON_PROPERTIES, InboxStatusIcon } from "./inbox-status-icon"; diff --git a/web/core/components/inbox/modals/create-modal/create-root.tsx b/web/core/components/inbox/modals/create-modal/create-root.tsx index b0a7cb342..1afccf7e0 100644 --- a/web/core/components/inbox/modals/create-modal/create-root.tsx +++ b/web/core/components/inbox/modals/create-modal/create-root.tsx @@ -10,12 +10,11 @@ import { EditorRefApi } from "@plane/editor"; import { useTranslation } from "@plane/i18n"; import { TIssue } from "@plane/types"; import { Button, ToggleSwitch, TOAST_TYPE, setToast } from "@plane/ui"; +import { renderFormattedPayloadDate, getTabIndex } from "@plane/utils"; // components import { InboxIssueTitle, InboxIssueDescription, InboxIssueProperties } from "@/components/inbox/modals/create-modal"; // constants // helpers -import { renderFormattedPayloadDate } from "@/helpers/date-time.helper"; -import { getTabIndex } from "@/helpers/tab-indices.helper"; // hooks import { useEventTracker, useProject, useProjectInbox, useWorkspace } from "@/hooks/store"; import { useAppRouter } from "@/hooks/use-app-router"; diff --git a/web/core/components/inbox/modals/create-modal/issue-description.tsx b/web/core/components/inbox/modals/create-modal/issue-description.tsx index 4841f9ca1..b2511e5d5 100644 --- a/web/core/components/inbox/modals/create-modal/issue-description.tsx +++ b/web/core/components/inbox/modals/create-modal/issue-description.tsx @@ -13,11 +13,10 @@ import { TIssue } from "@plane/types"; import { EFileAssetType } from "@plane/types/src/enums"; // ui import { Loader } from "@plane/ui"; +import { getDescriptionPlaceholderI18n, getTabIndex } from "@plane/utils"; // components import { RichTextEditor } from "@/components/editor/rich-text-editor/rich-text-editor"; // helpers -import { getDescriptionPlaceholderI18n } from "@/helpers/issue.helper"; -import { getTabIndex } from "@/helpers/tab-indices.helper"; // hooks import { useEditorAsset, useProjectInbox } from "@/hooks/store"; import { usePlatformOS } from "@/hooks/use-platform-os"; diff --git a/web/core/components/inbox/modals/create-modal/issue-properties.tsx b/web/core/components/inbox/modals/create-modal/issue-properties.tsx index e237503f4..5f87202cd 100644 --- a/web/core/components/inbox/modals/create-modal/issue-properties.tsx +++ b/web/core/components/inbox/modals/create-modal/issue-properties.tsx @@ -5,6 +5,7 @@ import { LayoutPanelTop } from "lucide-react"; import { ETabIndices } from "@plane/constants"; import { ISearchIssueResponse, TIssue } from "@plane/types"; import { CustomMenu } from "@plane/ui"; +import { renderFormattedPayloadDate, getDate, getTabIndex } from "@plane/utils"; // components import { CycleDropdown, @@ -18,8 +19,6 @@ import { import { ParentIssuesListModal } from "@/components/issues"; import { IssueLabelSelect } from "@/components/issues/select"; // helpers -import { renderFormattedPayloadDate, getDate } from "@/helpers/date-time.helper"; -import { getTabIndex } from "@/helpers/tab-indices.helper"; // hooks import { useProjectEstimates } from "@/hooks/store"; import { usePlatformOS } from "@/hooks/use-platform-os"; diff --git a/web/core/components/inbox/modals/create-modal/issue-title.tsx b/web/core/components/inbox/modals/create-modal/issue-title.tsx index cd645fb70..18a38e1f5 100644 --- a/web/core/components/inbox/modals/create-modal/issue-title.tsx +++ b/web/core/components/inbox/modals/create-modal/issue-title.tsx @@ -8,7 +8,7 @@ import { useTranslation } from "@plane/i18n"; import { TIssue } from "@plane/types"; import { Input } from "@plane/ui"; // helpers -import { getTabIndex } from "@/helpers/tab-indices.helper"; +import { getTabIndex } from "@plane/utils"; // hooks import { usePlatformOS } from "@/hooks/use-platform-os"; diff --git a/web/core/components/inbox/root.tsx b/web/core/components/inbox/root.tsx index 010b76e96..17e8aefef 100644 --- a/web/core/components/inbox/root.tsx +++ b/web/core/components/inbox/root.tsx @@ -2,15 +2,15 @@ import { FC, useEffect, useState } from "react"; import { observer } from "mobx-react"; import { PanelLeft } from "lucide-react"; // plane imports +import { EInboxIssueCurrentTab } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { Intake } from "@plane/ui"; // components +import { cn } from "@plane/utils"; import { SimpleEmptyState } from "@/components/empty-state"; import { InboxSidebar, InboxContentRoot } from "@/components/inbox"; import { InboxLayoutLoader } from "@/components/ui"; // helpers -import { cn } from "@/helpers/common.helper"; -import { EInboxIssueCurrentTab } from "@/helpers/inbox.helper"; // hooks import { useProjectInbox } from "@/hooks/store"; import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; diff --git a/web/core/components/inbox/sidebar/inbox-list-item.tsx b/web/core/components/inbox/sidebar/inbox-list-item.tsx index 05fcf9f3c..308092fee 100644 --- a/web/core/components/inbox/sidebar/inbox-list-item.tsx +++ b/web/core/components/inbox/sidebar/inbox-list-item.tsx @@ -5,14 +5,12 @@ import { observer } from "mobx-react"; import Link from "next/link"; import { useSearchParams } from "next/navigation"; import { Tooltip, PriorityIcon, Row, Avatar } from "@plane/ui"; +import { cn, renderFormattedDate, getFileURL } from "@plane/utils"; // components import { ButtonAvatars } from "@/components/dropdowns/member/avatar"; import { InboxIssueStatus } from "@/components/inbox"; // helpers -import { cn } from "@/helpers/common.helper"; -import { renderFormattedDate } from "@/helpers/date-time.helper"; // hooks -import { getFileURL } from "@/helpers/file.helper"; import { useLabel, useMember, useProjectInbox } from "@/hooks/store"; import { usePlatformOS } from "@/hooks/use-platform-os"; import { InboxSourcePill } from "@/plane-web/components/inbox/source-pill"; diff --git a/web/core/components/inbox/sidebar/root.tsx b/web/core/components/inbox/sidebar/root.tsx index f30f5565d..15c0410f0 100644 --- a/web/core/components/inbox/sidebar/root.tsx +++ b/web/core/components/inbox/sidebar/root.tsx @@ -2,17 +2,16 @@ import { FC, useCallback, useEffect, useRef, useState } from "react"; import { observer } from "mobx-react"; +import { TInboxIssueCurrentTab, EInboxIssueCurrentTab } from "@plane/constants"; // plane imports -import { TInboxIssueCurrentTab } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { Header, Loader, EHeaderVariant } from "@plane/ui"; // components +import { cn } from "@plane/utils"; import { SimpleEmptyState } from "@/components/empty-state"; import { FiltersRoot, InboxIssueAppliedFilters, InboxIssueList } from "@/components/inbox"; import { InboxSidebarLoader } from "@/components/ui"; // helpers -import { cn } from "@/helpers/common.helper"; -import { EInboxIssueCurrentTab } from "@/helpers/inbox.helper"; // hooks import { useProject, useProjectInbox } from "@/hooks/store"; import { useAppRouter } from "@/hooks/use-app-router"; diff --git a/web/core/components/instance/not-ready-view.tsx b/web/core/components/instance/not-ready-view.tsx index 8d317634a..1d978b7ab 100644 --- a/web/core/components/instance/not-ready-view.tsx +++ b/web/core/components/instance/not-ready-view.tsx @@ -4,9 +4,9 @@ import { FC } from "react"; import Image from "next/image"; import Link from "next/link"; import { useTheme } from "next-themes"; +import { GOD_MODE_URL } from "@plane/constants"; import { Button } from "@plane/ui"; // helpers -import { GOD_MODE_URL } from "@/helpers/common.helper"; // images // assets import PlaneBackgroundPatternDark from "@/public/auth/background-pattern-dark.svg"; diff --git a/web/core/components/integration/github/import-data.tsx b/web/core/components/integration/github/import-data.tsx index 75226e788..d1343b523 100644 --- a/web/core/components/integration/github/import-data.tsx +++ b/web/core/components/integration/github/import-data.tsx @@ -7,10 +7,10 @@ import { IWorkspaceIntegration } from "@plane/types"; // hooks // components import { Button, CustomSearchSelect, ToggleSwitch } from "@plane/ui"; +import { truncateText } from "@plane/utils"; import { SelectRepository, TFormValues, TIntegrationSteps } from "@/components/integration"; // ui // helpers -import { truncateText } from "@/helpers/string.helper"; import { useProject } from "@/hooks/store"; // types diff --git a/web/core/components/integration/github/select-repository.tsx b/web/core/components/integration/github/select-repository.tsx index 122e534df..05936cfa7 100644 --- a/web/core/components/integration/github/select-repository.tsx +++ b/web/core/components/integration/github/select-repository.tsx @@ -8,7 +8,7 @@ import { IWorkspaceIntegration } from "@plane/types"; // ui import { CustomSearchSelect } from "@plane/ui"; // helpers -import { truncateText } from "@/helpers/string.helper"; +import { truncateText } from "@plane/utils"; import { ProjectService } from "@/services/project"; // types diff --git a/web/core/components/integration/github/single-user-select.tsx b/web/core/components/integration/github/single-user-select.tsx index c2ecda03e..3b24338bc 100644 --- a/web/core/components/integration/github/single-user-select.tsx +++ b/web/core/components/integration/github/single-user-select.tsx @@ -7,9 +7,9 @@ import { IGithubRepoCollaborator } from "@plane/types"; // plane ui import { Avatar, CustomSelect, CustomSearchSelect, Input } from "@plane/ui"; // constants +import { getFileURL } from "@plane/utils"; import { WORKSPACE_MEMBERS } from "@/constants/fetch-keys"; // helpers -import { getFileURL } from "@/helpers/file.helper"; // plane web services import { WorkspaceService } from "@/plane-web/services"; // types diff --git a/web/core/components/integration/jira/give-details.tsx b/web/core/components/integration/jira/give-details.tsx index f26318e3a..a9f12e709 100644 --- a/web/core/components/integration/jira/give-details.tsx +++ b/web/core/components/integration/jira/give-details.tsx @@ -10,7 +10,7 @@ import { IJiraImporterForm } from "@plane/types"; // components import { CustomSelect, Input } from "@plane/ui"; // helpers -import { checkEmailValidity } from "@/helpers/string.helper"; +import { checkEmailValidity } from "@plane/utils"; import { useCommandPalette, useEventTracker, useProject } from "@/hooks/store"; // types diff --git a/web/core/components/integration/jira/import-users.tsx b/web/core/components/integration/jira/import-users.tsx index 6bffece7f..7a35aa9d4 100644 --- a/web/core/components/integration/jira/import-users.tsx +++ b/web/core/components/integration/jira/import-users.tsx @@ -9,9 +9,9 @@ import { IJiraImporterForm } from "@plane/types"; // plane ui import { Avatar, CustomSelect, CustomSearchSelect, Input, ToggleSwitch } from "@plane/ui"; // constants +import { getFileURL } from "@plane/utils"; import { WORKSPACE_MEMBERS } from "@/constants/fetch-keys"; // helpers -import { getFileURL } from "@/helpers/file.helper"; // plane web services import { WorkspaceService } from "@/plane-web/services"; diff --git a/web/core/components/integration/single-import.tsx b/web/core/components/integration/single-import.tsx index 6be8c02c9..247fb0f8e 100644 --- a/web/core/components/integration/single-import.tsx +++ b/web/core/components/integration/single-import.tsx @@ -10,7 +10,7 @@ import { CustomMenu } from "@plane/ui"; // icons // helpers -import { renderFormattedDate } from "@/helpers/date-time.helper"; +import { renderFormattedDate } from "@plane/utils"; // types // constants diff --git a/web/core/components/issues/archived-issues-header.tsx b/web/core/components/issues/archived-issues-header.tsx index 4e2eaebd9..28bc63e08 100644 --- a/web/core/components/issues/archived-issues-header.tsx +++ b/web/core/components/issues/archived-issues-header.tsx @@ -8,10 +8,10 @@ import { useTranslation } from "@plane/i18n"; // types import type { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types"; // components +import { isIssueFilterActive } from "@plane/utils"; import { ArchiveTabsList } from "@/components/archives"; import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "@/components/issues"; // helpers -import { isIssueFilterActive } from "@/helpers/filter.helper"; // hooks import { useIssues, useLabel, useMember, useProject, useProjectState } from "@/hooks/store"; diff --git a/web/core/components/issues/attachment/attachment-detail.tsx b/web/core/components/issues/attachment/attachment-detail.tsx index 315d579f5..ecfc0f4c4 100644 --- a/web/core/components/issues/attachment/attachment-detail.tsx +++ b/web/core/components/issues/attachment/attachment-detail.tsx @@ -6,15 +6,13 @@ import Link from "next/link"; import { AlertCircle, X } from "lucide-react"; // ui import { Tooltip } from "@plane/ui"; +import { convertBytesToSize, getFileExtension, getFileName, getFileURL, renderFormattedDate, truncateText } from "@plane/utils"; // icons +// import { getFileIcon } from "@/components/icons"; // components import { IssueAttachmentDeleteModal } from "@/components/issues"; // helpers -import { convertBytesToSize, getFileExtension, getFileName } from "@/helpers/attachment.helper"; -import { renderFormattedDate } from "@/helpers/date-time.helper"; -import { getFileURL } from "@/helpers/file.helper"; -import { truncateText } from "@/helpers/string.helper"; // hooks import { useIssueDetail, useMember } from "@/hooks/store"; import { usePlatformOS } from "@/hooks/use-platform-os"; diff --git a/web/core/components/issues/attachment/attachment-list-item.tsx b/web/core/components/issues/attachment/attachment-list-item.tsx index 0458f4485..e7df230a2 100644 --- a/web/core/components/issues/attachment/attachment-list-item.tsx +++ b/web/core/components/issues/attachment/attachment-list-item.tsx @@ -8,13 +8,12 @@ import { useTranslation } from "@plane/i18n"; import { TIssueServiceType } from "@plane/types"; // ui import { CustomMenu, Tooltip } from "@plane/ui"; +import { convertBytesToSize, getFileExtension, getFileName, getFileURL, renderFormattedDate } from "@plane/utils"; // components +// import { ButtonAvatars } from "@/components/dropdowns/member/avatar"; import { getFileIcon } from "@/components/icons"; // helpers -import { convertBytesToSize, getFileExtension, getFileName } from "@/helpers/attachment.helper"; -import { renderFormattedDate } from "@/helpers/date-time.helper"; -import { getFileURL } from "@/helpers/file.helper"; // hooks import { useIssueDetail, useMember } from "@/hooks/store"; import { usePlatformOS } from "@/hooks/use-platform-os"; diff --git a/web/core/components/issues/attachment/attachment-list-upload-item.tsx b/web/core/components/issues/attachment/attachment-list-upload-item.tsx index 8ea9d3470..61924a25f 100644 --- a/web/core/components/issues/attachment/attachment-list-upload-item.tsx +++ b/web/core/components/issues/attachment/attachment-list-upload-item.tsx @@ -4,9 +4,9 @@ import { observer } from "mobx-react"; // ui import { CircularProgressIndicator, Tooltip } from "@plane/ui"; // components +import { getFileExtension } from "@plane/utils"; import { getFileIcon } from "@/components/icons"; // helpers -import { getFileExtension } from "@/helpers/attachment.helper"; // hooks import { usePlatformOS } from "@/hooks/use-platform-os"; // types diff --git a/web/core/components/issues/attachment/attachment-upload-details.tsx b/web/core/components/issues/attachment/attachment-upload-details.tsx index 6d4eca96e..1bc36318b 100644 --- a/web/core/components/issues/attachment/attachment-upload-details.tsx +++ b/web/core/components/issues/attachment/attachment-upload-details.tsx @@ -1,13 +1,12 @@ "use client"; import { observer } from "mobx-react"; -// ui import { CircularProgressIndicator, Tooltip } from "@plane/ui"; +import { getFileExtension, truncateText } from "@plane/utils"; +// ui // icons import { getFileIcon } from "@/components/icons"; // helpers -import { getFileExtension } from "@/helpers/attachment.helper"; -import { truncateText } from "@/helpers/string.helper"; // hooks import { usePlatformOS } from "@/hooks/use-platform-os"; // types diff --git a/web/core/components/issues/attachment/delete-attachment-modal.tsx b/web/core/components/issues/attachment/delete-attachment-modal.tsx index 2b2833fd3..046b90a5d 100644 --- a/web/core/components/issues/attachment/delete-attachment-modal.tsx +++ b/web/core/components/issues/attachment/delete-attachment-modal.tsx @@ -9,7 +9,7 @@ import { TIssueServiceType } from "@plane/types"; // ui import { AlertModalCore } from "@plane/ui"; // helper -import { getFileName } from "@/helpers/attachment.helper"; +import { getFileName } from "@plane/utils"; // hooks import { useIssueDetail } from "@/hooks/store"; // types diff --git a/web/core/components/issues/bulk-operations/upgrade-banner.tsx b/web/core/components/issues/bulk-operations/upgrade-banner.tsx index fef0cb617..b7ecc9cf9 100644 --- a/web/core/components/issues/bulk-operations/upgrade-banner.tsx +++ b/web/core/components/issues/bulk-operations/upgrade-banner.tsx @@ -2,7 +2,7 @@ import { MARKETING_PLANE_ONE_PAGE_LINK } from "@plane/constants"; import { getButtonStyling } from "@plane/ui"; -import { cn } from "@/helpers/common.helper"; +import { cn } from "@plane/utils"; type Props = { className?: string; diff --git a/web/core/components/issues/create-issue-toast-action-items.tsx b/web/core/components/issues/create-issue-toast-action-items.tsx index f324710f2..f0a19249e 100644 --- a/web/core/components/issues/create-issue-toast-action-items.tsx +++ b/web/core/components/issues/create-issue-toast-action-items.tsx @@ -1,10 +1,9 @@ "use client"; import React, { FC, useState } from "react"; import { observer } from "mobx-react"; +import { copyUrlToClipboard, generateWorkItemLink } from "@plane/utils"; // plane imports -import { copyUrlToClipboard } from "@plane/utils"; // helpers -import { generateWorkItemLink } from "@/helpers/issue.helper"; // hooks import { useIssueDetail, useProject } from "@/hooks/store"; diff --git a/web/core/components/issues/description-input.tsx b/web/core/components/issues/description-input.tsx index fd8ae823e..b12a45c50 100644 --- a/web/core/components/issues/description-input.tsx +++ b/web/core/components/issues/description-input.tsx @@ -11,10 +11,10 @@ import { TIssue, TNameDescriptionLoader } from "@plane/types"; import { EFileAssetType } from "@plane/types/src/enums"; import { Loader } from "@plane/ui"; // components +import { getDescriptionPlaceholderI18n } from "@plane/utils"; import { RichTextEditor, RichTextReadOnlyEditor } from "@/components/editor"; import { TIssueOperations } from "@/components/issues/issue-detail"; // helpers -import { getDescriptionPlaceholderI18n } from "@/helpers/issue.helper"; // hooks import { useEditorAsset, useWorkspace } from "@/hooks/store"; // plane web services diff --git a/web/core/components/issues/filters.tsx b/web/core/components/issues/filters.tsx index 9ef8706ea..8be85a07d 100644 --- a/web/core/components/issues/filters.tsx +++ b/web/core/components/issues/filters.tsx @@ -10,9 +10,9 @@ import { useTranslation } from "@plane/i18n"; import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types"; import { Button } from "@plane/ui"; // components +import { isIssueFilterActive } from "@plane/utils"; import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues"; // helpers -import { isIssueFilterActive } from "@/helpers/filter.helper"; // hooks import { useLabel, useProjectState, useMember, useIssues } from "@/hooks/store"; // plane web types diff --git a/web/core/components/issues/issue-detail-widgets/sub-issues/issues-list/list-item.tsx b/web/core/components/issues/issue-detail-widgets/sub-issues/issues-list/list-item.tsx index 1d581929c..73e5b29f0 100644 --- a/web/core/components/issues/issue-detail-widgets/sub-issues/issues-list/list-item.tsx +++ b/web/core/components/issues/issue-detail-widgets/sub-issues/issues-list/list-item.tsx @@ -7,11 +7,10 @@ import { EIssueServiceType, EIssuesStoreType } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { TIssue, TIssueServiceType, TSubIssueOperations } from "@plane/types"; import { ControlLink, CustomMenu, Tooltip } from "@plane/ui"; +import { cn, generateWorkItemLink } from "@plane/utils"; // helpers import { useSubIssueOperations } from "@/components/issues/issue-detail-widgets/sub-issues/helper"; import { WithDisplayPropertiesHOC } from "@/components/issues/issue-layouts/properties/with-display-properties-HOC"; -import { cn } from "@/helpers/common.helper"; -import { generateWorkItemLink } from "@/helpers/issue.helper"; // hooks import { useIssueDetail, useProject, useProjectState } from "@/hooks/store"; import useIssuePeekOverviewRedirection from "@/hooks/use-issue-peek-overview-redirection"; diff --git a/web/core/components/issues/issue-detail-widgets/sub-issues/issues-list/properties.tsx b/web/core/components/issues/issue-detail-widgets/sub-issues/issues-list/properties.tsx index 29b6e2770..96ed4a2a9 100644 --- a/web/core/components/issues/issue-detail-widgets/sub-issues/issues-list/properties.tsx +++ b/web/core/components/issues/issue-detail-widgets/sub-issues/issues-list/properties.tsx @@ -1,13 +1,12 @@ // plane imports import { SyntheticEvent } from "react"; import { observer } from "mobx-react"; -import { useTranslation } from "@plane/i18n"; import { IIssueDisplayProperties, TIssue } from "@plane/types"; +import { getDate, renderFormattedPayloadDate } from "@plane/utils"; // components import { PriorityDropdown, MemberDropdown, StateDropdown, DateRangeDropdown } from "@/components/dropdowns"; // hooks import { WithDisplayPropertiesHOC } from "@/components/issues/issue-layouts/properties/with-display-properties-HOC"; -import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper"; type Props = { workspaceSlug: string; @@ -28,8 +27,6 @@ type Props = { export const SubIssuesListItemProperties: React.FC = observer((props) => { const { workspaceSlug, parentIssueId, issueId, disabled, updateSubIssue, displayProperties, issue } = props; - // hooks - const { t } = useTranslation(); const handleEventPropagation = (e: SyntheticEvent) => { e.stopPropagation(); diff --git a/web/core/components/issues/issue-detail-widgets/widget-button.tsx b/web/core/components/issues/issue-detail-widgets/widget-button.tsx index a683920e9..b34846b87 100644 --- a/web/core/components/issues/issue-detail-widgets/widget-button.tsx +++ b/web/core/components/issues/issue-detail-widgets/widget-button.tsx @@ -1,7 +1,7 @@ "use client"; import React, { FC } from "react"; // helpers -import { cn } from "@/helpers/common.helper"; +import { cn } from "@plane/utils"; type Props = { icon: JSX.Element; diff --git a/web/core/components/issues/issue-detail/cycle-select.tsx b/web/core/components/issues/issue-detail/cycle-select.tsx index c82f67102..cbf7493a8 100644 --- a/web/core/components/issues/issue-detail/cycle-select.tsx +++ b/web/core/components/issues/issue-detail/cycle-select.tsx @@ -3,10 +3,10 @@ import { observer } from "mobx-react"; import { useTranslation } from "@plane/i18n"; // hooks // components +import { cn } from "@plane/utils"; import { CycleDropdown } from "@/components/dropdowns"; // ui // helpers -import { cn } from "@/helpers/common.helper"; import { useIssueDetail } from "@/hooks/store"; // types import type { TIssueOperations } from "./root"; diff --git a/web/core/components/issues/issue-detail/issue-activity/activity-filter.tsx b/web/core/components/issues/issue-detail/issue-activity/activity-filter.tsx index 40162dc15..88bd2ac86 100644 --- a/web/core/components/issues/issue-detail/issue-activity/activity-filter.tsx +++ b/web/core/components/issues/issue-detail/issue-activity/activity-filter.tsx @@ -5,7 +5,7 @@ import { TActivityFilters, TActivityFilterOption } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { Button, PopoverMenu } from "@plane/ui"; // helper -import { cn } from "@/helpers/common.helper"; +import { cn } from "@plane/utils"; // constants type TActivityFilter = { diff --git a/web/core/components/issues/issue-detail/issue-activity/activity/actions/helpers/activity-block.tsx b/web/core/components/issues/issue-detail/issue-activity/activity/actions/helpers/activity-block.tsx index 6e3ff562c..663d2aa2c 100644 --- a/web/core/components/issues/issue-detail/issue-activity/activity/actions/helpers/activity-block.tsx +++ b/web/core/components/issues/issue-detail/issue-activity/activity/actions/helpers/activity-block.tsx @@ -4,7 +4,7 @@ import { FC, ReactNode } from "react"; import { Network } from "lucide-react"; // hooks import { Tooltip } from "@plane/ui"; -import { renderFormattedTime, renderFormattedDate, calculateTimeAgo } from "@/helpers/date-time.helper"; +import { renderFormattedTime, renderFormattedDate, calculateTimeAgo } from "@plane/utils"; import { useIssueDetail } from "@/hooks/store"; import { usePlatformOS } from "@/hooks/use-platform-os"; // ui diff --git a/web/core/components/issues/issue-detail/issue-activity/activity/actions/helpers/issue-link.tsx b/web/core/components/issues/issue-detail/issue-activity/activity/actions/helpers/issue-link.tsx index 661b513ec..80451cb02 100644 --- a/web/core/components/issues/issue-detail/issue-activity/activity/actions/helpers/issue-link.tsx +++ b/web/core/components/issues/issue-detail/issue-activity/activity/actions/helpers/issue-link.tsx @@ -3,7 +3,7 @@ import { FC } from "react"; // hooks import { Tooltip } from "@plane/ui"; -import { generateWorkItemLink } from "@/helpers/issue.helper"; +import { generateWorkItemLink } from "@plane/utils"; import { useIssueDetail } from "@/hooks/store"; import { usePlatformOS } from "@/hooks/use-platform-os"; // ui diff --git a/web/core/components/issues/issue-detail/issue-activity/activity/actions/start_date.tsx b/web/core/components/issues/issue-detail/issue-activity/activity/actions/start_date.tsx index b1e0ce03c..a55071bf9 100644 --- a/web/core/components/issues/issue-detail/issue-activity/activity/actions/start_date.tsx +++ b/web/core/components/issues/issue-detail/issue-activity/activity/actions/start_date.tsx @@ -2,7 +2,7 @@ import { FC } from "react"; import { observer } from "mobx-react"; import { CalendarDays } from "lucide-react"; // hooks -import { renderFormattedDate } from "@/helpers/date-time.helper"; +import { renderFormattedDate } from "@plane/utils"; import { useIssueDetail } from "@/hooks/store"; // components import { IssueActivityBlockComponent, IssueLink } from "./"; diff --git a/web/core/components/issues/issue-detail/issue-activity/activity/actions/target_date.tsx b/web/core/components/issues/issue-detail/issue-activity/activity/actions/target_date.tsx index 7d0367bd8..12457d59d 100644 --- a/web/core/components/issues/issue-detail/issue-activity/activity/actions/target_date.tsx +++ b/web/core/components/issues/issue-detail/issue-activity/activity/actions/target_date.tsx @@ -2,7 +2,7 @@ import { FC } from "react"; import { observer } from "mobx-react"; import { CalendarDays } from "lucide-react"; // hooks -import { renderFormattedDate } from "@/helpers/date-time.helper"; +import { renderFormattedDate } from "@plane/utils"; import { useIssueDetail } from "@/hooks/store"; // components import { IssueActivityBlockComponent, IssueLink } from "./"; diff --git a/web/core/components/issues/issue-detail/issue-activity/activity/activity-list.tsx b/web/core/components/issues/issue-detail/issue-activity/activity/activity-list.tsx index b0f8b3d4f..32c49915c 100644 --- a/web/core/components/issues/issue-detail/issue-activity/activity/activity-list.tsx +++ b/web/core/components/issues/issue-detail/issue-activity/activity/activity-list.tsx @@ -1,7 +1,7 @@ import { FC } from "react"; import { observer } from "mobx-react"; // helpers -import { getValidKeysFromObject } from "@/helpers/array.helper"; +import { getValidKeysFromObject } from "@plane/utils"; // hooks import { useIssueDetail } from "@/hooks/store"; // plane web components diff --git a/web/core/components/issues/issue-detail/issue-activity/helper.tsx b/web/core/components/issues/issue-detail/issue-activity/helper.tsx index 5bd50d064..af30a389f 100644 --- a/web/core/components/issues/issue-detail/issue-activity/helper.tsx +++ b/web/core/components/issues/issue-detail/issue-activity/helper.tsx @@ -3,7 +3,7 @@ import { useTranslation } from "@plane/i18n"; import { TCommentsOperations, TIssueActivity, TIssueComment } from "@plane/types"; import { EFileAssetType } from "@plane/types/src/enums"; import { setToast, TOAST_TYPE } from "@plane/ui"; -import { formatTextList } from "@/helpers/issue.helper"; +import { formatTextList } from "@plane/utils"; import { useEditorAsset, useIssueDetail, useMember, useUser } from "@/hooks/store"; export const useCommentOperations = ( diff --git a/web/core/components/issues/issue-detail/issue-detail-quick-actions.tsx b/web/core/components/issues/issue-detail/issue-detail-quick-actions.tsx index 6bf4bf757..df536d900 100644 --- a/web/core/components/issues/issue-detail/issue-detail-quick-actions.tsx +++ b/web/core/components/issues/issue-detail/issue-detail-quick-actions.tsx @@ -14,12 +14,10 @@ import { } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { TOAST_TYPE, Tooltip, setToast } from "@plane/ui"; +import { cn, generateWorkItemLink, copyTextToClipboard } from "@plane/utils"; // components import { ArchiveIssueModal, DeleteIssueModal, IssueSubscription } from "@/components/issues"; // helpers -import { cn } from "@/helpers/common.helper"; -import { generateWorkItemLink } from "@/helpers/issue.helper"; -import { copyTextToClipboard } from "@/helpers/string.helper"; // hooks import { useEventTracker, diff --git a/web/core/components/issues/issue-detail/label/select/label-select.tsx b/web/core/components/issues/issue-detail/label/select/label-select.tsx index 8fc4829bc..509b0a5ca 100644 --- a/web/core/components/issues/issue-detail/label/select/label-select.tsx +++ b/web/core/components/issues/issue-detail/label/select/label-select.tsx @@ -8,7 +8,7 @@ import { EUserPermissionsLevel, EUserProjectRoles, getRandomLabelColor } from "@ import { useTranslation } from "@plane/i18n"; import { IIssueLabel } from "@plane/types"; // helpers -import { getTabIndex } from "@/helpers/tab-indices.helper"; +import { getTabIndex } from "@plane/utils"; // hooks import { useLabel, useUserPermissions } from "@/hooks/store"; import { usePlatformOS } from "@/hooks/use-platform-os"; diff --git a/web/core/components/issues/issue-detail/links/link-detail.tsx b/web/core/components/issues/issue-detail/links/link-detail.tsx index 673874bb1..9a24ce5b1 100644 --- a/web/core/components/issues/issue-detail/links/link-detail.tsx +++ b/web/core/components/issues/issue-detail/links/link-detail.tsx @@ -5,12 +5,11 @@ import { FC } from "react"; // ui import { Pencil, Trash2, ExternalLink } from "lucide-react"; import { Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; -import { getIconForLink } from "@plane/utils"; +import { getIconForLink, copyTextToClipboard, calculateTimeAgo } from "@plane/utils"; // icons // types // helpers -import { calculateTimeAgo } from "@/helpers/date-time.helper"; -import { copyTextToClipboard } from "@/helpers/string.helper"; +// import { useIssueDetail, useMember } from "@/hooks/store"; import { usePlatformOS } from "@/hooks/use-platform-os"; import { TLinkOperationsModal } from "./create-update-link-modal"; diff --git a/web/core/components/issues/issue-detail/links/link-item.tsx b/web/core/components/issues/issue-detail/links/link-item.tsx index 83ddc4a7d..526336c73 100644 --- a/web/core/components/issues/issue-detail/links/link-item.tsx +++ b/web/core/components/issues/issue-detail/links/link-item.tsx @@ -8,9 +8,8 @@ import { useTranslation } from "@plane/i18n"; import { TIssueServiceType } from "@plane/types"; // ui import { Tooltip, TOAST_TYPE, setToast, CustomMenu } from "@plane/ui"; -import { calculateTimeAgo, getIconForLink } from "@plane/utils"; +import { calculateTimeAgo, getIconForLink, copyTextToClipboard } from "@plane/utils"; // helpers -import { copyTextToClipboard } from "@/helpers/string.helper"; // hooks import { useIssueDetail } from "@/hooks/store"; import { usePlatformOS } from "@/hooks/use-platform-os"; diff --git a/web/core/components/issues/issue-detail/main-content.tsx b/web/core/components/issues/issue-detail/main-content.tsx index 8087bf351..175f03f1e 100644 --- a/web/core/components/issues/issue-detail/main-content.tsx +++ b/web/core/components/issues/issue-detail/main-content.tsx @@ -7,6 +7,7 @@ import { EIssueServiceType } from "@plane/constants"; import { EditorRefApi } from "@plane/editor"; import { TNameDescriptionLoader } from "@plane/types"; // components +import { getTextContent } from "@plane/utils"; import { DescriptionVersionsRoot } from "@/components/core/description-versions"; import { IssueActivity, @@ -19,7 +20,6 @@ import { PeekOverviewProperties, } from "@/components/issues"; // helpers -import { getTextContent } from "@/helpers/editor.helper"; // hooks import { useIssueDetail, useMember, useProject, useUser } from "@/hooks/store"; import useReloadConfirmations from "@/hooks/use-reload-confirmation"; diff --git a/web/core/components/issues/issue-detail/module-select.tsx b/web/core/components/issues/issue-detail/module-select.tsx index d021958e8..41b802795 100644 --- a/web/core/components/issues/issue-detail/module-select.tsx +++ b/web/core/components/issues/issue-detail/module-select.tsx @@ -4,10 +4,10 @@ import { observer } from "mobx-react"; import { useTranslation } from "@plane/i18n"; // hooks // components +import { cn } from "@plane/utils"; import { ModuleDropdown } from "@/components/dropdowns"; // ui // helpers -import { cn } from "@/helpers/common.helper"; import { useIssueDetail } from "@/hooks/store"; // types import type { TIssueOperations } from "./root"; diff --git a/web/core/components/issues/issue-detail/parent-select.tsx b/web/core/components/issues/issue-detail/parent-select.tsx index dc50864a9..9b2d544a9 100644 --- a/web/core/components/issues/issue-detail/parent-select.tsx +++ b/web/core/components/issues/issue-detail/parent-select.tsx @@ -8,9 +8,9 @@ import { useTranslation } from "@plane/i18n"; // ui import { Tooltip } from "@plane/ui"; // components +import { cn } from "@plane/utils"; import { ParentIssuesListModal } from "@/components/issues"; // helpers -import { cn } from "@/helpers/common.helper"; // hooks import { useIssueDetail, useProject } from "@/hooks/store"; import { usePlatformOS } from "@/hooks/use-platform-os"; diff --git a/web/core/components/issues/issue-detail/parent/root.tsx b/web/core/components/issues/issue-detail/parent/root.tsx index 66345b4ae..0682d4b54 100644 --- a/web/core/components/issues/issue-detail/parent/root.tsx +++ b/web/core/components/issues/issue-detail/parent/root.tsx @@ -10,7 +10,7 @@ import { TIssue } from "@plane/types"; // ui import { ControlLink, CustomMenu } from "@plane/ui"; // helpers -import { generateWorkItemLink } from "@/helpers/issue.helper"; +import { generateWorkItemLink } from "@plane/utils"; // hooks import { useIssues, useProject, useProjectState } from "@/hooks/store"; import useIssuePeekOverviewRedirection from "@/hooks/use-issue-peek-overview-redirection"; diff --git a/web/core/components/issues/issue-detail/parent/sibling-item.tsx b/web/core/components/issues/issue-detail/parent/sibling-item.tsx index 2947e49df..04cb3745d 100644 --- a/web/core/components/issues/issue-detail/parent/sibling-item.tsx +++ b/web/core/components/issues/issue-detail/parent/sibling-item.tsx @@ -6,7 +6,7 @@ import Link from "next/link"; // ui import { CustomMenu } from "@plane/ui"; // helpers -import { generateWorkItemLink } from "@/helpers/issue.helper"; +import { generateWorkItemLink } from "@plane/utils"; // hooks import { useIssueDetail, useProject } from "@/hooks/store"; // plane web components diff --git a/web/core/components/issues/issue-detail/reactions/issue-comment.tsx b/web/core/components/issues/issue-detail/reactions/issue-comment.tsx index 26f50e057..caaeb65ff 100644 --- a/web/core/components/issues/issue-detail/reactions/issue-comment.tsx +++ b/web/core/components/issues/issue-detail/reactions/issue-comment.tsx @@ -5,10 +5,9 @@ import { observer } from "mobx-react"; import { IUser } from "@plane/types"; // components import { TOAST_TYPE, Tooltip, setToast } from "@plane/ui"; +import { cn, formatTextList } from "@plane/utils"; // helper -import { cn } from "@/helpers/common.helper"; import { renderEmoji } from "@/helpers/emoji.helper"; -import { formatTextList } from "@/helpers/issue.helper"; // hooks import { useIssueDetail, useMember } from "@/hooks/store"; // types diff --git a/web/core/components/issues/issue-detail/reactions/issue.tsx b/web/core/components/issues/issue-detail/reactions/issue.tsx index 9b723e785..5e5994ff3 100644 --- a/web/core/components/issues/issue-detail/reactions/issue.tsx +++ b/web/core/components/issues/issue-detail/reactions/issue.tsx @@ -6,10 +6,9 @@ import { IUser } from "@plane/types"; // hooks // ui import { TOAST_TYPE, Tooltip, setToast } from "@plane/ui"; +import { cn, formatTextList } from "@plane/utils"; // helpers -import { cn } from "@/helpers/common.helper"; import { renderEmoji } from "@/helpers/emoji.helper"; -import { formatTextList } from "@/helpers/issue.helper"; import { useIssueDetail, useMember } from "@/hooks/store"; // types import { ReactionSelector } from "./reaction-selector"; diff --git a/web/core/components/issues/issue-detail/relation-select.tsx b/web/core/components/issues/issue-detail/relation-select.tsx index 86fde40eb..d7d0a6947 100644 --- a/web/core/components/issues/issue-detail/relation-select.tsx +++ b/web/core/components/issues/issue-detail/relation-select.tsx @@ -7,11 +7,10 @@ import { CircleDot, CopyPlus, Pencil, X, XCircle } from "lucide-react"; // Plane import { ISearchIssueResponse } from "@plane/types"; import { RelatedIcon, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; +import { cn, generateWorkItemLink } from "@plane/utils"; // components import { ExistingIssuesListModal } from "@/components/core"; // helpers -import { cn } from "@/helpers/common.helper"; -import { generateWorkItemLink } from "@/helpers/issue.helper"; // hooks import { useIssueDetail, useIssues, useProject } from "@/hooks/store"; import { usePlatformOS } from "@/hooks/use-platform-os"; diff --git a/web/core/components/issues/issue-detail/sidebar.tsx b/web/core/components/issues/issue-detail/sidebar.tsx index 8e7bf151b..c93baf884 100644 --- a/web/core/components/issues/issue-detail/sidebar.tsx +++ b/web/core/components/issues/issue-detail/sidebar.tsx @@ -7,6 +7,7 @@ import { CalendarCheck2, CalendarClock, LayoutPanelTop, Signal, Tag, Triangle, U import { useTranslation } from "@plane/i18n"; // ui import { ContrastIcon, DiceIcon, DoubleCircleIcon } from "@plane/ui"; +import { cn, getDate, renderFormattedPayloadDate, shouldHighlightIssueDueDate } from "@plane/utils"; // components import { DateDropdown, @@ -18,9 +19,6 @@ import { import { ButtonAvatars } from "@/components/dropdowns/member/avatar"; import { IssueCycleSelect, IssueLabel, IssueModuleSelect } from "@/components/issues"; // helpers -import { cn } from "@/helpers/common.helper"; -import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper"; -import { shouldHighlightIssueDueDate } from "@/helpers/issue.helper"; // hooks import { useProjectEstimates, useIssueDetail, useProject, useProjectState, useMember } from "@/hooks/store"; // plane web components diff --git a/web/core/components/issues/issue-layouts/calendar/calendar.tsx b/web/core/components/issues/issue-layouts/calendar/calendar.tsx index 9a0284f47..428beafc0 100644 --- a/web/core/components/issues/issue-layouts/calendar/calendar.tsx +++ b/web/core/components/issues/issue-layouts/calendar/calendar.tsx @@ -16,16 +16,16 @@ import type { TIssueKanbanFilters, TIssueMap, TPaginationData, + ICalendarWeek, } from "@plane/types"; // ui import { Spinner } from "@plane/ui"; +import { renderFormattedPayloadDate, cn } from "@plane/utils"; // components import { CalendarHeader, CalendarIssueBlocks, CalendarWeekDays, CalendarWeekHeader } from "@/components/issues"; // constants import { MONTHS_LIST } from "@/constants/calendar"; // helpers -import { cn } from "@/helpers/common.helper"; -import { renderFormattedPayloadDate } from "@/helpers/date-time.helper"; // hooks import { useIssues } from "@/hooks/store"; import useSize from "@/hooks/use-window-size"; @@ -38,7 +38,6 @@ import { IProjectIssuesFilter } from "@/store/issue/project"; import { IProjectViewIssuesFilter } from "@/store/issue/project-views"; import { IssueLayoutHOC } from "../issue-layout-HOC"; import { TRenderQuickActions } from "../list/list-view-types"; -import type { ICalendarWeek } from "./types"; type Props = { issuesFilterStore: diff --git a/web/core/components/issues/issue-layouts/calendar/day-tile.tsx b/web/core/components/issues/issue-layouts/calendar/day-tile.tsx index e80e629e6..b484a5c49 100644 --- a/web/core/components/issues/issue-layouts/calendar/day-tile.tsx +++ b/web/core/components/issues/issue-layouts/calendar/day-tile.tsx @@ -5,18 +5,17 @@ import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine"; import { dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; import { differenceInCalendarDays } from "date-fns/differenceInCalendarDays"; import { observer } from "mobx-react"; +import { TGroupedIssues, TIssue, TIssueMap, TPaginationData, ICalendarDate } from "@plane/types"; // types -import { TGroupedIssues, TIssue, TIssueMap, TPaginationData } from "@plane/types"; // ui import { TOAST_TYPE, setToast } from "@plane/ui"; // components -import { CalendarIssueBlocks, ICalendarDate } from "@/components/issues"; +import { cn, renderFormattedPayloadDate } from "@plane/utils"; +import { CalendarIssueBlocks } from "@/components/issues/issue-layouts/calendar"; import { highlightIssueOnDrop } from "@/components/issues/issue-layouts/utils"; // helpers import { MONTHS_LIST } from "@/constants/calendar"; // helpers -import { cn } from "@/helpers/common.helper"; -import { renderFormattedPayloadDate } from "@/helpers/date-time.helper"; // types import { IProjectEpicsFilter } from "@/plane-web/store/issue/epic"; import { ICycleIssuesFilter } from "@/store/issue/cycle"; diff --git a/web/core/components/issues/issue-layouts/calendar/dropdowns/months-dropdown.tsx b/web/core/components/issues/issue-layouts/calendar/dropdowns/months-dropdown.tsx index 05e5a1631..f0563f5ba 100644 --- a/web/core/components/issues/issue-layouts/calendar/dropdowns/months-dropdown.tsx +++ b/web/core/components/issues/issue-layouts/calendar/dropdowns/months-dropdown.tsx @@ -6,8 +6,8 @@ import { Popover, Transition } from "@headlessui/react"; //hooks // icons // constants +import { getDate } from "@plane/utils"; import { MONTHS_LIST } from "@/constants/calendar"; -import { getDate } from "@/helpers/date-time.helper"; import { useCalendarView } from "@/hooks/store"; import { IProjectEpicsFilter } from "@/plane-web/store/issue/epic"; import { ICycleIssuesFilter } from "@/store/issue/cycle"; diff --git a/web/core/components/issues/issue-layouts/calendar/index.ts b/web/core/components/issues/issue-layouts/calendar/index.ts index d6028f4f5..e9a722af0 100644 --- a/web/core/components/issues/issue-layouts/calendar/index.ts +++ b/web/core/components/issues/issue-layouts/calendar/index.ts @@ -1,7 +1,6 @@ export * from "./dropdowns"; export * from "./roots"; export * from "./calendar"; -export * from "./types.d"; export * from "./day-tile"; export * from "./header"; export * from "./issue-blocks"; diff --git a/web/core/components/issues/issue-layouts/calendar/issue-block.tsx b/web/core/components/issues/issue-layouts/calendar/issue-block.tsx index 127ebfb47..ce1181b73 100644 --- a/web/core/components/issues/issue-layouts/calendar/issue-block.tsx +++ b/web/core/components/issues/issue-layouts/calendar/issue-block.tsx @@ -11,9 +11,8 @@ import { useOutsideClickDetector } from "@plane/hooks"; import { TIssue } from "@plane/types"; // ui import { Tooltip, ControlLink } from "@plane/ui"; +import { cn, generateWorkItemLink } from "@plane/utils"; // helpers -import { cn } from "@/helpers/common.helper"; -import { generateWorkItemLink } from "@/helpers/issue.helper"; // hooks import { useIssueDetail, useIssues, useProject, useProjectState } from "@/hooks/store"; import { useIssueStoreType } from "@/hooks/use-issue-layout-store"; diff --git a/web/core/components/issues/issue-layouts/calendar/issue-blocks.tsx b/web/core/components/issues/issue-layouts/calendar/issue-blocks.tsx index 9e7109fa3..ff2bbe76b 100644 --- a/web/core/components/issues/issue-layouts/calendar/issue-blocks.tsx +++ b/web/core/components/issues/issue-layouts/calendar/issue-blocks.tsx @@ -2,9 +2,9 @@ import { observer } from "mobx-react"; import { useTranslation } from "@plane/i18n"; import { TIssue, TPaginationData } from "@plane/types"; // components +import { renderFormattedPayloadDate } from "@plane/utils"; import { CalendarQuickAddIssueActions, CalendarIssueBlockRoot } from "@/components/issues"; // helpers -import { renderFormattedPayloadDate } from "@/helpers/date-time.helper"; import { useIssuesStore } from "@/hooks/use-issue-layout-store"; import { TRenderQuickActions } from "../list/list-view-types"; // types diff --git a/web/core/components/issues/issue-layouts/calendar/quick-add-issue-actions.tsx b/web/core/components/issues/issue-layouts/calendar/quick-add-issue-actions.tsx index a293d0141..a668e6a11 100644 --- a/web/core/components/issues/issue-layouts/calendar/quick-add-issue-actions.tsx +++ b/web/core/components/issues/issue-layouts/calendar/quick-add-issue-actions.tsx @@ -14,10 +14,10 @@ import { ISearchIssueResponse, TIssue } from "@plane/types"; // ui import { CustomMenu, setPromiseToast } from "@plane/ui"; // components +import { cn } from "@plane/utils"; import { ExistingIssuesListModal } from "@/components/core"; import { QuickAddIssueRoot } from "@/components/issues"; // helpers -import { cn } from "@/helpers/common.helper"; // hooks import { useIssueDetail } from "@/hooks/store"; diff --git a/web/core/components/issues/issue-layouts/calendar/types.d.ts b/web/core/components/issues/issue-layouts/calendar/types.d.ts deleted file mode 100644 index 3855aeb86..000000000 --- a/web/core/components/issues/issue-layouts/calendar/types.d.ts +++ /dev/null @@ -1,24 +0,0 @@ -export interface ICalendarDate { - date: Date; - year: number; - month: number; - day: number; - week: number; // week number wrt year, eg- 51, 52 - is_current_month: boolean; - is_current_week: boolean; - is_today: boolean; -} - -export interface ICalendarWeek { - [date: string]: ICalendarDate; -} - -export interface ICalendarMonth { - [monthIndex: string]: { - [weekNumber: string]: ICalendarWeek; - }; -} - -export interface ICalendarPayload { - [year: string]: ICalendarMonth; -} diff --git a/web/core/components/issues/issue-layouts/calendar/week-days.tsx b/web/core/components/issues/issue-layouts/calendar/week-days.tsx index c5ba104ee..2f843d4d6 100644 --- a/web/core/components/issues/issue-layouts/calendar/week-days.tsx +++ b/web/core/components/issues/issue-layouts/calendar/week-days.tsx @@ -1,12 +1,10 @@ import { observer } from "mobx-react"; -import { EStartOfTheWeek } from "@plane/constants"; -import { TGroupedIssues, TIssue, TIssueMap, TPaginationData } from "@plane/types"; -import { cn } from "@plane/utils"; +import { TGroupedIssues, TIssue, TIssueMap, TPaginationData, ICalendarDate, ICalendarWeek } from "@plane/types"; +import { cn, getOrderedDays, renderFormattedPayloadDate } from "@plane/utils"; // components import { CalendarDayTile } from "@/components/issues"; // helpers -import { getOrderedDays } from "@/helpers/calendar.helper"; -import { renderFormattedPayloadDate } from "@/helpers/date-time.helper"; +// // hooks import { useUserProfile } from "@/hooks/store"; // types @@ -16,7 +14,6 @@ import { IModuleIssuesFilter } from "@/store/issue/module"; import { IProjectIssuesFilter } from "@/store/issue/project"; import { IProjectViewIssuesFilter } from "@/store/issue/project-views"; import { TRenderQuickActions } from "../list/list-view-types"; -import { ICalendarDate, ICalendarWeek } from "./types"; type Props = { issuesFilterStore: diff --git a/web/core/components/issues/issue-layouts/calendar/week-header.tsx b/web/core/components/issues/issue-layouts/calendar/week-header.tsx index d97a9340e..dc2c9fbcf 100644 --- a/web/core/components/issues/issue-layouts/calendar/week-header.tsx +++ b/web/core/components/issues/issue-layouts/calendar/week-header.tsx @@ -1,8 +1,8 @@ import { observer } from "mobx-react"; import { EStartOfTheWeek } from "@plane/constants"; +import { getOrderedDays } from "@plane/utils"; import { DAYS_LIST } from "@/constants/calendar"; // helpers -import { getOrderedDays } from "@/helpers/calendar.helper"; // hooks import { useUserProfile } from "@/hooks/store"; diff --git a/web/core/components/issues/issue-layouts/filters/applied-filters/date.tsx b/web/core/components/issues/issue-layouts/filters/applied-filters/date.tsx index a64626047..e17db764b 100644 --- a/web/core/components/issues/issue-layouts/filters/applied-filters/date.tsx +++ b/web/core/components/issues/issue-layouts/filters/applied-filters/date.tsx @@ -3,8 +3,7 @@ import { observer } from "mobx-react"; import { X } from "lucide-react"; // helpers import { DATE_AFTER_FILTER_OPTIONS } from "@plane/constants"; -import { renderFormattedDate } from "@/helpers/date-time.helper"; -import { capitalizeFirstLetter } from "@/helpers/string.helper"; +import { renderFormattedDate, capitalizeFirstLetter } from "@plane/utils"; // constants type Props = { diff --git a/web/core/components/issues/issue-layouts/filters/applied-filters/filters-list.tsx b/web/core/components/issues/issue-layouts/filters/applied-filters/filters-list.tsx index 03b659d93..17fcbbfec 100644 --- a/web/core/components/issues/issue-layouts/filters/applied-filters/filters-list.tsx +++ b/web/core/components/issues/issue-layouts/filters/applied-filters/filters-list.tsx @@ -6,6 +6,7 @@ import { useTranslation } from "@plane/i18n"; import { IIssueFilterOptions, IIssueLabel, IState } from "@plane/types"; // components import { Tag } from "@plane/ui"; +import { replaceUnderscoreIfSnakeCase } from "@plane/utils"; import { AppliedCycleFilters, AppliedDateFilters, @@ -19,7 +20,6 @@ import { } from "@/components/issues"; // constants // helpers -import { replaceUnderscoreIfSnakeCase } from "@/helpers/string.helper"; // hooks import { useUserPermissions } from "@/hooks/store"; // plane web components diff --git a/web/core/components/issues/issue-layouts/filters/applied-filters/members.tsx b/web/core/components/issues/issue-layouts/filters/applied-filters/members.tsx index ed0b6a154..cf9dc582a 100644 --- a/web/core/components/issues/issue-layouts/filters/applied-filters/members.tsx +++ b/web/core/components/issues/issue-layouts/filters/applied-filters/members.tsx @@ -5,7 +5,7 @@ import { X } from "lucide-react"; // plane ui import { Avatar } from "@plane/ui"; // helpers -import { getFileURL } from "@/helpers/file.helper"; +import { getFileURL } from "@plane/utils"; // hooks import { useMember } from "@/hooks/store"; diff --git a/web/core/components/issues/issue-layouts/filters/applied-filters/roots/global-view-root.tsx b/web/core/components/issues/issue-layouts/filters/applied-filters/roots/global-view-root.tsx index 4aee53f32..4086a5c06 100644 --- a/web/core/components/issues/issue-layouts/filters/applied-filters/roots/global-view-root.tsx +++ b/web/core/components/issues/issue-layouts/filters/applied-filters/roots/global-view-root.tsx @@ -17,12 +17,12 @@ import { IIssueFilterOptions, TStaticViewTypes } from "@plane/types"; //ui // components import { Header, EHeaderVariant, Loader } from "@plane/ui"; +import { cn } from "@plane/utils"; import { AppliedFiltersList } from "@/components/issues"; import { UpdateViewComponent } from "@/components/views/update-view-component"; import { CreateUpdateWorkspaceViewModal } from "@/components/workspace"; // constants // helpers -import { cn } from "@/helpers/common.helper"; // hooks import { useEventTracker, useGlobalView, useIssues, useLabel, useUser, useUserPermissions } from "@/hooks/store"; import { getAreFiltersEqual } from "../../../utils"; diff --git a/web/core/components/issues/issue-layouts/filters/applied-filters/state.tsx b/web/core/components/issues/issue-layouts/filters/applied-filters/state.tsx index 65d679c64..d02fd6dad 100644 --- a/web/core/components/issues/issue-layouts/filters/applied-filters/state.tsx +++ b/web/core/components/issues/issue-layouts/filters/applied-filters/state.tsx @@ -4,8 +4,8 @@ import { observer } from "mobx-react"; // icons import { X } from "lucide-react"; -import { IState } from "@plane/types"; import { EIconSize } from "@plane/constants"; +import { IState } from "@plane/types"; import { StateGroupIcon } from "@plane/ui"; // types diff --git a/web/core/components/issues/issue-layouts/filters/header/filters/assignee.tsx b/web/core/components/issues/issue-layouts/filters/header/filters/assignee.tsx index 553fd7d41..f4a261316 100644 --- a/web/core/components/issues/issue-layouts/filters/header/filters/assignee.tsx +++ b/web/core/components/issues/issue-layouts/filters/header/filters/assignee.tsx @@ -6,9 +6,9 @@ import { observer } from "mobx-react"; // plane ui import { Avatar, Loader } from "@plane/ui"; // components +import { getFileURL } from "@plane/utils"; import { FilterHeader, FilterOption } from "@/components/issues"; // helpers -import { getFileURL } from "@/helpers/file.helper"; // hooks import { useMember, useUser } from "@/hooks/store"; diff --git a/web/core/components/issues/issue-layouts/filters/header/filters/created-by.tsx b/web/core/components/issues/issue-layouts/filters/header/filters/created-by.tsx index 2fbcf6233..9dd7c1d09 100644 --- a/web/core/components/issues/issue-layouts/filters/header/filters/created-by.tsx +++ b/web/core/components/issues/issue-layouts/filters/header/filters/created-by.tsx @@ -6,9 +6,9 @@ import { observer } from "mobx-react"; // plane ui import { Avatar, Loader } from "@plane/ui"; // components +import { getFileURL } from "@plane/utils"; import { FilterHeader, FilterOption } from "@/components/issues"; // helpers -import { getFileURL } from "@/helpers/file.helper"; // hooks import { useMember, useUser } from "@/hooks/store"; diff --git a/web/core/components/issues/issue-layouts/filters/header/filters/mentions.tsx b/web/core/components/issues/issue-layouts/filters/header/filters/mentions.tsx index e38f5794d..7e60eb5bb 100644 --- a/web/core/components/issues/issue-layouts/filters/header/filters/mentions.tsx +++ b/web/core/components/issues/issue-layouts/filters/header/filters/mentions.tsx @@ -6,9 +6,9 @@ import { observer } from "mobx-react"; // plane ui import { Loader, Avatar } from "@plane/ui"; // components +import { getFileURL } from "@plane/utils"; import { FilterHeader, FilterOption } from "@/components/issues"; // helpers -import { getFileURL } from "@/helpers/file.helper"; // hooks import { useMember, useUser } from "@/hooks/store"; diff --git a/web/core/components/issues/issue-layouts/gantt/base-gantt-root.tsx b/web/core/components/issues/issue-layouts/gantt/base-gantt-root.tsx index 43410d135..103b49f35 100644 --- a/web/core/components/issues/issue-layouts/gantt/base-gantt-root.tsx +++ b/web/core/components/issues/issue-layouts/gantt/base-gantt-root.tsx @@ -10,15 +10,16 @@ import { EUserPermissionsLevel, } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; -import { TIssue } from "@plane/types"; +import type { IBlockUpdateData, TIssue } from "@plane/types"; import { setToast, TOAST_TYPE } from "@plane/ui"; // hooks -import { GanttChartRoot, IBlockUpdateData, IssueGanttSidebar } from "@/components/gantt-chart"; +import { renderFormattedPayloadDate } from "@plane/utils"; import { ETimeLineTypeType, TimeLineTypeContext } from "@/components/gantt-chart/contexts"; +import { GanttChartRoot } from "@/components/gantt-chart/root"; +import { IssueGanttSidebar } from "@/components/gantt-chart/sidebar/issues/sidebar"; import { QuickAddIssueRoot, IssueGanttBlock, GanttQuickAddIssueButton } from "@/components/issues"; //constants // helpers -import { renderFormattedPayloadDate } from "@/helpers/date-time.helper"; //hooks import { useIssues, useUserPermissions } from "@/hooks/store"; import { useIssueStoreType } from "@/hooks/use-issue-layout-store"; diff --git a/web/core/components/issues/issue-layouts/gantt/blocks.tsx b/web/core/components/issues/issue-layouts/gantt/blocks.tsx index 0281e8b49..e2babd371 100644 --- a/web/core/components/issues/issue-layouts/gantt/blocks.tsx +++ b/web/core/components/issues/issue-layouts/gantt/blocks.tsx @@ -4,11 +4,10 @@ import { observer } from "mobx-react"; import { useParams } from "next/navigation"; // ui import { Tooltip, ControlLink } from "@plane/ui"; +import { findTotalDaysInRange, generateWorkItemLink } from "@plane/utils"; // components -import { findTotalDaysInRange } from "@plane/utils"; import { SIDEBAR_WIDTH } from "@/components/gantt-chart/constants"; // helpers -import { generateWorkItemLink } from "@/helpers/issue.helper"; // hooks import { useIssueDetail, useIssues, useProject, useProjectState } from "@/hooks/store"; import { useIssueStoreType } from "@/hooks/use-issue-layout-store"; diff --git a/web/core/components/issues/issue-layouts/group-drag-overlay.tsx b/web/core/components/issues/issue-layouts/group-drag-overlay.tsx index df62e37f6..eee5f5a3a 100644 --- a/web/core/components/issues/issue-layouts/group-drag-overlay.tsx +++ b/web/core/components/issues/issue-layouts/group-drag-overlay.tsx @@ -5,7 +5,7 @@ import { ISSUE_ORDER_BY_OPTIONS } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { TIssueOrderByOptions } from "@plane/types"; // helpers -import { cn } from "@/helpers/common.helper"; +import { cn } from "@plane/utils"; // plane web imports import { WorkFlowDisabledOverlay } from "@/plane-web/components/workflow"; diff --git a/web/core/components/issues/issue-layouts/kanban/block.tsx b/web/core/components/issues/issue-layouts/kanban/block.tsx index 87ff9baf4..2347df968 100644 --- a/web/core/components/issues/issue-layouts/kanban/block.tsx +++ b/web/core/components/issues/issue-layouts/kanban/block.tsx @@ -13,12 +13,11 @@ import { useOutsideClickDetector } from "@plane/hooks"; import { TIssue, IIssueDisplayProperties, IIssueMap } from "@plane/types"; // ui import { ControlLink, DropIndicator, TOAST_TYPE, Tooltip, setToast } from "@plane/ui"; +import { cn, generateWorkItemLink } from "@plane/utils"; // components import RenderIfVisible from "@/components/core/render-if-visible-HOC"; import { HIGHLIGHT_CLASS } from "@/components/issues/issue-layouts/utils"; // helpers -import { cn } from "@/helpers/common.helper"; -import { generateWorkItemLink } from "@/helpers/issue.helper"; // hooks import { useIssueDetail, useKanbanView, useProject } from "@/hooks/store"; import useIssuePeekOverviewRedirection from "@/hooks/use-issue-peek-overview-redirection"; diff --git a/web/core/components/issues/issue-layouts/kanban/kanban-group.tsx b/web/core/components/issues/issue-layouts/kanban/kanban-group.tsx index 02ae41d6a..988f24486 100644 --- a/web/core/components/issues/issue-layouts/kanban/kanban-group.tsx +++ b/web/core/components/issues/issue-layouts/kanban/kanban-group.tsx @@ -20,11 +20,11 @@ import { TIssueOrderByOptions, } from "@plane/types"; import { TOAST_TYPE, setToast } from "@plane/ui"; +import { cn } from "@plane/utils"; import { KanbanQuickAddIssueButton, QuickAddIssueRoot } from "@/components/issues"; import { highlightIssueOnDrop } from "@/components/issues/issue-layouts/utils"; import { KanbanIssueBlockLoader } from "@/components/ui"; // helpers -import { cn } from "@/helpers/common.helper"; // hooks import { useProjectState } from "@/hooks/store"; import { useIntersectionObserver } from "@/hooks/use-intersection-observer"; diff --git a/web/core/components/issues/issue-layouts/list/block.tsx b/web/core/components/issues/issue-layouts/list/block.tsx index 0893fb492..67d282753 100644 --- a/web/core/components/issues/issue-layouts/list/block.tsx +++ b/web/core/components/issues/issue-layouts/list/block.tsx @@ -11,12 +11,11 @@ import { EIssueServiceType } from "@plane/constants"; import { TIssue, IIssueDisplayProperties, TIssueMap } from "@plane/types"; // ui import { Spinner, Tooltip, ControlLink, setToast, TOAST_TYPE, Row } from "@plane/ui"; +import { cn, generateWorkItemLink } from "@plane/utils"; // components import { MultipleSelectEntityAction } from "@/components/core"; import { IssueProperties } from "@/components/issues/issue-layouts/properties"; // helpers -import { cn } from "@/helpers/common.helper"; -import { generateWorkItemLink } from "@/helpers/issue.helper"; // hooks import { useAppTheme, useIssueDetail, useProject } from "@/hooks/store"; import { TSelectionHelper } from "@/hooks/use-multiple-select"; diff --git a/web/core/components/issues/issue-layouts/list/headers/group-by-card.tsx b/web/core/components/issues/issue-layouts/list/headers/group-by-card.tsx index 4ba026989..44bc95bff 100644 --- a/web/core/components/issues/issue-layouts/list/headers/group-by-card.tsx +++ b/web/core/components/issues/issue-layouts/list/headers/group-by-card.tsx @@ -9,11 +9,11 @@ import { TIssue, ISearchIssueResponse, TIssueGroupByOptions } from "@plane/types // ui import { CustomMenu, TOAST_TYPE, setToast } from "@plane/ui"; // components +import { cn } from "@plane/utils"; import { ExistingIssuesListModal, MultipleSelectGroupAction } from "@/components/core"; import { CreateUpdateIssueModal } from "@/components/issues"; // constants // helpers -import { cn } from "@/helpers/common.helper"; // hooks import { useEventTracker } from "@/hooks/store"; import { useIssueStoreType } from "@/hooks/use-issue-layout-store"; diff --git a/web/core/components/issues/issue-layouts/properties/all-properties.tsx b/web/core/components/issues/issue-layouts/properties/all-properties.tsx index aacabc448..877aa85d9 100644 --- a/web/core/components/issues/issue-layouts/properties/all-properties.tsx +++ b/web/core/components/issues/issue-layouts/properties/all-properties.tsx @@ -13,6 +13,7 @@ import { useTranslation } from "@plane/i18n"; import { TIssue, IIssueDisplayProperties, TIssuePriorities } from "@plane/types"; // ui import { Tooltip } from "@plane/ui"; +import { cn, getDate, renderFormattedPayloadDate, generateWorkItemLink, shouldHighlightIssueDueDate } from "@plane/utils"; // components import { EstimateDropdown, @@ -25,9 +26,6 @@ import { } from "@/components/dropdowns"; // constants // helpers -import { cn } from "@/helpers/common.helper"; -import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper"; -import { generateWorkItemLink, shouldHighlightIssueDueDate } from "@/helpers/issue.helper"; // hooks import { useEventTracker, useLabel, useIssues, useProjectState, useProject, useProjectEstimates } from "@/hooks/store"; import { useAppRouter } from "@/hooks/use-app-router"; diff --git a/web/core/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx b/web/core/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx index 0cb10fdbc..d3524559d 100644 --- a/web/core/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx +++ b/web/core/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx @@ -8,10 +8,9 @@ import { useParams } from "next/navigation"; import { ARCHIVABLE_STATE_GROUPS, EIssuesStoreType } from "@plane/constants"; import { TIssue } from "@plane/types"; import { ContextMenu, CustomMenu } from "@plane/ui"; +import { cn } from "@plane/utils"; // components import { ArchiveIssueModal, CreateUpdateIssueModal, DeleteIssueModal } from "@/components/issues"; -// helpers -import { cn } from "@/helpers/common.helper"; // hooks import { useEventTracker, useProject, useProjectState } from "@/hooks/store"; // plane-web components diff --git a/web/core/components/issues/issue-layouts/quick-action-dropdowns/archived-issue.tsx b/web/core/components/issues/issue-layouts/quick-action-dropdowns/archived-issue.tsx index 654a572ff..b8b74764d 100644 --- a/web/core/components/issues/issue-layouts/quick-action-dropdowns/archived-issue.tsx +++ b/web/core/components/issues/issue-layouts/quick-action-dropdowns/archived-issue.tsx @@ -6,10 +6,10 @@ import { useParams } from "next/navigation"; // ui import { EIssuesStoreType, EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; import { ContextMenu, CustomMenu } from "@plane/ui"; +import { cn } from "@plane/utils"; // components import { DeleteIssueModal } from "@/components/issues"; // helpers -import { cn } from "@/helpers/common.helper"; // hooks import { useEventTracker, useIssues, useUserPermissions } from "@/hooks/store"; // types diff --git a/web/core/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx b/web/core/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx index dd7e66fc5..75bb8633a 100644 --- a/web/core/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx +++ b/web/core/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx @@ -8,10 +8,9 @@ import { useParams } from "next/navigation"; import { ARCHIVABLE_STATE_GROUPS, EIssuesStoreType, EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; import { TIssue } from "@plane/types"; import { ContextMenu, CustomMenu } from "@plane/ui"; +import { cn } from "@plane/utils"; // components import { ArchiveIssueModal, CreateUpdateIssueModal, DeleteIssueModal } from "@/components/issues"; -// helpers -import { cn } from "@/helpers/common.helper"; // hooks import { useEventTracker, useIssues, useProject, useProjectState, useUserPermissions } from "@/hooks/store"; // plane-web components diff --git a/web/core/components/issues/issue-layouts/quick-action-dropdowns/draft-issue.tsx b/web/core/components/issues/issue-layouts/quick-action-dropdowns/draft-issue.tsx index 040c093d5..e3d89f8a2 100644 --- a/web/core/components/issues/issue-layouts/quick-action-dropdowns/draft-issue.tsx +++ b/web/core/components/issues/issue-layouts/quick-action-dropdowns/draft-issue.tsx @@ -8,15 +8,13 @@ import { useParams, usePathname } from "next/navigation"; import { EIssuesStoreType, EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; import { TIssue } from "@plane/types"; import { ContextMenu, CustomMenu } from "@plane/ui"; +import { cn } from "@plane/utils"; // components import { CreateUpdateIssueModal, DeleteIssueModal } from "@/components/issues"; -// helpers -import { cn } from "@/helpers/common.helper"; // hooks import { useEventTracker, useUserPermissions } from "@/hooks/store"; -// types +// local imports import { IQuickActionProps } from "../list/list-view-types"; -// helper import { useDraftIssueMenuItems, MenuItemFactoryProps } from "./helper"; export const DraftIssueQuickActions: React.FC = observer((props) => { diff --git a/web/core/components/issues/issue-layouts/quick-action-dropdowns/helper.tsx b/web/core/components/issues/issue-layouts/quick-action-dropdowns/helper.tsx index 790beff46..3ebcc7e70 100644 --- a/web/core/components/issues/issue-layouts/quick-action-dropdowns/helper.tsx +++ b/web/core/components/issues/issue-layouts/quick-action-dropdowns/helper.tsx @@ -5,9 +5,7 @@ import { EIssuesStoreType } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { TIssue } from "@plane/types"; import { ArchiveIcon, TContextMenuItem, TOAST_TYPE, setToast } from "@plane/ui"; -import { copyUrlToClipboard } from "@plane/utils"; -// helpers -import { generateWorkItemLink } from "@/helpers/issue.helper"; +import { copyUrlToClipboard, generateWorkItemLink } from "@plane/utils"; // types import { createCopyMenuWithDuplication } from "@/plane-web/components/issues/issue-layouts/quick-action-dropdowns"; diff --git a/web/core/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx b/web/core/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx index 6a8840ace..e23ff7501 100644 --- a/web/core/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx +++ b/web/core/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx @@ -8,10 +8,9 @@ import { useParams } from "next/navigation"; import { ARCHIVABLE_STATE_GROUPS, EIssuesStoreType, EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; import { TIssue } from "@plane/types"; import { ContextMenu, CustomMenu } from "@plane/ui"; +import { cn } from "@plane/utils"; // components import { ArchiveIssueModal, CreateUpdateIssueModal, DeleteIssueModal } from "@/components/issues"; -// helpers -import { cn } from "@/helpers/common.helper"; // hooks import { useIssues, useEventTracker, useProjectState, useUserPermissions, useProject } from "@/hooks/store"; // plane-web components diff --git a/web/core/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx b/web/core/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx index 4326743fa..994648544 100644 --- a/web/core/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx +++ b/web/core/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx @@ -8,10 +8,9 @@ import { useParams, usePathname } from "next/navigation"; import { ARCHIVABLE_STATE_GROUPS, EIssuesStoreType, EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; import { TIssue } from "@plane/types"; import { ContextMenu, CustomMenu } from "@plane/ui"; +import { cn } from "@plane/utils"; // components import { ArchiveIssueModal, CreateUpdateIssueModal, DeleteIssueModal } from "@/components/issues"; -// helpers -import { cn } from "@/helpers/common.helper"; // hooks import { useEventTracker, useIssues, useProject, useProjectState, useUserPermissions } from "@/hooks/store"; // plane-web components diff --git a/web/core/components/issues/issue-layouts/quick-add/form/gantt.tsx b/web/core/components/issues/issue-layouts/quick-add/form/gantt.tsx index 19f047676..e5f27e0c5 100644 --- a/web/core/components/issues/issue-layouts/quick-add/form/gantt.tsx +++ b/web/core/components/issues/issue-layouts/quick-add/form/gantt.tsx @@ -1,7 +1,7 @@ import { FC } from "react"; import { observer } from "mobx-react"; import { useTranslation } from "@plane/i18n"; -import { cn } from "@/helpers/common.helper"; +import { cn } from "@plane/utils"; import { TQuickAddIssueForm } from "../root"; export const GanttQuickAddIssueForm: FC = observer((props) => { diff --git a/web/core/components/issues/issue-layouts/quick-add/root.tsx b/web/core/components/issues/issue-layouts/quick-add/root.tsx index 6bc6997a4..28c4edbe5 100644 --- a/web/core/components/issues/issue-layouts/quick-add/root.tsx +++ b/web/core/components/issues/issue-layouts/quick-add/root.tsx @@ -12,12 +12,11 @@ import { useTranslation } from "@plane/i18n"; import { IProject, TIssue } from "@plane/types"; // ui import { setPromiseToast } from "@plane/ui"; +import { cn, createIssuePayload } from "@plane/utils"; // components import { CreateIssueToastActionItems } from "@/components/issues"; // constants // helpers -import { cn } from "@/helpers/common.helper"; -import { createIssuePayload } from "@/helpers/issue.helper"; // hooks import { useEventTracker } from "@/hooks/store"; // plane web components diff --git a/web/core/components/issues/issue-layouts/spreadsheet/columns/created-on-column.tsx b/web/core/components/issues/issue-layouts/spreadsheet/columns/created-on-column.tsx index 79ac6e924..d1e2b4b54 100644 --- a/web/core/components/issues/issue-layouts/spreadsheet/columns/created-on-column.tsx +++ b/web/core/components/issues/issue-layouts/spreadsheet/columns/created-on-column.tsx @@ -4,7 +4,7 @@ import { observer } from "mobx-react"; import { TIssue } from "@plane/types"; // helpers import { Row } from "@plane/ui"; -import { renderFormattedDate } from "@/helpers/date-time.helper"; +import { renderFormattedDate } from "@plane/utils"; type Props = { issue: TIssue; diff --git a/web/core/components/issues/issue-layouts/spreadsheet/columns/due-date-column.tsx b/web/core/components/issues/issue-layouts/spreadsheet/columns/due-date-column.tsx index 57858ff75..98612b46f 100644 --- a/web/core/components/issues/issue-layouts/spreadsheet/columns/due-date-column.tsx +++ b/web/core/components/issues/issue-layouts/spreadsheet/columns/due-date-column.tsx @@ -3,12 +3,10 @@ import { observer } from "mobx-react"; import { CalendarCheck2 } from "lucide-react"; // types import { TIssue } from "@plane/types"; +import { cn, getDate, renderFormattedPayloadDate, shouldHighlightIssueDueDate } from "@plane/utils"; // components import { DateDropdown } from "@/components/dropdowns"; // helpers -import { cn } from "@/helpers/common.helper"; -import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper"; -import { shouldHighlightIssueDueDate } from "@/helpers/issue.helper"; // hooks import { useProjectState } from "@/hooks/store"; diff --git a/web/core/components/issues/issue-layouts/spreadsheet/columns/start-date-column.tsx b/web/core/components/issues/issue-layouts/spreadsheet/columns/start-date-column.tsx index a20c24722..397e68905 100644 --- a/web/core/components/issues/issue-layouts/spreadsheet/columns/start-date-column.tsx +++ b/web/core/components/issues/issue-layouts/spreadsheet/columns/start-date-column.tsx @@ -4,9 +4,9 @@ import { CalendarClock } from "lucide-react"; // types import { TIssue } from "@plane/types"; // components +import { getDate, renderFormattedPayloadDate } from "@plane/utils"; import { DateDropdown } from "@/components/dropdowns"; // helpers -import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper"; type Props = { issue: TIssue; diff --git a/web/core/components/issues/issue-layouts/spreadsheet/columns/sub-issue-column.tsx b/web/core/components/issues/issue-layouts/spreadsheet/columns/sub-issue-column.tsx index 54f3d8586..21e171445 100644 --- a/web/core/components/issues/issue-layouts/spreadsheet/columns/sub-issue-column.tsx +++ b/web/core/components/issues/issue-layouts/spreadsheet/columns/sub-issue-column.tsx @@ -5,7 +5,7 @@ import { useParams } from "next/navigation"; import { TIssue } from "@plane/types"; // helpers import { Row } from "@plane/ui"; -import { cn } from "@/helpers/common.helper"; +import { cn } from "@plane/utils"; // hooks import { useAppRouter } from "@/hooks/use-app-router"; import { IssueStats } from "@/plane-web/components/issues/issue-layouts/issue-stats"; diff --git a/web/core/components/issues/issue-layouts/spreadsheet/columns/updated-on-column.tsx b/web/core/components/issues/issue-layouts/spreadsheet/columns/updated-on-column.tsx index a93c01700..9cdf79720 100644 --- a/web/core/components/issues/issue-layouts/spreadsheet/columns/updated-on-column.tsx +++ b/web/core/components/issues/issue-layouts/spreadsheet/columns/updated-on-column.tsx @@ -4,7 +4,7 @@ import { observer } from "mobx-react"; import { TIssue } from "@plane/types"; // helpers import { Row } from "@plane/ui"; -import { renderFormattedDate } from "@/helpers/date-time.helper"; +import { renderFormattedDate } from "@plane/utils"; type Props = { issue: TIssue; diff --git a/web/core/components/issues/issue-layouts/spreadsheet/issue-row.tsx b/web/core/components/issues/issue-layouts/spreadsheet/issue-row.tsx index c60519053..a645bffe2 100644 --- a/web/core/components/issues/issue-layouts/spreadsheet/issue-row.tsx +++ b/web/core/components/issues/issue-layouts/spreadsheet/issue-row.tsx @@ -11,12 +11,11 @@ import { useOutsideClickDetector } from "@plane/hooks"; import { IIssueDisplayProperties, TIssue } from "@plane/types"; // ui import { ControlLink, Row, Tooltip } from "@plane/ui"; +import { cn, generateWorkItemLink } from "@plane/utils"; // components import { MultipleSelectEntityAction } from "@/components/core"; import RenderIfVisible from "@/components/core/render-if-visible-HOC"; // helper -import { cn } from "@/helpers/common.helper"; -import { generateWorkItemLink } from "@/helpers/issue.helper"; // hooks import { useIssueDetail, useIssues, useProject } from "@/hooks/store"; import useIssuePeekOverviewRedirection from "@/hooks/use-issue-peek-overview-redirection"; diff --git a/web/core/components/issues/issue-layouts/spreadsheet/spreadsheet-header.tsx b/web/core/components/issues/issue-layouts/spreadsheet/spreadsheet-header.tsx index d58fc2654..1a68aba4d 100644 --- a/web/core/components/issues/issue-layouts/spreadsheet/spreadsheet-header.tsx +++ b/web/core/components/issues/issue-layouts/spreadsheet/spreadsheet-header.tsx @@ -6,10 +6,10 @@ import { SPREADSHEET_SELECT_GROUP } from "@plane/constants"; import { IIssueDisplayFilterOptions, IIssueDisplayProperties } from "@plane/types"; // components import { Row } from "@plane/ui"; +import { cn } from "@plane/utils"; import { MultipleSelectGroupAction } from "@/components/core"; import { SpreadsheetHeaderColumn } from "@/components/issues/issue-layouts"; // helpers -import { cn } from "@/helpers/common.helper"; // hooks import { TSelectionHelper } from "@/hooks/use-multiple-select"; diff --git a/web/core/components/issues/issue-layouts/utils.tsx b/web/core/components/issues/issue-layouts/utils.tsx index c0e9dc658..f16bdcc49 100644 --- a/web/core/components/issues/issue-layouts/utils.tsx +++ b/web/core/components/issues/issue-layouts/utils.tsx @@ -30,11 +30,10 @@ import { } from "@plane/types"; // plane ui import { Avatar, CycleGroupIcon, DiceIcon, ISvgIcons, PriorityIcon, StateGroupIcon } from "@plane/ui"; +import { renderFormattedDate, getFileURL } from "@plane/utils"; // components import { Logo } from "@/components/common"; // helpers -import { renderFormattedDate } from "@/helpers/date-time.helper"; -import { getFileURL } from "@/helpers/file.helper"; // store import { store } from "@/lib/store-context"; // plane web store diff --git a/web/core/components/issues/issue-modal/components/default-properties.tsx b/web/core/components/issues/issue-modal/components/default-properties.tsx index 5696743c7..20eb13a4c 100644 --- a/web/core/components/issues/issue-modal/components/default-properties.tsx +++ b/web/core/components/issues/issue-modal/components/default-properties.tsx @@ -11,6 +11,7 @@ import { useTranslation } from "@plane/i18n"; import { ISearchIssueResponse, TIssue } from "@plane/types"; // ui import { CustomMenu } from "@plane/ui"; +import { getDate, renderFormattedPayloadDate, getTabIndex } from "@plane/utils"; // components import { CycleDropdown, @@ -24,8 +25,6 @@ import { import { ParentIssuesListModal } from "@/components/issues"; import { IssueLabelSelect } from "@/components/issues/select"; // helpers -import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper"; -import { getTabIndex } from "@/helpers/tab-indices.helper"; // hooks import { useProjectEstimates, useProject, useUserPermissions } from "@/hooks/store"; import { usePlatformOS } from "@/hooks/use-platform-os"; diff --git a/web/core/components/issues/issue-modal/components/description-editor.tsx b/web/core/components/issues/issue-modal/components/description-editor.tsx index 9cae8d840..b1bbce5b9 100644 --- a/web/core/components/issues/issue-modal/components/description-editor.tsx +++ b/web/core/components/issues/issue-modal/components/description-editor.tsx @@ -15,12 +15,11 @@ import { TIssue } from "@plane/types"; import { EFileAssetType } from "@plane/types/src/enums"; // ui import { Loader, setToast, TOAST_TYPE } from "@plane/ui"; +import { getDescriptionPlaceholderI18n, getTabIndex } from "@plane/utils"; // components import { GptAssistantPopover } from "@/components/core"; import { RichTextEditor } from "@/components/editor"; // helpers -import { getDescriptionPlaceholderI18n } from "@/helpers/issue.helper"; -import { getTabIndex } from "@/helpers/tab-indices.helper"; // hooks import { useEditorAsset, useInstance, useWorkspace } from "@/hooks/store"; import useKeypress from "@/hooks/use-keypress"; diff --git a/web/core/components/issues/issue-modal/components/parent-tag.tsx b/web/core/components/issues/issue-modal/components/parent-tag.tsx index 583bc850b..10d936001 100644 --- a/web/core/components/issues/issue-modal/components/parent-tag.tsx +++ b/web/core/components/issues/issue-modal/components/parent-tag.tsx @@ -9,7 +9,7 @@ import { ETabIndices } from "@plane/constants"; // types import { ISearchIssueResponse, TIssue } from "@plane/types"; // helpers -import { getTabIndex } from "@/helpers/tab-indices.helper"; +import { getTabIndex } from "@plane/utils"; // hooks import { usePlatformOS } from "@/hooks/use-platform-os"; // plane web components diff --git a/web/core/components/issues/issue-modal/components/project-select.tsx b/web/core/components/issues/issue-modal/components/project-select.tsx index 3c2d235d0..726e2f38a 100644 --- a/web/core/components/issues/issue-modal/components/project-select.tsx +++ b/web/core/components/issues/issue-modal/components/project-select.tsx @@ -7,10 +7,9 @@ import { Control, Controller } from "react-hook-form"; import { ETabIndices } from "@plane/constants"; // types import { TIssue } from "@plane/types"; +import { getTabIndex } from "@plane/utils"; // components import { ProjectDropdown } from "@/components/dropdowns"; -// helpers -import { getTabIndex } from "@/helpers/tab-indices.helper"; // hooks import { useIssueModal } from "@/hooks/context/use-issue-modal"; import { usePlatformOS } from "@/hooks/use-platform-os"; diff --git a/web/core/components/issues/issue-modal/components/title-input.tsx b/web/core/components/issues/issue-modal/components/title-input.tsx index aec939149..11c07eaae 100644 --- a/web/core/components/issues/issue-modal/components/title-input.tsx +++ b/web/core/components/issues/issue-modal/components/title-input.tsx @@ -11,7 +11,7 @@ import { TIssue } from "@plane/types"; // ui import { Input } from "@plane/ui"; // helpers -import { getTabIndex } from "@/helpers/tab-indices.helper"; +import { getTabIndex } from "@plane/utils"; // hooks import { usePlatformOS } from "@/hooks/use-platform-os"; diff --git a/web/core/components/issues/issue-modal/context/issue-modal-context.tsx b/web/core/components/issues/issue-modal/context/issue-modal-context.tsx index 064640885..e2c2f3957 100644 --- a/web/core/components/issues/issue-modal/context/issue-modal-context.tsx +++ b/web/core/components/issues/issue-modal/context/issue-modal-context.tsx @@ -1,12 +1,12 @@ import { createContext } from "react"; // ce imports -import { TIssueFields } from "ce/components/issues"; // react-hook-form import { UseFormReset, UseFormWatch } from "react-hook-form"; // plane imports import { EditorRefApi } from "@plane/editor"; import { ISearchIssueResponse, TIssue } from "@plane/types"; import { TIssuePropertyValues, TIssuePropertyValueErrors } from "@/plane-web/types/issue-types"; +import { TIssueFields } from "ce/components/issues"; export type TPropertyValuesValidationProps = { projectId: string | null; diff --git a/web/core/components/issues/issue-modal/draft-issue-layout.tsx b/web/core/components/issues/issue-modal/draft-issue-layout.tsx index e10ca34b7..96cc4a322 100644 --- a/web/core/components/issues/issue-modal/draft-issue-layout.tsx +++ b/web/core/components/issues/issue-modal/draft-issue-layout.tsx @@ -10,9 +10,9 @@ import type { TIssue } from "@plane/types"; // ui import { TOAST_TYPE, setToast } from "@plane/ui"; // components +import { isEmptyHtmlString } from "@plane/utils"; import { ConfirmIssueDiscard } from "@/components/issues"; // helpers -import { isEmptyHtmlString } from "@/helpers/string.helper"; // hooks import { useIssueModal } from "@/hooks/context/use-issue-modal"; import { useEventTracker, useWorkspaceDraftIssues } from "@/hooks/store"; diff --git a/web/core/components/issues/issue-modal/form.tsx b/web/core/components/issues/issue-modal/form.tsx index ca37cbf24..addf1917a 100644 --- a/web/core/components/issues/issue-modal/form.tsx +++ b/web/core/components/issues/issue-modal/form.tsx @@ -13,8 +13,8 @@ import { useTranslation } from "@plane/i18n"; import type { TIssue, TWorkspaceDraftIssue } from "@plane/types"; // hooks import { Button, ToggleSwitch, TOAST_TYPE, setToast } from "@plane/ui"; +import { convertWorkItemDataToSearchResponse, getUpdateFormDataForReset, cn, getTextContent, getChangedIssuefields, getTabIndex } from "@plane/utils"; // components -import { convertWorkItemDataToSearchResponse, getUpdateFormDataForReset } from "@plane/utils"; import { IssueDefaultProperties, IssueDescriptionEditor, @@ -24,10 +24,6 @@ import { } from "@/components/issues/issue-modal/components"; import { CreateLabelModal } from "@/components/labels"; // helpers -import { cn } from "@/helpers/common.helper"; -import { getTextContent } from "@/helpers/editor.helper"; -import { getChangedIssuefields } from "@/helpers/issue-modal.helper"; -import { getTabIndex } from "@/helpers/tab-indices.helper"; // hooks import { useIssueModal } from "@/hooks/context/use-issue-modal"; import { useIssueDetail, useProject, useProjectState, useWorkspaceDraftIssues } from "@/hooks/store"; diff --git a/web/core/components/issues/parent-issues-list-modal.tsx b/web/core/components/issues/parent-issues-list-modal.tsx index e289caf2a..835663a94 100644 --- a/web/core/components/issues/parent-issues-list-modal.tsx +++ b/web/core/components/issues/parent-issues-list-modal.tsx @@ -12,11 +12,10 @@ import { useTranslation } from "@plane/i18n"; import { ISearchIssueResponse } from "@plane/types"; // ui import { Loader } from "@plane/ui"; +import { generateWorkItemLink, getTabIndex } from "@plane/utils"; // components import { IssueSearchModalEmptyState } from "@/components/core"; // helpers -import { generateWorkItemLink } from "@/helpers/issue.helper"; -import { getTabIndex } from "@/helpers/tab-indices.helper"; // hooks import useDebounce from "@/hooks/use-debounce"; import { usePlatformOS } from "@/hooks/use-platform-os"; diff --git a/web/core/components/issues/peek-overview/header.tsx b/web/core/components/issues/peek-overview/header.tsx index c5d66e87d..2078d4c8a 100644 --- a/web/core/components/issues/peek-overview/header.tsx +++ b/web/core/components/issues/peek-overview/header.tsx @@ -18,12 +18,10 @@ import { Tooltip, setToast, } from "@plane/ui"; -import { copyUrlToClipboard } from "@plane/utils"; +import { copyUrlToClipboard, cn, generateWorkItemLink } from "@plane/utils"; // components import { IssueSubscription, NameDescriptionUpdateStatus } from "@/components/issues"; // helpers -import { cn } from "@/helpers/common.helper"; -import { generateWorkItemLink } from "@/helpers/issue.helper"; // store hooks import { useIssueDetail, useProject, useProjectState, useUser } from "@/hooks/store"; // hooks diff --git a/web/core/components/issues/peek-overview/issue-detail.tsx b/web/core/components/issues/peek-overview/issue-detail.tsx index 7960f80e1..6873da488 100644 --- a/web/core/components/issues/peek-overview/issue-detail.tsx +++ b/web/core/components/issues/peek-overview/issue-detail.tsx @@ -5,10 +5,10 @@ import { observer } from "mobx-react"; import { EditorRefApi } from "@plane/editor"; import { TNameDescriptionLoader } from "@plane/types"; // components +import { getTextContent } from "@plane/utils"; import { DescriptionVersionsRoot } from "@/components/core/description-versions"; import { IssueParentDetail, TIssueOperations } from "@/components/issues"; // helpers -import { getTextContent } from "@/helpers/editor.helper"; // hooks import { useIssueDetail, useMember, useProject, useUser } from "@/hooks/store"; import useReloadConfirmations from "@/hooks/use-reload-confirmation"; diff --git a/web/core/components/issues/peek-overview/properties.tsx b/web/core/components/issues/peek-overview/properties.tsx index 93516297f..41921cf56 100644 --- a/web/core/components/issues/peek-overview/properties.tsx +++ b/web/core/components/issues/peek-overview/properties.tsx @@ -7,6 +7,7 @@ import { Signal, Tag, Triangle, LayoutPanelTop, CalendarClock, CalendarCheck2, U import { useTranslation } from "@plane/i18n"; // ui icons import { DiceIcon, DoubleCircleIcon, ContrastIcon } from "@plane/ui"; +import { cn, getDate, renderFormattedPayloadDate, shouldHighlightIssueDueDate } from "@plane/utils"; // components import { DateDropdown, @@ -18,9 +19,6 @@ import { import { ButtonAvatars } from "@/components/dropdowns/member/avatar"; import { IssueCycleSelect, IssueModuleSelect, IssueLabel, TIssueOperations } from "@/components/issues"; // helpers -import { cn } from "@/helpers/common.helper"; -import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper"; -import { shouldHighlightIssueDueDate } from "@/helpers/issue.helper"; import { useIssueDetail, useMember, useProject, useProjectState } from "@/hooks/store"; // plane web components import { IssueParentSelectRoot, IssueWorklogProperty } from "@/plane-web/components/issues"; diff --git a/web/core/components/issues/peek-overview/view.tsx b/web/core/components/issues/peek-overview/view.tsx index 72bca2408..e75b91268 100644 --- a/web/core/components/issues/peek-overview/view.tsx +++ b/web/core/components/issues/peek-overview/view.tsx @@ -5,6 +5,7 @@ import { EIssueServiceType } from "@plane/constants"; // types import { TNameDescriptionLoader } from "@plane/types"; // components +import { cn } from "@plane/utils"; import { DeleteIssueModal, IssuePeekOverviewHeader, @@ -18,7 +19,6 @@ import { IssueDetailWidgets, } from "@/components/issues"; // helpers -import { cn } from "@/helpers/common.helper"; // hooks import { useIssueDetail } from "@/hooks/store"; import useKeypress from "@/hooks/use-keypress"; diff --git a/web/core/components/issues/relations/issue-list-item.tsx b/web/core/components/issues/relations/issue-list-item.tsx index ee2225327..eeedabdf9 100644 --- a/web/core/components/issues/relations/issue-list-item.tsx +++ b/web/core/components/issues/relations/issue-list-item.tsx @@ -9,9 +9,9 @@ import { useTranslation } from "@plane/i18n"; import { TIssue, TIssueServiceType } from "@plane/types"; import { ControlLink, CustomMenu, Tooltip } from "@plane/ui"; // components +import { generateWorkItemLink } from "@plane/utils"; import { RelationIssueProperty } from "@/components/issues/relations"; // helpers -import { generateWorkItemLink } from "@/helpers/issue.helper"; // hooks import { useIssueDetail, useProject, useProjectState } from "@/hooks/store"; import useIssuePeekOverviewRedirection from "@/hooks/use-issue-peek-overview-redirection"; diff --git a/web/core/components/issues/select/label.tsx b/web/core/components/issues/select/label.tsx index 149ca0f77..6c9b72a91 100644 --- a/web/core/components/issues/select/label.tsx +++ b/web/core/components/issues/select/label.tsx @@ -8,9 +8,9 @@ import { Combobox } from "@headlessui/react"; import { useOutsideClickDetector } from "@plane/hooks"; import { useTranslation } from "@plane/i18n"; // components +import { cn } from "@plane/utils"; import { IssueLabelsList } from "@/components/ui"; // helpers -import { cn } from "@/helpers/common.helper"; // hooks import { useLabel } from "@/hooks/store"; import { useDropdownKeyDown } from "@/hooks/use-dropdown-key-down"; diff --git a/web/core/components/issues/title-input.tsx b/web/core/components/issues/title-input.tsx index 463b81a79..bc04dbbb4 100644 --- a/web/core/components/issues/title-input.tsx +++ b/web/core/components/issues/title-input.tsx @@ -7,7 +7,7 @@ import { TNameDescriptionLoader } from "@plane/types"; // components import { TextArea } from "@plane/ui"; // types -import { cn } from "@/helpers/common.helper"; +import { cn } from "@plane/utils"; import useDebounce from "@/hooks/use-debounce"; import { TIssueOperations } from "./issue-detail"; // hooks diff --git a/web/core/components/issues/workspace-draft/draft-issue-block.tsx b/web/core/components/issues/workspace-draft/draft-issue-block.tsx index ab2639858..423231c6c 100644 --- a/web/core/components/issues/workspace-draft/draft-issue-block.tsx +++ b/web/core/components/issues/workspace-draft/draft-issue-block.tsx @@ -10,7 +10,7 @@ import { TWorkspaceDraftIssue } from "@plane/types"; import { Row, TContextMenuItem, Tooltip } from "@plane/ui"; // constants // helper -import { cn } from "@/helpers/common.helper"; +import { cn } from "@plane/utils"; // hooks import { useAppTheme, useProject, useWorkspaceDraftIssues } from "@/hooks/store"; // plane-web components diff --git a/web/core/components/issues/workspace-draft/draft-issue-properties.tsx b/web/core/components/issues/workspace-draft/draft-issue-properties.tsx index 92f61f8e0..4318bc2ea 100644 --- a/web/core/components/issues/workspace-draft/draft-issue-properties.tsx +++ b/web/core/components/issues/workspace-draft/draft-issue-properties.tsx @@ -7,6 +7,7 @@ import { useParams } from "next/navigation"; import { CalendarCheck2, CalendarClock } from "lucide-react"; // types import { TIssuePriorities, TWorkspaceDraftIssue } from "@plane/types"; +import { getDate, renderFormattedPayloadDate, shouldHighlightIssueDueDate } from "@plane/utils"; // components import { DateDropdown, @@ -18,8 +19,6 @@ import { StateDropdown, } from "@/components/dropdowns"; // helpers -import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper"; -import { shouldHighlightIssueDueDate } from "@/helpers/issue.helper"; // hooks import { useLabel, useProjectState, useProject, useProjectEstimates, useWorkspaceDraftIssues } from "@/hooks/store"; import { usePlatformOS } from "@/hooks/use-platform-os"; diff --git a/web/core/components/issues/workspace-draft/quick-action.tsx b/web/core/components/issues/workspace-draft/quick-action.tsx index 676c79ba6..2a52d83d6 100644 --- a/web/core/components/issues/workspace-draft/quick-action.tsx +++ b/web/core/components/issues/workspace-draft/quick-action.tsx @@ -5,7 +5,7 @@ import { useTranslation } from "@plane/i18n"; // ui import { ContextMenu, CustomMenu, TContextMenuItem } from "@plane/ui"; // helpers -import { cn } from "@/helpers/common.helper"; +import { cn } from "@plane/utils"; export interface Props { parentRef: React.RefObject; diff --git a/web/core/components/issues/workspace-draft/root.tsx b/web/core/components/issues/workspace-draft/root.tsx index 540839cd2..8f4577e80 100644 --- a/web/core/components/issues/workspace-draft/root.tsx +++ b/web/core/components/issues/workspace-draft/root.tsx @@ -8,11 +8,11 @@ import { EDraftIssuePaginationType } from "@plane/constants"; import { EUserPermissionsLevel, EUserWorkspaceRoles } from "@plane/constants/src/user"; import { useTranslation } from "@plane/i18n"; // components +import { cn } from "@plane/utils"; import { ComicBoxButton, DetailedEmptyState } from "@/components/empty-state"; // constants // helpers -import { cn } from "@/helpers/common.helper"; // hooks import { useCommandPalette, useProject, useUserPermissions, useWorkspaceDraftIssues } from "@/hooks/store"; import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; diff --git a/web/core/components/labels/create-label-modal.tsx b/web/core/components/labels/create-label-modal.tsx index 6fcbfb6a2..a186783b4 100644 --- a/web/core/components/labels/create-label-modal.tsx +++ b/web/core/components/labels/create-label-modal.tsx @@ -14,7 +14,7 @@ import type { IIssueLabel, IState } from "@plane/types"; // ui import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // helpers -import { getTabIndex } from "@/helpers/tab-indices.helper"; +import { getTabIndex } from "@plane/utils"; // hooks import { useLabel } from "@/hooks/store"; import { usePlatformOS } from "@/hooks/use-platform-os"; diff --git a/web/core/components/labels/label-block/label-item-block.tsx b/web/core/components/labels/label-block/label-item-block.tsx index 6e1d2d82e..d74fabc76 100644 --- a/web/core/components/labels/label-block/label-item-block.tsx +++ b/web/core/components/labels/label-block/label-item-block.tsx @@ -9,7 +9,7 @@ import { IIssueLabel } from "@plane/types"; // ui import { CustomMenu, DragHandle } from "@plane/ui"; // helpers -import { cn } from "@/helpers/common.helper"; +import { cn } from "@plane/utils"; // components import { LabelName } from "./label-name"; diff --git a/web/core/components/license/modal/card/free-plan.tsx b/web/core/components/license/modal/card/free-plan.tsx index 946742671..250ac4164 100644 --- a/web/core/components/license/modal/card/free-plan.tsx +++ b/web/core/components/license/modal/card/free-plan.tsx @@ -5,7 +5,7 @@ import { CircleX } from "lucide-react"; // plane constants import { FREE_PLAN_UPGRADE_FEATURES } from "@plane/constants"; // helpers -import { cn } from "@/helpers/common.helper"; +import { cn } from "@plane/utils"; type FreePlanCardProps = { isOnFreePlan: boolean; diff --git a/web/core/components/modules/analytics-sidebar/issue-progress.tsx b/web/core/components/modules/analytics-sidebar/issue-progress.tsx index 05950d285..81ee66a80 100644 --- a/web/core/components/modules/analytics-sidebar/issue-progress.tsx +++ b/web/core/components/modules/analytics-sidebar/issue-progress.tsx @@ -6,21 +6,19 @@ import { observer } from "mobx-react"; import { useSearchParams } from "next/navigation"; import { AlertCircle, ChevronUp, ChevronDown } from "lucide-react"; import { Disclosure, Transition } from "@headlessui/react"; -import { EIssueFilterType, EIssuesStoreType } from "@plane/constants"; +import { EIssueFilterType, EIssuesStoreType, EEstimateSystem } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { IIssueFilterOptions, TModulePlotType } from "@plane/types"; import { CustomSelect, Spinner } from "@plane/ui"; // components -import ProgressChart from "@/components/core/sidebar/progress-chart"; -import { ModuleProgressStats } from "@/components/modules"; // constants // helpers -import { getDate } from "@/helpers/date-time.helper"; +import { getDate } from "@plane/utils"; +import ProgressChart from "@/components/core/sidebar/progress-chart"; +import { ModuleProgressStats } from "@/components/modules"; // hooks import { useIssues, useModule, useProjectEstimates } from "@/hooks/store"; // plane web constants -import { EEstimateSystem } from "@/plane-web/constants/estimates"; - type TModuleAnalyticsProgress = { workspaceSlug: string; projectId: string; diff --git a/web/core/components/modules/analytics-sidebar/progress-stats.tsx b/web/core/components/modules/analytics-sidebar/progress-stats.tsx index 5c92786d7..b30811219 100644 --- a/web/core/components/modules/analytics-sidebar/progress-stats.tsx +++ b/web/core/components/modules/analytics-sidebar/progress-stats.tsx @@ -14,11 +14,10 @@ import { TStateGroups, } from "@plane/types"; import { Avatar, StateGroupIcon } from "@plane/ui"; +import { cn, getFileURL } from "@plane/utils"; // components import { SingleProgressStats } from "@/components/core"; // helpers -import { cn } from "@/helpers/common.helper"; -import { getFileURL } from "@/helpers/file.helper"; // hooks import { useProjectState } from "@/hooks/store"; import useLocalStorage from "@/hooks/use-local-storage"; diff --git a/web/core/components/modules/analytics-sidebar/root.tsx b/web/core/components/modules/analytics-sidebar/root.tsx index ce6068566..bbe1e0d14 100644 --- a/web/core/components/modules/analytics-sidebar/root.tsx +++ b/web/core/components/modules/analytics-sidebar/root.tsx @@ -6,21 +6,15 @@ import { useParams } from "next/navigation"; import { Controller, useForm } from "react-hook-form"; import { CalendarClock, ChevronDown, ChevronRight, Info, Plus, SquareUser, Users } from "lucide-react"; import { Disclosure, Transition } from "@headlessui/react"; +import { MODULE_STATUS, MODULE_LINK_CREATED, MODULE_LINK_DELETED, MODULE_LINK_UPDATED, MODULE_UPDATED, EUserPermissions, EUserPermissionsLevel, EEstimateSystem } from "@plane/constants"; // plane types -import { - MODULE_STATUS, - MODULE_LINK_CREATED, - MODULE_LINK_DELETED, - MODULE_LINK_UPDATED, - MODULE_UPDATED, - EUserPermissions, - EUserPermissionsLevel, -} from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { ILinkDetails, IModule, ModuleLink } from "@plane/types"; // plane ui import { Loader, LayersIcon, CustomSelect, ModuleStatusIcon, TOAST_TYPE, setToast, TextArea } from "@plane/ui"; // components +// helpers +import { getDate, renderFormattedPayloadDate } from "@plane/utils"; import { DateRangeDropdown, MemberDropdown } from "@/components/dropdowns"; import { ArchiveModuleModal, @@ -29,13 +23,9 @@ import { ModuleAnalyticsProgress, ModuleLinksList, } from "@/components/modules"; -// helpers -import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper"; // hooks import { useModule, useEventTracker, useProjectEstimates, useUserPermissions } from "@/hooks/store"; // plane web constants -import { EEstimateSystem } from "@/plane-web/constants/estimates"; - const defaultValues: Partial = { lead_id: "", member_ids: [], diff --git a/web/core/components/modules/applied-filters/date.tsx b/web/core/components/modules/applied-filters/date.tsx index 53c24a3a9..f3babd765 100644 --- a/web/core/components/modules/applied-filters/date.tsx +++ b/web/core/components/modules/applied-filters/date.tsx @@ -2,9 +2,8 @@ import { observer } from "mobx-react"; // icons import { X } from "lucide-react"; import { DATE_AFTER_FILTER_OPTIONS } from "@plane/constants"; +import { renderFormattedDate, capitalizeFirstLetter } from "@plane/utils"; // helpers -import { renderFormattedDate } from "@/helpers/date-time.helper"; -import { capitalizeFirstLetter } from "@/helpers/string.helper"; // constants type Props = { diff --git a/web/core/components/modules/applied-filters/members.tsx b/web/core/components/modules/applied-filters/members.tsx index ccb8c90c9..e1c8b6834 100644 --- a/web/core/components/modules/applied-filters/members.tsx +++ b/web/core/components/modules/applied-filters/members.tsx @@ -5,7 +5,7 @@ import { X } from "lucide-react"; // plane ui import { Avatar } from "@plane/ui"; // helpers -import { getFileURL } from "@/helpers/file.helper"; +import { getFileURL } from "@plane/utils"; // hooks import { useMember } from "@/hooks/store"; diff --git a/web/core/components/modules/applied-filters/root.tsx b/web/core/components/modules/applied-filters/root.tsx index f8b4fac08..2ba2765e2 100644 --- a/web/core/components/modules/applied-filters/root.tsx +++ b/web/core/components/modules/applied-filters/root.tsx @@ -3,9 +3,9 @@ import { useTranslation } from "@plane/i18n"; import { TModuleDisplayFilters, TModuleFilters } from "@plane/types"; // components import { Header, EHeaderVariant, Tag } from "@plane/ui"; +import { replaceUnderscoreIfSnakeCase } from "@plane/utils"; import { AppliedDateFilters, AppliedMembersFilters, AppliedStatusFilters } from "@/components/modules"; // helpers -import { replaceUnderscoreIfSnakeCase } from "@/helpers/string.helper"; // types type Props = { diff --git a/web/core/components/modules/archived-modules/header.tsx b/web/core/components/modules/archived-modules/header.tsx index 47687e2b4..c0bd2d930 100644 --- a/web/core/components/modules/archived-modules/header.tsx +++ b/web/core/components/modules/archived-modules/header.tsx @@ -7,13 +7,12 @@ import { ListFilter, Search, X } from "lucide-react"; import { useOutsideClickDetector } from "@plane/hooks"; // types import type { TModuleFilters } from "@plane/types"; +import { cn, calculateTotalFilters } from "@plane/utils"; // components import { ArchiveTabsList } from "@/components/archives"; import { FiltersDropdown } from "@/components/issues"; import { ModuleFiltersSelection, ModuleOrderByDropdown } from "@/components/modules"; // helpers -import { cn } from "@/helpers/common.helper"; -import { calculateTotalFilters } from "@/helpers/filter.helper"; // hooks import { useMember, useModuleFilter } from "@/hooks/store"; diff --git a/web/core/components/modules/archived-modules/root.tsx b/web/core/components/modules/archived-modules/root.tsx index 30ae74a5c..9bcb50c6e 100644 --- a/web/core/components/modules/archived-modules/root.tsx +++ b/web/core/components/modules/archived-modules/root.tsx @@ -6,11 +6,11 @@ import useSWR from "swr"; import { useTranslation } from "@plane/i18n"; import { TModuleFilters } from "@plane/types"; // components +import { calculateTotalFilters } from "@plane/utils"; import { DetailedEmptyState } from "@/components/empty-state"; import { ArchivedModulesView, ModuleAppliedFiltersList } from "@/components/modules"; import { CycleModuleListLayout } from "@/components/ui"; // helpers -import { calculateTotalFilters } from "@/helpers/filter.helper"; // hooks import { useModule, useModuleFilter } from "@/hooks/store"; import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; diff --git a/web/core/components/modules/dropdowns/filters/lead.tsx b/web/core/components/modules/dropdowns/filters/lead.tsx index 7b225dee9..a6de50b4e 100644 --- a/web/core/components/modules/dropdowns/filters/lead.tsx +++ b/web/core/components/modules/dropdowns/filters/lead.tsx @@ -6,9 +6,9 @@ import { observer } from "mobx-react"; // plane ui import { Avatar, Loader } from "@plane/ui"; // components +import { getFileURL } from "@plane/utils"; import { FilterHeader, FilterOption } from "@/components/issues"; // helpers -import { getFileURL } from "@/helpers/file.helper"; // hooks import { useMember, useUser } from "@/hooks/store"; diff --git a/web/core/components/modules/dropdowns/filters/members.tsx b/web/core/components/modules/dropdowns/filters/members.tsx index 752858547..1f593203a 100644 --- a/web/core/components/modules/dropdowns/filters/members.tsx +++ b/web/core/components/modules/dropdowns/filters/members.tsx @@ -6,9 +6,9 @@ import { observer } from "mobx-react"; // plane ui import { Avatar, Loader } from "@plane/ui"; // components +import { getFileURL } from "@plane/utils"; import { FilterHeader, FilterOption } from "@/components/issues"; // helpers -import { getFileURL } from "@/helpers/file.helper"; // hooks import { useMember, useUser } from "@/hooks/store"; diff --git a/web/core/components/modules/dropdowns/filters/start-date.tsx b/web/core/components/modules/dropdowns/filters/start-date.tsx index e43a7e50c..2d7b9c17f 100644 --- a/web/core/components/modules/dropdowns/filters/start-date.tsx +++ b/web/core/components/modules/dropdowns/filters/start-date.tsx @@ -3,11 +3,11 @@ import { observer } from "mobx-react"; // constants import { DATE_AFTER_FILTER_OPTIONS } from "@plane/constants"; // components +import { isInDateFormat } from "@plane/utils"; import { DateFilterModal } from "@/components/core"; import { FilterHeader, FilterOption } from "@/components/issues"; // helpers -import { isInDateFormat } from "@/helpers/date-time.helper"; type Props = { appliedFilters: string[] | null; diff --git a/web/core/components/modules/dropdowns/filters/target-date.tsx b/web/core/components/modules/dropdowns/filters/target-date.tsx index c7ed9c840..2bb0f84c2 100644 --- a/web/core/components/modules/dropdowns/filters/target-date.tsx +++ b/web/core/components/modules/dropdowns/filters/target-date.tsx @@ -3,10 +3,10 @@ import { observer } from "mobx-react"; // plane constants import { DATE_AFTER_FILTER_OPTIONS } from "@plane/constants"; // components +import { isInDateFormat } from "@plane/utils"; import { DateFilterModal } from "@/components/core"; import { FilterHeader, FilterOption } from "@/components/issues"; // helpers -import { isInDateFormat } from "@/helpers/date-time.helper"; type Props = { appliedFilters: string[] | null; diff --git a/web/core/components/modules/dropdowns/order-by.tsx b/web/core/components/modules/dropdowns/order-by.tsx index 6a373527c..4fe3b3c70 100644 --- a/web/core/components/modules/dropdowns/order-by.tsx +++ b/web/core/components/modules/dropdowns/order-by.tsx @@ -7,7 +7,7 @@ import { TModuleOrderByOptions } from "@plane/types"; // ui import { CustomMenu, getButtonStyling } from "@plane/ui"; // helpers -import { cn } from "@/helpers/common.helper"; +import { cn } from "@plane/utils"; // types // constants diff --git a/web/core/components/modules/form.tsx b/web/core/components/modules/form.tsx index 675850e7e..7a702c584 100644 --- a/web/core/components/modules/form.tsx +++ b/web/core/components/modules/form.tsx @@ -8,12 +8,10 @@ import { useTranslation } from "@plane/i18n"; import { IModule } from "@plane/types"; // ui import { Button, Input, TextArea } from "@plane/ui"; +import { getDate, renderFormattedPayloadDate, getTabIndex } from "@plane/utils"; // components import { DateRangeDropdown, ProjectDropdown, MemberDropdown } from "@/components/dropdowns"; import { ModuleStatusSelect } from "@/components/modules"; -// helpers -import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper"; -import { getTabIndex } from "@/helpers/tab-indices.helper"; // hooks import { useUser } from "@/hooks/store/user/user-user"; diff --git a/web/core/components/modules/gantt-chart/modules-list-layout.tsx b/web/core/components/modules/gantt-chart/modules-list-layout.tsx index 29d158557..c87f1c4d5 100644 --- a/web/core/components/modules/gantt-chart/modules-list-layout.tsx +++ b/web/core/components/modules/gantt-chart/modules-list-layout.tsx @@ -1,12 +1,10 @@ import { observer } from "mobx-react"; import { useParams } from "next/navigation"; // PLane -import { IModule } from "@plane/types"; +import { IBlockUpdateData, IBlockUpdateDependencyData, IModule } from "@plane/types"; // components import { GanttChartRoot, - IBlockUpdateData, - IBlockUpdateDependencyData, ModuleGanttSidebar, } from "@/components/gantt-chart"; import { ETimeLineTypeType, TimeLineTypeContext } from "@/components/gantt-chart/contexts"; @@ -19,7 +17,7 @@ export const ModulesListGanttChartView: React.FC = observer(() => { const { workspaceSlug, projectId } = useParams(); // store const { currentProjectDetails } = useProject(); - const { getFilteredModuleIds, updateModuleDetails, getModuleById } = useModule(); + const { getFilteredModuleIds, updateModuleDetails } = useModule(); const { currentProjectDisplayFilters: displayFilters } = useModuleFilter(); // derived values diff --git a/web/core/components/modules/links/list-item.tsx b/web/core/components/modules/links/list-item.tsx index e816525c9..283062cc7 100644 --- a/web/core/components/modules/links/list-item.tsx +++ b/web/core/components/modules/links/list-item.tsx @@ -4,10 +4,9 @@ import { Copy, Pencil, Trash2 } from "lucide-react"; import { ILinkDetails } from "@plane/types"; // plane ui import { setToast, TOAST_TYPE, Tooltip } from "@plane/ui"; -import { getIconForLink } from "@plane/utils"; +import { getIconForLink, copyTextToClipboard, calculateTimeAgo } from "@plane/utils"; // helpers -import { calculateTimeAgo } from "@/helpers/date-time.helper"; -import { copyTextToClipboard } from "@/helpers/string.helper"; +// // hooks import { useMember } from "@/hooks/store"; import { usePlatformOS } from "@/hooks/use-platform-os"; diff --git a/web/core/components/modules/module-card-item.tsx b/web/core/components/modules/module-card-item.tsx index 2204738cf..59b269c4a 100644 --- a/web/core/components/modules/module-card-item.tsx +++ b/web/core/components/modules/module-card-item.tsx @@ -27,14 +27,13 @@ import { setPromiseToast, setToast, } from "@plane/ui"; +import { getDate, renderFormattedPayloadDate, generateQueryParams } from "@plane/utils"; // components import { DateRangeDropdown } from "@/components/dropdowns"; import { ButtonAvatars } from "@/components/dropdowns/member/avatar"; import { ModuleQuickActions } from "@/components/modules"; import { ModuleStatusDropdown } from "@/components/modules/module-status-dropdown"; // helpers -import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper"; -import { generateQueryParams } from "@/helpers/router.helper"; // hooks import { useEventTracker, useMember, useModule, useUserPermissions } from "@/hooks/store"; import { useAppRouter } from "@/hooks/use-app-router"; diff --git a/web/core/components/modules/module-list-item-action.tsx b/web/core/components/modules/module-list-item-action.tsx index 8235ad8b2..fe96bdad0 100644 --- a/web/core/components/modules/module-list-item-action.tsx +++ b/web/core/components/modules/module-list-item-action.tsx @@ -20,12 +20,12 @@ import { IModule } from "@plane/types"; // ui import { FavoriteStar, TOAST_TYPE, Tooltip, setPromiseToast, setToast } from "@plane/ui"; // components +import { renderFormattedPayloadDate, getDate } from "@plane/utils"; import { DateRangeDropdown } from "@/components/dropdowns"; import { ModuleQuickActions } from "@/components/modules"; import { ModuleStatusDropdown } from "@/components/modules/module-status-dropdown"; // constants // hooks -import { renderFormattedPayloadDate, getDate } from "@/helpers/date-time.helper"; import { useEventTracker, useMember, useModule, useUserPermissions } from "@/hooks/store"; import { ButtonAvatars } from "../dropdowns/member/avatar"; diff --git a/web/core/components/modules/module-list-item.tsx b/web/core/components/modules/module-list-item.tsx index 64dfe7452..0d20bd224 100644 --- a/web/core/components/modules/module-list-item.tsx +++ b/web/core/components/modules/module-list-item.tsx @@ -8,10 +8,10 @@ import { Check, Info } from "lucide-react"; // ui import { CircularProgressIndicator } from "@plane/ui"; // components +import { generateQueryParams } from "@plane/utils"; import { ListItem } from "@/components/core/list"; import { ModuleListItemAction, ModuleQuickActions } from "@/components/modules"; // helpers -import { generateQueryParams } from "@/helpers/router.helper"; // hooks import { useModule } from "@/hooks/store"; import { useAppRouter } from "@/hooks/use-app-router"; diff --git a/web/core/components/modules/module-peek-overview.tsx b/web/core/components/modules/module-peek-overview.tsx index 02fb85747..590ef67e8 100644 --- a/web/core/components/modules/module-peek-overview.tsx +++ b/web/core/components/modules/module-peek-overview.tsx @@ -2,7 +2,7 @@ import React, { useEffect } from "react"; import { observer } from "mobx-react"; import { usePathname, useSearchParams } from "next/navigation"; // hooks -import { generateQueryParams } from "@/helpers/router.helper"; +import { generateQueryParams } from "@plane/utils"; import { useModule } from "@/hooks/store"; import { useAppRouter } from "@/hooks/use-app-router"; // components diff --git a/web/core/components/modules/module-view-header.tsx b/web/core/components/modules/module-view-header.tsx index 149545113..efa96c6c6 100644 --- a/web/core/components/modules/module-view-header.tsx +++ b/web/core/components/modules/module-view-header.tsx @@ -12,14 +12,13 @@ import { useTranslation } from "@plane/i18n"; import { TModuleFilters } from "@plane/types"; // ui import { Tooltip } from "@plane/ui"; +import { cn, calculateTotalFilters } from "@plane/utils"; // plane utils -import { cn } from "@plane/utils"; // components import { FiltersDropdown } from "@/components/issues"; import { ModuleFiltersSelection, ModuleOrderByDropdown } from "@/components/modules/dropdowns"; // constants // helpers -import { calculateTotalFilters } from "@/helpers/filter.helper"; // hooks import { useMember, useModuleFilter } from "@/hooks/store"; import { usePlatformOS } from "@/hooks/use-platform-os"; diff --git a/web/core/components/modules/quick-actions.tsx b/web/core/components/modules/quick-actions.tsx index 9945380af..8e61b0593 100644 --- a/web/core/components/modules/quick-actions.tsx +++ b/web/core/components/modules/quick-actions.tsx @@ -10,11 +10,10 @@ import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; // ui import { ArchiveIcon, ContextMenu, CustomMenu, TContextMenuItem, TOAST_TYPE, setToast } from "@plane/ui"; -import { copyUrlToClipboard } from "@plane/utils"; +import { copyUrlToClipboard, cn } from "@plane/utils"; // components import { ArchiveModuleModal, CreateUpdateModuleModal, DeleteModuleModal } from "@/components/modules"; // helpers -import { cn } from "@/helpers/common.helper"; // hooks import { useModule, useEventTracker, useUserPermissions } from "@/hooks/store"; import { useAppRouter } from "@/hooks/use-app-router"; diff --git a/web/core/components/onboarding/invitations.tsx b/web/core/components/onboarding/invitations.tsx index 1bfdd063c..0be9ae8db 100644 --- a/web/core/components/onboarding/invitations.tsx +++ b/web/core/components/onboarding/invitations.tsx @@ -7,11 +7,10 @@ import { ROLE, MEMBER_ACCEPTED } from "@plane/constants"; import { IWorkspaceMemberInvitation } from "@plane/types"; // ui import { Button, Checkbox, Spinner } from "@plane/ui"; +import { truncateText, getUserRole } from "@plane/utils"; // constants // helpers import { WorkspaceLogo } from "@/components/workspace/logo"; -import { truncateText } from "@/helpers/string.helper"; -import { getUserRole } from "@/helpers/user.helper"; // hooks import { useEventTracker, useUserSettings, useWorkspace } from "@/hooks/store"; // services diff --git a/web/core/components/onboarding/invite-members.tsx b/web/core/components/onboarding/invite-members.tsx index 34887be71..55408c42b 100644 --- a/web/core/components/onboarding/invite-members.tsx +++ b/web/core/components/onboarding/invite-members.tsx @@ -28,7 +28,7 @@ import { IUser, IWorkspace } from "@plane/types"; import { Button, Input, Spinner, TOAST_TYPE, setToast } from "@plane/ui"; // constants // helpers -import { getUserRole } from "@/helpers/user.helper"; +import { getUserRole } from "@plane/utils"; // hooks import { useEventTracker } from "@/hooks/store"; // services diff --git a/web/core/components/onboarding/profile-setup.tsx b/web/core/components/onboarding/profile-setup.tsx index 76cf98e91..4cbb02555 100644 --- a/web/core/components/onboarding/profile-setup.tsx +++ b/web/core/components/onboarding/profile-setup.tsx @@ -6,20 +6,19 @@ import Image from "next/image"; import { useTheme } from "next-themes"; import { Controller, useForm } from "react-hook-form"; import { Eye, EyeOff } from "lucide-react"; +import { USER_DETAILS, E_ONBOARDING_STEP_1, E_ONBOARDING_STEP_2, E_PASSWORD_STRENGTH } from "@plane/constants"; // types -import { USER_DETAILS, E_ONBOARDING_STEP_1, E_ONBOARDING_STEP_2 } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { IUser, TUserProfile, TOnboardingSteps } from "@plane/types"; // ui import { Button, Input, Spinner, TOAST_TYPE, setToast } from "@plane/ui"; // components +import { getFileURL, getPasswordStrength } from "@plane/utils"; import { PasswordStrengthMeter } from "@/components/account"; import { UserImageUploadModal } from "@/components/core"; import { OnboardingHeader, SwitchAccountDropdown } from "@/components/onboarding"; // constants // helpers -import { getFileURL } from "@/helpers/file.helper"; -import { E_PASSWORD_STRENGTH, getPasswordStrength } from "@/helpers/password.helper"; // hooks import { useEventTracker, useUser, useUserProfile } from "@/hooks/store"; // assets diff --git a/web/core/components/onboarding/step-indicator.tsx b/web/core/components/onboarding/step-indicator.tsx index c85485760..f10c3bc84 100644 --- a/web/core/components/onboarding/step-indicator.tsx +++ b/web/core/components/onboarding/step-indicator.tsx @@ -1,6 +1,6 @@ import React from "react"; // helpers -import { cn } from "@/helpers/common.helper"; +import { cn } from "@plane/utils"; interface OnboardingStepIndicatorProps { currentStep: number; diff --git a/web/core/components/onboarding/switch-account-dropdown.tsx b/web/core/components/onboarding/switch-account-dropdown.tsx index 7ccdb117f..4769966ff 100644 --- a/web/core/components/onboarding/switch-account-dropdown.tsx +++ b/web/core/components/onboarding/switch-account-dropdown.tsx @@ -6,9 +6,8 @@ import { ChevronDown } from "lucide-react"; import { Menu, Transition } from "@headlessui/react"; // ui import { Avatar } from "@plane/ui"; +import { cn, getFileURL } from "@plane/utils"; // helpers -import { cn } from "@/helpers/common.helper"; -import { getFileURL } from "@/helpers/file.helper"; // hooks import { useUser } from "@/hooks/store"; // components diff --git a/web/core/components/pages/dropdowns/actions.tsx b/web/core/components/pages/dropdowns/actions.tsx index b03713455..fbff3201f 100644 --- a/web/core/components/pages/dropdowns/actions.tsx +++ b/web/core/components/pages/dropdowns/actions.tsx @@ -22,9 +22,9 @@ import { EditorRefApi } from "@plane/editor"; // plane ui import { ArchiveIcon, ContextMenu, CustomMenu, TContextMenuItem } from "@plane/ui"; // components +import { cn } from "@plane/utils"; import { DeletePageModal } from "@/components/pages"; // helpers -import { cn } from "@/helpers/common.helper"; // hooks import { usePageOperations } from "@/hooks/use-page-operations"; // plane web components diff --git a/web/core/components/pages/editor/editor-body.tsx b/web/core/components/pages/editor/editor-body.tsx index 5f1948183..98193b560 100644 --- a/web/core/components/pages/editor/editor-body.tsx +++ b/web/core/components/pages/editor/editor-body.tsx @@ -1,6 +1,6 @@ import { Dispatch, SetStateAction, useCallback, useMemo } from "react"; import { observer } from "mobx-react"; -// plane imports +import { LIVE_BASE_PATH, LIVE_BASE_URL } from "@plane/constants"; import { CollaborativeDocumentEditorWithRef, EditorRefApi, @@ -10,15 +10,14 @@ import { TRealtimeConfig, TServerHandler, } from "@plane/editor"; +// plane imports import { TSearchEntityRequestPayload, TSearchResponse, TWebhookConnectionQueryParams } from "@plane/types"; import { ERowVariant, Row } from "@plane/ui"; -import { cn } from "@plane/utils"; +import { cn, generateRandomColor, hslToHex } from "@plane/utils"; // components import { EditorMentionsRoot } from "@/components/editor"; import { PageContentBrowser, PageContentLoader, PageEditorTitle } from "@/components/pages"; // helpers -import { LIVE_BASE_PATH, LIVE_BASE_URL } from "@/helpers/common.helper"; -import { generateRandomColor } from "@/helpers/string.helper"; // hooks import { useEditorMention } from "@/hooks/editor"; import { useUser, useWorkspace, useMember } from "@/hooks/store"; @@ -147,7 +146,7 @@ export const PageEditorBody: React.FC = observer((props) => { () => ({ id: currentUser?.id ?? "", name: currentUser?.display_name ?? "", - color: generateRandomColor(currentUser?.id ?? ""), + color: hslToHex(generateRandomColor(currentUser?.id ?? "")), }), [currentUser?.display_name, currentUser?.id] ); diff --git a/web/core/components/pages/editor/title.tsx b/web/core/components/pages/editor/title.tsx index 5864ac5d9..416c43e07 100644 --- a/web/core/components/pages/editor/title.tsx +++ b/web/core/components/pages/editor/title.tsx @@ -6,9 +6,8 @@ import { observer } from "mobx-react"; import { EditorRefApi } from "@plane/editor"; // ui import { TextArea } from "@plane/ui"; +import { cn, getPageName } from "@plane/utils"; // helpers -import { cn } from "@/helpers/common.helper"; -import { getPageName } from "@/helpers/page.helper"; // hooks import { usePageFilters } from "@/hooks/use-page-filters"; diff --git a/web/core/components/pages/editor/toolbar/color-dropdown.tsx b/web/core/components/pages/editor/toolbar/color-dropdown.tsx index 2809336c1..b0a3c658f 100644 --- a/web/core/components/pages/editor/toolbar/color-dropdown.tsx +++ b/web/core/components/pages/editor/toolbar/color-dropdown.tsx @@ -6,7 +6,7 @@ import { Popover } from "@headlessui/react"; // plane editor import { COLORS_LIST, TEditorCommands } from "@plane/editor"; // helpers -import { cn } from "@/helpers/common.helper"; +import { cn } from "@plane/utils"; type Props = { handleColorSelect: ( diff --git a/web/core/components/pages/editor/toolbar/info-popover.tsx b/web/core/components/pages/editor/toolbar/info-popover.tsx index d5a281459..49ad06b9b 100644 --- a/web/core/components/pages/editor/toolbar/info-popover.tsx +++ b/web/core/components/pages/editor/toolbar/info-popover.tsx @@ -6,12 +6,10 @@ import { usePopper } from "react-popper"; import { Info } from "lucide-react"; // plane imports import { Avatar } from "@plane/ui"; -import { getFileURL, renderFormattedDate } from "@plane/utils"; -// helpers -import { calculateTimeAgoShort, getReadTimeFromWordsCount } from "@/helpers/date-time.helper"; +import { calculateTimeAgoShort, getFileURL, getReadTimeFromWordsCount, renderFormattedDate } from "@plane/utils"; // hooks import { useMember } from "@/hooks/store"; -// store types +// store import { TPageInstance } from "@/store/pages/base-page"; type Props = { diff --git a/web/core/components/pages/editor/toolbar/options-dropdown.tsx b/web/core/components/pages/editor/toolbar/options-dropdown.tsx index 4dbdbf50a..407ee03c4 100644 --- a/web/core/components/pages/editor/toolbar/options-dropdown.tsx +++ b/web/core/components/pages/editor/toolbar/options-dropdown.tsx @@ -7,9 +7,9 @@ import { ArrowUpToLine, Clipboard, History } from "lucide-react"; // plane imports import { TContextMenuItem, TOAST_TYPE, ToggleSwitch, setToast } from "@plane/ui"; // components +import { copyTextToClipboard } from "@plane/utils"; import { ExportPageModal, PageActions, TPageActions } from "@/components/pages"; // helpers -import { copyTextToClipboard } from "@/helpers/string.helper"; // hooks import { usePageFilters } from "@/hooks/use-page-filters"; import { useQueryParams } from "@/hooks/use-query-params"; diff --git a/web/core/components/pages/editor/toolbar/root.tsx b/web/core/components/pages/editor/toolbar/root.tsx index 8ce0bd005..72c9da3d4 100644 --- a/web/core/components/pages/editor/toolbar/root.tsx +++ b/web/core/components/pages/editor/toolbar/root.tsx @@ -1,8 +1,8 @@ import { observer } from "mobx-react"; // components +import { cn } from "@plane/utils"; import { PageToolbar } from "@/components/pages"; // helpers -import { cn } from "@/helpers/common.helper"; // hooks import { usePageFilters } from "@/hooks/use-page-filters"; // plane web components diff --git a/web/core/components/pages/editor/toolbar/toolbar.tsx b/web/core/components/pages/editor/toolbar/toolbar.tsx index f6dd30691..ca85e6066 100644 --- a/web/core/components/pages/editor/toolbar/toolbar.tsx +++ b/web/core/components/pages/editor/toolbar/toolbar.tsx @@ -7,11 +7,11 @@ import { EditorRefApi } from "@plane/editor"; // ui import { CustomMenu, Tooltip } from "@plane/ui"; // components +import { cn } from "@plane/utils"; import { ColorDropdown } from "@/components/pages"; // constants import { TOOLBAR_ITEMS, TYPOGRAPHY_ITEMS, ToolbarMenuItem } from "@/constants/editor"; // helpers -import { cn } from "@/helpers/common.helper"; type Props = { editorRef: EditorRefApi; diff --git a/web/core/components/pages/header/root.tsx b/web/core/components/pages/header/root.tsx index a50fa9529..f546c804b 100644 --- a/web/core/components/pages/header/root.tsx +++ b/web/core/components/pages/header/root.tsx @@ -5,6 +5,7 @@ import { useTranslation } from "@plane/i18n"; import { TPageFilterProps, TPageNavigationTabs } from "@plane/types"; // components import { Header, EHeaderVariant } from "@plane/ui"; +import { calculateTotalFilters } from "@plane/utils"; import { FiltersDropdown } from "@/components/issues"; import { PageAppliedFiltersList, @@ -14,7 +15,6 @@ import { PageTabNavigation, } from "@/components/pages"; // helpers -import { calculateTotalFilters } from "@/helpers/filter.helper"; // hooks import { useMember } from "@/hooks/store"; // plane web hooks diff --git a/web/core/components/pages/list/applied-filters/root.tsx b/web/core/components/pages/list/applied-filters/root.tsx index df8063b28..99558163c 100644 --- a/web/core/components/pages/list/applied-filters/root.tsx +++ b/web/core/components/pages/list/applied-filters/root.tsx @@ -3,9 +3,9 @@ import { useTranslation } from "@plane/i18n"; import { TPageFilterProps } from "@plane/types"; // components import { Tag } from "@plane/ui"; +import { replaceUnderscoreIfSnakeCase } from "@plane/utils"; import { AppliedDateFilters, AppliedMembersFilters } from "@/components/common/applied-filters"; // helpers -import { replaceUnderscoreIfSnakeCase } from "@/helpers/string.helper"; // types type Props = { diff --git a/web/core/components/pages/list/block-item-action.tsx b/web/core/components/pages/list/block-item-action.tsx index 58e3a8e2a..4b382b488 100644 --- a/web/core/components/pages/list/block-item-action.tsx +++ b/web/core/components/pages/list/block-item-action.tsx @@ -5,11 +5,10 @@ import { observer } from "mobx-react"; import { Earth, Info, Lock, Minus } from "lucide-react"; // ui import { Avatar, FavoriteStar, Tooltip } from "@plane/ui"; +import { renderFormattedDate, getFileURL } from "@plane/utils"; // components import { PageActions } from "@/components/pages"; // helpers -import { renderFormattedDate } from "@/helpers/date-time.helper"; -import { getFileURL } from "@/helpers/file.helper"; // hooks import { useMember } from "@/hooks/store"; import { usePageOperations } from "@/hooks/use-page-operations"; diff --git a/web/core/components/pages/list/block.tsx b/web/core/components/pages/list/block.tsx index cd3ddbdae..6cf7471ba 100644 --- a/web/core/components/pages/list/block.tsx +++ b/web/core/components/pages/list/block.tsx @@ -4,11 +4,11 @@ import { FC, useRef } from "react"; import { observer } from "mobx-react"; import { FileText } from "lucide-react"; // components +import { getPageName } from "@plane/utils"; import { Logo } from "@/components/common"; import { ListItem } from "@/components/core/list"; import { BlockItemAction } from "@/components/pages/list"; // helpers -import { getPageName } from "@/helpers/page.helper"; // hooks import { usePlatformOS } from "@/hooks/use-platform-os"; // plane web hooks diff --git a/web/core/components/pages/list/order-by.tsx b/web/core/components/pages/list/order-by.tsx index 5385ee5ae..37f5827da 100644 --- a/web/core/components/pages/list/order-by.tsx +++ b/web/core/components/pages/list/order-by.tsx @@ -6,7 +6,7 @@ import { TPageFiltersSortBy, TPageFiltersSortKey } from "@plane/types"; // ui import { CustomMenu, getButtonStyling } from "@plane/ui"; // helpers -import { cn } from "@/helpers/common.helper"; +import { cn } from "@plane/utils"; type Props = { onChange: (value: { key?: TPageFiltersSortKey; order?: TPageFiltersSortBy }) => void; diff --git a/web/core/components/pages/list/search-input.tsx b/web/core/components/pages/list/search-input.tsx index 6b116a552..f7be11f76 100644 --- a/web/core/components/pages/list/search-input.tsx +++ b/web/core/components/pages/list/search-input.tsx @@ -3,7 +3,7 @@ import { Search, X } from "lucide-react"; // plane helpers import { useOutsideClickDetector } from "@plane/hooks"; // helpers -import { cn } from "@/helpers/common.helper"; +import { cn } from "@plane/utils"; type Props = { searchQuery: string; diff --git a/web/core/components/pages/list/tab-navigation.tsx b/web/core/components/pages/list/tab-navigation.tsx index fc28d50a2..35d6d1b94 100644 --- a/web/core/components/pages/list/tab-navigation.tsx +++ b/web/core/components/pages/list/tab-navigation.tsx @@ -3,7 +3,7 @@ import Link from "next/link"; // types import { TPageNavigationTabs } from "@plane/types"; // helpers -import { cn } from "@/helpers/common.helper"; +import { cn } from "@plane/utils"; type TPageTabNavigation = { workspaceSlug: string; diff --git a/web/core/components/pages/modals/page-form.tsx b/web/core/components/pages/modals/page-form.tsx index 4bc2a8428..32a2a8ad2 100644 --- a/web/core/components/pages/modals/page-form.tsx +++ b/web/core/components/pages/modals/page-form.tsx @@ -10,12 +10,11 @@ import { useTranslation } from "@plane/i18n"; import { TPage } from "@plane/types"; // ui import { Button, EmojiIconPicker, EmojiIconPickerTypes, Input } from "@plane/ui"; +import { convertHexEmojiToDecimal, getTabIndex } from "@plane/utils"; import { Logo } from "@/components/common"; // constants import { AccessField } from "@/components/common/access-field"; // helpers -import { convertHexEmojiToDecimal } from "@/helpers/emoji.helper"; -import { getTabIndex } from "@/helpers/tab-indices.helper"; // hooks import { usePlatformOS } from "@/hooks/use-platform-os"; diff --git a/web/core/components/pages/version/main-content.tsx b/web/core/components/pages/version/main-content.tsx index b36820fbb..e94bfefa7 100644 --- a/web/core/components/pages/version/main-content.tsx +++ b/web/core/components/pages/version/main-content.tsx @@ -7,9 +7,9 @@ import { TPageVersion } from "@plane/types"; // plane ui import { Button, setToast, TOAST_TYPE } from "@plane/ui"; // components +import { renderFormattedDate, renderFormattedTime } from "@plane/utils"; import { TVersionEditorProps } from "@/components/pages"; // helpers -import { renderFormattedDate, renderFormattedTime } from "@/helpers/date-time.helper"; type Props = { activeVersion: string | null; diff --git a/web/core/components/pages/version/root.tsx b/web/core/components/pages/version/root.tsx index 0717be012..f1dd0248b 100644 --- a/web/core/components/pages/version/root.tsx +++ b/web/core/components/pages/version/root.tsx @@ -2,9 +2,9 @@ import { observer } from "mobx-react"; // plane types import { TPageVersion } from "@plane/types"; // components +import { cn } from "@plane/utils"; import { PageVersionsMainContent, PageVersionsSidebarRoot, TVersionEditorProps } from "@/components/pages"; // helpers -import { cn } from "@/helpers/common.helper"; type Props = { activeVersion: string | null; diff --git a/web/core/components/pages/version/sidebar-list-item.tsx b/web/core/components/pages/version/sidebar-list-item.tsx index 5459bc166..c5df2c0d2 100644 --- a/web/core/components/pages/version/sidebar-list-item.tsx +++ b/web/core/components/pages/version/sidebar-list-item.tsx @@ -4,10 +4,8 @@ import Link from "next/link"; import { useTranslation } from "@plane/i18n"; import { TPageVersion } from "@plane/types"; import { Avatar } from "@plane/ui"; +import { cn, renderFormattedDate, renderFormattedTime, getFileURL } from "@plane/utils"; // helpers -import { cn } from "@/helpers/common.helper"; -import { renderFormattedDate, renderFormattedTime } from "@/helpers/date-time.helper"; -import { getFileURL } from "@/helpers/file.helper"; // hooks import { useMember } from "@/hooks/store"; diff --git a/web/core/components/pages/version/sidebar-list.tsx b/web/core/components/pages/version/sidebar-list.tsx index cf276742b..bff9c3698 100644 --- a/web/core/components/pages/version/sidebar-list.tsx +++ b/web/core/components/pages/version/sidebar-list.tsx @@ -7,9 +7,9 @@ import { TPageVersion } from "@plane/types"; // plane ui import { Button, Loader } from "@plane/ui"; // components +import { cn } from "@plane/utils"; import { PlaneVersionsSidebarListItem } from "@/components/pages"; // helpers -import { cn } from "@/helpers/common.helper"; // hooks import { useQueryParams } from "@/hooks/use-query-params"; diff --git a/web/core/components/profile/activity/activity-list.tsx b/web/core/components/profile/activity/activity-list.tsx index 671fd9dc2..5eb8e8b7d 100644 --- a/web/core/components/profile/activity/activity-list.tsx +++ b/web/core/components/profile/activity/activity-list.tsx @@ -4,6 +4,7 @@ import { useParams } from "next/navigation"; // icons import { History, MessageSquare } from "lucide-react"; import { IUserActivityResponse } from "@plane/types"; +import { calculateTimeAgo, getFileURL } from "@plane/utils"; // hooks // components import { ActivityIcon, ActivityMessage, IssueLink } from "@/components/core"; @@ -12,8 +13,6 @@ import { RichTextReadOnlyEditor } from "@/components/editor/rich-text-editor/ric // ui import { ActivitySettingsLoader } from "@/components/ui"; // helpers -import { calculateTimeAgo } from "@/helpers/date-time.helper"; -import { getFileURL } from "@/helpers/file.helper"; // hooks import { useUser, useWorkspace } from "@/hooks/store"; diff --git a/web/core/components/profile/activity/download-button.tsx b/web/core/components/profile/activity/download-button.tsx index 0092299ff..f8595cae8 100644 --- a/web/core/components/profile/activity/download-button.tsx +++ b/web/core/components/profile/activity/download-button.tsx @@ -7,7 +7,7 @@ import { useParams } from "next/navigation"; import { useTranslation } from "@plane/i18n"; import { Button } from "@plane/ui"; // helpers -import { renderFormattedPayloadDate } from "@/helpers/date-time.helper"; +import { renderFormattedPayloadDate } from "@plane/utils"; import { UserService } from "@/services/user.service"; const userService = new UserService(); diff --git a/web/core/components/profile/activity/profile-activity-list.tsx b/web/core/components/profile/activity/profile-activity-list.tsx index 36eca920f..a647d9571 100644 --- a/web/core/components/profile/activity/profile-activity-list.tsx +++ b/web/core/components/profile/activity/profile-activity-list.tsx @@ -4,6 +4,7 @@ import Link from "next/link"; import useSWR from "swr"; // icons import { History, MessageSquare } from "lucide-react"; +import { calculateTimeAgo, getFileURL } from "@plane/utils"; // hooks import { ActivityIcon, ActivityMessage, IssueLink } from "@/components/core"; import { RichTextReadOnlyEditor } from "@/components/editor/rich-text-editor/rich-text-read-only-editor"; @@ -11,8 +12,6 @@ import { ActivitySettingsLoader } from "@/components/ui"; // constants import { USER_ACTIVITY } from "@/constants/fetch-keys"; // helpers -import { calculateTimeAgo } from "@/helpers/date-time.helper"; -import { getFileURL } from "@/helpers/file.helper"; // hooks import { useUser } from "@/hooks/store"; // services diff --git a/web/core/components/profile/form.tsx b/web/core/components/profile/form.tsx index 46127f802..0cd08d75d 100644 --- a/web/core/components/profile/form.tsx +++ b/web/core/components/profile/form.tsx @@ -11,11 +11,10 @@ import type { IUser, TUserProfile } from "@plane/types"; import { Button, Input, TOAST_TYPE, setPromiseToast, setToast } from "@plane/ui"; // components import { getButtonStyling } from "@plane/ui/src/button"; -import { cn } from "@plane/utils"; +import { cn, getFileURL } from "@plane/utils"; import { DeactivateAccountModal } from "@/components/account"; import { ImagePickerPopover, UserImageUploadModal } from "@/components/core"; // helpers -import { getFileURL } from "@/helpers/file.helper"; // hooks import { useUser, useUserProfile } from "@/hooks/store"; diff --git a/web/core/components/profile/overview/activity.tsx b/web/core/components/profile/overview/activity.tsx index 9ac14b2aa..10313ef92 100644 --- a/web/core/components/profile/overview/activity.tsx +++ b/web/core/components/profile/overview/activity.tsx @@ -6,14 +6,13 @@ import useSWR from "swr"; // ui import { useTranslation } from "@plane/i18n"; import { Loader, Card } from "@plane/ui"; +import { calculateTimeAgo, getFileURL } from "@plane/utils"; // components import { ActivityMessage, IssueLink } from "@/components/core"; import { ProfileEmptyState } from "@/components/ui"; // constants import { USER_PROFILE_ACTIVITY } from "@/constants/fetch-keys"; // helpers -import { calculateTimeAgo } from "@/helpers/date-time.helper"; -import { getFileURL } from "@/helpers/file.helper"; // hooks import { useUser } from "@/hooks/store"; // assets diff --git a/web/core/components/profile/overview/priority-distribution.tsx b/web/core/components/profile/overview/priority-distribution.tsx index 575e1561b..a352deb0c 100644 --- a/web/core/components/profile/overview/priority-distribution.tsx +++ b/web/core/components/profile/overview/priority-distribution.tsx @@ -5,9 +5,9 @@ import { useTranslation } from "@plane/i18n"; import { BarChart } from "@plane/propel/charts/bar-chart"; import { IUserProfileData } from "@plane/types"; import { Loader, Card } from "@plane/ui"; +import { capitalizeFirstLetter } from "@plane/utils"; import { ProfileEmptyState } from "@/components/ui"; // image -import { capitalizeFirstLetter } from "@/helpers/string.helper"; import emptyBarGraph from "@/public/empty-state/empty_bar_graph.svg"; // helpers // types diff --git a/web/core/components/profile/overview/state-distribution.tsx b/web/core/components/profile/overview/state-distribution.tsx index 2d95f1463..ee07a7189 100644 --- a/web/core/components/profile/overview/state-distribution.tsx +++ b/web/core/components/profile/overview/state-distribution.tsx @@ -5,9 +5,9 @@ import { PieChart } from "@plane/propel/charts/pie-chart"; import { IUserProfileData, IUserStateDistribution } from "@plane/types"; // ui import { Card } from "@plane/ui"; +import { capitalizeFirstLetter } from "@plane/utils"; import { ProfileEmptyState } from "@/components/ui"; // helpers -import { capitalizeFirstLetter } from "@/helpers/string.helper"; // image import stateGraph from "@/public/empty-state/state_graph.svg"; diff --git a/web/core/components/profile/profile-issues-filter.tsx b/web/core/components/profile/profile-issues-filter.tsx index baf85c210..70a76de95 100644 --- a/web/core/components/profile/profile-issues-filter.tsx +++ b/web/core/components/profile/profile-issues-filter.tsx @@ -8,9 +8,9 @@ import { useTranslation } from "@plane/i18n"; // types import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types"; // components +import { isIssueFilterActive } from "@plane/utils"; import { DisplayFiltersSelection, FilterSelection, FiltersDropdown, LayoutSelection } from "@/components/issues"; // helpers -import { isIssueFilterActive } from "@/helpers/filter.helper"; // hooks import { useIssues, useLabel } from "@/hooks/store"; diff --git a/web/core/components/profile/profile-setting-content-wrapper.tsx b/web/core/components/profile/profile-setting-content-wrapper.tsx index c9741ca5d..85c580399 100644 --- a/web/core/components/profile/profile-setting-content-wrapper.tsx +++ b/web/core/components/profile/profile-setting-content-wrapper.tsx @@ -1,7 +1,7 @@ "use client"; import React, { FC } from "react"; // helpers -import { cn } from "@/helpers/common.helper"; +import { cn } from "@plane/utils"; import { SidebarHamburgerToggle } from "../core"; type Props = { diff --git a/web/core/components/profile/sidebar.tsx b/web/core/components/profile/sidebar.tsx index 0fd78782b..1508fd2e9 100644 --- a/web/core/components/profile/sidebar.tsx +++ b/web/core/components/profile/sidebar.tsx @@ -15,12 +15,10 @@ import { useTranslation } from "@plane/i18n"; import { IUserProfileProjectSegregation } from "@plane/types"; // plane ui import { Loader, Tooltip } from "@plane/ui"; +import { cn, renderFormattedDate, getFileURL } from "@plane/utils"; // components import { Logo } from "@/components/common"; // helpers -import { cn } from "@/helpers/common.helper"; -import { renderFormattedDate } from "@/helpers/date-time.helper"; -import { getFileURL } from "@/helpers/file.helper"; // hooks import { useAppTheme, useProject, useUser } from "@/hooks/store"; import { usePlatformOS } from "@/hooks/use-platform-os"; diff --git a/web/core/components/project-states/state-item.tsx b/web/core/components/project-states/state-item.tsx index 192211da3..b8359c208 100644 --- a/web/core/components/project-states/state-item.tsx +++ b/web/core/components/project-states/state-item.tsx @@ -9,12 +9,10 @@ import { observer } from "mobx-react"; import { TDraggableData } from "@plane/constants"; import { IState, TStateGroups, TStateOperationsCallbacks } from "@plane/types"; import { DropIndicator } from "@plane/ui"; +import { cn, getCurrentStateSequence } from "@plane/utils"; // components import { StateItemTitle, StateUpdate } from "@/components/project-states"; // helpers -import { cn } from "@/helpers/common.helper"; -import { getCurrentStateSequence } from "@/helpers/state.helper"; - type TStateItem = { groupKey: TStateGroups; groupedStates: Record; diff --git a/web/core/components/project/applied-filters/date.tsx b/web/core/components/project/applied-filters/date.tsx index d5879989f..27ac20613 100644 --- a/web/core/components/project/applied-filters/date.tsx +++ b/web/core/components/project/applied-filters/date.tsx @@ -2,8 +2,7 @@ import { observer } from "mobx-react"; import { X } from "lucide-react"; // helpers import { PROJECT_CREATED_AT_FILTER_OPTIONS } from "@plane/constants"; -import { renderFormattedDate } from "@/helpers/date-time.helper"; -import { capitalizeFirstLetter } from "@/helpers/string.helper"; +import { renderFormattedDate, capitalizeFirstLetter } from "@plane/utils"; // constants type Props = { diff --git a/web/core/components/project/applied-filters/members.tsx b/web/core/components/project/applied-filters/members.tsx index a8d2e480d..f0e4519ca 100644 --- a/web/core/components/project/applied-filters/members.tsx +++ b/web/core/components/project/applied-filters/members.tsx @@ -5,7 +5,7 @@ import { X } from "lucide-react"; // ui import { Avatar } from "@plane/ui"; // helpers -import { getFileURL } from "@/helpers/file.helper"; +import { getFileURL } from "@plane/utils"; // types import { useMember } from "@/hooks/store"; diff --git a/web/core/components/project/applied-filters/root.tsx b/web/core/components/project/applied-filters/root.tsx index 2069aa28b..c2489054d 100644 --- a/web/core/components/project/applied-filters/root.tsx +++ b/web/core/components/project/applied-filters/root.tsx @@ -8,6 +8,7 @@ import { TProjectAppliedDisplayFilterKeys, TProjectFilters } from "@plane/types" // ui import { EHeaderVariant, Header, Tag, Tooltip } from "@plane/ui"; // components +import { replaceUnderscoreIfSnakeCase } from "@plane/utils"; import { AppliedAccessFilters, AppliedDateFilters, @@ -15,7 +16,6 @@ import { AppliedProjectDisplayFilters, } from "@/components/project"; // helpers -import { replaceUnderscoreIfSnakeCase } from "@/helpers/string.helper"; type Props = { appliedFilters: TProjectFilters; diff --git a/web/core/components/project/card.tsx b/web/core/components/project/card.tsx index 48ff34d24..a90838720 100644 --- a/web/core/components/project/card.tsx +++ b/web/core/components/project/card.tsx @@ -5,11 +5,10 @@ import { observer } from "mobx-react"; import Link from "next/link"; import { useParams } from "next/navigation"; import { ArchiveRestoreIcon, Check, ExternalLink, LinkIcon, Lock, Settings, Trash2, UserPlus } from "lucide-react"; -// types +// plane imports import { EUserPermissions, EUserPermissionsLevel, IS_FAVORITE_MENU_OPEN } from "@plane/constants"; import { useLocalStorage } from "@plane/hooks"; import type { IProject } from "@plane/types"; -// ui import { Avatar, AvatarGroup, @@ -22,19 +21,14 @@ import { TContextMenuItem, FavoriteStar, } from "@plane/ui"; -import { copyUrlToClipboard } from "@plane/utils"; +import { copyUrlToClipboard, cn, getFileURL, renderFormattedDate } from "@plane/utils"; // components -import { Logo } from "@/components/common"; +import { Logo } from "@/components/common/logo"; import { ArchiveRestoreProjectModal, DeleteProjectModal, JoinProjectModal } from "@/components/project"; -// helpers -import { cn } from "@/helpers/common.helper"; -import { renderFormattedDate } from "@/helpers/date-time.helper"; -import { getFileURL } from "@/helpers/file.helper"; // hooks import { useMember, useProject, useUserPermissions } from "@/hooks/store"; import { useAppRouter } from "@/hooks/use-app-router"; import { usePlatformOS } from "@/hooks/use-platform-os"; -// plane-web constants type Props = { project: IProject; diff --git a/web/core/components/project/create-project-modal.tsx b/web/core/components/project/create-project-modal.tsx index 2360f458e..716f3ef4e 100644 --- a/web/core/components/project/create-project-modal.tsx +++ b/web/core/components/project/create-project-modal.tsx @@ -1,9 +1,8 @@ import { useEffect, FC, useState } from "react"; -// plane ui import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui"; +import { getAssetIdFromUrl, checkURLValidity } from "@plane/utils"; +// plane ui // helpers -import { getAssetIdFromUrl } from "@/helpers/file.helper"; -import { checkURLValidity } from "@/helpers/string.helper"; // hooks import useKeypress from "@/hooks/use-keypress"; // plane web components diff --git a/web/core/components/project/create/common-attributes.tsx b/web/core/components/project/create/common-attributes.tsx index 97807e09c..2586f8be6 100644 --- a/web/core/components/project/create/common-attributes.tsx +++ b/web/core/components/project/create/common-attributes.tsx @@ -6,11 +6,9 @@ import { ETabIndices } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; // ui import { Input, TextArea, Tooltip } from "@plane/ui"; +import { cn, projectIdentifierSanitizer, getTabIndex } from "@plane/utils"; // plane utils -import { cn } from "@plane/utils"; // helpers -import { projectIdentifierSanitizer } from "@/helpers/project.helper"; -import { getTabIndex } from "@/helpers/tab-indices.helper"; // plane-web types import { TProject } from "@/plane-web/types/projects"; diff --git a/web/core/components/project/create/header.tsx b/web/core/components/project/create/header.tsx index 6bc1e8e1a..2c393aff1 100644 --- a/web/core/components/project/create/header.tsx +++ b/web/core/components/project/create/header.tsx @@ -8,12 +8,10 @@ import { useTranslation } from "@plane/i18n"; import { IProject } from "@plane/types"; // plane ui import { CustomEmojiIconPicker, EmojiIconPickerTypes, Logo } from "@plane/ui"; +import { convertHexEmojiToDecimal, getFileURL, getTabIndex } from "@plane/utils"; // components import { ImagePickerPopover } from "@/components/core"; // helpers -import { convertHexEmojiToDecimal } from "@/helpers/emoji.helper"; -import { getFileURL } from "@/helpers/file.helper"; -import { getTabIndex } from "@/helpers/tab-indices.helper"; // plane web imports import { ProjectTemplateSelect } from "@/plane-web/components/projects/create/template-select"; diff --git a/web/core/components/project/create/project-create-buttons.tsx b/web/core/components/project/create/project-create-buttons.tsx index 98a6e80f9..e30370757 100644 --- a/web/core/components/project/create/project-create-buttons.tsx +++ b/web/core/components/project/create/project-create-buttons.tsx @@ -6,7 +6,7 @@ import { IProject } from "@plane/types"; // ui import { Button } from "@plane/ui"; // helpers -import { getTabIndex } from "@/helpers/tab-indices.helper"; +import { getTabIndex } from "@plane/utils"; type Props = { handleClose: () => void; diff --git a/web/core/components/project/dropdowns/filters/created-at.tsx b/web/core/components/project/dropdowns/filters/created-at.tsx index 87d45a2a5..6b223f259 100644 --- a/web/core/components/project/dropdowns/filters/created-at.tsx +++ b/web/core/components/project/dropdowns/filters/created-at.tsx @@ -3,11 +3,11 @@ import { observer } from "mobx-react"; // plane constants import { PROJECT_CREATED_AT_FILTER_OPTIONS } from "@plane/constants"; // components +import { isInDateFormat } from "@plane/utils"; import { DateFilterModal } from "@/components/core"; import { FilterHeader, FilterOption } from "@/components/issues"; // helpers -import { isInDateFormat } from "@/helpers/date-time.helper"; type Props = { appliedFilters: string[] | null; diff --git a/web/core/components/project/dropdowns/filters/lead.tsx b/web/core/components/project/dropdowns/filters/lead.tsx index 7b225dee9..a6de50b4e 100644 --- a/web/core/components/project/dropdowns/filters/lead.tsx +++ b/web/core/components/project/dropdowns/filters/lead.tsx @@ -6,9 +6,9 @@ import { observer } from "mobx-react"; // plane ui import { Avatar, Loader } from "@plane/ui"; // components +import { getFileURL } from "@plane/utils"; import { FilterHeader, FilterOption } from "@/components/issues"; // helpers -import { getFileURL } from "@/helpers/file.helper"; // hooks import { useMember, useUser } from "@/hooks/store"; diff --git a/web/core/components/project/dropdowns/filters/members.tsx b/web/core/components/project/dropdowns/filters/members.tsx index 752858547..1f593203a 100644 --- a/web/core/components/project/dropdowns/filters/members.tsx +++ b/web/core/components/project/dropdowns/filters/members.tsx @@ -6,9 +6,9 @@ import { observer } from "mobx-react"; // plane ui import { Avatar, Loader } from "@plane/ui"; // components +import { getFileURL } from "@plane/utils"; import { FilterHeader, FilterOption } from "@/components/issues"; // helpers -import { getFileURL } from "@/helpers/file.helper"; // hooks import { useMember, useUser } from "@/hooks/store"; diff --git a/web/core/components/project/dropdowns/order-by.tsx b/web/core/components/project/dropdowns/order-by.tsx index b9665b61a..e75e256f0 100644 --- a/web/core/components/project/dropdowns/order-by.tsx +++ b/web/core/components/project/dropdowns/order-by.tsx @@ -7,7 +7,7 @@ import { TProjectOrderByOptions } from "@plane/types"; // ui import { CustomMenu, getButtonStyling } from "@plane/ui"; // helpers -import { cn } from "@/helpers/common.helper"; +import { cn } from "@plane/utils"; // types // constants diff --git a/web/core/components/project/filters.tsx b/web/core/components/project/filters.tsx index 54ae3a752..5a97acde0 100644 --- a/web/core/components/project/filters.tsx +++ b/web/core/components/project/filters.tsx @@ -6,13 +6,12 @@ import { ListFilter } from "lucide-react"; import { useTranslation } from "@plane/i18n"; // plane types import { TProjectFilters } from "@plane/types"; +import { cn, calculateTotalFilters } from "@plane/utils"; // plane utils -import { cn } from "@plane/utils"; // components import { FiltersDropdown } from "@/components/issues"; import { ProjectFiltersSelection, ProjectOrderByDropdown } from "@/components/project"; // helpers -import { calculateTotalFilters } from "@/helpers/filter.helper"; // hooks import { useMember, useProjectFilter } from "@/hooks/store"; diff --git a/web/core/components/project/form.tsx b/web/core/components/project/form.tsx index f87f8ba84..f5b6320c7 100644 --- a/web/core/components/project/form.tsx +++ b/web/core/components/project/form.tsx @@ -19,15 +19,13 @@ import { EmojiIconPickerTypes, Tooltip, } from "@plane/ui"; +import { renderFormattedDate, convertHexEmojiToDecimal, getFileURL } from "@plane/utils"; // components import { Logo } from "@/components/common"; import { ImagePickerPopover } from "@/components/core"; import { TimezoneSelect } from "@/components/global"; import { ProjectNetworkIcon } from "@/components/project"; // helpers -import { renderFormattedDate } from "@/helpers/date-time.helper"; -import { convertHexEmojiToDecimal } from "@/helpers/emoji.helper"; -import { getFileURL } from "@/helpers/file.helper"; // hooks import { useEventTracker, useProject } from "@/hooks/store"; import { usePlatformOS } from "@/hooks/use-platform-os"; diff --git a/web/core/components/project/member-select.tsx b/web/core/components/project/member-select.tsx index 44cc6382e..15aaee224 100644 --- a/web/core/components/project/member-select.tsx +++ b/web/core/components/project/member-select.tsx @@ -8,7 +8,7 @@ import { Ban } from "lucide-react"; import { EUserProjectRoles } from "@plane/constants"; import { Avatar, CustomSearchSelect } from "@plane/ui"; // helpers -import { getFileURL } from "@/helpers/file.helper"; +import { getFileURL } from "@plane/utils"; // hooks import { useMember } from "@/hooks/store"; diff --git a/web/core/components/project/multi-select-modal.tsx b/web/core/components/project/multi-select-modal.tsx index ade55675d..0ab59bcce 100644 --- a/web/core/components/project/multi-select-modal.tsx +++ b/web/core/components/project/multi-select-modal.tsx @@ -7,10 +7,10 @@ import { Combobox } from "@headlessui/react"; import { useTranslation } from "@plane/i18n"; import { Button, Checkbox, EModalPosition, EModalWidth, ModalCore } from "@plane/ui"; // components +import { cn } from "@plane/utils"; import { Logo } from "@/components/common"; import { SimpleEmptyState } from "@/components/empty-state"; // helpers -import { cn } from "@/helpers/common.helper"; // hooks import { useProject } from "@/hooks/store"; import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; diff --git a/web/core/components/project/publish-project/modal.tsx b/web/core/components/project/publish-project/modal.tsx index 5496e6993..a980b2eac 100644 --- a/web/core/components/project/publish-project/modal.tsx +++ b/web/core/components/project/publish-project/modal.tsx @@ -6,12 +6,12 @@ import { useParams } from "next/navigation"; import { Controller, useForm } from "react-hook-form"; import { Check, ExternalLink, Globe2 } from "lucide-react"; // types +import { SPACE_BASE_PATH, SPACE_BASE_URL } from "@plane/constants"; import { IProject, TProjectPublishLayouts, TProjectPublishSettings } from "@plane/types"; // ui import { Button, Loader, ToggleSwitch, TOAST_TYPE, setToast, CustomSelect, ModalCore, EModalWidth } from "@plane/ui"; // helpers -import { SPACE_BASE_PATH, SPACE_BASE_URL } from "@/helpers/common.helper"; -import { copyTextToClipboard } from "@/helpers/string.helper"; +import { copyTextToClipboard } from "@plane/utils"; // hooks import { useProjectPublish } from "@/hooks/store"; diff --git a/web/core/components/project/root.tsx b/web/core/components/project/root.tsx index 9475563fd..e0be0292a 100644 --- a/web/core/components/project/root.tsx +++ b/web/core/components/project/root.tsx @@ -7,10 +7,10 @@ import { useParams, usePathname } from "next/navigation"; import { useTranslation } from "@plane/i18n"; import { TProjectAppliedDisplayFilterKeys, TProjectFilters } from "@plane/types"; // components -import { PageHead } from "@/components/core"; +import { calculateTotalFilters } from "@plane/utils"; +import { PageHead } from "@/components/core/page-title"; import { ProjectAppliedFiltersList, ProjectCardList } from "@/components/project"; // helpers -import { calculateTotalFilters } from "@/helpers/filter.helper"; // hooks import { useProject, useProjectFilter, useWorkspace } from "@/hooks/store"; diff --git a/web/core/components/project/search-projects.tsx b/web/core/components/project/search-projects.tsx index 412e979e9..c7d41280f 100644 --- a/web/core/components/project/search-projects.tsx +++ b/web/core/components/project/search-projects.tsx @@ -8,7 +8,7 @@ import { useOutsideClickDetector } from "@plane/hooks"; // i18n import { useTranslation } from "@plane/i18n"; // helpers -import { cn } from "@/helpers/common.helper"; +import { cn } from "@plane/utils"; // hooks import { useProjectFilter } from "@/hooks/store"; diff --git a/web/core/components/project/send-project-invitation-modal.tsx b/web/core/components/project/send-project-invitation-modal.tsx index 9b0a0882f..9906a40a5 100644 --- a/web/core/components/project/send-project-invitation-modal.tsx +++ b/web/core/components/project/send-project-invitation-modal.tsx @@ -10,7 +10,7 @@ import { ROLE, PROJECT_MEMBER_ADDED, EUserPermissions } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { Avatar, Button, CustomSelect, CustomSearchSelect, TOAST_TYPE, setToast } from "@plane/ui"; // helpers -import { getFileURL } from "@/helpers/file.helper"; +import { getFileURL } from "@plane/utils"; // hooks import { useEventTracker, useMember, useUserPermissions } from "@/hooks/store"; diff --git a/web/core/components/settings/project/sidebar/nav-item-children.tsx b/web/core/components/settings/project/sidebar/nav-item-children.tsx index 514394844..4cf4c6b99 100644 --- a/web/core/components/settings/project/sidebar/nav-item-children.tsx +++ b/web/core/components/settings/project/sidebar/nav-item-children.tsx @@ -5,7 +5,7 @@ import { usePathname, useParams } from "next/navigation"; import { EUserPermissionsLevel } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { Loader } from "@plane/ui"; -import { cn } from "@/helpers/common.helper"; +import { cn } from "@plane/utils"; import { useProject, useUserPermissions, useUserSettings } from "@/hooks/store"; import { PROJECT_SETTINGS_LINKS } from "@/plane-web/constants/project"; import { getProjectSettingsPageLabelI18nKey } from "@/plane-web/helpers/project-settings"; diff --git a/web/core/components/settings/project/sidebar/root.tsx b/web/core/components/settings/project/sidebar/root.tsx index a7a5b0b8d..de3d0c4e4 100644 --- a/web/core/components/settings/project/sidebar/root.tsx +++ b/web/core/components/settings/project/sidebar/root.tsx @@ -1,10 +1,13 @@ import { observer } from "mobx-react"; import { useParams } from "next/navigation"; +// plane imports import { PROJECT_SETTINGS_CATEGORIES, PROJECT_SETTINGS_CATEGORY } from "@plane/constants"; +import { getUserRole } from "@plane/utils"; +// components import { Logo } from "@/components/common"; - -import { getUserRole } from "@/helpers/user.helper"; +// hooks import { useProject } from "@/hooks/store/use-project"; +// local imports import { SettingsSidebar } from "../.."; import { NavItemChildren } from "./nav-item-children"; diff --git a/web/core/components/settings/sidebar/header.tsx b/web/core/components/settings/sidebar/header.tsx index b5b17fb0d..56fd49ffa 100644 --- a/web/core/components/settings/sidebar/header.tsx +++ b/web/core/components/settings/sidebar/header.tsx @@ -1,7 +1,11 @@ import { observer } from "mobx-react"; +// plane imports +import { getUserRole } from "@plane/utils"; +// components import { WorkspaceLogo } from "@/components/workspace"; -import { getUserRole } from "@/helpers/user.helper"; +// hooks import { useWorkspace } from "@/hooks/store/use-workspace"; +// plane web imports import { SubscriptionPill } from "@/plane-web/components/common"; export const SettingsSidebarHeader = observer((props: { customHeader?: React.ReactNode }) => { diff --git a/web/core/components/settings/sidebar/nav-item.tsx b/web/core/components/settings/sidebar/nav-item.tsx index d06930ef5..53ebcc97a 100644 --- a/web/core/components/settings/sidebar/nav-item.tsx +++ b/web/core/components/settings/sidebar/nav-item.tsx @@ -3,9 +3,11 @@ import { observer } from "mobx-react"; import Link from "next/link"; import { useParams } from "next/navigation"; import { Disclosure } from "@headlessui/react"; +// plane imports import { EUserWorkspaceRoles } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; -import { cn } from "@/helpers/common.helper"; +import { cn } from "@plane/utils"; +// hooks import { useUserSettings } from "@/hooks/store"; export type TSettingItem = { diff --git a/web/core/components/sidebar/sidebar-navigation.tsx b/web/core/components/sidebar/sidebar-navigation.tsx index f31a93c9c..0e52c6ef9 100644 --- a/web/core/components/sidebar/sidebar-navigation.tsx +++ b/web/core/components/sidebar/sidebar-navigation.tsx @@ -1,7 +1,7 @@ "use client"; import React, { FC } from "react"; // helpers -import { cn } from "@/helpers/common.helper"; +import { cn } from "@plane/utils"; type TSidebarNavItem = { className?: string; diff --git a/web/core/components/stickies/modal/search.tsx b/web/core/components/stickies/modal/search.tsx index 22499c03a..8cdee6cc4 100644 --- a/web/core/components/stickies/modal/search.tsx +++ b/web/core/components/stickies/modal/search.tsx @@ -9,7 +9,7 @@ import { Search, X } from "lucide-react"; import { useOutsideClickDetector } from "@plane/hooks"; // helpers import { useTranslation } from "@plane/i18n"; -import { cn } from "@/helpers/common.helper"; +import { cn } from "@plane/utils"; import { useSticky } from "@/hooks/use-stickies"; export const StickySearch: FC = observer(() => { diff --git a/web/core/components/stickies/sticky/sticky-item-drag-handle.tsx b/web/core/components/stickies/sticky/sticky-item-drag-handle.tsx index e46c981f6..2269a9db4 100644 --- a/web/core/components/stickies/sticky/sticky-item-drag-handle.tsx +++ b/web/core/components/stickies/sticky/sticky-item-drag-handle.tsx @@ -4,7 +4,7 @@ import { observer } from "mobx-react"; // ui import { DragHandle } from "@plane/ui"; // helper -import { cn } from "@/helpers/common.helper"; +import { cn } from "@plane/utils"; type Props = { isDragging: boolean; diff --git a/web/core/components/views/applied-filters/root.tsx b/web/core/components/views/applied-filters/root.tsx index 8a881d2d0..bb654338e 100644 --- a/web/core/components/views/applied-filters/root.tsx +++ b/web/core/components/views/applied-filters/root.tsx @@ -4,10 +4,10 @@ import { useTranslation } from "@plane/i18n"; import { TViewFilterProps } from "@plane/types"; // components import { Tag } from "@plane/ui"; +import { replaceUnderscoreIfSnakeCase } from "@plane/utils"; import { AppliedDateFilters, AppliedMembersFilters } from "@/components/common/applied-filters"; // constants // helpers -import { replaceUnderscoreIfSnakeCase } from "@/helpers/string.helper"; import { AppliedAccessFilters } from "./access"; // types diff --git a/web/core/components/views/form.tsx b/web/core/components/views/form.tsx index 3e1d0fba3..2b5e6ab9f 100644 --- a/web/core/components/views/form.tsx +++ b/web/core/components/views/form.tsx @@ -12,13 +12,11 @@ import { useTranslation } from "@plane/i18n"; import { IProjectView, IIssueFilterOptions, IIssueDisplayProperties, IIssueDisplayFilterOptions } from "@plane/types"; // ui import { Button, EmojiIconPicker, EmojiIconPickerTypes, Input, TextArea } from "@plane/ui"; +import { convertHexEmojiToDecimal, getComputedDisplayFilters, getComputedDisplayProperties, getTabIndex } from "@plane/utils"; // components import { Logo } from "@/components/common"; import { AppliedFiltersList, DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "@/components/issues"; // helpers -import { convertHexEmojiToDecimal } from "@/helpers/emoji.helper"; -import { getComputedDisplayFilters, getComputedDisplayProperties } from "@/helpers/issue.helper"; -import { getTabIndex } from "@/helpers/tab-indices.helper"; // hooks import { useLabel, useMember, useProject, useProjectState } from "@/hooks/store"; import { usePlatformOS } from "@/hooks/use-platform-os"; diff --git a/web/core/components/views/quick-actions.tsx b/web/core/components/views/quick-actions.tsx index d80624998..27b2e73ea 100644 --- a/web/core/components/views/quick-actions.tsx +++ b/web/core/components/views/quick-actions.tsx @@ -8,11 +8,10 @@ import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; import { IProjectView } from "@plane/types"; // ui import { ContextMenu, CustomMenu, TContextMenuItem, TOAST_TYPE, setToast } from "@plane/ui"; -import { copyUrlToClipboard } from "@plane/utils"; +import { copyUrlToClipboard, cn } from "@plane/utils"; // components import { CreateUpdateProjectViewModal, DeleteProjectViewModal } from "@/components/views"; // helpers -import { cn } from "@/helpers/common.helper"; // hooks import { useUser, useUserPermissions } from "@/hooks/store"; import { PublishViewModal, useViewPublish } from "@/plane-web/components/views/publish"; diff --git a/web/core/components/views/view-list-header.tsx b/web/core/components/views/view-list-header.tsx index 8c66e1a68..e28aa37d8 100644 --- a/web/core/components/views/view-list-header.tsx +++ b/web/core/components/views/view-list-header.tsx @@ -5,7 +5,7 @@ import { ListFilter, Search, X } from "lucide-react"; // plane helpers import { useOutsideClickDetector } from "@plane/hooks"; // helpers -import { cn } from "@/helpers/common.helper"; +import { cn } from "@plane/utils"; // hooks import { useMember, useProjectView } from "@/hooks/store"; import { FiltersDropdown } from "../issues"; diff --git a/web/core/components/views/view-list-item-action.tsx b/web/core/components/views/view-list-item-action.tsx index eaeb412ef..1dbed08ac 100644 --- a/web/core/components/views/view-list-item-action.tsx +++ b/web/core/components/views/view-list-item-action.tsx @@ -8,11 +8,10 @@ import { useLocalStorage } from "@plane/hooks"; import { IProjectView } from "@plane/types"; // ui import { Tooltip, FavoriteStar } from "@plane/ui"; +import { calculateTotalFilters, getPublishViewLink } from "@plane/utils"; // components import { DeleteProjectViewModal, CreateUpdateProjectViewModal, ViewQuickActions } from "@/components/views"; // helpers -import { calculateTotalFilters } from "@/helpers/filter.helper"; -import { getPublishViewLink } from "@/helpers/project-views.helpers"; // hooks import { useMember, useProjectView, useUserPermissions } from "@/hooks/store"; import { PublishViewModal } from "@/plane-web/components/views/publish"; diff --git a/web/core/components/web-hooks/create-webhook-modal.tsx b/web/core/components/web-hooks/create-webhook-modal.tsx index 7dadef9a3..838916a69 100644 --- a/web/core/components/web-hooks/create-webhook-modal.tsx +++ b/web/core/components/web-hooks/create-webhook-modal.tsx @@ -8,7 +8,7 @@ import { IWebhook, IWorkspace, TWebhookEventTypes } from "@plane/types"; // ui import { EModalPosition, EModalWidth, ModalCore, TOAST_TYPE, setToast } from "@plane/ui"; // helpers -import { csvDownload } from "@/helpers/download.helper"; +import { csvDownload } from "@plane/utils"; // hooks import useKeypress from "@/hooks/use-keypress"; // components diff --git a/web/core/components/web-hooks/form/secret-key.tsx b/web/core/components/web-hooks/form/secret-key.tsx index f2d16c14e..0853595c4 100644 --- a/web/core/components/web-hooks/form/secret-key.tsx +++ b/web/core/components/web-hooks/form/secret-key.tsx @@ -10,9 +10,8 @@ import { useTranslation } from "@plane/i18n"; import { IWebhook } from "@plane/types"; // ui import { Button, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; +import { csvDownload, copyTextToClipboard } from "@plane/utils"; // helpers -import { csvDownload } from "@/helpers/download.helper"; -import { copyTextToClipboard } from "@/helpers/string.helper"; // hooks import { useWebhook, useWorkspace } from "@/hooks/store"; // types diff --git a/web/core/components/web-hooks/utils.ts b/web/core/components/web-hooks/utils.ts index caebe6fc1..8ae441234 100644 --- a/web/core/components/web-hooks/utils.ts +++ b/web/core/components/web-hooks/utils.ts @@ -1,6 +1,6 @@ // helpers import { IWebhook, IWorkspace } from "@plane/types"; -import { renderFormattedPayloadDate } from "@/helpers/date-time.helper"; +import { renderFormattedPayloadDate } from "@plane/utils"; // types export const getCurrentHookAsCSV = ( diff --git a/web/core/components/workspace-notifications/notification-app-sidebar-option.tsx b/web/core/components/workspace-notifications/notification-app-sidebar-option.tsx index 443169910..358026806 100644 --- a/web/core/components/workspace-notifications/notification-app-sidebar-option.tsx +++ b/web/core/components/workspace-notifications/notification-app-sidebar-option.tsx @@ -3,10 +3,10 @@ import { FC } from "react"; import { observer } from "mobx-react"; import useSWR from "swr"; +// plane imports +import { getNumberCount } from "@plane/utils"; // components -import { CountChip } from "@/components/common"; -// helpers -import { getNumberCount } from "@/helpers/string.helper"; +import { CountChip } from "@/components/common/count-chip"; // hooks import { useWorkspaceNotifications } from "@/hooks/store"; diff --git a/web/core/components/workspace-notifications/root.tsx b/web/core/components/workspace-notifications/root.tsx index 6b41a9cea..f11db704a 100644 --- a/web/core/components/workspace-notifications/root.tsx +++ b/web/core/components/workspace-notifications/root.tsx @@ -6,8 +6,8 @@ import useSWR from "swr"; // plane imports import { ENotificationLoader, ENotificationQueryParamType } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; -// components import { cn } from "@plane/utils"; +// components import { LogoSpinner } from "@/components/common"; import { SimpleEmptyState } from "@/components/empty-state"; import { InboxContentRoot } from "@/components/inbox"; diff --git a/web/core/components/workspace-notifications/sidebar/filters/menu/menu-option-item.tsx b/web/core/components/workspace-notifications/sidebar/filters/menu/menu-option-item.tsx index 6fdca5595..206f4b046 100644 --- a/web/core/components/workspace-notifications/sidebar/filters/menu/menu-option-item.tsx +++ b/web/core/components/workspace-notifications/sidebar/filters/menu/menu-option-item.tsx @@ -6,7 +6,7 @@ import { Check } from "lucide-react"; // plane imports import { ENotificationFilterType } from "@plane/constants"; // helpers -import { cn } from "@/helpers/common.helper"; +import { cn } from "@plane/utils"; // hooks import { useWorkspaceNotifications } from "@/hooks/store"; diff --git a/web/core/components/workspace-notifications/sidebar/header/options/menu-option/menu-item.tsx b/web/core/components/workspace-notifications/sidebar/header/options/menu-option/menu-item.tsx index 918966310..2c1a59ce0 100644 --- a/web/core/components/workspace-notifications/sidebar/header/options/menu-option/menu-item.tsx +++ b/web/core/components/workspace-notifications/sidebar/header/options/menu-option/menu-item.tsx @@ -3,9 +3,9 @@ import { FC } from "react"; import { observer } from "mobx-react"; // components +import { cn } from "@plane/utils"; import type { TPopoverMenuOptions } from "@/components/workspace-notifications"; // helpers -import { cn } from "@/helpers/common.helper"; export const NotificationMenuOptionItem: FC = observer((props) => { const { type, label = "", isActive, prependIcon, appendIcon, onClick } = props; diff --git a/web/core/components/workspace-notifications/sidebar/notification-card/content.tsx b/web/core/components/workspace-notifications/sidebar/notification-card/content.tsx index 7d58b3a8f..46a623f6d 100644 --- a/web/core/components/workspace-notifications/sidebar/notification-card/content.tsx +++ b/web/core/components/workspace-notifications/sidebar/notification-card/content.tsx @@ -1,11 +1,9 @@ import { FC } from "react"; import { TNotification } from "@plane/types"; +import { convertMinutesToHoursMinutesString, renderFormattedDate, sanitizeCommentForNotification, replaceUnderscoreIfSnakeCase, stripAndTruncateHTML } from "@plane/utils"; // components -import { LiteTextReadOnlyEditor } from "@/components/editor"; // helpers -import { convertMinutesToHoursMinutesString, renderFormattedDate } from "@/helpers/date-time.helper"; -import { sanitizeCommentForNotification } from "@/helpers/notification.helper"; -import { replaceUnderscoreIfSnakeCase, stripAndTruncateHTML } from "@/helpers/string.helper"; +import { LiteTextReadOnlyEditor } from "@/components/editor"; export const NotificationContent: FC<{ notification: TNotification; diff --git a/web/core/components/workspace-notifications/sidebar/notification-card/item.tsx b/web/core/components/workspace-notifications/sidebar/notification-card/item.tsx index c7fa3c487..ab1c43a43 100644 --- a/web/core/components/workspace-notifications/sidebar/notification-card/item.tsx +++ b/web/core/components/workspace-notifications/sidebar/notification-card/item.tsx @@ -4,12 +4,10 @@ import { FC, useState } from "react"; import { observer } from "mobx-react"; import { Clock } from "lucide-react"; import { Avatar, Row } from "@plane/ui"; +import { cn, calculateTimeAgo, renderFormattedDate, renderFormattedTime, getFileURL } from "@plane/utils"; // components import { NotificationOption } from "@/components/workspace-notifications"; // helpers -import { cn } from "@/helpers/common.helper"; -import { calculateTimeAgo, renderFormattedDate, renderFormattedTime } from "@/helpers/date-time.helper"; -import { getFileURL } from "@/helpers/file.helper"; // hooks import { useIssueDetail, useNotification, useWorkspace, useWorkspaceNotifications } from "@/hooks/store"; import { NotificationContent } from "./content"; diff --git a/web/core/components/workspace-notifications/sidebar/notification-card/options/button.tsx b/web/core/components/workspace-notifications/sidebar/notification-card/options/button.tsx index cfaf26cf5..e1b5d7917 100644 --- a/web/core/components/workspace-notifications/sidebar/notification-card/options/button.tsx +++ b/web/core/components/workspace-notifications/sidebar/notification-card/options/button.tsx @@ -3,7 +3,7 @@ import { FC, ReactNode } from "react"; import { Tooltip } from "@plane/ui"; // helpers -import { cn } from "@/helpers/common.helper"; +import { cn } from "@plane/utils"; // hooks import { usePlatformOS } from "@/hooks/use-platform-os"; diff --git a/web/core/components/workspace-notifications/sidebar/notification-card/options/root.tsx b/web/core/components/workspace-notifications/sidebar/notification-card/options/root.tsx index 2017c0db8..7d0284ae5 100644 --- a/web/core/components/workspace-notifications/sidebar/notification-card/options/root.tsx +++ b/web/core/components/workspace-notifications/sidebar/notification-card/options/root.tsx @@ -3,13 +3,13 @@ import { FC, Dispatch, SetStateAction } from "react"; import { observer } from "mobx-react"; // components +import { cn } from "@plane/utils"; import { NotificationItemReadOption, NotificationItemArchiveOption, NotificationItemSnoozeOption, } from "@/components/workspace-notifications"; // helpers -import { cn } from "@/helpers/common.helper"; // hooks import { useNotification } from "@/hooks/store"; diff --git a/web/core/components/workspace-notifications/sidebar/notification-card/options/snooze/modal.tsx b/web/core/components/workspace-notifications/sidebar/notification-card/options/snooze/modal.tsx index 8b34af477..cc3b274f0 100644 --- a/web/core/components/workspace-notifications/sidebar/notification-card/options/snooze/modal.tsx +++ b/web/core/components/workspace-notifications/sidebar/notification-card/options/snooze/modal.tsx @@ -9,9 +9,9 @@ import { Transition, Dialog } from "@headlessui/react"; import { allTimeIn30MinutesInterval12HoursFormat } from "@plane/constants"; import { Button, CustomSelect } from "@plane/ui"; // components +import { getDate } from "@plane/utils"; import { DateDropdown } from "@/components/dropdowns"; // helpers -import { getDate } from "@/helpers/date-time.helper"; type TNotificationSnoozeModal = { isOpen: boolean; diff --git a/web/core/components/workspace-notifications/sidebar/notification-card/options/snooze/root.tsx b/web/core/components/workspace-notifications/sidebar/notification-card/options/snooze/root.tsx index 1695cd8c8..b96783b09 100644 --- a/web/core/components/workspace-notifications/sidebar/notification-card/options/snooze/root.tsx +++ b/web/core/components/workspace-notifications/sidebar/notification-card/options/snooze/root.tsx @@ -9,9 +9,9 @@ import { NOTIFICATION_SNOOZE_OPTIONS } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { Tooltip, setToast, TOAST_TYPE } from "@plane/ui"; // components +import { cn } from "@plane/utils"; import { NotificationSnoozeModal } from "@/components/workspace-notifications"; // helpers -import { cn } from "@/helpers/common.helper"; // hooks import { useWorkspaceNotifications } from "@/hooks/store"; import { usePlatformOS } from "@/hooks/use-platform-os"; diff --git a/web/core/components/workspace-notifications/sidebar/root.tsx b/web/core/components/workspace-notifications/sidebar/root.tsx index 48b94bb1a..95d70d8fa 100644 --- a/web/core/components/workspace-notifications/sidebar/root.tsx +++ b/web/core/components/workspace-notifications/sidebar/root.tsx @@ -8,6 +8,7 @@ import { NOTIFICATION_TABS, TNotificationTab } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; // components import { Header, Row, ERowVariant, EHeaderVariant, ContentWrapper } from "@plane/ui"; +import { cn, getNumberCount } from "@plane/utils"; import { CountChip } from "@/components/common"; import { NotificationsLoader, @@ -15,9 +16,6 @@ import { NotificationSidebarHeader, AppliedFilters, } from "@/components/workspace-notifications"; -// helpers -import { cn } from "@/helpers/common.helper"; -import { getNumberCount } from "@/helpers/string.helper"; // hooks import { useWorkspace, useWorkspaceNotifications } from "@/hooks/store"; diff --git a/web/core/components/workspace/logo.tsx b/web/core/components/workspace/logo.tsx index f25615dfc..c956fe5ab 100644 --- a/web/core/components/workspace/logo.tsx +++ b/web/core/components/workspace/logo.tsx @@ -1,9 +1,8 @@ import { observer } from "mobx-react"; // plane imports import { useTranslation } from "@plane/i18n"; -import { cn } from "@plane/utils"; -// helpers -import { getFileURL } from "@/helpers/file.helper"; +import { cn, getFileURL } from "@plane/utils"; + type Props = { logo: string | null | undefined; diff --git a/web/core/components/workspace/settings/member-columns.tsx b/web/core/components/workspace/settings/member-columns.tsx index 160af368f..1e5036fef 100644 --- a/web/core/components/workspace/settings/member-columns.tsx +++ b/web/core/components/workspace/settings/member-columns.tsx @@ -10,7 +10,7 @@ import { IUser, IWorkspaceMember } from "@plane/types"; import { CustomSelect, PopoverMenu, TOAST_TYPE, setToast } from "@plane/ui"; // constants // helpers -import { getFileURL } from "@/helpers/file.helper"; +import { getFileURL } from "@plane/utils"; // hooks import { useMember, useUser, useUserPermissions } from "@/hooks/store"; // plane web constants diff --git a/web/core/components/workspace/settings/workspace-details.tsx b/web/core/components/workspace/settings/workspace-details.tsx index 39cd7f124..8c996b274 100644 --- a/web/core/components/workspace/settings/workspace-details.tsx +++ b/web/core/components/workspace/settings/workspace-details.tsx @@ -9,12 +9,11 @@ import { ORGANIZATION_SIZE, WORKSPACE_UPDATED, EUserPermissions, EUserPermission import { useTranslation } from "@plane/i18n"; import { IWorkspace } from "@plane/types"; import { Button, CustomSelect, Input, TOAST_TYPE, setToast } from "@plane/ui"; -import { copyUrlToClipboard } from "@plane/utils"; +import { copyUrlToClipboard, getFileURL } from "@plane/utils"; // components import { LogoSpinner } from "@/components/common"; import { WorkspaceImageUploadModal } from "@/components/core"; // helpers -import { getFileURL } from "@/helpers/file.helper"; // hooks import { useEventTracker, useUserPermissions, useWorkspace } from "@/hooks/store"; // plane web components diff --git a/web/core/components/workspace/sidebar/dropdown-item.tsx b/web/core/components/workspace/sidebar/dropdown-item.tsx index 9eef151d1..68b2d5b2e 100644 --- a/web/core/components/workspace/sidebar/dropdown-item.tsx +++ b/web/core/components/workspace/sidebar/dropdown-item.tsx @@ -8,9 +8,8 @@ import { Menu } from "@headlessui/react"; import { EUserPermissions } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { IWorkspace } from "@plane/types"; -import { cn, getFileURL } from "@plane/utils"; +import { cn, getFileURL, getUserRole } from "@plane/utils"; // helpers -import { getUserRole } from "@/helpers/user.helper"; // plane web imports import { SubscriptionPill } from "@/plane-web/components/common/subscription"; diff --git a/web/core/components/workspace/sidebar/dropdown.tsx b/web/core/components/workspace/sidebar/dropdown.tsx index d24fb369a..d1d5e5249 100644 --- a/web/core/components/workspace/sidebar/dropdown.tsx +++ b/web/core/components/workspace/sidebar/dropdown.tsx @@ -10,13 +10,12 @@ import { ChevronDown, CirclePlus, LogOut, Mails, Settings } from "lucide-react"; // ui import { Menu, Transition } from "@headlessui/react"; // plane imports +import { GOD_MODE_URL } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { IWorkspace } from "@plane/types"; import { Avatar, Loader, TOAST_TYPE, setToast } from "@plane/ui"; -import { orderWorkspacesList } from "@plane/utils"; +import { orderWorkspacesList, cn, getFileURL } from "@plane/utils"; // helpers -import { GOD_MODE_URL, cn } from "@/helpers/common.helper"; -import { getFileURL } from "@/helpers/file.helper"; // hooks import { useAppTheme, useUser, useUserProfile, useWorkspace } from "@/hooks/store"; // plane web helpers diff --git a/web/core/components/workspace/sidebar/favorites/favorite-folder.tsx b/web/core/components/workspace/sidebar/favorites/favorite-folder.tsx index 9f81deeaf..91f8addd8 100644 --- a/web/core/components/workspace/sidebar/favorites/favorite-folder.tsx +++ b/web/core/components/workspace/sidebar/favorites/favorite-folder.tsx @@ -23,7 +23,7 @@ import { useTranslation } from "@plane/i18n"; import { IFavorite, InstructionType } from "@plane/types"; import { CustomMenu, Tooltip, DropIndicator, FavoriteFolderIcon, DragHandle } from "@plane/ui"; // helpers -import { cn } from "@/helpers/common.helper"; +import { cn } from "@plane/utils"; // hooks import { useAppTheme } from "@/hooks/store"; import { useFavorite } from "@/hooks/store/use-favorite"; diff --git a/web/core/components/workspace/sidebar/favorites/favorite-items/common/favorite-item-drag-handle.tsx b/web/core/components/workspace/sidebar/favorites/favorite-items/common/favorite-item-drag-handle.tsx index 6c8e8666e..91f9107c7 100644 --- a/web/core/components/workspace/sidebar/favorites/favorite-items/common/favorite-item-drag-handle.tsx +++ b/web/core/components/workspace/sidebar/favorites/favorite-items/common/favorite-item-drag-handle.tsx @@ -4,7 +4,7 @@ import { observer } from "mobx-react"; // ui import { DragHandle, Tooltip } from "@plane/ui"; // helper -import { cn } from "@/helpers/common.helper"; +import { cn } from "@plane/utils"; // hooks import { usePlatformOS } from "@/hooks/use-platform-os"; diff --git a/web/core/components/workspace/sidebar/favorites/favorite-items/common/favorite-item-quick-action.tsx b/web/core/components/workspace/sidebar/favorites/favorite-items/common/favorite-item-quick-action.tsx index cf6436733..92bd9292e 100644 --- a/web/core/components/workspace/sidebar/favorites/favorite-items/common/favorite-item-quick-action.tsx +++ b/web/core/components/workspace/sidebar/favorites/favorite-items/common/favorite-item-quick-action.tsx @@ -7,7 +7,7 @@ import { useTranslation } from "@plane/i18n"; import { IFavorite } from "@plane/types"; import { CustomMenu } from "@plane/ui"; // helpers -import { cn } from "@/helpers/common.helper"; +import { cn } from "@plane/utils"; type Props = { ref: React.MutableRefObject; diff --git a/web/core/components/workspace/sidebar/favorites/favorite-items/common/favorite-item-wrapper.tsx b/web/core/components/workspace/sidebar/favorites/favorite-items/common/favorite-item-wrapper.tsx index f1ffde408..9c3c64b8d 100644 --- a/web/core/components/workspace/sidebar/favorites/favorite-items/common/favorite-item-wrapper.tsx +++ b/web/core/components/workspace/sidebar/favorites/favorite-items/common/favorite-item-wrapper.tsx @@ -1,7 +1,7 @@ "use client"; import React, { FC } from "react"; // helpers -import { cn } from "@/helpers/common.helper"; +import { cn } from "@plane/utils"; type Props = { children: React.ReactNode; diff --git a/web/core/components/workspace/sidebar/favorites/favorites-menu.tsx b/web/core/components/workspace/sidebar/favorites/favorites-menu.tsx index f83f0ed06..d2d30a37d 100644 --- a/web/core/components/workspace/sidebar/favorites/favorites-menu.tsx +++ b/web/core/components/workspace/sidebar/favorites/favorites-menu.tsx @@ -21,7 +21,7 @@ import { setToast, TOAST_TYPE, Tooltip } from "@plane/ui"; // constants // helpers -import { cn } from "@/helpers/common.helper"; +import { cn } from "@plane/utils"; // hooks import { useAppTheme } from "@/hooks/store"; import { useFavorite } from "@/hooks/store/use-favorite"; diff --git a/web/core/components/workspace/sidebar/help-section.tsx b/web/core/components/workspace/sidebar/help-section.tsx index 9c1b98fe8..73f036cd4 100644 --- a/web/core/components/workspace/sidebar/help-section.tsx +++ b/web/core/components/workspace/sidebar/help-section.tsx @@ -8,9 +8,9 @@ import { useTranslation } from "@plane/i18n"; // ui import { CustomMenu, Tooltip, ToggleSwitch } from "@plane/ui"; // components +import { cn } from "@plane/utils"; import { ProductUpdatesModal } from "@/components/global"; // helpers -import { cn } from "@/helpers/common.helper"; // hooks import { useAppTheme, useCommandPalette, useInstance, useTransient, useUserSettings } from "@/hooks/store"; import { usePlatformOS } from "@/hooks/use-platform-os"; diff --git a/web/core/components/workspace/sidebar/projects-list-item.tsx b/web/core/components/workspace/sidebar/projects-list-item.tsx index 097b3da54..581054193 100644 --- a/web/core/components/workspace/sidebar/projects-list-item.tsx +++ b/web/core/components/workspace/sidebar/projects-list-item.tsx @@ -19,10 +19,10 @@ import { useTranslation } from "@plane/i18n"; // ui import { CustomMenu, Tooltip, ArchiveIcon, DropIndicator, DragHandle, ControlLink } from "@plane/ui"; // components -import { Logo } from "@/components/common"; +import { cn } from "@plane/utils"; +import { Logo } from "@/components/common/logo"; import { LeaveProjectModal, PublishProjectModal } from "@/components/project"; // helpers -import { cn } from "@/helpers/common.helper"; // hooks import { useAppTheme, useCommandPalette, useEventTracker, useProject, useUserPermissions } from "@/hooks/store"; import { usePlatformOS } from "@/hooks/use-platform-os"; diff --git a/web/core/components/workspace/sidebar/projects-list.tsx b/web/core/components/workspace/sidebar/projects-list.tsx index bea70b0d6..2ef487468 100644 --- a/web/core/components/workspace/sidebar/projects-list.tsx +++ b/web/core/components/workspace/sidebar/projects-list.tsx @@ -11,13 +11,11 @@ import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; // ui import { Loader, TOAST_TYPE, Tooltip, setToast } from "@plane/ui"; -import { copyUrlToClipboard } from "@plane/utils"; +import { copyUrlToClipboard, cn, orderJoinedProjects } from "@plane/utils"; // components import { CreateProjectModal } from "@/components/project"; import { SidebarProjectsListItem } from "@/components/workspace"; // helpers -import { cn } from "@/helpers/common.helper"; -import { orderJoinedProjects } from "@/helpers/project.helper"; // hooks import { useAppTheme, useCommandPalette, useEventTracker, useProject, useUserPermissions } from "@/hooks/store"; // plane web types diff --git a/web/core/components/workspace/sidebar/quick-actions.tsx b/web/core/components/workspace/sidebar/quick-actions.tsx index 86ab89603..8ba2506c3 100644 --- a/web/core/components/workspace/sidebar/quick-actions.tsx +++ b/web/core/components/workspace/sidebar/quick-actions.tsx @@ -7,10 +7,10 @@ import { useTranslation } from "@plane/i18n"; // types import { TIssue } from "@plane/types"; // components +import { cn } from "@plane/utils"; import { CreateUpdateIssueModal } from "@/components/issues"; // constants // helpers -import { cn } from "@/helpers/common.helper"; // hooks import { useAppTheme, useCommandPalette, useEventTracker, useProject, useUserPermissions } from "@/hooks/store"; import useLocalStorage from "@/hooks/use-local-storage"; diff --git a/web/core/components/workspace/sidebar/user-menu.tsx b/web/core/components/workspace/sidebar/user-menu.tsx index 2d625ae6b..5e3ee4b0a 100644 --- a/web/core/components/workspace/sidebar/user-menu.tsx +++ b/web/core/components/workspace/sidebar/user-menu.tsx @@ -8,9 +8,9 @@ import { Home, Inbox, PenSquare } from "lucide-react"; import { EUserWorkspaceRoles } from "@plane/constants"; import { UserActivityIcon } from "@plane/ui"; // components +import { cn } from "@plane/utils"; import { SidebarUserMenuItem } from "@/components/workspace/sidebar"; // helpers -import { cn } from "@/helpers/common.helper"; // hooks import { useAppTheme, useUserPermissions, useUser } from "@/hooks/store"; diff --git a/web/core/components/workspace/sidebar/workspace-menu.tsx b/web/core/components/workspace/sidebar/workspace-menu.tsx index 2b2fa90b3..57582c6c8 100644 --- a/web/core/components/workspace/sidebar/workspace-menu.tsx +++ b/web/core/components/workspace/sidebar/workspace-menu.tsx @@ -9,9 +9,9 @@ import { Disclosure, Transition } from "@headlessui/react"; import { EUserWorkspaceRoles } from "@plane/constants"; import { ContrastIcon } from "@plane/ui"; // components +import { cn } from "@plane/utils"; import { SidebarWorkspaceMenuHeader, SidebarWorkspaceMenuItem } from "@/components/workspace/sidebar"; // helpers -import { cn } from "@/helpers/common.helper"; // hooks import { useAppTheme } from "@/hooks/store"; import useLocalStorage from "@/hooks/use-local-storage"; diff --git a/web/core/components/workspace/views/default-view-list-item.tsx b/web/core/components/workspace/views/default-view-list-item.tsx index de64052cf..536d7ff8a 100644 --- a/web/core/components/workspace/views/default-view-list-item.tsx +++ b/web/core/components/workspace/views/default-view-list-item.tsx @@ -3,7 +3,7 @@ import Link from "next/link"; import { useParams } from "next/navigation"; import { useTranslation } from "@plane/i18n"; // helpers -import { truncateText } from "@/helpers/string.helper"; +import { truncateText } from "@plane/utils"; type Props = { view: { key: string; i18n_label: string } }; diff --git a/web/core/components/workspace/views/default-view-quick-action.tsx b/web/core/components/workspace/views/default-view-quick-action.tsx index 9ade52acb..e7f0e276a 100644 --- a/web/core/components/workspace/views/default-view-quick-action.tsx +++ b/web/core/components/workspace/views/default-view-quick-action.tsx @@ -8,10 +8,8 @@ import { useTranslation } from "@plane/i18n"; // ui import { TStaticViewTypes } from "@plane/types"; import { ContextMenu, CustomMenu, TContextMenuItem, TOAST_TYPE, setToast } from "@plane/ui"; -import { copyUrlToClipboard } from "@plane/utils"; +import { copyUrlToClipboard, cn } from "@plane/utils"; // helpers -import { cn } from "@/helpers/common.helper"; - type Props = { parentRef: React.RefObject; workspaceSlug: string; diff --git a/web/core/components/workspace/views/form.tsx b/web/core/components/workspace/views/form.tsx index 5abe7f2dd..676182216 100644 --- a/web/core/components/workspace/views/form.tsx +++ b/web/core/components/workspace/views/form.tsx @@ -12,9 +12,9 @@ import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOption // ui import { Button, Input, TextArea } from "@plane/ui"; // components +import { getComputedDisplayFilters, getComputedDisplayProperties } from "@plane/utils"; import { AppliedFiltersList, DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "@/components/issues"; // helpers -import { getComputedDisplayFilters, getComputedDisplayProperties } from "@/helpers/issue.helper"; // hooks import { useLabel, useMember } from "@/hooks/store"; import { AccessController } from "@/plane-web/components/views/access-controller"; diff --git a/web/core/components/workspace/views/quick-action.tsx b/web/core/components/workspace/views/quick-action.tsx index 5fbdffc4f..e0950a78a 100644 --- a/web/core/components/workspace/views/quick-action.tsx +++ b/web/core/components/workspace/views/quick-action.tsx @@ -9,13 +9,11 @@ import { EViewAccess, EUserPermissions, EUserPermissionsLevel } from "@plane/con import { useTranslation } from "@plane/i18n"; import { IWorkspaceView } from "@plane/types"; import { ContextMenu, TContextMenuItem, TOAST_TYPE, setToast } from "@plane/ui"; -import { copyUrlToClipboard } from "@plane/utils"; +import { copyUrlToClipboard, cn } from "@plane/utils"; // components import { CreateUpdateWorkspaceViewModal, DeleteGlobalViewModal } from "@/components/workspace"; // constants // helpers -import { cn } from "@/helpers/common.helper"; -import {} from "@/helpers/string.helper"; // hooks import { useUser, useUserPermissions } from "@/hooks/store"; diff --git a/web/core/components/workspace/views/view-list-item.tsx b/web/core/components/workspace/views/view-list-item.tsx index 23bab4f5e..38fd9b578 100644 --- a/web/core/components/workspace/views/view-list-item.tsx +++ b/web/core/components/workspace/views/view-list-item.tsx @@ -8,11 +8,10 @@ import { useParams } from "next/navigation"; import { Pencil, Trash2 } from "lucide-react"; // ui import { CustomMenu } from "@plane/ui"; +import { calculateTotalFilters, truncateText } from "@plane/utils"; // components import { CreateUpdateWorkspaceViewModal, DeleteGlobalViewModal } from "@/components/workspace"; // helpers -import { calculateTotalFilters } from "@/helpers/filter.helper"; -import { truncateText } from "@/helpers/string.helper"; // store hooks import { useEventTracker, useGlobalView } from "@/hooks/store"; diff --git a/web/core/constants/editor.ts b/web/core/constants/editor.ts index 5e8c723d7..ef9c91402 100644 --- a/web/core/constants/editor.ts +++ b/web/core/constants/editor.ts @@ -23,12 +23,10 @@ import { TextQuote, Underline, } from "lucide-react"; -// editor +// plane imports import { TCommandExtraProps, TEditorCommands, TEditorFontStyle } from "@plane/editor"; -// ui import { MonospaceIcon, SansSerifIcon, SerifIcon } from "@plane/ui"; -// helpers -import { convertRemToPixel } from "@/helpers/common.helper"; +import { convertRemToPixel } from "@plane/utils"; type TEditorTypes = "lite" | "document" | "sticky"; diff --git a/web/core/hooks/editor/use-editor-config.ts b/web/core/hooks/editor/use-editor-config.ts index 00eff1752..7e5692b07 100644 --- a/web/core/hooks/editor/use-editor-config.ts +++ b/web/core/hooks/editor/use-editor-config.ts @@ -2,7 +2,7 @@ import { useCallback } from "react"; // plane editor import { TFileHandler, TReadOnlyFileHandler } from "@plane/editor"; // helpers -import { getEditorAssetSrc } from "@/helpers/editor.helper"; +import { getEditorAssetSrc } from "@plane/utils"; // hooks import { useEditorAsset } from "@/hooks/store"; // plane web hooks diff --git a/web/core/hooks/editor/use-editor-mention.tsx b/web/core/hooks/editor/use-editor-mention.tsx index 183cc1ec9..04e3686dd 100644 --- a/web/core/hooks/editor/use-editor-mention.tsx +++ b/web/core/hooks/editor/use-editor-mention.tsx @@ -6,7 +6,7 @@ import { TSearchEntities, TSearchEntityRequestPayload, TSearchResponse, TUserSea // plane ui import { Avatar } from "@plane/ui"; // helpers -import { getFileURL } from "@/helpers/file.helper"; +import { getFileURL } from "@plane/utils"; // plane web constants import { EDITOR_MENTION_TYPES } from "@/plane-web/constants/editor"; // plane web hooks diff --git a/web/core/hooks/use-favorite-item-details.tsx b/web/core/hooks/use-favorite-item-details.tsx index eadc64f7c..b9cd11304 100644 --- a/web/core/hooks/use-favorite-item-details.tsx +++ b/web/core/hooks/use-favorite-item-details.tsx @@ -1,12 +1,12 @@ // plane imports import { IFavorite } from "@plane/types"; // components +import { getPageName } from "@plane/utils"; import { generateFavoriteItemLink, getFavoriteItemIcon, } from "@/components/workspace/sidebar/favorites/favorite-items/common"; // helpers -import { getPageName } from "@/helpers/page.helper"; // hooks import { useProject, useProjectView, useCycle, useModule } from "@/hooks/store"; // plane web hooks diff --git a/web/core/hooks/use-issue-peek-overview-redirection.tsx b/web/core/hooks/use-issue-peek-overview-redirection.tsx index beb27b70b..9b980fe32 100644 --- a/web/core/hooks/use-issue-peek-overview-redirection.tsx +++ b/web/core/hooks/use-issue-peek-overview-redirection.tsx @@ -4,7 +4,7 @@ import { EIssueServiceType } from "@plane/constants"; // types import { TIssue } from "@plane/types"; // helpers -import { generateWorkItemLink } from "@/helpers/issue.helper"; +import { generateWorkItemLink } from "@plane/utils"; // hooks import { useIssueDetail, useProject } from "./store"; diff --git a/web/core/hooks/use-parse-editor-content.ts b/web/core/hooks/use-parse-editor-content.ts index 86e13a73f..8e4f3ba97 100644 --- a/web/core/hooks/use-parse-editor-content.ts +++ b/web/core/hooks/use-parse-editor-content.ts @@ -3,7 +3,7 @@ import { useParams } from "next/navigation"; // plane types import { TSearchEntities } from "@plane/types"; // helpers -import { getBase64Image } from "@/helpers/file.helper"; +import { getBase64Image } from "@plane/utils"; // hooks import { useMember } from "@/hooks/store"; // plane web hooks diff --git a/web/core/lib/posthog-provider.tsx b/web/core/lib/posthog-provider.tsx index 8799e827a..f0aa05458 100644 --- a/web/core/lib/posthog-provider.tsx +++ b/web/core/lib/posthog-provider.tsx @@ -9,7 +9,7 @@ import { PostHogProvider as PHProvider } from "posthog-js/react"; // constants import { GROUP_WORKSPACE } from "@plane/constants"; // helpers -import { getUserRole } from "@/helpers/user.helper"; +import { getUserRole } from "@plane/utils"; // hooks import { useWorkspace, useUser, useInstance, useUserPermissions } from "@/hooks/store"; // dynamic imports diff --git a/web/core/lib/wrappers/store-wrapper.tsx b/web/core/lib/wrappers/store-wrapper.tsx index 0243c34f4..a665199bb 100644 --- a/web/core/lib/wrappers/store-wrapper.tsx +++ b/web/core/lib/wrappers/store-wrapper.tsx @@ -4,7 +4,7 @@ import { useParams } from "next/navigation"; import { useTheme } from "next-themes"; import { useTranslation, TLanguage } from "@plane/i18n"; // helpers -import { applyTheme, unsetCustomCssVariables } from "@/helpers/theme.helper"; +import { applyTheme, unsetCustomCssVariables } from "@plane/utils"; // hooks import { useRouterParams, useAppTheme, useUserProfile } from "@/hooks/store"; diff --git a/web/core/local-db/utils/load-workspace.ts b/web/core/local-db/utils/load-workspace.ts index 0cd493118..509e093a9 100644 --- a/web/core/local-db/utils/load-workspace.ts +++ b/web/core/local-db/utils/load-workspace.ts @@ -1,6 +1,6 @@ import { difference } from "lodash"; +import { API_BASE_URL } from "@plane/constants"; import { IEstimate, IEstimatePoint, IWorkspaceMember, TIssue } from "@plane/types"; -import { API_BASE_URL } from "@/helpers/common.helper"; import { EstimateService } from "@/plane-web/services/project/estimate.service"; import { CycleService } from "@/services/cycle.service"; import { IssueLabelService } from "@/services/issue/issue_label.service"; diff --git a/web/core/services/ai.service.ts b/web/core/services/ai.service.ts index bb0241e06..7588e309b 100644 --- a/web/core/services/ai.service.ts +++ b/web/core/services/ai.service.ts @@ -1,5 +1,5 @@ // helpers -import { API_BASE_URL } from "@/helpers/common.helper"; +import { API_BASE_URL } from "@plane/constants"; // plane web constants import { AI_EDITOR_TASKS } from "@/plane-web/constants/ai"; // services diff --git a/web/core/services/analytics.service.ts b/web/core/services/analytics.service.ts index de8e489d3..1391e487f 100644 --- a/web/core/services/analytics.service.ts +++ b/web/core/services/analytics.service.ts @@ -1,5 +1,7 @@ +// plane imports import { API_BASE_URL } from "@plane/constants"; import { IAnalyticsResponse, TAnalyticsTabsBase, TAnalyticsGraphsBase, TAnalyticsFilterParams } from "@plane/types"; +// services import { APIService } from "./api.service"; export class AnalyticsService extends APIService { diff --git a/web/core/services/api_token.service.ts b/web/core/services/api_token.service.ts index 51078396c..ba0acfb39 100644 --- a/web/core/services/api_token.service.ts +++ b/web/core/services/api_token.service.ts @@ -1,5 +1,5 @@ +import { API_BASE_URL } from "@plane/constants"; import { IApiToken } from "@plane/types"; -import { API_BASE_URL } from "@/helpers/common.helper"; import { APIService } from "./api.service"; export class APITokenService extends APIService { diff --git a/web/core/services/app_config.service.ts b/web/core/services/app_config.service.ts index 52794a46d..e930ac232 100644 --- a/web/core/services/app_config.service.ts +++ b/web/core/services/app_config.service.ts @@ -1,5 +1,5 @@ // services -import { API_BASE_URL } from "@/helpers/common.helper"; +import { API_BASE_URL } from "@plane/constants"; import { APIService } from "@/services/api.service"; // helper // types diff --git a/web/core/services/app_installation.service.ts b/web/core/services/app_installation.service.ts index 6c584feb6..6c5f0e4de 100644 --- a/web/core/services/app_installation.service.ts +++ b/web/core/services/app_installation.service.ts @@ -1,5 +1,5 @@ // services -import { API_BASE_URL } from "@/helpers/common.helper"; +import { API_BASE_URL } from "@plane/constants"; import { APIService } from "@/services/api.service"; // helpers diff --git a/web/core/services/auth.service.ts b/web/core/services/auth.service.ts index 7663afde8..9e167f981 100644 --- a/web/core/services/auth.service.ts +++ b/web/core/services/auth.service.ts @@ -1,7 +1,7 @@ // types +import { API_BASE_URL } from "@plane/constants"; import { ICsrfTokenData, IEmailCheckData, IEmailCheckResponse } from "@plane/types"; // helpers -import { API_BASE_URL } from "@/helpers/common.helper"; // services import { APIService } from "@/services/api.service"; diff --git a/web/core/services/cycle.service.ts b/web/core/services/cycle.service.ts index 1b580d18f..222f04bf2 100644 --- a/web/core/services/cycle.service.ts +++ b/web/core/services/cycle.service.ts @@ -1,4 +1,5 @@ // services +import { API_BASE_URL } from "@plane/constants"; import type { CycleDateCheckData, ICycle, @@ -8,7 +9,6 @@ import type { TProgressSnapshot, TCycleEstimateDistribution, } from "@plane/types"; -import { API_BASE_URL } from "@/helpers/common.helper"; import { APIService } from "@/services/api.service"; export class CycleService extends APIService { diff --git a/web/core/services/cycle_archive.service.ts b/web/core/services/cycle_archive.service.ts index 06f3f426a..3042b1049 100644 --- a/web/core/services/cycle_archive.service.ts +++ b/web/core/services/cycle_archive.service.ts @@ -1,7 +1,7 @@ // type +import { API_BASE_URL } from "@plane/constants"; import { ICycle } from "@plane/types"; // helpers -import { API_BASE_URL } from "@/helpers/common.helper"; // services import { APIService } from "@/services/api.service"; diff --git a/web/core/services/dashboard.service.ts b/web/core/services/dashboard.service.ts index ad86932f9..a7416b173 100644 --- a/web/core/services/dashboard.service.ts +++ b/web/core/services/dashboard.service.ts @@ -1,5 +1,5 @@ +import { API_BASE_URL } from "@plane/constants"; import { THomeDashboardResponse, TWidget, TWidgetStatsResponse, TWidgetStatsRequestParams } from "@plane/types"; -import { API_BASE_URL } from "@/helpers/common.helper"; import { APIService } from "@/services/api.service"; // helpers // types diff --git a/web/core/services/favorite/favorite.service.ts b/web/core/services/favorite/favorite.service.ts index 8a4963cdf..13168932f 100644 --- a/web/core/services/favorite/favorite.service.ts +++ b/web/core/services/favorite/favorite.service.ts @@ -1,6 +1,6 @@ +import { API_BASE_URL } from "@plane/constants"; import type { IFavorite } from "@plane/types"; // helpers -import { API_BASE_URL } from "@/helpers/common.helper"; // services import { APIService } from "@/services/api.service"; // types diff --git a/web/core/services/file.service.ts b/web/core/services/file.service.ts index cbf7ae597..5de44bab6 100644 --- a/web/core/services/file.service.ts +++ b/web/core/services/file.service.ts @@ -1,9 +1,9 @@ import { AxiosRequestConfig } from "axios"; // plane types +import { API_BASE_URL } from "@plane/constants"; import { TFileEntityInfo, TFileSignedURLResponse } from "@plane/types"; // helpers -import { API_BASE_URL } from "@/helpers/common.helper"; -import { generateFileUploadPayload, getAssetIdFromUrl, getFileMetaDataForUpload } from "@/helpers/file.helper"; +import { generateFileUploadPayload, getAssetIdFromUrl, getFileMetaDataForUpload } from "@plane/utils"; // services import { APIService } from "@/services/api.service"; import { FileUploadService } from "@/services/file-upload.service"; diff --git a/web/core/services/inbox/inbox-issue.service.ts b/web/core/services/inbox/inbox-issue.service.ts index 007c364c5..bccb6b224 100644 --- a/web/core/services/inbox/inbox-issue.service.ts +++ b/web/core/services/inbox/inbox-issue.service.ts @@ -1,8 +1,7 @@ // plane imports -import { EInboxIssueSource, TInboxIssue } from "@plane/constants"; +import { EInboxIssueSource, TInboxIssue, API_BASE_URL } from "@plane/constants"; import type { TIssue, TInboxIssueWithPagination } from "@plane/types"; // helpers -import { API_BASE_URL } from "@/helpers/common.helper"; // services import { APIService } from "@/services/api.service"; diff --git a/web/core/services/inbox/intake-work_item_version.service.ts b/web/core/services/inbox/intake-work_item_version.service.ts index c8ebaa284..34c47bcd0 100644 --- a/web/core/services/inbox/intake-work_item_version.service.ts +++ b/web/core/services/inbox/intake-work_item_version.service.ts @@ -1,7 +1,7 @@ // plane imports +import { API_BASE_URL } from "@plane/constants"; import { type TDescriptionVersionsListResponse, type TDescriptionVersionDetails } from "@plane/types"; // helpers -import { API_BASE_URL } from "@/helpers/common.helper"; // services import { APIService } from "@/services/api.service"; diff --git a/web/core/services/instance.service.ts b/web/core/services/instance.service.ts index 29b93bb55..b85fad7db 100644 --- a/web/core/services/instance.service.ts +++ b/web/core/services/instance.service.ts @@ -1,7 +1,7 @@ // types +import { API_BASE_URL } from "@plane/constants"; import type { IInstanceInfo, TPage } from "@plane/types"; // helpers -import { API_BASE_URL } from "@/helpers/common.helper"; // services import { APIService } from "@/services/api.service"; diff --git a/web/core/services/integrations/github.service.ts b/web/core/services/integrations/github.service.ts index 634eb6436..4e8129d40 100644 --- a/web/core/services/integrations/github.service.ts +++ b/web/core/services/integrations/github.service.ts @@ -1,5 +1,5 @@ +import { API_BASE_URL } from "@plane/constants"; import { IGithubRepoInfo, IGithubServiceImportFormData } from "@plane/types"; -import { API_BASE_URL } from "@/helpers/common.helper"; import { APIService } from "@/services/api.service"; // helpers // types diff --git a/web/core/services/integrations/integration.service.ts b/web/core/services/integrations/integration.service.ts index bf62f8fbe..f284473fc 100644 --- a/web/core/services/integrations/integration.service.ts +++ b/web/core/services/integrations/integration.service.ts @@ -1,5 +1,5 @@ +import { API_BASE_URL } from "@plane/constants"; import { IAppIntegration, IImporterService, IWorkspaceIntegration, IExportServiceResponse } from "@plane/types"; -import { API_BASE_URL } from "@/helpers/common.helper"; import { APIService } from "@/services/api.service"; // types // helper diff --git a/web/core/services/integrations/jira.service.ts b/web/core/services/integrations/jira.service.ts index 14130ed16..6be50b2ec 100644 --- a/web/core/services/integrations/jira.service.ts +++ b/web/core/services/integrations/jira.service.ts @@ -1,5 +1,5 @@ +import { API_BASE_URL } from "@plane/constants"; import { IJiraMetadata, IJiraResponse, IJiraImporterForm } from "@plane/types"; -import { API_BASE_URL } from "@/helpers/common.helper"; import { APIService } from "@/services/api.service"; // types diff --git a/web/core/services/issue/issue.service.ts b/web/core/services/issue/issue.service.ts index e53492b81..88df42b89 100644 --- a/web/core/services/issue/issue.service.ts +++ b/web/core/services/issue/issue.service.ts @@ -1,5 +1,5 @@ -import { EIssueServiceType } from "@plane/constants"; // types +import { EIssueServiceType, API_BASE_URL } from "@plane/constants"; import { TIssueParams, type IIssueDisplayProperties, @@ -12,8 +12,7 @@ import { type TIssueSubIssues, } from "@plane/types"; // helpers -import { API_BASE_URL } from "@/helpers/common.helper"; -import { getIssuesShouldFallbackToServer } from "@/helpers/issue.helper"; +import { getIssuesShouldFallbackToServer } from "@plane/utils"; import { persistence } from "@/local-db/storage.sqlite"; // services diff --git a/web/core/services/issue/issue_activity.service.ts b/web/core/services/issue/issue_activity.service.ts index 103cf6e21..4eb5e90b0 100644 --- a/web/core/services/issue/issue_activity.service.ts +++ b/web/core/services/issue/issue_activity.service.ts @@ -1,6 +1,5 @@ -import { EIssueServiceType } from "@plane/constants"; +import { EIssueServiceType, API_BASE_URL } from "@plane/constants"; import { TIssueActivity, TIssueServiceType } from "@plane/types"; -import { API_BASE_URL } from "@/helpers/common.helper"; import { APIService } from "@/services/api.service"; // types // helper diff --git a/web/core/services/issue/issue_archive.service.ts b/web/core/services/issue/issue_archive.service.ts index b86886ca9..ae4eefd63 100644 --- a/web/core/services/issue/issue_archive.service.ts +++ b/web/core/services/issue/issue_archive.service.ts @@ -1,6 +1,5 @@ -import { EIssueServiceType } from "@plane/constants"; +import { EIssueServiceType, API_BASE_URL } from "@plane/constants"; import { TIssue, TIssueServiceType } from "@plane/types"; -import { API_BASE_URL } from "@/helpers/common.helper"; import { APIService } from "@/services/api.service"; // types // constants diff --git a/web/core/services/issue/issue_attachment.service.ts b/web/core/services/issue/issue_attachment.service.ts index 322e8c2c6..46ef780c2 100644 --- a/web/core/services/issue/issue_attachment.service.ts +++ b/web/core/services/issue/issue_attachment.service.ts @@ -1,10 +1,9 @@ import { AxiosRequestConfig } from "axios"; -import { EIssueServiceType } from "@plane/constants"; +import { EIssueServiceType, API_BASE_URL } from "@plane/constants"; // plane types import { TIssueAttachment, TIssueAttachmentUploadResponse, TIssueServiceType } from "@plane/types"; // helpers -import { API_BASE_URL } from "@/helpers/common.helper"; -import { generateFileUploadPayload, getFileMetaDataForUpload } from "@/helpers/file.helper"; +import { generateFileUploadPayload, getFileMetaDataForUpload } from "@plane/utils"; // services import { APIService } from "@/services/api.service"; import { FileUploadService } from "@/services/file-upload.service"; diff --git a/web/core/services/issue/issue_comment.service.ts b/web/core/services/issue/issue_comment.service.ts index 8a55f49a1..dc559c69a 100644 --- a/web/core/services/issue/issue_comment.service.ts +++ b/web/core/services/issue/issue_comment.service.ts @@ -1,9 +1,8 @@ -import { EIssueServiceType } from "@plane/constants"; // plane types +import { EIssueServiceType, API_BASE_URL } from "@plane/constants"; import { TFileSignedURLResponse, TIssueComment, TIssueServiceType } from "@plane/types"; // helpers -import { API_BASE_URL } from "@/helpers/common.helper"; -import { generateFileUploadPayload, getFileMetaDataForUpload } from "@/helpers/file.helper"; +import { generateFileUploadPayload, getFileMetaDataForUpload } from "@plane/utils"; // services import { APIService } from "@/services/api.service"; import { FileUploadService } from "@/services/file-upload.service"; diff --git a/web/core/services/issue/issue_draft.service.ts b/web/core/services/issue/issue_draft.service.ts index 919031285..ebac86205 100644 --- a/web/core/services/issue/issue_draft.service.ts +++ b/web/core/services/issue/issue_draft.service.ts @@ -1,5 +1,5 @@ +import { API_BASE_URL } from "@plane/constants"; import { TIssue, TIssuesResponse } from "@plane/types"; -import { API_BASE_URL } from "@/helpers/common.helper"; import { APIService } from "@/services/api.service"; // helpers diff --git a/web/core/services/issue/issue_label.service.ts b/web/core/services/issue/issue_label.service.ts index 7a685218c..21a5531fc 100644 --- a/web/core/services/issue/issue_label.service.ts +++ b/web/core/services/issue/issue_label.service.ts @@ -1,5 +1,5 @@ +import { API_BASE_URL } from "@plane/constants"; import { IIssueLabel } from "@plane/types"; -import { API_BASE_URL } from "@/helpers/common.helper"; // services import { APIService } from "@/services/api.service"; // types diff --git a/web/core/services/issue/issue_reaction.service.ts b/web/core/services/issue/issue_reaction.service.ts index 39fc8406a..2cd6e9f7f 100644 --- a/web/core/services/issue/issue_reaction.service.ts +++ b/web/core/services/issue/issue_reaction.service.ts @@ -1,6 +1,5 @@ -import { EIssueServiceType } from "@plane/constants"; +import { EIssueServiceType, API_BASE_URL } from "@plane/constants"; import { type TIssueCommentReaction, type TIssueReaction, type TIssueServiceType } from "@plane/types"; -import { API_BASE_URL } from "@/helpers/common.helper"; // services import { APIService } from "@/services/api.service"; // types diff --git a/web/core/services/issue/issue_relation.service.ts b/web/core/services/issue/issue_relation.service.ts index 2168cdb44..86aa8800b 100644 --- a/web/core/services/issue/issue_relation.service.ts +++ b/web/core/services/issue/issue_relation.service.ts @@ -1,6 +1,6 @@ +import { API_BASE_URL } from "@plane/constants"; import type { TIssueRelation, TIssue } from "@plane/types"; // helpers -import { API_BASE_URL } from "@/helpers/common.helper"; // Plane-web import { TIssueRelationTypes } from "@/plane-web/types"; // services diff --git a/web/core/services/issue/work_item_version.service.ts b/web/core/services/issue/work_item_version.service.ts index cf36842fa..84a034124 100644 --- a/web/core/services/issue/work_item_version.service.ts +++ b/web/core/services/issue/work_item_version.service.ts @@ -1,12 +1,11 @@ // plane imports -import { EIssueServiceType } from "@plane/constants"; +import { EIssueServiceType, API_BASE_URL } from "@plane/constants"; import { type TDescriptionVersionsListResponse, type TDescriptionVersionDetails, type TIssueServiceType, } from "@plane/types"; // helpers -import { API_BASE_URL } from "@/helpers/common.helper"; // services import { APIService } from "@/services/api.service"; diff --git a/web/core/services/issue/workspace_draft.service.ts b/web/core/services/issue/workspace_draft.service.ts index d1d1ff176..bccfb213e 100644 --- a/web/core/services/issue/workspace_draft.service.ts +++ b/web/core/services/issue/workspace_draft.service.ts @@ -1,6 +1,6 @@ +import { API_BASE_URL } from "@plane/constants"; import { TIssue, TWorkspaceDraftIssue, TWorkspaceDraftPaginationInfo } from "@plane/types"; // helpers -import { API_BASE_URL } from "@/helpers/common.helper"; // services import { APIService } from "@/services/api.service"; diff --git a/web/core/services/issue_filter.service.ts b/web/core/services/issue_filter.service.ts index 3e288bb1a..d6f67b108 100644 --- a/web/core/services/issue_filter.service.ts +++ b/web/core/services/issue_filter.service.ts @@ -1,6 +1,6 @@ // services +import { API_BASE_URL } from "@plane/constants"; import type { IIssueFiltersResponse } from "@plane/types"; -import { API_BASE_URL } from "@/helpers/common.helper"; import { APIService } from "@/services/api.service"; // types diff --git a/web/core/services/module.service.ts b/web/core/services/module.service.ts index 89ecc1726..cf67fcf94 100644 --- a/web/core/services/module.service.ts +++ b/web/core/services/module.service.ts @@ -1,7 +1,7 @@ // types +import { API_BASE_URL } from "@plane/constants"; import type { IModule, ILinkDetails, ModuleLink, TIssuesResponse } from "@plane/types"; // services -import { API_BASE_URL } from "@/helpers/common.helper"; import { APIService } from "@/services/api.service"; export class ModuleService extends APIService { diff --git a/web/core/services/module_archive.service.ts b/web/core/services/module_archive.service.ts index c74f4c640..a53e85aea 100644 --- a/web/core/services/module_archive.service.ts +++ b/web/core/services/module_archive.service.ts @@ -1,7 +1,7 @@ // type +import { API_BASE_URL } from "@plane/constants"; import { IModule } from "@plane/types"; // helpers -import { API_BASE_URL } from "@/helpers/common.helper"; // services import { APIService } from "@/services/api.service"; diff --git a/web/core/services/page/project-page-version.service.ts b/web/core/services/page/project-page-version.service.ts index 05732e3d2..2f973f0fa 100644 --- a/web/core/services/page/project-page-version.service.ts +++ b/web/core/services/page/project-page-version.service.ts @@ -1,7 +1,7 @@ // plane types +import { API_BASE_URL } from "@plane/constants"; import { TPageVersion } from "@plane/types"; // helpers -import { API_BASE_URL } from "@/helpers/common.helper"; // services import { APIService } from "@/services/api.service"; diff --git a/web/core/services/page/project-page.service.ts b/web/core/services/page/project-page.service.ts index cafee7621..5d5326512 100644 --- a/web/core/services/page/project-page.service.ts +++ b/web/core/services/page/project-page.service.ts @@ -1,7 +1,7 @@ // types +import { API_BASE_URL } from "@plane/constants"; import { TDocumentPayload, TPage } from "@plane/types"; // helpers -import { API_BASE_URL } from "@/helpers/common.helper"; // services import { APIService } from "@/services/api.service"; import { FileUploadService } from "@/services/file-upload.service"; diff --git a/web/core/services/project/project-archive.service.ts b/web/core/services/project/project-archive.service.ts index 5fdca54b6..8e0d87773 100644 --- a/web/core/services/project/project-archive.service.ts +++ b/web/core/services/project/project-archive.service.ts @@ -1,5 +1,5 @@ // helpers -import { API_BASE_URL } from "@/helpers/common.helper"; +import { API_BASE_URL } from "@plane/constants"; // services import { APIService } from "@/services/api.service"; diff --git a/web/core/services/project/project-export.service.ts b/web/core/services/project/project-export.service.ts index c94383d15..a1232c953 100644 --- a/web/core/services/project/project-export.service.ts +++ b/web/core/services/project/project-export.service.ts @@ -1,4 +1,4 @@ -import { API_BASE_URL } from "@/helpers/common.helper"; +import { API_BASE_URL } from "@plane/constants"; import { APIService } from "@/services/api.service"; // helpers diff --git a/web/core/services/project/project-member.service.ts b/web/core/services/project/project-member.service.ts index 6f07c5778..ce5a19a57 100644 --- a/web/core/services/project/project-member.service.ts +++ b/web/core/services/project/project-member.service.ts @@ -1,6 +1,6 @@ // types +import { API_BASE_URL } from "@plane/constants"; import type { IProjectBulkAddFormData, TProjectMembership } from "@plane/types"; -import { API_BASE_URL } from "@/helpers/common.helper"; // services import { APIService } from "@/services/api.service"; diff --git a/web/core/services/project/project-publish.service.ts b/web/core/services/project/project-publish.service.ts index 134cc4b5f..edbe3fbb8 100644 --- a/web/core/services/project/project-publish.service.ts +++ b/web/core/services/project/project-publish.service.ts @@ -1,7 +1,7 @@ // types +import { API_BASE_URL } from "@plane/constants"; import { TProjectPublishSettings } from "@plane/types"; // helpers -import { API_BASE_URL } from "@/helpers/common.helper"; // services import { APIService } from "@/services/api.service"; diff --git a/web/core/services/project/project-state.service.ts b/web/core/services/project/project-state.service.ts index 17d3468d8..d74fc1391 100644 --- a/web/core/services/project/project-state.service.ts +++ b/web/core/services/project/project-state.service.ts @@ -1,6 +1,6 @@ // services +import { API_BASE_URL } from "@plane/constants"; import type { IState } from "@plane/types"; -import { API_BASE_URL } from "@/helpers/common.helper"; import { APIService } from "@/services/api.service"; // helpers // types diff --git a/web/core/services/project/project.service.ts b/web/core/services/project/project.service.ts index fc8ed9442..bf5a2ce2e 100644 --- a/web/core/services/project/project.service.ts +++ b/web/core/services/project/project.service.ts @@ -1,3 +1,4 @@ +import { API_BASE_URL } from "@plane/constants"; import type { GithubRepositoriesResponse, ISearchIssueResponse, @@ -6,7 +7,6 @@ import type { TProjectIssuesSearchParams, } from "@plane/types"; // helpers -import { API_BASE_URL } from "@/helpers/common.helper"; // plane web types import { TProject, TPartialProject } from "@/plane-web/types"; // services diff --git a/web/core/services/sticky.service.ts b/web/core/services/sticky.service.ts index 7e31b311b..1dfbc2c92 100644 --- a/web/core/services/sticky.service.ts +++ b/web/core/services/sticky.service.ts @@ -1,7 +1,6 @@ // helpers -import { STICKIES_PER_PAGE } from "@plane/constants"; +import { STICKIES_PER_PAGE, API_BASE_URL } from "@plane/constants"; import { TSticky } from "@plane/types"; -import { API_BASE_URL } from "@/helpers/common.helper"; // services import { APIService } from "@/services/api.service"; diff --git a/web/core/services/timezone.service.ts b/web/core/services/timezone.service.ts index d19e7e964..4bcee3f7b 100644 --- a/web/core/services/timezone.service.ts +++ b/web/core/services/timezone.service.ts @@ -1,6 +1,6 @@ +import { API_BASE_URL } from "@plane/constants"; import { TTimezones } from "@plane/types"; // helpers -import { API_BASE_URL } from "@/helpers/common.helper"; // api services import { APIService } from "@/services/api.service"; diff --git a/web/core/services/user.service.ts b/web/core/services/user.service.ts index c888dfa67..585c88b6c 100644 --- a/web/core/services/user.service.ts +++ b/web/core/services/user.service.ts @@ -1,4 +1,5 @@ // services +import { API_BASE_URL } from "@plane/constants"; import type { TIssue, IUser, @@ -11,7 +12,6 @@ import type { TIssuesResponse, TUserProfile, } from "@plane/types"; -import { API_BASE_URL } from "@/helpers/common.helper"; import { APIService } from "@/services/api.service"; // types // helpers diff --git a/web/core/services/webhook.service.ts b/web/core/services/webhook.service.ts index f85d4966a..408baac80 100644 --- a/web/core/services/webhook.service.ts +++ b/web/core/services/webhook.service.ts @@ -1,6 +1,6 @@ // api services +import { API_BASE_URL } from "@plane/constants"; import { IWebhook } from "@plane/types"; -import { API_BASE_URL } from "@/helpers/common.helper"; import { APIService } from "@/services/api.service"; // helpers // types diff --git a/web/core/services/workspace-notification.service.ts b/web/core/services/workspace-notification.service.ts index 933b82063..640fa99a0 100644 --- a/web/core/services/workspace-notification.service.ts +++ b/web/core/services/workspace-notification.service.ts @@ -1,5 +1,6 @@ /* eslint-disable no-useless-catch */ +import { API_BASE_URL } from "@plane/constants"; import type { TNotificationPaginatedInfo, TNotificationPaginatedInfoQueryParams, @@ -7,7 +8,6 @@ import type { TUnreadNotificationsCount, } from "@plane/types"; // helpers -import { API_BASE_URL } from "@/helpers/common.helper"; // services import { APIService } from "@/services/api.service"; diff --git a/web/core/store/cycle.store.ts b/web/core/store/cycle.store.ts index d08d8e935..c23af6850 100644 --- a/web/core/store/cycle.store.ts +++ b/web/core/store/cycle.store.ts @@ -13,10 +13,8 @@ import { TCycleDistribution, TCycleEstimateType, } from "@plane/types"; +import { orderCycles, shouldFilterCycle, getDate, DistributionUpdates, updateDistribution } from "@plane/utils"; // helpers -import { orderCycles, shouldFilterCycle } from "@/helpers/cycle.helper"; -import { getDate } from "@/helpers/date-time.helper"; -import { DistributionUpdates, updateDistribution } from "@/helpers/distribution-update.helper"; // services import { syncIssuesWithDeletedCycles } from "@/local-db/utils/load-workspace"; import { CycleService } from "@/services/cycle.service"; diff --git a/web/core/store/inbox/inbox-issue.store.ts b/web/core/store/inbox/inbox-issue.store.ts index 8e193435e..40421c30c 100644 --- a/web/core/store/inbox/inbox-issue.store.ts +++ b/web/core/store/inbox/inbox-issue.store.ts @@ -1,10 +1,9 @@ import clone from "lodash/clone"; import set from "lodash/set"; import { makeObservable, observable, runInAction, action } from "mobx"; -import { TInboxIssue, TInboxIssueStatus, EInboxIssueSource } from "@plane/constants"; +import { TInboxIssue, TInboxIssueStatus, EInboxIssueSource, EInboxIssueStatus } from "@plane/constants"; import { TIssue, TInboxDuplicateIssueDetails } from "@plane/types"; // helpers -import { EInboxIssueStatus } from "@/helpers/inbox.helper"; // local db import { addIssueToPersistanceLayer } from "@/local-db/utils/utils"; // services diff --git a/web/core/store/inbox/project-inbox.store.ts b/web/core/store/inbox/project-inbox.store.ts index 996f85d0d..33e974e4e 100644 --- a/web/core/store/inbox/project-inbox.store.ts +++ b/web/core/store/inbox/project-inbox.store.ts @@ -4,16 +4,16 @@ import omit from "lodash/omit"; import set from "lodash/set"; import { action, computed, makeObservable, observable, runInAction } from "mobx"; import { computedFn } from "mobx-utils"; +import { TInboxIssue, TInboxIssueCurrentTab, EInboxIssueCurrentTab, EInboxIssueStatus, EPastDurationFilters } from "@plane/constants"; // types -import { TInboxIssue, TInboxIssueCurrentTab } from "@plane/constants"; import { TInboxIssueFilter, TInboxIssueSorting, TInboxIssuePaginationInfo, TInboxIssueSortingOrderByQueryParam, } from "@plane/types"; +import { getCustomDates} from "@plane/utils"; // helpers -import { EInboxIssueCurrentTab, EInboxIssueStatus, EPastDurationFilters, getCustomDates } from "@/helpers/inbox.helper"; // services import { InboxIssueService } from "@/services/inbox"; // root store diff --git a/web/core/store/issue/archived/filter.store.ts b/web/core/store/issue/archived/filter.store.ts index d9cbb9ab1..3c171add1 100644 --- a/web/core/store/issue/archived/filter.store.ts +++ b/web/core/store/issue/archived/filter.store.ts @@ -15,7 +15,7 @@ import { TIssueParams, IssuePaginationOptions, } from "@plane/types"; -import { handleIssueQueryParamsByLayout } from "@/helpers/issue.helper"; +import { handleIssueQueryParamsByLayout } from "@plane/utils"; import { IssueFiltersService } from "@/services/issue_filter.service"; import { IBaseIssueFilterStore, IssueFilterHelperStore } from "../helpers/issue-filter-helper.store"; // helpers diff --git a/web/core/store/issue/cycle/filter.store.ts b/web/core/store/issue/cycle/filter.store.ts index c8a0bc0fb..c335c5a53 100644 --- a/web/core/store/issue/cycle/filter.store.ts +++ b/web/core/store/issue/cycle/filter.store.ts @@ -13,7 +13,7 @@ import { TIssueParams, IssuePaginationOptions, } from "@plane/types"; -import { handleIssueQueryParamsByLayout } from "@/helpers/issue.helper"; +import { handleIssueQueryParamsByLayout } from "@plane/utils"; import { IssueFiltersService } from "@/services/issue_filter.service"; import { IBaseIssueFilterStore, IssueFilterHelperStore } from "../helpers/issue-filter-helper.store"; // helpers diff --git a/web/core/store/issue/cycle/issue.store.ts b/web/core/store/issue/cycle/issue.store.ts index 9bf2c7e16..34917d7d1 100644 --- a/web/core/store/issue/cycle/issue.store.ts +++ b/web/core/store/issue/cycle/issue.store.ts @@ -19,7 +19,7 @@ import { TBulkOperationsPayload, } from "@plane/types"; // helpers -import { getDistributionPathsPostUpdate } from "@/helpers/distribution-update.helper"; +import { getDistributionPathsPostUpdate } from "@plane/utils"; //local import { storage } from "@/lib/local-storage"; import { persistence } from "@/local-db/storage.sqlite"; diff --git a/web/core/store/issue/draft/filter.store.ts b/web/core/store/issue/draft/filter.store.ts index 7b06262c9..45e0855c8 100644 --- a/web/core/store/issue/draft/filter.store.ts +++ b/web/core/store/issue/draft/filter.store.ts @@ -15,7 +15,7 @@ import { TIssueParams, IssuePaginationOptions, } from "@plane/types"; -import { handleIssueQueryParamsByLayout } from "@/helpers/issue.helper"; +import { handleIssueQueryParamsByLayout } from "@plane/utils"; import { IssueFiltersService } from "@/services/issue_filter.service"; import { IBaseIssueFilterStore, IssueFilterHelperStore } from "../helpers/issue-filter-helper.store"; // helpers diff --git a/web/core/store/issue/helpers/base-issues-utils.ts b/web/core/store/issue/helpers/base-issues-utils.ts index 331715949..1ca57d6cd 100644 --- a/web/core/store/issue/helpers/base-issues-utils.ts +++ b/web/core/store/issue/helpers/base-issues-utils.ts @@ -15,7 +15,7 @@ import { TIssueGroupByOptions, TIssueOrderByOptions, } from "@plane/types"; -import { checkDateCriteria, convertToISODateString, parseDateFilter } from "@/helpers/date-time.helper"; +import { checkDateCriteria, convertToISODateString, parseDateFilter } from "@plane/utils"; import { store } from "@/lib/store-context"; import { EIssueGroupedAction, ISSUE_GROUP_BY_KEY } from "./base-issues.store"; diff --git a/web/core/store/issue/helpers/base-issues.store.ts b/web/core/store/issue/helpers/base-issues.store.ts index 30ac6fb04..f7dd980d2 100644 --- a/web/core/store/issue/helpers/base-issues.store.ts +++ b/web/core/store/issue/helpers/base-issues.store.ts @@ -29,11 +29,10 @@ import { TGroupedIssueCount, TPaginationData, TBulkOperationsPayload, + IBlockUpdateDependencyData, } from "@plane/types"; -// components -import { IBlockUpdateDependencyData } from "@/components/gantt-chart"; // helpers -import { convertToISODateString } from "@/helpers/date-time.helper"; +import { convertToISODateString } from "@plane/utils"; // local-db import { SPECIAL_ORDER_BY } from "@/local-db/utils/query-constructor"; import { updatePersistentLayer } from "@/local-db/utils/utils"; diff --git a/web/core/store/issue/helpers/issue-filter-helper.store.ts b/web/core/store/issue/helpers/issue-filter-helper.store.ts index c0c31d160..6284cfe10 100644 --- a/web/core/store/issue/helpers/issue-filter-helper.store.ts +++ b/web/core/store/issue/helpers/issue-filter-helper.store.ts @@ -20,7 +20,7 @@ import { TStaticViewTypes, } from "@plane/types"; // helpers -import { getComputedDisplayFilters, getComputedDisplayProperties } from "@/helpers/issue.helper"; +import { getComputedDisplayFilters, getComputedDisplayProperties } from "@plane/utils"; // lib import { storage } from "@/lib/local-storage"; diff --git a/web/core/store/issue/issue-details/comment_reaction.store.ts b/web/core/store/issue/issue-details/comment_reaction.store.ts index a8865f798..ae679990e 100644 --- a/web/core/store/issue/issue-details/comment_reaction.store.ts +++ b/web/core/store/issue/issue-details/comment_reaction.store.ts @@ -8,7 +8,7 @@ import { action, makeObservable, observable, runInAction } from "mobx"; // types // helpers import { TIssueCommentReaction, TIssueCommentReactionIdMap, TIssueCommentReactionMap } from "@plane/types"; -import { groupReactions } from "@/helpers/emoji.helper"; +import { groupReactions } from "@plane/utils"; import { IssueReactionService } from "@/services/issue"; import { IIssueDetail } from "./root.store"; diff --git a/web/core/store/issue/issue-details/reaction.store.ts b/web/core/store/issue/issue-details/reaction.store.ts index 5fd0e2245..f7eda21cc 100644 --- a/web/core/store/issue/issue-details/reaction.store.ts +++ b/web/core/store/issue/issue-details/reaction.store.ts @@ -8,7 +8,7 @@ import { action, makeObservable, observable, runInAction } from "mobx"; // types // helpers import { TIssueReaction, TIssueReactionMap, TIssueReactionIdMap, TIssue, TIssueServiceType } from "@plane/types"; -import { groupReactions } from "@/helpers/emoji.helper"; +import { groupReactions } from "@plane/utils"; import { IssueReactionService } from "@/services/issue"; import { IIssueDetail } from "./root.store"; diff --git a/web/core/store/issue/issue.store.ts b/web/core/store/issue/issue.store.ts index 56e8ed22a..871abd5d1 100644 --- a/web/core/store/issue/issue.store.ts +++ b/web/core/store/issue/issue.store.ts @@ -6,7 +6,7 @@ import { computedFn } from "mobx-utils"; // types import { TIssue } from "@plane/types"; // helpers -import { getCurrentDateTimeInISO } from "@/helpers/date-time.helper"; +import { getCurrentDateTimeInISO } from "@plane/utils"; import { rootStore } from "@/lib/store-context"; // services import { deleteIssueFromLocal } from "@/local-db/utils/load-issues"; diff --git a/web/core/store/issue/issue_calendar_view.store.ts b/web/core/store/issue/issue_calendar_view.store.ts index 4757fb5b3..ad1489fba 100644 --- a/web/core/store/issue/issue_calendar_view.store.ts +++ b/web/core/store/issue/issue_calendar_view.store.ts @@ -2,11 +2,9 @@ import { observable, action, makeObservable, runInAction, computed } from "mobx" // helpers import { computedFn } from "mobx-utils"; -import { ICalendarPayload, ICalendarWeek } from "@/components/issues"; -import { generateCalendarData } from "@/helpers/calendar.helper"; +import { ICalendarPayload, ICalendarWeek } from "@plane/types"; +import { generateCalendarData, getWeekNumberOfDate } from "@plane/utils"; // types -import { getWeekNumberOfDate } from "@/helpers/date-time.helper"; - export interface ICalendarStore { calendarFilters: { activeMonthDate: Date; diff --git a/web/core/store/issue/issue_gantt_view.store.ts b/web/core/store/issue/issue_gantt_view.store.ts index 974c78b72..d39b0a273 100644 --- a/web/core/store/issue/issue_gantt_view.store.ts +++ b/web/core/store/issue/issue_gantt_view.store.ts @@ -1,7 +1,7 @@ import { action, makeObservable, observable, runInAction } from "mobx"; import { computedFn } from "mobx-utils"; // helpers -import { ChartDataType, TGanttViews } from "@/components/gantt-chart"; +import type { ChartDataType, TGanttViews } from "@plane/types"; import { currentViewDataWithView } from "@/components/gantt-chart/data"; // types diff --git a/web/core/store/issue/module/filter.store.ts b/web/core/store/issue/module/filter.store.ts index e1bd1b070..8516af7f3 100644 --- a/web/core/store/issue/module/filter.store.ts +++ b/web/core/store/issue/module/filter.store.ts @@ -13,7 +13,7 @@ import { TIssueParams, IssuePaginationOptions, } from "@plane/types"; -import { handleIssueQueryParamsByLayout } from "@/helpers/issue.helper"; +import { handleIssueQueryParamsByLayout } from "@plane/utils"; import { IssueFiltersService } from "@/services/issue_filter.service"; import { IBaseIssueFilterStore, IssueFilterHelperStore } from "../helpers/issue-filter-helper.store"; // helpers diff --git a/web/core/store/issue/module/issue.store.ts b/web/core/store/issue/module/issue.store.ts index 8d9dc6be9..345fdf61f 100644 --- a/web/core/store/issue/module/issue.store.ts +++ b/web/core/store/issue/module/issue.store.ts @@ -9,7 +9,7 @@ import { TBulkOperationsPayload, } from "@plane/types"; // helpers -import { getDistributionPathsPostUpdate } from "@/helpers/distribution-update.helper"; +import { getDistributionPathsPostUpdate } from "@plane/utils"; import { BaseIssuesStore, IBaseIssuesStore } from "../helpers/base-issues.store"; // import { IIssueRootStore } from "../root.store"; diff --git a/web/core/store/issue/profile/filter.store.ts b/web/core/store/issue/profile/filter.store.ts index 5ba808328..4938c9c69 100644 --- a/web/core/store/issue/profile/filter.store.ts +++ b/web/core/store/issue/profile/filter.store.ts @@ -13,7 +13,7 @@ import { TIssueParams, IssuePaginationOptions, } from "@plane/types"; -import { handleIssueQueryParamsByLayout } from "@/helpers/issue.helper"; +import { handleIssueQueryParamsByLayout } from "@plane/utils"; import { IssueFiltersService } from "@/services/issue_filter.service"; import { IBaseIssueFilterStore, IssueFilterHelperStore } from "../helpers/issue-filter-helper.store"; // helpers diff --git a/web/core/store/issue/project-views/filter.store.ts b/web/core/store/issue/project-views/filter.store.ts index a25a53cff..a8ccbfa3e 100644 --- a/web/core/store/issue/project-views/filter.store.ts +++ b/web/core/store/issue/project-views/filter.store.ts @@ -13,7 +13,7 @@ import { TIssueParams, IssuePaginationOptions, } from "@plane/types"; -import { handleIssueQueryParamsByLayout } from "@/helpers/issue.helper"; +import { handleIssueQueryParamsByLayout } from "@plane/utils"; // services import { ViewService } from "@/plane-web/services"; import { IBaseIssueFilterStore, IssueFilterHelperStore } from "../helpers/issue-filter-helper.store"; diff --git a/web/core/store/issue/project/filter.store.ts b/web/core/store/issue/project/filter.store.ts index 4d194e2f1..31dd56612 100644 --- a/web/core/store/issue/project/filter.store.ts +++ b/web/core/store/issue/project/filter.store.ts @@ -13,7 +13,7 @@ import { TIssueParams, IssuePaginationOptions, } from "@plane/types"; -import { handleIssueQueryParamsByLayout } from "@/helpers/issue.helper"; +import { handleIssueQueryParamsByLayout } from "@plane/utils"; import { IssueFiltersService } from "@/services/issue_filter.service"; import { IBaseIssueFilterStore, IssueFilterHelperStore } from "../helpers/issue-filter-helper.store"; // helpers diff --git a/web/core/store/issue/workspace-draft/filter.store.ts b/web/core/store/issue/workspace-draft/filter.store.ts index 7d504596f..05e5ea511 100644 --- a/web/core/store/issue/workspace-draft/filter.store.ts +++ b/web/core/store/issue/workspace-draft/filter.store.ts @@ -14,7 +14,7 @@ import { TIssueParams, IssuePaginationOptions, } from "@plane/types"; -import { handleIssueQueryParamsByLayout } from "@/helpers/issue.helper"; +import { handleIssueQueryParamsByLayout } from "@plane/utils"; import { IssueFiltersService } from "@/services/issue_filter.service"; import { IBaseIssueFilterStore, IssueFilterHelperStore } from "../helpers/issue-filter-helper.store"; // helpers diff --git a/web/core/store/issue/workspace-draft/issue.store.ts b/web/core/store/issue/workspace-draft/issue.store.ts index 6783b89b1..3aebd98b6 100644 --- a/web/core/store/issue/workspace-draft/issue.store.ts +++ b/web/core/store/issue/workspace-draft/issue.store.ts @@ -22,7 +22,7 @@ import { } from "@plane/types"; // constants // helpers -import { getCurrentDateTimeInISO, convertToISODateString } from "@/helpers/date-time.helper"; +import { getCurrentDateTimeInISO, convertToISODateString } from "@plane/utils"; // local-db import { addIssueToPersistanceLayer } from "@/local-db/utils/utils"; // services diff --git a/web/core/store/issue/workspace/filter.store.ts b/web/core/store/issue/workspace/filter.store.ts index ffc36e650..2a98ba03c 100644 --- a/web/core/store/issue/workspace/filter.store.ts +++ b/web/core/store/issue/workspace/filter.store.ts @@ -18,7 +18,7 @@ import { IssuePaginationOptions, } from "@plane/types"; // services -import { handleIssueQueryParamsByLayout } from "@/helpers/issue.helper"; +import { handleIssueQueryParamsByLayout } from "@plane/utils"; import { WorkspaceService } from "@/plane-web/services"; import { IBaseIssueFilterStore, IssueFilterHelperStore } from "../helpers/issue-filter-helper.store"; // helpers diff --git a/web/core/store/label.store.ts b/web/core/store/label.store.ts index c22059361..688f0b974 100644 --- a/web/core/store/label.store.ts +++ b/web/core/store/label.store.ts @@ -5,7 +5,7 @@ import { computedFn } from "mobx-utils"; // types import { IIssueLabel, IIssueLabelTree } from "@plane/types"; // helpers -import { buildTree } from "@/helpers/array.helper"; +import { buildTree } from "@plane/utils"; // services import { syncIssuesWithDeletedLabels } from "@/local-db/utils/load-workspace"; import { IssueLabelService } from "@/services/issue"; diff --git a/web/core/store/module.store.ts b/web/core/store/module.store.ts index 5e4c0b294..5592835cc 100644 --- a/web/core/store/module.store.ts +++ b/web/core/store/module.store.ts @@ -6,9 +6,8 @@ import { action, computed, observable, makeObservable, runInAction } from "mobx" import { computedFn } from "mobx-utils"; // types import { IModule, ILinkDetails, TModulePlotType } from "@plane/types"; +import { DistributionUpdates, updateDistribution, orderModules, shouldFilterModule } from "@plane/utils"; // helpers -import { DistributionUpdates, updateDistribution } from "@/helpers/distribution-update.helper"; -import { orderModules, shouldFilterModule } from "@/helpers/module.helper"; // services import { syncIssuesWithDeletedModules } from "@/local-db/utils/load-workspace"; import { ModuleService } from "@/services/module.service"; diff --git a/web/core/store/notifications/workspace-notifications.store.ts b/web/core/store/notifications/workspace-notifications.store.ts index 5e0edd816..ef8d20bf5 100644 --- a/web/core/store/notifications/workspace-notifications.store.ts +++ b/web/core/store/notifications/workspace-notifications.store.ts @@ -15,7 +15,7 @@ import { TUnreadNotificationsCount, } from "@plane/types"; // helpers -import { convertToEpoch } from "@/helpers/date-time.helper"; +import { convertToEpoch } from "@plane/utils"; // services import workspaceNotificationService from "@/services/workspace-notification.service"; // store diff --git a/web/core/store/pages/project-page.store.ts b/web/core/store/pages/project-page.store.ts index 070021bdc..4f519a583 100644 --- a/web/core/store/pages/project-page.store.ts +++ b/web/core/store/pages/project-page.store.ts @@ -6,7 +6,7 @@ import { computedFn } from "mobx-utils"; import { EUserPermissions, EUserProjectRoles } from "@plane/constants"; import { TPage, TPageFilters, TPageNavigationTabs } from "@plane/types"; // helpers -import { filterPagesByPageType, getPageName, orderPages, shouldFilterPage } from "@/helpers/page.helper"; +import { filterPagesByPageType, getPageName, orderPages, shouldFilterPage } from "@plane/utils"; // plane web constants // plane web store import { RootStore } from "@/plane-web/store/root.store"; diff --git a/web/core/store/project-view.store.ts b/web/core/store/project-view.store.ts index d2e911aa6..9e30308d7 100644 --- a/web/core/store/project-view.store.ts +++ b/web/core/store/project-view.store.ts @@ -6,7 +6,7 @@ import { EViewAccess } from "@plane/constants"; import { IProjectView, TPublishViewDetails, TPublishViewSettings, TViewFilters } from "@plane/types"; // constants // helpers -import { getValidatedViewFilters, getViewName, orderViews, shouldFilterView } from "@/helpers/project-views.helpers"; +import { getValidatedViewFilters, getViewName, orderViews, shouldFilterView } from "@plane/utils"; // services import { ViewService } from "@/plane-web/services"; // store diff --git a/web/core/store/project/project.store.ts b/web/core/store/project/project.store.ts index e460b1087..47b888636 100644 --- a/web/core/store/project/project.store.ts +++ b/web/core/store/project/project.store.ts @@ -7,7 +7,7 @@ import { computedFn } from "mobx-utils"; // plane imports import { TFetchStatus, TLoader, TProjectAnalyticsCount, TProjectAnalyticsCountParams } from "@plane/types"; // helpers -import { orderProjects, shouldFilterProject } from "@/helpers/project.helper"; +import { orderProjects, shouldFilterProject } from "@plane/utils"; // services import { TProject, TPartialProject } from "@/plane-web/types/projects"; import { IssueLabelService, IssueService } from "@/services/issue"; diff --git a/web/core/store/state.store.ts b/web/core/store/state.store.ts index 2097bdc47..8f1d324b6 100644 --- a/web/core/store/state.store.ts +++ b/web/core/store/state.store.ts @@ -6,7 +6,7 @@ import { computedFn } from "mobx-utils"; import { STATE_GROUPS } from "@plane/constants"; import { IState } from "@plane/types"; // helpers -import { sortStates } from "@/helpers/state.helper"; +import { sortStates } from "@plane/utils"; // plane web import { syncIssuesWithDeletedStates } from "@/local-db/utils/load-workspace"; import { ProjectStateService } from "@/plane-web/services/project/project-state.service"; diff --git a/web/core/store/user/index.ts b/web/core/store/user/index.ts index 2d4b71e64..27a8a7362 100644 --- a/web/core/store/user/index.ts +++ b/web/core/store/user/index.ts @@ -1,13 +1,12 @@ import cloneDeep from "lodash/cloneDeep"; import set from "lodash/set"; import { action, makeObservable, observable, runInAction, computed } from "mobx"; -// plane imports -import { EUserPermissions } from "@plane/constants"; +import { EUserPermissions, API_BASE_URL } from "@plane/constants"; +// types import { IUser } from "@plane/types"; import { TUserPermissions } from "@plane/types/src/enums"; // helpers -import { API_BASE_URL } from "@/helpers/common.helper"; -// local db +// local import { persistence } from "@/local-db/storage.sqlite"; // plane web imports import { RootStore } from "@/plane-web/store/root.store"; diff --git a/web/ee/constants/estimates.ts b/web/ee/constants/estimates.ts deleted file mode 100644 index 06376ef9d..000000000 --- a/web/ee/constants/estimates.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "ce/constants/estimates"; diff --git a/web/helpers/array.helper.ts b/web/helpers/array.helper.ts deleted file mode 100644 index 028d6c0ea..000000000 --- a/web/helpers/array.helper.ts +++ /dev/null @@ -1,119 +0,0 @@ -import isEmpty from "lodash/isEmpty"; -import { IIssueLabel, IIssueLabelTree } from "@plane/types"; - -export const groupBy = (array: any[], key: string) => { - const innerKey = key.split("."); // split the key by dot - return array.reduce((result, currentValue) => { - const key = innerKey.reduce((obj, i) => obj?.[i], currentValue) ?? "None"; // get the value of the inner key - (result[key] = result[key] || []).push(currentValue); - return result; - }, {}); -}; - -export const orderArrayBy = (orgArray: any[], key: string, ordering: "ascending" | "descending" = "ascending") => { - if (!orgArray || !Array.isArray(orgArray) || orgArray.length === 0) return []; - - const array = [...orgArray]; - - if (key[0] === "-") { - ordering = "descending"; - key = key.slice(1); - } - - const innerKey = key.split("."); // split the key by dot - - return array.sort((a, b) => { - const keyA = innerKey.reduce((obj, i) => obj[i], a); // get the value of the inner key - const keyB = innerKey.reduce((obj, i) => obj[i], b); // get the value of the inner key - if (keyA < keyB) { - return ordering === "ascending" ? -1 : 1; - } - if (keyA > keyB) { - return ordering === "ascending" ? 1 : -1; - } - return 0; - }); -}; - -export const checkDuplicates = (array: any[]) => new Set(array).size !== array.length; - -export const findStringWithMostCharacters = (strings: string[]): string => { - if (!strings || strings.length === 0) return ""; - - return strings.reduce((longestString, currentString) => - currentString.length > longestString.length ? currentString : longestString - ); -}; - -export const checkIfArraysHaveSameElements = (arr1: any[] | null, arr2: any[] | null): boolean => { - if (!arr1 || !arr2) return false; - if (!Array.isArray(arr1) || !Array.isArray(arr2)) return false; - if (arr1.length === 0 && arr2.length === 0) return true; - - return arr1.length === arr2.length && arr1.every((e) => arr2.includes(e)); -}; - -type GroupedItems = { [key: string]: T[] }; - -export const groupByField = (array: T[], field: keyof T): GroupedItems => - array.reduce((grouped: GroupedItems, item: T) => { - const key = String(item[field]); - grouped[key] = (grouped[key] || []).concat(item); - return grouped; - }, {}); - -export const sortByField = (array: any[], field: string): any[] => - array.sort((a, b) => (a[field] < b[field] ? -1 : a[field] > b[field] ? 1 : 0)); - -export const orderGroupedDataByField = (groupedData: GroupedItems, orderBy: keyof T): GroupedItems => { - for (const key in groupedData) { - if (groupedData.hasOwnProperty(key)) { - groupedData[key] = groupedData[key].sort((a, b) => { - if (a[orderBy] < b[orderBy]) return -1; - if (a[orderBy] > b[orderBy]) return 1; - return 0; - }); - } - } - return groupedData; -}; - -export const buildTree = (array: IIssueLabel[], parent = null) => { - const tree: IIssueLabelTree[] = []; - - array.forEach((item: any) => { - if (item.parent === parent) { - const children = buildTree(array, item.id); - item.children = children; - tree.push(item); - } - }); - - return tree; -}; - -/** - * Returns Valid keys from object whose value is not falsy - * @param obj - * @returns - */ -export const getValidKeysFromObject = (obj: any) => { - if (!obj || isEmpty(obj) || typeof obj !== "object" || Array.isArray(obj)) return []; - - return Object.keys(obj).filter((key) => !!obj[key]); -}; - -/** - * Convert an array into an object of keys and boolean strue - * @param arrayStrings - * @returns - */ -export const convertStringArrayToBooleanObject = (arrayStrings: string[]) => { - const obj: { [key: string]: boolean } = {}; - - for (const arrayString of arrayStrings) { - obj[arrayString] = true; - } - - return obj; -}; diff --git a/web/helpers/attachment.helper.ts b/web/helpers/attachment.helper.ts deleted file mode 100644 index 1f9f4f5a3..000000000 --- a/web/helpers/attachment.helper.ts +++ /dev/null @@ -1,32 +0,0 @@ -export const generateFileName = (fileName: string) => { - const date = new Date(); - const timestamp = date.getTime(); - - const _fileName = getFileName(fileName); - const nameWithoutExtension = _fileName.length > 80 ? _fileName.substring(0, 80) : _fileName; - const extension = getFileExtension(fileName); - - return `${nameWithoutExtension}-${timestamp}.${extension}`; -}; - -export const getFileExtension = (filename: string) => filename.slice(((filename.lastIndexOf(".") - 1) >>> 0) + 2); - -export const getFileName = (fileName: string) => { - const dotIndex = fileName.lastIndexOf("."); - - const nameWithoutExtension = fileName.substring(0, dotIndex); - - return nameWithoutExtension; -}; - -export const convertBytesToSize = (bytes: number) => { - let size; - - if (bytes < 1024 * 1024) { - size = Math.round(bytes / 1024) + " KB"; - } else { - size = Math.round(bytes / (1024 * 1024)) + " MB"; - } - - return size; -}; diff --git a/web/helpers/authentication.helper.tsx b/web/helpers/authentication.helper.tsx index 0cb2c80a7..e8ecd592c 100644 --- a/web/helpers/authentication.helper.tsx +++ b/web/helpers/authentication.helper.tsx @@ -1,7 +1,7 @@ import { ReactNode } from "react"; import Link from "next/link"; -// helpers -import { SUPPORT_EMAIL } from "./common.helper"; +// plane imports +import { SUPPORT_EMAIL } from "@plane/constants"; export enum EPageTypes { PUBLIC = "PUBLIC", diff --git a/web/helpers/color.helper.ts b/web/helpers/color.helper.ts deleted file mode 100644 index 14b157a7a..000000000 --- a/web/helpers/color.helper.ts +++ /dev/null @@ -1,144 +0,0 @@ -export type TRgb = { r: number; g: number; b: number }; - -export const hexToRgb = (hex: string): TRgb => { - const r = parseInt(hex.slice(1, 3), 16); - const g = parseInt(hex.slice(3, 5), 16); - const b = parseInt(hex.slice(5, 7), 16); - - return { r, g, b }; -}; - -export const rgbToHex = (rgb: TRgb): string => { - const { r, g, b } = rgb; - - const hexR = r.toString(16).padStart(2, "0"); - const hexG = g.toString(16).padStart(2, "0"); - const hexB = b.toString(16).padStart(2, "0"); - - return `#${hexR}${hexG}${hexB}`; -}; - -/** - * Calculate relative luminance of a color according to WCAG - * @param {Object} rgb - RGB color object with r, g, b properties - * @returns {number} Relative luminance value - */ -export function getLuminance({ r, g, b }: { r: number; g: number; b: number }) { - // Convert RGB to sRGB - const sR = r / 255; - const sG = g / 255; - const sB = b / 255; - - // Convert sRGB to linear RGB with gamma correction - const R = sR <= 0.03928 ? sR / 12.92 : Math.pow((sR + 0.055) / 1.055, 2.4); - const G = sG <= 0.03928 ? sG / 12.92 : Math.pow((sG + 0.055) / 1.055, 2.4); - const B = sB <= 0.03928 ? sB / 12.92 : Math.pow((sB + 0.055) / 1.055, 2.4); - - // Calculate luminance - return 0.2126 * R + 0.7152 * G + 0.0722 * B; -} - -/** - * Calculate contrast ratio between two colors - * @param {Object} rgb1 - First RGB color object - * @param {Object} rgb2 - Second RGB color object - * @returns {number} Contrast ratio between the colors - */ -export function getContrastRatio(rgb1: { r: number; g: number; b: number }, rgb2: { r: number; g: number; b: number }) { - const luminance1 = getLuminance(rgb1); - const luminance2 = getLuminance(rgb2); - - const lighter = Math.max(luminance1, luminance2); - const darker = Math.min(luminance1, luminance2); - - return (lighter + 0.05) / (darker + 0.05); -} - -/** - * Lighten a color by a specified amount - * @param {Object} rgb - RGB color object - * @param {number} amount - Amount to lighten (0-1) - * @returns {Object} Lightened RGB color - */ -export function lightenColor(rgb: { r: number; g: number; b: number }, amount: number) { - return { - r: rgb.r + (255 - rgb.r) * amount, - g: rgb.g + (255 - rgb.g) * amount, - b: rgb.b + (255 - rgb.b) * amount, - }; -} - -/** - * Darken a color by a specified amount - * @param {Object} rgb - RGB color object - * @param {number} amount - Amount to darken (0-1) - * @returns {Object} Darkened RGB color - */ -export function darkenColor(rgb: { r: number; g: number; b: number }, amount: number) { - return { - r: rgb.r * (1 - amount), - g: rgb.g * (1 - amount), - b: rgb.b * (1 - amount), - }; -} - -/** - * Generate appropriate foreground and background colors based on input color - * @param {string} color - Input color in hex format - * @returns {Object} Object containing foreground and background colors in hex format - */ -export function generateIconColors(color: string) { - // Parse input color - const rgbColor = hexToRgb(color); - const luminance = getLuminance(rgbColor); - - // Initialize output colors - let foregroundColor = rgbColor; - - // Constants for color adjustment - const MIN_CONTRAST_RATIO = 3.0; // Minimum acceptable contrast ratio - - // For light colors, use as foreground and darken for background - if (luminance > 0.5) { - // Make sure the foreground color is dark enough for visibility - let adjustedForeground = foregroundColor; - const whiteContrast = getContrastRatio(foregroundColor, { r: 255, g: 255, b: 255 }); - - if (whiteContrast < MIN_CONTRAST_RATIO) { - // Darken the foreground color until it has enough contrast - let darkenAmount = 0.1; - while (darkenAmount <= 0.9) { - adjustedForeground = darkenColor(foregroundColor, darkenAmount); - if (getContrastRatio(adjustedForeground, { r: 255, g: 255, b: 255 }) >= MIN_CONTRAST_RATIO) { - break; - } - darkenAmount += 0.1; - } - foregroundColor = adjustedForeground; - } - } - // For dark colors, use as foreground and lighten for background - else { - // Make sure the foreground color is light enough for visibility - let adjustedForeground = foregroundColor; - const blackContrast = getContrastRatio(foregroundColor, { r: 0, g: 0, b: 0 }); - - if (blackContrast < MIN_CONTRAST_RATIO) { - // Lighten the foreground color until it has enough contrast - let lightenAmount = 0.1; - while (lightenAmount <= 0.9) { - adjustedForeground = lightenColor(foregroundColor, lightenAmount); - if (getContrastRatio(adjustedForeground, { r: 0, g: 0, b: 0 }) >= MIN_CONTRAST_RATIO) { - break; - } - lightenAmount += 0.1; - } - foregroundColor = adjustedForeground; - } - } - - return { - foreground: rgbToHex({ r: foregroundColor.r, g: foregroundColor.g, b: foregroundColor.b }), - background: `rgba(${foregroundColor.r}, ${foregroundColor.g}, ${foregroundColor.b}, 0.25)`, - }; -} diff --git a/web/helpers/command-palette.ts b/web/helpers/command-palette.ts deleted file mode 100644 index 7a4518321..000000000 --- a/web/helpers/command-palette.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "ce/helpers/command-palette"; diff --git a/web/helpers/common.helper.ts b/web/helpers/common.helper.ts deleted file mode 100644 index a17d28563..000000000 --- a/web/helpers/common.helper.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { clsx, type ClassValue } from "clsx"; -import { twMerge } from "tailwind-merge"; - -export const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || ""; - -export const ADMIN_BASE_URL = process.env.NEXT_PUBLIC_ADMIN_BASE_URL || ""; -export const ADMIN_BASE_PATH = process.env.NEXT_PUBLIC_ADMIN_BASE_PATH || ""; - -export const SPACE_BASE_URL = process.env.NEXT_PUBLIC_SPACE_BASE_URL || ""; -export const SPACE_BASE_PATH = process.env.NEXT_PUBLIC_SPACE_BASE_PATH || ""; - -export const LIVE_BASE_URL = process.env.NEXT_PUBLIC_LIVE_BASE_URL || ""; -export const LIVE_BASE_PATH = process.env.NEXT_PUBLIC_LIVE_BASE_PATH || ""; -export const LIVE_URL = `${LIVE_BASE_URL}${LIVE_BASE_PATH}`; - -export const SUPPORT_EMAIL = process.env.NEXT_PUBLIC_SUPPORT_EMAIL || ""; - -export const GOD_MODE_URL = encodeURI(`${ADMIN_BASE_URL}${ADMIN_BASE_PATH}/`); - -export const debounce = (func: any, wait: number, immediate: boolean = false) => { - let timeout: any; - - return function executedFunction(...args: any) { - const later = () => { - timeout = null; - if (!immediate) func(...args); - }; - - const callNow = immediate && !timeout; - - clearTimeout(timeout); - - timeout = setTimeout(later, wait); - - if (callNow) func(...args); - }; -}; - -export const cn = (...inputs: ClassValue[]) => twMerge(clsx(inputs)); - -export const convertRemToPixel = (rem: number): number => rem * 0.9 * 16; - -export const getProgress = (completed: number | undefined, total: number | undefined) => - total && total > 0 ? Math.round(((completed ?? 0) / total) * 100) : 0; diff --git a/web/helpers/dashboard.helper.ts b/web/helpers/dashboard.helper.ts index 239c259e7..f429c415b 100644 --- a/web/helpers/dashboard.helper.ts +++ b/web/helpers/dashboard.helper.ts @@ -4,7 +4,9 @@ import { endOfMonth, endOfWeek, endOfYear, startOfMonth, startOfWeek, startOfYea import { DURATION_FILTER_OPTIONS, EDurationFilters } from "@plane/constants"; import { TIssuesListTypes } from "@plane/types"; // constants -import { renderFormattedDate, renderFormattedPayloadDate } from "./date-time.helper"; +import { renderFormattedDate, renderFormattedPayloadDate } from "@plane/utils"; + +// -------------------- DEPRECATED -------------------- /** * @description returns date range based on the duration filter diff --git a/web/helpers/date-time.helper.ts b/web/helpers/date-time.helper.ts deleted file mode 100644 index cff0da2c2..000000000 --- a/web/helpers/date-time.helper.ts +++ /dev/null @@ -1,477 +0,0 @@ -import { differenceInDays, format, formatDistanceToNow, isAfter, isEqual, isValid, parseISO } from "date-fns"; -import isNumber from "lodash/isNumber"; - -// Format Date Helpers -/** - * @returns {string | null} formatted date in the desired format or platform default format (MMM dd, yyyy) - * @description Returns date in the formatted format - * @param {Date | string} date - * @param {string} formatToken (optional) // default MMM dd, yyyy - * @example renderFormattedDate("2024-01-01", "MM-DD-YYYY") // Jan 01, 2024 - * @example renderFormattedDate("2024-01-01") // Jan 01, 2024 - */ -export const renderFormattedDate = ( - date: string | Date | undefined | null, - formatToken: string = "MMM dd, yyyy" -): string | undefined => { - // Parse the date to check if it is valid - const parsedDate = getDate(date); - // return if undefined - if (!parsedDate) return; - // Check if the parsed date is valid before formatting - if (!isValid(parsedDate)) return; // Return null for invalid dates - let formattedDate; - try { - // Format the date in the format provided or default format (MMM dd, yyyy) - formattedDate = format(parsedDate, formatToken); - } catch (e) { - // Format the date in format (MMM dd, yyyy) in case of any error - formattedDate = format(parsedDate, "MMM dd, yyyy"); - } - return formattedDate; -}; - -/** - * @returns {string} formatted date in the format of MMM dd - * @description Returns date in the formatted format - * @param {string | Date} date - * @example renderShortDateFormat("2024-01-01") // Jan 01 - */ -export const renderFormattedDateWithoutYear = (date: string | Date): string => { - // Parse the date to check if it is valid - const parsedDate = getDate(date); - // return if undefined - if (!parsedDate) return ""; - // Check if the parsed date is valid before formatting - if (!isValid(parsedDate)) return ""; // Return empty string for invalid dates - // Format the date in short format (MMM dd) - const formattedDate = format(parsedDate, "MMM dd"); - return formattedDate; -}; - -/** - * @returns {string | null} formatted date in the format of yyyy-mm-dd to be used in payload - * @description Returns date in the formatted format to be used in payload - * @param {Date | string} date - * @example renderFormattedPayloadDate("Jan 01, 20224") // "2024-01-01" - */ -export const renderFormattedPayloadDate = (date: Date | string | undefined | null): string | undefined => { - // Parse the date to check if it is valid - const parsedDate = getDate(date); - // return if undefined - if (!parsedDate) return; - // Check if the parsed date is valid before formatting - if (!isValid(parsedDate)) return; // Return null for invalid dates - // Format the date in payload format (yyyy-mm-dd) - const formattedDate = format(parsedDate, "yyyy-MM-dd"); - return formattedDate; -}; - -// Format Time Helpers -/** - * @returns {string} formatted date in the format of hh:mm a or HH:mm - * @description Returns date in 12 hour format if in12HourFormat is true else 24 hour format - * @param {string | Date} date - * @param {boolean} timeFormat (optional) // default 24 hour - * @example renderFormattedTime("2024-01-01 13:00:00") // 13:00 - * @example renderFormattedTime("2024-01-01 13:00:00", "12-hour") // 01:00 PM - */ -export const renderFormattedTime = (date: string | Date, timeFormat: "12-hour" | "24-hour" = "24-hour"): string => { - // Parse the date to check if it is valid - const parsedDate = new Date(date); - // return if undefined - if (!parsedDate) return ""; - // Check if the parsed date is valid - if (!isValid(parsedDate)) return ""; // Return empty string for invalid dates - // Format the date in 12 hour format if in12HourFormat is true - if (timeFormat === "12-hour") { - const formattedTime = format(parsedDate, "hh:mm a"); - return formattedTime; - } - // Format the date in 24 hour format - const formattedTime = format(parsedDate, "HH:mm"); - return formattedTime; -}; - -// Date Difference Helpers -/** - * @returns {number} total number of days in range - * @description Returns total number of days in range - * @param {string} startDate - * @param {string} endDate - * @param {boolean} inclusive - * @example checkIfStringIsDate("2021-01-01", "2021-01-08") // 8 - */ -export const findTotalDaysInRange = ( - startDate: Date | string | undefined | null, - endDate: Date | string | undefined | null, - inclusive: boolean = true -): number | undefined => { - // Parse the dates to check if they are valid - const parsedStartDate = getDate(startDate); - const parsedEndDate = getDate(endDate); - // return if undefined - if (!parsedStartDate || !parsedEndDate) return; - // Check if the parsed dates are valid before calculating the difference - if (!isValid(parsedStartDate) || !isValid(parsedEndDate)) return 0; // Return 0 for invalid dates - // Calculate the difference in days - const diffInDays = differenceInDays(parsedEndDate, parsedStartDate); - // Return the difference in days based on inclusive flag - return inclusive ? diffInDays + 1 : diffInDays; -}; - -/** - * Add number of days to the provided date and return a resulting new date - * @param startDate - * @param numberOfDays - * @returns - */ -export const addDaysToDate = (startDate: Date | string | undefined | null, numberOfDays: number) => { - // Parse the dates to check if they are valid - const parsedStartDate = getDate(startDate); - - // return if undefined - if (!parsedStartDate) return; - - const newDate = new Date(parsedStartDate); - newDate.setDate(newDate.getDate() + numberOfDays); - - return newDate; -}; - -/** - * @returns {number} number of days left from today - * @description Returns number of days left from today - * @param {string | Date} date - * @param {boolean} inclusive (optional) // default true - * @example findHowManyDaysLeft("2024-01-01") // 3 - */ -export const findHowManyDaysLeft = ( - date: Date | string | undefined | null, - inclusive: boolean = true -): number | undefined => { - if (!date) return undefined; - // Pass the date to findTotalDaysInRange function to find the total number of days in range from today - return findTotalDaysInRange(new Date(), date, inclusive); -}; - -// Time Difference Helpers -/** - * @returns {string} formatted date in the form of amount of time passed since the event happened - * @description Returns time passed since the event happened - * @param {string | Date} time - * @example calculateTimeAgo("2023-01-01") // 1 year ago - */ -export const calculateTimeAgo = (time: string | number | Date | null): string => { - if (!time) return ""; - // Parse the time to check if it is valid - const parsedTime = typeof time === "string" || typeof time === "number" ? parseISO(String(time)) : time; - // return if undefined - if (!parsedTime) return ""; // Return empty string for invalid dates - // Format the time in the form of amount of time passed since the event happened - const distance = formatDistanceToNow(parsedTime, { addSuffix: true }); - return distance; -}; - -export function calculateTimeAgoShort(date: string | number | Date | null): string { - if (!date) { - return ""; - } - - const parsedDate = typeof date === "string" ? parseISO(date) : new Date(date); - const now = new Date(); - const diffInSeconds = (now.getTime() - parsedDate.getTime()) / 1000; - - if (diffInSeconds < 60) { - return `${Math.floor(diffInSeconds)}s`; - } - - const diffInMinutes = diffInSeconds / 60; - if (diffInMinutes < 60) { - return `${Math.floor(diffInMinutes)}m`; - } - - const diffInHours = diffInMinutes / 60; - if (diffInHours < 24) { - return `${Math.floor(diffInHours)}h`; - } - - const diffInDays = diffInHours / 24; - if (diffInDays < 30) { - return `${Math.floor(diffInDays)}d`; - } - - const diffInMonths = diffInDays / 30; - if (diffInMonths < 12) { - return `${Math.floor(diffInMonths)}mo`; - } - - const diffInYears = diffInMonths / 12; - return `${Math.floor(diffInYears)}y`; -} - -// Date Validation Helpers -/** - * @returns {string} boolean value depending on whether the date is greater than today - * @description Returns boolean value depending on whether the date is greater than today - * @param {string} dateStr - * @example isDateGreaterThanToday("2024-01-01") // true - */ -export const isDateGreaterThanToday = (dateStr: string): boolean => { - // Return false if dateStr is not present - if (!dateStr) return false; - // Parse the date to check if it is valid - const date = parseISO(dateStr); - const today = new Date(); - // Check if the parsed date is valid - if (!isValid(date)) return false; // Return false for invalid dates - // Return true if the date is greater than today - return isAfter(date, today); -}; - -// Week Related Helpers -/** - * @returns {number} week number of date - * @description Returns week number of date - * @param {Date} date - * @example getWeekNumber(new Date("2023-09-01")) // 35 - */ -export const getWeekNumberOfDate = (date: Date): number => { - const currentDate = date; - // Adjust the starting day to Sunday (0) instead of Monday (1) - const startDate = new Date(currentDate.getFullYear(), 0, 1); - // Calculate the number of days between currentDate and startDate - const days = Math.floor((currentDate.getTime() - startDate.getTime()) / (24 * 60 * 60 * 1000)); - // Adjust the calculation for weekNumber - const weekNumber = Math.ceil((days + 1) / 7); - return weekNumber; -}; - -/** - * @returns {boolean} boolean value depending on whether the dates are equal - * @description Returns boolean value depending on whether the dates are equal - * @param date1 - * @param date2 - * @example checkIfDatesAreEqual("2024-01-01", "2024-01-01") // true - * @example checkIfDatesAreEqual("2024-01-01", "2024-01-02") // false - */ -export const checkIfDatesAreEqual = ( - date1: Date | string | null | undefined, - date2: Date | string | null | undefined -): boolean => { - const parsedDate1 = getDate(date1); - const parsedDate2 = getDate(date2); - // return if undefined - if (!parsedDate1 && !parsedDate2) return true; - if (!parsedDate1 || !parsedDate2) return false; - - return isEqual(parsedDate1, parsedDate2); -}; - -/** - * This method returns a date from string of type yyyy-mm-dd - * This method is recommended to use instead of new Date() as this does not introduce any timezone offsets - * @param date - * @returns date or undefined - */ -export const getDate = (date: string | Date | undefined | null): Date | undefined => { - try { - if (!date || date === "") return; - - if (typeof date !== "string" && !(date instanceof String)) return date; - - const [yearString, monthString, dayString] = date.substring(0, 10).split("-"); - const year = parseInt(yearString); - const month = parseInt(monthString); - const day = parseInt(dayString); - if (!isNumber(year) || !isNumber(month) || !isNumber(day)) return; - - return new Date(year, month - 1, day); - } catch (e) { - return undefined; - } -}; - -export const isInDateFormat = (date: string) => { - const datePattern = /^\d{4}-\d{2}-\d{2}$/; - return datePattern.test(date); -}; - -/** - * returns the date string in ISO format regardless of the timezone in input date string - * @param dateString - * @returns - */ -export const convertToISODateString = (dateString: string | undefined) => { - if (!dateString) return dateString; - - const date = new Date(dateString); - return date.toISOString(); -}; - -/** - * returns the date string in Epoch regardless of the timezone in input date string - * @param dateString - * @returns - */ -export const convertToEpoch = (dateString: string | undefined) => { - if (!dateString) return dateString; - - const date = new Date(dateString); - return date.getTime(); -}; - -/** - * get current Date time in UTC ISO format - * @returns - */ -export const getCurrentDateTimeInISO = () => { - const date = new Date(); - return date.toISOString(); -}; - -/** - * @description converts hours and minutes to minutes - * @param { number } hours - * @param { number } minutes - * @returns { number } minutes - * @example convertHoursMinutesToMinutes(2, 30) // Output: 150 - */ -export const convertHoursMinutesToMinutes = (hours: number, minutes: number): number => hours * 60 + minutes; - -/** - * @description converts minutes to hours and minutes - * @param { number } mins - * @returns { number, number } hours and minutes - * @example convertMinutesToHoursAndMinutes(150) // Output: { hours: 2, minutes: 30 } - */ -export const convertMinutesToHoursAndMinutes = (mins: number): { hours: number; minutes: number } => { - const hours = Math.floor(mins / 60); - const minutes = Math.floor(mins % 60); - - return { hours: hours, minutes: minutes }; -}; - -/** - * @description converts minutes to hours and minutes string - * @param { number } totalMinutes - * @returns { string } 0h 0m - * @example convertMinutesToHoursAndMinutes(150) // Output: 2h 10m - */ -export const convertMinutesToHoursMinutesString = (totalMinutes: number): string => { - const { hours, minutes } = convertMinutesToHoursAndMinutes(totalMinutes); - - return `${hours ? `${hours}h ` : ``}${minutes ? `${minutes}m ` : ``}`; -}; - -/** - * @description calculates the read time for a document using the words count - * @param {number} wordsCount - * @returns {number} total number of seconds - * @example getReadTimeFromWordsCount(400) // Output: 120 - * @example getReadTimeFromWordsCount(100) // Output: 30s - */ -export const getReadTimeFromWordsCount = (wordsCount: number): number => { - const wordsPerMinute = 200; - const minutes = wordsCount / wordsPerMinute; - return minutes * 60; -}; - -/** - * @description generates an array of dates between the start and end dates - * @param startDate - * @param endDate - * @returns - */ -export const generateDateArray = (startDate: string | Date, endDate: string | Date) => { - // Convert the start and end dates to Date objects if they aren't already - const start = new Date(startDate); - // start.setDate(start.getDate() + 1); - const end = new Date(endDate); - end.setDate(end.getDate() + 1); - - // Create an empty array to store the dates - const dateArray = []; - - // Use a while loop to generate dates between the range - while (start <= end) { - // Increment the date by 1 day (86400000 milliseconds) - start.setDate(start.getDate() + 1); - // Push the current date (converted to ISO string for consistency) - dateArray.push({ - date: new Date(start).toISOString().split("T")[0], - }); - } - - return dateArray; -}; - -/** - * Processes relative date strings like "1_weeks", "2_months" etc and returns a Date - * @param value The relative date string (e.g., "1_weeks", "2_months") - * @returns Date object representing the calculated date - */ -export const processRelativeDate = (value: string): Date => { - const [amountStr, unit] = value.split("_"); - const amount = parseInt(amountStr, 10); - if (isNaN(amount)) { - throw new Error(`Invalid relative amount: ${amountStr}`); - } - const date = new Date(); - - switch (unit) { - case "days": - date.setDate(date.getDate() + amount); - break; - case "weeks": - date.setDate(date.getDate() + amount * 7); - break; - case "months": - date.setMonth(date.getMonth() + amount); - break; - default: - throw new Error(`Unsupported time unit: ${unit}`); - } - - return date; -}; - -/** - * Parses a date filter string and returns the comparison type and date - * @param filterValue The date filter string (e.g., "1_weeks;after;fromnow" or "2024-12-01;after") - * @returns Object containing the comparison type and target date - */ -export const parseDateFilter = (filterValue: string): { type: "after" | "before"; date: Date } => { - const parts = filterValue.split(";"); - const dateStr = parts[0]; - const type = parts[1] as "after" | "before"; - - let date: Date; - if (dateStr.includes("_")) { - // Handle relative dates (e.g., "1_weeks;after;fromnow") - date = processRelativeDate(dateStr); - } else { - // Handle absolute dates (e.g., "2024-12-01;after") - date = new Date(dateStr); - } - - return { type, date }; -}; - -/** - * Checks if a date meets the filter criteria - * @param dateToCheck The date to check - * @param filterDate The filter date to compare against - * @param type The type of comparison ('after' or 'before') - * @returns boolean indicating if the date meets the criteria - */ -export const checkDateCriteria = (dateToCheck: Date | null, filterDate: Date, type: "after" | "before"): boolean => { - if (!dateToCheck) return false; - - const checkDate = new Date(dateToCheck); - const normalizedCheck = new Date(checkDate.setHours(0, 0, 0, 0)); - const normalizedFilter = new Date(filterDate.getTime()); - normalizedFilter.setHours(0, 0, 0, 0); - - return type === "after" ? normalizedCheck >= normalizedFilter : normalizedCheck <= normalizedFilter; -}; diff --git a/web/helpers/download.helper.ts b/web/helpers/download.helper.ts deleted file mode 100644 index 2709edd01..000000000 --- a/web/helpers/download.helper.ts +++ /dev/null @@ -1,14 +0,0 @@ -export const csvDownload = (data: Array> | { [key: string]: string }, name: string) => { - const rows = Array.isArray(data) ? [...data] : [Object.keys(data), Object.values(data)]; - - const csvContent = "data:text/csv;charset=utf-8," + rows.map((e) => e.join(",")).join("\n"); - const encodedUri = encodeURI(csvContent); - - const link = document.createElement("a"); - link.href = encodedUri; - link.download = `${name}.csv`; - - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); // Cleanup after the download link is clicked -}; diff --git a/web/helpers/emoji.helper.tsx b/web/helpers/emoji.helper.tsx index 626bd5c3e..6932f63af 100644 --- a/web/helpers/emoji.helper.tsx +++ b/web/helpers/emoji.helper.tsx @@ -1,11 +1,8 @@ -// plane imports -import { RANDOM_EMOJI_CODES } from "@plane/constants"; -import { LUCIDE_ICONS_LIST } from "@plane/ui"; - -export const getRandomEmoji = () => RANDOM_EMOJI_CODES[Math.floor(Math.random() * RANDOM_EMOJI_CODES.length)]; - -export const getRandomIconName = () => LUCIDE_ICONS_LIST[Math.floor(Math.random() * LUCIDE_ICONS_LIST.length)].name; - +/** + * Renders an emoji or icon + * @param {string | { name: string; color: string }} emoji - The emoji or icon to render + * @returns {React.ReactNode} The rendered emoji or icon + */ export const renderEmoji = ( emoji: | string @@ -13,7 +10,7 @@ export const renderEmoji = ( name: string; color: string; } -) => { +): React.ReactNode => { if (!emoji) return; if (typeof emoji === "object") @@ -24,54 +21,3 @@ export const renderEmoji = ( ); else return isNaN(parseInt(emoji)) ? emoji : String.fromCodePoint(parseInt(emoji)); }; - -export const groupReactions: (reactions: any[], key: string) => { [key: string]: any[] } = ( - reactions: any, - key: string -) => { - if (!Array.isArray(reactions)) { - console.error("Expected an array of reactions, but got:", reactions); - return {}; - } - - const groupedReactions = reactions.reduce( - (acc: any, reaction: any) => { - if (!reaction || typeof reaction !== "object" || !Object.prototype.hasOwnProperty.call(reaction, key)) { - console.warn("Skipping undefined reaction or missing key:", reaction); - return acc; // Skip undefined reactions or those without the specified key - } - - if (!acc[reaction[key]]) { - acc[reaction[key]] = []; - } - acc[reaction[key]].push(reaction); - return acc; - }, - {} as { [key: string]: any[] } - ); - - return groupedReactions; -}; - -export const convertHexEmojiToDecimal = (emojiUnified: string): string => { - if (!emojiUnified) return ""; - - return emojiUnified - .toString() - .split("-") - .map((e) => parseInt(e, 16)) - .join("-"); -}; - -export const emojiCodeToUnicode = (emoji: string) => { - if (!emoji) return ""; - - // convert emoji code to unicode - const uniCodeEmoji = emoji - .toString() - .split("-") - .map((emoji) => parseInt(emoji, 10).toString(16)) - .join("-"); - - return uniCodeEmoji; -}; diff --git a/web/helpers/file.helper.ts b/web/helpers/file.helper.ts deleted file mode 100644 index a64177a7a..000000000 --- a/web/helpers/file.helper.ts +++ /dev/null @@ -1,96 +0,0 @@ -// types -import { TFileMetaDataLite, TFileSignedURLResponse } from "@plane/types"; -// helpers -import { API_BASE_URL } from "@/helpers/common.helper"; - -/** - * @description from the provided signed URL response, generate a payload to be used to upload the file - * @param {TFileSignedURLResponse} signedURLResponse - * @param {File} file - * @returns {FormData} file upload request payload - */ -export const generateFileUploadPayload = (signedURLResponse: TFileSignedURLResponse, file: File): FormData => { - const formData = new FormData(); - Object.entries(signedURLResponse.upload_data.fields).forEach(([key, value]) => formData.append(key, value)); - formData.append("file", file); - return formData; -}; - -/** - * @description combine the file path with the base URL - * @param {string} path - * @returns {string} final URL with the base URL - */ -export const getFileURL = (path: string): string | undefined => { - if (!path) return undefined; - const isValidURL = path.startsWith("http"); - if (isValidURL) return path; - return `${API_BASE_URL}${path}`; -}; - -/** - * @description returns the necessary file meta data to upload a file - * @param {File} file - * @returns {TFileMetaDataLite} payload with file info - */ -export const getFileMetaDataForUpload = (file: File): TFileMetaDataLite => ({ - name: file.name, - size: file.size, - type: file.type, -}); - -/** - * @description this function returns the assetId from the asset source - * @param {string} src - * @returns {string} assetId - */ -export const getAssetIdFromUrl = (src: string): string => { - // remove the last char if it is a slash - if (src.charAt(src.length - 1) === "/") src = src.slice(0, -1); - const sourcePaths = src.split("/"); - const assetUrl = sourcePaths[sourcePaths.length - 1]; - return assetUrl; -}; - -/** - * @description encode image via URL to base64 - * @param {string} url - * @returns - */ -export const getBase64Image = async (url: string): Promise => { - if (!url || typeof url !== "string") { - throw new Error("Invalid URL provided"); - } - - // Try to create a URL object to validate the URL - try { - new URL(url); - } catch (error) { - throw new Error("Invalid URL format"); - } - - const response = await fetch(url); - // check if the response is OK - if (!response.ok) { - throw new Error(`Failed to fetch image: ${response.statusText}`); - } - - const blob = await response.blob(); - return new Promise((resolve, reject) => { - const reader = new FileReader(); - - reader.onloadend = () => { - if (reader.result) { - resolve(reader.result as string); - } else { - reject(new Error("Failed to convert image to base64.")); - } - }; - - reader.onerror = () => { - reject(new Error("Failed to read the image file.")); - }; - - reader.readAsDataURL(blob); - }); -}; diff --git a/web/helpers/graph.helper.ts b/web/helpers/graph.helper.ts index 3cc79518b..0ca5d3ffa 100644 --- a/web/helpers/graph.helper.ts +++ b/web/helpers/graph.helper.ts @@ -1,3 +1,5 @@ +// ------------ DEPRECATED (Use re-charts and its helpers instead) ------------ + export const generateYAxisTickValues = (data: number[]) => { if (!data || !Array.isArray(data) || data.length === 0) return []; diff --git a/web/helpers/issue-modal.helper.ts b/web/helpers/issue-modal.helper.ts deleted file mode 100644 index 11322652e..000000000 --- a/web/helpers/issue-modal.helper.ts +++ /dev/null @@ -1,16 +0,0 @@ -import set from "lodash/set"; -// types -import type { TIssue } from "@plane/types"; - -export function getChangedIssuefields(formData: Partial, dirtyFields: { [key: string]: boolean | undefined }) { - const changedFields = {}; - - const dirtyFieldKeys = Object.keys(dirtyFields) as (keyof TIssue)[]; - for (const dirtyField of dirtyFieldKeys) { - if (!!dirtyFields[dirtyField]) { - set(changedFields, [dirtyField], formData[dirtyField]); - } - } - - return changedFields as Partial; -} diff --git a/web/helpers/password.helper.ts b/web/helpers/password.helper.ts deleted file mode 100644 index dfe9a5c65..000000000 --- a/web/helpers/password.helper.ts +++ /dev/null @@ -1,67 +0,0 @@ -import zxcvbn from "zxcvbn"; - -export enum E_PASSWORD_STRENGTH { - EMPTY = "empty", - LENGTH_NOT_VALID = "length_not_valid", - STRENGTH_NOT_VALID = "strength_not_valid", - STRENGTH_VALID = "strength_valid", -} - -const PASSWORD_MIN_LENGTH = 8; -// const PASSWORD_NUMBER_REGEX = /\d/; -// const PASSWORD_CHAR_CAPS_REGEX = /[A-Z]/; -// const PASSWORD_SPECIAL_CHAR_REGEX = /[`!@#$%^&*()_\-+=\[\]{};':"\\|,.<>\/?~ ]/; - -export const PASSWORD_CRITERIA = [ - { - key: "min_8_char", - label: "Min 8 characters", - isCriteriaValid: (password: string) => password.length >= PASSWORD_MIN_LENGTH, - }, - // { - // key: "min_1_upper_case", - // label: "Min 1 upper-case letter", - // isCriteriaValid: (password: string) => PASSWORD_NUMBER_REGEX.test(password), - // }, - // { - // key: "min_1_number", - // label: "Min 1 number", - // isCriteriaValid: (password: string) => PASSWORD_CHAR_CAPS_REGEX.test(password), - // }, - // { - // key: "min_1_special_char", - // label: "Min 1 special character", - // isCriteriaValid: (password: string) => PASSWORD_SPECIAL_CHAR_REGEX.test(password), - // }, -]; - -export const getPasswordStrength = (password: string): E_PASSWORD_STRENGTH => { - let passwordStrength: E_PASSWORD_STRENGTH = E_PASSWORD_STRENGTH.EMPTY; - - if (!password || password === "" || password.length <= 0) { - return passwordStrength; - } - - if (password.length >= PASSWORD_MIN_LENGTH) { - passwordStrength = E_PASSWORD_STRENGTH.STRENGTH_NOT_VALID; - } else { - passwordStrength = E_PASSWORD_STRENGTH.LENGTH_NOT_VALID; - return passwordStrength; - } - - const passwordCriteriaValidation = PASSWORD_CRITERIA.map((criteria) => criteria.isCriteriaValid(password)).every( - (criterion) => criterion - ); - const passwordStrengthScore = zxcvbn(password).score; - - if (passwordCriteriaValidation === false || passwordStrengthScore <= 2) { - passwordStrength = E_PASSWORD_STRENGTH.STRENGTH_NOT_VALID; - return passwordStrength; - } - - if (passwordCriteriaValidation === true && passwordStrengthScore >= 3) { - passwordStrength = E_PASSWORD_STRENGTH.STRENGTH_VALID; - } - - return passwordStrength; -}; diff --git a/web/helpers/string.helper.ts b/web/helpers/string.helper.ts deleted file mode 100644 index 4c85e3ca8..000000000 --- a/web/helpers/string.helper.ts +++ /dev/null @@ -1,236 +0,0 @@ -import DOMPurify from "isomorphic-dompurify"; - -export const addSpaceIfCamelCase = (str: string) => { - if (str === undefined || str === null) return ""; - - if (typeof str !== "string") str = `${str}`; - - return str.replace(/([a-z])([A-Z])/g, "$1 $2"); -}; - -export const replaceUnderscoreIfSnakeCase = (str: string) => str.replace(/_/g, " "); - -export const capitalizeFirstLetter = (str: string) => str.charAt(0).toUpperCase() + str.slice(1); - -export const truncateText = (str: string, length: number) => { - if (!str || str === "") return ""; - - return str.length > length ? `${str.substring(0, length)}...` : str; -}; - -export const createSimilarString = (str: string) => { - const shuffled = str - .split("") - .sort(() => Math.random() - 0.5) - .join(""); - - return shuffled; -}; - -const fallbackCopyTextToClipboard = (text: string) => { - const textArea = document.createElement("textarea"); - textArea.value = text; - - // Avoid scrolling to bottom - textArea.style.top = "0"; - textArea.style.left = "0"; - textArea.style.position = "fixed"; - - document.body.appendChild(textArea); - textArea.focus(); - textArea.select(); - - try { - // FIXME: Even though we are using this as a fallback, execCommand is deprecated 👎. We should find a better way to do this. - // https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand - document.execCommand("copy"); - } catch (err) { - // catch fallback error - } - - document.body.removeChild(textArea); -}; - -export const copyTextToClipboard = async (text: string) => { - if (!navigator.clipboard) { - fallbackCopyTextToClipboard(text); - return; - } - await navigator.clipboard.writeText(text); -}; - -export const generateRandomColor = (string: string): string => { - if (!string) return "rgb(var(--color-primary-100))"; - - string = `${string}`; - - const uniqueId = string.length.toString() + string; // Unique identifier based on string length - const combinedString = uniqueId + string; - - const hash = Array.from(combinedString).reduce((acc, char) => { - const charCode = char.charCodeAt(0); - return (acc << 5) - acc + charCode; - }, 0); - - const hue = hash % 360; - const saturation = 70; // Higher saturation for pastel colors - const lightness = 60; // Mid-range lightness for pastel colors - - const randomColor = `hsl(${hue}, ${saturation}%, ${lightness}%)`; - - return randomColor; -}; - -export const getFirstCharacters = (str: string) => { - const words = str.trim().split(" "); - if (words.length === 1) { - return words[0].charAt(0); - } else { - return words[0].charAt(0) + words[1].charAt(0); - } -}; - -/** - * @description: This function will remove all the HTML tags from the string - * @param {string} html - * @return {string} - * @example: - * const html = "

Some text

"; - * const text = stripHTML(html); - * console.log(text); // Some text - */ - -export const sanitizeHTML = (htmlString: string) => { - const sanitizedText = DOMPurify.sanitize(htmlString, { ALLOWED_TAGS: [] }); // sanitize the string to remove all HTML tags - return sanitizedText.trim(); // trim the string to remove leading and trailing whitespaces -}; - -/** - * - * @example: - * const html = "

Some text

"; - * const text = stripAndTruncateHTML(html); - * console.log(text); // Some text - */ - -export const stripAndTruncateHTML = (html: string, length: number = 55) => truncateText(sanitizeHTML(html), length); - -/** - * @description: This function return number count in string if number is more than 100 then it will return 99+ - * @param {number} number - * @return {string} - * @example: - * const number = 100; - * const text = getNumberCount(number); - * console.log(text); // 99+ - */ - -export const getNumberCount = (number: number): string => { - if (number > 99) { - return "99+"; - } - return number.toString(); -}; - -export const objToQueryParams = (obj: any) => { - const params = new URLSearchParams(); - - if (!obj) return params.toString(); - - for (const [key, value] of Object.entries(obj)) { - if (value !== undefined && value !== null) params.append(key, value as string); - } - - return params.toString(); -}; - -/** - * @returns {boolean} true if searchQuery is substring of text in the same order, false otherwise - * @description Returns true if searchQuery is substring of text in the same order, false otherwise - * @param {string} text string to compare from - * @param {string} searchQuery - * @example substringMatch("hello world", "hlo") => true - * @example substringMatch("hello world", "hoe") => false - */ -export const substringMatch = (text: string, searchQuery: string): boolean => { - try { - let searchIndex = 0; - - for (let i = 0; i < text.length; i++) { - if (text[i].toLowerCase() === searchQuery[searchIndex]?.toLowerCase()) searchIndex++; - - // All characters of searchQuery found in order - if (searchIndex === searchQuery.length) return true; - } - - // Not all characters of searchQuery found in order - return false; - } catch (error) { - return false; - } -}; - -/** - * @returns {boolean} true if email is valid, false otherwise - * @description Returns true if email is valid, false otherwise - * @param {string} email string to check if it is a valid email - * @example checkEmailIsValid("hello world") => false - * @example checkEmailIsValid("example@plane.so") => true - */ -export const checkEmailValidity = (email: string): boolean => { - if (!email) return false; - - const isEmailValid = - /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test( - email - ); - - return isEmailValid; -}; - -export const isEmptyHtmlString = (htmlString: string, allowedHTMLTags: string[] = []) => { - // Remove HTML tags using regex - const cleanText = DOMPurify.sanitize(htmlString, { ALLOWED_TAGS: allowedHTMLTags }); - // Trim the string and check if it's empty - return cleanText.trim() === ""; -}; - -/** - * @description this function returns whether a comment is empty or not by checking for the following conditions- - * 1. If comment is undefined - * 2. If comment is an empty string - * 3. If comment is "

" - * @param {string | undefined} comment - * @returns {boolean} - */ -export const isCommentEmpty = (comment: string | undefined): boolean => { - // return true if comment is undefined - if (!comment) return true; - return ( - comment?.trim() === "" || - comment === "

" || - isEmptyHtmlString(comment ?? "", ["img", "mention-component", "image-component"]) - ); -}; - -/** - * @description - * This function test whether a URL is valid or not. - * - * It accepts URLs with or without the protocol. - * @param {string} url - * @returns {boolean} - * @example - * checkURLValidity("https://example.com") => true - * checkURLValidity("example.com") => true - * checkURLValidity("example") => false - */ -export const checkURLValidity = (url: string): boolean => { - if (!url) return false; - - // regex to support complex query parameters and fragments - const urlPattern = - /^(https?:\/\/)?((([a-z\d-]+\.)*[a-z\d-]+\.[a-z]{2,6})|(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}))(:\d+)?(\/[\w.-]*)*(\?[^#\s]*)?(#[\w-]*)?$/i; - - return urlPattern.test(url); -}; diff --git a/web/helpers/theme.helper.ts b/web/helpers/theme.helper.ts deleted file mode 100644 index 9b0638c1f..000000000 --- a/web/helpers/theme.helper.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { TRgb, hexToRgb } from "@/helpers/color.helper"; - -type TShades = { - 10: TRgb; - 20: TRgb; - 30: TRgb; - 40: TRgb; - 50: TRgb; - 60: TRgb; - 70: TRgb; - 80: TRgb; - 90: TRgb; - 100: TRgb; - 200: TRgb; - 300: TRgb; - 400: TRgb; - 500: TRgb; - 600: TRgb; - 700: TRgb; - 800: TRgb; - 900: TRgb; -}; - -const calculateShades = (hexValue: string): TShades => { - const shades: Partial = {}; - const { r, g, b } = hexToRgb(hexValue); - - const convertHexToSpecificShade = (shade: number): TRgb => { - if (shade <= 100) { - const decimalValue = (100 - shade) / 100; - - const newR = Math.floor(r + (255 - r) * decimalValue); - const newG = Math.floor(g + (255 - g) * decimalValue); - const newB = Math.floor(b + (255 - b) * decimalValue); - - return { - r: newR, - g: newG, - b: newB, - }; - } else { - const decimalValue = 1 - Math.ceil((shade - 100) / 100) / 10; - - const newR = Math.ceil(r * decimalValue); - const newG = Math.ceil(g * decimalValue); - const newB = Math.ceil(b * decimalValue); - - return { - r: newR, - g: newG, - b: newB, - }; - } - }; - - for (let i = 10; i <= 900; i >= 100 ? (i += 100) : (i += 10)) - shades[i as keyof TShades] = convertHexToSpecificShade(i); - - return shades as TShades; -}; - -export const applyTheme = (palette: string, isDarkPalette: boolean) => { - if (!palette) return; - const themeElement = document?.querySelector("html"); - // palette: [bg, text, primary, sidebarBg, sidebarText] - const values: string[] = palette.split(","); - values.push(isDarkPalette ? "dark" : "light"); - - const bgShades = calculateShades(values[0]); - const textShades = calculateShades(values[1]); - const primaryShades = calculateShades(values[2]); - const sidebarBackgroundShades = calculateShades(values[3]); - const sidebarTextShades = calculateShades(values[4]); - - for (let i = 10; i <= 900; i >= 100 ? (i += 100) : (i += 10)) { - const shade = i as keyof TShades; - - const bgRgbValues = `${bgShades[shade].r}, ${bgShades[shade].g}, ${bgShades[shade].b}`; - const textRgbValues = `${textShades[shade].r}, ${textShades[shade].g}, ${textShades[shade].b}`; - const primaryRgbValues = `${primaryShades[shade].r}, ${primaryShades[shade].g}, ${primaryShades[shade].b}`; - const sidebarBackgroundRgbValues = `${sidebarBackgroundShades[shade].r}, ${sidebarBackgroundShades[shade].g}, ${sidebarBackgroundShades[shade].b}`; - const sidebarTextRgbValues = `${sidebarTextShades[shade].r}, ${sidebarTextShades[shade].g}, ${sidebarTextShades[shade].b}`; - - themeElement?.style.setProperty(`--color-background-${shade}`, bgRgbValues); - themeElement?.style.setProperty(`--color-text-${shade}`, textRgbValues); - themeElement?.style.setProperty(`--color-primary-${shade}`, primaryRgbValues); - themeElement?.style.setProperty(`--color-sidebar-background-${shade}`, sidebarBackgroundRgbValues); - themeElement?.style.setProperty(`--color-sidebar-text-${shade}`, sidebarTextRgbValues); - - if (i >= 100 && i <= 400) { - const borderShade = i === 100 ? 70 : i === 200 ? 80 : i === 300 ? 90 : 100; - - themeElement?.style.setProperty( - `--color-border-${shade}`, - `${bgShades[borderShade].r}, ${bgShades[borderShade].g}, ${bgShades[borderShade].b}` - ); - themeElement?.style.setProperty( - `--color-sidebar-border-${shade}`, - `${sidebarBackgroundShades[borderShade].r}, ${sidebarBackgroundShades[borderShade].g}, ${sidebarBackgroundShades[borderShade].b}` - ); - } - } - - themeElement?.style.setProperty("--color-scheme", values[5]); -}; - -export const unsetCustomCssVariables = () => { - for (let i = 10; i <= 900; i >= 100 ? (i += 100) : (i += 10)) { - const dom = document.querySelector("[data-theme='custom']"); - - dom?.style.removeProperty(`--color-background-${i}`); - dom?.style.removeProperty(`--color-text-${i}`); - dom?.style.removeProperty(`--color-border-${i}`); - dom?.style.removeProperty(`--color-primary-${i}`); - dom?.style.removeProperty(`--color-sidebar-background-${i}`); - dom?.style.removeProperty(`--color-sidebar-text-${i}`); - dom?.style.removeProperty(`--color-sidebar-border-${i}`); - dom?.style.removeProperty("--color-scheme"); - } -}; - -export const resolveGeneralTheme = (resolvedTheme: string | undefined) => - resolvedTheme?.includes("light") ? "light" : resolvedTheme?.includes("dark") ? "dark" : "system"; diff --git a/web/helpers/user.helper.ts b/web/helpers/user.helper.ts deleted file mode 100644 index 7d0637afb..000000000 --- a/web/helpers/user.helper.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { EUserProjectRoles, EUserWorkspaceRoles, EUserPermissions } from "@plane/constants"; - -export const getUserRole = (role: EUserPermissions | EUserWorkspaceRoles | EUserProjectRoles) => { - switch (role) { - case EUserPermissions.GUEST: - return "GUEST"; - case EUserPermissions.MEMBER: - return "MEMBER"; - case EUserPermissions.ADMIN: - return "ADMIN"; - } -}; From 0983e5f44dce266faba2c9736bf58289433aafc5 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Mon, 16 Jun 2025 17:19:44 +0530 Subject: [PATCH 166/201] [WEB-4281] chore: project error message updated (#7190) * chore: project error message updated * fix: error message for project creation * fix: incorrect error code * chore: code refactor * chore: code refactor --------- Co-authored-by: sangeethailango --- apiserver/plane/app/views/project/base.py | 10 +++++-- .../i18n/src/locales/cs/translations.json | 2 ++ .../i18n/src/locales/de/translations.json | 4 ++- .../i18n/src/locales/en/translations.json | 2 ++ .../i18n/src/locales/es/translations.json | 2 ++ .../i18n/src/locales/fr/translations.json | 2 ++ .../i18n/src/locales/id/translations.json | 2 ++ .../i18n/src/locales/it/translations.json | 2 ++ .../i18n/src/locales/ja/translations.json | 2 ++ .../i18n/src/locales/ko/translations.json | 2 ++ .../i18n/src/locales/pl/translations.json | 2 ++ .../i18n/src/locales/pt-BR/translations.json | 2 ++ .../i18n/src/locales/ro/translations.json | 2 ++ .../i18n/src/locales/ru/translations.json | 2 ++ .../i18n/src/locales/sk/translations.json | 2 ++ .../i18n/src/locales/tr-TR/translations.json | 2 ++ .../i18n/src/locales/ua/translations.json | 2 ++ .../i18n/src/locales/vi-VN/translations.json | 2 ++ .../i18n/src/locales/zh-CN/translations.json | 2 ++ .../i18n/src/locales/zh-TW/translations.json | 2 ++ web/ce/components/projects/create/root.tsx | 29 ++++++++++++------- 21 files changed, 65 insertions(+), 14 deletions(-) diff --git a/apiserver/plane/app/views/project/base.py b/apiserver/plane/app/views/project/base.py index 8e4ea5246..2728bf4de 100644 --- a/apiserver/plane/app/views/project/base.py +++ b/apiserver/plane/app/views/project/base.py @@ -341,7 +341,10 @@ class ProjectViewSet(BaseViewSet): except IntegrityError as e: if "already exists" in str(e): return Response( - {"name": "The project name is already taken"}, + { + "name": "The project name is already taken", + "code": "PROJECT_NAME_ALREADY_EXIST", + }, status=status.HTTP_409_CONFLICT, ) except Workspace.DoesNotExist: @@ -350,7 +353,10 @@ class ProjectViewSet(BaseViewSet): ) except serializers.ValidationError: return Response( - {"identifier": "The project identifier is already taken"}, + { + "identifier": "The project identifier is already taken", + "code": "PROJECT_IDENTIFIER_ALREADY_EXIST", + }, status=status.HTTP_409_CONFLICT, ) diff --git a/packages/i18n/src/locales/cs/translations.json b/packages/i18n/src/locales/cs/translations.json index 599765916..396ca03e5 100644 --- a/packages/i18n/src/locales/cs/translations.json +++ b/packages/i18n/src/locales/cs/translations.json @@ -316,6 +316,8 @@ "failed_to_remove_project_from_favorites": "Nepodařilo se odstranit projekt z oblíbených. Zkuste to prosím znovu.", "project_created_successfully": "Projekt úspěšně vytvořen", "project_created_successfully_description": "Projekt byl úspěšně vytvořen. Nyní můžete začít přidávat pracovní položky.", + "project_name_already_taken": "Název projektu už je zabraný.", + "project_identifier_already_taken": "Identifikátor projektu už je zabraný.", "project_cover_image_alt": "Úvodní obrázek projektu", "name_is_required": "Název je povinný", "title_should_be_less_than_255_characters": "Název by měl být kratší než 255 znaků", diff --git a/packages/i18n/src/locales/de/translations.json b/packages/i18n/src/locales/de/translations.json index 1cac1d99f..1b6e4778e 100644 --- a/packages/i18n/src/locales/de/translations.json +++ b/packages/i18n/src/locales/de/translations.json @@ -316,6 +316,8 @@ "failed_to_remove_project_from_favorites": "Projekt konnte nicht aus den Favoriten entfernt werden. Bitte versuchen Sie es erneut.", "project_created_successfully": "Projekt erfolgreich erstellt", "project_created_successfully_description": "Das Projekt wurde erfolgreich erstellt. Sie können nun Arbeitselemente hinzufügen.", + "project_name_already_taken": "Der Projektname ist bereits vergeben.", + "project_identifier_already_taken": "Der Projekt-Identifier ist bereits vergeben.", "project_cover_image_alt": "Titelbild des Projekts", "name_is_required": "Name ist erforderlich", "title_should_be_less_than_255_characters": "Der Titel sollte weniger als 255 Zeichen enthalten", @@ -2468,4 +2470,4 @@ "plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Plane ist nicht gestartet. Dies könnte daran liegen, dass einer oder mehrere Plane-Services nicht starten konnten.", "choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "Wählen Sie View Logs aus setup.sh und Docker-Logs, um sicherzugehen." } -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/en/translations.json b/packages/i18n/src/locales/en/translations.json index 654ce3cc6..fafed9c77 100644 --- a/packages/i18n/src/locales/en/translations.json +++ b/packages/i18n/src/locales/en/translations.json @@ -153,6 +153,8 @@ "failed_to_remove_project_from_favorites": "Couldn't remove the project from favorites. Please try again.", "project_created_successfully": "Project created successfully", "project_created_successfully_description": "Project created successfully. You can now start adding work items to it.", + "project_name_already_taken": "The project name is already taken.", + "project_identifier_already_taken": "The project identifier is already taken.", "project_cover_image_alt": "Project cover image", "name_is_required": "Name is required", "title_should_be_less_than_255_characters": "Title should be less than 255 characters", diff --git a/packages/i18n/src/locales/es/translations.json b/packages/i18n/src/locales/es/translations.json index 97f0792f2..49ca53ea1 100644 --- a/packages/i18n/src/locales/es/translations.json +++ b/packages/i18n/src/locales/es/translations.json @@ -318,6 +318,8 @@ "failed_to_remove_project_from_favorites": "No se pudo eliminar el proyecto de favoritos. Por favor, inténtalo de nuevo.", "project_created_successfully": "Proyecto creado exitosamente", "project_created_successfully_description": "Proyecto creado exitosamente. Ahora puedes comenzar a agregar elementos de trabajo.", + "project_name_already_taken": "El nombre del proyecto ya está en uso.", + "project_identifier_already_taken": "El identificador del proyecto ya está en uso.", "project_cover_image_alt": "Imagen de portada del proyecto", "name_is_required": "El nombre es requerido", "title_should_be_less_than_255_characters": "El título debe tener menos de 255 caracteres", diff --git a/packages/i18n/src/locales/fr/translations.json b/packages/i18n/src/locales/fr/translations.json index 9fce5a002..e42db2c52 100644 --- a/packages/i18n/src/locales/fr/translations.json +++ b/packages/i18n/src/locales/fr/translations.json @@ -316,6 +316,8 @@ "failed_to_remove_project_from_favorites": "Impossible de supprimer le projet des favoris. Veuillez réessayer.", "project_created_successfully": "Projet créé avec succès", "project_created_successfully_description": "Projet créé avec succès. Vous pouvez maintenant commencer à ajouter des éléments de travail.", + "project_name_already_taken": "Le nom du projet est déjà pris.", + "project_identifier_already_taken": "L’identifiant du projet est déjà pris.", "project_cover_image_alt": "Image de couverture du projet", "name_is_required": "Le nom est requis", "title_should_be_less_than_255_characters": "Le titre doit faire moins de 255 caractères", diff --git a/packages/i18n/src/locales/id/translations.json b/packages/i18n/src/locales/id/translations.json index 87c4a952f..372aefde9 100644 --- a/packages/i18n/src/locales/id/translations.json +++ b/packages/i18n/src/locales/id/translations.json @@ -316,6 +316,8 @@ "failed_to_remove_project_from_favorites": "Tidak dapat menghapus proyek dari favorit. Silakan coba lagi.", "project_created_successfully": "Proyek berhasil dibuat", "project_created_successfully_description": "Proyek berhasil dibuat. Anda sekarang dapat mulai menambahkan item kerja ke dalamnya.", + "project_name_already_taken": "Nama proyek sudah digunakan", + "project_identifier_already_taken": "ID proyek sudah digunakan", "project_cover_image_alt": "Gambar sampul proyek", "name_is_required": "Nama diperlukan", "title_should_be_less_than_255_characters": "Judul harus kurang dari 255 karakter", diff --git a/packages/i18n/src/locales/it/translations.json b/packages/i18n/src/locales/it/translations.json index 75df8e9b5..b859ce217 100644 --- a/packages/i18n/src/locales/it/translations.json +++ b/packages/i18n/src/locales/it/translations.json @@ -316,6 +316,8 @@ "failed_to_remove_project_from_favorites": "Impossibile rimuovere il progetto dai preferiti. Per favore, riprova.", "project_created_successfully": "Progetto creato con successo", "project_created_successfully_description": "Progetto creato con successo. Ora puoi iniziare ad aggiungere elementi di lavoro.", + "project_name_already_taken": "Il nome del progetto è già stato utilizzato.", + "project_identifier_already_taken": "L'identificatore del progetto è già stato utilizzato.", "project_cover_image_alt": "Immagine di copertina del progetto", "name_is_required": "Il nome è obbligatorio", "title_should_be_less_than_255_characters": "Il titolo deve contenere meno di 255 caratteri", diff --git a/packages/i18n/src/locales/ja/translations.json b/packages/i18n/src/locales/ja/translations.json index e7610eac3..4c6f27a6e 100644 --- a/packages/i18n/src/locales/ja/translations.json +++ b/packages/i18n/src/locales/ja/translations.json @@ -316,6 +316,8 @@ "failed_to_remove_project_from_favorites": "プロジェクトをお気に入りから削除できませんでした。もう一度お試しください。", "project_created_successfully": "プロジェクトが正常に作成されました", "project_created_successfully_description": "プロジェクトが正常に作成されました。作業項目を追加できるようになりました。", + "project_name_already_taken": "プロジェクト名は既に使用されています。", + "project_identifier_already_taken": "プロジェクト識別子は既に使用されています。", "project_cover_image_alt": "プロジェクトのカバー画像", "name_is_required": "名前は必須です", "title_should_be_less_than_255_characters": "タイトルは255文字未満である必要があります", diff --git a/packages/i18n/src/locales/ko/translations.json b/packages/i18n/src/locales/ko/translations.json index 6100f3fd6..ee1f61adc 100644 --- a/packages/i18n/src/locales/ko/translations.json +++ b/packages/i18n/src/locales/ko/translations.json @@ -316,6 +316,8 @@ "failed_to_remove_project_from_favorites": "프로젝트를 즐겨찾기에서 제거하지 못했습니다. 다시 시도해주세요.", "project_created_successfully": "프로젝트가 성공적으로 생성되었습니다", "project_created_successfully_description": "프로젝트가 성공적으로 생성되었습니다. 이제 작업 항목을 추가할 수 있습니다.", + "project_name_already_taken": "프로젝트 이름이 이미 사용 중입니다.", + "project_identifier_already_taken": "프로젝트 식별자가 이미 사용 중입니다.", "project_cover_image_alt": "프로젝트 커버 이미지", "name_is_required": "이름이 필요합니다", "title_should_be_less_than_255_characters": "제목은 255자 미만이어야 합니다", diff --git a/packages/i18n/src/locales/pl/translations.json b/packages/i18n/src/locales/pl/translations.json index 7cd8ba385..b26e6e2f4 100644 --- a/packages/i18n/src/locales/pl/translations.json +++ b/packages/i18n/src/locales/pl/translations.json @@ -316,6 +316,8 @@ "failed_to_remove_project_from_favorites": "Nie udało się usunąć projektu z ulubionych. Spróbuj ponownie.", "project_created_successfully": "Projekt utworzono pomyślnie", "project_created_successfully_description": "Projekt został pomyślnie utworzony. Teraz możesz dodawać elementy pracy.", + "project_name_already_taken": "Nazwa projektu jest już zajęta.", + "project_identifier_already_taken": "Identyfikator projektu jest już zajęty.", "project_cover_image_alt": "Obraz w tle projektu", "name_is_required": "Nazwa jest wymagana", "title_should_be_less_than_255_characters": "Nazwa musi mieć mniej niż 255 znaków", diff --git a/packages/i18n/src/locales/pt-BR/translations.json b/packages/i18n/src/locales/pt-BR/translations.json index f640a9f01..6e7f216ab 100644 --- a/packages/i18n/src/locales/pt-BR/translations.json +++ b/packages/i18n/src/locales/pt-BR/translations.json @@ -316,6 +316,8 @@ "failed_to_remove_project_from_favorites": "Não foi possível remover o projeto dos favoritos. Por favor, tente novamente.", "project_created_successfully": "Projeto criado com sucesso", "project_created_successfully_description": "Projeto criado com sucesso. Agora você pode começar a adicionar itens de trabalho a ele.", + "project_name_already_taken": "O nome do projeto já está em uso.", + "project_identifier_already_taken": "O identificador do projeto já está em uso.", "project_cover_image_alt": "Imagem de capa do projeto", "name_is_required": "Nome é obrigatório", "title_should_be_less_than_255_characters": "O título deve ter menos de 255 caracteres", diff --git a/packages/i18n/src/locales/ro/translations.json b/packages/i18n/src/locales/ro/translations.json index fd59eb3e8..8f40c0a22 100644 --- a/packages/i18n/src/locales/ro/translations.json +++ b/packages/i18n/src/locales/ro/translations.json @@ -316,6 +316,8 @@ "failed_to_remove_project_from_favorites": "Nu s-a putut elimina proiectul din favorite. Încearcă din nou.", "project_created_successfully": "Proiect creat cu succes", "project_created_successfully_description": "Proiect creat cu succes. Poți începe să adaugi activități în el.", + "project_name_already_taken": "Numele proiectului este deja folosit.", + "project_identifier_already_taken": "Identificatorul proiectului este deja folosit.", "project_cover_image_alt": "Coperta proiectului", "name_is_required": "Numele este obligatoriu", "title_should_be_less_than_255_characters": "Titlul trebuie să conțină mai puțin de 255 de caractere", diff --git a/packages/i18n/src/locales/ru/translations.json b/packages/i18n/src/locales/ru/translations.json index 4dcb5d0e6..1981999b7 100644 --- a/packages/i18n/src/locales/ru/translations.json +++ b/packages/i18n/src/locales/ru/translations.json @@ -316,6 +316,8 @@ "failed_to_remove_project_from_favorites": "Не удалось удалить проект из избранного. Попробуйте снова.", "project_created_successfully": "Проект успешно создан", "project_created_successfully_description": "Проект успешно создан. Теперь вы можете добавлять рабочие элементы.", + "project_name_already_taken": "Имя проекта уже используется.", + "project_identifier_already_taken": "Идентификатор проекта уже используется.", "project_cover_image_alt": "Обложка проекта", "name_is_required": "Требуется имя", "title_should_be_less_than_255_characters": "Заголовок должен быть короче 255 символов", diff --git a/packages/i18n/src/locales/sk/translations.json b/packages/i18n/src/locales/sk/translations.json index e3c3e864e..af6971aae 100644 --- a/packages/i18n/src/locales/sk/translations.json +++ b/packages/i18n/src/locales/sk/translations.json @@ -316,6 +316,8 @@ "failed_to_remove_project_from_favorites": "Nepodarilo sa odstrániť projekt z obľúbených. Skúste to prosím znova.", "project_created_successfully": "Projekt bol úspešne vytvorený", "project_created_successfully_description": "Projekt bol úspešne vytvorený. Teraz môžete začať pridávať pracovné položky.", + "project_name_already_taken": "Názov projektu je už použitý.", + "project_identifier_already_taken": "Identifikátor projektu je už použitý.", "project_cover_image_alt": "Úvodný obrázok projektu", "name_is_required": "Názov je povinný", "title_should_be_less_than_255_characters": "Názov by mal byť kratší ako 255 znakov", diff --git a/packages/i18n/src/locales/tr-TR/translations.json b/packages/i18n/src/locales/tr-TR/translations.json index 4d1c4ab12..a4ae00670 100644 --- a/packages/i18n/src/locales/tr-TR/translations.json +++ b/packages/i18n/src/locales/tr-TR/translations.json @@ -316,6 +316,8 @@ "failed_to_remove_project_from_favorites": "Proje favorilerden kaldırılamadı. Lütfen tekrar deneyin.", "project_created_successfully": "Proje başarıyla oluşturuldu", "project_created_successfully_description": "Proje başarıyla oluşturuldu. Artık iş öğeleri eklemeye başlayabilirsiniz.", + "project_name_already_taken": "Proje ismi zaten kullanılıyor.", + "project_identifier_already_taken": "Proje kimliği zaten kullanılıyor.", "project_cover_image_alt": "Proje kapak resmi", "name_is_required": "Ad gereklidir", "title_should_be_less_than_255_characters": "Başlık 255 karakterden az olmalı", diff --git a/packages/i18n/src/locales/ua/translations.json b/packages/i18n/src/locales/ua/translations.json index 252a858d5..bfa6c3281 100644 --- a/packages/i18n/src/locales/ua/translations.json +++ b/packages/i18n/src/locales/ua/translations.json @@ -316,6 +316,8 @@ "failed_to_remove_project_from_favorites": "Не вдалося видалити проєкт із вибраного. Спробуйте ще раз.", "project_created_successfully": "Проєкт успішно створено", "project_created_successfully_description": "Проєкт успішно створений. Тепер ви можете почати додавати робочі одиниці.", + "project_name_already_taken": "Назва проекту вже використовується.", + "project_identifier_already_taken": "Ідентифікатор проекту вже використовується.", "project_cover_image_alt": "Обкладинка проєкту", "name_is_required": "Назва є обов’язковою", "title_should_be_less_than_255_characters": "Назва має бути коротшою за 255 символів", diff --git a/packages/i18n/src/locales/vi-VN/translations.json b/packages/i18n/src/locales/vi-VN/translations.json index d6e6d7999..3b31f81fe 100644 --- a/packages/i18n/src/locales/vi-VN/translations.json +++ b/packages/i18n/src/locales/vi-VN/translations.json @@ -316,6 +316,8 @@ "failed_to_remove_project_from_favorites": "Không thể xóa dự án khỏi mục yêu thích. Vui lòng thử lại.", "project_created_successfully": "Dự án đã được tạo thành công", "project_created_successfully_description": "Dự án đã được tạo thành công. Bây giờ bạn có thể bắt đầu thêm mục công việc.", + "project_name_already_taken": "Tên dự án đã được sử dụng.", + "project_identifier_already_taken": "ID dự án đã được sử dụng.", "project_cover_image_alt": "Ảnh bìa dự án", "name_is_required": "Tên là bắt buộc", "title_should_be_less_than_255_characters": "Tiêu đề phải ít hơn 255 ký tự", diff --git a/packages/i18n/src/locales/zh-CN/translations.json b/packages/i18n/src/locales/zh-CN/translations.json index d815a5ad7..304b435a8 100644 --- a/packages/i18n/src/locales/zh-CN/translations.json +++ b/packages/i18n/src/locales/zh-CN/translations.json @@ -316,6 +316,8 @@ "failed_to_remove_project_from_favorites": "无法从收藏中移除项目。请重试。", "project_created_successfully": "项目创建成功", "project_created_successfully_description": "项目创建成功。您现在可以开始添加工作项了。", + "project_name_already_taken": "项目名称已被使用。", + "project_identifier_already_taken": "项目标识符已被使用。", "project_cover_image_alt": "项目封面图片", "name_is_required": "名称为必填项", "title_should_be_less_than_255_characters": "标题应少于255个字符", diff --git a/packages/i18n/src/locales/zh-TW/translations.json b/packages/i18n/src/locales/zh-TW/translations.json index bec40f2ea..5f3165ecb 100644 --- a/packages/i18n/src/locales/zh-TW/translations.json +++ b/packages/i18n/src/locales/zh-TW/translations.json @@ -316,6 +316,8 @@ "failed_to_remove_project_from_favorites": "無法從我的最愛移除專案。請再試一次。", "project_created_successfully": "專案建立成功", "project_created_successfully_description": "專案建立成功。您現在可以開始新增工作事項。", + "project_name_already_taken": "專案名稱已被使用。", + "project_identifier_already_taken": "專案識別碼已被使用。", "project_cover_image_alt": "專案封面圖片", "name_is_required": "名稱為必填", "title_should_be_less_than_255_characters": "標題不應超過 255 個字元", diff --git a/web/ce/components/projects/create/root.tsx b/web/ce/components/projects/create/root.tsx index 490d3c6b8..72f378ea2 100644 --- a/web/ce/components/projects/create/root.tsx +++ b/web/ce/components/projects/create/root.tsx @@ -49,7 +49,7 @@ export const CreateProjectForm: FC = observer((props) = addProjectToFavorites(workspaceSlug.toString(), projectId).catch(() => { setToast({ type: TOAST_TYPE.ERROR, - title: t("error"), + title: t("toast.error"), message: t("failed_to_remove_project_from_favorites"), }); }); @@ -89,20 +89,27 @@ export const CreateProjectForm: FC = observer((props) = handleNextStep(res.id); }) .catch((err) => { - Object.keys(err?.data ?? {}).map((key) => { + if (err?.data.code === "PROJECT_NAME_ALREADY_EXIST") { setToast({ type: TOAST_TYPE.ERROR, - title: t("error"), - message: err.data[key], + title: t("toast.error"), + message: t("project_name_already_taken"), }); - captureProjectEvent({ - eventName: PROJECT_CREATED, - payload: { - ...formData, - state: "FAILED", - }, + } else if (err?.data.code === "PROJECT_IDENTIFIER_ALREADY_EXIST") { + setToast({ + type: TOAST_TYPE.ERROR, + title: t("toast.error"), + message: t("project_identifier_already_taken"), }); - }); + } else { + Object.keys(err?.data ?? {}).map((key) => { + setToast({ + type: TOAST_TYPE.ERROR, + title: t("error"), + message: err.data[key], + }); + }); + } }); }; From 75f89c4c12f3e96ff3e66fda920cfda49cda9d8a Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Tue, 17 Jun 2025 14:08:50 +0530 Subject: [PATCH 167/201] fix: docker build (#7220) * fix: docker build * fix: build --- packages/constants/src/auth.ts | 5 ++--- packages/utils/src/router.ts | 11 +++++------ 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/constants/src/auth.ts b/packages/constants/src/auth.ts index 01ed762ff..1b6cb9111 100644 --- a/packages/constants/src/auth.ts +++ b/packages/constants/src/auth.ts @@ -71,10 +71,9 @@ export type TAuthErrorInfo = { type: EErrorAlertType; code: EAuthErrorCodes; title: string; - message: React.ReactNode; + message: any; }; - export enum EAdminAuthErrorCodes { // Admin ADMIN_ALREADY_EXIST = "5150", @@ -92,7 +91,7 @@ export type TAdminAuthErrorInfo = { type: EErrorAlertType; code: EAdminAuthErrorCodes; title: string; - message: React.ReactNode; + message: any; }; export enum EAuthErrorCodes { diff --git a/packages/utils/src/router.ts b/packages/utils/src/router.ts index 4b795e7e2..e9355de9f 100644 --- a/packages/utils/src/router.ts +++ b/packages/utils/src/router.ts @@ -1,9 +1,8 @@ -import { ReadonlyURLSearchParams } from "next/navigation"; - -export const generateQueryParams = (searchParams: ReadonlyURLSearchParams, excludedParamKeys?: string[]): string => { +export const generateQueryParams = (searchParams: URLSearchParams, excludedParamKeys?: string[]): string => { const params = new URLSearchParams(searchParams); - excludedParamKeys && excludedParamKeys.forEach((key) => { - params.delete(key); - }); + excludedParamKeys && + excludedParamKeys.forEach((key) => { + params.delete(key); + }); return params.toString(); }; From 53e6a62a12ad9b8b6e7632c97c743fa720012667 Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Tue, 17 Jun 2025 17:06:05 +0530 Subject: [PATCH 168/201] fix: move lucide related constants to ui package (#7226) * fix: move lucide related constants to ui package * chore: update yarn.lock --- packages/constants/src/index.ts | 1 - .../src => ui/src/constants}/icons.ts | 4 +++ packages/ui/src/constants/index.ts | 1 + packages/ui/src/emoji/icons-list.tsx | 2 +- packages/ui/src/emoji/logo.tsx | 3 +- packages/ui/src/emoji/lucide-icons-list.tsx | 3 +- packages/ui/src/index.ts | 36 ++++++++++--------- packages/ui/src/utils/icons.ts | 7 ++++ packages/ui/src/utils/index.ts | 1 + packages/utils/src/emoji.ts | 8 +---- web/core/components/common/logo.tsx | 2 +- 11 files changed, 37 insertions(+), 31 deletions(-) rename packages/{constants/src => ui/src/constants}/icons.ts (99%) create mode 100644 packages/ui/src/constants/index.ts create mode 100644 packages/ui/src/utils/icons.ts create mode 100644 packages/ui/src/utils/index.ts diff --git a/packages/constants/src/index.ts b/packages/constants/src/index.ts index a7452ebe5..d7ccebd31 100644 --- a/packages/constants/src/index.ts +++ b/packages/constants/src/index.ts @@ -5,7 +5,6 @@ export * from "./endpoints"; export * from "./file"; export * from "./filter"; export * from "./graph"; -export * from "./icons"; export * from "./instance"; export * from "./issue"; export * from "./metadata"; diff --git a/packages/constants/src/icons.ts b/packages/ui/src/constants/icons.ts similarity index 99% rename from packages/constants/src/icons.ts rename to packages/ui/src/constants/icons.ts index ab9aa69b3..634d91686 100644 --- a/packages/constants/src/icons.ts +++ b/packages/ui/src/constants/icons.ts @@ -152,6 +152,8 @@ import { CircleChevronDown, UsersRound, ToggleLeft, + Search, + User, } from "lucide-react"; export const MATERIAL_ICONS_LIST = [ @@ -912,6 +914,8 @@ export const LUCIDE_ICONS_LIST = [ { name: "Minus", element: Minus }, { name: "MinusCircle", element: MinusCircle }, { name: "MinusSquare", element: MinusSquare }, + { name: "Search", element: Search }, { name: "ToggleLeft", element: ToggleLeft }, + { name: "User", element: User }, { name: "UsersRound", element: UsersRound }, ]; diff --git a/packages/ui/src/constants/index.ts b/packages/ui/src/constants/index.ts new file mode 100644 index 000000000..6ef8d54d4 --- /dev/null +++ b/packages/ui/src/constants/index.ts @@ -0,0 +1 @@ +export * from "./icons"; diff --git a/packages/ui/src/emoji/icons-list.tsx b/packages/ui/src/emoji/icons-list.tsx index bae22ffb8..7c6d25ba5 100644 --- a/packages/ui/src/emoji/icons-list.tsx +++ b/packages/ui/src/emoji/icons-list.tsx @@ -2,7 +2,7 @@ import { Search } from "lucide-react"; import React, { useEffect, useState } from "react"; // icons import useFontFaceObserver from "use-font-face-observer"; -import { MATERIAL_ICONS_LIST } from "@plane/constants"; +import { MATERIAL_ICONS_LIST } from ".."; import { cn } from "../../helpers"; import { Input } from "../form-fields"; import { InfoIcon } from "../icons"; diff --git a/packages/ui/src/emoji/logo.tsx b/packages/ui/src/emoji/logo.tsx index 0d598bac3..fd2727df6 100644 --- a/packages/ui/src/emoji/logo.tsx +++ b/packages/ui/src/emoji/logo.tsx @@ -1,9 +1,8 @@ import { Emoji } from "emoji-picker-react"; import React, { FC } from "react"; import useFontFaceObserver from "use-font-face-observer"; -// plane imports -import { LUCIDE_ICONS_LIST } from "@plane/constants"; // local imports +import { LUCIDE_ICONS_LIST } from ".."; import { emojiCodeToUnicode } from "./helpers"; export type TEmojiLogoProps = { diff --git a/packages/ui/src/emoji/lucide-icons-list.tsx b/packages/ui/src/emoji/lucide-icons-list.tsx index d569feb26..ee5b2fc20 100644 --- a/packages/ui/src/emoji/lucide-icons-list.tsx +++ b/packages/ui/src/emoji/lucide-icons-list.tsx @@ -1,8 +1,7 @@ import { Search } from "lucide-react"; import React, { useEffect, useState } from "react"; -// plane imports -import { LUCIDE_ICONS_LIST } from "@plane/constants"; // local imports +import { LUCIDE_ICONS_LIST } from ".."; import { cn } from "../../helpers"; import { Input } from "../form-fields"; import { InfoIcon } from "../icons"; diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index 29bf6f248..a21979fa1 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -2,34 +2,36 @@ export * from "./avatar"; export * from "./badge"; export * from "./breadcrumbs"; export * from "./button"; +export * from "./calendar"; +export * from "./card"; +export * from "./collapsible"; +export * from "./color-picker"; +export * from "./constants"; +export * from "./content-wrapper"; export * from "./control-link"; +export * from "./drag-handle"; +export * from "./drop-indicator"; export * from "./dropdown"; export * from "./dropdowns"; export * from "./emoji"; +export * from "./favorite-star"; export * from "./form-fields"; +export * from "./header"; export * from "./hooks"; export * from "./icons"; +export * from "./link"; +export * from "./loader"; export * from "./modals"; +export * from "./popovers"; export * from "./progress"; +export * from "./row"; +export * from "./scroll-area"; export * from "./sortable"; export * from "./spinners"; +export * from "./tables"; +export * from "./tabs"; +export * from "./tag"; export * from "./toast"; export * from "./tooltip"; export * from "./typography"; -export * from "./drag-handle"; -export * from "./drop-indicator"; -export * from "./favorite-star"; -export * from "./loader"; -export * from "./collapsible"; -export * from "./popovers"; -export * from "./tables"; -export * from "./header"; -export * from "./row"; -export * from "./scroll-area"; -export * from "./content-wrapper"; -export * from "./card"; -export * from "./tag"; -export * from "./tabs"; -export * from "./calendar"; -export * from "./color-picker"; -export * from "./link"; +export * from "./utils"; diff --git a/packages/ui/src/utils/icons.ts b/packages/ui/src/utils/icons.ts new file mode 100644 index 000000000..de06cc735 --- /dev/null +++ b/packages/ui/src/utils/icons.ts @@ -0,0 +1,7 @@ +import { LUCIDE_ICONS_LIST } from ".."; + +/** + * Returns a random icon name from the LUCIDE_ICONS_LIST array + */ +export const getRandomIconName = (): string => + LUCIDE_ICONS_LIST[Math.floor(Math.random() * LUCIDE_ICONS_LIST.length)].name; diff --git a/packages/ui/src/utils/index.ts b/packages/ui/src/utils/index.ts new file mode 100644 index 000000000..6ef8d54d4 --- /dev/null +++ b/packages/ui/src/utils/index.ts @@ -0,0 +1 @@ +export * from "./icons"; diff --git a/packages/utils/src/emoji.ts b/packages/utils/src/emoji.ts index 00272f41a..2438cfeb7 100644 --- a/packages/utils/src/emoji.ts +++ b/packages/utils/src/emoji.ts @@ -1,7 +1,7 @@ "use client"; // plane imports -import { LUCIDE_ICONS_LIST, RANDOM_EMOJI_CODES } from "@plane/constants"; +import { RANDOM_EMOJI_CODES } from "@plane/constants"; /** * Converts a hyphen-separated hexadecimal emoji code to its decimal representation @@ -83,9 +83,3 @@ export const groupReactions: (reactions: any[], key: string) => { [key: string]: * @returns {string} A random emoji code */ export const getRandomEmoji = (): string => RANDOM_EMOJI_CODES[Math.floor(Math.random() * RANDOM_EMOJI_CODES.length)]; - -/** - * Returns a random icon name from the LUCIDE_ICONS_LIST array - */ -export const getRandomIconName = (): string => - LUCIDE_ICONS_LIST[Math.floor(Math.random() * LUCIDE_ICONS_LIST.length)].name; diff --git a/web/core/components/common/logo.tsx b/web/core/components/common/logo.tsx index f02fd2896..6e4ddb42c 100644 --- a/web/core/components/common/logo.tsx +++ b/web/core/components/common/logo.tsx @@ -7,8 +7,8 @@ import { Emoji } from "emoji-picker-react"; // eslint-disable-next-line import/order import useFontFaceObserver from "use-font-face-observer"; // plane imports -import { LUCIDE_ICONS_LIST } from "@plane/constants"; import { TLogoProps } from "@plane/types"; +import { LUCIDE_ICONS_LIST } from "@plane/ui"; import { emojiCodeToUnicode } from "@plane/utils"; type Props = { From 89b8cdbe6e22dce1f2d3592de3f2ee0e374e4276 Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Tue, 17 Jun 2025 17:17:04 +0530 Subject: [PATCH 169/201] [WEB-4335] improvement: optimize assignee grouping with improved member scope handling (#7227) --- packages/types/src/issues.d.ts | 5 ++ .../components/issues/issue-layouts/utils.tsx | 32 +++++++- .../components/issues/issue-layouts/utils.tsx | 73 +++++++++---------- 3 files changed, 70 insertions(+), 40 deletions(-) diff --git a/packages/types/src/issues.d.ts b/packages/types/src/issues.d.ts index a630d0ba2..fc7c14ed8 100644 --- a/packages/types/src/issues.d.ts +++ b/packages/types/src/issues.d.ts @@ -220,6 +220,11 @@ export type GroupByColumnTypes = | "created_by" | "team_project"; +type TGetColumns = { + isWorkspaceLevel?: boolean; + projectId?: string; +}; + export interface IGroupByColumn { id: string; name: string; diff --git a/web/ce/components/issues/issue-layouts/utils.tsx b/web/ce/components/issues/issue-layouts/utils.tsx index 1c0eed554..62c8b35e6 100644 --- a/web/ce/components/issues/issue-layouts/utils.tsx +++ b/web/ce/components/issues/issue-layouts/utils.tsx @@ -13,7 +13,7 @@ import { Users, } from "lucide-react"; // types -import { IGroupByColumn, IIssueDisplayProperties, TSpreadsheetColumn } from "@plane/types"; +import { IGroupByColumn, IIssueDisplayProperties, TGetColumns, TSpreadsheetColumn } from "@plane/types"; import { DiceIcon, DoubleCircleIcon, ISvgIcons } from "@plane/ui"; // components import { @@ -32,6 +32,36 @@ import { SpreadsheetSubIssueColumn, SpreadsheetUpdatedOnColumn, } from "@/components/issues/issue-layouts/spreadsheet"; +// store +import { store } from "@/lib/store-context"; + +export type TGetScopeMemberIdsResult = { + memberIds: string[]; + includeNone: boolean; +}; + +export const getScopeMemberIds = ({ isWorkspaceLevel, projectId }: TGetColumns): TGetScopeMemberIdsResult => { + // store values + const { workspaceMemberIds } = store.memberRoot.workspace; + const { projectMemberIds } = store.memberRoot.project; + // derived values + const memberIds = workspaceMemberIds; + + if (isWorkspaceLevel) { + return { memberIds: memberIds ?? [], includeNone: true }; + } + + if (projectId || (projectMemberIds && projectMemberIds.length > 0)) { + const { getProjectMemberIds } = store.memberRoot.project; + const _projectMemberIds = projectId ? getProjectMemberIds(projectId, false) : projectMemberIds; + return { + memberIds: _projectMemberIds ?? [], + includeNone: true, + }; + } + + return { memberIds: [], includeNone: true }; +}; export const getTeamProjectColumns = (): IGroupByColumn[] | undefined => undefined; diff --git a/web/core/components/issues/issue-layouts/utils.tsx b/web/core/components/issues/issue-layouts/utils.tsx index f16bdcc49..510fbf5a6 100644 --- a/web/core/components/issues/issue-layouts/utils.tsx +++ b/web/core/components/issues/issue-layouts/utils.tsx @@ -2,7 +2,6 @@ import { CSSProperties, FC } from "react"; import { extractInstruction } from "@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item"; -import { isEmpty } from "lodash"; import clone from "lodash/clone"; import concat from "lodash/concat"; import isEqual from "lodash/isEqual"; @@ -27,6 +26,7 @@ import { TGroupedIssues, IWorkspaceView, IIssueDisplayFilterOptions, + TGetColumns, } from "@plane/types"; // plane ui import { Avatar, CycleGroupIcon, DiceIcon, ISvgIcons, PriorityIcon, StateGroupIcon } from "@plane/ui"; @@ -37,7 +37,11 @@ import { Logo } from "@/components/common"; // store import { store } from "@/lib/store-context"; // plane web store -import { getTeamProjectColumns, SpreadSheetPropertyIconMap } from "@/plane-web/components/issues/issue-layouts/utils"; +import { + getScopeMemberIds, + getTeamProjectColumns, + SpreadSheetPropertyIconMap, +} from "@/plane-web/components/issues/issue-layouts/utils"; // store import { ISSUE_FILTER_DEFAULT_DATA } from "@/store/issue/helpers/base-issues.store"; import { DEFAULT_DISPLAY_PROPERTIES } from "@/store/issue/issue-details/sub_issues_filter.store"; @@ -60,13 +64,16 @@ export type IssueUpdates = { }; }; -type TGetColumns = { - isWorkspaceLevel?: boolean; - projectId?: string; -}; - export const isWorkspaceLevel = (type: EIssuesStoreType) => - [EIssuesStoreType.PROFILE, EIssuesStoreType.GLOBAL].includes(type) ? true : false; + [ + EIssuesStoreType.PROFILE, + EIssuesStoreType.GLOBAL, + EIssuesStoreType.TEAM, + EIssuesStoreType.TEAM_VIEW, + EIssuesStoreType.PROJECT_VIEW, + ].includes(type) + ? true + : false; type TGetGroupByColumns = { groupBy: GroupByColumnTypes | null; @@ -264,40 +271,28 @@ const getLabelsColumns = ({ isWorkspaceLevel }: TGetColumns): IGroupByColumn[] = }; const getAssigneeColumns = ({ isWorkspaceLevel, projectId }: TGetColumns): IGroupByColumn[] | undefined => { + // store values + const { getUserDetails } = store.memberRoot; + // derived values + const { memberIds, includeNone } = getScopeMemberIds({ isWorkspaceLevel, projectId }); const assigneeColumns: IGroupByColumn[] = []; - const { - project: { projectMemberIds, getProjectMemberIds }, - getUserDetails, - } = store.memberRoot; - // if workspace level - if (isWorkspaceLevel) { - const { workspaceMemberIds } = store.memberRoot.workspace; - if (!workspaceMemberIds) return; - workspaceMemberIds.forEach((memberId) => { - const member = getUserDetails(memberId); - assigneeColumns.push({ - id: memberId, - name: member?.display_name || "", - icon: , - payload: { assignee_ids: [memberId] }, - }); - }); - } else { - // if project level - const _projectMemberIds = projectId ? getProjectMemberIds(projectId, false) : projectMemberIds; - if (!_projectMemberIds) return; - // Map project member ids to group by assignee columns - _projectMemberIds.forEach((memberId) => { - const member = getUserDetails(memberId); - assigneeColumns.push({ - id: memberId, - name: member?.display_name || "", - icon: , - payload: { assignee_ids: [memberId] }, - }); + + if (!memberIds) return []; + + memberIds.forEach((memberId) => { + const member = getUserDetails(memberId); + if (!member) return; + assigneeColumns.push({ + id: memberId, + name: member?.display_name || "", + icon: , + payload: { assignee_ids: [memberId] }, }); + }); + if (includeNone) { + assigneeColumns.push({ id: "None", name: "None", icon: , payload: {} }); } - assigneeColumns.push({ id: "None", name: "None", icon: , payload: {} }); + return assigneeColumns; }; From 8129f5f9696c5614addf663413f5cfac7aa2c1d0 Mon Sep 17 00:00:00 2001 From: Sangeetha Date: Wed, 18 Jun 2025 15:14:21 +0530 Subject: [PATCH 170/201] [WEB-4340] fix: duplicate assignees in user recents (#7216) * fix: duplicate assignees in user recents * chore: optimize filtering logic * chore: filter with deleted_at field * chore: tests for IssueRecentSerializer --- apiserver/plane/app/serializers/workspace.py | 11 ++- .../serializers/test_issue_recent_visit.py | 75 +++++++++++++++++++ 2 files changed, 83 insertions(+), 3 deletions(-) create mode 100644 apiserver/plane/tests/unit/serializers/test_issue_recent_visit.py diff --git a/apiserver/plane/app/serializers/workspace.py b/apiserver/plane/app/serializers/workspace.py index 7a9289266..b4e75a506 100644 --- a/apiserver/plane/app/serializers/workspace.py +++ b/apiserver/plane/app/serializers/workspace.py @@ -1,7 +1,5 @@ # Third party imports from rest_framework import serializers -from rest_framework import status -from rest_framework.response import Response # Module imports from .base import BaseSerializer, DynamicBaseSerializer @@ -198,6 +196,7 @@ class WorkspaceUserLinkSerializer(BaseSerializer): class IssueRecentVisitSerializer(serializers.ModelSerializer): project_identifier = serializers.SerializerMethodField() + assignees = serializers.SerializerMethodField() class Meta: model = Issue @@ -215,9 +214,15 @@ class IssueRecentVisitSerializer(serializers.ModelSerializer): def get_project_identifier(self, obj): project = obj.project - return project.identifier if project else None + def get_assignees(self, obj): + return list( + obj.assignees.filter(issue_assignee__deleted_at__isnull=True).values_list( + "id", flat=True + ) + ) + class ProjectRecentVisitSerializer(serializers.ModelSerializer): project_members = serializers.SerializerMethodField() diff --git a/apiserver/plane/tests/unit/serializers/test_issue_recent_visit.py b/apiserver/plane/tests/unit/serializers/test_issue_recent_visit.py new file mode 100644 index 000000000..72d1f3384 --- /dev/null +++ b/apiserver/plane/tests/unit/serializers/test_issue_recent_visit.py @@ -0,0 +1,75 @@ +import pytest + +from plane.db.models import ( + Workspace, + Project, + Issue, + User, + IssueAssignee, + WorkspaceMember, + ProjectMember, +) +from plane.app.serializers.workspace import IssueRecentVisitSerializer +from django.utils import timezone + + +@pytest.mark.unit +class TestIssueRecentVisitSerializer: + """Test the IssueRecentVisitSerializer""" + + def test_issue_recent_visit_serializer_fields(self, db): + """Test that the serializer includes the correct fields""" + + test_user_1 = User.objects.create( + email="test_user_1@example.com", first_name="Test", last_name="User" + ) + + # To test for deleted issue assignee + test_user_2 = User.objects.create( + email="test_user_2@example.com", + first_name="Other", + last_name="User", + username="some user name", + ) + + workspace = Workspace.objects.create( + name="Test Workspace", slug="test-workspace", owner=test_user_1 + ) + + WorkspaceMember.objects.create(member=test_user_2, role=15, workspace=workspace) + + project = Project.objects.create( + name="Test Project", identifier="test-project", workspace=workspace + ) + ProjectMember.objects.create(project=project, member=test_user_2) + + issue = Issue.objects.create( + name="Test Issue", + workspace=workspace, + project=project, + ) + + IssueAssignee.objects.create(issue=issue, assignee=test_user_1, project=project) + + # Deleted issue assignee + IssueAssignee.objects.create( + issue=issue, + assignee=test_user_2, + project=project, + deleted_at=timezone.now(), + ) + + serialized_data = IssueRecentVisitSerializer( + issue, + ).data + + # Check fields are present and correct + assert "name" in serialized_data + assert "assignees" in serialized_data + assert "project_identifier" in serialized_data + + assert serialized_data["name"] == "Test Issue" + assert serialized_data["project_identifier"] == "TEST-PROJECT" + + # Only including non-deleted issue assignees + assert serialized_data["assignees"] == [test_user_1.id] From 9cdfb2224a153b3f2cd9ba1161a03594cf63abc0 Mon Sep 17 00:00:00 2001 From: JayashTripathy <76092296+JayashTripathy@users.noreply.github.com> Date: Wed, 18 Jun 2025 15:33:06 +0530 Subject: [PATCH 171/201] [WEB-4160]: Context menu close after clicking on menu item of project #7231 --- .../workspace/sidebar/projects-list-item.tsx | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/web/core/components/workspace/sidebar/projects-list-item.tsx b/web/core/components/workspace/sidebar/projects-list-item.tsx index 581054193..d6f436579 100644 --- a/web/core/components/workspace/sidebar/projects-list-item.tsx +++ b/web/core/components/workspace/sidebar/projects-list-item.tsx @@ -7,8 +7,7 @@ import { pointerOutsideOfPreview } from "@atlaskit/pragmatic-drag-and-drop/eleme import { setCustomNativeDragPreview } from "@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview"; import { attachInstruction, extractInstruction } from "@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item"; import { observer } from "mobx-react"; -import Link from "next/link"; -import { useParams } from "next/navigation"; +import { useParams, useRouter } from "next/navigation"; import { createRoot } from "react-dom/client"; import { LinkIcon, Settings, Share2, LogOut, MoreHorizontal, ChevronRight } from "lucide-react"; import { Disclosure, Transition } from "@headlessui/react"; @@ -78,6 +77,7 @@ export const SidebarProjectsListItem: React.FC = observer((props) => { const dragHandleRef = useRef(null); // router const { workspaceSlug, projectId: URLProjectId } = useParams(); + const router = useRouter(); // derived values const project = getPartialProjectById(projectId); // toggle project list open @@ -353,26 +353,26 @@ export const SidebarProjectsListItem: React.FC = observer((props) => { {isAuthorized && ( - - -
- - {t("archives")} -
- + { + router.push(`/${workspaceSlug}/projects/${project?.id}/archives/issues`); + }} + > +
+ + {t("archives")} +
)} { - setIsMenuActive(false); + router.push(`/${workspaceSlug}/settings/projects/${project?.id}`); }} > - -
- - {t("settings")} -
- +
+ + {t("settings")} +
{/* leave project */} {!isAuthorized && ( From c7d17d00b7f2c67459e2203e81d41b6cce5097fb Mon Sep 17 00:00:00 2001 From: Akshita Goyal <36129505+gakshita@users.noreply.github.com> Date: Wed, 18 Jun 2025 15:59:26 +0530 Subject: [PATCH 172/201] [WEB-4017] fix: hooks and store refactoring for issue-details (#7107) * fix: hooks and store splitting for issue-details * fix: refactoring * fix: refactoring * fix: refactor * fix: css --- .../(projects)/browse/[workItem]/page.tsx | 9 +++++++++ web/ce/hooks/use-issue-properties.tsx | 10 ++++++++++ web/core/components/issues/peek-overview/root.tsx | 9 +++++++++ web/core/components/settings/content-wrapper.tsx | 2 +- web/core/hooks/store/use-issue-detail.ts | 2 +- web/core/store/issue/issue-details/root.store.ts | 2 +- web/ee/hooks/use-issue-properties.tsx | 1 + 7 files changed, 32 insertions(+), 3 deletions(-) create mode 100644 web/ce/hooks/use-issue-properties.tsx create mode 100644 web/ee/hooks/use-issue-properties.tsx diff --git a/web/app/(all)/[workspaceSlug]/(projects)/browse/[workItem]/page.tsx b/web/app/(all)/[workspaceSlug]/(projects)/browse/[workItem]/page.tsx index e117d0ab2..735474cd4 100644 --- a/web/app/(all)/[workspaceSlug]/(projects)/browse/[workItem]/page.tsx +++ b/web/app/(all)/[workspaceSlug]/(projects)/browse/[workItem]/page.tsx @@ -6,6 +6,7 @@ import { useParams } from "next/navigation"; import { useTheme } from "next-themes"; import useSWR from "swr"; // plane imports +import { EIssueServiceType } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { Loader } from "@plane/ui"; // components @@ -16,6 +17,7 @@ import { IssueDetailRoot } from "@/components/issues"; import { useAppTheme, useIssueDetail, useProject } from "@/hooks/store"; // assets import { useAppRouter } from "@/hooks/use-app-router"; +import { useWorkItemProperties } from "@/plane-web/hooks/use-issue-properties"; import { ProjectAuthWrapper } from "@/plane-web/layouts/project-wrapper"; import emptyIssueDark from "@/public/empty-state/search/issues-dark.webp"; import emptyIssueLight from "@/public/empty-state/search/issues-light.webp"; @@ -53,6 +55,13 @@ const IssueDetailsPage = observer(() => { const issueLoader = !issue || isLoading; const pageTitle = project && issue ? `${project?.identifier}-${issue?.sequence_id} ${issue?.name}` : undefined; + useWorkItemProperties( + projectId, + workspaceSlug.toString(), + issueId, + issue?.is_epic ? EIssueServiceType.EPICS : EIssueServiceType.ISSUES + ); + useEffect(() => { const handleToggleIssueDetailSidebar = () => { if (window && window.innerWidth < 768) { diff --git a/web/ce/hooks/use-issue-properties.tsx b/web/ce/hooks/use-issue-properties.tsx new file mode 100644 index 000000000..c4d35d6ad --- /dev/null +++ b/web/ce/hooks/use-issue-properties.tsx @@ -0,0 +1,10 @@ +import { TIssueServiceType } from "@plane/types"; + +export const useWorkItemProperties = ( + projectId: string | null | undefined, + workspaceSlug: string | null | undefined, + workItemId: string | null | undefined, + issueServiceType: TIssueServiceType +) => { + if (!projectId || !workspaceSlug || !workItemId) return; +}; diff --git a/web/core/components/issues/peek-overview/root.tsx b/web/core/components/issues/peek-overview/root.tsx index 9d6653440..5ea7855bb 100644 --- a/web/core/components/issues/peek-overview/root.tsx +++ b/web/core/components/issues/peek-overview/root.tsx @@ -12,6 +12,7 @@ import { ISSUE_RESTORED, EUserPermissions, EUserPermissionsLevel, + EIssueServiceType, } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { TIssue, IWorkItemPeekOverview } from "@plane/types"; @@ -23,6 +24,7 @@ import { IssueView, TIssueOperations } from "@/components/issues"; // hooks import { useEventTracker, useIssueDetail, useIssues, useUserPermissions } from "@/hooks/store"; import { useIssueStoreType } from "@/hooks/use-issue-layout-store"; +import { useWorkItemProperties } from "@/plane-web/hooks/use-issue-properties"; export const IssuePeekOverview: FC = observer((props) => { @@ -51,6 +53,13 @@ export const IssuePeekOverview: FC = observer((props) => const storeType = issueStoreFromProps ?? issueStoreType; const { issues } = useIssues(storeType); const { captureIssueEvent } = useEventTracker(); + + useWorkItemProperties( + peekIssue?.projectId, + peekIssue?.workspaceSlug, + peekIssue?.issueId, + storeType === EIssuesStoreType.EPIC ? EIssueServiceType.EPICS : EIssueServiceType.ISSUES + ); // state const [error, setError] = useState(false); diff --git a/web/core/components/settings/content-wrapper.tsx b/web/core/components/settings/content-wrapper.tsx index dce3d197c..87a695416 100644 --- a/web/core/components/settings/content-wrapper.tsx +++ b/web/core/components/settings/content-wrapper.tsx @@ -12,7 +12,7 @@ export const SettingsContentWrapper = observer((props: TProps) => { return (
diff --git a/web/core/hooks/store/use-issue-detail.ts b/web/core/hooks/store/use-issue-detail.ts index 309bfedba..dd96ddf05 100644 --- a/web/core/hooks/store/use-issue-detail.ts +++ b/web/core/hooks/store/use-issue-detail.ts @@ -4,7 +4,7 @@ import { TIssueServiceType } from "@plane/types"; // mobx store import { StoreContext } from "@/lib/store-context"; // types -import { IIssueDetail } from "@/store/issue/issue-details/root.store"; +import { IIssueDetail } from "@/plane-web/store/issue/issue-details/root.store"; export const useIssueDetail = (serviceType: TIssueServiceType = EIssueServiceType.ISSUES): IIssueDetail => { const context = useContext(StoreContext); diff --git a/web/core/store/issue/issue-details/root.store.ts b/web/core/store/issue/issue-details/root.store.ts index 1d795dd94..69248ccec 100644 --- a/web/core/store/issue/issue-details/root.store.ts +++ b/web/core/store/issue/issue-details/root.store.ts @@ -114,7 +114,7 @@ export interface IIssueDetail relation: IIssueRelationStore; } -export class IssueDetail implements IIssueDetail { +export abstract class IssueDetail implements IIssueDetail { // observables peekIssue: TPeekIssue | undefined = undefined; relationKey: TIssueRelationTypes | null = null; diff --git a/web/ee/hooks/use-issue-properties.tsx b/web/ee/hooks/use-issue-properties.tsx new file mode 100644 index 000000000..05a59ba56 --- /dev/null +++ b/web/ee/hooks/use-issue-properties.tsx @@ -0,0 +1 @@ +export * from "ce/hooks/use-issue-properties"; From d65f0e264e10a61ddece6d1ab5ab7cf1865a487e Mon Sep 17 00:00:00 2001 From: Akshita Goyal <36129505+gakshita@users.noreply.github.com> Date: Wed, 18 Jun 2025 16:08:11 +0530 Subject: [PATCH 173/201] [WEB-4327] Chore PAT permissions (#7224) * chore: improved pat permissions * fix: err message * fix: removed permission from backend * [WEB-4330] refactor: update API token endpoints to use user context instead of workspace slug - Changed URL patterns for API token endpoints to use "users/api-tokens/" instead of "workspaces//api-tokens/". - Refactored ApiTokenEndpoint methods to remove workspace slug parameter and adjust database queries accordingly. - Added new test cases for API token creation, retrieval, deletion, and updates, including support for bot users and minimal data submissions. * fix: removed workspace slug from api-tokens * fix: refactor * chore: url.py code rabbit suggestion * fix: APITokenService moved to package --------- Co-authored-by: Dheeraj Kumar Ketireddy Co-authored-by: sriramveeraghanta --- apiserver/plane/app/urls/api.py | 6 +- apiserver/plane/app/views/api.py | 30 +- apiserver/plane/tests/conftest.py | 48 ++- .../tests/contract/app/test_api_token.py | 372 ++++++++++++++++++ .../src/developer/api-token.service.ts | 20 +- .../settings/account/api-tokens/page.tsx | 20 +- .../(settings)/settings/account/sidebar.tsx | 16 +- .../api-token/delete-token-modal.tsx | 11 +- .../api-token/modal/create-token-modal.tsx | 13 +- web/core/components/settings/heading.tsx | 4 +- web/core/constants/fetch-keys.ts | 2 +- web/core/services/api_token.service.ts | 41 -- web/core/store/workspace/api-token.store.ts | 32 +- 13 files changed, 469 insertions(+), 146 deletions(-) create mode 100644 apiserver/plane/tests/contract/app/test_api_token.py delete mode 100644 web/core/services/api_token.service.ts diff --git a/apiserver/plane/app/urls/api.py b/apiserver/plane/app/urls/api.py index 592ff53b5..c74aeddbf 100644 --- a/apiserver/plane/app/urls/api.py +++ b/apiserver/plane/app/urls/api.py @@ -4,14 +4,14 @@ from plane.app.views import ApiTokenEndpoint, ServiceApiTokenEndpoint urlpatterns = [ # API Tokens path( - "workspaces//api-tokens/", + "users/api-tokens/", ApiTokenEndpoint.as_view(), name="api-tokens", ), path( - "workspaces//api-tokens//", + "users/api-tokens//", ApiTokenEndpoint.as_view(), - name="api-tokens", + name="api-tokens-details", ), path( "workspaces//service-api-tokens/", diff --git a/apiserver/plane/app/views/api.py b/apiserver/plane/app/views/api.py index 98a2588a1..fa7cc7466 100644 --- a/apiserver/plane/app/views/api.py +++ b/apiserver/plane/app/views/api.py @@ -1,8 +1,10 @@ # Python import from uuid import uuid4 +from typing import Optional # Third party from rest_framework.response import Response +from rest_framework.request import Request from rest_framework import status # Module import @@ -13,12 +15,9 @@ from plane.app.permissions import WorkspaceEntityPermission class ApiTokenEndpoint(BaseAPIView): - permission_classes = [WorkspaceEntityPermission] - - def post(self, request, slug): + def post(self, request: Request) -> Response: label = request.data.get("label", str(uuid4().hex)) description = request.data.get("description", "") - workspace = Workspace.objects.get(slug=slug) expired_at = request.data.get("expired_at", None) # Check the user type @@ -28,7 +27,6 @@ class ApiTokenEndpoint(BaseAPIView): label=label, description=description, user=request.user, - workspace=workspace, user_type=user_type, expired_at=expired_at, ) @@ -37,29 +35,23 @@ class ApiTokenEndpoint(BaseAPIView): # Token will be only visible while creating return Response(serializer.data, status=status.HTTP_201_CREATED) - def get(self, request, slug, pk=None): + def get(self, request: Request, pk: Optional[str] = None) -> Response: if pk is None: - api_tokens = APIToken.objects.filter( - user=request.user, workspace__slug=slug, is_service=False - ) + api_tokens = APIToken.objects.filter(user=request.user, is_service=False) serializer = APITokenReadSerializer(api_tokens, many=True) return Response(serializer.data, status=status.HTTP_200_OK) else: - api_tokens = APIToken.objects.get( - user=request.user, workspace__slug=slug, pk=pk - ) + api_tokens = APIToken.objects.get(user=request.user, pk=pk) serializer = APITokenReadSerializer(api_tokens) return Response(serializer.data, status=status.HTTP_200_OK) - def delete(self, request, slug, pk): - api_token = APIToken.objects.get( - workspace__slug=slug, user=request.user, pk=pk, is_service=False - ) + def delete(self, request: Request, pk: str) -> Response: + api_token = APIToken.objects.get(user=request.user, pk=pk, is_service=False) api_token.delete() return Response(status=status.HTTP_204_NO_CONTENT) - def patch(self, request, slug, pk): - api_token = APIToken.objects.get(workspace__slug=slug, user=request.user, pk=pk) + def patch(self, request: Request, pk: str) -> Response: + api_token = APIToken.objects.get(user=request.user, pk=pk) serializer = APITokenSerializer(api_token, data=request.data, partial=True) if serializer.is_valid(): serializer.save() @@ -70,7 +62,7 @@ class ApiTokenEndpoint(BaseAPIView): class ServiceApiTokenEndpoint(BaseAPIView): permission_classes = [WorkspaceEntityPermission] - def post(self, request, slug): + def post(self, request: Request, slug: str) -> Response: workspace = Workspace.objects.get(slug=slug) api_token = APIToken.objects.filter( diff --git a/apiserver/plane/tests/conftest.py b/apiserver/plane/tests/conftest.py index ce0d3be2b..832558810 100644 --- a/apiserver/plane/tests/conftest.py +++ b/apiserver/plane/tests/conftest.py @@ -27,7 +27,7 @@ def user_data(): "email": "test@plane.so", "password": "test-password", "first_name": "Test", - "last_name": "User" + "last_name": "User", } @@ -37,7 +37,7 @@ def create_user(db, user_data): user = User.objects.create( email=user_data["email"], first_name=user_data["first_name"], - last_name=user_data["last_name"] + last_name=user_data["last_name"], ) user.set_password(user_data["password"]) user.save() @@ -69,10 +69,52 @@ def session_client(api_client, create_user): return api_client +@pytest.fixture +def create_bot_user(db): + """Create and return a bot user instance""" + from uuid import uuid4 + + unique_id = uuid4().hex[:8] + user = User.objects.create( + email=f"bot-{unique_id}@plane.so", + username=f"bot_user_{unique_id}", + first_name="Bot", + last_name="User", + is_bot=True, + ) + user.set_password("bot@123") + user.save() + return user + + +@pytest.fixture +def api_token_data(): + """Return sample API token data for testing""" + from django.utils import timezone + from datetime import timedelta + + return { + "label": "Test API Token", + "description": "Test description for API token", + "expired_at": (timezone.now() + timedelta(days=30)).isoformat(), + } + + +@pytest.fixture +def create_api_token_for_user(db, create_user): + """Create and return an API token for a specific user""" + return APIToken.objects.create( + label="Test Token", + description="Test token description", + user=create_user, + user_type=0, + ) + + @pytest.fixture def plane_server(live_server): """ Renamed version of live_server fixture to avoid name clashes. Returns a live Django server for testing HTTP requests. """ - return live_server \ No newline at end of file + return live_server diff --git a/apiserver/plane/tests/contract/app/test_api_token.py b/apiserver/plane/tests/contract/app/test_api_token.py new file mode 100644 index 000000000..5160788de --- /dev/null +++ b/apiserver/plane/tests/contract/app/test_api_token.py @@ -0,0 +1,372 @@ +import pytest +from datetime import timedelta +from uuid import uuid4 +from django.urls import reverse +from django.utils import timezone +from rest_framework import status + +from plane.db.models import APIToken, User + + +@pytest.mark.contract +class TestApiTokenEndpoint: + """Test cases for ApiTokenEndpoint""" + + # POST /user/api-tokens/ tests + @pytest.mark.django_db + def test_create_api_token_success( + self, session_client, create_user, api_token_data + ): + """Test successful API token creation""" + # Arrange + session_client.force_authenticate(user=create_user) + url = reverse("api-tokens") + + # Act + response = session_client.post(url, api_token_data, format="json") + + # Assert + assert response.status_code == status.HTTP_201_CREATED + assert "token" in response.data + assert response.data["label"] == api_token_data["label"] + assert response.data["description"] == api_token_data["description"] + assert response.data["user_type"] == 0 # Human user + + # Verify token was created in database + token = APIToken.objects.get(pk=response.data["id"]) + assert token.user == create_user + assert token.label == api_token_data["label"] + + @pytest.mark.django_db + def test_create_api_token_for_bot_user( + self, session_client, create_bot_user, api_token_data + ): + """Test API token creation for bot user""" + # Arrange + session_client.force_authenticate(user=create_bot_user) + url = reverse("api-tokens") + + # Act + response = session_client.post(url, api_token_data, format="json") + + # Assert + assert response.status_code == status.HTTP_201_CREATED + assert response.data["user_type"] == 1 # Bot user + + @pytest.mark.django_db + def test_create_api_token_minimal_data(self, session_client, create_user): + """Test API token creation with minimal data""" + # Arrange + session_client.force_authenticate(user=create_user) + url = reverse("api-tokens") + + # Act + response = session_client.post(url, {}, format="json") + + # Assert + assert response.status_code == status.HTTP_201_CREATED + assert "token" in response.data + assert len(response.data["label"]) == 32 # UUID hex length + assert response.data["description"] == "" + + @pytest.mark.django_db + def test_create_api_token_with_expiry(self, session_client, create_user): + """Test API token creation with expiry date""" + # Arrange + session_client.force_authenticate(user=create_user) + url = reverse("api-tokens") + future_date = timezone.now() + timedelta(days=30) + data = {"label": "Expiring Token", "expired_at": future_date.isoformat()} + + # Act + response = session_client.post(url, data, format="json") + + # Assert + assert response.status_code == status.HTTP_201_CREATED + + # Verify expiry date was set + token = APIToken.objects.get(pk=response.data["id"]) + assert token.expired_at is not None + + @pytest.mark.django_db + def test_create_api_token_unauthenticated(self, api_client, api_token_data): + """Test API token creation without authentication""" + # Arrange + url = reverse("api-tokens") + + # Act + response = api_client.post(url, api_token_data, format="json") + + # Assert + assert response.status_code == status.HTTP_401_UNAUTHORIZED + + # GET /user/api-tokens/ tests + @pytest.mark.django_db + def test_get_all_api_tokens(self, session_client, create_user): + """Test retrieving all API tokens for user""" + # Arrange + session_client.force_authenticate(user=create_user) + + # Create multiple tokens + APIToken.objects.create(label="Token 1", user=create_user, user_type=0) + APIToken.objects.create(label="Token 2", user=create_user, user_type=0) + # Create a service token (should be excluded) + APIToken.objects.create( + label="Service Token", user=create_user, user_type=0, is_service=True + ) + url = reverse("api-tokens") + + # Act + response = session_client.get(url) + + # Assert + assert response.status_code == status.HTTP_200_OK + assert len(response.data) == 2 # Only non-service tokens + assert all(token["is_service"] is False for token in response.data) + + @pytest.mark.django_db + def test_get_empty_api_tokens_list(self, session_client, create_user): + """Test retrieving API tokens when none exist""" + # Arrange + session_client.force_authenticate(user=create_user) + url = reverse("api-tokens") + + # Act + response = session_client.get(url) + + # Assert + assert response.status_code == status.HTTP_200_OK + assert response.data == [] + + # GET /user/api-tokens// tests + @pytest.mark.django_db + def test_get_specific_api_token( + self, session_client, create_user, create_api_token_for_user + ): + """Test retrieving a specific API token""" + # Arrange + session_client.force_authenticate(user=create_user) + url = reverse("api-tokens", kwargs={"pk": create_api_token_for_user.pk}) + + # Act + response = session_client.get(url) + + # Assert + assert response.status_code == status.HTTP_200_OK + assert str(response.data["id"]) == str(create_api_token_for_user.pk) + assert response.data["label"] == create_api_token_for_user.label + assert ( + "token" not in response.data + ) # Token should not be visible in read serializer + + @pytest.mark.django_db + def test_get_nonexistent_api_token(self, session_client, create_user): + """Test retrieving a non-existent API token""" + # Arrange + session_client.force_authenticate(user=create_user) + fake_pk = uuid4() + url = reverse("api-tokens", kwargs={"pk": fake_pk}) + + # Act + response = session_client.get(url) + + # Assert + assert response.status_code == status.HTTP_404_NOT_FOUND + + @pytest.mark.django_db + def test_get_other_users_api_token(self, session_client, create_user, db): + """Test retrieving another user's API token (should fail)""" + # Arrange + # Create another user and their token with unique email and username + unique_id = uuid4().hex[:8] + unique_email = f"other-{unique_id}@plane.so" + unique_username = f"other_user_{unique_id}" + other_user = User.objects.create(email=unique_email, username=unique_username) + other_token = APIToken.objects.create( + label="Other Token", user=other_user, user_type=0 + ) + session_client.force_authenticate(user=create_user) + url = reverse("api-tokens", kwargs={"pk": other_token.pk}) + + # Act + response = session_client.get(url) + + # Assert + assert response.status_code == status.HTTP_404_NOT_FOUND + + # DELETE /user/api-tokens// tests + @pytest.mark.django_db + def test_delete_api_token_success( + self, session_client, create_user, create_api_token_for_user + ): + """Test successful API token deletion""" + # Arrange + session_client.force_authenticate(user=create_user) + url = reverse("api-tokens", kwargs={"pk": create_api_token_for_user.pk}) + + # Act + response = session_client.delete(url) + + # Assert + assert response.status_code == status.HTTP_204_NO_CONTENT + assert not APIToken.objects.filter(pk=create_api_token_for_user.pk).exists() + + @pytest.mark.django_db + def test_delete_nonexistent_api_token(self, session_client, create_user): + """Test deleting a non-existent API token""" + # Arrange + session_client.force_authenticate(user=create_user) + fake_pk = uuid4() + url = reverse("api-tokens", kwargs={"pk": fake_pk}) + + # Act + response = session_client.delete(url) + + # Assert + assert response.status_code == status.HTTP_404_NOT_FOUND + + @pytest.mark.django_db + def test_delete_other_users_api_token(self, session_client, create_user, db): + """Test deleting another user's API token (should fail)""" + # Arrange + # Create another user and their token with unique email and username + unique_id = uuid4().hex[:8] + unique_email = f"delete-other-{unique_id}@plane.so" + unique_username = f"delete_other_user_{unique_id}" + other_user = User.objects.create(email=unique_email, username=unique_username) + other_token = APIToken.objects.create( + label="Other Token", user=other_user, user_type=0 + ) + session_client.force_authenticate(user=create_user) + url = reverse("api-tokens", kwargs={"pk": other_token.pk}) + + # Act + response = session_client.delete(url) + + # Assert + assert response.status_code == status.HTTP_404_NOT_FOUND + # Verify token still exists + assert APIToken.objects.filter(pk=other_token.pk).exists() + + @pytest.mark.django_db + def test_delete_service_api_token_forbidden(self, session_client, create_user): + """Test deleting a service API token (should fail)""" + # Arrange + service_token = APIToken.objects.create( + label="Service Token", user=create_user, user_type=0, is_service=True + ) + session_client.force_authenticate(user=create_user) + url = reverse("api-tokens", kwargs={"pk": service_token.pk}) + + # Act + response = session_client.delete(url) + + # Assert + assert response.status_code == status.HTTP_404_NOT_FOUND + # Verify token still exists + assert APIToken.objects.filter(pk=service_token.pk).exists() + + # PATCH /user/api-tokens// tests + @pytest.mark.django_db + def test_patch_api_token_success( + self, session_client, create_user, create_api_token_for_user + ): + """Test successful API token update""" + # Arrange + session_client.force_authenticate(user=create_user) + url = reverse("api-tokens", kwargs={"pk": create_api_token_for_user.pk}) + update_data = { + "label": "Updated Token Label", + "description": "Updated description", + } + + # Act + response = session_client.patch(url, update_data, format="json") + + # Assert + assert response.status_code == status.HTTP_200_OK + assert response.data["label"] == update_data["label"] + assert response.data["description"] == update_data["description"] + + # Verify database was updated + create_api_token_for_user.refresh_from_db() + assert create_api_token_for_user.label == update_data["label"] + assert create_api_token_for_user.description == update_data["description"] + + @pytest.mark.django_db + def test_patch_api_token_partial_update( + self, session_client, create_user, create_api_token_for_user + ): + """Test partial API token update""" + # Arrange + session_client.force_authenticate(user=create_user) + url = reverse("api-tokens", kwargs={"pk": create_api_token_for_user.pk}) + original_description = create_api_token_for_user.description + update_data = {"label": "Only Label Updated"} + + # Act + response = session_client.patch(url, update_data, format="json") + + # Assert + assert response.status_code == status.HTTP_200_OK + assert response.data["label"] == update_data["label"] + assert response.data["description"] == original_description + + @pytest.mark.django_db + def test_patch_nonexistent_api_token(self, session_client, create_user): + """Test updating a non-existent API token""" + # Arrange + session_client.force_authenticate(user=create_user) + fake_pk = uuid4() + url = reverse("api-tokens", kwargs={"pk": fake_pk}) + update_data = {"label": "New Label"} + + # Act + response = session_client.patch(url, update_data, format="json") + + # Assert + assert response.status_code == status.HTTP_404_NOT_FOUND + + @pytest.mark.django_db + def test_patch_other_users_api_token(self, session_client, create_user, db): + """Test updating another user's API token (should fail)""" + # Arrange + # Create another user and their token with unique email and username + unique_id = uuid4().hex[:8] + unique_email = f"patch-other-{unique_id}@plane.so" + unique_username = f"patch_other_user_{unique_id}" + other_user = User.objects.create(email=unique_email, username=unique_username) + other_token = APIToken.objects.create( + label="Other Token", user=other_user, user_type=0 + ) + session_client.force_authenticate(user=create_user) + url = reverse("api-tokens", kwargs={"pk": other_token.pk}) + update_data = {"label": "Hacked Label"} + + # Act + response = session_client.patch(url, update_data, format="json") + + # Assert + assert response.status_code == status.HTTP_404_NOT_FOUND + + # Verify token was not updated + other_token.refresh_from_db() + assert other_token.label == "Other Token" + + # Authentication tests + @pytest.mark.django_db + def test_all_endpoints_require_authentication(self, api_client): + """Test that all endpoints require authentication""" + # Arrange + endpoints = [ + (reverse("api-tokens"), "get"), + (reverse("api-tokens"), "post"), + (reverse("api-tokens", kwargs={"pk": uuid4()}), "get"), + (reverse("api-tokens", kwargs={"pk": uuid4()}), "patch"), + (reverse("api-tokens", kwargs={"pk": uuid4()}), "delete"), + ] + + # Act & Assert + for url, method in endpoints: + response = getattr(api_client, method)(url) + assert response.status_code == status.HTTP_401_UNAUTHORIZED diff --git a/packages/services/src/developer/api-token.service.ts b/packages/services/src/developer/api-token.service.ts index 74dc9135d..703ec9d32 100644 --- a/packages/services/src/developer/api-token.service.ts +++ b/packages/services/src/developer/api-token.service.ts @@ -9,12 +9,11 @@ export class APITokenService extends APIService { /** * Retrieves all API tokens for a specific workspace - * @param {string} workspaceSlug - The unique identifier for the workspace * @returns {Promise} Array of API tokens associated with the workspace * @throws {Error} Throws response data if the request fails */ - async list(workspaceSlug: string): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/api-tokens/`) + async list(): Promise { + return this.get(`/api/users/api-tokens/`) .then((response) => response?.data) .catch((error) => { throw error?.response?.data; @@ -23,13 +22,12 @@ export class APITokenService extends APIService { /** * Retrieves a specific API token by its ID - * @param {string} workspaceSlug - The unique identifier for the workspace * @param {string} tokenId - The unique identifier of the API token * @returns {Promise} The requested API token's details * @throws {Error} Throws response data if the request fails */ - async retrieve(workspaceSlug: string, tokenId: string): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/api-tokens/${tokenId}`) + async retrieve(tokenId: string): Promise { + return this.get(`/api/users/api-tokens/${tokenId}`) .then((response) => response?.data) .catch((error) => { throw error?.response?.data; @@ -38,13 +36,12 @@ export class APITokenService extends APIService { /** * Creates a new API token for a workspace - * @param {string} workspaceSlug - The unique identifier for the workspace * @param {Partial} data - The data for creating the new API token * @returns {Promise} The newly created API token * @throws {Error} Throws response data if the request fails */ - async create(workspaceSlug: string, data: Partial): Promise { - return this.post(`/api/workspaces/${workspaceSlug}/api-tokens/`, data) + async create(data: Partial): Promise { + return this.post(`/api/users/api-tokens/`, data) .then((response) => response?.data) .catch((error) => { throw error?.response?.data; @@ -53,13 +50,12 @@ export class APITokenService extends APIService { /** * Deletes a specific API token from the workspace - * @param {string} workspaceSlug - The unique identifier for the workspace * @param {string} tokenId - The unique identifier of the API token to delete * @returns {Promise} The deleted API token's details * @throws {Error} Throws response data if the request fails */ - async destroy(workspaceSlug: string, tokenId: string): Promise { - return this.delete(`/api/workspaces/${workspaceSlug}/api-tokens/${tokenId}`) + async destroy(tokenId: string): Promise { + return this.delete(`/api/users/api-tokens/${tokenId}`) .then((response) => response?.data) .catch((error) => { throw error?.response?.data; diff --git a/web/app/(all)/[workspaceSlug]/(settings)/settings/account/api-tokens/page.tsx b/web/app/(all)/[workspaceSlug]/(settings)/settings/account/api-tokens/page.tsx index cbbdb3a55..9a1883255 100644 --- a/web/app/(all)/[workspaceSlug]/(settings)/settings/account/api-tokens/page.tsx +++ b/web/app/(all)/[workspaceSlug]/(settings)/settings/account/api-tokens/page.tsx @@ -2,24 +2,21 @@ import React, { useState } from "react"; import { observer } from "mobx-react"; -import { useParams } from "next/navigation"; import useSWR from "swr"; // plane imports -import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; // component +import { APITokenService } from "@plane/services"; import { ApiTokenListItem, CreateApiTokenModal } from "@/components/api-token"; -import { NotAuthorizedView } from "@/components/auth-screens"; import { PageHead } from "@/components/core"; import { DetailedEmptyState } from "@/components/empty-state"; import { SettingsHeading } from "@/components/settings"; import { APITokenSettingsLoader } from "@/components/ui"; import { API_TOKENS_LIST } from "@/constants/fetch-keys"; // store hooks -import { useUserPermissions, useWorkspace } from "@/hooks/store"; +import { useWorkspace } from "@/hooks/store"; import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; // services -import { APITokenService } from "@/services/api_token.service"; const apiTokenService = new APITokenService(); @@ -27,30 +24,19 @@ const ApiTokensPage = observer(() => { // states const [isCreateTokenModalOpen, setIsCreateTokenModalOpen] = useState(false); // router - const { workspaceSlug } = useParams(); // plane hooks const { t } = useTranslation(); // store hooks const { currentWorkspace } = useWorkspace(); - const { workspaceUserInfo, allowPermissions } = useUserPermissions(); // derived values - const canPerformWorkspaceAdminActions = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE); const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/workspace-settings/api-tokens" }); - const { data: tokens } = useSWR( - workspaceSlug && canPerformWorkspaceAdminActions ? API_TOKENS_LIST(workspaceSlug.toString()) : null, - () => - workspaceSlug && canPerformWorkspaceAdminActions ? apiTokenService.getApiTokens(workspaceSlug.toString()) : null - ); + const { data: tokens } = useSWR(API_TOKENS_LIST, () => apiTokenService.list()); const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - ${t("workspace_settings.settings.api_tokens.title")}` : undefined; - if (workspaceUserInfo && !canPerformWorkspaceAdminActions) { - return ; - } - if (!tokens) { return ; } diff --git a/web/app/(all)/[workspaceSlug]/(settings)/settings/account/sidebar.tsx b/web/app/(all)/[workspaceSlug]/(settings)/settings/account/sidebar.tsx index addc59596..7153e11be 100644 --- a/web/app/(all)/[workspaceSlug]/(settings)/settings/account/sidebar.tsx +++ b/web/app/(all)/[workspaceSlug]/(settings)/settings/account/sidebar.tsx @@ -2,18 +2,12 @@ import { observer } from "mobx-react"; import { useParams, usePathname } from "next/navigation"; import { CircleUser, Activity, Bell, CircleUserRound, KeyRound, Settings2, Blocks, Lock } from "lucide-react"; // plane imports -import { - EUserPermissions, - EUserPermissionsLevel, - GROUPED_PROFILE_SETTINGS, - PROFILE_SETTINGS_CATEGORIES, - PROFILE_SETTINGS_CATEGORY, -} from "@plane/constants"; +import { GROUPED_PROFILE_SETTINGS, PROFILE_SETTINGS_CATEGORIES } from "@plane/constants"; import { getFileURL } from "@plane/utils"; // components import { SettingsSidebar } from "@/components/settings"; // hooks -import { useUser, useUserPermissions } from "@/hooks/store/user"; +import { useUser } from "@/hooks/store/user"; const ICONS = { profile: CircleUser, @@ -44,14 +38,10 @@ export const ProfileSidebar = observer((props: TProfileSidebarProps) => { // store hooks const { data: currentUser } = useUser(); - const { allowPermissions } = useUserPermissions(); - const isAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE); return ( isAdmin || category !== PROFILE_SETTINGS_CATEGORY.DEVELOPER - )} + categories={PROFILE_SETTINGS_CATEGORIES} groupedSettings={GROUPED_PROFILE_SETTINGS} workspaceSlug={workspaceSlug?.toString() ?? ""} isActive={(data: { href: string }) => pathname === `/${workspaceSlug}${data.href}/`} diff --git a/web/core/components/api-token/delete-token-modal.tsx b/web/core/components/api-token/delete-token-modal.tsx index ecc85a558..eed0ecdb9 100644 --- a/web/core/components/api-token/delete-token-modal.tsx +++ b/web/core/components/api-token/delete-token-modal.tsx @@ -1,17 +1,15 @@ "use client"; import { useState, FC } from "react"; -import { useParams } from "next/navigation"; import { mutate } from "swr"; // types import { useTranslation } from "@plane/i18n"; +import { APITokenService } from "@plane/services"; import { IApiToken } from "@plane/types"; // ui import { AlertModalCore, TOAST_TYPE, setToast } from "@plane/ui"; // fetch-keys import { API_TOKENS_LIST } from "@/constants/fetch-keys"; -// services -import { APITokenService } from "@/services/api_token.service"; type Props = { isOpen: boolean; @@ -26,7 +24,6 @@ export const DeleteApiTokenModal: FC = (props) => { // states const [deleteLoading, setDeleteLoading] = useState(false); // router params - const { workspaceSlug } = useParams(); const { t } = useTranslation(); const handleClose = () => { @@ -35,12 +32,10 @@ export const DeleteApiTokenModal: FC = (props) => { }; const handleDeletion = async () => { - if (!workspaceSlug) return; - setDeleteLoading(true); await apiTokenService - .deleteApiToken(workspaceSlug.toString(), tokenId) + .destroy(tokenId) .then(() => { setToast({ type: TOAST_TYPE.SUCCESS, @@ -49,7 +44,7 @@ export const DeleteApiTokenModal: FC = (props) => { }); mutate( - API_TOKENS_LIST(workspaceSlug.toString()), + API_TOKENS_LIST, (prevData) => (prevData ?? []).filter((token) => token.id !== tokenId), false ); diff --git a/web/core/components/api-token/modal/create-token-modal.tsx b/web/core/components/api-token/modal/create-token-modal.tsx index a848520b1..94d72c56d 100644 --- a/web/core/components/api-token/modal/create-token-modal.tsx +++ b/web/core/components/api-token/modal/create-token-modal.tsx @@ -1,9 +1,9 @@ "use client"; import React, { useState } from "react"; -import { useParams } from "next/navigation"; import { mutate } from "swr"; // types +import { APITokenService } from "@plane/services"; import { IApiToken } from "@plane/types"; // ui import { EModalPosition, EModalWidth, ModalCore, TOAST_TYPE, setToast } from "@plane/ui"; @@ -14,7 +14,6 @@ import { CreateApiTokenForm, GeneratedTokenDetails } from "@/components/api-toke import { API_TOKENS_LIST } from "@/constants/fetch-keys"; // helpers // services -import { APITokenService } from "@/services/api_token.service"; type Props = { isOpen: boolean; @@ -29,8 +28,6 @@ export const CreateApiTokenModal: React.FC = (props) => { // states const [neverExpires, setNeverExpires] = useState(false); const [generatedToken, setGeneratedToken] = useState(null); - // router - const { workspaceSlug } = useParams(); const handleClose = () => { onClose(); @@ -53,17 +50,15 @@ export const CreateApiTokenModal: React.FC = (props) => { }; const handleCreateToken = async (data: Partial) => { - if (!workspaceSlug) return; - // make the request to generate the token await apiTokenService - .createApiToken(workspaceSlug.toString(), data) + .create(data) .then((res) => { setGeneratedToken(res); downloadSecretKey(res); mutate( - API_TOKENS_LIST(workspaceSlug.toString()), + API_TOKENS_LIST, (prevData) => { if (!prevData) return; @@ -76,7 +71,7 @@ export const CreateApiTokenModal: React.FC = (props) => { setToast({ type: TOAST_TYPE.ERROR, title: "Error!", - message: err.message, + message: err.message || err.detail, }); throw err; diff --git a/web/core/components/settings/heading.tsx b/web/core/components/settings/heading.tsx index 40405a1ec..620801d33 100644 --- a/web/core/components/settings/heading.tsx +++ b/web/core/components/settings/heading.tsx @@ -20,14 +20,14 @@ export const SettingsHeading = ({ customButton, showButton = true, }: Props) => ( -
+
{typeof title === "string" ?

{title}

: title} {description &&
{description}
}
{showButton && customButton} {button && showButton && ( - )} diff --git a/web/core/constants/fetch-keys.ts b/web/core/constants/fetch-keys.ts index f0c9551a4..537a7e3c1 100644 --- a/web/core/constants/fetch-keys.ts +++ b/web/core/constants/fetch-keys.ts @@ -114,4 +114,4 @@ export const USER_PROFILE_PROJECT_SEGREGATION = (workspaceSlug: string, userId: `USER_PROFILE_PROJECT_SEGREGATION_${workspaceSlug.toUpperCase()}_${userId.toUpperCase()}`; // api-tokens -export const API_TOKENS_LIST = (workspaceSlug: string) => `API_TOKENS_LIST_${workspaceSlug.toUpperCase()}`; +export const API_TOKENS_LIST = `API_TOKENS_LIST`; diff --git a/web/core/services/api_token.service.ts b/web/core/services/api_token.service.ts deleted file mode 100644 index ba0acfb39..000000000 --- a/web/core/services/api_token.service.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { API_BASE_URL } from "@plane/constants"; -import { IApiToken } from "@plane/types"; -import { APIService } from "./api.service"; - -export class APITokenService extends APIService { - constructor() { - super(API_BASE_URL); - } - - async getApiTokens(workspaceSlug: string): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/api-tokens/`) - .then((response) => response?.data) - .catch((error) => { - throw error?.response?.data; - }); - } - - async retrieveApiToken(workspaceSlug: string, tokenId: string): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/api-tokens/${tokenId}`) - .then((response) => response?.data) - .catch((error) => { - throw error?.response?.data; - }); - } - - async createApiToken(workspaceSlug: string, data: Partial): Promise { - return this.post(`/api/workspaces/${workspaceSlug}/api-tokens/`, data) - .then((response) => response?.data) - .catch((error) => { - throw error?.response?.data; - }); - } - - async deleteApiToken(workspaceSlug: string, tokenId: string): Promise { - return this.delete(`/api/workspaces/${workspaceSlug}/api-tokens/${tokenId}`) - .then((response) => response?.data) - .catch((error) => { - throw error?.response?.data; - }); - } -} diff --git a/web/core/store/workspace/api-token.store.ts b/web/core/store/workspace/api-token.store.ts index c6cf6e82a..2f51e6605 100644 --- a/web/core/store/workspace/api-token.store.ts +++ b/web/core/store/workspace/api-token.store.ts @@ -1,9 +1,9 @@ import { action, observable, makeObservable, runInAction } from "mobx"; import { computedFn } from "mobx-utils"; // types +import { APITokenService } from "@plane/services"; import { IApiToken } from "@plane/types"; // services -import { APITokenService } from "@/services/api_token.service"; // store import { CoreRootStore } from "../root.store"; @@ -13,11 +13,11 @@ export interface IApiTokenStore { // computed actions getApiTokenById: (apiTokenId: string) => IApiToken | null; // fetch actions - fetchApiTokens: (workspaceSlug: string) => Promise; - fetchApiTokenDetails: (workspaceSlug: string, tokenId: string) => Promise; + fetchApiTokens: () => Promise; + fetchApiTokenDetails: (tokenId: string) => Promise; // crud actions - createApiToken: (workspaceSlug: string, data: Partial) => Promise; - deleteApiToken: (workspaceSlug: string, tokenId: string) => Promise; + createApiToken: (data: Partial) => Promise; + deleteApiToken: (tokenId: string) => Promise; } export class ApiTokenStore implements IApiTokenStore { @@ -55,11 +55,10 @@ export class ApiTokenStore implements IApiTokenStore { }); /** - * fetch all the API tokens for a workspace - * @param workspaceSlug + * fetch all the API tokens */ - fetchApiTokens = async (workspaceSlug: string) => - await this.apiTokenService.getApiTokens(workspaceSlug).then((response) => { + fetchApiTokens = async () => + await this.apiTokenService.list().then((response) => { const apiTokensObject: { [apiTokenId: string]: IApiToken } = response.reduce((accumulator, currentWebhook) => { if (currentWebhook && currentWebhook.id) { return { ...accumulator, [currentWebhook.id]: currentWebhook }; @@ -74,11 +73,10 @@ export class ApiTokenStore implements IApiTokenStore { /** * fetch API token details using token id - * @param workspaceSlug * @param tokenId */ - fetchApiTokenDetails = async (workspaceSlug: string, tokenId: string) => - await this.apiTokenService.retrieveApiToken(workspaceSlug, tokenId).then((response) => { + fetchApiTokenDetails = async (tokenId: string) => + await this.apiTokenService.retrieve(tokenId).then((response) => { runInAction(() => { this.apiTokens = { ...this.apiTokens, [response.id]: response }; }); @@ -87,11 +85,10 @@ export class ApiTokenStore implements IApiTokenStore { /** * create API token using data - * @param workspaceSlug * @param data */ - createApiToken = async (workspaceSlug: string, data: Partial) => - await this.apiTokenService.createApiToken(workspaceSlug, data).then((response) => { + createApiToken = async (data: Partial) => + await this.apiTokenService.create(data).then((response) => { runInAction(() => { this.apiTokens = { ...this.apiTokens, [response.id]: response }; }); @@ -100,11 +97,10 @@ export class ApiTokenStore implements IApiTokenStore { /** * delete API token using token id - * @param workspaceSlug * @param tokenId */ - deleteApiToken = async (workspaceSlug: string, tokenId: string) => - await this.apiTokenService.deleteApiToken(workspaceSlug, tokenId).then(() => { + deleteApiToken = async (tokenId: string) => + await this.apiTokenService.destroy(tokenId).then(() => { const updatedApiTokens = { ...this.apiTokens }; delete updatedApiTokens[tokenId]; runInAction(() => { From 171099667ee1cb0cf50cdc63153976fd0467e1b9 Mon Sep 17 00:00:00 2001 From: JayashTripathy <76092296+JayashTripathy@users.noreply.github.com> Date: Thu, 19 Jun 2025 15:57:19 +0530 Subject: [PATCH 174/201] [WEB-4339] fix: projects dropdown shows all projects (#7238) * fix: projects drop only shows joined project * refactor: removed unused things from header --- .../(projects)/analytics/header.tsx | 44 +------------------ .../analytics/analytics-filter-actions.tsx | 4 +- 2 files changed, 3 insertions(+), 45 deletions(-) diff --git a/web/app/(all)/[workspaceSlug]/(projects)/analytics/header.tsx b/web/app/(all)/[workspaceSlug]/(projects)/analytics/header.tsx index b42b9d218..e5ff07c90 100644 --- a/web/app/(all)/[workspaceSlug]/(projects)/analytics/header.tsx +++ b/web/app/(all)/[workspaceSlug]/(projects)/analytics/header.tsx @@ -1,40 +1,15 @@ "use client"; -import { useEffect } from "react"; import { observer } from "mobx-react"; -import { useSearchParams } from "next/navigation"; -import { BarChart2, PanelRight } from "lucide-react"; +import { BarChart2 } from "lucide-react"; // plane imports import { useTranslation } from "@plane/i18n"; import { Breadcrumbs, Header } from "@plane/ui"; -import { cn } from "@plane/utils"; // components import { BreadcrumbLink } from "@/components/common/breadcrumb-link"; -// hooks -import { useAppTheme } from "@/hooks/store"; export const WorkspaceAnalyticsHeader = observer(() => { const { t } = useTranslation(); - const searchParams = useSearchParams(); - const analytics_tab = searchParams.get("analytics_tab"); - // store hooks - const { workspaceAnalyticsSidebarCollapsed, toggleWorkspaceAnalyticsSidebar } = useAppTheme(); - - useEffect(() => { - const handleToggleWorkspaceAnalyticsSidebar = () => { - if (window && window.innerWidth < 768) { - toggleWorkspaceAnalyticsSidebar(true); - } - if (window && workspaceAnalyticsSidebarCollapsed && window.innerWidth >= 768) { - toggleWorkspaceAnalyticsSidebar(false); - } - }; - - window.addEventListener("resize", handleToggleWorkspaceAnalyticsSidebar); - handleToggleWorkspaceAnalyticsSidebar(); - return () => window.removeEventListener("resize", handleToggleWorkspaceAnalyticsSidebar); - }, [toggleWorkspaceAnalyticsSidebar, workspaceAnalyticsSidebarCollapsed]); - return (
@@ -49,23 +24,6 @@ export const WorkspaceAnalyticsHeader = observer(() => { } /> - {analytics_tab === "custom" ? ( - - ) : ( - <> - )}
); diff --git a/web/core/components/analytics/analytics-filter-actions.tsx b/web/core/components/analytics/analytics-filter-actions.tsx index adfe7822b..13019063b 100644 --- a/web/core/components/analytics/analytics-filter-actions.tsx +++ b/web/core/components/analytics/analytics-filter-actions.tsx @@ -9,7 +9,7 @@ import { ProjectSelect } from "./select/project"; const AnalyticsFilterActions = observer(() => { const { selectedProjects, selectedDuration, updateSelectedProjects, updateSelectedDuration } = useAnalytics(); - const { workspaceProjectIds } = useProject(); + const { joinedProjectIds } = useProject(); return (
{ onChange={(val) => { updateSelectedProjects(val ?? []); }} - projectIds={workspaceProjectIds} + projectIds={joinedProjectIds} /> {/* Date: Thu, 19 Jun 2025 15:59:38 +0530 Subject: [PATCH 175/201] [WIKI-384] chore: editor core refactor (#7235) * fix: extra actions * chore: page flags --- .../(detail)/[projectId]/pages/(detail)/header.tsx | 2 +- web/ce/components/pages/extra-actions.tsx | 2 ++ web/ce/components/pages/header/share-control.tsx | 12 ++++++++++++ web/ce/hooks/use-page-flag.ts | 2 ++ web/core/components/pages/header/actions.tsx | 2 ++ 5 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 web/ce/components/pages/header/share-control.tsx diff --git a/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/header.tsx b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/header.tsx index da7b90291..34e2a2bee 100644 --- a/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/header.tsx +++ b/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/header.tsx @@ -110,7 +110,7 @@ export const PageDetailsHeader = observer(() => {
- + diff --git a/web/ce/components/pages/extra-actions.tsx b/web/ce/components/pages/extra-actions.tsx index dd15dab7e..acfb9dfc7 100644 --- a/web/ce/components/pages/extra-actions.tsx +++ b/web/ce/components/pages/extra-actions.tsx @@ -1,8 +1,10 @@ // store +import { EPageStoreType } from "@/plane-web/hooks/store"; import { TPageInstance } from "@/store/pages/base-page"; export type TPageHeaderExtraActionsProps = { page: TPageInstance; + storeType: EPageStoreType; }; export const PageDetailsHeaderExtraActions: React.FC = () => null; diff --git a/web/ce/components/pages/header/share-control.tsx b/web/ce/components/pages/header/share-control.tsx new file mode 100644 index 000000000..bedd0322c --- /dev/null +++ b/web/ce/components/pages/header/share-control.tsx @@ -0,0 +1,12 @@ +"use client"; + +import { type EPageStoreType } from "@/plane-web/hooks/store"; +// store +import { TPageInstance } from "@/store/pages/base-page"; + +export type TPageShareControlProps = { + page: TPageInstance; + storeType: EPageStoreType; +}; + +export const PageShareControl = ({}: TPageShareControlProps) => null; diff --git a/web/ce/hooks/use-page-flag.ts b/web/ce/hooks/use-page-flag.ts index 84dc31c0d..94d72065a 100644 --- a/web/ce/hooks/use-page-flag.ts +++ b/web/ce/hooks/use-page-flag.ts @@ -4,11 +4,13 @@ export type TPageFlagHookArgs = { export type TPageFlagHookReturnType = { isMovePageEnabled: boolean; + isPageSharingEnabled: boolean; }; export const usePageFlag = (args: TPageFlagHookArgs): TPageFlagHookReturnType => { const {} = args; return { isMovePageEnabled: false, + isPageSharingEnabled: false, }; }; diff --git a/web/core/components/pages/header/actions.tsx b/web/core/components/pages/header/actions.tsx index 3c73d42ef..6c6cb2f6c 100644 --- a/web/core/components/pages/header/actions.tsx +++ b/web/core/components/pages/header/actions.tsx @@ -6,6 +6,7 @@ import { PageInfoPopover, PageOptionsDropdown } from "@/components/pages"; // plane web components import { PageLockControl } from "@/plane-web/components/pages/header/lock-control"; import { PageMoveControl } from "@/plane-web/components/pages/header/move-control"; +import { PageShareControl } from "@/plane-web/components/pages/header/share-control"; // plane web hooks import { EPageStoreType } from "@/plane-web/hooks/store"; // store @@ -33,6 +34,7 @@ export const PageHeaderActions: React.FC = observer((props) => { +
); From eb5ffebcc6cd85bf08b3b19664f082af194503b1 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Thu, 19 Jun 2025 16:00:18 +0530 Subject: [PATCH 176/201] [WIKI-458] refactor: base page instance for additional properties (#7228) * refactor: create a super class for base page * fix: path --------- Co-authored-by: Palanikannan M --- packages/types/src/index.d.ts | 2 +- .../types/src/{pages.d.ts => page/core.d.ts} | 9 ++++----- packages/types/src/page/extended.d.ts | 1 + packages/types/src/page/index.d.ts | 2 ++ packages/types/src/search.d.ts | 2 +- web/ce/store/pages/extended-base-page.ts | 16 ++++++++++++++++ web/core/store/pages/base-page.ts | 13 +++++-------- 7 files changed, 30 insertions(+), 15 deletions(-) rename packages/types/src/{pages.d.ts => page/core.d.ts} (91%) create mode 100644 packages/types/src/page/extended.d.ts create mode 100644 packages/types/src/page/index.d.ts create mode 100644 web/ce/store/pages/extended-base-page.ts diff --git a/packages/types/src/index.d.ts b/packages/types/src/index.d.ts index d8b95fca4..ba70ec5c7 100644 --- a/packages/types/src/index.d.ts +++ b/packages/types/src/index.d.ts @@ -10,7 +10,7 @@ export * from "./issues"; export * from "./module"; export * from "./views"; export * from "./integration"; -export * from "./pages"; +export * from "./page"; export * from "./ai"; export * from "./estimate"; export * from "./importer"; diff --git a/packages/types/src/pages.d.ts b/packages/types/src/page/core.d.ts similarity index 91% rename from packages/types/src/pages.d.ts rename to packages/types/src/page/core.d.ts index 183d015bf..5dcc44149 100644 --- a/packages/types/src/pages.d.ts +++ b/packages/types/src/page/core.d.ts @@ -1,9 +1,9 @@ -import { TLogoProps } from "./common"; -import { EPageAccess } from "./enums"; +import { TLogoProps } from "../common"; +import { EPageAccess } from "../enums"; +import { TPageExtended } from "./extended"; -export type TPage = { +export type TPage = TPageExtended & { access: EPageAccess | undefined; - anchor?: string | null | undefined; archived_at: string | null | undefined; color: string | undefined; created_at: Date | undefined; @@ -16,7 +16,6 @@ export type TPage = { name: string | undefined; owned_by: string | undefined; project_ids?: string[] | undefined; - team: string | null | undefined; updated_at: Date | undefined; updated_by: string | undefined; workspace: string | undefined; diff --git a/packages/types/src/page/extended.d.ts b/packages/types/src/page/extended.d.ts new file mode 100644 index 000000000..7edfae828 --- /dev/null +++ b/packages/types/src/page/extended.d.ts @@ -0,0 +1 @@ +export type TPageExtended = {}; diff --git a/packages/types/src/page/index.d.ts b/packages/types/src/page/index.d.ts new file mode 100644 index 000000000..c6c1c2a06 --- /dev/null +++ b/packages/types/src/page/index.d.ts @@ -0,0 +1,2 @@ +export * from "./core"; +export * from "./extended"; diff --git a/packages/types/src/search.d.ts b/packages/types/src/search.d.ts index 41138a46e..413ca6bc0 100644 --- a/packages/types/src/search.d.ts +++ b/packages/types/src/search.d.ts @@ -1,7 +1,7 @@ import { ICycle } from "./cycle"; import { TIssue } from "./issues/issue"; import { IModule } from "./module"; -import { TPage } from "./pages"; +import { TPage } from "./page"; import { IProject } from "./project"; import { IUser } from "./users"; import { IWorkspace } from "./workspace"; diff --git a/web/ce/store/pages/extended-base-page.ts b/web/ce/store/pages/extended-base-page.ts new file mode 100644 index 000000000..a80e5e4e3 --- /dev/null +++ b/web/ce/store/pages/extended-base-page.ts @@ -0,0 +1,16 @@ +import { TPage, TPageExtended } from "@plane/types"; +import { RootStore } from "@/plane-web/store/root.store"; +import { TBasePageServices } from "@/store/pages/base-page"; + +export type TExtendedPageInstance = TPageExtended & { + asJSONExtended: TPageExtended; +}; + +export class ExtendedBasePage implements TExtendedPageInstance { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + constructor(store: RootStore, page: TPage, services: TBasePageServices) {} + + get asJSONExtended(): TExtendedPageInstance["asJSONExtended"] { + return {}; + } +} diff --git a/web/core/store/pages/base-page.ts b/web/core/store/pages/base-page.ts index 6639e8e84..294c370e7 100644 --- a/web/core/store/pages/base-page.ts +++ b/web/core/store/pages/base-page.ts @@ -7,6 +7,7 @@ import { TDocumentPayload, TLogoProps, TNameDescriptionLoader, TPage } from "@pl import { TChangeHandlerProps } from "@plane/ui"; import { convertHexEmojiToDecimal } from "@plane/utils"; // plane web store +import { ExtendedBasePage } from "@/plane-web/store/pages/extended-base-page"; import { RootStore } from "@/plane-web/store/root.store"; export type TBasePage = TPage & { @@ -69,7 +70,7 @@ export type TPageInstance = TBasePage & getRedirectionLink: () => string; }; -export class BasePage implements TBasePage { +export class BasePage extends ExtendedBasePage implements TBasePage { // loaders isSubmitting: TNameDescriptionLoader = "saved"; editorRef: EditorRefApi | null = null; @@ -82,13 +83,11 @@ export class BasePage implements TBasePage { label_ids: string[] | undefined; owned_by: string | undefined; access: EPageAccess | undefined; - anchor?: string | null | undefined; is_favorite: boolean; is_locked: boolean; archived_at: string | null | undefined; workspace: string | undefined; project_ids?: string[] | undefined; - team: string | null | undefined; created_by: string | undefined; updated_by: string | undefined; created_at: Date | undefined; @@ -106,6 +105,8 @@ export class BasePage implements TBasePage { page: TPage, services: TBasePageServices ) { + super(store, page, services); + this.id = page?.id || undefined; this.name = page?.name; this.logo_props = page?.logo_props || undefined; @@ -114,13 +115,11 @@ export class BasePage implements TBasePage { this.label_ids = page?.label_ids || undefined; this.owned_by = page?.owned_by || undefined; this.access = page?.access || EPageAccess.PUBLIC; - this.anchor = page?.anchor || undefined; this.is_favorite = page?.is_favorite || false; this.is_locked = page?.is_locked || false; this.archived_at = page?.archived_at || undefined; this.workspace = page?.workspace || undefined; this.project_ids = page?.project_ids || undefined; - this.team = page?.team || undefined; this.created_by = page?.created_by || undefined; this.updated_by = page?.updated_by || undefined; this.created_at = page?.created_at || undefined; @@ -140,7 +139,6 @@ export class BasePage implements TBasePage { label_ids: observable, owned_by: observable.ref, access: observable.ref, - anchor: observable.ref, is_favorite: observable.ref, is_locked: observable.ref, archived_at: observable.ref, @@ -212,18 +210,17 @@ export class BasePage implements TBasePage { label_ids: this.label_ids, owned_by: this.owned_by, access: this.access, - anchor: this.anchor, logo_props: this.logo_props, is_favorite: this.is_favorite, is_locked: this.is_locked, archived_at: this.archived_at, workspace: this.workspace, project_ids: this.project_ids, - team: this.team, created_by: this.created_by, updated_by: this.updated_by, created_at: this.created_at, updated_at: this.updated_at, + ...this.asJSONExtended, }; } From 8988cf9a85d9f263901401ccacb876bd63cc8f32 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Thu, 19 Jun 2025 16:25:52 +0530 Subject: [PATCH 177/201] [WEB-462] refactor: editor props structure (#7233) * refactor: editor props structure * chore: add missing prop * fix: space app build * chore: export ce types --- .../src/ce/extensions/core/extensions.ts | 14 +-- .../extensions/core/read-only-extensions.ts | 15 ++-- .../src/ce/extensions/document-extensions.tsx | 28 +++--- .../ce/extensions/rich-text/extensions.tsx | 19 ++-- .../rich-text/read-only-extensions.tsx | 10 +-- .../src/ce/extensions/slash-commands.tsx | 8 +- .../editors/document/collaborative-editor.tsx | 10 ++- .../editors/document/page-renderer.tsx | 4 +- .../editors/document/read-only-editor.tsx | 31 ++----- .../components/editors/editor-wrapper.tsx | 2 + .../components/editors/lite-text/editor.tsx | 22 ++--- .../editors/lite-text/read-only-editor.tsx | 4 +- .../editors/read-only-editor-wrapper.tsx | 2 + .../components/editors/rich-text/editor.tsx | 14 +-- .../editors/rich-text/read-only-editor.tsx | 19 ++-- .../editor/src/core/extensions/extensions.ts | 24 +++-- .../core/extensions/read-only-extensions.ts | 11 +-- .../slash-commands/command-items-list.tsx | 3 +- .../core/extensions/slash-commands/root.tsx | 5 +- .../editor/src/core/extensions/utility.ts | 5 +- .../core/hooks/use-collaborative-editor.ts | 13 +-- packages/editor/src/core/hooks/use-editor.ts | 51 +++-------- .../src/core/hooks/use-read-only-editor.ts | 38 +++----- .../editor/src/core/types/collaboration.ts | 46 ---------- packages/editor/src/core/types/config.ts | 15 ++++ packages/editor/src/core/types/editor.ts | 88 +++++++------------ packages/editor/src/core/types/hook.ts | 50 +++++++++++ packages/editor/src/core/types/index.ts | 1 + packages/editor/src/index.ts | 1 - .../components/editor/lite-text-editor.tsx | 9 +- .../editor/lite-text-read-only-editor.tsx | 9 +- .../components/editor/rich-text-editor.tsx | 10 ++- .../editor/rich-text-read-only-editor.tsx | 9 +- web/ce/hooks/use-editor-flagging.ts | 38 +++++--- .../lite-text-editor/lite-text-editor.tsx | 14 +-- .../lite-text-read-only-editor.tsx | 11 +-- .../rich-text-editor/rich-text-editor.tsx | 12 ++- .../rich-text-read-only-editor.tsx | 11 +-- .../editor/sticky-editor/editor.tsx | 9 +- .../components/pages/editor/editor-body.tsx | 5 +- web/core/components/pages/version/editor.tsx | 5 +- 41 files changed, 348 insertions(+), 347 deletions(-) create mode 100644 packages/editor/src/core/types/hook.ts diff --git a/packages/editor/src/ce/extensions/core/extensions.ts b/packages/editor/src/ce/extensions/core/extensions.ts index cecfb38b4..a72bcc215 100644 --- a/packages/editor/src/ce/extensions/core/extensions.ts +++ b/packages/editor/src/ce/extensions/core/extensions.ts @@ -1,13 +1,13 @@ -import { Extensions } from "@tiptap/core"; +import type { Extensions } from "@tiptap/core"; // types -import { TExtensions, TFileHandler } from "@/types"; +import type { IEditorProps } from "@/types"; -type Props = { - disabledExtensions: TExtensions[]; - fileHandler: TFileHandler; -}; +export type TCoreAdditionalExtensionsProps = Pick< + IEditorProps, + "disabledExtensions" | "flaggedExtensions" | "fileHandler" +>; -export const CoreEditorAdditionalExtensions = (props: Props): Extensions => { +export const CoreEditorAdditionalExtensions = (props: TCoreAdditionalExtensionsProps): Extensions => { const {} = props; return []; }; diff --git a/packages/editor/src/ce/extensions/core/read-only-extensions.ts b/packages/editor/src/ce/extensions/core/read-only-extensions.ts index 398848e31..4f9306da3 100644 --- a/packages/editor/src/ce/extensions/core/read-only-extensions.ts +++ b/packages/editor/src/ce/extensions/core/read-only-extensions.ts @@ -1,12 +1,15 @@ -import { Extensions } from "@tiptap/core"; +import type { Extensions } from "@tiptap/core"; // types -import { TExtensions } from "@/types"; +import type { IReadOnlyEditorProps } from "@/types"; -type Props = { - disabledExtensions: TExtensions[]; -}; +export type TCoreReadOnlyEditorAdditionalExtensionsProps = Pick< + IReadOnlyEditorProps, + "disabledExtensions" | "flaggedExtensions" +>; -export const CoreReadOnlyEditorAdditionalExtensions = (props: Props): Extensions => { +export const CoreReadOnlyEditorAdditionalExtensions = ( + props: TCoreReadOnlyEditorAdditionalExtensionsProps +): Extensions => { const {} = props; return []; }; diff --git a/packages/editor/src/ce/extensions/document-extensions.tsx b/packages/editor/src/ce/extensions/document-extensions.tsx index da2790b2a..8815e2d26 100644 --- a/packages/editor/src/ce/extensions/document-extensions.tsx +++ b/packages/editor/src/ce/extensions/document-extensions.tsx @@ -1,37 +1,39 @@ -import { HocuspocusProvider } from "@hocuspocus/provider"; -import { AnyExtension } from "@tiptap/core"; +import type { HocuspocusProvider } from "@hocuspocus/provider"; +import type { AnyExtension } from "@tiptap/core"; import { SlashCommands } from "@/extensions"; // plane editor types -import { TEmbedConfig } from "@/plane-editor/types"; +import type { TEmbedConfig } from "@/plane-editor/types"; // types -import { TExtensions, TFileHandler, TUserDetails } from "@/types"; +import type { IEditorProps, TExtensions, TUserDetails } from "@/types"; -export type TDocumentEditorAdditionalExtensionsProps = { - disabledExtensions: TExtensions[]; +export type TDocumentEditorAdditionalExtensionsProps = Pick< + IEditorProps, + "disabledExtensions" | "flaggedExtensions" | "fileHandler" +> & { embedConfig: TEmbedConfig | undefined; - fileHandler: TFileHandler; provider?: HocuspocusProvider; userDetails: TUserDetails; }; export type TDocumentEditorAdditionalExtensionsRegistry = { - isEnabled: (disabledExtensions: TExtensions[]) => boolean; + isEnabled: (disabledExtensions: TExtensions[], flaggedExtensions: TExtensions[]) => boolean; getExtension: (props: TDocumentEditorAdditionalExtensionsProps) => AnyExtension; }; const extensionRegistry: TDocumentEditorAdditionalExtensionsRegistry[] = [ { isEnabled: (disabledExtensions) => !disabledExtensions.includes("slash-commands"), - getExtension: ({ disabledExtensions }) => SlashCommands({ disabledExtensions }), + getExtension: ({ disabledExtensions, flaggedExtensions }) => + SlashCommands({ disabledExtensions, flaggedExtensions }), }, ]; -export const DocumentEditorAdditionalExtensions = (_props: TDocumentEditorAdditionalExtensionsProps) => { - const { disabledExtensions = [] } = _props; +export const DocumentEditorAdditionalExtensions = (props: TDocumentEditorAdditionalExtensionsProps) => { + const { disabledExtensions, flaggedExtensions } = props; const documentExtensions = extensionRegistry - .filter((config) => config.isEnabled(disabledExtensions)) - .map((config) => config.getExtension(_props)); + .filter((config) => config.isEnabled(disabledExtensions, flaggedExtensions)) + .map((config) => config.getExtension(props)); return documentExtensions; }; diff --git a/packages/editor/src/ce/extensions/rich-text/extensions.tsx b/packages/editor/src/ce/extensions/rich-text/extensions.tsx index 0eedd1e87..520dfa10e 100644 --- a/packages/editor/src/ce/extensions/rich-text/extensions.tsx +++ b/packages/editor/src/ce/extensions/rich-text/extensions.tsx @@ -2,19 +2,19 @@ import { AnyExtension, Extensions } from "@tiptap/core"; // extensions import { SlashCommands } from "@/extensions/slash-commands/root"; // types -import { TExtensions, TFileHandler } from "@/types"; +import { IEditorProps, TExtensions } from "@/types"; -export type TRichTextEditorAdditionalExtensionsProps = { - disabledExtensions: TExtensions[]; - fileHandler: TFileHandler; -}; +export type TRichTextEditorAdditionalExtensionsProps = Pick< + IEditorProps, + "disabledExtensions" | "flaggedExtensions" | "fileHandler" +>; /** * Registry entry configuration for extensions */ export type TRichTextEditorAdditionalExtensionsRegistry = { /** Determines if the extension should be enabled based on disabled extensions */ - isEnabled: (disabledExtensions: TExtensions[]) => boolean; + isEnabled: (disabledExtensions: TExtensions[], flaggedExtensions: TExtensions[]) => boolean; /** Returns the extension instance(s) when enabled */ getExtension: (props: TRichTextEditorAdditionalExtensionsProps) => AnyExtension | undefined; }; @@ -22,18 +22,19 @@ export type TRichTextEditorAdditionalExtensionsRegistry = { const extensionRegistry: TRichTextEditorAdditionalExtensionsRegistry[] = [ { isEnabled: (disabledExtensions) => !disabledExtensions.includes("slash-commands"), - getExtension: ({ disabledExtensions }) => + getExtension: ({ disabledExtensions, flaggedExtensions }) => SlashCommands({ disabledExtensions, + flaggedExtensions, }), }, ]; export const RichTextEditorAdditionalExtensions = (props: TRichTextEditorAdditionalExtensionsProps) => { - const { disabledExtensions } = props; + const { disabledExtensions, flaggedExtensions } = props; const extensions: Extensions = extensionRegistry - .filter((config) => config.isEnabled(disabledExtensions)) + .filter((config) => config.isEnabled(disabledExtensions, flaggedExtensions)) .map((config) => config.getExtension(props)) .filter((extension): extension is AnyExtension => extension !== undefined); diff --git a/packages/editor/src/ce/extensions/rich-text/read-only-extensions.tsx b/packages/editor/src/ce/extensions/rich-text/read-only-extensions.tsx index 015630117..0b7cbc730 100644 --- a/packages/editor/src/ce/extensions/rich-text/read-only-extensions.tsx +++ b/packages/editor/src/ce/extensions/rich-text/read-only-extensions.tsx @@ -1,11 +1,11 @@ import { AnyExtension, Extensions } from "@tiptap/core"; // types -import { TExtensions, TReadOnlyFileHandler } from "@/types"; +import { IReadOnlyEditorProps, TExtensions } from "@/types"; -export type TRichTextReadOnlyEditorAdditionalExtensionsProps = { - disabledExtensions: TExtensions[]; - fileHandler: TReadOnlyFileHandler; -}; +export type TRichTextReadOnlyEditorAdditionalExtensionsProps = Pick< + IReadOnlyEditorProps, + "disabledExtensions" | "flaggedExtensions" | "fileHandler" +>; /** * Registry entry configuration for extensions diff --git a/packages/editor/src/ce/extensions/slash-commands.tsx b/packages/editor/src/ce/extensions/slash-commands.tsx index faefa7452..d61d056c8 100644 --- a/packages/editor/src/ce/extensions/slash-commands.tsx +++ b/packages/editor/src/ce/extensions/slash-commands.tsx @@ -1,11 +1,9 @@ // extensions -import { TSlashCommandAdditionalOption } from "@/extensions"; +import type { TSlashCommandAdditionalOption } from "@/extensions"; // types -import { TExtensions } from "@/types"; +import type { IEditorProps } from "@/types"; -type Props = { - disabledExtensions?: TExtensions[]; -}; +type Props = Pick; export const coreEditorAdditionalSlashCommandOptions = (props: Props): TSlashCommandAdditionalOption[] => { const {} = props; diff --git a/packages/editor/src/core/components/editors/document/collaborative-editor.tsx b/packages/editor/src/core/components/editors/document/collaborative-editor.tsx index d1398ff5a..8bbf2e7ce 100644 --- a/packages/editor/src/core/components/editors/document/collaborative-editor.tsx +++ b/packages/editor/src/core/components/editors/document/collaborative-editor.tsx @@ -13,10 +13,11 @@ import { getEditorClassNames } from "@/helpers/common"; // hooks import { useCollaborativeEditor } from "@/hooks/use-collaborative-editor"; // types -import { EditorRefApi, ICollaborativeDocumentEditor } from "@/types"; +import { EditorRefApi, ICollaborativeDocumentEditorProps } from "@/types"; -const CollaborativeDocumentEditor = (props: ICollaborativeDocumentEditor) => { +const CollaborativeDocumentEditor: React.FC = (props) => { const { + onChange, onTransaction, aiHandler, bubbleMenuEnabled = true, @@ -27,6 +28,7 @@ const CollaborativeDocumentEditor = (props: ICollaborativeDocumentEditor) => { editorClassName = "", embedHandler, fileHandler, + flaggedExtensions, forwardedRef, handleEditorReady, id, @@ -56,10 +58,12 @@ const CollaborativeDocumentEditor = (props: ICollaborativeDocumentEditor) => { embedHandler, extensions, fileHandler, + flaggedExtensions, forwardedRef, handleEditorReady, id, mentionHandler, + onChange, onTransaction, placeholder, realtimeConfig, @@ -95,7 +99,7 @@ const CollaborativeDocumentEditor = (props: ICollaborativeDocumentEditor) => { ); }; -const CollaborativeDocumentEditorWithRef = React.forwardRef( +const CollaborativeDocumentEditorWithRef = React.forwardRef( (props, ref) => ( } /> ) diff --git a/packages/editor/src/core/components/editors/document/page-renderer.tsx b/packages/editor/src/core/components/editors/document/page-renderer.tsx index 0be3c17c1..62613b0a1 100644 --- a/packages/editor/src/core/components/editors/document/page-renderer.tsx +++ b/packages/editor/src/core/components/editors/document/page-renderer.tsx @@ -5,7 +5,7 @@ import { AIFeaturesMenu, BlockMenu, EditorBubbleMenu } from "@/components/menus" // types import { TAIHandler, TDisplayConfig } from "@/types"; -type IPageRenderer = { +type Props = { aiHandler?: TAIHandler; bubbleMenuEnabled: boolean; displayConfig: TDisplayConfig; @@ -15,7 +15,7 @@ type IPageRenderer = { tabIndex?: number; }; -export const PageRenderer = (props: IPageRenderer) => { +export const PageRenderer = (props: Props) => { const { aiHandler, bubbleMenuEnabled, displayConfig, editor, editorContainerClassName, id, tabIndex } = props; return ( diff --git a/packages/editor/src/core/components/editors/document/read-only-editor.tsx b/packages/editor/src/core/components/editors/document/read-only-editor.tsx index 2d2e30830..8f0d67ddc 100644 --- a/packages/editor/src/core/components/editors/document/read-only-editor.tsx +++ b/packages/editor/src/core/components/editors/document/read-only-editor.tsx @@ -1,5 +1,5 @@ import { Extensions } from "@tiptap/core"; -import { forwardRef, MutableRefObject } from "react"; +import React, { forwardRef, MutableRefObject } from "react"; // plane imports import { cn } from "@plane/utils"; // components @@ -13,30 +13,9 @@ import { getEditorClassNames } from "@/helpers/common"; // hooks import { useReadOnlyEditor } from "@/hooks/use-read-only-editor"; // types -import { - EditorReadOnlyRefApi, - TDisplayConfig, - TExtensions, - TReadOnlyFileHandler, - TReadOnlyMentionHandler, -} from "@/types"; +import { EditorReadOnlyRefApi, IDocumentReadOnlyEditorProps } from "@/types"; -interface IDocumentReadOnlyEditor { - disabledExtensions: TExtensions[]; - id: string; - initialValue: string; - containerClassName: string; - displayConfig?: TDisplayConfig; - editorClassName?: string; - embedHandler: any; - fileHandler: TReadOnlyFileHandler; - tabIndex?: number; - handleEditorReady?: (value: boolean) => void; - mentionHandler: TReadOnlyMentionHandler; - forwardedRef?: React.MutableRefObject; -} - -const DocumentReadOnlyEditor = (props: IDocumentReadOnlyEditor) => { +const DocumentReadOnlyEditor: React.FC = (props) => { const { containerClassName, disabledExtensions, @@ -44,6 +23,7 @@ const DocumentReadOnlyEditor = (props: IDocumentReadOnlyEditor) => { editorClassName = "", embedHandler, fileHandler, + flaggedExtensions, id, forwardedRef, handleEditorReady, @@ -64,6 +44,7 @@ const DocumentReadOnlyEditor = (props: IDocumentReadOnlyEditor) => { editorClassName, extensions, fileHandler, + flaggedExtensions, forwardedRef, handleEditorReady, initialValue, @@ -87,7 +68,7 @@ const DocumentReadOnlyEditor = (props: IDocumentReadOnlyEditor) => { ); }; -const DocumentReadOnlyEditorWithRef = forwardRef((props, ref) => ( +const DocumentReadOnlyEditorWithRef = forwardRef((props, ref) => ( } /> )); diff --git a/packages/editor/src/core/components/editors/editor-wrapper.tsx b/packages/editor/src/core/components/editors/editor-wrapper.tsx index 9d1297e23..2c1ef52b7 100644 --- a/packages/editor/src/core/components/editors/editor-wrapper.tsx +++ b/packages/editor/src/core/components/editors/editor-wrapper.tsx @@ -26,6 +26,7 @@ export const EditorWrapper: React.FC = (props) => { id, initialValue, fileHandler, + flaggedExtensions, forwardedRef, mentionHandler, onChange, @@ -44,6 +45,7 @@ export const EditorWrapper: React.FC = (props) => { enableHistory: true, extensions, fileHandler, + flaggedExtensions, forwardedRef, id, initialValue, diff --git a/packages/editor/src/core/components/editors/lite-text/editor.tsx b/packages/editor/src/core/components/editors/lite-text/editor.tsx index 849a3c3e2..df89521ae 100644 --- a/packages/editor/src/core/components/editors/lite-text/editor.tsx +++ b/packages/editor/src/core/components/editors/lite-text/editor.tsx @@ -4,23 +4,25 @@ import { EditorWrapper } from "@/components/editors/editor-wrapper"; // extensions import { EnterKeyExtension } from "@/extensions"; // types -import { EditorRefApi, ILiteTextEditor } from "@/types"; +import { EditorRefApi, ILiteTextEditorProps } from "@/types"; -const LiteTextEditor = (props: ILiteTextEditor) => { +const LiteTextEditor: React.FC = (props) => { const { onEnterKeyPress, disabledExtensions, extensions: externalExtensions = [] } = props; - const extensions = useMemo( - () => [ - ...externalExtensions, - ...(disabledExtensions?.includes("enter-key") ? [] : [EnterKeyExtension(onEnterKeyPress)]), - ], - [externalExtensions, disabledExtensions, onEnterKeyPress] - ); + const extensions = useMemo(() => { + const resolvedExtensions = [...externalExtensions]; + + if (!disabledExtensions?.includes("enter-key")) { + resolvedExtensions.push(EnterKeyExtension(onEnterKeyPress)); + } + + return resolvedExtensions; + }, [externalExtensions, disabledExtensions, onEnterKeyPress]); return ; }; -const LiteTextEditorWithRef = forwardRef((props, ref) => ( +const LiteTextEditorWithRef = forwardRef((props, ref) => ( } /> )); diff --git a/packages/editor/src/core/components/editors/lite-text/read-only-editor.tsx b/packages/editor/src/core/components/editors/lite-text/read-only-editor.tsx index b721c84c5..75e02791d 100644 --- a/packages/editor/src/core/components/editors/lite-text/read-only-editor.tsx +++ b/packages/editor/src/core/components/editors/lite-text/read-only-editor.tsx @@ -2,9 +2,9 @@ import { forwardRef } from "react"; // components import { ReadOnlyEditorWrapper } from "@/components/editors"; // types -import { EditorReadOnlyRefApi, ILiteTextReadOnlyEditor } from "@/types"; +import { EditorReadOnlyRefApi, ILiteTextReadOnlyEditorProps } from "@/types"; -const LiteTextReadOnlyEditorWithRef = forwardRef((props, ref) => ( +const LiteTextReadOnlyEditorWithRef = forwardRef((props, ref) => ( } /> )); diff --git a/packages/editor/src/core/components/editors/read-only-editor-wrapper.tsx b/packages/editor/src/core/components/editors/read-only-editor-wrapper.tsx index ffed8ba8b..b6abd1a6a 100644 --- a/packages/editor/src/core/components/editors/read-only-editor-wrapper.tsx +++ b/packages/editor/src/core/components/editors/read-only-editor-wrapper.tsx @@ -17,6 +17,7 @@ export const ReadOnlyEditorWrapper = (props: IReadOnlyEditorProps) => { editorClassName = "", extensions, fileHandler, + flaggedExtensions, forwardedRef, id, initialValue, @@ -28,6 +29,7 @@ export const ReadOnlyEditorWrapper = (props: IReadOnlyEditorProps) => { editorClassName, extensions, fileHandler, + flaggedExtensions, forwardedRef, initialValue, mentionHandler, diff --git a/packages/editor/src/core/components/editors/rich-text/editor.tsx b/packages/editor/src/core/components/editors/rich-text/editor.tsx index 2334d9bab..8544dcc83 100644 --- a/packages/editor/src/core/components/editors/rich-text/editor.tsx +++ b/packages/editor/src/core/components/editors/rich-text/editor.tsx @@ -7,15 +7,16 @@ import { SideMenuExtension } from "@/extensions"; // plane editor imports import { RichTextEditorAdditionalExtensions } from "@/plane-editor/extensions/rich-text/extensions"; // types -import { EditorRefApi, IRichTextEditor } from "@/types"; +import { EditorRefApi, IRichTextEditorProps } from "@/types"; -const RichTextEditor = (props: IRichTextEditor) => { +const RichTextEditor: React.FC = (props) => { const { + bubbleMenuEnabled = true, disabledExtensions, dragDropEnabled, - fileHandler, - bubbleMenuEnabled = true, extensions: externalExtensions = [], + fileHandler, + flaggedExtensions, } = props; const getExtensions = useCallback(() => { @@ -28,11 +29,12 @@ const RichTextEditor = (props: IRichTextEditor) => { ...RichTextEditorAdditionalExtensions({ disabledExtensions, fileHandler, + flaggedExtensions, }), ]; return extensions; - }, [dragDropEnabled, disabledExtensions, externalExtensions, fileHandler]); + }, [dragDropEnabled, disabledExtensions, externalExtensions, fileHandler, flaggedExtensions]); return ( @@ -41,7 +43,7 @@ const RichTextEditor = (props: IRichTextEditor) => { ); }; -const RichTextEditorWithRef = forwardRef((props, ref) => ( +const RichTextEditorWithRef = forwardRef((props, ref) => ( } /> )); diff --git a/packages/editor/src/core/components/editors/rich-text/read-only-editor.tsx b/packages/editor/src/core/components/editors/rich-text/read-only-editor.tsx index 18d960ca5..efad3d6ac 100644 --- a/packages/editor/src/core/components/editors/rich-text/read-only-editor.tsx +++ b/packages/editor/src/core/components/editors/rich-text/read-only-editor.tsx @@ -2,23 +2,22 @@ import { forwardRef, useCallback } from "react"; // plane editor extensions import { RichTextReadOnlyEditorAdditionalExtensions } from "@/plane-editor/extensions/rich-text/read-only-extensions"; // types -import { EditorReadOnlyRefApi, IRichTextReadOnlyEditor } from "@/types"; +import { EditorReadOnlyRefApi, IRichTextReadOnlyEditorProps } from "@/types"; // local imports import { ReadOnlyEditorWrapper } from "../read-only-editor-wrapper"; -const RichTextReadOnlyEditorWithRef = forwardRef((props, ref) => { - const { disabledExtensions, fileHandler } = props; +const RichTextReadOnlyEditorWithRef = forwardRef((props, ref) => { + const { disabledExtensions, fileHandler, flaggedExtensions } = props; const getExtensions = useCallback(() => { - const extensions = [ - ...RichTextReadOnlyEditorAdditionalExtensions({ - disabledExtensions, - fileHandler, - }), - ]; + const extensions = RichTextReadOnlyEditorAdditionalExtensions({ + disabledExtensions, + fileHandler, + flaggedExtensions, + }); return extensions; - }, [disabledExtensions, fileHandler]); + }, [disabledExtensions, fileHandler, flaggedExtensions]); return ( & { enableHistory: boolean; - fileHandler: TFileHandler; - mentionHandler: TMentionHandler; - placeholder?: string | ((isFocused: boolean, value: string) => string); - tabIndex?: number; editable: boolean; }; export const CoreEditorExtensions = (args: TArguments): Extensions => { - const { disabledExtensions, enableHistory, fileHandler, mentionHandler, placeholder, tabIndex, editable } = args; + const { + disabledExtensions, + enableHistory, + fileHandler, + flaggedExtensions, + mentionHandler, + placeholder, + tabIndex, + editable, + } = args; const extensions = [ StarterKit.configure({ @@ -177,6 +184,7 @@ export const CoreEditorExtensions = (args: TArguments): Extensions => { CustomColorExtension, ...CoreEditorAdditionalExtensions({ disabledExtensions, + flaggedExtensions, fileHandler, }), ]; diff --git a/packages/editor/src/core/extensions/read-only-extensions.ts b/packages/editor/src/core/extensions/read-only-extensions.ts index b28e8a67a..0f422c620 100644 --- a/packages/editor/src/core/extensions/read-only-extensions.ts +++ b/packages/editor/src/core/extensions/read-only-extensions.ts @@ -31,16 +31,12 @@ import { isValidHttpUrl } from "@/helpers/common"; // plane editor extensions import { CoreReadOnlyEditorAdditionalExtensions } from "@/plane-editor/extensions"; // types -import { TExtensions, TReadOnlyFileHandler, TReadOnlyMentionHandler } from "@/types"; +import type { IReadOnlyEditorProps } from "@/types"; -type Props = { - disabledExtensions: TExtensions[]; - fileHandler: TReadOnlyFileHandler; - mentionHandler: TReadOnlyMentionHandler; -}; +type Props = Pick; export const CoreReadOnlyEditorExtensions = (props: Props): Extensions => { - const { disabledExtensions, fileHandler, mentionHandler } = props; + const { disabledExtensions, fileHandler, flaggedExtensions, mentionHandler } = props; const extensions = [ StarterKit.configure({ @@ -133,6 +129,7 @@ export const CoreReadOnlyEditorExtensions = (props: Props): Extensions => { }), ...CoreReadOnlyEditorAdditionalExtensions({ disabledExtensions, + flaggedExtensions, }), ]; diff --git a/packages/editor/src/core/extensions/slash-commands/command-items-list.tsx b/packages/editor/src/core/extensions/slash-commands/command-items-list.tsx index fe9ec06a6..d3ca4856e 100644 --- a/packages/editor/src/core/extensions/slash-commands/command-items-list.tsx +++ b/packages/editor/src/core/extensions/slash-commands/command-items-list.tsx @@ -49,7 +49,7 @@ export type TSlashCommandSection = { export const getSlashCommandFilteredSections = (args: TExtensionProps) => ({ query }: { query: string }): TSlashCommandSection[] => { - const { additionalOptions: externalAdditionalOptions, disabledExtensions } = args; + const { additionalOptions: externalAdditionalOptions, disabledExtensions, flaggedExtensions } = args; const SLASH_COMMAND_SECTIONS: TSlashCommandSection[] = [ { key: "general", @@ -290,6 +290,7 @@ export const getSlashCommandFilteredSections = ...(externalAdditionalOptions ?? []), ...coreEditorAdditionalSlashCommandOptions({ disabledExtensions, + flaggedExtensions, }), ]?.forEach((item) => { const sectionToPushTo = SLASH_COMMAND_SECTIONS.find((s) => s.key === item.section) ?? SLASH_COMMAND_SECTIONS[0]; diff --git a/packages/editor/src/core/extensions/slash-commands/root.tsx b/packages/editor/src/core/extensions/slash-commands/root.tsx index 828149d50..3a29e932c 100644 --- a/packages/editor/src/core/extensions/slash-commands/root.tsx +++ b/packages/editor/src/core/extensions/slash-commands/root.tsx @@ -7,7 +7,7 @@ import { CORE_EXTENSIONS } from "@/constants/extension"; // helpers import { CommandListInstance } from "@/helpers/tippy"; // types -import { ISlashCommandItem, TEditorCommands, TExtensions, TSlashCommandSectionKeys } from "@/types"; +import { IEditorProps, ISlashCommandItem, TEditorCommands, TSlashCommandSectionKeys } from "@/types"; // components import { getSlashCommandFilteredSections } from "./command-items-list"; import { SlashCommandsMenu, SlashCommandsMenuProps } from "./command-menu"; @@ -106,9 +106,8 @@ const renderItems = () => { }; }; -export type TExtensionProps = { +export type TExtensionProps = Pick & { additionalOptions?: TSlashCommandAdditionalOption[]; - disabledExtensions?: TExtensions[]; }; export const SlashCommands = (props: TExtensionProps) => diff --git a/packages/editor/src/core/extensions/utility.ts b/packages/editor/src/core/extensions/utility.ts index 9252e300c..758c74241 100644 --- a/packages/editor/src/core/extensions/utility.ts +++ b/packages/editor/src/core/extensions/utility.ts @@ -8,7 +8,7 @@ import { DropHandlerPlugin } from "@/plugins/drop"; import { FilePlugins } from "@/plugins/file/root"; import { MarkdownClipboardPlugin } from "@/plugins/markdown-clipboard"; // types -import { TExtensions, TFileHandler, TReadOnlyFileHandler } from "@/types"; +import type { IEditorProps, TFileHandler, TReadOnlyFileHandler } from "@/types"; declare module "@tiptap/core" { interface Commands { @@ -23,8 +23,7 @@ export interface UtilityExtensionStorage { uploadInProgress: boolean; } -type Props = { - disabledExtensions: TExtensions[]; +type Props = Pick & { fileHandler: TFileHandler | TReadOnlyFileHandler; isEditable: boolean; }; diff --git a/packages/editor/src/core/hooks/use-collaborative-editor.ts b/packages/editor/src/core/hooks/use-collaborative-editor.ts index 11e7ba320..570315d75 100644 --- a/packages/editor/src/core/hooks/use-collaborative-editor.ts +++ b/packages/editor/src/core/hooks/use-collaborative-editor.ts @@ -9,18 +9,19 @@ import { useEditor } from "@/hooks/use-editor"; // plane editor extensions import { DocumentEditorAdditionalExtensions } from "@/plane-editor/extensions"; // types -import { TCollaborativeEditorProps } from "@/types"; +import { TCollaborativeEditorHookProps } from "@/types"; -export const useCollaborativeEditor = (props: TCollaborativeEditorProps) => { +export const useCollaborativeEditor = (props: TCollaborativeEditorHookProps) => { const { onTransaction, disabledExtensions, editable, - editorClassName, + editorClassName = "", editorProps = {}, embedHandler, - extensions, + extensions = [], fileHandler, + flaggedExtensions, forwardedRef, handleEditorReady, id, @@ -89,16 +90,18 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorProps) => { Collaboration.configure({ document: provider.document, }), - ...(extensions ?? []), + ...extensions, ...DocumentEditorAdditionalExtensions({ disabledExtensions, embedConfig: embedHandler, fileHandler, + flaggedExtensions, provider, userDetails: user, }), ], fileHandler, + flaggedExtensions, forwardedRef, handleEditorReady, mentionHandler, diff --git a/packages/editor/src/core/hooks/use-editor.ts b/packages/editor/src/core/hooks/use-editor.ts index ce3cdbe5f..4c1b93d84 100644 --- a/packages/editor/src/core/hooks/use-editor.ts +++ b/packages/editor/src/core/hooks/use-editor.ts @@ -1,13 +1,12 @@ -import { HocuspocusProvider } from "@hocuspocus/provider"; import { DOMSerializer } from "@tiptap/pm/model"; -import { EditorProps } from "@tiptap/pm/view"; -import { useEditor as useTiptapEditor, Extensions } from "@tiptap/react"; -import { useImperativeHandle, MutableRefObject, useEffect } from "react"; +import { useEditor as useTiptapEditor } from "@tiptap/react"; +import { useImperativeHandle, useEffect } from "react"; import * as Y from "yjs"; // components import { getEditorMenuItems } from "@/components/menus"; // constants import { CORE_EXTENSIONS } from "@/constants/extension"; +import { CORE_EDITOR_META } from "@/constants/meta"; // extensions import { CoreEditorExtensions } from "@/extensions"; // helpers @@ -18,49 +17,19 @@ import { IMarking, scrollSummary, scrollToNodeViaDOMCoordinates } from "@/helper // props import { CoreEditorProps } from "@/props"; // types -import type { - TDocumentEventsServer, - EditorRefApi, - TEditorCommands, - TFileHandler, - TExtensions, - TMentionHandler, -} from "@/types"; -import { CORE_EDITOR_META } from "@/constants/meta"; +import type { TDocumentEventsServer, TEditorCommands, TEditorHookProps } from "@/types"; -export interface CustomEditorProps { - editable: boolean; - editorClassName: string; - editorProps?: EditorProps; - enableHistory: boolean; - disabledExtensions: TExtensions[]; - extensions?: Extensions; - fileHandler: TFileHandler; - forwardedRef?: MutableRefObject; - handleEditorReady?: (value: boolean) => void; - id?: string; - initialValue?: string; - mentionHandler: TMentionHandler; - onChange?: (json: object, html: string) => void; - onTransaction?: () => void; - autofocus?: boolean; - placeholder?: string | ((isFocused: boolean, value: string) => string); - provider?: HocuspocusProvider; - tabIndex?: number; - // undefined when prop is not passed, null if intentionally passed to stop - // swr syncing - value?: string | null | undefined; -} - -export const useEditor = (props: CustomEditorProps) => { +export const useEditor = (props: TEditorHookProps) => { const { + autofocus = false, disabledExtensions, editable = true, - editorClassName, + editorClassName = "", editorProps = {}, enableHistory, extensions = [], fileHandler, + flaggedExtensions, forwardedRef, handleEditorReady, id = "", @@ -69,10 +38,9 @@ export const useEditor = (props: CustomEditorProps) => { onChange, onTransaction, placeholder, + provider, tabIndex, value, - provider, - autofocus = false, } = props; const editor = useTiptapEditor( @@ -94,6 +62,7 @@ export const useEditor = (props: CustomEditorProps) => { disabledExtensions, enableHistory, fileHandler, + flaggedExtensions, mentionHandler, placeholder, tabIndex, diff --git a/packages/editor/src/core/hooks/use-read-only-editor.ts b/packages/editor/src/core/hooks/use-read-only-editor.ts index 6a6e25d9f..d259470ac 100644 --- a/packages/editor/src/core/hooks/use-read-only-editor.ts +++ b/packages/editor/src/core/hooks/use-read-only-editor.ts @@ -1,8 +1,8 @@ -import { HocuspocusProvider } from "@hocuspocus/provider"; -import { EditorProps } from "@tiptap/pm/view"; -import { useEditor as useTiptapEditor, Extensions } from "@tiptap/react"; -import { useImperativeHandle, MutableRefObject, useEffect } from "react"; +import { useEditor as useTiptapEditor } from "@tiptap/react"; +import { useImperativeHandle, useEffect } from "react"; import * as Y from "yjs"; +// constants +import { CORE_EDITOR_META } from "@/constants/meta"; // extensions import { CoreReadOnlyEditorExtensions } from "@/extensions"; // helpers @@ -11,32 +11,19 @@ import { IMarking, scrollSummary } from "@/helpers/scroll-to-node"; // props import { CoreReadOnlyEditorProps } from "@/props"; // types -import type { EditorReadOnlyRefApi, TExtensions, TReadOnlyFileHandler, TReadOnlyMentionHandler } from "@/types"; -import { CORE_EDITOR_META } from "@/constants/meta"; +import type { TReadOnlyEditorHookProps } from "@/types"; -interface CustomReadOnlyEditorProps { - disabledExtensions: TExtensions[]; - editorClassName: string; - editorProps?: EditorProps; - extensions?: Extensions; - forwardedRef?: MutableRefObject; - initialValue?: string; - fileHandler: TReadOnlyFileHandler; - handleEditorReady?: (value: boolean) => void; - mentionHandler: TReadOnlyMentionHandler; - provider?: HocuspocusProvider; -} - -export const useReadOnlyEditor = (props: CustomReadOnlyEditorProps) => { +export const useReadOnlyEditor = (props: TReadOnlyEditorHookProps) => { const { disabledExtensions, - initialValue, - editorClassName, - forwardedRef, - extensions = [], + editorClassName = "", editorProps = {}, + extensions = [], fileHandler, + flaggedExtensions, + forwardedRef, handleEditorReady, + initialValue, mentionHandler, provider, } = props; @@ -59,8 +46,9 @@ export const useReadOnlyEditor = (props: CustomReadOnlyEditorProps) => { extensions: [ ...CoreReadOnlyEditorExtensions({ disabledExtensions, - mentionHandler, fileHandler, + flaggedExtensions, + mentionHandler, }), ...extensions, ], diff --git a/packages/editor/src/core/types/collaboration.ts b/packages/editor/src/core/types/collaboration.ts index 556086232..8921e8f05 100644 --- a/packages/editor/src/core/types/collaboration.ts +++ b/packages/editor/src/core/types/collaboration.ts @@ -1,50 +1,4 @@ -import { Extensions } from "@tiptap/core"; -import { EditorProps } from "@tiptap/pm/view"; -// plane editor types -import { TEmbedConfig } from "@/plane-editor/types"; -// types -import { - EditorReadOnlyRefApi, - EditorRefApi, - TExtensions, - TFileHandler, - TMentionHandler, - TReadOnlyFileHandler, - TReadOnlyMentionHandler, - TRealtimeConfig, - TUserDetails, -} from "@/types"; - export type TServerHandler = { onConnect?: () => void; onServerError?: () => void; }; - -type TCollaborativeEditorHookProps = { - disabledExtensions: TExtensions[]; - editable: boolean; - editorClassName: string; - editorProps?: EditorProps; - extensions?: Extensions; - handleEditorReady?: (value: boolean) => void; - id: string; - realtimeConfig: TRealtimeConfig; - serverHandler?: TServerHandler; - user: TUserDetails; -}; - -export type TCollaborativeEditorProps = TCollaborativeEditorHookProps & { - onTransaction?: () => void; - embedHandler?: TEmbedConfig; - fileHandler: TFileHandler; - forwardedRef?: React.MutableRefObject; - mentionHandler: TMentionHandler; - placeholder?: string | ((isFocused: boolean, value: string) => string); - tabIndex?: number; -}; - -export type TReadOnlyCollaborativeEditorProps = TCollaborativeEditorHookProps & { - fileHandler: TReadOnlyFileHandler; - forwardedRef?: React.MutableRefObject; - mentionHandler: TReadOnlyMentionHandler; -}; diff --git a/packages/editor/src/core/types/config.ts b/packages/editor/src/core/types/config.ts index ace2220ed..60ccfa841 100644 --- a/packages/editor/src/core/types/config.ts +++ b/packages/editor/src/core/types/config.ts @@ -1,3 +1,6 @@ +// plane imports +import { TWebhookConnectionQueryParams } from "@plane/types"; + export type TReadOnlyFileHandler = { checkIfAssetExists: (assetId: string) => Promise; getAssetSrc: (path: string) => Promise; @@ -30,3 +33,15 @@ export type TDisplayConfig = { lineSpacing?: TEditorLineSpacing; wideLayout?: boolean; }; + +export type TUserDetails = { + color: string; + id: string; + name: string; + cookie?: string; +}; + +export type TRealtimeConfig = { + url: string; + queryParams: TWebhookConnectionQueryParams; +}; diff --git a/packages/editor/src/core/types/editor.ts b/packages/editor/src/core/types/editor.ts index ace1048ce..cf3d7d2c7 100644 --- a/packages/editor/src/core/types/editor.ts +++ b/packages/editor/src/core/types/editor.ts @@ -1,13 +1,11 @@ -import { Extensions, JSONContent } from "@tiptap/core"; -import { Selection } from "@tiptap/pm/state"; -// plane types -import { TWebhookConnectionQueryParams } from "@plane/types"; +import type { Extensions, JSONContent } from "@tiptap/core"; +import type { Selection } from "@tiptap/pm/state"; // extension types -import { TTextAlign } from "@/extensions"; +import type { TTextAlign } from "@/extensions"; // helpers -import { IMarking } from "@/helpers/scroll-to-node"; +import type { IMarking } from "@/helpers/scroll-to-node"; // types -import { +import type { TAIHandler, TDisplayConfig, TDocumentEventEmitter, @@ -18,7 +16,9 @@ import { TMentionHandler, TReadOnlyFileHandler, TReadOnlyMentionHandler, + TRealtimeConfig, TServerHandler, + TUserDetails, } from "@/types"; export type TEditorCommands = @@ -114,90 +114,70 @@ export interface EditorRefApi extends EditorReadOnlyRefApi { // editor props export interface IEditorProps { + autofocus?: boolean; + bubbleMenuEnabled?: boolean; containerClassName?: string; displayConfig?: TDisplayConfig; disabledExtensions: TExtensions[]; editorClassName?: string; + extensions?: Extensions; + flaggedExtensions: TExtensions[]; fileHandler: TFileHandler; forwardedRef?: React.MutableRefObject; + handleEditorReady?: (value: boolean) => void; id: string; initialValue: string; mentionHandler: TMentionHandler; onChange?: (json: object, html: string) => void; - onTransaction?: () => void; - handleEditorReady?: (value: boolean) => void; - autofocus?: boolean; onEnterKeyPress?: (e?: any) => void; + onTransaction?: () => void; placeholder?: string | ((isFocused: boolean, value: string) => string); tabIndex?: number; value?: string | null; - bubbleMenuEnabled?: boolean; } -export interface ILiteTextEditor extends IEditorProps { - extensions?: Extensions; -} -export interface IRichTextEditor extends IEditorProps { - extensions?: Extensions; + +export type ILiteTextEditorProps = IEditorProps; +export interface IRichTextEditorProps extends IEditorProps { dragDropEnabled?: boolean; } -export interface ICollaborativeDocumentEditor - extends Omit { +export interface ICollaborativeDocumentEditorProps + extends Omit { aiHandler?: TAIHandler; - bubbleMenuEnabled?: boolean; editable: boolean; embedHandler: TEmbedConfig; - handleEditorReady?: (value: boolean) => void; - id: string; realtimeConfig: TRealtimeConfig; serverHandler?: TServerHandler; user: TUserDetails; } // read only editor props -export interface IReadOnlyEditorProps { - containerClassName?: string; - disabledExtensions: TExtensions[]; - displayConfig?: TDisplayConfig; - editorClassName?: string; - extensions?: Extensions; +export interface IReadOnlyEditorProps + extends Pick< + IEditorProps, + | "containerClassName" + | "disabledExtensions" + | "flaggedExtensions" + | "displayConfig" + | "editorClassName" + | "extensions" + | "handleEditorReady" + | "id" + | "initialValue" + > { fileHandler: TReadOnlyFileHandler; forwardedRef?: React.MutableRefObject; - id: string; - initialValue: string; mentionHandler: TReadOnlyMentionHandler; } -export type ILiteTextReadOnlyEditor = IReadOnlyEditorProps; +export type ILiteTextReadOnlyEditorProps = IReadOnlyEditorProps; -export type IRichTextReadOnlyEditor = IReadOnlyEditorProps; +export type IRichTextReadOnlyEditorProps = IReadOnlyEditorProps; -export interface ICollaborativeDocumentReadOnlyEditor extends Omit { +export interface IDocumentReadOnlyEditorProps extends IReadOnlyEditorProps { embedHandler: TEmbedConfig; - handleEditorReady?: (value: boolean) => void; - id: string; - realtimeConfig: TRealtimeConfig; - serverHandler?: TServerHandler; - user: TUserDetails; } -export interface IDocumentReadOnlyEditor extends IReadOnlyEditorProps { - embedHandler: TEmbedConfig; - handleEditorReady?: (value: boolean) => void; -} - -export type TUserDetails = { - color: string; - id: string; - name: string; - cookie?: string; -}; - -export type TRealtimeConfig = { - url: string; - queryParams: TWebhookConnectionQueryParams; -}; - export interface EditorEvents { beforeCreate: never; create: never; diff --git a/packages/editor/src/core/types/hook.ts b/packages/editor/src/core/types/hook.ts new file mode 100644 index 000000000..2224935ca --- /dev/null +++ b/packages/editor/src/core/types/hook.ts @@ -0,0 +1,50 @@ +import type { HocuspocusProvider } from "@hocuspocus/provider"; +import type { EditorProps } from "@tiptap/pm/view"; +// local imports +import type { ICollaborativeDocumentEditorProps, IEditorProps, IReadOnlyEditorProps } from "./editor"; + +type TCoreHookProps = Pick< + IEditorProps, + "disabledExtensions" | "editorClassName" | "extensions" | "flaggedExtensions" | "handleEditorReady" +> & { + editorProps?: EditorProps; +}; + +export type TEditorHookProps = TCoreHookProps & + Pick< + IEditorProps, + | "autofocus" + | "fileHandler" + | "forwardedRef" + | "id" + | "mentionHandler" + | "onChange" + | "onTransaction" + | "placeholder" + | "tabIndex" + | "value" + > & { + editable: boolean; + enableHistory: boolean; + initialValue?: string; + provider?: HocuspocusProvider; + }; + +export type TCollaborativeEditorHookProps = TCoreHookProps & + Pick< + TEditorHookProps, + | "editable" + | "fileHandler" + | "forwardedRef" + | "id" + | "mentionHandler" + | "onChange" + | "onTransaction" + | "placeholder" + | "tabIndex" + > & + Pick; + +export type TReadOnlyEditorHookProps = TCoreHookProps & + Pick & + Pick; diff --git a/packages/editor/src/core/types/index.ts b/packages/editor/src/core/types/index.ts index 66cb24942..619fa0c78 100644 --- a/packages/editor/src/core/types/index.ts +++ b/packages/editor/src/core/types/index.ts @@ -4,6 +4,7 @@ export * from "./config"; export * from "./editor"; export * from "./embed"; export * from "./extensions"; +export * from "./hook"; export * from "./mention"; export * from "./slash-commands-suggestion"; export * from "@/plane-editor/types"; diff --git a/packages/editor/src/index.ts b/packages/editor/src/index.ts index a2a9afaf9..fec933f91 100644 --- a/packages/editor/src/index.ts +++ b/packages/editor/src/index.ts @@ -36,5 +36,4 @@ export { type IMarking, useEditorMarkings } from "@/hooks/use-editor-markings"; export { useReadOnlyEditor } from "@/hooks/use-read-only-editor"; // types -export type { CustomEditorProps } from "@/hooks/use-editor"; export * from "@/types"; diff --git a/space/core/components/editor/lite-text-editor.tsx b/space/core/components/editor/lite-text-editor.tsx index 5c62d14f8..6342d9344 100644 --- a/space/core/components/editor/lite-text-editor.tsx +++ b/space/core/components/editor/lite-text-editor.tsx @@ -1,6 +1,6 @@ import React from "react"; // plane imports -import { EditorRefApi, ILiteTextEditor, LiteTextEditorWithRef, TFileHandler } from "@plane/editor"; +import { EditorRefApi, ILiteTextEditorProps, LiteTextEditorWithRef, TFileHandler } from "@plane/editor"; import { MakeOptional } from "@plane/types"; import { cn } from "@plane/utils"; // components @@ -10,7 +10,10 @@ import { getEditorFileHandlers } from "@/helpers/editor.helper"; import { isCommentEmpty } from "@/helpers/string.helper"; interface LiteTextEditorWrapperProps - extends MakeOptional, "disabledExtensions"> { + extends MakeOptional< + Omit, + "disabledExtensions" | "flaggedExtensions" + > { anchor: string; workspaceId: string; isSubmitting?: boolean; @@ -27,6 +30,7 @@ export const LiteTextEditor = React.forwardRef(ref: React.ForwardedRef): ref is React.MutableRefObject { @@ -41,6 +45,7 @@ export const LiteTextEditor = React.forwardRef, - "disabledExtensions" + Omit, + "disabledExtensions" | "flaggedExtensions" > & { anchor: string; workspaceId: string; }; export const LiteTextReadOnlyEditor = React.forwardRef( - ({ anchor, workspaceId, disabledExtensions, ...props }, ref) => { + ({ anchor, workspaceId, disabledExtensions, flaggedExtensions, ...props }, ref) => { const { getMemberById } = useMember(); return ( , "disabledExtensions"> { + extends MakeOptional< + Omit, + "disabledExtensions" | "flaggedExtensions" + > { anchor: string; uploadFile: TFileHandler["upload"]; workspaceId: string; } export const RichTextEditor = forwardRef((props, ref) => { - const { anchor, containerClassName, uploadFile, workspaceId, disabledExtensions, ...rest } = props; + const { anchor, containerClassName, uploadFile, workspaceId, disabledExtensions, flaggedExtensions, ...rest } = props; const { getMemberById } = useMember(); return ( , - "disabledExtensions" + Omit, + "disabledExtensions" | "flaggedExtensions" > & { anchor: string; workspaceId: string; }; export const RichTextReadOnlyEditor = React.forwardRef( - ({ anchor, workspaceId, disabledExtensions, ...props }, ref) => { + ({ anchor, workspaceId, disabledExtensions, flaggedExtensions, ...props }, ref) => { const { getMemberById } = useMember(); return ( ({ - documentEditor: ["ai", "collaboration-cursor"], - liteTextEditor: ["ai", "collaboration-cursor"], - richTextEditor: ["ai", "collaboration-cursor"], +export const useEditorFlagging = (workspaceSlug: string): TEditorFlaggingHookReturnType => ({ + document: { + disabled: ["ai", "collaboration-cursor"], + flagged: [], + }, + liteText: { + disabled: ["ai", "collaboration-cursor"], + flagged: [], + }, + richText: { + disabled: ["ai", "collaboration-cursor"], + flagged: [], + }, }); diff --git a/web/core/components/editor/lite-text-editor/lite-text-editor.tsx b/web/core/components/editor/lite-text-editor/lite-text-editor.tsx index a29f84a63..088676358 100644 --- a/web/core/components/editor/lite-text-editor/lite-text-editor.tsx +++ b/web/core/components/editor/lite-text-editor/lite-text-editor.tsx @@ -2,7 +2,7 @@ import React, { useState } from "react"; // plane constants import { EIssueCommentAccessSpecifier } from "@plane/constants"; // plane editor -import { EditorRefApi, ILiteTextEditor, LiteTextEditorWithRef, TFileHandler } from "@plane/editor"; +import { EditorRefApi, ILiteTextEditorProps, LiteTextEditorWithRef, TFileHandler } from "@plane/editor"; // i18n import { useTranslation } from "@plane/i18n"; // components @@ -21,7 +21,10 @@ import { WorkspaceService } from "@/plane-web/services"; const workspaceService = new WorkspaceService(); interface LiteTextEditorWrapperProps - extends MakeOptional, "disabledExtensions"> { + extends MakeOptional< + Omit, + "disabledExtensions" | "flaggedExtensions" + > { workspaceSlug: string; workspaceId: string; projectId?: string; @@ -55,13 +58,13 @@ export const LiteTextEditor = React.forwardRef , - "disabledExtensions" + Omit, + "disabledExtensions" | "flaggedExtensions" > & { workspaceId: string; workspaceSlug: string; @@ -28,14 +28,15 @@ export const LiteTextReadOnlyEditor = React.forwardRef, "disabledExtensions"> { + extends MakeOptional< + Omit, + "disabledExtensions" | "flaggedExtensions" + > { searchMentionCallback: (payload: TSearchEntityRequestPayload) => Promise; workspaceSlug: string; workspaceId: string; @@ -36,7 +39,7 @@ export const RichTextEditor = forwardRef await searchMentionCallback(payload), @@ -47,7 +50,8 @@ export const RichTextEditor = forwardRef, - "disabledExtensions" + Omit, + "disabledExtensions" | "flaggedExtensions" > & { workspaceId: string; workspaceSlug: string; @@ -30,14 +30,15 @@ export const RichTextReadOnlyEditor = React.forwardRef { + extends Omit { workspaceSlug: string; workspaceId: string; projectId?: string; @@ -48,7 +48,7 @@ export const StickyEditor = React.forwardRef(ref: React.ForwardedRef): ref is React.MutableRefObject { @@ -65,7 +65,8 @@ export const StickyEditor = React.forwardRef = observer((props) => { searchEntity: handlers.fetchEntity, }); // editor flaggings - const { documentEditor: disabledExtensions } = useEditorFlagging(workspaceSlug); + const { document: documentEditorExtensions } = useEditorFlagging(workspaceSlug); // page filters const { fontSize, fontStyle, isFullWidth } = usePageFilters(); // derived values @@ -213,7 +213,8 @@ export const PageEditorBody: React.FC = observer((props) => { realtimeConfig={realtimeConfig} serverHandler={serverHandler} user={userConfig} - disabledExtensions={disabledExtensions} + disabledExtensions={documentEditorExtensions.disabled} + flaggedExtensions={documentEditorExtensions.flagged} aiHandler={{ menu: getAIMenu, }} diff --git a/web/core/components/pages/version/editor.tsx b/web/core/components/pages/version/editor.tsx index f0b28e24e..8491408ac 100644 --- a/web/core/components/pages/version/editor.tsx +++ b/web/core/components/pages/version/editor.tsx @@ -32,7 +32,7 @@ export const PagesVersionEditor: React.FC = observer((props // derived values const workspaceDetails = getWorkspaceBySlug(workspaceSlug?.toString() ?? ""); // editor flaggings - const { documentEditor: disabledExtensions } = useEditorFlagging(workspaceSlug?.toString() ?? ""); + const { document: documentEditorExtensions } = useEditorFlagging(workspaceSlug?.toString() ?? ""); // editor config const { getReadOnlyEditorFileHandlers } = useEditorConfig(); // issue-embed @@ -99,7 +99,8 @@ export const PagesVersionEditor: React.FC = observer((props id={activeVersion ?? ""} initialValue={description ?? "

"} containerClassName="p-0 pb-64 border-none" - disabledExtensions={disabledExtensions} + disabledExtensions={documentEditorExtensions.disabled} + flaggedExtensions={documentEditorExtensions.flagged} displayConfig={displayConfig} editorClassName="pl-10" fileHandler={getReadOnlyEditorFileHandlers({ From 64fd0b2830952c172cd1022db577589e04892573 Mon Sep 17 00:00:00 2001 From: Vamsi Krishna <46787868+vamsikrishnamathala@users.noreply.github.com> Date: Thu, 19 Jun 2025 16:26:32 +0530 Subject: [PATCH 178/201] [WEB-4321]chore: workspace views refactor (#7214) * chore: workspace views reafactor * chore: resolved coderabbit suggestions * chore: added project level workspace filter * chore: added enum for roles * chore: removed redundant type definition * chore: optimised the query --------- Co-authored-by: NarayanBavisetti --- apiserver/plane/app/views/issue/base.py | 27 ++- packages/types/src/layout/gantt.d.ts | 2 + packages/utils/src/work-item/base.ts | 3 +- .../(projects)/workspace-views/header.tsx | 34 +++- web/ce/components/views/helper.tsx | 12 ++ web/ce/store/issue/workspace/issue.store.ts | 1 + web/ce/store/timeline/base-timeline.store.ts | 2 + .../components/gantt-chart/blocks/block.tsx | 3 + .../gantt-chart/blocks/blocks-list.tsx | 3 + .../gantt-chart/chart/main-content.tsx | 3 + .../components/gantt-chart/chart/root.tsx | 3 + .../gantt-chart/helpers/draggable.tsx | 10 +- web/core/components/gantt-chart/root.tsx | 3 + .../issue-layouts/gantt/base-gantt-root.tsx | 4 +- .../issue-layouts/properties/labels.tsx | 2 - .../roots/all-issue-layout-root.tsx | 157 +++++------------- .../spreadsheet/roots/workspace-root.tsx | 137 +++++++++++++++ web/core/components/views/helper.tsx | 51 ++++++ web/core/hooks/store/use-issues.ts | 3 +- web/core/services/workspace.service.ts | 5 +- .../store/issue/helpers/base-issues.store.ts | 7 +- .../issue/issue-details/relation.store.ts | 2 +- web/core/store/issue/root.store.ts | 3 +- .../store/issue/workspace/filter.store.ts | 76 ++++----- 24 files changed, 381 insertions(+), 172 deletions(-) create mode 100644 web/ce/components/views/helper.tsx create mode 100644 web/ce/store/issue/workspace/issue.store.ts create mode 100644 web/core/components/issues/issue-layouts/spreadsheet/roots/workspace-root.tsx create mode 100644 web/core/components/views/helper.tsx diff --git a/apiserver/plane/app/views/issue/base.py b/apiserver/plane/app/views/issue/base.py index 2a7e9d021..edce172f9 100644 --- a/apiserver/plane/app/views/issue/base.py +++ b/apiserver/plane/app/views/issue/base.py @@ -944,9 +944,33 @@ class IssueDetailEndpoint(BaseAPIView): @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) def get(self, request, slug, project_id): filters = issue_filters(request.query_params, "GET") + + # check for the project member role, if the role is 5 then check for the guest_view_all_features + # if it is true then show all the issues else show only the issues created by the user + project_member_subquery = ProjectMember.objects.filter( + project_id=OuterRef("project_id"), + member=self.request.user, + is_active=True, + ).filter( + Q(role__gt=ROLE.GUEST.value) + | Q( + role=ROLE.GUEST.value, project__guest_view_all_features=True + ) + ) + + # Main issue query issue = ( Issue.issue_objects.filter(workspace__slug=slug, project_id=project_id) - .select_related("workspace", "project", "state", "parent") + .filter( + Q(Exists(project_member_subquery)) + | Q( + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, + project__project_projectmember__role=ROLE.GUEST.value, + project__guest_view_all_features=False, + created_by=self.request.user, + ) + ) .prefetch_related("assignees", "labels", "issue_module__module") .annotate( cycle_id=Subquery( @@ -1014,6 +1038,7 @@ class IssueDetailEndpoint(BaseAPIView): .values("count") ) ) + issue = issue.filter(**filters) order_by_param = request.GET.get("order_by", "-created_at") # Issue queryset diff --git a/packages/types/src/layout/gantt.d.ts b/packages/types/src/layout/gantt.d.ts index ad5b2afde..990ae3fc3 100644 --- a/packages/types/src/layout/gantt.d.ts +++ b/packages/types/src/layout/gantt.d.ts @@ -9,6 +9,7 @@ export interface IGanttBlock { sort_order: number | undefined; start_date: string | undefined; target_date: string | undefined; + project_id: string | undefined; } export interface IBlockUpdateData { @@ -25,6 +26,7 @@ export interface IBlockUpdateDependencyData { id: string; start_date?: string; target_date?: string; + project_id?: string; } export type TGanttViews = "week" | "month" | "quarter"; diff --git a/packages/utils/src/work-item/base.ts b/packages/utils/src/work-item/base.ts index 9c37605e0..6a7068928 100644 --- a/packages/utils/src/work-item/base.ts +++ b/packages/utils/src/work-item/base.ts @@ -185,6 +185,7 @@ export const getIssueBlocksStructure = (block: TIssue): IGanttBlock => ({ sort_order: block?.sort_order, start_date: block?.start_date ?? undefined, target_date: block?.target_date ?? undefined, + project_id: block?.project_id ?? undefined, }); export const formatTextList = (TextArray: string[]): string => { @@ -260,7 +261,7 @@ export const getComputedDisplayFilters = ( displayFilters: IIssueDisplayFilterOptions = {}, defaultValues?: IIssueDisplayFilterOptions ): IIssueDisplayFilterOptions => { - const filters = displayFilters || defaultValues; + const filters = !isEmpty(displayFilters) ? displayFilters : defaultValues; return { calendar: { diff --git a/web/app/(all)/[workspaceSlug]/(projects)/workspace-views/header.tsx b/web/app/(all)/[workspaceSlug]/(projects)/workspace-views/header.tsx index 1819ac0ee..f5ba4ecb9 100644 --- a/web/app/(all)/[workspaceSlug]/(projects)/workspace-views/header.tsx +++ b/web/app/(all)/[workspaceSlug]/(projects)/workspace-views/header.tsx @@ -1,11 +1,11 @@ "use client"; -import { useCallback, useState } from "react"; +import { useCallback, useMemo, useState } from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; import { Layers } from "lucide-react"; // plane constants -import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_PAGE } from "@plane/constants"; +import { EIssueFilterType, EIssueLayoutTypes, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_PAGE } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; // types import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types"; @@ -19,6 +19,7 @@ import { CreateUpdateWorkspaceViewModal } from "@/components/workspace"; // helpers // hooks import { useLabel, useMember, useIssues, useGlobalView } from "@/hooks/store"; +import { GlobalViewLayoutSelection } from "@/plane-web/components/views/helper"; export const GlobalIssuesHeader = observer(() => { // states @@ -38,6 +39,7 @@ export const GlobalIssuesHeader = observer(() => { const issueFilters = globalViewId ? filters[globalViewId.toString()] : undefined; + const activeLayout = issueFilters?.displayFilters?.layout; const viewDetails = getViewDetailsById(globalViewId.toString()); const handleFiltersUpdate = useCallback( @@ -95,8 +97,27 @@ export const GlobalIssuesHeader = observer(() => { [workspaceSlug, updateFilters, globalViewId] ); + const handleLayoutChange = useCallback( + (layout: EIssueLayoutTypes) => { + if (!workspaceSlug || !globalViewId) return; + updateFilters( + workspaceSlug.toString(), + undefined, + EIssueFilterType.DISPLAY_FILTERS, + { layout: layout }, + globalViewId.toString() + ); + }, + [workspaceSlug, updateFilters, globalViewId] + ); + const isLocked = viewDetails?.is_locked; + const currentLayoutFilters = useMemo(() => { + const layout = activeLayout ?? EIssueLayoutTypes.SPREADSHEET; + return ISSUE_DISPLAY_FILTERS_BY_PAGE.my_issues[layout]; + }, [activeLayout]); + return ( <> setCreateViewModal(false)} /> @@ -113,13 +134,18 @@ export const GlobalIssuesHeader = observer(() => { {!isLocked ? ( <> + { void; + selectedLayout: EIssueLayoutTypes; + workspaceSlug: string; +}; + +export const GlobalViewLayoutSelection = (props: TLayoutSelectionProps) => <>; + +export const WorkspaceAdditionalLayouts = (props: TWorkspaceLayoutProps) => <>; diff --git a/web/ce/store/issue/workspace/issue.store.ts b/web/ce/store/issue/workspace/issue.store.ts new file mode 100644 index 000000000..7317da96d --- /dev/null +++ b/web/ce/store/issue/workspace/issue.store.ts @@ -0,0 +1 @@ +export * from "@/store/issue/workspace/issue.store"; diff --git a/web/ce/store/timeline/base-timeline.store.ts b/web/ce/store/timeline/base-timeline.store.ts index 57a67cc5e..c021fa93e 100644 --- a/web/ce/store/timeline/base-timeline.store.ts +++ b/web/ce/store/timeline/base-timeline.store.ts @@ -22,6 +22,7 @@ type BlockData = { sort_order: number | null; start_date?: string | undefined | null; target_date?: string | undefined | null; + project_id?: string | undefined | null; }; export interface IBaseTimelineStore { @@ -194,6 +195,7 @@ export class BaseTimeLineStore implements IBaseTimelineStore { sort_order: blockData?.sort_order ?? undefined, start_date: blockData?.start_date ?? undefined, target_date: blockData?.target_date ?? undefined, + project_id: blockData?.project_id ?? undefined, }; if (this.currentViewData && (this.currentViewData?.data?.startDate || this.currentViewData?.data?.dayWidth)) { block.position = getItemPositionWidth(this.currentViewData, block); diff --git a/web/core/components/gantt-chart/blocks/block.tsx b/web/core/components/gantt-chart/blocks/block.tsx index f459a02af..0087a3896 100644 --- a/web/core/components/gantt-chart/blocks/block.tsx +++ b/web/core/components/gantt-chart/blocks/block.tsx @@ -20,6 +20,7 @@ type Props = { enableBlockLeftResize: boolean; enableBlockRightResize: boolean; enableBlockMove: boolean; + enableDependency: boolean; ganttContainerRef: RefObject; updateBlockDates?: (updates: IBlockUpdateDependencyData[]) => Promise; }; @@ -33,6 +34,7 @@ export const GanttChartBlock: React.FC = observer((props) => { enableBlockRightResize, enableBlockMove, ganttContainerRef, + enableDependency, updateBlockDates, } = props; // store hooks @@ -90,6 +92,7 @@ export const GanttChartBlock: React.FC = observer((props) => { enableBlockLeftResize={enableBlockLeftResize} enableBlockRightResize={enableBlockRightResize} enableBlockMove={enableBlockMove && !!isBlockComplete} + enableDependency={enableDependency} isMoving={isMoving} ganttContainerRef={ganttContainerRef} /> diff --git a/web/core/components/gantt-chart/blocks/blocks-list.tsx b/web/core/components/gantt-chart/blocks/blocks-list.tsx index c8644b465..154a72d9f 100644 --- a/web/core/components/gantt-chart/blocks/blocks-list.tsx +++ b/web/core/components/gantt-chart/blocks/blocks-list.tsx @@ -12,6 +12,7 @@ export type GanttChartBlocksProps = { ganttContainerRef: React.RefObject; showAllBlocks: boolean; updateBlockDates?: (updates: IBlockUpdateDependencyData[]) => Promise; + enableDependency: boolean | ((blockId: string) => boolean); }; export const GanttChartBlocksList: FC = (props) => { @@ -24,6 +25,7 @@ export const GanttChartBlocksList: FC = (props) => { ganttContainerRef, showAllBlocks, updateBlockDates, + enableDependency, } = props; return ( @@ -41,6 +43,7 @@ export const GanttChartBlocksList: FC = (props) => { typeof enableBlockRightResize === "function" ? enableBlockRightResize(blockId) : enableBlockRightResize } enableBlockMove={typeof enableBlockMove === "function" ? enableBlockMove(blockId) : enableBlockMove} + enableDependency={typeof enableDependency === "function" ? enableDependency(blockId) : enableDependency} ganttContainerRef={ganttContainerRef} updateBlockDates={updateBlockDates} /> diff --git a/web/core/components/gantt-chart/chart/main-content.tsx b/web/core/components/gantt-chart/chart/main-content.tsx index 8297bfae5..5f9fb0043 100644 --- a/web/core/components/gantt-chart/chart/main-content.tsx +++ b/web/core/components/gantt-chart/chart/main-content.tsx @@ -41,6 +41,7 @@ type Props = { enableReorder: boolean | ((blockId: string) => boolean); enableSelection: boolean | ((blockId: string) => boolean); enableAddBlock: boolean | ((blockId: string) => boolean); + enableDependency: boolean | ((blockId: string) => boolean); itemsContainerWidth: number; showAllBlocks: boolean; sidebarToRender: (props: any) => React.ReactNode; @@ -67,6 +68,7 @@ export const GanttChartMainContent: React.FC = observer((props) => { enableReorder, enableAddBlock, enableSelection, + enableDependency, itemsContainerWidth, showAllBlocks, sidebarToRender, @@ -215,6 +217,7 @@ export const GanttChartMainContent: React.FC = observer((props) => { enableBlockRightResize={enableBlockRightResize} enableBlockMove={enableBlockMove} ganttContainerRef={ganttContainerRef} + enableDependency={enableDependency} showAllBlocks={showAllBlocks} updateBlockDates={updateBlockDates} /> diff --git a/web/core/components/gantt-chart/chart/root.tsx b/web/core/components/gantt-chart/chart/root.tsx index 2509e8d55..b69254cae 100644 --- a/web/core/components/gantt-chart/chart/root.tsx +++ b/web/core/components/gantt-chart/chart/root.tsx @@ -37,6 +37,7 @@ type ChartViewRootProps = { enableReorder: boolean | ((blockId: string) => boolean); enableAddBlock: boolean | ((blockId: string) => boolean); enableSelection: boolean | ((blockId: string) => boolean); + enableDependency: boolean | ((blockId: string) => boolean); bottomSpacing: boolean; showAllBlocks: boolean; loadMoreBlocks?: () => void; @@ -70,6 +71,7 @@ export const ChartViewRoot: FC = observer((props) => { enableReorder, enableAddBlock, enableSelection, + enableDependency, bottomSpacing, showAllBlocks, quickAdd, @@ -204,6 +206,7 @@ export const ChartViewRoot: FC = observer((props) => { enableReorder={enableReorder} enableSelection={enableSelection} enableAddBlock={enableAddBlock} + enableDependency={enableDependency} itemsContainerWidth={itemsContainerWidth} showAllBlocks={showAllBlocks} sidebarToRender={sidebarToRender} diff --git a/web/core/components/gantt-chart/helpers/draggable.tsx b/web/core/components/gantt-chart/helpers/draggable.tsx index 35babf9c3..6031f3efd 100644 --- a/web/core/components/gantt-chart/helpers/draggable.tsx +++ b/web/core/components/gantt-chart/helpers/draggable.tsx @@ -18,6 +18,7 @@ type Props = { enableBlockLeftResize: boolean; enableBlockRightResize: boolean; enableBlockMove: boolean; + enableDependency: boolean | ((blockId: string) => boolean); ganttContainerRef: RefObject; }; @@ -29,6 +30,7 @@ export const ChartDraggable: React.FC = observer((props) => { enableBlockLeftResize, enableBlockRightResize, enableBlockMove, + enableDependency, isMoving, ganttContainerRef, } = props; @@ -36,7 +38,9 @@ export const ChartDraggable: React.FC = observer((props) => { return (
{/* left resize drag handle */} - + {(typeof enableDependency === "function" ? enableDependency(block.id) : enableDependency) && ( + + )} = observer((props) => { isMoving={isMoving} position={block.position} /> - + {(typeof enableDependency === "function" ? enableDependency(block.id) : enableDependency) && ( + + )}
); }); diff --git a/web/core/components/gantt-chart/root.tsx b/web/core/components/gantt-chart/root.tsx index 3e761477c..67930aa58 100644 --- a/web/core/components/gantt-chart/root.tsx +++ b/web/core/components/gantt-chart/root.tsx @@ -24,6 +24,7 @@ type GanttChartRootProps = { enableReorder?: boolean | ((blockId: string) => boolean); enableAddBlock?: boolean | ((blockId: string) => boolean); enableSelection?: boolean | ((blockId: string) => boolean); + enableDependency?: boolean | ((blockId: string) => boolean); bottomSpacing?: boolean; showAllBlocks?: boolean; showToday?: boolean; @@ -47,6 +48,7 @@ export const GanttChartRoot: FC = observer((props) => { enableReorder = false, enableAddBlock = false, enableSelection = false, + enableDependency = false, bottomSpacing = false, showAllBlocks = false, showToday = true, @@ -79,6 +81,7 @@ export const GanttChartRoot: FC = observer((props) => { enableReorder={enableReorder} enableAddBlock={enableAddBlock} enableSelection={enableSelection} + enableDependency={enableDependency} bottomSpacing={bottomSpacing} showAllBlocks={showAllBlocks} quickAdd={quickAdd} diff --git a/web/core/components/issues/issue-layouts/gantt/base-gantt-root.tsx b/web/core/components/issues/issue-layouts/gantt/base-gantt-root.tsx index 103b49f35..96cff38ad 100644 --- a/web/core/components/issues/issue-layouts/gantt/base-gantt-root.tsx +++ b/web/core/components/issues/issue-layouts/gantt/base-gantt-root.tsx @@ -98,14 +98,14 @@ export const BaseGanttRoot: React.FC = observer((props: IBaseGan target_date?: string; }[] ) => - issues.updateIssueDates(workspaceSlug.toString(), projectId.toString(), updates).catch(() => { + issues.updateIssueDates(workspaceSlug.toString(), updates, projectId.toString()).catch(() => { setToast({ type: TOAST_TYPE.ERROR, title: t("toast.error"), message: "Error while updating work item dates, Please try again Later", }); }), - [issues] + [issues, projectId, workspaceSlug] ); const quickAdd = diff --git a/web/core/components/issues/issue-layouts/properties/labels.tsx b/web/core/components/issues/issue-layouts/properties/labels.tsx index 7c0ed4cd2..b8f21bfba 100644 --- a/web/core/components/issues/issue-layouts/properties/labels.tsx +++ b/web/core/components/issues/issue-layouts/properties/labels.tsx @@ -82,8 +82,6 @@ export const IssuePropertyLabels: React.FC = observer((pro } }, [isOpen, isMobile]); - if (!value) return null; - let projectLabels: IIssueLabel[] = defaultOptions as IIssueLabel[]; if (storeLabels && storeLabels.length > 0) projectLabels = storeLabels; diff --git a/web/core/components/issues/issue-layouts/roots/all-issue-layout-root.tsx b/web/core/components/issues/issue-layouts/roots/all-issue-layout-root.tsx index 372c5605b..cb29d55a2 100644 --- a/web/core/components/issues/issue-layouts/roots/all-issue-layout-root.tsx +++ b/web/core/components/issues/issue-layouts/roots/all-issue-layout-root.tsx @@ -1,34 +1,20 @@ import React, { useCallback } from "react"; -import isEmpty from "lodash/isEmpty"; +import { isEmpty } from "lodash"; import { observer } from "mobx-react"; import { useParams, useSearchParams } from "next/navigation"; -import useSWR from "swr"; // plane constants -import { - ALL_ISSUES, - EIssueLayoutTypes, - EIssueFilterType, - EIssuesStoreType, - ISSUE_DISPLAY_FILTERS_BY_PAGE -,EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; -import { IIssueDisplayFilterOptions } from "@plane/types"; +import useSWR from "swr"; +import { EIssueFilterType, EIssueLayoutTypes, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_PAGE } from "@plane/constants"; // hooks // components -import { EmptyState } from "@/components/common"; -import { SpreadsheetView } from "@/components/issues/issue-layouts"; -import { AllIssueQuickActions } from "@/components/issues/issue-layouts/quick-action-dropdowns"; -import { SpreadsheetLayoutLoader } from "@/components/ui"; // hooks -import { useGlobalView, useIssues, useUserPermissions } from "@/hooks/store"; +import { EmptyState } from "@/components/common"; +import { WorkspaceActiveLayout } from "@/components/views/helper"; +import { useGlobalView, useIssues } from "@/hooks/store"; import { useAppRouter } from "@/hooks/use-app-router"; -import { IssuesStoreContext } from "@/hooks/use-issue-layout-store"; -import { useIssuesActions } from "@/hooks/use-issues-actions"; import { useWorkspaceIssueProperties } from "@/hooks/use-workspace-issue-properties"; // store import emptyView from "@/public/empty-state/view.svg"; -import { IssuePeekOverview } from "../../peek-overview"; -import { IssueLayoutHOC } from "../issue-layout-HOC"; -import { TRenderQuickActions } from "../list/list-view-types"; type Props = { isDefaultView: boolean; @@ -38,32 +24,34 @@ type Props = { export const AllIssueLayoutRoot: React.FC = observer((props: Props) => { const { isDefaultView, isLoading = false, toggleLoading } = props; - // router - const { workspaceSlug, globalViewId } = useParams(); + + // Router hooks const router = useAppRouter(); + const { workspaceSlug, globalViewId } = useParams(); const searchParams = useSearchParams(); - const routeFilters: { - [key: string]: string; - } = {}; + + // Store hooks + const { + issuesFilter: { filters, fetchFilters, updateFilters }, + issues: { clear, groupedIssueIds, fetchIssues, fetchNextIssues }, + } = useIssues(EIssuesStoreType.GLOBAL); + const { fetchAllGlobalViews, getViewDetailsById } = useGlobalView(); + + // Custom hooks + useWorkspaceIssueProperties(workspaceSlug); + + // Derived values + const viewDetails = getViewDetailsById(globalViewId?.toString()); + const issueFilters = globalViewId ? filters?.[globalViewId.toString()] : undefined; + const activeLayout: EIssueLayoutTypes | undefined = issueFilters?.displayFilters?.layout; + + // Route filters + const routeFilters: { [key: string]: string } = {}; searchParams.forEach((value: string, key: string) => { routeFilters[key] = value; }); - //swr hook for fetching issue properties - useWorkspaceIssueProperties(workspaceSlug); - // store - const { - issuesFilter: { filters, fetchFilters, updateFilters }, - issues: { clear, getIssueLoader, getPaginationData, groupedIssueIds, fetchIssues, fetchNextIssues }, - } = useIssues(EIssuesStoreType.GLOBAL); - const { updateIssue, removeIssue, archiveIssue } = useIssuesActions(EIssuesStoreType.GLOBAL); - - const { allowPermissions } = useUserPermissions(); - - const { fetchAllGlobalViews, getViewDetailsById } = useGlobalView(); - - const viewDetails = getViewDetailsById(globalViewId?.toString()); - // filter init from the query params + // Apply route filters to store const routerFilterParams = () => { if ( workspaceSlug && @@ -89,10 +77,12 @@ export const AllIssueLayoutRoot: React.FC = observer((props: Props) => { } }; + // Fetch next pages callback const fetchNextPages = useCallback(() => { if (workspaceSlug && globalViewId) fetchNextIssues(workspaceSlug.toString(), globalViewId.toString()); }, [fetchNextIssues, workspaceSlug, globalViewId]); + // Fetch global views const { isLoading: globalViewsLoading } = useSWR( workspaceSlug ? `WORKSPACE_GLOBAL_VIEWS_${workspaceSlug}` : null, async () => { @@ -103,6 +93,7 @@ export const AllIssueLayoutRoot: React.FC = observer((props: Props) => { { revalidateIfStale: false, revalidateOnFocus: false } ); + // Fetch issues const { isLoading: issuesLoading } = useSWR( workspaceSlug && globalViewId ? `WORKSPACE_GLOBAL_VIEW_ISSUES_${workspaceSlug}_${globalViewId}` : null, async () => { @@ -126,54 +117,7 @@ export const AllIssueLayoutRoot: React.FC = observer((props: Props) => { { revalidateIfStale: false, revalidateOnFocus: false } ); - const canEditProperties = useCallback( - (projectId: string | undefined) => { - if (!projectId) return false; - return allowPermissions( - [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - EUserPermissionsLevel.PROJECT, - workspaceSlug.toString(), - projectId - ); - }, - [workspaceSlug] - ); - - const issueFilters = globalViewId ? filters?.[globalViewId.toString()] : undefined; - - const handleDisplayFiltersUpdate = useCallback( - (updatedDisplayFilter: Partial) => { - if (!workspaceSlug || !globalViewId) return; - - updateFilters( - workspaceSlug.toString(), - undefined, - EIssueFilterType.DISPLAY_FILTERS, - { ...updatedDisplayFilter }, - globalViewId.toString() - ); - }, - [updateFilters, workspaceSlug, globalViewId] - ); - - const renderQuickActions: TRenderQuickActions = useCallback( - ({ issue, parentRef, customActionButton, placement, portalElement }) => ( - removeIssue(issue.project_id, issue.id)} - handleUpdate={async (data) => updateIssue && updateIssue(issue.project_id, issue.id, data)} - handleArchive={async () => archiveIssue && archiveIssue(issue.project_id, issue.id)} - portalElement={portalElement} - readOnly={!canEditProperties(issue.project_id ?? undefined)} - placements={placement} - /> - ), - [canEditProperties, removeIssue, updateIssue, archiveIssue] - ); - - // when the call is not loading and the view does not exist and the view is not a default view, show empty state + // Empty state if (!isLoading && !globalViewsLoading && !issuesLoading && !viewDetails && !isDefaultView) { return ( = observer((props: Props) => { ); } - if ((isLoading && issuesLoading && getIssueLoader() === "init-loader") || !globalViewId || !groupedIssueIds) { - return ; - } - - const issueIds = groupedIssueIds[ALL_ISSUES]; - const nextPageResults = getPaginationData(ALL_ISSUES, undefined)?.nextPageResults; - return ( - - - - {/* peek overview */} - - - + ); }); diff --git a/web/core/components/issues/issue-layouts/spreadsheet/roots/workspace-root.tsx b/web/core/components/issues/issue-layouts/spreadsheet/roots/workspace-root.tsx new file mode 100644 index 000000000..caba1176d --- /dev/null +++ b/web/core/components/issues/issue-layouts/spreadsheet/roots/workspace-root.tsx @@ -0,0 +1,137 @@ +import React, { useCallback } from "react"; +import { observer } from "mobx-react"; +// plane constants +import { + ALL_ISSUES, + EIssueLayoutTypes, + EIssueFilterType, + EIssuesStoreType, + EUserPermissions, + EUserPermissionsLevel, +} from "@plane/constants"; +import { IIssueDisplayFilterOptions } from "@plane/types"; +// hooks +// components +import { SpreadsheetView } from "@/components/issues/issue-layouts"; +import { AllIssueQuickActions } from "@/components/issues/issue-layouts/quick-action-dropdowns"; +import { SpreadsheetLayoutLoader } from "@/components/ui"; +// hooks +import { useIssues, useUserPermissions } from "@/hooks/store"; +import { IssuesStoreContext } from "@/hooks/use-issue-layout-store"; +import { useIssuesActions } from "@/hooks/use-issues-actions"; +import { useWorkspaceIssueProperties } from "@/hooks/use-workspace-issue-properties"; +// store +import { IssuePeekOverview } from "../../../peek-overview"; +import { IssueLayoutHOC } from "../../issue-layout-HOC"; +import { TRenderQuickActions } from "../../list/list-view-types"; + +type Props = { + isDefaultView: boolean; + isLoading?: boolean; + toggleLoading: (value: boolean) => void; + workspaceSlug: string; + globalViewId: string; + routeFilters: { + [key: string]: string; + }; + fetchNextPages: () => void; + globalViewsLoading: boolean; + issuesLoading: boolean; +}; + +export const WorkspaceSpreadsheetRoot: React.FC = observer((props: Props) => { + const { isLoading = false, workspaceSlug, globalViewId, fetchNextPages, issuesLoading } = props; + + // Custom hooks + useWorkspaceIssueProperties(workspaceSlug); + + // Store hooks + const { + issuesFilter: { filters, updateFilters }, + issues: { getIssueLoader, getPaginationData, groupedIssueIds }, + } = useIssues(EIssuesStoreType.GLOBAL); + const { updateIssue, removeIssue, archiveIssue } = useIssuesActions(EIssuesStoreType.GLOBAL); + const { allowPermissions } = useUserPermissions(); + + // Derived values + const issueFilters = globalViewId ? filters?.[globalViewId.toString()] : undefined; + + // Permission checker + const canEditProperties = useCallback( + (projectId: string | undefined) => { + if (!projectId) return false; + return allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.PROJECT, + workspaceSlug.toString(), + projectId + ); + }, + [allowPermissions, workspaceSlug] + ); + + // Display filters handler + const handleDisplayFiltersUpdate = useCallback( + (updatedDisplayFilter: Partial) => { + if (!workspaceSlug || !globalViewId) return; + + updateFilters( + workspaceSlug.toString(), + undefined, + EIssueFilterType.DISPLAY_FILTERS, + { ...updatedDisplayFilter }, + globalViewId.toString() + ); + }, + [updateFilters, workspaceSlug, globalViewId] + ); + + // Quick actions renderer + const renderQuickActions: TRenderQuickActions = useCallback( + ({ issue, parentRef, customActionButton, placement, portalElement }) => ( + removeIssue(issue.project_id, issue.id)} + handleUpdate={async (data) => updateIssue && updateIssue(issue.project_id, issue.id, data)} + handleArchive={async () => archiveIssue && archiveIssue(issue.project_id, issue.id)} + portalElement={portalElement} + readOnly={!canEditProperties(issue.project_id ?? undefined)} + placements={placement} + /> + ), + [canEditProperties, removeIssue, updateIssue, archiveIssue] + ); + + // Loading state + if ((isLoading && issuesLoading && getIssueLoader() === "init-loader") || !globalViewId || !groupedIssueIds) { + return ; + } + + // Computed values + const issueIds = groupedIssueIds[ALL_ISSUES]; + const nextPageResults = getPaginationData(ALL_ISSUES, undefined)?.nextPageResults; + + // Render spreadsheet + return ( + + + + {/* peek overview */} + + + + ); +}); diff --git a/web/core/components/views/helper.tsx b/web/core/components/views/helper.tsx new file mode 100644 index 000000000..975e103fd --- /dev/null +++ b/web/core/components/views/helper.tsx @@ -0,0 +1,51 @@ +import { EIssueLayoutTypes } from "@plane/constants"; +import { WorkspaceAdditionalLayouts } from "@/plane-web/components/views/helper"; +import { WorkspaceSpreadsheetRoot } from "../issues/issue-layouts/spreadsheet/roots/workspace-root"; + +export type TWorkspaceLayoutProps = { + activeLayout: EIssueLayoutTypes | undefined; + isDefaultView: boolean; + isLoading?: boolean; + toggleLoading: (value: boolean) => void; + workspaceSlug: string; + globalViewId: string; + routeFilters: { + [key: string]: string; + }; + fetchNextPages: () => void; + globalViewsLoading: boolean; + issuesLoading: boolean; +}; + +export const WorkspaceActiveLayout = (props: TWorkspaceLayoutProps) => { + const { + activeLayout = EIssueLayoutTypes.SPREADSHEET, + isDefaultView, + isLoading, + toggleLoading, + workspaceSlug, + globalViewId, + routeFilters, + fetchNextPages, + globalViewsLoading, + issuesLoading, + } = props; + switch (activeLayout) { + case EIssueLayoutTypes.SPREADSHEET: + return ( + + ); + default: + return ; + } +}; diff --git a/web/core/hooks/store/use-issues.ts b/web/core/hooks/store/use-issues.ts index e5842cd1c..bdb442c5f 100644 --- a/web/core/hooks/store/use-issues.ts +++ b/web/core/hooks/store/use-issues.ts @@ -9,6 +9,7 @@ import { IProjectEpics, IProjectEpicsFilter } from "@/plane-web/store/issue/epic // types import { ITeamIssues, ITeamIssuesFilter } from "@/plane-web/store/issue/team"; import { ITeamViewIssues, ITeamViewIssuesFilter } from "@/plane-web/store/issue/team-views"; +import { IWorkspaceIssues } from "@/plane-web/store/issue/workspace/issue.store"; import { IArchivedIssues, IArchivedIssuesFilter } from "@/store/issue/archived"; import { ICycleIssues, ICycleIssuesFilter } from "@/store/issue/cycle"; import { IDraftIssues, IDraftIssuesFilter } from "@/store/issue/draft"; @@ -16,7 +17,7 @@ import { IModuleIssues, IModuleIssuesFilter } from "@/store/issue/module"; import { IProfileIssues, IProfileIssuesFilter } from "@/store/issue/profile"; import { IProjectIssues, IProjectIssuesFilter } from "@/store/issue/project"; import { IProjectViewIssues, IProjectViewIssuesFilter } from "@/store/issue/project-views"; -import { IWorkspaceIssues, IWorkspaceIssuesFilter } from "@/store/issue/workspace"; +import { IWorkspaceIssuesFilter } from "@/store/issue/workspace"; import { IWorkspaceDraftIssues, IWorkspaceDraftIssuesFilter } from "@/store/issue/workspace-draft"; // constants diff --git a/web/core/services/workspace.service.ts b/web/core/services/workspace.service.ts index 95ed2c976..bb91bc121 100644 --- a/web/core/services/workspace.service.ts +++ b/web/core/services/workspace.service.ts @@ -263,8 +263,11 @@ export class WorkspaceService extends APIService { } async getViewIssues(workspaceSlug: string, params: any, config = {}): Promise { + const path = params.expand?.includes("issue_relation") + ? `/api/workspaces/${workspaceSlug}/issues-detail/` + : `/api/workspaces/${workspaceSlug}/issues/`; return this.get( - `/api/workspaces/${workspaceSlug}/issues/`, + path, { params, }, diff --git a/web/core/store/issue/helpers/base-issues.store.ts b/web/core/store/issue/helpers/base-issues.store.ts index f7dd980d2..7c84d848b 100644 --- a/web/core/store/issue/helpers/base-issues.store.ts +++ b/web/core/store/issue/helpers/base-issues.store.ts @@ -113,7 +113,7 @@ export interface IBaseIssuesStore { addModuleIds: string[], removeModuleIds: string[] ): Promise; - updateIssueDates(workspaceSlug: string, projectId: string, updates: IBlockUpdateDependencyData[]): Promise; + updateIssueDates(workspaceSlug: string, updates: IBlockUpdateDependencyData[], projectId?: string): Promise; } // This constant maps the group by keys to the respective issue property that the key relies on @@ -826,9 +826,10 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore { async updateIssueDates( workspaceSlug: string, - projectId: string, - updates: { id: string; start_date?: string; target_date?: string }[] + updates: { id: string; start_date?: string; target_date?: string }[], + projectId?: string ) { + if(!projectId) return; const issueDatesBeforeChange: { id: string; start_date?: string; target_date?: string }[] = []; try { const getIssueById = this.rootIssueStore.issues.getIssueById; diff --git a/web/core/store/issue/issue-details/relation.store.ts b/web/core/store/issue/issue-details/relation.store.ts index edabc1a9f..cb446e32c 100644 --- a/web/core/store/issue/issue-details/relation.store.ts +++ b/web/core/store/issue/issue-details/relation.store.ts @@ -182,7 +182,7 @@ export class IssueRelationStore implements IIssueRelationStore { */ createCurrentRelation = async (issueId: string, relationType: TIssueRelationTypes, relatedIssueId: string) => { const workspaceSlug = this.rootIssueDetailStore.rootIssueStore.workspaceSlug; - const projectId = this.rootIssueDetailStore.rootIssueStore.projectId; + const projectId = this.rootIssueDetailStore.issue.getIssueById(issueId)?.project_id; if (!workspaceSlug || !projectId) return; diff --git a/web/core/store/issue/root.store.ts b/web/core/store/issue/root.store.ts index 31e1db54e..33710fae1 100644 --- a/web/core/store/issue/root.store.ts +++ b/web/core/store/issue/root.store.ts @@ -14,6 +14,7 @@ import { TeamViewIssuesFilter, } from "@/plane-web/store/issue/team-views"; // root store +import { IWorkspaceIssues, WorkspaceIssues } from "@/plane-web/store/issue/workspace/issue.store"; import { RootStore } from "@/plane-web/store/root.store"; import { IWorkspaceMembership } from "@/store/member/workspace-member.store"; // issues data store @@ -32,7 +33,7 @@ import { IProjectViewIssues, ProjectViewIssues, } from "./project-views"; -import { WorkspaceIssuesFilter, IWorkspaceIssues, WorkspaceIssues, IWorkspaceIssuesFilter } from "./workspace"; +import { WorkspaceIssuesFilter, IWorkspaceIssuesFilter } from "./workspace"; import { IWorkspaceDraftIssues, IWorkspaceDraftIssuesFilter, diff --git a/web/core/store/issue/workspace/filter.store.ts b/web/core/store/issue/workspace/filter.store.ts index 2a98ba03c..71ce319d3 100644 --- a/web/core/store/issue/workspace/filter.store.ts +++ b/web/core/store/issue/workspace/filter.store.ts @@ -132,53 +132,49 @@ export class WorkspaceIssuesFilter extends IssueFilterHelperStore implements IWo ); fetchFilters = async (workspaceSlug: string, viewId: TWorkspaceFilters) => { - try { - let filters: IIssueFilterOptions; - let displayFilters: IIssueDisplayFilterOptions; - let displayProperties: IIssueDisplayProperties; - let kanbanFilters: TIssueKanbanFilters = { - group_by: [], - sub_group_by: [], - }; + let filters: IIssueFilterOptions; + let displayFilters: IIssueDisplayFilterOptions; + let displayProperties: IIssueDisplayProperties; + let kanbanFilters: TIssueKanbanFilters = { + group_by: [], + sub_group_by: [], + }; - const _filters = this.handleIssuesLocalFilters.get(EIssuesStoreType.GLOBAL, workspaceSlug, undefined, viewId); + const _filters = this.handleIssuesLocalFilters.get(EIssuesStoreType.GLOBAL, workspaceSlug, undefined, viewId); + displayFilters = this.computedDisplayFilters(_filters?.display_filters, { + layout: EIssueLayoutTypes.SPREADSHEET, + order_by: "-created_at", + }); + displayProperties = this.computedDisplayProperties(_filters?.display_properties); + kanbanFilters = { + group_by: _filters?.kanban_filters?.group_by || [], + sub_group_by: _filters?.kanban_filters?.sub_group_by || [], + }; + + if (["all-issues", "assigned", "created", "subscribed"].includes(viewId)) { + const currentUserId = this.rootIssueStore.currentUserId; + filters = this.getComputedFiltersBasedOnViews(currentUserId, viewId as TStaticViewTypes); + } else { + const _filters = await this.issueFilterService.getViewDetails(workspaceSlug, viewId); + filters = this.computedFilters(_filters?.filters); displayFilters = this.computedDisplayFilters(_filters?.display_filters, { layout: EIssueLayoutTypes.SPREADSHEET, order_by: "-created_at", }); displayProperties = this.computedDisplayProperties(_filters?.display_properties); - kanbanFilters = { - group_by: _filters?.kanban_filters?.group_by || [], - sub_group_by: _filters?.kanban_filters?.sub_group_by || [], - }; - - if (["all-issues", "assigned", "created", "subscribed"].includes(viewId)) { - const currentUserId = this.rootIssueStore.currentUserId; - filters = this.getComputedFiltersBasedOnViews(currentUserId, viewId as TStaticViewTypes); - } else { - const _filters = await this.issueFilterService.getViewDetails(workspaceSlug, viewId); - filters = this.computedFilters(_filters?.filters); - displayFilters = this.computedDisplayFilters(_filters?.display_filters, { - layout: EIssueLayoutTypes.SPREADSHEET, - order_by: "-created_at", - }); - displayProperties = this.computedDisplayProperties(_filters?.display_properties); - } - - // override existing order by if ordered by manual sort_order - if (displayFilters.order_by === "sort_order") { - displayFilters.order_by = "-created_at"; - } - - runInAction(() => { - set(this.filters, [viewId, "filters"], filters); - set(this.filters, [viewId, "displayFilters"], displayFilters); - set(this.filters, [viewId, "displayProperties"], displayProperties); - set(this.filters, [viewId, "kanbanFilters"], kanbanFilters); - }); - } catch (error) { - throw error; } + + // override existing order by if ordered by manual sort_order + if (displayFilters.order_by === "sort_order") { + displayFilters.order_by = "-created_at"; + } + + runInAction(() => { + set(this.filters, [viewId, "filters"], filters); + set(this.filters, [viewId, "displayFilters"], displayFilters); + set(this.filters, [viewId, "displayProperties"], displayProperties); + set(this.filters, [viewId, "kanbanFilters"], kanbanFilters); + }); }; updateFilters = async ( From 2b7a17b484fdf356713381a856ab68f0895517d9 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Thu, 19 Jun 2025 17:17:14 +0530 Subject: [PATCH 179/201] [WEB-4050] feat: breadcrumbs revamp (#7188) * chore: project feature enum added * feat: revamp breadcrumb and add navigation dropdown component * chore: custom search select component refactoring * chore: breadcrumb stories added * chore: switch label and breadcrumb link component refactor * chore: project navigation helper function added * chore: common breadcrumb component added * chore: breadcrumb refactoring * chore: code refactor * chore: code refactor * fix: build error * fix: nprogress and button tooltip * chore: code refactor * chore: workspace view breadcrumb improvements * chore: code refactor * chore: code refactor * chore: code refactor * chore: code refactor --------- Co-authored-by: vamsikrishnamathala --- admin/core/components/auth-header.tsx | 10 +- packages/constants/src/project.ts | 9 + .../src/breadcrumbs/breadcrumbs.stories.tsx | 233 ++++++++++++++++++ packages/ui/src/breadcrumbs/breadcrumbs.tsx | 172 ++++++++++--- packages/ui/src/breadcrumbs/index.ts | 1 + .../src/breadcrumbs/navigation-dropdown.tsx | 82 ++++-- .../navigation-search-dropdown.tsx | 96 ++++++++ .../ui/src/dropdowns/custom-search-select.tsx | 14 +- packages/ui/src/header/header.tsx | 5 +- .../(projects)/active-cycles/header.tsx | 25 +- .../(projects)/analytics/header.tsx | 5 +- .../(projects)/browse/[workItem]/header.tsx | 68 ++--- .../(projects)/drafts/header.tsx | 5 +- .../[workspaceSlug]/(projects)/header.tsx | 5 +- .../(projects)/profile/[userId]/header.tsx | 5 +- .../(detail)/[projectId]/archives/header.tsx | 12 +- .../archives/issues/(detail)/header.tsx | 17 +- .../[projectId]/cycles/(detail)/header.tsx | 85 +++---- .../[projectId]/cycles/(list)/header.tsx | 24 +- .../[projectId]/draft-issues/header.tsx | 7 +- .../[projectId]/modules/(detail)/header.tsx | 100 +++----- .../[projectId]/modules/(list)/header.tsx | 20 +- .../[projectId]/pages/(detail)/header.tsx | 57 ++--- .../[projectId]/pages/(list)/header.tsx | 24 +- .../views/(detail)/[viewId]/header.tsx | 39 +-- .../[projectId]/views/(list)/header.tsx | 16 +- .../(projects)/stickies/header.tsx | 5 +- .../workspace-views/[globalViewId]/page.tsx | 2 - .../(projects)/workspace-views/header.tsx | 84 ++++++- web/ce/components/breadcrumbs/common.tsx | 32 +++ web/ce/components/breadcrumbs/index.ts | 2 + .../breadcrumbs/project-feature.tsx | 69 ++++++ web/ce/components/breadcrumbs/project.tsx | 87 +++++-- web/ce/components/issues/header.tsx | 34 +-- .../components/projects/navigation/helper.tsx | 77 ++++++ .../components/projects/navigation/index.ts | 1 + .../projects/settings/intake/header.tsx | 19 +- .../components/common/breadcrumb-link.tsx | 95 ++++--- web/core/components/common/switcher-label.tsx | 34 ++- web/core/components/project/header.tsx | 7 +- .../sidebar/header/root.tsx | 5 +- .../views/default-view-quick-action.tsx | 43 +--- .../components/workspace/views/header.tsx | 15 +- .../workspace/views/quick-action.tsx | 85 ++++--- 44 files changed, 1251 insertions(+), 581 deletions(-) create mode 100644 packages/ui/src/breadcrumbs/breadcrumbs.stories.tsx create mode 100644 packages/ui/src/breadcrumbs/navigation-search-dropdown.tsx create mode 100644 web/ce/components/breadcrumbs/common.tsx create mode 100644 web/ce/components/breadcrumbs/project-feature.tsx create mode 100644 web/ce/components/projects/navigation/helper.tsx create mode 100644 web/ce/components/projects/navigation/index.ts diff --git a/admin/core/components/auth-header.tsx b/admin/core/components/auth-header.tsx index 5edcb6118..b97dd7c9e 100644 --- a/admin/core/components/auth-header.tsx +++ b/admin/core/components/auth-header.tsx @@ -67,9 +67,8 @@ export const InstanceHeader: FC = observer(() => { {breadcrumbItems.length >= 0 && (
- { {breadcrumbItems.map( (item) => item.title && ( - } + component={} /> ) )} diff --git a/packages/constants/src/project.ts b/packages/constants/src/project.ts index df22641e8..590dbb89e 100644 --- a/packages/constants/src/project.ts +++ b/packages/constants/src/project.ts @@ -149,3 +149,12 @@ export const DEFAULT_PROJECT_FORM_VALUES: Partial = { network: 2, project_lead: null, }; + +export enum EProjectFeatureKey { + WORK_ITEMS = "work_items", + CYCLES = "cycles", + MODULES = "modules", + VIEWS = "views", + PAGES = "pages", + INTAKE = "intake", +} diff --git a/packages/ui/src/breadcrumbs/breadcrumbs.stories.tsx b/packages/ui/src/breadcrumbs/breadcrumbs.stories.tsx new file mode 100644 index 000000000..9b8cb8140 --- /dev/null +++ b/packages/ui/src/breadcrumbs/breadcrumbs.stories.tsx @@ -0,0 +1,233 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { Home, Settings, Briefcase, GridIcon, Layers2, FileIcon } from "lucide-react"; +import * as React from "react"; +import { ContrastIcon, EpicIcon, LayersIcon } from "../icons"; +import { Breadcrumbs } from "./breadcrumbs"; +import { BreadcrumbNavigationDropdown } from "./navigation-dropdown"; + +const meta: Meta = { + title: "UI/Breadcrumbs", + component: Breadcrumbs, + tags: ["autodocs"], + argTypes: { + isLoading: { + control: "boolean", + description: "Shows loading state of breadcrumbs", + }, + onBack: { + action: "onBack", + description: "Callback function when back button is clicked", + }, + }, +}; + +type TBreadcrumbBlockProps = { + href?: string; + label?: string; + icon?: React.ReactNode; + disableTooltip?: boolean; +}; + +// TODO: remove this component and use web Link component +const BreadcrumbBlock: React.FC = (props) => { + const { label, icon, disableTooltip = false } = props; + + return ( + <> + + {icon &&
{icon}
} + {label &&
{label}
} +
+ + ); +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + children: [ + } />, + } />, + } + />, + ], + }, +}; + +export const WithLoading: Story = { + args: { + isLoading: true, + children: [ + } />, + } />, + ], + }, +}; + +export const WithCustomComponent: Story = { + args: { + children: [ + } />, + + + Custom Component +
+ } + />, + ], + }, +}; + +export const SingleItem: Story = { + args: { + children: [} />], + }, +}; + +export const WithNavigationDropdown: Story = { + args: { + children: [ + } />, + console.log("Project Alpha selected"), + }, + { + key: "project-2", + title: "Project Beta", + + action: () => console.log("Project Beta selected"), + }, + { + key: "project-3", + title: "Project Gamma", + + action: () => console.log("Project Gamma selected"), + }, + ]} + /> + } + showSeparator={false} + />, + } />, + ], + }, +}; + +export const WithNavigationDropdownAndIcons: Story = { + args: { + children: [ + } />} + />, + console.log("Project Alpha selected"), + }, + { + key: "project-2", + title: "Project Beta", + icon: Briefcase, + + // disabled: true, + action: () => console.log("Project Beta selected"), + }, + { + key: "project-3", + title: "Project Gamma", + icon: Briefcase, + + action: () => console.log("Project Gamma selected"), + }, + ]} + /> + } + showSeparator={false} + />, + console.log("Feature Alpha selected"), + }, + { + key: "feature-2", + title: "Work items", + icon: LayersIcon, + + // disabled: true, + action: () => console.log("Feature Beta selected"), + }, + { + key: "feature-3", + title: "Cycles", + icon: ContrastIcon, + + action: () => console.log("Feature Gamma selected"), + }, + { + key: "feature-3", + title: "Modules", + icon: GridIcon, + + action: () => console.log("Feature Gamma selected"), + }, + { + key: "feature-3", + title: "Views", + icon: Layers2, + + action: () => console.log("Feature Gamma selected"), + }, + { + key: "feature-3", + title: "Pages", + icon: FileIcon, + + action: () => console.log("Feature Gamma selected"), + }, + ]} + /> + } + showSeparator={false} + />, + } />} + isLast + />, + ], + }, +}; diff --git a/packages/ui/src/breadcrumbs/breadcrumbs.tsx b/packages/ui/src/breadcrumbs/breadcrumbs.tsx index 031825691..af0ba9b4f 100644 --- a/packages/ui/src/breadcrumbs/breadcrumbs.tsx +++ b/packages/ui/src/breadcrumbs/breadcrumbs.tsx @@ -1,13 +1,25 @@ -import * as React from "react"; import { ChevronRight } from "lucide-react"; +import * as React from "react"; +import { cn } from "../../helpers"; +import { Tooltip } from "../tooltip"; type BreadcrumbsProps = { + className?: string; children: React.ReactNode; onBack?: () => void; isLoading?: boolean; }; -const Breadcrumbs = ({ children, onBack, isLoading = false }: BreadcrumbsProps) => { +export const BreadcrumbItemLoader = () => ( +
+
+ + +
+
+); + +const Breadcrumbs = ({ className, children, onBack, isLoading = false }: BreadcrumbsProps) => { const [isSmallScreen, setIsSmallScreen] = React.useState(false); React.useEffect(() => { @@ -22,35 +34,31 @@ const Breadcrumbs = ({ children, onBack, isLoading = false }: BreadcrumbsProps) const childrenArray = React.Children.toArray(children); - const BreadcrumbItemLoader = ( -
- - -
- ); - return ( -
+
{!isSmallScreen && ( <> - {childrenArray.map((child, index) => ( - - {index > 0 && !isSmallScreen && ( -
-
- )} -
0 ? "hidden sm:flex" : "flex"}`}> - {isLoading ? BreadcrumbItemLoader : child} -
-
- ))} + {childrenArray.map((child, index) => { + if (isLoading) { + return ( + <> + + + ); + } + if (React.isValidElement(child)) { + return React.cloneElement(child, { + isLast: index === childrenArray.length - 1, + }); + } + return child; + })} )} {isSmallScreen && childrenArray.length > 1 && ( <> -
+
{onBack && ( ... @@ -58,8 +66,16 @@ const Breadcrumbs = ({ children, onBack, isLoading = false }: BreadcrumbsProps) )}
-
- {isLoading ? BreadcrumbItemLoader : childrenArray[childrenArray.length - 1]} +
+ {isLoading ? ( + + ) : React.isValidElement(childrenArray[childrenArray.length - 1]) ? ( + React.cloneElement(childrenArray[childrenArray.length - 1] as React.ReactElement, { + isLast: true, + }) + ) : ( + childrenArray[childrenArray.length - 1] + )}
)} @@ -68,17 +84,107 @@ const Breadcrumbs = ({ children, onBack, isLoading = false }: BreadcrumbsProps) ); }; -type Props = { - type?: "text" | "component"; +// breadcrumb item +type BreadcrumbItemProps = { component?: React.ReactNode; - link?: JSX.Element; + showSeparator?: boolean; + isLast?: boolean; }; -const BreadcrumbItem: React.FC = (props) => { - const { type = "text", component, link } = props; - return <>{type !== "text" ?
{component}
: link}; +const BreadcrumbItem: React.FC = (props) => { + const { component, showSeparator = true, isLast = false } = props; + return ( +
+ {component} + {showSeparator && !isLast && } +
+ ); }; -Breadcrumbs.BreadcrumbItem = BreadcrumbItem; +// breadcrumb icon +type BreadcrumbIconProps = { + children: React.ReactNode; + className?: string; +}; -export { Breadcrumbs, BreadcrumbItem }; +const BreadcrumbIcon: React.FC = (props) => { + const { children, className } = props; + return
{children}
; +}; + +// breadcrumb label +type BreadcrumbLabelProps = { + children: React.ReactNode; + className?: string; +}; + +const BreadcrumbLabel: React.FC = (props) => { + const { children, className } = props; + return ( +
+ {children} +
+ ); +}; + +// breadcrumb separator +type BreadcrumbSeparatorProps = { + className?: string; + containerClassName?: string; + iconClassName?: string; + showDivider?: boolean; +}; + +const BreadcrumbSeparator: React.FC = (props) => { + const { className, containerClassName, iconClassName, showDivider = false } = props; + return ( +
+ {showDivider && } +
+ +
+
+ ); +}; + +// breadcrumb wrapper +type BreadcrumbItemWrapperProps = { + label?: string; + disableTooltip?: boolean; + children: React.ReactNode; + className?: string; + type?: "link" | "text"; + isLast?: boolean; +}; + +const BreadcrumbItemWrapper: React.FC = (props) => { + const { label, disableTooltip = false, children, className, type = "link", isLast = false } = props; + return ( + +
+ {children} +
+
+ ); +}; + +Breadcrumbs.Item = BreadcrumbItem; +Breadcrumbs.Icon = BreadcrumbIcon; +Breadcrumbs.Label = BreadcrumbLabel; +Breadcrumbs.Separator = BreadcrumbSeparator; +Breadcrumbs.ItemWrapper = BreadcrumbItemWrapper; + +export { Breadcrumbs, BreadcrumbItem, BreadcrumbIcon, BreadcrumbLabel, BreadcrumbSeparator, BreadcrumbItemWrapper }; diff --git a/packages/ui/src/breadcrumbs/index.ts b/packages/ui/src/breadcrumbs/index.ts index 05a8bdbf1..192bd5751 100644 --- a/packages/ui/src/breadcrumbs/index.ts +++ b/packages/ui/src/breadcrumbs/index.ts @@ -1,2 +1,3 @@ export * from "./breadcrumbs"; export * from "./navigation-dropdown"; +export * from "./navigation-search-dropdown"; diff --git a/packages/ui/src/breadcrumbs/navigation-dropdown.tsx b/packages/ui/src/breadcrumbs/navigation-dropdown.tsx index a716ca65e..503e13eb2 100644 --- a/packages/ui/src/breadcrumbs/navigation-dropdown.tsx +++ b/packages/ui/src/breadcrumbs/navigation-dropdown.tsx @@ -1,42 +1,54 @@ "use client"; +import { CheckIcon } from "lucide-react"; import * as React from "react"; -import { CheckIcon, ChevronDownIcon } from "lucide-react"; +import { cn } from "../../helpers"; // ui import { CustomMenu, TContextMenuItem } from "../dropdowns"; -// helpers -import { cn } from "../../helpers"; +import { Tooltip } from "../tooltip"; +import { Breadcrumbs } from "./breadcrumbs"; type TBreadcrumbNavigationDropdownProps = { selectedItemKey: string; navigationItems: TContextMenuItem[]; navigationDisabled?: boolean; + handleOnClick?: () => void; + isLast?: boolean; }; export const BreadcrumbNavigationDropdown = (props: TBreadcrumbNavigationDropdownProps) => { - const { selectedItemKey, navigationItems, navigationDisabled = false } = props; + const { selectedItemKey, navigationItems, navigationDisabled = false, handleOnClick, isLast = false } = props; + const [isOpen, setIsOpen] = React.useState(false); // derived values const selectedItem = navigationItems.find((item) => item.key === selectedItemKey); const selectedItemIcon = selectedItem?.icon ? ( - + ) : undefined; // if no selected item, return null if (!selectedItem) return null; - const NavigationButton = ({ className }: { className?: string }) => ( -
  • - {selectedItemIcon && ( -
    {selectedItemIcon}
    - )} -
    {selectedItem.title}
    -
  • + const NavigationButton = () => ( + + + ); if (navigationDisabled) { @@ -46,13 +58,37 @@ export const BreadcrumbNavigationDropdown = (props: TBreadcrumbNavigationDropdow return ( - - -
    + <> + + + } placement="bottom-start" + className="h-full rounded" + customButtonClassName={cn( + "group flex items-center gap-0.5 rounded hover:bg-custom-background-90 outline-none cursor-pointer h-full rounded", + { + "bg-custom-background-90": isOpen, + } + )} closeOnSelect + menuButtonOnClick={() => { + setIsOpen(!isOpen); + }} + onMenuClose={() => { + setIsOpen(false); + }} > {navigationItems.map((item) => { if (item.shouldRender === false) return null; @@ -74,7 +110,7 @@ export const BreadcrumbNavigationDropdown = (props: TBreadcrumbNavigationDropdow )} disabled={item.disabled} > - {item.icon && } + {item.icon && }
    {item.title}
    {item.description && ( diff --git a/packages/ui/src/breadcrumbs/navigation-search-dropdown.tsx b/packages/ui/src/breadcrumbs/navigation-search-dropdown.tsx new file mode 100644 index 000000000..0439d1d33 --- /dev/null +++ b/packages/ui/src/breadcrumbs/navigation-search-dropdown.tsx @@ -0,0 +1,96 @@ +import * as React from "react"; +import { useState } from "react"; +import { ICustomSearchSelectOption } from "@plane/types"; +import { cn } from "../../helpers"; +import { CustomSearchSelect } from "../dropdowns"; +import { Tooltip } from "../tooltip"; +import { Breadcrumbs } from "./breadcrumbs"; + +type TBreadcrumbNavigationSearchDropdownProps = { + icon?: React.JSX.Element; + title?: string; + selectedItem: string; + navigationItems: ICustomSearchSelectOption[]; + onChange?: (value: string) => void; + navigationDisabled?: boolean; + isLast?: boolean; + handleOnClick?: () => void; + disableRootHover?: boolean; +}; + +export const BreadcrumbNavigationSearchDropdown: React.FC = (props) => { + const { + icon, + title, + selectedItem, + navigationItems, + onChange, + navigationDisabled = false, + isLast = false, + handleOnClick, + } = props; + // state + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + + return ( + { + setIsDropdownOpen(true); + }} + onClose={() => { + setIsDropdownOpen(false); + }} + options={navigationItems} + value={selectedItem} + onChange={(value: string) => { + if (value !== selectedItem) { + onChange?.(value); + } + }} + customButton={ + <> + + + + + + } + disabled={navigationDisabled} + className="h-full rounded" + customButtonClassName={cn( + "group flex items-center gap-0.5 rounded hover:bg-custom-background-90 outline-none cursor-pointer h-full rounded", + { + "bg-custom-background-90": isDropdownOpen, + } + )} + /> + ); +}; diff --git a/packages/ui/src/dropdowns/custom-search-select.tsx b/packages/ui/src/dropdowns/custom-search-select.tsx index e592f0dc2..d26163e69 100644 --- a/packages/ui/src/dropdowns/custom-search-select.tsx +++ b/packages/ui/src/dropdowns/custom-search-select.tsx @@ -61,6 +61,7 @@ export const CustomSearchSelect = (props: ICustomSearchSelectProps) => { const openDropdown = () => { setIsOpen(true); if (referenceElement) referenceElement.focus(); + if (onOpen) onOpen(); }; const closeDropdown = () => { @@ -95,11 +96,14 @@ export const CustomSearchSelect = (props: ICustomSearchSelectProps) => { +
    + {viewDetails && } + {isDefaultView && defaultViewDetails && ( + + )} +
    diff --git a/web/ce/components/breadcrumbs/common.tsx b/web/ce/components/breadcrumbs/common.tsx new file mode 100644 index 000000000..5b2f573cb --- /dev/null +++ b/web/ce/components/breadcrumbs/common.tsx @@ -0,0 +1,32 @@ +"use client"; + +import { FC } from "react"; +// plane imports +import { EProjectFeatureKey } from "@plane/constants"; +// local components +import { ProjectFeatureBreadcrumb } from "./project-feature"; +import { ProjectBreadcrumb } from "./project"; + +type TCommonProjectBreadcrumbProps = { + workspaceSlug: string; + projectId: string; + featureKey?: EProjectFeatureKey; + isLast?: boolean; +}; + +export const CommonProjectBreadcrumbs: FC = (props) => { + const { workspaceSlug, projectId, featureKey, isLast = false } = props; + return ( + <> + + {featureKey && ( + + )} + + ); +}; diff --git a/web/ce/components/breadcrumbs/index.ts b/web/ce/components/breadcrumbs/index.ts index 9ff8c7dff..aad2cb352 100644 --- a/web/ce/components/breadcrumbs/index.ts +++ b/web/ce/components/breadcrumbs/index.ts @@ -1 +1,3 @@ +export * from "./common"; +export * from "./project-feature"; export * from "./project"; diff --git a/web/ce/components/breadcrumbs/project-feature.tsx b/web/ce/components/breadcrumbs/project-feature.tsx new file mode 100644 index 000000000..c606a2d3f --- /dev/null +++ b/web/ce/components/breadcrumbs/project-feature.tsx @@ -0,0 +1,69 @@ +"use client"; + +import { FC } from "react"; +import { observer } from "mobx-react"; +// ui +import { EProjectFeatureKey } from "@plane/constants"; +import { BreadcrumbNavigationDropdown, Breadcrumbs, ISvgIcons } from "@plane/ui"; +// components +import { SwitcherLabel } from "@/components/common"; +import { TNavigationItem } from "@/components/workspace"; +// hooks +import { useProject } from "@/hooks/store"; +import { useAppRouter } from "@/hooks/use-app-router"; +// local components +import { getProjectFeatureNavigation } from "../projects/navigation"; + +type TProjectFeatureBreadcrumbProps = { + workspaceSlug: string; + projectId: string; + featureKey: EProjectFeatureKey; + isLast?: boolean; + additionalNavigationItems?: TNavigationItem[]; +}; + +export const ProjectFeatureBreadcrumb = observer((props: TProjectFeatureBreadcrumbProps) => { + const { workspaceSlug, projectId, featureKey, isLast = false, additionalNavigationItems } = props; + // router + const router = useAppRouter(); + // store hooks + const { getPartialProjectById } = useProject(); + // derived values + const project = getPartialProjectById(projectId); + + if (!project) return null; + + const navigationItems = getProjectFeatureNavigation(workspaceSlug, projectId, project); + + // if additional navigation items are provided, add them to the navigation items + const allNavigationItems = [...(additionalNavigationItems || []), ...navigationItems]; + + return ( + <> + item.shouldRender) + .map((item) => ({ + key: item.key, + title: item.name, + customContent: } />, + action: () => router.push(item.href), + icon: item.icon as FC, + }))} + handleOnClick={() => { + router.push( + `/${workspaceSlug}/projects/${projectId}/${featureKey === EProjectFeatureKey.WORK_ITEMS ? "issues" : featureKey}/` + ); + }} + isLast={isLast} + /> + } + showSeparator={false} + isLast={isLast} + /> + + ); +}); diff --git a/web/ce/components/breadcrumbs/project.tsx b/web/ce/components/breadcrumbs/project.tsx index 3b49bb211..e59f948df 100644 --- a/web/ce/components/breadcrumbs/project.tsx +++ b/web/ce/components/breadcrumbs/project.tsx @@ -2,38 +2,73 @@ import { observer } from "mobx-react"; import { Briefcase } from "lucide-react"; -// ui -import { Breadcrumbs, Logo } from "@plane/ui"; +// plane imports +import { ICustomSearchSelectOption } from "@plane/types"; +import { BreadcrumbNavigationSearchDropdown, Breadcrumbs, Logo } from "@plane/ui"; // components -import { BreadcrumbLink } from "@/components/common"; +import { SwitcherLabel } from "@/components/common"; // hooks import { useProject } from "@/hooks/store"; +import { useAppRouter } from "@/hooks/use-app-router"; +import { TProject } from "@/plane-web/types"; -export const ProjectBreadcrumb = observer(() => { +type TProjectBreadcrumbProps = { + workspaceSlug: string; + projectId: string; + handleOnClick?: () => void; +}; + +export const ProjectBreadcrumb = observer((props: TProjectBreadcrumbProps) => { + const { workspaceSlug, projectId, handleOnClick } = props; + // router + const router = useAppRouter(); // store hooks - const { currentProjectDetails } = useProject(); + const { joinedProjectIds, getPartialProjectById } = useProject(); + const currentProjectDetails = getPartialProjectById(projectId); + + // store hooks + + if (!currentProjectDetails) return null; + + // derived values + const switcherOptions = joinedProjectIds + .map((projectId) => { + const project = getPartialProjectById(projectId); + return { + value: projectId, + query: project?.name, + content: , + }; + }) + .filter((option) => option !== undefined) as ICustomSearchSelectOption[]; + + // helpers + const renderIcon = (projectDetails: TProject) => ( + + + + ); return ( - - - - ) - ) : ( - - - - ) - } - /> - } - /> + <> + { + router.push(`/${workspaceSlug}/projects/${value}/issues`); + }} + title={currentProjectDetails?.name} + icon={renderIcon(currentProjectDetails)} + handleOnClick={() => { + if (handleOnClick) handleOnClick(); + else router.push(`/${workspaceSlug}/projects/${currentProjectDetails.id}/issues/`); + }} + /> + } + showSeparator={false} + /> + ); }); diff --git a/web/ce/components/issues/header.tsx b/web/ce/components/issues/header.tsx index 7e7073cd9..3250ad4d5 100644 --- a/web/ce/components/issues/header.tsx +++ b/web/ce/components/issues/header.tsx @@ -4,13 +4,20 @@ import { observer } from "mobx-react"; import { useParams } from "next/navigation"; // icons import { Circle, ExternalLink } from "lucide-react"; -import { EIssuesStoreType, EUserPermissions, EUserPermissionsLevel, SPACE_BASE_PATH, SPACE_BASE_URL } from "@plane/constants"; +import { + EIssuesStoreType, + EProjectFeatureKey, + EUserPermissions, + EUserPermissionsLevel, + SPACE_BASE_PATH, + SPACE_BASE_URL, +} from "@plane/constants"; // plane constants import { useTranslation } from "@plane/i18n"; // ui -import { Breadcrumbs, Button, LayersIcon, Tooltip, Header } from "@plane/ui"; +import { Breadcrumbs, Button, Tooltip, Header } from "@plane/ui"; // components -import { BreadcrumbLink, CountChip } from "@/components/common"; +import { CountChip } from "@/components/common"; // constants import HeaderFilters from "@/components/issues/filters"; // helpers @@ -20,7 +27,7 @@ import { useIssues } from "@/hooks/store/use-issues"; import { useAppRouter } from "@/hooks/use-app-router"; import { usePlatformOS } from "@/hooks/use-platform-os"; // plane web -import { ProjectBreadcrumb } from "@/plane-web/components/breadcrumbs"; +import { CommonProjectBreadcrumbs } from "../breadcrumbs/common"; export const IssuesHeader = observer(() => { // router @@ -52,18 +59,13 @@ export const IssuesHeader = observer(() => { return (
    -
    - router.back()} isLoading={loader === "init-loader"}> - - - } - /> - } +
    + router.back()} isLoading={loader === "init-loader"} className="flex-grow-0"> + {issuesCount && issuesCount > 0 ? ( diff --git a/web/ce/components/projects/navigation/helper.tsx b/web/ce/components/projects/navigation/helper.tsx new file mode 100644 index 000000000..1a99262c6 --- /dev/null +++ b/web/ce/components/projects/navigation/helper.tsx @@ -0,0 +1,77 @@ +import { FileText, Layers } from "lucide-react"; +import { EUserPermissions, EProjectFeatureKey } from "@plane/constants"; +import { ContrastIcon, DiceIcon, Intake, LayersIcon } from "@plane/ui"; +import { TNavigationItem } from "@/components/workspace"; + +export const getProjectFeatureNavigation = ( + workspaceSlug: string, + projectId: string, + project: { + cycle_view: boolean; + module_view: boolean; + issue_views_view: boolean; + page_view: boolean; + inbox_view: boolean; + } +): TNavigationItem[] => [ + { + i18n_key: "sidebar.work_items", + key: EProjectFeatureKey.WORK_ITEMS, + name: "Work items", + href: `/${workspaceSlug}/projects/${projectId}/issues`, + icon: LayersIcon, + access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST], + shouldRender: true, + sortOrder: 1, + }, + { + i18n_key: "sidebar.cycles", + key: EProjectFeatureKey.CYCLES, + name: "Cycles", + href: `/${workspaceSlug}/projects/${projectId}/cycles`, + icon: ContrastIcon, + access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + shouldRender: project.cycle_view, + sortOrder: 2, + }, + { + i18n_key: "sidebar.modules", + key: EProjectFeatureKey.MODULES, + name: "Modules", + href: `/${workspaceSlug}/projects/${projectId}/modules`, + icon: DiceIcon, + access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + shouldRender: project.module_view, + sortOrder: 3, + }, + { + i18n_key: "sidebar.views", + key: EProjectFeatureKey.VIEWS, + name: "Views", + href: `/${workspaceSlug}/projects/${projectId}/views`, + icon: Layers, + access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST], + shouldRender: project.issue_views_view, + sortOrder: 4, + }, + { + i18n_key: "sidebar.pages", + key: EProjectFeatureKey.PAGES, + name: "Pages", + href: `/${workspaceSlug}/projects/${projectId}/pages`, + icon: FileText, + access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST], + shouldRender: project.page_view, + sortOrder: 5, + }, + { + i18n_key: "sidebar.intake", + key: EProjectFeatureKey.INTAKE, + name: "Intake", + href: `/${workspaceSlug}/projects/${projectId}/intake`, + icon: Intake, + access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST], + shouldRender: project.inbox_view, + sortOrder: 6, + }, +]; diff --git a/web/ce/components/projects/navigation/index.ts b/web/ce/components/projects/navigation/index.ts new file mode 100644 index 000000000..b9755e783 --- /dev/null +++ b/web/ce/components/projects/navigation/index.ts @@ -0,0 +1 @@ +export * from "./helper"; diff --git a/web/ce/components/projects/settings/intake/header.tsx b/web/ce/components/projects/settings/intake/header.tsx index 32a93894f..9b0c994b5 100644 --- a/web/ce/components/projects/settings/intake/header.tsx +++ b/web/ce/components/projects/settings/intake/header.tsx @@ -5,16 +5,15 @@ import { observer } from "mobx-react"; import { useParams } from "next/navigation"; import { RefreshCcw } from "lucide-react"; // ui -import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; +import { EProjectFeatureKey, EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; -import { Breadcrumbs, Button, Intake, Header } from "@plane/ui"; +import { Breadcrumbs, Button, Header } from "@plane/ui"; // components -import { BreadcrumbLink } from "@/components/common"; import { InboxIssueCreateModalRoot } from "@/components/inbox"; // hooks import { useProject, useProjectInbox, useUserPermissions } from "@/hooks/store"; // plane web -import { ProjectBreadcrumb } from "@/plane-web/components/breadcrumbs"; +import { CommonProjectBreadcrumbs } from "@/plane-web/components/breadcrumbs"; export const ProjectInboxHeader: FC = observer(() => { // states @@ -37,13 +36,13 @@ export const ProjectInboxHeader: FC = observer(() => { return (
    -
    +
    - - - } />} + diff --git a/web/core/components/common/breadcrumb-link.tsx b/web/core/components/common/breadcrumb-link.tsx index bf421a50c..1dc85e3e2 100644 --- a/web/core/components/common/breadcrumb-link.tsx +++ b/web/core/components/common/breadcrumb-link.tsx @@ -1,44 +1,75 @@ "use client"; -import { ReactNode } from "react"; +import React, { ReactNode, useMemo, FC } from "react"; +import { observer } from "mobx-react-lite"; import Link from "next/link"; -import { Tooltip } from "@plane/ui"; +import { Breadcrumbs } from "@plane/ui"; import { usePlatformOS } from "@/hooks/use-platform-os"; type Props = { - label?: string | ReactNode; + label?: string; href?: string; - icon?: React.ReactNode | undefined; + icon?: React.ReactNode; disableTooltip?: boolean; + isLast?: boolean; }; -export const BreadcrumbLink: React.FC = (props) => { - const { href, label, icon, disableTooltip = false } = props; - const { isMobile } = usePlatformOS(); +const IconWrapper = React.memo(({ icon }: { icon: React.ReactNode }) => ( +
    {icon}
    +)); + +IconWrapper.displayName = "IconWrapper"; + +const LabelWrapper = React.memo(({ label }: { label: ReactNode }) => ( +
    {label}
    +)); + +LabelWrapper.displayName = "LabelWrapper"; + +const BreadcrumbContent = React.memo(({ icon, label }: { icon?: React.ReactNode; label?: ReactNode }) => { + if (!icon && !label) return null; + return ( - -
  • -
    - {href ? ( - - {icon && ( -
    {icon}
    - )} - {label && ( -
    {label}
    - )} - - ) : ( -
    - {icon &&
    {icon}
    } -
    {label}
    -
    - )} -
    -
  • -
    + <> + {icon && } + {label && } + ); -}; +}); + +BreadcrumbContent.displayName = "BreadcrumbContent"; + +const ItemWrapper = React.memo(({ children, ...props }: React.ComponentProps) => ( + {children} +)); + +ItemWrapper.displayName = "ItemWrapper"; + +export const BreadcrumbLink: FC = observer((props) => { + const { href, label, icon, disableTooltip = false, isLast = false } = props; + const { isMobile } = usePlatformOS(); + + const itemWrapperProps = useMemo( + () => ({ + label: label?.toString(), + disableTooltip: isMobile || disableTooltip, + type: (href && href !== "" ? "link" : "text") as "link" | "text", + isLast, + }), + [href, label, isMobile, disableTooltip, isLast] + ); + + const content = useMemo(() => , [icon, label]); + + if (href) { + return ( + + {content} + + ); + } + + return {content}; +}); + +BreadcrumbLink.displayName = "BreadcrumbLink"; diff --git a/web/core/components/common/switcher-label.tsx b/web/core/components/common/switcher-label.tsx index 6bb4c346f..d1f1fc4bc 100644 --- a/web/core/components/common/switcher-label.tsx +++ b/web/core/components/common/switcher-label.tsx @@ -2,6 +2,32 @@ import { FC } from "react"; import { TLogoProps } from "@plane/types"; import { ISvgIcons, Logo } from "@plane/ui"; import { getFileURL, truncateText } from "@plane/utils"; + +type TSwitcherIconProps = { + logo_props?: TLogoProps; + logo_url?: string; + LabelIcon: FC; + size?: number; +}; + +export const SwitcherIcon: FC = ({ logo_props, logo_url, LabelIcon, size = 12 }) => { + if (logo_props?.in_use) { + return ; + } + + if (logo_url) { + return ( + logo + ); + } + return ; +}; + type TSwitcherLabelProps = { logo_props?: TLogoProps; logo_url?: string; @@ -13,13 +39,7 @@ export const SwitcherLabel: FC = (props) => { const { logo_props, name, LabelIcon, logo_url } = props; return (
    - {logo_props?.in_use ? ( - - ) : logo_url ? ( - logo - ) : ( - - )} + {truncateText(name ?? "", 40)}
    ); diff --git a/web/core/components/project/header.tsx b/web/core/components/project/header.tsx index 17adc130e..e40280373 100644 --- a/web/core/components/project/header.tsx +++ b/web/core/components/project/header.tsx @@ -37,16 +37,15 @@ export const ProjectsBaseHeader = observer(() => {
    - } /> } /> - {isArchived && } />} + {isArchived && } />} diff --git a/web/core/components/workspace-notifications/sidebar/header/root.tsx b/web/core/components/workspace-notifications/sidebar/header/root.tsx index cdda51fbf..7ed9ea528 100644 --- a/web/core/components/workspace-notifications/sidebar/header/root.tsx +++ b/web/core/components/workspace-notifications/sidebar/header/root.tsx @@ -26,9 +26,8 @@ export const NotificationSidebarHeader: FC = observe
    - } diff --git a/web/core/components/workspace/views/default-view-quick-action.tsx b/web/core/components/workspace/views/default-view-quick-action.tsx index e7f0e276a..869c2d096 100644 --- a/web/core/components/workspace/views/default-view-quick-action.tsx +++ b/web/core/components/workspace/views/default-view-quick-action.tsx @@ -1,19 +1,16 @@ "use client"; import { observer } from "mobx-react"; -import Link from "next/link"; import { ExternalLink, LinkIcon } from "lucide-react"; // plane imports import { useTranslation } from "@plane/i18n"; // ui import { TStaticViewTypes } from "@plane/types"; -import { ContextMenu, CustomMenu, TContextMenuItem, TOAST_TYPE, setToast } from "@plane/ui"; +import { CustomMenu, TContextMenuItem, TOAST_TYPE, setToast } from "@plane/ui"; import { copyUrlToClipboard, cn } from "@plane/utils"; // helpers type Props = { - parentRef: React.RefObject; workspaceSlug: string; - globalViewId: string | undefined; view: { key: TStaticViewTypes; i18n_label: string; @@ -21,7 +18,7 @@ type Props = { }; export const DefaultWorkspaceViewQuickActions: React.FC = observer((props) => { - const { parentRef, globalViewId, view, workspaceSlug } = props; + const { workspaceSlug, view } = props; const { t } = useTranslation(); @@ -53,43 +50,11 @@ export const DefaultWorkspaceViewQuickActions: React.FC = observer((props return ( <> - - - {view.key === globalViewId ? ( - - {t(view.i18n_label)} - - ) : ( - - - {t(view.i18n_label)} - - - )} - - } + ellipsis placement="bottom-end" - menuItemsClassName="z-20" closeOnSelect + buttonClassName="flex-shrink-0 flex items-center justify-center size-[26px] bg-custom-background-80/70 rounded" > {MENU_ITEMS.map((item) => { if (item.shouldRender === false) return null; diff --git a/web/core/components/workspace/views/header.tsx b/web/core/components/workspace/views/header.tsx index f449c553b..e73129800 100644 --- a/web/core/components/workspace/views/header.tsx +++ b/web/core/components/workspace/views/header.tsx @@ -37,13 +37,7 @@ const ViewTab = observer((props: { viewId: string }) => { return (
    - +
    ); }); @@ -63,12 +57,7 @@ const DefaultViewTab = (props: { if (!workspaceSlug || !globalViewId) return null; return (
    - +
    ); }; diff --git a/web/core/components/workspace/views/quick-action.tsx b/web/core/components/workspace/views/quick-action.tsx index e0950a78a..4db3d537c 100644 --- a/web/core/components/workspace/views/quick-action.tsx +++ b/web/core/components/workspace/views/quick-action.tsx @@ -2,13 +2,12 @@ import { useState } from "react"; import { observer } from "mobx-react"; -import Link from "next/link"; -import { ExternalLink, LinkIcon, Pencil, Trash2, Lock } from "lucide-react"; +import { ExternalLink, LinkIcon, Pencil, Trash2 } from "lucide-react"; // types -import { EViewAccess, EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; +import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { IWorkspaceView } from "@plane/types"; -import { ContextMenu, TContextMenuItem, TOAST_TYPE, setToast } from "@plane/ui"; +import { CustomMenu, TContextMenuItem, TOAST_TYPE, setToast } from "@plane/ui"; import { copyUrlToClipboard, cn } from "@plane/utils"; // components import { CreateUpdateWorkspaceViewModal, DeleteGlobalViewModal } from "@/components/workspace"; @@ -18,15 +17,12 @@ import { CreateUpdateWorkspaceViewModal, DeleteGlobalViewModal } from "@/compone import { useUser, useUserPermissions } from "@/hooks/store"; type Props = { - parentRef: React.RefObject; workspaceSlug: string; - globalViewId: string; - viewId: string; view: IWorkspaceView; }; export const WorkspaceViewQuickActions: React.FC = observer((props) => { - const { parentRef, view, globalViewId, viewId, workspaceSlug } = props; + const { workspaceSlug, view } = props; // states const [updateViewModal, setUpdateViewModal] = useState(false); const [deleteViewModal, setDeleteViewModal] = useState(false); @@ -78,42 +74,53 @@ export const WorkspaceViewQuickActions: React.FC = observer((props) => { }, ]; - const isSelected = viewId === globalViewId; - const isPrivateView = view.access === EViewAccess.PRIVATE; - - let customButton = ( -
    - - {view.name} - - {isPrivateView && ( - - )} -
    - ); - - if (!isSelected) { - customButton = ( - - {customButton} - - ); - } - return ( <> setUpdateViewModal(false)} /> setDeleteViewModal(false)} /> - - - {customButton} + + {MENU_ITEMS.map((item) => { + if (item.shouldRender === false) return null; + return ( + { + e.preventDefault(); + e.stopPropagation(); + item.action(); + }} + className={cn( + "flex items-center gap-2", + { + "text-custom-text-400": item.disabled, + }, + item.className + )} + disabled={item.disabled} + > + {item.icon && } +
    +
    {item.title}
    + {item.description && ( +

    + {item.description} +

    + )} +
    +
    + ); + })} +
    ); }); From 24e57009af2dc54702d2ca0bc858b6c3f0f3ec39 Mon Sep 17 00:00:00 2001 From: Vipin Chaudhary Date: Thu, 19 Jun 2025 17:17:56 +0530 Subject: [PATCH 180/201] [WIKI-465] fix : Add new node on click of doc end (#7063) * fix : handle last node * fix : handle unexpected node * remove logs * feat: handle focus --------- Co-authored-by: M. Palanikannan <73993394+Palanikannan1437@users.noreply.github.com> --- .../components/editors/editor-container.tsx | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/packages/editor/src/core/components/editors/editor-container.tsx b/packages/editor/src/core/components/editors/editor-container.tsx index 6daa0719a..3553f07fd 100644 --- a/packages/editor/src/core/components/editors/editor-container.tsx +++ b/packages/editor/src/core/components/editors/editor-container.tsx @@ -53,17 +53,14 @@ export const EditorContainer: FC = (props) => { const lastNodePos = editor.state.doc.resolve(Math.max(0, docSize - 2)); const lastNode = lastNodePos.node(); - // Check if the last node is a not paragraph - if (lastNode && lastNode.type.name !== CORE_EXTENSIONS.PARAGRAPH) { - // If last node is not a paragraph, insert a new paragraph at the end - const endPosition = editor?.state.doc.content.size; - editor?.chain().insertContentAt(endPosition, { type: CORE_EXTENSIONS.PARAGRAPH }).run(); - - // Focus the newly added paragraph for immediate editing - editor - .chain() - .setTextSelection(endPosition + 1) - .run(); + // Check if its last node and add new node + if (lastNode) { + const isLastNodeEmptyParagraph = lastNode.type.name === CORE_EXTENSIONS.PARAGRAPH && lastNode.content.size === 0; + // Only insert a new paragraph if the last node is not an empty paragraph and not a doc node + if (!isLastNodeEmptyParagraph && lastNode.type.name !== "doc") { + const endPosition = editor?.state.doc.content.size; + editor?.chain().insertContentAt(endPosition, { type: "paragraph" }).focus("end").run(); + } } } catch (error) { console.error("An error occurred while handling container click to insert new empty node at bottom:", error); From c3c1aef7a9ccd069f1c559e37dd3558aa9a274f4 Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Thu, 19 Jun 2025 19:09:59 +0530 Subject: [PATCH 181/201] [WEB-4357] fix: remove trailing slash from asset url #7240 --- packages/utils/src/file.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/utils/src/file.ts b/packages/utils/src/file.ts index d119ecc18..225c574c1 100644 --- a/packages/utils/src/file.ts +++ b/packages/utils/src/file.ts @@ -44,6 +44,8 @@ export const getFileMetaDataForUpload = (file: File): TFileMetaDataLite => ({ * @returns {string} assetId */ export const getAssetIdFromUrl = (src: string): string => { + // remove the last char if it is a slash + if (src.charAt(src.length - 1) === "/") src = src.slice(0, -1); const sourcePaths = src.split("/"); const assetUrl = sourcePaths[sourcePaths.length - 1]; return assetUrl; From f26b4d3d068a7232be1a1cfb5d83b9b5d5c837e4 Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Fri, 20 Jun 2025 15:16:16 +0530 Subject: [PATCH 182/201] [WEB-4359] fix: application crash when creating work item via quick add (#7245) --- packages/utils/src/work-item/base.ts | 5 +++++ .../issues/issue-layouts/properties/all-properties.tsx | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/utils/src/work-item/base.ts b/packages/utils/src/work-item/base.ts index 6a7068928..7ebca1ffd 100644 --- a/packages/utils/src/work-item/base.ts +++ b/packages/utils/src/work-item/base.ts @@ -146,6 +146,11 @@ export const createIssuePayload: (projectId: string, formData: Partial) id: uuidv4(), project_id: projectId, priority: "none", + label_ids: [], + assignee_ids: [], + sub_issues_count: 0, + attachment_count: 0, + link_count: 0, // tempId is used for optimistic updates. It is not a part of the API response. tempId: uuidv4(), // to be overridden by the form data diff --git a/web/core/components/issues/issue-layouts/properties/all-properties.tsx b/web/core/components/issues/issue-layouts/properties/all-properties.tsx index 877aa85d9..3b7e5c90b 100644 --- a/web/core/components/issues/issue-layouts/properties/all-properties.tsx +++ b/web/core/components/issues/issue-layouts/properties/all-properties.tsx @@ -503,7 +503,7 @@ export const IssueProperties: React.FC = observer((props) => { Date: Fri, 20 Jun 2025 17:24:49 +0530 Subject: [PATCH 183/201] [WEB-4361] fix: add onChange to collaborative editor #7246 --- packages/editor/src/core/hooks/use-collaborative-editor.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/editor/src/core/hooks/use-collaborative-editor.ts b/packages/editor/src/core/hooks/use-collaborative-editor.ts index 570315d75..9c436dff2 100644 --- a/packages/editor/src/core/hooks/use-collaborative-editor.ts +++ b/packages/editor/src/core/hooks/use-collaborative-editor.ts @@ -13,6 +13,7 @@ import { TCollaborativeEditorHookProps } from "@/types"; export const useCollaborativeEditor = (props: TCollaborativeEditorHookProps) => { const { + onChange, onTransaction, disabledExtensions, editable, @@ -105,6 +106,7 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorHookProps) => forwardedRef, handleEditorReady, mentionHandler, + onChange, onTransaction, placeholder, provider, From c1fa372c8403e340c3bfee6ea25d70fd46fbab56 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Tue, 24 Jun 2025 14:05:11 +0530 Subject: [PATCH 184/201] [WIKI-471] refactor: custom image extension (#7247) * refactor: custom image extension * refactor: extension config * revert: image full screen component * fix: undo operation --- packages/editor/src/ce/types/storage.ts | 2 +- .../src/core/extensions/core-without-props.ts | 12 +- .../components/{image-block.tsx => block.tsx} | 80 +++----- .../custom-image/components/index.ts | 4 - .../{image-node.tsx => node-view.tsx} | 47 +++-- .../components/toolbar/full-screen.tsx | 6 +- .../custom-image/components/toolbar/root.tsx | 8 +- .../{image-uploader.tsx => uploader.tsx} | 24 +-- .../extensions/custom-image/custom-image.ts | 180 ------------------ .../custom-image/extension-config.ts | 47 +++++ .../core/extensions/custom-image/extension.ts | 121 ++++++++++++ .../src/core/extensions/custom-image/index.ts | 3 - .../custom-image/read-only-custom-image.ts | 79 -------- .../src/core/extensions/custom-image/types.ts | 51 +++++ .../src/core/extensions/custom-image/utils.ts | 33 ++++ .../editor/src/core/extensions/extensions.ts | 14 +- ...without-props.tsx => extension-config.tsx} | 8 +- .../src/core/extensions/image/extension.tsx | 55 +++--- .../image/image-component-without-props.tsx | 56 ------ .../editor/src/core/extensions/image/index.ts | 3 +- .../core/extensions/image/read-only-image.tsx | 37 ---- packages/editor/src/core/extensions/index.ts | 1 - .../core/extensions/read-only-extensions.ts | 16 +- .../src/core/helpers/editor-commands.ts | 2 +- 24 files changed, 375 insertions(+), 514 deletions(-) rename packages/editor/src/core/extensions/custom-image/components/{image-block.tsx => block.tsx} (89%) delete mode 100644 packages/editor/src/core/extensions/custom-image/components/index.ts rename packages/editor/src/core/extensions/custom-image/components/{image-node.tsx => node-view.tsx} (69%) rename packages/editor/src/core/extensions/custom-image/components/{image-uploader.tsx => uploader.tsx} (91%) delete mode 100644 packages/editor/src/core/extensions/custom-image/custom-image.ts create mode 100644 packages/editor/src/core/extensions/custom-image/extension-config.ts create mode 100644 packages/editor/src/core/extensions/custom-image/extension.ts delete mode 100644 packages/editor/src/core/extensions/custom-image/index.ts delete mode 100644 packages/editor/src/core/extensions/custom-image/read-only-custom-image.ts create mode 100644 packages/editor/src/core/extensions/custom-image/types.ts create mode 100644 packages/editor/src/core/extensions/custom-image/utils.ts rename packages/editor/src/core/extensions/image/{image-extension-without-props.tsx => extension-config.tsx} (50%) delete mode 100644 packages/editor/src/core/extensions/image/image-component-without-props.tsx delete mode 100644 packages/editor/src/core/extensions/image/read-only-image.tsx diff --git a/packages/editor/src/ce/types/storage.ts b/packages/editor/src/ce/types/storage.ts index 5f576df50..84eee65f9 100644 --- a/packages/editor/src/ce/types/storage.ts +++ b/packages/editor/src/ce/types/storage.ts @@ -2,7 +2,7 @@ import { CORE_EXTENSIONS } from "@/constants/extension"; // extensions import { type HeadingExtensionStorage } from "@/extensions"; -import { type CustomImageExtensionStorage } from "@/extensions/custom-image"; +import { type CustomImageExtensionStorage } from "@/extensions/custom-image/types"; import { type CustomLinkStorage } from "@/extensions/custom-link"; import { type ImageExtensionStorage } from "@/extensions/image"; import { type MentionExtensionStorage } from "@/extensions/mentions"; diff --git a/packages/editor/src/core/extensions/core-without-props.ts b/packages/editor/src/core/extensions/core-without-props.ts index a309c2013..d66cae7bd 100644 --- a/packages/editor/src/core/extensions/core-without-props.ts +++ b/packages/editor/src/core/extensions/core-without-props.ts @@ -12,10 +12,10 @@ import { CustomCalloutExtensionConfig } from "./callout/extension-config"; import { CustomCodeBlockExtensionWithoutProps } from "./code/without-props"; import { CustomCodeInlineExtension } from "./code-inline"; import { CustomColorExtension } from "./custom-color"; +import { CustomImageExtensionConfig } from "./custom-image/extension-config"; import { CustomLinkExtension } from "./custom-link"; import { CustomHorizontalRule } from "./horizontal-rule"; -import { ImageExtensionWithoutProps } from "./image"; -import { CustomImageComponentWithoutProps } from "./image/image-component-without-props"; +import { ImageExtensionConfig } from "./image"; import { CustomMentionExtensionConfig } from "./mentions/extension-config"; import { CustomQuoteExtension } from "./quote"; import { TableHeader, TableCell, TableRow, Table } from "./table"; @@ -72,12 +72,8 @@ export const CoreEditorExtensionsWithoutProps = [ "text-custom-primary-300 underline underline-offset-[3px] hover:text-custom-primary-500 transition-colors cursor-pointer", }, }), - ImageExtensionWithoutProps.configure({ - HTMLAttributes: { - class: "rounded-md", - }, - }), - CustomImageComponentWithoutProps, + ImageExtensionConfig, + CustomImageExtensionConfig, TiptapUnderline, TextStyle, TaskList.configure({ diff --git a/packages/editor/src/core/extensions/custom-image/components/image-block.tsx b/packages/editor/src/core/extensions/custom-image/components/block.tsx similarity index 89% rename from packages/editor/src/core/extensions/custom-image/components/image-block.tsx rename to packages/editor/src/core/extensions/custom-image/components/block.tsx index 5dfbad012..1ff36abca 100644 --- a/packages/editor/src/core/extensions/custom-image/components/image-block.tsx +++ b/packages/editor/src/core/extensions/custom-image/components/block.tsx @@ -1,68 +1,42 @@ import { NodeSelection } from "@tiptap/pm/state"; import React, { useRef, useState, useCallback, useLayoutEffect, useEffect } from "react"; -// plane utils +// plane imports import { cn } from "@plane/utils"; -// extensions -import { CustomBaseImageNodeViewProps, ImageToolbarRoot } from "@/extensions/custom-image"; +// local imports +import { Pixel, TCustomImageAttributes, TCustomImageSize } from "../types"; +import { ensurePixelString } from "../utils"; +import type { CustomImageNodeViewProps } from "./node-view"; +import { ImageToolbarRoot } from "./toolbar"; import { ImageUploadStatus } from "./upload-status"; const MIN_SIZE = 100; -type Pixel = `${number}px`; - -type PixelAttribute = Pixel | TDefault; - -export type ImageAttributes = { - src: string | null; - width: PixelAttribute<"35%" | number>; - height: PixelAttribute<"auto" | number>; - aspectRatio: number | null; - id: string | null; -}; - -type Size = { - width: PixelAttribute<"35%">; - height: PixelAttribute<"auto">; - aspectRatio: number | null; -}; - -const ensurePixelString = (value: Pixel | TDefault | number | undefined | null, defaultValue?: TDefault) => { - if (!value || value === defaultValue) { - return defaultValue; - } - - if (typeof value === "number") { - return `${value}px` satisfies Pixel; - } - - return value; -}; - -type CustomImageBlockProps = CustomBaseImageNodeViewProps & { - imageFromFileSystem: string | undefined; - setFailedToLoadImage: (isError: boolean) => void; +type CustomImageBlockProps = CustomImageNodeViewProps & { editorContainer: HTMLDivElement | null; + imageFromFileSystem: string | undefined; setEditorContainer: (editorContainer: HTMLDivElement | null) => void; + setFailedToLoadImage: (isError: boolean) => void; src: string | undefined; }; export const CustomImageBlock: React.FC = (props) => { // props const { - node, - updateAttributes, - setFailedToLoadImage, - imageFromFileSystem, - selected, - getPos, editor, editorContainer, - src: resolvedImageSrc, + extension, + getPos, + imageFromFileSystem, + node, + selected, setEditorContainer, + setFailedToLoadImage, + src: resolvedImageSrc, + updateAttributes, } = props; const { width: nodeWidth, height: nodeHeight, aspectRatio: nodeAspectRatio, src: imgNodeSrc } = node.attrs; // states - const [size, setSize] = useState({ + const [size, setSize] = useState({ width: ensurePixelString(nodeWidth, "35%") ?? "35%", height: ensurePixelString(nodeHeight, "auto") ?? "auto", aspectRatio: nodeAspectRatio || null, @@ -77,7 +51,7 @@ export const CustomImageBlock: React.FC = (props) => { const [hasTriedRestoringImageOnce, setHasTriedRestoringImageOnce] = useState(false); const updateAttributesSafely = useCallback( - (attributes: Partial, errorMessage: string) => { + (attributes: Partial, errorMessage: string) => { try { updateAttributes(attributes); } catch (error) { @@ -114,7 +88,7 @@ export const CustomImageBlock: React.FC = (props) => { const initialWidth = Math.max(editorWidth * 0.35, MIN_SIZE); const initialHeight = initialWidth / aspectRatioCalculated; - const initialComputedSize = { + const initialComputedSize: TCustomImageSize = { width: `${Math.round(initialWidth)}px` satisfies Pixel, height: `${Math.round(initialHeight)}px` satisfies Pixel, aspectRatio: aspectRatioCalculated, @@ -139,7 +113,7 @@ export const CustomImageBlock: React.FC = (props) => { } } setInitialResizeComplete(true); - }, [nodeWidth, updateAttributes, editorContainer, nodeAspectRatio]); + }, [nodeWidth, updateAttributesSafely, editorContainer, nodeAspectRatio, setEditorContainer]); // for real time resizing useLayoutEffect(() => { @@ -168,7 +142,7 @@ export const CustomImageBlock: React.FC = (props) => { const handleResizeEnd = useCallback(() => { setIsResizing(false); updateAttributesSafely(size, "Failed to update attributes at the end of resizing:"); - }, [size, updateAttributes]); + }, [size, updateAttributesSafely]); const handleResizeStart = useCallback((e: React.MouseEvent | React.TouchEvent) => { e.preventDefault(); @@ -242,7 +216,7 @@ export const CustomImageBlock: React.FC = (props) => { onLoad={handleImageLoad} onError={async (e) => { // for old image extension this command doesn't exist or if the image failed to load for the first time - if (!editor?.commands.restoreImage || hasTriedRestoringImageOnce) { + if (!extension.options.restoreImage || hasTriedRestoringImageOnce) { setFailedToLoadImage(true); return; } @@ -253,7 +227,7 @@ export const CustomImageBlock: React.FC = (props) => { if (!imgNodeSrc) { throw new Error("No source image to restore from"); } - await editor?.commands.restoreImage?.(imgNodeSrc); + await extension.options.restoreImage?.(imgNodeSrc); if (!imageRef.current) { throw new Error("Image reference not found"); } @@ -289,10 +263,10 @@ export const CustomImageBlock: React.FC = (props) => { "absolute top-1 right-1 z-20 bg-black/40 rounded opacity-0 pointer-events-none group-hover/image-component:opacity-100 group-hover/image-component:pointer-events-auto transition-opacity" } image={{ - src: resolvedImageSrc, - aspectRatio: size.aspectRatio === null ? 1 : size.aspectRatio, - height: size.height, width: size.width, + height: size.height, + aspectRatio: size.aspectRatio === null ? 1 : size.aspectRatio, + src: resolvedImageSrc, }} /> )} diff --git a/packages/editor/src/core/extensions/custom-image/components/index.ts b/packages/editor/src/core/extensions/custom-image/components/index.ts deleted file mode 100644 index 9d12c3ecf..000000000 --- a/packages/editor/src/core/extensions/custom-image/components/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from "./toolbar"; -export * from "./image-block"; -export * from "./image-node"; -export * from "./image-uploader"; diff --git a/packages/editor/src/core/extensions/custom-image/components/image-node.tsx b/packages/editor/src/core/extensions/custom-image/components/node-view.tsx similarity index 69% rename from packages/editor/src/core/extensions/custom-image/components/image-node.tsx rename to packages/editor/src/core/extensions/custom-image/components/node-view.tsx index 8dfe6974b..74ea2c38c 100644 --- a/packages/editor/src/core/extensions/custom-image/components/image-node.tsx +++ b/packages/editor/src/core/extensions/custom-image/components/node-view.tsx @@ -2,25 +2,26 @@ import { Editor, NodeViewProps, NodeViewWrapper } from "@tiptap/react"; import { useEffect, useRef, useState } from "react"; // constants import { CORE_EXTENSIONS } from "@/constants/extension"; -// extensions -import { CustomImageBlock, CustomImageUploader, ImageAttributes } from "@/extensions/custom-image"; // helpers import { getExtensionStorage } from "@/helpers/get-extension-storage"; +// local imports +import type { CustomImageExtension, TCustomImageAttributes } from "../types"; +import { CustomImageBlock } from "./block"; +import { CustomImageUploader } from "./uploader"; -export type CustomBaseImageNodeViewProps = { +export type CustomImageNodeViewProps = Omit & { + extension: CustomImageExtension; getPos: () => number; editor: Editor; node: NodeViewProps["node"] & { - attrs: ImageAttributes; + attrs: TCustomImageAttributes; }; - updateAttributes: (attrs: Partial) => void; + updateAttributes: (attrs: Partial) => void; selected: boolean; }; -export type CustomImageNodeProps = NodeViewProps & CustomBaseImageNodeViewProps; - -export const CustomImageNode = (props: CustomImageNodeProps) => { - const { getPos, editor, node, updateAttributes, selected } = props; +export const CustomImageNodeView: React.FC = (props) => { + const { editor, extension, node } = props; const { src: imgNodeSrc } = node.attrs; const [isUploaded, setIsUploaded] = useState(false); @@ -50,41 +51,37 @@ export const CustomImageNode = (props: CustomImageNodeProps) => { }, [resolvedSrc]); useEffect(() => { + if (!imgNodeSrc) { + setResolvedSrc(undefined); + return; + } + const getImageSource = async () => { - // @ts-expect-error function not expected here, but will still work and don't remove await - const url: string = await editor?.commands?.getImageSource?.(imgNodeSrc); - setResolvedSrc(url as string); + const url = await extension.options.getImageSource?.(imgNodeSrc); + setResolvedSrc(url); }; getImageSource(); - }, [imgNodeSrc]); + }, [imgNodeSrc, extension.options]); return (
    {(isUploaded || imageFromFileSystem) && !failedToLoadImage ? ( ) : ( )}
    diff --git a/packages/editor/src/core/extensions/custom-image/components/toolbar/full-screen.tsx b/packages/editor/src/core/extensions/custom-image/components/toolbar/full-screen.tsx index 61ae307bb..43f178dc8 100644 --- a/packages/editor/src/core/extensions/custom-image/components/toolbar/full-screen.tsx +++ b/packages/editor/src/core/extensions/custom-image/components/toolbar/full-screen.tsx @@ -1,14 +1,14 @@ import { ExternalLink, Maximize, Minus, Plus, X } from "lucide-react"; import { useCallback, useEffect, useMemo, useState, useRef } from "react"; -// plane utils +// plane imports import { cn } from "@plane/utils"; type Props = { image: { - src: string; - height: string; width: string; + height: string; aspectRatio: number; + src: string; }; isOpen: boolean; toggleFullScreenMode: (val: boolean) => void; diff --git a/packages/editor/src/core/extensions/custom-image/components/toolbar/root.tsx b/packages/editor/src/core/extensions/custom-image/components/toolbar/root.tsx index 3179db0d4..f9cd28d48 100644 --- a/packages/editor/src/core/extensions/custom-image/components/toolbar/root.tsx +++ b/packages/editor/src/core/extensions/custom-image/components/toolbar/root.tsx @@ -1,16 +1,16 @@ import { useState } from "react"; -// plane utils +// plane imports import { cn } from "@plane/utils"; -// components +// local imports import { ImageFullScreenAction } from "./full-screen"; type Props = { containerClassName?: string; image: { - src: string; - height: string; width: string; + height: string; aspectRatio: number; + src: string; }; }; diff --git a/packages/editor/src/core/extensions/custom-image/components/image-uploader.tsx b/packages/editor/src/core/extensions/custom-image/components/uploader.tsx similarity index 91% rename from packages/editor/src/core/extensions/custom-image/components/image-uploader.tsx rename to packages/editor/src/core/extensions/custom-image/components/uploader.tsx index 17c9f8177..68626084a 100644 --- a/packages/editor/src/core/extensions/custom-image/components/image-uploader.tsx +++ b/packages/editor/src/core/extensions/custom-image/components/uploader.tsx @@ -1,28 +1,30 @@ import { ImageIcon } from "lucide-react"; import { ChangeEvent, useCallback, useEffect, useMemo, useRef } from "react"; -// plane utils +// plane imports import { cn } from "@plane/utils"; // constants import { ACCEPTED_IMAGE_MIME_TYPES } from "@/constants/config"; import { CORE_EXTENSIONS } from "@/constants/extension"; -// extensions -import { CustomBaseImageNodeViewProps, getImageComponentImageFileMap } from "@/extensions/custom-image"; // helpers import { EFileError } from "@/helpers/file"; import { getExtensionStorage } from "@/helpers/get-extension-storage"; // hooks import { useUploader, useDropZone, uploadFirstFileAndInsertRemaining } from "@/hooks/use-file-upload"; +// local imports +import { getImageComponentImageFileMap } from "../utils"; +import type { CustomImageNodeViewProps } from "./node-view"; -type CustomImageUploaderProps = CustomBaseImageNodeViewProps & { - maxFileSize: number; - loadImageFromFileSystem: (file: string) => void; +type CustomImageUploaderProps = CustomImageNodeViewProps & { failedToLoadImage: boolean; + loadImageFromFileSystem: (file: string) => void; + maxFileSize: number; setIsUploaded: (isUploaded: boolean) => void; }; export const CustomImageUploader = (props: CustomImageUploaderProps) => { const { editor, + extension, failedToLoadImage, getPos, loadImageFromFileSystem, @@ -71,12 +73,13 @@ export const CustomImageUploader = (props: CustomImageUploaderProps) => { } } }, + // eslint-disable-next-line react-hooks/exhaustive-deps [imageComponentImageFileMap, imageEntityId, updateAttributes, getPos] ); const uploadImageEditorCommand = useCallback( - async (file: File) => await editor?.commands.uploadImage(imageEntityId ?? "", file), - [editor, imageEntityId] + async (file: File) => await extension.options.uploadImage?.(imageEntityId ?? "", file), + [extension.options, imageEntityId] ); const handleProgressStatus = useCallback( @@ -93,7 +96,6 @@ export const CustomImageUploader = (props: CustomImageUploaderProps) => { // hooks const { isUploading: isImageBeingUploaded, uploadFile } = useUploader({ acceptedMimeTypes: ACCEPTED_IMAGE_MIME_TYPES, - // @ts-expect-error - TODO: fix typings, and don't remove await from here for now editorCommand: uploadImageEditorCommand, handleProgressStatus, loadFileFromFileSystem: loadImageFromFileSystem, @@ -128,7 +130,7 @@ export const CustomImageUploader = (props: CustomImageUploaderProps) => { imageComponentImageFileMap?.set(imageEntityId ?? "", { ...meta, hasOpenedFileInputOnce: true }); } } - }, [meta, uploadFile, imageComponentImageFileMap]); + }, [meta, uploadFile, imageComponentImageFileMap, imageEntityId]); const onFileChange = useCallback( async (e: ChangeEvent) => { @@ -163,7 +165,7 @@ export const CustomImageUploader = (props: CustomImageUploaderProps) => { } return "Add an image"; - }, [draggedInside, failedToLoadImage, isImageBeingUploaded]); + }, [draggedInside, failedToLoadImage, isImageBeingUploaded, editor.isEditable]); return (
    { - [CORE_EXTENSIONS.CUSTOM_IMAGE]: { - insertImageComponent: ({ file, pos, event }: InsertImageComponentProps) => ReturnType; - uploadImage: (blockId: string, file: File) => () => Promise | undefined; - getImageSource?: (path: string) => () => Promise; - restoreImage: (src: string) => () => Promise; - }; - } -} - -export const getImageComponentImageFileMap = (editor: Editor) => - getExtensionStorage(editor, CORE_EXTENSIONS.CUSTOM_IMAGE)?.fileMap; - -export interface CustomImageExtensionStorage { - fileMap: Map; - deletedImageSet: Map; - maxFileSize: number; -} - -export type UploadEntity = ({ event: "insert" } | { event: "drop"; file: File }) & { hasOpenedFileInputOnce?: boolean }; - -export const CustomImageExtension = (props: TFileHandler) => { - const { - getAssetSrc, - upload, - restore: restoreImageFn, - validation: { maxFileSize }, - } = props; - - return BaseImageExtension.extend, CustomImageExtensionStorage>({ - name: CORE_EXTENSIONS.CUSTOM_IMAGE, - selectable: true, - group: "block", - atom: true, - draggable: true, - - addAttributes() { - return { - ...this.parent?.(), - width: { - default: "35%", - }, - src: { - default: null, - }, - height: { - default: "auto", - }, - ["id"]: { - default: null, - }, - aspectRatio: { - default: null, - }, - }; - }, - - parseHTML() { - return [ - { - tag: "image-component", - }, - ]; - }, - - renderHTML({ HTMLAttributes }) { - return ["image-component", mergeAttributes(HTMLAttributes)]; - }, - - addKeyboardShortcuts() { - return { - ArrowDown: insertEmptyParagraphAtNodeBoundaries("down", this.name), - ArrowUp: insertEmptyParagraphAtNodeBoundaries("up", this.name), - }; - }, - - addStorage() { - return { - fileMap: new Map(), - deletedImageSet: new Map(), - maxFileSize, - // escape markdown for images - markdown: { - serialize() {}, - }, - }; - }, - - addCommands() { - return { - insertImageComponent: - (props) => - ({ commands }) => { - // Early return if there's an invalid file being dropped - if ( - props?.file && - !isFileValid({ - acceptedMimeTypes: ACCEPTED_IMAGE_MIME_TYPES, - file: props.file, - maxFileSize, - onError: (_error, message) => alert(message), - }) - ) { - return false; - } - - // generate a unique id for the image to keep track of dropped - // files' file data - const fileId = uuidv4(); - - const imageComponentImageFileMap = getImageComponentImageFileMap(this.editor); - - if (imageComponentImageFileMap) { - if (props?.event === "drop" && props.file) { - imageComponentImageFileMap.set(fileId, { - file: props.file, - event: props.event, - }); - } else if (props.event === "insert") { - imageComponentImageFileMap.set(fileId, { - event: props.event, - hasOpenedFileInputOnce: false, - }); - } - } - - const attributes = { - id: fileId, - }; - - if (props.pos) { - return commands.insertContentAt(props.pos, { - type: this.name, - attrs: attributes, - }); - } - return commands.insertContent({ - type: this.name, - attrs: attributes, - }); - }, - uploadImage: (blockId, file) => async () => { - const fileUrl = await upload(blockId, file); - return fileUrl; - }, - getImageSource: (path) => async () => await getAssetSrc(path), - restoreImage: (src) => async () => { - await restoreImageFn(src); - }, - }; - }, - - addNodeView() { - return ReactNodeViewRenderer(CustomImageNode); - }, - }); -}; diff --git a/packages/editor/src/core/extensions/custom-image/extension-config.ts b/packages/editor/src/core/extensions/custom-image/extension-config.ts new file mode 100644 index 000000000..56714f533 --- /dev/null +++ b/packages/editor/src/core/extensions/custom-image/extension-config.ts @@ -0,0 +1,47 @@ +import { mergeAttributes } from "@tiptap/core"; +import { Image as BaseImageExtension } from "@tiptap/extension-image"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; +// local imports +import { type CustomImageExtension, ECustomImageAttributeNames, type InsertImageComponentProps } from "./types"; +import { DEFAULT_CUSTOM_IMAGE_ATTRIBUTES } from "./utils"; + +declare module "@tiptap/core" { + interface Commands { + [CORE_EXTENSIONS.CUSTOM_IMAGE]: { + insertImageComponent: ({ file, pos, event }: InsertImageComponentProps) => ReturnType; + }; + } +} + +export const CustomImageExtensionConfig: CustomImageExtension = BaseImageExtension.extend({ + name: CORE_EXTENSIONS.CUSTOM_IMAGE, + group: "block", + atom: true, + + addAttributes() { + const attributes = { + ...this.parent?.(), + ...Object.values(ECustomImageAttributeNames).reduce((acc, value) => { + acc[value] = { + default: DEFAULT_CUSTOM_IMAGE_ATTRIBUTES[value], + }; + return acc; + }, {}), + }; + + return attributes; + }, + + parseHTML() { + return [ + { + tag: "image-component", + }, + ]; + }, + + renderHTML({ HTMLAttributes }) { + return ["image-component", mergeAttributes(HTMLAttributes)]; + }, +}); diff --git a/packages/editor/src/core/extensions/custom-image/extension.ts b/packages/editor/src/core/extensions/custom-image/extension.ts new file mode 100644 index 000000000..ec795da84 --- /dev/null +++ b/packages/editor/src/core/extensions/custom-image/extension.ts @@ -0,0 +1,121 @@ +import { ReactNodeViewRenderer } from "@tiptap/react"; +import { v4 as uuidv4 } from "uuid"; +// constants +import { ACCEPTED_IMAGE_MIME_TYPES } from "@/constants/config"; +// helpers +import { isFileValid } from "@/helpers/file"; +import { insertEmptyParagraphAtNodeBoundaries } from "@/helpers/insert-empty-paragraph-at-node-boundary"; +// types +import type { TFileHandler, TReadOnlyFileHandler } from "@/types"; +// local imports +import { CustomImageNodeView } from "./components/node-view"; +import { CustomImageExtensionConfig } from "./extension-config"; +import { getImageComponentImageFileMap } from "./utils"; + +type Props = { + fileHandler: TFileHandler | TReadOnlyFileHandler; + isEditable: boolean; +}; + +export const CustomImageExtension = (props: Props) => { + const { fileHandler, isEditable } = props; + // derived values + const { getAssetSrc, restore: restoreImageFn } = fileHandler; + + return CustomImageExtensionConfig.extend({ + selectable: isEditable, + draggable: isEditable, + + addOptions() { + const upload = "upload" in fileHandler ? fileHandler.upload : undefined; + + return { + ...this.parent?.(), + getImageSource: getAssetSrc, + restoreImage: restoreImageFn, + uploadImage: upload, + }; + }, + + addStorage() { + const maxFileSize = "validation" in fileHandler ? fileHandler.validation?.maxFileSize : 0; + + return { + fileMap: new Map(), + deletedImageSet: new Map(), + maxFileSize, + // escape markdown for images + markdown: { + serialize() {}, + }, + }; + }, + + addCommands() { + return { + insertImageComponent: + (props) => + ({ commands }) => { + // Early return if there's an invalid file being dropped + if ( + props?.file && + !isFileValid({ + acceptedMimeTypes: ACCEPTED_IMAGE_MIME_TYPES, + file: props.file, + maxFileSize: this.storage.maxFileSize, + onError: (_error, message) => alert(message), + }) + ) { + return false; + } + + // generate a unique id for the image to keep track of dropped + // files' file data + const fileId = uuidv4(); + + const imageComponentImageFileMap = getImageComponentImageFileMap(this.editor); + + if (imageComponentImageFileMap) { + if (props?.event === "drop" && props.file) { + imageComponentImageFileMap.set(fileId, { + file: props.file, + event: props.event, + }); + } else if (props.event === "insert") { + imageComponentImageFileMap.set(fileId, { + event: props.event, + hasOpenedFileInputOnce: false, + }); + } + } + + const attributes = { + id: fileId, + }; + + if (props.pos) { + return commands.insertContentAt(props.pos, { + type: this.name, + attrs: attributes, + }); + } + return commands.insertContent({ + type: this.name, + attrs: attributes, + }); + }, + }; + }, + + addKeyboardShortcuts() { + return { + ArrowDown: insertEmptyParagraphAtNodeBoundaries("down", this.name), + ArrowUp: insertEmptyParagraphAtNodeBoundaries("up", this.name), + }; + }, + + addNodeView() { + return ReactNodeViewRenderer(CustomImageNodeView); + }, + }); +}; diff --git a/packages/editor/src/core/extensions/custom-image/index.ts b/packages/editor/src/core/extensions/custom-image/index.ts deleted file mode 100644 index de2bb3878..000000000 --- a/packages/editor/src/core/extensions/custom-image/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./components"; -export * from "./custom-image"; -export * from "./read-only-custom-image"; diff --git a/packages/editor/src/core/extensions/custom-image/read-only-custom-image.ts b/packages/editor/src/core/extensions/custom-image/read-only-custom-image.ts deleted file mode 100644 index 4a85ffd94..000000000 --- a/packages/editor/src/core/extensions/custom-image/read-only-custom-image.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { mergeAttributes } from "@tiptap/core"; -import { Image as BaseImageExtension } from "@tiptap/extension-image"; -import { ReactNodeViewRenderer } from "@tiptap/react"; -// constants -import { CORE_EXTENSIONS } from "@/constants/extension"; -// components -import { CustomImageNode, CustomImageExtensionStorage } from "@/extensions/custom-image"; -// types -import { TReadOnlyFileHandler } from "@/types"; - -export const CustomReadOnlyImageExtension = (props: TReadOnlyFileHandler) => { - const { getAssetSrc, restore: restoreImageFn } = props; - - return BaseImageExtension.extend, CustomImageExtensionStorage>({ - name: CORE_EXTENSIONS.CUSTOM_IMAGE, - selectable: false, - group: "block", - atom: true, - draggable: false, - - addAttributes() { - return { - ...this.parent?.(), - width: { - default: "35%", - }, - src: { - default: null, - }, - height: { - default: "auto", - }, - ["id"]: { - default: null, - }, - aspectRatio: { - default: null, - }, - }; - }, - - parseHTML() { - return [ - { - tag: "image-component", - }, - ]; - }, - - renderHTML({ HTMLAttributes }) { - return ["image-component", mergeAttributes(HTMLAttributes)]; - }, - - addStorage() { - return { - fileMap: new Map(), - deletedImageSet: new Map(), - maxFileSize: 0, - // escape markdown for images - markdown: { - serialize() {}, - }, - }; - }, - - addCommands() { - return { - getImageSource: (path: string) => async () => await getAssetSrc(path), - restoreImage: (src) => async () => { - await restoreImageFn(src); - }, - }; - }, - - addNodeView() { - return ReactNodeViewRenderer(CustomImageNode); - }, - }); -}; diff --git a/packages/editor/src/core/extensions/custom-image/types.ts b/packages/editor/src/core/extensions/custom-image/types.ts new file mode 100644 index 000000000..675d8a221 --- /dev/null +++ b/packages/editor/src/core/extensions/custom-image/types.ts @@ -0,0 +1,51 @@ +import type { Node } from "@tiptap/core"; +// types +import type { TFileHandler } from "@/types"; + +export enum ECustomImageAttributeNames { + ID = "id", + WIDTH = "width", + HEIGHT = "height", + ASPECT_RATIO = "aspectRatio", + SOURCE = "src", +} + +export type Pixel = `${number}px`; + +export type PixelAttribute = Pixel | TDefault; + +export type TCustomImageSize = { + width: PixelAttribute<"35%">; + height: PixelAttribute<"auto">; + aspectRatio: number | null; +}; + +export type TCustomImageAttributes = { + [ECustomImageAttributeNames.ID]: string | null; + [ECustomImageAttributeNames.WIDTH]: PixelAttribute<"35%" | number> | null; + [ECustomImageAttributeNames.HEIGHT]: PixelAttribute<"auto" | number> | null; + [ECustomImageAttributeNames.ASPECT_RATIO]: number | null; + [ECustomImageAttributeNames.SOURCE]: string | null; +}; + +export type UploadEntity = ({ event: "insert" } | { event: "drop"; file: File }) & { hasOpenedFileInputOnce?: boolean }; + +export type InsertImageComponentProps = { + file?: File; + pos?: number; + event: "insert" | "drop"; +}; + +export type CustomImageExtensionOptions = { + getImageSource: TFileHandler["getAssetSrc"]; + restoreImage: TFileHandler["restore"]; + uploadImage?: TFileHandler["upload"]; +}; + +export type CustomImageExtensionStorage = { + fileMap: Map; + deletedImageSet: Map; + maxFileSize: number; +}; + +export type CustomImageExtension = Node; diff --git a/packages/editor/src/core/extensions/custom-image/utils.ts b/packages/editor/src/core/extensions/custom-image/utils.ts new file mode 100644 index 000000000..0711e094f --- /dev/null +++ b/packages/editor/src/core/extensions/custom-image/utils.ts @@ -0,0 +1,33 @@ +import type { Editor } from "@tiptap/core"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; +// helpers +import { getExtensionStorage } from "@/helpers/get-extension-storage"; +// local imports +import { ECustomImageAttributeNames, type Pixel, type TCustomImageAttributes } from "./types"; + +export const DEFAULT_CUSTOM_IMAGE_ATTRIBUTES: TCustomImageAttributes = { + [ECustomImageAttributeNames.SOURCE]: null, + [ECustomImageAttributeNames.ID]: null, + [ECustomImageAttributeNames.WIDTH]: "35%", + [ECustomImageAttributeNames.HEIGHT]: "auto", + [ECustomImageAttributeNames.ASPECT_RATIO]: null, +}; + +export const getImageComponentImageFileMap = (editor: Editor) => + getExtensionStorage(editor, CORE_EXTENSIONS.CUSTOM_IMAGE)?.fileMap; + +export const ensurePixelString = ( + value: Pixel | TDefault | number | undefined | null, + defaultValue?: TDefault +) => { + if (!value || value === defaultValue) { + return defaultValue; + } + + if (typeof value === "number") { + return `${value}px` satisfies Pixel; + } + + return value; +}; diff --git a/packages/editor/src/core/extensions/extensions.ts b/packages/editor/src/core/extensions/extensions.ts index 0807fa62d..cc8882005 100644 --- a/packages/editor/src/core/extensions/extensions.ts +++ b/packages/editor/src/core/extensions/extensions.ts @@ -16,7 +16,6 @@ import { CustomCodeInlineExtension, CustomColorExtension, CustomHorizontalRule, - CustomImageExtension, CustomKeymap, CustomLinkExtension, CustomMentionExtension, @@ -38,6 +37,8 @@ import { getExtensionStorage } from "@/helpers/get-extension-storage"; import { CoreEditorAdditionalExtensions } from "@/plane-editor/extensions"; // types import type { IEditorProps } from "@/types"; +// local imports +import { CustomImageExtension } from "./custom-image/extension"; type TArguments = Pick< IEditorProps, @@ -191,12 +192,13 @@ export const CoreEditorExtensions = (args: TArguments): Extensions => { if (!disabledExtensions.includes("image")) { extensions.push( - ImageExtension(fileHandler).configure({ - HTMLAttributes: { - class: "rounded-md", - }, + ImageExtension({ + fileHandler, }), - CustomImageExtension(fileHandler) + CustomImageExtension({ + fileHandler, + isEditable: editable, + }) ); } diff --git a/packages/editor/src/core/extensions/image/image-extension-without-props.tsx b/packages/editor/src/core/extensions/image/extension-config.tsx similarity index 50% rename from packages/editor/src/core/extensions/image/image-extension-without-props.tsx rename to packages/editor/src/core/extensions/image/extension-config.tsx index ba064bef4..6dbad2d24 100644 --- a/packages/editor/src/core/extensions/image/image-extension-without-props.tsx +++ b/packages/editor/src/core/extensions/image/extension-config.tsx @@ -1,6 +1,12 @@ import { Image as BaseImageExtension } from "@tiptap/extension-image"; +// local imports +import { CustomImageExtensionOptions } from "../custom-image/types"; +import { ImageExtensionStorage } from "./extension"; -export const ImageExtensionWithoutProps = BaseImageExtension.extend({ +export const ImageExtensionConfig = BaseImageExtension.extend< + Pick, + ImageExtensionStorage +>({ addAttributes() { return { ...this.parent?.(), diff --git a/packages/editor/src/core/extensions/image/extension.tsx b/packages/editor/src/core/extensions/image/extension.tsx index 12844149c..80cf7c182 100644 --- a/packages/editor/src/core/extensions/image/extension.tsx +++ b/packages/editor/src/core/extensions/image/extension.tsx @@ -1,23 +1,33 @@ -import { Image as BaseImageExtension } from "@tiptap/extension-image"; import { ReactNodeViewRenderer } from "@tiptap/react"; -// extensions -import { CustomImageNode } from "@/extensions"; // helpers import { insertEmptyParagraphAtNodeBoundaries } from "@/helpers/insert-empty-paragraph-at-node-boundary"; // types -import { TFileHandler } from "@/types"; +import type { TFileHandler, TReadOnlyFileHandler } from "@/types"; +// local imports +import { CustomImageNodeView } from "../custom-image/components/node-view"; +import { ImageExtensionConfig } from "./extension-config"; export type ImageExtensionStorage = { deletedImageSet: Map; }; -export const ImageExtension = (fileHandler: TFileHandler) => { - const { - getAssetSrc, - validation: { maxFileSize }, - } = fileHandler; +type Props = { + fileHandler: TFileHandler | TReadOnlyFileHandler; +}; + +export const ImageExtension = (props: Props) => { + const { fileHandler } = props; + // derived values + const { getAssetSrc } = fileHandler; + + return ImageExtensionConfig.extend({ + addOptions() { + return { + ...this.parent?.(), + getImageSource: getAssetSrc, + }; + }, - return BaseImageExtension.extend({ addKeyboardShortcuts() { return { ArrowDown: insertEmptyParagraphAtNodeBoundaries("down", this.name), @@ -27,36 +37,17 @@ export const ImageExtension = (fileHandler: TFileHandler) => { // storage to keep track of image states Map addStorage() { + const maxFileSize = "validation" in fileHandler ? fileHandler.validation?.maxFileSize : 0; + return { deletedImageSet: new Map(), maxFileSize, }; }, - addAttributes() { - return { - ...this.parent?.(), - width: { - default: "35%", - }, - height: { - default: null, - }, - aspectRatio: { - default: null, - }, - }; - }, - - addCommands() { - return { - getImageSource: (path: string) => async () => await getAssetSrc(path), - }; - }, - // render custom image node addNodeView() { - return ReactNodeViewRenderer(CustomImageNode); + return ReactNodeViewRenderer(CustomImageNodeView); }, }); }; diff --git a/packages/editor/src/core/extensions/image/image-component-without-props.tsx b/packages/editor/src/core/extensions/image/image-component-without-props.tsx deleted file mode 100644 index bd2c3f16b..000000000 --- a/packages/editor/src/core/extensions/image/image-component-without-props.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { mergeAttributes } from "@tiptap/core"; -import { Image as BaseImageExtension } from "@tiptap/extension-image"; -// local imports -import { ImageExtensionStorage } from "./extension"; - -export const CustomImageComponentWithoutProps = BaseImageExtension.extend< - Record, - ImageExtensionStorage ->({ - name: "imageComponent", - selectable: true, - group: "block", - atom: true, - draggable: true, - - addAttributes() { - return { - ...this.parent?.(), - width: { - default: "35%", - }, - src: { - default: null, - }, - height: { - default: "auto", - }, - ["id"]: { - default: null, - }, - aspectRatio: { - default: null, - }, - }; - }, - - parseHTML() { - return [ - { - tag: "image-component", - }, - ]; - }, - - renderHTML({ HTMLAttributes }) { - return ["image-component", mergeAttributes(HTMLAttributes)]; - }, - - addStorage() { - return { - fileMap: new Map(), - deletedImageSet: new Map(), - maxFileSize: 0, - }; - }, -}); diff --git a/packages/editor/src/core/extensions/image/index.ts b/packages/editor/src/core/extensions/image/index.ts index 9c7dc65d7..02b5a53d6 100644 --- a/packages/editor/src/core/extensions/image/index.ts +++ b/packages/editor/src/core/extensions/image/index.ts @@ -1,3 +1,2 @@ export * from "./extension"; -export * from "./image-extension-without-props"; -export * from "./read-only-image"; +export * from "./extension-config"; diff --git a/packages/editor/src/core/extensions/image/read-only-image.tsx b/packages/editor/src/core/extensions/image/read-only-image.tsx deleted file mode 100644 index 271c39fd8..000000000 --- a/packages/editor/src/core/extensions/image/read-only-image.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { Image as BaseImageExtension } from "@tiptap/extension-image"; -import { ReactNodeViewRenderer } from "@tiptap/react"; -// extensions -import { CustomImageNode } from "@/extensions"; -// types -import { TReadOnlyFileHandler } from "@/types"; - -export const ReadOnlyImageExtension = (props: TReadOnlyFileHandler) => { - const { getAssetSrc } = props; - - return BaseImageExtension.extend({ - addAttributes() { - return { - ...this.parent?.(), - width: { - default: "35%", - }, - height: { - default: null, - }, - aspectRatio: { - default: null, - }, - }; - }, - - addCommands() { - return { - getImageSource: (path: string) => async () => await getAssetSrc(path), - }; - }, - - addNodeView() { - return ReactNodeViewRenderer(CustomImageNode); - }, - }); -}; diff --git a/packages/editor/src/core/extensions/index.ts b/packages/editor/src/core/extensions/index.ts index 3c3232885..c3a8e5d5c 100644 --- a/packages/editor/src/core/extensions/index.ts +++ b/packages/editor/src/core/extensions/index.ts @@ -1,7 +1,6 @@ export * from "./callout"; export * from "./code"; export * from "./code-inline"; -export * from "./custom-image"; export * from "./custom-link"; export * from "./custom-list-keymap"; export * from "./image"; diff --git a/packages/editor/src/core/extensions/read-only-extensions.ts b/packages/editor/src/core/extensions/read-only-extensions.ts index 0f422c620..c99b02312 100644 --- a/packages/editor/src/core/extensions/read-only-extensions.ts +++ b/packages/editor/src/core/extensions/read-only-extensions.ts @@ -12,7 +12,6 @@ import { CustomHorizontalRule, CustomLinkExtension, CustomTypographyExtension, - ReadOnlyImageExtension, CustomCodeBlockExtension, CustomCodeInlineExtension, TableHeader, @@ -20,11 +19,11 @@ import { TableRow, Table, CustomMentionExtension, - CustomReadOnlyImageExtension, CustomTextAlignExtension, CustomCalloutReadOnlyExtension, CustomColorExtension, UtilityExtension, + ImageExtension, } from "@/extensions"; // helpers import { isValidHttpUrl } from "@/helpers/common"; @@ -32,6 +31,8 @@ import { isValidHttpUrl } from "@/helpers/common"; import { CoreReadOnlyEditorAdditionalExtensions } from "@/plane-editor/extensions"; // types import type { IReadOnlyEditorProps } from "@/types"; +// local imports +import { CustomImageExtension } from "./custom-image/extension"; type Props = Pick; @@ -135,12 +136,13 @@ export const CoreReadOnlyEditorExtensions = (props: Props): Extensions => { if (!disabledExtensions.includes("image")) { extensions.push( - ReadOnlyImageExtension(fileHandler).configure({ - HTMLAttributes: { - class: "rounded-md", - }, + ImageExtension({ + fileHandler, }), - CustomReadOnlyImageExtension(fileHandler) + CustomImageExtension({ + fileHandler, + isEditable: false, + }) ); } diff --git a/packages/editor/src/core/helpers/editor-commands.ts b/packages/editor/src/core/helpers/editor-commands.ts index 5fa15cb08..415a42bb3 100644 --- a/packages/editor/src/core/helpers/editor-commands.ts +++ b/packages/editor/src/core/helpers/editor-commands.ts @@ -2,8 +2,8 @@ import { Editor, Range } from "@tiptap/core"; // constants import { CORE_EXTENSIONS } from "@/constants/extension"; // extensions -import { InsertImageComponentProps } from "@/extensions"; import { replaceCodeWithText } from "@/extensions/code/utils/replace-code-block-with-text"; +import type { InsertImageComponentProps } from "@/extensions/custom-image/types"; // helpers import { findTableAncestor } from "@/helpers/common"; From fbcc8fc8a09f8e719199ab5a1dccb9937ed1360e Mon Sep 17 00:00:00 2001 From: Vipin Chaudhary Date: Tue, 24 Jun 2025 14:16:07 +0530 Subject: [PATCH 185/201] [WIKI-421] fix: Toolbar not reflecting strikethrough state (#7255) * fix: strick through * fix: bubble menu options types --------- Co-authored-by: vipin chaudhary --- .../components/menus/bubble-menu/root.tsx | 34 +++++++++++-------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/packages/editor/src/core/components/menus/bubble-menu/root.tsx b/packages/editor/src/core/components/menus/bubble-menu/root.tsx index 30a7c5620..a3fa3e2d7 100644 --- a/packages/editor/src/core/components/menus/bubble-menu/root.tsx +++ b/packages/editor/src/core/components/menus/bubble-menu/root.tsx @@ -10,6 +10,7 @@ import { BubbleMenuLinkSelector, BubbleMenuNodeSelector, CodeItem, + EditorMenuItem, ItalicItem, StrikeThroughItem, TextAlignItem, @@ -23,6 +24,7 @@ import { CORE_EXTENSIONS } from "@/constants/extension"; import { isCellSelection } from "@/extensions/table/table/utilities/is-cell-selection"; // local components import { TextAlignmentSelector } from "./alignment-selector"; +import { TEditorCommands } from "@/types"; type EditorBubbleMenuProps = Omit; @@ -31,19 +33,19 @@ export interface EditorStateType { bold: boolean; italic: boolean; underline: boolean; - strike: boolean; + strikethrough: boolean; left: boolean; right: boolean; center: boolean; color: { key: string; label: string; textColor: string; backgroundColor: string } | undefined; backgroundColor: - | { - key: string; - label: string; - textColor: string; - backgroundColor: string; - } - | undefined; + | { + key: string; + label: string; + textColor: string; + backgroundColor: string; + } + | undefined; } export const EditorBubbleMenu: FC = (props: { editor: Editor }) => { @@ -58,8 +60,10 @@ export const EditorBubbleMenu: FC = (props: { editor: Edi bold: BoldItem(props.editor), italic: ItalicItem(props.editor), underline: UnderLineItem(props.editor), - strike: StrikeThroughItem(props.editor), - textAlign: TextAlignItem(props.editor), + strikethrough: StrikeThroughItem(props.editor), + "text-align": TextAlignItem(props.editor), + } satisfies { + [K in TEditorCommands]?: EditorMenuItem; }; const editorState: EditorStateType = useEditorState({ @@ -69,10 +73,10 @@ export const EditorBubbleMenu: FC = (props: { editor: Edi bold: formattingItems.bold.isActive(), italic: formattingItems.italic.isActive(), underline: formattingItems.underline.isActive(), - strike: formattingItems.strike.isActive(), - left: formattingItems.textAlign.isActive({ alignment: "left" }), - right: formattingItems.textAlign.isActive({ alignment: "right" }), - center: formattingItems.textAlign.isActive({ alignment: "center" }), + strikethrough: formattingItems.strikethrough.isActive(), + left: formattingItems["text-align"].isActive({ alignment: "left" }), + right: formattingItems["text-align"].isActive({ alignment: "right" }), + center: formattingItems["text-align"].isActive({ alignment: "center" }), color: COLORS_LIST.find((c) => TextColorItem(editor).isActive({ color: c.key })), backgroundColor: COLORS_LIST.find((c) => BackgroundColorItem(editor).isActive({ color: c.key })), }), @@ -80,7 +84,7 @@ export const EditorBubbleMenu: FC = (props: { editor: Edi const basicFormattingOptions = editorState.code ? [formattingItems.code] - : [formattingItems.bold, formattingItems.italic, formattingItems.underline, formattingItems.strike]; + : [formattingItems.bold, formattingItems.italic, formattingItems.underline, formattingItems.strikethrough]; const bubbleMenuProps: EditorBubbleMenuProps = { ...props, From 22a9d48ca39a8301fc1bf6fdf6e94a99e1025b74 Mon Sep 17 00:00:00 2001 From: JayashTripathy <76092296+JayashTripathy@users.noreply.github.com> Date: Tue, 24 Jun 2025 14:17:40 +0530 Subject: [PATCH 186/201] [WEB-4363]: Analytics tab improvements #7248 fix: padding in tabs --- .../analytics/{ => [tabId]}/header.tsx | 0 .../analytics/{ => [tabId]}/layout.tsx | 3 +- .../analytics/{ => [tabId]}/page.tsx | 37 +++++++++++++------ web/next.config.js | 5 +++ 4 files changed, 31 insertions(+), 14 deletions(-) rename web/app/(all)/[workspaceSlug]/(projects)/analytics/{ => [tabId]}/header.tsx (100%) rename web/app/(all)/[workspaceSlug]/(projects)/analytics/{ => [tabId]}/layout.tsx (70%) rename web/app/(all)/[workspaceSlug]/(projects)/analytics/{ => [tabId]}/page.tsx (87%) diff --git a/web/app/(all)/[workspaceSlug]/(projects)/analytics/header.tsx b/web/app/(all)/[workspaceSlug]/(projects)/analytics/[tabId]/header.tsx similarity index 100% rename from web/app/(all)/[workspaceSlug]/(projects)/analytics/header.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/analytics/[tabId]/header.tsx diff --git a/web/app/(all)/[workspaceSlug]/(projects)/analytics/layout.tsx b/web/app/(all)/[workspaceSlug]/(projects)/analytics/[tabId]/layout.tsx similarity index 70% rename from web/app/(all)/[workspaceSlug]/(projects)/analytics/layout.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/analytics/[tabId]/layout.tsx index 6f087aa56..3a531a7b2 100644 --- a/web/app/(all)/[workspaceSlug]/(projects)/analytics/layout.tsx +++ b/web/app/(all)/[workspaceSlug]/(projects)/analytics/[tabId]/layout.tsx @@ -1,10 +1,9 @@ "use client"; // components import { AppHeader, ContentWrapper } from "@/components/core"; -// plane web components import { WorkspaceAnalyticsHeader } from "./header"; -export default function WorkspaceAnalyticsLayout({ children }: { children: React.ReactNode }) { +export default function WorkspaceAnalyticsTabLayout({ children }: { children: React.ReactNode }) { return ( <> } /> diff --git a/web/app/(all)/[workspaceSlug]/(projects)/analytics/page.tsx b/web/app/(all)/[workspaceSlug]/(projects)/analytics/[tabId]/page.tsx similarity index 87% rename from web/app/(all)/[workspaceSlug]/(projects)/analytics/page.tsx rename to web/app/(all)/[workspaceSlug]/(projects)/analytics/[tabId]/page.tsx index 7e9e0ac9e..477534a59 100644 --- a/web/app/(all)/[workspaceSlug]/(projects)/analytics/page.tsx +++ b/web/app/(all)/[workspaceSlug]/(projects)/analytics/[tabId]/page.tsx @@ -16,23 +16,32 @@ import { useCommandPalette, useEventTracker, useProject, useUserPermissions, use import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; import { getAnalyticsTabs } from "@/plane-web/components/analytics/tabs"; -const AnalyticsPage = observer(() => { +type Props = { + params: { + tabId: string; + }; +}; + +const AnalyticsPage = observer((props: Props) => { + // props + const { params } = props; + const { tabId } = params; + + // hooks const router = useRouter(); - const searchParams = useSearchParams(); + // plane imports const { t } = useTranslation(); + // store hooks const { toggleCreateProjectModal } = useCommandPalette(); const { setTrackElement } = useEventTracker(); const { workspaceProjectIds, loader } = useProject(); const { currentWorkspace } = useWorkspace(); const { allowPermissions } = useUserPermissions(); + // helper hooks const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/onboarding/analytics" }); - // derived values - const pageTitle = currentWorkspace?.name - ? t(`workspace_analytics.page_label`, { workspace: currentWorkspace?.name }) - : undefined; // permissions const canPerformEmptyStateActions = allowPermissions( @@ -40,8 +49,11 @@ const AnalyticsPage = observer(() => { EUserPermissionsLevel.WORKSPACE ); + // derived values + const pageTitle = currentWorkspace?.name + ? t(`workspace_analytics.page_label`, { workspace: currentWorkspace?.name }) + : undefined; const ANALYTICS_TABS = useMemo(() => getAnalyticsTabs(t), [t]); - const tabs = useMemo( () => ANALYTICS_TABS.map((tab) => ({ @@ -49,13 +61,13 @@ const AnalyticsPage = observer(() => { label: tab.label, content: , onClick: () => { - router.push(`?tab=${tab.key}`); + router.push(`/${currentWorkspace?.slug}/analytics/${tab.key}`); }, isDisabled: tab.isDisabled, })), - [ANALYTICS_TABS, router] + [ANALYTICS_TABS, router, currentWorkspace?.slug] ); - const defaultTab = searchParams.get("tab") || ANALYTICS_TABS[0].key; + const defaultTab = tabId || ANALYTICS_TABS[0].key; return ( <> @@ -70,8 +82,9 @@ const AnalyticsPage = observer(() => { defaultTab={defaultTab} size="md" tabListContainerClassName="px-6 py-2 border-b border-custom-border-200 flex items-center justify-between" - tabListClassName="my-2 max-w-36" - tabPanelClassName="h-full w-full overflow-hidden overflow-y-auto" + tabListClassName="my-2 w-auto" + tabClassName="px-3" + tabPanelClassName="h-full overflow-hidden overflow-y-auto px-2" storeInLocalStorage={false} actions={} /> diff --git a/web/next.config.js b/web/next.config.js index 41ed049c5..34c3c3a7f 100644 --- a/web/next.config.js +++ b/web/next.config.js @@ -53,6 +53,11 @@ const nextConfig = { destination: "/:workspaceSlug/settings/projects/:projectId/:path*", permanent: true, }, + { + source: "/:workspaceSlug/analytics", + destination: "/:workspaceSlug/analytics/overview", + permanent: true, + }, { source: "/:workspaceSlug/settings/api-tokens", destination: "/:workspaceSlug/settings/account/api-tokens", From 79c2dfd293ee55fc2c7fa1ab3ec03006b1b2e5d2 Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Tue, 24 Jun 2025 14:19:00 +0530 Subject: [PATCH 187/201] [WEB-4375] fix: minor issues in timeline layout (#7257) --- .../components/issues/issue-layouts/gantt/base-gantt-root.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/web/core/components/issues/issue-layouts/gantt/base-gantt-root.tsx b/web/core/components/issues/issue-layouts/gantt/base-gantt-root.tsx index 96cff38ad..594bcd2fa 100644 --- a/web/core/components/issues/issue-layouts/gantt/base-gantt-root.tsx +++ b/web/core/components/issues/issue-layouts/gantt/base-gantt-root.tsx @@ -146,6 +146,7 @@ export const BaseGanttRoot: React.FC = observer((props: IBaseGan canLoadMoreBlocks={nextPageResults} updateBlockDates={updateBlockDates} showAllBlocks + enableDependency isEpic={isEpic} />
    From 072f2e2cacc76ba06cae0b6b2f431b58eab89c43 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Tue, 24 Jun 2025 16:08:30 +0530 Subject: [PATCH 188/201] [WEB-4363] regression: analytics tabs improvements #7260 --- .../(projects)/analytics/[tabId]/page.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/web/app/(all)/[workspaceSlug]/(projects)/analytics/[tabId]/page.tsx b/web/app/(all)/[workspaceSlug]/(projects)/analytics/[tabId]/page.tsx index 477534a59..6100bc8d5 100644 --- a/web/app/(all)/[workspaceSlug]/(projects)/analytics/[tabId]/page.tsx +++ b/web/app/(all)/[workspaceSlug]/(projects)/analytics/[tabId]/page.tsx @@ -2,11 +2,11 @@ import { useMemo } from "react"; import { observer } from "mobx-react"; -import { useRouter, useSearchParams } from "next/navigation"; +import { useRouter } from "next/navigation"; // plane package imports import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; -import { Tabs } from "@plane/ui"; +import { type TabItem, Tabs } from "@plane/ui"; // components import AnalyticsFilterActions from "@/components/analytics/analytics-filter-actions"; import { PageHead } from "@/components/core"; @@ -19,6 +19,7 @@ import { getAnalyticsTabs } from "@/plane-web/components/analytics/tabs"; type Props = { params: { tabId: string; + workspaceSlug: string; }; }; @@ -54,7 +55,7 @@ const AnalyticsPage = observer((props: Props) => { ? t(`workspace_analytics.page_label`, { workspace: currentWorkspace?.name }) : undefined; const ANALYTICS_TABS = useMemo(() => getAnalyticsTabs(t), [t]); - const tabs = useMemo( + const tabs: TabItem[] = useMemo( () => ANALYTICS_TABS.map((tab) => ({ key: tab.key, @@ -63,7 +64,7 @@ const AnalyticsPage = observer((props: Props) => { onClick: () => { router.push(`/${currentWorkspace?.slug}/analytics/${tab.key}`); }, - isDisabled: tab.isDisabled, + disabled: tab.isDisabled, })), [ANALYTICS_TABS, router, currentWorkspace?.slug] ); From dee8f00a71bba6f1df5379a00f44134fb65a44fc Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Tue, 24 Jun 2025 20:24:35 +0530 Subject: [PATCH 189/201] [WEB-4384] fix: power k page search redirection #7263 --- web/ce/components/command-palette/helpers.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/web/ce/components/command-palette/helpers.tsx b/web/ce/components/command-palette/helpers.tsx index 141f65bdd..3788e8f2b 100644 --- a/web/ce/components/command-palette/helpers.tsx +++ b/web/ce/components/command-palette/helpers.tsx @@ -93,7 +93,9 @@ export const commandGroups: TCommandGroups = { path: (page: IWorkspacePageSearchResult, projectId: string | undefined) => { let redirectProjectId = page?.project_ids?.[0]; if (!!projectId && page?.project_ids?.includes(projectId)) redirectProjectId = projectId; - return `/${page?.workspace__slug}/projects/${redirectProjectId}/pages/${page?.id}`; + return redirectProjectId + ? `/${page?.workspace__slug}/projects/${redirectProjectId}/pages/${page?.id}` + : `/${page?.workspace__slug}/pages/${page?.id}`; }, title: "Pages", }, From 40c09227264208518ac9734989ef02a6eba9a9f6 Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Tue, 24 Jun 2025 20:25:26 +0530 Subject: [PATCH 190/201] [WEB-4387] fix: global layout when filter is set to `list` (#7264) --- .../issues/issue-layouts/roots/all-issue-layout-root.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/web/core/components/issues/issue-layouts/roots/all-issue-layout-root.tsx b/web/core/components/issues/issue-layouts/roots/all-issue-layout-root.tsx index cb29d55a2..2bebdb96a 100644 --- a/web/core/components/issues/issue-layouts/roots/all-issue-layout-root.tsx +++ b/web/core/components/issues/issue-layouts/roots/all-issue-layout-root.tsx @@ -32,7 +32,7 @@ export const AllIssueLayoutRoot: React.FC = observer((props: Props) => { // Store hooks const { - issuesFilter: { filters, fetchFilters, updateFilters }, + issuesFilter: { fetchFilters, updateFilters }, issues: { clear, groupedIssueIds, fetchIssues, fetchNextIssues }, } = useIssues(EIssuesStoreType.GLOBAL); const { fetchAllGlobalViews, getViewDetailsById } = useGlobalView(); @@ -42,8 +42,7 @@ export const AllIssueLayoutRoot: React.FC = observer((props: Props) => { // Derived values const viewDetails = getViewDetailsById(globalViewId?.toString()); - const issueFilters = globalViewId ? filters?.[globalViewId.toString()] : undefined; - const activeLayout: EIssueLayoutTypes | undefined = issueFilters?.displayFilters?.layout; + const activeLayout: EIssueLayoutTypes | undefined = EIssueLayoutTypes.SPREADSHEET; // Route filters const routeFilters: { [key: string]: string } = {}; From 0e91feacc3b0ea4c2b2bf1c4c3795b2708d1620a Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Wed, 25 Jun 2025 19:09:54 +0530 Subject: [PATCH 191/201] [WIKI-74] fix: peek overview closing on escape key #7259 --- .../extensions/custom-image/components/toolbar/full-screen.tsx | 2 +- web/core/components/issues/peek-overview/view.tsx | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/editor/src/core/extensions/custom-image/components/toolbar/full-screen.tsx b/packages/editor/src/core/extensions/custom-image/components/toolbar/full-screen.tsx index 43f178dc8..1d2e52ca0 100644 --- a/packages/editor/src/core/extensions/custom-image/components/toolbar/full-screen.tsx +++ b/packages/editor/src/core/extensions/custom-image/components/toolbar/full-screen.tsx @@ -189,7 +189,7 @@ export const ImageFullScreenAction: React.FC = (props) => { <>
    = observer((props) => { const handleKeyDown = () => { const slashCommandDropdownElement = document.querySelector("#slash-command"); + const editorImageFullScreenModalElement = document.querySelector(".editor-image-full-screen-modal"); const dropdownElement = document.activeElement?.tagName === "INPUT"; - if (!isAnyModalOpen && !slashCommandDropdownElement && !dropdownElement) { + if (!isAnyModalOpen && !slashCommandDropdownElement && !dropdownElement && !editorImageFullScreenModalElement) { removeRoutePeekId(); const issueElement = document.getElementById(`issue-${issueId}`); if (issueElement) issueElement?.focus(); From b8043f92b13d0b14747baea8bc7411d6643b9acf Mon Sep 17 00:00:00 2001 From: Nikhil <118773738+pablohashescobar@users.noreply.github.com> Date: Wed, 25 Jun 2025 19:10:24 +0530 Subject: [PATCH 192/201] [WEB-4373]: optimize backend query for workspace views and Project gantt view (#7267) * feat: add IssueListDetailSerializer for detailed issue representation - Introduced IssueListDetailSerializer to enhance issue data representation with expanded fields. - Updated issue detail endpoint to utilize the new serializer for improved data handling. - Added methods for retrieving related module, label, and assignee IDs, along with support for expanded relations. * feat: add ViewIssueListSerializer and enhance issue ordering - Introduced ViewIssueListSerializer for improved issue representation, including assignee, label, and module IDs. - Updated WorkspaceViewIssuesViewSet to utilize the new serializer and optimized queryset with prefetching. - Enhanced order_issue_queryset to maintain consistent ordering by created_at alongside other fields. - Modified pagination logic to support total count retrieval for better performance. * fix: optimize issue filtering and pagination logic - Updated WorkspaceViewIssuesViewSet to apply filters more efficiently in the issue query. - Refined pagination logic in OffsetPaginator to ensure consistent behavior using limit instead of cursor.value, improving overall pagination accuracy. * fix: improve pagination logic in OffsetPaginator - Updated the next_cursor calculation to use the length of results instead of cursor.value, ensuring accurate pagination behavior. - Added a comment to clarify the purpose of checking for additional results after the current page. * Move the common permission filters into a separate method * fix: handle deleted related issues in serializers - Updated IssueListDetailSerializer to skip null related issues when building relations. - Enhanced ViewIssueListSerializer to safely access state.group, returning None if state is not present. - Removed unused User import in base.py for cleaner code. --------- Co-authored-by: Dheeraj Kumar Ketireddy --- apiserver/plane/app/serializers/__init__.py | 3 +- apiserver/plane/app/serializers/issue.py | 104 +++++++++ apiserver/plane/app/serializers/view.py | 43 ++++ apiserver/plane/app/views/issue/base.py | 113 +++++----- apiserver/plane/app/views/view/base.py | 236 ++++++-------------- apiserver/plane/utils/order_queryset.py | 15 +- apiserver/plane/utils/paginator.py | 29 ++- 7 files changed, 316 insertions(+), 227 deletions(-) diff --git a/apiserver/plane/app/serializers/__init__.py b/apiserver/plane/app/serializers/__init__.py index 623071573..f0d98886e 100644 --- a/apiserver/plane/app/serializers/__init__.py +++ b/apiserver/plane/app/serializers/__init__.py @@ -39,7 +39,7 @@ from .project import ( ProjectMemberRoleSerializer, ) from .state import StateSerializer, StateLiteSerializer -from .view import IssueViewSerializer +from .view import IssueViewSerializer, ViewIssueListSerializer from .cycle import ( CycleSerializer, CycleIssueSerializer, @@ -74,6 +74,7 @@ from .issue import ( IssueLinkLiteSerializer, IssueVersionDetailSerializer, IssueDescriptionVersionDetailSerializer, + IssueListDetailSerializer, ) from .module import ( diff --git a/apiserver/plane/app/serializers/issue.py b/apiserver/plane/app/serializers/issue.py index e2e943805..c2aca4f81 100644 --- a/apiserver/plane/app/serializers/issue.py +++ b/apiserver/plane/app/serializers/issue.py @@ -725,6 +725,110 @@ class IssueSerializer(DynamicBaseSerializer): read_only_fields = fields +class IssueListDetailSerializer(serializers.Serializer): + + def __init__(self, *args, **kwargs): + # Extract expand parameter and store it as instance variable + self.expand = kwargs.pop("expand", []) or [] + # Extract fields parameter and store it as instance variable + self.fields = kwargs.pop("fields", []) or [] + super().__init__(*args, **kwargs) + + def get_module_ids(self, obj): + return [module.module_id for module in obj.issue_module.all()] + + def get_label_ids(self, obj): + return [label.label_id for label in obj.label_issue.all()] + + def get_assignee_ids(self, obj): + return [assignee.assignee_id for assignee in obj.issue_assignee.all()] + + def to_representation(self, instance): + data = { + # Basic fields + "id": instance.id, + "name": instance.name, + "state_id": instance.state_id, + "sort_order": instance.sort_order, + "completed_at": instance.completed_at, + "estimate_point": instance.estimate_point_id, + "priority": instance.priority, + "start_date": instance.start_date, + "target_date": instance.target_date, + "sequence_id": instance.sequence_id, + "project_id": instance.project_id, + "parent_id": instance.parent_id, + "created_at": instance.created_at, + "updated_at": instance.updated_at, + "created_by": instance.created_by_id, + "updated_by": instance.updated_by_id, + "is_draft": instance.is_draft, + "archived_at": instance.archived_at, + # Computed fields + "cycle_id": instance.cycle_id, + "module_ids": self.get_module_ids(instance), + "label_ids": self.get_label_ids(instance), + "assignee_ids": self.get_assignee_ids(instance), + "sub_issues_count": instance.sub_issues_count, + "attachment_count": instance.attachment_count, + "link_count": instance.link_count, + } + + # Handle expanded fields only when requested - using direct field access + if self.expand: + if "issue_relation" in self.expand: + relations = [] + for relation in instance.issue_relation.all(): + related_issue = relation.related_issue + # If the related issue is deleted, skip it + if not related_issue: + continue + # Add the related issue to the relations list + relations.append( + { + "id": related_issue.id, + "project_id": related_issue.project_id, + "sequence_id": related_issue.sequence_id, + "name": related_issue.name, + "relation_type": relation.relation_type, + "state_id": related_issue.state_id, + "priority": related_issue.priority, + "created_by": related_issue.created_by_id, + "created_at": related_issue.created_at, + "updated_at": related_issue.updated_at, + "updated_by": related_issue.updated_by_id, + } + ) + data["issue_relation"] = relations + + if "issue_related" in self.expand: + related = [] + for relation in instance.issue_related.all(): + issue = relation.issue + # If the related issue is deleted, skip it + if not issue: + continue + # Add the related issue to the related list + related.append( + { + "id": issue.id, + "project_id": issue.project_id, + "sequence_id": issue.sequence_id, + "name": issue.name, + "relation_type": relation.relation_type, + "state_id": issue.state_id, + "priority": issue.priority, + "created_by": issue.created_by_id, + "created_at": issue.created_at, + "updated_at": issue.updated_at, + "updated_by": issue.updated_by_id, + } + ) + data["issue_related"] = related + + return data + + class IssueLiteSerializer(DynamicBaseSerializer): class Meta: model = Issue diff --git a/apiserver/plane/app/serializers/view.py b/apiserver/plane/app/serializers/view.py index b8376a047..94ff68de3 100644 --- a/apiserver/plane/app/serializers/view.py +++ b/apiserver/plane/app/serializers/view.py @@ -7,6 +7,49 @@ from plane.db.models import IssueView from plane.utils.issue_filters import issue_filters +class ViewIssueListSerializer(serializers.Serializer): + + def get_assignee_ids(self, instance): + return [assignee.assignee_id for assignee in instance.issue_assignee.all()] + + def get_label_ids(self, instance): + return [label.label_id for label in instance.label_issue.all()] + + def get_module_ids(self, instance): + return [module.module_id for module in instance.issue_module.all()] + + def to_representation(self, instance): + data = { + "id": instance.id, + "name": instance.name, + "state_id": instance.state_id, + "sort_order": instance.sort_order, + "completed_at": instance.completed_at, + "estimate_point": instance.estimate_point_id, + "priority": instance.priority, + "start_date": instance.start_date, + "target_date": instance.target_date, + "sequence_id": instance.sequence_id, + "project_id": instance.project_id, + "parent_id": instance.parent_id, + "cycle_id": instance.cycle_id, + "sub_issues_count": instance.sub_issues_count, + "created_at": instance.created_at, + "updated_at": instance.updated_at, + "created_by": instance.created_by_id, + "updated_by": instance.updated_by_id, + "attachment_count": instance.attachment_count, + "link_count": instance.link_count, + "is_draft": instance.is_draft, + "archived_at": instance.archived_at, + "state__group": instance.state.group if instance.state else None, + "assignee_ids": self.get_assignee_ids(instance), + "label_ids": self.get_label_ids(instance), + "module_ids": self.get_module_ids(instance), + } + return data + + class IssueViewSerializer(DynamicBaseSerializer): is_favorite = serializers.BooleanField(read_only=True) diff --git a/apiserver/plane/app/views/issue/base.py b/apiserver/plane/app/views/issue/base.py index edce172f9..d0b4e7d5e 100644 --- a/apiserver/plane/app/views/issue/base.py +++ b/apiserver/plane/app/views/issue/base.py @@ -32,6 +32,7 @@ from plane.app.serializers import ( IssueDetailSerializer, IssueUserPropertySerializer, IssueSerializer, + IssueListDetailSerializer, ) from plane.bgtasks.issue_activities_task import issue_activity from plane.db.models import ( @@ -46,6 +47,9 @@ from plane.db.models import ( CycleIssue, UserRecentVisit, ModuleIssue, + IssueRelation, + IssueAssignee, + IssueLabel, ) from plane.utils.grouper import ( issue_group_values, @@ -947,22 +951,22 @@ class IssueDetailEndpoint(BaseAPIView): # check for the project member role, if the role is 5 then check for the guest_view_all_features # if it is true then show all the issues else show only the issues created by the user - project_member_subquery = ProjectMember.objects.filter( - project_id=OuterRef("project_id"), - member=self.request.user, - is_active=True, - ).filter( - Q(role__gt=ROLE.GUEST.value) - | Q( - role=ROLE.GUEST.value, project__guest_view_all_features=True + permission_subquery = ( + Issue.issue_objects.filter( + workspace__slug=slug, project_id=project_id, id=OuterRef("id") ) - ) - - # Main issue query - issue = ( - Issue.issue_objects.filter(workspace__slug=slug, project_id=project_id) .filter( - Q(Exists(project_member_subquery)) + Q( + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, + project__project_projectmember__role__gt=ROLE.GUEST.value, + ) + | Q( + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, + project__project_projectmember__role=ROLE.GUEST.value, + project__guest_view_all_features=True, + ) | Q( project__project_projectmember__member=self.request.user, project__project_projectmember__is_active=True, @@ -971,7 +975,30 @@ class IssueDetailEndpoint(BaseAPIView): created_by=self.request.user, ) ) - .prefetch_related("assignees", "labels", "issue_module__module") + .values("id") + ) + # Main issue query + issue = ( + Issue.issue_objects.filter(workspace__slug=slug, project_id=project_id) + .filter(Exists(permission_subquery)) + .prefetch_related( + Prefetch( + "issue_assignee", + queryset=IssueAssignee.objects.all(), + ) + ) + .prefetch_related( + Prefetch( + "label_issue", + queryset=IssueLabel.objects.all(), + ) + ) + .prefetch_related( + Prefetch( + "issue_module", + queryset=ModuleIssue.objects.all(), + ) + ) .annotate( cycle_id=Subquery( CycleIssue.objects.filter( @@ -979,43 +1006,6 @@ class IssueDetailEndpoint(BaseAPIView): ).values("cycle_id")[:1] ) ) - .annotate( - label_ids=Coalesce( - ArrayAgg( - "labels__id", - distinct=True, - filter=Q( - ~Q(labels__id__isnull=True) - & Q(label_issue__deleted_at__isnull=True) - ), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - assignee_ids=Coalesce( - ArrayAgg( - "assignees__id", - distinct=True, - filter=Q( - ~Q(assignees__id__isnull=True) - & Q(assignees__member_project__is_active=True) - & Q(issue_assignee__deleted_at__isnull=True) - ), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - module_ids=Coalesce( - ArrayAgg( - "issue_module__module_id", - distinct=True, - filter=Q( - ~Q(issue_module__module_id__isnull=True) - & Q(issue_module__module__archived_at__isnull=True) - & Q(issue_module__deleted_at__isnull=True) - ), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - ) .annotate( link_count=IssueLink.objects.filter(issue=OuterRef("id")) .order_by() @@ -1039,6 +1029,23 @@ class IssueDetailEndpoint(BaseAPIView): ) ) + # Add additional prefetch based on expand parameter + if self.expand: + if "issue_relation" in self.expand: + issue = issue.prefetch_related( + Prefetch( + "issue_relation", + queryset=IssueRelation.objects.select_related("related_issue"), + ) + ) + if "issue_related" in self.expand: + issue = issue.prefetch_related( + Prefetch( + "issue_related", + queryset=IssueRelation.objects.select_related("issue"), + ) + ) + issue = issue.filter(**filters) order_by_param = request.GET.get("order_by", "-created_at") # Issue queryset @@ -1049,7 +1056,7 @@ class IssueDetailEndpoint(BaseAPIView): request=request, order_by=order_by_param, queryset=(issue), - on_results=lambda issue: IssueSerializer( + on_results=lambda issue: IssueListDetailSerializer( issue, many=True, fields=self.fields, expand=self.expand ).data, ) diff --git a/apiserver/plane/app/views/view/base.py b/apiserver/plane/app/views/view/base.py index 5d66fc65c..c1dd2631d 100644 --- a/apiserver/plane/app/views/view/base.py +++ b/apiserver/plane/app/views/view/base.py @@ -1,8 +1,13 @@ # Django imports -from django.contrib.postgres.aggregates import ArrayAgg -from django.contrib.postgres.fields import ArrayField -from django.db.models import Exists, F, Func, OuterRef, Q, UUIDField, Value, Subquery -from django.db.models.functions import Coalesce +from django.db.models import ( + Exists, + F, + Func, + OuterRef, + Q, + Subquery, + Prefetch, +) from django.utils.decorators import method_decorator from django.views.decorators.gzip import gzip_page from django.db import transaction @@ -13,7 +18,7 @@ from rest_framework.response import Response # Module imports from plane.app.permissions import allow_permission, ROLE -from plane.app.serializers import IssueViewSerializer +from plane.app.serializers import IssueViewSerializer, ViewIssueListSerializer from plane.db.models import ( Issue, FileAsset, @@ -25,15 +30,12 @@ from plane.db.models import ( Project, CycleIssue, UserRecentVisit, -) -from plane.utils.grouper import ( - issue_group_values, - issue_on_results, - issue_queryset_grouper, + IssueAssignee, + IssueLabel, + ModuleIssue, ) from plane.utils.issue_filters import issue_filters from plane.utils.order_queryset import order_issue_queryset -from plane.utils.paginator import GroupedOffsetPaginator, SubGroupedOffsetPaginator from plane.bgtasks.recent_visited_task import recent_visited_task from .. import BaseViewSet from plane.db.models import UserFavorite @@ -143,6 +145,28 @@ class WorkspaceViewViewSet(BaseViewSet): class WorkspaceViewIssuesViewSet(BaseViewSet): + def _get_project_permission_filters(self): + """ + Get common project permission filters for guest users and role-based access control. + Returns Q object for filtering issues based on user role and project settings. + """ + return Q( + Q( + project__project_projectmember__role=5, + project__guest_view_all_features=True, + ) + | Q( + project__project_projectmember__role=5, + project__guest_view_all_features=False, + created_by=self.request.user, + ) + | + # For other roles (role > 5), show all issues + Q(project__project_projectmember__role__gt=5), + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, + ) + def get_queryset(self): return ( Issue.issue_objects.annotate( @@ -152,12 +176,25 @@ class WorkspaceViewIssuesViewSet(BaseViewSet): .values("count") ) .filter(workspace__slug=self.kwargs.get("slug")) - .filter( - project__project_projectmember__member=self.request.user, - project__project_projectmember__is_active=True, + .select_related("state") + .prefetch_related( + Prefetch( + "issue_assignee", + queryset=IssueAssignee.objects.all(), + ) + ) + .prefetch_related( + Prefetch( + "label_issue", + queryset=IssueLabel.objects.all(), + ) + ) + .prefetch_related( + Prefetch( + "issue_module", + queryset=ModuleIssue.objects.all(), + ) ) - .select_related("workspace", "project", "state", "parent") - .prefetch_related("assignees", "labels", "issue_module__module") .annotate( cycle_id=Subquery( CycleIssue.objects.filter( @@ -186,43 +223,6 @@ class WorkspaceViewIssuesViewSet(BaseViewSet): .annotate(count=Func(F("id"), function="Count")) .values("count") ) - .annotate( - label_ids=Coalesce( - ArrayAgg( - "labels__id", - distinct=True, - filter=Q( - ~Q(labels__id__isnull=True) - & Q(label_issue__deleted_at__isnull=True) - ), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - assignee_ids=Coalesce( - ArrayAgg( - "assignees__id", - distinct=True, - filter=Q( - ~Q(assignees__id__isnull=True) - & Q(assignees__member_project__is_active=True) - & Q(issue_assignee__deleted_at__isnull=True) - ), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - module_ids=Coalesce( - ArrayAgg( - "issue_module__module_id", - distinct=True, - filter=Q( - ~Q(issue_module__module_id__isnull=True) - & Q(issue_module__module__archived_at__isnull=True) - & Q(issue_module__deleted_at__isnull=True) - ), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - ) ) @method_decorator(gzip_page) @@ -233,126 +233,36 @@ class WorkspaceViewIssuesViewSet(BaseViewSet): filters = issue_filters(request.query_params, "GET") order_by_param = request.GET.get("order_by", "-created_at") - issue_queryset = ( - self.get_queryset() - .filter(**filters) - .annotate( - cycle_id=Subquery( - CycleIssue.objects.filter( - issue=OuterRef("id"), deleted_at__isnull=True - ).values("cycle_id")[:1] - ) - ) + issue_queryset = self.get_queryset().filter(**filters) + + # Get common project permission filters + permission_filters = self._get_project_permission_filters() + + # Base query for the counts + total_issue_count = ( + Issue.issue_objects.filter(**filters) + .filter(workspace__slug=slug) + .filter(permission_filters) + .only("id") ) - # check for the project member role, if the role is 5 then check for the guest_view_all_features if it is true then show all the issues else show only the issues created by the user - - issue_queryset = issue_queryset.filter( - Q( - project__project_projectmember__role=5, - project__guest_view_all_features=True, - ) - | Q( - project__project_projectmember__role=5, - project__guest_view_all_features=False, - created_by=self.request.user, - ) - | - # For other roles (role < 5), show all issues - Q(project__project_projectmember__role__gt=5), - project__project_projectmember__member=self.request.user, - project__project_projectmember__is_active=True, - ) + # Apply project permission filters to the issue queryset + issue_queryset = issue_queryset.filter(permission_filters) # Issue queryset issue_queryset, order_by_param = order_issue_queryset( issue_queryset=issue_queryset, order_by_param=order_by_param ) - # Group by - group_by = request.GET.get("group_by", False) - sub_group_by = request.GET.get("sub_group_by", False) - - # issue queryset - issue_queryset = issue_queryset_grouper( - queryset=issue_queryset, group_by=group_by, sub_group_by=sub_group_by + # List Paginate + return self.paginate( + order_by=order_by_param, + request=request, + queryset=issue_queryset, + on_results=lambda issues: ViewIssueListSerializer(issues, many=True).data, + total_count_queryset=total_issue_count, ) - if group_by: - # Check group and sub group value paginate - if sub_group_by: - if group_by == sub_group_by: - return Response( - { - "error": "Group by and sub group by cannot have same parameters" - }, - status=status.HTTP_400_BAD_REQUEST, - ) - else: - # group and sub group pagination - return self.paginate( - request=request, - order_by=order_by_param, - queryset=issue_queryset, - on_results=lambda issues: issue_on_results( - group_by=group_by, issues=issues, sub_group_by=sub_group_by - ), - paginator_cls=SubGroupedOffsetPaginator, - group_by_fields=issue_group_values( - field=group_by, slug=slug, project_id=None, filters=filters - ), - sub_group_by_fields=issue_group_values( - field=sub_group_by, - slug=slug, - project_id=None, - filters=filters, - ), - group_by_field_name=group_by, - sub_group_by_field_name=sub_group_by, - count_filter=Q( - Q(issue_intake__status=1) - | Q(issue_intake__status=-1) - | Q(issue_intake__status=2) - | Q(issue_intake__isnull=True), - archived_at__isnull=True, - is_draft=False, - ), - ) - # Group Paginate - else: - # Group paginate - return self.paginate( - request=request, - order_by=order_by_param, - queryset=issue_queryset, - on_results=lambda issues: issue_on_results( - group_by=group_by, issues=issues, sub_group_by=sub_group_by - ), - paginator_cls=GroupedOffsetPaginator, - group_by_fields=issue_group_values( - field=group_by, slug=slug, project_id=None, filters=filters - ), - group_by_field_name=group_by, - count_filter=Q( - Q(issue_intake__status=1) - | Q(issue_intake__status=-1) - | Q(issue_intake__status=2) - | Q(issue_intake__isnull=True), - archived_at__isnull=True, - is_draft=False, - ), - ) - else: - # List Paginate - return self.paginate( - order_by=order_by_param, - request=request, - queryset=issue_queryset, - on_results=lambda issues: issue_on_results( - group_by=group_by, issues=issues, sub_group_by=sub_group_by - ), - ) - class IssueViewViewSet(BaseViewSet): serializer_class = IssueViewSerializer diff --git a/apiserver/plane/utils/order_queryset.py b/apiserver/plane/utils/order_queryset.py index 174637b74..9138cb31e 100644 --- a/apiserver/plane/utils/order_queryset.py +++ b/apiserver/plane/utils/order_queryset.py @@ -16,7 +16,7 @@ def order_issue_queryset(issue_queryset, order_by_param="-created_at"): ], output_field=CharField(), ) - ).order_by("priority_order") + ).order_by("priority_order", "-created_at") order_by_param = ( "priority_order" if order_by_param.startswith("-") else "-priority_order" ) @@ -36,7 +36,7 @@ def order_issue_queryset(issue_queryset, order_by_param="-created_at"): default=Value(len(state_order)), output_field=CharField(), ) - ).order_by("state_order") + ).order_by("state_order", "-created_at") order_by_param = ( "-state_order" if order_by_param.startswith("-") else "state_order" ) @@ -55,11 +55,18 @@ def order_issue_queryset(issue_queryset, order_by_param="-created_at"): if order_by_param.startswith("-") else order_by_param ) - ).order_by("-min_values" if order_by_param.startswith("-") else "min_values") + ).order_by( + "-min_values" if order_by_param.startswith("-") else "min_values", + "-created_at", + ) order_by_param = ( "-min_values" if order_by_param.startswith("-") else "min_values" ) else: - issue_queryset = issue_queryset.order_by(order_by_param) + # If the order_by_param is created_at, then don't add the -created_at + if "created_at" in order_by_param: + issue_queryset = issue_queryset.order_by(order_by_param) + else: + issue_queryset = issue_queryset.order_by(order_by_param, "-created_at") order_by_param = order_by_param return issue_queryset, order_by_param diff --git a/apiserver/plane/utils/paginator.py b/apiserver/plane/utils/paginator.py index 6bec093e7..2b8c27f76 100644 --- a/apiserver/plane/utils/paginator.py +++ b/apiserver/plane/utils/paginator.py @@ -102,6 +102,7 @@ class OffsetPaginator: max_limit=MAX_LIMIT, max_offset=None, on_results=None, + total_count_queryset=None, ): # Key tuple and remove `-` if descending order by self.key = ( @@ -115,6 +116,7 @@ class OffsetPaginator: self.max_limit = max_limit self.max_offset = max_offset self.on_results = on_results + self.total_count_queryset = total_count_queryset def get_result(self, limit=1000, cursor=None): # offset is page # @@ -138,9 +140,9 @@ class OffsetPaginator: ) # The current page page = cursor.offset - # The offset - offset = cursor.offset * cursor.value - stop = offset + (cursor.value or limit) + 1 + # The offset - use limit instead of cursor.value for consistent pagination + offset = cursor.offset * limit + stop = offset + limit + 1 if self.max_offset is not None and offset >= self.max_offset: raise BadPaginationError("Pagination offset too large") @@ -148,11 +150,21 @@ class OffsetPaginator: raise BadPaginationError("Pagination offset cannot be negative") results = queryset[offset:stop] - if cursor.value != limit: + + # Only slice from the end if we're going backwards (previous page) + if cursor.value != limit and cursor.is_prev: results = results[-(limit + 1) :] + total_count = ( + self.total_count_queryset.count() + if self.total_count_queryset + else results.count() + ) + + # Check if there are more results available after the current page + # Adjust cursors based on the results for pagination - next_cursor = Cursor(limit, page + 1, False, results.count() > limit) + next_cursor = Cursor(limit, page + 1, False, len(results) > limit) # If the page is greater than 0, then set the previous cursor prev_cursor = Cursor(limit, page - 1, True, page > 0) @@ -164,7 +176,7 @@ class OffsetPaginator: results = self.on_results(results) # Count the queryset - count = queryset.count() + count = total_count # Optionally, calculate the total count and max_hits if needed max_hits = math.ceil(count / limit) @@ -196,6 +208,7 @@ class GroupedOffsetPaginator(OffsetPaginator): group_by_field_name, group_by_fields, count_filter, + total_count_queryset=None, *args, **kwargs, ): @@ -404,6 +417,7 @@ class SubGroupedOffsetPaginator(OffsetPaginator): group_by_fields, sub_group_by_fields, count_filter, + total_count_queryset=None, *args, **kwargs, ): @@ -694,6 +708,7 @@ class BasePaginator: sub_group_by_field_name=None, sub_group_by_fields=None, count_filter=None, + total_count_queryset=None, **paginator_kwargs, ): """Paginate the request""" @@ -719,6 +734,8 @@ class BasePaginator: ) paginator_kwargs["sub_group_by_fields"] = sub_group_by_fields + paginator_kwargs["total_count_queryset"] = total_count_queryset + paginator = paginator_cls(**paginator_kwargs) try: From b5538565c70942ebeee3fc164089c4750dff6b65 Mon Sep 17 00:00:00 2001 From: JayashTripathy <76092296+JayashTripathy@users.noreply.github.com> Date: Wed, 25 Jun 2025 23:43:10 +0530 Subject: [PATCH 193/201] [WEB-4371] feat: bar chart component with lollipop shape variant (#7268) * feat: enhance bar chart component with shape variants and custom tooltip * Update packages/propel/src/charts/bar-chart/bar.tsx removed the unknown props Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Update packages/propel/src/charts/bar-chart/bar.tsx removed console log Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * refactor: replace inline percentage text with PercentageText component in bar chart * Added new variant - lollipop-dotted * added some comments --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- packages/propel/src/charts/bar-chart/bar.tsx | 175 ++++++++++++++---- packages/propel/src/charts/bar-chart/root.tsx | 44 ++--- packages/types/src/charts/index.d.ts | 3 + 3 files changed, 159 insertions(+), 63 deletions(-) diff --git a/packages/propel/src/charts/bar-chart/bar.tsx b/packages/propel/src/charts/bar-chart/bar.tsx index 5cc9dac2f..a13e154b2 100644 --- a/packages/propel/src/charts/bar-chart/bar.tsx +++ b/packages/propel/src/charts/bar-chart/bar.tsx @@ -1,10 +1,38 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import React from "react"; // plane imports -import { TChartData } from "@plane/types"; +import { TBarChartShapeVariant, TBarItem, TChartData } from "@plane/types"; import { cn } from "@plane/utils"; -// Helper to calculate percentage +// Constants +const MIN_BAR_HEIGHT_FOR_INTERNAL_TEXT = 14; // Minimum height required to show text inside bar +const BAR_TOP_BORDER_RADIUS = 4; // Border radius for the top of bars +const BAR_BOTTOM_BORDER_RADIUS = 4; // Border radius for the bottom of bars +const DEFAULT_LOLLIPOP_LINE_WIDTH = 2; // Width of lollipop stick +const DEFAULT_LOLLIPOP_CIRCLE_RADIUS = 8; // Radius of lollipop circle + +// Types +interface TShapeProps { + x: number; + y: number; + width: number; + height: number; + dataKey: string; + payload: any; + opacity?: number; +} + +interface TBarProps extends TShapeProps { + fill: string | ((payload: any) => string); + stackKeys: string[]; + textClassName?: string; + showPercentage?: boolean; + showTopBorderRadius?: boolean; + showBottomBorderRadius?: boolean; + dotted?: boolean; +} + +// Helper Functions const calculatePercentage = ( data: TChartData, stackKeys: T[], @@ -14,11 +42,36 @@ const calculatePercentage = ( return total === 0 ? 0 : Math.round((data[currentKey] / total) * 100); }; -const MIN_BAR_HEIGHT_FOR_INTERNAL_TEXT = 14; // Minimum height needed to show text inside -const BAR_TOP_BORDER_RADIUS = 4; // Border radius for each bar -const BAR_BOTTOM_BORDER_RADIUS = 4; // Border radius for each bar +const getBarPath = (x: number, y: number, width: number, height: number, topRadius: number, bottomRadius: number) => ` + M${x},${y + topRadius} + Q${x},${y} ${x + topRadius},${y} + L${x + width - topRadius},${y} + Q${x + width},${y} ${x + width},${y + topRadius} + L${x + width},${y + height - bottomRadius} + Q${x + width},${y + height} ${x + width - bottomRadius},${y + height} + L${x + bottomRadius},${y + height} + Q${x},${y + height} ${x},${y + height - bottomRadius} + Z +`; -export const CustomBar = React.memo((props: any) => { +const PercentageText = ({ + x, + y, + percentage, + className, +}: { + x: number; + y: number; + percentage: number; + className?: string; +}) => ( + + {percentage}% + +); + +// Base Components +const CustomBar = React.memo((props: TBarProps) => { const { opacity, fill, @@ -34,56 +87,104 @@ export const CustomBar = React.memo((props: any) => { showTopBorderRadius, showBottomBorderRadius, } = props; - // Calculate text position - const TEXT_PADDING_Y = Math.min(6, Math.abs(MIN_BAR_HEIGHT_FOR_INTERNAL_TEXT - height / 2)); - const textY = y + height - TEXT_PADDING_Y; // Position inside bar if tall enough - // derived values + + if (!height) return null; + const currentBarPercentage = calculatePercentage(payload, stackKeys, dataKey); + const TEXT_PADDING_Y = Math.min(6, Math.abs(MIN_BAR_HEIGHT_FOR_INTERNAL_TEXT - height / 2)); + const textY = y + height - TEXT_PADDING_Y; + const showText = - // from props showPercentage && - // height of the bar is greater than or equal to the minimum height required to show the text height >= MIN_BAR_HEIGHT_FOR_INTERNAL_TEXT && - // bar percentage text has some value currentBarPercentage !== undefined && - // bar percentage is a number !Number.isNaN(currentBarPercentage); const topBorderRadius = showTopBorderRadius ? BAR_TOP_BORDER_RADIUS : 0; const bottomBorderRadius = showBottomBorderRadius ? BAR_BOTTOM_BORDER_RADIUS : 0; - if (!height) return null; - return ( {showText && ( - - {currentBarPercentage}% - + )} ); }); + +const CustomBarLollipop = React.memo((props: TBarProps) => { + const { fill, x, y, width, height, dataKey, stackKeys, payload, textClassName, showPercentage, dotted } = props; + + const currentBarPercentage = calculatePercentage(payload, stackKeys, dataKey); + + return ( + + + + {showPercentage && ( + + )} + + ); +}); + +// Shape Variants +/** + * Factory function to create shape variants with consistent props + * @param Component - The base component to render + * @param factoryProps - Additional props to pass to the component + * @returns A function that creates the shape with proper props + */ +const createShapeVariant = + (Component: React.ComponentType, factoryProps?: Partial) => + (shapeProps: TShapeProps, bar: TBarItem, stackKeys: string[]): JSX.Element => { + const showTopBorderRadius = bar.showTopBorderRadius?.(shapeProps.dataKey, shapeProps.payload); + const showBottomBorderRadius = bar.showBottomBorderRadius?.(shapeProps.dataKey, shapeProps.payload); + + return ( + + ); + }; + +export const barShapeVariants: Record< + TBarChartShapeVariant, + (props: TShapeProps, bar: TBarItem, stackKeys: string[]) => JSX.Element +> = { + bar: createShapeVariant(CustomBar), // Standard bar with rounded corners + lollipop: createShapeVariant(CustomBarLollipop), // Line with circle at top + "lollipop-dotted": createShapeVariant(CustomBarLollipop, { dotted: true }), // Dotted line lollipop variant +}; + +// Display names CustomBar.displayName = "CustomBar"; +CustomBarLollipop.displayName = "CustomBarLollipop"; diff --git a/packages/propel/src/charts/bar-chart/root.tsx b/packages/propel/src/charts/bar-chart/root.tsx index 8826a55cf..e66242524 100644 --- a/packages/propel/src/charts/bar-chart/root.tsx +++ b/packages/propel/src/charts/bar-chart/root.tsx @@ -19,7 +19,7 @@ import { TBarChartProps } from "@plane/types"; import { getLegendProps } from "../components/legend"; import { CustomXAxisTick, CustomYAxisTick } from "../components/tick"; import { CustomTooltip } from "../components/tooltip"; -import { CustomBar } from "./bar"; +import { barShapeVariants } from "./bar"; export const BarChart = React.memo((props: TBarChartProps) => { const { @@ -36,6 +36,7 @@ export const BarChart = React.memo((props: T y: 10, }, showTooltip = true, + customTooltipContent, } = props; // states const [activeBar, setActiveBar] = useState(null); @@ -66,20 +67,8 @@ export const BarChart = React.memo((props: T stackId={bar.stackId} opacity={!!activeLegend && activeLegend !== bar.key ? 0.1 : 1} shape={(shapeProps: any) => { - const showTopBorderRadius = bar.showTopBorderRadius?.(shapeProps.dataKey, shapeProps.payload); - const showBottomBorderRadius = bar.showBottomBorderRadius?.(shapeProps.dataKey, shapeProps.payload); - - return ( - - ); + const shapeVariant = barShapeVariants[bar.shapeVariant ?? "bar"]; + return shapeVariant(shapeProps, bar, stackKeys); }} className="[&_path]:transition-opacity [&_path]:duration-200" onMouseEnter={() => setActiveBar(bar.key)} @@ -150,17 +139,20 @@ export const BarChart = React.memo((props: T wrapperStyle={{ pointerEvents: "auto", }} - content={({ active, label, payload }) => ( - - )} + content={({ active, label, payload }) => { + if (customTooltipContent) return customTooltipContent({ active, label, payload }); + return ( + + ); + }} /> )} {renderBars} diff --git a/packages/types/src/charts/index.d.ts b/packages/types/src/charts/index.d.ts index 316cfd6b8..685aed214 100644 --- a/packages/types/src/charts/index.d.ts +++ b/packages/types/src/charts/index.d.ts @@ -53,6 +53,8 @@ type TChartProps = { // Bar Chart // ============================================================ +export type TBarChartShapeVariant = "bar" | "lollipop" | "lollipop-dotted"; + export type TBarItem = { key: T; label: string; @@ -62,6 +64,7 @@ export type TBarItem = { stackId: string; showTopBorderRadius?: (barKey: string, payload: any) => boolean; showBottomBorderRadius?: (barKey: string, payload: any) => boolean; + shapeVariant?: TBarChartShapeVariant; }; export type TBarChartProps = TChartProps & { From 25a6cd49fce331aa7fd599f3294932f890074870 Mon Sep 17 00:00:00 2001 From: Akshita Goyal <36129505+gakshita@users.noreply.github.com> Date: Thu, 26 Jun 2025 14:33:13 +0530 Subject: [PATCH 194/201] fix: added @plane/services to the web dependencies (#7271) --- web/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/web/package.json b/web/package.json index 2be55d235..f87326870 100644 --- a/web/package.json +++ b/web/package.json @@ -26,6 +26,7 @@ "@plane/hooks": "*", "@plane/i18n": "*", "@plane/propel": "*", + "@plane/services": "*", "@plane/types": "*", "@plane/ui": "*", "@plane/utils": "*", From e09aeab5b8e22bd730676740be71eda28569bcbd Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Fri, 27 Jun 2025 16:05:38 +0530 Subject: [PATCH 195/201] [WIKI-481] refactor: editor parser #7261 --- packages/editor/src/ce/helpers/parser.ts | 19 ++++++++ packages/editor/src/core/helpers/parser.ts | 50 ++++++++++++---------- 2 files changed, 47 insertions(+), 22 deletions(-) create mode 100644 packages/editor/src/ce/helpers/parser.ts diff --git a/packages/editor/src/ce/helpers/parser.ts b/packages/editor/src/ce/helpers/parser.ts new file mode 100644 index 000000000..5b96c9cfd --- /dev/null +++ b/packages/editor/src/ce/helpers/parser.ts @@ -0,0 +1,19 @@ +/** + * @description function to extract all additional assets from HTML content + * @param htmlContent + * @returns {string[]} array of additional asset sources + */ +export const extractAdditionalAssetsFromHTMLContent = (_htmlContent: string): string[] => []; + +/** + * @description function to replace additional assets in HTML content with new IDs + * @param props + * @returns {string} HTML content with replaced additional assets + */ +export const replaceAdditionalAssetsInHTMLContent = (props: { + htmlContent: string; + assetMap: Record; +}): string => { + const { htmlContent } = props; + return htmlContent; +}; diff --git a/packages/editor/src/core/helpers/parser.ts b/packages/editor/src/core/helpers/parser.ts index 844c7b6af..13b105323 100644 --- a/packages/editor/src/core/helpers/parser.ts +++ b/packages/editor/src/core/helpers/parser.ts @@ -1,40 +1,42 @@ // plane imports import { TDocumentPayload, TDuplicateAssetData, TDuplicateAssetResponse } from "@plane/types"; import { TEditorAssetType } from "@plane/types/src/enums"; +// plane web imports +import { + extractAdditionalAssetsFromHTMLContent, + replaceAdditionalAssetsInHTMLContent, +} from "@/plane-editor/helpers/parser"; // local imports import { convertHTMLDocumentToAllFormats } from "./yjs-utils"; /** - * @description function to extract all image assets from HTML content + * @description function to extract all assets from HTML content * @param htmlContent - * @returns {string[]} array of image asset sources + * @returns {string[]} array of asset sources */ -export const extractImageAssetsFromHTMLContent = (htmlContent: string): string[] => { +const extractAssetsFromHTMLContent = (htmlContent: string): string[] => { // create a DOM parser const parser = new DOMParser(); // parse the HTML string into a DOM document const doc = parser.parseFromString(htmlContent, "text/html"); - // get all image components - const imageComponents = doc.querySelectorAll("image-component"); - // collect all unique image sources - const imageSources = new Set(); + // collect all unique asset sources + const assetSources = new Set(); // extract sources from image components + const imageComponents = doc.querySelectorAll("image-component"); imageComponents.forEach((component) => { const src = component.getAttribute("src"); - if (src) imageSources.add(src); + if (src) assetSources.add(src); }); - return Array.from(imageSources); + const additionalAssetIds = extractAdditionalAssetsFromHTMLContent(htmlContent); + return [...Array.from(assetSources), ...additionalAssetIds]; }; /** - * @description function to replace image assets in HTML content with new IDs + * @description function to replace assets in HTML content with new IDs * @param props - * @returns {string} HTML content with replaced image assets + * @returns {string} HTML content with replaced assets */ -export const replaceImageAssetsInHTMLContent = (props: { - htmlContent: string; - assetMap: Record; -}): string => { +const replaceAssetsInHTMLContent = (props: { htmlContent: string; assetMap: Record }): string => { const { htmlContent, assetMap } = props; // create a DOM parser const parser = new DOMParser(); @@ -48,11 +50,15 @@ export const replaceImageAssetsInHTMLContent = (props: { component.setAttribute("src", assetMap[oldSrc]); } }); - // serialize the document back into a string - return doc.body.innerHTML; + // replace additional sources + const replacedHTMLContent = replaceAdditionalAssetsInHTMLContent({ + htmlContent: doc.body.innerHTML, + assetMap, + }); + return replacedHTMLContent; }; -export const getEditorContentWithReplacedImageAssets = async (props: { +export const getEditorContentWithReplacedAssets = async (props: { descriptionHTML: string; entityId: string; entityType: TEditorAssetType; @@ -63,18 +69,18 @@ export const getEditorContentWithReplacedImageAssets = async (props: { const { descriptionHTML, entityId, entityType, projectId, variant, duplicateAssetService } = props; let replacedDescription = descriptionHTML; // step 1: extract image assets from the description - const imageAssets = extractImageAssetsFromHTMLContent(descriptionHTML); - if (imageAssets.length !== 0) { + const assetIds = extractAssetsFromHTMLContent(descriptionHTML); + if (assetIds.length !== 0) { // step 2: duplicate the image assets const duplicateAssetsResponse = await duplicateAssetService({ entity_id: entityId, entity_type: entityType, project_id: projectId, - asset_ids: imageAssets, + asset_ids: assetIds, }); if (Object.keys(duplicateAssetsResponse ?? {}).length > 0) { // step 3: replace the image assets in the description - replacedDescription = replaceImageAssetsInHTMLContent({ + replacedDescription = replaceAssetsInHTMLContent({ htmlContent: descriptionHTML, assetMap: duplicateAssetsResponse, }); From 4a065e14d04cfa858990049e78c27c0af75cd02a Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Fri, 27 Jun 2025 19:01:00 +0530 Subject: [PATCH 196/201] [WEB-4409] refactor: event constants (#7276) * refactor: event constants * fix: cycle event keys * chore: store extension * chore: update events naming convention --------- Co-authored-by: sriramveeraghanta --- packages/constants/src/event-tracker.ts | 238 ---------------- packages/constants/src/event-tracker/core.ts | 258 ++++++++++++++++++ packages/constants/src/event-tracker/index.ts | 1 + .../[workspaceSlug]/(projects)/header.tsx | 4 +- .../settings/(workspace)/members/page.tsx | 6 +- .../(all)/accounts/forgot-password/page.tsx | 8 +- web/app/(all)/invitations/page.tsx | 6 +- web/app/(all)/onboarding/page.tsx | 4 +- web/app/(all)/sign-up/page.tsx | 4 +- web/app/(home)/page.tsx | 4 +- web/ce/components/projects/create/root.tsx | 4 +- web/ce/store/event-tracker.store.ts | 11 + .../account/auth-forms/password.tsx | 12 +- .../account/auth-forms/unique-code.tsx | 4 +- .../analytics-sidebar/sidebar-header.tsx | 6 +- web/core/components/cycles/delete-modal.tsx | 6 +- .../cycles/list/cycle-list-item-action.tsx | 16 +- web/core/components/cycles/modal.tsx | 10 +- web/core/components/home/root.tsx | 4 +- .../components/inbox/content/issue-root.tsx | 10 +- .../inbox/modals/create-modal/create-root.tsx | 6 +- .../issue-detail-widgets/relations/helper.tsx | 10 +- .../issue-detail-quick-actions.tsx | 15 +- .../components/issues/issue-detail/root.tsx | 48 ++-- .../roots/global-view-root.tsx | 8 +- .../issue-layouts/kanban/base-kanban-root.tsx | 8 +- .../properties/all-properties.tsx | 28 +- .../issues/issue-layouts/quick-add/root.tsx | 6 +- .../components/issues/issue-modal/base.tsx | 12 +- .../components/issues/peek-overview/root.tsx | 42 ++- .../modules/analytics-sidebar/root.tsx | 18 +- .../modules/delete-module-modal.tsx | 6 +- web/core/components/modules/modal.tsx | 10 +- .../components/modules/module-card-item.tsx | 7 +- .../modules/module-list-item-action.tsx | 7 +- .../onboarding/create-workspace.tsx | 19 +- .../components/onboarding/invitations.tsx | 6 +- .../components/onboarding/invite-members.tsx | 8 +- .../components/onboarding/profile-setup.tsx | 22 +- web/core/components/onboarding/tour/root.tsx | 6 +- .../pages/modals/create-page-modal.tsx | 6 +- .../pages/modals/delete-page-modal.tsx | 6 +- .../project-states/create-update/create.tsx | 6 +- .../project-states/create-update/update.tsx | 6 +- .../project-states/options/delete.tsx | 6 +- .../project-states/state-delete-modal.tsx | 6 +- .../project/delete-project-modal.tsx | 6 +- web/core/components/project/form.tsx | 6 +- .../project/leave-project-modal.tsx | 6 +- .../components/project/member-list-item.tsx | 4 +- .../project/send-project-invitation-modal.tsx | 6 +- .../sidebar/header/options/root.tsx | 4 +- .../notification-card/options/archive.tsx | 4 +- .../notification-card/options/read.tsx | 4 +- .../workspace/create-workspace-form.tsx | 17 +- .../workspace/delete-workspace-form.tsx | 6 +- .../workspace/settings/members-list-item.tsx | 4 +- .../workspace/settings/workspace-details.tsx | 6 +- .../workspace/sidebar/user-menu-item.tsx | 4 +- .../workspace/views/delete-view-modal.tsx | 6 +- .../components/workspace/views/header.tsx | 4 +- web/core/components/workspace/views/modal.tsx | 10 +- web/core/hooks/store/use-event-tracker.ts | 2 +- web/core/lib/posthog-provider.tsx | 4 +- web/core/store/event-tracker.store.ts | 12 +- web/core/store/root.store.ts | 6 +- web/ee/store/event-tracker.store.ts | 1 + 67 files changed, 553 insertions(+), 513 deletions(-) delete mode 100644 packages/constants/src/event-tracker.ts create mode 100644 packages/constants/src/event-tracker/core.ts create mode 100644 packages/constants/src/event-tracker/index.ts create mode 100644 web/ce/store/event-tracker.store.ts create mode 100644 web/ee/store/event-tracker.store.ts diff --git a/packages/constants/src/event-tracker.ts b/packages/constants/src/event-tracker.ts deleted file mode 100644 index 4fb1ea15c..000000000 --- a/packages/constants/src/event-tracker.ts +++ /dev/null @@ -1,238 +0,0 @@ -export type IssueEventProps = { - eventName: string; - payload: any; - updates?: any; - path?: string; -}; - -export type EventProps = { - eventName: string; - payload: any; - updates?: any; - path?: string; -}; - -export const getWorkspaceEventPayload = (payload: any) => ({ - workspace_id: payload.id, - created_at: payload.created_at, - updated_at: payload.updated_at, - organization_size: payload.organization_size, - first_time: payload.first_time, - state: payload.state, - element: payload.element, -}); - -export const getProjectEventPayload = (payload: any) => ({ - workspace_id: payload.workspace_id, - project_id: payload.id, - identifier: payload.identifier, - project_visibility: payload.network == 2 ? "Public" : "Private", - changed_properties: payload.changed_properties, - lead_id: payload.project_lead, - created_at: payload.created_at, - updated_at: payload.updated_at, - state: payload.state, - element: payload.element, -}); - -export const getCycleEventPayload = (payload: any) => ({ - workspace_id: payload.workspace_id, - project_id: payload.project, - cycle_id: payload.id, - created_at: payload.created_at, - updated_at: payload.updated_at, - start_date: payload.start_date, - target_date: payload.target_date, - cycle_status: payload.status, - changed_properties: payload.changed_properties, - state: payload.state, - element: payload.element, -}); - -export const getModuleEventPayload = (payload: any) => ({ - workspace_id: payload.workspace_id, - project_id: payload.project, - module_id: payload.id, - created_at: payload.created_at, - updated_at: payload.updated_at, - start_date: payload.start_date, - target_date: payload.target_date, - module_status: payload.status, - lead_id: payload.lead, - changed_properties: payload.changed_properties, - member_ids: payload.members, - state: payload.state, - element: payload.element, -}); - -export const getPageEventPayload = (payload: any) => ({ - workspace_id: payload.workspace_id, - project_id: payload.project, - created_at: payload.created_at, - updated_at: payload.updated_at, - access: payload.access === 0 ? "Public" : "Private", - is_locked: payload.is_locked, - archived_at: payload.archived_at, - created_by: payload.created_by, - state: payload.state, - element: payload.element, -}); - -export const getIssueEventPayload = (props: IssueEventProps) => { - const { eventName, payload, updates, path } = props; - let eventPayload: any = { - issue_id: payload.id, - estimate_point: payload.estimate_point, - link_count: payload.link_count, - target_date: payload.target_date, - is_draft: payload.is_draft, - label_ids: payload.label_ids, - assignee_ids: payload.assignee_ids, - created_at: payload.created_at, - updated_at: payload.updated_at, - sequence_id: payload.sequence_id, - module_ids: payload.module_ids, - sub_issues_count: payload.sub_issues_count, - parent_id: payload.parent_id, - project_id: payload.project_id, - workspace_id: payload.workspace_id, - priority: payload.priority, - state_id: payload.state_id, - start_date: payload.start_date, - attachment_count: payload.attachment_count, - cycle_id: payload.cycle_id, - module_id: payload.module_id, - archived_at: payload.archived_at, - state: payload.state, - view_id: - path?.includes("workspace-views") || path?.includes("views") - ? path.split("/").pop() - : "", - }; - - if (eventName === ISSUE_UPDATED) { - eventPayload = { - ...eventPayload, - ...updates, - updated_from: props.path?.includes("workspace-views") - ? "All views" - : props.path?.includes("cycles") - ? "Cycle" - : props.path?.includes("modules") - ? "Module" - : props.path?.includes("views") - ? "Project view" - : props.path?.includes("inbox") - ? "Inbox" - : props.path?.includes("draft") - ? "Draft" - : "Project", - }; - } - return eventPayload; -}; - -export const getProjectStateEventPayload = (payload: any) => ({ - workspace_id: payload.workspace_id, - project_id: payload.id, - state_id: payload.id, - created_at: payload.created_at, - updated_at: payload.updated_at, - group: payload.group, - color: payload.color, - default: payload.default, - state: payload.state, - element: payload.element, -}); - -// Workspace crud Events -export const WORKSPACE_CREATED = "Workspace created"; -export const WORKSPACE_UPDATED = "Workspace updated"; -export const WORKSPACE_DELETED = "Workspace deleted"; -// Project Events -export const PROJECT_CREATED = "Project created"; -export const PROJECT_UPDATED = "Project updated"; -export const PROJECT_DELETED = "Project deleted"; -// Cycle Events -export const CYCLE_CREATED = "Cycle created"; -export const CYCLE_UPDATED = "Cycle updated"; -export const CYCLE_DELETED = "Cycle deleted"; -export const CYCLE_FAVORITED = "Cycle favorited"; -export const CYCLE_UNFAVORITED = "Cycle unfavorited"; -// Module Events -export const MODULE_CREATED = "Module created"; -export const MODULE_UPDATED = "Module updated"; -export const MODULE_DELETED = "Module deleted"; -export const MODULE_FAVORITED = "Module favorited"; -export const MODULE_UNFAVORITED = "Module unfavorited"; -export const MODULE_LINK_CREATED = "Module link created"; -export const MODULE_LINK_UPDATED = "Module link updated"; -export const MODULE_LINK_DELETED = "Module link deleted"; -// Issue Events -export const ISSUE_CREATED = "Work item created"; -export const ISSUE_UPDATED = "Work item updated"; -export const ISSUE_DELETED = "Work item deleted"; -export const ISSUE_ARCHIVED = "Work item archived"; -export const ISSUE_RESTORED = "Work item restored"; -export const ISSUE_OPENED = "Work item opened"; -// Project State Events -export const STATE_CREATED = "State created"; -export const STATE_UPDATED = "State updated"; -export const STATE_DELETED = "State deleted"; -// Project Page Events -export const PAGE_CREATED = "Page created"; -export const PAGE_UPDATED = "Page updated"; -export const PAGE_DELETED = "Page deleted"; -// Member Events -export const MEMBER_INVITED = "Member invited"; -export const MEMBER_ACCEPTED = "Member accepted"; -export const PROJECT_MEMBER_ADDED = "Project member added"; -export const PROJECT_MEMBER_LEAVE = "Project member leave"; -export const WORKSPACE_MEMBER_LEAVE = "Workspace member leave"; -// Sign-in & Sign-up Events -export const NAVIGATE_TO_SIGNUP = "Navigate to sign-up page"; -export const NAVIGATE_TO_SIGNIN = "Navigate to sign-in page"; -export const CODE_VERIFIED = "Code verified"; -export const SETUP_PASSWORD = "Password setup"; -export const PASSWORD_CREATE_SELECTED = "Password created"; -export const PASSWORD_CREATE_SKIPPED = "Skipped to setup"; -export const SIGN_IN_WITH_PASSWORD = "Sign in with password"; -export const SIGN_UP_WITH_PASSWORD = "Sign up with password"; -export const SIGN_IN_WITH_CODE = "Sign in with magic link"; -export const FORGOT_PASSWORD = "Forgot password clicked"; -export const FORGOT_PASS_LINK = "Forgot password link generated"; -export const NEW_PASS_CREATED = "New password created"; -// Onboarding Events -export const USER_DETAILS = "User details added"; -export const USER_ONBOARDING_COMPLETED = "User onboarding completed"; -// Product Tour Events -export const PRODUCT_TOUR_STARTED = "Product tour started"; -export const PRODUCT_TOUR_COMPLETED = "Product tour completed"; -export const PRODUCT_TOUR_SKIPPED = "Product tour skipped"; -// Dashboard Events -export const CHANGELOG_REDIRECTED = "Changelog redirected"; -export const GITHUB_REDIRECTED = "GitHub redirected"; -// Sidebar Events -export const SIDEBAR_CLICKED = "Sidenav clicked"; -// Global View Events -export const GLOBAL_VIEW_CREATED = "Global view created"; -export const GLOBAL_VIEW_UPDATED = "Global view updated"; -export const GLOBAL_VIEW_DELETED = "Global view deleted"; -export const GLOBAL_VIEW_OPENED = "Global view opened"; -// Notification Events -export const NOTIFICATION_ARCHIVED = "Notification archived"; -export const NOTIFICATION_SNOOZED = "Notification snoozed"; -export const NOTIFICATION_READ = "Notification marked read"; -export const UNREAD_NOTIFICATIONS = "Unread notifications viewed"; -export const NOTIFICATIONS_READ = "All notifications marked read"; -export const SNOOZED_NOTIFICATIONS = "Snoozed notifications viewed"; -export const ARCHIVED_NOTIFICATIONS = "Archived notifications viewed"; -// Groups -export const GROUP_WORKSPACE = "Workspace_metrics"; - -//Elements -export const E_ONBOARDING = "Onboarding"; -export const E_ONBOARDING_STEP_1 = "Onboarding step 1"; -export const E_ONBOARDING_STEP_2 = "Onboarding step 2"; -// Favorites -export const FAVORITE_ADDED = "Favorite added"; diff --git a/packages/constants/src/event-tracker/core.ts b/packages/constants/src/event-tracker/core.ts new file mode 100644 index 000000000..85f2ea6e2 --- /dev/null +++ b/packages/constants/src/event-tracker/core.ts @@ -0,0 +1,258 @@ +export type IssueEventProps = { + eventName: string; + payload: any; + updates?: any; + path?: string; +}; + +export type EventProps = { + eventName: string; + payload: any; + updates?: any; + path?: string; +}; + +export const getWorkspaceEventPayload = (payload: any) => ({ + workspace_id: payload.id, + created_at: payload.created_at, + updated_at: payload.updated_at, + organization_size: payload.organization_size, + first_time: payload.first_time, + state: payload.state, + element: payload.element, +}); + +export const getProjectEventPayload = (payload: any) => ({ + workspace_id: payload.workspace_id, + project_id: payload.id, + identifier: payload.identifier, + project_visibility: payload.network == 2 ? "Public" : "Private", + changed_properties: payload.changed_properties, + lead_id: payload.project_lead, + created_at: payload.created_at, + updated_at: payload.updated_at, + state: payload.state, + element: payload.element, +}); + +export const getCycleEventPayload = (payload: any) => ({ + workspace_id: payload.workspace_id, + project_id: payload.project, + cycle_id: payload.id, + created_at: payload.created_at, + updated_at: payload.updated_at, + start_date: payload.start_date, + target_date: payload.target_date, + cycle_status: payload.status, + changed_properties: payload.changed_properties, + state: payload.state, + element: payload.element, +}); + +export const getModuleEventPayload = (payload: any) => ({ + workspace_id: payload.workspace_id, + project_id: payload.project, + module_id: payload.id, + created_at: payload.created_at, + updated_at: payload.updated_at, + start_date: payload.start_date, + target_date: payload.target_date, + module_status: payload.status, + lead_id: payload.lead, + changed_properties: payload.changed_properties, + member_ids: payload.members, + state: payload.state, + element: payload.element, +}); + +export const getPageEventPayload = (payload: any) => ({ + workspace_id: payload.workspace_id, + project_id: payload.project, + created_at: payload.created_at, + updated_at: payload.updated_at, + access: payload.access === 0 ? "Public" : "Private", + is_locked: payload.is_locked, + archived_at: payload.archived_at, + created_by: payload.created_by, + state: payload.state, + element: payload.element, +}); + +export const getIssueEventPayload = (props: IssueEventProps) => { + const { eventName, payload, updates, path } = props; + let eventPayload: any = { + issue_id: payload.id, + estimate_point: payload.estimate_point, + link_count: payload.link_count, + target_date: payload.target_date, + is_draft: payload.is_draft, + label_ids: payload.label_ids, + assignee_ids: payload.assignee_ids, + created_at: payload.created_at, + updated_at: payload.updated_at, + sequence_id: payload.sequence_id, + module_ids: payload.module_ids, + sub_issues_count: payload.sub_issues_count, + parent_id: payload.parent_id, + project_id: payload.project_id, + workspace_id: payload.workspace_id, + priority: payload.priority, + state_id: payload.state_id, + start_date: payload.start_date, + attachment_count: payload.attachment_count, + cycle_id: payload.cycle_id, + module_id: payload.module_id, + archived_at: payload.archived_at, + state: payload.state, + view_id: path?.includes("workspace-views") || path?.includes("views") ? path.split("/").pop() : "", + }; + + if (eventName === WORK_ITEM_TRACKER_EVENTS.update) { + eventPayload = { + ...eventPayload, + ...updates, + updated_from: props.path?.includes("workspace-views") + ? "All views" + : props.path?.includes("cycles") + ? "Cycle" + : props.path?.includes("modules") + ? "Module" + : props.path?.includes("views") + ? "Project view" + : props.path?.includes("inbox") + ? "Inbox" + : props.path?.includes("draft") + ? "Draft" + : "Project", + }; + } + return eventPayload; +}; + +export const getProjectStateEventPayload = (payload: any) => ({ + workspace_id: payload.workspace_id, + project_id: payload.id, + state_id: payload.id, + created_at: payload.created_at, + updated_at: payload.updated_at, + group: payload.group, + color: payload.color, + default: payload.default, + state: payload.state, + element: payload.element, +}); + +// Dashboard Events +export const GITHUB_REDIRECTED_TRACKER_EVENT = "github_redirected"; +// Groups +export const GROUP_WORKSPACE_TRACKER_EVENT = "workspace_metrics"; + +export const WORKSPACE_TRACKER_EVENTS = { + create: "workspace_created", + update: "workspace_updated", + delete: "workspace_deleted", +}; + +export const PROJECT_TRACKER_EVENTS = { + create: "project_created", + update: "project_updated", + delete: "project_deleted", +}; + +export const CYCLE_TRACKER_EVENTS = { + create: "cycle_created", + update: "cycle_updated", + delete: "cycle_deleted", + favorite: "cycle_favorited", + unfavorite: "cycle_unfavorited", +}; + +export const MODULE_TRACKER_EVENTS = { + create: "module_created", + update: "module_updated", + delete: "module_deleted", + favorite: "module_favorited", + unfavorite: "module_unfavorited", + link: { + create: "module_link_created", + update: "module_link_updated", + delete: "module_link_deleted", + }, +}; + +export const WORK_ITEM_TRACKER_EVENTS = { + create: "work_item_created", + update: "work_item_updated", + delete: "work_item_deleted", + archive: "work_item_archived", + restore: "work_item_restored", +}; + +export const STATE_TRACKER_EVENTS = { + create: "state_created", + update: "state_updated", + delete: "state_deleted", +}; + +export const PROJECT_PAGE_TRACKER_EVENTS = { + create: "project_page_created", + update: "project_page_updated", + delete: "project_page_deleted", +}; + +export const MEMBER_TRACKER_EVENTS = { + invite: "member_invited", + accept: "member_accepted", + project: { + add: "project_member_added", + leave: "project_member_left", + }, + workspace: { + leave: "workspace_member_left", + }, +}; + +export const AUTH_TRACKER_EVENTS = { + navigate: { + sign_up: "navigate_to_sign_up_page", + sign_in: "navigate_to_sign_in_page", + }, + code_verify: "code_verified", + sign_up_with_password: "sign_up_with_password", + sign_in_with_password: "sign_in_with_password", + sign_in_with_code: "sign_in_with_magic_link", + forgot_password: "forgot_password_clicked", +}; + +export const PRODUCT_TOUR_TRACKER_EVENTS = { + start: "product_tour_started", + complete: "product_tour_completed", + skip: "product_tour_skipped", +}; + +export const GLOBAL_VIEW_TOUR_TRACKER_EVENTS = { + create: "global_view_created", + update: "global_view_updated", + delete: "global_view_deleted", + open: "global_view_opened", +}; + +export const NOTIFICATION_TRACKER_EVENTS = { + archive: "notification_archived", + all_marked_read: "all_notifications_marked_read", +}; + +export const USER_TRACKER_EVENTS = { + add_details: "user_details_added", + onboarding_complete: "user_onboarding_completed", +}; + +export const ONBOARDING_TRACKER_EVENTS = { + root: "onboarding", + step_1: "onboarding_step_1", + step_2: "onboarding_step_2", +}; + +export const SIDEBAR_TRACKER_EVENTS = { + click: "sidenav_clicked", +}; diff --git a/packages/constants/src/event-tracker/index.ts b/packages/constants/src/event-tracker/index.ts new file mode 100644 index 000000000..8d119dee8 --- /dev/null +++ b/packages/constants/src/event-tracker/index.ts @@ -0,0 +1 @@ +export * from "./core"; diff --git a/web/app/(all)/[workspaceSlug]/(projects)/header.tsx b/web/app/(all)/[workspaceSlug]/(projects)/header.tsx index ef210ec27..42e3222b3 100644 --- a/web/app/(all)/[workspaceSlug]/(projects)/header.tsx +++ b/web/app/(all)/[workspaceSlug]/(projects)/header.tsx @@ -7,7 +7,7 @@ import { Home } from "lucide-react"; import githubBlackImage from "/public/logos/github-black.png"; import githubWhiteImage from "/public/logos/github-white.png"; // ui -import { GITHUB_REDIRECTED } from "@plane/constants"; +import { GITHUB_REDIRECTED_TRACKER_EVENT } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { Breadcrumbs, Header } from "@plane/ui"; // components @@ -39,7 +39,7 @@ export const WorkspaceDashboardHeader = () => { - captureEvent(GITHUB_REDIRECTED, { + captureEvent(GITHUB_REDIRECTED_TRACKER_EVENT, { element: "navbar", }) } diff --git a/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/members/page.tsx b/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/members/page.tsx index d2c65340e..001c00dd2 100644 --- a/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/members/page.tsx +++ b/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/members/page.tsx @@ -5,7 +5,7 @@ import { observer } from "mobx-react"; import { useParams } from "next/navigation"; import { Search } from "lucide-react"; // types -import { MEMBER_INVITED, EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; +import { EUserPermissions, EUserPermissionsLevel, MEMBER_TRACKER_EVENTS } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { IWorkspaceBulkInviteFormData } from "@plane/types"; // ui @@ -52,7 +52,7 @@ const WorkspaceMembersSettingsPage = observer(() => { return inviteMembersToWorkspace(workspaceSlug.toString(), data) .then(() => { setInviteModal(false); - captureEvent(MEMBER_INVITED, { + captureEvent(MEMBER_TRACKER_EVENTS.invite, { emails: [ ...data.emails.map((email) => ({ email: email.email, @@ -70,7 +70,7 @@ const WorkspaceMembersSettingsPage = observer(() => { }); }) .catch((err) => { - captureEvent(MEMBER_INVITED, { + captureEvent(MEMBER_TRACKER_EVENTS.invite, { emails: [ ...data.emails.map((email) => ({ email: email.email, diff --git a/web/app/(all)/accounts/forgot-password/page.tsx b/web/app/(all)/accounts/forgot-password/page.tsx index 8cf7eae4a..a7e3e7ed4 100644 --- a/web/app/(all)/accounts/forgot-password/page.tsx +++ b/web/app/(all)/accounts/forgot-password/page.tsx @@ -9,7 +9,7 @@ import { Controller, useForm } from "react-hook-form"; // icons import { CircleCheck } from "lucide-react"; // plane imports -import { FORGOT_PASS_LINK, NAVIGATE_TO_SIGNUP } from "@plane/constants"; +import { AUTH_TRACKER_EVENTS } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { Button, Input, TOAST_TYPE, getButtonStyling, setToast } from "@plane/ui"; import { cn, checkEmailValidity } from "@plane/utils"; @@ -71,7 +71,7 @@ const ForgotPasswordPage = observer(() => { email: formData.email, }) .then(() => { - captureEvent(FORGOT_PASS_LINK, { + captureEvent(AUTH_TRACKER_EVENTS.forgot_password, { state: "SUCCESS", }); setToast({ @@ -82,7 +82,7 @@ const ForgotPasswordPage = observer(() => { setResendCodeTimer(30); }) .catch((err) => { - captureEvent(FORGOT_PASS_LINK, { + captureEvent(AUTH_TRACKER_EVENTS.forgot_password, { state: "FAILED", }); setToast({ @@ -120,7 +120,7 @@ const ForgotPasswordPage = observer(() => { {t("auth.common.new_to_plane")} captureEvent(NAVIGATE_TO_SIGNUP, {})} + onClick={() => captureEvent(AUTH_TRACKER_EVENTS.navigate.sign_up, {})} className="font-semibold text-custom-primary-100 hover:underline" > {t("auth.common.create_account")} diff --git a/web/app/(all)/invitations/page.tsx b/web/app/(all)/invitations/page.tsx index 38d50f2e6..5e5f1958f 100644 --- a/web/app/(all)/invitations/page.tsx +++ b/web/app/(all)/invitations/page.tsx @@ -9,7 +9,7 @@ import { useTheme } from "next-themes"; import useSWR, { mutate } from "swr"; import { CheckCircle2 } from "lucide-react"; // plane imports -import { ROLE, MEMBER_ACCEPTED, EUserPermissions } from "@plane/constants"; +import { ROLE, EUserPermissions, MEMBER_TRACKER_EVENTS } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; // types import type { IWorkspaceMemberInvitation } from "@plane/types"; @@ -86,7 +86,7 @@ const UserInvitationsPage = observer(() => { const invitation = invitations?.find((i) => i.id === firstInviteId); const redirectWorkspace = invitations?.find((i) => i.id === firstInviteId)?.workspace; joinWorkspaceMetricGroup(redirectWorkspace?.id); - captureEvent(MEMBER_ACCEPTED, { + captureEvent(MEMBER_TRACKER_EVENTS.accept, { member_id: invitation?.id, // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain role: getUserRole((invitation?.role as unknown as EUserPermissions)!), @@ -112,7 +112,7 @@ const UserInvitationsPage = observer(() => { }); }) .catch(() => { - captureEvent(MEMBER_ACCEPTED, { + captureEvent(MEMBER_TRACKER_EVENTS.accept, { project_id: undefined, accepted_from: "App", state: "FAILED", diff --git a/web/app/(all)/onboarding/page.tsx b/web/app/(all)/onboarding/page.tsx index 2211ad07d..078d6fefa 100644 --- a/web/app/(all)/onboarding/page.tsx +++ b/web/app/(all)/onboarding/page.tsx @@ -4,7 +4,7 @@ import { useEffect, useState } from "react"; import { observer } from "mobx-react"; import useSWR from "swr"; // types -import { USER_ONBOARDING_COMPLETED } from "@plane/constants"; +import { USER_TRACKER_EVENTS } from "@plane/constants"; import { TOnboardingSteps, TUserProfile } from "@plane/types"; // ui import { TOAST_TYPE, setToast } from "@plane/ui"; @@ -73,7 +73,7 @@ const OnboardingPage = observer(() => { await finishUserOnboarding() .then(() => { - captureEvent(USER_ONBOARDING_COMPLETED, { + captureEvent(USER_TRACKER_EVENTS.onboarding_complete, { email: user.email, user_id: user.id, status: "SUCCESS", diff --git a/web/app/(all)/sign-up/page.tsx b/web/app/(all)/sign-up/page.tsx index aa18cf085..786b97384 100644 --- a/web/app/(all)/sign-up/page.tsx +++ b/web/app/(all)/sign-up/page.tsx @@ -6,7 +6,7 @@ import Link from "next/link"; // ui import { useTheme } from "next-themes"; // components -import { NAVIGATE_TO_SIGNIN } from "@plane/constants"; +import { AUTH_TRACKER_EVENTS } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { AuthRoot } from "@/components/account"; // constants @@ -54,7 +54,7 @@ const SignInPage = observer(() => { {t("auth.common.already_have_an_account")} captureEvent(NAVIGATE_TO_SIGNIN, {})} + onClick={() => captureEvent(AUTH_TRACKER_EVENTS.navigate.sign_in, {})} className="font-semibold text-custom-primary-100 hover:underline" > {t("auth.common.login")} diff --git a/web/app/(home)/page.tsx b/web/app/(home)/page.tsx index aac36f0a1..21e5c12d0 100644 --- a/web/app/(home)/page.tsx +++ b/web/app/(home)/page.tsx @@ -6,7 +6,7 @@ import Link from "next/link"; // ui import { useTheme } from "next-themes"; // components -import { NAVIGATE_TO_SIGNUP } from "@plane/constants"; +import { AUTH_TRACKER_EVENTS } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { AuthRoot } from "@/components/account"; import { PageHead } from "@/components/core"; @@ -63,7 +63,7 @@ const HomePage = observer(() => { {t("auth.common.new_to_plane")} captureEvent(NAVIGATE_TO_SIGNUP, {})} + onClick={() => captureEvent(AUTH_TRACKER_EVENTS.navigate.sign_up, {})} className="font-semibold text-custom-primary-100 hover:underline" > {t("auth.common.create_account")} diff --git a/web/ce/components/projects/create/root.tsx b/web/ce/components/projects/create/root.tsx index 72f378ea2..18f4878fb 100644 --- a/web/ce/components/projects/create/root.tsx +++ b/web/ce/components/projects/create/root.tsx @@ -3,7 +3,7 @@ import { useState, FC } from "react"; import { observer } from "mobx-react"; import { FormProvider, useForm } from "react-hook-form"; -import { PROJECT_CREATED, DEFAULT_PROJECT_FORM_VALUES } from "@plane/constants"; +import { DEFAULT_PROJECT_FORM_VALUES, PROJECT_TRACKER_EVENTS } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; // ui import { setToast, TOAST_TYPE } from "@plane/ui"; @@ -75,7 +75,7 @@ export const CreateProjectForm: FC = observer((props) = state: "SUCCESS", }; captureProjectEvent({ - eventName: PROJECT_CREATED, + eventName: PROJECT_TRACKER_EVENTS.create, payload: newPayload, }); setToast({ diff --git a/web/ce/store/event-tracker.store.ts b/web/ce/store/event-tracker.store.ts new file mode 100644 index 000000000..4f5074dd9 --- /dev/null +++ b/web/ce/store/event-tracker.store.ts @@ -0,0 +1,11 @@ +import { RootStore } from "@/plane-web/store/root.store"; +import { CoreEventTrackerStore, ICoreEventTrackerStore } from "@/store/event-tracker.store"; + +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export interface IEventTrackerStore extends ICoreEventTrackerStore {} + +export class EventTrackerStore extends CoreEventTrackerStore implements IEventTrackerStore { + constructor(_rootStore: RootStore) { + super(_rootStore); + } +} diff --git a/web/core/components/account/auth-forms/password.tsx b/web/core/components/account/auth-forms/password.tsx index b3740acea..3c2927418 100644 --- a/web/core/components/account/auth-forms/password.tsx +++ b/web/core/components/account/auth-forms/password.tsx @@ -6,7 +6,7 @@ import Link from "next/link"; // icons import { Eye, EyeOff, Info, X, XCircle } from "lucide-react"; // plane imports -import { FORGOT_PASSWORD, SIGN_IN_WITH_CODE, SIGN_IN_WITH_PASSWORD, SIGN_UP_WITH_PASSWORD, API_BASE_URL, E_PASSWORD_STRENGTH } from "@plane/constants"; +import { API_BASE_URL, E_PASSWORD_STRENGTH, AUTH_TRACKER_EVENTS } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { Button, Input, Spinner } from "@plane/ui"; import { getPasswordStrength } from "@plane/utils"; @@ -77,7 +77,7 @@ export const AuthPasswordForm: React.FC = observer((props: Props) => { const redirectToUniqueCodeSignIn = async () => { handleAuthStep(EAuthSteps.UNIQUE_CODE); - captureEvent(SIGN_IN_WITH_CODE); + captureEvent(AUTH_TRACKER_EVENTS.sign_in_with_code); }; const passwordSupport = @@ -85,7 +85,7 @@ export const AuthPasswordForm: React.FC = observer((props: Props) => {
    {isSMTPConfigured ? ( captureEvent(FORGOT_PASSWORD)} + onClick={() => captureEvent(AUTH_TRACKER_EVENTS.forgot_password)} href={`/accounts/forgot-password?email=${encodeURIComponent(email)}`} className="text-xs font-medium text-custom-primary-100" > @@ -154,7 +154,11 @@ export const AuthPasswordForm: React.FC = observer((props: Props) => { : true; if (isPasswordValid) { setIsSubmitting(true); - captureEvent(mode === EAuthModes.SIGN_IN ? SIGN_IN_WITH_PASSWORD : SIGN_UP_WITH_PASSWORD); + captureEvent( + mode === EAuthModes.SIGN_IN + ? AUTH_TRACKER_EVENTS.sign_in_with_password + : AUTH_TRACKER_EVENTS.sign_up_with_password + ); if (formRef.current) formRef.current.submit(); // Manually submit the form if the condition is met } else { setBannerMessage(true); diff --git a/web/core/components/account/auth-forms/unique-code.tsx b/web/core/components/account/auth-forms/unique-code.tsx index a6c06b70d..d6e3c91d8 100644 --- a/web/core/components/account/auth-forms/unique-code.tsx +++ b/web/core/components/account/auth-forms/unique-code.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useState } from "react"; import { CircleCheck, XCircle } from "lucide-react"; -import { CODE_VERIFIED, API_BASE_URL } from "@plane/constants"; +import { API_BASE_URL, AUTH_TRACKER_EVENTS } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { Button, Input, Spinner } from "@plane/ui"; // constants @@ -84,7 +84,7 @@ export const AuthUniqueCodeForm: React.FC = (props) => { action={`${API_BASE_URL}/auth/${mode === EAuthModes.SIGN_IN ? "magic-sign-in" : "magic-sign-up"}/`} onSubmit={() => { setIsSubmitting(true); - captureEvent(CODE_VERIFIED, { + captureEvent(AUTH_TRACKER_EVENTS.code_verify, { state: "SUCCESS", first_time: !isExistingEmail, }); diff --git a/web/core/components/cycles/analytics-sidebar/sidebar-header.tsx b/web/core/components/cycles/analytics-sidebar/sidebar-header.tsx index ebf94e0c6..bad633516 100644 --- a/web/core/components/cycles/analytics-sidebar/sidebar-header.tsx +++ b/web/core/components/cycles/analytics-sidebar/sidebar-header.tsx @@ -5,7 +5,7 @@ import { observer } from "mobx-react"; import { Controller, useForm } from "react-hook-form"; import { ArrowRight, ChevronRight } from "lucide-react"; // Plane Imports -import { CYCLE_STATUS, CYCLE_UPDATED, EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; +import { CYCLE_TRACKER_EVENTS, CYCLE_STATUS, EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { ICycle } from "@plane/types"; import { setToast, TOAST_TYPE } from "@plane/ui"; @@ -67,7 +67,7 @@ export const CycleSidebarHeader: FC = observer((props) => { await updateCycleDetails(workspaceSlug.toString(), projectId.toString(), cycleDetails.id.toString(), data) .then((res) => { captureCycleEvent({ - eventName: CYCLE_UPDATED, + eventName: CYCLE_TRACKER_EVENTS.update, payload: { ...res, changed_properties: [changedProperty], @@ -79,7 +79,7 @@ export const CycleSidebarHeader: FC = observer((props) => { .catch(() => { captureCycleEvent({ - eventName: CYCLE_UPDATED, + eventName: CYCLE_TRACKER_EVENTS.update, payload: { ...data, element: "Right side-peek", diff --git a/web/core/components/cycles/delete-modal.tsx b/web/core/components/cycles/delete-modal.tsx index 210b1a13a..f48733f75 100644 --- a/web/core/components/cycles/delete-modal.tsx +++ b/web/core/components/cycles/delete-modal.tsx @@ -4,7 +4,7 @@ import { useState } from "react"; import { observer } from "mobx-react"; import { useParams, useSearchParams } from "next/navigation"; // types -import { PROJECT_ERROR_MESSAGES, CYCLE_DELETED } from "@plane/constants"; +import { PROJECT_ERROR_MESSAGES, CYCLE_TRACKER_EVENTS } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { ICycle } from "@plane/types"; // ui @@ -50,7 +50,7 @@ export const CycleDeleteModal: React.FC = observer((props) => { message: "Cycle deleted successfully.", }); captureCycleEvent({ - eventName: CYCLE_DELETED, + eventName: CYCLE_TRACKER_EVENTS.delete, payload: { ...cycle, state: "SUCCESS" }, }); }) @@ -65,7 +65,7 @@ export const CycleDeleteModal: React.FC = observer((props) => { message: currentError.i18n_message && t(currentError.i18n_message), }); captureCycleEvent({ - eventName: CYCLE_DELETED, + eventName: CYCLE_TRACKER_EVENTS.delete, payload: { ...cycle, state: "FAILED" }, }); }) diff --git a/web/core/components/cycles/list/cycle-list-item-action.tsx b/web/core/components/cycles/list/cycle-list-item-action.tsx index 79b1650e1..c5978ebd3 100644 --- a/web/core/components/cycles/list/cycle-list-item-action.tsx +++ b/web/core/components/cycles/list/cycle-list-item-action.tsx @@ -6,13 +6,7 @@ import { useParams, usePathname, useSearchParams } from "next/navigation"; import { useForm } from "react-hook-form"; import { Eye, Users, ArrowRight, CalendarDays } from "lucide-react"; // types -import { - CYCLE_FAVORITED, - CYCLE_UNFAVORITED, - EUserPermissions, - EUserPermissionsLevel, - IS_FAVORITE_MENU_OPEN, -} from "@plane/constants"; +import { CYCLE_TRACKER_EVENTS, EUserPermissions, EUserPermissionsLevel, IS_FAVORITE_MENU_OPEN } from "@plane/constants"; import { useLocalStorage } from "@plane/hooks"; import { useTranslation } from "@plane/i18n"; import { ICycle, TCycleGroups } from "@plane/types"; @@ -62,7 +56,7 @@ export const CycleListItemAction: FC = observer((props) => { const searchParams = useSearchParams(); const pathname = usePathname(); // store hooks - const { addCycleToFavorites, removeCycleFromFavorites, updateCycleDetails } = useCycle(); + const { addCycleToFavorites, removeCycleFromFavorites } = useCycle(); const { captureEvent } = useEventTracker(); const { allowPermissions } = useUserPermissions(); @@ -75,7 +69,7 @@ export const CycleListItemAction: FC = observer((props) => { const { getUserDetails } = useMember(); // form - const { control, reset, getValues } = useForm({ + const { reset } = useForm({ defaultValues, }); @@ -107,7 +101,7 @@ export const CycleListItemAction: FC = observer((props) => { const addToFavoritePromise = addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId).then( () => { if (!isFavoriteMenuOpen) toggleFavoriteMenu(true); - captureEvent(CYCLE_FAVORITED, { + captureEvent(CYCLE_TRACKER_EVENTS.favorite, { cycle_id: cycleId, element: "List layout", state: "SUCCESS", @@ -137,7 +131,7 @@ export const CycleListItemAction: FC = observer((props) => { projectId.toString(), cycleId ).then(() => { - captureEvent(CYCLE_UNFAVORITED, { + captureEvent(CYCLE_TRACKER_EVENTS.unfavorite, { cycle_id: cycleId, element: "List layout", state: "SUCCESS", diff --git a/web/core/components/cycles/modal.tsx b/web/core/components/cycles/modal.tsx index 15fa4a5b9..003eed531 100644 --- a/web/core/components/cycles/modal.tsx +++ b/web/core/components/cycles/modal.tsx @@ -4,7 +4,7 @@ import React, { useEffect, useState } from "react"; import { format } from "date-fns"; import { mutate } from "swr"; // types -import { CYCLE_CREATED, CYCLE_UPDATED } from "@plane/constants"; +import { CYCLE_TRACKER_EVENTS } from "@plane/constants"; import type { CycleDateCheckData, ICycle, TCycleTabOptions } from "@plane/types"; // ui import { EModalPosition, EModalWidth, ModalCore, TOAST_TYPE, setToast } from "@plane/ui"; @@ -64,7 +64,7 @@ export const CycleCreateUpdateModal: React.FC = (props) => { message: "Cycle created successfully.", }); captureCycleEvent({ - eventName: CYCLE_CREATED, + eventName: CYCLE_TRACKER_EVENTS.create, payload: { ...res, state: "SUCCESS" }, }); }) @@ -75,7 +75,7 @@ export const CycleCreateUpdateModal: React.FC = (props) => { message: err?.detail ?? "Error in creating cycle. Please try again.", }); captureCycleEvent({ - eventName: CYCLE_CREATED, + eventName: CYCLE_TRACKER_EVENTS.create, payload: { ...payload, state: "FAILED" }, }); }); @@ -89,7 +89,7 @@ export const CycleCreateUpdateModal: React.FC = (props) => { .then((res) => { const changed_properties = Object.keys(dirtyFields); captureCycleEvent({ - eventName: CYCLE_UPDATED, + eventName: CYCLE_TRACKER_EVENTS.update, payload: { ...res, changed_properties: changed_properties, state: "SUCCESS" }, }); setToast({ @@ -100,7 +100,7 @@ export const CycleCreateUpdateModal: React.FC = (props) => { }) .catch((err) => { captureCycleEvent({ - eventName: CYCLE_UPDATED, + eventName: CYCLE_TRACKER_EVENTS.update, payload: { ...payload, state: "FAILED" }, }); setToast({ diff --git a/web/core/components/home/root.tsx b/web/core/components/home/root.tsx index c350d3119..4c25bc3f6 100644 --- a/web/core/components/home/root.tsx +++ b/web/core/components/home/root.tsx @@ -2,7 +2,7 @@ import { observer } from "mobx-react"; import { useParams } from "next/navigation"; // components import useSWR from "swr"; -import { PRODUCT_TOUR_COMPLETED } from "@plane/constants"; +import { PRODUCT_TOUR_TRACKER_EVENTS } from "@plane/constants"; import { ContentWrapper } from "@plane/ui"; import { cn } from "@plane/utils"; import { TourRoot } from "@/components/onboarding"; @@ -38,7 +38,7 @@ export const WorkspaceHomeView = observer(() => { const handleTourCompleted = () => { updateTourCompleted() .then(() => { - captureEvent(PRODUCT_TOUR_COMPLETED, { + captureEvent(PRODUCT_TOUR_TRACKER_EVENTS.complete, { user_id: currentUser?.id, state: "SUCCESS", }); diff --git a/web/core/components/inbox/content/issue-root.tsx b/web/core/components/inbox/content/issue-root.tsx index e04698343..7f6558cbb 100644 --- a/web/core/components/inbox/content/issue-root.tsx +++ b/web/core/components/inbox/content/issue-root.tsx @@ -4,7 +4,7 @@ import { Dispatch, SetStateAction, useEffect, useMemo, useRef } from "react"; import { observer } from "mobx-react"; import { usePathname } from "next/navigation"; // plane imports -import { EInboxIssueSource, ISSUE_ARCHIVED, ISSUE_DELETED } from "@plane/constants"; +import { EInboxIssueSource, WORK_ITEM_TRACKER_EVENTS } from "@plane/constants"; import { EditorRefApi } from "@plane/editor"; import { TIssue, TNameDescriptionLoader } from "@plane/types"; import { Loader, TOAST_TYPE, setToast } from "@plane/ui"; @@ -105,7 +105,7 @@ export const InboxIssueMainContent: React.FC = observer((props) => { message: "Work item deleted successfully", }); captureIssueEvent({ - eventName: ISSUE_DELETED, + eventName: WORK_ITEM_TRACKER_EVENTS.delete, payload: { id: _issueId, state: "SUCCESS", element: "Work item detail page" }, path: pathname, }); @@ -117,7 +117,7 @@ export const InboxIssueMainContent: React.FC = observer((props) => { message: "Work item delete failed", }); captureIssueEvent({ - eventName: ISSUE_DELETED, + eventName: WORK_ITEM_TRACKER_EVENTS.delete, payload: { id: _issueId, state: "FAILED", element: "Work item detail page" }, path: pathname, }); @@ -156,14 +156,14 @@ export const InboxIssueMainContent: React.FC = observer((props) => { try { await archiveIssue(workspaceSlug, projectId, issueId); captureIssueEvent({ - eventName: ISSUE_ARCHIVED, + eventName: WORK_ITEM_TRACKER_EVENTS.archive, payload: { id: issueId, state: "SUCCESS", element: "Work item details page" }, path: pathname, }); } catch (error) { console.log("Error in archiving issue:", error); captureIssueEvent({ - eventName: ISSUE_ARCHIVED, + eventName: WORK_ITEM_TRACKER_EVENTS.archive, payload: { id: issueId, state: "FAILED", element: "Work item details page" }, path: pathname, }); diff --git a/web/core/components/inbox/modals/create-modal/create-root.tsx b/web/core/components/inbox/modals/create-modal/create-root.tsx index 1afccf7e0..923a148e3 100644 --- a/web/core/components/inbox/modals/create-modal/create-root.tsx +++ b/web/core/components/inbox/modals/create-modal/create-root.tsx @@ -4,7 +4,7 @@ import { FC, FormEvent, useCallback, useEffect, useRef, useState } from "react"; import { observer } from "mobx-react"; import { usePathname } from "next/navigation"; // plane imports -import { ETabIndices, ISSUE_CREATED } from "@plane/constants"; +import { ETabIndices, WORK_ITEM_TRACKER_EVENTS } from "@plane/constants"; import { EditorRefApi } from "@plane/editor"; // types import { useTranslation } from "@plane/i18n"; @@ -168,7 +168,7 @@ export const InboxIssueCreateRoot: FC = observer((props) setFormData(defaultIssueData); } captureIssueEvent({ - eventName: ISSUE_CREATED, + eventName: WORK_ITEM_TRACKER_EVENTS.create, payload: { ...formData, state: "SUCCESS", @@ -185,7 +185,7 @@ export const InboxIssueCreateRoot: FC = observer((props) .catch((error) => { console.error(error); captureIssueEvent({ - eventName: ISSUE_CREATED, + eventName: WORK_ITEM_TRACKER_EVENTS.create, payload: { ...formData, state: "FAILED", diff --git a/web/core/components/issues/issue-detail-widgets/relations/helper.tsx b/web/core/components/issues/issue-detail-widgets/relations/helper.tsx index f92a3601f..6e68be0d4 100644 --- a/web/core/components/issues/issue-detail-widgets/relations/helper.tsx +++ b/web/core/components/issues/issue-detail-widgets/relations/helper.tsx @@ -2,7 +2,7 @@ import { useMemo } from "react"; import { usePathname } from "next/navigation"; // plane imports -import { EIssueServiceType, ISSUE_DELETED, ISSUE_UPDATED } from "@plane/constants"; +import { EIssueServiceType, WORK_ITEM_TRACKER_EVENTS } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { TIssue, TIssueServiceType } from "@plane/types"; import { TOAST_TYPE, setToast } from "@plane/ui"; @@ -41,7 +41,7 @@ export const useRelationOperations = ( try { await updateIssue(workspaceSlug, projectId, issueId, data); captureIssueEvent({ - eventName: ISSUE_UPDATED, + eventName: WORK_ITEM_TRACKER_EVENTS.update, payload: { ...data, issueId, state: "SUCCESS", element: "Issue detail page" }, updates: { changed_property: Object.keys(data).join(","), @@ -56,7 +56,7 @@ export const useRelationOperations = ( }); } catch { captureIssueEvent({ - eventName: ISSUE_UPDATED, + eventName: WORK_ITEM_TRACKER_EVENTS.update, payload: { state: "FAILED", element: "Issue detail page" }, updates: { changed_property: Object.keys(data).join(","), @@ -75,14 +75,14 @@ export const useRelationOperations = ( try { return removeIssue(workspaceSlug, projectId, issueId).then(() => { captureIssueEvent({ - eventName: ISSUE_DELETED, + eventName: WORK_ITEM_TRACKER_EVENTS.delete, payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" }, path: pathname, }); }); } catch { captureIssueEvent({ - eventName: ISSUE_DELETED, + eventName: WORK_ITEM_TRACKER_EVENTS.delete, payload: { id: issueId, state: "FAILED", element: "Issue detail page" }, path: pathname, }); diff --git a/web/core/components/issues/issue-detail/issue-detail-quick-actions.tsx b/web/core/components/issues/issue-detail/issue-detail-quick-actions.tsx index df536d900..4be7686c2 100644 --- a/web/core/components/issues/issue-detail/issue-detail-quick-actions.tsx +++ b/web/core/components/issues/issue-detail/issue-detail-quick-actions.tsx @@ -5,12 +5,11 @@ import { observer } from "mobx-react"; import { usePathname } from "next/navigation"; import { ArchiveIcon, ArchiveRestoreIcon, LinkIcon, Trash2 } from "lucide-react"; import { - ISSUE_ARCHIVED, - ISSUE_DELETED, ARCHIVABLE_STATE_GROUPS, EIssuesStoreType, EUserPermissions, EUserPermissionsLevel, + WORK_ITEM_TRACKER_EVENTS, } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { TOAST_TYPE, Tooltip, setToast } from "@plane/ui"; @@ -105,19 +104,19 @@ export const IssueDetailQuickActions: FC = observer((props) => { return deleteIssue(workspaceSlug, projectId, issueId).then(() => { router.push(redirectionPath); captureIssueEvent({ - eventName: ISSUE_DELETED, + eventName: WORK_ITEM_TRACKER_EVENTS.delete, payload: { id: issueId, state: "SUCCESS", element: "Work item detail page" }, path: pathname, }); }); - } catch (error) { + } catch { setToast({ title: t("toast.error "), type: TOAST_TYPE.ERROR, message: t("entity.delete.failed", { entity: t("issue.label", { count: 1 }) }), }); captureIssueEvent({ - eventName: ISSUE_DELETED, + eventName: WORK_ITEM_TRACKER_EVENTS.delete, payload: { id: issueId, state: "FAILED", element: "Work item detail page" }, path: pathname, }); @@ -130,13 +129,13 @@ export const IssueDetailQuickActions: FC = observer((props) => { router.push(`/${workspaceSlug}/projects/${projectId}/archives/issues/${issue.id}`); }); captureIssueEvent({ - eventName: ISSUE_ARCHIVED, + eventName: WORK_ITEM_TRACKER_EVENTS.archive, payload: { id: issueId, state: "SUCCESS", element: "Issue details page" }, path: pathname, }); - } catch (error) { + } catch { captureIssueEvent({ - eventName: ISSUE_ARCHIVED, + eventName: WORK_ITEM_TRACKER_EVENTS.archive, payload: { id: issueId, state: "FAILED", element: "Issue details page" }, path: pathname, }); diff --git a/web/core/components/issues/issue-detail/root.tsx b/web/core/components/issues/issue-detail/root.tsx index 4ad3b8d8d..cbd679045 100644 --- a/web/core/components/issues/issue-detail/root.tsx +++ b/web/core/components/issues/issue-detail/root.tsx @@ -4,14 +4,7 @@ import { FC, useMemo } from "react"; import { observer } from "mobx-react"; import { usePathname } from "next/navigation"; // types -import { - EIssuesStoreType, - ISSUE_UPDATED, - ISSUE_DELETED, - ISSUE_ARCHIVED, - EUserPermissions, - EUserPermissionsLevel, -} from "@plane/constants"; +import { EIssuesStoreType, EUserPermissions, EUserPermissionsLevel, WORK_ITEM_TRACKER_EVENTS } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { TIssue } from "@plane/types"; // ui @@ -98,7 +91,7 @@ export const IssueDetailRoot: FC = observer((props) => { try { await updateIssue(workspaceSlug, projectId, issueId, data); captureIssueEvent({ - eventName: ISSUE_UPDATED, + eventName: WORK_ITEM_TRACKER_EVENTS.update, payload: { ...data, issueId, state: "SUCCESS", element: "Issue detail page" }, updates: { changed_property: Object.keys(data).join(","), @@ -109,7 +102,7 @@ export const IssueDetailRoot: FC = observer((props) => { } catch (error) { console.log("Error in updating issue:", error); captureIssueEvent({ - eventName: ISSUE_UPDATED, + eventName: WORK_ITEM_TRACKER_EVENTS.update, payload: { state: "FAILED", element: "Issue detail page" }, updates: { changed_property: Object.keys(data).join(","), @@ -134,7 +127,7 @@ export const IssueDetailRoot: FC = observer((props) => { message: t("entity.delete.success", { entity: t("issue.label") }), }); captureIssueEvent({ - eventName: ISSUE_DELETED, + eventName: WORK_ITEM_TRACKER_EVENTS.delete, payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" }, path: pathname, }); @@ -146,7 +139,7 @@ export const IssueDetailRoot: FC = observer((props) => { message: t("entity.delete.failed", { entity: t("issue.label") }), }); captureIssueEvent({ - eventName: ISSUE_DELETED, + eventName: WORK_ITEM_TRACKER_EVENTS.delete, payload: { id: issueId, state: "FAILED", element: "Issue detail page" }, path: pathname, }); @@ -156,14 +149,14 @@ export const IssueDetailRoot: FC = observer((props) => { try { await archiveIssue(workspaceSlug, projectId, issueId); captureIssueEvent({ - eventName: ISSUE_ARCHIVED, + eventName: WORK_ITEM_TRACKER_EVENTS.archive, payload: { id: issueId, state: "SUCCESS", element: "Issue details page" }, path: pathname, }); } catch (error) { console.log("Error in archiving issue:", error); captureIssueEvent({ - eventName: ISSUE_ARCHIVED, + eventName: WORK_ITEM_TRACKER_EVENTS.archive, payload: { id: issueId, state: "FAILED", element: "Issue details page" }, path: pathname, }); @@ -173,7 +166,7 @@ export const IssueDetailRoot: FC = observer((props) => { try { await addCycleToIssue(workspaceSlug, projectId, cycleId, issueId); captureIssueEvent({ - eventName: ISSUE_UPDATED, + eventName: WORK_ITEM_TRACKER_EVENTS.update, payload: { issueId, state: "SUCCESS", element: "Issue detail page" }, updates: { changed_property: "cycle_id", @@ -181,14 +174,14 @@ export const IssueDetailRoot: FC = observer((props) => { }, path: pathname, }); - } catch (error) { + } catch { setToast({ type: TOAST_TYPE.ERROR, title: t("common.error.label"), message: t("issue.add.cycle.failed"), }); captureIssueEvent({ - eventName: ISSUE_UPDATED, + eventName: WORK_ITEM_TRACKER_EVENTS.update, payload: { state: "FAILED", element: "Issue detail page" }, updates: { changed_property: "cycle_id", @@ -202,7 +195,7 @@ export const IssueDetailRoot: FC = observer((props) => { try { await addIssueToCycle(workspaceSlug, projectId, cycleId, issueIds); captureIssueEvent({ - eventName: ISSUE_UPDATED, + eventName: WORK_ITEM_TRACKER_EVENTS.update, payload: { ...issueIds, state: "SUCCESS", element: "Issue detail page" }, updates: { changed_property: "cycle_id", @@ -210,14 +203,14 @@ export const IssueDetailRoot: FC = observer((props) => { }, path: pathname, }); - } catch (error) { + } catch { setToast({ type: TOAST_TYPE.ERROR, title: t("common.error.label"), message: t("issue.add.cycle.failed"), }); captureIssueEvent({ - eventName: ISSUE_UPDATED, + eventName: WORK_ITEM_TRACKER_EVENTS.update, payload: { state: "FAILED", element: "Issue detail page" }, updates: { changed_property: "cycle_id", @@ -243,7 +236,7 @@ export const IssueDetailRoot: FC = observer((props) => { }); await removeFromCyclePromise; captureIssueEvent({ - eventName: ISSUE_UPDATED, + eventName: WORK_ITEM_TRACKER_EVENTS.update, payload: { issueId, state: "SUCCESS", element: "Issue detail page" }, updates: { changed_property: "cycle_id", @@ -251,9 +244,9 @@ export const IssueDetailRoot: FC = observer((props) => { }, path: pathname, }); - } catch (error) { + } catch { captureIssueEvent({ - eventName: ISSUE_UPDATED, + eventName: WORK_ITEM_TRACKER_EVENTS.update, payload: { state: "FAILED", element: "Issue detail page" }, updates: { changed_property: "cycle_id", @@ -279,7 +272,7 @@ export const IssueDetailRoot: FC = observer((props) => { }); await removeFromModulePromise; captureIssueEvent({ - eventName: ISSUE_UPDATED, + eventName: WORK_ITEM_TRACKER_EVENTS.update, payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" }, updates: { changed_property: "module_id", @@ -287,9 +280,9 @@ export const IssueDetailRoot: FC = observer((props) => { }, path: pathname, }); - } catch (error) { + } catch { captureIssueEvent({ - eventName: ISSUE_UPDATED, + eventName: WORK_ITEM_TRACKER_EVENTS.update, payload: { id: issueId, state: "FAILED", element: "Issue detail page" }, updates: { changed_property: "module_id", @@ -308,7 +301,7 @@ export const IssueDetailRoot: FC = observer((props) => { ) => { const promise = await changeModulesInIssue(workspaceSlug, projectId, issueId, addModuleIds, removeModuleIds); captureIssueEvent({ - eventName: ISSUE_UPDATED, + eventName: WORK_ITEM_TRACKER_EVENTS.update, payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" }, updates: { changed_property: "module_id", @@ -333,6 +326,7 @@ export const IssueDetailRoot: FC = observer((props) => { removeIssueFromModule, captureIssueEvent, pathname, + t, ] ); diff --git a/web/core/components/issues/issue-layouts/filters/applied-filters/roots/global-view-root.tsx b/web/core/components/issues/issue-layouts/filters/applied-filters/roots/global-view-root.tsx index 4086a5c06..6bf8ed361 100644 --- a/web/core/components/issues/issue-layouts/filters/applied-filters/roots/global-view-root.tsx +++ b/web/core/components/issues/issue-layouts/filters/applied-filters/roots/global-view-root.tsx @@ -11,8 +11,10 @@ import { EIssueFilterType, EIssuesStoreType, EViewAccess, - GLOBAL_VIEW_UPDATED, - EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; + EUserPermissions, + EUserPermissionsLevel, + GLOBAL_VIEW_TOUR_TRACKER_EVENTS, +} from "@plane/constants"; import { IIssueFilterOptions, TStaticViewTypes } from "@plane/types"; //ui // components @@ -112,7 +114,7 @@ export const GlobalViewsAppliedFiltersRoot = observer((props: Props) => { updateGlobalView(workspaceSlug.toString(), globalViewId.toString(), viewFilters).then((res) => { if (res) - captureEvent(GLOBAL_VIEW_UPDATED, { + captureEvent(GLOBAL_VIEW_TOUR_TRACKER_EVENTS.update, { view_id: res.id, applied_filters: res.filters, state: "SUCCESS", diff --git a/web/core/components/issues/issue-layouts/kanban/base-kanban-root.tsx b/web/core/components/issues/issue-layouts/kanban/base-kanban-root.tsx index 33ab962a5..d4b29a35d 100644 --- a/web/core/components/issues/issue-layouts/kanban/base-kanban-root.tsx +++ b/web/core/components/issues/issue-layouts/kanban/base-kanban-root.tsx @@ -11,9 +11,9 @@ import { EIssueServiceType, EIssueFilterType, EIssuesStoreType, - ISSUE_DELETED, EUserPermissions, EUserPermissionsLevel, + WORK_ITEM_TRACKER_EVENTS, } from "@plane/constants"; import { DeleteIssueModal } from "@/components/issues"; //constants @@ -152,7 +152,7 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas element, }) ); - }, [scrollableContainerRef?.current]); + }, []); // Make the Issue Delete Box a Drop Target useEffect(() => { @@ -181,7 +181,7 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas }, }) ); - }, [deleteAreaRef?.current, setIsDragOverDelete, setDraggedIssueId, setDeleteIssueModal]); + }, [setIsDragOverDelete, setDraggedIssueId, setDeleteIssueModal]); const renderQuickActions: TRenderQuickActions = useCallback( ({ issue, parentRef, customActionButton }) => ( @@ -210,7 +210,7 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas setDeleteIssueModal(false); setDraggedIssueId(undefined); captureIssueEvent({ - eventName: ISSUE_DELETED, + eventName: WORK_ITEM_TRACKER_EVENTS.delete, payload: { id: draggedIssueId, state: "FAILED", element: "Kanban layout drag & drop" }, path: pathname, }); diff --git a/web/core/components/issues/issue-layouts/properties/all-properties.tsx b/web/core/components/issues/issue-layouts/properties/all-properties.tsx index 3b7e5c90b..e60a0d1a4 100644 --- a/web/core/components/issues/issue-layouts/properties/all-properties.tsx +++ b/web/core/components/issues/issue-layouts/properties/all-properties.tsx @@ -7,13 +7,19 @@ import { useParams, usePathname } from "next/navigation"; // icons import { Layers, Link, Paperclip } from "lucide-react"; // types -import { ISSUE_UPDATED } from "@plane/constants"; +import { WORK_ITEM_TRACKER_EVENTS } from "@plane/constants"; // i18n import { useTranslation } from "@plane/i18n"; import { TIssue, IIssueDisplayProperties, TIssuePriorities } from "@plane/types"; // ui import { Tooltip } from "@plane/ui"; -import { cn, getDate, renderFormattedPayloadDate, generateWorkItemLink, shouldHighlightIssueDueDate } from "@plane/utils"; +import { + cn, + getDate, + renderFormattedPayloadDate, + generateWorkItemLink, + shouldHighlightIssueDueDate, +} from "@plane/utils"; // components import { EstimateDropdown, @@ -103,7 +109,7 @@ export const IssueProperties: React.FC = observer((props) => { if (updateIssue) updateIssue(issue.project_id, issue.id, { state_id: stateId }).then(() => { captureIssueEvent({ - eventName: ISSUE_UPDATED, + eventName: WORK_ITEM_TRACKER_EVENTS.update, payload: { ...issue, state: "SUCCESS", element: currentLayout }, path: pathname, updates: { @@ -118,7 +124,7 @@ export const IssueProperties: React.FC = observer((props) => { if (updateIssue) updateIssue(issue.project_id, issue.id, { priority: value }).then(() => { captureIssueEvent({ - eventName: ISSUE_UPDATED, + eventName: WORK_ITEM_TRACKER_EVENTS.update, payload: { ...issue, state: "SUCCESS", element: currentLayout }, path: pathname, updates: { @@ -133,7 +139,7 @@ export const IssueProperties: React.FC = observer((props) => { if (updateIssue) updateIssue(issue.project_id, issue.id, { label_ids: ids }).then(() => { captureIssueEvent({ - eventName: ISSUE_UPDATED, + eventName: WORK_ITEM_TRACKER_EVENTS.update, payload: { ...issue, state: "SUCCESS", element: currentLayout }, path: pathname, updates: { @@ -148,7 +154,7 @@ export const IssueProperties: React.FC = observer((props) => { if (updateIssue) updateIssue(issue.project_id, issue.id, { assignee_ids: ids }).then(() => { captureIssueEvent({ - eventName: ISSUE_UPDATED, + eventName: WORK_ITEM_TRACKER_EVENTS.update, payload: { ...issue, state: "SUCCESS", element: currentLayout }, path: pathname, updates: { @@ -173,7 +179,7 @@ export const IssueProperties: React.FC = observer((props) => { if (modulesToRemove.length > 0) issueOperations.removeModulesFromIssue(modulesToRemove); captureIssueEvent({ - eventName: ISSUE_UPDATED, + eventName: WORK_ITEM_TRACKER_EVENTS.update, payload: { ...issue, state: "SUCCESS", element: currentLayout }, path: pathname, updates: { changed_property: "module_ids", change_details: { module_ids: moduleIds } }, @@ -189,7 +195,7 @@ export const IssueProperties: React.FC = observer((props) => { else issueOperations.removeIssueFromCycle?.(); captureIssueEvent({ - eventName: ISSUE_UPDATED, + eventName: WORK_ITEM_TRACKER_EVENTS.update, payload: { ...issue, state: "SUCCESS", element: currentLayout }, path: pathname, updates: { changed_property: "cycle", change_details: { cycle_id: cycleId } }, @@ -203,7 +209,7 @@ export const IssueProperties: React.FC = observer((props) => { updateIssue(issue.project_id, issue.id, { start_date: date ? renderFormattedPayloadDate(date) : null }).then( () => { captureIssueEvent({ - eventName: ISSUE_UPDATED, + eventName: WORK_ITEM_TRACKER_EVENTS.update, payload: { ...issue, state: "SUCCESS", element: currentLayout }, path: pathname, updates: { @@ -220,7 +226,7 @@ export const IssueProperties: React.FC = observer((props) => { updateIssue(issue.project_id, issue.id, { target_date: date ? renderFormattedPayloadDate(date) : null }).then( () => { captureIssueEvent({ - eventName: ISSUE_UPDATED, + eventName: WORK_ITEM_TRACKER_EVENTS.update, payload: { ...issue, state: "SUCCESS", element: currentLayout }, path: pathname, updates: { @@ -236,7 +242,7 @@ export const IssueProperties: React.FC = observer((props) => { if (updateIssue) updateIssue(issue.project_id, issue.id, { estimate_point: value }).then(() => { captureIssueEvent({ - eventName: ISSUE_UPDATED, + eventName: WORK_ITEM_TRACKER_EVENTS.update, payload: { ...issue, state: "SUCCESS", element: currentLayout }, path: pathname, updates: { diff --git a/web/core/components/issues/issue-layouts/quick-add/root.tsx b/web/core/components/issues/issue-layouts/quick-add/root.tsx index 28c4edbe5..b6cd83e66 100644 --- a/web/core/components/issues/issue-layouts/quick-add/root.tsx +++ b/web/core/components/issues/issue-layouts/quick-add/root.tsx @@ -6,7 +6,7 @@ import { useParams, usePathname } from "next/navigation"; import { useForm, UseFormRegister } from "react-hook-form"; import { PlusIcon } from "lucide-react"; // plane constants -import { EIssueLayoutTypes, EIssueServiceType, ISSUE_CREATED } from "@plane/constants"; +import { EIssueLayoutTypes, WORK_ITEM_TRACKER_EVENTS } from "@plane/constants"; // i18n import { useTranslation } from "@plane/i18n"; import { IProject, TIssue } from "@plane/types"; @@ -137,14 +137,14 @@ export const QuickAddIssueRoot: FC = observer((props) => { await quickAddPromise .then((res) => { captureIssueEvent({ - eventName: ISSUE_CREATED, + eventName: WORK_ITEM_TRACKER_EVENTS.create, payload: { ...res, state: "SUCCESS", element: ` ${layout} quick add` }, path: pathname, }); }) .catch(() => { captureIssueEvent({ - eventName: ISSUE_CREATED, + eventName: WORK_ITEM_TRACKER_EVENTS.create, payload: { ...payload, state: "FAILED", element: `${layout} quick ad` }, path: pathname, }); diff --git a/web/core/components/issues/issue-modal/base.tsx b/web/core/components/issues/issue-modal/base.tsx index 80370100f..494eb80e7 100644 --- a/web/core/components/issues/issue-modal/base.tsx +++ b/web/core/components/issues/issue-modal/base.tsx @@ -3,7 +3,7 @@ import React, { useEffect, useRef, useState } from "react"; import { observer } from "mobx-react"; import { useParams, usePathname } from "next/navigation"; -import { EIssuesStoreType, ISSUE_CREATED, ISSUE_UPDATED } from "@plane/constants"; +import { EIssuesStoreType, WORK_ITEM_TRACKER_EVENTS } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; // types import type { TBaseIssue, TIssue } from "@plane/types"; @@ -241,7 +241,7 @@ export const CreateUpdateIssueModalBase: React.FC = observer(( ), }); captureIssueEvent({ - eventName: ISSUE_CREATED, + eventName: WORK_ITEM_TRACKER_EVENTS.create, payload: { ...response, state: "SUCCESS" }, path: pathname, }); @@ -257,7 +257,7 @@ export const CreateUpdateIssueModalBase: React.FC = observer(( message: error?.error ?? t(is_draft_issue ? "draft_creation_failed" : "issue_creation_failed"), }); captureIssueEvent({ - eventName: ISSUE_CREATED, + eventName: WORK_ITEM_TRACKER_EVENTS.create, payload: { ...payload, state: "FAILED" }, path: pathname, }); @@ -303,7 +303,7 @@ export const CreateUpdateIssueModalBase: React.FC = observer(( message: t("issue_updated_successfully"), }); captureIssueEvent({ - eventName: ISSUE_UPDATED, + eventName: WORK_ITEM_TRACKER_EVENTS.update, payload: { ...payload, issueId: data.id, state: "SUCCESS" }, path: pathname, }); @@ -316,7 +316,7 @@ export const CreateUpdateIssueModalBase: React.FC = observer(( message: error?.error ?? t("issue_could_not_be_updated"), }); captureIssueEvent({ - eventName: ISSUE_UPDATED, + eventName: WORK_ITEM_TRACKER_EVENTS.update, payload: { ...payload, state: "FAILED" }, path: pathname, }); @@ -334,8 +334,6 @@ export const CreateUpdateIssueModalBase: React.FC = observer(( if (beforeFormSubmit) await beforeFormSubmit(); if (!data?.id) response = await handleCreateIssue(payload, is_draft_issue); else response = await handleUpdateIssue(payload); - } catch (error) { - throw error; } finally { if (response != undefined && onSubmit) await onSubmit(response); } diff --git a/web/core/components/issues/peek-overview/root.tsx b/web/core/components/issues/peek-overview/root.tsx index 5ea7855bb..e7a823902 100644 --- a/web/core/components/issues/peek-overview/root.tsx +++ b/web/core/components/issues/peek-overview/root.tsx @@ -6,13 +6,10 @@ import { usePathname } from "next/navigation"; // plane types import { EIssuesStoreType, - ISSUE_UPDATED, - ISSUE_DELETED, - ISSUE_ARCHIVED, - ISSUE_RESTORED, EUserPermissions, EUserPermissionsLevel, EIssueServiceType, + WORK_ITEM_TRACKER_EVENTS, } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { TIssue, IWorkItemPeekOverview } from "@plane/types"; @@ -26,7 +23,6 @@ import { useEventTracker, useIssueDetail, useIssues, useUserPermissions } from " import { useIssueStoreType } from "@/hooks/use-issue-layout-store"; import { useWorkItemProperties } from "@/plane-web/hooks/use-issue-properties"; - export const IssuePeekOverview: FC = observer((props) => { const { embedIssue = false, @@ -86,7 +82,7 @@ export const IssuePeekOverview: FC = observer((props) => .then(async () => { fetchActivities(workspaceSlug, projectId, issueId); captureIssueEvent({ - eventName: ISSUE_UPDATED, + eventName: WORK_ITEM_TRACKER_EVENTS.update, payload: { ...data, issueId, state: "SUCCESS", element: "Issue peek-overview" }, updates: { changed_property: Object.keys(data).join(","), @@ -97,7 +93,7 @@ export const IssuePeekOverview: FC = observer((props) => }) .catch(() => { captureIssueEvent({ - eventName: ISSUE_UPDATED, + eventName: WORK_ITEM_TRACKER_EVENTS.update, payload: { state: "FAILED", element: "Issue peek-overview" }, path: pathname, }); @@ -113,7 +109,7 @@ export const IssuePeekOverview: FC = observer((props) => try { return issues?.removeIssue(workspaceSlug, projectId, issueId).then(() => { captureIssueEvent({ - eventName: ISSUE_DELETED, + eventName: WORK_ITEM_TRACKER_EVENTS.delete, payload: { id: issueId, state: "SUCCESS", element: "Issue peek-overview" }, path: pathname, }); @@ -126,7 +122,7 @@ export const IssuePeekOverview: FC = observer((props) => message: t("entity.delete.failed", { entity: t("issue.label", { count: 1 }) }), }); captureIssueEvent({ - eventName: ISSUE_DELETED, + eventName: WORK_ITEM_TRACKER_EVENTS.delete, payload: { id: issueId, state: "FAILED", element: "Issue peek-overview" }, path: pathname, }); @@ -137,13 +133,13 @@ export const IssuePeekOverview: FC = observer((props) => if (!issues?.archiveIssue) return; await issues.archiveIssue(workspaceSlug, projectId, issueId); captureIssueEvent({ - eventName: ISSUE_ARCHIVED, + eventName: WORK_ITEM_TRACKER_EVENTS.archive, payload: { id: issueId, state: "SUCCESS", element: "Issue peek-overview" }, path: pathname, }); } catch { captureIssueEvent({ - eventName: ISSUE_ARCHIVED, + eventName: WORK_ITEM_TRACKER_EVENTS.archive, payload: { id: issueId, state: "FAILED", element: "Issue peek-overview" }, path: pathname, }); @@ -158,7 +154,7 @@ export const IssuePeekOverview: FC = observer((props) => message: t("issue.restore.success.message"), }); captureIssueEvent({ - eventName: ISSUE_RESTORED, + eventName: WORK_ITEM_TRACKER_EVENTS.restore, payload: { id: issueId, state: "SUCCESS", element: "Issue peek-overview" }, path: pathname, }); @@ -169,7 +165,7 @@ export const IssuePeekOverview: FC = observer((props) => message: t("issue.restore.failed.message"), }); captureIssueEvent({ - eventName: ISSUE_RESTORED, + eventName: WORK_ITEM_TRACKER_EVENTS.restore, payload: { id: issueId, state: "FAILED", element: "Issue peek-overview" }, path: pathname, }); @@ -180,7 +176,7 @@ export const IssuePeekOverview: FC = observer((props) => await issues.addCycleToIssue(workspaceSlug, projectId, cycleId, issueId); fetchActivities(workspaceSlug, projectId, issueId); captureIssueEvent({ - eventName: ISSUE_UPDATED, + eventName: WORK_ITEM_TRACKER_EVENTS.update, payload: { issueId, state: "SUCCESS", element: "Issue peek-overview" }, updates: { changed_property: "cycle_id", @@ -195,7 +191,7 @@ export const IssuePeekOverview: FC = observer((props) => message: t("issue.add.cycle.failed"), }); captureIssueEvent({ - eventName: ISSUE_UPDATED, + eventName: WORK_ITEM_TRACKER_EVENTS.update, payload: { state: "FAILED", element: "Issue peek-overview" }, updates: { changed_property: "cycle_id", @@ -209,7 +205,7 @@ export const IssuePeekOverview: FC = observer((props) => try { await issues.addIssueToCycle(workspaceSlug, projectId, cycleId, issueIds); captureIssueEvent({ - eventName: ISSUE_UPDATED, + eventName: WORK_ITEM_TRACKER_EVENTS.update, payload: { ...issueIds, state: "SUCCESS", element: "Issue peek-overview" }, updates: { changed_property: "cycle_id", @@ -224,7 +220,7 @@ export const IssuePeekOverview: FC = observer((props) => message: t("issue.add.cycle.failed"), }); captureIssueEvent({ - eventName: ISSUE_UPDATED, + eventName: WORK_ITEM_TRACKER_EVENTS.update, payload: { state: "FAILED", element: "Issue peek-overview" }, updates: { changed_property: "cycle_id", @@ -251,7 +247,7 @@ export const IssuePeekOverview: FC = observer((props) => await removeFromCyclePromise; fetchActivities(workspaceSlug, projectId, issueId); captureIssueEvent({ - eventName: ISSUE_UPDATED, + eventName: WORK_ITEM_TRACKER_EVENTS.update, payload: { issueId, state: "SUCCESS", element: "Issue peek-overview" }, updates: { changed_property: "cycle_id", @@ -261,7 +257,7 @@ export const IssuePeekOverview: FC = observer((props) => }); } catch { captureIssueEvent({ - eventName: ISSUE_UPDATED, + eventName: WORK_ITEM_TRACKER_EVENTS.update, payload: { state: "FAILED", element: "Issue peek-overview" }, updates: { changed_property: "cycle_id", @@ -287,7 +283,7 @@ export const IssuePeekOverview: FC = observer((props) => ); fetchActivities(workspaceSlug, projectId, issueId); captureIssueEvent({ - eventName: ISSUE_UPDATED, + eventName: WORK_ITEM_TRACKER_EVENTS.update, payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" }, updates: { changed_property: "module_id", @@ -314,7 +310,7 @@ export const IssuePeekOverview: FC = observer((props) => await removeFromModulePromise; fetchActivities(workspaceSlug, projectId, issueId); captureIssueEvent({ - eventName: ISSUE_UPDATED, + eventName: WORK_ITEM_TRACKER_EVENTS.update, payload: { id: issueId, state: "SUCCESS", element: "Issue peek-overview" }, updates: { changed_property: "module_id", @@ -324,7 +320,7 @@ export const IssuePeekOverview: FC = observer((props) => }); } catch { captureIssueEvent({ - eventName: ISSUE_UPDATED, + eventName: WORK_ITEM_TRACKER_EVENTS.update, payload: { id: issueId, state: "FAILED", element: "Issue peek-overview" }, updates: { changed_property: "module_id", @@ -335,7 +331,7 @@ export const IssuePeekOverview: FC = observer((props) => } }, }), - [fetchIssue, is_draft, issues, fetchActivities, captureIssueEvent, pathname, removeRoutePeekId, restoreIssue] + [fetchIssue, is_draft, issues, fetchActivities, captureIssueEvent, pathname, removeRoutePeekId, restoreIssue, t] ); useEffect(() => { diff --git a/web/core/components/modules/analytics-sidebar/root.tsx b/web/core/components/modules/analytics-sidebar/root.tsx index bbe1e0d14..8142515b0 100644 --- a/web/core/components/modules/analytics-sidebar/root.tsx +++ b/web/core/components/modules/analytics-sidebar/root.tsx @@ -6,7 +6,13 @@ import { useParams } from "next/navigation"; import { Controller, useForm } from "react-hook-form"; import { CalendarClock, ChevronDown, ChevronRight, Info, Plus, SquareUser, Users } from "lucide-react"; import { Disclosure, Transition } from "@headlessui/react"; -import { MODULE_STATUS, MODULE_LINK_CREATED, MODULE_LINK_DELETED, MODULE_LINK_UPDATED, MODULE_UPDATED, EUserPermissions, EUserPermissionsLevel, EEstimateSystem } from "@plane/constants"; +import { + MODULE_STATUS, + EUserPermissions, + EUserPermissionsLevel, + EEstimateSystem, + MODULE_TRACKER_EVENTS, +} from "@plane/constants"; // plane types import { useTranslation } from "@plane/i18n"; import { ILinkDetails, IModule, ModuleLink } from "@plane/types"; @@ -74,13 +80,13 @@ export const ModuleAnalyticsSidebar: React.FC = observer((props) => { updateModuleDetails(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), data) .then((res) => { captureModuleEvent({ - eventName: MODULE_UPDATED, + eventName: MODULE_TRACKER_EVENTS.update, payload: { ...res, changed_properties: Object.keys(data)[0], element: "Right side-peek", state: "SUCCESS" }, }); }) .catch(() => { captureModuleEvent({ - eventName: MODULE_UPDATED, + eventName: MODULE_TRACKER_EVENTS.update, payload: { ...data, state: "FAILED" }, }); }); @@ -92,7 +98,7 @@ export const ModuleAnalyticsSidebar: React.FC = observer((props) => { const payload = { metadata: {}, ...formData }; await createModuleLink(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), payload).then(() => - captureEvent(MODULE_LINK_CREATED, { + captureEvent(MODULE_TRACKER_EVENTS.link.create, { module_id: moduleId, state: "SUCCESS", }) @@ -106,7 +112,7 @@ export const ModuleAnalyticsSidebar: React.FC = observer((props) => { await updateModuleLink(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), linkId, payload).then( () => - captureEvent(MODULE_LINK_UPDATED, { + captureEvent(MODULE_TRACKER_EVENTS.link.update, { module_id: moduleId, state: "SUCCESS", }) @@ -118,7 +124,7 @@ export const ModuleAnalyticsSidebar: React.FC = observer((props) => { deleteModuleLink(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), linkId) .then(() => { - captureEvent(MODULE_LINK_DELETED, { + captureEvent(MODULE_TRACKER_EVENTS.link.delete, { module_id: moduleId, state: "SUCCESS", }); diff --git a/web/core/components/modules/delete-module-modal.tsx b/web/core/components/modules/delete-module-modal.tsx index b35356f67..28128a584 100644 --- a/web/core/components/modules/delete-module-modal.tsx +++ b/web/core/components/modules/delete-module-modal.tsx @@ -4,7 +4,7 @@ import React, { useState } from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; // types -import { PROJECT_ERROR_MESSAGES, MODULE_DELETED } from "@plane/constants"; +import { MODULE_TRACKER_EVENTS, PROJECT_ERROR_MESSAGES } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import type { IModule } from "@plane/types"; // ui @@ -52,7 +52,7 @@ export const DeleteModuleModal: React.FC = observer((props) => { message: "Module deleted successfully.", }); captureModuleEvent({ - eventName: MODULE_DELETED, + eventName: MODULE_TRACKER_EVENTS.delete, payload: { ...data, state: "SUCCESS" }, }); }) @@ -67,7 +67,7 @@ export const DeleteModuleModal: React.FC = observer((props) => { message: currentError.i18n_message && t(currentError.i18n_message), }); captureModuleEvent({ - eventName: MODULE_DELETED, + eventName: MODULE_TRACKER_EVENTS.delete, payload: { ...data, state: "FAILED" }, }); }) diff --git a/web/core/components/modules/modal.tsx b/web/core/components/modules/modal.tsx index e1bc7c7ef..f8e8378d6 100644 --- a/web/core/components/modules/modal.tsx +++ b/web/core/components/modules/modal.tsx @@ -4,7 +4,7 @@ import React, { useEffect, useState } from "react"; import { observer } from "mobx-react"; import { useForm } from "react-hook-form"; // types -import { MODULE_CREATED, MODULE_UPDATED } from "@plane/constants"; +import { MODULE_TRACKER_EVENTS } from "@plane/constants"; import type { IModule } from "@plane/types"; // ui import { EModalPosition, EModalWidth, ModalCore, TOAST_TYPE, setToast } from "@plane/ui"; @@ -64,7 +64,7 @@ export const CreateUpdateModuleModal: React.FC = observer((props) => { message: "Module created successfully.", }); captureModuleEvent({ - eventName: MODULE_CREATED, + eventName: MODULE_TRACKER_EVENTS.create, payload: { ...res, state: "SUCCESS" }, }); }) @@ -75,7 +75,7 @@ export const CreateUpdateModuleModal: React.FC = observer((props) => { message: err?.detail ?? err?.error ?? "Module could not be created. Please try again.", }); captureModuleEvent({ - eventName: MODULE_CREATED, + eventName: MODULE_TRACKER_EVENTS.create, payload: { ...data, state: "FAILED" }, }); }); @@ -95,7 +95,7 @@ export const CreateUpdateModuleModal: React.FC = observer((props) => { message: "Module updated successfully.", }); captureModuleEvent({ - eventName: MODULE_UPDATED, + eventName: MODULE_TRACKER_EVENTS.update, payload: { ...res, changed_properties: Object.keys(dirtyFields || {}), state: "SUCCESS" }, }); }) @@ -106,7 +106,7 @@ export const CreateUpdateModuleModal: React.FC = observer((props) => { message: err?.detail ?? err?.error ?? "Module could not be updated. Please try again.", }); captureModuleEvent({ - eventName: MODULE_UPDATED, + eventName: MODULE_TRACKER_EVENTS.update, payload: { ...data, state: "FAILED" }, }); }); diff --git a/web/core/components/modules/module-card-item.tsx b/web/core/components/modules/module-card-item.tsx index 59b269c4a..8c8dd33a0 100644 --- a/web/core/components/modules/module-card-item.tsx +++ b/web/core/components/modules/module-card-item.tsx @@ -9,11 +9,10 @@ import { Info, SquareUser } from "lucide-react"; import { MODULE_STATUS, PROGRESS_STATE_GROUPS_DETAILS, - MODULE_FAVORITED, - MODULE_UNFAVORITED, EUserPermissions, EUserPermissionsLevel, IS_FAVORITE_MENU_OPEN, + MODULE_TRACKER_EVENTS, } from "@plane/constants"; import { useLocalStorage } from "@plane/hooks"; import { IModule } from "@plane/types"; @@ -80,7 +79,7 @@ export const ModuleCardItem: React.FC = observer((props) => { const addToFavoritePromise = addModuleToFavorites(workspaceSlug.toString(), projectId.toString(), moduleId).then( () => { if (!storedValue) toggleFavoriteMenu(true); - captureEvent(MODULE_FAVORITED, { + captureEvent(MODULE_TRACKER_EVENTS.favorite, { module_id: moduleId, element: "Grid layout", state: "SUCCESS", @@ -111,7 +110,7 @@ export const ModuleCardItem: React.FC = observer((props) => { projectId.toString(), moduleId ).then(() => { - captureEvent(MODULE_UNFAVORITED, { + captureEvent(MODULE_TRACKER_EVENTS.unfavorite, { module_id: moduleId, element: "Grid layout", state: "SUCCESS", diff --git a/web/core/components/modules/module-list-item-action.tsx b/web/core/components/modules/module-list-item-action.tsx index fe96bdad0..c9ac57da5 100644 --- a/web/core/components/modules/module-list-item-action.tsx +++ b/web/core/components/modules/module-list-item-action.tsx @@ -8,11 +8,10 @@ import { SquareUser } from "lucide-react"; // types import { MODULE_STATUS, - MODULE_FAVORITED, - MODULE_UNFAVORITED, EUserPermissions, EUserPermissionsLevel, IS_FAVORITE_MENU_OPEN, + MODULE_TRACKER_EVENTS, } from "@plane/constants"; import { useLocalStorage } from "@plane/hooks"; import { useTranslation } from "@plane/i18n"; @@ -69,7 +68,7 @@ export const ModuleListItemAction: FC = observer((props) => { () => { // open favorites menu if closed if (!storedValue) toggleFavoriteMenu(true); - captureEvent(MODULE_FAVORITED, { + captureEvent(MODULE_TRACKER_EVENTS.favorite, { module_id: moduleId, element: "Grid layout", state: "SUCCESS", @@ -100,7 +99,7 @@ export const ModuleListItemAction: FC = observer((props) => { projectId.toString(), moduleId ).then(() => { - captureEvent(MODULE_UNFAVORITED, { + captureEvent(MODULE_TRACKER_EVENTS.unfavorite, { module_id: moduleId, element: "Grid layout", state: "SUCCESS", diff --git a/web/core/components/onboarding/create-workspace.tsx b/web/core/components/onboarding/create-workspace.tsx index 7be711a1e..157beec74 100644 --- a/web/core/components/onboarding/create-workspace.tsx +++ b/web/core/components/onboarding/create-workspace.tsx @@ -4,7 +4,12 @@ import { useState } from "react"; import { observer } from "mobx-react"; import { Controller, useForm } from "react-hook-form"; // constants -import { ORGANIZATION_SIZE, RESTRICTED_URLS, WORKSPACE_CREATED, E_ONBOARDING } from "@plane/constants"; +import { + ONBOARDING_TRACKER_EVENTS, + ORGANIZATION_SIZE, + RESTRICTED_URLS, + WORKSPACE_TRACKER_EVENTS, +} from "@plane/constants"; // types import { useTranslation } from "@plane/i18n"; import { IUser, IWorkspace, TOnboardingSteps } from "@plane/types"; @@ -69,12 +74,12 @@ export const CreateWorkspace: React.FC = observer((props) => { message: t("workspace_creation.toast.success.message"), }); captureWorkspaceEvent({ - eventName: WORKSPACE_CREATED, + eventName: WORKSPACE_TRACKER_EVENTS.create, payload: { ...workspaceResponse, state: "SUCCESS", first_time: true, - element: E_ONBOARDING, + element: ONBOARDING_TRACKER_EVENTS.root, }, }); await fetchWorkspaces(); @@ -82,11 +87,11 @@ export const CreateWorkspace: React.FC = observer((props) => { }) .catch(() => { captureWorkspaceEvent({ - eventName: WORKSPACE_CREATED, + eventName: WORKSPACE_TRACKER_EVENTS.create, payload: { state: "FAILED", first_time: true, - element: E_ONBOARDING, + element: ONBOARDING_TRACKER_EVENTS.root, }, }); setToast({ @@ -263,7 +268,9 @@ export const CreateWorkspace: React.FC = observer((props) => { onChange={onChange} label={ ORGANIZATION_SIZE.find((c) => c === value) ?? ( - {t("workspace_creation.form.organization_size.placeholder")} + + {t("workspace_creation.form.organization_size.placeholder")} + ) } buttonClassName="!border-[0.5px] !border-onboarding-border-100 !shadow-none !rounded-md" diff --git a/web/core/components/onboarding/invitations.tsx b/web/core/components/onboarding/invitations.tsx index 0be9ae8db..349b91431 100644 --- a/web/core/components/onboarding/invitations.tsx +++ b/web/core/components/onboarding/invitations.tsx @@ -2,7 +2,7 @@ import React, { useState } from "react"; // plane imports -import { ROLE, MEMBER_ACCEPTED } from "@plane/constants"; +import { ROLE, MEMBER_TRACKER_EVENTS } from "@plane/constants"; // types import { IWorkspaceMemberInvitation } from "@plane/types"; // ui @@ -50,7 +50,7 @@ export const Invitations: React.FC = (props) => { try { await workspaceService.joinWorkspaces({ invitations: invitationsRespond }); - captureEvent(MEMBER_ACCEPTED, { + captureEvent(MEMBER_TRACKER_EVENTS.accept, { member_id: invitation?.id, role: getUserRole(invitation?.role as any), project_id: undefined, @@ -63,7 +63,7 @@ export const Invitations: React.FC = (props) => { await handleNextStep(); } catch (error) { console.error(error); - captureEvent(MEMBER_ACCEPTED, { + captureEvent(MEMBER_TRACKER_EVENTS.accept, { member_id: invitation?.id, role: getUserRole(invitation?.role as any), project_id: undefined, diff --git a/web/core/components/onboarding/invite-members.tsx b/web/core/components/onboarding/invite-members.tsx index 55408c42b..6865b7856 100644 --- a/web/core/components/onboarding/invite-members.tsx +++ b/web/core/components/onboarding/invite-members.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useEffect, useRef, useState } from "react"; +import React, { useEffect, useState } from "react"; import { observer } from "mobx-react"; import Image from "next/image"; import { useTheme } from "next-themes"; @@ -20,7 +20,7 @@ import { usePopper } from "react-popper"; import { Check, ChevronDown, Plus, XCircle } from "lucide-react"; import { Listbox } from "@headlessui/react"; // plane imports -import { ROLE, ROLE_DETAILS, MEMBER_INVITED, EUserPermissions } from "@plane/constants"; +import { ROLE, ROLE_DETAILS, EUserPermissions, MEMBER_TRACKER_EVENTS } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; // types import { IUser, IWorkspace } from "@plane/types"; @@ -311,7 +311,7 @@ export const InviteMembers: React.FC = (props) => { })), }) .then(async () => { - captureEvent(MEMBER_INVITED, { + captureEvent(MEMBER_TRACKER_EVENTS.invite, { emails: [ ...payload.emails.map((email) => ({ email: email.email, @@ -331,7 +331,7 @@ export const InviteMembers: React.FC = (props) => { await nextStep(); }) .catch((err) => { - captureEvent(MEMBER_INVITED, { + captureEvent(MEMBER_TRACKER_EVENTS.invite, { project_id: undefined, state: "FAILED", element: "Onboarding", diff --git a/web/core/components/onboarding/profile-setup.tsx b/web/core/components/onboarding/profile-setup.tsx index 4cbb02555..68c4d7c05 100644 --- a/web/core/components/onboarding/profile-setup.tsx +++ b/web/core/components/onboarding/profile-setup.tsx @@ -6,7 +6,7 @@ import Image from "next/image"; import { useTheme } from "next-themes"; import { Controller, useForm } from "react-hook-form"; import { Eye, EyeOff } from "lucide-react"; -import { USER_DETAILS, E_ONBOARDING_STEP_1, E_ONBOARDING_STEP_2, E_PASSWORD_STRENGTH } from "@plane/constants"; +import { E_PASSWORD_STRENGTH, ONBOARDING_TRACKER_EVENTS, USER_TRACKER_EVENTS } from "@plane/constants"; // types import { useTranslation } from "@plane/i18n"; import { IUser, TUserProfile, TOnboardingSteps } from "@plane/types"; @@ -143,11 +143,11 @@ export const ProfileSetup: React.FC = observer((props) => { updateUserProfile(profileUpdatePayload), totalSteps > 2 && stepChange({ profile_complete: true }), ]); - captureEvent(USER_DETAILS, { + captureEvent(USER_TRACKER_EVENTS.add_details, { use_case: formData.use_case, role: formData.role, state: "SUCCESS", - element: E_ONBOARDING_STEP_1, + element: ONBOARDING_TRACKER_EVENTS.step_1, }); setToast({ type: TOAST_TYPE.SUCCESS, @@ -159,9 +159,9 @@ export const ProfileSetup: React.FC = observer((props) => { finishOnboarding(); } } catch { - captureEvent(USER_DETAILS, { + captureEvent(USER_TRACKER_EVENTS.add_details, { state: "FAILED", - element: E_ONBOARDING_STEP_1, + element: ONBOARDING_TRACKER_EVENTS.step_1, }); setToast({ type: TOAST_TYPE.ERROR, @@ -183,9 +183,9 @@ export const ProfileSetup: React.FC = observer((props) => { formData.password && handleSetPassword(formData.password), ]).then(() => setProfileSetupStep(EProfileSetupSteps.USER_PERSONALIZATION)); } catch { - captureEvent(USER_DETAILS, { + captureEvent(USER_TRACKER_EVENTS.add_details, { state: "FAILED", - element: E_ONBOARDING_STEP_1, + element: ONBOARDING_TRACKER_EVENTS.step_1, }); setToast({ type: TOAST_TYPE.ERROR, @@ -205,11 +205,11 @@ export const ProfileSetup: React.FC = observer((props) => { updateUserProfile(profileUpdatePayload), totalSteps > 2 && stepChange({ profile_complete: true }), ]); - captureEvent(USER_DETAILS, { + captureEvent(USER_TRACKER_EVENTS.add_details, { use_case: formData.use_case, role: formData.role, state: "SUCCESS", - element: E_ONBOARDING_STEP_2, + element: ONBOARDING_TRACKER_EVENTS.step_2, }); setToast({ type: TOAST_TYPE.SUCCESS, @@ -221,9 +221,9 @@ export const ProfileSetup: React.FC = observer((props) => { finishOnboarding(); } } catch { - captureEvent(USER_DETAILS, { + captureEvent(USER_TRACKER_EVENTS.add_details, { state: "FAILED", - element: E_ONBOARDING_STEP_2, + element: ONBOARDING_TRACKER_EVENTS.step_2, }); setToast({ type: TOAST_TYPE.ERROR, diff --git a/web/core/components/onboarding/tour/root.tsx b/web/core/components/onboarding/tour/root.tsx index e118ee64b..90a183e0b 100644 --- a/web/core/components/onboarding/tour/root.tsx +++ b/web/core/components/onboarding/tour/root.tsx @@ -5,7 +5,7 @@ import { observer } from "mobx-react"; import Image, { StaticImageData } from "next/image"; import { X } from "lucide-react"; // ui -import { PRODUCT_TOUR_SKIPPED, PRODUCT_TOUR_STARTED } from "@plane/constants"; +import { PRODUCT_TOUR_TRACKER_EVENTS } from "@plane/constants"; import { Button } from "@plane/ui"; // components import { TourSidebar } from "@/components/onboarding"; @@ -112,7 +112,7 @@ export const TourRoot: React.FC = observer((props) => {
    - {slugError &&

    {t("workspace_creation.errors.validation.url_already_taken")}

    } + {slugError && ( +

    {t("workspace_creation.errors.validation.url_already_taken")}

    + )} {invalidSlug && (

    {t("workspace_creation.errors.validation.url_alphanumeric")}

    )} @@ -221,7 +222,9 @@ export const CreateWorkspaceForm: FC = observer((props) => { onChange={onChange} label={ ORGANIZATION_SIZE.find((c) => c === value) ?? ( - {t("workspace_creation.form.organization_size.placeholder")} + + {t("workspace_creation.form.organization_size.placeholder")} + ) } buttonClassName="!border-[0.5px] !border-custom-border-200 !shadow-none" diff --git a/web/core/components/workspace/delete-workspace-form.tsx b/web/core/components/workspace/delete-workspace-form.tsx index bc99d3598..c668ad300 100644 --- a/web/core/components/workspace/delete-workspace-form.tsx +++ b/web/core/components/workspace/delete-workspace-form.tsx @@ -5,7 +5,7 @@ import { observer } from "mobx-react"; import { Controller, useForm } from "react-hook-form"; import { AlertTriangle } from "lucide-react"; // types -import { WORKSPACE_DELETED } from "@plane/constants"; +import { WORKSPACE_TRACKER_EVENTS } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import type { IWorkspace } from "@plane/types"; // ui @@ -65,7 +65,7 @@ export const DeleteWorkspaceForm: React.FC = observer((props) => { handleClose(); router.push(getWorkspaceRedirectionUrl()); captureWorkspaceEvent({ - eventName: WORKSPACE_DELETED, + eventName: WORKSPACE_TRACKER_EVENTS.delete, payload: { ...data, state: "SUCCESS", @@ -85,7 +85,7 @@ export const DeleteWorkspaceForm: React.FC = observer((props) => { message: t("workspace_settings.settings.general.delete_modal.error_message"), }); captureWorkspaceEvent({ - eventName: WORKSPACE_DELETED, + eventName: WORKSPACE_TRACKER_EVENTS.delete, payload: { ...data, state: "FAILED", diff --git a/web/core/components/workspace/settings/members-list-item.tsx b/web/core/components/workspace/settings/members-list-item.tsx index 31a462c75..978b757b3 100644 --- a/web/core/components/workspace/settings/members-list-item.tsx +++ b/web/core/components/workspace/settings/members-list-item.tsx @@ -4,7 +4,7 @@ import { FC } from "react"; import { isEmpty } from "lodash"; import { observer } from "mobx-react"; // ui -import { WORKSPACE_MEMBER_LEAVE } from "@plane/constants"; +import { MEMBER_TRACKER_EVENTS } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { IWorkspaceMember } from "@plane/types"; import { TOAST_TYPE, Table, setToast } from "@plane/ui"; @@ -45,7 +45,7 @@ export const WorkspaceMembersListItem: FC = observer((props) => { .then(async () => { await fetchCurrentUserSettings(); router.push(getWorkspaceRedirectionUrl()); - captureEvent(WORKSPACE_MEMBER_LEAVE, { + captureEvent(MEMBER_TRACKER_EVENTS.workspace.leave, { state: "SUCCESS", element: "Workspace settings members page", }); diff --git a/web/core/components/workspace/settings/workspace-details.tsx b/web/core/components/workspace/settings/workspace-details.tsx index 8c996b274..e9a83784b 100644 --- a/web/core/components/workspace/settings/workspace-details.tsx +++ b/web/core/components/workspace/settings/workspace-details.tsx @@ -5,7 +5,7 @@ import { observer } from "mobx-react"; import { Controller, useForm } from "react-hook-form"; import { Pencil } from "lucide-react"; // constants -import { ORGANIZATION_SIZE, WORKSPACE_UPDATED, EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; +import { ORGANIZATION_SIZE, EUserPermissions, EUserPermissionsLevel, WORKSPACE_TRACKER_EVENTS } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { IWorkspace } from "@plane/types"; import { Button, CustomSelect, Input, TOAST_TYPE, setToast } from "@plane/ui"; @@ -62,7 +62,7 @@ export const WorkspaceDetails: FC = observer(() => { await updateWorkspace(currentWorkspace.slug, payload) .then((res) => { captureWorkspaceEvent({ - eventName: WORKSPACE_UPDATED, + eventName: WORKSPACE_TRACKER_EVENTS.update, payload: { ...res, state: "SUCCESS", @@ -77,7 +77,7 @@ export const WorkspaceDetails: FC = observer(() => { }) .catch((err) => { captureWorkspaceEvent({ - eventName: WORKSPACE_UPDATED, + eventName: WORKSPACE_TRACKER_EVENTS.update, payload: { state: "FAILED", element: "Workspace general settings page", diff --git a/web/core/components/workspace/sidebar/user-menu-item.tsx b/web/core/components/workspace/sidebar/user-menu-item.tsx index f8f0f120a..3f89dd687 100644 --- a/web/core/components/workspace/sidebar/user-menu-item.tsx +++ b/web/core/components/workspace/sidebar/user-menu-item.tsx @@ -3,7 +3,7 @@ import { observer } from "mobx-react"; import Link from "next/link"; import { useParams, usePathname } from "next/navigation"; // plane imports -import { EUserPermissionsLevel, SIDEBAR_CLICKED, EUserWorkspaceRoles } from "@plane/constants"; +import { EUserPermissionsLevel, EUserWorkspaceRoles, SIDEBAR_TRACKER_EVENTS } from "@plane/constants"; import { usePlatformOS } from "@plane/hooks"; import { useTranslation } from "@plane/i18n"; import { Tooltip } from "@plane/ui"; @@ -49,7 +49,7 @@ export const SidebarUserMenuItem: FC = observer((props if (window.innerWidth < 768) { toggleSidebar(); } - captureEvent(SIDEBAR_CLICKED, { + captureEvent(SIDEBAR_TRACKER_EVENTS.click, { destination: itemKey, }); }; diff --git a/web/core/components/workspace/views/delete-view-modal.tsx b/web/core/components/workspace/views/delete-view-modal.tsx index 63016d157..65f4efdd4 100644 --- a/web/core/components/workspace/views/delete-view-modal.tsx +++ b/web/core/components/workspace/views/delete-view-modal.tsx @@ -4,7 +4,7 @@ import React, { useState } from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; // types -import { GLOBAL_VIEW_DELETED } from "@plane/constants"; +import { GLOBAL_VIEW_TOUR_TRACKER_EVENTS } from "@plane/constants"; import { IWorkspaceView } from "@plane/types"; // ui import { AlertModalCore, TOAST_TYPE, setToast } from "@plane/ui"; @@ -37,13 +37,13 @@ export const DeleteGlobalViewModal: React.FC = observer((props) => { await deleteGlobalView(workspaceSlug.toString(), data.id) .then(() => { - captureEvent(GLOBAL_VIEW_DELETED, { + captureEvent(GLOBAL_VIEW_TOUR_TRACKER_EVENTS.delete, { view_id: data.id, state: "SUCCESS", }); }) .catch((error: any) => { - captureEvent(GLOBAL_VIEW_DELETED, { + captureEvent(GLOBAL_VIEW_TOUR_TRACKER_EVENTS.delete, { view_id: data.id, state: "FAILED", }); diff --git a/web/core/components/workspace/views/header.tsx b/web/core/components/workspace/views/header.tsx index e73129800..4aa17493c 100644 --- a/web/core/components/workspace/views/header.tsx +++ b/web/core/components/workspace/views/header.tsx @@ -6,9 +6,9 @@ import { Plus } from "lucide-react"; // plane imports import { DEFAULT_GLOBAL_VIEWS_LIST, - GLOBAL_VIEW_OPENED, EUserPermissions, EUserPermissionsLevel, + GLOBAL_VIEW_TOUR_TRACKER_EVENTS, } from "@plane/constants"; import { TStaticViewTypes } from "@plane/types"; // components @@ -77,7 +77,7 @@ export const GlobalViewsHeader: React.FC = observer(() => { // bring the active view to the centre of the header useEffect(() => { if (globalViewId && currentWorkspaceViews) { - captureEvent(GLOBAL_VIEW_OPENED, { + captureEvent(GLOBAL_VIEW_TOUR_TRACKER_EVENTS.open, { view_id: globalViewId, view_type: ["all-issues", "assigned", "created", "subscribed"].includes(globalViewId.toString()) ? "Default" diff --git a/web/core/components/workspace/views/modal.tsx b/web/core/components/workspace/views/modal.tsx index 7ed07d0ec..cccc76357 100644 --- a/web/core/components/workspace/views/modal.tsx +++ b/web/core/components/workspace/views/modal.tsx @@ -4,7 +4,7 @@ import React from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; // types -import { GLOBAL_VIEW_CREATED, GLOBAL_VIEW_UPDATED } from "@plane/constants"; +import { GLOBAL_VIEW_TOUR_TRACKER_EVENTS } from "@plane/constants"; import { IWorkspaceView } from "@plane/types"; // ui import { EModalPosition, EModalWidth, ModalCore, TOAST_TYPE, setToast } from "@plane/ui"; @@ -47,7 +47,7 @@ export const CreateUpdateWorkspaceViewModal: React.FC = observer((props) await createGlobalView(workspaceSlug.toString(), payloadData) .then((res) => { - captureEvent(GLOBAL_VIEW_CREATED, { + captureEvent(GLOBAL_VIEW_TOUR_TRACKER_EVENTS.create, { view_id: res.id, applied_filters: res.filters, state: "SUCCESS", @@ -62,7 +62,7 @@ export const CreateUpdateWorkspaceViewModal: React.FC = observer((props) handleClose(); }) .catch(() => { - captureEvent(GLOBAL_VIEW_CREATED, { + captureEvent(GLOBAL_VIEW_TOUR_TRACKER_EVENTS.create, { applied_filters: payload?.filters, state: "FAILED", }); @@ -87,7 +87,7 @@ export const CreateUpdateWorkspaceViewModal: React.FC = observer((props) await updateGlobalView(workspaceSlug.toString(), data.id, payloadData) .then((res) => { if (res) { - captureEvent(GLOBAL_VIEW_UPDATED, { + captureEvent(GLOBAL_VIEW_TOUR_TRACKER_EVENTS.update, { view_id: res.id, applied_filters: res.filters, state: "SUCCESS", @@ -101,7 +101,7 @@ export const CreateUpdateWorkspaceViewModal: React.FC = observer((props) } }) .catch(() => { - captureEvent(GLOBAL_VIEW_UPDATED, { + captureEvent(GLOBAL_VIEW_TOUR_TRACKER_EVENTS.update, { view_id: data.id, applied_filters: data.filters, state: "FAILED", diff --git a/web/core/hooks/store/use-event-tracker.ts b/web/core/hooks/store/use-event-tracker.ts index 081710af0..0f92aaced 100644 --- a/web/core/hooks/store/use-event-tracker.ts +++ b/web/core/hooks/store/use-event-tracker.ts @@ -2,7 +2,7 @@ import { useContext } from "react"; // mobx store import { StoreContext } from "@/lib/store-context"; // types -import { IEventTrackerStore } from "@/store/event-tracker.store"; +import { IEventTrackerStore } from "@/plane-web/store/event-tracker.store"; export const useEventTracker = (): IEventTrackerStore => { const context = useContext(StoreContext); diff --git a/web/core/lib/posthog-provider.tsx b/web/core/lib/posthog-provider.tsx index f0aa05458..f714a9e73 100644 --- a/web/core/lib/posthog-provider.tsx +++ b/web/core/lib/posthog-provider.tsx @@ -7,7 +7,7 @@ import { useParams } from "next/navigation"; import posthog from "posthog-js"; import { PostHogProvider as PHProvider } from "posthog-js/react"; // constants -import { GROUP_WORKSPACE } from "@plane/constants"; +import { GROUP_WORKSPACE_TRACKER_EVENT } from "@plane/constants"; // helpers import { getUserRole } from "@plane/utils"; // hooks @@ -46,7 +46,7 @@ const PostHogProvider: FC = observer((props) => { project_role: currentProjectRole ? getUserRole(currentProjectRole) : undefined, }); if (currentWorkspace) { - posthog?.group(GROUP_WORKSPACE, currentWorkspace?.id); + posthog?.group(GROUP_WORKSPACE_TRACKER_EVENT, currentWorkspace?.id); } } }, [user, currentProjectRole, currentWorkspaceRole, currentWorkspace]); diff --git a/web/core/store/event-tracker.store.ts b/web/core/store/event-tracker.store.ts index c6f68a097..8116eaaf0 100644 --- a/web/core/store/event-tracker.store.ts +++ b/web/core/store/event-tracker.store.ts @@ -2,8 +2,7 @@ import { action, computed, makeObservable, observable } from "mobx"; import posthog from "posthog-js"; // store import { - GROUP_WORKSPACE, - WORKSPACE_CREATED, + GROUP_WORKSPACE_TRACKER_EVENT, EventProps, IssueEventProps, getCycleEventPayload, @@ -13,10 +12,11 @@ import { getProjectStateEventPayload, getWorkspaceEventPayload, getPageEventPayload, + WORKSPACE_TRACKER_EVENTS, } from "@plane/constants"; import { CoreRootStore } from "./root.store"; -export interface IEventTrackerStore { +export interface ICoreEventTrackerStore { // properties trackElement: string | undefined; // computed @@ -35,7 +35,7 @@ export interface IEventTrackerStore { captureProjectStateEvent: (props: EventProps) => void; } -export class EventTrackerStore implements IEventTrackerStore { +export abstract class CoreEventTrackerStore implements ICoreEventTrackerStore { trackElement: string | undefined = undefined; rootStore; constructor(_rootStore: CoreRootStore) { @@ -89,7 +89,7 @@ export class EventTrackerStore implements IEventTrackerStore { */ joinWorkspaceMetricGroup = (workspaceId?: string) => { if (!workspaceId) return; - posthog?.group(GROUP_WORKSPACE, workspaceId, { + posthog?.group(GROUP_WORKSPACE_TRACKER_EVENT, workspaceId, { date: new Date().toDateString(), workspace_id: workspaceId, }); @@ -115,7 +115,7 @@ export class EventTrackerStore implements IEventTrackerStore { */ captureWorkspaceEvent = (props: EventProps) => { const { eventName, payload } = props; - if (eventName === WORKSPACE_CREATED && payload.state == "SUCCESS") { + if (eventName === WORKSPACE_TRACKER_EVENTS.create && payload.state == "SUCCESS") { this.joinWorkspaceMetricGroup(payload.id); } const eventPayload: any = getWorkspaceEventPayload({ diff --git a/web/core/store/root.store.ts b/web/core/store/root.store.ts index e210754cc..e2ef10ae3 100644 --- a/web/core/store/root.store.ts +++ b/web/core/store/root.store.ts @@ -4,6 +4,7 @@ import { FALLBACK_LANGUAGE, LANGUAGE_STORAGE_KEY } from "@plane/i18n"; // plane web store import { AnalyticsStore, IAnalyticsStore } from "@/plane-web/store/analytics.store"; import { CommandPaletteStore, ICommandPaletteStore } from "@/plane-web/store/command-palette.store"; +import { EventTrackerStore, IEventTrackerStore } from "@/plane-web/store/event-tracker.store"; import { RootStore } from "@/plane-web/store/root.store"; import { IStateStore, StateStore } from "@/plane-web/store/state.store"; // stores @@ -12,7 +13,6 @@ import { CycleFilterStore, ICycleFilterStore } from "./cycle_filter.store"; import { DashboardStore, IDashboardStore } from "./dashboard.store"; import { EditorAssetStore, IEditorAssetStore } from "./editor/asset.store"; import { IProjectEstimateStore, ProjectEstimateStore } from "./estimates/project-estimate.store"; -import { EventTrackerStore, IEventTrackerStore } from "./event-tracker.store"; import { FavoriteStore, IFavoriteStore } from "./favorite.store"; import { GlobalViewStore, IGlobalViewStore } from "./global-view.store"; import { IProjectInboxStore, ProjectInboxStore } from "./inbox/project-inbox.store"; @@ -86,7 +86,7 @@ export class CoreRootStore { this.state = new StateStore(this as unknown as RootStore); this.label = new LabelStore(this); this.dashboard = new DashboardStore(this); - this.eventTracker = new EventTrackerStore(this); + this.eventTracker = new EventTrackerStore(this as unknown as RootStore); this.multipleSelect = new MultipleSelectStore(); this.projectInbox = new ProjectInboxStore(this); this.projectPages = new ProjectPageStore(this as unknown as RootStore); @@ -120,7 +120,7 @@ export class CoreRootStore { this.state = new StateStore(this as unknown as RootStore); this.label = new LabelStore(this); this.dashboard = new DashboardStore(this); - this.eventTracker = new EventTrackerStore(this); + this.eventTracker = new EventTrackerStore(this as unknown as RootStore); this.projectInbox = new ProjectInboxStore(this); this.projectPages = new ProjectPageStore(this as unknown as RootStore); this.multipleSelect = new MultipleSelectStore(); diff --git a/web/ee/store/event-tracker.store.ts b/web/ee/store/event-tracker.store.ts new file mode 100644 index 000000000..68f6a3a74 --- /dev/null +++ b/web/ee/store/event-tracker.store.ts @@ -0,0 +1 @@ +export * from "ce/store/event-tracker.store"; From 912246c592dc8caef1c3513deef82cde2316f696 Mon Sep 17 00:00:00 2001 From: Vamsi Krishna <46787868+vamsikrishnamathala@users.noreply.github.com> Date: Mon, 30 Jun 2025 16:10:06 +0530 Subject: [PATCH 197/201] [WEB-4365] fix: dates display properties toggle #7262 --- .../sub-issues/issues-list/properties.tsx | 78 +++++++++++++++++-- .../properties/all-properties.tsx | 60 +++++++++++++- 2 files changed, 131 insertions(+), 7 deletions(-) diff --git a/web/core/components/issues/issue-detail-widgets/sub-issues/issues-list/properties.tsx b/web/core/components/issues/issue-detail-widgets/sub-issues/issues-list/properties.tsx index 96ed4a2a9..92a8bc0d6 100644 --- a/web/core/components/issues/issue-detail-widgets/sub-issues/issues-list/properties.tsx +++ b/web/core/components/issues/issue-detail-widgets/sub-issues/issues-list/properties.tsx @@ -1,12 +1,21 @@ // plane imports -import { SyntheticEvent } from "react"; +import { SyntheticEvent, useMemo } from "react"; import { observer } from "mobx-react"; +import { CalendarCheck2, CalendarClock } from "lucide-react"; +import { useTranslation } from "@plane/i18n"; import { IIssueDisplayProperties, TIssue } from "@plane/types"; -import { getDate, renderFormattedPayloadDate } from "@plane/utils"; +import { getDate, renderFormattedPayloadDate, shouldHighlightIssueDueDate } from "@plane/utils"; // components -import { PriorityDropdown, MemberDropdown, StateDropdown, DateRangeDropdown } from "@/components/dropdowns"; +import { + PriorityDropdown, + MemberDropdown, + StateDropdown, + DateRangeDropdown, + DateDropdown, +} from "@/components/dropdowns"; // hooks import { WithDisplayPropertiesHOC } from "@/components/issues/issue-layouts/properties/with-display-properties-HOC"; +import { useProjectState } from "@/hooks/store/use-project-state"; type Props = { workspaceSlug: string; @@ -27,6 +36,8 @@ type Props = { export const SubIssuesListItemProperties: React.FC = observer((props) => { const { workspaceSlug, parentIssueId, issueId, disabled, updateSubIssue, displayProperties, issue } = props; + const { t } = useTranslation(); + const { getStateById } = useProjectState(); const handleEventPropagation = (e: SyntheticEvent) => { e.stopPropagation(); @@ -49,10 +60,22 @@ export const SubIssuesListItemProperties: React.FC = observer((props) => } }; + //derived values + const stateDetails = useMemo(() => getStateById(issue.state_id), [getStateById, issue.state_id]); + const shouldHighlight = useMemo( + () => shouldHighlightIssueDueDate(issue.target_date, stateDetails?.group), + [issue.target_date, stateDetails?.group] + ); + // date range is enabled only when both dates are available and both dates are enabled + const isDateRangeEnabled: boolean = Boolean( + issue.start_date && issue.target_date && displayProperties?.start_date && displayProperties?.due_date + ); + if (!displayProperties) return <>; const maxDate = getDate(issue.target_date); - maxDate?.setDate(maxDate.getDate()); + const minDate = getDate(issue.start_date); + return (
    @@ -104,7 +127,7 @@ export const SubIssuesListItemProperties: React.FC = observer((props) => !!(properties.start_date || properties.due_date)} + shouldRenderProperty={() => isDateRangeEnabled} >
    = observer((props) => isClearable mergeDates buttonVariant={issue.start_date || issue.target_date ? "border-with-text" : "border-without-text"} + buttonClassName={shouldHighlight ? "text-red-500" : ""} disabled={!disabled} showTooltip customTooltipHeading="Date Range" @@ -130,6 +154,50 @@ export const SubIssuesListItemProperties: React.FC = observer((props) =>
    + {/* start date */} + !isDateRangeEnabled} + > +
    + } + buttonVariant={issue.start_date ? "border-with-text" : "border-without-text"} + optionsClassName="z-30" + disabled={!disabled} + showTooltip + /> +
    +
    + + {/* target/due date */} + !isDateRangeEnabled} + > +
    + } + buttonVariant={issue.target_date ? "border-with-text" : "border-without-text"} + buttonClassName={shouldHighlight ? "text-red-500" : ""} + clearIconClassName="text-custom-text-100" + optionsClassName="z-30" + disabled={!disabled} + showTooltip + /> +
    +
    +
    = observer((props) => { if (!displayProperties || !issue.project_id) return null; + // date range is enabled only when both dates are available and both dates are enabled + const isDateRangeEnabled: boolean = Boolean( + issue.start_date && issue.target_date && displayProperties.start_date && displayProperties.due_date + ); + const defaultLabelOptions = issue?.label_ids?.map((id) => labelMap[id]) || []; + const minDate = getDate(issue.start_date); + const maxDate = getDate(issue.target_date); + const handleEventPropagation = (e: SyntheticEvent) => { e.stopPropagation(); e.preventDefault(); @@ -312,7 +321,7 @@ export const IssueProperties: React.FC = observer((props) => { !!(properties.start_date || properties.due_date)} + shouldRenderProperty={() => isDateRangeEnabled} >
    = observer((props) => { mergeDates buttonVariant={issue.start_date || issue.target_date ? "border-with-text" : "border-without-text"} buttonClassName={shouldHighlightIssueDueDate(issue.target_date, stateDetails?.group) ? "text-red-500" : ""} + clearIconClassName="!text-custom-text-100" disabled={isReadOnly} renderByDefault={isMobile} showTooltip @@ -340,6 +350,52 @@ export const IssueProperties: React.FC = observer((props) => {
    + {/* start date */} + !isDateRangeEnabled} + > +
    + } + buttonVariant={issue.start_date ? "border-with-text" : "border-without-text"} + optionsClassName="z-10" + disabled={isReadOnly} + renderByDefault={isMobile} + showTooltip + /> +
    +
    + + {/* target/due date */} + !isDateRangeEnabled} + > +
    + } + buttonVariant={issue.target_date ? "border-with-text" : "border-without-text"} + buttonClassName={shouldHighlightIssueDueDate(issue.target_date, stateDetails?.group) ? "text-red-500" : ""} + clearIconClassName="!text-custom-text-100" + optionsClassName="z-10" + disabled={isReadOnly} + renderByDefault={isMobile} + showTooltip + /> +
    +
    + {/* assignee */}
    From 2f6923fca081857e0d1b2a248acf43ed38bb8bb0 Mon Sep 17 00:00:00 2001 From: Jayash Tripathy <76092296+JayashTripathy@users.noreply.github.com> Date: Mon, 30 Jun 2025 16:10:50 +0530 Subject: [PATCH 198/201] fix: work item peek infinite loop (#7284) * fix: removed t function from dependency array which was causing infinite loop * fix: add eslint disable comment for exhaustive-deps warning in IssuePeekOverview --- web/core/components/issues/peek-overview/root.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/core/components/issues/peek-overview/root.tsx b/web/core/components/issues/peek-overview/root.tsx index e7a823902..dfeedadce 100644 --- a/web/core/components/issues/peek-overview/root.tsx +++ b/web/core/components/issues/peek-overview/root.tsx @@ -331,7 +331,8 @@ export const IssuePeekOverview: FC = observer((props) => } }, }), - [fetchIssue, is_draft, issues, fetchActivities, captureIssueEvent, pathname, removeRoutePeekId, restoreIssue, t] + // eslint-disable-next-line react-hooks/exhaustive-deps + [fetchIssue, is_draft, issues, fetchActivities, captureIssueEvent, pathname, removeRoutePeekId, restoreIssue] ); useEffect(() => { From e7d888d81761b6328e169e384b4fe2147fc37eb5 Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Mon, 30 Jun 2025 23:56:34 +0530 Subject: [PATCH 199/201] chore: package version updated --- admin/package.json | 2 +- apiserver/package.json | 2 +- live/package.json | 2 +- package.json | 2 +- packages/constants/package.json | 2 +- packages/editor/package.json | 2 +- packages/eslint-config/package.json | 2 +- packages/hooks/package.json | 2 +- packages/i18n/package.json | 2 +- packages/logger/package.json | 2 +- packages/propel/package.json | 4 ++-- packages/services/package.json | 2 +- packages/shared-state/package.json | 2 +- packages/tailwind-config/package.json | 2 +- packages/types/package.json | 2 +- packages/typescript-config/package.json | 2 +- packages/ui/package.json | 2 +- packages/utils/package.json | 2 +- space/package.json | 2 +- web/package.json | 2 +- 20 files changed, 21 insertions(+), 21 deletions(-) diff --git a/admin/package.json b/admin/package.json index 7b1746726..0925e9d95 100644 --- a/admin/package.json +++ b/admin/package.json @@ -1,7 +1,7 @@ { "name": "admin", "description": "Admin UI for Plane", - "version": "0.26.1", + "version": "0.27.0", "license": "AGPL-3.0", "private": true, "scripts": { diff --git a/apiserver/package.json b/apiserver/package.json index a6ae894c3..6b3118020 100644 --- a/apiserver/package.json +++ b/apiserver/package.json @@ -1,6 +1,6 @@ { "name": "plane-api", - "version": "0.26.1", + "version": "0.27.0", "license": "AGPL-3.0", "private": true, "description": "API server powering Plane's backend" diff --git a/live/package.json b/live/package.json index 0c1be213f..7399de761 100644 --- a/live/package.json +++ b/live/package.json @@ -1,6 +1,6 @@ { "name": "live", - "version": "0.26.1", + "version": "0.27.0", "license": "AGPL-3.0", "description": "A realtime collaborative server powers Plane's rich text editor", "main": "./src/server.ts", diff --git a/package.json b/package.json index d1711c577..73c8697a1 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "plane", "description": "Open-source project management that unlocks customer value", "repository": "https://github.com/makeplane/plane.git", - "version": "0.26.1", + "version": "0.27.0", "license": "AGPL-3.0", "private": true, "workspaces": [ diff --git a/packages/constants/package.json b/packages/constants/package.json index d69253bc7..e007dc10b 100644 --- a/packages/constants/package.json +++ b/packages/constants/package.json @@ -1,6 +1,6 @@ { "name": "@plane/constants", - "version": "0.26.1", + "version": "0.27.0", "private": true, "main": "./src/index.ts", "license": "AGPL-3.0" diff --git a/packages/editor/package.json b/packages/editor/package.json index 52c5d8dba..c511d554f 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -1,6 +1,6 @@ { "name": "@plane/editor", - "version": "0.26.1", + "version": "0.27.0", "description": "Core Editor that powers Plane", "license": "AGPL-3.0", "private": true, diff --git a/packages/eslint-config/package.json b/packages/eslint-config/package.json index 9b63b542a..0e7e2382b 100644 --- a/packages/eslint-config/package.json +++ b/packages/eslint-config/package.json @@ -1,7 +1,7 @@ { "name": "@plane/eslint-config", "private": true, - "version": "0.26.1", + "version": "0.27.0", "license": "AGPL-3.0", "files": [ "library.js", diff --git a/packages/hooks/package.json b/packages/hooks/package.json index 82897a8b6..81484513d 100644 --- a/packages/hooks/package.json +++ b/packages/hooks/package.json @@ -1,6 +1,6 @@ { "name": "@plane/hooks", - "version": "0.26.1", + "version": "0.27.0", "license": "AGPL-3.0", "description": "React hooks that are shared across multiple apps internally", "private": true, diff --git a/packages/i18n/package.json b/packages/i18n/package.json index 1ccc02071..d4faaf017 100644 --- a/packages/i18n/package.json +++ b/packages/i18n/package.json @@ -1,6 +1,6 @@ { "name": "@plane/i18n", - "version": "0.26.1", + "version": "0.27.0", "license": "AGPL-3.0", "description": "I18n shared across multiple apps internally", "private": true, diff --git a/packages/logger/package.json b/packages/logger/package.json index 83179ba2d..c81c467c2 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -1,6 +1,6 @@ { "name": "@plane/logger", - "version": "0.26.1", + "version": "0.27.0", "license": "AGPL-3.0", "description": "Logger shared across multiple apps internally", "private": true, diff --git a/packages/propel/package.json b/packages/propel/package.json index f686d5e1c..e6922c718 100644 --- a/packages/propel/package.json +++ b/packages/propel/package.json @@ -1,6 +1,6 @@ { "name": "@plane/propel", - "version": "0.26.1", + "version": "0.27.0", "private": true, "license": "AGPL-3.0", "scripts": { @@ -31,4 +31,4 @@ "@types/react-dom": "18.3.0", "typescript": "5.8.3" } -} \ No newline at end of file +} diff --git a/packages/services/package.json b/packages/services/package.json index 0ef4ed415..449e9efed 100644 --- a/packages/services/package.json +++ b/packages/services/package.json @@ -1,6 +1,6 @@ { "name": "@plane/services", - "version": "0.26.1", + "version": "0.27.0", "license": "AGPL-3.0", "private": true, "main": "./src/index.ts", diff --git a/packages/shared-state/package.json b/packages/shared-state/package.json index 0f559de72..167c1794a 100644 --- a/packages/shared-state/package.json +++ b/packages/shared-state/package.json @@ -1,6 +1,6 @@ { "name": "@plane/shared-state", - "version": "0.26.1", + "version": "0.27.0", "license": "AGPL-3.0", "description": "Shared state shared across multiple apps internally", "private": true, diff --git a/packages/tailwind-config/package.json b/packages/tailwind-config/package.json index e07c39fe9..8e6f51822 100644 --- a/packages/tailwind-config/package.json +++ b/packages/tailwind-config/package.json @@ -1,6 +1,6 @@ { "name": "@plane/tailwind-config", - "version": "0.26.1", + "version": "0.27.0", "license": "AGPL-3.0", "description": "common tailwind configuration across monorepo", "main": "tailwind.config.js", diff --git a/packages/types/package.json b/packages/types/package.json index 609e3537a..2249a9cec 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@plane/types", - "version": "0.26.1", + "version": "0.27.0", "license": "AGPL-3.0", "private": true, "types": "./src/index.d.ts", diff --git a/packages/typescript-config/package.json b/packages/typescript-config/package.json index e190ff3c8..f5b3d1a85 100644 --- a/packages/typescript-config/package.json +++ b/packages/typescript-config/package.json @@ -1,6 +1,6 @@ { "name": "@plane/typescript-config", - "version": "0.26.1", + "version": "0.27.0", "license": "AGPL-3.0", "private": true, "files": [ diff --git a/packages/ui/package.json b/packages/ui/package.json index 213af997a..5b3cab5cd 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -2,7 +2,7 @@ "name": "@plane/ui", "description": "UI components shared across multiple apps internally", "private": true, - "version": "0.26.1", + "version": "0.27.0", "main": "./dist/index.js", "module": "./dist/index.mjs", "types": "./dist/index.d.ts", diff --git a/packages/utils/package.json b/packages/utils/package.json index 5aebea9ac..255bbbfe8 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -1,6 +1,6 @@ { "name": "@plane/utils", - "version": "0.26.1", + "version": "0.27.0", "description": "Helper functions shared across multiple apps internally", "license": "AGPL-3.0", "private": true, diff --git a/space/package.json b/space/package.json index 9a74895b4..2159adf56 100644 --- a/space/package.json +++ b/space/package.json @@ -1,6 +1,6 @@ { "name": "space", - "version": "0.26.1", + "version": "0.27.0", "private": true, "license": "AGPL-3.0", "scripts": { diff --git a/web/package.json b/web/package.json index f87326870..c1f4d1d2c 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "web", - "version": "0.26.1", + "version": "0.27.0", "private": true, "license": "AGPL-3.0", "scripts": { From 670da9fa03e976db17da0745df23d38d30c63693 Mon Sep 17 00:00:00 2001 From: Vamsi Krishna <46787868+vamsikrishnamathala@users.noreply.github.com> Date: Tue, 1 Jul 2025 13:21:29 +0530 Subject: [PATCH 200/201] [WEB-4416]fix: Unnecessary border on Intake header #7297 --- web/ce/components/projects/settings/intake/header.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/ce/components/projects/settings/intake/header.tsx b/web/ce/components/projects/settings/intake/header.tsx index 9b0c994b5..0aa77e470 100644 --- a/web/ce/components/projects/settings/intake/header.tsx +++ b/web/ce/components/projects/settings/intake/header.tsx @@ -36,7 +36,7 @@ export const ProjectInboxHeader: FC = observer(() => { return (
    -
    +
    Date: Tue, 1 Jul 2025 15:03:20 +0530 Subject: [PATCH 201/201] [WEB-4418]fix: fixed the pagination for issues #7301 --- apiserver/plane/utils/paginator.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apiserver/plane/utils/paginator.py b/apiserver/plane/utils/paginator.py index 2b8c27f76..0793d2a30 100644 --- a/apiserver/plane/utils/paginator.py +++ b/apiserver/plane/utils/paginator.py @@ -150,6 +150,8 @@ class OffsetPaginator: raise BadPaginationError("Pagination offset cannot be negative") results = queryset[offset:stop] + # Duplicate the queryset so it does not evaluate on any python ops + page_results = queryset[offset:stop].values("id") # Only slice from the end if we're going backwards (previous page) if cursor.value != limit and cursor.is_prev: @@ -164,7 +166,7 @@ class OffsetPaginator: # Check if there are more results available after the current page # Adjust cursors based on the results for pagination - next_cursor = Cursor(limit, page + 1, False, len(results) > limit) + next_cursor = Cursor(limit, page + 1, False, page_results.count() > limit) # If the page is greater than 0, then set the previous cursor prev_cursor = Cursor(limit, page - 1, True, page > 0)