Compare commits
26 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 |
@@ -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
|
||||
@@ -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",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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%;
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
);
|
||||
}}
|
||||
|
||||
@@ -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,12 +3,11 @@ 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";
|
||||
@@ -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,10 +81,10 @@ 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,
|
||||
})}
|
||||
>
|
||||
<StickyEditorToolbar
|
||||
executeCommand={(item) => {
|
||||
|
||||
@@ -58,7 +58,7 @@ export const StickyEditorToolbar: 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} />}
|
||||
|
||||
@@ -2,14 +2,18 @@
|
||||
|
||||
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 { NoProjectsEmptyState, RecentsEmptyState } from "../empty-states";
|
||||
import { EWidgetKeys, WidgetLoader } from "../loaders";
|
||||
@@ -34,12 +38,16 @@ type TRecentWidgetProps = THomeWidgetProps & {
|
||||
|
||||
export const RecentActivityWidget: React.FC<TRecentWidgetProps> = observer((props) => {
|
||||
const { presetFilter, showFilterSelect = true, workspaceSlug } = props;
|
||||
// state
|
||||
// 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,
|
||||
@@ -71,7 +79,7 @@ export const RecentActivityWidget: React.FC<TRecentWidgetProps> = observer((prop
|
||||
}
|
||||
};
|
||||
|
||||
if (!loader && joinedProjectIds?.length === 0) return <NoProjectsEmptyState />;
|
||||
if (!loader && !isWikiApp && joinedProjectIds?.length === 0) return <NoProjectsEmptyState />;
|
||||
|
||||
if (!isLoading && recents?.length === 0)
|
||||
return (
|
||||
|
||||
@@ -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={
|
||||
|
||||
@@ -10,6 +10,7 @@ import { getFileURL } from "@plane/utils";
|
||||
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";
|
||||
|
||||
@@ -36,27 +37,26 @@ export const RecentPage = (props: BlockProps) => {
|
||||
<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>
|
||||
{pageDetails?.project_identifier && (
|
||||
<div className="font-medium text-custom-sidebar-text-400 text-sm whitespace-nowrap">
|
||||
{pageDetails?.project_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">
|
||||
{pageDetails?.logo_props?.in_use ? (
|
||||
<Logo logo={pageDetails?.logo_props} size={16} type="lucide" />
|
||||
) : (
|
||||
<FileText className="size-4 text-custom-text-350" />
|
||||
)}
|
||||
</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>
|
||||
{pageDetails?.project_identifier && (
|
||||
<div className="font-medium text-custom-text-400 text-sm whitespace-nowrap">
|
||||
{pageDetails?.project_identifier}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
appendTitleElement={
|
||||
<div className="flex-shrink-0 font-medium text-xs text-custom-text-400">
|
||||
{calculateTimeAgo(activity.visited_at)}
|
||||
</div>
|
||||
}
|
||||
quickActionElement={
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -20,7 +20,6 @@ import { useRelationOperations } from "./helper";
|
||||
|
||||
type Props = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
issueId: string;
|
||||
disabled: boolean;
|
||||
issueServiceType?: TIssueServiceType;
|
||||
@@ -37,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;
|
||||
@@ -127,7 +126,6 @@ export const RelationsCollapsibleContent: FC<Props> = observer((props) => {
|
||||
>
|
||||
<RelationIssueList
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
relationKey={relation.relationKey}
|
||||
issueIds={relation.issueIds}
|
||||
@@ -149,10 +147,20 @@ export const RelationsCollapsibleContent: FC<Props> = observer((props) => {
|
||||
}}
|
||||
data={issueCrudState?.delete?.issue as TIssue}
|
||||
onSubmit={async () => {
|
||||
const deleteOperation = !!issueCrudState.delete.issue?.is_epic
|
||||
? epicOperations.remove
|
||||
: issueOperations.remove;
|
||||
await deleteOperation(workspaceSlug, projectId, issueCrudState?.delete?.issue?.id as string);
|
||||
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}
|
||||
/>
|
||||
@@ -169,7 +177,8 @@ export const RelationsCollapsibleContent: FC<Props> = observer((props) => {
|
||||
}}
|
||||
data={issueCrudState?.update?.issue ?? undefined}
|
||||
onSubmit={async (_issue: TIssue) => {
|
||||
await epicOperations.update(workspaceSlug, projectId, _issue.id, _issue);
|
||||
if (!_issue.id || !_issue.project_id) return;
|
||||
await epicOperations.update(workspaceSlug, _issue.project_id, _issue.id, _issue);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
@@ -181,7 +190,8 @@ export const RelationsCollapsibleContent: FC<Props> = observer((props) => {
|
||||
}}
|
||||
data={issueCrudState?.update?.issue ?? undefined}
|
||||
onSubmit={async (_issue: TIssue) => {
|
||||
await issueOperations.update(workspaceSlug, projectId, _issue.id, _issue);
|
||||
if (!_issue.id || !_issue.project_id) return;
|
||||
await issueOperations.update(workspaceSlug, _issue.project_id, _issue.id, _issue);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -21,7 +21,6 @@ import { useRelationOperations } from "../issue-detail-widgets/relations/helper"
|
||||
|
||||
type Props = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
issueId: string;
|
||||
relationKey: TIssueRelationTypes;
|
||||
relationIssueId: string;
|
||||
@@ -33,7 +32,6 @@ type Props = {
|
||||
export const RelationIssueListItem: FC<Props> = observer((props) => {
|
||||
const {
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
issueId,
|
||||
relationKey,
|
||||
relationIssueId,
|
||||
@@ -57,15 +55,16 @@ export const RelationIssueListItem: FC<Props> = observer((props) => {
|
||||
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;
|
||||
|
||||
@@ -11,7 +11,6 @@ import { TIssueRelationTypes } from "@/plane-web/types";
|
||||
|
||||
type Props = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
issueId: string;
|
||||
issueIds: string[];
|
||||
relationKey: TIssueRelationTypes;
|
||||
@@ -23,7 +22,6 @@ type Props = {
|
||||
export const RelationIssueList: FC<Props> = observer((props) => {
|
||||
const {
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
issueId,
|
||||
issueIds,
|
||||
relationKey,
|
||||
@@ -40,7 +38,6 @@ export const RelationIssueList: FC<Props> = observer((props) => {
|
||||
<RelationIssueListItem
|
||||
key={relationIssueId}
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
relationKey={relationKey}
|
||||
relationIssueId={relationIssueId}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
}}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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",
|
||||
|
||||
+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