Compare commits
35
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2ff9f1d775 | ||
|
|
ddad1767a2 | ||
|
|
6a37a2ce21 | ||
|
|
01bd1bde64 | ||
|
|
9268180aec | ||
|
|
ff778b98f5 | ||
|
|
8f5ce6b232 | ||
|
|
58a4ca9f36 | ||
|
|
312b077657 | ||
|
|
c65e42f807 | ||
|
|
f4af78c0fc | ||
|
|
c0b6abc3d5 | ||
|
|
2f2e6626c6 | ||
|
|
6a8d3202b7 | ||
|
|
51b52a7fc3 | ||
|
|
23ede81737 | ||
|
|
b698f44500 | ||
|
|
421839ec51 | ||
|
|
940b5e4e44 | ||
|
|
6003c88d62 | ||
|
|
74913a6659 | ||
|
|
97578684c6 | ||
|
|
88b4d32220 | ||
|
|
f32635a6a8 | ||
|
|
7fe58e0ea9 | ||
|
|
7f22cd1ac1 | ||
|
|
e2550e0b2d | ||
|
|
b016ed78cf | ||
|
|
c429ca7b36 | ||
|
|
ee22dbba1b | ||
|
|
f4a208bd44 | ||
|
|
8edff26ccd | ||
|
|
d08c03f557 | ||
|
|
0b53912295 | ||
|
|
586a320d86 |
@@ -15,3 +15,4 @@ from .state import StateLiteSerializer, StateSerializer
|
||||
from .cycle import CycleSerializer, CycleIssueSerializer, CycleLiteSerializer
|
||||
from .module import ModuleSerializer, ModuleIssueSerializer, ModuleLiteSerializer
|
||||
from .intake import IntakeIssueSerializer
|
||||
from .estimate import EstimatePointSerializer
|
||||
@@ -72,6 +72,7 @@ class BaseSerializer(serializers.ModelSerializer):
|
||||
StateLiteSerializer,
|
||||
UserLiteSerializer,
|
||||
WorkspaceLiteSerializer,
|
||||
EstimatePointSerializer,
|
||||
)
|
||||
|
||||
# Expansion mapper
|
||||
@@ -88,6 +89,7 @@ class BaseSerializer(serializers.ModelSerializer):
|
||||
"owned_by": UserLiteSerializer,
|
||||
"members": UserLiteSerializer,
|
||||
"parent": IssueLiteSerializer,
|
||||
"estimate_point": EstimatePointSerializer,
|
||||
}
|
||||
# Check if field in expansion then expand the field
|
||||
if expand in expansion:
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
# Module imports
|
||||
from plane.db.models import EstimatePoint
|
||||
from .base import BaseSerializer
|
||||
|
||||
|
||||
class EstimatePointSerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = EstimatePoint
|
||||
fields = ["id", "value"]
|
||||
read_only_fields = fields
|
||||
@@ -207,6 +207,7 @@ class IssueSerializer(BaseSerializer):
|
||||
for assignee_id in assignees
|
||||
],
|
||||
batch_size=10,
|
||||
ignore_conflicts=True,
|
||||
)
|
||||
|
||||
if labels is not None:
|
||||
@@ -224,6 +225,7 @@ class IssueSerializer(BaseSerializer):
|
||||
for label_id in labels
|
||||
],
|
||||
batch_size=10,
|
||||
ignore_conflicts=True,
|
||||
)
|
||||
|
||||
# Time updation occues even when other related models are updated
|
||||
|
||||
@@ -71,4 +71,9 @@ urlpatterns = [
|
||||
IssueAttachmentEndpoint.as_view(),
|
||||
name="attachment",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-attachments/<uuid:pk>/",
|
||||
IssueAttachmentEndpoint.as_view(),
|
||||
name="issue-attachment",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -203,6 +203,7 @@ class IssueCreateSerializer(BaseSerializer):
|
||||
for user in assignees
|
||||
],
|
||||
batch_size=10,
|
||||
ignore_conflicts=True,
|
||||
)
|
||||
|
||||
if labels is not None:
|
||||
@@ -220,6 +221,7 @@ class IssueCreateSerializer(BaseSerializer):
|
||||
for label in labels
|
||||
],
|
||||
batch_size=10,
|
||||
ignore_conflicts=True,
|
||||
)
|
||||
|
||||
# Time updation occues even when other related models are updated
|
||||
@@ -283,10 +285,26 @@ class IssueRelationSerializer(BaseSerializer):
|
||||
)
|
||||
name = serializers.CharField(source="related_issue.name", read_only=True)
|
||||
relation_type = serializers.CharField(read_only=True)
|
||||
state_id = serializers.UUIDField(source="related_issue.state.id", read_only=True)
|
||||
priority = serializers.CharField(source="related_issue.priority", read_only=True)
|
||||
assignee_ids = serializers.ListField(
|
||||
child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()),
|
||||
write_only=True,
|
||||
required=False,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = IssueRelation
|
||||
fields = ["id", "project_id", "sequence_id", "relation_type", "name"]
|
||||
fields = [
|
||||
"id",
|
||||
"project_id",
|
||||
"sequence_id",
|
||||
"relation_type",
|
||||
"name",
|
||||
"state_id",
|
||||
"priority",
|
||||
"assignee_ids",
|
||||
]
|
||||
read_only_fields = ["workspace", "project"]
|
||||
|
||||
|
||||
@@ -298,10 +316,26 @@ class RelatedIssueSerializer(BaseSerializer):
|
||||
sequence_id = serializers.IntegerField(source="issue.sequence_id", read_only=True)
|
||||
name = serializers.CharField(source="issue.name", read_only=True)
|
||||
relation_type = serializers.CharField(read_only=True)
|
||||
state_id = serializers.UUIDField(source="issue.state.id", read_only=True)
|
||||
priority = serializers.CharField(source="issue.priority", read_only=True)
|
||||
assignee_ids = serializers.ListField(
|
||||
child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()),
|
||||
write_only=True,
|
||||
required=False,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = IssueRelation
|
||||
fields = ["id", "project_id", "sequence_id", "relation_type", "name"]
|
||||
fields = [
|
||||
"id",
|
||||
"project_id",
|
||||
"sequence_id",
|
||||
"relation_type",
|
||||
"name",
|
||||
"state_id",
|
||||
"priority",
|
||||
"assignee_ids",
|
||||
]
|
||||
read_only_fields = ["workspace", "project"]
|
||||
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ from plane.db.models import (
|
||||
ProjectMember,
|
||||
WorkspaceHomePreference,
|
||||
Sticky,
|
||||
WorkspaceUserPreference,
|
||||
)
|
||||
from plane.utils.constants import RESTRICTED_WORKSPACE_SLUGS
|
||||
|
||||
@@ -258,3 +259,10 @@ class StickySerializer(BaseSerializer):
|
||||
fields = "__all__"
|
||||
read_only_fields = ["workspace", "owner"]
|
||||
extra_kwargs = {"name": {"required": False}}
|
||||
|
||||
|
||||
class WorkspaceUserPreferenceSerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = WorkspaceUserPreference
|
||||
fields = ["key", "is_pinned", "sort_order"]
|
||||
read_only_fields = ["workspace", "created_by", "updated_by"]
|
||||
|
||||
@@ -31,6 +31,7 @@ from plane.app.views import (
|
||||
UserRecentVisitViewSet,
|
||||
WorkspaceHomePreferenceViewSet,
|
||||
WorkspaceStickyViewSet,
|
||||
WorkspaceUserPreferenceViewSet,
|
||||
)
|
||||
|
||||
|
||||
@@ -258,4 +259,15 @@ urlpatterns = [
|
||||
),
|
||||
name="workspace-sticky",
|
||||
),
|
||||
# User Preference
|
||||
path(
|
||||
"workspaces/<str:slug>/sidebar-preferences/",
|
||||
WorkspaceUserPreferenceViewSet.as_view(),
|
||||
name="workspace-user-preference",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/sidebar-preferences/<str:key>/",
|
||||
WorkspaceUserPreferenceViewSet.as_view(),
|
||||
name="workspace-user-preference",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -48,6 +48,7 @@ from .workspace.favorite import (
|
||||
WorkspaceFavoriteGroupEndpoint,
|
||||
)
|
||||
from .workspace.recent_visit import UserRecentVisitViewSet
|
||||
from .workspace.user_preference import WorkspaceUserPreferenceViewSet
|
||||
|
||||
from .workspace.member import (
|
||||
WorkSpaceMemberViewSet,
|
||||
|
||||
@@ -47,6 +47,7 @@ from plane.db.models import (
|
||||
User,
|
||||
Project,
|
||||
ProjectMember,
|
||||
UserRecentVisit,
|
||||
)
|
||||
from plane.utils.analytics_plot import burndown_plot
|
||||
from plane.bgtasks.recent_visited_task import recent_visited_task
|
||||
@@ -543,6 +544,13 @@ class CycleViewSet(BaseViewSet):
|
||||
entity_identifier=pk,
|
||||
project_id=project_id,
|
||||
).delete()
|
||||
# Delete the cycle from recent visits
|
||||
UserRecentVisit.objects.filter(
|
||||
project_id=project_id,
|
||||
workspace__slug=slug,
|
||||
entity_identifier=pk,
|
||||
entity_name="cycle",
|
||||
).delete(soft=False)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ from plane.db.models import (
|
||||
Project,
|
||||
ProjectMember,
|
||||
CycleIssue,
|
||||
UserRecentVisit,
|
||||
)
|
||||
from plane.utils.grouper import (
|
||||
issue_group_values,
|
||||
@@ -671,6 +672,13 @@ class IssueViewSet(BaseViewSet):
|
||||
issue = Issue.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
|
||||
|
||||
issue.delete()
|
||||
# delete the issue from recent visits
|
||||
UserRecentVisit.objects.filter(
|
||||
project_id=project_id,
|
||||
workspace__slug=slug,
|
||||
entity_identifier=pk,
|
||||
entity_name="issue",
|
||||
).delete(soft=False)
|
||||
issue_activity.delay(
|
||||
type="issue.activity.deleted",
|
||||
requested_data=json.dumps({"issue_id": str(pk)}),
|
||||
|
||||
@@ -54,6 +54,7 @@ from plane.db.models import (
|
||||
ModuleLink,
|
||||
ModuleUserProperties,
|
||||
Project,
|
||||
UserRecentVisit,
|
||||
)
|
||||
from plane.utils.analytics_plot import burndown_plot
|
||||
from plane.utils.timezone_converter import user_timezone_converter
|
||||
@@ -808,6 +809,13 @@ class ModuleViewSet(BaseViewSet):
|
||||
entity_identifier=pk,
|
||||
project_id=project_id,
|
||||
).delete()
|
||||
# delete the module from recent visits
|
||||
UserRecentVisit.objects.filter(
|
||||
project_id=project_id,
|
||||
workspace__slug=slug,
|
||||
entity_identifier=pk,
|
||||
entity_name="module",
|
||||
).delete(soft=False)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ from plane.db.models import (
|
||||
ProjectMember,
|
||||
ProjectPage,
|
||||
Project,
|
||||
UserRecentVisit,
|
||||
)
|
||||
from plane.utils.error_codes import ERROR_CODES
|
||||
from ..base import BaseAPIView, BaseViewSet
|
||||
@@ -387,6 +388,13 @@ class PageViewSet(BaseViewSet):
|
||||
entity_identifier=pk,
|
||||
entity_type="page",
|
||||
).delete()
|
||||
# Delete the page from recent visit
|
||||
UserRecentVisit.objects.filter(
|
||||
project_id=project_id,
|
||||
workspace__slug=slug,
|
||||
entity_identifier=pk,
|
||||
entity_name="page",
|
||||
).delete(soft=False)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
|
||||
@@ -53,6 +53,23 @@ class StateViewSet(BaseViewSet):
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
def partial_update(self, request, slug, project_id, pk):
|
||||
try:
|
||||
state = State.objects.get(
|
||||
pk=pk, project_id=project_id, workspace__slug=slug
|
||||
)
|
||||
serializer = StateSerializer(state, data=request.data, partial=True)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
except IntegrityError as e:
|
||||
if "already exists" in str(e):
|
||||
return Response(
|
||||
{"name": "The state name is already taken"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
def list(self, request, slug, project_id):
|
||||
|
||||
@@ -24,6 +24,7 @@ from plane.db.models import (
|
||||
ProjectMember,
|
||||
Project,
|
||||
CycleIssue,
|
||||
UserRecentVisit,
|
||||
)
|
||||
from plane.utils.grouper import (
|
||||
issue_group_values,
|
||||
@@ -495,6 +496,13 @@ class IssueViewViewSet(BaseViewSet):
|
||||
entity_identifier=pk,
|
||||
entity_type="view",
|
||||
).delete()
|
||||
# Delete the page from recent visit
|
||||
UserRecentVisit.objects.filter(
|
||||
project_id=project_id,
|
||||
workspace__slug=slug,
|
||||
entity_identifier=pk,
|
||||
entity_name="view",
|
||||
).delete(soft=False)
|
||||
else:
|
||||
return Response(
|
||||
{"error": "Only admin or owner can delete the view"},
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
# Module imports
|
||||
from ..base import BaseAPIView
|
||||
from plane.db.models.workspace import WorkspaceUserPreference
|
||||
from plane.app.serializers.workspace import WorkspaceUserPreferenceSerializer
|
||||
from plane.app.permissions import allow_permission, ROLE
|
||||
from plane.db.models import Workspace
|
||||
|
||||
|
||||
# Third party imports
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
|
||||
|
||||
class WorkspaceUserPreferenceViewSet(BaseAPIView):
|
||||
model = WorkspaceUserPreference
|
||||
|
||||
def get_serializer_class(self):
|
||||
return WorkspaceUserPreferenceSerializer
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
|
||||
def get(self, request, slug):
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
|
||||
get_preference = WorkspaceUserPreference.objects.filter(
|
||||
user=request.user, workspace_id=workspace.id
|
||||
)
|
||||
|
||||
create_preference_keys = []
|
||||
|
||||
keys = [
|
||||
key
|
||||
for key, _ in WorkspaceUserPreference.UserPreferenceKeys.choices
|
||||
if key not in ["projects"]
|
||||
]
|
||||
|
||||
for preference in keys:
|
||||
if preference not in get_preference.values_list("key", flat=True):
|
||||
create_preference_keys.append(preference)
|
||||
|
||||
preference = WorkspaceUserPreference.objects.bulk_create(
|
||||
[
|
||||
WorkspaceUserPreference(
|
||||
key=key, user=request.user, workspace=workspace
|
||||
)
|
||||
for key in create_preference_keys
|
||||
],
|
||||
batch_size=10,
|
||||
ignore_conflicts=True,
|
||||
)
|
||||
|
||||
preference = WorkspaceUserPreference.objects.filter(
|
||||
user=request.user, workspace_id=workspace.id
|
||||
)
|
||||
|
||||
return Response(
|
||||
preference.values("key", "is_pinned", "sort_order"),
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
|
||||
def patch(self, request, slug, key):
|
||||
preference = WorkspaceUserPreference.objects.filter(
|
||||
key=key, workspace__slug=slug, user=request.user
|
||||
).first()
|
||||
|
||||
if preference:
|
||||
serializer = WorkspaceUserPreferenceSerializer(
|
||||
preference, data=request.data, partial=True
|
||||
)
|
||||
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
return Response(
|
||||
{"detail": "Preference not found"}, status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
@@ -53,7 +53,6 @@ urlpatterns = [
|
||||
path("magic-generate/", MagicGenerateEndpoint.as_view(), name="magic-generate"),
|
||||
path("magic-sign-in/", MagicSignInEndpoint.as_view(), name="magic-sign-in"),
|
||||
path("magic-sign-up/", MagicSignUpEndpoint.as_view(), name="magic-sign-up"),
|
||||
path("get-csrf-token/", CSRFTokenEndpoint.as_view(), name="get_csrf_token"),
|
||||
path(
|
||||
"spaces/magic-generate/",
|
||||
MagicGenerateSpaceEndpoint.as_view(),
|
||||
|
||||
@@ -69,7 +69,8 @@ from .workspace import (
|
||||
WorkspaceTheme,
|
||||
WorkspaceUserProperties,
|
||||
WorkspaceUserLink,
|
||||
WorkspaceHomePreference
|
||||
WorkspaceHomePreference,
|
||||
WorkspaceUserPreference,
|
||||
)
|
||||
|
||||
from .favorite import UserFavorite
|
||||
|
||||
@@ -388,15 +388,14 @@ class WorkspaceHomePreference(BaseModel):
|
||||
return f"{self.workspace.name} {self.user.email} {self.key}"
|
||||
|
||||
|
||||
|
||||
class WorkspaceUserPreference(BaseModel):
|
||||
"""Preference for the workspace for a user"""
|
||||
|
||||
class UserPreferenceKeys(models.TextChoices):
|
||||
PROJECTS = "projects", "Projects"
|
||||
ANALYTICS = "analytics", "Analytics"
|
||||
CYCLES = "cycles", "Cycles"
|
||||
VIEWS = "views", "Views"
|
||||
ANALYTICS = "analytics", "Analytics"
|
||||
PROJECTS = "projects", "Projects"
|
||||
|
||||
workspace = models.ForeignKey(
|
||||
"db.Workspace",
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
# Python imports
|
||||
import pytz
|
||||
from plane.db.models import Project
|
||||
from datetime import datetime, time
|
||||
from datetime import timedelta
|
||||
|
||||
# Django imports
|
||||
from django.utils import timezone
|
||||
|
||||
# Module imports
|
||||
from plane.db.models import Project
|
||||
|
||||
|
||||
def user_timezone_converter(queryset, datetime_fields, user_timezone):
|
||||
# Create a timezone object for the user's timezone
|
||||
@@ -65,16 +71,27 @@ def convert_to_utc(
|
||||
if is_start_date:
|
||||
localized_datetime += timedelta(minutes=0, seconds=1)
|
||||
|
||||
# If it's start an end date are equal, add 23 hours, 59 minutes, and 59 seconds
|
||||
# to make it the end of the day
|
||||
if is_start_date_end_date_equal:
|
||||
localized_datetime += timedelta(hours=23, minutes=59, seconds=59)
|
||||
# Convert the localized datetime to UTC
|
||||
utc_datetime = localized_datetime.astimezone(pytz.utc)
|
||||
|
||||
# Convert the localized datetime to UTC
|
||||
utc_datetime = localized_datetime.astimezone(pytz.utc)
|
||||
current_datetime_in_project_tz = timezone.now().astimezone(local_tz)
|
||||
current_datetime_in_utc = current_datetime_in_project_tz.astimezone(pytz.utc)
|
||||
|
||||
# Return the UTC datetime for storage
|
||||
return utc_datetime
|
||||
if utc_datetime.date() == current_datetime_in_utc.date():
|
||||
return current_datetime_in_utc
|
||||
|
||||
return utc_datetime
|
||||
else:
|
||||
# If it's start an end date are equal, add 23 hours, 59 minutes, and 59 seconds
|
||||
# to make it the end of the day
|
||||
if is_start_date_end_date_equal:
|
||||
localized_datetime += timedelta(hours=23, minutes=59, seconds=59)
|
||||
|
||||
# Convert the localized datetime to UTC
|
||||
utc_datetime = localized_datetime.astimezone(pytz.utc)
|
||||
|
||||
# Return the UTC datetime for storage
|
||||
return utc_datetime
|
||||
|
||||
|
||||
def convert_utc_to_project_timezone(utc_datetime, project_id):
|
||||
|
||||
@@ -131,17 +131,18 @@ function updateEnvFile() {
|
||||
fi
|
||||
|
||||
if [ -f "$file" ]; then
|
||||
# check if key exists in the file
|
||||
grep -q "^$key=" "$file"
|
||||
if [ $? -ne 0 ]; then
|
||||
# Check if key exists in the file
|
||||
if ! grep -q "^$key=" "$file"; then
|
||||
echo "$key=$value" >> "$file"
|
||||
return
|
||||
else
|
||||
else
|
||||
# Escape special characters in value for sed
|
||||
value=$(printf '%s\n' "$value" | sed -e 's/[\/&]/\\&/g')
|
||||
|
||||
if [ "$OS_NAME" == "Darwin" ]; then
|
||||
value=$(echo "$value" | sed 's/|/\\|/g')
|
||||
sed -i '' "s|^$key=.*|$key=$value|g" "$file"
|
||||
sed -i '' "s|^$key=.*|$key=$value|" "$file"
|
||||
else
|
||||
sed -i "s/^$key=.*/$key=$value/g" "$file"
|
||||
sed -i "s|^$key=.*|$key=$value|" "$file"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
|
||||
@@ -16,6 +16,7 @@ const CollaborativeDocumentEditor = (props: ICollaborativeDocumentEditor) => {
|
||||
const {
|
||||
onTransaction,
|
||||
aiHandler,
|
||||
bubbleMenuEnabled = true,
|
||||
containerClassName,
|
||||
disabledExtensions,
|
||||
displayConfig = DEFAULT_DISPLAY_CONFIG,
|
||||
@@ -75,8 +76,9 @@ const CollaborativeDocumentEditor = (props: ICollaborativeDocumentEditor) => {
|
||||
|
||||
return (
|
||||
<PageRenderer
|
||||
displayConfig={displayConfig}
|
||||
aiHandler={aiHandler}
|
||||
bubbleMenuEnabled={bubbleMenuEnabled}
|
||||
displayConfig={displayConfig}
|
||||
editor={editor}
|
||||
editorContainerClassName={editorContainerClassNames}
|
||||
id={id}
|
||||
|
||||
@@ -15,12 +15,13 @@ import { Editor, ReactRenderer } from "@tiptap/react";
|
||||
// components
|
||||
import { EditorContainer, EditorContentWrapper } from "@/components/editors";
|
||||
import { LinkView, LinkViewProps } from "@/components/links";
|
||||
import { AIFeaturesMenu, BlockMenu } from "@/components/menus";
|
||||
import { AIFeaturesMenu, BlockMenu, EditorBubbleMenu } from "@/components/menus";
|
||||
// types
|
||||
import { TAIHandler, TDisplayConfig } from "@/types";
|
||||
|
||||
type IPageRenderer = {
|
||||
aiHandler?: TAIHandler;
|
||||
bubbleMenuEnabled: boolean;
|
||||
displayConfig: TDisplayConfig;
|
||||
editor: Editor;
|
||||
editorContainerClassName: string;
|
||||
@@ -29,7 +30,7 @@ type IPageRenderer = {
|
||||
};
|
||||
|
||||
export const PageRenderer = (props: IPageRenderer) => {
|
||||
const { aiHandler, displayConfig, editor, editorContainerClassName, id, tabIndex } = props;
|
||||
const { aiHandler, bubbleMenuEnabled, displayConfig, editor, editorContainerClassName, id, tabIndex } = props;
|
||||
// states
|
||||
const [linkViewProps, setLinkViewProps] = useState<LinkViewProps>();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
@@ -141,6 +142,7 @@ export const PageRenderer = (props: IPageRenderer) => {
|
||||
<EditorContentWrapper editor={editor} id={id} tabIndex={tabIndex} />
|
||||
{editor.isEditable && (
|
||||
<div>
|
||||
{bubbleMenuEnabled && <EditorBubbleMenu editor={editor} />}
|
||||
<BlockMenu editor={editor} />
|
||||
<AIFeaturesMenu menu={aiHandler?.menu} />
|
||||
</div>
|
||||
|
||||
@@ -69,6 +69,7 @@ const DocumentReadOnlyEditor = (props: IDocumentReadOnlyEditor) => {
|
||||
|
||||
return (
|
||||
<PageRenderer
|
||||
bubbleMenuEnabled={false}
|
||||
displayConfig={displayConfig}
|
||||
editor={editor}
|
||||
editorContainerClassName={editorContainerClassName}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Dispatch, FC, SetStateAction, useCallback, useEffect, useRef } from "react";
|
||||
import { Editor } from "@tiptap/core";
|
||||
import { Check, Link, Trash } from "lucide-react";
|
||||
import { Check, Link, Trash2 } from "lucide-react";
|
||||
import { Dispatch, FC, SetStateAction, useCallback, useRef, useState } from "react";
|
||||
// plane utils
|
||||
import { cn } from "@plane/utils";
|
||||
// helpers
|
||||
@@ -15,22 +15,26 @@ type Props = {
|
||||
|
||||
export const BubbleMenuLinkSelector: FC<Props> = (props) => {
|
||||
const { editor, isOpen, setIsOpen } = props;
|
||||
// states
|
||||
const [error, setError] = useState(false);
|
||||
// refs
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const onLinkSubmit = useCallback(() => {
|
||||
const handleLinkSubmit = useCallback(() => {
|
||||
const input = inputRef.current;
|
||||
const url = input?.value;
|
||||
if (url && isValidHttpUrl(url)) {
|
||||
if (!input) return;
|
||||
let url = input.value;
|
||||
if (!url) return;
|
||||
if (!url.startsWith("http")) url = `http://${url}`;
|
||||
if (isValidHttpUrl(url)) {
|
||||
setLinkEditor(editor, url);
|
||||
setIsOpen(false);
|
||||
setError(false);
|
||||
} else {
|
||||
setError(true);
|
||||
}
|
||||
}, [editor, inputRef, setIsOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
inputRef.current && inputRef.current?.focus();
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="relative h-full">
|
||||
<button
|
||||
@@ -47,52 +51,62 @@ export const BubbleMenuLinkSelector: FC<Props> = (props) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<span>Link</span>
|
||||
Link
|
||||
<Link className="flex-shrink-0 size-3" />
|
||||
</button>
|
||||
{isOpen && (
|
||||
<div
|
||||
className="dow-xl fixed top-full z-[99999] mt-1 flex w-60 overflow-hidden rounded border border-custom-border-300 bg-custom-background-100 animate-in fade-in slide-in-from-top-1"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
onLinkSubmit();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="url"
|
||||
placeholder="Paste a link"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
className="flex-1 border-r border-custom-border-300 bg-custom-background-100 p-1 text-sm outline-none placeholder:text-custom-text-400"
|
||||
defaultValue={editor.getAttributes("link").href || ""}
|
||||
/>
|
||||
{editor.getAttributes("link").href ? (
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center rounded-sm p-1 text-red-600 transition-all hover:bg-red-100 dark:hover:bg-red-800"
|
||||
onClick={(e) => {
|
||||
unsetLinkEditor(editor);
|
||||
setIsOpen(false);
|
||||
e.stopPropagation();
|
||||
<div className="fixed top-full z-[99999] mt-1 w-60 animate-in fade-in slide-in-from-top-1 rounded bg-custom-background-100 shadow-custom-shadow-rg">
|
||||
<div
|
||||
className={cn("flex rounded border border-custom-border-300 transition-colors", {
|
||||
"border-red-500": error,
|
||||
})}
|
||||
>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="url"
|
||||
placeholder="Enter or paste a link"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="flex-1 border-r border-custom-border-300 bg-custom-background-100 py-2 px-1.5 text-xs outline-none placeholder:text-custom-text-400 rounded"
|
||||
defaultValue={editor.getAttributes("link").href || ""}
|
||||
onKeyDown={(e) => {
|
||||
setError(false);
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
handleLinkSubmit();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Trash className="h-4 w-4" />
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
className="flex items-center rounded-sm p-1 text-custom-text-300 transition-all hover:bg-custom-background-90"
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
onLinkSubmit();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<Check className="h-4 w-4" />
|
||||
</button>
|
||||
onFocus={() => setError(false)}
|
||||
autoFocus
|
||||
/>
|
||||
{editor.getAttributes("link").href ? (
|
||||
<button
|
||||
type="button"
|
||||
className="grid place-items-center rounded-sm p-1 text-red-500 hover:bg-red-500/20 transition-all"
|
||||
onClick={(e) => {
|
||||
unsetLinkEditor(editor);
|
||||
setIsOpen(false);
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<Trash2 className="size-4" />
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="h-full aspect-square grid place-items-center p-1 rounded-sm text-custom-text-300 hover:bg-custom-background-80 transition-all"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleLinkSubmit();
|
||||
}}
|
||||
>
|
||||
<Check className="size-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{error && (
|
||||
<p className="text-xs text-red-500 my-1 px-2 pointer-events-none animate-in fade-in slide-in-from-top-0">
|
||||
Please enter a valid URL
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -93,6 +93,19 @@ export const CustomColorExtension = Mark.create({
|
||||
};
|
||||
},
|
||||
|
||||
addStorage() {
|
||||
return {
|
||||
markdown: {
|
||||
serialize: {
|
||||
open: "",
|
||||
close: "",
|
||||
mixable: true,
|
||||
expelEnclosingWhitespace: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
|
||||
@@ -134,10 +134,6 @@ const SideMenu = (options: SideMenuPluginProps) => {
|
||||
rect.left -= 8;
|
||||
}
|
||||
|
||||
if (node.parentElement?.matches("td") || node.parentElement?.matches("th")) {
|
||||
rect.left += 8;
|
||||
}
|
||||
|
||||
rect.width = options.dragHandleWidth;
|
||||
|
||||
if (!editorSideMenu) return;
|
||||
|
||||
@@ -39,7 +39,12 @@ export interface TableOptions {
|
||||
declare module "@tiptap/core" {
|
||||
interface Commands<ReturnType> {
|
||||
table: {
|
||||
insertTable: (options?: { rows?: number; cols?: number; withHeaderRow?: boolean }) => ReturnType;
|
||||
insertTable: (options?: {
|
||||
rows?: number;
|
||||
cols?: number;
|
||||
withHeaderRow?: boolean;
|
||||
columnWidth?: number;
|
||||
}) => ReturnType;
|
||||
addColumnBefore: () => ReturnType;
|
||||
addColumnAfter: () => ReturnType;
|
||||
deleteColumn: () => ReturnType;
|
||||
@@ -108,9 +113,9 @@ export const Table = Node.create({
|
||||
addCommands() {
|
||||
return {
|
||||
insertTable:
|
||||
({ rows = 3, cols = 3, withHeaderRow = false } = {}) =>
|
||||
({ rows = 3, cols = 3, withHeaderRow = false, columnWidth = 150 } = {}) =>
|
||||
({ tr, dispatch, editor }) => {
|
||||
const node = createTable(editor.schema, rows, cols, withHeaderRow);
|
||||
const node = createTable(editor.schema, rows, cols, withHeaderRow, undefined, columnWidth);
|
||||
if (dispatch) {
|
||||
const offset = tr.selection.anchor + 1;
|
||||
|
||||
|
||||
@@ -2,11 +2,12 @@ import { Fragment, Node as ProsemirrorNode, NodeType } from "@tiptap/pm/model";
|
||||
|
||||
export function createCell(
|
||||
cellType: NodeType,
|
||||
cellContent?: Fragment | ProsemirrorNode | Array<ProsemirrorNode>
|
||||
cellContent?: Fragment | ProsemirrorNode | Array<ProsemirrorNode>,
|
||||
attrs?: Record<string, any>
|
||||
): ProsemirrorNode | null | undefined {
|
||||
if (cellContent) {
|
||||
return cellType.createChecked(null, cellContent);
|
||||
return cellType.createChecked(attrs, cellContent);
|
||||
}
|
||||
|
||||
return cellType.createAndFill();
|
||||
return cellType.createAndFill(attrs);
|
||||
}
|
||||
|
||||
@@ -8,21 +8,22 @@ export function createTable(
|
||||
rowsCount: number,
|
||||
colsCount: number,
|
||||
withHeaderRow: boolean,
|
||||
cellContent?: Fragment | ProsemirrorNode | Array<ProsemirrorNode>
|
||||
cellContent?: Fragment | ProsemirrorNode | Array<ProsemirrorNode>,
|
||||
columnWidth: number = 100
|
||||
): ProsemirrorNode {
|
||||
const types = getTableNodeTypes(schema);
|
||||
const headerCells: ProsemirrorNode[] = [];
|
||||
const cells: ProsemirrorNode[] = [];
|
||||
|
||||
for (let index = 0; index < colsCount; index += 1) {
|
||||
const cell = createCell(types.cell, cellContent);
|
||||
const cell = createCell(types.cell, cellContent, { colwidth: [columnWidth] });
|
||||
|
||||
if (cell) {
|
||||
cells.push(cell);
|
||||
}
|
||||
|
||||
if (withHeaderRow) {
|
||||
const headerCell = createCell(types.header_cell, cellContent);
|
||||
const headerCell = createCell(types.header_cell, cellContent, { colwidth: [columnWidth] });
|
||||
|
||||
if (headerCell) {
|
||||
headerCells.push(headerCell);
|
||||
|
||||
@@ -138,8 +138,9 @@ export const insertTableCommand = (editor: Editor, range?: Range) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (range) editor.chain().focus().deleteRange(range).clearNodes().insertTable({ rows: 3, cols: 3 }).run();
|
||||
else editor.chain().focus().clearNodes().insertTable({ rows: 3, cols: 3 }).run();
|
||||
if (range)
|
||||
editor.chain().focus().deleteRange(range).clearNodes().insertTable({ rows: 3, cols: 3, columnWidth: 150 }).run();
|
||||
else editor.chain().focus().clearNodes().insertTable({ rows: 3, cols: 3, columnWidth: 150 }).run();
|
||||
};
|
||||
|
||||
export const insertImage = ({
|
||||
|
||||
@@ -88,16 +88,18 @@ export const nodeDOMAtCoords = (coords: { x: number; y: number }) => {
|
||||
const elements = document.elementsFromPoint(coords.x, coords.y);
|
||||
|
||||
for (const elem of elements) {
|
||||
// Check for table wrapper first
|
||||
if (elem.matches(".table-wrapper")) {
|
||||
return elem;
|
||||
}
|
||||
|
||||
if (elem.matches("p:first-child") && elem.parentElement?.matches(".ProseMirror")) {
|
||||
return elem;
|
||||
}
|
||||
|
||||
// if the element is a <p> tag that is the first child of a td or th
|
||||
if (
|
||||
(elem.matches("td > p:first-child") || elem.matches("th > p:first-child")) &&
|
||||
elem?.textContent?.trim() !== ""
|
||||
) {
|
||||
return elem; // Return only if p tag is not empty in td or th
|
||||
// Skip table cells
|
||||
if (elem.closest(".table-wrapper")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// apply general selector
|
||||
|
||||
@@ -138,8 +138,9 @@ export interface IRichTextEditor extends IEditorProps {
|
||||
|
||||
export interface ICollaborativeDocumentEditor
|
||||
extends Omit<IEditorProps, "initialValue" | "onChange" | "onEnterKeyPress" | "value"> {
|
||||
editable: boolean;
|
||||
aiHandler?: TAIHandler;
|
||||
bubbleMenuEnabled?: boolean;
|
||||
editable: boolean;
|
||||
embedHandler: TEmbedConfig;
|
||||
handleEditorReady?: (value: boolean) => void;
|
||||
id: string;
|
||||
|
||||
@@ -105,14 +105,14 @@ ul[data-type="taskList"] li > div {
|
||||
}
|
||||
|
||||
ul[data-type="taskList"] li > label input[type="checkbox"] {
|
||||
border: 1px solid rgba(var(--color-border-300)) !important;
|
||||
border: 1px solid rgba(var(--color-text-100), 0.2) !important;
|
||||
outline: none;
|
||||
border-radius: 2px;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.ProseMirror[contenteditable="true"] input[type="checkbox"]:hover {
|
||||
background-color: rgba(var(--color-background-80));
|
||||
background-color: rgba(var(--color-text-100), 0.1);
|
||||
}
|
||||
|
||||
.ProseMirror[contenteditable="false"] input[type="checkbox"] {
|
||||
@@ -408,12 +408,14 @@ p.editor-paragraph-block {
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
&:not(td p.editor-paragraph-block, th p.editor-paragraph-block) {
|
||||
&:last-child {
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
&:not(:last-child) {
|
||||
padding-bottom: 8px;
|
||||
&:not(:last-child) {
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
font-size: var(--font-size-regular);
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
.table-wrapper table th {
|
||||
min-width: 1em;
|
||||
border: 1px solid rgba(var(--color-border-200));
|
||||
padding: 10px 20px;
|
||||
padding: 7px 10px;
|
||||
vertical-align: top;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
@@ -48,7 +48,7 @@
|
||||
/* table dropdown */
|
||||
.table-wrapper table .column-resize-handle {
|
||||
position: absolute;
|
||||
right: -2px;
|
||||
right: 0;
|
||||
top: 0;
|
||||
width: 2px;
|
||||
height: 100%;
|
||||
|
||||
Vendored
+4
-4
@@ -1,7 +1,7 @@
|
||||
import { TLogoProps } from "./common";
|
||||
import { TIssuePriorities } from "./issues";
|
||||
|
||||
export type TRecentActivityFilterKeys = "all item" | "issue" | "page" | "project";
|
||||
export type TRecentActivityFilterKeys = "all item" | "issue" | "page" | "project" | "workspace_page";
|
||||
export type THomeWidgetKeys = "quick_links" | "recents" | "my_stickies" | "quick_tutorial" | "new_at_plane";
|
||||
|
||||
export type THomeWidgetProps = {
|
||||
@@ -12,9 +12,9 @@ export type TPageEntityData = {
|
||||
id: string;
|
||||
name: string;
|
||||
logo_props: TLogoProps;
|
||||
project_id: string;
|
||||
project_id?: string;
|
||||
owned_by: string;
|
||||
project_identifier: string;
|
||||
project_identifier?: string;
|
||||
};
|
||||
|
||||
export type TProjectEntityData = {
|
||||
@@ -39,7 +39,7 @@ export type TIssueEntityData = {
|
||||
|
||||
export type TActivityEntityData = {
|
||||
id: string;
|
||||
entity_name: "page" | "project" | "issue";
|
||||
entity_name: "page" | "project" | "issue" | "workspace_page";
|
||||
entity_identifier: string;
|
||||
visited_at: string;
|
||||
entity_data: TPageEntityData | TProjectEntityData | TIssueEntityData;
|
||||
|
||||
@@ -300,6 +300,53 @@
|
||||
--color-sidebar-border-300: var(--color-border-300); /* strong sidebar border- 1 */
|
||||
--color-sidebar-border-400: var(--color-border-400); /* strong sidebar border- 2 */
|
||||
}
|
||||
|
||||
/* stickies and editor colors */
|
||||
:root {
|
||||
/* text colors */
|
||||
--editor-colors-gray-text: #5c5e63;
|
||||
--editor-colors-peach-text: #ff5b59;
|
||||
--editor-colors-pink-text: #f65385;
|
||||
--editor-colors-orange-text: #fd9038;
|
||||
--editor-colors-green-text: #0fc27b;
|
||||
--editor-colors-light-blue-text: #17bee9;
|
||||
--editor-colors-dark-blue-text: #266df0;
|
||||
--editor-colors-purple-text: #9162f9;
|
||||
/* end text colors */
|
||||
|
||||
/* background colors */
|
||||
--editor-colors-gray-background: #d6d6d8;
|
||||
--editor-colors-peach-background: #ffd5d7;
|
||||
--editor-colors-pink-background: #fdd4e3;
|
||||
--editor-colors-orange-background: #ffe3cd;
|
||||
--editor-colors-green-background: #c3f0de;
|
||||
--editor-colors-light-blue-background: #c5eff9;
|
||||
--editor-colors-dark-blue-background: #c9dafb;
|
||||
--editor-colors-purple-background: #e3d8fd;
|
||||
/* end background colors */
|
||||
}
|
||||
/* background colors */
|
||||
[data-theme*="light"] {
|
||||
--editor-colors-gray-background: #d6d6d8;
|
||||
--editor-colors-peach-background: #ffd5d7;
|
||||
--editor-colors-pink-background: #fdd4e3;
|
||||
--editor-colors-orange-background: #ffe3cd;
|
||||
--editor-colors-green-background: #c3f0de;
|
||||
--editor-colors-light-blue-background: #c5eff9;
|
||||
--editor-colors-dark-blue-background: #c9dafb;
|
||||
--editor-colors-purple-background: #e3d8fd;
|
||||
}
|
||||
[data-theme*="dark"] {
|
||||
--editor-colors-gray-background: #404144;
|
||||
--editor-colors-peach-background: #593032;
|
||||
--editor-colors-pink-background: #562e3d;
|
||||
--editor-colors-orange-background: #583e2a;
|
||||
--editor-colors-green-background: #1d4a3b;
|
||||
--editor-colors-light-blue-background: #1f495c;
|
||||
--editor-colors-dark-blue-background: #223558;
|
||||
--editor-colors-purple-background: #3d325a;
|
||||
}
|
||||
/* end background colors */
|
||||
}
|
||||
|
||||
* {
|
||||
@@ -355,7 +402,6 @@ body {
|
||||
-webkit-background-clip: text;
|
||||
}
|
||||
|
||||
|
||||
@-moz-document url-prefix() {
|
||||
* {
|
||||
scrollbar-width: none;
|
||||
|
||||
@@ -241,8 +241,8 @@ export const CustomTreeMapContent: React.FC<any> = ({
|
||||
y={pY + pHeight - LAYOUT.TEXT.PADDING_LEFT}
|
||||
textAnchor="start"
|
||||
className={cn(
|
||||
"text-xs font-extralight tracking-wider select-none",
|
||||
textClassName || "text-custom-text-400"
|
||||
"text-sm font-extralight tracking-wider select-none",
|
||||
textClassName || "text-custom-text-300"
|
||||
)}
|
||||
fill="currentColor"
|
||||
>
|
||||
@@ -252,8 +252,8 @@ export const CustomTreeMapContent: React.FC<any> = ({
|
||||
{bottom.labelTruncated
|
||||
? truncateText(
|
||||
label,
|
||||
availableTextWidth - calculateContentWidth(value, LAYOUT.TEXT.FONT_SIZES.XS) - 4,
|
||||
LAYOUT.TEXT.FONT_SIZES.XS
|
||||
availableTextWidth - calculateContentWidth(value, LAYOUT.TEXT.FONT_SIZES.SM) - 4,
|
||||
LAYOUT.TEXT.FONT_SIZES.SM
|
||||
)
|
||||
: label}
|
||||
</tspan>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
import { Fragment } from "react";
|
||||
|
||||
import { DayPicker } from "react-day-picker";
|
||||
import { DayPicker, getDefaultClassNames } from "react-day-picker";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
|
||||
import { X } from "lucide-react";
|
||||
@@ -31,6 +31,8 @@ const defaultValues: TFormValues = {
|
||||
date2: new Date(new Date().getFullYear(), new Date().getMonth() + 1, new Date().getDate()),
|
||||
};
|
||||
|
||||
const defaultClassNames = getDefaultClassNames();
|
||||
|
||||
export const DateFilterModal: React.FC<Props> = ({ title, handleClose, isOpen, onSelect }) => {
|
||||
const { handleSubmit, watch, control } = useForm<TFormValues>({
|
||||
defaultValues,
|
||||
@@ -97,6 +99,10 @@ export const DateFilterModal: React.FC<Props> = ({ title, handleClose, isOpen, o
|
||||
const date2Value = getDate(watch("date2"));
|
||||
return (
|
||||
<DayPicker
|
||||
classNames={{
|
||||
root: `${defaultClassNames.root} border border-custom-border-200 p-3 rounded-md`,
|
||||
}}
|
||||
captionLayout="dropdown"
|
||||
selected={dateValue}
|
||||
defaultMonth={dateValue}
|
||||
onSelect={(date) => {
|
||||
@@ -105,7 +111,6 @@ export const DateFilterModal: React.FC<Props> = ({ title, handleClose, isOpen, o
|
||||
}}
|
||||
mode="single"
|
||||
disabled={date2Value ? [{ after: date2Value }] : undefined}
|
||||
className="border border-custom-border-200 p-3 rounded-md"
|
||||
/>
|
||||
);
|
||||
}}
|
||||
@@ -119,6 +124,10 @@ export const DateFilterModal: React.FC<Props> = ({ title, handleClose, isOpen, o
|
||||
const date1Value = getDate(watch("date1"));
|
||||
return (
|
||||
<DayPicker
|
||||
classNames={{
|
||||
root: `${defaultClassNames.root} border border-custom-border-200 p-3 rounded-md`,
|
||||
}}
|
||||
captionLayout="dropdown"
|
||||
selected={dateValue}
|
||||
defaultMonth={dateValue}
|
||||
onSelect={(date) => {
|
||||
@@ -127,7 +136,6 @@ export const DateFilterModal: React.FC<Props> = ({ title, handleClose, isOpen, o
|
||||
}}
|
||||
mode="single"
|
||||
disabled={date1Value ? [{ before: date1Value }] : undefined}
|
||||
className="border border-custom-border-200 p-3 rounded-md"
|
||||
/>
|
||||
);
|
||||
}}
|
||||
|
||||
@@ -21,8 +21,8 @@ export const SingleProgressStats: React.FC<TSingleProgressStatsProps> = ({
|
||||
} ${selected ? "bg-custom-background-90" : ""}`}
|
||||
onClick={onClick}
|
||||
>
|
||||
<div className="w-1/2">{title}</div>
|
||||
<div className="flex w-1/2 items-center justify-end gap-1 px-2">
|
||||
<div className="w-4/6">{title}</div>
|
||||
<div className="flex w-2/6 items-center justify-end gap-1 px-2">
|
||||
<div className="flex h-5 items-center justify-center gap-1">
|
||||
<span className="w-8 text-right">
|
||||
{isNaN(Math.round((completed / total) * 100)) ? "0" : Math.round((completed / total) * 100)}%
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { Placement } from "@popperjs/core";
|
||||
import { DateRange, DayPicker, Matcher } from "react-day-picker";
|
||||
import { DateRange, DayPicker, Matcher, getDefaultClassNames } from "react-day-picker";
|
||||
import { usePopper } from "react-popper";
|
||||
import { ArrowRight, CalendarCheck2, CalendarDays } from "lucide-react";
|
||||
import { Combobox } from "@headlessui/react";
|
||||
@@ -52,6 +52,8 @@ type Props = {
|
||||
renderPlaceholder?: boolean;
|
||||
};
|
||||
|
||||
const defaultClassNames = getDefaultClassNames();
|
||||
|
||||
export const DateRangeDropdown: React.FC<Props> = (props) => {
|
||||
const {
|
||||
applyButtonText = "Apply changes",
|
||||
@@ -198,12 +200,14 @@ export const DateRangeDropdown: React.FC<Props> = (props) => {
|
||||
{isOpen && (
|
||||
<Combobox.Options className="fixed z-10" static>
|
||||
<div
|
||||
className="my-1 bg-custom-background-100 shadow-custom-shadow-rg rounded-md overflow-hidden p-3"
|
||||
className="my-1 bg-custom-background-100 shadow-custom-shadow-rg overflow-hidden"
|
||||
ref={setPopperElement}
|
||||
style={styles.popper}
|
||||
{...attributes.popper}
|
||||
>
|
||||
<DayPicker
|
||||
captionLayout="dropdown"
|
||||
classNames={{ root: `${defaultClassNames.root} p-3 rounded-md` }}
|
||||
selected={dateRange}
|
||||
onSelect={(val) => {
|
||||
// if both the dates are not required, immediately call onSelect
|
||||
@@ -216,7 +220,8 @@ export const DateRangeDropdown: React.FC<Props> = (props) => {
|
||||
mode="range"
|
||||
disabled={disabledDays}
|
||||
showOutsideDays
|
||||
initialFocus
|
||||
autoFocus
|
||||
fixedWeeks
|
||||
footer={
|
||||
bothRequired && (
|
||||
<div className="grid grid-cols-2 items-center gap-3.5 pt-6 relative">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useRef, useState } from "react";
|
||||
import { DayPicker, Matcher } from "react-day-picker";
|
||||
import { DayPicker, Matcher, getDefaultClassNames } from "react-day-picker";
|
||||
import { createPortal } from "react-dom";
|
||||
import { usePopper } from "react-popper";
|
||||
import { CalendarDays, X } from "lucide-react";
|
||||
@@ -33,6 +33,8 @@ type Props = TDropdownProps & {
|
||||
renderByDefault?: boolean;
|
||||
};
|
||||
|
||||
const defaultClassNames = getDefaultClassNames();
|
||||
|
||||
export const DateDropdown: React.FC<Props> = (props) => {
|
||||
const {
|
||||
buttonClassName = "",
|
||||
@@ -166,7 +168,7 @@ export const DateDropdown: React.FC<Props> = (props) => {
|
||||
<Combobox.Options data-prevent-outside-click static>
|
||||
<div
|
||||
className={cn(
|
||||
"my-1 bg-custom-background-100 shadow-custom-shadow-rg rounded-md overflow-hidden p-3 z-20",
|
||||
"my-1 bg-custom-background-100 shadow-custom-shadow-rg overflow-hidden z-20",
|
||||
optionsClassName
|
||||
)}
|
||||
ref={setPopperElement}
|
||||
@@ -174,15 +176,18 @@ export const DateDropdown: React.FC<Props> = (props) => {
|
||||
{...attributes.popper}
|
||||
>
|
||||
<DayPicker
|
||||
captionLayout="dropdown"
|
||||
classNames={{ root: `${defaultClassNames.root} p-3 rounded-md` }}
|
||||
selected={getDate(value)}
|
||||
defaultMonth={getDate(value)}
|
||||
onSelect={(date) => {
|
||||
dropdownOnChange(date ?? null);
|
||||
}}
|
||||
showOutsideDays
|
||||
initialFocus
|
||||
autoFocus
|
||||
disabled={disabledDays}
|
||||
mode="single"
|
||||
fixedWeeks
|
||||
/>
|
||||
</div>
|
||||
</Combobox.Options>,
|
||||
|
||||
@@ -57,7 +57,6 @@ export const IssueCommentToolbar: React.FC<Props> = (props) => {
|
||||
showSubmitButton,
|
||||
editorRef,
|
||||
} = props;
|
||||
|
||||
// State to manage active states of toolbar items
|
||||
const [activeStates, setActiveStates] = useState<Record<string, boolean>>({});
|
||||
|
||||
@@ -86,6 +85,9 @@ export const IssueCommentToolbar: React.FC<Props> = (props) => {
|
||||
return () => unsubscribe();
|
||||
}, [editorRef, updateActiveStates]);
|
||||
|
||||
const isEditorReadyToDiscard = editorRef?.isEditorReadyToDiscard();
|
||||
const isSubmitButtonDisabled = isCommentEmpty || !isEditorReadyToDiscard;
|
||||
|
||||
return (
|
||||
<div className="flex h-9 w-full items-stretch gap-1.5 bg-custom-background-90 overflow-x-scroll">
|
||||
{showAccessSpecifier && (
|
||||
@@ -166,7 +168,7 @@ export const IssueCommentToolbar: React.FC<Props> = (props) => {
|
||||
variant="primary"
|
||||
className="px-2.5 py-1.5 text-xs"
|
||||
onClick={handleSubmit}
|
||||
disabled={isCommentEmpty}
|
||||
disabled={isSubmitButtonDisabled}
|
||||
loading={isSubmitting}
|
||||
>
|
||||
Comment
|
||||
|
||||
@@ -3,16 +3,15 @@ import React, { useState } from "react";
|
||||
import { EIssueCommentAccessSpecifier } from "@plane/constants";
|
||||
// plane editor
|
||||
import { EditorRefApi, ILiteTextEditor, LiteTextEditorWithRef } from "@plane/editor";
|
||||
// components
|
||||
// plane types
|
||||
import { TSticky } from "@plane/types";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { getEditorFileHandlers } from "@/helpers/editor.helper";
|
||||
// hooks
|
||||
// plane web hooks
|
||||
import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging";
|
||||
import { useFileSize } from "@/plane-web/hooks/use-file-size";
|
||||
import { Toolbar } from "./toolbar";
|
||||
import { StickyEditorToolbar } from "./toolbar";
|
||||
|
||||
interface StickyEditorWrapperProps
|
||||
extends Omit<ILiteTextEditor, "disabledExtensions" | "fileHandler" | "mentionHandler"> {
|
||||
@@ -60,7 +59,7 @@ export const StickyEditor = React.forwardRef<EditorRefApi, StickyEditorWrapperPr
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("relative border border-custom-border-200 rounded p-3", parentClassName)}
|
||||
className={cn("relative border border-custom-border-200 rounded", parentClassName)}
|
||||
onFocus={() => !showToolbarInitially && setIsFocused(true)}
|
||||
onBlur={() => !showToolbarInitially && setIsFocused(false)}
|
||||
>
|
||||
@@ -82,12 +81,12 @@ export const StickyEditor = React.forwardRef<EditorRefApi, StickyEditorWrapperPr
|
||||
/>
|
||||
{showToolbar && (
|
||||
<div
|
||||
className={cn(
|
||||
"transition-all duration-300 ease-out origin-top",
|
||||
isFocused ? "max-h-[200px] opacity-100 scale-y-100 mt-3" : "max-h-0 opacity-0 scale-y-0 invisible"
|
||||
)}
|
||||
className={cn("transition-all duration-300 ease-out origin-top px-4 h-[60px]", {
|
||||
"max-h-[60px] opacity-100 scale-y-100": isFocused,
|
||||
"max-h-0 opacity-0 scale-y-0 invisible": !isFocused,
|
||||
})}
|
||||
>
|
||||
<Toolbar
|
||||
<StickyEditorToolbar
|
||||
executeCommand={(item) => {
|
||||
// TODO: update this while toolbar homogenization
|
||||
// @ts-expect-error type mismatch here
|
||||
|
||||
@@ -23,7 +23,7 @@ type Props = {
|
||||
|
||||
const toolbarItems = TOOLBAR_ITEMS.sticky;
|
||||
|
||||
export const Toolbar: React.FC<Props> = (props) => {
|
||||
export const StickyEditorToolbar: React.FC<Props> = (props) => {
|
||||
const { executeCommand, editorRef, handleColorChange, handleDelete } = props;
|
||||
|
||||
// State to manage active states of toolbar items
|
||||
@@ -58,7 +58,7 @@ export const Toolbar: React.FC<Props> = (props) => {
|
||||
useOutsideClickDetector(colorPaletteRef, () => setShowColorPalette(false));
|
||||
|
||||
return (
|
||||
<div className="flex w-full justify-between mt-2 h-full">
|
||||
<div className="flex w-full justify-between h-full">
|
||||
<div className="flex my-auto gap-4" ref={colorPaletteRef}>
|
||||
{/* color palette */}
|
||||
{showColorPalette && <ColorPalette handleUpdate={handleColorChange} />}
|
||||
@@ -69,7 +69,11 @@ export const Toolbar: React.FC<Props> = (props) => {
|
||||
</p>
|
||||
}
|
||||
>
|
||||
<button onClick={() => setShowColorPalette(!showColorPalette)} className="flex text-custom-text-300">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowColorPalette(!showColorPalette)}
|
||||
className="flex text-custom-text-100/50"
|
||||
>
|
||||
<Palette className="size-4 my-auto" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
@@ -95,7 +99,7 @@ export const Toolbar: React.FC<Props> = (props) => {
|
||||
type="button"
|
||||
onClick={() => executeCommand(item)}
|
||||
className={cn(
|
||||
"grid place-items-center aspect-square rounded-sm p-0.5 text-custom-text-300",
|
||||
"grid place-items-center aspect-square rounded-sm p-0.5 text-custom-text-100/50",
|
||||
{}
|
||||
)}
|
||||
>
|
||||
@@ -122,7 +126,7 @@ export const Toolbar: React.FC<Props> = (props) => {
|
||||
</p>
|
||||
}
|
||||
>
|
||||
<button onClick={handleDelete} className="my-auto text-custom-text-300">
|
||||
<button type="button" onClick={handleDelete} className="my-auto text-custom-text-100/50">
|
||||
<Trash2 className="size-4" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
@@ -25,7 +25,7 @@ export const HOME_WIDGETS_LIST: {
|
||||
quick_links: {
|
||||
component: DashboardQuickLinks,
|
||||
fullWidth: false,
|
||||
title: "Quick links",
|
||||
title: "Quicklinks",
|
||||
},
|
||||
recents: {
|
||||
component: RecentActivityWidget,
|
||||
|
||||
@@ -1 +1,4 @@
|
||||
export * from "./root";
|
||||
export * from "./links";
|
||||
export * from "./no-projects";
|
||||
export * from "./recents";
|
||||
export * from "./stickies";
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { Link2 } from "lucide-react";
|
||||
|
||||
export const LinksEmptyState = () => (
|
||||
<div className="min-h-[110px] flex w-full justify-center py-6 bg-custom-border-100 rounded">
|
||||
<div className="m-auto flex gap-2">
|
||||
<Link2 size={30} className="text-custom-text-400/40 -rotate-45" />
|
||||
<div className="text-custom-text-400 text-sm text-center my-auto">
|
||||
Add any links you need for quick access to your work.
|
||||
</div>
|
||||
</div>
|
||||
<div className="min-h-[110px] w-full flex items-center justify-center gap-2 py-6 bg-custom-background-90 text-custom-text-400 rounded">
|
||||
<div className="flex-shrink-0 size-[30px] grid place-items-center">
|
||||
<Link2 className="size-6 -rotate-45" />
|
||||
</div>
|
||||
);
|
||||
<p className="text-sm text-center font-medium">Save links to work things that you{"'"}d like handy.</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
+33
-40
@@ -2,6 +2,8 @@ import React from "react";
|
||||
import Link from "next/link";
|
||||
import { useParams } from "next/navigation";
|
||||
import { Briefcase, Hotel, Users } from "lucide-react";
|
||||
// plane ui
|
||||
import { Avatar } from "@plane/ui";
|
||||
// helpers
|
||||
import { getFileURL } from "@/helpers/file.helper";
|
||||
// hooks
|
||||
@@ -9,7 +11,7 @@ import { useCommandPalette, useEventTracker, useUser, useUserPermissions } from
|
||||
// plane web constants
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants";
|
||||
|
||||
export const EmptyWorkspace = () => {
|
||||
export const NoProjectsEmptyState = () => {
|
||||
// navigation
|
||||
const { workspaceSlug } = useParams();
|
||||
// store hooks
|
||||
@@ -26,11 +28,11 @@ export const EmptyWorkspace = () => {
|
||||
const EMPTY_STATE_DATA = [
|
||||
{
|
||||
id: "create-project",
|
||||
title: "Create a project",
|
||||
description: "Create your first project now to get started",
|
||||
icon: <Briefcase className="w-[40px] h-[40px] text-custom-primary-100" />,
|
||||
title: "Create a project.",
|
||||
description: "Most things start with a project in Plane.",
|
||||
icon: <Briefcase className="size-12 text-custom-primary-100" />,
|
||||
cta: {
|
||||
text: "Create Project",
|
||||
text: "Get started",
|
||||
onClick: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
if (!canCreateProject) return;
|
||||
e.preventDefault();
|
||||
@@ -42,66 +44,56 @@ export const EmptyWorkspace = () => {
|
||||
},
|
||||
{
|
||||
id: "invite-team",
|
||||
title: "Invite your team",
|
||||
description: "The sub text will be of two lines and that will be placed.",
|
||||
icon: <Users className="w-[40px] h-[40px] text-custom-primary-100" />,
|
||||
title: "Invite your team.",
|
||||
description: "Build, ship, and manage with coworkers.",
|
||||
icon: <Users className="size-12 text-custom-primary-100" />,
|
||||
cta: {
|
||||
text: "Invite now",
|
||||
text: "Get them in",
|
||||
link: `/${workspaceSlug}/settings/members`,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "configure-workspace",
|
||||
title: "Configure your workspace",
|
||||
description: "The sub text will be of two lines and that will be placed.",
|
||||
icon: <Hotel className="w-[40px] h-[40px] text-custom-primary-100" />,
|
||||
title: "Set up your workspace.",
|
||||
description: "Turn features on or off or go beyond that.",
|
||||
icon: <Hotel className="size-12 text-custom-primary-100" />,
|
||||
cta: {
|
||||
text: "Configure workspace",
|
||||
text: "Configure this workspace",
|
||||
link: "settings",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "personalize-account",
|
||||
title: "Personalize your account",
|
||||
description: "The sub text will be of two lines and that will be placed.",
|
||||
icon:
|
||||
currentUser?.avatar_url && currentUser?.avatar_url.trim() !== "" ? (
|
||||
<Link href={`/${workspaceSlug}/profile/${currentUser?.id}`}>
|
||||
<span className="relative flex h-6 w-6 items-center justify-center rounded-full p-4 capitalize text-white">
|
||||
<img
|
||||
src={getFileURL(currentUser?.avatar_url)}
|
||||
className="absolute left-0 top-0 h-full w-full rounded-full object-cover"
|
||||
alt={currentUser?.display_name || currentUser?.email}
|
||||
/>
|
||||
</span>
|
||||
</Link>
|
||||
) : (
|
||||
<Link href={`/${workspaceSlug}/profile/${currentUser?.id}`}>
|
||||
<span className="relative flex h-6 w-6 items-center justify-center rounded-full bg-gray-700 p-4 capitalize text-white text-sm">
|
||||
{(currentUser?.email ?? currentUser?.display_name ?? "?")[0]}
|
||||
</span>
|
||||
</Link>
|
||||
),
|
||||
title: "Make Plane yours.",
|
||||
description: "Choose your picture, colors, and more.",
|
||||
icon: (
|
||||
<Avatar
|
||||
src={getFileURL(currentUser?.avatar_url ?? "")}
|
||||
name={currentUser?.display_name}
|
||||
size={48}
|
||||
className="text-xl"
|
||||
showTooltip={false}
|
||||
/>
|
||||
),
|
||||
cta: {
|
||||
text: "Personalize account",
|
||||
text: "Personalize now",
|
||||
link: "/profile",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-4">
|
||||
{EMPTY_STATE_DATA.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="flex flex-col items-center justify-center py-8 bg-custom-background-100 rounded-lg text-center border border-custom-border-200/40"
|
||||
className="flex flex-col items-center justify-center p-6 bg-custom-background-100 rounded-lg text-center border border-custom-border-200/40"
|
||||
>
|
||||
<div className="flex items-center justify-center bg-custom-primary-100/10 rounded-full w-[80px] h-[80px] mb-4">
|
||||
<div className="grid place-items-center bg-custom-primary-100/10 rounded-full size-24 mb-3">
|
||||
<span className="text-3xl my-auto">{item.icon}</span>
|
||||
</div>
|
||||
<h3 className="text-lg font-medium text-custom-text-100 mb-2">{item.title}</h3>
|
||||
<p className="text-sm text-custom-text-200 mb-4 w-[80%] flex-1">{item.description}</p>
|
||||
|
||||
<h3 className="text-base font-medium text-custom-text-100 mb-2">{item.title}</h3>
|
||||
<p className="text-sm text-custom-text-300 mb-2">{item.description}</p>
|
||||
{item.cta.link ? (
|
||||
<Link
|
||||
href={item.cta.link}
|
||||
@@ -111,6 +103,7 @@ export const EmptyWorkspace = () => {
|
||||
</Link>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="text-custom-primary-100 hover:text-custom-primary-200 text-sm font-medium"
|
||||
onClick={item.cta.onClick}
|
||||
>
|
||||
@@ -1,38 +1,41 @@
|
||||
import { Briefcase, FileText, History } from "lucide-react";
|
||||
// plane ui
|
||||
import { LayersIcon } from "@plane/ui";
|
||||
|
||||
const getDisplayContent = (type: string) => {
|
||||
switch (type) {
|
||||
case "project":
|
||||
return {
|
||||
icon: Briefcase,
|
||||
text: "Projects you go into or have assigned work in will show up here.",
|
||||
};
|
||||
case "page":
|
||||
return {
|
||||
icon: FileText,
|
||||
text: "Create, see, or change something on pages you have access to and see them here.",
|
||||
};
|
||||
case "issue":
|
||||
return {
|
||||
icon: LayersIcon,
|
||||
text: "Let's see some issues to see them show up here.",
|
||||
};
|
||||
default:
|
||||
return {
|
||||
icon: History,
|
||||
text: "Whatever you see and act on in Plane will show up here.",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const RecentsEmptyState = ({ type }: { type: string }) => {
|
||||
const getDisplayContent = () => {
|
||||
switch (type) {
|
||||
case "project":
|
||||
return {
|
||||
icon: <Briefcase size={30} className="text-custom-text-400/40" />,
|
||||
text: "Your recent projects will appear here once you visit one.",
|
||||
};
|
||||
case "page":
|
||||
return {
|
||||
icon: <FileText size={30} className="text-custom-text-400/40" />,
|
||||
text: "Your recent pages will appear here once you visit one.",
|
||||
};
|
||||
case "issue":
|
||||
return {
|
||||
icon: <LayersIcon className="text-custom-text-400/40 w-[30px] h-[30px]" />,
|
||||
text: "Your recent issues will appear here once you visit one.",
|
||||
};
|
||||
default:
|
||||
return {
|
||||
icon: <History size={30} className="text-custom-text-400/40" />,
|
||||
text: "You don’t have any recent items yet.",
|
||||
};
|
||||
}
|
||||
};
|
||||
const { icon, text } = getDisplayContent();
|
||||
const displayContent = getDisplayContent(type);
|
||||
|
||||
return (
|
||||
<div className="min-h-[120px] flex w-full justify-center py-6 bg-custom-border-100 rounded">
|
||||
<div className="m-auto flex gap-2">
|
||||
{icon} <div className="text-custom-text-400 text-sm text-center my-auto">{text}</div>
|
||||
<div className="min-h-[110px] w-full flex items-center justify-center gap-2 py-6 bg-custom-background-90 text-custom-text-400 rounded">
|
||||
<div className="flex-shrink-0 size-[30px] grid place-items-center">
|
||||
<displayContent.icon className="size-6" />
|
||||
</div>
|
||||
<p className="text-sm text-center font-medium">{displayContent.text}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
import { RecentStickyIcon } from "@plane/ui";
|
||||
|
||||
export const StickiesEmptyState = () => (
|
||||
<div className="min-h-[110px] flex w-full justify-center py-6 bg-custom-border-100 rounded">
|
||||
<div className="m-auto flex gap-2">
|
||||
<RecentStickyIcon className="h-[30px] w-[30px] text-custom-text-400/40" />
|
||||
<div className="text-custom-text-400 text-sm text-center my-auto">
|
||||
No stickies yet. Add one to start making quick notes.
|
||||
</div>
|
||||
<div className="min-h-[110px] w-full flex items-center justify-center gap-2 py-6 bg-custom-background-90 text-custom-text-400 rounded">
|
||||
<div className="flex-shrink-0 size-[30px] grid place-items-center">
|
||||
<RecentStickyIcon className="size-6" />
|
||||
</div>
|
||||
<p className="text-sm text-center font-medium">
|
||||
Jot down an idea, capture an aha, or record a brainwave. Add a sticky to get started.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -14,7 +14,7 @@ export const AddLink = (props: TProps) => {
|
||||
<div className="rounded p-2 bg-custom-background-80/40 w-8 h-8 my-auto">
|
||||
<PlusIcon className="h-4 w-4 stroke-2 text-custom-text-350" />
|
||||
</div>
|
||||
<div className="text-sm font-medium my-auto">Add quick Link</div>
|
||||
<div className="text-sm font-medium my-auto">Add quicklink</div>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -66,9 +66,7 @@ export const LinkCreateUpdateModal: FC<TLinkCreateEditModal> = observer((props)
|
||||
<ModalCore isOpen={isModalOpen} handleClose={onClose}>
|
||||
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
||||
<div className="space-y-5 p-5">
|
||||
<h3 className="text-xl font-medium text-custom-text-200">
|
||||
{preloadedData?.id ? "Update" : "Add"} quick link
|
||||
</h3>
|
||||
<h3 className="text-xl font-medium text-custom-text-200">{preloadedData?.id ? "Update" : "Add"} quicklink</h3>
|
||||
<div className="mt-2 space-y-3">
|
||||
<div>
|
||||
<label htmlFor="url" className="mb-2 text-custom-text-200 text-base font-medium">
|
||||
@@ -124,7 +122,7 @@ export const LinkCreateUpdateModal: FC<TLinkCreateEditModal> = observer((props)
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="primary" size="sm" type="submit" loading={isSubmitting}>
|
||||
{preloadedData?.id ? (isSubmitting ? "Updating" : "Update") : isSubmitting ? "Adding" : "Add"} quick link
|
||||
{preloadedData?.id ? (isSubmitting ? "Updating" : "Update") : isSubmitting ? "Adding" : "Add"} quicklink
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
"use client";
|
||||
|
||||
import { FC } from "react";
|
||||
// hooks
|
||||
// ui
|
||||
import { observer } from "mobx-react";
|
||||
import { Pencil, Trash2, ExternalLink, EllipsisVertical, Link2, Link } from "lucide-react";
|
||||
import { Pencil, Trash2, ExternalLink, Link2, Link } from "lucide-react";
|
||||
// plane ui
|
||||
import { TOAST_TYPE, setToast, CustomMenu, TContextMenuItem } from "@plane/ui";
|
||||
// helpers
|
||||
// plane utils
|
||||
import { cn, copyTextToClipboard } from "@plane/utils";
|
||||
// helpers
|
||||
import { calculateTimeAgo } from "@/helpers/date-time.helper";
|
||||
// hooks
|
||||
import { useHome } from "@/hooks/store/use-home";
|
||||
// types
|
||||
import { TLinkOperations } from "./use-links";
|
||||
|
||||
export type TProjectLinkDetail = {
|
||||
@@ -75,54 +77,47 @@ export const ProjectLinkDetail: FC<TProjectLinkDetail> = observer((props) => {
|
||||
return (
|
||||
<div
|
||||
onClick={handleOpenInNewTab}
|
||||
className="cursor-pointer group btn btn-primary flex bg-custom-background-100 px-4 w-[230px] h-[56px] border-[0.5px] border-custom-border-200 rounded-md gap-4 hover:shadow-md"
|
||||
className="cursor-pointer group flex items-center bg-custom-background-100 px-4 w-[230px] h-[56px] border-[0.5px] border-custom-border-200 rounded-md gap-4 hover:shadow-md transition-shadow"
|
||||
>
|
||||
<div className="rounded p-2 bg-custom-background-80/40 w-8 h-8 my-auto">
|
||||
<Link2 className="h-4 w-4 stroke-2 text-custom-text-350 -rotate-45" />
|
||||
<div className="flex-shrink-0 size-8 rounded p-2 bg-custom-background-80 grid place-items-center">
|
||||
<Link2 className="size-4 stroke-2 text-custom-text-350 -rotate-45" />
|
||||
</div>
|
||||
<div className="my-auto flex-1">
|
||||
<div className="flex-1 truncate">
|
||||
<div className="text-sm font-medium truncate">{linkDetail.title || linkDetail.url}</div>
|
||||
<div className="text-xs font-medium text-custom-text-400">{calculateTimeAgo(linkDetail.created_at)}</div>
|
||||
</div>
|
||||
|
||||
<CustomMenu
|
||||
customButton={
|
||||
<EllipsisVertical className="opacity-0 h-4 w-4 stroke-2 text-custom-text-350 group-hover:opacity-100" />
|
||||
}
|
||||
placement="bottom-end"
|
||||
menuItemsClassName="z-20"
|
||||
closeOnSelect
|
||||
className=" my-auto"
|
||||
>
|
||||
{MENU_ITEMS.map((item) => (
|
||||
<CustomMenu.MenuItem
|
||||
key={item.key}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
item.action();
|
||||
}}
|
||||
className={cn("flex items-center gap-2 w-full ", {
|
||||
"text-custom-text-400": item.disabled,
|
||||
})}
|
||||
disabled={item.disabled}
|
||||
>
|
||||
{item.icon && <item.icon className={cn("h-3 w-3", item.iconClassName)} />}
|
||||
<div>
|
||||
<h5>{item.title}</h5>
|
||||
{item.description && (
|
||||
<p
|
||||
className={cn("text-custom-text-300 whitespace-pre-line", {
|
||||
"text-custom-text-400": item.disabled,
|
||||
})}
|
||||
>
|
||||
{item.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
))}
|
||||
</CustomMenu>
|
||||
<div className="hidden group-hover:block">
|
||||
<CustomMenu placement="bottom-end" menuItemsClassName="z-20" closeOnSelect verticalEllipsis>
|
||||
{MENU_ITEMS.map((item) => (
|
||||
<CustomMenu.MenuItem
|
||||
key={item.key}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
item.action();
|
||||
}}
|
||||
className={cn("flex items-center gap-2 w-full ", {
|
||||
"text-custom-text-400": item.disabled,
|
||||
})}
|
||||
disabled={item.disabled}
|
||||
>
|
||||
{item.icon && <item.icon className={cn("h-3 w-3", item.iconClassName)} />}
|
||||
<div>
|
||||
<h5>{item.title}</h5>
|
||||
{item.description && (
|
||||
<p
|
||||
className={cn("text-custom-text-300 whitespace-pre-line", {
|
||||
"text-custom-text-400": item.disabled,
|
||||
})}
|
||||
>
|
||||
{item.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
))}
|
||||
</CustomMenu>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -38,9 +38,9 @@ export const ProjectLinkList: FC<TProjectLinkList> = observer((props) => {
|
||||
buttonClassName="bg-custom-background-90/20"
|
||||
>
|
||||
<div className="flex gap-2 mb-2 flex-wrap flex-1">
|
||||
{links &&
|
||||
links.length > 0 &&
|
||||
links.map((linkId) => <ProjectLinkDetail key={linkId} linkId={linkId} linkOperations={linkOperations} />)}
|
||||
{links.map((linkId) => (
|
||||
<ProjectLinkDetail key={linkId} linkId={linkId} linkOperations={linkOperations} />
|
||||
))}
|
||||
</div>
|
||||
</ContentOverflowWrapper>
|
||||
</div>
|
||||
|
||||
@@ -34,14 +34,14 @@ export const DashboardQuickLinks = observer((props: THomeWidgetProps) => {
|
||||
/>
|
||||
<div className="mb-2">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="text-base font-semibold text-custom-text-350">Quick links</div>
|
||||
<div className="text-base font-semibold text-custom-text-350">Quicklinks</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
toggleLinkModal(true);
|
||||
}}
|
||||
className="flex gap-1 text-sm font-medium text-custom-primary-100 my-auto"
|
||||
>
|
||||
<Plus className="size-4 my-auto" /> <span>Add quick link</span>
|
||||
<Plus className="size-4 my-auto" /> <span>Add quicklink</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-wrap w-full">
|
||||
|
||||
@@ -2,17 +2,20 @@
|
||||
|
||||
import { useRef, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// types
|
||||
import { usePathname } from "next/navigation";
|
||||
import useSWR from "swr";
|
||||
import { Briefcase, FileText } from "lucide-react";
|
||||
// plane types
|
||||
import { TActivityEntityData, THomeWidgetProps, TRecentActivityFilterKeys } from "@plane/types";
|
||||
// components
|
||||
// plane ui
|
||||
import { LayersIcon } from "@plane/ui";
|
||||
// components
|
||||
import { ContentOverflowWrapper } from "@/components/core/content-overflow-HOC";
|
||||
// hooks
|
||||
import { useProject } from "@/hooks/store";
|
||||
// plane web services
|
||||
import { WorkspaceService } from "@/plane-web/services";
|
||||
import { EmptyWorkspace } from "../empty-states";
|
||||
import { RecentsEmptyState } from "../empty-states/recents";
|
||||
import { NoProjectsEmptyState, RecentsEmptyState } from "../empty-states";
|
||||
import { EWidgetKeys, WidgetLoader } from "../loaders";
|
||||
import { FiltersDropdown } from "./filters";
|
||||
import { RecentIssue } from "./issue";
|
||||
@@ -23,18 +26,28 @@ const WIDGET_KEY = EWidgetKeys.RECENT_ACTIVITY;
|
||||
const workspaceService = new WorkspaceService();
|
||||
const filters: { name: TRecentActivityFilterKeys; icon?: React.ReactNode }[] = [
|
||||
{ name: "all item" },
|
||||
{ name: "issue", icon: <LayersIcon className="w-4 h-4" /> },
|
||||
{ name: "page", icon: <FileText size={16} /> },
|
||||
{ name: "project", icon: <Briefcase size={16} /> },
|
||||
{ name: "issue", icon: <LayersIcon className="flex-shrink-0 size-4" /> },
|
||||
{ name: "page", icon: <FileText className="flex-shrink-0 size-4" /> },
|
||||
{ name: "project", icon: <Briefcase className="flex-shrink-0 size-4" /> },
|
||||
];
|
||||
|
||||
export const RecentActivityWidget: React.FC<THomeWidgetProps> = observer((props) => {
|
||||
const { workspaceSlug } = props;
|
||||
// state
|
||||
const [filter, setFilter] = useState<TRecentActivityFilterKeys>(filters[0].name);
|
||||
type TRecentWidgetProps = THomeWidgetProps & {
|
||||
presetFilter?: TRecentActivityFilterKeys;
|
||||
showFilterSelect?: boolean;
|
||||
};
|
||||
|
||||
export const RecentActivityWidget: React.FC<TRecentWidgetProps> = observer((props) => {
|
||||
const { presetFilter, showFilterSelect = true, workspaceSlug } = props;
|
||||
// states
|
||||
const [filter, setFilter] = useState<TRecentActivityFilterKeys>(presetFilter ?? filters[0].name);
|
||||
// navigation
|
||||
const pathname = usePathname();
|
||||
// ref
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
// store hooks
|
||||
const { joinedProjectIds, loader } = useProject();
|
||||
// derived values
|
||||
const isWikiApp = pathname.includes(`/${workspaceSlug.toString()}/pages`);
|
||||
|
||||
const { data: recents, isLoading } = useSWR(
|
||||
workspaceSlug ? `WORKSPACE_RECENT_ACTIVITY_${workspaceSlug}_${filter}` : null,
|
||||
@@ -55,6 +68,7 @@ export const RecentActivityWidget: React.FC<THomeWidgetProps> = observer((props)
|
||||
const resolveRecent = (activity: TActivityEntityData) => {
|
||||
switch (activity.entity_name) {
|
||||
case "page":
|
||||
case "workspace_page":
|
||||
return <RecentPage activity={activity} ref={ref} workspaceSlug={workspaceSlug} />;
|
||||
case "project":
|
||||
return <RecentProject activity={activity} ref={ref} workspaceSlug={workspaceSlug} />;
|
||||
@@ -65,13 +79,14 @@ export const RecentActivityWidget: React.FC<THomeWidgetProps> = observer((props)
|
||||
}
|
||||
};
|
||||
|
||||
if (!loader && joinedProjectIds?.length === 0) return <EmptyWorkspace />;
|
||||
if (!loader && !isWikiApp && joinedProjectIds?.length === 0) return <NoProjectsEmptyState />;
|
||||
|
||||
if (!isLoading && recents?.length === 0)
|
||||
return (
|
||||
<div ref={ref} className=" max-h-[500px] overflow-y-scroll">
|
||||
<div ref={ref} className="max-h-[500px] overflow-y-scroll">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="text-base font-semibold text-custom-text-350">Recents</div>
|
||||
<FiltersDropdown filters={filters} activeFilter={filter} setActiveFilter={setFilter} />
|
||||
{showFilterSelect && <FiltersDropdown filters={filters} activeFilter={filter} setActiveFilter={setFilter} />}
|
||||
</div>
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<RecentsEmptyState type={filter} />
|
||||
@@ -88,16 +103,14 @@ export const RecentActivityWidget: React.FC<THomeWidgetProps> = observer((props)
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="text-base font-semibold text-custom-text-350">Recents</div>
|
||||
|
||||
<FiltersDropdown filters={filters} activeFilter={filter} setActiveFilter={setFilter} />
|
||||
{showFilterSelect && <FiltersDropdown filters={filters} activeFilter={filter} setActiveFilter={setFilter} />}
|
||||
</div>
|
||||
<div className="min-h-[250px] flex flex-col">
|
||||
{isLoading && <WidgetLoader widgetKey={WIDGET_KEY} />}
|
||||
{!isLoading &&
|
||||
recents?.length > 0 &&
|
||||
recents
|
||||
.filter((recent: TActivityEntityData) => recent.entity_data)
|
||||
.map((activity: TActivityEntityData) => <div key={activity.id}>{resolveRecent(activity)}</div>)}
|
||||
?.filter((recent) => recent.entity_data)
|
||||
.map((activity) => <div key={activity.id}>{resolveRecent(activity)}</div>)}
|
||||
</div>
|
||||
</ContentOverflowWrapper>
|
||||
);
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
// plane types
|
||||
import { TActivityEntityData, TIssueEntityData } from "@plane/types";
|
||||
// plane ui
|
||||
import { LayersIcon, PriorityIcon, StateGroupIcon, Tooltip } from "@plane/ui";
|
||||
// components
|
||||
import { ListItem } from "@/components/core/list";
|
||||
import { MemberDropdown } from "@/components/dropdowns";
|
||||
// helpers
|
||||
import { calculateTimeAgo } from "@/helpers/date-time.helper";
|
||||
// hooks
|
||||
import { useIssueDetail, useProjectState } from "@/hooks/store";
|
||||
// plane web components
|
||||
import { IssueIdentifier } from "@/plane-web/components/issues";
|
||||
|
||||
type BlockProps = {
|
||||
@@ -24,9 +30,9 @@ export const RecentIssue = (props: BlockProps) => {
|
||||
<ListItem
|
||||
key={activity.id}
|
||||
itemLink=""
|
||||
title={""}
|
||||
title={issueDetails?.name}
|
||||
prependTitleElement={
|
||||
<div className="flex flex-shrink-0 items-center justify-center rounded-md gap-4 ">
|
||||
<div className="flex-shrink-0 flex items-center gap-2">
|
||||
{issueDetails.type ? (
|
||||
<IssueIdentifier
|
||||
size="lg"
|
||||
@@ -38,16 +44,19 @@ export const RecentIssue = (props: BlockProps) => {
|
||||
/>
|
||||
) : (
|
||||
<div className="flex gap-2 items-center justify-center">
|
||||
<div className="flex flex-shrink-0 items-center justify-center rounded gap-4 bg-custom-background-80 w-[25.5px] h-[25.5px]">
|
||||
<LayersIcon className="w-4 h-4 text-custom-text-350" />
|
||||
<div className="flex-shrink-0 grid place-items-center rounded bg-custom-background-80 size-8">
|
||||
<LayersIcon className="size-4 text-custom-text-350" />
|
||||
</div>
|
||||
<div className="font-medium text-custom-sidebar-text-400 text-sm whitespace-nowrap">
|
||||
<div className="font-medium text-custom-text-400 text-sm whitespace-nowrap">
|
||||
{issueDetails?.project_identifier}-{issueDetails?.sequence_id}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="text-custom-text-200 font-medium text-sm whitespace-nowrap">{issueDetails?.name}</div>
|
||||
<div className="font-medium text-xs text-custom-text-400">{calculateTimeAgo(activity.visited_at)}</div>
|
||||
</div>
|
||||
}
|
||||
appendTitleElement={
|
||||
<div className="flex-shrink-0 font-medium text-xs text-custom-text-400">
|
||||
{calculateTimeAgo(activity.visited_at)}
|
||||
</div>
|
||||
}
|
||||
quickActionElement={
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
import { useRouter } from "next/navigation";
|
||||
import { FileText } from "lucide-react";
|
||||
// plane types
|
||||
import { TActivityEntityData, TPageEntityData } from "@plane/types";
|
||||
// plane ui
|
||||
import { Avatar, Logo } from "@plane/ui";
|
||||
// plane utils
|
||||
import { getFileURL } from "@plane/utils";
|
||||
// components
|
||||
import { ListItem } from "@/components/core/list";
|
||||
// helpers
|
||||
import { calculateTimeAgo } from "@/helpers/date-time.helper";
|
||||
import { getPageName } from "@/helpers/page.helper";
|
||||
// hooks
|
||||
import { useMember } from "@/hooks/store";
|
||||
|
||||
type BlockProps = {
|
||||
@@ -12,38 +19,44 @@ type BlockProps = {
|
||||
ref: React.RefObject<HTMLDivElement>;
|
||||
workspaceSlug: string;
|
||||
};
|
||||
|
||||
export const RecentPage = (props: BlockProps) => {
|
||||
const { activity, ref, workspaceSlug } = props;
|
||||
// router
|
||||
const router = useRouter();
|
||||
// hooks
|
||||
// store hooks
|
||||
const { getUserDetails } = useMember();
|
||||
// derived values
|
||||
const pageDetails: TPageEntityData = activity.entity_data as TPageEntityData;
|
||||
const pageDetails = activity.entity_data as TPageEntityData;
|
||||
const ownerDetails = getUserDetails(pageDetails?.owned_by);
|
||||
const pageLink = pageDetails.project_id
|
||||
? `/${workspaceSlug}/projects/${pageDetails.project_id}/pages/${pageDetails.id}`
|
||||
: `/${workspaceSlug}/pages/${pageDetails.id}`;
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
key={activity.id}
|
||||
itemLink=""
|
||||
title={""}
|
||||
title={getPageName(pageDetails?.name)}
|
||||
prependTitleElement={
|
||||
<div className="flex flex-shrink-0 items-center justify-center rounded-md gap-4 ">
|
||||
<div className="flex gap-2 items-center justify-center">
|
||||
<div className="flex flex-shrink-0 items-center justify-center rounded gap-2 bg-custom-background-80 w-[25.5px] h-[25.5px]">
|
||||
<>
|
||||
{pageDetails?.logo_props?.in_use ? (
|
||||
<Logo logo={pageDetails?.logo_props} size={16} type="lucide" />
|
||||
) : (
|
||||
<FileText className="h-4 w-4 text-custom-text-350" />
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
<div className="font-medium text-custom-sidebar-text-400 text-sm whitespace-nowrap">
|
||||
<div className="flex-shrink-0 flex items-center gap-2">
|
||||
<div className="flex-shrink-0 grid place-items-center rounded bg-custom-background-80 size-8">
|
||||
{pageDetails?.logo_props?.in_use ? (
|
||||
<Logo logo={pageDetails?.logo_props} size={16} type="lucide" />
|
||||
) : (
|
||||
<FileText className="size-4 text-custom-text-350" />
|
||||
)}
|
||||
</div>
|
||||
{pageDetails?.project_identifier && (
|
||||
<div className="font-medium text-custom-text-400 text-sm whitespace-nowrap">
|
||||
{pageDetails?.project_identifier}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-custom-text-200 font-medium text-sm whitespace-nowrap">{pageDetails?.name}</div>
|
||||
<div className="font-medium text-xs text-custom-text-400">{calculateTimeAgo(activity.visited_at)}</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
appendTitleElement={
|
||||
<div className="flex-shrink-0 font-medium text-xs text-custom-text-400">
|
||||
{calculateTimeAgo(activity.visited_at)}
|
||||
</div>
|
||||
}
|
||||
quickActionElement={
|
||||
@@ -58,7 +71,7 @@ export const RecentPage = (props: BlockProps) => {
|
||||
onItemClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
router.push(`/${workspaceSlug}/projects/${pageDetails?.project_id}/pages/${pageDetails.id}`);
|
||||
router.push(pageLink);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { useRouter } from "next/navigation";
|
||||
// plane types
|
||||
import { TActivityEntityData, TProjectEntityData } from "@plane/types";
|
||||
// plane ui
|
||||
import { Logo } from "@plane/ui";
|
||||
// components
|
||||
import { ListItem } from "@/components/core/list";
|
||||
import { MemberDropdown } from "@/components/dropdowns";
|
||||
// helpers
|
||||
import { calculateTimeAgo } from "@/helpers/date-time.helper";
|
||||
|
||||
type BlockProps = {
|
||||
@@ -21,19 +25,18 @@ export const RecentProject = (props: BlockProps) => {
|
||||
<ListItem
|
||||
key={activity.id}
|
||||
itemLink=""
|
||||
title={""}
|
||||
title={projectDetails?.name}
|
||||
prependTitleElement={
|
||||
<div className="flex flex-shrink-0 items-center justify-center rounded-md gap-4 ">
|
||||
<div className="flex gap-2 items-center justify-center">
|
||||
<div className="flex flex-shrink-0 items-center justify-center rounded gap-4 bg-custom-background-80 w-[25.5px] h-[25.5px]">
|
||||
<Logo logo={projectDetails?.logo_props} size={16} />
|
||||
</div>
|
||||
<div className="font-medium text-custom-sidebar-text-400 text-sm whitespace-nowrap">
|
||||
{projectDetails?.identifier}
|
||||
</div>
|
||||
<div className="flex-shrink-0 flex items-center gap-2">
|
||||
<div className="flex-shrink-0 grid place-items-center rounded bg-custom-background-80 size-8">
|
||||
<Logo logo={projectDetails?.logo_props} size={16} />
|
||||
</div>
|
||||
<div className="text-custom-text-200 font-medium text-sm whitespace-nowrap">{projectDetails?.name}</div>
|
||||
<div className="font-medium text-xs text-custom-text-400">{calculateTimeAgo(activity.visited_at)}</div>
|
||||
<div className="font-medium text-custom-text-400 text-sm whitespace-nowrap">{projectDetails?.identifier}</div>
|
||||
</div>
|
||||
}
|
||||
appendTitleElement={
|
||||
<div className="flex-shrink-0 font-medium text-xs text-custom-text-400">
|
||||
{calculateTimeAgo(activity.visited_at)}
|
||||
</div>
|
||||
}
|
||||
quickActionElement={
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { FC, Fragment, useState } from "react";
|
||||
import { DayPicker } from "react-day-picker";
|
||||
import { DayPicker, getDefaultClassNames } from "react-day-picker";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// ui
|
||||
import { Button } from "@plane/ui";
|
||||
@@ -18,6 +18,8 @@ export const InboxIssueSnoozeModal: FC<InboxIssueSnoozeModalProps> = (props) =>
|
||||
// states
|
||||
const [date, setDate] = useState(value || new Date());
|
||||
|
||||
const defaultClassNames = getDefaultClassNames();
|
||||
|
||||
return (
|
||||
<Transition.Root show={isOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-20" onClose={handleClose}>
|
||||
@@ -46,6 +48,8 @@ export const InboxIssueSnoozeModal: FC<InboxIssueSnoozeModalProps> = (props) =>
|
||||
<Dialog.Panel className="relative flex transform rounded-lg bg-custom-background-100 px-5 py-8 text-left shadow-custom-shadow-md transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6">
|
||||
<div className="flex h-full w-full flex-col gap-y-1">
|
||||
<DayPicker
|
||||
captionLayout="dropdown"
|
||||
classNames={{root: `${defaultClassNames.root} rounded-md border border-custom-border-200 p-3`}}
|
||||
selected={date ? new Date(date) : undefined}
|
||||
defaultMonth={date ? new Date(date) : undefined}
|
||||
onSelect={(date) => {
|
||||
@@ -53,7 +57,6 @@ export const InboxIssueSnoozeModal: FC<InboxIssueSnoozeModalProps> = (props) =>
|
||||
setDate(date);
|
||||
}}
|
||||
mode="single"
|
||||
className="rounded-md border border-custom-border-200 p-3"
|
||||
disabled={[
|
||||
{
|
||||
before: new Date(),
|
||||
|
||||
+1
-6
@@ -55,12 +55,7 @@ export const IssueDetailWidgetCollapsibles: FC<Props> = observer((props) => {
|
||||
/>
|
||||
)}
|
||||
{shouldRenderRelations && (
|
||||
<RelationsCollapsible
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<RelationsCollapsible workspaceSlug={workspaceSlug} issueId={issueId} disabled={disabled} />
|
||||
)}
|
||||
{shouldRenderLinks && (
|
||||
<LinksCollapsible workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} disabled={disabled} />
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
"use client";
|
||||
import { FC, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// plane imports
|
||||
import { EIssueServiceType } from "@plane/constants";
|
||||
import { TIssue, TIssueRelationIdMap, TIssueServiceType } from "@plane/types";
|
||||
import { TIssue, TIssueServiceType } from "@plane/types";
|
||||
import { Collapsible } from "@plane/ui";
|
||||
// components
|
||||
import { RelationIssueList } from "@/components/issues";
|
||||
@@ -11,6 +12,7 @@ import { CreateUpdateIssueModal } from "@/components/issues/issue-modal";
|
||||
// hooks
|
||||
import { useIssueDetail } from "@/hooks/store";
|
||||
// Plane-web
|
||||
import { CreateUpdateEpicModal } from "@/plane-web/components/epics";
|
||||
import { useTimeLineRelationOptions } from "@/plane-web/components/relations";
|
||||
import { TIssueRelationTypes } from "@/plane-web/types";
|
||||
// helper
|
||||
@@ -18,7 +20,6 @@ import { useRelationOperations } from "./helper";
|
||||
|
||||
type Props = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
issueId: string;
|
||||
disabled: boolean;
|
||||
issueServiceType?: TIssueServiceType;
|
||||
@@ -35,7 +36,7 @@ export type TRelationObject = {
|
||||
};
|
||||
|
||||
export const RelationsCollapsibleContent: FC<Props> = observer((props) => {
|
||||
const { workspaceSlug, projectId, issueId, disabled = false, issueServiceType = EIssueServiceType.ISSUES } = props;
|
||||
const { workspaceSlug, issueId, disabled = false, issueServiceType = EIssueServiceType.ISSUES } = props;
|
||||
// state
|
||||
const [issueCrudState, setIssueCrudState] = useState<{
|
||||
update: TIssueCrudState;
|
||||
@@ -62,6 +63,7 @@ export const RelationsCollapsibleContent: FC<Props> = observer((props) => {
|
||||
|
||||
// helper
|
||||
const issueOperations = useRelationOperations();
|
||||
const epicOperations = useRelationOperations(EIssueServiceType.EPICS);
|
||||
|
||||
// derived values
|
||||
const relations = getRelationsByIssueId(issueId);
|
||||
@@ -124,12 +126,10 @@ export const RelationsCollapsibleContent: FC<Props> = observer((props) => {
|
||||
>
|
||||
<RelationIssueList
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
relationKey={relation.relationKey}
|
||||
issueIds={relation.issueIds}
|
||||
disabled={disabled}
|
||||
issueOperations={issueOperations}
|
||||
handleIssueCrudState={handleIssueCrudState}
|
||||
issueServiceType={issueServiceType}
|
||||
/>
|
||||
@@ -146,24 +146,56 @@ export const RelationsCollapsibleContent: FC<Props> = observer((props) => {
|
||||
toggleDeleteIssueModal(null);
|
||||
}}
|
||||
data={issueCrudState?.delete?.issue as TIssue}
|
||||
onSubmit={async () =>
|
||||
await issueOperations.remove(workspaceSlug, projectId, issueCrudState?.delete?.issue?.id as string)
|
||||
}
|
||||
onSubmit={async () => {
|
||||
if (
|
||||
issueCrudState.delete.issue &&
|
||||
issueCrudState.delete.issue.id &&
|
||||
issueCrudState.delete.issue.project_id
|
||||
) {
|
||||
const deleteOperation = !!issueCrudState.delete.issue?.is_epic
|
||||
? epicOperations.remove
|
||||
: issueOperations.remove;
|
||||
await deleteOperation(
|
||||
workspaceSlug,
|
||||
issueCrudState.delete.issue?.project_id,
|
||||
issueCrudState?.delete?.issue?.id as string
|
||||
);
|
||||
}
|
||||
}}
|
||||
isEpic={!!issueCrudState.delete.issue?.is_epic}
|
||||
/>
|
||||
)}
|
||||
|
||||
{shouldRenderIssueUpdateModal && (
|
||||
<CreateUpdateIssueModal
|
||||
isOpen={issueCrudState?.update?.toggle}
|
||||
onClose={() => {
|
||||
handleIssueCrudState("update", null, null);
|
||||
toggleCreateIssueModal(false);
|
||||
}}
|
||||
data={issueCrudState?.update?.issue ?? undefined}
|
||||
onSubmit={async (_issue: TIssue) => {
|
||||
await issueOperations.update(workspaceSlug, projectId, _issue.id, _issue);
|
||||
}}
|
||||
/>
|
||||
<>
|
||||
{!!issueCrudState?.update?.issue?.is_epic ? (
|
||||
<CreateUpdateEpicModal
|
||||
isOpen={issueCrudState?.update?.toggle}
|
||||
onClose={() => {
|
||||
handleIssueCrudState("update", null, null);
|
||||
toggleCreateIssueModal(false);
|
||||
}}
|
||||
data={issueCrudState?.update?.issue ?? undefined}
|
||||
onSubmit={async (_issue: TIssue) => {
|
||||
if (!_issue.id || !_issue.project_id) return;
|
||||
await epicOperations.update(workspaceSlug, _issue.project_id, _issue.id, _issue);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<CreateUpdateIssueModal
|
||||
isOpen={issueCrudState?.update?.toggle}
|
||||
onClose={() => {
|
||||
handleIssueCrudState("update", null, null);
|
||||
toggleCreateIssueModal(false);
|
||||
}}
|
||||
data={issueCrudState?.update?.issue ?? undefined}
|
||||
onSubmit={async (_issue: TIssue) => {
|
||||
if (!_issue.id || !_issue.project_id) return;
|
||||
await issueOperations.update(workspaceSlug, _issue.project_id, _issue.id, _issue);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -23,6 +23,8 @@ export const useRelationOperations = (
|
||||
const { updateIssue, removeIssue } = useIssueDetail(issueServiceType);
|
||||
const { captureIssueEvent } = useEventTracker();
|
||||
const pathname = usePathname();
|
||||
// derived values
|
||||
const entityName = issueServiceType === EIssueServiceType.ISSUES ? "Issue" : "Epic";
|
||||
|
||||
const issueOperations: TRelationIssueOperations = useMemo(
|
||||
() => ({
|
||||
@@ -32,7 +34,7 @@ export const useRelationOperations = (
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Link Copied!",
|
||||
message: "Issue link copied to clipboard.",
|
||||
message: `${entityName} link copied to clipboard.`,
|
||||
});
|
||||
});
|
||||
},
|
||||
@@ -51,7 +53,7 @@ export const useRelationOperations = (
|
||||
setToast({
|
||||
title: "Success!",
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
message: "Issue updated successfully",
|
||||
message: `${entityName} updated successfully`,
|
||||
});
|
||||
} catch (error) {
|
||||
captureIssueEvent({
|
||||
@@ -66,7 +68,7 @@ export const useRelationOperations = (
|
||||
setToast({
|
||||
title: "Error!",
|
||||
type: TOAST_TYPE.ERROR,
|
||||
message: "Issue update failed",
|
||||
message: `${entityName} update failed`,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -11,14 +11,13 @@ import { useIssueDetail } from "@/hooks/store";
|
||||
|
||||
type Props = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
issueId: string;
|
||||
disabled?: boolean;
|
||||
issueServiceType?: TIssueServiceType;
|
||||
};
|
||||
|
||||
export const RelationsCollapsible: FC<Props> = observer((props) => {
|
||||
const { workspaceSlug, projectId, issueId, disabled = false, issueServiceType = EIssueServiceType.ISSUES } = props;
|
||||
const { workspaceSlug, issueId, disabled = false, issueServiceType = EIssueServiceType.ISSUES } = props;
|
||||
// store hooks
|
||||
const { openWidgets, toggleOpenWidget } = useIssueDetail(issueServiceType);
|
||||
|
||||
@@ -41,7 +40,6 @@ export const RelationsCollapsible: FC<Props> = observer((props) => {
|
||||
>
|
||||
<RelationsCollapsibleContent
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
disabled={disabled}
|
||||
issueServiceType={issueServiceType}
|
||||
|
||||
@@ -4,11 +4,16 @@ import { FC, useEffect, useRef, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { Check, Globe2, Lock, Pencil, Trash2, X } from "lucide-react";
|
||||
// plane constants
|
||||
import { EIssueCommentAccessSpecifier } from "@plane/constants";
|
||||
// plane editor
|
||||
import { EditorReadOnlyRefApi, EditorRefApi } from "@plane/editor";
|
||||
// plane types
|
||||
import { TIssueComment } from "@plane/types";
|
||||
// ui
|
||||
// plane ui
|
||||
import { CustomMenu } from "@plane/ui";
|
||||
// plane utils
|
||||
import { cn } from "@plane/utils";
|
||||
// components
|
||||
import { LiteTextEditor, LiteTextReadOnlyEditor } from "@/components/editor";
|
||||
// helpers
|
||||
@@ -42,21 +47,21 @@ export const IssueCommentCard: FC<TIssueCommentCard> = observer((props) => {
|
||||
showAccessSpecifier = false,
|
||||
disabled = false,
|
||||
} = props;
|
||||
// hooks
|
||||
// states
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
// refs
|
||||
const editorRef = useRef<EditorRefApi>(null);
|
||||
const showEditorRef = useRef<EditorReadOnlyRefApi>(null);
|
||||
// store hooks
|
||||
const {
|
||||
comment: { getCommentById },
|
||||
} = useIssueDetail();
|
||||
const { data: currentUser } = useUser();
|
||||
// refs
|
||||
const editorRef = useRef<EditorRefApi>(null);
|
||||
const showEditorRef = useRef<EditorReadOnlyRefApi>(null);
|
||||
// state
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
|
||||
// derived values
|
||||
const comment = getCommentById(commentId);
|
||||
const workspaceStore = useWorkspace();
|
||||
const workspaceId = workspaceStore.getWorkspaceBySlug(comment?.workspace_detail?.slug as string)?.id as string;
|
||||
|
||||
// form info
|
||||
const {
|
||||
formState: { isSubmitting },
|
||||
handleSubmit,
|
||||
@@ -66,6 +71,11 @@ export const IssueCommentCard: FC<TIssueCommentCard> = observer((props) => {
|
||||
} = useForm<Partial<TIssueComment>>({
|
||||
defaultValues: { comment_html: comment?.comment_html },
|
||||
});
|
||||
// derived values
|
||||
const commentHTML = watch("comment_html");
|
||||
const isEmpty = isCommentEmpty(commentHTML);
|
||||
const isEditorReadyToDiscard = editorRef.current?.isEditorReadyToDiscard();
|
||||
const isSubmitButtonDisabled = isSubmitting || !isEditorReadyToDiscard;
|
||||
|
||||
const onEnter = async (formData: Partial<TIssueComment>) => {
|
||||
if (isSubmitting || !comment) return;
|
||||
@@ -83,10 +93,8 @@ export const IssueCommentCard: FC<TIssueCommentCard> = observer((props) => {
|
||||
}
|
||||
}, [isEditing, setFocus]);
|
||||
|
||||
const commentHTML = watch("comment_html");
|
||||
const isEmpty = isCommentEmpty(commentHTML);
|
||||
|
||||
if (!comment || !currentUser) return <></>;
|
||||
|
||||
return (
|
||||
<IssueCommentBlock
|
||||
commentId={commentId}
|
||||
@@ -95,8 +103,8 @@ export const IssueCommentCard: FC<TIssueCommentCard> = observer((props) => {
|
||||
{!disabled && currentUser?.id === comment.actor && (
|
||||
<CustomMenu ellipsis closeOnSelect>
|
||||
<CustomMenu.MenuItem onClick={() => setIsEditing(true)} className="flex items-center gap-1">
|
||||
<Pencil className="h-3 w-3" />
|
||||
Edit comment
|
||||
<Pencil className="flex-shrink-0 size-3" />
|
||||
Edit
|
||||
</CustomMenu.MenuItem>
|
||||
{showAccessSpecifier && (
|
||||
<>
|
||||
@@ -107,7 +115,7 @@ export const IssueCommentCard: FC<TIssueCommentCard> = observer((props) => {
|
||||
}
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
<Globe2 className="h-3 w-3" />
|
||||
<Globe2 className="flex-shrink-0 size-3" />
|
||||
Switch to public comment
|
||||
</CustomMenu.MenuItem>
|
||||
) : (
|
||||
@@ -117,7 +125,7 @@ export const IssueCommentCard: FC<TIssueCommentCard> = observer((props) => {
|
||||
}
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
<Lock className="h-3 w-3" />
|
||||
<Lock className="flex-shrink-0 size-3" />
|
||||
Switch to private comment
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
@@ -127,8 +135,8 @@ export const IssueCommentCard: FC<TIssueCommentCard> = observer((props) => {
|
||||
onClick={() => activityOperations.removeComment(comment.id)}
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
Delete comment
|
||||
<Trash2 className="flex-shrink-0 size-3" />
|
||||
Delete
|
||||
</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
)}
|
||||
@@ -166,24 +174,27 @@ export const IssueCommentCard: FC<TIssueCommentCard> = observer((props) => {
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-1 self-end">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSubmit(onEnter)}
|
||||
disabled={isSubmitting || isEmpty}
|
||||
className={`group rounded border border-green-500 bg-green-500/20 p-2 shadow-md duration-300 ${
|
||||
isEmpty ? "cursor-not-allowed bg-gray-200" : "hover:bg-green-500"
|
||||
}`}
|
||||
>
|
||||
<Check
|
||||
className={`h-3 w-3 text-green-500 duration-300 ${isEmpty ? "text-black" : "group-hover:text-white"}`}
|
||||
/>
|
||||
</button>
|
||||
{!isEmpty && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSubmit(onEnter)}
|
||||
disabled={isSubmitButtonDisabled}
|
||||
className={cn(
|
||||
"group rounded border border-green-500 text-green-500 hover:text-white bg-green-500/20 hover:bg-green-500 p-2 shadow-md duration-300",
|
||||
{
|
||||
"pointer-events-none": isSubmitButtonDisabled,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<Check className="size-3" />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="group rounded border border-red-500 bg-red-500/20 p-2 shadow-md duration-300 hover:bg-red-500"
|
||||
onClick={() => setIsEditing(false)}
|
||||
>
|
||||
<X className="h-3 w-3 text-red-500 duration-300 group-hover:text-white" />
|
||||
<X className="size-3 text-red-500 duration-300 group-hover:text-white" />
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -132,7 +132,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
|
||||
const isSubGroup = !!sub_group_id && sub_group_id !== "null";
|
||||
|
||||
return (
|
||||
<ContentWrapper className={`flex-row relative gap-4 py-4`}>
|
||||
<ContentWrapper className={`flex-row relative gap-4 !pt-2 !pb-0`}>
|
||||
{list &&
|
||||
list.length > 0 &&
|
||||
list.map((subList: IGroupByColumn, groupIndex) => {
|
||||
|
||||
@@ -42,7 +42,11 @@ export const CreateUpdateIssueModalBase: React.FC<IssuesModalProps> = observer((
|
||||
} = props;
|
||||
const issueStoreType = useIssueStoreType();
|
||||
|
||||
const storeType = issueStoreFromProps ?? issueStoreType;
|
||||
let storeType = issueStoreFromProps ?? issueStoreType;
|
||||
// Fallback to project store if epic store is used in issue modal.
|
||||
if (storeType === EIssuesStoreType.EPIC) {
|
||||
storeType = EIssuesStoreType.PROJECT;
|
||||
}
|
||||
// ref
|
||||
const issueTitleRef = useRef<HTMLInputElement>(null);
|
||||
// states
|
||||
|
||||
@@ -16,17 +16,15 @@ import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// plane web components
|
||||
import { IssueIdentifier } from "@/plane-web/components/issues";
|
||||
import { TIssueRelationTypes } from "@/plane-web/types";
|
||||
//
|
||||
import { TRelationIssueOperations } from "../issue-detail-widgets/relations/helper";
|
||||
// local imports
|
||||
import { useRelationOperations } from "../issue-detail-widgets/relations/helper";
|
||||
|
||||
type Props = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
issueId: string;
|
||||
relationKey: TIssueRelationTypes;
|
||||
relationIssueId: string;
|
||||
disabled: boolean;
|
||||
issueOperations: TRelationIssueOperations;
|
||||
handleIssueCrudState: (key: "update" | "delete", issueId: string, issue?: TIssue | null) => void;
|
||||
issueServiceType?: TIssueServiceType;
|
||||
};
|
||||
@@ -34,12 +32,10 @@ type Props = {
|
||||
export const RelationIssueListItem: FC<Props> = observer((props) => {
|
||||
const {
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
issueId,
|
||||
relationKey,
|
||||
relationIssueId,
|
||||
disabled = false,
|
||||
issueOperations,
|
||||
handleIssueCrudState,
|
||||
issueServiceType = EIssueServiceType.ISSUES,
|
||||
} = props;
|
||||
@@ -57,16 +53,18 @@ export const RelationIssueListItem: FC<Props> = observer((props) => {
|
||||
// derived values
|
||||
const issue = getIssueById(relationIssueId);
|
||||
const { handleRedirection } = useIssuePeekOverviewRedirection(!!issue?.is_epic);
|
||||
const issueOperations = useRelationOperations(!!issue?.is_epic ? EIssueServiceType.EPICS : EIssueServiceType.ISSUES);
|
||||
const projectDetail = (issue && issue.project_id && project.getProjectById(issue.project_id)) || undefined;
|
||||
const projectId = issue?.project_id;
|
||||
const currentIssueStateDetail =
|
||||
(issue?.project_id && getProjectStates(issue?.project_id)?.find((state) => issue?.state_id == state.id)) ||
|
||||
undefined;
|
||||
if (!issue) return <></>;
|
||||
if (!issue || !projectId) return <></>;
|
||||
const issueLink = `/${workspaceSlug}/projects/${projectId}/${issue.is_epic ? "epics" : "issues"}/${issue.id}`;
|
||||
|
||||
// handlers
|
||||
const handleIssuePeekOverview = (issue: TIssue) => {
|
||||
if (issueServiceType === EIssueServiceType.ISSUES && issue.is_epic) {
|
||||
if (issue.is_epic) {
|
||||
// open epics in new tab
|
||||
window.open(issueLink, "_blank");
|
||||
return;
|
||||
@@ -155,7 +153,7 @@ export const RelationIssueListItem: FC<Props> = observer((props) => {
|
||||
<CustomMenu.MenuItem onClick={handleEditIssue}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Pencil className="h-3.5 w-3.5" strokeWidth={2} />
|
||||
<span>Edit issue</span>
|
||||
<span>Edit</span>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
@@ -163,7 +161,7 @@ export const RelationIssueListItem: FC<Props> = observer((props) => {
|
||||
<CustomMenu.MenuItem onClick={handleCopyIssueLink}>
|
||||
<div className="flex items-center gap-2">
|
||||
<LinkIcon className="h-3.5 w-3.5" strokeWidth={2} />
|
||||
<span>Copy issue link</span>
|
||||
<span>Copy link</span>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
|
||||
@@ -180,7 +178,7 @@ export const RelationIssueListItem: FC<Props> = observer((props) => {
|
||||
<CustomMenu.MenuItem onClick={handleDeleteIssue}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Trash className="h-3.5 w-3.5" strokeWidth={2} />
|
||||
<span>Delete issue</span>
|
||||
<span>Delete</span>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
|
||||
@@ -8,16 +8,12 @@ import { TIssue, TIssueServiceType } from "@plane/types";
|
||||
import { RelationIssueListItem } from "@/components/issues/relations";
|
||||
// Plane-web
|
||||
import { TIssueRelationTypes } from "@/plane-web/types";
|
||||
//
|
||||
import { TRelationIssueOperations } from "../issue-detail-widgets/relations/helper";
|
||||
|
||||
type Props = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
issueId: string;
|
||||
issueIds: string[];
|
||||
relationKey: TIssueRelationTypes;
|
||||
issueOperations: TRelationIssueOperations;
|
||||
handleIssueCrudState: (key: "update" | "delete", issueId: string, issue?: TIssue | null) => void;
|
||||
disabled?: boolean;
|
||||
issueServiceType?: TIssueServiceType;
|
||||
@@ -26,12 +22,10 @@ type Props = {
|
||||
export const RelationIssueList: FC<Props> = observer((props) => {
|
||||
const {
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
issueId,
|
||||
issueIds,
|
||||
relationKey,
|
||||
disabled = false,
|
||||
issueOperations,
|
||||
handleIssueCrudState,
|
||||
issueServiceType = EIssueServiceType.ISSUES,
|
||||
} = props;
|
||||
@@ -44,13 +38,11 @@ export const RelationIssueList: FC<Props> = observer((props) => {
|
||||
<RelationIssueListItem
|
||||
key={relationIssueId}
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
relationKey={relationKey}
|
||||
relationIssueId={relationIssueId}
|
||||
disabled={disabled}
|
||||
handleIssueCrudState={handleIssueCrudState}
|
||||
issueOperations={issueOperations}
|
||||
issueServiceType={issueServiceType}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -135,14 +135,14 @@ export const LabelStatComponent = observer((props: TLabelStatComponent) => {
|
||||
<SingleProgressStats
|
||||
key={label.id}
|
||||
title={
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className="block h-3 w-3 rounded-full"
|
||||
<div className="flex items-center gap-2 truncate">
|
||||
<div
|
||||
className="h-3 w-3 rounded-full flex-shrink-0"
|
||||
style={{
|
||||
backgroundColor: label.color ?? "transparent",
|
||||
}}
|
||||
/>
|
||||
<span className="text-xs">{label.title ?? "No labels"}</span>
|
||||
<p className="text-xs text-ellipsis truncate">{label.title ?? "No labels"}</p>
|
||||
</div>
|
||||
}
|
||||
completed={label.completed}
|
||||
|
||||
@@ -36,6 +36,7 @@ import { TPageInstance } from "@/store/pages/base-page";
|
||||
|
||||
export type TPageActions =
|
||||
| "full-screen"
|
||||
| "sticky-toolbar"
|
||||
| "copy-markdown"
|
||||
| "toggle-lock"
|
||||
| "toggle-access"
|
||||
|
||||
@@ -30,9 +30,9 @@ export const PageOptionsDropdown: React.FC<Props> = observer((props) => {
|
||||
// router
|
||||
const router = useRouter();
|
||||
// store values
|
||||
const { name } = page;
|
||||
const { name, isContentEditable } = page;
|
||||
// page filters
|
||||
const { isFullWidth, handleFullWidth } = usePageFilters();
|
||||
const { isFullWidth, handleFullWidth, isStickyToolbarEnabled, handleStickyToolbar } = usePageFilters();
|
||||
// update query params
|
||||
const { updateQueryParams } = useQueryParams();
|
||||
// menu items list
|
||||
@@ -49,6 +49,18 @@ export const PageOptionsDropdown: React.FC<Props> = observer((props) => {
|
||||
),
|
||||
className: "flex items-center justify-between gap-2",
|
||||
},
|
||||
{
|
||||
key: "sticky-toolbar",
|
||||
action: () => handleStickyToolbar(!isStickyToolbarEnabled),
|
||||
customContent: (
|
||||
<>
|
||||
Sticky toolbar
|
||||
<ToggleSwitch value={isStickyToolbarEnabled} onChange={() => {}} />
|
||||
</>
|
||||
),
|
||||
className: "flex items-center justify-between gap-2",
|
||||
shouldRender: isContentEditable,
|
||||
},
|
||||
{
|
||||
key: "copy-markdown",
|
||||
action: () => {
|
||||
@@ -86,7 +98,16 @@ export const PageOptionsDropdown: React.FC<Props> = observer((props) => {
|
||||
shouldRender: true,
|
||||
},
|
||||
],
|
||||
[editorRef, handleFullWidth, isFullWidth, router, updateQueryParams]
|
||||
[
|
||||
editorRef,
|
||||
handleFullWidth,
|
||||
handleStickyToolbar,
|
||||
isContentEditable,
|
||||
isFullWidth,
|
||||
isStickyToolbarEnabled,
|
||||
router,
|
||||
updateQueryParams,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -102,6 +123,7 @@ export const PageOptionsDropdown: React.FC<Props> = observer((props) => {
|
||||
extraOptions={EXTRA_MENU_OPTIONS}
|
||||
optionsOrder={[
|
||||
"full-screen",
|
||||
"sticky-toolbar",
|
||||
"copy-link",
|
||||
"make-a-copy",
|
||||
"move",
|
||||
|
||||
@@ -23,7 +23,7 @@ export const PageEditorHeaderRoot: React.FC<Props> = observer((props) => {
|
||||
// derived values
|
||||
const { isContentEditable } = page;
|
||||
// page filters
|
||||
const { isFullWidth } = usePageFilters();
|
||||
const { isFullWidth, isStickyToolbarEnabled } = usePageFilters();
|
||||
// derived values
|
||||
const resolvedEditorRef = editorRef.current;
|
||||
|
||||
@@ -48,7 +48,9 @@ export const PageEditorHeaderRoot: React.FC<Props> = observer((props) => {
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{editorReady && isContentEditable && editorRef.current && <PageToolbar editorRef={editorRef?.current} />}
|
||||
{isStickyToolbarEnabled && editorReady && isContentEditable && editorRef.current && (
|
||||
<PageToolbar editorRef={editorRef?.current} />
|
||||
)}
|
||||
</Header.LeftItem>
|
||||
<PageExtraOptions editorRef={resolvedEditorRef} page={page} />
|
||||
</Header>
|
||||
|
||||
@@ -84,7 +84,7 @@ export const StickiesList = observer((props: TProps) => {
|
||||
|
||||
if (loader === "loaded" && workspaceStickyIds.length === 0) {
|
||||
return (
|
||||
<div className="size-full grid place-items-center">
|
||||
<div className="size-full grid place-items-center px-2">
|
||||
{isStickiesPage || searchQuery ? (
|
||||
<EmptyState
|
||||
type={searchQuery ? EmptyStateType.STICKIES_SEARCH : EmptyStateType.STICKIES}
|
||||
|
||||
@@ -121,15 +121,20 @@ export const StickyDNDWrapper = observer((props: Props) => {
|
||||
}, [handleDrop, isDragging, isLastChild, pathname, stickyId, workspaceSlug]);
|
||||
|
||||
return (
|
||||
<div className="flex min-h-[300px] box-border p-2 flex-col" style={{ width: itemWidth }}>
|
||||
{!isInFirstRow && <DropIndicator isVisible={instruction === "reorder-above"} />}
|
||||
<div
|
||||
className="flex flex-col box-border p-[8px]"
|
||||
style={{
|
||||
width: itemWidth,
|
||||
}}
|
||||
>
|
||||
{/* {!isInFirstRow && <DropIndicator isVisible={instruction === "reorder-above"} />} */}
|
||||
<StickyNote
|
||||
key={stickyId || "new"}
|
||||
workspaceSlug={workspaceSlug}
|
||||
stickyId={stickyId}
|
||||
handleLayout={handleLayout}
|
||||
/>
|
||||
{!isInLastRow && <DropIndicator isVisible={instruction === "reorder-below"} />}
|
||||
{/* {!isInLastRow && <DropIndicator isVisible={instruction === "reorder-below"} />} */}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
// plane editor
|
||||
import { EditorRefApi } from "@plane/editor";
|
||||
// plane types
|
||||
import { TSticky } from "@plane/types";
|
||||
// plane utils
|
||||
import { isCommentEmpty } from "@plane/utils";
|
||||
import { cn, isCommentEmpty } from "@plane/utils";
|
||||
// hooks
|
||||
import { useWorkspace } from "@/hooks/store";
|
||||
// components
|
||||
@@ -25,10 +26,13 @@ export const StickyInput = (props: TProps) => {
|
||||
const { stickyData, workspaceSlug, handleUpdate, stickyId, handleDelete, handleChange, showToolbar } = props;
|
||||
// refs
|
||||
const editorRef = useRef<EditorRefApi>(null);
|
||||
// navigation
|
||||
const pathname = usePathname();
|
||||
// store hooks
|
||||
const { getWorkspaceBySlug } = useWorkspace();
|
||||
// derived values
|
||||
const workspaceId = getWorkspaceBySlug(workspaceSlug)?.id?.toString() ?? "";
|
||||
const isStickiesPage = pathname?.includes("stickies");
|
||||
// form info
|
||||
const { handleSubmit, reset, control } = useForm<TSticky>({
|
||||
defaultValues: {
|
||||
@@ -74,10 +78,15 @@ export const StickyInput = (props: TProps) => {
|
||||
if (!isContentEmpty) return "";
|
||||
return "Click to type here";
|
||||
}}
|
||||
containerClassName="px-0 text-base min-h-[250px] w-full"
|
||||
containerClassName={cn(
|
||||
"w-full min-h-[256px] max-h-[540px] overflow-y-scroll vertical-scrollbar scrollbar-sm p-4 text-base",
|
||||
{
|
||||
"max-h-[588px]": isStickiesPage,
|
||||
}
|
||||
)}
|
||||
uploadFile={async () => ""}
|
||||
showToolbar={showToolbar}
|
||||
parentClassName={"border-none p-0"}
|
||||
parentClassName="border-none p-0"
|
||||
handleDelete={handleDelete}
|
||||
handleColorChange={handleChange}
|
||||
ref={editorRef}
|
||||
|
||||
@@ -74,7 +74,7 @@ export const StickyNote = observer((props: TProps) => {
|
||||
handleClose={() => setIsDeleteModalOpen(false)}
|
||||
/>
|
||||
<div
|
||||
className={cn("w-full flex flex-col h-fit rounded p-4 group/sticky max-h-[650px] overflow-y-scroll", className)}
|
||||
className={cn("w-full h-fit flex flex-col rounded group/sticky overflow-y-scroll", className)}
|
||||
style={{
|
||||
backgroundColor,
|
||||
}}
|
||||
|
||||
@@ -922,7 +922,7 @@ const emptyStateDetails: Record<EmptyStateType, EmptyStateDetails> = {
|
||||
key: EmptyStateType.STICKIES,
|
||||
title: "Stickies are quick notes and to-dos you take down on the fly.",
|
||||
description:
|
||||
"Capture your thoughts and ideas effortlessly by creating stickies that you can access anytime and from anywhere.",
|
||||
"Capture ideas, ahas, brainwaves, light-bulb moments without scrambling for a pen and paper, hunting for the recorder app on your phone, or firing up a notes app only to forget all about it later. Keep them all right next to your work so you can easily come back, build them up, or well, discard them.",
|
||||
path: "/empty-state/stickies/stickies",
|
||||
primaryButton: {
|
||||
icon: <Plus className="size-4" />,
|
||||
@@ -945,8 +945,8 @@ const emptyStateDetails: Record<EmptyStateType, EmptyStateDetails> = {
|
||||
},
|
||||
[EmptyStateType.HOME_WIDGETS]: {
|
||||
key: EmptyStateType.HOME_WIDGETS,
|
||||
title: "It's Quiet Without Widgets, Turn Them On",
|
||||
description: "It looks like all your widgets are turned off. Enable them\nnow to enhance your experience!",
|
||||
title: "So much to add, yet such empty",
|
||||
description: "You have turned off all your widgets. Turn some or\nall of them back on to see delightful things.",
|
||||
path: "/empty-state/dashboard/widgets",
|
||||
primaryButton: {
|
||||
icon: <Shapes className="size-4" />,
|
||||
|
||||
@@ -7,7 +7,7 @@ export const IssuesStoreContext = createContext<EIssuesStoreType | undefined>(un
|
||||
|
||||
export const useIssueStoreType = () => {
|
||||
const storeType = useContext(IssuesStoreContext);
|
||||
const { globalViewId, viewId, projectId, cycleId, moduleId, userId, epicId, teamId } = useParams();
|
||||
const { globalViewId, viewId, projectId, cycleId, moduleId, userId, epicId, teamspaceId } = useParams();
|
||||
|
||||
// If store type exists in context, use that store type
|
||||
if (storeType) return storeType;
|
||||
@@ -27,9 +27,9 @@ export const useIssueStoreType = () => {
|
||||
|
||||
if (projectId) return EIssuesStoreType.PROJECT;
|
||||
|
||||
if (teamId) return EIssuesStoreType.TEAM;
|
||||
if (teamspaceId) return EIssuesStoreType.TEAM;
|
||||
|
||||
if (teamId && viewId) return EIssuesStoreType.TEAM_VIEW;
|
||||
if (teamspaceId && viewId) return EIssuesStoreType.TEAM_VIEW;
|
||||
|
||||
return EIssuesStoreType.PROJECT;
|
||||
};
|
||||
|
||||
@@ -8,12 +8,14 @@ export type TPagesPersonalizationConfig = {
|
||||
full_width: boolean;
|
||||
font_size: TEditorFontSize;
|
||||
font_style: TEditorFontStyle;
|
||||
sticky_toolbar: boolean;
|
||||
};
|
||||
|
||||
const DEFAULT_PERSONALIZATION_VALUES: TPagesPersonalizationConfig = {
|
||||
full_width: false,
|
||||
font_size: "large-font",
|
||||
font_style: "sans-serif",
|
||||
sticky_toolbar: true,
|
||||
};
|
||||
|
||||
export const usePageFilters = () => {
|
||||
@@ -23,7 +25,17 @@ export const usePageFilters = () => {
|
||||
DEFAULT_PERSONALIZATION_VALUES
|
||||
);
|
||||
// stored values
|
||||
const isFullWidth = useMemo(() => !!pagesConfig?.full_width, [pagesConfig?.full_width]);
|
||||
const isFullWidth = useMemo(
|
||||
() => (pagesConfig?.full_width === undefined ? DEFAULT_PERSONALIZATION_VALUES.full_width : pagesConfig?.full_width),
|
||||
[pagesConfig?.full_width]
|
||||
);
|
||||
const isStickyToolbarEnabled = useMemo(
|
||||
() =>
|
||||
pagesConfig?.sticky_toolbar === undefined
|
||||
? DEFAULT_PERSONALIZATION_VALUES.sticky_toolbar
|
||||
: pagesConfig?.sticky_toolbar,
|
||||
[pagesConfig?.sticky_toolbar]
|
||||
);
|
||||
const fontSize = useMemo(
|
||||
() => pagesConfig?.font_size ?? DEFAULT_PERSONALIZATION_VALUES.font_size,
|
||||
[pagesConfig?.font_size]
|
||||
@@ -78,6 +90,18 @@ export const usePageFilters = () => {
|
||||
},
|
||||
[handleUpdateConfig]
|
||||
);
|
||||
/**
|
||||
* @description action to update full_width value
|
||||
* @param {boolean} value
|
||||
*/
|
||||
const handleStickyToolbar = useCallback(
|
||||
(value: boolean) => {
|
||||
handleUpdateConfig({
|
||||
sticky_toolbar: value,
|
||||
});
|
||||
},
|
||||
[handleUpdateConfig]
|
||||
);
|
||||
|
||||
return {
|
||||
fontSize,
|
||||
@@ -86,5 +110,7 @@ export const usePageFilters = () => {
|
||||
handleFontStyle,
|
||||
isFullWidth,
|
||||
handleFullWidth,
|
||||
isStickyToolbarEnabled,
|
||||
handleStickyToolbar,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -117,6 +117,10 @@ export class IssueService extends APIService {
|
||||
if (response.data && this.serviceType === EIssueServiceType.ISSUES) {
|
||||
updateIssue({ ...response.data, is_local_update: 1 });
|
||||
}
|
||||
// add is_epic flag when the service type is epic
|
||||
if (response.data && this.serviceType === EIssueServiceType.EPICS) {
|
||||
response.data.is_epic = true;
|
||||
}
|
||||
return response?.data;
|
||||
})
|
||||
.catch((error) => {
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
TSearchResponse,
|
||||
TSearchEntityRequestPayload,
|
||||
TWidgetEntityData,
|
||||
TActivityEntityData,
|
||||
} from "@plane/types";
|
||||
import { APIService } from "@/services/api.service";
|
||||
// helpers
|
||||
@@ -282,7 +283,7 @@ export class WorkspaceService extends APIService {
|
||||
});
|
||||
}
|
||||
|
||||
// quick links
|
||||
// quicklinks
|
||||
async fetchWorkspaceLinks(workspaceSlug: string): Promise<TLink[]> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/quick-links/`)
|
||||
.then((response) => response?.data)
|
||||
@@ -329,7 +330,7 @@ export class WorkspaceService extends APIService {
|
||||
}
|
||||
|
||||
// recents
|
||||
async fetchWorkspaceRecents(workspaceSlug: string, entity_name?: string) {
|
||||
async fetchWorkspaceRecents(workspaceSlug: string, entity_name?: string): Promise<TActivityEntityData[]> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/recent-visits/`, {
|
||||
params: {
|
||||
entity_name,
|
||||
|
||||
@@ -188,6 +188,7 @@ export class IssueStore implements IIssueStore {
|
||||
updated_by: issue?.updated_by,
|
||||
is_draft: issue?.is_draft,
|
||||
is_subscribed: issue?.is_subscribed,
|
||||
is_epic: issue?.is_epic,
|
||||
};
|
||||
|
||||
this.rootIssueDetailStore.rootIssueStore.issues.addIssue([issuePayload]);
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
// root store
|
||||
import { RootStore } from "@/plane-web/store/root.store";
|
||||
import { IWorkspaceMembership } from "@/store/member/workspace-member.store";
|
||||
import { IStateStore, StateStore } from "../state.store";
|
||||
// issues data store
|
||||
import { IArchivedIssuesFilter, ArchivedIssuesFilter, IArchivedIssues, ArchivedIssues } from "./archived";
|
||||
import { ICycleIssuesFilter, CycleIssuesFilter, ICycleIssues, CycleIssues } from "./cycle";
|
||||
@@ -44,7 +43,7 @@ import {
|
||||
export interface IIssueRootStore {
|
||||
currentUserId: string | undefined;
|
||||
workspaceSlug: string | undefined;
|
||||
teamId: string | undefined;
|
||||
teamspaceId: string | undefined;
|
||||
projectId: string | undefined;
|
||||
cycleId: string | undefined;
|
||||
moduleId: string | undefined;
|
||||
@@ -112,7 +111,7 @@ export interface IIssueRootStore {
|
||||
export class IssueRootStore implements IIssueRootStore {
|
||||
currentUserId: string | undefined = undefined;
|
||||
workspaceSlug: string | undefined = undefined;
|
||||
teamId: string | undefined = undefined;
|
||||
teamspaceId: string | undefined = undefined;
|
||||
projectId: string | undefined = undefined;
|
||||
cycleId: string | undefined = undefined;
|
||||
moduleId: string | undefined = undefined;
|
||||
@@ -179,7 +178,7 @@ export class IssueRootStore implements IIssueRootStore {
|
||||
constructor(rootStore: RootStore, serviceType: TIssueServiceType = EIssueServiceType.ISSUES) {
|
||||
makeObservable(this, {
|
||||
workspaceSlug: observable.ref,
|
||||
teamId: observable.ref,
|
||||
teamspaceId: observable.ref,
|
||||
projectId: observable.ref,
|
||||
cycleId: observable.ref,
|
||||
moduleId: observable.ref,
|
||||
@@ -203,7 +202,7 @@ export class IssueRootStore implements IIssueRootStore {
|
||||
autorun(() => {
|
||||
if (rootStore?.user?.data?.id) this.currentUserId = rootStore?.user?.data?.id;
|
||||
if (this.workspaceSlug !== rootStore.router.workspaceSlug) this.workspaceSlug = rootStore.router.workspaceSlug;
|
||||
if (this.teamId !== rootStore.router.teamId) this.teamId = rootStore.router.teamId;
|
||||
if (this.teamspaceId !== rootStore.router.teamspaceId) this.teamspaceId = rootStore.router.teamspaceId;
|
||||
if (this.projectId !== rootStore.router.projectId) this.projectId = rootStore.router.projectId;
|
||||
if (this.cycleId !== rootStore.router.cycleId) this.cycleId = rootStore.router.cycleId;
|
||||
if (this.moduleId !== rootStore.router.moduleId) this.moduleId = rootStore.router.moduleId;
|
||||
|
||||
@@ -9,7 +9,7 @@ export interface IRouterStore {
|
||||
setQuery: (query: ParsedUrlQuery) => void;
|
||||
// computed
|
||||
workspaceSlug: string | undefined;
|
||||
teamId: string | undefined;
|
||||
teamspaceId: string | undefined;
|
||||
projectId: string | undefined;
|
||||
cycleId: string | undefined;
|
||||
moduleId: string | undefined;
|
||||
@@ -36,7 +36,7 @@ export class RouterStore implements IRouterStore {
|
||||
setQuery: action.bound,
|
||||
//computed
|
||||
workspaceSlug: computed,
|
||||
teamId: computed,
|
||||
teamspaceId: computed,
|
||||
projectId: computed,
|
||||
cycleId: computed,
|
||||
moduleId: computed,
|
||||
@@ -71,11 +71,11 @@ export class RouterStore implements IRouterStore {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the team id from the query
|
||||
* Returns the teamspace id from the query
|
||||
* @returns string|undefined
|
||||
*/
|
||||
get teamId() {
|
||||
return this.query?.teamId?.toString();
|
||||
get teamspaceId() {
|
||||
return this.query?.teamspaceId?.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+1
-1
@@ -55,7 +55,7 @@
|
||||
"posthog-js": "^1.131.3",
|
||||
"react": "^18.3.1",
|
||||
"react-color": "^2.19.3",
|
||||
"react-day-picker": "^8.10.0",
|
||||
"react-day-picker": "^9.5.0",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-dropzone": "^14.2.3",
|
||||
"react-hook-form": "7.51.5",
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 76 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 83 KiB |
+218
-275
@@ -1,9 +1,9 @@
|
||||
.rdp {
|
||||
.rdp-root {
|
||||
font-size: 12px;
|
||||
|
||||
--rdp-cell-size: 40px;
|
||||
/* Size of the day cells. */
|
||||
--rdp-caption-font-size: 1.15rem;
|
||||
--rdp-caption-font-size: 1rem;
|
||||
/* Font size for the caption labels. */
|
||||
--rdp-caption-navigation-size: 1.25rem;
|
||||
/* Font size for the caption labels. */
|
||||
@@ -21,260 +21,16 @@
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* Hide elements for devices that are not screen readers */
|
||||
.rdp-vhidden {
|
||||
.rdp-root {
|
||||
position: relative; /* Required to position the nav. */
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
-moz-appearance: none;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
position: absolute !important;
|
||||
top: 0;
|
||||
width: 1px !important;
|
||||
height: 1px !important;
|
||||
padding: 0 !important;
|
||||
overflow: hidden !important;
|
||||
clip: rect(1px, 1px, 1px, 1px) !important;
|
||||
border: 0 !important;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.rdp-button_reset {
|
||||
appearance: none;
|
||||
position: relative;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
cursor: default;
|
||||
color: inherit;
|
||||
background: none;
|
||||
font: inherit;
|
||||
|
||||
-moz-appearance: none;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
.rdp-button_reset:focus-visible {
|
||||
/* Make sure to reset outline only when :focus-visible is supported */
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.rdp-button {
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
.rdp-button[disabled]:not(.rdp-day_selected) {
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
.rdp-button:not([disabled]) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.rdp-button:focus-visible:not([disabled]):not(.rdp-day_selected) {
|
||||
color: inherit;
|
||||
background-color: var(--rdp-background-color);
|
||||
}
|
||||
|
||||
.rdp-button:focus-visible:not([disabled]).rdp-day_selected:not(.rdp-day_range_middle) {
|
||||
outline: var(--rdp-outline);
|
||||
outline-offset: 2px;
|
||||
background-color: var(--rdp-dark-background-color);
|
||||
outline-width: thin;
|
||||
}
|
||||
|
||||
.rdp-button:hover:not([disabled]).rdp-day_selected {
|
||||
background-color: var(--rdp-dark-background-color);
|
||||
}
|
||||
|
||||
.rdp-button:hover:not([disabled]):not(.rdp-day_selected) {
|
||||
background-color: var(--rdp-background-color);
|
||||
}
|
||||
|
||||
.rdp-months {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.rdp-month {
|
||||
margin: 0 1em;
|
||||
}
|
||||
|
||||
.rdp-month:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.rdp-month:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.rdp-table {
|
||||
margin: 0;
|
||||
max-width: calc(var(--rdp-cell-size) * 7);
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.rdp-with_weeknumber .rdp-table {
|
||||
max-width: calc(var(--rdp-cell-size) * 8);
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.rdp-caption {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.rdp-multiple_months .rdp-caption {
|
||||
position: relative;
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.rdp-caption_dropdowns {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.rdp-caption_label {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
padding: 0 0.25em;
|
||||
white-space: nowrap;
|
||||
color: currentColor;
|
||||
border: 0;
|
||||
border: 2px solid transparent;
|
||||
font-family: inherit;
|
||||
font-size: var(--rdp-caption-font-size);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.rdp-nav {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.rdp-multiple_months .rdp-caption_start .rdp-nav {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.rdp-multiple_months .rdp-caption_end .rdp-nav {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 0;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.rdp-nav_button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: var(--rdp-caption-navigation-size);
|
||||
height: var(--rdp-caption-navigation-size);
|
||||
padding: 0.25em;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.rdp-nav_button:hover,
|
||||
.rdp-nav_button:focus-visible {
|
||||
background-color: rgba(var(--color-background-80)) !important;
|
||||
}
|
||||
|
||||
/* ---------- */
|
||||
/* Dropdowns */
|
||||
/* ---------- */
|
||||
/* Day Buttons */
|
||||
/* ----------- */
|
||||
|
||||
.rdp-dropdown_year,
|
||||
.rdp-dropdown_month {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.rdp-dropdown {
|
||||
appearance: none;
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
cursor: inherit;
|
||||
opacity: 0;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
.rdp-dropdown[disabled] {
|
||||
opacity: unset;
|
||||
color: unset;
|
||||
}
|
||||
|
||||
.rdp-dropdown:focus-visible:not([disabled]) + .rdp-caption_label {
|
||||
background-color: var(--rdp-background-color);
|
||||
border: var(--rdp-outline);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.rdp-dropdown_icon {
|
||||
margin: 0 0 0 5px;
|
||||
}
|
||||
|
||||
.rdp-head {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.rdp-head_row,
|
||||
.rdp-row {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.rdp-head_cell {
|
||||
vertical-align: middle;
|
||||
font-size: 0.75em;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
height: 100%;
|
||||
height: var(--rdp-cell-size);
|
||||
padding: 0;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.rdp-tbody {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.rdp-tfoot {
|
||||
margin: 0.5em;
|
||||
}
|
||||
|
||||
.rdp-cell {
|
||||
width: var(--rdp-cell-size);
|
||||
height: 100%;
|
||||
height: var(--rdp-cell-size);
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.rdp-weeknumber {
|
||||
font-size: 0.75em;
|
||||
}
|
||||
|
||||
.rdp-weeknumber,
|
||||
.rdp-day {
|
||||
.rdp-day_button {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
align-items: center;
|
||||
@@ -285,14 +41,58 @@
|
||||
height: var(--rdp-cell-size);
|
||||
margin: 0;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 100%;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.rdp-day_today:not(.rdp-day_outside) {
|
||||
.rdp-day.rdp-outside:not(.rdp-selected) .rdp-day_button {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.rdp-day.rdp-disabled:not(.rdp-selected) .rdp-day_button {
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
.rdp-day:not(.rdp-disabled) .rdp-day_button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.rdp-day:not(.rdp-selected, .rdp-disabled) .rdp-day_button:focus-visible {
|
||||
color: inherit;
|
||||
background-color: var(--rdp-background-color);
|
||||
}
|
||||
|
||||
.rdp-selected:not(.rdp-range_middle, .rdp-disabled) .rdp-day_button:focus-visible {
|
||||
outline: var(--rdp-outline);
|
||||
outline-offset: 2px;
|
||||
background-color: var(--rdp-dark-background-color);
|
||||
outline-width: thin;
|
||||
}
|
||||
|
||||
.rdp-day:not(.rdp-disabled) .rdp-day_button:hover {
|
||||
background-color: var(--rdp-background-color);
|
||||
}
|
||||
|
||||
.rdp-selected .rdp-day_button {
|
||||
background-color: var(--rdp-accent-color);
|
||||
border-radius: 50%;
|
||||
color: var(--rdp-selected-color);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.rdp-selected .rdp-day_button:hover:not(.rdp-disabled) {
|
||||
background-color: var(--rdp-dark-background-color);
|
||||
}
|
||||
|
||||
.rdp-week {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.rdp-today:not(.rdp-outside) {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.rdp-day_today:not(.rdp-day_outside)::after {
|
||||
.rdp-today:not(.rdp-outside)::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
@@ -304,31 +104,173 @@
|
||||
transform: translate(-50%, 0);
|
||||
}
|
||||
|
||||
.rdp-day_selected,
|
||||
.rdp-day_selected:focus-visible,
|
||||
.rdp-day_selected:hover {
|
||||
.rdp-selected .rdp-day_button:focus-visible,
|
||||
.rdp-selected .rdp-day_button:hover {
|
||||
color: var(--rdp-selected-color);
|
||||
opacity: 1;
|
||||
background-color: var(--rdp-accent-color);
|
||||
}
|
||||
|
||||
.rdp-day_outside:not(.rdp-day_selected) {
|
||||
opacity: 0.5;
|
||||
.rdp-weekday {
|
||||
vertical-align: middle;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
font-size: 0.75em;
|
||||
height: var(--rdp-cell-size);
|
||||
padding: 0;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.rdp-day_selected:focus-visible {
|
||||
/* ---------- */
|
||||
/* Top Nav */
|
||||
/* ---------- */
|
||||
|
||||
.rdp-nav {
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
padding: inherit;
|
||||
|
||||
top: 1.2em;
|
||||
right: 1em;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.rdp-button_next,
|
||||
.rdp-button_previous {
|
||||
border: none;
|
||||
background: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
-moz-appearance: none;
|
||||
-webkit-appearance: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
appearance: none;
|
||||
width: var(--rdp-caption-navigation-size);
|
||||
height: var(--rdp-caption-navigation-size);
|
||||
padding: 0.25em;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.rdp-chevron {
|
||||
fill: rgba(var(--color-text-200));
|
||||
height: 0.75rem;
|
||||
width: 0.75rem;
|
||||
}
|
||||
|
||||
.rdp-button_next:hover,
|
||||
.rdp-button_previous:hover,
|
||||
.rdp-button_next:focus-visible,
|
||||
.rdp-button_previous:focus-visible {
|
||||
background-color: rgba(var(--color-background-80)) !important;
|
||||
}
|
||||
|
||||
/* ---------- */
|
||||
/* Dropdowns */
|
||||
/* ---------- */
|
||||
|
||||
.rdp-dropdowns {
|
||||
position: relative;
|
||||
/* width: 100%; */
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.rdp-dropdown {
|
||||
appearance: none;
|
||||
--webkit-appearance: none;
|
||||
--moz-appearance: none;
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
opacity: 0;
|
||||
border: none;
|
||||
font-family: inherit;
|
||||
font-size: 1rem;
|
||||
line-height: inherit;
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(var(--color-background-80)) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.rdp-dropdown_root {
|
||||
margin: 0;
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.rdp-months_dropdown {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.rdp-dropdown[data-disabled="true"] {
|
||||
opacity: unset;
|
||||
color: unset;
|
||||
}
|
||||
|
||||
.rdp-caption_label {
|
||||
z-index: 1;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
margin: 0;
|
||||
padding: 0 0.25em;
|
||||
white-space: nowrap;
|
||||
color: currentColor;
|
||||
border: 0;
|
||||
border: 2px solid transparent;
|
||||
font-family: inherit;
|
||||
font-size: var(--rdp-caption-font-size);
|
||||
font-weight: 600;
|
||||
background: transparent;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
td:has(.rdp-day_range_start),
|
||||
td:has(.rdp-day_range_middle),
|
||||
td:has(.rdp-day_range_end) {
|
||||
.rdp-dropdown:not([data-disabled="true"]) {
|
||||
&:focus-visible + .rdp-caption_label {
|
||||
border: var(--rdp-outline);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
& + .rdp-caption_label {
|
||||
background-color: rgba(var(--color-background-80)) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.rdp-dropdown_icon {
|
||||
margin: 0 0 0 5px;
|
||||
}
|
||||
|
||||
/* --------------- */
|
||||
/* Range selection */
|
||||
/* --------------- */
|
||||
|
||||
.rdp-range_start,
|
||||
.rdp-range_middle,
|
||||
.rdp-range_end {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
td:has(.rdp-day_range_start)::before,
|
||||
td:has(.rdp-day_range_middle)::before,
|
||||
td:has(.rdp-day_range_end)::before {
|
||||
.rdp-range_start::before,
|
||||
.rdp-range_middle::before,
|
||||
.rdp-range_end::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
background-color: var(--rdp-background-color);
|
||||
@@ -336,33 +278,34 @@ td:has(.rdp-day_range_end)::before {
|
||||
height: 100%;
|
||||
width: 50%;
|
||||
transform: translate(0, -50%);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
td:has(.rdp-day_range_start)::before {
|
||||
.rdp-range_start::before {
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
td:has(.rdp-day_range_middle)::before {
|
||||
.rdp-range_middle::before {
|
||||
left: 50%;
|
||||
width: 100%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
td:has(.rdp-day_range_end)::before {
|
||||
.rdp-range_end::before {
|
||||
right: 50%;
|
||||
}
|
||||
|
||||
td:has(.rdp-day_range_start.rdp-day_range_end)::before {
|
||||
.rdp-range_start.rdp-range_end::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.rdp-day_range_middle {
|
||||
.rdp-range_middle .rdp-day_button {
|
||||
background-color: transparent;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.rdp-day_range_middle:hover,
|
||||
.rdp-day_range_middle:focus-visible {
|
||||
background-color: var(--rdp-background-color) !important;
|
||||
color: inherit !important;
|
||||
.rdp-day.rdp-range_middle .rdp-day_button:hover,
|
||||
.rdp-day.rdp-range_middle .rdp-day_button:focus-visible {
|
||||
background-color: var(--rdp-background-color);
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
@@ -987,6 +987,11 @@
|
||||
enabled "2.0.x"
|
||||
kuler "^2.0.0"
|
||||
|
||||
"@date-fns/tz@^1.2.0":
|
||||
version "1.2.0"
|
||||
resolved "https://registry.npmjs.org/@date-fns/tz/-/tz-1.2.0.tgz#81cb3211693830babaf3b96aff51607e143030a6"
|
||||
integrity sha512-LBrd7MiJZ9McsOgxqWX7AaxrDjcFVjWH/tIKJd7pnR7McaslGYOP1QmmiBXdJH/H/yLCT+rcQ7FaPBUxRGUtrg==
|
||||
|
||||
"@emotion/babel-plugin@^11.13.5":
|
||||
version "11.13.5"
|
||||
resolved "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz"
|
||||
@@ -6032,6 +6037,11 @@ data-view-byte-offset@^1.0.1:
|
||||
es-errors "^1.3.0"
|
||||
is-data-view "^1.0.1"
|
||||
|
||||
date-fns-jalali@^4.1.0-0:
|
||||
version "4.1.0-0"
|
||||
resolved "https://registry.npmjs.org/date-fns-jalali/-/date-fns-jalali-4.1.0-0.tgz#9c7fb286004fab267a300d3e9f1ada9f10b4b6b0"
|
||||
integrity sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==
|
||||
|
||||
date-fns@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-4.1.0.tgz#64b3d83fff5aa80438f5b1a633c2e83b8a1c2d14"
|
||||
@@ -10684,10 +10694,14 @@ react-confetti@^6.1.0:
|
||||
dependencies:
|
||||
tween-functions "^1.2.0"
|
||||
|
||||
react-day-picker@^8.10.0:
|
||||
version "8.10.1"
|
||||
resolved "https://registry.npmjs.org/react-day-picker/-/react-day-picker-8.10.1.tgz"
|
||||
integrity sha512-TMx7fNbhLk15eqcMt+7Z7S2KF7mfTId/XJDjKE8f+IUcFn0l08/kI4FiYTL/0yuOLmEcbR4Fwe3GJf/NiiMnPA==
|
||||
react-day-picker@^9.5.0:
|
||||
version "9.5.1"
|
||||
resolved "https://registry.npmjs.org/react-day-picker/-/react-day-picker-9.5.1.tgz#ec40acdcc3ffbf7c0b9bfea8b6f97924249ea974"
|
||||
integrity sha512-PxuK8inYLlYgM2zZUVBPsaBM5jI40suPeG+naKyx7kpyF032RRlEAUEjkpW9/poTASh/vyWAOVqjGuGw+47isw==
|
||||
dependencies:
|
||||
"@date-fns/tz" "^1.2.0"
|
||||
date-fns "^4.1.0"
|
||||
date-fns-jalali "^4.1.0-0"
|
||||
|
||||
react-docgen-typescript@^2.2.2:
|
||||
version "2.2.2"
|
||||
@@ -11705,7 +11719,16 @@ streamx@^2.15.0, streamx@^2.20.0:
|
||||
optionalDependencies:
|
||||
bare-events "^2.2.0"
|
||||
|
||||
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
|
||||
"string-width-cjs@npm:string-width@^4.2.0":
|
||||
version "4.2.3"
|
||||
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
dependencies:
|
||||
emoji-regex "^8.0.0"
|
||||
is-fullwidth-code-point "^3.0.0"
|
||||
strip-ansi "^6.0.1"
|
||||
|
||||
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
|
||||
version "4.2.3"
|
||||
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
@@ -11817,7 +11840,14 @@ string_decoder@^1.1.1, string_decoder@^1.3.0:
|
||||
dependencies:
|
||||
safe-buffer "~5.2.0"
|
||||
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
|
||||
version "6.0.1"
|
||||
resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
dependencies:
|
||||
ansi-regex "^5.0.1"
|
||||
|
||||
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
@@ -13110,7 +13140,16 @@ word-wrap@^1.2.5:
|
||||
resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz"
|
||||
integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
|
||||
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
|
||||
version "7.0.0"
|
||||
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
dependencies:
|
||||
ansi-styles "^4.0.0"
|
||||
string-width "^4.1.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
wrap-ansi@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
|
||||
Reference in New Issue
Block a user