Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb62c21a48 |
+1
-1
@@ -4,7 +4,7 @@ module.exports = {
|
||||
extends: ["custom"],
|
||||
settings: {
|
||||
next: {
|
||||
rootDir: ["web/", "space/"],
|
||||
rootDir: ["apps/*"],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
name: Build Pull Request Contents
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: ["opened", "synchronize"]
|
||||
|
||||
jobs:
|
||||
build-pull-request-contents:
|
||||
name: Build Pull Request Contents
|
||||
runs-on: ubuntu-20.04
|
||||
permissions:
|
||||
pull-requests: read
|
||||
|
||||
steps:
|
||||
- name: Checkout Repository to Actions
|
||||
uses: actions/checkout@v3.3.0
|
||||
|
||||
- name: Setup Node.js 18.x
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 18.x
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Get changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v38
|
||||
with:
|
||||
files_yaml: |
|
||||
apiserver:
|
||||
- apiserver/**
|
||||
web:
|
||||
- web/**
|
||||
deploy:
|
||||
- space/**
|
||||
|
||||
- name: Setup .npmrc for repository
|
||||
run: |
|
||||
echo -e "@tiptap-pro:registry=https://registry.tiptap.dev/\n//registry.tiptap.dev/:_authToken=${{ secrets.TIPTAP_TOKEN }}" > .npmrc
|
||||
|
||||
- name: Build Plane's Main App
|
||||
if: steps.changed-files.outputs.web_any_changed == 'true'
|
||||
run: |
|
||||
mv ./.npmrc ./web
|
||||
cd web
|
||||
yarn
|
||||
yarn build
|
||||
|
||||
- name: Build Plane's Deploy App
|
||||
if: steps.changed-files.outputs.deploy_any_changed == 'true'
|
||||
run: |
|
||||
cd space
|
||||
yarn
|
||||
yarn build
|
||||
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
name: Update Docker Images for Plane on Release
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [released]
|
||||
|
||||
jobs:
|
||||
build_push_backend:
|
||||
name: Build and Push Api Server Docker Image
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v3.3.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2.5.0
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.1.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Setup .npmrc for repository
|
||||
run: |
|
||||
echo -e "@tiptap-pro:registry=https://registry.tiptap.dev/\n//registry.tiptap.dev/:_authToken=${{ secrets.TIPTAP_TOKEN }}" > .npmrc
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker (Docker Hub) from Github Release
|
||||
id: metaFrontend
|
||||
uses: docker/metadata-action@v4.3.0
|
||||
with:
|
||||
images: ${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend
|
||||
tags: |
|
||||
type=ref,event=tag
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker (Docker Hub) from Github Release
|
||||
id: metaBackend
|
||||
uses: docker/metadata-action@v4.3.0
|
||||
with:
|
||||
images: ${{ secrets.DOCKERHUB_USERNAME }}/plane-backend
|
||||
tags: |
|
||||
type=ref,event=tag
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker (Docker Hub) from Github Release
|
||||
id: metaDeploy
|
||||
uses: docker/metadata-action@v4.3.0
|
||||
with:
|
||||
images: ${{ secrets.DOCKERHUB_USERNAME }}/plane-deploy
|
||||
tags: |
|
||||
type=ref,event=tag
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker (Docker Hub) from Github Release
|
||||
id: metaProxy
|
||||
uses: docker/metadata-action@v4.3.0
|
||||
with:
|
||||
images: ${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy
|
||||
tags: |
|
||||
type=ref,event=tag
|
||||
|
||||
- name: Build and Push Frontend to Docker Container Registry
|
||||
uses: docker/build-push-action@v4.0.0
|
||||
with:
|
||||
context: .
|
||||
file: ./web/Dockerfile.web
|
||||
platforms: linux/amd64
|
||||
tags: ${{ steps.metaFrontend.outputs.tags }}
|
||||
push: true
|
||||
env:
|
||||
DOCKER_BUILDKIT: 1
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
DOCKET_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and Push Backend to Docker Hub
|
||||
uses: docker/build-push-action@v4.0.0
|
||||
with:
|
||||
context: ./apiserver
|
||||
file: ./apiserver/Dockerfile.api
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
tags: ${{ steps.metaBackend.outputs.tags }}
|
||||
env:
|
||||
DOCKER_BUILDKIT: 1
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
DOCKET_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and Push Plane-Deploy to Docker Hub
|
||||
uses: docker/build-push-action@v4.0.0
|
||||
with:
|
||||
context: .
|
||||
file: ./space/Dockerfile.space
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
tags: ${{ steps.metaDeploy.outputs.tags }}
|
||||
env:
|
||||
DOCKER_BUILDKIT: 1
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
DOCKET_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and Push Plane-Proxy to Docker Hub
|
||||
uses: docker/build-push-action@v4.0.0
|
||||
with:
|
||||
context: ./nginx
|
||||
file: ./nginx/Dockerfile
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
tags: ${{ steps.metaProxy.outputs.tags }}
|
||||
env:
|
||||
DOCKER_BUILDKIT: 1
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
DOCKET_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
@@ -0,0 +1,77 @@
|
||||
name: Build and Push Backend Docker Image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'develop'
|
||||
- 'master'
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
build_push_backend:
|
||||
name: Build and Push Api Server Docker Image
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v3.3.0
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2.1.0
|
||||
with:
|
||||
platforms: linux/arm64,linux/amd64
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2.5.0
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.1.0
|
||||
with:
|
||||
registry: "ghcr.io"
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.1.0
|
||||
with:
|
||||
registry: "registry.hub.docker.com"
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker (Docker Hub)
|
||||
id: ghmeta
|
||||
uses: docker/metadata-action@v4.3.0
|
||||
with:
|
||||
images: makeplane/plane-backend
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker (Github)
|
||||
id: dkrmeta
|
||||
uses: docker/metadata-action@v4.3.0
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository }}-backend
|
||||
|
||||
- name: Build and Push to GitHub Container Registry
|
||||
uses: docker/build-push-action@v4.0.0
|
||||
with:
|
||||
context: ./apiserver
|
||||
file: ./apiserver/Dockerfile.api
|
||||
platforms: linux/arm64,linux/amd64
|
||||
push: true
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha
|
||||
tags: ${{ steps.ghmeta.outputs.tags }}
|
||||
labels: ${{ steps.ghmeta.outputs.labels }}
|
||||
|
||||
- name: Build and Push to Docker Hub
|
||||
uses: docker/build-push-action@v4.0.0
|
||||
with:
|
||||
context: ./apiserver
|
||||
file: ./apiserver/Dockerfile.api
|
||||
platforms: linux/arm64,linux/amd64
|
||||
push: true
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha
|
||||
tags: ${{ steps.dkrmeta.outputs.tags }}
|
||||
labels: ${{ steps.dkrmeta.outputs.labels }}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
name: Build and Push Frontend Docker Image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'develop'
|
||||
- 'master'
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
build_push_frontend:
|
||||
name: Build Frontend Docker Image
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v3.3.0
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2.1.0
|
||||
with:
|
||||
platforms: linux/arm64,linux/amd64
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2.5.0
|
||||
|
||||
- name: Login to Github Container Registry
|
||||
uses: docker/login-action@v2.1.0
|
||||
with:
|
||||
registry: "ghcr.io"
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.1.0
|
||||
with:
|
||||
registry: "registry.hub.docker.com"
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker (Docker Hub)
|
||||
id: ghmeta
|
||||
uses: docker/metadata-action@v4.3.0
|
||||
with:
|
||||
images: makeplane/plane-frontend
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker (Github)
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4.3.0
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository }}-frontend
|
||||
|
||||
- name: Build and Push to GitHub Container Registry
|
||||
uses: docker/build-push-action@v4.0.0
|
||||
with:
|
||||
context: .
|
||||
file: ./apps/app/Dockerfile.web
|
||||
platforms: linux/arm64,linux/amd64
|
||||
push: true
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha
|
||||
tags: ${{ steps.ghmeta.outputs.tags }}
|
||||
labels: ${{ steps.ghmeta.outputs.labels }}
|
||||
|
||||
- name: Build and Push to Docker Container Registry
|
||||
uses: docker/build-push-action@v4.0.0
|
||||
with:
|
||||
context: .
|
||||
file: ./apps/app/Dockerfile.web
|
||||
platforms: linux/arm64,linux/amd64
|
||||
push: true
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha
|
||||
tags: ${{ steps.dkrmeta.outputs.tags }}
|
||||
labels: ${{ steps.dkrmeta.outputs.labels }}
|
||||
|
||||
+12
-12
@@ -17,23 +17,23 @@ diverse, inclusive, and healthy community.
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
- Demonstrating empathy and kindness toward other people
|
||||
- Being respectful of differing opinions, viewpoints, and experiences
|
||||
- Giving and gracefully accepting constructive feedback
|
||||
- Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
- Focusing on what is best not just for us as individuals, but for the
|
||||
* Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
- The use of sexualized language or imagery, and sexual attention or
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
- Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
- Public or private harassment
|
||||
- Publishing others' private information, such as a physical or email
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
- Other conduct which could reasonably be considered inappropriate in a
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
@@ -106,7 +106,7 @@ Violating these terms may lead to a permanent ban.
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
@@ -125,4 +125,4 @@ enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at
|
||||
https://www.contributor-covenant.org/translations.
|
||||
https://www.contributor-covenant.org/translations.
|
||||
@@ -35,10 +35,12 @@
|
||||
|
||||
Meet [Plane](https://plane.so). An open-source software development tool to manage issues, sprints, and product roadmaps with peace of mind 🧘♀️.
|
||||
|
||||
|
||||
> Plane is still in its early days, not everything will be perfect yet, and hiccups may happen. Please let us know of any suggestions, ideas, or bugs that you encounter on our [Discord](https://discord.com/invite/A92xrEGCge) or GitHub issues, and we will use your feedback to improve on our upcoming releases.
|
||||
|
||||
The easiest way to get started with Plane is by creating a [Plane Cloud](https://app.plane.so) account. Plane Cloud offers a hosted solution for Plane. If you prefer to self-host Plane, please refer to our [deployment documentation](https://docs.plane.so/self-hosting).
|
||||
|
||||
|
||||
## ⚡️ Quick start with Docker Compose
|
||||
|
||||
### Docker Compose Setup
|
||||
@@ -54,7 +56,7 @@ chmod +x setup.sh
|
||||
- Run setup.sh
|
||||
|
||||
```bash
|
||||
./setup.sh http://localhost
|
||||
./setup.sh http://localhost
|
||||
```
|
||||
|
||||
> If running in a cloud env replace localhost with public facing IP address of the VM
|
||||
@@ -63,32 +65,31 @@ chmod +x setup.sh
|
||||
|
||||
Visit [Tiptap Pro](https://collab.tiptap.dev/pro-extensions) and signup (it is free).
|
||||
|
||||
Create a **`.npmrc`** file, copy the following and replace your registry token generated from Tiptap Pro.
|
||||
Create a **`.npmrc`** file, copy the following and replace your registry token generated from Tiptap Pro.
|
||||
|
||||
```
|
||||
@tiptap-pro:registry=https://registry.tiptap.dev/
|
||||
//registry.tiptap.dev/:_authToken=YOUR_REGISTRY_TOKEN
|
||||
```
|
||||
|
||||
- Run Docker compose up
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
<strong>You can use the default email and password for your first login `captain@plane.so` and `password123`.</strong>
|
||||
<strong>You can use the default email and password for your first login `captain@plane.so` and `password123`.</strong>
|
||||
|
||||
## 🚀 Features
|
||||
|
||||
- **Issue Planning and Tracking**: Quickly create issues and add details using a powerful rich text editor that supports file uploads. Add sub-properties and references to issues for better organization and tracking.
|
||||
- **Issue Attachments**: Collaborate effectively by attaching files to issues, making it easy for your team to find and share important project-related documents.
|
||||
- **Layouts**: Customize your project view with your preferred layout - choose from List, Kanban, or Calendar to visualize your project in a way that makes sense to you.
|
||||
- **Cycles**: Plan sprints with Cycles to keep your team on track and productive. Gain insights into your project's progress with burn-down charts and other useful features.
|
||||
- **Modules**: Break down your large projects into smaller, more manageable modules. Assign modules between teams to easily track and plan your project's progress.
|
||||
- **Views**: Create custom filters to display only the issues that matter to you. Save and share your filters in just a few clicks.
|
||||
- **Pages**: Plane pages function as an AI-powered notepad, allowing you to easily document issues, cycle plans, and module details, and then synchronize them with your issues.
|
||||
- **Command K**: Enjoy a better user experience with the new Command + K menu. Easily manage and navigate through your projects from one convenient location.
|
||||
- **GitHub Sync**: Streamline your planning process by syncing your GitHub issues with Plane. Keep all your issues in one place for better tracking and collaboration.
|
||||
* **Issue Planning and Tracking**: Quickly create issues and add details using a powerful rich text editor that supports file uploads. Add sub-properties and references to issues for better organization and tracking.
|
||||
* **Issue Attachments**: Collaborate effectively by attaching files to issues, making it easy for your team to find and share important project-related documents.
|
||||
* **Layouts**: Customize your project view with your preferred layout - choose from List, Kanban, or Calendar to visualize your project in a way that makes sense to you.
|
||||
* **Cycles**: Plan sprints with Cycles to keep your team on track and productive. Gain insights into your project's progress with burn-down charts and other useful features.
|
||||
* **Modules**: Break down your large projects into smaller, more manageable modules. Assign modules between teams to easily track and plan your project's progress.
|
||||
* **Views**: Create custom filters to display only the issues that matter to you. Save and share your filters in just a few clicks.
|
||||
* **Pages**: Plane pages function as an AI-powered notepad, allowing you to easily document issues, cycle plans, and module details, and then synchronize them with your issues.
|
||||
* **Command K**: Enjoy a better user experience with the new Command + K menu. Easily manage and navigate through your projects from one convenient location.
|
||||
* **GitHub Sync**: Streamline your planning process by syncing your GitHub issues with Plane. Keep all your issues in one place for better tracking and collaboration.
|
||||
|
||||
## 📸 Screenshots
|
||||
|
||||
@@ -149,6 +150,7 @@ docker compose up -d
|
||||
</p>
|
||||
</p>
|
||||
|
||||
|
||||
## 📚Documentation
|
||||
|
||||
For full documentation, visit [docs.plane.so](https://docs.plane.so/)
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
web: gunicorn -w 4 -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:$PORT --max-requests 10000 --max-requests-jitter 1000 --access-logfile -
|
||||
web: gunicorn -w 4 -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:$PORT --config gunicorn.config.py --max-requests 10000 --max-requests-jitter 1000 --access-logfile -
|
||||
worker: celery -A plane worker -l info
|
||||
beat: celery -A plane beat -l INFO
|
||||
@@ -20,7 +20,6 @@ from .project import (
|
||||
ProjectMemberLiteSerializer,
|
||||
ProjectDeployBoardSerializer,
|
||||
ProjectMemberAdminSerializer,
|
||||
ProjectPublicMemberSerializer
|
||||
)
|
||||
from .state import StateSerializer, StateLiteSerializer
|
||||
from .view import IssueViewSerializer, IssueViewFavoriteSerializer
|
||||
@@ -45,7 +44,6 @@ from .issue import (
|
||||
IssueReactionSerializer,
|
||||
CommentReactionSerializer,
|
||||
IssueVoteSerializer,
|
||||
IssuePublicSerializer,
|
||||
)
|
||||
|
||||
from .module import (
|
||||
|
||||
@@ -113,11 +113,7 @@ class IssueCreateSerializer(BaseSerializer):
|
||||
]
|
||||
|
||||
def validate(self, data):
|
||||
if (
|
||||
data.get("start_date", None) is not None
|
||||
and data.get("target_date", None) is not None
|
||||
and data.get("start_date", None) > data.get("target_date", None)
|
||||
):
|
||||
if data.get("start_date", None) is not None and data.get("target_date", None) is not None and data.get("start_date", None) > data.get("target_date", None):
|
||||
raise serializers.ValidationError("Start date cannot exceed target date")
|
||||
return data
|
||||
|
||||
@@ -514,9 +510,6 @@ class IssueAttachmentSerializer(BaseSerializer):
|
||||
|
||||
|
||||
class IssueReactionSerializer(BaseSerializer):
|
||||
|
||||
actor_detail = UserLiteSerializer(read_only=True, source="actor")
|
||||
|
||||
class Meta:
|
||||
model = IssueReaction
|
||||
fields = "__all__"
|
||||
@@ -528,6 +521,19 @@ class IssueReactionSerializer(BaseSerializer):
|
||||
]
|
||||
|
||||
|
||||
class IssueReactionLiteSerializer(BaseSerializer):
|
||||
actor_detail = UserLiteSerializer(read_only=True, source="actor")
|
||||
|
||||
class Meta:
|
||||
model = IssueReaction
|
||||
fields = [
|
||||
"id",
|
||||
"reaction",
|
||||
"issue",
|
||||
"actor_detail",
|
||||
]
|
||||
|
||||
|
||||
class CommentReactionLiteSerializer(BaseSerializer):
|
||||
actor_detail = UserLiteSerializer(read_only=True, source="actor")
|
||||
|
||||
@@ -548,13 +554,12 @@ class CommentReactionSerializer(BaseSerializer):
|
||||
read_only_fields = ["workspace", "project", "comment", "actor"]
|
||||
|
||||
|
||||
class IssueVoteSerializer(BaseSerializer):
|
||||
|
||||
actor_detail = UserLiteSerializer(read_only=True, source="actor")
|
||||
class IssueVoteSerializer(BaseSerializer):
|
||||
|
||||
class Meta:
|
||||
model = IssueVote
|
||||
fields = ["issue", "vote", "workspace", "project", "actor", "actor_detail"]
|
||||
fields = ["issue", "vote", "workspace_id", "project_id", "actor"]
|
||||
read_only_fields = fields
|
||||
|
||||
|
||||
@@ -564,7 +569,7 @@ class IssueCommentSerializer(BaseSerializer):
|
||||
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
||||
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
|
||||
comment_reactions = CommentReactionLiteSerializer(read_only=True, many=True)
|
||||
is_member = serializers.BooleanField(read_only=True)
|
||||
|
||||
|
||||
class Meta:
|
||||
model = IssueComment
|
||||
@@ -577,6 +582,7 @@ class IssueCommentSerializer(BaseSerializer):
|
||||
"updated_by",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"access",
|
||||
]
|
||||
|
||||
|
||||
@@ -626,7 +632,7 @@ class IssueSerializer(BaseSerializer):
|
||||
issue_link = IssueLinkSerializer(read_only=True, many=True)
|
||||
issue_attachment = IssueAttachmentSerializer(read_only=True, many=True)
|
||||
sub_issues_count = serializers.IntegerField(read_only=True)
|
||||
issue_reactions = IssueReactionSerializer(read_only=True, many=True)
|
||||
issue_reactions = IssueReactionLiteSerializer(read_only=True, many=True)
|
||||
|
||||
class Meta:
|
||||
model = Issue
|
||||
@@ -652,7 +658,7 @@ class IssueLiteSerializer(BaseSerializer):
|
||||
module_id = serializers.UUIDField(read_only=True)
|
||||
attachment_count = serializers.IntegerField(read_only=True)
|
||||
link_count = serializers.IntegerField(read_only=True)
|
||||
issue_reactions = IssueReactionSerializer(read_only=True, many=True)
|
||||
issue_reactions = IssueReactionLiteSerializer(read_only=True, many=True)
|
||||
|
||||
class Meta:
|
||||
model = Issue
|
||||
@@ -670,33 +676,6 @@ class IssueLiteSerializer(BaseSerializer):
|
||||
]
|
||||
|
||||
|
||||
class IssuePublicSerializer(BaseSerializer):
|
||||
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
||||
state_detail = StateLiteSerializer(read_only=True, source="state")
|
||||
reactions = IssueReactionSerializer(read_only=True, many=True, source="issue_reactions")
|
||||
votes = IssueVoteSerializer(read_only=True, many=True)
|
||||
|
||||
class Meta:
|
||||
model = Issue
|
||||
fields = [
|
||||
"id",
|
||||
"name",
|
||||
"description_html",
|
||||
"sequence_id",
|
||||
"state",
|
||||
"state_detail",
|
||||
"project",
|
||||
"project_detail",
|
||||
"workspace",
|
||||
"priority",
|
||||
"target_date",
|
||||
"reactions",
|
||||
"votes",
|
||||
]
|
||||
read_only_fields = fields
|
||||
|
||||
|
||||
|
||||
class IssueSubscriberSerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = IssueSubscriber
|
||||
|
||||
@@ -15,7 +15,6 @@ from plane.db.models import (
|
||||
ProjectIdentifier,
|
||||
ProjectFavorite,
|
||||
ProjectDeployBoard,
|
||||
ProjectPublicMember,
|
||||
)
|
||||
|
||||
|
||||
@@ -113,7 +112,7 @@ class ProjectDetailSerializer(BaseSerializer):
|
||||
|
||||
|
||||
class ProjectMemberSerializer(BaseSerializer):
|
||||
workspace = WorkspaceLiteSerializer(read_only=True)
|
||||
workspace = WorkSpaceSerializer(read_only=True)
|
||||
project = ProjectLiteSerializer(read_only=True)
|
||||
member = UserLiteSerializer(read_only=True)
|
||||
|
||||
@@ -178,17 +177,5 @@ class ProjectDeployBoardSerializer(BaseSerializer):
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
"project", "anchor",
|
||||
]
|
||||
|
||||
|
||||
class ProjectPublicMemberSerializer(BaseSerializer):
|
||||
|
||||
class Meta:
|
||||
model = ProjectPublicMember
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
"project",
|
||||
"member",
|
||||
"project" "anchor",
|
||||
]
|
||||
|
||||
@@ -51,7 +51,6 @@ from plane.api.views import (
|
||||
WorkspaceUserProfileEndpoint,
|
||||
WorkspaceUserProfileIssuesEndpoint,
|
||||
WorkspaceLabelsEndpoint,
|
||||
LeaveWorkspaceEndpoint,
|
||||
## End Workspaces
|
||||
# File Assets
|
||||
FileAssetEndpoint,
|
||||
@@ -69,7 +68,6 @@ from plane.api.views import (
|
||||
UserProjectInvitationsViewset,
|
||||
ProjectIdentifierEndpoint,
|
||||
ProjectFavoritesViewSet,
|
||||
LeaveProjectEndpoint,
|
||||
## End Projects
|
||||
# Issues
|
||||
IssueViewSet,
|
||||
@@ -91,6 +89,7 @@ from plane.api.views import (
|
||||
IssueCommentPublicViewSet,
|
||||
IssueReactionViewSet,
|
||||
CommentReactionViewSet,
|
||||
ExportIssuesEndpoint,
|
||||
## End Issues
|
||||
# States
|
||||
StateViewSet,
|
||||
@@ -166,23 +165,16 @@ from plane.api.views import (
|
||||
# Notification
|
||||
NotificationViewSet,
|
||||
UnreadNotificationEndpoint,
|
||||
MarkAllReadNotificationViewSet,
|
||||
## End Notification
|
||||
# Public Boards
|
||||
ProjectDeployBoardViewSet,
|
||||
ProjectIssuesPublicEndpoint,
|
||||
ProjectDeployBoardIssuesPublicEndpoint,
|
||||
ProjectDeployBoardPublicSettingsEndpoint,
|
||||
IssueReactionPublicViewSet,
|
||||
CommentReactionPublicViewSet,
|
||||
InboxIssuePublicViewSet,
|
||||
IssueVotePublicViewSet,
|
||||
WorkspaceProjectDeployBoardEndpoint,
|
||||
IssueRetrievePublicEndpoint,
|
||||
## End Public Boards
|
||||
## Exporter
|
||||
ExportIssuesEndpoint,
|
||||
## End Exporter
|
||||
|
||||
)
|
||||
|
||||
|
||||
@@ -239,7 +231,7 @@ urlpatterns = [
|
||||
UpdateUserTourCompletedEndpoint.as_view(),
|
||||
name="user-tour",
|
||||
),
|
||||
path("users/workspaces/<str:slug>/activities/", UserActivityEndpoint.as_view(), name="user-activities"),
|
||||
path("users/activities/", UserActivityEndpoint.as_view(), name="user-activities"),
|
||||
# user workspaces
|
||||
path(
|
||||
"users/me/workspaces/",
|
||||
@@ -443,11 +435,6 @@ urlpatterns = [
|
||||
WorkspaceLabelsEndpoint.as_view(),
|
||||
name="workspace-labels",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/members/leave/",
|
||||
LeaveWorkspaceEndpoint.as_view(),
|
||||
name="workspace-labels",
|
||||
),
|
||||
## End Workspaces ##
|
||||
# Projects
|
||||
path(
|
||||
@@ -561,11 +548,6 @@ urlpatterns = [
|
||||
),
|
||||
name="project",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/members/leave/",
|
||||
LeaveProjectEndpoint.as_view(),
|
||||
name="project",
|
||||
),
|
||||
# End Projects
|
||||
# States
|
||||
path(
|
||||
@@ -1508,15 +1490,6 @@ urlpatterns = [
|
||||
UnreadNotificationEndpoint.as_view(),
|
||||
name="unread-notifications",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/users/notifications/mark-all-read/",
|
||||
MarkAllReadNotificationViewSet.as_view(
|
||||
{
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="mark-all-read-notifications",
|
||||
),
|
||||
## End Notification
|
||||
# Public Boards
|
||||
path(
|
||||
@@ -1547,14 +1520,9 @@ urlpatterns = [
|
||||
),
|
||||
path(
|
||||
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/",
|
||||
ProjectIssuesPublicEndpoint.as_view(),
|
||||
ProjectDeployBoardIssuesPublicEndpoint.as_view(),
|
||||
name="project-deploy-board",
|
||||
),
|
||||
path(
|
||||
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/<uuid:issue_id>/",
|
||||
IssueRetrievePublicEndpoint.as_view(),
|
||||
name="workspace-project-boards",
|
||||
),
|
||||
path(
|
||||
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/<uuid:issue_id>/comments/",
|
||||
IssueCommentPublicViewSet.as_view(
|
||||
@@ -1646,10 +1614,5 @@ urlpatterns = [
|
||||
),
|
||||
name="issue-vote-project-board",
|
||||
),
|
||||
path(
|
||||
"public/workspaces/<str:slug>/project-boards/",
|
||||
WorkspaceProjectDeployBoardEndpoint.as_view(),
|
||||
name="workspace-project-boards",
|
||||
),
|
||||
## End Public Boards
|
||||
]
|
||||
|
||||
@@ -12,11 +12,10 @@ from .project import (
|
||||
ProjectUserViewsEndpoint,
|
||||
ProjectMemberUserEndpoint,
|
||||
ProjectFavoritesViewSet,
|
||||
ProjectDeployBoardIssuesPublicEndpoint,
|
||||
ProjectDeployBoardViewSet,
|
||||
ProjectDeployBoardPublicSettingsEndpoint,
|
||||
ProjectMemberEndpoint,
|
||||
WorkspaceProjectDeployBoardEndpoint,
|
||||
LeaveProjectEndpoint,
|
||||
)
|
||||
from .user import (
|
||||
UserEndpoint,
|
||||
@@ -53,7 +52,6 @@ from .workspace import (
|
||||
WorkspaceUserProfileIssuesEndpoint,
|
||||
WorkspaceLabelsEndpoint,
|
||||
WorkspaceMembersEndpoint,
|
||||
LeaveWorkspaceEndpoint,
|
||||
)
|
||||
from .state import StateViewSet
|
||||
from .view import IssueViewViewSet, ViewIssuesEndpoint, IssueViewFavoriteViewSet
|
||||
@@ -86,8 +84,6 @@ from .issue import (
|
||||
IssueReactionPublicViewSet,
|
||||
CommentReactionPublicViewSet,
|
||||
IssueVotePublicViewSet,
|
||||
IssueRetrievePublicEndpoint,
|
||||
ProjectIssuesPublicEndpoint,
|
||||
)
|
||||
|
||||
from .auth_extended import (
|
||||
@@ -165,7 +161,7 @@ from .analytic import (
|
||||
DefaultAnalyticsEndpoint,
|
||||
)
|
||||
|
||||
from .notification import NotificationViewSet, UnreadNotificationEndpoint, MarkAllReadNotificationViewSet
|
||||
from .notification import NotificationViewSet, UnreadNotificationEndpoint
|
||||
|
||||
from .exporter import (
|
||||
ExportIssuesEndpoint,
|
||||
|
||||
@@ -18,21 +18,10 @@ class FileAssetEndpoint(BaseAPIView):
|
||||
"""
|
||||
|
||||
def get(self, request, workspace_id, asset_key):
|
||||
try:
|
||||
asset_key = str(workspace_id) + "/" + asset_key
|
||||
files = FileAsset.objects.filter(asset=asset_key)
|
||||
if files.exists():
|
||||
serializer = FileAssetSerializer(files, context={"request": request}, many=True)
|
||||
return Response({"data": serializer.data, "status": True}, status=status.HTTP_200_OK)
|
||||
else:
|
||||
return Response({"error": "Asset key does not exist", "status": False}, status=status.HTTP_200_OK)
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
asset_key = str(workspace_id) + "/" + asset_key
|
||||
files = FileAsset.objects.filter(asset=asset_key)
|
||||
serializer = FileAssetSerializer(files, context={"request": request}, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
def post(self, request, slug):
|
||||
try:
|
||||
@@ -79,16 +68,11 @@ class UserAssetsEndpoint(BaseAPIView):
|
||||
def get(self, request, asset_key):
|
||||
try:
|
||||
files = FileAsset.objects.filter(asset=asset_key, created_by=request.user)
|
||||
if files.exists():
|
||||
serializer = FileAssetSerializer(files, context={"request": request})
|
||||
return Response({"data": serializer.data, "status": True}, status=status.HTTP_200_OK)
|
||||
else:
|
||||
return Response({"error": "Asset key does not exist", "status": False}, status=status.HTTP_200_OK)
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
serializer = FileAssetSerializer(files, context={"request": request})
|
||||
return Response(serializer.data)
|
||||
except FileAsset.DoesNotExist:
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
{"error": "File Asset does not exist"}, status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
|
||||
def post(self, request):
|
||||
|
||||
@@ -1,41 +1,24 @@
|
||||
# Python imports
|
||||
import zoneinfo
|
||||
|
||||
# Django imports
|
||||
from django.urls import resolve
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
# Third part imports
|
||||
|
||||
# Third part imports
|
||||
from rest_framework import status
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
from rest_framework.exceptions import APIException
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.filters import SearchFilter
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.exceptions import NotFound
|
||||
from sentry_sdk import capture_exception
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
|
||||
# Module imports
|
||||
from plane.db.models import Workspace, Project
|
||||
from plane.utils.paginator import BasePaginator
|
||||
|
||||
|
||||
class TimezoneMixin:
|
||||
"""
|
||||
This enables timezone conversion according
|
||||
to the user set timezone
|
||||
"""
|
||||
def initial(self, request, *args, **kwargs):
|
||||
super().initial(request, *args, **kwargs)
|
||||
if request.user.is_authenticated:
|
||||
timezone.activate(zoneinfo.ZoneInfo(request.user.user_timezone))
|
||||
else:
|
||||
timezone.deactivate()
|
||||
|
||||
|
||||
|
||||
|
||||
class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator):
|
||||
class BaseViewSet(ModelViewSet, BasePaginator):
|
||||
|
||||
model = None
|
||||
|
||||
@@ -84,7 +67,7 @@ class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator):
|
||||
return self.kwargs.get("pk", None)
|
||||
|
||||
|
||||
class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
|
||||
class BaseAPIView(APIView, BasePaginator):
|
||||
|
||||
permission_classes = [
|
||||
IsAuthenticated,
|
||||
|
||||
@@ -191,10 +191,11 @@ class CycleViewSet(BaseViewSet):
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
)
|
||||
.annotate(display_name=F("assignees__display_name"))
|
||||
.annotate(first_name=F("assignees__first_name"))
|
||||
.annotate(last_name=F("assignees__last_name"))
|
||||
.annotate(assignee_id=F("assignees__id"))
|
||||
.annotate(avatar=F("assignees__avatar"))
|
||||
.values("display_name", "assignee_id", "avatar")
|
||||
.values("first_name", "last_name", "assignee_id", "avatar")
|
||||
.annotate(total_issues=Count("assignee_id"))
|
||||
.annotate(
|
||||
completed_issues=Count(
|
||||
@@ -208,7 +209,7 @@ class CycleViewSet(BaseViewSet):
|
||||
filter=Q(completed_at__isnull=True),
|
||||
)
|
||||
)
|
||||
.order_by("display_name")
|
||||
.order_by("first_name", "last_name")
|
||||
)
|
||||
|
||||
label_distribution = (
|
||||
@@ -333,21 +334,13 @@ class CycleViewSet(BaseViewSet):
|
||||
workspace__slug=slug, project_id=project_id, pk=pk
|
||||
)
|
||||
|
||||
request_data = request.data
|
||||
|
||||
if cycle.end_date is not None and cycle.end_date < timezone.now().date():
|
||||
if "sort_order" in request_data:
|
||||
# Can only change sort order
|
||||
request_data = {
|
||||
"sort_order": request_data.get("sort_order", cycle.sort_order)
|
||||
}
|
||||
else:
|
||||
return Response(
|
||||
{
|
||||
"error": "The Cycle has already been completed so it cannot be edited"
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
return Response(
|
||||
{
|
||||
"error": "The Cycle has already been completed so it cannot be edited"
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
serializer = CycleWriteSerializer(cycle, data=request.data, partial=True)
|
||||
if serializer.is_valid():
|
||||
@@ -381,9 +374,7 @@ class CycleViewSet(BaseViewSet):
|
||||
.annotate(assignee_id=F("assignees__id"))
|
||||
.annotate(avatar=F("assignees__avatar"))
|
||||
.annotate(display_name=F("assignees__display_name"))
|
||||
.values(
|
||||
"first_name", "last_name", "assignee_id", "avatar", "display_name"
|
||||
)
|
||||
.values("first_name", "last_name", "assignee_id", "avatar", "display_name")
|
||||
.annotate(total_issues=Count("assignee_id"))
|
||||
.annotate(
|
||||
completed_issues=Count(
|
||||
@@ -719,6 +710,7 @@ class CycleDateCheckEndpoint(BaseAPIView):
|
||||
|
||||
|
||||
class CycleFavoriteViewSet(BaseViewSet):
|
||||
|
||||
serializer_class = CycleFavoriteSerializer
|
||||
model = CycleFavorite
|
||||
|
||||
|
||||
@@ -41,9 +41,9 @@ class GPTIntegrationEndpoint(BaseAPIView):
|
||||
final_text = task + "\n" + prompt
|
||||
|
||||
openai.api_key = settings.OPENAI_API_KEY
|
||||
response = openai.ChatCompletion.create(
|
||||
response = openai.Completion.create(
|
||||
model=settings.GPT_ENGINE,
|
||||
messages=[{"role": "user", "content": final_text}],
|
||||
prompt=final_text,
|
||||
temperature=0.7,
|
||||
max_tokens=1024,
|
||||
)
|
||||
@@ -51,7 +51,7 @@ class GPTIntegrationEndpoint(BaseAPIView):
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
project = Project.objects.get(pk=project_id)
|
||||
|
||||
text = response.choices[0].message.content.strip()
|
||||
text = response.choices[0].text.strip()
|
||||
text_html = text.replace("\n", "<br/>")
|
||||
return Response(
|
||||
{
|
||||
|
||||
@@ -17,19 +17,17 @@ from django.db.models import (
|
||||
When,
|
||||
Exists,
|
||||
Max,
|
||||
IntegerField,
|
||||
)
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.gzip import gzip_page
|
||||
from django.db import IntegrityError
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.conf import settings
|
||||
|
||||
# Third Party imports
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from rest_framework.parsers import MultiPartParser, FormParser
|
||||
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||
from sentry_sdk import capture_exception
|
||||
|
||||
# Module imports
|
||||
@@ -51,7 +49,6 @@ from plane.api.serializers import (
|
||||
IssueReactionSerializer,
|
||||
CommentReactionSerializer,
|
||||
IssueVoteSerializer,
|
||||
IssuePublicSerializer,
|
||||
)
|
||||
from plane.api.permissions import (
|
||||
WorkspaceEntityPermission,
|
||||
@@ -76,12 +73,10 @@ from plane.db.models import (
|
||||
CommentReaction,
|
||||
ProjectDeployBoard,
|
||||
IssueVote,
|
||||
ProjectPublicMember,
|
||||
)
|
||||
from plane.bgtasks.issue_activites_task import issue_activity
|
||||
from plane.utils.grouper import group_results
|
||||
from plane.utils.issue_filters import issue_filters
|
||||
from plane.bgtasks.export_task import issue_export_task
|
||||
|
||||
|
||||
class IssueViewSet(BaseViewSet):
|
||||
@@ -338,11 +333,7 @@ class UserWorkSpaceIssues(BaseAPIView):
|
||||
|
||||
issue_queryset = (
|
||||
Issue.issue_objects.filter(
|
||||
(
|
||||
Q(assignees__in=[request.user])
|
||||
| Q(created_by=request.user)
|
||||
| Q(issue_subscribers__subscriber=request.user)
|
||||
),
|
||||
(Q(assignees__in=[request.user]) | Q(created_by=request.user)),
|
||||
workspace__slug=slug,
|
||||
)
|
||||
.annotate(
|
||||
@@ -491,7 +482,7 @@ class IssueActivityEndpoint(BaseAPIView):
|
||||
issue_activities = (
|
||||
IssueActivity.objects.filter(issue_id=issue_id)
|
||||
.filter(
|
||||
~Q(field__in=["comment", "vote", "reaction"]),
|
||||
~Q(field="comment"),
|
||||
project__project_projectmember__member=self.request.user,
|
||||
)
|
||||
.select_related("actor", "workspace", "issue", "project")
|
||||
@@ -501,12 +492,6 @@ class IssueActivityEndpoint(BaseAPIView):
|
||||
.filter(project__project_projectmember__member=self.request.user)
|
||||
.order_by("created_at")
|
||||
.select_related("actor", "issue", "project", "workspace")
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"comment_reactions",
|
||||
queryset=CommentReaction.objects.select_related("actor"),
|
||||
)
|
||||
)
|
||||
)
|
||||
issue_activities = IssueActivitySerializer(issue_activities, many=True).data
|
||||
issue_comments = IssueCommentSerializer(issue_comments, many=True).data
|
||||
@@ -603,15 +588,6 @@ class IssueCommentViewSet(BaseViewSet):
|
||||
.select_related("project")
|
||||
.select_related("workspace")
|
||||
.select_related("issue")
|
||||
.annotate(
|
||||
is_member=Exists(
|
||||
ProjectMember.objects.filter(
|
||||
workspace__slug=self.kwargs.get("slug"),
|
||||
project_id=self.kwargs.get("project_id"),
|
||||
member_id=self.request.user.id,
|
||||
)
|
||||
)
|
||||
)
|
||||
.distinct()
|
||||
)
|
||||
|
||||
@@ -793,9 +769,7 @@ class SubIssuesEndpoint(BaseAPIView):
|
||||
.order_by("state_group")
|
||||
)
|
||||
|
||||
result = {
|
||||
item["state_group"]: item["state_count"] for item in state_distribution
|
||||
}
|
||||
result = {item["state_group"]: item["state_count"] for item in state_distribution}
|
||||
|
||||
serializer = IssueLiteSerializer(
|
||||
sub_issues,
|
||||
@@ -1410,14 +1384,6 @@ class IssueReactionViewSet(BaseViewSet):
|
||||
project_id=self.kwargs.get("project_id"),
|
||||
actor=self.request.user,
|
||||
)
|
||||
issue_activity.delay(
|
||||
type="issue_reaction.activity.created",
|
||||
requested_data=json.dumps(self.request.data, cls=DjangoJSONEncoder),
|
||||
actor_id=str(self.request.user.id),
|
||||
issue_id=str(self.kwargs.get("issue_id", None)),
|
||||
project_id=str(self.kwargs.get("project_id", None)),
|
||||
current_instance=None,
|
||||
)
|
||||
|
||||
def destroy(self, request, slug, project_id, issue_id, reaction_code):
|
||||
try:
|
||||
@@ -1428,19 +1394,6 @@ class IssueReactionViewSet(BaseViewSet):
|
||||
reaction=reaction_code,
|
||||
actor=request.user,
|
||||
)
|
||||
issue_activity.delay(
|
||||
type="issue_reaction.activity.deleted",
|
||||
requested_data=None,
|
||||
actor_id=str(self.request.user.id),
|
||||
issue_id=str(self.kwargs.get("issue_id", None)),
|
||||
project_id=str(self.kwargs.get("project_id", None)),
|
||||
current_instance=json.dumps(
|
||||
{
|
||||
"reaction": str(reaction_code),
|
||||
"identifier": str(issue_reaction.id),
|
||||
}
|
||||
),
|
||||
)
|
||||
issue_reaction.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
except IssueReaction.DoesNotExist:
|
||||
@@ -1481,14 +1434,6 @@ class CommentReactionViewSet(BaseViewSet):
|
||||
comment_id=self.kwargs.get("comment_id"),
|
||||
project_id=self.kwargs.get("project_id"),
|
||||
)
|
||||
issue_activity.delay(
|
||||
type="comment_reaction.activity.created",
|
||||
requested_data=json.dumps(self.request.data, cls=DjangoJSONEncoder),
|
||||
actor_id=str(self.request.user.id),
|
||||
issue_id=None,
|
||||
project_id=str(self.kwargs.get("project_id", None)),
|
||||
current_instance=None,
|
||||
)
|
||||
|
||||
def destroy(self, request, slug, project_id, comment_id, reaction_code):
|
||||
try:
|
||||
@@ -1499,20 +1444,6 @@ class CommentReactionViewSet(BaseViewSet):
|
||||
reaction=reaction_code,
|
||||
actor=request.user,
|
||||
)
|
||||
issue_activity.delay(
|
||||
type="comment_reaction.activity.deleted",
|
||||
requested_data=None,
|
||||
actor_id=str(self.request.user.id),
|
||||
issue_id=None,
|
||||
project_id=str(self.kwargs.get("project_id", None)),
|
||||
current_instance=json.dumps(
|
||||
{
|
||||
"reaction": str(reaction_code),
|
||||
"identifier": str(comment_reaction.id),
|
||||
"comment_id": str(comment_id),
|
||||
}
|
||||
),
|
||||
)
|
||||
comment_reaction.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
except CommentReaction.DoesNotExist:
|
||||
@@ -1537,48 +1468,23 @@ class IssueCommentPublicViewSet(BaseViewSet):
|
||||
"workspace__id",
|
||||
]
|
||||
|
||||
def get_permissions(self):
|
||||
if self.action in ["list", "retrieve"]:
|
||||
self.permission_classes = [
|
||||
AllowAny,
|
||||
]
|
||||
else:
|
||||
self.permission_classes = [
|
||||
IsAuthenticated,
|
||||
]
|
||||
|
||||
return super(IssueCommentPublicViewSet, self).get_permissions()
|
||||
|
||||
def get_queryset(self):
|
||||
try:
|
||||
project_deploy_board = ProjectDeployBoard.objects.get(
|
||||
workspace__slug=self.kwargs.get("slug"),
|
||||
project_id=self.kwargs.get("project_id"),
|
||||
project_deploy_board = ProjectDeployBoard.objects.get(
|
||||
workspace__slug=self.kwargs.get("slug"),
|
||||
project_id=self.kwargs.get("project_id"),
|
||||
)
|
||||
if project_deploy_board.comments:
|
||||
return self.filter_queryset(
|
||||
super()
|
||||
.get_queryset()
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.filter(issue_id=self.kwargs.get("issue_id"))
|
||||
.select_related("project")
|
||||
.select_related("workspace")
|
||||
.select_related("issue")
|
||||
.distinct()
|
||||
)
|
||||
if project_deploy_board.comments:
|
||||
return self.filter_queryset(
|
||||
super()
|
||||
.get_queryset()
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.filter(issue_id=self.kwargs.get("issue_id"))
|
||||
.filter(access="EXTERNAL")
|
||||
.select_related("project")
|
||||
.select_related("workspace")
|
||||
.select_related("issue")
|
||||
.annotate(
|
||||
is_member=Exists(
|
||||
ProjectMember.objects.filter(
|
||||
workspace__slug=self.kwargs.get("slug"),
|
||||
project_id=self.kwargs.get("project_id"),
|
||||
member_id=self.request.user.id,
|
||||
)
|
||||
)
|
||||
)
|
||||
.distinct()
|
||||
)
|
||||
else:
|
||||
return IssueComment.objects.none()
|
||||
except ProjectDeployBoard.DoesNotExist:
|
||||
else:
|
||||
return IssueComment.objects.none()
|
||||
|
||||
def create(self, request, slug, project_id, issue_id):
|
||||
@@ -1593,13 +1499,21 @@ class IssueCommentPublicViewSet(BaseViewSet):
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
access = (
|
||||
"INTERNAL"
|
||||
if ProjectMember.objects.filter(
|
||||
project_id=project_id, member=request.user
|
||||
).exists()
|
||||
else "EXTERNAL"
|
||||
)
|
||||
|
||||
serializer = IssueCommentSerializer(data=request.data)
|
||||
if serializer.is_valid():
|
||||
serializer.save(
|
||||
project_id=project_id,
|
||||
issue_id=issue_id,
|
||||
actor=request.user,
|
||||
access="EXTERNAL",
|
||||
access=access,
|
||||
)
|
||||
issue_activity.delay(
|
||||
type="comment.activity.created",
|
||||
@@ -1609,16 +1523,6 @@ class IssueCommentPublicViewSet(BaseViewSet):
|
||||
project_id=str(project_id),
|
||||
current_instance=None,
|
||||
)
|
||||
if not ProjectMember.objects.filter(
|
||||
project_id=project_id,
|
||||
member=request.user,
|
||||
).exists():
|
||||
# Add the user for workspace tracking
|
||||
_ = ProjectPublicMember.objects.get_or_create(
|
||||
project_id=project_id,
|
||||
member=request.user,
|
||||
)
|
||||
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
except Exception as e:
|
||||
@@ -1663,8 +1567,7 @@ class IssueCommentPublicViewSet(BaseViewSet):
|
||||
except (IssueComment.DoesNotExist, ProjectDeployBoard.DoesNotExist):
|
||||
return Response(
|
||||
{"error": "IssueComent Does not exists"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
status=status.HTTP_400_BAD_REQUEST,)
|
||||
|
||||
def destroy(self, request, slug, project_id, issue_id, pk):
|
||||
try:
|
||||
@@ -1711,24 +1614,21 @@ class IssueReactionPublicViewSet(BaseViewSet):
|
||||
model = IssueReaction
|
||||
|
||||
def get_queryset(self):
|
||||
try:
|
||||
project_deploy_board = ProjectDeployBoard.objects.get(
|
||||
workspace__slug=self.kwargs.get("slug"),
|
||||
project_id=self.kwargs.get("project_id"),
|
||||
project_deploy_board = ProjectDeployBoard.objects.get(
|
||||
workspace__slug=self.kwargs.get("slug"),
|
||||
project_id=self.kwargs.get("project_id"),
|
||||
)
|
||||
if project_deploy_board.reactions:
|
||||
return (
|
||||
super()
|
||||
.get_queryset()
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.filter(project_id=self.kwargs.get("project_id"))
|
||||
.filter(issue_id=self.kwargs.get("issue_id"))
|
||||
.order_by("-created_at")
|
||||
.distinct()
|
||||
)
|
||||
if project_deploy_board.reactions:
|
||||
return (
|
||||
super()
|
||||
.get_queryset()
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.filter(project_id=self.kwargs.get("project_id"))
|
||||
.filter(issue_id=self.kwargs.get("issue_id"))
|
||||
.order_by("-created_at")
|
||||
.distinct()
|
||||
)
|
||||
else:
|
||||
return IssueReaction.objects.none()
|
||||
except ProjectDeployBoard.DoesNotExist:
|
||||
else:
|
||||
return IssueReaction.objects.none()
|
||||
|
||||
def create(self, request, slug, project_id, issue_id):
|
||||
@@ -1748,23 +1648,6 @@ class IssueReactionPublicViewSet(BaseViewSet):
|
||||
serializer.save(
|
||||
project_id=project_id, issue_id=issue_id, actor=request.user
|
||||
)
|
||||
if not ProjectMember.objects.filter(
|
||||
project_id=project_id,
|
||||
member=request.user,
|
||||
).exists():
|
||||
# Add the user for workspace tracking
|
||||
_ = ProjectPublicMember.objects.get_or_create(
|
||||
project_id=project_id,
|
||||
member=request.user,
|
||||
)
|
||||
issue_activity.delay(
|
||||
type="issue_reaction.activity.created",
|
||||
requested_data=json.dumps(self.request.data, cls=DjangoJSONEncoder),
|
||||
actor_id=str(self.request.user.id),
|
||||
issue_id=str(self.kwargs.get("issue_id", None)),
|
||||
project_id=str(self.kwargs.get("project_id", None)),
|
||||
current_instance=None,
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
except ProjectDeployBoard.DoesNotExist:
|
||||
@@ -1796,19 +1679,6 @@ class IssueReactionPublicViewSet(BaseViewSet):
|
||||
reaction=reaction_code,
|
||||
actor=request.user,
|
||||
)
|
||||
issue_activity.delay(
|
||||
type="issue_reaction.activity.deleted",
|
||||
requested_data=None,
|
||||
actor_id=str(self.request.user.id),
|
||||
issue_id=str(self.kwargs.get("issue_id", None)),
|
||||
project_id=str(self.kwargs.get("project_id", None)),
|
||||
current_instance=json.dumps(
|
||||
{
|
||||
"reaction": str(reaction_code),
|
||||
"identifier": str(issue_reaction.id),
|
||||
}
|
||||
),
|
||||
)
|
||||
issue_reaction.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
except IssueReaction.DoesNotExist:
|
||||
@@ -1829,24 +1699,21 @@ class CommentReactionPublicViewSet(BaseViewSet):
|
||||
model = CommentReaction
|
||||
|
||||
def get_queryset(self):
|
||||
try:
|
||||
project_deploy_board = ProjectDeployBoard.objects.get(
|
||||
workspace__slug=self.kwargs.get("slug"),
|
||||
project_id=self.kwargs.get("project_id"),
|
||||
project_deploy_board = ProjectDeployBoard.objects.get(
|
||||
workspace__slug=self.kwargs.get("slug"),
|
||||
project_id=self.kwargs.get("project_id"),
|
||||
)
|
||||
if project_deploy_board.reactions:
|
||||
return (
|
||||
super()
|
||||
.get_queryset()
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.filter(project_id=self.kwargs.get("project_id"))
|
||||
.filter(comment_id=self.kwargs.get("comment_id"))
|
||||
.order_by("-created_at")
|
||||
.distinct()
|
||||
)
|
||||
if project_deploy_board.reactions:
|
||||
return (
|
||||
super()
|
||||
.get_queryset()
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.filter(project_id=self.kwargs.get("project_id"))
|
||||
.filter(comment_id=self.kwargs.get("comment_id"))
|
||||
.order_by("-created_at")
|
||||
.distinct()
|
||||
)
|
||||
else:
|
||||
return CommentReaction.objects.none()
|
||||
except ProjectDeployBoard.DoesNotExist:
|
||||
else:
|
||||
return CommentReaction.objects.none()
|
||||
|
||||
def create(self, request, slug, project_id, comment_id):
|
||||
@@ -1866,29 +1733,8 @@ class CommentReactionPublicViewSet(BaseViewSet):
|
||||
serializer.save(
|
||||
project_id=project_id, comment_id=comment_id, actor=request.user
|
||||
)
|
||||
if not ProjectMember.objects.filter(
|
||||
project_id=project_id, member=request.user
|
||||
).exists():
|
||||
# Add the user for workspace tracking
|
||||
_ = ProjectPublicMember.objects.get_or_create(
|
||||
project_id=project_id,
|
||||
member=request.user,
|
||||
)
|
||||
issue_activity.delay(
|
||||
type="comment_reaction.activity.created",
|
||||
requested_data=json.dumps(self.request.data, cls=DjangoJSONEncoder),
|
||||
actor_id=str(self.request.user.id),
|
||||
issue_id=None,
|
||||
project_id=str(self.kwargs.get("project_id", None)),
|
||||
current_instance=None,
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
except IssueComment.DoesNotExist:
|
||||
return Response(
|
||||
{"error": "Comment does not exist"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
except ProjectDeployBoard.DoesNotExist:
|
||||
return Response(
|
||||
{"error": "Project board does not exist"},
|
||||
@@ -1919,20 +1765,6 @@ class CommentReactionPublicViewSet(BaseViewSet):
|
||||
reaction=reaction_code,
|
||||
actor=request.user,
|
||||
)
|
||||
issue_activity.delay(
|
||||
type="comment_reaction.activity.deleted",
|
||||
requested_data=None,
|
||||
actor_id=str(self.request.user.id),
|
||||
issue_id=None,
|
||||
project_id=str(self.kwargs.get("project_id", None)),
|
||||
current_instance=json.dumps(
|
||||
{
|
||||
"reaction": str(reaction_code),
|
||||
"identifier": str(comment_reaction.id),
|
||||
"comment_id": str(comment_id),
|
||||
}
|
||||
),
|
||||
)
|
||||
comment_reaction.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
except CommentReaction.DoesNotExist:
|
||||
@@ -1953,23 +1785,13 @@ class IssueVotePublicViewSet(BaseViewSet):
|
||||
serializer_class = IssueVoteSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
try:
|
||||
project_deploy_board = ProjectDeployBoard.objects.get(
|
||||
workspace__slug=self.kwargs.get("slug"),
|
||||
project_id=self.kwargs.get("project_id"),
|
||||
)
|
||||
if project_deploy_board.votes:
|
||||
return (
|
||||
super()
|
||||
.get_queryset()
|
||||
.filter(issue_id=self.kwargs.get("issue_id"))
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.filter(project_id=self.kwargs.get("project_id"))
|
||||
)
|
||||
else:
|
||||
return IssueVote.objects.none()
|
||||
except ProjectDeployBoard.DoesNotExist:
|
||||
return IssueVote.objects.none()
|
||||
return (
|
||||
super()
|
||||
.get_queryset()
|
||||
.filter(issue_id=self.kwargs.get("issue_id"))
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.filter(project_id=self.kwargs.get("project_id"))
|
||||
)
|
||||
|
||||
def create(self, request, slug, project_id, issue_id):
|
||||
try:
|
||||
@@ -1977,31 +1799,10 @@ class IssueVotePublicViewSet(BaseViewSet):
|
||||
actor_id=request.user.id,
|
||||
project_id=project_id,
|
||||
issue_id=issue_id,
|
||||
)
|
||||
# Add the user for workspace tracking
|
||||
if not ProjectMember.objects.filter(
|
||||
project_id=project_id, member=request.user
|
||||
).exists():
|
||||
_ = ProjectPublicMember.objects.get_or_create(
|
||||
project_id=project_id,
|
||||
member=request.user,
|
||||
)
|
||||
issue_vote.vote = request.data.get("vote", 1)
|
||||
issue_vote.save()
|
||||
issue_activity.delay(
|
||||
type="issue_vote.activity.created",
|
||||
requested_data=json.dumps(self.request.data, cls=DjangoJSONEncoder),
|
||||
actor_id=str(self.request.user.id),
|
||||
issue_id=str(self.kwargs.get("issue_id", None)),
|
||||
project_id=str(self.kwargs.get("project_id", None)),
|
||||
current_instance=None,
|
||||
vote=request.data.get("vote", 1),
|
||||
)
|
||||
serializer = IssueVoteSerializer(issue_vote)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
except IntegrityError:
|
||||
return Response(
|
||||
{"error": "Reaction already exists"}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
return Response(
|
||||
@@ -2017,19 +1818,6 @@ class IssueVotePublicViewSet(BaseViewSet):
|
||||
issue_id=issue_id,
|
||||
actor_id=request.user.id,
|
||||
)
|
||||
issue_activity.delay(
|
||||
type="issue_vote.activity.deleted",
|
||||
requested_data=None,
|
||||
actor_id=str(self.request.user.id),
|
||||
issue_id=str(self.kwargs.get("issue_id", None)),
|
||||
project_id=str(self.kwargs.get("project_id", None)),
|
||||
current_instance=json.dumps(
|
||||
{
|
||||
"vote": str(issue_vote.vote),
|
||||
"identifier": str(issue_vote.id),
|
||||
}
|
||||
),
|
||||
)
|
||||
issue_vote.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
except Exception as e:
|
||||
@@ -2040,196 +1828,27 @@ class IssueVotePublicViewSet(BaseViewSet):
|
||||
)
|
||||
|
||||
|
||||
class IssueRetrievePublicEndpoint(BaseAPIView):
|
||||
class ExportIssuesEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
AllowAny,
|
||||
WorkSpaceAdminPermission,
|
||||
]
|
||||
|
||||
def get(self, request, slug, project_id, issue_id):
|
||||
def post(self, request, slug):
|
||||
try:
|
||||
issue = Issue.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, pk=issue_id
|
||||
|
||||
issue_export_task.delay(
|
||||
email=request.user.email, data=request.data, slug=slug ,exporter_name=request.user.first_name
|
||||
)
|
||||
serializer = IssuePublicSerializer(issue)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
except Issue.DoesNotExist:
|
||||
return Response(
|
||||
{"error": "Issue Does not exist"}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
class ProjectIssuesPublicEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
AllowAny,
|
||||
]
|
||||
|
||||
def get(self, request, slug, project_id):
|
||||
try:
|
||||
project_deploy_board = ProjectDeployBoard.objects.get(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
|
||||
filters = issue_filters(request.query_params, "GET")
|
||||
|
||||
# Custom ordering for priority and state
|
||||
priority_order = ["urgent", "high", "medium", "low", None]
|
||||
state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
|
||||
|
||||
order_by_param = request.GET.get("order_by", "-created_at")
|
||||
|
||||
issue_queryset = (
|
||||
Issue.issue_objects.annotate(
|
||||
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
.filter(project_id=project_id)
|
||||
.filter(workspace__slug=slug)
|
||||
.select_related("project", "workspace", "state", "parent")
|
||||
.prefetch_related("assignees", "labels")
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"issue_reactions",
|
||||
queryset=IssueReaction.objects.select_related("actor"),
|
||||
)
|
||||
)
|
||||
.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
|
||||
if order_by_param == "priority" or order_by_param == "-priority":
|
||||
priority_order = (
|
||||
priority_order
|
||||
if order_by_param == "priority"
|
||||
else priority_order[::-1]
|
||||
)
|
||||
issue_queryset = issue_queryset.annotate(
|
||||
priority_order=Case(
|
||||
*[
|
||||
When(priority=p, then=Value(i))
|
||||
for i, p in enumerate(priority_order)
|
||||
],
|
||||
output_field=CharField(),
|
||||
)
|
||||
).order_by("priority_order")
|
||||
|
||||
# State Ordering
|
||||
elif order_by_param in [
|
||||
"state__name",
|
||||
"state__group",
|
||||
"-state__name",
|
||||
"-state__group",
|
||||
]:
|
||||
state_order = (
|
||||
state_order
|
||||
if order_by_param in ["state__name", "state__group"]
|
||||
else state_order[::-1]
|
||||
)
|
||||
issue_queryset = issue_queryset.annotate(
|
||||
state_order=Case(
|
||||
*[
|
||||
When(state__group=state_group, then=Value(i))
|
||||
for i, state_group in enumerate(state_order)
|
||||
],
|
||||
default=Value(len(state_order)),
|
||||
output_field=CharField(),
|
||||
)
|
||||
).order_by("state_order")
|
||||
# assignee and label ordering
|
||||
elif order_by_param in [
|
||||
"labels__name",
|
||||
"-labels__name",
|
||||
"assignees__first_name",
|
||||
"-assignees__first_name",
|
||||
]:
|
||||
issue_queryset = issue_queryset.annotate(
|
||||
max_values=Max(
|
||||
order_by_param[1::]
|
||||
if order_by_param.startswith("-")
|
||||
else order_by_param
|
||||
)
|
||||
).order_by(
|
||||
"-max_values" if order_by_param.startswith("-") else "max_values"
|
||||
)
|
||||
else:
|
||||
issue_queryset = issue_queryset.order_by(order_by_param)
|
||||
|
||||
issues = IssuePublicSerializer(issue_queryset, many=True).data
|
||||
|
||||
state_group_order = [
|
||||
"backlog",
|
||||
"unstarted",
|
||||
"started",
|
||||
"completed",
|
||||
"cancelled",
|
||||
]
|
||||
|
||||
states = (
|
||||
State.objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
)
|
||||
.annotate(
|
||||
custom_order=Case(
|
||||
*[
|
||||
When(group=value, then=Value(index))
|
||||
for index, value in enumerate(state_group_order)
|
||||
],
|
||||
default=Value(len(state_group_order)),
|
||||
output_field=IntegerField(),
|
||||
),
|
||||
)
|
||||
.values("name", "group", "color", "id")
|
||||
.order_by("custom_order", "sequence")
|
||||
)
|
||||
|
||||
labels = Label.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
).values("id", "name", "color", "parent")
|
||||
|
||||
## Grouping the results
|
||||
group_by = request.GET.get("group_by", False)
|
||||
if group_by:
|
||||
issues = group_results(issues, group_by)
|
||||
|
||||
return Response(
|
||||
{
|
||||
"issues": issues,
|
||||
"states": states,
|
||||
"labels": labels,
|
||||
"message": f"Once the export is ready it will be emailed to you at {str(request.user.email)}"
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
except ProjectDeployBoard.DoesNotExist:
|
||||
return Response(
|
||||
{"error": "Board does not exists"}, status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
)
|
||||
@@ -10,13 +10,7 @@ from plane.utils.paginator import BasePaginator
|
||||
|
||||
# Module imports
|
||||
from .base import BaseViewSet, BaseAPIView
|
||||
from plane.db.models import (
|
||||
Notification,
|
||||
IssueAssignee,
|
||||
IssueSubscriber,
|
||||
Issue,
|
||||
WorkspaceMember,
|
||||
)
|
||||
from plane.db.models import Notification, IssueAssignee, IssueSubscriber, Issue, WorkspaceMember
|
||||
from plane.api.serializers import NotificationSerializer
|
||||
|
||||
|
||||
@@ -89,17 +83,13 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
|
||||
|
||||
# Created issues
|
||||
if type == "created":
|
||||
if WorkspaceMember.objects.filter(
|
||||
workspace__slug=slug, member=request.user, role__lt=15
|
||||
).exists():
|
||||
if WorkspaceMember.objects.filter(workspace__slug=slug, member=request.user, role__lt=15).exists():
|
||||
notifications = Notification.objects.none()
|
||||
else:
|
||||
issue_ids = Issue.objects.filter(
|
||||
workspace__slug=slug, created_by=request.user
|
||||
).values_list("pk", flat=True)
|
||||
notifications = notifications.filter(
|
||||
entity_identifier__in=issue_ids
|
||||
)
|
||||
notifications = notifications.filter(entity_identifier__in=issue_ids)
|
||||
|
||||
# Pagination
|
||||
if request.GET.get("per_page", False) and request.GET.get("cursor", False):
|
||||
@@ -284,80 +274,3 @@ class UnreadNotificationEndpoint(BaseAPIView):
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
class MarkAllReadNotificationViewSet(BaseViewSet):
|
||||
def create(self, request, slug):
|
||||
try:
|
||||
snoozed = request.data.get("snoozed", False)
|
||||
archived = request.data.get("archived", False)
|
||||
type = request.data.get("type", "all")
|
||||
|
||||
notifications = (
|
||||
Notification.objects.filter(
|
||||
workspace__slug=slug,
|
||||
receiver_id=request.user.id,
|
||||
read_at__isnull=True,
|
||||
)
|
||||
.select_related("workspace", "project", "triggered_by", "receiver")
|
||||
.order_by("snoozed_till", "-created_at")
|
||||
)
|
||||
|
||||
# Filter for snoozed notifications
|
||||
if snoozed:
|
||||
notifications = notifications.filter(
|
||||
Q(snoozed_till__lt=timezone.now()) | Q(snoozed_till__isnull=False)
|
||||
)
|
||||
else:
|
||||
notifications = notifications.filter(
|
||||
Q(snoozed_till__gte=timezone.now()) | Q(snoozed_till__isnull=True),
|
||||
)
|
||||
|
||||
# Filter for archived or unarchive
|
||||
if archived:
|
||||
notifications = notifications.filter(archived_at__isnull=False)
|
||||
else:
|
||||
notifications = notifications.filter(archived_at__isnull=True)
|
||||
|
||||
# Subscribed issues
|
||||
if type == "watching":
|
||||
issue_ids = IssueSubscriber.objects.filter(
|
||||
workspace__slug=slug, subscriber_id=request.user.id
|
||||
).values_list("issue_id", flat=True)
|
||||
notifications = notifications.filter(entity_identifier__in=issue_ids)
|
||||
|
||||
# Assigned Issues
|
||||
if type == "assigned":
|
||||
issue_ids = IssueAssignee.objects.filter(
|
||||
workspace__slug=slug, assignee_id=request.user.id
|
||||
).values_list("issue_id", flat=True)
|
||||
notifications = notifications.filter(entity_identifier__in=issue_ids)
|
||||
|
||||
# Created issues
|
||||
if type == "created":
|
||||
if WorkspaceMember.objects.filter(
|
||||
workspace__slug=slug, member=request.user, role__lt=15
|
||||
).exists():
|
||||
notifications = Notification.objects.none()
|
||||
else:
|
||||
issue_ids = Issue.objects.filter(
|
||||
workspace__slug=slug, created_by=request.user
|
||||
).values_list("pk", flat=True)
|
||||
notifications = notifications.filter(
|
||||
entity_identifier__in=issue_ids
|
||||
)
|
||||
|
||||
updated_notifications = []
|
||||
for notification in notifications:
|
||||
notification.read_at = timezone.now()
|
||||
updated_notifications.append(notification)
|
||||
Notification.objects.bulk_update(
|
||||
updated_notifications, ["read_at"], batch_size=100
|
||||
)
|
||||
return Response({"message": "Successful"}, status=status.HTTP_200_OK)
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@@ -11,8 +11,14 @@ from django.db.models import (
|
||||
OuterRef,
|
||||
Func,
|
||||
F,
|
||||
Max,
|
||||
CharField,
|
||||
Func,
|
||||
Subquery,
|
||||
Prefetch,
|
||||
When,
|
||||
Case,
|
||||
Value,
|
||||
)
|
||||
from django.core.validators import validate_email
|
||||
from django.conf import settings
|
||||
@@ -41,7 +47,6 @@ from plane.api.permissions import (
|
||||
ProjectBasePermission,
|
||||
ProjectEntityPermission,
|
||||
ProjectMemberPermission,
|
||||
ProjectLitePermission,
|
||||
)
|
||||
|
||||
from plane.db.models import (
|
||||
@@ -66,9 +71,16 @@ from plane.db.models import (
|
||||
ModuleMember,
|
||||
Inbox,
|
||||
ProjectDeployBoard,
|
||||
Issue,
|
||||
IssueReaction,
|
||||
IssueLink,
|
||||
IssueAttachment,
|
||||
Label,
|
||||
)
|
||||
|
||||
from plane.bgtasks.project_invitation_task import project_invitation
|
||||
from plane.utils.grouper import group_results
|
||||
from plane.utils.issue_filters import issue_filters
|
||||
|
||||
|
||||
class ProjectViewSet(BaseViewSet):
|
||||
@@ -275,10 +287,7 @@ class ProjectViewSet(BaseViewSet):
|
||||
)
|
||||
|
||||
data = serializer.data
|
||||
# Additional fields of the member
|
||||
data["sort_order"] = project_member.sort_order
|
||||
data["member_role"] = project_member.role
|
||||
data["is_member"] = True
|
||||
return Response(data, status=status.HTTP_201_CREATED)
|
||||
return Response(
|
||||
serializer.errors,
|
||||
@@ -617,7 +626,7 @@ class ProjectMemberViewSet(BaseViewSet):
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
except ProjectMember.DoesNotExist:
|
||||
return Response(
|
||||
{"error": "Project Member does not exist"}, status=status.HTTP_400_BAD_REQUEST
|
||||
{"error": "Project Member does not exist"}, status=status.HTTP_400
|
||||
)
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
@@ -1131,78 +1140,145 @@ class ProjectDeployBoardPublicSettingsEndpoint(BaseAPIView):
|
||||
)
|
||||
|
||||
|
||||
class WorkspaceProjectDeployBoardEndpoint(BaseAPIView):
|
||||
class ProjectDeployBoardIssuesPublicEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
AllowAny,
|
||||
]
|
||||
|
||||
def get(self, request, slug):
|
||||
def get(self, request, slug, project_id):
|
||||
try:
|
||||
projects = (
|
||||
Project.objects.filter(workspace__slug=slug)
|
||||
.annotate(
|
||||
is_public=Exists(
|
||||
ProjectDeployBoard.objects.filter(
|
||||
workspace__slug=slug, project_id=OuterRef("pk")
|
||||
)
|
||||
project_deploy_board = ProjectDeployBoard.objects.get(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
|
||||
filters = issue_filters(request.query_params, "GET")
|
||||
|
||||
# Custom ordering for priority and state
|
||||
priority_order = ["urgent", "high", "medium", "low", None]
|
||||
state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
|
||||
|
||||
order_by_param = request.GET.get("order_by", "-created_at")
|
||||
|
||||
issue_queryset = (
|
||||
Issue.issue_objects.annotate(
|
||||
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
.filter(project_id=project_id)
|
||||
.filter(workspace__slug=slug)
|
||||
.select_related("project", "workspace", "state", "parent")
|
||||
.prefetch_related("assignees", "labels")
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"issue_reactions",
|
||||
queryset=IssueReaction.objects.select_related("actor"),
|
||||
)
|
||||
)
|
||||
.filter(is_public=True)
|
||||
).values(
|
||||
"id",
|
||||
"identifier",
|
||||
"name",
|
||||
"description",
|
||||
"emoji",
|
||||
"icon_prop",
|
||||
"cover_image",
|
||||
)
|
||||
|
||||
return Response(projects, status=status.HTTP_200_OK)
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
class LeaveProjectEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
ProjectLitePermission,
|
||||
]
|
||||
|
||||
def delete(self, request, slug, project_id):
|
||||
try:
|
||||
project_member = ProjectMember.objects.get(
|
||||
workspace__slug=slug,
|
||||
member=request.user,
|
||||
project_id=project_id,
|
||||
)
|
||||
|
||||
# Only Admin case
|
||||
if (
|
||||
project_member.role == 20
|
||||
and ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
role=20,
|
||||
project_id=project_id,
|
||||
).count()
|
||||
== 1
|
||||
):
|
||||
return Response(
|
||||
{
|
||||
"error": "You cannot leave the project since you are the only admin of the project you should delete the project"
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
.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")
|
||||
)
|
||||
# Delete the member from workspace
|
||||
project_member.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
except ProjectMember.DoesNotExist:
|
||||
.annotate(
|
||||
attachment_count=IssueAttachment.objects.filter(
|
||||
issue=OuterRef("id")
|
||||
)
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
)
|
||||
|
||||
# Priority Ordering
|
||||
if order_by_param == "priority" or order_by_param == "-priority":
|
||||
priority_order = (
|
||||
priority_order
|
||||
if order_by_param == "priority"
|
||||
else priority_order[::-1]
|
||||
)
|
||||
issue_queryset = issue_queryset.annotate(
|
||||
priority_order=Case(
|
||||
*[
|
||||
When(priority=p, then=Value(i))
|
||||
for i, p in enumerate(priority_order)
|
||||
],
|
||||
output_field=CharField(),
|
||||
)
|
||||
).order_by("priority_order")
|
||||
|
||||
# State Ordering
|
||||
elif order_by_param in [
|
||||
"state__name",
|
||||
"state__group",
|
||||
"-state__name",
|
||||
"-state__group",
|
||||
]:
|
||||
state_order = (
|
||||
state_order
|
||||
if order_by_param in ["state__name", "state__group"]
|
||||
else state_order[::-1]
|
||||
)
|
||||
issue_queryset = issue_queryset.annotate(
|
||||
state_order=Case(
|
||||
*[
|
||||
When(state__group=state_group, then=Value(i))
|
||||
for i, state_group in enumerate(state_order)
|
||||
],
|
||||
default=Value(len(state_order)),
|
||||
output_field=CharField(),
|
||||
)
|
||||
).order_by("state_order")
|
||||
# assignee and label ordering
|
||||
elif order_by_param in [
|
||||
"labels__name",
|
||||
"-labels__name",
|
||||
"assignees__first_name",
|
||||
"-assignees__first_name",
|
||||
]:
|
||||
issue_queryset = issue_queryset.annotate(
|
||||
max_values=Max(
|
||||
order_by_param[1::]
|
||||
if order_by_param.startswith("-")
|
||||
else order_by_param
|
||||
)
|
||||
).order_by(
|
||||
"-max_values" if order_by_param.startswith("-") else "max_values"
|
||||
)
|
||||
else:
|
||||
issue_queryset = issue_queryset.order_by(order_by_param)
|
||||
|
||||
issues = IssueLiteSerializer(issue_queryset, many=True).data
|
||||
|
||||
states = State.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
).values("name", "group", "color", "id")
|
||||
|
||||
labels = Label.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
).values("id", "name", "color", "parent")
|
||||
|
||||
## Grouping the results
|
||||
group_by = request.GET.get("group_by", False)
|
||||
if group_by:
|
||||
issues = group_results(issues, group_by)
|
||||
|
||||
return Response(
|
||||
{"error": "Workspace member does not exists"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
{
|
||||
"issues": issues,
|
||||
"states": states,
|
||||
"labels": labels,
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
except ProjectDeployBoard.DoesNotExist:
|
||||
return Response(
|
||||
{"error": "Board does not exists"}, status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
|
||||
@@ -137,11 +137,11 @@ class UpdateUserTourCompletedEndpoint(BaseAPIView):
|
||||
|
||||
|
||||
class UserActivityEndpoint(BaseAPIView, BasePaginator):
|
||||
def get(self, request, slug):
|
||||
def get(self, request):
|
||||
try:
|
||||
queryset = IssueActivity.objects.filter(
|
||||
actor=request.user, workspace__slug=slug
|
||||
).select_related("actor", "workspace", "issue", "project")
|
||||
queryset = IssueActivity.objects.filter(actor=request.user).select_related(
|
||||
"actor", "workspace", "issue", "project"
|
||||
)
|
||||
|
||||
return self.paginate(
|
||||
request=request,
|
||||
|
||||
@@ -1100,6 +1100,7 @@ class WorkspaceUserProfileStatsEndpoint(BaseAPIView):
|
||||
created_issues = (
|
||||
Issue.issue_objects.filter(
|
||||
workspace__slug=slug,
|
||||
assignees__in=[user_id],
|
||||
project__project_projectmember__member=request.user,
|
||||
created_by_id=user_id,
|
||||
)
|
||||
@@ -1197,7 +1198,6 @@ class WorkspaceUserActivityEndpoint(BaseAPIView):
|
||||
projects = request.query_params.getlist("project", [])
|
||||
|
||||
queryset = IssueActivity.objects.filter(
|
||||
~Q(field__in=["comment", "vote", "reaction"]),
|
||||
workspace__slug=slug,
|
||||
project__project_projectmember__member=request.user,
|
||||
actor=user_id,
|
||||
@@ -1473,44 +1473,3 @@ class WorkspaceMembersEndpoint(BaseAPIView):
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
class LeaveWorkspaceEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
WorkspaceEntityPermission,
|
||||
]
|
||||
|
||||
def delete(self, request, slug):
|
||||
try:
|
||||
workspace_member = WorkspaceMember.objects.get(
|
||||
workspace__slug=slug, member=request.user
|
||||
)
|
||||
|
||||
# Only Admin case
|
||||
if (
|
||||
workspace_member.role == 20
|
||||
and WorkspaceMember.objects.filter(
|
||||
workspace__slug=slug, role=20
|
||||
).count()
|
||||
== 1
|
||||
):
|
||||
return Response(
|
||||
{
|
||||
"error": "You cannot leave the workspace since you are the only admin of the workspace you should delete the workspace"
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
# Delete the member from workspace
|
||||
workspace_member.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
except WorkspaceMember.DoesNotExist:
|
||||
return Response(
|
||||
{"error": "Workspace member does not exists"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@@ -4,7 +4,6 @@ import io
|
||||
import json
|
||||
import boto3
|
||||
import zipfile
|
||||
from urllib.parse import urlparse, urlunparse
|
||||
|
||||
# Django imports
|
||||
from django.conf import settings
|
||||
@@ -24,11 +23,9 @@ def dateTimeConverter(time):
|
||||
if time:
|
||||
return time.strftime("%a, %d %b %Y %I:%M:%S %Z%z")
|
||||
|
||||
|
||||
def dateConverter(time):
|
||||
if time:
|
||||
return time.strftime("%a, %d %b %Y")
|
||||
|
||||
return time.strftime("%a, %d %b %Y")
|
||||
|
||||
def create_csv_file(data):
|
||||
csv_buffer = io.StringIO()
|
||||
@@ -69,53 +66,28 @@ def create_zip_file(files):
|
||||
|
||||
|
||||
def upload_to_s3(zip_file, workspace_id, token_id, slug):
|
||||
s3 = boto3.client(
|
||||
"s3",
|
||||
region_name=settings.AWS_REGION,
|
||||
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
|
||||
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
|
||||
config=Config(signature_version="s3v4"),
|
||||
)
|
||||
file_name = f"{workspace_id}/export-{slug}-{token_id[:6]}-{timezone.now()}.zip"
|
||||
|
||||
s3.upload_fileobj(
|
||||
zip_file,
|
||||
settings.AWS_S3_BUCKET_NAME,
|
||||
file_name,
|
||||
ExtraArgs={"ACL": "public-read", "ContentType": "application/zip"},
|
||||
)
|
||||
|
||||
expires_in = 7 * 24 * 60 * 60
|
||||
|
||||
if settings.DOCKERIZED and settings.USE_MINIO:
|
||||
s3 = boto3.client(
|
||||
"s3",
|
||||
endpoint_url=settings.AWS_S3_ENDPOINT_URL,
|
||||
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
|
||||
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
|
||||
config=Config(signature_version="s3v4"),
|
||||
)
|
||||
s3.upload_fileobj(
|
||||
zip_file,
|
||||
settings.AWS_STORAGE_BUCKET_NAME,
|
||||
file_name,
|
||||
ExtraArgs={"ACL": "public-read", "ContentType": "application/zip"},
|
||||
)
|
||||
presigned_url = s3.generate_presigned_url(
|
||||
"get_object",
|
||||
Params={"Bucket": settings.AWS_STORAGE_BUCKET_NAME, "Key": file_name},
|
||||
ExpiresIn=expires_in,
|
||||
)
|
||||
# Create the new url with updated domain and protocol
|
||||
presigned_url = presigned_url.replace(
|
||||
"http://plane-minio:9000/uploads/",
|
||||
f"{settings.AWS_S3_URL_PROTOCOL}//{settings.AWS_S3_CUSTOM_DOMAIN}/",
|
||||
)
|
||||
else:
|
||||
s3 = boto3.client(
|
||||
"s3",
|
||||
region_name=settings.AWS_REGION,
|
||||
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
|
||||
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
|
||||
config=Config(signature_version="s3v4"),
|
||||
)
|
||||
s3.upload_fileobj(
|
||||
zip_file,
|
||||
settings.AWS_S3_BUCKET_NAME,
|
||||
file_name,
|
||||
ExtraArgs={"ACL": "public-read", "ContentType": "application/zip"},
|
||||
)
|
||||
|
||||
presigned_url = s3.generate_presigned_url(
|
||||
"get_object",
|
||||
Params={"Bucket": settings.AWS_S3_BUCKET_NAME, "Key": file_name},
|
||||
ExpiresIn=expires_in,
|
||||
)
|
||||
presigned_url = s3.generate_presigned_url(
|
||||
"get_object",
|
||||
Params={"Bucket": settings.AWS_S3_BUCKET_NAME, "Key": file_name},
|
||||
ExpiresIn=expires_in,
|
||||
)
|
||||
|
||||
exporter_instance = ExporterHistory.objects.get(token=token_id)
|
||||
|
||||
@@ -126,7 +98,7 @@ def upload_to_s3(zip_file, workspace_id, token_id, slug):
|
||||
else:
|
||||
exporter_instance.status = "failed"
|
||||
|
||||
exporter_instance.save(update_fields=["status", "url", "key"])
|
||||
exporter_instance.save(update_fields=["status", "url","key"])
|
||||
|
||||
|
||||
def generate_table_row(issue):
|
||||
@@ -173,7 +145,7 @@ def generate_json_row(issue):
|
||||
else "",
|
||||
"Labels": issue["labels__name"],
|
||||
"Cycle Name": issue["issue_cycle__cycle__name"],
|
||||
"Cycle Start Date": dateConverter(issue["issue_cycle__cycle__start_date"]),
|
||||
"Cycle Start Date": dateConverter(issue["issue_cycle__cycle__start_date"]),
|
||||
"Cycle End Date": dateConverter(issue["issue_cycle__cycle__end_date"]),
|
||||
"Module Name": issue["issue_module__module__name"],
|
||||
"Module Start Date": dateConverter(issue["issue_module__module__start_date"]),
|
||||
@@ -270,9 +242,7 @@ def issue_export_task(provider, workspace_id, project_ids, token_id, multiple, s
|
||||
workspace_issues = (
|
||||
(
|
||||
Issue.objects.filter(
|
||||
workspace__id=workspace_id,
|
||||
project_id__in=project_ids,
|
||||
project__project_projectmember__member=exporter_instance.initiated_by_id,
|
||||
workspace__id=workspace_id, project_id__in=project_ids
|
||||
)
|
||||
.select_related("project", "workspace", "state", "parent", "created_by")
|
||||
.prefetch_related(
|
||||
@@ -305,7 +275,7 @@ def issue_export_task(provider, workspace_id, project_ids, token_id, multiple, s
|
||||
"labels__name",
|
||||
)
|
||||
)
|
||||
.order_by("project__identifier", "sequence_id")
|
||||
.order_by("project__identifier","sequence_id")
|
||||
.distinct()
|
||||
)
|
||||
# CSV header
|
||||
@@ -368,6 +338,7 @@ def issue_export_task(provider, workspace_id, project_ids, token_id, multiple, s
|
||||
exporter_instance.status = "failed"
|
||||
exporter_instance.reason = str(e)
|
||||
exporter_instance.save(update_fields=["status", "reason"])
|
||||
|
||||
# Print logs if in DEBUG mode
|
||||
if settings.DEBUG:
|
||||
print(e)
|
||||
|
||||
@@ -21,29 +21,18 @@ def delete_old_s3_link():
|
||||
expired_exporter_history = ExporterHistory.objects.filter(
|
||||
Q(url__isnull=False) & Q(created_at__lte=timezone.now() - timedelta(days=8))
|
||||
).values_list("key", "id")
|
||||
if settings.DOCKERIZED and settings.USE_MINIO:
|
||||
s3 = boto3.client(
|
||||
"s3",
|
||||
endpoint_url=settings.AWS_S3_ENDPOINT_URL,
|
||||
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
|
||||
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
|
||||
config=Config(signature_version="s3v4"),
|
||||
)
|
||||
else:
|
||||
s3 = boto3.client(
|
||||
"s3",
|
||||
region_name="ap-south-1",
|
||||
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
|
||||
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
|
||||
config=Config(signature_version="s3v4"),
|
||||
)
|
||||
|
||||
s3 = boto3.client(
|
||||
"s3",
|
||||
region_name="ap-south-1",
|
||||
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
|
||||
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
|
||||
config=Config(signature_version="s3v4"),
|
||||
)
|
||||
|
||||
for file_name, exporter_id in expired_exporter_history:
|
||||
# Delete object from S3
|
||||
if file_name:
|
||||
if settings.DOCKERIZED and settings.USE_MINIO:
|
||||
s3.delete_object(Bucket=settings.AWS_STORAGE_BUCKET_NAME, Key=file_name)
|
||||
else:
|
||||
s3.delete_object(Bucket=settings.AWS_S3_BUCKET_NAME, Key=file_name)
|
||||
s3.delete_object(Bucket=settings.AWS_S3_BUCKET_NAME, Key=file_name)
|
||||
|
||||
ExporterHistory.objects.filter(id=exporter_id).update(url=None)
|
||||
|
||||
@@ -24,9 +24,6 @@ from plane.db.models import (
|
||||
IssueSubscriber,
|
||||
Notification,
|
||||
IssueAssignee,
|
||||
IssueReaction,
|
||||
CommentReaction,
|
||||
IssueComment,
|
||||
)
|
||||
from plane.api.serializers import IssueActivitySerializer
|
||||
|
||||
@@ -632,7 +629,7 @@ def update_issue_activity(
|
||||
"parent": track_parent,
|
||||
"priority": track_priority,
|
||||
"state": track_state,
|
||||
"description_html": track_description,
|
||||
"description": track_description,
|
||||
"target_date": track_target_date,
|
||||
"start_date": track_start_date,
|
||||
"labels_list": track_labels,
|
||||
@@ -1025,150 +1022,6 @@ def delete_attachment_activity(
|
||||
)
|
||||
)
|
||||
|
||||
def create_issue_reaction_activity(
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||
):
|
||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||
if requested_data and requested_data.get("reaction") is not None:
|
||||
issue_reaction = IssueReaction.objects.filter(reaction=requested_data.get("reaction"), project=project, actor=actor).values_list('id', flat=True).first()
|
||||
if issue_reaction is not None:
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="created",
|
||||
old_value=None,
|
||||
new_value=requested_data.get("reaction"),
|
||||
field="reaction",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment="added the reaction",
|
||||
old_identifier=None,
|
||||
new_identifier=issue_reaction,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def delete_issue_reaction_activity(
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||
):
|
||||
current_instance = (
|
||||
json.loads(current_instance) if current_instance is not None else None
|
||||
)
|
||||
if current_instance and current_instance.get("reaction") is not None:
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="deleted",
|
||||
old_value=current_instance.get("reaction"),
|
||||
new_value=None,
|
||||
field="reaction",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment="removed the reaction",
|
||||
old_identifier=current_instance.get("identifier"),
|
||||
new_identifier=None,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def create_comment_reaction_activity(
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||
):
|
||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||
if requested_data and requested_data.get("reaction") is not None:
|
||||
comment_reaction_id, comment_id = CommentReaction.objects.filter(reaction=requested_data.get("reaction"), project=project, actor=actor).values_list('id', 'comment__id').first()
|
||||
comment = IssueComment.objects.get(pk=comment_id,project=project)
|
||||
if comment is not None and comment_reaction_id is not None and comment_id is not None:
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=comment.issue_id,
|
||||
actor=actor,
|
||||
verb="created",
|
||||
old_value=None,
|
||||
new_value=requested_data.get("reaction"),
|
||||
field="reaction",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment="added the reaction",
|
||||
old_identifier=None,
|
||||
new_identifier=comment_reaction_id,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def delete_comment_reaction_activity(
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||
):
|
||||
current_instance = (
|
||||
json.loads(current_instance) if current_instance is not None else None
|
||||
)
|
||||
if current_instance and current_instance.get("reaction") is not None:
|
||||
issue_id = IssueComment.objects.filter(pk=current_instance.get("comment_id"), project=project).values_list('issue_id', flat=True).first()
|
||||
if issue_id is not None:
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="deleted",
|
||||
old_value=current_instance.get("reaction"),
|
||||
new_value=None,
|
||||
field="reaction",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment="removed the reaction",
|
||||
old_identifier=current_instance.get("identifier"),
|
||||
new_identifier=None,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def create_issue_vote_activity(
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||
):
|
||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||
if requested_data and requested_data.get("vote") is not None:
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="created",
|
||||
old_value=None,
|
||||
new_value=requested_data.get("vote"),
|
||||
field="vote",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment="added the vote",
|
||||
old_identifier=None,
|
||||
new_identifier=None,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def delete_issue_vote_activity(
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||
):
|
||||
current_instance = (
|
||||
json.loads(current_instance) if current_instance is not None else None
|
||||
)
|
||||
if current_instance and current_instance.get("vote") is not None:
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="deleted",
|
||||
old_value=current_instance.get("vote"),
|
||||
new_value=None,
|
||||
field="vote",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment="removed the vote",
|
||||
old_identifier=current_instance.get("identifier"),
|
||||
new_identifier=None,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# Receive message from room group
|
||||
@shared_task
|
||||
@@ -1192,12 +1045,6 @@ def issue_activity(
|
||||
"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 = Issue.objects.filter(pk=issue_id).first()
|
||||
|
||||
@@ -1233,12 +1080,6 @@ def issue_activity(
|
||||
"link.activity.deleted": delete_link_activity,
|
||||
"attachment.activity.created": create_attachment_activity,
|
||||
"attachment.activity.deleted": delete_attachment_activity,
|
||||
"issue_reaction.activity.created": create_issue_reaction_activity,
|
||||
"issue_reaction.activity.deleted": delete_issue_reaction_activity,
|
||||
"comment_reaction.activity.created": create_comment_reaction_activity,
|
||||
"comment_reaction.activity.deleted": delete_comment_reaction_activity,
|
||||
"issue_vote.activity.created": create_issue_vote_activity,
|
||||
"issue_vote.activity.deleted": delete_issue_vote_activity,
|
||||
}
|
||||
|
||||
func = ACTIVITY_MAPPER.get(type)
|
||||
@@ -1278,12 +1119,6 @@ def issue_activity(
|
||||
"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",
|
||||
]:
|
||||
# Create Notifications
|
||||
bulk_notifications = []
|
||||
|
||||
@@ -64,7 +64,7 @@ def archive_old_issues():
|
||||
issues_to_update.append(issue)
|
||||
|
||||
# Bulk Update the issues and log the activity
|
||||
updated_issues = Issue.objects.bulk_update(
|
||||
Issue.objects.bulk_update(
|
||||
issues_to_update, ["archived_at"], batch_size=100
|
||||
)
|
||||
[
|
||||
@@ -77,7 +77,7 @@ def archive_old_issues():
|
||||
current_instance=None,
|
||||
subscriber=False,
|
||||
)
|
||||
for issue in updated_issues
|
||||
for issue in issues_to_update
|
||||
]
|
||||
return
|
||||
except Exception as e:
|
||||
@@ -136,7 +136,7 @@ def close_old_issues():
|
||||
issues_to_update.append(issue)
|
||||
|
||||
# Bulk Update the issues and log the activity
|
||||
updated_issues = Issue.objects.bulk_update(issues_to_update, ["state"], batch_size=100)
|
||||
Issue.objects.bulk_update(issues_to_update, ["state"], batch_size=100)
|
||||
[
|
||||
issue_activity.delay(
|
||||
type="issue.activity.updated",
|
||||
@@ -147,7 +147,7 @@ def close_old_issues():
|
||||
current_instance=None,
|
||||
subscriber=False,
|
||||
)
|
||||
for issue in updated_issues
|
||||
for issue in issues_to_update
|
||||
]
|
||||
return
|
||||
except Exception as e:
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -19,7 +19,6 @@ from .project import (
|
||||
ProjectIdentifier,
|
||||
ProjectFavorite,
|
||||
ProjectDeployBoard,
|
||||
ProjectPublicMember,
|
||||
)
|
||||
|
||||
from .issue import (
|
||||
|
||||
@@ -293,7 +293,7 @@ class IssueComment(ProjectBaseModel):
|
||||
comment_json = models.JSONField(blank=True, default=dict)
|
||||
comment_html = models.TextField(blank=True, default="<p></p>")
|
||||
attachments = ArrayField(models.URLField(), size=10, blank=True, default=list)
|
||||
issue = models.ForeignKey(Issue, on_delete=models.CASCADE, related_name="issue_comments")
|
||||
issue = models.ForeignKey(Issue, on_delete=models.CASCADE)
|
||||
# System can also create comment
|
||||
actor = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
@@ -476,12 +476,10 @@ class IssueVote(ProjectBaseModel):
|
||||
choices=(
|
||||
(-1, "DOWNVOTE"),
|
||||
(1, "UPVOTE"),
|
||||
),
|
||||
default=1,
|
||||
)
|
||||
)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["issue", "actor",]
|
||||
unique_together = ["issue", "actor"]
|
||||
verbose_name = "Issue Vote"
|
||||
verbose_name_plural = "Issue Votes"
|
||||
db_table = "issue_votes"
|
||||
|
||||
@@ -254,18 +254,3 @@ class ProjectDeployBoard(ProjectBaseModel):
|
||||
def __str__(self):
|
||||
"""Return project and anchor"""
|
||||
return f"{self.anchor} <{self.project.name}>"
|
||||
|
||||
|
||||
class ProjectPublicMember(ProjectBaseModel):
|
||||
member = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="public_project_members",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["project", "member"]
|
||||
verbose_name = "Project Public Member"
|
||||
verbose_name_plural = "Project Public Members"
|
||||
db_table = "project_public_members"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import uuid
|
||||
import string
|
||||
import random
|
||||
import pytz
|
||||
|
||||
# Django imports
|
||||
from django.db import models
|
||||
@@ -10,6 +9,9 @@ from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
from django.contrib.auth.models import AbstractBaseUser, UserManager, PermissionsMixin
|
||||
from django.utils import timezone
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.html import strip_tags
|
||||
from django.conf import settings
|
||||
|
||||
# Third party imports
|
||||
@@ -64,8 +66,7 @@ class User(AbstractBaseUser, PermissionsMixin):
|
||||
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)
|
||||
user_timezone = models.CharField(max_length=255, default="Asia/Kolkata")
|
||||
|
||||
last_active = models.DateTimeField(default=timezone.now, null=True)
|
||||
last_login_time = models.DateTimeField(null=True)
|
||||
|
||||
@@ -49,7 +49,7 @@ MIDDLEWARE = [
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
"crum.CurrentRequestUserMiddleware",
|
||||
"django.middleware.gzip.GZipMiddleware",
|
||||
]
|
||||
]
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
"DEFAULT_AUTHENTICATION_CLASSES": (
|
||||
@@ -161,7 +161,7 @@ MEDIA_URL = "/media/"
|
||||
|
||||
LANGUAGE_CODE = "en-us"
|
||||
|
||||
TIME_ZONE = "UTC"
|
||||
TIME_ZONE = "Asia/Kolkata"
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
python-3.11.5
|
||||
python-3.11.4
|
||||
@@ -1,56 +1,61 @@
|
||||
FROM node:18-alpine AS builder
|
||||
RUN apk add --no-cache libc6-compat
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
ENV NEXT_PUBLIC_API_BASE_URL=http://NEXT_PUBLIC_API_BASE_URL_PLACEHOLDER
|
||||
|
||||
RUN yarn global add turbo
|
||||
COPY . .
|
||||
|
||||
RUN turbo prune --scope=space --docker
|
||||
RUN turbo prune --scope=app --docker
|
||||
|
||||
# Add lockfile and package.json's of isolated subworkspace
|
||||
FROM node:18-alpine AS installer
|
||||
|
||||
RUN apk add --no-cache libc6-compat
|
||||
WORKDIR /app
|
||||
ARG NEXT_PUBLIC_API_BASE_URL=http://localhost:8000
|
||||
|
||||
# First install the dependencies (as they change less often)
|
||||
COPY .gitignore .gitignore
|
||||
COPY --from=builder /app/out/json/ .
|
||||
COPY --from=builder /app/out/yarn.lock ./yarn.lock
|
||||
RUN yarn install --network-timeout 500000
|
||||
|
||||
# Build the project
|
||||
COPY --from=builder /app/out/full/ .
|
||||
COPY turbo.json turbo.json
|
||||
COPY replace-env-vars.sh /usr/local/bin/
|
||||
USER root
|
||||
RUN chmod +x /usr/local/bin/replace-env-vars.sh
|
||||
|
||||
ARG NEXT_PUBLIC_API_BASE_URL=http://localhost:8000
|
||||
ARG NEXT_PUBLIC_DEPLOY_WITH_NGINX=1
|
||||
RUN yarn turbo run build --filter=app
|
||||
|
||||
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL BUILT_NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL NEXT_PUBLIC_DEPLOY_WITH_NGINX=$NEXT_PUBLIC_DEPLOY_WITH_NGINX
|
||||
|
||||
RUN yarn turbo run build --filter=space
|
||||
|
||||
RUN /usr/local/bin/replace-env-vars.sh http://NEXT_PUBLIC_WEBAPP_URL_PLACEHOLDER ${NEXT_PUBLIC_API_BASE_URL} space
|
||||
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \
|
||||
BUILT_NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
|
||||
ENV NEXT_PUBLIC_DEPLOY_URL=${NEXT_PUBLIC_DEPLOY_URL}
|
||||
RUN /usr/local/bin/replace-env-vars.sh http://NEXT_PUBLIC_WEBAPP_URL_PLACEHOLDER ${NEXT_PUBLIC_API_BASE_URL} app
|
||||
|
||||
FROM node:18-alpine AS runner
|
||||
WORKDIR /app
|
||||
|
||||
# Don't run production as root
|
||||
RUN addgroup --system --gid 1001 plane
|
||||
RUN adduser --system --uid 1001 captain
|
||||
USER captain
|
||||
|
||||
COPY --from=installer /app/space/next.config.js .
|
||||
COPY --from=installer /app/space/package.json .
|
||||
COPY --from=installer /app/apps/app/next.config.js .
|
||||
COPY --from=installer /app/apps/app/package.json .
|
||||
|
||||
COPY --from=installer --chown=captain:plane /app/space/.next/standalone ./
|
||||
# Automatically leverage output traces to reduce image size
|
||||
# https://nextjs.org/docs/advanced-features/output-file-tracing
|
||||
COPY --from=installer --chown=captain:plane /app/apps/app/.next/standalone ./
|
||||
|
||||
COPY --from=installer --chown=captain:plane /app/space/.next ./space/.next
|
||||
COPY --from=installer --chown=captain:plane /app/space/public ./space/public
|
||||
COPY --from=installer --chown=captain:plane /app/apps/app/.next ./apps/app/.next
|
||||
|
||||
ARG NEXT_PUBLIC_API_BASE_URL=http://localhost:8000
|
||||
ARG NEXT_PUBLIC_DEPLOY_WITH_NGINX=1
|
||||
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL BUILT_NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL NEXT_PUBLIC_DEPLOY_WITH_NGINX=$NEXT_PUBLIC_DEPLOY_WITH_NGINX
|
||||
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \
|
||||
BUILT_NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
|
||||
|
||||
USER root
|
||||
COPY replace-env-vars.sh /usr/local/bin/
|
||||
+1
-1
@@ -665,7 +665,7 @@ export const CommandK: React.FC<Props> = ({ deleteIssue, isPaletteOpen, setIsPal
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<DiscordIcon className="h-4 w-4" color="rgb(var(--color-text-200))" />
|
||||
<DiscordIcon className="h-4 w-4" color="#6b7280" />
|
||||
Join our Discord
|
||||
</div>
|
||||
</Command.Item>
|
||||
+32
-32
@@ -51,7 +51,7 @@ export const CommandPalette: React.FC = observer(() => {
|
||||
workspaceSlug && projectId && issueId ? ISSUE_DETAILS(issueId as string) : null,
|
||||
workspaceSlug && projectId && issueId
|
||||
? () =>
|
||||
issuesService.retrieve(workspaceSlug as string, projectId as string, issueId as string)
|
||||
issuesService.retrieve(workspaceSlug as string, projectId as string, issueId as string)
|
||||
: null
|
||||
);
|
||||
|
||||
@@ -89,37 +89,37 @@ export const CommandPalette: React.FC = observer(() => {
|
||||
)
|
||||
return;
|
||||
|
||||
if (cmdClicked) {
|
||||
if (keyPressed === "k") {
|
||||
e.preventDefault();
|
||||
setIsPaletteOpen(true);
|
||||
} else if (keyPressed === "c" && altKey) {
|
||||
e.preventDefault();
|
||||
copyIssueUrlToClipboard();
|
||||
} else if (keyPressed === "b") {
|
||||
e.preventDefault();
|
||||
store.theme.setSidebarCollapsed(!store?.theme?.sidebarCollapsed);
|
||||
if (cmdClicked) {
|
||||
if (keyPressed === "k") {
|
||||
e.preventDefault();
|
||||
setIsPaletteOpen(true);
|
||||
} else if (keyPressed === "c" && altKey) {
|
||||
e.preventDefault();
|
||||
copyIssueUrlToClipboard();
|
||||
} else if (keyPressed === "b") {
|
||||
e.preventDefault();
|
||||
store.theme.setSidebarCollapsed(!store?.theme?.sidebarCollapsed);
|
||||
}
|
||||
} else {
|
||||
if (keyPressed === "c") {
|
||||
setIsIssueModalOpen(true);
|
||||
} else if (keyPressed === "p") {
|
||||
setIsProjectModalOpen(true);
|
||||
} else if (keyPressed === "v") {
|
||||
setIsCreateViewModalOpen(true);
|
||||
} else if (keyPressed === "d") {
|
||||
setIsCreateUpdatePageModalOpen(true);
|
||||
} else if (keyPressed === "h") {
|
||||
setIsShortcutsModalOpen(true);
|
||||
} else if (keyPressed === "q") {
|
||||
setIsCreateCycleModalOpen(true);
|
||||
} else if (keyPressed === "m") {
|
||||
setIsCreateModuleModalOpen(true);
|
||||
} else if (keyPressed === "backspace" || keyPressed === "delete") {
|
||||
e.preventDefault();
|
||||
setIsBulkDeleteIssuesModalOpen(true);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (keyPressed === "c") {
|
||||
setIsIssueModalOpen(true);
|
||||
} else if (keyPressed === "p") {
|
||||
setIsProjectModalOpen(true);
|
||||
} else if (keyPressed === "v") {
|
||||
setIsCreateViewModalOpen(true);
|
||||
} else if (keyPressed === "d") {
|
||||
setIsCreateUpdatePageModalOpen(true);
|
||||
} else if (keyPressed === "h") {
|
||||
setIsShortcutsModalOpen(true);
|
||||
} else if (keyPressed === "q") {
|
||||
setIsCreateCycleModalOpen(true);
|
||||
} else if (keyPressed === "m") {
|
||||
setIsCreateModuleModalOpen(true);
|
||||
} else if (keyPressed === "backspace" || keyPressed === "delete") {
|
||||
e.preventDefault();
|
||||
setIsBulkDeleteIssuesModalOpen(true);
|
||||
}
|
||||
}
|
||||
},
|
||||
[copyIssueUrlToClipboard]
|
||||
);
|
||||
@@ -196,4 +196,4 @@ export const CommandPalette: React.FC = observer(() => {
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
})
|
||||
+2
@@ -22,6 +22,8 @@ const shortcuts = [
|
||||
{ keys: "↓", description: "Move down" },
|
||||
{ keys: "←", description: "Move left" },
|
||||
{ keys: "→", description: "Move right" },
|
||||
{ keys: "Enter", description: "Select" },
|
||||
{ keys: "Esc", description: "Close" },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -53,11 +53,7 @@ const UserLink = ({ activity }: { activity: IIssueActivity }) => {
|
||||
|
||||
const activityDetails: {
|
||||
[key: string]: {
|
||||
message: (
|
||||
activity: IIssueActivity,
|
||||
showIssue: boolean,
|
||||
workspaceSlug: string
|
||||
) => React.ReactNode;
|
||||
message: (activity: IIssueActivity, showIssue: boolean) => React.ReactNode;
|
||||
icon: React.ReactNode;
|
||||
};
|
||||
} = {
|
||||
@@ -177,50 +173,26 @@ const activityDetails: {
|
||||
icon: <BlockedIcon height="12" width="12" color="#6b7280" />,
|
||||
},
|
||||
cycles: {
|
||||
message: (activity, showIssue, workspaceSlug) => {
|
||||
message: (activity) => {
|
||||
if (activity.verb === "created")
|
||||
return (
|
||||
<>
|
||||
added this issue to the cycle{" "}
|
||||
<a
|
||||
href={`/${workspaceSlug}/projects/${activity.project}/cycles/${activity.new_identifier}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
||||
>
|
||||
{activity.new_value}
|
||||
<Icon iconName="launch" className="!text-xs" />
|
||||
</a>
|
||||
<span className="font-medium text-custom-text-100">{activity.new_value}</span>.
|
||||
</>
|
||||
);
|
||||
else if (activity.verb === "updated")
|
||||
return (
|
||||
<>
|
||||
set the cycle to{" "}
|
||||
<a
|
||||
href={`/${workspaceSlug}/projects/${activity.project}/cycles/${activity.new_identifier}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
||||
>
|
||||
{activity.new_value}
|
||||
<Icon iconName="launch" className="!text-xs" />
|
||||
</a>
|
||||
<span className="font-medium text-custom-text-100">{activity.new_value}</span>.
|
||||
</>
|
||||
);
|
||||
else
|
||||
return (
|
||||
<>
|
||||
removed the issue from the cycle{" "}
|
||||
<a
|
||||
href={`/${workspaceSlug}/projects/${activity.project}/cycles/${activity.old_identifier}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
||||
>
|
||||
{activity.old_value}
|
||||
<Icon iconName="launch" className="!text-xs" />
|
||||
</a>
|
||||
<span className="font-medium text-custom-text-100">{activity.old_value}</span>.
|
||||
</>
|
||||
);
|
||||
},
|
||||
@@ -353,28 +325,6 @@ const activityDetails: {
|
||||
.
|
||||
</>
|
||||
);
|
||||
else if (activity.verb === "updated")
|
||||
return (
|
||||
<>
|
||||
updated the{" "}
|
||||
<a
|
||||
href={`${activity.old_value}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
||||
>
|
||||
link
|
||||
<Icon iconName="launch" className="!text-xs" />
|
||||
</a>
|
||||
{showIssue && (
|
||||
<>
|
||||
{" "}
|
||||
from <IssueLink activity={activity} />
|
||||
</>
|
||||
)}
|
||||
.
|
||||
</>
|
||||
);
|
||||
else
|
||||
return (
|
||||
<>
|
||||
@@ -401,50 +351,26 @@ const activityDetails: {
|
||||
icon: <Icon iconName="link" className="!text-sm" aria-hidden="true" />,
|
||||
},
|
||||
modules: {
|
||||
message: (activity, showIssue, workspaceSlug) => {
|
||||
message: (activity) => {
|
||||
if (activity.verb === "created")
|
||||
return (
|
||||
<>
|
||||
added this issue to the module{" "}
|
||||
<a
|
||||
href={`/${workspaceSlug}/projects/${activity.project}/modules/${activity.new_identifier}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
||||
>
|
||||
{activity.new_value}
|
||||
<Icon iconName="launch" className="!text-xs" />
|
||||
</a>
|
||||
<span className="font-medium text-custom-text-100">{activity.new_value}</span>.
|
||||
</>
|
||||
);
|
||||
else if (activity.verb === "updated")
|
||||
return (
|
||||
<>
|
||||
set the module to{" "}
|
||||
<a
|
||||
href={`/${workspaceSlug}/projects/${activity.project}/modules/${activity.new_identifier}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
||||
>
|
||||
{activity.new_value}
|
||||
<Icon iconName="launch" className="!text-xs" />
|
||||
</a>
|
||||
<span className="font-medium text-custom-text-100">{activity.new_value}</span>.
|
||||
</>
|
||||
);
|
||||
else
|
||||
return (
|
||||
<>
|
||||
removed the issue from the module{" "}
|
||||
<a
|
||||
href={`/${workspaceSlug}/projects/${activity.project}/modules/${activity.old_identifier}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
||||
>
|
||||
{activity.old_value}
|
||||
<Icon iconName="launch" className="!text-xs" />
|
||||
</a>
|
||||
<span className="font-medium text-custom-text-100">{activity.old_value}</span>.
|
||||
</>
|
||||
);
|
||||
},
|
||||
@@ -612,17 +538,8 @@ export const ActivityMessage = ({
|
||||
}: {
|
||||
activity: IIssueActivity;
|
||||
showIssue?: boolean;
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
return (
|
||||
<>
|
||||
{activityDetails[activity.field as keyof typeof activityDetails]?.message(
|
||||
activity,
|
||||
showIssue,
|
||||
workspaceSlug?.toString() ?? ""
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
}) => (
|
||||
<>
|
||||
{activityDetails[activity.field as keyof typeof activityDetails]?.message(activity, showIssue)}
|
||||
</>
|
||||
);
|
||||
+7
-10
@@ -11,18 +11,15 @@ import { Dialog, Transition } from "@headlessui/react";
|
||||
// hooks
|
||||
import useIssuesView from "hooks/use-issues-view";
|
||||
// components
|
||||
import { DateFilterSelect } from "./date-filter-select";
|
||||
import { DueDateFilterSelect } from "./due-date-filter-select";
|
||||
// ui
|
||||
import { PrimaryButton, SecondaryButton } from "components/ui";
|
||||
// icons
|
||||
import { XMarkIcon } from "@heroicons/react/20/solid";
|
||||
// helpers
|
||||
import { renderDateFormat, renderShortDateWithYearFormat } from "helpers/date-time.helper";
|
||||
import { IIssueFilterOptions } from "types";
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
field: keyof IIssueFilterOptions;
|
||||
isOpen: boolean;
|
||||
handleClose: () => void;
|
||||
};
|
||||
@@ -39,7 +36,7 @@ const defaultValues: TFormValues = {
|
||||
date2: new Date(new Date().getFullYear(), new Date().getMonth() + 1, new Date().getDate()),
|
||||
};
|
||||
|
||||
export const DateFilterModal: React.FC<Props> = ({ title, field, isOpen, handleClose }) => {
|
||||
export const DueDateFilterModal: React.FC<Props> = ({ isOpen, handleClose }) => {
|
||||
const { filters, setFilters } = useIssuesView();
|
||||
|
||||
const router = useRouter();
|
||||
@@ -54,11 +51,11 @@ export const DateFilterModal: React.FC<Props> = ({ title, field, isOpen, handleC
|
||||
|
||||
if (filterType === "range") {
|
||||
setFilters(
|
||||
{ [field]: [`${renderDateFormat(date1)};after`, `${renderDateFormat(date2)};before`] },
|
||||
{ target_date: [`${renderDateFormat(date1)};after`, `${renderDateFormat(date2)};before`] },
|
||||
!Boolean(viewId)
|
||||
);
|
||||
} else {
|
||||
const filteredArray = (filters?.[field] as string[])?.filter((item) => {
|
||||
const filteredArray = filters?.target_date?.filter((item) => {
|
||||
if (item?.includes(filterType)) return false;
|
||||
|
||||
return true;
|
||||
@@ -67,13 +64,13 @@ export const DateFilterModal: React.FC<Props> = ({ title, field, isOpen, handleC
|
||||
const filterOne = filteredArray && filteredArray?.length > 0 ? filteredArray[0] : null;
|
||||
if (filterOne)
|
||||
setFilters(
|
||||
{ [field]: [filterOne, `${renderDateFormat(date1)};${filterType}`] },
|
||||
{ target_date: [filterOne, `${renderDateFormat(date1)};${filterType}`] },
|
||||
!Boolean(viewId)
|
||||
);
|
||||
else
|
||||
setFilters(
|
||||
{
|
||||
[field]: [`${renderDateFormat(date1)};${filterType}`],
|
||||
target_date: [`${renderDateFormat(date1)};${filterType}`],
|
||||
},
|
||||
!Boolean(viewId)
|
||||
);
|
||||
@@ -119,7 +116,7 @@ export const DateFilterModal: React.FC<Props> = ({ title, field, isOpen, handleC
|
||||
control={control}
|
||||
name="filterType"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<DateFilterSelect title={title} value={value} onChange={onChange} />
|
||||
<DueDateFilterSelect value={value} onChange={onChange} />
|
||||
)}
|
||||
/>
|
||||
<XMarkIcon
|
||||
+6
-9
@@ -7,7 +7,6 @@ import { CalendarBeforeIcon, CalendarAfterIcon, CalendarMonthIcon } from "compon
|
||||
// fetch-keys
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
};
|
||||
@@ -20,31 +19,29 @@ type DueDate = {
|
||||
|
||||
const dueDateRange: DueDate[] = [
|
||||
{
|
||||
name: "before",
|
||||
name: "Due date before",
|
||||
value: "before",
|
||||
icon: <CalendarBeforeIcon className="h-4 w-4 " />,
|
||||
},
|
||||
{
|
||||
name: "after",
|
||||
name: "Due date after",
|
||||
value: "after",
|
||||
icon: <CalendarAfterIcon className="h-4 w-4 " />,
|
||||
},
|
||||
{
|
||||
name: "range",
|
||||
name: "Due date range",
|
||||
value: "range",
|
||||
icon: <CalendarMonthIcon className="h-4 w-4 " />,
|
||||
},
|
||||
];
|
||||
|
||||
export const DateFilterSelect: React.FC<Props> = ({ title, value, onChange }) => (
|
||||
export const DueDateFilterSelect: React.FC<Props> = ({ value, onChange }) => (
|
||||
<CustomSelect
|
||||
value={value}
|
||||
label={
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
{dueDateRange.find((item) => item.value === value)?.icon}
|
||||
<span>
|
||||
{title} {dueDateRange.find((item) => item.value === value)?.name}
|
||||
</span>
|
||||
<span>{dueDateRange.find((item) => item.value === value)?.name}</span>
|
||||
</div>
|
||||
}
|
||||
onChange={onChange}
|
||||
@@ -53,7 +50,7 @@ export const DateFilterSelect: React.FC<Props> = ({ title, value, onChange }) =>
|
||||
<CustomSelect.Option key={index} value={option.value}>
|
||||
<>
|
||||
<span>{option.icon}</span>
|
||||
{title} {option.name}
|
||||
{option.name}
|
||||
</>
|
||||
</CustomSelect.Option>
|
||||
))}
|
||||
-28
@@ -240,34 +240,6 @@ export const FiltersList: React.FC<Props> = ({
|
||||
</div>
|
||||
);
|
||||
})
|
||||
: key === "start_date"
|
||||
? filters.start_date?.map((date: string) => {
|
||||
if (filters.start_date && filters.start_date.length <= 0) return null;
|
||||
|
||||
const splitDate = date.split(";");
|
||||
|
||||
return (
|
||||
<div
|
||||
key={date}
|
||||
className="inline-flex items-center gap-x-1 rounded-full border border-custom-border-200 bg-custom-background-100 px-1 py-0.5"
|
||||
>
|
||||
<div className="h-1.5 w-1.5 rounded-full" />
|
||||
<span className="capitalize">
|
||||
{splitDate[1]} {renderShortDateWithYearFormat(splitDate[0])}
|
||||
</span>
|
||||
<span
|
||||
className="cursor-pointer"
|
||||
onClick={() =>
|
||||
setFilters({
|
||||
start_date: filters.start_date?.filter((d: any) => d !== date),
|
||||
})
|
||||
}
|
||||
>
|
||||
<XMarkIcon className="h-3 w-3" />
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
: key === "target_date"
|
||||
? filters.target_date?.map((date: string) => {
|
||||
if (filters.target_date && filters.target_date.length <= 0) return null;
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from "./due-date-filter-modal";
|
||||
export * from "./due-date-filter-select";
|
||||
export * from "./filters-list";
|
||||
export * from "./issues-view-filter";
|
||||
+39
-34
@@ -113,41 +113,46 @@ export const IssuesFilterView: React.FC = () => {
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<SelectFilters
|
||||
filters={filters}
|
||||
onSelect={(option) => {
|
||||
const key = option.key as keyof typeof filters;
|
||||
{issueView !== "gantt_chart" && (
|
||||
<SelectFilters
|
||||
filters={filters}
|
||||
onSelect={(option) => {
|
||||
const key = option.key as keyof typeof filters;
|
||||
|
||||
if (key === "start_date" || key === "target_date") {
|
||||
const valueExists = checkIfArraysHaveSameElements(filters[key] ?? [], option.value);
|
||||
|
||||
setFilters({
|
||||
[key]: valueExists ? null : option.value,
|
||||
});
|
||||
} else {
|
||||
const valueExists = filters[key]?.includes(option.value);
|
||||
|
||||
if (valueExists)
|
||||
setFilters(
|
||||
{
|
||||
[option.key]: ((filters[key] ?? []) as any[])?.filter(
|
||||
(val) => val !== option.value
|
||||
),
|
||||
},
|
||||
!Boolean(viewId)
|
||||
if (key === "target_date") {
|
||||
const valueExists = checkIfArraysHaveSameElements(
|
||||
filters.target_date ?? [],
|
||||
option.value
|
||||
);
|
||||
else
|
||||
setFilters(
|
||||
{
|
||||
[option.key]: [...((filters[key] ?? []) as any[]), option.value],
|
||||
},
|
||||
!Boolean(viewId)
|
||||
);
|
||||
}
|
||||
}}
|
||||
direction="left"
|
||||
height="rg"
|
||||
/>
|
||||
|
||||
setFilters({
|
||||
target_date: valueExists ? null : option.value,
|
||||
});
|
||||
} else {
|
||||
const valueExists = filters[key]?.includes(option.value);
|
||||
|
||||
if (valueExists)
|
||||
setFilters(
|
||||
{
|
||||
[option.key]: ((filters[key] ?? []) as any[])?.filter(
|
||||
(val) => val !== option.value
|
||||
),
|
||||
},
|
||||
!Boolean(viewId)
|
||||
);
|
||||
else
|
||||
setFilters(
|
||||
{
|
||||
[option.key]: [...((filters[key] ?? []) as any[]), option.value],
|
||||
},
|
||||
!Boolean(viewId)
|
||||
);
|
||||
}
|
||||
}}
|
||||
direction="left"
|
||||
height="rg"
|
||||
/>
|
||||
)}
|
||||
<Popover className="relative">
|
||||
{({ open }) => (
|
||||
<>
|
||||
@@ -158,7 +163,7 @@ export const IssuesFilterView: React.FC = () => {
|
||||
: "text-custom-sidebar-text-200"
|
||||
}`}
|
||||
>
|
||||
Display
|
||||
View
|
||||
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
|
||||
</Popover.Button>
|
||||
|
||||
+1
-8
@@ -40,15 +40,9 @@ type Props = {
|
||||
label: string | React.ReactNode;
|
||||
value: string | null;
|
||||
onChange: (data: string) => void;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const ImagePickerPopover: React.FC<Props> = ({
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
disabled = false,
|
||||
}) => {
|
||||
export const ImagePickerPopover: React.FC<Props> = ({ label, value, onChange }) => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const router = useRouter();
|
||||
@@ -123,7 +117,6 @@ export const ImagePickerPopover: React.FC<Props> = ({
|
||||
<Popover.Button
|
||||
className="rounded-md border border-custom-border-300 bg-custom-background-100 px-2 py-1 text-xs text-custom-text-200 hover:text-custom-text-100"
|
||||
onClick={() => setIsOpen((prev) => !prev)}
|
||||
disabled={disabled}
|
||||
>
|
||||
{label}
|
||||
</Popover.Button>
|
||||
+18
-13
@@ -1,5 +1,7 @@
|
||||
import React, { useEffect, useState, forwardRef, useRef } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// react-hook-form
|
||||
import { useForm } from "react-hook-form";
|
||||
// services
|
||||
@@ -10,10 +12,9 @@ import useToast from "hooks/use-toast";
|
||||
import useUserAuth from "hooks/use-user-auth";
|
||||
// ui
|
||||
import { Input, PrimaryButton, SecondaryButton } from "components/ui";
|
||||
import { TipTapEditor } from "components/tiptap";
|
||||
// types
|
||||
import { IIssue, IPageBlock } from "types";
|
||||
|
||||
import { IIssue, IPageBlock } from "types";
|
||||
import Tiptap, { ITiptapRichTextEditor } from "components/tiptap";
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
handleClose: () => void;
|
||||
@@ -31,6 +32,12 @@ type FormData = {
|
||||
task: string;
|
||||
};
|
||||
|
||||
const TiptapEditor = React.forwardRef<ITiptapRichTextEditor, ITiptapRichTextEditor>(
|
||||
(props, ref) => <Tiptap {...props} forwardedRef={ref} />
|
||||
);
|
||||
|
||||
TiptapEditor.displayName = "TiptapEditor";
|
||||
|
||||
export const GptAssistantModal: React.FC<Props> = ({
|
||||
isOpen,
|
||||
handleClose,
|
||||
@@ -133,14 +140,13 @@ export const GptAssistantModal: React.FC<Props> = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`absolute ${inset} z-20 w-full space-y-4 rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-4 shadow ${
|
||||
isOpen ? "block" : "hidden"
|
||||
}`}
|
||||
className={`absolute ${inset} z-20 w-full space-y-4 rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-4 shadow ${isOpen ? "block" : "hidden"
|
||||
}`}
|
||||
>
|
||||
{((content && content !== "") || (htmlContent && htmlContent !== "<p></p>")) && (
|
||||
<div className="text-sm">
|
||||
Content:
|
||||
<TipTapEditor
|
||||
<TiptapEditor
|
||||
workspaceSlug={workspaceSlug as string}
|
||||
value={htmlContent ?? `<p>${content}</p>`}
|
||||
customClassName="-m-3"
|
||||
@@ -154,7 +160,7 @@ export const GptAssistantModal: React.FC<Props> = ({
|
||||
{response !== "" && (
|
||||
<div className="page-block-section text-sm">
|
||||
Response:
|
||||
<TipTapEditor
|
||||
<Tiptap
|
||||
workspaceSlug={workspaceSlug as string}
|
||||
value={`<p>${response}</p>`}
|
||||
customClassName="-mx-3 -my-3"
|
||||
@@ -174,11 +180,10 @@ export const GptAssistantModal: React.FC<Props> = ({
|
||||
type="text"
|
||||
name="task"
|
||||
register={register}
|
||||
placeholder={`${
|
||||
content && content !== ""
|
||||
placeholder={`${content && content !== ""
|
||||
? "Tell AI what action to perform on this content..."
|
||||
: "Ask AI anything..."
|
||||
}`}
|
||||
}`}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<div className={`flex gap-2 ${response === "" ? "justify-end" : "justify-between"}`}>
|
||||
@@ -214,8 +219,8 @@ export const GptAssistantModal: React.FC<Props> = ({
|
||||
{isSubmitting
|
||||
? "Generating response..."
|
||||
: response === ""
|
||||
? "Generate response"
|
||||
: "Generate again"}
|
||||
? "Generate response"
|
||||
: "Generate again"}
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
</div>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user