Compare commits

...

8 Commits

Author SHA1 Message Date
NarayanBavisetti
5277788be4 chore: issue description version histroy 2024-11-14 18:17:02 +05:30
Aaryan Khandelwal
89588d4451 fix: issue and module link validation (#5994)
* fix: issue and module link validation

* chore: removed reset logic
2024-11-13 19:47:30 +05:30
Akshita Goyal
3eb911837c fix: display property in take (#5993) 2024-11-13 18:02:24 +05:30
rahulramesha
4b50b27a74 [WEB-2442] feat: Minor Timeline view Enhancements (#5987)
* fix timeline scroll to the right in some cases

(cherry picked from commit 17043a6c7f)

* add get position based on Date

(cherry picked from commit 2fbe22d689)

* Add sticky block name to enable it to be read throughout the block regardless of scroll position

(cherry picked from commit 447af2e05a)

* Enable blocks to have a single date on the block charts

(cherry picked from commit cb055d566b)

* revert back date-range changes

* change gradient of half blocks on Timeline

* Add instance Id for Timeline Sidebar dragging to avoid enabling dropping of other drag instances

* fix timeline scrolling height
2024-11-13 15:40:37 +05:30
rahulramesha
f44db89f41 [WEB-2628] fix: Sorting by estimates (#5988)
* fix estimates sorting in Front end side

* change estimate sorting keys

* - Fix estimate sorting when local db is enabled
- Fix a bug with with sorting on special fields on spreadsheet layout
- Cleanup logging

* Add logic for order by based on layout for special cases of no load

---------

Co-authored-by: Satish Gandham <satish.iitg@gmail.com>
2024-11-13 15:38:43 +05:30
Akshita Goyal
8c3189e1be fix: intake status count (#5990) 2024-11-13 15:38:03 +05:30
sriram veeraghanta
eee2145734 fix: code spliting and instance maintenance screens 2024-11-12 19:48:31 +05:30
Aaryan Khandelwal
106710f3d0 fix: custom background color for table header (#5989) 2024-11-12 15:26:57 +05:30
52 changed files with 777 additions and 213 deletions

View File

@@ -71,6 +71,8 @@ from .issue import (
IssueReactionLiteSerializer,
IssueAttachmentLiteSerializer,
IssueLinkLiteSerializer,
IssueDescriptionVersionSerializer,
IssueDescriptionVersionDetailSerializer,
)
from .module import (

View File

@@ -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",
]

View File

@@ -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
]

View File

@@ -138,6 +138,8 @@ from .issue.activity import (
IssueActivityEndpoint,
)
from .issue.version import IssueVersionEndpoint
from .issue.archive import IssueArchiveViewSet, BulkArchiveIssuesEndpoint
from .issue.attachment import (

View 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)

View File

@@ -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,
)

View File

@@ -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)

View File

@@ -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

View 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

View 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",),
},
),
]

View File

@@ -41,6 +41,7 @@ from .issue import (
IssueSequence,
IssueSubscriber,
IssueVote,
IssueDescriptionVersion
)
from .module import (
Module,

View File

@@ -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"),

View File

@@ -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 {

View File

@@ -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"

View File

@@ -0,0 +1 @@
export * from "./maintenance-message";

View 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&apos;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>
);

View File

@@ -1,5 +0,0 @@
"use client";
import { FC, Fragment } from "react";
export const MaintenanceMode: FC = () => <Fragment />;

View File

@@ -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
*/

View File

@@ -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);

View File

@@ -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}
/>

View File

@@ -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

View File

@@ -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 */}

View File

@@ -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

View File

@@ -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

View File

@@ -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";

View File

@@ -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;

View File

@@ -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 };

View File

@@ -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;

View File

@@ -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

View File

@@ -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;
};

View File

@@ -1 +1,2 @@
export * from "./not-ready-view";
export * from "./maintenance-view";

View 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>
);

View File

@@ -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) => {

View File

@@ -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

View File

@@ -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) => {

View File

@@ -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

View File

@@ -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,
};
};

View File

@@ -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>
)}

View File

@@ -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>
);
});

View File

@@ -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

View File

@@ -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,

View File

@@ -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}</>;

View File

@@ -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[] = [];

View File

@@ -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);

View File

@@ -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;
};

View File

@@ -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);
};

View File

@@ -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,

View File

@@ -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"]
)

View File

@@ -0,0 +1 @@
export * from "ce/components/instance";

View File

@@ -0,0 +1 @@
export const MaintenanceMessage = () => <></>;

View File

@@ -1 +0,0 @@
export * from "ce/components/maintenance-mode";

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB