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:
commit
e61ff879c4
71 changed files with 5094 additions and 1263 deletions
4
.github/workflows/build-aio-branch.yml
vendored
4
.github/workflows/build-aio-branch.yml
vendored
|
|
@ -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
|
||||||
|
|
|
||||||
29
.github/workflows/build-branch.yml
vendored
29
.github/workflows/build-branch.yml
vendored
|
|
@ -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
|
||||||
|
|
|
||||||
2
.github/workflows/feature-deployment.yml
vendored
2
.github/workflows/feature-deployment.yml
vendored
|
|
@ -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
|
||||||
|
|
|
||||||
2
.github/workflows/sync-repo.yml
vendored
2
.github/workflows/sync-repo.yml
vendored
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
||||||
|
|
|
||||||
|
|
@ -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"]
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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",
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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",)
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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"
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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"];
|
||||||
|
|
|
||||||
|
|
@ -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: {
|
||||||
|
|
|
||||||
|
|
@ -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: {
|
||||||
|
|
|
||||||
|
|
@ -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: {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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 */
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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";
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
2363
packages/i18n/src/locales/it/translations.json
Normal file
2363
packages/i18n/src/locales/it/translations.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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": "ワークスペース",
|
||||||
|
|
|
||||||
2365
packages/i18n/src/locales/ru/translations.json
Normal file
2365
packages/i18n/src/locales/ru/translations.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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": "工作区",
|
||||||
|
|
|
||||||
|
|
@ -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}`);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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": {
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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/**"
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
/>
|
}}
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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}
|
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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" />
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue