Compare commits

...

26 Commits

Author SHA1 Message Date
akshat5302 2ff9f1d775 fix: sed cmd warnings fixed 2025-01-31 15:20:57 +05:30
Aaryan Khandelwal ddad1767a2 fix: table flixker on resize (#6524) 2025-01-31 02:31:17 +05:30
Aaryan Khandelwal 6a37a2ce21 fix: link without protocol (#6517) 2025-01-30 20:25:00 +05:30
Aaryan Khandelwal 01bd1bde64 fix: table resize overflow issues (#6520) 2025-01-30 19:27:12 +05:30
Abenezer Belachew 9268180aec path already defined on line 51 (#6427) 2025-01-30 13:36:16 +05:30
Aaryan Khandelwal ff778b98f5 [WEB-3095] fix: recents widget title truncate (#6512)
* fix: recents widget title truncate

* chore: revert prop changes

* chore: revert list item changes
2025-01-30 13:34:42 +05:30
Sangeetha 8f5ce6b232 feat: user preference url and sort order change (#6505)
* fix: change url

* Change order of user preference keys
2025-01-30 13:29:39 +05:30
Anmol Singh Bhatia 58a4ca9f36 chore: board layout padding improvement (#6507) 2025-01-29 16:56:27 +05:30
guru_sainath 312b077657 [WEB-3177] fix: resolve cycle creation issue for equal start_date and completed_date (#6504)
* fix: fixed cycle startdate if the the start_date and completed cyles dates are today

* chore: updated validation for date match
2025-01-29 16:35:25 +05:30
Bavisetti Narayan c65e42f807 chore: add attachment in intake issue (#6503) 2025-01-29 15:53:34 +05:30
Prateek Shourya f4af78c0fc [WEB-3218] fix: redirection for cross projects issue relations (#6457) 2025-01-29 13:00:24 +05:30
Prateek Shourya c0b6abc3d5 refactor: minor store level changes (#6500) 2025-01-29 01:04:54 +05:30
sriram veeraghanta 2f2e6626c6 fix: typo fixes 2025-01-28 21:07:59 +05:30
Sangeetha 6a8d3202b7 feat: workspace user preference api (#6497)
* feat: workspace user preference api

* feat: remove sort order calculation

* Return 404 error
2025-01-28 20:50:24 +05:30
Bavisetti Narayan 51b52a7fc3 [WEB-3249] chore: delete the user recent visits (#6496)
* chore: delete the user recent visits

* chore: hard deleted the recent visits
2025-01-28 20:50:15 +05:30
Bavisetti Narayan 23ede81737 chore: update project state (#6467) 2025-01-28 20:49:36 +05:30
Aaryan Khandelwal b698f44500 [PE-155] chore: floating toolbar for pages (#6482)
* chore: add floating toolbar to pages

* fix: locked page toolbar
2025-01-28 20:21:09 +05:30
M. Palanikannan 421839ec51 [PE-255] fix: remove drag handles from content within table cells (#6487)
* fix: remove drag handles from content within table cells

* style: table cell padding

* style: table cell padding

* fix: insert resizable tables

---------

Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
2025-01-28 20:20:40 +05:30
M. Palanikannan 940b5e4e44 fix: custom color extension markdown rule added now (#6471) 2025-01-28 20:20:23 +05:30
Aaryan Khandelwal 6003c88d62 fix: disable comment submit while uploading an image (#6445) 2025-01-28 20:19:01 +05:30
Aaryan Khandelwal 74913a6659 fix: page name and recents empty state (#6491) 2025-01-28 17:13:20 +05:30
sriram veeraghanta 97578684c6 chore: lock file updates 2025-01-28 16:18:30 +05:30
Aaryan Khandelwal 88b4d32220 [WEB-3237, 3238] dev: date picker enhancements (#6470)
* [WEB-3238] dev: datepicker with month and year selection dropdowns (#6391)

* feat: react-day-picker upgrade and caption dropdowns

* style fixes

* style: css and autofocus improved

* fix: fixed weeks for datepicker to ensure static height

---------

Co-authored-by: Vineet K <55555696+vineetk13@users.noreply.github.com>
2025-01-28 16:15:18 +05:30
Aaryan Khandelwal f32635a6a8 [WEB-3203] fix: stickies height, overflow (#6484)
* fix: stickies height

* chore: remove unused drop indicators
2025-01-28 15:33:25 +05:30
Bavisetti Narayan 7fe58e0ea9 chore: added estimate point value in expand issues (#6483) 2025-01-28 15:11:33 +05:30
Prateek Shourya 7f22cd1ac1 [WEB-3229] fix: issue creation from modal (#6460) 2025-01-27 13:13:32 +05:30
65 changed files with 866 additions and 532 deletions
@@ -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
+2
View File
@@ -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
+5
View File
@@ -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"]
+12
View File
@@ -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",
),
]
+1
View File
@@ -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,
+8
View File
@@ -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)
+8
View File
@@ -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)}),
+8
View File
@@ -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)
+8
View File
@@ -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)
+17
View File
@@ -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):
+8
View File
@@ -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
)
-1
View File
@@ -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(),
+2 -1
View File
@@ -69,7 +69,8 @@ from .workspace import (
WorkspaceTheme,
WorkspaceUserProperties,
WorkspaceUserLink,
WorkspaceHomePreference
WorkspaceHomePreference,
WorkspaceUserPreference,
)
from .favorite import UserFavorite
+2 -3
View File
@@ -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",
+26 -9
View File
@@ -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):
+8 -7
View File
@@ -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
+2 -1
View File
@@ -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;
+7 -5
View File
@@ -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);
+2 -2
View File
@@ -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"
/>
);
}}
+8 -3
View File
@@ -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">
+8 -3
View File
@@ -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(),
@@ -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>
);
});
+12 -3
View File
@@ -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}
+1 -1
View File
@@ -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,
}}
+3 -3
View File
@@ -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;
};
+27 -1
View File
@@ -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,
};
};
+4 -5
View File
@@ -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;
+5 -5
View File
@@ -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
View File
@@ -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
View File
@@ -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;
}
+46 -7
View File
@@ -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==