mirror of
https://github.com/makeplane/plane
synced 2025-08-07 19:59:33 +00:00
Compare commits
8 Commits
chore-issu
...
chore/issu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5277788be4 | ||
|
|
89588d4451 | ||
|
|
3eb911837c | ||
|
|
4b50b27a74 | ||
|
|
f44db89f41 | ||
|
|
8c3189e1be | ||
|
|
eee2145734 | ||
|
|
106710f3d0 |
@@ -71,6 +71,8 @@ from .issue import (
|
||||
IssueReactionLiteSerializer,
|
||||
IssueAttachmentLiteSerializer,
|
||||
IssueLinkLiteSerializer,
|
||||
IssueDescriptionVersionSerializer,
|
||||
IssueDescriptionVersionDetailSerializer,
|
||||
)
|
||||
|
||||
from .module import (
|
||||
|
||||
@@ -33,6 +33,7 @@ from plane.db.models import (
|
||||
IssueVote,
|
||||
IssueRelation,
|
||||
State,
|
||||
IssueDescriptionVersion
|
||||
)
|
||||
|
||||
|
||||
@@ -781,3 +782,46 @@ class IssueSubscriberSerializer(BaseSerializer):
|
||||
"project",
|
||||
"issue",
|
||||
]
|
||||
|
||||
|
||||
class IssueDescriptionVersionSerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = IssueDescriptionVersion
|
||||
fields = [
|
||||
"id",
|
||||
"workspace",
|
||||
"issue",
|
||||
"last_saved_at",
|
||||
"owned_by",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
]
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
"issue",
|
||||
]
|
||||
|
||||
|
||||
class IssueDescriptionVersionDetailSerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = IssueDescriptionVersion
|
||||
fields = [
|
||||
"id",
|
||||
"workspace",
|
||||
"issue",
|
||||
"last_saved_at",
|
||||
"description_binary",
|
||||
"description_html",
|
||||
"description_json",
|
||||
"owned_by",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
]
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
"issue",
|
||||
]
|
||||
|
||||
@@ -24,6 +24,7 @@ from plane.app.views import (
|
||||
IssueDetailEndpoint,
|
||||
IssueAttachmentV2Endpoint,
|
||||
IssueBulkUpdateDateEndpoint,
|
||||
IssueVersionEndpoint,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
@@ -320,4 +321,16 @@ urlpatterns = [
|
||||
IssueBulkUpdateDateEndpoint.as_view(),
|
||||
name="project-issue-dates",
|
||||
),
|
||||
# Issue Description versions
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/versions/",
|
||||
IssueVersionEndpoint.as_view(),
|
||||
name="issue-versions",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/versions/<uuid:pk>/",
|
||||
IssueVersionEndpoint.as_view(),
|
||||
name="issue-versions",
|
||||
),
|
||||
## End Issue Description versions
|
||||
]
|
||||
|
||||
@@ -138,6 +138,8 @@ from .issue.activity import (
|
||||
IssueActivityEndpoint,
|
||||
)
|
||||
|
||||
from .issue.version import IssueVersionEndpoint
|
||||
|
||||
from .issue.archive import IssueArchiveViewSet, BulkArchiveIssuesEndpoint
|
||||
|
||||
from .issue.attachment import (
|
||||
|
||||
39
apiserver/plane/app/views/issue/version.py
Normal file
39
apiserver/plane/app/views/issue/version.py
Normal file
@@ -0,0 +1,39 @@
|
||||
# Third party imports
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
|
||||
# Module imports
|
||||
from plane.db.models import IssueDescriptionVersion
|
||||
from ..base import BaseAPIView
|
||||
from plane.app.serializers import (
|
||||
IssueDescriptionVersionSerializer,
|
||||
IssueDescriptionVersionDetailSerializer,
|
||||
)
|
||||
from plane.app.permissions import allow_permission, ROLE
|
||||
|
||||
|
||||
class IssueVersionEndpoint(BaseAPIView):
|
||||
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
def get(self, request, slug, project_id, issue_id, pk=None):
|
||||
# Check if pk is provided
|
||||
if pk:
|
||||
# Return a single issue version
|
||||
issue_version = IssueDescriptionVersion.objects.get(
|
||||
workspace__slug=slug,
|
||||
issue_id=issue_id,
|
||||
pk=pk,
|
||||
)
|
||||
# Serialize the issue version
|
||||
serializer = IssueDescriptionVersionDetailSerializer(issue_version)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
# Return all issue versions
|
||||
issue_versions = IssueDescriptionVersion.objects.filter(
|
||||
workspace__slug=slug,
|
||||
issue_id=issue_id,
|
||||
).order_by("-last_saved_at")[:20]
|
||||
# Serialize the issue versions
|
||||
serializer = IssueDescriptionVersionSerializer(
|
||||
issue_versions, many=True
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
@@ -37,7 +37,7 @@ from plane.db.models import (
|
||||
from plane.utils.error_codes import ERROR_CODES
|
||||
from ..base import BaseAPIView, BaseViewSet
|
||||
from plane.bgtasks.page_transaction_task import page_transaction
|
||||
from plane.bgtasks.page_version_task import page_version
|
||||
from plane.bgtasks.version_task import version_task
|
||||
from plane.bgtasks.recent_visited_task import recent_visited_task
|
||||
|
||||
|
||||
@@ -621,8 +621,9 @@ class PagesDescriptionViewSet(BaseViewSet):
|
||||
page.description = request.data.get("description")
|
||||
page.save()
|
||||
# Return a success response
|
||||
page_version.delay(
|
||||
page_id=page.id,
|
||||
version_task.delay(
|
||||
entity_type="PAGE",
|
||||
entity_identifier=page.id,
|
||||
existing_instance=existing_instance,
|
||||
user_id=request.user.id,
|
||||
)
|
||||
|
||||
@@ -14,9 +14,7 @@ from plane.app.permissions import allow_permission, ROLE
|
||||
|
||||
class PageVersionEndpoint(BaseAPIView):
|
||||
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]
|
||||
)
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
def get(self, request, slug, project_id, page_id, pk=None):
|
||||
# Check if pk is provided
|
||||
if pk:
|
||||
@@ -33,7 +31,7 @@ class PageVersionEndpoint(BaseAPIView):
|
||||
page_versions = PageVersion.objects.filter(
|
||||
workspace__slug=slug,
|
||||
page_id=page_id,
|
||||
)
|
||||
).order_by("-last_saved_at")[:20]
|
||||
# Serialize the page versions
|
||||
serializer = PageVersionSerializer(page_versions, many=True)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
# Python imports
|
||||
import json
|
||||
|
||||
# Third party imports
|
||||
from celery import shared_task
|
||||
|
||||
# Module imports
|
||||
from plane.db.models import Page, PageVersion
|
||||
from plane.utils.exception_logger import log_exception
|
||||
|
||||
|
||||
@shared_task
|
||||
def page_version(
|
||||
page_id,
|
||||
existing_instance,
|
||||
user_id,
|
||||
):
|
||||
try:
|
||||
# Get the page
|
||||
page = Page.objects.get(id=page_id)
|
||||
|
||||
# Get the current instance
|
||||
current_instance = (
|
||||
json.loads(existing_instance)
|
||||
if existing_instance is not None
|
||||
else {}
|
||||
)
|
||||
|
||||
# Create a version if description_html is updated
|
||||
if current_instance.get("description_html") != page.description_html:
|
||||
# Create a new page version
|
||||
PageVersion.objects.create(
|
||||
page_id=page_id,
|
||||
workspace_id=page.workspace_id,
|
||||
description_html=page.description_html,
|
||||
description_binary=page.description_binary,
|
||||
owned_by_id=user_id,
|
||||
last_saved_at=page.updated_at,
|
||||
)
|
||||
|
||||
# If page versions are greater than 20 delete the oldest one
|
||||
if PageVersion.objects.filter(page_id=page_id).count() > 20:
|
||||
# Delete the old page version
|
||||
PageVersion.objects.filter(page_id=page_id).order_by(
|
||||
"last_saved_at"
|
||||
).first().delete()
|
||||
|
||||
return
|
||||
except Page.DoesNotExist:
|
||||
return
|
||||
except Exception as e:
|
||||
log_exception(e)
|
||||
return
|
||||
148
apiserver/plane/bgtasks/version_task.py
Normal file
148
apiserver/plane/bgtasks/version_task.py
Normal file
@@ -0,0 +1,148 @@
|
||||
# Python imports
|
||||
import json
|
||||
|
||||
# Django imports
|
||||
from django.utils import timezone
|
||||
|
||||
# Third party imports
|
||||
from celery import shared_task
|
||||
|
||||
# Module imports
|
||||
from plane.db.models import Page, PageVersion, Issue, IssueDescriptionVersion
|
||||
from plane.utils.exception_logger import log_exception
|
||||
|
||||
|
||||
@shared_task
|
||||
def version_task(
|
||||
entity_type,
|
||||
entity_identifier,
|
||||
existing_instance,
|
||||
user_id,
|
||||
):
|
||||
try:
|
||||
|
||||
# Get the current instance
|
||||
current_instance = (
|
||||
json.loads(existing_instance)
|
||||
if existing_instance is not None
|
||||
else {}
|
||||
)
|
||||
|
||||
if entity_type == "PAGE":
|
||||
# Get the page
|
||||
page = Page.objects.get(id=entity_identifier)
|
||||
|
||||
# Create a version if description_html is updated
|
||||
if (
|
||||
current_instance.get("description_html")
|
||||
!= page.description_html
|
||||
):
|
||||
# Fetch the latest page version
|
||||
page_version = (
|
||||
PageVersion.objects.filter(page_id=entity_identifier)
|
||||
.order_by("-last_saved_at")
|
||||
.first()
|
||||
)
|
||||
|
||||
# Get the latest page version if it exists and is owned by the user
|
||||
if (
|
||||
page_version
|
||||
and str(page_version.owned_by_id) == str(user_id)
|
||||
and (
|
||||
timezone.now() - page_version.last_saved_at
|
||||
).total_seconds()
|
||||
<= 600
|
||||
):
|
||||
page_version.description_html = page.description_html
|
||||
page_version.description_binary = page.description_binary
|
||||
page_version.description_json = page.description
|
||||
page_version.description_stripped = (
|
||||
page.description_stripped
|
||||
)
|
||||
page_version.last_saved_at = timezone.now()
|
||||
page_version.save(
|
||||
update_fields=[
|
||||
"description_html",
|
||||
"description_binary",
|
||||
"description_json",
|
||||
"description_stripped",
|
||||
"last_saved_at",
|
||||
]
|
||||
)
|
||||
else:
|
||||
# Create a new page version
|
||||
PageVersion.objects.create(
|
||||
page_id=entity_identifier,
|
||||
workspace_id=page.workspace_id,
|
||||
description_html=page.description_html,
|
||||
description_binary=page.description_binary,
|
||||
description_stripped=page.description_stripped,
|
||||
owned_by_id=user_id,
|
||||
last_saved_at=page.updated_at,
|
||||
description_json=page.description,
|
||||
)
|
||||
|
||||
if entity_type == "ISSUE":
|
||||
# Get the issue
|
||||
issue = Issue.objects.get(id=entity_identifier)
|
||||
|
||||
# Create a version if description_html is updated
|
||||
if (
|
||||
current_instance.get("description_html")
|
||||
!= issue.description_html
|
||||
):
|
||||
# Fetch the latest issue version
|
||||
issue_version = (
|
||||
IssueDescriptionVersion.objects.filter(
|
||||
issue_id=entity_identifier
|
||||
)
|
||||
.order_by("-last_saved_at")
|
||||
.first()
|
||||
)
|
||||
|
||||
# Get the latest issue version if it exists and is owned by the user
|
||||
if (
|
||||
issue_version
|
||||
and str(issue_version.owned_by_id) == str(user_id)
|
||||
and (
|
||||
timezone.now() - issue_version.last_saved_at
|
||||
).total_seconds()
|
||||
<= 600
|
||||
):
|
||||
issue_version.description_html = issue.description_html
|
||||
issue_version.description_binary = issue.description_binary
|
||||
issue_version.description_json = issue.description
|
||||
issue_version.description_stripped = (
|
||||
issue.description_stripped
|
||||
)
|
||||
issue_version.last_saved_at = timezone.now()
|
||||
issue_version.save(
|
||||
update_fields=[
|
||||
"description_html",
|
||||
"description_binary",
|
||||
"description_json",
|
||||
"description_stripped",
|
||||
"last_saved_at",
|
||||
]
|
||||
)
|
||||
else:
|
||||
# Create a new issue version
|
||||
IssueDescriptionVersion.objects.create(
|
||||
issue_id=entity_identifier,
|
||||
workspace_id=issue.workspace_id,
|
||||
description_html=issue.description_html,
|
||||
description_binary=issue.description_binary,
|
||||
description_stripped=issue.description_stripped,
|
||||
owned_by_id=user_id,
|
||||
last_saved_at=issue.updated_at,
|
||||
description_json=issue.description,
|
||||
)
|
||||
|
||||
return
|
||||
except Issue.DoesNotExist:
|
||||
return
|
||||
except Page.DoesNotExist:
|
||||
return
|
||||
except Exception as e:
|
||||
log_exception(e)
|
||||
return
|
||||
126
apiserver/plane/db/migrations/0087_issuedescriptionversion.py
Normal file
126
apiserver/plane/db/migrations/0087_issuedescriptionversion.py
Normal file
@@ -0,0 +1,126 @@
|
||||
# Generated by Django 4.2.15 on 2024-11-09 12:11
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("db", "0086_alter_teammember_unique_together_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="IssueDescriptionVersion",
|
||||
fields=[
|
||||
(
|
||||
"created_at",
|
||||
models.DateTimeField(
|
||||
auto_now_add=True, verbose_name="Created At"
|
||||
),
|
||||
),
|
||||
(
|
||||
"updated_at",
|
||||
models.DateTimeField(
|
||||
auto_now=True, verbose_name="Last Modified At"
|
||||
),
|
||||
),
|
||||
(
|
||||
"deleted_at",
|
||||
models.DateTimeField(
|
||||
blank=True, null=True, verbose_name="Deleted At"
|
||||
),
|
||||
),
|
||||
(
|
||||
"id",
|
||||
models.UUIDField(
|
||||
db_index=True,
|
||||
default=uuid.uuid4,
|
||||
editable=False,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
unique=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"last_saved_at",
|
||||
models.DateTimeField(default=django.utils.timezone.now),
|
||||
),
|
||||
("description_binary", models.BinaryField(null=True)),
|
||||
(
|
||||
"description_html",
|
||||
models.TextField(blank=True, default="<p></p>"),
|
||||
),
|
||||
(
|
||||
"description_stripped",
|
||||
models.TextField(blank=True, null=True),
|
||||
),
|
||||
(
|
||||
"description_json",
|
||||
models.JSONField(blank=True, default=dict),
|
||||
),
|
||||
(
|
||||
"created_by",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="%(class)s_created_by",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
verbose_name="Created By",
|
||||
),
|
||||
),
|
||||
(
|
||||
"issue",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="issue_description_versions",
|
||||
to="db.issue",
|
||||
),
|
||||
),
|
||||
(
|
||||
"owned_by",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="issue_versions",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
(
|
||||
"project",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="project_%(class)s",
|
||||
to="db.project",
|
||||
),
|
||||
),
|
||||
(
|
||||
"updated_by",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="%(class)s_updated_by",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
verbose_name="Last Modified By",
|
||||
),
|
||||
),
|
||||
(
|
||||
"workspace",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="workspace_%(class)s",
|
||||
to="db.workspace",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Issue Description Version",
|
||||
"verbose_name_plural": "Issue Description Versions",
|
||||
"db_table": "issue_description_versions",
|
||||
"ordering": ("-created_at",),
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -41,6 +41,7 @@ from .issue import (
|
||||
IssueSequence,
|
||||
IssueSubscriber,
|
||||
IssueVote,
|
||||
IssueDescriptionVersion
|
||||
)
|
||||
from .module import (
|
||||
Module,
|
||||
|
||||
@@ -274,6 +274,39 @@ class IssueBlocker(ProjectBaseModel):
|
||||
return f"{self.block.name} {self.blocked_by.name}"
|
||||
|
||||
|
||||
class IssueDescriptionVersion(ProjectBaseModel):
|
||||
issue = models.ForeignKey(
|
||||
"db.Issue",
|
||||
on_delete=models.CASCADE,
|
||||
related_name="issue_description_versions",
|
||||
)
|
||||
last_saved_at = models.DateTimeField(default=timezone.now)
|
||||
owned_by = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="issue_versions",
|
||||
)
|
||||
description_binary = models.BinaryField(null=True)
|
||||
description_html = models.TextField(blank=True, default="<p></p>")
|
||||
description_stripped = models.TextField(blank=True, null=True)
|
||||
description_json = models.JSONField(default=dict, blank=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Issue Description Version"
|
||||
verbose_name_plural = "Issue Description Versions"
|
||||
db_table = "issue_description_versions"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# Strip the html tags using html parser
|
||||
self.description_stripped = (
|
||||
None
|
||||
if (self.description_html == "" or self.description_html is None)
|
||||
else strip_tags(self.description_html)
|
||||
)
|
||||
super(IssueDescriptionVersion, self).save(*args, **kwargs)
|
||||
|
||||
|
||||
class IssueRelation(ProjectBaseModel):
|
||||
RELATION_CHOICES = (
|
||||
("duplicate", "Duplicate"),
|
||||
|
||||
@@ -27,10 +27,18 @@
|
||||
}
|
||||
}
|
||||
|
||||
.table-wrapper table th {
|
||||
font-weight: 500;
|
||||
text-align: left;
|
||||
background-color: rgba(var(--color-background-90));
|
||||
.table-wrapper table {
|
||||
th {
|
||||
font-weight: 500;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
tr[background="none"],
|
||||
tr:not([background]) {
|
||||
th {
|
||||
background-color: rgba(var(--color-background-90));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table-wrapper table .selectedCell {
|
||||
|
||||
4
packages/types/src/view-props.d.ts
vendored
4
packages/types/src/view-props.d.ts
vendored
@@ -40,8 +40,8 @@ export type TIssueOrderByOptions =
|
||||
| "-issue_cycle__cycle__name"
|
||||
| "target_date"
|
||||
| "-target_date"
|
||||
| "estimate_point"
|
||||
| "-estimate_point"
|
||||
| "estimate_point__key"
|
||||
| "-estimate_point__key"
|
||||
| "start_date"
|
||||
| "-start_date"
|
||||
| "link_count"
|
||||
|
||||
1
web/ce/components/instance/index.ts
Normal file
1
web/ce/components/instance/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./maintenance-message";
|
||||
6
web/ce/components/instance/maintenance-message.tsx
Normal file
6
web/ce/components/instance/maintenance-message.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
export const MaintenanceMessage = () => (
|
||||
<h1 className="text-xl font-medium text-custom-text-100 text-center md:text-left">
|
||||
Plane didn't start up. This could be because one or more Plane services failed to start. <br /> Choose View
|
||||
Logs from setup.sh and Docker logs to be sure.
|
||||
</h1>
|
||||
);
|
||||
@@ -1,5 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { FC, Fragment } from "react";
|
||||
|
||||
export const MaintenanceMode: FC = () => <Fragment />;
|
||||
@@ -5,7 +5,11 @@ import { computedFn } from "mobx-utils";
|
||||
// components
|
||||
import { ChartDataType, IBlockUpdateDependencyData, IGanttBlock, TGanttViews } from "@/components/gantt-chart";
|
||||
import { currentViewDataWithView } from "@/components/gantt-chart/data";
|
||||
import { getDateFromPositionOnGantt, getItemPositionWidth } from "@/components/gantt-chart/views/helpers";
|
||||
import {
|
||||
getDateFromPositionOnGantt,
|
||||
getItemPositionWidth,
|
||||
getPositionFromDate,
|
||||
} from "@/components/gantt-chart/views/helpers";
|
||||
// helpers
|
||||
import { renderFormattedPayloadDate } from "@/helpers/date-time.helper";
|
||||
// store
|
||||
@@ -47,6 +51,7 @@ export interface IBaseTimelineStore {
|
||||
initGantt: () => void;
|
||||
|
||||
getDateFromPositionOnGantt: (position: number, offsetDays: number) => Date | undefined;
|
||||
getPositionFromDateOnGantt: (date: string | Date, offSetWidth: number) => number | undefined;
|
||||
}
|
||||
|
||||
export class BaseTimeLineStore implements IBaseTimelineStore {
|
||||
@@ -186,7 +191,7 @@ export class BaseTimeLineStore implements IBaseTimelineStore {
|
||||
start_date: blockData?.start_date ?? undefined,
|
||||
target_date: blockData?.target_date ?? undefined,
|
||||
};
|
||||
if (this.currentViewData && this.currentViewData?.data?.startDate && this.currentViewData?.data?.dayWidth) {
|
||||
if (this.currentViewData && (this.currentViewData?.data?.startDate || this.currentViewData?.data?.dayWidth)) {
|
||||
block.position = getItemPositionWidth(this.currentViewData, block);
|
||||
}
|
||||
|
||||
@@ -227,6 +232,15 @@ export class BaseTimeLineStore implements IBaseTimelineStore {
|
||||
return Math.round(position / this.currentViewData.data.dayWidth);
|
||||
};
|
||||
|
||||
/**
|
||||
* returns position of the date on chart
|
||||
*/
|
||||
getPositionFromDateOnGantt = computedFn((date: string | Date, offSetWidth: number) => {
|
||||
if (!this.currentViewData) return;
|
||||
|
||||
return getPositionFromDate(this.currentViewData, date, offSetWidth);
|
||||
});
|
||||
|
||||
/**
|
||||
* returns the date at which the position corresponds to on the timeline chart
|
||||
*/
|
||||
|
||||
@@ -68,7 +68,7 @@ export const BlockRow: React.FC<Props> = observer((props) => {
|
||||
// hide the block if it doesn't have start and target dates and showAllBlocks is false
|
||||
if (!block || !block.data || (!showAllBlocks && !(block.start_date && block.target_date))) return null;
|
||||
|
||||
const isBlockVisibleOnChart = block.start_date && block.target_date;
|
||||
const isBlockVisibleOnChart = block.start_date || block.target_date;
|
||||
const isBlockSelected = selectionHelpers.getIsEntitySelected(block.id);
|
||||
const isBlockFocused = selectionHelpers.getIsEntityActive(block.id);
|
||||
const isBlockHoveredOn = isBlockActive(block.id);
|
||||
|
||||
@@ -46,10 +46,11 @@ export const GanttChartBlock: React.FC<Props> = observer((props) => {
|
||||
|
||||
const { isMoving, handleBlockDrag } = useGanttResizable(block, resizableRef, ganttContainerRef, updateBlockDates);
|
||||
|
||||
// hide the block if it doesn't have start and target dates and showAllBlocks is false
|
||||
if (!block || (!showAllBlocks && !(block.start_date && block.target_date))) return null;
|
||||
const isBlockVisibleOnChart = block?.start_date || block?.target_date;
|
||||
const isBlockComplete = block?.start_date && block?.target_date;
|
||||
|
||||
const isBlockVisibleOnChart = block.start_date && block.target_date;
|
||||
// hide the block if it doesn't have start and target dates and showAllBlocks is false
|
||||
if (!block || (!showAllBlocks && !isBlockVisibleOnChart)) return null;
|
||||
|
||||
if (!block.data) return null;
|
||||
|
||||
@@ -63,7 +64,7 @@ export const GanttChartBlock: React.FC<Props> = observer((props) => {
|
||||
ref={resizableRef}
|
||||
style={{
|
||||
height: `${BLOCK_HEIGHT}px`,
|
||||
transform: `translateX(${block.position?.marginLeft}px)`,
|
||||
marginLeft: `${block.position?.marginLeft}px`,
|
||||
width: `${block.position?.width}px`,
|
||||
}}
|
||||
>
|
||||
@@ -88,7 +89,7 @@ export const GanttChartBlock: React.FC<Props> = observer((props) => {
|
||||
handleBlockDrag={handleBlockDrag}
|
||||
enableBlockLeftResize={enableBlockLeftResize}
|
||||
enableBlockRightResize={enableBlockRightResize}
|
||||
enableBlockMove={enableBlockMove}
|
||||
enableBlockMove={enableBlockMove && !!isBlockComplete}
|
||||
isMoving={isMoving}
|
||||
ganttContainerRef={ganttContainerRef}
|
||||
/>
|
||||
|
||||
@@ -28,7 +28,7 @@ import { IssueBulkOperationsRoot } from "@/plane-web/components/issues";
|
||||
import { useBulkOperationStatus } from "@/plane-web/hooks/use-bulk-operation-status";
|
||||
//
|
||||
import { GanttChartRowList } from "../blocks/block-row-list";
|
||||
import { GANTT_SELECT_GROUP, HEADER_HEIGHT } from "../constants";
|
||||
import { DEFAULT_BLOCK_WIDTH, GANTT_SELECT_GROUP, HEADER_HEIGHT } from "../constants";
|
||||
import { getItemPositionWidth } from "../views";
|
||||
import { TimelineDragHelper } from "./timeline-drag-helper";
|
||||
|
||||
@@ -108,14 +108,20 @@ export const GanttChartMainContent: React.FC<Props> = observer((props) => {
|
||||
|
||||
const approxRangeLeft = scrollLeft;
|
||||
const approxRangeRight = scrollWidth - (scrollLeft + clientWidth);
|
||||
const calculatedRangeRight = itemsContainerWidth - (scrollLeft + clientWidth);
|
||||
|
||||
if (approxRangeRight < clientWidth) updateCurrentViewRenderPayload("right", currentView);
|
||||
if (approxRangeLeft < clientWidth) updateCurrentViewRenderPayload("left", currentView);
|
||||
if (approxRangeRight < clientWidth || calculatedRangeRight < clientWidth) {
|
||||
updateCurrentViewRenderPayload("right", currentView);
|
||||
}
|
||||
if (approxRangeLeft < clientWidth) {
|
||||
updateCurrentViewRenderPayload("left", currentView);
|
||||
}
|
||||
};
|
||||
|
||||
const handleScrollToBlock = (block: IGanttBlock) => {
|
||||
const scrollContainer = ganttContainerRef.current as HTMLDivElement;
|
||||
const scrollToDate = getDate(block.start_date);
|
||||
const scrollToEndDate = !block.start_date && block.target_date;
|
||||
const scrollToDate = block.start_date ? getDate(block.start_date) : getDate(block.target_date);
|
||||
let chartData;
|
||||
|
||||
if (!scrollContainer || !currentViewData || !scrollToDate) return;
|
||||
@@ -129,7 +135,8 @@ export const GanttChartMainContent: React.FC<Props> = observer((props) => {
|
||||
const updatedPosition = getItemPositionWidth(chartData ?? currentViewData, block);
|
||||
|
||||
setTimeout(() => {
|
||||
if (updatedPosition) scrollContainer.scrollLeft = updatedPosition.marginLeft - 4;
|
||||
if (updatedPosition)
|
||||
scrollContainer.scrollLeft = updatedPosition.marginLeft - 4 - (scrollToEndDate ? DEFAULT_BLOCK_WIDTH : 0);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -189,6 +196,7 @@ export const GanttChartMainContent: React.FC<Props> = observer((props) => {
|
||||
style={{
|
||||
width: `${itemsContainerWidth}px`,
|
||||
transform: `translateY(${HEADER_HEIGHT}px)`,
|
||||
paddingBottom: `${HEADER_HEIGHT}px`,
|
||||
}}
|
||||
>
|
||||
<GanttChartRowList
|
||||
|
||||
@@ -25,7 +25,7 @@ export const MonthChartView: FC<any> = observer(() => {
|
||||
const marginLeftDays = getNumberOfDaysBetweenTwoDates(monthsStartDate, weeksStartDate);
|
||||
|
||||
return (
|
||||
<div className={`absolute top-0 left-0 h-max w-max flex`} style={{ minHeight: `calc(100% + ${HEADER_HEIGHT}px` }}>
|
||||
<div className={`absolute top-0 left-0 min-h-full h-max w-max flex`}>
|
||||
{currentViewData && (
|
||||
<div className="relative flex flex-col outline-[0.25px] outline outline-custom-border-200">
|
||||
{/** Header Div */}
|
||||
|
||||
@@ -15,7 +15,7 @@ export const QuarterChartView: FC<any> = observer(() => {
|
||||
const quarterBlocks: IQuarterMonthBlock[] = groupMonthsToQuarters(monthBlocks);
|
||||
|
||||
return (
|
||||
<div className={`absolute top-0 left-0 h-max w-max flex`} style={{ minHeight: `calc(100% + ${HEADER_HEIGHT}px` }}>
|
||||
<div className={`absolute top-0 left-0 min-h-full h-max w-max flex`}>
|
||||
{currentViewData &&
|
||||
quarterBlocks?.map((quarterBlock, rootIndex) => (
|
||||
<div
|
||||
|
||||
@@ -13,7 +13,7 @@ export const WeekChartView: FC<any> = observer(() => {
|
||||
const weekBlocks: IWeekBlock[] = renderView;
|
||||
|
||||
return (
|
||||
<div className={`absolute top-0 left-0 h-max w-max flex`} style={{ minHeight: `calc(100% + ${HEADER_HEIGHT}px` }}>
|
||||
<div className={`absolute top-0 left-0 min-h-full h-max w-max flex`}>
|
||||
{currentViewData &&
|
||||
weekBlocks?.map((block, rootIndex) => (
|
||||
<div
|
||||
|
||||
@@ -6,4 +6,6 @@ export const GANTT_BREADCRUMBS_HEIGHT = 40;
|
||||
|
||||
export const SIDEBAR_WIDTH = 360;
|
||||
|
||||
export const DEFAULT_BLOCK_WIDTH = 60;
|
||||
|
||||
export const GANTT_SELECT_GROUP = "gantt-issues";
|
||||
|
||||
@@ -71,7 +71,7 @@ export const useGanttResizable = (
|
||||
// calculate new marginLeft and update the initial marginLeft to the newly calculated one
|
||||
marginLeft = Math.round(mouseX / dayWidth) * dayWidth;
|
||||
// get Dimensions from dom's style
|
||||
const prevMarginLeft = parseFloat(resizableDiv.style.transform.slice(11, -3));
|
||||
const prevMarginLeft = parseFloat(resizableDiv.style.marginLeft.slice(0, -2));
|
||||
const prevWidth = parseFloat(resizableDiv.style.width.slice(0, -2));
|
||||
// calculate new width
|
||||
const marginDelta = prevMarginLeft - marginLeft;
|
||||
@@ -88,7 +88,7 @@ export const useGanttResizable = (
|
||||
if (width < dayWidth) return;
|
||||
|
||||
resizableDiv.style.width = `${width}px`;
|
||||
resizableDiv.style.transform = `translateX(${marginLeft}px)`;
|
||||
resizableDiv.style.marginLeft = `${marginLeft}px`;
|
||||
|
||||
const deltaLeft = Math.round((marginLeft - (block.position?.marginLeft ?? 0)) / dayWidth) * dayWidth;
|
||||
const deltaWidth = Math.round((width - (block.position?.width ?? 0)) / dayWidth) * dayWidth;
|
||||
|
||||
@@ -34,7 +34,7 @@ export const GanttDnDHOC = observer((props: Props) => {
|
||||
draggable({
|
||||
element,
|
||||
canDrag: () => isDragEnabled,
|
||||
getInitialData: () => ({ id }),
|
||||
getInitialData: () => ({ id, dragInstanceId: "GANTT_REORDER" }),
|
||||
onDragStart: () => {
|
||||
setIsDragging(true);
|
||||
},
|
||||
@@ -44,7 +44,7 @@ export const GanttDnDHOC = observer((props: Props) => {
|
||||
}),
|
||||
dropTargetForElements({
|
||||
element,
|
||||
canDrop: ({ source }) => source?.data?.id !== id,
|
||||
canDrop: ({ source }) => source?.data?.id !== id && source?.data?.dragInstanceId === "GANTT_REORDER",
|
||||
getData: ({ input, element }) => {
|
||||
const data = { id };
|
||||
|
||||
|
||||
@@ -27,8 +27,8 @@ export const IssuesSidebarBlock = observer((props: Props) => {
|
||||
const { updateActiveBlockId, isBlockActive, getNumberOfDaysFromPosition } = useTimeLineChartStore();
|
||||
const { getIsIssuePeeked } = useIssueDetail();
|
||||
|
||||
const isBlockVisibleOnChart = !!block?.start_date && !!block?.target_date;
|
||||
const duration = isBlockVisibleOnChart ? getNumberOfDaysFromPosition(block?.position?.width) : undefined;
|
||||
const isBlockComplete = !!block?.start_date && !!block?.target_date;
|
||||
const duration = isBlockComplete ? getNumberOfDaysFromPosition(block?.position?.width) : undefined;
|
||||
|
||||
if (!block?.data) return null;
|
||||
|
||||
|
||||
@@ -22,8 +22,8 @@ export const ModulesSidebarBlock: React.FC<Props> = observer((props) => {
|
||||
|
||||
if (!block) return <></>;
|
||||
|
||||
const isBlockVisibleOnChart = !!block.start_date && !!block.target_date;
|
||||
const duration = isBlockVisibleOnChart ? getNumberOfDaysFromPosition(block?.position?.width) : undefined;
|
||||
const isBlockComplete = !!block.start_date && !!block.target_date;
|
||||
const duration = isBlockComplete ? getNumberOfDaysFromPosition(block?.position?.width) : undefined;
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { addDaysToDate, findTotalDaysInRange, getDate } from "@/helpers/date-time.helper";
|
||||
import { DEFAULT_BLOCK_WIDTH } from "../constants";
|
||||
import { ChartDataType, IGanttBlock } from "../types";
|
||||
import { IMonthBlock, IMonthView, monthView } from "./month-view";
|
||||
import { quarterView } from "./quarter-view";
|
||||
import { IWeekBlock, weekView } from "./week-view";
|
||||
|
||||
/**
|
||||
* Generates Date by using Day, month and Year
|
||||
@@ -84,7 +82,7 @@ export const getDateFromPositionOnGantt = (position: number, chartData: ChartDat
|
||||
*/
|
||||
export const getItemPositionWidth = (chartData: ChartDataType, itemData: IGanttBlock) => {
|
||||
let scrollPosition: number = 0;
|
||||
let scrollWidth: number = 0;
|
||||
let scrollWidth: number = DEFAULT_BLOCK_WIDTH;
|
||||
|
||||
const { startDate: chartStartDate } = chartData.data;
|
||||
const { start_date, target_date } = itemData;
|
||||
@@ -92,24 +90,42 @@ export const getItemPositionWidth = (chartData: ChartDataType, itemData: IGanttB
|
||||
const itemStartDate = getDate(start_date);
|
||||
const itemTargetDate = getDate(target_date);
|
||||
|
||||
if (!itemStartDate || !itemTargetDate) return;
|
||||
|
||||
chartStartDate.setHours(0, 0, 0, 0);
|
||||
itemStartDate.setHours(0, 0, 0, 0);
|
||||
itemTargetDate.setHours(0, 0, 0, 0);
|
||||
itemStartDate?.setHours(0, 0, 0, 0);
|
||||
itemTargetDate?.setHours(0, 0, 0, 0);
|
||||
|
||||
// get number of days from chart start date to block's start date
|
||||
const positionDaysDifference = Math.round(findTotalDaysInRange(chartStartDate, itemStartDate, false) ?? 0);
|
||||
|
||||
if (!positionDaysDifference) return;
|
||||
if (!itemStartDate && !itemTargetDate) return;
|
||||
|
||||
// get scroll position from the number of days and width of each day
|
||||
scrollPosition = positionDaysDifference * chartData.data.dayWidth;
|
||||
scrollPosition = itemStartDate
|
||||
? getPositionFromDate(chartData, itemStartDate, 0)
|
||||
: getPositionFromDate(chartData, itemTargetDate!, -1 * DEFAULT_BLOCK_WIDTH);
|
||||
|
||||
// get width of block
|
||||
const widthTimeDifference: number = itemStartDate.getTime() - itemTargetDate.getTime();
|
||||
const widthDaysDifference: number = Math.abs(Math.floor(widthTimeDifference / (1000 * 60 * 60 * 24)));
|
||||
scrollWidth = (widthDaysDifference + 1) * chartData.data.dayWidth;
|
||||
if (itemStartDate && itemTargetDate) {
|
||||
// get width of block
|
||||
const widthTimeDifference: number = itemStartDate.getTime() - itemTargetDate.getTime();
|
||||
const widthDaysDifference: number = Math.abs(Math.floor(widthTimeDifference / (1000 * 60 * 60 * 24)));
|
||||
scrollWidth = (widthDaysDifference + 1) * chartData.data.dayWidth;
|
||||
}
|
||||
|
||||
return { marginLeft: scrollPosition, width: scrollWidth };
|
||||
};
|
||||
|
||||
export const getPositionFromDate = (chartData: ChartDataType, date: string | Date, offsetWidth: number) => {
|
||||
const currDate = getDate(date);
|
||||
|
||||
const { startDate: chartStartDate } = chartData.data;
|
||||
|
||||
if (!currDate || !chartStartDate) return 0;
|
||||
|
||||
chartStartDate.setHours(0, 0, 0, 0);
|
||||
currDate.setHours(0, 0, 0, 0);
|
||||
|
||||
// get number of days from chart start date to block's start date
|
||||
const positionDaysDifference = Math.round(findTotalDaysInRange(chartStartDate, currDate, false) ?? 0);
|
||||
|
||||
if (!positionDaysDifference) return 0;
|
||||
|
||||
// get scroll position from the number of days and width of each day
|
||||
return positionDaysDifference * chartData.data.dayWidth + offsetWidth;
|
||||
};
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export * from "./not-ready-view";
|
||||
export * from "./maintenance-view";
|
||||
|
||||
34
web/core/components/instance/maintenance-view.tsx
Normal file
34
web/core/components/instance/maintenance-view.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
"use client";
|
||||
|
||||
import { FC } from "react";
|
||||
import Image from "next/image";
|
||||
// ui
|
||||
import { Button } from "@plane/ui";
|
||||
// layouts
|
||||
import DefaultLayout from "@/layouts/default-layout";
|
||||
// components
|
||||
import { MaintenanceMessage } from "@/plane-web/components/instance";
|
||||
// images
|
||||
import maintenanceModeImage from "@/public/maintenance-mode.webp";
|
||||
|
||||
export const MaintenanceView: FC = () => (
|
||||
<DefaultLayout>
|
||||
<div className="relative container mx-auto h-full w-full flex flex-col md:flex-row gap-2 items-center justify-center gap-y-5 bg-custom-background-100 text-center">
|
||||
<div className="relative w-full">
|
||||
<Image
|
||||
src={maintenanceModeImage}
|
||||
height="176"
|
||||
width="288"
|
||||
alt="ProjectSettingImg"
|
||||
className="w-full h-full object-fill object-center"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full space-y-4 relative flex flex-col justify-center md:justify-start items-center md:items-start">
|
||||
<MaintenanceMessage />
|
||||
<Button variant="outline-primary" onClick={() => window.location.reload()}>
|
||||
Reload
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DefaultLayout>
|
||||
);
|
||||
@@ -27,6 +27,7 @@ export const useLinkOperations = (workspaceSlug: string, projectId: string, issu
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Link not created",
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
update: async (linkId: string, data: Partial<TIssueLink>) => {
|
||||
@@ -44,6 +45,7 @@ export const useLinkOperations = (workspaceSlug: string, projectId: string, issu
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Link not updated",
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
remove: async (linkId: string) => {
|
||||
|
||||
@@ -7,8 +7,6 @@ import { Controller, useForm } from "react-hook-form";
|
||||
import type { TIssueLinkEditableFields } from "@plane/types";
|
||||
// plane ui
|
||||
import { Button, Input, ModalCore } from "@plane/ui";
|
||||
// helpers
|
||||
import { checkURLValidity } from "@/helpers/string.helper";
|
||||
// hooks
|
||||
import { useIssueDetail } from "@/hooks/store";
|
||||
// types
|
||||
@@ -48,14 +46,18 @@ export const IssueLinkCreateUpdateModal: FC<TIssueLinkCreateEditModal> = observe
|
||||
|
||||
const onClose = () => {
|
||||
setIssueLinkData(null);
|
||||
reset();
|
||||
if (handleOnClose) handleOnClose();
|
||||
};
|
||||
|
||||
const handleFormSubmit = async (formData: TIssueLinkCreateFormFieldOptions) => {
|
||||
if (!formData || !formData.id) await linkOperations.create({ title: formData.title, url: formData.url });
|
||||
else await linkOperations.update(formData.id as string, { title: formData.title, url: formData.url });
|
||||
onClose();
|
||||
const parsedUrl = formData.url.startsWith("http") ? formData.url : `http://${formData.url}`;
|
||||
try {
|
||||
if (!formData || !formData.id) await linkOperations.create({ title: formData.title, url: parsedUrl });
|
||||
else await linkOperations.update(formData.id, { title: formData.title, url: parsedUrl });
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error("error", error);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -77,7 +79,6 @@ export const IssueLinkCreateUpdateModal: FC<TIssueLinkCreateEditModal> = observe
|
||||
name="url"
|
||||
rules={{
|
||||
required: "URL is required",
|
||||
validate: (value) => checkURLValidity(value) || "URL is invalid",
|
||||
}}
|
||||
render={({ field: { value, onChange, ref } }) => (
|
||||
<Input
|
||||
|
||||
@@ -58,6 +58,7 @@ export const IssueLinkRoot: FC<TIssueLinkRoot> = (props) => {
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Link not created",
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
update: async (linkId: string, data: Partial<TIssueLink>) => {
|
||||
@@ -76,6 +77,7 @@ export const IssueLinkRoot: FC<TIssueLinkRoot> = (props) => {
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Link not updated",
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
remove: async (linkId: string) => {
|
||||
|
||||
@@ -4,6 +4,8 @@ import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// ui
|
||||
import { Tooltip, ControlLink } from "@plane/ui";
|
||||
// components
|
||||
import { SIDEBAR_WIDTH } from "@/components/gantt-chart/constants";
|
||||
// helpers
|
||||
import { renderFormattedDate } from "@/helpers/date-time.helper";
|
||||
// hooks
|
||||
@@ -13,7 +15,8 @@ import useIssuePeekOverviewRedirection from "@/hooks/use-issue-peek-overview-red
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// plane web components
|
||||
import { IssueIdentifier } from "@/plane-web/components/issues";
|
||||
// local types
|
||||
//
|
||||
import { getBlockViewDetails } from "../utils";
|
||||
import { GanttStoreType } from "./base-gantt-root";
|
||||
|
||||
type Props = {
|
||||
@@ -39,36 +42,37 @@ export const IssueGanttBlock: React.FC<Props> = observer((props) => {
|
||||
const stateDetails =
|
||||
issueDetails && getProjectStates(issueDetails?.project_id)?.find((state) => state?.id == issueDetails?.state_id);
|
||||
|
||||
const { message, blockStyle } = getBlockViewDetails(issueDetails, stateDetails?.color ?? "");
|
||||
|
||||
const handleIssuePeekOverview = () => handleRedirection(workspaceSlug, issueDetails, isMobile);
|
||||
|
||||
return (
|
||||
<div
|
||||
id={`issue-${issueId}`}
|
||||
className="relative flex h-full w-full cursor-pointer items-center rounded"
|
||||
style={{
|
||||
backgroundColor: stateDetails?.color,
|
||||
}}
|
||||
onClick={handleIssuePeekOverview}
|
||||
<Tooltip
|
||||
isMobile={isMobile}
|
||||
tooltipContent={
|
||||
<div className="space-y-1">
|
||||
<h5>{issueDetails?.name}</h5>
|
||||
<div>{message}</div>
|
||||
</div>
|
||||
}
|
||||
position="top-left"
|
||||
disabled={!message}
|
||||
>
|
||||
<div className="absolute left-0 top-0 h-full w-full bg-custom-background-100/50" />
|
||||
<Tooltip
|
||||
isMobile={isMobile}
|
||||
tooltipContent={
|
||||
<div className="space-y-1">
|
||||
<h5>{issueDetails?.name}</h5>
|
||||
<div>
|
||||
{renderFormattedDate(issueDetails?.start_date ?? "")} to{" "}
|
||||
{renderFormattedDate(issueDetails?.target_date ?? "")}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
position="top-left"
|
||||
<div
|
||||
id={`issue-${issueId}`}
|
||||
className="relative flex h-full w-full cursor-pointer items-center rounded"
|
||||
style={blockStyle}
|
||||
onClick={handleIssuePeekOverview}
|
||||
>
|
||||
<div className="relative w-full overflow-hidden truncate px-2.5 py-1 text-sm text-custom-text-100">
|
||||
<div className="absolute left-0 top-0 h-full w-full bg-custom-background-100/50" />
|
||||
<div
|
||||
className="sticky w-auto overflow-hidden truncate px-2.5 py-1 text-sm text-custom-text-100"
|
||||
style={{ left: `${SIDEBAR_WIDTH}px` }}
|
||||
>
|
||||
{issueDetails?.name}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -92,7 +96,11 @@ export const IssueGanttSidebarBlock: React.FC<Props> = observer((props) => {
|
||||
// derived values
|
||||
const issueDetails = getIssueById(issueId);
|
||||
|
||||
const handleIssuePeekOverview = () => handleRedirection(workspaceSlug, issueDetails, isMobile);
|
||||
const handleIssuePeekOverview = (e: any) => {
|
||||
e.stopPropagation(true);
|
||||
e.preventDefault();
|
||||
handleRedirection(workspaceSlug, issueDetails, isMobile);
|
||||
};
|
||||
|
||||
return (
|
||||
<ControlLink
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { CSSProperties } from "react";
|
||||
import { extractInstruction } from "@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item";
|
||||
import clone from "lodash/clone";
|
||||
import concat from "lodash/concat";
|
||||
@@ -32,6 +33,7 @@ import { Logo } from "@/components/common";
|
||||
import { ISSUE_PRIORITIES, EIssuesStoreType } from "@/constants/issue";
|
||||
import { STATE_GROUPS } from "@/constants/state";
|
||||
// helpers
|
||||
import { renderFormattedDate } from "@/helpers/date-time.helper";
|
||||
import { getFileURL } from "@/helpers/file.helper";
|
||||
// store
|
||||
import { ICycleStore } from "@/store/cycle.store";
|
||||
@@ -672,3 +674,39 @@ export function getApproximateCardHeight(displayProperties: IIssueDisplayPropert
|
||||
|
||||
return cardHeight;
|
||||
}
|
||||
|
||||
/**
|
||||
* This Method is used to get Block view details, that returns block style and tooltip message
|
||||
* @param block
|
||||
* @param backgroundColor
|
||||
* @returns
|
||||
*/
|
||||
export const getBlockViewDetails = (
|
||||
block: { start_date: string | undefined | null; target_date: string | undefined | null } | undefined | null,
|
||||
backgroundColor: string
|
||||
) => {
|
||||
const isBlockVisibleOnChart = block?.start_date || block?.target_date;
|
||||
const isBlockComplete = block?.start_date && block?.target_date;
|
||||
|
||||
let message;
|
||||
const blockStyle: CSSProperties = {
|
||||
backgroundColor,
|
||||
};
|
||||
|
||||
if (isBlockVisibleOnChart && !isBlockComplete) {
|
||||
if (block?.start_date) {
|
||||
message = `From ${renderFormattedDate(block.start_date)}`;
|
||||
blockStyle.maskImage = `linear-gradient(to right, ${backgroundColor} 50%, transparent 95%)`;
|
||||
} else if (block?.target_date) {
|
||||
message = `Till ${renderFormattedDate(block.target_date)}`;
|
||||
blockStyle.maskImage = `linear-gradient(to left, ${backgroundColor} 50%, transparent 95%)`;
|
||||
}
|
||||
} else if (isBlockComplete) {
|
||||
message = `${renderFormattedDate(block?.start_date)} to ${renderFormattedDate(block?.target_date)}`;
|
||||
}
|
||||
|
||||
return {
|
||||
message,
|
||||
blockStyle,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -135,8 +135,13 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
|
||||
<span>Created by</span>
|
||||
</div>
|
||||
<div className="w-full h-full flex items-center gap-1.5 rounded px-2 py-0.5 text-sm justify-between cursor-not-allowed">
|
||||
<ButtonAvatars showTooltip userIds={createdByDetails?.id} />
|
||||
<span className="flex-grow truncate text-xs leading-5">{createdByDetails?.display_name}</span>
|
||||
<ButtonAvatars
|
||||
showTooltip
|
||||
userIds={createdByDetails?.display_name.includes("-intake") ? null : createdByDetails?.id}
|
||||
/>
|
||||
<span className="flex-grow truncate text-xs leading-5">
|
||||
{createdByDetails?.display_name.includes("-intake") ? "Plane" : createdByDetails?.display_name}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -3,13 +3,14 @@
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { useParams } from "next/navigation";
|
||||
// hooks
|
||||
// ui
|
||||
import { Tooltip, ModuleStatusIcon } from "@plane/ui";
|
||||
// helpers
|
||||
import { MODULE_STATUS } from "@/constants/module";
|
||||
import { renderFormattedDate } from "@/helpers/date-time.helper";
|
||||
// components
|
||||
import { SIDEBAR_WIDTH } from "@/components/gantt-chart/constants";
|
||||
import { getBlockViewDetails } from "@/components/issues/issue-layouts/utils";
|
||||
// constants
|
||||
import { MODULE_STATUS } from "@/constants/module";
|
||||
// hooks
|
||||
import { useModule } from "@/hooks/store";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
@@ -30,31 +31,40 @@ export const ModuleGanttBlock: React.FC<Props> = observer((props) => {
|
||||
// hooks
|
||||
const { isMobile } = usePlatformOS();
|
||||
|
||||
const { message, blockStyle } = getBlockViewDetails(
|
||||
moduleDetails,
|
||||
MODULE_STATUS.find((s) => s.value === moduleDetails?.status)?.color ?? ""
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative flex h-full w-full items-center rounded"
|
||||
style={{ backgroundColor: MODULE_STATUS.find((s) => s.value === moduleDetails?.status)?.color }}
|
||||
onClick={() =>
|
||||
router.push(`/${workspaceSlug?.toString()}/projects/${moduleDetails?.project_id}/modules/${moduleDetails?.id}`)
|
||||
<Tooltip
|
||||
isMobile={isMobile}
|
||||
tooltipContent={
|
||||
<div className="space-y-1">
|
||||
<h5>{moduleDetails?.name}</h5>
|
||||
<div>{message}</div>
|
||||
</div>
|
||||
}
|
||||
position="top-left"
|
||||
>
|
||||
<div className="absolute left-0 top-0 h-full w-full bg-custom-background-100/50" />
|
||||
<Tooltip
|
||||
isMobile={isMobile}
|
||||
tooltipContent={
|
||||
<div className="space-y-1">
|
||||
<h5>{moduleDetails?.name}</h5>
|
||||
<div>
|
||||
{renderFormattedDate(moduleDetails?.start_date ?? "")} to{" "}
|
||||
{renderFormattedDate(moduleDetails?.target_date ?? "")}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="relative flex h-full w-full cursor-pointer items-center rounded"
|
||||
style={blockStyle}
|
||||
onClick={() =>
|
||||
router.push(
|
||||
`/${workspaceSlug?.toString()}/projects/${moduleDetails?.project_id}/modules/${moduleDetails?.id}`
|
||||
)
|
||||
}
|
||||
position="top-left"
|
||||
>
|
||||
<div className="relative w-full truncate px-2.5 py-1 text-sm text-custom-text-100">{moduleDetails?.name}</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="absolute left-0 top-0 h-full w-full bg-custom-background-100/50" />
|
||||
<div
|
||||
className="sticky w-auto overflow-hidden truncate px-2.5 py-1 text-sm text-custom-text-100"
|
||||
style={{ left: `${SIDEBAR_WIDTH}px` }}
|
||||
>
|
||||
{moduleDetails?.name}
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -6,8 +6,6 @@ import { Controller, useForm } from "react-hook-form";
|
||||
import type { ILinkDetails, ModuleLink } from "@plane/types";
|
||||
// plane ui
|
||||
import { Button, Input, ModalCore, setToast, TOAST_TYPE } from "@plane/ui";
|
||||
// helpers
|
||||
import { checkURLValidity } from "@/helpers/string.helper";
|
||||
|
||||
type Props = {
|
||||
createLink: (formData: ModuleLink) => Promise<void>;
|
||||
@@ -39,9 +37,10 @@ export const CreateUpdateModuleLinkModal: FC<Props> = (props) => {
|
||||
};
|
||||
|
||||
const handleFormSubmit = async (formData: ModuleLink) => {
|
||||
const parsedUrl = formData.url.startsWith("http") ? formData.url : `http://${formData.url}`;
|
||||
const payload = {
|
||||
title: formData.title,
|
||||
url: formData.url,
|
||||
url: parsedUrl,
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -92,7 +91,6 @@ export const CreateUpdateModuleLinkModal: FC<Props> = (props) => {
|
||||
name="url"
|
||||
rules={{
|
||||
required: "URL is required",
|
||||
validate: (value) => checkURLValidity(value) || "URL is invalid",
|
||||
}}
|
||||
render={({ field: { value, onChange, ref } }) => (
|
||||
<Input
|
||||
|
||||
@@ -79,9 +79,9 @@ export const SPREADSHEET_PROPERTY_DETAILS: {
|
||||
},
|
||||
estimate: {
|
||||
title: "Estimate",
|
||||
ascendingOrderKey: "estimate_point",
|
||||
ascendingOrderKey: "estimate_point__key",
|
||||
ascendingOrderTitle: "Low",
|
||||
descendingOrderKey: "-estimate_point",
|
||||
descendingOrderKey: "-estimate_point__key",
|
||||
descendingOrderTitle: "High",
|
||||
icon: Triangle,
|
||||
Column: SpreadsheetEstimateColumn,
|
||||
|
||||
@@ -3,11 +3,9 @@ import { observer } from "mobx-react";
|
||||
import useSWR from "swr";
|
||||
// components
|
||||
import { LogoSpinner } from "@/components/common";
|
||||
import { InstanceNotReady } from "@/components/instance";
|
||||
import { InstanceNotReady, MaintenanceView } from "@/components/instance";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks/store";
|
||||
// plane web components
|
||||
import { MaintenanceMode } from "@/plane-web/components/maintenance-mode";
|
||||
|
||||
type TInstanceWrapper = {
|
||||
children: ReactNode;
|
||||
@@ -32,7 +30,7 @@ export const InstanceWrapper: FC<TInstanceWrapper> = observer((props) => {
|
||||
</div>
|
||||
);
|
||||
|
||||
if (instanceSWRError) return <MaintenanceMode />;
|
||||
if (instanceSWRError) return <MaintenanceView />;
|
||||
|
||||
// something went wrong while in the request
|
||||
if (error && error?.status === "error") return <>{children}</>;
|
||||
|
||||
@@ -300,6 +300,7 @@ export class Storage {
|
||||
const { cursor, group_by, sub_group_by } = queries;
|
||||
|
||||
const query = issueFilterQueryConstructor(this.workspaceSlug, projectId, queries);
|
||||
log("#### Query", query);
|
||||
const countQuery = issueFilterCountQueryConstructor(this.workspaceSlug, projectId, queries);
|
||||
const start = performance.now();
|
||||
let issuesRaw: any[] = [];
|
||||
|
||||
@@ -87,10 +87,10 @@ export const getStates = async (workspaceSlug: string) => {
|
||||
export const getEstimatePoints = async (workspaceSlug: string) => {
|
||||
const estimateService = new EstimateService();
|
||||
const estimates = await estimateService.fetchWorkspaceEstimates(workspaceSlug);
|
||||
const objects: IEstimatePoint[] = [];
|
||||
let objects: IEstimatePoint[] = [];
|
||||
(estimates || []).forEach((estimate: IEstimate) => {
|
||||
if (estimate?.points) {
|
||||
objects.concat(estimate.points);
|
||||
objects = objects.concat(estimate.points);
|
||||
}
|
||||
});
|
||||
return objects;
|
||||
@@ -104,6 +104,9 @@ export const getMembers = async (workspaceSlug: string) => {
|
||||
};
|
||||
|
||||
export const loadWorkSpaceData = async (workspaceSlug: string) => {
|
||||
if (!persistence.db || !persistence.db.exec) {
|
||||
return;
|
||||
}
|
||||
log("Loading workspace data");
|
||||
const promises = [];
|
||||
promises.push(getLabels(workspaceSlug));
|
||||
@@ -112,7 +115,7 @@ export const loadWorkSpaceData = async (workspaceSlug: string) => {
|
||||
promises.push(getStates(workspaceSlug));
|
||||
promises.push(getEstimatePoints(workspaceSlug));
|
||||
promises.push(getMembers(workspaceSlug));
|
||||
const [labels, modules, cycles, states, estimates, memebers] = await Promise.all(promises);
|
||||
const [labels, modules, cycles, states, estimates, members] = await Promise.all(promises);
|
||||
|
||||
const start = performance.now();
|
||||
await persistence.db.exec("BEGIN;");
|
||||
@@ -121,7 +124,7 @@ export const loadWorkSpaceData = async (workspaceSlug: string) => {
|
||||
await batchInserts(cycles, "cycles", cycleSchema);
|
||||
await batchInserts(states, "states", stateSchema);
|
||||
await batchInserts(estimates, "estimate_points", estimatePointSchema);
|
||||
await batchInserts(memebers, "members", memberSchema);
|
||||
await batchInserts(members, "members", memberSchema);
|
||||
await persistence.db.exec("COMMIT;");
|
||||
const end = performance.now();
|
||||
log("Time taken to load workspace data", end - start);
|
||||
|
||||
@@ -18,6 +18,8 @@ export const SPECIAL_ORDER_BY = {
|
||||
"-issue_cycle__cycle__name": "cycles",
|
||||
state__name: "states",
|
||||
"-state__name": "states",
|
||||
estimate_point__key: "estimate_point",
|
||||
"-estimate_point__key": "estimate_point",
|
||||
};
|
||||
export const issueFilterQueryConstructor = (workspaceSlug: string, projectId: string, queries: any) => {
|
||||
const {
|
||||
@@ -48,8 +50,6 @@ export const issueFilterQueryConstructor = (workspaceSlug: string, projectId: st
|
||||
|
||||
`;
|
||||
|
||||
log("###", sql);
|
||||
|
||||
return sql;
|
||||
}
|
||||
if (group_by) {
|
||||
@@ -64,8 +64,6 @@ export const issueFilterQueryConstructor = (workspaceSlug: string, projectId: st
|
||||
WHERE rank <= ${per_page}
|
||||
`;
|
||||
|
||||
log("###", sql);
|
||||
|
||||
return sql;
|
||||
}
|
||||
|
||||
@@ -78,8 +76,10 @@ export const issueFilterQueryConstructor = (workspaceSlug: string, projectId: st
|
||||
sql += `SELECT fi.* , `;
|
||||
if (order_by.includes("assignee")) {
|
||||
sql += ` s.first_name as ${name} `;
|
||||
} else if (order_by.includes("estimate")) {
|
||||
sql += ` s.key as ${name} `;
|
||||
} else {
|
||||
sql += ` s.name as ${name} `;
|
||||
sql += ` s.name as ${name} `;
|
||||
}
|
||||
sql += `FROM fi `;
|
||||
if (order_by && Object.keys(SPECIAL_ORDER_BY).includes(order_by)) {
|
||||
@@ -87,7 +87,7 @@ export const issueFilterQueryConstructor = (workspaceSlug: string, projectId: st
|
||||
sql += `
|
||||
LEFT JOIN cycles s on fi.cycle_id = s.id`;
|
||||
}
|
||||
if (order_by.includes("estimate_point")) {
|
||||
if (order_by.includes("estimate_point__key")) {
|
||||
sql += `
|
||||
LEFT JOIN estimate_points s on fi.estimate_point = s.id`;
|
||||
}
|
||||
@@ -120,7 +120,6 @@ export const issueFilterQueryConstructor = (workspaceSlug: string, projectId: st
|
||||
`;
|
||||
sql += ` group by i.id ${orderByString} LIMIT ${pageSize} OFFSET ${offset * 1 + page * pageSize};`;
|
||||
|
||||
log("######$$$", sql);
|
||||
return sql;
|
||||
}
|
||||
|
||||
@@ -149,7 +148,6 @@ export const issueFilterQueryConstructor = (workspaceSlug: string, projectId: st
|
||||
// Add offset and paging to query
|
||||
sql += ` LIMIT ${pageSize} OFFSET ${offset * 1 + page * pageSize};`;
|
||||
|
||||
log("$$$", sql);
|
||||
return sql;
|
||||
};
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ export const translateQueryParams = (queries: any) => {
|
||||
}
|
||||
|
||||
// Fix invalid orderby when switching from spreadsheet layout
|
||||
if (layout === "spreadsheet" && Object.keys(SPECIAL_ORDER_BY).includes(order_by)) {
|
||||
if (layout !== "spreadsheet" && Object.keys(SPECIAL_ORDER_BY).includes(order_by)) {
|
||||
otherProps.order_by = "sort_order";
|
||||
}
|
||||
// For each property value, replace None with empty string
|
||||
@@ -336,6 +336,9 @@ const getSingleFilterFields = (queries: any) => {
|
||||
if (state_group) {
|
||||
fields.add("states.'group' as state_group");
|
||||
}
|
||||
if (order_by?.includes("estimate_point__key")) {
|
||||
fields.add("estimate_point");
|
||||
}
|
||||
return Array.from(fields);
|
||||
};
|
||||
|
||||
|
||||
@@ -388,7 +388,7 @@ export class ProjectInboxStore implements IProjectInboxStore {
|
||||
else this.loader = "mutation-loading";
|
||||
if (loadingType) this.loader = loadingType;
|
||||
|
||||
const status = this.inboxFilters?.status && uniq([...this.inboxFilters.status, EInboxIssueStatus.SNOOZED]);
|
||||
const status = this.inboxFilters?.status;
|
||||
const queryParams = this.inboxIssueQueryParams(
|
||||
{ ...this.inboxFilters, status },
|
||||
this.inboxSorting,
|
||||
|
||||
@@ -36,6 +36,7 @@ import { EIssueLayoutTypes, ISSUE_PRIORITIES } from "@/constants/issue";
|
||||
// helpers
|
||||
import { convertToISODateString } from "@/helpers/date-time.helper";
|
||||
// local-db
|
||||
import { SPECIAL_ORDER_BY } from "@/local-db/utils/query-constructor";
|
||||
import { updatePersistentLayer } from "@/local-db/utils/utils";
|
||||
// services
|
||||
import { CycleService } from "@/services/cycle.service";
|
||||
@@ -164,8 +165,8 @@ const ISSUE_ORDERBY_KEY: Record<TIssueOrderByOptions, keyof TIssue> = {
|
||||
"-issue_cycle__cycle__name": "cycle_id",
|
||||
target_date: "target_date",
|
||||
"-target_date": "target_date",
|
||||
estimate_point: "estimate_point",
|
||||
"-estimate_point": "estimate_point",
|
||||
estimate_point__key: "estimate_point",
|
||||
"-estimate_point__key": "estimate_point",
|
||||
start_date: "start_date",
|
||||
"-start_date": "start_date",
|
||||
link_count: "link_count",
|
||||
@@ -282,6 +283,19 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
|
||||
const displayFilters = this.issueFilterStore?.issueFilters?.displayFilters;
|
||||
if (!displayFilters) return;
|
||||
|
||||
const layout = displayFilters.layout;
|
||||
const orderBy = displayFilters.order_by;
|
||||
|
||||
// Temporary code to fix no load order by
|
||||
if (
|
||||
this.rootIssueStore.rootStore.user.localDBEnabled &&
|
||||
layout !== EIssueLayoutTypes.SPREADSHEET &&
|
||||
orderBy &&
|
||||
Object.keys(SPECIAL_ORDER_BY).includes(orderBy)
|
||||
) {
|
||||
return "sort_order";
|
||||
}
|
||||
|
||||
return displayFilters?.order_by;
|
||||
}
|
||||
|
||||
@@ -1701,13 +1715,14 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
|
||||
* @returns string | string[] of sortable fields to be used for sorting
|
||||
*/
|
||||
populateIssueDataForSorting(
|
||||
dataType: "state_id" | "label_ids" | "assignee_ids" | "module_ids" | "cycle_id",
|
||||
dataType: "state_id" | "label_ids" | "assignee_ids" | "module_ids" | "cycle_id" | "estimate_point",
|
||||
dataIds: string | string[] | null | undefined,
|
||||
projectId: string | undefined | null,
|
||||
order?: "asc" | "desc"
|
||||
) {
|
||||
if (!dataIds) return;
|
||||
|
||||
const dataValues: string[] = [];
|
||||
const dataValues: (string | number)[] = [];
|
||||
const isDataIdsArray = Array.isArray(dataIds);
|
||||
const dataIdsArray = isDataIdsArray ? dataIds : [dataIds];
|
||||
|
||||
@@ -1757,6 +1772,26 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "estimate_point": {
|
||||
// return if project Id does not exist
|
||||
if (!projectId) break;
|
||||
// get the estimate ID for the current Project
|
||||
const currentProjectEstimateId =
|
||||
this.rootIssueStore.rootStore.projectEstimate.currentActiveEstimateIdByProjectId(projectId);
|
||||
// return if current Estimate Id for the project is not available
|
||||
if (!currentProjectEstimateId) break;
|
||||
// get Estimate based on Id
|
||||
const estimate = this.rootIssueStore.rootStore.projectEstimate.estimateById(currentProjectEstimateId);
|
||||
// If Estimate is not available, then return
|
||||
if (!estimate) break;
|
||||
// Get Estimate Value
|
||||
const estimateKey = estimate?.estimatePointById(dataIds as string)?.key;
|
||||
|
||||
// If Value string i not available or empty then return
|
||||
if (estimateKey === undefined) break;
|
||||
|
||||
dataValues.push(estimateKey);
|
||||
}
|
||||
}
|
||||
|
||||
return isDataIdsArray ? (order ? orderBy(dataValues, undefined, [order]) : dataValues) : dataValues;
|
||||
@@ -1771,11 +1806,17 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
|
||||
return getIssueIds(orderBy(array, "sort_order"));
|
||||
case "state__name":
|
||||
return getIssueIds(
|
||||
orderBy(array, (issue) => this.populateIssueDataForSorting("state_id", issue?.["state_id"]))
|
||||
orderBy(array, (issue) =>
|
||||
this.populateIssueDataForSorting("state_id", issue?.["state_id"], issue?.["project_id"])
|
||||
)
|
||||
);
|
||||
case "-state__name":
|
||||
return getIssueIds(
|
||||
orderBy(array, (issue) => this.populateIssueDataForSorting("state_id", issue?.["state_id"]), ["desc"])
|
||||
orderBy(
|
||||
array,
|
||||
(issue) => this.populateIssueDataForSorting("state_id", issue?.["state_id"], issue?.["project_id"]),
|
||||
["desc"]
|
||||
)
|
||||
);
|
||||
// dates
|
||||
case "created_at":
|
||||
@@ -1826,15 +1867,23 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
|
||||
case "-attachment_count":
|
||||
return getIssueIds(orderBy(array, "attachment_count", ["desc"]));
|
||||
|
||||
case "estimate_point":
|
||||
case "estimate_point__key":
|
||||
return getIssueIds(
|
||||
orderBy(array, [getSortOrderToFilterEmptyValues.bind(null, "estimate_point"), "estimate_point"])
|
||||
orderBy(array, [
|
||||
getSortOrderToFilterEmptyValues.bind(null, "estimate_point"),
|
||||
(issue) =>
|
||||
this.populateIssueDataForSorting("estimate_point", issue?.["estimate_point"], issue?.["project_id"]),
|
||||
])
|
||||
); //preferring sorting based on empty values to always keep the empty values below
|
||||
case "-estimate_point":
|
||||
case "-estimate_point__key":
|
||||
return getIssueIds(
|
||||
orderBy(
|
||||
array,
|
||||
[getSortOrderToFilterEmptyValues.bind(null, "estimate_point"), "estimate_point"], //preferring sorting based on empty values to always keep the empty values below
|
||||
[
|
||||
getSortOrderToFilterEmptyValues.bind(null, "estimate_point"),
|
||||
(issue) =>
|
||||
this.populateIssueDataForSorting("estimate_point", issue?.["estimate_point"], issue?.["project_id"]),
|
||||
], //preferring sorting based on empty values to always keep the empty values below
|
||||
["asc", "desc"]
|
||||
)
|
||||
);
|
||||
@@ -1854,7 +1903,8 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
|
||||
return getIssueIds(
|
||||
orderBy(array, [
|
||||
getSortOrderToFilterEmptyValues.bind(null, "label_ids"), //preferring sorting based on empty values to always keep the empty values below
|
||||
(issue) => this.populateIssueDataForSorting("label_ids", issue?.["label_ids"], "asc"),
|
||||
(issue) =>
|
||||
this.populateIssueDataForSorting("label_ids", issue?.["label_ids"], issue?.["project_id"], "asc"),
|
||||
])
|
||||
);
|
||||
case "-labels__name":
|
||||
@@ -1863,7 +1913,8 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
|
||||
array,
|
||||
[
|
||||
getSortOrderToFilterEmptyValues.bind(null, "label_ids"), //preferring sorting based on empty values to always keep the empty values below
|
||||
(issue) => this.populateIssueDataForSorting("label_ids", issue?.["label_ids"], "asc"),
|
||||
(issue) =>
|
||||
this.populateIssueDataForSorting("label_ids", issue?.["label_ids"], issue?.["project_id"], "asc"),
|
||||
],
|
||||
["asc", "desc"]
|
||||
)
|
||||
@@ -1873,7 +1924,8 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
|
||||
return getIssueIds(
|
||||
orderBy(array, [
|
||||
getSortOrderToFilterEmptyValues.bind(null, "module_ids"), //preferring sorting based on empty values to always keep the empty values below
|
||||
(issue) => this.populateIssueDataForSorting("module_ids", issue?.["module_ids"], "asc"),
|
||||
(issue) =>
|
||||
this.populateIssueDataForSorting("module_ids", issue?.["module_ids"], issue?.["project_id"], "asc"),
|
||||
])
|
||||
);
|
||||
case "-issue_module__module__name":
|
||||
@@ -1882,7 +1934,8 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
|
||||
array,
|
||||
[
|
||||
getSortOrderToFilterEmptyValues.bind(null, "module_ids"), //preferring sorting based on empty values to always keep the empty values below
|
||||
(issue) => this.populateIssueDataForSorting("module_ids", issue?.["module_ids"], "asc"),
|
||||
(issue) =>
|
||||
this.populateIssueDataForSorting("module_ids", issue?.["module_ids"], issue?.["project_id"], "asc"),
|
||||
],
|
||||
["asc", "desc"]
|
||||
)
|
||||
@@ -1892,7 +1945,7 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
|
||||
return getIssueIds(
|
||||
orderBy(array, [
|
||||
getSortOrderToFilterEmptyValues.bind(null, "cycle_id"), //preferring sorting based on empty values to always keep the empty values below
|
||||
(issue) => this.populateIssueDataForSorting("cycle_id", issue?.["cycle_id"], "asc"),
|
||||
(issue) => this.populateIssueDataForSorting("cycle_id", issue?.["cycle_id"], issue?.["project_id"], "asc"),
|
||||
])
|
||||
);
|
||||
case "-issue_cycle__cycle__name":
|
||||
@@ -1901,7 +1954,8 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
|
||||
array,
|
||||
[
|
||||
getSortOrderToFilterEmptyValues.bind(null, "cycle_id"), //preferring sorting based on empty values to always keep the empty values below
|
||||
(issue) => this.populateIssueDataForSorting("cycle_id", issue?.["cycle_id"], "asc"),
|
||||
(issue) =>
|
||||
this.populateIssueDataForSorting("cycle_id", issue?.["cycle_id"], issue?.["project_id"], "asc"),
|
||||
],
|
||||
["asc", "desc"]
|
||||
)
|
||||
@@ -1911,7 +1965,8 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
|
||||
return getIssueIds(
|
||||
orderBy(array, [
|
||||
getSortOrderToFilterEmptyValues.bind(null, "assignee_ids"), //preferring sorting based on empty values to always keep the empty values below
|
||||
(issue) => this.populateIssueDataForSorting("assignee_ids", issue?.["assignee_ids"], "asc"),
|
||||
(issue) =>
|
||||
this.populateIssueDataForSorting("assignee_ids", issue?.["assignee_ids"], issue?.["project_id"], "asc"),
|
||||
])
|
||||
);
|
||||
case "-assignees__first_name":
|
||||
@@ -1920,7 +1975,8 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
|
||||
array,
|
||||
[
|
||||
getSortOrderToFilterEmptyValues.bind(null, "assignee_ids"), //preferring sorting based on empty values to always keep the empty values below
|
||||
(issue) => this.populateIssueDataForSorting("assignee_ids", issue?.["assignee_ids"], "asc"),
|
||||
(issue) =>
|
||||
this.populateIssueDataForSorting("assignee_ids", issue?.["assignee_ids"], issue?.["project_id"], "asc"),
|
||||
],
|
||||
["asc", "desc"]
|
||||
)
|
||||
|
||||
1
web/ee/components/instance/index.ts
Normal file
1
web/ee/components/instance/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "ce/components/instance";
|
||||
1
web/ee/components/instance/maintenance-message.tsx
Normal file
1
web/ee/components/instance/maintenance-message.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export const MaintenanceMessage = () => <></>;
|
||||
@@ -1 +0,0 @@
|
||||
export * from "ce/components/maintenance-mode";
|
||||
BIN
web/public/maintenance-mode.webp
Normal file
BIN
web/public/maintenance-mode.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 117 KiB |
Reference in New Issue
Block a user