Compare commits
297
Commits
@@ -62,14 +62,14 @@ jobs:
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [branch_build_setup]
|
||||
env:
|
||||
FRONTEND_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend-ce:${{ needs.branch_build_setup.outputs.gh_branch_name }}
|
||||
FRONTEND_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend:${{ needs.branch_build_setup.outputs.gh_branch_name }}
|
||||
steps:
|
||||
- name: Set Frontend Docker Tag
|
||||
run: |
|
||||
if [ "${{ needs.branch_build_setup.outputs.gh_branch_name }}" == "master" ] && [ "${{ github.event_name }}" == "release" ]; then
|
||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend-ce:latest,${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend-ce:${{ github.event.release.tag_name }}
|
||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend:latest,${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend:${{ github.event.release.tag_name }}
|
||||
elif [ "${{ needs.branch_build_setup.outputs.gh_branch_name }}" == "master" ]; then
|
||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend-ce:stable
|
||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend:stable
|
||||
else
|
||||
TAG=${{ env.FRONTEND_TAG }}
|
||||
fi
|
||||
@@ -104,14 +104,14 @@ jobs:
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [branch_build_setup]
|
||||
env:
|
||||
SPACE_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-space-ce:${{ needs.branch_build_setup.outputs.gh_branch_name }}
|
||||
SPACE_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-space:${{ needs.branch_build_setup.outputs.gh_branch_name }}
|
||||
steps:
|
||||
- name: Set Space Docker Tag
|
||||
run: |
|
||||
if [ "${{ needs.branch_build_setup.outputs.gh_branch_name }}" == "master" ] && [ "${{ github.event_name }}" == "release" ]; then
|
||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-space-ce:latest,${{ secrets.DOCKERHUB_USERNAME }}/plane-space-ce:${{ github.event.release.tag_name }}
|
||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-space:latest,${{ secrets.DOCKERHUB_USERNAME }}/plane-space:${{ github.event.release.tag_name }}
|
||||
elif [ "${{ needs.branch_build_setup.outputs.gh_branch_name }}" == "master" ]; then
|
||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-space-ce:stable
|
||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-space:stable
|
||||
else
|
||||
TAG=${{ env.SPACE_TAG }}
|
||||
fi
|
||||
@@ -146,14 +146,14 @@ jobs:
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [branch_build_setup]
|
||||
env:
|
||||
BACKEND_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-backend-ce:${{ needs.branch_build_setup.outputs.gh_branch_name }}
|
||||
BACKEND_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-backend:${{ needs.branch_build_setup.outputs.gh_branch_name }}
|
||||
steps:
|
||||
- name: Set Backend Docker Tag
|
||||
run: |
|
||||
if [ "${{ needs.branch_build_setup.outputs.gh_branch_name }}" == "master" ] && [ "${{ github.event_name }}" == "release" ]; then
|
||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-backend-ce:latest,${{ secrets.DOCKERHUB_USERNAME }}/plane-backend-ce:${{ github.event.release.tag_name }}
|
||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-backend:latest,${{ secrets.DOCKERHUB_USERNAME }}/plane-backend:${{ github.event.release.tag_name }}
|
||||
elif [ "${{ needs.branch_build_setup.outputs.gh_branch_name }}" == "master" ]; then
|
||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-backend-ce:stable
|
||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-backend:stable
|
||||
else
|
||||
TAG=${{ env.BACKEND_TAG }}
|
||||
fi
|
||||
@@ -188,14 +188,14 @@ jobs:
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [branch_build_setup]
|
||||
env:
|
||||
PROXY_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy-ce:${{ needs.branch_build_setup.outputs.gh_branch_name }}
|
||||
PROXY_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy:${{ needs.branch_build_setup.outputs.gh_branch_name }}
|
||||
steps:
|
||||
- name: Set Proxy Docker Tag
|
||||
run: |
|
||||
if [ "${{ needs.branch_build_setup.outputs.gh_branch_name }}" == "master" ] && [ "${{ github.event_name }}" == "release" ]; then
|
||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy-ce:latest,${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy-ce:${{ github.event.release.tag_name }}
|
||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy:latest,${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy:${{ github.event.release.tag_name }}
|
||||
elif [ "${{ needs.branch_build_setup.outputs.gh_branch_name }}" == "master" ]; then
|
||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy-ce:stable
|
||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy:stable
|
||||
else
|
||||
TAG=${{ env.PROXY_TAG }}
|
||||
fi
|
||||
|
||||
@@ -49,24 +49,10 @@ NGINX_PORT=80
|
||||
|
||||
|
||||
```
|
||||
# Enable/Disable OAUTH - default 0 for selfhosted instance
|
||||
NEXT_PUBLIC_ENABLE_OAUTH=0
|
||||
# Public boards deploy URL
|
||||
NEXT_PUBLIC_DEPLOY_URL="http://localhost/spaces"
|
||||
```
|
||||
|
||||
|
||||
|
||||
## {PROJECT_FOLDER}/spaces/.env.example
|
||||
|
||||
|
||||
|
||||
```
|
||||
# Flag to toggle OAuth
|
||||
NEXT_PUBLIC_ENABLE_OAUTH=0
|
||||
```
|
||||
|
||||
|
||||
|
||||
## {PROJECT_FOLDER}/apiserver/.env
|
||||
|
||||
|
||||
@@ -27,7 +27,8 @@ WORKDIR /code
|
||||
COPY requirements.txt ./requirements.txt
|
||||
ADD requirements ./requirements
|
||||
|
||||
RUN pip install -r requirements.txt --compile --no-cache-dir
|
||||
# Install the local development settings
|
||||
RUN pip install -r requirements/local.txt --compile --no-cache-dir
|
||||
|
||||
RUN addgroup -S plane && \
|
||||
adduser -S captain -G plane
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"name": "plane-api",
|
||||
"version": "0.13.2"
|
||||
}
|
||||
"name": "plane-api",
|
||||
"version": "0.14.0"
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ from plane.app.views import (
|
||||
UserProjectInvitationsViewset,
|
||||
ProjectPublicCoverImagesEndpoint,
|
||||
ProjectDeployBoardViewSet,
|
||||
UserProjectRolesEndpoint,
|
||||
)
|
||||
|
||||
|
||||
@@ -74,6 +75,11 @@ urlpatterns = [
|
||||
),
|
||||
name="user-project-invitations",
|
||||
),
|
||||
path(
|
||||
"users/me/workspaces/<str:slug>/project-roles/",
|
||||
UserProjectRolesEndpoint.as_view(),
|
||||
name="user-project-roles",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/join/<uuid:pk>/",
|
||||
ProjectJoinEndpoint.as_view(),
|
||||
|
||||
@@ -11,6 +11,7 @@ from .project import (
|
||||
ProjectFavoritesViewSet,
|
||||
ProjectPublicCoverImagesEndpoint,
|
||||
ProjectDeployBoardViewSet,
|
||||
UserProjectRolesEndpoint,
|
||||
)
|
||||
from .user import (
|
||||
UserEndpoint,
|
||||
|
||||
@@ -343,13 +343,6 @@ class MagicSignInEndpoint(BaseAPIView):
|
||||
|
||||
if str(token) == str(user_token):
|
||||
user = User.objects.get(email=email)
|
||||
if not user.is_active:
|
||||
return Response(
|
||||
{
|
||||
"error": "Your account has been deactivated. Please contact your site administrator."
|
||||
},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
# Send event
|
||||
auth_events.delay(
|
||||
user=user.id,
|
||||
|
||||
@@ -90,8 +90,8 @@ class ConfigurationEndpoint(BaseAPIView):
|
||||
|
||||
data = {}
|
||||
# Authentication
|
||||
data["google_client_id"] = GOOGLE_CLIENT_ID
|
||||
data["github_client_id"] = GITHUB_CLIENT_ID
|
||||
data["google_client_id"] = GOOGLE_CLIENT_ID if GOOGLE_CLIENT_ID and GOOGLE_CLIENT_ID != "\"\"" else None
|
||||
data["github_client_id"] = GITHUB_CLIENT_ID if GITHUB_CLIENT_ID and GITHUB_CLIENT_ID != "\"\"" else None
|
||||
data["github_app_name"] = GITHUB_APP_NAME
|
||||
data["magic_login"] = (
|
||||
bool(EMAIL_HOST_USER) and bool(EMAIL_HOST_PASSWORD)
|
||||
@@ -106,7 +106,7 @@ class ConfigurationEndpoint(BaseAPIView):
|
||||
data["posthog_host"] = POSTHOG_HOST
|
||||
|
||||
# Unsplash
|
||||
data["has_unsplash_configured"] = UNSPLASH_ACCESS_KEY
|
||||
data["has_unsplash_configured"] = bool(UNSPLASH_ACCESS_KEY)
|
||||
|
||||
# Open AI settings
|
||||
data["has_openai_configured"] = bool(OPENAI_API_KEY)
|
||||
|
||||
@@ -86,7 +86,14 @@ def get_access_token(request_token: str, client_id: str) -> str:
|
||||
if not request_token:
|
||||
raise ValueError("The request token has to be supplied!")
|
||||
|
||||
CLIENT_SECRET = os.environ.get("GITHUB_CLIENT_SECRET")
|
||||
(CLIENT_SECRET,) = get_configuration_value(
|
||||
[
|
||||
{
|
||||
"key": "GITHUB_CLIENT_SECRET",
|
||||
"default": os.environ.get("GITHUB_CLIENT_SECRET", None),
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
url = f"https://github.com/login/oauth/access_token?client_id={client_id}&client_secret={CLIENT_SECRET}&code={request_token}"
|
||||
headers = {"accept": "application/json"}
|
||||
@@ -160,12 +167,6 @@ class OauthEndpoint(BaseAPIView):
|
||||
]
|
||||
)
|
||||
|
||||
if not GOOGLE_CLIENT_ID or not GITHUB_CLIENT_ID:
|
||||
return Response(
|
||||
{"error": "Github or Google login is not configured"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
if not medium or not id_token:
|
||||
return Response(
|
||||
{
|
||||
@@ -175,9 +176,19 @@ class OauthEndpoint(BaseAPIView):
|
||||
)
|
||||
|
||||
if medium == "google":
|
||||
if not GOOGLE_CLIENT_ID:
|
||||
return Response(
|
||||
{"error": "Google login is not configured"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
data = validate_google_token(id_token, client_id)
|
||||
|
||||
if medium == "github":
|
||||
if not GITHUB_CLIENT_ID:
|
||||
return Response(
|
||||
{"error": "Github login is not configured"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
access_token = get_access_token(id_token, client_id)
|
||||
data = get_user_data(access_token)
|
||||
|
||||
@@ -299,7 +310,7 @@ class OauthEndpoint(BaseAPIView):
|
||||
return Response(data, status=status.HTTP_200_OK)
|
||||
|
||||
except User.DoesNotExist:
|
||||
ENABLE_SIGNUP, = get_configuration_value(
|
||||
(ENABLE_SIGNUP,) = get_configuration_value(
|
||||
[
|
||||
{
|
||||
"key": "ENABLE_SIGNUP",
|
||||
|
||||
@@ -39,8 +39,10 @@ from plane.app.serializers import (
|
||||
)
|
||||
|
||||
from plane.app.permissions import (
|
||||
WorkspaceUserPermission,
|
||||
ProjectBasePermission,
|
||||
ProjectMemberPermission,
|
||||
ProjectLitePermission,
|
||||
)
|
||||
|
||||
from plane.db.models import (
|
||||
@@ -165,7 +167,7 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
|
||||
workspace__slug=slug,
|
||||
is_active=True,
|
||||
).select_related("member"),
|
||||
to_attr='members_list'
|
||||
to_attr="members_list",
|
||||
)
|
||||
)
|
||||
.order_by("sort_order", "name")
|
||||
@@ -601,6 +603,18 @@ class ProjectMemberViewSet(BaseViewSet):
|
||||
ProjectMemberPermission,
|
||||
]
|
||||
|
||||
def get_permissions(self):
|
||||
if self.action == "leave":
|
||||
self.permission_classes = [
|
||||
ProjectLitePermission,
|
||||
]
|
||||
else:
|
||||
self.permission_classes = [
|
||||
ProjectMemberPermission,
|
||||
]
|
||||
|
||||
return super(ProjectMemberViewSet, self).get_permissions()
|
||||
|
||||
search_fields = [
|
||||
"member__display_name",
|
||||
"member__first_name",
|
||||
@@ -1049,3 +1063,20 @@ class ProjectDeployBoardViewSet(BaseViewSet):
|
||||
|
||||
serializer = ProjectDeployBoardSerializer(project_deploy_board)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class UserProjectRolesEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
WorkspaceUserPermission,
|
||||
]
|
||||
|
||||
def get(self, request, slug):
|
||||
project_members = ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
member_id=request.user.id,
|
||||
).values("project_id", "role")
|
||||
|
||||
project_members = {
|
||||
str(member["project_id"]): member["role"] for member in project_members
|
||||
}
|
||||
return Response(project_members, status=status.HTTP_200_OK)
|
||||
|
||||
@@ -49,6 +49,10 @@ class UserEndpoint(BaseViewSet):
|
||||
# Check all workspace user is active
|
||||
user = self.get_object()
|
||||
|
||||
# Instance admin check
|
||||
if InstanceAdmin.objects.filter(user=user).exists():
|
||||
return Response({"error": "You cannot deactivate your account since you are an instance admin"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
projects_to_deactivate = []
|
||||
workspaces_to_deactivate = []
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ from plane.license.api.serializers import (
|
||||
from plane.license.api.permissions import (
|
||||
InstanceAdminPermission,
|
||||
)
|
||||
from plane.db.models import User
|
||||
from plane.db.models import User, WorkspaceMember, ProjectMember
|
||||
from plane.license.utils.encryption import encrypt_data
|
||||
|
||||
|
||||
|
||||
@@ -9,14 +9,20 @@ def derive_key(secret_key):
|
||||
dk = hashlib.pbkdf2_hmac('sha256', secret_key.encode(), b'salt', 100000)
|
||||
return base64.urlsafe_b64encode(dk)
|
||||
|
||||
|
||||
# Encrypt data
|
||||
def encrypt_data(data):
|
||||
cipher_suite = Fernet(derive_key(settings.SECRET_KEY))
|
||||
encrypted_data = cipher_suite.encrypt(data.encode())
|
||||
return encrypted_data.decode() # Convert bytes to string
|
||||
|
||||
if data:
|
||||
cipher_suite = Fernet(derive_key(settings.SECRET_KEY))
|
||||
encrypted_data = cipher_suite.encrypt(data.encode())
|
||||
return encrypted_data.decode() # Convert bytes to string
|
||||
else:
|
||||
return ""
|
||||
|
||||
# Decrypt data
|
||||
def decrypt_data(encrypted_data):
|
||||
cipher_suite = Fernet(derive_key(settings.SECRET_KEY))
|
||||
decrypted_data = cipher_suite.decrypt(encrypted_data.encode()) # Convert string back to bytes
|
||||
return decrypted_data.decode()
|
||||
if encrypted_data:
|
||||
cipher_suite = Fernet(derive_key(settings.SECRET_KEY))
|
||||
decrypted_data = cipher_suite.decrypt(encrypted_data.encode()) # Convert string back to bytes
|
||||
return decrypted_data.decode()
|
||||
else:
|
||||
return ""
|
||||
@@ -19,8 +19,11 @@ urlpatterns = [
|
||||
|
||||
|
||||
if settings.DEBUG:
|
||||
import debug_toolbar
|
||||
try:
|
||||
import debug_toolbar
|
||||
|
||||
urlpatterns = [
|
||||
re_path(r"^__debug__/", include(debug_toolbar.urls)),
|
||||
] + urlpatterns
|
||||
urlpatterns = [
|
||||
re_path(r"^__debug__/", include(debug_toolbar.urls)),
|
||||
] + urlpatterns
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
## Coolify Setup
|
||||
## Coolify Setup
|
||||
|
||||
Access the `coolify-docker-compose` file [here](https://raw.githubusercontent.com/makeplane/plane/master/deploy/coolify/coolify-docker-compose.yml) or download using using below command
|
||||
|
||||
```
|
||||
curl -fsSL https://raw.githubusercontent.com/makeplane/plane/master/deploy/coolify/coolify-docker-compose.yml
|
||||
|
||||
```
|
||||
```
|
||||
|
||||
@@ -7,7 +7,6 @@ services:
|
||||
restart: always
|
||||
command: /usr/local/bin/start.sh web/server.js web
|
||||
environment:
|
||||
- NEXT_PUBLIC_ENABLE_OAUTH=${NEXT_PUBLIC_ENABLE_OAUTH:-0}
|
||||
- NEXT_PUBLIC_DEPLOY_URL=$SERVICE_FQDN_SPACE_8082
|
||||
depends_on:
|
||||
- api
|
||||
@@ -21,7 +20,6 @@ services:
|
||||
command: /usr/local/bin/start.sh space/server.js space
|
||||
environment:
|
||||
- SERVICE_FQDN_SPACE_8082=/api
|
||||
- NEXT_PUBLIC_ENABLE_OAUTH=${NEXT_PUBLIC_ENABLE_OAUTH:-0}
|
||||
depends_on:
|
||||
- api
|
||||
- worker
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
|
||||
# Helm Chart
|
||||
|
||||
Click on the below link to access the helm chart instructions.
|
||||
Click on the below link to access the helm chart instructions.
|
||||
|
||||
[](https://artifacthub.io/packages/search?repo=makeplane)
|
||||
|
||||
|
||||
|
||||
+43
-39
@@ -1,18 +1,20 @@
|
||||
# Self Hosting
|
||||
# Self Hosting
|
||||
|
||||
In this guide, we will walk you through the process of setting up a self-hosted environment. Self-hosting allows you to have full control over your applications and data. It's a great way to ensure privacy, control, and customization.
|
||||
In this guide, we will walk you through the process of setting up a self-hosted environment. Self-hosting allows you to have full control over your applications and data. It's a great way to ensure privacy, control, and customization.
|
||||
|
||||
We will cover two main options for setting up your self-hosted environment: using a cloud server or using your desktop. For the cloud server, we will use an AWS EC2 instance. For the desktop, we will use Docker to create a local environment.
|
||||
We will cover two main options for setting up your self-hosted environment: using a cloud server or using your desktop. For the cloud server, we will use an AWS EC2 instance. For the desktop, we will use Docker to create a local environment.
|
||||
|
||||
Let's get started!
|
||||
|
||||
## Setting up Docker Environment
|
||||
## Setting up Docker Environment
|
||||
|
||||
<details>
|
||||
<summary>Option 1 - Using Cloud Server</summary>
|
||||
<p>Best way to start is to create EC2 maching on AWS. It must of minimum t3.medium/t3a/medium</p>
|
||||
<p>Run the below command to install docker engine.</p>
|
||||
|
||||
```curl -fsSL https://get.docker.com -o install-docker.sh```
|
||||
`curl -fsSL https://get.docker.com -o install-docker.sh`
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
@@ -20,30 +22,34 @@ Let's get started!
|
||||
<details>
|
||||
<summary>Option 2 - Using Desktop</summary>
|
||||
|
||||
#### For Mac
|
||||
#### For Mac
|
||||
|
||||
<ol>
|
||||
<li> Download Docker Desktop for Mac from the <a href="https://hub.docker.com/editions/community/docker-ce-desktop-mac/" target="_blank">Docker Hub</a>. </li>
|
||||
<li> Double-click the downloaded `.dmg` file and drag the Docker app icon to the Applications folder. </li>
|
||||
<li>Open Docker Desktop from the Applications folder. You might be asked to provide your system password to install additional software.</li>
|
||||
</ol>
|
||||
|
||||
#### For Windows:
|
||||
#### For Windows:
|
||||
|
||||
<ol>
|
||||
<li>Download Docker Desktop for Windows from the <a href="https://hub.docker.com/editions/community/docker-ce-desktop-windows/" target="_blank">Docker Hub</a>.</li>
|
||||
<li>Run the installer and follow the instructions. You might be asked to enable Hyper-V and "Containers" Windows features.</li>
|
||||
<li>Open Docker Desktop. You might be asked to log out and log back in, or restart your machine, for changes to take effect.</li>
|
||||
</ol>
|
||||
|
||||
After installation, you can verify the installation by opening a terminal (Command Prompt on Windows, Terminal app on Mac) and running the command `docker --version`. This should display the installed version of Docker.
|
||||
After installation, you can verify the installation by opening a terminal (Command Prompt on Windows, Terminal app on Mac) and running the command `docker --version`. This should display the installed version of Docker.
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
## Installing Plane
|
||||
|
||||
Installing plane is a very easy and minimal step process.
|
||||
Installing plane is a very easy and minimal step process.
|
||||
|
||||
### Prerequisite
|
||||
|
||||
### Prerequisite
|
||||
- Docker installed and running
|
||||
- OS with bash scripting enabled (Ubuntu, Linux AMI, macos). Windows systems need to have [gitbash](https://git-scm.com/download/win)
|
||||
- User context used must have access to docker services. In most cases, use sudo su to switch as root user
|
||||
@@ -82,11 +88,11 @@ chmod +x setup.sh
|
||||
|
||||
### Proceed with setup
|
||||
|
||||
Above steps will set you ready to install and start plane services.
|
||||
Above steps will set you ready to install and start plane services.
|
||||
|
||||
Lets get started by running the `./setup.sh` command.
|
||||
Lets get started by running the `./setup.sh` command.
|
||||
|
||||
This will prompt you with the below options.
|
||||
This will prompt you with the below options.
|
||||
|
||||
```
|
||||
Select a Action you want to perform:
|
||||
@@ -100,26 +106,27 @@ Select a Action you want to perform:
|
||||
Action [2]: 1
|
||||
```
|
||||
|
||||
For the 1st time setup, type "1" as action input.
|
||||
For the 1st time setup, type "1" as action input.
|
||||
|
||||
This will create a create a folder `plane-app` or `plane-app-preview` (in case of preview deployment) and will download 2 files inside that
|
||||
|
||||
- `docker-compose.yaml`
|
||||
- `.env`
|
||||
|
||||
Again the `options [1-6]` will be popped up and this time hit `6` to exit.
|
||||
Again the `options [1-6]` will be popped up and this time hit `6` to exit.
|
||||
|
||||
---
|
||||
|
||||
### Continue with setup - Environment Settings
|
||||
|
||||
Before proceeding, we suggest used to review `.env` file and set the values.
|
||||
Below are the most import keys you must refer to. *<span style="color: #fcba03">You can use any text editor to edit this file</span>*.
|
||||
Before proceeding, we suggest used to review `.env` file and set the values.
|
||||
Below are the most import keys you must refer to. _<span style="color: #fcba03">You can use any text editor to edit this file</span>_.
|
||||
|
||||
> `NGINX_PORT` - This is default set to `80`. Make sure the port you choose to use is not preoccupied. (e.g `NGINX_PORT=8080`)
|
||||
|
||||
> `WEB_URL` - This is default set to `http://localhost`. Change this to the FQDN you plan to use along with NGINX_PORT (eg. `https://plane.example.com:8080` or `http://[IP-ADDRESS]:8080`)
|
||||
> `WEB_URL` - This is default set to `http://localhost`. Change this to the FQDN you plan to use along with NGINX_PORT (eg. `https://plane.example.com:8080` or `http://[IP-ADDRESS]:8080`)
|
||||
|
||||
> `CORS_ALLOWED_ORIGINS` - This is default set to `http://localhost`. Change this to the FQDN you plan to use along with NGINX_PORT (eg. `https://plane.example.com:8080` or `http://[IP-ADDRESS]:8080`)
|
||||
> `CORS_ALLOWED_ORIGINS` - This is default set to `http://localhost`. Change this to the FQDN you plan to use along with NGINX_PORT (eg. `https://plane.example.com:8080` or `http://[IP-ADDRESS]:8080`)
|
||||
|
||||
There are many other settings you can play with, but we suggest you configure `EMAIL SETTINGS` as it will enable you to invite your teammates onto the platform.
|
||||
|
||||
@@ -148,15 +155,15 @@ Be patient as it might take sometime based on download speed and system configur
|
||||
|
||||

|
||||
|
||||
This is the confirmation that all images were downloaded and the services are up & running.
|
||||
This is the confirmation that all images were downloaded and the services are up & running.
|
||||
|
||||
You have successfully self hosted `Plane` instance. Access the application by going to IP or domain you have configured it (e.g `https://plane.example.com:8080` or `http://[IP-ADDRESS]:8080`)
|
||||
You have successfully self hosted `Plane` instance. Access the application by going to IP or domain you have configured it (e.g `https://plane.example.com:8080` or `http://[IP-ADDRESS]:8080`)
|
||||
|
||||
---
|
||||
|
||||
### Stopping the Server
|
||||
|
||||
In case you want to make changes to `.env` variables, we suggest you to stop the services before doing that.
|
||||
In case you want to make changes to `.env` variables, we suggest you to stop the services before doing that.
|
||||
|
||||
Lets again run the `./setup.sh` command. You will again be prompted with the below options. This time select `3` to stop the sevices
|
||||
|
||||
@@ -180,7 +187,7 @@ If all goes well, you must see something like this
|
||||
|
||||
### Restarting the Server
|
||||
|
||||
In case you want to make changes to `.env` variables, without stopping the server or you noticed some abnormalies in services, you can restart the services with RESTART option.
|
||||
In case you want to make changes to `.env` variables, without stopping the server or you noticed some abnormalies in services, you can restart the services with RESTART option.
|
||||
|
||||
Lets again run the `./setup.sh` command. You will again be prompted with the below options. This time select `4` to restart the sevices
|
||||
|
||||
@@ -206,7 +213,7 @@ If all goes well, you must see something like this
|
||||
|
||||
It is always advised to keep Plane up to date with the latest release.
|
||||
|
||||
Lets again run the `./setup.sh` command. You will again be prompted with the below options. This time select `5` to upgrade the release.
|
||||
Lets again run the `./setup.sh` command. You will again be prompted with the below options. This time select `5` to upgrade the release.
|
||||
|
||||
```
|
||||
Select a Action you want to perform:
|
||||
@@ -220,7 +227,7 @@ Select a Action you want to perform:
|
||||
Action [2]: 5
|
||||
```
|
||||
|
||||
By choosing this, it will stop the services and then will download the latest `docker-compose.yaml` and `variables-upgrade.env`. Here system will not replace `.env` with the new one.
|
||||
By choosing this, it will stop the services and then will download the latest `docker-compose.yaml` and `variables-upgrade.env`. Here system will not replace `.env` with the new one.
|
||||
|
||||
You must expect the below message
|
||||
|
||||
@@ -228,18 +235,17 @@ You must expect the below message
|
||||
|
||||
Once done, choose `6` to exit from prompt.
|
||||
|
||||
> It is very important for you to compare the 2 files `variables-upgrade.env` and `.env`. Copy the newly added variable from downloaded file to `.env` and set the expected values.
|
||||
> It is very important for you to compare the 2 files `variables-upgrade.env` and `.env`. Copy the newly added variable from downloaded file to `.env` and set the expected values.
|
||||
|
||||
Once done with making changes in `.env` file, jump on to `Start Server`
|
||||
|
||||
|
||||
## Upgrading from v0.13.2 to v0.14.x
|
||||
|
||||
This is one time activity for users who are upgrading from v0.13.2 to v0.14.0
|
||||
|
||||
As there has been significant changes to Self Hosting process, this step mainly covers the data migration from current (v0.13.2) docker volumes from newly created volumes
|
||||
|
||||
> Before we begin with migration, make sure your v0.14.0 was started and then stopped. This is required to know the newly created docker volume names.
|
||||
> Before we begin with migration, make sure your v0.14.0 was started and then stopped. This is required to know the newly created docker volume names.
|
||||
|
||||
Begin with downloading the migration script using below command
|
||||
|
||||
@@ -256,15 +262,15 @@ Now run the `./migrate.sh` command and expect the instructions as below
|
||||
```
|
||||
******************************************************************
|
||||
|
||||
This script is solely for the migration purpose only.
|
||||
This script is solely for the migration purpose only.
|
||||
This is a 1 time migration of volume data from v0.13.2 => v0.14.x
|
||||
|
||||
Assumption:
|
||||
Assumption:
|
||||
1. Postgres data volume name ends with _pgdata
|
||||
2. Minio data volume name ends with _uploads
|
||||
3. Redis data volume name ends with _redisdata
|
||||
|
||||
Any changes to this script can break the migration.
|
||||
Any changes to this script can break the migration.
|
||||
|
||||
Before you proceed, make sure you run the below command
|
||||
to know the docker volumes
|
||||
@@ -275,12 +281,12 @@ docker volume ls -q | grep -i "_redisdata"
|
||||
|
||||
*******************************************************
|
||||
|
||||
Given below list of REDIS volumes, identify the prefix of source and destination volumes leaving "_redisdata"
|
||||
Given below list of REDIS volumes, identify the prefix of source and destination volumes leaving "_redisdata"
|
||||
---------------------
|
||||
plane-app_redisdata
|
||||
v0132_redisdata
|
||||
|
||||
Provide the Source Volume Prefix :
|
||||
Provide the Source Volume Prefix :
|
||||
```
|
||||
|
||||
**Open another terminal window**, and run the mentioned 3 command. This may be different for users who have changed the volume names in their previous setup (v0.13.2)
|
||||
@@ -289,9 +295,9 @@ For every command you must see 2 records something like shown in above example o
|
||||
|
||||
To move forward, you would need PREFIX of old setup and new setup. As per above example, `v0132` is the prefix of v0.13.2 and `plane-app` is the prefix of v0.14.0 setup
|
||||
|
||||
**Back to original terminal window**, *Provide the Source Volume Prefix* and hit ENTER.
|
||||
**Back to original terminal window**, _Provide the Source Volume Prefix_ and hit ENTER.
|
||||
|
||||
Now you will be prompted to *Provide Destination Volume Prefix*. Provide the value and hit ENTER
|
||||
Now you will be prompted to _Provide Destination Volume Prefix_. Provide the value and hit ENTER
|
||||
|
||||
```
|
||||
Provide the Source Volume Prefix : v0132
|
||||
@@ -302,8 +308,6 @@ In case the suffixes are wrong or the mentioned volumes are not found, you will
|
||||
|
||||

|
||||
|
||||
In case of successful migration, it will be a silent exit without error.
|
||||
|
||||
Now its time to restart v0.14.0 setup.
|
||||
|
||||
In case of successful migration, it will be a silent exit without error.
|
||||
|
||||
Now its time to restart v0.14.0 setup.
|
||||
|
||||
@@ -6,7 +6,6 @@ x-app-env : &app-env
|
||||
- WEB_URL=${WEB_URL:-http://localhost}
|
||||
- DEBUG=${DEBUG:-0}
|
||||
- DJANGO_SETTINGS_MODULE=${DJANGO_SETTINGS_MODULE:-plane.settings.production} # deprecated
|
||||
- NEXT_PUBLIC_ENABLE_OAUTH=${NEXT_PUBLIC_ENABLE_OAUTH:-0} # deprecated
|
||||
- NEXT_PUBLIC_DEPLOY_URL=${NEXT_PUBLIC_DEPLOY_URL:-http://localhost/spaces} # deprecated
|
||||
- SENTRY_DSN=${SENTRY_DSN:-""}
|
||||
- SENTRY_ENVIRONMENT=${SENTRY_ENVIRONMENT:-"production"}
|
||||
@@ -41,7 +40,7 @@ x-app-env : &app-env
|
||||
- DEFAULT_PASSWORD=${DEFAULT_PASSWORD:-password123}
|
||||
# OPENAI SETTINGS - Deprecated can be configured through admin panel
|
||||
- OPENAI_API_BASE=${OPENAI_API_BASE:-https://api.openai.com/v1}
|
||||
- OPENAI_API_KEY=${OPENAI_API_KEY:-"sk-"}
|
||||
- OPENAI_API_KEY=${OPENAI_API_KEY:-""}
|
||||
- GPT_ENGINE=${GPT_ENGINE:-"gpt-3.5-turbo"}
|
||||
# LOGIN/SIGNUP SETTINGS - Deprecated can be configured through admin panel
|
||||
- ENABLE_SIGNUP=${ENABLE_SIGNUP:-1}
|
||||
@@ -67,7 +66,7 @@ services:
|
||||
web:
|
||||
<<: *app-env
|
||||
platform: linux/amd64
|
||||
image: makeplane/plane-frontend-ce:${APP_RELEASE:-latest}
|
||||
image: makeplane/plane-frontend:${APP_RELEASE:-latest}
|
||||
restart: unless-stopped
|
||||
command: /usr/local/bin/start.sh web/server.js web
|
||||
deploy:
|
||||
@@ -79,7 +78,7 @@ services:
|
||||
space:
|
||||
<<: *app-env
|
||||
platform: linux/amd64
|
||||
image: makeplane/plane-space-ce:${APP_RELEASE:-latest}
|
||||
image: makeplane/plane-space:${APP_RELEASE:-latest}
|
||||
restart: unless-stopped
|
||||
command: /usr/local/bin/start.sh space/server.js space
|
||||
deploy:
|
||||
@@ -92,7 +91,7 @@ services:
|
||||
api:
|
||||
<<: *app-env
|
||||
platform: linux/amd64
|
||||
image: makeplane/plane-backend-ce:${APP_RELEASE:-latest}
|
||||
image: makeplane/plane-backend:${APP_RELEASE:-latest}
|
||||
restart: unless-stopped
|
||||
command: ./bin/takeoff
|
||||
deploy:
|
||||
@@ -104,7 +103,7 @@ services:
|
||||
worker:
|
||||
<<: *app-env
|
||||
platform: linux/amd64
|
||||
image: makeplane/plane-backend-ce:${APP_RELEASE:-latest}
|
||||
image: makeplane/plane-backend:${APP_RELEASE:-latest}
|
||||
restart: unless-stopped
|
||||
command: ./bin/worker
|
||||
depends_on:
|
||||
@@ -115,7 +114,7 @@ services:
|
||||
beat-worker:
|
||||
<<: *app-env
|
||||
platform: linux/amd64
|
||||
image: makeplane/plane-backend-ce:${APP_RELEASE:-latest}
|
||||
image: makeplane/plane-backend:${APP_RELEASE:-latest}
|
||||
restart: unless-stopped
|
||||
command: ./bin/beat
|
||||
depends_on:
|
||||
@@ -150,7 +149,7 @@ services:
|
||||
proxy:
|
||||
<<: *app-env
|
||||
platform: linux/amd64
|
||||
image: makeplane/plane-proxy-ce:${APP_RELEASE:-latest}
|
||||
image: makeplane/plane-proxy:${APP_RELEASE:-latest}
|
||||
ports:
|
||||
- ${NGINX_PORT}:80
|
||||
depends_on:
|
||||
|
||||
@@ -7,7 +7,6 @@ API_REPLICAS=1
|
||||
NGINX_PORT=80
|
||||
WEB_URL=http://localhost
|
||||
DEBUG=0
|
||||
NEXT_PUBLIC_ENABLE_OAUTH=0
|
||||
NEXT_PUBLIC_DEPLOY_URL=http://localhost/spaces
|
||||
SENTRY_DSN=""
|
||||
SENTRY_ENVIRONMENT="production"
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"repository": "https://github.com/makeplane/plane.git",
|
||||
"version": "0.13.2",
|
||||
"version": "0.14.0",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
@@ -27,7 +27,7 @@
|
||||
"prettier": "latest",
|
||||
"prettier-plugin-tailwindcss": "^0.5.4",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"turbo": "^1.10.16"
|
||||
"turbo": "^1.11.1"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/react": "18.2.42"
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ["custom"],
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
.next
|
||||
.vercel
|
||||
.tubro
|
||||
out/
|
||||
dis/
|
||||
build/
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"printWidth": 120,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/editor-core",
|
||||
"version": "0.0.1",
|
||||
"version": "0.14.0",
|
||||
"description": "Core Editor that powers Plane",
|
||||
"private": true,
|
||||
"main": "./dist/index.mjs",
|
||||
@@ -55,7 +55,7 @@
|
||||
"highlight.js": "^11.8.0",
|
||||
"jsx-dom-cjs": "^8.0.3",
|
||||
"lowlight": "^3.0.0",
|
||||
"lucide-react": "^0.244.0",
|
||||
"lucide-react": "^0.294.0",
|
||||
"react-moveable": "^0.54.2",
|
||||
"tailwind-merge": "^1.14.0",
|
||||
"tippy.js": "^6.3.7",
|
||||
@@ -80,4 +80,4 @@
|
||||
"nextjs",
|
||||
"react"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,35 +4,17 @@ import { startImageUpload } from "../ui/plugins/upload-image";
|
||||
import { findTableAncestor } from "./utils";
|
||||
|
||||
export const toggleHeadingOne = (editor: Editor, range?: Range) => {
|
||||
if (range)
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.deleteRange(range)
|
||||
.setNode("heading", { level: 1 })
|
||||
.run();
|
||||
if (range) editor.chain().focus().deleteRange(range).setNode("heading", { level: 1 }).run();
|
||||
else editor.chain().focus().toggleHeading({ level: 1 }).run();
|
||||
};
|
||||
|
||||
export const toggleHeadingTwo = (editor: Editor, range?: Range) => {
|
||||
if (range)
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.deleteRange(range)
|
||||
.setNode("heading", { level: 2 })
|
||||
.run();
|
||||
if (range) editor.chain().focus().deleteRange(range).setNode("heading", { level: 2 }).run();
|
||||
else editor.chain().focus().toggleHeading({ level: 2 }).run();
|
||||
};
|
||||
|
||||
export const toggleHeadingThree = (editor: Editor, range?: Range) => {
|
||||
if (range)
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.deleteRange(range)
|
||||
.setNode("heading", { level: 3 })
|
||||
.run();
|
||||
if (range) editor.chain().focus().deleteRange(range).setNode("heading", { level: 3 }).run();
|
||||
else editor.chain().focus().toggleHeading({ level: 3 }).run();
|
||||
};
|
||||
|
||||
@@ -57,8 +39,7 @@ export const toggleCodeBlock = (editor: Editor, range?: Range) => {
|
||||
};
|
||||
|
||||
export const toggleOrderedList = (editor: Editor, range?: Range) => {
|
||||
if (range)
|
||||
editor.chain().focus().deleteRange(range).toggleOrderedList().run();
|
||||
if (range) editor.chain().focus().deleteRange(range).toggleOrderedList().run();
|
||||
else editor.chain().focus().toggleOrderedList().run();
|
||||
};
|
||||
|
||||
@@ -78,21 +59,8 @@ export const toggleStrike = (editor: Editor, range?: Range) => {
|
||||
};
|
||||
|
||||
export const toggleBlockquote = (editor: Editor, range?: Range) => {
|
||||
if (range)
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.deleteRange(range)
|
||||
.toggleNode("paragraph", "paragraph")
|
||||
.toggleBlockquote()
|
||||
.run();
|
||||
else
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.toggleNode("paragraph", "paragraph")
|
||||
.toggleBlockquote()
|
||||
.run();
|
||||
if (range) editor.chain().focus().deleteRange(range).toggleNode("paragraph", "paragraph").toggleBlockquote().run();
|
||||
else editor.chain().focus().toggleNode("paragraph", "paragraph").toggleBlockquote().run();
|
||||
};
|
||||
|
||||
export const insertTableCommand = (editor: Editor, range?: Range) => {
|
||||
@@ -105,19 +73,8 @@ export const insertTableCommand = (editor: Editor, range?: Range) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (range)
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.deleteRange(range)
|
||||
.insertTable({ rows: 3, cols: 3, withHeaderRow: true })
|
||||
.run();
|
||||
else
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.insertTable({ rows: 3, cols: 3, withHeaderRow: true })
|
||||
.run();
|
||||
if (range) editor.chain().focus().deleteRange(range).insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run();
|
||||
else editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run();
|
||||
};
|
||||
|
||||
export const unsetLinkEditor = (editor: Editor) => {
|
||||
@@ -131,10 +88,8 @@ export const setLinkEditor = (editor: Editor, url: string) => {
|
||||
export const insertImageCommand = (
|
||||
editor: Editor,
|
||||
uploadFile: UploadImage,
|
||||
setIsSubmitting?: (
|
||||
isSubmitting: "submitting" | "submitted" | "saved",
|
||||
) => void,
|
||||
range?: Range,
|
||||
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void,
|
||||
range?: Range
|
||||
) => {
|
||||
if (range) editor.chain().focus().deleteRange(range).run();
|
||||
const input = document.createElement("input");
|
||||
|
||||
@@ -6,25 +6,19 @@ interface EditorClassNames {
|
||||
customClassName?: string;
|
||||
}
|
||||
|
||||
export const getEditorClassNames = ({
|
||||
noBorder,
|
||||
borderOnFocus,
|
||||
customClassName,
|
||||
}: EditorClassNames) =>
|
||||
export const getEditorClassNames = ({ noBorder, borderOnFocus, customClassName }: EditorClassNames) =>
|
||||
cn(
|
||||
"relative w-full max-w-full sm:rounded-lg mt-2 p-3 relative focus:outline-none rounded-md",
|
||||
noBorder ? "" : "border border-custom-border-200",
|
||||
borderOnFocus ? "focus:border border-custom-border-300" : "focus:border-0",
|
||||
customClassName,
|
||||
customClassName
|
||||
);
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
export const findTableAncestor = (
|
||||
node: Node | null,
|
||||
): HTMLTableElement | null => {
|
||||
export const findTableAncestor = (node: Node | null): HTMLTableElement | null => {
|
||||
while (node !== null && node.nodeName !== "TABLE") {
|
||||
node = node.parentNode;
|
||||
}
|
||||
|
||||
@@ -7,11 +7,7 @@ interface EditorContainerProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const EditorContainer = ({
|
||||
editor,
|
||||
editorClassNames,
|
||||
children,
|
||||
}: EditorContainerProps) => (
|
||||
export const EditorContainer = ({ editor, editorClassNames, children }: EditorContainerProps) => (
|
||||
<div
|
||||
id="editor-container"
|
||||
onClick={() => {
|
||||
|
||||
@@ -8,16 +8,10 @@ interface EditorContentProps {
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export const EditorContentWrapper = ({
|
||||
editor,
|
||||
editorContentCustomClassNames = "",
|
||||
children,
|
||||
}: EditorContentProps) => (
|
||||
export const EditorContentWrapper = ({ editor, editorContentCustomClassNames = "", children }: EditorContentProps) => (
|
||||
<div className={`contentEditor ${editorContentCustomClassNames}`}>
|
||||
<EditorContent editor={editor} />
|
||||
{editor?.isActive("image") && editor?.isEditable && (
|
||||
<ImageResizer editor={editor} />
|
||||
)}
|
||||
{editor?.isActive("image") && editor?.isEditable && <ImageResizer editor={editor} />}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
+1
-4
@@ -2,10 +2,7 @@ import { getNodeType } from "@tiptap/core";
|
||||
import { NodeType } from "@tiptap/pm/model";
|
||||
import { EditorState } from "@tiptap/pm/state";
|
||||
|
||||
export const findListItemPos = (
|
||||
typeOrName: string | NodeType,
|
||||
state: EditorState,
|
||||
) => {
|
||||
export const findListItemPos = (typeOrName: string | NodeType, state: EditorState) => {
|
||||
const { $from } = state.selection;
|
||||
const nodeType = getNodeType(typeOrName, state.schema);
|
||||
|
||||
|
||||
+1
-5
@@ -10,11 +10,7 @@ export const getNextListDepth = (typeOrName: string, state: EditorState) => {
|
||||
return false;
|
||||
}
|
||||
|
||||
const [, depth] = getNodeAtPosition(
|
||||
state,
|
||||
typeOrName,
|
||||
listItemPos.$pos.pos + 4,
|
||||
);
|
||||
const [, depth] = getNodeAtPosition(state, typeOrName, listItemPos.$pos.pos + 4);
|
||||
|
||||
return depth;
|
||||
};
|
||||
|
||||
+4
-16
@@ -4,11 +4,7 @@ import { Node } from "@tiptap/pm/model";
|
||||
import { findListItemPos } from "./find-list-item-pos";
|
||||
import { hasListBefore } from "./has-list-before";
|
||||
|
||||
export const handleBackspace = (
|
||||
editor: Editor,
|
||||
name: string,
|
||||
parentListTypes: string[],
|
||||
) => {
|
||||
export const handleBackspace = (editor: Editor, name: string, parentListTypes: string[]) => {
|
||||
// this is required to still handle the undo handling
|
||||
if (editor.commands.undoInputRule()) {
|
||||
return true;
|
||||
@@ -23,10 +19,7 @@ export const handleBackspace = (
|
||||
// if the current item is NOT inside a list item &
|
||||
// the previous item is a list (orderedList or bulletList)
|
||||
// move the cursor into the list and delete the current item
|
||||
if (
|
||||
!isNodeActive(editor.state, name) &&
|
||||
hasListBefore(editor.state, name, parentListTypes)
|
||||
) {
|
||||
if (!isNodeActive(editor.state, name) && hasListBefore(editor.state, name, parentListTypes)) {
|
||||
const { $anchor } = editor.state.selection;
|
||||
|
||||
const $listPos = editor.state.doc.resolve($anchor.before() - 1);
|
||||
@@ -45,16 +38,11 @@ export const handleBackspace = (
|
||||
return false;
|
||||
}
|
||||
|
||||
const $lastItemPos = editor.state.doc.resolve(
|
||||
$listPos.start() + lastItem.pos + 1,
|
||||
);
|
||||
const $lastItemPos = editor.state.doc.resolve($listPos.start() + lastItem.pos + 1);
|
||||
|
||||
return editor
|
||||
.chain()
|
||||
.cut(
|
||||
{ from: $anchor.start() - 1, to: $anchor.end() + 1 },
|
||||
$lastItemPos.end(),
|
||||
)
|
||||
.cut({ from: $anchor.start() - 1, to: $anchor.end() + 1 }, $lastItemPos.end())
|
||||
.joinForward()
|
||||
.run();
|
||||
}
|
||||
|
||||
+1
-5
@@ -1,10 +1,6 @@
|
||||
import { EditorState } from "@tiptap/pm/state";
|
||||
|
||||
export const hasListBefore = (
|
||||
editorState: EditorState,
|
||||
name: string,
|
||||
parentListTypes: string[],
|
||||
) => {
|
||||
export const hasListBefore = (editorState: EditorState, name: string, parentListTypes: string[]) => {
|
||||
const { $anchor } = editorState.selection;
|
||||
|
||||
const previousNodePos = Math.max(0, $anchor.pos - 2);
|
||||
|
||||
+1
-4
@@ -1,9 +1,6 @@
|
||||
import { EditorState } from "@tiptap/pm/state";
|
||||
|
||||
export const hasListItemAfter = (
|
||||
typeOrName: string,
|
||||
state: EditorState,
|
||||
): boolean => {
|
||||
export const hasListItemAfter = (typeOrName: string, state: EditorState): boolean => {
|
||||
const { $anchor } = state.selection;
|
||||
|
||||
const $targetPos = state.doc.resolve($anchor.pos - $anchor.parentOffset - 2);
|
||||
|
||||
+1
-4
@@ -1,9 +1,6 @@
|
||||
import { EditorState } from "@tiptap/pm/state";
|
||||
|
||||
export const hasListItemBefore = (
|
||||
typeOrName: string,
|
||||
state: EditorState,
|
||||
): boolean => {
|
||||
export const hasListItemBefore = (typeOrName: string, state: EditorState): boolean => {
|
||||
const { $anchor } = state.selection;
|
||||
|
||||
const $targetPos = state.doc.resolve($anchor.pos - 2);
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
import { TextSelection } from "prosemirror-state";
|
||||
|
||||
import {
|
||||
InputRule,
|
||||
mergeAttributes,
|
||||
Node,
|
||||
nodeInputRule,
|
||||
wrappingInputRule,
|
||||
} from "@tiptap/core";
|
||||
import { InputRule, mergeAttributes, Node, nodeInputRule, wrappingInputRule } from "@tiptap/core";
|
||||
|
||||
/**
|
||||
* Extension based on:
|
||||
@@ -83,8 +77,7 @@ export default Node.create<HorizontalRuleOptions>({
|
||||
tr.setSelection(TextSelection.create(tr.doc, $to.pos));
|
||||
} else {
|
||||
// add node after horizontal rule if it’s the end of the document
|
||||
const node =
|
||||
$to.parent.type.contentMatch.defaultType?.create();
|
||||
const node = $to.parent.type.contentMatch.defaultType?.create();
|
||||
|
||||
if (node) {
|
||||
tr.insert(posAfter, node);
|
||||
|
||||
@@ -4,9 +4,7 @@ import Moveable from "react-moveable";
|
||||
|
||||
export const ImageResizer = ({ editor }: { editor: Editor }) => {
|
||||
const updateMediaSize = () => {
|
||||
const imageInfo = document.querySelector(
|
||||
".ProseMirror-selectednode",
|
||||
) as HTMLImageElement;
|
||||
const imageInfo = document.querySelector(".ProseMirror-selectednode") as HTMLImageElement;
|
||||
if (imageInfo) {
|
||||
const selection = editor.state.selection;
|
||||
editor.commands.setImage({
|
||||
@@ -32,9 +30,7 @@ export const ImageResizer = ({ editor }: { editor: Editor }) => {
|
||||
resizable
|
||||
throttleResize={0}
|
||||
onResizeStart={() => {
|
||||
const imageInfo = document.querySelector(
|
||||
".ProseMirror-selectednode",
|
||||
) as HTMLImageElement;
|
||||
const imageInfo = document.querySelector(".ProseMirror-selectednode") as HTMLImageElement;
|
||||
if (imageInfo) {
|
||||
const originalWidth = Number(imageInfo.width);
|
||||
const originalHeight = Number(imageInfo.height);
|
||||
|
||||
@@ -15,22 +15,14 @@ interface ImageNode extends ProseMirrorNode {
|
||||
const deleteKey = new PluginKey("delete-image");
|
||||
const IMAGE_NODE_TYPE = "image";
|
||||
|
||||
const ImageExtension = (
|
||||
deleteImage: DeleteImage,
|
||||
restoreFile: RestoreImage,
|
||||
cancelUploadImage?: () => any,
|
||||
) =>
|
||||
const ImageExtension = (deleteImage: DeleteImage, restoreFile: RestoreImage, cancelUploadImage?: () => any) =>
|
||||
ImageExt.extend({
|
||||
addProseMirrorPlugins() {
|
||||
return [
|
||||
UploadImagesPlugin(cancelUploadImage),
|
||||
new Plugin({
|
||||
key: deleteKey,
|
||||
appendTransaction: (
|
||||
transactions: readonly Transaction[],
|
||||
oldState: EditorState,
|
||||
newState: EditorState,
|
||||
) => {
|
||||
appendTransaction: (transactions: readonly Transaction[], oldState: EditorState, newState: EditorState) => {
|
||||
const newImageSources = new Set<string>();
|
||||
newState.doc.descendants((node) => {
|
||||
if (node.type.name === IMAGE_NODE_TYPE) {
|
||||
@@ -67,11 +59,7 @@ const ImageExtension = (
|
||||
}),
|
||||
new Plugin({
|
||||
key: new PluginKey("imageRestoration"),
|
||||
appendTransaction: (
|
||||
transactions: readonly Transaction[],
|
||||
oldState: EditorState,
|
||||
newState: EditorState,
|
||||
) => {
|
||||
appendTransaction: (transactions: readonly Transaction[], oldState: EditorState, newState: EditorState) => {
|
||||
const oldImageSources = new Set<string>();
|
||||
oldState.doc.descendants((node) => {
|
||||
if (node.type.name === IMAGE_NODE_TYPE) {
|
||||
|
||||
@@ -22,11 +22,7 @@ import { CustomKeymap } from "./keymap";
|
||||
import { CustomCodeBlock } from "./code";
|
||||
import { CustomQuoteExtension } from "./quote";
|
||||
import { ListKeymap } from "./custom-list-keymap";
|
||||
import {
|
||||
IMentionSuggestion,
|
||||
DeleteImage,
|
||||
RestoreImage,
|
||||
} from "@plane/editor-types";
|
||||
import { IMentionSuggestion, DeleteImage, RestoreImage } from "@plane/editor-types";
|
||||
|
||||
export const CoreEditorExtensions = (
|
||||
mentionConfig: {
|
||||
@@ -109,9 +105,5 @@ export const CoreEditorExtensions = (
|
||||
TableHeader,
|
||||
TableCell,
|
||||
TableRow,
|
||||
Mentions(
|
||||
mentionConfig.mentionSuggestions,
|
||||
mentionConfig.mentionHighlights,
|
||||
false
|
||||
),
|
||||
Mentions(mentionConfig.mentionSuggestions, mentionConfig.mentionHighlights, false),
|
||||
];
|
||||
|
||||
@@ -22,10 +22,6 @@ export default Node.create<TableRowOptions>({
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return [
|
||||
"tr",
|
||||
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
|
||||
0,
|
||||
];
|
||||
return ["tr", mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0];
|
||||
},
|
||||
});
|
||||
|
||||
@@ -20,15 +20,12 @@ export function tableControls() {
|
||||
mousemove: (view, event) => {
|
||||
const pluginState = key.getState(view.state);
|
||||
|
||||
if (
|
||||
!(event.target as HTMLElement).closest(".tableWrapper") &&
|
||||
pluginState.values.hoveredTable
|
||||
) {
|
||||
if (!(event.target as HTMLElement).closest(".tableWrapper") && pluginState.values.hoveredTable) {
|
||||
return view.dispatch(
|
||||
view.state.tr.setMeta(key, {
|
||||
setHoveredTable: null,
|
||||
setHoveredCell: null,
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -40,13 +37,11 @@ export function tableControls() {
|
||||
if (!pos) return;
|
||||
|
||||
const table = findParentNode((node) => node.type.name === "table")(
|
||||
TextSelection.create(view.state.doc, pos.pos),
|
||||
TextSelection.create(view.state.doc, pos.pos)
|
||||
);
|
||||
const cell = findParentNode((node) => node.type.name === "tableCell" || node.type.name === "tableHeader")(
|
||||
TextSelection.create(view.state.doc, pos.pos)
|
||||
);
|
||||
const cell = findParentNode(
|
||||
(node) =>
|
||||
node.type.name === "tableCell" ||
|
||||
node.type.name === "tableHeader",
|
||||
)(TextSelection.create(view.state.doc, pos.pos));
|
||||
|
||||
if (!table || !cell) return;
|
||||
|
||||
@@ -55,7 +50,7 @@ export function tableControls() {
|
||||
view.state.tr.setMeta(key, {
|
||||
setHoveredTable: table,
|
||||
setHoveredCell: cell,
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
},
|
||||
@@ -68,12 +63,7 @@ export function tableControls() {
|
||||
|
||||
const { hoveredTable, hoveredCell } = pluginState.values;
|
||||
const docSize = state.doc.content.size;
|
||||
if (
|
||||
hoveredTable &&
|
||||
hoveredCell &&
|
||||
hoveredTable.pos < docSize &&
|
||||
hoveredCell.pos < docSize
|
||||
) {
|
||||
if (hoveredTable && hoveredCell && hoveredTable.pos < docSize && hoveredCell.pos < docSize) {
|
||||
const decorations = [
|
||||
Decoration.node(
|
||||
hoveredTable.pos,
|
||||
@@ -82,7 +72,7 @@ export function tableControls() {
|
||||
{
|
||||
hoveredTable,
|
||||
hoveredCell,
|
||||
},
|
||||
}
|
||||
),
|
||||
];
|
||||
|
||||
|
||||
@@ -4,11 +4,7 @@ import { Decoration, NodeView } from "@tiptap/pm/view";
|
||||
import tippy, { Instance, Props } from "tippy.js";
|
||||
|
||||
import { Editor } from "@tiptap/core";
|
||||
import {
|
||||
CellSelection,
|
||||
TableMap,
|
||||
updateColumnsOnResize,
|
||||
} from "@tiptap/prosemirror-tables";
|
||||
import { CellSelection, TableMap, updateColumnsOnResize } from "@tiptap/prosemirror-tables";
|
||||
|
||||
import icons from "./icons";
|
||||
|
||||
@@ -18,7 +14,7 @@ export function updateColumns(
|
||||
table: HTMLElement,
|
||||
cellMinWidth: number,
|
||||
overrideCol?: number,
|
||||
overrideValue?: any,
|
||||
overrideValue?: any
|
||||
) {
|
||||
let totalWidth = 0;
|
||||
let fixedWidth = true;
|
||||
@@ -31,8 +27,7 @@ export function updateColumns(
|
||||
const { colspan, colwidth } = row.child(i).attrs;
|
||||
|
||||
for (let j = 0; j < colspan; j += 1, col += 1) {
|
||||
const hasWidth =
|
||||
overrideCol === col ? overrideValue : colwidth && colwidth[j];
|
||||
const hasWidth = overrideCol === col ? overrideValue : colwidth && colwidth[j];
|
||||
const cssWidth = hasWidth ? `${hasWidth}px` : "";
|
||||
|
||||
totalWidth += hasWidth || cellMinWidth;
|
||||
@@ -42,8 +37,7 @@ export function updateColumns(
|
||||
}
|
||||
|
||||
if (!nextDOM) {
|
||||
colgroup.appendChild(document.createElement("col")).style.width =
|
||||
cssWidth;
|
||||
colgroup.appendChild(document.createElement("col")).style.width = cssWidth;
|
||||
} else {
|
||||
if (nextDOM.style.width !== cssWidth) {
|
||||
nextDOM.style.width = cssWidth;
|
||||
@@ -98,14 +92,12 @@ const columnsToolboxItems = [
|
||||
{
|
||||
label: "Add Column Before",
|
||||
icon: icons.insertLeftTableIcon,
|
||||
action: ({ editor }: { editor: Editor }) =>
|
||||
editor.chain().focus().addColumnBefore().run(),
|
||||
action: ({ editor }: { editor: Editor }) => editor.chain().focus().addColumnBefore().run(),
|
||||
},
|
||||
{
|
||||
label: "Add Column After",
|
||||
icon: icons.insertRightTableIcon,
|
||||
action: ({ editor }: { editor: Editor }) =>
|
||||
editor.chain().focus().addColumnAfter().run(),
|
||||
action: ({ editor }: { editor: Editor }) => editor.chain().focus().addColumnAfter().run(),
|
||||
},
|
||||
{
|
||||
label: "Pick Column Color",
|
||||
@@ -131,8 +123,7 @@ const columnsToolboxItems = [
|
||||
{
|
||||
label: "Delete Column",
|
||||
icon: icons.deleteColumn,
|
||||
action: ({ editor }: { editor: Editor }) =>
|
||||
editor.chain().focus().deleteColumn().run(),
|
||||
action: ({ editor }: { editor: Editor }) => editor.chain().focus().deleteColumn().run(),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -140,14 +131,12 @@ const rowsToolboxItems = [
|
||||
{
|
||||
label: "Add Row Above",
|
||||
icon: icons.insertTopTableIcon,
|
||||
action: ({ editor }: { editor: Editor }) =>
|
||||
editor.chain().focus().addRowBefore().run(),
|
||||
action: ({ editor }: { editor: Editor }) => editor.chain().focus().addRowBefore().run(),
|
||||
},
|
||||
{
|
||||
label: "Add Row Below",
|
||||
icon: icons.insertBottomTableIcon,
|
||||
action: ({ editor }: { editor: Editor }) =>
|
||||
editor.chain().focus().addRowAfter().run(),
|
||||
action: ({ editor }: { editor: Editor }) => editor.chain().focus().addRowAfter().run(),
|
||||
},
|
||||
{
|
||||
label: "Pick Row Color",
|
||||
@@ -159,11 +148,7 @@ const rowsToolboxItems = [
|
||||
}: {
|
||||
editor: Editor;
|
||||
triggerButton: HTMLButtonElement;
|
||||
controlsContainer:
|
||||
| Element
|
||||
| "parent"
|
||||
| ((ref: Element) => Element)
|
||||
| undefined;
|
||||
controlsContainer: Element | "parent" | ((ref: Element) => Element) | undefined;
|
||||
}) => {
|
||||
createColorPickerToolbox({
|
||||
triggerButton,
|
||||
@@ -177,8 +162,7 @@ const rowsToolboxItems = [
|
||||
{
|
||||
label: "Delete Row",
|
||||
icon: icons.deleteRow,
|
||||
action: ({ editor }: { editor: Editor }) =>
|
||||
editor.chain().focus().deleteRow().run(),
|
||||
action: ({ editor }: { editor: Editor }) => editor.chain().focus().deleteRow().run(),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -213,9 +197,9 @@ function createToolbox({
|
||||
innerHTML: item.icon,
|
||||
}),
|
||||
h("div", { className: "label" }, item.label),
|
||||
],
|
||||
),
|
||||
),
|
||||
]
|
||||
)
|
||||
)
|
||||
),
|
||||
...tippyOptions,
|
||||
});
|
||||
@@ -272,11 +256,11 @@ function createColorPickerToolbox({
|
||||
{
|
||||
className: "label",
|
||||
},
|
||||
key,
|
||||
key
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
]
|
||||
)
|
||||
)
|
||||
),
|
||||
onHidden: (instance) => {
|
||||
instance.destroy();
|
||||
@@ -319,7 +303,7 @@ export class TableView implements NodeView {
|
||||
cellMinWidth: number,
|
||||
decorations: Decoration[],
|
||||
editor: Editor,
|
||||
getPos: () => number,
|
||||
getPos: () => number
|
||||
) {
|
||||
this.node = node;
|
||||
this.cellMinWidth = cellMinWidth;
|
||||
@@ -337,7 +321,7 @@ export class TableView implements NodeView {
|
||||
itemType: "button",
|
||||
className: "rowsControlDiv",
|
||||
onClick: () => this.selectRow(),
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
this.columnsControl = h(
|
||||
@@ -347,14 +331,14 @@ export class TableView implements NodeView {
|
||||
itemType: "button",
|
||||
className: "columnsControlDiv",
|
||||
onClick: () => this.selectColumn(),
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
this.controls = h(
|
||||
"div",
|
||||
{ className: "tableControls", contentEditable: "false" },
|
||||
this.rowsControl,
|
||||
this.columnsControl,
|
||||
this.columnsControl
|
||||
);
|
||||
|
||||
this.columnsToolbox = createToolbox({
|
||||
@@ -397,7 +381,7 @@ export class TableView implements NodeView {
|
||||
this.colgroup = h(
|
||||
"colgroup",
|
||||
null,
|
||||
Array.from({ length: this.map.width }, () => 1).map(() => h("col")),
|
||||
Array.from({ length: this.map.width }, () => 1).map(() => h("col"))
|
||||
);
|
||||
this.tbody = h("tbody");
|
||||
this.table = h("table", null, this.colgroup, this.tbody);
|
||||
@@ -408,7 +392,7 @@ export class TableView implements NodeView {
|
||||
className: "tableWrapper controls--disabled",
|
||||
},
|
||||
this.controls,
|
||||
this.table,
|
||||
this.table
|
||||
);
|
||||
|
||||
this.render();
|
||||
@@ -434,18 +418,11 @@ export class TableView implements NodeView {
|
||||
|
||||
render() {
|
||||
if (this.colgroup.children.length !== this.map.width) {
|
||||
const cols = Array.from({ length: this.map.width }, () => 1).map(() =>
|
||||
h("col"),
|
||||
);
|
||||
const cols = Array.from({ length: this.map.width }, () => 1).map(() => h("col"));
|
||||
this.colgroup.replaceChildren(...cols);
|
||||
}
|
||||
|
||||
updateColumnsOnResize(
|
||||
this.node,
|
||||
this.colgroup,
|
||||
this.table,
|
||||
this.cellMinWidth,
|
||||
);
|
||||
updateColumnsOnResize(this.node, this.colgroup, this.table, this.cellMinWidth);
|
||||
}
|
||||
|
||||
ignoreMutation() {
|
||||
@@ -453,9 +430,7 @@ export class TableView implements NodeView {
|
||||
}
|
||||
|
||||
updateControls() {
|
||||
const { hoveredTable: table, hoveredCell: cell } = Object.values(
|
||||
this.decorations,
|
||||
).reduce(
|
||||
const { hoveredTable: table, hoveredCell: cell } = Object.values(this.decorations).reduce(
|
||||
(acc, curr) => {
|
||||
if (curr.spec.hoveredCell !== undefined) {
|
||||
acc["hoveredCell"] = curr.spec.hoveredCell;
|
||||
@@ -466,7 +441,7 @@ export class TableView implements NodeView {
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, HTMLElement>,
|
||||
{} as Record<string, HTMLElement>
|
||||
) as any;
|
||||
|
||||
if (table === undefined || cell === undefined) {
|
||||
@@ -481,9 +456,7 @@ export class TableView implements NodeView {
|
||||
const tableRect = this.table.getBoundingClientRect();
|
||||
const cellRect = cellDom.getBoundingClientRect();
|
||||
|
||||
this.columnsControl.style.left = `${
|
||||
cellRect.left - tableRect.left - this.table.parentElement!.scrollLeft
|
||||
}px`;
|
||||
this.columnsControl.style.left = `${cellRect.left - tableRect.left - this.table.parentElement!.scrollLeft}px`;
|
||||
this.columnsControl.style.width = `${cellRect.width}px`;
|
||||
|
||||
this.rowsControl.style.top = `${cellRect.top - tableRect.top}px`;
|
||||
@@ -493,22 +466,14 @@ export class TableView implements NodeView {
|
||||
selectColumn() {
|
||||
if (!this.hoveredCell) return;
|
||||
|
||||
const colIndex = this.map.colCount(
|
||||
this.hoveredCell.pos - (this.getPos() + 1),
|
||||
);
|
||||
const colIndex = this.map.colCount(this.hoveredCell.pos - (this.getPos() + 1));
|
||||
const anchorCellPos = this.hoveredCell.pos;
|
||||
const headCellPos =
|
||||
this.map.map[colIndex + this.map.width * (this.map.height - 1)] +
|
||||
(this.getPos() + 1);
|
||||
const headCellPos = this.map.map[colIndex + this.map.width * (this.map.height - 1)] + (this.getPos() + 1);
|
||||
|
||||
const cellSelection = CellSelection.create(
|
||||
this.editor.view.state.doc,
|
||||
anchorCellPos,
|
||||
headCellPos,
|
||||
);
|
||||
const cellSelection = CellSelection.create(this.editor.view.state.doc, anchorCellPos, headCellPos);
|
||||
this.editor.view.dispatch(
|
||||
// @ts-ignore
|
||||
this.editor.state.tr.setSelection(cellSelection),
|
||||
this.editor.state.tr.setSelection(cellSelection)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -516,21 +481,13 @@ export class TableView implements NodeView {
|
||||
if (!this.hoveredCell) return;
|
||||
|
||||
const anchorCellPos = this.hoveredCell.pos;
|
||||
const anchorCellIndex = this.map.map.indexOf(
|
||||
anchorCellPos - (this.getPos() + 1),
|
||||
);
|
||||
const headCellPos =
|
||||
this.map.map[anchorCellIndex + (this.map.width - 1)] +
|
||||
(this.getPos() + 1);
|
||||
const anchorCellIndex = this.map.map.indexOf(anchorCellPos - (this.getPos() + 1));
|
||||
const headCellPos = this.map.map[anchorCellIndex + (this.map.width - 1)] + (this.getPos() + 1);
|
||||
|
||||
const cellSelection = CellSelection.create(
|
||||
this.editor.state.doc,
|
||||
anchorCellPos,
|
||||
headCellPos,
|
||||
);
|
||||
const cellSelection = CellSelection.create(this.editor.state.doc, anchorCellPos, headCellPos);
|
||||
this.editor.view.dispatch(
|
||||
// @ts-ignore
|
||||
this.editor.view.state.tr.setSelection(cellSelection),
|
||||
this.editor.view.state.tr.setSelection(cellSelection)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
import { TextSelection } from "@tiptap/pm/state";
|
||||
|
||||
import {
|
||||
callOrReturn,
|
||||
getExtensionField,
|
||||
mergeAttributes,
|
||||
Node,
|
||||
ParentConfig,
|
||||
} from "@tiptap/core";
|
||||
import { callOrReturn, getExtensionField, mergeAttributes, Node, ParentConfig } from "@tiptap/core";
|
||||
import {
|
||||
addColumnAfter,
|
||||
addColumnBefore,
|
||||
@@ -44,11 +38,7 @@ export interface TableOptions {
|
||||
declare module "@tiptap/core" {
|
||||
interface Commands<ReturnType> {
|
||||
table: {
|
||||
insertTable: (options?: {
|
||||
rows?: number;
|
||||
cols?: number;
|
||||
withHeaderRow?: boolean;
|
||||
}) => ReturnType;
|
||||
insertTable: (options?: { rows?: number; cols?: number; withHeaderRow?: boolean }) => ReturnType;
|
||||
addColumnBefore: () => ReturnType;
|
||||
addColumnAfter: () => ReturnType;
|
||||
deleteColumn: () => ReturnType;
|
||||
@@ -66,10 +56,7 @@ declare module "@tiptap/core" {
|
||||
goToNextCell: () => ReturnType;
|
||||
goToPreviousCell: () => ReturnType;
|
||||
fixTables: () => ReturnType;
|
||||
setCellSelection: (position: {
|
||||
anchorCell: number;
|
||||
headCell?: number;
|
||||
}) => ReturnType;
|
||||
setCellSelection: (position: { anchorCell: number; headCell?: number }) => ReturnType;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -114,11 +101,7 @@ export default Node.create({
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return [
|
||||
"table",
|
||||
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
|
||||
["tbody", 0],
|
||||
];
|
||||
return ["table", mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), ["tbody", 0]];
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
@@ -220,11 +203,7 @@ export default Node.create({
|
||||
(position) =>
|
||||
({ tr, dispatch }) => {
|
||||
if (dispatch) {
|
||||
const selection = CellSelection.create(
|
||||
tr.doc,
|
||||
position.anchorCell,
|
||||
position.headCell,
|
||||
);
|
||||
const selection = CellSelection.create(tr.doc, position.anchorCell, position.headCell);
|
||||
|
||||
// @ts-ignore
|
||||
tr.setSelection(selection);
|
||||
@@ -260,13 +239,7 @@ export default Node.create({
|
||||
return ({ editor, getPos, node, decorations }) => {
|
||||
const { cellMinWidth } = this.options;
|
||||
|
||||
return new TableView(
|
||||
node,
|
||||
cellMinWidth,
|
||||
decorations,
|
||||
editor,
|
||||
getPos as () => number,
|
||||
);
|
||||
return new TableView(node, cellMinWidth, decorations, editor, getPos as () => number);
|
||||
};
|
||||
},
|
||||
|
||||
@@ -289,7 +262,7 @@ export default Node.create({
|
||||
|
||||
// @ts-ignore
|
||||
lastColumnResizable: this.options.lastColumnResizable,
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -304,9 +277,7 @@ export default Node.create({
|
||||
};
|
||||
|
||||
return {
|
||||
tableRole: callOrReturn(
|
||||
getExtensionField(extension, "tableRole", context),
|
||||
),
|
||||
tableRole: callOrReturn(getExtensionField(extension, "tableRole", context)),
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Fragment, Node as ProsemirrorNode, NodeType } from "prosemirror-model";
|
||||
|
||||
export function createCell(
|
||||
cellType: NodeType,
|
||||
cellContent?: Fragment | ProsemirrorNode | Array<ProsemirrorNode>,
|
||||
cellContent?: Fragment | ProsemirrorNode | Array<ProsemirrorNode>
|
||||
): ProsemirrorNode | null | undefined {
|
||||
if (cellContent) {
|
||||
return cellType.createChecked(null, cellContent);
|
||||
|
||||
@@ -8,7 +8,7 @@ export function createTable(
|
||||
rowsCount: number,
|
||||
colsCount: number,
|
||||
withHeaderRow: boolean,
|
||||
cellContent?: Fragment | ProsemirrorNode | Array<ProsemirrorNode>,
|
||||
cellContent?: Fragment | ProsemirrorNode | Array<ProsemirrorNode>
|
||||
): ProsemirrorNode {
|
||||
const types = getTableNodeTypes(schema);
|
||||
const headerCells: ProsemirrorNode[] = [];
|
||||
@@ -33,12 +33,7 @@ export function createTable(
|
||||
const rows: ProsemirrorNode[] = [];
|
||||
|
||||
for (let index = 0; index < rowsCount; index += 1) {
|
||||
rows.push(
|
||||
types.row.createChecked(
|
||||
null,
|
||||
withHeaderRow && index === 0 ? headerCells : cells,
|
||||
),
|
||||
);
|
||||
rows.push(types.row.createChecked(null, withHeaderRow && index === 0 ? headerCells : cells));
|
||||
}
|
||||
|
||||
return types.table.createChecked(null, rows);
|
||||
|
||||
+3
-11
@@ -1,13 +1,8 @@
|
||||
import {
|
||||
findParentNodeClosestToPos,
|
||||
KeyboardShortcutCommand,
|
||||
} from "@tiptap/core";
|
||||
import { findParentNodeClosestToPos, KeyboardShortcutCommand } from "@tiptap/core";
|
||||
|
||||
import { isCellSelection } from "./is-cell-selection";
|
||||
|
||||
export const deleteTableWhenAllCellsSelected: KeyboardShortcutCommand = ({
|
||||
editor,
|
||||
}) => {
|
||||
export const deleteTableWhenAllCellsSelected: KeyboardShortcutCommand = ({ editor }) => {
|
||||
const { selection } = editor.state;
|
||||
|
||||
if (!isCellSelection(selection)) {
|
||||
@@ -15,10 +10,7 @@ export const deleteTableWhenAllCellsSelected: KeyboardShortcutCommand = ({
|
||||
}
|
||||
|
||||
let cellCount = 0;
|
||||
const table = findParentNodeClosestToPos(
|
||||
selection.ranges[0].$from,
|
||||
(node) => node.type.name === "table",
|
||||
);
|
||||
const table = findParentNodeClosestToPos(selection.ranges[0].$from, (node) => node.type.name === "table");
|
||||
|
||||
table?.node.descendants((node) => {
|
||||
if (node.type.name === "table") {
|
||||
|
||||
@@ -4,12 +4,7 @@ import { CoreEditorProps } from "../props";
|
||||
import { CoreEditorExtensions } from "../extensions";
|
||||
import { EditorProps } from "@tiptap/pm/view";
|
||||
import { getTrimmedHTML } from "../../lib/utils";
|
||||
import {
|
||||
DeleteImage,
|
||||
IMentionSuggestion,
|
||||
RestoreImage,
|
||||
UploadImage,
|
||||
} from "@plane/editor-types";
|
||||
import { DeleteImage, IMentionSuggestion, RestoreImage, UploadImage } from "@plane/editor-types";
|
||||
|
||||
interface CustomEditorProps {
|
||||
uploadFile: UploadImage;
|
||||
@@ -20,9 +15,7 @@ interface CustomEditorProps {
|
||||
};
|
||||
deleteFile: DeleteImage;
|
||||
cancelUploadImage?: () => any;
|
||||
setIsSubmitting?: (
|
||||
isSubmitting: "submitting" | "submitted" | "saved",
|
||||
) => void;
|
||||
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void;
|
||||
setShouldShowAlert?: (showAlert: boolean) => void;
|
||||
value: string;
|
||||
debouncedUpdatesEnabled?: boolean;
|
||||
@@ -66,12 +59,11 @@ export const useEditor = ({
|
||||
},
|
||||
deleteFile,
|
||||
restoreFile,
|
||||
cancelUploadImage,
|
||||
cancelUploadImage
|
||||
),
|
||||
...extensions,
|
||||
],
|
||||
content:
|
||||
typeof value === "string" && value.trim() !== "" ? value : "<p></p>",
|
||||
content: typeof value === "string" && value.trim() !== "" ? value : "<p></p>",
|
||||
onCreate: async ({ editor }) => {
|
||||
onStart?.(editor.getJSON(), getTrimmedHTML(editor.getHTML()));
|
||||
},
|
||||
@@ -82,7 +74,7 @@ export const useEditor = ({
|
||||
onChange?.(editor.getJSON(), getTrimmedHTML(editor.getHTML()));
|
||||
},
|
||||
},
|
||||
[rerenderOnPropsChange],
|
||||
[rerenderOnPropsChange]
|
||||
);
|
||||
|
||||
const editorRef: MutableRefObject<Editor | null> = useRef(null);
|
||||
|
||||
@@ -30,8 +30,7 @@ export const useReadOnlyEditor = ({
|
||||
const editor = useCustomEditor(
|
||||
{
|
||||
editable: false,
|
||||
content:
|
||||
typeof value === "string" && value.trim() !== "" ? value : "<p></p>",
|
||||
content: typeof value === "string" && value.trim() !== "" ? value : "<p></p>",
|
||||
editorProps: {
|
||||
...CoreReadOnlyEditorProps,
|
||||
...editorProps,
|
||||
@@ -44,7 +43,7 @@ export const useReadOnlyEditor = ({
|
||||
...extensions,
|
||||
],
|
||||
},
|
||||
[rerenderOnPropsChange],
|
||||
[rerenderOnPropsChange]
|
||||
);
|
||||
|
||||
const editorRef: MutableRefObject<Editor | null> = useRef(null);
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
"use client";
|
||||
import * as React from "react";
|
||||
import { Extension } from "@tiptap/react";
|
||||
import { getEditorClassNames } from "../lib/utils";
|
||||
import { EditorProps } from "@tiptap/pm/view";
|
||||
import { useEditor } from "./hooks/use-editor";
|
||||
import { EditorContainer } from "../ui/components/editor-container";
|
||||
import { EditorContentWrapper } from "../ui/components/editor-content";
|
||||
import {
|
||||
UploadImage,
|
||||
DeleteImage,
|
||||
IMentionSuggestion,
|
||||
} from "@plane/editor-types";
|
||||
|
||||
interface ICoreEditor {
|
||||
value: string;
|
||||
uploadFile: UploadImage;
|
||||
deleteFile: DeleteImage;
|
||||
noBorder?: boolean;
|
||||
borderOnFocus?: boolean;
|
||||
customClassName?: string;
|
||||
editorContentCustomClassNames?: string;
|
||||
onChange?: (json: any, html: string) => void;
|
||||
setIsSubmitting?: (
|
||||
isSubmitting: "submitting" | "submitted" | "saved",
|
||||
) => void;
|
||||
setShouldShowAlert?: (showAlert: boolean) => void;
|
||||
editable?: boolean;
|
||||
forwardedRef?: any;
|
||||
debouncedUpdatesEnabled?: boolean;
|
||||
accessValue: string;
|
||||
onAccessChange: (accessKey: string) => void;
|
||||
commentAccess: {
|
||||
icon: string;
|
||||
key: string;
|
||||
label: "Private" | "Public";
|
||||
}[];
|
||||
mentionHighlights?: string[];
|
||||
mentionSuggestions?: IMentionSuggestion[];
|
||||
extensions?: Extension[];
|
||||
editorProps?: EditorProps;
|
||||
}
|
||||
|
||||
interface EditorCoreProps extends ICoreEditor {
|
||||
forwardedRef?: React.Ref<EditorHandle>;
|
||||
}
|
||||
|
||||
interface EditorHandle {
|
||||
clearEditor: () => void;
|
||||
setEditorValue: (content: string) => void;
|
||||
}
|
||||
|
||||
const CoreEditor = ({
|
||||
onChange,
|
||||
debouncedUpdatesEnabled,
|
||||
editable,
|
||||
setIsSubmitting,
|
||||
setShouldShowAlert,
|
||||
editorContentCustomClassNames,
|
||||
value,
|
||||
uploadFile,
|
||||
deleteFile,
|
||||
noBorder,
|
||||
borderOnFocus,
|
||||
customClassName,
|
||||
forwardedRef,
|
||||
}: EditorCoreProps) => {
|
||||
const editor = useEditor({
|
||||
onChange,
|
||||
debouncedUpdatesEnabled,
|
||||
setIsSubmitting,
|
||||
setShouldShowAlert,
|
||||
value,
|
||||
uploadFile,
|
||||
deleteFile,
|
||||
forwardedRef,
|
||||
});
|
||||
|
||||
const editorClassNames = getEditorClassNames({
|
||||
noBorder,
|
||||
borderOnFocus,
|
||||
customClassName,
|
||||
});
|
||||
|
||||
if (!editor) return null;
|
||||
|
||||
return (
|
||||
<EditorContainer editor={editor} editorClassNames={editorClassNames}>
|
||||
<div className="flex flex-col">
|
||||
<EditorContentWrapper
|
||||
editor={editor}
|
||||
editorContentCustomClassNames={editorContentCustomClassNames}
|
||||
/>
|
||||
</div>
|
||||
</EditorContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const CoreEditorWithRef = React.forwardRef<EditorHandle, ICoreEditor>(
|
||||
(props, ref) => <CoreEditor {...props} forwardedRef={ref} />,
|
||||
);
|
||||
|
||||
CoreEditorWithRef.displayName = "CoreEditorWithRef";
|
||||
|
||||
export { CoreEditor, CoreEditorWithRef };
|
||||
@@ -1,21 +1,10 @@
|
||||
import { IMentionSuggestion } from "@plane/editor-types";
|
||||
import { Editor } from "@tiptap/react";
|
||||
import React, {
|
||||
forwardRef,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useState,
|
||||
} from "react";
|
||||
import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useState } from "react";
|
||||
|
||||
interface MentionListProps {
|
||||
items: IMentionSuggestion[];
|
||||
command: (item: {
|
||||
id: string;
|
||||
label: string;
|
||||
target: string;
|
||||
redirect_uri: string;
|
||||
}) => void;
|
||||
command: (item: { id: string; label: string; target: string; redirect_uri: string }) => void;
|
||||
editor: Editor;
|
||||
}
|
||||
|
||||
@@ -37,9 +26,7 @@ const MentionList = forwardRef((props: MentionListProps, ref) => {
|
||||
};
|
||||
|
||||
const upHandler = () => {
|
||||
setSelectedIndex(
|
||||
(selectedIndex + props.items.length - 1) % props.items.length,
|
||||
);
|
||||
setSelectedIndex((selectedIndex + props.items.length - 1) % props.items.length);
|
||||
};
|
||||
|
||||
const downHandler = () => {
|
||||
@@ -76,31 +63,27 @@ const MentionList = forwardRef((props: MentionListProps, ref) => {
|
||||
}));
|
||||
|
||||
return props.items && props.items.length !== 0 ? (
|
||||
<div className="mentions absolute max-h-40 bg-custom-background-100 rounded-md shadow-custom-shadow-sm text-custom-text-300 text-sm overflow-y-auto w-48 p-1 space-y-0.5">
|
||||
<div className="mentions absolute max-h-40 w-48 space-y-0.5 overflow-y-auto rounded-md bg-custom-background-100 p-1 text-sm text-custom-text-300 shadow-custom-shadow-sm">
|
||||
{props.items.length ? (
|
||||
props.items.map((item, index) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className={`flex items-center gap-2 rounded p-1 hover:bg-custom-background-80 cursor-pointer ${
|
||||
className={`flex cursor-pointer items-center gap-2 rounded p-1 hover:bg-custom-background-80 ${
|
||||
index === selectedIndex ? "bg-custom-background-80" : ""
|
||||
}`}
|
||||
onClick={() => selectItem(index)}
|
||||
>
|
||||
<div className="flex-shrink-0 h-4 w-4 grid place-items-center overflow-hidden">
|
||||
<div className="grid h-4 w-4 flex-shrink-0 place-items-center overflow-hidden">
|
||||
{item.avatar && item.avatar.trim() !== "" ? (
|
||||
<img
|
||||
src={item.avatar}
|
||||
className="h-full w-full object-cover rounded-sm"
|
||||
alt={item.title}
|
||||
/>
|
||||
<img src={item.avatar} className="h-full w-full rounded-sm object-cover" alt={item.title} />
|
||||
) : (
|
||||
<div className="h-full w-full grid place-items-center text-xs capitalize text-white rounded-sm bg-gray-700">
|
||||
<div className="grid h-full w-full place-items-center rounded-sm bg-gray-700 text-xs capitalize text-white">
|
||||
{item.title[0]}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-grow space-y-1 truncate">
|
||||
<p className="text-sm font-medium truncate">{item.title}</p>
|
||||
<p className="truncate text-sm font-medium">{item.title}</p>
|
||||
{/* <p className="text-xs text-gray-400">{item.subtitle}</p> */}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,11 +4,7 @@ import suggestion from "./suggestion";
|
||||
import { CustomMention } from "./custom";
|
||||
import { IMentionHighlight, IMentionSuggestion } from "@plane/editor-types";
|
||||
|
||||
export const Mentions = (
|
||||
mentionSuggestions: IMentionSuggestion[],
|
||||
mentionHighlights: IMentionHighlight[],
|
||||
readonly,
|
||||
) =>
|
||||
export const Mentions = (mentionSuggestions: IMentionSuggestion[], mentionHighlights: IMentionHighlight[], readonly) =>
|
||||
CustomMention.configure({
|
||||
HTMLAttributes: {
|
||||
class: "mention",
|
||||
|
||||
@@ -8,8 +8,7 @@ import { IMentionHighlight } from "@plane/editor-types";
|
||||
// eslint-disable-next-line import/no-anonymous-default-export
|
||||
export default (props) => {
|
||||
const router = useRouter();
|
||||
const highlights = props.extension.options
|
||||
.mentionHighlights as IMentionHighlight[];
|
||||
const highlights = props.extension.options.mentionHighlights as IMentionHighlight[];
|
||||
|
||||
const handleClick = () => {
|
||||
if (!props.extension.options.readonly) {
|
||||
@@ -18,18 +17,13 @@ export default (props) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<NodeViewWrapper className="w-fit inline mention-component">
|
||||
<NodeViewWrapper className="mention-component inline w-fit">
|
||||
<span
|
||||
className={cn(
|
||||
"px-1 py-0.5 bg-custom-primary-100/20 text-custom-primary-100 rounded font-medium mention",
|
||||
{
|
||||
"text-yellow-500 bg-yellow-500/20": highlights
|
||||
? highlights.includes(props.node.attrs.id)
|
||||
: false,
|
||||
"cursor-pointer": !props.extension.options.readonly,
|
||||
// "hover:bg-custom-primary-300" : !props.extension.options.readonly && !highlights.includes(props.node.attrs.id)
|
||||
},
|
||||
)}
|
||||
className={cn("mention rounded bg-custom-primary-100/20 px-1 py-0.5 font-medium text-custom-primary-100", {
|
||||
"bg-yellow-500/20 text-yellow-500": highlights ? highlights.includes(props.node.attrs.id) : false,
|
||||
"cursor-pointer": !props.extension.options.readonly,
|
||||
// "hover:bg-custom-primary-300" : !props.extension.options.readonly && !highlights.includes(props.node.attrs.id)
|
||||
})}
|
||||
onClick={handleClick}
|
||||
data-mention-target={props.node.attrs.target}
|
||||
data-mention-id={props.node.attrs.id}
|
||||
|
||||
@@ -7,11 +7,7 @@ import { IMentionSuggestion } from "@plane/editor-types";
|
||||
|
||||
const Suggestion = (suggestions: IMentionSuggestion[]) => ({
|
||||
items: ({ query }: { query: string }) =>
|
||||
suggestions
|
||||
.filter((suggestion) =>
|
||||
suggestion.title.toLowerCase().startsWith(query.toLowerCase()),
|
||||
)
|
||||
.slice(0, 5),
|
||||
suggestions.filter((suggestion) => suggestion.title.toLowerCase().startsWith(query.toLowerCase())).slice(0, 5),
|
||||
render: () => {
|
||||
let reactRenderer: ReactRenderer | null = null;
|
||||
let popup: any | null = null;
|
||||
|
||||
@@ -134,9 +134,7 @@ export const TableItem = (editor: Editor): EditorMenuItem => ({
|
||||
export const ImageItem = (
|
||||
editor: Editor,
|
||||
uploadFile: UploadImage,
|
||||
setIsSubmitting?: (
|
||||
isSubmitting: "submitting" | "submitted" | "saved",
|
||||
) => void,
|
||||
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void
|
||||
): EditorMenuItem => ({
|
||||
name: "image",
|
||||
isActive: () => editor?.isActive("image"),
|
||||
|
||||
@@ -15,11 +15,7 @@ interface ImageNode extends ProseMirrorNode {
|
||||
const TrackImageDeletionPlugin = (deleteImage: DeleteImage): Plugin =>
|
||||
new Plugin({
|
||||
key: deleteKey,
|
||||
appendTransaction: (
|
||||
transactions: readonly Transaction[],
|
||||
oldState: EditorState,
|
||||
newState: EditorState,
|
||||
) => {
|
||||
appendTransaction: (transactions: readonly Transaction[], oldState: EditorState, newState: EditorState) => {
|
||||
const newImageSources = new Set<string>();
|
||||
newState.doc.descendants((node) => {
|
||||
if (node.type.name === IMAGE_NODE_TYPE) {
|
||||
@@ -59,10 +55,7 @@ const TrackImageDeletionPlugin = (deleteImage: DeleteImage): Plugin =>
|
||||
|
||||
export default TrackImageDeletionPlugin;
|
||||
|
||||
export async function onNodeDeleted(
|
||||
src: string,
|
||||
deleteImage: DeleteImage,
|
||||
): Promise<void> {
|
||||
export async function onNodeDeleted(src: string, deleteImage: DeleteImage): Promise<void> {
|
||||
try {
|
||||
const assetUrlWithWorkspaceId = new URL(src).pathname.substring(1);
|
||||
const resStatus = await deleteImage(assetUrlWithWorkspaceId);
|
||||
@@ -74,10 +67,7 @@ export async function onNodeDeleted(
|
||||
}
|
||||
}
|
||||
|
||||
export async function onNodeRestored(
|
||||
src: string,
|
||||
restoreImage: RestoreImage,
|
||||
): Promise<void> {
|
||||
export async function onNodeRestored(src: string, restoreImage: RestoreImage): Promise<void> {
|
||||
try {
|
||||
const assetUrlWithWorkspaceId = new URL(src).pathname.substring(1);
|
||||
const resStatus = await restoreImage(assetUrlWithWorkspaceId);
|
||||
|
||||
@@ -21,10 +21,7 @@ const UploadImagesPlugin = (cancelUploadImage?: () => any) =>
|
||||
const placeholder = document.createElement("div");
|
||||
placeholder.setAttribute("class", "img-placeholder");
|
||||
const image = document.createElement("img");
|
||||
image.setAttribute(
|
||||
"class",
|
||||
"opacity-10 rounded-lg border border-custom-border-300",
|
||||
);
|
||||
image.setAttribute("class", "opacity-10 rounded-lg border border-custom-border-300");
|
||||
image.src = src;
|
||||
placeholder.appendChild(image);
|
||||
|
||||
@@ -42,10 +39,7 @@ const UploadImagesPlugin = (cancelUploadImage?: () => any) =>
|
||||
// Create an SVG element from the SVG string
|
||||
const svgString = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-circle"><circle cx="12" cy="12" r="10"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/></svg>`;
|
||||
const parser = new DOMParser();
|
||||
const svgElement = parser.parseFromString(
|
||||
svgString,
|
||||
"image/svg+xml",
|
||||
).documentElement;
|
||||
const svgElement = parser.parseFromString(svgString, "image/svg+xml").documentElement;
|
||||
|
||||
cancelButton.appendChild(svgElement);
|
||||
placeholder.appendChild(cancelButton);
|
||||
@@ -54,13 +48,7 @@ const UploadImagesPlugin = (cancelUploadImage?: () => any) =>
|
||||
});
|
||||
set = set.add(tr.doc, [deco]);
|
||||
} else if (action && action.remove) {
|
||||
set = set.remove(
|
||||
set.find(
|
||||
undefined,
|
||||
undefined,
|
||||
(spec) => spec.id == action.remove.id,
|
||||
),
|
||||
);
|
||||
set = set.remove(set.find(undefined, undefined, (spec) => spec.id == action.remove.id));
|
||||
}
|
||||
return set;
|
||||
},
|
||||
@@ -76,11 +64,7 @@ export default UploadImagesPlugin;
|
||||
|
||||
function findPlaceholder(state: EditorState, id: {}) {
|
||||
const decos = uploadKey.getState(state);
|
||||
const found = decos.find(
|
||||
undefined,
|
||||
undefined,
|
||||
(spec: { id: number | undefined }) => spec.id == id,
|
||||
);
|
||||
const found = decos.find(undefined, undefined, (spec: { id: number | undefined }) => spec.id == id);
|
||||
return found.length ? found[0].from : null;
|
||||
}
|
||||
|
||||
@@ -96,9 +80,7 @@ export async function startImageUpload(
|
||||
view: EditorView,
|
||||
pos: number,
|
||||
uploadFile: UploadImage,
|
||||
setIsSubmitting?: (
|
||||
isSubmitting: "submitting" | "submitted" | "saved",
|
||||
) => void,
|
||||
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void
|
||||
) {
|
||||
if (!file) {
|
||||
alert("No file selected. Please select a file to upload.");
|
||||
@@ -151,9 +133,7 @@ export async function startImageUpload(
|
||||
const imageSrc = typeof src === "object" ? reader.result : src;
|
||||
|
||||
const node = schema.nodes.image.create({ src: imageSrc });
|
||||
const transaction = view.state.tr
|
||||
.replaceWith(pos, pos, node)
|
||||
.setMeta(uploadKey, { remove: { id } });
|
||||
const transaction = view.state.tr.replaceWith(pos, pos, node).setMeta(uploadKey, { remove: { id } });
|
||||
view.dispatch(transaction);
|
||||
} catch (error) {
|
||||
console.error("Upload error: ", error);
|
||||
@@ -161,10 +141,7 @@ export async function startImageUpload(
|
||||
}
|
||||
}
|
||||
|
||||
const UploadImageHandler = (
|
||||
file: File,
|
||||
uploadFile: UploadImage,
|
||||
): Promise<string> => {
|
||||
const UploadImageHandler = (file: File, uploadFile: UploadImage): Promise<string> => {
|
||||
try {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
|
||||
@@ -5,9 +5,7 @@ import { startImageUpload } from "./plugins/upload-image";
|
||||
|
||||
export function CoreEditorProps(
|
||||
uploadFile: UploadImage,
|
||||
setIsSubmitting?: (
|
||||
isSubmitting: "submitting" | "submitted" | "saved",
|
||||
) => void,
|
||||
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void
|
||||
): EditorProps {
|
||||
return {
|
||||
attributes: {
|
||||
@@ -34,11 +32,7 @@ export function CoreEditorProps(
|
||||
}
|
||||
}
|
||||
}
|
||||
if (
|
||||
event.clipboardData &&
|
||||
event.clipboardData.files &&
|
||||
event.clipboardData.files[0]
|
||||
) {
|
||||
if (event.clipboardData && event.clipboardData.files && event.clipboardData.files[0]) {
|
||||
event.preventDefault();
|
||||
const file = event.clipboardData.files[0];
|
||||
const pos = view.state.selection.from;
|
||||
@@ -57,12 +51,7 @@ export function CoreEditorProps(
|
||||
}
|
||||
}
|
||||
}
|
||||
if (
|
||||
!moved &&
|
||||
event.dataTransfer &&
|
||||
event.dataTransfer.files &&
|
||||
event.dataTransfer.files[0]
|
||||
) {
|
||||
if (!moved && event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files[0]) {
|
||||
event.preventDefault();
|
||||
const file = event.dataTransfer.files[0];
|
||||
const coordinates = view.posAtCoords({
|
||||
@@ -70,13 +59,7 @@ export function CoreEditorProps(
|
||||
top: event.clientY,
|
||||
});
|
||||
if (coordinates) {
|
||||
startImageUpload(
|
||||
file,
|
||||
view,
|
||||
coordinates.pos - 1,
|
||||
uploadFile,
|
||||
setIsSubmitting,
|
||||
);
|
||||
startImageUpload(file, view, coordinates.pos - 1, uploadFile, setIsSubmitting);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -45,8 +45,7 @@ export const CoreReadOnlyEditorExtensions = (mentionConfig: {
|
||||
},
|
||||
code: {
|
||||
HTMLAttributes: {
|
||||
class:
|
||||
"rounded-md bg-custom-primary-30 mx-1 px-1 py-1 font-mono font-medium text-custom-text-1000",
|
||||
class: "rounded-md bg-custom-primary-30 mx-1 px-1 py-1 font-mono font-medium text-custom-text-1000",
|
||||
spellcheck: "false",
|
||||
},
|
||||
},
|
||||
@@ -94,9 +93,5 @@ export const CoreReadOnlyEditorExtensions = (mentionConfig: {
|
||||
TableHeader,
|
||||
TableCell,
|
||||
TableRow,
|
||||
Mentions(
|
||||
mentionConfig.mentionSuggestions,
|
||||
mentionConfig.mentionHighlights,
|
||||
true,
|
||||
),
|
||||
Mentions(mentionConfig.mentionSuggestions, mentionConfig.mentionHighlights, true),
|
||||
];
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ["custom"],
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
.next
|
||||
.vercel
|
||||
.tubro
|
||||
out/
|
||||
dis/
|
||||
build/
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"printWidth": 120,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/document-editor",
|
||||
"version": "0.1.0",
|
||||
"version": "0.14.0",
|
||||
"description": "Package that powers Plane's Pages Editor",
|
||||
"main": "./dist/index.mjs",
|
||||
"module": "./dist/index.mjs",
|
||||
@@ -36,9 +36,6 @@
|
||||
"@tiptap/extension-placeholder": "^2.1.11",
|
||||
"@tiptap/pm": "^2.1.12",
|
||||
"@tiptap/suggestion": "^2.1.12",
|
||||
"@types/node": "18.15.3",
|
||||
"@types/react": "^18.2.39",
|
||||
"@types/react-dom": "18.0.11",
|
||||
"eslint": "8.36.0",
|
||||
"eslint-config-next": "13.2.4",
|
||||
"react-popper": "^2.3.0",
|
||||
@@ -63,4 +60,4 @@
|
||||
"nextjs",
|
||||
"react"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
export { DocumentEditor, DocumentEditorWithRef } from "./ui";
|
||||
export {
|
||||
DocumentReadOnlyEditor,
|
||||
DocumentReadOnlyEditorWithRef,
|
||||
} from "./ui/readonly";
|
||||
export { DocumentReadOnlyEditor, DocumentReadOnlyEditorWithRef } from "./ui/readonly";
|
||||
export { FixedMenu } from "./ui/menu/fixed-menu";
|
||||
|
||||
@@ -12,7 +12,7 @@ export const AlertLabel = (props: IAlertLabelProps) => {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`h-7 flex items-center gap-2 font-medium py-0.5 px-3 rounded-full text-xs ${backgroundColor} ${textColor}`}
|
||||
className={`flex h-7 items-center gap-2 rounded-full px-3 py-0.5 text-xs font-medium ${backgroundColor} ${textColor}`}
|
||||
>
|
||||
{Icon && <Icon className="h-3 w-3" />}
|
||||
<span>{label}</span>
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import {
|
||||
HeadingComp,
|
||||
HeadingThreeComp,
|
||||
SubheadingComp,
|
||||
} from "./heading-component";
|
||||
import { HeadingComp, HeadingThreeComp, SubheadingComp } from "./heading-component";
|
||||
import { IMarking } from "..";
|
||||
import { Editor } from "@tiptap/react";
|
||||
import { scrollSummary } from "../utils/editor-summary-utils";
|
||||
@@ -16,32 +12,21 @@ export const ContentBrowser = (props: ContentBrowserProps) => {
|
||||
const { editor, markings } = props;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full overflow-hidden">
|
||||
<div className="flex h-full flex-col overflow-hidden">
|
||||
<h2 className="font-medium">Table of Contents</h2>
|
||||
<div className="h-full overflow-y-auto">
|
||||
{markings.length !== 0 ? (
|
||||
markings.map((marking) =>
|
||||
marking.level === 1 ? (
|
||||
<HeadingComp
|
||||
onClick={() => scrollSummary(editor, marking)}
|
||||
heading={marking.text}
|
||||
/>
|
||||
<HeadingComp onClick={() => scrollSummary(editor, marking)} heading={marking.text} />
|
||||
) : marking.level === 2 ? (
|
||||
<SubheadingComp
|
||||
onClick={() => scrollSummary(editor, marking)}
|
||||
subHeading={marking.text}
|
||||
/>
|
||||
<SubheadingComp onClick={() => scrollSummary(editor, marking)} subHeading={marking.text} />
|
||||
) : (
|
||||
<HeadingThreeComp
|
||||
heading={marking.text}
|
||||
onClick={() => scrollSummary(editor, marking)}
|
||||
/>
|
||||
),
|
||||
<HeadingThreeComp heading={marking.text} onClick={() => scrollSummary(editor, marking)} />
|
||||
)
|
||||
)
|
||||
) : (
|
||||
<p className="mt-3 text-xs text-custom-text-400">
|
||||
Headings will be displayed here for navigation
|
||||
</p>
|
||||
<p className="mt-3 text-xs text-custom-text-400">Headings will be displayed here for navigation</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,10 +5,7 @@ import { FixedMenu } from "../menu";
|
||||
import { UploadImage } from "@plane/editor-types";
|
||||
import { DocumentDetails } from "../types/editor-types";
|
||||
import { AlertLabel } from "./alert-label";
|
||||
import {
|
||||
IVerticalDropdownItemProps,
|
||||
VerticalDropdownMenu,
|
||||
} from "./vertical-dropdown-menu";
|
||||
import { IVerticalDropdownItemProps, VerticalDropdownMenu } from "./vertical-dropdown-menu";
|
||||
import { SummaryPopover } from "./summary-popover";
|
||||
import { InfoPopover } from "./info-popover";
|
||||
|
||||
@@ -23,9 +20,7 @@ interface IEditorHeader {
|
||||
archivedAt?: Date;
|
||||
readonly: boolean;
|
||||
uploadFile?: UploadImage;
|
||||
setIsSubmitting?: (
|
||||
isSubmitting: "submitting" | "submitted" | "saved",
|
||||
) => void;
|
||||
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void;
|
||||
documentDetails: DocumentDetails;
|
||||
isSubmitting?: "submitting" | "submitted" | "saved";
|
||||
}
|
||||
@@ -48,8 +43,8 @@ export const EditorHeader = (props: IEditorHeader) => {
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div className="flex items-center border-b border-custom-border-200 py-2 px-5">
|
||||
<div className="flex-shrink-0 w-56 lg:w-72">
|
||||
<div className="flex items-center border-b border-custom-border-200 px-5 py-2">
|
||||
<div className="w-56 flex-shrink-0 lg:w-72">
|
||||
<SummaryPopover
|
||||
editor={editor}
|
||||
markings={markings}
|
||||
@@ -60,15 +55,11 @@ export const EditorHeader = (props: IEditorHeader) => {
|
||||
|
||||
<div className="flex-shrink-0">
|
||||
{!readonly && uploadFile && (
|
||||
<FixedMenu
|
||||
editor={editor}
|
||||
uploadFile={uploadFile}
|
||||
setIsSubmitting={setIsSubmitting}
|
||||
/>
|
||||
<FixedMenu editor={editor} uploadFile={uploadFile} setIsSubmitting={setIsSubmitting} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex-grow flex items-center justify-end gap-3">
|
||||
<div className="flex flex-grow items-center justify-end gap-3">
|
||||
{isLocked && (
|
||||
<AlertLabel
|
||||
Icon={Lock}
|
||||
@@ -88,7 +79,7 @@ export const EditorHeader = (props: IEditorHeader) => {
|
||||
|
||||
{!isLocked && !isArchived ? (
|
||||
<div
|
||||
className={`flex absolute right-[120px] transition-all duration-300 items-center gap-x-2 ${
|
||||
className={`absolute right-[120px] flex items-center gap-x-2 transition-all duration-300 ${
|
||||
isSubmitting === "saved" ? "fadeOut" : "fadeIn"
|
||||
}`}
|
||||
>
|
||||
|
||||
@@ -23,7 +23,7 @@ export const SubheadingComp = ({
|
||||
}) => (
|
||||
<p
|
||||
onClick={onClick}
|
||||
className="ml-6 mt-2 text-xs cursor-pointer font-medium tracking-tight text-gray-400 hover:text-custom-primary"
|
||||
className="ml-6 mt-2 cursor-pointer text-xs font-medium tracking-tight text-gray-400 hover:text-custom-primary"
|
||||
role="button"
|
||||
>
|
||||
{subHeading}
|
||||
@@ -39,7 +39,7 @@ export const HeadingThreeComp = ({
|
||||
}) => (
|
||||
<p
|
||||
onClick={onClick}
|
||||
className="ml-8 mt-2 text-xs cursor-pointer font-medium tracking-tight text-gray-400 hover:text-custom-primary"
|
||||
className="ml-8 mt-2 cursor-pointer text-xs font-medium tracking-tight text-gray-400 hover:text-custom-primary"
|
||||
role="button"
|
||||
>
|
||||
{heading}
|
||||
|
||||
@@ -19,10 +19,7 @@ const renderDate = (date: Date): string => {
|
||||
hour12: true,
|
||||
};
|
||||
|
||||
const formattedDate: string = new Intl.DateTimeFormat(
|
||||
"en-US",
|
||||
options,
|
||||
).format(date);
|
||||
const formattedDate: string = new Intl.DateTimeFormat("en-US", options).format(date);
|
||||
|
||||
return formattedDate;
|
||||
};
|
||||
@@ -32,42 +29,35 @@ export const InfoPopover: React.FC<Props> = (props) => {
|
||||
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false);
|
||||
|
||||
const [referenceElement, setReferenceElement] =
|
||||
useState<HTMLButtonElement | null>(null);
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(
|
||||
null,
|
||||
);
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||
|
||||
const { styles: infoPopoverStyles, attributes: infoPopoverAttributes } =
|
||||
usePopper(referenceElement, popperElement, {
|
||||
placement: "bottom-start",
|
||||
});
|
||||
const { styles: infoPopoverStyles, attributes: infoPopoverAttributes } = usePopper(referenceElement, popperElement, {
|
||||
placement: "bottom-start",
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
onMouseEnter={() => setIsPopoverOpen(true)}
|
||||
onMouseLeave={() => setIsPopoverOpen(false)}
|
||||
>
|
||||
<div onMouseEnter={() => setIsPopoverOpen(true)} onMouseLeave={() => setIsPopoverOpen(false)}>
|
||||
<button type="button" ref={setReferenceElement} className="block">
|
||||
<Info className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
{isPopoverOpen && (
|
||||
<div
|
||||
className="z-10 w-64 shadow-custom-shadow-rg rounded border-[0.5px] border-custom-border-200 bg-custom-background-100 p-3 space-y-2.5"
|
||||
className="z-10 w-64 space-y-2.5 rounded border-[0.5px] border-custom-border-200 bg-custom-background-100 p-3 shadow-custom-shadow-rg"
|
||||
ref={setPopperElement}
|
||||
style={infoPopoverStyles.popper}
|
||||
{...infoPopoverAttributes.popper}
|
||||
>
|
||||
<div className="space-y-1.5">
|
||||
<h6 className="text-custom-text-400 text-xs">Last updated on</h6>
|
||||
<h5 className="text-sm flex items-center gap-1">
|
||||
<h6 className="text-xs text-custom-text-400">Last updated on</h6>
|
||||
<h5 className="flex items-center gap-1 text-sm">
|
||||
<History className="h-3 w-3" />
|
||||
{renderDate(new Date(documentDetails.last_updated_at))}
|
||||
</h5>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<h6 className="text-custom-text-400 text-xs">Created on</h6>
|
||||
<h5 className="text-sm flex items-center gap-1">
|
||||
<h6 className="text-xs text-custom-text-400">Created on</h6>
|
||||
<h5 className="flex items-center gap-1 text-sm">
|
||||
<Calendar className="h-3 w-3" />
|
||||
{renderDate(new Date(documentDetails.created_on))}
|
||||
</h5>
|
||||
|
||||
@@ -25,14 +25,7 @@ const debounce = (func: (...args: any[]) => void, wait: number) => {
|
||||
};
|
||||
|
||||
export const PageRenderer = (props: IPageRenderer) => {
|
||||
const {
|
||||
documentDetails,
|
||||
editor,
|
||||
editorClassNames,
|
||||
editorContentCustomClassNames,
|
||||
updatePageTitle,
|
||||
readonly,
|
||||
} = props;
|
||||
const { documentDetails, editor, editorClassNames, editorContentCustomClassNames, updatePageTitle, readonly } = props;
|
||||
|
||||
const [pageTitle, setPagetitle] = useState(documentDetails.title);
|
||||
|
||||
@@ -44,27 +37,24 @@ export const PageRenderer = (props: IPageRenderer) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full pl-7 pt-5 pb-64">
|
||||
<div className="w-full pb-64 pl-7 pt-5">
|
||||
{!readonly ? (
|
||||
<input
|
||||
onChange={(e) => handlePageTitleChange(e.target.value)}
|
||||
className="text-4xl bg-custom-background font-bold break-words pr-5 -mt-2 w-full border-none outline-none"
|
||||
className="-mt-2 w-full break-words border-none bg-custom-background pr-5 text-4xl font-bold outline-none"
|
||||
value={pageTitle}
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
onChange={(e) => handlePageTitleChange(e.target.value)}
|
||||
className="text-4xl bg-custom-background font-bold break-words pr-5 -mt-2 w-full border-none outline-none overflow-x-clip"
|
||||
className="-mt-2 w-full overflow-x-clip break-words border-none bg-custom-background pr-5 text-4xl font-bold outline-none"
|
||||
value={pageTitle}
|
||||
disabled
|
||||
/>
|
||||
)}
|
||||
<div className="flex flex-col h-full w-full pr-5">
|
||||
<div className="flex h-full w-full flex-col pr-5">
|
||||
<EditorContainer editor={editor} editorClassNames={editorClassNames}>
|
||||
<EditorContentWrapper
|
||||
editor={editor}
|
||||
editorContentCustomClassNames={editorContentCustomClassNames}
|
||||
/>
|
||||
<EditorContentWrapper editor={editor} editorContentCustomClassNames={editorContentCustomClassNames} />
|
||||
</EditorContainer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -17,26 +17,24 @@ type Props = {
|
||||
export const SummaryPopover: React.FC<Props> = (props) => {
|
||||
const { editor, markings, sidePeekVisible, setSidePeekVisible } = props;
|
||||
|
||||
const [referenceElement, setReferenceElement] =
|
||||
useState<HTMLButtonElement | null>(null);
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(
|
||||
null,
|
||||
);
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||
|
||||
const { styles: summaryPopoverStyles, attributes: summaryPopoverAttributes } =
|
||||
usePopper(referenceElement, popperElement, {
|
||||
const { styles: summaryPopoverStyles, attributes: summaryPopoverAttributes } = usePopper(
|
||||
referenceElement,
|
||||
popperElement,
|
||||
{
|
||||
placement: "bottom-start",
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="group/summary-popover w-min whitespace-nowrap">
|
||||
<button
|
||||
type="button"
|
||||
ref={setReferenceElement}
|
||||
className={`h-7 w-7 grid place-items-center rounded ${
|
||||
sidePeekVisible
|
||||
? "bg-custom-primary-100/20 text-custom-primary-100"
|
||||
: "text-custom-text-300"
|
||||
className={`grid h-7 w-7 place-items-center rounded ${
|
||||
sidePeekVisible ? "bg-custom-primary-100/20 text-custom-primary-100" : "text-custom-text-300"
|
||||
}`}
|
||||
onClick={() => setSidePeekVisible(!sidePeekVisible)}
|
||||
>
|
||||
@@ -44,7 +42,7 @@ export const SummaryPopover: React.FC<Props> = (props) => {
|
||||
</button>
|
||||
{!sidePeekVisible && (
|
||||
<div
|
||||
className="hidden group-hover/summary-popover:block z-10 max-h-80 w-64 shadow-custom-shadow-rg rounded border-[0.5px] border-custom-border-200 bg-custom-background-100 p-3 overflow-y-auto"
|
||||
className="z-10 hidden max-h-80 w-64 overflow-y-auto rounded border-[0.5px] border-custom-border-200 bg-custom-background-100 p-3 shadow-custom-shadow-rg group-hover/summary-popover:block"
|
||||
ref={setPopperElement}
|
||||
style={summaryPopoverStyles.popper}
|
||||
{...summaryPopoverAttributes.popper}
|
||||
|
||||
@@ -8,14 +8,10 @@ interface ISummarySideBarProps {
|
||||
sidePeekVisible: boolean;
|
||||
}
|
||||
|
||||
export const SummarySideBar = ({
|
||||
editor,
|
||||
markings,
|
||||
sidePeekVisible,
|
||||
}: ISummarySideBarProps) => {
|
||||
export const SummarySideBar = ({ editor, markings, sidePeekVisible }: ISummarySideBarProps) => {
|
||||
return (
|
||||
<div
|
||||
className={`h-full p-5 transition-all duration-200 transform overflow-hidden ${
|
||||
className={`h-full transform overflow-hidden p-5 transition-all duration-200 ${
|
||||
sidePeekVisible ? "translate-x-0" : "-translate-x-full"
|
||||
}`}
|
||||
>
|
||||
|
||||
@@ -23,11 +23,7 @@ export interface IVerticalDropdownMenuProps {
|
||||
items: IVerticalDropdownItemProps[];
|
||||
}
|
||||
|
||||
const VerticalDropdownItem = ({
|
||||
Icon,
|
||||
label,
|
||||
action,
|
||||
}: IVerticalDropdownItemProps) => {
|
||||
const VerticalDropdownItem = ({ Icon, label, action }: IVerticalDropdownItemProps) => {
|
||||
return (
|
||||
<CustomMenu.MenuItem onClick={action} className="flex items-center gap-2">
|
||||
<Icon className="h-3 w-3" />
|
||||
@@ -42,19 +38,11 @@ export const VerticalDropdownMenu = ({ items }: IVerticalDropdownMenuProps) => {
|
||||
maxHeight={"md"}
|
||||
className={"h-4.5 mt-1"}
|
||||
placement={"bottom-start"}
|
||||
optionsClassName={
|
||||
"border-custom-border border-r border-solid transition-all duration-200 ease-in-out "
|
||||
}
|
||||
optionsClassName={"border-custom-border border-r border-solid transition-all duration-200 ease-in-out "}
|
||||
customButton={<MoreVertical size={14} />}
|
||||
>
|
||||
{items.map((item, index) => (
|
||||
<VerticalDropdownItem
|
||||
key={index}
|
||||
type={item.type}
|
||||
Icon={item.Icon}
|
||||
label={item.label}
|
||||
action={item.action}
|
||||
/>
|
||||
<VerticalDropdownItem key={index} type={item.type} Icon={item.Icon} label={item.label} action={item.action} />
|
||||
))}
|
||||
</CustomMenu>
|
||||
);
|
||||
|
||||
@@ -11,23 +11,22 @@ import { LayersIcon } from "@plane/ui";
|
||||
export const DocumentEditorExtensions = (
|
||||
uploadFile: UploadImage,
|
||||
issueEmbedConfig?: IIssueEmbedConfig,
|
||||
setIsSubmitting?: (
|
||||
isSubmitting: "submitting" | "submitted" | "saved",
|
||||
) => void,
|
||||
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void
|
||||
) => {
|
||||
const additonalOptions: ISlashCommandItem[] = [
|
||||
const additionalOptions: ISlashCommandItem[] = [
|
||||
{
|
||||
title: "Issue Embed",
|
||||
description: "Embed an issue from the project",
|
||||
searchTerms: ["Issue", "Iss"],
|
||||
icon: <LayersIcon height={"20px"} width={"20px"} />,
|
||||
key: "issue_embed",
|
||||
title: "Issue embed",
|
||||
description: "Embed an issue from the project.",
|
||||
searchTerms: ["issue", "link", "embed"],
|
||||
icon: <LayersIcon className="h-3.5 w-3.5" />,
|
||||
command: ({ editor, range }) => {
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.insertContentAt(
|
||||
range,
|
||||
"<p class='text-sm bg-gray-300 w-fit pl-3 pr-3 pt-1 pb-1 rounded shadow-sm'>#issue_</p>",
|
||||
"<p class='text-sm bg-gray-300 w-fit pl-3 pr-3 pt-1 pb-1 rounded shadow-sm'>#issue_</p>"
|
||||
)
|
||||
.run();
|
||||
},
|
||||
@@ -35,7 +34,7 @@ export const DocumentEditorExtensions = (
|
||||
];
|
||||
|
||||
return [
|
||||
SlashCommand(uploadFile, setIsSubmitting, additonalOptions),
|
||||
SlashCommand(uploadFile, setIsSubmitting, additionalOptions),
|
||||
DragAndDrop,
|
||||
Placeholder.configure({
|
||||
placeholder: ({ node }) => {
|
||||
|
||||
+26
-28
@@ -18,34 +18,32 @@ export interface IIssueListSuggestion {
|
||||
}
|
||||
|
||||
export const IssueSuggestions = (suggestions: any[]) => {
|
||||
const mappedSuggestions: IIssueListSuggestion[] = suggestions.map(
|
||||
(suggestion): IIssueListSuggestion => {
|
||||
let transactionId = uuidv4();
|
||||
return {
|
||||
title: suggestion.name,
|
||||
priority: suggestion.priority.toString(),
|
||||
identifier: `${suggestion.project_detail.identifier}-${suggestion.sequence_id}`,
|
||||
state: suggestion.state_detail.name,
|
||||
command: ({ editor, range }) => {
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.insertContentAt(range, {
|
||||
type: "issue-embed-component",
|
||||
attrs: {
|
||||
entity_identifier: suggestion.id,
|
||||
id: transactionId,
|
||||
title: suggestion.name,
|
||||
project_identifier: suggestion.project_detail.identifier,
|
||||
sequence_id: suggestion.sequence_id,
|
||||
entity_name: "issue",
|
||||
},
|
||||
})
|
||||
.run();
|
||||
},
|
||||
};
|
||||
},
|
||||
);
|
||||
const mappedSuggestions: IIssueListSuggestion[] = suggestions.map((suggestion): IIssueListSuggestion => {
|
||||
let transactionId = uuidv4();
|
||||
return {
|
||||
title: suggestion.name,
|
||||
priority: suggestion.priority.toString(),
|
||||
identifier: `${suggestion.project_detail.identifier}-${suggestion.sequence_id}`,
|
||||
state: suggestion.state_detail.name,
|
||||
command: ({ editor, range }) => {
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.insertContentAt(range, {
|
||||
type: "issue-embed-component",
|
||||
attrs: {
|
||||
entity_identifier: suggestion.id,
|
||||
id: transactionId,
|
||||
title: suggestion.name,
|
||||
project_identifier: suggestion.project_detail.identifier,
|
||||
sequence_id: suggestion.sequence_id,
|
||||
entity_name: "issue",
|
||||
},
|
||||
})
|
||||
.run();
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return IssueEmbedSuggestions.configure({
|
||||
suggestion: {
|
||||
|
||||
+1
-9
@@ -9,15 +9,7 @@ export const IssueEmbedSuggestions = Extension.create({
|
||||
addOptions() {
|
||||
return {
|
||||
suggestion: {
|
||||
command: ({
|
||||
editor,
|
||||
range,
|
||||
props,
|
||||
}: {
|
||||
editor: Editor;
|
||||
range: Range;
|
||||
props: any;
|
||||
}) => {
|
||||
command: ({ editor, range, props }: { editor: Editor; range: Range; props: any }) => {
|
||||
props.command({ editor, range });
|
||||
},
|
||||
},
|
||||
|
||||
+1
-3
@@ -1,8 +1,6 @@
|
||||
import { IIssueListSuggestion } from ".";
|
||||
|
||||
export const getIssueSuggestionItems = (
|
||||
issueSuggestions: Array<IIssueListSuggestion>,
|
||||
) => {
|
||||
export const getIssueSuggestionItems = (issueSuggestions: Array<IIssueListSuggestion>) => {
|
||||
return ({ query }: { query: string }) => {
|
||||
const search = query.toLowerCase();
|
||||
const filteredSuggestions = issueSuggestions.filter((item) => {
|
||||
|
||||
+33
-73
@@ -2,13 +2,7 @@ import { cn } from "@plane/editor-core";
|
||||
import { Editor } from "@tiptap/core";
|
||||
import tippy from "tippy.js";
|
||||
import { ReactRenderer } from "@tiptap/react";
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
|
||||
import { PriorityIcon } from "@plane/ui";
|
||||
|
||||
const updateScrollView = (container: HTMLElement, item: HTMLElement) => {
|
||||
@@ -62,9 +56,7 @@ const IssueSuggestionList = ({
|
||||
let newDisplayedItems: { [key: string]: IssueSuggestionProps[] } = {};
|
||||
let totalLength = 0;
|
||||
sections.forEach((section) => {
|
||||
newDisplayedItems[section] = items
|
||||
.filter((item) => item.state === section)
|
||||
.slice(0, 5);
|
||||
newDisplayedItems[section] = items.filter((item) => item.state === section).slice(0, 5);
|
||||
|
||||
totalLength += newDisplayedItems[section].length;
|
||||
});
|
||||
@@ -79,7 +71,7 @@ const IssueSuggestionList = ({
|
||||
command(item);
|
||||
}
|
||||
},
|
||||
[command, displayedItems, currentSection],
|
||||
[command, displayedItems, currentSection]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -93,22 +85,17 @@ const IssueSuggestionList = ({
|
||||
// }
|
||||
if (e.key === "ArrowUp") {
|
||||
setSelectedIndex(
|
||||
(selectedIndex + displayedItems[currentSection].length - 1) %
|
||||
displayedItems[currentSection].length,
|
||||
(selectedIndex + displayedItems[currentSection].length - 1) % displayedItems[currentSection].length
|
||||
);
|
||||
return true;
|
||||
}
|
||||
if (e.key === "ArrowDown") {
|
||||
const nextIndex =
|
||||
(selectedIndex + 1) % displayedItems[currentSection].length;
|
||||
const nextIndex = (selectedIndex + 1) % displayedItems[currentSection].length;
|
||||
setSelectedIndex(nextIndex);
|
||||
if (nextIndex === 4) {
|
||||
const nextItems = items
|
||||
.filter((item) => item.state === currentSection)
|
||||
.slice(
|
||||
displayedItems[currentSection].length,
|
||||
displayedItems[currentSection].length + 5,
|
||||
);
|
||||
.slice(displayedItems[currentSection].length, displayedItems[currentSection].length + 5);
|
||||
setDisplayedItems((prevItems) => ({
|
||||
...prevItems,
|
||||
[currentSection]: [...prevItems[currentSection], ...nextItems],
|
||||
@@ -138,29 +125,17 @@ const IssueSuggestionList = ({
|
||||
return () => {
|
||||
document.removeEventListener("keydown", onKeyDown);
|
||||
};
|
||||
}, [
|
||||
displayedItems,
|
||||
selectedIndex,
|
||||
setSelectedIndex,
|
||||
selectItem,
|
||||
currentSection,
|
||||
]);
|
||||
}, [displayedItems, selectedIndex, setSelectedIndex, selectItem, currentSection]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const container = commandListContainer?.current;
|
||||
if (container) {
|
||||
const sectionContainer = container?.querySelector(
|
||||
`#${currentSection}-container`,
|
||||
) as HTMLDivElement;
|
||||
const sectionContainer = container?.querySelector(`#${currentSection}-container`) as HTMLDivElement;
|
||||
if (sectionContainer) {
|
||||
updateScrollView(container, sectionContainer);
|
||||
}
|
||||
const sectionScrollContainer = container?.querySelector(
|
||||
`#${currentSection}`,
|
||||
) as HTMLElement;
|
||||
const item = sectionScrollContainer?.children[
|
||||
selectedIndex
|
||||
] as HTMLElement;
|
||||
const sectionScrollContainer = container?.querySelector(`#${currentSection}`) as HTMLElement;
|
||||
const item = sectionScrollContainer?.children[selectedIndex] as HTMLElement;
|
||||
if (item && sectionScrollContainer) {
|
||||
updateScrollView(sectionScrollContainer, item);
|
||||
}
|
||||
@@ -171,56 +146,41 @@ const IssueSuggestionList = ({
|
||||
<div
|
||||
id="issue-list-container"
|
||||
ref={commandListContainer}
|
||||
className="z-[10] fixed max-h-80 w-60 overflow-y-auto overflow-x-hidden rounded-md border border-custom-border-100 bg-custom-background-100 px-1 shadow-custom-shadow-xs transition-all"
|
||||
className="fixed z-[10] max-h-80 w-60 overflow-y-auto overflow-x-hidden rounded-md border border-custom-border-100 bg-custom-background-100 px-1 shadow-custom-shadow-xs transition-all"
|
||||
>
|
||||
{sections.map((section) => {
|
||||
const sectionItems = displayedItems[section];
|
||||
return (
|
||||
sectionItems &&
|
||||
sectionItems.length > 0 && (
|
||||
<div
|
||||
className={"h-full w-full flex flex-col"}
|
||||
key={`${section}-container`}
|
||||
id={`${section}-container`}
|
||||
>
|
||||
<div className={"flex h-full w-full flex-col"} key={`${section}-container`} id={`${section}-container`}>
|
||||
<h6
|
||||
className={
|
||||
"sticky top-0 z-[10] bg-custom-background-100 text-xs text-custom-text-400 font-medium px-2 py-1"
|
||||
"sticky top-0 z-[10] bg-custom-background-100 px-2 py-1 text-xs font-medium text-custom-text-400"
|
||||
}
|
||||
>
|
||||
{section}
|
||||
</h6>
|
||||
<div
|
||||
key={section}
|
||||
id={section}
|
||||
className={"max-h-[140px] overflow-y-scroll overflow-x-hidden"}
|
||||
>
|
||||
{sectionItems.map(
|
||||
(item: IssueSuggestionProps, index: number) => (
|
||||
<button
|
||||
className={cn(
|
||||
`flex w-full items-center space-x-2 rounded-md px-2 py-1 text-left text-sm text-custom-text-200 hover:bg-custom-primary-100/5 hover:text-custom-text-100`,
|
||||
{
|
||||
"bg-custom-primary-100/5 text-custom-text-100":
|
||||
section === currentSection &&
|
||||
index === selectedIndex,
|
||||
},
|
||||
)}
|
||||
key={index}
|
||||
onClick={() => selectItem(index)}
|
||||
>
|
||||
<h5 className="text-xs text-custom-text-300 whitespace-nowrap">
|
||||
{item.identifier}
|
||||
</h5>
|
||||
<PriorityIcon priority={item.priority} />
|
||||
<div>
|
||||
<p className="flex-grow text-xs truncate">
|
||||
{item.title}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
),
|
||||
)}
|
||||
<div key={section} id={section} className={"max-h-[140px] overflow-x-hidden overflow-y-scroll"}>
|
||||
{sectionItems.map((item: IssueSuggestionProps, index: number) => (
|
||||
<button
|
||||
className={cn(
|
||||
`flex w-full items-center space-x-2 rounded-md px-2 py-1 text-left text-sm text-custom-text-200 hover:bg-custom-primary-100/5 hover:text-custom-text-100`,
|
||||
{
|
||||
"bg-custom-primary-100/5 text-custom-text-100":
|
||||
section === currentSection && index === selectedIndex,
|
||||
}
|
||||
)}
|
||||
key={index}
|
||||
onClick={() => selectItem(index)}
|
||||
>
|
||||
<h5 className="whitespace-nowrap text-xs text-custom-text-300">{item.identifier}</h5>
|
||||
<PriorityIcon priority={item.priority} />
|
||||
<div>
|
||||
<p className="flex-grow truncate text-xs">{item.title}</p>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
+4
-5
@@ -5,8 +5,7 @@ interface IssueWidgetExtensionProps {
|
||||
issueEmbedConfig?: IIssueEmbedConfig;
|
||||
}
|
||||
|
||||
export const IssueWidgetExtension = ({
|
||||
issueEmbedConfig,
|
||||
}: IssueWidgetExtensionProps) => IssueWidget.configure({
|
||||
issueEmbedConfig,
|
||||
});
|
||||
export const IssueWidgetExtension = ({ issueEmbedConfig }: IssueWidgetExtensionProps) =>
|
||||
IssueWidget.configure({
|
||||
issueEmbedConfig,
|
||||
});
|
||||
|
||||
+9
-18
@@ -30,15 +30,13 @@ const IssueWidgetCard = (props) => {
|
||||
{loading == 0 ? (
|
||||
<div
|
||||
onClick={completeIssueEmbedAction}
|
||||
className="cursor-pointer w-full space-y-2 border-[0.5px] border-custom-border-200 rounded-md p-3 shadow-custom-shadow-2xs"
|
||||
className="w-full cursor-pointer space-y-2 rounded-md border-[0.5px] border-custom-border-200 p-3 shadow-custom-shadow-2xs"
|
||||
>
|
||||
<h5 className="text-xs text-custom-text-300">
|
||||
{issueDetails.project_detail.identifier}-{issueDetails.sequence_id}
|
||||
</h5>
|
||||
<h4 className="break-words text-sm font-medium">
|
||||
{issueDetails.name}
|
||||
</h4>
|
||||
<div className="flex items-center flex-wrap gap-x-3 gap-y-2">
|
||||
<h4 className="break-words text-sm font-medium">{issueDetails.name}</h4>
|
||||
<div className="flex flex-wrap items-center gap-x-3 gap-y-2">
|
||||
<div>
|
||||
<PriorityIcon priority={issueDetails.priority} />
|
||||
</div>
|
||||
@@ -46,18 +44,13 @@ const IssueWidgetCard = (props) => {
|
||||
<AvatarGroup size="sm">
|
||||
{issueDetails.assignee_details.map((assignee) => {
|
||||
return (
|
||||
<Avatar
|
||||
key={assignee.id}
|
||||
name={assignee.display_name}
|
||||
src={assignee.avatar}
|
||||
className={"m-0"}
|
||||
/>
|
||||
<Avatar key={assignee.id} name={assignee.display_name} src={assignee.avatar} className={"m-0"} />
|
||||
);
|
||||
})}
|
||||
</AvatarGroup>
|
||||
</div>
|
||||
{issueDetails.target_date && (
|
||||
<div className="rounded flex px-2.5 py-1 items-center border-[0.5px] border-custom-border-300 gap-1 text-custom-text-100 text-xs h-5">
|
||||
<div className="flex h-5 items-center gap-1 rounded border-[0.5px] border-custom-border-300 px-2.5 py-1 text-xs text-custom-text-100">
|
||||
<Calendar className="h-3 w-3" strokeWidth={1.5} />
|
||||
{new Date(issueDetails.target_date).toLocaleDateString()}
|
||||
</div>
|
||||
@@ -65,17 +58,15 @@ const IssueWidgetCard = (props) => {
|
||||
</div>
|
||||
</div>
|
||||
) : loading == -1 ? (
|
||||
<div className="flex gap-[8px] items-center pb-[10px] pt-[10px] pl-[13px] rounded border-[#D97706] border-2 bg-[#FFFBEB] text-[#D97706]">
|
||||
<div className="flex items-center gap-[8px] rounded border-2 border-[#D97706] bg-[#FFFBEB] pb-[10px] pl-[13px] pt-[10px] text-[#D97706]">
|
||||
<AlertTriangle color={"#D97706"} />
|
||||
{
|
||||
"This Issue embed is not found in any project. It can no longer be updated or accessed from here."
|
||||
}
|
||||
{"This Issue embed is not found in any project. It can no longer be updated or accessed from here."}
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full space-y-2 border-[0.5px] border-custom-border-200 rounded-md p-3 shadow-custom-shadow-2xs">
|
||||
<div className="w-full space-y-2 rounded-md border-[0.5px] border-custom-border-200 p-3 shadow-custom-shadow-2xs">
|
||||
<Loader className={"px-6"}>
|
||||
<Loader.Item height={"30px"} />
|
||||
<div className={"space-y-2 mt-3"}>
|
||||
<div className={"mt-3 space-y-2"}>
|
||||
<Loader.Item height={"20px"} width={"70%"} />
|
||||
<Loader.Item height={"20px"} width={"60%"} />
|
||||
</div>
|
||||
|
||||
+1
-4
@@ -35,10 +35,7 @@ export const IssueWidget = Node.create({
|
||||
|
||||
addNodeView() {
|
||||
return ReactNodeViewRenderer((props: Object) => (
|
||||
<IssueWidgetCard
|
||||
{...props}
|
||||
issueEmbedConfig={this.options.issueEmbedConfig}
|
||||
/>
|
||||
<IssueWidgetCard {...props} issueEmbedConfig={this.options.issueEmbedConfig} />
|
||||
));
|
||||
},
|
||||
|
||||
|
||||
@@ -5,5 +5,5 @@ export interface IEmbedConfig {
|
||||
export interface IIssueEmbedConfig {
|
||||
fetchIssue: (issueId: string) => Promise<any>;
|
||||
clickAction: (issueId: string, issueTitle: string) => void;
|
||||
issues: Array<any>;
|
||||
issues: Array<any>;
|
||||
}
|
||||
|
||||
@@ -15,21 +15,14 @@ export const useEditorMarkings = () => {
|
||||
nodes.forEach((node) => {
|
||||
if (
|
||||
node.type === "heading" &&
|
||||
(node.attrs.level === 1 ||
|
||||
node.attrs.level === 2 ||
|
||||
node.attrs.level === 3) &&
|
||||
(node.attrs.level === 1 || node.attrs.level === 2 || node.attrs.level === 3) &&
|
||||
node.content
|
||||
) {
|
||||
tempMarkings.push({
|
||||
type: "heading",
|
||||
level: node.attrs.level,
|
||||
text: node.content[0].text,
|
||||
sequence:
|
||||
node.attrs.level === 1
|
||||
? ++h1Sequence
|
||||
: node.attrs.level === 2
|
||||
? ++h2Sequence
|
||||
: ++h3Sequence,
|
||||
sequence: node.attrs.level === 1 ? ++h1Sequence : node.attrs.level === 2 ? ++h2Sequence : ++h3Sequence,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2,11 +2,7 @@
|
||||
import React, { useState } from "react";
|
||||
import { getEditorClassNames, useEditor } from "@plane/editor-core";
|
||||
import { DocumentEditorExtensions } from "./extensions";
|
||||
import {
|
||||
IDuplicationConfig,
|
||||
IPageArchiveConfig,
|
||||
IPageLockConfig,
|
||||
} from "./types/menu-actions";
|
||||
import { IDuplicationConfig, IPageArchiveConfig, IPageLockConfig } from "./types/menu-actions";
|
||||
import { EditorHeader } from "./components/editor-header";
|
||||
import { useEditorMarkings } from "./hooks/use-editor-markings";
|
||||
import { SummarySideBar } from "./components/summary-side-bar";
|
||||
@@ -41,9 +37,7 @@ interface IDocumentEditor {
|
||||
customClassName?: string;
|
||||
editorContentCustomClassNames?: string;
|
||||
onChange: (json: any, html: string) => void;
|
||||
setIsSubmitting?: (
|
||||
isSubmitting: "submitting" | "submitted" | "saved",
|
||||
) => void;
|
||||
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void;
|
||||
setShouldShowAlert?: (showAlert: boolean) => void;
|
||||
forwardedRef?: any;
|
||||
updatePageTitle: (title: string) => Promise<void>;
|
||||
@@ -118,11 +112,7 @@ const DocumentEditor = ({
|
||||
cancelUploadImage,
|
||||
rerenderOnPropsChange,
|
||||
forwardedRef,
|
||||
extensions: DocumentEditorExtensions(
|
||||
uploadFile,
|
||||
embedConfig?.issueEmbedConfig,
|
||||
setIsSubmitting,
|
||||
),
|
||||
extensions: DocumentEditorExtensions(uploadFile, embedConfig?.issueEmbedConfig, setIsSubmitting),
|
||||
});
|
||||
|
||||
if (!editor) {
|
||||
@@ -147,7 +137,7 @@ const DocumentEditor = ({
|
||||
if (!editor) return null;
|
||||
|
||||
return (
|
||||
<div className="h-full w-full flex flex-col overflow-hidden">
|
||||
<div className="flex h-full w-full flex-col overflow-hidden">
|
||||
<EditorHeader
|
||||
readonly={false}
|
||||
KanbanMenuOptions={KanbanMenuOptions}
|
||||
@@ -163,13 +153,9 @@ const DocumentEditor = ({
|
||||
documentDetails={documentDetails}
|
||||
isSubmitting={isSubmitting}
|
||||
/>
|
||||
<div className="h-full w-full flex overflow-y-auto">
|
||||
<div className="flex-shrink-0 h-full w-56 lg:w-72 sticky top-0">
|
||||
<SummarySideBar
|
||||
editor={editor}
|
||||
markings={markings}
|
||||
sidePeekVisible={sidePeekVisible}
|
||||
/>
|
||||
<div className="flex h-full w-full overflow-y-auto">
|
||||
<div className="sticky top-0 h-full w-56 flex-shrink-0 lg:w-72">
|
||||
<SummarySideBar editor={editor} markings={markings} sidePeekVisible={sidePeekVisible} />
|
||||
</div>
|
||||
<div className="h-full w-[calc(100%-14rem)] lg:w-[calc(100%-18rem-18rem)]">
|
||||
<PageRenderer
|
||||
@@ -181,15 +167,15 @@ const DocumentEditor = ({
|
||||
updatePageTitle={updatePageTitle}
|
||||
/>
|
||||
</div>
|
||||
<div className="hidden lg:block flex-shrink-0 w-56 lg:w-72" />
|
||||
<div className="hidden w-56 flex-shrink-0 lg:block lg:w-72" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const DocumentEditorWithRef = React.forwardRef<EditorHandle, IDocumentEditor>(
|
||||
(props, ref) => <DocumentEditor {...props} forwardedRef={ref} />,
|
||||
);
|
||||
const DocumentEditorWithRef = React.forwardRef<EditorHandle, IDocumentEditor>((props, ref) => (
|
||||
<DocumentEditor {...props} forwardedRef={ref} />
|
||||
));
|
||||
|
||||
DocumentEditorWithRef.displayName = "DocumentEditorWithRef";
|
||||
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { Editor } from "@tiptap/react";
|
||||
import { BoldIcon } from "lucide-react";
|
||||
|
||||
import {
|
||||
BoldItem,
|
||||
BulletListItem,
|
||||
@@ -18,22 +16,16 @@ import {
|
||||
HeadingTwoItem,
|
||||
HeadingThreeItem,
|
||||
findTableAncestor,
|
||||
EditorMenuItem,
|
||||
} from "@plane/editor-core";
|
||||
import { UploadImage } from "@plane/editor-types";
|
||||
|
||||
export interface BubbleMenuItem {
|
||||
name: string;
|
||||
isActive: () => boolean;
|
||||
command: () => void;
|
||||
icon: typeof BoldIcon;
|
||||
}
|
||||
export type BubbleMenuItem = EditorMenuItem;
|
||||
|
||||
type EditorBubbleMenuProps = {
|
||||
editor: Editor;
|
||||
uploadFile: UploadImage;
|
||||
setIsSubmitting?: (
|
||||
isSubmitting: "submitting" | "submitted" | "saved",
|
||||
) => void;
|
||||
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void;
|
||||
};
|
||||
|
||||
export const FixedMenu = (props: EditorBubbleMenuProps) => {
|
||||
@@ -49,15 +41,9 @@ export const FixedMenu = (props: EditorBubbleMenuProps) => {
|
||||
StrikeThroughItem(editor),
|
||||
];
|
||||
|
||||
const listItems: BubbleMenuItem[] = [
|
||||
BulletListItem(editor),
|
||||
NumberedListItem(editor),
|
||||
];
|
||||
const listItems: BubbleMenuItem[] = [BulletListItem(editor), NumberedListItem(editor)];
|
||||
|
||||
const userActionItems: BubbleMenuItem[] = [
|
||||
QuoteItem(editor),
|
||||
CodeItem(editor),
|
||||
];
|
||||
const userActionItems: BubbleMenuItem[] = [QuoteItem(editor), CodeItem(editor)];
|
||||
|
||||
function getComplexItems(): BubbleMenuItem[] {
|
||||
const items: BubbleMenuItem[] = [TableItem(editor)];
|
||||
@@ -99,10 +85,10 @@ export const FixedMenu = (props: EditorBubbleMenuProps) => {
|
||||
type="button"
|
||||
onClick={item.command}
|
||||
className={cn(
|
||||
"h-7 w-7 grid place-items-center text-custom-text-300 hover:bg-custom-background-80 rounded",
|
||||
"grid h-7 w-7 place-items-center rounded text-custom-text-300 hover:bg-custom-background-80",
|
||||
{
|
||||
"text-custom-text-100 bg-custom-background-80": item.isActive(),
|
||||
},
|
||||
"bg-custom-background-80 text-custom-text-100": item.isActive(),
|
||||
}
|
||||
)}
|
||||
>
|
||||
<item.icon className="h-4 w-4" />
|
||||
@@ -116,10 +102,10 @@ export const FixedMenu = (props: EditorBubbleMenuProps) => {
|
||||
type="button"
|
||||
onClick={item.command}
|
||||
className={cn(
|
||||
"h-7 w-7 grid place-items-center text-custom-text-300 hover:bg-custom-background-80 rounded",
|
||||
"grid h-7 w-7 place-items-center rounded text-custom-text-300 hover:bg-custom-background-80",
|
||||
{
|
||||
"text-custom-text-100 bg-custom-background-80": item.isActive(),
|
||||
},
|
||||
"bg-custom-background-80 text-custom-text-100": item.isActive(),
|
||||
}
|
||||
)}
|
||||
>
|
||||
<item.icon
|
||||
@@ -137,10 +123,10 @@ export const FixedMenu = (props: EditorBubbleMenuProps) => {
|
||||
type="button"
|
||||
onClick={item.command}
|
||||
className={cn(
|
||||
"h-7 w-7 grid place-items-center text-custom-text-300 hover:bg-custom-background-80 rounded",
|
||||
"grid h-7 w-7 place-items-center rounded text-custom-text-300 hover:bg-custom-background-80",
|
||||
{
|
||||
"text-custom-text-100 bg-custom-background-80": item.isActive(),
|
||||
},
|
||||
"bg-custom-background-80 text-custom-text-100": item.isActive(),
|
||||
}
|
||||
)}
|
||||
>
|
||||
<item.icon
|
||||
@@ -158,10 +144,10 @@ export const FixedMenu = (props: EditorBubbleMenuProps) => {
|
||||
type="button"
|
||||
onClick={item.command}
|
||||
className={cn(
|
||||
"h-7 w-7 grid place-items-center text-custom-text-300 hover:bg-custom-background-80 rounded",
|
||||
"grid h-7 w-7 place-items-center rounded text-custom-text-300 hover:bg-custom-background-80",
|
||||
{
|
||||
"text-custom-text-100 bg-custom-background-80": item.isActive(),
|
||||
},
|
||||
"bg-custom-background-80 text-custom-text-100": item.isActive(),
|
||||
}
|
||||
)}
|
||||
>
|
||||
<item.icon
|
||||
|
||||
@@ -6,9 +6,5 @@ type Props = {
|
||||
};
|
||||
|
||||
export const Icon: React.FC<Props> = ({ iconName, className = "" }) => (
|
||||
<span
|
||||
className={`material-symbols-rounded text-sm leading-5 font-light ${className}`}
|
||||
>
|
||||
{iconName}
|
||||
</span>
|
||||
<span className={`material-symbols-rounded text-sm font-light leading-5 ${className}`}>{iconName}</span>
|
||||
);
|
||||
|
||||
@@ -8,11 +8,7 @@ import { IssueWidgetExtension } from "../extensions/widgets/IssueEmbedWidget";
|
||||
import { IEmbedConfig } from "../extensions/widgets/IssueEmbedWidget/types";
|
||||
import { useEditorMarkings } from "../hooks/use-editor-markings";
|
||||
import { DocumentDetails } from "../types/editor-types";
|
||||
import {
|
||||
IPageArchiveConfig,
|
||||
IPageLockConfig,
|
||||
IDuplicationConfig,
|
||||
} from "../types/menu-actions";
|
||||
import { IPageArchiveConfig, IPageLockConfig, IDuplicationConfig } from "../types/menu-actions";
|
||||
import { getMenuOptions } from "../utils/menu-options";
|
||||
|
||||
interface IDocumentReadOnlyEditor {
|
||||
@@ -67,9 +63,7 @@ const DocumentReadOnlyEditor = ({
|
||||
value,
|
||||
forwardedRef,
|
||||
rerenderOnPropsChange,
|
||||
extensions: [
|
||||
IssueWidgetExtension({ issueEmbedConfig: embedConfig?.issueEmbedConfig }),
|
||||
],
|
||||
extensions: [IssueWidgetExtension({ issueEmbedConfig: embedConfig?.issueEmbedConfig })],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -98,7 +92,7 @@ const DocumentReadOnlyEditor = ({
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="h-full w-full flex flex-col overflow-hidden">
|
||||
<div className="flex h-full w-full flex-col overflow-hidden">
|
||||
<EditorHeader
|
||||
isLocked={!pageLockConfig ? false : pageLockConfig.is_locked}
|
||||
isArchived={!pageArchiveConfig ? false : pageArchiveConfig.is_archived}
|
||||
@@ -111,13 +105,9 @@ const DocumentReadOnlyEditor = ({
|
||||
documentDetails={documentDetails}
|
||||
archivedAt={pageArchiveConfig && pageArchiveConfig.archived_at}
|
||||
/>
|
||||
<div className="h-full w-full flex overflow-y-auto">
|
||||
<div className="flex-shrink-0 h-full w-56 lg:w-80 sticky top-0">
|
||||
<SummarySideBar
|
||||
editor={editor}
|
||||
markings={markings}
|
||||
sidePeekVisible={sidePeekVisible}
|
||||
/>
|
||||
<div className="flex h-full w-full overflow-y-auto">
|
||||
<div className="sticky top-0 h-full w-56 flex-shrink-0 lg:w-80">
|
||||
<SummarySideBar editor={editor} markings={markings} sidePeekVisible={sidePeekVisible} />
|
||||
</div>
|
||||
<div className="h-full w-full">
|
||||
<PageRenderer
|
||||
@@ -128,16 +118,15 @@ const DocumentReadOnlyEditor = ({
|
||||
documentDetails={documentDetails}
|
||||
/>
|
||||
</div>
|
||||
<div className="hidden lg:block flex-shrink-0 w-56 lg:w-80" />
|
||||
<div className="hidden w-56 flex-shrink-0 lg:block lg:w-80" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const DocumentReadOnlyEditorWithRef = forwardRef<
|
||||
EditorHandle,
|
||||
IDocumentReadOnlyEditor
|
||||
>((props, ref) => <DocumentReadOnlyEditor {...props} forwardedRef={ref} />);
|
||||
const DocumentReadOnlyEditorWithRef = forwardRef<EditorHandle, IDocumentReadOnlyEditor>((props, ref) => (
|
||||
<DocumentReadOnlyEditor {...props} forwardedRef={ref} />
|
||||
));
|
||||
|
||||
DocumentReadOnlyEditorWithRef.displayName = "DocumentReadOnlyEditorWithRef";
|
||||
|
||||
|
||||
@@ -51,17 +51,11 @@ export const Tooltip: React.FC<Props> = ({
|
||||
content={
|
||||
<div
|
||||
className={`relative z-50 max-w-xs gap-1 rounded-md p-2 text-xs shadow-md ${
|
||||
theme === "custom"
|
||||
? "bg-custom-background-100 text-custom-text-200"
|
||||
: "bg-black text-gray-400"
|
||||
} break-words overflow-hidden ${className}`}
|
||||
theme === "custom" ? "bg-custom-background-100 text-custom-text-200" : "bg-black text-gray-400"
|
||||
} overflow-hidden break-words ${className}`}
|
||||
>
|
||||
{tooltipHeading && (
|
||||
<h5
|
||||
className={`font-medium ${
|
||||
theme === "custom" ? "text-custom-text-100" : "text-white"
|
||||
}`}
|
||||
>
|
||||
<h5 className={`font-medium ${theme === "custom" ? "text-custom-text-100" : "text-white"}`}>
|
||||
{tooltipHeading}
|
||||
</h5>
|
||||
)}
|
||||
@@ -69,11 +63,7 @@ export const Tooltip: React.FC<Props> = ({
|
||||
</div>
|
||||
}
|
||||
position={position}
|
||||
renderTarget={({
|
||||
isOpen: isTooltipOpen,
|
||||
ref: eleReference,
|
||||
...tooltipProps
|
||||
}) =>
|
||||
renderTarget={({ isOpen: isTooltipOpen, ref: eleReference, ...tooltipProps }) =>
|
||||
React.cloneElement(children, {
|
||||
ref: eleReference,
|
||||
...tooltipProps,
|
||||
|
||||
@@ -12,11 +12,7 @@ import {
|
||||
} from "lucide-react";
|
||||
import { NextRouter } from "next/router";
|
||||
import { IVerticalDropdownItemProps } from "../components/vertical-dropdown-menu";
|
||||
import {
|
||||
IDuplicationConfig,
|
||||
IPageArchiveConfig,
|
||||
IPageLockConfig,
|
||||
} from "../types/menu-actions";
|
||||
import { IDuplicationConfig, IPageArchiveConfig, IPageLockConfig } from "../types/menu-actions";
|
||||
import { copyMarkdownToClipboard, CopyPageLink } from "./menu-actions";
|
||||
|
||||
export interface MenuOptionsProps {
|
||||
@@ -90,8 +86,7 @@ export const getMenuOptions = ({
|
||||
.then(() => {
|
||||
onActionCompleteHandler({
|
||||
title: "Page Copied",
|
||||
message:
|
||||
"Page has been copied as 'Copy of' followed by page title",
|
||||
message: "Page has been copied as 'Copy of' followed by page title",
|
||||
type: "success",
|
||||
});
|
||||
})
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ["custom"],
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
.next
|
||||
.vercel
|
||||
.tubro
|
||||
out/
|
||||
dis/
|
||||
build/
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"printWidth": 120,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/editor-extensions",
|
||||
"version": "0.1.0",
|
||||
"version": "0.14.0",
|
||||
"description": "Package that powers Plane's Editor with extensions",
|
||||
"private": true,
|
||||
"main": "./dist/index.mjs",
|
||||
@@ -28,16 +28,16 @@
|
||||
"react-dom": "18.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tiptap/react": "^2.1.7",
|
||||
"@tiptap/core": "^2.1.7",
|
||||
"@tiptap/suggestion": "^2.0.4",
|
||||
"@plane/editor-types": "*",
|
||||
"@plane/editor-core": "*",
|
||||
"@plane/editor-types": "*",
|
||||
"@tiptap/core": "^2.1.7",
|
||||
"@tiptap/pm": "^2.1.7",
|
||||
"@tiptap/react": "^2.1.7",
|
||||
"@tiptap/suggestion": "^2.0.4",
|
||||
"eslint": "8.36.0",
|
||||
"eslint-config-next": "13.2.4",
|
||||
"lucide-react": "^0.244.0",
|
||||
"tippy.js": "^6.3.7",
|
||||
"@tiptap/pm": "^2.1.7"
|
||||
"lucide-react": "^0.294.0",
|
||||
"tippy.js": "^6.3.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "18.15.3",
|
||||
@@ -57,4 +57,4 @@
|
||||
"nextjs",
|
||||
"react"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,24 +43,22 @@ function absoluteRect(node: Element) {
|
||||
}
|
||||
|
||||
function nodeDOMAtCoords(coords: { x: number; y: number }) {
|
||||
return document
|
||||
.elementsFromPoint(coords.x, coords.y)
|
||||
.find((elem: Element) => {
|
||||
return (
|
||||
elem.parentElement?.matches?.(".ProseMirror") ||
|
||||
elem.matches(
|
||||
[
|
||||
"li",
|
||||
"p:not(:first-child)",
|
||||
"pre",
|
||||
"blockquote",
|
||||
"h1, h2, h3",
|
||||
"[data-type=horizontalRule]",
|
||||
".tableWrapper",
|
||||
].join(", "),
|
||||
)
|
||||
);
|
||||
});
|
||||
return document.elementsFromPoint(coords.x, coords.y).find((elem: Element) => {
|
||||
return (
|
||||
elem.parentElement?.matches?.(".ProseMirror") ||
|
||||
elem.matches(
|
||||
[
|
||||
"li",
|
||||
"p:not(:first-child)",
|
||||
"pre",
|
||||
"blockquote",
|
||||
"h1, h2, h3",
|
||||
"[data-type=horizontalRule]",
|
||||
".tableWrapper",
|
||||
].join(", ")
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function nodePosAtDOM(node: Element, view: EditorView) {
|
||||
@@ -104,9 +102,7 @@ function DragHandle(options: DragHandleOptions) {
|
||||
const nodePos = nodePosAtDOM(node, view);
|
||||
if (nodePos === null || nodePos === undefined || nodePos < 0) return;
|
||||
|
||||
view.dispatch(
|
||||
view.state.tr.setSelection(NodeSelection.create(view.state.doc, nodePos)),
|
||||
);
|
||||
view.dispatch(view.state.tr.setSelection(NodeSelection.create(view.state.doc, nodePos)));
|
||||
|
||||
const slice = view.state.selection.content();
|
||||
const { dom, text } = __serializeForClipboard(view, slice);
|
||||
@@ -137,9 +133,7 @@ function DragHandle(options: DragHandleOptions) {
|
||||
|
||||
if (nodePos === null || nodePos === undefined || nodePos < 0) return;
|
||||
|
||||
view.dispatch(
|
||||
view.state.tr.setSelection(NodeSelection.create(view.state.doc, nodePos)),
|
||||
);
|
||||
view.dispatch(view.state.tr.setSelection(NodeSelection.create(view.state.doc, nodePos)));
|
||||
}
|
||||
|
||||
let dragHandleElement: HTMLElement | null = null;
|
||||
|
||||
@@ -1,28 +1,21 @@
|
||||
import {
|
||||
useState,
|
||||
useEffect,
|
||||
useCallback,
|
||||
ReactNode,
|
||||
useRef,
|
||||
useLayoutEffect,
|
||||
} from "react";
|
||||
import { useState, useEffect, useCallback, ReactNode, useRef, useLayoutEffect } from "react";
|
||||
import { Editor, Range, Extension } from "@tiptap/core";
|
||||
import Suggestion from "@tiptap/suggestion";
|
||||
import { ReactRenderer } from "@tiptap/react";
|
||||
import tippy from "tippy.js";
|
||||
import type { UploadImage, ISlashCommandItem, CommandProps } from "@plane/editor-types";
|
||||
import type { UploadImage, ISlashCommandItem, CommandProps } from "@plane/editor-types";
|
||||
import {
|
||||
CaseSensitive,
|
||||
Code2,
|
||||
Heading1,
|
||||
Heading2,
|
||||
Heading3,
|
||||
ImageIcon,
|
||||
List,
|
||||
ListOrdered,
|
||||
Text,
|
||||
TextQuote,
|
||||
Code,
|
||||
ListTodo,
|
||||
MinusSquare,
|
||||
CheckSquare,
|
||||
ImageIcon,
|
||||
Quote,
|
||||
Table,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
@@ -39,6 +32,7 @@ import {
|
||||
} from "@plane/editor-core";
|
||||
|
||||
interface CommandItemProps {
|
||||
key: string;
|
||||
title: string;
|
||||
description: string;
|
||||
icon: ReactNode;
|
||||
@@ -50,15 +44,7 @@ const Command = Extension.create({
|
||||
return {
|
||||
suggestion: {
|
||||
char: "/",
|
||||
command: ({
|
||||
editor,
|
||||
range,
|
||||
props,
|
||||
}: {
|
||||
editor: Editor;
|
||||
range: Range;
|
||||
props: any;
|
||||
}) => {
|
||||
command: ({ editor, range, props }: { editor: Editor; range: Range; props: any }) => {
|
||||
props.command({ editor, range });
|
||||
},
|
||||
},
|
||||
@@ -80,149 +66,152 @@ const Command = Extension.create({
|
||||
const getSuggestionItems =
|
||||
(
|
||||
uploadFile: UploadImage,
|
||||
setIsSubmitting?: (
|
||||
isSubmitting: "submitting" | "submitted" | "saved",
|
||||
) => void,
|
||||
additonalOptions?: Array<ISlashCommandItem>
|
||||
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void,
|
||||
additionalOptions?: Array<ISlashCommandItem>
|
||||
) =>
|
||||
({ query }: { query: string }) => {
|
||||
let slashCommands: ISlashCommandItem[] = [
|
||||
{
|
||||
title: "Text",
|
||||
description: "Just start typing with plain text.",
|
||||
searchTerms: ["p", "paragraph"],
|
||||
icon: <Text size={18} />,
|
||||
command: ({ editor, range }: CommandProps) => {
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.deleteRange(range)
|
||||
.toggleNode("paragraph", "paragraph")
|
||||
.run();
|
||||
},
|
||||
({ query }: { query: string }) => {
|
||||
let slashCommands: ISlashCommandItem[] = [
|
||||
{
|
||||
key: "text",
|
||||
title: "Text",
|
||||
description: "Just start typing with plain text.",
|
||||
searchTerms: ["p", "paragraph"],
|
||||
icon: <CaseSensitive className="h-3.5 w-3.5" />,
|
||||
command: ({ editor, range }: CommandProps) => {
|
||||
editor.chain().focus().deleteRange(range).toggleNode("paragraph", "paragraph").run();
|
||||
},
|
||||
{
|
||||
title: "Heading 1",
|
||||
description: "Big section heading.",
|
||||
searchTerms: ["title", "big", "large"],
|
||||
icon: <Heading1 size={18} />,
|
||||
command: ({ editor, range }: CommandProps) => {
|
||||
toggleHeadingOne(editor, range);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "heading_1",
|
||||
title: "Heading 1",
|
||||
description: "Big section heading.",
|
||||
searchTerms: ["title", "big", "large"],
|
||||
icon: <Heading1 className="h-3.5 w-3.5" />,
|
||||
command: ({ editor, range }: CommandProps) => {
|
||||
toggleHeadingOne(editor, range);
|
||||
},
|
||||
{
|
||||
title: "Heading 2",
|
||||
description: "Medium section heading.",
|
||||
searchTerms: ["subtitle", "medium"],
|
||||
icon: <Heading2 size={18} />,
|
||||
command: ({ editor, range }: CommandProps) => {
|
||||
toggleHeadingTwo(editor, range);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "heading_2",
|
||||
title: "Heading 2",
|
||||
description: "Medium section heading.",
|
||||
searchTerms: ["subtitle", "medium"],
|
||||
icon: <Heading2 className="h-3.5 w-3.5" />,
|
||||
command: ({ editor, range }: CommandProps) => {
|
||||
toggleHeadingTwo(editor, range);
|
||||
},
|
||||
{
|
||||
title: "Heading 3",
|
||||
description: "Small section heading.",
|
||||
searchTerms: ["subtitle", "small"],
|
||||
icon: <Heading3 size={18} />,
|
||||
command: ({ editor, range }: CommandProps) => {
|
||||
toggleHeadingThree(editor, range);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "heading_3",
|
||||
title: "Heading 3",
|
||||
description: "Small section heading.",
|
||||
searchTerms: ["subtitle", "small"],
|
||||
icon: <Heading3 className="h-3.5 w-3.5" />,
|
||||
command: ({ editor, range }: CommandProps) => {
|
||||
toggleHeadingThree(editor, range);
|
||||
},
|
||||
{
|
||||
title: "To-do List",
|
||||
description: "Track tasks with a to-do list.",
|
||||
searchTerms: ["todo", "task", "list", "check", "checkbox"],
|
||||
icon: <CheckSquare size={18} />,
|
||||
command: ({ editor, range }: CommandProps) => {
|
||||
toggleTaskList(editor, range);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "todo_list",
|
||||
title: "To do",
|
||||
description: "Track tasks with a to-do list.",
|
||||
searchTerms: ["todo", "task", "list", "check", "checkbox"],
|
||||
icon: <ListTodo className="h-3.5 w-3.5" />,
|
||||
command: ({ editor, range }: CommandProps) => {
|
||||
toggleTaskList(editor, range);
|
||||
},
|
||||
{
|
||||
title: "Bullet List",
|
||||
description: "Create a simple bullet list.",
|
||||
searchTerms: ["unordered", "point"],
|
||||
icon: <List size={18} />,
|
||||
command: ({ editor, range }: CommandProps) => {
|
||||
toggleBulletList(editor, range);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "bullet_list",
|
||||
title: "Bullet list",
|
||||
description: "Create a simple bullet list.",
|
||||
searchTerms: ["unordered", "point"],
|
||||
icon: <List className="h-3.5 w-3.5" />,
|
||||
command: ({ editor, range }: CommandProps) => {
|
||||
toggleBulletList(editor, range);
|
||||
},
|
||||
{
|
||||
title: "Divider",
|
||||
description: "Visually divide blocks",
|
||||
searchTerms: ["line", "divider", "horizontal", "rule", "separate"],
|
||||
icon: <MinusSquare size={18} />,
|
||||
command: ({ editor, range }: CommandProps) => {
|
||||
// @ts-expect-error I have to move this to the core
|
||||
editor.chain().focus().deleteRange(range).setHorizontalRule().run();
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "numbered_list",
|
||||
title: "Numbered list",
|
||||
description: "Create a list with numbering.",
|
||||
searchTerms: ["ordered"],
|
||||
icon: <ListOrdered className="h-3.5 w-3.5" />,
|
||||
command: ({ editor, range }: CommandProps) => {
|
||||
toggleOrderedList(editor, range);
|
||||
},
|
||||
{
|
||||
title: "Table",
|
||||
description: "Create a Table",
|
||||
searchTerms: ["table", "cell", "db", "data", "tabular"],
|
||||
icon: <Table size={18} />,
|
||||
command: ({ editor, range }: CommandProps) => {
|
||||
insertTableCommand(editor, range);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "table",
|
||||
title: "Table",
|
||||
description: "Create a table",
|
||||
searchTerms: ["table", "cell", "db", "data", "tabular"],
|
||||
icon: <Table className="h-3.5 w-3.5" />,
|
||||
command: ({ editor, range }: CommandProps) => {
|
||||
insertTableCommand(editor, range);
|
||||
},
|
||||
{
|
||||
title: "Numbered List",
|
||||
description: "Create a list with numbering.",
|
||||
searchTerms: ["ordered"],
|
||||
icon: <ListOrdered size={18} />,
|
||||
command: ({ editor, range }: CommandProps) => {
|
||||
toggleOrderedList(editor, range);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "quote_block",
|
||||
title: "Quote",
|
||||
description: "Capture a quote.",
|
||||
searchTerms: ["blockquote"],
|
||||
icon: <Quote className="h-3.5 w-3.5" />,
|
||||
command: ({ editor, range }: CommandProps) => toggleBlockquote(editor, range),
|
||||
},
|
||||
{
|
||||
key: "code_block",
|
||||
title: "Code",
|
||||
description: "Capture a code snippet.",
|
||||
searchTerms: ["codeblock"],
|
||||
icon: <Code2 className="h-3.5 w-3.5" />,
|
||||
command: ({ editor, range }: CommandProps) =>
|
||||
// @ts-expect-error I have to move this to the core
|
||||
editor.chain().focus().deleteRange(range).toggleCodeBlock().run(),
|
||||
},
|
||||
{
|
||||
key: "image",
|
||||
title: "Image",
|
||||
description: "Upload an image from your computer.",
|
||||
searchTerms: ["photo", "picture", "media"],
|
||||
icon: <ImageIcon className="h-3.5 w-3.5" />,
|
||||
command: ({ editor, range }: CommandProps) => {
|
||||
insertImageCommand(editor, uploadFile, setIsSubmitting, range);
|
||||
},
|
||||
{
|
||||
title: "Quote",
|
||||
description: "Capture a quote.",
|
||||
searchTerms: ["blockquote"],
|
||||
icon: <TextQuote size={18} />,
|
||||
command: ({ editor, range }: CommandProps) =>
|
||||
toggleBlockquote(editor, range),
|
||||
},
|
||||
{
|
||||
key: "divider",
|
||||
title: "Divider",
|
||||
description: "Visually divide blocks.",
|
||||
searchTerms: ["line", "divider", "horizontal", "rule", "separate"],
|
||||
icon: <MinusSquare className="h-3.5 w-3.5" />,
|
||||
command: ({ editor, range }: CommandProps) => {
|
||||
// @ts-expect-error I have to move this to the core
|
||||
editor.chain().focus().deleteRange(range).setHorizontalRule().run();
|
||||
},
|
||||
{
|
||||
title: "Code",
|
||||
description: "Capture a code snippet.",
|
||||
searchTerms: ["codeblock"],
|
||||
icon: <Code size={18} />,
|
||||
command: ({ editor, range }: CommandProps) =>
|
||||
// @ts-expect-error I have to move this to the core
|
||||
editor.chain().focus().deleteRange(range).toggleCodeBlock().run(),
|
||||
},
|
||||
{
|
||||
title: "Image",
|
||||
description: "Upload an image from your computer.",
|
||||
searchTerms: ["photo", "picture", "media"],
|
||||
icon: <ImageIcon size={18} />,
|
||||
command: ({ editor, range }: CommandProps) => {
|
||||
insertImageCommand(editor, uploadFile, setIsSubmitting, range);
|
||||
},
|
||||
},
|
||||
]
|
||||
},
|
||||
];
|
||||
|
||||
if (additonalOptions) {
|
||||
additonalOptions.map(item => {
|
||||
slashCommands.push(item)
|
||||
})
|
||||
if (additionalOptions) {
|
||||
additionalOptions.map((item) => {
|
||||
slashCommands.push(item);
|
||||
});
|
||||
}
|
||||
|
||||
slashCommands = slashCommands.filter((item) => {
|
||||
if (typeof query === "string" && query.length > 0) {
|
||||
const search = query.toLowerCase();
|
||||
return (
|
||||
item.title.toLowerCase().includes(search) ||
|
||||
item.description.toLowerCase().includes(search) ||
|
||||
(item.searchTerms && item.searchTerms.some((term: string) => term.includes(search)))
|
||||
);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
slashCommands = slashCommands.filter((item) => {
|
||||
if (typeof query === "string" && query.length > 0) {
|
||||
const search = query.toLowerCase();
|
||||
return (
|
||||
item.title.toLowerCase().includes(search) ||
|
||||
item.description.toLowerCase().includes(search) ||
|
||||
(item.searchTerms &&
|
||||
item.searchTerms.some((term: string) => term.includes(search)))
|
||||
);
|
||||
}
|
||||
return true;
|
||||
})
|
||||
|
||||
return slashCommands
|
||||
};
|
||||
return slashCommands;
|
||||
};
|
||||
|
||||
export const updateScrollView = (container: HTMLElement, item: HTMLElement) => {
|
||||
const containerHeight = container.offsetHeight;
|
||||
@@ -238,15 +227,7 @@ export const updateScrollView = (container: HTMLElement, item: HTMLElement) => {
|
||||
}
|
||||
};
|
||||
|
||||
const CommandList = ({
|
||||
items,
|
||||
command,
|
||||
}: {
|
||||
items: CommandItemProps[];
|
||||
command: any;
|
||||
editor: any;
|
||||
range: any;
|
||||
}) => {
|
||||
const CommandList = ({ items, command }: { items: CommandItemProps[]; command: any; editor: any; range: any }) => {
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
|
||||
const selectItem = useCallback(
|
||||
@@ -256,7 +237,7 @@ const CommandList = ({
|
||||
command(item);
|
||||
}
|
||||
},
|
||||
[command, items],
|
||||
[command, items]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -303,27 +284,21 @@ const CommandList = ({
|
||||
<div
|
||||
id="slash-command"
|
||||
ref={commandListContainer}
|
||||
className="z-50 fixed h-auto max-h-[330px] w-72 overflow-y-auto rounded-md border border-custom-border-300 bg-custom-background-100 px-1 py-2 shadow-md transition-all"
|
||||
className="fixed z-50 h-auto max-h-[330px] w-52 overflow-y-auto rounded-md border border-custom-border-300 bg-custom-background-100 px-1 py-2 shadow-md transition-all"
|
||||
>
|
||||
{items.map((item: CommandItemProps, index: number) => (
|
||||
{items.map((item, index) => (
|
||||
<button
|
||||
key={item.key}
|
||||
className={cn(
|
||||
`flex w-full items-center space-x-2 rounded-md px-2 py-1 text-left text-sm text-custom-text-200 hover:bg-custom-primary-100/5 hover:text-custom-text-100`,
|
||||
`flex w-full items-center gap-2 rounded-md px-2.5 py-1.5 text-sm text-custom-text-100 hover:bg-custom-primary-100/5`,
|
||||
{
|
||||
"bg-custom-primary-100/5 text-custom-text-100":
|
||||
index === selectedIndex,
|
||||
},
|
||||
"bg-custom-primary-100/5": index === selectedIndex,
|
||||
}
|
||||
)}
|
||||
key={index}
|
||||
onClick={() => selectItem(index)}
|
||||
>
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-md border border-custom-border-300 bg-custom-background-100">
|
||||
{item.icon}
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium">{item.title}</p>
|
||||
<p className="text-xs text-custom-text-300">{item.description}</p>
|
||||
</div>
|
||||
<div className="grid flex-shrink-0 place-items-center">{item.icon}</div>
|
||||
<p>{item.title}</p>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -380,14 +355,12 @@ const renderItems = () => {
|
||||
|
||||
export const SlashCommand = (
|
||||
uploadFile: UploadImage,
|
||||
setIsSubmitting?: (
|
||||
isSubmitting: "submitting" | "submitted" | "saved",
|
||||
) => void,
|
||||
additonalOptions?: Array<ISlashCommandItem>,
|
||||
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void,
|
||||
additionalOptions?: Array<ISlashCommandItem>
|
||||
) =>
|
||||
Command.configure({
|
||||
suggestion: {
|
||||
items: getSuggestionItems(uploadFile, setIsSubmitting, additonalOptions),
|
||||
items: getSuggestionItems(uploadFile, setIsSubmitting, additionalOptions),
|
||||
render: renderItems,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ["custom"],
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
.next
|
||||
.vercel
|
||||
.tubro
|
||||
out/
|
||||
dis/
|
||||
build/
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"printWidth": 120,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/lite-text-editor",
|
||||
"version": "0.1.0",
|
||||
"version": "0.14.0",
|
||||
"description": "Package that powers Plane's Comment Editor",
|
||||
"private": true,
|
||||
"main": "./dist/index.mjs",
|
||||
@@ -52,4 +52,4 @@
|
||||
"nextjs",
|
||||
"react"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
export { LiteTextEditor, LiteTextEditorWithRef } from "./ui";
|
||||
export { LiteReadOnlyEditor, LiteReadOnlyEditorWithRef } from "./ui/read-only";
|
||||
export type {
|
||||
IMentionSuggestion,
|
||||
IMentionHighlight,
|
||||
} from "@plane/editor-types";
|
||||
export type { IMentionSuggestion, IMentionHighlight } from "@plane/editor-types";
|
||||
|
||||
@@ -1,18 +1,8 @@
|
||||
import * as React from "react";
|
||||
import {
|
||||
EditorContainer,
|
||||
EditorContentWrapper,
|
||||
getEditorClassNames,
|
||||
useEditor,
|
||||
} from "@plane/editor-core";
|
||||
import { EditorContainer, EditorContentWrapper, getEditorClassNames, useEditor } from "@plane/editor-core";
|
||||
import { FixedMenu } from "./menus/fixed-menu";
|
||||
import { LiteTextEditorExtensions } from "./extensions";
|
||||
import {
|
||||
UploadImage,
|
||||
DeleteImage,
|
||||
IMentionSuggestion,
|
||||
RestoreImage,
|
||||
} from "@plane/editor-types";
|
||||
import { UploadImage, DeleteImage, IMentionSuggestion, RestoreImage } from "@plane/editor-types";
|
||||
|
||||
interface ILiteTextEditor {
|
||||
value: string;
|
||||
@@ -25,9 +15,7 @@ interface ILiteTextEditor {
|
||||
customClassName?: string;
|
||||
editorContentCustomClassNames?: string;
|
||||
onChange?: (json: any, html: string) => void;
|
||||
setIsSubmitting?: (
|
||||
isSubmitting: "submitting" | "submitted" | "saved",
|
||||
) => void;
|
||||
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void;
|
||||
setShouldShowAlert?: (showAlert: boolean) => void;
|
||||
forwardedRef?: any;
|
||||
debouncedUpdatesEnabled?: boolean;
|
||||
@@ -107,11 +95,8 @@ const LiteTextEditor = (props: LiteTextEditorProps) => {
|
||||
return (
|
||||
<EditorContainer editor={editor} editorClassNames={editorClassNames}>
|
||||
<div className="flex flex-col">
|
||||
<EditorContentWrapper
|
||||
editor={editor}
|
||||
editorContentCustomClassNames={editorContentCustomClassNames}
|
||||
/>
|
||||
<div className="w-full mt-4">
|
||||
<EditorContentWrapper editor={editor} editorContentCustomClassNames={editorContentCustomClassNames} />
|
||||
<div className="mt-4 w-full">
|
||||
<FixedMenu
|
||||
editor={editor}
|
||||
uploadFile={uploadFile}
|
||||
@@ -125,9 +110,9 @@ const LiteTextEditor = (props: LiteTextEditorProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
const LiteTextEditorWithRef = React.forwardRef<EditorHandle, ILiteTextEditor>(
|
||||
(props, ref) => <LiteTextEditor {...props} forwardedRef={ref} />,
|
||||
);
|
||||
const LiteTextEditorWithRef = React.forwardRef<EditorHandle, ILiteTextEditor>((props, ref) => (
|
||||
<LiteTextEditor {...props} forwardedRef={ref} />
|
||||
));
|
||||
|
||||
LiteTextEditorWithRef.displayName = "LiteTextEditorWithRef";
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user