Compare commits

..

17 Commits

Author SHA1 Message Date
Satish Gandham 733b725b5e Extract utility functions out of Storage class 2024-12-24 16:17:00 +05:30
Satish Gandham 3bee051ee9 Fix typos and add comments 2024-12-24 14:32:53 +05:30
Satish Gandham 6b902d458e Add comments to stroage class 2024-12-18 16:00:17 +05:30
sriram veeraghanta 51ca353577 Merge branch 'preview' of github.com:makeplane/plane into preview 2024-12-18 14:58:13 +05:30
sriram veeraghanta 881c744eb9 fix: build errors 2024-12-18 14:57:59 +05:30
Bavisetti Narayan ec41ae61b4 chore: removed the deleted votes and reaction (#6218) 2024-12-18 14:54:03 +05:30
Aaryan Khandelwal 5773c2bde3 chore: gif support for editor (#6219) 2024-12-18 13:17:05 +05:30
M. Palanikannan e33bae2125 [PE-92] fix: removing readonly collaborative document editor (#6209)
* fix: removing readonly editor

* fix: sync state

* fix: indexeddb sync loader added

* fix: remove node error fixed

* style: page title and checkbox

* chore: removing the syncing logic

* revert: is editable check removed in display message

* fix: editable field optional

* fix: editable removed as optional prop

* fix: extra options import fix

---------

Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
2024-12-18 12:58:18 +05:30
Aaryan Khandelwal 580c4b1930 refactor: remove cn helper function from the editor package (#6217) 2024-12-18 12:22:14 +05:30
Vamsi Krishna ddd4b51b4e fix: labels empty state for drop down (#6216) 2024-12-17 19:14:10 +05:30
Satish Gandham ede4aad55b - Do not clear temp files that are locked. (#6214)
- Handle edge cases in sync workspace
2024-12-17 17:46:24 +05:30
Akshita Goyal 1a715c98b2 chore: added common component for project activity (#6212)
* chore: added common component for project activity

* fix: added enum

* fix: added enum for initiatives
2024-12-17 17:02:59 +05:30
Vamsi Krishna 8e6d885731 [WEB-2678]feat: added functionality to add labels directly from dropdown (#6211)
* enhancement:added functionality to add features directly from dropdown

* fix: fixed import order

* fix: fixed lint errors
2024-12-17 14:29:56 +05:30
Prateek Shourya 4507802aba refactor: enhance workspace and project wrapper modularity (#6207) 2024-12-16 19:01:37 +05:30
Anmol Singh Bhatia 438cc33046 code refactor and improvement (#6203)
* chore: package code refactoring

* chore: component restructuring and refactor

* chore: comment create improvement
2024-12-16 17:24:50 +05:30
Vamsi Krishna 442b0fd7e5 fix: added project sync after transfer issues (#6200) 2024-12-16 15:15:48 +05:30
Dancia 1119b9dc36 Updated README.md (#6182)
* Updated README.md

* minor fixes

* minor fixes
2024-12-16 14:33:08 +05:30
276 changed files with 2760 additions and 1589 deletions
+73 -50
View File
@@ -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 | [![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) |
| 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) |
`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 teams 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).
Thats it! Youre all set to begin coding. Remember to refresh your browser if changes dont 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
![Plane Repo Activity](https://repobeats.axiom.co/api/embed/2523c6ed2f77c082b7908c33e2ab208981d76c39.svg "Repobeats analytics image")
## 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)
## 📸 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. Wed 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
![Plane Repo Activity](https://repobeats.axiom.co/api/embed/2523c6ed2f77c082b7908c33e2ab208981d76c39.svg "Repobeats analytics image")
### 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 -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
+7 -1
View File
@@ -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(
{
+3 -11
View File
@@ -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(
+2 -2
View File
@@ -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,
+1 -2
View File
@@ -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()
+3 -4
View File
@@ -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(
+1 -2
View File
@@ -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:
(
+1 -4
View File
@@ -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)
+1 -4
View File
@@ -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 -13
View File
@@ -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
-1
View File
@@ -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)
-33
View File
@@ -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 -4
View File
@@ -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()
+15 -75
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,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"
+4 -2
View File
@@ -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")
+7 -1
View File
@@ -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(
{
+12 -2
View File
@@ -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(),
),
+5
View File
@@ -39,3 +39,8 @@ export enum EServerGroupByToFilterOptions {
"project_id" = "project",
"created_by" = "created_by",
}
export enum EIssueServiceType {
ISSUES = "issues",
EPICS = "epics",
}
-2
View File
@@ -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,
@@ -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";
+54 -45
View File
@@ -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";
+2 -6
View File
@@ -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,
+37 -31
View File
@@ -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;
}
+2 -2
View File
@@ -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;
+2 -2
View File
@@ -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;
+1
View File
@@ -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;
-1
View File
@@ -9,7 +9,6 @@ import "./styles/drag-drop.css";
// editors
export {
CollaborativeDocumentEditorWithRef,
CollaborativeDocumentReadOnlyEditorWithRef,
DocumentReadOnlyEditorWithRef,
LiteTextEditorWithRef,
LiteTextReadOnlyEditorWithRef,
+6 -6
View File
@@ -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));
}
+2
View File
@@ -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",
}
+6
View File
@@ -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
View File
@@ -136,6 +136,7 @@ export type TProjectIssuesSearchParams = {
issue_id?: string;
workspace_search: boolean;
target_date?: string;
epic?: boolean;
};
export interface ISearchIssueResponse {
+1
View File
@@ -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",
}
+3 -1
View File
@@ -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": "*",
+4
View File
@@ -0,0 +1,4 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export const cn = (...inputs: ClassValue[]) => twMerge(clsx(inputs));
+1
View File
@@ -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) => <></>;
+1
View File
@@ -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 }
+15
View File
@@ -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>;
});
+15
View File
@@ -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