Compare commits
98 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2475097b4e | |||
| ad20079e0c | |||
| e14baf17a7 | |||
| 542b18a585 | |||
| 37d88cc05b | |||
| b652d1a8f1 | |||
| 98b7a941f9 | |||
| dc131ee05b | |||
| 0e3d15215d | |||
| 816b6abf3b | |||
| 6ecaa661a7 | |||
| 0b1efb173f | |||
| aceee7d2e2 | |||
| 0a41eff435 | |||
| 5f681973a0 | |||
| 9c65657a66 | |||
| 6fab75f9ab | |||
| 111070bdb9 | |||
| d76840ff6f | |||
| 1a2186cca4 | |||
| d4b6f4faf1 | |||
| fe9519314a | |||
| ac39bb9b1d | |||
| b8b58d3acd | |||
| 75ca932682 | |||
| be9c0be886 | |||
| ee08feb59b | |||
| 22b0bb4615 | |||
| 1e464d8613 | |||
| 3224dc4fe6 | |||
| 7010448c34 | |||
| 1cc18a0915 | |||
| a04ad4c4e2 | |||
| e141091e99 | |||
| 40b8b0ac35 | |||
| 47d6b152a0 | |||
| 216a7c8fda | |||
| 7bff8d2ec5 | |||
| 6f2cce081f | |||
| a86dafc11c | |||
| 81256d6373 | |||
| c9792da4a1 | |||
| 37df0bcdd8 | |||
| 18c86bd8cc | |||
| 0ee6c20272 | |||
| 184db0156c | |||
| b7a0f3c693 | |||
| e40f38e2e1 | |||
| 05e7afab8d | |||
| 969a51f425 | |||
| 849bc92aea | |||
| a37dec45d9 | |||
| e1793dda74 | |||
| 31fdaf2659 | |||
| 8a1a6c6f62 | |||
| 3d83101f69 | |||
| d473ba9d0d | |||
| e5902152ab | |||
| f03a9a6de8 | |||
| b7e2f1e57a | |||
| 885de6f679 | |||
| ce9714ff12 | |||
| 74b141eea2 | |||
| 08425c9614 | |||
| ecfcc03ef0 | |||
| 829c08f0ee | |||
| 2edd2d947e | |||
| 5b67f27345 | |||
| 7684a2c091 | |||
| 4e2bf24e8d | |||
| aafac9ed1d | |||
| 3adf48e429 | |||
| 1c546e3cc5 | |||
| 5c7382d894 | |||
| 1f8ae3a5ad | |||
| f1ed0c979c | |||
| 78b29eb81b | |||
| 8d3a0a2eec | |||
| 910d1a1de3 | |||
| 6004f29bbc | |||
| 9d0056cfee | |||
| 4bb99d5fbf | |||
| 6c61fbd102 | |||
| 2605b938f0 | |||
| b4f51cb5af | |||
| fe80ca3e1c | |||
| b78e83d81b | |||
| ee68c3ae86 | |||
| 239f68e260 | |||
| f949d57fa0 | |||
| dadd2cf39b | |||
| 13d7832d35 | |||
| 0f892d4670 | |||
| e8945f244d | |||
| a0588be405 | |||
| 29a0ba4ddc | |||
| 644073f063 | |||
| 63a15f2bf9 |
@@ -9,6 +9,7 @@ on:
|
||||
- preview
|
||||
- qa
|
||||
- develop
|
||||
- release-*
|
||||
release:
|
||||
types: [released, prereleased]
|
||||
|
||||
@@ -62,14 +63,14 @@ jobs:
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [branch_build_setup]
|
||||
env:
|
||||
FRONTEND_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend:${{ needs.branch_build_setup.outputs.gh_branch_name }}
|
||||
FRONTEND_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend${{ secrets.DOCKER_REPO_SUFFIX || '' }}:${{ 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:latest,${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend:${{ github.event.release.tag_name }}
|
||||
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 }}
|
||||
elif [ "${{ needs.branch_build_setup.outputs.gh_branch_name }}" == "master" ]; then
|
||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend:stable
|
||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend${{ secrets.DOCKER_REPO_SUFFIX || '' }}:stable
|
||||
else
|
||||
TAG=${{ env.FRONTEND_TAG }}
|
||||
fi
|
||||
@@ -104,14 +105,14 @@ jobs:
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [branch_build_setup]
|
||||
env:
|
||||
SPACE_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-space:${{ needs.branch_build_setup.outputs.gh_branch_name }}
|
||||
SPACE_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-space${{ secrets.DOCKER_REPO_SUFFIX || '' }}:${{ 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:latest,${{ secrets.DOCKERHUB_USERNAME }}/plane-space:${{ github.event.release.tag_name }}
|
||||
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 }}
|
||||
elif [ "${{ needs.branch_build_setup.outputs.gh_branch_name }}" == "master" ]; then
|
||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-space:stable
|
||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-space${{ secrets.DOCKER_REPO_SUFFIX || '' }}:stable
|
||||
else
|
||||
TAG=${{ env.SPACE_TAG }}
|
||||
fi
|
||||
@@ -146,14 +147,14 @@ jobs:
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [branch_build_setup]
|
||||
env:
|
||||
BACKEND_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-backend:${{ needs.branch_build_setup.outputs.gh_branch_name }}
|
||||
BACKEND_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-backend${{ secrets.DOCKER_REPO_SUFFIX || '' }}:${{ 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:latest,${{ secrets.DOCKERHUB_USERNAME }}/plane-backend:${{ github.event.release.tag_name }}
|
||||
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 }}
|
||||
elif [ "${{ needs.branch_build_setup.outputs.gh_branch_name }}" == "master" ]; then
|
||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-backend:stable
|
||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-backend${{ secrets.DOCKER_REPO_SUFFIX || '' }}:stable
|
||||
else
|
||||
TAG=${{ env.BACKEND_TAG }}
|
||||
fi
|
||||
@@ -188,14 +189,14 @@ jobs:
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [branch_build_setup]
|
||||
env:
|
||||
PROXY_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy:${{ needs.branch_build_setup.outputs.gh_branch_name }}
|
||||
PROXY_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy${{ secrets.DOCKER_REPO_SUFFIX || '' }}:${{ 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:latest,${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy:${{ github.event.release.tag_name }}
|
||||
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 }}
|
||||
elif [ "${{ needs.branch_build_setup.outputs.gh_branch_name }}" == "master" ]; then
|
||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy:stable
|
||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy${{ secrets.DOCKER_REPO_SUFFIX || '' }}:stable
|
||||
else
|
||||
TAG=${{ env.PROXY_TAG }}
|
||||
fi
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
name: Create PR in Plane EE Repository to sync the changes
|
||||
name: Create Sync Action
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- preview
|
||||
types:
|
||||
- closed
|
||||
env:
|
||||
SOURCE_BRANCH_NAME: ${{github.event.pull_request.base.ref}}
|
||||
|
||||
jobs:
|
||||
create_pr:
|
||||
@@ -16,27 +18,13 @@ 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
|
||||
@@ -45,35 +33,14 @@ jobs:
|
||||
sudo apt update
|
||||
sudo apt install gh -y
|
||||
|
||||
- name: Create Pull Request
|
||||
if: steps.check_repo.outputs.is_correct_repo == 'true'
|
||||
- name: Push Changes to Target Repo
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
||||
run: |
|
||||
TARGET_REPO="${{ secrets.TARGET_REPO_NAME }}"
|
||||
TARGET_BRANCH="${{ secrets.TARGET_REPO_BRANCH }}"
|
||||
TARGET_REPO="${{ secrets.SYNC_TARGET_REPO_NAME }}"
|
||||
TARGET_BRANCH="${{ secrets.SYNC_TARGET_BRANCH_NAME }}"
|
||||
SOURCE_BRANCH="${{ env.SOURCE_BRANCH_NAME }}"
|
||||
|
||||
git checkout $SOURCE_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
|
||||
git remote add target-origin "https://$GH_TOKEN@github.com/$TARGET_REPO.git"
|
||||
git push target-origin $SOURCE_BRANCH:$TARGET_BRANCH
|
||||
@@ -80,3 +80,6 @@ tmp/
|
||||
## packages
|
||||
dist
|
||||
.temp/
|
||||
|
||||
# logs
|
||||
combined.log
|
||||
|
||||
+4
-25
@@ -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
|
||||
cd plane
|
||||
git clone https://github.com/makeplane/plane.git [folder-name]
|
||||
cd [folder-name]
|
||||
chmod +x setup.sh
|
||||
```
|
||||
|
||||
@@ -44,33 +44,12 @@ chmod +x setup.sh
|
||||
./setup.sh
|
||||
```
|
||||
|
||||
3. Define `NEXT_PUBLIC_API_BASE_URL=http://localhost` in **web/.env** and **space/.env** file
|
||||
3. Start the containers
|
||||
|
||||
```bash
|
||||
echo "\nNEXT_PUBLIC_API_BASE_URL=http://localhost\n" >> ./web/.env
|
||||
docker compose -f docker-compose-local.yml up
|
||||
```
|
||||
|
||||
```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?
|
||||
|
||||
|
||||
@@ -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).
|
||||
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).
|
||||
|
||||
## ⚡️ 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) documentation page
|
||||
For self hosting environment setup, visit the [Self Hosting](https://docs.plane.so/self-hosting/docker-compose) documentation page
|
||||
|
||||
## 🚀 Features
|
||||
|
||||
|
||||
@@ -49,5 +49,5 @@ USER captain
|
||||
# Expose container port and run entry point script
|
||||
EXPOSE 8000
|
||||
|
||||
# CMD [ "./bin/takeoff" ]
|
||||
CMD [ "./bin/takeoff.local" ]
|
||||
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
web: gunicorn -w 4 -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:$PORT --max-requests 10000 --max-requests-jitter 1000 --access-logfile -
|
||||
worker: celery -A plane worker -l info
|
||||
worker: celery -A plane worker -l info -Q internal_tasks,external_tasks
|
||||
beat: celery -A plane beat -l INFO
|
||||
Executable
+31
@@ -0,0 +1,31 @@
|
||||
#!/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
|
||||
|
||||
@@ -281,20 +281,22 @@ class CycleAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
)
|
||||
cycle = Cycle.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
|
||||
|
||||
issue_activity.delay(
|
||||
type="cycle.activity.deleted",
|
||||
requested_data=json.dumps(
|
||||
{
|
||||
issue_activity.apply_async(
|
||||
args=[],
|
||||
kwargs={
|
||||
'type': "cycle.activity.deleted",
|
||||
'requested_data': json.dumps({
|
||||
"cycle_id": str(pk),
|
||||
"cycle_name": str(cycle.name),
|
||||
"issues": [str(issue_id) for issue_id in cycle_issues],
|
||||
}
|
||||
),
|
||||
actor_id=str(request.user.id),
|
||||
issue_id=None,
|
||||
project_id=str(project_id),
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
}),
|
||||
'actor_id': str(request.user.id),
|
||||
'issue_id': None,
|
||||
'project_id': str(project_id),
|
||||
'current_instance': None,
|
||||
'epoch': int(timezone.now().timestamp()),
|
||||
},
|
||||
routing_key='external',
|
||||
)
|
||||
# Delete the cycle
|
||||
cycle.delete()
|
||||
@@ -454,21 +456,21 @@ class CycleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
)
|
||||
|
||||
# Capture Issue Activity
|
||||
issue_activity.delay(
|
||||
type="cycle.activity.created",
|
||||
requested_data=json.dumps({"cycles_list": str(issues)}),
|
||||
actor_id=str(self.request.user.id),
|
||||
issue_id=None,
|
||||
project_id=str(self.kwargs.get("project_id", None)),
|
||||
current_instance=json.dumps(
|
||||
{
|
||||
issue_activity.apply_async(
|
||||
args=[],
|
||||
kwargs={
|
||||
'type': "cycle.activity.created",
|
||||
'requested_data': json.dumps({"cycles_list": str(issues)}),
|
||||
'actor_id': str(self.request.user.id),
|
||||
'issue_id': None,
|
||||
'project_id': str(self.kwargs.get("project_id", None)),
|
||||
'current_instance': json.dumps({
|
||||
"updated_cycle_issues": update_cycle_issue_activity,
|
||||
"created_cycle_issues": serializers.serialize(
|
||||
"json", record_to_create
|
||||
),
|
||||
}
|
||||
),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
"created_cycle_issues": serializers.serialize("json", record_to_create),
|
||||
}),
|
||||
'epoch': int(timezone.now().timestamp()),
|
||||
},
|
||||
routing_key='external',
|
||||
)
|
||||
|
||||
# Return all Cycle Issues
|
||||
@@ -483,19 +485,21 @@ class CycleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
)
|
||||
issue_id = cycle_issue.issue_id
|
||||
cycle_issue.delete()
|
||||
issue_activity.delay(
|
||||
type="cycle.activity.deleted",
|
||||
requested_data=json.dumps(
|
||||
{
|
||||
issue_activity.apply_async(
|
||||
args=[],
|
||||
kwargs={
|
||||
'type': "cycle.activity.deleted",
|
||||
'requested_data': json.dumps({
|
||||
"cycle_id": str(self.kwargs.get("cycle_id")),
|
||||
"issues": [str(issue_id)],
|
||||
}
|
||||
),
|
||||
actor_id=str(self.request.user.id),
|
||||
issue_id=str(issue_id),
|
||||
project_id=str(self.kwargs.get("project_id", None)),
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
}),
|
||||
'actor_id': str(self.request.user.id),
|
||||
'issue_id': str(issue_id),
|
||||
'project_id': str(self.kwargs.get("project_id", None)),
|
||||
'current_instance': None,
|
||||
'epoch': int(timezone.now().timestamp()),
|
||||
},
|
||||
routing_key='external',
|
||||
)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
@@ -142,14 +142,18 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
||||
)
|
||||
|
||||
# Create an Issue Activity
|
||||
issue_activity.delay(
|
||||
type="issue.activity.created",
|
||||
requested_data=json.dumps(request.data, cls=DjangoJSONEncoder),
|
||||
actor_id=str(request.user.id),
|
||||
issue_id=str(issue.id),
|
||||
project_id=str(project_id),
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
issue_activity.apply_async(
|
||||
args=[], # If no positional arguments are required
|
||||
kwargs={
|
||||
"type": "issue.activity.created",
|
||||
"requested_data": json.dumps(request.data, cls=DjangoJSONEncoder),
|
||||
"actor_id": str(request.user.id),
|
||||
"issue_id": str(issue.id),
|
||||
"project_id": str(project_id),
|
||||
"current_instance": None,
|
||||
"epoch": int(timezone.now().timestamp()),
|
||||
},
|
||||
routing_key="external",
|
||||
)
|
||||
|
||||
# create an inbox issue
|
||||
@@ -232,17 +236,21 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
||||
# Log all the updates
|
||||
requested_data = json.dumps(issue_data, cls=DjangoJSONEncoder)
|
||||
if issue is not None:
|
||||
issue_activity.delay(
|
||||
type="issue.activity.updated",
|
||||
requested_data=requested_data,
|
||||
actor_id=str(request.user.id),
|
||||
issue_id=str(issue_id),
|
||||
project_id=str(project_id),
|
||||
current_instance=json.dumps(
|
||||
IssueSerializer(current_instance).data,
|
||||
cls=DjangoJSONEncoder,
|
||||
),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
issue_activity.apply_async(
|
||||
args=[],
|
||||
kwargs={
|
||||
"type": "issue.activity.updated",
|
||||
"requested_data": requested_data,
|
||||
"actor_id": str(request.user.id),
|
||||
"issue_id": str(issue_id),
|
||||
"project_id": str(project_id),
|
||||
"current_instance": json.dumps(
|
||||
IssueSerializer(current_instance).data,
|
||||
cls=DjangoJSONEncoder,
|
||||
),
|
||||
"epoch": int(timezone.now().timestamp()),
|
||||
},
|
||||
routing_key="external",
|
||||
)
|
||||
issue_serializer.save()
|
||||
else:
|
||||
|
||||
@@ -207,14 +207,18 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
serializer.save()
|
||||
|
||||
# Track the issue
|
||||
issue_activity.delay(
|
||||
type="issue.activity.created",
|
||||
requested_data=json.dumps(self.request.data, cls=DjangoJSONEncoder),
|
||||
actor_id=str(request.user.id),
|
||||
issue_id=str(serializer.data.get("id", None)),
|
||||
project_id=str(project_id),
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
issue_activity.apply_async(
|
||||
args=[], # If no positional arguments are required
|
||||
kwargs={
|
||||
'type': "issue.activity.created",
|
||||
'requested_data': json.dumps(self.request.data, cls=DjangoJSONEncoder),
|
||||
'actor_id': str(request.user.id),
|
||||
'issue_id': str(serializer.data.get("id", None)),
|
||||
'project_id': str(project_id),
|
||||
'current_instance': None,
|
||||
'epoch': int(timezone.now().timestamp()),
|
||||
},
|
||||
routing_key='external',
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
@@ -228,14 +232,18 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
serializer = IssueSerializer(issue, data=request.data, partial=True)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
issue_activity.delay(
|
||||
type="issue.activity.updated",
|
||||
requested_data=requested_data,
|
||||
actor_id=str(request.user.id),
|
||||
issue_id=str(pk),
|
||||
project_id=str(project_id),
|
||||
current_instance=current_instance,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
issue_activity.apply_async(
|
||||
args=[],
|
||||
kwargs={
|
||||
'type': "issue.activity.updated",
|
||||
'requested_data': requested_data,
|
||||
'actor_id': str(request.user.id),
|
||||
'issue_id': str(pk),
|
||||
'project_id': str(project_id),
|
||||
'current_instance': current_instance,
|
||||
'epoch': int(timezone.now().timestamp()),
|
||||
},
|
||||
routing_key='external',
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
@@ -246,14 +254,19 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
IssueSerializer(issue).data, cls=DjangoJSONEncoder
|
||||
)
|
||||
issue.delete()
|
||||
issue_activity.delay(
|
||||
type="issue.activity.deleted",
|
||||
requested_data=json.dumps({"issue_id": str(pk)}),
|
||||
actor_id=str(request.user.id),
|
||||
issue_id=str(pk),
|
||||
project_id=str(project_id),
|
||||
current_instance=current_instance,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
issue_activity.apply_async(
|
||||
args=[],
|
||||
kwargs={
|
||||
'type': "issue.activity.deleted",
|
||||
'requested_data': json.dumps({"issue_id": str(pk)}),
|
||||
'actor_id': str(request.user.id),
|
||||
'issue_id': str(pk),
|
||||
'project_id': str(project_id),
|
||||
'current_instance': current_instance,
|
||||
'epoch': int(timezone.now().timestamp()),
|
||||
},
|
||||
routing_key='your_routing_key',
|
||||
queue='your_queue_name'
|
||||
)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@@ -309,7 +322,11 @@ class LabelAPIEndpoint(BaseAPIView):
|
||||
).data,
|
||||
)
|
||||
label = self.get_queryset().get(pk=pk)
|
||||
serializer = LabelSerializer(label, fields=self.fields, expand=self.expand,)
|
||||
serializer = LabelSerializer(
|
||||
label,
|
||||
fields=self.fields,
|
||||
expand=self.expand,
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
def patch(self, request, slug, project_id, pk=None):
|
||||
@@ -319,7 +336,6 @@ class LabelAPIEndpoint(BaseAPIView):
|
||||
serializer.save()
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
def delete(self, request, slug, project_id, pk=None):
|
||||
label = self.get_queryset().get(pk=pk)
|
||||
@@ -384,14 +400,18 @@ class IssueLinkAPIEndpoint(BaseAPIView):
|
||||
project_id=project_id,
|
||||
issue_id=issue_id,
|
||||
)
|
||||
issue_activity.delay(
|
||||
type="link.activity.created",
|
||||
requested_data=json.dumps(serializer.data, cls=DjangoJSONEncoder),
|
||||
actor_id=str(self.request.user.id),
|
||||
issue_id=str(self.kwargs.get("issue_id")),
|
||||
project_id=str(self.kwargs.get("project_id")),
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
issue_activity.apply_async(
|
||||
args=[], # If no positional arguments are required
|
||||
kwargs={
|
||||
'type': "link.activity.created",
|
||||
'requested_data': json.dumps(serializer.data, cls=DjangoJSONEncoder),
|
||||
'actor_id': str(self.request.user.id),
|
||||
'issue_id': str(self.kwargs.get("issue_id")),
|
||||
'project_id': str(self.kwargs.get("project_id")),
|
||||
'current_instance': None,
|
||||
'epoch': int(timezone.now().timestamp()),
|
||||
},
|
||||
routing_key='external',
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
@@ -408,14 +428,18 @@ class IssueLinkAPIEndpoint(BaseAPIView):
|
||||
serializer = IssueLinkSerializer(issue_link, data=request.data, partial=True)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
issue_activity.delay(
|
||||
type="link.activity.updated",
|
||||
requested_data=requested_data,
|
||||
actor_id=str(request.user.id),
|
||||
issue_id=str(issue_id),
|
||||
project_id=str(project_id),
|
||||
current_instance=current_instance,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
issue_activity.apply_async(
|
||||
args=[], # If no positional arguments are required
|
||||
kwargs={
|
||||
'type': "link.activity.updated",
|
||||
'requested_data': requested_data,
|
||||
'actor_id': str(request.user.id),
|
||||
'issue_id': str(issue_id),
|
||||
'project_id': str(project_id),
|
||||
'current_instance': current_instance,
|
||||
'epoch': int(timezone.now().timestamp()),
|
||||
},
|
||||
routing_key='external',
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
@@ -428,14 +452,18 @@ class IssueLinkAPIEndpoint(BaseAPIView):
|
||||
IssueLinkSerializer(issue_link).data,
|
||||
cls=DjangoJSONEncoder,
|
||||
)
|
||||
issue_activity.delay(
|
||||
type="link.activity.deleted",
|
||||
requested_data=json.dumps({"link_id": str(pk)}),
|
||||
actor_id=str(request.user.id),
|
||||
issue_id=str(issue_id),
|
||||
project_id=str(project_id),
|
||||
current_instance=current_instance,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
issue_activity.apply_async(
|
||||
args=[], # If no positional arguments are required
|
||||
kwargs={
|
||||
'type': "link.activity.deleted",
|
||||
'requested_data': json.dumps({"link_id": str(pk)}),
|
||||
'actor_id': str(request.user.id),
|
||||
'issue_id': str(issue_id),
|
||||
'project_id': str(project_id),
|
||||
'current_instance': current_instance,
|
||||
'epoch': int(timezone.now().timestamp()),
|
||||
},
|
||||
routing_key='external',
|
||||
)
|
||||
issue_link.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
@@ -507,14 +535,20 @@ class IssueCommentAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
issue_id=issue_id,
|
||||
actor=request.user,
|
||||
)
|
||||
issue_activity.delay(
|
||||
type="comment.activity.created",
|
||||
requested_data=json.dumps(serializer.data, cls=DjangoJSONEncoder),
|
||||
actor_id=str(self.request.user.id),
|
||||
issue_id=str(self.kwargs.get("issue_id")),
|
||||
project_id=str(self.kwargs.get("project_id")),
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
issue_activity.apply_async(
|
||||
args=[],
|
||||
kwargs={
|
||||
"type": "comment.activity.created",
|
||||
"requested_data": json.dumps(
|
||||
serializer.data, cls=DjangoJSONEncoder
|
||||
),
|
||||
"actor_id": str(self.request.user.id),
|
||||
"issue_id": str(self.kwargs.get("issue_id")),
|
||||
"project_id": str(self.kwargs.get("project_id")),
|
||||
"current_instance": None,
|
||||
"epoch": int(timezone.now().timestamp()),
|
||||
},
|
||||
routing_key="external",
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
@@ -533,14 +567,18 @@ class IssueCommentAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
issue_activity.delay(
|
||||
type="comment.activity.updated",
|
||||
requested_data=requested_data,
|
||||
actor_id=str(request.user.id),
|
||||
issue_id=str(issue_id),
|
||||
project_id=str(project_id),
|
||||
current_instance=current_instance,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
issue_activity.apply_async(
|
||||
args=[],
|
||||
kwargs={
|
||||
"type": "comment.activity.updated",
|
||||
"requested_data": requested_data,
|
||||
"actor_id": str(request.user.id),
|
||||
"issue_id": str(issue_id),
|
||||
"project_id": str(project_id),
|
||||
"current_instance": current_instance,
|
||||
"epoch": int(timezone.now().timestamp()),
|
||||
},
|
||||
routing_key="external",
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
@@ -554,14 +592,18 @@ class IssueCommentAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
cls=DjangoJSONEncoder,
|
||||
)
|
||||
issue_comment.delete()
|
||||
issue_activity.delay(
|
||||
type="comment.activity.deleted",
|
||||
requested_data=json.dumps({"comment_id": str(pk)}),
|
||||
actor_id=str(request.user.id),
|
||||
issue_id=str(issue_id),
|
||||
project_id=str(project_id),
|
||||
current_instance=current_instance,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
issue_activity.apply_async(
|
||||
args=[], # If no positional arguments are required
|
||||
kwargs={
|
||||
"type": "comment.activity.deleted",
|
||||
"requested_data": json.dumps({"comment_id": str(pk)}),
|
||||
"actor_id": str(request.user.id),
|
||||
"issue_id": str(issue_id),
|
||||
"project_id": str(project_id),
|
||||
"current_instance": current_instance,
|
||||
"epoch": int(timezone.now().timestamp()),
|
||||
},
|
||||
routing_key="external",
|
||||
)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@@ -582,7 +624,7 @@ class IssueActivityAPIEndpoint(BaseAPIView):
|
||||
)
|
||||
.select_related("actor", "workspace", "issue", "project")
|
||||
).order_by(request.GET.get("order_by", "created_at"))
|
||||
|
||||
|
||||
if pk:
|
||||
issue_activities = issue_activities.get(pk=pk)
|
||||
serializer = IssueActivitySerializer(issue_activities)
|
||||
|
||||
@@ -166,20 +166,22 @@ class ModuleAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
module_issues = list(
|
||||
ModuleIssue.objects.filter(module_id=pk).values_list("issue", flat=True)
|
||||
)
|
||||
issue_activity.delay(
|
||||
type="module.activity.deleted",
|
||||
requested_data=json.dumps(
|
||||
{
|
||||
issue_activity.apply_async(
|
||||
args=[],
|
||||
kwargs={
|
||||
'type': "module.activity.deleted",
|
||||
'requested_data': json.dumps({
|
||||
"module_id": str(pk),
|
||||
"module_name": str(module.name),
|
||||
"issues": [str(issue_id) for issue_id in module_issues],
|
||||
}
|
||||
),
|
||||
actor_id=str(request.user.id),
|
||||
issue_id=None,
|
||||
project_id=str(project_id),
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
}),
|
||||
'actor_id': str(request.user.id),
|
||||
'issue_id': None,
|
||||
'project_id': str(project_id),
|
||||
'current_instance': None,
|
||||
'epoch': int(timezone.now().timestamp()),
|
||||
},
|
||||
routing_key='external',
|
||||
)
|
||||
module.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
@@ -330,21 +332,21 @@ class ModuleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
)
|
||||
|
||||
# Capture Issue Activity
|
||||
issue_activity.delay(
|
||||
type="module.activity.created",
|
||||
requested_data=json.dumps({"modules_list": str(issues)}),
|
||||
actor_id=str(self.request.user.id),
|
||||
issue_id=None,
|
||||
project_id=str(self.kwargs.get("project_id", None)),
|
||||
current_instance=json.dumps(
|
||||
{
|
||||
issue_activity.apply_async(
|
||||
args=[],
|
||||
kwargs={
|
||||
'type': "module.activity.created",
|
||||
'requested_data': json.dumps({"modules_list": str(issues)}),
|
||||
'actor_id': str(self.request.user.id),
|
||||
'issue_id': None,
|
||||
'project_id': str(self.kwargs.get("project_id", None)),
|
||||
'current_instance': json.dumps({
|
||||
"updated_module_issues": update_module_issue_activity,
|
||||
"created_module_issues": serializers.serialize(
|
||||
"json", record_to_create
|
||||
),
|
||||
}
|
||||
),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
"created_module_issues": serializers.serialize("json", record_to_create),
|
||||
}),
|
||||
'epoch': int(timezone.now().timestamp()),
|
||||
},
|
||||
routing_key='external',
|
||||
)
|
||||
|
||||
return Response(
|
||||
@@ -357,18 +359,20 @@ class ModuleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
workspace__slug=slug, project_id=project_id, module_id=module_id, issue_id=issue_id
|
||||
)
|
||||
module_issue.delete()
|
||||
issue_activity.delay(
|
||||
type="module.activity.deleted",
|
||||
requested_data=json.dumps(
|
||||
{
|
||||
issue_activity.apply_async(
|
||||
args=[], # If no positional arguments are required
|
||||
kwargs={
|
||||
'type': "module.activity.deleted",
|
||||
'requested_data': json.dumps({
|
||||
"module_id": str(module_id),
|
||||
"issues": [str(module_issue.issue_id)],
|
||||
}
|
||||
),
|
||||
actor_id=str(request.user.id),
|
||||
issue_id=str(issue_id),
|
||||
project_id=str(project_id),
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
}),
|
||||
'actor_id': str(request.user.id),
|
||||
'issue_id': str(issue_id),
|
||||
'project_id': str(project_id),
|
||||
'current_instance': None,
|
||||
'epoch': int(timezone.now().timestamp()),
|
||||
},
|
||||
routing_key='external',
|
||||
)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
@@ -1,5 +1,7 @@
|
||||
# Python imports
|
||||
import uuid
|
||||
import json
|
||||
import requests
|
||||
|
||||
# Third party imports
|
||||
from rest_framework import status
|
||||
@@ -7,7 +9,7 @@ from rest_framework.response import Response
|
||||
|
||||
# Django imports
|
||||
from django.db.models import Max, Q
|
||||
|
||||
from django.conf import settings
|
||||
# Module imports
|
||||
from plane.app.views import BaseAPIView
|
||||
from plane.db.models import (
|
||||
@@ -34,20 +36,15 @@ from plane.app.serializers import (
|
||||
IssueFlatSerializer,
|
||||
ModuleSerializer,
|
||||
)
|
||||
from plane.utils.integrations.github import get_github_repo_details
|
||||
from plane.utils.importers.jira import jira_project_issue_summary
|
||||
from plane.bgtasks.importer_task import service_importer
|
||||
from plane.utils.html_processor import strip_tags
|
||||
from plane.app.permissions import WorkSpaceAdminPermission
|
||||
|
||||
from plane.bgtasks.importer_task import service_importer
|
||||
|
||||
class ServiceIssueImportSummaryEndpoint(BaseAPIView):
|
||||
|
||||
def get(self, request, slug, service):
|
||||
if service == "github":
|
||||
owner = request.GET.get("owner", False)
|
||||
repo = request.GET.get("repo", False)
|
||||
|
||||
if not owner or not repo:
|
||||
return Response(
|
||||
{"error": "Owner and repo are required"},
|
||||
@@ -58,11 +55,10 @@ class ServiceIssueImportSummaryEndpoint(BaseAPIView):
|
||||
integration__provider="github", workspace__slug=slug
|
||||
)
|
||||
|
||||
access_tokens_url = workspace_integration.metadata.get(
|
||||
"access_tokens_url", False
|
||||
)
|
||||
installtion_id = workspace_integration.config.get("installation_id", False)
|
||||
|
||||
if not access_tokens_url:
|
||||
# Check for the installation id
|
||||
if not installtion_id:
|
||||
return Response(
|
||||
{
|
||||
"error": "There was an error during the installation of the GitHub app. To resolve this issue, we recommend reinstalling the GitHub app."
|
||||
@@ -70,18 +66,33 @@ class ServiceIssueImportSummaryEndpoint(BaseAPIView):
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
issue_count, labels, collaborators = get_github_repo_details(
|
||||
access_tokens_url, owner, repo
|
||||
)
|
||||
# Request segway for the required information
|
||||
if settings.SEGWAY_BASE_URL:
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"x-api-key": settings.SEGWAY_KEY,
|
||||
}
|
||||
data = {
|
||||
"owner": owner,
|
||||
"repo": repo,
|
||||
"installationId": installtion_id,
|
||||
}
|
||||
res = requests.post(
|
||||
f"{settings.SEGWAY_BASE_URL}/api/github",
|
||||
data=json.dumps(data),
|
||||
headers=headers,
|
||||
)
|
||||
if "error" in res.json():
|
||||
return Response(res.json(), status=status.HTTP_400_BAD_REQUEST)
|
||||
else:
|
||||
return Response(
|
||||
res.json(),
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
return Response(
|
||||
{
|
||||
"issue_count": issue_count,
|
||||
"labels": labels,
|
||||
"collaborators": collaborators,
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
{"error": "Inetgration service is not available please try later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
if service == "jira":
|
||||
# Check for all the keys
|
||||
params = {
|
||||
@@ -102,16 +113,35 @@ class ServiceIssueImportSummaryEndpoint(BaseAPIView):
|
||||
email = request.GET.get("email", "")
|
||||
cloud_hostname = request.GET.get("cloud_hostname", "")
|
||||
|
||||
response = jira_project_issue_summary(
|
||||
email, api_token, project_key, cloud_hostname
|
||||
)
|
||||
if "error" in response:
|
||||
return Response(response, status=status.HTTP_400_BAD_REQUEST)
|
||||
else:
|
||||
return Response(
|
||||
response,
|
||||
status=status.HTTP_200_OK,
|
||||
if settings.SEGWAY_BASE_URL:
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"x-api-key": settings.SEGWAY_KEY,
|
||||
}
|
||||
data = {
|
||||
"project_key": project_key,
|
||||
"api_token": api_token,
|
||||
"email": email,
|
||||
"cloud_hostname": cloud_hostname,
|
||||
}
|
||||
res = requests.post(
|
||||
f"{settings.SEGWAY_BASE_URL}/api/jira",
|
||||
data=json.dumps(data),
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
if "error" in res.json():
|
||||
return Response(res.json(), status=status.HTTP_400_BAD_REQUEST)
|
||||
else:
|
||||
return Response(
|
||||
res.json(),
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
return Response(
|
||||
{"error": "Inetgration service is not available please try later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
return Response(
|
||||
{"error": "Service not supported yet"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
@@ -122,7 +152,21 @@ class ImportServiceEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
WorkSpaceAdminPermission,
|
||||
]
|
||||
|
||||
def post(self, request, slug, service):
|
||||
if service not in ["github", "jira"]:
|
||||
return Response(
|
||||
{"error": "Servivce not supported yet"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
if service == "github":
|
||||
workspace_integration = WorkspaceIntegration.objects.get(
|
||||
integration__provider="github", workspace__slug=slug
|
||||
)
|
||||
|
||||
installation_id = workspace_integration.config.get("installation_id", False)
|
||||
|
||||
project_id = request.data.get("project_id", False)
|
||||
|
||||
if not project_id:
|
||||
@@ -130,87 +174,84 @@ class ImportServiceEndpoint(BaseAPIView):
|
||||
{"error": "Project ID is required"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
|
||||
if service == "github":
|
||||
data = request.data.get("data", False)
|
||||
metadata = request.data.get("metadata", False)
|
||||
config = request.data.get("config", False)
|
||||
if not data or not metadata or not config:
|
||||
return Response(
|
||||
{"error": "Data, config and metadata are required"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
api_token = APIToken.objects.filter(
|
||||
user=request.user, workspace=workspace
|
||||
).first()
|
||||
if api_token is None:
|
||||
api_token = APIToken.objects.create(
|
||||
user=request.user,
|
||||
label="Importer",
|
||||
workspace=workspace,
|
||||
)
|
||||
|
||||
importer = Importer.objects.create(
|
||||
service=service,
|
||||
project_id=project_id,
|
||||
status="queued",
|
||||
initiated_by=request.user,
|
||||
data=data,
|
||||
metadata=metadata,
|
||||
token=api_token,
|
||||
config=config,
|
||||
created_by=request.user,
|
||||
updated_by=request.user,
|
||||
# Validate the data
|
||||
data = request.data.get("data", False)
|
||||
metadata = request.data.get("metadata", False)
|
||||
config = request.data.get("config", False)
|
||||
if not data or not metadata or not config:
|
||||
return Response(
|
||||
{"error": "Data, config and metadata are required"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
service_importer.delay(service, importer.id)
|
||||
serializer = ImporterSerializer(importer)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
# Update config
|
||||
if config and service == "github":
|
||||
config.update({"installation_id": installation_id})
|
||||
|
||||
if service == "jira":
|
||||
data = request.data.get("data", False)
|
||||
metadata = request.data.get("metadata", False)
|
||||
config = request.data.get("config", False)
|
||||
if not data or not metadata:
|
||||
return Response(
|
||||
{"error": "Data, config and metadata are required"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
api_token = APIToken.objects.filter(
|
||||
user=request.user, workspace=workspace
|
||||
).first()
|
||||
if api_token is None:
|
||||
api_token = APIToken.objects.create(
|
||||
user=request.user,
|
||||
label="Importer",
|
||||
workspace=workspace,
|
||||
)
|
||||
|
||||
importer = Importer.objects.create(
|
||||
service=service,
|
||||
project_id=project_id,
|
||||
status="queued",
|
||||
initiated_by=request.user,
|
||||
data=data,
|
||||
metadata=metadata,
|
||||
token=api_token,
|
||||
config=config,
|
||||
created_by=request.user,
|
||||
updated_by=request.user,
|
||||
# Get the api token -- # derecated
|
||||
api_token = APIToken.objects.filter(
|
||||
user=request.user, workspace=workspace
|
||||
).first()
|
||||
if api_token is None:
|
||||
api_token = APIToken.objects.create(
|
||||
user=request.user,
|
||||
label="Importer",
|
||||
workspace=workspace,
|
||||
)
|
||||
|
||||
service_importer.delay(service, importer.id)
|
||||
serializer = ImporterSerializer(importer)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
return Response(
|
||||
{"error": "Servivce not supported yet"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
# Create an import
|
||||
importer = Importer.objects.create(
|
||||
service=service,
|
||||
project_id=project_id,
|
||||
status="queued",
|
||||
initiated_by=request.user,
|
||||
data=data,
|
||||
metadata=metadata,
|
||||
token=api_token,
|
||||
config=config,
|
||||
created_by=request.user,
|
||||
updated_by=request.user,
|
||||
)
|
||||
|
||||
# Push it to segway
|
||||
if settings.SEGWAY_BASE_URL:
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"x-api-key": settings.SEGWAY_KEY,
|
||||
}
|
||||
data = {
|
||||
"metadata": metadata,
|
||||
"data": data,
|
||||
"config": config,
|
||||
"workspace_id": str(workspace.id),
|
||||
"project_id": str(project_id),
|
||||
"created_by": str(request.user.id),
|
||||
"importer_id": str(importer.id),
|
||||
}
|
||||
res = requests.post(
|
||||
f"{settings.SEGWAY_BASE_URL}/api/github/import",
|
||||
data=json.dumps(data),
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
if "error" in res.json():
|
||||
importer.status = "failed"
|
||||
importer.reason = str(res.json())
|
||||
importer.save()
|
||||
else:
|
||||
importer.status = "processing"
|
||||
importer.save(update_fields=["status"])
|
||||
else:
|
||||
importer.status = "failed"
|
||||
importer.reason = "Segway base url is not present"
|
||||
importer.save(update_fields=["status", "reason"])
|
||||
|
||||
# return the response
|
||||
serializer = ImporterSerializer(importer)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
def get(self, request, slug):
|
||||
imports = (
|
||||
Importer.objects.filter(workspace__slug=slug)
|
||||
@@ -221,9 +262,7 @@ class ImportServiceEndpoint(BaseAPIView):
|
||||
return Response(serializer.data)
|
||||
|
||||
def delete(self, request, slug, service, pk):
|
||||
importer = Importer.objects.get(
|
||||
pk=pk, service=service, workspace__slug=slug
|
||||
)
|
||||
importer = Importer.objects.get(pk=pk, service=service, workspace__slug=slug)
|
||||
|
||||
if importer.imported_data is not None:
|
||||
# Delete all imported Issues
|
||||
@@ -241,9 +280,7 @@ class ImportServiceEndpoint(BaseAPIView):
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
def patch(self, request, slug, service, pk):
|
||||
importer = Importer.objects.get(
|
||||
pk=pk, service=service, workspace__slug=slug
|
||||
)
|
||||
importer = Importer.objects.get(pk=pk, service=service, workspace__slug=slug)
|
||||
serializer = ImporterSerializer(importer, data=request.data, partial=True)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
@@ -479,9 +516,7 @@ class BulkImportModulesEndpoint(BaseAPIView):
|
||||
[
|
||||
ModuleLink(
|
||||
module=module,
|
||||
url=module_data.get("link", {}).get(
|
||||
"url", "https://plane.so"
|
||||
),
|
||||
url=module_data.get("link", {}).get("url", "https://plane.so"),
|
||||
title=module_data.get("link", {}).get(
|
||||
"title", "Original Issue"
|
||||
),
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
# Python improts
|
||||
import uuid
|
||||
import requests
|
||||
import json
|
||||
|
||||
# Django imports
|
||||
from django.contrib.auth.hashers import make_password
|
||||
from django.conf import settings
|
||||
|
||||
# Third party imports
|
||||
from rest_framework.response import Response
|
||||
@@ -27,6 +30,7 @@ from plane.utils.integrations.github import (
|
||||
from plane.app.permissions import WorkSpaceAdminPermission
|
||||
from plane.utils.integrations.slack import slack_oauth
|
||||
|
||||
|
||||
class IntegrationViewSet(BaseViewSet):
|
||||
serializer_class = IntegrationSerializer
|
||||
model = Integration
|
||||
@@ -46,9 +50,7 @@ class IntegrationViewSet(BaseViewSet):
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
serializer = IntegrationSerializer(
|
||||
integration, data=request.data, partial=True
|
||||
)
|
||||
serializer = IntegrationSerializer(integration, data=request.data, partial=True)
|
||||
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
@@ -94,14 +96,30 @@ class WorkspaceIntegrationViewSet(BaseViewSet):
|
||||
{"error": "Installation ID is required"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
metadata = get_github_metadata(installation_id)
|
||||
# Push it to segway
|
||||
if settings.SEGWAY_BASE_URL:
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"x-api-key": settings.SEGWAY_KEY,
|
||||
}
|
||||
data = {"installationId": installation_id}
|
||||
res = requests.post(
|
||||
f"{settings.SEGWAY_BASE_URL}/api/github/metadata",
|
||||
data=json.dumps(data),
|
||||
headers=headers,
|
||||
)
|
||||
if "error" in res.json():
|
||||
return Response(res.json(), status=status.HTTP_400_BAD_REQUEST)
|
||||
metadata = res.json()
|
||||
config = {"installation_id": installation_id}
|
||||
|
||||
if provider == "slack":
|
||||
code = request.data.get("code", False)
|
||||
|
||||
if not code:
|
||||
return Response({"error": "Code is required"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return Response(
|
||||
{"error": "Code is required"}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
slack_response = slack_oauth(code=code)
|
||||
|
||||
@@ -123,9 +141,7 @@ class WorkspaceIntegrationViewSet(BaseViewSet):
|
||||
is_password_autoset=True,
|
||||
is_bot=True,
|
||||
first_name=integration.title,
|
||||
avatar=integration.avatar_url
|
||||
if integration.avatar_url is not None
|
||||
else "",
|
||||
avatar=integration.avatar_url if integration.avatar_url is not None else "",
|
||||
)
|
||||
|
||||
# Create an API Token for the bot user
|
||||
@@ -161,9 +177,7 @@ class WorkspaceIntegrationViewSet(BaseViewSet):
|
||||
)
|
||||
|
||||
if workspace_integration.integration.provider == "github":
|
||||
installation_id = workspace_integration.config.get(
|
||||
"installation_id", False
|
||||
)
|
||||
installation_id = workspace_integration.config.get("installation_id", False)
|
||||
if installation_id:
|
||||
delete_github_installation(installation_id=installation_id)
|
||||
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
# Python imports
|
||||
import json
|
||||
import requests
|
||||
|
||||
# Third party imports
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
from sentry_sdk import capture_exception
|
||||
|
||||
# Django imports
|
||||
from django.conf import settings
|
||||
|
||||
# Module imports
|
||||
from plane.app.views import BaseViewSet, BaseAPIView
|
||||
from plane.db.models import (
|
||||
@@ -35,19 +42,32 @@ class GithubRepositoriesEndpoint(BaseAPIView):
|
||||
workspace__slug=slug, pk=workspace_integration_id
|
||||
)
|
||||
|
||||
if workspace_integration.integration.provider != "github":
|
||||
installation_id = workspace_integration.config.get("installation_id")
|
||||
|
||||
if not installation_id:
|
||||
return Response(
|
||||
{"error": "Not a github integration"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
access_tokens_url = workspace_integration.metadata["access_tokens_url"]
|
||||
repositories_url = (
|
||||
workspace_integration.metadata["repositories_url"]
|
||||
+ f"?per_page=100&page={page}"
|
||||
)
|
||||
repositories = get_github_repos(access_tokens_url, repositories_url)
|
||||
return Response(repositories, status=status.HTTP_200_OK)
|
||||
# Push it to segway
|
||||
if settings.SEGWAY_BASE_URL:
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"x-api-key": settings.SEGWAY_KEY,
|
||||
}
|
||||
data = {
|
||||
"installationId": installation_id,
|
||||
"page": page,
|
||||
}
|
||||
res = requests.post(
|
||||
f"{settings.SEGWAY_BASE_URL}/api/github/repos",
|
||||
data=json.dumps(data),
|
||||
headers=headers,
|
||||
)
|
||||
if "error" in res.json():
|
||||
return Response(res.json(), status=status.HTTP_400_BAD_REQUEST)
|
||||
else:
|
||||
return Response(res.json(), status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class GithubRepositorySyncViewSet(BaseViewSet):
|
||||
|
||||
@@ -145,6 +145,16 @@ 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()
|
||||
)
|
||||
|
||||
@@ -160,16 +170,6 @@ 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):
|
||||
@@ -679,6 +679,25 @@ 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,
|
||||
@@ -991,11 +1010,18 @@ class ProjectPublicCoverImagesEndpoint(BaseAPIView):
|
||||
|
||||
def get(self, request):
|
||||
files = []
|
||||
s3 = boto3.client(
|
||||
"s3",
|
||||
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
|
||||
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
|
||||
)
|
||||
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)
|
||||
|
||||
params = {
|
||||
"Bucket": settings.AWS_STORAGE_BUCKET_NAME,
|
||||
"Prefix": "static/project-cover/",
|
||||
@@ -1008,9 +1034,19 @@ class ProjectPublicCoverImagesEndpoint(BaseAPIView):
|
||||
if not content["Key"].endswith(
|
||||
"/"
|
||||
): # This line ensures we're only getting files, not "sub-folders"
|
||||
files.append(
|
||||
f"https://{settings.AWS_STORAGE_BUCKET_NAME}.s3.{settings.AWS_REGION}.amazonaws.com/{content['Key']}"
|
||||
)
|
||||
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']}"
|
||||
)
|
||||
|
||||
return Response(files, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
@@ -70,6 +70,7 @@ 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
|
||||
@@ -495,6 +496,18 @@ 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",
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
from .issue_sync_task import issue_sync
|
||||
@@ -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=bool(EMAIL_USE_TLS),
|
||||
use_tls=EMAIL_USE_TLS == "1",
|
||||
)
|
||||
|
||||
msg = EmailMultiAlternatives(
|
||||
@@ -373,7 +373,7 @@ def generate_non_segmented_rows(
|
||||
return [tuple(row_zero)] + rows
|
||||
|
||||
|
||||
@shared_task
|
||||
@shared_task(queue='internal_tasks')
|
||||
def analytic_export_task(email, data, slug):
|
||||
try:
|
||||
filters = issue_filters(data, "POST")
|
||||
|
||||
@@ -29,7 +29,7 @@ def posthogConfiguration():
|
||||
return None, None
|
||||
|
||||
|
||||
@shared_task
|
||||
@shared_task(queue='internal_tasks')
|
||||
def auth_events(user, email, user_agent, ip, event_name, medium, first_time):
|
||||
try:
|
||||
POSTHOG_API_KEY, POSTHOG_HOST = posthogConfiguration()
|
||||
@@ -54,7 +54,7 @@ def auth_events(user, email, user_agent, ip, event_name, medium, first_time):
|
||||
capture_exception(e)
|
||||
|
||||
|
||||
@shared_task
|
||||
@shared_task(queue='internal_tasks')
|
||||
def workspace_invite_event(user, email, user_agent, ip, event_name, accepted_from):
|
||||
try:
|
||||
POSTHOG_API_KEY, POSTHOG_HOST = posthogConfiguration()
|
||||
|
||||
@@ -259,7 +259,7 @@ def generate_xlsx(header, project_id, issues, files):
|
||||
files.append((f"{project_id}.xlsx", xlsx_file))
|
||||
|
||||
|
||||
@shared_task
|
||||
@shared_task(queue='internal_tasks')
|
||||
def issue_export_task(provider, workspace_id, project_ids, token_id, multiple, slug):
|
||||
try:
|
||||
exporter_instance = ExporterHistory.objects.get(token=token_id)
|
||||
|
||||
@@ -15,7 +15,7 @@ from botocore.client import Config
|
||||
from plane.db.models import ExporterHistory
|
||||
|
||||
|
||||
@shared_task
|
||||
@shared_task(queue='internal_tasks')
|
||||
def delete_old_s3_link():
|
||||
# Get a list of keys and IDs to process
|
||||
expired_exporter_history = ExporterHistory.objects.filter(
|
||||
|
||||
@@ -12,7 +12,7 @@ from celery import shared_task
|
||||
from plane.db.models import FileAsset
|
||||
|
||||
|
||||
@shared_task
|
||||
@shared_task(queue='internal_tasks')
|
||||
def delete_file_asset():
|
||||
|
||||
# file assets to delete
|
||||
|
||||
@@ -17,7 +17,7 @@ from sentry_sdk import capture_exception
|
||||
from plane.license.utils.instance_value import get_email_configuration
|
||||
|
||||
|
||||
@shared_task
|
||||
@shared_task(queue='internal_tasks')
|
||||
def forgot_password(first_name, email, uidb64, token, current_site):
|
||||
try:
|
||||
relative_link = (
|
||||
@@ -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=bool(EMAIL_USE_TLS),
|
||||
use_tls=EMAIL_USE_TLS == "1",
|
||||
)
|
||||
|
||||
msg = EmailMultiAlternatives(
|
||||
|
||||
@@ -1,200 +1,467 @@
|
||||
# Python imports
|
||||
import json
|
||||
import requests
|
||||
import uuid
|
||||
|
||||
# Django imports
|
||||
from django.conf import settings
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.db.models import Q, Max
|
||||
from django.contrib.auth.hashers import make_password
|
||||
|
||||
# Third Party imports
|
||||
from celery import shared_task
|
||||
from sentry_sdk import capture_exception
|
||||
from celery.exceptions import MaxRetriesExceededError
|
||||
|
||||
# Module imports
|
||||
from plane.app.serializers import ImporterSerializer
|
||||
from plane.db.models import (
|
||||
Importer,
|
||||
WorkspaceMember,
|
||||
GithubRepositorySync,
|
||||
GithubRepository,
|
||||
ProjectMember,
|
||||
WorkspaceIntegration,
|
||||
Label,
|
||||
User,
|
||||
IssueProperty,
|
||||
IssueAssignee,
|
||||
IssueLabel,
|
||||
IssueSequence,
|
||||
IssueActivity,
|
||||
IssueComment,
|
||||
IssueLink,
|
||||
ModuleIssue,
|
||||
State,
|
||||
Module,
|
||||
Issue,
|
||||
Cycle,
|
||||
)
|
||||
from plane.bgtasks.user_welcome_task import send_welcome_slack
|
||||
|
||||
|
||||
@shared_task
|
||||
@shared_task(queue="internal_tasks")
|
||||
def service_importer(service, importer_id):
|
||||
pass
|
||||
|
||||
|
||||
## Utility functions
|
||||
def get_label_id(name, data):
|
||||
try:
|
||||
importer = Importer.objects.get(pk=importer_id)
|
||||
importer.status = "processing"
|
||||
importer.save()
|
||||
existing_label = (
|
||||
Label.objects.filter(
|
||||
project_id=data.get("project_id"),
|
||||
workspace_id=data.get("workspace_id"),
|
||||
name__iexact=name,
|
||||
)
|
||||
.values("id")
|
||||
.first()
|
||||
)
|
||||
return existing_label
|
||||
except Label.DoesNotExist:
|
||||
return None
|
||||
|
||||
users = importer.data.get("users", [])
|
||||
|
||||
# Check if we need to import users as well
|
||||
if len(users):
|
||||
# For all invited users create the users
|
||||
new_users = User.objects.bulk_create(
|
||||
[
|
||||
User(
|
||||
email=user.get("email").strip().lower(),
|
||||
username=uuid.uuid4().hex,
|
||||
password=make_password(uuid.uuid4().hex),
|
||||
is_password_autoset=True,
|
||||
)
|
||||
for user in users
|
||||
if user.get("import", False) == "invite"
|
||||
],
|
||||
batch_size=10,
|
||||
ignore_conflicts=True,
|
||||
def get_state_id(name, data):
|
||||
try:
|
||||
existing_state = (
|
||||
State.objects.filter(
|
||||
name__iexact=name,
|
||||
project_id=data.get("project_id"),
|
||||
workspace_id=data.get("workspace_id"),
|
||||
)
|
||||
.values("id")
|
||||
.first()
|
||||
)
|
||||
return existing_state
|
||||
except State.DoesNotExist:
|
||||
return None
|
||||
|
||||
|
||||
def get_user_id(name):
|
||||
try:
|
||||
existing_user = User.objects.filter(email=name).values("id").first()
|
||||
return existing_user
|
||||
except User.DoesNotExist:
|
||||
return None
|
||||
|
||||
|
||||
def update_imported_items(importer_id, entity, entity_id):
|
||||
importer = Importer.objects.get(pk=importer_id)
|
||||
if importer.imported_data:
|
||||
importer.imported_data.setdefault(str(entity), []).append(str(entity_id))
|
||||
else:
|
||||
importer.imported_data = {
|
||||
str(entity): [str(entity_id)]
|
||||
}
|
||||
importer.save()
|
||||
|
||||
|
||||
## Sync functions
|
||||
def members_sync(data):
|
||||
try:
|
||||
user = User.objects.get(email=data.get("email"))
|
||||
_ = WorkspaceMember.objects.get_or_create(
|
||||
member_id=user.id, workspace_id=data.get("workspace_id")
|
||||
)
|
||||
_ = ProjectMember.objects.get_or_create(
|
||||
member_id=user.id,
|
||||
project_id=data.get("project_id"),
|
||||
workspace_id=data.get("workspace_id"),
|
||||
)
|
||||
_ = IssueProperty.objects.get_or_create(
|
||||
project_id=data.get("project_id"),
|
||||
workspace_id=data.get("workspace_id"),
|
||||
user_id=user.id,
|
||||
created_by_id=data.get("created_by"),
|
||||
)
|
||||
|
||||
except User.DoesNotExist:
|
||||
# For all invited users create the users
|
||||
new_user = User.objects.create(
|
||||
email=data.get("email").strip().lower(),
|
||||
username=uuid.uuid4().hex,
|
||||
password=make_password(uuid.uuid4().hex),
|
||||
is_password_autoset=True,
|
||||
)
|
||||
|
||||
service = data.get("external_source")
|
||||
|
||||
WorkspaceMember.objects.create(
|
||||
member_id=new_user.id,
|
||||
workspace_id=data.get("workspace_id"),
|
||||
created_by_id=data.get("created_by"),
|
||||
)
|
||||
|
||||
ProjectMember.objects.create(
|
||||
project_id=data.get("project_id"),
|
||||
workspace_id=data.get("workspace_id"),
|
||||
member_id=new_user.id,
|
||||
created_by_id=data.get("created_by"),
|
||||
)
|
||||
|
||||
IssueProperty.objects.create(
|
||||
project_id=data.get("project_id"),
|
||||
workspace_id=data.get("workspace_id"),
|
||||
user_id=new_user.id,
|
||||
created_by_id=data.get("created_by"),
|
||||
)
|
||||
if data.get("source", False) == "slack":
|
||||
send_welcome_slack.delay(
|
||||
str(new_user.id),
|
||||
True,
|
||||
f"{new_user.email} was imported to Plane from {service}",
|
||||
)
|
||||
|
||||
_ = [
|
||||
send_welcome_slack.delay(
|
||||
str(user.id),
|
||||
True,
|
||||
f"{user.email} was imported to Plane from {service}",
|
||||
)
|
||||
for user in new_users
|
||||
]
|
||||
|
||||
workspace_users = User.objects.filter(
|
||||
email__in=[
|
||||
user.get("email").strip().lower()
|
||||
for user in users
|
||||
if user.get("import", False) == "invite"
|
||||
or user.get("import", False) == "map"
|
||||
]
|
||||
def label_sync(data):
|
||||
existing_label = Label.objects.filter(
|
||||
project_id=data.get("project_id"),
|
||||
workspace_id=data.get("workspace_id"),
|
||||
name__iexact=data.get("name"),
|
||||
external_id=data.get("external_id", None),
|
||||
external_source=data.get("external_source"),
|
||||
)
|
||||
|
||||
if not existing_label.exists() and data.get("name"):
|
||||
label = Label.objects.create(
|
||||
project_id=data.get("project_id"),
|
||||
workspace_id=data.get("workspace_id"),
|
||||
name=data.get("name"),
|
||||
color=data.get("color"),
|
||||
created_by_id=data.get("created_by"),
|
||||
external_id=data.get("external_id", None),
|
||||
external_source=data.get("external_source"),
|
||||
)
|
||||
update_imported_items(data.get("importer_id"), "labels", label.id)
|
||||
|
||||
|
||||
def state_sync(data):
|
||||
try:
|
||||
state = State.objects.get(
|
||||
external_id=data.get("external_id"),
|
||||
project_id=data.get("project_id"),
|
||||
workspace_id=data.get("workspace_id"),
|
||||
)
|
||||
|
||||
except State.DoesNotExist:
|
||||
existing_states = State.objects.filter(
|
||||
project_id=data.get("project_id"),
|
||||
workspace_id=data.get("workspace_id"),
|
||||
group=data.get("state_group"),
|
||||
name__iexact=data.get("state_name"),
|
||||
)
|
||||
|
||||
if existing_states.exists():
|
||||
existing_state = existing_states.first()
|
||||
existing_state.external_id = data.get("external_id")
|
||||
existing_state.external_source = data.get("external_source")
|
||||
existing_state.save()
|
||||
else:
|
||||
state = State.objects.create(
|
||||
project_id=data.get("project_id"),
|
||||
workspace_id=data.get("workspace_id"),
|
||||
name=data.get("state_name"),
|
||||
group=data.get("state_group"),
|
||||
created_by_id=data.get("created_by"),
|
||||
external_id=data.get("external_id"),
|
||||
external_source=data.get("external_source"),
|
||||
)
|
||||
update_imported_items(data.get("importer_id"), "states", state.id)
|
||||
|
||||
# Check if any of the users are already member of workspace
|
||||
_ = WorkspaceMember.objects.filter(
|
||||
member__in=[user for user in workspace_users],
|
||||
workspace_id=importer.workspace_id,
|
||||
).update(is_active=True)
|
||||
|
||||
# Add new users to Workspace and project automatically
|
||||
WorkspaceMember.objects.bulk_create(
|
||||
[
|
||||
WorkspaceMember(
|
||||
member=user,
|
||||
workspace_id=importer.workspace_id,
|
||||
created_by=importer.created_by,
|
||||
)
|
||||
for user in workspace_users
|
||||
],
|
||||
batch_size=100,
|
||||
ignore_conflicts=True,
|
||||
)
|
||||
def issue_sync(data):
|
||||
try:
|
||||
issue = Issue.objects.get(
|
||||
external_id=data.get("external_id"),
|
||||
external_source=data.get("external_source"),
|
||||
project_id=data.get("project_id"),
|
||||
workspace_id=data.get("workspace_id"),
|
||||
)
|
||||
except Issue.DoesNotExist:
|
||||
# Get the default state
|
||||
default_state = State.objects.filter(
|
||||
~Q(name="Triage"), project_id=data.get("project_id"), default=True
|
||||
).first()
|
||||
|
||||
ProjectMember.objects.bulk_create(
|
||||
[
|
||||
ProjectMember(
|
||||
project_id=importer.project_id,
|
||||
workspace_id=importer.workspace_id,
|
||||
member=user,
|
||||
created_by=importer.created_by,
|
||||
)
|
||||
for user in workspace_users
|
||||
],
|
||||
batch_size=100,
|
||||
ignore_conflicts=True,
|
||||
)
|
||||
|
||||
IssueProperty.objects.bulk_create(
|
||||
[
|
||||
IssueProperty(
|
||||
project_id=importer.project_id,
|
||||
workspace_id=importer.workspace_id,
|
||||
user=user,
|
||||
created_by=importer.created_by,
|
||||
)
|
||||
for user in workspace_users
|
||||
],
|
||||
batch_size=100,
|
||||
ignore_conflicts=True,
|
||||
)
|
||||
|
||||
# Check if sync config is on for github importers
|
||||
if service == "github" and importer.config.get("sync", False):
|
||||
name = importer.metadata.get("name", False)
|
||||
url = importer.metadata.get("url", False)
|
||||
config = importer.metadata.get("config", {})
|
||||
owner = importer.metadata.get("owner", False)
|
||||
repository_id = importer.metadata.get("repository_id", False)
|
||||
|
||||
workspace_integration = WorkspaceIntegration.objects.get(
|
||||
workspace_id=importer.workspace_id, integration__provider="github"
|
||||
)
|
||||
|
||||
# Delete the old repository object
|
||||
GithubRepositorySync.objects.filter(project_id=importer.project_id).delete()
|
||||
GithubRepository.objects.filter(project_id=importer.project_id).delete()
|
||||
|
||||
# Create a Label for github
|
||||
label = Label.objects.filter(
|
||||
name="GitHub", project_id=importer.project_id
|
||||
# if there is no default state assign any random state
|
||||
if default_state is None:
|
||||
default_state = State.objects.filter(
|
||||
~Q(name="Triage"), project_id=data.get("project_id")
|
||||
).first()
|
||||
|
||||
if label is None:
|
||||
label = Label.objects.create(
|
||||
name="GitHub",
|
||||
project_id=importer.project_id,
|
||||
description="Label to sync Plane issues with GitHub issues",
|
||||
color="#003773",
|
||||
)
|
||||
# Create repository
|
||||
repo = GithubRepository.objects.create(
|
||||
name=name,
|
||||
url=url,
|
||||
config=config,
|
||||
repository_id=repository_id,
|
||||
owner=owner,
|
||||
project_id=importer.project_id,
|
||||
)
|
||||
# Get the maximum sequence_id
|
||||
last_id = IssueSequence.objects.filter(
|
||||
project_id=data.get("project_id")
|
||||
).aggregate(largest=Max("sequence"))["largest"]
|
||||
|
||||
# Create repo sync
|
||||
_ = GithubRepositorySync.objects.create(
|
||||
repository=repo,
|
||||
workspace_integration=workspace_integration,
|
||||
actor=workspace_integration.actor,
|
||||
credentials=importer.data.get("credentials", {}),
|
||||
project_id=importer.project_id,
|
||||
label=label,
|
||||
)
|
||||
last_id = 1 if last_id is None else last_id + 1
|
||||
|
||||
# Add bot as a member in the project
|
||||
_ = ProjectMember.objects.get_or_create(
|
||||
member=workspace_integration.actor,
|
||||
role=20,
|
||||
project_id=importer.project_id,
|
||||
)
|
||||
# Get the maximum sort order
|
||||
largest_sort_order = Issue.objects.filter(
|
||||
project_id=data.get("project_id"), state=default_state
|
||||
).aggregate(largest=Max("sort_order"))["largest"]
|
||||
|
||||
if settings.PROXY_BASE_URL:
|
||||
headers = {"Content-Type": "application/json"}
|
||||
import_data_json = json.dumps(
|
||||
ImporterSerializer(importer).data,
|
||||
cls=DjangoJSONEncoder,
|
||||
)
|
||||
_ = requests.post(
|
||||
f"{settings.PROXY_BASE_URL}/hooks/workspaces/{str(importer.workspace_id)}/projects/{str(importer.project_id)}/importers/{str(service)}/",
|
||||
json=import_data_json,
|
||||
headers=headers,
|
||||
)
|
||||
largest_sort_order = (
|
||||
65535 if largest_sort_order is None else largest_sort_order + 10000
|
||||
)
|
||||
parent_id = None
|
||||
if data.get("parent_id", False):
|
||||
parent_id = Issue.objects.filter(
|
||||
external_id=data.get("parent_id"),
|
||||
external_source=data.get("external_source"),
|
||||
project_id=data.get("project_id"),
|
||||
workspace_id=data.get("workspace_id"),
|
||||
).values("id")
|
||||
|
||||
# Issues
|
||||
issue = Issue.objects.create(
|
||||
project_id=data.get("project_id"),
|
||||
workspace_id=data.get("workspace_id"),
|
||||
state_id=get_state_id(data.get("state"), data).get("id")
|
||||
if get_state_id(data.get("state"), data)
|
||||
else default_state.id,
|
||||
name=data.get("name", "Issue Created through Importer")[:255],
|
||||
description_html=data.get("description_html", "<p></p>"),
|
||||
sequence_id=last_id,
|
||||
sort_order=largest_sort_order,
|
||||
start_date=data.get("start_date", None),
|
||||
target_date=data.get("target_date", None),
|
||||
priority=data.get("priority", "none"),
|
||||
created_by_id=data.get("created_by_id"),
|
||||
external_id=data.get("external_id"),
|
||||
external_source=data.get("external_source"),
|
||||
parent_id=parent_id,
|
||||
)
|
||||
|
||||
# Sequences
|
||||
_ = IssueSequence.objects.create(
|
||||
issue=issue,
|
||||
sequence=issue.sequence_id,
|
||||
project_id=data.get("project_id"),
|
||||
workspace_id=data.get("workspace_id"),
|
||||
)
|
||||
|
||||
# Attach Links
|
||||
_ = IssueLink.objects.create(
|
||||
issue=issue,
|
||||
url=data.get("link", {}).get("url", "https://github.com"),
|
||||
title=data.get("link", {}).get("title", "Original Issue"),
|
||||
project_id=data.get("project_id"),
|
||||
workspace_id=data.get("workspace_id"),
|
||||
created_by_id=data.get("created_by_id"),
|
||||
)
|
||||
|
||||
# Track the issue activities
|
||||
_ = IssueActivity.objects.create(
|
||||
issue=issue,
|
||||
actor_id=data.get("created_by_id"),
|
||||
project_id=data.get("project_id"),
|
||||
workspace_id=data.get("workspace_id"),
|
||||
comment=f"imported the issue from {data.get('external_source')}",
|
||||
verb="created",
|
||||
created_by_id=data.get("created_by_id"),
|
||||
)
|
||||
|
||||
update_imported_items(data.get("importer_id"), "issues", issue.id)
|
||||
|
||||
|
||||
def issue_label_sync(data):
|
||||
issue = Issue.objects.get(
|
||||
external_source=data.get("external_issue_source"),
|
||||
external_id=data.get("external_issue_id"),
|
||||
project_id=data.get("project_id"),
|
||||
workspace_id=data.get("workspace_id"),
|
||||
)
|
||||
if get_label_id(data.get("name"), data):
|
||||
IssueLabel.objects.create(
|
||||
issue=issue,
|
||||
label_id=get_label_id(data.get("name"), data).get("id"),
|
||||
project_id=data.get("project_id"),
|
||||
workspace_id=data.get("workspace_id"),
|
||||
created_by_id=data.get("created_by_id"),
|
||||
)
|
||||
|
||||
|
||||
def issue_assignee_sync(data):
|
||||
issue = Issue.objects.get(
|
||||
external_source=data.get("external_issue_source"),
|
||||
external_id=data.get("external_issue_id"),
|
||||
project_id=data.get("project_id"),
|
||||
workspace_id=data.get("workspace_id"),
|
||||
)
|
||||
user = User.objects.filter(email=data.get("email")).values("id")
|
||||
|
||||
IssueAssignee.objects.create(
|
||||
issue=issue,
|
||||
assignee_id=user,
|
||||
project_id=data.get("project_id"),
|
||||
workspace_id=data.get("workspace_id"),
|
||||
created_by_id=data.get("created_by_id"),
|
||||
)
|
||||
|
||||
|
||||
def issue_comment_sync(data):
|
||||
# Create Comments
|
||||
issue = Issue.objects.get(
|
||||
external_source=data.get("external_issue_source"),
|
||||
external_id=data.get("external_issue_id"),
|
||||
project_id=data.get("project_id"),
|
||||
workspace_id=data.get("workspace_id"),
|
||||
)
|
||||
IssueComment.objects.create(
|
||||
issue=issue,
|
||||
comment_html=data.get("comment_html", "<p></p>"),
|
||||
actor_id=data.get("created_by_id"),
|
||||
project_id=data.get("project_id"),
|
||||
workspace_id=data.get("workspace_id"),
|
||||
created_by_id=get_user_id(data.get("created_by_id")).get("id")
|
||||
if get_user_id(data.get("created_by_id"))
|
||||
else data.get("created_by_id"),
|
||||
external_id=data.get("external_id"),
|
||||
external_source=data.get("external_source"),
|
||||
)
|
||||
|
||||
|
||||
def cycles_sync(data):
|
||||
try:
|
||||
_ = Cycle.objects.get(
|
||||
external_id=data.get("external_id"),
|
||||
external_source=data.get("external_source"),
|
||||
project_id=data.get("project_id"),
|
||||
workspace_id=data.get("workspace_id"),
|
||||
)
|
||||
except Cycle.DoesNotExist:
|
||||
cycle = Cycle.objects.create(
|
||||
name=data.get("name"),
|
||||
description_html=data.get("description_html", "<p></p>"),
|
||||
project_id=data.get("project_id"),
|
||||
workspace_id=data.get("workspace_id"),
|
||||
created_by_id=data.get("created_by"),
|
||||
external_id=data.get("external_id"),
|
||||
external_source=data.get("external_source"),
|
||||
)
|
||||
update_imported_items(data.get("importer_id"), "cycles", cycle.id)
|
||||
|
||||
|
||||
def module_sync(data):
|
||||
try:
|
||||
_ = Module.objects.get(
|
||||
external_id=data.get("external_id"),
|
||||
external_source=data.get("external_source"),
|
||||
project_id=data.get("project_id"),
|
||||
workspace_id=data.get("workspace_id"),
|
||||
)
|
||||
except Module.DoesNotExist:
|
||||
module = Module.objects.create(
|
||||
name=data.get("name"),
|
||||
description_html=data.get("description_html", "<p></p>"),
|
||||
project_id=data.get("project_id"),
|
||||
workspace_id=data.get("workspace_id"),
|
||||
created_by_id=data.get("created_by"),
|
||||
external_id=data.get("external_id"),
|
||||
external_source=data.get("external_source"),
|
||||
)
|
||||
update_imported_items(data.get("importer_id"), "modules", module.id)
|
||||
|
||||
|
||||
def modules_issue_sync(data):
|
||||
module = Module.objects.get(
|
||||
external_id=data.get("module_id"),
|
||||
project_id=data.get("project_id"),
|
||||
workspace_id=data.get("workspace_id"),
|
||||
external_source=data.get("external_source"),
|
||||
)
|
||||
issue = Issue.objects.get(
|
||||
external_id=data.get("issue_id"),
|
||||
external_source=data.get("external_source"),
|
||||
project_id=data.get("project_id"),
|
||||
workspace_id=data.get("workspace_id"),
|
||||
)
|
||||
|
||||
_ = ModuleIssue.objects.create(
|
||||
module=module,
|
||||
issue=issue,
|
||||
project_id=data.get("project_id"),
|
||||
workspace_id=data.get("workspace_id"),
|
||||
created_by_id=data.get("created_by"),
|
||||
)
|
||||
|
||||
|
||||
def import_sync(data):
|
||||
importer = Importer.objects.get(pk=data.get("importer_id"))
|
||||
importer.status = data.get("status")
|
||||
importer.save(update_fields=["status"])
|
||||
|
||||
|
||||
@shared_task(bind=True, queue="segway_task", max_retries=5)
|
||||
def import_task(self, data):
|
||||
type = data.get("type")
|
||||
|
||||
if type is None:
|
||||
return
|
||||
|
||||
TYPE_MAPPER = {
|
||||
"member.sync": members_sync,
|
||||
"label.sync": label_sync,
|
||||
"state.sync": state_sync,
|
||||
"issue.sync": issue_sync,
|
||||
"issue.label.sync": issue_label_sync,
|
||||
"issue.assignee.sync": issue_assignee_sync,
|
||||
"issue.comment.sync": issue_comment_sync,
|
||||
"cycle.sync": cycles_sync,
|
||||
"module.sync": module_sync,
|
||||
"module.issue.sync": modules_issue_sync,
|
||||
"import.sync": import_sync,
|
||||
}
|
||||
try:
|
||||
func = TYPE_MAPPER.get(type)
|
||||
if func is None:
|
||||
return
|
||||
# Call the function
|
||||
func(data)
|
||||
return
|
||||
except Exception as e:
|
||||
importer = Importer.objects.get(pk=importer_id)
|
||||
importer.status = "failed"
|
||||
importer.save()
|
||||
# Print logs if in DEBUG mode
|
||||
if settings.DEBUG:
|
||||
print(e)
|
||||
capture_exception(e)
|
||||
try:
|
||||
# Retry with exponential backoff
|
||||
self.retry(exc=e, countdown=50, backoff=2)
|
||||
except MaxRetriesExceededError:
|
||||
# For max retries reached items fail the import
|
||||
importer = Importer.objects.get(pk=data.get("importer_id"))
|
||||
importer.status = "failed"
|
||||
importer.reason = e
|
||||
importer.save()
|
||||
|
||||
return
|
||||
|
||||
@@ -1460,7 +1460,7 @@ def delete_draft_issue_activity(
|
||||
|
||||
|
||||
# Receive message from room group
|
||||
@shared_task
|
||||
@shared_task(queue='internal_tasks')
|
||||
def issue_activity(
|
||||
type,
|
||||
requested_data,
|
||||
|
||||
@@ -16,7 +16,7 @@ from plane.db.models import Issue, Project, State
|
||||
from plane.bgtasks.issue_activites_task import issue_activity
|
||||
|
||||
|
||||
@shared_task
|
||||
@shared_task(queue='internal_tasks')
|
||||
def archive_and_close_old_issues():
|
||||
archive_old_issues()
|
||||
close_old_issues()
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
from celery import shared_task
|
||||
|
||||
@shared_task(queue="segway_tasks")
|
||||
def issue_sync(data):
|
||||
print(f"Received data from Segway: {data}")
|
||||
@@ -17,7 +17,7 @@ from sentry_sdk import capture_exception
|
||||
from plane.license.utils.instance_value import get_email_configuration
|
||||
|
||||
|
||||
@shared_task
|
||||
@shared_task(queue='internal_tasks')
|
||||
def magic_link(email, key, token, current_site):
|
||||
try:
|
||||
(
|
||||
@@ -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=bool(EMAIL_USE_TLS),
|
||||
use_tls=EMAIL_USE_TLS == "1",
|
||||
)
|
||||
|
||||
msg = EmailMultiAlternatives(
|
||||
|
||||
@@ -183,7 +183,7 @@ def createMentionNotification(project, notification_comment, issue, actor_id, me
|
||||
)
|
||||
|
||||
|
||||
@shared_task
|
||||
@shared_task(queue='internal_tasks')
|
||||
def notifications(type, issue_id, project_id, actor_id, subscriber, issue_activities_created, requested_data, current_instance):
|
||||
issue_activities_created = (
|
||||
json.loads(
|
||||
|
||||
@@ -15,7 +15,7 @@ from sentry_sdk import capture_exception
|
||||
from plane.db.models import Project, User, ProjectMemberInvite
|
||||
from plane.license.utils.instance_value import get_email_configuration
|
||||
|
||||
@shared_task
|
||||
@shared_task(queue='internal_tasks')
|
||||
def project_invitation(email, project_id, token, current_site, invitor):
|
||||
try:
|
||||
user = User.objects.get(email=invitor)
|
||||
@@ -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=bool(EMAIL_USE_TLS),
|
||||
use_tls=EMAIL_USE_TLS == "1",
|
||||
)
|
||||
|
||||
msg = EmailMultiAlternatives(
|
||||
|
||||
@@ -11,7 +11,7 @@ from slack_sdk.errors import SlackApiError
|
||||
from plane.db.models import User
|
||||
|
||||
|
||||
@shared_task
|
||||
@shared_task(queue='internal_tasks')
|
||||
def send_welcome_slack(user_id, created, message):
|
||||
try:
|
||||
instance = User.objects.get(pk=user_id)
|
||||
|
||||
@@ -71,6 +71,7 @@ def get_model_data(event, event_id, many=False):
|
||||
retry_backoff=600,
|
||||
max_retries=5,
|
||||
retry_jitter=True,
|
||||
queue='internal_tasks'
|
||||
)
|
||||
def webhook_task(self, webhook, slug, event, event_data, action):
|
||||
try:
|
||||
@@ -161,7 +162,7 @@ def webhook_task(self, webhook, slug, event, event_data, action):
|
||||
return
|
||||
|
||||
|
||||
@shared_task()
|
||||
@shared_task(queue='internal_tasks')
|
||||
def send_webhook(event, payload, kw, action, slug, bulk):
|
||||
try:
|
||||
webhooks = Webhook.objects.filter(workspace__slug=slug, is_active=True)
|
||||
|
||||
@@ -20,7 +20,7 @@ from plane.db.models import Workspace, WorkspaceMemberInvite, User
|
||||
from plane.license.utils.instance_value import get_email_configuration
|
||||
|
||||
|
||||
@shared_task
|
||||
@shared_task(queue='internal_tasks')
|
||||
def workspace_invitation(email, workspace_id, token, current_site, invitor):
|
||||
try:
|
||||
user = User.objects.get(email=invitor)
|
||||
@@ -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=bool(EMAIL_USE_TLS),
|
||||
use_tls=EMAIL_USE_TLS == "1",
|
||||
)
|
||||
|
||||
msg = EmailMultiAlternatives(
|
||||
|
||||
@@ -8,7 +8,7 @@ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "plane.settings.production")
|
||||
|
||||
ri = redis_instance()
|
||||
|
||||
app = Celery("plane")
|
||||
app = Celery('tasks', broker='pyamqp://guest:guest@localhost:5672//')
|
||||
|
||||
# Using a string here means the worker will not have to
|
||||
# pickle the object when using Windows.
|
||||
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
# Generated by Django 4.2.7 on 2023-12-20 14:33
|
||||
|
||||
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, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='importer',
|
||||
name='reason',
|
||||
field=models.TextField(blank=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, 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, 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, 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, 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, null=True),
|
||||
),
|
||||
]
|
||||
@@ -22,8 +22,6 @@ class BaseModel(AuditModel):
|
||||
user = get_current_user()
|
||||
|
||||
if user is None or user.is_anonymous:
|
||||
self.created_by = None
|
||||
self.updated_by = None
|
||||
super(BaseModel, self).save(*args, **kwargs)
|
||||
else:
|
||||
# Check if the model is being created or updated
|
||||
|
||||
@@ -18,6 +18,8 @@ class Cycle(ProjectBaseModel):
|
||||
)
|
||||
view_props = models.JSONField(default=dict)
|
||||
sort_order = models.FloatField(default=65535)
|
||||
external_source = models.CharField(null=True, blank=True)
|
||||
external_id = models.CharField(max_length=255, blank=True, null=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Cycle"
|
||||
@@ -27,9 +29,9 @@ class Cycle(ProjectBaseModel):
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self._state.adding:
|
||||
smallest_sort_order = Cycle.objects.filter(
|
||||
project=self.project
|
||||
).aggregate(smallest=models.Min("sort_order"))["smallest"]
|
||||
smallest_sort_order = Cycle.objects.filter(project=self.project).aggregate(
|
||||
smallest=models.Min("sort_order")
|
||||
)["smallest"]
|
||||
|
||||
if smallest_sort_order is not None:
|
||||
self.sort_order = smallest_sort_order - 10000
|
||||
|
||||
@@ -34,6 +34,7 @@ class Importer(ProjectBaseModel):
|
||||
"db.APIToken", on_delete=models.CASCADE, related_name="importer"
|
||||
)
|
||||
imported_data = models.JSONField(null=True)
|
||||
reason = models.TextField(blank=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Importer"
|
||||
|
||||
@@ -102,6 +102,8 @@ 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(null=True, blank=True)
|
||||
external_id = models.CharField(max_length=255, blank=True, null=True)
|
||||
|
||||
objects = models.Manager()
|
||||
issue_objects = IssueManager()
|
||||
@@ -132,7 +134,6 @@ class Issue(ProjectBaseModel):
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
if self._state.adding:
|
||||
# Get the maximum display_id value from the database
|
||||
last_id = IssueSequence.objects.filter(project=self.project).aggregate(
|
||||
@@ -210,8 +211,9 @@ class IssueRelation(ProjectBaseModel):
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.issue.name} {self.related_issue.name}"
|
||||
|
||||
return f"{self.issue.name} {self.related_issue.name}"
|
||||
|
||||
|
||||
class IssueMention(ProjectBaseModel):
|
||||
issue = models.ForeignKey(
|
||||
Issue, on_delete=models.CASCADE, related_name="issue_mention"
|
||||
@@ -221,6 +223,7 @@ class IssueMention(ProjectBaseModel):
|
||||
on_delete=models.CASCADE,
|
||||
related_name="issue_mention",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["issue", "mention"]
|
||||
verbose_name = "Issue Mention"
|
||||
@@ -229,7 +232,7 @@ class IssueMention(ProjectBaseModel):
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.issue.name} {self.mention.email}"
|
||||
return f"{self.issue.name} {self.mention.email}"
|
||||
|
||||
|
||||
class IssueAssignee(ProjectBaseModel):
|
||||
@@ -366,6 +369,8 @@ class IssueComment(ProjectBaseModel):
|
||||
default="INTERNAL",
|
||||
max_length=100,
|
||||
)
|
||||
external_source = models.CharField(null=True, blank=True)
|
||||
external_id = models.CharField(max_length=255, blank=True, null=True)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.comment_stripped = (
|
||||
@@ -416,6 +421,8 @@ 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(null=True, blank=True)
|
||||
external_id = models.CharField(max_length=255, blank=True, null=True)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["name", "project"]
|
||||
|
||||
@@ -41,6 +41,8 @@ class Module(ProjectBaseModel):
|
||||
)
|
||||
view_props = models.JSONField(default=dict)
|
||||
sort_order = models.FloatField(default=65535)
|
||||
external_source = models.CharField(null=True, blank=True)
|
||||
external_id = models.CharField(max_length=255, blank=True, null=True)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["name", "project"]
|
||||
|
||||
@@ -24,6 +24,8 @@ class State(ProjectBaseModel):
|
||||
max_length=20,
|
||||
)
|
||||
default = models.BooleanField(default=False)
|
||||
external_source = models.CharField(null=True, blank=True)
|
||||
external_id = models.CharField(max_length=255, blank=True, null=True)
|
||||
|
||||
def __str__(self):
|
||||
"""Return name of the state"""
|
||||
|
||||
@@ -5,6 +5,7 @@ import ssl
|
||||
import certifi
|
||||
from datetime import timedelta
|
||||
from urllib.parse import urlparse
|
||||
from kombu import Exchange, Queue
|
||||
|
||||
# Django imports
|
||||
from django.core.management.utils import get_random_secret_key
|
||||
@@ -148,6 +149,9 @@ else:
|
||||
REDIS_URL = os.environ.get("REDIS_URL")
|
||||
REDIS_SSL = REDIS_URL and "rediss" in REDIS_URL
|
||||
|
||||
# RabbitMq Config
|
||||
RABBITMQ_URL = os.environ.get("RABBITMQ_URL")
|
||||
|
||||
if REDIS_SSL:
|
||||
CACHES = {
|
||||
"default": {
|
||||
@@ -270,18 +274,28 @@ SIMPLE_JWT = {
|
||||
# Celery Configuration
|
||||
CELERY_TIMEZONE = TIME_ZONE
|
||||
CELERY_TASK_SERIALIZER = "json"
|
||||
CELERY_ACCEPT_CONTENT = ["application/json"]
|
||||
CELERY_ACCEPT_CONTENT = ["json"]
|
||||
|
||||
if REDIS_SSL:
|
||||
redis_url = os.environ.get("REDIS_URL")
|
||||
broker_url = (
|
||||
f"{redis_url}?ssl_cert_reqs={ssl.CERT_NONE.name}&ssl_ca_certs={certifi.where()}"
|
||||
)
|
||||
CELERY_BROKER_URL = broker_url
|
||||
CELERY_RESULT_BACKEND = broker_url
|
||||
else:
|
||||
CELERY_BROKER_URL = REDIS_URL
|
||||
CELERY_RESULT_BACKEND = REDIS_URL
|
||||
CELERY_BROKER_URL = RABBITMQ_URL
|
||||
CELERY_RESULT_BACKEND = REDIS_URL
|
||||
|
||||
CELERY_QUEUES = (
|
||||
Queue(
|
||||
"internal_tasks",
|
||||
Exchange("internal_exchange", type="direct"),
|
||||
routing_key="internal",
|
||||
),
|
||||
Queue(
|
||||
"external_tasks",
|
||||
Exchange("external_exchange", type="direct"),
|
||||
routing_key="external",
|
||||
),
|
||||
Queue(
|
||||
"segway_tasks",
|
||||
Exchange("segway_exchange", type="direct"),
|
||||
routing_key="segway",
|
||||
),
|
||||
)
|
||||
|
||||
CELERY_IMPORTS = (
|
||||
"plane.bgtasks.issue_automation_task",
|
||||
@@ -291,7 +305,9 @@ 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=[
|
||||
@@ -327,10 +343,11 @@ USE_MINIO = int(os.environ.get("USE_MINIO", 0)) == 1
|
||||
POSTHOG_API_KEY = os.environ.get("POSTHOG_API_KEY", False)
|
||||
POSTHOG_HOST = os.environ.get("POSTHOG_HOST", False)
|
||||
|
||||
# instance key
|
||||
INSTANCE_KEY = os.environ.get(
|
||||
"INSTANCE_KEY", "ae6517d563dfc13d8270bd45cf17b08f70b37d989128a9dab46ff687603333c3"
|
||||
)
|
||||
|
||||
# Skip environment variable configuration
|
||||
SKIP_ENV_VAR = os.environ.get("SKIP_ENV_VAR", "1") == "1"
|
||||
|
||||
DATA_UPLOAD_MAX_MEMORY_SIZE = int(os.environ.get("FILE_SIZE_LIMIT", 5242880))
|
||||
|
||||
# Segway
|
||||
SEGWAY_BASE_URL = os.environ.get("SEGWAY_BASE_URL", "http://localhost:9000")
|
||||
SEGWAY_KEY = os.environ.get("SEGWAY_KEY", False)
|
||||
|
||||
@@ -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.5
|
||||
cryptography==41.0.6
|
||||
lxml==4.9.3
|
||||
boto3==1.28.40
|
||||
|
||||
|
||||
@@ -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 availabe as 'variables-upgrade.env'. Please compare and set the required values in '.env 'file."
|
||||
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 ""
|
||||
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ volumes:
|
||||
|
||||
services:
|
||||
plane-redis:
|
||||
container_name: plane-redis
|
||||
image: redis:6.2.7-alpine
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
@@ -21,7 +20,6 @@ services:
|
||||
- redisdata:/data
|
||||
|
||||
plane-minio:
|
||||
container_name: plane-minio
|
||||
image: minio/minio
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
@@ -36,7 +34,6 @@ services:
|
||||
MINIO_ROOT_PASSWORD: ${AWS_SECRET_ACCESS_KEY}
|
||||
|
||||
plane-db:
|
||||
container_name: plane-db
|
||||
image: postgres:15.2-alpine
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
@@ -53,7 +50,6 @@ services:
|
||||
PGDATA: /var/lib/postgresql/data
|
||||
|
||||
web:
|
||||
container_name: web
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./web/Dockerfile.dev
|
||||
@@ -61,8 +57,7 @@ services:
|
||||
networks:
|
||||
- dev_env
|
||||
volumes:
|
||||
- .:/app
|
||||
command: yarn dev --filter=web
|
||||
- ./web:/app/web
|
||||
env_file:
|
||||
- ./web/.env
|
||||
depends_on:
|
||||
@@ -73,22 +68,17 @@ services:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./space/Dockerfile.dev
|
||||
container_name: space
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- dev_env
|
||||
volumes:
|
||||
- .:/app
|
||||
command: yarn dev --filter=space
|
||||
env_file:
|
||||
- ./space/.env
|
||||
- ./space:/app/space
|
||||
depends_on:
|
||||
- api
|
||||
- worker
|
||||
- web
|
||||
|
||||
api:
|
||||
container_name: api
|
||||
build:
|
||||
context: ./apiserver
|
||||
dockerfile: Dockerfile.dev
|
||||
@@ -99,7 +89,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:
|
||||
@@ -107,7 +97,6 @@ services:
|
||||
- plane-redis
|
||||
|
||||
worker:
|
||||
container_name: bgworker
|
||||
build:
|
||||
context: ./apiserver
|
||||
dockerfile: Dockerfile.dev
|
||||
@@ -127,7 +116,6 @@ services:
|
||||
- plane-redis
|
||||
|
||||
beat-worker:
|
||||
container_name: beatworker
|
||||
build:
|
||||
context: ./apiserver
|
||||
dockerfile: Dockerfile.dev
|
||||
@@ -147,10 +135,9 @@ services:
|
||||
- plane-redis
|
||||
|
||||
proxy:
|
||||
container_name: proxy
|
||||
build:
|
||||
context: ./nginx
|
||||
dockerfile: Dockerfile
|
||||
dockerfile: Dockerfile.dev
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- dev_env
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
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 /space/ {
|
||||
location /spaces/ {
|
||||
proxy_pass http://localhost:4000/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
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/;
|
||||
}
|
||||
|
||||
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/;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,28 +28,22 @@
|
||||
"react-dom": "18.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@plane/editor-types": "*",
|
||||
"@tiptap/core": "^2.1.7",
|
||||
"@tiptap/core": "^2.1.13",
|
||||
"@tiptap/extension-blockquote": "^2.1.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",
|
||||
"@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",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^1.2.1",
|
||||
"highlight.js": "^11.8.0",
|
||||
|
||||
+7
-4
@@ -1,10 +1,13 @@
|
||||
import { useEditor as useCustomEditor, Editor } from "@tiptap/react";
|
||||
import { useImperativeHandle, useRef, MutableRefObject } from "react";
|
||||
import { CoreEditorProps } from "../props";
|
||||
import { CoreEditorExtensions } from "../extensions";
|
||||
import { CoreEditorProps } from "src/ui/props";
|
||||
import { CoreEditorExtensions } from "src/ui/extensions";
|
||||
import { EditorProps } from "@tiptap/pm/view";
|
||||
import { getTrimmedHTML } from "../../lib/utils";
|
||||
import { DeleteImage, IMentionSuggestion, RestoreImage, UploadImage } from "@plane/editor-types";
|
||||
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";
|
||||
|
||||
interface CustomEditorProps {
|
||||
uploadFile: UploadImage;
|
||||
+3
-3
@@ -1,9 +1,9 @@
|
||||
import { useEditor as useCustomEditor, Editor } from "@tiptap/react";
|
||||
import { useImperativeHandle, useRef, MutableRefObject } from "react";
|
||||
import { CoreReadOnlyEditorExtensions } from "../read-only/extensions";
|
||||
import { CoreReadOnlyEditorProps } from "../read-only/props";
|
||||
import { CoreReadOnlyEditorExtensions } from "src/ui/read-only/extensions";
|
||||
import { CoreReadOnlyEditorProps } from "src/ui/read-only/props";
|
||||
import { EditorProps } from "@tiptap/pm/view";
|
||||
import { IMentionSuggestion } from "@plane/editor-types";
|
||||
import { IMentionSuggestion } from "src/types/mention-suggestion";
|
||||
|
||||
interface CustomReadOnlyEditorProps {
|
||||
value: string;
|
||||
@@ -1,23 +1,32 @@
|
||||
// styles
|
||||
// import "./styles/tailwind.css";
|
||||
// import "./styles/editor.css";
|
||||
import "./styles/github-dark.css";
|
||||
import "src/styles/editor.css";
|
||||
import "src/styles/table.css";
|
||||
import "src/styles/github-dark.css";
|
||||
|
||||
export { isCellSelection } from "./ui/extensions/table/table/utilities/is-cell-selection";
|
||||
export { isCellSelection } from "src/ui/extensions/table/table/utilities/is-cell-selection";
|
||||
|
||||
// utils
|
||||
export * from "./lib/utils";
|
||||
export * from "./ui/extensions/table/table";
|
||||
export { startImageUpload } from "./ui/plugins/upload-image";
|
||||
export * from "src/lib/utils";
|
||||
export * from "src/ui/extensions/table/table";
|
||||
export { startImageUpload } from "src/ui/plugins/upload-image";
|
||||
|
||||
// components
|
||||
export { EditorContainer } from "./ui/components/editor-container";
|
||||
export { EditorContentWrapper } from "./ui/components/editor-content";
|
||||
export { EditorContainer } from "src/ui/components/editor-container";
|
||||
export { EditorContentWrapper } from "src/ui/components/editor-content";
|
||||
|
||||
// hooks
|
||||
export { useEditor } from "./ui/hooks/use-editor";
|
||||
export { useReadOnlyEditor } from "./ui/hooks/use-read-only-editor";
|
||||
export { useEditor } from "src/hooks/use-editor";
|
||||
export { useReadOnlyEditor } from "src/hooks/use-read-only-editor";
|
||||
|
||||
// helper items
|
||||
export * from "./ui/menus/menu-items";
|
||||
export * from "./lib/editor-commands";
|
||||
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";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { UploadImage } from "@plane/editor-types";
|
||||
import { Editor, Range } from "@tiptap/core";
|
||||
import { startImageUpload } from "../ui/plugins/upload-image";
|
||||
import { findTableAncestor } from "./utils";
|
||||
import { startImageUpload } from "src/ui/plugins/upload-image";
|
||||
import { findTableAncestor } from "src/lib/utils";
|
||||
import { UploadImage } from "src/types/upload-image";
|
||||
|
||||
export const toggleHeadingOne = (editor: Editor, range?: Range) => {
|
||||
if (range) editor.chain().focus().deleteRange(range).setNode("heading", { level: 1 }).run();
|
||||
|
||||
@@ -6,6 +6,12 @@
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/* block quotes */
|
||||
.ProseMirror blockquote p::before,
|
||||
.ProseMirror blockquote p::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ProseMirror .is-empty::before {
|
||||
content: attr(data-placeholder);
|
||||
float: left;
|
||||
@@ -15,9 +21,10 @@
|
||||
}
|
||||
|
||||
/* Custom image styles */
|
||||
|
||||
.ProseMirror img {
|
||||
transition: filter 0.1s ease-in-out;
|
||||
margin-top: 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
@@ -53,11 +60,12 @@ ul[data-type="taskList"] li > label input[type="checkbox"] {
|
||||
background-color: rgb(var(--color-background-100));
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
width: 1.2rem;
|
||||
height: 1.2rem;
|
||||
width: 0.8rem;
|
||||
height: 0.8rem;
|
||||
position: relative;
|
||||
border: 2px solid rgb(var(--color-text-100));
|
||||
margin-right: 0.3rem;
|
||||
border: 1.5px solid rgb(var(--color-text-100));
|
||||
margin-right: 0.2rem;
|
||||
margin-top: 0.15rem;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
|
||||
@@ -71,8 +79,8 @@ ul[data-type="taskList"] li > label input[type="checkbox"] {
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
width: 0.65em;
|
||||
height: 0.65em;
|
||||
width: 0.5em;
|
||||
height: 0.5em;
|
||||
transform: scale(0);
|
||||
transition: 120ms transform ease-in-out;
|
||||
box-shadow: inset 1em 1em;
|
||||
@@ -133,6 +141,8 @@ 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: "";
|
||||
@@ -159,7 +169,8 @@ ul[data-type="taskList"] li[data-checked="true"] > div > p {
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
margin: 0;
|
||||
margin: 0.5em 0 0.5em 0;
|
||||
|
||||
border: 1px solid rgb(var(--color-border-200));
|
||||
width: 100%;
|
||||
|
||||
@@ -229,3 +240,34 @@ 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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import { Smile } from "lucide-react";
|
||||
|
||||
export type LucideIconType = typeof Smile;
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Editor, EditorContent } from "@tiptap/react";
|
||||
import { ReactNode } from "react";
|
||||
import { ImageResizer } from "../extensions/image/image-resize";
|
||||
import { ImageResizer } from "src/ui/extensions/image/image-resize";
|
||||
|
||||
interface EditorContentProps {
|
||||
editor: Editor | null;
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import { getNodeAtPosition } from "@tiptap/core";
|
||||
import { EditorState } from "@tiptap/pm/state";
|
||||
|
||||
import { findListItemPos } from "./find-list-item-pos";
|
||||
import { findListItemPos } from "src/ui/extensions/custom-list-keymap/list-helpers/find-list-item-pos";
|
||||
|
||||
export const getNextListDepth = (typeOrName: string, state: EditorState) => {
|
||||
const listItemPos = findListItemPos(typeOrName, state);
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
import { Editor, isAtStartOfNode, isNodeActive } from "@tiptap/core";
|
||||
import { Node } from "@tiptap/pm/model";
|
||||
|
||||
import { findListItemPos } from "./find-list-item-pos";
|
||||
import { hasListBefore } from "./has-list-before";
|
||||
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";
|
||||
|
||||
export const handleBackspace = (editor: Editor, name: string, parentListTypes: string[]) => {
|
||||
// this is required to still handle the undo handling
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import { Editor, isAtEndOfNode, isNodeActive } from "@tiptap/core";
|
||||
|
||||
import { nextListIsDeeper } from "./next-list-is-deeper";
|
||||
import { nextListIsHigher } from "./next-list-is-higher";
|
||||
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";
|
||||
|
||||
export const handleDelete = (editor: Editor, name: string) => {
|
||||
// if the cursor is not inside the current node type
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import { EditorState } from "@tiptap/pm/state";
|
||||
|
||||
import { findListItemPos } from "./find-list-item-pos";
|
||||
import { getNextListDepth } from "./get-next-list-depth";
|
||||
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";
|
||||
|
||||
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 "./find-list-item-pos";
|
||||
import { getNextListDepth } from "./get-next-list-depth";
|
||||
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";
|
||||
|
||||
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 "./list-helpers";
|
||||
import { handleBackspace, handleDelete } from "src/ui/extensions/custom-list-keymap/list-helpers";
|
||||
|
||||
export type ListKeymapOptions = {
|
||||
listTypes: Array<{
|
||||
|
||||
@@ -22,7 +22,7 @@ declare module "@tiptap/core" {
|
||||
}
|
||||
}
|
||||
|
||||
export default Node.create<HorizontalRuleOptions>({
|
||||
export const HorizontalRule = Node.create<HorizontalRuleOptions>({
|
||||
name: "horizontalRule",
|
||||
|
||||
addOptions() {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { EditorState, Plugin, PluginKey, Transaction } from "@tiptap/pm/state";
|
||||
import { Node as ProseMirrorNode } from "@tiptap/pm/model";
|
||||
import UploadImagesPlugin from "../../plugins/upload-image";
|
||||
import { UploadImagesPlugin } from "src/ui/plugins/upload-image";
|
||||
import ImageExt from "@tiptap/extension-image";
|
||||
import { onNodeDeleted, onNodeRestored } from "../../plugins/delete-image";
|
||||
import { DeleteImage, RestoreImage } from "@plane/editor-types";
|
||||
import { onNodeDeleted, onNodeRestored } from "src/ui/plugins/delete-image";
|
||||
import { DeleteImage } from "src/types/delete-image";
|
||||
import { RestoreImage } from "src/types/restore-image";
|
||||
|
||||
interface ImageNode extends ProseMirrorNode {
|
||||
attrs: {
|
||||
@@ -15,7 +16,7 @@ interface ImageNode extends ProseMirrorNode {
|
||||
const deleteKey = new PluginKey("delete-image");
|
||||
const IMAGE_NODE_TYPE = "image";
|
||||
|
||||
const ImageExtension = (deleteImage: DeleteImage, restoreFile: RestoreImage, cancelUploadImage?: () => any) =>
|
||||
export const ImageExtension = (deleteImage: DeleteImage, restoreFile: RestoreImage, cancelUploadImage?: () => any) =>
|
||||
ImageExt.extend({
|
||||
addProseMirrorPlugins() {
|
||||
return [
|
||||
@@ -130,5 +131,3 @@ const ImageExtension = (deleteImage: DeleteImage, restoreFile: RestoreImage, can
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default ImageExtension;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Image from "@tiptap/extension-image";
|
||||
|
||||
const ReadOnlyImageExtension = Image.extend({
|
||||
export const ReadOnlyImageExtension = Image.extend({
|
||||
addAttributes() {
|
||||
return {
|
||||
...this.parent?.(),
|
||||
@@ -13,5 +13,3 @@ const ReadOnlyImageExtension = Image.extend({
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default ReadOnlyImageExtension;
|
||||
|
||||
@@ -7,22 +7,25 @@ import TaskItem from "@tiptap/extension-task-item";
|
||||
import TaskList from "@tiptap/extension-task-list";
|
||||
import { Markdown } from "tiptap-markdown";
|
||||
|
||||
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 { 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 ImageExtension from "./image";
|
||||
import { ImageExtension } from "src/ui/extensions/image";
|
||||
|
||||
import { isValidHttpUrl } from "../../lib/utils";
|
||||
import { Mentions } from "../mentions";
|
||||
import { isValidHttpUrl } from "src/lib/utils";
|
||||
import { Mentions } from "src/ui/mentions";
|
||||
|
||||
import { CustomKeymap } from "./keymap";
|
||||
import { CustomCodeBlock } from "./code";
|
||||
import { CustomQuoteExtension } from "./quote";
|
||||
import { ListKeymap } from "./custom-list-keymap";
|
||||
import { IMentionSuggestion, DeleteImage, RestoreImage } from "@plane/editor-types";
|
||||
import { 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";
|
||||
|
||||
export const CoreEditorExtensions = (
|
||||
mentionConfig: {
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { default as default } from "./table-cell";
|
||||
export { TableCell } from "./table-cell";
|
||||
|
||||
@@ -4,7 +4,7 @@ export interface TableCellOptions {
|
||||
HTMLAttributes: Record<string, any>;
|
||||
}
|
||||
|
||||
export default Node.create<TableCellOptions>({
|
||||
export const TableCell = Node.create<TableCellOptions>({
|
||||
name: "tableCell",
|
||||
|
||||
addOptions() {
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { default as default } from "./table-header";
|
||||
export { TableHeader } from "./table-header";
|
||||
|
||||
@@ -3,7 +3,8 @@ import { mergeAttributes, Node } from "@tiptap/core";
|
||||
export interface TableHeaderOptions {
|
||||
HTMLAttributes: Record<string, any>;
|
||||
}
|
||||
export default Node.create<TableHeaderOptions>({
|
||||
|
||||
export const TableHeader = Node.create<TableHeaderOptions>({
|
||||
name: "tableHeader",
|
||||
|
||||
addOptions() {
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { default as default } from "./table-row";
|
||||
export { TableRow } from "./table-row";
|
||||
|
||||
@@ -4,7 +4,7 @@ export interface TableRowOptions {
|
||||
HTMLAttributes: Record<string, any>;
|
||||
}
|
||||
|
||||
export default Node.create<TableRowOptions>({
|
||||
export const TableRow = Node.create<TableRowOptions>({
|
||||
name: "tableRow",
|
||||
|
||||
addOptions() {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const icons = {
|
||||
export 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,5 +47,3 @@ const icons = {
|
||||
</svg>
|
||||
`,
|
||||
};
|
||||
|
||||
export default icons;
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { default as default } from "./table";
|
||||
export { Table } from "./table";
|
||||
|
||||
@@ -4,9 +4,9 @@ import { Decoration, NodeView } from "@tiptap/pm/view";
|
||||
import tippy, { Instance, Props } from "tippy.js";
|
||||
|
||||
import { Editor } from "@tiptap/core";
|
||||
import { CellSelection, TableMap, updateColumnsOnResize } from "@tiptap/prosemirror-tables";
|
||||
import { CellSelection, TableMap, updateColumnsOnResize } from "@tiptap/pm/tables";
|
||||
|
||||
import icons from "./icons";
|
||||
import { icons } from "src/ui/extensions/table/table/icons";
|
||||
|
||||
export function updateColumns(
|
||||
node: ProseMirrorNode,
|
||||
|
||||
@@ -19,12 +19,12 @@ import {
|
||||
tableEditing,
|
||||
toggleHeader,
|
||||
toggleHeaderCell,
|
||||
} from "@tiptap/prosemirror-tables";
|
||||
} from "@tiptap/pm/tables";
|
||||
|
||||
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";
|
||||
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";
|
||||
|
||||
export interface TableOptions {
|
||||
HTMLAttributes: Record<string, any>;
|
||||
@@ -72,7 +72,7 @@ declare module "@tiptap/core" {
|
||||
}
|
||||
}
|
||||
|
||||
export default Node.create({
|
||||
export const Table = Node.create({
|
||||
name: "table",
|
||||
|
||||
addOptions() {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Fragment, Node as ProsemirrorNode, Schema } from "@tiptap/pm/model";
|
||||
|
||||
import { createCell } from "./create-cell";
|
||||
import { getTableNodeTypes } from "./get-table-node-types";
|
||||
import { createCell } from "src/ui/extensions/table/table/utilities/create-cell";
|
||||
import { getTableNodeTypes } from "src/ui/extensions/table/table/utilities/get-table-node-types";
|
||||
|
||||
export function createTable(
|
||||
schema: Schema,
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { findParentNodeClosestToPos, KeyboardShortcutCommand } from "@tiptap/core";
|
||||
|
||||
import { isCellSelection } from "./is-cell-selection";
|
||||
import { isCellSelection } from "src/ui/extensions/table/table/utilities/is-cell-selection";
|
||||
|
||||
export const deleteTableWhenAllCellsSelected: KeyboardShortcutCommand = ({ editor }) => {
|
||||
const { selection } = editor.state;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CellSelection } from "@tiptap/prosemirror-tables";
|
||||
import { CellSelection } from "@tiptap/pm/tables";
|
||||
|
||||
export function isCellSelection(value: unknown): value is CellSelection {
|
||||
return value instanceof CellSelection;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Mention, MentionOptions } from "@tiptap/extension-mention";
|
||||
import { mergeAttributes } from "@tiptap/core";
|
||||
import { ReactNodeViewRenderer } from "@tiptap/react";
|
||||
import mentionNodeView from "./mentionNodeView";
|
||||
import { IMentionHighlight } from "@plane/editor-types";
|
||||
import { MentionNodeView } from "src/ui/mentions/mention-node-view";
|
||||
import { IMentionHighlight } from "src/types/mention-suggestion";
|
||||
|
||||
export interface CustomMentionOptions extends MentionOptions {
|
||||
mentionHighlights: IMentionHighlight[];
|
||||
@@ -31,7 +31,7 @@ export const CustomMention = Mention.extend<CustomMentionOptions>({
|
||||
},
|
||||
|
||||
addNodeView() {
|
||||
return ReactNodeViewRenderer(mentionNodeView);
|
||||
return ReactNodeViewRenderer(MentionNodeView);
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// @ts-nocheck
|
||||
|
||||
import suggestion from "./suggestion";
|
||||
import { CustomMention } from "./custom";
|
||||
import { IMentionHighlight, IMentionSuggestion } from "@plane/editor-types";
|
||||
import { Suggestion } from "src/ui/mentions/suggestion";
|
||||
import { CustomMention } from "src/ui/mentions/custom";
|
||||
import { IMentionHighlight } from "src/types/mention-suggestion";
|
||||
|
||||
export const Mentions = (mentionSuggestions: IMentionSuggestion[], mentionHighlights: IMentionHighlight[], readonly) =>
|
||||
CustomMention.configure({
|
||||
@@ -11,5 +11,5 @@ export const Mentions = (mentionSuggestions: IMentionSuggestion[], mentionHighli
|
||||
},
|
||||
readonly: readonly,
|
||||
mentionHighlights: mentionHighlights,
|
||||
suggestion: suggestion(mentionSuggestions),
|
||||
suggestion: Suggestion(mentionSuggestions),
|
||||
});
|
||||
|
||||
+3
-5
@@ -1,6 +1,6 @@
|
||||
import { IMentionSuggestion } from "@plane/editor-types";
|
||||
import { Editor } from "@tiptap/react";
|
||||
import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useState } from "react";
|
||||
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
|
||||
import { IMentionSuggestion } from "src/types/mention-suggestion";
|
||||
|
||||
interface MentionListProps {
|
||||
items: IMentionSuggestion[];
|
||||
@@ -9,7 +9,7 @@ interface MentionListProps {
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react/display-name
|
||||
const MentionList = forwardRef((props: MentionListProps, ref) => {
|
||||
export const MentionList = forwardRef((props: MentionListProps, ref) => {
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
|
||||
const selectItem = (index: number) => {
|
||||
@@ -98,5 +98,3 @@ const MentionList = forwardRef((props: MentionListProps, ref) => {
|
||||
});
|
||||
|
||||
MentionList.displayName = "MentionList";
|
||||
|
||||
export default MentionList;
|
||||
+3
-3
@@ -1,12 +1,12 @@
|
||||
/* eslint-disable react/display-name */
|
||||
// @ts-nocheck
|
||||
import { NodeViewWrapper } from "@tiptap/react";
|
||||
import { cn } from "../../lib/utils";
|
||||
import { cn } from "src/lib/utils";
|
||||
import { useRouter } from "next/router";
|
||||
import { IMentionHighlight } from "@plane/editor-types";
|
||||
import { IMentionHighlight } from "src/types/mention-suggestion";
|
||||
|
||||
// eslint-disable-next-line import/no-anonymous-default-export
|
||||
export default (props) => {
|
||||
export const MentionNodeView = (props) => {
|
||||
const router = useRouter();
|
||||
const highlights = props.extension.options.mentionHighlights as IMentionHighlight[];
|
||||
|
||||
@@ -2,10 +2,10 @@ import { ReactRenderer } from "@tiptap/react";
|
||||
import { Editor } from "@tiptap/core";
|
||||
import tippy from "tippy.js";
|
||||
|
||||
import MentionList from "./MentionList";
|
||||
import { IMentionSuggestion } from "@plane/editor-types";
|
||||
import { MentionList } from "src/ui/mentions/mention-list";
|
||||
import { IMentionSuggestion } from "src/types/mention-suggestion";
|
||||
|
||||
const Suggestion = (suggestions: IMentionSuggestion[]) => ({
|
||||
export const Suggestion = (suggestions: IMentionSuggestion[]) => ({
|
||||
items: ({ query }: { query: string }) =>
|
||||
suggestions.filter((suggestion) => suggestion.title.toLowerCase().startsWith(query.toLowerCase())).slice(0, 5),
|
||||
render: () => {
|
||||
@@ -55,5 +55,3 @@ const Suggestion = (suggestions: IMentionSuggestion[]) => ({
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default Suggestion;
|
||||
|
||||
@@ -30,14 +30,15 @@ import {
|
||||
toggleStrike,
|
||||
toggleTaskList,
|
||||
toggleUnderline,
|
||||
} from "../../../lib/editor-commands";
|
||||
import { UploadImage } from "@plane/editor-types";
|
||||
} from "src/lib/editor-commands";
|
||||
import { LucideIconType } from "src/types/lucide-icon";
|
||||
import { UploadImage } from "src/types/upload-image";
|
||||
|
||||
export interface EditorMenuItem {
|
||||
name: string;
|
||||
isActive: () => boolean;
|
||||
command: () => void;
|
||||
icon: typeof BoldIcon;
|
||||
icon: LucideIconType;
|
||||
}
|
||||
|
||||
export const HeadingOneItem = (editor: Editor): EditorMenuItem => ({
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { EditorState, Plugin, PluginKey, Transaction } from "@tiptap/pm/state";
|
||||
import { Node as ProseMirrorNode } from "@tiptap/pm/model";
|
||||
import { DeleteImage, RestoreImage } from "@plane/editor-types";
|
||||
import { DeleteImage } from "src/types/delete-image";
|
||||
import { RestoreImage } from "src/types/restore-image";
|
||||
|
||||
const deleteKey = new PluginKey("delete-image");
|
||||
const IMAGE_NODE_TYPE = "image";
|
||||
@@ -12,7 +13,7 @@ interface ImageNode extends ProseMirrorNode {
|
||||
};
|
||||
}
|
||||
|
||||
const TrackImageDeletionPlugin = (deleteImage: DeleteImage): Plugin =>
|
||||
export const TrackImageDeletionPlugin = (deleteImage: DeleteImage): Plugin =>
|
||||
new Plugin({
|
||||
key: deleteKey,
|
||||
appendTransaction: (transactions: readonly Transaction[], oldState: EditorState, newState: EditorState) => {
|
||||
@@ -53,8 +54,6 @@ const TrackImageDeletionPlugin = (deleteImage: DeleteImage): Plugin =>
|
||||
},
|
||||
});
|
||||
|
||||
export default TrackImageDeletionPlugin;
|
||||
|
||||
export async function onNodeDeleted(src: string, deleteImage: DeleteImage): Promise<void> {
|
||||
try {
|
||||
const assetUrlWithWorkspaceId = new URL(src).pathname.substring(1);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { UploadImage } from "@plane/editor-types";
|
||||
import { EditorState, Plugin, PluginKey } from "@tiptap/pm/state";
|
||||
import { Decoration, DecorationSet, EditorView } from "@tiptap/pm/view";
|
||||
import { UploadImage } from "src/types/upload-image";
|
||||
|
||||
const uploadKey = new PluginKey("upload-image");
|
||||
|
||||
const UploadImagesPlugin = (cancelUploadImage?: () => any) =>
|
||||
export const UploadImagesPlugin = (cancelUploadImage?: () => any) =>
|
||||
new Plugin({
|
||||
key: uploadKey,
|
||||
state: {
|
||||
@@ -43,7 +43,7 @@ const UploadImagesPlugin = (cancelUploadImage?: () => any) =>
|
||||
|
||||
cancelButton.appendChild(svgElement);
|
||||
placeholder.appendChild(cancelButton);
|
||||
const deco = Decoration.widget(pos + 1, placeholder, {
|
||||
const deco = Decoration.widget(pos, placeholder, {
|
||||
id,
|
||||
});
|
||||
set = set.add(tr.doc, [deco]);
|
||||
@@ -60,8 +60,6 @@ const UploadImagesPlugin = (cancelUploadImage?: () => any) =>
|
||||
},
|
||||
});
|
||||
|
||||
export default UploadImagesPlugin;
|
||||
|
||||
function findPlaceholder(state: EditorState, id: {}) {
|
||||
const decos = uploadKey.getState(state);
|
||||
const found = decos.find(undefined, undefined, (spec: { id: number | undefined }) => spec.id == id);
|
||||
@@ -133,7 +131,8 @@ export async function startImageUpload(
|
||||
const imageSrc = typeof src === "object" ? reader.result : src;
|
||||
|
||||
const node = schema.nodes.image.create({ src: imageSrc });
|
||||
const transaction = view.state.tr.replaceWith(pos, pos, node).setMeta(uploadKey, { remove: { id } });
|
||||
const transaction = view.state.tr.insert(pos - 1, node).setMeta(uploadKey, { remove: { id } });
|
||||
|
||||
view.dispatch(transaction);
|
||||
} catch (error) {
|
||||
console.error("Upload error: ", error);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { UploadImage } from "@plane/editor-types";
|
||||
import { EditorProps } from "@tiptap/pm/view";
|
||||
import { findTableAncestor } from "../lib/utils";
|
||||
import { startImageUpload } from "./plugins/upload-image";
|
||||
import { findTableAncestor } from "src/lib/utils";
|
||||
import { UploadImage } from "src/types/upload-image";
|
||||
import { startImageUpload } from "src/ui/plugins/upload-image";
|
||||
|
||||
export function CoreEditorProps(
|
||||
uploadFile: UploadImage,
|
||||
|
||||
@@ -8,15 +8,16 @@ import TaskList from "@tiptap/extension-task-list";
|
||||
import { Markdown } from "tiptap-markdown";
|
||||
import Gapcursor from "@tiptap/extension-gapcursor";
|
||||
|
||||
import TableHeader from "../extensions/table/table-header/table-header";
|
||||
import Table from "../extensions/table/table";
|
||||
import TableCell from "../extensions/table/table-cell/table-cell";
|
||||
import TableRow from "../extensions/table/table-row/table-row";
|
||||
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 ReadOnlyImageExtension from "../extensions/image/read-only-image";
|
||||
import { isValidHttpUrl } from "../../lib/utils";
|
||||
import { Mentions } from "../mentions";
|
||||
import { IMentionSuggestion } from "@plane/editor-types";
|
||||
import { ReadOnlyImageExtension } from "src/ui/extensions/image/read-only-image";
|
||||
import { isValidHttpUrl } from "src/lib/utils";
|
||||
import { Mentions } from "src/ui/mentions";
|
||||
import { IMentionSuggestion } from "src/types/mention-suggestion";
|
||||
|
||||
export const CoreReadOnlyEditorExtensions = (mentionConfig: {
|
||||
mentionSuggestions: IMentionSuggestion[];
|
||||
@@ -71,6 +72,7 @@ export const CoreReadOnlyEditorExtensions = (mentionConfig: {
|
||||
class: "rounded-lg border border-custom-border-300",
|
||||
},
|
||||
}),
|
||||
HorizontalRule,
|
||||
TiptapUnderline,
|
||||
TextStyle,
|
||||
Color,
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
{
|
||||
"extends": "tsconfig/react-library.json",
|
||||
"include": ["src/**/*", "index.d.ts"],
|
||||
"exclude": ["dist", "build", "node_modules"]
|
||||
"include": [
|
||||
"src/**/*",
|
||||
"index.d.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"dist",
|
||||
"build",
|
||||
"node_modules"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"baseUrl": "."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,12 +30,11 @@
|
||||
"dependencies": {
|
||||
"@plane/editor-core": "*",
|
||||
"@plane/editor-extensions": "*",
|
||||
"@plane/editor-types": "*",
|
||||
"@plane/ui": "*",
|
||||
"@tiptap/core": "^2.1.7",
|
||||
"@tiptap/extension-placeholder": "^2.1.11",
|
||||
"@tiptap/pm": "^2.1.12",
|
||||
"@tiptap/suggestion": "^2.1.12",
|
||||
"@tiptap/core": "^2.1.13",
|
||||
"@tiptap/extension-placeholder": "^2.1.13",
|
||||
"@tiptap/pm": "^2.1.13",
|
||||
"@tiptap/suggestion": "^2.1.13",
|
||||
"eslint": "8.36.0",
|
||||
"eslint-config-next": "13.2.4",
|
||||
"react-popper": "^2.3.0",
|
||||
|
||||
+1
-2
@@ -1,6 +1,5 @@
|
||||
import { Editor } from "@tiptap/react";
|
||||
import { useState } from "react";
|
||||
import { IMarking } from "..";
|
||||
import { IMarking } from "src/types/editor-types";
|
||||
|
||||
export const useEditorMarkings = () => {
|
||||
const [markings, setMarkings] = useState<IMarking[]>([]);
|
||||
@@ -1,3 +1,3 @@
|
||||
export { DocumentEditor, DocumentEditorWithRef } from "./ui";
|
||||
export { DocumentReadOnlyEditor, DocumentReadOnlyEditorWithRef } from "./ui/readonly";
|
||||
export { FixedMenu } from "./ui/menu/fixed-menu";
|
||||
export { DocumentEditor, DocumentEditorWithRef } from "src/ui";
|
||||
export { DocumentReadOnlyEditor, DocumentReadOnlyEditorWithRef } from "src/ui/readonly";
|
||||
export { FixedMenu } from "src/ui/menu/fixed-menu";
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user