Compare commits

..

3 Commits

Author SHA1 Message Date
gurusainath 4ec2c61e0d chore: updated the discriptiob_binary key 2024-11-14 17:40:52 +05:30
gurusainath 641ef9fd14 chore: handled descritpion_binary error 2024-11-14 17:10:55 +05:30
gurusainath 68a4d02847 chore: added description_binary in issues v2 pagination list 2024-11-12 15:20:02 +05:30
69 changed files with 328 additions and 594 deletions
+2 -2
View File
@@ -83,7 +83,7 @@ jobs:
endpoint: ${{ env.BUILDX_ENDPOINT }}
- name: Build and Push to Docker Hub
uses: docker/build-push-action@v6.9.0
uses: docker/build-push-action@v5.1.0
with:
context: ./aio
file: ./aio/Dockerfile-base-full
@@ -124,7 +124,7 @@ jobs:
endpoint: ${{ env.BUILDX_ENDPOINT }}
- name: Build and Push to Docker Hub
uses: docker/build-push-action@v6.9.0
uses: docker/build-push-action@v5.1.0
with:
context: ./aio
file: ./aio/Dockerfile-base-slim
+2 -2
View File
@@ -128,7 +128,7 @@ jobs:
uses: actions/checkout@v4
- name: Build and Push to Docker Hub
uses: docker/build-push-action@v6.9.0
uses: docker/build-push-action@v5.1.0
with:
context: .
file: ./aio/Dockerfile-app
@@ -188,7 +188,7 @@ jobs:
uses: actions/checkout@v4
- name: Build and Push to Docker Hub
uses: docker/build-push-action@v6.9.0
uses: docker/build-push-action@v5.1.0
with:
context: .
file: ./aio/Dockerfile-app
+1 -1
View File
@@ -367,7 +367,7 @@ jobs:
- name: Create Release
id: create_release
uses: softprops/action-gh-release@v2.1.0
uses: softprops/action-gh-release@v2.0.8
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token
with:
+4 -4
View File
@@ -29,11 +29,11 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -46,7 +46,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v3
uses: github/codeql-action/autobuild@v2
# ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@@ -59,6 +59,6 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
uses: github/codeql-action/analyze@v2
with:
category: "/language:${{matrix.language}}"
+1 -1
View File
@@ -79,7 +79,7 @@ jobs:
uses: actions/checkout@v4
- name: Build and Push to Docker Hub
uses: docker/build-push-action@v6.9.0
uses: docker/build-push-action@v5.1.0
with:
context: .
file: ./aio/Dockerfile-app
+1 -1
View File
@@ -21,7 +21,7 @@ jobs:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v4.1.1
with:
fetch-depth: 0 # Fetch all history for all branches and tags
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
contents: read
steps:
- name: Checkout Code
uses: actions/checkout@v4
uses: actions/checkout@v4.1.1
with:
persist-credentials: false
fetch-depth: 0
+4 -27
View File
@@ -146,12 +146,7 @@ class UserAssetsV2Endpoint(BaseAPIView):
)
# Check if the file type is allowed
allowed_types = [
"image/jpeg",
"image/png",
"image/webp",
"image/jpg",
]
allowed_types = ["image/jpeg", "image/png", "image/webp", "image/jpg"]
if type not in allowed_types:
return Response(
{
@@ -322,7 +317,7 @@ class WorkspaceFileAssetEndpoint(BaseAPIView):
# Project Cover
elif entity_type == FileAsset.EntityTypeContext.PROJECT_COVER:
project = Project.objects.filter(id=asset.project_id).first()
project = Project.objects.filter(id=asset.workspace_id).first()
if project is None:
return
# Delete the previous cover image
@@ -392,13 +387,7 @@ class WorkspaceFileAssetEndpoint(BaseAPIView):
)
# Check if the file type is allowed
allowed_types = [
"image/jpeg",
"image/png",
"image/webp",
"image/jpg",
"image/gif",
]
allowed_types = ["image/jpeg", "image/png", "image/webp", "image/jpg"]
if type not in allowed_types:
return Response(
{
@@ -631,13 +620,7 @@ class ProjectAssetEndpoint(BaseAPIView):
)
# Check if the file type is allowed
allowed_types = [
"image/jpeg",
"image/png",
"image/webp",
"image/jpg",
"image/gif",
]
allowed_types = ["image/jpeg", "image/png", "image/webp", "image/jpg"]
if type not in allowed_types:
return Response(
{
@@ -755,11 +738,6 @@ class ProjectAssetEndpoint(BaseAPIView):
class ProjectBulkAssetEndpoint(BaseAPIView):
def save_project_cover(self, asset, project_id):
project = Project.objects.get(id=project_id)
project.cover_image_asset_id = asset.id
project.save()
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def post(self, request, slug, project_id, entity_id):
asset_ids = request.data.get("asset_ids", [])
@@ -795,7 +773,6 @@ class ProjectBulkAssetEndpoint(BaseAPIView):
assets.update(
project_id=project_id,
)
[self.save_project_cover(asset, project_id) for asset in assets]
if asset.entity_type == FileAsset.EntityTypeContext.ISSUE_DESCRIPTION:
assets.update(
+30 -8
View File
@@ -1,5 +1,6 @@
# Python imports
import json
import base64
# Django imports
from django.contrib.postgres.aggregates import ArrayAgg
@@ -480,9 +481,7 @@ class IssueViewSet(BaseViewSet):
project = Project.objects.get(pk=project_id, workspace__slug=slug)
issue = (
Issue.objects.filter(
project_id=self.kwargs.get("project_id")
)
Issue.objects.filter(project_id=self.kwargs.get("project_id"))
.filter(workspace__slug=self.kwargs.get("slug"))
.select_related("workspace", "project", "state", "parent")
.prefetch_related("assignees", "labels", "issue_module__module")
@@ -517,7 +516,7 @@ class IssueViewSet(BaseViewSet):
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
)
.filter(pk=pk)
.annotate(
label_ids=Coalesce(
@@ -852,9 +851,21 @@ class IssuePaginatedViewSet(BaseViewSet):
)
).distinct()
def process_paginated_result(self, fields, results, timezone):
def process_paginated_result(
self, fields, results, timezone, description_binary_required=False
):
paginated_data = results.values(*fields)
# handling the description binary field
if description_binary_required:
for item in paginated_data:
if item["description_binary"]:
item["description_binary"] = base64.b64encode(
item["description_binary"]
).decode("utf-8")
else:
item["description_binary"] = None
# converting the datetime fields in paginated data
datetime_fields = ["created_at", "updated_at"]
paginated_data = user_timezone_converter(
@@ -867,6 +878,9 @@ class IssuePaginatedViewSet(BaseViewSet):
def list(self, request, slug, project_id):
cursor = request.GET.get("cursor", None)
is_description_required = request.GET.get("description", "false")
is_description_binary_required = request.GET.get(
"description_binary", "false"
)
updated_at = request.GET.get("updated_at__gt", None)
# required fields
@@ -902,6 +916,9 @@ class IssuePaginatedViewSet(BaseViewSet):
if str(is_description_required).lower() == "true":
required_fields.append("description_html")
if str(is_description_binary_required).lower() == "true":
required_fields.append("description_binary")
# querying issues
base_queryset = Issue.issue_objects.filter(
workspace__slug=slug, project_id=project_id
@@ -966,12 +983,19 @@ class IssuePaginatedViewSet(BaseViewSet):
),
)
is_description_binary_required = False
if required_fields.__contains__("description_binary"):
is_description_binary_required = True
paginated_data = paginate(
base_queryset=base_queryset,
queryset=queryset,
cursor=cursor,
on_result=lambda results: self.process_paginated_result(
required_fields, results, request.user.user_timezone
required_fields,
results,
request.user.user_timezone,
is_description_binary_required,
),
)
@@ -1077,7 +1101,6 @@ class IssueDetailEndpoint(BaseAPIView):
class IssueBulkUpdateDateEndpoint(BaseAPIView):
def validate_dates(
self, current_start, current_target, new_start, new_target
):
@@ -1093,7 +1116,6 @@ class IssueBulkUpdateDateEndpoint(BaseAPIView):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
def post(self, request, slug, project_id):
updates = request.data.get("updates", [])
issue_ids = [update["id"] for update in updates]
+17
View File
@@ -32,6 +32,7 @@ from plane.db.models import (
Session,
)
from plane.license.models import Instance, InstanceAdmin
from plane.utils.cache import cache_response, invalidate_cache
from plane.utils.paginator import BasePaginator
from plane.authentication.utils.host import user_ip
from plane.bgtasks.user_deactivation_email_task import user_deactivation_email
@@ -48,6 +49,7 @@ class UserEndpoint(BaseViewSet):
def get_object(self):
return self.request.user
@cache_response(60 * 60)
@method_decorator(cache_control(private=True, max_age=12))
@method_decorator(vary_on_cookie)
def retrieve(self, request):
@@ -57,12 +59,14 @@ class UserEndpoint(BaseViewSet):
status=status.HTTP_200_OK,
)
@cache_response(60 * 60)
@method_decorator(cache_control(private=True, max_age=12))
@method_decorator(vary_on_cookie)
def retrieve_user_settings(self, request):
serialized_data = UserMeSettingsSerializer(request.user).data
return Response(serialized_data, status=status.HTTP_200_OK)
@cache_response(60 * 60)
def retrieve_instance_admin(self, request):
instance = Instance.objects.first()
is_admin = InstanceAdmin.objects.filter(
@@ -72,9 +76,19 @@ class UserEndpoint(BaseViewSet):
{"is_instance_admin": is_admin}, status=status.HTTP_200_OK
)
@invalidate_cache(
path="/api/users/me/",
)
@invalidate_cache(
path="/api/users/me/settings/",
)
def partial_update(self, request, *args, **kwargs):
return super().partial_update(request, *args, **kwargs)
@invalidate_cache(path="/api/users/me/")
@invalidate_cache(
path="/api/users/me/workspaces/", multiple=True, user=False
)
def deactivate(self, request):
# Check all workspace user is active
user = self.get_object()
@@ -221,6 +235,7 @@ class UserSessionEndpoint(BaseAPIView):
class UpdateUserOnBoardedEndpoint(BaseAPIView):
@invalidate_cache(path="/api/users/me/")
def patch(self, request):
profile = Profile.objects.get(user_id=request.user.id)
profile.is_onboarded = request.data.get("is_onboarded", False)
@@ -232,6 +247,7 @@ class UpdateUserOnBoardedEndpoint(BaseAPIView):
class UpdateUserTourCompletedEndpoint(BaseAPIView):
@invalidate_cache(path="/api/users/me/")
def patch(self, request):
profile = Profile.objects.get(user_id=request.user.id)
profile.is_tour_completed = request.data.get(
@@ -289,6 +305,7 @@ class ProfileEndpoint(BaseAPIView):
serializer = ProfileSerializer(profile)
return Response(serializer.data, status=status.HTTP_200_OK)
@invalidate_cache("/api/users/me/settings/")
def patch(self, request):
profile = Profile.objects.get(user=request.user)
serializer = ProfileSerializer(
@@ -44,6 +44,7 @@ from plane.db.models import (
WorkspaceTheme,
)
from plane.app.permissions import ROLE, allow_permission
from plane.utils.cache import cache_response, invalidate_cache
from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_control
from django.views.decorators.vary import vary_on_cookie
@@ -98,6 +99,9 @@ class WorkSpaceViewSet(BaseViewSet):
.select_related("owner")
)
@invalidate_cache(path="/api/workspaces/", user=False)
@invalidate_cache(path="/api/users/me/workspaces/")
@invalidate_cache(path="/api/instances/", user=False)
def create(self, request):
try:
serializer = WorkSpaceSerializer(data=request.data)
@@ -143,6 +147,7 @@ class WorkSpaceViewSet(BaseViewSet):
status=status.HTTP_410_GONE,
)
@cache_response(60 * 60 * 2)
@allow_permission(
[
ROLE.ADMIN,
@@ -154,6 +159,8 @@ class WorkSpaceViewSet(BaseViewSet):
def list(self, request, *args, **kwargs):
return super().list(request, *args, **kwargs)
@invalidate_cache(path="/api/workspaces/", user=False)
@invalidate_cache(path="/api/users/me/workspaces/")
@allow_permission(
[
ROLE.ADMIN,
@@ -163,6 +170,13 @@ class WorkSpaceViewSet(BaseViewSet):
def partial_update(self, request, *args, **kwargs):
return super().partial_update(request, *args, **kwargs)
@invalidate_cache(path="/api/workspaces/", user=False)
@invalidate_cache(
path="/api/users/me/workspaces/", multiple=True, user=False
)
@invalidate_cache(
path="/api/users/me/settings/", multiple=True, user=False
)
@allow_permission([ROLE.ADMIN], level="WORKSPACE")
def destroy(self, request, *args, **kwargs):
return super().destroy(request, *args, **kwargs)
@@ -176,6 +190,7 @@ class UserWorkSpacesEndpoint(BaseAPIView):
"owner",
]
@cache_response(60 * 60 * 2)
@method_decorator(cache_control(private=True, max_age=12))
@method_decorator(vary_on_cookie)
def get(self, request):
+18 -1
View File
@@ -40,7 +40,7 @@ from plane.db.models import (
WorkspaceMember,
DraftIssue,
)
from plane.utils.cache import invalidate_cache
from plane.utils.cache import cache_response, invalidate_cache
from .. import BaseViewSet
@@ -66,6 +66,7 @@ class WorkSpaceMemberViewSet(BaseViewSet):
.select_related("member")
)
@cache_response(60 * 60 * 2)
@allow_permission(
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
)
@@ -92,6 +93,12 @@ class WorkSpaceMemberViewSet(BaseViewSet):
)
return Response(serializer.data, status=status.HTTP_200_OK)
@invalidate_cache(
path="/api/workspaces/:slug/members/",
url_params=True,
user=False,
multiple=True,
)
@allow_permission(allowed_roles=[ROLE.ADMIN], level="WORKSPACE")
def partial_update(self, request, slug, pk):
workspace_member = WorkspaceMember.objects.get(
@@ -120,6 +127,16 @@ class WorkSpaceMemberViewSet(BaseViewSet):
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@invalidate_cache(
path="/api/workspaces/:slug/members/",
url_params=True,
user=False,
multiple=True,
)
@invalidate_cache(path="/api/users/me/settings/", multiple=True)
@invalidate_cache(
path="/api/users/me/workspaces/", user=False, multiple=True
)
@allow_permission(allowed_roles=[ROLE.ADMIN], level="WORKSPACE")
def destroy(self, request, slug, pk):
# Check the user role who is deleting the user
+1 -1
View File
@@ -42,7 +42,7 @@ app.conf.beat_schedule = {
},
"run-every-6-hours-for-instance-trace": {
"task": "plane.license.bgtasks.tracer.instance_traces",
"schedule": crontab(hour="*/6", minute=0),
"schedule": crontab(hour="*/6"),
},
}
@@ -13,6 +13,7 @@ class InstanceSerializer(BaseSerializer):
model = Instance
exclude = [
"license_key",
"user_count"
]
read_only_fields = [
"id",
+1 -20
View File
@@ -3,7 +3,7 @@ from celery import shared_task
from opentelemetry import trace
# Module imports
from plane.license.models import Instance, InstanceAdmin
from plane.license.models import Instance
from plane.db.models import (
User,
Workspace,
@@ -25,7 +25,6 @@ def instance_traces():
# Check if the instance is registered
instance = Instance.objects.first()
instance_admin = InstanceAdmin.objects.first()
# If instance is None then return
if instance is None:
@@ -53,17 +52,6 @@ def instance_traces():
span.set_attribute(
"is_telemetry_enabled", instance.is_telemetry_enabled
)
span.set_attribute(
"is_support_required", instance.is_support_required
)
span.set_attribute("is_setup_done", instance.is_setup_done)
span.set_attribute(
"is_signup_screen_visited", instance.is_signup_screen_visited
)
span.set_attribute("is_verified", instance.is_verified)
span.set_attribute("edition", instance.edition)
span.set_attribute("domain", instance.domain)
span.set_attribute("is_test", instance.is_test)
span.set_attribute("user_count", user_count)
span.set_attribute("workspace_count", workspace_count)
span.set_attribute("project_count", project_count)
@@ -74,13 +62,6 @@ def instance_traces():
span.set_attribute("module_issue_count", module_issue_count)
span.set_attribute("page_count", page_count)
if instance_admin:
span.set_attribute("admin_email", instance_admin.user.email)
span.set_attribute(
"admin_name",
f"{instance_admin.user.first_name} {instance_admin.user.last_name}",
)
# Workspace details
for workspace in Workspace.objects.all():
# Count of all models
@@ -1,7 +1,6 @@
# Python imports
import json
import secrets
import os
# Django imports
from django.core.management.base import BaseCommand, CommandError
@@ -9,7 +8,10 @@ from django.utils import timezone
from django.conf import settings
# Module imports
from plane.license.models import Instance, EditiontTypes
from plane.license.models import Instance
from plane.db.models import (
User,
)
from plane.license.bgtasks.tracer import instance_traces
@@ -30,6 +32,7 @@ class Command(BaseCommand):
payload = {
"instance_key": settings.INSTANCE_KEY,
"version": data.get("version", 0.1),
"user_count": User.objects.filter(is_bot=False).count(),
}
return payload
@@ -51,11 +54,11 @@ class Command(BaseCommand):
instance = Instance.objects.create(
instance_name="Plane Community Edition",
instance_id=secrets.token_hex(12),
license_key=None,
current_version=payload.get("version"),
latest_version=payload.get("version"),
last_checked_at=timezone.now(),
is_test=os.environ.get("IS_TEST", "0") == "1",
edition=EditiontTypes.PLANE_CE.value,
user_count=payload.get("user_count", 0),
)
self.stdout.write(self.style.SUCCESS("Instance registered"))
@@ -66,10 +69,9 @@ class Command(BaseCommand):
payload = self.read_package_json()
# Update the instance details
instance.last_checked_at = timezone.now()
instance.user_count = payload.get("user_count", 0)
instance.current_version = payload.get("version")
instance.latest_version = payload.get("version")
instance.is_test = os.environ.get("IS_TEST", "0") == "1"
instance.edition = EditiontTypes.PLANE_CE.value
instance.save()
# Call the instance traces task
@@ -1,31 +0,0 @@
# Generated by Django 4.2.15 on 2024-11-19 14:04
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('license', '0004_changelog_deleted_at_instance_deleted_at_and_more'),
]
operations = [
migrations.RenameField(
model_name='instance',
old_name='product',
new_name='edition',
),
migrations.RemoveField(
model_name='instance',
name='license_key',
),
migrations.RemoveField(
model_name='instance',
name='user_count',
),
migrations.AddField(
model_name='instance',
name='is_test',
field=models.BooleanField(default=False),
),
]
+1 -6
View File
@@ -1,6 +1 @@
from .instance import (
Instance,
InstanceAdmin,
InstanceConfiguration,
EditiontTypes,
)
from .instance import Instance, InstanceAdmin, InstanceConfiguration
+6 -4
View File
@@ -11,7 +11,7 @@ from plane.db.models import BaseModel
ROLE_CHOICES = ((20, "Admin"),)
class EditiontTypes(Enum):
class ProductTypes(Enum):
PLANE_CE = "plane-ce"
@@ -20,10 +20,11 @@ class Instance(BaseModel):
instance_name = models.CharField(max_length=255)
whitelist_emails = models.TextField(blank=True, null=True)
instance_id = models.CharField(max_length=255, unique=True)
license_key = models.CharField(max_length=256, null=True, blank=True)
current_version = models.CharField(max_length=255)
latest_version = models.CharField(max_length=255, null=True, blank=True)
edition = models.CharField(
max_length=255, default=EditiontTypes.PLANE_CE.value
product = models.CharField(
max_length=255, default=ProductTypes.PLANE_CE.value
)
domain = models.TextField(blank=True)
# Instance specifics
@@ -36,8 +37,9 @@ class Instance(BaseModel):
is_setup_done = models.BooleanField(default=False)
# signup screen
is_signup_screen_visited = models.BooleanField(default=False)
# users
user_count = models.PositiveBigIntegerField(default=0)
is_verified = models.BooleanField(default=False)
is_test = models.BooleanField(default=False)
class Meta:
verbose_name = "Instance"
+2 -2
View File
@@ -35,6 +35,8 @@ SECRET_KEY = os.environ.get("SECRET_KEY", get_random_secret_key())
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = int(os.environ.get("DEBUG", "0"))
# Initialize Django instrumentation
DjangoInstrumentor().instrument()
# Configure the tracer provider
service_name = os.environ.get("SERVICE_NAME", "plane-ce-api")
resource = Resource.create({"service.name": service_name})
@@ -44,8 +46,6 @@ otel_endpoint = os.environ.get("OTLP_ENDPOINT", "https://telemetry.plane.so")
otlp_exporter = OTLPSpanExporter(endpoint=otel_endpoint)
span_processor = BatchSpanProcessor(otlp_exporter)
trace.get_tracer_provider().add_span_processor(span_processor)
# Initialize Django instrumentation
DjangoInstrumentor().instrument()
# Allowed Hosts
+4 -4
View File
@@ -63,7 +63,7 @@ pytz==2024.1
# jwt
PyJWT==2.8.0
# OpenTelemetry
opentelemetry-api==1.28.1
opentelemetry-sdk==1.28.1
opentelemetry-instrumentation-django==0.49b1
opentelemetry-exporter-otlp==1.28.1
opentelemetry-api==1.27.0
opentelemetry-sdk==1.27.0
opentelemetry-instrumentation-django==0.48b0
opentelemetry-exporter-otlp==1.27.0
+1 -1
View File
@@ -22,7 +22,7 @@
"devDependencies": {
"prettier": "latest",
"prettier-plugin-tailwindcss": "^0.5.4",
"turbo": "^2.3.0"
"turbo": "^2.1.1"
},
"packageManager": "yarn@1.22.22",
"name": "plane"
+4 -12
View File
@@ -27,18 +27,10 @@
}
}
.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 th {
font-weight: 500;
text-align: left;
background-color: rgba(var(--color-background-90));
}
.table-wrapper table .selectedCell {
+1 -5
View File
@@ -18,11 +18,7 @@ export interface IProject {
close_in: number;
created_at: Date;
created_by: string;
// only for uploading the cover image
cover_image_asset?: null;
cover_image?: string;
// only for rendering the cover image
cover_image_url: readonly string;
cover_image_url: string;
cycle_view: boolean;
issue_views_view: boolean;
module_view: boolean;
+4 -13
View File
@@ -3,6 +3,7 @@ import { TUserPermissions } from "./enums";
type TLoginMediums = "email" | "magic-code" | "github" | "gitlab" | "google";
export interface IUserLite {
avatar_url: string;
display_name: string;
@@ -13,11 +14,7 @@ export interface IUserLite {
last_name: string;
}
export interface IUser extends IUserLite {
// only for uploading the cover image
cover_image_asset?: string | null;
cover_image?: string | null;
// only for rendering the cover image
cover_image_url: readonly (string | null);
cover_image_url: string | null;
date_joined: string;
email: string;
is_active: boolean;
@@ -93,6 +90,7 @@ export interface IUserTheme {
sidebarBackground: string | undefined;
}
export interface IUserMemberLite extends IUserLite {
email?: string;
}
@@ -155,14 +153,7 @@ export interface IUserProfileProjectSegregation {
id: string;
pending_issues: number;
}[];
user_data: Pick<
IUser,
| "avatar_url"
| "cover_image_url"
| "display_name"
| "first_name"
| "last_name"
> & {
user_data: Pick<IUser, "avatar_url" | "cover_image_url" | "display_name" | "first_name" | "last_name"> & {
date_joined: Date;
user_timezone: string;
};
+2 -2
View File
@@ -40,8 +40,8 @@ export type TIssueOrderByOptions =
| "-issue_cycle__cycle__name"
| "target_date"
| "-target_date"
| "estimate_point__key"
| "-estimate_point__key"
| "estimate_point"
| "-estimate_point"
| "start_date"
| "-start_date"
| "link_count"
@@ -1,6 +1,6 @@
"use client";
import { useCallback, useEffect } from "react";
import { useEffect } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import useSWR from "swr";
@@ -62,11 +62,6 @@ const WorkspaceDashboardPage = observer(() => {
workspace_slug && project_id && is_inbox_issue ? () => fetchUserProjectInfo(workspace_slug, project_id) : null
);
const embedRemoveCurrentNotification = useCallback(
() => setCurrentSelectedNotificationId(undefined),
[setCurrentSelectedNotificationId]
);
// clearing up the selected notifications when unmounting the page
useEffect(
() => () => {
@@ -100,12 +95,15 @@ const WorkspaceDashboardPage = observer(() => {
projectId={project_id}
inboxIssueId={issue_id}
isNotificationEmbed
embedRemoveCurrentNotification={embedRemoveCurrentNotification}
embedRemoveCurrentNotification={() => setCurrentSelectedNotificationId(undefined)}
/>
)}
</>
) : (
<IssuePeekOverview embedIssue embedRemoveCurrentNotification={embedRemoveCurrentNotification} />
<IssuePeekOverview
embedIssue
embedRemoveCurrentNotification={() => setCurrentSelectedNotificationId(undefined)}
/>
)}
</>
)}
+1 -5
View File
@@ -70,15 +70,11 @@ const ProfileSettingsPage = observer(() => {
first_name: formData.first_name,
last_name: formData.last_name,
avatar_url: formData.avatar_url,
cover_image_url: formData.cover_image_url,
role: formData.role,
display_name: formData?.display_name,
user_timezone: formData.user_timezone,
};
// if unsplash or a pre-defined image is uploaded, delete the old uploaded asset
if (formData.cover_image_url?.startsWith("http")) {
payload.cover_image = formData.cover_image_url;
payload.cover_image_asset = null;
}
const updateCurrentUserDetail = updateCurrentUser(payload).finally(() => setIsLoading(false));
setPromiseToast(updateCurrentUserDetail, {
-1
View File
@@ -1 +0,0 @@
export * from "./maintenance-message";
@@ -1,6 +0,0 @@
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>
);
+5
View File
@@ -0,0 +1,5 @@
"use client";
import { FC, Fragment } from "react";
export const MaintenanceMode: FC = () => <Fragment />;
@@ -74,11 +74,6 @@ export const CreateProjectForm: FC<TCreateProjectFormProps> = observer((props) =
// Upper case identifier
formData.identifier = formData.identifier?.toUpperCase();
const coverImage = formData.cover_image_url;
// if unsplash or a pre-defined image is uploaded, delete the old uploaded asset
if (coverImage?.startsWith("http")) {
formData.cover_image = coverImage;
formData.cover_image_asset = null;
}
return createProject(workspaceSlug.toString(), formData)
.then(async (res) => {
+2 -16
View File
@@ -5,11 +5,7 @@ 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,
getPositionFromDate,
} from "@/components/gantt-chart/views/helpers";
import { getDateFromPositionOnGantt, getItemPositionWidth } from "@/components/gantt-chart/views/helpers";
// helpers
import { renderFormattedPayloadDate } from "@/helpers/date-time.helper";
// store
@@ -51,7 +47,6 @@ 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 {
@@ -191,7 +186,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);
}
@@ -232,15 +227,6 @@ 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,11 +46,10 @@ export const GanttChartBlock: React.FC<Props> = observer((props) => {
const { isMoving, handleBlockDrag } = useGanttResizable(block, resizableRef, ganttContainerRef, updateBlockDates);
const isBlockVisibleOnChart = block?.start_date || block?.target_date;
const isBlockComplete = 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 || (!showAllBlocks && !(block.start_date && block.target_date))) return null;
const isBlockVisibleOnChart = block.start_date && block.target_date;
if (!block.data) return null;
@@ -64,7 +63,7 @@ export const GanttChartBlock: React.FC<Props> = observer((props) => {
ref={resizableRef}
style={{
height: `${BLOCK_HEIGHT}px`,
marginLeft: `${block.position?.marginLeft}px`,
transform: `translateX(${block.position?.marginLeft}px)`,
width: `${block.position?.width}px`,
}}
>
@@ -89,7 +88,7 @@ export const GanttChartBlock: React.FC<Props> = observer((props) => {
handleBlockDrag={handleBlockDrag}
enableBlockLeftResize={enableBlockLeftResize}
enableBlockRightResize={enableBlockRightResize}
enableBlockMove={enableBlockMove && !!isBlockComplete}
enableBlockMove={enableBlockMove}
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 { DEFAULT_BLOCK_WIDTH, GANTT_SELECT_GROUP, HEADER_HEIGHT } from "../constants";
import { GANTT_SELECT_GROUP, HEADER_HEIGHT } from "../constants";
import { getItemPositionWidth } from "../views";
import { TimelineDragHelper } from "./timeline-drag-helper";
@@ -108,20 +108,14 @@ export const GanttChartMainContent: React.FC<Props> = observer((props) => {
const approxRangeLeft = scrollLeft;
const approxRangeRight = scrollWidth - (scrollLeft + clientWidth);
const calculatedRangeRight = itemsContainerWidth - (scrollLeft + clientWidth);
if (approxRangeRight < clientWidth || calculatedRangeRight < clientWidth) {
updateCurrentViewRenderPayload("right", currentView);
}
if (approxRangeLeft < clientWidth) {
updateCurrentViewRenderPayload("left", currentView);
}
if (approxRangeRight < clientWidth) updateCurrentViewRenderPayload("right", currentView);
if (approxRangeLeft < clientWidth) updateCurrentViewRenderPayload("left", currentView);
};
const handleScrollToBlock = (block: IGanttBlock) => {
const scrollContainer = ganttContainerRef.current as HTMLDivElement;
const scrollToEndDate = !block.start_date && block.target_date;
const scrollToDate = block.start_date ? getDate(block.start_date) : getDate(block.target_date);
const scrollToDate = getDate(block.start_date);
let chartData;
if (!scrollContainer || !currentViewData || !scrollToDate) return;
@@ -135,8 +129,7 @@ export const GanttChartMainContent: React.FC<Props> = observer((props) => {
const updatedPosition = getItemPositionWidth(chartData ?? currentViewData, block);
setTimeout(() => {
if (updatedPosition)
scrollContainer.scrollLeft = updatedPosition.marginLeft - 4 - (scrollToEndDate ? DEFAULT_BLOCK_WIDTH : 0);
if (updatedPosition) scrollContainer.scrollLeft = updatedPosition.marginLeft - 4;
});
};
@@ -196,7 +189,6 @@ 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 min-h-full h-max w-max flex`}>
<div className={`absolute top-0 left-0 h-max w-max flex`} style={{ minHeight: `calc(100% + ${HEADER_HEIGHT}px` }}>
{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 min-h-full h-max w-max flex`}>
<div className={`absolute top-0 left-0 h-max w-max flex`} style={{ minHeight: `calc(100% + ${HEADER_HEIGHT}px` }}>
{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 min-h-full h-max w-max flex`}>
<div className={`absolute top-0 left-0 h-max w-max flex`} style={{ minHeight: `calc(100% + ${HEADER_HEIGHT}px` }}>
{currentViewData &&
weekBlocks?.map((block, rootIndex) => (
<div
@@ -6,6 +6,4 @@ 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.marginLeft.slice(0, -2));
const prevMarginLeft = parseFloat(resizableDiv.style.transform.slice(11, -3));
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.marginLeft = `${marginLeft}px`;
resizableDiv.style.transform = `translateX(${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, dragInstanceId: "GANTT_REORDER" }),
getInitialData: () => ({ id }),
onDragStart: () => {
setIsDragging(true);
},
@@ -44,7 +44,7 @@ export const GanttDnDHOC = observer((props: Props) => {
}),
dropTargetForElements({
element,
canDrop: ({ source }) => source?.data?.id !== id && source?.data?.dragInstanceId === "GANTT_REORDER",
canDrop: ({ source }) => source?.data?.id !== id,
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 isBlockComplete = !!block?.start_date && !!block?.target_date;
const duration = isBlockComplete ? getNumberOfDaysFromPosition(block?.position?.width) : undefined;
const isBlockVisibleOnChart = !!block?.start_date && !!block?.target_date;
const duration = isBlockVisibleOnChart ? 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 isBlockComplete = !!block.start_date && !!block.target_date;
const duration = isBlockComplete ? getNumberOfDaysFromPosition(block?.position?.width) : undefined;
const isBlockVisibleOnChart = !!block.start_date && !!block.target_date;
const duration = isBlockVisibleOnChart ? getNumberOfDaysFromPosition(block?.position?.width) : undefined;
return (
<div
@@ -1,6 +1,8 @@
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
@@ -82,7 +84,7 @@ export const getDateFromPositionOnGantt = (position: number, chartData: ChartDat
*/
export const getItemPositionWidth = (chartData: ChartDataType, itemData: IGanttBlock) => {
let scrollPosition: number = 0;
let scrollWidth: number = DEFAULT_BLOCK_WIDTH;
let scrollWidth: number = 0;
const { startDate: chartStartDate } = chartData.data;
const { start_date, target_date } = itemData;
@@ -90,42 +92,24 @@ export const getItemPositionWidth = (chartData: ChartDataType, itemData: IGanttB
const itemStartDate = getDate(start_date);
const itemTargetDate = getDate(target_date);
chartStartDate.setHours(0, 0, 0, 0);
itemStartDate?.setHours(0, 0, 0, 0);
itemTargetDate?.setHours(0, 0, 0, 0);
if (!itemStartDate || !itemTargetDate) return;
if (!itemStartDate && !itemTargetDate) return;
chartStartDate.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;
// get scroll position from the number of days and width of each day
scrollPosition = itemStartDate
? getPositionFromDate(chartData, itemStartDate, 0)
: getPositionFromDate(chartData, itemTargetDate!, -1 * DEFAULT_BLOCK_WIDTH);
scrollPosition = positionDaysDifference * 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;
}
// 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
View File
@@ -1,2 +1 @@
export * from "./not-ready-view";
export * from "./maintenance-view";
@@ -1,34 +0,0 @@
"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,7 +27,6 @@ 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>) => {
@@ -45,7 +44,6 @@ export const useLinkOperations = (workspaceSlug: string, projectId: string, issu
type: TOAST_TYPE.ERROR,
title: "Link not updated",
});
throw error;
}
},
remove: async (linkId: string) => {
@@ -7,6 +7,8 @@ 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
@@ -46,18 +48,14 @@ export const IssueLinkCreateUpdateModal: FC<TIssueLinkCreateEditModal> = observe
const onClose = () => {
setIssueLinkData(null);
reset();
if (handleOnClose) handleOnClose();
};
const handleFormSubmit = async (formData: TIssueLinkCreateFormFieldOptions) => {
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);
}
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();
};
useEffect(() => {
@@ -79,6 +77,7 @@ 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,7 +58,6 @@ export const IssueLinkRoot: FC<TIssueLinkRoot> = (props) => {
type: TOAST_TYPE.ERROR,
title: "Link not created",
});
throw error;
}
},
update: async (linkId: string, data: Partial<TIssueLink>) => {
@@ -77,7 +76,6 @@ export const IssueLinkRoot: FC<TIssueLinkRoot> = (props) => {
type: TOAST_TYPE.ERROR,
title: "Link not updated",
});
throw error;
}
},
remove: async (linkId: string) => {
@@ -4,8 +4,6 @@ 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
@@ -15,8 +13,7 @@ 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";
//
import { getBlockViewDetails } from "../utils";
// local types
import { GanttStoreType } from "./base-gantt-root";
type Props = {
@@ -42,37 +39,36 @@ 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 (
<Tooltip
isMobile={isMobile}
tooltipContent={
<div className="space-y-1">
<h5>{issueDetails?.name}</h5>
<div>{message}</div>
</div>
}
position="top-left"
disabled={!message}
<div
id={`issue-${issueId}`}
className="relative flex h-full w-full cursor-pointer items-center rounded"
style={{
backgroundColor: stateDetails?.color,
}}
onClick={handleIssuePeekOverview}
>
<div
id={`issue-${issueId}`}
className="relative flex h-full w-full cursor-pointer items-center rounded"
style={blockStyle}
onClick={handleIssuePeekOverview}
<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 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` }}
>
<div className="relative w-full overflow-hidden truncate px-2.5 py-1 text-sm text-custom-text-100">
{issueDetails?.name}
</div>
</div>
</Tooltip>
</Tooltip>
</div>
);
});
@@ -96,11 +92,7 @@ export const IssueGanttSidebarBlock: React.FC<Props> = observer((props) => {
// derived values
const issueDetails = getIssueById(issueId);
const handleIssuePeekOverview = (e: any) => {
e.stopPropagation(true);
e.preventDefault();
handleRedirection(workspaceSlug, issueDetails, isMobile);
};
const handleIssuePeekOverview = () => handleRedirection(workspaceSlug, issueDetails, isMobile);
return (
<ControlLink
@@ -1,6 +1,5 @@
"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";
@@ -33,7 +32,6 @@ 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";
@@ -674,39 +672,3 @@ 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,13 +135,8 @@ 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?.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>
<ButtonAvatars showTooltip userIds={createdByDetails?.id} />
<span className="flex-grow truncate text-xs leading-5">{createdByDetails?.display_name}</span>
</div>
</div>
)}
@@ -3,14 +3,13 @@
import { observer } from "mobx-react";
import Link from "next/link";
import { useParams } from "next/navigation";
// hooks
// ui
import { Tooltip, ModuleStatusIcon } from "@plane/ui";
// components
import { SIDEBAR_WIDTH } from "@/components/gantt-chart/constants";
import { getBlockViewDetails } from "@/components/issues/issue-layouts/utils";
// constants
// helpers
import { MODULE_STATUS } from "@/constants/module";
// hooks
import { renderFormattedDate } from "@/helpers/date-time.helper";
// constants
import { useModule } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router";
import { usePlatformOS } from "@/hooks/use-platform-os";
@@ -31,40 +30,31 @@ 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 (
<Tooltip
isMobile={isMobile}
tooltipContent={
<div className="space-y-1">
<h5>{moduleDetails?.name}</h5>
<div>{message}</div>
</div>
<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}`)
}
position="top-left"
>
<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}`
)
<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>
}
position="top-left"
>
<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>
<div className="relative w-full truncate px-2.5 py-1 text-sm text-custom-text-100">{moduleDetails?.name}</div>
</Tooltip>
</div>
);
});
@@ -6,6 +6,8 @@ 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>;
@@ -37,10 +39,9 @@ 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: parsedUrl,
url: formData.url,
};
try {
@@ -91,6 +92,7 @@ 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
+1 -6
View File
@@ -144,15 +144,10 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
network: formData.network,
identifier: formData.identifier,
description: formData.description,
cover_image_url: formData.cover_image_url,
logo_props: formData.logo_props,
// timezone: formData.timezone,
};
// if unsplash or a pre-defined image is uploaded, delete the old uploaded asset
if (formData.cover_image_url?.startsWith("http")) {
payload.cover_image = formData.cover_image_url;
payload.cover_image_asset = null;
}
if (project.identifier !== formData.identifier)
await projectService
+2 -2
View File
@@ -79,9 +79,9 @@ export const SPREADSHEET_PROPERTY_DETAILS: {
},
estimate: {
title: "Estimate",
ascendingOrderKey: "estimate_point__key",
ascendingOrderKey: "estimate_point",
ascendingOrderTitle: "Low",
descendingOrderKey: "-estimate_point__key",
descendingOrderKey: "-estimate_point",
descendingOrderTitle: "High",
icon: Triangle,
Column: SpreadsheetEstimateColumn,
+4 -2
View File
@@ -3,9 +3,11 @@ import { observer } from "mobx-react";
import useSWR from "swr";
// components
import { LogoSpinner } from "@/components/common";
import { InstanceNotReady, MaintenanceView } from "@/components/instance";
import { InstanceNotReady } from "@/components/instance";
// hooks
import { useInstance } from "@/hooks/store";
// plane web components
import { MaintenanceMode } from "@/plane-web/components/maintenance-mode";
type TInstanceWrapper = {
children: ReactNode;
@@ -30,7 +32,7 @@ export const InstanceWrapper: FC<TInstanceWrapper> = observer((props) => {
</div>
);
if (instanceSWRError) return <MaintenanceView />;
if (instanceSWRError) return <MaintenanceMode />;
// something went wrong while in the request
if (error && error?.status === "error") return <>{children}</>;
-1
View File
@@ -300,7 +300,6 @@ 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[] = [];
+4 -7
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);
let objects: IEstimatePoint[] = [];
const objects: IEstimatePoint[] = [];
(estimates || []).forEach((estimate: IEstimate) => {
if (estimate?.points) {
objects = objects.concat(estimate.points);
objects.concat(estimate.points);
}
});
return objects;
@@ -104,9 +104,6 @@ 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));
@@ -115,7 +112,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, members] = await Promise.all(promises);
const [labels, modules, cycles, states, estimates, memebers] = await Promise.all(promises);
const start = performance.now();
await persistence.db.exec("BEGIN;");
@@ -124,7 +121,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(members, "members", memberSchema);
await batchInserts(memebers, "members", memberSchema);
await persistence.db.exec("COMMIT;");
const end = performance.now();
log("Time taken to load workspace data", end - start);
+8 -6
View File
@@ -18,8 +18,6 @@ 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 {
@@ -50,6 +48,8 @@ export const issueFilterQueryConstructor = (workspaceSlug: string, projectId: st
`;
log("###", sql);
return sql;
}
if (group_by) {
@@ -64,6 +64,8 @@ export const issueFilterQueryConstructor = (workspaceSlug: string, projectId: st
WHERE rank <= ${per_page}
`;
log("###", sql);
return sql;
}
@@ -76,10 +78,8 @@ 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__key")) {
if (order_by.includes("estimate_point")) {
sql += `
LEFT JOIN estimate_points s on fi.estimate_point = s.id`;
}
@@ -120,6 +120,7 @@ export const issueFilterQueryConstructor = (workspaceSlug: string, projectId: st
`;
sql += ` group by i.id ${orderByString} LIMIT ${pageSize} OFFSET ${offset * 1 + page * pageSize};`;
log("######$$$", sql);
return sql;
}
@@ -148,6 +149,7 @@ 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;
};
+1 -4
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,9 +336,6 @@ 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);
};
+1 -1
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;
const status = this.inboxFilters?.status && uniq([...this.inboxFilters.status, EInboxIssueStatus.SNOOZED]);
const queryParams = this.inboxIssueQueryParams(
{ ...this.inboxFilters, status },
this.inboxSorting,
@@ -36,7 +36,6 @@ 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";
@@ -165,8 +164,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__key: "estimate_point",
"-estimate_point__key": "estimate_point",
estimate_point: "estimate_point",
"-estimate_point": "estimate_point",
start_date: "start_date",
"-start_date": "start_date",
link_count: "link_count",
@@ -283,19 +282,6 @@ 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;
}
@@ -1715,14 +1701,13 @@ 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" | "estimate_point",
dataType: "state_id" | "label_ids" | "assignee_ids" | "module_ids" | "cycle_id",
dataIds: string | string[] | null | undefined,
projectId: string | undefined | null,
order?: "asc" | "desc"
) {
if (!dataIds) return;
const dataValues: (string | number)[] = [];
const dataValues: string[] = [];
const isDataIdsArray = Array.isArray(dataIds);
const dataIdsArray = isDataIdsArray ? dataIds : [dataIds];
@@ -1772,26 +1757,6 @@ 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;
@@ -1806,17 +1771,11 @@ 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"], issue?.["project_id"])
)
orderBy(array, (issue) => this.populateIssueDataForSorting("state_id", issue?.["state_id"]))
);
case "-state__name":
return getIssueIds(
orderBy(
array,
(issue) => this.populateIssueDataForSorting("state_id", issue?.["state_id"], issue?.["project_id"]),
["desc"]
)
orderBy(array, (issue) => this.populateIssueDataForSorting("state_id", issue?.["state_id"]), ["desc"])
);
// dates
case "created_at":
@@ -1867,23 +1826,15 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
case "-attachment_count":
return getIssueIds(orderBy(array, "attachment_count", ["desc"]));
case "estimate_point__key":
case "estimate_point":
return getIssueIds(
orderBy(array, [
getSortOrderToFilterEmptyValues.bind(null, "estimate_point"),
(issue) =>
this.populateIssueDataForSorting("estimate_point", issue?.["estimate_point"], issue?.["project_id"]),
])
orderBy(array, [getSortOrderToFilterEmptyValues.bind(null, "estimate_point"), "estimate_point"])
); //preferring sorting based on empty values to always keep the empty values below
case "-estimate_point__key":
case "-estimate_point":
return getIssueIds(
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
[getSortOrderToFilterEmptyValues.bind(null, "estimate_point"), "estimate_point"], //preferring sorting based on empty values to always keep the empty values below
["asc", "desc"]
)
);
@@ -1903,8 +1854,7 @@ 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"], issue?.["project_id"], "asc"),
(issue) => this.populateIssueDataForSorting("label_ids", issue?.["label_ids"], "asc"),
])
);
case "-labels__name":
@@ -1913,8 +1863,7 @@ 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"], issue?.["project_id"], "asc"),
(issue) => this.populateIssueDataForSorting("label_ids", issue?.["label_ids"], "asc"),
],
["asc", "desc"]
)
@@ -1924,8 +1873,7 @@ 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"], issue?.["project_id"], "asc"),
(issue) => this.populateIssueDataForSorting("module_ids", issue?.["module_ids"], "asc"),
])
);
case "-issue_module__module__name":
@@ -1934,8 +1882,7 @@ 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"], issue?.["project_id"], "asc"),
(issue) => this.populateIssueDataForSorting("module_ids", issue?.["module_ids"], "asc"),
],
["asc", "desc"]
)
@@ -1945,7 +1892,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"], issue?.["project_id"], "asc"),
(issue) => this.populateIssueDataForSorting("cycle_id", issue?.["cycle_id"], "asc"),
])
);
case "-issue_cycle__cycle__name":
@@ -1954,8 +1901,7 @@ 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"], issue?.["project_id"], "asc"),
(issue) => this.populateIssueDataForSorting("cycle_id", issue?.["cycle_id"], "asc"),
],
["asc", "desc"]
)
@@ -1965,8 +1911,7 @@ 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"], issue?.["project_id"], "asc"),
(issue) => this.populateIssueDataForSorting("assignee_ids", issue?.["assignee_ids"], "asc"),
])
);
case "-assignees__first_name":
@@ -1975,8 +1920,7 @@ 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"], issue?.["project_id"], "asc"),
(issue) => this.populateIssueDataForSorting("assignee_ids", issue?.["assignee_ids"], "asc"),
],
["asc", "desc"]
)
-1
View File
@@ -1 +0,0 @@
export * from "ce/components/instance";
@@ -1 +0,0 @@
export const MaintenanceMessage = () => <></>;
+1
View File
@@ -0,0 +1 @@
export * from "ce/components/maintenance-mode";
Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

+40 -65
View File
@@ -5647,9 +5647,9 @@ cross-fetch@^3.1.5:
node-fetch "^2.6.12"
cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3:
version "7.0.5"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.5.tgz#910aac880ff5243da96b728bc6521a5f6c2f2f82"
integrity sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==
version "7.0.3"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
dependencies:
path-key "^3.1.0"
shebang-command "^2.0.0"
@@ -11287,16 +11287,7 @@ streamx@^2.15.0, streamx@^2.20.0:
optionalDependencies:
bare-events "^2.2.0"
"string-width-cjs@npm:string-width@^4.2.0":
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -11384,14 +11375,7 @@ string_decoder@^1.1.1, string_decoder@^1.3.0:
dependencies:
safe-buffer "~5.2.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -11928,47 +11912,47 @@ tunnel-agent@^0.6.0:
dependencies:
safe-buffer "^5.0.1"
turbo-darwin-64@2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/turbo-darwin-64/-/turbo-darwin-64-2.3.0.tgz#cf82cf4a816a267c65a71d2d3ec1baef5c6b0f78"
integrity sha512-pji+D49PhFItyQjf2QVoLZw2d3oRGo8gJgKyOiRzvip78Rzie74quA8XNwSg/DuzM7xx6gJ3p2/LylTTlgZXxQ==
turbo-darwin-64@2.2.3:
version "2.2.3"
resolved "https://registry.yarnpkg.com/turbo-darwin-64/-/turbo-darwin-64-2.2.3.tgz#f0ced75ed031091e52851cbe8bb05d21a161a22b"
integrity sha512-Rcm10CuMKQGcdIBS3R/9PMeuYnv6beYIHqfZFeKWVYEWH69sauj4INs83zKMTUiZJ3/hWGZ4jet9AOwhsssLyg==
turbo-darwin-arm64@2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/turbo-darwin-arm64/-/turbo-darwin-arm64-2.3.0.tgz#3e058a4e41130abce9df49a1fb5e271af85a1d99"
integrity sha512-AJrGIL9BO41mwDF/IBHsNGwvtdyB911vp8f5mbNo1wG66gWTvOBg7WCtYQBvCo11XTenTfXPRSsAb7w3WAZb6w==
turbo-darwin-arm64@2.2.3:
version "2.2.3"
resolved "https://registry.yarnpkg.com/turbo-darwin-arm64/-/turbo-darwin-arm64-2.2.3.tgz#0b4741383ab5070d8383891a65861a8869cc7202"
integrity sha512-+EIMHkuLFqUdJYsA3roj66t9+9IciCajgj+DVek+QezEdOJKcRxlvDOS2BUaeN8kEzVSsNiAGnoysFWYw4K0HA==
turbo-linux-64@2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/turbo-linux-64/-/turbo-linux-64-2.3.0.tgz#0aefff6047faed0ffdbf0980d5dd4f11ace51d65"
integrity sha512-jZqW6vc2sPJT3M/3ZmV1Cg4ecQVPqsbHncG/RnogHpBu783KCSXIndgxvUQNm9qfgBYbZDBnP1md63O4UTElhw==
turbo-linux-64@2.2.3:
version "2.2.3"
resolved "https://registry.yarnpkg.com/turbo-linux-64/-/turbo-linux-64-2.2.3.tgz#2b339db50c12bc52ce99139c156d5555717a209d"
integrity sha512-UBhJCYnqtaeOBQLmLo8BAisWbc9v9daL9G8upLR+XGj6vuN/Nz6qUAhverN4Pyej1g4Nt1BhROnj6GLOPYyqxQ==
turbo-linux-arm64@2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/turbo-linux-arm64/-/turbo-linux-arm64-2.3.0.tgz#a00db7c7a88400cc0357bfeac2beb383a35e255e"
integrity sha512-HUbDLJlvd/hxuyCNO0BmEWYQj0TugRMvSQeG8vHJH+Lq8qOgDAe7J0K73bFNbZejZQxW3C3XEiZFB3pnpO78+A==
turbo-linux-arm64@2.2.3:
version "2.2.3"
resolved "https://registry.yarnpkg.com/turbo-linux-arm64/-/turbo-linux-arm64-2.2.3.tgz#a4daf6e0872a4e2652e2d05d68ad18cee5b10e94"
integrity sha512-hJYT9dN06XCQ3jBka/EWvvAETnHRs3xuO/rb5bESmDfG+d9yQjeTMlhRXKrr4eyIMt6cLDt1LBfyi+6CQ+VAwQ==
turbo-windows-64@2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/turbo-windows-64/-/turbo-windows-64-2.3.0.tgz#f082688f17c73d345efbdc43fb589b1df70cd53f"
integrity sha512-c5rxrGNTYDWX9QeMzWLFE9frOXnKjHGEvQMp1SfldDlbZYsloX9UKs31TzUThzfTgTiz8NYuShaXJ2UvTMnV/g==
turbo-windows-64@2.2.3:
version "2.2.3"
resolved "https://registry.yarnpkg.com/turbo-windows-64/-/turbo-windows-64-2.2.3.tgz#d44b3385948bd0f2ef5c2d53391f142bdd467b18"
integrity sha512-NPrjacrZypMBF31b4HE4ROg4P3nhMBPHKS5WTpMwf7wydZ8uvdEHpESVNMOtqhlp857zbnKYgP+yJF30H3N2dQ==
turbo-windows-arm64@2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/turbo-windows-arm64/-/turbo-windows-arm64-2.3.0.tgz#42d77fe99f72b4862bb4cbbb0cb5dca73427270a"
integrity sha512-7qfUuYhfIVb1AZgs89DxhXK+zZez6O2ocmixEQ4hXZK7ytnBt5vaz2zGNJJKFNYIL5HX1C3tuHolnpNgDNCUIg==
turbo-windows-arm64@2.2.3:
version "2.2.3"
resolved "https://registry.yarnpkg.com/turbo-windows-arm64/-/turbo-windows-arm64-2.2.3.tgz#d0625ec53f467013a6f259f87f7fc4ae8670aaa4"
integrity sha512-fnNrYBCqn6zgKPKLHu4sOkihBI/+0oYFr075duRxqUZ+1aLWTAGfHZLgjVeLh3zR37CVzuerGIPWAEkNhkWEIw==
turbo@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/turbo/-/turbo-2.3.0.tgz#01e7841fafdd870564e1ad376b42dbc8a71d52b3"
integrity sha512-/uOq5o2jwRPyaUDnwBpOR5k9mQq4c3wziBgWNWttiYQPmbhDtrKYPRBxTvA2WpgQwRIbt8UM612RMN8n/TvmHA==
turbo@^2.1.1:
version "2.2.3"
resolved "https://registry.yarnpkg.com/turbo/-/turbo-2.2.3.tgz#0f45612d62526c98c75da0682aa8c26b902b5e07"
integrity sha512-5lDvSqIxCYJ/BAd6rQGK/AzFRhBkbu4JHVMLmGh/hCb7U3CqSnr5Tjwfy9vc+/5wG2DJ6wttgAaA7MoCgvBKZQ==
optionalDependencies:
turbo-darwin-64 "2.3.0"
turbo-darwin-arm64 "2.3.0"
turbo-linux-64 "2.3.0"
turbo-linux-arm64 "2.3.0"
turbo-windows-64 "2.3.0"
turbo-windows-arm64 "2.3.0"
turbo-darwin-64 "2.2.3"
turbo-darwin-arm64 "2.2.3"
turbo-linux-64 "2.2.3"
turbo-linux-arm64 "2.2.3"
turbo-windows-64 "2.2.3"
turbo-windows-arm64 "2.2.3"
tween-functions@^1.2.0:
version "1.2.0"
@@ -12623,16 +12607,7 @@ word-wrap@^1.2.5:
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"
integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^7.0.0:
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==