Compare commits
220
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
24f993dbee | ||
|
|
1ff0538077 | ||
|
|
0e055666e7 | ||
|
|
1b98b65a80 | ||
|
|
03c8aeed57 | ||
|
|
6c8df73ad4 | ||
|
|
074e35525c | ||
|
|
8ea42aa0b6 | ||
|
|
1df067ff61 | ||
|
|
a79dbdadb5 | ||
|
|
bffba6b9dc | ||
|
|
bd4b5040b2 | ||
|
|
0d762d74dc | ||
|
|
567c3fadc0 | ||
|
|
cb0af255f9 | ||
|
|
d204cc7d6c | ||
|
|
5317de5919 | ||
|
|
b499e2c5a5 | ||
|
|
956d7acfa6 | ||
|
|
4040441ab1 | ||
|
|
dc5c0fe5bf | ||
|
|
675e747fed | ||
|
|
c9517610da | ||
|
|
0e1ee80512 | ||
|
|
0fc273aca6 | ||
|
|
d6b23fe380 | ||
|
|
11987994a1 | ||
|
|
b957e4e935 | ||
|
|
e7ac7e1da8 | ||
|
|
ac18906604 | ||
|
|
aec50e2c48 | ||
|
|
8dee7e51ca | ||
|
|
8b2d78ef92 | ||
|
|
9649f42ff3 | ||
|
|
db1166411a | ||
|
|
effad33f67 | ||
|
|
fd73f18d95 | ||
|
|
32bfa4d7cf | ||
|
|
7b12d54d83 | ||
|
|
2cf847e8a7 | ||
|
|
e2524799d2 | ||
|
|
8a8eea38f9 | ||
|
|
b527ec582f | ||
|
|
fa990ed444 | ||
|
|
79fa860b28 | ||
|
|
59110c7205 | ||
|
|
9756abece4 | ||
|
|
f4aa059da6 | ||
|
|
d22c258c00 | ||
|
|
65253c6383 | ||
|
|
8c462f96ee | ||
|
|
332e56bb0d | ||
|
|
eca96da837 | ||
|
|
5446447231 | ||
|
|
dec1fb5a63 | ||
|
|
2b63827caa | ||
|
|
5993fc38f0 | ||
|
|
ab37ce979a | ||
|
|
c6764111a3 | ||
|
|
4a2f8d93b1 | ||
|
|
5db9b2b638 | ||
|
|
e2b704ea6f | ||
|
|
452c9f0d5b | ||
|
|
139c0857eb | ||
|
|
2556d052de | ||
|
|
c65915665b | ||
|
|
6ece5a57e2 | ||
|
|
a779fa1497 | ||
|
|
5eb7740833 | ||
|
|
86408fcd46 | ||
|
|
86de3188b1 | ||
|
|
c5996382bc | ||
|
|
2796754e5f | ||
|
|
3488dc3014 | ||
|
|
c7a3d8f0a0 | ||
|
|
6d97dd814a | ||
|
|
804c03bd15 | ||
|
|
59981e3e68 | ||
|
|
1be1b9f4a3 | ||
|
|
7cd02401c1 | ||
|
|
6d175c0e56 | ||
|
|
6a4f521b47 | ||
|
|
cdc4fd27a5 | ||
|
|
f4af3db7b6 | ||
|
|
4f64f431a7 | ||
|
|
537901f046 | ||
|
|
404a61aee5 | ||
|
|
1a2b1e648d | ||
|
|
3400c119bc | ||
|
|
d5853405ca | ||
|
|
db510dcfcd | ||
|
|
62c0615012 | ||
|
|
3914a75334 | ||
|
|
0cbb201348 | ||
|
|
f7264364bd | ||
|
|
35f8ffa5ab | ||
|
|
6607caade7 | ||
|
|
8ee8270697 | ||
|
|
41ab962dd7 | ||
|
|
c22c6bb9b2 | ||
|
|
67de6d0729 | ||
|
|
f361cd045e | ||
|
|
06d3cd7e73 | ||
|
|
10c52bf89b | ||
|
|
b717518fbe | ||
|
|
f48cd6f50c | ||
|
|
3203ae6549 | ||
|
|
b8f603f920 | ||
|
|
96ff76af94 | ||
|
|
830675741f | ||
|
|
e489ad50dc | ||
|
|
3dc18bc8fd | ||
|
|
2d04917951 | ||
|
|
2bf7e63625 | ||
|
|
eb78fd6088 | ||
|
|
202ecd21df | ||
|
|
b2ac7b9ac6 | ||
|
|
51dff31926 | ||
|
|
e89f152779 | ||
|
|
3c9f57f8f4 | ||
|
|
1bc859c68c | ||
|
|
11d57a5bf0 | ||
|
|
2980c7b00d | ||
|
|
5c6a59ba35 | ||
|
|
a3ea7c8f10 | ||
|
|
cb922fb113 | ||
|
|
06564ee856 | ||
|
|
c7e6118804 | ||
|
|
069b8b3ed9 | ||
|
|
38a5b7bec0 | ||
|
|
236caaafe8 | ||
|
|
a6d5eab634 | ||
|
|
8d76c96a6f | ||
|
|
97be4b60ae | ||
|
|
dece103873 | ||
|
|
c6125876be | ||
|
|
1f85bf2302 | ||
|
|
20baba3bb0 | ||
|
|
85907b32d1 | ||
|
|
ef2bef83dc | ||
|
|
6e7a96394a | ||
|
|
5726f6955c | ||
|
|
82665a35ee | ||
|
|
4efd225599 | ||
|
|
2481706581 | ||
|
|
a17b08dd15 | ||
|
|
a7d6b528bd | ||
|
|
9ba724b78d | ||
|
|
c2da9783a3 | ||
|
|
784be47e91 | ||
|
|
0fdd9c28bf | ||
|
|
644b06749b | ||
|
|
dd8c7a7487 | ||
|
|
e6a1f34713 | ||
|
|
1dff6b63f8 | ||
|
|
59dbbb29cd | ||
|
|
6cb3939835 | ||
|
|
021c0675b7 | ||
|
|
67000892e5 | ||
|
|
3df4794e77 | ||
|
|
42ccd1de58 | ||
|
|
c8c89007c0 | ||
|
|
4cf3e69e22 | ||
|
|
fb1f65c2c1 | ||
|
|
d91b4e6fa1 | ||
|
|
561223ea71 | ||
|
|
982eba0bd1 | ||
|
|
7aaf840fb1 | ||
|
|
15927c9cae | ||
|
|
d46d70fcd5 | ||
|
|
de581102e3 | ||
|
|
b903126e5a | ||
|
|
f44f70168f | ||
|
|
3c10f00b04 | ||
|
|
f1de05e4de | ||
|
|
61d4e2e016 | ||
|
|
c1eb5055e5 | ||
|
|
8d942e28da | ||
|
|
f7461af3f5 | ||
|
|
29f3e02adc | ||
|
|
9a704458b3 | ||
|
|
668dfd2e38 | ||
|
|
3b3f94ed03 | ||
|
|
e945aa9b71 | ||
|
|
6595a387d0 | ||
|
|
8839e42dc0 | ||
|
|
9db6312081 | ||
|
|
779ef2a4aa | ||
|
|
51e17643a2 | ||
|
|
4c2074b6ff | ||
|
|
c9ffc9465f | ||
|
|
2b6c489513 | ||
|
|
0c63f21718 | ||
|
|
a987df38f4 | ||
|
|
878707f444 | ||
|
|
9369ee5008 | ||
|
|
0a88db975a | ||
|
|
dd60dec887 | ||
|
|
0c1097592e | ||
|
|
bed66235f2 | ||
|
|
26b1e9d5f1 | ||
|
+1 |
79347ec62b | ||
|
|
7b965179d8 | ||
|
|
fc51ffc589 | ||
|
|
96f6e37cc5 | ||
|
|
29774ce84a | ||
|
|
8cbe9c26fc | ||
|
|
7f42566207 | ||
|
|
b60237b676 | ||
|
|
1fe09d369f | ||
|
|
b7757c6b1a | ||
|
|
1a25bacce1 | ||
|
|
6797df239d | ||
|
|
43e7c10eb7 | ||
|
|
bdc9c9c2a8 | ||
|
|
f0c72bf249 | ||
|
|
a8904bfc48 | ||
|
|
b31041726b | ||
|
|
e6f947ad90 | ||
|
|
7963993171 |
@@ -9,7 +9,6 @@ on:
|
||||
- preview
|
||||
- qa
|
||||
- develop
|
||||
- release-*
|
||||
release:
|
||||
types: [released, prereleased]
|
||||
|
||||
@@ -63,14 +62,14 @@ jobs:
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [branch_build_setup]
|
||||
env:
|
||||
FRONTEND_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend${{ secrets.DOCKER_REPO_SUFFIX || '' }}:${{ needs.branch_build_setup.outputs.gh_branch_name }}
|
||||
FRONTEND_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend-ce:${{ 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${{ secrets.DOCKER_REPO_SUFFIX || '' }}:latest,${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend${{ secrets.DOCKER_REPO_SUFFIX || '' }}:${{ github.event.release.tag_name }}
|
||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend-ce:latest,${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend-ce:${{ github.event.release.tag_name }}
|
||||
elif [ "${{ needs.branch_build_setup.outputs.gh_branch_name }}" == "master" ]; then
|
||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend${{ secrets.DOCKER_REPO_SUFFIX || '' }}:stable
|
||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend-ce:stable
|
||||
else
|
||||
TAG=${{ env.FRONTEND_TAG }}
|
||||
fi
|
||||
@@ -105,14 +104,14 @@ jobs:
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [branch_build_setup]
|
||||
env:
|
||||
SPACE_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-space${{ secrets.DOCKER_REPO_SUFFIX || '' }}:${{ needs.branch_build_setup.outputs.gh_branch_name }}
|
||||
SPACE_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-space-ce:${{ 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${{ secrets.DOCKER_REPO_SUFFIX || '' }}:latest,${{ secrets.DOCKERHUB_USERNAME }}/plane-space${{ secrets.DOCKER_REPO_SUFFIX || '' }}:${{ github.event.release.tag_name }}
|
||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-space-ce:latest,${{ secrets.DOCKERHUB_USERNAME }}/plane-space-ce:${{ github.event.release.tag_name }}
|
||||
elif [ "${{ needs.branch_build_setup.outputs.gh_branch_name }}" == "master" ]; then
|
||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-space${{ secrets.DOCKER_REPO_SUFFIX || '' }}:stable
|
||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-space-ce:stable
|
||||
else
|
||||
TAG=${{ env.SPACE_TAG }}
|
||||
fi
|
||||
@@ -147,14 +146,14 @@ jobs:
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [branch_build_setup]
|
||||
env:
|
||||
BACKEND_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-backend${{ secrets.DOCKER_REPO_SUFFIX || '' }}:${{ needs.branch_build_setup.outputs.gh_branch_name }}
|
||||
BACKEND_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-backend-ce:${{ 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${{ secrets.DOCKER_REPO_SUFFIX || '' }}:latest,${{ secrets.DOCKERHUB_USERNAME }}/plane-backend${{ secrets.DOCKER_REPO_SUFFIX || '' }}:${{ github.event.release.tag_name }}
|
||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-backend-ce:latest,${{ secrets.DOCKERHUB_USERNAME }}/plane-backend-ce:${{ github.event.release.tag_name }}
|
||||
elif [ "${{ needs.branch_build_setup.outputs.gh_branch_name }}" == "master" ]; then
|
||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-backend${{ secrets.DOCKER_REPO_SUFFIX || '' }}:stable
|
||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-backend-ce:stable
|
||||
else
|
||||
TAG=${{ env.BACKEND_TAG }}
|
||||
fi
|
||||
@@ -189,14 +188,14 @@ jobs:
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [branch_build_setup]
|
||||
env:
|
||||
PROXY_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy${{ secrets.DOCKER_REPO_SUFFIX || '' }}:${{ needs.branch_build_setup.outputs.gh_branch_name }}
|
||||
PROXY_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy-ce:${{ 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${{ secrets.DOCKER_REPO_SUFFIX || '' }}:latest,${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy${{ secrets.DOCKER_REPO_SUFFIX || '' }}:${{ github.event.release.tag_name }}
|
||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy-ce:latest,${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy-ce:${{ github.event.release.tag_name }}
|
||||
elif [ "${{ needs.branch_build_setup.outputs.gh_branch_name }}" == "master" ]; then
|
||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy${{ secrets.DOCKER_REPO_SUFFIX || '' }}:stable
|
||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy-ce:stable
|
||||
else
|
||||
TAG=${{ env.PROXY_TAG }}
|
||||
fi
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
name: Build Pull Request Contents
|
||||
|
||||
on:
|
||||
on:
|
||||
pull_request:
|
||||
types: ["opened", "synchronize"]
|
||||
|
||||
@@ -14,15 +14,13 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout Repository to Actions
|
||||
uses: actions/checkout@v3.3.0
|
||||
with:
|
||||
token: ${{ secrets.ACCESS_TOKEN }}
|
||||
|
||||
- name: Setup Node.js 18.x
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 18.x
|
||||
cache: "yarn"
|
||||
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Get changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v38
|
||||
@@ -46,3 +44,5 @@ jobs:
|
||||
run: |
|
||||
yarn
|
||||
yarn build --filter=space
|
||||
|
||||
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
name: Create Sync Action
|
||||
name: Create PR in Plane EE Repository to sync the changes
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- preview
|
||||
- master
|
||||
types:
|
||||
- closed
|
||||
env:
|
||||
SOURCE_BRANCH_NAME: ${{github.event.pull_request.base.ref}}
|
||||
|
||||
jobs:
|
||||
create_pr:
|
||||
@@ -18,13 +16,27 @@ jobs:
|
||||
pull-requests: write
|
||||
contents: read
|
||||
steps:
|
||||
- name: Check SOURCE_REPO
|
||||
id: check_repo
|
||||
env:
|
||||
SOURCE_REPO: ${{ secrets.SOURCE_REPO_NAME }}
|
||||
run: |
|
||||
echo "::set-output name=is_correct_repo::$(if [[ "$SOURCE_REPO" == "makeplane/plane" ]]; then echo 'true'; else echo 'false'; fi)"
|
||||
|
||||
- name: Checkout Code
|
||||
if: steps.check_repo.outputs.is_correct_repo == 'true'
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Branch Name
|
||||
if: steps.check_repo.outputs.is_correct_repo == 'true'
|
||||
run: |
|
||||
echo "SOURCE_BRANCH_NAME=${{ github.head_ref }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup GH CLI
|
||||
if: steps.check_repo.outputs.is_correct_repo == 'true'
|
||||
run: |
|
||||
type -p curl >/dev/null || (sudo apt update && sudo apt install curl -y)
|
||||
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg
|
||||
@@ -33,14 +45,35 @@ jobs:
|
||||
sudo apt update
|
||||
sudo apt install gh -y
|
||||
|
||||
- name: Push Changes to Target Repo
|
||||
- name: Create Pull Request
|
||||
if: steps.check_repo.outputs.is_correct_repo == 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
||||
run: |
|
||||
TARGET_REPO="${{ secrets.SYNC_TARGET_REPO_NAME }}"
|
||||
TARGET_BRANCH="${{ secrets.SYNC_TARGET_BRANCH_NAME }}"
|
||||
TARGET_REPO="${{ secrets.TARGET_REPO_NAME }}"
|
||||
TARGET_BRANCH="${{ secrets.TARGET_REPO_BRANCH }}"
|
||||
SOURCE_BRANCH="${{ env.SOURCE_BRANCH_NAME }}"
|
||||
|
||||
git checkout $SOURCE_BRANCH
|
||||
git remote add target-origin "https://$GH_TOKEN@github.com/$TARGET_REPO.git"
|
||||
git push target-origin $SOURCE_BRANCH:$TARGET_BRANCH
|
||||
git remote add target "https://$GH_TOKEN@github.com/$TARGET_REPO.git"
|
||||
git push target $SOURCE_BRANCH:$SOURCE_BRANCH
|
||||
|
||||
PR_TITLE="${{ github.event.pull_request.title }}"
|
||||
PR_BODY="${{ github.event.pull_request.body }}"
|
||||
|
||||
# Remove double quotes
|
||||
PR_TITLE_CLEANED="${PR_TITLE//\"/}"
|
||||
PR_BODY_CLEANED="${PR_BODY//\"/}"
|
||||
|
||||
# Construct PR_BODY_CONTENT using a here-document
|
||||
PR_BODY_CONTENT=$(cat <<EOF
|
||||
$PR_BODY_CLEANED
|
||||
EOF
|
||||
)
|
||||
|
||||
gh pr create \
|
||||
--base $TARGET_BRANCH \
|
||||
--head $SOURCE_BRANCH \
|
||||
--title "[SYNC] $PR_TITLE_CLEANED" \
|
||||
--body "$PR_BODY_CONTENT" \
|
||||
--repo $TARGET_REPO
|
||||
|
||||
+25
-4
@@ -33,8 +33,8 @@ The backend is a django project which is kept inside apiserver
|
||||
1. Clone the repo
|
||||
|
||||
```bash
|
||||
git clone https://github.com/makeplane/plane.git [folder-name]
|
||||
cd [folder-name]
|
||||
git clone https://github.com/makeplane/plane
|
||||
cd plane
|
||||
chmod +x setup.sh
|
||||
```
|
||||
|
||||
@@ -44,12 +44,33 @@ chmod +x setup.sh
|
||||
./setup.sh
|
||||
```
|
||||
|
||||
3. Start the containers
|
||||
3. Define `NEXT_PUBLIC_API_BASE_URL=http://localhost` in **web/.env** and **space/.env** file
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose-local.yml up
|
||||
echo "\nNEXT_PUBLIC_API_BASE_URL=http://localhost\n" >> ./web/.env
|
||||
```
|
||||
|
||||
```bash
|
||||
echo "\nNEXT_PUBLIC_API_BASE_URL=http://localhost\n" >> ./space/.env
|
||||
```
|
||||
|
||||
4. Run Docker compose up
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
5. Install dependencies
|
||||
|
||||
```bash
|
||||
yarn install
|
||||
```
|
||||
|
||||
6. Run the web app in development mode
|
||||
|
||||
```bash
|
||||
yarn dev
|
||||
```
|
||||
|
||||
## Missing a Feature?
|
||||
|
||||
|
||||
@@ -49,10 +49,24 @@ 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
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ Meet [Plane](https://plane.so). An open-source software development tool to mana
|
||||
|
||||
> Plane is still in its early days, not everything will be perfect yet, and hiccups may happen. Please let us know of any suggestions, ideas, or bugs that you encounter on our [Discord](https://discord.com/invite/A92xrEGCge) or GitHub issues, and we will use your feedback to improve on our upcoming releases.
|
||||
|
||||
The easiest way to get started with Plane is by creating a [Plane Cloud](https://app.plane.so) account. Plane Cloud offers a hosted solution for Plane. If you prefer to self-host Plane, please refer to our [deployment documentation](https://docs.plane.so/self-hosting/docker-compose).
|
||||
The easiest way to get started with Plane is by creating a [Plane Cloud](https://app.plane.so) account. Plane Cloud offers a hosted solution for Plane. If you prefer to self-host Plane, please refer to our [deployment documentation](https://docs.plane.so/self-hosting).
|
||||
|
||||
## ⚡️ Contributors Quick Start
|
||||
|
||||
@@ -63,7 +63,7 @@ Thats it!
|
||||
|
||||
## 🍙 Self Hosting
|
||||
|
||||
For self hosting environment setup, visit the [Self Hosting](https://docs.plane.so/self-hosting/docker-compose) documentation page
|
||||
For self hosting environment setup, visit the [Self Hosting](https://docs.plane.so/self-hosting) documentation page
|
||||
|
||||
## 🚀 Features
|
||||
|
||||
|
||||
@@ -27,8 +27,7 @@ WORKDIR /code
|
||||
COPY requirements.txt ./requirements.txt
|
||||
ADD requirements ./requirements
|
||||
|
||||
# Install the local development settings
|
||||
RUN pip install -r requirements/local.txt --compile --no-cache-dir
|
||||
RUN pip install -r requirements.txt --compile --no-cache-dir
|
||||
|
||||
RUN addgroup -S plane && \
|
||||
adduser -S captain -G plane
|
||||
@@ -49,5 +48,5 @@ USER captain
|
||||
# Expose container port and run entry point script
|
||||
EXPOSE 8000
|
||||
|
||||
CMD [ "./bin/takeoff.local" ]
|
||||
# CMD [ "./bin/takeoff" ]
|
||||
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
python manage.py wait_for_db
|
||||
python manage.py migrate
|
||||
|
||||
# Create the default bucket
|
||||
#!/bin/bash
|
||||
|
||||
# Collect system information
|
||||
HOSTNAME=$(hostname)
|
||||
MAC_ADDRESS=$(ip link show | awk '/ether/ {print $2}' | head -n 1)
|
||||
CPU_INFO=$(cat /proc/cpuinfo)
|
||||
MEMORY_INFO=$(free -h)
|
||||
DISK_INFO=$(df -h)
|
||||
|
||||
# Concatenate information and compute SHA-256 hash
|
||||
SIGNATURE=$(echo "$HOSTNAME$MAC_ADDRESS$CPU_INFO$MEMORY_INFO$DISK_INFO" | sha256sum | awk '{print $1}')
|
||||
|
||||
# Export the variables
|
||||
export MACHINE_SIGNATURE=$SIGNATURE
|
||||
|
||||
# Register instance
|
||||
python manage.py register_instance $MACHINE_SIGNATURE
|
||||
# Load the configuration variable
|
||||
python manage.py configure_instance
|
||||
|
||||
# Create the default bucket
|
||||
python manage.py create_bucket
|
||||
|
||||
python manage.py runserver 0.0.0.0:8000 --settings=plane.settings.local
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"name": "plane-api",
|
||||
"version": "0.14.0"
|
||||
}
|
||||
"name": "plane-api",
|
||||
"version": "0.13.2"
|
||||
}
|
||||
@@ -96,7 +96,7 @@ class IssueSerializer(BaseSerializer):
|
||||
if (
|
||||
data.get("state")
|
||||
and not State.objects.filter(
|
||||
project_id=self.context.get("project_id"), pk=data.get("state").id
|
||||
project_id=self.context.get("project_id"), pk=data.get("state")
|
||||
).exists()
|
||||
):
|
||||
raise serializers.ValidationError(
|
||||
@@ -107,7 +107,7 @@ class IssueSerializer(BaseSerializer):
|
||||
if (
|
||||
data.get("parent")
|
||||
and not Issue.objects.filter(
|
||||
workspace_id=self.context.get("workspace_id"), pk=data.get("parent").id
|
||||
workspace_id=self.context.get("workspace_id"), pk=data.get("parent")
|
||||
).exists()
|
||||
):
|
||||
raise serializers.ValidationError(
|
||||
|
||||
@@ -65,18 +65,18 @@ class ModuleSerializer(BaseSerializer):
|
||||
def create(self, validated_data):
|
||||
members = validated_data.pop("members", None)
|
||||
|
||||
project_id = self.context["project_id"]
|
||||
workspace_id = self.context["workspace_id"]
|
||||
project = self.context["project"]
|
||||
|
||||
module = Module.objects.create(**validated_data, project=project)
|
||||
|
||||
module = Module.objects.create(**validated_data, project_id=project_id)
|
||||
if members is not None:
|
||||
ModuleMember.objects.bulk_create(
|
||||
[
|
||||
ModuleMember(
|
||||
module=module,
|
||||
member_id=str(member),
|
||||
project_id=project_id,
|
||||
workspace_id=workspace_id,
|
||||
member=member,
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
created_by=module.created_by,
|
||||
updated_by=module.updated_by,
|
||||
)
|
||||
@@ -97,7 +97,7 @@ class ModuleSerializer(BaseSerializer):
|
||||
[
|
||||
ModuleMember(
|
||||
module=instance,
|
||||
member_id=str(member),
|
||||
member=member,
|
||||
project=instance.project,
|
||||
workspace=instance.project.workspace,
|
||||
created_by=instance.created_by,
|
||||
|
||||
@@ -221,20 +221,11 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
|
||||
def patch(self, request, slug, project_id, pk=None):
|
||||
issue = Issue.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
|
||||
project = Project.objects.get(pk=project_id)
|
||||
current_instance = json.dumps(
|
||||
IssueSerializer(issue).data, cls=DjangoJSONEncoder
|
||||
)
|
||||
requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder)
|
||||
serializer = IssueSerializer(
|
||||
issue,
|
||||
data=request.data,
|
||||
context={
|
||||
"project_id": project_id,
|
||||
"workspace_id": project.workspace_id,
|
||||
},
|
||||
partial=True,
|
||||
)
|
||||
serializer = IssueSerializer(issue, data=request.data, partial=True)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
issue_activity.delay(
|
||||
|
||||
@@ -121,8 +121,8 @@ class ModuleAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
)
|
||||
|
||||
def post(self, request, slug, project_id):
|
||||
project = Project.objects.get(pk=project_id, workspace__slug=slug)
|
||||
serializer = ModuleSerializer(data=request.data, context={"project_id": project_id, "workspace_id": project.workspace_id})
|
||||
project = Project.objects.get(workspace__slug=slug, pk=project_id)
|
||||
serializer = ModuleSerializer(data=request.data, context={"project": project})
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
module = Module.objects.get(pk=serializer.data["id"])
|
||||
@@ -132,7 +132,7 @@ class ModuleAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
|
||||
def patch(self, request, slug, project_id, pk):
|
||||
module = Module.objects.get(pk=pk, project_id=project_id, workspace__slug=slug)
|
||||
serializer = ModuleSerializer(module, data=request.data, context={"project_id": project_id}, partial=True)
|
||||
serializer = ModuleSerializer(module, data=request.data)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
@@ -40,7 +40,6 @@ class CycleSerializer(BaseSerializer):
|
||||
started_estimates = serializers.IntegerField(read_only=True)
|
||||
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
|
||||
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
||||
status = serializers.CharField(read_only=True)
|
||||
|
||||
def validate(self, data):
|
||||
if (
|
||||
|
||||
@@ -4,7 +4,6 @@ from .base import BaseSerializer
|
||||
from plane.db.models import Estimate, EstimatePoint
|
||||
from plane.app.serializers import WorkspaceLiteSerializer, ProjectLiteSerializer
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
class EstimateSerializer(BaseSerializer):
|
||||
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
|
||||
@@ -20,15 +19,6 @@ class EstimateSerializer(BaseSerializer):
|
||||
|
||||
|
||||
class EstimatePointSerializer(BaseSerializer):
|
||||
|
||||
def validate(self, data):
|
||||
if not data:
|
||||
raise serializers.ValidationError("Estimate points are required")
|
||||
value = data.get("value")
|
||||
if value and len(value) > 20:
|
||||
raise serializers.ValidationError("Value can't be more than 20 characters")
|
||||
return data
|
||||
|
||||
class Meta:
|
||||
model = EstimatePoint
|
||||
fields = "__all__"
|
||||
|
||||
@@ -13,7 +13,6 @@ from plane.app.views import (
|
||||
UserProjectInvitationsViewset,
|
||||
ProjectPublicCoverImagesEndpoint,
|
||||
ProjectDeployBoardViewSet,
|
||||
UserProjectRolesEndpoint,
|
||||
)
|
||||
|
||||
|
||||
@@ -75,11 +74,6 @@ 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,7 +11,6 @@ from .project import (
|
||||
ProjectFavoritesViewSet,
|
||||
ProjectPublicCoverImagesEndpoint,
|
||||
ProjectDeployBoardViewSet,
|
||||
UserProjectRolesEndpoint,
|
||||
)
|
||||
from .user import (
|
||||
UserEndpoint,
|
||||
|
||||
@@ -343,6 +343,13 @@ 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 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["google_client_id"] = GOOGLE_CLIENT_ID
|
||||
data["github_client_id"] = GITHUB_CLIENT_ID
|
||||
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"] = bool(UNSPLASH_ACCESS_KEY)
|
||||
data["has_unsplash_configured"] = UNSPLASH_ACCESS_KEY
|
||||
|
||||
# Open AI settings
|
||||
data["has_openai_configured"] = bool(OPENAI_API_KEY)
|
||||
|
||||
@@ -11,10 +11,6 @@ from django.db.models import (
|
||||
Count,
|
||||
Prefetch,
|
||||
Sum,
|
||||
Case,
|
||||
When,
|
||||
Value,
|
||||
CharField
|
||||
)
|
||||
from django.core import serializers
|
||||
from django.utils import timezone
|
||||
@@ -161,28 +157,6 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
|
||||
),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
status=Case(
|
||||
When(
|
||||
Q(start_date__lte=timezone.now()) & Q(end_date__gte=timezone.now()),
|
||||
then=Value("CURRENT")
|
||||
),
|
||||
When(
|
||||
start_date__gt=timezone.now(),
|
||||
then=Value("UPCOMING")
|
||||
),
|
||||
When(
|
||||
end_date__lt=timezone.now(),
|
||||
then=Value("COMPLETED")
|
||||
),
|
||||
When(
|
||||
Q(start_date__isnull=True) & Q(end_date__isnull=True),
|
||||
then=Value("DRAFT")
|
||||
),
|
||||
default=Value("DRAFT"),
|
||||
output_field=CharField(),
|
||||
)
|
||||
)
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"issue_cycle__issue__assignees",
|
||||
@@ -203,7 +177,7 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
|
||||
queryset = self.get_queryset()
|
||||
cycle_view = request.GET.get("cycle_view", "all")
|
||||
|
||||
queryset = queryset.order_by("-is_favorite", "-created_at")
|
||||
queryset = queryset.order_by("-is_favorite","-created_at")
|
||||
|
||||
# Current Cycle
|
||||
if cycle_view == "current":
|
||||
@@ -601,9 +575,7 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
|
||||
)
|
||||
)
|
||||
|
||||
issues = IssueStateSerializer(
|
||||
issues, many=True, fields=fields if fields else None
|
||||
).data
|
||||
issues = IssueStateSerializer(issues, many=True, fields=fields if fields else None).data
|
||||
issue_dict = {str(issue["id"]): issue for issue in issues}
|
||||
return Response(issue_dict, status=status.HTTP_200_OK)
|
||||
|
||||
@@ -833,4 +805,4 @@ class TransferCycleIssueEndpoint(BaseAPIView):
|
||||
updated_cycles, ["cycle_id"], batch_size=100
|
||||
)
|
||||
|
||||
return Response({"message": "Success"}, status=status.HTTP_200_OK)
|
||||
return Response({"message": "Success"}, status=status.HTTP_200_OK)
|
||||
@@ -53,11 +53,11 @@ class BulkEstimatePointEndpoint(BaseViewSet):
|
||||
)
|
||||
|
||||
estimate_points = request.data.get("estimate_points", [])
|
||||
|
||||
serializer = EstimatePointSerializer(data=request.data.get("estimate_points"), many=True)
|
||||
if not serializer.is_valid():
|
||||
|
||||
if not len(estimate_points) or len(estimate_points) > 8:
|
||||
return Response(
|
||||
serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
||||
{"error": "Estimate points are required"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
estimate_serializer = EstimateSerializer(data=request.data.get("estimate"))
|
||||
|
||||
@@ -86,14 +86,7 @@ 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,) = get_configuration_value(
|
||||
[
|
||||
{
|
||||
"key": "GITHUB_CLIENT_SECRET",
|
||||
"default": os.environ.get("GITHUB_CLIENT_SECRET", None),
|
||||
},
|
||||
]
|
||||
)
|
||||
CLIENT_SECRET = os.environ.get("GITHUB_CLIENT_SECRET")
|
||||
|
||||
url = f"https://github.com/login/oauth/access_token?client_id={client_id}&client_secret={CLIENT_SECRET}&code={request_token}"
|
||||
headers = {"accept": "application/json"}
|
||||
@@ -167,6 +160,12 @@ 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(
|
||||
{
|
||||
@@ -176,19 +175,9 @@ 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)
|
||||
|
||||
@@ -310,7 +299,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,10 +39,8 @@ from plane.app.serializers import (
|
||||
)
|
||||
|
||||
from plane.app.permissions import (
|
||||
WorkspaceUserPermission,
|
||||
ProjectBasePermission,
|
||||
ProjectMemberPermission,
|
||||
ProjectLitePermission,
|
||||
)
|
||||
|
||||
from plane.db.models import (
|
||||
@@ -145,16 +143,6 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
|
||||
)
|
||||
)
|
||||
)
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"project_projectmember",
|
||||
queryset=ProjectMember.objects.filter(
|
||||
workspace__slug=self.kwargs.get("slug"),
|
||||
is_active=True,
|
||||
).select_related("member"),
|
||||
to_attr="members_list",
|
||||
)
|
||||
)
|
||||
.distinct()
|
||||
)
|
||||
|
||||
@@ -170,6 +158,16 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
|
||||
projects = (
|
||||
self.get_queryset()
|
||||
.annotate(sort_order=Subquery(sort_order_query))
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"project_projectmember",
|
||||
queryset=ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
is_active=True,
|
||||
).select_related("member"),
|
||||
to_attr='members_list'
|
||||
)
|
||||
)
|
||||
.order_by("sort_order", "name")
|
||||
)
|
||||
if request.GET.get("per_page", False) and request.GET.get("cursor", False):
|
||||
@@ -603,18 +601,6 @@ 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",
|
||||
@@ -679,25 +665,6 @@ class ProjectMemberViewSet(BaseViewSet):
|
||||
)
|
||||
)
|
||||
|
||||
# Check if the user is already a member of the project and is inactive
|
||||
if ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
member_id=member.get("member_id"),
|
||||
is_active=False,
|
||||
).exists():
|
||||
member_detail = ProjectMember.objects.get(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
member_id=member.get("member_id"),
|
||||
is_active=False,
|
||||
)
|
||||
# Check if the user has not deactivated the account
|
||||
user = User.objects.filter(pk=member.get("member_id")).first()
|
||||
if user.is_active:
|
||||
member_detail.is_active = True
|
||||
member_detail.save(update_fields=["is_active"])
|
||||
|
||||
project_members = ProjectMember.objects.bulk_create(
|
||||
bulk_project_members,
|
||||
batch_size=10,
|
||||
@@ -1010,18 +977,11 @@ class ProjectPublicCoverImagesEndpoint(BaseAPIView):
|
||||
|
||||
def get(self, request):
|
||||
files = []
|
||||
s3_client_params = {
|
||||
"service_name": "s3",
|
||||
"aws_access_key_id": settings.AWS_ACCESS_KEY_ID,
|
||||
"aws_secret_access_key": settings.AWS_SECRET_ACCESS_KEY,
|
||||
}
|
||||
|
||||
# Use AWS_S3_ENDPOINT_URL if it is present in the settings
|
||||
if hasattr(settings, "AWS_S3_ENDPOINT_URL") and settings.AWS_S3_ENDPOINT_URL:
|
||||
s3_client_params["endpoint_url"] = settings.AWS_S3_ENDPOINT_URL
|
||||
|
||||
s3 = boto3.client(**s3_client_params)
|
||||
|
||||
s3 = boto3.client(
|
||||
"s3",
|
||||
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
|
||||
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
|
||||
)
|
||||
params = {
|
||||
"Bucket": settings.AWS_STORAGE_BUCKET_NAME,
|
||||
"Prefix": "static/project-cover/",
|
||||
@@ -1034,19 +994,9 @@ class ProjectPublicCoverImagesEndpoint(BaseAPIView):
|
||||
if not content["Key"].endswith(
|
||||
"/"
|
||||
): # This line ensures we're only getting files, not "sub-folders"
|
||||
if (
|
||||
hasattr(settings, "AWS_S3_CUSTOM_DOMAIN")
|
||||
and settings.AWS_S3_CUSTOM_DOMAIN
|
||||
and hasattr(settings, "AWS_S3_URL_PROTOCOL")
|
||||
and settings.AWS_S3_URL_PROTOCOL
|
||||
):
|
||||
files.append(
|
||||
f"{settings.AWS_S3_URL_PROTOCOL}//{settings.AWS_S3_CUSTOM_DOMAIN}/{content['Key']}"
|
||||
)
|
||||
else:
|
||||
files.append(
|
||||
f"https://{settings.AWS_STORAGE_BUCKET_NAME}.s3.{settings.AWS_REGION}.amazonaws.com/{content['Key']}"
|
||||
)
|
||||
files.append(
|
||||
f"https://{settings.AWS_STORAGE_BUCKET_NAME}.s3.{settings.AWS_REGION}.amazonaws.com/{content['Key']}"
|
||||
)
|
||||
|
||||
return Response(files, status=status.HTTP_200_OK)
|
||||
|
||||
@@ -1099,20 +1049,3 @@ 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)
|
||||
|
||||
@@ -50,8 +50,7 @@ class GlobalSearchEndpoint(BaseAPIView):
|
||||
q = Q()
|
||||
for field in fields:
|
||||
if field == "sequence_id":
|
||||
# Match whole integers only (exclude decimal numbers)
|
||||
sequences = re.findall(r"\b\d+\b", query)
|
||||
sequences = re.findall(r"\d+\.\d+|\d+", query)
|
||||
for sequence_id in sequences:
|
||||
q |= Q(**{"sequence_id": sequence_id})
|
||||
else:
|
||||
|
||||
@@ -49,10 +49,6 @@ 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 = []
|
||||
|
||||
|
||||
@@ -70,7 +70,6 @@ from plane.app.permissions import (
|
||||
WorkSpaceAdminPermission,
|
||||
WorkspaceEntityPermission,
|
||||
WorkspaceViewerPermission,
|
||||
WorkspaceUserPermission,
|
||||
)
|
||||
from plane.bgtasks.workspace_invitation_task import workspace_invitation
|
||||
from plane.utils.issue_filters import issue_filters
|
||||
@@ -496,18 +495,6 @@ class WorkSpaceMemberViewSet(BaseViewSet):
|
||||
WorkspaceEntityPermission,
|
||||
]
|
||||
|
||||
def get_permissions(self):
|
||||
if self.action == "leave":
|
||||
self.permission_classes = [
|
||||
WorkspaceUserPermission,
|
||||
]
|
||||
else:
|
||||
self.permission_classes = [
|
||||
WorkspaceEntityPermission,
|
||||
]
|
||||
|
||||
return super(WorkSpaceMemberViewSet, self).get_permissions()
|
||||
|
||||
search_fields = [
|
||||
"member__display_name",
|
||||
"member__first_name",
|
||||
|
||||
@@ -65,7 +65,7 @@ def send_export_email(email, slug, csv_buffer, rows):
|
||||
port=int(EMAIL_PORT),
|
||||
username=EMAIL_HOST_USER,
|
||||
password=EMAIL_HOST_PASSWORD,
|
||||
use_tls=EMAIL_USE_TLS == "1",
|
||||
use_tls=bool(EMAIL_USE_TLS),
|
||||
)
|
||||
|
||||
msg = EmailMultiAlternatives(
|
||||
|
||||
@@ -51,7 +51,7 @@ def forgot_password(first_name, email, uidb64, token, current_site):
|
||||
port=int(EMAIL_PORT),
|
||||
username=EMAIL_HOST_USER,
|
||||
password=EMAIL_HOST_PASSWORD,
|
||||
use_tls=EMAIL_USE_TLS == "1",
|
||||
use_tls=bool(EMAIL_USE_TLS),
|
||||
)
|
||||
|
||||
msg = EmailMultiAlternatives(
|
||||
|
||||
@@ -112,8 +112,8 @@ def track_parent(
|
||||
epoch,
|
||||
):
|
||||
if current_instance.get("parent") != requested_data.get("parent"):
|
||||
old_parent = Issue.objects.filter(pk=current_instance.get("parent")).first() if current_instance.get("parent") is not None else None
|
||||
new_parent = Issue.objects.filter(pk=requested_data.get("parent")).first() if requested_data.get("parent") is not None else None
|
||||
old_parent = Issue.objects.filter(pk=current_instance.get("parent")).first()
|
||||
new_parent = Issue.objects.filter(pk=requested_data.get("parent")).first()
|
||||
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
|
||||
@@ -41,7 +41,7 @@ def magic_link(email, key, token, current_site):
|
||||
port=int(EMAIL_PORT),
|
||||
username=EMAIL_HOST_USER,
|
||||
password=EMAIL_HOST_PASSWORD,
|
||||
use_tls=EMAIL_USE_TLS == "1",
|
||||
use_tls=bool(EMAIL_USE_TLS),
|
||||
)
|
||||
|
||||
msg = EmailMultiAlternatives(
|
||||
|
||||
@@ -60,7 +60,7 @@ def project_invitation(email, project_id, token, current_site, invitor):
|
||||
port=int(EMAIL_PORT),
|
||||
username=EMAIL_HOST_USER,
|
||||
password=EMAIL_HOST_PASSWORD,
|
||||
use_tls=EMAIL_USE_TLS == "1",
|
||||
use_tls=bool(EMAIL_USE_TLS),
|
||||
)
|
||||
|
||||
msg = EmailMultiAlternatives(
|
||||
|
||||
@@ -70,7 +70,7 @@ def workspace_invitation(email, workspace_id, token, current_site, invitor):
|
||||
port=int(EMAIL_PORT),
|
||||
username=EMAIL_HOST_USER,
|
||||
password=EMAIL_HOST_PASSWORD,
|
||||
use_tls=EMAIL_USE_TLS == "1",
|
||||
use_tls=bool(EMAIL_USE_TLS),
|
||||
)
|
||||
|
||||
msg = EmailMultiAlternatives(
|
||||
|
||||
-83
@@ -1,83 +0,0 @@
|
||||
# Generated by Django 4.2.7 on 2023-12-29 10:20
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('db', '0050_user_use_case_alter_workspace_organization_size'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='cycle',
|
||||
name='external_id',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='cycle',
|
||||
name='external_source',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='inboxissue',
|
||||
name='external_id',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='inboxissue',
|
||||
name='external_source',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='issue',
|
||||
name='external_id',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='issue',
|
||||
name='external_source',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='issuecomment',
|
||||
name='external_id',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='issuecomment',
|
||||
name='external_source',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='label',
|
||||
name='external_id',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='label',
|
||||
name='external_source',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='module',
|
||||
name='external_id',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='module',
|
||||
name='external_source',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='state',
|
||||
name='external_id',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='state',
|
||||
name='external_source',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
]
|
||||
@@ -18,8 +18,6 @@ class Cycle(ProjectBaseModel):
|
||||
)
|
||||
view_props = models.JSONField(default=dict)
|
||||
sort_order = models.FloatField(default=65535)
|
||||
external_source = models.CharField(max_length=255, null=True, blank=True)
|
||||
external_id = models.CharField(max_length=255, blank=True, null=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Cycle"
|
||||
|
||||
@@ -39,8 +39,6 @@ class InboxIssue(ProjectBaseModel):
|
||||
"db.Issue", related_name="inbox_duplicate", on_delete=models.SET_NULL, null=True
|
||||
)
|
||||
source = models.TextField(blank=True, null=True)
|
||||
external_source = models.CharField(max_length=255, null=True, blank=True)
|
||||
external_id = models.CharField(max_length=255, blank=True, null=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "InboxIssue"
|
||||
|
||||
@@ -102,8 +102,6 @@ class Issue(ProjectBaseModel):
|
||||
completed_at = models.DateTimeField(null=True)
|
||||
archived_at = models.DateField(null=True)
|
||||
is_draft = models.BooleanField(default=False)
|
||||
external_source = models.CharField(max_length=255, null=True, blank=True)
|
||||
external_id = models.CharField(max_length=255, blank=True, null=True)
|
||||
|
||||
objects = models.Manager()
|
||||
issue_objects = IssueManager()
|
||||
@@ -368,8 +366,6 @@ class IssueComment(ProjectBaseModel):
|
||||
default="INTERNAL",
|
||||
max_length=100,
|
||||
)
|
||||
external_source = models.CharField(max_length=255, null=True, blank=True)
|
||||
external_id = models.CharField(max_length=255, blank=True, null=True)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.comment_stripped = (
|
||||
@@ -420,8 +416,6 @@ class Label(ProjectBaseModel):
|
||||
description = models.TextField(blank=True)
|
||||
color = models.CharField(max_length=255, blank=True)
|
||||
sort_order = models.FloatField(default=65535)
|
||||
external_source = models.CharField(max_length=255, null=True, blank=True)
|
||||
external_id = models.CharField(max_length=255, blank=True, null=True)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["name", "project"]
|
||||
|
||||
@@ -41,8 +41,6 @@ class Module(ProjectBaseModel):
|
||||
)
|
||||
view_props = models.JSONField(default=dict)
|
||||
sort_order = models.FloatField(default=65535)
|
||||
external_source = models.CharField(max_length=255, null=True, blank=True)
|
||||
external_id = models.CharField(max_length=255, blank=True, null=True)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["name", "project"]
|
||||
|
||||
@@ -24,8 +24,6 @@ class State(ProjectBaseModel):
|
||||
max_length=20,
|
||||
)
|
||||
default = models.BooleanField(default=False)
|
||||
external_source = models.CharField(max_length=255, null=True, blank=True)
|
||||
external_id = models.CharField(max_length=255, blank=True, null=True)
|
||||
|
||||
def __str__(self):
|
||||
"""Return name of the state"""
|
||||
|
||||
@@ -30,7 +30,7 @@ from plane.license.api.serializers import (
|
||||
from plane.license.api.permissions import (
|
||||
InstanceAdminPermission,
|
||||
)
|
||||
from plane.db.models import User, WorkspaceMember, ProjectMember
|
||||
from plane.db.models import User
|
||||
from plane.license.utils.encryption import encrypt_data
|
||||
|
||||
|
||||
|
||||
@@ -9,20 +9,14 @@ 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):
|
||||
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 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
|
||||
|
||||
|
||||
def decrypt_data(encrypted_data):
|
||||
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 ""
|
||||
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()
|
||||
|
||||
@@ -291,9 +291,7 @@ CELERY_IMPORTS = (
|
||||
|
||||
# Sentry Settings
|
||||
# Enable Sentry Settings
|
||||
if bool(os.environ.get("SENTRY_DSN", False)) and os.environ.get(
|
||||
"SENTRY_DSN"
|
||||
).startswith("https://"):
|
||||
if bool(os.environ.get("SENTRY_DSN", False)) and os.environ.get("SENTRY_DSN").startswith("https://"):
|
||||
sentry_sdk.init(
|
||||
dsn=os.environ.get("SENTRY_DSN", ""),
|
||||
integrations=[
|
||||
@@ -336,5 +334,3 @@ INSTANCE_KEY = os.environ.get(
|
||||
|
||||
# Skip environment variable configuration
|
||||
SKIP_ENV_VAR = os.environ.get("SKIP_ENV_VAR", "1") == "1"
|
||||
|
||||
DATA_UPLOAD_MAX_MEMORY_SIZE = int(os.environ.get("FILE_SIZE_LIMIT", 5242880))
|
||||
|
||||
@@ -19,11 +19,8 @@ urlpatterns = [
|
||||
|
||||
|
||||
if settings.DEBUG:
|
||||
try:
|
||||
import debug_toolbar
|
||||
import debug_toolbar
|
||||
|
||||
urlpatterns = [
|
||||
re_path(r"^__debug__/", include(debug_toolbar.urls)),
|
||||
] + urlpatterns
|
||||
except ImportError:
|
||||
pass
|
||||
urlpatterns = [
|
||||
re_path(r"^__debug__/", include(debug_toolbar.urls)),
|
||||
] + urlpatterns
|
||||
|
||||
@@ -30,7 +30,7 @@ openpyxl==3.1.2
|
||||
beautifulsoup4==4.12.2
|
||||
dj-database-url==2.1.0
|
||||
posthog==3.0.2
|
||||
cryptography==41.0.6
|
||||
cryptography==41.0.5
|
||||
lxml==4.9.3
|
||||
boto3==1.28.40
|
||||
|
||||
|
||||
@@ -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,6 +7,7 @@ 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
|
||||
@@ -20,6 +21,7 @@ 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,5 +1,8 @@
|
||||
|
||||
# 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)
|
||||
|
||||
|
||||
|
||||
+39
-43
@@ -1,20 +1,18 @@
|
||||
# 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>
|
||||
|
||||
---
|
||||
@@ -22,34 +20,30 @@ 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.
|
||||
|
||||
### Prerequisite
|
||||
Installing plane is a very easy and minimal step process.
|
||||
|
||||
### 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
|
||||
@@ -88,11 +82,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:
|
||||
@@ -106,27 +100,26 @@ 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.
|
||||
|
||||
@@ -155,15 +148,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
|
||||
|
||||
@@ -187,7 +180,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
|
||||
|
||||
@@ -213,7 +206,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:
|
||||
@@ -227,7 +220,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
|
||||
|
||||
@@ -235,17 +228,18 @@ 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
|
||||
|
||||
@@ -262,15 +256,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
|
||||
@@ -281,12 +275,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)
|
||||
@@ -295,9 +289,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
|
||||
@@ -308,6 +302,8 @@ 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.
|
||||
In case of successful migration, it will be a silent exit without error.
|
||||
|
||||
Now its time to restart v0.14.0 setup.
|
||||
|
||||
|
||||
Now its time to restart v0.14.0 setup.
|
||||
|
||||
@@ -6,6 +6,7 @@ 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"}
|
||||
@@ -40,7 +41,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:-""}
|
||||
- OPENAI_API_KEY=${OPENAI_API_KEY:-"sk-"}
|
||||
- GPT_ENGINE=${GPT_ENGINE:-"gpt-3.5-turbo"}
|
||||
# LOGIN/SIGNUP SETTINGS - Deprecated can be configured through admin panel
|
||||
- ENABLE_SIGNUP=${ENABLE_SIGNUP:-1}
|
||||
@@ -66,7 +67,7 @@ services:
|
||||
web:
|
||||
<<: *app-env
|
||||
platform: linux/amd64
|
||||
image: makeplane/plane-frontend:${APP_RELEASE:-latest}
|
||||
image: makeplane/plane-frontend-ce:${APP_RELEASE:-latest}
|
||||
restart: unless-stopped
|
||||
command: /usr/local/bin/start.sh web/server.js web
|
||||
deploy:
|
||||
@@ -78,7 +79,7 @@ services:
|
||||
space:
|
||||
<<: *app-env
|
||||
platform: linux/amd64
|
||||
image: makeplane/plane-space:${APP_RELEASE:-latest}
|
||||
image: makeplane/plane-space-ce:${APP_RELEASE:-latest}
|
||||
restart: unless-stopped
|
||||
command: /usr/local/bin/start.sh space/server.js space
|
||||
deploy:
|
||||
@@ -91,7 +92,7 @@ services:
|
||||
api:
|
||||
<<: *app-env
|
||||
platform: linux/amd64
|
||||
image: makeplane/plane-backend:${APP_RELEASE:-latest}
|
||||
image: makeplane/plane-backend-ce:${APP_RELEASE:-latest}
|
||||
restart: unless-stopped
|
||||
command: ./bin/takeoff
|
||||
deploy:
|
||||
@@ -103,7 +104,7 @@ services:
|
||||
worker:
|
||||
<<: *app-env
|
||||
platform: linux/amd64
|
||||
image: makeplane/plane-backend:${APP_RELEASE:-latest}
|
||||
image: makeplane/plane-backend-ce:${APP_RELEASE:-latest}
|
||||
restart: unless-stopped
|
||||
command: ./bin/worker
|
||||
depends_on:
|
||||
@@ -114,7 +115,7 @@ services:
|
||||
beat-worker:
|
||||
<<: *app-env
|
||||
platform: linux/amd64
|
||||
image: makeplane/plane-backend:${APP_RELEASE:-latest}
|
||||
image: makeplane/plane-backend-ce:${APP_RELEASE:-latest}
|
||||
restart: unless-stopped
|
||||
command: ./bin/beat
|
||||
depends_on:
|
||||
@@ -149,7 +150,7 @@ services:
|
||||
proxy:
|
||||
<<: *app-env
|
||||
platform: linux/amd64
|
||||
image: makeplane/plane-proxy:${APP_RELEASE:-latest}
|
||||
image: makeplane/plane-proxy-ce:${APP_RELEASE:-latest}
|
||||
ports:
|
||||
- ${NGINX_PORT}:80
|
||||
depends_on:
|
||||
|
||||
@@ -39,7 +39,7 @@ function download(){
|
||||
echo ""
|
||||
echo "Latest version is now available for you to use"
|
||||
echo ""
|
||||
echo "In case of Upgrade, your new setting file is available as 'variables-upgrade.env'. Please compare and set the required values in '.env 'file."
|
||||
echo "In case of Upgrade, your new setting file is availabe as 'variables-upgrade.env'. Please compare and set the required values in '.env 'file."
|
||||
echo ""
|
||||
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ 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"
|
||||
|
||||
@@ -12,6 +12,7 @@ volumes:
|
||||
|
||||
services:
|
||||
plane-redis:
|
||||
container_name: plane-redis
|
||||
image: redis:6.2.7-alpine
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
@@ -20,6 +21,7 @@ services:
|
||||
- redisdata:/data
|
||||
|
||||
plane-minio:
|
||||
container_name: plane-minio
|
||||
image: minio/minio
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
@@ -34,6 +36,7 @@ services:
|
||||
MINIO_ROOT_PASSWORD: ${AWS_SECRET_ACCESS_KEY}
|
||||
|
||||
plane-db:
|
||||
container_name: plane-db
|
||||
image: postgres:15.2-alpine
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
@@ -50,6 +53,7 @@ services:
|
||||
PGDATA: /var/lib/postgresql/data
|
||||
|
||||
web:
|
||||
container_name: web
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./web/Dockerfile.dev
|
||||
@@ -57,7 +61,8 @@ services:
|
||||
networks:
|
||||
- dev_env
|
||||
volumes:
|
||||
- ./web:/app/web
|
||||
- .:/app
|
||||
command: yarn dev --filter=web
|
||||
env_file:
|
||||
- ./web/.env
|
||||
depends_on:
|
||||
@@ -68,17 +73,22 @@ services:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./space/Dockerfile.dev
|
||||
container_name: space
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- dev_env
|
||||
volumes:
|
||||
- ./space:/app/space
|
||||
- .:/app
|
||||
command: yarn dev --filter=space
|
||||
env_file:
|
||||
- ./space/.env
|
||||
depends_on:
|
||||
- api
|
||||
- worker
|
||||
- web
|
||||
|
||||
api:
|
||||
container_name: api
|
||||
build:
|
||||
context: ./apiserver
|
||||
dockerfile: Dockerfile.dev
|
||||
@@ -89,7 +99,7 @@ services:
|
||||
- dev_env
|
||||
volumes:
|
||||
- ./apiserver:/code
|
||||
# command: /bin/sh -c "python manage.py migrate && python manage.py runserver 0.0.0.0:8000 --settings=plane.settings.local"
|
||||
command: /bin/sh -c "python manage.py migrate && python manage.py runserver 0.0.0.0:8000 --settings=plane.settings.local"
|
||||
env_file:
|
||||
- ./apiserver/.env
|
||||
depends_on:
|
||||
@@ -97,6 +107,7 @@ services:
|
||||
- plane-redis
|
||||
|
||||
worker:
|
||||
container_name: bgworker
|
||||
build:
|
||||
context: ./apiserver
|
||||
dockerfile: Dockerfile.dev
|
||||
@@ -116,6 +127,7 @@ services:
|
||||
- plane-redis
|
||||
|
||||
beat-worker:
|
||||
container_name: beatworker
|
||||
build:
|
||||
context: ./apiserver
|
||||
dockerfile: Dockerfile.dev
|
||||
@@ -135,9 +147,10 @@ services:
|
||||
- plane-redis
|
||||
|
||||
proxy:
|
||||
container_name: proxy
|
||||
build:
|
||||
context: ./nginx
|
||||
dockerfile: Dockerfile.dev
|
||||
dockerfile: Dockerfile
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- dev_env
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
FROM nginx:1.25.0-alpine
|
||||
|
||||
RUN rm /etc/nginx/conf.d/default.conf
|
||||
COPY nginx.conf.dev /etc/nginx/nginx.conf.template
|
||||
|
||||
COPY ./env.sh /docker-entrypoint.sh
|
||||
|
||||
RUN chmod +x /docker-entrypoint.sh
|
||||
# Update all environment variables
|
||||
CMD ["/docker-entrypoint.sh"]
|
||||
@@ -18,7 +18,7 @@ server {
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
location /spaces/ {
|
||||
location /space/ {
|
||||
proxy_pass http://localhost:4000/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
events {
|
||||
}
|
||||
|
||||
http {
|
||||
sendfile on;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
root /www/data/;
|
||||
access_log /var/log/nginx/access.log;
|
||||
|
||||
client_max_body_size ${FILE_SIZE_LIMIT};
|
||||
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header Referrer-Policy "no-referrer-when-downgrade" always;
|
||||
add_header Permissions-Policy "interest-cohort=()" always;
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
|
||||
location / {
|
||||
proxy_pass http://web:3000/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://api:8000/api/;
|
||||
}
|
||||
|
||||
location /spaces/ {
|
||||
rewrite ^/spaces/?$ /spaces/login break;
|
||||
proxy_pass http://space:4000/spaces/;
|
||||
}
|
||||
|
||||
location /${BUCKET_NAME}/ {
|
||||
proxy_pass http://plane-minio:9000/uploads/;
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"repository": "https://github.com/makeplane/plane.git",
|
||||
"version": "0.14.0",
|
||||
"version": "0.13.2",
|
||||
"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.11.2"
|
||||
"turbo": "^1.10.16"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/react": "18.2.42"
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ["custom"],
|
||||
};
|
||||
@@ -1,6 +0,0 @@
|
||||
.next
|
||||
.vercel
|
||||
.tubro
|
||||
out/
|
||||
dis/
|
||||
build/
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"printWidth": 120,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/editor-core",
|
||||
"version": "0.14.0",
|
||||
"version": "0.0.1",
|
||||
"description": "Core Editor that powers Plane",
|
||||
"private": true,
|
||||
"main": "./dist/index.mjs",
|
||||
@@ -28,28 +28,34 @@
|
||||
"react-dom": "18.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tiptap/core": "^2.1.13",
|
||||
"@plane/editor-types": "*",
|
||||
"@tiptap/core": "^2.1.7",
|
||||
"@tiptap/extension-blockquote": "^2.1.13",
|
||||
"@tiptap/extension-code-block-lowlight": "^2.1.13",
|
||||
"@tiptap/extension-color": "^2.1.13",
|
||||
"@tiptap/extension-image": "^2.1.13",
|
||||
"@tiptap/extension-link": "^2.1.13",
|
||||
"@tiptap/extension-list-item": "^2.1.13",
|
||||
"@tiptap/extension-mention": "^2.1.13",
|
||||
"@tiptap/extension-task-item": "^2.1.13",
|
||||
"@tiptap/extension-task-list": "^2.1.13",
|
||||
"@tiptap/extension-text-style": "^2.1.13",
|
||||
"@tiptap/extension-underline": "^2.1.13",
|
||||
"@tiptap/pm": "^2.1.13",
|
||||
"@tiptap/react": "^2.1.13",
|
||||
"@tiptap/starter-kit": "^2.1.13",
|
||||
"@tiptap/suggestion": "^2.0.13",
|
||||
"@tiptap/extension-code-block-lowlight": "^2.1.12",
|
||||
"@tiptap/extension-color": "^2.1.11",
|
||||
"@tiptap/extension-image": "^2.1.7",
|
||||
"@tiptap/extension-link": "^2.1.7",
|
||||
"@tiptap/extension-list-item": "^2.1.12",
|
||||
"@tiptap/extension-mention": "^2.1.12",
|
||||
"@tiptap/extension-table": "^2.1.6",
|
||||
"@tiptap/extension-table-cell": "^2.1.6",
|
||||
"@tiptap/extension-table-header": "^2.1.6",
|
||||
"@tiptap/extension-table-row": "^2.1.6",
|
||||
"@tiptap/extension-task-item": "^2.1.7",
|
||||
"@tiptap/extension-task-list": "^2.1.7",
|
||||
"@tiptap/extension-text-style": "^2.1.11",
|
||||
"@tiptap/extension-underline": "^2.1.7",
|
||||
"@tiptap/pm": "^2.1.7",
|
||||
"@tiptap/prosemirror-tables": "^1.1.4",
|
||||
"@tiptap/react": "^2.1.7",
|
||||
"@tiptap/starter-kit": "^2.1.10",
|
||||
"@tiptap/suggestion": "^2.0.4",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^1.2.1",
|
||||
"highlight.js": "^11.8.0",
|
||||
"jsx-dom-cjs": "^8.0.3",
|
||||
"lowlight": "^3.0.0",
|
||||
"lucide-react": "^0.294.0",
|
||||
"lucide-react": "^0.244.0",
|
||||
"react-moveable": "^0.54.2",
|
||||
"tailwind-merge": "^1.14.0",
|
||||
"tippy.js": "^6.3.7",
|
||||
@@ -74,4 +80,4 @@
|
||||
"nextjs",
|
||||
"react"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,32 +1,23 @@
|
||||
// styles
|
||||
// import "./styles/tailwind.css";
|
||||
import "src/styles/editor.css";
|
||||
import "src/styles/table.css";
|
||||
import "src/styles/github-dark.css";
|
||||
// import "./styles/editor.css";
|
||||
import "./styles/github-dark.css";
|
||||
|
||||
export { isCellSelection } from "src/ui/extensions/table/table/utilities/is-cell-selection";
|
||||
export { isCellSelection } from "./ui/extensions/table/table/utilities/is-cell-selection";
|
||||
|
||||
// utils
|
||||
export * from "src/lib/utils";
|
||||
export * from "src/ui/extensions/table/table";
|
||||
export { startImageUpload } from "src/ui/plugins/upload-image";
|
||||
export * from "./lib/utils";
|
||||
export * from "./ui/extensions/table/table";
|
||||
export { startImageUpload } from "./ui/plugins/upload-image";
|
||||
|
||||
// components
|
||||
export { EditorContainer } from "src/ui/components/editor-container";
|
||||
export { EditorContentWrapper } from "src/ui/components/editor-content";
|
||||
export { EditorContainer } from "./ui/components/editor-container";
|
||||
export { EditorContentWrapper } from "./ui/components/editor-content";
|
||||
|
||||
// hooks
|
||||
export { useEditor } from "src/hooks/use-editor";
|
||||
export { useReadOnlyEditor } from "src/hooks/use-read-only-editor";
|
||||
export { useEditor } from "./ui/hooks/use-editor";
|
||||
export { useReadOnlyEditor } from "./ui/hooks/use-read-only-editor";
|
||||
|
||||
// helper items
|
||||
export * from "src/ui/menus/menu-items";
|
||||
export * from "src/lib/editor-commands";
|
||||
|
||||
// types
|
||||
export type { DeleteImage } from "src/types/delete-image";
|
||||
export type { UploadImage } from "src/types/upload-image";
|
||||
export type { RestoreImage } from "src/types/restore-image";
|
||||
export type { IMentionHighlight, IMentionSuggestion } from "src/types/mention-suggestion";
|
||||
export type { ISlashCommandItem, CommandProps } from "src/types/slash-commands-suggestion";
|
||||
export type { LucideIconType } from "src/types/lucide-icon";
|
||||
export * from "./ui/menus/menu-items";
|
||||
export * from "./lib/editor-commands";
|
||||
|
||||
@@ -1,20 +1,38 @@
|
||||
import { UploadImage } from "@plane/editor-types";
|
||||
import { Editor, Range } from "@tiptap/core";
|
||||
import { startImageUpload } from "src/ui/plugins/upload-image";
|
||||
import { findTableAncestor } from "src/lib/utils";
|
||||
import { UploadImage } from "src/types/upload-image";
|
||||
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();
|
||||
};
|
||||
|
||||
@@ -39,7 +57,8 @@ 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();
|
||||
};
|
||||
|
||||
@@ -59,8 +78,21 @@ 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) => {
|
||||
@@ -73,8 +105,19 @@ 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) => {
|
||||
@@ -88,8 +131,10 @@ 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,19 +6,25 @@ 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;
|
||||
}
|
||||
|
||||
@@ -6,12 +6,6 @@
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/* block quotes */
|
||||
.ProseMirror blockquote p::before,
|
||||
.ProseMirror blockquote p::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ProseMirror .is-empty::before {
|
||||
content: attr(data-placeholder);
|
||||
float: left;
|
||||
@@ -21,10 +15,9 @@
|
||||
}
|
||||
|
||||
/* Custom image styles */
|
||||
|
||||
.ProseMirror img {
|
||||
transition: filter 0.1s ease-in-out;
|
||||
margin-top: 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
@@ -60,12 +53,11 @@ ul[data-type="taskList"] li > label input[type="checkbox"] {
|
||||
background-color: rgb(var(--color-background-100));
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
width: 0.8rem;
|
||||
height: 0.8rem;
|
||||
width: 1.2rem;
|
||||
height: 1.2rem;
|
||||
position: relative;
|
||||
border: 1.5px solid rgb(var(--color-text-100));
|
||||
margin-right: 0.2rem;
|
||||
margin-top: 0.15rem;
|
||||
border: 2px solid rgb(var(--color-text-100));
|
||||
margin-right: 0.3rem;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
|
||||
@@ -79,8 +71,8 @@ ul[data-type="taskList"] li > label input[type="checkbox"] {
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
width: 0.5em;
|
||||
height: 0.5em;
|
||||
width: 0.65em;
|
||||
height: 0.65em;
|
||||
transform: scale(0);
|
||||
transition: 120ms transform ease-in-out;
|
||||
box-shadow: inset 1em 1em;
|
||||
@@ -141,8 +133,6 @@ ul[data-type="taskList"] li[data-checked="true"] > div > p {
|
||||
.img-placeholder {
|
||||
position: relative;
|
||||
width: 35%;
|
||||
margin-top: 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
|
||||
&:before {
|
||||
content: "";
|
||||
@@ -169,8 +159,7 @@ ul[data-type="taskList"] li[data-checked="true"] > div > p {
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
margin: 0.5em 0 0.5em 0;
|
||||
|
||||
margin: 0;
|
||||
border: 1px solid rgb(var(--color-border-200));
|
||||
width: 100%;
|
||||
|
||||
@@ -240,34 +229,3 @@ ul[data-type="taskList"] li[data-checked="true"] > div > p {
|
||||
.ProseMirror table * .is-empty::before {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.ProseMirror pre {
|
||||
background: rgba(var(--color-background-80));
|
||||
border-radius: 0.5rem;
|
||||
color: rgba(var(--color-text-100));
|
||||
font-family: "JetBrainsMono", monospace;
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
|
||||
.ProseMirror pre code {
|
||||
background: none;
|
||||
color: inherit;
|
||||
font-size: 0.8rem;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
div[data-type="horizontalRule"] {
|
||||
line-height: 0;
|
||||
padding: 0.25rem 0;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
|
||||
& > div {
|
||||
border-bottom: 1px solid rgb(var(--color-text-100));
|
||||
}
|
||||
}
|
||||
|
||||
/* image resizer */
|
||||
.moveable-control-box {
|
||||
z-index: 10 !important;
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
import { Smile } from "lucide-react";
|
||||
|
||||
export type LucideIconType = typeof Smile;
|
||||
@@ -7,7 +7,11 @@ interface EditorContainerProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const EditorContainer = ({ editor, editorClassNames, children }: EditorContainerProps) => (
|
||||
export const EditorContainer = ({
|
||||
editor,
|
||||
editorClassNames,
|
||||
children,
|
||||
}: EditorContainerProps) => (
|
||||
<div
|
||||
id="editor-container"
|
||||
onClick={() => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Editor, EditorContent } from "@tiptap/react";
|
||||
import { ReactNode } from "react";
|
||||
import { ImageResizer } from "src/ui/extensions/image/image-resize";
|
||||
import { ImageResizer } from "../extensions/image/image-resize";
|
||||
|
||||
interface EditorContentProps {
|
||||
editor: Editor | null;
|
||||
@@ -8,10 +8,16 @@ 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>
|
||||
);
|
||||
|
||||
+4
-1
@@ -2,7 +2,10 @@ 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);
|
||||
|
||||
|
||||
+6
-2
@@ -1,7 +1,7 @@
|
||||
import { getNodeAtPosition } from "@tiptap/core";
|
||||
import { EditorState } from "@tiptap/pm/state";
|
||||
|
||||
import { findListItemPos } from "src/ui/extensions/custom-list-keymap/list-helpers/find-list-item-pos";
|
||||
import { findListItemPos } from "./find-list-item-pos";
|
||||
|
||||
export const getNextListDepth = (typeOrName: string, state: EditorState) => {
|
||||
const listItemPos = findListItemPos(typeOrName, state);
|
||||
@@ -10,7 +10,11 @@ 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;
|
||||
};
|
||||
|
||||
+18
-6
@@ -1,10 +1,14 @@
|
||||
import { Editor, isAtStartOfNode, isNodeActive } from "@tiptap/core";
|
||||
import { Node } from "@tiptap/pm/model";
|
||||
|
||||
import { findListItemPos } from "src/ui/extensions/custom-list-keymap/list-helpers/find-list-item-pos";
|
||||
import { hasListBefore } from "src/ui/extensions/custom-list-keymap/list-helpers/has-list-before";
|
||||
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;
|
||||
@@ -19,7 +23,10 @@ export const handleBackspace = (editor: Editor, name: string, parentListTypes: s
|
||||
// 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);
|
||||
@@ -38,11 +45,16 @@ export const handleBackspace = (editor: Editor, name: string, parentListTypes: s
|
||||
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();
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import { Editor, isAtEndOfNode, isNodeActive } from "@tiptap/core";
|
||||
|
||||
import { nextListIsDeeper } from "src/ui/extensions/custom-list-keymap/list-helpers/next-list-is-deeper";
|
||||
import { nextListIsHigher } from "src/ui/extensions/custom-list-keymap/list-helpers/next-list-is-higher";
|
||||
import { nextListIsDeeper } from "./next-list-is-deeper";
|
||||
import { nextListIsHigher } from "./next-list-is-higher";
|
||||
|
||||
export const handleDelete = (editor: Editor, name: string) => {
|
||||
// if the cursor is not inside the current node type
|
||||
|
||||
+5
-1
@@ -1,6 +1,10 @@
|
||||
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);
|
||||
|
||||
+4
-1
@@ -1,6 +1,9 @@
|
||||
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);
|
||||
|
||||
+4
-1
@@ -1,6 +1,9 @@
|
||||
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);
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import { EditorState } from "@tiptap/pm/state";
|
||||
|
||||
import { findListItemPos } from "src/ui/extensions/custom-list-keymap/list-helpers/find-list-item-pos";
|
||||
import { getNextListDepth } from "src/ui/extensions/custom-list-keymap/list-helpers/get-next-list-depth";
|
||||
import { findListItemPos } from "./find-list-item-pos";
|
||||
import { getNextListDepth } from "./get-next-list-depth";
|
||||
|
||||
export const nextListIsDeeper = (typeOrName: string, state: EditorState) => {
|
||||
const listDepth = getNextListDepth(typeOrName, state);
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import { EditorState } from "@tiptap/pm/state";
|
||||
|
||||
import { findListItemPos } from "src/ui/extensions/custom-list-keymap/list-helpers/find-list-item-pos";
|
||||
import { getNextListDepth } from "src/ui/extensions/custom-list-keymap/list-helpers/get-next-list-depth";
|
||||
import { findListItemPos } from "./find-list-item-pos";
|
||||
import { getNextListDepth } from "./get-next-list-depth";
|
||||
|
||||
export const nextListIsHigher = (typeOrName: string, state: EditorState) => {
|
||||
const listDepth = getNextListDepth(typeOrName, state);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Extension } from "@tiptap/core";
|
||||
|
||||
import { handleBackspace, handleDelete } from "src/ui/extensions/custom-list-keymap/list-helpers";
|
||||
import { handleBackspace, handleDelete } from "./list-helpers";
|
||||
|
||||
export type ListKeymapOptions = {
|
||||
listTypes: Array<{
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
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:
|
||||
@@ -22,7 +28,7 @@ declare module "@tiptap/core" {
|
||||
}
|
||||
}
|
||||
|
||||
export const HorizontalRule = Node.create<HorizontalRuleOptions>({
|
||||
export default Node.create<HorizontalRuleOptions>({
|
||||
name: "horizontalRule",
|
||||
|
||||
addOptions() {
|
||||
@@ -77,7 +83,8 @@ export const HorizontalRule = 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,7 +4,9 @@ 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({
|
||||
@@ -30,7 +32,9 @@ 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);
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { EditorState, Plugin, PluginKey, Transaction } from "@tiptap/pm/state";
|
||||
import { Node as ProseMirrorNode } from "@tiptap/pm/model";
|
||||
import { UploadImagesPlugin } from "src/ui/plugins/upload-image";
|
||||
import UploadImagesPlugin from "../../plugins/upload-image";
|
||||
import ImageExt from "@tiptap/extension-image";
|
||||
import { onNodeDeleted, onNodeRestored } from "src/ui/plugins/delete-image";
|
||||
import { DeleteImage } from "src/types/delete-image";
|
||||
import { RestoreImage } from "src/types/restore-image";
|
||||
import { onNodeDeleted, onNodeRestored } from "../../plugins/delete-image";
|
||||
import { DeleteImage, RestoreImage } from "@plane/editor-types";
|
||||
|
||||
interface ImageNode extends ProseMirrorNode {
|
||||
attrs: {
|
||||
@@ -16,14 +15,22 @@ interface ImageNode extends ProseMirrorNode {
|
||||
const deleteKey = new PluginKey("delete-image");
|
||||
const IMAGE_NODE_TYPE = "image";
|
||||
|
||||
export 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) {
|
||||
@@ -60,7 +67,11 @@ export const ImageExtension = (deleteImage: DeleteImage, restoreFile: RestoreIma
|
||||
}),
|
||||
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) {
|
||||
@@ -131,3 +142,5 @@ export const ImageExtension = (deleteImage: DeleteImage, restoreFile: RestoreIma
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default ImageExtension;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Image from "@tiptap/extension-image";
|
||||
|
||||
export const ReadOnlyImageExtension = Image.extend({
|
||||
const ReadOnlyImageExtension = Image.extend({
|
||||
addAttributes() {
|
||||
return {
|
||||
...this.parent?.(),
|
||||
@@ -13,3 +13,5 @@ export const ReadOnlyImageExtension = Image.extend({
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default ReadOnlyImageExtension;
|
||||
|
||||
@@ -7,25 +7,26 @@ import TaskItem from "@tiptap/extension-task-item";
|
||||
import TaskList from "@tiptap/extension-task-list";
|
||||
import { Markdown } from "tiptap-markdown";
|
||||
|
||||
import { TableHeader } from "src/ui/extensions/table/table-header/table-header";
|
||||
import { Table } from "src/ui/extensions/table/table";
|
||||
import { TableCell } from "src/ui/extensions/table/table-cell/table-cell";
|
||||
import { TableRow } from "src/ui/extensions/table/table-row/table-row";
|
||||
import { HorizontalRule } from "src/ui/extensions/horizontal-rule";
|
||||
import TableHeader from "./table/table-header/table-header";
|
||||
import Table from "./table/table";
|
||||
import TableCell from "./table/table-cell/table-cell";
|
||||
import TableRow from "./table/table-row/table-row";
|
||||
import HorizontalRule from "./horizontal-rule";
|
||||
|
||||
import { ImageExtension } from "src/ui/extensions/image";
|
||||
import ImageExtension from "./image";
|
||||
|
||||
import { isValidHttpUrl } from "src/lib/utils";
|
||||
import { Mentions } from "src/ui/mentions";
|
||||
import { isValidHttpUrl } from "../../lib/utils";
|
||||
import { Mentions } from "../mentions";
|
||||
|
||||
import { CustomKeymap } from "src/ui/extensions/keymap";
|
||||
import { CustomCodeBlock } from "src/ui/extensions/code";
|
||||
import { CustomQuoteExtension } from "src/ui/extensions/quote";
|
||||
import { ListKeymap } from "src/ui/extensions/custom-list-keymap";
|
||||
|
||||
import { DeleteImage } from "src/types/delete-image";
|
||||
import { IMentionSuggestion } from "src/types/mention-suggestion";
|
||||
import { RestoreImage } from "src/types/restore-image";
|
||||
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";
|
||||
|
||||
export const CoreEditorExtensions = (
|
||||
mentionConfig: {
|
||||
@@ -52,12 +53,12 @@ export const CoreEditorExtensions = (
|
||||
class: "leading-normal -mb-2",
|
||||
},
|
||||
},
|
||||
code: {
|
||||
HTMLAttributes: {
|
||||
class: "rounded-md bg-custom-primary-30 mx-1 px-1 py-1 font-mono font-medium text-custom-text-1000",
|
||||
spellcheck: "false",
|
||||
},
|
||||
},
|
||||
// blockquote: {
|
||||
// HTMLAttributes: {
|
||||
// class: "border-l-4 border-custom-border-300",
|
||||
// },
|
||||
// },
|
||||
code: false,
|
||||
codeBlock: false,
|
||||
horizontalRule: false,
|
||||
dropcursor: {
|
||||
@@ -108,5 +109,9 @@ export const CoreEditorExtensions = (
|
||||
TableHeader,
|
||||
TableCell,
|
||||
TableRow,
|
||||
Mentions(mentionConfig.mentionSuggestions, mentionConfig.mentionHighlights, false),
|
||||
Mentions(
|
||||
mentionConfig.mentionSuggestions,
|
||||
mentionConfig.mentionHighlights,
|
||||
false
|
||||
),
|
||||
];
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { TableCell } from "./table-cell";
|
||||
export { default as default } from "./table-cell";
|
||||
|
||||
@@ -4,7 +4,7 @@ export interface TableCellOptions {
|
||||
HTMLAttributes: Record<string, any>;
|
||||
}
|
||||
|
||||
export const TableCell = Node.create<TableCellOptions>({
|
||||
export default Node.create<TableCellOptions>({
|
||||
name: "tableCell",
|
||||
|
||||
addOptions() {
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { TableHeader } from "./table-header";
|
||||
export { default as default } from "./table-header";
|
||||
|
||||
@@ -3,8 +3,7 @@ import { mergeAttributes, Node } from "@tiptap/core";
|
||||
export interface TableHeaderOptions {
|
||||
HTMLAttributes: Record<string, any>;
|
||||
}
|
||||
|
||||
export const TableHeader = Node.create<TableHeaderOptions>({
|
||||
export default Node.create<TableHeaderOptions>({
|
||||
name: "tableHeader",
|
||||
|
||||
addOptions() {
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { TableRow } from "./table-row";
|
||||
export { default as default } from "./table-row";
|
||||
|
||||
@@ -4,7 +4,7 @@ export interface TableRowOptions {
|
||||
HTMLAttributes: Record<string, any>;
|
||||
}
|
||||
|
||||
export const TableRow = Node.create<TableRowOptions>({
|
||||
export default Node.create<TableRowOptions>({
|
||||
name: "tableRow",
|
||||
|
||||
addOptions() {
|
||||
@@ -22,6 +22,10 @@ export const TableRow = Node.create<TableRowOptions>({
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ["tr", mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0];
|
||||
return [
|
||||
"tr",
|
||||
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
|
||||
0,
|
||||
];
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const icons = {
|
||||
const icons = {
|
||||
colorPicker: `<svg xmlns="http://www.w3.org/2000/svg" length="24" viewBox="0 0 24 24" style="transform: ;msFilter:;"><path fill="rgb(var(--color-text-300))" d="M20 14c-.092.064-2 2.083-2 3.5 0 1.494.949 2.448 2 2.5.906.044 2-.891 2-2.5 0-1.5-1.908-3.436-2-3.5zM9.586 20c.378.378.88.586 1.414.586s1.036-.208 1.414-.586l7-7-.707-.707L11 4.586 8.707 2.293 7.293 3.707 9.586 6 4 11.586c-.378.378-.586.88-.586 1.414s.208 1.036.586 1.414L9.586 20zM11 7.414 16.586 13H5.414L11 7.414z"></path></svg>`,
|
||||
deleteColumn: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" length="24"><path fill="#e53e3e" d="M0 0H24V24H0z"/><path d="M12 3c.552 0 1 .448 1 1v8c.835-.628 1.874-1 3-1 2.761 0 5 2.239 5 5s-2.239 5-5 5c-1.032 0-1.99-.313-2.787-.848L13 20c0 .552-.448 1-1 1H6c-.552 0-1-.448-1-1V4c0-.552.448-1 1-1h6zm-1 2H7v14h4V5zm8 10h-6v2h6v-2z"/></svg>`,
|
||||
deleteRow: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" length="24"><path fill="#e53e3e" d="M0 0H24V24H0z"/><path d="M20 5c.552 0 1 .448 1 1v6c0 .552-.448 1-1 1 .628.835 1 1.874 1 3 0 2.761-2.239 5-5 5s-5-2.239-5-5c0-1.126.372-2.165 1-3H4c-.552 0-1-.448-1-1V6c0-.552.448-1 1-1h16zm-7 10v2h6v-2h-6zm6-8H5v4h14V7z"/></svg>`,
|
||||
@@ -47,3 +47,5 @@ export const icons = {
|
||||
</svg>
|
||||
`,
|
||||
};
|
||||
|
||||
export default icons;
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { Table } from "./table";
|
||||
export { default as default } from "./table";
|
||||
|
||||
@@ -20,12 +20,15 @@ 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,
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -37,11 +40,13 @@ export function tableControls() {
|
||||
if (!pos) return;
|
||||
|
||||
const table = findParentNode((node) => node.type.name === "table")(
|
||||
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)
|
||||
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;
|
||||
|
||||
@@ -50,7 +55,7 @@ export function tableControls() {
|
||||
view.state.tr.setMeta(key, {
|
||||
setHoveredTable: table,
|
||||
setHoveredCell: cell,
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
},
|
||||
@@ -63,7 +68,12 @@ 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,
|
||||
@@ -72,7 +82,7 @@ export function tableControls() {
|
||||
{
|
||||
hoveredTable,
|
||||
hoveredCell,
|
||||
}
|
||||
},
|
||||
),
|
||||
];
|
||||
|
||||
|
||||
@@ -4,9 +4,13 @@ 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/pm/tables";
|
||||
import {
|
||||
CellSelection,
|
||||
TableMap,
|
||||
updateColumnsOnResize,
|
||||
} from "@tiptap/prosemirror-tables";
|
||||
|
||||
import { icons } from "src/ui/extensions/table/table/icons";
|
||||
import icons from "./icons";
|
||||
|
||||
export function updateColumns(
|
||||
node: ProseMirrorNode,
|
||||
@@ -14,7 +18,7 @@ export function updateColumns(
|
||||
table: HTMLElement,
|
||||
cellMinWidth: number,
|
||||
overrideCol?: number,
|
||||
overrideValue?: any
|
||||
overrideValue?: any,
|
||||
) {
|
||||
let totalWidth = 0;
|
||||
let fixedWidth = true;
|
||||
@@ -27,7 +31,8 @@ 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;
|
||||
@@ -37,7 +42,8 @@ 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;
|
||||
@@ -92,12 +98,14 @@ 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",
|
||||
@@ -123,7 +131,8 @@ 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(),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -131,12 +140,14 @@ 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",
|
||||
@@ -148,7 +159,11 @@ const rowsToolboxItems = [
|
||||
}: {
|
||||
editor: Editor;
|
||||
triggerButton: HTMLButtonElement;
|
||||
controlsContainer: Element | "parent" | ((ref: Element) => Element) | undefined;
|
||||
controlsContainer:
|
||||
| Element
|
||||
| "parent"
|
||||
| ((ref: Element) => Element)
|
||||
| undefined;
|
||||
}) => {
|
||||
createColorPickerToolbox({
|
||||
triggerButton,
|
||||
@@ -162,7 +177,8 @@ 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(),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -197,9 +213,9 @@ function createToolbox({
|
||||
innerHTML: item.icon,
|
||||
}),
|
||||
h("div", { className: "label" }, item.label),
|
||||
]
|
||||
)
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
...tippyOptions,
|
||||
});
|
||||
@@ -256,11 +272,11 @@ function createColorPickerToolbox({
|
||||
{
|
||||
className: "label",
|
||||
},
|
||||
key
|
||||
key,
|
||||
),
|
||||
]
|
||||
)
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
onHidden: (instance) => {
|
||||
instance.destroy();
|
||||
@@ -303,7 +319,7 @@ export class TableView implements NodeView {
|
||||
cellMinWidth: number,
|
||||
decorations: Decoration[],
|
||||
editor: Editor,
|
||||
getPos: () => number
|
||||
getPos: () => number,
|
||||
) {
|
||||
this.node = node;
|
||||
this.cellMinWidth = cellMinWidth;
|
||||
@@ -321,7 +337,7 @@ export class TableView implements NodeView {
|
||||
itemType: "button",
|
||||
className: "rowsControlDiv",
|
||||
onClick: () => this.selectRow(),
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
this.columnsControl = h(
|
||||
@@ -331,14 +347,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({
|
||||
@@ -381,7 +397,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);
|
||||
@@ -392,7 +408,7 @@ export class TableView implements NodeView {
|
||||
className: "tableWrapper controls--disabled",
|
||||
},
|
||||
this.controls,
|
||||
this.table
|
||||
this.table,
|
||||
);
|
||||
|
||||
this.render();
|
||||
@@ -418,11 +434,18 @@ 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() {
|
||||
@@ -430,7 +453,9 @@ 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;
|
||||
@@ -441,7 +466,7 @@ export class TableView implements NodeView {
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, HTMLElement>
|
||||
{} as Record<string, HTMLElement>,
|
||||
) as any;
|
||||
|
||||
if (table === undefined || cell === undefined) {
|
||||
@@ -456,7 +481,9 @@ 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`;
|
||||
@@ -466,14 +493,22 @@ 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),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -481,13 +516,21 @@ 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,6 +1,12 @@
|
||||
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,
|
||||
@@ -19,12 +25,12 @@ import {
|
||||
tableEditing,
|
||||
toggleHeader,
|
||||
toggleHeaderCell,
|
||||
} from "@tiptap/pm/tables";
|
||||
} from "@tiptap/prosemirror-tables";
|
||||
|
||||
import { tableControls } from "src/ui/extensions/table/table/table-controls";
|
||||
import { TableView } from "src/ui/extensions/table/table/table-view";
|
||||
import { createTable } from "src/ui/extensions/table/table/utilities/create-table";
|
||||
import { deleteTableWhenAllCellsSelected } from "src/ui/extensions/table/table/utilities/delete-table-when-all-cells-selected";
|
||||
import { tableControls } from "./table-controls";
|
||||
import { TableView } from "./table-view";
|
||||
import { createTable } from "./utilities/create-table";
|
||||
import { deleteTableWhenAllCellsSelected } from "./utilities/delete-table-when-all-cells-selected";
|
||||
|
||||
export interface TableOptions {
|
||||
HTMLAttributes: Record<string, any>;
|
||||
@@ -38,7 +44,11 @@ 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;
|
||||
@@ -56,7 +66,10 @@ declare module "@tiptap/core" {
|
||||
goToNextCell: () => ReturnType;
|
||||
goToPreviousCell: () => ReturnType;
|
||||
fixTables: () => ReturnType;
|
||||
setCellSelection: (position: { anchorCell: number; headCell?: number }) => ReturnType;
|
||||
setCellSelection: (position: {
|
||||
anchorCell: number;
|
||||
headCell?: number;
|
||||
}) => ReturnType;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -72,7 +85,7 @@ declare module "@tiptap/core" {
|
||||
}
|
||||
}
|
||||
|
||||
export const Table = Node.create({
|
||||
export default Node.create({
|
||||
name: "table",
|
||||
|
||||
addOptions() {
|
||||
@@ -101,7 +114,11 @@ export const Table = Node.create({
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ["table", mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), ["tbody", 0]];
|
||||
return [
|
||||
"table",
|
||||
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
|
||||
["tbody", 0],
|
||||
];
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
@@ -203,7 +220,11 @@ export const Table = 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);
|
||||
@@ -239,7 +260,13 @@ export const Table = 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,
|
||||
);
|
||||
};
|
||||
},
|
||||
|
||||
@@ -262,7 +289,7 @@ export const Table = Node.create({
|
||||
|
||||
// @ts-ignore
|
||||
lastColumnResizable: this.options.lastColumnResizable,
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -277,7 +304,9 @@ export const Table = 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);
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { Fragment, Node as ProsemirrorNode, Schema } from "@tiptap/pm/model";
|
||||
|
||||
import { createCell } from "src/ui/extensions/table/table/utilities/create-cell";
|
||||
import { getTableNodeTypes } from "src/ui/extensions/table/table/utilities/get-table-node-types";
|
||||
import { createCell } from "./create-cell";
|
||||
import { getTableNodeTypes } from "./get-table-node-types";
|
||||
|
||||
export function createTable(
|
||||
schema: Schema,
|
||||
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,7 +33,12 @@ 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);
|
||||
|
||||
+12
-4
@@ -1,8 +1,13 @@
|
||||
import { findParentNodeClosestToPos, KeyboardShortcutCommand } from "@tiptap/core";
|
||||
import {
|
||||
findParentNodeClosestToPos,
|
||||
KeyboardShortcutCommand,
|
||||
} from "@tiptap/core";
|
||||
|
||||
import { isCellSelection } from "src/ui/extensions/table/table/utilities/is-cell-selection";
|
||||
import { isCellSelection } from "./is-cell-selection";
|
||||
|
||||
export const deleteTableWhenAllCellsSelected: KeyboardShortcutCommand = ({ editor }) => {
|
||||
export const deleteTableWhenAllCellsSelected: KeyboardShortcutCommand = ({
|
||||
editor,
|
||||
}) => {
|
||||
const { selection } = editor.state;
|
||||
|
||||
if (!isCellSelection(selection)) {
|
||||
@@ -10,7 +15,10 @@ export const deleteTableWhenAllCellsSelected: KeyboardShortcutCommand = ({ edito
|
||||
}
|
||||
|
||||
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") {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CellSelection } from "@tiptap/pm/tables";
|
||||
import { CellSelection } from "@tiptap/prosemirror-tables";
|
||||
|
||||
export function isCellSelection(value: unknown): value is CellSelection {
|
||||
return value instanceof CellSelection;
|
||||
|
||||
+16
-11
@@ -1,13 +1,15 @@
|
||||
import { useEditor as useCustomEditor, Editor } from "@tiptap/react";
|
||||
import { useImperativeHandle, useRef, MutableRefObject } from "react";
|
||||
import { CoreEditorProps } from "src/ui/props";
|
||||
import { CoreEditorExtensions } from "src/ui/extensions";
|
||||
import { CoreEditorProps } from "../props";
|
||||
import { CoreEditorExtensions } from "../extensions";
|
||||
import { EditorProps } from "@tiptap/pm/view";
|
||||
import { getTrimmedHTML } from "src/lib/utils";
|
||||
import { DeleteImage } from "src/types/delete-image";
|
||||
import { IMentionSuggestion } from "src/types/mention-suggestion";
|
||||
import { RestoreImage } from "src/types/restore-image";
|
||||
import { UploadImage } from "src/types/upload-image";
|
||||
import { getTrimmedHTML } from "../../lib/utils";
|
||||
import {
|
||||
DeleteImage,
|
||||
IMentionSuggestion,
|
||||
RestoreImage,
|
||||
UploadImage,
|
||||
} from "@plane/editor-types";
|
||||
|
||||
interface CustomEditorProps {
|
||||
uploadFile: UploadImage;
|
||||
@@ -18,7 +20,9 @@ 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;
|
||||
@@ -62,11 +66,12 @@ 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()));
|
||||
},
|
||||
@@ -77,7 +82,7 @@ export const useEditor = ({
|
||||
onChange?.(editor.getJSON(), getTrimmedHTML(editor.getHTML()));
|
||||
},
|
||||
},
|
||||
[rerenderOnPropsChange]
|
||||
[rerenderOnPropsChange],
|
||||
);
|
||||
|
||||
const editorRef: MutableRefObject<Editor | null> = useRef(null);
|
||||
+6
-5
@@ -1,9 +1,9 @@
|
||||
import { useEditor as useCustomEditor, Editor } from "@tiptap/react";
|
||||
import { useImperativeHandle, useRef, MutableRefObject } from "react";
|
||||
import { CoreReadOnlyEditorExtensions } from "src/ui/read-only/extensions";
|
||||
import { CoreReadOnlyEditorProps } from "src/ui/read-only/props";
|
||||
import { CoreReadOnlyEditorExtensions } from "../read-only/extensions";
|
||||
import { CoreReadOnlyEditorProps } from "../read-only/props";
|
||||
import { EditorProps } from "@tiptap/pm/view";
|
||||
import { IMentionSuggestion } from "src/types/mention-suggestion";
|
||||
import { IMentionSuggestion } from "@plane/editor-types";
|
||||
|
||||
interface CustomReadOnlyEditorProps {
|
||||
value: string;
|
||||
@@ -30,7 +30,8 @@ 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,
|
||||
@@ -43,7 +44,7 @@ export const useReadOnlyEditor = ({
|
||||
...extensions,
|
||||
],
|
||||
},
|
||||
[rerenderOnPropsChange]
|
||||
[rerenderOnPropsChange],
|
||||
);
|
||||
|
||||
const editorRef: MutableRefObject<Editor | null> = useRef(null);
|
||||
@@ -0,0 +1,105 @@
|
||||
"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 };
|
||||
+30
-11
@@ -1,15 +1,26 @@
|
||||
import { IMentionSuggestion } from "@plane/editor-types";
|
||||
import { Editor } from "@tiptap/react";
|
||||
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
|
||||
import { IMentionSuggestion } from "src/types/mention-suggestion";
|
||||
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;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react/display-name
|
||||
export const MentionList = forwardRef((props: MentionListProps, ref) => {
|
||||
const MentionList = forwardRef((props: MentionListProps, ref) => {
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
|
||||
const selectItem = (index: number) => {
|
||||
@@ -26,7 +37,9 @@ export 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 = () => {
|
||||
@@ -63,27 +76,31 @@ export const MentionList = forwardRef((props: MentionListProps, ref) => {
|
||||
}));
|
||||
|
||||
return props.items && props.items.length !== 0 ? (
|
||||
<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">
|
||||
<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">
|
||||
{props.items.length ? (
|
||||
props.items.map((item, index) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className={`flex cursor-pointer items-center gap-2 rounded p-1 hover:bg-custom-background-80 ${
|
||||
className={`flex items-center gap-2 rounded p-1 hover:bg-custom-background-80 cursor-pointer ${
|
||||
index === selectedIndex ? "bg-custom-background-80" : ""
|
||||
}`}
|
||||
onClick={() => selectItem(index)}
|
||||
>
|
||||
<div className="grid h-4 w-4 flex-shrink-0 place-items-center overflow-hidden">
|
||||
<div className="flex-shrink-0 h-4 w-4 grid place-items-center overflow-hidden">
|
||||
{item.avatar && item.avatar.trim() !== "" ? (
|
||||
<img src={item.avatar} className="h-full w-full rounded-sm object-cover" alt={item.title} />
|
||||
<img
|
||||
src={item.avatar}
|
||||
className="h-full w-full object-cover rounded-sm"
|
||||
alt={item.title}
|
||||
/>
|
||||
) : (
|
||||
<div className="grid h-full w-full place-items-center rounded-sm bg-gray-700 text-xs capitalize text-white">
|
||||
<div className="h-full w-full grid place-items-center text-xs capitalize text-white rounded-sm bg-gray-700">
|
||||
{item.title[0]}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-grow space-y-1 truncate">
|
||||
<p className="truncate text-sm font-medium">{item.title}</p>
|
||||
<p className="text-sm font-medium truncate">{item.title}</p>
|
||||
{/* <p className="text-xs text-gray-400">{item.subtitle}</p> */}
|
||||
</div>
|
||||
</div>
|
||||
@@ -98,3 +115,5 @@ export const MentionList = forwardRef((props: MentionListProps, ref) => {
|
||||
});
|
||||
|
||||
MentionList.displayName = "MentionList";
|
||||
|
||||
export default MentionList;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user