release: v0.25.1

* fix: issue activity for project id validation (#6668)

* fix: work item attachment count mutation (#6670)

* updated the action to modify the release build assets (#6669)

* feat: russian translation (#6666)

* chore: ru translation updated (#6672)

* fix: state drop down refactor

* fix: intake work item creation refactor

* fix: cleanup for deprecated functions

* fix: date range picker on cycles and modules list (#6676)

* fix: Handled workspace switcher closing on click

* fix: replaced date range picker with date picker at some places

* chore: add common translation keys (#6688)

* chore: add missing translation keys

* chore: add russian translation keys

* fix: issue activity task (#6689)

* changed github workflow action ubuntu version to `ubuntu-22.04` (#6683)

* chore: update russian translation (#6682)

* chore: update russian translation

* chore: rename issues to work items in russian translation

* [PE-275] chore: editor line spacing variables (#6678)

* chore: variable editor line spacing

* chore: variable list spacing

---------

Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>

* [WEB-3475] fix: cycle dates dropdown (#6690)

* fix: Handled workspace switcher closing on click

* fix: Cycle date picker

* fix: Made onSelect optional in range range component

* fix: module date picker (#6691)

* fix: Handled workspace switcher closing on click

* fix: reverted module date picker changes

* chore: extended sidebar improvement (#6693)

* feat: italian translations (#6692)

* Create translations.json - ITALIAN translation (#6667)

* chore: italian translation updated

* feat: italian translation added

* fix: module end date translation

---------

Co-authored-by: Nicolas Bossi <nicolasbossi@gmail.com>
Co-authored-by: gakshita <akshitagoyal1516@gmail.com>

* fix: attachment item created by (#6695)

* fix: module flicker issue on property updation (#6699)

* [WEB-3477] fix: mutation issue on moving work items for a manually ended cycle (#6696)

* fix: package version update

* fix: esbuild version fix

* fix: package license repliation

* [WEB-3488] improvement: assignee validation for work item creation (#6701)

* fix: work item assignee update validation (#6704)

---------

Co-authored-by: Nikhil <118773738+pablohashescobar@users.noreply.github.com>
Co-authored-by: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com>
Co-authored-by: Manish Gupta <59428681+mguptahub@users.noreply.github.com>
Co-authored-by: Nikita Mitasov <32384814+ch4og@users.noreply.github.com>
Co-authored-by: Akshita Goyal <36129505+gakshita@users.noreply.github.com>
Co-authored-by: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com>
Co-authored-by: Akshat Jain <akshatjain9782@gmail.com>
Co-authored-by: Lakhan Baheti <94619783+1akhanBaheti@users.noreply.github.com>
Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
Co-authored-by: Nicolas Bossi <nicolasbossi@gmail.com>
Co-authored-by: gakshita <akshitagoyal1516@gmail.com>
Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com>
This commit is contained in:
sriram veeraghanta 2025-03-05 19:15:33 +05:30 committed by GitHub
commit e61ff879c4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
71 changed files with 5094 additions and 1263 deletions

View file

@ -88,7 +88,7 @@ jobs:
full_build_push: full_build_push:
if: ${{ needs.branch_build_setup.outputs.do_full_build == 'true' }} if: ${{ needs.branch_build_setup.outputs.do_full_build == 'true' }}
runs-on: ubuntu-20.04 runs-on: ubuntu-22.04
needs: [branch_build_setup] needs: [branch_build_setup]
env: env:
BUILD_TYPE: full BUILD_TYPE: full
@ -148,7 +148,7 @@ jobs:
slim_build_push: slim_build_push:
if: ${{ needs.branch_build_setup.outputs.do_slim_build == 'true' }} if: ${{ needs.branch_build_setup.outputs.do_slim_build == 'true' }}
runs-on: ubuntu-20.04 runs-on: ubuntu-22.04
needs: [branch_build_setup] needs: [branch_build_setup]
env: env:
BUILD_TYPE: slim BUILD_TYPE: slim

View file

@ -299,32 +299,6 @@ jobs:
buildx-platforms: ${{ needs.branch_build_setup.outputs.gh_buildx_platforms }} buildx-platforms: ${{ needs.branch_build_setup.outputs.gh_buildx_platforms }}
buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }} buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
attach_assets_to_build:
if: ${{ needs.branch_build_setup.outputs.build_type == 'Release' }}
name: Attach Assets to Release
runs-on: ubuntu-22.04
needs: [branch_build_setup]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Update Assets
run: |
cp ./deploy/selfhost/install.sh deploy/selfhost/setup.sh
- name: Attach Assets
id: attach_assets
uses: actions/upload-artifact@v4
with:
name: selfhost-assets
retention-days: 2
path: |
${{ github.workspace }}/deploy/selfhost/setup.sh
${{ github.workspace }}/deploy/selfhost/swarm.sh
${{ github.workspace }}/deploy/selfhost/restore.sh
${{ github.workspace }}/deploy/selfhost/docker-compose.yml
${{ github.workspace }}/deploy/selfhost/variables.env
publish_release: publish_release:
if: ${{ needs.branch_build_setup.outputs.build_type == 'Release' }} if: ${{ needs.branch_build_setup.outputs.build_type == 'Release' }}
name: Build Release name: Build Release
@ -338,7 +312,6 @@ jobs:
branch_build_push_live, branch_build_push_live,
branch_build_push_apiserver, branch_build_push_apiserver,
branch_build_push_proxy, branch_build_push_proxy,
attach_assets_to_build,
] ]
env: env:
REL_VERSION: ${{ needs.branch_build_setup.outputs.release_version }} REL_VERSION: ${{ needs.branch_build_setup.outputs.release_version }}
@ -349,6 +322,8 @@ jobs:
- name: Update Assets - name: Update Assets
run: | run: |
cp ./deploy/selfhost/install.sh deploy/selfhost/setup.sh cp ./deploy/selfhost/install.sh deploy/selfhost/setup.sh
sed -i 's/${APP_RELEASE:-stable}/${APP_RELEASE:-'${REL_VERSION}'}/g' deploy/selfhost/docker-compose.yml
sed -i 's/APP_RELEASE=stable/APP_RELEASE='${REL_VERSION}'/g' deploy/selfhost/variables.env
- name: Create Release - name: Create Release
id: create_release id: create_release

View file

@ -51,7 +51,7 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
full_build_push: full_build_push:
runs-on: ubuntu-20.04 runs-on: ubuntu-22.04
needs: [branch_build_setup] needs: [branch_build_setup]
env: env:
BUILD_TYPE: full BUILD_TYPE: full

View file

@ -11,7 +11,7 @@ env:
jobs: jobs:
sync_changes: sync_changes:
runs-on: ubuntu-20.04 runs-on: ubuntu-22.04
permissions: permissions:
pull-requests: write pull-requests: write
contents: read contents: read

View file

@ -1,6 +1,8 @@
{ {
"name": "admin", "name": "admin",
"version": "0.25.0", "description": "Admin UI for Plane",
"version": "0.25.1",
"license": "AGPL-3.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "turbo run develop", "dev": "turbo run develop",

View file

@ -1,4 +1,7 @@
{ {
"name": "plane-api", "name": "plane-api",
"version": "0.25.0" "version": "0.25.1",
"license": "AGPL-3.0",
"private": true,
"description": "API server powering Plane's backend"
} }

View file

@ -80,6 +80,7 @@ class IssueSerializer(BaseSerializer):
data["assignees"] = ProjectMember.objects.filter( data["assignees"] = ProjectMember.objects.filter(
project_id=self.context.get("project_id"), project_id=self.context.get("project_id"),
is_active=True, is_active=True,
role__gte=15,
member_id__in=data["assignees"], member_id__in=data["assignees"],
).values_list("member_id", flat=True) ).values_list("member_id", flat=True)
@ -158,8 +159,13 @@ class IssueSerializer(BaseSerializer):
pass pass
else: else:
try: try:
# Then assign it to default assignee # Then assign it to default assignee, if it is a valid assignee
if default_assignee_id is not None: if default_assignee_id is not None and ProjectMember.objects.filter(
member_id=default_assignee_id,
project_id=project_id,
role__gte=15,
is_active=True
).exists():
IssueAssignee.objects.create( IssueAssignee.objects.create(
assignee_id=default_assignee_id, assignee_id=default_assignee_id,
issue=issue, issue=issue,

View file

@ -121,8 +121,6 @@ from .exporter import ExporterHistorySerializer
from .webhook import WebhookSerializer, WebhookLogSerializer from .webhook import WebhookSerializer, WebhookLogSerializer
from .dashboard import DashboardSerializer, WidgetSerializer
from .favorite import UserFavoriteSerializer from .favorite import UserFavoriteSerializer
from .draft import ( from .draft import (

View file

@ -1,21 +0,0 @@
# Module imports
from .base import BaseSerializer
from plane.db.models import DeprecatedDashboard, DeprecatedWidget
# Third party frameworks
from rest_framework import serializers
class DashboardSerializer(BaseSerializer):
class Meta:
model = DeprecatedDashboard
fields = "__all__"
class WidgetSerializer(BaseSerializer):
is_visible = serializers.BooleanField(read_only=True)
widget_filters = serializers.JSONField(read_only=True)
class Meta:
model = DeprecatedWidget
fields = ["id", "key", "is_visible", "widget_filters"]

View file

@ -36,6 +36,7 @@ from plane.db.models import (
State, State,
IssueVersion, IssueVersion,
IssueDescriptionVersion, IssueDescriptionVersion,
ProjectMember,
) )
@ -119,6 +120,17 @@ class IssueCreateSerializer(BaseSerializer):
raise serializers.ValidationError("Start date cannot exceed target date") raise serializers.ValidationError("Start date cannot exceed target date")
return data return data
def get_valid_assignees(self, assignees, project_id):
if not assignees:
return []
return ProjectMember.objects.filter(
project_id=project_id,
role__gte=15,
is_active=True,
member_id__in=assignees
).values_list('member_id', flat=True)
def create(self, validated_data): def create(self, validated_data):
assignees = validated_data.pop("assignee_ids", None) assignees = validated_data.pop("assignee_ids", None)
labels = validated_data.pop("label_ids", None) labels = validated_data.pop("label_ids", None)
@ -134,27 +146,33 @@ class IssueCreateSerializer(BaseSerializer):
created_by_id = issue.created_by_id created_by_id = issue.created_by_id
updated_by_id = issue.updated_by_id updated_by_id = issue.updated_by_id
if assignees is not None and len(assignees): valid_assignee_ids = self.get_valid_assignees(assignees, project_id)
if valid_assignee_ids is not None and len(valid_assignee_ids):
try: try:
IssueAssignee.objects.bulk_create( IssueAssignee.objects.bulk_create(
[ [
IssueAssignee( IssueAssignee(
assignee=user, assignee_id=user_id,
issue=issue, issue=issue,
project_id=project_id, project_id=project_id,
workspace_id=workspace_id, workspace_id=workspace_id,
created_by_id=created_by_id, created_by_id=created_by_id,
updated_by_id=updated_by_id, updated_by_id=updated_by_id,
) )
for user in assignees for user_id in valid_assignee_ids
], ],
batch_size=10, batch_size=10,
) )
except IntegrityError: except IntegrityError:
pass pass
else: else:
# Then assign it to default assignee # Then assign it to default assignee, if it is a valid assignee
if default_assignee_id is not None: if default_assignee_id is not None and ProjectMember.objects.filter(
member_id=default_assignee_id,
project_id=project_id,
role__gte=15,
is_active=True
).exists():
try: try:
IssueAssignee.objects.create( IssueAssignee.objects.create(
assignee_id=default_assignee_id, assignee_id=default_assignee_id,
@ -198,20 +216,21 @@ class IssueCreateSerializer(BaseSerializer):
created_by_id = instance.created_by_id created_by_id = instance.created_by_id
updated_by_id = instance.updated_by_id updated_by_id = instance.updated_by_id
if assignees is not None: valid_assignee_ids = self.get_valid_assignees(assignees, project_id)
if valid_assignee_ids is not None:
IssueAssignee.objects.filter(issue=instance).delete() IssueAssignee.objects.filter(issue=instance).delete()
try: try:
IssueAssignee.objects.bulk_create( IssueAssignee.objects.bulk_create(
[ [
IssueAssignee( IssueAssignee(
assignee=user, assignee_id=user_id,
issue=instance, issue=instance,
project_id=project_id, project_id=project_id,
workspace_id=workspace_id, workspace_id=workspace_id,
created_by_id=created_by_id, created_by_id=created_by_id,
updated_by_id=updated_by_id, updated_by_id=updated_by_id,
) )
for user in assignees for user_id in valid_assignee_ids
], ],
batch_size=10, batch_size=10,
ignore_conflicts=True, ignore_conflicts=True,

View file

@ -2,7 +2,6 @@ from .analytic import urlpatterns as analytic_urls
from .api import urlpatterns as api_urls from .api import urlpatterns as api_urls
from .asset import urlpatterns as asset_urls from .asset import urlpatterns as asset_urls
from .cycle import urlpatterns as cycle_urls from .cycle import urlpatterns as cycle_urls
from .dashboard import urlpatterns as dashboard_urls
from .estimate import urlpatterns as estimate_urls from .estimate import urlpatterns as estimate_urls
from .external import urlpatterns as external_urls from .external import urlpatterns as external_urls
from .intake import urlpatterns as intake_urls from .intake import urlpatterns as intake_urls
@ -23,7 +22,6 @@ urlpatterns = [
*analytic_urls, *analytic_urls,
*asset_urls, *asset_urls,
*cycle_urls, *cycle_urls,
*dashboard_urls,
*estimate_urls, *estimate_urls,
*external_urls, *external_urls,
*intake_urls, *intake_urls,

View file

@ -1,23 +0,0 @@
from django.urls import path
from plane.app.views import DashboardEndpoint, WidgetsEndpoint
urlpatterns = [
path(
"workspaces/<str:slug>/dashboard/",
DashboardEndpoint.as_view(),
name="dashboard",
),
path(
"workspaces/<str:slug>/dashboard/<uuid:dashboard_id>/",
DashboardEndpoint.as_view(),
name="dashboard",
),
path(
"dashboard/<uuid:dashboard_id>/widgets/<uuid:widget_id>/",
WidgetsEndpoint.as_view(),
name="widgets",
),
]

View file

@ -210,8 +210,6 @@ from .webhook.base import (
WebhookSecretRegenerateEndpoint, WebhookSecretRegenerateEndpoint,
) )
from .dashboard.base import DashboardEndpoint, WidgetsEndpoint
from .error_404 import custom_404_view from .error_404 import custom_404_view
from .notification.base import MarkAllReadNotificationViewSet from .notification.base import MarkAllReadNotificationViewSet

View file

@ -1,812 +0,0 @@
# Django imports
from django.contrib.postgres.aggregates import ArrayAgg
from django.contrib.postgres.fields import ArrayField
from django.db.models import (
Case,
CharField,
Count,
Exists,
F,
Func,
IntegerField,
JSONField,
OuterRef,
Prefetch,
Q,
Subquery,
UUIDField,
Value,
When,
)
from django.db.models.functions import Coalesce
from django.utils import timezone
from rest_framework import status
# Third Party imports
from rest_framework.response import Response
from plane.app.serializers import (
DashboardSerializer,
IssueActivitySerializer,
IssueSerializer,
WidgetSerializer,
)
from plane.db.models import (
DeprecatedDashboard,
DeprecatedDashboardWidget,
Issue,
IssueActivity,
FileAsset,
IssueLink,
IssueRelation,
Project,
DeprecatedWidget,
WorkspaceMember,
CycleIssue,
)
from plane.utils.issue_filters import issue_filters
# Module imports
from .. import BaseAPIView
def dashboard_overview_stats(self, request, slug):
assigned_issues = (
Issue.issue_objects.filter(
(Q(assignees__in=[request.user]) & Q(issue_assignee__deleted_at__isnull=True)),
project__project_projectmember__is_active=True,
project__project_projectmember__member=request.user,
workspace__slug=slug,
)
.filter(
Q(
project__project_projectmember__role=5,
project__guest_view_all_features=True,
)
| Q(
project__project_projectmember__role=5,
project__guest_view_all_features=False,
created_by=self.request.user,
)
|
# For other roles (role < 5), show all issues
Q(project__project_projectmember__role__gt=5),
project__project_projectmember__member=self.request.user,
project__project_projectmember__is_active=True,
)
.count()
)
pending_issues_count = (
Issue.issue_objects.filter(
~Q(state__group__in=["completed", "cancelled"]),
target_date__lt=timezone.now().date(),
project__project_projectmember__is_active=True,
project__project_projectmember__member=request.user,
workspace__slug=slug,
assignees__in=[request.user],
)
.filter(
Q(
project__project_projectmember__role=5,
project__guest_view_all_features=True,
)
| Q(
project__project_projectmember__role=5,
project__guest_view_all_features=False,
created_by=self.request.user,
)
|
# For other roles (role < 5), show all issues
Q(project__project_projectmember__role__gt=5),
project__project_projectmember__member=self.request.user,
project__project_projectmember__is_active=True,
)
.count()
)
created_issues_count = (
Issue.issue_objects.filter(
workspace__slug=slug,
project__project_projectmember__is_active=True,
project__project_projectmember__member=request.user,
created_by_id=request.user.id,
)
.filter(
Q(
project__project_projectmember__role=5,
project__guest_view_all_features=True,
)
| Q(
project__project_projectmember__role=5,
project__guest_view_all_features=False,
created_by=self.request.user,
)
|
# For other roles (role < 5), show all issues
Q(project__project_projectmember__role__gt=5),
project__project_projectmember__member=self.request.user,
project__project_projectmember__is_active=True,
)
.count()
)
completed_issues_count = (
Issue.issue_objects.filter(
(
Q(assignees__in=[request.user])
& Q(issue_assignee__deleted_at__isnull=True)
),
workspace__slug=slug,
project__project_projectmember__is_active=True,
project__project_projectmember__member=request.user,
state__group="completed",
)
.filter(
Q(
project__project_projectmember__role=5,
project__guest_view_all_features=True,
)
| Q(
project__project_projectmember__role=5,
project__guest_view_all_features=False,
created_by=self.request.user,
)
|
# For other roles (role < 5), show all issues
Q(project__project_projectmember__role__gt=5),
project__project_projectmember__member=self.request.user,
project__project_projectmember__is_active=True,
)
.count()
)
return Response(
{
"assigned_issues_count": assigned_issues,
"pending_issues_count": pending_issues_count,
"completed_issues_count": completed_issues_count,
"created_issues_count": created_issues_count,
},
status=status.HTTP_200_OK,
)
def dashboard_assigned_issues(self, request, slug):
filters = issue_filters(request.query_params, "GET")
issue_type = request.GET.get("issue_type", None)
# get all the assigned issues
assigned_issues = (
Issue.issue_objects.filter(
(
Q(assignees__in=[request.user])
& Q(issue_assignee__deleted_at__isnull=True)
),
workspace__slug=slug,
project__project_projectmember__member=request.user,
project__project_projectmember__is_active=True,
)
.filter(**filters)
.select_related("workspace", "project", "state", "parent")
.prefetch_related("assignees", "labels", "issue_module__module")
.prefetch_related(
Prefetch(
"issue_relation",
queryset=IssueRelation.objects.select_related(
"related_issue"
).select_related("issue"),
)
)
.annotate(
cycle_id=Subquery(
CycleIssue.objects.filter(
issue=OuterRef("id"), deleted_at__isnull=True
).values("cycle_id")[:1]
)
)
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
attachment_count=FileAsset.objects.filter(
issue_id=OuterRef("id"),
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
label_ids=Coalesce(
ArrayAgg(
"labels__id",
distinct=True,
filter=Q(
~Q(labels__id__isnull=True)
& Q(label_issue__deleted_at__isnull=True)
),
),
Value([], output_field=ArrayField(UUIDField())),
),
assignee_ids=Coalesce(
ArrayAgg(
"assignees__id",
distinct=True,
filter=Q(
~Q(assignees__id__isnull=True)
& Q(assignees__member_project__is_active=True)
& Q(issue_assignee__deleted_at__isnull=True)
),
),
Value([], output_field=ArrayField(UUIDField())),
),
module_ids=Coalesce(
ArrayAgg(
"issue_module__module_id",
distinct=True,
filter=Q(
~Q(issue_module__module_id__isnull=True)
& Q(issue_module__module__archived_at__isnull=True)
& Q(issue_module__deleted_at__isnull=True)
),
),
Value([], output_field=ArrayField(UUIDField())),
),
)
)
if WorkspaceMember.objects.filter(
workspace__slug=slug, member=request.user, role=5, is_active=True
).exists():
assigned_issues = assigned_issues.filter(created_by=request.user)
# Priority Ordering
priority_order = ["urgent", "high", "medium", "low", "none"]
assigned_issues = assigned_issues.annotate(
priority_order=Case(
*[When(priority=p, then=Value(i)) for i, p in enumerate(priority_order)],
output_field=CharField(),
)
).order_by("priority_order")
if issue_type == "pending":
pending_issues_count = assigned_issues.filter(
state__group__in=["backlog", "started", "unstarted"]
).count()
pending_issues = assigned_issues.filter(
state__group__in=["backlog", "started", "unstarted"]
)[:5]
return Response(
{
"issues": IssueSerializer(
pending_issues, many=True, expand=self.expand
).data,
"count": pending_issues_count,
},
status=status.HTTP_200_OK,
)
if issue_type == "completed":
completed_issues_count = assigned_issues.filter(
state__group__in=["completed"]
).count()
completed_issues = assigned_issues.filter(state__group__in=["completed"])[:5]
return Response(
{
"issues": IssueSerializer(
completed_issues, many=True, expand=self.expand
).data,
"count": completed_issues_count,
},
status=status.HTTP_200_OK,
)
if issue_type == "overdue":
overdue_issues_count = assigned_issues.filter(
state__group__in=["backlog", "unstarted", "started"],
target_date__lt=timezone.now(),
).count()
overdue_issues = assigned_issues.filter(
state__group__in=["backlog", "unstarted", "started"],
target_date__lt=timezone.now(),
)[:5]
return Response(
{
"issues": IssueSerializer(
overdue_issues, many=True, expand=self.expand
).data,
"count": overdue_issues_count,
},
status=status.HTTP_200_OK,
)
if issue_type == "upcoming":
upcoming_issues_count = assigned_issues.filter(
state__group__in=["backlog", "unstarted", "started"],
target_date__gte=timezone.now(),
).count()
upcoming_issues = assigned_issues.filter(
state__group__in=["backlog", "unstarted", "started"],
target_date__gte=timezone.now(),
)[:5]
return Response(
{
"issues": IssueSerializer(
upcoming_issues, many=True, expand=self.expand
).data,
"count": upcoming_issues_count,
},
status=status.HTTP_200_OK,
)
return Response(
{"error": "Please specify a valid issue type"},
status=status.HTTP_400_BAD_REQUEST,
)
def dashboard_created_issues(self, request, slug):
filters = issue_filters(request.query_params, "GET")
issue_type = request.GET.get("issue_type", None)
# get all the assigned issues
created_issues = (
Issue.issue_objects.filter(
workspace__slug=slug,
project__project_projectmember__member=request.user,
project__project_projectmember__is_active=True,
created_by=request.user,
)
.filter(**filters)
.select_related("workspace", "project", "state", "parent")
.prefetch_related("assignees", "labels", "issue_module__module")
.annotate(
cycle_id=Subquery(
CycleIssue.objects.filter(
issue=OuterRef("id"), deleted_at__isnull=True
).values("cycle_id")[:1]
)
)
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
attachment_count=FileAsset.objects.filter(
issue_id=OuterRef("id"),
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
label_ids=Coalesce(
ArrayAgg(
"labels__id",
distinct=True,
filter=Q(
~Q(labels__id__isnull=True)
& Q(label_issue__deleted_at__isnull=True)
),
),
Value([], output_field=ArrayField(UUIDField())),
),
assignee_ids=Coalesce(
ArrayAgg(
"assignees__id",
distinct=True,
filter=Q(
~Q(assignees__id__isnull=True)
& Q(assignees__member_project__is_active=True)
& Q(issue_assignee__deleted_at__isnull=True)
),
),
Value([], output_field=ArrayField(UUIDField())),
),
module_ids=Coalesce(
ArrayAgg(
"issue_module__module_id",
distinct=True,
filter=Q(
~Q(issue_module__module_id__isnull=True)
& Q(issue_module__module__archived_at__isnull=True)
& Q(issue_module__deleted_at__isnull=True)
),
),
Value([], output_field=ArrayField(UUIDField())),
),
)
.order_by("created_at")
)
# Priority Ordering
priority_order = ["urgent", "high", "medium", "low", "none"]
created_issues = created_issues.annotate(
priority_order=Case(
*[When(priority=p, then=Value(i)) for i, p in enumerate(priority_order)],
output_field=CharField(),
)
).order_by("priority_order")
if issue_type == "pending":
pending_issues_count = created_issues.filter(
state__group__in=["backlog", "started", "unstarted"]
).count()
pending_issues = created_issues.filter(
state__group__in=["backlog", "started", "unstarted"]
)[:5]
return Response(
{
"issues": IssueSerializer(
pending_issues, many=True, expand=self.expand
).data,
"count": pending_issues_count,
},
status=status.HTTP_200_OK,
)
if issue_type == "completed":
completed_issues_count = created_issues.filter(
state__group__in=["completed"]
).count()
completed_issues = created_issues.filter(state__group__in=["completed"])[:5]
return Response(
{
"issues": IssueSerializer(completed_issues, many=True).data,
"count": completed_issues_count,
},
status=status.HTTP_200_OK,
)
if issue_type == "overdue":
overdue_issues_count = created_issues.filter(
state__group__in=["backlog", "unstarted", "started"],
target_date__lt=timezone.now(),
).count()
overdue_issues = created_issues.filter(
state__group__in=["backlog", "unstarted", "started"],
target_date__lt=timezone.now(),
)[:5]
return Response(
{
"issues": IssueSerializer(overdue_issues, many=True).data,
"count": overdue_issues_count,
},
status=status.HTTP_200_OK,
)
if issue_type == "upcoming":
upcoming_issues_count = created_issues.filter(
state__group__in=["backlog", "unstarted", "started"],
target_date__gte=timezone.now(),
).count()
upcoming_issues = created_issues.filter(
state__group__in=["backlog", "unstarted", "started"],
target_date__gte=timezone.now(),
)[:5]
return Response(
{
"issues": IssueSerializer(upcoming_issues, many=True).data,
"count": upcoming_issues_count,
},
status=status.HTTP_200_OK,
)
return Response(
{"error": "Please specify a valid issue type"},
status=status.HTTP_400_BAD_REQUEST,
)
def dashboard_issues_by_state_groups(self, request, slug):
filters = issue_filters(request.query_params, "GET")
state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
extra_filters = {}
if WorkspaceMember.objects.filter(
workspace__slug=slug, member=request.user, role=5, is_active=True
).exists():
extra_filters = {"created_by": request.user}
issues_by_state_groups = (
Issue.issue_objects.filter(
workspace__slug=slug,
project__project_projectmember__is_active=True,
project__project_projectmember__member=request.user,
assignees__in=[request.user],
)
.filter(**filters, **extra_filters)
.values("state__group")
.annotate(count=Count("id"))
)
# default state
all_groups = {state: 0 for state in state_order}
# Update counts for existing groups
for entry in issues_by_state_groups:
all_groups[entry["state__group"]] = entry["count"]
# Prepare output including all groups with their counts
output_data = [
{"state": group, "count": count} for group, count in all_groups.items()
]
return Response(output_data, status=status.HTTP_200_OK)
def dashboard_issues_by_priority(self, request, slug):
filters = issue_filters(request.query_params, "GET")
priority_order = ["urgent", "high", "medium", "low", "none"]
extra_filters = {}
if WorkspaceMember.objects.filter(
workspace__slug=slug, member=request.user, role=5, is_active=True
).exists():
extra_filters = {"created_by": request.user}
issues_by_priority = (
Issue.issue_objects.filter(
workspace__slug=slug,
project__project_projectmember__is_active=True,
project__project_projectmember__member=request.user,
assignees__in=[request.user],
)
.filter(**filters, **extra_filters)
.values("priority")
.annotate(count=Count("id"))
)
# default priority
all_groups = {priority: 0 for priority in priority_order}
# Update counts for existing groups
for entry in issues_by_priority:
all_groups[entry["priority"]] = entry["count"]
# Prepare output including all groups with their counts
output_data = [
{"priority": group, "count": count} for group, count in all_groups.items()
]
return Response(output_data, status=status.HTTP_200_OK)
def dashboard_recent_activity(self, request, slug):
queryset = IssueActivity.objects.filter(
~Q(field__in=["comment", "vote", "reaction", "draft"]),
workspace__slug=slug,
project__project_projectmember__member=request.user,
project__project_projectmember__is_active=True,
project__archived_at__isnull=True,
actor=request.user,
).select_related("actor", "workspace", "issue", "project")[:8]
return Response(
IssueActivitySerializer(queryset, many=True).data, status=status.HTTP_200_OK
)
def dashboard_recent_projects(self, request, slug):
project_ids = (
IssueActivity.objects.filter(
workspace__slug=slug,
project__project_projectmember__member=request.user,
project__project_projectmember__is_active=True,
project__archived_at__isnull=True,
actor=request.user,
)
.values_list("project_id", flat=True)
.distinct()
)
# Extract project IDs from the recent projects
unique_project_ids = set(project_id for project_id in project_ids)
# Fetch additional projects only if needed
if len(unique_project_ids) < 4:
additional_projects = Project.objects.filter(
project_projectmember__member=request.user,
project_projectmember__is_active=True,
archived_at__isnull=True,
workspace__slug=slug,
).exclude(id__in=unique_project_ids)
# Append additional project IDs to the existing list
unique_project_ids.update(additional_projects.values_list("id", flat=True))
return Response(list(unique_project_ids)[:4], status=status.HTTP_200_OK)
def dashboard_recent_collaborators(self, request, slug):
project_members_with_activities = (
WorkspaceMember.objects.filter(workspace__slug=slug, is_active=True)
.annotate(
active_issue_count=Count(
Case(
When(
member__issue_assignee__issue__state__group__in=[
"unstarted",
"started",
],
member__issue_assignee__issue__workspace__slug=slug,
member__issue_assignee__issue__project__project_projectmember__member=request.user,
member__issue_assignee__issue__project__project_projectmember__is_active=True,
then=F("member__issue_assignee__issue__id"),
),
distinct=True,
output_field=IntegerField(),
),
distinct=True,
),
user_id=F("member_id"),
)
.values("user_id", "active_issue_count")
.order_by("-active_issue_count")
.distinct()
)
return Response((project_members_with_activities), status=status.HTTP_200_OK)
class DashboardEndpoint(BaseAPIView):
def create(self, request, slug):
serializer = DashboardSerializer(data=request.data)
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)
def patch(self, request, slug, pk):
serializer = DashboardSerializer(data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def delete(self, request, slug, pk):
serializer = DashboardSerializer(data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_204_NO_CONTENT)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def get(self, request, slug, dashboard_id=None):
if not dashboard_id:
dashboard_type = request.GET.get("dashboard_type", None)
if dashboard_type == "home":
dashboard, created = DeprecatedDashboard.objects.get_or_create(
type_identifier=dashboard_type,
owned_by=request.user,
is_default=True,
)
if created:
widgets_to_fetch = [
"overview_stats",
"assigned_issues",
"created_issues",
"issues_by_state_groups",
"issues_by_priority",
"recent_activity",
"recent_projects",
"recent_collaborators",
]
updated_dashboard_widgets = []
for widget_key in widgets_to_fetch:
widget = DeprecatedWidget.objects.filter(
key=widget_key
).values_list("id", flat=True)
if widget:
updated_dashboard_widgets.append(
DeprecatedDashboardWidget(
widget_id=widget, dashboard_id=dashboard.id
)
)
DeprecatedDashboardWidget.objects.bulk_create(
updated_dashboard_widgets, batch_size=100
)
widgets = (
DeprecatedWidget.objects.annotate(
is_visible=Exists(
DeprecatedDashboardWidget.objects.filter(
widget_id=OuterRef("pk"),
dashboard_id=dashboard.id,
is_visible=True,
)
)
)
.annotate(
dashboard_filters=Subquery(
DeprecatedDashboardWidget.objects.filter(
widget_id=OuterRef("pk"),
dashboard_id=dashboard.id,
filters__isnull=False,
)
.exclude(filters={})
.values("filters")[:1]
)
)
.annotate(
widget_filters=Case(
When(
dashboard_filters__isnull=False,
then=F("dashboard_filters"),
),
default=F("filters"),
output_field=JSONField(),
)
)
)
return Response(
{
"dashboard": DashboardSerializer(dashboard).data,
"widgets": WidgetSerializer(widgets, many=True).data,
},
status=status.HTTP_200_OK,
)
return Response(
{"error": "Please specify a valid dashboard type"},
status=status.HTTP_400_BAD_REQUEST,
)
widget_key = request.GET.get("widget_key", "overview_stats")
WIDGETS_MAPPER = {
"overview_stats": dashboard_overview_stats,
"assigned_issues": dashboard_assigned_issues,
"created_issues": dashboard_created_issues,
"issues_by_state_groups": dashboard_issues_by_state_groups,
"issues_by_priority": dashboard_issues_by_priority,
"recent_activity": dashboard_recent_activity,
"recent_projects": dashboard_recent_projects,
"recent_collaborators": dashboard_recent_collaborators,
}
func = WIDGETS_MAPPER.get(widget_key)
if func is not None:
response = func(self, request=request, slug=slug)
if isinstance(response, Response):
return response
return Response(
{"error": "Please specify a valid widget key"},
status=status.HTTP_400_BAD_REQUEST,
)
class WidgetsEndpoint(BaseAPIView):
def patch(self, request, dashboard_id, widget_id):
dashboard_widget = DeprecatedDashboardWidget.objects.filter(
widget_id=widget_id, dashboard_id=dashboard_id
).first()
dashboard_widget.is_visible = request.data.get(
"is_visible", dashboard_widget.is_visible
)
dashboard_widget.sort_order = request.data.get(
"sort_order", dashboard_widget.sort_order
)
dashboard_widget.filters = request.data.get("filters", dashboard_widget.filters)
dashboard_widget.save()
return Response({"message": "successfully updated"}, status=status.HTTP_200_OK)

View file

@ -34,6 +34,7 @@ from plane.bgtasks.webhook_task import webhook_activity
from plane.utils.issue_relation_mapper import get_inverse_relation from plane.utils.issue_relation_mapper import get_inverse_relation
from plane.utils.valid_uuid import is_valid_uuid from plane.utils.valid_uuid import is_valid_uuid
# Track Changes in name # Track Changes in name
def track_name( def track_name(
requested_data, requested_data,
@ -852,7 +853,7 @@ def delete_cycle_issue_activity(
issues = requested_data.get("issues") issues = requested_data.get("issues")
for issue in issues: for issue in issues:
current_issue = Issue.objects.filter(pk=issue).first() current_issue = Issue.objects.filter(pk=issue).first()
if issue: if current_issue:
current_issue.updated_at = timezone.now() current_issue.updated_at = timezone.now()
current_issue.save(update_fields=["updated_at"]) current_issue.save(update_fields=["updated_at"])
issue_activities.append( issue_activities.append(
@ -1569,13 +1570,12 @@ def issue_activity(
issue_activities = [] issue_activities = []
# check if project_id is valid # check if project_id is valid
if not is_valid_uuid(project_id): if not is_valid_uuid(str(project_id)):
return return
project = Project.objects.get(pk=project_id) project = Project.objects.get(pk=project_id)
workspace_id = project.workspace_id workspace_id = project.workspace_id
if issue_id is not None: if issue_id is not None:
if origin: if origin:
ri = redis_instance() ri = redis_instance()

View file

@ -0,0 +1,41 @@
# Generated by Django 4.2.18 on 2025-02-25 15:48
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("db", "0091_issuecomment_edited_at_and_more"),
]
operations = [
migrations.AlterUniqueTogether(
name="deprecateddashboardwidget",
unique_together=None,
),
migrations.RemoveField(
model_name="deprecateddashboardwidget",
name="created_by",
),
migrations.RemoveField(
model_name="deprecateddashboardwidget",
name="dashboard",
),
migrations.RemoveField(
model_name="deprecateddashboardwidget",
name="updated_by",
),
migrations.RemoveField(
model_name="deprecateddashboardwidget",
name="widget",
),
migrations.DeleteModel(
name="DeprecatedDashboard",
),
migrations.DeleteModel(
name="DeprecatedDashboardWidget",
),
migrations.DeleteModel(
name="DeprecatedWidget",
),
]

View file

@ -3,7 +3,6 @@ from .api import APIActivityLog, APIToken
from .asset import FileAsset from .asset import FileAsset
from .base import BaseModel from .base import BaseModel
from .cycle import Cycle, CycleIssue, CycleUserProperties from .cycle import Cycle, CycleIssue, CycleUserProperties
from .dashboard import DeprecatedDashboard, DeprecatedDashboardWidget, DeprecatedWidget
from .deploy_board import DeployBoard from .deploy_board import DeployBoard
from .draft import ( from .draft import (
DraftIssue, DraftIssue,

View file

@ -1,92 +0,0 @@
import uuid
# Django imports
from django.db import models
# Module imports
from ..mixins import TimeAuditModel
from .base import BaseModel
class DeprecatedDashboard(BaseModel):
DASHBOARD_CHOICES = (
("workspace", "Workspace"),
("project", "Project"),
("home", "Home"),
("team", "Team"),
("user", "User"),
)
name = models.CharField(max_length=255)
description_html = models.TextField(blank=True, default="<p></p>")
identifier = models.UUIDField(null=True)
owned_by = models.ForeignKey(
"db.User", on_delete=models.CASCADE, related_name="dashboards"
)
is_default = models.BooleanField(default=False)
type_identifier = models.CharField(
max_length=30,
choices=DASHBOARD_CHOICES,
verbose_name="Dashboard Type",
default="home",
)
logo_props = models.JSONField(default=dict)
def __str__(self):
"""Return name of the dashboard"""
return f"{self.name}"
class Meta:
verbose_name = "DeprecatedDashboard"
verbose_name_plural = "DeprecatedDashboards"
db_table = "deprecated_dashboards"
ordering = ("-created_at",)
class DeprecatedWidget(TimeAuditModel):
id = models.UUIDField(
default=uuid.uuid4, unique=True, editable=False, db_index=True, primary_key=True
)
key = models.CharField(max_length=255)
filters = models.JSONField(default=dict)
logo_props = models.JSONField(default=dict)
def __str__(self):
"""Return name of the widget"""
return f"{self.key}"
class Meta:
verbose_name = "DeprecatedWidget"
verbose_name_plural = "DeprecatedWidgets"
db_table = "deprecated_widgets"
ordering = ("-created_at",)
class DeprecatedDashboardWidget(BaseModel):
widget = models.ForeignKey(
DeprecatedWidget, on_delete=models.CASCADE, related_name="dashboard_widgets"
)
dashboard = models.ForeignKey(
DeprecatedDashboard, on_delete=models.CASCADE, related_name="dashboard_widgets"
)
is_visible = models.BooleanField(default=True)
sort_order = models.FloatField(default=65535)
filters = models.JSONField(default=dict)
properties = models.JSONField(default=dict)
def __str__(self):
"""Return name of the dashboard"""
return f"{self.dashboard.name} {self.widget.key}"
class Meta:
unique_together = ("widget", "dashboard", "deleted_at")
constraints = [
models.UniqueConstraint(
fields=["widget", "dashboard"],
condition=models.Q(deleted_at__isnull=True),
name="dashboard_widget_unique_widget_dashboard_when_deleted_at_null",
)
]
verbose_name = "Deprecated Dashboard Widget"
verbose_name_plural = "Deprecated Dashboard Widgets"
db_table = "deprecated_dashboard_widgets"
ordering = ("-created_at",)

View file

@ -1,7 +1,8 @@
{ {
"name": "live", "name": "live",
"version": "0.25.0", "version": "0.25.1",
"description": "", "license": "AGPL-3.0",
"description": "A realtime collaborative server powers Plane's rich text editor",
"main": "./src/server.ts", "main": "./src/server.ts",
"private": true, "private": true,
"type": "module", "type": "module",
@ -14,7 +15,6 @@
}, },
"keywords": [], "keywords": [],
"author": "", "author": "",
"license": "ISC",
"dependencies": { "dependencies": {
"@hocuspocus/extension-database": "^2.15.0", "@hocuspocus/extension-database": "^2.15.0",
"@hocuspocus/extension-logger": "^2.15.0", "@hocuspocus/extension-logger": "^2.15.0",

View file

@ -1,6 +1,8 @@
{ {
"name": "plane",
"description": "Open-source project management that unlocks customer value",
"repository": "https://github.com/makeplane/plane.git", "repository": "https://github.com/makeplane/plane.git",
"version": "0.25.0", "version": "0.25.1",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"private": true, "private": true,
"workspaces": [ "workspaces": [
@ -28,6 +30,5 @@
"nanoid": "3.3.8", "nanoid": "3.3.8",
"esbuild": "0.25.0" "esbuild": "0.25.0"
}, },
"packageManager": "yarn@1.22.22", "packageManager": "yarn@1.22.22"
"name": "plane"
} }

View file

@ -1,6 +1,7 @@
{ {
"name": "@plane/constants", "name": "@plane/constants",
"version": "0.25.0", "version": "0.25.1",
"private": true, "private": true,
"main": "./src/index.ts" "main": "./src/index.ts",
"license": "AGPL-3.0"
} }

View file

@ -1,7 +1,8 @@
{ {
"name": "@plane/editor", "name": "@plane/editor",
"version": "0.25.0", "version": "0.25.1",
"description": "Core Editor that powers Plane", "description": "Core Editor that powers Plane",
"license": "AGPL-3.0",
"private": true, "private": true,
"main": "./dist/index.mjs", "main": "./dist/index.mjs",
"module": "./dist/index.mjs", "module": "./dist/index.mjs",

View file

@ -1,5 +1,5 @@
import { FC, ReactNode } from "react";
import { Editor } from "@tiptap/react"; import { Editor } from "@tiptap/react";
import { FC, ReactNode } from "react";
// plane utils // plane utils
import { cn } from "@plane/utils"; import { cn } from "@plane/utils";
// constants // constants
@ -71,7 +71,7 @@ export const EditorContainer: FC<EditorContainerProps> = (props) => {
onClick={handleContainerClick} onClick={handleContainerClick}
onMouseLeave={handleContainerMouseLeave} onMouseLeave={handleContainerMouseLeave}
className={cn( className={cn(
"editor-container cursor-text relative", `editor-container cursor-text relative line-spacing-${displayConfig.lineSpacing ?? DEFAULT_DISPLAY_CONFIG.lineSpacing}`,
{ {
"active-editor": editor?.isFocused && editor?.isEditable, "active-editor": editor?.isFocused && editor?.isEditable,
}, },

View file

@ -4,6 +4,7 @@ import { TDisplayConfig } from "@/types";
export const DEFAULT_DISPLAY_CONFIG: TDisplayConfig = { export const DEFAULT_DISPLAY_CONFIG: TDisplayConfig = {
fontSize: "large-font", fontSize: "large-font",
fontStyle: "sans-serif", fontStyle: "sans-serif",
lineSpacing: "regular",
}; };
export const ACCEPTED_FILE_MIME_TYPES = ["image/jpeg", "image/jpg", "image/png", "image/webp", "image/gif"]; export const ACCEPTED_FILE_MIME_TYPES = ["image/jpeg", "image/jpg", "image/png", "image/webp", "image/gif"];

View file

@ -26,12 +26,12 @@ export const CoreEditorExtensionsWithoutProps = [
StarterKit.configure({ StarterKit.configure({
bulletList: { bulletList: {
HTMLAttributes: { HTMLAttributes: {
class: "list-disc pl-7 space-y-2", class: "list-disc pl-7 space-y-[--list-spacing-y]",
}, },
}, },
orderedList: { orderedList: {
HTMLAttributes: { HTMLAttributes: {
class: "list-decimal pl-7 space-y-2", class: "list-decimal pl-7 space-y-[--list-spacing-y]",
}, },
}, },
listItem: { listItem: {

View file

@ -55,12 +55,12 @@ export const CoreEditorExtensions = (args: TArguments): Extensions => {
StarterKit.configure({ StarterKit.configure({
bulletList: { bulletList: {
HTMLAttributes: { HTMLAttributes: {
class: "list-disc pl-7 space-y-2", class: "list-disc pl-7 space-y-[--list-spacing-y]",
}, },
}, },
orderedList: { orderedList: {
HTMLAttributes: { HTMLAttributes: {
class: "list-decimal pl-7 space-y-2", class: "list-decimal pl-7 space-y-[--list-spacing-y]",
}, },
}, },
listItem: { listItem: {

View file

@ -46,12 +46,12 @@ export const CoreReadOnlyEditorExtensions = (props: Props): Extensions => {
StarterKit.configure({ StarterKit.configure({
bulletList: { bulletList: {
HTMLAttributes: { HTMLAttributes: {
class: "list-disc pl-7 space-y-2", class: "list-disc pl-7 space-y-[--list-spacing-y]",
}, },
}, },
orderedList: { orderedList: {
HTMLAttributes: { HTMLAttributes: {
class: "list-decimal pl-7 space-y-2", class: "list-decimal pl-7 space-y-[--list-spacing-y]",
}, },
}, },
listItem: { listItem: {

View file

@ -23,7 +23,10 @@ export type TEditorFontStyle = "sans-serif" | "serif" | "monospace";
export type TEditorFontSize = "small-font" | "large-font"; export type TEditorFontSize = "small-font" | "large-font";
export type TEditorLineSpacing = "regular" | "small";
export type TDisplayConfig = { export type TDisplayConfig = {
fontStyle?: TEditorFontStyle; fontStyle?: TEditorFontStyle;
fontSize?: TEditorFontSize; fontSize?: TEditorFontSize;
lineSpacing?: TEditorLineSpacing;
}; };

View file

@ -252,7 +252,7 @@ ul[data-type="taskList"] li[data-checked="true"] {
div[data-type="horizontalRule"] { div[data-type="horizontalRule"] {
line-height: 0; line-height: 0;
padding: 0.25rem 0; padding: var(--divider-padding-top) 0 var(--divider-padding-bottom) 0;
margin-top: 0; margin-top: 0;
margin-bottom: 0; margin-bottom: 0;
@ -335,10 +335,10 @@ p.editor-paragraph-block {
/* tailwind typography */ /* tailwind typography */
.prose :where(h1.editor-heading-block):not(:where([class~="not-prose"], [class~="not-prose"] *)) { .prose :where(h1.editor-heading-block):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
&:not(:first-child) { &:not(:first-child) {
padding-top: 28px; padding-top: var(--heading-1-padding-top);
} }
padding-bottom: 4px; padding-bottom: var(--heading-1-padding-bottom);
font-size: var(--font-size-h1); font-size: var(--font-size-h1);
line-height: var(--line-height-h1); line-height: var(--line-height-h1);
font-weight: 600; font-weight: 600;
@ -346,10 +346,10 @@ p.editor-paragraph-block {
.prose :where(h2.editor-heading-block):not(:where([class~="not-prose"], [class~="not-prose"] *)) { .prose :where(h2.editor-heading-block):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
&:not(:first-child) { &:not(:first-child) {
padding-top: 28px; padding-top: var(--heading-2-padding-top);
} }
padding-bottom: 4px; padding-bottom: var(--heading-2-padding-bottom);
font-size: var(--font-size-h2); font-size: var(--font-size-h2);
line-height: var(--line-height-h2); line-height: var(--line-height-h2);
font-weight: 600; font-weight: 600;
@ -357,10 +357,10 @@ p.editor-paragraph-block {
.prose :where(h3.editor-heading-block):not(:where([class~="not-prose"], [class~="not-prose"] *)) { .prose :where(h3.editor-heading-block):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
&:not(:first-child) { &:not(:first-child) {
padding-top: 28px; padding-top: var(--heading-3-padding-top);
} }
padding-bottom: 4px; padding-bottom: var(--heading-3-padding-bottom);
font-size: var(--font-size-h3); font-size: var(--font-size-h3);
line-height: var(--line-height-h3); line-height: var(--line-height-h3);
font-weight: 600; font-weight: 600;
@ -368,10 +368,10 @@ p.editor-paragraph-block {
.prose :where(h4.editor-heading-block):not(:where([class~="not-prose"], [class~="not-prose"] *)) { .prose :where(h4.editor-heading-block):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
&:not(:first-child) { &:not(:first-child) {
padding-top: 28px; padding-top: var(--heading-4-padding-top);
} }
padding-bottom: 4px; padding-bottom: var(--heading-4-padding-bottom);
font-size: var(--font-size-h4); font-size: var(--font-size-h4);
line-height: var(--line-height-h4); line-height: var(--line-height-h4);
font-weight: 600; font-weight: 600;
@ -379,10 +379,10 @@ p.editor-paragraph-block {
.prose :where(h5.editor-heading-block):not(:where([class~="not-prose"], [class~="not-prose"] *)) { .prose :where(h5.editor-heading-block):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
&:not(:first-child) { &:not(:first-child) {
padding-top: 20px; padding-top: var(--heading-5-padding-top);
} }
padding-bottom: 4px; padding-bottom: var(--heading-5-padding-bottom);
font-size: var(--font-size-h5); font-size: var(--font-size-h5);
line-height: var(--line-height-h5); line-height: var(--line-height-h5);
font-weight: 600; font-weight: 600;
@ -390,10 +390,10 @@ p.editor-paragraph-block {
.prose :where(h6.editor-heading-block):not(:where([class~="not-prose"], [class~="not-prose"] *)) { .prose :where(h6.editor-heading-block):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
&:not(:first-child) { &:not(:first-child) {
padding-top: 20px; padding-top: var(--heading-6-padding-top);
} }
padding-bottom: 4px; padding-bottom: var(--heading-6-padding-bottom);
font-size: var(--font-size-h6); font-size: var(--font-size-h6);
line-height: var(--line-height-h6); line-height: var(--line-height-h6);
font-weight: 600; font-weight: 600;
@ -405,16 +405,16 @@ p.editor-paragraph-block {
} }
&:not(:first-child) { &:not(:first-child) {
padding-top: 4px; padding-top: var(--paragraph-padding-top);
} }
&:not(td p.editor-paragraph-block, th p.editor-paragraph-block) { &:not(td p.editor-paragraph-block, th p.editor-paragraph-block) {
&:last-child { &:last-child {
padding-bottom: 4px; padding-bottom: var(--paragraph-padding-bottom);
} }
&:not(:last-child) { &:not(:last-child) {
padding-bottom: 8px; padding-bottom: var(--paragraph-padding-between);
} }
} }
@ -423,7 +423,7 @@ p.editor-paragraph-block {
} }
p.editor-paragraph-block + p.editor-paragraph-block { p.editor-paragraph-block + p.editor-paragraph-block {
padding-top: 8px !important; padding-top: var(--paragraph-padding-between) !important;
} }
.prose :where(ol):not(:where([class~="not-prose"], [class~="not-prose"] *)) li p.editor-paragraph-block, .prose :where(ol):not(:where([class~="not-prose"], [class~="not-prose"] *)) li p.editor-paragraph-block,

View file

@ -57,4 +57,48 @@
--font-style: monospace; --font-style: monospace;
} }
/* end font styles */ /* end font styles */
/* spacing */
&.line-spacing-regular {
--heading-1-padding-top: 28px;
--heading-1-padding-bottom: 4px;
--heading-2-padding-top: 28px;
--heading-2-padding-bottom: 4px;
--heading-3-padding-top: 28px;
--heading-3-padding-bottom: 4px;
--heading-4-padding-top: 28px;
--heading-4-padding-bottom: 4px;
--heading-5-padding-top: 20px;
--heading-5-padding-bottom: 4px;
--heading-6-padding-top: 20px;
--heading-6-padding-bottom: 4px;
--paragraph-padding-top: 4px;
--paragraph-padding-bottom: 4px;
--paragraph-padding-between: 8px;
--list-spacing-y: 8px;
--divider-padding-top: 4px;
--divider-padding-bottom: 4px;
}
&.line-spacing-small {
--heading-1-padding-top: 16px;
--heading-1-padding-bottom: 4px;
--heading-2-padding-top: 16px;
--heading-2-padding-bottom: 4px;
--heading-3-padding-top: 16px;
--heading-3-padding-bottom: 4px;
--heading-4-padding-top: 16px;
--heading-4-padding-bottom: 4px;
--heading-5-padding-top: 12px;
--heading-5-padding-bottom: 4px;
--heading-6-padding-top: 12px;
--heading-6-padding-bottom: 4px;
--paragraph-padding-top: 2px;
--paragraph-padding-bottom: 2px;
--paragraph-padding-between: 4px;
--list-spacing-y: 0px;
--divider-padding-top: 0px;
--divider-padding-bottom: 4px;
}
/* end spacing */
} }

View file

@ -1,7 +1,8 @@
{ {
"name": "@plane/eslint-config", "name": "@plane/eslint-config",
"private": true, "private": true,
"version": "0.25.0", "version": "0.25.1",
"license": "AGPL-3.0",
"files": [ "files": [
"library.js", "library.js",
"next.js", "next.js",

View file

@ -1,6 +1,7 @@
{ {
"name": "@plane/hooks", "name": "@plane/hooks",
"version": "0.25.0", "version": "0.25.1",
"license": "AGPL-3.0",
"description": "React hooks that are shared across multiple apps internally", "description": "React hooks that are shared across multiple apps internally",
"private": true, "private": true,
"main": "./dist/index.js", "main": "./dist/index.js",

View file

@ -1,6 +1,7 @@
{ {
"name": "@plane/i18n", "name": "@plane/i18n",
"version": "0.25.0", "version": "0.25.1",
"license": "AGPL-3.0",
"description": "I18n shared across multiple apps internally", "description": "I18n shared across multiple apps internally",
"private": true, "private": true,
"main": "./src/index.ts", "main": "./src/index.ts",

View file

@ -8,6 +8,8 @@ export const SUPPORTED_LANGUAGES: ILanguageOption[] = [
{ label: "Español", value: "es" }, { label: "Español", value: "es" },
{ label: "日本語", value: "ja" }, { label: "日本語", value: "ja" },
{ label: "中文", value: "zh-CN" }, { label: "中文", value: "zh-CN" },
{ label: "Русский", value: "ru" },
{ label: "Italian", value: "it" },
]; ];
export const STORAGE_KEY = "userLanguage"; export const STORAGE_KEY = "userLanguage";

View file

@ -461,6 +461,8 @@
"states": "States", "states": "States",
"state": "State", "state": "State",
"state_groups": "State groups", "state_groups": "State groups",
"state_group": "State group",
"priorities": "Priorities",
"priority": "Priority", "priority": "Priority",
"team_project": "Team project", "team_project": "Team project",
"project": "Project", "project": "Project",
@ -469,12 +471,16 @@
"module": "Module", "module": "Module",
"modules": "Modules", "modules": "Modules",
"labels": "Labels", "labels": "Labels",
"label": "Label",
"assignees": "Assignees", "assignees": "Assignees",
"assignee": "Assignee", "assignee": "Assignee",
"created_by": "Created by", "created_by": "Created by",
"none": "None", "none": "None",
"link": "Link", "link": "Link",
"estimates": "Estimates",
"estimate": "Estimate", "estimate": "Estimate",
"created_at": "Created at",
"completed_at": "Completed at",
"layout": "Layout", "layout": "Layout",
"filters": "Filters", "filters": "Filters",
"display": "Display", "display": "Display",
@ -517,7 +523,6 @@
"add_more": "Add more", "add_more": "Add more",
"defaults": "Defaults", "defaults": "Defaults",
"add_label": "Add label", "add_label": "Add label",
"estimates": "Estimates",
"customize_time_range": "Customize time range", "customize_time_range": "Customize time range",
"loading": "Loading", "loading": "Loading",
"attachments": "Attachments", "attachments": "Attachments",
@ -656,8 +661,6 @@
"select": "Select", "select": "Select",
"upgrade": "Upgrade", "upgrade": "Upgrade",
"add_seats": "Add Seats", "add_seats": "Add Seats",
"label": "Label",
"priorities": "Priorities",
"projects": "Projects", "projects": "Projects",
"workspace": "Workspace", "workspace": "Workspace",
"workspaces": "Workspaces", "workspaces": "Workspaces",

View file

@ -633,6 +633,8 @@
"states": "Estados", "states": "Estados",
"state": "Estado", "state": "Estado",
"state_groups": "Grupos de estados", "state_groups": "Grupos de estados",
"state_group": "Grupos de estado",
"priorities": "Prioridades",
"priority": "Prioridad", "priority": "Prioridad",
"team_project": "Proyecto de equipo", "team_project": "Proyecto de equipo",
"project": "Proyecto", "project": "Proyecto",
@ -641,12 +643,16 @@
"module": "Módulo", "module": "Módulo",
"modules": "Módulos", "modules": "Módulos",
"labels": "Etiquetas", "labels": "Etiquetas",
"label": "Etiqueta",
"assignees": "Asignados", "assignees": "Asignados",
"assignee": "Asignado", "assignee": "Asignado",
"created_by": "Creado por", "created_by": "Creado por",
"none": "Ninguno", "none": "Ninguno",
"link": "Enlace", "link": "Enlace",
"estimates": "Estimaciones",
"estimate": "Estimación", "estimate": "Estimación",
"created_at": "Creado en",
"completed_at": "Completado en",
"layout": "Diseño", "layout": "Diseño",
"filters": "Filtros", "filters": "Filtros",
"display": "Mostrar", "display": "Mostrar",
@ -689,7 +695,6 @@
"add_more": "Agregar más", "add_more": "Agregar más",
"defaults": "Valores predeterminados", "defaults": "Valores predeterminados",
"add_label": "Agregar etiqueta", "add_label": "Agregar etiqueta",
"estimates": "Estimaciones",
"customize_time_range": "Personalizar rango de tiempo", "customize_time_range": "Personalizar rango de tiempo",
"loading": "Cargando", "loading": "Cargando",
"attachments": "Archivos adjuntos", "attachments": "Archivos adjuntos",
@ -827,8 +832,6 @@
"select": "Seleccionar", "select": "Seleccionar",
"upgrade": "Mejorar", "upgrade": "Mejorar",
"add_seats": "Agregar asientos", "add_seats": "Agregar asientos",
"label": "Etiqueta",
"priorities": "Prioridades",
"projects": "Proyectos", "projects": "Proyectos",
"workspace": "Espacio de trabajo", "workspace": "Espacio de trabajo",
"workspaces": "Espacios de trabajo", "workspaces": "Espacios de trabajo",

View file

@ -631,6 +631,8 @@
"states": "États", "states": "États",
"state": "État", "state": "État",
"state_groups": "Groupes d'états", "state_groups": "Groupes d'états",
"state_group": "Groupe d'état",
"priorities": "Priorités",
"priority": "Priorité", "priority": "Priorité",
"team_project": "Projet d'équipe", "team_project": "Projet d'équipe",
"project": "Projet", "project": "Projet",
@ -639,12 +641,16 @@
"module": "Module", "module": "Module",
"modules": "Modules", "modules": "Modules",
"labels": "Étiquettes", "labels": "Étiquettes",
"label": "Étiquette",
"assignees": "Assignés", "assignees": "Assignés",
"assignee": "Assigné", "assignee": "Assigné",
"created_by": "Créé par", "created_by": "Créé par",
"none": "Aucun", "none": "Aucun",
"link": "Lien", "link": "Lien",
"estimates": "Estimations",
"estimate": "Estimation", "estimate": "Estimation",
"created_at": "Créé le",
"completed_at": "Terminé le",
"layout": "Disposition", "layout": "Disposition",
"filters": "Filtres", "filters": "Filtres",
"display": "Affichage", "display": "Affichage",
@ -687,7 +693,6 @@
"add_more": "Ajouter plus", "add_more": "Ajouter plus",
"defaults": "Par défaut", "defaults": "Par défaut",
"add_label": "Ajouter une étiquette", "add_label": "Ajouter une étiquette",
"estimates": "Estimations",
"customize_time_range": "Personnaliser la plage de temps", "customize_time_range": "Personnaliser la plage de temps",
"loading": "Chargement", "loading": "Chargement",
"attachments": "Pièces jointes", "attachments": "Pièces jointes",
@ -825,8 +830,6 @@
"select": "Sélectionner", "select": "Sélectionner",
"upgrade": "Mettre à niveau", "upgrade": "Mettre à niveau",
"add_seats": "Ajouter des sièges", "add_seats": "Ajouter des sièges",
"label": "Étiquette",
"priorities": "Priorités",
"projects": "Projets", "projects": "Projets",
"workspace": "Espace de travail", "workspace": "Espace de travail",
"workspaces": "Espaces de travail", "workspaces": "Espaces de travail",

File diff suppressed because it is too large Load diff

View file

@ -631,6 +631,8 @@
"states": "ステータス", "states": "ステータス",
"state": "ステータス", "state": "ステータス",
"state_groups": "ステータスグループ", "state_groups": "ステータスグループ",
"state_group": "ステート グループ",
"priorities": "優先度",
"priority": "優先度", "priority": "優先度",
"team_project": "チームプロジェクト", "team_project": "チームプロジェクト",
"project": "プロジェクト", "project": "プロジェクト",
@ -639,12 +641,16 @@
"module": "モジュール", "module": "モジュール",
"modules": "モジュール", "modules": "モジュール",
"labels": "ラベル", "labels": "ラベル",
"label": "ラベル",
"assignees": "担当者", "assignees": "担当者",
"assignee": "担当者", "assignee": "担当者",
"created_by": "作成者", "created_by": "作成者",
"none": "なし", "none": "なし",
"link": "リンク", "link": "リンク",
"estimates": "見積もり",
"estimate": "見積もり", "estimate": "見積もり",
"created_at": "クリエイテッド アット",
"completed_at": "コンプリーテッド アット",
"layout": "レイアウト", "layout": "レイアウト",
"filters": "フィルター", "filters": "フィルター",
"display": "表示", "display": "表示",
@ -687,7 +693,6 @@
"add_more": "さらに追加", "add_more": "さらに追加",
"defaults": "デフォルト", "defaults": "デフォルト",
"add_label": "ラベルを追加", "add_label": "ラベルを追加",
"estimates": "見積もり",
"customize_time_range": "期間をカスタマイズ", "customize_time_range": "期間をカスタマイズ",
"loading": "読み込み中", "loading": "読み込み中",
"attachments": "添付ファイル", "attachments": "添付ファイル",
@ -825,8 +830,6 @@
"select": "選択", "select": "選択",
"upgrade": "アップグレード", "upgrade": "アップグレード",
"add_seats": "シートを追加", "add_seats": "シートを追加",
"label": "ラベル",
"priorities": "優先度",
"projects": "プロジェクト", "projects": "プロジェクト",
"workspace": "ワークスペース", "workspace": "ワークスペース",
"workspaces": "ワークスペース", "workspaces": "ワークスペース",

File diff suppressed because it is too large Load diff

View file

@ -631,6 +631,8 @@
"states": "状态", "states": "状态",
"state": "状态", "state": "状态",
"state_groups": "状态组", "state_groups": "状态组",
"state_group": "状态组",
"priorities": "优先级",
"priority": "优先级", "priority": "优先级",
"team_project": "团队项目", "team_project": "团队项目",
"project": "项目", "project": "项目",
@ -639,12 +641,16 @@
"module": "模块", "module": "模块",
"modules": "模块", "modules": "模块",
"labels": "标签", "labels": "标签",
"label": "标签",
"assignees": "负责人", "assignees": "负责人",
"assignee": "负责人", "assignee": "负责人",
"created_by": "创建者", "created_by": "创建者",
"none": "无", "none": "无",
"link": "链接", "link": "链接",
"estimates": "估算",
"estimate": "估算", "estimate": "估算",
"created_at": "创建于",
"completed_at": "完成于",
"layout": "布局", "layout": "布局",
"filters": "筛选", "filters": "筛选",
"display": "显示", "display": "显示",
@ -687,7 +693,6 @@
"add_more": "添加更多", "add_more": "添加更多",
"defaults": "默认值", "defaults": "默认值",
"add_label": "添加标签", "add_label": "添加标签",
"estimates": "估算",
"customize_time_range": "自定义时间范围", "customize_time_range": "自定义时间范围",
"loading": "加载中", "loading": "加载中",
"attachments": "附件", "attachments": "附件",
@ -825,8 +830,6 @@
"select": "选择", "select": "选择",
"upgrade": "升级", "upgrade": "升级",
"add_seats": "添加席位", "add_seats": "添加席位",
"label": "标签",
"priorities": "优先级",
"projects": "项目", "projects": "项目",
"workspace": "工作区", "workspace": "工作区",
"workspaces": "工作区", "workspaces": "工作区",

View file

@ -147,6 +147,10 @@ export class TranslationStore {
return import("../locales/ja/translations.json"); return import("../locales/ja/translations.json");
case "zh-CN": case "zh-CN":
return import("../locales/zh-CN/translations.json"); return import("../locales/zh-CN/translations.json");
case "ru":
return import("../locales/ru/translations.json");
case "it":
return import("../locales/it/translations.json");
default: default:
throw new Error(`Unsupported language: ${language}`); throw new Error(`Unsupported language: ${language}`);
} }

View file

@ -1,4 +1,4 @@
export type TLanguage = "en" | "fr" | "es" | "ja" | "zh-CN"; export type TLanguage = "en" | "fr" | "es" | "ja" | "zh-CN" | "ru" | "it";
export interface ILanguageOption { export interface ILanguageOption {
label: string; label: string;

View file

@ -1,6 +1,7 @@
{ {
"name": "@plane/logger", "name": "@plane/logger",
"version": "0.25.0", "version": "0.25.1",
"license": "AGPL-3.0",
"description": "Logger shared across multiple apps internally", "description": "Logger shared across multiple apps internally",
"private": true, "private": true,
"main": "./src/index.ts", "main": "./src/index.ts",

View file

@ -1,7 +1,8 @@
{ {
"name": "@plane/propel", "name": "@plane/propel",
"version": "0.25.0", "version": "0.25.1",
"private": true, "private": true,
"license": "AGPL-3.0",
"scripts": { "scripts": {
"lint": "eslint src --ext .ts,.tsx", "lint": "eslint src --ext .ts,.tsx",
"lint:errors": "eslint src --ext .ts,.tsx --quiet" "lint:errors": "eslint src --ext .ts,.tsx --quiet"

View file

@ -1,6 +1,7 @@
{ {
"name": "@plane/services", "name": "@plane/services",
"version": "0.25.0", "version": "0.25.1",
"license": "AGPL-3.0",
"private": true, "private": true,
"main": "./src/index.ts", "main": "./src/index.ts",
"scripts": { "scripts": {

View file

@ -1,6 +1,7 @@
{ {
"name": "@plane/shared-state", "name": "@plane/shared-state",
"version": "0.25.0", "version": "0.25.1",
"license": "AGPL-3.0",
"description": "Shared state shared across multiple apps internally", "description": "Shared state shared across multiple apps internally",
"private": true, "private": true,
"main": "./src/index.ts", "main": "./src/index.ts",

View file

@ -1,6 +1,7 @@
{ {
"name": "@plane/tailwind-config", "name": "@plane/tailwind-config",
"version": "0.25.0", "version": "0.25.1",
"license": "AGPL-3.0",
"description": "common tailwind configuration across monorepo", "description": "common tailwind configuration across monorepo",
"main": "tailwind.config.js", "main": "tailwind.config.js",
"private": true, "private": true,

View file

@ -1,6 +1,7 @@
{ {
"name": "@plane/types", "name": "@plane/types",
"version": "0.25.0", "version": "0.25.1",
"license": "AGPL-3.0",
"private": true, "private": true,
"types": "./src/index.d.ts", "types": "./src/index.d.ts",
"main": "./src/index.d.ts" "main": "./src/index.d.ts"

View file

@ -1,6 +1,7 @@
{ {
"name": "@plane/typescript-config", "name": "@plane/typescript-config",
"version": "0.25.0", "version": "0.25.1",
"license": "AGPL-3.0",
"private": true, "private": true,
"files": [ "files": [
"base.json", "base.json",

View file

@ -2,12 +2,12 @@
"name": "@plane/ui", "name": "@plane/ui",
"description": "UI components shared across multiple apps internally", "description": "UI components shared across multiple apps internally",
"private": true, "private": true,
"version": "0.25.0", "version": "0.25.1",
"main": "./dist/index.js", "main": "./dist/index.js",
"module": "./dist/index.mjs", "module": "./dist/index.mjs",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"sideEffects": false, "sideEffects": false,
"license": "MIT", "license": "AGPL-3.0",
"files": [ "files": [
"dist/**" "dist/**"
], ],

View file

@ -1,7 +1,8 @@
{ {
"name": "@plane/utils", "name": "@plane/utils",
"version": "0.25.0", "version": "0.25.1",
"description": "Helper functions shared across multiple apps internally", "description": "Helper functions shared across multiple apps internally",
"license": "AGPL-3.0",
"private": true, "private": true,
"main": "./dist/index.js", "main": "./dist/index.js",
"module": "./dist/index.mjs", "module": "./dist/index.mjs",

View file

@ -1,7 +1,8 @@
{ {
"name": "space", "name": "space",
"version": "0.25.0", "version": "0.25.1",
"private": true, "private": true,
"license": "AGPL-3.0",
"scripts": { "scripts": {
"dev": "turbo run develop", "dev": "turbo run develop",
"develop": "next dev -p 3002", "develop": "next dev -p 3002",

View file

@ -108,7 +108,7 @@ export const ExtendedProjectSidebar = observer(() => {
<div <div
ref={extendedProjectSidebarRef} ref={extendedProjectSidebarRef}
className={cn( className={cn(
"fixed top-0 h-full z-[19] flex flex-col gap-2 w-[300px] transform transition-all duration-300 ease-in-out bg-custom-sidebar-background-100 border-r border-custom-sidebar-border-200 shadow-md", "absolute top-0 h-full z-[19] flex flex-col gap-2 w-[300px] transform transition-all duration-300 ease-in-out bg-custom-sidebar-background-100 border-r border-custom-sidebar-border-200 shadow-md",
{ {
"translate-x-0 opacity-100 pointer-events-auto": extendedProjectSidebarCollapsed, "translate-x-0 opacity-100 pointer-events-auto": extendedProjectSidebarCollapsed,
"-translate-x-full opacity-0 pointer-events-none": !extendedProjectSidebarCollapsed, "-translate-x-full opacity-0 pointer-events-none": !extendedProjectSidebarCollapsed,

View file

@ -104,7 +104,7 @@ export const ExtendedAppSidebar = observer(() => {
<div <div
ref={extendedSidebarRef} ref={extendedSidebarRef}
className={cn( className={cn(
"fixed top-0 h-full z-[19] flex flex-col gap-0.5 w-[300px] transform transition-all duration-300 ease-in-out bg-custom-sidebar-background-100 border-r border-custom-sidebar-border-200 p-4 shadow-md pb-6", "absolute top-0 h-full z-[19] flex flex-col gap-0.5 w-[300px] transform transition-all duration-300 ease-in-out bg-custom-sidebar-background-100 border-r border-custom-sidebar-border-200 p-4 shadow-md pb-6",
{ {
"translate-x-0 opacity-100 pointer-events-auto": extendedSidebarCollapsed, "translate-x-0 opacity-100 pointer-events-auto": extendedSidebarCollapsed,
"-translate-x-full opacity-0 pointer-events-none": !extendedSidebarCollapsed, "-translate-x-full opacity-0 pointer-events-none": !extendedSidebarCollapsed,

View file

@ -2,20 +2,21 @@ import { observer } from "mobx-react";
import { Check } from "lucide-react"; import { Check } from "lucide-react";
import { Combobox } from "@headlessui/react"; import { Combobox } from "@headlessui/react";
type Props = { export type TStateOptionProps = {
projectId: string | null | undefined; projectId: string | null | undefined;
option: { option: {
value: string | undefined; value: string | undefined;
query: string; query: string;
content: JSX.Element; content: JSX.Element;
}; };
filterAvailableStateIds: boolean;
selectedValue: string | null | undefined; selectedValue: string | null | undefined;
className?: string; className?: string;
filterAvailableStateIds?: boolean;
isForWorkItemCreation?: boolean; isForWorkItemCreation?: boolean;
alwaysAllowStateChange?: boolean;
}; };
export const StateOption = observer((props: Props) => { export const StateOption = observer((props: TStateOptionProps) => {
const { option, className = "" } = props; const { option, className = "" } = props;
return ( return (

View file

@ -101,10 +101,10 @@ export const CycleSidebarHeader: FC<Props> = observer((props) => {
}); });
}; };
const submitChanges = (data: Partial<ICycle>, changedProperty: string) => { const submitChanges = async (data: Partial<ICycle>, changedProperty: string) => {
if (!workspaceSlug || !projectId || !cycleDetails.id) return; if (!workspaceSlug || !projectId || !cycleDetails.id) return;
updateCycleDetails(workspaceSlug.toString(), projectId.toString(), cycleDetails.id.toString(), data) await updateCycleDetails(workspaceSlug.toString(), projectId.toString(), cycleDetails.id.toString(), data)
.then((res) => { .then((res) => {
captureCycleEvent({ captureCycleEvent({
eventName: CYCLE_UPDATED, eventName: CYCLE_UPDATED,
@ -146,22 +146,21 @@ export const CycleSidebarHeader: FC<Props> = observer((props) => {
}; };
const handleDateChange = async (startDate: Date | undefined, endDate: Date | undefined) => { const handleDateChange = async (startDate: Date | undefined, endDate: Date | undefined) => {
if (!startDate || !endDate) return;
let isDateValid = false; let isDateValid = false;
const payload = { const payload = {
start_date: renderFormattedPayloadDate(startDate), start_date: renderFormattedPayloadDate(startDate) || null,
end_date: renderFormattedPayloadDate(endDate), end_date: renderFormattedPayloadDate(endDate) || null,
}; };
if (cycleDetails?.start_date && cycleDetails.end_date) if (payload?.start_date && payload.end_date) {
isDateValid = await dateChecker({ isDateValid = await dateChecker({
...payload, ...payload,
cycle_id: cycleDetails.id, cycle_id: cycleDetails.id,
}); });
else isDateValid = await dateChecker(payload); } else {
isDateValid = true;
}
if (isDateValid) { if (isDateValid) {
submitChanges(payload, "date_range"); submitChanges(payload, "date_range");
setToast({ setToast({
@ -175,8 +174,8 @@ export const CycleSidebarHeader: FC<Props> = observer((props) => {
title: t("project_cycles.action.update.failed.title"), title: t("project_cycles.action.update.failed.title"),
message: t("project_cycles.action.update.error.already_exists"), message: t("project_cycles.action.update.error.already_exists"),
}); });
reset({ ...cycleDetails });
} }
return isDateValid;
}; };
const isEditingAllowed = allowPermissions( const isEditingAllowed = allowPermissions(
@ -296,16 +295,18 @@ export const CycleSidebarHeader: FC<Props> = observer((props) => {
render={({ field: { value: endDateValue, onChange: onChangeEndDate } }) => ( render={({ field: { value: endDateValue, onChange: onChangeEndDate } }) => (
<DateRangeDropdown <DateRangeDropdown
className="h-7" className="h-7"
buttonVariant="transparent-with-text" buttonVariant="border-with-text"
minDate={new Date()} minDate={new Date()}
value={{ value={{
from: getDate(startDateValue), from: getDate(startDateValue),
to: getDate(endDateValue), to: getDate(endDateValue),
}} }}
onSelect={(val) => { onSelect={async (val) => {
const isDateValid = await handleDateChange(val?.from, val?.to);
if (isDateValid) {
onChangeStartDate(val?.from ? renderFormattedPayloadDate(val.from) : null); onChangeStartDate(val?.from ? renderFormattedPayloadDate(val.from) : null);
onChangeEndDate(val?.to ? renderFormattedPayloadDate(val.to) : null); onChangeEndDate(val?.to ? renderFormattedPayloadDate(val.to) : null);
handleDateChange(val?.from, val?.to); }
}} }}
placeholder={{ placeholder={{
from: "Start date", from: "Start date",

View file

@ -3,31 +3,21 @@
import React, { FC, MouseEvent, useEffect, useMemo, useState } from "react"; import React, { FC, MouseEvent, useEffect, useMemo, useState } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useParams, usePathname, useSearchParams } from "next/navigation"; import { useParams, usePathname, useSearchParams } from "next/navigation";
import { Controller, useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { Eye, Users } from "lucide-react"; import { Eye, Users } from "lucide-react";
// types // types
import { CYCLE_FAVORITED, CYCLE_UNFAVORITED, EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; import { CYCLE_FAVORITED, CYCLE_UNFAVORITED, EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
import { ICycle, TCycleGroups } from "@plane/types"; import { ICycle, TCycleGroups } from "@plane/types";
// ui // ui
import { import { Avatar, AvatarGroup, FavoriteStar, LayersIcon, Tooltip, TransferIcon, setPromiseToast } from "@plane/ui";
Avatar,
AvatarGroup,
FavoriteStar,
LayersIcon,
TOAST_TYPE,
Tooltip,
TransferIcon,
setPromiseToast,
setToast,
} from "@plane/ui";
// components // components
import { CycleQuickActions, TransferIssuesModal } from "@/components/cycles"; import { CycleQuickActions, TransferIssuesModal } from "@/components/cycles";
import { DateRangeDropdown } from "@/components/dropdowns"; import { DateRangeDropdown } from "@/components/dropdowns";
import { ButtonAvatars } from "@/components/dropdowns/member/avatar"; import { ButtonAvatars } from "@/components/dropdowns/member/avatar";
// constants // constants
// helpers // helpers
import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper"; import { getDate } from "@/helpers/date-time.helper";
import { getFileURL } from "@/helpers/file.helper"; import { getFileURL } from "@/helpers/file.helper";
// hooks // hooks
import { generateQueryParams } from "@/helpers/router.helper"; import { generateQueryParams } from "@/helpers/router.helper";
@ -36,11 +26,6 @@ import { useAppRouter } from "@/hooks/use-app-router";
import { usePlatformOS } from "@/hooks/use-platform-os"; import { usePlatformOS } from "@/hooks/use-platform-os";
// plane web components // plane web components
import { CycleAdditionalActions } from "@/plane-web/components/cycles"; import { CycleAdditionalActions } from "@/plane-web/components/cycles";
// plane web constants
// services
import { CycleService } from "@/services/cycle.service";
const cycleService = new CycleService();
type Props = { type Props = {
workspaceSlug: string; workspaceSlug: string;
@ -77,7 +62,7 @@ export const CycleListItemAction: FC<Props> = observer((props) => {
const { getUserDetails } = useMember(); const { getUserDetails } = useMember();
// form // form
const { control, reset } = useForm({ const { control, reset, getValues } = useForm({
defaultValues, defaultValues,
}); });
@ -98,7 +83,6 @@ export const CycleListItemAction: FC<Props> = observer((props) => {
workspaceSlug, workspaceSlug,
projectId projectId
); );
const renderIcon = Boolean(cycleDetails.start_date) || Boolean(cycleDetails.end_date);
// handlers // handlers
const handleAddToFavorites = (e: MouseEvent<HTMLButtonElement>) => { const handleAddToFavorites = (e: MouseEvent<HTMLButtonElement>) => {
@ -157,54 +141,6 @@ export const CycleListItemAction: FC<Props> = observer((props) => {
}); });
}; };
const submitChanges = (data: Partial<ICycle>) => {
if (!workspaceSlug || !projectId || !cycleId) return;
updateCycleDetails(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), data);
};
const dateChecker = async (payload: any) => {
try {
const res = await cycleService.cycleDateCheck(workspaceSlug as string, projectId as string, payload);
return res.status;
} catch {
return false;
}
};
const handleDateChange = async (startDate: Date | undefined, endDate: Date | undefined) => {
if (!startDate || !endDate) return;
let isDateValid = false;
const payload = {
start_date: renderFormattedPayloadDate(startDate),
end_date: renderFormattedPayloadDate(endDate),
};
if (cycleDetails && cycleDetails.start_date && cycleDetails.end_date)
isDateValid = await dateChecker({
...payload,
cycle_id: cycleDetails.id,
});
else isDateValid = await dateChecker(payload);
if (isDateValid) {
submitChanges(payload);
setToast({
type: TOAST_TYPE.SUCCESS,
title: t("project_cycles.action.update.success.title"),
message: t("project_cycles.action.update.success.description"),
});
} else {
setToast({
type: TOAST_TYPE.ERROR,
title: t("project_cycles.action.update.failed.title"),
message: t("project_cycles.action.update.error.already_exists"),
});
reset({ ...cycleDetails });
}
};
const createdByDetails = cycleDetails.created_by ? getUserDetails(cycleDetails.created_by) : undefined; const createdByDetails = cycleDetails.created_by ? getUserDetails(cycleDetails.created_by) : undefined;
useEffect(() => { useEffect(() => {
@ -214,10 +150,6 @@ export const CycleListItemAction: FC<Props> = observer((props) => {
}); });
}, [cycleDetails, reset]); }, [cycleDetails, reset]);
const isArchived = Boolean(cycleDetails.archived_at);
const isCompleted = cycleStatus === "completed";
const isDisabled = !isEditingAllowed || isArchived || isCompleted;
// handlers // handlers
const openCycleOverview = (e: MouseEvent<HTMLButtonElement | HTMLAnchorElement>) => { const openCycleOverview = (e: MouseEvent<HTMLButtonElement | HTMLAnchorElement>) => {
e.preventDefault(); e.preventDefault();
@ -266,39 +198,27 @@ export const CycleListItemAction: FC<Props> = observer((props) => {
</div> </div>
)} )}
{!isActive && ( {!isActive && cycleDetails.start_date && (
<Controller
control={control}
name="start_date"
render={({ field: { value: startDateValue, onChange: onChangeStartDate } }) => (
<Controller
control={control}
name="end_date"
render={({ field: { value: endDateValue, onChange: onChangeEndDate } }) => (
<DateRangeDropdown <DateRangeDropdown
buttonContainerClassName={`h-6 w-full flex ${isDisabled ? "cursor-not-allowed" : "cursor-pointer"} items-center gap-1.5 text-custom-text-300 border-[0.5px] border-custom-border-300 rounded text-xs`} buttonVariant={"transparent-with-text"}
buttonVariant="transparent-with-text" buttonContainerClassName={`h-6 w-full cursor-auto flex items-center gap-1.5 text-custom-text-300 rounded text-xs [&>div]:hover:bg-transparent`}
buttonClassName="p-0"
minDate={new Date()} minDate={new Date()}
value={{ value={{
from: getDate(startDateValue), from: getDate(cycleDetails.start_date),
to: getDate(endDateValue), to: getDate(cycleDetails.end_date),
}}
onSelect={(val) => {
onChangeStartDate(val?.from ? renderFormattedPayloadDate(val.from) : null);
onChangeEndDate(val?.to ? renderFormattedPayloadDate(val.to) : null);
handleDateChange(val?.from, val?.to);
}} }}
placeholder={{ placeholder={{
from: "Start date", from: "Start date",
to: "End date", to: "End date",
}} }}
showTooltip
required={cycleDetails.status !== "draft"} required={cycleDetails.status !== "draft"}
disabled={isDisabled} disabled
hideIcon={{ from: renderIcon ?? true, to: renderIcon }} hideIcon={{
/> from: false,
)} to: false,
/> }}
)}
/> />
)} )}

View file

@ -35,7 +35,7 @@ type Props = {
}; };
minDate?: Date; minDate?: Date;
maxDate?: Date; maxDate?: Date;
onSelect: (range: DateRange | undefined) => void; onSelect?: (range: DateRange | undefined) => void;
placeholder?: { placeholder?: {
from?: string; from?: string;
to?: string; to?: string;
@ -204,11 +204,7 @@ export const DateRangeDropdown: React.FC<Props> = (props) => {
classNames={{ root: `p-3 rounded-md` }} classNames={{ root: `p-3 rounded-md` }}
selected={dateRange} selected={dateRange}
onSelect={(val) => { onSelect={(val) => {
onSelect(val); onSelect?.(val);
setDateRange({
from: val?.from ?? undefined,
to: val?.to ?? undefined,
});
}} }}
mode="range" mode="range"
disabled={disabledDays} disabled={disabledDays}

View file

@ -36,6 +36,7 @@ type Props = TDropdownProps & {
stateIds?: string[]; stateIds?: string[];
filterAvailableStateIds?: boolean; filterAvailableStateIds?: boolean;
isForWorkItemCreation?: boolean; isForWorkItemCreation?: boolean;
alwaysAllowStateChange?: boolean;
}; };
export const StateDropdown: React.FC<Props> = observer((props) => { export const StateDropdown: React.FC<Props> = observer((props) => {
@ -59,8 +60,6 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
value, value,
renderByDefault = true, renderByDefault = true,
stateIds, stateIds,
filterAvailableStateIds = true,
isForWorkItemCreation = false,
} = props; } = props;
// states // states
const [query, setQuery] = useState(""); const [query, setQuery] = useState("");
@ -235,13 +234,11 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
filteredOptions.length > 0 ? ( filteredOptions.length > 0 ? (
filteredOptions.map((option) => ( filteredOptions.map((option) => (
<StateOption <StateOption
{...props}
key={option.value} key={option.value}
option={option} option={option}
projectId={projectId}
filterAvailableStateIds={filterAvailableStateIds}
selectedValue={value} selectedValue={value}
className="flex w-full cursor-pointer select-none items-center justify-between gap-2 truncate rounded px-1 py-1.5" className="flex w-full cursor-pointer select-none items-center justify-between gap-2 truncate rounded px-1 py-1.5"
isForWorkItemCreation={isForWorkItemCreation}
/> />
)) ))
) : ( ) : (

View file

@ -61,6 +61,7 @@ export const InboxIssueProperties: FC<TInboxIssueProperties> = observer((props)
projectId={projectId} projectId={projectId}
buttonVariant="border-with-text" buttonVariant="border-with-text"
tabIndex={getIndex("state_id")} tabIndex={getIndex("state_id")}
isForWorkItemCreation={!data?.id}
/> />
</div> </div>

View file

@ -66,7 +66,7 @@ export const IssueAttachmentsListItem: FC<TIssueAttachmentsListItem> = observer(
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
{attachment?.updated_by && ( {attachment?.created_by && (
<> <>
<Tooltip <Tooltip
isMobile={isMobile} isMobile={isMobile}

View file

@ -26,7 +26,7 @@ export const IssueDetailWidgetCollapsibles: FC<Props> = observer((props) => {
const { const {
issue: { getIssueById }, issue: { getIssueById },
subIssues: { subIssuesByIssueId }, subIssues: { subIssuesByIssueId },
attachment: { getAttachmentsUploadStatusByIssueId }, attachment: { getAttachmentsCountByIssueId, getAttachmentsUploadStatusByIssueId },
relation: { getRelationCountByIssueId }, relation: { getRelationCountByIssueId },
} = useIssueDetail(); } = useIssueDetail();
@ -41,8 +41,8 @@ export const IssueDetailWidgetCollapsibles: FC<Props> = observer((props) => {
const shouldRenderRelations = issueRelationsCount > 0; const shouldRenderRelations = issueRelationsCount > 0;
const shouldRenderLinks = !!issue?.link_count && issue?.link_count > 0; const shouldRenderLinks = !!issue?.link_count && issue?.link_count > 0;
const attachmentUploads = getAttachmentsUploadStatusByIssueId(issueId); const attachmentUploads = getAttachmentsUploadStatusByIssueId(issueId);
const shouldRenderAttachments = const attachmentsCount = getAttachmentsCountByIssueId(issueId);
(!!issue?.attachment_count && issue?.attachment_count > 0) || (!!attachmentUploads && attachmentUploads.length > 0); const shouldRenderAttachments = attachmentsCount > 0 || (!!attachmentUploads && attachmentUploads.length > 0);
return ( return (
<div className="flex flex-col"> <div className="flex flex-col">

View file

@ -244,6 +244,7 @@ export const ListGroup = observer((props: Props) => {
const isDragAllowed = !!group_by && DRAG_ALLOWED_GROUPS.includes(group_by); const isDragAllowed = !!group_by && DRAG_ALLOWED_GROUPS.includes(group_by);
const canOverlayBeVisible = isWorkflowDropDisabled || orderBy !== "sort_order" || !!group.isDropDisabled; const canOverlayBeVisible = isWorkflowDropDisabled || orderBy !== "sort_order" || !!group.isDropDisabled;
const isDropDisabled = isWorkflowDropDisabled || !!group.isDropDisabled;
const isGroupByCreatedBy = group_by === "created_by"; const isGroupByCreatedBy = group_by === "created_by";
const shouldExpand = (!!groupIssueCount && isExpanded) || !group_by; const shouldExpand = (!!groupIssueCount && isExpanded) || !group_by;
@ -253,7 +254,7 @@ export const ListGroup = observer((props: Props) => {
ref={groupRef} ref={groupRef}
className={cn(`relative flex flex-shrink-0 flex-col border-[1px] border-transparent`, { className={cn(`relative flex flex-shrink-0 flex-col border-[1px] border-transparent`, {
"border-custom-primary-100": isDraggingOverColumn, "border-custom-primary-100": isDraggingOverColumn,
"border-custom-error-200": isDraggingOverColumn && !!group.isDropDisabled, "border-custom-error-200": isDraggingOverColumn && isDropDisabled,
})} })}
> >
<Row <Row
@ -283,7 +284,7 @@ export const ListGroup = observer((props: Props) => {
<GroupDragOverlay <GroupDragOverlay
dragColumnOrientation={dragColumnOrientation} dragColumnOrientation={dragColumnOrientation}
canOverlayBeVisible={canOverlayBeVisible} canOverlayBeVisible={canOverlayBeVisible}
isDropDisabled={isWorkflowDropDisabled || !!group.isDropDisabled} isDropDisabled={isDropDisabled}
workflowDisabledSource={workflowDisabledSource} workflowDisabledSource={workflowDisabledSource}
dropErrorMessage={group.dropErrorMessage} dropErrorMessage={group.dropErrorMessage}
orderBy={orderBy} orderBy={orderBy}

View file

@ -411,12 +411,13 @@ export const ModuleAnalyticsSidebar: React.FC<Props> = observer((props) => {
/> />
)} )}
<div className="flex flex-col gap-5 pb-6 pt-2.5">
<div className="flex items-center justify-start gap-1"> <div className="flex items-center justify-start gap-1">
<div className="flex w-2/5 items-center justify-start gap-2 text-custom-text-300"> <div className="flex w-2/5 items-center justify-start gap-2 text-custom-text-300">
<CalendarClock className="h-4 w-4" /> <CalendarClock className="h-4 w-4" />
<span className="text-base">{t("date_range")}</span> <span className="text-base">{t("date_range")}</span>
</div> </div>
<div className="h-7 w-3/5"> <div className="h-7">
<Controller <Controller
control={control} control={control}
name="start_date" name="start_date"
@ -442,7 +443,7 @@ export const ModuleAnalyticsSidebar: React.FC<Props> = observer((props) => {
}} }}
placeholder={{ placeholder={{
from: t("start_date"), from: t("start_date"),
to: t("target_date"), to: t("end_date"),
}} }}
disabled={!isEditingAllowed || isArchived} disabled={!isEditingAllowed || isArchived}
/> />
@ -453,8 +454,6 @@ export const ModuleAnalyticsSidebar: React.FC<Props> = observer((props) => {
/> />
</div> </div>
</div> </div>
<div className="flex flex-col gap-5 pb-6 pt-2.5">
<div className="flex items-center justify-start gap-1"> <div className="flex items-center justify-start gap-1">
<div className="flex w-2/5 items-center justify-start gap-2 text-custom-text-300"> <div className="flex w-2/5 items-center justify-start gap-2 text-custom-text-300">
<SquareUser className="h-4 w-4" /> <SquareUser className="h-4 w-4" />

View file

@ -6,7 +6,13 @@ import { useParams } from "next/navigation";
// icons // icons
import { SquareUser } from "lucide-react"; import { SquareUser } from "lucide-react";
// types // types
import { MODULE_STATUS, MODULE_FAVORITED, MODULE_UNFAVORITED , EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; import {
MODULE_STATUS,
MODULE_FAVORITED,
MODULE_UNFAVORITED,
EUserPermissions,
EUserPermissionsLevel,
} from "@plane/constants";
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
import { IModule } from "@plane/types"; import { IModule } from "@plane/types";
// ui // ui

View file

@ -179,7 +179,9 @@ export class CycleStore implements ICycleStore {
const endDate = getDate(c.end_date); const endDate = getDate(c.end_date);
const hasEndDatePassed = endDate && isPast(endDate); const hasEndDatePassed = endDate && isPast(endDate);
const isEndDateToday = endDate && isToday(endDate); const isEndDateToday = endDate && isToday(endDate);
return c.project_id === projectId && hasEndDatePassed && !isEndDateToday && !c?.archived_at; return (
c.project_id === projectId && ((hasEndDatePassed && !isEndDateToday) || c.status?.toLowerCase() === "completed")
);
}); });
completedCycles = sortBy(completedCycles, [(c) => c.sort_order]); completedCycles = sortBy(completedCycles, [(c) => c.sort_order]);
const completedCycleIds = completedCycles.map((c) => c.id); const completedCycleIds = completedCycles.map((c) => c.id);
@ -195,7 +197,9 @@ export class CycleStore implements ICycleStore {
let incompleteCycles = Object.values(this.cycleMap ?? {}).filter((c) => { let incompleteCycles = Object.values(this.cycleMap ?? {}).filter((c) => {
const endDate = getDate(c.end_date); const endDate = getDate(c.end_date);
const hasEndDatePassed = endDate && isPast(endDate); const hasEndDatePassed = endDate && isPast(endDate);
return c.project_id === projectId && !hasEndDatePassed && !c?.archived_at; return (
c.project_id === projectId && !hasEndDatePassed && !c?.archived_at && c.status?.toLowerCase() !== "completed"
);
}); });
incompleteCycles = sortBy(incompleteCycles, [(c) => c.sort_order]); incompleteCycles = sortBy(incompleteCycles, [(c) => c.sort_order]);
const incompleteCycleIds = incompleteCycles.map((c) => c.id); const incompleteCycleIds = incompleteCycles.map((c) => c.id);

View file

@ -51,6 +51,7 @@ export interface IIssueAttachmentStore extends IIssueAttachmentStoreActions {
getAttachmentsUploadStatusByIssueId: (issueId: string) => TAttachmentUploadStatus[] | undefined; getAttachmentsUploadStatusByIssueId: (issueId: string) => TAttachmentUploadStatus[] | undefined;
getAttachmentsByIssueId: (issueId: string) => string[] | undefined; getAttachmentsByIssueId: (issueId: string) => string[] | undefined;
getAttachmentById: (attachmentId: string) => TIssueAttachment | undefined; getAttachmentById: (attachmentId: string) => TIssueAttachment | undefined;
getAttachmentsCountByIssueId: (issueId: string) => number;
} }
export class IssueAttachmentStore implements IIssueAttachmentStore { export class IssueAttachmentStore implements IIssueAttachmentStore {
@ -109,6 +110,11 @@ export class IssueAttachmentStore implements IIssueAttachmentStore {
return this.attachmentMap[attachmentId] ?? undefined; return this.attachmentMap[attachmentId] ?? undefined;
}; };
getAttachmentsCountByIssueId = (issueId: string) => {
const attachments = this.getAttachmentsByIssueId(issueId);
return attachments?.length ?? 0;
};
// actions // actions
addAttachments = (issueId: string, attachments: TIssueAttachment[]) => { addAttachments = (issueId: string, attachments: TIssueAttachment[]) => {
if (attachments && attachments.length > 0) { if (attachments && attachments.length > 0) {
@ -155,12 +161,14 @@ export class IssueAttachmentStore implements IIssueAttachmentStore {
this.debouncedUpdateProgress(issueId, tempId, progressPercentage); this.debouncedUpdateProgress(issueId, tempId, progressPercentage);
} }
); );
const issueAttachmentsCount = this.getAttachmentsByIssueId(issueId)?.length ?? 0;
if (response && response.id) { if (response && response.id) {
runInAction(() => { runInAction(() => {
update(this.attachments, [issueId], (attachmentIds = []) => uniq(concat(attachmentIds, [response.id]))); update(this.attachments, [issueId], (attachmentIds = []) => uniq(concat(attachmentIds, [response.id])));
set(this.attachmentMap, response.id, response); set(this.attachmentMap, response.id, response);
this.rootIssueStore.issues.updateIssue(issueId, {
attachment_count: this.getAttachmentsCountByIssueId(issueId),
});
}); });
} }
@ -182,7 +190,6 @@ export class IssueAttachmentStore implements IIssueAttachmentStore {
issueId, issueId,
attachmentId attachmentId
); );
const issueAttachmentsCount = this.getAttachmentsByIssueId(issueId)?.length ?? 1;
runInAction(() => { runInAction(() => {
update(this.attachments, [issueId], (attachmentIds = []) => { update(this.attachments, [issueId], (attachmentIds = []) => {
@ -191,7 +198,7 @@ export class IssueAttachmentStore implements IIssueAttachmentStore {
}); });
delete this.attachmentMap[attachmentId]; delete this.attachmentMap[attachmentId];
this.rootIssueStore.issues.updateIssue(issueId, { this.rootIssueStore.issues.updateIssue(issueId, {
attachment_count: issueAttachmentsCount - 1, // decrement attachment count attachment_count: this.getAttachmentsCountByIssueId(issueId),
}); });
}); });

View file

@ -425,7 +425,6 @@ export class ModulesStore implements IModuleStore {
set(this.moduleMap, [moduleId], { ...originalModuleDetails, ...data }); set(this.moduleMap, [moduleId], { ...originalModuleDetails, ...data });
}); });
const response = await this.moduleService.patchModule(workspaceSlug, projectId, moduleId, data); const response = await this.moduleService.patchModule(workspaceSlug, projectId, moduleId, data);
this.fetchModuleDetails(workspaceSlug, projectId, moduleId);
return response; return response;
} catch (error) { } catch (error) {
console.error("Failed to update module in module store", error); console.error("Failed to update module in module store", error);

View file

@ -1,7 +1,8 @@
{ {
"name": "web", "name": "web",
"version": "0.25.0", "version": "0.25.1",
"private": true, "private": true,
"license": "AGPL-3.0",
"scripts": { "scripts": {
"dev": "turbo run develop", "dev": "turbo run develop",
"develop": "next dev --port 3000", "develop": "next dev --port 3000",

View file

@ -10381,11 +10381,6 @@ react-confetti@^6.1.0:
dependencies: dependencies:
tween-functions "^1.2.0" tween-functions "^1.2.0"
react-day-picker@8.10.1:
version "8.10.1"
resolved "https://registry.yarnpkg.com/react-day-picker/-/react-day-picker-8.10.1.tgz#4762ec298865919b93ec09ba69621580835b8e80"
integrity sha512-TMx7fNbhLk15eqcMt+7Z7S2KF7mfTId/XJDjKE8f+IUcFn0l08/kI4FiYTL/0yuOLmEcbR4Fwe3GJf/NiiMnPA==
react-day-picker@9.5.0: react-day-picker@9.5.0:
version "9.5.0" version "9.5.0"
resolved "https://registry.yarnpkg.com/react-day-picker/-/react-day-picker-9.5.0.tgz#2ae36e85d6506026d72e350f49b5607d011cfd6f" resolved "https://registry.yarnpkg.com/react-day-picker/-/react-day-picker-9.5.0.tgz#2ae36e85d6506026d72e350f49b5607d011cfd6f"