Compare commits
142
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0876ea6b55 | ||
|
|
7915121dd9 | ||
|
|
1e7f451d0b | ||
|
|
c67e097fc2 | ||
|
|
804dd8300d | ||
|
|
c6d6b9a0e9 | ||
|
|
9debd81a50 | ||
|
|
d53a086206 | ||
|
|
ef8472ce5e | ||
|
|
4aa34f3eda | ||
|
|
c7616fda11 | ||
|
|
483fc57601 | ||
|
|
09a1a55da8 | ||
|
|
61f92563a9 | ||
|
|
00e07443b0 | ||
|
|
c4efdcd704 | ||
|
|
3c9679dff9 | ||
|
|
b3393f5c48 | ||
|
|
f995736642 | ||
|
|
f7f1f2bea4 | ||
|
|
532da80375 | ||
|
|
212f2b54f8 | ||
|
|
9ecdcc6fde | ||
|
|
ddae745669 | ||
|
|
e78c1f2060 | ||
|
|
ebc891b985 | ||
|
|
60b5589c48 | ||
|
|
f8208b1b5e | ||
|
|
6c6b764421 | ||
|
|
5c912b8821 | ||
|
|
ff19980502 | ||
|
|
a1a24e4574 | ||
|
|
c1c2a6ddce | ||
|
|
ec3cad1f25 | ||
|
|
336c97d336 | ||
|
|
e4a3d0db5c | ||
|
|
7f2e99dd2d | ||
|
|
a104cc4814 | ||
|
|
03cbad5110 | ||
|
|
2956c43ed5 | ||
|
|
eae32593cb | ||
|
|
7fd625e0e3 | ||
|
|
f007dcff26 | ||
|
|
adf091fa07 | ||
|
|
b66f07845a | ||
|
|
911211cf3d | ||
|
|
53b41481a2 | ||
|
|
9d9d703c62 | ||
|
|
a2f34e9573 | ||
|
|
81f84f24f7 | ||
|
|
87f39d7372 | ||
|
|
1a1594e818 | ||
|
|
8d3ea5bb3e | ||
|
|
6a2be6afc4 | ||
|
|
338d58f79d | ||
|
|
e23e4bc392 | ||
|
|
c1598c3d38 | ||
|
|
4a436eeee2 | ||
|
|
47681fe9f8 | ||
|
|
f27efb80e1 | ||
|
|
c1e1b81b99 | ||
|
|
d9db765ae3 | ||
|
|
0531dc3308 | ||
|
|
e36b7a5ab9 | ||
|
|
c6b756d918 | ||
|
|
f3ae57bc85 | ||
|
|
2374161030 | ||
|
|
512ad83c08 | ||
|
|
801f75f406 | ||
|
|
f88109ef04 | ||
|
|
bb50df0dff | ||
|
|
2986769f28 | ||
|
|
fd5326dec6 | ||
|
|
49452a68ab | ||
|
|
b3ac9def8d | ||
|
|
be62662bb1 | ||
|
|
4eba5c115a | ||
|
|
24442ccc9c | ||
|
|
e32a50fba6 | ||
|
|
4baf2a2f09 | ||
|
|
577118ca02 | ||
|
|
4a26f11e23 | ||
|
|
9926e321f6 | ||
|
|
7f5028a4f6 | ||
|
|
06a7bdffd7 | ||
|
|
d3dedc8e51 | ||
|
|
d656f8e62a | ||
|
|
864519e770 | ||
|
|
034f0a06db | ||
|
|
7263cb072c | ||
|
|
ea3a0362b0 | ||
|
|
c9337d4a41 | ||
|
|
f347c1cd69 | ||
|
|
3f07c48b35 | ||
|
|
04b2214bf2 | ||
|
|
1adb38655a | ||
|
|
4ab64b6905 | ||
|
|
4b0d85591e | ||
|
|
8e2789af3e | ||
|
|
3592feb3d7 | ||
|
|
bc6a2542f5 | ||
|
|
5e2d93df52 | ||
|
|
cce349b805 | ||
|
|
fadda7cf04 | ||
|
|
c4093d29a7 | ||
|
|
8c89e9cc01 | ||
|
|
5625a3581a | ||
|
|
ee387c4222 | ||
|
|
6a16a98b03 | ||
|
|
11f84a986c | ||
|
|
4b0d48b290 | ||
|
|
9789068880 | ||
|
|
151c355177 | ||
|
|
38580c3940 | ||
|
|
7ff91fdb82 | ||
|
|
a679b42200 | ||
|
|
27762ea500 | ||
|
|
2cd5dbcd02 | ||
|
|
96868760a3 | ||
|
|
df97b35a99 | ||
|
|
4611ec0b83 | ||
|
|
e6b31e2550 | ||
|
|
59fb371e3d | ||
|
|
23e5306f6d | ||
|
|
0f99fb302b | ||
|
|
0e49d616b7 | ||
|
|
e72920d33e | ||
|
|
80dc38b649 | ||
|
|
43b503c756 | ||
|
|
68d370fd86 | ||
|
|
5e03f3dd82 | ||
|
|
1257a88089 | ||
|
|
12a3392722 | ||
|
|
b62a1b11b1 | ||
|
|
0a05aef046 | ||
|
|
efd3ebf067 | ||
|
|
266f14d550 | ||
|
|
9eb8f41008 | ||
|
|
b340232e76 | ||
|
|
64ca267db4 | ||
|
|
46e79dde27 | ||
|
|
7272c54439 |
@@ -1,61 +1,30 @@
|
||||
name: Branch Build
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- closed
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
branch_name:
|
||||
description: "Branch Name"
|
||||
required: true
|
||||
default: "preview"
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- preview
|
||||
- qa
|
||||
- develop
|
||||
- release-*
|
||||
release:
|
||||
types: [released, prereleased]
|
||||
|
||||
env:
|
||||
TARGET_BRANCH: ${{ github.event.pull_request.base.ref || github.event.release.target_commitish }}
|
||||
TARGET_BRANCH: ${{ inputs.branch_name || github.ref_name || github.event.release.target_commitish }}
|
||||
|
||||
jobs:
|
||||
branch_build_setup:
|
||||
if: ${{ (github.event_name == 'pull_request' && github.event.action =='closed' && github.event.pull_request.merged == true) || github.event_name == 'release' }}
|
||||
name: Build-Push Web/Space/API/Proxy Docker Image
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v3.3.0
|
||||
|
||||
- name: Uploading Proxy Source
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: proxy-src-code
|
||||
path: ./nginx
|
||||
- name: Uploading Backend Source
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: backend-src-code
|
||||
path: ./apiserver
|
||||
- name: Uploading Web Source
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: web-src-code
|
||||
path: |
|
||||
./
|
||||
!./apiserver
|
||||
!./nginx
|
||||
!./deploy
|
||||
!./space
|
||||
- name: Uploading Space Source
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: space-src-code
|
||||
path: |
|
||||
./
|
||||
!./apiserver
|
||||
!./nginx
|
||||
!./deploy
|
||||
!./web
|
||||
outputs:
|
||||
gh_branch_name: ${{ env.TARGET_BRANCH }}
|
||||
|
||||
@@ -63,33 +32,38 @@ jobs:
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [branch_build_setup]
|
||||
env:
|
||||
FRONTEND_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend${{ secrets.DOCKER_REPO_SUFFIX || '' }}:${{ needs.branch_build_setup.outputs.gh_branch_name }}
|
||||
FRONTEND_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend:${{ needs.branch_build_setup.outputs.gh_branch_name }}
|
||||
steps:
|
||||
- name: Set Frontend Docker Tag
|
||||
- name: Set Frontend Docker Tag
|
||||
run: |
|
||||
if [ "${{ needs.branch_build_setup.outputs.gh_branch_name }}" == "master" ] && [ "${{ github.event_name }}" == "release" ]; then
|
||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend${{ secrets.DOCKER_REPO_SUFFIX || '' }}:latest,${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend${{ secrets.DOCKER_REPO_SUFFIX || '' }}:${{ github.event.release.tag_name }}
|
||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend:latest,${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend:${{ github.event.release.tag_name }}
|
||||
elif [ "${{ needs.branch_build_setup.outputs.gh_branch_name }}" == "master" ]; then
|
||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend${{ secrets.DOCKER_REPO_SUFFIX || '' }}:stable
|
||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend:stable
|
||||
else
|
||||
TAG=${{ env.FRONTEND_TAG }}
|
||||
fi
|
||||
echo "FRONTEND_TAG=${TAG}" >> $GITHUB_ENV
|
||||
- name: Docker Setup QEMU
|
||||
uses: docker/setup-qemu-action@v3.0.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2.5.0
|
||||
uses: docker/setup-buildx-action@v3.0.0
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
buildkitd-flags: "--allow-insecure-entitlement security.insecure"
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.1.0
|
||||
uses: docker/login-action@v3.0.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Downloading Web Source Code
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: web-src-code
|
||||
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v4.1.1
|
||||
|
||||
- name: Build and Push Frontend to Docker Container Registry
|
||||
uses: docker/build-push-action@v4.0.0
|
||||
uses: docker/build-push-action@v5.1.0
|
||||
with:
|
||||
context: .
|
||||
file: ./web/Dockerfile.web
|
||||
@@ -105,33 +79,39 @@ jobs:
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [branch_build_setup]
|
||||
env:
|
||||
SPACE_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-space${{ secrets.DOCKER_REPO_SUFFIX || '' }}:${{ needs.branch_build_setup.outputs.gh_branch_name }}
|
||||
SPACE_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-space:${{ needs.branch_build_setup.outputs.gh_branch_name }}
|
||||
steps:
|
||||
- name: Set Space Docker Tag
|
||||
- name: Set Space Docker Tag
|
||||
run: |
|
||||
if [ "${{ needs.branch_build_setup.outputs.gh_branch_name }}" == "master" ] && [ "${{ github.event_name }}" == "release" ]; then
|
||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-space${{ secrets.DOCKER_REPO_SUFFIX || '' }}:latest,${{ secrets.DOCKERHUB_USERNAME }}/plane-space${{ secrets.DOCKER_REPO_SUFFIX || '' }}:${{ github.event.release.tag_name }}
|
||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-space:latest,${{ secrets.DOCKERHUB_USERNAME }}/plane-space:${{ github.event.release.tag_name }}
|
||||
elif [ "${{ needs.branch_build_setup.outputs.gh_branch_name }}" == "master" ]; then
|
||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-space${{ secrets.DOCKER_REPO_SUFFIX || '' }}:stable
|
||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-space:stable
|
||||
else
|
||||
TAG=${{ env.SPACE_TAG }}
|
||||
fi
|
||||
echo "SPACE_TAG=${TAG}" >> $GITHUB_ENV
|
||||
|
||||
- name: Docker Setup QEMU
|
||||
uses: docker/setup-qemu-action@v3.0.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2.5.0
|
||||
uses: docker/setup-buildx-action@v3.0.0
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
buildkitd-flags: "--allow-insecure-entitlement security.insecure"
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.1.0
|
||||
uses: docker/login-action@v3.0.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Downloading Space Source Code
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: space-src-code
|
||||
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v4.1.1
|
||||
|
||||
- name: Build and Push Space to Docker Hub
|
||||
uses: docker/build-push-action@v4.0.0
|
||||
uses: docker/build-push-action@v5.1.0
|
||||
with:
|
||||
context: .
|
||||
file: ./space/Dockerfile.space
|
||||
@@ -147,36 +127,42 @@ jobs:
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [branch_build_setup]
|
||||
env:
|
||||
BACKEND_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-backend${{ secrets.DOCKER_REPO_SUFFIX || '' }}:${{ needs.branch_build_setup.outputs.gh_branch_name }}
|
||||
BACKEND_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-backend:${{ needs.branch_build_setup.outputs.gh_branch_name }}
|
||||
steps:
|
||||
- name: Set Backend Docker Tag
|
||||
- name: Set Backend Docker Tag
|
||||
run: |
|
||||
if [ "${{ needs.branch_build_setup.outputs.gh_branch_name }}" == "master" ] && [ "${{ github.event_name }}" == "release" ]; then
|
||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-backend${{ secrets.DOCKER_REPO_SUFFIX || '' }}:latest,${{ secrets.DOCKERHUB_USERNAME }}/plane-backend${{ secrets.DOCKER_REPO_SUFFIX || '' }}:${{ github.event.release.tag_name }}
|
||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-backend:latest,${{ secrets.DOCKERHUB_USERNAME }}/plane-backend:${{ github.event.release.tag_name }}
|
||||
elif [ "${{ needs.branch_build_setup.outputs.gh_branch_name }}" == "master" ]; then
|
||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-backend${{ secrets.DOCKER_REPO_SUFFIX || '' }}:stable
|
||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-backend:stable
|
||||
else
|
||||
TAG=${{ env.BACKEND_TAG }}
|
||||
fi
|
||||
echo "BACKEND_TAG=${TAG}" >> $GITHUB_ENV
|
||||
|
||||
- name: Docker Setup QEMU
|
||||
uses: docker/setup-qemu-action@v3.0.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2.5.0
|
||||
uses: docker/setup-buildx-action@v3.0.0
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
buildkitd-flags: "--allow-insecure-entitlement security.insecure"
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.1.0
|
||||
uses: docker/login-action@v3.0.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Downloading Backend Source Code
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: backend-src-code
|
||||
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v4.1.1
|
||||
|
||||
- name: Build and Push Backend to Docker Hub
|
||||
uses: docker/build-push-action@v4.0.0
|
||||
uses: docker/build-push-action@v5.1.0
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile.api
|
||||
context: ./apiserver
|
||||
file: ./apiserver/Dockerfile.api
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
tags: ${{ env.BACKEND_TAG }}
|
||||
@@ -189,37 +175,42 @@ jobs:
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [branch_build_setup]
|
||||
env:
|
||||
PROXY_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy${{ secrets.DOCKER_REPO_SUFFIX || '' }}:${{ needs.branch_build_setup.outputs.gh_branch_name }}
|
||||
PROXY_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy:${{ needs.branch_build_setup.outputs.gh_branch_name }}
|
||||
steps:
|
||||
- name: Set Proxy Docker Tag
|
||||
- name: Set Proxy Docker Tag
|
||||
run: |
|
||||
if [ "${{ needs.branch_build_setup.outputs.gh_branch_name }}" == "master" ] && [ "${{ github.event_name }}" == "release" ]; then
|
||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy${{ secrets.DOCKER_REPO_SUFFIX || '' }}:latest,${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy${{ secrets.DOCKER_REPO_SUFFIX || '' }}:${{ github.event.release.tag_name }}
|
||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy:latest,${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy:${{ github.event.release.tag_name }}
|
||||
elif [ "${{ needs.branch_build_setup.outputs.gh_branch_name }}" == "master" ]; then
|
||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy${{ secrets.DOCKER_REPO_SUFFIX || '' }}:stable
|
||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy:stable
|
||||
else
|
||||
TAG=${{ env.PROXY_TAG }}
|
||||
fi
|
||||
echo "PROXY_TAG=${TAG}" >> $GITHUB_ENV
|
||||
|
||||
- name: Docker Setup QEMU
|
||||
uses: docker/setup-qemu-action@v3.0.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2.5.0
|
||||
uses: docker/setup-buildx-action@v3.0.0
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
buildkitd-flags: "--allow-insecure-entitlement security.insecure"
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.1.0
|
||||
uses: docker/login-action@v3.0.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Downloading Proxy Source Code
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: proxy-src-code
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v4.1.1
|
||||
|
||||
- name: Build and Push Plane-Proxy to Docker Hub
|
||||
uses: docker/build-push-action@v4.0.0
|
||||
uses: docker/build-push-action@v5.1.0
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
context: ./nginx
|
||||
file: ./nginx/Dockerfile
|
||||
platforms: linux/amd64
|
||||
tags: ${{ env.PROXY_TAG }}
|
||||
push: true
|
||||
|
||||
@@ -1,25 +1,23 @@
|
||||
name: Create Sync Action
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- preview
|
||||
types:
|
||||
- closed
|
||||
|
||||
env:
|
||||
SOURCE_BRANCH_NAME: ${{github.event.pull_request.base.ref}}
|
||||
SOURCE_BRANCH_NAME: ${{ github.ref_name }}
|
||||
|
||||
jobs:
|
||||
sync_changes:
|
||||
# Only run the job when a PR is merged
|
||||
if: github.event.pull_request.merged == true
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4.1.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -33,15 +33,10 @@ RUN pip install -r requirements/local.txt --compile --no-cache-dir
|
||||
RUN addgroup -S plane && \
|
||||
adduser -S captain -G plane
|
||||
|
||||
RUN chown captain.plane /code
|
||||
COPY . .
|
||||
|
||||
USER captain
|
||||
|
||||
# Add in Django deps and generate Django's static files
|
||||
|
||||
USER root
|
||||
|
||||
# RUN chmod +x ./bin/takeoff ./bin/worker ./bin/beat
|
||||
RUN chown -R captain.plane /code
|
||||
RUN chmod -R +x /code/bin
|
||||
RUN chmod -R 777 /code
|
||||
|
||||
USER captain
|
||||
|
||||
Regular → Executable
+3
@@ -2,4 +2,7 @@
|
||||
set -e
|
||||
|
||||
python manage.py wait_for_db
|
||||
# Wait for migrations
|
||||
python manage.py wait_for_migrations
|
||||
# Run the processes
|
||||
celery -A plane beat -l info
|
||||
@@ -1,7 +1,8 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
python manage.py wait_for_db
|
||||
python manage.py migrate
|
||||
# Wait for migrations
|
||||
python manage.py wait_for_migrations
|
||||
|
||||
# Create the default bucket
|
||||
#!/bin/bash
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
python manage.py wait_for_db
|
||||
python manage.py migrate
|
||||
# Wait for migrations
|
||||
python manage.py wait_for_migrations
|
||||
|
||||
# Create the default bucket
|
||||
#!/bin/bash
|
||||
|
||||
@@ -2,4 +2,7 @@
|
||||
set -e
|
||||
|
||||
python manage.py wait_for_db
|
||||
# Wait for migrations
|
||||
python manage.py wait_for_migrations
|
||||
# Run the processes
|
||||
celery -A plane worker -l info
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"name": "plane-api",
|
||||
"version": "0.14.0"
|
||||
"version": "0.15.0"
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ from .user import (
|
||||
UserAdminLiteSerializer,
|
||||
UserMeSerializer,
|
||||
UserMeSettingsSerializer,
|
||||
ProfileSerializer,
|
||||
AccountSerializer,
|
||||
)
|
||||
from .workspace import (
|
||||
WorkSpaceSerializer,
|
||||
@@ -104,17 +106,22 @@ from .estimate import (
|
||||
EstimateSerializer,
|
||||
EstimatePointSerializer,
|
||||
EstimateReadSerializer,
|
||||
WorkspaceEstimateSerializer,
|
||||
)
|
||||
|
||||
from .inbox import (
|
||||
InboxSerializer,
|
||||
InboxIssueSerializer,
|
||||
IssueStateInboxSerializer,
|
||||
InboxIssueLiteSerializer,
|
||||
)
|
||||
|
||||
from .analytic import AnalyticViewSerializer
|
||||
|
||||
from .notification import NotificationSerializer
|
||||
from .notification import (
|
||||
NotificationSerializer,
|
||||
UserNotificationPreferenceSerializer,
|
||||
)
|
||||
|
||||
from .exporter import ExporterHistorySerializer
|
||||
|
||||
|
||||
@@ -60,6 +60,7 @@ class DynamicBaseSerializer(BaseSerializer):
|
||||
CycleIssueSerializer,
|
||||
IssueFlatSerializer,
|
||||
IssueRelationSerializer,
|
||||
InboxIssueLiteSerializer
|
||||
)
|
||||
|
||||
# Expansion mapper
|
||||
@@ -80,9 +81,10 @@ class DynamicBaseSerializer(BaseSerializer):
|
||||
"issue_cycle": CycleIssueSerializer,
|
||||
"parent": IssueSerializer,
|
||||
"issue_relation": IssueRelationSerializer,
|
||||
"issue_inbox" : InboxIssueLiteSerializer,
|
||||
}
|
||||
|
||||
self.fields[field] = expansion[field](many=True if field in ["members", "assignees", "labels", "issue_cycle", "issue_relation"] else False)
|
||||
self.fields[field] = expansion[field](many=True if field in ["members", "assignees", "labels", "issue_cycle", "issue_relation", "issue_inbox"] else False)
|
||||
|
||||
return self.fields
|
||||
|
||||
@@ -103,6 +105,7 @@ class DynamicBaseSerializer(BaseSerializer):
|
||||
LabelSerializer,
|
||||
CycleIssueSerializer,
|
||||
IssueRelationSerializer,
|
||||
InboxIssueLiteSerializer
|
||||
)
|
||||
|
||||
# Expansion mapper
|
||||
@@ -122,7 +125,8 @@ class DynamicBaseSerializer(BaseSerializer):
|
||||
"labels": LabelSerializer,
|
||||
"issue_cycle": CycleIssueSerializer,
|
||||
"parent": IssueSerializer,
|
||||
"issue_relation": IssueRelationSerializer
|
||||
"issue_relation": IssueRelationSerializer,
|
||||
"issue_inbox" : InboxIssueLiteSerializer,
|
||||
}
|
||||
# Check if field in expansion then expand the field
|
||||
if expand in expansion:
|
||||
|
||||
@@ -61,3 +61,18 @@ class EstimateReadSerializer(BaseSerializer):
|
||||
"name",
|
||||
"description",
|
||||
]
|
||||
|
||||
|
||||
class WorkspaceEstimateSerializer(BaseSerializer):
|
||||
points = EstimatePointSerializer(read_only=True, many=True)
|
||||
|
||||
class Meta:
|
||||
model = Estimate
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"points",
|
||||
"name",
|
||||
"description",
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -259,14 +259,17 @@ class IssuePropertySerializer(BaseSerializer):
|
||||
|
||||
|
||||
class LabelSerializer(BaseSerializer):
|
||||
workspace_detail = WorkspaceLiteSerializer(
|
||||
source="workspace", read_only=True
|
||||
)
|
||||
project_detail = ProjectLiteSerializer(source="project", read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Label
|
||||
fields = "__all__"
|
||||
fields = [
|
||||
"parent",
|
||||
"name",
|
||||
"color",
|
||||
"id",
|
||||
"project_id",
|
||||
"workspace_id",
|
||||
"sort_order",
|
||||
]
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
"project",
|
||||
@@ -295,8 +298,13 @@ class IssueLabelSerializer(BaseSerializer):
|
||||
|
||||
class IssueRelationSerializer(BaseSerializer):
|
||||
id = serializers.UUIDField(source="related_issue.id", read_only=True)
|
||||
project_id = serializers.PrimaryKeyRelatedField(source="related_issue.project_id", read_only=True)
|
||||
sequence_id = serializers.IntegerField(source="related_issue.sequence_id", read_only=True)
|
||||
project_id = serializers.PrimaryKeyRelatedField(
|
||||
source="related_issue.project_id", read_only=True
|
||||
)
|
||||
sequence_id = serializers.IntegerField(
|
||||
source="related_issue.sequence_id", read_only=True
|
||||
)
|
||||
name = serializers.CharField(source="related_issue.name", read_only=True)
|
||||
relation_type = serializers.CharField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
@@ -306,6 +314,7 @@ class IssueRelationSerializer(BaseSerializer):
|
||||
"project_id",
|
||||
"sequence_id",
|
||||
"relation_type",
|
||||
"name",
|
||||
]
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
@@ -315,8 +324,13 @@ class IssueRelationSerializer(BaseSerializer):
|
||||
|
||||
class RelatedIssueSerializer(BaseSerializer):
|
||||
id = serializers.UUIDField(source="issue.id", read_only=True)
|
||||
project_id = serializers.PrimaryKeyRelatedField(source="issue.project_id", read_only=True)
|
||||
sequence_id = serializers.IntegerField(source="issue.sequence_id", read_only=True)
|
||||
project_id = serializers.PrimaryKeyRelatedField(
|
||||
source="issue.project_id", read_only=True
|
||||
)
|
||||
sequence_id = serializers.IntegerField(
|
||||
source="issue.sequence_id", read_only=True
|
||||
)
|
||||
name = serializers.CharField(source="issue.name", read_only=True)
|
||||
relation_type = serializers.CharField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
@@ -326,6 +340,7 @@ class RelatedIssueSerializer(BaseSerializer):
|
||||
"project_id",
|
||||
"sequence_id",
|
||||
"relation_type",
|
||||
"name",
|
||||
]
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
@@ -458,19 +473,6 @@ class IssueReactionSerializer(BaseSerializer):
|
||||
]
|
||||
|
||||
|
||||
class CommentReactionLiteSerializer(BaseSerializer):
|
||||
actor_detail = UserLiteSerializer(read_only=True, source="actor")
|
||||
|
||||
class Meta:
|
||||
model = CommentReaction
|
||||
fields = [
|
||||
"id",
|
||||
"reaction",
|
||||
"comment",
|
||||
"actor_detail",
|
||||
]
|
||||
|
||||
|
||||
class CommentReactionSerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = CommentReaction
|
||||
@@ -501,7 +503,7 @@ class IssueCommentSerializer(BaseSerializer):
|
||||
workspace_detail = WorkspaceLiteSerializer(
|
||||
read_only=True, source="workspace"
|
||||
)
|
||||
comment_reactions = CommentReactionLiteSerializer(
|
||||
comment_reactions = CommentReactionSerializer(
|
||||
read_only=True, many=True
|
||||
)
|
||||
is_member = serializers.BooleanField(read_only=True)
|
||||
@@ -560,7 +562,7 @@ class IssueSerializer(DynamicBaseSerializer):
|
||||
state_id = serializers.PrimaryKeyRelatedField(read_only=True)
|
||||
parent_id = serializers.PrimaryKeyRelatedField(read_only=True)
|
||||
cycle_id = serializers.PrimaryKeyRelatedField(read_only=True)
|
||||
module_id = serializers.PrimaryKeyRelatedField(read_only=True)
|
||||
module_ids = serializers.SerializerMethodField()
|
||||
|
||||
# Many to many
|
||||
label_ids = serializers.PrimaryKeyRelatedField(
|
||||
@@ -595,7 +597,7 @@ class IssueSerializer(DynamicBaseSerializer):
|
||||
"project_id",
|
||||
"parent_id",
|
||||
"cycle_id",
|
||||
"module_id",
|
||||
"module_ids",
|
||||
"label_ids",
|
||||
"assignee_ids",
|
||||
"sub_issues_count",
|
||||
@@ -611,6 +613,10 @@ class IssueSerializer(DynamicBaseSerializer):
|
||||
]
|
||||
read_only_fields = fields
|
||||
|
||||
def get_module_ids(self, obj):
|
||||
# Access the prefetched modules and extract module IDs
|
||||
return [module for module in obj.issue_module.values_list("module_id", flat=True)]
|
||||
|
||||
|
||||
class IssueLiteSerializer(DynamicBaseSerializer):
|
||||
workspace_detail = WorkspaceLiteSerializer(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Module imports
|
||||
from .base import BaseSerializer
|
||||
from .user import UserLiteSerializer
|
||||
from plane.db.models import Notification
|
||||
from plane.db.models import Notification, UserNotificationPreference
|
||||
|
||||
|
||||
class NotificationSerializer(BaseSerializer):
|
||||
@@ -12,3 +12,10 @@ class NotificationSerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = Notification
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class UserNotificationPreferenceSerializer(BaseSerializer):
|
||||
|
||||
class Meta:
|
||||
model = UserNotificationPreference
|
||||
fields = "__all__"
|
||||
|
||||
@@ -8,7 +8,17 @@ from plane.db.models import State
|
||||
class StateSerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = State
|
||||
fields = "__all__"
|
||||
fields = [
|
||||
"id",
|
||||
"project_id",
|
||||
"workspace_id",
|
||||
"name",
|
||||
"color",
|
||||
"group",
|
||||
"default",
|
||||
"description",
|
||||
"sequence",
|
||||
]
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
"project",
|
||||
|
||||
@@ -3,7 +3,7 @@ from rest_framework import serializers
|
||||
|
||||
# Module import
|
||||
from .base import BaseSerializer
|
||||
from plane.db.models import User, Workspace, WorkspaceMemberInvite
|
||||
from plane.db.models import User, Workspace, WorkspaceMemberInvite, Profile, Account
|
||||
from plane.license.models import InstanceAdmin, Instance
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ class UserSerializer(BaseSerializer):
|
||||
"last_logout_ip",
|
||||
"last_login_uagent",
|
||||
"token_updated_at",
|
||||
"is_onboarded",
|
||||
"is_bot",
|
||||
"is_password_autoset",
|
||||
"is_email_verified",
|
||||
@@ -52,16 +51,9 @@ class UserMeSerializer(BaseSerializer):
|
||||
"is_bot",
|
||||
"is_email_verified",
|
||||
"is_managed",
|
||||
"is_onboarded",
|
||||
"is_tour_completed",
|
||||
"mobile_number",
|
||||
"role",
|
||||
"onboarding_step",
|
||||
"user_timezone",
|
||||
"username",
|
||||
"theme",
|
||||
"last_workspace_id",
|
||||
"use_case",
|
||||
"is_password_autoset",
|
||||
"is_email_verified",
|
||||
]
|
||||
@@ -84,25 +76,28 @@ class UserMeSettingsSerializer(BaseSerializer):
|
||||
workspace_invites = WorkspaceMemberInvite.objects.filter(
|
||||
email=obj.email
|
||||
).count()
|
||||
|
||||
# profile
|
||||
profile = Profile.objects.get(user=obj)
|
||||
if (
|
||||
obj.last_workspace_id is not None
|
||||
profile.last_workspace_id is not None
|
||||
and Workspace.objects.filter(
|
||||
pk=obj.last_workspace_id,
|
||||
pk=profile.last_workspace_id,
|
||||
workspace_member__member=obj.id,
|
||||
workspace_member__is_active=True,
|
||||
).exists()
|
||||
):
|
||||
workspace = Workspace.objects.filter(
|
||||
pk=obj.last_workspace_id,
|
||||
pk=profile.last_workspace_id,
|
||||
workspace_member__member=obj.id,
|
||||
workspace_member__is_active=True,
|
||||
).first()
|
||||
return {
|
||||
"last_workspace_id": obj.last_workspace_id,
|
||||
"last_workspace_id": profile.last_workspace_id,
|
||||
"last_workspace_slug": workspace.slug
|
||||
if workspace is not None
|
||||
else "",
|
||||
"fallback_workspace_id": obj.last_workspace_id,
|
||||
"fallback_workspace_id": profile.last_workspace_id,
|
||||
"fallback_workspace_slug": workspace.slug
|
||||
if workspace is not None
|
||||
else "",
|
||||
@@ -197,3 +192,16 @@ class ResetPasswordSerializer(serializers.Serializer):
|
||||
"""
|
||||
|
||||
new_password = serializers.CharField(required=True, min_length=8)
|
||||
|
||||
class ProfileSerializer(BaseSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Profile
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class AccountSerializer(BaseSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Account
|
||||
fields = "__all__"
|
||||
@@ -8,16 +8,10 @@ from plane.app.views import (
|
||||
CycleFavoriteViewSet,
|
||||
TransferCycleIssueEndpoint,
|
||||
CycleUserPropertiesEndpoint,
|
||||
ActiveCycleEndpoint
|
||||
)
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
"workspaces/<str:slug>/active-cycles/",
|
||||
ActiveCycleEndpoint.as_view(),
|
||||
name="workspace-active-cycle",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/",
|
||||
CycleViewSet.as_view(
|
||||
|
||||
@@ -35,17 +35,26 @@ urlpatterns = [
|
||||
name="project-modules",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/module-issues/",
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/modules/",
|
||||
ModuleIssueViewSet.as_view(
|
||||
{
|
||||
"post": "create_issue_modules",
|
||||
}
|
||||
),
|
||||
name="issue-module",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/issues/",
|
||||
ModuleIssueViewSet.as_view(
|
||||
{
|
||||
"post": "create_module_issues",
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="project-module-issues",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/module-issues/<uuid:issue_id>/",
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/issues/<uuid:issue_id>/",
|
||||
ModuleIssueViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
|
||||
@@ -5,6 +5,7 @@ from plane.app.views import (
|
||||
NotificationViewSet,
|
||||
UnreadNotificationEndpoint,
|
||||
MarkAllReadNotificationViewSet,
|
||||
UserNotificationPreferenceEndpoint,
|
||||
)
|
||||
|
||||
|
||||
@@ -63,4 +64,9 @@ urlpatterns = [
|
||||
),
|
||||
name="mark-all-read-notifications",
|
||||
),
|
||||
path(
|
||||
"users/me/notification-preferences/",
|
||||
UserNotificationPreferenceEndpoint.as_view(),
|
||||
name="user-notification-preferences",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -3,6 +3,8 @@ from django.urls import path
|
||||
from plane.app.views import (
|
||||
## User
|
||||
UserEndpoint,
|
||||
ProfileEndpoint,
|
||||
AccountEndpoint,
|
||||
UpdateUserOnBoardedEndpoint,
|
||||
UpdateUserTourCompletedEndpoint,
|
||||
UserActivityEndpoint,
|
||||
@@ -39,6 +41,16 @@ urlpatterns = [
|
||||
),
|
||||
name="users",
|
||||
),
|
||||
path(
|
||||
"users/me/profile/",
|
||||
ProfileEndpoint.as_view(),
|
||||
name="accounts",
|
||||
),
|
||||
path(
|
||||
"users/me/accounts/",
|
||||
AccountEndpoint.as_view(),
|
||||
name="accounts",
|
||||
),
|
||||
path(
|
||||
"users/me/instance-admin/",
|
||||
UserEndpoint.as_view(
|
||||
@@ -96,4 +108,4 @@ urlpatterns = [
|
||||
name="set-password",
|
||||
),
|
||||
## End User Graph
|
||||
]
|
||||
]
|
||||
@@ -20,6 +20,8 @@ from plane.app.views import (
|
||||
WorkspaceLabelsEndpoint,
|
||||
WorkspaceProjectMemberEndpoint,
|
||||
WorkspaceUserPropertiesEndpoint,
|
||||
WorkspaceStatesEndpoint,
|
||||
WorkspaceEstimatesEndpoint,
|
||||
)
|
||||
|
||||
|
||||
@@ -207,4 +209,14 @@ urlpatterns = [
|
||||
WorkspaceUserPropertiesEndpoint.as_view(),
|
||||
name="workspace-user-filters",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/states/",
|
||||
WorkspaceStatesEndpoint.as_view(),
|
||||
name="workspace-state",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/estimates/",
|
||||
WorkspaceEstimatesEndpoint.as_view(),
|
||||
name="workspace-estimate",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -18,6 +18,8 @@ from .user import (
|
||||
UpdateUserOnBoardedEndpoint,
|
||||
UpdateUserTourCompletedEndpoint,
|
||||
UserActivityEndpoint,
|
||||
ProfileEndpoint,
|
||||
AccountEndpoint,
|
||||
)
|
||||
|
||||
from .oauth import OauthEndpoint
|
||||
@@ -47,6 +49,8 @@ from .workspace import (
|
||||
WorkspaceLabelsEndpoint,
|
||||
WorkspaceProjectMemberEndpoint,
|
||||
WorkspaceUserPropertiesEndpoint,
|
||||
WorkspaceStatesEndpoint,
|
||||
WorkspaceEstimatesEndpoint,
|
||||
)
|
||||
from .state import StateViewSet
|
||||
from .view import (
|
||||
@@ -62,7 +66,6 @@ from .cycle import (
|
||||
CycleFavoriteViewSet,
|
||||
TransferCycleIssueEndpoint,
|
||||
CycleUserPropertiesEndpoint,
|
||||
ActiveCycleEndpoint,
|
||||
)
|
||||
from .asset import FileAssetEndpoint, UserAssetsEndpoint, FileAssetViewSet
|
||||
from .issue import (
|
||||
@@ -166,6 +169,7 @@ from .notification import (
|
||||
NotificationViewSet,
|
||||
UnreadNotificationEndpoint,
|
||||
MarkAllReadNotificationViewSet,
|
||||
UserNotificationPreferenceEndpoint,
|
||||
)
|
||||
|
||||
from .exporter import ExportIssuesEndpoint
|
||||
@@ -178,7 +182,4 @@ from .webhook import (
|
||||
WebhookSecretRegenerateEndpoint,
|
||||
)
|
||||
|
||||
from .dashboard import (
|
||||
DashboardEndpoint,
|
||||
WidgetsEndpoint
|
||||
)
|
||||
from .dashboard import DashboardEndpoint, WidgetsEndpoint
|
||||
|
||||
@@ -39,7 +39,6 @@ from plane.app.serializers import (
|
||||
from plane.app.permissions import (
|
||||
ProjectEntityPermission,
|
||||
ProjectLitePermission,
|
||||
WorkspaceUserPermission
|
||||
)
|
||||
from plane.db.models import (
|
||||
User,
|
||||
@@ -531,6 +530,8 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
|
||||
project_id=str(project_id),
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
# Delete the cycle
|
||||
cycle.delete()
|
||||
@@ -598,16 +599,11 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
|
||||
)
|
||||
.filter(project_id=project_id)
|
||||
.filter(workspace__slug=slug)
|
||||
.select_related("project")
|
||||
.select_related("workspace")
|
||||
.select_related("state")
|
||||
.select_related("parent")
|
||||
.prefetch_related("assignees")
|
||||
.prefetch_related("labels")
|
||||
.select_related("workspace", "project", "state", "parent")
|
||||
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||
.order_by(order_by)
|
||||
.filter(**filters)
|
||||
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||
.annotate(module_id=F("issue_module__module_id"))
|
||||
.annotate(
|
||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||
.order_by()
|
||||
@@ -722,6 +718,8 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
|
||||
}
|
||||
),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
|
||||
# Return all Cycle Issues
|
||||
@@ -754,6 +752,8 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
|
||||
project_id=str(self.kwargs.get("project_id", None)),
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
cycle_issue.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
@@ -910,235 +910,3 @@ class CycleUserPropertiesEndpoint(BaseAPIView):
|
||||
)
|
||||
serializer = CycleUserPropertiesSerializer(cycle_properties)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class ActiveCycleEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
WorkspaceUserPermission,
|
||||
]
|
||||
def get(self, request, slug):
|
||||
subquery = CycleFavorite.objects.filter(
|
||||
user=self.request.user,
|
||||
cycle_id=OuterRef("pk"),
|
||||
project_id=self.kwargs.get("project_id"),
|
||||
workspace__slug=self.kwargs.get("slug"),
|
||||
)
|
||||
active_cycles = (
|
||||
Cycle.objects.filter(
|
||||
workspace__slug=slug,
|
||||
project__project_projectmember__member=self.request.user,
|
||||
start_date__lte=timezone.now(),
|
||||
end_date__gte=timezone.now(),
|
||||
)
|
||||
.select_related("project")
|
||||
.select_related("workspace")
|
||||
.select_related("owned_by")
|
||||
.annotate(is_favorite=Exists(subquery))
|
||||
.annotate(
|
||||
total_issues=Count(
|
||||
"issue_cycle",
|
||||
filter=Q(
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
completed_issues=Count(
|
||||
"issue_cycle__issue__state__group",
|
||||
filter=Q(
|
||||
issue_cycle__issue__state__group="completed",
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
cancelled_issues=Count(
|
||||
"issue_cycle__issue__state__group",
|
||||
filter=Q(
|
||||
issue_cycle__issue__state__group="cancelled",
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
started_issues=Count(
|
||||
"issue_cycle__issue__state__group",
|
||||
filter=Q(
|
||||
issue_cycle__issue__state__group="started",
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
unstarted_issues=Count(
|
||||
"issue_cycle__issue__state__group",
|
||||
filter=Q(
|
||||
issue_cycle__issue__state__group="unstarted",
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
backlog_issues=Count(
|
||||
"issue_cycle__issue__state__group",
|
||||
filter=Q(
|
||||
issue_cycle__issue__state__group="backlog",
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
),
|
||||
)
|
||||
)
|
||||
.annotate(total_estimates=Sum("issue_cycle__issue__estimate_point"))
|
||||
.annotate(
|
||||
completed_estimates=Sum(
|
||||
"issue_cycle__issue__estimate_point",
|
||||
filter=Q(
|
||||
issue_cycle__issue__state__group="completed",
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
started_estimates=Sum(
|
||||
"issue_cycle__issue__estimate_point",
|
||||
filter=Q(
|
||||
issue_cycle__issue__state__group="started",
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
status=Case(
|
||||
When(
|
||||
Q(start_date__lte=timezone.now())
|
||||
& Q(end_date__gte=timezone.now()),
|
||||
then=Value("CURRENT"),
|
||||
),
|
||||
When(start_date__gt=timezone.now(), then=Value("UPCOMING")),
|
||||
When(end_date__lt=timezone.now(), then=Value("COMPLETED")),
|
||||
When(
|
||||
Q(start_date__isnull=True) & Q(end_date__isnull=True),
|
||||
then=Value("DRAFT"),
|
||||
),
|
||||
default=Value("DRAFT"),
|
||||
output_field=CharField(),
|
||||
)
|
||||
)
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"issue_cycle__issue__assignees",
|
||||
queryset=User.objects.only("avatar", "first_name", "id").distinct(),
|
||||
)
|
||||
)
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"issue_cycle__issue__labels",
|
||||
queryset=Label.objects.only("name", "color", "id").distinct(),
|
||||
)
|
||||
)
|
||||
.order_by("-created_at")
|
||||
)
|
||||
|
||||
cycles = CycleSerializer(active_cycles, many=True).data
|
||||
|
||||
for cycle in cycles:
|
||||
assignee_distribution = (
|
||||
Issue.objects.filter(
|
||||
issue_cycle__cycle_id=cycle["id"],
|
||||
project_id=cycle["project"],
|
||||
workspace__slug=slug,
|
||||
)
|
||||
.annotate(display_name=F("assignees__display_name"))
|
||||
.annotate(assignee_id=F("assignees__id"))
|
||||
.annotate(avatar=F("assignees__avatar"))
|
||||
.values("display_name", "assignee_id", "avatar")
|
||||
.annotate(
|
||||
total_issues=Count(
|
||||
"assignee_id",
|
||||
filter=Q(archived_at__isnull=True, is_draft=False),
|
||||
),
|
||||
)
|
||||
.annotate(
|
||||
completed_issues=Count(
|
||||
"assignee_id",
|
||||
filter=Q(
|
||||
completed_at__isnull=False,
|
||||
archived_at__isnull=True,
|
||||
is_draft=False,
|
||||
),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
pending_issues=Count(
|
||||
"assignee_id",
|
||||
filter=Q(
|
||||
completed_at__isnull=True,
|
||||
archived_at__isnull=True,
|
||||
is_draft=False,
|
||||
),
|
||||
)
|
||||
)
|
||||
.order_by("display_name")
|
||||
)
|
||||
|
||||
label_distribution = (
|
||||
Issue.objects.filter(
|
||||
issue_cycle__cycle_id=cycle["id"],
|
||||
project_id=cycle["project"],
|
||||
workspace__slug=slug,
|
||||
)
|
||||
.annotate(label_name=F("labels__name"))
|
||||
.annotate(color=F("labels__color"))
|
||||
.annotate(label_id=F("labels__id"))
|
||||
.values("label_name", "color", "label_id")
|
||||
.annotate(
|
||||
total_issues=Count(
|
||||
"label_id",
|
||||
filter=Q(archived_at__isnull=True, is_draft=False),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
completed_issues=Count(
|
||||
"label_id",
|
||||
filter=Q(
|
||||
completed_at__isnull=False,
|
||||
archived_at__isnull=True,
|
||||
is_draft=False,
|
||||
),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
pending_issues=Count(
|
||||
"label_id",
|
||||
filter=Q(
|
||||
completed_at__isnull=True,
|
||||
archived_at__isnull=True,
|
||||
is_draft=False,
|
||||
),
|
||||
)
|
||||
)
|
||||
.order_by("label_name")
|
||||
)
|
||||
cycle["distribution"] = {
|
||||
"assignees": assignee_distribution,
|
||||
"labels": label_distribution,
|
||||
"completion_chart": {},
|
||||
}
|
||||
if cycle["start_date"] and cycle["end_date"]:
|
||||
cycle["distribution"][
|
||||
"completion_chart"
|
||||
] = burndown_plot(
|
||||
queryset=active_cycles.get(pk=cycle["id"]),
|
||||
slug=slug,
|
||||
project_id=cycle["project"],
|
||||
cycle_id=cycle["id"],
|
||||
)
|
||||
|
||||
return Response(cycles, status=status.HTTP_200_OK)
|
||||
|
||||
@@ -100,7 +100,7 @@ def dashboard_assigned_issues(self, request, slug):
|
||||
)
|
||||
.filter(**filters)
|
||||
.select_related("workspace", "project", "state", "parent")
|
||||
.prefetch_related("assignees", "labels")
|
||||
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"issue_relation",
|
||||
@@ -110,7 +110,6 @@ def dashboard_assigned_issues(self, request, slug):
|
||||
)
|
||||
)
|
||||
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||
.annotate(module_id=F("issue_module__module_id"))
|
||||
.annotate(
|
||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||
.order_by()
|
||||
@@ -221,9 +220,8 @@ def dashboard_created_issues(self, request, slug):
|
||||
)
|
||||
.filter(**filters)
|
||||
.select_related("workspace", "project", "state", "parent")
|
||||
.prefetch_related("assignees", "labels")
|
||||
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||
.annotate(module_id=F("issue_module__module_id"))
|
||||
.annotate(
|
||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||
.order_by()
|
||||
|
||||
@@ -88,39 +88,23 @@ class InboxIssueViewSet(BaseViewSet):
|
||||
]
|
||||
|
||||
def get_queryset(self):
|
||||
return self.filter_queryset(
|
||||
super()
|
||||
.get_queryset()
|
||||
.filter(
|
||||
Q(snoozed_till__gte=timezone.now())
|
||||
| Q(snoozed_till__isnull=True),
|
||||
workspace__slug=self.kwargs.get("slug"),
|
||||
project_id=self.kwargs.get("project_id"),
|
||||
inbox_id=self.kwargs.get("inbox_id"),
|
||||
)
|
||||
.select_related("issue", "workspace", "project")
|
||||
)
|
||||
|
||||
def list(self, request, slug, project_id, inbox_id):
|
||||
filters = issue_filters(request.query_params, "GET")
|
||||
issues = (
|
||||
return (
|
||||
Issue.objects.filter(
|
||||
issue_inbox__inbox_id=inbox_id,
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
project_id=self.kwargs.get("project_id"),
|
||||
workspace__slug=self.kwargs.get("slug"),
|
||||
issue_inbox__inbox_id=self.kwargs.get("inbox_id")
|
||||
)
|
||||
.filter(**filters)
|
||||
.select_related("workspace", "project", "state", "parent")
|
||||
.prefetch_related("assignees", "labels")
|
||||
.order_by("issue_inbox__snoozed_till", "issue_inbox__status")
|
||||
.annotate(
|
||||
sub_issues_count=Issue.issue_objects.filter(
|
||||
parent=OuterRef("id")
|
||||
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"issue_inbox",
|
||||
queryset=InboxIssue.objects.only(
|
||||
"status", "duplicate_to", "snoozed_till", "source"
|
||||
),
|
||||
)
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||
.annotate(
|
||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||
.order_by()
|
||||
@@ -135,16 +119,20 @@ class InboxIssueViewSet(BaseViewSet):
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"issue_inbox",
|
||||
queryset=InboxIssue.objects.only(
|
||||
"status", "duplicate_to", "snoozed_till", "source"
|
||||
),
|
||||
.annotate(
|
||||
sub_issues_count=Issue.issue_objects.filter(
|
||||
parent=OuterRef("id")
|
||||
)
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
)
|
||||
issues_data = IssueStateInboxSerializer(issues, many=True).data
|
||||
).distinct()
|
||||
|
||||
def list(self, request, slug, project_id, inbox_id):
|
||||
filters = issue_filters(request.query_params, "GET")
|
||||
issue_queryset = self.get_queryset().filter(**filters).order_by("issue_inbox__snoozed_till", "issue_inbox__status")
|
||||
issues_data = IssueSerializer(issue_queryset, expand=self.expand, many=True).data
|
||||
return Response(
|
||||
issues_data,
|
||||
status=status.HTTP_200_OK,
|
||||
@@ -200,6 +188,8 @@ class InboxIssueViewSet(BaseViewSet):
|
||||
project_id=str(project_id),
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
# create an inbox issue
|
||||
InboxIssue.objects.create(
|
||||
@@ -209,7 +199,8 @@ class InboxIssueViewSet(BaseViewSet):
|
||||
source=request.data.get("source", "in-app"),
|
||||
)
|
||||
|
||||
serializer = IssueStateInboxSerializer(issue)
|
||||
issue = (self.get_queryset().filter(pk=issue.id).first())
|
||||
serializer = IssueSerializer(issue ,expand=self.expand)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
def partial_update(self, request, slug, project_id, inbox_id, issue_id):
|
||||
@@ -277,6 +268,8 @@ class InboxIssueViewSet(BaseViewSet):
|
||||
cls=DjangoJSONEncoder,
|
||||
),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
issue_serializer.save()
|
||||
else:
|
||||
@@ -327,22 +320,20 @@ class InboxIssueViewSet(BaseViewSet):
|
||||
if state is not None:
|
||||
issue.state = state
|
||||
issue.save()
|
||||
|
||||
issue = (self.get_queryset().filter(pk=issue_id).first())
|
||||
serializer = IssueSerializer(issue, expand=self.expand)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(
|
||||
serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
else:
|
||||
return Response(
|
||||
InboxIssueSerializer(inbox_issue).data,
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
issue = (self.get_queryset().filter(pk=issue_id).first())
|
||||
serializer = IssueSerializer(issue ,expand=self.expand)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
def retrieve(self, request, slug, project_id, inbox_id, issue_id):
|
||||
issue = Issue.objects.get(
|
||||
pk=issue_id, workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
serializer = IssueStateInboxSerializer(issue)
|
||||
issue = self.get_queryset().filter(pk=issue_id).first()
|
||||
serializer = IssueSerializer(issue, expand=self.expand,)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
def destroy(self, request, slug, project_id, inbox_id, issue_id):
|
||||
|
||||
@@ -48,10 +48,8 @@ from plane.app.serializers import (
|
||||
ProjectMemberLiteSerializer,
|
||||
IssueReactionSerializer,
|
||||
CommentReactionSerializer,
|
||||
IssueVoteSerializer,
|
||||
IssueRelationSerializer,
|
||||
RelatedIssueSerializer,
|
||||
IssuePublicSerializer,
|
||||
)
|
||||
from plane.app.permissions import (
|
||||
ProjectEntityPermission,
|
||||
@@ -83,6 +81,7 @@ from plane.utils.grouper import group_results
|
||||
from plane.utils.issue_filters import issue_filters
|
||||
from collections import defaultdict
|
||||
|
||||
|
||||
class IssueViewSet(WebhookMixin, BaseViewSet):
|
||||
def get_serializer_class(self):
|
||||
return (
|
||||
@@ -113,12 +112,8 @@ class IssueViewSet(WebhookMixin, BaseViewSet):
|
||||
project_id=self.kwargs.get("project_id")
|
||||
)
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.select_related("project")
|
||||
.select_related("workspace")
|
||||
.select_related("state")
|
||||
.select_related("parent")
|
||||
.prefetch_related("assignees")
|
||||
.prefetch_related("labels")
|
||||
.select_related("workspace", "project", "state", "parent")
|
||||
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"issue_reactions",
|
||||
@@ -126,7 +121,6 @@ class IssueViewSet(WebhookMixin, BaseViewSet):
|
||||
)
|
||||
)
|
||||
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||
.annotate(module_id=F("issue_module__module_id"))
|
||||
.annotate(
|
||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||
.order_by()
|
||||
@@ -260,6 +254,8 @@ class IssueViewSet(WebhookMixin, BaseViewSet):
|
||||
project_id=str(project_id),
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
issue = (
|
||||
self.get_queryset().filter(pk=serializer.data["id"]).first()
|
||||
@@ -298,6 +294,8 @@ class IssueViewSet(WebhookMixin, BaseViewSet):
|
||||
project_id=str(project_id),
|
||||
current_instance=current_instance,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
issue = self.get_queryset().filter(pk=pk).first()
|
||||
return Response(
|
||||
@@ -321,6 +319,8 @@ class IssueViewSet(WebhookMixin, BaseViewSet):
|
||||
project_id=str(project_id),
|
||||
current_instance=current_instance,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@@ -492,17 +492,27 @@ class IssueActivityEndpoint(BaseAPIView):
|
||||
|
||||
@method_decorator(gzip_page)
|
||||
def get(self, request, slug, project_id, issue_id):
|
||||
filters = {}
|
||||
if request.GET.get("created_at__gt", None) is not None:
|
||||
filters = {"created_at__gt": request.GET.get("created_at__gt")}
|
||||
|
||||
issue_activities = (
|
||||
IssueActivity.objects.filter(issue_id=issue_id)
|
||||
.filter(
|
||||
~Q(field__in=["comment", "vote", "reaction", "draft"]),
|
||||
project__project_projectmember__member=self.request.user,
|
||||
workspace__slug=slug,
|
||||
)
|
||||
.filter(**filters)
|
||||
.select_related("actor", "workspace", "issue", "project")
|
||||
).order_by("created_at")
|
||||
issue_comments = (
|
||||
IssueComment.objects.filter(issue_id=issue_id)
|
||||
.filter(project__project_projectmember__member=self.request.user)
|
||||
.filter(
|
||||
project__project_projectmember__member=self.request.user,
|
||||
workspace__slug=slug,
|
||||
)
|
||||
.filter(**filters)
|
||||
.order_by("created_at")
|
||||
.select_related("actor", "issue", "project", "workspace")
|
||||
.prefetch_related(
|
||||
@@ -517,6 +527,12 @@ class IssueActivityEndpoint(BaseAPIView):
|
||||
).data
|
||||
issue_comments = IssueCommentSerializer(issue_comments, many=True).data
|
||||
|
||||
if request.GET.get("activity_type", None) == "issue-property":
|
||||
return Response(issue_activities, status=status.HTTP_200_OK)
|
||||
|
||||
if request.GET.get("activity_type", None) == "issue-comment":
|
||||
return Response(issue_comments, status=status.HTTP_200_OK)
|
||||
|
||||
result_list = sorted(
|
||||
chain(issue_activities, issue_comments),
|
||||
key=lambda instance: instance["created_at"],
|
||||
@@ -580,6 +596,8 @@ class IssueCommentViewSet(WebhookMixin, BaseViewSet):
|
||||
project_id=str(self.kwargs.get("project_id")),
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
@@ -609,6 +627,8 @@ class IssueCommentViewSet(WebhookMixin, BaseViewSet):
|
||||
project_id=str(project_id),
|
||||
current_instance=current_instance,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
@@ -633,6 +653,8 @@ class IssueCommentViewSet(WebhookMixin, BaseViewSet):
|
||||
project_id=str(project_id),
|
||||
current_instance=current_instance,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@@ -821,7 +843,9 @@ class SubIssuesEndpoint(BaseAPIView):
|
||||
|
||||
_ = Issue.objects.bulk_update(sub_issues, ["parent"], batch_size=10)
|
||||
|
||||
updated_sub_issues = Issue.issue_objects.filter(id__in=sub_issue_ids).annotate(state_group=F("state__group"))
|
||||
updated_sub_issues = Issue.issue_objects.filter(
|
||||
id__in=sub_issue_ids
|
||||
).annotate(state_group=F("state__group"))
|
||||
|
||||
# Track the issue
|
||||
_ = [
|
||||
@@ -833,10 +857,12 @@ class SubIssuesEndpoint(BaseAPIView):
|
||||
project_id=str(project_id),
|
||||
current_instance=json.dumps({"parent": str(sub_issue_id)}),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
for sub_issue_id in sub_issue_ids
|
||||
]
|
||||
|
||||
|
||||
# create's a dict with state group name with their respective issue id's
|
||||
result = defaultdict(list)
|
||||
for sub_issue in updated_sub_issues:
|
||||
@@ -853,7 +879,6 @@ class SubIssuesEndpoint(BaseAPIView):
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
|
||||
|
||||
class IssueLinkViewSet(BaseViewSet):
|
||||
@@ -893,6 +918,8 @@ class IssueLinkViewSet(BaseViewSet):
|
||||
project_id=str(self.kwargs.get("project_id")),
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
@@ -922,6 +949,8 @@ class IssueLinkViewSet(BaseViewSet):
|
||||
project_id=str(project_id),
|
||||
current_instance=current_instance,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
@@ -945,6 +974,8 @@ class IssueLinkViewSet(BaseViewSet):
|
||||
project_id=str(project_id),
|
||||
current_instance=current_instance,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
issue_link.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
@@ -1001,6 +1032,8 @@ class IssueAttachmentEndpoint(BaseAPIView):
|
||||
cls=DjangoJSONEncoder,
|
||||
),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
@@ -1017,6 +1050,8 @@ class IssueAttachmentEndpoint(BaseAPIView):
|
||||
project_id=str(self.kwargs.get("project_id", None)),
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
@@ -1047,12 +1082,31 @@ class IssueArchiveViewSet(BaseViewSet):
|
||||
.filter(archived_at__isnull=False)
|
||||
.filter(project_id=self.kwargs.get("project_id"))
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.select_related("project")
|
||||
.select_related("workspace")
|
||||
.select_related("state")
|
||||
.select_related("parent")
|
||||
.prefetch_related("assignees")
|
||||
.prefetch_related("labels")
|
||||
.select_related("workspace", "project", "state", "parent")
|
||||
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||
.annotate(
|
||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
.annotate(
|
||||
attachment_count=IssueAttachment.objects.filter(
|
||||
issue=OuterRef("id")
|
||||
)
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
.annotate(
|
||||
sub_issues_count=Issue.issue_objects.filter(
|
||||
parent=OuterRef("id")
|
||||
)
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
)
|
||||
|
||||
@method_decorator(gzip_page)
|
||||
@@ -1080,22 +1134,6 @@ class IssueArchiveViewSet(BaseViewSet):
|
||||
issue_queryset = (
|
||||
self.get_queryset()
|
||||
.filter(**filters)
|
||||
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||
.annotate(module_id=F("issue_module__module_id"))
|
||||
.annotate(
|
||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
.annotate(
|
||||
attachment_count=IssueAttachment.objects.filter(
|
||||
issue=OuterRef("id")
|
||||
)
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
)
|
||||
|
||||
# Priority Ordering
|
||||
@@ -1195,6 +1233,8 @@ class IssueArchiveViewSet(BaseViewSet):
|
||||
IssueSerializer(issue).data, cls=DjangoJSONEncoder
|
||||
),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
issue.archived_at = None
|
||||
issue.save()
|
||||
@@ -1340,6 +1380,8 @@ class IssueReactionViewSet(BaseViewSet):
|
||||
project_id=str(project_id),
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
@@ -1365,6 +1407,8 @@ class IssueReactionViewSet(BaseViewSet):
|
||||
}
|
||||
),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
issue_reaction.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
@@ -1405,6 +1449,8 @@ class CommentReactionViewSet(BaseViewSet):
|
||||
project_id=str(project_id),
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
@@ -1431,6 +1477,8 @@ class CommentReactionViewSet(BaseViewSet):
|
||||
}
|
||||
),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
comment_reaction.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
@@ -1559,6 +1607,8 @@ class IssueRelationViewSet(BaseViewSet):
|
||||
project_id=str(project_id),
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
|
||||
if relation_type == "blocking":
|
||||
@@ -1603,6 +1653,8 @@ class IssueRelationViewSet(BaseViewSet):
|
||||
project_id=str(project_id),
|
||||
current_instance=current_instance,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@@ -1627,18 +1679,37 @@ class IssueDraftViewSet(BaseViewSet):
|
||||
.filter(project_id=self.kwargs.get("project_id"))
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.filter(is_draft=True)
|
||||
.select_related("project")
|
||||
.select_related("workspace")
|
||||
.select_related("state")
|
||||
.select_related("parent")
|
||||
.prefetch_related("assignees")
|
||||
.prefetch_related("labels")
|
||||
.select_related("workspace", "project", "state", "parent")
|
||||
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"issue_reactions",
|
||||
queryset=IssueReaction.objects.select_related("actor"),
|
||||
)
|
||||
)
|
||||
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||
.annotate(
|
||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
.annotate(
|
||||
attachment_count=IssueAttachment.objects.filter(
|
||||
issue=OuterRef("id")
|
||||
)
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
.annotate(
|
||||
sub_issues_count=Issue.issue_objects.filter(
|
||||
parent=OuterRef("id")
|
||||
)
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
)
|
||||
|
||||
@method_decorator(gzip_page)
|
||||
@@ -1665,22 +1736,6 @@ class IssueDraftViewSet(BaseViewSet):
|
||||
issue_queryset = (
|
||||
self.get_queryset()
|
||||
.filter(**filters)
|
||||
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||
.annotate(module_id=F("issue_module__module_id"))
|
||||
.annotate(
|
||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
.annotate(
|
||||
attachment_count=IssueAttachment.objects.filter(
|
||||
issue=OuterRef("id")
|
||||
)
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
)
|
||||
|
||||
# Priority Ordering
|
||||
@@ -1774,6 +1829,8 @@ class IssueDraftViewSet(BaseViewSet):
|
||||
project_id=str(project_id),
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
@@ -1804,6 +1861,8 @@ class IssueDraftViewSet(BaseViewSet):
|
||||
cls=DjangoJSONEncoder,
|
||||
),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
@@ -1830,5 +1889,7 @@ class IssueDraftViewSet(BaseViewSet):
|
||||
project_id=str(project_id),
|
||||
current_instance=current_instance,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
+120
-151
@@ -7,6 +7,8 @@ from django.db.models import Prefetch, F, OuterRef, Func, Exists, Count, Q
|
||||
from django.core import serializers
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.gzip import gzip_page
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
|
||||
|
||||
# Third party imports
|
||||
from rest_framework.response import Response
|
||||
@@ -296,21 +298,20 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
|
||||
"issue", flat=True
|
||||
)
|
||||
)
|
||||
issue_activity.delay(
|
||||
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=str(pk),
|
||||
project_id=str(project_id),
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
)
|
||||
_ = [
|
||||
issue_activity.delay(
|
||||
type="module.activity.deleted",
|
||||
requested_data=json.dumps({"module_id": str(pk)}),
|
||||
actor_id=str(request.user.id),
|
||||
issue_id=str(issue),
|
||||
project_id=project_id,
|
||||
current_instance=json.dumps({"module_name": str(module.name)}),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
for issue in module_issues
|
||||
]
|
||||
module.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@@ -330,62 +331,18 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet):
|
||||
ProjectEntityPermission,
|
||||
]
|
||||
|
||||
def get_queryset(self):
|
||||
return self.filter_queryset(
|
||||
super()
|
||||
.get_queryset()
|
||||
.annotate(
|
||||
sub_issues_count=Issue.issue_objects.filter(
|
||||
parent=OuterRef("issue")
|
||||
)
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.filter(project_id=self.kwargs.get("project_id"))
|
||||
.filter(module_id=self.kwargs.get("module_id"))
|
||||
.filter(project__project_projectmember__member=self.request.user)
|
||||
.select_related("project")
|
||||
.select_related("workspace")
|
||||
.select_related("module")
|
||||
.select_related("issue", "issue__state", "issue__project")
|
||||
.prefetch_related("issue__assignees", "issue__labels")
|
||||
.prefetch_related("module__members")
|
||||
.distinct()
|
||||
)
|
||||
|
||||
@method_decorator(gzip_page)
|
||||
def list(self, request, slug, project_id, module_id):
|
||||
fields = [
|
||||
field
|
||||
for field in request.GET.get("fields", "").split(",")
|
||||
if field
|
||||
]
|
||||
order_by = request.GET.get("order_by", "created_at")
|
||||
filters = issue_filters(request.query_params, "GET")
|
||||
issues = (
|
||||
Issue.issue_objects.filter(issue_module__module_id=module_id)
|
||||
.annotate(
|
||||
sub_issues_count=Issue.issue_objects.filter(
|
||||
parent=OuterRef("id")
|
||||
)
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
def get_queryset(self):
|
||||
return (
|
||||
Issue.objects.filter(
|
||||
project_id=self.kwargs.get("project_id"),
|
||||
workspace__slug=self.kwargs.get("slug"),
|
||||
issue_module__module_id=self.kwargs.get("module_id")
|
||||
)
|
||||
.filter(project_id=project_id)
|
||||
.filter(workspace__slug=slug)
|
||||
.select_related("project")
|
||||
.select_related("workspace")
|
||||
.select_related("state")
|
||||
.select_related("parent")
|
||||
.prefetch_related("assignees")
|
||||
.prefetch_related("labels")
|
||||
.order_by(order_by)
|
||||
.filter(**filters)
|
||||
.select_related("workspace", "project", "state", "parent")
|
||||
.prefetch_related("labels", "assignees")
|
||||
.prefetch_related('issue_module__module')
|
||||
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||
.annotate(module_id=F("issue_module__module_id"))
|
||||
.annotate(
|
||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||
.order_by()
|
||||
@@ -401,103 +358,118 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet):
|
||||
.values("count")
|
||||
)
|
||||
.annotate(
|
||||
is_subscribed=Exists(
|
||||
IssueSubscriber.objects.filter(
|
||||
subscriber=self.request.user, issue_id=OuterRef("id")
|
||||
)
|
||||
sub_issues_count=Issue.issue_objects.filter(
|
||||
parent=OuterRef("id")
|
||||
)
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
)
|
||||
).distinct()
|
||||
|
||||
@method_decorator(gzip_page)
|
||||
def list(self, request, slug, project_id, module_id):
|
||||
fields = [
|
||||
field
|
||||
for field in request.GET.get("fields", "").split(",")
|
||||
if field
|
||||
]
|
||||
filters = issue_filters(request.query_params, "GET")
|
||||
issue_queryset = self.get_queryset().filter(**filters)
|
||||
serializer = IssueSerializer(
|
||||
issues, many=True, fields=fields if fields else None
|
||||
issue_queryset, many=True, fields=fields if fields else None
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
def create(self, request, slug, project_id, module_id):
|
||||
# create multiple issues inside a module
|
||||
def create_module_issues(self, request, slug, project_id, module_id):
|
||||
issues = request.data.get("issues", [])
|
||||
if not len(issues):
|
||||
return Response(
|
||||
{"error": "Issues are required"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
module = Module.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, pk=module_id
|
||||
)
|
||||
|
||||
module_issues = list(ModuleIssue.objects.filter(issue_id__in=issues))
|
||||
|
||||
update_module_issue_activity = []
|
||||
records_to_update = []
|
||||
record_to_create = []
|
||||
|
||||
for issue in issues:
|
||||
module_issue = [
|
||||
module_issue
|
||||
for module_issue in module_issues
|
||||
if str(module_issue.issue_id) in issues
|
||||
]
|
||||
|
||||
if len(module_issue):
|
||||
if module_issue[0].module_id != module_id:
|
||||
update_module_issue_activity.append(
|
||||
{
|
||||
"old_module_id": str(module_issue[0].module_id),
|
||||
"new_module_id": str(module_id),
|
||||
"issue_id": str(module_issue[0].issue_id),
|
||||
}
|
||||
)
|
||||
module_issue[0].module_id = module_id
|
||||
records_to_update.append(module_issue[0])
|
||||
else:
|
||||
record_to_create.append(
|
||||
ModuleIssue(
|
||||
module=module,
|
||||
issue_id=issue,
|
||||
project_id=project_id,
|
||||
workspace=module.workspace,
|
||||
created_by=request.user,
|
||||
updated_by=request.user,
|
||||
)
|
||||
project = Project.objects.get(pk=project_id)
|
||||
_ = ModuleIssue.objects.bulk_create(
|
||||
[
|
||||
ModuleIssue(
|
||||
issue_id=str(issue),
|
||||
module_id=module_id,
|
||||
project_id=project_id,
|
||||
workspace_id=project.workspace_id,
|
||||
created_by=request.user,
|
||||
updated_by=request.user,
|
||||
)
|
||||
|
||||
ModuleIssue.objects.bulk_create(
|
||||
record_to_create,
|
||||
for issue in issues
|
||||
],
|
||||
batch_size=10,
|
||||
ignore_conflicts=True,
|
||||
)
|
||||
# Bulk Update the activity
|
||||
_ = [
|
||||
issue_activity.delay(
|
||||
type="module.activity.created",
|
||||
requested_data=json.dumps({"module_id": str(module_id)}),
|
||||
actor_id=str(request.user.id),
|
||||
issue_id=str(issue),
|
||||
project_id=project_id,
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
for issue in issues
|
||||
]
|
||||
issues = (self.get_queryset().filter(pk__in=issues))
|
||||
serializer = IssueSerializer(issues , many=True)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
ModuleIssue.objects.bulk_update(
|
||||
records_to_update,
|
||||
["module"],
|
||||
# create multiple module inside an issue
|
||||
def create_issue_modules(self, request, slug, project_id, issue_id):
|
||||
modules = request.data.get("modules", [])
|
||||
if not len(modules):
|
||||
return Response(
|
||||
{"error": "Modules are required"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
project = Project.objects.get(pk=project_id)
|
||||
_ = ModuleIssue.objects.bulk_create(
|
||||
[
|
||||
ModuleIssue(
|
||||
issue_id=issue_id,
|
||||
module_id=module,
|
||||
project_id=project_id,
|
||||
workspace_id=project.workspace_id,
|
||||
created_by=request.user,
|
||||
updated_by=request.user,
|
||||
)
|
||||
for module in modules
|
||||
],
|
||||
batch_size=10,
|
||||
ignore_conflicts=True,
|
||||
)
|
||||
# Bulk Update the activity
|
||||
_ = [
|
||||
issue_activity.delay(
|
||||
type="module.activity.created",
|
||||
requested_data=json.dumps({"module_id": module}),
|
||||
actor_id=str(request.user.id),
|
||||
issue_id=issue_id,
|
||||
project_id=project_id,
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
for module in modules
|
||||
]
|
||||
|
||||
# Capture Issue Activity
|
||||
issue_activity.delay(
|
||||
type="module.activity.created",
|
||||
requested_data=json.dumps({"modules_list": 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()),
|
||||
)
|
||||
issue = (self.get_queryset().filter(pk=issue_id).first())
|
||||
serializer = IssueSerializer(issue)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
issues = self.get_queryset().values_list("issue_id", flat=True)
|
||||
|
||||
return Response(
|
||||
IssueSerializer(
|
||||
Issue.objects.filter(pk__in=issues), many=True
|
||||
).data,
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
def destroy(self, request, slug, project_id, module_id, issue_id):
|
||||
module_issue = ModuleIssue.objects.get(
|
||||
@@ -508,17 +480,14 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet):
|
||||
)
|
||||
issue_activity.delay(
|
||||
type="module.activity.deleted",
|
||||
requested_data=json.dumps(
|
||||
{
|
||||
"module_id": str(module_id),
|
||||
"issues": [str(issue_id)],
|
||||
}
|
||||
),
|
||||
requested_data=json.dumps({"module_id": str(module_id)}),
|
||||
actor_id=str(request.user.id),
|
||||
issue_id=str(issue_id),
|
||||
project_id=str(project_id),
|
||||
current_instance=None,
|
||||
current_instance=json.dumps({"module_name": module_issue.module.name}),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
module_issue.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Django imports
|
||||
from django.db.models import Q
|
||||
from django.db.models import Q, OuterRef, Exists
|
||||
from django.utils import timezone
|
||||
|
||||
# Third party imports
|
||||
@@ -15,8 +15,9 @@ from plane.db.models import (
|
||||
IssueSubscriber,
|
||||
Issue,
|
||||
WorkspaceMember,
|
||||
UserNotificationPreference,
|
||||
)
|
||||
from plane.app.serializers import NotificationSerializer
|
||||
from plane.app.serializers import NotificationSerializer, UserNotificationPreferenceSerializer
|
||||
|
||||
|
||||
class NotificationViewSet(BaseViewSet, BasePaginator):
|
||||
@@ -71,11 +72,29 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
|
||||
|
||||
# Subscribed issues
|
||||
if type == "watching":
|
||||
issue_ids = IssueSubscriber.objects.filter(
|
||||
workspace__slug=slug, subscriber_id=request.user.id
|
||||
).values_list("issue_id", flat=True)
|
||||
issue_ids = (
|
||||
IssueSubscriber.objects.filter(
|
||||
workspace__slug=slug, subscriber_id=request.user.id
|
||||
)
|
||||
.annotate(
|
||||
created=Exists(
|
||||
Issue.objects.filter(
|
||||
created_by=request.user, pk=OuterRef("issue_id")
|
||||
)
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
assigned=Exists(
|
||||
IssueAssignee.objects.filter(
|
||||
pk=OuterRef("issue_id"), assignee=request.user
|
||||
)
|
||||
)
|
||||
)
|
||||
.filter(created=False, assigned=False)
|
||||
.values_list("issue_id", flat=True)
|
||||
)
|
||||
notifications = notifications.filter(
|
||||
entity_identifier__in=issue_ids
|
||||
entity_identifier__in=issue_ids,
|
||||
)
|
||||
|
||||
# Assigned Issues
|
||||
@@ -295,3 +314,31 @@ class MarkAllReadNotificationViewSet(BaseViewSet):
|
||||
updated_notifications, ["read_at"], batch_size=100
|
||||
)
|
||||
return Response({"message": "Successful"}, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class UserNotificationPreferenceEndpoint(BaseAPIView):
|
||||
model = UserNotificationPreference
|
||||
serializer_class = UserNotificationPreferenceSerializer
|
||||
|
||||
# request the object
|
||||
def get(self, request):
|
||||
user_notification_preference = UserNotificationPreference.objects.get(
|
||||
user=request.user
|
||||
)
|
||||
serializer = UserNotificationPreferenceSerializer(
|
||||
user_notification_preference
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
# update the object
|
||||
def patch(self, request):
|
||||
user_notification_preference = UserNotificationPreference.objects.get(
|
||||
user=request.user
|
||||
)
|
||||
serializer = UserNotificationPreferenceSerializer(
|
||||
user_notification_preference, data=request.data, partial=True
|
||||
)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@@ -158,18 +158,18 @@ class PageViewSet(BaseViewSet):
|
||||
pk=page_id, workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
|
||||
# only the owner and admin can archive the page
|
||||
# only the owner or admin can archive the page
|
||||
if (
|
||||
ProjectMember.objects.filter(
|
||||
project_id=project_id,
|
||||
member=request.user,
|
||||
is_active=True,
|
||||
role__gte=20,
|
||||
role__lte=15,
|
||||
).exists()
|
||||
or request.user.id != page.owned_by_id
|
||||
and request.user.id != page.owned_by_id
|
||||
):
|
||||
return Response(
|
||||
{"error": "Only the owner and admin can archive the page"},
|
||||
{"error": "Only the owner or admin can archive the page"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@@ -182,18 +182,18 @@ class PageViewSet(BaseViewSet):
|
||||
pk=page_id, workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
|
||||
# only the owner and admin can un archive the page
|
||||
# only the owner or admin can un archive the page
|
||||
if (
|
||||
ProjectMember.objects.filter(
|
||||
project_id=project_id,
|
||||
member=request.user,
|
||||
is_active=True,
|
||||
role__gt=20,
|
||||
role__lte=15,
|
||||
).exists()
|
||||
or request.user.id != page.owned_by_id
|
||||
and request.user.id != page.owned_by_id
|
||||
):
|
||||
return Response(
|
||||
{"error": "Only the owner and admin can un archive the page"},
|
||||
{"error": "Only the owner or admin can un archive the page"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ from plane.bgtasks.project_invitation_task import project_invitation
|
||||
|
||||
|
||||
class ProjectViewSet(WebhookMixin, BaseViewSet):
|
||||
serializer_class = ProjectSerializer
|
||||
serializer_class = ProjectListSerializer
|
||||
model = Project
|
||||
webhook_event = "project"
|
||||
|
||||
@@ -76,11 +76,6 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
|
||||
ProjectBasePermission,
|
||||
]
|
||||
|
||||
def get_serializer_class(self, *args, **kwargs):
|
||||
if self.action in ["update", "partial_update"]:
|
||||
return ProjectSerializer
|
||||
return ProjectDetailSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
return self.filter_queryset(
|
||||
super()
|
||||
@@ -690,6 +685,19 @@ class ProjectMemberViewSet(BaseViewSet):
|
||||
.order_by("sort_order")
|
||||
)
|
||||
|
||||
bulk_project_members = []
|
||||
member_roles = {member.get("member_id"): member.get("role") for member in members}
|
||||
# Update roles in the members array based on the member_roles dictionary
|
||||
for project_member in ProjectMember.objects.filter(project_id=project_id, member_id__in=[member.get("member_id") for member in members]):
|
||||
project_member.role = member_roles[str(project_member.member_id)]
|
||||
project_member.is_active = True
|
||||
bulk_project_members.append(project_member)
|
||||
|
||||
# Update the roles of the existing members
|
||||
ProjectMember.objects.bulk_update(
|
||||
bulk_project_members, ["is_active", "role"], batch_size=100
|
||||
)
|
||||
|
||||
for member in members:
|
||||
sort_order = [
|
||||
project_member.get("sort_order")
|
||||
@@ -716,25 +724,6 @@ class ProjectMemberViewSet(BaseViewSet):
|
||||
)
|
||||
)
|
||||
|
||||
# Check if the user is already a member of the project and is inactive
|
||||
if ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
member_id=member.get("member_id"),
|
||||
is_active=False,
|
||||
).exists():
|
||||
member_detail = ProjectMember.objects.get(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
member_id=member.get("member_id"),
|
||||
is_active=False,
|
||||
)
|
||||
# Check if the user has not deactivated the account
|
||||
user = User.objects.filter(pk=member.get("member_id")).first()
|
||||
if user.is_active:
|
||||
member_detail.is_active = True
|
||||
member_detail.save(update_fields=["is_active"])
|
||||
|
||||
project_members = ProjectMember.objects.bulk_create(
|
||||
bulk_project_members,
|
||||
batch_size=10,
|
||||
@@ -745,8 +734,8 @@ class ProjectMemberViewSet(BaseViewSet):
|
||||
bulk_issue_props, batch_size=10, ignore_conflicts=True
|
||||
)
|
||||
|
||||
serializer = ProjectMemberSerializer(project_members, many=True)
|
||||
|
||||
project_members = ProjectMember.objects.filter(project_id=project_id, member_id__in=[member.get("member_id") for member in members])
|
||||
serializer = ProjectMemberRoleSerializer(project_members, many=True)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
def list(self, request, slug, project_id):
|
||||
|
||||
@@ -228,7 +228,7 @@ class IssueSearchEndpoint(BaseAPIView):
|
||||
parent = request.query_params.get("parent", "false")
|
||||
issue_relation = request.query_params.get("issue_relation", "false")
|
||||
cycle = request.query_params.get("cycle", "false")
|
||||
module = request.query_params.get("module", "false")
|
||||
module = request.query_params.get("module", False)
|
||||
sub_issue = request.query_params.get("sub_issue", "false")
|
||||
|
||||
issue_id = request.query_params.get("issue_id", False)
|
||||
@@ -269,8 +269,8 @@ class IssueSearchEndpoint(BaseAPIView):
|
||||
if cycle == "true":
|
||||
issues = issues.exclude(issue_cycle__isnull=False)
|
||||
|
||||
if module == "true":
|
||||
issues = issues.exclude(issue_module__isnull=False)
|
||||
if module:
|
||||
issues = issues.exclude(issue_module__module=module)
|
||||
|
||||
return Response(
|
||||
issues.values(
|
||||
|
||||
@@ -9,9 +9,12 @@ from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
|
||||
# Module imports
|
||||
from . import BaseViewSet
|
||||
from . import BaseViewSet, BaseAPIView
|
||||
from plane.app.serializers import StateSerializer
|
||||
from plane.app.permissions import ProjectEntityPermission
|
||||
from plane.app.permissions import (
|
||||
ProjectEntityPermission,
|
||||
WorkspaceEntityPermission,
|
||||
)
|
||||
from plane.db.models import State, Issue
|
||||
|
||||
|
||||
@@ -22,9 +25,6 @@ class StateViewSet(BaseViewSet):
|
||||
ProjectEntityPermission,
|
||||
]
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(project_id=self.kwargs.get("project_id"))
|
||||
|
||||
def get_queryset(self):
|
||||
return self.filter_queryset(
|
||||
super()
|
||||
|
||||
@@ -9,10 +9,19 @@ from plane.app.serializers import (
|
||||
IssueActivitySerializer,
|
||||
UserMeSerializer,
|
||||
UserMeSettingsSerializer,
|
||||
ProfileSerializer,
|
||||
AccountSerializer,
|
||||
)
|
||||
|
||||
from plane.app.views.base import BaseViewSet, BaseAPIView
|
||||
from plane.db.models import User, IssueActivity, WorkspaceMember, ProjectMember
|
||||
from plane.db.models import (
|
||||
Account,
|
||||
User,
|
||||
IssueActivity,
|
||||
WorkspaceMember,
|
||||
ProjectMember,
|
||||
Profile,
|
||||
)
|
||||
from plane.license.models import Instance, InstanceAdmin
|
||||
from plane.utils.paginator import BasePaginator
|
||||
|
||||
@@ -131,24 +140,29 @@ class UserEndpoint(BaseViewSet):
|
||||
|
||||
# Deactivate the user
|
||||
user.is_active = False
|
||||
user.last_workspace_id = None
|
||||
user.is_tour_completed = False
|
||||
user.is_onboarded = False
|
||||
user.onboarding_step = {
|
||||
|
||||
# Profile updates
|
||||
profile = Profile.objects.get(user=user)
|
||||
|
||||
profile.last_workspace_id = None
|
||||
profile.is_tour_completed = False
|
||||
profile.is_onboarded = False
|
||||
profile.onboarding_step = {
|
||||
"workspace_join": False,
|
||||
"profile_complete": False,
|
||||
"workspace_create": False,
|
||||
"workspace_invite": False,
|
||||
}
|
||||
profile.save()
|
||||
user.save()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
class UpdateUserOnBoardedEndpoint(BaseAPIView):
|
||||
def patch(self, request):
|
||||
user = User.objects.get(pk=request.user.id, is_active=True)
|
||||
user.is_onboarded = request.data.get("is_onboarded", False)
|
||||
user.save()
|
||||
profile = Profile.objects.get(user_id=request.user.id)
|
||||
profile.is_onboarded = request.data.get("is_onboarded", False)
|
||||
profile.save()
|
||||
return Response(
|
||||
{"message": "Updated successfully"}, status=status.HTTP_200_OK
|
||||
)
|
||||
@@ -156,9 +170,11 @@ class UpdateUserOnBoardedEndpoint(BaseAPIView):
|
||||
|
||||
class UpdateUserTourCompletedEndpoint(BaseAPIView):
|
||||
def patch(self, request):
|
||||
user = User.objects.get(pk=request.user.id, is_active=True)
|
||||
user.is_tour_completed = request.data.get("is_tour_completed", False)
|
||||
user.save()
|
||||
profile = Profile.objects.get(user_id=request.user.id, is_active=True)
|
||||
profile.is_tour_completed = request.data.get(
|
||||
"is_tour_completed", False
|
||||
)
|
||||
profile.save()
|
||||
return Response(
|
||||
{"message": "Updated successfully"}, status=status.HTTP_200_OK
|
||||
)
|
||||
@@ -177,3 +193,18 @@ class UserActivityEndpoint(BaseAPIView, BasePaginator):
|
||||
issue_activities, many=True
|
||||
).data,
|
||||
)
|
||||
|
||||
|
||||
class ProfileEndpoint(BaseAPIView):
|
||||
def get(self, request):
|
||||
profile = Profile.objects.get(user=request.user)
|
||||
serializer = ProfileSerializer(profile)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class AccountEndpoint(BaseAPIView):
|
||||
|
||||
def get(self, request):
|
||||
accounts = Account.objects.filter(user=request.user)
|
||||
serializer = AccountSerializer(accounts, many=True)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
@@ -87,12 +87,8 @@ class GlobalViewIssuesViewSet(BaseViewSet):
|
||||
.values("count")
|
||||
)
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.select_related("project")
|
||||
.select_related("workspace")
|
||||
.select_related("state")
|
||||
.select_related("parent")
|
||||
.prefetch_related("assignees")
|
||||
.prefetch_related("labels")
|
||||
.select_related("workspace", "project", "state", "parent")
|
||||
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"issue_reactions",
|
||||
@@ -127,7 +123,6 @@ class GlobalViewIssuesViewSet(BaseViewSet):
|
||||
.filter(**filters)
|
||||
.filter(project__project_projectmember__member=self.request.user)
|
||||
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||
.annotate(module_id=F("issue_module__module_id"))
|
||||
.annotate(
|
||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||
.order_by()
|
||||
@@ -150,13 +145,6 @@ class GlobalViewIssuesViewSet(BaseViewSet):
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
.annotate(
|
||||
is_subscribed=Exists(
|
||||
IssueSubscriber.objects.filter(
|
||||
subscriber=self.request.user, issue_id=OuterRef("id")
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# Priority Ordering
|
||||
|
||||
@@ -46,10 +46,14 @@ from plane.app.serializers import (
|
||||
WorkspaceMemberMeSerializer,
|
||||
ProjectMemberRoleSerializer,
|
||||
WorkspaceUserPropertiesSerializer,
|
||||
WorkspaceEstimateSerializer,
|
||||
StateSerializer,
|
||||
LabelSerializer,
|
||||
)
|
||||
from plane.app.views.base import BaseAPIView
|
||||
from . import BaseViewSet
|
||||
from plane.db.models import (
|
||||
State,
|
||||
User,
|
||||
Workspace,
|
||||
WorkspaceMemberInvite,
|
||||
@@ -67,6 +71,8 @@ from plane.db.models import (
|
||||
CycleIssue,
|
||||
IssueReaction,
|
||||
WorkspaceUserProperties,
|
||||
Estimate,
|
||||
EstimatePoint,
|
||||
)
|
||||
from plane.app.permissions import (
|
||||
WorkSpaceBasePermission,
|
||||
@@ -1340,9 +1346,8 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
|
||||
)
|
||||
.filter(**filters)
|
||||
.select_related("workspace", "project", "state", "parent")
|
||||
.prefetch_related("assignees", "labels")
|
||||
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||
.annotate(module_id=F("issue_module__module_id"))
|
||||
.annotate(
|
||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||
.order_by()
|
||||
@@ -1358,7 +1363,9 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
|
||||
.values("count")
|
||||
)
|
||||
.annotate(
|
||||
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
|
||||
sub_issues_count=Issue.issue_objects.filter(
|
||||
parent=OuterRef("id")
|
||||
)
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
@@ -1441,10 +1448,46 @@ class WorkspaceLabelsEndpoint(BaseAPIView):
|
||||
labels = Label.objects.filter(
|
||||
workspace__slug=slug,
|
||||
project__project_projectmember__member=request.user,
|
||||
).values(
|
||||
"parent", "name", "color", "id", "project_id", "workspace__slug"
|
||||
)
|
||||
return Response(labels, status=status.HTTP_200_OK)
|
||||
serializer = LabelSerializer(labels, many=True).data
|
||||
return Response(serializer, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class WorkspaceStatesEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
WorkspaceEntityPermission,
|
||||
]
|
||||
|
||||
def get(self, request, slug):
|
||||
states = State.objects.filter(
|
||||
workspace__slug=slug,
|
||||
project__project_projectmember__member=request.user,
|
||||
)
|
||||
serializer = StateSerializer(states, many=True).data
|
||||
return Response(serializer, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class WorkspaceEstimatesEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
WorkspaceEntityPermission,
|
||||
]
|
||||
|
||||
def get(self, request, slug):
|
||||
estimate_ids = Project.objects.filter(
|
||||
workspace__slug=slug, estimate__isnull=False
|
||||
).values_list("estimate_id", flat=True)
|
||||
estimates = Estimate.objects.filter(
|
||||
pk__in=estimate_ids
|
||||
).prefetch_related(
|
||||
Prefetch(
|
||||
"points",
|
||||
queryset=EstimatePoint.objects.select_related(
|
||||
"estimate", "workspace", "project"
|
||||
),
|
||||
)
|
||||
)
|
||||
serializer = WorkspaceEstimateSerializer(estimates, many=True)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class WorkspaceUserPropertiesEndpoint(BaseAPIView):
|
||||
|
||||
@@ -0,0 +1,236 @@
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
# Third party imports
|
||||
from celery import shared_task
|
||||
|
||||
# Django imports
|
||||
from django.utils import timezone
|
||||
from django.core.mail import EmailMultiAlternatives, get_connection
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.html import strip_tags
|
||||
from django.conf import settings
|
||||
|
||||
# Module imports
|
||||
from plane.db.models import EmailNotificationLog, User, Issue
|
||||
from plane.license.utils.instance_value import get_email_configuration
|
||||
from plane.settings.redis import redis_instance
|
||||
|
||||
@shared_task
|
||||
def stack_email_notification():
|
||||
# get all email notifications
|
||||
email_notifications = (
|
||||
EmailNotificationLog.objects.filter(processed_at__isnull=True)
|
||||
.order_by("receiver")
|
||||
.values()
|
||||
)
|
||||
|
||||
# Create the below format for each of the issues
|
||||
# {"issue_id" : { "actor_id1": [ { data }, { data } ], "actor_id2": [ { data }, { data } ] }}
|
||||
|
||||
# Convert to unique receivers list
|
||||
receivers = list(
|
||||
set(
|
||||
[
|
||||
str(notification.get("receiver_id"))
|
||||
for notification in email_notifications
|
||||
]
|
||||
)
|
||||
)
|
||||
processed_notifications = []
|
||||
# Loop through all the issues to create the emails
|
||||
for receiver_id in receivers:
|
||||
# Notifcation triggered for the receiver
|
||||
receiver_notifications = [
|
||||
notification
|
||||
for notification in email_notifications
|
||||
if str(notification.get("receiver_id")) == receiver_id
|
||||
]
|
||||
# create payload for all issues
|
||||
payload = {}
|
||||
email_notification_ids = []
|
||||
for receiver_notification in receiver_notifications:
|
||||
payload.setdefault(
|
||||
receiver_notification.get("entity_identifier"), {}
|
||||
).setdefault(
|
||||
str(receiver_notification.get("triggered_by_id")), []
|
||||
).append(
|
||||
receiver_notification.get("data")
|
||||
)
|
||||
# append processed notifications
|
||||
processed_notifications.append(receiver_notification.get("id"))
|
||||
email_notification_ids.append(receiver_notification.get("id"))
|
||||
|
||||
# Create emails for all the issues
|
||||
for issue_id, notification_data in payload.items():
|
||||
send_email_notification.delay(
|
||||
issue_id=issue_id,
|
||||
notification_data=notification_data,
|
||||
receiver_id=receiver_id,
|
||||
email_notification_ids=email_notification_ids,
|
||||
)
|
||||
|
||||
# Update the email notification log
|
||||
EmailNotificationLog.objects.filter(pk__in=processed_notifications).update(
|
||||
processed_at=timezone.now()
|
||||
)
|
||||
|
||||
|
||||
def create_payload(notification_data):
|
||||
# return format {"actor_id": { "key": { "old_value": [], "new_value": [] } }}
|
||||
data = {}
|
||||
for actor_id, changes in notification_data.items():
|
||||
for change in changes:
|
||||
issue_activity = change.get("issue_activity")
|
||||
if issue_activity: # Ensure issue_activity is not None
|
||||
field = issue_activity.get("field")
|
||||
old_value = str(issue_activity.get("old_value"))
|
||||
new_value = str(issue_activity.get("new_value"))
|
||||
|
||||
# Append old_value if it's not empty and not already in the list
|
||||
if old_value:
|
||||
data.setdefault(actor_id, {}).setdefault(
|
||||
field, {}
|
||||
).setdefault("old_value", []).append(
|
||||
old_value
|
||||
) if old_value not in data.setdefault(
|
||||
actor_id, {}
|
||||
).setdefault(
|
||||
field, {}
|
||||
).get(
|
||||
"old_value", []
|
||||
) else None
|
||||
|
||||
# Append new_value if it's not empty and not already in the list
|
||||
if new_value:
|
||||
data.setdefault(actor_id, {}).setdefault(
|
||||
field, {}
|
||||
).setdefault("new_value", []).append(
|
||||
new_value
|
||||
) if new_value not in data.setdefault(
|
||||
actor_id, {}
|
||||
).setdefault(
|
||||
field, {}
|
||||
).get(
|
||||
"new_value", []
|
||||
) else None
|
||||
|
||||
if not data.get("actor_id", {}).get("activity_time", False):
|
||||
data[actor_id]["activity_time"] = str(
|
||||
datetime.fromisoformat(
|
||||
issue_activity.get("activity_time").rstrip("Z")
|
||||
).strftime("%Y-%m-%d %H:%M:%S")
|
||||
)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
@shared_task
|
||||
def send_email_notification(
|
||||
issue_id, notification_data, receiver_id, email_notification_ids
|
||||
):
|
||||
ri = redis_instance()
|
||||
base_api = (ri.get(str(issue_id)).decode())
|
||||
data = create_payload(notification_data=notification_data)
|
||||
|
||||
# Get email configurations
|
||||
(
|
||||
EMAIL_HOST,
|
||||
EMAIL_HOST_USER,
|
||||
EMAIL_HOST_PASSWORD,
|
||||
EMAIL_PORT,
|
||||
EMAIL_USE_TLS,
|
||||
EMAIL_FROM,
|
||||
) = get_email_configuration()
|
||||
|
||||
receiver = User.objects.get(pk=receiver_id)
|
||||
issue = Issue.objects.get(pk=issue_id)
|
||||
template_data = []
|
||||
total_changes = 0
|
||||
comments = []
|
||||
for actor_id, changes in data.items():
|
||||
actor = User.objects.get(pk=actor_id)
|
||||
total_changes = total_changes + len(changes)
|
||||
comment = changes.pop("comment", False)
|
||||
if comment:
|
||||
comments.append(
|
||||
{
|
||||
"actor_comments": comment,
|
||||
"actor_detail": {
|
||||
"avatar_url": actor.avatar,
|
||||
"first_name": actor.first_name,
|
||||
"last_name": actor.last_name,
|
||||
},
|
||||
}
|
||||
)
|
||||
activity_time = changes.pop("activity_time")
|
||||
# Parse the input string into a datetime object
|
||||
formatted_time = datetime.strptime(activity_time, "%Y-%m-%d %H:%M:%S").strftime("%H:%M %p")
|
||||
|
||||
if changes:
|
||||
template_data.append(
|
||||
{
|
||||
"actor_detail": {
|
||||
"avatar_url": actor.avatar,
|
||||
"first_name": actor.first_name,
|
||||
"last_name": actor.last_name,
|
||||
},
|
||||
"changes": changes,
|
||||
"issue_details": {
|
||||
"name": issue.name,
|
||||
"identifier": f"{issue.project.identifier}-{issue.sequence_id}",
|
||||
},
|
||||
"activity_time": str(formatted_time),
|
||||
}
|
||||
)
|
||||
|
||||
summary = "updates were made to the issue by"
|
||||
|
||||
# Send the mail
|
||||
subject = f"{issue.project.identifier}-{issue.sequence_id} {issue.name}"
|
||||
context = {
|
||||
"data": template_data,
|
||||
"summary": summary,
|
||||
"issue": {
|
||||
"issue_identifier": f"{str(issue.project.identifier)}-{str(issue.sequence_id)}",
|
||||
"name": issue.name,
|
||||
"issue_url": f"{base_api}/{str(issue.project.workspace.slug)}/projects/{str(issue.project.id)}/issues/{str(issue.id)}",
|
||||
},
|
||||
"receiver": {
|
||||
"email": receiver.email,
|
||||
},
|
||||
"issue_url": f"{base_api}/{str(issue.project.workspace.slug)}/projects/{str(issue.project.id)}/issues/{str(issue.id)}",
|
||||
"user_preference": f"{base_api}/profile/preferences/email",
|
||||
"comments": comments,
|
||||
}
|
||||
html_content = render_to_string(
|
||||
"emails/notifications/issue-updates.html", context
|
||||
)
|
||||
text_content = strip_tags(html_content)
|
||||
|
||||
try:
|
||||
connection = get_connection(
|
||||
host=EMAIL_HOST,
|
||||
port=int(EMAIL_PORT),
|
||||
username=EMAIL_HOST_USER,
|
||||
password=EMAIL_HOST_PASSWORD,
|
||||
use_tls=EMAIL_USE_TLS == "1",
|
||||
)
|
||||
|
||||
msg = EmailMultiAlternatives(
|
||||
subject=subject,
|
||||
body=text_content,
|
||||
from_email=EMAIL_FROM,
|
||||
to=[receiver.email],
|
||||
connection=connection,
|
||||
)
|
||||
msg.attach_alternative(html_content, "text/html")
|
||||
msg.send()
|
||||
|
||||
EmailNotificationLog.objects.filter(
|
||||
pk__in=email_notification_ids
|
||||
).update(sent_at=timezone.now())
|
||||
return
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return
|
||||
@@ -97,7 +97,7 @@ def upload_to_s3(zip_file, workspace_id, token_id, slug):
|
||||
)
|
||||
# Create the new url with updated domain and protocol
|
||||
presigned_url = presigned_url.replace(
|
||||
"http://plane-minio:9000/uploads/",
|
||||
f"{settings.AWS_S3_ENDPOINT_URL}/{settings.AWS_STORAGE_BUCKET_NAME}/",
|
||||
f"{settings.AWS_S3_URL_PROTOCOL}//{settings.AWS_S3_CUSTOM_DOMAIN}/",
|
||||
)
|
||||
else:
|
||||
|
||||
@@ -21,7 +21,7 @@ from plane.license.utils.instance_value import get_email_configuration
|
||||
def forgot_password(first_name, email, uidb64, token, current_site):
|
||||
try:
|
||||
relative_link = (
|
||||
f"/accounts/password/?uidb64={uidb64}&token={token}&email={email}"
|
||||
f"/accounts/reset-password/?uidb64={uidb64}&token={token}&email={email}"
|
||||
)
|
||||
abs_url = str(current_site) + relative_link
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ from plane.db.models import (
|
||||
Label,
|
||||
User,
|
||||
IssueProperty,
|
||||
UserNotificationPreference,
|
||||
)
|
||||
|
||||
|
||||
@@ -50,10 +51,24 @@ def service_importer(service, importer_id):
|
||||
for user in users
|
||||
if user.get("import", False) == "invite"
|
||||
],
|
||||
batch_size=10,
|
||||
batch_size=100,
|
||||
ignore_conflicts=True,
|
||||
)
|
||||
|
||||
_ = UserNotificationPreference.objects.bulk_create(
|
||||
[UserNotificationPreference(user=user) for user in new_users],
|
||||
batch_size=100,
|
||||
)
|
||||
|
||||
_ = [
|
||||
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()
|
||||
|
||||
@@ -24,9 +24,11 @@ from plane.db.models import (
|
||||
IssueReaction,
|
||||
CommentReaction,
|
||||
IssueComment,
|
||||
IssueSubscriber,
|
||||
)
|
||||
from plane.app.serializers import IssueActivitySerializer
|
||||
from plane.bgtasks.notification_task import notifications
|
||||
from plane.settings.redis import redis_instance
|
||||
|
||||
|
||||
# Track Changes in name
|
||||
@@ -111,15 +113,15 @@ def track_parent(
|
||||
issue_activities,
|
||||
epoch,
|
||||
):
|
||||
if current_instance.get("parent") != requested_data.get("parent"):
|
||||
if current_instance.get("parent_id") != requested_data.get("parent_id"):
|
||||
old_parent = (
|
||||
Issue.objects.filter(pk=current_instance.get("parent")).first()
|
||||
if current_instance.get("parent") is not None
|
||||
Issue.objects.filter(pk=current_instance.get("parent_id")).first()
|
||||
if current_instance.get("parent_id") is not None
|
||||
else None
|
||||
)
|
||||
new_parent = (
|
||||
Issue.objects.filter(pk=requested_data.get("parent")).first()
|
||||
if requested_data.get("parent") is not None
|
||||
Issue.objects.filter(pk=requested_data.get("parent_id")).first()
|
||||
if requested_data.get("parent_id") is not None
|
||||
else None
|
||||
)
|
||||
|
||||
@@ -188,9 +190,11 @@ def track_state(
|
||||
issue_activities,
|
||||
epoch,
|
||||
):
|
||||
if current_instance.get("state") != requested_data.get("state"):
|
||||
new_state = State.objects.get(pk=requested_data.get("state", None))
|
||||
old_state = State.objects.get(pk=current_instance.get("state", None))
|
||||
if current_instance.get("state_id") != requested_data.get("state_id"):
|
||||
new_state = State.objects.get(pk=requested_data.get("state_id", None))
|
||||
old_state = State.objects.get(
|
||||
pk=current_instance.get("state_id", None)
|
||||
)
|
||||
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
@@ -288,10 +292,10 @@ def track_labels(
|
||||
epoch,
|
||||
):
|
||||
requested_labels = set(
|
||||
[str(lab) for lab in requested_data.get("labels", [])]
|
||||
[str(lab) for lab in requested_data.get("label_ids", [])]
|
||||
)
|
||||
current_labels = set(
|
||||
[str(lab) for lab in current_instance.get("labels", [])]
|
||||
[str(lab) for lab in current_instance.get("label_ids", [])]
|
||||
)
|
||||
|
||||
added_labels = requested_labels - current_labels
|
||||
@@ -350,15 +354,16 @@ def track_assignees(
|
||||
epoch,
|
||||
):
|
||||
requested_assignees = set(
|
||||
[str(asg) for asg in requested_data.get("assignees", [])]
|
||||
[str(asg) for asg in requested_data.get("assignee_ids", [])]
|
||||
)
|
||||
current_assignees = set(
|
||||
[str(asg) for asg in current_instance.get("assignees", [])]
|
||||
[str(asg) for asg in current_instance.get("assignee_ids", [])]
|
||||
)
|
||||
|
||||
added_assignees = requested_assignees - current_assignees
|
||||
dropped_assginees = current_assignees - requested_assignees
|
||||
|
||||
bulk_subscribers = []
|
||||
for added_asignee in added_assignees:
|
||||
assignee = User.objects.get(pk=added_asignee)
|
||||
issue_activities.append(
|
||||
@@ -376,6 +381,21 @@ def track_assignees(
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
bulk_subscribers.append(
|
||||
IssueSubscriber(
|
||||
subscriber_id=assignee.id,
|
||||
issue_id=issue_id,
|
||||
workspace_id=workspace_id,
|
||||
project_id=project_id,
|
||||
created_by_id=assignee.id,
|
||||
updated_by_id=assignee.id,
|
||||
)
|
||||
)
|
||||
|
||||
# Create assignees subscribers to the issue and ignore if already
|
||||
IssueSubscriber.objects.bulk_create(
|
||||
bulk_subscribers, batch_size=10, ignore_conflicts=True
|
||||
)
|
||||
|
||||
for dropped_assignee in dropped_assginees:
|
||||
assignee = User.objects.get(pk=dropped_assignee)
|
||||
@@ -541,14 +561,14 @@ def update_issue_activity(
|
||||
):
|
||||
ISSUE_ACTIVITY_MAPPER = {
|
||||
"name": track_name,
|
||||
"parent": track_parent,
|
||||
"parent_id": track_parent,
|
||||
"priority": track_priority,
|
||||
"state": track_state,
|
||||
"state_id": track_state,
|
||||
"description_html": track_description,
|
||||
"target_date": track_target_date,
|
||||
"start_date": track_start_date,
|
||||
"labels": track_labels,
|
||||
"assignees": track_assignees,
|
||||
"label_ids": track_labels,
|
||||
"assignee_ids": track_assignees,
|
||||
"estimate_point": track_estimate_points,
|
||||
"archived_at": track_archive_at,
|
||||
"closed_to": track_closed_to,
|
||||
@@ -833,70 +853,26 @@ def create_module_issue_activity(
|
||||
requested_data = (
|
||||
json.loads(requested_data) if requested_data is not None else None
|
||||
)
|
||||
current_instance = (
|
||||
json.loads(current_instance) if current_instance is not None else None
|
||||
)
|
||||
|
||||
# Updated Records:
|
||||
updated_records = current_instance.get("updated_module_issues", [])
|
||||
created_records = json.loads(
|
||||
current_instance.get("created_module_issues", [])
|
||||
)
|
||||
|
||||
for updated_record in updated_records:
|
||||
old_module = Module.objects.filter(
|
||||
pk=updated_record.get("old_module_id", None)
|
||||
).first()
|
||||
new_module = Module.objects.filter(
|
||||
pk=updated_record.get("new_module_id", None)
|
||||
).first()
|
||||
issue = Issue.objects.filter(pk=updated_record.get("issue_id")).first()
|
||||
if issue:
|
||||
issue.updated_at = timezone.now()
|
||||
issue.save(update_fields=["updated_at"])
|
||||
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=updated_record.get("issue_id"),
|
||||
actor_id=actor_id,
|
||||
verb="updated",
|
||||
old_value=old_module.name,
|
||||
new_value=new_module.name,
|
||||
field="modules",
|
||||
project_id=project_id,
|
||||
workspace_id=workspace_id,
|
||||
comment=f"updated module to ",
|
||||
old_identifier=old_module.id,
|
||||
new_identifier=new_module.id,
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
for created_record in created_records:
|
||||
module = Module.objects.filter(
|
||||
pk=created_record.get("fields").get("module")
|
||||
).first()
|
||||
issue = Issue.objects.filter(
|
||||
pk=created_record.get("fields").get("issue")
|
||||
).first()
|
||||
if issue:
|
||||
issue.updated_at = timezone.now()
|
||||
issue.save(update_fields=["updated_at"])
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=created_record.get("fields").get("issue"),
|
||||
actor_id=actor_id,
|
||||
verb="created",
|
||||
old_value="",
|
||||
new_value=module.name,
|
||||
field="modules",
|
||||
project_id=project_id,
|
||||
workspace_id=workspace_id,
|
||||
comment=f"added module {module.name}",
|
||||
new_identifier=module.id,
|
||||
epoch=epoch,
|
||||
)
|
||||
module = Module.objects.filter(pk=requested_data.get("module_id")).first()
|
||||
issue = Issue.objects.filter(pk=issue_id).first()
|
||||
if issue:
|
||||
issue.updated_at = timezone.now()
|
||||
issue.save(update_fields=["updated_at"])
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor_id=actor_id,
|
||||
verb="created",
|
||||
old_value="",
|
||||
new_value=module.name,
|
||||
field="modules",
|
||||
project_id=project_id,
|
||||
workspace_id=workspace_id,
|
||||
comment=f"added module {module.name}",
|
||||
new_identifier=requested_data.get("module_id"),
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def delete_module_issue_activity(
|
||||
@@ -915,32 +891,26 @@ def delete_module_issue_activity(
|
||||
current_instance = (
|
||||
json.loads(current_instance) if current_instance is not None else None
|
||||
)
|
||||
|
||||
module_id = requested_data.get("module_id", "")
|
||||
module_name = requested_data.get("module_name", "")
|
||||
module = Module.objects.filter(pk=module_id).first()
|
||||
issues = requested_data.get("issues")
|
||||
|
||||
for issue in issues:
|
||||
current_issue = Issue.objects.filter(pk=issue).first()
|
||||
if issue:
|
||||
current_issue.updated_at = timezone.now()
|
||||
current_issue.save(update_fields=["updated_at"])
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue,
|
||||
actor_id=actor_id,
|
||||
verb="deleted",
|
||||
old_value=module.name if module is not None else module_name,
|
||||
new_value="",
|
||||
field="modules",
|
||||
project_id=project_id,
|
||||
workspace_id=workspace_id,
|
||||
comment=f"removed this issue from {module.name if module is not None else module_name}",
|
||||
old_identifier=module_id if module_id is not None else None,
|
||||
epoch=epoch,
|
||||
)
|
||||
module_name = current_instance.get("module_name")
|
||||
current_issue = Issue.objects.filter(pk=issue_id).first()
|
||||
if current_issue:
|
||||
current_issue.updated_at = timezone.now()
|
||||
current_issue.save(update_fields=["updated_at"])
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor_id=actor_id,
|
||||
verb="deleted",
|
||||
old_value=module_name,
|
||||
new_value="",
|
||||
field="modules",
|
||||
project_id=project_id,
|
||||
workspace_id=workspace_id,
|
||||
comment=f"removed this issue from {module_name}",
|
||||
old_identifier=requested_data.get("module_id") if requested_data.get("module_id") is not None else None,
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def create_link_activity(
|
||||
@@ -1543,6 +1513,8 @@ def issue_activity(
|
||||
project_id,
|
||||
epoch,
|
||||
subscriber=True,
|
||||
notification=False,
|
||||
origin=None,
|
||||
):
|
||||
try:
|
||||
issue_activities = []
|
||||
@@ -1551,6 +1523,10 @@ def issue_activity(
|
||||
workspace_id = project.workspace_id
|
||||
|
||||
if issue_id is not None:
|
||||
if origin:
|
||||
ri = redis_instance()
|
||||
# set the request origin in redis
|
||||
ri.set(str(issue_id), origin, ex=600)
|
||||
issue = Issue.objects.filter(pk=issue_id).first()
|
||||
if issue:
|
||||
try:
|
||||
@@ -1624,21 +1600,22 @@ def issue_activity(
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
|
||||
notifications.delay(
|
||||
type=type,
|
||||
issue_id=issue_id,
|
||||
actor_id=actor_id,
|
||||
project_id=project_id,
|
||||
subscriber=subscriber,
|
||||
issue_activities_created=json.dumps(
|
||||
IssueActivitySerializer(
|
||||
issue_activities_created, many=True
|
||||
).data,
|
||||
cls=DjangoJSONEncoder,
|
||||
),
|
||||
requested_data=requested_data,
|
||||
current_instance=current_instance,
|
||||
)
|
||||
if notification:
|
||||
notifications.delay(
|
||||
type=type,
|
||||
issue_id=issue_id,
|
||||
actor_id=actor_id,
|
||||
project_id=project_id,
|
||||
subscriber=subscriber,
|
||||
issue_activities_created=json.dumps(
|
||||
IssueActivitySerializer(
|
||||
issue_activities_created, many=True
|
||||
).data,
|
||||
cls=DjangoJSONEncoder,
|
||||
),
|
||||
requested_data=requested_data,
|
||||
current_instance=current_instance,
|
||||
)
|
||||
|
||||
return
|
||||
except Exception as e:
|
||||
|
||||
@@ -87,6 +87,7 @@ def archive_old_issues():
|
||||
current_instance=json.dumps({"archived_at": None}),
|
||||
subscriber=False,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
)
|
||||
for issue in issues_to_update
|
||||
]
|
||||
@@ -169,6 +170,7 @@ def close_old_issues():
|
||||
current_instance=None,
|
||||
subscriber=False,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
)
|
||||
for issue in issues_to_update
|
||||
]
|
||||
|
||||
@@ -10,9 +10,12 @@ from plane.db.models import (
|
||||
User,
|
||||
IssueAssignee,
|
||||
Issue,
|
||||
State,
|
||||
EmailNotificationLog,
|
||||
Notification,
|
||||
IssueComment,
|
||||
IssueActivity,
|
||||
UserNotificationPreference,
|
||||
)
|
||||
|
||||
# Third Party imports
|
||||
@@ -20,7 +23,7 @@ from celery import shared_task
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
|
||||
# =========== Issue Description Html Parsing and Notification Functions ======================
|
||||
# =========== Issue Description Html Parsing and notification Functions ======================
|
||||
|
||||
|
||||
def update_mentions_for_issue(issue, project, new_mentions, removed_mention):
|
||||
@@ -37,9 +40,7 @@ def update_mentions_for_issue(issue, project, new_mentions, removed_mention):
|
||||
)
|
||||
|
||||
IssueMention.objects.bulk_create(aggregated_issue_mentions, batch_size=100)
|
||||
IssueMention.objects.filter(
|
||||
issue=issue, mention__in=removed_mention
|
||||
).delete()
|
||||
IssueMention.objects.filter(issue=issue, mention__in=removed_mention).delete()
|
||||
|
||||
|
||||
def get_new_mentions(requested_instance, current_instance):
|
||||
@@ -60,8 +61,6 @@ def get_new_mentions(requested_instance, current_instance):
|
||||
|
||||
|
||||
# Get Removed Mention
|
||||
|
||||
|
||||
def get_removed_mentions(requested_instance, current_instance):
|
||||
# requested_data is the newer instance of the current issue
|
||||
# current_instance is the older instance of the current issue, saved in the database
|
||||
@@ -79,8 +78,6 @@ def get_removed_mentions(requested_instance, current_instance):
|
||||
|
||||
|
||||
# Adds mentions as subscribers
|
||||
|
||||
|
||||
def extract_mentions_as_subscribers(project_id, issue_id, mentions):
|
||||
# mentions is an array of User IDs representing the FILTERED set of mentioned users
|
||||
|
||||
@@ -95,9 +92,7 @@ def extract_mentions_as_subscribers(project_id, issue_id, mentions):
|
||||
project_id=project_id,
|
||||
).exists()
|
||||
and not IssueAssignee.objects.filter(
|
||||
project_id=project_id,
|
||||
issue_id=issue_id,
|
||||
assignee_id=mention_id,
|
||||
project_id=project_id, issue_id=issue_id, assignee_id=mention_id
|
||||
).exists()
|
||||
and not Issue.objects.filter(
|
||||
project_id=project_id, pk=issue_id, created_by_id=mention_id
|
||||
@@ -125,9 +120,7 @@ def extract_mentions(issue_instance):
|
||||
data = json.loads(issue_instance)
|
||||
html = data.get("description_html")
|
||||
soup = BeautifulSoup(html, "html.parser")
|
||||
mention_tags = soup.find_all(
|
||||
"mention-component", attrs={"target": "users"}
|
||||
)
|
||||
mention_tags = soup.find_all("mention-component", attrs={"target": "users"})
|
||||
|
||||
mentions = [mention_tag["id"] for mention_tag in mention_tags]
|
||||
|
||||
@@ -136,14 +129,12 @@ def extract_mentions(issue_instance):
|
||||
return []
|
||||
|
||||
|
||||
# =========== Comment Parsing and Notification Functions ======================
|
||||
# =========== Comment Parsing and notification Functions ======================
|
||||
def extract_comment_mentions(comment_value):
|
||||
try:
|
||||
mentions = []
|
||||
soup = BeautifulSoup(comment_value, "html.parser")
|
||||
mentions_tags = soup.find_all(
|
||||
"mention-component", attrs={"target": "users"}
|
||||
)
|
||||
mentions_tags = soup.find_all("mention-component", attrs={"target": "users"})
|
||||
for mention_tag in mentions_tags:
|
||||
mentions.append(mention_tag["id"])
|
||||
return list(set(mentions))
|
||||
@@ -165,14 +156,8 @@ def get_new_comment_mentions(new_value, old_value):
|
||||
return new_mentions
|
||||
|
||||
|
||||
def createMentionNotification(
|
||||
project,
|
||||
notification_comment,
|
||||
issue,
|
||||
actor_id,
|
||||
mention_id,
|
||||
issue_id,
|
||||
activity,
|
||||
def create_mention_notification(
|
||||
project, notification_comment, issue, actor_id, mention_id, issue_id, activity
|
||||
):
|
||||
return Notification(
|
||||
workspace=project.workspace,
|
||||
@@ -215,244 +200,199 @@ def notifications(
|
||||
requested_data,
|
||||
current_instance,
|
||||
):
|
||||
issue_activities_created = (
|
||||
json.loads(issue_activities_created)
|
||||
if issue_activities_created is not None
|
||||
else None
|
||||
)
|
||||
if type not in [
|
||||
"issue.activity.deleted",
|
||||
"cycle.activity.created",
|
||||
"cycle.activity.deleted",
|
||||
"module.activity.created",
|
||||
"module.activity.deleted",
|
||||
"issue_reaction.activity.created",
|
||||
"issue_reaction.activity.deleted",
|
||||
"comment_reaction.activity.created",
|
||||
"comment_reaction.activity.deleted",
|
||||
"issue_vote.activity.created",
|
||||
"issue_vote.activity.deleted",
|
||||
"issue_draft.activity.created",
|
||||
"issue_draft.activity.updated",
|
||||
"issue_draft.activity.deleted",
|
||||
]:
|
||||
# Create Notifications
|
||||
bulk_notifications = []
|
||||
|
||||
"""
|
||||
Mention Tasks
|
||||
1. Perform Diffing and Extract the mentions, that mention notification needs to be sent
|
||||
2. From the latest set of mentions, extract the users which are not a subscribers & make them subscribers
|
||||
"""
|
||||
|
||||
# Get new mentions from the newer instance
|
||||
new_mentions = get_new_mentions(
|
||||
requested_instance=requested_data,
|
||||
current_instance=current_instance,
|
||||
)
|
||||
removed_mention = get_removed_mentions(
|
||||
requested_instance=requested_data,
|
||||
current_instance=current_instance,
|
||||
try:
|
||||
issue_activities_created = (
|
||||
json.loads(issue_activities_created)
|
||||
if issue_activities_created is not None
|
||||
else None
|
||||
)
|
||||
if type not in [
|
||||
"issue.activity.deleted",
|
||||
"cycle.activity.created",
|
||||
"cycle.activity.deleted",
|
||||
"module.activity.created",
|
||||
"module.activity.deleted",
|
||||
"issue_reaction.activity.created",
|
||||
"issue_reaction.activity.deleted",
|
||||
"comment_reaction.activity.created",
|
||||
"comment_reaction.activity.deleted",
|
||||
"issue_vote.activity.created",
|
||||
"issue_vote.activity.deleted",
|
||||
"issue_draft.activity.created",
|
||||
"issue_draft.activity.updated",
|
||||
"issue_draft.activity.deleted",
|
||||
]:
|
||||
# Create Notifications
|
||||
bulk_notifications = []
|
||||
bulk_email_logs = []
|
||||
|
||||
comment_mentions = []
|
||||
all_comment_mentions = []
|
||||
"""
|
||||
Mention Tasks
|
||||
1. Perform Diffing and Extract the mentions, that mention notification needs to be sent
|
||||
2. From the latest set of mentions, extract the users which are not a subscribers & make them subscribers
|
||||
"""
|
||||
|
||||
# Get New Subscribers from the mentions of the newer instance
|
||||
requested_mentions = extract_mentions(issue_instance=requested_data)
|
||||
mention_subscribers = extract_mentions_as_subscribers(
|
||||
project_id=project_id,
|
||||
issue_id=issue_id,
|
||||
mentions=requested_mentions,
|
||||
)
|
||||
|
||||
for issue_activity in issue_activities_created:
|
||||
issue_comment = issue_activity.get("issue_comment")
|
||||
issue_comment_new_value = issue_activity.get("new_value")
|
||||
issue_comment_old_value = issue_activity.get("old_value")
|
||||
if issue_comment is not None:
|
||||
# TODO: Maybe save the comment mentions, so that in future, we can filter out the issues based on comment mentions as well.
|
||||
|
||||
all_comment_mentions = (
|
||||
all_comment_mentions
|
||||
+ extract_comment_mentions(issue_comment_new_value)
|
||||
)
|
||||
|
||||
new_comment_mentions = get_new_comment_mentions(
|
||||
old_value=issue_comment_old_value,
|
||||
new_value=issue_comment_new_value,
|
||||
)
|
||||
comment_mentions = comment_mentions + new_comment_mentions
|
||||
|
||||
comment_mention_subscribers = extract_mentions_as_subscribers(
|
||||
project_id=project_id,
|
||||
issue_id=issue_id,
|
||||
mentions=all_comment_mentions,
|
||||
)
|
||||
"""
|
||||
We will not send subscription activity notification to the below mentioned user sets
|
||||
- Those who have been newly mentioned in the issue description, we will send mention notification to them.
|
||||
- When the activity is a comment_created and there exist a mention in the comment, then we have to send the "mention_in_comment" notification
|
||||
- When the activity is a comment_updated and there exist a mention change, then also we have to send the "mention_in_comment" notification
|
||||
"""
|
||||
|
||||
issue_assignees = list(
|
||||
IssueAssignee.objects.filter(
|
||||
project_id=project_id, issue_id=issue_id
|
||||
# Get new mentions from the newer instance
|
||||
new_mentions = get_new_mentions(
|
||||
requested_instance=requested_data,
|
||||
current_instance=current_instance,
|
||||
)
|
||||
.exclude(assignee_id__in=list(new_mentions + comment_mentions))
|
||||
.values_list("assignee", flat=True)
|
||||
)
|
||||
|
||||
issue_subscribers = list(
|
||||
IssueSubscriber.objects.filter(
|
||||
project_id=project_id, issue_id=issue_id
|
||||
removed_mention = get_removed_mentions(
|
||||
requested_instance=requested_data,
|
||||
current_instance=current_instance,
|
||||
)
|
||||
.exclude(
|
||||
subscriber_id__in=list(
|
||||
new_mentions + comment_mentions + [actor_id]
|
||||
)
|
||||
|
||||
comment_mentions = []
|
||||
all_comment_mentions = []
|
||||
|
||||
# Get New Subscribers from the mentions of the newer instance
|
||||
requested_mentions = extract_mentions(
|
||||
issue_instance=requested_data
|
||||
)
|
||||
mention_subscribers = extract_mentions_as_subscribers(
|
||||
project_id=project_id,
|
||||
issue_id=issue_id,
|
||||
mentions=requested_mentions,
|
||||
)
|
||||
.values_list("subscriber", flat=True)
|
||||
)
|
||||
|
||||
issue = Issue.objects.filter(pk=issue_id).first()
|
||||
|
||||
if issue.created_by_id is not None and str(issue.created_by_id) != str(
|
||||
actor_id
|
||||
):
|
||||
issue_subscribers = issue_subscribers + [issue.created_by_id]
|
||||
|
||||
if subscriber:
|
||||
# add the user to issue subscriber
|
||||
try:
|
||||
if (
|
||||
str(issue.created_by_id) != str(actor_id)
|
||||
and uuid.UUID(actor_id) not in issue_assignees
|
||||
):
|
||||
_ = IssueSubscriber.objects.get_or_create(
|
||||
project_id=project_id,
|
||||
issue_id=issue_id,
|
||||
subscriber_id=actor_id,
|
||||
)
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
project = Project.objects.get(pk=project_id)
|
||||
|
||||
issue_subscribers = list(
|
||||
set(issue_subscribers + issue_assignees) - {uuid.UUID(actor_id)}
|
||||
)
|
||||
|
||||
for subscriber in issue_subscribers:
|
||||
if subscriber in issue_subscribers:
|
||||
sender = "in_app:issue_activities:subscribed"
|
||||
if (
|
||||
issue.created_by_id is not None
|
||||
and subscriber == issue.created_by_id
|
||||
):
|
||||
sender = "in_app:issue_activities:created"
|
||||
if subscriber in issue_assignees:
|
||||
sender = "in_app:issue_activities:assigned"
|
||||
|
||||
for issue_activity in issue_activities_created:
|
||||
# Do not send notification for description update
|
||||
if issue_activity.get("field") == "description":
|
||||
continue
|
||||
issue_comment = issue_activity.get("issue_comment")
|
||||
issue_comment_new_value = issue_activity.get("new_value")
|
||||
issue_comment_old_value = issue_activity.get("old_value")
|
||||
if issue_comment is not None:
|
||||
issue_comment = IssueComment.objects.get(
|
||||
id=issue_comment,
|
||||
issue_id=issue_id,
|
||||
project_id=project_id,
|
||||
workspace_id=project.workspace_id,
|
||||
# TODO: Maybe save the comment mentions, so that in future, we can filter out the issues based on comment mentions as well.
|
||||
|
||||
all_comment_mentions = (
|
||||
all_comment_mentions
|
||||
+ extract_comment_mentions(issue_comment_new_value)
|
||||
)
|
||||
|
||||
bulk_notifications.append(
|
||||
Notification(
|
||||
workspace=project.workspace,
|
||||
sender=sender,
|
||||
triggered_by_id=actor_id,
|
||||
receiver_id=subscriber,
|
||||
entity_identifier=issue_id,
|
||||
entity_name="issue",
|
||||
project=project,
|
||||
title=issue_activity.get("comment"),
|
||||
data={
|
||||
"issue": {
|
||||
"id": str(issue_id),
|
||||
"name": str(issue.name),
|
||||
"identifier": str(issue.project.identifier),
|
||||
"sequence_id": issue.sequence_id,
|
||||
"state_name": issue.state.name,
|
||||
"state_group": issue.state.group,
|
||||
},
|
||||
"issue_activity": {
|
||||
"id": str(issue_activity.get("id")),
|
||||
"verb": str(issue_activity.get("verb")),
|
||||
"field": str(issue_activity.get("field")),
|
||||
"actor": str(issue_activity.get("actor_id")),
|
||||
"new_value": str(
|
||||
issue_activity.get("new_value")
|
||||
),
|
||||
"old_value": str(
|
||||
issue_activity.get("old_value")
|
||||
),
|
||||
"issue_comment": str(
|
||||
issue_comment.comment_stripped
|
||||
if issue_activity.get("issue_comment")
|
||||
is not None
|
||||
else ""
|
||||
),
|
||||
},
|
||||
},
|
||||
new_comment_mentions = get_new_comment_mentions(
|
||||
old_value=issue_comment_old_value,
|
||||
new_value=issue_comment_new_value,
|
||||
)
|
||||
comment_mentions = comment_mentions + new_comment_mentions
|
||||
|
||||
comment_mention_subscribers = extract_mentions_as_subscribers(
|
||||
project_id=project_id,
|
||||
issue_id=issue_id,
|
||||
mentions=all_comment_mentions,
|
||||
)
|
||||
"""
|
||||
We will not send subscription activity notification to the below mentioned user sets
|
||||
- Those who have been newly mentioned in the issue description, we will send mention notification to them.
|
||||
- When the activity is a comment_created and there exist a mention in the comment, then we have to send the "mention_in_comment" notification
|
||||
- When the activity is a comment_updated and there exist a mention change, then also we have to send the "mention_in_comment" notification
|
||||
"""
|
||||
|
||||
# ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- #
|
||||
issue_subscribers = list(
|
||||
IssueSubscriber.objects.filter(
|
||||
project_id=project_id, issue_id=issue_id
|
||||
)
|
||||
.exclude(
|
||||
subscriber_id__in=list(
|
||||
new_mentions + comment_mentions + [actor_id]
|
||||
)
|
||||
)
|
||||
.values_list("subscriber", flat=True)
|
||||
)
|
||||
|
||||
# Add Mentioned as Issue Subscribers
|
||||
IssueSubscriber.objects.bulk_create(
|
||||
mention_subscribers + comment_mention_subscribers, batch_size=100
|
||||
)
|
||||
issue = Issue.objects.filter(pk=issue_id).first()
|
||||
|
||||
last_activity = (
|
||||
IssueActivity.objects.filter(issue_id=issue_id)
|
||||
.order_by("-created_at")
|
||||
.first()
|
||||
)
|
||||
|
||||
actor = User.objects.get(pk=actor_id)
|
||||
|
||||
for mention_id in comment_mentions:
|
||||
if mention_id != actor_id:
|
||||
for issue_activity in issue_activities_created:
|
||||
notification = createMentionNotification(
|
||||
project=project,
|
||||
issue=issue,
|
||||
notification_comment=f"{actor.display_name} has mentioned you in a comment in issue {issue.name}",
|
||||
actor_id=actor_id,
|
||||
mention_id=mention_id,
|
||||
issue_id=issue_id,
|
||||
activity=issue_activity,
|
||||
if subscriber:
|
||||
# add the user to issue subscriber
|
||||
try:
|
||||
_ = IssueSubscriber.objects.get_or_create(
|
||||
project_id=project_id, issue_id=issue_id, subscriber_id=actor_id
|
||||
)
|
||||
bulk_notifications.append(notification)
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
for mention_id in new_mentions:
|
||||
if mention_id != actor_id:
|
||||
if (
|
||||
last_activity is not None
|
||||
and last_activity.field == "description"
|
||||
and actor_id == str(last_activity.actor_id)
|
||||
project = Project.objects.get(pk=project_id)
|
||||
|
||||
issue_assignees = IssueAssignee.objects.filter(
|
||||
issue_id=issue_id, project_id=project_id
|
||||
).values_list("assignee", flat=True)
|
||||
|
||||
issue_subscribers = list(
|
||||
set(issue_subscribers) - {uuid.UUID(actor_id)}
|
||||
)
|
||||
|
||||
for subscriber in issue_subscribers:
|
||||
if issue.created_by_id and issue.created_by_id == subscriber:
|
||||
sender = "in_app:issue_activities:created"
|
||||
elif (
|
||||
subscriber in issue_assignees
|
||||
and issue.created_by_id not in issue_assignees
|
||||
):
|
||||
sender = "in_app:issue_activities:assigned"
|
||||
else:
|
||||
sender = "in_app:issue_activities:subscribed"
|
||||
|
||||
preference = UserNotificationPreference.objects.get(
|
||||
user_id=subscriber
|
||||
)
|
||||
|
||||
for issue_activity in issue_activities_created:
|
||||
# If activity done in blocking then blocked by email should not go
|
||||
if issue_activity.get("issue_detail").get("id") != issue_id:
|
||||
continue;
|
||||
|
||||
# Do not send notification for description update
|
||||
if issue_activity.get("field") == "description":
|
||||
continue
|
||||
|
||||
# Check if the value should be sent or not
|
||||
send_email = False
|
||||
if (
|
||||
issue_activity.get("field") == "state"
|
||||
and preference.state_change
|
||||
):
|
||||
send_email = True
|
||||
elif (
|
||||
issue_activity.get("field") == "state"
|
||||
and preference.issue_completed
|
||||
and State.objects.filter(
|
||||
project_id=project_id,
|
||||
pk=issue_activity.get("new_identifier"),
|
||||
group="completed",
|
||||
).exists()
|
||||
):
|
||||
send_email = True
|
||||
elif (
|
||||
issue_activity.get("field") == "comment"
|
||||
and preference.comment
|
||||
):
|
||||
send_email = True
|
||||
elif preference.property_change:
|
||||
send_email = True
|
||||
else:
|
||||
send_email = False
|
||||
|
||||
# If activity is of issue comment fetch the comment
|
||||
issue_comment = (
|
||||
IssueComment.objects.filter(
|
||||
id=issue_activity.get("issue_comment"),
|
||||
issue_id=issue_id,
|
||||
project_id=project_id,
|
||||
workspace_id=project.workspace_id,
|
||||
).first()
|
||||
if issue_activity.get("issue_comment")
|
||||
else None
|
||||
)
|
||||
|
||||
# Create in app notification
|
||||
bulk_notifications.append(
|
||||
Notification(
|
||||
workspace=project.workspace,
|
||||
sender="in_app:issue_activities:mentioned",
|
||||
sender=sender,
|
||||
triggered_by_id=actor_id,
|
||||
receiver_id=mention_id,
|
||||
receiver_id=subscriber,
|
||||
entity_identifier=issue_id,
|
||||
entity_name="issue",
|
||||
project=project,
|
||||
message=f"You have been mentioned in the issue {issue.name}",
|
||||
title=issue_activity.get("comment"),
|
||||
data={
|
||||
"issue": {
|
||||
"id": str(issue_id),
|
||||
@@ -465,36 +405,317 @@ def notifications(
|
||||
"state_group": issue.state.group,
|
||||
},
|
||||
"issue_activity": {
|
||||
"id": str(last_activity.id),
|
||||
"verb": str(last_activity.verb),
|
||||
"field": str(last_activity.field),
|
||||
"actor": str(last_activity.actor_id),
|
||||
"new_value": str(last_activity.new_value),
|
||||
"old_value": str(last_activity.old_value),
|
||||
"id": str(issue_activity.get("id")),
|
||||
"verb": str(issue_activity.get("verb")),
|
||||
"field": str(issue_activity.get("field")),
|
||||
"actor": str(
|
||||
issue_activity.get("actor_id")
|
||||
),
|
||||
"new_value": str(
|
||||
issue_activity.get("new_value")
|
||||
),
|
||||
"old_value": str(
|
||||
issue_activity.get("old_value")
|
||||
),
|
||||
"issue_comment": str(
|
||||
issue_comment.comment_stripped
|
||||
if issue_comment is not None
|
||||
else ""
|
||||
),
|
||||
},
|
||||
},
|
||||
)
|
||||
)
|
||||
else:
|
||||
# Create email notification
|
||||
if send_email:
|
||||
bulk_email_logs.append(
|
||||
EmailNotificationLog(
|
||||
triggered_by_id=actor_id,
|
||||
receiver_id=subscriber,
|
||||
entity_identifier=issue_id,
|
||||
entity_name="issue",
|
||||
data={
|
||||
"issue": {
|
||||
"id": str(issue_id),
|
||||
"name": str(issue.name),
|
||||
"identifier": str(
|
||||
issue.project.identifier
|
||||
),
|
||||
"project_id": str(issue.project.id),
|
||||
"workspace_slug": str(
|
||||
issue.project.workspace.slug
|
||||
),
|
||||
"sequence_id": issue.sequence_id,
|
||||
"state_name": issue.state.name,
|
||||
"state_group": issue.state.group,
|
||||
},
|
||||
"issue_activity": {
|
||||
"id": str(issue_activity.get("id")),
|
||||
"verb": str(
|
||||
issue_activity.get("verb")
|
||||
),
|
||||
"field": str(
|
||||
issue_activity.get("field")
|
||||
),
|
||||
"actor": str(
|
||||
issue_activity.get("actor_id")
|
||||
),
|
||||
"new_value": str(
|
||||
issue_activity.get("new_value")
|
||||
),
|
||||
"old_value": str(
|
||||
issue_activity.get("old_value")
|
||||
),
|
||||
"issue_comment": str(
|
||||
issue_comment.comment_stripped
|
||||
if issue_comment is not None
|
||||
else ""
|
||||
),
|
||||
"activity_time": issue_activity.get("created_at"),
|
||||
},
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
# ----------------------------------------------------------------------------------------------------------------- #
|
||||
|
||||
# Add Mentioned as Issue Subscribers
|
||||
IssueSubscriber.objects.bulk_create(
|
||||
mention_subscribers + comment_mention_subscribers,
|
||||
batch_size=100,
|
||||
ignore_conflicts=True,
|
||||
)
|
||||
|
||||
last_activity = (
|
||||
IssueActivity.objects.filter(issue_id=issue_id)
|
||||
.order_by("-created_at")
|
||||
.first()
|
||||
)
|
||||
|
||||
actor = User.objects.get(pk=actor_id)
|
||||
|
||||
for mention_id in comment_mentions:
|
||||
if mention_id != actor_id:
|
||||
preference = UserNotificationPreference.objects.get(
|
||||
user_id=mention_id
|
||||
)
|
||||
for issue_activity in issue_activities_created:
|
||||
notification = createMentionNotification(
|
||||
notification = create_mention_notification(
|
||||
project=project,
|
||||
issue=issue,
|
||||
notification_comment=f"You have been mentioned in the issue {issue.name}",
|
||||
notification_comment=f"{actor.display_name} has mentioned you in a comment in issue {issue.name}",
|
||||
actor_id=actor_id,
|
||||
mention_id=mention_id,
|
||||
issue_id=issue_id,
|
||||
activity=issue_activity,
|
||||
)
|
||||
|
||||
# check for email notifications
|
||||
if preference.mention:
|
||||
bulk_email_logs.append(
|
||||
EmailNotificationLog(
|
||||
triggered_by_id=actor_id,
|
||||
receiver_id=subscriber,
|
||||
entity_identifier=issue_id,
|
||||
entity_name="issue",
|
||||
data={
|
||||
"issue": {
|
||||
"id": str(issue_id),
|
||||
"name": str(issue.name),
|
||||
"identifier": str(
|
||||
issue.project.identifier
|
||||
),
|
||||
"sequence_id": issue.sequence_id,
|
||||
"state_name": issue.state.name,
|
||||
"state_group": issue.state.group,
|
||||
"project_id": str(
|
||||
issue.project.id
|
||||
),
|
||||
"workspace_slug": str(
|
||||
issue.project.workspace.slug
|
||||
),
|
||||
},
|
||||
"issue_activity": {
|
||||
"id": str(
|
||||
issue_activity.get("id")
|
||||
),
|
||||
"verb": str(
|
||||
issue_activity.get("verb")
|
||||
),
|
||||
"field": str("mention"),
|
||||
"actor": str(
|
||||
issue_activity.get("actor_id")
|
||||
),
|
||||
"new_value": str(
|
||||
issue_activity.get("new_value")
|
||||
),
|
||||
"old_value": str(
|
||||
issue_activity.get("old_value")
|
||||
),
|
||||
},
|
||||
},
|
||||
)
|
||||
)
|
||||
bulk_notifications.append(notification)
|
||||
|
||||
# save new mentions for the particular issue and remove the mentions that has been deleted from the description
|
||||
update_mentions_for_issue(
|
||||
issue=issue,
|
||||
project=project,
|
||||
new_mentions=new_mentions,
|
||||
removed_mention=removed_mention,
|
||||
)
|
||||
for mention_id in new_mentions:
|
||||
if mention_id != actor_id:
|
||||
preference = UserNotificationPreference.objects.get(
|
||||
user_id=mention_id
|
||||
)
|
||||
if (
|
||||
last_activity is not None
|
||||
and last_activity.field == "description"
|
||||
and actor_id == str(last_activity.actor_id)
|
||||
):
|
||||
bulk_notifications.append(
|
||||
Notification(
|
||||
workspace=project.workspace,
|
||||
sender="in_app:issue_activities:mentioned",
|
||||
triggered_by_id=actor_id,
|
||||
receiver_id=mention_id,
|
||||
entity_identifier=issue_id,
|
||||
entity_name="issue",
|
||||
project=project,
|
||||
message=f"You have been mentioned in the issue {issue.name}",
|
||||
data={
|
||||
"issue": {
|
||||
"id": str(issue_id),
|
||||
"name": str(issue.name),
|
||||
"identifier": str(
|
||||
issue.project.identifier
|
||||
),
|
||||
"sequence_id": issue.sequence_id,
|
||||
"state_name": issue.state.name,
|
||||
"state_group": issue.state.group,
|
||||
"project_id": str(issue.project.id),
|
||||
"workspace_slug": str(
|
||||
issue.project.workspace.slug
|
||||
),
|
||||
},
|
||||
"issue_activity": {
|
||||
"id": str(last_activity.id),
|
||||
"verb": str(last_activity.verb),
|
||||
"field": str(last_activity.field),
|
||||
"actor": str(last_activity.actor_id),
|
||||
"new_value": str(
|
||||
last_activity.new_value
|
||||
),
|
||||
"old_value": str(
|
||||
last_activity.old_value
|
||||
),
|
||||
},
|
||||
},
|
||||
)
|
||||
)
|
||||
if preference.mention:
|
||||
bulk_email_logs.append(
|
||||
EmailNotificationLog(
|
||||
triggered_by_id=actor_id,
|
||||
receiver_id=subscriber,
|
||||
entity_identifier=issue_id,
|
||||
entity_name="issue",
|
||||
data={
|
||||
"issue": {
|
||||
"id": str(issue_id),
|
||||
"name": str(issue.name),
|
||||
"identifier": str(
|
||||
issue.project.identifier
|
||||
),
|
||||
"sequence_id": issue.sequence_id,
|
||||
"state_name": issue.state.name,
|
||||
"state_group": issue.state.group,
|
||||
},
|
||||
"issue_activity": {
|
||||
"id": str(last_activity.id),
|
||||
"verb": str(last_activity.verb),
|
||||
"field": "mention",
|
||||
"actor": str(
|
||||
last_activity.actor_id
|
||||
),
|
||||
"new_value": str(
|
||||
last_activity.new_value
|
||||
),
|
||||
"old_value": str(
|
||||
last_activity.old_value
|
||||
),
|
||||
},
|
||||
},
|
||||
)
|
||||
)
|
||||
else:
|
||||
for issue_activity in issue_activities_created:
|
||||
notification = create_mention_notification(
|
||||
project=project,
|
||||
issue=issue,
|
||||
notification_comment=f"You have been mentioned in the issue {issue.name}",
|
||||
actor_id=actor_id,
|
||||
mention_id=mention_id,
|
||||
issue_id=issue_id,
|
||||
activity=issue_activity,
|
||||
)
|
||||
if preference.mention:
|
||||
bulk_email_logs.append(
|
||||
EmailNotificationLog(
|
||||
triggered_by_id=actor_id,
|
||||
receiver_id=subscriber,
|
||||
entity_identifier=issue_id,
|
||||
entity_name="issue",
|
||||
data={
|
||||
"issue": {
|
||||
"id": str(issue_id),
|
||||
"name": str(issue.name),
|
||||
"identifier": str(
|
||||
issue.project.identifier
|
||||
),
|
||||
"sequence_id": issue.sequence_id,
|
||||
"state_name": issue.state.name,
|
||||
"state_group": issue.state.group,
|
||||
},
|
||||
"issue_activity": {
|
||||
"id": str(
|
||||
issue_activity.get("id")
|
||||
),
|
||||
"verb": str(
|
||||
issue_activity.get("verb")
|
||||
),
|
||||
"field": str("mention"),
|
||||
"actor": str(
|
||||
issue_activity.get(
|
||||
"actor_id"
|
||||
)
|
||||
),
|
||||
"new_value": str(
|
||||
issue_activity.get(
|
||||
"new_value"
|
||||
)
|
||||
),
|
||||
"old_value": str(
|
||||
issue_activity.get(
|
||||
"old_value"
|
||||
)
|
||||
),
|
||||
},
|
||||
},
|
||||
)
|
||||
)
|
||||
bulk_notifications.append(notification)
|
||||
|
||||
# Bulk create notifications
|
||||
Notification.objects.bulk_create(bulk_notifications, batch_size=100)
|
||||
# save new mentions for the particular issue and remove the mentions that has been deleted from the description
|
||||
update_mentions_for_issue(
|
||||
issue=issue,
|
||||
project=project,
|
||||
new_mentions=new_mentions,
|
||||
removed_mention=removed_mention,
|
||||
)
|
||||
# Bulk create notifications
|
||||
Notification.objects.bulk_create(
|
||||
bulk_notifications, batch_size=100
|
||||
)
|
||||
EmailNotificationLog.objects.bulk_create(
|
||||
bulk_email_logs, batch_size=100, ignore_conflicts=True
|
||||
)
|
||||
return
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return
|
||||
|
||||
@@ -2,6 +2,7 @@ import os
|
||||
from celery import Celery
|
||||
from plane.settings.redis import redis_instance
|
||||
from celery.schedules import crontab
|
||||
from django.utils.timezone import timedelta
|
||||
|
||||
# Set the default Django settings module for the 'celery' program.
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "plane.settings.production")
|
||||
@@ -28,6 +29,10 @@ app.conf.beat_schedule = {
|
||||
"task": "plane.bgtasks.file_asset_task.delete_file_asset",
|
||||
"schedule": crontab(hour=0, minute=0),
|
||||
},
|
||||
"check-every-five-minutes-to-send-email-notifications": {
|
||||
"task": "plane.bgtasks.email_notification_task.stack_email_notification",
|
||||
"schedule": crontab(minute='*/5')
|
||||
},
|
||||
}
|
||||
|
||||
# Load task modules from all registered Django app configs.
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# wait_for_migrations.py
|
||||
import time
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db.migrations.executor import MigrationExecutor
|
||||
from django.db import connections, DEFAULT_DB_ALIAS
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Wait for database migrations to complete before starting Celery worker/beat'
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
while self._pending_migrations():
|
||||
self.stdout.write("Waiting for database migrations to complete...")
|
||||
time.sleep(10) # wait for 10 seconds before checking again
|
||||
|
||||
self.stdout.write(self.style.SUCCESS("No migrations Pending. Starting processes ..."))
|
||||
|
||||
def _pending_migrations(self):
|
||||
connection = connections[DEFAULT_DB_ALIAS]
|
||||
executor = MigrationExecutor(connection)
|
||||
targets = executor.loader.graph.leaf_nodes()
|
||||
return bool(executor.migration_plan(targets))
|
||||
+184
@@ -0,0 +1,184 @@
|
||||
# Generated by Django 4.2.7 on 2024-01-22 08:55
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("db", "0055_auto_20240108_0648"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="UserNotificationPreference",
|
||||
fields=[
|
||||
(
|
||||
"created_at",
|
||||
models.DateTimeField(
|
||||
auto_now_add=True, verbose_name="Created At"
|
||||
),
|
||||
),
|
||||
(
|
||||
"updated_at",
|
||||
models.DateTimeField(
|
||||
auto_now=True, verbose_name="Last Modified At"
|
||||
),
|
||||
),
|
||||
(
|
||||
"id",
|
||||
models.UUIDField(
|
||||
db_index=True,
|
||||
default=uuid.uuid4,
|
||||
editable=False,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
unique=True,
|
||||
),
|
||||
),
|
||||
("property_change", models.BooleanField(default=True)),
|
||||
("state_change", models.BooleanField(default=True)),
|
||||
("comment", models.BooleanField(default=True)),
|
||||
("mention", models.BooleanField(default=True)),
|
||||
("issue_completed", models.BooleanField(default=True)),
|
||||
(
|
||||
"created_by",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="%(class)s_created_by",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
verbose_name="Created By",
|
||||
),
|
||||
),
|
||||
(
|
||||
"project",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="project_notification_preferences",
|
||||
to="db.project",
|
||||
),
|
||||
),
|
||||
(
|
||||
"updated_by",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="%(class)s_updated_by",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
verbose_name="Last Modified By",
|
||||
),
|
||||
),
|
||||
(
|
||||
"user",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="notification_preferences",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
(
|
||||
"workspace",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="workspace_notification_preferences",
|
||||
to="db.workspace",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "UserNotificationPreference",
|
||||
"verbose_name_plural": "UserNotificationPreferences",
|
||||
"db_table": "user_notification_preferences",
|
||||
"ordering": ("-created_at",),
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="EmailNotificationLog",
|
||||
fields=[
|
||||
(
|
||||
"created_at",
|
||||
models.DateTimeField(
|
||||
auto_now_add=True, verbose_name="Created At"
|
||||
),
|
||||
),
|
||||
(
|
||||
"updated_at",
|
||||
models.DateTimeField(
|
||||
auto_now=True, verbose_name="Last Modified At"
|
||||
),
|
||||
),
|
||||
(
|
||||
"id",
|
||||
models.UUIDField(
|
||||
db_index=True,
|
||||
default=uuid.uuid4,
|
||||
editable=False,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
unique=True,
|
||||
),
|
||||
),
|
||||
("entity_identifier", models.UUIDField(null=True)),
|
||||
("entity_name", models.CharField(max_length=255)),
|
||||
("data", models.JSONField(null=True)),
|
||||
("processed_at", models.DateTimeField(null=True)),
|
||||
("sent_at", models.DateTimeField(null=True)),
|
||||
("entity", models.CharField(max_length=200)),
|
||||
(
|
||||
"old_value",
|
||||
models.CharField(blank=True, max_length=300, null=True),
|
||||
),
|
||||
(
|
||||
"new_value",
|
||||
models.CharField(blank=True, max_length=300, null=True),
|
||||
),
|
||||
(
|
||||
"created_by",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="%(class)s_created_by",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
verbose_name="Created By",
|
||||
),
|
||||
),
|
||||
(
|
||||
"receiver",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="email_notifications",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
(
|
||||
"triggered_by",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="triggered_emails",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
(
|
||||
"updated_by",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="%(class)s_updated_by",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
verbose_name="Last Modified By",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Email Notification Log",
|
||||
"verbose_name_plural": "Email Notification Logs",
|
||||
"db_table": "email_notification_logs",
|
||||
"ordering": ("-created_at",),
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,28 @@
|
||||
# Generated by Django 4.2.7 on 2024-01-22 09:01
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
def create_notification_preferences(apps, schema_editor):
|
||||
UserNotificationPreference = apps.get_model("db", "UserNotificationPreference")
|
||||
User = apps.get_model("db", "User")
|
||||
|
||||
bulk_notification_preferences = []
|
||||
for user_id in User.objects.filter(is_bot=False).values_list("id", flat=True):
|
||||
bulk_notification_preferences.append(
|
||||
UserNotificationPreference(
|
||||
user_id=user_id,
|
||||
created_by_id=user_id,
|
||||
)
|
||||
)
|
||||
UserNotificationPreference.objects.bulk_create(
|
||||
bulk_notification_preferences, batch_size=1000, ignore_conflicts=True
|
||||
)
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("db", "0056_usernotificationpreference_emailnotificationlog"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(create_notification_preferences)
|
||||
]
|
||||
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 4.2.7 on 2024-01-24 18:55
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('db', '0057_auto_20240122_0901'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='moduleissue',
|
||||
name='issue',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_module', to='db.issue'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='moduleissue',
|
||||
unique_together={('issue', 'module')},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,213 @@
|
||||
# Generated by Django 4.2.7 on 2024-01-26 12:35
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
import plane.db.models.user
|
||||
|
||||
|
||||
def migrate_user_extra_profile(apps, schema_editor):
|
||||
Profile = apps.get_model("db", "Profile")
|
||||
User = apps.get_model("db", "User")
|
||||
|
||||
Profile.objects.bulk_create(
|
||||
[
|
||||
Profile(
|
||||
user_id=user.get("id"),
|
||||
theme=user.get("theme"),
|
||||
is_tour_completed=user.get("is_tour_completed"),
|
||||
use_case=user.get("use_case"),
|
||||
is_onboarded=user.get("is_onboarded"),
|
||||
last_workspace_id=user.get("last_workspace_id"),
|
||||
billing_address_country=user.get("billing_address_country"),
|
||||
billing_address=user.get("billing_address"),
|
||||
has_billing_address=user.get("has_billing_address"),
|
||||
)
|
||||
for user in User.objects.values(
|
||||
"id",
|
||||
"theme",
|
||||
"is_tour_completed",
|
||||
"onboarding_step",
|
||||
"use_case",
|
||||
"role",
|
||||
"is_onboarded",
|
||||
"last_workspace_id",
|
||||
"billing_address_country",
|
||||
"billing_address",
|
||||
"has_billing_address",
|
||||
)
|
||||
],
|
||||
batch_size=1000,
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("db", "0058_alter_moduleissue_issue_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="Profile",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"created_at",
|
||||
models.DateTimeField(
|
||||
auto_now_add=True, verbose_name="Created At"
|
||||
),
|
||||
),
|
||||
(
|
||||
"updated_at",
|
||||
models.DateTimeField(
|
||||
auto_now=True, verbose_name="Last Modified At"
|
||||
),
|
||||
),
|
||||
("theme", models.JSONField(default=dict)),
|
||||
("is_tour_completed", models.BooleanField(default=False)),
|
||||
(
|
||||
"onboarding_step",
|
||||
models.JSONField(
|
||||
default=plane.db.models.user.get_default_onboarding
|
||||
),
|
||||
),
|
||||
("use_case", models.TextField(blank=True, null=True)),
|
||||
(
|
||||
"role",
|
||||
models.CharField(blank=True, max_length=300, null=True),
|
||||
),
|
||||
("is_onboarded", models.BooleanField(default=False)),
|
||||
("last_workspace_id", models.UUIDField(null=True)),
|
||||
(
|
||||
"billing_address_country",
|
||||
models.CharField(default="INDIA", max_length=255),
|
||||
),
|
||||
("billing_address", models.JSONField(null=True)),
|
||||
("has_billing_address", models.BooleanField(default=False)),
|
||||
(
|
||||
"user",
|
||||
models.OneToOneField(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="profile",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Profile",
|
||||
"verbose_name_plural": "Profiles",
|
||||
"db_table": "profiles",
|
||||
"ordering": ("-created_at",),
|
||||
},
|
||||
),
|
||||
migrations.RunPython(migrate_user_extra_profile),
|
||||
migrations.RemoveField(
|
||||
model_name="user",
|
||||
name="billing_address",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="user",
|
||||
name="billing_address_country",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="user",
|
||||
name="has_billing_address",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="user",
|
||||
name="is_onboarded",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="user",
|
||||
name="is_tour_completed",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="user",
|
||||
name="last_workspace_id",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="user",
|
||||
name="my_issues_prop",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="user",
|
||||
name="onboarding_step",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="user",
|
||||
name="role",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="user",
|
||||
name="theme",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="user",
|
||||
name="use_case",
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Account",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"created_at",
|
||||
models.DateTimeField(
|
||||
auto_now_add=True, verbose_name="Created At"
|
||||
),
|
||||
),
|
||||
(
|
||||
"updated_at",
|
||||
models.DateTimeField(
|
||||
auto_now=True, verbose_name="Last Modified At"
|
||||
),
|
||||
),
|
||||
("provider_account_id", models.CharField(max_length=255)),
|
||||
(
|
||||
"provider",
|
||||
models.CharField(
|
||||
choices=[("google", "Google"), ("github", "Github")]
|
||||
),
|
||||
),
|
||||
("access_token", models.TextField()),
|
||||
("access_token_expired_at", models.DateTimeField(null=True)),
|
||||
("refresh_token", models.TextField(blank=True, null=True)),
|
||||
("refresh_token_expired_at", models.DateTimeField(null=True)),
|
||||
(
|
||||
"last_connected_at",
|
||||
models.DateTimeField(default=django.utils.timezone.now),
|
||||
),
|
||||
("metadata", models.JSONField(default=dict)),
|
||||
(
|
||||
"user",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="accounts",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Account",
|
||||
"verbose_name_plural": "Accounts",
|
||||
"db_table": "accounts",
|
||||
"ordering": ("-created_at",),
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -1,6 +1,6 @@
|
||||
from .base import BaseModel
|
||||
|
||||
from .user import User
|
||||
from .user import User, Profile, Account
|
||||
|
||||
from .workspace import (
|
||||
Workspace,
|
||||
@@ -85,7 +85,7 @@ from .inbox import Inbox, InboxIssue
|
||||
|
||||
from .analytic import AnalyticView
|
||||
|
||||
from .notification import Notification
|
||||
from .notification import Notification, UserNotificationPreference, EmailNotificationLog
|
||||
|
||||
from .exporter import ExporterHistory
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils import timezone
|
||||
|
||||
# Module imports
|
||||
from . import ProjectBaseModel
|
||||
@@ -183,6 +184,17 @@ class Issue(ProjectBaseModel):
|
||||
self.state = default_state
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
from plane.db.models import State
|
||||
|
||||
# Check if the current issue state group is completed or not
|
||||
if self.state.group == "completed":
|
||||
self.completed_at = timezone.now()
|
||||
else:
|
||||
self.completed_at = None
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
if self._state.adding:
|
||||
# Get the maximum display_id value from the database
|
||||
|
||||
@@ -134,11 +134,12 @@ class ModuleIssue(ProjectBaseModel):
|
||||
module = models.ForeignKey(
|
||||
"db.Module", on_delete=models.CASCADE, related_name="issue_module"
|
||||
)
|
||||
issue = models.OneToOneField(
|
||||
issue = models.ForeignKey(
|
||||
"db.Issue", on_delete=models.CASCADE, related_name="issue_module"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["issue", "module"]
|
||||
verbose_name = "Module Issue"
|
||||
verbose_name_plural = "Module Issues"
|
||||
db_table = "module_issues"
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# Django imports
|
||||
from django.db import models
|
||||
from django.conf import settings
|
||||
|
||||
# Third party imports
|
||||
from .base import BaseModel
|
||||
|
||||
# Module imports
|
||||
from . import BaseModel
|
||||
|
||||
class Notification(BaseModel):
|
||||
workspace = models.ForeignKey(
|
||||
@@ -47,3 +47,82 @@ class Notification(BaseModel):
|
||||
def __str__(self):
|
||||
"""Return name of the notifications"""
|
||||
return f"{self.receiver.email} <{self.workspace.name}>"
|
||||
|
||||
|
||||
def get_default_preference():
|
||||
return {
|
||||
"property_change": {
|
||||
"email": True,
|
||||
},
|
||||
"state": {
|
||||
"email": True,
|
||||
},
|
||||
"comment": {
|
||||
"email": True,
|
||||
},
|
||||
"mentions": {
|
||||
"email": True,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class UserNotificationPreference(BaseModel):
|
||||
# user it is related to
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="notification_preferences",
|
||||
)
|
||||
# workspace if it is applicable
|
||||
workspace = models.ForeignKey(
|
||||
"db.Workspace",
|
||||
on_delete=models.CASCADE,
|
||||
related_name="workspace_notification_preferences",
|
||||
null=True,
|
||||
)
|
||||
# project
|
||||
project = models.ForeignKey(
|
||||
"db.Project",
|
||||
on_delete=models.CASCADE,
|
||||
related_name="project_notification_preferences",
|
||||
null=True,
|
||||
)
|
||||
|
||||
# preference fields
|
||||
property_change = models.BooleanField(default=True)
|
||||
state_change = models.BooleanField(default=True)
|
||||
comment = models.BooleanField(default=True)
|
||||
mention = models.BooleanField(default=True)
|
||||
issue_completed = models.BooleanField(default=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "UserNotificationPreference"
|
||||
verbose_name_plural = "UserNotificationPreferences"
|
||||
db_table = "user_notification_preferences"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def __str__(self):
|
||||
"""Return the user"""
|
||||
return f"<{self.user}>"
|
||||
|
||||
class EmailNotificationLog(BaseModel):
|
||||
# receiver
|
||||
receiver = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="email_notifications")
|
||||
triggered_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="triggered_emails")
|
||||
# entity - can be issues, pages, etc.
|
||||
entity_identifier = models.UUIDField(null=True)
|
||||
entity_name = models.CharField(max_length=255)
|
||||
# data
|
||||
data = models.JSONField(null=True)
|
||||
# sent at
|
||||
processed_at = models.DateTimeField(null=True)
|
||||
sent_at = models.DateTimeField(null=True)
|
||||
entity = models.CharField(max_length=200)
|
||||
old_value = models.CharField(max_length=300, blank=True, null=True)
|
||||
new_value = models.CharField(max_length=300, blank=True, null=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Email Notification Log"
|
||||
verbose_name_plural = "Email Notification Logs"
|
||||
db_table = "email_notification_logs"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
@@ -11,8 +11,19 @@ from django.contrib.auth.models import (
|
||||
UserManager,
|
||||
PermissionsMixin,
|
||||
)
|
||||
from django.db.models.signals import post_save
|
||||
from django.conf import settings
|
||||
from django.dispatch import receiver
|
||||
from django.utils import timezone
|
||||
|
||||
# Third party imports
|
||||
from sentry_sdk import capture_exception
|
||||
from slack_sdk import WebClient
|
||||
from slack_sdk.errors import SlackApiError
|
||||
|
||||
# Module imports
|
||||
from ..mixins import TimeAuditModel
|
||||
|
||||
|
||||
def get_default_onboarding():
|
||||
return {
|
||||
@@ -32,12 +43,14 @@ class User(AbstractBaseUser, PermissionsMixin):
|
||||
primary_key=True,
|
||||
)
|
||||
username = models.CharField(max_length=128, unique=True)
|
||||
|
||||
# user fields
|
||||
mobile_number = models.CharField(max_length=255, blank=True, null=True)
|
||||
email = models.CharField(
|
||||
max_length=255, null=True, blank=True, unique=True
|
||||
)
|
||||
|
||||
# identity
|
||||
display_name = models.CharField(max_length=255, default="")
|
||||
first_name = models.CharField(max_length=255, blank=True)
|
||||
last_name = models.CharField(max_length=255, blank=True)
|
||||
avatar = models.CharField(max_length=255, blank=True)
|
||||
@@ -64,19 +77,10 @@ class User(AbstractBaseUser, PermissionsMixin):
|
||||
is_staff = models.BooleanField(default=False)
|
||||
is_email_verified = models.BooleanField(default=False)
|
||||
is_password_autoset = models.BooleanField(default=False)
|
||||
is_onboarded = models.BooleanField(default=False)
|
||||
|
||||
# random token generated
|
||||
token = models.CharField(max_length=64, blank=True)
|
||||
|
||||
billing_address_country = models.CharField(max_length=255, default="INDIA")
|
||||
billing_address = models.JSONField(null=True)
|
||||
has_billing_address = models.BooleanField(default=False)
|
||||
|
||||
USER_TIMEZONE_CHOICES = tuple(zip(pytz.all_timezones, pytz.all_timezones))
|
||||
user_timezone = models.CharField(
|
||||
max_length=255, default="UTC", choices=USER_TIMEZONE_CHOICES
|
||||
)
|
||||
|
||||
last_active = models.DateTimeField(default=timezone.now, null=True)
|
||||
last_login_time = models.DateTimeField(null=True)
|
||||
last_logout_time = models.DateTimeField(null=True)
|
||||
@@ -88,18 +92,17 @@ class User(AbstractBaseUser, PermissionsMixin):
|
||||
)
|
||||
last_login_uagent = models.TextField(blank=True)
|
||||
token_updated_at = models.DateTimeField(null=True)
|
||||
last_workspace_id = models.UUIDField(null=True)
|
||||
my_issues_prop = models.JSONField(null=True)
|
||||
role = models.CharField(max_length=300, null=True, blank=True)
|
||||
# my_issues_prop = models.JSONField(null=True)
|
||||
|
||||
is_bot = models.BooleanField(default=False)
|
||||
theme = models.JSONField(default=dict)
|
||||
display_name = models.CharField(max_length=255, default="")
|
||||
is_tour_completed = models.BooleanField(default=False)
|
||||
onboarding_step = models.JSONField(default=get_default_onboarding)
|
||||
use_case = models.TextField(blank=True, null=True)
|
||||
|
||||
# timezone
|
||||
USER_TIMEZONE_CHOICES = tuple(zip(pytz.all_timezones, pytz.all_timezones))
|
||||
user_timezone = models.CharField(
|
||||
max_length=255, default="UTC", choices=USER_TIMEZONE_CHOICES
|
||||
)
|
||||
|
||||
USERNAME_FIELD = "email"
|
||||
|
||||
REQUIRED_FIELDS = ["username"]
|
||||
|
||||
objects = UserManager()
|
||||
@@ -134,3 +137,89 @@ class User(AbstractBaseUser, PermissionsMixin):
|
||||
self.is_staff = True
|
||||
|
||||
super(User, self).save(*args, **kwargs)
|
||||
|
||||
|
||||
|
||||
class Profile(TimeAuditModel):
|
||||
# User
|
||||
user = models.OneToOneField("db.User", on_delete=models.CASCADE, related_name="profile")
|
||||
|
||||
# General
|
||||
theme = models.JSONField(default=dict)
|
||||
|
||||
# Onboarding
|
||||
is_tour_completed = models.BooleanField(default=False)
|
||||
onboarding_step = models.JSONField(default=get_default_onboarding)
|
||||
use_case = models.TextField(blank=True, null=True)
|
||||
role = models.CharField(max_length=300, null=True, blank=True) # job role
|
||||
is_onboarded = models.BooleanField(default=False)
|
||||
|
||||
# Last visited workspace
|
||||
last_workspace_id = models.UUIDField(null=True)
|
||||
|
||||
# address data
|
||||
billing_address_country = models.CharField(max_length=255, default="INDIA")
|
||||
billing_address = models.JSONField(null=True)
|
||||
has_billing_address = models.BooleanField(default=False)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Profile"
|
||||
verbose_name_plural = "Profiles"
|
||||
db_table = "profiles"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
|
||||
|
||||
class Account(TimeAuditModel):
|
||||
user = models.ForeignKey(
|
||||
"db.User", on_delete=models.CASCADE, related_name="accounts"
|
||||
)
|
||||
provider_account_id = models.CharField(max_length=255)
|
||||
provider = models.CharField(
|
||||
choices=(("google", "Google"), ("github", "Github"))
|
||||
)
|
||||
access_token = models.TextField()
|
||||
access_token_expired_at = models.DateTimeField(null=True)
|
||||
refresh_token = models.TextField(null=True, blank=True)
|
||||
refresh_token_expired_at = models.DateTimeField(null=True)
|
||||
last_connected_at = models.DateTimeField(default=timezone.now)
|
||||
metadata = models.JSONField(default=dict)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Account"
|
||||
verbose_name_plural = "Accounts"
|
||||
db_table = "accounts"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
|
||||
|
||||
@receiver(post_save, sender=User)
|
||||
def send_welcome_slack(sender, instance, created, **kwargs):
|
||||
try:
|
||||
if created and not instance.is_bot:
|
||||
# Send message on slack as well
|
||||
if settings.SLACK_BOT_TOKEN:
|
||||
client = WebClient(token=settings.SLACK_BOT_TOKEN)
|
||||
try:
|
||||
_ = client.chat_postMessage(
|
||||
channel="#trackers",
|
||||
text=f"New user {instance.email} has signed up and begun the onboarding journey.",
|
||||
)
|
||||
except SlackApiError as e:
|
||||
print(f"Got an error: {e.response['error']}")
|
||||
return
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
return
|
||||
|
||||
|
||||
@receiver(post_save, sender=User)
|
||||
def create_user_notification(sender, instance, created, **kwargs):
|
||||
# create preferences
|
||||
if created and not instance.is_bot:
|
||||
# Module imports
|
||||
from plane.db.models import UserNotificationPreference
|
||||
|
||||
UserNotificationPreference.objects.create(
|
||||
user=instance,
|
||||
)
|
||||
@@ -291,6 +291,7 @@ CELERY_IMPORTS = (
|
||||
"plane.bgtasks.issue_automation_task",
|
||||
"plane.bgtasks.exporter_expired_task",
|
||||
"plane.bgtasks.file_asset_task",
|
||||
"plane.bgtasks.email_notification_task",
|
||||
)
|
||||
|
||||
# Sentry Settings
|
||||
|
||||
@@ -4,6 +4,7 @@ from datetime import timedelta
|
||||
|
||||
# Django import
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from django.db.models.functions import TruncDate
|
||||
from django.db.models import Count, F, Sum, Value, Case, When, CharField
|
||||
from django.db.models.functions import (
|
||||
@@ -168,6 +169,9 @@ def burndown_plot(queryset, slug, project_id, cycle_id=None, module_id=None):
|
||||
if item["date"] is not None and item["date"] <= date
|
||||
)
|
||||
cumulative_pending_issues -= total_completed
|
||||
chart_data[str(date)] = cumulative_pending_issues
|
||||
if date > timezone.now().date():
|
||||
chart_data[str(date)] = None
|
||||
else:
|
||||
chart_data[str(date)] = cumulative_pending_issues
|
||||
|
||||
return chart_data
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
python-3.11.6
|
||||
python-3.11.7
|
||||
@@ -0,0 +1,978 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||
<html
|
||||
xmlns="http://www.w3.org/1999/xhtml"
|
||||
xmlns:v="urn:schemas-microsoft-com:vml"
|
||||
xmlns:o="urn:schemas-microsoft-com:office:office"
|
||||
>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Updates on issue</title>
|
||||
<style type="text/css" emogrify="no">
|
||||
html {
|
||||
font-family: system-ui;
|
||||
}
|
||||
p,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
ol,
|
||||
ul {
|
||||
margin: 0;
|
||||
}
|
||||
h-full {
|
||||
height: 100%;
|
||||
}
|
||||
a:hover {
|
||||
color: #3358d4 !important;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body
|
||||
bgcolor="#ffffff"
|
||||
text="#3b3f44"
|
||||
link="#3f76ff"
|
||||
yahoo="fix"
|
||||
style="background-color: #f7f9ff; margin: 20px"
|
||||
>
|
||||
<div
|
||||
style="
|
||||
width: 600px;
|
||||
table-layout: fixed;
|
||||
height: 100%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
"
|
||||
>
|
||||
<!-- Header -->
|
||||
<div>
|
||||
<table style="width: 600px" cellspacing="0">
|
||||
<tr>
|
||||
<td>
|
||||
<div
|
||||
style="margin-left: 30px; margin-bottom: 20px; margin-top: 20px"
|
||||
>
|
||||
<img
|
||||
src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/plane-logo.webp"
|
||||
width="130"
|
||||
height="40"
|
||||
border="0"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!-- Body -->
|
||||
<div
|
||||
style="
|
||||
color: #1f2d5c;
|
||||
padding: 30px;
|
||||
border-radius: 4px;
|
||||
background-color: #fcfcfd;
|
||||
max-width: 100%;
|
||||
"
|
||||
>
|
||||
<div>
|
||||
<table style="width: 100%">
|
||||
<tr>
|
||||
<td>
|
||||
<p style="font-size: 1rem; color: #1f2d5c; font-weight: 600">
|
||||
{{ issue.issue_identifier }} updates
|
||||
</p>
|
||||
<p style="font-size: 1rem; font-weight: 500; color: #1f2d5c; line-height: 28px">
|
||||
{{ issue.name }}: {{ issue.issue_identifier }}
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<hr
|
||||
style="
|
||||
background-color: #f0f0f3;
|
||||
height: 1px;
|
||||
border: 0;
|
||||
margin-top: 15px;
|
||||
margin-bottom: 15px;
|
||||
"
|
||||
/>
|
||||
|
||||
<p style="font-size: 1rem;color: #1f2d5c; line-height: 28pxl">
|
||||
{% if data.1 %}{{ data|length }}{% endif %} {{ summary }}
|
||||
<span style="font-size: 1rem; font-weight: 700; line-height: 28px">
|
||||
{{ data.0.actor_detail.first_name}}
|
||||
{{data.0.actor_detail.last_name }}
|
||||
</span>
|
||||
</p>
|
||||
{% if comments.0 %}
|
||||
<p style="font-size: 1rem;color: #1f2d5c; line-height: 28px">
|
||||
{{ comments|length }} {% if comments|length == 1 %}comment was{% else %}comments were{% endif %} left by
|
||||
<span style="font-size: 1rem; font-weight: 700; line-height: 28px">
|
||||
{% if comments|length == 1 %}
|
||||
{{ data.0.actor_detail.first_name }}
|
||||
{{ data.0.actor_detail.last_name }}
|
||||
{% else %}
|
||||
{{ data.0.actor_detail.first_name }}
|
||||
{{ data.0.actor_detail.last_name }} and others
|
||||
{% endif %}
|
||||
</span>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if mentions and comments.0 and data.0 %}
|
||||
<p style="font-size: 1rem;color: #1f2d5c; line-height: 28px">
|
||||
There are 3 new updates, added 1 new comment and, you were
|
||||
<span style="font-size: 1rem; font-weight: 700; line-height: 28px">
|
||||
@{{ data.0.actor_detail.first_name}}
|
||||
{{data.0.actor_detail.last_name }}
|
||||
</span>
|
||||
mentioned a comment of this issue.
|
||||
</p>
|
||||
{% endif %}
|
||||
{% for update in data %} {% if update.changes.name %}
|
||||
<!-- Issue title updated -->
|
||||
<p style="font-size: 1rem; line-height: 28px; color: #1f2d5c">
|
||||
The issue title has been updated from “{{update.changes.user.old_value.0}}“ to "{{update.changes.user.new_value|last}}"
|
||||
</p>
|
||||
{% endif %}
|
||||
<!-- Outer update Box start -->
|
||||
{% if data %}
|
||||
<div
|
||||
style="
|
||||
background-color: #f7f9ff;
|
||||
border-radius: 8px;
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-color: #c1d0ff;
|
||||
padding: 20px;
|
||||
margin-top: 15px;
|
||||
max-width: 100%;
|
||||
"
|
||||
>
|
||||
<!-- Block Heading -->
|
||||
<div style="padding-bottom: 20px">
|
||||
<p style="font-size: 0.8rem; font-weight: 600; color: #121a26">
|
||||
Updates
|
||||
</p>
|
||||
</div>
|
||||
<!-- Property Updates -->
|
||||
<div
|
||||
style="
|
||||
background-color: white;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
padding-left: 15px;
|
||||
padding-bottom: 15px;
|
||||
"
|
||||
>
|
||||
<!-- action performer -->
|
||||
<table
|
||||
role="presentation"
|
||||
cellspacing="0"
|
||||
cellpadding="0"
|
||||
style="max-width: 100%"
|
||||
>
|
||||
<tr style="border-radius: 8px; margin-top: 20px">
|
||||
<td style="width: 30px">
|
||||
{% if update.actor_detail.avatar_url %}
|
||||
<img src="{{ update.actor_detail.avatar_url }}" width="25" height="25" border="0" />
|
||||
{% else %}
|
||||
<table cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td
|
||||
style="
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
background-color: #4f3422;
|
||||
border-radius: 50%;
|
||||
text-align: center;
|
||||
"
|
||||
>
|
||||
<span
|
||||
style="
|
||||
color: white;
|
||||
font-weight: 500;
|
||||
font-size: 13px;
|
||||
line-height: 25px;
|
||||
display: inline-block;
|
||||
"
|
||||
>
|
||||
{{ update.actor_detail.first_name.0 }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td style="padding-top: 20px; padding-bottom: 20px">
|
||||
<p
|
||||
style="
|
||||
font-weight: 500;
|
||||
font-size: 0.8rem;
|
||||
color: #1c2024;
|
||||
width: fit-content;
|
||||
margin-left: 5px;
|
||||
"
|
||||
>
|
||||
{{ update.actor_detail.first_name }} {{ update.actor_detail.last_name }}
|
||||
</p>
|
||||
</td>
|
||||
<td>
|
||||
<p
|
||||
style="
|
||||
font-weight: 500;
|
||||
font-size: 0.6rem;
|
||||
color: #80838d;
|
||||
margin-left: 10px;
|
||||
"
|
||||
>
|
||||
{{ update.activity_time }}
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% if update.changes.target_date %}
|
||||
<!-- due date changed -->
|
||||
<table
|
||||
role="presentation"
|
||||
style="max-width: 100%; padding-bottom: 15px"
|
||||
>
|
||||
<tr>
|
||||
<td>
|
||||
<img
|
||||
src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/due-date.webp"
|
||||
width="12"
|
||||
height="12"
|
||||
border="0"
|
||||
style="display: block"
|
||||
/>
|
||||
</td>
|
||||
<td style="padding: 0px; text-align: center">
|
||||
<div>
|
||||
<p
|
||||
style="
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
color: #525252;
|
||||
"
|
||||
>
|
||||
Due Date:
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<p
|
||||
style="
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
color: #171717;
|
||||
margin-left: 5px;
|
||||
padding: 0px;
|
||||
"
|
||||
>
|
||||
{{ update.changes.target_date.new_value.0 }}
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% endif %} {% if update.changes.duplicate %}
|
||||
<!-- duplicate changed -->
|
||||
<table
|
||||
role="presentation"
|
||||
style="max-width: 100%; padding-bottom: 15px"
|
||||
>
|
||||
<tr>
|
||||
<td style="overflow-wrap: break-word;">
|
||||
<img
|
||||
src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/duplicate.webp"
|
||||
width="12"
|
||||
height="12"
|
||||
border="0"
|
||||
style="display: inline-block"
|
||||
/>
|
||||
<span
|
||||
style="
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
color: #525252;
|
||||
"
|
||||
>
|
||||
Duplicate:
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td style="padding-left: 5px;">
|
||||
{% for duplicate in update.changes.duplicate.new_value %}
|
||||
<span
|
||||
style="
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
color: #3a5bc7;
|
||||
margin-right: 3px;
|
||||
padding-top: 0px;
|
||||
"
|
||||
>
|
||||
{{ duplicate }}
|
||||
</span>
|
||||
{% endfor %}
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
</table>
|
||||
{% endif %}
|
||||
<!-- Assignee changed-->
|
||||
{% if update.changes.assignees %}
|
||||
<table
|
||||
role="presentation"
|
||||
style="max-width: 100%; padding-bottom: 15px"
|
||||
>
|
||||
<tr>
|
||||
<td>
|
||||
<img
|
||||
src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/assignee.webp"
|
||||
width="12"
|
||||
height="12"
|
||||
border="0"
|
||||
style="display: block"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<p
|
||||
style="
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
color: #525252;
|
||||
"
|
||||
>
|
||||
Assignees:
|
||||
</p>
|
||||
</td>
|
||||
{% if update.changes.assignees.new_value.0 %}
|
||||
<td>
|
||||
<p
|
||||
style="
|
||||
font-size: 0.8rem;
|
||||
color: #0d74ce;
|
||||
background-color: #e6f4fe;
|
||||
margin-left: 5px;
|
||||
padding-left: 4px;
|
||||
padding-right: 4px;
|
||||
padding-top: 2px;
|
||||
padding-bottom: 2px;
|
||||
font-weight: 500;
|
||||
border-radius: 4px;
|
||||
"
|
||||
>
|
||||
{{ update.changes.assignees.new_value.0 }}
|
||||
</p>
|
||||
</td>
|
||||
{% endif %} {% if update.changes.assignees.new_value.1 %}
|
||||
<td>
|
||||
<p
|
||||
style="
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
color: #0d74ce;
|
||||
margin-left: 2px;
|
||||
"
|
||||
>
|
||||
+{{ update.changes.assignees.new_value|length|add:"-1"}}
|
||||
more
|
||||
</p>
|
||||
</td>
|
||||
{% endif %} {% if update.changes.assignees.old_value.0 %}
|
||||
<td>
|
||||
<p
|
||||
style="
|
||||
font-size: 0.8rem;
|
||||
padding-left: 4px;
|
||||
padding-right: 4px;
|
||||
padding-top: 2px;
|
||||
padding-bottom: 2px;
|
||||
font-weight: 500;
|
||||
text-decoration: line-through;
|
||||
color: #641723;
|
||||
background-color: #feebec;
|
||||
margin-left: 5px;
|
||||
border-radius: 4px;
|
||||
"
|
||||
>
|
||||
{{update.changes.assignees.old_value.0}}
|
||||
</p>
|
||||
</td>
|
||||
{% endif %} {% if update.changes.assignees.old_value.1 %}
|
||||
<td>
|
||||
<p
|
||||
style="
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
color: #641723;
|
||||
margin-left: 2px;
|
||||
"
|
||||
>
|
||||
+{{ update.changes.assignees.old_value|length|add:"-1"}}
|
||||
more
|
||||
</p>
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</table>
|
||||
{% endif %} {% if update.changes.labels %}
|
||||
<!-- Labels -->
|
||||
<table
|
||||
role="presentation"
|
||||
style="padding-bottom: 15px; max-width: 100%"
|
||||
>
|
||||
<tr>
|
||||
<td>
|
||||
<img
|
||||
src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/labels.webp"
|
||||
width="12"
|
||||
height="12"
|
||||
border="0"
|
||||
style="display: block"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<p
|
||||
style="
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
color: #525252;
|
||||
"
|
||||
>
|
||||
Labels:
|
||||
</p>
|
||||
</td>
|
||||
{% if update.changes.labels.new_value.0 %}
|
||||
<td>
|
||||
<p
|
||||
style="
|
||||
font-size: 0.8rem;
|
||||
color: #0d74ce;
|
||||
background-color: #e6f4fe;
|
||||
margin-left: 5px;
|
||||
padding-left: 4px;
|
||||
padding-right: 4px;
|
||||
padding-top: 2px;
|
||||
padding-bottom: 2px;
|
||||
font-weight: 500;
|
||||
border-radius: 4px;
|
||||
"
|
||||
>
|
||||
{{update.changes.labels.new_value.0}}
|
||||
</p>
|
||||
</td>
|
||||
{% endif %}
|
||||
{% if update.changes.labels.new_value.1 %}
|
||||
<td>
|
||||
<p
|
||||
style="
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
color: #0d74ce;
|
||||
margin-left: 2px;
|
||||
"
|
||||
>
|
||||
+{{ update.changes.labels.new_value|length|add:"-1"}} more
|
||||
</p>
|
||||
</td>
|
||||
{% endif %}
|
||||
{% if update.changes.labels.old_value.0 %}
|
||||
<td>
|
||||
<p
|
||||
style="
|
||||
font-size: 0.8rem;
|
||||
padding-left: 4px;
|
||||
padding-right: 4px;
|
||||
padding-top: 2px;
|
||||
padding-bottom: 2px;
|
||||
font-weight: 500;
|
||||
text-decoration: line-through;
|
||||
color: #641723;
|
||||
background-color: #feebec;
|
||||
margin-left: 5px;
|
||||
border-radius: 4px;
|
||||
"
|
||||
>
|
||||
{{update.changes.labels.old_value.0}}
|
||||
</p>
|
||||
</td>
|
||||
{% endif %}
|
||||
{% if update.changes.labels.old_value.1 %}
|
||||
<td>
|
||||
<p
|
||||
style="
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
color: #641723;
|
||||
margin-left: 2px;
|
||||
"
|
||||
>
|
||||
+{{ update.changes.labels.old_value|length|add:"-1"}} more
|
||||
</p>
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</table>
|
||||
{% endif %}
|
||||
<!-- State changed -->
|
||||
{% if update.changes.state %}
|
||||
<table
|
||||
role="presentation"
|
||||
style="padding-bottom: 15px; max-width: 100%"
|
||||
>
|
||||
<tr>
|
||||
<td>
|
||||
<img
|
||||
src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/state.webp"
|
||||
width="12"
|
||||
height="12"
|
||||
border="0"
|
||||
style="display: block"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<p
|
||||
style="
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
color: #525252;
|
||||
"
|
||||
>
|
||||
State:
|
||||
</p>
|
||||
</td>
|
||||
<td >
|
||||
<img
|
||||
src="{% if update.changes.state.old_value.0 == 'Backlog' %}https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/backlog.webp{% endif %}{% if update.changes.state.old_value.0 == 'In Progress' %}https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/in-progress.webp{% endif %}{% if update.changes.state.old_value.0 == 'Done' %}https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/done.webp{% endif %}{% if update.changes.state.old_value.0 == 'Cancelled' %}https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/cancelled.webp{% endif %}"
|
||||
width="12"
|
||||
height="12"
|
||||
border="0"
|
||||
style="display: block; margin-left: 5px;"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<p
|
||||
style="
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
color: #60646C;
|
||||
"
|
||||
>
|
||||
{{ update.changes.state.old_value.0 }}
|
||||
</p>
|
||||
</td>
|
||||
<td style="padding-left: 10px; padding-right: 10px;">
|
||||
<img
|
||||
src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/forward-arrow.webp"
|
||||
width="16"
|
||||
height="16"
|
||||
border="0"
|
||||
style="display: block;"
|
||||
/>
|
||||
</td>
|
||||
<td >
|
||||
<img
|
||||
src="{% if update.changes.state.new_value|last == 'Backlog' %}https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/backlog.webp{% elif update.changes.state.new_value|last == 'In Progress' %}https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/in-progress.webp{% elif update.changes.state.new_value|last == 'Todo' %}https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/todo.webp{% elif update.changes.state.new_value|last == 'Done' %}https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/done.webp{% elif update.changes.state.new_value|last == 'Cancelled' %}https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/cancelled.webp{% endif %}"
|
||||
width="12"
|
||||
height="12"
|
||||
border="0"
|
||||
style="display: block;"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<p
|
||||
style="
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
color: #60646c;
|
||||
"
|
||||
>
|
||||
{{update.changes.state.new_value|last }}
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% endif %} {% if update.changes.link %}
|
||||
<!-- Link Added -->
|
||||
<table
|
||||
role="presentation"
|
||||
style="padding-bottom: 15px; max-width: 100%"
|
||||
>
|
||||
<tr>
|
||||
<td valign="top">
|
||||
<img
|
||||
src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/link.webp"
|
||||
width="12"
|
||||
height="12"
|
||||
border="0"
|
||||
style="display: block; margin-top: 3px;"
|
||||
/>
|
||||
</td>
|
||||
<td valign="top">
|
||||
<p
|
||||
style="
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
color: #525252;
|
||||
margin-right: 5px;
|
||||
|
||||
"
|
||||
>
|
||||
Links:
|
||||
</p>
|
||||
</td>
|
||||
<td>
|
||||
{% for link in update.changes.link.new_value %}
|
||||
<a
|
||||
href="{{link}}"
|
||||
style="
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
color: #3a5bc7;
|
||||
display: block;
|
||||
padding-bottom: 5px;
|
||||
"
|
||||
>
|
||||
{{ link }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
{% if update.changes.link.old_value|length > 0 %}
|
||||
{% if update.changes.link.old_value.0 != "None" %}
|
||||
<p
|
||||
href=""
|
||||
style="
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
color: #60646c;
|
||||
"
|
||||
>
|
||||
2 Links were removed
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% endif %}
|
||||
{% if update.changes.priority %}
|
||||
<!-- Priority changed -->
|
||||
<table
|
||||
role="presentation"
|
||||
style="padding-bottom: 15px; max-width: 100%"
|
||||
>
|
||||
<tr>
|
||||
<td>
|
||||
<img
|
||||
src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/priority.webp"
|
||||
width="12"
|
||||
height="12"
|
||||
border="0"
|
||||
style="display: block; margin: 0px; padding: 0px"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<p
|
||||
style="
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
color: #525252;
|
||||
margin-right: 5px;
|
||||
"
|
||||
>
|
||||
Priority:
|
||||
</p>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<p
|
||||
style="
|
||||
font-size: 0.8rem;
|
||||
padding-left: 4px;
|
||||
padding-right: 4px;
|
||||
padding-bottom: 2px;
|
||||
font-weight: 500;
|
||||
border-radius: 4px;
|
||||
text-transform: capitalize;
|
||||
{% if update.changes.priority.old_value.0 == 'urgent' %}background-color: #FFDBDC; color: #CE2C31;{% endif %}
|
||||
{% if update.changes.priority.old_value.0 == 'high' %}background-color: #FFE2C7; color: #F04610;{% endif %}
|
||||
{% if update.changes.priority.old_value.0 == 'medium' %}background-color: #FEFBE9; color: #E2A336;{% endif %}
|
||||
{% if update.changes.priority.old_value.0 == 'low' %}background-color: #E9F6E9; color: #2A7E3B;{% endif %}
|
||||
"
|
||||
>
|
||||
{{ update.changes.priority.old_value.0 }}
|
||||
</p>
|
||||
</td>
|
||||
<td style="padding-left: 10px; padding-right: 10px;">
|
||||
<img
|
||||
src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/forward-arrow.webp"
|
||||
width="16"
|
||||
height="16"
|
||||
border="0"
|
||||
style="display: block;"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<p
|
||||
style="
|
||||
font-size: 0.8rem;
|
||||
padding-left: 4px;
|
||||
padding-right: 4px;
|
||||
padding-bottom: 2px;
|
||||
font-weight: 500;
|
||||
border-radius: 4px;
|
||||
text-transform: capitalize;
|
||||
{% if update.changes.priority.new_value|last == 'urgent' %}background-color: #FFDBDC; color: #CE2C31;{% endif %}
|
||||
{% if update.changes.priority.new_value|last == 'high' %}background-color: #FEEBEC; color: #CE2C31;{% endif %}
|
||||
{% if update.changes.priority.new_value|last == 'medium' %}background-color: #FEFBE9; color: #E2A336;{% endif %}
|
||||
{% if update.changes.priority.new_value|last == 'low' %}background-color: #E9F6E9; color: #2A7E3B;{% endif %}
|
||||
"
|
||||
>
|
||||
{{ update.changes.priority.new_value|last }}
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% endif %}
|
||||
{% if update.changes.blocking.new_value %}
|
||||
<!-- Blocking changed -->
|
||||
<table
|
||||
role="presentation"
|
||||
style="padding-bottom: 15px; max-width: 100%"
|
||||
>
|
||||
<tr style="overflow-wrap: break-word;">
|
||||
<td>
|
||||
<img
|
||||
src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/blocking.webp"
|
||||
width="12"
|
||||
height="12"
|
||||
border="0"
|
||||
style="display: inline-block;"
|
||||
/>
|
||||
<span
|
||||
style="
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
color: #525252;
|
||||
margin-right: 5px;
|
||||
"
|
||||
>
|
||||
Blocking:
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
{% for blocking in update.changes.blocking.new_value %}
|
||||
<span href="" style="font-size: 0.8rem; font-weight: 500; margin-right: 5px; color: #3358d4;">
|
||||
{{blocking}}
|
||||
</span>
|
||||
{% endfor %}
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
</table>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<!-- Outer update Box end -->
|
||||
{% endfor %} {% if comments.0 %}
|
||||
<!-- Comments outer update Box -->
|
||||
<div
|
||||
style="
|
||||
max-width: 100%;
|
||||
background-color: #f7f9ff;
|
||||
border-radius: 8px;
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-color: #c1d0ff;
|
||||
padding: 20px;
|
||||
margin-top: 15px;
|
||||
"
|
||||
>
|
||||
<!-- Block Heading -->
|
||||
<p style="font-size: 0.8rem; font-weight: 600; color: #121a26; padding-bottom: 20px;">
|
||||
Comments
|
||||
</p>
|
||||
<!-- Comments -->
|
||||
{% for comment in comments %}
|
||||
<table cellspacing="0">
|
||||
<tr style="border-radius: 8px">
|
||||
<td valign="top">
|
||||
{% if comment.actor_detail.avatar_url %}
|
||||
<img
|
||||
src="{{ comment.actor_detail.avatar_url }}"
|
||||
width="25"
|
||||
height="25"
|
||||
border="0"
|
||||
/>
|
||||
{% else %}
|
||||
<table cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td
|
||||
style="
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
background-color: #4f3422;
|
||||
border-radius: 50%;
|
||||
text-align: center;
|
||||
"
|
||||
>
|
||||
<span
|
||||
style="
|
||||
color: white;
|
||||
font-weight: 500;
|
||||
font-size: 13px;
|
||||
line-height: 25px;
|
||||
display: inline-block;
|
||||
"
|
||||
>
|
||||
{{ comment.actor_detail.first_name.0 }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td style="padding-bottom: 15px">
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
<p
|
||||
style="
|
||||
font-weight: 500;
|
||||
font-size: 0.8rem;
|
||||
color: #1c2024;
|
||||
margin-left: 8px;
|
||||
"
|
||||
>
|
||||
{{ comment.actor_detail.first_name }} {{ comment.actor_detail.last_name }}
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
{% for actor_comment in comment.actor_comments.new_value %}
|
||||
<tr>
|
||||
<td>
|
||||
<div
|
||||
style="
|
||||
padding: 6px 10px;
|
||||
margin-left: 10px;
|
||||
background-color: white;
|
||||
font-size: 0.8rem;
|
||||
color: #525252;
|
||||
margin-top: 5px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
"
|
||||
>
|
||||
<p>
|
||||
{{ actor_comment|safe }}
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<a href="{{ issue_url }}" style="text-decoration: none;">
|
||||
<div
|
||||
style="
|
||||
max-width: min-content;
|
||||
white-space: nowrap;
|
||||
background-color: #3e63dd;
|
||||
padding: 10px 15px;
|
||||
border: 1px solid #2f4ba8;
|
||||
border-radius: 4px;
|
||||
margin-top: 15px;
|
||||
cursor: pointer;
|
||||
font-size: 0.8rem;
|
||||
color: white;
|
||||
"
|
||||
>
|
||||
View issue
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<!-- Footer -->
|
||||
<table style="width: 100%; padding: 20px; justify-content: center">
|
||||
<tr>
|
||||
<td>
|
||||
<div style="font-size: 0.8rem; color: #1c2024">
|
||||
This email was sent to
|
||||
<a
|
||||
href="mailto:{{receiver.email}}"
|
||||
style="color: #3a5bc7; font-weight: 500; text-decoration: none"
|
||||
>{{ receiver.email }}.</a
|
||||
>
|
||||
If you'd rather not receive this kind of email,
|
||||
<a
|
||||
href="{{ issue_url }}"
|
||||
style="color: #3a5bc7; text-decoration: none"
|
||||
>you can unsubscribe to the issue</a
|
||||
>
|
||||
or
|
||||
<a
|
||||
href="{{ user_preference }}"
|
||||
style="color: #3a5bc7; text-decoration: none"
|
||||
>manage your email preferences</a
|
||||
>.
|
||||
<!-- Github | LinkedIn | Twitter -->
|
||||
<div style="margin-top: 60px; float: right">
|
||||
<a
|
||||
href="https://github.com/makeplane"
|
||||
target="_blank"
|
||||
style="margin-left: 10px; text-decoration: none"
|
||||
>
|
||||
<img
|
||||
src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/github_32px.png"
|
||||
width="25"
|
||||
height="25"
|
||||
border="0"
|
||||
style="display: inline-block"
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
href="https://www.linkedin.com/company/planepowers/"
|
||||
target="_blank"
|
||||
style="margin-left: 10px; text-decoration: none"
|
||||
>
|
||||
<img
|
||||
src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/linkedin_32px.png"
|
||||
width="25"
|
||||
height="25"
|
||||
border="0"
|
||||
style="display: inline-block"
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
href="https://twitter.com/planepowers"
|
||||
target="_blank"
|
||||
style="margin-left: 10px; text-decoration: none"
|
||||
>
|
||||
<img
|
||||
src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/twitter_32px.png"
|
||||
width="25"
|
||||
height="25"
|
||||
border="0"
|
||||
style="display: inline-block"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,16 @@
|
||||
#!/bin/bash
|
||||
|
||||
if command -v curl &> /dev/null; then
|
||||
sudo curl -sSL \
|
||||
-o /usr/local/bin/plane-app \
|
||||
https://raw.githubusercontent.com/makeplane/plane/${BRANCH:-master}/deploy/1-click/plane-app?token=$(date +%s)
|
||||
else
|
||||
sudo wget -q \
|
||||
-O /usr/local/bin/plane-app \
|
||||
https://raw.githubusercontent.com/makeplane/plane/${BRANCH:-master}/deploy/1-click/plane-app?token=$(date +%s)
|
||||
fi
|
||||
|
||||
sudo chmod +x /usr/local/bin/plane-app
|
||||
sudo sed -i 's/export BRANCH=${BRANCH:-master}/export BRANCH='${BRANCH:-master}'/' /usr/local/bin/plane-app
|
||||
|
||||
sudo plane-app --help
|
||||
@@ -0,0 +1,713 @@
|
||||
#!/bin/bash
|
||||
|
||||
function print_header() {
|
||||
clear
|
||||
|
||||
cat <<"EOF"
|
||||
---------------------------------------
|
||||
____ _
|
||||
| _ \| | __ _ _ __ ___
|
||||
| |_) | |/ _` | '_ \ / _ \
|
||||
| __/| | (_| | | | | __/
|
||||
|_| |_|\__,_|_| |_|\___|
|
||||
|
||||
---------------------------------------
|
||||
Project management tool from the future
|
||||
---------------------------------------
|
||||
|
||||
EOF
|
||||
}
|
||||
function update_env_files() {
|
||||
config_file=$1
|
||||
key=$2
|
||||
value=$3
|
||||
|
||||
# Check if the config file exists
|
||||
if [ ! -f "$config_file" ]; then
|
||||
echo "Config file not found. Creating a new one..." >&2
|
||||
touch "$config_file"
|
||||
fi
|
||||
|
||||
# Check if the key already exists in the config file
|
||||
if grep -q "^$key=" "$config_file"; then
|
||||
awk -v key="$key" -v value="$value" -F '=' '{if ($1 == key) $2 = value} 1' OFS='=' "$config_file" > "$config_file.tmp" && mv "$config_file.tmp" "$config_file"
|
||||
else
|
||||
echo "$key=$value" >> "$config_file"
|
||||
fi
|
||||
}
|
||||
function read_env_file() {
|
||||
config_file=$1
|
||||
key=$2
|
||||
|
||||
# Check if the config file exists
|
||||
if [ ! -f "$config_file" ]; then
|
||||
echo "Config file not found. Creating a new one..." >&2
|
||||
touch "$config_file"
|
||||
fi
|
||||
|
||||
# Check if the key already exists in the config file
|
||||
if grep -q "^$key=" "$config_file"; then
|
||||
value=$(awk -v key="$key" -F '=' '{if ($1 == key) print $2}' "$config_file")
|
||||
echo "$value"
|
||||
else
|
||||
echo ""
|
||||
fi
|
||||
}
|
||||
function update_config() {
|
||||
config_file="$PLANE_INSTALL_DIR/config.env"
|
||||
update_env_files "$config_file" "$1" "$2"
|
||||
}
|
||||
function read_config() {
|
||||
config_file="$PLANE_INSTALL_DIR/config.env"
|
||||
read_env_file "$config_file" "$1"
|
||||
}
|
||||
function update_env() {
|
||||
config_file="$PLANE_INSTALL_DIR/.env"
|
||||
update_env_files "$config_file" "$1" "$2"
|
||||
}
|
||||
function read_env() {
|
||||
config_file="$PLANE_INSTALL_DIR/.env"
|
||||
read_env_file "$config_file" "$1"
|
||||
}
|
||||
function show_message() {
|
||||
print_header
|
||||
|
||||
if [ "$2" == "replace_last_line" ]; then
|
||||
PROGRESS_MSG[-1]="$1"
|
||||
else
|
||||
PROGRESS_MSG+=("$1")
|
||||
fi
|
||||
|
||||
for statement in "${PROGRESS_MSG[@]}"; do
|
||||
echo "$statement"
|
||||
done
|
||||
|
||||
}
|
||||
function prepare_environment() {
|
||||
show_message "Prepare Environment..." >&2
|
||||
|
||||
show_message "- Updating OS with required tools ✋" >&2
|
||||
sudo apt-get update -y &> /dev/null
|
||||
sudo apt-get upgrade -y &> /dev/null
|
||||
|
||||
required_tools=("curl" "awk" "wget" "nano" "dialog" "git")
|
||||
|
||||
for tool in "${required_tools[@]}"; do
|
||||
if ! command -v $tool &> /dev/null; then
|
||||
sudo apt install -y $tool &> /dev/null
|
||||
fi
|
||||
done
|
||||
|
||||
show_message "- OS Updated ✅" "replace_last_line" >&2
|
||||
|
||||
# Install Docker if not installed
|
||||
if ! command -v docker &> /dev/null; then
|
||||
show_message "- Installing Docker ✋" >&2
|
||||
sudo curl -o- https://get.docker.com | bash -
|
||||
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
dockerd-rootless-setuptool.sh install &> /dev/null
|
||||
fi
|
||||
show_message "- Docker Installed ✅" "replace_last_line" >&2
|
||||
else
|
||||
show_message "- Docker is already installed ✅" >&2
|
||||
fi
|
||||
|
||||
update_config "PLANE_ARCH" "$CPU_ARCH"
|
||||
update_config "DOCKER_VERSION" "$(docker -v | awk '{print $3}' | sed 's/,//g')"
|
||||
update_config "PLANE_DATA_DIR" "$DATA_DIR"
|
||||
update_config "PLANE_LOG_DIR" "$LOG_DIR"
|
||||
|
||||
# echo "TRUE"
|
||||
echo "Environment prepared successfully ✅"
|
||||
show_message "Environment prepared successfully ✅" >&2
|
||||
show_message "" >&2
|
||||
return 0
|
||||
}
|
||||
function download_plane() {
|
||||
# Download Docker Compose File from github url
|
||||
show_message "Downloading Plane Setup Files ✋" >&2
|
||||
curl -H 'Cache-Control: no-cache, no-store' \
|
||||
-s -o $PLANE_INSTALL_DIR/docker-compose.yaml \
|
||||
https://raw.githubusercontent.com/makeplane/plane/$BRANCH/deploy/selfhost/docker-compose.yml?$(date +%s)
|
||||
|
||||
curl -H 'Cache-Control: no-cache, no-store' \
|
||||
-s -o $PLANE_INSTALL_DIR/variables-upgrade.env \
|
||||
https://raw.githubusercontent.com/makeplane/plane/$BRANCH/deploy/selfhost/variables.env?$(date +%s)
|
||||
|
||||
# if .env does not exists rename variables-upgrade.env to .env
|
||||
if [ ! -f "$PLANE_INSTALL_DIR/.env" ]; then
|
||||
mv $PLANE_INSTALL_DIR/variables-upgrade.env $PLANE_INSTALL_DIR/.env
|
||||
fi
|
||||
|
||||
show_message "Plane Setup Files Downloaded ✅" "replace_last_line" >&2
|
||||
show_message "" >&2
|
||||
|
||||
echo "PLANE_DOWNLOADED"
|
||||
return 0
|
||||
}
|
||||
function printUsageInstructions() {
|
||||
show_message "" >&2
|
||||
show_message "----------------------------------" >&2
|
||||
show_message "Usage Instructions" >&2
|
||||
show_message "----------------------------------" >&2
|
||||
show_message "" >&2
|
||||
show_message "To use the Plane Setup utility, use below commands" >&2
|
||||
show_message "" >&2
|
||||
|
||||
show_message "Usage: plane-app [OPTION]" >&2
|
||||
show_message "" >&2
|
||||
show_message " start Start Server" >&2
|
||||
show_message " stop Stop Server" >&2
|
||||
show_message " restart Restart Server" >&2
|
||||
show_message "" >&2
|
||||
show_message "other options" >&2
|
||||
show_message " -i, --install Install Plane" >&2
|
||||
show_message " -c, --configure Configure Plane" >&2
|
||||
show_message " -up, --upgrade Upgrade Plane" >&2
|
||||
show_message " -un, --uninstall Uninstall Plane" >&2
|
||||
show_message " -ui, --update-installer Update Plane Installer" >&2
|
||||
show_message " -h, --help Show help" >&2
|
||||
show_message "" >&2
|
||||
show_message "" >&2
|
||||
show_message "Application Data is stored in mentioned folders" >&2
|
||||
show_message " - DB Data: $DATA_DIR/postgres" >&2
|
||||
show_message " - Redis Data: $DATA_DIR/redis" >&2
|
||||
show_message " - Minio Data: $DATA_DIR/minio" >&2
|
||||
show_message "" >&2
|
||||
show_message "" >&2
|
||||
show_message "----------------------------------" >&2
|
||||
show_message "" >&2
|
||||
}
|
||||
function build_local_image() {
|
||||
show_message "- Downloading Plane Source Code ✋" >&2
|
||||
REPO=https://github.com/makeplane/plane.git
|
||||
CURR_DIR=$PWD
|
||||
PLANE_TEMP_CODE_DIR=$PLANE_INSTALL_DIR/temp
|
||||
sudo rm -rf $PLANE_TEMP_CODE_DIR > /dev/null
|
||||
|
||||
sudo git clone $REPO $PLANE_TEMP_CODE_DIR --branch $BRANCH --single-branch -q > /dev/null
|
||||
|
||||
sudo cp $PLANE_TEMP_CODE_DIR/deploy/selfhost/build.yml $PLANE_TEMP_CODE_DIR/build.yml
|
||||
|
||||
show_message "- Plane Source Code Downloaded ✅" "replace_last_line" >&2
|
||||
|
||||
show_message "- Building Docker Images ✋" >&2
|
||||
sudo docker compose --env-file=$PLANE_INSTALL_DIR/.env -f $PLANE_TEMP_CODE_DIR/build.yml build --no-cache
|
||||
}
|
||||
function check_for_docker_images() {
|
||||
show_message "" >&2
|
||||
# show_message "Building Plane Images" >&2
|
||||
|
||||
update_env "DOCKERHUB_USER" "makeplane"
|
||||
update_env "PULL_POLICY" "always"
|
||||
CURR_DIR=$(pwd)
|
||||
|
||||
if [ "$BRANCH" == "master" ]; then
|
||||
update_env "APP_RELEASE" "latest"
|
||||
export APP_RELEASE=latest
|
||||
else
|
||||
update_env "APP_RELEASE" "$BRANCH"
|
||||
export APP_RELEASE=$BRANCH
|
||||
fi
|
||||
|
||||
if [ $CPU_ARCH == "amd64" ] || [ $CPU_ARCH == "x86_64" ]; then
|
||||
# show_message "Building Plane Images for $CPU_ARCH is not required. Skipping... ✅" "replace_last_line" >&2
|
||||
echo "Building Plane Images for $CPU_ARCH is not required. Skipping..."
|
||||
else
|
||||
export DOCKERHUB_USER=myplane
|
||||
show_message "Building Plane Images for $CPU_ARCH " >&2
|
||||
update_env "DOCKERHUB_USER" "myplane"
|
||||
update_env "PULL_POLICY" "never"
|
||||
|
||||
build_local_image
|
||||
|
||||
sudo rm -rf $PLANE_INSTALL_DIR/temp > /dev/null
|
||||
|
||||
show_message "- Docker Images Built ✅" "replace_last_line" >&2
|
||||
sudo cd $CURR_DIR
|
||||
fi
|
||||
|
||||
sudo sed -i "s|- pgdata:|- $DATA_DIR/postgres:|g" $PLANE_INSTALL_DIR/docker-compose.yaml
|
||||
sudo sed -i "s|- redisdata:|- $DATA_DIR/redis:|g" $PLANE_INSTALL_DIR/docker-compose.yaml
|
||||
sudo sed -i "s|- uploads:|- $DATA_DIR/minio:|g" $PLANE_INSTALL_DIR/docker-compose.yaml
|
||||
|
||||
show_message "Downloading Plane Images for $CPU_ARCH ✋" >&2
|
||||
docker compose -f $PLANE_INSTALL_DIR/docker-compose.yaml --env-file=$PLANE_INSTALL_DIR/.env pull
|
||||
show_message "Plane Images Downloaded ✅" "replace_last_line" >&2
|
||||
}
|
||||
function configure_plane() {
|
||||
show_message "" >&2
|
||||
show_message "Configuring Plane" >&2
|
||||
show_message "" >&2
|
||||
|
||||
exec 3>&1
|
||||
|
||||
nginx_port=$(read_env "NGINX_PORT")
|
||||
domain_name=$(read_env "DOMAIN_NAME")
|
||||
upload_limit=$(read_env "FILE_SIZE_LIMIT")
|
||||
|
||||
NGINX_SETTINGS=$(dialog \
|
||||
--ok-label "Next" \
|
||||
--cancel-label "Skip" \
|
||||
--backtitle "Plane Configuration" \
|
||||
--title "Nginx Settings" \
|
||||
--form "" \
|
||||
0 0 0 \
|
||||
"Port:" 1 1 "${nginx_port:-80}" 1 10 50 0 \
|
||||
"Domain:" 2 1 "${domain_name:-localhost}" 2 10 50 0 \
|
||||
"Upload Limit:" 3 1 "${upload_limit:-5242880}" 3 10 15 0 \
|
||||
2>&1 1>&3)
|
||||
|
||||
save_nginx_settings=0
|
||||
if [ $? -eq 0 ]; then
|
||||
save_nginx_settings=1
|
||||
nginx_port=$(echo "$NGINX_SETTINGS" | sed -n 1p)
|
||||
domain_name=$(echo "$NGINX_SETTINGS" | sed -n 2p)
|
||||
upload_limit=$(echo "$NGINX_SETTINGS" | sed -n 3p)
|
||||
fi
|
||||
|
||||
|
||||
smtp_host=$(read_env "EMAIL_HOST")
|
||||
smtp_user=$(read_env "EMAIL_HOST_USER")
|
||||
smtp_password=$(read_env "EMAIL_HOST_PASSWORD")
|
||||
smtp_port=$(read_env "EMAIL_PORT")
|
||||
smtp_from=$(read_env "EMAIL_FROM")
|
||||
smtp_tls=$(read_env "EMAIL_USE_TLS")
|
||||
smtp_ssl=$(read_env "EMAIL_USE_SSL")
|
||||
|
||||
SMTP_SETTINGS=$(dialog \
|
||||
--ok-label "Next" \
|
||||
--cancel-label "Skip" \
|
||||
--backtitle "Plane Configuration" \
|
||||
--title "SMTP Settings" \
|
||||
--form "" \
|
||||
0 0 0 \
|
||||
"Host:" 1 1 "$smtp_host" 1 10 80 0 \
|
||||
"User:" 2 1 "$smtp_user" 2 10 80 0 \
|
||||
"Password:" 3 1 "$smtp_password" 3 10 80 0 \
|
||||
"Port:" 4 1 "${smtp_port:-587}" 4 10 5 0 \
|
||||
"From:" 5 1 "${smtp_from:-Mailer <mailer@example.com>}" 5 10 80 0 \
|
||||
"TLS:" 6 1 "${smtp_tls:-1}" 6 10 1 1 \
|
||||
"SSL:" 7 1 "${smtp_ssl:-0}" 7 10 1 1 \
|
||||
2>&1 1>&3)
|
||||
|
||||
save_smtp_settings=0
|
||||
if [ $? -eq 0 ]; then
|
||||
save_smtp_settings=1
|
||||
smtp_host=$(echo "$SMTP_SETTINGS" | sed -n 1p)
|
||||
smtp_user=$(echo "$SMTP_SETTINGS" | sed -n 2p)
|
||||
smtp_password=$(echo "$SMTP_SETTINGS" | sed -n 3p)
|
||||
smtp_port=$(echo "$SMTP_SETTINGS" | sed -n 4p)
|
||||
smtp_from=$(echo "$SMTP_SETTINGS" | sed -n 5p)
|
||||
smtp_tls=$(echo "$SMTP_SETTINGS" | sed -n 6p)
|
||||
fi
|
||||
external_pgdb_url=$(dialog \
|
||||
--backtitle "Plane Configuration" \
|
||||
--title "Using External Postgres Database ?" \
|
||||
--ok-label "Next" \
|
||||
--cancel-label "Skip" \
|
||||
--inputbox "Enter your external database url" \
|
||||
8 60 3>&1 1>&2 2>&3)
|
||||
|
||||
|
||||
external_redis_url=$(dialog \
|
||||
--backtitle "Plane Configuration" \
|
||||
--title "Using External Redis Database ?" \
|
||||
--ok-label "Next" \
|
||||
--cancel-label "Skip" \
|
||||
--inputbox "Enter your external redis url" \
|
||||
8 60 3>&1 1>&2 2>&3)
|
||||
|
||||
|
||||
aws_region=$(read_env "AWS_REGION")
|
||||
aws_access_key=$(read_env "AWS_ACCESS_KEY_ID")
|
||||
aws_secret_key=$(read_env "AWS_SECRET_ACCESS_KEY")
|
||||
aws_bucket=$(read_env "AWS_S3_BUCKET_NAME")
|
||||
|
||||
|
||||
AWS_S3_SETTINGS=$(dialog \
|
||||
--ok-label "Next" \
|
||||
--cancel-label "Skip" \
|
||||
--backtitle "Plane Configuration" \
|
||||
--title "AWS S3 Bucket Configuration" \
|
||||
--form "" \
|
||||
0 0 0 \
|
||||
"Region:" 1 1 "$aws_region" 1 10 50 0 \
|
||||
"Access Key:" 2 1 "$aws_access_key" 2 10 50 0 \
|
||||
"Secret Key:" 3 1 "$aws_secret_key" 3 10 50 0 \
|
||||
"Bucket:" 4 1 "$aws_bucket" 4 10 50 0 \
|
||||
2>&1 1>&3)
|
||||
|
||||
save_aws_settings=0
|
||||
if [ $? -eq 0 ]; then
|
||||
save_aws_settings=1
|
||||
aws_region=$(echo "$AWS_S3_SETTINGS" | sed -n 1p)
|
||||
aws_access_key=$(echo "$AWS_S3_SETTINGS" | sed -n 2p)
|
||||
aws_secret_key=$(echo "$AWS_S3_SETTINGS" | sed -n 3p)
|
||||
aws_bucket=$(echo "$AWS_S3_SETTINGS" | sed -n 4p)
|
||||
fi
|
||||
|
||||
# display dialogbox asking for confirmation to continue
|
||||
CONFIRM_CONFIG=$(dialog \
|
||||
--title "Confirm Configuration" \
|
||||
--backtitle "Plane Configuration" \
|
||||
--yes-label "Confirm" \
|
||||
--no-label "Cancel" \
|
||||
--yesno \
|
||||
"
|
||||
save_ngnix_settings: $save_nginx_settings
|
||||
nginx_port: $nginx_port
|
||||
domain_name: $domain_name
|
||||
upload_limit: $upload_limit
|
||||
|
||||
save_smtp_settings: $save_smtp_settings
|
||||
smtp_host: $smtp_host
|
||||
smtp_user: $smtp_user
|
||||
smtp_password: $smtp_password
|
||||
smtp_port: $smtp_port
|
||||
smtp_from: $smtp_from
|
||||
smtp_tls: $smtp_tls
|
||||
smtp_ssl: $smtp_ssl
|
||||
|
||||
save_aws_settings: $save_aws_settings
|
||||
aws_region: $aws_region
|
||||
aws_access_key: $aws_access_key
|
||||
aws_secret_key: $aws_secret_key
|
||||
aws_bucket: $aws_bucket
|
||||
|
||||
pdgb_url: $external_pgdb_url
|
||||
redis_url: $external_redis_url
|
||||
" \
|
||||
0 0 3>&1 1>&2 2>&3)
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
if [ $save_nginx_settings == 1 ]; then
|
||||
update_env "NGINX_PORT" "$nginx_port"
|
||||
update_env "DOMAIN_NAME" "$domain_name"
|
||||
update_env "WEB_URL" "http://$domain_name"
|
||||
update_env "CORS_ALLOWED_ORIGINS" "http://$domain_name"
|
||||
update_env "FILE_SIZE_LIMIT" "$upload_limit"
|
||||
fi
|
||||
|
||||
# check enable smpt settings value
|
||||
if [ $save_smtp_settings == 1 ]; then
|
||||
update_env "EMAIL_HOST" "$smtp_host"
|
||||
update_env "EMAIL_HOST_USER" "$smtp_user"
|
||||
update_env "EMAIL_HOST_PASSWORD" "$smtp_password"
|
||||
update_env "EMAIL_PORT" "$smtp_port"
|
||||
update_env "EMAIL_FROM" "$smtp_from"
|
||||
update_env "EMAIL_USE_TLS" "$smtp_tls"
|
||||
update_env "EMAIL_USE_SSL" "$smtp_ssl"
|
||||
fi
|
||||
|
||||
# check enable aws settings value
|
||||
if [[ $save_aws_settings == 1 && $aws_access_key != "" && $aws_secret_key != "" ]] ; then
|
||||
update_env "USE_MINIO" "0"
|
||||
update_env "AWS_REGION" "$aws_region"
|
||||
update_env "AWS_ACCESS_KEY_ID" "$aws_access_key"
|
||||
update_env "AWS_SECRET_ACCESS_KEY" "$aws_secret_key"
|
||||
update_env "AWS_S3_BUCKET_NAME" "$aws_bucket"
|
||||
elif [[ -z $aws_access_key || -z $aws_secret_key ]] ; then
|
||||
update_env "USE_MINIO" "1"
|
||||
update_env "AWS_REGION" ""
|
||||
update_env "AWS_ACCESS_KEY_ID" ""
|
||||
update_env "AWS_SECRET_ACCESS_KEY" ""
|
||||
update_env "AWS_S3_BUCKET_NAME" "uploads"
|
||||
fi
|
||||
|
||||
if [ "$external_pgdb_url" != "" ]; then
|
||||
update_env "DATABASE_URL" "$external_pgdb_url"
|
||||
fi
|
||||
if [ "$external_redis_url" != "" ]; then
|
||||
update_env "REDIS_URL" "$external_redis_url"
|
||||
fi
|
||||
fi
|
||||
|
||||
exec 3>&-
|
||||
}
|
||||
function upgrade_configuration() {
|
||||
upg_env_file="$PLANE_INSTALL_DIR/variables-upgrade.env"
|
||||
# Check if the file exists
|
||||
if [ -f "$upg_env_file" ]; then
|
||||
# Read each line from the file
|
||||
while IFS= read -r line; do
|
||||
# Skip comments and empty lines
|
||||
if [[ "$line" =~ ^\s*#.*$ ]] || [[ -z "$line" ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
# Split the line into key and value
|
||||
key=$(echo "$line" | cut -d'=' -f1)
|
||||
value=$(echo "$line" | cut -d'=' -f2-)
|
||||
|
||||
current_value=$(read_env "$key")
|
||||
|
||||
if [ -z "$current_value" ]; then
|
||||
update_env "$key" "$value"
|
||||
fi
|
||||
done < "$upg_env_file"
|
||||
fi
|
||||
}
|
||||
function install() {
|
||||
show_message ""
|
||||
if [ "$(uname)" == "Linux" ]; then
|
||||
OS="linux"
|
||||
OS_NAME=$(awk -F= '/^ID=/{print $2}' /etc/os-release)
|
||||
# check the OS
|
||||
if [ "$OS_NAME" == "ubuntu" ]; then
|
||||
OS_SUPPORTED=true
|
||||
show_message "******** Installing Plane ********"
|
||||
show_message ""
|
||||
|
||||
prepare_environment
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
download_plane
|
||||
if [ $? -eq 0 ]; then
|
||||
# create_service
|
||||
check_for_docker_images
|
||||
|
||||
last_installed_on=$(read_config "INSTALLATION_DATE")
|
||||
if [ "$last_installed_on" == "" ]; then
|
||||
configure_plane
|
||||
fi
|
||||
printUsageInstructions
|
||||
|
||||
update_config "INSTALLATION_DATE" "$(date)"
|
||||
|
||||
show_message "Plane Installed Successfully ✅"
|
||||
show_message ""
|
||||
else
|
||||
show_message "Download Failed ❌"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
show_message "Initialization Failed ❌"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
else
|
||||
PROGRESS_MSG="❌❌❌ Unsupported OS Detected ❌❌❌"
|
||||
show_message ""
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
PROGRESS_MSG="❌❌❌ Unsupported OS Detected : $(uname) ❌❌❌"
|
||||
show_message ""
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
function upgrade() {
|
||||
if [ "$(uname)" == "Linux" ]; then
|
||||
OS="linux"
|
||||
OS_NAME=$(awk -F= '/^ID=/{print $2}' /etc/os-release)
|
||||
# check the OS
|
||||
if [ "$OS_NAME" == "ubuntu" ]; then
|
||||
OS_SUPPORTED=true
|
||||
|
||||
prepare_environment
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
download_plane
|
||||
if [ $? -eq 0 ]; then
|
||||
check_for_docker_images
|
||||
upgrade_configuration
|
||||
update_config "UPGRADE_DATE" "$(date)"
|
||||
|
||||
show_message ""
|
||||
show_message "Plane Upgraded Successfully ✅"
|
||||
show_message ""
|
||||
printUsageInstructions
|
||||
else
|
||||
show_message "Download Failed ❌"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
show_message "Initialization Failed ❌"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
PROGRESS_MSG="Unsupported OS Detected"
|
||||
show_message ""
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
PROGRESS_MSG="Unsupported OS Detected : $(uname)"
|
||||
show_message ""
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
function uninstall() {
|
||||
if [ "$(uname)" == "Linux" ]; then
|
||||
OS="linux"
|
||||
OS_NAME=$(awk -F= '/^ID=/{print $2}' /etc/os-release)
|
||||
# check the OS
|
||||
if [ "$OS_NAME" == "ubuntu" ]; then
|
||||
OS_SUPPORTED=true
|
||||
show_message "******** Uninstalling Plane ********"
|
||||
show_message ""
|
||||
|
||||
stop_server
|
||||
# CHECK IF PLANE SERVICE EXISTS
|
||||
# if [ -f "/etc/systemd/system/plane.service" ]; then
|
||||
# sudo systemctl stop plane.service &> /dev/null
|
||||
# sudo systemctl disable plane.service &> /dev/null
|
||||
# sudo rm /etc/systemd/system/plane.service &> /dev/null
|
||||
# sudo systemctl daemon-reload &> /dev/null
|
||||
# fi
|
||||
# show_message "- Plane Service removed ✅"
|
||||
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo "DOCKER_NOT_INSTALLED" &> /dev/null
|
||||
else
|
||||
# Ask of user input to confirm uninstall docker ?
|
||||
CONFIRM_DOCKER_PURGE=$(dialog --title "Uninstall Docker" --yesno "Are you sure you want to uninstall docker ?" 8 60 3>&1 1>&2 2>&3)
|
||||
if [ $? -eq 0 ]; then
|
||||
show_message "- Uninstalling Docker ✋"
|
||||
sudo apt-get purge -y docker-engine docker docker.io docker-ce docker-ce-cli docker-compose-plugin &> /dev/null
|
||||
sudo apt-get autoremove -y --purge docker-engine docker docker.io docker-ce docker-compose-plugin &> /dev/null
|
||||
show_message "- Docker Uninstalled ✅" "replace_last_line" >&2
|
||||
fi
|
||||
fi
|
||||
|
||||
rm $PLANE_INSTALL_DIR/.env &> /dev/null
|
||||
rm $PLANE_INSTALL_DIR/variables-upgrade.env &> /dev/null
|
||||
rm $PLANE_INSTALL_DIR/config.env &> /dev/null
|
||||
rm $PLANE_INSTALL_DIR/docker-compose.yaml &> /dev/null
|
||||
|
||||
# rm -rf $PLANE_INSTALL_DIR &> /dev/null
|
||||
show_message "- Configuration Cleaned ✅"
|
||||
|
||||
show_message ""
|
||||
show_message "******** Plane Uninstalled ********"
|
||||
show_message ""
|
||||
show_message ""
|
||||
show_message "Plane Configuration Cleaned with some exceptions"
|
||||
show_message "- DB Data: $DATA_DIR/postgres"
|
||||
show_message "- Redis Data: $DATA_DIR/redis"
|
||||
show_message "- Minio Data: $DATA_DIR/minio"
|
||||
show_message ""
|
||||
show_message ""
|
||||
show_message "Thank you for using Plane. We hope to see you again soon."
|
||||
show_message ""
|
||||
show_message ""
|
||||
else
|
||||
PROGRESS_MSG="Unsupported OS Detected : $(uname) ❌"
|
||||
show_message ""
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
PROGRESS_MSG="Unsupported OS Detected : $(uname) ❌"
|
||||
show_message ""
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
function start_server() {
|
||||
docker_compose_file="$PLANE_INSTALL_DIR/docker-compose.yaml"
|
||||
env_file="$PLANE_INSTALL_DIR/.env"
|
||||
# check if both the files exits
|
||||
if [ -f "$docker_compose_file" ] && [ -f "$env_file" ]; then
|
||||
show_message "Starting Plane Server ✋"
|
||||
docker compose -f $docker_compose_file --env-file=$env_file up -d
|
||||
|
||||
# Wait for containers to be running
|
||||
echo "Waiting for containers to start..."
|
||||
while ! docker compose -f "$docker_compose_file" --env-file="$env_file" ps --services --filter "status=running" --quiet | grep -q "."; do
|
||||
sleep 1
|
||||
done
|
||||
show_message "Plane Server Started ✅" "replace_last_line" >&2
|
||||
else
|
||||
show_message "Plane Server not installed. Please install Plane first ❌" "replace_last_line" >&2
|
||||
fi
|
||||
}
|
||||
function stop_server() {
|
||||
docker_compose_file="$PLANE_INSTALL_DIR/docker-compose.yaml"
|
||||
env_file="$PLANE_INSTALL_DIR/.env"
|
||||
# check if both the files exits
|
||||
if [ -f "$docker_compose_file" ] && [ -f "$env_file" ]; then
|
||||
show_message "Stopping Plane Server ✋"
|
||||
docker compose -f $docker_compose_file --env-file=$env_file down
|
||||
show_message "Plane Server Stopped ✅" "replace_last_line" >&2
|
||||
else
|
||||
show_message "Plane Server not installed. Please install Plane first ❌" "replace_last_line" >&2
|
||||
fi
|
||||
}
|
||||
function restart_server() {
|
||||
docker_compose_file="$PLANE_INSTALL_DIR/docker-compose.yaml"
|
||||
env_file="$PLANE_INSTALL_DIR/.env"
|
||||
# check if both the files exits
|
||||
if [ -f "$docker_compose_file" ] && [ -f "$env_file" ]; then
|
||||
show_message "Restarting Plane Server ✋"
|
||||
docker compose -f $docker_compose_file --env-file=$env_file restart
|
||||
show_message "Plane Server Restarted ✅" "replace_last_line" >&2
|
||||
else
|
||||
show_message "Plane Server not installed. Please install Plane first ❌" "replace_last_line" >&2
|
||||
fi
|
||||
}
|
||||
function show_help() {
|
||||
# print_header
|
||||
show_message "Usage: plane-app [OPTION]" >&2
|
||||
show_message "" >&2
|
||||
show_message " start Start Server" >&2
|
||||
show_message " stop Stop Server" >&2
|
||||
show_message " restart Restart Server" >&2
|
||||
show_message "" >&2
|
||||
show_message "other options" >&2
|
||||
show_message " -i, --install Install Plane" >&2
|
||||
show_message " -c, --configure Configure Plane" >&2
|
||||
show_message " -up, --upgrade Upgrade Plane" >&2
|
||||
show_message " -un, --uninstall Uninstall Plane" >&2
|
||||
show_message " -ui, --update-installer Update Plane Installer" >&2
|
||||
show_message " -h, --help Show help" >&2
|
||||
show_message "" >&2
|
||||
exit 1
|
||||
|
||||
}
|
||||
function update_installer() {
|
||||
show_message "Updating Plane Installer ✋" >&2
|
||||
curl -H 'Cache-Control: no-cache, no-store' \
|
||||
-s -o /usr/local/bin/plane-app \
|
||||
https://raw.githubusercontent.com/makeplane/plane/$BRANCH/deploy/1-click/install.sh?token=$(date +%s)
|
||||
|
||||
chmod +x /usr/local/bin/plane-app > /dev/null&> /dev/null
|
||||
show_message "Plane Installer Updated ✅" "replace_last_line" >&2
|
||||
}
|
||||
|
||||
export BRANCH=${BRANCH:-master}
|
||||
export APP_RELEASE=$BRANCH
|
||||
export DOCKERHUB_USER=makeplane
|
||||
export PULL_POLICY=always
|
||||
|
||||
PLANE_INSTALL_DIR=/opt/plane
|
||||
DATA_DIR=$PLANE_INSTALL_DIR/data
|
||||
LOG_DIR=$PLANE_INSTALL_DIR/log
|
||||
OS_SUPPORTED=false
|
||||
CPU_ARCH=$(uname -m)
|
||||
PROGRESS_MSG=""
|
||||
USE_GLOBAL_IMAGES=1
|
||||
|
||||
mkdir -p $PLANE_INSTALL_DIR/{data,log}
|
||||
|
||||
if [ "$1" == "start" ]; then
|
||||
start_server
|
||||
elif [ "$1" == "stop" ]; then
|
||||
stop_server
|
||||
elif [ "$1" == "restart" ]; then
|
||||
restart_server
|
||||
elif [ "$1" == "--install" ] || [ "$1" == "-i" ]; then
|
||||
install
|
||||
elif [ "$1" == "--configure" ] || [ "$1" == "-c" ]; then
|
||||
configure_plane
|
||||
printUsageInstructions
|
||||
elif [ "$1" == "--upgrade" ] || [ "$1" == "-up" ]; then
|
||||
upgrade
|
||||
elif [ "$1" == "--uninstall" ] || [ "$1" == "-un" ]; then
|
||||
uninstall
|
||||
elif [ "$1" == "--update-installer" ] || [ "$1" == "-ui" ] ; then
|
||||
update_installer
|
||||
elif [ "$1" == "--help" ] || [ "$1" == "-h" ]; then
|
||||
show_help
|
||||
else
|
||||
show_help
|
||||
fi
|
||||
@@ -122,6 +122,18 @@ services:
|
||||
- plane-db
|
||||
- plane-redis
|
||||
|
||||
migrator:
|
||||
<<: *app-env
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-latest}
|
||||
pull_policy: ${PULL_POLICY:-always}
|
||||
restart: no
|
||||
command: >
|
||||
sh -c "python manage.py wait_for_db &&
|
||||
python manage.py migrate"
|
||||
depends_on:
|
||||
- plane-db
|
||||
- plane-redis
|
||||
|
||||
plane-db:
|
||||
<<: *app-env
|
||||
image: postgres:15.2-alpine
|
||||
|
||||
@@ -86,7 +86,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/takeoff.local
|
||||
env_file:
|
||||
- ./apiserver/.env
|
||||
depends_on:
|
||||
@@ -104,7 +104,7 @@ services:
|
||||
- dev_env
|
||||
volumes:
|
||||
- ./apiserver:/code
|
||||
command: /bin/sh -c "celery -A plane worker -l info"
|
||||
command: ./bin/worker
|
||||
env_file:
|
||||
- ./apiserver/.env
|
||||
depends_on:
|
||||
@@ -123,7 +123,7 @@ services:
|
||||
- dev_env
|
||||
volumes:
|
||||
- ./apiserver:/code
|
||||
command: /bin/sh -c "celery -A plane beat -l info"
|
||||
command: ./bin/beat
|
||||
env_file:
|
||||
- ./apiserver/.env
|
||||
depends_on:
|
||||
@@ -131,6 +131,26 @@ services:
|
||||
- plane-db
|
||||
- plane-redis
|
||||
|
||||
migrator:
|
||||
build:
|
||||
context: ./apiserver
|
||||
dockerfile: Dockerfile.dev
|
||||
args:
|
||||
DOCKER_BUILDKIT: 1
|
||||
restart: no
|
||||
networks:
|
||||
- dev_env
|
||||
volumes:
|
||||
- ./apiserver:/code
|
||||
command: >
|
||||
sh -c "python manage.py wait_for_db --settings=plane.settings.local &&
|
||||
python manage.py migrate --settings=plane.settings.local"
|
||||
env_file:
|
||||
- ./apiserver/.env
|
||||
depends_on:
|
||||
- plane-db
|
||||
- plane-redis
|
||||
|
||||
proxy:
|
||||
build:
|
||||
context: ./nginx
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"repository": "https://github.com/makeplane/plane.git",
|
||||
"version": "0.14.0",
|
||||
"version": "0.15.0",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
@@ -15,7 +15,7 @@
|
||||
],
|
||||
"scripts": {
|
||||
"build": "turbo run build",
|
||||
"dev": "turbo run dev",
|
||||
"dev": "turbo run dev --concurrency=13",
|
||||
"start": "turbo run start",
|
||||
"lint": "turbo run lint",
|
||||
"clean": "turbo run clean",
|
||||
@@ -34,4 +34,4 @@
|
||||
"@types/react": "18.2.42"
|
||||
},
|
||||
"packageManager": "yarn@1.22.19"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/editor-core",
|
||||
"version": "0.14.0",
|
||||
"version": "0.15.0",
|
||||
"description": "Core Editor that powers Plane",
|
||||
"private": true,
|
||||
"main": "./dist/index.mjs",
|
||||
@@ -30,7 +30,6 @@
|
||||
"dependencies": {
|
||||
"@tiptap/core": "^2.1.13",
|
||||
"@tiptap/extension-blockquote": "^2.1.13",
|
||||
"@tiptap/extension-code": "^2.1.13",
|
||||
"@tiptap/extension-code-block-lowlight": "^2.1.13",
|
||||
"@tiptap/extension-color": "^2.1.13",
|
||||
"@tiptap/extension-image": "^2.1.13",
|
||||
|
||||
@@ -34,8 +34,32 @@ export const toggleUnderline = (editor: Editor, range?: Range) => {
|
||||
};
|
||||
|
||||
export const toggleCodeBlock = (editor: Editor, range?: Range) => {
|
||||
if (range) editor.chain().focus().deleteRange(range).toggleCodeBlock().run();
|
||||
else editor.chain().focus().toggleCodeBlock().run();
|
||||
// Check if code block is active then toggle code block
|
||||
if (editor.isActive("codeBlock")) {
|
||||
if (range) {
|
||||
editor.chain().focus().deleteRange(range).toggleCodeBlock().run();
|
||||
return;
|
||||
}
|
||||
editor.chain().focus().toggleCodeBlock().run();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if user hasn't selected any text
|
||||
const isSelectionEmpty = editor.state.selection.empty;
|
||||
|
||||
if (isSelectionEmpty) {
|
||||
if (range) {
|
||||
editor.chain().focus().deleteRange(range).toggleCodeBlock().run();
|
||||
return;
|
||||
}
|
||||
editor.chain().focus().toggleCodeBlock().run();
|
||||
} else {
|
||||
if (range) {
|
||||
editor.chain().focus().deleteRange(range).toggleCode().run();
|
||||
return;
|
||||
}
|
||||
editor.chain().focus().toggleCode().run();
|
||||
}
|
||||
};
|
||||
|
||||
export const toggleOrderedList = (editor: Editor, range?: Range) => {
|
||||
@@ -59,8 +83,8 @@ export const toggleStrike = (editor: Editor, range?: Range) => {
|
||||
};
|
||||
|
||||
export const toggleBlockquote = (editor: Editor, range?: Range) => {
|
||||
if (range) editor.chain().focus().deleteRange(range).toggleNode("paragraph", "paragraph").toggleBlockquote().run();
|
||||
else editor.chain().focus().toggleNode("paragraph", "paragraph").toggleBlockquote().run();
|
||||
if (range) editor.chain().focus().deleteRange(range).toggleBlockquote().run();
|
||||
else editor.chain().focus().toggleBlockquote().run();
|
||||
};
|
||||
|
||||
export const insertTableCommand = (editor: Editor, range?: Range) => {
|
||||
|
||||
@@ -12,7 +12,7 @@ export const EditorContainer = ({ editor, editorClassNames, hideDragHandle, chil
|
||||
<div
|
||||
id="editor-container"
|
||||
onClick={() => {
|
||||
editor?.chain().focus().run();
|
||||
editor?.chain().focus(undefined, { scrollIntoView: false }).run();
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
hideDragHandle?.();
|
||||
|
||||
@@ -1,12 +1,80 @@
|
||||
import { markInputRule, markPasteRule } from "@tiptap/core";
|
||||
import Code from "@tiptap/extension-code";
|
||||
import { Mark, markInputRule, markPasteRule, mergeAttributes } from "@tiptap/core";
|
||||
|
||||
export const inputRegex = /(?<!`)`([^`]*)`(?!`)/;
|
||||
export const pasteRegex = /(?<!`)`([^`]+)`(?!`)/g;
|
||||
export interface CodeOptions {
|
||||
HTMLAttributes: Record<string, any>;
|
||||
}
|
||||
|
||||
declare module "@tiptap/core" {
|
||||
interface Commands<ReturnType> {
|
||||
code: {
|
||||
/**
|
||||
* Set a code mark
|
||||
*/
|
||||
setCode: () => ReturnType;
|
||||
/**
|
||||
* Toggle inline code
|
||||
*/
|
||||
toggleCode: () => ReturnType;
|
||||
/**
|
||||
* Unset a code mark
|
||||
*/
|
||||
unsetCode: () => ReturnType;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const inputRegex = /(?:^|\s)((?:`)((?:[^`]+))(?:`))$/;
|
||||
export const pasteRegex = /(?:^|\s)((?:`)((?:[^`]+))(?:`))/g;
|
||||
|
||||
export const CustomCodeInlineExtension = Mark.create<CodeOptions>({
|
||||
name: "code",
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
HTMLAttributes: {
|
||||
class: "rounded-md bg-custom-primary-30 mx-1 px-1 py-[2px] font-mono font-medium text-custom-text-1000",
|
||||
spellcheck: "false",
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
excludes: "_",
|
||||
|
||||
code: true,
|
||||
|
||||
export const CustomCodeInlineExtension = Code.extend({
|
||||
exitable: true,
|
||||
inclusive: false,
|
||||
|
||||
parseHTML() {
|
||||
return [{ tag: "code" }];
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ["code", mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0];
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
setCode:
|
||||
() =>
|
||||
({ commands }) =>
|
||||
commands.setMark(this.name),
|
||||
toggleCode:
|
||||
() =>
|
||||
({ commands }) =>
|
||||
commands.toggleMark(this.name),
|
||||
unsetCode:
|
||||
() =>
|
||||
({ commands }) =>
|
||||
commands.unsetMark(this.name),
|
||||
};
|
||||
},
|
||||
|
||||
addKeyboardShortcuts() {
|
||||
return {
|
||||
"Mod-e": () => this.editor.commands.toggleCode(),
|
||||
};
|
||||
},
|
||||
|
||||
addInputRules() {
|
||||
return [
|
||||
markInputRule({
|
||||
@@ -15,6 +83,7 @@ export const CustomCodeInlineExtension = Code.extend({
|
||||
}),
|
||||
];
|
||||
},
|
||||
|
||||
addPasteRules() {
|
||||
return [
|
||||
markPasteRule({
|
||||
@@ -23,9 +92,4 @@ export const CustomCodeInlineExtension = Code.extend({
|
||||
}),
|
||||
];
|
||||
},
|
||||
}).configure({
|
||||
HTMLAttributes: {
|
||||
class: "rounded-md bg-custom-primary-30 mx-1 px-1 py-[2px] font-mono font-medium text-custom-text-1000",
|
||||
spellcheck: "false",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -53,11 +53,6 @@ export const CoreEditorExtensions = (
|
||||
class: "leading-normal -mb-2",
|
||||
},
|
||||
},
|
||||
hardBreak: {
|
||||
HTMLAttributes: {
|
||||
class: "p-2",
|
||||
},
|
||||
},
|
||||
code: false,
|
||||
codeBlock: false,
|
||||
horizontalRule: false,
|
||||
|
||||
@@ -106,7 +106,7 @@ export const TodoListItem = (editor: Editor): EditorMenuItem => ({
|
||||
|
||||
export const CodeItem = (editor: Editor): EditorMenuItem => ({
|
||||
name: "code",
|
||||
isActive: () => editor?.isActive("code"),
|
||||
isActive: () => editor?.isActive("code") || editor?.isActive("codeBlock"),
|
||||
command: () => toggleCodeBlock(editor),
|
||||
icon: CodeIcon,
|
||||
});
|
||||
@@ -120,7 +120,7 @@ export const NumberedListItem = (editor: Editor): EditorMenuItem => ({
|
||||
|
||||
export const QuoteItem = (editor: Editor): EditorMenuItem => ({
|
||||
name: "quote",
|
||||
isActive: () => editor?.isActive("quote"),
|
||||
isActive: () => editor?.isActive("blockquote"),
|
||||
command: () => toggleBlockquote(editor),
|
||||
icon: QuoteIcon,
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/document-editor",
|
||||
"version": "0.14.0",
|
||||
"version": "0.15.0",
|
||||
"description": "Package that powers Plane's Pages Editor",
|
||||
"main": "./dist/index.mjs",
|
||||
"module": "./dist/index.mjs",
|
||||
|
||||
@@ -27,11 +27,20 @@ type IPageRenderer = {
|
||||
}) => void;
|
||||
editorClassNames: string;
|
||||
editorContentCustomClassNames?: string;
|
||||
hideDragHandle?: () => void;
|
||||
readonly: boolean;
|
||||
};
|
||||
|
||||
export const PageRenderer = (props: IPageRenderer) => {
|
||||
const { documentDetails, editor, editorClassNames, editorContentCustomClassNames, updatePageTitle, readonly } = props;
|
||||
const {
|
||||
documentDetails,
|
||||
editor,
|
||||
editorClassNames,
|
||||
editorContentCustomClassNames,
|
||||
updatePageTitle,
|
||||
readonly,
|
||||
hideDragHandle,
|
||||
} = props;
|
||||
|
||||
const [pageTitle, setPagetitle] = useState(documentDetails.title);
|
||||
|
||||
@@ -65,14 +74,6 @@ export const PageRenderer = (props: IPageRenderer) => {
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const switchLinkView = (view: "LinkPreview" | "LinkEditView" | "LinkInputView") => {
|
||||
if (!linkViewProps) return;
|
||||
setLinkViewProps({
|
||||
...linkViewProps,
|
||||
view: view,
|
||||
});
|
||||
};
|
||||
|
||||
const handleLinkHover = useCallback(
|
||||
(event: React.MouseEvent) => {
|
||||
if (!editor) return;
|
||||
@@ -167,7 +168,7 @@ export const PageRenderer = (props: IPageRenderer) => {
|
||||
/>
|
||||
)}
|
||||
<div className="flex relative h-full w-full flex-col pr-5 editor-renderer" onMouseOver={handleLinkHover}>
|
||||
<EditorContainer editor={editor} editorClassNames={editorClassNames}>
|
||||
<EditorContainer hideDragHandle={hideDragHandle} editor={editor} editorClassNames={editorClassNames}>
|
||||
<EditorContentWrapper editor={editor} editorContentCustomClassNames={editorContentCustomClassNames} />
|
||||
</EditorContainer>
|
||||
</div>
|
||||
|
||||
@@ -1,55 +1,29 @@
|
||||
import Placeholder from "@tiptap/extension-placeholder";
|
||||
import { IssueWidgetExtension } from "src/ui/extensions/widgets/issue-embed-widget";
|
||||
|
||||
import { IIssueEmbedConfig } from "src/ui/extensions/widgets/issue-embed-widget/types";
|
||||
import { IssueWidgetPlaceholder } from "src/ui/extensions/widgets/issue-embed-widget";
|
||||
|
||||
import { SlashCommand, DragAndDrop } from "@plane/editor-extensions";
|
||||
import { ISlashCommandItem, UploadImage } from "@plane/editor-core";
|
||||
import { IssueSuggestions } from "src/ui/extensions/widgets/issue-embed-suggestion-list";
|
||||
import { LayersIcon } from "@plane/ui";
|
||||
import { UploadImage } from "@plane/editor-core";
|
||||
|
||||
export const DocumentEditorExtensions = (
|
||||
uploadFile: UploadImage,
|
||||
issueEmbedConfig?: IIssueEmbedConfig,
|
||||
setHideDragHandle?: (hideDragHandlerFromDragDrop: () => void) => void,
|
||||
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void
|
||||
) => {
|
||||
const additionalOptions: ISlashCommandItem[] = [
|
||||
{
|
||||
key: "issue_embed",
|
||||
title: "Issue embed",
|
||||
description: "Embed an issue from the project.",
|
||||
searchTerms: ["issue", "link", "embed"],
|
||||
icon: <LayersIcon className="h-3.5 w-3.5" />,
|
||||
command: ({ editor, range }) => {
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.insertContentAt(
|
||||
range,
|
||||
"<p class='text-sm bg-gray-300 w-fit pl-3 pr-3 pt-1 pb-1 rounded shadow-sm'>#issue_</p>\n"
|
||||
)
|
||||
.run();
|
||||
},
|
||||
) => [
|
||||
SlashCommand(uploadFile, setIsSubmitting),
|
||||
DragAndDrop(setHideDragHandle),
|
||||
Placeholder.configure({
|
||||
placeholder: ({ node }) => {
|
||||
if (node.type.name === "heading") {
|
||||
return `Heading ${node.attrs.level}`;
|
||||
}
|
||||
if (node.type.name === "image" || node.type.name === "table") {
|
||||
return "";
|
||||
}
|
||||
|
||||
return "Press '/' for commands...";
|
||||
},
|
||||
];
|
||||
includeChildren: true,
|
||||
}),
|
||||
IssueWidgetPlaceholder(),
|
||||
];
|
||||
|
||||
return [
|
||||
SlashCommand(uploadFile, setIsSubmitting, additionalOptions),
|
||||
DragAndDrop,
|
||||
Placeholder.configure({
|
||||
placeholder: ({ node }) => {
|
||||
if (node.type.name === "heading") {
|
||||
return `Heading ${node.attrs.level}`;
|
||||
}
|
||||
if (node.type.name === "image" || node.type.name === "table") {
|
||||
return "";
|
||||
}
|
||||
|
||||
return "Press '/' for commands...";
|
||||
},
|
||||
includeChildren: true,
|
||||
}),
|
||||
IssueWidgetExtension({ issueEmbedConfig }),
|
||||
IssueSuggestions(issueEmbedConfig ? issueEmbedConfig.issues : []),
|
||||
];
|
||||
};
|
||||
|
||||
+23
-12
@@ -78,7 +78,6 @@ const IssueSuggestionList = ({
|
||||
const navigationKeys = ["ArrowUp", "ArrowDown", "Enter", "Tab"];
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
if (navigationKeys.includes(e.key)) {
|
||||
e.preventDefault();
|
||||
// if (editor.isFocused) {
|
||||
// editor.chain().blur();
|
||||
// commandListContainer.current?.focus();
|
||||
@@ -87,7 +86,6 @@ const IssueSuggestionList = ({
|
||||
setSelectedIndex(
|
||||
(selectedIndex + displayedItems[currentSection].length - 1) % displayedItems[currentSection].length
|
||||
);
|
||||
e.stopPropagation();
|
||||
return true;
|
||||
}
|
||||
if (e.key === "ArrowDown") {
|
||||
@@ -102,12 +100,10 @@ const IssueSuggestionList = ({
|
||||
[currentSection]: [...prevItems[currentSection], ...nextItems],
|
||||
}));
|
||||
}
|
||||
e.stopPropagation();
|
||||
return true;
|
||||
}
|
||||
if (e.key === "Enter") {
|
||||
selectItem(currentSection, selectedIndex);
|
||||
e.stopPropagation();
|
||||
return true;
|
||||
}
|
||||
if (e.key === "Tab") {
|
||||
@@ -115,7 +111,6 @@ const IssueSuggestionList = ({
|
||||
const nextSectionIndex = (currentSectionIndex + 1) % sections.length;
|
||||
setCurrentSection(sections[nextSectionIndex]);
|
||||
setSelectedIndex(0);
|
||||
e.stopPropagation();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -150,7 +145,7 @@ const IssueSuggestionList = ({
|
||||
<div
|
||||
id="issue-list-container"
|
||||
ref={commandListContainer}
|
||||
className="fixed z-[10] max-h-80 w-60 overflow-y-auto overflow-x-hidden rounded-md border border-custom-border-100 bg-custom-background-100 px-1 shadow-custom-shadow-xs transition-all"
|
||||
className=" fixed z-[10] max-h-80 w-60 overflow-y-auto overflow-x-hidden rounded-md border border-custom-border-100 bg-custom-background-100 px-1 shadow-custom-shadow-xs transition-all"
|
||||
>
|
||||
{sections.map((section) => {
|
||||
const sectionItems = displayedItems[section];
|
||||
@@ -193,29 +188,35 @@ const IssueSuggestionList = ({
|
||||
</div>
|
||||
) : null;
|
||||
};
|
||||
|
||||
export const IssueListRenderer = () => {
|
||||
let component: ReactRenderer | null = null;
|
||||
let popup: any | null = null;
|
||||
|
||||
return {
|
||||
onStart: (props: { editor: Editor; clientRect?: (() => DOMRect | null) | null }) => {
|
||||
const container = document.querySelector(".frame-renderer") as HTMLElement;
|
||||
component = new ReactRenderer(IssueSuggestionList, {
|
||||
props,
|
||||
// @ts-ignore
|
||||
editor: props.editor,
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
popup = tippy("body", {
|
||||
popup = tippy(".frame-renderer", {
|
||||
flipbehavior: ["bottom", "top"],
|
||||
appendTo: () => document.querySelector(".frame-renderer") as HTMLElement,
|
||||
flip: true,
|
||||
flipOnUpdate: true,
|
||||
getReferenceClientRect: props.clientRect,
|
||||
appendTo: () => document.querySelector("#editor-container"),
|
||||
content: component.element,
|
||||
showOnCreate: true,
|
||||
interactive: true,
|
||||
trigger: "manual",
|
||||
placement: "bottom-start",
|
||||
});
|
||||
|
||||
container.addEventListener("scroll", () => {
|
||||
popup?.[0].destroy();
|
||||
});
|
||||
},
|
||||
onUpdate: (props: { editor: Editor; clientRect?: (() => DOMRect | null) | null }) => {
|
||||
component?.updateProps(props);
|
||||
@@ -230,10 +231,20 @@ export const IssueListRenderer = () => {
|
||||
popup?.[0].hide();
|
||||
return true;
|
||||
}
|
||||
// @ts-ignore
|
||||
return component?.ref?.onKeyDown(props);
|
||||
|
||||
const navigationKeys = ["ArrowUp", "ArrowDown", "Enter", "Tab"];
|
||||
if (navigationKeys.includes(props.event.key)) {
|
||||
// @ts-ignore
|
||||
component?.ref?.onKeyDown(props);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
onExit: (e) => {
|
||||
const container = document.querySelector(".frame-renderer") as HTMLElement;
|
||||
if (container) {
|
||||
container.removeEventListener("scroll", () => {});
|
||||
}
|
||||
popup?.[0].destroy();
|
||||
setTimeout(() => {
|
||||
component?.destroy();
|
||||
|
||||
+1
-9
@@ -1,11 +1,3 @@
|
||||
import { IssueWidget } from "src/ui/extensions/widgets/issue-embed-widget/issue-widget-node";
|
||||
import { IIssueEmbedConfig } from "src/ui/extensions/widgets/issue-embed-widget/types";
|
||||
|
||||
interface IssueWidgetExtensionProps {
|
||||
issueEmbedConfig?: IIssueEmbedConfig;
|
||||
}
|
||||
|
||||
export const IssueWidgetExtension = ({ issueEmbedConfig }: IssueWidgetExtensionProps) =>
|
||||
IssueWidget.configure({
|
||||
issueEmbedConfig,
|
||||
});
|
||||
export const IssueWidgetPlaceholder = () => IssueWidget.configure({});
|
||||
|
||||
+26
-70
@@ -1,77 +1,33 @@
|
||||
// @ts-nocheck
|
||||
import { useState, useEffect } from "react";
|
||||
import { Button } from "@plane/ui";
|
||||
import { NodeViewWrapper } from "@tiptap/react";
|
||||
import { Avatar, AvatarGroup, Loader, PriorityIcon } from "@plane/ui";
|
||||
import { Calendar, AlertTriangle } from "lucide-react";
|
||||
import { Crown } from "lucide-react";
|
||||
|
||||
export const IssueWidgetCard = (props) => {
|
||||
const [loading, setLoading] = useState<number>(1);
|
||||
const [issueDetails, setIssueDetails] = useState();
|
||||
|
||||
useEffect(() => {
|
||||
props.issueEmbedConfig
|
||||
.fetchIssue(props.node.attrs.entity_identifier)
|
||||
.then((issue) => {
|
||||
setIssueDetails(issue);
|
||||
setLoading(0);
|
||||
})
|
||||
.catch(() => {
|
||||
setLoading(-1);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const completeIssueEmbedAction = () => {
|
||||
props.issueEmbedConfig.clickAction(issueDetails.id, props.node.attrs.title);
|
||||
};
|
||||
|
||||
return (
|
||||
<NodeViewWrapper className="issue-embed-component m-2">
|
||||
{loading == 0 ? (
|
||||
<div
|
||||
onClick={completeIssueEmbedAction}
|
||||
className={`${
|
||||
props.selected ? "border-custom-primary-200 border-[2px]" : ""
|
||||
} w-full cursor-pointer space-y-2 rounded-md border-[0.5px] border-custom-border-200 p-3 shadow-custom-shadow-2xs`}
|
||||
>
|
||||
<h5 className="text-xs text-custom-text-300">
|
||||
{issueDetails.project_detail.identifier}-{issueDetails.sequence_id}
|
||||
</h5>
|
||||
<h4 className="break-words text-sm font-medium">{issueDetails.name}</h4>
|
||||
<div className="flex flex-wrap items-center gap-x-3 gap-y-2">
|
||||
<div>
|
||||
<PriorityIcon priority={issueDetails.priority} />
|
||||
export const IssueWidgetCard = (props) => (
|
||||
<NodeViewWrapper className="issue-embed-component m-2">
|
||||
<div
|
||||
className={`${
|
||||
props.selected ? "border-custom-primary-200 border-[2px]" : ""
|
||||
} w-full h-[100px] cursor-pointer space-y-2 rounded-md border-[0.5px] border-custom-border-200 shadow-custom-shadow-2xs`}
|
||||
>
|
||||
<h5 className="h-[20%] text-xs text-custom-text-300 p-2">
|
||||
{props.node.attrs.project_identifier}-{props.node.attrs.sequence_id}
|
||||
</h5>
|
||||
<div className="relative h-[71%]">
|
||||
<div className="h-full backdrop-filter backdrop-blur-[30px] bg-custom-background-80 bg-opacity-30 flex items-center w-full justify-between gap-5 mt-2.5 pl-4 pr-5 py-3 max-md:max-w-full max-md:flex-wrap relative">
|
||||
<div className="flex gap-2 items-center">
|
||||
<div className="rounded">
|
||||
<Crown className="m-2" size={16} color="#FFBA18" />
|
||||
</div>
|
||||
<div>
|
||||
<AvatarGroup size="sm">
|
||||
{issueDetails.assignee_details.map((assignee) => (
|
||||
<Avatar key={assignee.id} name={assignee.display_name} src={assignee.avatar} className={"m-0"} />
|
||||
))}
|
||||
</AvatarGroup>
|
||||
<div className="text-custom-text text-sm">
|
||||
Embed and access issues in pages seamlessly, upgrade to plane pro now.
|
||||
</div>
|
||||
{issueDetails.target_date && (
|
||||
<div className="flex h-5 items-center gap-1 rounded border-[0.5px] border-custom-border-300 px-2.5 py-1 text-xs text-custom-text-100">
|
||||
<Calendar className="h-3 w-3" strokeWidth={1.5} />
|
||||
{new Date(issueDetails.target_date).toLocaleDateString()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<a href="https://plane.so/pricing" target="_blank" rel="noreferrer">
|
||||
<Button>Upgrade</Button>
|
||||
</a>
|
||||
</div>
|
||||
) : loading == -1 ? (
|
||||
<div className="flex items-center gap-[8px] rounded border-2 border-[#D97706] bg-[#FFFBEB] pb-[10px] pl-[13px] pt-[10px] text-[#D97706]">
|
||||
<AlertTriangle color={"#D97706"} />
|
||||
{"This Issue embed is not found in any project. It can no longer be updated or accessed from here."}
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full space-y-2 rounded-md border-[0.5px] border-custom-border-200 p-3 shadow-custom-shadow-2xs">
|
||||
<Loader className={"px-6"}>
|
||||
<Loader.Item height={"30px"} />
|
||||
<div className={"mt-3 space-y-2"}>
|
||||
<Loader.Item height={"20px"} width={"70%"} />
|
||||
<Loader.Item height={"20px"} width={"60%"} />
|
||||
</div>
|
||||
</Loader>
|
||||
</div>
|
||||
)}
|
||||
</NodeViewWrapper>
|
||||
);
|
||||
};
|
||||
</div>
|
||||
</div>
|
||||
</NodeViewWrapper>
|
||||
);
|
||||
|
||||
+1
-3
@@ -34,9 +34,7 @@ export const IssueWidget = Node.create({
|
||||
},
|
||||
|
||||
addNodeView() {
|
||||
return ReactNodeViewRenderer((props: Object) => (
|
||||
<IssueWidgetCard {...props} issueEmbedConfig={this.options.issueEmbedConfig} />
|
||||
));
|
||||
return ReactNodeViewRenderer((props: Object) => <IssueWidgetCard {...props} />);
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
export interface IEmbedConfig {
|
||||
issueEmbedConfig: IIssueEmbedConfig;
|
||||
}
|
||||
|
||||
export interface IIssueEmbedConfig {
|
||||
fetchIssue: (issueId: string) => Promise<any>;
|
||||
clickAction: (issueId: string, issueTitle: string) => void;
|
||||
issues: Array<any>;
|
||||
}
|
||||
@@ -10,7 +10,6 @@ import { DocumentDetails } from "src/types/editor-types";
|
||||
import { PageRenderer } from "src/ui/components/page-renderer";
|
||||
import { getMenuOptions } from "src/utils/menu-options";
|
||||
import { useRouter } from "next/router";
|
||||
import { IEmbedConfig } from "src/ui/extensions/widgets/issue-embed-widget/types";
|
||||
|
||||
interface IDocumentEditor {
|
||||
// document info
|
||||
@@ -47,7 +46,6 @@ interface IDocumentEditor {
|
||||
duplicationConfig?: IDuplicationConfig;
|
||||
pageLockConfig?: IPageLockConfig;
|
||||
pageArchiveConfig?: IPageArchiveConfig;
|
||||
embedConfig?: IEmbedConfig;
|
||||
}
|
||||
interface DocumentEditorProps extends IDocumentEditor {
|
||||
forwardedRef?: React.Ref<EditorHandle>;
|
||||
@@ -75,17 +73,23 @@ const DocumentEditor = ({
|
||||
duplicationConfig,
|
||||
pageLockConfig,
|
||||
pageArchiveConfig,
|
||||
embedConfig,
|
||||
updatePageTitle,
|
||||
cancelUploadImage,
|
||||
onActionCompleteHandler,
|
||||
rerenderOnPropsChange,
|
||||
}: IDocumentEditor) => {
|
||||
// const [alert, setAlert] = useState<string>("")
|
||||
const { markings, updateMarkings } = useEditorMarkings();
|
||||
const [sidePeekVisible, setSidePeekVisible] = useState(true);
|
||||
const router = useRouter();
|
||||
|
||||
const [hideDragHandleOnMouseLeave, setHideDragHandleOnMouseLeave] = React.useState<() => void>(() => {});
|
||||
|
||||
// this essentially sets the hideDragHandle function from the DragAndDrop extension as the Plugin
|
||||
// loads such that we can invoke it from react when the cursor leaves the container
|
||||
const setHideDragHandleFunction = (hideDragHandlerFromDragDrop: () => void) => {
|
||||
setHideDragHandleOnMouseLeave(() => hideDragHandlerFromDragDrop);
|
||||
};
|
||||
|
||||
const editor = useEditor({
|
||||
onChange(json, html) {
|
||||
updateMarkings(json);
|
||||
@@ -104,7 +108,7 @@ const DocumentEditor = ({
|
||||
cancelUploadImage,
|
||||
rerenderOnPropsChange,
|
||||
forwardedRef,
|
||||
extensions: DocumentEditorExtensions(uploadFile, embedConfig?.issueEmbedConfig, setIsSubmitting),
|
||||
extensions: DocumentEditorExtensions(uploadFile, setHideDragHandleFunction, setIsSubmitting),
|
||||
});
|
||||
|
||||
if (!editor) {
|
||||
@@ -145,13 +149,14 @@ const DocumentEditor = ({
|
||||
documentDetails={documentDetails}
|
||||
isSubmitting={isSubmitting}
|
||||
/>
|
||||
<div className="flex h-full w-full overflow-y-auto">
|
||||
<div className="flex h-full w-full overflow-y-auto frame-renderer">
|
||||
<div className="sticky top-0 h-full w-56 flex-shrink-0 lg:w-72">
|
||||
<SummarySideBar editor={editor} markings={markings} sidePeekVisible={sidePeekVisible} />
|
||||
</div>
|
||||
<div className="h-full w-[calc(100%-14rem)] lg:w-[calc(100%-18rem-18rem)]">
|
||||
<div className="h-full w-[calc(100%-14rem)] lg:w-[calc(100%-18rem-18rem)] page-renderer">
|
||||
<PageRenderer
|
||||
onActionCompleteHandler={onActionCompleteHandler}
|
||||
hideDragHandle={hideDragHandleOnMouseLeave}
|
||||
readonly={false}
|
||||
editor={editor}
|
||||
editorContentCustomClassNames={editorContentCustomClassNames}
|
||||
|
||||
@@ -4,12 +4,11 @@ import { useState, forwardRef, useEffect } from "react";
|
||||
import { EditorHeader } from "src/ui/components/editor-header";
|
||||
import { PageRenderer } from "src/ui/components/page-renderer";
|
||||
import { SummarySideBar } from "src/ui/components/summary-side-bar";
|
||||
import { IssueWidgetExtension } from "src/ui/extensions/widgets/issue-embed-widget";
|
||||
import { IEmbedConfig } from "src/ui/extensions/widgets/issue-embed-widget/types";
|
||||
import { useEditorMarkings } from "src/hooks/use-editor-markings";
|
||||
import { DocumentDetails } from "src/types/editor-types";
|
||||
import { IPageArchiveConfig, IPageLockConfig, IDuplicationConfig } from "src/types/menu-actions";
|
||||
import { getMenuOptions } from "src/utils/menu-options";
|
||||
import { IssueWidgetPlaceholder } from "../extensions/widgets/issue-embed-widget";
|
||||
|
||||
interface IDocumentReadOnlyEditor {
|
||||
value: string;
|
||||
@@ -29,7 +28,6 @@ interface IDocumentReadOnlyEditor {
|
||||
message: string;
|
||||
type: "success" | "error" | "warning" | "info";
|
||||
}) => void;
|
||||
embedConfig?: IEmbedConfig;
|
||||
}
|
||||
|
||||
interface DocumentReadOnlyEditorProps extends IDocumentReadOnlyEditor {
|
||||
@@ -51,7 +49,6 @@ const DocumentReadOnlyEditor = ({
|
||||
pageDuplicationConfig,
|
||||
pageLockConfig,
|
||||
pageArchiveConfig,
|
||||
embedConfig,
|
||||
rerenderOnPropsChange,
|
||||
onActionCompleteHandler,
|
||||
}: DocumentReadOnlyEditorProps) => {
|
||||
@@ -63,7 +60,7 @@ const DocumentReadOnlyEditor = ({
|
||||
value,
|
||||
forwardedRef,
|
||||
rerenderOnPropsChange,
|
||||
extensions: [IssueWidgetExtension({ issueEmbedConfig: embedConfig?.issueEmbedConfig })],
|
||||
extensions: [IssueWidgetPlaceholder()],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -105,11 +102,11 @@ const DocumentReadOnlyEditor = ({
|
||||
documentDetails={documentDetails}
|
||||
archivedAt={pageArchiveConfig && pageArchiveConfig.archived_at}
|
||||
/>
|
||||
<div className="flex h-full w-full overflow-y-auto">
|
||||
<div className="flex h-full w-full overflow-y-auto frame-renderer">
|
||||
<div className="sticky top-0 h-full w-56 flex-shrink-0 lg:w-80">
|
||||
<SummarySideBar editor={editor} markings={markings} sidePeekVisible={sidePeekVisible} />
|
||||
</div>
|
||||
<div className="h-full w-[calc(100%-14rem)] lg:w-[calc(100%-18rem-18rem)]">
|
||||
<div className="h-full w-[calc(100%-14rem)] lg:w-[calc(100%-18rem-18rem)] page-renderer">
|
||||
<PageRenderer
|
||||
onActionCompleteHandler={onActionCompleteHandler}
|
||||
updatePageTitle={() => Promise.resolve()}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/editor-extensions",
|
||||
"version": "0.14.0",
|
||||
"version": "0.15.0",
|
||||
"description": "Package that powers Plane's Editor with extensions",
|
||||
"private": true,
|
||||
"main": "./dist/index.mjs",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/lite-text-editor",
|
||||
"version": "0.14.0",
|
||||
"version": "0.15.0",
|
||||
"description": "Package that powers Plane's Comment Editor",
|
||||
"private": true,
|
||||
"main": "./dist/index.mjs",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/rich-text-editor",
|
||||
"version": "0.14.0",
|
||||
"version": "0.15.0",
|
||||
"description": "Rich Text Editor that powers Plane",
|
||||
"private": true,
|
||||
"main": "./dist/index.mjs",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "eslint-config-custom",
|
||||
"private": true,
|
||||
"version": "0.14.0",
|
||||
"version": "0.15.0",
|
||||
"main": "index.js",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tailwind-config-custom",
|
||||
"version": "0.14.0",
|
||||
"version": "0.15.0",
|
||||
"description": "common tailwind configuration across monorepo",
|
||||
"main": "index.js",
|
||||
"private": true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tsconfig",
|
||||
"version": "0.14.0",
|
||||
"version": "0.15.0",
|
||||
"private": true,
|
||||
"files": [
|
||||
"base.json",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"name": "@plane/types",
|
||||
"version": "0.14.0",
|
||||
"version": "0.15.0",
|
||||
"private": true,
|
||||
"main": "./src/index.d.ts"
|
||||
}
|
||||
|
||||
Vendored
+5
-5
@@ -1,14 +1,14 @@
|
||||
export interface IAppConfig {
|
||||
email_password_login: boolean;
|
||||
file_size_limit: number;
|
||||
google_client_id: string | null;
|
||||
github_app_name: string | null;
|
||||
github_client_id: string | null;
|
||||
magic_login: boolean;
|
||||
slack_client_id: string | null;
|
||||
posthog_api_key: string | null;
|
||||
posthog_host: string | null;
|
||||
google_client_id: string | null;
|
||||
has_openai_configured: boolean;
|
||||
has_unsplash_configured: boolean;
|
||||
is_smtp_configured: boolean;
|
||||
magic_login: boolean;
|
||||
posthog_api_key: string | null;
|
||||
posthog_host: string | null;
|
||||
slack_client_id: string | null;
|
||||
}
|
||||
|
||||
Vendored
+1
-2
@@ -47,7 +47,6 @@ export interface ICycle {
|
||||
};
|
||||
workspace: string;
|
||||
workspace_detail: IWorkspaceLite;
|
||||
issues?: TIssue[];
|
||||
}
|
||||
|
||||
export type TAssigneesDistribution = {
|
||||
@@ -62,7 +61,7 @@ export type TAssigneesDistribution = {
|
||||
};
|
||||
|
||||
export type TCompletionChartDistribution = {
|
||||
[key: string]: number;
|
||||
[key: string]: number | null;
|
||||
};
|
||||
|
||||
export type TLabelsDistribution = {
|
||||
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
import { TIssue } from "../issues/base";
|
||||
|
||||
export enum EInboxStatus {
|
||||
PENDING = -2,
|
||||
REJECT = -1,
|
||||
SNOOZED = 0,
|
||||
ACCEPTED = 1,
|
||||
DUPLICATE = 2,
|
||||
}
|
||||
|
||||
export type TInboxStatus =
|
||||
| EInboxStatus.PENDING
|
||||
| EInboxStatus.REJECT
|
||||
| EInboxStatus.SNOOZED
|
||||
| EInboxStatus.ACCEPTED
|
||||
| EInboxStatus.DUPLICATE;
|
||||
|
||||
export type TInboxIssueDetail = {
|
||||
id?: string;
|
||||
source: "in-app";
|
||||
status: TInboxStatus;
|
||||
duplicate_to: string | undefined;
|
||||
snoozed_till: Date | undefined;
|
||||
};
|
||||
|
||||
export type TInboxIssueDetailMap = Record<
|
||||
string,
|
||||
Record<string, TInboxIssueDetail>
|
||||
>; // inbox_id -> issue_id -> TInboxIssueDetail
|
||||
|
||||
export type TInboxIssueDetailIdMap = Record<string, string[]>; // inbox_id -> issue_id[]
|
||||
|
||||
export type TInboxIssueExtendedDetail = TIssue & {
|
||||
issue_inbox: TInboxIssueDetail[];
|
||||
};
|
||||
|
||||
// property type checks
|
||||
export type TInboxPendingStatus = {
|
||||
status: EInboxStatus.PENDING;
|
||||
};
|
||||
|
||||
export type TInboxRejectStatus = {
|
||||
status: EInboxStatus.REJECT;
|
||||
};
|
||||
|
||||
export type TInboxSnoozedStatus = {
|
||||
status: EInboxStatus.SNOOZED;
|
||||
snoozed_till: Date;
|
||||
};
|
||||
|
||||
export type TInboxAcceptedStatus = {
|
||||
status: EInboxStatus.ACCEPTED;
|
||||
};
|
||||
|
||||
export type TInboxDuplicateStatus = {
|
||||
status: EInboxStatus.DUPLICATE;
|
||||
duplicate_to: string; // issue_id
|
||||
};
|
||||
|
||||
export type TInboxDetailedStatus =
|
||||
| TInboxPendingStatus
|
||||
| TInboxRejectStatus
|
||||
| TInboxSnoozedStatus
|
||||
| TInboxAcceptedStatus
|
||||
| TInboxDuplicateStatus;
|
||||
Vendored
+27
@@ -0,0 +1,27 @@
|
||||
export type TInboxIssueFilterOptions = {
|
||||
priority: string[];
|
||||
inbox_status: number[];
|
||||
};
|
||||
|
||||
export type TInboxIssueQueryParams = "priority" | "inbox_status";
|
||||
|
||||
export type TInboxIssueFilters = { filters: TInboxIssueFilterOptions };
|
||||
|
||||
export type TInbox = {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
workspace: string;
|
||||
project: string;
|
||||
is_default: boolean;
|
||||
view_props: TInboxIssueFilters;
|
||||
created_by: string;
|
||||
updated_by: string;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
pending_issue_count: number;
|
||||
};
|
||||
|
||||
export type TInboxDetailMap = Record<string, TInbox>; // inbox_id -> TInbox
|
||||
|
||||
export type TInboxDetailIdMap = Record<string, string[]>; // project_id -> inbox_id[]
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
export * from "./inbox";
|
||||
export * from "./inbox-issue";
|
||||
Vendored
+4
@@ -13,7 +13,11 @@ export * from "./pages";
|
||||
export * from "./ai";
|
||||
export * from "./estimate";
|
||||
export * from "./importer";
|
||||
|
||||
// FIXME: Remove this after development and the refactor/mobx-store-issue branch is stable
|
||||
export * from "./inbox";
|
||||
export * from "./inbox/root";
|
||||
|
||||
export * from "./analytics";
|
||||
export * from "./calendar";
|
||||
export * from "./notifications";
|
||||
|
||||
Vendored
+1
@@ -14,6 +14,7 @@ export interface IInstance {
|
||||
namespace: string | null;
|
||||
is_telemetry_enabled: boolean;
|
||||
is_support_required: boolean;
|
||||
is_signup_screen_visited: boolean;
|
||||
created_by: string | null;
|
||||
updated_by: string | null;
|
||||
is_activated: boolean;
|
||||
|
||||
Vendored
+2
-9
@@ -109,17 +109,10 @@ export type IssuePriorities = {
|
||||
|
||||
export interface IIssueLabel {
|
||||
id: string;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
name: string;
|
||||
description: string;
|
||||
color: string;
|
||||
created_by: string;
|
||||
updated_by: string;
|
||||
project: string;
|
||||
project_detail: IProjectLite;
|
||||
workspace: string;
|
||||
workspace_detail: IWorkspaceLite;
|
||||
project_id: string;
|
||||
workspace_id: string;
|
||||
parent: string | null;
|
||||
sort_order: number;
|
||||
}
|
||||
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
export * from "./issue_activity";
|
||||
export * from "./issue_comment";
|
||||
export * from "./issue_comment_reaction";
|
||||
|
||||
import { TIssuePriorities } from "../issues";
|
||||
|
||||
// root types
|
||||
export type TIssueActivityWorkspaceDetail = {
|
||||
name: string;
|
||||
slug: string;
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type TIssueActivityProjectDetail = {
|
||||
id: string;
|
||||
identifier: string;
|
||||
name: string;
|
||||
cover_image: string;
|
||||
description: string | null;
|
||||
emoji: string | null;
|
||||
icon_prop: {
|
||||
name: string;
|
||||
color: string;
|
||||
} | null;
|
||||
};
|
||||
|
||||
export type TIssueActivityIssueDetail = {
|
||||
id: string;
|
||||
sequence_id: boolean;
|
||||
sort_order: boolean;
|
||||
name: string;
|
||||
description_html: string;
|
||||
priority: TIssuePriorities;
|
||||
start_date: string;
|
||||
target_date: string;
|
||||
is_draft: boolean;
|
||||
};
|
||||
|
||||
export type TIssueActivityUserDetail = {
|
||||
id: string;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
avatar: string;
|
||||
is_bot: boolean;
|
||||
display_name: string;
|
||||
};
|
||||
|
||||
export type TIssueActivityComment =
|
||||
| {
|
||||
id: string;
|
||||
activity_type: "COMMENT";
|
||||
created_at?: string;
|
||||
}
|
||||
| {
|
||||
id: string;
|
||||
activity_type: "ACTIVITY";
|
||||
created_at?: string;
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
import {
|
||||
TIssueActivityWorkspaceDetail,
|
||||
TIssueActivityProjectDetail,
|
||||
TIssueActivityIssueDetail,
|
||||
TIssueActivityUserDetail,
|
||||
} from "./base";
|
||||
|
||||
export type TIssueActivity = {
|
||||
id: string;
|
||||
workspace: string;
|
||||
workspace_detail: TIssueActivityWorkspaceDetail;
|
||||
project: string;
|
||||
project_detail: TIssueActivityProjectDetail;
|
||||
issue: string;
|
||||
issue_detail: TIssueActivityIssueDetail;
|
||||
actor: string;
|
||||
actor_detail: TIssueActivityUserDetail;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
created_by: string | undefined;
|
||||
updated_by: string | undefined;
|
||||
attachments: any[];
|
||||
|
||||
verb: string;
|
||||
field: string | undefined;
|
||||
old_value: string | undefined;
|
||||
new_value: string | undefined;
|
||||
comment: string | undefined;
|
||||
old_identifier: string | undefined;
|
||||
new_identifier: string | undefined;
|
||||
epoch: number;
|
||||
issue_comment: string | null;
|
||||
};
|
||||
|
||||
export type TIssueActivityMap = {
|
||||
[issue_id: string]: TIssueActivity;
|
||||
};
|
||||
|
||||
export type TIssueActivityIdMap = {
|
||||
[issue_id: string]: string[];
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
import {
|
||||
TIssueActivityWorkspaceDetail,
|
||||
TIssueActivityProjectDetail,
|
||||
TIssueActivityIssueDetail,
|
||||
TIssueActivityUserDetail,
|
||||
} from "./base";
|
||||
|
||||
export type TIssueComment = {
|
||||
id: string;
|
||||
workspace: string;
|
||||
workspace_detail: TIssueActivityWorkspaceDetail;
|
||||
project: string;
|
||||
project_detail: TIssueActivityProjectDetail;
|
||||
issue: string;
|
||||
issue_detail: TIssueActivityIssueDetail;
|
||||
actor: string;
|
||||
actor_detail: TIssueActivityUserDetail;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
created_by: string | undefined;
|
||||
updated_by: string | undefined;
|
||||
attachments: any[];
|
||||
|
||||
comment_reactions: any[];
|
||||
comment_stripped: string;
|
||||
comment_html: string;
|
||||
comment_json: object;
|
||||
external_id: string | undefined;
|
||||
external_source: string | undefined;
|
||||
access: "EXTERNAL" | "INTERNAL";
|
||||
};
|
||||
|
||||
export type TIssueCommentMap = {
|
||||
[issue_id: string]: TIssueComment;
|
||||
};
|
||||
|
||||
export type TIssueCommentIdMap = {
|
||||
[issue_id: string]: string[];
|
||||
};
|
||||
+7
-7
@@ -1,20 +1,20 @@
|
||||
export type TIssueCommentReaction = {
|
||||
id: string;
|
||||
comment: string;
|
||||
actor: string;
|
||||
reaction: string;
|
||||
workspace: string;
|
||||
project: string;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
reaction: string;
|
||||
created_by: string;
|
||||
updated_by: string;
|
||||
project: string;
|
||||
workspace: string;
|
||||
actor: string;
|
||||
comment: string;
|
||||
};
|
||||
|
||||
export type TIssueCommentReactionMap = {
|
||||
[issue_id: string]: TIssueCommentReaction;
|
||||
[reaction_id: string]: TIssueCommentReaction;
|
||||
};
|
||||
|
||||
export type TIssueCommentReactionIdMap = {
|
||||
[issue_id: string]: string[];
|
||||
[comment_id: string]: { [reaction: string]: string[] };
|
||||
};
|
||||
Vendored
+1
-2
@@ -4,9 +4,8 @@ export * from "./issue_reaction";
|
||||
export * from "./issue_link";
|
||||
export * from "./issue_attachment";
|
||||
export * from "./issue_relation";
|
||||
export * from "./issue_activity";
|
||||
export * from "./issue_comment_reaction";
|
||||
export * from "./issue_sub_issues";
|
||||
export * from "./activity/base";
|
||||
|
||||
export type TLoader = "init-loader" | "mutation" | undefined;
|
||||
|
||||
|
||||
Vendored
+1
-1
@@ -21,7 +21,7 @@ export type TIssue = {
|
||||
project_id: string;
|
||||
parent_id: string | null;
|
||||
cycle_id: string | null;
|
||||
module_id: string | null;
|
||||
module_ids: string[] | null;
|
||||
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
|
||||
-41
@@ -1,41 +0,0 @@
|
||||
export type TIssueActivity = {
|
||||
access?: "EXTERNAL" | "INTERNAL";
|
||||
actor: string;
|
||||
actor_detail: IUserLite;
|
||||
attachments: any[];
|
||||
comment?: string;
|
||||
comment_html?: string;
|
||||
comment_stripped?: string;
|
||||
created_at: Date;
|
||||
created_by: string;
|
||||
field: string | null;
|
||||
id: string;
|
||||
issue: string | null;
|
||||
issue_comment?: string | null;
|
||||
issue_detail: {
|
||||
description_html: string;
|
||||
id: string;
|
||||
name: string;
|
||||
priority: string | null;
|
||||
sequence_id: string;
|
||||
} | null;
|
||||
new_identifier: string | null;
|
||||
new_value: string | null;
|
||||
old_identifier: string | null;
|
||||
old_value: string | null;
|
||||
project: string;
|
||||
project_detail: IProjectLite;
|
||||
updated_at: Date;
|
||||
updated_by: string;
|
||||
verb: string;
|
||||
workspace: string;
|
||||
workspace_detail?: IWorkspaceLite;
|
||||
};
|
||||
|
||||
export type TIssueActivityMap = {
|
||||
[issue_id: string]: TIssueActivity;
|
||||
};
|
||||
|
||||
export type TIssueActivityIdMap = {
|
||||
[issue_id: string]: string[];
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user