Compare commits

..

11 Commits

Author SHA1 Message Date
gurusainath 4ec2c61e0d chore: updated the discriptiob_binary key 2024-11-14 17:40:52 +05:30
gurusainath 641ef9fd14 chore: handled descritpion_binary error 2024-11-14 17:10:55 +05:30
gurusainath 68a4d02847 chore: added description_binary in issues v2 pagination list 2024-11-12 15:20:02 +05:30
Anmol Singh Bhatia db8c4f92e8 chore: theme and code refactor (#5983)
* chore: added pi colors

* chore: de-dupe modal height

---------

Co-authored-by: gakshita <akshitagoyal1516@gmail.com>
2024-11-11 19:53:43 +05:30
Anmol Singh Bhatia a6cc2c93f8 chore: worklog enhancements (#5982) 2024-11-11 19:27:07 +05:30
Bavisetti Narayan 0428ea06f6 chore: filter the deleted issue assignee (#5984) 2024-11-11 19:25:38 +05:30
Aaryan Khandelwal 7082f7014d style: remove unnecessary bottom padding from the rich text editor (#5976) 2024-11-11 16:11:34 +05:30
Anmol Singh Bhatia c7c729d81b [WEB-2283] fix: create issue modal parent select ui (#5980)
* fix: create issue modal parent select ui

* chore: code refactor
2024-11-11 16:11:10 +05:30
Aaryan Khandelwal 97eb8d43d4 style: updated margins and font styles for editor (#5978)
* style: updated margins and font styles for editor

* fix: code block font size in small font

* fix: remove duplicate code
2024-11-11 16:10:47 +05:30
Anmol Singh Bhatia 1217af1d5f chore: restrict sub-issue to have different project id than parent (#5981) 2024-11-11 16:10:27 +05:30
Bavisetti Narayan 13083a77eb chore: enable intake from project settings (#5977) 2024-11-09 17:01:21 +05:30
31 changed files with 553 additions and 194 deletions
@@ -13,6 +13,7 @@ from .user import (
from .workspace import (
WorkSpaceSerializer,
WorkSpaceMemberSerializer,
TeamSerializer,
WorkSpaceMemberInviteSerializer,
WorkspaceLiteSerializer,
WorkspaceThemeSerializer,
@@ -9,6 +9,8 @@ from plane.db.models import (
User,
Workspace,
WorkspaceMember,
Team,
TeamMember,
WorkspaceMemberInvite,
WorkspaceTheme,
WorkspaceUserProperties,
@@ -97,6 +99,57 @@ class WorkSpaceMemberInviteSerializer(BaseSerializer):
"updated_at",
]
class TeamSerializer(BaseSerializer):
members_detail = UserLiteSerializer(
read_only=True, source="members", many=True
)
members = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()),
write_only=True,
required=False,
)
class Meta:
model = Team
fields = "__all__"
read_only_fields = [
"workspace",
"created_by",
"updated_by",
"created_at",
"updated_at",
]
def create(self, validated_data, **kwargs):
if "members" in validated_data:
members = validated_data.pop("members")
workspace = self.context["workspace"]
team = Team.objects.create(**validated_data, workspace=workspace)
team_members = [
TeamMember(member=member, team=team, workspace=workspace)
for member in members
]
TeamMember.objects.bulk_create(team_members, batch_size=10)
return team
team = Team.objects.create(**validated_data)
return team
def update(self, instance, validated_data):
if "members" in validated_data:
members = validated_data.pop("members")
TeamMember.objects.filter(team=instance).delete()
team_members = [
TeamMember(
member=member, team=instance, workspace=instance.workspace
)
for member in members
]
TeamMember.objects.bulk_create(team_members, batch_size=10)
return super().update(instance, validated_data)
return super().update(instance, validated_data)
class WorkspaceThemeSerializer(BaseSerializer):
class Meta:
model = WorkspaceTheme
+6
View File
@@ -7,6 +7,7 @@ from plane.app.views import (
ProjectMemberViewSet,
ProjectMemberUserEndpoint,
ProjectJoinEndpoint,
AddTeamToProjectEndpoint,
ProjectUserViewsEndpoint,
ProjectIdentifierEndpoint,
ProjectFavoritesViewSet,
@@ -115,6 +116,11 @@ urlpatterns = [
),
name="project-member",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/team-invite/",
AddTeamToProjectEndpoint.as_view(),
name="projects",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/project-views/",
ProjectUserViewsEndpoint.as_view(),
+23
View File
@@ -10,6 +10,7 @@ from plane.app.views import (
WorkspaceMemberUserEndpoint,
WorkspaceMemberUserViewsEndpoint,
WorkSpaceAvailabilityCheckEndpoint,
TeamMemberViewSet,
UserLastProjectWithWorkspaceEndpoint,
WorkspaceThemeViewSet,
WorkspaceUserProfileStatsEndpoint,
@@ -126,6 +127,28 @@ urlpatterns = [
),
name="leave-workspace-members",
),
path(
"workspaces/<str:slug>/teams/",
TeamMemberViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="workspace-team-members",
),
path(
"workspaces/<str:slug>/teams/<uuid:pk>/",
TeamMemberViewSet.as_view(
{
"put": "update",
"patch": "partial_update",
"delete": "destroy",
"get": "retrieve",
}
),
name="workspace-team-members",
),
path(
"users/last-visited-workspace/",
UserLastProjectWithWorkspaceEndpoint.as_view(),
+2
View File
@@ -16,6 +16,7 @@ from .project.invite import (
from .project.member import (
ProjectMemberViewSet,
AddTeamToProjectEndpoint,
ProjectMemberUserEndpoint,
UserProjectRolesEndpoint,
)
@@ -48,6 +49,7 @@ from .workspace.favorite import (
from .workspace.member import (
WorkSpaceMemberViewSet,
TeamMemberViewSet,
WorkspaceMemberUserEndpoint,
WorkspaceProjectMemberEndpoint,
WorkspaceMemberUserViewsEndpoint,
+30 -8
View File
@@ -1,5 +1,6 @@
# Python imports
import json
import base64
# Django imports
from django.contrib.postgres.aggregates import ArrayAgg
@@ -480,9 +481,7 @@ class IssueViewSet(BaseViewSet):
project = Project.objects.get(pk=project_id, workspace__slug=slug)
issue = (
Issue.objects.filter(
project_id=self.kwargs.get("project_id")
)
Issue.objects.filter(project_id=self.kwargs.get("project_id"))
.filter(workspace__slug=self.kwargs.get("slug"))
.select_related("workspace", "project", "state", "parent")
.prefetch_related("assignees", "labels", "issue_module__module")
@@ -517,7 +516,7 @@ class IssueViewSet(BaseViewSet):
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
)
.filter(pk=pk)
.annotate(
label_ids=Coalesce(
@@ -852,9 +851,21 @@ class IssuePaginatedViewSet(BaseViewSet):
)
).distinct()
def process_paginated_result(self, fields, results, timezone):
def process_paginated_result(
self, fields, results, timezone, description_binary_required=False
):
paginated_data = results.values(*fields)
# handling the description binary field
if description_binary_required:
for item in paginated_data:
if item["description_binary"]:
item["description_binary"] = base64.b64encode(
item["description_binary"]
).decode("utf-8")
else:
item["description_binary"] = None
# converting the datetime fields in paginated data
datetime_fields = ["created_at", "updated_at"]
paginated_data = user_timezone_converter(
@@ -867,6 +878,9 @@ class IssuePaginatedViewSet(BaseViewSet):
def list(self, request, slug, project_id):
cursor = request.GET.get("cursor", None)
is_description_required = request.GET.get("description", "false")
is_description_binary_required = request.GET.get(
"description_binary", "false"
)
updated_at = request.GET.get("updated_at__gt", None)
# required fields
@@ -902,6 +916,9 @@ class IssuePaginatedViewSet(BaseViewSet):
if str(is_description_required).lower() == "true":
required_fields.append("description_html")
if str(is_description_binary_required).lower() == "true":
required_fields.append("description_binary")
# querying issues
base_queryset = Issue.issue_objects.filter(
workspace__slug=slug, project_id=project_id
@@ -966,12 +983,19 @@ class IssuePaginatedViewSet(BaseViewSet):
),
)
is_description_binary_required = False
if required_fields.__contains__("description_binary"):
is_description_binary_required = True
paginated_data = paginate(
base_queryset=base_queryset,
queryset=queryset,
cursor=cursor,
on_result=lambda results: self.process_paginated_result(
required_fields, results, request.user.user_timezone
required_fields,
results,
request.user.user_timezone,
is_description_binary_required,
),
)
@@ -1077,7 +1101,6 @@ class IssueDetailEndpoint(BaseAPIView):
class IssueBulkUpdateDateEndpoint(BaseAPIView):
def validate_dates(
self, current_start, current_target, new_start, new_target
):
@@ -1093,7 +1116,6 @@ class IssueBulkUpdateDateEndpoint(BaseAPIView):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
def post(self, request, slug, project_id):
updates = request.data.get("updates", [])
issue_ids = [update["id"] for update in updates]
+8 -2
View File
@@ -429,6 +429,9 @@ class ProjectViewSet(BaseViewSet):
)
workspace = Workspace.objects.get(slug=slug)
intake_view = request.data.get(
"inbox_view", request.data.get("intake_view", False)
)
project = Project.objects.get(pk=pk)
current_instance = json.dumps(
@@ -442,14 +445,17 @@ class ProjectViewSet(BaseViewSet):
serializer = ProjectSerializer(
project,
data={**request.data},
data={
**request.data,
"intake_view": intake_view,
},
context={"workspace_id": workspace.id},
partial=True,
)
if serializer.is_valid():
serializer.save()
if serializer.data["intake_view"] or request.data.get("inbox_view", False):
if intake_view:
intake = Intake.objects.filter(
project=project,
is_default=True,
@@ -21,6 +21,7 @@ from plane.db.models import (
Project,
ProjectMember,
Workspace,
TeamMember,
IssueUserProperty,
WorkspaceMember,
)
@@ -341,6 +342,54 @@ class ProjectMemberViewSet(BaseViewSet):
return Response(status=status.HTTP_204_NO_CONTENT)
class AddTeamToProjectEndpoint(BaseAPIView):
permission_classes = [
ProjectBasePermission,
]
def post(self, request, slug, project_id):
team_members = TeamMember.objects.filter(
workspace__slug=slug, team__in=request.data.get("teams", [])
).values_list("member", flat=True)
if len(team_members) == 0:
return Response(
{"error": "No such team exists"},
status=status.HTTP_400_BAD_REQUEST,
)
workspace = Workspace.objects.get(slug=slug)
project_members = []
issue_props = []
for member in team_members:
project_members.append(
ProjectMember(
project_id=project_id,
member_id=member,
workspace=workspace,
created_by=request.user,
)
)
issue_props.append(
IssueUserProperty(
project_id=project_id,
user_id=member,
workspace=workspace,
created_by=request.user,
)
)
ProjectMember.objects.bulk_create(
project_members, batch_size=10, ignore_conflicts=True
)
_ = IssueUserProperty.objects.bulk_create(
issue_props, batch_size=10, ignore_conflicts=True
)
serializer = ProjectMemberSerializer(project_members, many=True)
return Response(serializer.data, status=status.HTTP_201_CREATED)
class ProjectMemberUserEndpoint(BaseAPIView):
+62 -1
View File
@@ -24,6 +24,7 @@ from plane.app.permissions import (
# Module imports
from plane.app.serializers import (
ProjectMemberRoleSerializer,
TeamSerializer,
UserLiteSerializer,
WorkspaceMemberAdminSerializer,
WorkspaceMemberMeSerializer,
@@ -33,6 +34,7 @@ from plane.app.views.base import BaseAPIView
from plane.db.models import (
Project,
ProjectMember,
Team,
User,
Workspace,
WorkspaceMember,
@@ -349,4 +351,63 @@ class WorkspaceProjectMemberEndpoint(BaseAPIView):
project_members_dict[str(project_id)] = []
project_members_dict[str(project_id)].append(project_member)
return Response(project_members_dict, status=status.HTTP_200_OK)
return Response(project_members_dict, status=status.HTTP_200_OK)
class TeamMemberViewSet(BaseViewSet):
serializer_class = TeamSerializer
model = Team
permission_classes = [
WorkSpaceAdminPermission,
]
search_fields = [
"member__display_name",
"member__first_name",
]
def get_queryset(self):
return self.filter_queryset(
super()
.get_queryset()
.filter(workspace__slug=self.kwargs.get("slug"))
.select_related("workspace", "workspace__owner")
.prefetch_related("members")
)
def create(self, request, slug):
members = list(
WorkspaceMember.objects.filter(
workspace__slug=slug,
member__id__in=request.data.get("members", []),
is_active=True,
)
.annotate(member_str_id=Cast("member", output_field=CharField()))
.distinct()
.values_list("member_str_id", flat=True)
)
if len(members) != len(request.data.get("members", [])):
users = list(
set(request.data.get("members", [])).difference(members)
)
users = User.objects.filter(pk__in=users)
serializer = UserLiteSerializer(users, many=True)
return Response(
{
"error": f"{len(users)} of the member(s) are not a part of the workspace",
"members": serializer.data,
},
status=status.HTTP_400_BAD_REQUEST,
)
workspace = Workspace.objects.get(slug=slug)
serializer = TeamSerializer(
data=request.data, context={"workspace": workspace}
)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@@ -1,74 +0,0 @@
# Generated by Django 4.2.15 on 2024-11-08 13:30
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('db', '0084_remove_label_label_unique_name_project_when_deleted_at_null_and_more'),
]
operations = [
migrations.AlterUniqueTogether(
name='teammember',
unique_together=None,
),
migrations.RemoveField(
model_name='teammember',
name='created_by',
),
migrations.RemoveField(
model_name='teammember',
name='member',
),
migrations.RemoveField(
model_name='teammember',
name='team',
),
migrations.RemoveField(
model_name='teammember',
name='updated_by',
),
migrations.RemoveField(
model_name='teammember',
name='workspace',
),
migrations.AlterUniqueTogether(
name='teampage',
unique_together=None,
),
migrations.RemoveField(
model_name='teampage',
name='created_by',
),
migrations.RemoveField(
model_name='teampage',
name='page',
),
migrations.RemoveField(
model_name='teampage',
name='team',
),
migrations.RemoveField(
model_name='teampage',
name='updated_by',
),
migrations.RemoveField(
model_name='teampage',
name='workspace',
),
migrations.RemoveField(
model_name='page',
name='teams',
),
migrations.DeleteModel(
name='Team',
),
migrations.DeleteModel(
name='TeamMember',
),
migrations.DeleteModel(
name='TeamPage',
),
]
+2
View File
@@ -77,6 +77,8 @@ from .user import Account, Profile, User
from .view import IssueView
from .webhook import Webhook, WebhookLog
from .workspace import (
Team,
TeamMember,
Workspace,
WorkspaceBaseModel,
WorkspaceMember,
+30
View File
@@ -52,6 +52,9 @@ class Page(BaseModel):
projects = models.ManyToManyField(
"db.Project", related_name="pages", through="db.ProjectPage"
)
teams = models.ManyToManyField(
"db.Team", related_name="pages", through="db.TeamPage"
)
class Meta:
verbose_name = "Page"
@@ -166,6 +169,33 @@ class ProjectPage(BaseModel):
def __str__(self):
return f"{self.project.name} {self.page.name}"
class TeamPage(BaseModel):
team = models.ForeignKey(
"db.Team", on_delete=models.CASCADE, related_name="team_pages"
)
page = models.ForeignKey(
"db.Page", on_delete=models.CASCADE, related_name="team_pages"
)
workspace = models.ForeignKey(
"db.Workspace", on_delete=models.CASCADE, related_name="team_pages"
)
class Meta:
unique_together = ["team", "page", "deleted_at"]
constraints = [
models.UniqueConstraint(
fields=["team", "page"],
condition=models.Q(deleted_at__isnull=True),
name="team_page_unique_team_page_when_deleted_at_null",
)
]
verbose_name = "Team Page"
verbose_name_plural = "Team Pages"
db_table = "team_pages"
ordering = ("-created_at",)
class PageVersion(BaseModel):
workspace = models.ForeignKey(
"db.Workspace",
+65
View File
@@ -259,6 +259,71 @@ class WorkspaceMemberInvite(BaseModel):
return f"{self.workspace.name} {self.email} {self.accepted}"
class Team(BaseModel):
name = models.CharField(max_length=255, verbose_name="Team Name")
description = models.TextField(verbose_name="Team Description", blank=True)
members = models.ManyToManyField(
settings.AUTH_USER_MODEL,
blank=True,
related_name="members",
through="TeamMember",
through_fields=("team", "member"),
)
workspace = models.ForeignKey(
Workspace, on_delete=models.CASCADE, related_name="workspace_team"
)
logo_props = models.JSONField(default=dict)
def __str__(self):
"""Return name of the team"""
return f"{self.name} <{self.workspace.name}>"
class Meta:
unique_together = ["name", "workspace", "deleted_at"]
constraints = [
models.UniqueConstraint(
fields=["name", "workspace"],
condition=models.Q(deleted_at__isnull=True),
name="team_unique_name_workspace_when_deleted_at_null",
)
]
verbose_name = "Team"
verbose_name_plural = "Teams"
db_table = "teams"
ordering = ("-created_at",)
class TeamMember(BaseModel):
workspace = models.ForeignKey(
Workspace, on_delete=models.CASCADE, related_name="team_member"
)
team = models.ForeignKey(
Team, on_delete=models.CASCADE, related_name="team_member"
)
member = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="team_member",
)
def __str__(self):
return self.team.name
class Meta:
unique_together = ["team", "member", "deleted_at"]
constraints = [
models.UniqueConstraint(
fields=["team", "member"],
condition=models.Q(deleted_at__isnull=True),
name="team_member_unique_team_member_when_deleted_at_null",
)
]
verbose_name = "Team Member"
verbose_name_plural = "Team Members"
db_table = "team_members"
ordering = ("-created_at",)
class WorkspaceTheme(BaseModel):
workspace = models.ForeignKey(
"db.Workspace", on_delete=models.CASCADE, related_name="themes"
+1
View File
@@ -233,6 +233,7 @@ def filter_assignees(params, issue_filter, method, prefix=""):
and params.get("assignees") != "null"
):
issue_filter[f"{prefix}assignees__in"] = params.get("assignees")
issue_filter[f"{prefix}issue_assignee__deleted_at__isnull"] = True
return issue_filter
@@ -46,7 +46,7 @@ export const CoreEditorExtensionsWithoutProps = [
CustomQuoteExtension,
CustomHorizontalRule.configure({
HTMLAttributes: {
class: "my-4 border-custom-border-400",
class: "py-4 border-custom-border-400",
},
}),
CustomLinkExtension.configure({
@@ -78,7 +78,7 @@ export const CoreEditorExtensions = (args: TArguments) => {
DropHandlerExtension(),
CustomHorizontalRule.configure({
HTMLAttributes: {
class: "my-4 border-custom-border-400",
class: "py-4 border-custom-border-400",
},
}),
CustomKeymap,
@@ -67,7 +67,7 @@ export const CoreReadOnlyEditorExtensions = (props: Props) => {
CustomQuoteExtension,
CustomHorizontalRule.configure({
HTMLAttributes: {
class: "my-4 border-custom-border-400",
class: "py-4 border-custom-border-400",
},
}),
CustomLinkExtension.configure({
+40 -30
View File
@@ -12,7 +12,7 @@
cursor: text;
font-family: var(--font-style);
font-size: var(--font-size-regular);
line-height: 1.2;
font-weight: 400;
color: inherit;
-moz-box-sizing: border-box;
box-sizing: border-box;
@@ -248,11 +248,6 @@ div[data-type="horizontalRule"] {
}
}
/* image resizer */
.moveable-control-box {
z-index: 10 !important;
}
/* Cursor styles for the inline code blocks */
@keyframes blink {
49% {
@@ -314,13 +309,23 @@ ul[data-type="taskList"] ul[data-type="taskList"] {
}
/* end numbered, bulleted and to-do lists spacing */
h1,
h2,
h3,
h4,
h5,
h6,
p {
margin: 0 !important;
}
/* tailwind typography */
.prose :where(h1):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
&:not(:first-child) {
margin-top: 2rem;
padding-top: 28px;
}
margin-bottom: 4px;
padding-bottom: 4px;
font-size: var(--font-size-h1);
line-height: var(--line-height-h1);
font-weight: 600;
@@ -328,10 +333,10 @@ ul[data-type="taskList"] ul[data-type="taskList"] {
.prose :where(h2):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
&:not(:first-child) {
margin-top: 1.4rem;
padding-top: 28px;
}
margin-bottom: 1px;
padding-bottom: 4px;
font-size: var(--font-size-h2);
line-height: var(--line-height-h2);
font-weight: 600;
@@ -339,10 +344,10 @@ ul[data-type="taskList"] ul[data-type="taskList"] {
.prose :where(h3):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
&:not(:first-child) {
margin-top: 1rem;
padding-top: 28px;
}
margin-bottom: 1px;
padding-bottom: 4px;
font-size: var(--font-size-h3);
line-height: var(--line-height-h3);
font-weight: 600;
@@ -350,10 +355,10 @@ ul[data-type="taskList"] ul[data-type="taskList"] {
.prose :where(h4):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
&:not(:first-child) {
margin-top: 1rem;
padding-top: 28px;
}
margin-bottom: 1px;
padding-bottom: 4px;
font-size: var(--font-size-h4);
line-height: var(--line-height-h4);
font-weight: 600;
@@ -361,10 +366,10 @@ ul[data-type="taskList"] ul[data-type="taskList"] {
.prose :where(h5):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
&:not(:first-child) {
margin-top: 1rem;
padding-top: 20px;
}
margin-bottom: 1px;
padding-bottom: 4px;
font-size: var(--font-size-h5);
line-height: var(--line-height-h5);
font-weight: 600;
@@ -372,30 +377,40 @@ ul[data-type="taskList"] ul[data-type="taskList"] {
.prose :where(h6):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
&:not(:first-child) {
margin-top: 1rem;
padding-top: 20px;
}
margin-bottom: 1px;
padding-bottom: 4px;
font-size: var(--font-size-h6);
line-height: var(--line-height-h6);
font-weight: 600;
}
.prose :where(p):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
&:not(:first-child) {
margin-top: 0.25rem;
}
&:first-child {
margin-top: 0;
padding-top: 0;
}
&:not(:first-child) {
padding-top: 4px;
}
&:last-child {
padding-bottom: 4px;
}
&:not(:last-child) {
padding-bottom: 8px;
}
margin-bottom: 1px;
padding: 3px 0;
font-size: var(--font-size-regular);
line-height: var(--line-height-regular);
}
p + p {
padding-top: 8px !important;
}
.prose :where(ol):not(:where([class~="not-prose"], [class~="not-prose"] *)) li p,
.prose :where(ul):not(:where([class~="not-prose"], [class~="not-prose"] *)) li p {
font-size: var(--font-size-list);
@@ -432,11 +447,6 @@ ul[data-type="taskList"] ul[data-type="taskList"] {
[data-text-color="purple"] {
color: var(--editor-colors-purple-text);
}
/* [data-text-color="pink-blue-gradient"] {
background-clip: text;
color: transparent;
background-image: linear-gradient(90deg, #a961cd 50%, #e75962 100%);
} */
/* end text colors */
/* background colors */
+4 -4
View File
@@ -46,8 +46,8 @@
--font-size-h5: 1.125rem;
--font-size-h6: 1rem;
--font-size-regular: 1rem;
--font-size-code: 0.85rem;
--font-size-list: var(--font-size-regular);
--font-size-code: var(--font-size-regular);
--line-height-h1: 2.25rem;
--line-height-h2: 2rem;
@@ -56,8 +56,8 @@
--line-height-h5: 1.5rem;
--line-height-h6: 1.5rem;
--line-height-regular: 1.5rem;
--line-height-code: 1.5rem;
--line-height-list: var(--line-height-regular);
--line-height-code: var(--line-height-regular);
}
&.small-font {
--font-size-h1: 1.4rem;
@@ -67,8 +67,8 @@
--font-size-h5: 0.9rem;
--font-size-h6: 0.8rem;
--font-size-regular: 0.8rem;
--font-size-code: 0.8rem;
--font-size-list: var(--font-size-regular);
--font-size-code: var(--font-size-regular);
--line-height-h1: 1.8rem;
--line-height-h2: 1.6rem;
@@ -77,8 +77,8 @@
--line-height-h5: 1.2rem;
--line-height-h6: 1.2rem;
--line-height-regular: 1.2rem;
--line-height-code: 1.2rem;
--line-height-list: var(--line-height-regular);
--line-height-code: var(--line-height-regular);
}
/* end font sizes and line heights */
+38
View File
@@ -101,6 +101,19 @@
--color-sidebar-shadow-2xl: var(--color-shadow-2xl);
--color-sidebar-shadow-3xl: var(--color-shadow-3xl);
--color-sidebar-shadow-4xl: var(--color-shadow-4xl);
/* pi */
--color-pi-50: var(--color-background-90);
--color-pi-100: var(--color-background-90);
--color-pi-200: var(--color-primary-200);
--color-pi-300: var(--color-primary-200);
--color-pi-400: var(--color-primary-200);
--color-pi-500: var(--color-primary-200);
--color-pi-600: 151, 150, 246;
--color-pi-700: var(--color-primary-100);
--color-pi-800: 57, 56, 149;
--color-pi-900: 30, 29, 78;
--color-pi-950: 14, 14, 37;
}
[data-theme="light"],
@@ -110,6 +123,19 @@
--color-background-100: 255, 255, 255; /* primary bg */
--color-background-90: 247, 247, 247; /* secondary bg */
--color-background-80: 232, 232, 232; /* tertiary bg */
/* pi */
--color-pi-50: var(--color-background-90);
--color-pi-100: var(--color-background-90);
--color-pi-200: var(--color-primary-200);
--color-pi-300: var(--color-primary-200);
--color-pi-400: var(--color-primary-200);
--color-pi-500: var(--color-primary-200);
--color-pi-600: 151, 150, 246;
--color-pi-700: var(--color-primary-100);
--color-pi-800: 57, 56, 149;
--color-pi-900: 30, 29, 78;
--color-pi-950: 14, 14, 37;
}
[data-theme="light"] {
@@ -200,6 +226,18 @@
--color-shadow-xl: 0px 0px 14px 0px rgba(0, 0, 0, 0.25), 0px 6px 10px 0px rgba(0, 0, 0, 0.55);
--color-shadow-2xl: 0px 0px 18px 0px rgba(0, 0, 0, 0.25), 0px 8px 12px 0px rgba(0, 0, 0, 0.6);
--color-shadow-3xl: 0px 4px 24px 0px rgba(0, 0, 0, 0.3), 0px 12px 40px 0px rgba(0, 0, 0, 0.65);
/* pi */
--color-pi-50: var(--color-background-90);
--color-pi-100: var(--color-background-90);
--color-pi-200: var(--color-primary-200);
--color-pi-300: var(--color-primary-200);
--color-pi-400: var(--color-primary-200);
--color-pi-500: var(--color-primary-200);
--color-pi-600: 151, 150, 246;
--color-pi-700: var(--color-primary-100);
--color-pi-800: 57, 56, 149;
--color-pi-900: 30, 29, 78;
--color-pi-950: 14, 14, 37;
}
[data-theme="dark"] {
@@ -286,7 +286,7 @@ export const InboxIssueCreateRoot: FC<TInboxIssueCreateRoot> = observer((props)
{shouldRenderDuplicateModal && (
<div
ref={modalContainerRef}
className="relative flex flex-col gap-2.5 h-full px-3 py-4 rounded-lg shadow-xl bg-pi-50"
className="relative flex flex-col gap-2.5 px-3 py-4 rounded-lg shadow-xl bg-pi-50"
style={{ maxHeight: formRef?.current?.offsetHeight ? `${formRef.current.offsetHeight}px` : "436px" }}
>
<DuplicateModalRoot
@@ -91,7 +91,7 @@ export const InboxIssueProperties: FC<TInboxIssueProperties> = observer((props)
{/* labels */}
<div className="h-7">
<IssueLabelSelect
setIsOpen={() => { }}
setIsOpen={() => {}}
value={data?.label_ids || []}
onChange={(labelIds) => handleData("label_ids", labelIds)}
projectId={projectId}
@@ -171,13 +171,13 @@ export const InboxIssueProperties: FC<TInboxIssueProperties> = observer((props)
{/* add parent */}
{isVisible && (
<>
<div className="h-7">
{selectedParentIssue ? (
<CustomMenu
customButton={
<button
type="button"
className="flex cursor-pointer items-center justify-between gap-1 rounded border-[0.5px] border-custom-border-300 px-2 py-1.5 text-xs hover:bg-custom-background-80"
className="flex cursor-pointer items-center justify-between gap-1 h-full rounded border-[0.5px] border-custom-border-300 px-2 py-0.5 text-xs hover:bg-custom-background-80"
>
<LayoutPanelTop className="h-3 w-3 flex-shrink-0" />
<span className="whitespace-nowrap">
@@ -188,6 +188,8 @@ export const InboxIssueProperties: FC<TInboxIssueProperties> = observer((props)
</button>
}
placement="bottom-start"
className="h-full w-full"
customButtonClassName="h-full"
tabIndex={getIndex("parent_id")}
>
<>
@@ -208,7 +210,7 @@ export const InboxIssueProperties: FC<TInboxIssueProperties> = observer((props)
) : (
<button
type="button"
className="flex cursor-pointer items-center justify-between gap-1 rounded border-[0.5px] border-custom-border-300 px-2 py-1.5 text-xs hover:bg-custom-background-80"
className="flex cursor-pointer items-center justify-between gap-1 h-full rounded border-[0.5px] border-custom-border-300 px-2 py-0.5 text-xs hover:bg-custom-background-80"
onClick={() => setParentIssueModalOpen(true)}
>
<LayoutPanelTop className="h-3 w-3 flex-shrink-0" />
@@ -226,7 +228,7 @@ export const InboxIssueProperties: FC<TInboxIssueProperties> = observer((props)
projectId={projectId}
issueId={undefined}
/>
</>
</div>
)}
</div>
);
@@ -151,6 +151,7 @@ export const IssueDetailWidgetModals: FC<Props> = observer((props) => {
data={createUpdateModalData}
onClose={handleCreateUpdateModalClose}
onSubmit={handleCreateUpdateModalOnSubmit}
isProjectSelectionDisabled
/>
)}
@@ -162,7 +163,6 @@ export const IssueDetailWidgetModals: FC<Props> = observer((props) => {
handleClose={handleExistingIssuesModalClose}
searchParams={existingIssuesModalSearchParams}
handleOnSubmit={handleExistingIssuesModalOnSubmit}
workspaceLevelToggle
/>
)}
@@ -11,7 +11,7 @@ import { TOAST_TYPE, setToast } from "@plane/ui";
import { IssueCommentCreate } from "@/components/issues";
import { IssueActivityCommentRoot } from "@/components/issues/issue-detail";
// hooks
import { useIssueDetail, useProject, useUserPermissions } from "@/hooks/store";
import { useIssueDetail, useProject, useUser, useUserPermissions } from "@/hooks/store";
// plane web components
import { ActivityFilterRoot, IssueActivityWorklogCreateButton } from "@/plane-web/components/issues/worklog";
// plane web constants
@@ -38,15 +38,25 @@ export type TActivityOperations = {
export const IssueActivity: FC<TIssueActivity> = observer((props) => {
const { workspaceSlug, projectId, issueId, disabled = false, isIntakeIssue = false } = props;
// hooks
const { createComment, updateComment, removeComment } = useIssueDetail();
const { projectPermissionsByWorkspaceSlugAndProjectId } = useUserPermissions();
const { getProjectById } = useProject();
//derived values
const isGuest = (projectPermissionsByWorkspaceSlugAndProjectId(workspaceSlug, projectId) ?? EUserPermissions.GUEST) === EUserPermissions.GUEST;
const isWorklogButtonEnabled = !isIntakeIssue && !isGuest;
// state
const [selectedFilters, setSelectedFilters] = useState<TActivityFilters[]>(defaultActivityFilters);
// hooks
const {
issue: { getIssueById },
createComment,
updateComment,
removeComment,
} = useIssueDetail();
const { projectPermissionsByWorkspaceSlugAndProjectId } = useUserPermissions();
const { getProjectById } = useProject();
const { data: currentUser } = useUser();
//derived values
const issue = issueId ? getIssueById(issueId) : undefined;
const currentUserProjectRole = projectPermissionsByWorkspaceSlugAndProjectId(workspaceSlug, projectId);
const isAdmin = (currentUserProjectRole ?? EUserPermissions.GUEST) === EUserPermissions.ADMIN;
const isGuest = (currentUserProjectRole ?? EUserPermissions.GUEST) === EUserPermissions.GUEST;
const isAssigned = issue?.assignee_ids && currentUser?.id ? issue?.assignee_ids.includes(currentUser?.id) : false;
const isWorklogButtonEnabled = !isIntakeIssue && !isGuest && (isAdmin || isAssigned);
// toggle filter
const toggleFilter = (filter: TActivityFilters) => {
setSelectedFilters((prevFilters) => {
@@ -37,6 +37,7 @@ export const CreateUpdateIssueModalBase: React.FC<IssuesModalProps> = observer((
moveToIssue = false,
modalTitle,
primaryButtonText,
isProjectSelectionDisabled = false,
} = props;
const issueStoreType = useIssueStoreType();
@@ -361,6 +362,7 @@ export const CreateUpdateIssueModalBase: React.FC<IssuesModalProps> = observer((
moveToIssue={moveToIssue}
isDuplicateModalOpen={isDuplicateModalOpen}
handleDuplicateIssueModal={handleDuplicateIssueModal}
isProjectSelectionDisabled={isProjectSelectionDisabled}
/>
) : (
<IssueFormRoot
@@ -383,6 +385,7 @@ export const CreateUpdateIssueModalBase: React.FC<IssuesModalProps> = observer((
primaryButtonText={primaryButtonText}
isDuplicateModalOpen={isDuplicateModalOpen}
handleDuplicateIssueModal={handleDuplicateIssueModal}
isProjectSelectionDisabled={isProjectSelectionDisabled}
/>
)}
</ModalCore>
@@ -262,58 +262,62 @@ export const IssueDefaultProperties: React.FC<TIssueDefaultPropertiesProps> = ob
)}
/>
)}
{parentId ? (
<CustomMenu
customButton={
<button
type="button"
className="flex cursor-pointer items-center justify-between gap-1 rounded border-[0.5px] border-custom-border-300 px-2 py-1.5 text-xs hover:bg-custom-background-80"
>
{selectedParentIssue?.project_id && (
<IssueIdentifier
projectId={selectedParentIssue.project_id}
issueTypeId={selectedParentIssue.type_id}
projectIdentifier={selectedParentIssue?.project__identifier}
issueSequenceId={selectedParentIssue.sequence_id}
textContainerClassName="text-xs"
/>
)}
</button>
}
placement="bottom-start"
tabIndex={getIndex("parent_id")}
>
<>
<CustomMenu.MenuItem className="!p-1" onClick={() => setParentIssueListModalOpen(true)}>
Change parent issue
</CustomMenu.MenuItem>
<Controller
control={control}
name="parent_id"
render={({ field: { onChange } }) => (
<CustomMenu.MenuItem
className="!p-1"
onClick={() => {
onChange(null);
handleFormChange();
}}
>
Remove parent issue
</CustomMenu.MenuItem>
)}
/>
</>
</CustomMenu>
) : (
<button
type="button"
className="flex cursor-pointer items-center justify-between gap-1 rounded border-[0.5px] border-custom-border-300 px-2 py-1.5 text-xs hover:bg-custom-background-80"
onClick={() => setParentIssueListModalOpen(true)}
>
<LayoutPanelTop className="h-3 w-3 flex-shrink-0" />
<span className="whitespace-nowrap">Add parent</span>
</button>
)}
<div className="h-7">
{parentId ? (
<CustomMenu
customButton={
<button
type="button"
className="flex cursor-pointer items-center justify-between gap-1 h-full rounded border-[0.5px] border-custom-border-300 px-2 py-0.5 text-xs hover:bg-custom-background-80"
>
{selectedParentIssue?.project_id && (
<IssueIdentifier
projectId={selectedParentIssue.project_id}
issueTypeId={selectedParentIssue.type_id}
projectIdentifier={selectedParentIssue?.project__identifier}
issueSequenceId={selectedParentIssue.sequence_id}
textContainerClassName="text-xs"
/>
)}
</button>
}
placement="bottom-start"
className="h-full w-full"
customButtonClassName="h-full"
tabIndex={getIndex("parent_id")}
>
<>
<CustomMenu.MenuItem className="!p-1" onClick={() => setParentIssueListModalOpen(true)}>
Change parent issue
</CustomMenu.MenuItem>
<Controller
control={control}
name="parent_id"
render={({ field: { onChange } }) => (
<CustomMenu.MenuItem
className="!p-1"
onClick={() => {
onChange(null);
handleFormChange();
}}
>
Remove parent issue
</CustomMenu.MenuItem>
)}
/>
</>
</CustomMenu>
) : (
<button
type="button"
className="flex cursor-pointer items-center justify-between gap-1 h-full rounded border-[0.5px] border-custom-border-300 px-2 py-0.5 text-xs hover:bg-custom-background-80"
onClick={() => setParentIssueListModalOpen(true)}
>
<LayoutPanelTop className="h-3 w-3 flex-shrink-0" />
<span className="whitespace-nowrap">Add parent</span>
</button>
)}
</div>
<Controller
control={control}
name="parent_id"
@@ -38,6 +38,7 @@ export interface DraftIssueProps {
};
isDuplicateModalOpen: boolean;
handleDuplicateIssueModal: (isOpen: boolean) => void;
isProjectSelectionDisabled?: boolean;
}
export const DraftIssueLayout: React.FC<DraftIssueProps> = observer((props) => {
@@ -58,6 +59,7 @@ export const DraftIssueLayout: React.FC<DraftIssueProps> = observer((props) => {
primaryButtonText,
isDuplicateModalOpen,
handleDuplicateIssueModal,
isProjectSelectionDisabled = false,
} = props;
// states
const [issueDiscardModal, setIssueDiscardModal] = useState(false);
@@ -179,6 +181,7 @@ export const DraftIssueLayout: React.FC<DraftIssueProps> = observer((props) => {
primaryButtonText={primaryButtonText}
isDuplicateModalOpen={isDuplicateModalOpen}
handleDuplicateIssueModal={handleDuplicateIssueModal}
isProjectSelectionDisabled={isProjectSelectionDisabled}
/>
</>
);
@@ -71,6 +71,7 @@ export interface IssueFormProps {
};
isDuplicateModalOpen: boolean;
handleDuplicateIssueModal: (isOpen: boolean) => void;
isProjectSelectionDisabled?: boolean;
}
export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
@@ -93,6 +94,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
},
isDuplicateModalOpen,
handleDuplicateIssueModal,
isProjectSelectionDisabled = false,
} = props;
// states
@@ -336,7 +338,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
<div className="flex items-center gap-x-1">
<IssueProjectSelect
control={control}
disabled={!!data?.id || !!data?.sourceIssueId}
disabled={!!data?.id || !!data?.sourceIssueId || isProjectSelectionDisabled}
handleFormChange={handleFormChange}
/>
{projectId && (
@@ -511,7 +513,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
{shouldRenderDuplicateModal && (
<div
ref={modalContainerRef}
className="relative flex flex-col gap-2.5 h-full px-3 py-4 rounded-lg shadow-xl bg-pi-50"
className="relative flex flex-col gap-2.5 px-3 py-4 rounded-lg shadow-xl bg-pi-50"
style={{ maxHeight: formRef?.current?.offsetHeight ? `${formRef.current.offsetHeight}px` : "436px" }}
>
<DuplicateModalRoot
@@ -27,6 +27,7 @@ export interface IssuesModalProps {
default: string;
loading: string;
};
isProjectSelectionDisabled?: boolean;
}
export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer(
+3 -3
View File
@@ -25,9 +25,9 @@ export const PageEditorTitle: React.FC<Props> = observer((props) => {
// page filters
const { fontSize } = usePageFilters();
// ui
const titleClassName = cn("bg-transparent tracking-[-2%] font-semibold", {
"text-[1.6rem] leading-[1.8rem]": fontSize === "small-font",
"text-[2rem] leading-[2.25rem]": fontSize === "large-font",
const titleClassName = cn("bg-transparent tracking-[-2%] font-bold", {
"text-[1.6rem] leading-[1.9rem]": fontSize === "small-font",
"text-[2rem] leading-[2.375rem]": fontSize === "large-font",
});
return (
+39
View File
@@ -120,6 +120,19 @@
--color-sidebar-shadow-2xl: var(--color-shadow-2xl);
--color-sidebar-shadow-3xl: var(--color-shadow-3xl);
--color-sidebar-shadow-4xl: var(--color-shadow-4xl);
/* pi */
--color-pi-50: var(--color-background-90);
--color-pi-100: var(--color-background-90);
--color-pi-200: var(--color-primary-200);
--color-pi-300: var(--color-primary-200);
--color-pi-400: var(--color-primary-200);
--color-pi-500: var(--color-primary-200);
--color-pi-600: 151, 150, 246;
--color-pi-700: var(--color-primary-100);
--color-pi-800: 57, 56, 149;
--color-pi-900: 30, 29, 78;
--color-pi-950: 14, 14, 37;
}
[data-theme="light"],
@@ -129,6 +142,19 @@
--color-background-100: 255, 255, 255; /* primary bg */
--color-background-90: 247, 247, 247; /* secondary bg */
--color-background-80: 232, 232, 232; /* tertiary bg */
/* pi */
--color-pi-50: var(--color-background-90);
--color-pi-100: var(--color-background-90);
--color-pi-200: var(--color-primary-200);
--color-pi-300: var(--color-primary-200);
--color-pi-400: var(--color-primary-200);
--color-pi-500: var(--color-primary-200);
--color-pi-600: 151, 150, 246;
--color-pi-700: var(--color-primary-100);
--color-pi-800: 57, 56, 149;
--color-pi-900: 30, 29, 78;
--color-pi-950: 14, 14, 37;
}
[data-theme="light"] {
@@ -230,6 +256,19 @@
--color-shadow-xl: 0px 0px 14px 0px rgba(0, 0, 0, 0.25), 0px 6px 10px 0px rgba(0, 0, 0, 0.55);
--color-shadow-2xl: 0px 0px 18px 0px rgba(0, 0, 0, 0.25), 0px 8px 12px 0px rgba(0, 0, 0, 0.6);
--color-shadow-3xl: 0px 4px 24px 0px rgba(0, 0, 0, 0.3), 0px 12px 40px 0px rgba(0, 0, 0, 0.65);
/* pi */
--color-pi-50: var(--color-background-90);
--color-pi-100: var(--color-background-90);
--color-pi-200: var(--color-primary-200);
--color-pi-300: var(--color-primary-200);
--color-pi-400: var(--color-primary-200);
--color-pi-500: var(--color-primary-200);
--color-pi-600: 151, 150, 246;
--color-pi-700: var(--color-primary-100);
--color-pi-800: 57, 56, 149;
--color-pi-900: 30, 29, 78;
--color-pi-950: 14, 14, 37;
}
[data-theme="dark"] {