Compare commits

..

4 Commits

Author SHA1 Message Date
pablohashescobar 024a870e50 chore: optimize worker startups 2024-12-20 11:41:49 +05:30
pablohashescobar 5ba921edd8 chore: add task queue separations 2024-12-16 17:59:25 +05:30
pablohashescobar 08ba25a8f8 chore: add queue distribution for background workers 2024-12-16 15:53:06 +05:30
pablohashescobar bc54bed157 chore: add celery backend setup and cleanup settings 2024-12-16 14:22:39 +05:30
276 changed files with 1588 additions and 2759 deletions
+50 -73
View File
@@ -5,7 +5,9 @@
<img src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-readme/plane_logo_.webp" alt="Plane Logo" width="70">
</a>
</p>
<h1 align="center"><b>Plane</b></h1>
<h3 align="center"><b>Plane</b></h3>
<p align="center"><b>Open-source project management that unlocks customer value</b></p>
<p align="center">
<a href="https://discord.com/invite/A92xrEGCge">
@@ -42,85 +44,79 @@ Meet [Plane](https://dub.sh/plane-website-readme), an open-source project manage
> Plane is evolving every day. Your suggestions, ideas, and reported bugs help us immensely. Do not hesitate to join in the conversation on [Discord](https://discord.com/invite/A92xrEGCge) or raise a GitHub issue. We read everything and respond to most.
## 🚀 Installation
## Installation
Getting started with Plane is simple. Choose the setup that works best for you:
The easiest way to get started with Plane is by creating a [Plane Cloud](https://app.plane.so) account.
- **Plane Cloud**
Sign up for a free account on [Plane Cloud](https://app.plane.so)—it's the fastest way to get up and running without worrying about infrastructure.
- **Self-host Plane**
Prefer full control over your data and infrastructure? Install and run Plane on your own servers. Follow our detailed [deployment guides](https://developers.plane.so/self-hosting/overview) to get started.
If you would like to self-host Plane, please see our [deployment guide](https://docs.plane.so/docker-compose).
| Installation methods | Docs link |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Docker | [![Docker](https://img.shields.io/badge/docker-%230db7ed.svg?style=for-the-badge&logo=docker&logoColor=white)](https://developers.plane.so/self-hosting/methods/docker-compose) |
| Kubernetes | [![Kubernetes](https://img.shields.io/badge/kubernetes-%23326ce5.svg?style=for-the-badge&logo=kubernetes&logoColor=white)](https://developers.plane.so/self-hosting/methods/kubernetes) |
| Docker | [![Docker](https://img.shields.io/badge/docker-%230db7ed.svg?style=for-the-badge&logo=docker&logoColor=white)](https://docs.plane.so/self-hosting/methods/docker-compose) |
| Kubernetes | [![Kubernetes](https://img.shields.io/badge/kubernetes-%23326ce5.svg?style=for-the-badge&logo=kubernetes&logoColor=white)](https://docs.plane.so/kubernetes) |
`Instance admins` can manage and customize settings using [God mode](https://developers.plane.so/self-hosting/govern/instance-admin).
`Instance admins` can configure instance settings with [God-mode](https://docs.plane.so/instance-admin).
## 🌟 Features
## 🚀 Features
- **Issues**
Efficiently create and manage tasks with a robust rich text editor that supports file uploads. Enhance organization and tracking by adding sub-properties and referencing related issues.
- **Issues**: Quickly create issues and add details using a powerful rich text editor that supports file uploads. Add sub-properties and references to problems for better organization and tracking.
- **Cycles**
Maintain your teams momentum with Cycles. Track progress effortlessly using burn-down charts and other insightful tools.
- **Cycles**:
Keep up your team's momentum with Cycles. Gain insights into your project's progress with burn-down charts and other valuable features.
- **Modules**
Simplify complex projects by dividing them into smaller, manageable modules.
- **Modules**: Break down your large projects into smaller, more manageable modules. Assign modules between teams to track and plan your project's progress easily.
- **Views**
Customize your workflow by creating filters to display only the most relevant issues. Save and share these views with ease.
- **Views**: Create custom filters to display only the issues that matter to you. Save and share your filters in just a few clicks.
- **Pages**
Capture and organize ideas using Plane Pages, complete with AI capabilities and a rich text editor. Format text, insert images, add hyperlinks, or convert your notes into actionable items.
- **Pages**: Plane pages, equipped with AI and a rich text editor, let you jot down your thoughts on the fly. Format your text, upload images, hyperlink, or sync your existing ideas into an actionable item or issue.
- **Analytics**
Access real-time insights across all your Plane data. Visualize trends, remove blockers, and keep your projects moving forward.
- **Analytics**: Get insights into all your Plane data in real-time. Visualize issue data to spot trends, remove blockers, and progress your work.
- **Drive** (_coming soon_): The drive helps you share documents, images, videos, or any other files that make sense to you or your team and align on the problem/solution.
## 🛠️ Quick start for contributors
## 🛠️ Local development
> Development system must have docker engine installed and running.
### Pre-requisites
- Ensure Docker Engine is installed and running.
Setting up local environment is extremely easy and straight forward. Follow the below step and you will be ready to contribute -
### Development setup
Setting up your local environment is simple and straightforward. Follow these steps to get started:
1. Clone the repository:
1. Clone the code locally using:
```
git clone https://github.com/makeplane/plane.git
```
2. Navigate to the project folder:
2. Switch to the code folder:
```
cd plane
```
3. Create a new branch for your feature or fix:
3. Create your feature or fix branch you plan to work on using:
```
git checkout -b <feature-branch-name>
```
4. Run the setup script in the terminal:
4. Open terminal and run:
```
./setup.sh
```
5. Open the project in an IDE such as VS Code.
6. Review the `.env` files in the relevant folders. Refer to [Environment Setup](./ENV_SETUP.md) for details on the environment variables used.
7. Start the services using Docker:
5. Open the code on VSCode or similar equivalent IDE.
6. Review the `.env` files available in various folders.
Visit [Environment Setup](./ENV_SETUP.md) to know about various environment variables used in system.
7. Run the docker command to initiate services:
```
docker compose -f docker-compose-local.yml up -d
```
Thats it! Youre all set to begin coding. Remember to refresh your browser if changes dont auto-reload. Happy contributing! 🎉
You are ready to make changes to the code. Do not forget to refresh the browser (in case it does not auto-reload).
## Built with
[![Next JS](https://img.shields.io/badge/next.js-000000?style=for-the-badge&logo=nextdotjs&logoColor=white)](https://nextjs.org/)<br/>
[![Django](https://img.shields.io/badge/Django-092E20?style=for-the-badge&logo=django&logoColor=green)](https://www.djangoproject.com/)<br/>
[![Node JS](https://img.shields.io/badge/node.js-339933?style=for-the-badge&logo=Node.js&logoColor=white)](https://nodejs.org/en)
Thats it!
## ❤️ Community
The Plane community can be found on [GitHub Discussions](https://github.com/orgs/makeplane/discussions), and our [Discord server](https://discord.com/invite/A92xrEGCge). Our [Code of conduct](https://github.com/makeplane/plane/blob/master/CODE_OF_CONDUCT.md) applies to all Plane community chanels.
Ask questions, report bugs, join discussions, voice ideas, make feature requests, or share your projects.
### Repo Activity
![Plane Repo Activity](https://repobeats.axiom.co/api/embed/2523c6ed2f77c082b7908c33e2ab208981d76c39.svg "Repobeats analytics image")
## 📸 Screenshots
@@ -169,7 +165,7 @@ Thats it! Youre all set to begin coding. Remember to refresh your browser
</a>
</p>
</p>
<p>
<p>
<a href="https://plane.so" target="_blank">
<img
src="https://ik.imagekit.io/w2okwbtu2/Drive_LlfeY4xn3.png?updatedAt=1709298837917"
@@ -180,42 +176,23 @@ Thats it! Youre all set to begin coding. Remember to refresh your browser
</p>
</p>
## 📝 Documentation
Explore Plane's [product documentation](https://docs.plane.so/) and [developer documentation](https://developers.plane.so/) to learn about features, setup, and usage.
## ⛓️ Security
## ❤️ Community
If you believe you have found a security vulnerability in Plane, we encourage you to responsibly disclose this and not open a public issue. We will investigate all legitimate reports.
Join the Plane community on [GitHub Discussions](https://github.com/orgs/makeplane/discussions) and our [Discord server](https://discord.com/invite/A92xrEGCge). We follow a [Code of conduct](https://github.com/makeplane/plane/blob/master/CODE_OF_CONDUCT.md) in all our community channels.
Email squawk@plane.so to disclose any security vulnerabilities.
Feel free to ask questions, report bugs, participate in discussions, share ideas, request features, or showcase your projects. Wed love to hear from you!
## ❤️ Contribute
## 🛡️ Security
There are many ways to contribute to Plane, including:
If you discover a security vulnerability in Plane, please report it responsibly instead of opening a public issue. We take all legitimate reports seriously and will investigate them promptly. See [Security policy](https://github.com/makeplane/plane/blob/master/SECURITY.md) for more info.
To disclose any security issues, please email us at security@plane.so.
## 🤝 Contributing
There are many ways you can contribute to Plane:
- Report [bugs](https://github.com/makeplane/plane/issues/new?assignees=srinivaspendem%2Cpushya22&labels=%F0%9F%90%9Bbug&projects=&template=--bug-report.yaml&title=%5Bbug%5D%3A+) or submit [feature requests](https://github.com/makeplane/plane/issues/new?assignees=srinivaspendem%2Cpushya22&labels=%E2%9C%A8feature&projects=&template=--feature-request.yaml&title=%5Bfeature%5D%3A+).
- Review the [documentation](https://docs.plane.so/) and submit [pull requests](https://github.com/makeplane/docs) to improve it—whether it's fixing typos or adding new content.
- Talk or write about Plane or any other ecosystem integration and [let us know](https://discord.com/invite/A92xrEGCge)!
- Show your support by upvoting [popular feature requests](https://github.com/makeplane/plane/issues).
Please read [CONTRIBUTING.md](https://github.com/makeplane/plane/blob/master/CONTRIBUTING.md) for details on the process for submitting pull requests to us.
### Repo activity
![Plane Repo Activity](https://repobeats.axiom.co/api/embed/2523c6ed2f77c082b7908c33e2ab208981d76c39.svg "Repobeats analytics image")
- Submitting [bugs](https://github.com/makeplane/plane/issues/new?assignees=srinivaspendem%2Cpushya22&labels=%F0%9F%90%9Bbug&projects=&template=--bug-report.yaml&title=%5Bbug%5D%3A+) and [feature requests](https://github.com/makeplane/plane/issues/new?assignees=srinivaspendem%2Cpushya22&labels=%E2%9C%A8feature&projects=&template=--feature-request.yaml&title=%5Bfeature%5D%3A+) for various components.
- Reviewing [the documentation](https://docs.plane.so/) and submitting [pull requests](https://github.com/makeplane/plane), from fixing typos to adding new features.
- Speaking or writing about Plane or any other ecosystem integration and [letting us know](https://discord.com/invite/A92xrEGCge)!
- Upvoting [popular feature requests](https://github.com/makeplane/plane/issues) to show your support.
### We couldn't have done this without you.
<a href="https://github.com/makeplane/plane/graphs/contributors">
<img src="https://contrib.rocks/image?repo=makeplane/plane" />
</a>
## License
This project is licensed under the [GNU Affero General Public License v3.0](https://github.com/makeplane/plane/blob/master/LICENSE.txt).
+1 -1
View File
@@ -1,3 +1,3 @@
web: gunicorn -w 4 -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:$PORT --max-requests 10000 --max-requests-jitter 1000 --access-logfile -
worker: celery -A plane worker -l info
worker: celery -A plane worker -O fair --prefetch-multiplier=1 --max-tasks-per-child=100 --max-memory-per-child=150000 -l INFO
beat: celery -A plane beat -l INFO
+1 -7
View File
@@ -126,13 +126,7 @@ class UserAssetsV2Endpoint(BaseAPIView):
)
# Check if the file type is allowed
allowed_types = [
"image/jpeg",
"image/png",
"image/webp",
"image/jpg",
"image/gif",
]
allowed_types = ["image/jpeg", "image/png", "image/webp", "image/jpg"]
if type not in allowed_types:
return Response(
{
+11 -3
View File
@@ -1,10 +1,18 @@
from django.utils import timezone
# Python imports
from datetime import timedelta
from plane.db.models import APIActivityLog
# Django imports
from django.utils import timezone
from django.conf import settings
# Third party imports
from celery import shared_task
# Module imports
from plane.db.models import APIActivityLog
@shared_task
@shared_task(queue=settings.TASK_SCHEDULER_QUEUE)
def delete_api_logs():
# Get the logs older than 30 days to delete
logs_to_delete = APIActivityLog.objects.filter(
+2 -2
View File
@@ -10,7 +10,7 @@ from django.db.models.fields.related import OneToOneRel
from celery import shared_task
@shared_task
@shared_task(queue=settings.TASK_DEFAULT_QUEUE)
def soft_delete_related_objects(app_label, model_name, instance_pk, using=None):
"""
Soft delete related objects for a given model instance
@@ -111,7 +111,7 @@ def restore_related_objects(app_label, model_name, instance_pk, using=None):
pass
@shared_task
@shared_task(queue=settings.TASK_SCHEDULER_QUEUE)
def hard_delete():
from plane.db.models import (
Workspace,
+2 -1
View File
@@ -5,6 +5,7 @@ from datetime import datetime, timedelta
# Django imports
from django.db.models import Max
from django.conf import settings
# Third party imports
from celery import shared_task
@@ -531,7 +532,7 @@ def create_module_issues(workspace, project, user_id, issue_count):
)
@shared_task
@shared_task(queue=settings.TASK_LOW_QUEUE)
def create_dummy_data(
slug,
email,
@@ -2,16 +2,16 @@ import logging
import re
from datetime import datetime
from bs4 import BeautifulSoup
# Third party imports
from celery import shared_task
# Django imports
from django.core.mail import EmailMultiAlternatives, get_connection
from django.template.loader import render_to_string
# Django imports
from django.utils import timezone
from django.utils.html import strip_tags
from django.conf import settings
# Third party imports
from bs4 import BeautifulSoup
from celery import shared_task
# Module imports
from plane.db.models import EmailNotificationLog, Issue, User
@@ -168,7 +168,7 @@ def process_html_content(content):
return processed_content_list
@shared_task
@shared_task(queue=settings.TASK_NOTIFICATION_QUEUE)
def send_email_notification(
issue_id, notification_data, receiver_id, email_notification_ids
):
@@ -1,6 +1,10 @@
# Python imports
import os
import uuid
# Django imports
from django.conf import settings
# third party imports
from celery import shared_task
from posthog import Posthog
@@ -49,7 +53,7 @@ def auth_events(user, email, user_agent, ip, event_name, medium, first_time):
return
@shared_task
@shared_task(queue=settings.TASK_LOW_QUEUE)
def workspace_invite_event(user, email, user_agent, ip, event_name, accepted_from):
try:
POSTHOG_API_KEY, POSTHOG_HOST = posthogConfiguration()
+4 -3
View File
@@ -7,12 +7,13 @@ import zipfile
import boto3
from botocore.client import Config
# Third party imports
from celery import shared_task
# Django imports
from django.conf import settings
from django.utils import timezone
# Third party imports
from celery import shared_task
from openpyxl import Workbook
# Module imports
@@ -299,7 +300,7 @@ def generate_xlsx(header, project_id, issues, files):
files.append((f"{project_id}.xlsx", xlsx_file))
@shared_task
@shared_task(queue=settings.TASK_LOW_QUEUE)
def issue_export_task(provider, workspace_id, project_ids, token_id, multiple, slug):
try:
exporter_instance = ExporterHistory.objects.get(token=token_id)
@@ -15,7 +15,7 @@ from botocore.client import Config
from plane.db.models import ExporterHistory
@shared_task
@shared_task(queue=settings.TASK_SCHEDULER_QUEUE)
def delete_old_s3_link():
# Get a list of keys and IDs to process
expired_exporter_history = ExporterHistory.objects.filter(
+2 -1
View File
@@ -5,6 +5,7 @@ from datetime import timedelta
# Django imports
from django.utils import timezone
from django.db.models import Q
from django.conf import settings
# Third party imports
from celery import shared_task
@@ -13,7 +14,7 @@ from celery import shared_task
from plane.db.models import FileAsset
@shared_task
@shared_task(queue=settings.TASK_HIGH_QUEUE)
def delete_unuploaded_file_asset():
"""This task deletes unuploaded file assets older than a certain number of days."""
FileAsset.objects.filter(
@@ -1,21 +1,22 @@
# Python imports
import logging
# Third party imports
from celery import shared_task
# Django imports
# Third party imports
from django.core.mail import EmailMultiAlternatives, get_connection
from django.template.loader import render_to_string
from django.utils.html import strip_tags
from django.conf import settings
# Third party imports
from celery import shared_task
# Module imports
from plane.license.utils.instance_value import get_email_configuration
from plane.utils.exception_logger import log_exception
@shared_task
@shared_task(queue=settings.TASK_HIGH_QUEUE)
def forgot_password(first_name, email, uidb64, token, current_site):
try:
relative_link = (
@@ -2,17 +2,17 @@
import json
# Third Party imports
from celery import shared_task
# Django imports
from django.core.serializers.json import DjangoJSONEncoder
from django.utils import timezone
from django.conf import settings
from plane.app.serializers import IssueActivitySerializer
from plane.bgtasks.notification_task import notifications
# Third Party imports
from celery import shared_task
# Module imports
from plane.app.serializers import IssueActivitySerializer
from plane.bgtasks.notification_task import notifications
from plane.db.models import (
CommentReaction,
Cycle,
@@ -1548,7 +1548,7 @@ def create_intake_activity(
# Receive message from room group
@shared_task
@shared_task(queue=settings.TASK_HIGH_QUEUE)
def issue_activity(
type,
requested_data,
@@ -2,12 +2,13 @@
import json
from datetime import timedelta
# Django imports
from django.db.models import Q
from django.utils import timezone
from django.conf import settings
# Third party imports
from celery import shared_task
from django.db.models import Q
# Django imports
from django.utils import timezone
# Module imports
from plane.bgtasks.issue_activities_task import issue_activity
@@ -15,7 +16,7 @@ from plane.db.models import Issue, Project, State
from plane.utils.exception_logger import log_exception
@shared_task
@shared_task(queue=settings.TASK_SCHEDULER_QUEUE)
def archive_and_close_old_issues():
archive_old_issues()
close_old_issues()
@@ -1,9 +1,16 @@
from celery import shared_task
from django.db import transaction
from django.utils import timezone
# Python imports
from typing import Optional, Dict
import json
# Django imports
from django.db import transaction
from django.utils import timezone
from django.conf import settings
# Third party imports
from celery import shared_task
# Module imports
from plane.db.models import Issue, IssueDescriptionVersion
from plane.utils.exception_logger import log_exception
@@ -39,7 +46,7 @@ def update_existing_version(version: IssueDescriptionVersion, issue) -> None:
)
@shared_task
@shared_task(queue=settings.TASK_HIGH_QUEUE)
def issue_description_version_task(
updated_issue, issue_id, user_id, is_creating=False
) -> Optional[bool]:
@@ -1,21 +1,21 @@
# Python imports
import logging
# Third party imports
from celery import shared_task
# Django imports
# Third party imports
from django.core.mail import EmailMultiAlternatives, get_connection
from django.template.loader import render_to_string
from django.utils.html import strip_tags
from django.conf import settings
# Third party imports
from celery import shared_task
# Module imports
from plane.license.utils.instance_value import get_email_configuration
from plane.utils.exception_logger import log_exception
@shared_task
@shared_task(queue=settings.TASK_NOTIFICATION_QUEUE)
def magic_link(email, key, token, current_site):
try:
(
+4 -1
View File
@@ -3,6 +3,9 @@ import json
import uuid
from uuid import UUID
# Django imports
from django.conf import settings
# Module imports
from plane.db.models import (
@@ -204,7 +207,7 @@ def create_mention_notification(
)
@shared_task
@shared_task(queue=settings.TASK_NOTIFICATION_QUEUE)
def notifications(
type,
issue_id,
@@ -3,13 +3,14 @@ import json
# Django imports
from django.utils import timezone
from django.conf import settings
# Third-party imports
from bs4 import BeautifulSoup
from celery import shared_task
# Module imports
from plane.db.models import Page, PageLog
from celery import shared_task
from plane.utils.exception_logger import log_exception
@@ -33,7 +34,7 @@ def extract_components(value, tag):
return []
@shared_task
@shared_task(queue=settings.TASK_LOW_QUEUE)
def page_transaction(new_value, old_value, page_id):
try:
page = Page.objects.get(pk=page_id)
+4 -1
View File
@@ -4,12 +4,15 @@ import json
# Third party imports
from celery import shared_task
# Django imports
from django.conf import settings
# Module imports
from plane.db.models import Page, PageVersion
from plane.utils.exception_logger import log_exception
@shared_task
@shared_task(queue=settings.TASK_DEFAULT_QUEUE)
def page_version(page_id, existing_instance, user_id):
try:
# Get the page
@@ -1,13 +1,15 @@
# Python imports
import logging
# Third party imports
from celery import shared_task
# Third party imports
# Django imports
from django.core.mail import EmailMultiAlternatives, get_connection
from django.template.loader import render_to_string
from django.utils.html import strip_tags
from django.conf import settings
# Third party imports
from celery import shared_task
# Module imports
@@ -17,7 +19,7 @@ from plane.db.models import ProjectMember
from plane.db.models import User
@shared_task
@shared_task(queue=settings.TASK_DEFAULT_QUEUE)
def project_add_user_email(current_site, project_member_id, invitor_id):
try:
# Get the invitor
@@ -1,14 +1,15 @@
# Python imports
import logging
# Third party imports
from celery import shared_task
# Django imports
# Third party imports
from django.core.mail import EmailMultiAlternatives, get_connection
from django.template.loader import render_to_string
from django.utils.html import strip_tags
from django.conf import settings
# Third party imports
from celery import shared_task
# Module imports
from plane.db.models import Project, ProjectMemberInvite, User
@@ -16,7 +17,7 @@ from plane.license.utils.instance_value import get_email_configuration
from plane.utils.exception_logger import log_exception
@shared_task
@shared_task(queue=settings.TASK_DEFAULT_QUEUE)
def project_invitation(email, project_id, token, current_site, invitor):
try:
user = User.objects.get(email=invitor)
+13 -1
View File
@@ -1,5 +1,6 @@
# Python imports
from django.utils import timezone
from django.conf import settings
# Third party imports
from celery import shared_task
@@ -7,11 +8,18 @@ from celery import shared_task
# Module imports
from plane.db.models import UserRecentVisit, Workspace
from plane.utils.exception_logger import log_exception
from plane.settings.redis import redis_instance
@shared_task
@shared_task(queue=settings.TASK_LOW_QUEUE)
def recent_visited_task(entity_name, entity_identifier, user_id, project_id, slug):
try:
ri = redis_instance()
# Check if the same entity is set in redis for the user
if ri.exists(f"recent_visited:{user_id}:{entity_name}:{entity_identifier}"):
return
# Check if the same entity is set in redis for the workspace
workspace = Workspace.objects.get(slug=slug)
recent_visited = UserRecentVisit.objects.filter(
entity_name=entity_name,
@@ -50,6 +58,10 @@ def recent_visited_task(entity_name, entity_identifier, user_id, project_id, slu
recent_activity.updated_by_id = user_id
recent_activity.save(update_fields=["created_by_id", "updated_by_id"])
# Set in redis
ri.set(
f"recent_visited:{user_id}:{entity_name}:{entity_identifier}", 1, ex=60 * 10
)
return
except Exception as e:
log_exception(e)
@@ -1,3 +1,6 @@
# Django imports
from django.conf import settings
# Third party imports
from celery import shared_task
@@ -7,7 +10,7 @@ from plane.settings.storage import S3Storage
from plane.utils.exception_logger import log_exception
@shared_task
@shared_task(queue=settings.TASK_DEFAULT_QUEUE)
def get_asset_object_metadata(asset_id):
try:
# Get the asset
@@ -5,6 +5,7 @@ import logging
from django.core.mail import EmailMultiAlternatives, get_connection
from django.template.loader import render_to_string
from django.utils.html import strip_tags
from django.conf import settings
# Third party imports
from celery import shared_task
@@ -15,7 +16,7 @@ from plane.license.utils.instance_value import get_email_configuration
from plane.utils.exception_logger import log_exception
@shared_task
@shared_task(queue=settings.TASK_HIGH_QUEUE)
def user_activation_email(current_site, user_id):
try:
# Send email to user when account is activated
@@ -5,6 +5,7 @@ import logging
from django.core.mail import EmailMultiAlternatives, get_connection
from django.template.loader import render_to_string
from django.utils.html import strip_tags
from django.conf import settings
# Third party imports
from celery import shared_task
@@ -15,7 +16,7 @@ from plane.license.utils.instance_value import get_email_configuration
from plane.utils.exception_logger import log_exception
@shared_task
@shared_task(queue=settings.TASK_DEFAULT_QUEUE)
def user_deactivation_email(current_site, user_id):
try:
# Send email to user when account is deactivated
+1
View File
@@ -86,6 +86,7 @@ def get_model_data(event, event_id, many=False):
retry_backoff=600,
max_retries=5,
retry_jitter=True,
queue=settings.TASK_NOTIFICATION_QUEUE,
)
def webhook_task(self, webhook, slug, event, event_data, action, current_site):
try:
@@ -8,6 +8,7 @@ from celery import shared_task
from django.core.mail import EmailMultiAlternatives, get_connection
from django.template.loader import render_to_string
from django.utils.html import strip_tags
from django.conf import settings
# Module imports
from plane.db.models import User, Workspace, WorkspaceMemberInvite
@@ -15,7 +16,7 @@ from plane.license.utils.instance_value import get_email_configuration
from plane.utils.exception_logger import log_exception
@shared_task
@shared_task(queue=settings.TASK_DEFAULT_QUEUE)
def workspace_invitation(email, workspace_id, token, current_site, invitor):
try:
user = User.objects.get(email=invitor)
+33
View File
@@ -14,6 +14,39 @@ app = Celery("plane")
# pickle the object when using Windows.
app.config_from_object("django.conf:settings", namespace="CELERY")
# Add additional configurations
CELERY_CONFIGURATIONS = {
"worker_prefetch_multiplier": int(os.environ.get("WORKER_PREFETCH_MULTIPLIER", 1)),
"worker_max_tasks_per_child": int(
os.environ.get("WORKER_MAX_TASKS_PER_CHILD", 100)
),
"worker_max_memory_per_child": int(
os.environ.get("WORKER_MAX_MEMORY_PER_CHILD", 150000)
),
"task_time_limit": int(os.environ.get("TASK_TIME_LIMIT", 3600)), # Hard time limit
"task_soft_time_limit": int(
os.environ.get("TASK_SOFT_TIME_LIMIT", 1800)
), # Soft time limit (30 minutes)
"worker_send_task_events": bool(
os.environ.get("WORKER_SEND_TASK_EVENTS", "0") == "1"
),
"task_ignore_result": bool(
os.environ.get("TASK_IGNORE_RESULT", "1") == "1"
), # Ignore results unless explicitly needed
"task_store_errors_even_if_ignored": bool(
os.environ.get("TASK_STORE_ERRORS_EVEN_IF_IGNORED", "1") == "1"
), # Store errors even if results are ignored
"task_acks_late": bool(
os.environ.get("TASK_ACKS_LATE", "1") == "1"
), # Acknowledge tasks after completion
"task_reject_on_worker_lost": bool(
os.environ.get("TASK_REJECT_ON_WORKER_LOST", "1") == "1"
), # Reject tasks if worker is lost
}
app.conf.update(**CELERY_CONFIGURATIONS)
app.conf.beat_schedule = {
# Executes every day at 12 AM
"check-every-day-to-archive-and-close": {
+4 -1
View File
@@ -1,3 +1,6 @@
# Django imports
from django.conf import settings
# Third party imports
from celery import shared_task
from opentelemetry import trace
@@ -19,7 +22,7 @@ from plane.db.models import (
from plane.utils.telemetry import init_tracer, shutdown_tracer
@shared_task
@shared_task(queue=settings.TASK_LOW_QUEUE)
def instance_traces():
try:
init_tracer()
+75 -15
View File
@@ -3,7 +3,7 @@
# Python imports
import os
from urllib.parse import urlparse
from kombu import Queue, Exchange
# Third party imports
import dj_database_url
@@ -146,8 +146,20 @@ else:
}
}
REDIS_HOST = os.environ.get("REDIS_HOST")
REDIS_PORT = os.environ.get("REDIS_PORT", "6379")
REDIS_USER = os.environ.get("REDIS_USER", "")
REDIS_PASSWORD = os.environ.get("REDIS_PASSWORD", "")
# Redis Config
REDIS_URL = os.environ.get("REDIS_URL")
if os.environ.get("REDIS_URL"):
REDIS_URL = os.environ.get("REDIS_URL")
else:
if not REDIS_HOST:
raise Exception("REDIS_HOST is not set")
REDIS_URL = f"redis://{REDIS_USER}:{REDIS_PASSWORD}@{REDIS_HOST}:{REDIS_PORT}" # noqa
# Check if redis url is ssl
REDIS_SSL = REDIS_URL and "rediss" in REDIS_URL
if REDIS_SSL:
@@ -233,25 +245,79 @@ if AWS_S3_ENDPOINT_URL and USE_MINIO:
AWS_S3_URL_PROTOCOL = f"{parsed_url.scheme}:"
# RabbitMQ connection settings
RABBITMQ_HOST = os.environ.get("RABBITMQ_HOST", "localhost")
RABBITMQ_HOST = os.environ.get("RABBITMQ_HOST")
RABBITMQ_PORT = os.environ.get("RABBITMQ_PORT", "5672")
RABBITMQ_USER = os.environ.get("RABBITMQ_USER", "guest")
RABBITMQ_PASSWORD = os.environ.get("RABBITMQ_PASSWORD", "guest")
RABBITMQ_VHOST = os.environ.get("RABBITMQ_VHOST", "/")
AMQP_URL = os.environ.get("AMQP_URL")
if os.environ.get("AMQP_URL"):
AMQP_URL = os.environ.get("AMQP_URL")
else:
if not RABBITMQ_HOST:
raise Exception("RABBITMQ_HOST is not set")
AMQP_URL = f"amqp://{RABBITMQ_USER}:{RABBITMQ_PASSWORD}@{RABBITMQ_HOST}:{RABBITMQ_PORT}/{RABBITMQ_VHOST}"
# Celery Configuration
if AMQP_URL:
CELERY_BROKER_URL = AMQP_URL
else:
CELERY_BROKER_URL = f"amqp://{RABBITMQ_USER}:{RABBITMQ_PASSWORD}@{RABBITMQ_HOST}:{RABBITMQ_PORT}/{RABBITMQ_VHOST}"
CELERY_BROKER_URL = AMQP_URL
CELERY_RESULT_BACKEND = REDIS_URL
CELERY_TIMEZONE = TIME_ZONE
CELERY_TASK_SERIALIZER = "json"
CELERY_RESULT_SERIALIZER = "json"
CELERY_ACCEPT_CONTENT = ["application/json"]
CELERY_RESULT_EXPIRES = int(
os.environ.get("CELERY_RESULT_EXPIRES", 60 * 60 * 24)
) # Expire results after 24 hours
QUEUE_SUFFIX = os.environ.get("QUEUE_SUFFIX", "plane")
TASK_HIGH_QUEUE = f"{QUEUE_SUFFIX}_high"
TASK_DEFAULT_QUEUE = f"{QUEUE_SUFFIX}_default"
TASK_LOW_QUEUE = f"{QUEUE_SUFFIX}_low"
TASK_SCHEDULER_QUEUE = f"{QUEUE_SUFFIX}_schedule"
TASK_NOTIFICATION_QUEUE = f"{QUEUE_SUFFIX}_notification"
# Define queues with their exchanges
CELERY_TASK_QUEUES = (
Queue(
TASK_HIGH_QUEUE,
Exchange("high_exchange", type="direct"),
routing_key="high.#",
queue_arguments={"x-max-priority": 10, "x-queue-mode": "lazy"},
),
Queue(
TASK_DEFAULT_QUEUE,
Exchange("default_exchange", type="direct"),
routing_key="default.#",
queue_arguments={"x-max-priority": 5, "x-queue-mode": "lazy"},
),
Queue(
TASK_LOW_QUEUE,
Exchange("low_exchange", type="direct"),
routing_key="low.#",
queue_arguments={"x-max-priority": 1, "x-queue-mode": "lazy"},
),
Queue(
TASK_SCHEDULER_QUEUE,
Exchange("scheduled_exchange", type="direct"),
routing_key="scheduled.#",
queue_arguments={"x-max-priority": 3, "x-queue-mode": "lazy"},
),
Queue(
TASK_NOTIFICATION_QUEUE,
Exchange("notifications_exchange", type="direct"),
routing_key="notifications.#",
queue_arguments={"x-max-priority": 4, "x-queue-mode": "lazy"},
),
)
# Default queue settings
CELERY_TASK_DEFAULT_QUEUE = "default"
CELERY_TASK_DEFAULT_EXCHANGE = "default_exchange"
CELERY_TASK_DEFAULT_ROUTING_KEY = "default.#"
CELERY_IMPORTS = (
# scheduled tasks
"plane.bgtasks.issue_automation_task",
@@ -297,16 +363,10 @@ GITHUB_ACCESS_TOKEN = os.environ.get("GITHUB_ACCESS_TOKEN", False)
ANALYTICS_SECRET_KEY = os.environ.get("ANALYTICS_SECRET_KEY", False)
ANALYTICS_BASE_API = os.environ.get("ANALYTICS_BASE_API", False)
# Posthog settings
POSTHOG_API_KEY = os.environ.get("POSTHOG_API_KEY", False)
POSTHOG_HOST = os.environ.get("POSTHOG_HOST", False)
# instance key
INSTANCE_KEY = os.environ.get(
"INSTANCE_KEY", "ae6517d563dfc13d8270bd45cf17b08f70b37d989128a9dab46ff687603333c3"
)
# Skip environment variable configuration
SKIP_ENV_VAR = os.environ.get("SKIP_ENV_VAR", "1") == "1"
+2 -4
View File
@@ -91,7 +91,6 @@ def issue_on_results(issues, group_by, sub_group_by):
Case(
When(
votes__isnull=False,
votes__deleted_at__isnull=True,
then=JSONObject(
vote=F("votes__vote"),
actor_details=JSONObject(
@@ -118,14 +117,13 @@ def issue_on_results(issues, group_by, sub_group_by):
default=None,
output_field=JSONField(),
),
filter=Q(votes__isnull=False,votes__deleted_at__isnull=True),
filter=Q(votes__isnull=False),
distinct=True,
),
reaction_items=ArrayAgg(
Case(
When(
issue_reactions__isnull=False,
issue_reactions__deleted_at__isnull=True,
then=JSONObject(
reaction=F("issue_reactions__reaction"),
actor_details=JSONObject(
@@ -152,7 +150,7 @@ def issue_on_results(issues, group_by, sub_group_by):
default=None,
output_field=JSONField(),
),
filter=Q(issue_reactions__isnull=False, issue_reactions__deleted_at__isnull=True),
filter=Q(issue_reactions__isnull=False),
distinct=True,
),
).values(*required_fields, "vote_items", "reaction_items")
+1 -7
View File
@@ -86,13 +86,7 @@ class EntityAssetEndpoint(BaseAPIView):
)
# Check if the file type is allowed
allowed_types = [
"image/jpeg",
"image/png",
"image/webp",
"image/jpg",
"image/gif",
]
allowed_types = ["image/jpeg", "image/png", "image/webp"]
if type not in allowed_types:
return Response(
{
+2 -12
View File
@@ -701,7 +701,6 @@ class IssueRetrievePublicEndpoint(BaseAPIView):
Case(
When(
votes__isnull=False,
votes__deleted_at__isnull=True,
then=JSONObject(
vote=F("votes__vote"),
actor_details=JSONObject(
@@ -733,11 +732,7 @@ class IssueRetrievePublicEndpoint(BaseAPIView):
output_field=JSONField(),
),
filter=Case(
When(
votes__isnull=False,
votes__deleted_at__isnull=True,
then=True,
),
When(votes__isnull=False, then=True),
default=False,
output_field=JSONField(),
),
@@ -747,7 +742,6 @@ class IssueRetrievePublicEndpoint(BaseAPIView):
Case(
When(
issue_reactions__isnull=False,
issue_reactions__deleted_at__isnull=True,
then=JSONObject(
reaction=F("issue_reactions__reaction"),
actor_details=JSONObject(
@@ -781,11 +775,7 @@ class IssueRetrievePublicEndpoint(BaseAPIView):
output_field=JSONField(),
),
filter=Case(
When(
issue_reactions__isnull=False,
issue_reactions__deleted_at__isnull=True,
then=True,
),
When(issue_reactions__isnull=False, then=True),
default=False,
output_field=JSONField(),
),
-5
View File
@@ -39,8 +39,3 @@ export enum EServerGroupByToFilterOptions {
"project_id" = "project",
"created_by" = "created_by",
}
export enum EIssueServiceType {
ISSUES = "issues",
EPICS = "epics",
}
+2
View File
@@ -57,6 +57,7 @@
"@tiptap/starter-kit": "^2.1.13",
"@tiptap/suggestion": "^2.0.13",
"class-variance-authority": "^0.7.0",
"clsx": "^1.2.1",
"highlight.js": "^11.8.0",
"jsx-dom-cjs": "^8.0.3",
"linkifyjs": "^4.1.3",
@@ -65,6 +66,7 @@
"prosemirror-codemark": "^0.4.2",
"prosemirror-utils": "^1.2.2",
"react-moveable": "^0.54.2",
"tailwind-merge": "^1.14.0",
"tippy.js": "^6.3.7",
"tiptap-markdown": "^0.8.9",
"uuid": "^10.0.0",
@@ -19,7 +19,6 @@ const CollaborativeDocumentEditor = (props: ICollaborativeDocumentEditor) => {
containerClassName,
disabledExtensions,
displayConfig = DEFAULT_DISPLAY_CONFIG,
editable,
editorClassName = "",
embedHandler,
fileHandler,
@@ -45,8 +44,8 @@ const CollaborativeDocumentEditor = (props: ICollaborativeDocumentEditor) => {
// use document editor
const { editor, hasServerConnectionFailed, hasServerSynced } = useCollaborativeEditor({
onTransaction,
disabledExtensions,
editable,
editorClassName,
embedHandler,
extensions,
@@ -55,7 +54,6 @@ const CollaborativeDocumentEditor = (props: ICollaborativeDocumentEditor) => {
handleEditorReady,
id,
mentionHandler,
onTransaction,
placeholder,
realtimeConfig,
serverHandler,
@@ -0,0 +1,81 @@
import { forwardRef, MutableRefObject } from "react";
// components
import { DocumentContentLoader, PageRenderer } from "@/components/editors";
// constants
import { DEFAULT_DISPLAY_CONFIG } from "@/constants/config";
// extensions
import { IssueWidget } from "@/extensions";
// helpers
import { getEditorClassNames } from "@/helpers/common";
// hooks
import { useReadOnlyCollaborativeEditor } from "@/hooks/use-read-only-collaborative-editor";
// types
import { EditorReadOnlyRefApi, ICollaborativeDocumentReadOnlyEditor } from "@/types";
const CollaborativeDocumentReadOnlyEditor = (props: ICollaborativeDocumentReadOnlyEditor) => {
const {
containerClassName,
disabledExtensions,
displayConfig = DEFAULT_DISPLAY_CONFIG,
editorClassName = "",
embedHandler,
fileHandler,
forwardedRef,
handleEditorReady,
id,
mentionHandler,
realtimeConfig,
serverHandler,
user,
} = props;
const extensions = [];
if (embedHandler?.issue) {
extensions.push(
IssueWidget({
widgetCallback: embedHandler.issue.widgetCallback,
})
);
}
const { editor, hasServerConnectionFailed, hasServerSynced } = useReadOnlyCollaborativeEditor({
disabledExtensions,
editorClassName,
extensions,
fileHandler,
forwardedRef,
handleEditorReady,
id,
mentionHandler,
realtimeConfig,
serverHandler,
user,
});
const editorContainerClassName = getEditorClassNames({
containerClassName,
});
if (!editor) return null;
if (!hasServerSynced && !hasServerConnectionFailed) return <DocumentContentLoader />;
return (
<PageRenderer
displayConfig={displayConfig}
id={id}
editor={editor}
editorContainerClassName={editorContainerClassName}
/>
);
};
const CollaborativeDocumentReadOnlyEditorWithRef = forwardRef<
EditorReadOnlyRefApi,
ICollaborativeDocumentReadOnlyEditor
>((props, ref) => (
<CollaborativeDocumentReadOnlyEditor {...props} forwardedRef={ref as MutableRefObject<EditorReadOnlyRefApi | null>} />
));
CollaborativeDocumentReadOnlyEditorWithRef.displayName = "CollaborativeDocumentReadOnlyEditorWithRef";
export { CollaborativeDocumentReadOnlyEditorWithRef };
@@ -1,4 +1,5 @@
export * from "./collaborative-editor";
export * from "./collaborative-read-only-editor";
export * from "./loader";
export * from "./page-renderer";
export * from "./read-only-editor";
@@ -140,10 +140,10 @@ export const PageRenderer = (props: IPageRenderer) => {
>
<EditorContentWrapper editor={editor} id={id} tabIndex={tabIndex} />
{editor.isEditable && (
<div>
<>
<BlockMenu editor={editor} />
<AIFeaturesMenu menu={aiHandler?.menu} />
</div>
</>
)}
</EditorContainer>
</div>
@@ -1,9 +1,9 @@
import { FC, ReactNode } from "react";
import { Editor } from "@tiptap/react";
// plane utils
import { cn } from "@plane/utils";
// constants
import { DEFAULT_DISPLAY_CONFIG } from "@/constants/config";
// helpers
import { cn } from "@/helpers/common";
// types
import { TDisplayConfig } from "@/types";
@@ -38,7 +38,6 @@ export const EditorWrapper: React.FC<Props> = (props) => {
} = props;
const editor = useEditor({
editable: true,
disabledExtensions,
editorClassName,
enableHistory: true,
@@ -1,7 +1,7 @@
import { useCallback, useEffect, useRef, useState } from "react";
import tippy, { Instance } from "tippy.js";
// plane utils
import { cn } from "@plane/utils";
// helpers
import { cn } from "@/helpers/common";
// types
import { TAIHandler } from "@/types";
@@ -1,9 +1,9 @@
import { Editor } from "@tiptap/core";
import { AlignCenter, AlignLeft, AlignRight, LucideIcon } from "lucide-react";
// plane utils
import { cn } from "@plane/utils";
// components
import { TextAlignItem } from "@/components/menus";
// helpers
import { cn } from "@/helpers/common";
// types
import { TEditorCommands } from "@/types";
@@ -1,11 +1,10 @@
import { Dispatch, FC, SetStateAction } from "react";
import { Editor } from "@tiptap/react";
import { ALargeSmall, Ban } from "lucide-react";
// plane utils
import { cn } from "@plane/utils";
// constants
import { COLORS_LIST } from "@/constants/common";
// helpers
import { cn } from "@/helpers/common";
import { BackgroundColorItem, TextColorItem } from "../menu-items";
type Props = {
@@ -1,10 +1,8 @@
import { Dispatch, FC, SetStateAction, useCallback, useEffect, useRef } from "react";
import { Editor } from "@tiptap/core";
import { Check, Link, Trash } from "lucide-react";
// plane utils
import { cn } from "@plane/utils";
// helpers
import { isValidHttpUrl } from "@/helpers/common";
import { cn, isValidHttpUrl } from "@/helpers/common";
import { setLinkEditor, unsetLinkEditor } from "@/helpers/editor-commands";
type Props = {
@@ -1,8 +1,6 @@
import { Dispatch, FC, SetStateAction } from "react";
import { Editor } from "@tiptap/react";
import { Check, ChevronDown } from "lucide-react";
// plane utils
import { cn } from "@plane/utils";
// components
import {
BulletListItem,
@@ -19,6 +17,8 @@ import {
HeadingSixItem,
EditorMenuItem,
} from "@/components/menus";
// helpers
import { cn } from "@/helpers/common";
// types
import { TEditorCommands } from "@/types";
@@ -1,7 +1,5 @@
import { FC, useEffect, useState } from "react";
import { BubbleMenu, BubbleMenuProps, Editor, isNodeSelection } from "@tiptap/react";
// plane utils
import { cn } from "@plane/utils";
// components
import {
BoldItem,
@@ -15,6 +13,8 @@ import {
} from "@/components/menus";
// extensions
import { isCellSelection } from "@/extensions/table/table/utilities/is-cell-selection";
// helpers
import { cn } from "@/helpers/common";
// local components
import { TextAlignmentSelector } from "./alignment-selector";
@@ -5,6 +5,3 @@ export const DEFAULT_DISPLAY_CONFIG: TDisplayConfig = {
fontSize: "large-font",
fontStyle: "sans-serif",
};
export const ACCEPTED_FILE_MIME_TYPES = ["image/jpeg", "image/jpg", "image/png", "image/webp", "image/gif"];
export const ACCEPTED_FILE_EXTENSIONS = ACCEPTED_FILE_MIME_TYPES.map((type) => `.${type.split("/")[1]}`);
@@ -1,8 +1,8 @@
import { Ban, ChevronDown } from "lucide-react";
// plane utils
import { cn } from "@plane/utils";
// constants
import { COLORS_LIST } from "@/constants/common";
// helpers
import { cn } from "@/helpers/common";
type Props = {
disabled: boolean;
@@ -2,8 +2,8 @@
import { convertHexEmojiToDecimal } from "@plane/utils";
// plane ui
import { EmojiIconPicker, EmojiIconPickerTypes, Logo, TEmojiLogoProps } from "@plane/ui";
// plane utils
import { cn } from "@plane/utils";
// helpers
import { cn } from "@/helpers/common";
// types
import { TCalloutBlockAttributes } from "./types";
// utils
@@ -8,8 +8,8 @@ import { common, createLowlight } from "lowlight";
import { CopyIcon, CheckIcon } from "lucide-react";
// ui
import { Tooltip } from "@plane/ui";
// plane utils
import { cn } from "@plane/utils";
// helpers
import { cn } from "@/helpers/common";
// we just have ts support for now
const lowlight = createLowlight(common);
@@ -1,9 +1,9 @@
import React, { useRef, useState, useCallback, useLayoutEffect, useEffect } from "react";
import { NodeSelection } from "@tiptap/pm/state";
// plane utils
import { cn } from "@plane/utils";
// extensions
import { CustoBaseImageNodeViewProps, ImageToolbarRoot } from "@/extensions/custom-image";
// helpers
import { cn } from "@/helpers/common";
const MIN_SIZE = 100;
@@ -1,9 +1,7 @@
import { ChangeEvent, useCallback, useEffect, useMemo, useRef } from "react";
import { ImageIcon } from "lucide-react";
// plane utils
import { cn } from "@plane/utils";
// constants
import { ACCEPTED_FILE_EXTENSIONS } from "@/constants/config";
// helpers
import { cn } from "@/helpers/common";
// hooks
import { useUploader, useDropZone, uploadFirstImageAndInsertRemaining } from "@/hooks/use-file-upload";
// extensions
@@ -129,7 +127,7 @@ export const CustomImageUploader = (props: CustomImageUploaderProps) => {
return "Uploading...";
}
if (draggedInside && editor.isEditable) {
if (draggedInside) {
return "Drop image here";
}
@@ -139,16 +137,14 @@ export const CustomImageUploader = (props: CustomImageUploaderProps) => {
return (
<div
className={cn(
"image-upload-component flex items-center justify-start gap-2 py-3 px-2 rounded-lg text-custom-text-300 bg-custom-background-90 border border-dashed border-custom-border-300 transition-all duration-200 ease-in-out cursor-default",
"image-upload-component flex items-center justify-start gap-2 py-3 px-2 rounded-lg text-custom-text-300 hover:text-custom-text-200 bg-custom-background-90 hover:bg-custom-background-80 border border-dashed border-custom-border-300 transition-all duration-200 ease-in-out cursor-default",
{
"hover:text-custom-text-200 hover:bg-custom-background-80 cursor-pointer": editor.isEditable,
"bg-custom-background-80 text-custom-text-200": draggedInside && editor.isEditable,
"text-custom-primary-200 bg-custom-primary-100/10 border-custom-primary-200/10 hover:bg-custom-primary-100/10 hover:text-custom-primary-200":
selected && editor.isEditable,
"text-red-500 cursor-default": failedToLoadImage,
"hover:text-red-500": failedToLoadImage && editor.isEditable,
"bg-red-500/10": failedToLoadImage && selected,
"hover:bg-red-500/10": failedToLoadImage && selected && editor.isEditable,
"hover:text-custom-text-200 cursor-pointer": editor.isEditable,
"bg-custom-background-80 text-custom-text-200": draggedInside,
"text-custom-primary-200 bg-custom-primary-100/10 hover:bg-custom-primary-100/10 hover:text-custom-primary-200 border-custom-primary-200/10":
selected,
"text-red-500 cursor-default hover:text-red-500": failedToLoadImage,
"bg-red-500/10 hover:bg-red-500/10": failedToLoadImage && selected,
}
)}
onDrop={onDrop}
@@ -168,7 +164,7 @@ export const CustomImageUploader = (props: CustomImageUploaderProps) => {
ref={fileInputRef}
hidden
type="file"
accept={ACCEPTED_FILE_EXTENSIONS.join(",")}
accept=".jpg,.jpeg,.png,.webp"
onChange={onFileChange}
multiple
/>
@@ -1,7 +1,7 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { ExternalLink, Maximize, Minus, Plus, X } from "lucide-react";
// plane utils
import { cn } from "@plane/utils";
// helpers
import { cn } from "@/helpers/common";
type Props = {
image: {
@@ -1,6 +1,6 @@
import { useState } from "react";
// plane utils
import { cn } from "@plane/utils";
// helpers
import { cn } from "@/helpers/common";
// components
import { ImageFullScreenAction } from "./full-screen";
+45 -54
View File
@@ -2,67 +2,58 @@ import { Extension, Editor } from "@tiptap/core";
import { Plugin, PluginKey } from "@tiptap/pm/state";
import { EditorView } from "@tiptap/pm/view";
export const DropHandlerExtension = Extension.create({
name: "dropHandler",
priority: 1000,
export const DropHandlerExtension = () =>
Extension.create({
name: "dropHandler",
priority: 1000,
addProseMirrorPlugins() {
const editor = this.editor;
return [
new Plugin({
key: new PluginKey("drop-handler-plugin"),
props: {
handlePaste: (view: EditorView, event: ClipboardEvent) => {
if (
editor.isEditable &&
event.clipboardData &&
event.clipboardData.files &&
event.clipboardData.files.length > 0
) {
event.preventDefault();
const files = Array.from(event.clipboardData.files);
const imageFiles = files.filter((file) => file.type.startsWith("image"));
addProseMirrorPlugins() {
const editor = this.editor;
return [
new Plugin({
key: new PluginKey("drop-handler-plugin"),
props: {
handlePaste: (view: EditorView, event: ClipboardEvent) => {
if (event.clipboardData && event.clipboardData.files && event.clipboardData.files.length > 0) {
event.preventDefault();
const files = Array.from(event.clipboardData.files);
const imageFiles = files.filter((file) => file.type.startsWith("image"));
if (imageFiles.length > 0) {
const pos = view.state.selection.from;
insertImagesSafely({ editor, files: imageFiles, initialPos: pos, event: "drop" });
}
return true;
}
return false;
},
handleDrop: (view: EditorView, event: DragEvent, _slice: any, moved: boolean) => {
if (
editor.isEditable &&
!moved &&
event.dataTransfer &&
event.dataTransfer.files &&
event.dataTransfer.files.length > 0
) {
event.preventDefault();
const files = Array.from(event.dataTransfer.files);
const imageFiles = files.filter((file) => file.type.startsWith("image"));
if (imageFiles.length > 0) {
const coordinates = view.posAtCoords({
left: event.clientX,
top: event.clientY,
});
if (coordinates) {
const pos = coordinates.pos;
if (imageFiles.length > 0) {
const pos = view.state.selection.from;
insertImagesSafely({ editor, files: imageFiles, initialPos: pos, event: "drop" });
}
return true;
}
}
return false;
return false;
},
handleDrop: (view: EditorView, event: DragEvent, _slice: any, moved: boolean) => {
if (!moved && event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files.length > 0) {
event.preventDefault();
const files = Array.from(event.dataTransfer.files);
const imageFiles = files.filter((file) => file.type.startsWith("image"));
if (imageFiles.length > 0) {
const coordinates = view.posAtCoords({
left: event.clientX,
top: event.clientY,
});
if (coordinates) {
const pos = coordinates.pos;
insertImagesSafely({ editor, files: imageFiles, initialPos: pos, event: "drop" });
}
return true;
}
}
return false;
},
},
},
}),
];
},
});
}),
];
},
});
export const insertImagesSafely = async ({
editor,
files,
@@ -47,11 +47,10 @@ type TArguments = {
};
placeholder?: string | ((isFocused: boolean, value: string) => string);
tabIndex?: number;
editable: boolean;
};
export const CoreEditorExtensions = (args: TArguments): Extensions => {
const { disabledExtensions, enableHistory, fileHandler, mentionConfig, placeholder, tabIndex, editable } = args;
const { disabledExtensions, enableHistory, fileHandler, mentionConfig, placeholder, tabIndex } = args;
return [
StarterKit.configure({
@@ -90,7 +89,7 @@ export const CoreEditorExtensions = (args: TArguments): Extensions => {
...(enableHistory ? {} : { history: false }),
}),
CustomQuoteExtension,
DropHandlerExtension,
DropHandlerExtension(),
CustomHorizontalRule.configure({
HTMLAttributes: {
class: "py-4 border-custom-border-400",
@@ -138,7 +137,6 @@ export const CoreEditorExtensions = (args: TArguments): Extensions => {
CustomCodeInlineExtension,
Markdown.configure({
html: true,
transformCopiedText: true,
transformPastedText: true,
breaks: true,
}),
@@ -147,14 +145,12 @@ export const CoreEditorExtensions = (args: TArguments): Extensions => {
TableCell,
TableRow,
CustomMention({
mentionSuggestions: editable ? mentionConfig.mentionSuggestions : undefined,
mentionSuggestions: mentionConfig.mentionSuggestions,
mentionHighlights: mentionConfig.mentionHighlights,
readonly: !editable,
readonly: false,
}),
Placeholder.configure({
placeholder: ({ editor, node }) => {
if (!editor.isEditable) return;
if (node.type.name === "heading") return `Heading ${node.attrs.level}`;
if (editor.storage.imageComponent.uploadInProgress) return "";
@@ -5,8 +5,8 @@
// @ts-nocheck
import { useEffect, useState } from "react";
import { NodeViewWrapper } from "@tiptap/react";
// plane utils
import { cn } from "@plane/utils";
// helpers
import { cn } from "@/helpers/common";
// types
import { IMentionHighlight } from "@/types";
@@ -5,8 +5,8 @@ import { Editor } from "@tiptap/react";
import { v4 as uuidv4 } from "uuid";
// ui
import { Avatar } from "@plane/ui";
// plane utils
import { cn } from "@plane/utils";
// helpers
import { cn } from "@/helpers/common";
// types
import { IMentionSuggestion } from "@/types";
@@ -20,6 +20,7 @@ import {
TableRow,
Table,
CustomMention,
HeadingListExtension,
CustomReadOnlyImageExtension,
CustomTextAlignExtension,
CustomCalloutReadOnlyExtension,
@@ -138,6 +139,7 @@ export const CoreReadOnlyEditorExtensions = (props: Props): Extensions => {
}),
CharacterCount,
CustomColorExtension,
HeadingListExtension,
CustomTextAlignExtension,
CustomCalloutReadOnlyExtension,
...CoreReadOnlyEditorAdditionalExtensions({
@@ -1,5 +1,5 @@
// plane utils
import { cn } from "@plane/utils";
// helpers
import { cn } from "@/helpers/common";
// types
import { ISlashCommandItem } from "@/types";
+6 -2
View File
@@ -1,6 +1,6 @@
import { EditorState, Selection } from "@tiptap/pm/state";
// plane utils
import { cn } from "@plane/utils";
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
interface EditorClassNames {
noBorder?: boolean;
@@ -18,6 +18,10 @@ export const getEditorClassNames = ({ noBorder, borderOnFocus, containerClassNam
containerClassName
);
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
// Helper function to find the parent node of a specific type
export function findParentNodeOfType(selection: Selection, typeName: string) {
let depth = selection.$anchor.depth;
@@ -15,7 +15,6 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorProps) => {
const {
onTransaction,
disabledExtensions,
editable,
editorClassName,
editorProps = {},
embedHandler,
@@ -76,7 +75,7 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorProps) => {
const editor = useEditor({
disabledExtensions,
id,
editable,
onTransaction,
editorProps,
editorClassName,
enableHistory: false,
@@ -98,10 +97,9 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorProps) => {
}),
],
fileHandler,
forwardedRef,
handleEditorReady,
forwardedRef,
mentionHandler,
onTransaction,
placeholder,
provider,
tabIndex,
+31 -37
View File
@@ -27,7 +27,6 @@ import type {
} from "@/types";
export interface CustomEditorProps {
editable: boolean;
editorClassName: string;
editorProps?: EditorProps;
enableHistory: boolean;
@@ -56,7 +55,6 @@ export interface CustomEditorProps {
export const useEditor = (props: CustomEditorProps) => {
const {
disabledExtensions,
editable = true,
editorClassName,
editorProps = {},
enableHistory,
@@ -76,46 +74,42 @@ export const useEditor = (props: CustomEditorProps) => {
autofocus = false,
} = props;
// states
const [savedSelection, setSavedSelection] = useState<Selection | null>(null);
// refs
const editorRef: MutableRefObject<Editor | null> = useRef(null);
const savedSelectionRef = useRef(savedSelection);
const editor = useTiptapEditor(
{
editable,
autofocus,
editorProps: {
...CoreEditorProps({
editorClassName,
}),
...editorProps,
},
extensions: [
...CoreEditorExtensions({
editable,
disabledExtensions,
enableHistory,
fileHandler,
mentionConfig: {
mentionSuggestions: mentionHandler.suggestions ?? (() => Promise.resolve<IMentionSuggestion[]>([])),
mentionHighlights: mentionHandler.highlights,
},
placeholder,
tabIndex,
}),
...extensions,
],
content: typeof initialValue === "string" && initialValue.trim() !== "" ? initialValue : "<p></p>",
onCreate: () => handleEditorReady?.(true),
onTransaction: ({ editor }) => {
setSavedSelection(editor.state.selection);
onTransaction?.();
},
onUpdate: ({ editor }) => onChange?.(editor.getJSON(), editor.getHTML()),
onDestroy: () => handleEditorReady?.(false),
const editor = useTiptapEditor({
autofocus,
editorProps: {
...CoreEditorProps({
editorClassName,
}),
...editorProps,
},
[editable]
);
extensions: [
...CoreEditorExtensions({
disabledExtensions,
enableHistory,
fileHandler,
mentionConfig: {
mentionSuggestions: mentionHandler.suggestions ?? (() => Promise.resolve<IMentionSuggestion[]>([])),
mentionHighlights: mentionHandler.highlights,
},
placeholder,
tabIndex,
}),
...extensions,
],
content: typeof initialValue === "string" && initialValue.trim() !== "" ? initialValue : "<p></p>",
onCreate: () => handleEditorReady?.(true),
onTransaction: ({ editor }) => {
setSavedSelection(editor.state.selection);
onTransaction?.();
},
onUpdate: ({ editor }) => onChange?.(editor.getJSON(), editor.getHTML()),
onDestroy: () => handleEditorReady?.(false),
});
// Update the ref whenever savedSelection changes
useEffect(() => {
@@ -105,7 +105,7 @@ export const useDropZone = (args: TDropzoneArgs) => {
async (e: DragEvent<HTMLDivElement>) => {
e.preventDefault();
setDraggedInside(false);
if (e.dataTransfer.files.length === 0 || !editor.isEditable) {
if (e.dataTransfer.files.length === 0) {
return;
}
const filesList = e.dataTransfer.files;
@@ -0,0 +1,92 @@
import { useEffect, useMemo, useState } from "react";
import { HocuspocusProvider } from "@hocuspocus/provider";
import Collaboration from "@tiptap/extension-collaboration";
import { IndexeddbPersistence } from "y-indexeddb";
// extensions
import { HeadingListExtension } from "@/extensions";
// hooks
import { useReadOnlyEditor } from "@/hooks/use-read-only-editor";
// types
import { TReadOnlyCollaborativeEditorProps } from "@/types";
export const useReadOnlyCollaborativeEditor = (props: TReadOnlyCollaborativeEditorProps) => {
const {
disabledExtensions,
editorClassName,
editorProps = {},
extensions,
fileHandler,
forwardedRef,
handleEditorReady,
id,
mentionHandler,
realtimeConfig,
serverHandler,
user,
} = props;
// states
const [hasServerConnectionFailed, setHasServerConnectionFailed] = useState(false);
const [hasServerSynced, setHasServerSynced] = useState(false);
// initialize Hocuspocus provider
const provider = useMemo(
() =>
new HocuspocusProvider({
name: id,
url: realtimeConfig.url,
token: JSON.stringify(user),
parameters: realtimeConfig.queryParams,
onAuthenticationFailed: () => {
serverHandler?.onServerError?.();
setHasServerConnectionFailed(true);
},
onConnect: () => serverHandler?.onConnect?.(),
onClose: (data) => {
if (data.event.code === 1006) {
serverHandler?.onServerError?.();
setHasServerConnectionFailed(true);
}
},
onSynced: () => setHasServerSynced(true),
}),
[id, realtimeConfig, serverHandler, user]
);
// indexed db integration for offline support
const localProvider = useMemo(
() => (id ? new IndexeddbPersistence(id, provider.document) : undefined),
[id, provider]
);
// destroy and disconnect connection on unmount
useEffect(
() => () => {
provider.destroy();
localProvider?.destroy();
},
[provider, localProvider]
);
const editor = useReadOnlyEditor({
disabledExtensions,
editorProps,
editorClassName,
extensions: [
...(extensions ?? []),
HeadingListExtension,
Collaboration.configure({
document: provider.document,
}),
],
fileHandler,
forwardedRef,
handleEditorReady,
mentionHandler,
provider,
});
return {
editor,
hasServerConnectionFailed,
hasServerSynced,
};
};
@@ -1,6 +1,3 @@
// constants
import { ACCEPTED_FILE_MIME_TYPES } from "@/constants/config";
type TArgs = {
file: File;
maxFileSize: number;
@@ -14,8 +11,9 @@ export const isFileValid = (args: TArgs): boolean => {
return false;
}
if (!ACCEPTED_FILE_MIME_TYPES.includes(file.type)) {
alert("Invalid file type. Please select a JPEG, JPG, PNG, WEBP or GIF file.");
const allowedTypes = ["image/jpeg", "image/jpg", "image/png", "image/webp"];
if (!allowedTypes.includes(file.type)) {
alert("Invalid file type. Please select a JPEG, JPG, PNG, or WEBP image file.");
return false;
}
+2 -2
View File
@@ -1,6 +1,6 @@
import { EditorProps } from "@tiptap/pm/view";
// plane utils
import { cn } from "@plane/utils";
// helpers
import { cn } from "@/helpers/common";
export type TCoreEditorProps = {
editorClassName: string;
+2 -2
View File
@@ -1,6 +1,6 @@
import { EditorProps } from "@tiptap/pm/view";
// plane utils
import { cn } from "@plane/utils";
// helpers
import { cn } from "@/helpers/common";
// props
import { TCoreEditorProps } from "@/props";
@@ -21,7 +21,6 @@ export type TServerHandler = {
type TCollaborativeEditorHookProps = {
disabledExtensions: TExtensions[];
editable?: boolean;
editorClassName: string;
editorProps?: EditorProps;
extensions?: Extensions;
-1
View File
@@ -138,7 +138,6 @@ export interface IRichTextEditor extends IEditorProps {
export interface ICollaborativeDocumentEditor
extends Omit<IEditorProps, "initialValue" | "onChange" | "onEnterKeyPress" | "value"> {
editable: boolean;
aiHandler?: TAIHandler;
embedHandler: TEmbedConfig;
handleEditorReady?: (value: boolean) => void;
+1
View File
@@ -9,6 +9,7 @@ import "./styles/drag-drop.css";
// editors
export {
CollaborativeDocumentEditorWithRef,
CollaborativeDocumentReadOnlyEditorWithRef,
DocumentReadOnlyEditorWithRef,
LiteTextEditorWithRef,
LiteTextReadOnlyEditorWithRef,
+6 -6
View File
@@ -111,12 +111,8 @@ ul[data-type="taskList"] li > label input[type="checkbox"] {
transform: scale(1.05);
}
.ProseMirror[contenteditable="true"] input[type="checkbox"]:hover {
background-color: rgba(var(--color-background-80));
}
.ProseMirror[contenteditable="false"] input[type="checkbox"] {
pointer-events: none;
ul[data-type="taskList"] li > label input[type="checkbox"]:hover {
background-color: rgba(var(--color-background-80)) !important;
}
ul[data-type="taskList"] li > label input[type="checkbox"][checked] {
@@ -155,6 +151,10 @@ ul[data-type="taskList"] li > label input[type="checkbox"] {
margin-right: 0.2rem;
margin-top: 0.15rem;
&:hover {
background-color: rgb(var(--color-background-80));
}
&:active {
background-color: rgb(var(--color-background-90));
}
-2
View File
@@ -60,6 +60,4 @@ export enum EFileAssetType {
USER_COVER = "USER_COVER",
WORKSPACE_LOGO = "WORKSPACE_LOGO",
TEAM_SPACE_DESCRIPTION = "TEAM_SPACE_DESCRIPTION",
INITIATIVE_DESCRIPTION = "INITIATIVE_DESCRIPTION",
PROJECT_DESCRIPTION = "PROJECT_DESCRIPTION",
}
-6
View File
@@ -1,4 +1,3 @@
import { EIssueServiceType } from "@plane/constants";
import { TIssuePriorities } from "../issues";
import { TIssueAttachment } from "./issue_attachment";
import { TIssueLink } from "./issue_link";
@@ -40,7 +39,6 @@ export type TBaseIssue = {
updated_by: string;
is_draft: boolean;
is_epic?: boolean;
};
export type IssueRelation = {
@@ -123,7 +121,3 @@ export type TIssueDetailWidget =
| "relations"
| "links"
| "attachments";
export type TIssueServiceType =
| EIssueServiceType.ISSUES
| EIssueServiceType.EPICS;
-1
View File
@@ -136,7 +136,6 @@ export type TProjectIssuesSearchParams = {
issue_id?: string;
workspace_search: boolean;
target_date?: string;
epic?: boolean;
};
export interface ISearchIssueResponse {
-1
View File
@@ -13,5 +13,4 @@ export enum EModalWidth {
XXXXL = "sm:max-w-4xl",
VXL = "sm:max-w-5xl",
VIXL = "sm:max-w-6xl",
VIIXL = "sm:max-w-7xl",
}
+1 -3
View File
@@ -15,10 +15,8 @@
"lint:errors": "eslint src --ext .ts,.tsx --quiet"
},
"dependencies": {
"clsx": "^2.1.1",
"isomorphic-dompurify": "^2.16.0",
"react": "^18.3.1",
"tailwind-merge": "^2.5.5"
"react": "^18.3.1"
},
"devDependencies": {
"@plane/eslint-config": "*",
-4
View File
@@ -1,4 +0,0 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export const cn = (...inputs: ClassValue[]) => twMerge(clsx(inputs));
-1
View File
@@ -1,4 +1,3 @@
export * from "./color";
export * from "./common";
export * from "./emoji";
export * from "./string";
@@ -1,7 +1,7 @@
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// plane utils
import { cn } from "@plane/utils";
//plane
import { cn } from "@plane/editor";
// components
import { IssueEmojiReactions, IssueVotes } from "@/components/issues/reactions";
// hooks
@@ -4,12 +4,10 @@ import { MutableRefObject } from "react";
import { observer } from "mobx-react";
import Link from "next/link";
import { useParams, useSearchParams } from "next/navigation";
// plane types
// plane
import { cn } from "@plane/editor";
import { IIssueDisplayProperties } from "@plane/types";
// plane ui
import { Tooltip } from "@plane/ui";
// plane utils
import { cn } from "@plane/utils";
// components
import { WithDisplayPropertiesHOC } from "@/components/issues/issue-layouts/with-display-properties-HOC";
// helpers
@@ -4,12 +4,10 @@ import { useRef } from "react";
import { observer } from "mobx-react";
import Link from "next/link";
import { useParams, useSearchParams } from "next/navigation";
// plane types
// types
import { cn } from "@plane/editor";
import { IIssueDisplayProperties } from "@plane/types";
// plane ui
import { Tooltip } from "@plane/ui";
// plane utils
import { cn } from "@plane/utils";
// helpers
import { queryParamGenerator } from "@/helpers/query-param-generator";
// hooks
@@ -2,10 +2,9 @@
import { Fragment, MutableRefObject, forwardRef, useRef, useState } from "react";
import { observer } from "mobx-react";
// plane types
import { cn } from "@plane/editor";
// plane
import { IGroupByColumn, TIssueGroupByOptions, IIssueDisplayProperties, TPaginationData, TLoader } from "@plane/types";
// plane utils
import { cn } from "@plane/utils";
// hooks
import { useIntersectionObserver } from "@/hooks/use-intersection-observer";
//
@@ -2,12 +2,11 @@
import { observer } from "mobx-react";
import { Layers, Link, Paperclip } from "lucide-react";
// plane types
// types
import { cn } from "@plane/editor";
import { IIssueDisplayProperties } from "@plane/types";
// plane ui
import { Tooltip } from "@plane/ui";
// plane utils
import { cn } from "@plane/utils";
// ui
// components
import {
IssueBlockDate,
@@ -1,10 +1,9 @@
"use client";
import { observer } from "mobx-react";
// plane ui
// ui
import { cn } from "@plane/editor";
import { ContrastIcon, Tooltip } from "@plane/ui";
// plane utils
import { cn } from "@plane/utils";
//hooks
import { useCycle } from "@/hooks/store/use-cycle";
@@ -3,10 +3,9 @@
import { observer } from "mobx-react";
// icons
import { LucideIcon, Users } from "lucide-react";
// plane ui
// ui
import { cn } from "@plane/editor";
import { Avatar, AvatarGroup } from "@plane/ui";
// plane utils
import { cn } from "@plane/utils";
// hooks
import { useMember } from "@/hooks/store/use-member";
//
@@ -1,10 +1,9 @@
"use client";
import { observer } from "mobx-react";
// plane ui
// planes
import { cn } from "@plane/editor";
import { DiceIcon, Tooltip } from "@plane/ui";
// plane utils
import { cn } from "@plane/utils";
// hooks
import { useModule } from "@/hooks/store/use-module";
@@ -1,10 +1,9 @@
"use client";
import { observer } from "mobx-react";
// plane ui
// ui
import { cn } from "@plane/editor";
import { StateGroupIcon, Tooltip } from "@plane/ui";
// plane utils
import { cn } from "@plane/utils";
//hooks
import { useStates } from "@/hooks/store";
@@ -1,9 +1,8 @@
"use client";
import { CommandPalette } from "@/components/command-palette";
import { WorkspaceAuthWrapper } from "@/layouts/auth-layout";
import { AuthenticationWrapper } from "@/lib/wrappers";
// plane web components
import { WorkspaceAuthWrapper } from "@/plane-web/layouts/workspace-wrapper";
import { AppSidebar } from "./sidebar";
export default function WorkspaceLayout({ children }: { children: React.ReactNode }) {
@@ -1,8 +1,8 @@
"use client";
import { ReactNode } from "react";
// plane web layouts
import { ProjectAuthWrapper } from "@/plane-web/layouts/project-wrapper";
// layouts
import { ProjectAuthWrapper } from "@/layouts/auth-layout";
const ProjectDetailLayout = ({ children }: { children: ReactNode }) => (
<ProjectAuthWrapper>{children}</ProjectAuthWrapper>
@@ -1 +0,0 @@
export * from "./modal";
@@ -1,19 +0,0 @@
"use client";
import React, { FC } from "react";
import { TIssue } from "@plane/types";
export interface EpicModalProps {
data?: Partial<TIssue>;
isOpen: boolean;
onClose: () => void;
beforeFormSubmit?: () => Promise<void>;
onSubmit?: (res: TIssue) => Promise<void>;
fetchIssueDetails?: boolean;
primaryButtonText?: {
default: string;
loading: string;
};
isProjectSelectionDisabled?: boolean;
}
export const CreateUpdateEpicModal: FC<EpicModalProps> = (props) => <></>;
-1
View File
@@ -1 +0,0 @@
export * from "./epic-modal";
@@ -1,9 +1 @@
import { FC } from "react";
type Props = {
isEpic?: boolean;
};
export const TimelineDependencyPaths: FC<Props> = (props) => {
const { isEpic = false } = props;
return <></>;
};
export const TimelineDependencyPaths = () => <></>;
@@ -1,12 +1,9 @@
import { TIssueServiceType } from "@plane/types";
export type TIssueAdditionalPropertyValuesUpdateProps = {
issueId: string;
issueTypeId: string;
projectId: string;
workspaceSlug: string;
isDisabled: boolean;
issueServiceType?: TIssueServiceType;
};
export const IssueAdditionalPropertyValuesUpdate: React.FC<TIssueAdditionalPropertyValuesUpdateProps> = () => <></>;
@@ -1,6 +1,6 @@
import { Plus } from "lucide-react";
// plane utils
import { cn } from "@plane/utils";
// Plane
import { cn } from "@plane/editor";
type Props = {
workspaceSlug: string;
@@ -1,7 +1,6 @@
import { TDeDupeIssue } from "@plane/types";
export const useDebouncedDuplicateIssues = (
workspaceSlug: string | undefined,
workspaceId: string | undefined,
projectId: string | undefined,
formData: { name: string | undefined; description_html?: string | undefined; issueId?: string | undefined }
-15
View File
@@ -1,15 +0,0 @@
import { FC } from "react";
import { observer } from "mobx-react";
// layouts
import { ProjectAuthWrapper as CoreProjectAuthWrapper } from "@/layouts/auth-layout";
export type IProjectAuthWrapper = {
children: React.ReactNode;
};
export const ProjectAuthWrapper: FC<IProjectAuthWrapper> = observer((props) => {
// props
const { children } = props;
return <CoreProjectAuthWrapper>{children}</CoreProjectAuthWrapper>;
});
-15
View File
@@ -1,15 +0,0 @@
import { FC } from "react";
import { observer } from "mobx-react";
// layouts
import { WorkspaceAuthWrapper as CoreWorkspaceAuthWrapper } from "@/layouts/auth-layout";
export type IWorkspaceAuthWrapper = {
children: React.ReactNode;
};
export const WorkspaceAuthWrapper: FC<IWorkspaceAuthWrapper> = observer((props) => {
// props
const { children } = props;
return <CoreWorkspaceAuthWrapper>{children}</CoreWorkspaceAuthWrapper>;
});

Some files were not shown because too many files have changed in this diff Show More