Compare commits

...
Author SHA1 Message Date
Vihar Kurama 036da787fe refactor command palette into modular command system 2025-09-13 18:28:37 +01:00
c3e7cfd16b [WEB-4723] fix: disable project features on project create (#7625)
* fix: disbale project features on project create

* Implement migration 0105 to alter project cycle view fields to Boolean with default values

* Add project view settings in workspace seed task

* Add is_current_version_deprecated field to Instance model

Index user_id field in Session model

---------

Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
2025-09-12 13:01:03 +05:30
Jayash TripathyandGitHub 9ffc30f7b1 [WEB-4889] refactor: add fill in bar chart bar stroke (#7776)
* ♻️ refactor: add fill in barchart bar stroke

* ♻️ refactor: added fill in the circle
2025-09-12 00:04:16 +05:30
Dheeraj Kumar KetireddyandGitHub b60f12a88e [WEB-4861] fix: update redirection path in MagicSignInEndpoint to home page (#7774)
* fix: update redirection path in MagicSignInEndpoint to home page

* Use / for clarity
2025-09-11 18:56:32 +05:30
Vamsi KrishnaandGitHub 76a0b38dd1 [WEB-4890]fix: dropdown width #7778 2025-09-11 18:56:04 +05:30
SangeethaandGitHub 8ee665f491 [WEB-4875] fix: unsubscribed work items on workspace subscribed work item filter #7775 2025-09-11 17:52:51 +05:30
Vamsi KrishnaandGitHub 85f23b450d [WEB-4852] chore: views refactor (#7729)
* chore: refactored view store and services

* chore: removed unused import

* chore: refactored update view component

* fix: lint errors
2025-09-11 17:09:56 +05:30
Vamsi KrishnaandGitHub 8bf059535a [WEB-4858]chore: updated content for error page (#7766)
* chore: updated content for error page

* chore: updated btn url
2025-09-11 14:22:46 +05:30
Vamsi KrishnaandGitHub 4cfea87108 [WEB-4857] fix: applied filters root update #7750 2025-09-11 14:21:30 +05:30
Vamsi KrishnaandGitHub 4fe2ef706b [WEB-4441]fix: members account type dropdown position #7759 2025-09-11 14:20:10 +05:30
sriram veeraghantaGitHubCopilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
8d354b3eb2 Potential fix for code scanning alert no. 636: URL redirection from remote source (#7760)
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2025-09-11 14:18:13 +05:30
SangeethaandGitHub ec541c2557 [WEB-4854] chore: project admin accesss to workspace admins (#7749)
* chore: project admin accesss to workspace admins

* chore: frontend changes

* chore: remove console.log

* chore: refactor permission decorator

* chore: role enum

* chore: rearrange role_choices
2025-09-11 14:16:36 +05:30
11cd8d11e4 [WIKI-632] chore: accept additional props for document collaborative editor (#7718)
* chore: add collaborative document editor extended props

* fix: additional rich text extension props

* fix: formatting

* chore: add types to the trailing node extension

---------

Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
2025-09-11 14:15:39 +05:30
Jayash TripathyandGitHub 0f7bfdde91 [WEB-4877] fix: webapp crash because of bar chart (#7763)
* 🔧 fix: dynamic bar color handling and refactored color retrieval logic.

* ♻️  refactor: updated any to Record in getBarColor
2025-09-10 17:44:52 +05:30
Vamsi KrishnaandGitHub ac835bf287 [WEB-4874]fix: calendar picker build errors and styles override (#7762)
* fix: calender picker build errors

* fix: styles override in the picker implementation
2025-09-10 17:10:23 +05:30
sriram veeraghanta db18c3555c Merge branch 'canary' of github.com:makeplane/plane into preview 2025-09-10 14:45:14 +05:30
sriram veeraghantaandCopilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> b696ae91ed Potential fix for code scanning alert no. 646: Server-side request forgery (#7758)
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2025-09-10 14:44:34 +05:30
sriramveeraghanta 61e91bd09c chore (deps): django and vite version bump 2025-09-10 14:44:34 +05:30
sriram veeraghantaGitHubCopilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
20d773042b Potential fix for code scanning alert no. 646: Server-side request forgery (#7758)
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2025-09-10 14:42:46 +05:30
sriramveeraghanta 30b175108b chore (deps): django and vite version bump 2025-09-10 13:38:30 +05:30
6d116beea3 [WEB-4320] dev: propel emoji reaction component (#7741)
* dev: animated counter added to propel

* chore: animated counter story added

* chore: propel config updated

* chore: code refactor

* dev: emoji reaction and renderer component added to propel

* dev: emoji reaction story added

* chore: propel config updated

* chore: code refactor

* fix: format error

* chore: lint error resolved

---------

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
2025-09-10 00:54:47 +05:30
b0db4fcf10 [WEB-4736] dev: propel button (#7746)
* dev: button added to propel

* dev: button story added

* chore: propel config updated

---------

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
2025-09-10 00:34:17 +05:30
Anmol Singh BhatiaandGitHub 7e03264758 [WEB-4730] dev: propel context menu component (#7745)
* dev: context menu component added

* dev: context menu story added

* chore: propel config updated
2025-09-10 00:15:55 +05:30
Anmol Singh BhatiaandGitHub 1c8ac3d247 [WEB-4737] dev: propel pill component (#7743)
* dev: pill component added to propel

* dev: pill story added

* chore: propel config updated

* chore: code refactor
2025-09-10 00:15:37 +05:30
3b8bb1effc [WEB-4733] dev: propel toolbar component (#7742)
* dev: toolbar component added to propel

* dev: toolbar story added

* chore: propel config updated

* chore: code refactor

---------

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
2025-09-10 00:15:10 +05:30
Anmol Singh BhatiaandGitHub 5a63e6dad2 [WEB-4858] chore: error page ui revamp #7747 2025-09-10 00:03:39 +05:30
Anmol Singh BhatiaandGitHub 45688bdc72 [WEB-4860] dev: propel animated counter component (#7740)
* dev: animated counter added to propel

* chore: animated counter story added

* chore: propel config updated

* chore: code refactor

* chore: code refactor

* fix: format error
2025-09-09 23:51:45 +05:30
Jayash TripathyandGitHub 43b7a6ad0a [WEB-4855] refactor: chart tick improvements (#7732)
* 🚧 WIP: Introduced customTicks prop in BarChart for flexible tick rendering.

*  feat: added customTicks to axis charts for flexible tick rendering

* 🔧 fix: update default bar fill color to black and ensure consistent color usage in BarChart

*  feat: add customTooltipContent prop to LineChart for enhanced tooltip flexibility

* 🔧 fix: update bar fill color handling to support dynamic colors based on data and removed DEFAULT_BAR_FILL_COLOR

* 🔧 fix: correct bar fill color handling in BarChart to ensure proper color assignment for tooltips

* 🔧 fix: update customTicks prop types in TAxisChartProps to use unknown type for better type safety

* 📝 chore: updated translations and cleaned up insight card

* 🚨 fix: lint

* 🔧 fix: remove unused translation key "no_of" from Russian translations
2025-09-09 23:51:13 +05:30
Anmol Singh BhatiaandGitHub 498613284e [WEB-4841] chore: calendar component migration UI to propel (#7730)
* chore: move calendar components and dependencies

* chore: update package configurations

* chore: calendar import updated

* chore: propel config updated

* chore: propel calendar code refactor

* chore: code refactor

* fix: build error
2025-09-09 23:50:51 +05:30
Anmol Singh BhatiaandGitHub 9ab3143a73 [WEB-4816] chore: add label flow (#7716)
* chore: remove create label modal

* fix: label spinner

* chore: add label flow improvements

* chore: code refactor

* chore: code refactor

* chore: code refactor
2025-09-09 23:50:39 +05:30
56cd0fc445 [WEB-4025] fix: external user comment and reaction (#7692)
* chore: reactions types updated

* fix: external user comments

* fix: external user reactions

* chore: added display name for actor

* chore: merge conflicts

* chore: updated the created_by and updated_by

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
2025-09-09 23:50:11 +05:30
Vamsi KrishnaandGitHub 260d9a053d [WEB-4802] fix: module sort order (#7674)
* fix: module sort order

* chore: removed secondary sort for progress

* chore: sort refactor
2025-09-09 23:49:30 +05:30
Vipin ChaudharyandGitHub 34bdc2ad76 [WIKI-636] fix: replace project loader with page loader #7752 2025-09-09 19:27:00 +05:30
99bc4262c5 [WIKI-654] fix: version history to load from html (#7754)
* fix: versions history to load from html

* chore: added json and stripped to store in version history (#7755)

---------

Co-authored-by: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com>
2025-09-09 19:26:19 +05:30
Aaryan KhandelwalandGitHub 49127ebeea [WIKI-653] fix: page content loader #7753 2025-09-09 18:20:31 +05:30
Vamsi KrishnaandGitHub a8a6536379 [WEB-4688]feat: added collapsible to propel (#7643)
* chore: added collapsibl to propel

* fix: export path

* chore: made collapsible a compound component

* fix: lint and format errors

* chore: updated propel exports order and added collapsible to tsdown config
2025-09-09 13:59:00 +05:30
Vamsi KrishnaandGitHub 11b83cf4f2 [WEB-4692]feat: added toast to propel package (#7640)
* * chore: migrated toast to base ui toast
* chore: added toast to propel package

* fix: lint errors

* fix: export path

* fix: lint errors

* chore: imporoved props handling

* fix: lint errors

* chore: updated import paths

* chore: replaced clsx with cn

* * chore: updated tsdown config
* chore: updated exports order
2025-09-09 13:58:11 +05:30
Anmol Singh BhatiaandGitHub fbd48c33f5 [WEB-4729] dev: propel scrollarea (#7748)
* dev: scrollarea added to propel

* dev: scrollarea story added

* chore: propel config updated

* fix: format error
2025-09-09 13:56:28 +05:30
Vamsi KrishnaandGitHub 37ce8a9fe6 [WEB-4800]chore: settings header component refactor #7669 2025-09-09 13:42:40 +05:30
Bavisetti NarayanandGitHub 291101a8e5 chore: added attributes in ol tag (#7736) 2025-09-08 17:17:38 +05:30
Vipin ChaudharyandGitHub 0cb4976e38 [WIKI-647] chore: update image path (#7734)
* chore: update image path

* chore: update asset path
2025-09-08 15:44:56 +05:30
Anmol Singh BhatiaandGitHub 20d139cc9e [WEB-4839] chore: maintenance mode empty state updated (#7728)
* chore: maintenance mode empty state updated

* chore: code refactor

* chore: code refactor

* chore: code refactor
2025-09-05 20:16:39 +05:30
ff181e566f [WEB-4738] feat: add Skeleton component with respective stories in propel (#7704)
*  feat: add Skeleton component with respective stories in propel

* 🚨 fix: lint issues

* ♻️ refactor : enhance Skeleton component with improved props and structure

*  feat: add Skeleton component entry to package.json

* 🚨 fix: lint

* ♻️ refactor: rename Skeleton to skeleton

* fix: rename fixes

* ♻️ refactor: rename skeleton

* 🚨 fix: lint

* ♻️ refactor: add skeleton entry to tsdown config and package.json

* ♻️  refactor: sorted propel exports

---------

Co-authored-by: sriramveeraghanta <veeraghanta.sriram@gmail.com>
2025-09-05 15:51:05 +05:30
Jayash TripathyandGitHub a696b6039c [WEB-4835] feat: storybook enhancements (#7702)
*  feat: integrate Storybook addons and enhance Tabs component

- Added `@storybook/addon-designs` and `@storybook/addon-docs` to the project dependencies.
- Enhanced the Tabs component stories to support dynamic tab options.
- Introduced a custom theme for Storybook with a new manager configuration.
- Added a new SVG logo for branding in Storybook.
- Pinned the storybook and related packages version

* ♻️  refactor: update Storybook manager configuration by removing unused import

* ♻️  refactor: enhance Tabs story with size labels and layout adjustments

* 🚨 fix: lint issues

* 🚨 fix: lock file

* ✏️  fix: update brand image path in Storybook manager configuration

* ♻️ refactor: improve Tabs story by ensuring safe default value and enhancing content rendering
2025-09-05 14:55:33 +05:30
Vipin ChaudharyandGitHub af1dcd335e [WIKI-640] chore: refactor link view #7727 2025-09-05 14:14:30 +05:30
Aaryan KhandelwalandGitHub a1500c2206 [WIKI-644] fix: remove unnecessary props from the editor package #7726 2025-09-05 13:50:44 +05:30
Aaryan KhandelwalandGitHub 88f194ca8e [WIKI-637] fix: work item description version history (#7725)
* fix: work item description version history

* chore: add key to the editor
2025-09-04 18:57:50 +05:30
Aaryan KhandelwalandGitHub b68d2ca921 [WIKI-642] fix: editor bubble menu overflow issues #7724 2025-09-04 18:16:03 +05:30
Aaryan KhandelwalandGitHub 78c6aede52 [WIKI-641] fix: overwrite tailwind typography plugin styles #7723 2025-09-04 18:14:48 +05:30
sriramveeraghanta c6c46f9aab Merge branch 'canary' of github.com:makeplane/plane into preview 2025-09-04 14:48:27 +05:30
Anmol Singh BhatiaandGitHub 7d7e37439d [WEB-4840] chore: icon migration from @plane/ui to @plane/propel/icons (#7721)
* chore: move icons from ui package to propel package

* chore: package and tsdown config updated

* chore: migrate all icon imports from @plane/ui to @plane/propel/icons

* chore: remove icon components from @plane/ui package (migrated to
  @plane/propel/icons)

* chore: code refactoring

* chore: migrate remaining icon components from @Plane/ui to @Plane/propel/icons

* fix: lint error

* chore: code refactor
2025-09-04 14:44:53 +05:30
Akshat JainandGitHub d258080ee7 [INFRA-235] fix: docker compose file restart_policy changed to any (#7689)
* chore: update docker-compose.yml to change restart policy condition from 'on-failure' to 'any' and remove SSL variable from variables.env

* fix: update docker-compose.yml to change restart policy condition from 'any' to 'on-failure'
2025-09-04 14:18:45 +05:30
Bavisetti Narayanandsriramveeraghanta c40c7804e7 [WEB-4806] chore: updated html attribute validation #7719 2025-09-04 13:53:21 +05:30
Bavisetti NarayanandGitHub 36c735bede [WEB-4806] chore: updated html attribute validation #7719 2025-09-04 13:52:43 +05:30
Bavisetti Narayanandsriramveeraghanta cab5d20217 [WEB-4806] chore: improved the html validation (#7676)
* chore: improved the html validation

* chore: removed the models changes

* chore: removed extra filters

* chore: changed the protocols
2025-09-03 19:57:37 +05:30
426 changed files with 4250 additions and 1719 deletions
@@ -7,8 +7,8 @@ import { ExternalLink, FileText, HelpCircle, MoveLeft } from "lucide-react";
import { Transition } from "@headlessui/react";
// plane internal packages
import { WEB_BASE_URL } from "@plane/constants";
import { DiscordIcon, GithubIcon } from "@plane/propel/icons";
import { Tooltip } from "@plane/propel/tooltip";
import { DiscordIcon, GithubIcon } from "@plane/ui";
import { cn } from "@plane/utils";
// hooks
import { useTheme } from "@/hooks/store";
@@ -5,8 +5,8 @@ import Link from "next/link";
import { usePathname } from "next/navigation";
import { Image, BrainCog, Cog, Lock, Mail } from "lucide-react";
// plane internal packages
import { WorkspaceIcon } from "@plane/propel/icons";
import { Tooltip } from "@plane/propel/tooltip";
import { WorkspaceIcon } from "@plane/ui";
import { cn } from "@plane/utils";
// hooks
import { useTheme } from "@/hooks/store";
+1 -1
View File
@@ -1,7 +1,7 @@
"use client";
import Link from "next/link";
import { PlaneLockup } from "@plane/ui";
import { PlaneLockup } from "@plane/propel/icons";
export const AuthHeader = () => (
<div className="flex items-center justify-between gap-6 w-full flex-shrink-0 sticky top-0">
+20 -2
View File
@@ -39,13 +39,31 @@ def allow_permission(allowed_roles, level="PROJECT", creator=False, model=None):
).exists():
return view_func(instance, request, *args, **kwargs)
else:
if ProjectMember.objects.filter(
is_user_has_allowed_role = ProjectMember.objects.filter(
member=request.user,
workspace__slug=kwargs["slug"],
project_id=kwargs["project_id"],
role__in=allowed_role_values,
is_active=True,
).exists():
).exists()
# Return if the user has the allowed role else if they are workspace admin and part of the project regardless of the role
if is_user_has_allowed_role:
return view_func(instance, request, *args, **kwargs)
elif (
ProjectMember.objects.filter(
member=request.user,
workspace__slug=kwargs["slug"],
project_id=kwargs["project_id"],
is_active=True,
).exists()
and WorkspaceMember.objects.filter(
member=request.user,
workspace__slug=kwargs["slug"],
role=ROLE.ADMIN.value,
is_active=True,
).exists()
):
return view_func(instance, request, *args, **kwargs)
# Return permission denied if no conditions are met
+22 -13
View File
@@ -3,11 +3,7 @@ from rest_framework.permissions import SAFE_METHODS, BasePermission
# Module import
from plane.db.models import ProjectMember, WorkspaceMember
# Permission Mappings
Admin = 20
Member = 15
Guest = 5
from plane.db.models.project import ROLE
class ProjectBasePermission(BasePermission):
@@ -26,18 +22,31 @@ class ProjectBasePermission(BasePermission):
return WorkspaceMember.objects.filter(
workspace__slug=view.workspace_slug,
member=request.user,
role__in=[Admin, Member],
role__in=[ROLE.ADMIN.value, ROLE.MEMBER.value],
is_active=True,
).exists()
## Only Project Admins can update project attributes
return ProjectMember.objects.filter(
project_member_qs = ProjectMember.objects.filter(
workspace__slug=view.workspace_slug,
member=request.user,
role=Admin,
project_id=view.project_id,
is_active=True,
).exists()
)
## Only project admins or workspace admin who is part of the project can access
if project_member_qs.filter(role=ROLE.ADMIN.value).exists():
return True
else:
return (
project_member_qs.exists()
and WorkspaceMember.objects.filter(
member=request.user,
workspace__slug=view.workspace_slug,
role=ROLE.ADMIN.value,
is_active=True,
).exists()
)
class ProjectMemberPermission(BasePermission):
@@ -55,7 +64,7 @@ class ProjectMemberPermission(BasePermission):
return WorkspaceMember.objects.filter(
workspace__slug=view.workspace_slug,
member=request.user,
role__in=[Admin, Member],
role__in=[ROLE.ADMIN.value, ROLE.MEMBER.value],
is_active=True,
).exists()
@@ -63,7 +72,7 @@ class ProjectMemberPermission(BasePermission):
return ProjectMember.objects.filter(
workspace__slug=view.workspace_slug,
member=request.user,
role__in=[Admin, Member],
role__in=[ROLE.ADMIN.value, ROLE.MEMBER.value],
project_id=view.project_id,
is_active=True,
).exists()
@@ -97,7 +106,7 @@ class ProjectEntityPermission(BasePermission):
return ProjectMember.objects.filter(
workspace__slug=view.workspace_slug,
member=request.user,
role__in=[Admin, Member],
role__in=[ROLE.ADMIN.value, ROLE.MEMBER.value],
project_id=view.project_id,
is_active=True,
).exists()
+20 -3
View File
@@ -667,16 +667,33 @@ class IssueReactionSerializer(BaseSerializer):
class IssueReactionLiteSerializer(DynamicBaseSerializer):
display_name = serializers.CharField(source="actor.display_name", read_only=True)
class Meta:
model = IssueReaction
fields = ["id", "actor", "issue", "reaction"]
fields = ["id", "actor", "issue", "reaction", "display_name"]
class CommentReactionSerializer(BaseSerializer):
display_name = serializers.CharField(source="actor.display_name", read_only=True)
class Meta:
model = CommentReaction
fields = "__all__"
read_only_fields = ["workspace", "project", "comment", "actor", "deleted_at"]
fields = [
"id",
"actor",
"comment",
"reaction",
"display_name",
"deleted_at",
"workspace",
"project",
"created_at",
"updated_at",
"created_by",
"updated_by",
]
read_only_fields = ["workspace", "project", "comment", "actor", "deleted_at", "created_by", "updated_by"]
class IssueVoteSerializer(BaseSerializer):
@@ -15,7 +15,6 @@ from plane.db.models import (
)
from plane.utils.content_validator import (
validate_html_content,
validate_binary_data,
)
+39 -13
View File
@@ -5,13 +5,12 @@ from django.utils import timezone
import json
# Django imports
from django.db import IntegrityError
from django.db.models import Exists, F, OuterRef, Prefetch, Q, Subquery
from django.core.serializers.json import DjangoJSONEncoder
# Third Party imports
from rest_framework.response import Response
from rest_framework import serializers, status
from rest_framework import status
from rest_framework.permissions import AllowAny
# Module imports
@@ -106,7 +105,10 @@ class ProjectViewSet(BaseViewSet):
fields = [field for field in request.GET.get("fields", "").split(",") if field]
projects = self.get_queryset().order_by("sort_order", "name")
if WorkspaceMember.objects.filter(
member=request.user, workspace__slug=slug, is_active=True, role=5
member=request.user,
workspace__slug=slug,
is_active=True,
role=ROLE.GUEST.value,
).exists():
projects = projects.filter(
project_projectmember__member=self.request.user,
@@ -114,7 +116,10 @@ class ProjectViewSet(BaseViewSet):
)
if WorkspaceMember.objects.filter(
member=request.user, workspace__slug=slug, is_active=True, role=15
member=request.user,
workspace__slug=slug,
is_active=True,
role=ROLE.MEMBER.value,
).exists():
projects = projects.filter(
Q(
@@ -189,7 +194,10 @@ class ProjectViewSet(BaseViewSet):
)
if WorkspaceMember.objects.filter(
member=request.user, workspace__slug=slug, is_active=True, role=5
member=request.user,
workspace__slug=slug,
is_active=True,
role=ROLE.GUEST.value,
).exists():
projects = projects.filter(
project_projectmember__member=self.request.user,
@@ -197,7 +205,10 @@ class ProjectViewSet(BaseViewSet):
)
if WorkspaceMember.objects.filter(
member=request.user, workspace__slug=slug, is_active=True, role=15
member=request.user,
workspace__slug=slug,
is_active=True,
role=ROLE.MEMBER.value,
).exists():
projects = projects.filter(
Q(
@@ -250,7 +261,9 @@ class ProjectViewSet(BaseViewSet):
# Add the user as Administrator to the project
_ = ProjectMember.objects.create(
project_id=serializer.data["id"], member=request.user, role=20
project_id=serializer.data["id"],
member=request.user,
role=ROLE.ADMIN.value,
)
# Also create the issue property for the user
_ = IssueUserProperty.objects.create(
@@ -263,7 +276,7 @@ class ProjectViewSet(BaseViewSet):
ProjectMember.objects.create(
project_id=serializer.data["id"],
member_id=serializer.data["project_lead"],
role=20,
role=ROLE.ADMIN.value,
)
# Also create the issue property for the user
IssueUserProperty.objects.create(
@@ -341,13 +354,23 @@ class ProjectViewSet(BaseViewSet):
def partial_update(self, request, slug, pk=None):
# try:
if not ProjectMember.objects.filter(
is_workspace_admin = WorkspaceMember.objects.filter(
member=request.user,
workspace__slug=slug,
is_active=True,
role=ROLE.ADMIN.value,
).exists()
is_project_admin = ProjectMember.objects.filter(
member=request.user,
workspace__slug=slug,
project_id=pk,
role=20,
role=ROLE.ADMIN.value,
is_active=True,
).exists():
).exists()
# Return error for if the user is neither workspace admin nor project admin
if not is_project_admin and not is_workspace_admin:
return Response(
{"error": "You don't have the required permissions."},
status=status.HTTP_403_FORBIDDEN,
@@ -402,13 +425,16 @@ class ProjectViewSet(BaseViewSet):
def destroy(self, request, slug, pk):
if (
WorkspaceMember.objects.filter(
member=request.user, workspace__slug=slug, is_active=True, role=20
member=request.user,
workspace__slug=slug,
is_active=True,
role=ROLE.ADMIN.value,
).exists()
or ProjectMember.objects.filter(
member=request.user,
workspace__slug=slug,
project_id=pk,
role=20,
role=ROLE.ADMIN.value,
is_active=True,
).exists()
):
@@ -107,7 +107,8 @@ class MagicSignInEndpoint(View):
# Login the user and record his device info
user_login(request=request, user=user, is_app=True)
if user.is_password_autoset and profile.is_onboarded:
path = "accounts/set-password"
# Redirect to the home page
path = "/"
else:
# Get the redirection path
path = (
@@ -30,6 +30,8 @@ def page_version(page_id, existing_instance, user_id):
description_binary=page.description_binary,
owned_by_id=user_id,
last_saved_at=page.updated_at,
description_json=page.description,
description_stripped=page.description_stripped,
)
# If page versions are greater than 20 delete the oldest one
@@ -92,6 +92,10 @@ def create_project_and_member(workspace: Workspace) -> Dict[int, uuid.UUID]:
name=workspace.name, # Use workspace name
identifier=project_identifier,
created_by_id=workspace.created_by_id,
# Enable all views in seed data
cycle_view=True,
module_view=True,
issue_views_view=True,
)
# Create project members
@@ -0,0 +1,33 @@
# Generated by Django 4.2.22 on 2025-09-10 09:54
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("db", "0104_cycleuserproperties_rich_filters_and_more"),
]
operations = [
migrations.AlterField(
model_name="project",
name="cycle_view",
field=models.BooleanField(default=False),
),
migrations.AlterField(
model_name="project",
name="issue_views_view",
field=models.BooleanField(default=False),
),
migrations.AlterField(
model_name="project",
name="module_view",
field=models.BooleanField(default=False),
),
migrations.AlterField(
model_name="session",
name="user_id",
field=models.CharField(db_index=True, max_length=50, null=True),
),
]
+9 -3
View File
@@ -18,6 +18,12 @@ from .base import BaseModel
ROLE_CHOICES = ((20, "Admin"), (15, "Member"), (5, "Guest"))
class ROLE(Enum):
ADMIN = 20
MEMBER = 15
GUEST = 5
class ProjectNetwork(Enum):
SECRET = 0
PUBLIC = 2
@@ -89,9 +95,9 @@ class Project(BaseModel):
)
emoji = models.CharField(max_length=255, null=True, blank=True)
icon_prop = models.JSONField(null=True)
module_view = models.BooleanField(default=True)
cycle_view = models.BooleanField(default=True)
issue_views_view = models.BooleanField(default=True)
module_view = models.BooleanField(default=False)
cycle_view = models.BooleanField(default=False)
issue_views_view = models.BooleanField(default=False)
page_view = models.BooleanField(default=True)
intake_view = models.BooleanField(default=False)
is_time_tracking_enabled = models.BooleanField(default=False)
+1 -1
View File
@@ -13,7 +13,7 @@ VALID_KEY_CHARS = string.ascii_lowercase + string.digits
class Session(AbstractBaseSession):
device_info = models.JSONField(null=True, blank=True, default=None)
session_key = models.CharField(max_length=128, primary_key=True)
user_id = models.CharField(null=True, max_length=50)
user_id = models.CharField(null=True, max_length=50, db_index=True)
@classmethod
def get_session_store_class(cls):
@@ -0,0 +1,18 @@
# Generated by Django 4.2.22 on 2025-09-11 08:40
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("license", "0005_rename_product_instance_edition_and_more"),
]
operations = [
migrations.AddField(
model_name="instance",
name="is_current_version_deprecated",
field=models.BooleanField(default=False),
),
]
@@ -38,6 +38,8 @@ class Instance(BaseModel):
is_signup_screen_visited = models.BooleanField(default=False)
is_verified = models.BooleanField(default=False)
is_test = models.BooleanField(default=False)
# field for validating if the current version is deprecated
is_current_version_deprecated = models.BooleanField(default=False)
class Meta:
verbose_name = "Instance"
+46 -40
View File
@@ -67,20 +67,9 @@ def validate_binary_data(data):
# Combine custom components and editor-specific nodes into a single set of tags
CUSTOM_TAGS = {
# editor node/tag names
"imageComponent",
"image",
"mention",
"link",
"customColor",
"emoji",
"tableHeader",
"tableCell",
"tableRow",
"codeBlock",
"code",
"horizontalRule",
"calloutComponent",
# component-style tag used by editor embeds
"mention-component",
"label",
"input",
"image-component",
}
ALLOWED_TAGS = nh3.ALLOWED_TAGS | CUSTOM_TAGS
@@ -95,6 +84,8 @@ ATTRIBUTES = {
"aria-label",
"aria-hidden",
"style",
"start",
"type",
# common editor data-* attributes seen in stored HTML
# (wildcards like data-* are NOT supported by nh3; we add known keys
# here and dynamically include all data-* seen in the input below)
@@ -102,49 +93,64 @@ ATTRIBUTES = {
"data-node-type",
"data-type",
"data-checked",
"data-background",
"data-background-color",
"data-text-color",
"data-name",
# callout attributes
"data-icon-name",
"data-icon-color",
"data-background-color",
"data-background",
"data-emoji-unicode",
"data-emoji-url",
"data-logo-in-use",
"data-block-type",
"data-name",
"data-entity-id",
"data-entity-group-id",
},
"a": {"href", "target"},
# editor node/tag attributes
"imageComponent": {"id", "width", "height", "aspectRatio", "src", "alignment"},
"image": {"width", "height", "aspectRatio", "alignment", "src", "alt", "title"},
"mention": {"id", "entity_identifier", "entity_name"},
"link": {"href", "target"},
"customColor": {"color", "backgroundColor"},
"emoji": {"name"},
"tableHeader": {"colspan", "rowspan", "colwidth", "background", "hideContent"},
"tableCell": {
"image-component": {
"id",
"width",
"height",
"aspectRatio",
"aspectratio",
"src",
"alignment",
},
"img": {
"width",
"height",
"aspectRatio",
"aspectratio",
"alignment",
"src",
"alt",
"title",
},
"mention-component": {"id", "entity_identifier", "entity_name"},
"th": {
"colspan",
"rowspan",
"colwidth",
"background",
"hideContent",
"hidecontent",
"style",
},
"td": {
"colspan",
"rowspan",
"colwidth",
"background",
"textColor",
"textcolor",
"hideContent",
"hidecontent",
"style",
},
"tableRow": {"background", "textColor"},
"codeBlock": {"language"},
"calloutComponent": {
"data-icon-color",
"data-icon-name",
"data-emoji-unicode",
"data-emoji-url",
"data-logo-in-use",
"data-background",
"data-block-type",
},
# image-component (from editor extension and seeds)
"image-component": {"src", "id", "width", "height", "aspectratio", "alignment"},
"tr": {"background", "textColor", "textcolor", "style"},
"pre": {"language"},
"code": {"language", "spellcheck"},
"input": {"type", "checked"},
}
SAFE_PROTOCOLS = {"http", "https", "mailto", "tel"}
+2
View File
@@ -476,6 +476,8 @@ def filter_subscribed_issues(params, issue_filter, method, prefix=""):
issue_filter[f"{prefix}issue_subscribers__subscriber_id__in"] = params.get(
"subscriber"
)
issue_filter[f"{prefix}issue_subscribers__deleted_at__isnull"] = True
return issue_filter
+7 -5
View File
@@ -3,18 +3,20 @@ from urllib.parse import urlparse
def validate_next_path(next_path: str) -> str:
"""Validates that next_path is a valid path and extracts only the path component."""
"""Validates that next_path is a safe relative path for redirection."""
# Browsers interpret backslashes as forward slashes. Remove all backslashes.
next_path = next_path.replace("\\", "")
parsed_url = urlparse(next_path)
# Ensure next_path is not an absolute URL
# Block absolute URLs or anything with scheme/netloc
if parsed_url.scheme or parsed_url.netloc:
next_path = parsed_url.path # Extract only the path component
# Ensure it starts with a forward slash (indicating a valid relative path)
if not next_path.startswith("/"):
# Must start with a forward slash and not be empty
if not next_path or not next_path.startswith("/"):
return ""
# Ensure it does not contain dangerous path traversal sequences
# Prevent path traversal
if ".." in next_path:
return ""
+1 -1
View File
@@ -1,7 +1,7 @@
# base requirements
# django
Django==4.2.22
Django==4.2.24
# rest framework
djangorestframework==3.15.2
# postgres
@@ -13,6 +13,11 @@ export async function generateMetadata({ params }: Props) {
const { anchor } = params;
const DEFAULT_TITLE = "Plane";
const DEFAULT_DESCRIPTION = "Made with Plane, an AI-powered work management platform with publishing capabilities.";
// Validate anchor before using in request (only allow alphanumeric, -, _)
const ANCHOR_REGEX = /^[a-zA-Z0-9_-]+$/;
if (!ANCHOR_REGEX.test(anchor)) {
return { title: DEFAULT_TITLE, description: DEFAULT_DESCRIPTION };
}
try {
const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/api/public/anchor/${anchor}/meta/`);
const data = await response.json();
@@ -2,7 +2,7 @@
import { observer } from "mobx-react";
import Image from "next/image";
import { PlaneLockup } from "@plane/ui";
import { PlaneLockup } from "@plane/propel/icons";
// components
import { PoweredBy } from "@/components/common/powered-by";
import { UserAvatar } from "@/components/issues/navbar/user-avatar";
@@ -3,7 +3,7 @@
import { FC } from "react";
import { WEBSITE_URL } from "@plane/constants";
// assets
import { PlaneLogo } from "@plane/ui";
import { PlaneLogo } from "@plane/propel/icons";
type TPoweredBy = {
disabled?: boolean;
@@ -1,7 +1,7 @@
"use client";
import { X } from "lucide-react";
import { PriorityIcon, type TIssuePriorities } from "@plane/ui";
import { PriorityIcon, type TIssuePriorities } from "@plane/propel/icons";
type Props = {
handleRemove: (val: string) => void;
@@ -4,7 +4,7 @@ import { observer } from "mobx-react";
import { X } from "lucide-react";
// plane imports
import { EIconSize } from "@plane/constants";
import { StateGroupIcon } from "@plane/ui";
import { StateGroupIcon } from "@plane/propel/icons";
// hooks
import { useStates } from "@/hooks/store/use-state";
@@ -5,7 +5,7 @@ import { observer } from "mobx-react";
// plane imports
import { ISSUE_PRIORITY_FILTERS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { PriorityIcon } from "@plane/ui";
import { PriorityIcon } from "@plane/propel/icons";
// local imports
import { FilterHeader } from "./helpers/filter-header";
import { FilterOption } from "./helpers/filter-option";
@@ -4,7 +4,8 @@ import React, { useState } from "react";
import { observer } from "mobx-react";
// ui
import { EIconSize } from "@plane/constants";
import { Loader, StateGroupIcon } from "@plane/ui";
import { StateGroupIcon } from "@plane/propel/icons";
import { Loader } from "@plane/ui";
// hooks
import { useStates } from "@/hooks/store/use-state";
// local imports
@@ -2,8 +2,8 @@
import { observer } from "mobx-react";
// plane ui
import { ContrastIcon } from "@plane/propel/icons";
import { Tooltip } from "@plane/propel/tooltip";
import { ContrastIcon } from "@plane/ui";
// plane utils
import { cn } from "@plane/utils";
//hooks
@@ -2,8 +2,8 @@
import { observer } from "mobx-react";
// plane ui
import { DiceIcon } from "@plane/propel/icons";
import { Tooltip } from "@plane/propel/tooltip";
import { DiceIcon } from "@plane/ui";
// plane utils
import { cn } from "@plane/utils";
// hooks
@@ -3,9 +3,9 @@
import { SignalHigh } from "lucide-react";
import { useTranslation } from "@plane/i18n";
// types
import { PriorityIcon } from "@plane/propel/icons";
import { Tooltip } from "@plane/propel/tooltip";
import { TIssuePriorities } from "@plane/types";
import { PriorityIcon } from "@plane/ui";
// constants
import { cn, getIssuePriorityFilters } from "@plane/utils";
@@ -2,8 +2,8 @@
import { observer } from "mobx-react";
// plane ui
import { StateGroupIcon } from "@plane/propel/icons";
import { Tooltip } from "@plane/propel/tooltip";
import { StateGroupIcon } from "@plane/ui";
// plane utils
import { cn } from "@plane/utils";
//hooks
@@ -4,6 +4,7 @@ import isNil from "lodash/isNil";
import { ContrastIcon } from "lucide-react";
// types
import { EIconSize, ISSUE_PRIORITIES } from "@plane/constants";
import { CycleGroupIcon, DiceIcon, PriorityIcon, StateGroupIcon } from "@plane/propel/icons";
import {
GroupByColumnTypes,
IGroupByColumn,
@@ -12,7 +13,7 @@ import {
TGroupedIssues,
} from "@plane/types";
// ui
import { Avatar, CycleGroupIcon, DiceIcon, PriorityIcon, StateGroupIcon } from "@plane/ui";
import { Avatar } from "@plane/ui";
// components
// constants
// stores
@@ -5,7 +5,8 @@ import { observer } from "mobx-react";
import { Link2, MoveRight } from "lucide-react";
import { Listbox, Transition } from "@headlessui/react";
// ui
import { CenterPanelIcon, FullScreenPanelIcon, setToast, SidePanelIcon, TOAST_TYPE } from "@plane/ui";
import { CenterPanelIcon, FullScreenPanelIcon, SidePanelIcon } from "@plane/propel/icons";
import { setToast, TOAST_TYPE } from "@plane/ui";
// helpers
import { copyTextToClipboard } from "@/helpers/string.helper";
// hooks
@@ -5,7 +5,8 @@ import { useParams } from "next/navigation";
import { CalendarCheck2, Signal } from "lucide-react";
// plane imports
import { useTranslation } from "@plane/i18n";
import { DoubleCircleIcon, StateGroupIcon, TOAST_TYPE, setToast } from "@plane/ui";
import { DoubleCircleIcon, StateGroupIcon } from "@plane/propel/icons";
import { TOAST_TYPE, setToast } from "@plane/ui";
import { cn, getIssuePriorityFilters } from "@plane/utils";
// components
import { Icon } from "@/components/ui";
+1 -1
View File
@@ -2,7 +2,7 @@
import React from "react";
import Link from "next/link";
import { PlaneLockup } from "@plane/ui";
import { PlaneLockup } from "@plane/propel/icons";
export const AuthHeader = () => (
<div className="flex items-center justify-between gap-6 w-full flex-shrink-0 sticky top-0">
@@ -2,7 +2,8 @@
import { observer } from "mobx-react";
import { useTranslation } from "@plane/i18n";
// ui
import { Breadcrumbs, ContrastIcon, Header } from "@plane/ui";
import { ContrastIcon } from "@plane/propel/icons";
import { Breadcrumbs, Header } from "@plane/ui";
// components
import { BreadcrumbLink } from "@/components/common/breadcrumb-link";
// plane web components
@@ -8,8 +8,9 @@ import { useParams } from "next/navigation";
import { ChevronDown, PanelRight } from "lucide-react";
import { PROFILE_VIEWER_TAB, PROFILE_ADMINS_TAB, EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { UserActivityIcon } from "@plane/propel/icons";
import { IUserProfileProjectSegregation } from "@plane/types";
import { Breadcrumbs, Header, CustomMenu, UserActivityIcon } from "@plane/ui";
import { Breadcrumbs, Header, CustomMenu } from "@plane/ui";
import { cn } from "@plane/utils";
// components
import { BreadcrumbLink } from "@/components/common/breadcrumb-link";
@@ -3,10 +3,11 @@
import { FC } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { ArchiveIcon, ContrastIcon, DiceIcon, LayersIcon } from "@plane/propel/icons";
import { Tooltip } from "@plane/propel/tooltip";
import { EIssuesStoreType } from "@plane/types";
// ui
import { ArchiveIcon, Breadcrumbs, Header, ContrastIcon, DiceIcon, LayersIcon } from "@plane/ui";
import { Breadcrumbs, Header } from "@plane/ui";
// components
import { BreadcrumbLink } from "@/components/common/breadcrumb-link";
// hooks
@@ -4,7 +4,8 @@ import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import useSWR from "swr";
// ui
import { ArchiveIcon, Breadcrumbs, LayersIcon, Header } from "@plane/ui";
import { ArchiveIcon, LayersIcon } from "@plane/propel/icons";
import { Breadcrumbs, Header } from "@plane/ui";
// components
import { BreadcrumbLink } from "@/components/common/breadcrumb-link";
import { IssueDetailQuickActions } from "@/components/issues/issue-detail/issue-detail-quick-actions";
@@ -16,6 +16,7 @@ import {
} from "@plane/constants";
import { usePlatformOS } from "@plane/hooks";
import { useTranslation } from "@plane/i18n";
import { ContrastIcon } from "@plane/propel/icons";
import { Tooltip } from "@plane/propel/tooltip";
import {
EIssuesStoreType,
@@ -25,7 +26,7 @@ import {
IIssueFilterOptions,
EIssueLayoutTypes,
} from "@plane/types";
import { Breadcrumbs, Button, ContrastIcon, BreadcrumbNavigationSearchDropdown, Header } from "@plane/ui";
import { Breadcrumbs, Button, BreadcrumbNavigationSearchDropdown, Header } from "@plane/ui";
import { cn, isIssueFilterActive } from "@plane/utils";
// components
import { WorkItemsModal } from "@/components/analytics/work-items/modal";
@@ -14,6 +14,7 @@ import {
EProjectFeatureKey,
WORK_ITEM_TRACKER_ELEMENTS,
} from "@plane/constants";
import { DiceIcon } from "@plane/propel/icons";
import { Tooltip } from "@plane/propel/tooltip";
import {
EIssuesStoreType,
@@ -23,7 +24,7 @@ import {
IIssueFilterOptions,
EIssueLayoutTypes,
} from "@plane/types";
import { Breadcrumbs, Button, DiceIcon, Header, BreadcrumbNavigationSearchDropdown } from "@plane/ui";
import { Breadcrumbs, Button, Header, BreadcrumbNavigationSearchDropdown } from "@plane/ui";
import { cn, isIssueFilterActive } from "@plane/utils";
// components
import { WorkItemsModal } from "@/components/analytics/work-items/modal";
@@ -3,7 +3,8 @@
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// plane imports
import { Breadcrumbs, Button, Header, RecentStickyIcon } from "@plane/ui";
import { RecentStickyIcon } from "@plane/propel/icons";
import { Breadcrumbs, Button, Header } from "@plane/ui";
// components
import { BreadcrumbLink } from "@/components/common/breadcrumb-link";
import { StickySearch } from "@/components/stickies/modal/search";
@@ -191,7 +191,7 @@ export const GlobalIssuesHeader = observer(() => {
</Breadcrumbs>
</Header.LeftItem>
<Header.RightItem>
<Header.RightItem className="items-center">
{!isLocked ? (
<>
<GlobalViewLayoutSelection
+2 -1
View File
@@ -6,8 +6,9 @@ import Image from "next/image";
import Link from "next/link";
// plane imports
import { useTranslation } from "@plane/i18n";
import { PlaneLogo } from "@plane/propel/icons";
import { IWorkspace } from "@plane/types";
import { Button, getButtonStyling, PlaneLogo } from "@plane/ui";
import { Button, getButtonStyling } from "@plane/ui";
// components
import { CreateWorkspaceForm } from "@/components/workspace/create-workspace-form";
// hooks
+2 -1
View File
@@ -10,9 +10,10 @@ import { CheckCircle2 } from "lucide-react";
import { ROLE, MEMBER_TRACKER_EVENTS, MEMBER_TRACKER_ELEMENTS, GROUP_WORKSPACE_TRACKER_EVENT } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
// types
import { PlaneLogo } from "@plane/propel/icons";
import type { IWorkspaceMemberInvitation } from "@plane/types";
// ui
import { Button, TOAST_TYPE, setToast, PlaneLogo } from "@plane/ui";
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
import { truncateText } from "@plane/utils";
// components
import { EmptyState } from "@/components/common/empty-state";
+1 -1
View File
@@ -5,7 +5,7 @@ import { PreloadResources } from "./layout.preload";
// styles
import "@/styles/command-pallette.css";
import "@/styles/emoji.css";
import "@/styles/react-day-picker.css";
import "@plane/propel/styles/react-day-picker";
export const metadata: Metadata = {
robots: {
+63 -49
View File
@@ -1,69 +1,83 @@
"use client";
import Link from "next/link";
// plane imports
import { API_BASE_URL } from "@plane/constants";
import { Button, TOAST_TYPE, getButtonStyling, setToast } from "@plane/ui";
import { cn } from "@plane/utils";
// hooks
import { useAppRouter } from "@/hooks/use-app-router";
import Image from "next/image";
import { useTheme } from "next-themes";
// layouts
import { Button } from "@plane/ui";
import { useAppRouter } from "@/hooks/use-app-router";
import DefaultLayout from "@/layouts/default-layout";
// services
import { AuthService } from "@/services/auth.service";
// images
import maintenanceModeDarkModeImage from "@/public/instance/maintenance-mode-dark.svg";
import maintenanceModeLightModeImage from "@/public/instance/maintenance-mode-light.svg";
// services
const authService = new AuthService();
const linkMap = [
{
key: "mail_to",
label: "Contact Support",
value: "mailto:support@plane.so",
},
{
key: "status",
label: "Status Page",
value: "https://status.plane.so/",
},
{
key: "twitter_handle",
label: "@planepowers",
value: "https://x.com/planepowers",
},
];
export default function CustomErrorComponent() {
// hooks
const { resolvedTheme } = useTheme();
const router = useAppRouter();
const handleSignOut = async () => {
await authService
.signOut(API_BASE_URL)
.catch(() =>
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Failed to sign out. Please try again.",
})
)
.finally(() => router.push("/"));
};
// derived values
const maintenanceModeImage = resolvedTheme === "dark" ? maintenanceModeDarkModeImage : maintenanceModeLightModeImage;
return (
<DefaultLayout>
<div className={`h-screen w-full overflow-hidden bg-custom-background-100`}>
<div className="grid h-full place-items-center p-4">
<div className="space-y-8 text-center">
<div className="space-y-2 relative flex flex-col justify-center items-center">
<h3 className="text-lg font-semibold">Yikes! That doesn{"'"}t look good.</h3>
<p className="mx-auto md:w-1/2 text-sm text-custom-text-200">
That crashed Plane, pun intended. No worries, though. Our engineers have been notified. If you have more
details, please write to{" "}
<a href="mailto:support@plane.so" className="text-custom-primary">
support@plane.so
</a>{" "}
or on our{" "}
<div className="relative container mx-auto h-full w-full max-w-xl flex flex-col gap-2 items-center justify-center gap-y-6 bg-custom-background-100 text-center px-6">
<div className="relative w-full">
<Image
src={maintenanceModeImage}
height="176"
width="288"
alt="ProjectSettingImg"
className="w-full h-full object-fill object-center"
/>
</div>
<div className="w-full relative flex flex-col gap-4 mt-4">
<div className="flex flex-col gap-2.5">
<h1 className="text-xl font-semibold text-custom-text-100 text-left">
&#x1F6A7; Looks like something went wrong!
</h1>
<span className="text-base font-medium text-custom-text-200 text-left">
We track these errors automatically and working on getting things back up and running. If the problem
persists feel free to contact us. In the meantime, try refreshing.
</span>
</div>
<div className="flex items-center justify-start gap-6 mt-1">
{linkMap.map((link) => (
<div key={link.key}>
<a
href="https://discord.com/invite/A92xrEGCge"
href={link.value}
target="_blank"
className="text-custom-primary"
rel="noopener noreferrer"
className="text-custom-primary-100 hover:underline text-sm"
>
Discord
{link.label}
</a>
.
</p>
</div>
<div className="flex items-center justify-center gap-2">
<Link href="/" className={cn(getButtonStyling("primary", "md"))}>
Go to home
</Link>
<Button variant="neutral-primary" size="md" onClick={handleSignOut}>
Sign out
</Button>
</div>
</div>
))}
</div>
<div className="flex items-center justify-start gap-6">
<Button variant="primary" size="md" onClick={() => router.push("/")}>
Go to home
</Button>
</div>
</div>
</div>
@@ -4,7 +4,8 @@ import { FC } from "react";
import { observer } from "mobx-react";
// plane imports
import { EProjectFeatureKey } from "@plane/constants";
import { BreadcrumbNavigationDropdown, Breadcrumbs, ISvgIcons } from "@plane/ui";
import { ISvgIcons } from "@plane/propel/icons";
import { BreadcrumbNavigationDropdown, Breadcrumbs } from "@plane/ui";
// components
import { SwitcherLabel } from "@/components/common/switcher-label";
import type { TNavigationItem } from "@/components/workspace/sidebar/project-navigation";
@@ -3,7 +3,8 @@ import { observer } from "mobx-react";
import { Check } from "lucide-react";
// plane imports
import { EIconSize } from "@plane/constants";
import { Spinner, StateGroupIcon } from "@plane/ui";
import { StateGroupIcon } from "@plane/propel/icons";
import { Spinner } from "@plane/ui";
// store hooks
import { useProjectState } from "@/hooks/store/use-project-state";
@@ -2,6 +2,7 @@
// types
import { Briefcase, FileText, Layers, LayoutGrid } from "lucide-react";
import { ContrastIcon, DiceIcon } from "@plane/propel/icons";
import {
IWorkspaceDefaultSearchResult,
IWorkspaceIssueSearchResult,
@@ -10,7 +11,6 @@ import {
IWorkspaceSearchResult,
} from "@plane/types";
// ui
import { ContrastIcon, DiceIcon } from "@plane/ui";
// helpers
import { generateWorkItemLink } from "@plane/utils";
// plane web components
@@ -2,9 +2,8 @@ import { FC, ReactNode, useRef } from "react";
import { observer } from "mobx-react";
// plane imports
import { useTranslation } from "@plane/i18n";
import { Tooltip } from "@plane/propel/tooltip";
import { TIssueComment } from "@plane/types";
import { Avatar } from "@plane/ui";
import { EIssueCommentAccessSpecifier, TIssueComment } from "@plane/types";
import { Avatar, Tooltip } from "@plane/ui";
import { calculateTimeAgo, cn, getFileURL, renderFormattedDate, renderFormattedTime } from "@plane/utils";
// hooks
import { useMember } from "@/hooks/store/use-member";
@@ -27,7 +26,13 @@ export const CommentBlock: FC<TCommentBlock> = observer((props) => {
// translation
const { t } = useTranslation();
if (!comment || !userDetails) return null;
const displayName = comment?.actor_detail?.is_bot
? comment?.actor_detail?.first_name + ` ${t("bot")}`
: (userDetails?.display_name ?? comment?.actor_detail?.display_name);
const avatarUrl = userDetails?.avatar_url ?? comment?.actor_detail?.avatar_url;
if (!comment) return null;
return (
<div
@@ -43,20 +48,15 @@ export const CommentBlock: FC<TCommentBlock> = observer((props) => {
"flex-shrink-0 relative w-7 h-6 rounded-full transition-border duration-1000 flex justify-center items-center z-[3] uppercase font-medium"
)}
>
<Avatar
size="base"
name={userDetails?.display_name}
src={getFileURL(userDetails?.avatar_url)}
className="flex-shrink-0"
/>
<Avatar size="base" name={displayName} src={getFileURL(avatarUrl)} className="flex-shrink-0" />
</div>
<div className="flex flex-col gap-3 truncate flex-grow">
<div className="flex w-full gap-2">
<div className="flex-1 flex flex-wrap items-center gap-1">
<div className="text-xs font-medium">
{comment?.actor_detail?.is_bot
? comment?.actor_detail?.first_name + ` ${t("bot")}`
: comment?.actor_detail?.display_name || userDetails.display_name}
<div className="flex items-center gap-1">
<span className="text-xs font-medium">
{`${displayName}${comment.access === EIssueCommentAccessSpecifier.EXTERNAL ? " (External User)" : ""}`}
</span>
</div>
<div className="text-xs text-custom-text-300">
commented{" "}
@@ -1,6 +1,6 @@
import { observer } from "mobx-react";
import { useTranslation } from "@plane/i18n";
import { PlaneLogo } from "@plane/ui";
import { PlaneLogo } from "@plane/propel/icons";
// helpers
import { cn } from "@plane/utils";
// package.json
@@ -1,17 +1,37 @@
import { observer } from "mobx-react";
import { useTranslation } from "@plane/i18n";
export const MaintenanceMessage = observer(() => {
// hooks
const { t } = useTranslation();
export const MaintenanceMessage = () => {
const linkMap = [
{
key: "mail_to",
label: "Contact Support",
value: "mailto:support@plane.so",
},
];
return (
<h1 className="text-xl font-medium text-custom-text-100 text-center md:text-left">
{t(
"self_hosted_maintenance_message.plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start"
)}
<br />
{t("self_hosted_maintenance_message.choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure")}
</h1>
<>
<div className="flex flex-col gap-2.5">
<h1 className="text-xl font-semibold text-custom-text-100 text-left">
&#x1F6A7; Looks like Plane didn&apos;t start up correctly!
</h1>
<span className="text-base font-medium text-custom-text-200 text-left">
Some services might have failed to start. Please check your container logs to identify and resolve the issue.
If you&apos;re stuck, reach out to our support team for more help.
</span>
</div>
<div className="flex items-center justify-start gap-6 mt-1">
{linkMap.map((link) => (
<div key={link.key}>
<a
href={link.value}
target="_blank"
rel="noopener noreferrer"
className="text-custom-primary-100 hover:underline text-sm"
>
{link.label}
</a>
</div>
))}
</div>
</>
);
});
};
@@ -13,8 +13,8 @@ import {
Users,
} from "lucide-react";
// types
import { DiceIcon, DoubleCircleIcon, ISvgIcons } from "@plane/propel/icons";
import { IGroupByColumn, IIssueDisplayProperties, TGetColumns, TSpreadsheetColumn } from "@plane/types";
import { DiceIcon, DoubleCircleIcon, ISvgIcons } from "@plane/ui";
// components
import {
SpreadsheetAssigneeColumn,
@@ -6,7 +6,7 @@ import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
export const PageNavigationPaneAssetsTabEmptyState = () => {
// asset resolved path
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/pages/navigation-pane/assets" });
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/wiki/navigation-pane/assets" });
// translation
const { t } = useTranslation();
@@ -6,7 +6,7 @@ import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
export const PageNavigationPaneOutlineTabEmptyState = () => {
// asset resolved path
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/pages/navigation-pane/outline" });
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/wiki/navigation-pane/outline" });
// translation
const { t } = useTranslation();
@@ -1,7 +1,7 @@
import { FileText, Layers } from "lucide-react";
// plane imports
import { EUserPermissions, EProjectFeatureKey } from "@plane/constants";
import { ContrastIcon, DiceIcon, Intake, LayersIcon } from "@plane/ui";
import { ContrastIcon, DiceIcon, LayersIcon, Intake } from "@plane/propel/icons";
// components
import type { TNavigationItem } from "@/components/workspace/sidebar/project-navigation";
+1 -1
View File
@@ -1,5 +1,5 @@
import { CircleDot, CopyPlus, XCircle } from "lucide-react";
import { RelatedIcon } from "@plane/ui";
import { RelatedIcon } from "@plane/propel/icons";
import type { TRelationObject } from "@/components/issues/issue-detail-widgets/relations";
import type { TIssueRelationTypes } from "../../types";
+69 -1
View File
@@ -1,4 +1,7 @@
import { EIssueLayoutTypes } from "@plane/types";
import { ExternalLink, Link, Pencil, Trash2 } from "lucide-react";
import { useTranslation } from "@plane/i18n";
import { EIssueLayoutTypes, IProjectView } from "@plane/types";
import { TContextMenuItem } from "@plane/ui";
import { TWorkspaceLayoutProps } from "@/components/views/helper";
export type TLayoutSelectionProps = {
@@ -10,3 +13,68 @@ export type TLayoutSelectionProps = {
export const GlobalViewLayoutSelection = (props: TLayoutSelectionProps) => <></>;
export const WorkspaceAdditionalLayouts = (props: TWorkspaceLayoutProps) => <></>;
export type TMenuItemsFactoryProps = {
isOwner: boolean;
isAdmin: boolean;
setDeleteViewModal: (open: boolean) => void;
setCreateUpdateViewModal: (open: boolean) => void;
handleOpenInNewTab: () => void;
handleCopyText: () => void;
isLocked: boolean;
workspaceSlug: string;
projectId?: string;
viewId: string;
};
export const useMenuItemsFactory = (props: TMenuItemsFactoryProps) => {
const { isOwner, isAdmin, setDeleteViewModal, setCreateUpdateViewModal, handleOpenInNewTab, handleCopyText } = props;
const { t } = useTranslation();
const editMenuItem = () => ({
key: "edit",
action: () => setCreateUpdateViewModal(true),
title: t("edit"),
icon: Pencil,
shouldRender: isOwner,
});
const openInNewTabMenuItem = () => ({
key: "open-new-tab",
action: handleOpenInNewTab,
title: t("open_in_new_tab"),
icon: ExternalLink,
});
const copyLinkMenuItem = () => ({
key: "copy-link",
action: handleCopyText,
title: t("copy_link"),
icon: Link,
});
const deleteMenuItem = () => ({
key: "delete",
action: () => setDeleteViewModal(true),
title: t("delete"),
icon: Trash2,
shouldRender: isOwner || isAdmin,
});
return {
editMenuItem,
openInNewTabMenuItem,
copyLinkMenuItem,
deleteMenuItem,
};
};
export const useViewMenuItems = (props: TMenuItemsFactoryProps): TContextMenuItem[] => {
const factory = useMenuItemsFactory(props);
return [factory.editMenuItem(), factory.openInNewTabMenuItem(), factory.copyLinkMenuItem(), factory.deleteMenuItem()];
};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const AdditionalHeaderItems = (view: IProjectView) => <></>;
@@ -1,5 +1,5 @@
import { BarChart2, Briefcase, Home, Inbox, Layers, PenSquare } from "lucide-react";
import { ArchiveIcon, ContrastIcon, UserActivityIcon } from "@plane/ui";
import { ArchiveIcon, ContrastIcon, UserActivityIcon } from "@plane/propel/icons";
import { cn } from "@plane/utils";
export const getSidebarNavigationItemIcon = (key: string, className: string = "") => {
@@ -1,8 +1,8 @@
import { ReactNode } from "react";
import { FileText, Layers, Timer } from "lucide-react";
// plane imports
import { ContrastIcon, DiceIcon, Intake } from "@plane/propel/icons";
import { IProject } from "@plane/types";
import { ContrastIcon, DiceIcon, Intake } from "@plane/ui";
export type TProperties = {
key: string;
+1 -1
View File
@@ -1,7 +1,7 @@
import { Briefcase, FileText, Layers, LucideIcon } from "lucide-react";
// plane imports
import { ContrastIcon, DiceIcon, FavoriteFolderIcon, ISvgIcons } from "@plane/propel/icons";
import { IFavorite } from "@plane/types";
import { ContrastIcon, DiceIcon, FavoriteFolderIcon, ISvgIcons } from "@plane/ui";
export const FAVORITE_ITEM_ICONS: Record<string, React.FC<ISvgIcons> | LucideIcon> = {
page: FileText,
+1 -1
View File
@@ -1,2 +1,2 @@
export * from "./project";
export * from "./workspace.service";
export * from "@/services/workspace.service";
+1 -1
View File
@@ -1,2 +1,2 @@
export * from "./estimate.service";
export * from "./view.service";
export * from "@/services/view.service";
@@ -1,60 +0,0 @@
import { API_BASE_URL } from "@plane/constants";
import { EViewAccess, TPublishViewSettings } from "@plane/types";
import { ViewService as CoreViewService } from "@/services/view.service";
export class ViewService extends CoreViewService {
constructor() {
super(API_BASE_URL);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async updateViewAccess(workspaceSlug: string, projectId: string, viewId: string, access: EViewAccess) {
return Promise.resolve();
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async lockView(workspaceSlug: string, projectId: string, viewId: string) {
return Promise.resolve();
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async unLockView(workspaceSlug: string, projectId: string, viewId: string) {
return Promise.resolve();
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async getPublishDetails(workspaceSlug: string, projectId: string, viewId: string): Promise<any> {
return Promise.resolve({});
}
async publishView(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
workspaceSlug: string,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
projectId: string,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
viewId: string,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
data: TPublishViewSettings
): Promise<any> {
return Promise.resolve();
}
async updatePublishedView(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
workspaceSlug: string,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
projectId: string,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
viewId: string,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
data: Partial<TPublishViewSettings>
): Promise<void> {
return Promise.resolve();
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async unPublishView(workspaceSlug: string, projectId: string, viewId: string): Promise<void> {
return Promise.resolve();
}
}
-24
View File
@@ -1,24 +0,0 @@
import { API_BASE_URL } from "@plane/constants";
import { EViewAccess } from "@plane/types";
import { WorkspaceService as CoreWorkspaceService } from "@/services/workspace.service";
export class WorkspaceService extends CoreWorkspaceService {
constructor() {
super(API_BASE_URL);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async updateViewAccess(workspaceSlug: string, viewId: string, access: EViewAccess) {
return Promise.resolve();
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async lockView(workspaceSlug: string, viewId: string) {
return Promise.resolve();
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async unLockView(workspaceSlug: string, viewId: string) {
return Promise.resolve();
}
}
+1
View File
@@ -0,0 +1 @@
export * from "@/store/global-view.store";
+1
View File
@@ -0,0 +1 @@
export * from "@/store/project-view.store";
@@ -1,28 +1,17 @@
// plane package imports
import React, { useMemo } from "react";
import React from "react";
import { IAnalyticsResponseFields } from "@plane/types";
import { Loader } from "@plane/ui";
// components
import TrendPiece from "./trend-piece";
export type InsightCardProps = {
data?: IAnalyticsResponseFields;
label: string;
isLoading?: boolean;
versus?: string | null;
};
const InsightCard = (props: InsightCardProps) => {
const { data, label, isLoading, versus } = props;
const { count, filter_count } = data || {};
const percentage = useMemo(() => {
if (count != null && filter_count != null) {
const result = ((count - filter_count) / count) * 100;
const isFiniteAndNotNaNOrZero = Number.isFinite(result) && !Number.isNaN(result) && result !== 0;
return isFiniteAndNotNaNOrZero ? result : null;
}
return null;
}, [count, filter_count]);
const { data, label, isLoading = false } = props;
const count = data?.count ?? 0;
return (
<div className="flex flex-col gap-3">
@@ -30,12 +19,6 @@ const InsightCard = (props: InsightCardProps) => {
{!isLoading ? (
<div className="flex flex-col gap-1">
<div className="text-2xl font-bold text-custom-text-100">{count}</div>
{/* {percentage && (
<div className="flex gap-1 text-xs text-custom-text-300">
<TrendPiece percentage={percentage} size="xs" />
{versus && <div>vs {versus}</div>}
</div>
)} */}
</div>
) : (
<Loader.Item height="50px" width="100%" />
@@ -19,7 +19,7 @@ const getInsightLabel = (
analyticsType: TAnalyticsTabsBase,
item: IInsightField,
isEpic: boolean | undefined,
t: (key: string, options?: any) => string
t: (key: string, params?: Record<string, unknown>) => string
) => {
if (analyticsType === "work-items") {
return isEpic
@@ -50,15 +50,7 @@ const TotalInsights: React.FC<{
const params = useParams();
const workspaceSlug = params.workspaceSlug.toString();
const { t } = useTranslation();
const {
selectedDuration,
selectedProjects,
selectedDurationLabel,
selectedCycle,
selectedModule,
isPeekView,
isEpic,
} = useAnalytics();
const { selectedDuration, selectedProjects, selectedCycle, selectedModule, isPeekView, isEpic } = useAnalytics();
const { data: totalInsightsData, isLoading } = useSWR(
`total-insights-${analyticsType}-${selectedDuration}-${selectedProjects}-${selectedCycle}-${selectedModule}-${isEpic}`,
() =>
@@ -92,7 +84,6 @@ const TotalInsights: React.FC<{
isLoading={isLoading}
data={totalInsightsData?.[item.key]}
label={getInsightLabel(analyticsType, item, isEpic, t)}
versus={selectedDurationLabel}
/>
))}
</div>
@@ -1,6 +1,6 @@
"use client";
import React from "react";
import { AccentureLogo, DolbyLogo, SonyLogo, ZerodhaLogo } from "@plane/ui";
import { AccentureLogo, DolbyLogo, SonyLogo, ZerodhaLogo } from "@plane/propel/icons";
const BRAND_LOGOS: {
id: string;
@@ -5,7 +5,7 @@ import { observer } from "mobx-react";
import Link from "next/link";
import { AUTH_TRACKER_ELEMENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { PlaneLockup } from "@plane/ui";
import { PlaneLockup } from "@plane/propel/icons";
import { PageHead } from "@/components/core/page-title";
import { EAuthModes } from "@/helpers/authentication.helper";
import { useInstance } from "@/hooks/store/use-instance";
@@ -15,9 +15,10 @@ import {
PROJECT_SETTINGS_TRACKER_EVENTS,
} from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { StateGroupIcon, DoubleCircleIcon } from "@plane/propel/icons";
import { IProject } from "@plane/types";
// ui
import { CustomSelect, CustomSearchSelect, ToggleSwitch, StateGroupIcon, DoubleCircleIcon, Loader } from "@plane/ui";
import { CustomSelect, CustomSearchSelect, ToggleSwitch, Loader } from "@plane/ui";
// component
import { SelectMonthModal } from "@/components/automation";
// constants
@@ -3,7 +3,7 @@ import { Command } from "cmdk";
import { observer } from "mobx-react";
import { FileText, GithubIcon, MessageSquare, Rocket } from "lucide-react";
// ui
import { DiscordIcon } from "@plane/ui";
import { DiscordIcon } from "@plane/propel/icons";
// hooks
import { useCommandPalette } from "@/hooks/store/use-command-palette";
import { useTransient } from "@/hooks/store/use-transient";
@@ -4,9 +4,10 @@ import { Command } from "cmdk";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { LinkIcon, Signal, Trash2, UserMinus2, UserPlus2, Users } from "lucide-react";
import { DoubleCircleIcon } from "@plane/propel/icons";
import { EIssueServiceType, TIssue } from "@plane/types";
// hooks
import { DoubleCircleIcon, TOAST_TYPE, setToast } from "@plane/ui";
import { TOAST_TYPE, setToast } from "@plane/ui";
// helpers
import { copyTextToClipboard } from "@plane/utils";
// hooks
@@ -7,9 +7,9 @@ import { Check } from "lucide-react";
// plane constants
import { ISSUE_PRIORITIES } from "@plane/constants";
// plane types
import { PriorityIcon } from "@plane/propel/icons";
import { EIssueServiceType, TIssue, TIssuePriorities } from "@plane/types";
// mobx store
import { PriorityIcon } from "@plane/ui";
import { useIssueDetail } from "@/hooks/store/use-issue-detail";
// ui
// types
@@ -9,7 +9,7 @@ import {
PROJECT_PAGE_TRACKER_ELEMENTS,
PROJECT_VIEW_TRACKER_ELEMENTS,
} from "@plane/constants";
import { DiceIcon } from "@plane/ui";
import { DiceIcon } from "@plane/propel/icons";
// hooks
import { useCommandPalette } from "@/hooks/store/use-command-palette";
// ui
@@ -0,0 +1,135 @@
import React from "react";
import { FolderPlus, Settings } from "lucide-react";
import { LayersIcon } from "@plane/propel/icons";
import {
PROJECT_TRACKER_ELEMENTS,
WORK_ITEM_TRACKER_ELEMENTS,
} from "@plane/constants";
import { captureClick } from "@/helpers/event-tracker.helper";
import { PaletteCommandGroup } from "./types";
interface BuildParams {
workspaceSlug?: string | string[];
pages: string[];
workspaceProjectIds?: string[];
canPerformAnyCreateAction: boolean;
canPerformWorkspaceActions: boolean;
closePalette: () => void;
toggleCreateIssueModal: (v: boolean) => void;
toggleCreateProjectModal: (v: boolean) => void;
setPages: React.Dispatch<React.SetStateAction<string[]>>;
setPlaceholder: (v: string) => void;
setSearchTerm: (v: string) => void;
createNewWorkspace: () => void;
}
export const buildCommandGroups = (params: BuildParams): PaletteCommandGroup[] => {
const {
workspaceSlug,
pages,
workspaceProjectIds,
canPerformAnyCreateAction,
canPerformWorkspaceActions,
closePalette,
toggleCreateIssueModal,
toggleCreateProjectModal,
setPages,
setPlaceholder,
setSearchTerm,
createNewWorkspace,
} = params;
const groups: PaletteCommandGroup[] = [];
if (
workspaceSlug &&
workspaceProjectIds &&
workspaceProjectIds.length > 0 &&
canPerformAnyCreateAction
) {
groups.push({
id: "work-item",
heading: "Work item",
commands: [
{
id: "create-work-item",
label: "Create new work item",
shortcut: "C",
icon: LayersIcon,
perform: () => {
closePalette();
captureClick({ elementName: WORK_ITEM_TRACKER_ELEMENTS.COMMAND_PALETTE_ADD_BUTTON });
toggleCreateIssueModal(true);
},
enabled: true,
},
],
});
}
if (workspaceSlug && canPerformWorkspaceActions) {
groups.push({
id: "project",
heading: "Project",
commands: [
{
id: "create-project",
label: "Create new project",
shortcut: "P",
icon: FolderPlus,
perform: () => {
closePalette();
captureClick({ elementName: PROJECT_TRACKER_ELEMENTS.COMMAND_PALETTE_CREATE_BUTTON });
toggleCreateProjectModal(true);
},
enabled: true,
},
],
});
}
groups.push({
id: "workspace-settings",
heading: "Workspace Settings",
commands: [
{
id: "search-settings",
label: "Search settings...",
icon: Settings,
perform: () => {
setPlaceholder("Search workspace settings...");
setSearchTerm("");
setPages((p) => [...p, "settings"]);
},
enabled: !!(canPerformWorkspaceActions && workspaceSlug),
},
],
});
groups.push({
id: "account",
heading: "Account",
commands: [
{
id: "create-workspace",
label: "Create new workspace",
icon: FolderPlus,
perform: createNewWorkspace,
enabled: true,
},
{
id: "change-interface-theme",
label: "Change interface theme...",
icon: Settings,
perform: () => {
setPlaceholder("Change interface theme...");
setSearchTerm("");
setPages((p) => [...p, "change-interface-theme"]);
},
enabled: true,
},
],
});
return groups;
};
@@ -1,6 +1,6 @@
"use client";
import React, { useEffect, useState } from "react";
import React, { useEffect, useMemo, useState } from "react";
import { Command } from "cmdk";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
@@ -16,8 +16,9 @@ import {
WORKSPACE_DEFAULT_SEARCH_RESULT,
} from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { LayersIcon } from "@plane/propel/icons";
import { IWorkspaceSearchResults } from "@plane/types";
import { LayersIcon, Loader, ToggleSwitch } from "@plane/ui";
import { Loader, ToggleSwitch } from "@plane/ui";
import { cn, getTabIndex } from "@plane/utils";
// components
import {
@@ -34,7 +35,6 @@ import {
import { SimpleEmptyState } from "@/components/empty-state/simple-empty-state-root";
// helpers
// hooks
import { captureClick } from "@/helpers/event-tracker.helper";
import { useCommandPalette } from "@/hooks/store/use-command-palette";
import { useIssueDetail } from "@/hooks/store/use-issue-detail";
import { useProject } from "@/hooks/store/use-project";
@@ -45,6 +45,8 @@ import { usePlatformOS } from "@/hooks/use-platform-os";
// plane web components
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
import { IssueIdentifier } from "@/plane-web/components/issues/issue-details/issue-identifier";
import { buildCommandGroups } from "./command-config";
import { PaletteCommandGroup } from "./types";
// plane web services
import { WorkspaceService } from "@/plane-web/services";
@@ -123,6 +125,38 @@ export const CommandModal: React.FC = observer(() => {
router.push("/create-workspace");
};
const commandGroups: PaletteCommandGroup[] = useMemo(
() =>
buildCommandGroups({
workspaceSlug,
pages,
workspaceProjectIds,
canPerformAnyCreateAction,
canPerformWorkspaceActions,
closePalette,
toggleCreateIssueModal,
toggleCreateProjectModal,
setPages,
setPlaceholder,
setSearchTerm,
createNewWorkspace,
}),
[
workspaceSlug,
pages,
workspaceProjectIds,
canPerformAnyCreateAction,
canPerformWorkspaceActions,
closePalette,
toggleCreateIssueModal,
toggleCreateProjectModal,
setPages,
setPlaceholder,
setSearchTerm,
createNewWorkspace,
]
);
useEffect(
() => {
if (!workspaceSlug) return;
@@ -340,91 +374,32 @@ export const CommandModal: React.FC = observer(() => {
setSearchTerm={(newSearchTerm) => setSearchTerm(newSearchTerm)}
/>
)}
{workspaceSlug &&
workspaceProjectIds &&
workspaceProjectIds.length > 0 &&
canPerformAnyCreateAction && (
<Command.Group heading="Work item">
<Command.Item
onSelect={() => {
closePalette();
captureClick({
elementName: WORK_ITEM_TRACKER_ELEMENTS.COMMAND_PALETTE_ADD_BUTTON,
});
toggleCreateIssueModal(true);
}}
className="focus:bg-custom-background-80"
>
<div className="flex items-center gap-2 text-custom-text-200">
<LayersIcon className="h-3.5 w-3.5" />
Create new work item
</div>
<kbd>C</kbd>
</Command.Item>
{commandGroups.map((group) => (
<React.Fragment key={group.id}>
{group.id === "workspace-settings" &&
projectId &&
canPerformAnyCreateAction && (
<CommandPaletteProjectActions closePalette={closePalette} />
)}
<Command.Group heading={group.heading}>
{group.commands
.filter((c) => c.enabled !== false)
.map((c) => (
<Command.Item
key={c.id}
onSelect={() => c.perform()}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-custom-text-200">
{c.icon && <c.icon className="h-3.5 w-3.5" />}
{c.label}
</div>
{c.shortcut && <kbd>{c.shortcut}</kbd>}
</Command.Item>
))}
</Command.Group>
)}
{workspaceSlug && canPerformWorkspaceActions && (
<Command.Group heading="Project">
<Command.Item
onSelect={() => {
closePalette();
captureClick({ elementName: PROJECT_TRACKER_ELEMENTS.COMMAND_PALETTE_CREATE_BUTTON });
toggleCreateProjectModal(true);
}}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-custom-text-200">
<FolderPlus className="h-3.5 w-3.5" />
Create new project
</div>
<kbd>P</kbd>
</Command.Item>
</Command.Group>
)}
{/* project actions */}
{projectId && canPerformAnyCreateAction && (
<CommandPaletteProjectActions closePalette={closePalette} />
)}
{canPerformWorkspaceActions && (
<Command.Group heading="Workspace Settings">
<Command.Item
onSelect={() => {
setPlaceholder("Search workspace settings...");
setSearchTerm("");
setPages([...pages, "settings"]);
}}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-custom-text-200">
<Settings className="h-3.5 w-3.5" />
Search settings...
</div>
</Command.Item>
</Command.Group>
)}
<Command.Group heading="Account">
<Command.Item onSelect={createNewWorkspace} className="focus:outline-none">
<div className="flex items-center gap-2 text-custom-text-200">
<FolderPlus className="h-3.5 w-3.5" />
Create new workspace
</div>
</Command.Item>
<Command.Item
onSelect={() => {
setPlaceholder("Change interface theme...");
setSearchTerm("");
setPages([...pages, "change-interface-theme"]);
}}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-custom-text-200">
<Settings className="h-3.5 w-3.5" />
Change interface theme...
</div>
</Command.Item>
</Command.Group>
</React.Fragment>
))}
{/* help options */}
<CommandPaletteHelpActions closePalette={closePalette} />
</>
@@ -1,6 +1,6 @@
"use client";
import React, { useCallback, useEffect, FC, useMemo } from "react";
import React, { useCallback, FC, useMemo } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import useSWR from "swr";
@@ -10,6 +10,7 @@ import { TOAST_TYPE, setToast } from "@plane/ui";
// components
import { copyTextToClipboard } from "@plane/utils";
import { CommandModal, ShortcutsModal } from "@/components/command-palette";
import { useShortcuts, Shortcut } from "./use-shortcuts";
// helpers
// hooks
import { captureClick } from "@/helpers/event-tracker.helper";
@@ -158,98 +159,119 @@ export const CommandPalette: FC = observer(() => {
[]
);
const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
const { key, ctrlKey, metaKey, altKey, shiftKey } = e;
if (!key) return;
const isEditable = (e: KeyboardEvent) => {
const target = e.target as HTMLElement;
return (
target instanceof HTMLTextAreaElement ||
target instanceof HTMLInputElement ||
target.isContentEditable ||
target.classList.contains("ProseMirror")
);
};
const keyPressed = key.toLowerCase();
const cmdClicked = ctrlKey || metaKey;
const shiftClicked = shiftKey;
const deleteKey = keyPressed === "backspace" || keyPressed === "delete";
const shortcuts: Shortcut[] = useMemo(() => {
const list: Shortcut[] = [
{
keys: ["meta", "k"],
handler: () => toggleCommandPaletteModal(true),
enabled: () => !isAnyModalOpen,
},
{
keys: ["control", "k"],
handler: () => toggleCommandPaletteModal(true),
enabled: () => !isAnyModalOpen,
},
{
keys: ["shift", "?"],
handler: () => toggleShortcutModal(true),
enabled: () => !isAnyModalOpen,
},
{
keys: ["shift", "/"],
handler: () => toggleShortcutModal(true),
enabled: () => !isAnyModalOpen,
},
{
keys: platform === "MacOS" ? ["control", "meta", "c"] : ["control", "alt", "c"],
handler: () => copyIssueUrlToClipboard(),
enabled: () => !!workItem,
},
{
keys: ["meta", "b"],
handler: () => toggleSidebar(),
enabled: (e) => !isEditable(e),
},
{
keys: ["control", "b"],
handler: () => toggleSidebar(),
enabled: (e) => !isEditable(e),
},
];
if (cmdClicked && keyPressed === "k" && !isAnyModalOpen) {
e.preventDefault();
toggleCommandPaletteModal(true);
}
Object.keys(shortcutsList.global).forEach((k) => {
list.push({
sequence: [k],
handler: () => {
captureClick({ elementName: COMMAND_PALETTE_TRACKER_ELEMENTS.COMMAND_PALETTE_SHORTCUT_KEY });
shortcutsList.global[k].action();
},
enabled: (e) =>
!isEditable(e) &&
!isAnyModalOpen &&
(((!projectId && performAnyProjectCreateActions()) || performProjectCreateActions())),
});
});
// if on input, textarea or editor, don't do anything
if (
e.target instanceof HTMLTextAreaElement ||
e.target instanceof HTMLInputElement ||
(e.target as Element)?.classList?.contains("ProseMirror")
)
return;
Object.keys(shortcutsList.workspace).forEach((k) => {
list.push({
sequence: [k],
handler: () => {
captureClick({ elementName: COMMAND_PALETTE_TRACKER_ELEMENTS.COMMAND_PALETTE_SHORTCUT_KEY });
shortcutsList.workspace[k].action();
},
enabled: (e) =>
!isEditable(e) &&
!isAnyModalOpen &&
!!workspaceSlug &&
performWorkspaceCreateActions(),
});
});
if (shiftClicked && (keyPressed === "?" || keyPressed === "/") && !isAnyModalOpen) {
e.preventDefault();
toggleShortcutModal(true);
}
Object.entries(shortcutsList.project).forEach(([k, v]) => {
const isDeleteKey = k === "delete" || k === "backspace";
list.push({
sequence: [k],
handler: () => {
captureClick({ elementName: COMMAND_PALETTE_TRACKER_ELEMENTS.COMMAND_PALETTE_SHORTCUT_KEY });
v.action();
},
enabled: (e) =>
!isEditable(e) &&
!isAnyModalOpen &&
!!projectId &&
(isDeleteKey ? performProjectBulkDeleteActions() : performProjectCreateActions()),
});
});
if (deleteKey) {
if (performProjectBulkDeleteActions()) {
shortcutsList.project.delete.action();
}
} else if (cmdClicked) {
if (keyPressed === "c" && ((platform === "MacOS" && ctrlKey) || altKey)) {
e.preventDefault();
copyIssueUrlToClipboard();
} else if (keyPressed === "b") {
e.preventDefault();
toggleSidebar();
}
} else if (!isAnyModalOpen) {
captureClick({ elementName: COMMAND_PALETTE_TRACKER_ELEMENTS.COMMAND_PALETTE_SHORTCUT_KEY });
if (
Object.keys(shortcutsList.global).includes(keyPressed) &&
((!projectId && performAnyProjectCreateActions()) || performProjectCreateActions())
) {
shortcutsList.global[keyPressed].action();
}
// workspace authorized actions
else if (
Object.keys(shortcutsList.workspace).includes(keyPressed) &&
workspaceSlug &&
performWorkspaceCreateActions()
) {
e.preventDefault();
shortcutsList.workspace[keyPressed].action();
}
// project authorized actions
else if (
Object.keys(shortcutsList.project).includes(keyPressed) &&
projectId &&
performProjectCreateActions()
) {
e.preventDefault();
// actions that can be performed only inside a project
shortcutsList.project[keyPressed].action();
}
}
// Additional keydown events
handleAdditionalKeyDownEvents(e);
},
[
copyIssueUrlToClipboard,
isAnyModalOpen,
platform,
performAnyProjectCreateActions,
performProjectBulkDeleteActions,
performProjectCreateActions,
performWorkspaceCreateActions,
projectId,
shortcutsList,
toggleCommandPaletteModal,
toggleShortcutModal,
toggleSidebar,
workspaceSlug,
]
);
return list;
}, [
copyIssueUrlToClipboard,
isAnyModalOpen,
performAnyProjectCreateActions,
performProjectBulkDeleteActions,
performProjectCreateActions,
performWorkspaceCreateActions,
platform,
projectId,
shortcutsList,
toggleCommandPaletteModal,
toggleShortcutModal,
toggleSidebar,
workspaceSlug,
workItem,
]);
useEffect(() => {
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, [handleKeyDown]);
useShortcuts(shortcuts, { additional: handleAdditionalKeyDownEvents });
if (!currentUser) return null;
@@ -0,0 +1,16 @@
import React from "react";
export interface PaletteCommand {
id: string;
label: string;
shortcut?: string;
icon?: React.ComponentType<{ className?: string }>;
perform: () => void;
enabled?: boolean;
}
export interface PaletteCommandGroup {
id: string;
heading: string;
commands: PaletteCommand[];
}
@@ -0,0 +1,67 @@
import { useCallback, useEffect, useRef } from "react";
export interface Shortcut {
keys?: string[]; // simultaneous combination
sequence?: string[]; // sequential keys
handler: (e: KeyboardEvent) => void;
enabled?: (e: KeyboardEvent) => boolean;
preventDefault?: boolean;
}
const matchCombo = (pressed: string[], combo: string[]) => {
if (pressed.length !== combo.length) return false;
return combo.every((k) => pressed.includes(k));
};
const matchSequence = (buffer: string[], sequence: string[]) => {
if (buffer.length < sequence.length) return false;
const start = buffer.length - sequence.length;
return sequence.every((k, i) => buffer[start + i] === k);
};
export const useShortcuts = (
shortcuts: Shortcut[],
options?: { filter?: (e: KeyboardEvent) => boolean; additional?: (e: KeyboardEvent) => void }
) => {
const bufferRef = useRef<string[]>([]);
const timerRef = useRef<NodeJS.Timeout>();
const listener = useCallback(
(e: KeyboardEvent) => {
if (options?.filter && !options.filter(e)) return;
const pressed: string[] = [];
if (e.metaKey) pressed.push("meta");
if (e.ctrlKey) pressed.push("control");
if (e.altKey) pressed.push("alt");
if (e.shiftKey) pressed.push("shift");
pressed.push(e.key.toLowerCase());
bufferRef.current.push(e.key.toLowerCase());
clearTimeout(timerRef.current);
timerRef.current = setTimeout(() => {
bufferRef.current = [];
}, 1000);
shortcuts.forEach((s) => {
if (s.enabled && !s.enabled(e)) return;
if (s.keys && matchCombo(pressed, s.keys)) {
if (s.preventDefault ?? true) e.preventDefault();
s.handler(e);
} else if (s.sequence && matchSequence(bufferRef.current, s.sequence)) {
if (s.preventDefault ?? true) e.preventDefault();
bufferRef.current = [];
s.handler(e);
}
});
options?.additional?.(e);
},
[shortcuts, options]
);
useEffect(() => {
document.addEventListener("keydown", listener);
return () => document.removeEventListener("keydown", listener);
}, [listener]);
};
@@ -25,7 +25,7 @@ import {
} from "lucide-react";
// components
import { ArchiveIcon, DoubleCircleIcon, ContrastIcon, DiceIcon, Intake } from "@plane/ui";
import { ArchiveIcon, DoubleCircleIcon, ContrastIcon, DiceIcon, Intake } from "@plane/propel/icons";
import { store } from "@/lib/store-context";
import { TProjectActivity } from "@/plane-web/types";
@@ -1,6 +1,6 @@
import { FC } from "react";
import { ISvgIcons } from "@plane/propel/icons";
import { TLogoProps } from "@plane/types";
import { ISvgIcons } from "@plane/ui";
import { getFileURL, truncateText } from "@plane/utils";
import { Logo } from "@/components/common/logo";
+1 -1
View File
@@ -20,9 +20,9 @@ import {
MessageSquareIcon,
UsersIcon,
} from "lucide-react";
import { BlockedIcon, BlockerIcon, RelatedIcon, LayersIcon, DiceIcon, EpicIcon, Intake } from "@plane/propel/icons";
import { Tooltip } from "@plane/propel/tooltip";
import { IIssueActivity } from "@plane/types";
import { BlockedIcon, BlockerIcon, RelatedIcon, LayersIcon, DiceIcon, Intake, EpicIcon } from "@plane/ui";
import { renderFormattedDate, generateWorkItemLink, capitalizeFirstLetter } from "@plane/utils";
// helpers
import { useLabel } from "@/hooks/store/use-label";
@@ -131,13 +131,14 @@ export const DescriptionVersionsModal: React.FC<Props> = observer((props) => {
{/* End header */}
{/* Version description */}
<div className="mt-4 pb-4">
{activeVersionDescription ? (
{activeVersionId && activeVersionDescription ? (
<RichTextEditor
key={activeVersionId}
editable={false}
containerClassName="p-0 !pl-0 border-none"
editorClassName="pl-0"
id={activeVersionId ?? ""}
initialValue={activeVersionDescription ?? "<p></p>"}
id={activeVersionId}
initialValue={activeVersionDescription}
projectId={projectId}
ref={editorRef}
workspaceId={workspaceId}
@@ -48,6 +48,9 @@ export const DescriptionVersionsRoot: React.FC<Props> = observer((props) => {
const versionsCount = versions?.length ?? 0;
const activeVersionDetails = versions?.find((version) => version.id === activeVersionId);
const activeVersionIndex = versions?.findIndex((version) => version.id === activeVersionId);
const activeVersionDescription = activeVersionResponse
? (activeVersionResponse.description_html ?? "<p></p>")
: undefined;
const handleNavigation = useCallback(
(direction: "prev" | "next") => {
@@ -64,7 +67,7 @@ export const DescriptionVersionsRoot: React.FC<Props> = observer((props) => {
return (
<>
<DescriptionVersionsModal
activeVersionDescription={activeVersionResponse?.description_html ?? "<p></p>"}
activeVersionDescription={activeVersionDescription}
activeVersionDetails={activeVersionDetails}
handleClose={() => {
setIsModalOpen(false);
@@ -5,7 +5,8 @@ import { Controller, useForm } from "react-hook-form";
import { X } from "lucide-react";
import { Dialog, Transition } from "@headlessui/react";
import { Button, Calendar } from "@plane/ui";
import { Calendar } from "@plane/propel/calendar";
import { Button } from "@plane/ui";
import { renderFormattedPayloadDate, renderFormattedDate, getDate } from "@plane/utils";
import { DateFilterSelect } from "./date-filter-select";
@@ -94,13 +95,11 @@ export const DateFilterModal: React.FC<Props> = ({ title, handleClose, isOpen, o
const date2Value = getDate(watch("date2"));
return (
<Calendar
classNames={{
root: ` border border-custom-border-200 p-3 rounded-md`,
}}
className="rounded-md border border-custom-border-200 p-3"
captionLayout="dropdown"
selected={dateValue}
defaultMonth={dateValue}
onSelect={(date) => {
onSelect={(date: Date | undefined) => {
if (!date) return;
onChange(date);
}}
@@ -119,13 +118,11 @@ export const DateFilterModal: React.FC<Props> = ({ title, handleClose, isOpen, o
const date1Value = getDate(watch("date1"));
return (
<Calendar
classNames={{
root: ` border border-custom-border-200 p-3 rounded-md`,
}}
className="rounded-md border border-custom-border-200 p-3"
captionLayout="dropdown"
selected={dateValue}
defaultMonth={dateValue}
onSelect={(date) => {
onSelect={(date: Date | undefined) => {
if (!date) return;
onChange(date);
}}
@@ -2,7 +2,8 @@
import React from "react";
import { CalendarDays } from "lucide-react";
// ui
import { CustomSelect, CalendarAfterIcon, CalendarBeforeIcon } from "@plane/ui";
import { CalendarAfterIcon, CalendarBeforeIcon } from "@plane/propel/icons";
import { CustomSelect } from "@plane/ui";
type Props = {
title: string;
@@ -8,10 +8,11 @@ import { CalendarCheck } from "lucide-react";
import { Tab } from "@headlessui/react";
// plane imports
import { useTranslation } from "@plane/i18n";
import { PriorityIcon } from "@plane/propel/icons";
import { Tooltip } from "@plane/propel/tooltip";
import { EIssuesStoreType, ICycle, IIssueFilterOptions } from "@plane/types";
// ui
import { Loader, PriorityIcon, Avatar } from "@plane/ui";
import { Loader, Avatar } from "@plane/ui";
import { cn, renderFormattedDate, renderFormattedDateWithoutYear, getFileURL } from "@plane/utils";
// components
import { SingleProgressStats } from "@/components/core/sidebar/single-progress-stats";
@@ -6,6 +6,7 @@ import Image from "next/image";
import { Tab } from "@headlessui/react";
// plane imports
import { useTranslation } from "@plane/i18n";
import { StateGroupIcon } from "@plane/propel/icons";
import {
IIssueFilterOptions,
IIssueFilters,
@@ -14,7 +15,7 @@ import {
TCyclePlotType,
TStateGroups,
} from "@plane/types";
import { Avatar, StateGroupIcon } from "@plane/ui";
import { Avatar } from "@plane/ui";
import { cn, getFileURL } from "@plane/utils";
// components
import { SingleProgressStats } from "@/components/core/sidebar/single-progress-stats";
@@ -3,9 +3,10 @@
import React, { FC } from "react";
import { ChevronDown } from "lucide-react";
// types
import { CycleGroupIcon } from "@plane/propel/icons";
import { TCycleGroups } from "@plane/types";
// icons
import { Row, CycleGroupIcon } from "@plane/ui";
import { Row } from "@plane/ui";
// helpers
import { cn } from "@plane/utils";
@@ -15,9 +15,10 @@ import {
} from "@plane/constants";
import { useLocalStorage } from "@plane/hooks";
import { useTranslation } from "@plane/i18n";
import { LayersIcon, TransferIcon } from "@plane/propel/icons";
import { Tooltip } from "@plane/propel/tooltip";
import { ICycle, TCycleGroups } from "@plane/types";
import { Avatar, AvatarGroup, FavoriteStar, LayersIcon, TransferIcon, setPromiseToast } from "@plane/ui";
import { Avatar, AvatarGroup, FavoriteStar, setPromiseToast } from "@plane/ui";
import { getDate, getFileURL, generateQueryParams } from "@plane/utils";
// components
import { DateRangeDropdown } from "@/components/dropdowns/date-range";
@@ -13,7 +13,8 @@ import {
CYCLE_TRACKER_ELEMENTS,
} from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { ArchiveIcon, ContextMenu, CustomMenu, TContextMenuItem, TOAST_TYPE, setToast } from "@plane/ui";
import { ArchiveIcon } from "@plane/propel/icons";
import { ContextMenu, CustomMenu, TContextMenuItem, TOAST_TYPE, setToast } from "@plane/ui";
import { copyUrlToClipboard, cn } from "@plane/utils";
// helpers
// hooks
@@ -5,11 +5,12 @@ import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { AlertCircle, Search, X } from "lucide-react";
import { Dialog, Transition } from "@headlessui/react";
import { ContrastIcon, TransferIcon } from "@plane/propel/icons";
import { EIssuesStoreType } from "@plane/types";
// hooks
// ui
//icons
import { ContrastIcon, TransferIcon, TOAST_TYPE, setToast } from "@plane/ui";
import { TOAST_TYPE, setToast } from "@plane/ui";
import { useCycle } from "@/hooks/store/use-cycle";
import { useIssues } from "@/hooks/store/use-issues";
//icons
@@ -2,7 +2,8 @@
import React from "react";
import { AlertCircle } from "lucide-react";
// ui
import { Button, TransferIcon } from "@plane/ui";
import { TransferIcon } from "@plane/propel/icons";
import { Button } from "@plane/ui";
type Props = {
handleClick: () => void;
@@ -11,9 +11,9 @@ import { Combobox } from "@headlessui/react";
// i18n
import { useTranslation } from "@plane/i18n";
// icon
import { ContrastIcon, CycleGroupIcon } from "@plane/propel/icons";
import { TCycleGroups } from "@plane/types";
// ui
import { ContrastIcon, CycleGroupIcon } from "@plane/ui";
// store hooks
import { useCycle } from "@/hooks/store/use-cycle";
import { usePlatformOS } from "@/hooks/use-platform-os";
@@ -5,7 +5,8 @@ import { observer } from "mobx-react";
import { ChevronDown } from "lucide-react";
import { useTranslation } from "@plane/i18n";
// ui
import { ComboDropDown, ContrastIcon } from "@plane/ui";
import { ContrastIcon } from "@plane/propel/icons";
import { ComboDropDown } from "@plane/ui";
// helpers
import { cn } from "@plane/utils";
// hooks
@@ -3,14 +3,14 @@
import React, { useEffect, useRef, useState } from "react";
import { Placement } from "@popperjs/core";
import { observer } from "mobx-react";
import { DateRange, Matcher } from "react-day-picker";
import { usePopper } from "react-popper";
import { ArrowRight, CalendarCheck2, CalendarDays, X } from "lucide-react";
import { Combobox } from "@headlessui/react";
// plane imports
import { useTranslation } from "@plane/i18n";
// ui
import { ComboDropDown, Calendar } from "@plane/ui";
import { Calendar, DateRange, Matcher } from "@plane/propel/calendar";
import { ComboDropDown } from "@plane/ui";
import { cn, renderFormattedDate } from "@plane/utils";
// helpers
// hooks
@@ -271,10 +271,10 @@ export const DateRangeDropdown: React.FC<Props> = observer((props) => {
{...attributes.popper}
>
<Calendar
className="rounded-md border border-custom-border-200 p-3"
captionLayout="dropdown"
classNames={{ root: `p-3 rounded-md` }}
selected={dateRange}
onSelect={(val) => {
onSelect={(val: DateRange | undefined) => {
onSelect?.(val);
}}
mode="range"
+4 -4
View File
@@ -1,12 +1,12 @@
import React, { useRef, useState } from "react";
import { observer } from "mobx-react";
import { Matcher } from "react-day-picker";
import { createPortal } from "react-dom";
import { usePopper } from "react-popper";
import { CalendarDays, X } from "lucide-react";
import { Combobox } from "@headlessui/react";
// ui
import { ComboDropDown, Calendar } from "@plane/ui";
import { Calendar, Matcher } from "@plane/propel/calendar";
import { ComboDropDown } from "@plane/ui";
import { cn, renderFormattedDate, getDate } from "@plane/utils";
// helpers
// hooks
@@ -178,11 +178,11 @@ export const DateDropdown: React.FC<Props> = observer((props) => {
{...attributes.popper}
>
<Calendar
className="rounded-md border border-custom-border-200 p-3"
captionLayout="dropdown"
classNames={{ root: `p-3 rounded-md` }}
selected={getDate(value)}
defaultMonth={getDate(value)}
onSelect={(date) => {
onSelect={(date: Date | undefined) => {
dropdownOnChange(date ?? null);
}}
showOutsideDays

Some files were not shown because too many files have changed in this diff Show More