diff --git a/admin/core/components/authentication/auth-banner.tsx b/admin/core/components/authentication/auth-banner.tsx new file mode 100644 index 00000000000..191d7a0a772 --- /dev/null +++ b/admin/core/components/authentication/auth-banner.tsx @@ -0,0 +1,29 @@ +import { FC } from "react"; +import { Info, X } from "lucide-react"; +// helpers +import { TAuthErrorInfo } from "@/helpers/authentication.helper"; + +type TAuthBanner = { + bannerData: TAuthErrorInfo | undefined; + handleBannerData?: (bannerData: TAuthErrorInfo | undefined) => void; +}; + +export const AuthBanner: FC = (props) => { + const { bannerData, handleBannerData } = props; + + if (!bannerData) return <>; + return ( +
+
+ +
+
{bannerData?.message}
+
handleBannerData && handleBannerData(undefined)} + > + +
+
+ ); +}; diff --git a/admin/core/components/authentication/index.ts b/admin/core/components/authentication/index.ts index 2c13b772894..d189a727ba6 100644 --- a/admin/core/components/authentication/index.ts +++ b/admin/core/components/authentication/index.ts @@ -1,3 +1,4 @@ +export * from "./auth-banner"; export * from "./email-config-switch"; export * from "./password-config-switch"; export * from "./authentication-method-card"; diff --git a/admin/core/components/login/sign-in-form.tsx b/admin/core/components/login/sign-in-form.tsx index 30d7011e7f1..aa67d4936e1 100644 --- a/admin/core/components/login/sign-in-form.tsx +++ b/admin/core/components/login/sign-in-form.tsx @@ -8,8 +8,16 @@ import { Button, Input, Spinner } from "@plane/ui"; // components import { Banner } from "@/components/common"; // helpers +import { + authErrorHandler, + EAuthenticationErrorCodes, + EErrorAlertType, + TAuthErrorInfo, +} from "@/helpers/authentication.helper"; + import { API_BASE_URL } from "@/helpers/common.helper"; import { AuthService } from "@/services/auth.service"; +import { AuthBanner } from "../authentication"; // ui // icons @@ -53,6 +61,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 handleFormChange = (key: keyof TFormData, value: string | boolean) => setFormData((prev) => ({ ...prev, [key]: value })); @@ -91,6 +100,15 @@ export const InstanceSignInForm: FC = (props) => { [formData.email, formData.password, isSubmitting] ); + useEffect(() => { + if (errorCode) { + const errorDetail = authErrorHandler(errorCode?.toString() as EAuthenticationErrorCodes); + if (errorDetail) { + setErrorInfo(errorDetail); + } + } + }, [errorCode]); + return (
@@ -103,7 +121,11 @@ export const InstanceSignInForm: FC = (props) => {

- {errorData.type && errorData?.message && } + {errorData.type && errorData?.message ? ( + + ) : ( + <>{errorInfo && setErrorInfo(value)} />} + )}
/projects//bulk-operation-issues/", - BulkIssueOperationsEndpoint.as_view(), - name="bulk-operations-issues", - ), + ) ] diff --git a/apiserver/plane/app/views/__init__.py b/apiserver/plane/app/views/__init__.py index 9d8929fda51..5568542f70a 100644 --- a/apiserver/plane/app/views/__init__.py +++ b/apiserver/plane/app/views/__init__.py @@ -156,9 +156,6 @@ IssueSubscriberViewSet, ) - -from .issue.bulk_operations import BulkIssueOperationsEndpoint - from .module.base import ( ModuleViewSet, ModuleLinkViewSet, diff --git a/apiserver/plane/app/views/cycle/archive.py b/apiserver/plane/app/views/cycle/archive.py index 5f7f1434738..22f21f4bf3e 100644 --- a/apiserver/plane/app/views/cycle/archive.py +++ b/apiserver/plane/app/views/cycle/archive.py @@ -607,6 +607,12 @@ def post(self, request, slug, project_id, cycle_id): cycle.archived_at = timezone.now() cycle.save() + UserFavorite.objects.filter( + entity_type="cycle", + entity_identifier=cycle_id, + project_id=project_id, + workspace__slug=slug, + ).delete() return Response( {"archived_at": str(cycle.archived_at)}, status=status.HTTP_200_OK, diff --git a/apiserver/plane/app/views/cycle/base.py b/apiserver/plane/app/views/cycle/base.py index 55cb24b85b0..86f9de0ef62 100644 --- a/apiserver/plane/app/views/cycle/base.py +++ b/apiserver/plane/app/views/cycle/base.py @@ -49,6 +49,7 @@ ProjectMember, ) from plane.utils.analytics_plot import burndown_plot +from plane.bgtasks.recent_visited_task import recent_visited_task # Module imports from .. import BaseAPIView, BaseViewSet @@ -1028,6 +1029,13 @@ def retrieve(self, request, slug, project_id, pk): cycle_id=pk, ) + recent_visited_task.delay( + slug=slug, + entity_name="cycle", + entity_identifier=pk, + user_id=request.user.id, + project_id=project_id, + ) return Response( data, status=status.HTTP_200_OK, diff --git a/apiserver/plane/app/views/exporter/base.py b/apiserver/plane/app/views/exporter/base.py index 50f9870d097..39255bd1ed8 100644 --- a/apiserver/plane/app/views/exporter/base.py +++ b/apiserver/plane/app/views/exporter/base.py @@ -15,7 +15,7 @@ class ExportIssuesEndpoint(BaseAPIView): model = ExporterHistory serializer_class = ExporterHistorySerializer - @allow_permission(allowed_roles=[ROLE.ADMIN], level="WORKSPACE") + @allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE") def post(self, request, slug): # Get the workspace workspace = Workspace.objects.get(slug=slug) @@ -62,7 +62,9 @@ def post(self, request, slug): status=status.HTTP_400_BAD_REQUEST, ) - @allow_permission(allowed_roles=[ROLE.ADMIN], level="WORKSPACE") + @allow_permission( + allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE" + ) def get(self, request, slug): exporter_history = ExporterHistory.objects.filter( workspace__slug=slug, diff --git a/apiserver/plane/app/views/issue/archive.py b/apiserver/plane/app/views/issue/archive.py index 53dec689818..811e6f8f92d 100644 --- a/apiserver/plane/app/views/issue/archive.py +++ b/apiserver/plane/app/views/issue/archive.py @@ -47,6 +47,7 @@ SubGroupedOffsetPaginator, ) from plane.app.permissions import allow_permission, ROLE +from plane.utils.error_codes import ERROR_CODES # Module imports from .. import BaseViewSet, BaseAPIView @@ -345,7 +346,9 @@ def post(self, request, slug, project_id): if issue.state.group not in ["completed", "cancelled"]: return Response( { - "error_code": 4091, + "error_code": ERROR_CODES[ + "INVALID_ARCHIVE_STATE_GROUP" + ], "error_message": "INVALID_ARCHIVE_STATE_GROUP", }, status=status.HTTP_400_BAD_REQUEST, diff --git a/apiserver/plane/app/views/issue/base.py b/apiserver/plane/app/views/issue/base.py index 6a4f3ce2b74..429eb0b408d 100644 --- a/apiserver/plane/app/views/issue/base.py +++ b/apiserver/plane/app/views/issue/base.py @@ -56,6 +56,7 @@ ) from .. import BaseAPIView, BaseViewSet from plane.utils.user_timezone_converter import user_timezone_converter +from plane.bgtasks.recent_visited_task import recent_visited_task class IssueListEndpoint(BaseAPIView): @@ -127,6 +128,14 @@ def get(self, request, slug, project_id): sub_group_by=sub_group_by, ) + recent_visited_task.delay( + slug=slug, + project_id=project_id, + entity_name="project", + entity_identifier=project_id, + user_id=request.user.id, + ) + if self.fields or self.expand: issues = IssueSerializer( queryset, many=True, fields=self.fields, expand=self.expand @@ -247,6 +256,13 @@ def list(self, request, slug, project_id): sub_group_by=sub_group_by, ) + recent_visited_task.delay( + slug=slug, + project_id=project_id, + entity_name="project", + entity_identifier=project_id, + user_id=request.user.id, + ) if ProjectMember.objects.filter( workspace__slug=slug, project_id=project_id, @@ -484,6 +500,14 @@ def retrieve(self, request, slug, project_id, pk=None): status=status.HTTP_404_NOT_FOUND, ) + recent_visited_task.delay( + slug=slug, + entity_name="issue", + entity_identifier=pk, + user_id=request.user.id, + project_id=project_id, + ) + serializer = IssueDetailSerializer(issue, expand=self.expand) return Response(serializer.data, status=status.HTTP_200_OK) diff --git a/apiserver/plane/app/views/issue/bulk_operations.py b/apiserver/plane/app/views/issue/bulk_operations.py deleted file mode 100644 index 1965b4e31ee..00000000000 --- a/apiserver/plane/app/views/issue/bulk_operations.py +++ /dev/null @@ -1,293 +0,0 @@ -# Python imports -import json -from datetime import datetime - -# Django imports -from django.utils import timezone - -# Third Party imports -from rest_framework.response import Response -from rest_framework import status - -# Module imports -from .. import BaseAPIView -from plane.app.permissions import ( - ProjectEntityPermission, -) -from plane.db.models import ( - Project, - Issue, - IssueLabel, - IssueAssignee, -) -from plane.bgtasks.issue_activities_task import issue_activity - - -class BulkIssueOperationsEndpoint(BaseAPIView): - permission_classes = [ - ProjectEntityPermission, - ] - - def post(self, request, slug, project_id): - issue_ids = request.data.get("issue_ids", []) - if not len(issue_ids): - return Response( - {"error": "Issue IDs are required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - # Get all the issues - issues = ( - Issue.objects.filter( - workspace__slug=slug, project_id=project_id, pk__in=issue_ids - ) - .select_related("state") - .prefetch_related("labels", "assignees") - ) - # Current epoch - epoch = int(timezone.now().timestamp()) - - # Project details - project = Project.objects.get(workspace__slug=slug, pk=project_id) - workspace_id = project.workspace_id - - # Initialize arrays - bulk_update_issues = [] - bulk_issue_activities = [] - bulk_update_issue_labels = [] - bulk_update_issue_assignees = [] - - properties = request.data.get("properties", {}) - - if properties.get("start_date", False) and properties.get( - "target_date", False - ): - if ( - datetime.strptime( - properties.get("start_date"), "%Y-%m-%d" - ).date() - > datetime.strptime( - properties.get("target_date"), "%Y-%m-%d" - ).date() - ): - return Response( - { - "error_code": 4100, - "error_message": "INVALID_ISSUE_DATES", - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - for issue in issues: - # Priority - if properties.get("priority", False): - bulk_issue_activities.append( - { - "type": "issue.activity.updated", - "requested_data": json.dumps( - {"priority": properties.get("priority")} - ), - "current_instance": json.dumps( - {"priority": (issue.priority)} - ), - "issue_id": str(issue.id), - "actor_id": str(request.user.id), - "project_id": str(project_id), - "epoch": epoch, - } - ) - issue.priority = properties.get("priority") - - # State - if properties.get("state_id", False): - bulk_issue_activities.append( - { - "type": "issue.activity.updated", - "requested_data": json.dumps( - {"state": properties.get("state")} - ), - "current_instance": json.dumps( - {"state": str(issue.state_id)} - ), - "issue_id": str(issue.id), - "actor_id": str(request.user.id), - "project_id": str(project_id), - "epoch": epoch, - } - ) - issue.state_id = properties.get("state_id") - - # Start date - if properties.get("start_date", False): - if ( - issue.target_date - and not properties.get("target_date", False) - and issue.target_date - <= datetime.strptime( - properties.get("start_date"), "%Y-%m-%d" - ).date() - ): - return Response( - { - "error_code": 4101, - "error_message": "INVALID_ISSUE_START_DATE", - }, - status=status.HTTP_400_BAD_REQUEST, - ) - bulk_issue_activities.append( - { - "type": "issue.activity.updated", - "requested_data": json.dumps( - {"start_date": properties.get("start_date")} - ), - "current_instance": json.dumps( - {"start_date": str(issue.start_date)} - ), - "issue_id": str(issue.id), - "actor_id": str(request.user.id), - "project_id": str(project_id), - "epoch": epoch, - } - ) - issue.start_date = properties.get("start_date") - - # Target date - if properties.get("target_date", False): - if ( - issue.start_date - and not properties.get("start_date", False) - and issue.start_date - >= datetime.strptime( - properties.get("target_date"), "%Y-%m-%d" - ).date() - ): - return Response( - { - "error_code": 4102, - "error_message": "INVALID_ISSUE_TARGET_DATE", - }, - status=status.HTTP_400_BAD_REQUEST, - ) - bulk_issue_activities.append( - { - "type": "issue.activity.updated", - "requested_data": json.dumps( - {"target_date": properties.get("target_date")} - ), - "current_instance": json.dumps( - {"target_date": str(issue.target_date)} - ), - "issue_id": str(issue.id), - "actor_id": str(request.user.id), - "project_id": str(project_id), - "epoch": epoch, - } - ) - issue.target_date = properties.get("target_date") - - bulk_update_issues.append(issue) - - # Labels - if properties.get("label_ids", []): - for label_id in properties.get("label_ids", []): - bulk_update_issue_labels.append( - IssueLabel( - issue=issue, - label_id=label_id, - created_by=request.user, - project_id=project_id, - workspace_id=workspace_id, - ) - ) - bulk_issue_activities.append( - { - "type": "issue.activity.updated", - "requested_data": json.dumps( - {"label_ids": properties.get("label_ids", [])} - ), - "current_instance": json.dumps( - { - "label_ids": [ - str(label.id) - for label in issue.labels.all() - ] - } - ), - "issue_id": str(issue.id), - "actor_id": str(request.user.id), - "project_id": str(project_id), - "epoch": epoch, - } - ) - - # Assignees - if properties.get("assignee_ids", []): - for assignee_id in properties.get( - "assignee_ids", issue.assignees - ): - bulk_update_issue_assignees.append( - IssueAssignee( - issue=issue, - assignee_id=assignee_id, - created_by=request.user, - project_id=project_id, - workspace_id=workspace_id, - ) - ) - bulk_issue_activities.append( - { - "type": "issue.activity.updated", - "requested_data": json.dumps( - { - "assignee_ids": properties.get( - "assignee_ids", [] - ) - } - ), - "current_instance": json.dumps( - { - "assignee_ids": [ - str(assignee.id) - for assignee in issue.assignees.all() - ] - } - ), - "issue_id": str(issue.id), - "actor_id": str(request.user.id), - "project_id": str(project_id), - "epoch": epoch, - } - ) - - # Bulk update all the objects - Issue.objects.bulk_update( - bulk_update_issues, - [ - "priority", - "start_date", - "target_date", - "state", - ], - batch_size=100, - ) - - # Create new labels - IssueLabel.objects.bulk_create( - bulk_update_issue_labels, - ignore_conflicts=True, - batch_size=100, - ) - - # Create new assignees - IssueAssignee.objects.bulk_create( - bulk_update_issue_assignees, - ignore_conflicts=True, - batch_size=100, - ) - # update the issue activity - [ - issue_activity.delay(**activity) - for activity in bulk_issue_activities - ] - - return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/app/views/module/archive.py b/apiserver/plane/app/views/module/archive.py index 243c680cad1..b38d83487c9 100644 --- a/apiserver/plane/app/views/module/archive.py +++ b/apiserver/plane/app/views/module/archive.py @@ -575,6 +575,12 @@ def post(self, request, slug, project_id, module_id): ) module.archived_at = timezone.now() module.save() + UserFavorite.objects.filter( + entity_type="module", + entity_identifier=module_id, + project_id=project_id, + workspace__slug=slug, + ).delete() return Response( {"archived_at": str(module.archived_at)}, status=status.HTTP_200_OK, diff --git a/apiserver/plane/app/views/module/base.py b/apiserver/plane/app/views/module/base.py index 6f19de6c2a4..975b6359cdf 100644 --- a/apiserver/plane/app/views/module/base.py +++ b/apiserver/plane/app/views/module/base.py @@ -55,6 +55,7 @@ from plane.utils.user_timezone_converter import user_timezone_converter from plane.bgtasks.webhook_task import model_activity from .. import BaseAPIView, BaseViewSet +from plane.bgtasks.recent_visited_task import recent_visited_task class ModuleViewSet(BaseViewSet): @@ -670,6 +671,14 @@ def retrieve(self, request, slug, project_id, pk): module_id=pk, ) + recent_visited_task.delay( + slug=slug, + entity_name="module", + entity_identifier=pk, + user_id=request.user.id, + project_id=project_id, + ) + return Response( data, status=status.HTTP_200_OK, diff --git a/apiserver/plane/app/views/notification/base.py b/apiserver/plane/app/views/notification/base.py index 9d664c6c42e..9218b34c4d6 100644 --- a/apiserver/plane/app/views/notification/base.py +++ b/apiserver/plane/app/views/notification/base.py @@ -195,7 +195,8 @@ def partial_update(self, request, slug, pk): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) @allow_permission( - allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE" + allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST], + level="WORKSPACE", ) def mark_read(self, request, slug, pk): notification = Notification.objects.get( @@ -244,7 +245,6 @@ def unarchive(self, request, slug, pk): class UnreadNotificationEndpoint(BaseAPIView): - @allow_permission( allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST], level="WORKSPACE", @@ -286,7 +286,6 @@ def get(self, request, slug): class MarkAllReadNotificationViewSet(BaseViewSet): - @allow_permission( allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE" ) @@ -373,9 +372,6 @@ class UserNotificationPreferenceEndpoint(BaseAPIView): serializer_class = UserNotificationPreferenceSerializer # request the object - @allow_permission( - allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE" - ) def get(self, request): user_notification_preference = UserNotificationPreference.objects.get( user=request.user @@ -386,9 +382,6 @@ def get(self, request): return Response(serializer.data, status=status.HTTP_200_OK) # update the object - @allow_permission( - allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE" - ) def patch(self, request): user_notification_preference = UserNotificationPreference.objects.get( user=request.user diff --git a/apiserver/plane/app/views/page/base.py b/apiserver/plane/app/views/page/base.py index cda44b227b9..01fa6649c22 100644 --- a/apiserver/plane/app/views/page/base.py +++ b/apiserver/plane/app/views/page/base.py @@ -33,12 +33,13 @@ ProjectMember, ProjectPage, ) - +from plane.utils.error_codes import ERROR_CODES # Module imports from ..base import BaseAPIView, BaseViewSet from plane.bgtasks.page_transaction_task import page_transaction from plane.bgtasks.page_version_task import page_version +from plane.bgtasks.recent_visited_task import recent_visited_task def unarchive_archive_page_and_descendants(page_id, archived_at): @@ -221,6 +222,13 @@ def retrieve(self, request, slug, project_id, pk=None): ).values_list("entity_identifier", flat=True) data = PageDetailSerializer(page).data data["issue_ids"] = issue_ids + recent_visited_task.delay( + slug=slug, + entity_name="page", + entity_identifier=pk, + user_id=request.user.id, + project_id=project_id, + ) return Response( data, status=status.HTTP_200_OK, @@ -297,6 +305,13 @@ def archive(self, request, slug, project_id, pk): status=status.HTTP_400_BAD_REQUEST, ) + UserFavorite.objects.filter( + entity_type="page", + entity_identifier=pk, + project_id=project_id, + workspace__slug=slug, + ).delete() + unarchive_archive_page_and_descendants(pk, datetime.now()) return Response( @@ -471,6 +486,11 @@ def retrieve(self, request, slug, project_id, pk): .filter(Q(owned_by=self.request.user) | Q(access=0)) .first() ) + if page is None: + return Response( + {"error": "Page not found"}, + status=404, + ) binary_data = page.description_binary def stream_data(): @@ -505,14 +525,20 @@ def partial_update(self, request, slug, project_id, pk): if page.is_locked: return Response( - {"error": "Page is locked"}, - status=471, + { + "error_code": ERROR_CODES["PAGE_LOCKED"], + "error_message": "PAGE_LOCKED", + }, + status=status.HTTP_400_BAD_REQUEST, ) if page.archived_at: return Response( - {"error": "Page is archived"}, - status=472, + { + "error_code": ERROR_CODES["PAGE_ARCHIVED"], + "error_message": "PAGE_ARCHIVED", + }, + status=status.HTTP_400_BAD_REQUEST, ) # Serialize the existing instance diff --git a/apiserver/plane/app/views/page/version.py b/apiserver/plane/app/views/page/version.py index 70f6bd978f4..995a0626362 100644 --- a/apiserver/plane/app/views/page/version.py +++ b/apiserver/plane/app/views/page/version.py @@ -5,16 +5,18 @@ # Module imports from plane.db.models import PageVersion from ..base import BaseAPIView -from plane.app.permissions import ProjectEntityPermission -from plane.app.serializers import PageVersionSerializer +from plane.app.serializers import ( + PageVersionSerializer, + PageVersionDetailSerializer, +) +from plane.app.permissions import allow_permission, ROLE class PageVersionEndpoint(BaseAPIView): - permission_classes = [ - ProjectEntityPermission, - ] - + @allow_permission( + allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST] + ) def get(self, request, slug, project_id, page_id, pk=None): # Check if pk is provided if pk: @@ -25,7 +27,7 @@ def get(self, request, slug, project_id, page_id, pk=None): pk=pk, ) # Serialize the page version - serializer = PageVersionSerializer(page_version) + serializer = PageVersionDetailSerializer(page_version) return Response(serializer.data, status=status.HTTP_200_OK) # Return all page versions page_versions = PageVersion.objects.filter( diff --git a/apiserver/plane/app/views/project/base.py b/apiserver/plane/app/views/project/base.py index 2aec4b3bba2..ebc0e83fd8e 100644 --- a/apiserver/plane/app/views/project/base.py +++ b/apiserver/plane/app/views/project/base.py @@ -52,6 +52,7 @@ ) from plane.utils.cache import cache_response from plane.bgtasks.webhook_task import model_activity +from plane.bgtasks.recent_visited_task import recent_visited_task class ProjectViewSet(BaseViewSet): @@ -264,6 +265,14 @@ def retrieve(self, request, slug, pk): status=status.HTTP_404_NOT_FOUND, ) + recent_visited_task.delay( + slug=slug, + project_id=pk, + entity_name="project", + entity_identifier=pk, + user_id=request.user.id, + ) + serializer = ProjectListSerializer(project) return Response(serializer.data, status=status.HTTP_200_OK) @@ -484,6 +493,10 @@ def post(self, request, slug, project_id): project = Project.objects.get(pk=project_id, workspace__slug=slug) project.archived_at = timezone.now() project.save() + UserFavorite.objects.filter( + workspace__slug=slug, + project=project_id, + ).delete() return Response( {"archived_at": str(project.archived_at)}, status=status.HTTP_200_OK, diff --git a/apiserver/plane/app/views/view/base.py b/apiserver/plane/app/views/view/base.py index 96b4242a9c5..4a571ef2576 100644 --- a/apiserver/plane/app/views/view/base.py +++ b/apiserver/plane/app/views/view/base.py @@ -13,12 +13,13 @@ from django.db.models.functions import Coalesce from django.utils.decorators import method_decorator from django.views.decorators.gzip import gzip_page -from rest_framework import status from django.db import transaction # Third party imports +from rest_framework import status from rest_framework.response import Response +# Module imports from plane.app.permissions import ( allow_permission, ROLE, @@ -46,10 +47,8 @@ GroupedOffsetPaginator, SubGroupedOffsetPaginator, ) - -# Module imports +from plane.bgtasks.recent_visited_task import recent_visited_task from .. import BaseViewSet - from plane.db.models import ( UserFavorite, ) @@ -134,8 +133,23 @@ def partial_update(self, request, slug, pk): serializer.errors, status=status.HTTP_400_BAD_REQUEST ) + def retrieve(self, request, slug, pk): + issue_view = self.get_queryset().filter(pk=pk).first() + serializer = IssueViewSerializer(issue_view) + recent_visited_task.delay( + slug=slug, + project_id=None, + entity_name="view", + entity_identifier=pk, + user_id=request.user.id, + ) + return Response( + serializer.data, + status=status.HTTP_200_OK, + ) + @allow_permission( - allowed_roles=[ROLE.ADMIN], + allowed_roles=[], level="WORKSPACE", creator=True, model=IssueView, @@ -145,19 +159,6 @@ def destroy(self, request, slug, pk): pk=pk, workspace__slug=slug, ) - if not ( - WorkspaceMember.objects.filter( - workspace__slug=slug, - member=request.user, - role=20, - is_active=True, - ).exists() - and workspace_view.owned_by_id != request.user.id - ): - return Response( - {"error": "You do not have permission to delete this view"}, - status=status.HTTP_403_FORBIDDEN, - ) workspace_member = WorkspaceMember.objects.filter( workspace__slug=slug, @@ -444,6 +445,27 @@ def list(self, request, slug, project_id): ).data return Response(views, status=status.HTTP_200_OK) + allow_permission( + allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.VIEWER, ROLE.GUEST] + ) + + def retrieve(self, request, slug, project_id, pk): + issue_view = ( + self.get_queryset().filter(pk=pk, project_id=project_id).first() + ) + serializer = IssueViewSerializer(issue_view) + recent_visited_task.delay( + slug=slug, + project_id=project_id, + entity_name="view", + entity_identifier=pk, + user_id=request.user.id, + ) + return Response( + serializer.data, + status=status.HTTP_200_OK, + ) + allow_permission(allowed_roles=[], creator=True, model=IssueView) def partial_update(self, request, slug, project_id, pk): diff --git a/apiserver/plane/bgtasks/issue_activities_task.py b/apiserver/plane/bgtasks/issue_activities_task.py index 8f5b5d03dd0..f836023993e 100644 --- a/apiserver/plane/bgtasks/issue_activities_task.py +++ b/apiserver/plane/bgtasks/issue_activities_task.py @@ -1697,23 +1697,6 @@ def issue_activity( ) # Post the updates to segway for integrations and webhooks if len(issue_activities_created): - # Don't send activities if the actor is a bot - try: - if settings.PROXY_BASE_URL: - for issue_activity in issue_activities_created: - headers = {"Content-Type": "application/json"} - issue_activity_json = json.dumps( - IssueActivitySerializer(issue_activity).data, - cls=DjangoJSONEncoder, - ) - _ = requests.post( - f"{settings.PROXY_BASE_URL}/hooks/workspaces/{str(issue_activity.workspace_id)}/projects/{str(issue_activity.project_id)}/issues/{str(issue_activity.issue_id)}/issue-activity-hooks/", - json=issue_activity_json, - headers=headers, - ) - except Exception as e: - log_exception(e) - for activity in issue_activities_created: webhook_activity.delay( event=( diff --git a/apiserver/plane/bgtasks/recent_visited_task.py b/apiserver/plane/bgtasks/recent_visited_task.py new file mode 100644 index 00000000000..9569abf5ef1 --- /dev/null +++ b/apiserver/plane/bgtasks/recent_visited_task.py @@ -0,0 +1,61 @@ +# Python imports +from django.utils import timezone + +# Third party imports +from celery import shared_task + +# Module imports +from plane.db.models import UserRecentVisit, Workspace +from plane.utils.exception_logger import log_exception + + +@shared_task +def recent_visited_task( + entity_name, entity_identifier, user_id, project_id, slug +): + try: + workspace = Workspace.objects.get(slug=slug) + recent_visited = UserRecentVisit.objects.filter( + entity_name=entity_name, + entity_identifier=entity_identifier, + user_id=user_id, + project_id=project_id, + workspace_id=workspace.id, + ).first() + + if recent_visited: + recent_visited.visited_at = timezone.now() + recent_visited.save(update_fields=["visited_at"]) + else: + + recent_visited_count = UserRecentVisit.objects.filter( + user_id=user_id, workspace_id=workspace.id + ).count() + if recent_visited_count == 20: + recent_visited = ( + UserRecentVisit.objects.filter( + user_id=user_id, workspace_id=workspace.id + ) + .order_by("created_at") + .first() + ) + recent_visited.delete() + + recent_activity = UserRecentVisit.objects.create( + entity_name=entity_name, + entity_identifier=entity_identifier, + user_id=user_id, + visited_at=timezone.now(), + project_id=project_id, + workspace_id=workspace.id, + ) + recent_activity.created_by_id = user_id + recent_activity.updated_by_id = user_id + recent_activity.save( + update_fields=["created_by_id", "updated_by_id"] + ) + + return + except Exception as e: + log_exception(e) + return diff --git a/apiserver/plane/db/models/__init__.py b/apiserver/plane/db/models/__init__.py index 4874902a420..e7def641d5c 100644 --- a/apiserver/plane/db/models/__init__.py +++ b/apiserver/plane/db/models/__init__.py @@ -111,4 +111,4 @@ from .issue_type import IssueType -from .recent_visit import UserRecentVisit \ No newline at end of file +from .recent_visit import UserRecentVisit diff --git a/apiserver/plane/settings/common.py b/apiserver/plane/settings/common.py index 16d5f8c541b..9fed15516eb 100644 --- a/apiserver/plane/settings/common.py +++ b/apiserver/plane/settings/common.py @@ -299,9 +299,6 @@ ) -# Application Envs -PROXY_BASE_URL = os.environ.get("PROXY_BASE_URL", False) # For External - FILE_SIZE_LIMIT = int(os.environ.get("FILE_SIZE_LIMIT", 5242880)) # Unsplash Access key diff --git a/apiserver/plane/utils/error_codes.py b/apiserver/plane/utils/error_codes.py new file mode 100644 index 00000000000..15d38f6bf96 --- /dev/null +++ b/apiserver/plane/utils/error_codes.py @@ -0,0 +1,10 @@ +ERROR_CODES = { + # issues + "INVALID_ARCHIVE_STATE_GROUP": 4091, + "INVALID_ISSUE_DATES": 4100, + "INVALID_ISSUE_START_DATE": 4101, + "INVALID_ISSUE_TARGET_DATE": 4102, + # pages + "PAGE_LOCKED": 4701, + "PAGE_ARCHIVED": 4702, +} diff --git a/deploy/selfhost/docker-compose.yml b/deploy/selfhost/docker-compose.yml index 638452fd761..c574c191509 100644 --- a/deploy/selfhost/docker-compose.yml +++ b/deploy/selfhost/docker-compose.yml @@ -138,7 +138,7 @@ services: plane-db: <<: *app-env - image: postgres:15.5-alpine + image: postgres:15.7-alpine pull_policy: if_not_present restart: unless-stopped command: postgres -c 'max_connections=1000' diff --git a/docker-compose-local.yml b/docker-compose-local.yml index aea05f958d9..ee7345b2e5f 100644 --- a/docker-compose-local.yml +++ b/docker-compose-local.yml @@ -31,7 +31,7 @@ services: MINIO_ROOT_PASSWORD: ${AWS_SECRET_ACCESS_KEY} plane-db: - image: postgres:15.2-alpine + image: postgres:15.7-alpine restart: unless-stopped networks: - dev_env diff --git a/docker-compose.yml b/docker-compose.yml index 08d40a85a2e..e833b9e7680 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -101,7 +101,7 @@ services: plane-db: container_name: plane-db - image: postgres:15.2-alpine + image: postgres:15.7-alpine restart: always command: postgres -c 'max_connections=1000' volumes: diff --git a/packages/editor/src/core/components/menus/ai-menu.tsx b/packages/editor/src/core/components/menus/ai-menu.tsx index 8a714a6552d..94c1f8e0ad2 100644 --- a/packages/editor/src/core/components/menus/ai-menu.tsx +++ b/packages/editor/src/core/components/menus/ai-menu.tsx @@ -87,6 +87,7 @@ export const AIFeaturesMenu: React.FC = (props) => { >
{menu?.({ + isOpen: isPopupVisible, onClose: hidePopup, })}
diff --git a/packages/editor/src/core/components/menus/index.ts b/packages/editor/src/core/components/menus/index.ts index da050b6831e..ed4f4e7cc64 100644 --- a/packages/editor/src/core/components/menus/index.ts +++ b/packages/editor/src/core/components/menus/index.ts @@ -1,4 +1,4 @@ -export * from "./bubble-menu"; export * from "./ai-menu"; +export * from "./bubble-menu"; export * from "./block-menu"; export * from "./menu-items"; diff --git a/packages/editor/src/core/extensions/side-menu.tsx b/packages/editor/src/core/extensions/side-menu.tsx index ccc13ed5eb0..75577c74fc6 100644 --- a/packages/editor/src/core/extensions/side-menu.tsx +++ b/packages/editor/src/core/extensions/side-menu.tsx @@ -182,7 +182,7 @@ const SideMenu = (options: SideMenuPluginProps) => { aiHandleDOMEvents?.mousemove?.(); } }, - keydown: () => hideSideMenu(), + // keydown: () => hideSideMenu(), mousewheel: () => hideSideMenu(), dragenter: (view) => { if (handlesConfig.dragDrop) { diff --git a/packages/editor/src/core/hooks/use-editor.ts b/packages/editor/src/core/hooks/use-editor.ts index f9e8fdd609f..523c4be0fa1 100644 --- a/packages/editor/src/core/hooks/use-editor.ts +++ b/packages/editor/src/core/hooks/use-editor.ts @@ -126,8 +126,8 @@ export const useEditor = (props: CustomEditorProps) => { useImperativeHandle( forwardedRef, () => ({ - clearEditor: () => { - editorRef.current?.commands.clearContent(); + clearEditor: (emitUpdate = false) => { + editorRef.current?.commands.clearContent(emitUpdate); }, setEditorValue: (content: string) => { editorRef.current?.commands.setContent(content); diff --git a/packages/editor/src/core/types/ai.ts b/packages/editor/src/core/types/ai.ts index f5470f51c0a..448482e6543 100644 --- a/packages/editor/src/core/types/ai.ts +++ b/packages/editor/src/core/types/ai.ts @@ -1,7 +1,8 @@ -type TMenuProps = { +export type TAIMenuProps = { + isOpen: boolean; onClose: () => void; }; export type TAIHandler = { - menu?: (props: TMenuProps) => React.ReactNode; + menu?: (props: TAIMenuProps) => React.ReactNode; }; diff --git a/packages/editor/src/core/types/editor.ts b/packages/editor/src/core/types/editor.ts index ac804b9b14f..b26fac38445 100644 --- a/packages/editor/src/core/types/editor.ts +++ b/packages/editor/src/core/types/editor.ts @@ -6,7 +6,7 @@ import { IMentionHighlight, IMentionSuggestion, TDisplayConfig, TEditorCommands, export type EditorReadOnlyRefApi = { getMarkDown: () => string; getHTML: () => string; - clearEditor: () => void; + clearEditor: (emitUpdate?: boolean) => void; setEditorValue: (content: string) => void; scrollSummary: (marking: IMarking) => void; }; diff --git a/packages/editor/src/styles/editor.css b/packages/editor/src/styles/editor.css index 2ec35dc259f..1117b7c5cb0 100644 --- a/packages/editor/src/styles/editor.css +++ b/packages/editor/src/styles/editor.css @@ -79,6 +79,7 @@ -moz-appearance: textfield; } +/* Placeholder only for the first line in an empty editor. */ .ProseMirror p.is-editor-empty:first-child::before { content: attr(data-placeholder); float: left; @@ -87,6 +88,15 @@ height: 0; } +/* Display Placeholders on every new line. */ +.ProseMirror p.is-empty::before { + content: attr(data-placeholder); + float: left; + color: rgb(var(--color-text-400)); + pointer-events: none; + height: 0; +} + .ProseMirror li blockquote { margin-top: 10px; padding-inline-start: 1em; @@ -110,14 +120,6 @@ display: none; } -.ProseMirror .is-empty::before { - content: attr(data-placeholder); - float: left; - color: rgb(var(--color-text-400)); - pointer-events: none; - height: 0; -} - /* Custom image styles */ .ProseMirror img { transition: filter 0.1s ease-in-out; diff --git a/packages/types/src/pages.d.ts b/packages/types/src/pages.d.ts index ea9b8b8ea5b..a78ff30568b 100644 --- a/packages/types/src/pages.d.ts +++ b/packages/types/src/pages.d.ts @@ -48,3 +48,19 @@ export type TPageFilters = { }; export type TPageEmbedType = "mention" | "issue"; + +export type TPageVersion = { + created_at: string; + created_by: string; + deleted_at: string | null; + description_binary?: string | null; + description_html?: string | null; + description_json?: object; + id: string; + last_saved_at: string; + owned_by: string; + page: string; + updated_at: string; + updated_by: string; + workspace: string; +} \ No newline at end of file diff --git a/packages/ui/src/dropdowns/custom-menu.tsx b/packages/ui/src/dropdowns/custom-menu.tsx index 00e8c3e2a61..0059e64983d 100644 --- a/packages/ui/src/dropdowns/custom-menu.tsx +++ b/packages/ui/src/dropdowns/custom-menu.tsx @@ -35,6 +35,7 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => { tabIndex, closeOnSelect, openOnHover = false, + useCaptureForOutsideClick = false, } = props; const [referenceElement, setReferenceElement] = React.useState(null); @@ -88,7 +89,7 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => { } }; - useOutsideClickDetector(dropdownRef, closeDropdown); + useOutsideClickDetector(dropdownRef, closeDropdown, useCaptureForOutsideClick); let menuItems = ( diff --git a/packages/ui/src/dropdowns/helper.tsx b/packages/ui/src/dropdowns/helper.tsx index 8566f183b23..56963cbf4bc 100644 --- a/packages/ui/src/dropdowns/helper.tsx +++ b/packages/ui/src/dropdowns/helper.tsx @@ -17,6 +17,7 @@ export interface IDropdownProps { optionsClassName?: string; placement?: Placement; tabIndex?: number; + useCaptureForOutsideClick?: boolean; } export interface ICustomMenuDropdownProps extends IDropdownProps { diff --git a/packages/ui/src/hooks/use-outside-click-detector.tsx b/packages/ui/src/hooks/use-outside-click-detector.tsx index c1a47780377..608b88df0aa 100644 --- a/packages/ui/src/hooks/use-outside-click-detector.tsx +++ b/packages/ui/src/hooks/use-outside-click-detector.tsx @@ -1,7 +1,7 @@ import React, { useEffect } from "react"; // TODO: move it to helpers package -const useOutsideClickDetector = (ref: React.RefObject, callback: () => void) => { +const useOutsideClickDetector = (ref: React.RefObject, callback: () => void, useCapture = false) => { const handleClick = (event: MouseEvent) => { if (ref.current && !ref.current.contains(event.target as Node)) { // get all the element with attribute name data-prevent-outside-click @@ -31,10 +31,10 @@ const useOutsideClickDetector = (ref: React.RefObject, callback: () }; useEffect(() => { - document.addEventListener("mousedown", handleClick); + document.addEventListener("mousedown", handleClick, useCapture); return () => { - document.removeEventListener("mousedown", handleClick); + document.removeEventListener("mousedown", handleClick, useCapture); }; }); }; diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/inbox/header.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/inbox/header.tsx index 20d5befb275..9d977116dd8 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/inbox/header.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/inbox/header.tsx @@ -9,8 +9,10 @@ import { Breadcrumbs, Button, Intake } from "@plane/ui"; // components import { BreadcrumbLink, Logo } from "@/components/common"; import { InboxIssueCreateEditModalRoot } from "@/components/inbox"; +// constants +import { EUserProjectRoles } from "@/constants/project"; // hooks -import { useProject, useProjectInbox } from "@/hooks/store"; +import { useProject, useProjectInbox, useUser } from "@/hooks/store"; export const ProjectInboxHeader: FC = observer(() => { // states @@ -18,9 +20,15 @@ export const ProjectInboxHeader: FC = observer(() => { // router const { workspaceSlug, projectId } = useParams(); // store hooks + const { + membership: { currentProjectRole }, + } = useUser(); const { currentProjectDetails, loader: currentProjectDetailsLoader } = useProject(); const { loader } = useProjectInbox(); + // derived value + const isViewer = currentProjectRole === EUserProjectRoles.VIEWER; + return (
@@ -58,7 +66,7 @@ export const ProjectInboxHeader: FC = observer(() => {
- {currentProjectDetails?.inbox_view && workspaceSlug && projectId && ( + {currentProjectDetails?.inbox_view && workspaceSlug && projectId && !isViewer && (
{ <>
-
+
diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/header.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/header.tsx index 6cbf91c8833..a4f8499a2a0 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/header.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/header.tsx @@ -2,7 +2,7 @@ import { useState } from "react"; import { observer } from "mobx-react"; -import { useParams } from "next/navigation"; +import { useParams, useSearchParams } from "next/navigation"; import { FileText } from "lucide-react"; // types import { TLogoProps } from "@plane/types"; @@ -25,6 +25,7 @@ export interface IPagesHeaderProps { export const PageDetailsHeader = observer(() => { // router const { workspaceSlug, pageId } = useParams(); + const searchParams = useSearchParams(); // state const [isOpen, setIsOpen] = useState(false); // store hooks @@ -56,6 +57,8 @@ export const PageDetailsHeader = observer(() => { } }; + const isVersionHistoryOverlayActive = !!searchParams.get("version"); + return (
@@ -158,7 +161,7 @@ export const PageDetailsHeader = observer(() => {
- {isContentEditable && ( + {isContentEditable && !isVersionHistoryOverlayActive && ( -
-
- )} - /> - )} -
-
- ( - { - onChange(e.target.value); - handleFormChange(); - }} - ref={issueTitleRef || ref} - hasError={Boolean(errors.name)} - placeholder="Title" - className="w-full text-base" - tabIndex={getTabIndex("name")} - autoFocus - /> - )} - /> - {errors?.name?.message} -
-
- {data?.description_html === undefined || !projectId ? ( - - -
- - -
-
- - -
- -
- -
-
- - -
-
- ) : ( - <> - ( - { - onChange(description_html); - handleFormChange(); - }} - onEnterKeyPress={() => submitBtnRef?.current?.click()} - ref={editorRef} - tabIndex={getTabIndex("description_html")} - placeholder={getDescriptionPlaceholder} - containerClassName="pt-3 min-h-[150px]" - /> - )} - /> -
- {issueName && issueName.trim() !== "" && config?.has_openai_configured && ( - - )} - {config?.has_openai_configured && projectId && ( - { - setGptAssistantModal((prevData) => !prevData); - // this is done so that the title do not reset after gpt popover closed - reset(getValues()); - }} - onResponse={(response) => { - handleAiAssistance(response); - }} - placement="top-end" - button={ - - } - /> - )} -
- - )} -
-
- ( -
- { - onChange(stateId); - handleFormChange(); - }} - projectId={projectId ?? undefined} - buttonVariant="border-with-text" - tabIndex={getTabIndex("state_id")} - /> -
- )} - /> - ( -
- { - onChange(priority); - handleFormChange(); - }} - buttonVariant="border-with-text" - tabIndex={getTabIndex("priority")} - /> -
- )} - /> - ( -
- { - onChange(assigneeIds); - handleFormChange(); - }} - buttonVariant={value?.length > 0 ? "transparent-without-text" : "border-with-text"} - buttonClassName={value?.length > 0 ? "hover:bg-transparent" : ""} - placeholder="Assignees" - multiple - tabIndex={getTabIndex("assignee_ids")} - /> -
- )} - /> - ( -
- { - onChange(labelIds); - handleFormChange(); - }} - projectId={projectId ?? undefined} - tabIndex={getTabIndex("label_ids")} - /> -
- )} - /> - ( -
- { - onChange(date ? renderFormattedPayloadDate(date) : null); - handleFormChange(); - }} - buttonVariant="border-with-text" - maxDate={maxDate ?? undefined} - placeholder="Start date" - tabIndex={getTabIndex("start_date")} - /> -
- )} - /> - ( -
- { - onChange(date ? renderFormattedPayloadDate(date) : null); - handleFormChange(); - }} - buttonVariant="border-with-text" - minDate={minDate ?? undefined} - placeholder="Due date" - tabIndex={getTabIndex("target_date")} - /> -
- )} - /> - {projectDetails?.cycle_view && ( - ( -
- { - onChange(cycleId); - handleFormChange(); - }} - placeholder="Cycle" - value={value} - buttonVariant="border-with-text" - tabIndex={getTabIndex("cycle_id")} - /> -
- )} - /> - )} - {projectDetails?.module_view && workspaceSlug && ( - ( -
- { - onChange(moduleIds); - handleFormChange(); - }} - placeholder="Modules" - buttonVariant="border-with-text" - tabIndex={getTabIndex("module_ids")} - multiple - showCount - /> -
- )} - /> - )} - {projectId && areEstimateEnabledByProjectId(projectId) && ( - ( -
- { - onChange(estimatePoint); - handleFormChange(); - }} - projectId={projectId} - buttonVariant="border-with-text" - tabIndex={getTabIndex("estimate_point")} - placeholder="Estimate" - /> -
- )} - /> - )} - {watch("parent_id") ? ( - - - - {selectedParentIssue && - `${selectedParentIssue.project__identifier}-${selectedParentIssue.sequence_id}`} - - - } - placement="bottom-start" - tabIndex={getTabIndex("parent_id")} - > - <> - setParentIssueListModalOpen(true)}> - Change parent issue - - ( - { - onChange(null); - handleFormChange(); - }} - > - Remove parent issue - - )} - /> - - - ) : ( - - )} - ( - setParentIssueListModalOpen(false)} - onChange={(issue) => { - onChange(issue.id); - handleFormChange(); - setSelectedParentIssue(issue); - }} - projectId={projectId ?? undefined} - issueId={isDraft ? undefined : data?.id} - /> - )} - /> -
-
-
-
-
- {!data?.id && ( -
onCreateMoreToggleChange(!isCreateMoreToggleEnabled)} - onKeyDown={(e) => { - if (e.key === "Enter") onCreateMoreToggleChange(!isCreateMoreToggleEnabled); - }} - tabIndex={getTabIndex("create_more")} - role="button" - > - {}} size="sm" /> - Create more -
- )} -
-
- - {isDraft && ( - <> - {data?.id ? ( - - ) : ( - - )} - - )} - -
-
- - - ); -}); diff --git a/web/ce/components/issues/issue-modal/index.ts b/web/ce/components/issues/issue-modal/index.ts index ba2baa60a7f..f2c8494163f 100644 --- a/web/ce/components/issues/issue-modal/index.ts +++ b/web/ce/components/issues/issue-modal/index.ts @@ -1,3 +1,3 @@ -export * from "./form"; -export * from "./draft-issue-layout"; -export * from "./modal"; +export * from "./provider"; +export * from "./issue-type-select"; +export * from "./additional-properties"; diff --git a/web/ce/components/issues/issue-modal/issue-type-select.tsx b/web/ce/components/issues/issue-modal/issue-type-select.tsx new file mode 100644 index 00000000000..a4b60103d6a --- /dev/null +++ b/web/ce/components/issues/issue-modal/issue-type-select.tsx @@ -0,0 +1,12 @@ +import { Control } from "react-hook-form"; +// types +import { TIssue } from "@plane/types"; + +type TIssueTypeSelectProps = { + control: Control; + projectId: string | null; + disabled?: boolean; + handleFormChange: () => void; +}; + +export const IssueTypeSelect: React.FC = () => <>; diff --git a/web/ce/components/issues/issue-modal/provider.tsx b/web/ce/components/issues/issue-modal/provider.tsx new file mode 100644 index 00000000000..f387feb5a82 --- /dev/null +++ b/web/ce/components/issues/issue-modal/provider.tsx @@ -0,0 +1,28 @@ +import React from "react"; +import { observer } from "mobx-react-lite"; +// components +import { IssueModalContext } from "@/components/issues"; + +type TIssueModalProviderProps = { + children: React.ReactNode; +}; + +export const IssueModalProvider = observer((props: TIssueModalProviderProps) => { + const { children } = props; + return ( + {}, + issuePropertyValueErrors: {}, + setIssuePropertyValueErrors: () => {}, + getIssueTypeIdOnProjectChange: () => null, + getActiveAdditionalPropertiesLength: () => 0, + handlePropertyValuesValidation: () => true, + handleCreateUpdatePropertyValues: () => Promise.resolve(), + }} + > + {children} + + ); +}); diff --git a/web/ce/components/pages/editor/ai/menu.tsx b/web/ce/components/pages/editor/ai/menu.tsx index 7610595f73f..edbed725df9 100644 --- a/web/ce/components/pages/editor/ai/menu.tsx +++ b/web/ce/components/pages/editor/ai/menu.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { RefObject, useRef, useState } from "react"; +import React, { RefObject, useEffect, useRef, useState } from "react"; import { useParams } from "next/navigation"; import { ChevronRight, CornerDownRight, LucideIcon, RefreshCcw, Sparkles, TriangleAlert } from "lucide-react"; // plane editor @@ -20,6 +20,7 @@ const aiService = new AIService(); type Props = { editorRef: RefObject; + isOpen: boolean; onClose: () => void; }; @@ -57,7 +58,7 @@ const TONES_LIST = [ ]; export const EditorAIMenu: React.FC = (props) => { - const { editorRef, onClose } = props; + const { editorRef, isOpen, onClose } = props; // states const [activeTask, setActiveTask] = useState(null); const [response, setResponse] = useState(undefined); @@ -126,6 +127,14 @@ export const EditorAIMenu: React.FC = (props) => { onClose(); }; + // reset on close + useEffect(() => { + if (!isOpen) { + setActiveTask(null); + setResponse(undefined); + } + }, [isOpen]); + return (
{ + const monthlyCost = monthlyPrice * 12; + const yearlyCost = yearlyPricePerMonth * 12; + const amountSaved = monthlyCost - yearlyCost; + const discountPercentage = (amountSaved / monthlyCost) * 100; + return Math.floor(discountPercentage); +}; + const PRO_PLAN_PRICES: TProPlanPrice[] = [ - { key: "monthly", price: "$7", recurring: "month" }, - { key: "yearly", price: "$5", recurring: "year" }, + { key: "monthly", currency: "$", price: 8, recurring: "month" }, + { key: "yearly", currency: "$", price: 6, recurring: "year" }, ]; export const ProPlanUpgrade: FC = (props) => { const { basePlan, features, verticalFeatureList = false, extraFeatures } = props; // states const [selectedPlan, setSelectedPlan] = useState("month"); + // derived + const monthlyPrice = PRO_PLAN_PRICES.find((price) => price.recurring === "month")?.price ?? 0; + const yearlyPrice = PRO_PLAN_PRICES.find((price) => price.recurring === "year")?.price ?? 0; + const yearlyDiscount = calculateYearlyDiscount(monthlyPrice, yearlyPrice); // env const PRO_PLAN_MONTHLY_PAYMENT_URL = process.env.NEXT_PUBLIC_PRO_PLAN_MONTHLY_PAYMENT_URL ?? "https://plane.so/pro"; const PRO_PLAN_YEARLY_PAYMENT_URL = process.env.NEXT_PUBLIC_PRO_PLAN_YEARLY_PAYMENT_URL ?? "https://plane.so/pro"; @@ -55,7 +69,7 @@ export const ProPlanUpgrade: FC = (props) => { {price.recurring === "year" && ("Yearly" as string)} {price.recurring === "year" && ( - -28% + -{yearlyDiscount}% )} @@ -69,8 +83,8 @@ export const ProPlanUpgrade: FC = (props) => {
Plane Pro
- {price.recurring === "month" && "$7"} - {price.recurring === "year" && "$5"} + {price.currency} + {price.price}
a user per month
diff --git a/web/ce/types/index.ts b/web/ce/types/index.ts new file mode 100644 index 00000000000..0d4b66523e9 --- /dev/null +++ b/web/ce/types/index.ts @@ -0,0 +1,2 @@ +export * from "./projects"; +export * from "./issue-types"; diff --git a/web/ce/types/issue-types/index.ts b/web/ce/types/issue-types/index.ts new file mode 100644 index 00000000000..7259fa35181 --- /dev/null +++ b/web/ce/types/issue-types/index.ts @@ -0,0 +1 @@ +export * from "./issue-property-values.d"; diff --git a/web/ce/types/issue-types/issue-property-values.d.ts b/web/ce/types/issue-types/issue-property-values.d.ts new file mode 100644 index 00000000000..e1d94dbc844 --- /dev/null +++ b/web/ce/types/issue-types/issue-property-values.d.ts @@ -0,0 +1,2 @@ +export type TIssuePropertyValues = object; +export type TIssuePropertyValueErrors = object; diff --git a/web/core/components/core/render-if-visible-HOC.tsx b/web/core/components/core/render-if-visible-HOC.tsx index a2259c6ca56..18c071d1462 100644 --- a/web/core/components/core/render-if-visible-HOC.tsx +++ b/web/core/components/core/render-if-visible-HOC.tsx @@ -10,6 +10,7 @@ type Props = { as?: keyof JSX.IntrinsicElements; classNames?: string; placeholderChildren?: ReactNode; + defaultValue?: boolean; }; const RenderIfVisible: React.FC = (props) => { @@ -20,10 +21,11 @@ const RenderIfVisible: React.FC = (props) => { horizontalOffset = 0, as = "div", children, + defaultValue = false, classNames = "", placeholderChildren = null, //placeholder children } = props; - const [shouldVisible, setShouldVisible] = useState(); + const [shouldVisible, setShouldVisible] = useState(defaultValue); const placeholderHeight = useRef(defaultHeight); const intersectionRef = useRef(null); diff --git a/web/core/components/empty-state/comic-box-button.tsx b/web/core/components/empty-state/comic-box-button.tsx index 83b2219fd61..6a1a1e59cc0 100644 --- a/web/core/components/empty-state/comic-box-button.tsx +++ b/web/core/components/empty-state/comic-box-button.tsx @@ -1,6 +1,6 @@ "use client"; -import { Ref, useState } from "react"; +import { Fragment, Ref, useState } from "react"; import { usePopper } from "react-popper"; import { Popover } from "@headlessui/react"; // popper @@ -43,33 +43,35 @@ export const ComicBoxButton: React.FC = (props) => { }); return ( - - -
- {icon} - {label} - -
-
- -
+ + + {isHovered && ( - } - style={styles.popper} - {...attributes.popper} - static - > -
-

{title}

-

{description}

+ +
} + style={styles.popper} + {...attributes.popper} + > +
+

{title}

+

{description}

+
)} diff --git a/web/core/components/inbox/content/inbox-issue-header.tsx b/web/core/components/inbox/content/inbox-issue-header.tsx index 8d4561fc12d..6384b4e9b8b 100644 --- a/web/core/components/inbox/content/inbox-issue-header.tsx +++ b/web/core/components/inbox/content/inbox-issue-header.tsx @@ -83,7 +83,9 @@ export const InboxIssueActionsHeader: FC = observer((p const canMarkAsDuplicate = isAllowed && (inboxIssue?.status === 0 || inboxIssue?.status === -2); const canMarkAsAccepted = isAllowed && (inboxIssue?.status === 0 || inboxIssue?.status === -2); const canMarkAsDeclined = isAllowed && (inboxIssue?.status === 0 || inboxIssue?.status === -2); - const canDelete = isAllowed || inboxIssue?.created_by === currentUser?.id; + // can delete only if admin or is creator of the issue + const canDelete = + (!!currentProjectRole && currentProjectRole >= EUserProjectRoles.ADMIN) || issue?.created_by === currentUser?.id; const isAcceptedOrDeclined = inboxIssue?.status ? [-1, 1, 2].includes(inboxIssue.status) : undefined; // days left for snooze const numberOfDaysLeft = findHowManyDaysLeft(inboxIssue?.snoozed_till); diff --git a/web/core/components/issues/attachment/attachment-item-list.tsx b/web/core/components/issues/attachment/attachment-item-list.tsx index 75881e27127..a0126b25121 100644 --- a/web/core/components/issues/attachment/attachment-item-list.tsx +++ b/web/core/components/issues/attachment/attachment-item-list.tsx @@ -1,8 +1,9 @@ import { FC, useCallback, useState } from "react"; import { observer } from "mobx-react"; -import { useDropzone } from "react-dropzone"; +import { FileRejection, useDropzone } from "react-dropzone"; import { UploadCloud } from "lucide-react"; // hooks +import {TOAST_TYPE, setToast } from "@plane/ui"; import { MAX_FILE_SIZE } from "@/constants/common"; import { generateFileName } from "@/helpers/attachment.helper"; import { useInstance, useIssueDetail } from "@/hooks/store"; @@ -36,24 +37,46 @@ export const IssueAttachmentItemList: FC = observer((p const issueAttachments = getAttachmentsByIssueId(issueId); const onDrop = useCallback( - (acceptedFiles: File[]) => { - const currentFile: File = acceptedFiles[0]; - if (!currentFile || !workspaceSlug) return; + (acceptedFiles: File[], rejectedFiles:FileRejection[] ) => { + const totalAttachedFiles = acceptedFiles.length + rejectedFiles.length; - const uploadedFile: File = new File([currentFile], generateFileName(currentFile.name), { - type: currentFile.type, - }); - const formData = new FormData(); - formData.append("asset", uploadedFile); - formData.append( - "attributes", - JSON.stringify({ - name: uploadedFile.name, - size: uploadedFile.size, + if(rejectedFiles.length===0){ + const currentFile: File = acceptedFiles[0]; + if (!currentFile || !workspaceSlug) return; + + const uploadedFile: File = new File([currentFile], generateFileName(currentFile.name), { + type: currentFile.type, + }); + const formData = new FormData(); + formData.append("asset", uploadedFile); + formData.append( + "attributes", + JSON.stringify({ + name: uploadedFile.name, + size: uploadedFile.size, + }) + ); + setIsLoading(true); + handleAttachmentOperations.create(formData) + .catch(()=>{ + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: "File could not be attached. Try uploading again.", + }) }) - ); - setIsLoading(true); - handleAttachmentOperations.create(formData).finally(() => setIsLoading(false)); + .finally(() => setIsLoading(false)); + return; + } + + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: (totalAttachedFiles>1)? + "Only one file can be uploaded at a time." : + "File must be 5MB or less.", + }) + return; }, [handleAttachmentOperations, workspaceSlug] ); diff --git a/web/core/components/issues/issue-detail-widgets/attachments/quick-action-button.tsx b/web/core/components/issues/issue-detail-widgets/attachments/quick-action-button.tsx index 2d7a5de86f4..01923b21068 100644 --- a/web/core/components/issues/issue-detail-widgets/attachments/quick-action-button.tsx +++ b/web/core/components/issues/issue-detail-widgets/attachments/quick-action-button.tsx @@ -1,8 +1,9 @@ "use client"; import React, { FC, useCallback, useState } from "react"; import { observer } from "mobx-react"; -import { useDropzone } from "react-dropzone"; +import { FileRejection, useDropzone } from "react-dropzone"; import { Plus } from "lucide-react"; +import {TOAST_TYPE, setToast } from "@plane/ui"; // constants import { MAX_FILE_SIZE } from "@/constants/common"; // helper @@ -33,31 +34,54 @@ export const IssueAttachmentActionButton: FC = observer((props) => { // handlers const onDrop = useCallback( - (acceptedFiles: File[]) => { - const currentFile: File = acceptedFiles[0]; - if (!currentFile || !workspaceSlug) return; + (acceptedFiles: File[], rejectedFiles:FileRejection[] ) => { + const totalAttachedFiles = acceptedFiles.length + rejectedFiles.length; - const uploadedFile: File = new File([currentFile], generateFileName(currentFile.name), { - type: currentFile.type, - }); - const formData = new FormData(); - formData.append("asset", uploadedFile); - formData.append( - "attributes", - JSON.stringify({ - name: uploadedFile.name, - size: uploadedFile.size, + if(rejectedFiles.length===0){ + const currentFile: File = acceptedFiles[0]; + if (!currentFile || !workspaceSlug) return; + + const uploadedFile: File = new File([currentFile], generateFileName(currentFile.name), { + type: currentFile.type, + }); + const formData = new FormData(); + formData.append("asset", uploadedFile); + formData.append( + "attributes", + JSON.stringify({ + name: uploadedFile.name, + size: uploadedFile.size, + }) + ); + setIsLoading(true); + handleAttachmentOperations.create(formData) + .catch(()=>{ + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: "File could not be attached. Try uploading again.", + }) }) - ); - setIsLoading(true); - handleAttachmentOperations.create(formData).finally(() => { - setLastWidgetAction("attachments"); - setIsLoading(false); + .finally(() => { + setLastWidgetAction("attachments"); + setIsLoading(false); }); + return; + } + + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: (totalAttachedFiles>1)? + "Only one file can be uploaded at a time." : + "File must be 5MB or less.", + }) + return; }, [handleAttachmentOperations, workspaceSlug] ); + const { getRootProps, getInputProps } = useDropzone({ onDrop, maxSize: config?.file_size_limit ?? MAX_FILE_SIZE, @@ -71,4 +95,4 @@ export const IssueAttachmentActionButton: FC = observer((props) => { {customButton ? customButton : } ); -}); +}); \ No newline at end of file diff --git a/web/core/components/issues/issue-detail/issue-activity/comments/comment-create.tsx b/web/core/components/issues/issue-detail/issue-activity/comments/comment-create.tsx index 9040ab268b6..d8c86ec1828 100644 --- a/web/core/components/issues/issue-detail/issue-activity/comments/comment-create.tsx +++ b/web/core/components/issues/issue-detail/issue-activity/comments/comment-create.tsx @@ -7,9 +7,10 @@ import { LiteTextEditor } from "@/components/editor/lite-text-editor/lite-text-e // constants import { EIssueCommentAccessSpecifier } from "@/constants/issue"; // helpers +import { cn } from "@/helpers/common.helper"; import { isEmptyHtmlString } from "@/helpers/string.helper"; // hooks -import { useWorkspace } from "@/hooks/store"; +import { useIssueDetail, useWorkspace } from "@/hooks/store"; // editor import { TActivityOperations } from "../root"; @@ -27,6 +28,7 @@ export const IssueCommentCreate: FC = (props) => { const editorRef = useRef(null); // store hooks const workspaceStore = useWorkspace(); + const { peekIssue } = useIssueDetail(); // derived values const workspaceId = workspaceStore.getWorkspaceBySlug(workspaceSlug as string)?.id as string; // form info @@ -58,6 +60,9 @@ export const IssueCommentCreate: FC = (props) => { return (
{ if (e.key === "Enter" && !e.shiftKey && !e.ctrlKey && !e.metaKey && !isEmpty && !isSubmitting) handleSubmit(onSubmit)(e); diff --git a/web/core/components/issues/issue-layouts/list/block-root.tsx b/web/core/components/issues/issue-layouts/list/block-root.tsx index 2302e9e2c89..71bcc6f15c9 100644 --- a/web/core/components/issues/issue-layouts/list/block-root.tsx +++ b/web/core/components/issues/issue-layouts/list/block-root.tsx @@ -16,7 +16,7 @@ import { useIssueDetail } from "@/hooks/store"; import { TSelectionHelper } from "@/hooks/use-multiple-select"; import useOutsideClickDetector from "@/hooks/use-outside-click-detector"; // types -import { HIGHLIGHT_CLASS, getIssueBlockId } from "../utils"; +import { HIGHLIGHT_CLASS, getIssueBlockId, isIssueNew } from "../utils"; import { TRenderQuickActions } from "./list-view-types"; type Props = { @@ -36,6 +36,7 @@ type Props = { canDropOverIssue: boolean; isParentIssueBeingDragged?: boolean; isLastChild?: boolean; + shouldRenderByDefault?: boolean; }; export const IssueBlockRoot: FC = observer((props) => { @@ -56,6 +57,7 @@ export const IssueBlockRoot: FC = observer((props) => { isParentIssueBeingDragged = false, isLastChild = false, selectionHelpers, + shouldRenderByDefault, } = props; // states const [isExpanded, setExpanded] = useState(false); @@ -114,7 +116,7 @@ export const IssueBlockRoot: FC = observer((props) => { issueBlockRef?.current?.classList?.remove(HIGHLIGHT_CLASS); }); - if (!issueId) return null; + if (!issueId || !issuesMap[issueId]?.created_at) return null; const subIssues = subIssuesStore.subIssuesByIssueId(issueId); return ( @@ -126,6 +128,7 @@ export const IssueBlockRoot: FC = observer((props) => { root={containerRef} classNames={`relative ${isLastChild && !isExpanded ? "" : "border-b border-b-custom-border-200"}`} verticalOffset={100} + defaultValue={shouldRenderByDefault || isIssueNew(issuesMap[issueId])} > = observer((props) => { isDragAllowed={isDragAllowed} canDropOverIssue={canDropOverIssue} isParentIssueBeingDragged={isParentIssueBeingDragged || isCurrentBlockDragging} + shouldRenderByDefault={isExpanded} /> ))} {isLastChild && } 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 3e4fd6db57c..6ce0ecd1c1a 100644 --- a/web/core/components/issues/issue-layouts/properties/all-properties.tsx +++ b/web/core/components/issues/issue-layouts/properties/all-properties.tsx @@ -32,8 +32,8 @@ import { useAppRouter } from "@/hooks/use-app-router"; import { useIssueStoreType } from "@/hooks/use-issue-layout-store"; import { useIsMobile } from "@/hooks/use-platform-os"; // local components -import { IssuePropertyLabels } from "../properties/labels"; -import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC"; +import { IssuePropertyLabels } from "./labels"; +import { WithDisplayPropertiesHOC } from "./with-display-properties-HOC"; export interface IIssueProperties { issue: TIssue; 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 2c12ed18be1..9ecbe44d0f0 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 @@ -163,6 +163,7 @@ export const AllIssueQuickActions: React.FC = observer((props placement={placements} menuItemsClassName="z-[14]" maxHeight="lg" + useCaptureForOutsideClick closeOnSelect > {MENU_ITEMS.map((item) => { 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 2134da80f27..ece43e585ec 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 @@ -125,6 +125,7 @@ export const ArchivedIssueQuickActions: React.FC = observer(( placement={placements} menuItemsClassName="z-[14]" maxHeight="lg" + useCaptureForOutsideClick closeOnSelect > {MENU_ITEMS.map((item) => { 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 6e5187b64b0..2a08bac9d11 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 @@ -183,6 +183,7 @@ export const CycleIssueQuickActions: React.FC = observer((pro portalElement={portalElement} menuItemsClassName="z-[14]" maxHeight="lg" + useCaptureForOutsideClick closeOnSelect > {MENU_ITEMS.map((item) => { 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 cdf5c4bc6a9..58807aee240 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 @@ -115,6 +115,7 @@ export const DraftIssueQuickActions: React.FC = observer((pro placement={placements} menuItemsClassName="z-[14]" maxHeight="lg" + useCaptureForOutsideClick closeOnSelect > {MENU_ITEMS.map((item) => { 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 1bfa57d06a7..8ec30042b75 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 @@ -180,6 +180,7 @@ export const ModuleIssueQuickActions: React.FC = observer((pr portalElement={portalElement} menuItemsClassName="z-[14]" maxHeight="lg" + useCaptureForOutsideClick closeOnSelect > {MENU_ITEMS.map((item) => { 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 c47d76af77b..40b53090be2 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 @@ -174,6 +174,7 @@ export const ProjectIssueQuickActions: React.FC = observer((p portalElement={portalElement} menuItemsClassName="z-[14]" maxHeight="lg" + useCaptureForOutsideClick closeOnSelect > {MENU_ITEMS.map((item) => { diff --git a/web/core/components/issues/issue-layouts/utils.tsx b/web/core/components/issues/issue-layouts/utils.tsx index 7840bb46217..ec333ef9230 100644 --- a/web/core/components/issues/issue-layouts/utils.tsx +++ b/web/core/components/issues/issue-layouts/utils.tsx @@ -606,4 +606,16 @@ export const isSubGrouped = (groupedIssueIds: TGroupedIssues) => { } return true; -}; \ No newline at end of file +}; + +/** + * This Method returns if the issue is new or not + * @param issue + * @returns + */ +export const isIssueNew = (issue: TIssue) => { + const createdDate = new Date(issue.created_at); + const currentDate = new Date(); + const diff = currentDate.getTime() - createdDate.getTime(); + return diff < 30000; +}; diff --git a/web/ce/components/issues/issue-modal/modal.tsx b/web/core/components/issues/issue-modal/base.tsx similarity index 93% rename from web/ce/components/issues/issue-modal/modal.tsx rename to web/core/components/issues/issue-modal/base.tsx index f671709916f..92c0be51c60 100644 --- a/web/ce/components/issues/issue-modal/modal.tsx +++ b/web/core/components/issues/issue-modal/base.tsx @@ -7,30 +7,21 @@ import { useParams, usePathname } from "next/navigation"; import type { TIssue } from "@plane/types"; // ui import { EModalPosition, EModalWidth, ModalCore, TOAST_TYPE, setToast } from "@plane/ui"; -import { CreateIssueToastActionItems } from "@/components/issues"; +import { CreateIssueToastActionItems, IssuesModalProps } from "@/components/issues"; // constants import { ISSUE_CREATED, ISSUE_UPDATED } from "@/constants/event-tracker"; import { EIssuesStoreType } from "@/constants/issue"; // hooks +import { useIssueModal } from "@/hooks/context/use-issue-modal"; import { useEventTracker, useCycle, useIssues, useModule, useProject, useIssueDetail } from "@/hooks/store"; import { useIssueStoreType } from "@/hooks/use-issue-layout-store"; import { useIssuesActions } from "@/hooks/use-issues-actions"; import useLocalStorage from "@/hooks/use-local-storage"; -// components +// local components import { DraftIssueLayout } from "./draft-issue-layout"; import { IssueFormRoot } from "./form"; -export interface IssuesModalProps { - data?: Partial; - isOpen: boolean; - onClose: () => void; - onSubmit?: (res: TIssue) => Promise; - withDraftIssueWrapper?: boolean; - storeType?: EIssuesStoreType; - isDraft?: boolean; -} - -export const CreateUpdateIssueModal: React.FC = observer((props) => { +export const CreateUpdateIssueModalBase: React.FC = observer((props) => { const { data, isOpen, @@ -60,6 +51,7 @@ export const CreateUpdateIssueModal: React.FC = observer((prop const { issues: projectIssues } = useIssues(EIssuesStoreType.PROJECT); const { issues: draftIssues } = useIssues(EIssuesStoreType.DRAFT); const { fetchIssue } = useIssueDetail(); + const { handleCreateUpdatePropertyValues } = useIssueModal(); // pathname const pathname = usePathname(); // local storage @@ -190,6 +182,15 @@ export const CreateUpdateIssueModal: React.FC = observer((prop await addIssueToModule(response, payload.module_ids); } + // add other property values + if (response.id && response.project_id) { + await handleCreateUpdatePropertyValues({ + issueId: response.id, + projectId: response.project_id, + workspaceSlug: workspaceSlug.toString(), + }); + } + setToast({ type: TOAST_TYPE.SUCCESS, title: "Success!", @@ -234,6 +235,13 @@ export const CreateUpdateIssueModal: React.FC = observer((prop ? await draftIssues.updateIssue(workspaceSlug.toString(), payload.project_id, data.id, payload) : updateIssue && (await updateIssue(payload.project_id, data.id, payload)); + // add other property values + await handleCreateUpdatePropertyValues({ + issueId: data.id, + projectId: payload.project_id, + workspaceSlug: workspaceSlug.toString(), + }); + setToast({ type: TOAST_TYPE.SUCCESS, title: "Success!", diff --git a/web/core/components/issues/issue-modal/components/default-properties.tsx b/web/core/components/issues/issue-modal/components/default-properties.tsx new file mode 100644 index 00000000000..e0012d9b83a --- /dev/null +++ b/web/core/components/issues/issue-modal/components/default-properties.tsx @@ -0,0 +1,325 @@ +"use client"; + +import React, { useState } from "react"; +import { observer } from "mobx-react"; +import { Control, Controller } from "react-hook-form"; +import { LayoutPanelTop } from "lucide-react"; +// types +import { ISearchIssueResponse, TIssue } from "@plane/types"; +// ui +import { CustomMenu } from "@plane/ui"; +// components +import { + CycleDropdown, + DateDropdown, + EstimateDropdown, + ModuleDropdown, + PriorityDropdown, + MemberDropdown, + StateDropdown, +} from "@/components/dropdowns"; +import { ParentIssuesListModal } from "@/components/issues"; +import { IssueLabelSelect } from "@/components/issues/select"; +// helpers +import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper"; +import { getTabIndex } from "@/helpers/issue-modal.helper"; +// hooks +import { useProjectEstimates, useProject } from "@/hooks/store"; +// plane web components +import { IssueIdentifier } from "@/plane-web/components/issues"; + +type TIssueDefaultPropertiesProps = { + control: Control; + id: string | undefined; + projectId: string | null; + workspaceSlug: string; + selectedParentIssue: ISearchIssueResponse | null; + startDate: string | null; + targetDate: string | null; + parentId: string | null; + isDraft: boolean; + handleFormChange: () => void; + setLabelModal: React.Dispatch>; + setSelectedParentIssue: (issue: ISearchIssueResponse) => void; +}; + +export const IssueDefaultProperties: React.FC = observer((props) => { + const { + control, + id, + projectId, + workspaceSlug, + selectedParentIssue, + startDate, + targetDate, + parentId, + isDraft, + handleFormChange, + setLabelModal, + setSelectedParentIssue, + } = props; + // states + const [parentIssueListModalOpen, setParentIssueListModalOpen] = useState(false); + // store hooks + const { areEstimateEnabledByProjectId } = useProjectEstimates(); + const { getProjectById } = useProject(); + // derived values + const projectDetails = getProjectById(projectId); + + const minDate = getDate(startDate); + minDate?.setDate(minDate.getDate()); + + const maxDate = getDate(targetDate); + maxDate?.setDate(maxDate.getDate()); + + return ( +
+ ( +
+ { + onChange(stateId); + handleFormChange(); + }} + projectId={projectId ?? undefined} + buttonVariant="border-with-text" + tabIndex={getTabIndex("state_id")} + /> +
+ )} + /> + ( +
+ { + onChange(priority); + handleFormChange(); + }} + buttonVariant="border-with-text" + tabIndex={getTabIndex("priority")} + /> +
+ )} + /> + ( +
+ { + onChange(assigneeIds); + handleFormChange(); + }} + buttonVariant={value?.length > 0 ? "transparent-without-text" : "border-with-text"} + buttonClassName={value?.length > 0 ? "hover:bg-transparent" : ""} + placeholder="Assignees" + multiple + tabIndex={getTabIndex("assignee_ids")} + /> +
+ )} + /> + ( +
+ { + onChange(labelIds); + handleFormChange(); + }} + projectId={projectId ?? undefined} + tabIndex={getTabIndex("label_ids")} + /> +
+ )} + /> + ( +
+ { + onChange(date ? renderFormattedPayloadDate(date) : null); + handleFormChange(); + }} + buttonVariant="border-with-text" + maxDate={maxDate ?? undefined} + placeholder="Start date" + tabIndex={getTabIndex("start_date")} + /> +
+ )} + /> + ( +
+ { + onChange(date ? renderFormattedPayloadDate(date) : null); + handleFormChange(); + }} + buttonVariant="border-with-text" + minDate={minDate ?? undefined} + placeholder="Due date" + tabIndex={getTabIndex("target_date")} + /> +
+ )} + /> + {projectDetails?.cycle_view && ( + ( +
+ { + onChange(cycleId); + handleFormChange(); + }} + placeholder="Cycle" + value={value} + buttonVariant="border-with-text" + tabIndex={getTabIndex("cycle_id")} + /> +
+ )} + /> + )} + {projectDetails?.module_view && workspaceSlug && ( + ( +
+ { + onChange(moduleIds); + handleFormChange(); + }} + placeholder="Modules" + buttonVariant="border-with-text" + tabIndex={getTabIndex("module_ids")} + multiple + showCount + /> +
+ )} + /> + )} + {projectId && areEstimateEnabledByProjectId(projectId) && ( + ( +
+ { + onChange(estimatePoint); + handleFormChange(); + }} + projectId={projectId} + buttonVariant="border-with-text" + tabIndex={getTabIndex("estimate_point")} + placeholder="Estimate" + /> +
+ )} + /> + )} + {parentId ? ( + + {selectedParentIssue?.project_id && ( + + )} + + } + placement="bottom-start" + tabIndex={getTabIndex("parent_id")} + > + <> + setParentIssueListModalOpen(true)}> + Change parent issue + + ( + { + onChange(null); + handleFormChange(); + }} + > + Remove parent issue + + )} + /> + + + ) : ( + + )} + ( + setParentIssueListModalOpen(false)} + onChange={(issue) => { + onChange(issue.id); + handleFormChange(); + setSelectedParentIssue(issue); + }} + projectId={projectId ?? undefined} + issueId={isDraft ? undefined : id} + /> + )} + /> +
+ ); +}); diff --git a/web/core/components/issues/issue-modal/components/description-editor.tsx b/web/core/components/issues/issue-modal/components/description-editor.tsx new file mode 100644 index 00000000000..0a16f22dded --- /dev/null +++ b/web/core/components/issues/issue-modal/components/description-editor.tsx @@ -0,0 +1,230 @@ +"use client"; + +import React, { useEffect, useState } from "react"; +import { observer } from "mobx-react"; +import { Control, Controller } from "react-hook-form"; +import { Sparkle } from "lucide-react"; +// editor +import { EditorRefApi } from "@plane/editor"; +// types +import { TIssue } from "@plane/types"; +// ui +import { Loader, setToast, TOAST_TYPE } from "@plane/ui"; +// components +import { GptAssistantPopover } from "@/components/core"; +import { RichTextEditor } from "@/components/editor"; +// helpers +import { getTabIndex } from "@/helpers/issue-modal.helper"; +import { getDescriptionPlaceholder } from "@/helpers/issue.helper"; +// hooks +import { useInstance, useWorkspace } from "@/hooks/store"; +import useKeypress from "@/hooks/use-keypress"; +// services +import { AIService } from "@/services/ai.service"; + +type TIssueDescriptionEditorProps = { + control: Control; + issueName: string; + descriptionHtmlData: string | undefined; + editorRef: React.MutableRefObject; + submitBtnRef: React.MutableRefObject; + gptAssistantModal: boolean; + workspaceSlug: string; + projectId: string | null; + handleFormChange: () => void; + handleDescriptionHTMLDataChange: (descriptionHtmlData: string) => void; + setGptAssistantModal: React.Dispatch>; + handleGptAssistantClose: () => void; + onClose: () => void; +}; + +// services +const aiService = new AIService(); + +export const IssueDescriptionEditor: React.FC = observer((props) => { + const { + control, + issueName, + descriptionHtmlData, + editorRef, + submitBtnRef, + gptAssistantModal, + workspaceSlug, + projectId, + handleFormChange, + handleDescriptionHTMLDataChange, + setGptAssistantModal, + handleGptAssistantClose, + onClose, + } = props; + // states + const [iAmFeelingLucky, setIAmFeelingLucky] = useState(false); + // store hooks + const { getWorkspaceBySlug } = useWorkspace(); + const workspaceId = getWorkspaceBySlug(workspaceSlug?.toString())?.id as string; + const { config } = useInstance(); + + useEffect(() => { + if (descriptionHtmlData) handleDescriptionHTMLDataChange(descriptionHtmlData); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [descriptionHtmlData]); + + const handleKeyDown = (event: KeyboardEvent) => { + if (editorRef.current?.isEditorReadyToDiscard()) { + onClose(); + } else { + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: "Editor is still processing changes. Please wait before proceeding.", + }); + event.preventDefault(); // Prevent default action if editor is not ready to discard + } + }; + + useKeypress("Escape", handleKeyDown); + + // handlers + const handleAiAssistance = async (response: string) => { + if (!workspaceSlug || !projectId) return; + + editorRef.current?.setEditorValueAtCursorPosition(response); + }; + + const handleAutoGenerateDescription = async () => { + if (!workspaceSlug || !projectId) return; + + setIAmFeelingLucky(true); + + aiService + .createGptTask(workspaceSlug.toString(), { + prompt: issueName, + task: "Generate a proper description for this issue.", + }) + .then((res) => { + if (res.response === "") + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: + "Issue title isn't informative enough to generate the description. Please try with a different title.", + }); + else handleAiAssistance(res.response_html); + }) + .catch((err) => { + const error = err?.data?.error; + + if (err.status === 429) + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: error || "You have reached the maximum number of requests of 50 requests per month per user.", + }); + else + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: error || "Some error occurred. Please try again.", + }); + }) + .finally(() => setIAmFeelingLucky(false)); + }; + + return ( +
+ {descriptionHtmlData === undefined || !projectId ? ( + + +
+ + +
+
+ + +
+ +
+ +
+
+ + +
+
+ ) : ( + <> + ( + { + onChange(description_html); + handleFormChange(); + }} + onEnterKeyPress={() => submitBtnRef?.current?.click()} + ref={editorRef} + tabIndex={getTabIndex("description_html")} + placeholder={getDescriptionPlaceholder} + containerClassName="pt-3 min-h-[120px]" + /> + )} + /> +
+ {issueName && issueName.trim() !== "" && config?.has_openai_configured && ( + + )} + {config?.has_openai_configured && projectId && ( + { + setGptAssistantModal((prevData) => !prevData); + // this is done so that the title do not reset after gpt popover closed + handleGptAssistantClose(); + }} + onResponse={(response) => { + handleAiAssistance(response); + }} + placement="top-end" + button={ + + } + /> + )} +
+ + )} +
+ ); +}); diff --git a/web/core/components/issues/issue-modal/components/index.ts b/web/core/components/issues/issue-modal/components/index.ts new file mode 100644 index 00000000000..3e0c1d4d9ac --- /dev/null +++ b/web/core/components/issues/issue-modal/components/index.ts @@ -0,0 +1,5 @@ +export * from "./project-select"; +export * from "./parent-tag"; +export * from "./title-input"; +export * from "./description-editor"; +export * from "./default-properties"; diff --git a/web/core/components/issues/issue-modal/components/parent-tag.tsx b/web/core/components/issues/issue-modal/components/parent-tag.tsx new file mode 100644 index 00000000000..74772b08c71 --- /dev/null +++ b/web/core/components/issues/issue-modal/components/parent-tag.tsx @@ -0,0 +1,66 @@ +"use client"; + +import React from "react"; +import { observer } from "mobx-react"; +import { Control, Controller } from "react-hook-form"; +import { X } from "lucide-react"; +// types +import { ISearchIssueResponse, TIssue } from "@plane/types"; +// helpers +import { getTabIndex } from "@/helpers/issue-modal.helper"; +// plane web components +import { IssueIdentifier } from "@/plane-web/components/issues"; + +type TIssueParentTagProps = { + control: Control; + selectedParentIssue: ISearchIssueResponse; + handleFormChange: () => void; + setSelectedParentIssue: (issue: ISearchIssueResponse | null) => void; +}; + +export const IssueParentTag: React.FC = observer((props) => { + const { control, selectedParentIssue, handleFormChange, setSelectedParentIssue } = props; + + return ( + ( +
+
+ + + {selectedParentIssue?.project_id && ( + + )} + + {selectedParentIssue.name.substring(0, 50)} + +
+
+ )} + /> + ); +}); diff --git a/web/core/components/issues/issue-modal/components/project-select.tsx b/web/core/components/issues/issue-modal/components/project-select.tsx new file mode 100644 index 00000000000..ada1e012b89 --- /dev/null +++ b/web/core/components/issues/issue-modal/components/project-select.tsx @@ -0,0 +1,55 @@ +"use client"; + +import React from "react"; +import { observer } from "mobx-react"; +import { Control, Controller } from "react-hook-form"; +// types +import { TIssue } from "@plane/types"; +// components +import { ProjectDropdown } from "@/components/dropdowns"; +// helpers +import { getTabIndex } from "@/helpers/issue-modal.helper"; +import { shouldRenderProject } from "@/helpers/project.helper"; +// store hooks +import { useUser } from "@/hooks/store"; + +type TIssueProjectSelectProps = { + control: Control; + disabled?: boolean; + handleFormChange: () => void; +}; + +export const IssueProjectSelect: React.FC = observer((props) => { + const { control, disabled = false, handleFormChange } = props; + // store hooks + const { projectsWithCreatePermissions } = useUser(); + + return ( + + projectsWithCreatePermissions && projectsWithCreatePermissions[value!] ? ( +
+ { + onChange(projectId); + handleFormChange(); + }} + buttonVariant="border-with-text" + renderCondition={(project) => shouldRenderProject(project)} + tabIndex={getTabIndex("project_id")} + disabled={disabled} + /> +
+ ) : ( + <> + ) + } + /> + ); +}); diff --git a/web/core/components/issues/issue-modal/components/title-input.tsx b/web/core/components/issues/issue-modal/components/title-input.tsx new file mode 100644 index 00000000000..99f6cbf4aab --- /dev/null +++ b/web/core/components/issues/issue-modal/components/title-input.tsx @@ -0,0 +1,57 @@ +"use client"; + +import React from "react"; +import { observer } from "mobx-react"; +import { Control, Controller, FieldErrors } from "react-hook-form"; +// types +import { TIssue } from "@plane/types"; +// ui +import { Input } from "@plane/ui"; +// helpers +import { getTabIndex } from "@/helpers/issue-modal.helper"; + +type TIssueTitleInputProps = { + control: Control; + issueTitleRef: React.MutableRefObject; + errors: FieldErrors; + handleFormChange: () => void; +}; + +export const IssueTitleInput: React.FC = observer((props) => { + const { control, issueTitleRef, errors, handleFormChange } = props; + + return ( + <> + ( + { + onChange(e.target.value); + handleFormChange(); + }} + ref={issueTitleRef || ref} + hasError={Boolean(errors.name)} + placeholder="Title" + className="w-full text-base" + tabIndex={getTabIndex("name")} + autoFocus + /> + )} + /> + {errors?.name?.message} + + ); +}); diff --git a/web/core/components/issues/issue-modal/context/index.ts b/web/core/components/issues/issue-modal/context/index.ts new file mode 100644 index 00000000000..61ad8c43aa5 --- /dev/null +++ b/web/core/components/issues/issue-modal/context/index.ts @@ -0,0 +1 @@ +export * from "./issue-modal"; diff --git a/web/core/components/issues/issue-modal/context/issue-modal.tsx b/web/core/components/issues/issue-modal/context/issue-modal.tsx new file mode 100644 index 00000000000..845aec5525f --- /dev/null +++ b/web/core/components/issues/issue-modal/context/issue-modal.tsx @@ -0,0 +1,37 @@ +import React, { createContext } from "react"; +import { UseFormWatch } from "react-hook-form"; +// types +import { TIssue } from "@plane/types"; +// plane web types +import { TIssuePropertyValueErrors, TIssuePropertyValues } from "@/plane-web/types"; + +export type TPropertyValuesValidationProps = { + projectId: string | null; + workspaceSlug: string; + watch: UseFormWatch; +}; + +export type TActiveAdditionalPropertiesProps = { + projectId: string | null; + workspaceSlug: string; + watch: UseFormWatch; +}; + +export type TCreateUpdatePropertyValuesProps = { + issueId: string; + projectId: string; + workspaceSlug: string; +}; + +export type TIssueModalContext = { + issuePropertyValues: TIssuePropertyValues; + setIssuePropertyValues: React.Dispatch>; + issuePropertyValueErrors: TIssuePropertyValueErrors; + setIssuePropertyValueErrors: React.Dispatch>; + getIssueTypeIdOnProjectChange: (projectId: string) => string | null; + getActiveAdditionalPropertiesLength: (props: TActiveAdditionalPropertiesProps) => number; + handlePropertyValuesValidation: (props: TPropertyValuesValidationProps) => boolean; + handleCreateUpdatePropertyValues: (props: TCreateUpdatePropertyValuesProps) => Promise; +}; + +export const IssueModalContext = createContext(undefined); diff --git a/web/ce/components/issues/issue-modal/draft-issue-layout.tsx b/web/core/components/issues/issue-modal/draft-issue-layout.tsx similarity index 88% rename from web/ce/components/issues/issue-modal/draft-issue-layout.tsx rename to web/core/components/issues/issue-modal/draft-issue-layout.tsx index d198756c378..da8622dc8d2 100644 --- a/web/ce/components/issues/issue-modal/draft-issue-layout.tsx +++ b/web/core/components/issues/issue-modal/draft-issue-layout.tsx @@ -4,15 +4,21 @@ import React, { useState } from "react"; import isEmpty from "lodash/isEmpty"; import { observer } from "mobx-react"; import { useParams, usePathname } from "next/navigation"; +// types import type { TIssue } from "@plane/types"; -// hooks +// ui import { TOAST_TYPE, setToast } from "@plane/ui"; +// components import { ConfirmIssueDiscard } from "@/components/issues"; +// helpers import { isEmptyHtmlString } from "@/helpers/string.helper"; +// hooks +import { useIssueModal } from "@/hooks/context/use-issue-modal"; import { useEventTracker } from "@/hooks/store"; -import { IssueFormRoot } from "@/plane-web/components/issues/issue-modal/form"; // services import { IssueDraftService } from "@/services/issue"; +// local components +import { IssueFormRoot } from "./form"; export interface DraftIssueProps { changesMade: Partial | null; @@ -22,7 +28,7 @@ export interface DraftIssueProps { onCreateMoreToggleChange: (value: boolean) => void; onChange: (formData: Partial | null) => void; onClose: (saveDraftIssueInLocalStorage?: boolean) => void; - onSubmit: (formData: Partial) => Promise; + onSubmit: (formData: Partial, is_draft_issue?: boolean) => Promise; projectId: string; isDraft: boolean; } @@ -50,6 +56,7 @@ export const DraftIssueLayout: React.FC = observer((props) => { const pathname = usePathname(); // store hooks const { captureIssueEvent } = useEventTracker(); + const { handleCreateUpdatePropertyValues } = useIssueModal(); const handleClose = () => { if (data?.id) { @@ -90,7 +97,7 @@ export const DraftIssueLayout: React.FC = observer((props) => { name: changesMade?.name && changesMade?.name?.trim() !== "" ? changesMade.name?.trim() : "Untitled", }; - await issueDraftService + const response = await issueDraftService .createDraftIssue(workspaceSlug.toString(), projectId.toString(), payload) .then((res) => { setToast({ @@ -106,6 +113,7 @@ export const DraftIssueLayout: React.FC = observer((props) => { onChange(null); setIssueDiscardModal(false); onClose(false); + return res; }) .catch(() => { setToast({ @@ -119,6 +127,14 @@ export const DraftIssueLayout: React.FC = observer((props) => { path: pathname, }); }); + + if (response && handleCreateUpdatePropertyValues) { + handleCreateUpdatePropertyValues({ + issueId: response.id, + projectId, + workspaceSlug: workspaceSlug?.toString(), + }); + } }; return ( diff --git a/web/core/components/issues/issue-modal/form.tsx b/web/core/components/issues/issue-modal/form.tsx new file mode 100644 index 00000000000..5a496087de3 --- /dev/null +++ b/web/core/components/issues/issue-modal/form.tsx @@ -0,0 +1,433 @@ +"use client"; + +import React, { FC, useState, useRef, useEffect } from "react"; +import { observer } from "mobx-react"; +import { useParams } from "next/navigation"; +import { useForm } from "react-hook-form"; +// editor +import { EditorRefApi } from "@plane/editor"; +// types +import type { TIssue, ISearchIssueResponse } from "@plane/types"; +// hooks +import { Button, ToggleSwitch, TOAST_TYPE, setToast } from "@plane/ui"; +// components +import { + IssueDefaultProperties, + IssueDescriptionEditor, + IssueParentTag, + IssueProjectSelect, + IssueTitleInput, +} from "@/components/issues/issue-modal/components"; +import { CreateLabelModal } from "@/components/labels"; +// helpers +import { cn } from "@/helpers/common.helper"; +import { getTabIndex } from "@/helpers/issue-modal.helper"; +import { getChangedIssuefields } from "@/helpers/issue.helper"; +// hooks +import { useIssueModal } from "@/hooks/context/use-issue-modal"; +import { useIssueDetail, useProject, useProjectState } from "@/hooks/store"; +import { useProjectIssueProperties } from "@/hooks/use-project-issue-properties"; +// plane web components +import { IssueAdditionalProperties, IssueTypeSelect } from "@/plane-web/components/issues/issue-modal"; + +const defaultValues: Partial = { + project_id: "", + type_id: null, + name: "", + description_html: "", + estimate_point: null, + state_id: "", + parent_id: null, + priority: "none", + assignee_ids: [], + label_ids: [], + cycle_id: null, + module_ids: null, + start_date: null, + target_date: null, +}; + +export interface IssueFormProps { + data?: Partial; + issueTitleRef: React.MutableRefObject; + isCreateMoreToggleEnabled: boolean; + onCreateMoreToggleChange: (value: boolean) => void; + onChange?: (formData: Partial | null) => void; + onClose: () => void; + onSubmit: (values: Partial, is_draft_issue?: boolean) => Promise; + projectId: string; + isDraft: boolean; +} + +export const IssueFormRoot: FC = observer((props) => { + const { + data, + issueTitleRef, + onChange, + onClose, + onSubmit, + projectId: defaultProjectId, + isCreateMoreToggleEnabled, + onCreateMoreToggleChange, + isDraft, + } = props; + // states + const [labelModal, setLabelModal] = useState(false); + const [selectedParentIssue, setSelectedParentIssue] = useState(null); + const [gptAssistantModal, setGptAssistantModal] = useState(false); + // refs + const editorRef = useRef(null); + const submitBtnRef = useRef(null); + // router + const { workspaceSlug, projectId: routeProjectId } = useParams(); + // store hooks + const { getProjectById } = useProject(); + const { getIssueTypeIdOnProjectChange, getActiveAdditionalPropertiesLength, handlePropertyValuesValidation } = + useIssueModal(); + + const { + issue: { getIssueById }, + } = useIssueDetail(); + const { fetchCycles } = useProjectIssueProperties(); + const { getStateById } = useProjectState(); + // form info + const { + formState: { errors, isDirty, isSubmitting, dirtyFields }, + handleSubmit, + reset, + watch, + control, + getValues, + setValue, + } = useForm({ + defaultValues: { ...defaultValues, project_id: defaultProjectId, ...data }, + reValidateMode: "onChange", + }); + + const projectId = watch("project_id"); + const activeAdditionalPropertiesLength = getActiveAdditionalPropertiesLength({ + projectId: projectId, + workspaceSlug: workspaceSlug?.toString(), + watch: watch, + }); + + //reset few fields on projectId change + useEffect(() => { + if (isDirty) { + const formData = getValues(); + + reset({ + ...defaultValues, + project_id: projectId, + name: formData.name, + description_html: formData.description_html, + priority: formData.priority, + start_date: formData.start_date, + target_date: formData.target_date, + parent_id: formData.parent_id, + }); + } + if (projectId && routeProjectId !== projectId) fetchCycles(workspaceSlug?.toString(), projectId); + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [projectId]); + + // Update the issue type id when the project id changes + useEffect(() => { + const issueTypeId = watch("type_id"); + + // if data is present, set active type id to the type id of the issue + if (data && data.type_id) { + setValue("type_id", data.type_id, { shouldValidate: true }); + return; + } + + // if issue type id is present or project not available, return + if (issueTypeId || !projectId) return; + + // get issue type id on project change + const issueTypeIdOnProjectChange = getIssueTypeIdOnProjectChange(projectId); + if (issueTypeIdOnProjectChange) setValue("type_id", issueTypeIdOnProjectChange, { shouldValidate: true }); + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [data, projectId]); + + const handleFormSubmit = async (formData: Partial, is_draft_issue = false) => { + // Check if the editor is ready to discard + if (!editorRef.current?.isEditorReadyToDiscard()) { + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: "Editor is not ready to discard changes.", + }); + return; + } + + // check for required properties validation + if ( + !handlePropertyValuesValidation({ + projectId: projectId, + workspaceSlug: workspaceSlug?.toString(), + watch: watch, + }) + ) + return; + + const submitData = !data?.id + ? formData + : { + ...getChangedIssuefields(formData, dirtyFields as { [key: string]: boolean | undefined }), + project_id: getValues<"project_id">("project_id"), + id: data.id, + description_html: formData.description_html ?? "

", + }; + + // this condition helps to move the issues from draft to project issues + if (formData.hasOwnProperty("is_draft")) submitData.is_draft = formData.is_draft; + + await onSubmit(submitData, is_draft_issue); + + setGptAssistantModal(false); + + reset({ + ...defaultValues, + ...(isCreateMoreToggleEnabled ? { ...data } : {}), + project_id: getValues<"project_id">("project_id"), + type_id: getValues<"type_id">("type_id"), + description_html: data?.description_html ?? "

", + }); + editorRef?.current?.clearEditor(); + }; + + const condition = + (watch("name") && watch("name") !== "") || (watch("description_html") && watch("description_html") !== "

"); + + const handleFormChange = () => { + if (!onChange) return; + + if (isDirty && condition) onChange(watch()); + else onChange(null); + }; + + // executing this useEffect when the parent_id coming from the component prop + useEffect(() => { + const parentId = watch("parent_id") || undefined; + if (!parentId) return; + if (parentId === selectedParentIssue?.id || selectedParentIssue) return; + + const issue = getIssueById(parentId); + if (!issue) return; + + const projectDetails = getProjectById(issue.project_id); + if (!projectDetails) return; + + const stateDetails = getStateById(issue.state_id); + + setSelectedParentIssue({ + id: issue.id, + name: issue.name, + project_id: issue.project_id, + project__identifier: projectDetails.identifier, + project__name: projectDetails.name, + sequence_id: issue.sequence_id, + type_id: issue.type_id, + state__color: stateDetails?.color, + } as ISearchIssueResponse); + }, [watch, getIssueById, getProjectById, selectedParentIssue, getStateById]); + + // executing this useEffect when isDirty changes + useEffect(() => { + if (!onChange) return; + + if (isDirty && condition) onChange(watch()); + else onChange(null); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isDirty]); + + return ( + <> + {projectId && ( + setLabelModal(false)} + projectId={projectId} + onSuccess={(response) => { + setValue<"label_ids">("label_ids", [...watch("label_ids"), response.id]); + handleFormChange(); + }} + /> + )} +
handleFormSubmit(data))}> +
+

{data?.id ? "Update" : "Create new"} issue

+ {/* Disable project selection if editing an issue */} +
+ + {projectId && ( + + )} +
+ {watch("parent_id") && selectedParentIssue && ( +
+ +
+ )} +
+ +
+
+
4 && + "max-h-[45vh] overflow-hidden overflow-y-auto vertical-scrollbar scrollbar-sm" + )} + > +
+ + setValue<"description_html">("description_html", description_html) + } + setGptAssistantModal={setGptAssistantModal} + handleGptAssistantClose={() => reset(getValues())} + onClose={onClose} + /> +
+
+ {projectId && ( + + )} +
+
+
+
+ +
+
+ {!data?.id && ( +
onCreateMoreToggleChange(!isCreateMoreToggleEnabled)} + onKeyDown={(e) => { + if (e.key === "Enter") onCreateMoreToggleChange(!isCreateMoreToggleEnabled); + }} + tabIndex={getTabIndex("create_more")} + role="button" + > + {}} size="sm" /> + Create more +
+ )} +
+ + {isDraft && ( + <> + {data?.id ? ( + + ) : ( + + )} + + )} + +
+
+
+
+ + ); +}); diff --git a/web/core/components/issues/issue-modal/index.ts b/web/core/components/issues/issue-modal/index.ts index 031608e25ff..48992294917 100644 --- a/web/core/components/issues/issue-modal/index.ts +++ b/web/core/components/issues/issue-modal/index.ts @@ -1 +1,5 @@ +export * from "./form"; +export * from "./base"; +export * from "./draft-issue-layout"; export * from "./modal"; +export * from "./context"; diff --git a/web/core/components/issues/issue-modal/modal.tsx b/web/core/components/issues/issue-modal/modal.tsx index 7a984104eb2..0f2d3ce0008 100644 --- a/web/core/components/issues/issue-modal/modal.tsx +++ b/web/core/components/issues/issue-modal/modal.tsx @@ -1,3 +1,28 @@ "use client"; -export * from "@/plane-web/components/issues/issue-modal/modal"; +import React from "react"; +import { observer } from "mobx-react"; +// types +import type { TIssue } from "@plane/types"; +// components +import { CreateUpdateIssueModalBase } from "@/components/issues"; +// constants +import { EIssuesStoreType } from "@/constants/issue"; +// plane web providers +import { IssueModalProvider } from "@/plane-web/components/issues"; + +export interface IssuesModalProps { + data?: Partial; + isOpen: boolean; + onClose: () => void; + onSubmit?: (res: TIssue) => Promise; + withDraftIssueWrapper?: boolean; + storeType?: EIssuesStoreType; + isDraft?: boolean; +} + +export const CreateUpdateIssueModal: React.FC = observer((props) => ( + + + +)); diff --git a/web/core/components/pages/editor/editor-body.tsx b/web/core/components/pages/editor/editor-body.tsx index b9b0f232077..5cd7e503c44 100644 --- a/web/core/components/pages/editor/editor-body.tsx +++ b/web/core/components/pages/editor/editor-body.tsx @@ -1,4 +1,4 @@ -import { useEffect } from "react"; +import { useCallback, useEffect } from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; // document-editor @@ -8,6 +8,7 @@ import { EditorReadOnlyRefApi, EditorRefApi, IMarking, + TAIMenuProps, TDisplayConfig, } from "@plane/editor"; // types @@ -95,6 +96,11 @@ export const PageEditorBody: React.FC = observer((props) => { fontStyle, }; + const getAIMenu = useCallback( + ({ isOpen, onClose }: TAIMenuProps) => , + [editorRef] + ); + useEffect(() => { updateMarkings(pageDescription ?? "

"); }, [pageDescription, updateMarkings]); @@ -158,7 +164,7 @@ export const PageEditorBody: React.FC = observer((props) => { }} disabledExtensions={documentEditor} aiHandler={{ - menu: ({ onClose }) => , + menu: getAIMenu, }} /> ) : ( diff --git a/web/core/components/pages/editor/header/options-dropdown.tsx b/web/core/components/pages/editor/header/options-dropdown.tsx index 016ca8eb570..14b352456a0 100644 --- a/web/core/components/pages/editor/header/options-dropdown.tsx +++ b/web/core/components/pages/editor/header/options-dropdown.tsx @@ -1,8 +1,8 @@ "use client"; import { observer } from "mobx-react"; -import { useParams } from "next/navigation"; -import { ArchiveRestoreIcon, Clipboard, Copy, Link, Lock, LockOpen } from "lucide-react"; +import { useParams, useRouter } from "next/navigation"; +import { ArchiveRestoreIcon, Clipboard, Copy, History, Link, Lock, LockOpen } from "lucide-react"; // document editor import { EditorReadOnlyRefApi, EditorRefApi } from "@plane/editor"; // ui @@ -11,6 +11,7 @@ import { ArchiveIcon, CustomMenu, TOAST_TYPE, ToggleSwitch, setToast } from "@pl import { copyTextToClipboard, copyUrlToClipboard } from "@/helpers/string.helper"; // hooks import { usePageFilters } from "@/hooks/use-page-filters"; +import { useQueryParams } from "@/hooks/use-query-params"; // store import { IPage } from "@/store/pages/page"; @@ -23,6 +24,8 @@ type Props = { export const PageOptionsDropdown: React.FC = observer((props) => { const { editorRef, handleDuplicatePage, page, handleSaveDescription } = props; + // router + const router = useRouter(); // store values const { archived_at, @@ -40,6 +43,8 @@ export const PageOptionsDropdown: React.FC = observer((props) => { const { workspaceSlug, projectId } = useParams(); // page filters const { isFullWidth, handleFullWidth } = usePageFilters(); + // update query params + const { updateQueryParams } = useQueryParams(); const handleArchivePage = async () => await archive().catch(() => @@ -145,6 +150,19 @@ export const PageOptionsDropdown: React.FC = observer((props) => { icon: archived_at ? ArchiveRestoreIcon : ArchiveIcon, shouldRender: canCurrentUserArchivePage, }, + { + key: "version-history", + action: () => { + // add query param, version=current to the route + const updatedRoute = updateQueryParams({ + paramsToAdd: { version: "current" }, + }); + router.push(updatedRoute); + }, + label: "Version history", + icon: History, + shouldRender: true, + }, ]; return ( diff --git a/web/core/components/pages/editor/page-root.tsx b/web/core/components/pages/editor/page-root.tsx index 7ca023261af..bbb5acf819c 100644 --- a/web/core/components/pages/editor/page-root.tsx +++ b/web/core/components/pages/editor/page-root.tsx @@ -1,12 +1,23 @@ -import { useRef, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { observer } from "mobx-react"; +import { useSearchParams } from "next/navigation"; +// plane editor import { EditorRefApi, useEditorMarkings } from "@plane/editor"; +// plane types import { TPage } from "@plane/types"; +// plane ui import { setToast, TOAST_TYPE } from "@plane/ui"; -import { PageEditorHeaderRoot, PageEditorBody } from "@/components/pages"; +// components +import { PageEditorHeaderRoot, PageEditorBody, PageVersionsOverlay } from "@/components/pages"; +// hooks import { useProjectPages } from "@/hooks/store"; import { useAppRouter } from "@/hooks/use-app-router"; import { usePageDescription } from "@/hooks/use-page-description"; +import { useQueryParams } from "@/hooks/use-query-params"; +// services +import { ProjectPageVersionService } from "@/services/page"; +const projectPageVersionService = new ProjectPageVersionService(); +// store import { IPage } from "@/store/pages/page"; type TPageRootProps = { @@ -16,34 +27,40 @@ type TPageRootProps = { }; export const PageRoot = observer((props: TPageRootProps) => { - // router - const router = useAppRouter(); const { projectId, workspaceSlug, page } = props; - const { createPage } = useProjectPages(); - const { access, description_html, name } = page; - // states const [editorReady, setEditorReady] = useState(false); const [readOnlyEditorReady, setReadOnlyEditorReady] = useState(false); - + const [sidePeekVisible, setSidePeekVisible] = useState(window.innerWidth >= 768); + const [isVersionsOverlayOpen, setIsVersionsOverlayOpen] = useState(false); // refs const editorRef = useRef(null); const readOnlyEditorRef = useRef(null); - + // router + const router = useAppRouter(); + // search params + const searchParams = useSearchParams(); + // store hooks + const { createPage } = useProjectPages(); + // derived values + const { access, description_html, name } = page; // editor markings hook const { markings, updateMarkings } = useEditorMarkings(); - - const [sidePeekVisible, setSidePeekVisible] = useState(window.innerWidth >= 768 ? true : false); - // project-description - const { handleDescriptionChange, isDescriptionReady, pageDescriptionYJS, handleSaveDescription } = usePageDescription( - { - editorRef, - page, - projectId, - workspaceSlug, - } - ); + const { + handleDescriptionChange, + isDescriptionReady, + pageDescriptionYJS, + handleSaveDescription, + manuallyUpdateDescription, + } = usePageDescription({ + editorRef, + page, + projectId, + workspaceSlug, + }); + // update query params + const { updateQueryParams } = useQueryParams(); const handleCreatePage = async (payload: Partial) => await createPage(payload); @@ -65,8 +82,48 @@ export const PageRoot = observer((props: TPageRootProps) => { ); }; + const version = searchParams.get("version"); + useEffect(() => { + if (!version) { + setIsVersionsOverlayOpen(false); + return; + } + setIsVersionsOverlayOpen(true); + }, [version]); + + const handleCloseVersionsOverlay = () => { + const updatedRoute = updateQueryParams({ + paramsToRemove: ["version"], + }); + router.push(updatedRoute); + }; + return ( <> + { + if (!workspaceSlug || !projectId) return; + return await projectPageVersionService.fetchAllVersions( + workspaceSlug.toString(), + projectId.toString(), + pageId + ); + }} + fetchVersionDetails={async (pageId, versionId) => { + if (!workspaceSlug || !projectId) return; + return await projectPageVersionService.fetchVersionById( + workspaceSlug.toString(), + projectId.toString(), + pageId, + versionId + ); + }} + handleRestore={manuallyUpdateDescription} + isOpen={isVersionsOverlayOpen} + onClose={handleCloseVersionsOverlay} + pageId={page.id ?? ""} + /> = observer((props) => { + const { activeVersion, isCurrentVersionActive, versionDetails } = props; + // params + const { workspaceSlug, projectId, pageId } = useParams(); + // store hooks + const { data: currentUser } = useUser(); + const { + getUserDetails, + project: { getProjectMemberIds }, + } = useMember(); + const { description_html } = usePage(pageId.toString() ?? ""); + // derived values + const projectMemberIds = projectId ? getProjectMemberIds(projectId.toString()) : []; + const projectMemberDetails = projectMemberIds?.map((id) => getUserDetails(id) as IUserLite); + // issue-embed + const { issueEmbedProps } = useIssueEmbed(workspaceSlug?.toString() ?? "", projectId?.toString() ?? ""); + // use-mention + const { mentionHighlights } = useMention({ + workspaceSlug: workspaceSlug?.toString() ?? "", + projectId: projectId?.toString() ?? "", + members: projectMemberDetails, + user: currentUser ?? undefined, + }); + // page filters + const { fontSize, fontStyle } = usePageFilters(); + + const displayConfig: TDisplayConfig = { + fontSize, + fontStyle, + }; + + if (!isCurrentVersionActive && !versionDetails) + return ( +
+ + +
+
+ +
+ +
+ + +
+
+ +
+ + +
+ + +
+
+ +
+ +
+ +
+ +
+
+ +
+ +
+
+
+
+ ); + + return ( +

"} + containerClassName="p-0 pb-64 border-none" + displayConfig={displayConfig} + editorClassName="pl-10" + mentionHandler={{ + highlights: mentionHighlights, + }} + embedHandler={{ + issue: { + widgetCallback: issueEmbedProps.widgetCallback, + }, + }} + /> + ); +}); diff --git a/web/core/components/pages/version/index.ts b/web/core/components/pages/version/index.ts new file mode 100644 index 00000000000..8e04e4de9e0 --- /dev/null +++ b/web/core/components/pages/version/index.ts @@ -0,0 +1,6 @@ +export * from "./editor"; +export * from "./main-content"; +export * from "./root"; +export * from "./sidebar-list-item"; +export * from "./sidebar-list"; +export * from "./sidebar-root"; diff --git a/web/core/components/pages/version/main-content.tsx b/web/core/components/pages/version/main-content.tsx new file mode 100644 index 00000000000..5d9d7414af1 --- /dev/null +++ b/web/core/components/pages/version/main-content.tsx @@ -0,0 +1,114 @@ +import { useState } from "react"; +import { observer } from "mobx-react"; +import useSWR from "swr"; +import { TriangleAlert } from "lucide-react"; +// plane types +import { TPageVersion } from "@plane/types"; +// plane ui +import { Button, setToast, TOAST_TYPE } from "@plane/ui"; +// components +import { PagesVersionEditor } from "@/components/pages"; +// helpers +import { renderFormattedDate, renderFormattedTime } from "@/helpers/date-time.helper"; + +type Props = { + activeVersion: string | null; + fetchVersionDetails: (pageId: string, versionId: string) => Promise; + handleClose: () => void; + handleRestore: (descriptionHTML: string) => Promise; + pageId: string; +}; + +export const PageVersionsMainContent: React.FC = observer((props) => { + const { activeVersion, fetchVersionDetails, handleClose, handleRestore, pageId } = props; + // states + const [isRestoring, setIsRestoring] = useState(false); + const [isRetrying, setIsRetrying] = useState(false); + + const { + data: versionDetails, + error: versionDetailsError, + mutate: mutateVersionDetails, + } = useSWR( + pageId && activeVersion && activeVersion !== "current" ? `PAGE_VERSION_${activeVersion}` : null, + pageId && activeVersion && activeVersion !== "current" ? () => fetchVersionDetails(pageId, activeVersion) : null + ); + + const isCurrentVersionActive = activeVersion === "current"; + + const handleRestoreVersion = async () => { + setIsRestoring(true); + await handleRestore(versionDetails?.description_html ?? "

") + .then(() => { + setToast({ + type: TOAST_TYPE.SUCCESS, + title: "Page version restored.", + }); + handleClose(); + }) + .catch(() => + setToast({ + type: TOAST_TYPE.ERROR, + title: "Failed to restore page version.", + }) + ) + .finally(() => setIsRestoring(false)); + }; + + const handleRetry = async () => { + setIsRetrying(true); + await mutateVersionDetails(); + setIsRetrying(false); + }; + + return ( +
+ {versionDetailsError ? ( +
+
+ + + +
+
Something went wrong!
+

The version could not be loaded, please try again.

+
+ +
+
+ ) : ( + <> +
+
+ {isCurrentVersionActive + ? "Current version" + : versionDetails + ? `${renderFormattedDate(versionDetails.last_saved_at)} ${renderFormattedTime(versionDetails.last_saved_at)}` + : "Loading version details"} +
+ {!isCurrentVersionActive && ( + + )} +
+
+ +
+ + )} +
+ ); +}); diff --git a/web/core/components/pages/version/root.tsx b/web/core/components/pages/version/root.tsx new file mode 100644 index 00000000000..443053d56db --- /dev/null +++ b/web/core/components/pages/version/root.tsx @@ -0,0 +1,50 @@ +// plane types +import { TPageVersion } from "@plane/types"; +// components +import { PageVersionsMainContent, PageVersionsSidebarRoot } from "@/components/pages"; +// helpers +import { cn } from "@/helpers/common.helper"; + +type Props = { + activeVersion: string | null; + fetchAllVersions: (pageId: string) => Promise; + fetchVersionDetails: (pageId: string, versionId: string) => Promise; + handleRestore: (descriptionHTML: string) => Promise; + isOpen: boolean; + onClose: () => void; + pageId: string; +}; + +export const PageVersionsOverlay: React.FC = (props) => { + const { activeVersion, fetchAllVersions, fetchVersionDetails, handleRestore, isOpen, onClose, pageId } = props; + + const handleClose = () => { + onClose(); + }; + + return ( +
+ + +
+ ); +}; diff --git a/web/core/components/pages/version/sidebar-list-item.tsx b/web/core/components/pages/version/sidebar-list-item.tsx new file mode 100644 index 00000000000..9c1c13e0de8 --- /dev/null +++ b/web/core/components/pages/version/sidebar-list-item.tsx @@ -0,0 +1,48 @@ +import { observer } from "mobx-react"; +import Link from "next/link"; +// plane types +import { TPageVersion } from "@plane/types"; +// plane ui +import { Avatar } from "@plane/ui"; +// helpers +import { cn } from "@/helpers/common.helper"; +import { renderFormattedDate, renderFormattedTime } from "@/helpers/date-time.helper"; +// hooks +import { useMember } from "@/hooks/store"; + +type Props = { + href: string; + isActive: boolean; + version: TPageVersion; +}; + +export const PlaneVersionsSidebarListItem: React.FC = observer((props) => { + const { href, isActive, version } = props; + // store hooks + const { getUserDetails } = useMember(); + // derived values + const ownerDetails = getUserDetails(version.owned_by); + + return ( + +

+ {renderFormattedDate(version.last_saved_at)} {renderFormattedTime(version.last_saved_at)} +

+

+ + {ownerDetails?.display_name} +

+ + ); +}); diff --git a/web/core/components/pages/version/sidebar-list.tsx b/web/core/components/pages/version/sidebar-list.tsx new file mode 100644 index 00000000000..cf276742b02 --- /dev/null +++ b/web/core/components/pages/version/sidebar-list.tsx @@ -0,0 +1,99 @@ +import { useState } from "react"; +import Link from "next/link"; +import useSWR from "swr"; +import { TriangleAlert } from "lucide-react"; +// plane types +import { TPageVersion } from "@plane/types"; +// plane ui +import { Button, Loader } from "@plane/ui"; +// components +import { PlaneVersionsSidebarListItem } from "@/components/pages"; +// helpers +import { cn } from "@/helpers/common.helper"; +// hooks +import { useQueryParams } from "@/hooks/use-query-params"; + +type Props = { + activeVersion: string | null; + fetchAllVersions: (pageId: string) => Promise; + isOpen: boolean; + pageId: string; +}; + +export const PageVersionsSidebarList: React.FC = (props) => { + const { activeVersion, fetchAllVersions, isOpen, pageId } = props; + // states + const [isRetrying, setIsRetrying] = useState(false); + // update query params + const { updateQueryParams } = useQueryParams(); + + const { + data: versionsList, + error: versionsListError, + mutate: mutateVersionsList, + } = useSWR( + pageId && isOpen ? `PAGE_VERSIONS_LIST_${pageId}` : null, + pageId && isOpen ? () => fetchAllVersions(pageId) : null + ); + + const handleRetry = async () => { + setIsRetrying(true); + await mutateVersionsList(); + setIsRetrying(false); + }; + + const getVersionLink = (versionID: string) => + updateQueryParams({ + paramsToAdd: { version: versionID }, + }); + + return ( +
+ +

Current version

+ + {versionsListError ? ( +
+
+ + + +
+
Something went wrong!
+

+ There was a problem while loading previous +
+ versions, please try again. +

+
+ +
+
+ ) : versionsList ? ( + versionsList.map((version) => ( + + )) + ) : ( + + + + + + + + )} +
+ ); +}; diff --git a/web/core/components/pages/version/sidebar-root.tsx b/web/core/components/pages/version/sidebar-root.tsx new file mode 100644 index 00000000000..793d7fed90f --- /dev/null +++ b/web/core/components/pages/version/sidebar-root.tsx @@ -0,0 +1,38 @@ +import { X } from "lucide-react"; +// plane types +import { TPageVersion } from "@plane/types"; +// components +import { PageVersionsSidebarList } from "@/components/pages"; + +type Props = { + activeVersion: string | null; + fetchAllVersions: (pageId: string) => Promise; + handleClose: () => void; + isOpen: boolean; + pageId: string; +}; + +export const PageVersionsSidebarRoot: React.FC = (props) => { + const { activeVersion, fetchAllVersions, handleClose, isOpen, pageId } = props; + + return ( +
+
+
Version history
+ +
+ +
+ ); +}; diff --git a/web/core/components/profile/notification/email-notification-form.tsx b/web/core/components/profile/notification/email-notification-form.tsx index 4166634a9d3..990ade067af 100644 --- a/web/core/components/profile/notification/email-notification-form.tsx +++ b/web/core/components/profile/notification/email-notification-form.tsx @@ -21,11 +21,9 @@ export const EmailNotificationForm: FC = (props) => // form data const { handleSubmit, - watch, control, - setValue, reset, - formState: { isSubmitting, isDirty, dirtyFields }, + formState: { isSubmitting, dirtyFields }, } = useForm({ defaultValues: { ...data, @@ -93,9 +91,7 @@ export const EmailNotificationForm: FC = (props) => render={({ field: { value, onChange } }) => ( { - setValue("issue_completed", !value, { shouldDirty: true }); onChange(!value); }} containerClassName="mx-2" @@ -155,7 +151,7 @@ export const EmailNotificationForm: FC = (props) =>
-
diff --git a/web/core/components/workspace/sidebar/favorites/favorite-folder.tsx b/web/core/components/workspace/sidebar/favorites/favorite-folder.tsx index e84327e542b..339bd4758c4 100644 --- a/web/core/components/workspace/sidebar/favorites/favorite-folder.tsx +++ b/web/core/components/workspace/sidebar/favorites/favorite-folder.tsx @@ -5,6 +5,7 @@ import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine"; import { draggable, dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; import { attachClosestEdge, extractClosestEdge } from "@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge"; +import uniqBy from "lodash/uniqBy"; import { useParams } from "next/navigation"; import { PenSquare, Star, MoreHorizontal, ChevronRight, GripVertical } from "lucide-react"; import { Disclosure, Transition } from "@headlessui/react"; @@ -37,7 +38,7 @@ export const FavoriteFolder: React.FC = (props) => { const { sidebarCollapsed: isSidebarCollapsed } = useAppTheme(); const isMobile = useIsMobile(); - const { moveFavorite, getGroupedFavorites, favoriteMap, moveFavoriteFolder } = useFavorite(); + const { moveFavorite, getGroupedFavorites, groupedFavorites, moveFavoriteFolder } = useFavorite(); const { workspaceSlug } = useParams(); // states const [isMenuActive, setIsMenuActive] = useState(false); @@ -110,7 +111,9 @@ export const FavoriteFolder: React.FC = (props) => { const edge = extractClosestEdge(destinationData) || undefined; const payload = { id: favorite.id, - sequence: Math.round(getDestinationStateSequence(favoriteMap, destinationData.id as string, edge) || 0), + sequence: Math.round( + getDestinationStateSequence(groupedFavorites, destinationData.id as string, edge) || 0 + ), }; handleOnDropFolder(payload); @@ -146,7 +149,7 @@ export const FavoriteFolder: React.FC = (props) => { if (source.data.is_folder) return; if (sourceId === destinationId) return; if (!sourceId || !destinationId) return; - if (favoriteMap[sourceId].parent === destinationId) return; + if (groupedFavorites[sourceId].parent === destinationId) return; handleOnDrop(sourceId, destinationId); }, }) @@ -313,14 +316,14 @@ export const FavoriteFolder: React.FC = (props) => { "px-2": !isSidebarCollapsed, })} > - {favorite.children.map((child) => ( + {uniqBy(favorite.children, "id").map((child) => ( ))} diff --git a/web/core/components/workspace/sidebar/favorites/favorites-menu.tsx b/web/core/components/workspace/sidebar/favorites/favorites-menu.tsx index 3de04418e8b..06d599314c4 100644 --- a/web/core/components/workspace/sidebar/favorites/favorites-menu.tsx +++ b/web/core/components/workspace/sidebar/favorites/favorites-menu.tsx @@ -3,7 +3,7 @@ import React, { useEffect, useRef, useState } from "react"; import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine"; import { dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; -import { orderBy, uniqBy } from "lodash"; +import orderBy from "lodash/orderBy"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; import { ChevronRight, FolderPlus } from "lucide-react"; @@ -33,7 +33,7 @@ export const SidebarFavoritesMenu = observer(() => { // store hooks const { sidebarCollapsed } = useAppTheme(); - const { favoriteIds, favoriteMap, deleteFavorite, removeFromFavoriteFolder } = useFavorite(); + const { favoriteIds, groupedFavorites, deleteFavorite, removeFromFavoriteFolder } = useFavorite(); const { workspaceSlug } = useParams(); const isMobile = useIsMobile(); @@ -108,7 +108,7 @@ export const SidebarFavoritesMenu = observer(() => { setIsDragging(false); const sourceId = source?.data?.id as string | undefined; console.log({ sourceId }); - if (!sourceId || !favoriteMap[sourceId].parent) return; + if (!sourceId || !groupedFavorites[sourceId].parent) return; handleRemoveFromFavoritesFolder(sourceId); }, }) @@ -170,14 +170,14 @@ export const SidebarFavoritesMenu = observer(() => { static > {createNewFolder && } - {Object.keys(favoriteMap).length === 0 ? ( + {Object.keys(groupedFavorites).length === 0 ? ( <> {!sidebarCollapsed && ( No favorites yet )} ) : ( - uniqBy(orderBy(Object.values(favoriteMap), "sequence", "desc"), "id") + orderBy(Object.values(groupedFavorites), "sequence", "desc") .filter((fav) => !fav.parent) .map((fav, index) => ( { favorite={fav} handleRemoveFromFavorites={handleRemoveFromFavorites} handleRemoveFromFavoritesFolder={handleRemoveFromFavoritesFolder} - favoriteMap={favoriteMap} + favoriteMap={groupedFavorites} /> )} diff --git a/web/core/constants/issue-modal.ts b/web/core/constants/issue-modal.ts new file mode 100644 index 00000000000..2e16176bd32 --- /dev/null +++ b/web/core/constants/issue-modal.ts @@ -0,0 +1,22 @@ +export const ISSUE_FORM_TAB_INDICES = [ + "name", + "description_html", + "feeling_lucky", + "ai_assistant", + "state_id", + "priority", + "assignee_ids", + "label_ids", + "start_date", + "target_date", + "cycle_id", + "module_ids", + "estimate_point", + "parent_id", + "create_more", + "discard_button", + "draft_button", + "submit_button", + "project_id", + "remove_parent", +]; diff --git a/web/core/hooks/context/use-issue-modal.tsx b/web/core/hooks/context/use-issue-modal.tsx new file mode 100644 index 00000000000..97fe6d0e126 --- /dev/null +++ b/web/core/hooks/context/use-issue-modal.tsx @@ -0,0 +1,9 @@ +import { useContext } from "react"; +// context +import { IssueModalContext, TIssueModalContext } from "@/components/issues"; + +export const useIssueModal = (): TIssueModalContext => { + const context = useContext(IssueModalContext); + if (context === undefined) throw new Error("useIssueModal must be used within IssueModalProvider"); + return context; +}; diff --git a/web/core/hooks/use-multiple-select.ts b/web/core/hooks/use-multiple-select.ts index 278304c4fe2..1e997bbfe16 100644 --- a/web/core/hooks/use-multiple-select.ts +++ b/web/core/hooks/use-multiple-select.ts @@ -3,6 +3,8 @@ import { useCallback, useEffect, useMemo } from "react"; // hooks import { useMultipleSelectStore } from "@/hooks/store"; +// +import useReloadConfirmations from "./use-reload-confirmation"; export type TEntityDetails = { entityID: string; @@ -52,6 +54,15 @@ export const useMultipleSelect = (props: Props) => { getEntityDetailsFromEntityID, } = useMultipleSelectStore(); + useReloadConfirmations( + selectedEntityIds && selectedEntityIds.length > 0, + "Are you sure you want to leave? Your current bulk operation selections will be lost.", + true, + () => { + clearSelection(); + } + ); + const groups = useMemo(() => Object.keys(entities), [entities]); const entitiesList: TEntityDetails[] = useMemo( diff --git a/web/core/hooks/use-page-description.ts b/web/core/hooks/use-page-description.ts index f7b467d4d0b..4273694505d 100644 --- a/web/core/hooks/use-page-description.ts +++ b/web/core/hooks/use-page-description.ts @@ -1,20 +1,19 @@ import React, { useCallback, useEffect, useState } from "react"; import useSWR from "swr"; - +// plane editor import { EditorRefApi, proseMirrorJSONToBinaryString, applyUpdates, generateJSONfromHTMLForDocumentEditor, } from "@plane/editor"; - // hooks import { setToast, TOAST_TYPE } from "@plane/ui"; import useAutoSave from "@/hooks/use-auto-save"; import useReloadConfirmations from "@/hooks/use-reload-confirmation"; - // services import { ProjectPageService } from "@/services/page"; +// store import { IPage } from "@/store/pages/page"; const projectPageService = new ProjectPageService(); @@ -183,6 +182,19 @@ export const usePageDescription = (props: Props) => { ] ); + const manuallyUpdateDescription = async (descriptionHTML: string) => { + const { contentJSON, editorSchema } = generateJSONfromHTMLForDocumentEditor(descriptionHTML ?? "

"); + const yDocBinaryString = proseMirrorJSONToBinaryString(contentJSON, "default", editorSchema); + + try { + editorRef.current?.clearEditor(true); + await updateDescription(yDocBinaryString, descriptionHTML ?? "

"); + await mutateDescriptionYJS(); + } catch (error) { + console.log("error", error); + } + }; + useAutoSave(handleSaveDescription); return { @@ -190,5 +202,6 @@ export const usePageDescription = (props: Props) => { isDescriptionReady, pageDescriptionYJS, handleSaveDescription, + manuallyUpdateDescription, }; }; diff --git a/web/core/hooks/use-query-params.ts b/web/core/hooks/use-query-params.ts new file mode 100644 index 00000000000..8b689f0cbe0 --- /dev/null +++ b/web/core/hooks/use-query-params.ts @@ -0,0 +1,39 @@ +import { useSearchParams, usePathname } from "next/navigation"; + +type TParamsToAdd = { + [key: string]: string; +}; + +export const useQueryParams = () => { + // next navigation + const searchParams = useSearchParams(); + const pathname = usePathname(); + + const updateQueryParams = ({ + paramsToAdd = {}, + paramsToRemove = [], + }: { + paramsToAdd?: TParamsToAdd; + paramsToRemove?: string[]; + }) => { + const currentParams = new URLSearchParams(searchParams.toString()); + + // add or update query parameters + Object.keys(paramsToAdd).forEach((key) => { + currentParams.set(key, paramsToAdd[key]); + }); + + // remove specified query parameters + paramsToRemove.forEach((key) => { + currentParams.delete(key); + }); + + // construct the new route with the updated query parameters + const newRoute = `${pathname}?${currentParams.toString()}`; + return newRoute; + }; + + return { + updateQueryParams, + }; +}; diff --git a/web/core/hooks/use-reload-confirmation.tsx b/web/core/hooks/use-reload-confirmation.tsx index 998c5845d98..6ba324d148e 100644 --- a/web/core/hooks/use-reload-confirmation.tsx +++ b/web/core/hooks/use-reload-confirmation.tsx @@ -1,8 +1,10 @@ import { useCallback, useEffect, useState } from "react"; //TODO: remove temp flag isActive later and use showAlert as the source of truth -const useReloadConfirmations = (isActive = true) => { - const [showAlert, setShowAlert] = useState(false); +const useReloadConfirmations = (isActive = true, message?: string, defaultShowAlert = false, onLeave?: () => void) => { + const [showAlert, setShowAlert] = useState(defaultShowAlert); + + const alertMessage = message ?? "Are you sure you want to leave? Changes you made may not be saved."; const handleBeforeUnload = useCallback( (event: BeforeUnloadEvent) => { @@ -28,8 +30,10 @@ const useReloadConfirmations = (isActive = true) => { const isAnchorTargetBlank = anchorElement.getAttribute("target") === "_blank"; if (isAnchorTargetBlank) return; // show confirm dialog - const leave = confirm("Are you sure you want to leave? Changes you made may not be saved."); - if (!leave) { + const isLeaving = confirm(alertMessage); + if (isLeaving) { + onLeave && onLeave(); + } else { event.preventDefault(); event.stopPropagation(); } diff --git a/web/core/services/page/index.ts b/web/core/services/page/index.ts index d89b175d633..b25199e7f3a 100644 --- a/web/core/services/page/index.ts +++ b/web/core/services/page/index.ts @@ -1 +1,2 @@ +export * from "./project-page-version.service"; export * from "./project-page.service"; diff --git a/web/core/services/page/project-page-version.service.ts b/web/core/services/page/project-page-version.service.ts new file mode 100644 index 00000000000..05732e3d225 --- /dev/null +++ b/web/core/services/page/project-page-version.service.ts @@ -0,0 +1,33 @@ +// plane types +import { TPageVersion } from "@plane/types"; +// helpers +import { API_BASE_URL } from "@/helpers/common.helper"; +// services +import { APIService } from "@/services/api.service"; + +export class ProjectPageVersionService extends APIService { + constructor() { + super(API_BASE_URL); + } + + async fetchAllVersions(workspaceSlug: string, projectId: string, pageId: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/versions/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async fetchVersionById( + workspaceSlug: string, + projectId: string, + pageId: string, + versionId: string + ): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/versions/${versionId}/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } +} diff --git a/web/core/store/cycle.store.ts b/web/core/store/cycle.store.ts index 2845527c23b..b9a1f147648 100644 --- a/web/core/store/cycle.store.ts +++ b/web/core/store/cycle.store.ts @@ -624,6 +624,7 @@ export class CycleStore implements ICycleStore { .then((response) => { runInAction(() => { set(this.cycleMap, [cycleId, "archived_at"], response.archived_at); + if (this.rootStore.favorite.entityMap[cycleId]) this.rootStore.favorite.removeFavoriteFromStore(cycleId); }); }) .catch((error) => { diff --git a/web/core/store/favorite.store.ts b/web/core/store/favorite.store.ts index 30917afcb24..9d04ed1303e 100644 --- a/web/core/store/favorite.store.ts +++ b/web/core/store/favorite.store.ts @@ -18,6 +18,7 @@ export interface IFavoriteStore { }; // computed actions existingFolders: string[]; + groupedFavorites: { [favoriteId: string]: IFavorite }; // actions fetchFavorite: (workspaceSlug: string) => Promise; // CRUD actions @@ -57,6 +58,7 @@ export class FavoriteStore implements IFavoriteStore { favoriteIds: observable, //computed existingFolders: computed, + groupedFavorites: computed, // action fetchFavorite: action, // CRUD actions @@ -80,6 +82,23 @@ export class FavoriteStore implements IFavoriteStore { return Object.values(this.favoriteMap).map((fav) => fav.name); } + get groupedFavorites() { + const data: { [favoriteId: string]: IFavorite } = JSON.parse(JSON.stringify(this.favoriteMap)); + + Object.values(data).forEach((fav) => { + if (fav.parent && data[fav.parent]) { + if (data[fav.parent].children) { + if (!data[fav.parent].children.some((f) => f.id === fav.id)) { + data[fav.parent].children.push(fav); + } + } else { + data[fav.parent].children = [fav]; + } + } + }); + return data; + } + /** * Creates a favorite in the workspace and adds it to the store * @param workspaceSlug @@ -151,22 +170,8 @@ export class FavoriteStore implements IFavoriteStore { */ moveFavorite = async (workspaceSlug: string, favoriteId: string, data: Partial) => { const oldParent = this.favoriteMap[favoriteId].parent; - const favorite = this.favoriteMap[favoriteId]; try { runInAction(() => { - // add the favorite to the new parent - if (!data.parent) return; - set(this.favoriteMap, [data.parent, "children"], [favorite, ...this.favoriteMap[data.parent].children]); - - // remove the favorite from the old parent - if (oldParent) { - set( - this.favoriteMap, - [oldParent, "children"], - this.favoriteMap[oldParent].children.filter((child) => child.id !== favoriteId) - ); - } - // add parent of the favorite set(this.favoriteMap, [favoriteId, "parent"], data.parent); }); @@ -177,21 +182,6 @@ export class FavoriteStore implements IFavoriteStore { // revert the changes runInAction(() => { if (!data.parent) return; - // remove the favorite from the new parent - set( - this.favoriteMap, - [data.parent, "children"], - this.favoriteMap[data.parent].children.filter((child) => child.id !== favoriteId) - ); - - // add the favorite back to the old parent - if (oldParent) { - set( - this.favoriteMap, - [oldParent, "children"], - [...this.favoriteMap[oldParent].children, this.favoriteMap[favoriteId]] - ); - } // revert the parent set(this.favoriteMap, [favoriteId, "parent"], oldParent); @@ -223,28 +213,13 @@ export class FavoriteStore implements IFavoriteStore { runInAction(() => { //remove parent set(this.favoriteMap, [favoriteId, "parent"], null); - - //remove children from parent - if (parent) { - set( - this.favoriteMap, - [parent, "children"], - this.favoriteMap[parent].children.filter((child) => child.id !== favoriteId) - ); - } }); await this.favoriteService.updateFavorite(workspaceSlug, favoriteId, data); } catch (error) { console.error("Failed to move favorite"); runInAction(() => { set(this.favoriteMap, [favoriteId, "parent"], parent); - if (parent) { - set( - this.favoriteMap, - [parent, "children"], - [...this.favoriteMap[parent].children, this.favoriteMap[favoriteId]] - ); - } + throw error; }); throw error; @@ -254,15 +229,26 @@ export class FavoriteStore implements IFavoriteStore { removeFavoriteEntityFromStore = (entity_identifier: string, entity_type: string) => { switch (entity_type) { case "view": - return (this.viewStore.viewMap[entity_identifier].is_favorite = false); + return ( + this.viewStore.viewMap[entity_identifier] && (this.viewStore.viewMap[entity_identifier].is_favorite = false) + ); case "module": - return (this.moduleStore.moduleMap[entity_identifier].is_favorite = false); + return ( + this.moduleStore.moduleMap[entity_identifier] && + (this.moduleStore.moduleMap[entity_identifier].is_favorite = false) + ); case "page": - return (this.pageStore.data[entity_identifier].is_favorite = false); + return this.pageStore.data[entity_identifier] && (this.pageStore.data[entity_identifier].is_favorite = false); case "cycle": - return (this.cycleStore.cycleMap[entity_identifier].is_favorite = false); + return ( + this.cycleStore.cycleMap[entity_identifier] && + (this.cycleStore.cycleMap[entity_identifier].is_favorite = false) + ); case "project": - return (this.projectStore.projectMap[entity_identifier].is_favorite = false); + return ( + this.projectStore.projectMap[entity_identifier] && + (this.projectStore.projectMap[entity_identifier].is_favorite = false) + ); default: return; } @@ -276,29 +262,21 @@ export class FavoriteStore implements IFavoriteStore { */ deleteFavorite = async (workspaceSlug: string, favoriteId: string) => { const parent = this.favoriteMap[favoriteId].parent; - const children = this.favoriteMap[favoriteId].children; + const children = this.groupedFavorites[favoriteId].children; const entity_identifier = this.favoriteMap[favoriteId].entity_identifier; const initialState = this.favoriteMap[favoriteId]; try { + await this.favoriteService.deleteFavorite(workspaceSlug, favoriteId); runInAction(() => { - if (parent) { - set( - this.favoriteMap, - [parent, "children"], - this.favoriteMap[parent].children.filter((child) => child.id !== favoriteId) - ); - } delete this.favoriteMap[favoriteId]; entity_identifier && delete this.entityMap[entity_identifier]; this.favoriteIds = this.favoriteIds.filter((id) => id !== favoriteId); }); - await this.favoriteService.deleteFavorite(workspaceSlug, favoriteId); runInAction(() => { entity_identifier && this.removeFavoriteEntityFromStore(entity_identifier, initialState.entity_type); if (children) { children.forEach((child) => { - console.log(child.entity_type); if (!child.entity_identifier) return; this.removeFavoriteEntityFromStore(child.entity_identifier, child.entity_type); }); @@ -326,9 +304,6 @@ export class FavoriteStore implements IFavoriteStore { const initialState = this.entityMap[entityId]; try { const favoriteId = this.entityMap[entityId].id; - runInAction(() => { - delete this.entityMap[entityId]; - }); await this.deleteFavorite(workspaceSlug, favoriteId); } catch (error) { console.error("Failed to remove favorite entity from favorite store", error); @@ -341,19 +316,22 @@ export class FavoriteStore implements IFavoriteStore { removeFavoriteFromStore = (entity_identifier: string) => { try { - const favoriteId = this.entityMap[entity_identifier].id; - const favorite = this.favoriteMap[favoriteId]; - const parent = favorite.parent; - + const favoriteId = this.entityMap[entity_identifier]?.id; + const oldData = this.favoriteMap[favoriteId]; + const projectData = Object.values(this.favoriteMap).filter( + (fav) => fav.project_id === entity_identifier && fav.entity_type !== "project" + ); runInAction(() => { - if (parent) { - set( - this.favoriteMap, - [parent, "children"], - this.favoriteMap[parent].children.filter((child) => child.id !== favoriteId) - ); - } + projectData && + projectData.forEach(async (fav) => { + this.removeFavoriteFromStore(fav.entity_identifier!); + this.removeFavoriteEntityFromStore(fav.entity_identifier!, fav.entity_type); + }); + + if (!favoriteId) return; delete this.favoriteMap[favoriteId]; + this.removeFavoriteEntityFromStore(entity_identifier!, oldData.entity_type); + delete this.entityMap[entity_identifier]; this.favoriteIds = this.favoriteIds.filter((id) => id !== favoriteId); }); @@ -373,8 +351,6 @@ export class FavoriteStore implements IFavoriteStore { try { const response = await this.favoriteService.getGroupedFavorites(workspaceSlug, favoriteId); runInAction(() => { - // add children to the favorite - set(this.favoriteMap, [favoriteId, "children"], response); // add the favorites to the map response.forEach((favorite) => { set(this.favoriteMap, [favorite.id], favorite); diff --git a/web/core/store/module.store.ts b/web/core/store/module.store.ts index 23e5cb0127a..c3401dfbe92 100644 --- a/web/core/store/module.store.ts +++ b/web/core/store/module.store.ts @@ -557,6 +557,7 @@ export class ModulesStore implements IModuleStore { .then((response) => { runInAction(() => { set(this.moduleMap, [moduleId, "archived_at"], response.archived_at); + if (this.rootStore.favorite.entityMap[moduleId]) this.rootStore.favorite.removeFavoriteFromStore(moduleId); }); }) .catch((error) => { diff --git a/web/core/store/pages/page.ts b/web/core/store/pages/page.ts index 507259de75a..47096912656 100644 --- a/web/core/store/pages/page.ts +++ b/web/core/store/pages/page.ts @@ -443,6 +443,7 @@ export class Page implements IPage { runInAction(() => { this.archived_at = response.archived_at; }); + if (this.rootStore.favorite.entityMap[this.id]) this.rootStore.favorite.removeFavoriteFromStore(this.id); }; /** diff --git a/web/core/store/project/project.store.ts b/web/core/store/project/project.store.ts index 6e1751f71ce..ef817dab126 100644 --- a/web/core/store/project/project.store.ts +++ b/web/core/store/project/project.store.ts @@ -418,6 +418,7 @@ export class ProjectStore implements IProjectStore { .then((response) => { runInAction(() => { set(this.projectMap, [projectId, "archived_at"], response.archived_at); + this.rootStore.favorite.removeFavoriteFromStore(projectId); }); }) .catch((error) => { diff --git a/web/ee/components/issues/issue-modal/additional-properties.tsx b/web/ee/components/issues/issue-modal/additional-properties.tsx new file mode 100644 index 00000000000..388efd52388 --- /dev/null +++ b/web/ee/components/issues/issue-modal/additional-properties.tsx @@ -0,0 +1 @@ +export * from "ce/components/issues/issue-modal/additional-properties"; diff --git a/web/ee/components/issues/issue-modal/index.ts b/web/ee/components/issues/issue-modal/index.ts index 031608e25ff..f2c8494163f 100644 --- a/web/ee/components/issues/issue-modal/index.ts +++ b/web/ee/components/issues/issue-modal/index.ts @@ -1 +1,3 @@ -export * from "./modal"; +export * from "./provider"; +export * from "./issue-type-select"; +export * from "./additional-properties"; diff --git a/web/ee/components/issues/issue-modal/issue-type-select.tsx b/web/ee/components/issues/issue-modal/issue-type-select.tsx new file mode 100644 index 00000000000..897305bf797 --- /dev/null +++ b/web/ee/components/issues/issue-modal/issue-type-select.tsx @@ -0,0 +1 @@ +export * from "ce/components/issues/issue-modal/issue-type-select"; diff --git a/web/ee/components/issues/issue-modal/modal.tsx b/web/ee/components/issues/issue-modal/modal.tsx deleted file mode 100644 index 609809f8628..00000000000 --- a/web/ee/components/issues/issue-modal/modal.tsx +++ /dev/null @@ -1 +0,0 @@ -export * from "ce/components/issues/issue-modal/modal"; diff --git a/web/ee/components/issues/issue-modal/provider.tsx b/web/ee/components/issues/issue-modal/provider.tsx new file mode 100644 index 00000000000..b79fe9ecdc6 --- /dev/null +++ b/web/ee/components/issues/issue-modal/provider.tsx @@ -0,0 +1 @@ +export * from "ce/components/issues/issue-modal/provider"; diff --git a/web/ee/types/index.ts b/web/ee/types/index.ts new file mode 100644 index 00000000000..0d4b66523e9 --- /dev/null +++ b/web/ee/types/index.ts @@ -0,0 +1,2 @@ +export * from "./projects"; +export * from "./issue-types"; diff --git a/web/ee/types/issue-types/index.ts b/web/ee/types/issue-types/index.ts new file mode 100644 index 00000000000..7259fa35181 --- /dev/null +++ b/web/ee/types/issue-types/index.ts @@ -0,0 +1 @@ +export * from "./issue-property-values.d"; diff --git a/web/ee/types/issue-types/issue-property-values.d.ts b/web/ee/types/issue-types/issue-property-values.d.ts new file mode 100644 index 00000000000..9aac710889d --- /dev/null +++ b/web/ee/types/issue-types/issue-property-values.d.ts @@ -0,0 +1 @@ +export * from "ce/types/issue-types/issue-property-values.d"; diff --git a/web/helpers/issue-modal.helper.ts b/web/helpers/issue-modal.helper.ts new file mode 100644 index 00000000000..8d0f8e7b3dc --- /dev/null +++ b/web/helpers/issue-modal.helper.ts @@ -0,0 +1,3 @@ +import { ISSUE_FORM_TAB_INDICES } from "@/constants/issue-modal"; + +export const getTabIndex = (key: string) => ISSUE_FORM_TAB_INDICES.findIndex((tabIndex) => tabIndex === key) + 1; diff --git a/web/next.config.js b/web/next.config.js index 47b81e0870a..18ac5936b9e 100644 --- a/web/next.config.js +++ b/web/next.config.js @@ -4,10 +4,6 @@ require("dotenv").config({ path: ".env" }); // const path = require("path"); const { withSentryConfig } = require("@sentry/nextjs"); -const withPWA = require("next-pwa")({ - dest: "public", - disable: process.env.NODE_ENV === "development", -}); const nextConfig = { trailingSlash: true, @@ -133,10 +129,8 @@ const sentryConfig = { automaticVercelMonitors: true, }; -const config = withPWA(nextConfig); - if (parseInt(process.env.SENTRY_MONITORING_ENABLED || "0", 10)) { - module.exports = withSentryConfig(config, sentryConfig); + module.exports = withSentryConfig(nextConfig, sentryConfig); } else { - module.exports = config; + module.exports = nextConfig; } diff --git a/web/package.json b/web/package.json index 52906217333..abb529781cb 100644 --- a/web/package.json +++ b/web/package.json @@ -44,7 +44,6 @@ "mobx-react": "^9.1.1", "mobx-utils": "^6.0.8", "next": "^14.2.3", - "next-pwa": "^5.6.0", "next-themes": "^0.2.1", "nprogress": "^0.2.0", "posthog-js": "^1.131.3", diff --git a/yarn.lock b/yarn.lock index e5284900c03..a33ac524f2f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -20,15 +20,6 @@ "@jridgewell/gen-mapping" "^0.3.5" "@jridgewell/trace-mapping" "^0.3.24" -"@apideck/better-ajv-errors@^0.3.1": - version "0.3.6" - resolved "https://registry.yarnpkg.com/@apideck/better-ajv-errors/-/better-ajv-errors-0.3.6.tgz#957d4c28e886a64a8141f7522783be65733ff097" - integrity sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA== - dependencies: - json-schema "^0.4.0" - jsonpointer "^5.0.0" - leven "^3.1.0" - "@atlaskit/pragmatic-drag-and-drop-auto-scroll@^1.3.0": version "1.3.0" resolved "https://registry.yarnpkg.com/@atlaskit/pragmatic-drag-and-drop-auto-scroll/-/pragmatic-drag-and-drop-auto-scroll-1.3.0.tgz#6af382a2d75924f5f0699ebf1b348e2ea8d5a2cd" @@ -74,7 +65,7 @@ resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.24.7.tgz#d23bbea508c3883ba8251fb4164982c36ea577ed" integrity sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw== -"@babel/core@^7.11.1", "@babel/core@^7.18.5", "@babel/core@^7.18.9", "@babel/core@^7.23.0", "@babel/core@^7.24.4": +"@babel/core@^7.18.5", "@babel/core@^7.18.9", "@babel/core@^7.23.0", "@babel/core@^7.24.4": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.24.7.tgz#b676450141e0b52a3d43bc91da86aa608f950ac4" integrity sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g== @@ -196,7 +187,7 @@ "@babel/traverse" "^7.24.7" "@babel/types" "^7.24.7" -"@babel/helper-module-imports@^7.10.4", "@babel/helper-module-imports@^7.16.7", "@babel/helper-module-imports@^7.24.7": +"@babel/helper-module-imports@^7.16.7", "@babel/helper-module-imports@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz#f2f980392de5b84c3328fc71d38bd81bbb83042b" integrity sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA== @@ -906,7 +897,7 @@ "@babel/helper-create-regexp-features-plugin" "^7.24.7" "@babel/helper-plugin-utils" "^7.24.7" -"@babel/preset-env@^7.11.0", "@babel/preset-env@^7.24.4": +"@babel/preset-env@^7.24.4": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.24.7.tgz#ff067b4e30ba4a72f225f12f123173e77b987f37" integrity sha512-1YZNsc+y6cTvWlDHidMBsQZrZfEFjRIo/BZCT906PMdzOyXtSLTgqGdrpcuTDCXyd11Am5uQULtDIcCfnTc8fQ== @@ -1038,7 +1029,7 @@ resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310" integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== -"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.3", "@babel/runtime@^7.21.0", "@babel/runtime@^7.23.2", "@babel/runtime@^7.23.9", "@babel/runtime@^7.24.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.3", "@babel/runtime@^7.21.0", "@babel/runtime@^7.23.2", "@babel/runtime@^7.23.9", "@babel/runtime@^7.24.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.7.tgz#f4f0d5530e8dbdf59b3451b9b3e594b6ba082e12" integrity sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw== @@ -2504,14 +2495,6 @@ resolved "https://registry.yarnpkg.com/@remirror/core-constants/-/core-constants-2.0.2.tgz#f05eccdc69e3a65e7d524b52548f567904a11a1a" integrity sha512-dyHY+sMF0ihPus3O27ODd4+agdHMEmuRdyiZJ2CCWjPV5UFmn17ZbElvk6WOGVE4rdCJKZQCrPV2BcikOMLUGQ== -"@rollup/plugin-babel@^5.2.0": - version "5.3.1" - resolved "https://registry.yarnpkg.com/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz#04bc0608f4aa4b2e4b1aebf284344d0f68fda283" - integrity sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q== - dependencies: - "@babel/helper-module-imports" "^7.10.4" - "@rollup/pluginutils" "^3.1.0" - "@rollup/plugin-commonjs@24.0.0": version "24.0.0" resolved "https://registry.yarnpkg.com/@rollup/plugin-commonjs/-/plugin-commonjs-24.0.0.tgz#fb7cf4a6029f07ec42b25daa535c75b05a43f75c" @@ -2524,35 +2507,6 @@ is-reference "1.2.1" magic-string "^0.27.0" -"@rollup/plugin-node-resolve@^11.2.1": - version "11.2.1" - resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-11.2.1.tgz#82aa59397a29cd4e13248b106e6a4a1880362a60" - integrity sha512-yc2n43jcqVyGE2sqV5/YCmocy9ArjVAP/BeXyTtADTBBX6V0e5UMqwO8CdQ0kzjb6zu5P1qMzsScCMRvE9OlVg== - dependencies: - "@rollup/pluginutils" "^3.1.0" - "@types/resolve" "1.17.1" - builtin-modules "^3.1.0" - deepmerge "^4.2.2" - is-module "^1.0.0" - resolve "^1.19.0" - -"@rollup/plugin-replace@^2.4.1": - version "2.4.2" - resolved "https://registry.yarnpkg.com/@rollup/plugin-replace/-/plugin-replace-2.4.2.tgz#a2d539314fbc77c244858faa523012825068510a" - integrity sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg== - dependencies: - "@rollup/pluginutils" "^3.1.0" - magic-string "^0.25.7" - -"@rollup/pluginutils@^3.1.0": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-3.1.0.tgz#706b4524ee6dc8b103b3c995533e5ad680c02b9b" - integrity sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg== - dependencies: - "@types/estree" "0.0.39" - estree-walker "^1.0.1" - picomatch "^2.2.2" - "@rollup/pluginutils@^5.0.1": version "5.1.0" resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-5.1.0.tgz#7e53eddc8c7f483a4ad0b94afb1f7f5fd3c771e0" @@ -3625,16 +3579,6 @@ "@types/express" "^4.7.0" file-system-cache "2.3.0" -"@surma/rollup-plugin-off-main-thread@^2.2.3": - version "2.2.3" - resolved "https://registry.yarnpkg.com/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz#ee34985952ca21558ab0d952f00298ad2190c053" - integrity sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ== - dependencies: - ejs "^3.1.6" - json5 "^2.2.0" - magic-string "^0.25.0" - string.prototype.matchall "^4.0.6" - "@swc/core-darwin-arm64@1.5.7": version "1.5.7" resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.5.7.tgz#2b5cdbd34e4162e50de6147dd1a5cb12d23b08e8" @@ -4154,11 +4098,6 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== -"@types/estree@0.0.39": - version "0.0.39" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" - integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== - "@types/estree@^0.0.51": version "0.0.51" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.51.tgz#cfd70924a25a3fd32b218e5e420e6897e1ac4f40" @@ -4184,14 +4123,6 @@ "@types/qs" "*" "@types/serve-static" "*" -"@types/glob@^7.1.1": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.2.0.tgz#bc1b5bf3aa92f25bd5dd39f35c57361bdce5b2eb" - integrity sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA== - dependencies: - "@types/minimatch" "*" - "@types/node" "*" - "@types/hast@^2.0.0": version "2.3.10" resolved "https://registry.yarnpkg.com/@types/hast/-/hast-2.3.10.tgz#5c9d9e0b304bbb8879b857225c5ebab2d81d7643" @@ -4226,7 +4157,7 @@ resolved "https://registry.yarnpkg.com/@types/js-cookie/-/js-cookie-3.0.6.tgz#a04ca19e877687bd449f5ad37d33b104b71fdf95" integrity sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ== -"@types/json-schema@*", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": +"@types/json-schema@*", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": version "7.0.15" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== @@ -4323,11 +4254,6 @@ resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690" integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w== -"@types/minimatch@*": - version "5.1.2" - resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-5.1.2.tgz#07508b45797cb81ec3f273011b054cd0755eddca" - integrity sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA== - "@types/ms@*": version "0.7.34" resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.34.tgz#10964ba0dee6ac4cd462e2795b6bebd407303433" @@ -4467,13 +4393,6 @@ dependencies: "@types/react" "*" -"@types/resolve@1.17.1": - version "1.17.1" - resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6" - integrity sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw== - dependencies: - "@types/node" "*" - "@types/resolve@^1.20.2": version "1.20.6" resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.20.6.tgz#e6e60dad29c2c8c206c026e6dd8d6d1bdda850b8" @@ -4511,7 +4430,7 @@ resolved "https://registry.yarnpkg.com/@types/shimmer/-/shimmer-1.0.5.tgz#491d8984d4510e550bfeb02d518791d7f59d2b88" integrity sha512-9Hp0ObzwwO57DpLFF0InUjUm/II8GmKAvzbefxQTihCb7KI6yc9yzf0nLc4mVdby5N4DRCgQM2wCup9KTieeww== -"@types/trusted-types@*", "@types/trusted-types@^2.0.2": +"@types/trusted-types@*": version "2.0.7" resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11" integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw== @@ -5030,7 +4949,7 @@ ajv@^6.12.4, ajv@^6.12.5: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ajv@^8.0.0, ajv@^8.6.0, ajv@^8.9.0: +ajv@^8.0.0, ajv@^8.9.0: version "8.16.0" resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.16.0.tgz#22e2a92b94f005f7e0f9c9d39652ef0b8f6f0cb4" integrity sha512-F0twR8U1ZU67JIEtekUcLkXkoO5mMMmgGD8sK/xUFzJ805jxHQl92hImFAqqXMyMYjSPOyUPAwHYhB72g5sTXw== @@ -5158,23 +5077,11 @@ array-includes@^3.1.6, array-includes@^3.1.7, array-includes@^3.1.8: get-intrinsic "^1.2.4" is-string "^1.0.7" -array-union@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" - integrity sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng== - dependencies: - array-uniq "^1.0.1" - array-union@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== -array-uniq@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" - integrity sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q== - array.prototype.findlast@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz#3e4fbcb30a15a7f5bf64cf2faae22d139c2e4904" @@ -5292,11 +5199,6 @@ asynckit@^0.4.0: resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== -at-least-node@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" - integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== - attr-accept@^2.2.2: version "2.2.2" resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-2.2.2.tgz#646613809660110749e92f2c10833b70968d929b" @@ -5364,16 +5266,6 @@ babel-core@^7.0.0-bridge.0: resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-7.0.0-bridge.0.tgz#95a492ddd90f9b4e9a4a1da14eb335b87b634ece" integrity sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg== -babel-loader@^8.2.5: - version "8.3.0" - resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.3.0.tgz#124936e841ba4fe8176786d6ff28add1f134d6a8" - integrity sha512-H8SvsMF+m9t15HNLMipppzkC+Y2Yq+v3SonZyU70RBL/h1gxPkH08Ot8pEE9Z4Kd+czyWJClmFS8qzIP9OZ04Q== - dependencies: - find-cache-dir "^3.3.1" - loader-utils "^2.0.0" - make-dir "^3.1.0" - schema-utils "^2.6.5" - babel-plugin-macros@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz#9ef6dc74deb934b4db344dc973ee851d148c50c1" @@ -5467,11 +5359,6 @@ big-integer@^1.6.44: resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.52.tgz#60a887f3047614a8e1bffe5d7173490a97dc8c85" integrity sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg== -big.js@^5.2.2: - version "5.2.2" - resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" - integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== - binary-extensions@^2.0.0: version "2.3.0" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" @@ -5578,11 +5465,6 @@ buffer@^5.5.0: base64-js "^1.3.1" ieee754 "^1.1.13" -builtin-modules@^3.1.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.3.0.tgz#cae62812b89801e9656336e46223e030386be7b6" - integrity sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw== - bundle-require@^3.0.2: version "3.1.2" resolved "https://registry.yarnpkg.com/bundle-require/-/bundle-require-3.1.2.tgz#1374a7bdcb8b330a7ccc862ccbf7c137cc43ad27" @@ -5806,13 +5688,6 @@ clean-css@^5.2.2: dependencies: source-map "~0.6.0" -clean-webpack-plugin@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/clean-webpack-plugin/-/clean-webpack-plugin-4.0.0.tgz#72947d4403d452f38ed61a9ff0ada8122aacd729" - integrity sha512-WuWE1nyTNAyW5T7oNyys2EN0cfP2fdRxhxnIQWiAp0bMabPdHhoGxM8A6YL2GhqwgrPnnaemVE7nv5XJ2Fhh2w== - dependencies: - del "^4.1.1" - cli-cursor@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" @@ -5982,11 +5857,6 @@ commander@~12.1.0: resolved "https://registry.yarnpkg.com/commander/-/commander-12.1.0.tgz#01423b36f501259fdaac4d0e4d60c96c991585d3" integrity sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA== -common-tags@^1.8.0: - version "1.8.2" - resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.2.tgz#94ebb3c076d26032745fd54face7f688ef5ac9c6" - integrity sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA== - commondir@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" @@ -6110,11 +5980,6 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" -crypto-random-string@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" - integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== - crypto-random-string@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-4.0.0.tgz#5a3cc53d7dd86183df5da0312816ceeeb5bb1fc2" @@ -6473,19 +6338,6 @@ defu@^6.1.4: resolved "https://registry.yarnpkg.com/defu/-/defu-6.1.4.tgz#4e0c9cf9ff68fe5f3d7f2765cc1a012dfdcb0479" integrity sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg== -del@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/del/-/del-4.1.1.tgz#9e8f117222ea44a31ff3a156c049b99052a9f0b4" - integrity sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ== - dependencies: - "@types/glob" "^7.1.1" - globby "^6.1.0" - is-path-cwd "^2.0.0" - is-path-in-cwd "^2.0.0" - p-map "^2.0.0" - pify "^4.0.1" - rimraf "^2.6.3" - delaunator@4: version "4.0.1" resolved "https://registry.yarnpkg.com/delaunator/-/delaunator-4.0.1.tgz#3d779687f57919a7a418f8ab947d3bddb6846957" @@ -6702,7 +6554,7 @@ ee-first@1.1.1: resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== -ejs@^3.1.10, ejs@^3.1.6: +ejs@^3.1.10: version "3.1.10" resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.10.tgz#69ab8358b14e896f80cc39e62087b88500c3ac3b" integrity sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA== @@ -6736,11 +6588,6 @@ emoji-regex@^9.2.2: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== -emojis-list@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" - integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== - encodeurl@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" @@ -7404,11 +7251,6 @@ estraverse@^5.1.0, estraverse@^5.2.0, estraverse@^5.3.0: resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== -estree-walker@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-1.0.1.tgz#31bc5d612c96b704106b477e6dd5d8aa138cb700" - integrity sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg== - estree-walker@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" @@ -7549,7 +7391,7 @@ fast-json-parse@^1.0.3: resolved "https://registry.yarnpkg.com/fast-json-parse/-/fast-json-parse-1.0.3.tgz#43e5c61ee4efa9265633046b770fb682a7577c4d" integrity sha512-FRWsaZRWEJ1ESVNbDWmsAlqDk96gPQezzLghafp5J4GUKjbCz3OkAHuZs5TuPEtkbVQERysLp9xv6c24fBm8Aw== -fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0: +fast-json-stable-stringify@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== @@ -7816,16 +7658,6 @@ fs-extra@^11.0.0, fs-extra@^11.1.0: jsonfile "^6.0.1" universalify "^2.0.0" -fs-extra@^9.0.1: - version "9.1.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" - integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== - dependencies: - at-least-node "^1.0.0" - graceful-fs "^4.2.0" - jsonfile "^6.0.1" - universalify "^2.0.0" - fs-minipass@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" @@ -7917,11 +7749,6 @@ get-npm-tarball-url@^2.0.3: resolved "https://registry.yarnpkg.com/get-npm-tarball-url/-/get-npm-tarball-url-2.1.0.tgz#cbd6bb25884622bc3191c761466c93ac83343213" integrity sha512-ro+DiMu5DXgRBabqXupW38h7WPZ9+Ad8UjwhvsmmN8w1sU7ab0nzAXvVZ4kqYg57OrqomRtJvepX5/xvFKNtjA== -get-own-enumerable-property-symbols@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz#b5fde77f22cbe35f390b4e089922c50bce6ef664" - integrity sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g== - get-stdin@^9.0.0: version "9.0.0" resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-9.0.0.tgz#3983ff82e03d56f1b2ea0d3e60325f39d703a575" @@ -8018,7 +7845,7 @@ glob@^10.0.0, glob@^10.3.10: minipass "^7.1.2" path-scurry "^1.11.1" -glob@^7.0.3, glob@^7.1.3, glob@^7.1.6: +glob@^7.1.3: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== @@ -8071,7 +7898,7 @@ globalthis@^1.0.3: define-properties "^1.2.1" gopd "^1.0.1" -globby@^11.0.3, globby@^11.0.4, globby@^11.1.0: +globby@^11.0.3, globby@^11.1.0: version "11.1.0" resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== @@ -8095,17 +7922,6 @@ globby@^14.0.0, globby@^14.0.1: slash "^5.1.0" unicorn-magic "^0.1.0" -globby@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c" - integrity sha512-KVbFv2TQtbzCoxAnfD6JcHZTYCzyliEaaeM/gH8qQdkKr5s0OP9scEgvdcngyk7AVdY6YVW/TJHd+lQ/Df3Daw== - dependencies: - array-union "^1.0.1" - glob "^7.0.3" - object-assign "^4.0.1" - pify "^2.0.0" - pinkie-promise "^2.0.0" - gopd@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" @@ -8379,11 +8195,6 @@ icss-utils@^5.0.0, icss-utils@^5.1.0: resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae" integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA== -idb@^7.0.1: - version "7.1.1" - resolved "https://registry.yarnpkg.com/idb/-/idb-7.1.1.tgz#d910ded866d32c7ced9befc5bfdf36f572ced72b" - integrity sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ== - ieee754@^1.1.13: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" @@ -8650,11 +8461,6 @@ is-map@^2.0.2, is-map@^2.0.3: resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.3.tgz#ede96b7fe1e270b3c4465e3a465658764926d62e" integrity sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw== -is-module@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591" - integrity sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g== - is-nan@^1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.3.2.tgz#043a54adea31748b55b6cd4e09aadafa69bd9e1d" @@ -8680,30 +8486,6 @@ is-number@^7.0.0: resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== -is-obj@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" - integrity sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg== - -is-path-cwd@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-2.2.0.tgz#67d43b82664a7b5191fd9119127eb300048a9fdb" - integrity sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ== - -is-path-in-cwd@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz#bfe2dca26c69f397265a4009963602935a053acb" - integrity sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ== - dependencies: - is-path-inside "^2.1.0" - -is-path-inside@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-2.1.0.tgz#7c9810587d659a40d27bcdb4d5616eab059494b2" - integrity sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg== - dependencies: - path-is-inside "^1.0.2" - is-path-inside@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" @@ -8746,11 +8528,6 @@ is-regex@^1.1.4: call-bind "^1.0.2" has-tostringtag "^1.0.0" -is-regexp@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-1.0.0.tgz#fd2d883545c46bac5a633e7b9a09e87fa2cb5069" - integrity sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA== - is-set@^2.0.2, is-set@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.3.tgz#8ab209ea424608141372ded6e0cb200ef1d9d01d" @@ -8899,15 +8676,6 @@ jake@^10.8.5: filelist "^1.0.4" minimatch "^3.1.2" -jest-worker@^26.2.1: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.6.2.tgz#7f72cbc4d643c365e27b9fd775f9d0eaa9c7a8ed" - integrity sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ== - dependencies: - "@types/node" "*" - merge-stream "^2.0.0" - supports-color "^7.0.0" - jest-worker@^27.4.5: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0" @@ -9027,11 +8795,6 @@ json-schema-traverse@^1.0.0: resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== -json-schema@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5" - integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA== - json-stable-stringify-without-jsonify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" @@ -9044,7 +8807,7 @@ json5@^1.0.2: dependencies: minimist "^1.2.0" -json5@^2.1.2, json5@^2.2.0, json5@^2.2.2, json5@^2.2.3: +json5@^2.2.2, json5@^2.2.3: version "2.2.3" resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== @@ -9058,11 +8821,6 @@ jsonfile@^6.0.1, jsonfile@^6.1.0: optionalDependencies: graceful-fs "^4.1.6" -jsonpointer@^5.0.0: - version "5.0.1" - resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-5.0.1.tgz#2110e0af0900fd37467b5907ecd13a7884a1b559" - integrity sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ== - "jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.3.5: version "3.3.5" resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz#4766bd05a8e2a11af222becd19e15575e52a853a" @@ -9223,15 +8981,6 @@ loader-runner@^4.2.0: resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.0.tgz#c1b4a163b99f614830353b16755e7149ac2314e1" integrity sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg== -loader-utils@^2.0.0: - version "2.0.4" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.4.tgz#8b5cb38b5c34a9a018ee1fc0e6a066d1dfcc528c" - integrity sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw== - dependencies: - big.js "^5.2.2" - emojis-list "^3.0.0" - json5 "^2.1.2" - locate-path@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" @@ -9386,13 +9135,6 @@ magic-string@0.30.8: dependencies: "@jridgewell/sourcemap-codec" "^1.4.15" -magic-string@^0.25.0, magic-string@^0.25.7: - version "0.25.9" - resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.9.tgz#de7f9faf91ef8a1c91d02c2e5314c8277dbcdd1c" - integrity sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ== - dependencies: - sourcemap-codec "^1.4.8" - magic-string@^0.27.0: version "0.27.0" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.27.0.tgz#e4a3413b4bab6d98d2becffd48b4a257effdbbf3" @@ -9415,7 +9157,7 @@ make-dir@^2.0.0, make-dir@^2.1.0: pify "^4.0.1" semver "^5.6.0" -make-dir@^3.0.2, make-dir@^3.1.0: +make-dir@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== @@ -9953,18 +9695,6 @@ neo-async@^2.5.0, neo-async@^2.6.2: resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== -next-pwa@^5.6.0: - version "5.6.0" - resolved "https://registry.yarnpkg.com/next-pwa/-/next-pwa-5.6.0.tgz#f7b1960c4fdd7be4253eb9b41b612ac773392bf4" - integrity sha512-XV8g8C6B7UmViXU8askMEYhWwQ4qc/XqJGnexbLV68hzKaGHZDMtHsm2TNxFcbR7+ypVuth/wwpiIlMwpRJJ5A== - dependencies: - babel-loader "^8.2.5" - clean-webpack-plugin "^4.0.0" - globby "^11.0.4" - terser-webpack-plugin "^5.3.3" - workbox-webpack-plugin "^6.5.4" - workbox-window "^6.5.4" - next-themes@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/next-themes/-/next-themes-0.2.1.tgz#0c9f128e847979daf6c67f70b38e6b6567856e45" @@ -10333,11 +10063,6 @@ p-locate@^5.0.0: dependencies: p-limit "^3.0.2" -p-map@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175" - integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw== - p-try@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" @@ -10421,11 +10146,6 @@ path-is-absolute@^1.0.0: resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== -path-is-inside@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" - integrity sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w== - path-key@^3.0.0, path-key@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" @@ -10527,7 +10247,7 @@ picocolors@^1.0.0, picocolors@^1.0.1: resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1" integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew== -picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.3.0, picomatch@^2.3.1: +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.0, picomatch@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== @@ -10537,7 +10257,7 @@ pidtree@~0.6.0: resolved "https://registry.yarnpkg.com/pidtree/-/pidtree-0.6.0.tgz#90ad7b6d42d5841e69e0a2419ef38f8883aa057c" integrity sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g== -pify@^2.0.0, pify@^2.3.0: +pify@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog== @@ -10547,18 +10267,6 @@ pify@^4.0.1: resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== -pinkie-promise@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" - integrity sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw== - dependencies: - pinkie "^2.0.0" - -pinkie@^2.0.0: - version "2.0.4" - resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" - integrity sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg== - pirates@^4.0.1, pirates@^4.0.6: version "4.0.6" resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.6.tgz#3018ae32ecfcff6c29ba2267cbf21166ac1f36b9" @@ -10859,11 +10567,6 @@ prettier@^3.1.1, prettier@^3.2.5, prettier@latest: resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.1.tgz#e68935518dd90bb7ec4821ba970e68f8de16e1ac" integrity sha512-7CAwy5dRsxs8PHXT3twixW9/OEll8MLE0VRPCJyl7CkS6VHGPSlsVaWTiASPTyGyYRyApxlaWTzwUxVNrhcwDg== -pretty-bytes@^5.3.0, pretty-bytes@^5.4.1: - version "5.6.0" - resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb" - integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg== - pretty-error@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-4.0.0.tgz#90a703f46dd7234adb46d0f84823e9d1cb8f10d6" @@ -11740,13 +11443,6 @@ rfdc@^1.3.1: resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.1.tgz#2b6d4df52dffe8bb346992a10ea9451f24373a8f" integrity sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg== -rimraf@^2.6.3: - version "2.7.1" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" - integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== - dependencies: - glob "^7.1.3" - rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" @@ -11761,16 +11457,6 @@ rimraf@~2.6.2: dependencies: glob "^7.1.3" -rollup-plugin-terser@^7.0.0: - version "7.0.2" - resolved "https://registry.yarnpkg.com/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz#e8fbba4869981b2dc35ae7e8a502d5c6c04d324d" - integrity sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ== - dependencies: - "@babel/code-frame" "^7.10.4" - jest-worker "^26.2.1" - serialize-javascript "^4.0.0" - terser "^5.0.0" - rollup@3.29.4: version "3.29.4" resolved "https://registry.yarnpkg.com/rollup/-/rollup-3.29.4.tgz#4d70c0f9834146df8705bfb69a9a19c9e1109981" @@ -11778,7 +11464,7 @@ rollup@3.29.4: optionalDependencies: fsevents "~2.3.2" -rollup@^2.43.1, rollup@^2.74.1: +rollup@^2.74.1: version "2.79.1" resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.79.1.tgz#bedee8faef7c9f93a2647ac0108748f497f081c7" integrity sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw== @@ -11887,15 +11573,6 @@ scheduler@^0.23.2: dependencies: loose-envify "^1.1.0" -schema-utils@^2.6.5: - version "2.7.1" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.1.tgz#1ca4f32d1b24c590c203b8e7a50bf0ea4cd394d7" - integrity sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg== - dependencies: - "@types/json-schema" "^7.0.5" - ajv "^6.12.4" - ajv-keywords "^3.5.2" - schema-utils@^3.1.1, schema-utils@^3.2.0: version "3.3.0" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.3.0.tgz#f50a88877c3c01652a15b622ae9e9795df7a60fe" @@ -11981,13 +11658,6 @@ sentence-case@^3.0.4: tslib "^2.0.3" upper-case-first "^2.0.2" -serialize-javascript@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa" - integrity sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw== - dependencies: - randombytes "^2.1.0" - serialize-javascript@^6.0.1: version "6.0.2" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" @@ -12162,11 +11832,6 @@ sonner@^1.4.41: resolved "https://registry.yarnpkg.com/sonner/-/sonner-1.5.0.tgz#af359f817063318415326b33aab54c5d17c747b7" integrity sha512-FBjhG/gnnbN6FY0jaNnqZOMmB73R+5IiyYAw8yBj7L54ER7HB3fOSE5OFiQiE2iXWxeXKvg6fIP4LtVppHEdJA== -source-list-map@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" - integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw== - source-map-js@^1.0.2, source-map-js@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af" @@ -12180,7 +11845,7 @@ source-map-support@^0.5.16, source-map-support@~0.5.20: buffer-from "^1.0.0" source-map "^0.6.0" -source-map@0.8.0-beta.0, source-map@^0.8.0-beta.0: +source-map@0.8.0-beta.0: version "0.8.0-beta.0" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.8.0-beta.0.tgz#d4c1bb42c3f7ee925f005927ba10709e0d1d1f11" integrity sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA== @@ -12197,11 +11862,6 @@ source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== -sourcemap-codec@^1.4.8: - version "1.4.8" - resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" - integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== - space-separated-tokens@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz#1ecd9d2350a3844572c3f4a312bceb018348859f" @@ -12326,7 +11986,7 @@ string-width@^7.0.0: get-east-asian-width "^1.0.0" strip-ansi "^7.1.0" -string.prototype.matchall@^4.0.11, string.prototype.matchall@^4.0.6: +string.prototype.matchall@^4.0.11: version "4.0.11" resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz#1092a72c59268d2abaad76582dccc687c0297e0a" integrity sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg== @@ -12386,15 +12046,6 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -stringify-object@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/stringify-object/-/stringify-object-3.3.0.tgz#703065aefca19300d3ce88af4f5b3956d7556629" - integrity sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw== - dependencies: - get-own-enumerable-property-symbols "^3.0.0" - is-obj "^1.0.1" - is-regexp "^1.0.0" - "strip-ansi-cjs@npm:strip-ansi@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" @@ -12421,11 +12072,6 @@ strip-bom@^3.0.0: resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== -strip-comments@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/strip-comments/-/strip-comments-2.0.1.tgz#4ad11c3fbcac177a67a40ac224ca339ca1c1ba9b" - integrity sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw== - 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" @@ -12504,7 +12150,7 @@ supports-color@^5.3.0: dependencies: has-flag "^3.0.0" -supports-color@^7.0.0, supports-color@^7.1.0: +supports-color@^7.1.0: version "7.2.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== @@ -12695,11 +12341,6 @@ telejson@^7.2.0: dependencies: memoizerific "^1.11.3" -temp-dir@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/temp-dir/-/temp-dir-2.0.0.tgz#bde92b05bdfeb1516e804c9c00ad45177f31321e" - integrity sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg== - temp-dir@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/temp-dir/-/temp-dir-3.0.0.tgz#7f147b42ee41234cc6ba3138cd8e8aa2302acffa" @@ -12712,16 +12353,6 @@ temp@^0.8.4: dependencies: rimraf "~2.6.2" -tempy@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/tempy/-/tempy-0.6.0.tgz#65e2c35abc06f1124a97f387b08303442bde59f3" - integrity sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw== - dependencies: - is-stream "^2.0.0" - temp-dir "^2.0.0" - type-fest "^0.16.0" - unique-string "^2.0.0" - tempy@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/tempy/-/tempy-3.1.0.tgz#00958b6df85db8589cb595465e691852aac038e9" @@ -12732,7 +12363,7 @@ tempy@^3.1.0: type-fest "^2.12.2" unique-string "^3.0.0" -terser-webpack-plugin@^5.3.1, terser-webpack-plugin@^5.3.10, terser-webpack-plugin@^5.3.3: +terser-webpack-plugin@^5.3.1, terser-webpack-plugin@^5.3.10: version "5.3.10" resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz#904f4c9193c6fd2a03f693a2150c62a92f40d199" integrity sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w== @@ -12743,7 +12374,7 @@ terser-webpack-plugin@^5.3.1, terser-webpack-plugin@^5.3.10, terser-webpack-plug serialize-javascript "^6.0.1" terser "^5.26.0" -terser@^5.0.0, terser@^5.10.0, terser@^5.26.0: +terser@^5.10.0, terser@^5.26.0: version "5.31.1" resolved "https://registry.yarnpkg.com/terser/-/terser-5.31.1.tgz#735de3c987dd671e95190e6b98cfe2f07f3cf0d4" integrity sha512-37upzU1+viGvuFtBo9NPufCb9dwM0+l9hMxYyWfBA+fbwrPqNJAhbZ6W47bBFnZHKHTUBnMvi87434qq+qnxOg== @@ -13052,11 +12683,6 @@ type-detect@^4.0.0, type-detect@^4.0.8: resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== -type-fest@^0.16.0: - version "0.16.0" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.16.0.tgz#3240b891a78b0deae910dbeb86553e552a148860" - integrity sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg== - type-fest@^0.20.2: version "0.20.2" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" @@ -13230,13 +12856,6 @@ unified@^10.0.0: trough "^2.0.0" vfile "^5.0.0" -unique-string@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-2.0.0.tgz#39c6451f81afb2749de2b233e3f7c5e8843bd89d" - integrity sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg== - dependencies: - crypto-random-string "^2.0.0" - unique-string@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-3.0.0.tgz#84a1c377aff5fd7a8bc6b55d8244b2bd90d75b9a" @@ -13351,11 +12970,6 @@ untildify@^4.0.0: resolved "https://registry.yarnpkg.com/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b" integrity sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw== -upath@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894" - integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg== - update-browserslist-db@^1.0.16: version "1.0.16" resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz#f6d489ed90fb2f07d67784eb3f53d7891f736356" @@ -13574,14 +13188,6 @@ webpack-hot-middleware@^2.25.1: html-entities "^2.1.0" strip-ansi "^6.0.0" -webpack-sources@^1.4.3: - version "1.4.3" - resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933" - integrity sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ== - dependencies: - source-list-map "^2.0.0" - source-map "~0.6.1" - webpack-sources@^3.2.3: version "3.2.3" resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" @@ -13731,175 +13337,6 @@ wordwrap@^1.0.0: resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== -workbox-background-sync@6.6.1: - version "6.6.1" - resolved "https://registry.yarnpkg.com/workbox-background-sync/-/workbox-background-sync-6.6.1.tgz#08d603a33717ce663e718c30cc336f74909aff2f" - integrity sha512-trJd3ovpWCvzu4sW0E8rV3FUyIcC0W8G+AZ+VcqzzA890AsWZlUGOTSxIMmIHVusUw/FDq1HFWfy/kC/WTRqSg== - dependencies: - idb "^7.0.1" - workbox-core "6.6.1" - -workbox-broadcast-update@6.6.1: - version "6.6.1" - resolved "https://registry.yarnpkg.com/workbox-broadcast-update/-/workbox-broadcast-update-6.6.1.tgz#0fad9454cf8e4ace0c293e5617c64c75d8a8c61e" - integrity sha512-fBhffRdaANdeQ1V8s692R9l/gzvjjRtydBOvR6WCSB0BNE2BacA29Z4r9/RHd9KaXCPl6JTdI9q0bR25YKP8TQ== - dependencies: - workbox-core "6.6.1" - -workbox-build@6.6.1: - version "6.6.1" - resolved "https://registry.yarnpkg.com/workbox-build/-/workbox-build-6.6.1.tgz#6010e9ce550910156761448f2dbea8cfcf759cb0" - integrity sha512-INPgDx6aRycAugUixbKgiEQBWD0MPZqU5r0jyr24CehvNuLPSXp/wGOpdRJmts656lNiXwqV7dC2nzyrzWEDnw== - dependencies: - "@apideck/better-ajv-errors" "^0.3.1" - "@babel/core" "^7.11.1" - "@babel/preset-env" "^7.11.0" - "@babel/runtime" "^7.11.2" - "@rollup/plugin-babel" "^5.2.0" - "@rollup/plugin-node-resolve" "^11.2.1" - "@rollup/plugin-replace" "^2.4.1" - "@surma/rollup-plugin-off-main-thread" "^2.2.3" - ajv "^8.6.0" - common-tags "^1.8.0" - fast-json-stable-stringify "^2.1.0" - fs-extra "^9.0.1" - glob "^7.1.6" - lodash "^4.17.20" - pretty-bytes "^5.3.0" - rollup "^2.43.1" - rollup-plugin-terser "^7.0.0" - source-map "^0.8.0-beta.0" - stringify-object "^3.3.0" - strip-comments "^2.0.1" - tempy "^0.6.0" - upath "^1.2.0" - workbox-background-sync "6.6.1" - workbox-broadcast-update "6.6.1" - workbox-cacheable-response "6.6.1" - workbox-core "6.6.1" - workbox-expiration "6.6.1" - workbox-google-analytics "6.6.1" - workbox-navigation-preload "6.6.1" - workbox-precaching "6.6.1" - workbox-range-requests "6.6.1" - workbox-recipes "6.6.1" - workbox-routing "6.6.1" - workbox-strategies "6.6.1" - workbox-streams "6.6.1" - workbox-sw "6.6.1" - workbox-window "6.6.1" - -workbox-cacheable-response@6.6.1: - version "6.6.1" - resolved "https://registry.yarnpkg.com/workbox-cacheable-response/-/workbox-cacheable-response-6.6.1.tgz#284c2b86be3f4fd191970ace8c8e99797bcf58e9" - integrity sha512-85LY4veT2CnTCDxaVG7ft3NKaFbH6i4urZXgLiU4AiwvKqS2ChL6/eILiGRYXfZ6gAwDnh5RkuDbr/GMS4KSag== - dependencies: - workbox-core "6.6.1" - -workbox-core@6.6.1: - version "6.6.1" - resolved "https://registry.yarnpkg.com/workbox-core/-/workbox-core-6.6.1.tgz#7184776d4134c5ed2f086878c882728fc9084265" - integrity sha512-ZrGBXjjaJLqzVothoE12qTbVnOAjFrHDXpZe7coCb6q65qI/59rDLwuFMO4PcZ7jcbxY+0+NhUVztzR/CbjEFw== - -workbox-expiration@6.6.1: - version "6.6.1" - resolved "https://registry.yarnpkg.com/workbox-expiration/-/workbox-expiration-6.6.1.tgz#a841fa36676104426dbfb9da1ef6a630b4f93739" - integrity sha512-qFiNeeINndiOxaCrd2DeL1Xh1RFug3JonzjxUHc5WkvkD2u5abY3gZL1xSUNt3vZKsFFGGORItSjVTVnWAZO4A== - dependencies: - idb "^7.0.1" - workbox-core "6.6.1" - -workbox-google-analytics@6.6.1: - version "6.6.1" - resolved "https://registry.yarnpkg.com/workbox-google-analytics/-/workbox-google-analytics-6.6.1.tgz#a07a6655ab33d89d1b0b0a935ffa5dea88618c5d" - integrity sha512-1TjSvbFSLmkpqLcBsF7FuGqqeDsf+uAXO/pjiINQKg3b1GN0nBngnxLcXDYo1n/XxK4N7RaRrpRlkwjY/3ocuA== - dependencies: - workbox-background-sync "6.6.1" - workbox-core "6.6.1" - workbox-routing "6.6.1" - workbox-strategies "6.6.1" - -workbox-navigation-preload@6.6.1: - version "6.6.1" - resolved "https://registry.yarnpkg.com/workbox-navigation-preload/-/workbox-navigation-preload-6.6.1.tgz#61a34fe125558dd88cf09237f11bd966504ea059" - integrity sha512-DQCZowCecO+wRoIxJI2V6bXWK6/53ff+hEXLGlQL4Rp9ZaPDLrgV/32nxwWIP7QpWDkVEtllTAK5h6cnhxNxDA== - dependencies: - workbox-core "6.6.1" - -workbox-precaching@6.6.1: - version "6.6.1" - resolved "https://registry.yarnpkg.com/workbox-precaching/-/workbox-precaching-6.6.1.tgz#dedeeba10a2d163d990bf99f1c2066ac0d1a19e2" - integrity sha512-K4znSJ7IKxCnCYEdhNkMr7X1kNh8cz+mFgx9v5jFdz1MfI84pq8C2zG+oAoeE5kFrUf7YkT5x4uLWBNg0DVZ5A== - dependencies: - workbox-core "6.6.1" - workbox-routing "6.6.1" - workbox-strategies "6.6.1" - -workbox-range-requests@6.6.1: - version "6.6.1" - resolved "https://registry.yarnpkg.com/workbox-range-requests/-/workbox-range-requests-6.6.1.tgz#ddaf7e73af11d362fbb2f136a9063a4c7f507a39" - integrity sha512-4BDzk28govqzg2ZpX0IFkthdRmCKgAKreontYRC5YsAPB2jDtPNxqx3WtTXgHw1NZalXpcH/E4LqUa9+2xbv1g== - dependencies: - workbox-core "6.6.1" - -workbox-recipes@6.6.1: - version "6.6.1" - resolved "https://registry.yarnpkg.com/workbox-recipes/-/workbox-recipes-6.6.1.tgz#ea70d2b2b0b0bce8de0a9d94f274d4a688e69fae" - integrity sha512-/oy8vCSzromXokDA+X+VgpeZJvtuf8SkQ8KL0xmRivMgJZrjwM3c2tpKTJn6PZA6TsbxGs3Sc7KwMoZVamcV2g== - dependencies: - workbox-cacheable-response "6.6.1" - workbox-core "6.6.1" - workbox-expiration "6.6.1" - workbox-precaching "6.6.1" - workbox-routing "6.6.1" - workbox-strategies "6.6.1" - -workbox-routing@6.6.1: - version "6.6.1" - resolved "https://registry.yarnpkg.com/workbox-routing/-/workbox-routing-6.6.1.tgz#cba9a1c7e0d1ea11e24b6f8c518840efdc94f581" - integrity sha512-j4ohlQvfpVdoR8vDYxTY9rA9VvxTHogkIDwGdJ+rb2VRZQ5vt1CWwUUZBeD/WGFAni12jD1HlMXvJ8JS7aBWTg== - dependencies: - workbox-core "6.6.1" - -workbox-strategies@6.6.1: - version "6.6.1" - resolved "https://registry.yarnpkg.com/workbox-strategies/-/workbox-strategies-6.6.1.tgz#38d0f0fbdddba97bd92e0c6418d0b1a2ccd5b8bf" - integrity sha512-WQLXkRnsk4L81fVPkkgon1rZNxnpdO5LsO+ws7tYBC6QQQFJVI6v98klrJEjFtZwzw/mB/HT5yVp7CcX0O+mrw== - dependencies: - workbox-core "6.6.1" - -workbox-streams@6.6.1: - version "6.6.1" - resolved "https://registry.yarnpkg.com/workbox-streams/-/workbox-streams-6.6.1.tgz#b2f7ba7b315c27a6e3a96a476593f99c5d227d26" - integrity sha512-maKG65FUq9e4BLotSKWSTzeF0sgctQdYyTMq529piEN24Dlu9b6WhrAfRpHdCncRS89Zi2QVpW5V33NX8PgH3Q== - dependencies: - workbox-core "6.6.1" - workbox-routing "6.6.1" - -workbox-sw@6.6.1: - version "6.6.1" - resolved "https://registry.yarnpkg.com/workbox-sw/-/workbox-sw-6.6.1.tgz#d4c4ca3125088e8b9fd7a748ed537fa0247bd72c" - integrity sha512-R7whwjvU2abHH/lR6kQTTXLHDFU2izht9kJOvBRYK65FbwutT4VvnUAJIgHvfWZ/fokrOPhfoWYoPCMpSgUKHQ== - -workbox-webpack-plugin@^6.5.4: - version "6.6.1" - resolved "https://registry.yarnpkg.com/workbox-webpack-plugin/-/workbox-webpack-plugin-6.6.1.tgz#4f81cc1ad4e5d2cd7477a86ba83c84ee2d187531" - integrity sha512-zpZ+ExFj9NmiI66cFEApyjk7hGsfJ1YMOaLXGXBoZf0v7Iu6hL0ZBe+83mnDq3YYWAfA3fnyFejritjOHkFcrA== - dependencies: - fast-json-stable-stringify "^2.1.0" - pretty-bytes "^5.4.1" - upath "^1.2.0" - webpack-sources "^1.4.3" - workbox-build "6.6.1" - -workbox-window@6.6.1, workbox-window@^6.5.4: - version "6.6.1" - resolved "https://registry.yarnpkg.com/workbox-window/-/workbox-window-6.6.1.tgz#f22a394cbac36240d0dadcbdebc35f711bb7b89e" - integrity sha512-wil4nwOY58nTdCvif/KEZjQ2NP8uk3gGeRNy2jPBbzypU4BT4D9L8xiwbmDBpZlSgJd2xsT9FvSNU0gsxV51JQ== - dependencies: - "@types/trusted-types" "^2.0.2" - workbox-core "6.6.1" - "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"