Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 954167c5ff | |||
| 4d12a94b46 | |||
| b0f20f59eb | |||
| 38e8a5c807 | |||
| a9bd2e243a | |||
| ca0d50b229 | |||
| 7fca7fd86c | |||
| 0ac68f2731 | |||
| 5a9ae66680 | |||
| 134644fdf1 | |||
| d0f3987aeb | |||
| f06b1b8c4a | |||
| 6e56ea4c60 | |||
| 216a69f991 | |||
| b4d008d4bb | |||
| daf0d9d3a5 | |||
| 7cfb1f8ace | |||
| 205395e079 | |||
| 3654f24f8e | |||
| 7f66604cc2 | |||
| d8cf08df91 |
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "admin",
|
||||
"version": "0.24.0",
|
||||
"version": "0.24.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "turbo run develop",
|
||||
|
||||
@@ -4,7 +4,7 @@ FROM python:3.12.5-alpine AS backend
|
||||
ENV PYTHONDONTWRITEBYTECODE 1
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
|
||||
ENV INSTANCE_CHANGELOG_URL https://api.plane.so/api/public/anchor/8e1c2e4c7bc5493eb7731be3862f6960/pages/
|
||||
ENV INSTANCE_CHANGELOG_URL https://sites.plane.so/pages/691ef037bcfe416a902e48cb55f59891/
|
||||
|
||||
WORKDIR /code
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ FROM python:3.12.5-alpine AS backend
|
||||
ENV PYTHONDONTWRITEBYTECODE 1
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
|
||||
ENV INSTANCE_CHANGELOG_URL https://api.plane.so/api/public/anchor/8e1c2e4c7bc5493eb7731be3862f6960/pages/
|
||||
ENV INSTANCE_CHANGELOG_URL https://sites.plane.so/pages/691ef037bcfe416a902e48cb55f59891/
|
||||
|
||||
RUN apk --no-cache add \
|
||||
"bash~=5.2" \
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"name": "plane-api",
|
||||
"version": "0.24.0"
|
||||
"version": "0.24.1"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
# Python imports
|
||||
from typing import Optional
|
||||
|
||||
# Django imports
|
||||
from django.utils import timezone
|
||||
from django.db import transaction
|
||||
|
||||
# Third party imports
|
||||
from celery import shared_task
|
||||
|
||||
# Module imports
|
||||
from plane.db.models import Issue, IssueDescriptionVersion, ProjectMember
|
||||
from plane.utils.exception_logger import log_exception
|
||||
|
||||
|
||||
def get_owner_id(issue: Issue) -> Optional[int]:
|
||||
"""Get the owner ID of the issue"""
|
||||
|
||||
if issue.updated_by_id:
|
||||
return issue.updated_by_id
|
||||
|
||||
if issue.created_by_id:
|
||||
return issue.created_by_id
|
||||
|
||||
# Find project admin as fallback
|
||||
project_member = ProjectMember.objects.filter(
|
||||
project_id=issue.project_id,
|
||||
role=20, # Admin role
|
||||
).first()
|
||||
|
||||
return project_member.member_id if project_member else None
|
||||
|
||||
|
||||
@shared_task
|
||||
def sync_issue_description_version(batch_size=5000, offset=0, countdown=300):
|
||||
"""Task to create IssueDescriptionVersion records for existing Issues in batches"""
|
||||
try:
|
||||
with transaction.atomic():
|
||||
base_query = Issue.objects
|
||||
total_issues_count = base_query.count()
|
||||
|
||||
if total_issues_count == 0:
|
||||
return
|
||||
|
||||
# Calculate batch range
|
||||
end_offset = min(offset + batch_size, total_issues_count)
|
||||
|
||||
# Fetch issues with related data
|
||||
issues_batch = (
|
||||
base_query.order_by("created_at")
|
||||
.select_related("workspace", "project")
|
||||
.only(
|
||||
"id",
|
||||
"workspace_id",
|
||||
"project_id",
|
||||
"created_by_id",
|
||||
"updated_by_id",
|
||||
"description_binary",
|
||||
"description_html",
|
||||
"description_stripped",
|
||||
"description",
|
||||
)[offset:end_offset]
|
||||
)
|
||||
|
||||
if not issues_batch:
|
||||
return
|
||||
|
||||
version_objects = []
|
||||
for issue in issues_batch:
|
||||
# Validate required fields
|
||||
if not issue.workspace_id or not issue.project_id:
|
||||
print(f"Skipping {issue.id} - missing workspace_id or project_id")
|
||||
continue
|
||||
|
||||
# Determine owned_by_id
|
||||
owned_by_id = get_owner_id(issue)
|
||||
if owned_by_id is None:
|
||||
print(f"Skipping issue {issue.id} - missing owned_by")
|
||||
continue
|
||||
|
||||
# Create version object
|
||||
version_objects.append(
|
||||
IssueDescriptionVersion(
|
||||
workspace_id=issue.workspace_id,
|
||||
project_id=issue.project_id,
|
||||
created_by_id=issue.created_by_id,
|
||||
updated_by_id=issue.updated_by_id,
|
||||
owned_by_id=owned_by_id,
|
||||
last_saved_at=timezone.now(),
|
||||
issue_id=issue.id,
|
||||
description_binary=issue.description_binary,
|
||||
description_html=issue.description_html,
|
||||
description_stripped=issue.description_stripped,
|
||||
description_json=issue.description,
|
||||
)
|
||||
)
|
||||
|
||||
# Bulk create version objects
|
||||
if version_objects:
|
||||
IssueDescriptionVersion.objects.bulk_create(version_objects)
|
||||
|
||||
# Schedule next batch if needed
|
||||
if end_offset < total_issues_count:
|
||||
sync_issue_description_version.apply_async(
|
||||
kwargs={
|
||||
"batch_size": batch_size,
|
||||
"offset": end_offset,
|
||||
"countdown": countdown,
|
||||
},
|
||||
countdown=countdown,
|
||||
)
|
||||
return
|
||||
except Exception as e:
|
||||
log_exception(e)
|
||||
return
|
||||
|
||||
|
||||
@shared_task
|
||||
def schedule_issue_description_version(batch_size=5000, countdown=300):
|
||||
sync_issue_description_version.delay(batch_size=batch_size, countdown=countdown)
|
||||
@@ -0,0 +1,85 @@
|
||||
from celery import shared_task
|
||||
from django.db import transaction
|
||||
from django.utils import timezone
|
||||
from typing import Optional, Dict, Any
|
||||
import json
|
||||
|
||||
from plane.db.models import Issue, IssueDescriptionVersion
|
||||
from plane.utils.logging import log_exception
|
||||
|
||||
|
||||
def should_update_existing_version(
|
||||
version: IssueDescriptionVersion, user_id: str, max_time_difference: int = 600
|
||||
) -> bool:
|
||||
if not version:
|
||||
return
|
||||
|
||||
time_difference = (timezone.now() - version.last_saved_at).total_seconds()
|
||||
return (
|
||||
str(version.owned_by_id) == str(user_id)
|
||||
and time_difference <= max_time_difference
|
||||
)
|
||||
|
||||
|
||||
def update_existing_version(
|
||||
version: IssueDescriptionVersion, description_data: Dict[str, Any]
|
||||
) -> None:
|
||||
version.description_json = description_data.get("description")
|
||||
version.description_html = description_data.get("description_html")
|
||||
version.description_binary = description_data.get("description_binary")
|
||||
version.description_stripped = description_data.get("description_stripped")
|
||||
version.last_saved_at = timezone.now()
|
||||
|
||||
version.save(
|
||||
update_fields=[
|
||||
"description_json",
|
||||
"description_html",
|
||||
"description_binary",
|
||||
"description_stripped",
|
||||
"last_saved_at",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@shared_task
|
||||
def issue_description_version_task(
|
||||
updated_issue: Optional[str], issue_id: str, user_id: str
|
||||
) -> Optional[bool]:
|
||||
try:
|
||||
# Parse updated issue data
|
||||
current_issue: Dict = json.loads(updated_issue) if updated_issue else {}
|
||||
|
||||
# Get current issue
|
||||
issue = Issue.objects.get(id=issue_id)
|
||||
|
||||
# Check if description has changed
|
||||
if current_issue.get("description_html") == issue.description_html:
|
||||
return
|
||||
|
||||
with transaction.atomic():
|
||||
# Get latest version
|
||||
latest_version = (
|
||||
IssueDescriptionVersion.objects.filter(issue_id=issue_id)
|
||||
.order_by("-last_saved_at")
|
||||
.first()
|
||||
)
|
||||
|
||||
# Determine whether to update existing or create new version
|
||||
if should_update_existing_version(latest_version, user_id):
|
||||
update_existing_version(latest_version, current_issue)
|
||||
else:
|
||||
IssueDescriptionVersion.log_issue_description_version(
|
||||
current_issue, user_id
|
||||
)
|
||||
|
||||
return
|
||||
|
||||
except Issue.DoesNotExist:
|
||||
# Issue no longer exists, skip processing
|
||||
return
|
||||
except json.JSONDecodeError as e:
|
||||
log_exception(f"Invalid JSON for updated_issue: {e}")
|
||||
return
|
||||
except Exception as e:
|
||||
log_exception(f"Error processing issue description version: {e}")
|
||||
return
|
||||
@@ -0,0 +1,255 @@
|
||||
# Python imports
|
||||
import json
|
||||
from typing import Optional, List, Dict
|
||||
from uuid import UUID
|
||||
from itertools import groupby
|
||||
|
||||
# Django imports
|
||||
from django.utils import timezone
|
||||
from django.db import transaction
|
||||
|
||||
# Third party imports
|
||||
from celery import shared_task
|
||||
|
||||
# Module imports
|
||||
from plane.db.models import (
|
||||
Issue,
|
||||
IssueVersion,
|
||||
ProjectMember,
|
||||
CycleIssue,
|
||||
ModuleIssue,
|
||||
IssueActivity,
|
||||
IssueAssignee,
|
||||
IssueLabel,
|
||||
)
|
||||
from plane.utils.exception_logger import log_exception
|
||||
|
||||
|
||||
@shared_task
|
||||
def issue_task(updated_issue, issue_id, user_id):
|
||||
try:
|
||||
current_issue = json.loads(updated_issue) if updated_issue else {}
|
||||
issue = Issue.objects.get(id=issue_id)
|
||||
|
||||
updated_current_issue = {}
|
||||
for key, value in current_issue.items():
|
||||
if getattr(issue, key) != value:
|
||||
updated_current_issue[key] = value
|
||||
|
||||
if updated_current_issue:
|
||||
issue_version = (
|
||||
IssueVersion.objects.filter(issue_id=issue_id)
|
||||
.order_by("-last_saved_at")
|
||||
.first()
|
||||
)
|
||||
|
||||
if (
|
||||
issue_version
|
||||
and str(issue_version.owned_by) == str(user_id)
|
||||
and (timezone.now() - issue_version.last_saved_at).total_seconds()
|
||||
<= 600
|
||||
):
|
||||
for key, value in updated_current_issue.items():
|
||||
setattr(issue_version, key, value)
|
||||
issue_version.last_saved_at = timezone.now()
|
||||
issue_version.save(
|
||||
update_fields=list(updated_current_issue.keys()) + ["last_saved_at"]
|
||||
)
|
||||
else:
|
||||
IssueVersion.log_issue_version(issue, user_id)
|
||||
|
||||
return
|
||||
except Issue.DoesNotExist:
|
||||
return
|
||||
except Exception as e:
|
||||
log_exception(e)
|
||||
return
|
||||
|
||||
|
||||
def get_owner_id(issue: Issue) -> Optional[int]:
|
||||
"""Get the owner ID of the issue"""
|
||||
|
||||
if issue.updated_by_id:
|
||||
return issue.updated_by_id
|
||||
|
||||
if issue.created_by_id:
|
||||
return issue.created_by_id
|
||||
|
||||
# Find project admin as fallback
|
||||
project_member = ProjectMember.objects.filter(
|
||||
project_id=issue.project_id,
|
||||
role=20, # Admin role
|
||||
).first()
|
||||
|
||||
return project_member.member_id if project_member else None
|
||||
|
||||
|
||||
def get_related_data(issue_ids: List[UUID]) -> Dict:
|
||||
"""Get related data for the given issue IDs"""
|
||||
|
||||
cycle_issues = {
|
||||
ci.issue_id: ci.cycle_id
|
||||
for ci in CycleIssue.objects.filter(issue_id__in=issue_ids)
|
||||
}
|
||||
|
||||
# Get assignees with proper grouping
|
||||
assignee_records = list(
|
||||
IssueAssignee.objects.filter(issue_id__in=issue_ids)
|
||||
.values_list("issue_id", "assignee_id")
|
||||
.order_by("issue_id")
|
||||
)
|
||||
assignees = {}
|
||||
for issue_id, group in groupby(assignee_records, key=lambda x: x[0]):
|
||||
assignees[issue_id] = [str(g[1]) for g in group]
|
||||
|
||||
# Get labels with proper grouping
|
||||
label_records = list(
|
||||
IssueLabel.objects.filter(issue_id__in=issue_ids)
|
||||
.values_list("issue_id", "label_id")
|
||||
.order_by("issue_id")
|
||||
)
|
||||
labels = {}
|
||||
for issue_id, group in groupby(label_records, key=lambda x: x[0]):
|
||||
labels[issue_id] = [str(g[1]) for g in group]
|
||||
|
||||
# Get modules with proper grouping
|
||||
module_records = list(
|
||||
ModuleIssue.objects.filter(issue_id__in=issue_ids)
|
||||
.values_list("issue_id", "module_id")
|
||||
.order_by("issue_id")
|
||||
)
|
||||
modules = {}
|
||||
for issue_id, group in groupby(module_records, key=lambda x: x[0]):
|
||||
modules[issue_id] = [str(g[1]) for g in group]
|
||||
|
||||
# Get latest activities
|
||||
latest_activities = {}
|
||||
activities = IssueActivity.objects.filter(issue_id__in=issue_ids).order_by(
|
||||
"issue_id", "-created_at"
|
||||
)
|
||||
for issue_id, activities_group in groupby(activities, key=lambda x: x.issue_id):
|
||||
first_activity = next(activities_group, None)
|
||||
if first_activity:
|
||||
latest_activities[issue_id] = first_activity.id
|
||||
|
||||
return {
|
||||
"cycle_issues": cycle_issues,
|
||||
"assignees": assignees,
|
||||
"labels": labels,
|
||||
"modules": modules,
|
||||
"activities": latest_activities,
|
||||
}
|
||||
|
||||
|
||||
def create_issue_version(issue: Issue, related_data: Dict) -> Optional[IssueVersion]:
|
||||
"""Create IssueVersion object from the given issue and related data"""
|
||||
|
||||
try:
|
||||
if not issue.workspace_id or not issue.project_id:
|
||||
print(f"Skipping issue {issue.id} - missing workspace_id or project_id")
|
||||
return None
|
||||
|
||||
owned_by_id = get_owner_id(issue)
|
||||
if owned_by_id is None:
|
||||
print(f"Skipping issue {issue.id} - missing owned_by")
|
||||
return None
|
||||
|
||||
return IssueVersion(
|
||||
workspace_id=issue.workspace_id,
|
||||
project_id=issue.project_id,
|
||||
created_by_id=issue.created_by_id,
|
||||
updated_by_id=issue.updated_by_id,
|
||||
owned_by_id=owned_by_id,
|
||||
last_saved_at=timezone.now(),
|
||||
activity_id=related_data["activities"].get(issue.id),
|
||||
properties=getattr(issue, "properties", {}),
|
||||
meta=getattr(issue, "meta", {}),
|
||||
issue_id=issue.id,
|
||||
parent=issue.parent_id,
|
||||
state=issue.state_id,
|
||||
point=issue.point,
|
||||
estimate_point=issue.estimate_point_id,
|
||||
name=issue.name,
|
||||
priority=issue.priority,
|
||||
start_date=issue.start_date,
|
||||
target_date=issue.target_date,
|
||||
assignees=related_data["assignees"].get(issue.id, []),
|
||||
sequence_id=issue.sequence_id,
|
||||
labels=related_data["labels"].get(issue.id, []),
|
||||
sort_order=issue.sort_order,
|
||||
completed_at=issue.completed_at,
|
||||
archived_at=issue.archived_at,
|
||||
is_draft=issue.is_draft,
|
||||
external_source=issue.external_source,
|
||||
external_id=issue.external_id,
|
||||
type=issue.type_id,
|
||||
cycle=related_data["cycle_issues"].get(issue.id),
|
||||
modules=related_data["modules"].get(issue.id, []),
|
||||
)
|
||||
except Exception as e:
|
||||
log_exception(e)
|
||||
return None
|
||||
|
||||
|
||||
@shared_task
|
||||
def sync_issue_version(batch_size=5000, offset=0, countdown=300):
|
||||
"""Task to create IssueVersion records for existing Issues in batches"""
|
||||
|
||||
try:
|
||||
with transaction.atomic():
|
||||
base_query = Issue.objects
|
||||
total_issues_count = base_query.count()
|
||||
|
||||
if total_issues_count == 0:
|
||||
return
|
||||
|
||||
print(f"Offset: {offset}")
|
||||
print(f"Total Issues: {total_issues_count}")
|
||||
|
||||
end_offset = min(offset + batch_size, total_issues_count)
|
||||
|
||||
# Get issues batch with optimized queries
|
||||
issues_batch = list(
|
||||
base_query.order_by("created_at")
|
||||
.select_related("workspace", "project")
|
||||
.all()[offset:end_offset]
|
||||
)
|
||||
|
||||
if not issues_batch:
|
||||
return
|
||||
|
||||
# Get all related data in bulk
|
||||
issue_ids = [issue.id for issue in issues_batch]
|
||||
related_data = get_related_data(issue_ids)
|
||||
|
||||
issue_versions = []
|
||||
for issue in issues_batch:
|
||||
version = create_issue_version(issue, related_data)
|
||||
if version:
|
||||
issue_versions.append(version)
|
||||
|
||||
# Bulk create versions
|
||||
if issue_versions:
|
||||
IssueVersion.objects.bulk_create(issue_versions, batch_size=1000)
|
||||
|
||||
# Schedule the next batch if there are more workspaces to process
|
||||
if end_offset < total_issues_count:
|
||||
sync_issue_version.apply_async(
|
||||
kwargs={
|
||||
"batch_size": batch_size,
|
||||
"offset": end_offset,
|
||||
"countdown": countdown,
|
||||
},
|
||||
countdown=countdown,
|
||||
)
|
||||
|
||||
print(f"Processed Issues: {end_offset}")
|
||||
return
|
||||
except Exception as e:
|
||||
log_exception(e)
|
||||
return
|
||||
|
||||
|
||||
@shared_task
|
||||
def schedule_issue_version(batch_size=5000, countdown=300):
|
||||
sync_issue_version.delay(batch_size=batch_size, countdown=countdown)
|
||||
@@ -0,0 +1,90 @@
|
||||
# Python imports
|
||||
import json
|
||||
from typing import Dict, Any, Optional, List
|
||||
|
||||
# Django imports
|
||||
from django.utils import timezone
|
||||
from django.db import transaction
|
||||
|
||||
# Third party imports
|
||||
from celery import shared_task
|
||||
|
||||
# Module imports
|
||||
from plane.db.models import Issue, IssueVersion
|
||||
from plane.utils.exception_logger import log_exception
|
||||
|
||||
|
||||
def get_changed_fields(current_issue: Dict[str, Any], issue: Issue) -> Dict[str, Any]:
|
||||
return {
|
||||
key: value
|
||||
for key, value in current_issue.items()
|
||||
if getattr(issue, key) != value
|
||||
}
|
||||
|
||||
|
||||
def should_update_existing_version(
|
||||
version: Optional[IssueVersion], user_id: str, max_time_difference: int = 600
|
||||
) -> bool:
|
||||
if not version:
|
||||
return False
|
||||
|
||||
time_difference = (timezone.now() - version.last_saved_at).total_seconds()
|
||||
return (
|
||||
str(version.owned_by_id) == str(user_id)
|
||||
and time_difference <= max_time_difference
|
||||
)
|
||||
|
||||
|
||||
def update_version_fields(
|
||||
version: IssueVersion, changed_fields: Dict[str, Any]
|
||||
) -> List[str]:
|
||||
for key, value in changed_fields.items():
|
||||
setattr(version, key, value)
|
||||
|
||||
version.last_saved_at = timezone.now()
|
||||
update_fields = list(changed_fields.keys()) + ["last_saved_at"]
|
||||
return update_fields
|
||||
|
||||
|
||||
@shared_task
|
||||
def issue_version_task(
|
||||
updated_issue: Optional[str], issue_id: str, user_id: str
|
||||
) -> Optional[bool]:
|
||||
try:
|
||||
# Parse updated issue data
|
||||
current_issue: Dict = json.loads(updated_issue) if updated_issue else {}
|
||||
|
||||
with transaction.atomic():
|
||||
# Get current issue
|
||||
issue = Issue.objects.get(id=issue_id)
|
||||
|
||||
# Get changed fields
|
||||
changed_fields = get_changed_fields(current_issue, issue)
|
||||
|
||||
if not changed_fields:
|
||||
return True
|
||||
|
||||
# Get latest version
|
||||
latest_version = (
|
||||
IssueVersion.objects.filter(issue_id=issue_id)
|
||||
.order_by("-last_saved_at")
|
||||
.first()
|
||||
)
|
||||
|
||||
# Update existing or create new version
|
||||
if should_update_existing_version(latest_version, user_id):
|
||||
update_fields = update_version_fields(latest_version, changed_fields)
|
||||
latest_version.save(update_fields=update_fields)
|
||||
else:
|
||||
IssueVersion.log_issue_version(issue, user_id)
|
||||
|
||||
return True
|
||||
|
||||
except Issue.DoesNotExist:
|
||||
return None
|
||||
except json.JSONDecodeError as e:
|
||||
log_exception(f"Invalid JSON for updated_issue: {e}")
|
||||
return False
|
||||
except Exception as e:
|
||||
log_exception(f"Error processing issue version: {e}")
|
||||
return False
|
||||
@@ -0,0 +1,23 @@
|
||||
# Django imports
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
# Module imports
|
||||
from plane.bgtasks.issue_description_version_sync import (
|
||||
schedule_issue_description_version,
|
||||
)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Creates IssueDescriptionVersion records for existing Issues in batches"
|
||||
|
||||
def handle(self, *args, **options):
|
||||
batch_size = input("Enter the batch size: ")
|
||||
batch_countdown = input("Enter the batch countdown: ")
|
||||
|
||||
schedule_issue_description_version.delay(
|
||||
batch_size=int(batch_size), countdown=int(batch_countdown)
|
||||
)
|
||||
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS("Successfully created issue description version task")
|
||||
)
|
||||
@@ -0,0 +1,19 @@
|
||||
# Django imports
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
# Module imports
|
||||
from plane.bgtasks.issue_version_sync import schedule_issue_version
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Creates IssueVersion records for existing Issues in batches"
|
||||
|
||||
def handle(self, *args, **options):
|
||||
batch_size = input("Enter the batch size: ")
|
||||
batch_countdown = input("Enter the batch countdown: ")
|
||||
|
||||
schedule_issue_version.delay(
|
||||
batch_size=int(batch_size), countdown=int(batch_countdown)
|
||||
)
|
||||
|
||||
self.stdout.write(self.style.SUCCESS("Successfully created issue version task"))
|
||||
@@ -0,0 +1,90 @@
|
||||
# Generated by Django 4.2.16 on 2024-12-09 10:03
|
||||
|
||||
from django.conf import settings
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
import plane.db.models.user
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('db', '0086_issueversion_alter_teampage_unique_together_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='issueversion',
|
||||
name='description',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='issueversion',
|
||||
name='description_binary',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='issueversion',
|
||||
name='description_html',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='issueversion',
|
||||
name='description_stripped',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='issueversion',
|
||||
name='activity',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='versions', to='db.issueactivity'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='issueversion',
|
||||
name='point',
|
||||
field=models.IntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(12)]),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='profile',
|
||||
name='is_mobile_onboarded',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='profile',
|
||||
name='mobile_onboarding_step',
|
||||
field=models.JSONField(default=plane.db.models.user.get_mobile_default_onboarding),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='profile',
|
||||
name='mobile_timezone_auto_set',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='issueversion',
|
||||
name='owned_by',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_versions', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
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)),
|
||||
('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)),
|
||||
('last_saved_at', models.DateTimeField(default=django.utils.timezone.now)),
|
||||
('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='description_versions', to='db.issue')),
|
||||
('owned_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_description_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',
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -41,6 +41,8 @@ from .issue import (
|
||||
IssueSequence,
|
||||
IssueSubscriber,
|
||||
IssueVote,
|
||||
IssueVersion,
|
||||
IssueDescriptionVersion,
|
||||
)
|
||||
from .module import Module, ModuleIssue, ModuleLink, ModuleMember, ModuleUserProperties
|
||||
from .notification import EmailNotificationLog, Notification, UserNotificationPreference
|
||||
|
||||
@@ -660,9 +660,6 @@ class IssueVote(ProjectBaseModel):
|
||||
|
||||
|
||||
class IssueVersion(ProjectBaseModel):
|
||||
issue = models.ForeignKey(
|
||||
"db.Issue", on_delete=models.CASCADE, related_name="versions"
|
||||
)
|
||||
PRIORITY_CHOICES = (
|
||||
("urgent", "Urgent"),
|
||||
("high", "High"),
|
||||
@@ -670,14 +667,17 @@ class IssueVersion(ProjectBaseModel):
|
||||
("low", "Low"),
|
||||
("none", "None"),
|
||||
)
|
||||
|
||||
issue = models.ForeignKey(
|
||||
"db.Issue", on_delete=models.CASCADE, related_name="versions"
|
||||
)
|
||||
parent = models.UUIDField(blank=True, null=True)
|
||||
state = models.UUIDField(blank=True, null=True)
|
||||
point = models.IntegerField(
|
||||
validators=[MinValueValidator(0), MaxValueValidator(12)], null=True, blank=True
|
||||
)
|
||||
estimate_point = models.UUIDField(blank=True, null=True)
|
||||
name = models.CharField(max_length=255, verbose_name="Issue Name")
|
||||
description = models.JSONField(blank=True, default=dict)
|
||||
description_html = models.TextField(blank=True, default="<p></p>")
|
||||
description_stripped = models.TextField(blank=True, null=True)
|
||||
description_binary = models.BinaryField(null=True)
|
||||
priority = models.CharField(
|
||||
max_length=30,
|
||||
choices=PRIORITY_CHOICES,
|
||||
@@ -686,7 +686,9 @@ class IssueVersion(ProjectBaseModel):
|
||||
)
|
||||
start_date = models.DateField(null=True, blank=True)
|
||||
target_date = models.DateField(null=True, blank=True)
|
||||
assignees = ArrayField(models.UUIDField(), blank=True, default=list)
|
||||
sequence_id = models.IntegerField(default=1, verbose_name="Issue Sequence ID")
|
||||
labels = ArrayField(models.UUIDField(), blank=True, default=list)
|
||||
sort_order = models.FloatField(default=65535)
|
||||
completed_at = models.DateTimeField(null=True)
|
||||
archived_at = models.DateField(null=True)
|
||||
@@ -694,14 +696,22 @@ class IssueVersion(ProjectBaseModel):
|
||||
external_source = models.CharField(max_length=255, null=True, blank=True)
|
||||
external_id = models.CharField(max_length=255, blank=True, null=True)
|
||||
type = models.UUIDField(blank=True, null=True)
|
||||
last_saved_at = models.DateTimeField(default=timezone.now)
|
||||
owned_by = models.UUIDField()
|
||||
assignees = ArrayField(models.UUIDField(), blank=True, default=list)
|
||||
labels = ArrayField(models.UUIDField(), blank=True, default=list)
|
||||
cycle = models.UUIDField(null=True, blank=True)
|
||||
modules = ArrayField(models.UUIDField(), blank=True, default=list)
|
||||
properties = models.JSONField(default=dict)
|
||||
meta = models.JSONField(default=dict)
|
||||
activity = models.ForeignKey(
|
||||
"db.IssueActivity",
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
related_name="versions",
|
||||
)
|
||||
properties = models.JSONField(default=dict) # issue properties
|
||||
meta = models.JSONField(default=dict) # issue meta
|
||||
last_saved_at = models.DateTimeField(default=timezone.now)
|
||||
owned_by = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="issue_versions",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Issue Version"
|
||||
@@ -721,36 +731,87 @@ class IssueVersion(ProjectBaseModel):
|
||||
|
||||
Module = apps.get_model("db.Module")
|
||||
CycleIssue = apps.get_model("db.CycleIssue")
|
||||
IssueAssignee = apps.get_model("db.IssueAssignee")
|
||||
IssueLabel = apps.get_model("db.IssueLabel")
|
||||
|
||||
cycle_issue = CycleIssue.objects.filter(issue=issue).first()
|
||||
|
||||
cls.objects.create(
|
||||
issue=issue,
|
||||
parent=issue.parent,
|
||||
state=issue.state,
|
||||
parent=issue.parent_id,
|
||||
state=issue.state_id,
|
||||
point=issue.point,
|
||||
estimate_point=issue.estimate_point,
|
||||
estimate_point=issue.estimate_point_id,
|
||||
name=issue.name,
|
||||
description=issue.description,
|
||||
description_html=issue.description_html,
|
||||
description_stripped=issue.description_stripped,
|
||||
description_binary=issue.description_binary,
|
||||
priority=issue.priority,
|
||||
start_date=issue.start_date,
|
||||
target_date=issue.target_date,
|
||||
assignees=list(
|
||||
IssueAssignee.objects.filter(issue=issue).values_list(
|
||||
"assignee_id", flat=True
|
||||
)
|
||||
),
|
||||
sequence_id=issue.sequence_id,
|
||||
labels=list(
|
||||
IssueLabel.objects.filter(issue=issue).values_list(
|
||||
"label_id", flat=True
|
||||
)
|
||||
),
|
||||
sort_order=issue.sort_order,
|
||||
completed_at=issue.completed_at,
|
||||
archived_at=issue.archived_at,
|
||||
is_draft=issue.is_draft,
|
||||
external_source=issue.external_source,
|
||||
external_id=issue.external_id,
|
||||
type=issue.type,
|
||||
last_saved_at=issue.last_saved_at,
|
||||
assignees=issue.assignees,
|
||||
labels=issue.labels,
|
||||
cycle=cycle_issue.cycle if cycle_issue else None,
|
||||
modules=Module.objects.filter(issue=issue).values_list("id", flat=True),
|
||||
type=issue.type_id,
|
||||
cycle=cycle_issue.cycle_id if cycle_issue else None,
|
||||
modules=list(
|
||||
Module.objects.filter(issue=issue).values_list("id", flat=True)
|
||||
),
|
||||
properties={},
|
||||
meta={},
|
||||
last_saved_at=timezone.now(),
|
||||
owned_by=user,
|
||||
)
|
||||
return True
|
||||
except Exception as e:
|
||||
log_exception(e)
|
||||
return False
|
||||
|
||||
|
||||
class IssueDescriptionVersion(ProjectBaseModel):
|
||||
issue = models.ForeignKey(
|
||||
"db.Issue", on_delete=models.CASCADE, related_name="description_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)
|
||||
last_saved_at = models.DateTimeField(default=timezone.now)
|
||||
owned_by = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="issue_description_versions",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Issue Description Version"
|
||||
verbose_name_plural = "Issue Description Versions"
|
||||
db_table = "issue_description_versions"
|
||||
|
||||
@classmethod
|
||||
def log_issue_description_version(cls, issue, user):
|
||||
try:
|
||||
"""
|
||||
Log the issue description version
|
||||
"""
|
||||
cls.objects.create(
|
||||
issue=issue,
|
||||
description_binary=issue.description_binary,
|
||||
description_html=issue.description_html,
|
||||
description_stripped=issue.description_stripped,
|
||||
description_json=issue.description,
|
||||
last_saved_at=timezone.now(),
|
||||
owned_by=user,
|
||||
)
|
||||
return True
|
||||
|
||||
@@ -26,6 +26,14 @@ def get_default_onboarding():
|
||||
}
|
||||
|
||||
|
||||
def get_mobile_default_onboarding():
|
||||
return {
|
||||
"profile_complete": False,
|
||||
"workspace_create": False,
|
||||
"workspace_join": False,
|
||||
}
|
||||
|
||||
|
||||
class User(AbstractBaseUser, PermissionsMixin):
|
||||
id = models.UUIDField(
|
||||
default=uuid.uuid4, unique=True, editable=False, db_index=True, primary_key=True
|
||||
@@ -178,6 +186,10 @@ class Profile(TimeAuditModel):
|
||||
billing_address = models.JSONField(null=True)
|
||||
has_billing_address = models.BooleanField(default=False)
|
||||
company_name = models.CharField(max_length=255, blank=True)
|
||||
# mobile
|
||||
is_mobile_onboarded = models.BooleanField(default=False)
|
||||
mobile_onboarding_step = models.JSONField(default=get_mobile_default_onboarding)
|
||||
mobile_timezone_auto_set = models.BooleanField(default=False)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Profile"
|
||||
|
||||
@@ -262,6 +262,9 @@ CELERY_IMPORTS = (
|
||||
"plane.license.bgtasks.tracer",
|
||||
# management tasks
|
||||
"plane.bgtasks.dummy_data_task",
|
||||
# issue sync tasks
|
||||
"plane.bgtasks.issue_version_sync",
|
||||
"plane.bgtasks.issue_description_version_sync",
|
||||
)
|
||||
|
||||
# Sentry Settings
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "live",
|
||||
"version": "0.24.0",
|
||||
"version": "0.24.1",
|
||||
"description": "",
|
||||
"main": "./src/server.ts",
|
||||
"private": true,
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"repository": "https://github.com/makeplane/plane.git",
|
||||
"version": "0.24.0",
|
||||
"version": "0.24.1",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/constants",
|
||||
"version": "0.24.0",
|
||||
"version": "0.24.1",
|
||||
"private": true,
|
||||
"main": "./src/index.ts"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/editor",
|
||||
"version": "0.24.0",
|
||||
"version": "0.24.1",
|
||||
"description": "Core Editor that powers Plane",
|
||||
"private": true,
|
||||
"main": "./dist/index.mjs",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@plane/eslint-config",
|
||||
"private": true,
|
||||
"version": "0.24.0",
|
||||
"version": "0.24.1",
|
||||
"files": [
|
||||
"library.js",
|
||||
"next.js",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/hooks",
|
||||
"version": "0.24.0",
|
||||
"version": "0.24.1",
|
||||
"description": "React hooks that are shared across multiple apps internally",
|
||||
"private": true,
|
||||
"main": "./dist/index.js",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tailwind-config-custom",
|
||||
"version": "0.24.0",
|
||||
"version": "0.24.1",
|
||||
"description": "common tailwind configuration across monorepo",
|
||||
"main": "index.js",
|
||||
"private": true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/types",
|
||||
"version": "0.24.0",
|
||||
"version": "0.24.1",
|
||||
"private": true,
|
||||
"types": "./src/index.d.ts",
|
||||
"main": "./src/index.d.ts"
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
export type TCommandPaletteActionList = Record<
|
||||
string,
|
||||
{ title: string; description: string; action: () => void }
|
||||
>;
|
||||
|
||||
export type TCommandPaletteShortcutList = {
|
||||
key: string;
|
||||
title: string;
|
||||
shortcuts: TCommandPaletteShortcut[];
|
||||
};
|
||||
|
||||
export type TCommandPaletteShortcut = {
|
||||
keys: string; // comma separated keys
|
||||
description: string;
|
||||
};
|
||||
Vendored
+1
@@ -32,3 +32,4 @@ export * from "./workspace-notifications";
|
||||
export * from "./favorite";
|
||||
export * from "./file";
|
||||
export * from "./workspace-draft-issues/base";
|
||||
export * from "./command-palette";
|
||||
|
||||
Vendored
+1
-1
@@ -216,7 +216,7 @@ export type GroupByColumnTypes =
|
||||
export interface IGroupByColumn {
|
||||
id: string;
|
||||
name: string;
|
||||
icon: ReactElement | undefined;
|
||||
icon?: ReactElement | undefined;
|
||||
payload: Partial<TIssue>;
|
||||
isDropDisabled?: boolean;
|
||||
dropErrorMessage?: string;
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ export type TNotificationData = {
|
||||
};
|
||||
|
||||
export type TNotification = {
|
||||
id: string | undefined;
|
||||
id: string;
|
||||
title: string | undefined;
|
||||
data: TNotificationData | undefined;
|
||||
entity_identifier: string | undefined;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/typescript-config",
|
||||
"version": "0.24.0",
|
||||
"version": "0.24.1",
|
||||
"private": true,
|
||||
"files": [
|
||||
"base.json",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@plane/ui",
|
||||
"description": "UI components shared across multiple apps internally",
|
||||
"private": true,
|
||||
"version": "0.24.0",
|
||||
"version": "0.24.1",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/utils",
|
||||
"version": "0.24.0",
|
||||
"version": "0.24.1",
|
||||
"description": "Helper functions shared across multiple apps internally",
|
||||
"private": true,
|
||||
"main": "./dist/index.js",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "space",
|
||||
"version": "0.24.0",
|
||||
"version": "0.24.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "turbo run develop",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
// components
|
||||
import { NotificationsSidebarRoot } from "@/plane-web/components/workspace-notifications";
|
||||
import { NotificationsSidebarRoot } from "@/components/workspace-notifications";
|
||||
|
||||
export default function ProjectInboxIssuesLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from "./workspace-level";
|
||||
export * from "./project-level";
|
||||
export * from "./issue-level";
|
||||
@@ -0,0 +1,73 @@
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams, usePathname } from "next/navigation";
|
||||
import useSWR from "swr";
|
||||
// components
|
||||
import { BulkDeleteIssuesModal } from "@/components/core";
|
||||
import { CreateUpdateIssueModal, DeleteIssueModal } from "@/components/issues";
|
||||
// constants
|
||||
import { ISSUE_DETAILS } from "@/constants/fetch-keys";
|
||||
// hooks
|
||||
import { useCommandPalette, useUser } from "@/hooks/store";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
import { useIssuesStore } from "@/hooks/use-issue-layout-store";
|
||||
// services
|
||||
import { IssueService } from "@/services/issue";
|
||||
|
||||
// services
|
||||
const issueService = new IssueService();
|
||||
|
||||
export const IssueLevelModals = observer(() => {
|
||||
// router
|
||||
const pathname = usePathname();
|
||||
const { workspaceSlug, projectId, issueId, cycleId, moduleId } = useParams();
|
||||
const router = useAppRouter();
|
||||
// store hooks
|
||||
const { data: currentUser } = useUser();
|
||||
const {
|
||||
issues: { removeIssue },
|
||||
} = useIssuesStore();
|
||||
const {
|
||||
isCreateIssueModalOpen,
|
||||
toggleCreateIssueModal,
|
||||
isDeleteIssueModalOpen,
|
||||
toggleDeleteIssueModal,
|
||||
isBulkDeleteIssueModalOpen,
|
||||
toggleBulkDeleteIssueModal,
|
||||
} = useCommandPalette();
|
||||
// derived values
|
||||
const isDraftIssue = pathname?.includes("draft-issues") || false;
|
||||
|
||||
const { data: issueDetails } = useSWR(
|
||||
workspaceSlug && projectId && issueId ? ISSUE_DETAILS(issueId as string) : null,
|
||||
workspaceSlug && projectId && issueId
|
||||
? () => issueService.retrieve(workspaceSlug as string, projectId as string, issueId as string)
|
||||
: null
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<CreateUpdateIssueModal
|
||||
isOpen={isCreateIssueModalOpen}
|
||||
onClose={() => toggleCreateIssueModal(false)}
|
||||
data={cycleId ? { cycle_id: cycleId.toString() } : moduleId ? { module_ids: [moduleId.toString()] } : undefined}
|
||||
isDraft={isDraftIssue}
|
||||
/>
|
||||
{workspaceSlug && projectId && issueId && issueDetails && (
|
||||
<DeleteIssueModal
|
||||
handleClose={() => toggleDeleteIssueModal(false)}
|
||||
isOpen={isDeleteIssueModalOpen}
|
||||
data={issueDetails}
|
||||
onSubmit={async () => {
|
||||
await removeIssue(workspaceSlug.toString(), projectId.toString(), issueId.toString());
|
||||
router.push(`/${workspaceSlug}/projects/${projectId}/issues`);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<BulkDeleteIssuesModal
|
||||
isOpen={isBulkDeleteIssueModalOpen}
|
||||
onClose={() => toggleBulkDeleteIssueModal(false)}
|
||||
user={currentUser}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
import { observer } from "mobx-react";
|
||||
// components
|
||||
import { CycleCreateUpdateModal } from "@/components/cycles";
|
||||
import { CreateUpdateModuleModal } from "@/components/modules";
|
||||
import { CreatePageModal } from "@/components/pages";
|
||||
import { CreateUpdateProjectViewModal } from "@/components/views";
|
||||
// hooks
|
||||
import { useCommandPalette } from "@/hooks/store";
|
||||
|
||||
export type TProjectLevelModalsProps = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export const ProjectLevelModals = observer((props: TProjectLevelModalsProps) => {
|
||||
const { workspaceSlug, projectId } = props;
|
||||
// store hooks
|
||||
const {
|
||||
isCreateCycleModalOpen,
|
||||
toggleCreateCycleModal,
|
||||
isCreateModuleModalOpen,
|
||||
toggleCreateModuleModal,
|
||||
isCreateViewModalOpen,
|
||||
toggleCreateViewModal,
|
||||
createPageModal,
|
||||
toggleCreatePageModal,
|
||||
} = useCommandPalette();
|
||||
|
||||
return (
|
||||
<>
|
||||
<CycleCreateUpdateModal
|
||||
isOpen={isCreateCycleModalOpen}
|
||||
handleClose={() => toggleCreateCycleModal(false)}
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
/>
|
||||
<CreateUpdateModuleModal
|
||||
isOpen={isCreateModuleModalOpen}
|
||||
onClose={() => toggleCreateModuleModal(false)}
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
/>
|
||||
<CreateUpdateProjectViewModal
|
||||
isOpen={isCreateViewModalOpen}
|
||||
onClose={() => toggleCreateViewModal(false)}
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
/>
|
||||
<CreatePageModal
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
isModalOpen={createPageModal.isOpen}
|
||||
pageAccess={createPageModal.pageAccess}
|
||||
handleModalClose={() => toggleCreatePageModal({ isOpen: false })}
|
||||
redirectionEnabled
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
import { observer } from "mobx-react";
|
||||
// components
|
||||
import { CreateProjectModal } from "@/components/project";
|
||||
// hooks
|
||||
import { useCommandPalette } from "@/hooks/store";
|
||||
|
||||
export type TWorkspaceLevelModalsProps = {
|
||||
workspaceSlug: string;
|
||||
};
|
||||
|
||||
export const WorkspaceLevelModals = observer((props: TWorkspaceLevelModalsProps) => {
|
||||
const { workspaceSlug } = props;
|
||||
// store hooks
|
||||
const { isCreateProjectModalOpen, toggleCreateProjectModal } = useCommandPalette();
|
||||
|
||||
return (
|
||||
<>
|
||||
<CreateProjectModal
|
||||
isOpen={isCreateProjectModalOpen}
|
||||
onClose={() => toggleCreateProjectModal(false)}
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -1 +1 @@
|
||||
export * from './root'
|
||||
export * from "./notification-card/root";
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
// types
|
||||
import { TCommandPaletteActionList, TCommandPaletteShortcut, TCommandPaletteShortcutList } from "@plane/types";
|
||||
// store
|
||||
import { store } from "@/lib/store-context";
|
||||
|
||||
export const getGlobalShortcutsList: () => TCommandPaletteActionList = () => {
|
||||
const { toggleCreateIssueModal } = store.commandPalette;
|
||||
|
||||
return {
|
||||
c: {
|
||||
title: "Create a new issue",
|
||||
description: "Create a new issue in the current project",
|
||||
action: () => toggleCreateIssueModal(true),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const getWorkspaceShortcutsList: () => TCommandPaletteActionList = () => {
|
||||
const { toggleCreateProjectModal } = store.commandPalette;
|
||||
|
||||
return {
|
||||
p: {
|
||||
title: "Create a new project",
|
||||
description: "Create a new project in the current workspace",
|
||||
action: () => toggleCreateProjectModal(true),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const getProjectShortcutsList: () => TCommandPaletteActionList = () => {
|
||||
const {
|
||||
toggleCreatePageModal,
|
||||
toggleCreateModuleModal,
|
||||
toggleCreateCycleModal,
|
||||
toggleCreateViewModal,
|
||||
toggleBulkDeleteIssueModal,
|
||||
} = store.commandPalette;
|
||||
|
||||
return {
|
||||
d: {
|
||||
title: "Create a new page",
|
||||
description: "Create a new page in the current project",
|
||||
action: () => toggleCreatePageModal({ isOpen: true }),
|
||||
},
|
||||
m: {
|
||||
title: "Create a new module",
|
||||
description: "Create a new module in the current project",
|
||||
action: () => toggleCreateModuleModal(true),
|
||||
},
|
||||
q: {
|
||||
title: "Create a new cycle",
|
||||
description: "Create a new cycle in the current project",
|
||||
action: () => toggleCreateCycleModal(true),
|
||||
},
|
||||
v: {
|
||||
title: "Create a new view",
|
||||
description: "Create a new view in the current project",
|
||||
action: () => toggleCreateViewModal(true),
|
||||
},
|
||||
backspace: {
|
||||
title: "Bulk delete issues",
|
||||
description: "Bulk delete issues in the current project",
|
||||
action: () => toggleBulkDeleteIssueModal(true),
|
||||
},
|
||||
delete: {
|
||||
title: "Bulk delete issues",
|
||||
description: "Bulk delete issues in the current project",
|
||||
action: () => toggleBulkDeleteIssueModal(true),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export const handleAdditionalKeyDownEvents = (e: KeyboardEvent) => null;
|
||||
|
||||
export const getNavigationShortcutsList = (): TCommandPaletteShortcut[] => [
|
||||
{ keys: "Ctrl,K", description: "Open command menu" },
|
||||
];
|
||||
|
||||
export const getCommonShortcutsList = (platform: string): TCommandPaletteShortcut[] => [
|
||||
{ keys: "P", description: "Create project" },
|
||||
{ keys: "C", description: "Create issue" },
|
||||
{ keys: "Q", description: "Create cycle" },
|
||||
{ keys: "M", description: "Create module" },
|
||||
{ keys: "V", description: "Create view" },
|
||||
{ keys: "D", description: "Create page" },
|
||||
{ keys: "Delete", description: "Bulk delete issues" },
|
||||
{ keys: "Shift,/", description: "Open shortcuts guide" },
|
||||
{
|
||||
keys: platform === "MacOS" ? "Ctrl,control,C" : "Ctrl,Alt,C",
|
||||
description: "Copy issue URL from the issue details page",
|
||||
},
|
||||
];
|
||||
|
||||
export const getAdditionalShortcutsList = (): TCommandPaletteShortcutList[] => [];
|
||||
@@ -0,0 +1,12 @@
|
||||
import { makeObservable } from "mobx";
|
||||
// types / constants
|
||||
import { BaseCommandPaletteStore, IBaseCommandPaletteStore } from "@/store/base-command-palette.store";
|
||||
|
||||
export type ICommandPaletteStore = IBaseCommandPaletteStore;
|
||||
|
||||
export class CommandPaletteStore extends BaseCommandPaletteStore implements ICommandPaletteStore {
|
||||
constructor() {
|
||||
super();
|
||||
makeObservable(this, {});
|
||||
}
|
||||
}
|
||||
@@ -2,87 +2,43 @@
|
||||
|
||||
import React, { useCallback, useEffect, FC, useMemo } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams, usePathname } from "next/navigation";
|
||||
import useSWR from "swr";
|
||||
import { useParams } from "next/navigation";
|
||||
// ui
|
||||
import { TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { CommandModal, ShortcutsModal } from "@/components/command-palette";
|
||||
import { BulkDeleteIssuesModal } from "@/components/core";
|
||||
import { CycleCreateUpdateModal } from "@/components/cycles";
|
||||
import { CreateUpdateIssueModal, DeleteIssueModal } from "@/components/issues";
|
||||
import { CreateUpdateModuleModal } from "@/components/modules";
|
||||
import { CreatePageModal } from "@/components/pages";
|
||||
import { CreateProjectModal } from "@/components/project";
|
||||
import { CreateUpdateProjectViewModal } from "@/components/views";
|
||||
// constants
|
||||
import { ISSUE_DETAILS } from "@/constants/fetch-keys";
|
||||
// helpers
|
||||
import { copyTextToClipboard } from "@/helpers/string.helper";
|
||||
// hooks
|
||||
import { useEventTracker, useUser, useAppTheme, useCommandPalette, useUserPermissions } from "@/hooks/store";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
import { useIssuesStore } from "@/hooks/use-issue-layout-store";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// plane web components
|
||||
import {
|
||||
IssueLevelModals,
|
||||
ProjectLevelModals,
|
||||
WorkspaceLevelModals,
|
||||
} from "@/plane-web/components/command-palette/modals";
|
||||
// plane web constants
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
|
||||
// services
|
||||
import { IssueService } from "@/services/issue";
|
||||
|
||||
// services
|
||||
const issueService = new IssueService();
|
||||
// plane web helpers
|
||||
import {
|
||||
getGlobalShortcutsList,
|
||||
getProjectShortcutsList,
|
||||
getWorkspaceShortcutsList,
|
||||
handleAdditionalKeyDownEvents,
|
||||
} from "@/plane-web/helpers/command-palette";
|
||||
|
||||
export const CommandPalette: FC = observer(() => {
|
||||
// router
|
||||
const router = useAppRouter();
|
||||
// router params
|
||||
const { workspaceSlug, projectId, issueId, cycleId, moduleId } = useParams();
|
||||
// pathname
|
||||
const pathname = usePathname();
|
||||
const { workspaceSlug, projectId, issueId } = useParams();
|
||||
// store hooks
|
||||
const { toggleSidebar } = useAppTheme();
|
||||
const { setTrackElement } = useEventTracker();
|
||||
const { platform } = usePlatformOS();
|
||||
const {
|
||||
data: currentUser,
|
||||
// canPerformProjectMemberActions,
|
||||
// canPerformWorkspaceMemberActions,
|
||||
canPerformAnyCreateAction,
|
||||
// canPerformProjectAdminActions,
|
||||
} = useUser();
|
||||
const {
|
||||
issues: { removeIssue },
|
||||
} = useIssuesStore();
|
||||
const {
|
||||
toggleCommandPaletteModal,
|
||||
isCreateIssueModalOpen,
|
||||
toggleCreateIssueModal,
|
||||
isCreateCycleModalOpen,
|
||||
toggleCreateCycleModal,
|
||||
createPageModal,
|
||||
toggleCreatePageModal,
|
||||
isCreateProjectModalOpen,
|
||||
toggleCreateProjectModal,
|
||||
isCreateModuleModalOpen,
|
||||
toggleCreateModuleModal,
|
||||
isCreateViewModalOpen,
|
||||
toggleCreateViewModal,
|
||||
isShortcutModalOpen,
|
||||
toggleShortcutModal,
|
||||
isBulkDeleteIssueModalOpen,
|
||||
toggleBulkDeleteIssueModal,
|
||||
isDeleteIssueModalOpen,
|
||||
toggleDeleteIssueModal,
|
||||
isAnyModalOpen,
|
||||
} = useCommandPalette();
|
||||
const { data: currentUser, canPerformAnyCreateAction } = useUser();
|
||||
const { toggleCommandPaletteModal, isShortcutModalOpen, toggleShortcutModal, isAnyModalOpen } = useCommandPalette();
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
|
||||
const { data: issueDetails } = useSWR(
|
||||
workspaceSlug && projectId && issueId ? ISSUE_DETAILS(issueId as string) : null,
|
||||
workspaceSlug && projectId && issueId
|
||||
? () => issueService.retrieve(workspaceSlug as string, projectId as string, issueId as string)
|
||||
: null
|
||||
);
|
||||
|
||||
// derived values
|
||||
const canPerformWorkspaceMemberActions = allowPermissions(
|
||||
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
|
||||
@@ -170,62 +126,11 @@ export const CommandPalette: FC = observer(() => {
|
||||
project: Record<string, { title: string; description: string; action: () => void }>;
|
||||
} = useMemo(
|
||||
() => ({
|
||||
global: {
|
||||
c: {
|
||||
title: "Create a new issue",
|
||||
description: "Create a new issue in the current project",
|
||||
action: () => toggleCreateIssueModal(true),
|
||||
},
|
||||
},
|
||||
workspace: {
|
||||
p: {
|
||||
title: "Create a new project",
|
||||
description: "Create a new project in the current workspace",
|
||||
action: () => toggleCreateProjectModal(true),
|
||||
},
|
||||
},
|
||||
project: {
|
||||
d: {
|
||||
title: "Create a new page",
|
||||
description: "Create a new page in the current project",
|
||||
action: () => toggleCreatePageModal({ isOpen: true }),
|
||||
},
|
||||
m: {
|
||||
title: "Create a new module",
|
||||
description: "Create a new module in the current project",
|
||||
action: () => toggleCreateModuleModal(true),
|
||||
},
|
||||
q: {
|
||||
title: "Create a new cycle",
|
||||
description: "Create a new cycle in the current project",
|
||||
action: () => toggleCreateCycleModal(true),
|
||||
},
|
||||
v: {
|
||||
title: "Create a new view",
|
||||
description: "Create a new view in the current project",
|
||||
action: () => toggleCreateViewModal(true),
|
||||
},
|
||||
backspace: {
|
||||
title: "Bulk delete issues",
|
||||
description: "Bulk delete issues in the current project",
|
||||
action: () => toggleBulkDeleteIssueModal(true),
|
||||
},
|
||||
delete: {
|
||||
title: "Bulk delete issues",
|
||||
description: "Bulk delete issues in the current project",
|
||||
action: () => toggleBulkDeleteIssueModal(true),
|
||||
},
|
||||
},
|
||||
global: getGlobalShortcutsList(),
|
||||
workspace: getWorkspaceShortcutsList(),
|
||||
project: getProjectShortcutsList(),
|
||||
}),
|
||||
[
|
||||
toggleBulkDeleteIssueModal,
|
||||
toggleCreateCycleModal,
|
||||
toggleCreateIssueModal,
|
||||
toggleCreateModuleModal,
|
||||
toggleCreatePageModal,
|
||||
toggleCreateProjectModal,
|
||||
toggleCreateViewModal,
|
||||
]
|
||||
[]
|
||||
);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
@@ -296,6 +201,8 @@ export const CommandPalette: FC = observer(() => {
|
||||
shortcutsList.project[keyPressed].action();
|
||||
}
|
||||
}
|
||||
// Additional keydown events
|
||||
handleAdditionalKeyDownEvents(e);
|
||||
},
|
||||
[
|
||||
copyIssueUrlToClipboard,
|
||||
@@ -320,75 +227,16 @@ export const CommandPalette: FC = observer(() => {
|
||||
return () => document.removeEventListener("keydown", handleKeyDown);
|
||||
}, [handleKeyDown]);
|
||||
|
||||
const isDraftIssue = pathname?.includes("draft-issues") || false;
|
||||
|
||||
if (!currentUser) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ShortcutsModal isOpen={isShortcutModalOpen} onClose={() => toggleShortcutModal(false)} />
|
||||
{workspaceSlug && (
|
||||
<CreateProjectModal
|
||||
isOpen={isCreateProjectModalOpen}
|
||||
onClose={() => toggleCreateProjectModal(false)}
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
/>
|
||||
)}
|
||||
{workspaceSlug && <WorkspaceLevelModals workspaceSlug={workspaceSlug.toString()} />}
|
||||
{workspaceSlug && projectId && (
|
||||
<>
|
||||
<CycleCreateUpdateModal
|
||||
isOpen={isCreateCycleModalOpen}
|
||||
handleClose={() => toggleCreateCycleModal(false)}
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
/>
|
||||
<CreateUpdateModuleModal
|
||||
isOpen={isCreateModuleModalOpen}
|
||||
onClose={() => toggleCreateModuleModal(false)}
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
/>
|
||||
<CreateUpdateProjectViewModal
|
||||
isOpen={isCreateViewModalOpen}
|
||||
onClose={() => toggleCreateViewModal(false)}
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
/>
|
||||
<CreatePageModal
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
isModalOpen={createPageModal.isOpen}
|
||||
pageAccess={createPageModal.pageAccess}
|
||||
handleModalClose={() => toggleCreatePageModal({ isOpen: false })}
|
||||
redirectionEnabled
|
||||
/>
|
||||
</>
|
||||
<ProjectLevelModals workspaceSlug={workspaceSlug.toString()} projectId={projectId.toString()} />
|
||||
)}
|
||||
|
||||
<CreateUpdateIssueModal
|
||||
isOpen={isCreateIssueModalOpen}
|
||||
onClose={() => toggleCreateIssueModal(false)}
|
||||
data={cycleId ? { cycle_id: cycleId.toString() } : moduleId ? { module_ids: [moduleId.toString()] } : undefined}
|
||||
isDraft={isDraftIssue}
|
||||
/>
|
||||
|
||||
{workspaceSlug && projectId && issueId && issueDetails && (
|
||||
<DeleteIssueModal
|
||||
handleClose={() => toggleDeleteIssueModal(false)}
|
||||
isOpen={isDeleteIssueModalOpen}
|
||||
data={issueDetails}
|
||||
onSubmit={async () => {
|
||||
await removeIssue(workspaceSlug.toString(), projectId.toString(), issueId.toString());
|
||||
router.push(`/${workspaceSlug}/projects/${projectId}/issues`);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<BulkDeleteIssuesModal
|
||||
isOpen={isBulkDeleteIssueModalOpen}
|
||||
onClose={() => toggleBulkDeleteIssueModal(false)}
|
||||
user={currentUser}
|
||||
/>
|
||||
<IssueLevelModals />
|
||||
<CommandModal />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -3,6 +3,12 @@ import { Command } from "lucide-react";
|
||||
import { substringMatch } from "@/helpers/string.helper";
|
||||
// hooks
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// plane web helpers
|
||||
import {
|
||||
getAdditionalShortcutsList,
|
||||
getCommonShortcutsList,
|
||||
getNavigationShortcutsList,
|
||||
} from "@/plane-web/helpers/command-palette";
|
||||
|
||||
type Props = {
|
||||
searchQuery: string;
|
||||
@@ -16,26 +22,14 @@ export const ShortcutCommandsList: React.FC<Props> = (props) => {
|
||||
{
|
||||
key: "navigation",
|
||||
title: "Navigation",
|
||||
shortcuts: [{ keys: "Ctrl,K", description: "Open command menu" }],
|
||||
shortcuts: getNavigationShortcutsList(),
|
||||
},
|
||||
{
|
||||
key: "common",
|
||||
title: "Common",
|
||||
shortcuts: [
|
||||
{ keys: "P", description: "Create project" },
|
||||
{ keys: "C", description: "Create issue" },
|
||||
{ keys: "Q", description: "Create cycle" },
|
||||
{ keys: "M", description: "Create module" },
|
||||
{ keys: "V", description: "Create view" },
|
||||
{ keys: "D", description: "Create page" },
|
||||
{ keys: "Delete", description: "Bulk delete issues" },
|
||||
{ keys: "Shift,/", description: "Open shortcuts guide" },
|
||||
{
|
||||
keys: platform === "MacOS" ? "Ctrl,control,C" : "Ctrl,Alt,C",
|
||||
description: "Copy issue URL from the issue details page",
|
||||
},
|
||||
],
|
||||
shortcuts: getCommonShortcutsList(platform),
|
||||
},
|
||||
...getAdditionalShortcutsList(),
|
||||
];
|
||||
|
||||
const filteredShortcuts = KEYBOARD_SHORTCUTS.map((category) => {
|
||||
@@ -69,7 +63,11 @@ export const ShortcutCommandsList: React.FC<Props> = (props) => {
|
||||
<div key={key} className="flex items-center gap-1">
|
||||
{key === "Ctrl" ? (
|
||||
<div className="grid h-6 min-w-[1.5rem] place-items-center rounded-sm border-[0.5px] border-custom-border-200 bg-custom-background-90 px-1.5 text-[10px] text-custom-text-200">
|
||||
{ platform === "MacOS" ? <Command className="h-2.5 w-2.5 text-custom-text-200" /> : 'Ctrl'}
|
||||
{platform === "MacOS" ? (
|
||||
<Command className="h-2.5 w-2.5 text-custom-text-200" />
|
||||
) : (
|
||||
"Ctrl"
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<kbd className="grid h-6 min-w-[1.5rem] place-items-center rounded-sm border-[0.5px] border-custom-border-200 bg-custom-background-90 px-1.5 text-[10px] text-custom-text-200">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useRef, useState } from "react";
|
||||
// Plane
|
||||
import { setToast } from "@plane/ui";
|
||||
import { setToast, TOAST_TYPE } from "@plane/ui";
|
||||
// hooks
|
||||
import { useTimeLineChartStore } from "@/hooks/use-timeline-chart";
|
||||
//
|
||||
@@ -103,7 +103,7 @@ export const useGanttResizable = (
|
||||
const deltaWidth = Math.round((width - (block.position?.width ?? 0)) / dayWidth) * dayWidth;
|
||||
|
||||
// call update blockPosition
|
||||
if (deltaWidth || deltaLeft) updateBlockPosition(block.id, deltaLeft, deltaWidth, dragDirection !== "move");
|
||||
if (deltaWidth || deltaLeft) updateBlockPosition(block.id, deltaLeft, deltaWidth);
|
||||
};
|
||||
|
||||
// remove event listeners and call updateBlockDates
|
||||
@@ -119,10 +119,14 @@ export const useGanttResizable = (
|
||||
(dragDirection === "left" && !block.start_date) || (dragDirection === "right" && !block.target_date);
|
||||
|
||||
try {
|
||||
const blockUpdates = getUpdatedPositionAfterDrag(block.id, shouldUpdateHalfBlock, dragDirection !== "move");
|
||||
updateBlockDates && updateBlockDates(blockUpdates);
|
||||
} catch (e) {
|
||||
setToast;
|
||||
const blockUpdates = getUpdatedPositionAfterDrag(block.id, shouldUpdateHalfBlock);
|
||||
if (updateBlockDates) updateBlockDates(blockUpdates);
|
||||
} catch {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error",
|
||||
message: "Something went wrong while updating block dates",
|
||||
});
|
||||
}
|
||||
|
||||
setIsDragging(false);
|
||||
|
||||
@@ -82,6 +82,7 @@ export const IssueActivityItem: FC<TIssueActivityItem> = observer((props) => {
|
||||
return <IssueAttachmentActivity {...componentDefaultProps} showIssue={false} />;
|
||||
case "archived_at":
|
||||
return <IssueArchivedAtActivity {...componentDefaultProps} />;
|
||||
case "intake":
|
||||
case "inbox":
|
||||
return <IssueInboxActivity {...componentDefaultProps} />;
|
||||
case "type":
|
||||
|
||||
@@ -18,7 +18,7 @@ import { ContentWrapper } from "@plane/ui";
|
||||
import RenderIfVisible from "@/components/core/render-if-visible-HOC";
|
||||
import { KanbanColumnLoader } from "@/components/ui";
|
||||
// hooks
|
||||
import { useCycle, useKanbanView, useLabel, useMember, useModule, useProject, useProjectState } from "@/hooks/store";
|
||||
import { useKanbanView } from "@/hooks/store";
|
||||
import { useIssueStoreType } from "@/hooks/use-issue-layout-store";
|
||||
// types
|
||||
// parent components
|
||||
@@ -87,30 +87,16 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
|
||||
dropErrorMessage,
|
||||
subGroupIndex = 0,
|
||||
} = props;
|
||||
|
||||
// store hooks
|
||||
const storeType = useIssueStoreType();
|
||||
|
||||
const member = useMember();
|
||||
const project = useProject();
|
||||
const label = useLabel();
|
||||
const cycle = useCycle();
|
||||
const moduleInfo = useModule();
|
||||
const projectState = useProjectState();
|
||||
const issueKanBanView = useKanbanView();
|
||||
|
||||
// derived values
|
||||
const isDragDisabled = !issueKanBanView?.getCanUserDragDrop(group_by, sub_group_by);
|
||||
|
||||
const list = getGroupByColumns(
|
||||
group_by as GroupByColumnTypes,
|
||||
project,
|
||||
cycle,
|
||||
moduleInfo,
|
||||
label,
|
||||
projectState,
|
||||
member,
|
||||
true,
|
||||
isWorkspaceLevel(storeType)
|
||||
);
|
||||
const list = getGroupByColumns({
|
||||
groupBy: group_by as GroupByColumnTypes,
|
||||
includeNone: true,
|
||||
isWorkspaceLevel: isWorkspaceLevel(storeType),
|
||||
});
|
||||
|
||||
if (!list) return null;
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
// UI
|
||||
import { Row } from "@plane/ui";
|
||||
// hooks
|
||||
import { useCycle, useLabel, useMember, useModule, useProject, useProjectState } from "@/hooks/store";
|
||||
import { useIssueStoreType } from "@/hooks/use-issue-layout-store";
|
||||
// components
|
||||
import { TRenderQuickActions } from "../list/list-view-types";
|
||||
@@ -262,38 +261,19 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
||||
quickAddCallback,
|
||||
scrollableContainerRef,
|
||||
} = props;
|
||||
|
||||
// store hooks
|
||||
const storeType = useIssueStoreType();
|
||||
|
||||
const member = useMember();
|
||||
const project = useProject();
|
||||
const label = useLabel();
|
||||
const cycle = useCycle();
|
||||
const projectModule = useModule();
|
||||
const projectState = useProjectState();
|
||||
|
||||
const groupByList = getGroupByColumns(
|
||||
group_by as GroupByColumnTypes,
|
||||
project,
|
||||
cycle,
|
||||
projectModule,
|
||||
label,
|
||||
projectState,
|
||||
member,
|
||||
true,
|
||||
isWorkspaceLevel(storeType)
|
||||
);
|
||||
const subGroupByList = getGroupByColumns(
|
||||
sub_group_by as GroupByColumnTypes,
|
||||
project,
|
||||
cycle,
|
||||
projectModule,
|
||||
label,
|
||||
projectState,
|
||||
member,
|
||||
true,
|
||||
isWorkspaceLevel(storeType)
|
||||
);
|
||||
// derived values
|
||||
const groupByList = getGroupByColumns({
|
||||
groupBy: group_by as GroupByColumnTypes,
|
||||
includeNone: true,
|
||||
isWorkspaceLevel: isWorkspaceLevel(storeType),
|
||||
});
|
||||
const subGroupByList = getGroupByColumns({
|
||||
groupBy: sub_group_by as GroupByColumnTypes,
|
||||
includeNone: true,
|
||||
isWorkspaceLevel: isWorkspaceLevel(storeType),
|
||||
});
|
||||
|
||||
if (!groupByList || !subGroupByList) return null;
|
||||
|
||||
|
||||
@@ -18,9 +18,7 @@ import {
|
||||
} from "@plane/types";
|
||||
// components
|
||||
import { MultipleSelectGroup } from "@/components/core";
|
||||
|
||||
// hooks
|
||||
import { useCycle, useLabel, useMember, useModule, useProject, useProjectState } from "@/hooks/store";
|
||||
import { useIssueStoreType } from "@/hooks/use-issue-layout-store";
|
||||
// plane web components
|
||||
import { IssueBulkOperationsRoot } from "@/plane-web/components/issues";
|
||||
@@ -75,29 +73,16 @@ export const List: React.FC<IList> = observer((props) => {
|
||||
} = props;
|
||||
|
||||
const storeType = useIssueStoreType();
|
||||
// store hooks
|
||||
const member = useMember();
|
||||
const project = useProject();
|
||||
const label = useLabel();
|
||||
const projectState = useProjectState();
|
||||
const cycle = useCycle();
|
||||
const projectModule = useModule();
|
||||
// plane web hooks
|
||||
const isBulkOperationsEnabled = useBulkOperationStatus();
|
||||
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const groups = getGroupByColumns(
|
||||
group_by as GroupByColumnTypes,
|
||||
project,
|
||||
cycle,
|
||||
projectModule,
|
||||
label,
|
||||
projectState,
|
||||
member,
|
||||
true,
|
||||
isWorkspaceLevel(storeType)
|
||||
);
|
||||
const groups = getGroupByColumns({
|
||||
groupBy: group_by as GroupByColumnTypes,
|
||||
includeNone: true,
|
||||
isWorkspaceLevel: isWorkspaceLevel(storeType),
|
||||
});
|
||||
|
||||
// Enable Auto Scroll for Main Kanban
|
||||
useEffect(() => {
|
||||
|
||||
@@ -36,13 +36,8 @@ import { STATE_GROUPS } from "@/constants/state";
|
||||
import { renderFormattedDate } from "@/helpers/date-time.helper";
|
||||
import { getFileURL } from "@/helpers/file.helper";
|
||||
// store
|
||||
import { ICycleStore } from "@/store/cycle.store";
|
||||
import { store } from "@/lib/store-context";
|
||||
import { ISSUE_FILTER_DEFAULT_DATA } from "@/store/issue/helpers/base-issues.store";
|
||||
import { ILabelStore } from "@/store/label.store";
|
||||
import { IMemberRootStore } from "@/store/member";
|
||||
import { IModuleStore } from "@/store/module.store";
|
||||
import { IProjectStore } from "@/store/project/project.store";
|
||||
import { IStateStore } from "@/store/state.store";
|
||||
|
||||
export const HIGHLIGHT_CLASS = "highlight";
|
||||
export const HIGHLIGHT_WITH_LINE = "highlight-with-line";
|
||||
@@ -65,51 +60,61 @@ export type IssueUpdates = {
|
||||
export const isWorkspaceLevel = (type: EIssuesStoreType) =>
|
||||
[EIssuesStoreType.PROFILE, EIssuesStoreType.GLOBAL].includes(type) ? true : false;
|
||||
|
||||
export const getGroupByColumns = (
|
||||
groupBy: GroupByColumnTypes | null,
|
||||
project: IProjectStore,
|
||||
cycle: ICycleStore,
|
||||
module: IModuleStore,
|
||||
label: ILabelStore,
|
||||
projectState: IStateStore,
|
||||
member: IMemberRootStore,
|
||||
includeNone?: boolean,
|
||||
isWorkspaceLevel?: boolean
|
||||
): IGroupByColumn[] | undefined => {
|
||||
switch (groupBy) {
|
||||
case "project":
|
||||
return getProjectColumns(project);
|
||||
case "cycle":
|
||||
return getCycleColumns(project, cycle);
|
||||
case "module":
|
||||
return getModuleColumns(project, module);
|
||||
case "state":
|
||||
return getStateColumns(projectState);
|
||||
case "state_detail.group":
|
||||
return getStateGroupColumns();
|
||||
case "priority":
|
||||
return getPriorityColumns();
|
||||
case "labels":
|
||||
return getLabelsColumns(label, isWorkspaceLevel) as any;
|
||||
case "assignees":
|
||||
return getAssigneeColumns(member) as any;
|
||||
case "created_by":
|
||||
return getCreatedByColumns(member) as any;
|
||||
default:
|
||||
if (includeNone) return [{ id: `All Issues`, name: `All Issues`, payload: {}, icon: undefined }];
|
||||
}
|
||||
type TGetGroupByColumns = {
|
||||
groupBy: GroupByColumnTypes | null;
|
||||
includeNone: boolean;
|
||||
isWorkspaceLevel: boolean;
|
||||
};
|
||||
|
||||
const getProjectColumns = (project: IProjectStore): IGroupByColumn[] | undefined => {
|
||||
const { workspaceProjectIds: projectIds, projectMap } = project;
|
||||
// NOTE: Type of groupBy is different compared to what's being passed from the components.
|
||||
// We are using `as` to typecast it to the expected type.
|
||||
// It can break the includeNone logic if not handled properly.
|
||||
export const getGroupByColumns = ({
|
||||
groupBy,
|
||||
includeNone,
|
||||
isWorkspaceLevel,
|
||||
}: TGetGroupByColumns): IGroupByColumn[] | undefined => {
|
||||
// If no groupBy is specified and includeNone is true, return "All Issues" group
|
||||
if (!groupBy && includeNone) {
|
||||
return [
|
||||
{
|
||||
id: "All Issues",
|
||||
name: "All Issues",
|
||||
payload: {},
|
||||
icon: undefined,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// Return undefined if no valid groupBy
|
||||
if (!groupBy) return undefined;
|
||||
|
||||
// Map of group by options to their corresponding column getter functions
|
||||
const groupByColumnMap: Record<GroupByColumnTypes, () => IGroupByColumn[] | undefined> = {
|
||||
project: getProjectColumns,
|
||||
cycle: getCycleColumns,
|
||||
module: getModuleColumns,
|
||||
state: getStateColumns,
|
||||
"state_detail.group": getStateGroupColumns,
|
||||
priority: getPriorityColumns,
|
||||
labels: () => getLabelsColumns(isWorkspaceLevel),
|
||||
assignees: getAssigneeColumns,
|
||||
created_by: getCreatedByColumns,
|
||||
};
|
||||
|
||||
// Get and return the columns for the specified group by option
|
||||
return groupByColumnMap[groupBy]?.();
|
||||
};
|
||||
|
||||
const getProjectColumns = (): IGroupByColumn[] | undefined => {
|
||||
const { joinedProjectIds: projectIds, projectMap } = store.projectRoot.project;
|
||||
// Return undefined if no project ids
|
||||
if (!projectIds) return;
|
||||
|
||||
// Map project ids to project columns
|
||||
return projectIds
|
||||
.filter((projectId) => !!projectMap[projectId])
|
||||
.map((projectId) => {
|
||||
.map((projectId: string) => {
|
||||
const project = projectMap[projectId];
|
||||
|
||||
if (!project) return;
|
||||
return {
|
||||
id: project.id,
|
||||
name: project.name,
|
||||
@@ -120,78 +125,71 @@ const getProjectColumns = (project: IProjectStore): IGroupByColumn[] | undefined
|
||||
),
|
||||
payload: { project_id: project.id },
|
||||
};
|
||||
}) as any;
|
||||
})
|
||||
.filter((column) => column !== undefined) as IGroupByColumn[];
|
||||
};
|
||||
|
||||
const getCycleColumns = (projectStore: IProjectStore, cycleStore: ICycleStore): IGroupByColumn[] | undefined => {
|
||||
const { currentProjectDetails } = projectStore;
|
||||
const { getProjectCycleIds, getCycleById } = cycleStore;
|
||||
|
||||
const getCycleColumns = (): IGroupByColumn[] | undefined => {
|
||||
const { currentProjectDetails } = store.projectRoot.project;
|
||||
// Check for the current project details
|
||||
if (!currentProjectDetails || !currentProjectDetails?.id) return;
|
||||
|
||||
const cycleIds = currentProjectDetails?.id ? getProjectCycleIds(currentProjectDetails?.id) : undefined;
|
||||
if (!cycleIds) return;
|
||||
|
||||
const cycles = [];
|
||||
|
||||
cycleIds.map((cycleId) => {
|
||||
const cycle = getCycleById(cycleId);
|
||||
if (cycle) {
|
||||
const cycleStatus = cycle.status ? (cycle.status.toLocaleLowerCase() as TCycleGroups) : "draft";
|
||||
const isDropDisabled = cycleStatus === "completed";
|
||||
cycles.push({
|
||||
id: cycle.id,
|
||||
name: cycle.name,
|
||||
icon: <CycleGroupIcon cycleGroup={cycleStatus as TCycleGroups} className="h-3.5 w-3.5" />,
|
||||
payload: { cycle_id: cycle.id },
|
||||
isDropDisabled,
|
||||
dropErrorMessage: isDropDisabled ? "Issue cannot be moved to completed cycles" : undefined,
|
||||
});
|
||||
}
|
||||
const { getProjectCycleDetails } = store.cycle;
|
||||
// Get the cycle details for the current project
|
||||
const cycleDetails = currentProjectDetails?.id ? getProjectCycleDetails(currentProjectDetails?.id) : undefined;
|
||||
// Map the cycle details to the group by columns
|
||||
const cycles: IGroupByColumn[] = [];
|
||||
cycleDetails?.map((cycle) => {
|
||||
const cycleStatus = cycle.status ? (cycle.status.toLocaleLowerCase() as TCycleGroups) : "draft";
|
||||
const isDropDisabled = cycleStatus === "completed";
|
||||
cycles.push({
|
||||
id: cycle.id,
|
||||
name: cycle.name,
|
||||
icon: <CycleGroupIcon cycleGroup={cycleStatus as TCycleGroups} className="h-3.5 w-3.5" />,
|
||||
payload: { cycle_id: cycle.id },
|
||||
isDropDisabled,
|
||||
dropErrorMessage: isDropDisabled ? "Issue cannot be moved to completed cycles" : undefined,
|
||||
});
|
||||
});
|
||||
cycles.push({
|
||||
id: "None",
|
||||
name: "None",
|
||||
icon: <ContrastIcon className="h-3.5 w-3.5" />,
|
||||
payload: {},
|
||||
});
|
||||
|
||||
return cycles as any;
|
||||
return cycles;
|
||||
};
|
||||
|
||||
const getModuleColumns = (projectStore: IProjectStore, moduleStore: IModuleStore): IGroupByColumn[] | undefined => {
|
||||
const { currentProjectDetails } = projectStore;
|
||||
const { getProjectModuleIds, getModuleById } = moduleStore;
|
||||
|
||||
const getModuleColumns = (): IGroupByColumn[] | undefined => {
|
||||
// get current project details
|
||||
const { currentProjectDetails } = store.projectRoot.project;
|
||||
if (!currentProjectDetails || !currentProjectDetails?.id) return;
|
||||
|
||||
const moduleIds = currentProjectDetails?.id ? getProjectModuleIds(currentProjectDetails?.id) : undefined;
|
||||
if (!moduleIds) return;
|
||||
|
||||
const modules = [];
|
||||
|
||||
moduleIds.map((moduleId) => {
|
||||
const moduleInfo = getModuleById(moduleId);
|
||||
if (moduleInfo)
|
||||
modules.push({
|
||||
id: moduleInfo.id,
|
||||
name: moduleInfo.name,
|
||||
icon: <DiceIcon className="h-3.5 w-3.5" />,
|
||||
payload: { module_ids: [moduleInfo.id] },
|
||||
});
|
||||
}) as any;
|
||||
// get project module ids and module details
|
||||
const { getProjectModuleDetails } = store.module;
|
||||
// get module details
|
||||
const moduleDetails = currentProjectDetails?.id ? getProjectModuleDetails(currentProjectDetails?.id) : undefined;
|
||||
// map module details to group by columns
|
||||
const modules: IGroupByColumn[] = [];
|
||||
moduleDetails?.map((module) => {
|
||||
modules.push({
|
||||
id: module.id,
|
||||
name: module.name,
|
||||
icon: <DiceIcon className="h-3.5 w-3.5" />,
|
||||
payload: { module_ids: [module.id] },
|
||||
});
|
||||
});
|
||||
modules.push({
|
||||
id: "None",
|
||||
name: "None",
|
||||
icon: <DiceIcon className="h-3.5 w-3.5" />,
|
||||
payload: {},
|
||||
});
|
||||
|
||||
return modules as any;
|
||||
return modules;
|
||||
};
|
||||
|
||||
const getStateColumns = (projectState: IStateStore): IGroupByColumn[] | undefined => {
|
||||
const { projectStates } = projectState;
|
||||
const getStateColumns = (): IGroupByColumn[] | undefined => {
|
||||
const { projectStates } = store.state;
|
||||
if (!projectStates) return;
|
||||
|
||||
// map project states to group by columns
|
||||
return projectStates.map((state) => ({
|
||||
id: state.id,
|
||||
name: state.name,
|
||||
@@ -201,12 +199,12 @@ const getStateColumns = (projectState: IStateStore): IGroupByColumn[] | undefine
|
||||
</div>
|
||||
),
|
||||
payload: { state_id: state.id },
|
||||
})) as any;
|
||||
}));
|
||||
};
|
||||
|
||||
const getStateGroupColumns = () => {
|
||||
const getStateGroupColumns = (): IGroupByColumn[] => {
|
||||
const stateGroups = STATE_GROUPS;
|
||||
|
||||
// map state groups to group by columns
|
||||
return Object.values(stateGroups).map((stateGroup) => ({
|
||||
id: stateGroup.key,
|
||||
name: stateGroup.label,
|
||||
@@ -219,9 +217,9 @@ const getStateGroupColumns = () => {
|
||||
}));
|
||||
};
|
||||
|
||||
const getPriorityColumns = () => {
|
||||
const getPriorityColumns = (): IGroupByColumn[] => {
|
||||
const priorities = ISSUE_PRIORITIES;
|
||||
|
||||
// map priorities to group by columns
|
||||
return priorities.map((priority) => ({
|
||||
id: priority.key,
|
||||
name: priority.title,
|
||||
@@ -230,14 +228,14 @@ const getPriorityColumns = () => {
|
||||
}));
|
||||
};
|
||||
|
||||
const getLabelsColumns = (label: ILabelStore, isWorkspaceLevel: boolean = false) => {
|
||||
const { workspaceLabels, projectLabels } = label;
|
||||
|
||||
const getLabelsColumns = (isWorkspaceLevel: boolean = false): IGroupByColumn[] => {
|
||||
const { workspaceLabels, projectLabels } = store.label;
|
||||
// map labels to group by columns
|
||||
const labels = [
|
||||
...(isWorkspaceLevel ? workspaceLabels || [] : projectLabels || []),
|
||||
{ id: "None", name: "None", color: "#666" },
|
||||
];
|
||||
|
||||
// map labels to group by columns
|
||||
return labels.map((label) => ({
|
||||
id: label.id,
|
||||
name: label.name,
|
||||
@@ -248,15 +246,14 @@ const getLabelsColumns = (label: ILabelStore, isWorkspaceLevel: boolean = false)
|
||||
}));
|
||||
};
|
||||
|
||||
const getAssigneeColumns = (member: IMemberRootStore) => {
|
||||
const getAssigneeColumns = (): IGroupByColumn[] | undefined => {
|
||||
const {
|
||||
project: { projectMemberIds },
|
||||
getUserDetails,
|
||||
} = member;
|
||||
|
||||
} = store.memberRoot;
|
||||
if (!projectMemberIds) return;
|
||||
|
||||
const assigneeColumns: any = projectMemberIds.map((memberId) => {
|
||||
// Map project member ids to group by assignee columns
|
||||
const assigneeColumns: IGroupByColumn[] = projectMemberIds.map((memberId) => {
|
||||
const member = getUserDetails(memberId);
|
||||
return {
|
||||
id: memberId,
|
||||
@@ -265,20 +262,17 @@ const getAssigneeColumns = (member: IMemberRootStore) => {
|
||||
payload: { assignee_ids: [memberId] },
|
||||
};
|
||||
});
|
||||
|
||||
assigneeColumns.push({ id: "None", name: "None", icon: <Avatar size="md" />, payload: {} });
|
||||
|
||||
return assigneeColumns;
|
||||
};
|
||||
|
||||
const getCreatedByColumns = (member: IMemberRootStore) => {
|
||||
const getCreatedByColumns = (): IGroupByColumn[] | undefined => {
|
||||
const {
|
||||
project: { projectMemberIds },
|
||||
getUserDetails,
|
||||
} = member;
|
||||
|
||||
} = store.memberRoot;
|
||||
if (!projectMemberIds) return;
|
||||
|
||||
// Map project member ids to group by created by columns
|
||||
return projectMemberIds.map((memberId) => {
|
||||
const member = getUserDetails(memberId);
|
||||
return {
|
||||
|
||||
@@ -4,7 +4,7 @@ import React, { FC, useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { IUserEmailNotificationSettings } from "@plane/types";
|
||||
// ui
|
||||
import { Button, Checkbox, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { ToggleSwitch, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// services
|
||||
import { UserService } from "@/services/user.service";
|
||||
// types
|
||||
@@ -20,35 +20,32 @@ export const EmailNotificationForm: FC<IEmailNotificationFormProps> = (props) =>
|
||||
const { data } = props;
|
||||
// form data
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
reset,
|
||||
formState: { isSubmitting, dirtyFields },
|
||||
} = useForm<IUserEmailNotificationSettings>({
|
||||
defaultValues: {
|
||||
...data,
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = async (formData: IUserEmailNotificationSettings) => {
|
||||
// Get the dirty fields from the form data and create a payload
|
||||
let payload = {};
|
||||
Object.keys(dirtyFields).forEach((key) => {
|
||||
payload = {
|
||||
...payload,
|
||||
[key]: formData[key as keyof IUserEmailNotificationSettings],
|
||||
};
|
||||
});
|
||||
await userService
|
||||
.updateCurrentUserEmailNotificationSettings(payload)
|
||||
.then(() =>
|
||||
setToast({
|
||||
title: "Success!",
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
message: "Email Notification Settings updated successfully",
|
||||
})
|
||||
)
|
||||
.catch((err) => console.error(err));
|
||||
const handleSettingChange = async (key: keyof IUserEmailNotificationSettings, value: boolean) => {
|
||||
try {
|
||||
await userService.updateCurrentUserEmailNotificationSettings({
|
||||
[key]: value,
|
||||
});
|
||||
setToast({
|
||||
title: "Success!",
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
message: "Email notification setting updated successfully",
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setToast({
|
||||
title: "Error!",
|
||||
type: TOAST_TYPE.ERROR,
|
||||
message: "Failed to update email notification setting",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -64,7 +61,7 @@ export const EmailNotificationForm: FC<IEmailNotificationFormProps> = (props) =>
|
||||
<div className="grow">
|
||||
<div className="pb-1 text-base font-medium text-custom-text-100">Property changes</div>
|
||||
<div className="text-sm font-normal text-custom-text-300">
|
||||
Notify me when issue’s properties like assignees, priority, estimates or anything else changes.
|
||||
Notify me when issue's properties like assignees, priority, estimates or anything else changes.
|
||||
</div>
|
||||
</div>
|
||||
<div className="shrink-0">
|
||||
@@ -72,7 +69,14 @@ export const EmailNotificationForm: FC<IEmailNotificationFormProps> = (props) =>
|
||||
control={control}
|
||||
name="property_change"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<Checkbox checked={value} onChange={() => onChange(!value)} containerClassName="mx-2" />
|
||||
<ToggleSwitch
|
||||
value={value}
|
||||
onChange={(newValue) => {
|
||||
onChange(newValue);
|
||||
handleSettingChange("property_change", newValue);
|
||||
}}
|
||||
size="sm"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
@@ -89,12 +93,13 @@ export const EmailNotificationForm: FC<IEmailNotificationFormProps> = (props) =>
|
||||
control={control}
|
||||
name="state_change"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<Checkbox
|
||||
checked={value}
|
||||
onChange={() => {
|
||||
onChange(!value);
|
||||
<ToggleSwitch
|
||||
value={value}
|
||||
onChange={(newValue) => {
|
||||
onChange(newValue);
|
||||
handleSettingChange("state_change", newValue);
|
||||
}}
|
||||
containerClassName="mx-2"
|
||||
size="sm"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@@ -110,7 +115,14 @@ export const EmailNotificationForm: FC<IEmailNotificationFormProps> = (props) =>
|
||||
control={control}
|
||||
name="issue_completed"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<Checkbox checked={value} onChange={() => onChange(!value)} containerClassName="mx-2" />
|
||||
<ToggleSwitch
|
||||
value={value}
|
||||
onChange={(newValue) => {
|
||||
onChange(newValue);
|
||||
handleSettingChange("issue_completed", newValue);
|
||||
}}
|
||||
size="sm"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
@@ -127,7 +139,14 @@ export const EmailNotificationForm: FC<IEmailNotificationFormProps> = (props) =>
|
||||
control={control}
|
||||
name="comment"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<Checkbox checked={value} onChange={() => onChange(!value)} containerClassName="mx-2" />
|
||||
<ToggleSwitch
|
||||
value={value}
|
||||
onChange={(newValue) => {
|
||||
onChange(newValue);
|
||||
handleSettingChange("comment", newValue);
|
||||
}}
|
||||
size="sm"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
@@ -144,17 +163,19 @@ export const EmailNotificationForm: FC<IEmailNotificationFormProps> = (props) =>
|
||||
control={control}
|
||||
name="mention"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<Checkbox checked={value} onChange={() => onChange(!value)} containerClassName="mx-2" />
|
||||
<ToggleSwitch
|
||||
value={value}
|
||||
onChange={(newValue) => {
|
||||
onChange(newValue);
|
||||
handleSettingChange("mention", newValue);
|
||||
}}
|
||||
size="sm"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center py-12">
|
||||
<Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isSubmitting}>
|
||||
{isSubmitting ? "Saving..." : "Save changes"}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from "./notification-app-sidebar-option";
|
||||
export * from "./sidebar";
|
||||
export * from "./root";
|
||||
|
||||
+2
-1
@@ -11,7 +11,6 @@ import {
|
||||
NotificationEmptyState,
|
||||
NotificationSidebarHeader,
|
||||
AppliedFilters,
|
||||
NotificationCardListRoot,
|
||||
} from "@/components/workspace-notifications";
|
||||
// constants
|
||||
import { NOTIFICATION_TABS, TNotificationTab } from "@/constants/notification";
|
||||
@@ -21,6 +20,8 @@ import { getNumberCount } from "@/helpers/string.helper";
|
||||
// hooks
|
||||
import { useWorkspace, useWorkspaceNotifications } from "@/hooks/store";
|
||||
|
||||
import { NotificationCardListRoot } from "@/plane-web/components/workspace-notifications";
|
||||
|
||||
export const NotificationsSidebarRoot: FC = observer(() => {
|
||||
const { workspaceSlug } = useParams();
|
||||
// hooks
|
||||
@@ -1,3 +1,2 @@
|
||||
export * from "./root";
|
||||
export * from "./item";
|
||||
export * from "./options";
|
||||
|
||||
@@ -27,6 +27,7 @@ import { CustomMenu, Tooltip, DropIndicator, FavoriteFolderIcon, DragHandle } fr
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
// hooks
|
||||
import { useAppTheme } from "@/hooks/store";
|
||||
import { useFavorite } from "@/hooks/store/use-favorite";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// constants
|
||||
import { FavoriteRoot } from "./favorite-items";
|
||||
@@ -45,7 +46,7 @@ export const FavoriteFolder: React.FC<Props> = (props) => {
|
||||
const { favorite, handleRemoveFromFavorites, isLastChild, handleDrop } = props;
|
||||
// store hooks
|
||||
const { sidebarCollapsed: isSidebarCollapsed } = useAppTheme();
|
||||
|
||||
const { getGroupedFavorites } = useFavorite();
|
||||
const { isMobile } = usePlatformOS();
|
||||
const { workspaceSlug } = useParams();
|
||||
// states
|
||||
@@ -58,6 +59,12 @@ export const FavoriteFolder: React.FC<Props> = (props) => {
|
||||
const actionSectionRef = useRef<HTMLDivElement | null>(null);
|
||||
const elementRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (favorite.children === undefined && workspaceSlug) {
|
||||
getGroupedFavorites(workspaceSlug.toString(), favorite.id);
|
||||
}
|
||||
}, [favorite.id, favorite.children, workspaceSlug, getGroupedFavorites]);
|
||||
|
||||
useEffect(() => {
|
||||
const element = elementRef.current;
|
||||
|
||||
@@ -123,7 +130,7 @@ export const FavoriteFolder: React.FC<Props> = (props) => {
|
||||
})
|
||||
);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isDragging, favorite.id]);
|
||||
}, [isDragging, favorite.id, isLastChild, favorite.id]);
|
||||
|
||||
useOutsideClickDetector(actionSectionRef, () => setIsMenuActive(false));
|
||||
|
||||
|
||||
@@ -131,7 +131,7 @@ export const FavoriteRoot: FC<Props> = observer((props) => {
|
||||
})
|
||||
);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [elementRef?.current, isDragging]);
|
||||
}, [elementRef?.current, isDragging, isLastChild, favorite.id]);
|
||||
|
||||
useOutsideClickDetector(actionSectionRef, () => setIsMenuActive(false));
|
||||
|
||||
|
||||
@@ -87,33 +87,27 @@ export const SidebarFavoritesMenu = observer(() => {
|
||||
const sourceData = source.data as TargetData;
|
||||
|
||||
if (!sourceData.id) return;
|
||||
|
||||
if (isFolder) {
|
||||
// handle move to a new parent folder if dropped on a folder
|
||||
if (parentId && parentId !== sourceData.parentId) {
|
||||
handleMoveToFolder(sourceData.id, parentId);
|
||||
handleMoveToFolder(sourceData.id, parentId); /**parent id */
|
||||
}
|
||||
//handle remove from folder if dropped outside of the folder
|
||||
if (parentId && parentId !== sourceData.parentId && sourceData.isChild) {
|
||||
handleRemoveFromFavoritesFolder(sourceData.id);
|
||||
}
|
||||
|
||||
// handle reordering at root level
|
||||
if (droppedFavId) {
|
||||
if (instruction != "make-child") {
|
||||
handleReorder(sourceData.id, droppedFavId, instruction);
|
||||
handleReorder(sourceData.id, droppedFavId, instruction); /** sequence */
|
||||
}
|
||||
}
|
||||
} else {
|
||||
//handling reordering for favorites
|
||||
if (droppedFavId) {
|
||||
handleReorder(sourceData.id, droppedFavId, instruction);
|
||||
handleReorder(sourceData.id, droppedFavId, instruction); /** sequence */
|
||||
}
|
||||
}
|
||||
|
||||
// handle removal from folder if dropped outside a folder
|
||||
if (!parentId && sourceData.isChild) {
|
||||
handleRemoveFromFavoritesFolder(sourceData.id);
|
||||
}
|
||||
/**remove if dropped outside and source is a child */
|
||||
if (!parentId && sourceData.isChild) {
|
||||
handleRemoveFromFavoritesFolder(sourceData.id); /**parent null */
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useContext } from "react";
|
||||
// mobx store
|
||||
import { StoreContext } from "@/lib/store-context";
|
||||
// types
|
||||
import { ICommandPaletteStore } from "@/store/command-palette.store";
|
||||
import { ICommandPaletteStore } from "@/plane-web/store/command-palette.store";
|
||||
|
||||
export const useCommandPalette = (): ICommandPaletteStore => {
|
||||
const context = useContext(StoreContext);
|
||||
|
||||
+13
-15
@@ -9,9 +9,8 @@ export interface ModalData {
|
||||
viewId: string;
|
||||
}
|
||||
|
||||
export interface ICommandPaletteStore {
|
||||
export interface IBaseCommandPaletteStore {
|
||||
// observables
|
||||
|
||||
isCommandPaletteOpen: boolean;
|
||||
isShortcutModalOpen: boolean;
|
||||
isCreateProjectModalOpen: boolean;
|
||||
@@ -22,6 +21,7 @@ export interface ICommandPaletteStore {
|
||||
isCreateIssueModalOpen: boolean;
|
||||
isDeleteIssueModalOpen: boolean;
|
||||
isBulkDeleteIssueModalOpen: boolean;
|
||||
createIssueStoreType: TCreateModalStoreTypes;
|
||||
// computed
|
||||
isAnyModalOpen: boolean;
|
||||
// toggle actions
|
||||
@@ -35,11 +35,9 @@ export interface ICommandPaletteStore {
|
||||
toggleCreateModuleModal: (value?: boolean) => void;
|
||||
toggleDeleteIssueModal: (value?: boolean) => void;
|
||||
toggleBulkDeleteIssueModal: (value?: boolean) => void;
|
||||
|
||||
createIssueStoreType: TCreateModalStoreTypes;
|
||||
}
|
||||
|
||||
export class CommandPaletteStore implements ICommandPaletteStore {
|
||||
export abstract class BaseCommandPaletteStore implements IBaseCommandPaletteStore {
|
||||
// observables
|
||||
isCommandPaletteOpen: boolean = false;
|
||||
isShortcutModalOpen: boolean = false;
|
||||
@@ -51,7 +49,6 @@ export class CommandPaletteStore implements ICommandPaletteStore {
|
||||
isDeleteIssueModalOpen: boolean = false;
|
||||
isBulkDeleteIssueModalOpen: boolean = false;
|
||||
createPageModal: TCreatePageModal = DEFAULT_CREATE_PAGE_MODAL_DATA;
|
||||
|
||||
createIssueStoreType: TCreateModalStoreTypes = EIssuesStoreType.PROJECT;
|
||||
|
||||
constructor() {
|
||||
@@ -67,6 +64,7 @@ export class CommandPaletteStore implements ICommandPaletteStore {
|
||||
isDeleteIssueModalOpen: observable.ref,
|
||||
isBulkDeleteIssueModalOpen: observable.ref,
|
||||
createPageModal: observable,
|
||||
createIssueStoreType: observable,
|
||||
// computed
|
||||
isAnyModalOpen: computed,
|
||||
// projectPages: computed,
|
||||
@@ -85,20 +83,20 @@ export class CommandPaletteStore implements ICommandPaletteStore {
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether any modal is open or not.
|
||||
* Checks whether any modal is open or not in the base command palette.
|
||||
* @returns boolean
|
||||
*/
|
||||
get isAnyModalOpen() {
|
||||
return Boolean(
|
||||
this.isCreateIssueModalOpen ||
|
||||
this.isCreateCycleModalOpen ||
|
||||
this.isCreateProjectModalOpen ||
|
||||
this.isCreateModuleModalOpen ||
|
||||
this.isCreateViewModalOpen ||
|
||||
this.isShortcutModalOpen ||
|
||||
this.isBulkDeleteIssueModalOpen ||
|
||||
this.isDeleteIssueModalOpen ||
|
||||
this.createPageModal.isOpen
|
||||
this.isCreateCycleModalOpen ||
|
||||
this.isCreateProjectModalOpen ||
|
||||
this.isCreateModuleModalOpen ||
|
||||
this.isCreateViewModalOpen ||
|
||||
this.isShortcutModalOpen ||
|
||||
this.isBulkDeleteIssueModalOpen ||
|
||||
this.isDeleteIssueModalOpen ||
|
||||
this.createPageModal.isOpen
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isFuture, isPast, isToday } from "date-fns";
|
||||
import { isPast, isToday } from "date-fns";
|
||||
import isEmpty from "lodash/isEmpty";
|
||||
import set from "lodash/set";
|
||||
import sortBy from "lodash/sortBy";
|
||||
@@ -7,16 +7,14 @@ import { computedFn } from "mobx-utils";
|
||||
// types
|
||||
import {
|
||||
ICycle,
|
||||
CycleDateCheckData,
|
||||
TCyclePlotType,
|
||||
TProgressSnapshot,
|
||||
TCycleEstimateDistribution,
|
||||
TCycleDistribution,
|
||||
TCycleEstimateType,
|
||||
TCycleProgress,
|
||||
} from "@plane/types";
|
||||
// helpers
|
||||
import { orderCycles, shouldFilterCycle, formatActiveCycle } from "@/helpers/cycle.helper";
|
||||
import { orderCycles, shouldFilterCycle } from "@/helpers/cycle.helper";
|
||||
import { getDate } from "@/helpers/date-time.helper";
|
||||
import { DistributionUpdates, updateDistribution } from "@/helpers/distribution-update.helper";
|
||||
// services
|
||||
@@ -42,21 +40,18 @@ export interface ICycleStore {
|
||||
// computed
|
||||
currentProjectCycleIds: string[] | null;
|
||||
currentProjectCompletedCycleIds: string[] | null;
|
||||
currentProjectUpcomingCycleIds: string[] | null;
|
||||
currentProjectIncompleteCycleIds: string[] | null;
|
||||
currentProjectDraftCycleIds: string[] | null;
|
||||
currentProjectActiveCycleId: string | null;
|
||||
currentProjectArchivedCycleIds: string[] | null;
|
||||
currentProjectActiveCycle: ICycle | null;
|
||||
|
||||
// computed actions
|
||||
getActiveCycleProgress: (cycleId?: string) => { cycle: ICycle; isBurnDown: boolean; isTypeIssue: boolean } | null;
|
||||
getFilteredCycleIds: (projectId: string, sortByManual: boolean) => string[] | null;
|
||||
getFilteredCompletedCycleIds: (projectId: string) => string[] | null;
|
||||
getFilteredArchivedCycleIds: (projectId: string) => string[] | null;
|
||||
getCycleById: (cycleId: string) => ICycle | null;
|
||||
getCycleNameById: (cycleId: string) => string | undefined;
|
||||
getActiveCycleById: (cycleId: string) => ICycle | null;
|
||||
getProjectCycleDetails: (projectId: string) => ICycle[] | null;
|
||||
getProjectCycleIds: (projectId: string) => string[] | null;
|
||||
getPlotTypeByCycleId: (cycleId: string) => TCyclePlotType;
|
||||
getEstimateTypeByCycleId: (cycleId: string) => TCycleEstimateType;
|
||||
@@ -64,7 +59,6 @@ export interface ICycleStore {
|
||||
|
||||
// actions
|
||||
updateCycleDistribution: (distributionUpdates: DistributionUpdates, cycleId: string) => void;
|
||||
validateDate: (workspaceSlug: string, projectId: string, payload: CycleDateCheckData) => Promise<any>;
|
||||
setPlotType: (cycleId: string, plotType: TCyclePlotType) => void;
|
||||
setEstimateType: (cycleId: string, estimateType: TCycleEstimateType) => void;
|
||||
// fetch
|
||||
@@ -130,15 +124,12 @@ export class CycleStore implements ICycleStore {
|
||||
// computed
|
||||
currentProjectCycleIds: computed,
|
||||
currentProjectCompletedCycleIds: computed,
|
||||
currentProjectUpcomingCycleIds: computed,
|
||||
currentProjectIncompleteCycleIds: computed,
|
||||
currentProjectDraftCycleIds: computed,
|
||||
currentProjectActiveCycleId: computed,
|
||||
currentProjectArchivedCycleIds: computed,
|
||||
currentProjectActiveCycle: computed,
|
||||
|
||||
// actions
|
||||
setPlotType: action,
|
||||
setEstimateType: action,
|
||||
fetchWorkspaceCycles: action,
|
||||
fetchAllCycles: action,
|
||||
@@ -195,22 +186,6 @@ export class CycleStore implements ICycleStore {
|
||||
return completedCycleIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns all upcoming cycle ids for a project
|
||||
*/
|
||||
get currentProjectUpcomingCycleIds() {
|
||||
const projectId = this.rootStore.router.projectId;
|
||||
if (!projectId || !this.fetchedMap[projectId]) return null;
|
||||
let upcomingCycles = Object.values(this.cycleMap ?? {}).filter((c) => {
|
||||
const startDate = getDate(c.start_date);
|
||||
const isStartDateUpcoming = startDate && isFuture(startDate);
|
||||
return c.project_id === projectId && isStartDateUpcoming && !c?.archived_at;
|
||||
});
|
||||
upcomingCycles = sortBy(upcomingCycles, [(c) => c.sort_order]);
|
||||
const upcomingCycleIds = upcomingCycles.map((c) => c.id);
|
||||
return upcomingCycleIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns all incomplete cycle ids for a project
|
||||
*/
|
||||
@@ -227,20 +202,6 @@ export class CycleStore implements ICycleStore {
|
||||
return incompleteCycleIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns all draft cycle ids for a project
|
||||
*/
|
||||
get currentProjectDraftCycleIds() {
|
||||
const projectId = this.rootStore.router.projectId;
|
||||
if (!projectId || !this.fetchedMap[projectId]) return null;
|
||||
let draftCycles = Object.values(this.cycleMap ?? {}).filter(
|
||||
(c) => c.project_id === projectId && !c.start_date && !c.end_date && !c?.archived_at
|
||||
);
|
||||
draftCycles = sortBy(draftCycles, [(c) => c.sort_order]);
|
||||
const draftCycleIds = draftCycles.map((c) => c.id);
|
||||
return draftCycleIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns active cycle id for a project
|
||||
*/
|
||||
@@ -285,19 +246,6 @@ export class CycleStore implements ICycleStore {
|
||||
} else return false;
|
||||
});
|
||||
|
||||
/**
|
||||
* returns active cycle progress for a project
|
||||
*/
|
||||
getActiveCycleProgress = computedFn((cycleId?: string) => {
|
||||
const cycle = cycleId ? this.cycleMap[cycleId] : this.currentProjectActiveCycle;
|
||||
if (!cycle) return null;
|
||||
|
||||
const isTypeIssue = this.getEstimateTypeByCycleId(cycle.id) === "issues";
|
||||
const isBurnDown = this.getPlotTypeByCycleId(cycle.id) === "burndown";
|
||||
|
||||
return { cycle, isTypeIssue, isBurnDown };
|
||||
});
|
||||
|
||||
/**
|
||||
* @description returns filtered cycle ids based on display filters and filters
|
||||
* @param {TCycleDisplayFilters} displayFilters
|
||||
@@ -379,37 +327,28 @@ export class CycleStore implements ICycleStore {
|
||||
getCycleNameById = computedFn((cycleId: string): string => this.cycleMap?.[cycleId]?.name);
|
||||
|
||||
/**
|
||||
* @description returns active cycle details by cycle id
|
||||
* @param cycleId
|
||||
* @returns
|
||||
* @description returns list of cycle details of the project id passed as argument
|
||||
* @param projectId
|
||||
*/
|
||||
getActiveCycleById = computedFn((cycleId: string): ICycle | null =>
|
||||
this.activeCycleIdMap?.[cycleId] && this.cycleMap?.[cycleId] ? this.cycleMap?.[cycleId] : null
|
||||
);
|
||||
getProjectCycleDetails = computedFn((projectId: string): ICycle[] | null => {
|
||||
if (!this.fetchedMap[projectId]) return null;
|
||||
|
||||
let cycles = Object.values(this.cycleMap ?? {}).filter((c) => c.project_id === projectId && !c?.archived_at);
|
||||
cycles = sortBy(cycles, [(c) => c.sort_order]);
|
||||
return cycles || null;
|
||||
});
|
||||
|
||||
/**
|
||||
* @description returns list of cycle ids of the project id passed as argument
|
||||
* @param projectId
|
||||
*/
|
||||
getProjectCycleIds = computedFn((projectId: string): string[] | null => {
|
||||
if (!this.fetchedMap[projectId]) return null;
|
||||
|
||||
let cycles = Object.values(this.cycleMap ?? {}).filter((c) => c.project_id === projectId && !c?.archived_at);
|
||||
cycles = sortBy(cycles, [(c) => c.sort_order]);
|
||||
const cycles = this.getProjectCycleDetails(projectId);
|
||||
if (!cycles) return null;
|
||||
const cycleIds = cycles.map((c) => c.id);
|
||||
return cycleIds || null;
|
||||
});
|
||||
|
||||
/**
|
||||
* @description validates cycle dates
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
* @param payload
|
||||
* @returns
|
||||
*/
|
||||
validateDate = async (workspaceSlug: string, projectId: string, payload: CycleDateCheckData) =>
|
||||
await this.cycleService.cycleDateCheck(workspaceSlug, projectId, payload);
|
||||
|
||||
/**
|
||||
* @description gets the plot type for the cycle store
|
||||
* @param {TCyclePlotType} plotType
|
||||
@@ -473,14 +412,16 @@ export class CycleStore implements ICycleStore {
|
||||
runInAction(() => {
|
||||
response.forEach((cycle) => {
|
||||
set(this.cycleMap, [cycle.id], cycle);
|
||||
cycle.status?.toLowerCase() === "current" && set(this.activeCycleIdMap, [cycle.id], true);
|
||||
if (cycle.status?.toLowerCase() === "current") {
|
||||
set(this.activeCycleIdMap, [cycle.id], true);
|
||||
}
|
||||
});
|
||||
set(this.fetchedMap, projectId, true);
|
||||
this.loader = false;
|
||||
});
|
||||
return response;
|
||||
});
|
||||
} catch (error) {
|
||||
} catch {
|
||||
this.loader = false;
|
||||
return undefined;
|
||||
}
|
||||
@@ -553,6 +494,7 @@ export class CycleStore implements ICycleStore {
|
||||
* @param cycleId
|
||||
* @returns
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
fetchActiveCycleProgressPro = action(async (workspaceSlug: string, projectId: string, cycleId: string) => {});
|
||||
|
||||
/**
|
||||
|
||||
@@ -174,23 +174,14 @@ export class FavoriteStore implements IFavoriteStore {
|
||||
* @returns Promise<void>
|
||||
*/
|
||||
moveFavoriteToFolder = async (workspaceSlug: string, favoriteId: string, data: Partial<IFavorite>) => {
|
||||
const oldParent = this.favoriteMap[favoriteId].parent;
|
||||
try {
|
||||
await this.favoriteService.updateFavorite(workspaceSlug, favoriteId, data);
|
||||
runInAction(() => {
|
||||
// add parent of the favorite
|
||||
set(this.favoriteMap, [favoriteId, "parent"], data.parent);
|
||||
});
|
||||
await this.favoriteService.updateFavorite(workspaceSlug, favoriteId, data);
|
||||
} catch (error) {
|
||||
console.error("Failed to move favorite from favorite store");
|
||||
|
||||
// revert the changes
|
||||
runInAction(() => {
|
||||
if (!data.parent) return;
|
||||
|
||||
// revert the parent
|
||||
set(this.favoriteMap, [favoriteId, "parent"], oldParent);
|
||||
});
|
||||
console.error("Failed to move favorite to folder", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -201,7 +192,6 @@ export class FavoriteStore implements IFavoriteStore {
|
||||
destinationId: string,
|
||||
edge: string | undefined
|
||||
) => {
|
||||
const initialSequence = this.favoriteMap[favoriteId].sequence;
|
||||
try {
|
||||
let resultSequence = 10000;
|
||||
if (edge) {
|
||||
@@ -221,35 +211,27 @@ export class FavoriteStore implements IFavoriteStore {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await this.favoriteService.updateFavorite(workspaceSlug, favoriteId, { sequence: resultSequence });
|
||||
|
||||
runInAction(() => {
|
||||
set(this.favoriteMap, [favoriteId, "sequence"], resultSequence);
|
||||
});
|
||||
|
||||
await this.favoriteService.updateFavorite(workspaceSlug, favoriteId, { sequence: resultSequence });
|
||||
} catch (error) {
|
||||
console.error("Failed to move favorite folder");
|
||||
runInAction(() => {
|
||||
set(this.favoriteMap, [favoriteId, "sequence"], initialSequence);
|
||||
throw error;
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
removeFromFavoriteFolder = async (workspaceSlug: string, favoriteId: string) => {
|
||||
const parent = this.favoriteMap[favoriteId].parent;
|
||||
try {
|
||||
await this.favoriteService.updateFavorite(workspaceSlug, favoriteId, { parent: null });
|
||||
runInAction(() => {
|
||||
//remove parent
|
||||
set(this.favoriteMap, [favoriteId, "parent"], null);
|
||||
});
|
||||
await this.favoriteService.updateFavorite(workspaceSlug, favoriteId, { parent: null });
|
||||
} catch (error) {
|
||||
console.error("Failed to move favorite");
|
||||
runInAction(() => {
|
||||
set(this.favoriteMap, [favoriteId, "parent"], parent);
|
||||
|
||||
throw error;
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -384,7 +366,7 @@ export class FavoriteStore implements IFavoriteStore {
|
||||
set(this.favoriteMap, [favorite.id], favorite);
|
||||
this.favoriteIds.push(favorite.id);
|
||||
this.favoriteIds = uniqBy(this.favoriteIds, (id) => id);
|
||||
favorite.entity_identifier && set(this.entityMap, [favorite.entity_identifier], favorite);
|
||||
if (favorite.entity_identifier) set(this.entityMap, [favorite.entity_identifier], favorite);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@ export class InboxIssueStore implements IInboxIssueStore {
|
||||
|
||||
// If issue accepted sync issue to local db
|
||||
if (status === EInboxIssueStatus.ACCEPTED) {
|
||||
addIssueToPersistanceLayer(inboxIssue.issue);
|
||||
addIssueToPersistanceLayer({ ...this.issue, ...inboxIssue.issue });
|
||||
}
|
||||
} catch {
|
||||
runInAction(() => set(this, "status", previousData.status));
|
||||
|
||||
@@ -352,7 +352,7 @@ export class WorkspaceDraftIssues implements IWorkspaceDraftIssues {
|
||||
}
|
||||
|
||||
// sync issue to local db
|
||||
addIssueToPersistanceLayer(response);
|
||||
addIssueToPersistanceLayer({ ...payload, ...response });
|
||||
|
||||
// Update draft issue count in workspaceUserInfo
|
||||
this.updateWorkspaceUserDraftIssueCount(workspaceSlug, -1);
|
||||
|
||||
@@ -32,6 +32,7 @@ export interface IModuleStore {
|
||||
getFilteredArchivedModuleIds: (projectId: string) => string[] | null;
|
||||
getModuleById: (moduleId: string) => IModule | null;
|
||||
getModuleNameById: (moduleId: string) => string;
|
||||
getProjectModuleDetails: (projectId: string) => IModule[] | null;
|
||||
getProjectModuleIds: (projectId: string) => string[] | null;
|
||||
getPlotTypeByModuleId: (moduleId: string) => TModulePlotType;
|
||||
// actions
|
||||
@@ -210,15 +211,24 @@ export class ModulesStore implements IModuleStore {
|
||||
*/
|
||||
getModuleNameById = computedFn((moduleId: string) => this.moduleMap?.[moduleId]?.name);
|
||||
|
||||
/**
|
||||
* @description returns list of module details of the project id passed as argument
|
||||
* @param projectId
|
||||
*/
|
||||
getProjectModuleDetails = computedFn((projectId: string) => {
|
||||
if (!this.fetchedMap[projectId]) return null;
|
||||
let projectModules = Object.values(this.moduleMap).filter((m) => m.project_id === projectId && !m.archived_at);
|
||||
projectModules = sortBy(projectModules, [(m) => m.sort_order]);
|
||||
return projectModules;
|
||||
});
|
||||
|
||||
/**
|
||||
* @description returns list of module ids of the project id passed as argument
|
||||
* @param projectId
|
||||
*/
|
||||
getProjectModuleIds = computedFn((projectId: string) => {
|
||||
if (!this.fetchedMap[projectId]) return null;
|
||||
|
||||
let projectModules = Object.values(this.moduleMap).filter((m) => m.project_id === projectId && !m.archived_at);
|
||||
projectModules = sortBy(projectModules, [(m) => m.sort_order]);
|
||||
const projectModules = this.getProjectModuleDetails(projectId);
|
||||
if (!projectModules) return null;
|
||||
const projectModuleIds = projectModules.map((m) => m.id);
|
||||
return projectModuleIds;
|
||||
});
|
||||
@@ -282,7 +292,7 @@ export class ModulesStore implements IModuleStore {
|
||||
});
|
||||
return response;
|
||||
});
|
||||
} catch (error) {
|
||||
} catch {
|
||||
this.loader = false;
|
||||
return undefined;
|
||||
}
|
||||
@@ -308,7 +318,7 @@ export class ModulesStore implements IModuleStore {
|
||||
});
|
||||
return projectModules;
|
||||
});
|
||||
} catch (error) {
|
||||
} catch {
|
||||
this.loader = false;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ export interface INotification extends TNotification {
|
||||
|
||||
export class Notification implements INotification {
|
||||
// observables
|
||||
id: string | undefined = undefined;
|
||||
id: string;
|
||||
title: string | undefined = undefined;
|
||||
data: TNotificationData | undefined = undefined;
|
||||
entity_identifier: string | undefined = undefined;
|
||||
@@ -54,6 +54,7 @@ export class Notification implements INotification {
|
||||
private store: CoreRootStore,
|
||||
private notification: TNotification
|
||||
) {
|
||||
this.id = this.notification.id;
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
id: observable.ref,
|
||||
@@ -90,7 +91,6 @@ export class Notification implements INotification {
|
||||
snoozeNotification: action,
|
||||
unSnoozeNotification: action,
|
||||
});
|
||||
this.id = this.notification.id;
|
||||
this.title = this.notification.title;
|
||||
this.data = this.notification.data;
|
||||
this.entity_identifier = this.notification.entity_identifier;
|
||||
@@ -169,8 +169,6 @@ export class Notification implements INotification {
|
||||
workspaceSlug: string,
|
||||
payload: Partial<TNotification>
|
||||
): Promise<TNotification | undefined> => {
|
||||
if (!this.id) return undefined;
|
||||
|
||||
try {
|
||||
const notification = await workspaceNotificationService.updateNotificationById(workspaceSlug, this.id, payload);
|
||||
if (notification) {
|
||||
@@ -188,8 +186,6 @@ export class Notification implements INotification {
|
||||
* @returns { TNotification | undefined }
|
||||
*/
|
||||
markNotificationAsRead = async (workspaceSlug: string): Promise<TNotification | undefined> => {
|
||||
if (!this.id) return undefined;
|
||||
|
||||
const currentNotificationReadAt = this.read_at;
|
||||
try {
|
||||
const payload: Partial<TNotification> = {
|
||||
@@ -215,8 +211,6 @@ export class Notification implements INotification {
|
||||
* @returns { TNotification | undefined }
|
||||
*/
|
||||
markNotificationAsUnRead = async (workspaceSlug: string): Promise<TNotification | undefined> => {
|
||||
if (!this.id) return undefined;
|
||||
|
||||
const currentNotificationReadAt = this.read_at;
|
||||
try {
|
||||
const payload: Partial<TNotification> = {
|
||||
@@ -242,8 +236,6 @@ export class Notification implements INotification {
|
||||
* @returns { TNotification | undefined }
|
||||
*/
|
||||
archiveNotification = async (workspaceSlug: string): Promise<TNotification | undefined> => {
|
||||
if (!this.id) return undefined;
|
||||
|
||||
const currentNotificationArchivedAt = this.archived_at;
|
||||
try {
|
||||
const payload: Partial<TNotification> = {
|
||||
@@ -267,8 +259,6 @@ export class Notification implements INotification {
|
||||
* @returns { TNotification | undefined }
|
||||
*/
|
||||
unArchiveNotification = async (workspaceSlug: string): Promise<TNotification | undefined> => {
|
||||
if (!this.id) return undefined;
|
||||
|
||||
const currentNotificationArchivedAt = this.archived_at;
|
||||
try {
|
||||
const payload: Partial<TNotification> = {
|
||||
@@ -293,8 +283,6 @@ export class Notification implements INotification {
|
||||
* @returns { TNotification | undefined }
|
||||
*/
|
||||
snoozeNotification = async (workspaceSlug: string, snoozeTill: Date): Promise<TNotification | undefined> => {
|
||||
if (!this.id) return undefined;
|
||||
|
||||
const currentNotificationSnoozeTill = this.snoozed_till;
|
||||
try {
|
||||
const payload: Partial<TNotification> = {
|
||||
@@ -315,8 +303,6 @@ export class Notification implements INotification {
|
||||
* @returns { TNotification | undefined }
|
||||
*/
|
||||
unSnoozeNotification = async (workspaceSlug: string): Promise<TNotification | undefined> => {
|
||||
if (!this.id) return undefined;
|
||||
|
||||
const currentNotificationSnoozeTill = this.snoozed_till;
|
||||
try {
|
||||
const payload: Partial<TNotification> = {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { enableStaticRendering } from "mobx-react";
|
||||
// plane web store
|
||||
import { CommandPaletteStore, ICommandPaletteStore } from "@/plane-web/store/command-palette.store";
|
||||
import { RootStore } from "@/plane-web/store/root.store";
|
||||
import { IStateStore, StateStore } from "@/plane-web/store/state.store";
|
||||
// stores
|
||||
import { CommandPaletteStore, ICommandPaletteStore } from "./command-palette.store";
|
||||
import { CycleStore, ICycleStore } from "./cycle.store";
|
||||
import { CycleFilterStore, ICycleFilterStore } from "./cycle_filter.store";
|
||||
import { DashboardStore, IDashboardStore } from "./dashboard.store";
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from "ce/components/command-palette/modals";
|
||||
@@ -0,0 +1 @@
|
||||
export * from "ce/helpers/command-palette";
|
||||
@@ -0,0 +1 @@
|
||||
export * from "ce/store/command-palette.store";
|
||||
@@ -0,0 +1 @@
|
||||
export * from "ce/helpers/command-palette";
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "web",
|
||||
"version": "0.24.0",
|
||||
"version": "0.24.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "turbo run develop",
|
||||
|
||||
Reference in New Issue
Block a user