Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 733b725b5e | |||
| 3bee051ee9 | |||
| 6b902d458e | |||
| 51ca353577 | |||
| 881c744eb9 | |||
| ec41ae61b4 | |||
| 5773c2bde3 | |||
| e33bae2125 | |||
| 580c4b1930 | |||
| ddd4b51b4e | |||
| ede4aad55b | |||
| 1a715c98b2 | |||
| 8e6d885731 | |||
| 4507802aba | |||
| 438cc33046 | |||
| 442b0fd7e5 | |||
| 1119b9dc36 |
@@ -5,9 +5,7 @@
|
||||
<img src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-readme/plane_logo_.webp" alt="Plane Logo" width="70">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<h3 align="center"><b>Plane</b></h3>
|
||||
<p align="center"><b>Open-source project management that unlocks customer value</b></p>
|
||||
<h1 align="center"><b>Plane</b></h1>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://discord.com/invite/A92xrEGCge">
|
||||
@@ -44,79 +42,85 @@ 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
|
||||
|
||||
The easiest way to get started with Plane is by creating a [Plane Cloud](https://app.plane.so) account.
|
||||
Getting started with Plane is simple. Choose the setup that works best for you:
|
||||
|
||||
If you would like to self-host Plane, please see our [deployment guide](https://docs.plane.so/docker-compose).
|
||||
- **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.
|
||||
|
||||
| Installation methods | Docs link |
|
||||
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| Docker | [](https://docs.plane.so/self-hosting/methods/docker-compose) |
|
||||
| Kubernetes | [](https://docs.plane.so/kubernetes) |
|
||||
| Docker | [](https://developers.plane.so/self-hosting/methods/docker-compose) |
|
||||
| Kubernetes | [](https://developers.plane.so/self-hosting/methods/kubernetes) |
|
||||
|
||||
`Instance admins` can configure instance settings with [God-mode](https://docs.plane.so/instance-admin).
|
||||
`Instance admins` can manage and customize settings using [God mode](https://developers.plane.so/self-hosting/govern/instance-admin).
|
||||
|
||||
## 🚀 Features
|
||||
## 🌟 Features
|
||||
|
||||
- **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.
|
||||
- **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.
|
||||
|
||||
- **Cycles**:
|
||||
Keep up your team's momentum with Cycles. Gain insights into your project's progress with burn-down charts and other valuable features.
|
||||
- **Cycles**
|
||||
Maintain your team’s momentum with Cycles. Track progress effortlessly using burn-down charts and other insightful tools.
|
||||
|
||||
- **Modules**: Break down your large projects into smaller, more manageable modules. Assign modules between teams to track and plan your project's progress easily.
|
||||
- **Modules**
|
||||
Simplify complex projects by dividing them into smaller, manageable modules.
|
||||
|
||||
- **Views**: Create custom filters to display only the issues that matter to you. Save and share your filters in just a few clicks.
|
||||
- **Views**
|
||||
Customize your workflow by creating filters to display only the most relevant issues. Save and share these views with ease.
|
||||
|
||||
- **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.
|
||||
- **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.
|
||||
|
||||
- **Analytics**: Get insights into all your Plane data in real-time. Visualize issue data to spot trends, remove blockers, and progress your work.
|
||||
- **Analytics**
|
||||
Access real-time insights across all your Plane data. Visualize trends, remove blockers, and keep your projects moving forward.
|
||||
|
||||
- **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
|
||||
|
||||
> Development system must have docker engine installed and running.
|
||||
## 🛠️ Local development
|
||||
|
||||
Setting up local environment is extremely easy and straight forward. Follow the below step and you will be ready to contribute -
|
||||
### Pre-requisites
|
||||
- Ensure Docker Engine is installed and running.
|
||||
|
||||
1. Clone the code locally using:
|
||||
### Development setup
|
||||
Setting up your local environment is simple and straightforward. Follow these steps to get started:
|
||||
|
||||
1. Clone the repository:
|
||||
```
|
||||
git clone https://github.com/makeplane/plane.git
|
||||
```
|
||||
2. Switch to the code folder:
|
||||
2. Navigate to the project folder:
|
||||
```
|
||||
cd plane
|
||||
```
|
||||
3. Create your feature or fix branch you plan to work on using:
|
||||
3. Create a new branch for your feature or fix:
|
||||
```
|
||||
git checkout -b <feature-branch-name>
|
||||
```
|
||||
4. Open terminal and run:
|
||||
4. Run the setup script in the terminal:
|
||||
```
|
||||
./setup.sh
|
||||
```
|
||||
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:
|
||||
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:
|
||||
```
|
||||
docker compose -f docker-compose-local.yml up -d
|
||||
```
|
||||
|
||||
You are ready to make changes to the code. Do not forget to refresh the browser (in case it does not auto-reload).
|
||||
That’s it! You’re all set to begin coding. Remember to refresh your browser if changes don’t auto-reload. Happy contributing! 🎉
|
||||
|
||||
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
|
||||
|
||||

|
||||
## Built with
|
||||
[](https://nextjs.org/)<br/>
|
||||
[](https://www.djangoproject.com/)<br/>
|
||||
[](https://nodejs.org/en)
|
||||
|
||||
## 📸 Screenshots
|
||||
|
||||
@@ -165,7 +169,7 @@ Ask questions, report bugs, join discussions, voice ideas, make feature requests
|
||||
</a>
|
||||
</p>
|
||||
</p>
|
||||
<p>
|
||||
<p>
|
||||
<a href="https://plane.so" target="_blank">
|
||||
<img
|
||||
src="https://ik.imagekit.io/w2okwbtu2/Drive_LlfeY4xn3.png?updatedAt=1709298837917"
|
||||
@@ -176,23 +180,42 @@ Ask questions, report bugs, join discussions, voice ideas, make feature requests
|
||||
</p>
|
||||
</p>
|
||||
|
||||
## ⛓️ Security
|
||||
## 📝 Documentation
|
||||
Explore Plane's [product documentation](https://docs.plane.so/) and [developer documentation](https://developers.plane.so/) to learn about features, setup, and usage.
|
||||
|
||||
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.
|
||||
## ❤️ Community
|
||||
|
||||
Email squawk@plane.so to disclose any security vulnerabilities.
|
||||
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.
|
||||
|
||||
## ❤️ Contribute
|
||||
Feel free to ask questions, report bugs, participate in discussions, share ideas, request features, or showcase your projects. We’d love to hear from you!
|
||||
|
||||
There are many ways to contribute to Plane, including:
|
||||
## 🛡️ Security
|
||||
|
||||
- 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.
|
||||
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
|
||||
|
||||

|
||||
|
||||
### 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
@@ -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 -O fair --prefetch-multiplier=1 --max-tasks-per-child=100 --max-memory-per-child=150000 -l INFO
|
||||
worker: celery -A plane worker -l info
|
||||
beat: celery -A plane beat -l INFO
|
||||
@@ -126,7 +126,13 @@ class UserAssetsV2Endpoint(BaseAPIView):
|
||||
)
|
||||
|
||||
# Check if the file type is allowed
|
||||
allowed_types = ["image/jpeg", "image/png", "image/webp", "image/jpg"]
|
||||
allowed_types = [
|
||||
"image/jpeg",
|
||||
"image/png",
|
||||
"image/webp",
|
||||
"image/jpg",
|
||||
"image/gif",
|
||||
]
|
||||
if type not in allowed_types:
|
||||
return Response(
|
||||
{
|
||||
|
||||
@@ -1,18 +1,10 @@
|
||||
# Python imports
|
||||
from datetime import timedelta
|
||||
|
||||
# Django imports
|
||||
from django.utils import timezone
|
||||
from django.conf import settings
|
||||
|
||||
# Third party imports
|
||||
from datetime import timedelta
|
||||
from plane.db.models import APIActivityLog
|
||||
from celery import shared_task
|
||||
|
||||
# Module imports
|
||||
from plane.db.models import APIActivityLog
|
||||
|
||||
|
||||
@shared_task(queue=settings.TASK_SCHEDULER_QUEUE)
|
||||
@shared_task
|
||||
def delete_api_logs():
|
||||
# Get the logs older than 30 days to delete
|
||||
logs_to_delete = APIActivityLog.objects.filter(
|
||||
|
||||
@@ -10,7 +10,7 @@ from django.db.models.fields.related import OneToOneRel
|
||||
from celery import shared_task
|
||||
|
||||
|
||||
@shared_task(queue=settings.TASK_DEFAULT_QUEUE)
|
||||
@shared_task
|
||||
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(queue=settings.TASK_SCHEDULER_QUEUE)
|
||||
@shared_task
|
||||
def hard_delete():
|
||||
from plane.db.models import (
|
||||
Workspace,
|
||||
|
||||
@@ -5,7 +5,6 @@ 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
|
||||
@@ -532,7 +531,7 @@ def create_module_issues(workspace, project, user_id, issue_count):
|
||||
)
|
||||
|
||||
|
||||
@shared_task(queue=settings.TASK_LOW_QUEUE)
|
||||
@shared_task
|
||||
def create_dummy_data(
|
||||
slug,
|
||||
email,
|
||||
|
||||
@@ -2,16 +2,16 @@ import logging
|
||||
import re
|
||||
from datetime import datetime
|
||||
|
||||
# Django imports
|
||||
from django.core.mail import EmailMultiAlternatives, get_connection
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils import timezone
|
||||
from django.utils.html import strip_tags
|
||||
from django.conf import settings
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
# Third party imports
|
||||
from bs4 import BeautifulSoup
|
||||
from celery import shared_task
|
||||
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
|
||||
|
||||
# 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(queue=settings.TASK_NOTIFICATION_QUEUE)
|
||||
@shared_task
|
||||
def send_email_notification(
|
||||
issue_id, notification_data, receiver_id, email_notification_ids
|
||||
):
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
# 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
|
||||
@@ -53,7 +49,7 @@ def auth_events(user, email, user_agent, ip, event_name, medium, first_time):
|
||||
return
|
||||
|
||||
|
||||
@shared_task(queue=settings.TASK_LOW_QUEUE)
|
||||
@shared_task
|
||||
def workspace_invite_event(user, email, user_agent, ip, event_name, accepted_from):
|
||||
try:
|
||||
POSTHOG_API_KEY, POSTHOG_HOST = posthogConfiguration()
|
||||
|
||||
@@ -7,13 +7,12 @@ 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
|
||||
@@ -300,7 +299,7 @@ def generate_xlsx(header, project_id, issues, files):
|
||||
files.append((f"{project_id}.xlsx", xlsx_file))
|
||||
|
||||
|
||||
@shared_task(queue=settings.TASK_LOW_QUEUE)
|
||||
@shared_task
|
||||
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(queue=settings.TASK_SCHEDULER_QUEUE)
|
||||
@shared_task
|
||||
def delete_old_s3_link():
|
||||
# Get a list of keys and IDs to process
|
||||
expired_exporter_history = ExporterHistory.objects.filter(
|
||||
|
||||
@@ -5,7 +5,6 @@ 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
|
||||
@@ -14,7 +13,7 @@ from celery import shared_task
|
||||
from plane.db.models import FileAsset
|
||||
|
||||
|
||||
@shared_task(queue=settings.TASK_HIGH_QUEUE)
|
||||
@shared_task
|
||||
def delete_unuploaded_file_asset():
|
||||
"""This task deletes unuploaded file assets older than a certain number of days."""
|
||||
FileAsset.objects.filter(
|
||||
|
||||
@@ -1,22 +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(queue=settings.TASK_HIGH_QUEUE)
|
||||
@shared_task
|
||||
def forgot_password(first_name, email, uidb64, token, current_site):
|
||||
try:
|
||||
relative_link = (
|
||||
|
||||
@@ -2,17 +2,17 @@
|
||||
import json
|
||||
|
||||
|
||||
# Django imports
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.utils import timezone
|
||||
from django.conf import settings
|
||||
|
||||
# Third Party imports
|
||||
from celery import shared_task
|
||||
|
||||
# Module imports
|
||||
# Django imports
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.utils import timezone
|
||||
|
||||
from plane.app.serializers import IssueActivitySerializer
|
||||
from plane.bgtasks.notification_task import notifications
|
||||
|
||||
# Module imports
|
||||
from plane.db.models import (
|
||||
CommentReaction,
|
||||
Cycle,
|
||||
@@ -1548,7 +1548,7 @@ def create_intake_activity(
|
||||
|
||||
|
||||
# Receive message from room group
|
||||
@shared_task(queue=settings.TASK_HIGH_QUEUE)
|
||||
@shared_task
|
||||
def issue_activity(
|
||||
type,
|
||||
requested_data,
|
||||
|
||||
@@ -2,13 +2,12 @@
|
||||
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
|
||||
@@ -16,7 +15,7 @@ from plane.db.models import Issue, Project, State
|
||||
from plane.utils.exception_logger import log_exception
|
||||
|
||||
|
||||
@shared_task(queue=settings.TASK_SCHEDULER_QUEUE)
|
||||
@shared_task
|
||||
def archive_and_close_old_issues():
|
||||
archive_old_issues()
|
||||
close_old_issues()
|
||||
|
||||
@@ -1,16 +1,9 @@
|
||||
# Python imports
|
||||
from celery import shared_task
|
||||
from django.db import transaction
|
||||
from django.utils import timezone
|
||||
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
|
||||
|
||||
@@ -46,7 +39,7 @@ def update_existing_version(version: IssueDescriptionVersion, issue) -> None:
|
||||
)
|
||||
|
||||
|
||||
@shared_task(queue=settings.TASK_HIGH_QUEUE)
|
||||
@shared_task
|
||||
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(queue=settings.TASK_NOTIFICATION_QUEUE)
|
||||
@shared_task
|
||||
def magic_link(email, key, token, current_site):
|
||||
try:
|
||||
(
|
||||
|
||||
@@ -3,9 +3,6 @@ import json
|
||||
import uuid
|
||||
from uuid import UUID
|
||||
|
||||
# Django imports
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
# Module imports
|
||||
from plane.db.models import (
|
||||
@@ -207,7 +204,7 @@ def create_mention_notification(
|
||||
)
|
||||
|
||||
|
||||
@shared_task(queue=settings.TASK_NOTIFICATION_QUEUE)
|
||||
@shared_task
|
||||
def notifications(
|
||||
type,
|
||||
issue_id,
|
||||
|
||||
@@ -3,14 +3,13 @@ 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
|
||||
|
||||
|
||||
@@ -34,7 +33,7 @@ def extract_components(value, tag):
|
||||
return []
|
||||
|
||||
|
||||
@shared_task(queue=settings.TASK_LOW_QUEUE)
|
||||
@shared_task
|
||||
def page_transaction(new_value, old_value, page_id):
|
||||
try:
|
||||
page = Page.objects.get(pk=page_id)
|
||||
|
||||
@@ -4,15 +4,12 @@ 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(queue=settings.TASK_DEFAULT_QUEUE)
|
||||
@shared_task
|
||||
def page_version(page_id, existing_instance, user_id):
|
||||
try:
|
||||
# Get the page
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
# 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
|
||||
@@ -19,7 +17,7 @@ from plane.db.models import ProjectMember
|
||||
from plane.db.models import User
|
||||
|
||||
|
||||
@shared_task(queue=settings.TASK_DEFAULT_QUEUE)
|
||||
@shared_task
|
||||
def project_add_user_email(current_site, project_member_id, invitor_id):
|
||||
try:
|
||||
# Get the invitor
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
# 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
|
||||
@@ -17,7 +16,7 @@ from plane.license.utils.instance_value import get_email_configuration
|
||||
from plane.utils.exception_logger import log_exception
|
||||
|
||||
|
||||
@shared_task(queue=settings.TASK_DEFAULT_QUEUE)
|
||||
@shared_task
|
||||
def project_invitation(email, project_id, token, current_site, invitor):
|
||||
try:
|
||||
user = User.objects.get(email=invitor)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
# Python imports
|
||||
from django.utils import timezone
|
||||
from django.conf import settings
|
||||
|
||||
# Third party imports
|
||||
from celery import shared_task
|
||||
@@ -8,18 +7,11 @@ 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(queue=settings.TASK_LOW_QUEUE)
|
||||
@shared_task
|
||||
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,
|
||||
@@ -58,10 +50,6 @@ 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,6 +1,3 @@
|
||||
# Django imports
|
||||
from django.conf import settings
|
||||
|
||||
# Third party imports
|
||||
from celery import shared_task
|
||||
|
||||
@@ -10,7 +7,7 @@ from plane.settings.storage import S3Storage
|
||||
from plane.utils.exception_logger import log_exception
|
||||
|
||||
|
||||
@shared_task(queue=settings.TASK_DEFAULT_QUEUE)
|
||||
@shared_task
|
||||
def get_asset_object_metadata(asset_id):
|
||||
try:
|
||||
# Get the asset
|
||||
|
||||
@@ -5,7 +5,6 @@ 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
|
||||
@@ -16,7 +15,7 @@ from plane.license.utils.instance_value import get_email_configuration
|
||||
from plane.utils.exception_logger import log_exception
|
||||
|
||||
|
||||
@shared_task(queue=settings.TASK_HIGH_QUEUE)
|
||||
@shared_task
|
||||
def user_activation_email(current_site, user_id):
|
||||
try:
|
||||
# Send email to user when account is activated
|
||||
|
||||
@@ -5,7 +5,6 @@ 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
|
||||
@@ -16,7 +15,7 @@ from plane.license.utils.instance_value import get_email_configuration
|
||||
from plane.utils.exception_logger import log_exception
|
||||
|
||||
|
||||
@shared_task(queue=settings.TASK_DEFAULT_QUEUE)
|
||||
@shared_task
|
||||
def user_deactivation_email(current_site, user_id):
|
||||
try:
|
||||
# Send email to user when account is deactivated
|
||||
|
||||
@@ -86,7 +86,6 @@ 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,7 +8,6 @@ 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
|
||||
@@ -16,7 +15,7 @@ from plane.license.utils.instance_value import get_email_configuration
|
||||
from plane.utils.exception_logger import log_exception
|
||||
|
||||
|
||||
@shared_task(queue=settings.TASK_DEFAULT_QUEUE)
|
||||
@shared_task
|
||||
def workspace_invitation(email, workspace_id, token, current_site, invitor):
|
||||
try:
|
||||
user = User.objects.get(email=invitor)
|
||||
|
||||
@@ -14,39 +14,6 @@ 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": {
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
# Django imports
|
||||
from django.conf import settings
|
||||
|
||||
# Third party imports
|
||||
from celery import shared_task
|
||||
from opentelemetry import trace
|
||||
@@ -22,7 +19,7 @@ from plane.db.models import (
|
||||
from plane.utils.telemetry import init_tracer, shutdown_tracer
|
||||
|
||||
|
||||
@shared_task(queue=settings.TASK_LOW_QUEUE)
|
||||
@shared_task
|
||||
def instance_traces():
|
||||
try:
|
||||
init_tracer()
|
||||
|
||||
@@ -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,20 +146,8 @@ 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
|
||||
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_URL = os.environ.get("REDIS_URL")
|
||||
REDIS_SSL = REDIS_URL and "rediss" in REDIS_URL
|
||||
|
||||
if REDIS_SSL:
|
||||
@@ -245,79 +233,25 @@ 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")
|
||||
RABBITMQ_HOST = os.environ.get("RABBITMQ_HOST", "localhost")
|
||||
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", "/")
|
||||
|
||||
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}"
|
||||
AMQP_URL = os.environ.get("AMQP_URL")
|
||||
|
||||
# Celery Configuration
|
||||
CELERY_BROKER_URL = AMQP_URL
|
||||
CELERY_RESULT_BACKEND = REDIS_URL
|
||||
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_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",
|
||||
@@ -363,10 +297,16 @@ 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"
|
||||
|
||||
|
||||
@@ -91,6 +91,7 @@ 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(
|
||||
@@ -117,13 +118,14 @@ def issue_on_results(issues, group_by, sub_group_by):
|
||||
default=None,
|
||||
output_field=JSONField(),
|
||||
),
|
||||
filter=Q(votes__isnull=False),
|
||||
filter=Q(votes__isnull=False,votes__deleted_at__isnull=True),
|
||||
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(
|
||||
@@ -150,7 +152,7 @@ def issue_on_results(issues, group_by, sub_group_by):
|
||||
default=None,
|
||||
output_field=JSONField(),
|
||||
),
|
||||
filter=Q(issue_reactions__isnull=False),
|
||||
filter=Q(issue_reactions__isnull=False, issue_reactions__deleted_at__isnull=True),
|
||||
distinct=True,
|
||||
),
|
||||
).values(*required_fields, "vote_items", "reaction_items")
|
||||
|
||||
@@ -86,7 +86,13 @@ class EntityAssetEndpoint(BaseAPIView):
|
||||
)
|
||||
|
||||
# Check if the file type is allowed
|
||||
allowed_types = ["image/jpeg", "image/png", "image/webp"]
|
||||
allowed_types = [
|
||||
"image/jpeg",
|
||||
"image/png",
|
||||
"image/webp",
|
||||
"image/jpg",
|
||||
"image/gif",
|
||||
]
|
||||
if type not in allowed_types:
|
||||
return Response(
|
||||
{
|
||||
|
||||
@@ -701,6 +701,7 @@ class IssueRetrievePublicEndpoint(BaseAPIView):
|
||||
Case(
|
||||
When(
|
||||
votes__isnull=False,
|
||||
votes__deleted_at__isnull=True,
|
||||
then=JSONObject(
|
||||
vote=F("votes__vote"),
|
||||
actor_details=JSONObject(
|
||||
@@ -732,7 +733,11 @@ class IssueRetrievePublicEndpoint(BaseAPIView):
|
||||
output_field=JSONField(),
|
||||
),
|
||||
filter=Case(
|
||||
When(votes__isnull=False, then=True),
|
||||
When(
|
||||
votes__isnull=False,
|
||||
votes__deleted_at__isnull=True,
|
||||
then=True,
|
||||
),
|
||||
default=False,
|
||||
output_field=JSONField(),
|
||||
),
|
||||
@@ -742,6 +747,7 @@ 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(
|
||||
@@ -775,7 +781,11 @@ class IssueRetrievePublicEndpoint(BaseAPIView):
|
||||
output_field=JSONField(),
|
||||
),
|
||||
filter=Case(
|
||||
When(issue_reactions__isnull=False, then=True),
|
||||
When(
|
||||
issue_reactions__isnull=False,
|
||||
issue_reactions__deleted_at__isnull=True,
|
||||
then=True,
|
||||
),
|
||||
default=False,
|
||||
output_field=JSONField(),
|
||||
),
|
||||
|
||||
@@ -39,3 +39,8 @@ export enum EServerGroupByToFilterOptions {
|
||||
"project_id" = "project",
|
||||
"created_by" = "created_by",
|
||||
}
|
||||
|
||||
export enum EIssueServiceType {
|
||||
ISSUES = "issues",
|
||||
EPICS = "epics",
|
||||
}
|
||||
|
||||
@@ -57,7 +57,6 @@
|
||||
"@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",
|
||||
@@ -66,7 +65,6 @@
|
||||
"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,6 +19,7 @@ const CollaborativeDocumentEditor = (props: ICollaborativeDocumentEditor) => {
|
||||
containerClassName,
|
||||
disabledExtensions,
|
||||
displayConfig = DEFAULT_DISPLAY_CONFIG,
|
||||
editable,
|
||||
editorClassName = "",
|
||||
embedHandler,
|
||||
fileHandler,
|
||||
@@ -44,8 +45,8 @@ const CollaborativeDocumentEditor = (props: ICollaborativeDocumentEditor) => {
|
||||
|
||||
// use document editor
|
||||
const { editor, hasServerConnectionFailed, hasServerSynced } = useCollaborativeEditor({
|
||||
onTransaction,
|
||||
disabledExtensions,
|
||||
editable,
|
||||
editorClassName,
|
||||
embedHandler,
|
||||
extensions,
|
||||
@@ -54,6 +55,7 @@ const CollaborativeDocumentEditor = (props: ICollaborativeDocumentEditor) => {
|
||||
handleEditorReady,
|
||||
id,
|
||||
mentionHandler,
|
||||
onTransaction,
|
||||
placeholder,
|
||||
realtimeConfig,
|
||||
serverHandler,
|
||||
|
||||
-81
@@ -1,81 +0,0 @@
|
||||
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,5 +1,4 @@
|
||||
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,6 +38,7 @@ 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";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common";
|
||||
// plane utils
|
||||
import { cn } from "@plane/utils";
|
||||
// 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,10 +1,11 @@
|
||||
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,8 +1,10 @@
|
||||
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 { cn, isValidHttpUrl } from "@/helpers/common";
|
||||
import { isValidHttpUrl } from "@/helpers/common";
|
||||
import { setLinkEditor, unsetLinkEditor } from "@/helpers/editor-commands";
|
||||
|
||||
type Props = {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
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,
|
||||
@@ -17,8 +19,6 @@ import {
|
||||
HeadingSixItem,
|
||||
EditorMenuItem,
|
||||
} from "@/components/menus";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common";
|
||||
// types
|
||||
import { TEditorCommands } from "@/types";
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { FC, useEffect, useState } from "react";
|
||||
import { BubbleMenu, BubbleMenuProps, Editor, isNodeSelection } from "@tiptap/react";
|
||||
// plane utils
|
||||
import { cn } from "@plane/utils";
|
||||
// components
|
||||
import {
|
||||
BoldItem,
|
||||
@@ -13,8 +15,6 @@ 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,3 +5,6 @@ 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";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common";
|
||||
// plane utils
|
||||
import { cn } from "@plane/utils";
|
||||
// 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";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common";
|
||||
// plane utils
|
||||
import { cn } from "@plane/utils";
|
||||
|
||||
// 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,7 +1,9 @@
|
||||
import { ChangeEvent, useCallback, useEffect, useMemo, useRef } from "react";
|
||||
import { ImageIcon } from "lucide-react";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common";
|
||||
// plane utils
|
||||
import { cn } from "@plane/utils";
|
||||
// constants
|
||||
import { ACCEPTED_FILE_EXTENSIONS } from "@/constants/config";
|
||||
// hooks
|
||||
import { useUploader, useDropZone, uploadFirstImageAndInsertRemaining } from "@/hooks/use-file-upload";
|
||||
// extensions
|
||||
@@ -127,7 +129,7 @@ export const CustomImageUploader = (props: CustomImageUploaderProps) => {
|
||||
return "Uploading...";
|
||||
}
|
||||
|
||||
if (draggedInside) {
|
||||
if (draggedInside && editor.isEditable) {
|
||||
return "Drop image here";
|
||||
}
|
||||
|
||||
@@ -137,14 +139,16 @@ 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 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",
|
||||
"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",
|
||||
{
|
||||
"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,
|
||||
"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,
|
||||
}
|
||||
)}
|
||||
onDrop={onDrop}
|
||||
@@ -164,7 +168,7 @@ export const CustomImageUploader = (props: CustomImageUploaderProps) => {
|
||||
ref={fileInputRef}
|
||||
hidden
|
||||
type="file"
|
||||
accept=".jpg,.jpeg,.png,.webp"
|
||||
accept={ACCEPTED_FILE_EXTENSIONS.join(",")}
|
||||
onChange={onFileChange}
|
||||
multiple
|
||||
/>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { ExternalLink, Maximize, Minus, Plus, X } from "lucide-react";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common";
|
||||
// plane utils
|
||||
import { cn } from "@plane/utils";
|
||||
|
||||
type Props = {
|
||||
image: {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from "react";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common";
|
||||
// plane utils
|
||||
import { cn } from "@plane/utils";
|
||||
// components
|
||||
import { ImageFullScreenAction } from "./full-screen";
|
||||
|
||||
|
||||
@@ -2,58 +2,67 @@ 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 (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 (
|
||||
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"));
|
||||
|
||||
if (imageFiles.length > 0) {
|
||||
const pos = view.state.selection.from;
|
||||
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;
|
||||
insertImagesSafely({ editor, files: imageFiles, initialPos: pos, event: "drop" });
|
||||
}
|
||||
return true;
|
||||
}
|
||||
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;
|
||||
},
|
||||
}
|
||||
return false;
|
||||
},
|
||||
}),
|
||||
];
|
||||
},
|
||||
});
|
||||
|
||||
},
|
||||
}),
|
||||
];
|
||||
},
|
||||
});
|
||||
export const insertImagesSafely = async ({
|
||||
editor,
|
||||
files,
|
||||
|
||||
@@ -47,10 +47,11 @@ 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 } = args;
|
||||
const { disabledExtensions, enableHistory, fileHandler, mentionConfig, placeholder, tabIndex, editable } = args;
|
||||
|
||||
return [
|
||||
StarterKit.configure({
|
||||
@@ -89,7 +90,7 @@ export const CoreEditorExtensions = (args: TArguments): Extensions => {
|
||||
...(enableHistory ? {} : { history: false }),
|
||||
}),
|
||||
CustomQuoteExtension,
|
||||
DropHandlerExtension(),
|
||||
DropHandlerExtension,
|
||||
CustomHorizontalRule.configure({
|
||||
HTMLAttributes: {
|
||||
class: "py-4 border-custom-border-400",
|
||||
@@ -137,6 +138,7 @@ export const CoreEditorExtensions = (args: TArguments): Extensions => {
|
||||
CustomCodeInlineExtension,
|
||||
Markdown.configure({
|
||||
html: true,
|
||||
transformCopiedText: true,
|
||||
transformPastedText: true,
|
||||
breaks: true,
|
||||
}),
|
||||
@@ -145,12 +147,14 @@ export const CoreEditorExtensions = (args: TArguments): Extensions => {
|
||||
TableCell,
|
||||
TableRow,
|
||||
CustomMention({
|
||||
mentionSuggestions: mentionConfig.mentionSuggestions,
|
||||
mentionSuggestions: editable ? mentionConfig.mentionSuggestions : undefined,
|
||||
mentionHighlights: mentionConfig.mentionHighlights,
|
||||
readonly: false,
|
||||
readonly: !editable,
|
||||
}),
|
||||
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";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common";
|
||||
// plane utils
|
||||
import { cn } from "@plane/utils";
|
||||
// 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";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common";
|
||||
// plane utils
|
||||
import { cn } from "@plane/utils";
|
||||
// types
|
||||
import { IMentionSuggestion } from "@/types";
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
TableRow,
|
||||
Table,
|
||||
CustomMention,
|
||||
HeadingListExtension,
|
||||
CustomReadOnlyImageExtension,
|
||||
CustomTextAlignExtension,
|
||||
CustomCalloutReadOnlyExtension,
|
||||
@@ -139,7 +138,6 @@ export const CoreReadOnlyEditorExtensions = (props: Props): Extensions => {
|
||||
}),
|
||||
CharacterCount,
|
||||
CustomColorExtension,
|
||||
HeadingListExtension,
|
||||
CustomTextAlignExtension,
|
||||
CustomCalloutReadOnlyExtension,
|
||||
...CoreReadOnlyEditorAdditionalExtensions({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common";
|
||||
// plane utils
|
||||
import { cn } from "@plane/utils";
|
||||
// types
|
||||
import { ISlashCommandItem } from "@/types";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { EditorState, Selection } from "@tiptap/pm/state";
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
// plane utils
|
||||
import { cn } from "@plane/utils";
|
||||
|
||||
interface EditorClassNames {
|
||||
noBorder?: boolean;
|
||||
@@ -18,10 +18,6 @@ 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,6 +15,7 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorProps) => {
|
||||
const {
|
||||
onTransaction,
|
||||
disabledExtensions,
|
||||
editable,
|
||||
editorClassName,
|
||||
editorProps = {},
|
||||
embedHandler,
|
||||
@@ -75,7 +76,7 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorProps) => {
|
||||
const editor = useEditor({
|
||||
disabledExtensions,
|
||||
id,
|
||||
onTransaction,
|
||||
editable,
|
||||
editorProps,
|
||||
editorClassName,
|
||||
enableHistory: false,
|
||||
@@ -97,9 +98,10 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorProps) => {
|
||||
}),
|
||||
],
|
||||
fileHandler,
|
||||
handleEditorReady,
|
||||
forwardedRef,
|
||||
handleEditorReady,
|
||||
mentionHandler,
|
||||
onTransaction,
|
||||
placeholder,
|
||||
provider,
|
||||
tabIndex,
|
||||
|
||||
@@ -27,6 +27,7 @@ import type {
|
||||
} from "@/types";
|
||||
|
||||
export interface CustomEditorProps {
|
||||
editable: boolean;
|
||||
editorClassName: string;
|
||||
editorProps?: EditorProps;
|
||||
enableHistory: boolean;
|
||||
@@ -55,6 +56,7 @@ export interface CustomEditorProps {
|
||||
export const useEditor = (props: CustomEditorProps) => {
|
||||
const {
|
||||
disabledExtensions,
|
||||
editable = true,
|
||||
editorClassName,
|
||||
editorProps = {},
|
||||
enableHistory,
|
||||
@@ -74,42 +76,46 @@ 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({
|
||||
autofocus,
|
||||
editorProps: {
|
||||
...CoreEditorProps({
|
||||
editorClassName,
|
||||
}),
|
||||
...editorProps,
|
||||
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),
|
||||
},
|
||||
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),
|
||||
});
|
||||
[editable]
|
||||
);
|
||||
|
||||
// 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) {
|
||||
if (e.dataTransfer.files.length === 0 || !editor.isEditable) {
|
||||
return;
|
||||
}
|
||||
const filesList = e.dataTransfer.files;
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
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,3 +1,6 @@
|
||||
// constants
|
||||
import { ACCEPTED_FILE_MIME_TYPES } from "@/constants/config";
|
||||
|
||||
type TArgs = {
|
||||
file: File;
|
||||
maxFileSize: number;
|
||||
@@ -11,9 +14,8 @@ export const isFileValid = (args: TArgs): boolean => {
|
||||
return false;
|
||||
}
|
||||
|
||||
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.");
|
||||
if (!ACCEPTED_FILE_MIME_TYPES.includes(file.type)) {
|
||||
alert("Invalid file type. Please select a JPEG, JPG, PNG, WEBP or GIF file.");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { EditorProps } from "@tiptap/pm/view";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common";
|
||||
// plane utils
|
||||
import { cn } from "@plane/utils";
|
||||
|
||||
export type TCoreEditorProps = {
|
||||
editorClassName: string;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { EditorProps } from "@tiptap/pm/view";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common";
|
||||
// plane utils
|
||||
import { cn } from "@plane/utils";
|
||||
// props
|
||||
import { TCoreEditorProps } from "@/props";
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ export type TServerHandler = {
|
||||
|
||||
type TCollaborativeEditorHookProps = {
|
||||
disabledExtensions: TExtensions[];
|
||||
editable?: boolean;
|
||||
editorClassName: string;
|
||||
editorProps?: EditorProps;
|
||||
extensions?: Extensions;
|
||||
|
||||
@@ -138,6 +138,7 @@ 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;
|
||||
|
||||
@@ -9,7 +9,6 @@ import "./styles/drag-drop.css";
|
||||
// editors
|
||||
export {
|
||||
CollaborativeDocumentEditorWithRef,
|
||||
CollaborativeDocumentReadOnlyEditorWithRef,
|
||||
DocumentReadOnlyEditorWithRef,
|
||||
LiteTextEditorWithRef,
|
||||
LiteTextReadOnlyEditorWithRef,
|
||||
|
||||
@@ -111,8 +111,12 @@ ul[data-type="taskList"] li > label input[type="checkbox"] {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
ul[data-type="taskList"] li > label input[type="checkbox"]:hover {
|
||||
background-color: rgba(var(--color-background-80)) !important;
|
||||
.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"][checked] {
|
||||
@@ -151,10 +155,6 @@ 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));
|
||||
}
|
||||
|
||||
@@ -60,4 +60,6 @@ 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",
|
||||
}
|
||||
|
||||
Vendored
+6
@@ -1,3 +1,4 @@
|
||||
import { EIssueServiceType } from "@plane/constants";
|
||||
import { TIssuePriorities } from "../issues";
|
||||
import { TIssueAttachment } from "./issue_attachment";
|
||||
import { TIssueLink } from "./issue_link";
|
||||
@@ -39,6 +40,7 @@ export type TBaseIssue = {
|
||||
updated_by: string;
|
||||
|
||||
is_draft: boolean;
|
||||
is_epic?: boolean;
|
||||
};
|
||||
|
||||
export type IssueRelation = {
|
||||
@@ -121,3 +123,7 @@ export type TIssueDetailWidget =
|
||||
| "relations"
|
||||
| "links"
|
||||
| "attachments";
|
||||
|
||||
export type TIssueServiceType =
|
||||
| EIssueServiceType.ISSUES
|
||||
| EIssueServiceType.EPICS;
|
||||
|
||||
+1
@@ -136,6 +136,7 @@ export type TProjectIssuesSearchParams = {
|
||||
issue_id?: string;
|
||||
workspace_search: boolean;
|
||||
target_date?: string;
|
||||
epic?: boolean;
|
||||
};
|
||||
|
||||
export interface ISearchIssueResponse {
|
||||
|
||||
@@ -13,4 +13,5 @@ export enum EModalWidth {
|
||||
XXXXL = "sm:max-w-4xl",
|
||||
VXL = "sm:max-w-5xl",
|
||||
VIXL = "sm:max-w-6xl",
|
||||
VIIXL = "sm:max-w-7xl",
|
||||
}
|
||||
|
||||
@@ -15,8 +15,10 @@
|
||||
"lint:errors": "eslint src --ext .ts,.tsx --quiet"
|
||||
},
|
||||
"dependencies": {
|
||||
"clsx": "^2.1.1",
|
||||
"isomorphic-dompurify": "^2.16.0",
|
||||
"react": "^18.3.1"
|
||||
"react": "^18.3.1",
|
||||
"tailwind-merge": "^2.5.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@plane/eslint-config": "*",
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export const cn = (...inputs: ClassValue[]) => twMerge(clsx(inputs));
|
||||
@@ -1,3 +1,4 @@
|
||||
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
|
||||
import { cn } from "@plane/editor";
|
||||
// plane utils
|
||||
import { cn } from "@plane/utils";
|
||||
// components
|
||||
import { IssueEmojiReactions, IssueVotes } from "@/components/issues/reactions";
|
||||
// hooks
|
||||
|
||||
@@ -4,10 +4,12 @@ import { MutableRefObject } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { useParams, useSearchParams } from "next/navigation";
|
||||
// plane
|
||||
import { cn } from "@plane/editor";
|
||||
// plane types
|
||||
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,10 +4,12 @@ import { useRef } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { useParams, useSearchParams } from "next/navigation";
|
||||
// types
|
||||
import { cn } from "@plane/editor";
|
||||
// plane types
|
||||
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,9 +2,10 @@
|
||||
|
||||
import { Fragment, MutableRefObject, forwardRef, useRef, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { cn } from "@plane/editor";
|
||||
// plane
|
||||
// plane types
|
||||
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,11 +2,12 @@
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
import { Layers, Link, Paperclip } from "lucide-react";
|
||||
// types
|
||||
import { cn } from "@plane/editor";
|
||||
// plane types
|
||||
import { IIssueDisplayProperties } from "@plane/types";
|
||||
// plane ui
|
||||
import { Tooltip } from "@plane/ui";
|
||||
// ui
|
||||
// plane utils
|
||||
import { cn } from "@plane/utils";
|
||||
// components
|
||||
import {
|
||||
IssueBlockDate,
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
// ui
|
||||
import { cn } from "@plane/editor";
|
||||
// plane ui
|
||||
import { ContrastIcon, Tooltip } from "@plane/ui";
|
||||
// plane utils
|
||||
import { cn } from "@plane/utils";
|
||||
//hooks
|
||||
import { useCycle } from "@/hooks/store/use-cycle";
|
||||
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
import { observer } from "mobx-react";
|
||||
// icons
|
||||
import { LucideIcon, Users } from "lucide-react";
|
||||
// ui
|
||||
import { cn } from "@plane/editor";
|
||||
// plane ui
|
||||
import { Avatar, AvatarGroup } from "@plane/ui";
|
||||
// plane utils
|
||||
import { cn } from "@plane/utils";
|
||||
// hooks
|
||||
import { useMember } from "@/hooks/store/use-member";
|
||||
//
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
// planes
|
||||
import { cn } from "@plane/editor";
|
||||
// plane ui
|
||||
import { DiceIcon, Tooltip } from "@plane/ui";
|
||||
// plane utils
|
||||
import { cn } from "@plane/utils";
|
||||
// hooks
|
||||
import { useModule } from "@/hooks/store/use-module";
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
// ui
|
||||
import { cn } from "@plane/editor";
|
||||
// plane ui
|
||||
import { StateGroupIcon, Tooltip } from "@plane/ui";
|
||||
// plane utils
|
||||
import { cn } from "@plane/utils";
|
||||
//hooks
|
||||
import { useStates } from "@/hooks/store";
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
"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";
|
||||
// layouts
|
||||
import { ProjectAuthWrapper } from "@/layouts/auth-layout";
|
||||
// plane web layouts
|
||||
import { ProjectAuthWrapper } from "@/plane-web/layouts/project-wrapper";
|
||||
|
||||
const ProjectDetailLayout = ({ children }: { children: ReactNode }) => (
|
||||
<ProjectAuthWrapper>{children}</ProjectAuthWrapper>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./modal";
|
||||
@@ -0,0 +1,19 @@
|
||||
"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) => <></>;
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./epic-modal";
|
||||
@@ -1 +1,9 @@
|
||||
export const TimelineDependencyPaths = () => <></>;
|
||||
import { FC } from "react";
|
||||
|
||||
type Props = {
|
||||
isEpic?: boolean;
|
||||
};
|
||||
export const TimelineDependencyPaths: FC<Props> = (props) => {
|
||||
const { isEpic = false } = props;
|
||||
return <></>;
|
||||
};
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
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
|
||||
import { cn } from "@plane/editor";
|
||||
// plane utils
|
||||
import { cn } from "@plane/utils";
|
||||
|
||||
type Props = {
|
||||
workspaceSlug: string;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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 }
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
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>;
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
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
Reference in New Issue
Block a user