Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 733b725b5e | |||
| 3bee051ee9 | |||
| 6b902d458e | |||
| 51ca353577 | |||
| 881c744eb9 | |||
| ec41ae61b4 | |||
| 5773c2bde3 | |||
| e33bae2125 | |||
| 580c4b1930 | |||
| ddd4b51b4e | |||
| ede4aad55b | |||
| 1a715c98b2 | |||
| 8e6d885731 | |||
| 4507802aba | |||
| 438cc33046 | |||
| 442b0fd7e5 | |||
| 1119b9dc36 | |||
| 47a76f48b4 | |||
| a0f03d07f3 |
@@ -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).
|
||||
@@ -54,8 +54,6 @@ class PageSerializer(BaseSerializer):
|
||||
labels = validated_data.pop("labels", None)
|
||||
project_id = self.context["project_id"]
|
||||
owned_by_id = self.context["owned_by_id"]
|
||||
description = self.context["description"]
|
||||
description_binary = self.context["description_binary"]
|
||||
description_html = self.context["description_html"]
|
||||
|
||||
# Get the workspace id from the project
|
||||
@@ -64,8 +62,6 @@ class PageSerializer(BaseSerializer):
|
||||
# Create the page
|
||||
page = Page.objects.create(
|
||||
**validated_data,
|
||||
description=description,
|
||||
description_binary=description_binary,
|
||||
description_html=description_html,
|
||||
owned_by_id=owned_by_id,
|
||||
workspace_id=project.workspace_id,
|
||||
|
||||
@@ -8,7 +8,6 @@ from plane.app.views import (
|
||||
SubPagesEndpoint,
|
||||
PagesDescriptionViewSet,
|
||||
PageVersionEndpoint,
|
||||
PageDuplicateEndpoint,
|
||||
)
|
||||
|
||||
|
||||
@@ -79,9 +78,4 @@ urlpatterns = [
|
||||
PageVersionEndpoint.as_view(),
|
||||
name="page-versions",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/duplicate/",
|
||||
PageDuplicateEndpoint.as_view(),
|
||||
name="page-duplicate",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -155,7 +155,6 @@ from .page.base import (
|
||||
PageLogEndpoint,
|
||||
SubPagesEndpoint,
|
||||
PagesDescriptionViewSet,
|
||||
PageDuplicateEndpoint,
|
||||
)
|
||||
from .page.version import PageVersionEndpoint
|
||||
|
||||
|
||||
@@ -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(
|
||||
{
|
||||
|
||||
@@ -121,8 +121,6 @@ class PageViewSet(BaseViewSet):
|
||||
context={
|
||||
"project_id": project_id,
|
||||
"owned_by_id": request.user.id,
|
||||
"description": request.data.get("description", {}),
|
||||
"description_binary": request.data.get("description_binary", None),
|
||||
"description_html": request.data.get("description_html", "<p></p>"),
|
||||
},
|
||||
)
|
||||
@@ -555,33 +553,3 @@ class PagesDescriptionViewSet(BaseViewSet):
|
||||
return Response({"message": "Updated successfully"})
|
||||
else:
|
||||
return Response({"error": "No binary data provided"})
|
||||
|
||||
|
||||
class PageDuplicateEndpoint(BaseAPIView):
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
def post(self, request, slug, project_id, page_id):
|
||||
page = Page.objects.filter(
|
||||
pk=page_id, workspace__slug=slug, projects__id=project_id
|
||||
).values()
|
||||
new_page_data = list(page)[0]
|
||||
new_page_data.name = f"{new_page_data.name} (Copy)"
|
||||
|
||||
serializer = PageSerializer(
|
||||
data=new_page_data,
|
||||
context={
|
||||
"project_id": project_id,
|
||||
"owned_by_id": request.user.id,
|
||||
"description": new_page_data.description,
|
||||
"description_binary": new_page_data.description_binary,
|
||||
"description_html": new_page_data.description_html,
|
||||
},
|
||||
)
|
||||
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
# capture the page transaction
|
||||
page_transaction.delay(request.data, None, serializer.data["id"])
|
||||
page = Page.objects.get(pk=serializer.data["id"])
|
||||
serializer = PageDetailSerializer(page)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
|
||||
@@ -62,7 +62,7 @@ mkdir plane-selfhost
|
||||
|
||||
cd plane-selfhost
|
||||
|
||||
curl -fsSL -o setup.sh https://raw.githubusercontent.com/makeplane/plane/master/deploy/selfhost/install.sh
|
||||
curl -fsSL -o setup.sh https://github.com/makeplane/plane/releases/latest/download/setup.sh
|
||||
|
||||
chmod +x setup.sh
|
||||
```
|
||||
|
||||
@@ -1,54 +1,63 @@
|
||||
x-app-env: &app-env
|
||||
environment:
|
||||
- NGINX_PORT=${NGINX_PORT:-80}
|
||||
- WEB_URL=${WEB_URL:-http://localhost}
|
||||
- DEBUG=${DEBUG:-0}
|
||||
- SENTRY_DSN=${SENTRY_DSN:-""}
|
||||
- SENTRY_ENVIRONMENT=${SENTRY_ENVIRONMENT:-"production"}
|
||||
- CORS_ALLOWED_ORIGINS=${CORS_ALLOWED_ORIGINS:-}
|
||||
# Gunicorn Workers
|
||||
- GUNICORN_WORKERS=${GUNICORN_WORKERS:-1}
|
||||
#DB SETTINGS
|
||||
- PGHOST=${PGHOST:-plane-db}
|
||||
- PGDATABASE=${PGDATABASE:-plane}
|
||||
- POSTGRES_USER=${POSTGRES_USER:-plane}
|
||||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-plane}
|
||||
- POSTGRES_DB=${POSTGRES_DB:-plane}
|
||||
- POSTGRES_PORT=${POSTGRES_PORT:-5432}
|
||||
- PGDATA=${PGDATA:-/var/lib/postgresql/data}
|
||||
- DATABASE_URL=${DATABASE_URL:-postgresql://plane:plane@plane-db/plane}
|
||||
# REDIS SETTINGS
|
||||
- REDIS_HOST=${REDIS_HOST:-plane-redis}
|
||||
- REDIS_PORT=${REDIS_PORT:-6379}
|
||||
- REDIS_URL=${REDIS_URL:-redis://plane-redis:6379/}
|
||||
x-db-env: &db-env
|
||||
PGHOST: ${PGHOST:-plane-db}
|
||||
PGDATABASE: ${PGDATABASE:-plane}
|
||||
POSTGRES_USER: ${POSTGRES_USER:-plane}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-plane}
|
||||
POSTGRES_DB: ${POSTGRES_DB:-plane}
|
||||
POSTGRES_PORT: ${POSTGRES_PORT:-5432}
|
||||
PGDATA: ${PGDATA:-/var/lib/postgresql/data}
|
||||
|
||||
x-redis-env: &redis-env
|
||||
REDIS_HOST: ${REDIS_HOST:-plane-redis}
|
||||
REDIS_PORT: ${REDIS_PORT:-6379}
|
||||
REDIS_URL: ${REDIS_URL:-redis://plane-redis:6379/}
|
||||
|
||||
x-minio-env: &minio-env
|
||||
MINIO_ROOT_USER: ${AWS_ACCESS_KEY_ID:-access-key}
|
||||
MINIO_ROOT_PASSWORD: ${AWS_SECRET_ACCESS_KEY:-secret-key}
|
||||
|
||||
x-aws-s3-env: &aws-s3-env
|
||||
AWS_REGION: ${AWS_REGION:-}
|
||||
AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID:-access-key}
|
||||
AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY:-secret-key}
|
||||
AWS_S3_ENDPOINT_URL: ${AWS_S3_ENDPOINT_URL:-http://plane-minio:9000}
|
||||
AWS_S3_BUCKET_NAME: ${AWS_S3_BUCKET_NAME:-uploads}
|
||||
|
||||
x-proxy-env: &proxy-env
|
||||
NGINX_PORT: ${NGINX_PORT:-80}
|
||||
BUCKET_NAME: ${AWS_S3_BUCKET_NAME:-uploads}
|
||||
FILE_SIZE_LIMIT: ${FILE_SIZE_LIMIT:-5242880}
|
||||
|
||||
x-mq-env: &mq-env
|
||||
# RabbitMQ Settings
|
||||
RABBITMQ_HOST: ${RABBITMQ_HOST:-plane-mq}
|
||||
RABBITMQ_PORT: ${RABBITMQ_PORT:-5672}
|
||||
RABBITMQ_DEFAULT_USER: ${RABBITMQ_USER:-plane}
|
||||
RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASSWORD:-plane}
|
||||
RABBITMQ_DEFAULT_VHOST: ${RABBITMQ_VHOST:-plane}
|
||||
RABBITMQ_VHOST: ${RABBITMQ_VHOST:-plane}
|
||||
|
||||
x-live-env: &live-env
|
||||
API_BASE_URL: ${API_BASE_URL:-http://api:8000}
|
||||
|
||||
x-app-env: &app-env
|
||||
WEB_URL: ${WEB_URL:-http://localhost}
|
||||
DEBUG: ${DEBUG:-0}
|
||||
SENTRY_DSN: ${SENTRY_DSN}
|
||||
SENTRY_ENVIRONMENT: ${SENTRY_ENVIRONMENT:-production}
|
||||
CORS_ALLOWED_ORIGINS: ${CORS_ALLOWED_ORIGINS}
|
||||
GUNICORN_WORKERS: 1
|
||||
USE_MINIO: ${USE_MINIO:-1}
|
||||
DATABASE_URL: ${DATABASE_URL:-postgresql://plane:plane@plane-db/plane}
|
||||
SECRET_KEY: ${SECRET_KEY:-60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5}
|
||||
ADMIN_BASE_URL: ${ADMIN_BASE_URL}
|
||||
SPACE_BASE_URL: ${SPACE_BASE_URL}
|
||||
APP_BASE_URL: ${APP_BASE_URL}
|
||||
AMQP_URL: ${AMQP_URL:-amqp://plane:plane@plane-mq:5672/plane}
|
||||
|
||||
# RabbitMQ Settings
|
||||
- RABBITMQ_HOST=${RABBITMQ_HOST:-plane-mq}
|
||||
- RABBITMQ_PORT=${RABBITMQ_PORT:-5672}
|
||||
- RABBITMQ_DEFAULT_USER=${RABBITMQ_USER:-plane}
|
||||
- RABBITMQ_DEFAULT_PASS=${RABBITMQ_PASSWORD:-plane}
|
||||
- RABBITMQ_DEFAULT_VHOST=${RABBITMQ_VHOST:-plane}
|
||||
- RABBITMQ_VHOST=${RABBITMQ_VHOST:-plane}
|
||||
- AMQP_URL=${AMQP_URL:-amqp://plane:plane@plane-mq:5672/plane}
|
||||
# Application secret
|
||||
- SECRET_KEY=${SECRET_KEY:-60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5}
|
||||
# DATA STORE SETTINGS
|
||||
- USE_MINIO=${USE_MINIO:-1}
|
||||
- AWS_REGION=${AWS_REGION:-}
|
||||
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID:-"access-key"}
|
||||
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY:-"secret-key"}
|
||||
- AWS_S3_ENDPOINT_URL=${AWS_S3_ENDPOINT_URL:-http://plane-minio:9000}
|
||||
- AWS_S3_BUCKET_NAME=${AWS_S3_BUCKET_NAME:-uploads}
|
||||
- MINIO_ROOT_USER=${MINIO_ROOT_USER:-"access-key"}
|
||||
- MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD:-"secret-key"}
|
||||
- BUCKET_NAME=${BUCKET_NAME:-uploads}
|
||||
- FILE_SIZE_LIMIT=${FILE_SIZE_LIMIT:-5242880}
|
||||
# Live server env
|
||||
- API_BASE_URL=${API_BASE_URL:-http://api:8000}
|
||||
|
||||
services:
|
||||
web:
|
||||
<<: *app-env
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-frontend:${APP_RELEASE:-stable}
|
||||
platform: ${DOCKER_PLATFORM:-}
|
||||
pull_policy: if_not_present
|
||||
@@ -61,7 +70,6 @@ services:
|
||||
- worker
|
||||
|
||||
space:
|
||||
<<: *app-env
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-space:${APP_RELEASE:-stable}
|
||||
platform: ${DOCKER_PLATFORM:-}
|
||||
pull_policy: if_not_present
|
||||
@@ -75,7 +83,6 @@ services:
|
||||
- web
|
||||
|
||||
admin:
|
||||
<<: *app-env
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-admin:${APP_RELEASE:-stable}
|
||||
platform: ${DOCKER_PLATFORM:-}
|
||||
pull_policy: if_not_present
|
||||
@@ -88,12 +95,13 @@ services:
|
||||
- web
|
||||
|
||||
live:
|
||||
<<: *app-env
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-live:${APP_RELEASE:-stable}
|
||||
platform: ${DOCKER_PLATFORM:-}
|
||||
pull_policy: if_not_present
|
||||
restart: unless-stopped
|
||||
command: node live/dist/server.js live
|
||||
environment:
|
||||
<<: [ *live-env ]
|
||||
deploy:
|
||||
replicas: ${LIVE_REPLICAS:-1}
|
||||
depends_on:
|
||||
@@ -101,7 +109,6 @@ services:
|
||||
- web
|
||||
|
||||
api:
|
||||
<<: *app-env
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-stable}
|
||||
platform: ${DOCKER_PLATFORM:-}
|
||||
pull_policy: if_not_present
|
||||
@@ -111,14 +118,14 @@ services:
|
||||
replicas: ${API_REPLICAS:-1}
|
||||
volumes:
|
||||
- logs_api:/code/plane/logs
|
||||
environment:
|
||||
<<: [ *app-env, *db-env, *redis-env, *minio-env, *aws-s3-env, *proxy-env ]
|
||||
depends_on:
|
||||
- plane-db
|
||||
- plane-redis
|
||||
- plane-mq
|
||||
|
||||
|
||||
worker:
|
||||
<<: *app-env
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-stable}
|
||||
platform: ${DOCKER_PLATFORM:-}
|
||||
pull_policy: if_not_present
|
||||
@@ -126,6 +133,8 @@ services:
|
||||
command: ./bin/docker-entrypoint-worker.sh
|
||||
volumes:
|
||||
- logs_worker:/code/plane/logs
|
||||
environment:
|
||||
<<: [ *app-env, *db-env, *redis-env, *minio-env, *aws-s3-env, *proxy-env ]
|
||||
depends_on:
|
||||
- api
|
||||
- plane-db
|
||||
@@ -133,7 +142,6 @@ services:
|
||||
- plane-mq
|
||||
|
||||
beat-worker:
|
||||
<<: *app-env
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-stable}
|
||||
platform: ${DOCKER_PLATFORM:-}
|
||||
pull_policy: if_not_present
|
||||
@@ -141,6 +149,8 @@ services:
|
||||
command: ./bin/docker-entrypoint-beat.sh
|
||||
volumes:
|
||||
- logs_beat-worker:/code/plane/logs
|
||||
environment:
|
||||
<<: [ *app-env, *db-env, *redis-env, *minio-env, *aws-s3-env, *proxy-env ]
|
||||
depends_on:
|
||||
- api
|
||||
- plane-db
|
||||
@@ -148,7 +158,6 @@ services:
|
||||
- plane-mq
|
||||
|
||||
migrator:
|
||||
<<: *app-env
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-stable}
|
||||
platform: ${DOCKER_PLATFORM:-}
|
||||
pull_policy: if_not_present
|
||||
@@ -156,21 +165,23 @@ services:
|
||||
command: ./bin/docker-entrypoint-migrator.sh
|
||||
volumes:
|
||||
- logs_migrator:/code/plane/logs
|
||||
environment:
|
||||
<<: [ *app-env, *db-env, *redis-env, *minio-env, *aws-s3-env, *proxy-env ]
|
||||
depends_on:
|
||||
- plane-db
|
||||
- plane-redis
|
||||
|
||||
plane-db:
|
||||
<<: *app-env
|
||||
image: postgres:15.7-alpine
|
||||
pull_policy: if_not_present
|
||||
restart: unless-stopped
|
||||
command: postgres -c 'max_connections=1000'
|
||||
environment:
|
||||
<<: *db-env
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
|
||||
plane-redis:
|
||||
<<: *app-env
|
||||
image: valkey/valkey:7.2.5-alpine
|
||||
pull_policy: if_not_present
|
||||
restart: unless-stopped
|
||||
@@ -178,30 +189,33 @@ services:
|
||||
- redisdata:/data
|
||||
|
||||
plane-mq:
|
||||
<<: *app-env
|
||||
image: rabbitmq:3.13.6-management-alpine
|
||||
restart: always
|
||||
environment:
|
||||
<<: *mq-env
|
||||
volumes:
|
||||
- rabbitmq_data:/var/lib/rabbitmq
|
||||
|
||||
plane-minio:
|
||||
<<: *app-env
|
||||
image: minio/minio:latest
|
||||
pull_policy: if_not_present
|
||||
restart: unless-stopped
|
||||
command: server /export --console-address ":9090"
|
||||
environment:
|
||||
<<: *minio-env
|
||||
volumes:
|
||||
- uploads:/export
|
||||
|
||||
# Comment this if you already have a reverse proxy running
|
||||
proxy:
|
||||
<<: *app-env
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-proxy:${APP_RELEASE:-stable}
|
||||
platform: ${DOCKER_PLATFORM:-}
|
||||
pull_policy: if_not_present
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- ${NGINX_PORT}:80
|
||||
environment:
|
||||
<<: *proxy-env
|
||||
depends_on:
|
||||
- web
|
||||
- api
|
||||
|
||||
+97
-13
@@ -4,9 +4,12 @@ BRANCH=${BRANCH:-master}
|
||||
SCRIPT_DIR=$PWD
|
||||
SERVICE_FOLDER=plane-app
|
||||
PLANE_INSTALL_DIR=$PWD/$SERVICE_FOLDER
|
||||
export APP_RELEASE="stable"
|
||||
export APP_RELEASE=stable
|
||||
export DOCKERHUB_USER=makeplane
|
||||
export PULL_POLICY=${PULL_POLICY:-if_not_present}
|
||||
export GH_REPO=makeplane/plane
|
||||
export RELEASE_DOWNLOAD_URL="https://github.com/$GH_REPO/releases/download"
|
||||
export FALLBACK_DOWNLOAD_URL="https://raw.githubusercontent.com/$GH_REPO/$BRANCH/deploy/selfhost"
|
||||
|
||||
CPU_ARCH=$(uname -m)
|
||||
OS_NAME=$(uname)
|
||||
@@ -16,13 +19,6 @@ mkdir -p $PLANE_INSTALL_DIR/archive
|
||||
DOCKER_FILE_PATH=$PLANE_INSTALL_DIR/docker-compose.yaml
|
||||
DOCKER_ENV_PATH=$PLANE_INSTALL_DIR/plane.env
|
||||
|
||||
SED_PREFIX=()
|
||||
if [ "$OS_NAME" == "Darwin" ]; then
|
||||
SED_PREFIX=("-i" "")
|
||||
else
|
||||
SED_PREFIX=("-i")
|
||||
fi
|
||||
|
||||
function print_header() {
|
||||
clear
|
||||
|
||||
@@ -59,6 +55,17 @@ function spinner() {
|
||||
printf " \b\b\b\b" >&2
|
||||
}
|
||||
|
||||
function checkLatestRelease(){
|
||||
echo "Checking for the latest release..." >&2
|
||||
local latest_release=$(curl -s https://api.github.com/repos/$GH_REPO/releases/latest | grep -o '"tag_name": "[^"]*"' | sed 's/"tag_name": "//;s/"//g')
|
||||
if [ -z "$latest_release" ]; then
|
||||
echo "Failed to check for the latest release. Exiting..." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo $latest_release
|
||||
}
|
||||
|
||||
function initialize(){
|
||||
printf "Please wait while we check the availability of Docker images for the selected release ($APP_RELEASE) with ${UPPER_CPU_ARCH} support." >&2
|
||||
|
||||
@@ -130,8 +137,12 @@ function updateEnvFile() {
|
||||
echo "$key=$value" >> "$file"
|
||||
return
|
||||
else
|
||||
# if key exists, update the value
|
||||
sed "${SED_PREFIX[@]}" "s/^$key=.*/$key=$value/g" "$file"
|
||||
if [ "$OS_NAME" == "Darwin" ]; then
|
||||
value=$(echo "$value" | sed 's/|/\\|/g')
|
||||
sed -i '' "s|^$key=.*|$key=$value|g" "$file"
|
||||
else
|
||||
sed -i "s/^$key=.*/$key=$value/g" "$file"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo "File not found: $file"
|
||||
@@ -182,7 +193,7 @@ function buildYourOwnImage(){
|
||||
local PLANE_TEMP_CODE_DIR=~/tmp/plane
|
||||
rm -rf $PLANE_TEMP_CODE_DIR
|
||||
mkdir -p $PLANE_TEMP_CODE_DIR
|
||||
REPO=https://github.com/makeplane/plane.git
|
||||
REPO=https://github.com/$GH_REPO.git
|
||||
git clone "$REPO" "$PLANE_TEMP_CODE_DIR" --branch "$BRANCH" --single-branch --depth 1
|
||||
|
||||
cp "$PLANE_TEMP_CODE_DIR/deploy/selfhost/build.yml" "$PLANE_TEMP_CODE_DIR/build.yml"
|
||||
@@ -204,6 +215,10 @@ function install() {
|
||||
echo "Begin Installing Plane"
|
||||
echo ""
|
||||
|
||||
if [ "$APP_RELEASE" == "stable" ]; then
|
||||
export APP_RELEASE=$(checkLatestRelease)
|
||||
fi
|
||||
|
||||
local build_image=$(initialize)
|
||||
|
||||
if [ "$build_image" == "build" ]; then
|
||||
@@ -232,8 +247,49 @@ function download() {
|
||||
mv $PLANE_INSTALL_DIR/docker-compose.yaml $PLANE_INSTALL_DIR/archive/$TS.docker-compose.yaml
|
||||
fi
|
||||
|
||||
curl -H 'Cache-Control: no-cache, no-store' -s -o $PLANE_INSTALL_DIR/docker-compose.yaml https://raw.githubusercontent.com/makeplane/plane/$BRANCH/deploy/selfhost/docker-compose.yml?$(date +%s)
|
||||
curl -H 'Cache-Control: no-cache, no-store' -s -o $PLANE_INSTALL_DIR/variables-upgrade.env https://raw.githubusercontent.com/makeplane/plane/$BRANCH/deploy/selfhost/variables.env?$(date +%s)
|
||||
RESPONSE=$(curl -H 'Cache-Control: no-cache, no-store' -s -w "HTTPSTATUS:%{http_code}" "$RELEASE_DOWNLOAD_URL/$APP_RELEASE/docker-compose.yml?$(date +%s)")
|
||||
BODY=$(echo "$RESPONSE" | sed -e 's/HTTPSTATUS\:.*//g')
|
||||
STATUS=$(echo "$RESPONSE" | tr -d '\n' | sed -e 's/.*HTTPSTATUS://')
|
||||
|
||||
if [ "$STATUS" -eq 200 ]; then
|
||||
echo "$BODY" > $PLANE_INSTALL_DIR/docker-compose.yaml
|
||||
else
|
||||
# Fallback to download from the raw github url
|
||||
RESPONSE=$(curl -H 'Cache-Control: no-cache, no-store' -s -w "HTTPSTATUS:%{http_code}" "$FALLBACK_DOWNLOAD_URL/docker-compose.yml?$(date +%s)")
|
||||
BODY=$(echo "$RESPONSE" | sed -e 's/HTTPSTATUS\:.*//g')
|
||||
STATUS=$(echo "$RESPONSE" | tr -d '\n' | sed -e 's/.*HTTPSTATUS://')
|
||||
|
||||
if [ "$STATUS" -eq 200 ]; then
|
||||
echo "$BODY" > $PLANE_INSTALL_DIR/docker-compose.yaml
|
||||
else
|
||||
echo "Failed to download docker-compose.yml. HTTP Status: $STATUS"
|
||||
echo "URL: $RELEASE_DOWNLOAD_URL/$APP_RELEASE/docker-compose.yml"
|
||||
mv $PLANE_INSTALL_DIR/archive/$TS.docker-compose.yaml $PLANE_INSTALL_DIR/docker-compose.yaml
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
RESPONSE=$(curl -H 'Cache-Control: no-cache, no-store' -s -w "HTTPSTATUS:%{http_code}" "$RELEASE_DOWNLOAD_URL/$APP_RELEASE/variables.env?$(date +%s)")
|
||||
BODY=$(echo "$RESPONSE" | sed -e 's/HTTPSTATUS\:.*//g')
|
||||
STATUS=$(echo "$RESPONSE" | tr -d '\n' | sed -e 's/.*HTTPSTATUS://')
|
||||
|
||||
if [ "$STATUS" -eq 200 ]; then
|
||||
echo "$BODY" > $PLANE_INSTALL_DIR/variables-upgrade.env
|
||||
else
|
||||
# Fallback to download from the raw github url
|
||||
RESPONSE=$(curl -H 'Cache-Control: no-cache, no-store' -s -w "HTTPSTATUS:%{http_code}" "$FALLBACK_DOWNLOAD_URL/variables.env?$(date +%s)")
|
||||
BODY=$(echo "$RESPONSE" | sed -e 's/HTTPSTATUS\:.*//g')
|
||||
STATUS=$(echo "$RESPONSE" | tr -d '\n' | sed -e 's/.*HTTPSTATUS://')
|
||||
|
||||
if [ "$STATUS" -eq 200 ]; then
|
||||
echo "$BODY" > $PLANE_INSTALL_DIR/variables-upgrade.env
|
||||
else
|
||||
echo "Failed to download variables.env. HTTP Status: $STATUS"
|
||||
echo "URL: $RELEASE_DOWNLOAD_URL/$APP_RELEASE/variables.env"
|
||||
mv $PLANE_INSTALL_DIR/archive/$TS.docker-compose.yaml $PLANE_INSTALL_DIR/docker-compose.yaml
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -f "$DOCKER_ENV_PATH" ];
|
||||
then
|
||||
@@ -335,6 +391,34 @@ function restartServices() {
|
||||
startServices
|
||||
}
|
||||
function upgrade() {
|
||||
local latest_release=$(checkLatestRelease)
|
||||
|
||||
echo ""
|
||||
echo "Current release: $APP_RELEASE"
|
||||
|
||||
if [ "$latest_release" == "$APP_RELEASE" ]; then
|
||||
echo ""
|
||||
echo "You are already using the latest release"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Latest release: $latest_release"
|
||||
echo ""
|
||||
|
||||
# Check for confirmation to upgrade
|
||||
echo "Do you want to upgrade to the latest release ($latest_release)?"
|
||||
read -p "Continue? [y/N]: " confirm
|
||||
|
||||
if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
|
||||
echo "Exiting..."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
export APP_RELEASE=$latest_release
|
||||
|
||||
echo "Upgrading Plane to the latest release..."
|
||||
echo ""
|
||||
|
||||
echo "***** STOPPING SERVICES ****"
|
||||
stopServices
|
||||
|
||||
|
||||
@@ -47,9 +47,6 @@ AWS_ACCESS_KEY_ID=access-key
|
||||
AWS_SECRET_ACCESS_KEY=secret-key
|
||||
AWS_S3_ENDPOINT_URL=http://plane-minio:9000
|
||||
AWS_S3_BUCKET_NAME=uploads
|
||||
MINIO_ROOT_USER=access-key
|
||||
MINIO_ROOT_PASSWORD=secret-key
|
||||
BUCKET_NAME=uploads
|
||||
FILE_SIZE_LIMIT=5242880
|
||||
|
||||
# Gunicorn Workers
|
||||
|
||||
@@ -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]}`);
|
||||
|
||||
@@ -3,6 +3,4 @@ export const DocumentCollaborativeEvents = {
|
||||
unlock: { client: "unlocked", server: "unlock" },
|
||||
archive: { client: "archived", server: "archive" },
|
||||
unarchive: { client: "unarchived", server: "unarchive" },
|
||||
"make-public": { client: "made-public", server: "make-public" },
|
||||
"make-private": { client: "made-private", server: "make-private" },
|
||||
} as const;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -36,23 +36,19 @@ export const ContextMenuItem: React.FC<ContextMenuItemProps> = (props) => {
|
||||
onMouseEnter={handleActiveItem}
|
||||
disabled={item.disabled}
|
||||
>
|
||||
{item.customContent ?? (
|
||||
<>
|
||||
{item.icon && <item.icon className={cn("h-3 w-3", item.iconClassName)} />}
|
||||
<div>
|
||||
<h5>{item.title}</h5>
|
||||
{item.description && (
|
||||
<p
|
||||
className={cn("text-custom-text-300 whitespace-pre-line", {
|
||||
"text-custom-text-400": item.disabled,
|
||||
})}
|
||||
>
|
||||
{item.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{item.icon && <item.icon className={cn("h-3 w-3", item.iconClassName)} />}
|
||||
<div>
|
||||
<h5>{item.title}</h5>
|
||||
{item.description && (
|
||||
<p
|
||||
className={cn("text-custom-text-300 whitespace-pre-line", {
|
||||
"text-custom-text-400": item.disabled,
|
||||
})}
|
||||
>
|
||||
{item.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -11,8 +11,7 @@ import { usePlatformOS } from "../../hooks/use-platform-os";
|
||||
|
||||
export type TContextMenuItem = {
|
||||
key: string;
|
||||
customContent?: React.ReactNode;
|
||||
title?: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
icon?: React.FC<any>;
|
||||
action: () => void;
|
||||
|
||||
@@ -54,7 +54,7 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
|
||||
if (referenceElement) referenceElement.focus();
|
||||
};
|
||||
const closeDropdown = () => {
|
||||
if (isOpen) onMenuClose?.();
|
||||
isOpen && onMenuClose && onMenuClose();
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
@@ -216,7 +216,7 @@ const MenuItem: React.FC<ICustomMenuItemProps> = (props) => {
|
||||
)}
|
||||
onClick={(e) => {
|
||||
close();
|
||||
onClick?.(e);
|
||||
onClick && onClick(e);
|
||||
}}
|
||||
disabled={disabled}
|
||||
>
|
||||
|
||||
@@ -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,3 +1,2 @@
|
||||
export * from "./editor";
|
||||
export * from "./modals";
|
||||
export * from "./extra-actions";
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./move-page-modal";
|
||||
@@ -1,10 +0,0 @@
|
||||
// store types
|
||||
import { IPage } from "@/store/pages/page";
|
||||
|
||||
export type TMovePageModalProps = {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
page: IPage;
|
||||
};
|
||||
|
||||
export const MovePageModal: React.FC<TMovePageModalProps> = () => null;
|
||||
@@ -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>;
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
import { IProjectIssuesFilter, ProjectIssuesFilter } from "@/store/issue/project";
|
||||
import { IIssueRootStore } from "@/store/issue/root.store";
|
||||
|
||||
// @ts-nocheck - This class will never be used, extending similar class to avoid type errors
|
||||
export type IProjectEpicsFilter = IProjectIssuesFilter;
|
||||
|
||||
// @ts-nocheck - This class will never be used, extending similar class to avoid type errors
|
||||
export class ProjectEpicsFilter extends ProjectIssuesFilter implements IProjectEpicsFilter {
|
||||
constructor(_rootStore: IIssueRootStore) {
|
||||
super(_rootStore);
|
||||
|
||||
// root store
|
||||
this.rootIssueStore = _rootStore;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./filter.store";
|
||||
export * from "./issue.store";
|
||||
@@ -0,0 +1,14 @@
|
||||
import { IProjectIssues, ProjectIssues } from "@/store/issue/project";
|
||||
import { IIssueRootStore } from "@/store/issue/root.store";
|
||||
import { IProjectEpicsFilter } from "./filter.store";
|
||||
|
||||
// @ts-nocheck - This class will never be used, extending similar class to avoid type errors
|
||||
|
||||
export type IProjectEpics = IProjectIssues;
|
||||
|
||||
// @ts-nocheck - This class will never be used, extending similar class to avoid type errors
|
||||
export class ProjectEpics extends ProjectIssues implements IProjectEpics {
|
||||
constructor(_rootStore: IIssueRootStore, issueFilterStore: IProjectEpicsFilter) {
|
||||
super(_rootStore, issueFilterStore);
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,14 @@ import uniq from "lodash/uniq";
|
||||
import update from "lodash/update";
|
||||
import { action, makeObservable, observable, runInAction } from "mobx";
|
||||
import { computedFn } from "mobx-utils";
|
||||
import { TIssueActivityComment, TIssueActivity, TIssueActivityMap, TIssueActivityIdMap } from "@plane/types";
|
||||
import { EIssueServiceType } from "@plane/constants";
|
||||
import {
|
||||
TIssueActivityComment,
|
||||
TIssueActivity,
|
||||
TIssueActivityMap,
|
||||
TIssueActivityIdMap,
|
||||
TIssueServiceType,
|
||||
} from "@plane/types";
|
||||
// plane web constants
|
||||
import { EActivityFilterType } from "@/plane-web/constants/issues";
|
||||
// services
|
||||
@@ -29,7 +36,7 @@ export interface IIssueActivityStoreActions {
|
||||
|
||||
export interface IIssueActivityStore extends IIssueActivityStoreActions {
|
||||
// observables
|
||||
sortOrder: 'asc' | 'desc'
|
||||
sortOrder: "asc" | "desc";
|
||||
loader: TActivityLoader;
|
||||
activities: TIssueActivityIdMap;
|
||||
activityMap: TIssueActivityMap;
|
||||
@@ -37,20 +44,24 @@ export interface IIssueActivityStore extends IIssueActivityStoreActions {
|
||||
getActivitiesByIssueId: (issueId: string) => string[] | undefined;
|
||||
getActivityById: (activityId: string) => TIssueActivity | undefined;
|
||||
getActivityCommentByIssueId: (issueId: string) => TIssueActivityComment[] | undefined;
|
||||
toggleSortOrder: ()=>void;
|
||||
toggleSortOrder: () => void;
|
||||
}
|
||||
|
||||
export class IssueActivityStore implements IIssueActivityStore {
|
||||
// observables
|
||||
sortOrder: "asc" | "desc" = 'asc';
|
||||
sortOrder: "asc" | "desc" = "asc";
|
||||
loader: TActivityLoader = "fetch";
|
||||
activities: TIssueActivityIdMap = {};
|
||||
activityMap: TIssueActivityMap = {};
|
||||
|
||||
// services
|
||||
serviceType;
|
||||
issueActivityService;
|
||||
|
||||
constructor(protected store: CoreRootStore) {
|
||||
constructor(
|
||||
protected store: CoreRootStore,
|
||||
serviceType: TIssueServiceType = EIssueServiceType.ISSUES
|
||||
) {
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
sortOrder: observable.ref,
|
||||
@@ -59,10 +70,11 @@ export class IssueActivityStore implements IIssueActivityStore {
|
||||
activityMap: observable,
|
||||
// actions
|
||||
fetchActivities: action,
|
||||
toggleSortOrder: action
|
||||
toggleSortOrder: action,
|
||||
});
|
||||
this.serviceType = serviceType;
|
||||
// services
|
||||
this.issueActivityService = new IssueActivityService();
|
||||
this.issueActivityService = new IssueActivityService(this.serviceType);
|
||||
}
|
||||
|
||||
// helper methods
|
||||
@@ -81,8 +93,10 @@ export class IssueActivityStore implements IIssueActivityStore {
|
||||
|
||||
let activityComments: TIssueActivityComment[] = [];
|
||||
|
||||
const currentStore = this.serviceType === EIssueServiceType.EPICS ? this.store.epic : this.store.issue;
|
||||
|
||||
const activities = this.getActivitiesByIssueId(issueId) || [];
|
||||
const comments = this.store.issue.issueDetail.comment.getCommentsByIssueId(issueId) || [];
|
||||
const comments = currentStore.issueDetail.comment.getCommentsByIssueId(issueId) || [];
|
||||
|
||||
activities.forEach((activityId) => {
|
||||
const activity = this.getActivityById(activityId);
|
||||
@@ -95,7 +109,7 @@ export class IssueActivityStore implements IIssueActivityStore {
|
||||
});
|
||||
|
||||
comments.forEach((commentId) => {
|
||||
const comment = this.store.issue.issueDetail.comment.getCommentById(commentId);
|
||||
const comment = currentStore.issueDetail.comment.getCommentById(commentId);
|
||||
if (!comment) return;
|
||||
activityComments.push({
|
||||
id: comment.id,
|
||||
@@ -104,14 +118,14 @@ export class IssueActivityStore implements IIssueActivityStore {
|
||||
});
|
||||
});
|
||||
|
||||
activityComments = orderBy(activityComments, (e)=>new Date(e.created_at || 0), this.sortOrder);
|
||||
activityComments = orderBy(activityComments, (e) => new Date(e.created_at || 0), this.sortOrder);
|
||||
|
||||
return activityComments;
|
||||
});
|
||||
|
||||
toggleSortOrder = ()=>{
|
||||
this.sortOrder = this.sortOrder === 'asc' ? 'desc' : 'asc';
|
||||
}
|
||||
toggleSortOrder = () => {
|
||||
this.sortOrder = this.sortOrder === "asc" ? "desc" : "asc";
|
||||
};
|
||||
|
||||
// actions
|
||||
public async fetchActivities(
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export * from "./projects";
|
||||
export * from "./project-activity";
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
export interface TProjectActivity {
|
||||
id: string;
|
||||
content: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
userId: string;
|
||||
projectId: string;
|
||||
created_at: string;
|
||||
field: string;
|
||||
verb: string;
|
||||
actor_detail: {
|
||||
display_name: string;
|
||||
id: string;
|
||||
};
|
||||
workspace_detail: {
|
||||
slug: string;
|
||||
};
|
||||
project_detail: {
|
||||
name: string;
|
||||
};
|
||||
new_value: string;
|
||||
old_value: string;
|
||||
project: string;
|
||||
new_identifier?: string;
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import { LucideIcon } from "lucide-react";
|
||||
import { cn } from "@plane/editor";
|
||||
// plane ui
|
||||
import { Tooltip } from "@plane/ui";
|
||||
// plane utils
|
||||
import { cn } from "@plane/utils";
|
||||
|
||||
type Props = {
|
||||
onChange: (value: number) => void;
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
"use client";
|
||||
|
||||
import { FC, ReactNode } from "react";
|
||||
import { Network } from "lucide-react";
|
||||
// hooks
|
||||
import { Tooltip } from "@plane/ui";
|
||||
import { renderFormattedTime, renderFormattedDate, calculateTimeAgo } from "@/helpers/date-time.helper";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
import { TProjectActivity } from "@/plane-web/types";
|
||||
import { User } from "./user";
|
||||
|
||||
type TActivityBlockComponent = {
|
||||
icon?: ReactNode;
|
||||
activity: TProjectActivity;
|
||||
ends: "top" | "bottom" | undefined;
|
||||
children: ReactNode;
|
||||
customUserName?: string;
|
||||
};
|
||||
|
||||
export const ActivityBlockComponent: FC<TActivityBlockComponent> = (props) => {
|
||||
const { icon, activity, ends, children, customUserName } = props;
|
||||
// hooks
|
||||
const { isMobile } = usePlatformOS();
|
||||
|
||||
if (!activity) return <></>;
|
||||
return (
|
||||
<div
|
||||
className={`relative flex items-center gap-3 text-xs ${
|
||||
ends === "top" ? `pb-2` : ends === "bottom" ? `pt-2` : `py-2`
|
||||
}`}
|
||||
>
|
||||
<div className="absolute left-[13px] top-0 bottom-0 w-0.5 bg-custom-background-80" aria-hidden />
|
||||
<div className="flex-shrink-0 ring-6 w-7 h-7 rounded-full overflow-hidden flex justify-center items-center z-[4] bg-custom-background-80 text-custom-text-200">
|
||||
{icon ? icon : <Network className="w-3.5 h-3.5" />}
|
||||
</div>
|
||||
<div className="w-full truncate text-custom-text-200">
|
||||
<User activity={activity} customUserName={customUserName} /> {children}
|
||||
<div className="mt-1">
|
||||
<Tooltip
|
||||
isMobile={isMobile}
|
||||
tooltipContent={`${renderFormattedDate(activity.created_at)}, ${renderFormattedTime(activity.created_at)}`}
|
||||
>
|
||||
<span className="whitespace-nowrap text-custom-text-350 font-medium">
|
||||
{calculateTimeAgo(activity.created_at)}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
"use client";
|
||||
|
||||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
|
||||
import { TProjectActivity } from "@/plane-web/types";
|
||||
import { ActivityBlockComponent } from "./activity-block";
|
||||
import { iconsMap, messages } from "./helper";
|
||||
|
||||
type TActivityItem = {
|
||||
activity: TProjectActivity;
|
||||
showProject?: boolean;
|
||||
ends?: "top" | "bottom" | undefined;
|
||||
};
|
||||
|
||||
export const ActivityItem: FC<TActivityItem> = observer((props) => {
|
||||
const { activity, showProject = true, ends } = props;
|
||||
|
||||
if (!activity) return null;
|
||||
|
||||
const activityType = activity.field;
|
||||
const { message, customUserName } = messages(activity);
|
||||
const icon = iconsMap[activityType] || iconsMap.default;
|
||||
|
||||
return (
|
||||
<ActivityBlockComponent icon={icon} activity={activity} ends={ends} customUserName={customUserName}>
|
||||
<>{message}</>
|
||||
</ActivityBlockComponent>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,279 @@
|
||||
import { ReactNode } from "react";
|
||||
import {
|
||||
Signal,
|
||||
RotateCcw,
|
||||
Network,
|
||||
Link as LinkIcon,
|
||||
Calendar,
|
||||
Tag,
|
||||
Inbox,
|
||||
AlignLeft,
|
||||
Users,
|
||||
Paperclip,
|
||||
Type,
|
||||
Triangle,
|
||||
FileText,
|
||||
Globe,
|
||||
Hash,
|
||||
Clock,
|
||||
Bell,
|
||||
LayoutGrid,
|
||||
GitBranch,
|
||||
Timer,
|
||||
ListTodo,
|
||||
Layers,
|
||||
} from "lucide-react";
|
||||
|
||||
// components
|
||||
import { ArchiveIcon, DoubleCircleIcon, ContrastIcon, DiceIcon, Intake } from "@plane/ui";
|
||||
import { TProjectActivity } from "@/plane-web/types";
|
||||
|
||||
type ActivityIconMap = {
|
||||
[key: string]: ReactNode;
|
||||
};
|
||||
export const iconsMap: ActivityIconMap = {
|
||||
priority: <Signal size={14} className="text-custom-text-200" />,
|
||||
archived_at: <ArchiveIcon className="h-3.5 w-3.5 text-custom-text-200" />,
|
||||
restored: <RotateCcw className="h-3.5 w-3.5 text-custom-text-200" />,
|
||||
link: <LinkIcon className="h-3.5 w-3.5 text-custom-text-200" />,
|
||||
start_date: <Calendar className="h-3.5 w-3.5 text-custom-text-200" />,
|
||||
target_date: <Calendar className="h-3.5 w-3.5 text-custom-text-200" />,
|
||||
label: <Tag className="h-3.5 w-3.5 text-custom-text-200" />,
|
||||
inbox: <Inbox className="h-3.5 w-3.5 text-custom-text-200" />,
|
||||
description: <AlignLeft className="h-3.5 w-3.5 text-custom-text-200" />,
|
||||
assignee: <Users className="h-3.5 w-3.5 text-custom-text-200" />,
|
||||
attachment: <Paperclip className="h-3.5 w-3.5 text-custom-text-200" />,
|
||||
name: <Type className="h-3.5 w-3.5 text-custom-text-200" />,
|
||||
state: <DoubleCircleIcon className="h-4 w-4 flex-shrink-0 text-custom-text-200" />,
|
||||
estimate: <Triangle size={14} className="text-custom-text-200" />,
|
||||
cycle: <ContrastIcon className="h-4 w-4 flex-shrink-0 text-custom-text-200" />,
|
||||
module: <DiceIcon className="h-4 w-4 flex-shrink-0 text-custom-text-200" />,
|
||||
page: <FileText className="h-3.5 w-3.5 text-custom-text-200" />,
|
||||
network: <Globe className="h-3.5 w-3.5 text-custom-text-200" />,
|
||||
identifier: <Hash className="h-3.5 w-3.5 text-custom-text-200" />,
|
||||
timezone: <Clock className="h-3.5 w-3.5 text-custom-text-200" />,
|
||||
is_project_updates_enabled: <Bell className="h-3.5 w-3.5 text-custom-text-200" />,
|
||||
is_epic_enabled: <LayoutGrid className="h-3.5 w-3.5 text-custom-text-200" />,
|
||||
is_workflow_enabled: <GitBranch className="h-3.5 w-3.5 text-custom-text-200" />,
|
||||
is_time_tracking_enabled: <Timer className="h-3.5 w-3.5 text-custom-text-200" />,
|
||||
is_issue_type_enabled: <ListTodo className="h-3.5 w-3.5 text-custom-text-200" />,
|
||||
default: <Network className="h-3.5 w-3.5 text-custom-text-200" />,
|
||||
module_view: <DiceIcon className="h-3.5 w-3.5 text-custom-text-200" />,
|
||||
cycle_view: <ContrastIcon className="h-3.5 w-3.5 text-custom-text-200" />,
|
||||
issue_views_view: <Layers className="h-3.5 w-3.5 text-custom-text-200" />,
|
||||
page_view: <FileText className="h-3.5 w-3.5 text-custom-text-200" />,
|
||||
intake_view: <Intake className="h-3.5 w-3.5 text-custom-text-200" />,
|
||||
};
|
||||
|
||||
export const messages = (activity: TProjectActivity): { message: string | ReactNode; customUserName?: string } => {
|
||||
const activityType = activity.field;
|
||||
const newValue = activity.new_value;
|
||||
const oldValue = activity.old_value;
|
||||
const verb = activity.verb;
|
||||
|
||||
const getBooleanActionText = (value: string) => {
|
||||
if (value === "true") return "enabled";
|
||||
if (value === "false") return "disabled";
|
||||
return verb;
|
||||
};
|
||||
|
||||
switch (activityType) {
|
||||
case "priority":
|
||||
return {
|
||||
message: (
|
||||
<>
|
||||
set the priority to <span className="font-medium text-custom-text-100">{newValue || "none"}</span>
|
||||
</>
|
||||
),
|
||||
};
|
||||
case "archived_at":
|
||||
return {
|
||||
message: newValue === "restore" ? "restored the project" : "archived the project",
|
||||
customUserName: newValue === "archive" ? "Plane" : undefined,
|
||||
};
|
||||
case "name":
|
||||
return {
|
||||
message: (
|
||||
<>
|
||||
renamed the project to <span className="font-medium text-custom-text-100">{newValue}</span>
|
||||
</>
|
||||
),
|
||||
};
|
||||
case "description":
|
||||
return {
|
||||
message: newValue ? "updated the project description" : "removed the project description",
|
||||
};
|
||||
case "start_date":
|
||||
return {
|
||||
message: (
|
||||
<>
|
||||
{newValue ? (
|
||||
<>
|
||||
set the start date to <span className="font-medium text-custom-text-100">{newValue}</span>
|
||||
</>
|
||||
) : (
|
||||
"removed the start date"
|
||||
)}
|
||||
</>
|
||||
),
|
||||
};
|
||||
case "target_date":
|
||||
return {
|
||||
message: (
|
||||
<>
|
||||
{newValue ? (
|
||||
<>
|
||||
set the target date to <span className="font-medium text-custom-text-100">{newValue}</span>
|
||||
</>
|
||||
) : (
|
||||
"removed the target date"
|
||||
)}
|
||||
</>
|
||||
),
|
||||
};
|
||||
case "state":
|
||||
return {
|
||||
message: (
|
||||
<>
|
||||
set the state to <span className="font-medium text-custom-text-100">{newValue || "none"}</span>
|
||||
</>
|
||||
),
|
||||
};
|
||||
case "estimate":
|
||||
return {
|
||||
message: (
|
||||
<>
|
||||
{newValue ? (
|
||||
<>
|
||||
set the estimate point to <span className="font-medium text-custom-text-100">{newValue}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
removed the estimate point
|
||||
{oldValue && (
|
||||
<>
|
||||
{" "}
|
||||
<span className="font-medium text-custom-text-100">{oldValue}</span>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
};
|
||||
case "cycles":
|
||||
return {
|
||||
message: (
|
||||
<>
|
||||
<span>
|
||||
{verb} this project {verb === "removed" ? "from" : "to"} the cycle{" "}
|
||||
</span>
|
||||
{verb !== "removed" ? (
|
||||
<a
|
||||
href={`/${activity.workspace_detail?.slug}/projects/${activity.project}/cycles/${activity.new_identifier}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex font-medium text-custom-text-100"
|
||||
>
|
||||
{activity.new_value}
|
||||
</a>
|
||||
) : (
|
||||
<span className="font-medium text-custom-text-100">{activity.old_value || "Unknown cycle"}</span>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
};
|
||||
case "modules":
|
||||
return {
|
||||
message: (
|
||||
<>
|
||||
<span>
|
||||
{verb} this project {verb === "removed" ? "from" : "to"} the module{" "}
|
||||
</span>
|
||||
<span className="font-medium text-custom-text-100">
|
||||
{verb === "removed" ? oldValue : newValue || "Unknown module"}
|
||||
</span>
|
||||
</>
|
||||
),
|
||||
};
|
||||
case "labels":
|
||||
return {
|
||||
message: (
|
||||
<>
|
||||
{verb} the label{" "}
|
||||
<span className="font-medium text-custom-text-100">{newValue || oldValue || "Untitled label"}</span>
|
||||
</>
|
||||
),
|
||||
};
|
||||
case "inbox":
|
||||
return {
|
||||
message: <>{newValue ? "enabled" : "disabled"} inbox</>,
|
||||
};
|
||||
case "page":
|
||||
return {
|
||||
message: (
|
||||
<>
|
||||
{newValue ? "created" : "removed"} the project page{" "}
|
||||
<span className="font-medium text-custom-text-100">{newValue || oldValue || "Untitled page"}</span>
|
||||
</>
|
||||
),
|
||||
};
|
||||
case "network":
|
||||
return {
|
||||
message: <>{newValue ? "enabled" : "disabled"} network access</>,
|
||||
};
|
||||
case "identifier":
|
||||
return {
|
||||
message: (
|
||||
<>
|
||||
updated project identifier to <span className="font-medium text-custom-text-100">{newValue || "none"}</span>
|
||||
</>
|
||||
),
|
||||
};
|
||||
case "timezone":
|
||||
return {
|
||||
message: (
|
||||
<>
|
||||
changed project timezone to{" "}
|
||||
<span className="font-medium text-custom-text-100">{newValue || "default"}</span>
|
||||
</>
|
||||
),
|
||||
};
|
||||
case "module_view":
|
||||
case "cycle_view":
|
||||
case "issue_views_view":
|
||||
case "page_view":
|
||||
case "intake_view":
|
||||
return {
|
||||
message: (
|
||||
<>
|
||||
{getBooleanActionText(newValue)} {activityType.replace(/_view$/, "").replace(/_/g, " ")} view
|
||||
</>
|
||||
),
|
||||
};
|
||||
case "is_project_updates_enabled":
|
||||
return {
|
||||
message: <>{getBooleanActionText(newValue)} project updates</>,
|
||||
};
|
||||
case "is_epic_enabled":
|
||||
return {
|
||||
message: <>{getBooleanActionText(newValue)} epics</>,
|
||||
};
|
||||
case "is_workflow_enabled":
|
||||
return {
|
||||
message: <>{getBooleanActionText(newValue)} custom workflow</>,
|
||||
};
|
||||
case "is_time_tracking_enabled":
|
||||
return {
|
||||
message: <>{getBooleanActionText(newValue)} time tracking</>,
|
||||
};
|
||||
case "is_issue_type_enabled":
|
||||
return {
|
||||
message: <>{getBooleanActionText(newValue)} issue types</>,
|
||||
};
|
||||
default:
|
||||
return {
|
||||
message: `${verb} ${activityType.replace(/_/g, " ")} `,
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./activity-item";
|
||||
@@ -0,0 +1,27 @@
|
||||
import { FC } from "react";
|
||||
import Link from "next/link";
|
||||
import { TProjectActivity } from "@/plane-web/types";
|
||||
|
||||
type TUser = {
|
||||
activity: TProjectActivity;
|
||||
customUserName?: string;
|
||||
};
|
||||
|
||||
export const User: FC<TUser> = (props) => {
|
||||
const { activity, customUserName } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
{customUserName || activity.actor_detail?.display_name.includes("-intake") ? (
|
||||
<span className="text-custom-text-100 font-medium">{customUserName || "Plane"}</span>
|
||||
) : (
|
||||
<Link
|
||||
href={`/${activity?.workspace_detail?.slug}/profile/${activity?.actor_detail?.id}`}
|
||||
className="hover:underline text-custom-text-100 font-medium"
|
||||
>
|
||||
{activity.actor_detail?.display_name}
|
||||
</Link>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -5,3 +5,4 @@ export * from "./logo-spinner";
|
||||
export * from "./logo";
|
||||
export * from "./pro-icon";
|
||||
export * from "./count-chip";
|
||||
export * from "./activity";
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user