forked from github/plane
Merge pull request #2309 from makeplane/stage-release
release: stage release to production
This commit is contained in:
commit
8d4ac9b430
54
.env.example
54
.env.example
@ -1,38 +1,3 @@
|
|||||||
# Frontend
|
|
||||||
# Extra image domains that need to be added for Next Image
|
|
||||||
NEXT_PUBLIC_EXTRA_IMAGE_DOMAINS=
|
|
||||||
# Google Client ID for Google OAuth
|
|
||||||
NEXT_PUBLIC_GOOGLE_CLIENTID=""
|
|
||||||
# Github ID for Github OAuth
|
|
||||||
NEXT_PUBLIC_GITHUB_ID=""
|
|
||||||
# Github App Name for GitHub Integration
|
|
||||||
NEXT_PUBLIC_GITHUB_APP_NAME=""
|
|
||||||
# Sentry DSN for error monitoring
|
|
||||||
NEXT_PUBLIC_SENTRY_DSN=""
|
|
||||||
# Enable/Disable OAUTH - default 0 for selfhosted instance
|
|
||||||
NEXT_PUBLIC_ENABLE_OAUTH=0
|
|
||||||
# Enable/Disable sentry
|
|
||||||
NEXT_PUBLIC_ENABLE_SENTRY=0
|
|
||||||
# Enable/Disable session recording
|
|
||||||
NEXT_PUBLIC_ENABLE_SESSION_RECORDER=0
|
|
||||||
# Enable/Disable event tracking
|
|
||||||
NEXT_PUBLIC_TRACK_EVENTS=0
|
|
||||||
# Slack for Slack Integration
|
|
||||||
NEXT_PUBLIC_SLACK_CLIENT_ID=""
|
|
||||||
# For Telemetry, set it to "app.plane.so"
|
|
||||||
NEXT_PUBLIC_PLAUSIBLE_DOMAIN=""
|
|
||||||
# public boards deploy url
|
|
||||||
NEXT_PUBLIC_DEPLOY_URL=""
|
|
||||||
# plane deploy using nginx
|
|
||||||
NEXT_PUBLIC_DEPLOY_WITH_NGINX=1
|
|
||||||
|
|
||||||
# Backend
|
|
||||||
# Debug value for api server use it as 0 for production use
|
|
||||||
DEBUG=0
|
|
||||||
|
|
||||||
# Error logs
|
|
||||||
SENTRY_DSN=""
|
|
||||||
|
|
||||||
# Database Settings
|
# Database Settings
|
||||||
PGUSER="plane"
|
PGUSER="plane"
|
||||||
PGPASSWORD="plane"
|
PGPASSWORD="plane"
|
||||||
@ -45,15 +10,6 @@ REDIS_HOST="plane-redis"
|
|||||||
REDIS_PORT="6379"
|
REDIS_PORT="6379"
|
||||||
REDIS_URL="redis://${REDIS_HOST}:6379/"
|
REDIS_URL="redis://${REDIS_HOST}:6379/"
|
||||||
|
|
||||||
# Email Settings
|
|
||||||
EMAIL_HOST=""
|
|
||||||
EMAIL_HOST_USER=""
|
|
||||||
EMAIL_HOST_PASSWORD=""
|
|
||||||
EMAIL_PORT=587
|
|
||||||
EMAIL_FROM="Team Plane <team@mailer.plane.so>"
|
|
||||||
EMAIL_USE_TLS="1"
|
|
||||||
EMAIL_USE_SSL="0"
|
|
||||||
|
|
||||||
# AWS Settings
|
# AWS Settings
|
||||||
AWS_REGION=""
|
AWS_REGION=""
|
||||||
AWS_ACCESS_KEY_ID="access-key"
|
AWS_ACCESS_KEY_ID="access-key"
|
||||||
@ -69,9 +25,6 @@ OPENAI_API_BASE="https://api.openai.com/v1" # change if using a custom endpoint
|
|||||||
OPENAI_API_KEY="sk-" # add your openai key here
|
OPENAI_API_KEY="sk-" # add your openai key here
|
||||||
GPT_ENGINE="gpt-3.5-turbo" # use "gpt-4" if you have access
|
GPT_ENGINE="gpt-3.5-turbo" # use "gpt-4" if you have access
|
||||||
|
|
||||||
# Github
|
|
||||||
GITHUB_CLIENT_SECRET="" # For fetching release notes
|
|
||||||
|
|
||||||
# Settings related to Docker
|
# Settings related to Docker
|
||||||
DOCKERIZED=1
|
DOCKERIZED=1
|
||||||
# set to 1 If using the pre-configured minio setup
|
# set to 1 If using the pre-configured minio setup
|
||||||
@ -80,10 +33,3 @@ USE_MINIO=1
|
|||||||
# Nginx Configuration
|
# Nginx Configuration
|
||||||
NGINX_PORT=80
|
NGINX_PORT=80
|
||||||
|
|
||||||
# Default Creds
|
|
||||||
DEFAULT_EMAIL="captain@plane.so"
|
|
||||||
DEFAULT_PASSWORD="password123"
|
|
||||||
|
|
||||||
# SignUps
|
|
||||||
ENABLE_SIGNUP="1"
|
|
||||||
# Auto generated and Required that will be generated from setup.sh
|
|
||||||
|
@ -33,14 +33,9 @@ jobs:
|
|||||||
deploy:
|
deploy:
|
||||||
- space/**
|
- 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
|
- name: Build Plane's Main App
|
||||||
if: steps.changed-files.outputs.web_any_changed == 'true'
|
if: steps.changed-files.outputs.web_any_changed == 'true'
|
||||||
run: |
|
run: |
|
||||||
mv ./.npmrc ./web
|
|
||||||
cd web
|
cd web
|
||||||
yarn
|
yarn
|
||||||
yarn build
|
yarn build
|
||||||
|
4
.github/workflows/Update_Docker_Images.yml
vendored
4
.github/workflows/Update_Docker_Images.yml
vendored
@ -22,10 +22,6 @@ jobs:
|
|||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
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
|
- name: Extract metadata (tags, labels) for Docker (Docker Hub) from Github Release
|
||||||
id: metaFrontend
|
id: metaFrontend
|
||||||
uses: docker/metadata-action@v4.3.0
|
uses: docker/metadata-action@v4.3.0
|
||||||
|
@ -8,8 +8,8 @@ Before submitting a new issue, please search the [issues](https://github.com/mak
|
|||||||
|
|
||||||
While we want to fix all the [issues](https://github.com/makeplane/plane/issues), before fixing a bug we need to be able to reproduce and confirm it. Please provide us with a minimal reproduction scenario using a repository or [Gist](https://gist.github.com/). Having a live, reproducible scenario gives us the information without asking questions back & forth with additional questions like:
|
While we want to fix all the [issues](https://github.com/makeplane/plane/issues), before fixing a bug we need to be able to reproduce and confirm it. Please provide us with a minimal reproduction scenario using a repository or [Gist](https://gist.github.com/). Having a live, reproducible scenario gives us the information without asking questions back & forth with additional questions like:
|
||||||
|
|
||||||
- 3rd-party libraries being used and their versions
|
- 3rd-party libraries being used and their versions
|
||||||
- a use-case that fails
|
- a use-case that fails
|
||||||
|
|
||||||
Without said minimal reproduction, we won't be able to investigate all [issues](https://github.com/makeplane/plane/issues), and the issue might not be resolved.
|
Without said minimal reproduction, we won't be able to investigate all [issues](https://github.com/makeplane/plane/issues), and the issue might not be resolved.
|
||||||
|
|
||||||
@ -19,10 +19,10 @@ You can open a new issue with this [issue form](https://github.com/makeplane/pla
|
|||||||
|
|
||||||
### Requirements
|
### Requirements
|
||||||
|
|
||||||
- Node.js version v16.18.0
|
- Node.js version v16.18.0
|
||||||
- Python version 3.8+
|
- Python version 3.8+
|
||||||
- Postgres version v14
|
- Postgres version v14
|
||||||
- Redis version v6.2.7
|
- Redis version v6.2.7
|
||||||
|
|
||||||
### Setup the project
|
### Setup the project
|
||||||
|
|
||||||
@ -30,6 +30,48 @@ The project is a monorepo, with backend api and frontend in a single repo.
|
|||||||
|
|
||||||
The backend is a django project which is kept inside apiserver
|
The backend is a django project which is kept inside apiserver
|
||||||
|
|
||||||
|
1. Clone the repo
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/makeplane/plane
|
||||||
|
cd plane
|
||||||
|
chmod +x setup.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Run setup.sh
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./setup.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Define `NEXT_PUBLIC_API_BASE_URL=http://localhost` in **web/.env** and **space/.env** file
|
||||||
|
|
||||||
|
```bash
|
||||||
|
echo "\nNEXT_PUBLIC_API_BASE_URL=http://localhost\n" >> ./web/.env
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
echo "\nNEXT_PUBLIC_API_BASE_URL=http://localhost\n" >> ./space/.env
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Run Docker compose up
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Install dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn install
|
||||||
|
```
|
||||||
|
|
||||||
|
6. Run the web app in development mode
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn dev
|
||||||
|
```
|
||||||
|
|
||||||
## Missing a Feature?
|
## Missing a Feature?
|
||||||
|
|
||||||
If a feature is missing, you can directly _request_ a new one [here](https://github.com/makeplane/plane/issues/new?assignees=&labels=feature&template=feature_request.yml&title=%F0%9F%9A%80+Feature%3A+). You also can do the same by choosing "🚀 Feature" when raising a [New Issue](https://github.com/makeplane/plane/issues/new/choose) on our GitHub Repository.
|
If a feature is missing, you can directly _request_ a new one [here](https://github.com/makeplane/plane/issues/new?assignees=&labels=feature&template=feature_request.yml&title=%F0%9F%9A%80+Feature%3A+). You also can do the same by choosing "🚀 Feature" when raising a [New Issue](https://github.com/makeplane/plane/issues/new/choose) on our GitHub Repository.
|
||||||
@ -39,8 +81,8 @@ If you would like to _implement_ it, an issue with your proposal must be submitt
|
|||||||
|
|
||||||
To ensure consistency throughout the source code, please keep these rules in mind as you are working:
|
To ensure consistency throughout the source code, please keep these rules in mind as you are working:
|
||||||
|
|
||||||
- All features or bug fixes must be tested by one or more specs (unit-tests).
|
- All features or bug fixes must be tested by one or more specs (unit-tests).
|
||||||
- We use [Eslint default rule guide](https://eslint.org/docs/rules/), with minor changes. An automated formatter is available using prettier.
|
- We use [Eslint default rule guide](https://eslint.org/docs/rules/), with minor changes. An automated formatter is available using prettier.
|
||||||
|
|
||||||
## Need help? Questions and suggestions
|
## Need help? Questions and suggestions
|
||||||
|
|
||||||
@ -48,11 +90,11 @@ Questions, suggestions, and thoughts are most welcome. We can also be reached in
|
|||||||
|
|
||||||
## Ways to contribute
|
## Ways to contribute
|
||||||
|
|
||||||
- Try Plane Cloud and the self hosting platform and give feedback
|
- Try Plane Cloud and the self hosting platform and give feedback
|
||||||
- Add new integrations
|
- Add new integrations
|
||||||
- Help with open [issues](https://github.com/makeplane/plane/issues) or [create your own](https://github.com/makeplane/plane/issues/new/choose)
|
- Help with open [issues](https://github.com/makeplane/plane/issues) or [create your own](https://github.com/makeplane/plane/issues/new/choose)
|
||||||
- Share your thoughts and suggestions with us
|
- Share your thoughts and suggestions with us
|
||||||
- Help create tutorials and blog posts
|
- Help create tutorials and blog posts
|
||||||
- Request a feature by submitting a proposal
|
- Request a feature by submitting a proposal
|
||||||
- Report a bug
|
- Report a bug
|
||||||
- **Improve documentation** - fix incomplete or missing [docs](https://docs.plane.so/), bad wording, examples or explanations.
|
- **Improve documentation** - fix incomplete or missing [docs](https://docs.plane.so/), bad wording, examples or explanations.
|
||||||
|
11
README.md
11
README.md
@ -59,17 +59,6 @@ chmod +x setup.sh
|
|||||||
|
|
||||||
> If running in a cloud env replace localhost with public facing IP address of the VM
|
> If running in a cloud env replace localhost with public facing IP address of the VM
|
||||||
|
|
||||||
- Setup Tiptap Pro
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
```
|
|
||||||
@tiptap-pro:registry=https://registry.tiptap.dev/
|
|
||||||
//registry.tiptap.dev/:_authToken=YOUR_REGISTRY_TOKEN
|
|
||||||
```
|
|
||||||
|
|
||||||
- Run Docker compose up
|
- Run Docker compose up
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
61
apiserver/.env.example
Normal file
61
apiserver/.env.example
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
# Backend
|
||||||
|
# Debug value for api server use it as 0 for production use
|
||||||
|
DEBUG=0
|
||||||
|
DJANGO_SETTINGS_MODULE="plane.settings.selfhosted"
|
||||||
|
|
||||||
|
# Error logs
|
||||||
|
SENTRY_DSN=""
|
||||||
|
|
||||||
|
# Database Settings
|
||||||
|
PGUSER="plane"
|
||||||
|
PGPASSWORD="plane"
|
||||||
|
PGHOST="plane-db"
|
||||||
|
PGDATABASE="plane"
|
||||||
|
DATABASE_URL=postgresql://${PGUSER}:${PGPASSWORD}@${PGHOST}/${PGDATABASE}
|
||||||
|
|
||||||
|
# Redis Settings
|
||||||
|
REDIS_HOST="plane-redis"
|
||||||
|
REDIS_PORT="6379"
|
||||||
|
REDIS_URL="redis://${REDIS_HOST}:6379/"
|
||||||
|
|
||||||
|
# Email Settings
|
||||||
|
EMAIL_HOST=""
|
||||||
|
EMAIL_HOST_USER=""
|
||||||
|
EMAIL_HOST_PASSWORD=""
|
||||||
|
EMAIL_PORT=587
|
||||||
|
EMAIL_FROM="Team Plane <team@mailer.plane.so>"
|
||||||
|
EMAIL_USE_TLS="1"
|
||||||
|
EMAIL_USE_SSL="0"
|
||||||
|
|
||||||
|
# AWS Settings
|
||||||
|
AWS_REGION=""
|
||||||
|
AWS_ACCESS_KEY_ID="access-key"
|
||||||
|
AWS_SECRET_ACCESS_KEY="secret-key"
|
||||||
|
AWS_S3_ENDPOINT_URL="http://plane-minio:9000"
|
||||||
|
# Changing this requires change in the nginx.conf for uploads if using minio setup
|
||||||
|
AWS_S3_BUCKET_NAME="uploads"
|
||||||
|
# Maximum file upload limit
|
||||||
|
FILE_SIZE_LIMIT=5242880
|
||||||
|
|
||||||
|
# GPT settings
|
||||||
|
OPENAI_API_BASE="https://api.openai.com/v1" # change if using a custom endpoint
|
||||||
|
OPENAI_API_KEY="sk-" # add your openai key here
|
||||||
|
GPT_ENGINE="gpt-3.5-turbo" # use "gpt-4" if you have access
|
||||||
|
|
||||||
|
# Github
|
||||||
|
GITHUB_CLIENT_SECRET="" # For fetching release notes
|
||||||
|
|
||||||
|
# Settings related to Docker
|
||||||
|
DOCKERIZED=1
|
||||||
|
# set to 1 If using the pre-configured minio setup
|
||||||
|
USE_MINIO=1
|
||||||
|
|
||||||
|
# Nginx Configuration
|
||||||
|
NGINX_PORT=80
|
||||||
|
|
||||||
|
# Default Creds
|
||||||
|
DEFAULT_EMAIL="captain@plane.so"
|
||||||
|
DEFAULT_PASSWORD="password123"
|
||||||
|
|
||||||
|
# SignUps
|
||||||
|
ENABLE_SIGNUP="1"
|
@ -58,8 +58,17 @@ class WorkspaceEntityPermission(BasePermission):
|
|||||||
if request.user.is_anonymous:
|
if request.user.is_anonymous:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
## Safe Methods -> Handle the filtering logic in queryset
|
||||||
|
if request.method in SAFE_METHODS:
|
||||||
|
return WorkspaceMember.objects.filter(
|
||||||
|
workspace__slug=view.workspace_slug,
|
||||||
|
member=request.user,
|
||||||
|
).exists()
|
||||||
|
|
||||||
return WorkspaceMember.objects.filter(
|
return WorkspaceMember.objects.filter(
|
||||||
member=request.user, workspace__slug=view.workspace_slug
|
member=request.user,
|
||||||
|
workspace__slug=view.workspace_slug,
|
||||||
|
role__in=[Owner, Admin],
|
||||||
).exists()
|
).exists()
|
||||||
|
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ from .project import (
|
|||||||
ProjectPublicMemberSerializer
|
ProjectPublicMemberSerializer
|
||||||
)
|
)
|
||||||
from .state import StateSerializer, StateLiteSerializer
|
from .state import StateSerializer, StateLiteSerializer
|
||||||
from .view import IssueViewSerializer, IssueViewFavoriteSerializer
|
from .view import GlobalViewSerializer, IssueViewSerializer, IssueViewFavoriteSerializer
|
||||||
from .cycle import CycleSerializer, CycleIssueSerializer, CycleFavoriteSerializer, CycleWriteSerializer
|
from .cycle import CycleSerializer, CycleIssueSerializer, CycleFavoriteSerializer, CycleWriteSerializer
|
||||||
from .asset import FileAssetSerializer
|
from .asset import FileAssetSerializer
|
||||||
from .issue import (
|
from .issue import (
|
||||||
@ -31,8 +31,6 @@ from .issue import (
|
|||||||
IssueActivitySerializer,
|
IssueActivitySerializer,
|
||||||
IssueCommentSerializer,
|
IssueCommentSerializer,
|
||||||
IssuePropertySerializer,
|
IssuePropertySerializer,
|
||||||
BlockerIssueSerializer,
|
|
||||||
BlockedIssueSerializer,
|
|
||||||
IssueAssigneeSerializer,
|
IssueAssigneeSerializer,
|
||||||
LabelSerializer,
|
LabelSerializer,
|
||||||
IssueSerializer,
|
IssueSerializer,
|
||||||
@ -45,6 +43,8 @@ from .issue import (
|
|||||||
IssueReactionSerializer,
|
IssueReactionSerializer,
|
||||||
CommentReactionSerializer,
|
CommentReactionSerializer,
|
||||||
IssueVoteSerializer,
|
IssueVoteSerializer,
|
||||||
|
IssueRelationSerializer,
|
||||||
|
RelatedIssueSerializer,
|
||||||
IssuePublicSerializer,
|
IssuePublicSerializer,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -34,7 +34,6 @@ class CycleSerializer(BaseSerializer):
|
|||||||
unstarted_issues = serializers.IntegerField(read_only=True)
|
unstarted_issues = serializers.IntegerField(read_only=True)
|
||||||
backlog_issues = serializers.IntegerField(read_only=True)
|
backlog_issues = serializers.IntegerField(read_only=True)
|
||||||
assignees = serializers.SerializerMethodField(read_only=True)
|
assignees = serializers.SerializerMethodField(read_only=True)
|
||||||
labels = serializers.SerializerMethodField(read_only=True)
|
|
||||||
total_estimates = serializers.IntegerField(read_only=True)
|
total_estimates = serializers.IntegerField(read_only=True)
|
||||||
completed_estimates = serializers.IntegerField(read_only=True)
|
completed_estimates = serializers.IntegerField(read_only=True)
|
||||||
started_estimates = serializers.IntegerField(read_only=True)
|
started_estimates = serializers.IntegerField(read_only=True)
|
||||||
@ -50,11 +49,10 @@ class CycleSerializer(BaseSerializer):
|
|||||||
members = [
|
members = [
|
||||||
{
|
{
|
||||||
"avatar": assignee.avatar,
|
"avatar": assignee.avatar,
|
||||||
"first_name": assignee.first_name,
|
|
||||||
"display_name": assignee.display_name,
|
"display_name": assignee.display_name,
|
||||||
"id": assignee.id,
|
"id": assignee.id,
|
||||||
}
|
}
|
||||||
for issue_cycle in obj.issue_cycle.all()
|
for issue_cycle in obj.issue_cycle.prefetch_related("issue__assignees").all()
|
||||||
for assignee in issue_cycle.issue.assignees.all()
|
for assignee in issue_cycle.issue.assignees.all()
|
||||||
]
|
]
|
||||||
# Use a set comprehension to return only the unique objects
|
# Use a set comprehension to return only the unique objects
|
||||||
@ -64,24 +62,6 @@ class CycleSerializer(BaseSerializer):
|
|||||||
unique_list = [dict(item) for item in unique_objects]
|
unique_list = [dict(item) for item in unique_objects]
|
||||||
|
|
||||||
return unique_list
|
return unique_list
|
||||||
|
|
||||||
def get_labels(self, obj):
|
|
||||||
labels = [
|
|
||||||
{
|
|
||||||
"name": label.name,
|
|
||||||
"color": label.color,
|
|
||||||
"id": label.id,
|
|
||||||
}
|
|
||||||
for issue_cycle in obj.issue_cycle.all()
|
|
||||||
for label in issue_cycle.issue.labels.all()
|
|
||||||
]
|
|
||||||
# Use a set comprehension to return only the unique objects
|
|
||||||
unique_objects = {frozenset(item.items()) for item in labels}
|
|
||||||
|
|
||||||
# Convert the set back to a list of dictionaries
|
|
||||||
unique_list = [dict(item) for item in unique_objects]
|
|
||||||
|
|
||||||
return unique_list
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Cycle
|
model = Cycle
|
||||||
|
@ -17,12 +17,10 @@ from plane.db.models import (
|
|||||||
IssueActivity,
|
IssueActivity,
|
||||||
IssueComment,
|
IssueComment,
|
||||||
IssueProperty,
|
IssueProperty,
|
||||||
IssueBlocker,
|
|
||||||
IssueAssignee,
|
IssueAssignee,
|
||||||
IssueSubscriber,
|
IssueSubscriber,
|
||||||
IssueLabel,
|
IssueLabel,
|
||||||
Label,
|
Label,
|
||||||
IssueBlocker,
|
|
||||||
CycleIssue,
|
CycleIssue,
|
||||||
Cycle,
|
Cycle,
|
||||||
Module,
|
Module,
|
||||||
@ -32,6 +30,7 @@ from plane.db.models import (
|
|||||||
IssueReaction,
|
IssueReaction,
|
||||||
CommentReaction,
|
CommentReaction,
|
||||||
IssueVote,
|
IssueVote,
|
||||||
|
IssueRelation,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -50,6 +49,7 @@ class IssueFlatSerializer(BaseSerializer):
|
|||||||
"target_date",
|
"target_date",
|
||||||
"sequence_id",
|
"sequence_id",
|
||||||
"sort_order",
|
"sort_order",
|
||||||
|
"is_draft",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -81,25 +81,12 @@ class IssueCreateSerializer(BaseSerializer):
|
|||||||
required=False,
|
required=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
# List of issues that are blocking this issue
|
|
||||||
blockers_list = serializers.ListField(
|
|
||||||
child=serializers.PrimaryKeyRelatedField(queryset=Issue.objects.all()),
|
|
||||||
write_only=True,
|
|
||||||
required=False,
|
|
||||||
)
|
|
||||||
labels_list = serializers.ListField(
|
labels_list = serializers.ListField(
|
||||||
child=serializers.PrimaryKeyRelatedField(queryset=Label.objects.all()),
|
child=serializers.PrimaryKeyRelatedField(queryset=Label.objects.all()),
|
||||||
write_only=True,
|
write_only=True,
|
||||||
required=False,
|
required=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
# List of issues that are blocked by this issue
|
|
||||||
blocks_list = serializers.ListField(
|
|
||||||
child=serializers.PrimaryKeyRelatedField(queryset=Issue.objects.all()),
|
|
||||||
write_only=True,
|
|
||||||
required=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Issue
|
model = Issue
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
@ -122,10 +109,8 @@ class IssueCreateSerializer(BaseSerializer):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
blockers = validated_data.pop("blockers_list", None)
|
|
||||||
assignees = validated_data.pop("assignees_list", None)
|
assignees = validated_data.pop("assignees_list", None)
|
||||||
labels = validated_data.pop("labels_list", None)
|
labels = validated_data.pop("labels_list", None)
|
||||||
blocks = validated_data.pop("blocks_list", None)
|
|
||||||
|
|
||||||
project_id = self.context["project_id"]
|
project_id = self.context["project_id"]
|
||||||
workspace_id = self.context["workspace_id"]
|
workspace_id = self.context["workspace_id"]
|
||||||
@ -137,22 +122,6 @@ class IssueCreateSerializer(BaseSerializer):
|
|||||||
created_by_id = issue.created_by_id
|
created_by_id = issue.created_by_id
|
||||||
updated_by_id = issue.updated_by_id
|
updated_by_id = issue.updated_by_id
|
||||||
|
|
||||||
if blockers is not None and len(blockers):
|
|
||||||
IssueBlocker.objects.bulk_create(
|
|
||||||
[
|
|
||||||
IssueBlocker(
|
|
||||||
block=issue,
|
|
||||||
blocked_by=blocker,
|
|
||||||
project_id=project_id,
|
|
||||||
workspace_id=workspace_id,
|
|
||||||
created_by_id=created_by_id,
|
|
||||||
updated_by_id=updated_by_id,
|
|
||||||
)
|
|
||||||
for blocker in blockers
|
|
||||||
],
|
|
||||||
batch_size=10,
|
|
||||||
)
|
|
||||||
|
|
||||||
if assignees is not None and len(assignees):
|
if assignees is not None and len(assignees):
|
||||||
IssueAssignee.objects.bulk_create(
|
IssueAssignee.objects.bulk_create(
|
||||||
[
|
[
|
||||||
@ -196,29 +165,11 @@ class IssueCreateSerializer(BaseSerializer):
|
|||||||
batch_size=10,
|
batch_size=10,
|
||||||
)
|
)
|
||||||
|
|
||||||
if blocks is not None and len(blocks):
|
|
||||||
IssueBlocker.objects.bulk_create(
|
|
||||||
[
|
|
||||||
IssueBlocker(
|
|
||||||
block=block,
|
|
||||||
blocked_by=issue,
|
|
||||||
project_id=project_id,
|
|
||||||
workspace_id=workspace_id,
|
|
||||||
created_by_id=created_by_id,
|
|
||||||
updated_by_id=updated_by_id,
|
|
||||||
)
|
|
||||||
for block in blocks
|
|
||||||
],
|
|
||||||
batch_size=10,
|
|
||||||
)
|
|
||||||
|
|
||||||
return issue
|
return issue
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
def update(self, instance, validated_data):
|
||||||
blockers = validated_data.pop("blockers_list", None)
|
|
||||||
assignees = validated_data.pop("assignees_list", None)
|
assignees = validated_data.pop("assignees_list", None)
|
||||||
labels = validated_data.pop("labels_list", None)
|
labels = validated_data.pop("labels_list", None)
|
||||||
blocks = validated_data.pop("blocks_list", None)
|
|
||||||
|
|
||||||
# Related models
|
# Related models
|
||||||
project_id = instance.project_id
|
project_id = instance.project_id
|
||||||
@ -226,23 +177,6 @@ class IssueCreateSerializer(BaseSerializer):
|
|||||||
created_by_id = instance.created_by_id
|
created_by_id = instance.created_by_id
|
||||||
updated_by_id = instance.updated_by_id
|
updated_by_id = instance.updated_by_id
|
||||||
|
|
||||||
if blockers is not None:
|
|
||||||
IssueBlocker.objects.filter(block=instance).delete()
|
|
||||||
IssueBlocker.objects.bulk_create(
|
|
||||||
[
|
|
||||||
IssueBlocker(
|
|
||||||
block=instance,
|
|
||||||
blocked_by=blocker,
|
|
||||||
project_id=project_id,
|
|
||||||
workspace_id=workspace_id,
|
|
||||||
created_by_id=created_by_id,
|
|
||||||
updated_by_id=updated_by_id,
|
|
||||||
)
|
|
||||||
for blocker in blockers
|
|
||||||
],
|
|
||||||
batch_size=10,
|
|
||||||
)
|
|
||||||
|
|
||||||
if assignees is not None:
|
if assignees is not None:
|
||||||
IssueAssignee.objects.filter(issue=instance).delete()
|
IssueAssignee.objects.filter(issue=instance).delete()
|
||||||
IssueAssignee.objects.bulk_create(
|
IssueAssignee.objects.bulk_create(
|
||||||
@ -277,23 +211,6 @@ class IssueCreateSerializer(BaseSerializer):
|
|||||||
batch_size=10,
|
batch_size=10,
|
||||||
)
|
)
|
||||||
|
|
||||||
if blocks is not None:
|
|
||||||
IssueBlocker.objects.filter(blocked_by=instance).delete()
|
|
||||||
IssueBlocker.objects.bulk_create(
|
|
||||||
[
|
|
||||||
IssueBlocker(
|
|
||||||
block=block,
|
|
||||||
blocked_by=instance,
|
|
||||||
project_id=project_id,
|
|
||||||
workspace_id=workspace_id,
|
|
||||||
created_by_id=created_by_id,
|
|
||||||
updated_by_id=updated_by_id,
|
|
||||||
)
|
|
||||||
for block in blocks
|
|
||||||
],
|
|
||||||
batch_size=10,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Time updation occues even when other related models are updated
|
# Time updation occues even when other related models are updated
|
||||||
instance.updated_at = timezone.now()
|
instance.updated_at = timezone.now()
|
||||||
return super().update(instance, validated_data)
|
return super().update(instance, validated_data)
|
||||||
@ -375,32 +292,39 @@ class IssueLabelSerializer(BaseSerializer):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class BlockedIssueSerializer(BaseSerializer):
|
class IssueRelationSerializer(BaseSerializer):
|
||||||
blocked_issue_detail = IssueProjectLiteSerializer(source="block", read_only=True)
|
issue_detail = IssueProjectLiteSerializer(read_only=True, source="related_issue")
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = IssueBlocker
|
model = IssueRelation
|
||||||
fields = [
|
fields = [
|
||||||
"blocked_issue_detail",
|
"issue_detail",
|
||||||
"blocked_by",
|
"relation_type",
|
||||||
"block",
|
"related_issue",
|
||||||
|
"issue",
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
read_only_fields = [
|
||||||
|
"workspace",
|
||||||
|
"project",
|
||||||
]
|
]
|
||||||
read_only_fields = fields
|
|
||||||
|
|
||||||
|
class RelatedIssueSerializer(BaseSerializer):
|
||||||
class BlockerIssueSerializer(BaseSerializer):
|
issue_detail = IssueProjectLiteSerializer(read_only=True, source="issue")
|
||||||
blocker_issue_detail = IssueProjectLiteSerializer(
|
|
||||||
source="blocked_by", read_only=True
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = IssueBlocker
|
model = IssueRelation
|
||||||
fields = [
|
fields = [
|
||||||
"blocker_issue_detail",
|
"issue_detail",
|
||||||
"blocked_by",
|
"relation_type",
|
||||||
"block",
|
"related_issue",
|
||||||
|
"issue",
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
read_only_fields = [
|
||||||
|
"workspace",
|
||||||
|
"project",
|
||||||
]
|
]
|
||||||
read_only_fields = fields
|
|
||||||
|
|
||||||
|
|
||||||
class IssueAssigneeSerializer(BaseSerializer):
|
class IssueAssigneeSerializer(BaseSerializer):
|
||||||
@ -617,10 +541,8 @@ class IssueSerializer(BaseSerializer):
|
|||||||
parent_detail = IssueStateFlatSerializer(read_only=True, source="parent")
|
parent_detail = IssueStateFlatSerializer(read_only=True, source="parent")
|
||||||
label_details = LabelSerializer(read_only=True, source="labels", many=True)
|
label_details = LabelSerializer(read_only=True, source="labels", many=True)
|
||||||
assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True)
|
assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True)
|
||||||
# List of issues blocked by this issue
|
related_issues = IssueRelationSerializer(read_only=True, source="issue_relation", many=True)
|
||||||
blocked_issues = BlockedIssueSerializer(read_only=True, many=True)
|
issue_relations = RelatedIssueSerializer(read_only=True, source="issue_related", many=True)
|
||||||
# List of issues that block this issue
|
|
||||||
blocker_issues = BlockerIssueSerializer(read_only=True, many=True)
|
|
||||||
issue_cycle = IssueCycleDetailSerializer(read_only=True)
|
issue_cycle = IssueCycleDetailSerializer(read_only=True)
|
||||||
issue_module = IssueModuleDetailSerializer(read_only=True)
|
issue_module = IssueModuleDetailSerializer(read_only=True)
|
||||||
issue_link = IssueLinkSerializer(read_only=True, many=True)
|
issue_link = IssueLinkSerializer(read_only=True, many=True)
|
||||||
|
@ -5,10 +5,39 @@ from rest_framework import serializers
|
|||||||
from .base import BaseSerializer
|
from .base import BaseSerializer
|
||||||
from .workspace import WorkspaceLiteSerializer
|
from .workspace import WorkspaceLiteSerializer
|
||||||
from .project import ProjectLiteSerializer
|
from .project import ProjectLiteSerializer
|
||||||
from plane.db.models import IssueView, IssueViewFavorite
|
from plane.db.models import GlobalView, IssueView, IssueViewFavorite
|
||||||
from plane.utils.issue_filters import issue_filters
|
from plane.utils.issue_filters import issue_filters
|
||||||
|
|
||||||
|
|
||||||
|
class GlobalViewSerializer(BaseSerializer):
|
||||||
|
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = GlobalView
|
||||||
|
fields = "__all__"
|
||||||
|
read_only_fields = [
|
||||||
|
"workspace",
|
||||||
|
"query",
|
||||||
|
]
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
query_params = validated_data.get("query_data", {})
|
||||||
|
if bool(query_params):
|
||||||
|
validated_data["query"] = issue_filters(query_params, "POST")
|
||||||
|
else:
|
||||||
|
validated_data["query"] = dict()
|
||||||
|
return GlobalView.objects.create(**validated_data)
|
||||||
|
|
||||||
|
def update(self, instance, validated_data):
|
||||||
|
query_params = validated_data.get("query_data", {})
|
||||||
|
if bool(query_params):
|
||||||
|
validated_data["query"] = issue_filters(query_params, "POST")
|
||||||
|
else:
|
||||||
|
validated_data["query"] = dict()
|
||||||
|
validated_data["query"] = issue_filters(query_params, "PATCH")
|
||||||
|
return super().update(instance, validated_data)
|
||||||
|
|
||||||
|
|
||||||
class IssueViewSerializer(BaseSerializer):
|
class IssueViewSerializer(BaseSerializer):
|
||||||
is_favorite = serializers.BooleanField(read_only=True)
|
is_favorite = serializers.BooleanField(read_only=True)
|
||||||
project_detail = ProjectLiteSerializer(source="project", read_only=True)
|
project_detail = ProjectLiteSerializer(source="project", read_only=True)
|
||||||
|
@ -90,7 +90,9 @@ from plane.api.views import (
|
|||||||
IssueSubscriberViewSet,
|
IssueSubscriberViewSet,
|
||||||
IssueCommentPublicViewSet,
|
IssueCommentPublicViewSet,
|
||||||
IssueReactionViewSet,
|
IssueReactionViewSet,
|
||||||
|
IssueRelationViewSet,
|
||||||
CommentReactionViewSet,
|
CommentReactionViewSet,
|
||||||
|
IssueDraftViewSet,
|
||||||
## End Issues
|
## End Issues
|
||||||
# States
|
# States
|
||||||
StateViewSet,
|
StateViewSet,
|
||||||
@ -100,6 +102,8 @@ from plane.api.views import (
|
|||||||
BulkEstimatePointEndpoint,
|
BulkEstimatePointEndpoint,
|
||||||
## End Estimates
|
## End Estimates
|
||||||
# Views
|
# Views
|
||||||
|
GlobalViewViewSet,
|
||||||
|
GlobalViewIssuesViewSet,
|
||||||
IssueViewViewSet,
|
IssueViewViewSet,
|
||||||
ViewIssuesEndpoint,
|
ViewIssuesEndpoint,
|
||||||
IssueViewFavoriteViewSet,
|
IssueViewFavoriteViewSet,
|
||||||
@ -182,7 +186,6 @@ from plane.api.views import (
|
|||||||
## Exporter
|
## Exporter
|
||||||
ExportIssuesEndpoint,
|
ExportIssuesEndpoint,
|
||||||
## End Exporter
|
## End Exporter
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -239,7 +242,11 @@ urlpatterns = [
|
|||||||
UpdateUserTourCompletedEndpoint.as_view(),
|
UpdateUserTourCompletedEndpoint.as_view(),
|
||||||
name="user-tour",
|
name="user-tour",
|
||||||
),
|
),
|
||||||
path("users/workspaces/<str:slug>/activities/", UserActivityEndpoint.as_view(), name="user-activities"),
|
path(
|
||||||
|
"users/workspaces/<str:slug>/activities/",
|
||||||
|
UserActivityEndpoint.as_view(),
|
||||||
|
name="user-activities",
|
||||||
|
),
|
||||||
# user workspaces
|
# user workspaces
|
||||||
path(
|
path(
|
||||||
"users/me/workspaces/",
|
"users/me/workspaces/",
|
||||||
@ -647,6 +654,37 @@ urlpatterns = [
|
|||||||
ViewIssuesEndpoint.as_view(),
|
ViewIssuesEndpoint.as_view(),
|
||||||
name="project-view-issues",
|
name="project-view-issues",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/views/",
|
||||||
|
GlobalViewViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "list",
|
||||||
|
"post": "create",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="global-view",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/views/<uuid:pk>/",
|
||||||
|
GlobalViewViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "retrieve",
|
||||||
|
"put": "update",
|
||||||
|
"patch": "partial_update",
|
||||||
|
"delete": "destroy",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="global-view",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/issues/",
|
||||||
|
GlobalViewIssuesViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "list",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="global-view-issues",
|
||||||
|
),
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-views/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-views/",
|
||||||
IssueViewFavoriteViewSet.as_view(
|
IssueViewFavoriteViewSet.as_view(
|
||||||
@ -765,11 +803,6 @@ urlpatterns = [
|
|||||||
),
|
),
|
||||||
name="project-issue",
|
name="project-issue",
|
||||||
),
|
),
|
||||||
path(
|
|
||||||
"workspaces/<str:slug>/issues/",
|
|
||||||
WorkSpaceIssuesEndpoint.as_view(),
|
|
||||||
name="workspace-issue",
|
|
||||||
),
|
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-labels/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-labels/",
|
||||||
LabelViewSet.as_view(
|
LabelViewSet.as_view(
|
||||||
@ -1010,6 +1043,49 @@ urlpatterns = [
|
|||||||
name="project-issue-archive",
|
name="project-issue-archive",
|
||||||
),
|
),
|
||||||
## End Issue Archives
|
## End Issue Archives
|
||||||
|
## Issue Relation
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-relation/",
|
||||||
|
IssueRelationViewSet.as_view(
|
||||||
|
{
|
||||||
|
"post": "create",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="issue-relation",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-relation/<uuid:pk>/",
|
||||||
|
IssueRelationViewSet.as_view(
|
||||||
|
{
|
||||||
|
"delete": "destroy",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="issue-relation",
|
||||||
|
),
|
||||||
|
## End Issue Relation
|
||||||
|
## Issue Drafts
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-drafts/",
|
||||||
|
IssueDraftViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "list",
|
||||||
|
"post": "create",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-issue-draft",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-drafts/<uuid:pk>/",
|
||||||
|
IssueDraftViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "retrieve",
|
||||||
|
"patch": "partial_update",
|
||||||
|
"delete": "destroy",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-issue-draft",
|
||||||
|
),
|
||||||
|
## End Issue Drafts
|
||||||
## File Assets
|
## File Assets
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/file-assets/",
|
"workspaces/<str:slug>/file-assets/",
|
||||||
|
@ -56,7 +56,7 @@ from .workspace import (
|
|||||||
LeaveWorkspaceEndpoint,
|
LeaveWorkspaceEndpoint,
|
||||||
)
|
)
|
||||||
from .state import StateViewSet
|
from .state import StateViewSet
|
||||||
from .view import IssueViewViewSet, ViewIssuesEndpoint, IssueViewFavoriteViewSet
|
from .view import GlobalViewViewSet, GlobalViewIssuesViewSet, IssueViewViewSet, ViewIssuesEndpoint, IssueViewFavoriteViewSet
|
||||||
from .cycle import (
|
from .cycle import (
|
||||||
CycleViewSet,
|
CycleViewSet,
|
||||||
CycleIssueViewSet,
|
CycleIssueViewSet,
|
||||||
@ -86,8 +86,10 @@ from .issue import (
|
|||||||
IssueReactionPublicViewSet,
|
IssueReactionPublicViewSet,
|
||||||
CommentReactionPublicViewSet,
|
CommentReactionPublicViewSet,
|
||||||
IssueVotePublicViewSet,
|
IssueVotePublicViewSet,
|
||||||
|
IssueRelationViewSet,
|
||||||
IssueRetrievePublicEndpoint,
|
IssueRetrievePublicEndpoint,
|
||||||
ProjectIssuesPublicEndpoint,
|
ProjectIssuesPublicEndpoint,
|
||||||
|
IssueDraftViewSet,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .auth_extended import (
|
from .auth_extended import (
|
||||||
@ -167,6 +169,4 @@ from .analytic import (
|
|||||||
|
|
||||||
from .notification import NotificationViewSet, UnreadNotificationEndpoint, MarkAllReadNotificationViewSet
|
from .notification import NotificationViewSet, UnreadNotificationEndpoint, MarkAllReadNotificationViewSet
|
||||||
|
|
||||||
from .exporter import (
|
from .exporter import ExportIssuesEndpoint
|
||||||
ExportIssuesEndpoint,
|
|
||||||
)
|
|
@ -80,6 +80,7 @@ class CycleViewSet(BaseViewSet):
|
|||||||
issue_id=str(self.kwargs.get("pk", None)),
|
issue_id=str(self.kwargs.get("pk", None)),
|
||||||
project_id=str(self.kwargs.get("project_id", None)),
|
project_id=str(self.kwargs.get("project_id", None)),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
|
|
||||||
return super().perform_destroy(instance)
|
return super().perform_destroy(instance)
|
||||||
@ -101,48 +102,84 @@ class CycleViewSet(BaseViewSet):
|
|||||||
.select_related("workspace")
|
.select_related("workspace")
|
||||||
.select_related("owned_by")
|
.select_related("owned_by")
|
||||||
.annotate(is_favorite=Exists(subquery))
|
.annotate(is_favorite=Exists(subquery))
|
||||||
.annotate(total_issues=Count("issue_cycle"))
|
.annotate(
|
||||||
|
total_issues=Count(
|
||||||
|
"issue_cycle",
|
||||||
|
filter=Q(
|
||||||
|
issue_cycle__issue__archived_at__isnull=True,
|
||||||
|
issue_cycle__issue__is_draft=False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
completed_issues=Count(
|
completed_issues=Count(
|
||||||
"issue_cycle__issue__state__group",
|
"issue_cycle__issue__state__group",
|
||||||
filter=Q(issue_cycle__issue__state__group="completed"),
|
filter=Q(
|
||||||
|
issue_cycle__issue__state__group="completed",
|
||||||
|
issue_cycle__issue__archived_at__isnull=True,
|
||||||
|
issue_cycle__issue__is_draft=False,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
cancelled_issues=Count(
|
cancelled_issues=Count(
|
||||||
"issue_cycle__issue__state__group",
|
"issue_cycle__issue__state__group",
|
||||||
filter=Q(issue_cycle__issue__state__group="cancelled"),
|
filter=Q(
|
||||||
|
issue_cycle__issue__state__group="cancelled",
|
||||||
|
issue_cycle__issue__archived_at__isnull=True,
|
||||||
|
issue_cycle__issue__is_draft=False,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
started_issues=Count(
|
started_issues=Count(
|
||||||
"issue_cycle__issue__state__group",
|
"issue_cycle__issue__state__group",
|
||||||
filter=Q(issue_cycle__issue__state__group="started"),
|
filter=Q(
|
||||||
|
issue_cycle__issue__state__group="started",
|
||||||
|
issue_cycle__issue__archived_at__isnull=True,
|
||||||
|
issue_cycle__issue__is_draft=False,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
unstarted_issues=Count(
|
unstarted_issues=Count(
|
||||||
"issue_cycle__issue__state__group",
|
"issue_cycle__issue__state__group",
|
||||||
filter=Q(issue_cycle__issue__state__group="unstarted"),
|
filter=Q(
|
||||||
|
issue_cycle__issue__state__group="unstarted",
|
||||||
|
issue_cycle__issue__archived_at__isnull=True,
|
||||||
|
issue_cycle__issue__is_draft=False,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
backlog_issues=Count(
|
backlog_issues=Count(
|
||||||
"issue_cycle__issue__state__group",
|
"issue_cycle__issue__state__group",
|
||||||
filter=Q(issue_cycle__issue__state__group="backlog"),
|
filter=Q(
|
||||||
|
issue_cycle__issue__state__group="backlog",
|
||||||
|
issue_cycle__issue__archived_at__isnull=True,
|
||||||
|
issue_cycle__issue__is_draft=False,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.annotate(total_estimates=Sum("issue_cycle__issue__estimate_point"))
|
.annotate(total_estimates=Sum("issue_cycle__issue__estimate_point"))
|
||||||
.annotate(
|
.annotate(
|
||||||
completed_estimates=Sum(
|
completed_estimates=Sum(
|
||||||
"issue_cycle__issue__estimate_point",
|
"issue_cycle__issue__estimate_point",
|
||||||
filter=Q(issue_cycle__issue__state__group="completed"),
|
filter=Q(
|
||||||
|
issue_cycle__issue__state__group="completed",
|
||||||
|
issue_cycle__issue__archived_at__isnull=True,
|
||||||
|
issue_cycle__issue__is_draft=False,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
started_estimates=Sum(
|
started_estimates=Sum(
|
||||||
"issue_cycle__issue__estimate_point",
|
"issue_cycle__issue__estimate_point",
|
||||||
filter=Q(issue_cycle__issue__state__group="started"),
|
filter=Q(
|
||||||
|
issue_cycle__issue__state__group="started",
|
||||||
|
issue_cycle__issue__archived_at__isnull=True,
|
||||||
|
issue_cycle__issue__is_draft=False,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.prefetch_related(
|
.prefetch_related(
|
||||||
@ -195,17 +232,30 @@ class CycleViewSet(BaseViewSet):
|
|||||||
.annotate(assignee_id=F("assignees__id"))
|
.annotate(assignee_id=F("assignees__id"))
|
||||||
.annotate(avatar=F("assignees__avatar"))
|
.annotate(avatar=F("assignees__avatar"))
|
||||||
.values("display_name", "assignee_id", "avatar")
|
.values("display_name", "assignee_id", "avatar")
|
||||||
.annotate(total_issues=Count("assignee_id"))
|
.annotate(
|
||||||
|
total_issues=Count(
|
||||||
|
"assignee_id",
|
||||||
|
filter=Q(archived_at__isnull=True, is_draft=False),
|
||||||
|
),
|
||||||
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
completed_issues=Count(
|
completed_issues=Count(
|
||||||
"assignee_id",
|
"assignee_id",
|
||||||
filter=Q(completed_at__isnull=False),
|
filter=Q(
|
||||||
|
completed_at__isnull=False,
|
||||||
|
archived_at__isnull=True,
|
||||||
|
is_draft=False,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
pending_issues=Count(
|
pending_issues=Count(
|
||||||
"assignee_id",
|
"assignee_id",
|
||||||
filter=Q(completed_at__isnull=True),
|
filter=Q(
|
||||||
|
completed_at__isnull=True,
|
||||||
|
archived_at__isnull=True,
|
||||||
|
is_draft=False,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.order_by("display_name")
|
.order_by("display_name")
|
||||||
@ -221,17 +271,30 @@ class CycleViewSet(BaseViewSet):
|
|||||||
.annotate(color=F("labels__color"))
|
.annotate(color=F("labels__color"))
|
||||||
.annotate(label_id=F("labels__id"))
|
.annotate(label_id=F("labels__id"))
|
||||||
.values("label_name", "color", "label_id")
|
.values("label_name", "color", "label_id")
|
||||||
.annotate(total_issues=Count("label_id"))
|
.annotate(
|
||||||
|
total_issues=Count(
|
||||||
|
"label_id",
|
||||||
|
filter=Q(archived_at__isnull=True, is_draft=False),
|
||||||
|
)
|
||||||
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
completed_issues=Count(
|
completed_issues=Count(
|
||||||
"label_id",
|
"label_id",
|
||||||
filter=Q(completed_at__isnull=False),
|
filter=Q(
|
||||||
|
completed_at__isnull=False,
|
||||||
|
archived_at__isnull=True,
|
||||||
|
is_draft=False,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
pending_issues=Count(
|
pending_issues=Count(
|
||||||
"label_id",
|
"label_id",
|
||||||
filter=Q(completed_at__isnull=True),
|
filter=Q(
|
||||||
|
completed_at__isnull=True,
|
||||||
|
archived_at__isnull=True,
|
||||||
|
is_draft=False,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.order_by("label_name")
|
.order_by("label_name")
|
||||||
@ -384,17 +447,30 @@ class CycleViewSet(BaseViewSet):
|
|||||||
.values(
|
.values(
|
||||||
"first_name", "last_name", "assignee_id", "avatar", "display_name"
|
"first_name", "last_name", "assignee_id", "avatar", "display_name"
|
||||||
)
|
)
|
||||||
.annotate(total_issues=Count("assignee_id"))
|
.annotate(
|
||||||
|
total_issues=Count(
|
||||||
|
"assignee_id",
|
||||||
|
filter=Q(archived_at__isnull=True, is_draft=False),
|
||||||
|
),
|
||||||
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
completed_issues=Count(
|
completed_issues=Count(
|
||||||
"assignee_id",
|
"assignee_id",
|
||||||
filter=Q(completed_at__isnull=False),
|
filter=Q(
|
||||||
|
completed_at__isnull=False,
|
||||||
|
archived_at__isnull=True,
|
||||||
|
is_draft=False,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
pending_issues=Count(
|
pending_issues=Count(
|
||||||
"assignee_id",
|
"assignee_id",
|
||||||
filter=Q(completed_at__isnull=True),
|
filter=Q(
|
||||||
|
completed_at__isnull=True,
|
||||||
|
archived_at__isnull=True,
|
||||||
|
is_draft=False,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.order_by("first_name", "last_name")
|
.order_by("first_name", "last_name")
|
||||||
@ -411,17 +487,30 @@ class CycleViewSet(BaseViewSet):
|
|||||||
.annotate(color=F("labels__color"))
|
.annotate(color=F("labels__color"))
|
||||||
.annotate(label_id=F("labels__id"))
|
.annotate(label_id=F("labels__id"))
|
||||||
.values("label_name", "color", "label_id")
|
.values("label_name", "color", "label_id")
|
||||||
.annotate(total_issues=Count("label_id"))
|
.annotate(
|
||||||
|
total_issues=Count(
|
||||||
|
"label_id",
|
||||||
|
filter=Q(archived_at__isnull=True, is_draft=False),
|
||||||
|
),
|
||||||
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
completed_issues=Count(
|
completed_issues=Count(
|
||||||
"label_id",
|
"label_id",
|
||||||
filter=Q(completed_at__isnull=False),
|
filter=Q(
|
||||||
|
completed_at__isnull=False,
|
||||||
|
archived_at__isnull=True,
|
||||||
|
is_draft=False,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
pending_issues=Count(
|
pending_issues=Count(
|
||||||
"label_id",
|
"label_id",
|
||||||
filter=Q(completed_at__isnull=True),
|
filter=Q(
|
||||||
|
completed_at__isnull=True,
|
||||||
|
archived_at__isnull=True,
|
||||||
|
is_draft=False,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.order_by("label_name")
|
.order_by("label_name")
|
||||||
@ -487,6 +576,7 @@ class CycleIssueViewSet(BaseViewSet):
|
|||||||
issue_id=str(self.kwargs.get("pk", None)),
|
issue_id=str(self.kwargs.get("pk", None)),
|
||||||
project_id=str(self.kwargs.get("project_id", None)),
|
project_id=str(self.kwargs.get("project_id", None)),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
return super().perform_destroy(instance)
|
return super().perform_destroy(instance)
|
||||||
|
|
||||||
@ -517,6 +607,7 @@ class CycleIssueViewSet(BaseViewSet):
|
|||||||
try:
|
try:
|
||||||
order_by = request.GET.get("order_by", "created_at")
|
order_by = request.GET.get("order_by", "created_at")
|
||||||
group_by = request.GET.get("group_by", False)
|
group_by = request.GET.get("group_by", False)
|
||||||
|
sub_group_by = request.GET.get("sub_group_by", False)
|
||||||
filters = issue_filters(request.query_params, "GET")
|
filters = issue_filters(request.query_params, "GET")
|
||||||
issues = (
|
issues = (
|
||||||
Issue.issue_objects.filter(issue_cycle__cycle_id=cycle_id)
|
Issue.issue_objects.filter(issue_cycle__cycle_id=cycle_id)
|
||||||
@ -555,9 +646,15 @@ class CycleIssueViewSet(BaseViewSet):
|
|||||||
|
|
||||||
issues_data = IssueStateSerializer(issues, many=True).data
|
issues_data = IssueStateSerializer(issues, many=True).data
|
||||||
|
|
||||||
|
if sub_group_by and sub_group_by == group_by:
|
||||||
|
return Response(
|
||||||
|
{"error": "Group by and sub group by cannot be same"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
if group_by:
|
if group_by:
|
||||||
return Response(
|
return Response(
|
||||||
group_results(issues_data, group_by),
|
group_results(issues_data, group_by, sub_group_by),
|
||||||
status=status.HTTP_200_OK,
|
status=status.HTTP_200_OK,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -655,6 +752,7 @@ class CycleIssueViewSet(BaseViewSet):
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
|
|
||||||
# Return all Cycle Issues
|
# Return all Cycle Issues
|
||||||
|
@ -384,7 +384,7 @@ class BulkImportIssuesEndpoint(BaseAPIView):
|
|||||||
sort_order=largest_sort_order,
|
sort_order=largest_sort_order,
|
||||||
start_date=issue_data.get("start_date", None),
|
start_date=issue_data.get("start_date", None),
|
||||||
target_date=issue_data.get("target_date", None),
|
target_date=issue_data.get("target_date", None),
|
||||||
priority=issue_data.get("priority", None),
|
priority=issue_data.get("priority", "none"),
|
||||||
created_by=request.user,
|
created_by=request.user,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -173,12 +173,12 @@ class InboxIssueViewSet(BaseViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Check for valid priority
|
# Check for valid priority
|
||||||
if not request.data.get("issue", {}).get("priority", None) in [
|
if not request.data.get("issue", {}).get("priority", "none") in [
|
||||||
"low",
|
"low",
|
||||||
"medium",
|
"medium",
|
||||||
"high",
|
"high",
|
||||||
"urgent",
|
"urgent",
|
||||||
None,
|
"none",
|
||||||
]:
|
]:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST
|
{"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST
|
||||||
@ -213,6 +213,7 @@ class InboxIssueViewSet(BaseViewSet):
|
|||||||
issue_id=str(issue.id),
|
issue_id=str(issue.id),
|
||||||
project_id=str(project_id),
|
project_id=str(project_id),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
# create an inbox issue
|
# create an inbox issue
|
||||||
InboxIssue.objects.create(
|
InboxIssue.objects.create(
|
||||||
@ -277,6 +278,7 @@ class InboxIssueViewSet(BaseViewSet):
|
|||||||
IssueSerializer(current_instance).data,
|
IssueSerializer(current_instance).data,
|
||||||
cls=DjangoJSONEncoder,
|
cls=DjangoJSONEncoder,
|
||||||
),
|
),
|
||||||
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
issue_serializer.save()
|
issue_serializer.save()
|
||||||
else:
|
else:
|
||||||
@ -368,6 +370,11 @@ class InboxIssueViewSet(BaseViewSet):
|
|||||||
if project_member.role <= 10 and str(inbox_issue.created_by_id) != str(request.user.id):
|
if project_member.role <= 10 and str(inbox_issue.created_by_id) != str(request.user.id):
|
||||||
return Response({"error": "You cannot delete inbox issue"}, status=status.HTTP_400_BAD_REQUEST)
|
return Response({"error": "You cannot delete inbox issue"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
# Check the issue status
|
||||||
|
if inbox_issue.status in [-2, -1, 0, 2]:
|
||||||
|
# Delete the issue also
|
||||||
|
Issue.objects.filter(workspace__slug=slug, project_id=project_id, pk=inbox_issue.issue_id).delete()
|
||||||
|
|
||||||
inbox_issue.delete()
|
inbox_issue.delete()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
except InboxIssue.DoesNotExist:
|
except InboxIssue.DoesNotExist:
|
||||||
@ -478,12 +485,12 @@ class InboxIssuePublicViewSet(BaseViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Check for valid priority
|
# Check for valid priority
|
||||||
if not request.data.get("issue", {}).get("priority", None) in [
|
if not request.data.get("issue", {}).get("priority", "none") in [
|
||||||
"low",
|
"low",
|
||||||
"medium",
|
"medium",
|
||||||
"high",
|
"high",
|
||||||
"urgent",
|
"urgent",
|
||||||
None,
|
"none",
|
||||||
]:
|
]:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST
|
{"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST
|
||||||
@ -518,6 +525,7 @@ class InboxIssuePublicViewSet(BaseViewSet):
|
|||||||
issue_id=str(issue.id),
|
issue_id=str(issue.id),
|
||||||
project_id=str(project_id),
|
project_id=str(project_id),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
# create an inbox issue
|
# create an inbox issue
|
||||||
InboxIssue.objects.create(
|
InboxIssue.objects.create(
|
||||||
@ -582,6 +590,7 @@ class InboxIssuePublicViewSet(BaseViewSet):
|
|||||||
IssueSerializer(current_instance).data,
|
IssueSerializer(current_instance).data,
|
||||||
cls=DjangoJSONEncoder,
|
cls=DjangoJSONEncoder,
|
||||||
),
|
),
|
||||||
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
issue_serializer.save()
|
issue_serializer.save()
|
||||||
return Response(issue_serializer.data, status=status.HTTP_200_OK)
|
return Response(issue_serializer.data, status=status.HTTP_200_OK)
|
||||||
|
@ -4,6 +4,7 @@ import random
|
|||||||
from itertools import chain
|
from itertools import chain
|
||||||
|
|
||||||
# Django imports
|
# Django imports
|
||||||
|
from django.utils import timezone
|
||||||
from django.db.models import (
|
from django.db.models import (
|
||||||
Prefetch,
|
Prefetch,
|
||||||
OuterRef,
|
OuterRef,
|
||||||
@ -23,7 +24,7 @@ from django.core.serializers.json import DjangoJSONEncoder
|
|||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
from django.views.decorators.gzip import gzip_page
|
from django.views.decorators.gzip import gzip_page
|
||||||
from django.db import IntegrityError
|
from django.db import IntegrityError
|
||||||
from django.conf import settings
|
from django.db import IntegrityError
|
||||||
|
|
||||||
# Third Party imports
|
# Third Party imports
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
@ -51,10 +52,11 @@ from plane.api.serializers import (
|
|||||||
IssueReactionSerializer,
|
IssueReactionSerializer,
|
||||||
CommentReactionSerializer,
|
CommentReactionSerializer,
|
||||||
IssueVoteSerializer,
|
IssueVoteSerializer,
|
||||||
|
IssueRelationSerializer,
|
||||||
|
RelatedIssueSerializer,
|
||||||
IssuePublicSerializer,
|
IssuePublicSerializer,
|
||||||
)
|
)
|
||||||
from plane.api.permissions import (
|
from plane.api.permissions import (
|
||||||
WorkspaceEntityPermission,
|
|
||||||
ProjectEntityPermission,
|
ProjectEntityPermission,
|
||||||
WorkSpaceAdminPermission,
|
WorkSpaceAdminPermission,
|
||||||
ProjectMemberPermission,
|
ProjectMemberPermission,
|
||||||
@ -76,6 +78,7 @@ from plane.db.models import (
|
|||||||
CommentReaction,
|
CommentReaction,
|
||||||
ProjectDeployBoard,
|
ProjectDeployBoard,
|
||||||
IssueVote,
|
IssueVote,
|
||||||
|
IssueRelation,
|
||||||
ProjectPublicMember,
|
ProjectPublicMember,
|
||||||
)
|
)
|
||||||
from plane.bgtasks.issue_activites_task import issue_activity
|
from plane.bgtasks.issue_activites_task import issue_activity
|
||||||
@ -125,6 +128,7 @@ class IssueViewSet(BaseViewSet):
|
|||||||
current_instance=json.dumps(
|
current_instance=json.dumps(
|
||||||
IssueSerializer(current_instance).data, cls=DjangoJSONEncoder
|
IssueSerializer(current_instance).data, cls=DjangoJSONEncoder
|
||||||
),
|
),
|
||||||
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
|
|
||||||
return super().perform_update(serializer)
|
return super().perform_update(serializer)
|
||||||
@ -145,6 +149,7 @@ class IssueViewSet(BaseViewSet):
|
|||||||
current_instance=json.dumps(
|
current_instance=json.dumps(
|
||||||
IssueSerializer(current_instance).data, cls=DjangoJSONEncoder
|
IssueSerializer(current_instance).data, cls=DjangoJSONEncoder
|
||||||
),
|
),
|
||||||
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
return super().perform_destroy(instance)
|
return super().perform_destroy(instance)
|
||||||
|
|
||||||
@ -178,7 +183,7 @@ class IssueViewSet(BaseViewSet):
|
|||||||
filters = issue_filters(request.query_params, "GET")
|
filters = issue_filters(request.query_params, "GET")
|
||||||
|
|
||||||
# Custom ordering for priority and state
|
# Custom ordering for priority and state
|
||||||
priority_order = ["urgent", "high", "medium", "low", None]
|
priority_order = ["urgent", "high", "medium", "low", "none"]
|
||||||
state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
|
state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
|
||||||
|
|
||||||
order_by_param = request.GET.get("order_by", "-created_at")
|
order_by_param = request.GET.get("order_by", "-created_at")
|
||||||
@ -266,9 +271,16 @@ class IssueViewSet(BaseViewSet):
|
|||||||
|
|
||||||
## Grouping the results
|
## Grouping the results
|
||||||
group_by = request.GET.get("group_by", False)
|
group_by = request.GET.get("group_by", False)
|
||||||
|
sub_group_by = request.GET.get("sub_group_by", False)
|
||||||
|
if sub_group_by and sub_group_by == group_by:
|
||||||
|
return Response(
|
||||||
|
{"error": "Group by and sub group by cannot be same"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
if group_by:
|
if group_by:
|
||||||
return Response(
|
return Response(
|
||||||
group_results(issues, group_by), status=status.HTTP_200_OK
|
group_results(issues, group_by, sub_group_by), status=status.HTTP_200_OK
|
||||||
)
|
)
|
||||||
|
|
||||||
return Response(issues, status=status.HTTP_200_OK)
|
return Response(issues, status=status.HTTP_200_OK)
|
||||||
@ -304,6 +316,7 @@ class IssueViewSet(BaseViewSet):
|
|||||||
issue_id=str(serializer.data.get("id", None)),
|
issue_id=str(serializer.data.get("id", None)),
|
||||||
project_id=str(project_id),
|
project_id=str(project_id),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
@ -315,7 +328,12 @@ class IssueViewSet(BaseViewSet):
|
|||||||
|
|
||||||
def retrieve(self, request, slug, project_id, pk=None):
|
def retrieve(self, request, slug, project_id, pk=None):
|
||||||
try:
|
try:
|
||||||
issue = Issue.issue_objects.get(
|
issue = 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")
|
||||||
|
).get(
|
||||||
workspace__slug=slug, project_id=project_id, pk=pk
|
workspace__slug=slug, project_id=project_id, pk=pk
|
||||||
)
|
)
|
||||||
return Response(IssueSerializer(issue).data, status=status.HTTP_200_OK)
|
return Response(IssueSerializer(issue).data, status=status.HTTP_200_OK)
|
||||||
@ -331,7 +349,7 @@ class UserWorkSpaceIssues(BaseAPIView):
|
|||||||
try:
|
try:
|
||||||
filters = issue_filters(request.query_params, "GET")
|
filters = issue_filters(request.query_params, "GET")
|
||||||
# Custom ordering for priority and state
|
# Custom ordering for priority and state
|
||||||
priority_order = ["urgent", "high", "medium", "low", None]
|
priority_order = ["urgent", "high", "medium", "low", "none"]
|
||||||
state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
|
state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
|
||||||
|
|
||||||
order_by_param = request.GET.get("order_by", "-created_at")
|
order_by_param = request.GET.get("order_by", "-created_at")
|
||||||
@ -443,9 +461,16 @@ class UserWorkSpaceIssues(BaseAPIView):
|
|||||||
|
|
||||||
## Grouping the results
|
## Grouping the results
|
||||||
group_by = request.GET.get("group_by", False)
|
group_by = request.GET.get("group_by", False)
|
||||||
|
sub_group_by = request.GET.get("sub_group_by", False)
|
||||||
|
if sub_group_by and sub_group_by == group_by:
|
||||||
|
return Response(
|
||||||
|
{"error": "Group by and sub group by cannot be same"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
if group_by:
|
if group_by:
|
||||||
return Response(
|
return Response(
|
||||||
group_results(issues, group_by), status=status.HTTP_200_OK
|
group_results(issues, group_by, sub_group_by), status=status.HTTP_200_OK
|
||||||
)
|
)
|
||||||
|
|
||||||
return Response(issues, status=status.HTTP_200_OK)
|
return Response(issues, status=status.HTTP_200_OK)
|
||||||
@ -491,7 +516,7 @@ class IssueActivityEndpoint(BaseAPIView):
|
|||||||
issue_activities = (
|
issue_activities = (
|
||||||
IssueActivity.objects.filter(issue_id=issue_id)
|
IssueActivity.objects.filter(issue_id=issue_id)
|
||||||
.filter(
|
.filter(
|
||||||
~Q(field__in=["comment", "vote", "reaction"]),
|
~Q(field__in=["comment", "vote", "reaction", "draft"]),
|
||||||
project__project_projectmember__member=self.request.user,
|
project__project_projectmember__member=self.request.user,
|
||||||
)
|
)
|
||||||
.select_related("actor", "workspace", "issue", "project")
|
.select_related("actor", "workspace", "issue", "project")
|
||||||
@ -550,6 +575,7 @@ class IssueCommentViewSet(BaseViewSet):
|
|||||||
issue_id=str(self.kwargs.get("issue_id")),
|
issue_id=str(self.kwargs.get("issue_id")),
|
||||||
project_id=str(self.kwargs.get("project_id")),
|
project_id=str(self.kwargs.get("project_id")),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
|
|
||||||
def perform_update(self, serializer):
|
def perform_update(self, serializer):
|
||||||
@ -568,6 +594,7 @@ class IssueCommentViewSet(BaseViewSet):
|
|||||||
IssueCommentSerializer(current_instance).data,
|
IssueCommentSerializer(current_instance).data,
|
||||||
cls=DjangoJSONEncoder,
|
cls=DjangoJSONEncoder,
|
||||||
),
|
),
|
||||||
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
|
|
||||||
return super().perform_update(serializer)
|
return super().perform_update(serializer)
|
||||||
@ -589,6 +616,7 @@ class IssueCommentViewSet(BaseViewSet):
|
|||||||
IssueCommentSerializer(current_instance).data,
|
IssueCommentSerializer(current_instance).data,
|
||||||
cls=DjangoJSONEncoder,
|
cls=DjangoJSONEncoder,
|
||||||
),
|
),
|
||||||
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
return super().perform_destroy(instance)
|
return super().perform_destroy(instance)
|
||||||
|
|
||||||
@ -684,10 +712,18 @@ class LabelViewSet(BaseViewSet):
|
|||||||
ProjectMemberPermission,
|
ProjectMemberPermission,
|
||||||
]
|
]
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
def create(self, request, slug, project_id):
|
||||||
serializer.save(
|
try:
|
||||||
project_id=self.kwargs.get("project_id"),
|
serializer = LabelSerializer(data=request.data)
|
||||||
)
|
if serializer.is_valid():
|
||||||
|
serializer.save(project_id=project_id)
|
||||||
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
except IntegrityError:
|
||||||
|
return Response({"error": "Label with the same name already exists in the project"}, 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)
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.filter_queryset(
|
return self.filter_queryset(
|
||||||
@ -872,6 +908,7 @@ class IssueLinkViewSet(BaseViewSet):
|
|||||||
issue_id=str(self.kwargs.get("issue_id")),
|
issue_id=str(self.kwargs.get("issue_id")),
|
||||||
project_id=str(self.kwargs.get("project_id")),
|
project_id=str(self.kwargs.get("project_id")),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
|
|
||||||
def perform_update(self, serializer):
|
def perform_update(self, serializer):
|
||||||
@ -890,6 +927,7 @@ class IssueLinkViewSet(BaseViewSet):
|
|||||||
IssueLinkSerializer(current_instance).data,
|
IssueLinkSerializer(current_instance).data,
|
||||||
cls=DjangoJSONEncoder,
|
cls=DjangoJSONEncoder,
|
||||||
),
|
),
|
||||||
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
|
|
||||||
return super().perform_update(serializer)
|
return super().perform_update(serializer)
|
||||||
@ -911,6 +949,7 @@ class IssueLinkViewSet(BaseViewSet):
|
|||||||
IssueLinkSerializer(current_instance).data,
|
IssueLinkSerializer(current_instance).data,
|
||||||
cls=DjangoJSONEncoder,
|
cls=DjangoJSONEncoder,
|
||||||
),
|
),
|
||||||
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
return super().perform_destroy(instance)
|
return super().perform_destroy(instance)
|
||||||
|
|
||||||
@ -989,6 +1028,7 @@ class IssueAttachmentEndpoint(BaseAPIView):
|
|||||||
serializer.data,
|
serializer.data,
|
||||||
cls=DjangoJSONEncoder,
|
cls=DjangoJSONEncoder,
|
||||||
),
|
),
|
||||||
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
@ -1011,6 +1051,7 @@ class IssueAttachmentEndpoint(BaseAPIView):
|
|||||||
issue_id=str(self.kwargs.get("issue_id", None)),
|
issue_id=str(self.kwargs.get("issue_id", None)),
|
||||||
project_id=str(self.kwargs.get("project_id", None)),
|
project_id=str(self.kwargs.get("project_id", None)),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
|
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
@ -1068,7 +1109,7 @@ class IssueArchiveViewSet(BaseViewSet):
|
|||||||
show_sub_issues = request.GET.get("show_sub_issues", "true")
|
show_sub_issues = request.GET.get("show_sub_issues", "true")
|
||||||
|
|
||||||
# Custom ordering for priority and state
|
# Custom ordering for priority and state
|
||||||
priority_order = ["urgent", "high", "medium", "low", None]
|
priority_order = ["urgent", "high", "medium", "low", "none"]
|
||||||
state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
|
state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
|
||||||
|
|
||||||
order_by_param = request.GET.get("order_by", "-created_at")
|
order_by_param = request.GET.get("order_by", "-created_at")
|
||||||
@ -1213,6 +1254,7 @@ class IssueArchiveViewSet(BaseViewSet):
|
|||||||
issue_id=str(issue.id),
|
issue_id=str(issue.id),
|
||||||
project_id=str(project_id),
|
project_id=str(project_id),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
|
|
||||||
return Response(IssueSerializer(issue).data, status=status.HTTP_200_OK)
|
return Response(IssueSerializer(issue).data, status=status.HTTP_200_OK)
|
||||||
@ -1417,6 +1459,7 @@ class IssueReactionViewSet(BaseViewSet):
|
|||||||
issue_id=str(self.kwargs.get("issue_id", None)),
|
issue_id=str(self.kwargs.get("issue_id", None)),
|
||||||
project_id=str(self.kwargs.get("project_id", None)),
|
project_id=str(self.kwargs.get("project_id", None)),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
|
|
||||||
def destroy(self, request, slug, project_id, issue_id, reaction_code):
|
def destroy(self, request, slug, project_id, issue_id, reaction_code):
|
||||||
@ -1440,6 +1483,7 @@ class IssueReactionViewSet(BaseViewSet):
|
|||||||
"identifier": str(issue_reaction.id),
|
"identifier": str(issue_reaction.id),
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
issue_reaction.delete()
|
issue_reaction.delete()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
@ -1488,6 +1532,7 @@ class CommentReactionViewSet(BaseViewSet):
|
|||||||
issue_id=None,
|
issue_id=None,
|
||||||
project_id=str(self.kwargs.get("project_id", None)),
|
project_id=str(self.kwargs.get("project_id", None)),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
|
|
||||||
def destroy(self, request, slug, project_id, comment_id, reaction_code):
|
def destroy(self, request, slug, project_id, comment_id, reaction_code):
|
||||||
@ -1512,6 +1557,7 @@ class CommentReactionViewSet(BaseViewSet):
|
|||||||
"comment_id": str(comment_id),
|
"comment_id": str(comment_id),
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
comment_reaction.delete()
|
comment_reaction.delete()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
@ -1608,6 +1654,7 @@ class IssueCommentPublicViewSet(BaseViewSet):
|
|||||||
issue_id=str(issue_id),
|
issue_id=str(issue_id),
|
||||||
project_id=str(project_id),
|
project_id=str(project_id),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
if not ProjectMember.objects.filter(
|
if not ProjectMember.objects.filter(
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
@ -1657,6 +1704,7 @@ class IssueCommentPublicViewSet(BaseViewSet):
|
|||||||
IssueCommentSerializer(comment).data,
|
IssueCommentSerializer(comment).data,
|
||||||
cls=DjangoJSONEncoder,
|
cls=DjangoJSONEncoder,
|
||||||
),
|
),
|
||||||
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
@ -1690,6 +1738,7 @@ class IssueCommentPublicViewSet(BaseViewSet):
|
|||||||
IssueCommentSerializer(comment).data,
|
IssueCommentSerializer(comment).data,
|
||||||
cls=DjangoJSONEncoder,
|
cls=DjangoJSONEncoder,
|
||||||
),
|
),
|
||||||
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
comment.delete()
|
comment.delete()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
@ -1764,6 +1813,7 @@ class IssueReactionPublicViewSet(BaseViewSet):
|
|||||||
issue_id=str(self.kwargs.get("issue_id", None)),
|
issue_id=str(self.kwargs.get("issue_id", None)),
|
||||||
project_id=str(self.kwargs.get("project_id", None)),
|
project_id=str(self.kwargs.get("project_id", None)),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
@ -1808,6 +1858,7 @@ class IssueReactionPublicViewSet(BaseViewSet):
|
|||||||
"identifier": str(issue_reaction.id),
|
"identifier": str(issue_reaction.id),
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
issue_reaction.delete()
|
issue_reaction.delete()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
@ -1881,6 +1932,7 @@ class CommentReactionPublicViewSet(BaseViewSet):
|
|||||||
issue_id=None,
|
issue_id=None,
|
||||||
project_id=str(self.kwargs.get("project_id", None)),
|
project_id=str(self.kwargs.get("project_id", None)),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
@ -1932,6 +1984,7 @@ class CommentReactionPublicViewSet(BaseViewSet):
|
|||||||
"comment_id": str(comment_id),
|
"comment_id": str(comment_id),
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
comment_reaction.delete()
|
comment_reaction.delete()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
@ -1995,6 +2048,7 @@ class IssueVotePublicViewSet(BaseViewSet):
|
|||||||
issue_id=str(self.kwargs.get("issue_id", None)),
|
issue_id=str(self.kwargs.get("issue_id", None)),
|
||||||
project_id=str(self.kwargs.get("project_id", None)),
|
project_id=str(self.kwargs.get("project_id", None)),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
serializer = IssueVoteSerializer(issue_vote)
|
serializer = IssueVoteSerializer(issue_vote)
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
@ -2029,6 +2083,7 @@ class IssueVotePublicViewSet(BaseViewSet):
|
|||||||
"identifier": str(issue_vote.id),
|
"identifier": str(issue_vote.id),
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
issue_vote.delete()
|
issue_vote.delete()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
@ -2040,6 +2095,109 @@ class IssueVotePublicViewSet(BaseViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class IssueRelationViewSet(BaseViewSet):
|
||||||
|
serializer_class = IssueRelationSerializer
|
||||||
|
model = IssueRelation
|
||||||
|
permission_classes = [
|
||||||
|
ProjectEntityPermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
def perform_destroy(self, instance):
|
||||||
|
current_instance = (
|
||||||
|
self.get_queryset().filter(pk=self.kwargs.get("pk", None)).first()
|
||||||
|
)
|
||||||
|
if current_instance is not None:
|
||||||
|
issue_activity.delay(
|
||||||
|
type="issue_relation.activity.deleted",
|
||||||
|
requested_data=json.dumps({"related_list": 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(
|
||||||
|
IssueRelationSerializer(current_instance).data,
|
||||||
|
cls=DjangoJSONEncoder,
|
||||||
|
),
|
||||||
|
epoch=int(timezone.now().timestamp())
|
||||||
|
)
|
||||||
|
return super().perform_destroy(instance)
|
||||||
|
|
||||||
|
def create(self, request, slug, project_id, issue_id):
|
||||||
|
try:
|
||||||
|
related_list = request.data.get("related_list", [])
|
||||||
|
relation = request.data.get("relation", None)
|
||||||
|
project = Project.objects.get(pk=project_id)
|
||||||
|
|
||||||
|
issue_relation = IssueRelation.objects.bulk_create(
|
||||||
|
[
|
||||||
|
IssueRelation(
|
||||||
|
issue_id=related_issue["issue"],
|
||||||
|
related_issue_id=related_issue["related_issue"],
|
||||||
|
relation_type=related_issue["relation_type"],
|
||||||
|
project_id=project_id,
|
||||||
|
workspace_id=project.workspace_id,
|
||||||
|
created_by=request.user,
|
||||||
|
updated_by=request.user,
|
||||||
|
)
|
||||||
|
for related_issue in related_list
|
||||||
|
],
|
||||||
|
batch_size=10,
|
||||||
|
ignore_conflicts=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
issue_activity.delay(
|
||||||
|
type="issue_relation.activity.created",
|
||||||
|
requested_data=json.dumps(request.data, cls=DjangoJSONEncoder),
|
||||||
|
actor_id=str(request.user.id),
|
||||||
|
issue_id=str(issue_id),
|
||||||
|
project_id=str(project_id),
|
||||||
|
current_instance=None,
|
||||||
|
epoch=int(timezone.now().timestamp())
|
||||||
|
)
|
||||||
|
|
||||||
|
if relation == "blocking":
|
||||||
|
return Response(
|
||||||
|
RelatedIssueSerializer(issue_relation, many=True).data,
|
||||||
|
status=status.HTTP_201_CREATED,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return Response(
|
||||||
|
IssueRelationSerializer(issue_relation, many=True).data,
|
||||||
|
status=status.HTTP_201_CREATED,
|
||||||
|
)
|
||||||
|
except IntegrityError as e:
|
||||||
|
if "already exists" in str(e):
|
||||||
|
return Response(
|
||||||
|
{"name": "The issue is already taken"},
|
||||||
|
status=status.HTTP_410_GONE,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
capture_exception(e)
|
||||||
|
return Response(
|
||||||
|
{"error": "Something went wrong please try again later"},
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.filter_queryset(
|
||||||
|
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"))
|
||||||
|
.filter(project__project_projectmember__member=self.request.user)
|
||||||
|
.select_related("project")
|
||||||
|
.select_related("workspace")
|
||||||
|
.select_related("issue")
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class IssueRetrievePublicEndpoint(BaseAPIView):
|
class IssueRetrievePublicEndpoint(BaseAPIView):
|
||||||
permission_classes = [
|
permission_classes = [
|
||||||
AllowAny,
|
AllowAny,
|
||||||
@ -2078,7 +2236,7 @@ class ProjectIssuesPublicEndpoint(BaseAPIView):
|
|||||||
filters = issue_filters(request.query_params, "GET")
|
filters = issue_filters(request.query_params, "GET")
|
||||||
|
|
||||||
# Custom ordering for priority and state
|
# Custom ordering for priority and state
|
||||||
priority_order = ["urgent", "high", "medium", "low", None]
|
priority_order = ["urgent", "high", "medium", "low", "none"]
|
||||||
state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
|
state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
|
||||||
|
|
||||||
order_by_param = request.GET.get("order_by", "-created_at")
|
order_by_param = request.GET.get("order_by", "-created_at")
|
||||||
@ -2240,3 +2398,256 @@ class ProjectIssuesPublicEndpoint(BaseAPIView):
|
|||||||
{"error": "Something went wrong please try again later"},
|
{"error": "Something went wrong please try again later"},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class IssueDraftViewSet(BaseViewSet):
|
||||||
|
permission_classes = [
|
||||||
|
ProjectEntityPermission,
|
||||||
|
]
|
||||||
|
serializer_class = IssueFlatSerializer
|
||||||
|
model = Issue
|
||||||
|
|
||||||
|
|
||||||
|
def perform_destroy(self, instance):
|
||||||
|
current_instance = (
|
||||||
|
self.get_queryset().filter(pk=self.kwargs.get("pk", None)).first()
|
||||||
|
)
|
||||||
|
if current_instance is not None:
|
||||||
|
issue_activity.delay(
|
||||||
|
type="issue_draft.activity.deleted",
|
||||||
|
requested_data=json.dumps(
|
||||||
|
{"issue_id": str(self.kwargs.get("pk", None))}
|
||||||
|
),
|
||||||
|
actor_id=str(self.request.user.id),
|
||||||
|
issue_id=str(self.kwargs.get("pk", None)),
|
||||||
|
project_id=str(self.kwargs.get("project_id", None)),
|
||||||
|
current_instance=json.dumps(
|
||||||
|
IssueSerializer(current_instance).data, cls=DjangoJSONEncoder
|
||||||
|
),
|
||||||
|
epoch=int(timezone.now().timestamp())
|
||||||
|
)
|
||||||
|
return super().perform_destroy(instance)
|
||||||
|
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return (
|
||||||
|
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=self.kwargs.get("project_id"))
|
||||||
|
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||||
|
.filter(is_draft=True)
|
||||||
|
.select_related("project")
|
||||||
|
.select_related("workspace")
|
||||||
|
.select_related("state")
|
||||||
|
.select_related("parent")
|
||||||
|
.prefetch_related("assignees")
|
||||||
|
.prefetch_related("labels")
|
||||||
|
.prefetch_related(
|
||||||
|
Prefetch(
|
||||||
|
"issue_reactions",
|
||||||
|
queryset=IssueReaction.objects.select_related("actor"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@method_decorator(gzip_page)
|
||||||
|
def list(self, request, slug, project_id):
|
||||||
|
try:
|
||||||
|
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 = (
|
||||||
|
self.get_queryset()
|
||||||
|
.filter(**filters)
|
||||||
|
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||||
|
.annotate(module_id=F("issue_module__module_id"))
|
||||||
|
.annotate(
|
||||||
|
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||||
|
.order_by()
|
||||||
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
|
.values("count")
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
attachment_count=IssueAttachment.objects.filter(
|
||||||
|
issue=OuterRef("id")
|
||||||
|
)
|
||||||
|
.order_by()
|
||||||
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
|
.values("count")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Priority Ordering
|
||||||
|
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
|
||||||
|
|
||||||
|
## Grouping the results
|
||||||
|
group_by = request.GET.get("group_by", False)
|
||||||
|
if group_by:
|
||||||
|
return Response(
|
||||||
|
group_results(issues, group_by), status=status.HTTP_200_OK
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response(issues, 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,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create(self, request, slug, project_id):
|
||||||
|
try:
|
||||||
|
project = Project.objects.get(pk=project_id)
|
||||||
|
|
||||||
|
serializer = IssueCreateSerializer(
|
||||||
|
data=request.data,
|
||||||
|
context={
|
||||||
|
"project_id": project_id,
|
||||||
|
"workspace_id": project.workspace_id,
|
||||||
|
"default_assignee_id": project.default_assignee_id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if serializer.is_valid():
|
||||||
|
serializer.save(is_draft=True)
|
||||||
|
|
||||||
|
# Track the issue
|
||||||
|
issue_activity.delay(
|
||||||
|
type="issue_draft.activity.created",
|
||||||
|
requested_data=json.dumps(self.request.data, cls=DjangoJSONEncoder),
|
||||||
|
actor_id=str(request.user.id),
|
||||||
|
issue_id=str(serializer.data.get("id", None)),
|
||||||
|
project_id=str(project_id),
|
||||||
|
current_instance=None,
|
||||||
|
epoch=int(timezone.now().timestamp())
|
||||||
|
)
|
||||||
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
except Project.DoesNotExist:
|
||||||
|
return Response(
|
||||||
|
{"error": "Project was not found"}, status=status.HTTP_404_NOT_FOUND
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def partial_update(self, request, slug, project_id, pk):
|
||||||
|
try:
|
||||||
|
issue = Issue.objects.get(
|
||||||
|
workspace__slug=slug, project_id=project_id, pk=pk
|
||||||
|
)
|
||||||
|
serializer = IssueSerializer(
|
||||||
|
issue, data=request.data, partial=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if serializer.is_valid():
|
||||||
|
if(request.data.get("is_draft") is not None and not request.data.get("is_draft")):
|
||||||
|
serializer.save(created_at=timezone.now(), updated_at=timezone.now())
|
||||||
|
else:
|
||||||
|
serializer.save()
|
||||||
|
issue_activity.delay(
|
||||||
|
type="issue_draft.activity.updated",
|
||||||
|
requested_data=json.dumps(request.data, cls=DjangoJSONEncoder),
|
||||||
|
actor_id=str(self.request.user.id),
|
||||||
|
issue_id=str(self.kwargs.get("pk", None)),
|
||||||
|
project_id=str(self.kwargs.get("project_id", None)),
|
||||||
|
current_instance=json.dumps(
|
||||||
|
IssueSerializer(issue).data,
|
||||||
|
cls=DjangoJSONEncoder,
|
||||||
|
),
|
||||||
|
epoch=int(timezone.now().timestamp())
|
||||||
|
)
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
except Issue.DoesNotExist:
|
||||||
|
return Response(
|
||||||
|
{"error": "Issue 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,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def retrieve(self, request, slug, project_id, pk=None):
|
||||||
|
try:
|
||||||
|
issue = Issue.objects.get(
|
||||||
|
workspace__slug=slug, project_id=project_id, pk=pk, is_draft=True
|
||||||
|
)
|
||||||
|
return Response(IssueSerializer(issue).data, status=status.HTTP_200_OK)
|
||||||
|
except Issue.DoesNotExist:
|
||||||
|
return Response(
|
||||||
|
{"error": "Issue Does not exist"}, status=status.HTTP_404_NOT_FOUND
|
||||||
|
)
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
# Django Imports
|
# Django Imports
|
||||||
|
from django.utils import timezone
|
||||||
from django.db import IntegrityError
|
from django.db import IntegrityError
|
||||||
from django.db.models import Prefetch, F, OuterRef, Func, Exists, Count, Q
|
from django.db.models import Prefetch, F, OuterRef, Func, Exists, Count, Q
|
||||||
from django.core import serializers
|
from django.core import serializers
|
||||||
@ -39,6 +40,7 @@ from plane.utils.grouper import group_results
|
|||||||
from plane.utils.issue_filters import issue_filters
|
from plane.utils.issue_filters import issue_filters
|
||||||
from plane.utils.analytics_plot import burndown_plot
|
from plane.utils.analytics_plot import burndown_plot
|
||||||
|
|
||||||
|
|
||||||
class ModuleViewSet(BaseViewSet):
|
class ModuleViewSet(BaseViewSet):
|
||||||
model = Module
|
model = Module
|
||||||
permission_classes = [
|
permission_classes = [
|
||||||
@ -77,35 +79,63 @@ class ModuleViewSet(BaseViewSet):
|
|||||||
queryset=ModuleLink.objects.select_related("module", "created_by"),
|
queryset=ModuleLink.objects.select_related("module", "created_by"),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.annotate(total_issues=Count("issue_module"))
|
.annotate(
|
||||||
|
total_issues=Count(
|
||||||
|
"issue_module",
|
||||||
|
filter=Q(
|
||||||
|
issue_module__issue__archived_at__isnull=True,
|
||||||
|
issue_module__issue__is_draft=False,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
completed_issues=Count(
|
completed_issues=Count(
|
||||||
"issue_module__issue__state__group",
|
"issue_module__issue__state__group",
|
||||||
filter=Q(issue_module__issue__state__group="completed"),
|
filter=Q(
|
||||||
|
issue_module__issue__state__group="completed",
|
||||||
|
issue_module__issue__archived_at__isnull=True,
|
||||||
|
issue_module__issue__is_draft=False,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
cancelled_issues=Count(
|
cancelled_issues=Count(
|
||||||
"issue_module__issue__state__group",
|
"issue_module__issue__state__group",
|
||||||
filter=Q(issue_module__issue__state__group="cancelled"),
|
filter=Q(
|
||||||
|
issue_module__issue__state__group="cancelled",
|
||||||
|
issue_module__issue__archived_at__isnull=True,
|
||||||
|
issue_module__issue__is_draft=False,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
started_issues=Count(
|
started_issues=Count(
|
||||||
"issue_module__issue__state__group",
|
"issue_module__issue__state__group",
|
||||||
filter=Q(issue_module__issue__state__group="started"),
|
filter=Q(
|
||||||
|
issue_module__issue__state__group="started",
|
||||||
|
issue_module__issue__archived_at__isnull=True,
|
||||||
|
issue_module__issue__is_draft=False,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
unstarted_issues=Count(
|
unstarted_issues=Count(
|
||||||
"issue_module__issue__state__group",
|
"issue_module__issue__state__group",
|
||||||
filter=Q(issue_module__issue__state__group="unstarted"),
|
filter=Q(
|
||||||
|
issue_module__issue__state__group="unstarted",
|
||||||
|
issue_module__issue__archived_at__isnull=True,
|
||||||
|
issue_module__issue__is_draft=False,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
backlog_issues=Count(
|
backlog_issues=Count(
|
||||||
"issue_module__issue__state__group",
|
"issue_module__issue__state__group",
|
||||||
filter=Q(issue_module__issue__state__group="backlog"),
|
filter=Q(
|
||||||
|
issue_module__issue__state__group="backlog",
|
||||||
|
issue_module__issue__archived_at__isnull=True,
|
||||||
|
issue_module__issue__is_draft=False,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.order_by(order_by, "name")
|
.order_by(order_by, "name")
|
||||||
@ -129,6 +159,7 @@ class ModuleViewSet(BaseViewSet):
|
|||||||
issue_id=str(self.kwargs.get("pk", None)),
|
issue_id=str(self.kwargs.get("pk", None)),
|
||||||
project_id=str(self.kwargs.get("project_id", None)),
|
project_id=str(self.kwargs.get("project_id", None)),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
|
|
||||||
return super().perform_destroy(instance)
|
return super().perform_destroy(instance)
|
||||||
@ -177,18 +208,36 @@ class ModuleViewSet(BaseViewSet):
|
|||||||
.annotate(assignee_id=F("assignees__id"))
|
.annotate(assignee_id=F("assignees__id"))
|
||||||
.annotate(display_name=F("assignees__display_name"))
|
.annotate(display_name=F("assignees__display_name"))
|
||||||
.annotate(avatar=F("assignees__avatar"))
|
.annotate(avatar=F("assignees__avatar"))
|
||||||
.values("first_name", "last_name", "assignee_id", "avatar", "display_name")
|
.values(
|
||||||
.annotate(total_issues=Count("assignee_id"))
|
"first_name", "last_name", "assignee_id", "avatar", "display_name"
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
total_issues=Count(
|
||||||
|
"assignee_id",
|
||||||
|
filter=Q(
|
||||||
|
archived_at__isnull=True,
|
||||||
|
is_draft=False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
completed_issues=Count(
|
completed_issues=Count(
|
||||||
"assignee_id",
|
"assignee_id",
|
||||||
filter=Q(completed_at__isnull=False),
|
filter=Q(
|
||||||
|
completed_at__isnull=False,
|
||||||
|
archived_at__isnull=True,
|
||||||
|
is_draft=False,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
pending_issues=Count(
|
pending_issues=Count(
|
||||||
"assignee_id",
|
"assignee_id",
|
||||||
filter=Q(completed_at__isnull=True),
|
filter=Q(
|
||||||
|
completed_at__isnull=True,
|
||||||
|
archived_at__isnull=True,
|
||||||
|
is_draft=False,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.order_by("first_name", "last_name")
|
.order_by("first_name", "last_name")
|
||||||
@ -204,17 +253,33 @@ class ModuleViewSet(BaseViewSet):
|
|||||||
.annotate(color=F("labels__color"))
|
.annotate(color=F("labels__color"))
|
||||||
.annotate(label_id=F("labels__id"))
|
.annotate(label_id=F("labels__id"))
|
||||||
.values("label_name", "color", "label_id")
|
.values("label_name", "color", "label_id")
|
||||||
.annotate(total_issues=Count("label_id"))
|
.annotate(
|
||||||
|
total_issues=Count(
|
||||||
|
"label_id",
|
||||||
|
filter=Q(
|
||||||
|
archived_at__isnull=True,
|
||||||
|
is_draft=False,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
completed_issues=Count(
|
completed_issues=Count(
|
||||||
"label_id",
|
"label_id",
|
||||||
filter=Q(completed_at__isnull=False),
|
filter=Q(
|
||||||
|
completed_at__isnull=False,
|
||||||
|
archived_at__isnull=True,
|
||||||
|
is_draft=False,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
pending_issues=Count(
|
pending_issues=Count(
|
||||||
"label_id",
|
"label_id",
|
||||||
filter=Q(completed_at__isnull=True),
|
filter=Q(
|
||||||
|
completed_at__isnull=True,
|
||||||
|
archived_at__isnull=True,
|
||||||
|
is_draft=False,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.order_by("label_name")
|
.order_by("label_name")
|
||||||
@ -277,6 +342,7 @@ class ModuleIssueViewSet(BaseViewSet):
|
|||||||
issue_id=str(self.kwargs.get("pk", None)),
|
issue_id=str(self.kwargs.get("pk", None)),
|
||||||
project_id=str(self.kwargs.get("project_id", None)),
|
project_id=str(self.kwargs.get("project_id", None)),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
return super().perform_destroy(instance)
|
return super().perform_destroy(instance)
|
||||||
|
|
||||||
@ -308,6 +374,7 @@ class ModuleIssueViewSet(BaseViewSet):
|
|||||||
try:
|
try:
|
||||||
order_by = request.GET.get("order_by", "created_at")
|
order_by = request.GET.get("order_by", "created_at")
|
||||||
group_by = request.GET.get("group_by", False)
|
group_by = request.GET.get("group_by", False)
|
||||||
|
sub_group_by = request.GET.get("sub_group_by", False)
|
||||||
filters = issue_filters(request.query_params, "GET")
|
filters = issue_filters(request.query_params, "GET")
|
||||||
issues = (
|
issues = (
|
||||||
Issue.issue_objects.filter(issue_module__module_id=module_id)
|
Issue.issue_objects.filter(issue_module__module_id=module_id)
|
||||||
@ -346,9 +413,15 @@ class ModuleIssueViewSet(BaseViewSet):
|
|||||||
|
|
||||||
issues_data = IssueStateSerializer(issues, many=True).data
|
issues_data = IssueStateSerializer(issues, many=True).data
|
||||||
|
|
||||||
|
if sub_group_by and sub_group_by == group_by:
|
||||||
|
return Response(
|
||||||
|
{"error": "Group by and sub group by cannot be same"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
if group_by:
|
if group_by:
|
||||||
return Response(
|
return Response(
|
||||||
group_results(issues_data, group_by),
|
group_results(issues_data, group_by, sub_group_by),
|
||||||
status=status.HTTP_200_OK,
|
status=status.HTTP_200_OK,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -437,6 +510,7 @@ class ModuleIssueViewSet(BaseViewSet):
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
@ -483,7 +557,6 @@ class ModuleLinkViewSet(BaseViewSet):
|
|||||||
|
|
||||||
|
|
||||||
class ModuleFavoriteViewSet(BaseViewSet):
|
class ModuleFavoriteViewSet(BaseViewSet):
|
||||||
|
|
||||||
serializer_class = ModuleFavoriteSerializer
|
serializer_class = ModuleFavoriteSerializer
|
||||||
model = ModuleFavorite
|
model = ModuleFavorite
|
||||||
|
|
||||||
|
@ -1094,7 +1094,7 @@ class ProjectMemberEndpoint(BaseAPIView):
|
|||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
workspace__slug=slug,
|
workspace__slug=slug,
|
||||||
member__is_bot=False,
|
member__is_bot=False,
|
||||||
).select_related("project", "member")
|
).select_related("project", "member", "workspace")
|
||||||
serializer = ProjectMemberSerializer(project_members, many=True)
|
serializer = ProjectMemberSerializer(project_members, many=True)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
@ -220,7 +220,7 @@ class IssueSearchEndpoint(BaseAPIView):
|
|||||||
query = request.query_params.get("search", False)
|
query = request.query_params.get("search", False)
|
||||||
workspace_search = request.query_params.get("workspace_search", "false")
|
workspace_search = request.query_params.get("workspace_search", "false")
|
||||||
parent = request.query_params.get("parent", "false")
|
parent = request.query_params.get("parent", "false")
|
||||||
blocker_blocked_by = request.query_params.get("blocker_blocked_by", "false")
|
issue_relation = request.query_params.get("issue_relation", "false")
|
||||||
cycle = request.query_params.get("cycle", "false")
|
cycle = request.query_params.get("cycle", "false")
|
||||||
module = request.query_params.get("module", "false")
|
module = request.query_params.get("module", "false")
|
||||||
sub_issue = request.query_params.get("sub_issue", "false")
|
sub_issue = request.query_params.get("sub_issue", "false")
|
||||||
@ -247,12 +247,12 @@ class IssueSearchEndpoint(BaseAPIView):
|
|||||||
"parent_id", flat=True
|
"parent_id", flat=True
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if blocker_blocked_by == "true" and issue_id:
|
if issue_relation == "true" and issue_id:
|
||||||
issue = Issue.issue_objects.get(pk=issue_id)
|
issue = Issue.issue_objects.get(pk=issue_id)
|
||||||
issues = issues.filter(
|
issues = issues.filter(
|
||||||
~Q(pk=issue_id),
|
~Q(pk=issue_id),
|
||||||
~Q(blocked_issues__block=issue),
|
~Q(issue_related__issue=issue),
|
||||||
~Q(blocker_issues__blocked_by=issue),
|
~Q(issue_relation__related_issue=issue),
|
||||||
)
|
)
|
||||||
if sub_issue == "true" and issue_id:
|
if sub_issue == "true" and issue_id:
|
||||||
issue = Issue.issue_objects.get(pk=issue_id)
|
issue = Issue.issue_objects.get(pk=issue_id)
|
||||||
|
@ -1,4 +1,18 @@
|
|||||||
# Django imports
|
# Django imports
|
||||||
|
from django.db.models import (
|
||||||
|
Prefetch,
|
||||||
|
OuterRef,
|
||||||
|
Func,
|
||||||
|
F,
|
||||||
|
Case,
|
||||||
|
Value,
|
||||||
|
CharField,
|
||||||
|
When,
|
||||||
|
Exists,
|
||||||
|
Max,
|
||||||
|
)
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
from django.views.decorators.gzip import gzip_page
|
||||||
from django.db import IntegrityError
|
from django.db import IntegrityError
|
||||||
from django.db.models import Prefetch, OuterRef, Exists
|
from django.db.models import Prefetch, OuterRef, Exists
|
||||||
|
|
||||||
@ -10,18 +24,192 @@ from sentry_sdk import capture_exception
|
|||||||
# Module imports
|
# Module imports
|
||||||
from . import BaseViewSet, BaseAPIView
|
from . import BaseViewSet, BaseAPIView
|
||||||
from plane.api.serializers import (
|
from plane.api.serializers import (
|
||||||
|
GlobalViewSerializer,
|
||||||
IssueViewSerializer,
|
IssueViewSerializer,
|
||||||
IssueLiteSerializer,
|
IssueLiteSerializer,
|
||||||
IssueViewFavoriteSerializer,
|
IssueViewFavoriteSerializer,
|
||||||
)
|
)
|
||||||
from plane.api.permissions import ProjectEntityPermission
|
from plane.api.permissions import WorkspaceEntityPermission, ProjectEntityPermission
|
||||||
from plane.db.models import (
|
from plane.db.models import (
|
||||||
|
Workspace,
|
||||||
|
GlobalView,
|
||||||
IssueView,
|
IssueView,
|
||||||
Issue,
|
Issue,
|
||||||
IssueViewFavorite,
|
IssueViewFavorite,
|
||||||
IssueReaction,
|
IssueReaction,
|
||||||
|
IssueLink,
|
||||||
|
IssueAttachment,
|
||||||
)
|
)
|
||||||
from plane.utils.issue_filters import issue_filters
|
from plane.utils.issue_filters import issue_filters
|
||||||
|
from plane.utils.grouper import group_results
|
||||||
|
|
||||||
|
|
||||||
|
class GlobalViewViewSet(BaseViewSet):
|
||||||
|
serializer_class = GlobalViewSerializer
|
||||||
|
model = GlobalView
|
||||||
|
permission_classes = [
|
||||||
|
WorkspaceEntityPermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
def perform_create(self, serializer):
|
||||||
|
workspace = Workspace.objects.get(slug=self.kwargs.get("slug"))
|
||||||
|
serializer.save(workspace_id=workspace.id)
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.filter_queryset(
|
||||||
|
super()
|
||||||
|
.get_queryset()
|
||||||
|
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||||
|
.select_related("workspace")
|
||||||
|
.order_by(self.request.GET.get("order_by", "-created_at"))
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class GlobalViewIssuesViewSet(BaseViewSet):
|
||||||
|
permission_classes = [
|
||||||
|
WorkspaceEntityPermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return (
|
||||||
|
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(workspace__slug=self.kwargs.get("slug"))
|
||||||
|
.select_related("project")
|
||||||
|
.select_related("workspace")
|
||||||
|
.select_related("state")
|
||||||
|
.select_related("parent")
|
||||||
|
.prefetch_related("assignees")
|
||||||
|
.prefetch_related("labels")
|
||||||
|
.prefetch_related(
|
||||||
|
Prefetch(
|
||||||
|
"issue_reactions",
|
||||||
|
queryset=IssueReaction.objects.select_related("actor"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@method_decorator(gzip_page)
|
||||||
|
def list(self, request, slug):
|
||||||
|
try:
|
||||||
|
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 = (
|
||||||
|
self.get_queryset()
|
||||||
|
.filter(**filters)
|
||||||
|
.filter(project__project_projectmember__member=self.request.user)
|
||||||
|
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||||
|
.annotate(module_id=F("issue_module__module_id"))
|
||||||
|
.annotate(
|
||||||
|
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||||
|
.order_by()
|
||||||
|
.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 = IssueLiteSerializer(issue_queryset, many=True).data
|
||||||
|
|
||||||
|
## Grouping the results
|
||||||
|
group_by = request.GET.get("group_by", False)
|
||||||
|
sub_group_by = request.GET.get("sub_group_by", False)
|
||||||
|
if sub_group_by and sub_group_by == group_by:
|
||||||
|
return Response(
|
||||||
|
{"error": "Group by and sub group by cannot be same"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
if group_by:
|
||||||
|
return Response(
|
||||||
|
group_results(issues, group_by, sub_group_by), status=status.HTTP_200_OK
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response(issues, 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 IssueViewViewSet(BaseViewSet):
|
class IssueViewViewSet(BaseViewSet):
|
||||||
|
@ -1072,7 +1072,7 @@ class WorkspaceUserProfileStatsEndpoint(BaseAPIView):
|
|||||||
.order_by("state_group")
|
.order_by("state_group")
|
||||||
)
|
)
|
||||||
|
|
||||||
priority_order = ["urgent", "high", "medium", "low", None]
|
priority_order = ["urgent", "high", "medium", "low", "none"]
|
||||||
|
|
||||||
priority_distribution = (
|
priority_distribution = (
|
||||||
Issue.issue_objects.filter(
|
Issue.issue_objects.filter(
|
||||||
@ -1239,13 +1239,21 @@ class WorkspaceUserProfileEndpoint(BaseAPIView):
|
|||||||
.annotate(
|
.annotate(
|
||||||
created_issues=Count(
|
created_issues=Count(
|
||||||
"project_issue",
|
"project_issue",
|
||||||
filter=Q(project_issue__created_by_id=user_id),
|
filter=Q(
|
||||||
|
project_issue__created_by_id=user_id,
|
||||||
|
project_issue__archived_at__isnull=True,
|
||||||
|
project_issue__is_draft=False,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
assigned_issues=Count(
|
assigned_issues=Count(
|
||||||
"project_issue",
|
"project_issue",
|
||||||
filter=Q(project_issue__assignees__in=[user_id]),
|
filter=Q(
|
||||||
|
project_issue__assignees__in=[user_id],
|
||||||
|
project_issue__archived_at__isnull=True,
|
||||||
|
project_issue__is_draft=False,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
@ -1254,6 +1262,8 @@ class WorkspaceUserProfileEndpoint(BaseAPIView):
|
|||||||
filter=Q(
|
filter=Q(
|
||||||
project_issue__completed_at__isnull=False,
|
project_issue__completed_at__isnull=False,
|
||||||
project_issue__assignees__in=[user_id],
|
project_issue__assignees__in=[user_id],
|
||||||
|
project_issue__archived_at__isnull=True,
|
||||||
|
project_issue__is_draft=False,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -1267,6 +1277,8 @@ class WorkspaceUserProfileEndpoint(BaseAPIView):
|
|||||||
"started",
|
"started",
|
||||||
],
|
],
|
||||||
project_issue__assignees__in=[user_id],
|
project_issue__assignees__in=[user_id],
|
||||||
|
project_issue__archived_at__isnull=True,
|
||||||
|
project_issue__is_draft=False,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -1317,6 +1329,11 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
|
|||||||
def get(self, request, slug, user_id):
|
def get(self, request, slug, user_id):
|
||||||
try:
|
try:
|
||||||
filters = issue_filters(request.query_params, "GET")
|
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")
|
order_by_param = request.GET.get("order_by", "-created_at")
|
||||||
issue_queryset = (
|
issue_queryset = (
|
||||||
Issue.issue_objects.filter(
|
Issue.issue_objects.filter(
|
||||||
|
@ -32,7 +32,7 @@ def delete_old_s3_link():
|
|||||||
else:
|
else:
|
||||||
s3 = boto3.client(
|
s3 = boto3.client(
|
||||||
"s3",
|
"s3",
|
||||||
region_name="ap-south-1",
|
region_name=settings.AWS_REGION,
|
||||||
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
|
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
|
||||||
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
|
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
|
||||||
config=Config(signature_version="s3v4"),
|
config=Config(signature_version="s3v4"),
|
||||||
|
@ -39,6 +39,7 @@ def track_name(
|
|||||||
project,
|
project,
|
||||||
actor,
|
actor,
|
||||||
issue_activities,
|
issue_activities,
|
||||||
|
epoch
|
||||||
):
|
):
|
||||||
if current_instance.get("name") != requested_data.get("name"):
|
if current_instance.get("name") != requested_data.get("name"):
|
||||||
issue_activities.append(
|
issue_activities.append(
|
||||||
@ -52,6 +53,7 @@ def track_name(
|
|||||||
project=project,
|
project=project,
|
||||||
workspace=project.workspace,
|
workspace=project.workspace,
|
||||||
comment=f"updated the name to {requested_data.get('name')}",
|
comment=f"updated the name to {requested_data.get('name')}",
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -64,6 +66,7 @@ def track_parent(
|
|||||||
project,
|
project,
|
||||||
actor,
|
actor,
|
||||||
issue_activities,
|
issue_activities,
|
||||||
|
epoch
|
||||||
):
|
):
|
||||||
if current_instance.get("parent") != requested_data.get("parent"):
|
if current_instance.get("parent") != requested_data.get("parent"):
|
||||||
if requested_data.get("parent") == None:
|
if requested_data.get("parent") == None:
|
||||||
@ -81,6 +84,7 @@ def track_parent(
|
|||||||
comment=f"updated the parent issue to None",
|
comment=f"updated the parent issue to None",
|
||||||
old_identifier=old_parent.id,
|
old_identifier=old_parent.id,
|
||||||
new_identifier=None,
|
new_identifier=None,
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@ -101,6 +105,7 @@ def track_parent(
|
|||||||
comment=f"updated the parent issue to {new_parent.name}",
|
comment=f"updated the parent issue to {new_parent.name}",
|
||||||
old_identifier=old_parent.id if old_parent is not None else None,
|
old_identifier=old_parent.id if old_parent is not None else None,
|
||||||
new_identifier=new_parent.id,
|
new_identifier=new_parent.id,
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -113,36 +118,23 @@ def track_priority(
|
|||||||
project,
|
project,
|
||||||
actor,
|
actor,
|
||||||
issue_activities,
|
issue_activities,
|
||||||
|
epoch
|
||||||
):
|
):
|
||||||
if current_instance.get("priority") != requested_data.get("priority"):
|
if current_instance.get("priority") != requested_data.get("priority"):
|
||||||
if requested_data.get("priority") == None:
|
issue_activities.append(
|
||||||
issue_activities.append(
|
IssueActivity(
|
||||||
IssueActivity(
|
issue_id=issue_id,
|
||||||
issue_id=issue_id,
|
actor=actor,
|
||||||
actor=actor,
|
verb="updated",
|
||||||
verb="updated",
|
old_value=current_instance.get("priority"),
|
||||||
old_value=current_instance.get("priority"),
|
new_value=requested_data.get("priority"),
|
||||||
new_value=None,
|
field="priority",
|
||||||
field="priority",
|
project=project,
|
||||||
project=project,
|
workspace=project.workspace,
|
||||||
workspace=project.workspace,
|
comment=f"updated the priority to {requested_data.get('priority')}",
|
||||||
comment=f"updated the priority to None",
|
epoch=epoch,
|
||||||
)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
issue_activities.append(
|
|
||||||
IssueActivity(
|
|
||||||
issue_id=issue_id,
|
|
||||||
actor=actor,
|
|
||||||
verb="updated",
|
|
||||||
old_value=current_instance.get("priority"),
|
|
||||||
new_value=requested_data.get("priority"),
|
|
||||||
field="priority",
|
|
||||||
project=project,
|
|
||||||
workspace=project.workspace,
|
|
||||||
comment=f"updated the priority to {requested_data.get('priority')}",
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# Track chnages in state of the issue
|
# Track chnages in state of the issue
|
||||||
@ -153,6 +145,7 @@ def track_state(
|
|||||||
project,
|
project,
|
||||||
actor,
|
actor,
|
||||||
issue_activities,
|
issue_activities,
|
||||||
|
epoch
|
||||||
):
|
):
|
||||||
if current_instance.get("state") != requested_data.get("state"):
|
if current_instance.get("state") != requested_data.get("state"):
|
||||||
new_state = State.objects.get(pk=requested_data.get("state", None))
|
new_state = State.objects.get(pk=requested_data.get("state", None))
|
||||||
@ -171,6 +164,7 @@ def track_state(
|
|||||||
comment=f"updated the state to {new_state.name}",
|
comment=f"updated the state to {new_state.name}",
|
||||||
old_identifier=old_state.id,
|
old_identifier=old_state.id,
|
||||||
new_identifier=new_state.id,
|
new_identifier=new_state.id,
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -183,6 +177,7 @@ def track_description(
|
|||||||
project,
|
project,
|
||||||
actor,
|
actor,
|
||||||
issue_activities,
|
issue_activities,
|
||||||
|
epoch
|
||||||
):
|
):
|
||||||
if current_instance.get("description_html") != requested_data.get(
|
if current_instance.get("description_html") != requested_data.get(
|
||||||
"description_html"
|
"description_html"
|
||||||
@ -203,6 +198,7 @@ def track_description(
|
|||||||
project=project,
|
project=project,
|
||||||
workspace=project.workspace,
|
workspace=project.workspace,
|
||||||
comment=f"updated the description to {requested_data.get('description_html')}",
|
comment=f"updated the description to {requested_data.get('description_html')}",
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -215,6 +211,7 @@ def track_target_date(
|
|||||||
project,
|
project,
|
||||||
actor,
|
actor,
|
||||||
issue_activities,
|
issue_activities,
|
||||||
|
epoch
|
||||||
):
|
):
|
||||||
if current_instance.get("target_date") != requested_data.get("target_date"):
|
if current_instance.get("target_date") != requested_data.get("target_date"):
|
||||||
if requested_data.get("target_date") == None:
|
if requested_data.get("target_date") == None:
|
||||||
@ -229,6 +226,7 @@ def track_target_date(
|
|||||||
project=project,
|
project=project,
|
||||||
workspace=project.workspace,
|
workspace=project.workspace,
|
||||||
comment=f"updated the target date to None",
|
comment=f"updated the target date to None",
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@ -243,6 +241,7 @@ def track_target_date(
|
|||||||
project=project,
|
project=project,
|
||||||
workspace=project.workspace,
|
workspace=project.workspace,
|
||||||
comment=f"updated the target date to {requested_data.get('target_date')}",
|
comment=f"updated the target date to {requested_data.get('target_date')}",
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -255,6 +254,7 @@ def track_start_date(
|
|||||||
project,
|
project,
|
||||||
actor,
|
actor,
|
||||||
issue_activities,
|
issue_activities,
|
||||||
|
epoch
|
||||||
):
|
):
|
||||||
if current_instance.get("start_date") != requested_data.get("start_date"):
|
if current_instance.get("start_date") != requested_data.get("start_date"):
|
||||||
if requested_data.get("start_date") == None:
|
if requested_data.get("start_date") == None:
|
||||||
@ -269,6 +269,7 @@ def track_start_date(
|
|||||||
project=project,
|
project=project,
|
||||||
workspace=project.workspace,
|
workspace=project.workspace,
|
||||||
comment=f"updated the start date to None",
|
comment=f"updated the start date to None",
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@ -283,6 +284,7 @@ def track_start_date(
|
|||||||
project=project,
|
project=project,
|
||||||
workspace=project.workspace,
|
workspace=project.workspace,
|
||||||
comment=f"updated the start date to {requested_data.get('start_date')}",
|
comment=f"updated the start date to {requested_data.get('start_date')}",
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -295,6 +297,7 @@ def track_labels(
|
|||||||
project,
|
project,
|
||||||
actor,
|
actor,
|
||||||
issue_activities,
|
issue_activities,
|
||||||
|
epoch
|
||||||
):
|
):
|
||||||
# Label Addition
|
# Label Addition
|
||||||
if len(requested_data.get("labels_list")) > len(current_instance.get("labels")):
|
if len(requested_data.get("labels_list")) > len(current_instance.get("labels")):
|
||||||
@ -314,6 +317,7 @@ def track_labels(
|
|||||||
comment=f"added label {label.name}",
|
comment=f"added label {label.name}",
|
||||||
new_identifier=label.id,
|
new_identifier=label.id,
|
||||||
old_identifier=None,
|
old_identifier=None,
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -335,6 +339,7 @@ def track_labels(
|
|||||||
comment=f"removed label {label.name}",
|
comment=f"removed label {label.name}",
|
||||||
old_identifier=label.id,
|
old_identifier=label.id,
|
||||||
new_identifier=None,
|
new_identifier=None,
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -347,6 +352,7 @@ def track_assignees(
|
|||||||
project,
|
project,
|
||||||
actor,
|
actor,
|
||||||
issue_activities,
|
issue_activities,
|
||||||
|
epoch
|
||||||
):
|
):
|
||||||
# Assignee Addition
|
# Assignee Addition
|
||||||
if len(requested_data.get("assignees_list")) > len(
|
if len(requested_data.get("assignees_list")) > len(
|
||||||
@ -367,6 +373,7 @@ def track_assignees(
|
|||||||
workspace=project.workspace,
|
workspace=project.workspace,
|
||||||
comment=f"added assignee {assignee.display_name}",
|
comment=f"added assignee {assignee.display_name}",
|
||||||
new_identifier=assignee.id,
|
new_identifier=assignee.id,
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -389,151 +396,29 @@ def track_assignees(
|
|||||||
workspace=project.workspace,
|
workspace=project.workspace,
|
||||||
comment=f"removed assignee {assignee.display_name}",
|
comment=f"removed assignee {assignee.display_name}",
|
||||||
old_identifier=assignee.id,
|
old_identifier=assignee.id,
|
||||||
)
|
epoch=epoch,
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# Track changes in blocking issues
|
|
||||||
def track_blocks(
|
|
||||||
requested_data,
|
|
||||||
current_instance,
|
|
||||||
issue_id,
|
|
||||||
project,
|
|
||||||
actor,
|
|
||||||
issue_activities,
|
|
||||||
):
|
|
||||||
if len(requested_data.get("blocks_list")) > len(
|
|
||||||
current_instance.get("blocked_issues")
|
|
||||||
):
|
|
||||||
for block in requested_data.get("blocks_list"):
|
|
||||||
if (
|
|
||||||
len(
|
|
||||||
[
|
|
||||||
blocked
|
|
||||||
for blocked in current_instance.get("blocked_issues")
|
|
||||||
if blocked.get("block") == block
|
|
||||||
]
|
|
||||||
)
|
|
||||||
== 0
|
|
||||||
):
|
|
||||||
issue = Issue.objects.get(pk=block)
|
|
||||||
issue_activities.append(
|
|
||||||
IssueActivity(
|
|
||||||
issue_id=issue_id,
|
|
||||||
actor=actor,
|
|
||||||
verb="updated",
|
|
||||||
old_value="",
|
|
||||||
new_value=f"{issue.project.identifier}-{issue.sequence_id}",
|
|
||||||
field="blocks",
|
|
||||||
project=project,
|
|
||||||
workspace=project.workspace,
|
|
||||||
comment=f"added blocking issue {project.identifier}-{issue.sequence_id}",
|
|
||||||
new_identifier=issue.id,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Blocked Issue Removal
|
|
||||||
if len(requested_data.get("blocks_list")) < len(
|
|
||||||
current_instance.get("blocked_issues")
|
|
||||||
):
|
|
||||||
for blocked in current_instance.get("blocked_issues"):
|
|
||||||
if blocked.get("block") not in requested_data.get("blocks_list"):
|
|
||||||
issue = Issue.objects.get(pk=blocked.get("block"))
|
|
||||||
issue_activities.append(
|
|
||||||
IssueActivity(
|
|
||||||
issue_id=issue_id,
|
|
||||||
actor=actor,
|
|
||||||
verb="updated",
|
|
||||||
old_value=f"{issue.project.identifier}-{issue.sequence_id}",
|
|
||||||
new_value="",
|
|
||||||
field="blocks",
|
|
||||||
project=project,
|
|
||||||
workspace=project.workspace,
|
|
||||||
comment=f"removed blocking issue {project.identifier}-{issue.sequence_id}",
|
|
||||||
old_identifier=issue.id,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# Track changes in blocked_by issues
|
|
||||||
def track_blockings(
|
|
||||||
requested_data,
|
|
||||||
current_instance,
|
|
||||||
issue_id,
|
|
||||||
project,
|
|
||||||
actor,
|
|
||||||
issue_activities,
|
|
||||||
):
|
|
||||||
if len(requested_data.get("blockers_list")) > len(
|
|
||||||
current_instance.get("blocker_issues")
|
|
||||||
):
|
|
||||||
for block in requested_data.get("blockers_list"):
|
|
||||||
if (
|
|
||||||
len(
|
|
||||||
[
|
|
||||||
blocked
|
|
||||||
for blocked in current_instance.get("blocker_issues")
|
|
||||||
if blocked.get("blocked_by") == block
|
|
||||||
]
|
|
||||||
)
|
|
||||||
== 0
|
|
||||||
):
|
|
||||||
issue = Issue.objects.get(pk=block)
|
|
||||||
issue_activities.append(
|
|
||||||
IssueActivity(
|
|
||||||
issue_id=issue_id,
|
|
||||||
actor=actor,
|
|
||||||
verb="updated",
|
|
||||||
old_value="",
|
|
||||||
new_value=f"{issue.project.identifier}-{issue.sequence_id}",
|
|
||||||
field="blocking",
|
|
||||||
project=project,
|
|
||||||
workspace=project.workspace,
|
|
||||||
comment=f"added blocked by issue {project.identifier}-{issue.sequence_id}",
|
|
||||||
new_identifier=issue.id,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Blocked Issue Removal
|
|
||||||
if len(requested_data.get("blockers_list")) < len(
|
|
||||||
current_instance.get("blocker_issues")
|
|
||||||
):
|
|
||||||
for blocked in current_instance.get("blocker_issues"):
|
|
||||||
if blocked.get("blocked_by") not in requested_data.get("blockers_list"):
|
|
||||||
issue = Issue.objects.get(pk=blocked.get("blocked_by"))
|
|
||||||
issue_activities.append(
|
|
||||||
IssueActivity(
|
|
||||||
issue_id=issue_id,
|
|
||||||
actor=actor,
|
|
||||||
verb="updated",
|
|
||||||
old_value=f"{issue.project.identifier}-{issue.sequence_id}",
|
|
||||||
new_value="",
|
|
||||||
field="blocking",
|
|
||||||
project=project,
|
|
||||||
workspace=project.workspace,
|
|
||||||
comment=f"removed blocked by issue {project.identifier}-{issue.sequence_id}",
|
|
||||||
old_identifier=issue.id,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def create_issue_activity(
|
def create_issue_activity(
|
||||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
):
|
):
|
||||||
issue_activities.append(
|
issue_activities.append(
|
||||||
IssueActivity(
|
IssueActivity(
|
||||||
issue_id=issue_id,
|
issue_id=issue_id,
|
||||||
project=project,
|
project=project,
|
||||||
workspace=project.workspace,
|
workspace=project.workspace,
|
||||||
comment=f"created the issue",
|
comment=f"created the issue",
|
||||||
verb="created",
|
verb="created",
|
||||||
actor=actor,
|
actor=actor,
|
||||||
|
epoch=epoch,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def track_estimate_points(
|
def track_estimate_points(
|
||||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
):
|
):
|
||||||
if current_instance.get("estimate_point") != requested_data.get("estimate_point"):
|
if current_instance.get("estimate_point") != requested_data.get("estimate_point"):
|
||||||
if requested_data.get("estimate_point") == None:
|
if requested_data.get("estimate_point") == None:
|
||||||
@ -548,6 +433,7 @@ def track_estimate_points(
|
|||||||
project=project,
|
project=project,
|
||||||
workspace=project.workspace,
|
workspace=project.workspace,
|
||||||
comment=f"updated the estimate point to None",
|
comment=f"updated the estimate point to None",
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@ -562,12 +448,13 @@ def track_estimate_points(
|
|||||||
project=project,
|
project=project,
|
||||||
workspace=project.workspace,
|
workspace=project.workspace,
|
||||||
comment=f"updated the estimate point to {requested_data.get('estimate_point')}",
|
comment=f"updated the estimate point to {requested_data.get('estimate_point')}",
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def track_archive_at(
|
def track_archive_at(
|
||||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
):
|
):
|
||||||
if requested_data.get("archived_at") is None:
|
if requested_data.get("archived_at") is None:
|
||||||
issue_activities.append(
|
issue_activities.append(
|
||||||
@ -581,6 +468,7 @@ def track_archive_at(
|
|||||||
field="archived_at",
|
field="archived_at",
|
||||||
old_value="archive",
|
old_value="archive",
|
||||||
new_value="restore",
|
new_value="restore",
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@ -595,12 +483,13 @@ def track_archive_at(
|
|||||||
field="archived_at",
|
field="archived_at",
|
||||||
old_value=None,
|
old_value=None,
|
||||||
new_value="archive",
|
new_value="archive",
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def track_closed_to(
|
def track_closed_to(
|
||||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
):
|
):
|
||||||
if requested_data.get("closed_to") is not None:
|
if requested_data.get("closed_to") is not None:
|
||||||
updated_state = State.objects.get(
|
updated_state = State.objects.get(
|
||||||
@ -620,12 +509,13 @@ def track_closed_to(
|
|||||||
comment=f"Plane updated the state to {updated_state.name}",
|
comment=f"Plane updated the state to {updated_state.name}",
|
||||||
old_identifier=None,
|
old_identifier=None,
|
||||||
new_identifier=updated_state.id,
|
new_identifier=updated_state.id,
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def update_issue_activity(
|
def update_issue_activity(
|
||||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
):
|
):
|
||||||
ISSUE_ACTIVITY_MAPPER = {
|
ISSUE_ACTIVITY_MAPPER = {
|
||||||
"name": track_name,
|
"name": track_name,
|
||||||
@ -637,8 +527,6 @@ def update_issue_activity(
|
|||||||
"start_date": track_start_date,
|
"start_date": track_start_date,
|
||||||
"labels_list": track_labels,
|
"labels_list": track_labels,
|
||||||
"assignees_list": track_assignees,
|
"assignees_list": track_assignees,
|
||||||
"blocks_list": track_blocks,
|
|
||||||
"blockers_list": track_blockings,
|
|
||||||
"estimate_point": track_estimate_points,
|
"estimate_point": track_estimate_points,
|
||||||
"archived_at": track_archive_at,
|
"archived_at": track_archive_at,
|
||||||
"closed_to": track_closed_to,
|
"closed_to": track_closed_to,
|
||||||
@ -659,11 +547,12 @@ def update_issue_activity(
|
|||||||
project,
|
project,
|
||||||
actor,
|
actor,
|
||||||
issue_activities,
|
issue_activities,
|
||||||
|
epoch
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def delete_issue_activity(
|
def delete_issue_activity(
|
||||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
):
|
):
|
||||||
issue_activities.append(
|
issue_activities.append(
|
||||||
IssueActivity(
|
IssueActivity(
|
||||||
@ -673,12 +562,13 @@ def delete_issue_activity(
|
|||||||
verb="deleted",
|
verb="deleted",
|
||||||
actor=actor,
|
actor=actor,
|
||||||
field="issue",
|
field="issue",
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def create_comment_activity(
|
def create_comment_activity(
|
||||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
):
|
):
|
||||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||||
current_instance = (
|
current_instance = (
|
||||||
@ -697,12 +587,13 @@ def create_comment_activity(
|
|||||||
new_value=requested_data.get("comment_html", ""),
|
new_value=requested_data.get("comment_html", ""),
|
||||||
new_identifier=requested_data.get("id", None),
|
new_identifier=requested_data.get("id", None),
|
||||||
issue_comment_id=requested_data.get("id", None),
|
issue_comment_id=requested_data.get("id", None),
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def update_comment_activity(
|
def update_comment_activity(
|
||||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
):
|
):
|
||||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||||
current_instance = (
|
current_instance = (
|
||||||
@ -724,12 +615,13 @@ def update_comment_activity(
|
|||||||
new_value=requested_data.get("comment_html", ""),
|
new_value=requested_data.get("comment_html", ""),
|
||||||
new_identifier=current_instance.get("id", None),
|
new_identifier=current_instance.get("id", None),
|
||||||
issue_comment_id=current_instance.get("id", None),
|
issue_comment_id=current_instance.get("id", None),
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def delete_comment_activity(
|
def delete_comment_activity(
|
||||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
):
|
):
|
||||||
issue_activities.append(
|
issue_activities.append(
|
||||||
IssueActivity(
|
IssueActivity(
|
||||||
@ -740,12 +632,13 @@ def delete_comment_activity(
|
|||||||
verb="deleted",
|
verb="deleted",
|
||||||
actor=actor,
|
actor=actor,
|
||||||
field="comment",
|
field="comment",
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def create_cycle_issue_activity(
|
def create_cycle_issue_activity(
|
||||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
):
|
):
|
||||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||||
current_instance = (
|
current_instance = (
|
||||||
@ -777,6 +670,7 @@ def create_cycle_issue_activity(
|
|||||||
comment=f"updated cycle from {old_cycle.name} to {new_cycle.name}",
|
comment=f"updated cycle from {old_cycle.name} to {new_cycle.name}",
|
||||||
old_identifier=old_cycle.id,
|
old_identifier=old_cycle.id,
|
||||||
new_identifier=new_cycle.id,
|
new_identifier=new_cycle.id,
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -797,12 +691,13 @@ def create_cycle_issue_activity(
|
|||||||
workspace=project.workspace,
|
workspace=project.workspace,
|
||||||
comment=f"added cycle {cycle.name}",
|
comment=f"added cycle {cycle.name}",
|
||||||
new_identifier=cycle.id,
|
new_identifier=cycle.id,
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def delete_cycle_issue_activity(
|
def delete_cycle_issue_activity(
|
||||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
):
|
):
|
||||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||||
current_instance = (
|
current_instance = (
|
||||||
@ -826,12 +721,13 @@ def delete_cycle_issue_activity(
|
|||||||
workspace=project.workspace,
|
workspace=project.workspace,
|
||||||
comment=f"removed this issue from {cycle.name if cycle is not None else None}",
|
comment=f"removed this issue from {cycle.name if cycle is not None else None}",
|
||||||
old_identifier=cycle.id if cycle is not None else None,
|
old_identifier=cycle.id if cycle is not None else None,
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def create_module_issue_activity(
|
def create_module_issue_activity(
|
||||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
):
|
):
|
||||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||||
current_instance = (
|
current_instance = (
|
||||||
@ -863,6 +759,7 @@ def create_module_issue_activity(
|
|||||||
comment=f"updated module from {old_module.name} to {new_module.name}",
|
comment=f"updated module from {old_module.name} to {new_module.name}",
|
||||||
old_identifier=old_module.id,
|
old_identifier=old_module.id,
|
||||||
new_identifier=new_module.id,
|
new_identifier=new_module.id,
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -882,12 +779,13 @@ def create_module_issue_activity(
|
|||||||
workspace=project.workspace,
|
workspace=project.workspace,
|
||||||
comment=f"added module {module.name}",
|
comment=f"added module {module.name}",
|
||||||
new_identifier=module.id,
|
new_identifier=module.id,
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def delete_module_issue_activity(
|
def delete_module_issue_activity(
|
||||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
):
|
):
|
||||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||||
current_instance = (
|
current_instance = (
|
||||||
@ -911,12 +809,13 @@ def delete_module_issue_activity(
|
|||||||
workspace=project.workspace,
|
workspace=project.workspace,
|
||||||
comment=f"removed this issue from {module.name if module is not None else None}",
|
comment=f"removed this issue from {module.name if module is not None else None}",
|
||||||
old_identifier=module.id if module is not None else None,
|
old_identifier=module.id if module is not None else None,
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def create_link_activity(
|
def create_link_activity(
|
||||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
):
|
):
|
||||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||||
current_instance = (
|
current_instance = (
|
||||||
@ -934,12 +833,13 @@ def create_link_activity(
|
|||||||
field="link",
|
field="link",
|
||||||
new_value=requested_data.get("url", ""),
|
new_value=requested_data.get("url", ""),
|
||||||
new_identifier=requested_data.get("id", None),
|
new_identifier=requested_data.get("id", None),
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def update_link_activity(
|
def update_link_activity(
|
||||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
):
|
):
|
||||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||||
current_instance = (
|
current_instance = (
|
||||||
@ -960,12 +860,13 @@ def update_link_activity(
|
|||||||
old_identifier=current_instance.get("id"),
|
old_identifier=current_instance.get("id"),
|
||||||
new_value=requested_data.get("url", ""),
|
new_value=requested_data.get("url", ""),
|
||||||
new_identifier=current_instance.get("id", None),
|
new_identifier=current_instance.get("id", None),
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def delete_link_activity(
|
def delete_link_activity(
|
||||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
):
|
):
|
||||||
|
|
||||||
current_instance = (
|
current_instance = (
|
||||||
@ -982,13 +883,14 @@ def delete_link_activity(
|
|||||||
actor=actor,
|
actor=actor,
|
||||||
field="link",
|
field="link",
|
||||||
old_value=current_instance.get("url", ""),
|
old_value=current_instance.get("url", ""),
|
||||||
new_value=""
|
new_value="",
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def create_attachment_activity(
|
def create_attachment_activity(
|
||||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
):
|
):
|
||||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||||
current_instance = (
|
current_instance = (
|
||||||
@ -1006,12 +908,13 @@ def create_attachment_activity(
|
|||||||
field="attachment",
|
field="attachment",
|
||||||
new_value=current_instance.get("asset", ""),
|
new_value=current_instance.get("asset", ""),
|
||||||
new_identifier=current_instance.get("id", None),
|
new_identifier=current_instance.get("id", None),
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def delete_attachment_activity(
|
def delete_attachment_activity(
|
||||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
):
|
):
|
||||||
issue_activities.append(
|
issue_activities.append(
|
||||||
IssueActivity(
|
IssueActivity(
|
||||||
@ -1022,11 +925,12 @@ def delete_attachment_activity(
|
|||||||
verb="deleted",
|
verb="deleted",
|
||||||
actor=actor,
|
actor=actor,
|
||||||
field="attachment",
|
field="attachment",
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def create_issue_reaction_activity(
|
def create_issue_reaction_activity(
|
||||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
):
|
):
|
||||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
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:
|
if requested_data and requested_data.get("reaction") is not None:
|
||||||
@ -1045,12 +949,13 @@ def create_issue_reaction_activity(
|
|||||||
comment="added the reaction",
|
comment="added the reaction",
|
||||||
old_identifier=None,
|
old_identifier=None,
|
||||||
new_identifier=issue_reaction,
|
new_identifier=issue_reaction,
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def delete_issue_reaction_activity(
|
def delete_issue_reaction_activity(
|
||||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
):
|
):
|
||||||
current_instance = (
|
current_instance = (
|
||||||
json.loads(current_instance) if current_instance is not None else None
|
json.loads(current_instance) if current_instance is not None else None
|
||||||
@ -1069,12 +974,13 @@ def delete_issue_reaction_activity(
|
|||||||
comment="removed the reaction",
|
comment="removed the reaction",
|
||||||
old_identifier=current_instance.get("identifier"),
|
old_identifier=current_instance.get("identifier"),
|
||||||
new_identifier=None,
|
new_identifier=None,
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def create_comment_reaction_activity(
|
def create_comment_reaction_activity(
|
||||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
):
|
):
|
||||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
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:
|
if requested_data and requested_data.get("reaction") is not None:
|
||||||
@ -1094,12 +1000,13 @@ def create_comment_reaction_activity(
|
|||||||
comment="added the reaction",
|
comment="added the reaction",
|
||||||
old_identifier=None,
|
old_identifier=None,
|
||||||
new_identifier=comment_reaction_id,
|
new_identifier=comment_reaction_id,
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def delete_comment_reaction_activity(
|
def delete_comment_reaction_activity(
|
||||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
):
|
):
|
||||||
current_instance = (
|
current_instance = (
|
||||||
json.loads(current_instance) if current_instance is not None else None
|
json.loads(current_instance) if current_instance is not None else None
|
||||||
@ -1120,12 +1027,13 @@ def delete_comment_reaction_activity(
|
|||||||
comment="removed the reaction",
|
comment="removed the reaction",
|
||||||
old_identifier=current_instance.get("identifier"),
|
old_identifier=current_instance.get("identifier"),
|
||||||
new_identifier=None,
|
new_identifier=None,
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def create_issue_vote_activity(
|
def create_issue_vote_activity(
|
||||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
):
|
):
|
||||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
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:
|
if requested_data and requested_data.get("vote") is not None:
|
||||||
@ -1142,12 +1050,13 @@ def create_issue_vote_activity(
|
|||||||
comment="added the vote",
|
comment="added the vote",
|
||||||
old_identifier=None,
|
old_identifier=None,
|
||||||
new_identifier=None,
|
new_identifier=None,
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def delete_issue_vote_activity(
|
def delete_issue_vote_activity(
|
||||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
):
|
):
|
||||||
current_instance = (
|
current_instance = (
|
||||||
json.loads(current_instance) if current_instance is not None else None
|
json.loads(current_instance) if current_instance is not None else None
|
||||||
@ -1166,10 +1075,170 @@ def delete_issue_vote_activity(
|
|||||||
comment="removed the vote",
|
comment="removed the vote",
|
||||||
old_identifier=current_instance.get("identifier"),
|
old_identifier=current_instance.get("identifier"),
|
||||||
new_identifier=None,
|
new_identifier=None,
|
||||||
|
epoch=epoch,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create_issue_relation_activity(
|
||||||
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
|
):
|
||||||
|
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||||
|
current_instance = (
|
||||||
|
json.loads(current_instance) if current_instance is not None else None
|
||||||
|
)
|
||||||
|
if current_instance is None and requested_data.get("related_list") is not None:
|
||||||
|
for issue_relation in requested_data.get("related_list"):
|
||||||
|
if issue_relation.get("relation_type") == "blocked_by":
|
||||||
|
relation_type = "blocking"
|
||||||
|
else:
|
||||||
|
relation_type = issue_relation.get("relation_type")
|
||||||
|
issue = Issue.objects.get(pk=issue_relation.get("issue"))
|
||||||
|
issue_activities.append(
|
||||||
|
IssueActivity(
|
||||||
|
issue_id=issue_relation.get("related_issue"),
|
||||||
|
actor=actor,
|
||||||
|
verb="created",
|
||||||
|
old_value="",
|
||||||
|
new_value=f"{project.identifier}-{issue.sequence_id}",
|
||||||
|
field=relation_type,
|
||||||
|
project=project,
|
||||||
|
workspace=project.workspace,
|
||||||
|
comment=f'added {relation_type} relation',
|
||||||
|
old_identifier=issue_relation.get("issue"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
issue = Issue.objects.get(pk=issue_relation.get("related_issue"))
|
||||||
|
issue_activities.append(
|
||||||
|
IssueActivity(
|
||||||
|
issue_id=issue_relation.get("issue"),
|
||||||
|
actor=actor,
|
||||||
|
verb="created",
|
||||||
|
old_value="",
|
||||||
|
new_value=f"{project.identifier}-{issue.sequence_id}",
|
||||||
|
field=f'{issue_relation.get("relation_type")}',
|
||||||
|
project=project,
|
||||||
|
workspace=project.workspace,
|
||||||
|
comment=f'added {issue_relation.get("relation_type")} relation',
|
||||||
|
old_identifier=issue_relation.get("related_issue"),
|
||||||
|
epoch=epoch,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def delete_issue_relation_activity(
|
||||||
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
|
):
|
||||||
|
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||||
|
current_instance = (
|
||||||
|
json.loads(current_instance) if current_instance is not None else None
|
||||||
|
)
|
||||||
|
if current_instance is not None and requested_data.get("related_list") is None:
|
||||||
|
if current_instance.get("relation_type") == "blocked_by":
|
||||||
|
relation_type = "blocking"
|
||||||
|
else:
|
||||||
|
relation_type = current_instance.get("relation_type")
|
||||||
|
issue = Issue.objects.get(pk=current_instance.get("issue"))
|
||||||
|
issue_activities.append(
|
||||||
|
IssueActivity(
|
||||||
|
issue_id=current_instance.get("related_issue"),
|
||||||
|
actor=actor,
|
||||||
|
verb="deleted",
|
||||||
|
old_value=f"{project.identifier}-{issue.sequence_id}",
|
||||||
|
new_value="",
|
||||||
|
field=relation_type,
|
||||||
|
project=project,
|
||||||
|
workspace=project.workspace,
|
||||||
|
comment=f'deleted {relation_type} relation',
|
||||||
|
old_identifier=current_instance.get("issue"),
|
||||||
|
epoch=epoch,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
issue = Issue.objects.get(pk=current_instance.get("related_issue"))
|
||||||
|
issue_activities.append(
|
||||||
|
IssueActivity(
|
||||||
|
issue_id=current_instance.get("issue"),
|
||||||
|
actor=actor,
|
||||||
|
verb="deleted",
|
||||||
|
old_value=f"{project.identifier}-{issue.sequence_id}",
|
||||||
|
new_value="",
|
||||||
|
field=f'{current_instance.get("relation_type")}',
|
||||||
|
project=project,
|
||||||
|
workspace=project.workspace,
|
||||||
|
comment=f'deleted {current_instance.get("relation_type")} relation',
|
||||||
|
old_identifier=current_instance.get("related_issue"),
|
||||||
|
epoch=epoch,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create_draft_issue_activity(
|
||||||
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
|
):
|
||||||
|
issue_activities.append(
|
||||||
|
IssueActivity(
|
||||||
|
issue_id=issue_id,
|
||||||
|
project=project,
|
||||||
|
workspace=project.workspace,
|
||||||
|
comment=f"drafted the issue",
|
||||||
|
field="draft",
|
||||||
|
verb="created",
|
||||||
|
actor=actor,
|
||||||
|
epoch=epoch,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def update_draft_issue_activity(
|
||||||
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
|
):
|
||||||
|
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||||
|
current_instance = (
|
||||||
|
json.loads(current_instance) if current_instance is not None else None
|
||||||
|
)
|
||||||
|
if requested_data.get("is_draft") is not None and requested_data.get("is_draft") == False:
|
||||||
|
issue_activities.append(
|
||||||
|
IssueActivity(
|
||||||
|
issue_id=issue_id,
|
||||||
|
project=project,
|
||||||
|
workspace=project.workspace,
|
||||||
|
comment=f"created the issue",
|
||||||
|
verb="updated",
|
||||||
|
actor=actor,
|
||||||
|
epoch=epoch,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
issue_activities.append(
|
||||||
|
IssueActivity(
|
||||||
|
issue_id=issue_id,
|
||||||
|
project=project,
|
||||||
|
workspace=project.workspace,
|
||||||
|
comment=f"updated the draft issue",
|
||||||
|
field="draft",
|
||||||
|
verb="updated",
|
||||||
|
actor=actor,
|
||||||
|
epoch=epoch,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def delete_draft_issue_activity(
|
||||||
|
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||||
|
):
|
||||||
|
issue_activities.append(
|
||||||
|
IssueActivity(
|
||||||
|
project=project,
|
||||||
|
workspace=project.workspace,
|
||||||
|
comment=f"deleted the draft issue",
|
||||||
|
field="draft",
|
||||||
|
verb="deleted",
|
||||||
|
actor=actor,
|
||||||
|
epoch=epoch,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
# Receive message from room group
|
# Receive message from room group
|
||||||
@shared_task
|
@shared_task
|
||||||
def issue_activity(
|
def issue_activity(
|
||||||
@ -1179,6 +1248,7 @@ def issue_activity(
|
|||||||
issue_id,
|
issue_id,
|
||||||
actor_id,
|
actor_id,
|
||||||
project_id,
|
project_id,
|
||||||
|
epoch,
|
||||||
subscriber=True,
|
subscriber=True,
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
@ -1233,12 +1303,17 @@ def issue_activity(
|
|||||||
"link.activity.deleted": delete_link_activity,
|
"link.activity.deleted": delete_link_activity,
|
||||||
"attachment.activity.created": create_attachment_activity,
|
"attachment.activity.created": create_attachment_activity,
|
||||||
"attachment.activity.deleted": delete_attachment_activity,
|
"attachment.activity.deleted": delete_attachment_activity,
|
||||||
|
"issue_relation.activity.created": create_issue_relation_activity,
|
||||||
|
"issue_relation.activity.deleted": delete_issue_relation_activity,
|
||||||
"issue_reaction.activity.created": create_issue_reaction_activity,
|
"issue_reaction.activity.created": create_issue_reaction_activity,
|
||||||
"issue_reaction.activity.deleted": delete_issue_reaction_activity,
|
"issue_reaction.activity.deleted": delete_issue_reaction_activity,
|
||||||
"comment_reaction.activity.created": create_comment_reaction_activity,
|
"comment_reaction.activity.created": create_comment_reaction_activity,
|
||||||
"comment_reaction.activity.deleted": delete_comment_reaction_activity,
|
"comment_reaction.activity.deleted": delete_comment_reaction_activity,
|
||||||
"issue_vote.activity.created": create_issue_vote_activity,
|
"issue_vote.activity.created": create_issue_vote_activity,
|
||||||
"issue_vote.activity.deleted": delete_issue_vote_activity,
|
"issue_vote.activity.deleted": delete_issue_vote_activity,
|
||||||
|
"issue_draft.activity.created": create_draft_issue_activity,
|
||||||
|
"issue_draft.activity.updated": update_draft_issue_activity,
|
||||||
|
"issue_draft.activity.deleted": delete_draft_issue_activity,
|
||||||
}
|
}
|
||||||
|
|
||||||
func = ACTIVITY_MAPPER.get(type)
|
func = ACTIVITY_MAPPER.get(type)
|
||||||
@ -1250,6 +1325,7 @@ def issue_activity(
|
|||||||
project,
|
project,
|
||||||
actor,
|
actor,
|
||||||
issue_activities,
|
issue_activities,
|
||||||
|
epoch,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Save all the values to database
|
# Save all the values to database
|
||||||
@ -1313,7 +1389,7 @@ def issue_activity(
|
|||||||
):
|
):
|
||||||
issue_subscribers = issue_subscribers + [issue.created_by_id]
|
issue_subscribers = issue_subscribers + [issue.created_by_id]
|
||||||
|
|
||||||
for subscriber in issue_subscribers:
|
for subscriber in list(set(issue_subscribers)):
|
||||||
for issue_activity in issue_activities_created:
|
for issue_activity in issue_activities_created:
|
||||||
bulk_notifications.append(
|
bulk_notifications.append(
|
||||||
Notification(
|
Notification(
|
||||||
|
@ -58,27 +58,31 @@ def archive_old_issues():
|
|||||||
|
|
||||||
# Check if Issues
|
# Check if Issues
|
||||||
if issues:
|
if issues:
|
||||||
|
# Set the archive time to current time
|
||||||
|
archive_at = timezone.now()
|
||||||
|
|
||||||
issues_to_update = []
|
issues_to_update = []
|
||||||
for issue in issues:
|
for issue in issues:
|
||||||
issue.archived_at = timezone.now()
|
issue.archived_at = archive_at
|
||||||
issues_to_update.append(issue)
|
issues_to_update.append(issue)
|
||||||
|
|
||||||
# Bulk Update the issues and log the activity
|
# Bulk Update the issues and log the activity
|
||||||
if issues_to_update:
|
if issues_to_update:
|
||||||
updated_issues = Issue.objects.bulk_update(
|
Issue.objects.bulk_update(
|
||||||
issues_to_update, ["archived_at"], batch_size=100
|
issues_to_update, ["archived_at"], batch_size=100
|
||||||
)
|
)
|
||||||
[
|
[
|
||||||
issue_activity.delay(
|
issue_activity.delay(
|
||||||
type="issue.activity.updated",
|
type="issue.activity.updated",
|
||||||
requested_data=json.dumps({"archived_at": str(issue.archived_at)}),
|
requested_data=json.dumps({"archived_at": str(archive_at)}),
|
||||||
actor_id=str(project.created_by_id),
|
actor_id=str(project.created_by_id),
|
||||||
issue_id=issue.id,
|
issue_id=issue.id,
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
subscriber=False,
|
subscriber=False,
|
||||||
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
for issue in updated_issues
|
for issue in issues_to_update
|
||||||
]
|
]
|
||||||
return
|
return
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -138,7 +142,7 @@ def close_old_issues():
|
|||||||
|
|
||||||
# Bulk Update the issues and log the activity
|
# Bulk Update the issues and log the activity
|
||||||
if issues_to_update:
|
if issues_to_update:
|
||||||
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(
|
issue_activity.delay(
|
||||||
type="issue.activity.updated",
|
type="issue.activity.updated",
|
||||||
@ -148,8 +152,9 @@ def close_old_issues():
|
|||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
subscriber=False,
|
subscriber=False,
|
||||||
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
for issue in updated_issues
|
for issue in issues_to_update
|
||||||
]
|
]
|
||||||
return
|
return
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
@ -0,0 +1,83 @@
|
|||||||
|
# Generated by Django 4.2.3 on 2023-09-12 07:29
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
from plane.db.models import IssueRelation
|
||||||
|
from sentry_sdk import capture_exception
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
def create_issue_relation(apps, schema_editor):
|
||||||
|
try:
|
||||||
|
IssueBlockerModel = apps.get_model("db", "IssueBlocker")
|
||||||
|
updated_issue_relation = []
|
||||||
|
for blocked_issue in IssueBlockerModel.objects.all():
|
||||||
|
updated_issue_relation.append(
|
||||||
|
IssueRelation(
|
||||||
|
issue_id=blocked_issue.block_id,
|
||||||
|
related_issue_id=blocked_issue.blocked_by_id,
|
||||||
|
relation_type="blocked_by",
|
||||||
|
project_id=blocked_issue.project_id,
|
||||||
|
workspace_id=blocked_issue.workspace_id,
|
||||||
|
created_by_id=blocked_issue.created_by_id,
|
||||||
|
updated_by_id=blocked_issue.updated_by_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
IssueRelation.objects.bulk_create(updated_issue_relation, batch_size=100)
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
capture_exception(e)
|
||||||
|
|
||||||
|
|
||||||
|
def update_issue_priority_choice(apps, schema_editor):
|
||||||
|
IssueModel = apps.get_model("db", "Issue")
|
||||||
|
updated_issues = []
|
||||||
|
for obj in IssueModel.objects.filter(priority=None):
|
||||||
|
obj.priority = "none"
|
||||||
|
updated_issues.append(obj)
|
||||||
|
IssueModel.objects.bulk_update(updated_issues, ["priority"], batch_size=100)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('db', '0042_alter_analyticview_created_by_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='IssueRelation',
|
||||||
|
fields=[
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
|
||||||
|
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
|
||||||
|
('relation_type', models.CharField(choices=[('duplicate', 'Duplicate'), ('relates_to', 'Relates To'), ('blocked_by', 'Blocked By')], default='blocked_by', max_length=20, verbose_name='Issue Relation Type')),
|
||||||
|
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
|
||||||
|
('issue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_relation', to='db.issue')),
|
||||||
|
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project')),
|
||||||
|
('related_issue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_related', to='db.issue')),
|
||||||
|
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
|
||||||
|
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Issue Relation',
|
||||||
|
'verbose_name_plural': 'Issue Relations',
|
||||||
|
'db_table': 'issue_relations',
|
||||||
|
'ordering': ('-created_at',),
|
||||||
|
'unique_together': {('issue', 'related_issue')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='issue',
|
||||||
|
name='is_draft',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issue',
|
||||||
|
name='priority',
|
||||||
|
field=models.CharField(choices=[('urgent', 'Urgent'), ('high', 'High'), ('medium', 'Medium'), ('low', 'Low'), ('none', 'None')], default='none', max_length=30, verbose_name='Issue Priority'),
|
||||||
|
),
|
||||||
|
migrations.RunPython(create_issue_relation),
|
||||||
|
migrations.RunPython(update_issue_priority_choice),
|
||||||
|
]
|
138
apiserver/plane/db/migrations/0044_auto_20230913_0709.py
Normal file
138
apiserver/plane/db/migrations/0044_auto_20230913_0709.py
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
# Generated by Django 4.2.3 on 2023-09-13 07:09
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def workspace_member_props(old_props):
|
||||||
|
new_props = {
|
||||||
|
"filters": {
|
||||||
|
"priority": old_props.get("filters", {}).get("priority", None),
|
||||||
|
"state": old_props.get("filters", {}).get("state", None),
|
||||||
|
"state_group": old_props.get("filters", {}).get("state_group", None),
|
||||||
|
"assignees": old_props.get("filters", {}).get("assignees", None),
|
||||||
|
"created_by": old_props.get("filters", {}).get("created_by", None),
|
||||||
|
"labels": old_props.get("filters", {}).get("labels", None),
|
||||||
|
"start_date": old_props.get("filters", {}).get("start_date", None),
|
||||||
|
"target_date": old_props.get("filters", {}).get("target_date", None),
|
||||||
|
"subscriber": old_props.get("filters", {}).get("subscriber", None),
|
||||||
|
},
|
||||||
|
"display_filters": {
|
||||||
|
"group_by": old_props.get("groupByProperty", None),
|
||||||
|
"order_by": old_props.get("orderBy", "-created_at"),
|
||||||
|
"type": old_props.get("filters", {}).get("type", None),
|
||||||
|
"sub_issue": old_props.get("showSubIssues", True),
|
||||||
|
"show_empty_groups": old_props.get("showEmptyGroups", True),
|
||||||
|
"layout": old_props.get("issueView", "list"),
|
||||||
|
"calendar_date_range": old_props.get("calendarDateRange", ""),
|
||||||
|
},
|
||||||
|
"display_properties": {
|
||||||
|
"assignee": old_props.get("properties", {}).get("assignee", True),
|
||||||
|
"attachment_count": old_props.get("properties", {}).get("attachment_count", True),
|
||||||
|
"created_on": old_props.get("properties", {}).get("created_on", True),
|
||||||
|
"due_date": old_props.get("properties", {}).get("due_date", True),
|
||||||
|
"estimate": old_props.get("properties", {}).get("estimate", True),
|
||||||
|
"key": old_props.get("properties", {}).get("key", True),
|
||||||
|
"labels": old_props.get("properties", {}).get("labels", True),
|
||||||
|
"link": old_props.get("properties", {}).get("link", True),
|
||||||
|
"priority": old_props.get("properties", {}).get("priority", True),
|
||||||
|
"start_date": old_props.get("properties", {}).get("start_date", True),
|
||||||
|
"state": old_props.get("properties", {}).get("state", True),
|
||||||
|
"sub_issue_count": old_props.get("properties", {}).get("sub_issue_count", True),
|
||||||
|
"updated_on": old_props.get("properties", {}).get("updated_on", True),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return new_props
|
||||||
|
|
||||||
|
|
||||||
|
def project_member_props(old_props):
|
||||||
|
new_props = {
|
||||||
|
"filters": {
|
||||||
|
"priority": old_props.get("filters", {}).get("priority", None),
|
||||||
|
"state": old_props.get("filters", {}).get("state", None),
|
||||||
|
"state_group": old_props.get("filters", {}).get("state_group", None),
|
||||||
|
"assignees": old_props.get("filters", {}).get("assignees", None),
|
||||||
|
"created_by": old_props.get("filters", {}).get("created_by", None),
|
||||||
|
"labels": old_props.get("filters", {}).get("labels", None),
|
||||||
|
"start_date": old_props.get("filters", {}).get("start_date", None),
|
||||||
|
"target_date": old_props.get("filters", {}).get("target_date", None),
|
||||||
|
"subscriber": old_props.get("filters", {}).get("subscriber", None),
|
||||||
|
},
|
||||||
|
"display_filters": {
|
||||||
|
"group_by": old_props.get("groupByProperty", None),
|
||||||
|
"order_by": old_props.get("orderBy", "-created_at"),
|
||||||
|
"type": old_props.get("filters", {}).get("type", None),
|
||||||
|
"sub_issue": old_props.get("showSubIssues", True),
|
||||||
|
"show_empty_groups": old_props.get("showEmptyGroups", True),
|
||||||
|
"layout": old_props.get("issueView", "list"),
|
||||||
|
"calendar_date_range": old_props.get("calendarDateRange", ""),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return new_props
|
||||||
|
|
||||||
|
|
||||||
|
def cycle_module_props(old_props):
|
||||||
|
new_props = {
|
||||||
|
"filters": {
|
||||||
|
"priority": old_props.get("filters", {}).get("priority", None),
|
||||||
|
"state": old_props.get("filters", {}).get("state", None),
|
||||||
|
"state_group": old_props.get("filters", {}).get("state_group", None),
|
||||||
|
"assignees": old_props.get("filters", {}).get("assignees", None),
|
||||||
|
"created_by": old_props.get("filters", {}).get("created_by", None),
|
||||||
|
"labels": old_props.get("filters", {}).get("labels", None),
|
||||||
|
"start_date": old_props.get("filters", {}).get("start_date", None),
|
||||||
|
"target_date": old_props.get("filters", {}).get("target_date", None),
|
||||||
|
"subscriber": old_props.get("filters", {}).get("subscriber", None),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return new_props
|
||||||
|
|
||||||
|
|
||||||
|
def update_workspace_member_view_props(apps, schema_editor):
|
||||||
|
WorkspaceMemberModel = apps.get_model("db", "WorkspaceMember")
|
||||||
|
updated_workspace_member = []
|
||||||
|
for obj in WorkspaceMemberModel.objects.all():
|
||||||
|
obj.view_props = workspace_member_props(obj.view_props)
|
||||||
|
obj.default_props = workspace_member_props(obj.default_props)
|
||||||
|
updated_workspace_member.append(obj)
|
||||||
|
WorkspaceMemberModel.objects.bulk_update(updated_workspace_member, ["view_props", "default_props"], batch_size=100)
|
||||||
|
|
||||||
|
def update_project_member_view_props(apps, schema_editor):
|
||||||
|
ProjectMemberModel = apps.get_model("db", "ProjectMember")
|
||||||
|
updated_project_member = []
|
||||||
|
for obj in ProjectMemberModel.objects.all():
|
||||||
|
obj.view_props = project_member_props(obj.view_props)
|
||||||
|
obj.default_props = project_member_props(obj.default_props)
|
||||||
|
updated_project_member.append(obj)
|
||||||
|
ProjectMemberModel.objects.bulk_update(updated_project_member, ["view_props", "default_props"], batch_size=100)
|
||||||
|
|
||||||
|
def update_cycle_props(apps, schema_editor):
|
||||||
|
CycleModel = apps.get_model("db", "Cycle")
|
||||||
|
updated_cycle = []
|
||||||
|
for obj in CycleModel.objects.all():
|
||||||
|
if "filter" in obj.view_props:
|
||||||
|
obj.view_props = cycle_module_props(obj.view_props)
|
||||||
|
updated_cycle.append(obj)
|
||||||
|
CycleModel.objects.bulk_update(updated_cycle, ["view_props"], batch_size=100)
|
||||||
|
|
||||||
|
def update_module_props(apps, schema_editor):
|
||||||
|
ModuleModel = apps.get_model("db", "Module")
|
||||||
|
updated_module = []
|
||||||
|
for obj in ModuleModel.objects.all():
|
||||||
|
if "filter" in obj.view_props:
|
||||||
|
obj.view_props = cycle_module_props(obj.view_props)
|
||||||
|
updated_module.append(obj)
|
||||||
|
ModuleModel.objects.bulk_update(updated_module, ["view_props"], batch_size=100)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('db', '0043_alter_analyticview_created_by_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(update_workspace_member_view_props),
|
||||||
|
migrations.RunPython(update_project_member_view_props),
|
||||||
|
migrations.RunPython(update_cycle_props),
|
||||||
|
migrations.RunPython(update_module_props),
|
||||||
|
]
|
@ -0,0 +1,79 @@
|
|||||||
|
# Generated by Django 4.2.5 on 2023-09-29 10:14
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import plane.db.models.workspace
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
def update_issue_activity_priority(apps, schema_editor):
|
||||||
|
IssueActivity = apps.get_model("db", "IssueActivity")
|
||||||
|
updated_issue_activity = []
|
||||||
|
for obj in IssueActivity.objects.filter(field="priority"):
|
||||||
|
# Set the old and new value to none if it is empty for Priority
|
||||||
|
obj.new_value = obj.new_value or "none"
|
||||||
|
obj.old_value = obj.old_value or "none"
|
||||||
|
updated_issue_activity.append(obj)
|
||||||
|
IssueActivity.objects.bulk_update(
|
||||||
|
updated_issue_activity,
|
||||||
|
["new_value", "old_value"],
|
||||||
|
batch_size=2000,
|
||||||
|
)
|
||||||
|
|
||||||
|
def update_issue_activity_blocked(apps, schema_editor):
|
||||||
|
IssueActivity = apps.get_model("db", "IssueActivity")
|
||||||
|
updated_issue_activity = []
|
||||||
|
for obj in IssueActivity.objects.filter(field="blocks"):
|
||||||
|
# Set the field to blocked_by
|
||||||
|
obj.field = "blocked_by"
|
||||||
|
updated_issue_activity.append(obj)
|
||||||
|
IssueActivity.objects.bulk_update(
|
||||||
|
updated_issue_activity,
|
||||||
|
["field"],
|
||||||
|
batch_size=1000,
|
||||||
|
)
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('db', '0044_auto_20230913_0709'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='GlobalView',
|
||||||
|
fields=[
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
|
||||||
|
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
|
||||||
|
('name', models.CharField(max_length=255, verbose_name='View Name')),
|
||||||
|
('description', models.TextField(blank=True, verbose_name='View Description')),
|
||||||
|
('query', models.JSONField(verbose_name='View Query')),
|
||||||
|
('access', models.PositiveSmallIntegerField(choices=[(0, 'Private'), (1, 'Public')], default=1)),
|
||||||
|
('query_data', models.JSONField(default=dict)),
|
||||||
|
('sort_order', models.FloatField(default=65535)),
|
||||||
|
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
|
||||||
|
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
|
||||||
|
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='global_views', to='db.workspace')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Global View',
|
||||||
|
'verbose_name_plural': 'Global Views',
|
||||||
|
'db_table': 'global_views',
|
||||||
|
'ordering': ('-created_at',),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='workspacemember',
|
||||||
|
name='issue_props',
|
||||||
|
field=models.JSONField(default=plane.db.models.workspace.get_issue_props),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='issueactivity',
|
||||||
|
name='epoch',
|
||||||
|
field=models.FloatField(null=True),
|
||||||
|
),
|
||||||
|
migrations.RunPython(update_issue_activity_priority),
|
||||||
|
migrations.RunPython(update_issue_activity_blocked),
|
||||||
|
]
|
@ -32,6 +32,7 @@ from .issue import (
|
|||||||
IssueAssignee,
|
IssueAssignee,
|
||||||
Label,
|
Label,
|
||||||
IssueBlocker,
|
IssueBlocker,
|
||||||
|
IssueRelation,
|
||||||
IssueLink,
|
IssueLink,
|
||||||
IssueSequence,
|
IssueSequence,
|
||||||
IssueAttachment,
|
IssueAttachment,
|
||||||
@ -49,7 +50,7 @@ from .state import State
|
|||||||
|
|
||||||
from .cycle import Cycle, CycleIssue, CycleFavorite
|
from .cycle import Cycle, CycleIssue, CycleFavorite
|
||||||
|
|
||||||
from .view import IssueView, IssueViewFavorite
|
from .view import GlobalView, IssueView, IssueViewFavorite
|
||||||
|
|
||||||
from .module import Module, ModuleMember, ModuleIssue, ModuleLink, ModuleFavorite
|
from .module import Module, ModuleMember, ModuleIssue, ModuleLink, ModuleFavorite
|
||||||
|
|
||||||
|
@ -29,6 +29,7 @@ class IssueManager(models.Manager):
|
|||||||
| models.Q(issue_inbox__isnull=True)
|
| models.Q(issue_inbox__isnull=True)
|
||||||
)
|
)
|
||||||
.exclude(archived_at__isnull=False)
|
.exclude(archived_at__isnull=False)
|
||||||
|
.exclude(is_draft=True)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -38,6 +39,7 @@ class Issue(ProjectBaseModel):
|
|||||||
("high", "High"),
|
("high", "High"),
|
||||||
("medium", "Medium"),
|
("medium", "Medium"),
|
||||||
("low", "Low"),
|
("low", "Low"),
|
||||||
|
("none", "None")
|
||||||
)
|
)
|
||||||
parent = models.ForeignKey(
|
parent = models.ForeignKey(
|
||||||
"self",
|
"self",
|
||||||
@ -64,8 +66,7 @@ class Issue(ProjectBaseModel):
|
|||||||
max_length=30,
|
max_length=30,
|
||||||
choices=PRIORITY_CHOICES,
|
choices=PRIORITY_CHOICES,
|
||||||
verbose_name="Issue Priority",
|
verbose_name="Issue Priority",
|
||||||
null=True,
|
default="none",
|
||||||
blank=True,
|
|
||||||
)
|
)
|
||||||
start_date = models.DateField(null=True, blank=True)
|
start_date = models.DateField(null=True, blank=True)
|
||||||
target_date = models.DateField(null=True, blank=True)
|
target_date = models.DateField(null=True, blank=True)
|
||||||
@ -83,6 +84,7 @@ class Issue(ProjectBaseModel):
|
|||||||
sort_order = models.FloatField(default=65535)
|
sort_order = models.FloatField(default=65535)
|
||||||
completed_at = models.DateTimeField(null=True)
|
completed_at = models.DateTimeField(null=True)
|
||||||
archived_at = models.DateField(null=True)
|
archived_at = models.DateField(null=True)
|
||||||
|
is_draft = models.BooleanField(default=False)
|
||||||
|
|
||||||
objects = models.Manager()
|
objects = models.Manager()
|
||||||
issue_objects = IssueManager()
|
issue_objects = IssueManager()
|
||||||
@ -178,6 +180,37 @@ class IssueBlocker(ProjectBaseModel):
|
|||||||
return f"{self.block.name} {self.blocked_by.name}"
|
return f"{self.block.name} {self.blocked_by.name}"
|
||||||
|
|
||||||
|
|
||||||
|
class IssueRelation(ProjectBaseModel):
|
||||||
|
RELATION_CHOICES = (
|
||||||
|
("duplicate", "Duplicate"),
|
||||||
|
("relates_to", "Relates To"),
|
||||||
|
("blocked_by", "Blocked By"),
|
||||||
|
)
|
||||||
|
|
||||||
|
issue = models.ForeignKey(
|
||||||
|
Issue, related_name="issue_relation", on_delete=models.CASCADE
|
||||||
|
)
|
||||||
|
related_issue = models.ForeignKey(
|
||||||
|
Issue, related_name="issue_related", on_delete=models.CASCADE
|
||||||
|
)
|
||||||
|
relation_type = models.CharField(
|
||||||
|
max_length=20,
|
||||||
|
choices=RELATION_CHOICES,
|
||||||
|
verbose_name="Issue Relation Type",
|
||||||
|
default="blocked_by",
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = ["issue", "related_issue"]
|
||||||
|
verbose_name = "Issue Relation"
|
||||||
|
verbose_name_plural = "Issue Relations"
|
||||||
|
db_table = "issue_relations"
|
||||||
|
ordering = ("-created_at",)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.issue.name} {self.related_issue.name}"
|
||||||
|
|
||||||
|
|
||||||
class IssueAssignee(ProjectBaseModel):
|
class IssueAssignee(ProjectBaseModel):
|
||||||
issue = models.ForeignKey(
|
issue = models.ForeignKey(
|
||||||
Issue, on_delete=models.CASCADE, related_name="issue_assignee"
|
Issue, on_delete=models.CASCADE, related_name="issue_assignee"
|
||||||
@ -276,6 +309,7 @@ class IssueActivity(ProjectBaseModel):
|
|||||||
)
|
)
|
||||||
old_identifier = models.UUIDField(null=True)
|
old_identifier = models.UUIDField(null=True)
|
||||||
new_identifier = models.UUIDField(null=True)
|
new_identifier = models.UUIDField(null=True)
|
||||||
|
epoch = models.FloatField(null=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "Issue Activity"
|
verbose_name = "Issue Activity"
|
||||||
|
@ -25,13 +25,26 @@ ROLE_CHOICES = (
|
|||||||
|
|
||||||
def get_default_props():
|
def get_default_props():
|
||||||
return {
|
return {
|
||||||
"filters": {"type": None},
|
"filters": {
|
||||||
"orderBy": "-created_at",
|
"priority": None,
|
||||||
"collapsed": True,
|
"state": None,
|
||||||
"issueView": "list",
|
"state_group": None,
|
||||||
"filterIssue": None,
|
"assignees": None,
|
||||||
"groupByProperty": None,
|
"created_by": None,
|
||||||
"showEmptyGroups": True,
|
"labels": None,
|
||||||
|
"start_date": None,
|
||||||
|
"target_date": None,
|
||||||
|
"subscriber": None,
|
||||||
|
},
|
||||||
|
"display_filters": {
|
||||||
|
"group_by": None,
|
||||||
|
"order_by": '-created_at',
|
||||||
|
"type": None,
|
||||||
|
"sub_issue": True,
|
||||||
|
"show_empty_groups": True,
|
||||||
|
"layout": "list",
|
||||||
|
"calendar_date_range": "",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -3,7 +3,41 @@ from django.db import models
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
# Module import
|
# Module import
|
||||||
from . import ProjectBaseModel
|
from . import ProjectBaseModel, BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class GlobalView(BaseModel):
|
||||||
|
workspace = models.ForeignKey(
|
||||||
|
"db.Workspace", on_delete=models.CASCADE, related_name="global_views"
|
||||||
|
)
|
||||||
|
name = models.CharField(max_length=255, verbose_name="View Name")
|
||||||
|
description = models.TextField(verbose_name="View Description", blank=True)
|
||||||
|
query = models.JSONField(verbose_name="View Query")
|
||||||
|
access = models.PositiveSmallIntegerField(
|
||||||
|
default=1, choices=((0, "Private"), (1, "Public"))
|
||||||
|
)
|
||||||
|
query_data = models.JSONField(default=dict)
|
||||||
|
sort_order = models.FloatField(default=65535)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Global View"
|
||||||
|
verbose_name_plural = "Global Views"
|
||||||
|
db_table = "global_views"
|
||||||
|
ordering = ("-created_at",)
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if self._state.adding:
|
||||||
|
largest_sort_order = GlobalView.objects.filter(
|
||||||
|
workspace=self.workspace
|
||||||
|
).aggregate(largest=models.Max("sort_order"))["largest"]
|
||||||
|
if largest_sort_order is not None:
|
||||||
|
self.sort_order = largest_sort_order + 10000
|
||||||
|
|
||||||
|
super(GlobalView, self).save(*args, **kwargs)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
"""Return name of the View"""
|
||||||
|
return f"{self.name} <{self.workspace.name}>"
|
||||||
|
|
||||||
|
|
||||||
class IssueView(ProjectBaseModel):
|
class IssueView(ProjectBaseModel):
|
||||||
|
@ -16,26 +16,50 @@ ROLE_CHOICES = (
|
|||||||
|
|
||||||
def get_default_props():
|
def get_default_props():
|
||||||
return {
|
return {
|
||||||
"filters": {"type": None},
|
"filters": {
|
||||||
"groupByProperty": None,
|
"priority": None,
|
||||||
"issueView": "list",
|
"state": None,
|
||||||
"orderBy": "-created_at",
|
"state_group": None,
|
||||||
"properties": {
|
"assignees": None,
|
||||||
|
"created_by": None,
|
||||||
|
"labels": None,
|
||||||
|
"start_date": None,
|
||||||
|
"target_date": None,
|
||||||
|
"subscriber": None,
|
||||||
|
},
|
||||||
|
"display_filters": {
|
||||||
|
"group_by": None,
|
||||||
|
"order_by": "-created_at",
|
||||||
|
"type": None,
|
||||||
|
"sub_issue": True,
|
||||||
|
"show_empty_groups": True,
|
||||||
|
"layout": "list",
|
||||||
|
"calendar_date_range": "",
|
||||||
|
},
|
||||||
|
"display_properties": {
|
||||||
"assignee": True,
|
"assignee": True,
|
||||||
|
"attachment_count": True,
|
||||||
|
"created_on": True,
|
||||||
"due_date": True,
|
"due_date": True,
|
||||||
|
"estimate": True,
|
||||||
"key": True,
|
"key": True,
|
||||||
"labels": True,
|
"labels": True,
|
||||||
|
"link": True,
|
||||||
"priority": True,
|
"priority": True,
|
||||||
|
"start_date": True,
|
||||||
"state": True,
|
"state": True,
|
||||||
"sub_issue_count": True,
|
"sub_issue_count": True,
|
||||||
"attachment_count": True,
|
|
||||||
"link": True,
|
|
||||||
"estimate": True,
|
|
||||||
"created_on": True,
|
|
||||||
"updated_on": True,
|
"updated_on": True,
|
||||||
"start_date": True,
|
}
|
||||||
},
|
}
|
||||||
"showEmptyGroups": True,
|
|
||||||
|
|
||||||
|
def get_issue_props():
|
||||||
|
return {
|
||||||
|
"subscribed": True,
|
||||||
|
"assigned": True,
|
||||||
|
"created": True,
|
||||||
|
"all_issues": True,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -74,6 +98,7 @@ class WorkspaceMember(BaseModel):
|
|||||||
company_role = models.TextField(null=True, blank=True)
|
company_role = models.TextField(null=True, blank=True)
|
||||||
view_props = models.JSONField(default=get_default_props)
|
view_props = models.JSONField(default=get_default_props)
|
||||||
default_props = models.JSONField(default=get_default_props)
|
default_props = models.JSONField(default=get_default_props)
|
||||||
|
issue_props = models.JSONField(default=get_issue_props)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = ["workspace", "member"]
|
unique_together = ["workspace", "member"]
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
"""Production settings and globals."""
|
"""Production settings and globals."""
|
||||||
from urllib.parse import urlparse
|
|
||||||
import ssl
|
import ssl
|
||||||
import certifi
|
import certifi
|
||||||
|
|
||||||
import dj_database_url
|
import dj_database_url
|
||||||
from urllib.parse import urlparse
|
|
||||||
|
|
||||||
import sentry_sdk
|
import sentry_sdk
|
||||||
from sentry_sdk.integrations.django import DjangoIntegration
|
from sentry_sdk.integrations.django import DjangoIntegration
|
||||||
@ -91,112 +89,89 @@ if bool(os.environ.get("SENTRY_DSN", False)):
|
|||||||
profiles_sample_rate=1.0,
|
profiles_sample_rate=1.0,
|
||||||
)
|
)
|
||||||
|
|
||||||
if DOCKERIZED and USE_MINIO:
|
# The AWS region to connect to.
|
||||||
INSTALLED_APPS += ("storages",)
|
AWS_REGION = os.environ.get("AWS_REGION", "")
|
||||||
STORAGES["default"] = {"BACKEND": "storages.backends.s3boto3.S3Boto3Storage"}
|
|
||||||
# The AWS access key to use.
|
|
||||||
AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID", "access-key")
|
|
||||||
# The AWS secret access key to use.
|
|
||||||
AWS_SECRET_ACCESS_KEY = os.environ.get("AWS_SECRET_ACCESS_KEY", "secret-key")
|
|
||||||
# The name of the bucket to store files in.
|
|
||||||
AWS_STORAGE_BUCKET_NAME = os.environ.get("AWS_S3_BUCKET_NAME", "uploads")
|
|
||||||
# The full URL to the S3 endpoint. Leave blank to use the default region URL.
|
|
||||||
AWS_S3_ENDPOINT_URL = os.environ.get(
|
|
||||||
"AWS_S3_ENDPOINT_URL", "http://plane-minio:9000"
|
|
||||||
)
|
|
||||||
# Default permissions
|
|
||||||
AWS_DEFAULT_ACL = "public-read"
|
|
||||||
AWS_QUERYSTRING_AUTH = False
|
|
||||||
AWS_S3_FILE_OVERWRITE = False
|
|
||||||
|
|
||||||
# Custom Domain settings
|
# The AWS access key to use.
|
||||||
parsed_url = urlparse(os.environ.get("WEB_URL", "http://localhost"))
|
AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID", "")
|
||||||
AWS_S3_CUSTOM_DOMAIN = f"{parsed_url.netloc}/{AWS_STORAGE_BUCKET_NAME}"
|
|
||||||
AWS_S3_URL_PROTOCOL = f"{parsed_url.scheme}:"
|
|
||||||
else:
|
|
||||||
# The AWS region to connect to.
|
|
||||||
AWS_REGION = os.environ.get("AWS_REGION", "")
|
|
||||||
|
|
||||||
# The AWS access key to use.
|
# The AWS secret access key to use.
|
||||||
AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID", "")
|
AWS_SECRET_ACCESS_KEY = os.environ.get("AWS_SECRET_ACCESS_KEY", "")
|
||||||
|
|
||||||
# The AWS secret access key to use.
|
# The optional AWS session token to use.
|
||||||
AWS_SECRET_ACCESS_KEY = os.environ.get("AWS_SECRET_ACCESS_KEY", "")
|
# AWS_SESSION_TOKEN = ""
|
||||||
|
|
||||||
# The optional AWS session token to use.
|
# The name of the bucket to store files in.
|
||||||
# AWS_SESSION_TOKEN = ""
|
AWS_S3_BUCKET_NAME = os.environ.get("AWS_S3_BUCKET_NAME")
|
||||||
|
|
||||||
# The name of the bucket to store files in.
|
# How to construct S3 URLs ("auto", "path", "virtual").
|
||||||
AWS_S3_BUCKET_NAME = os.environ.get("AWS_S3_BUCKET_NAME")
|
AWS_S3_ADDRESSING_STYLE = "auto"
|
||||||
|
|
||||||
# How to construct S3 URLs ("auto", "path", "virtual").
|
# The full URL to the S3 endpoint. Leave blank to use the default region URL.
|
||||||
AWS_S3_ADDRESSING_STYLE = "auto"
|
AWS_S3_ENDPOINT_URL = os.environ.get("AWS_S3_ENDPOINT_URL", "")
|
||||||
|
|
||||||
# The full URL to the S3 endpoint. Leave blank to use the default region URL.
|
# A prefix to be applied to every stored file. This will be joined to every filename using the "/" separator.
|
||||||
AWS_S3_ENDPOINT_URL = os.environ.get("AWS_S3_ENDPOINT_URL", "")
|
AWS_S3_KEY_PREFIX = ""
|
||||||
|
|
||||||
# A prefix to be applied to every stored file. This will be joined to every filename using the "/" separator.
|
# Whether to enable authentication for stored files. If True, then generated URLs will include an authentication
|
||||||
AWS_S3_KEY_PREFIX = ""
|
# token valid for `AWS_S3_MAX_AGE_SECONDS`. If False, then generated URLs will not include an authentication token,
|
||||||
|
# and their permissions will be set to "public-read".
|
||||||
|
AWS_S3_BUCKET_AUTH = False
|
||||||
|
|
||||||
# Whether to enable authentication for stored files. If True, then generated URLs will include an authentication
|
# How long generated URLs are valid for. This affects the expiry of authentication tokens if `AWS_S3_BUCKET_AUTH`
|
||||||
# token valid for `AWS_S3_MAX_AGE_SECONDS`. If False, then generated URLs will not include an authentication token,
|
# is True. It also affects the "Cache-Control" header of the files.
|
||||||
# and their permissions will be set to "public-read".
|
# Important: Changing this setting will not affect existing files.
|
||||||
AWS_S3_BUCKET_AUTH = False
|
AWS_S3_MAX_AGE_SECONDS = 60 * 60 # 1 hours.
|
||||||
|
|
||||||
# How long generated URLs are valid for. This affects the expiry of authentication tokens if `AWS_S3_BUCKET_AUTH`
|
# A URL prefix to be used for generated URLs. This is useful if your bucket is served through a CDN. This setting
|
||||||
# is True. It also affects the "Cache-Control" header of the files.
|
# cannot be used with `AWS_S3_BUCKET_AUTH`.
|
||||||
# Important: Changing this setting will not affect existing files.
|
AWS_S3_PUBLIC_URL = ""
|
||||||
AWS_S3_MAX_AGE_SECONDS = 60 * 60 # 1 hours.
|
|
||||||
|
|
||||||
# A URL prefix to be used for generated URLs. This is useful if your bucket is served through a CDN. This setting
|
# If True, then files will be stored with reduced redundancy. Check the S3 documentation and make sure you
|
||||||
# cannot be used with `AWS_S3_BUCKET_AUTH`.
|
# understand the consequences before enabling.
|
||||||
AWS_S3_PUBLIC_URL = ""
|
# Important: Changing this setting will not affect existing files.
|
||||||
|
AWS_S3_REDUCED_REDUNDANCY = False
|
||||||
|
|
||||||
# If True, then files will be stored with reduced redundancy. Check the S3 documentation and make sure you
|
# The Content-Disposition header used when the file is downloaded. This can be a string, or a function taking a
|
||||||
# understand the consequences before enabling.
|
# single `name` argument.
|
||||||
# Important: Changing this setting will not affect existing files.
|
# Important: Changing this setting will not affect existing files.
|
||||||
AWS_S3_REDUCED_REDUNDANCY = False
|
AWS_S3_CONTENT_DISPOSITION = ""
|
||||||
|
|
||||||
# The Content-Disposition header used when the file is downloaded. This can be a string, or a function taking a
|
# The Content-Language header used when the file is downloaded. This can be a string, or a function taking a
|
||||||
# single `name` argument.
|
# single `name` argument.
|
||||||
# Important: Changing this setting will not affect existing files.
|
# Important: Changing this setting will not affect existing files.
|
||||||
AWS_S3_CONTENT_DISPOSITION = ""
|
AWS_S3_CONTENT_LANGUAGE = ""
|
||||||
|
|
||||||
# The Content-Language header used when the file is downloaded. This can be a string, or a function taking a
|
# A mapping of custom metadata for each file. Each value can be a string, or a function taking a
|
||||||
# single `name` argument.
|
# single `name` argument.
|
||||||
# Important: Changing this setting will not affect existing files.
|
# Important: Changing this setting will not affect existing files.
|
||||||
AWS_S3_CONTENT_LANGUAGE = ""
|
AWS_S3_METADATA = {}
|
||||||
|
|
||||||
# A mapping of custom metadata for each file. Each value can be a string, or a function taking a
|
# If True, then files will be stored using AES256 server-side encryption.
|
||||||
# single `name` argument.
|
# If this is a string value (e.g., "aws:kms"), that encryption type will be used.
|
||||||
# Important: Changing this setting will not affect existing files.
|
# Otherwise, server-side encryption is not be enabled.
|
||||||
AWS_S3_METADATA = {}
|
# Important: Changing this setting will not affect existing files.
|
||||||
|
AWS_S3_ENCRYPT_KEY = False
|
||||||
|
|
||||||
# If True, then files will be stored using AES256 server-side encryption.
|
# The AWS S3 KMS encryption key ID (the `SSEKMSKeyId` parameter) is set from this string if present.
|
||||||
# If this is a string value (e.g., "aws:kms"), that encryption type will be used.
|
# This is only relevant if AWS S3 KMS server-side encryption is enabled (above).
|
||||||
# Otherwise, server-side encryption is not be enabled.
|
# AWS_S3_KMS_ENCRYPTION_KEY_ID = ""
|
||||||
# Important: Changing this setting will not affect existing files.
|
|
||||||
AWS_S3_ENCRYPT_KEY = False
|
|
||||||
|
|
||||||
# The AWS S3 KMS encryption key ID (the `SSEKMSKeyId` parameter) is set from this string if present.
|
# If True, then text files will be stored using gzip content encoding. Files will only be gzipped if their
|
||||||
# This is only relevant if AWS S3 KMS server-side encryption is enabled (above).
|
# compressed size is smaller than their uncompressed size.
|
||||||
# AWS_S3_KMS_ENCRYPTION_KEY_ID = ""
|
# Important: Changing this setting will not affect existing files.
|
||||||
|
AWS_S3_GZIP = True
|
||||||
|
|
||||||
# If True, then text files will be stored using gzip content encoding. Files will only be gzipped if their
|
# The signature version to use for S3 requests.
|
||||||
# compressed size is smaller than their uncompressed size.
|
AWS_S3_SIGNATURE_VERSION = None
|
||||||
# Important: Changing this setting will not affect existing files.
|
|
||||||
AWS_S3_GZIP = True
|
|
||||||
|
|
||||||
# The signature version to use for S3 requests.
|
# If True, then files with the same name will overwrite each other. By default it's set to False to have
|
||||||
AWS_S3_SIGNATURE_VERSION = None
|
# extra characters appended.
|
||||||
|
AWS_S3_FILE_OVERWRITE = False
|
||||||
|
|
||||||
# If True, then files with the same name will overwrite each other. By default it's set to False to have
|
STORAGES["default"] = {
|
||||||
# extra characters appended.
|
"BACKEND": "django_s3_storage.storage.S3Storage",
|
||||||
AWS_S3_FILE_OVERWRITE = False
|
}
|
||||||
|
|
||||||
STORAGES["default"] = {
|
|
||||||
"BACKEND": "django_s3_storage.storage.S3Storage",
|
|
||||||
}
|
|
||||||
|
|
||||||
# AWS Settings End
|
# AWS Settings End
|
||||||
|
|
||||||
@ -218,27 +193,16 @@ CSRF_COOKIE_SECURE = True
|
|||||||
|
|
||||||
REDIS_URL = os.environ.get("REDIS_URL")
|
REDIS_URL = os.environ.get("REDIS_URL")
|
||||||
|
|
||||||
if DOCKERIZED:
|
CACHES = {
|
||||||
CACHES = {
|
"default": {
|
||||||
"default": {
|
"BACKEND": "django_redis.cache.RedisCache",
|
||||||
"BACKEND": "django_redis.cache.RedisCache",
|
"LOCATION": REDIS_URL,
|
||||||
"LOCATION": REDIS_URL,
|
"OPTIONS": {
|
||||||
"OPTIONS": {
|
"CLIENT_CLASS": "django_redis.client.DefaultClient",
|
||||||
"CLIENT_CLASS": "django_redis.client.DefaultClient",
|
"CONNECTION_POOL_KWARGS": {"ssl_cert_reqs": False},
|
||||||
},
|
},
|
||||||
}
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
CACHES = {
|
|
||||||
"default": {
|
|
||||||
"BACKEND": "django_redis.cache.RedisCache",
|
|
||||||
"LOCATION": REDIS_URL,
|
|
||||||
"OPTIONS": {
|
|
||||||
"CLIENT_CLASS": "django_redis.client.DefaultClient",
|
|
||||||
"CONNECTION_POOL_KWARGS": {"ssl_cert_reqs": False},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
WEB_URL = os.environ.get("WEB_URL", "https://app.plane.so")
|
WEB_URL = os.environ.get("WEB_URL", "https://app.plane.so")
|
||||||
@ -261,19 +225,16 @@ broker_url = (
|
|||||||
f"{redis_url}?ssl_cert_reqs={ssl.CERT_NONE.name}&ssl_ca_certs={certifi.where()}"
|
f"{redis_url}?ssl_cert_reqs={ssl.CERT_NONE.name}&ssl_ca_certs={certifi.where()}"
|
||||||
)
|
)
|
||||||
|
|
||||||
if DOCKERIZED:
|
CELERY_RESULT_BACKEND = broker_url
|
||||||
CELERY_BROKER_URL = REDIS_URL
|
CELERY_BROKER_URL = broker_url
|
||||||
CELERY_RESULT_BACKEND = REDIS_URL
|
|
||||||
else:
|
|
||||||
CELERY_RESULT_BACKEND = broker_url
|
|
||||||
CELERY_BROKER_URL = broker_url
|
|
||||||
|
|
||||||
GITHUB_ACCESS_TOKEN = os.environ.get("GITHUB_ACCESS_TOKEN", False)
|
GITHUB_ACCESS_TOKEN = os.environ.get("GITHUB_ACCESS_TOKEN", False)
|
||||||
|
|
||||||
|
# Enable or Disable signups
|
||||||
ENABLE_SIGNUP = os.environ.get("ENABLE_SIGNUP", "1") == "1"
|
ENABLE_SIGNUP = os.environ.get("ENABLE_SIGNUP", "1") == "1"
|
||||||
|
|
||||||
# Scout Settings
|
# Scout Settings
|
||||||
SCOUT_MONITOR = os.environ.get("SCOUT_MONITOR", False)
|
SCOUT_MONITOR = os.environ.get("SCOUT_MONITOR", False)
|
||||||
SCOUT_KEY = os.environ.get("SCOUT_KEY", "")
|
SCOUT_KEY = os.environ.get("SCOUT_KEY", "")
|
||||||
SCOUT_NAME = "Plane"
|
SCOUT_NAME = "Plane"
|
||||||
|
|
||||||
|
128
apiserver/plane/settings/selfhosted.py
Normal file
128
apiserver/plane/settings/selfhosted.py
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
"""Self hosted settings and globals."""
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
import dj_database_url
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
|
||||||
|
from .common import * # noqa
|
||||||
|
|
||||||
|
# Database
|
||||||
|
DEBUG = int(os.environ.get("DEBUG", 0)) == 1
|
||||||
|
|
||||||
|
# Docker configurations
|
||||||
|
DOCKERIZED = 1
|
||||||
|
USE_MINIO = 1
|
||||||
|
|
||||||
|
DATABASES = {
|
||||||
|
"default": {
|
||||||
|
"ENGINE": "django.db.backends.postgresql",
|
||||||
|
"NAME": "plane",
|
||||||
|
"USER": os.environ.get("PGUSER", ""),
|
||||||
|
"PASSWORD": os.environ.get("PGPASSWORD", ""),
|
||||||
|
"HOST": os.environ.get("PGHOST", ""),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Parse database configuration from $DATABASE_URL
|
||||||
|
DATABASES["default"] = dj_database_url.config()
|
||||||
|
SITE_ID = 1
|
||||||
|
|
||||||
|
# File size limit
|
||||||
|
FILE_SIZE_LIMIT = int(os.environ.get("FILE_SIZE_LIMIT", 5242880))
|
||||||
|
|
||||||
|
CORS_ALLOW_METHODS = [
|
||||||
|
"DELETE",
|
||||||
|
"GET",
|
||||||
|
"OPTIONS",
|
||||||
|
"PATCH",
|
||||||
|
"POST",
|
||||||
|
"PUT",
|
||||||
|
]
|
||||||
|
|
||||||
|
CORS_ALLOW_HEADERS = [
|
||||||
|
"accept",
|
||||||
|
"accept-encoding",
|
||||||
|
"authorization",
|
||||||
|
"content-type",
|
||||||
|
"dnt",
|
||||||
|
"origin",
|
||||||
|
"user-agent",
|
||||||
|
"x-csrftoken",
|
||||||
|
"x-requested-with",
|
||||||
|
]
|
||||||
|
|
||||||
|
CORS_ALLOW_CREDENTIALS = True
|
||||||
|
CORS_ALLOW_ALL_ORIGINS = True
|
||||||
|
|
||||||
|
STORAGES = {
|
||||||
|
"staticfiles": {
|
||||||
|
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
INSTALLED_APPS += ("storages",)
|
||||||
|
STORAGES["default"] = {"BACKEND": "storages.backends.s3boto3.S3Boto3Storage"}
|
||||||
|
# The AWS access key to use.
|
||||||
|
AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID", "access-key")
|
||||||
|
# The AWS secret access key to use.
|
||||||
|
AWS_SECRET_ACCESS_KEY = os.environ.get("AWS_SECRET_ACCESS_KEY", "secret-key")
|
||||||
|
# The name of the bucket to store files in.
|
||||||
|
AWS_STORAGE_BUCKET_NAME = os.environ.get("AWS_S3_BUCKET_NAME", "uploads")
|
||||||
|
# The full URL to the S3 endpoint. Leave blank to use the default region URL.
|
||||||
|
AWS_S3_ENDPOINT_URL = os.environ.get(
|
||||||
|
"AWS_S3_ENDPOINT_URL", "http://plane-minio:9000"
|
||||||
|
)
|
||||||
|
# Default permissions
|
||||||
|
AWS_DEFAULT_ACL = "public-read"
|
||||||
|
AWS_QUERYSTRING_AUTH = False
|
||||||
|
AWS_S3_FILE_OVERWRITE = False
|
||||||
|
|
||||||
|
# Custom Domain settings
|
||||||
|
parsed_url = urlparse(os.environ.get("WEB_URL", "http://localhost"))
|
||||||
|
AWS_S3_CUSTOM_DOMAIN = f"{parsed_url.netloc}/{AWS_STORAGE_BUCKET_NAME}"
|
||||||
|
AWS_S3_URL_PROTOCOL = f"{parsed_url.scheme}:"
|
||||||
|
|
||||||
|
# Honor the 'X-Forwarded-Proto' header for request.is_secure()
|
||||||
|
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
|
||||||
|
|
||||||
|
# Allow all host headers
|
||||||
|
ALLOWED_HOSTS = [
|
||||||
|
"*",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Security settings
|
||||||
|
SESSION_COOKIE_SECURE = True
|
||||||
|
CSRF_COOKIE_SECURE = True
|
||||||
|
|
||||||
|
# Redis URL
|
||||||
|
REDIS_URL = os.environ.get("REDIS_URL")
|
||||||
|
|
||||||
|
# Caches
|
||||||
|
CACHES = {
|
||||||
|
"default": {
|
||||||
|
"BACKEND": "django_redis.cache.RedisCache",
|
||||||
|
"LOCATION": REDIS_URL,
|
||||||
|
"OPTIONS": {
|
||||||
|
"CLIENT_CLASS": "django_redis.client.DefaultClient",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# URL used for email redirects
|
||||||
|
WEB_URL = os.environ.get("WEB_URL", "http://localhost")
|
||||||
|
|
||||||
|
# Celery settings
|
||||||
|
CELERY_BROKER_URL = REDIS_URL
|
||||||
|
CELERY_RESULT_BACKEND = REDIS_URL
|
||||||
|
|
||||||
|
# Enable or Disable signups
|
||||||
|
ENABLE_SIGNUP = os.environ.get("ENABLE_SIGNUP", "1") == "1"
|
||||||
|
|
||||||
|
# Analytics
|
||||||
|
ANALYTICS_BASE_API = False
|
||||||
|
|
||||||
|
# OPEN AI Settings
|
||||||
|
OPENAI_API_BASE = os.environ.get("OPENAI_API_BASE", "https://api.openai.com/v1")
|
||||||
|
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", False)
|
||||||
|
GPT_ENGINE = os.environ.get("GPT_ENGINE", "gpt-3.5-turbo")
|
@ -74,10 +74,10 @@ def build_graph_plot(queryset, x_axis, y_axis, segment=None):
|
|||||||
|
|
||||||
sorted_data = grouped_data
|
sorted_data = grouped_data
|
||||||
if temp_axis == "priority":
|
if temp_axis == "priority":
|
||||||
order = ["low", "medium", "high", "urgent", "None"]
|
order = ["low", "medium", "high", "urgent", "none"]
|
||||||
sorted_data = {key: grouped_data[key] for key in order if key in grouped_data}
|
sorted_data = {key: grouped_data[key] for key in order if key in grouped_data}
|
||||||
else:
|
else:
|
||||||
sorted_data = dict(sorted(grouped_data.items(), key=lambda x: (x[0] == "None", x[0])))
|
sorted_data = dict(sorted(grouped_data.items(), key=lambda x: (x[0] == "none", x[0])))
|
||||||
return sorted_data
|
return sorted_data
|
||||||
|
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ def resolve_keys(group_keys, value):
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
def group_results(results_data, group_by):
|
def group_results(results_data, group_by, sub_group_by=False):
|
||||||
"""group results data into certain group_by
|
"""group results data into certain group_by
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -25,38 +25,140 @@ def group_results(results_data, group_by):
|
|||||||
Returns:
|
Returns:
|
||||||
obj: grouped results
|
obj: grouped results
|
||||||
"""
|
"""
|
||||||
response_dict = dict()
|
if sub_group_by:
|
||||||
|
main_responsive_dict = dict()
|
||||||
|
|
||||||
if group_by == "priority":
|
if sub_group_by == "priority":
|
||||||
response_dict = {
|
main_responsive_dict = {
|
||||||
"urgent": [],
|
"urgent": {},
|
||||||
"high": [],
|
"high": {},
|
||||||
"medium": [],
|
"medium": {},
|
||||||
"low": [],
|
"low": {},
|
||||||
"None": [],
|
"none": {},
|
||||||
}
|
}
|
||||||
|
|
||||||
for value in results_data:
|
for value in results_data:
|
||||||
group_attribute = resolve_keys(group_by, value)
|
main_group_attribute = resolve_keys(sub_group_by, value)
|
||||||
if isinstance(group_attribute, list):
|
group_attribute = resolve_keys(group_by, value)
|
||||||
if len(group_attribute):
|
if isinstance(main_group_attribute, list) and not isinstance(group_attribute, list):
|
||||||
for attrib in group_attribute:
|
if len(main_group_attribute):
|
||||||
if str(attrib) in response_dict:
|
for attrib in main_group_attribute:
|
||||||
response_dict[str(attrib)].append(value)
|
if str(attrib) not in main_responsive_dict:
|
||||||
else:
|
main_responsive_dict[str(attrib)] = {}
|
||||||
response_dict[str(attrib)] = []
|
if str(group_attribute) in main_responsive_dict[str(attrib)]:
|
||||||
response_dict[str(attrib)].append(value)
|
main_responsive_dict[str(attrib)][str(group_attribute)].append(value)
|
||||||
else:
|
else:
|
||||||
if str(None) in response_dict:
|
main_responsive_dict[str(attrib)][str(group_attribute)] = []
|
||||||
response_dict[str(None)].append(value)
|
main_responsive_dict[str(attrib)][str(group_attribute)].append(value)
|
||||||
else:
|
else:
|
||||||
response_dict[str(None)] = []
|
if str(None) not in main_responsive_dict:
|
||||||
response_dict[str(None)].append(value)
|
main_responsive_dict[str(None)] = {}
|
||||||
else:
|
|
||||||
if str(group_attribute) in response_dict:
|
|
||||||
response_dict[str(group_attribute)].append(value)
|
|
||||||
else:
|
|
||||||
response_dict[str(group_attribute)] = []
|
|
||||||
response_dict[str(group_attribute)].append(value)
|
|
||||||
|
|
||||||
return response_dict
|
if str(group_attribute) in main_responsive_dict[str(None)]:
|
||||||
|
main_responsive_dict[str(None)][str(group_attribute)].append(value)
|
||||||
|
else:
|
||||||
|
main_responsive_dict[str(None)][str(group_attribute)] = []
|
||||||
|
main_responsive_dict[str(None)][str(group_attribute)].append(value)
|
||||||
|
|
||||||
|
elif isinstance(group_attribute, list) and not isinstance(main_group_attribute, list):
|
||||||
|
if str(main_group_attribute) not in main_responsive_dict:
|
||||||
|
main_responsive_dict[str(main_group_attribute)] = {}
|
||||||
|
if len(group_attribute):
|
||||||
|
for attrib in group_attribute:
|
||||||
|
if str(attrib) in main_responsive_dict[str(main_group_attribute)]:
|
||||||
|
main_responsive_dict[str(main_group_attribute)][str(attrib)].append(value)
|
||||||
|
else:
|
||||||
|
main_responsive_dict[str(main_group_attribute)][str(attrib)] = []
|
||||||
|
main_responsive_dict[str(main_group_attribute)][str(attrib)].append(value)
|
||||||
|
else:
|
||||||
|
if str(None) in main_responsive_dict[str(main_group_attribute)]:
|
||||||
|
main_responsive_dict[str(main_group_attribute)][str(None)].append(value)
|
||||||
|
else:
|
||||||
|
main_responsive_dict[str(main_group_attribute)][str(None)] = []
|
||||||
|
main_responsive_dict[str(main_group_attribute)][str(None)].append(value)
|
||||||
|
|
||||||
|
elif isinstance(group_attribute, list) and isinstance(main_group_attribute, list):
|
||||||
|
if len(main_group_attribute):
|
||||||
|
for main_attrib in main_group_attribute:
|
||||||
|
if str(main_attrib) not in main_responsive_dict:
|
||||||
|
main_responsive_dict[str(main_attrib)] = {}
|
||||||
|
if len(group_attribute):
|
||||||
|
for attrib in group_attribute:
|
||||||
|
if str(attrib) in main_responsive_dict[str(main_attrib)]:
|
||||||
|
main_responsive_dict[str(main_attrib)][str(attrib)].append(value)
|
||||||
|
else:
|
||||||
|
main_responsive_dict[str(main_attrib)][str(attrib)] = []
|
||||||
|
main_responsive_dict[str(main_attrib)][str(attrib)].append(value)
|
||||||
|
else:
|
||||||
|
if str(None) in main_responsive_dict[str(main_attrib)]:
|
||||||
|
main_responsive_dict[str(main_attrib)][str(None)].append(value)
|
||||||
|
else:
|
||||||
|
main_responsive_dict[str(main_attrib)][str(None)] = []
|
||||||
|
main_responsive_dict[str(main_attrib)][str(None)].append(value)
|
||||||
|
else:
|
||||||
|
if str(None) not in main_responsive_dict:
|
||||||
|
main_responsive_dict[str(None)] = {}
|
||||||
|
if len(group_attribute):
|
||||||
|
for attrib in group_attribute:
|
||||||
|
if str(attrib) in main_responsive_dict[str(None)]:
|
||||||
|
main_responsive_dict[str(None)][str(attrib)].append(value)
|
||||||
|
else:
|
||||||
|
main_responsive_dict[str(None)][str(attrib)] = []
|
||||||
|
main_responsive_dict[str(None)][str(attrib)].append(value)
|
||||||
|
else:
|
||||||
|
if str(None) in main_responsive_dict[str(None)]:
|
||||||
|
main_responsive_dict[str(None)][str(None)].append(value)
|
||||||
|
else:
|
||||||
|
main_responsive_dict[str(None)][str(None)] = []
|
||||||
|
main_responsive_dict[str(None)][str(None)].append(value)
|
||||||
|
else:
|
||||||
|
main_group_attribute = resolve_keys(sub_group_by, value)
|
||||||
|
group_attribute = resolve_keys(group_by, value)
|
||||||
|
|
||||||
|
if str(main_group_attribute) not in main_responsive_dict:
|
||||||
|
main_responsive_dict[str(main_group_attribute)] = {}
|
||||||
|
|
||||||
|
if str(group_attribute) in main_responsive_dict[str(main_group_attribute)]:
|
||||||
|
main_responsive_dict[str(main_group_attribute)][str(group_attribute)].append(value)
|
||||||
|
else:
|
||||||
|
main_responsive_dict[str(main_group_attribute)][str(group_attribute)] = []
|
||||||
|
main_responsive_dict[str(main_group_attribute)][str(group_attribute)].append(value)
|
||||||
|
|
||||||
|
return main_responsive_dict
|
||||||
|
|
||||||
|
else:
|
||||||
|
response_dict = dict()
|
||||||
|
|
||||||
|
if group_by == "priority":
|
||||||
|
response_dict = {
|
||||||
|
"urgent": [],
|
||||||
|
"high": [],
|
||||||
|
"medium": [],
|
||||||
|
"low": [],
|
||||||
|
"none": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
for value in results_data:
|
||||||
|
group_attribute = resolve_keys(group_by, value)
|
||||||
|
if isinstance(group_attribute, list):
|
||||||
|
if len(group_attribute):
|
||||||
|
for attrib in group_attribute:
|
||||||
|
if str(attrib) in response_dict:
|
||||||
|
response_dict[str(attrib)].append(value)
|
||||||
|
else:
|
||||||
|
response_dict[str(attrib)] = []
|
||||||
|
response_dict[str(attrib)].append(value)
|
||||||
|
else:
|
||||||
|
if str(None) in response_dict:
|
||||||
|
response_dict[str(None)].append(value)
|
||||||
|
else:
|
||||||
|
response_dict[str(None)] = []
|
||||||
|
response_dict[str(None)].append(value)
|
||||||
|
else:
|
||||||
|
if str(group_attribute) in response_dict:
|
||||||
|
response_dict[str(group_attribute)].append(value)
|
||||||
|
else:
|
||||||
|
response_dict[str(group_attribute)] = []
|
||||||
|
response_dict[str(group_attribute)].append(value)
|
||||||
|
|
||||||
|
return response_dict
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
from django.utils.timezone import make_aware
|
from django.utils.timezone import make_aware
|
||||||
from django.utils.dateparse import parse_datetime
|
from django.utils.dateparse import parse_datetime
|
||||||
|
|
||||||
|
|
||||||
def filter_state(params, filter, method):
|
def filter_state(params, filter, method):
|
||||||
if method == "GET":
|
if method == "GET":
|
||||||
states = params.get("state").split(",")
|
states = params.get("state").split(",")
|
||||||
@ -23,7 +24,6 @@ def filter_state_group(params, filter, method):
|
|||||||
return filter
|
return filter
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def filter_estimate_point(params, filter, method):
|
def filter_estimate_point(params, filter, method):
|
||||||
if method == "GET":
|
if method == "GET":
|
||||||
estimate_points = params.get("estimate_point").split(",")
|
estimate_points = params.get("estimate_point").split(",")
|
||||||
@ -39,25 +39,7 @@ def filter_priority(params, filter, method):
|
|||||||
if method == "GET":
|
if method == "GET":
|
||||||
priorities = params.get("priority").split(",")
|
priorities = params.get("priority").split(",")
|
||||||
if len(priorities) and "" not in priorities:
|
if len(priorities) and "" not in priorities:
|
||||||
if len(priorities) == 1 and "null" in priorities:
|
filter["priority__in"] = priorities
|
||||||
filter["priority__isnull"] = True
|
|
||||||
elif len(priorities) > 1 and "null" in priorities:
|
|
||||||
filter["priority__isnull"] = True
|
|
||||||
filter["priority__in"] = [p for p in priorities if p != "null"]
|
|
||||||
else:
|
|
||||||
filter["priority__in"] = [p for p in priorities if p != "null"]
|
|
||||||
|
|
||||||
else:
|
|
||||||
if params.get("priority", None) and len(params.get("priority")):
|
|
||||||
priorities = params.get("priority")
|
|
||||||
if len(priorities) == 1 and "null" in priorities:
|
|
||||||
filter["priority__isnull"] = True
|
|
||||||
elif len(priorities) > 1 and "null" in priorities:
|
|
||||||
filter["priority__isnull"] = True
|
|
||||||
filter["priority__in"] = [p for p in priorities if p != "null"]
|
|
||||||
else:
|
|
||||||
filter["priority__in"] = [p for p in priorities if p != "null"]
|
|
||||||
|
|
||||||
return filter
|
return filter
|
||||||
|
|
||||||
|
|
||||||
@ -181,17 +163,17 @@ def filter_target_date(params, filter, method):
|
|||||||
for query in target_dates:
|
for query in target_dates:
|
||||||
target_date_query = query.split(";")
|
target_date_query = query.split(";")
|
||||||
if len(target_date_query) == 2 and "after" in target_date_query:
|
if len(target_date_query) == 2 and "after" in target_date_query:
|
||||||
filter["target_date__gt"] = target_date_query[0]
|
filter["target_date__gte"] = target_date_query[0]
|
||||||
else:
|
else:
|
||||||
filter["target_date__lt"] = target_date_query[0]
|
filter["target_date__lte"] = target_date_query[0]
|
||||||
else:
|
else:
|
||||||
if params.get("target_date", None) and len(params.get("target_date")):
|
if params.get("target_date", None) and len(params.get("target_date")):
|
||||||
for query in params.get("target_date"):
|
for query in params.get("target_date"):
|
||||||
target_date_query = query.split(";")
|
target_date_query = query.split(";")
|
||||||
if len(target_date_query) == 2 and "after" in target_date_query:
|
if len(target_date_query) == 2 and "after" in target_date_query:
|
||||||
filter["target_date__gt"] = target_date_query[0]
|
filter["target_date__gte"] = target_date_query[0]
|
||||||
else:
|
else:
|
||||||
filter["target_date__lt"] = target_date_query[0]
|
filter["target_date__lte"] = target_date_query[0]
|
||||||
|
|
||||||
return filter
|
return filter
|
||||||
|
|
||||||
@ -229,7 +211,6 @@ def filter_issue_state_type(params, filter, method):
|
|||||||
return filter
|
return filter
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def filter_project(params, filter, method):
|
def filter_project(params, filter, method):
|
||||||
if method == "GET":
|
if method == "GET":
|
||||||
projects = params.get("project").split(",")
|
projects = params.get("project").split(",")
|
||||||
@ -329,7 +310,7 @@ def issue_filters(query_params, method):
|
|||||||
"module": filter_module,
|
"module": filter_module,
|
||||||
"inbox_status": filter_inbox_status,
|
"inbox_status": filter_inbox_status,
|
||||||
"sub_issue": filter_sub_issue_toggle,
|
"sub_issue": filter_sub_issue_toggle,
|
||||||
"subscriber": filter_subscribed_issues,
|
"subscriber": filter_subscribed_issues,
|
||||||
"start_target_date": filter_start_target_date_issues,
|
"start_target_date": filter_start_target_date_issues,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,113 +1,61 @@
|
|||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
x-api-and-worker-env:
|
|
||||||
&api-and-worker-env
|
|
||||||
DEBUG: ${DEBUG}
|
|
||||||
SENTRY_DSN: ${SENTRY_DSN}
|
|
||||||
DJANGO_SETTINGS_MODULE: plane.settings.production
|
|
||||||
DATABASE_URL: postgres://${PGUSER}:${PGPASSWORD}@${PGHOST}:5432/${PGDATABASE}
|
|
||||||
REDIS_URL: redis://plane-redis:6379/
|
|
||||||
EMAIL_HOST: ${EMAIL_HOST}
|
|
||||||
EMAIL_HOST_USER: ${EMAIL_HOST_USER}
|
|
||||||
EMAIL_HOST_PASSWORD: ${EMAIL_HOST_PASSWORD}
|
|
||||||
EMAIL_PORT: ${EMAIL_PORT}
|
|
||||||
EMAIL_FROM: ${EMAIL_FROM}
|
|
||||||
EMAIL_USE_TLS: ${EMAIL_USE_TLS}
|
|
||||||
EMAIL_USE_SSL: ${EMAIL_USE_SSL}
|
|
||||||
AWS_REGION: ${AWS_REGION}
|
|
||||||
AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID}
|
|
||||||
AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY}
|
|
||||||
AWS_S3_BUCKET_NAME: ${AWS_S3_BUCKET_NAME}
|
|
||||||
AWS_S3_ENDPOINT_URL: ${AWS_S3_ENDPOINT_URL}
|
|
||||||
FILE_SIZE_LIMIT: ${FILE_SIZE_LIMIT}
|
|
||||||
WEB_URL: ${WEB_URL}
|
|
||||||
GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET}
|
|
||||||
DISABLE_COLLECTSTATIC: 1
|
|
||||||
DOCKERIZED: 1
|
|
||||||
OPENAI_API_BASE: ${OPENAI_API_BASE}
|
|
||||||
OPENAI_API_KEY: ${OPENAI_API_KEY}
|
|
||||||
GPT_ENGINE: ${GPT_ENGINE}
|
|
||||||
SECRET_KEY: ${SECRET_KEY}
|
|
||||||
DEFAULT_EMAIL: ${DEFAULT_EMAIL}
|
|
||||||
DEFAULT_PASSWORD: ${DEFAULT_PASSWORD}
|
|
||||||
USE_MINIO: ${USE_MINIO}
|
|
||||||
ENABLE_SIGNUP: ${ENABLE_SIGNUP}
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
plane-web:
|
web:
|
||||||
container_name: planefrontend
|
container_name: web
|
||||||
image: makeplane/plane-frontend:latest
|
image: makeplane/plane-frontend:latest
|
||||||
restart: always
|
restart: always
|
||||||
command: /usr/local/bin/start.sh web/server.js web
|
command: /usr/local/bin/start.sh web/server.js web
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- ./web/.env
|
||||||
environment:
|
|
||||||
NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL}
|
|
||||||
NEXT_PUBLIC_DEPLOY_URL: ${NEXT_PUBLIC_DEPLOY_URL}
|
|
||||||
NEXT_PUBLIC_GOOGLE_CLIENTID: "0"
|
|
||||||
NEXT_PUBLIC_GITHUB_APP_NAME: "0"
|
|
||||||
NEXT_PUBLIC_GITHUB_ID: "0"
|
|
||||||
NEXT_PUBLIC_SENTRY_DSN: "0"
|
|
||||||
NEXT_PUBLIC_ENABLE_OAUTH: "0"
|
|
||||||
NEXT_PUBLIC_ENABLE_SENTRY: "0"
|
|
||||||
NEXT_PUBLIC_ENABLE_SESSION_RECORDER: "0"
|
|
||||||
NEXT_PUBLIC_TRACK_EVENTS: "0"
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- plane-api
|
- api
|
||||||
- plane-worker
|
- worker
|
||||||
|
|
||||||
plane-deploy:
|
space:
|
||||||
container_name: planedeploy
|
container_name: space
|
||||||
image: makeplane/plane-deploy:latest
|
image: makeplane/plane-space:latest
|
||||||
restart: always
|
restart: always
|
||||||
command: /usr/local/bin/start.sh space/server.js space
|
command: /usr/local/bin/start.sh space/server.js space
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- ./space/.env
|
||||||
environment:
|
|
||||||
NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL}
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- plane-api
|
- api
|
||||||
- plane-worker
|
- worker
|
||||||
- plane-web
|
- web
|
||||||
|
|
||||||
plane-api:
|
api:
|
||||||
container_name: planebackend
|
container_name: api
|
||||||
image: makeplane/plane-backend:latest
|
image: makeplane/plane-backend:latest
|
||||||
restart: always
|
restart: always
|
||||||
command: ./bin/takeoff
|
command: ./bin/takeoff
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- ./apiserver/.env
|
||||||
environment:
|
|
||||||
<<: *api-and-worker-env
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- plane-db
|
- plane-db
|
||||||
- plane-redis
|
- plane-redis
|
||||||
|
|
||||||
plane-worker:
|
worker:
|
||||||
container_name: planebgworker
|
container_name: bgworker
|
||||||
image: makeplane/plane-backend:latest
|
image: makeplane/plane-backend:latest
|
||||||
restart: always
|
restart: always
|
||||||
command: ./bin/worker
|
command: ./bin/worker
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- ./apiserver/.env
|
||||||
environment:
|
|
||||||
<<: *api-and-worker-env
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- plane-api
|
- api
|
||||||
- plane-db
|
- plane-db
|
||||||
- plane-redis
|
- plane-redis
|
||||||
|
|
||||||
plane-beat-worker:
|
beat-worker:
|
||||||
container_name: planebeatworker
|
container_name: beatworker
|
||||||
image: makeplane/plane-backend:latest
|
image: makeplane/plane-backend:latest
|
||||||
restart: always
|
restart: always
|
||||||
command: ./bin/beat
|
command: ./bin/beat
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- ./apiserver/.env
|
||||||
environment:
|
|
||||||
<<: *api-and-worker-env
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- plane-api
|
- api
|
||||||
- plane-db
|
- plane-db
|
||||||
- plane-redis
|
- plane-redis
|
||||||
|
|
||||||
@ -157,8 +105,8 @@ services:
|
|||||||
- plane-minio
|
- plane-minio
|
||||||
|
|
||||||
# Comment this if you already have a reverse proxy running
|
# Comment this if you already have a reverse proxy running
|
||||||
plane-proxy:
|
proxy:
|
||||||
container_name: planeproxy
|
container_name: proxy
|
||||||
image: makeplane/plane-proxy:latest
|
image: makeplane/plane-proxy:latest
|
||||||
ports:
|
ports:
|
||||||
- ${NGINX_PORT}:80
|
- ${NGINX_PORT}:80
|
||||||
@ -168,8 +116,9 @@ services:
|
|||||||
FILE_SIZE_LIMIT: ${FILE_SIZE_LIMIT:-5242880}
|
FILE_SIZE_LIMIT: ${FILE_SIZE_LIMIT:-5242880}
|
||||||
BUCKET_NAME: ${AWS_S3_BUCKET_NAME:-uploads}
|
BUCKET_NAME: ${AWS_S3_BUCKET_NAME:-uploads}
|
||||||
depends_on:
|
depends_on:
|
||||||
- plane-web
|
- web
|
||||||
- plane-api
|
- api
|
||||||
|
- space
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
pgdata:
|
pgdata:
|
||||||
|
@ -1,88 +1,35 @@
|
|||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
x-api-and-worker-env: &api-and-worker-env
|
|
||||||
DEBUG: ${DEBUG}
|
|
||||||
SENTRY_DSN: ${SENTRY_DSN}
|
|
||||||
DJANGO_SETTINGS_MODULE: plane.settings.production
|
|
||||||
DATABASE_URL: postgres://${PGUSER}:${PGPASSWORD}@${PGHOST}:5432/${PGDATABASE}
|
|
||||||
REDIS_URL: redis://plane-redis:6379/
|
|
||||||
EMAIL_HOST: ${EMAIL_HOST}
|
|
||||||
EMAIL_HOST_USER: ${EMAIL_HOST_USER}
|
|
||||||
EMAIL_HOST_PASSWORD: ${EMAIL_HOST_PASSWORD}
|
|
||||||
EMAIL_PORT: ${EMAIL_PORT}
|
|
||||||
EMAIL_FROM: ${EMAIL_FROM}
|
|
||||||
EMAIL_USE_TLS: ${EMAIL_USE_TLS}
|
|
||||||
EMAIL_USE_SSL: ${EMAIL_USE_SSL}
|
|
||||||
AWS_REGION: ${AWS_REGION}
|
|
||||||
AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID}
|
|
||||||
AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY}
|
|
||||||
AWS_S3_BUCKET_NAME: ${AWS_S3_BUCKET_NAME}
|
|
||||||
AWS_S3_ENDPOINT_URL: ${AWS_S3_ENDPOINT_URL}
|
|
||||||
FILE_SIZE_LIMIT: ${FILE_SIZE_LIMIT}
|
|
||||||
WEB_URL: ${WEB_URL}
|
|
||||||
GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET}
|
|
||||||
DISABLE_COLLECTSTATIC: 1
|
|
||||||
DOCKERIZED: 1
|
|
||||||
OPENAI_API_BASE: ${OPENAI_API_BASE}
|
|
||||||
OPENAI_API_KEY: ${OPENAI_API_KEY}
|
|
||||||
GPT_ENGINE: ${GPT_ENGINE}
|
|
||||||
SECRET_KEY: ${SECRET_KEY}
|
|
||||||
DEFAULT_EMAIL: ${DEFAULT_EMAIL}
|
|
||||||
DEFAULT_PASSWORD: ${DEFAULT_PASSWORD}
|
|
||||||
USE_MINIO: ${USE_MINIO}
|
|
||||||
ENABLE_SIGNUP: ${ENABLE_SIGNUP}
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
plane-web:
|
web:
|
||||||
container_name: planefrontend
|
container_name: web
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: ./web/Dockerfile.web
|
dockerfile: ./web/Dockerfile.web
|
||||||
args:
|
args:
|
||||||
DOCKER_BUILDKIT: 1
|
DOCKER_BUILDKIT: 1
|
||||||
NEXT_PUBLIC_API_BASE_URL: http://localhost:8000
|
|
||||||
NEXT_PUBLIC_DEPLOY_URL: http://localhost/spaces
|
|
||||||
restart: always
|
restart: always
|
||||||
command: /usr/local/bin/start.sh web/server.js web
|
command: /usr/local/bin/start.sh web/server.js web
|
||||||
env_file:
|
|
||||||
- .env
|
|
||||||
environment:
|
|
||||||
NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL}
|
|
||||||
NEXT_PUBLIC_DEPLOY_URL: ${NEXT_PUBLIC_DEPLOY_URL}
|
|
||||||
NEXT_PUBLIC_GOOGLE_CLIENTID: "0"
|
|
||||||
NEXT_PUBLIC_GITHUB_APP_NAME: "0"
|
|
||||||
NEXT_PUBLIC_GITHUB_ID: "0"
|
|
||||||
NEXT_PUBLIC_SENTRY_DSN: "0"
|
|
||||||
NEXT_PUBLIC_ENABLE_OAUTH: "0"
|
|
||||||
NEXT_PUBLIC_ENABLE_SENTRY: "0"
|
|
||||||
NEXT_PUBLIC_ENABLE_SESSION_RECORDER: "0"
|
|
||||||
NEXT_PUBLIC_TRACK_EVENTS: "0"
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- plane-api
|
- api
|
||||||
- plane-worker
|
- worker
|
||||||
|
|
||||||
plane-deploy:
|
space:
|
||||||
container_name: planedeploy
|
container_name: space
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: ./space/Dockerfile.space
|
dockerfile: ./space/Dockerfile.space
|
||||||
args:
|
args:
|
||||||
DOCKER_BUILDKIT: 1
|
DOCKER_BUILDKIT: 1
|
||||||
NEXT_PUBLIC_DEPLOY_WITH_NGINX: 1
|
|
||||||
NEXT_PUBLIC_API_BASE_URL: http://localhost:8000
|
|
||||||
restart: always
|
restart: always
|
||||||
command: /usr/local/bin/start.sh space/server.js space
|
command: /usr/local/bin/start.sh space/server.js space
|
||||||
env_file:
|
|
||||||
- .env
|
|
||||||
environment:
|
|
||||||
- NEXT_PUBLIC_API_BASE_URL=${NEXT_PUBLIC_API_BASE_URL}
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- plane-api
|
- api
|
||||||
- plane-worker
|
- worker
|
||||||
- plane-web
|
- web
|
||||||
|
|
||||||
plane-api:
|
api:
|
||||||
container_name: planebackend
|
container_name: api
|
||||||
build:
|
build:
|
||||||
context: ./apiserver
|
context: ./apiserver
|
||||||
dockerfile: Dockerfile.api
|
dockerfile: Dockerfile.api
|
||||||
@ -91,15 +38,13 @@ services:
|
|||||||
restart: always
|
restart: always
|
||||||
command: ./bin/takeoff
|
command: ./bin/takeoff
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- ./apiserver/.env
|
||||||
environment:
|
|
||||||
<<: *api-and-worker-env
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- plane-db
|
- plane-db
|
||||||
- plane-redis
|
- plane-redis
|
||||||
|
|
||||||
plane-worker:
|
worker:
|
||||||
container_name: planebgworker
|
container_name: bgworker
|
||||||
build:
|
build:
|
||||||
context: ./apiserver
|
context: ./apiserver
|
||||||
dockerfile: Dockerfile.api
|
dockerfile: Dockerfile.api
|
||||||
@ -108,16 +53,14 @@ services:
|
|||||||
restart: always
|
restart: always
|
||||||
command: ./bin/worker
|
command: ./bin/worker
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- ./apiserver/.env
|
||||||
environment:
|
|
||||||
<<: *api-and-worker-env
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- plane-api
|
- api
|
||||||
- plane-db
|
- plane-db
|
||||||
- plane-redis
|
- plane-redis
|
||||||
|
|
||||||
plane-beat-worker:
|
beat-worker:
|
||||||
container_name: planebeatworker
|
container_name: beatworker
|
||||||
build:
|
build:
|
||||||
context: ./apiserver
|
context: ./apiserver
|
||||||
dockerfile: Dockerfile.api
|
dockerfile: Dockerfile.api
|
||||||
@ -126,11 +69,9 @@ services:
|
|||||||
restart: always
|
restart: always
|
||||||
command: ./bin/beat
|
command: ./bin/beat
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- ./apiserver/.env
|
||||||
environment:
|
|
||||||
<<: *api-and-worker-env
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- plane-api
|
- api
|
||||||
- plane-db
|
- plane-db
|
||||||
- plane-redis
|
- plane-redis
|
||||||
|
|
||||||
@ -163,8 +104,6 @@ services:
|
|||||||
command: server /export --console-address ":9090"
|
command: server /export --console-address ":9090"
|
||||||
volumes:
|
volumes:
|
||||||
- uploads:/export
|
- uploads:/export
|
||||||
env_file:
|
|
||||||
- .env
|
|
||||||
environment:
|
environment:
|
||||||
MINIO_ROOT_USER: ${AWS_ACCESS_KEY_ID}
|
MINIO_ROOT_USER: ${AWS_ACCESS_KEY_ID}
|
||||||
MINIO_ROOT_PASSWORD: ${AWS_SECRET_ACCESS_KEY}
|
MINIO_ROOT_PASSWORD: ${AWS_SECRET_ACCESS_KEY}
|
||||||
@ -179,22 +118,21 @@ services:
|
|||||||
- plane-minio
|
- plane-minio
|
||||||
|
|
||||||
# Comment this if you already have a reverse proxy running
|
# Comment this if you already have a reverse proxy running
|
||||||
plane-proxy:
|
proxy:
|
||||||
container_name: planeproxy
|
container_name: proxy
|
||||||
build:
|
build:
|
||||||
context: ./nginx
|
context: ./nginx
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
restart: always
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
- ${NGINX_PORT}:80
|
- ${NGINX_PORT}:80
|
||||||
env_file:
|
|
||||||
- .env
|
|
||||||
environment:
|
environment:
|
||||||
FILE_SIZE_LIMIT: ${FILE_SIZE_LIMIT:-5242880}
|
FILE_SIZE_LIMIT: ${FILE_SIZE_LIMIT:-5242880}
|
||||||
BUCKET_NAME: ${AWS_S3_BUCKET_NAME:-uploads}
|
BUCKET_NAME: ${AWS_S3_BUCKET_NAME:-uploads}
|
||||||
depends_on:
|
depends_on:
|
||||||
- plane-web
|
- web
|
||||||
- plane-api
|
- api
|
||||||
|
- space
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
pgdata:
|
pgdata:
|
||||||
|
@ -1,30 +1,36 @@
|
|||||||
events { }
|
events {
|
||||||
|
}
|
||||||
|
|
||||||
http {
|
http {
|
||||||
sendfile on;
|
sendfile on;
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
root /www/data/;
|
root /www/data/;
|
||||||
access_log /var/log/nginx/access.log;
|
access_log /var/log/nginx/access.log;
|
||||||
|
|
||||||
client_max_body_size ${FILE_SIZE_LIMIT};
|
client_max_body_size ${FILE_SIZE_LIMIT};
|
||||||
|
|
||||||
location / {
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
proxy_pass http://planefrontend:3000/;
|
add_header Referrer-Policy "no-referrer-when-downgrade" always;
|
||||||
}
|
add_header Permissions-Policy "interest-cohort=()" always;
|
||||||
|
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||||
|
|
||||||
location /api/ {
|
location / {
|
||||||
proxy_pass http://planebackend:8000/api/;
|
proxy_pass http://web:3000/;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /spaces/ {
|
location /api/ {
|
||||||
proxy_pass http://planedeploy:3000/spaces/;
|
proxy_pass http://api:8000/api/;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /${BUCKET_NAME}/ {
|
location /spaces/ {
|
||||||
proxy_pass http://plane-minio:9000/uploads/;
|
rewrite ^/spaces/?$ /spaces/login break;
|
||||||
|
proxy_pass http://space:3000/spaces/;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /${BUCKET_NAME}/ {
|
||||||
|
proxy_pass http://plane-minio:9000/uploads/;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
@ -16,8 +16,12 @@
|
|||||||
"format": "prettier --write \"**/*.{ts,tsx,md}\""
|
"format": "prettier --write \"**/*.{ts,tsx,md}\""
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"autoprefixer": "^10.4.15",
|
||||||
"eslint-config-custom": "*",
|
"eslint-config-custom": "*",
|
||||||
|
"postcss": "^8.4.29",
|
||||||
"prettier": "latest",
|
"prettier": "latest",
|
||||||
|
"prettier-plugin-tailwindcss": "^0.5.4",
|
||||||
|
"tailwindcss": "^3.3.3",
|
||||||
"turbo": "latest"
|
"turbo": "latest"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@1.22.19"
|
"packageManager": "yarn@1.22.19"
|
||||||
|
@ -16,5 +16,7 @@ module.exports = {
|
|||||||
"no-duplicate-imports": "error",
|
"no-duplicate-imports": "error",
|
||||||
"arrow-body-style": ["error", "as-needed"],
|
"arrow-body-style": ["error", "as-needed"],
|
||||||
"react/self-closing-comp": ["error", { component: true, html: true }],
|
"react/self-closing-comp": ["error", { component: true, html: true }],
|
||||||
|
"@next/next/no-img-element": "off",
|
||||||
|
"@typescript-eslint/no-unused-vars": ["warn"],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
10
packages/tailwind-config-custom/package.json
Normal file
10
packages/tailwind-config-custom/package.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"name": "tailwind-config-custom",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "common tailwind configuration across monorepo",
|
||||||
|
"main": "index.js",
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/typography": "^0.5.10",
|
||||||
|
"tailwindcss-animate": "^1.0.7"
|
||||||
|
}
|
||||||
|
}
|
7
packages/tailwind-config-custom/postcss.config.js
Normal file
7
packages/tailwind-config-custom/postcss.config.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
"tailwindcss/nesting": {},
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
};
|
212
packages/tailwind-config-custom/tailwind.config.js
Normal file
212
packages/tailwind-config-custom/tailwind.config.js
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
const convertToRGB = (variableName) => `rgba(var(${variableName}))`;
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
darkMode: "class",
|
||||||
|
content: [
|
||||||
|
"./components/**/*.tsx",
|
||||||
|
"./constants/**/*.{js,ts,jsx,tsx}",
|
||||||
|
"./layouts/**/*.tsx",
|
||||||
|
"./pages/**/*.tsx",
|
||||||
|
"./ui/**/*.tsx",
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
boxShadow: {
|
||||||
|
"custom-shadow-2xs": "var(--color-shadow-2xs)",
|
||||||
|
"custom-shadow-xs": "var(--color-shadow-xs)",
|
||||||
|
"custom-shadow-sm": "var(--color-shadow-sm)",
|
||||||
|
"custom-shadow-rg": "var(--color-shadow-rg)",
|
||||||
|
"custom-shadow-md": "var(--color-shadow-md)",
|
||||||
|
"custom-shadow-lg": "var(--color-shadow-lg)",
|
||||||
|
"custom-shadow-xl": "var(--color-shadow-xl)",
|
||||||
|
"custom-shadow-2xl": "var(--color-shadow-2xl)",
|
||||||
|
"custom-shadow-3xl": "var(--color-shadow-3xl)",
|
||||||
|
"custom-sidebar-shadow-2xs": "var(--color-sidebar-shadow-2xs)",
|
||||||
|
"custom-sidebar-shadow-xs": "var(--color-sidebar-shadow-xs)",
|
||||||
|
"custom-sidebar-shadow-sm": "var(--color-sidebar-shadow-sm)",
|
||||||
|
"custom-sidebar-shadow-rg": "var(--color-sidebar-shadow-rg)",
|
||||||
|
"custom-sidebar-shadow-md": "var(--color-sidebar-shadow-md)",
|
||||||
|
"custom-sidebar-shadow-lg": "var(--color-sidebar-shadow-lg)",
|
||||||
|
"custom-sidebar-shadow-xl": "var(--color-sidebar-shadow-xl)",
|
||||||
|
"custom-sidebar-shadow-2xl": "var(--color-sidebar-shadow-2xl)",
|
||||||
|
"custom-sidebar-shadow-3xl": "var(--color-sidebar-shadow-3xl)",
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
custom: {
|
||||||
|
primary: {
|
||||||
|
0: "rgb(255, 255, 255)",
|
||||||
|
10: convertToRGB("--color-primary-10"),
|
||||||
|
20: convertToRGB("--color-primary-20"),
|
||||||
|
30: convertToRGB("--color-primary-30"),
|
||||||
|
40: convertToRGB("--color-primary-40"),
|
||||||
|
50: convertToRGB("--color-primary-50"),
|
||||||
|
60: convertToRGB("--color-primary-60"),
|
||||||
|
70: convertToRGB("--color-primary-70"),
|
||||||
|
80: convertToRGB("--color-primary-80"),
|
||||||
|
90: convertToRGB("--color-primary-90"),
|
||||||
|
100: convertToRGB("--color-primary-100"),
|
||||||
|
200: convertToRGB("--color-primary-200"),
|
||||||
|
300: convertToRGB("--color-primary-300"),
|
||||||
|
400: convertToRGB("--color-primary-400"),
|
||||||
|
500: convertToRGB("--color-primary-500"),
|
||||||
|
600: convertToRGB("--color-primary-600"),
|
||||||
|
700: convertToRGB("--color-primary-700"),
|
||||||
|
800: convertToRGB("--color-primary-800"),
|
||||||
|
900: convertToRGB("--color-primary-900"),
|
||||||
|
1000: "rgb(0, 0, 0)",
|
||||||
|
DEFAULT: convertToRGB("--color-primary-100"),
|
||||||
|
},
|
||||||
|
background: {
|
||||||
|
0: "rgb(255, 255, 255)",
|
||||||
|
10: convertToRGB("--color-background-10"),
|
||||||
|
20: convertToRGB("--color-background-20"),
|
||||||
|
30: convertToRGB("--color-background-30"),
|
||||||
|
40: convertToRGB("--color-background-40"),
|
||||||
|
50: convertToRGB("--color-background-50"),
|
||||||
|
60: convertToRGB("--color-background-60"),
|
||||||
|
70: convertToRGB("--color-background-70"),
|
||||||
|
80: convertToRGB("--color-background-80"),
|
||||||
|
90: convertToRGB("--color-background-90"),
|
||||||
|
100: convertToRGB("--color-background-100"),
|
||||||
|
200: convertToRGB("--color-background-200"),
|
||||||
|
300: convertToRGB("--color-background-300"),
|
||||||
|
400: convertToRGB("--color-background-400"),
|
||||||
|
500: convertToRGB("--color-background-500"),
|
||||||
|
600: convertToRGB("--color-background-600"),
|
||||||
|
700: convertToRGB("--color-background-700"),
|
||||||
|
800: convertToRGB("--color-background-800"),
|
||||||
|
900: convertToRGB("--color-background-900"),
|
||||||
|
1000: "rgb(0, 0, 0)",
|
||||||
|
DEFAULT: convertToRGB("--color-background-100"),
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
0: "rgb(255, 255, 255)",
|
||||||
|
10: convertToRGB("--color-text-10"),
|
||||||
|
20: convertToRGB("--color-text-20"),
|
||||||
|
30: convertToRGB("--color-text-30"),
|
||||||
|
40: convertToRGB("--color-text-40"),
|
||||||
|
50: convertToRGB("--color-text-50"),
|
||||||
|
60: convertToRGB("--color-text-60"),
|
||||||
|
70: convertToRGB("--color-text-70"),
|
||||||
|
80: convertToRGB("--color-text-80"),
|
||||||
|
90: convertToRGB("--color-text-90"),
|
||||||
|
100: convertToRGB("--color-text-100"),
|
||||||
|
200: convertToRGB("--color-text-200"),
|
||||||
|
300: convertToRGB("--color-text-300"),
|
||||||
|
400: convertToRGB("--color-text-400"),
|
||||||
|
500: convertToRGB("--color-text-500"),
|
||||||
|
600: convertToRGB("--color-text-600"),
|
||||||
|
700: convertToRGB("--color-text-700"),
|
||||||
|
800: convertToRGB("--color-text-800"),
|
||||||
|
900: convertToRGB("--color-text-900"),
|
||||||
|
1000: "rgb(0, 0, 0)",
|
||||||
|
DEFAULT: convertToRGB("--color-text-100"),
|
||||||
|
},
|
||||||
|
border: {
|
||||||
|
0: "rgb(255, 255, 255)",
|
||||||
|
100: convertToRGB("--color-border-100"),
|
||||||
|
200: convertToRGB("--color-border-200"),
|
||||||
|
300: convertToRGB("--color-border-300"),
|
||||||
|
400: convertToRGB("--color-border-400"),
|
||||||
|
1000: "rgb(0, 0, 0)",
|
||||||
|
DEFAULT: convertToRGB("--color-border-200"),
|
||||||
|
},
|
||||||
|
sidebar: {
|
||||||
|
background: {
|
||||||
|
0: "rgb(255, 255, 255)",
|
||||||
|
10: convertToRGB("--color-sidebar-background-10"),
|
||||||
|
20: convertToRGB("--color-sidebar-background-20"),
|
||||||
|
30: convertToRGB("--color-sidebar-background-30"),
|
||||||
|
40: convertToRGB("--color-sidebar-background-40"),
|
||||||
|
50: convertToRGB("--color-sidebar-background-50"),
|
||||||
|
60: convertToRGB("--color-sidebar-background-60"),
|
||||||
|
70: convertToRGB("--color-sidebar-background-70"),
|
||||||
|
80: convertToRGB("--color-sidebar-background-80"),
|
||||||
|
90: convertToRGB("--color-sidebar-background-90"),
|
||||||
|
100: convertToRGB("--color-sidebar-background-100"),
|
||||||
|
200: convertToRGB("--color-sidebar-background-200"),
|
||||||
|
300: convertToRGB("--color-sidebar-background-300"),
|
||||||
|
400: convertToRGB("--color-sidebar-background-400"),
|
||||||
|
500: convertToRGB("--color-sidebar-background-500"),
|
||||||
|
600: convertToRGB("--color-sidebar-background-600"),
|
||||||
|
700: convertToRGB("--color-sidebar-background-700"),
|
||||||
|
800: convertToRGB("--color-sidebar-background-800"),
|
||||||
|
900: convertToRGB("--color-sidebar-background-900"),
|
||||||
|
1000: "rgb(0, 0, 0)",
|
||||||
|
DEFAULT: convertToRGB("--color-sidebar-background-100"),
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
0: "rgb(255, 255, 255)",
|
||||||
|
10: convertToRGB("--color-sidebar-text-10"),
|
||||||
|
20: convertToRGB("--color-sidebar-text-20"),
|
||||||
|
30: convertToRGB("--color-sidebar-text-30"),
|
||||||
|
40: convertToRGB("--color-sidebar-text-40"),
|
||||||
|
50: convertToRGB("--color-sidebar-text-50"),
|
||||||
|
60: convertToRGB("--color-sidebar-text-60"),
|
||||||
|
70: convertToRGB("--color-sidebar-text-70"),
|
||||||
|
80: convertToRGB("--color-sidebar-text-80"),
|
||||||
|
90: convertToRGB("--color-sidebar-text-90"),
|
||||||
|
100: convertToRGB("--color-sidebar-text-100"),
|
||||||
|
200: convertToRGB("--color-sidebar-text-200"),
|
||||||
|
300: convertToRGB("--color-sidebar-text-300"),
|
||||||
|
400: convertToRGB("--color-sidebar-text-400"),
|
||||||
|
500: convertToRGB("--color-sidebar-text-500"),
|
||||||
|
600: convertToRGB("--color-sidebar-text-600"),
|
||||||
|
700: convertToRGB("--color-sidebar-text-700"),
|
||||||
|
800: convertToRGB("--color-sidebar-text-800"),
|
||||||
|
900: convertToRGB("--color-sidebar-text-900"),
|
||||||
|
1000: "rgb(0, 0, 0)",
|
||||||
|
DEFAULT: convertToRGB("--color-sidebar-text-100"),
|
||||||
|
},
|
||||||
|
border: {
|
||||||
|
0: "rgb(255, 255, 255)",
|
||||||
|
100: convertToRGB("--color-sidebar-border-100"),
|
||||||
|
200: convertToRGB("--color-sidebar-border-200"),
|
||||||
|
300: convertToRGB("--color-sidebar-border-300"),
|
||||||
|
400: convertToRGB("--color-sidebar-border-400"),
|
||||||
|
1000: "rgb(0, 0, 0)",
|
||||||
|
DEFAULT: convertToRGB("--color-sidebar-border-200"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
backdrop: "#131313",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
keyframes: {
|
||||||
|
leftToaster: {
|
||||||
|
"0%": { left: "-20rem" },
|
||||||
|
"100%": { left: "0" },
|
||||||
|
},
|
||||||
|
rightToaster: {
|
||||||
|
"0%": { right: "-20rem" },
|
||||||
|
"100%": { right: "0" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
typography: ({ theme }) => ({
|
||||||
|
brand: {
|
||||||
|
css: {
|
||||||
|
"--tw-prose-body": convertToRGB("--color-text-100"),
|
||||||
|
"--tw-prose-p": convertToRGB("--color-text-100"),
|
||||||
|
"--tw-prose-headings": convertToRGB("--color-text-100"),
|
||||||
|
"--tw-prose-lead": convertToRGB("--color-text-100"),
|
||||||
|
"--tw-prose-links": convertToRGB("--color-primary-100"),
|
||||||
|
"--tw-prose-bold": convertToRGB("--color-text-100"),
|
||||||
|
"--tw-prose-counters": convertToRGB("--color-text-100"),
|
||||||
|
"--tw-prose-bullets": convertToRGB("--color-text-100"),
|
||||||
|
"--tw-prose-hr": convertToRGB("--color-text-100"),
|
||||||
|
"--tw-prose-quotes": convertToRGB("--color-text-100"),
|
||||||
|
"--tw-prose-quote-borders": convertToRGB("--color-border"),
|
||||||
|
"--tw-prose-code": convertToRGB("--color-text-100"),
|
||||||
|
"--tw-prose-pre-code": convertToRGB("--color-text-100"),
|
||||||
|
"--tw-prose-pre-bg": convertToRGB("--color-background-100"),
|
||||||
|
"--tw-prose-th-borders": convertToRGB("--color-border"),
|
||||||
|
"--tw-prose-td-borders": convertToRGB("--color-border"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
fontFamily: {
|
||||||
|
custom: ["Inter", "sans-serif"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [require("tailwindcss-animate"), require("@tailwindcss/typography")],
|
||||||
|
};
|
@ -17,6 +17,7 @@
|
|||||||
"next": "12.3.2",
|
"next": "12.3.2",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"tsconfig": "*",
|
"tsconfig": "*",
|
||||||
|
"tailwind-config-custom": "*",
|
||||||
"typescript": "4.7.4"
|
"typescript": "4.7.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
1
packages/ui/postcss.config.js
Normal file
1
packages/ui/postcss.config.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
module.exports = require("tailwind-config-custom/postcss.config");
|
1
packages/ui/tailwind.config.js
Normal file
1
packages/ui/tailwind.config.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
module.exports = require("tailwind-config-custom/tailwind.config");
|
@ -1,9 +1,5 @@
|
|||||||
{
|
{
|
||||||
"extends": "../tsconfig/nextjs.json",
|
"extends": "tsconfig/react-library.json",
|
||||||
"include": ["."],
|
"include": ["."],
|
||||||
"exclude": ["dist", "build", "node_modules"],
|
"exclude": ["dist", "build", "node_modules"]
|
||||||
"compilerOptions": {
|
|
||||||
"jsx": "react-jsx",
|
|
||||||
"lib": ["DOM"]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,15 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
FROM=$1
|
|
||||||
TO=$2
|
|
||||||
DIRECTORY=$3
|
|
||||||
|
|
||||||
if [ "${FROM}" = "${TO}" ]; then
|
|
||||||
echo "Nothing to replace, the value is already set to ${TO}."
|
|
||||||
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Only perform action if $FROM and $TO are different.
|
|
||||||
echo "Replacing all statically built instances of $FROM with this string $TO ."
|
|
||||||
|
|
||||||
grep -R -la "${FROM}" $DIRECTORY/.next | xargs -I{} sed -i "s|$FROM|$TO|g" "{}"
|
|
24
setup.sh
24
setup.sh
@ -5,25 +5,9 @@ cp ./.env.example ./.env
|
|||||||
export LC_ALL=C
|
export LC_ALL=C
|
||||||
export LC_CTYPE=C
|
export LC_CTYPE=C
|
||||||
|
|
||||||
|
cp ./web/.env.example ./web/.env
|
||||||
# Generate the NEXT_PUBLIC_API_BASE_URL with given IP
|
cp ./space/.env.example ./space/.env
|
||||||
echo -e "\nNEXT_PUBLIC_API_BASE_URL=$1" >> ./.env
|
cp ./apiserver/.env.example ./apiserver/.env
|
||||||
|
|
||||||
# Generate the SECRET_KEY that will be used by django
|
# Generate the SECRET_KEY that will be used by django
|
||||||
echo -e "SECRET_KEY=\"$(tr -dc 'a-z0-9' < /dev/urandom | head -c50)\"" >> ./.env
|
echo -e "SECRET_KEY=\"$(tr -dc 'a-z0-9' < /dev/urandom | head -c50)\"" >> ./apiserver/.env
|
||||||
|
|
||||||
# WEB_URL for email redirection and image saving
|
|
||||||
echo -e "WEB_URL=$1" >> ./.env
|
|
||||||
|
|
||||||
# Generate Prompt for taking tiptap auth key
|
|
||||||
echo -e "\n\e[1;38m Instructions for generating TipTap Pro Extensions Auth Token \e[0m \n"
|
|
||||||
|
|
||||||
echo -e "\e[1;38m 1. Head over to TipTap cloud's Pro Extensions Page, https://collab.tiptap.dev/pro-extensions \e[0m"
|
|
||||||
echo -e "\e[1;38m 2. Copy the token given to you under the first paragraph, after 'Here it is' \e[0m \n"
|
|
||||||
|
|
||||||
read -p $'\e[1;32m Please Enter Your TipTap Pro Extensions Authentication Token: \e[0m \e[1;36m' authToken
|
|
||||||
|
|
||||||
|
|
||||||
echo "@tiptap-pro:registry=https://registry.tiptap.dev/
|
|
||||||
//registry.tiptap.dev/:_authToken=${authToken}" > .npmrc
|
|
||||||
|
|
@ -1,8 +1,2 @@
|
|||||||
# Base url for the API requests
|
|
||||||
NEXT_PUBLIC_API_BASE_URL=""
|
|
||||||
# Public boards deploy URL
|
|
||||||
NEXT_PUBLIC_DEPLOY_URL=""
|
|
||||||
# Google Client ID for Google OAuth
|
|
||||||
NEXT_PUBLIC_GOOGLE_CLIENTID=""
|
|
||||||
# Flag to toggle OAuth
|
# Flag to toggle OAuth
|
||||||
NEXT_PUBLIC_ENABLE_OAUTH=1
|
NEXT_PUBLIC_ENABLE_OAUTH=0
|
@ -1,7 +1,4 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
root: true,
|
root: true,
|
||||||
extends: ["custom"],
|
extends: ["custom"],
|
||||||
rules: {
|
|
||||||
"@next/next/no-img-element": "off",
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
FROM node:18-alpine AS builder
|
FROM node:18-alpine AS builder
|
||||||
RUN apk add --no-cache libc6-compat
|
RUN apk add --no-cache libc6-compat
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
ENV NEXT_PUBLIC_API_BASE_URL=http://NEXT_PUBLIC_API_BASE_URL_PLACEHOLDER
|
|
||||||
|
|
||||||
RUN yarn global add turbo
|
RUN yarn global add turbo
|
||||||
COPY . .
|
COPY . .
|
||||||
@ -20,19 +19,16 @@ RUN yarn install --network-timeout 500000
|
|||||||
|
|
||||||
COPY --from=builder /app/out/full/ .
|
COPY --from=builder /app/out/full/ .
|
||||||
COPY turbo.json turbo.json
|
COPY turbo.json turbo.json
|
||||||
COPY replace-env-vars.sh /usr/local/bin/
|
|
||||||
USER root
|
USER root
|
||||||
RUN chmod +x /usr/local/bin/replace-env-vars.sh
|
|
||||||
|
|
||||||
ARG NEXT_PUBLIC_API_BASE_URL=http://localhost:8000
|
ARG NEXT_PUBLIC_API_BASE_URL=""
|
||||||
ARG NEXT_PUBLIC_DEPLOY_WITH_NGINX=1
|
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
|
||||||
|
ENV NEXT_PUBLIC_DEPLOY_WITH_NGINX=$NEXT_PUBLIC_DEPLOY_WITH_NGINX
|
||||||
|
|
||||||
RUN yarn turbo run build --filter=space
|
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
|
|
||||||
|
|
||||||
FROM node:18-alpine AS runner
|
FROM node:18-alpine AS runner
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
@ -48,14 +44,14 @@ COPY --from=installer --chown=captain:plane /app/space/.next/standalone ./
|
|||||||
COPY --from=installer --chown=captain:plane /app/space/.next ./space/.next
|
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/space/public ./space/public
|
||||||
|
|
||||||
ARG NEXT_PUBLIC_API_BASE_URL=http://localhost:8000
|
ARG NEXT_PUBLIC_API_BASE_URL=""
|
||||||
ARG NEXT_PUBLIC_DEPLOY_WITH_NGINX=1
|
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
|
||||||
|
ENV NEXT_PUBLIC_DEPLOY_WITH_NGINX=$NEXT_PUBLIC_DEPLOY_WITH_NGINX
|
||||||
|
|
||||||
USER root
|
USER root
|
||||||
COPY replace-env-vars.sh /usr/local/bin/
|
|
||||||
COPY start.sh /usr/local/bin/
|
COPY start.sh /usr/local/bin/
|
||||||
RUN chmod +x /usr/local/bin/replace-env-vars.sh
|
|
||||||
RUN chmod +x /usr/local/bin/start.sh
|
RUN chmod +x /usr/local/bin/start.sh
|
||||||
|
|
||||||
USER captain
|
USER captain
|
||||||
|
@ -1,9 +1,6 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
// react hook form
|
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
// components
|
// components
|
||||||
import { EmailResetPasswordForm } from "./email-reset-password-form";
|
import { EmailResetPasswordForm } from "./email-reset-password-form";
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React from "react";
|
import React, { useEffect } from "react";
|
||||||
|
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
@ -13,7 +13,7 @@ import useToast from "hooks/use-toast";
|
|||||||
// components
|
// components
|
||||||
import { EmailPasswordForm, GithubLoginButton, GoogleLoginButton, EmailCodeForm } from "components/accounts";
|
import { EmailPasswordForm, GithubLoginButton, GoogleLoginButton, EmailCodeForm } from "components/accounts";
|
||||||
// images
|
// images
|
||||||
const imagePrefix = process.env.NEXT_PUBLIC_DEPLOY_WITH_NGINX ? "/spaces/" : "";
|
const imagePrefix = Boolean(parseInt(process.env.NEXT_PUBLIC_DEPLOY_WITH_NGINX || "0")) ? "/spaces" : "";
|
||||||
|
|
||||||
export const SignInView = observer(() => {
|
export const SignInView = observer(() => {
|
||||||
const { user: userStore } = useMobxStore();
|
const { user: userStore } = useMobxStore();
|
||||||
@ -33,7 +33,7 @@ export const SignInView = observer(() => {
|
|||||||
const onSignInSuccess = (response: any) => {
|
const onSignInSuccess = (response: any) => {
|
||||||
const isOnboarded = response?.user?.onboarding_step?.profile_complete || false;
|
const isOnboarded = response?.user?.onboarding_step?.profile_complete || false;
|
||||||
|
|
||||||
const nextPath = router.asPath.includes("next_path") ? router.asPath.split("/?next_path=")[1] : "/";
|
const nextPath = router.asPath.includes("next_path") ? router.asPath.split("/?next_path=")[1] : "/login";
|
||||||
|
|
||||||
userStore.setCurrentUser(response?.user);
|
userStore.setCurrentUser(response?.user);
|
||||||
|
|
||||||
@ -41,7 +41,7 @@ export const SignInView = observer(() => {
|
|||||||
router.push(`/onboarding?next_path=${nextPath}`);
|
router.push(`/onboarding?next_path=${nextPath}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
router.push((nextPath ?? "/").toString());
|
router.push((nextPath ?? "/login").toString());
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleGoogleSignIn = async ({ clientId, credential }: any) => {
|
const handleGoogleSignIn = async ({ clientId, credential }: any) => {
|
||||||
|
@ -18,7 +18,6 @@ import Gapcursor from "@tiptap/extension-gapcursor";
|
|||||||
import ts from "highlight.js/lib/languages/typescript";
|
import ts from "highlight.js/lib/languages/typescript";
|
||||||
|
|
||||||
import "highlight.js/styles/github-dark.css";
|
import "highlight.js/styles/github-dark.css";
|
||||||
import UniqueID from "@tiptap-pro/extension-unique-id";
|
|
||||||
import UpdatedImage from "./updated-image";
|
import UpdatedImage from "./updated-image";
|
||||||
import isValidHttpUrl from "../bubble-menu/utils/link-validator";
|
import isValidHttpUrl from "../bubble-menu/utils/link-validator";
|
||||||
import { CustomTableCell } from "./table/table-cell";
|
import { CustomTableCell } from "./table/table-cell";
|
||||||
@ -121,9 +120,6 @@ export const TiptapExtensions = (
|
|||||||
},
|
},
|
||||||
includeChildren: true,
|
includeChildren: true,
|
||||||
}),
|
}),
|
||||||
UniqueID.configure({
|
|
||||||
types: ["image"],
|
|
||||||
}),
|
|
||||||
SlashCommand(workspaceSlug, setIsSubmitting),
|
SlashCommand(workspaceSlug, setIsSubmitting),
|
||||||
TiptapUnderline,
|
TiptapUnderline,
|
||||||
TextStyle,
|
TextStyle,
|
||||||
|
@ -1 +1 @@
|
|||||||
export * from "./home";
|
export * from "./login";
|
||||||
|
@ -4,7 +4,7 @@ import { useMobxStore } from "lib/mobx/store-provider";
|
|||||||
// components
|
// components
|
||||||
import { SignInView, UserLoggedIn } from "components/accounts";
|
import { SignInView, UserLoggedIn } from "components/accounts";
|
||||||
|
|
||||||
export const HomeView = observer(() => {
|
export const LoginView = observer(() => {
|
||||||
const { user: userStore } = useMobxStore();
|
const { user: userStore } = useMobxStore();
|
||||||
|
|
||||||
if (!userStore.currentUser) return <SignInView />;
|
if (!userStore.currentUser) return <SignInView />;
|
1
space/helpers/common.helper.ts
Normal file
1
space/helpers/common.helper.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL ? process.env.NEXT_PUBLIC_API_BASE_URL : "";
|
@ -17,8 +17,6 @@
|
|||||||
"@heroicons/react": "^2.0.12",
|
"@heroicons/react": "^2.0.12",
|
||||||
"@mui/icons-material": "^5.14.1",
|
"@mui/icons-material": "^5.14.1",
|
||||||
"@mui/material": "^5.14.1",
|
"@mui/material": "^5.14.1",
|
||||||
"@tailwindcss/typography": "^0.5.9",
|
|
||||||
"@tiptap-pro/extension-unique-id": "^2.1.0",
|
|
||||||
"@tiptap/extension-code-block-lowlight": "^2.0.4",
|
"@tiptap/extension-code-block-lowlight": "^2.0.4",
|
||||||
"@tiptap/extension-color": "^2.0.4",
|
"@tiptap/extension-color": "^2.0.4",
|
||||||
"@tiptap/extension-gapcursor": "^2.1.7",
|
"@tiptap/extension-gapcursor": "^2.1.7",
|
||||||
@ -62,7 +60,6 @@
|
|||||||
"uuid": "^9.0.0"
|
"uuid": "^9.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/typography": "^0.5.9",
|
|
||||||
"@types/js-cookie": "^3.0.3",
|
"@types/js-cookie": "^3.0.3",
|
||||||
"@types/node": "18.14.1",
|
"@types/node": "18.14.1",
|
||||||
"@types/nprogress": "^0.2.0",
|
"@types/nprogress": "^0.2.0",
|
||||||
@ -70,12 +67,10 @@
|
|||||||
"@types/react-dom": "18.0.11",
|
"@types/react-dom": "18.0.11",
|
||||||
"@types/uuid": "^9.0.1",
|
"@types/uuid": "^9.0.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.48.2",
|
"@typescript-eslint/eslint-plugin": "^5.48.2",
|
||||||
"autoprefixer": "^10.4.13",
|
|
||||||
"eslint": "8.34.0",
|
"eslint": "8.34.0",
|
||||||
"eslint-config-custom": "*",
|
"eslint-config-custom": "*",
|
||||||
"eslint-config-next": "13.2.1",
|
"eslint-config-next": "13.2.1",
|
||||||
"postcss": "^8.4.21",
|
|
||||||
"tsconfig": "*",
|
"tsconfig": "*",
|
||||||
"tailwindcss": "^3.2.7"
|
"tailwind-config-custom": "*"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
|
|
||||||
// components
|
|
||||||
import { HomeView } from "components/views";
|
|
||||||
|
|
||||||
const HomePage = () => <HomeView />;
|
|
||||||
|
|
||||||
export default HomePage;
|
|
8
space/pages/login/index.tsx
Normal file
8
space/pages/login/index.tsx
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
// components
|
||||||
|
import { LoginView } from "components/views";
|
||||||
|
|
||||||
|
const LoginPage = () => <LoginView />;
|
||||||
|
|
||||||
|
export default LoginPage;
|
@ -2,22 +2,16 @@ import React, { useEffect } from "react";
|
|||||||
// mobx
|
// mobx
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// services
|
|
||||||
import authenticationService from "services/authentication.service";
|
|
||||||
// hooks
|
|
||||||
import useToast from "hooks/use-toast";
|
|
||||||
// components
|
// components
|
||||||
import { OnBoardingForm } from "components/accounts/onboarding-form";
|
import { OnBoardingForm } from "components/accounts/onboarding-form";
|
||||||
|
|
||||||
const imagePrefix = process.env.NEXT_PUBLIC_DEPLOY_WITH_NGINX ? "/spaces/" : "";
|
const imagePrefix = Boolean(parseInt(process.env.NEXT_PUBLIC_DEPLOY_WITH_NGINX || "0")) ? "/spaces" : "";
|
||||||
|
|
||||||
const OnBoardingPage = () => {
|
const OnBoardingPage = () => {
|
||||||
const { user: userStore } = useMobxStore();
|
const { user: userStore } = useMobxStore();
|
||||||
|
|
||||||
const user = userStore?.currentUser;
|
const user = userStore?.currentUser;
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const user = userStore?.currentUser;
|
const user = userStore?.currentUser;
|
||||||
|
|
||||||
|
@ -1,7 +1 @@
|
|||||||
module.exports = {
|
module.exports = require("tailwind-config-custom/postcss.config");
|
||||||
plugins: {
|
|
||||||
"tailwindcss/nesting": {},
|
|
||||||
tailwindcss: {},
|
|
||||||
autoprefixer: {},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
// services
|
// services
|
||||||
import APIService from "services/api.service";
|
import APIService from "services/api.service";
|
||||||
|
import { API_BASE_URL } from "helpers/common.helper";
|
||||||
|
|
||||||
class AuthService extends APIService {
|
class AuthService extends APIService {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
|
super(API_BASE_URL);
|
||||||
}
|
}
|
||||||
|
|
||||||
async emailLogin(data: any) {
|
async emailLogin(data: any) {
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
// services
|
|
||||||
import APIService from "services/api.service";
|
import APIService from "services/api.service";
|
||||||
|
import { API_BASE_URL } from "helpers/common.helper";
|
||||||
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
|
|
||||||
|
|
||||||
interface UnSplashImage {
|
interface UnSplashImage {
|
||||||
id: string;
|
id: string;
|
||||||
@ -29,7 +27,7 @@ interface UnSplashImageUrls {
|
|||||||
|
|
||||||
class FileServices extends APIService {
|
class FileServices extends APIService {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
|
super(API_BASE_URL);
|
||||||
}
|
}
|
||||||
|
|
||||||
async uploadFile(workspaceSlug: string, file: FormData): Promise<any> {
|
async uploadFile(workspaceSlug: string, file: FormData): Promise<any> {
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
// services
|
// services
|
||||||
import APIService from "services/api.service";
|
import APIService from "services/api.service";
|
||||||
|
import { API_BASE_URL } from "helpers/common.helper";
|
||||||
|
|
||||||
class IssueService extends APIService {
|
class IssueService extends APIService {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
|
super(API_BASE_URL);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPublicIssues(workspace_slug: string, project_slug: string, params: any): Promise<any> {
|
async getPublicIssues(workspace_slug: string, project_slug: string, params: any): Promise<any> {
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
// services
|
// services
|
||||||
import APIService from "services/api.service";
|
import APIService from "services/api.service";
|
||||||
|
import { API_BASE_URL } from "helpers/common.helper";
|
||||||
|
|
||||||
class ProjectService extends APIService {
|
class ProjectService extends APIService {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
|
super(API_BASE_URL);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProjectSettings(workspace_slug: string, project_slug: string): Promise<any> {
|
async getProjectSettings(workspace_slug: string, project_slug: string): Promise<any> {
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
// services
|
// services
|
||||||
import APIService from "services/api.service";
|
import APIService from "services/api.service";
|
||||||
|
import { API_BASE_URL } from "helpers/common.helper";
|
||||||
|
|
||||||
class UserService extends APIService {
|
class UserService extends APIService {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
|
super(API_BASE_URL);
|
||||||
}
|
}
|
||||||
|
|
||||||
async currentUser(): Promise<any> {
|
async currentUser(): Promise<any> {
|
||||||
|
@ -1,203 +1 @@
|
|||||||
/** @type {import('tailwindcss').Config} */
|
module.exports = require("tailwind-config-custom/tailwind.config");
|
||||||
|
|
||||||
const convertToRGB = (variableName) => `rgba(var(${variableName}))`;
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
content: [
|
|
||||||
"./app/**/*.{js,ts,jsx,tsx}",
|
|
||||||
"./pages/**/*.{js,ts,jsx,tsx}",
|
|
||||||
"./layouts/**/*.tsx",
|
|
||||||
"./components/**/*.{js,ts,jsx,tsx}",
|
|
||||||
"./constants/**/*.{js,ts,jsx,tsx}",
|
|
||||||
],
|
|
||||||
theme: {
|
|
||||||
extend: {
|
|
||||||
boxShadow: {
|
|
||||||
"custom-shadow-2xs": "var(--color-shadow-2xs)",
|
|
||||||
"custom-shadow-xs": "var(--color-shadow-xs)",
|
|
||||||
"custom-shadow-sm": "var(--color-shadow-sm)",
|
|
||||||
"custom-shadow-rg": "var(--color-shadow-rg)",
|
|
||||||
"custom-shadow-md": "var(--color-shadow-md)",
|
|
||||||
"custom-shadow-lg": "var(--color-shadow-lg)",
|
|
||||||
"custom-shadow-xl": "var(--color-shadow-xl)",
|
|
||||||
"custom-shadow-2xl": "var(--color-shadow-2xl)",
|
|
||||||
"custom-shadow-3xl": "var(--color-shadow-3xl)",
|
|
||||||
"custom-sidebar-shadow-2xs": "var(--color-sidebar-shadow-2xs)",
|
|
||||||
"custom-sidebar-shadow-xs": "var(--color-sidebar-shadow-xs)",
|
|
||||||
"custom-sidebar-shadow-sm": "var(--color-sidebar-shadow-sm)",
|
|
||||||
"custom-sidebar-shadow-rg": "var(--color-sidebar-shadow-rg)",
|
|
||||||
"custom-sidebar-shadow-md": "var(--color-sidebar-shadow-md)",
|
|
||||||
"custom-sidebar-shadow-lg": "var(--color-sidebar-shadow-lg)",
|
|
||||||
"custom-sidebar-shadow-xl": "var(--color-sidebar-shadow-xl)",
|
|
||||||
"custom-sidebar-shadow-2xl": "var(--color-sidebar-shadow-2xl)",
|
|
||||||
"custom-sidebar-shadow-3xl": "var(--color-sidebar-shadow-3xl)",
|
|
||||||
},
|
|
||||||
colors: {
|
|
||||||
custom: {
|
|
||||||
primary: {
|
|
||||||
0: "rgb(255, 255, 255)",
|
|
||||||
10: convertToRGB("--color-primary-10"),
|
|
||||||
20: convertToRGB("--color-primary-20"),
|
|
||||||
30: convertToRGB("--color-primary-30"),
|
|
||||||
40: convertToRGB("--color-primary-40"),
|
|
||||||
50: convertToRGB("--color-primary-50"),
|
|
||||||
60: convertToRGB("--color-primary-60"),
|
|
||||||
70: convertToRGB("--color-primary-70"),
|
|
||||||
80: convertToRGB("--color-primary-80"),
|
|
||||||
90: convertToRGB("--color-primary-90"),
|
|
||||||
100: convertToRGB("--color-primary-100"),
|
|
||||||
200: convertToRGB("--color-primary-200"),
|
|
||||||
300: convertToRGB("--color-primary-300"),
|
|
||||||
400: convertToRGB("--color-primary-400"),
|
|
||||||
500: convertToRGB("--color-primary-500"),
|
|
||||||
600: convertToRGB("--color-primary-600"),
|
|
||||||
700: convertToRGB("--color-primary-700"),
|
|
||||||
800: convertToRGB("--color-primary-800"),
|
|
||||||
900: convertToRGB("--color-primary-900"),
|
|
||||||
1000: "rgb(0, 0, 0)",
|
|
||||||
DEFAULT: convertToRGB("--color-primary-100"),
|
|
||||||
},
|
|
||||||
background: {
|
|
||||||
0: "rgb(255, 255, 255)",
|
|
||||||
10: convertToRGB("--color-background-10"),
|
|
||||||
20: convertToRGB("--color-background-20"),
|
|
||||||
30: convertToRGB("--color-background-30"),
|
|
||||||
40: convertToRGB("--color-background-40"),
|
|
||||||
50: convertToRGB("--color-background-50"),
|
|
||||||
60: convertToRGB("--color-background-60"),
|
|
||||||
70: convertToRGB("--color-background-70"),
|
|
||||||
80: convertToRGB("--color-background-80"),
|
|
||||||
90: convertToRGB("--color-background-90"),
|
|
||||||
100: convertToRGB("--color-background-100"),
|
|
||||||
200: convertToRGB("--color-background-200"),
|
|
||||||
300: convertToRGB("--color-background-300"),
|
|
||||||
400: convertToRGB("--color-background-400"),
|
|
||||||
500: convertToRGB("--color-background-500"),
|
|
||||||
600: convertToRGB("--color-background-600"),
|
|
||||||
700: convertToRGB("--color-background-700"),
|
|
||||||
800: convertToRGB("--color-background-800"),
|
|
||||||
900: convertToRGB("--color-background-900"),
|
|
||||||
1000: "rgb(0, 0, 0)",
|
|
||||||
DEFAULT: convertToRGB("--color-background-100"),
|
|
||||||
},
|
|
||||||
text: {
|
|
||||||
0: "rgb(255, 255, 255)",
|
|
||||||
10: convertToRGB("--color-text-10"),
|
|
||||||
20: convertToRGB("--color-text-20"),
|
|
||||||
30: convertToRGB("--color-text-30"),
|
|
||||||
40: convertToRGB("--color-text-40"),
|
|
||||||
50: convertToRGB("--color-text-50"),
|
|
||||||
60: convertToRGB("--color-text-60"),
|
|
||||||
70: convertToRGB("--color-text-70"),
|
|
||||||
80: convertToRGB("--color-text-80"),
|
|
||||||
90: convertToRGB("--color-text-90"),
|
|
||||||
100: convertToRGB("--color-text-100"),
|
|
||||||
200: convertToRGB("--color-text-200"),
|
|
||||||
300: convertToRGB("--color-text-300"),
|
|
||||||
400: convertToRGB("--color-text-400"),
|
|
||||||
500: convertToRGB("--color-text-500"),
|
|
||||||
600: convertToRGB("--color-text-600"),
|
|
||||||
700: convertToRGB("--color-text-700"),
|
|
||||||
800: convertToRGB("--color-text-800"),
|
|
||||||
900: convertToRGB("--color-text-900"),
|
|
||||||
1000: "rgb(0, 0, 0)",
|
|
||||||
DEFAULT: convertToRGB("--color-text-100"),
|
|
||||||
},
|
|
||||||
border: {
|
|
||||||
0: "rgb(255, 255, 255)",
|
|
||||||
100: convertToRGB("--color-border-100"),
|
|
||||||
200: convertToRGB("--color-border-200"),
|
|
||||||
300: convertToRGB("--color-border-300"),
|
|
||||||
400: convertToRGB("--color-border-400"),
|
|
||||||
1000: "rgb(0, 0, 0)",
|
|
||||||
DEFAULT: convertToRGB("--color-border-200"),
|
|
||||||
},
|
|
||||||
sidebar: {
|
|
||||||
background: {
|
|
||||||
0: "rgb(255, 255, 255)",
|
|
||||||
10: convertToRGB("--color-sidebar-background-10"),
|
|
||||||
20: convertToRGB("--color-sidebar-background-20"),
|
|
||||||
30: convertToRGB("--color-sidebar-background-30"),
|
|
||||||
40: convertToRGB("--color-sidebar-background-40"),
|
|
||||||
50: convertToRGB("--color-sidebar-background-50"),
|
|
||||||
60: convertToRGB("--color-sidebar-background-60"),
|
|
||||||
70: convertToRGB("--color-sidebar-background-70"),
|
|
||||||
80: convertToRGB("--color-sidebar-background-80"),
|
|
||||||
90: convertToRGB("--color-sidebar-background-90"),
|
|
||||||
100: convertToRGB("--color-sidebar-background-100"),
|
|
||||||
200: convertToRGB("--color-sidebar-background-200"),
|
|
||||||
300: convertToRGB("--color-sidebar-background-300"),
|
|
||||||
400: convertToRGB("--color-sidebar-background-400"),
|
|
||||||
500: convertToRGB("--color-sidebar-background-500"),
|
|
||||||
600: convertToRGB("--color-sidebar-background-600"),
|
|
||||||
700: convertToRGB("--color-sidebar-background-700"),
|
|
||||||
800: convertToRGB("--color-sidebar-background-800"),
|
|
||||||
900: convertToRGB("--color-sidebar-background-900"),
|
|
||||||
1000: "rgb(0, 0, 0)",
|
|
||||||
DEFAULT: convertToRGB("--color-sidebar-background-100"),
|
|
||||||
},
|
|
||||||
text: {
|
|
||||||
0: "rgb(255, 255, 255)",
|
|
||||||
10: convertToRGB("--color-sidebar-text-10"),
|
|
||||||
20: convertToRGB("--color-sidebar-text-20"),
|
|
||||||
30: convertToRGB("--color-sidebar-text-30"),
|
|
||||||
40: convertToRGB("--color-sidebar-text-40"),
|
|
||||||
50: convertToRGB("--color-sidebar-text-50"),
|
|
||||||
60: convertToRGB("--color-sidebar-text-60"),
|
|
||||||
70: convertToRGB("--color-sidebar-text-70"),
|
|
||||||
80: convertToRGB("--color-sidebar-text-80"),
|
|
||||||
90: convertToRGB("--color-sidebar-text-90"),
|
|
||||||
100: convertToRGB("--color-sidebar-text-100"),
|
|
||||||
200: convertToRGB("--color-sidebar-text-200"),
|
|
||||||
300: convertToRGB("--color-sidebar-text-300"),
|
|
||||||
400: convertToRGB("--color-sidebar-text-400"),
|
|
||||||
500: convertToRGB("--color-sidebar-text-500"),
|
|
||||||
600: convertToRGB("--color-sidebar-text-600"),
|
|
||||||
700: convertToRGB("--color-sidebar-text-700"),
|
|
||||||
800: convertToRGB("--color-sidebar-text-800"),
|
|
||||||
900: convertToRGB("--color-sidebar-text-900"),
|
|
||||||
1000: "rgb(0, 0, 0)",
|
|
||||||
DEFAULT: convertToRGB("--color-sidebar-text-100"),
|
|
||||||
},
|
|
||||||
border: {
|
|
||||||
0: "rgb(255, 255, 255)",
|
|
||||||
100: convertToRGB("--color-sidebar-border-100"),
|
|
||||||
200: convertToRGB("--color-sidebar-border-200"),
|
|
||||||
300: convertToRGB("--color-sidebar-border-300"),
|
|
||||||
400: convertToRGB("--color-sidebar-border-400"),
|
|
||||||
1000: "rgb(0, 0, 0)",
|
|
||||||
DEFAULT: convertToRGB("--color-sidebar-border-200"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
backdrop: "#131313",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
typography: ({ theme }) => ({
|
|
||||||
brand: {
|
|
||||||
css: {
|
|
||||||
"--tw-prose-body": convertToRGB("--color-text-100"),
|
|
||||||
"--tw-prose-p": convertToRGB("--color-text-100"),
|
|
||||||
"--tw-prose-headings": convertToRGB("--color-text-100"),
|
|
||||||
"--tw-prose-lead": convertToRGB("--color-text-100"),
|
|
||||||
"--tw-prose-links": convertToRGB("--color-primary-100"),
|
|
||||||
"--tw-prose-bold": convertToRGB("--color-text-100"),
|
|
||||||
"--tw-prose-counters": convertToRGB("--color-text-100"),
|
|
||||||
"--tw-prose-bullets": convertToRGB("--color-text-100"),
|
|
||||||
"--tw-prose-hr": convertToRGB("--color-text-100"),
|
|
||||||
"--tw-prose-quotes": convertToRGB("--color-text-100"),
|
|
||||||
"--tw-prose-quote-borders": convertToRGB("--color-border"),
|
|
||||||
"--tw-prose-code": convertToRGB("--color-text-100"),
|
|
||||||
"--tw-prose-pre-code": convertToRGB("--color-text-100"),
|
|
||||||
"--tw-prose-pre-bg": convertToRGB("--color-background-100"),
|
|
||||||
"--tw-prose-th-borders": convertToRGB("--color-border"),
|
|
||||||
"--tw-prose-td-borders": convertToRGB("--color-border"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
fontFamily: {
|
|
||||||
custom: ["Inter", "sans-serif"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
plugins: [require("@tailwindcss/typography")],
|
|
||||||
};
|
|
||||||
|
4
start.sh
4
start.sh
@ -1,9 +1,5 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
set -x
|
set -x
|
||||||
|
|
||||||
# Replace the statically built BUILT_NEXT_PUBLIC_API_BASE_URL with run-time NEXT_PUBLIC_API_BASE_URL
|
|
||||||
# NOTE: if these values are the same, this will be skipped.
|
|
||||||
/usr/local/bin/replace-env-vars.sh "$BUILT_NEXT_PUBLIC_API_BASE_URL" "$NEXT_PUBLIC_API_BASE_URL" $2
|
|
||||||
|
|
||||||
echo "Starting Plane Frontend.."
|
echo "Starting Plane Frontend.."
|
||||||
node $1
|
node $1
|
||||||
|
17
turbo.json
17
turbo.json
@ -15,17 +15,20 @@
|
|||||||
"NEXT_PUBLIC_UNSPLASH_ACCESS",
|
"NEXT_PUBLIC_UNSPLASH_ACCESS",
|
||||||
"NEXT_PUBLIC_UNSPLASH_ENABLED",
|
"NEXT_PUBLIC_UNSPLASH_ENABLED",
|
||||||
"NEXT_PUBLIC_TRACK_EVENTS",
|
"NEXT_PUBLIC_TRACK_EVENTS",
|
||||||
"TRACKER_ACCESS_KEY",
|
"NEXT_PUBLIC_PLAUSIBLE_DOMAIN",
|
||||||
"NEXT_PUBLIC_CRISP_ID",
|
"NEXT_PUBLIC_CRISP_ID",
|
||||||
"NEXT_PUBLIC_ENABLE_SESSION_RECORDER",
|
"NEXT_PUBLIC_ENABLE_SESSION_RECORDER",
|
||||||
"NEXT_PUBLIC_SESSION_RECORDER_KEY",
|
"NEXT_PUBLIC_SESSION_RECORDER_KEY",
|
||||||
"NEXT_PUBLIC_EXTRA_IMAGE_DOMAINS",
|
"NEXT_PUBLIC_EXTRA_IMAGE_DOMAINS",
|
||||||
"NEXT_PUBLIC_SLACK_CLIENT_ID",
|
"NEXT_PUBLIC_DEPLOY_WITH_NGINX",
|
||||||
"NEXT_PUBLIC_SLACK_CLIENT_SECRET",
|
"NEXT_PUBLIC_POSTHOG_KEY",
|
||||||
"NEXT_PUBLIC_SUPABASE_URL",
|
"NEXT_PUBLIC_POSTHOG_HOST",
|
||||||
"NEXT_PUBLIC_SUPABASE_ANON_KEY",
|
"SLACK_OAUTH_URL",
|
||||||
"NEXT_PUBLIC_PLAUSIBLE_DOMAIN",
|
"SLACK_CLIENT_ID",
|
||||||
"NEXT_PUBLIC_DEPLOY_WITH_NGINX"
|
"SLACK_CLIENT_SECRET",
|
||||||
|
"JITSU_TRACKER_ACCESS_KEY",
|
||||||
|
"JITSU_TRACKER_HOST",
|
||||||
|
"UNSPLASH_ACCESS_KEY"
|
||||||
],
|
],
|
||||||
"pipeline": {
|
"pipeline": {
|
||||||
"build": {
|
"build": {
|
||||||
|
@ -1,26 +1,4 @@
|
|||||||
# Base url for the API requests
|
|
||||||
NEXT_PUBLIC_API_BASE_URL=""
|
|
||||||
# Extra image domains that need to be added for Next Image
|
|
||||||
NEXT_PUBLIC_EXTRA_IMAGE_DOMAINS=
|
|
||||||
# Google Client ID for Google OAuth
|
|
||||||
NEXT_PUBLIC_GOOGLE_CLIENTID=""
|
|
||||||
# GitHub App ID for GitHub OAuth
|
|
||||||
NEXT_PUBLIC_GITHUB_ID=""
|
|
||||||
# GitHub App Name for GitHub Integration
|
|
||||||
NEXT_PUBLIC_GITHUB_APP_NAME=""
|
|
||||||
# Sentry DSN for error monitoring
|
|
||||||
NEXT_PUBLIC_SENTRY_DSN=""
|
|
||||||
# Enable/Disable OAUTH - default 0 for selfhosted instance
|
# Enable/Disable OAUTH - default 0 for selfhosted instance
|
||||||
NEXT_PUBLIC_ENABLE_OAUTH=0
|
NEXT_PUBLIC_ENABLE_OAUTH=0
|
||||||
# Enable/Disable Sentry
|
|
||||||
NEXT_PUBLIC_ENABLE_SENTRY=0
|
|
||||||
# Enable/Disable session recording
|
|
||||||
NEXT_PUBLIC_ENABLE_SESSION_RECORDER=0
|
|
||||||
# Enable/Disable event tracking
|
|
||||||
NEXT_PUBLIC_TRACK_EVENTS=0
|
|
||||||
# Slack Client ID for Slack Integration
|
|
||||||
NEXT_PUBLIC_SLACK_CLIENT_ID=""
|
|
||||||
# For Telemetry, set it to "app.plane.so"
|
|
||||||
NEXT_PUBLIC_PLAUSIBLE_DOMAIN=""
|
|
||||||
# Public boards deploy URL
|
# Public boards deploy URL
|
||||||
NEXT_PUBLIC_DEPLOY_URL=""
|
NEXT_PUBLIC_DEPLOY_URL="http://localhost/spaces"
|
@ -1,7 +1,4 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
root: true,
|
root: true,
|
||||||
extends: ["custom"],
|
extends: ["custom"],
|
||||||
rules: {
|
|
||||||
"@next/next/no-img-element": "off",
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
@ -2,7 +2,6 @@ FROM node:18-alpine AS builder
|
|||||||
RUN apk add --no-cache libc6-compat
|
RUN apk add --no-cache libc6-compat
|
||||||
# Set working directory
|
# Set working directory
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
ENV NEXT_PUBLIC_API_BASE_URL=http://NEXT_PUBLIC_API_BASE_URL_PLACEHOLDER
|
|
||||||
|
|
||||||
RUN yarn global add turbo
|
RUN yarn global add turbo
|
||||||
COPY . .
|
COPY . .
|
||||||
@ -14,8 +13,8 @@ FROM node:18-alpine AS installer
|
|||||||
|
|
||||||
RUN apk add --no-cache libc6-compat
|
RUN apk add --no-cache libc6-compat
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
ARG NEXT_PUBLIC_API_BASE_URL=http://localhost:8000
|
ARG NEXT_PUBLIC_API_BASE_URL=""
|
||||||
ARG NEXT_PUBLIC_DEPLOY_URL=http://localhost/spaces
|
ARG NEXT_PUBLIC_DEPLOY_URL=""
|
||||||
|
|
||||||
# First install the dependencies (as they change less often)
|
# First install the dependencies (as they change less often)
|
||||||
COPY .gitignore .gitignore
|
COPY .gitignore .gitignore
|
||||||
@ -26,18 +25,12 @@ RUN yarn install --network-timeout 500000
|
|||||||
# Build the project
|
# Build the project
|
||||||
COPY --from=builder /app/out/full/ .
|
COPY --from=builder /app/out/full/ .
|
||||||
COPY turbo.json turbo.json
|
COPY turbo.json turbo.json
|
||||||
COPY replace-env-vars.sh /usr/local/bin/
|
|
||||||
USER root
|
USER root
|
||||||
RUN chmod +x /usr/local/bin/replace-env-vars.sh
|
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
|
||||||
|
ENV NEXT_PUBLIC_DEPLOY_URL=$NEXT_PUBLIC_DEPLOY_URL
|
||||||
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_URL=$NEXT_PUBLIC_DEPLOY_URL
|
|
||||||
|
|
||||||
RUN yarn turbo run build --filter=web
|
RUN yarn turbo run build --filter=web
|
||||||
|
|
||||||
RUN /usr/local/bin/replace-env-vars.sh http://NEXT_PUBLIC_WEBAPP_URL_PLACEHOLDER ${NEXT_PUBLIC_API_BASE_URL} web
|
|
||||||
|
|
||||||
FROM node:18-alpine AS runner
|
FROM node:18-alpine AS runner
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
@ -52,20 +45,15 @@ COPY --from=installer /app/web/package.json .
|
|||||||
# Automatically leverage output traces to reduce image size
|
# Automatically leverage output traces to reduce image size
|
||||||
# https://nextjs.org/docs/advanced-features/output-file-tracing
|
# https://nextjs.org/docs/advanced-features/output-file-tracing
|
||||||
COPY --from=installer --chown=captain:plane /app/web/.next/standalone ./
|
COPY --from=installer --chown=captain:plane /app/web/.next/standalone ./
|
||||||
|
|
||||||
COPY --from=installer --chown=captain:plane /app/web/.next ./web/.next
|
COPY --from=installer --chown=captain:plane /app/web/.next ./web/.next
|
||||||
|
|
||||||
ARG NEXT_PUBLIC_API_BASE_URL=http://localhost:8000
|
ARG NEXT_PUBLIC_API_BASE_URL=""
|
||||||
ARG NEXT_PUBLIC_DEPLOY_URL=http://localhost/spaces
|
ARG NEXT_PUBLIC_DEPLOY_URL=""
|
||||||
|
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
|
||||||
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \
|
ENV NEXT_PUBLIC_DEPLOY_URL=$NEXT_PUBLIC_DEPLOY_URL
|
||||||
BUILT_NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \
|
|
||||||
NEXT_PUBLIC_DEPLOY_URL=$NEXT_PUBLIC_DEPLOY_URL
|
|
||||||
|
|
||||||
USER root
|
USER root
|
||||||
COPY replace-env-vars.sh /usr/local/bin/
|
|
||||||
COPY start.sh /usr/local/bin/
|
COPY start.sh /usr/local/bin/
|
||||||
RUN chmod +x /usr/local/bin/replace-env-vars.sh
|
|
||||||
RUN chmod +x /usr/local/bin/start.sh
|
RUN chmod +x /usr/local/bin/start.sh
|
||||||
|
|
||||||
USER captain
|
USER captain
|
||||||
|
@ -9,7 +9,6 @@ import { findStringWithMostCharacters } from "helpers/array.helper";
|
|||||||
import { generateBarColor } from "helpers/analytics.helper";
|
import { generateBarColor } from "helpers/analytics.helper";
|
||||||
// types
|
// types
|
||||||
import { IAnalyticsParams, IAnalyticsResponse } from "types";
|
import { IAnalyticsParams, IAnalyticsResponse } from "types";
|
||||||
// constants
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
analytics: IAnalyticsResponse;
|
analytics: IAnalyticsResponse;
|
||||||
|
@ -15,17 +15,19 @@ export const AnalyticsScope: React.FC<Props> = ({ defaultAnalytics }) => (
|
|||||||
<div className="divide-y divide-custom-border-200">
|
<div className="divide-y divide-custom-border-200">
|
||||||
<div>
|
<div>
|
||||||
<h6 className="px-3 text-base font-medium">Pending issues</h6>
|
<h6 className="px-3 text-base font-medium">Pending issues</h6>
|
||||||
{defaultAnalytics.pending_issue_user.length > 0 ? (
|
{defaultAnalytics.pending_issue_user && defaultAnalytics.pending_issue_user.length > 0 ? (
|
||||||
<BarGraph
|
<BarGraph
|
||||||
data={defaultAnalytics.pending_issue_user}
|
data={defaultAnalytics.pending_issue_user}
|
||||||
indexBy="assignees__display_name"
|
indexBy="assignees__id"
|
||||||
keys={["count"]}
|
keys={["count"]}
|
||||||
height="250px"
|
height="250px"
|
||||||
colors={() => `#f97316`}
|
colors={() => `#f97316`}
|
||||||
customYAxisTickValues={defaultAnalytics.pending_issue_user.map((d) => d.count)}
|
customYAxisTickValues={defaultAnalytics.pending_issue_user.map((d) =>
|
||||||
|
d.count > 0 ? d.count : 50
|
||||||
|
)}
|
||||||
tooltip={(datum) => {
|
tooltip={(datum) => {
|
||||||
const assignee = defaultAnalytics.pending_issue_user.find(
|
const assignee = defaultAnalytics.pending_issue_user.find(
|
||||||
(a) => a.assignees__display_name === `${datum.indexValue}`
|
(a) => a.assignees__id === `${datum.indexValue}`
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -39,10 +41,9 @@ export const AnalyticsScope: React.FC<Props> = ({ defaultAnalytics }) => (
|
|||||||
}}
|
}}
|
||||||
axisBottom={{
|
axisBottom={{
|
||||||
renderTick: (datum) => {
|
renderTick: (datum) => {
|
||||||
const avatar =
|
const assignee = defaultAnalytics.pending_issue_user[datum.tickIndex] ?? "";
|
||||||
defaultAnalytics.pending_issue_user[datum.tickIndex]?.assignees__avatar ?? "";
|
|
||||||
|
|
||||||
if (avatar && avatar !== "")
|
if (assignee && assignee?.assignees__avatar && assignee?.assignees__avatar !== "")
|
||||||
return (
|
return (
|
||||||
<g transform={`translate(${datum.x},${datum.y})`}>
|
<g transform={`translate(${datum.x},${datum.y})`}>
|
||||||
<image
|
<image
|
||||||
@ -50,7 +51,7 @@ export const AnalyticsScope: React.FC<Props> = ({ defaultAnalytics }) => (
|
|||||||
y={10}
|
y={10}
|
||||||
width={16}
|
width={16}
|
||||||
height={16}
|
height={16}
|
||||||
xlinkHref={avatar}
|
xlinkHref={assignee?.assignees__avatar}
|
||||||
style={{ clipPath: "circle(50%)" }}
|
style={{ clipPath: "circle(50%)" }}
|
||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
@ -60,7 +61,7 @@ export const AnalyticsScope: React.FC<Props> = ({ defaultAnalytics }) => (
|
|||||||
<g transform={`translate(${datum.x},${datum.y})`}>
|
<g transform={`translate(${datum.x},${datum.y})`}>
|
||||||
<circle cy={18} r={8} fill="#374151" />
|
<circle cy={18} r={8} fill="#374151" />
|
||||||
<text x={0} y={21} textAnchor="middle" fontSize={9} fill="#ffffff">
|
<text x={0} y={21} textAnchor="middle" fontSize={9} fill="#ffffff">
|
||||||
{datum.value ? `${datum.value}`.toUpperCase()[0] : "?"}
|
{datum.value ? `${assignee.assignees__display_name}`.toUpperCase()[0] : "?"}
|
||||||
</text>
|
</text>
|
||||||
</g>
|
</g>
|
||||||
);
|
);
|
||||||
|
@ -3,8 +3,8 @@ import React, { useState } from "react";
|
|||||||
// component
|
// component
|
||||||
import { CustomSelect, ToggleSwitch } from "components/ui";
|
import { CustomSelect, ToggleSwitch } from "components/ui";
|
||||||
import { SelectMonthModal } from "components/automation";
|
import { SelectMonthModal } from "components/automation";
|
||||||
// icons
|
// icon
|
||||||
import { ChevronDownIcon } from "@heroicons/react/24/outline";
|
import { ArchiveRestore } from "lucide-react";
|
||||||
// constants
|
// constants
|
||||||
import { PROJECT_AUTOMATION_MONTHS } from "constants/project";
|
import { PROJECT_AUTOMATION_MONTHS } from "constants/project";
|
||||||
// types
|
// types
|
||||||
@ -28,14 +28,18 @@ export const AutoArchiveAutomation: React.FC<Props> = ({ projectDetails, handleC
|
|||||||
handleClose={() => setmonthModal(false)}
|
handleClose={() => setmonthModal(false)}
|
||||||
handleChange={handleChange}
|
handleChange={handleChange}
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-col gap-7 px-6 py-5 rounded-[10px] border border-custom-border-300 bg-custom-background-90">
|
<div className="flex flex-col gap-4 border-b border-custom-border-200 px-4 py-6">
|
||||||
<div className="flex items-center justify-between gap-x-8 gap-y-2">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex flex-col gap-2.5">
|
<div className="flex items-start gap-3">
|
||||||
<h4 className="text-lg font-semibold">Auto-archive closed issues</h4>
|
<div className="flex items-center justify-center p-3 rounded bg-custom-background-90">
|
||||||
<p className="text-sm text-custom-text-200">
|
<ArchiveRestore className="h-4 w-4 text-custom-text-100 flex-shrink-0" />
|
||||||
Plane will automatically archive issues that have been completed or cancelled for the
|
</div>
|
||||||
configured time period.
|
<div className="">
|
||||||
</p>
|
<h4 className="text-sm font-medium">Auto-archive closed issues</h4>
|
||||||
|
<p className="text-sm text-custom-text-200 tracking-tight">
|
||||||
|
Plane will auto archive issues that have been completed or canceled.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
value={projectDetails?.archive_in !== 0}
|
value={projectDetails?.archive_in !== 0}
|
||||||
@ -47,40 +51,43 @@ export const AutoArchiveAutomation: React.FC<Props> = ({ projectDetails, handleC
|
|||||||
size="sm"
|
size="sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{projectDetails?.archive_in !== 0 && (
|
|
||||||
<div className="flex items-center justify-between gap-2 w-full">
|
|
||||||
<div className="w-1/2 text-base font-medium">
|
|
||||||
Auto-archive issues that are closed for
|
|
||||||
</div>
|
|
||||||
<div className="w-1/2">
|
|
||||||
<CustomSelect
|
|
||||||
value={projectDetails?.archive_in}
|
|
||||||
label={`${projectDetails?.archive_in} ${
|
|
||||||
projectDetails?.archive_in === 1 ? "Month" : "Months"
|
|
||||||
}`}
|
|
||||||
onChange={(val: number) => {
|
|
||||||
handleChange({ archive_in: val });
|
|
||||||
}}
|
|
||||||
input
|
|
||||||
verticalPosition="top"
|
|
||||||
width="w-full"
|
|
||||||
>
|
|
||||||
<>
|
|
||||||
{PROJECT_AUTOMATION_MONTHS.map((month) => (
|
|
||||||
<CustomSelect.Option key={month.label} value={month.value}>
|
|
||||||
{month.label}
|
|
||||||
</CustomSelect.Option>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<button
|
{projectDetails?.archive_in !== 0 && (
|
||||||
type="button"
|
<div className="ml-12">
|
||||||
className="flex w-full select-none items-center rounded px-1 py-1.5 text-custom-text-200 hover:bg-custom-background-80"
|
<div className="flex items-center justify-between rounded px-5 py-4 bg-custom-background-90 border border-custom-border-200 gap-2 w-full">
|
||||||
onClick={() => setmonthModal(true)}
|
<div className="w-1/2 text-sm font-medium">
|
||||||
>
|
Auto-archive issues that are closed for
|
||||||
Customize Time Range
|
</div>
|
||||||
</button>
|
<div className="w-1/2">
|
||||||
</>
|
<CustomSelect
|
||||||
</CustomSelect>
|
value={projectDetails?.archive_in}
|
||||||
|
label={`${projectDetails?.archive_in} ${
|
||||||
|
projectDetails?.archive_in === 1 ? "Month" : "Months"
|
||||||
|
}`}
|
||||||
|
onChange={(val: number) => {
|
||||||
|
handleChange({ archive_in: val });
|
||||||
|
}}
|
||||||
|
input
|
||||||
|
verticalPosition="bottom"
|
||||||
|
width="w-full"
|
||||||
|
>
|
||||||
|
<>
|
||||||
|
{PROJECT_AUTOMATION_MONTHS.map((month) => (
|
||||||
|
<CustomSelect.Option key={month.label} value={month.value}>
|
||||||
|
<span className="text-sm">{month.label}</span>
|
||||||
|
</CustomSelect.Option>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="flex w-full text-sm select-none items-center rounded px-1 py-1.5 text-custom-text-200 hover:bg-custom-background-80"
|
||||||
|
onClick={() => setmonthModal(true)}
|
||||||
|
>
|
||||||
|
Customise Time Range
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
</CustomSelect>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -5,11 +5,12 @@ import useSWR from "swr";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
// component
|
// component
|
||||||
import { CustomSearchSelect, CustomSelect, ToggleSwitch } from "components/ui";
|
import { CustomSearchSelect, CustomSelect, Icon, ToggleSwitch } from "components/ui";
|
||||||
import { SelectMonthModal } from "components/automation";
|
import { SelectMonthModal } from "components/automation";
|
||||||
// icons
|
// icons
|
||||||
import { ChevronDownIcon, Squares2X2Icon } from "@heroicons/react/24/outline";
|
import { Squares2X2Icon } from "@heroicons/react/24/outline";
|
||||||
import { StateGroupIcon } from "components/icons";
|
import { StateGroupIcon } from "components/icons";
|
||||||
|
import { ArchiveX } from "lucide-react";
|
||||||
// services
|
// services
|
||||||
import stateService from "services/state.service";
|
import stateService from "services/state.service";
|
||||||
// constants
|
// constants
|
||||||
@ -76,14 +77,18 @@ export const AutoCloseAutomation: React.FC<Props> = ({ projectDetails, handleCha
|
|||||||
handleChange={handleChange}
|
handleChange={handleChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex flex-col gap-7 px-6 py-5 rounded-[10px] border border-custom-border-300 bg-custom-background-90">
|
<div className="flex flex-col gap-4 border-b border-custom-border-200 px-4 py-6">
|
||||||
<div className="flex items-center justify-between gap-x-8 gap-y-2 ">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex flex-col gap-2.5">
|
<div className="flex items-start gap-3">
|
||||||
<h4 className="text-lg font-semibold">Auto-close inactive issues</h4>
|
<div className="flex items-center justify-center p-3 rounded bg-custom-background-90">
|
||||||
<p className="text-sm text-custom-text-200">
|
<ArchiveX className="h-4 w-4 text-red-500 flex-shrink-0" />
|
||||||
Plane will automatically close the issues that have not been updated for the
|
</div>
|
||||||
configured time period.
|
<div className="">
|
||||||
</p>
|
<h4 className="text-sm font-medium">Auto-close issues</h4>
|
||||||
|
<p className="text-sm text-custom-text-200 tracking-tight">
|
||||||
|
Plane will automatically close issue that haven’t been completed or canceled.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
value={projectDetails?.close_in !== 0}
|
value={projectDetails?.close_in !== 0}
|
||||||
@ -95,82 +100,86 @@ export const AutoCloseAutomation: React.FC<Props> = ({ projectDetails, handleCha
|
|||||||
size="sm"
|
size="sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{projectDetails?.close_in !== 0 && (
|
{projectDetails?.close_in !== 0 && (
|
||||||
<div className="flex flex-col gap-4 w-full">
|
<div className="ml-12">
|
||||||
<div className="flex items-center justify-between gap-2 w-full">
|
<div className="flex flex-col rounded bg-custom-background-90 border border-custom-border-200 p-2">
|
||||||
<div className="w-1/2 text-base font-medium">
|
<div className="flex items-center justify-between px-5 py-4 gap-2 w-full">
|
||||||
Auto-close issues that are inactive for
|
<div className="w-1/2 text-sm font-medium">
|
||||||
|
Auto-close issues that are inactive for
|
||||||
|
</div>
|
||||||
|
<div className="w-1/2">
|
||||||
|
<CustomSelect
|
||||||
|
value={projectDetails?.close_in}
|
||||||
|
label={`${projectDetails?.close_in} ${
|
||||||
|
projectDetails?.close_in === 1 ? "Month" : "Months"
|
||||||
|
}`}
|
||||||
|
onChange={(val: number) => {
|
||||||
|
handleChange({ close_in: val });
|
||||||
|
}}
|
||||||
|
input
|
||||||
|
width="w-full"
|
||||||
|
>
|
||||||
|
<>
|
||||||
|
{PROJECT_AUTOMATION_MONTHS.map((month) => (
|
||||||
|
<CustomSelect.Option key={month.label} value={month.value}>
|
||||||
|
{month.label}
|
||||||
|
</CustomSelect.Option>
|
||||||
|
))}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="flex w-full select-none items-center rounded px-1 py-1.5 text-custom-text-200 hover:bg-custom-background-80"
|
||||||
|
onClick={() => setmonthModal(true)}
|
||||||
|
>
|
||||||
|
Customise Time Range
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
</CustomSelect>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-1/2">
|
|
||||||
<CustomSelect
|
<div className="flex items-center justify-between px-5 py-4 gap-2 w-full">
|
||||||
value={projectDetails?.close_in}
|
<div className="w-1/2 text-sm font-medium">Auto-close Status</div>
|
||||||
label={`${projectDetails?.close_in} ${
|
<div className="w-1/2 ">
|
||||||
projectDetails?.close_in === 1 ? "Month" : "Months"
|
<CustomSearchSelect
|
||||||
}`}
|
value={
|
||||||
onChange={(val: number) => {
|
projectDetails?.default_state ? projectDetails?.default_state : defaultState
|
||||||
handleChange({ close_in: val });
|
}
|
||||||
}}
|
label={
|
||||||
input
|
<div className="flex items-center gap-2">
|
||||||
width="w-full"
|
{selectedOption ? (
|
||||||
>
|
<StateGroupIcon
|
||||||
<>
|
stateGroup={selectedOption.group}
|
||||||
{PROJECT_AUTOMATION_MONTHS.map((month) => (
|
color={selectedOption.color}
|
||||||
<CustomSelect.Option key={month.label} value={month.value}>
|
height="16px"
|
||||||
{month.label}
|
width="16px"
|
||||||
</CustomSelect.Option>
|
/>
|
||||||
))}
|
) : currentDefaultState ? (
|
||||||
<button
|
<StateGroupIcon
|
||||||
type="button"
|
stateGroup={currentDefaultState.group}
|
||||||
className="flex w-full select-none items-center rounded px-1 py-1.5 text-custom-text-200 hover:bg-custom-background-80"
|
color={currentDefaultState.color}
|
||||||
onClick={() => setmonthModal(true)}
|
height="16px"
|
||||||
>
|
width="16px"
|
||||||
Customize Time Range
|
/>
|
||||||
</button>
|
) : (
|
||||||
</>
|
<Squares2X2Icon className="h-3.5 w-3.5 text-custom-text-200" />
|
||||||
</CustomSelect>
|
)}
|
||||||
</div>
|
{selectedOption?.name
|
||||||
</div>
|
? selectedOption.name
|
||||||
<div className="flex items-center justify-between gap-2 w-full">
|
: currentDefaultState?.name ?? (
|
||||||
<div className="w-1/2 text-base font-medium">Auto-close Status</div>
|
<span className="text-custom-text-200">State</span>
|
||||||
<div className="w-1/2 ">
|
)}
|
||||||
<CustomSearchSelect
|
</div>
|
||||||
value={
|
}
|
||||||
projectDetails?.default_state ? projectDetails?.default_state : defaultState
|
onChange={(val: string) => {
|
||||||
}
|
handleChange({ default_state: val });
|
||||||
label={
|
}}
|
||||||
<div className="flex items-center gap-2">
|
options={options}
|
||||||
{selectedOption ? (
|
disabled={!multipleOptions}
|
||||||
<StateGroupIcon
|
width="w-full"
|
||||||
stateGroup={selectedOption.group}
|
input
|
||||||
color={selectedOption.color}
|
/>
|
||||||
height="16px"
|
</div>
|
||||||
width="16px"
|
|
||||||
/>
|
|
||||||
) : currentDefaultState ? (
|
|
||||||
<StateGroupIcon
|
|
||||||
stateGroup={currentDefaultState.group}
|
|
||||||
color={currentDefaultState.color}
|
|
||||||
height="16px"
|
|
||||||
width="16px"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Squares2X2Icon className="h-3.5 w-3.5 text-custom-text-200" />
|
|
||||||
)}
|
|
||||||
{selectedOption?.name
|
|
||||||
? selectedOption.name
|
|
||||||
: currentDefaultState?.name ?? (
|
|
||||||
<span className="text-custom-text-200">State</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
onChange={(val: string) => {
|
|
||||||
handleChange({ default_state: val });
|
|
||||||
}}
|
|
||||||
options={options}
|
|
||||||
disabled={!multipleOptions}
|
|
||||||
width="w-full"
|
|
||||||
input
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -104,7 +104,7 @@ export const SelectMonthModal: React.FC<Props> = ({
|
|||||||
as="h3"
|
as="h3"
|
||||||
className="text-lg font-medium leading-6 text-custom-text-100"
|
className="text-lg font-medium leading-6 text-custom-text-100"
|
||||||
>
|
>
|
||||||
Customize Time Range
|
Customise Time Range
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
<div className="mt-8 flex items-center gap-2">
|
<div className="mt-8 flex items-center gap-2">
|
||||||
<div className="flex w-full flex-col gap-1 justify-center">
|
<div className="flex w-full flex-col gap-1 justify-center">
|
||||||
|
@ -41,7 +41,7 @@ export const CommandPalette: React.FC = observer(() => {
|
|||||||
const [isCreateUpdatePageModalOpen, setIsCreateUpdatePageModalOpen] = useState(false);
|
const [isCreateUpdatePageModalOpen, setIsCreateUpdatePageModalOpen] = useState(false);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, issueId, inboxId } = router.query;
|
const { workspaceSlug, projectId, issueId, inboxId, cycleId, moduleId } = router.query;
|
||||||
|
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
|
|
||||||
@ -183,6 +183,13 @@ export const CommandPalette: React.FC = observer(() => {
|
|||||||
isOpen={isIssueModalOpen}
|
isOpen={isIssueModalOpen}
|
||||||
handleClose={() => setIsIssueModalOpen(false)}
|
handleClose={() => setIsIssueModalOpen(false)}
|
||||||
fieldsToShow={inboxId ? ["name", "description", "priority"] : ["all"]}
|
fieldsToShow={inboxId ? ["name", "description", "priority"] : ["all"]}
|
||||||
|
prePopulateData={
|
||||||
|
cycleId
|
||||||
|
? { cycle: cycleId.toString() }
|
||||||
|
: moduleId
|
||||||
|
? { module: moduleId.toString() }
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<BulkDeleteIssuesModal
|
<BulkDeleteIssuesModal
|
||||||
isOpen={isBulkDeleteIssuesModalOpen}
|
isOpen={isBulkDeleteIssuesModalOpen}
|
||||||
|
@ -1,14 +1,41 @@
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
import useSWR from "swr";
|
||||||
|
|
||||||
|
// hook
|
||||||
|
import useEstimateOption from "hooks/use-estimate-option";
|
||||||
|
// services
|
||||||
|
import issuesService from "services/issues.service";
|
||||||
// icons
|
// icons
|
||||||
import { Icon, Tooltip } from "components/ui";
|
import { Icon, Tooltip } from "components/ui";
|
||||||
import { Squares2X2Icon } from "@heroicons/react/24/outline";
|
import {
|
||||||
import { BlockedIcon, BlockerIcon } from "components/icons";
|
TagIcon,
|
||||||
|
CopyPlus,
|
||||||
|
Calendar,
|
||||||
|
Link2Icon,
|
||||||
|
RocketIcon,
|
||||||
|
Users2Icon,
|
||||||
|
ArchiveIcon,
|
||||||
|
PaperclipIcon,
|
||||||
|
ContrastIcon,
|
||||||
|
TriangleIcon,
|
||||||
|
LayoutGridIcon,
|
||||||
|
SignalMediumIcon,
|
||||||
|
MessageSquareIcon,
|
||||||
|
} from "lucide-react";
|
||||||
|
import {
|
||||||
|
BlockedIcon,
|
||||||
|
BlockerIcon,
|
||||||
|
RelatedIcon,
|
||||||
|
StackedLayersHorizontalIcon,
|
||||||
|
} from "components/icons";
|
||||||
// helpers
|
// helpers
|
||||||
import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
|
import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
|
||||||
import { capitalizeFirstLetter } from "helpers/string.helper";
|
import { capitalizeFirstLetter } from "helpers/string.helper";
|
||||||
// types
|
// types
|
||||||
import { IIssueActivity } from "types";
|
import { IIssueActivity } from "types";
|
||||||
|
// fetch-keys
|
||||||
|
import { WORKSPACE_LABELS } from "constants/fetch-keys";
|
||||||
|
|
||||||
const IssueLink = ({ activity }: { activity: IIssueActivity }) => {
|
const IssueLink = ({ activity }: { activity: IIssueActivity }) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -29,7 +56,7 @@ const IssueLink = ({ activity }: { activity: IIssueActivity }) => {
|
|||||||
{activity.issue_detail
|
{activity.issue_detail
|
||||||
? `${activity.project_detail.identifier}-${activity.issue_detail.sequence_id}`
|
? `${activity.project_detail.identifier}-${activity.issue_detail.sequence_id}`
|
||||||
: "Issue"}
|
: "Issue"}
|
||||||
<Icon iconName="launch" className="!text-xs" />
|
<RocketIcon size={12} color="#6b7280" />
|
||||||
</a>
|
</a>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
@ -51,6 +78,38 @@ const UserLink = ({ activity }: { activity: IIssueActivity }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const LabelPill = ({ labelId }: { labelId: string }) => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug } = router.query;
|
||||||
|
|
||||||
|
const { data: labels } = useSWR(
|
||||||
|
workspaceSlug ? WORKSPACE_LABELS(workspaceSlug.toString()) : null,
|
||||||
|
workspaceSlug ? () => issuesService.getWorkspaceLabels(workspaceSlug.toString()) : null
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className="h-1.5 w-1.5 rounded-full"
|
||||||
|
style={{
|
||||||
|
backgroundColor: labels?.find((l) => l.id === labelId)?.color ?? "#000000",
|
||||||
|
}}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
const EstimatePoint = ({ point }: { point: string }) => {
|
||||||
|
const { estimateValue, isEstimateActive } = useEstimateOption(Number(point));
|
||||||
|
const currentPoint = Number(point) + 1;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className="font-medium text-custom-text-100">
|
||||||
|
{isEstimateActive
|
||||||
|
? estimateValue
|
||||||
|
: `${currentPoint} ${currentPoint > 1 ? "points" : "point"}`}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const activityDetails: {
|
const activityDetails: {
|
||||||
[key: string]: {
|
[key: string]: {
|
||||||
message: (
|
message: (
|
||||||
@ -90,14 +149,14 @@ const activityDetails: {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: <Icon iconName="group" className="!text-sm" aria-hidden="true" />,
|
icon: <Users2Icon size={12} color="#6b7280" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
archived_at: {
|
archived_at: {
|
||||||
message: (activity) => {
|
message: (activity) => {
|
||||||
if (activity.new_value === "restore") return "restored the issue.";
|
if (activity.new_value === "restore") return "restored the issue.";
|
||||||
else return "archived the issue.";
|
else return "archived the issue.";
|
||||||
},
|
},
|
||||||
icon: <Icon iconName="archive" className="!text-sm" aria-hidden="true" />,
|
icon: <ArchiveIcon size={12} color="#6b7280" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
attachment: {
|
attachment: {
|
||||||
message: (activity, showIssue) => {
|
message: (activity, showIssue) => {
|
||||||
@ -112,7 +171,7 @@ const activityDetails: {
|
|||||||
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
||||||
>
|
>
|
||||||
attachment
|
attachment
|
||||||
<Icon iconName="launch" className="!text-xs" />
|
<RocketIcon size={12} color="#6b7280" />
|
||||||
</a>
|
</a>
|
||||||
{showIssue && (
|
{showIssue && (
|
||||||
<>
|
<>
|
||||||
@ -136,7 +195,7 @@ const activityDetails: {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: <Icon iconName="attach_file" className="!text-sm" aria-hidden="true" />,
|
icon: <PaperclipIcon size={12} color="#6b7280" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
blocking: {
|
blocking: {
|
||||||
message: (activity) => {
|
message: (activity) => {
|
||||||
@ -157,7 +216,7 @@ const activityDetails: {
|
|||||||
},
|
},
|
||||||
icon: <BlockerIcon height="12" width="12" color="#6b7280" />,
|
icon: <BlockerIcon height="12" width="12" color="#6b7280" />,
|
||||||
},
|
},
|
||||||
blocks: {
|
blocked_by: {
|
||||||
message: (activity) => {
|
message: (activity) => {
|
||||||
if (activity.old_value === "")
|
if (activity.old_value === "")
|
||||||
return (
|
return (
|
||||||
@ -176,6 +235,44 @@ const activityDetails: {
|
|||||||
},
|
},
|
||||||
icon: <BlockedIcon height="12" width="12" color="#6b7280" />,
|
icon: <BlockedIcon height="12" width="12" color="#6b7280" />,
|
||||||
},
|
},
|
||||||
|
duplicate: {
|
||||||
|
message: (activity) => {
|
||||||
|
if (activity.old_value === "")
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
marked this issue as duplicate of{" "}
|
||||||
|
<span className="font-medium text-custom-text-100">{activity.new_value}</span>.
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
else
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
removed this issue as a duplicate of{" "}
|
||||||
|
<span className="font-medium text-custom-text-100">{activity.old_value}</span>.
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
icon: <CopyPlus size={12} color="#6b7280" />,
|
||||||
|
},
|
||||||
|
relates_to: {
|
||||||
|
message: (activity) => {
|
||||||
|
if (activity.old_value === "")
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
marked that this issue relates to{" "}
|
||||||
|
<span className="font-medium text-custom-text-100">{activity.new_value}</span>.
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
else
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
removed the relation from{" "}
|
||||||
|
<span className="font-medium text-custom-text-100">{activity.old_value}</span>.
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
icon: <RelatedIcon height="12" width="12" color="#6b7280" />,
|
||||||
|
},
|
||||||
cycles: {
|
cycles: {
|
||||||
message: (activity, showIssue, workspaceSlug) => {
|
message: (activity, showIssue, workspaceSlug) => {
|
||||||
if (activity.verb === "created")
|
if (activity.verb === "created")
|
||||||
@ -189,7 +286,7 @@ const activityDetails: {
|
|||||||
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
||||||
>
|
>
|
||||||
{activity.new_value}
|
{activity.new_value}
|
||||||
<Icon iconName="launch" className="!text-xs" />
|
<RocketIcon size={12} color="#6b7280" />
|
||||||
</a>
|
</a>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@ -204,7 +301,7 @@ const activityDetails: {
|
|||||||
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
||||||
>
|
>
|
||||||
{activity.new_value}
|
{activity.new_value}
|
||||||
<Icon iconName="launch" className="!text-xs" />
|
<RocketIcon size={12} color="#6b7280" />
|
||||||
</a>
|
</a>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@ -219,12 +316,12 @@ const activityDetails: {
|
|||||||
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
||||||
>
|
>
|
||||||
{activity.old_value}
|
{activity.old_value}
|
||||||
<Icon iconName="launch" className="!text-xs" />
|
<RocketIcon size={12} color="#6b7280" />
|
||||||
</a>
|
</a>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: <Icon iconName="contrast" className="!text-sm" aria-hidden="true" />,
|
icon: <ContrastIcon size={12} color="#6b7280" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
message: (activity, showIssue) => (
|
message: (activity, showIssue) => (
|
||||||
@ -239,7 +336,7 @@ const activityDetails: {
|
|||||||
.
|
.
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
icon: <Icon iconName="chat" className="!text-sm" aria-hidden="true" />,
|
icon: <MessageSquareIcon size={12} color="#6b7280" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
estimate_point: {
|
estimate_point: {
|
||||||
message: (activity, showIssue) => {
|
message: (activity, showIssue) => {
|
||||||
@ -259,8 +356,7 @@ const activityDetails: {
|
|||||||
else
|
else
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
set the estimate point to{" "}
|
set the estimate point to <EstimatePoint point={activity.new_value} />
|
||||||
<span className="font-medium text-custom-text-100">{activity.new_value}</span>
|
|
||||||
{showIssue && (
|
{showIssue && (
|
||||||
<>
|
<>
|
||||||
{" "}
|
{" "}
|
||||||
@ -271,14 +367,14 @@ const activityDetails: {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: <Icon iconName="change_history" className="!text-sm" aria-hidden="true" />,
|
icon: <TriangleIcon size={12} color="#6b7280" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
issue: {
|
issue: {
|
||||||
message: (activity) => {
|
message: (activity) => {
|
||||||
if (activity.verb === "created") return "created the issue.";
|
if (activity.verb === "created") return "created the issue.";
|
||||||
else return "deleted an issue.";
|
else return "deleted an issue.";
|
||||||
},
|
},
|
||||||
icon: <Icon iconName="stack" className="!text-sm" aria-hidden="true" />,
|
icon: <StackedLayersHorizontalIcon width={12} height={12} color="#6b7280" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
labels: {
|
labels: {
|
||||||
message: (activity, showIssue) => {
|
message: (activity, showIssue) => {
|
||||||
@ -286,14 +382,8 @@ const activityDetails: {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
added a new label{" "}
|
added a new label{" "}
|
||||||
<span className="inline-flex items-center gap-3 rounded-full border border-custom-border-300 px-2 py-0.5 text-xs">
|
<span className="inline-flex items-center gap-2 rounded-full border border-custom-border-300 px-2 py-0.5 text-xs">
|
||||||
<span
|
<LabelPill labelId={activity.new_identifier ?? ""} />
|
||||||
className="h-1.5 w-1.5 rounded-full"
|
|
||||||
style={{
|
|
||||||
backgroundColor: "#000000",
|
|
||||||
}}
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
<span className="font-medium text-custom-text-100">{activity.new_value}</span>
|
<span className="font-medium text-custom-text-100">{activity.new_value}</span>
|
||||||
</span>
|
</span>
|
||||||
{showIssue && (
|
{showIssue && (
|
||||||
@ -309,13 +399,7 @@ const activityDetails: {
|
|||||||
<>
|
<>
|
||||||
removed the label{" "}
|
removed the label{" "}
|
||||||
<span className="inline-flex items-center gap-3 rounded-full border border-custom-border-300 px-2 py-0.5 text-xs">
|
<span className="inline-flex items-center gap-3 rounded-full border border-custom-border-300 px-2 py-0.5 text-xs">
|
||||||
<span
|
<LabelPill labelId={activity.old_identifier ?? ""} />
|
||||||
className="h-1.5 w-1.5 rounded-full"
|
|
||||||
style={{
|
|
||||||
backgroundColor: "#000000",
|
|
||||||
}}
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
<span className="font-medium text-custom-text-100">{activity.old_value}</span>
|
<span className="font-medium text-custom-text-100">{activity.old_value}</span>
|
||||||
</span>
|
</span>
|
||||||
{showIssue && (
|
{showIssue && (
|
||||||
@ -327,7 +411,7 @@ const activityDetails: {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: <Icon iconName="sell" className="!text-sm" aria-hidden="true" />,
|
icon: <TagIcon size={12} color="#6b7280" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
link: {
|
link: {
|
||||||
message: (activity, showIssue) => {
|
message: (activity, showIssue) => {
|
||||||
@ -342,7 +426,7 @@ const activityDetails: {
|
|||||||
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
||||||
>
|
>
|
||||||
link
|
link
|
||||||
<Icon iconName="launch" className="!text-xs" />
|
<RocketIcon size={12} color="#6b7280" />
|
||||||
</a>
|
</a>
|
||||||
{showIssue && (
|
{showIssue && (
|
||||||
<>
|
<>
|
||||||
@ -364,7 +448,7 @@ const activityDetails: {
|
|||||||
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
||||||
>
|
>
|
||||||
link
|
link
|
||||||
<Icon iconName="launch" className="!text-xs" />
|
<RocketIcon size={12} color="#6b7280" />
|
||||||
</a>
|
</a>
|
||||||
{showIssue && (
|
{showIssue && (
|
||||||
<>
|
<>
|
||||||
@ -386,7 +470,7 @@ const activityDetails: {
|
|||||||
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
||||||
>
|
>
|
||||||
link
|
link
|
||||||
<Icon iconName="launch" className="!text-xs" />
|
<RocketIcon size={12} color="#6b7280" />
|
||||||
</a>
|
</a>
|
||||||
{showIssue && (
|
{showIssue && (
|
||||||
<>
|
<>
|
||||||
@ -398,7 +482,7 @@ const activityDetails: {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: <Icon iconName="link" className="!text-sm" aria-hidden="true" />,
|
icon: <Link2Icon size={12} color="#6b7280" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
modules: {
|
modules: {
|
||||||
message: (activity, showIssue, workspaceSlug) => {
|
message: (activity, showIssue, workspaceSlug) => {
|
||||||
@ -413,7 +497,7 @@ const activityDetails: {
|
|||||||
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
||||||
>
|
>
|
||||||
{activity.new_value}
|
{activity.new_value}
|
||||||
<Icon iconName="launch" className="!text-xs" />
|
<RocketIcon size={12} color="#6b7280" />
|
||||||
</a>
|
</a>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@ -428,7 +512,7 @@ const activityDetails: {
|
|||||||
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
||||||
>
|
>
|
||||||
{activity.new_value}
|
{activity.new_value}
|
||||||
<Icon iconName="launch" className="!text-xs" />
|
<RocketIcon size={12} color="#6b7280" />
|
||||||
</a>
|
</a>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@ -443,12 +527,12 @@ const activityDetails: {
|
|||||||
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
||||||
>
|
>
|
||||||
{activity.old_value}
|
{activity.old_value}
|
||||||
<Icon iconName="launch" className="!text-xs" />
|
<RocketIcon size={12} color="#6b7280" />
|
||||||
</a>
|
</a>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: <Icon iconName="dataset" className="!text-sm" aria-hidden="true" />,
|
icon: <Icon iconName="dataset" className="!text-xs !text-[#6b7280]" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
name: {
|
name: {
|
||||||
message: (activity, showIssue) => (
|
message: (activity, showIssue) => (
|
||||||
@ -463,7 +547,7 @@ const activityDetails: {
|
|||||||
.
|
.
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
icon: <Icon iconName="chat" className="!text-sm" aria-hidden="true" />,
|
icon: <MessageSquareIcon size={12} color="#6b7280" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
parent: {
|
parent: {
|
||||||
message: (activity, showIssue) => {
|
message: (activity, showIssue) => {
|
||||||
@ -496,7 +580,13 @@ const activityDetails: {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: <Icon iconName="supervised_user_circle" className="!text-sm" aria-hidden="true" />,
|
icon: (
|
||||||
|
<Icon
|
||||||
|
iconName="supervised_user_circle"
|
||||||
|
className="!text-xs !text-[#6b7280]"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
priority: {
|
priority: {
|
||||||
message: (activity, showIssue) => (
|
message: (activity, showIssue) => (
|
||||||
@ -514,7 +604,7 @@ const activityDetails: {
|
|||||||
.
|
.
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
icon: <Icon iconName="signal_cellular_alt" className="!text-sm" aria-hidden="true" />,
|
icon: <SignalMediumIcon size={12} color="#6b7280" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
start_date: {
|
start_date: {
|
||||||
message: (activity, showIssue) => {
|
message: (activity, showIssue) => {
|
||||||
@ -548,7 +638,7 @@ const activityDetails: {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: <Icon iconName="calendar_today" className="!text-sm" aria-hidden="true" />,
|
icon: <Calendar size={12} color="#6b7280" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
state: {
|
state: {
|
||||||
message: (activity, showIssue) => (
|
message: (activity, showIssue) => (
|
||||||
@ -564,7 +654,7 @@ const activityDetails: {
|
|||||||
.
|
.
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
icon: <Squares2X2Icon className="h-3 w-3" aria-hidden="true" />,
|
icon: <LayoutGridIcon size={12} color="#6b7280" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
target_date: {
|
target_date: {
|
||||||
message: (activity, showIssue) => {
|
message: (activity, showIssue) => {
|
||||||
@ -598,7 +688,7 @@ const activityDetails: {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: <Icon iconName="calendar_today" className="!text-sm" aria-hidden="true" />,
|
icon: <Calendar size={12} color="#6b7280" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -167,7 +167,9 @@ export const FiltersList: React.FC<Props> = ({
|
|||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setFilters({
|
setFilters({
|
||||||
assignees: filters.assignees?.filter((p: any) => p !== memberId),
|
subscriber: filters.subscriber?.filter(
|
||||||
|
(p: any) => p !== memberId
|
||||||
|
),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
export * from "./date-filter-modal";
|
export * from "./date-filter-modal";
|
||||||
export * from "./date-filter-select";
|
export * from "./date-filter-select";
|
||||||
export * from "./filters-list";
|
export * from "./filters-list";
|
||||||
|
export * from "./workspace-filters-list";
|
||||||
export * from "./issues-view-filter";
|
export * from "./issues-view-filter";
|
||||||
|
@ -52,22 +52,26 @@ const issueViewOptions: { type: TIssueViewOptions; Icon: any }[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const issueViewForDraftIssues: { type: TIssueViewOptions; Icon: any }[] = [
|
||||||
|
{
|
||||||
|
type: "list",
|
||||||
|
Icon: FormatListBulletedOutlined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "kanban",
|
||||||
|
Icon: GridViewOutlined,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
export const IssuesFilterView: React.FC = () => {
|
export const IssuesFilterView: React.FC = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, viewId } = router.query;
|
const { workspaceSlug, projectId, viewId } = router.query;
|
||||||
const isArchivedIssues = router.pathname.includes("archived-issues");
|
const isArchivedIssues = router.pathname.includes("archived-issues");
|
||||||
|
const isDraftIssues = router.pathname?.split("/")?.[4] === "draft-issues";
|
||||||
|
|
||||||
const {
|
const {
|
||||||
issueView,
|
displayFilters,
|
||||||
setIssueView,
|
setDisplayFilters,
|
||||||
groupByProperty,
|
|
||||||
setGroupByProperty,
|
|
||||||
orderBy,
|
|
||||||
setOrderBy,
|
|
||||||
showEmptyGroups,
|
|
||||||
showSubIssues,
|
|
||||||
setShowSubIssues,
|
|
||||||
setShowEmptyGroups,
|
|
||||||
filters,
|
filters,
|
||||||
setFilters,
|
setFilters,
|
||||||
resetFilterToDefault,
|
resetFilterToDefault,
|
||||||
@ -83,9 +87,41 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{!isArchivedIssues && (
|
{!isArchivedIssues && !isDraftIssues && (
|
||||||
<div className="flex items-center gap-x-1">
|
<div className="flex items-center gap-x-1">
|
||||||
{issueViewOptions.map((option) => (
|
{issueViewOptions.map((option) => (
|
||||||
|
<Tooltip
|
||||||
|
key={option.type}
|
||||||
|
tooltipContent={
|
||||||
|
<span className="capitalize">
|
||||||
|
{replaceUnderscoreIfSnakeCase(option.type)} Layout
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
position="bottom"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none hover:bg-custom-sidebar-background-80 duration-300 ${
|
||||||
|
displayFilters.layout === option.type
|
||||||
|
? "bg-custom-sidebar-background-80"
|
||||||
|
: "text-custom-sidebar-text-200"
|
||||||
|
}`}
|
||||||
|
onClick={() => setDisplayFilters({ layout: option.type })}
|
||||||
|
>
|
||||||
|
<option.Icon
|
||||||
|
sx={{
|
||||||
|
fontSize: 16,
|
||||||
|
}}
|
||||||
|
className={option.type === "gantt_chart" ? "rotate-90" : ""}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{isDraftIssues && (
|
||||||
|
<div className="flex items-center gap-x-1">
|
||||||
|
{issueViewForDraftIssues.map((option) => (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
key={option.type}
|
key={option.type}
|
||||||
tooltipContent={
|
tooltipContent={
|
||||||
@ -96,11 +132,11 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none hover:bg-custom-sidebar-background-80 duration-300 ${
|
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none hover:bg-custom-sidebar-background-80 duration-300 ${
|
||||||
issueView === option.type
|
displayFilters.layout === option.type
|
||||||
? "bg-custom-sidebar-background-80"
|
? "bg-custom-sidebar-background-80"
|
||||||
: "text-custom-sidebar-text-200"
|
: "text-custom-sidebar-text-200"
|
||||||
}`}
|
}`}
|
||||||
onClick={() => setIssueView(option.type)}
|
onClick={() => setDisplayFilters({ layout: option.type })}
|
||||||
>
|
>
|
||||||
<option.Icon
|
<option.Icon
|
||||||
sx={{
|
sx={{
|
||||||
@ -174,28 +210,33 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
<Popover.Panel className="absolute right-0 z-30 mt-1 w-screen max-w-xs transform rounded-lg border border-custom-border-200 bg-custom-background-90 p-3 shadow-lg">
|
<Popover.Panel className="absolute right-0 z-30 mt-1 w-screen max-w-xs transform rounded-lg border border-custom-border-200 bg-custom-background-90 p-3 shadow-lg">
|
||||||
<div className="relative divide-y-2 divide-custom-border-200">
|
<div className="relative divide-y-2 divide-custom-border-200">
|
||||||
<div className="space-y-4 pb-3 text-xs">
|
<div className="space-y-4 pb-3 text-xs">
|
||||||
{issueView !== "calendar" &&
|
{displayFilters.layout !== "calendar" &&
|
||||||
issueView !== "spreadsheet" &&
|
displayFilters.layout !== "spreadsheet" &&
|
||||||
issueView !== "gantt_chart" && (
|
displayFilters.layout !== "gantt_chart" && (
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h4 className="text-custom-text-200">Group by</h4>
|
<h4 className="text-custom-text-200">Group by</h4>
|
||||||
<div className="w-28">
|
<div className="w-28">
|
||||||
<CustomMenu
|
<CustomMenu
|
||||||
label={
|
label={
|
||||||
GROUP_BY_OPTIONS.find((option) => option.key === groupByProperty)
|
GROUP_BY_OPTIONS.find(
|
||||||
?.name ?? "Select"
|
(option) => option.key === displayFilters.group_by
|
||||||
|
)?.name ?? "Select"
|
||||||
}
|
}
|
||||||
className="!w-full"
|
className="!w-full"
|
||||||
buttonClassName="w-full"
|
buttonClassName="w-full"
|
||||||
>
|
>
|
||||||
{GROUP_BY_OPTIONS.map((option) => {
|
{GROUP_BY_OPTIONS.map((option) => {
|
||||||
if (issueView === "kanban" && option.key === null) return null;
|
if (displayFilters.layout === "kanban" && option.key === null)
|
||||||
|
return null;
|
||||||
if (option.key === "project") return null;
|
if (option.key === "project") return null;
|
||||||
|
|
||||||
|
if (isDraftIssues && option.key === "state_detail.group")
|
||||||
|
return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CustomMenu.MenuItem
|
<CustomMenu.MenuItem
|
||||||
key={option.key}
|
key={option.key}
|
||||||
onClick={() => setGroupByProperty(option.key)}
|
onClick={() => setDisplayFilters({ group_by: option.key })}
|
||||||
>
|
>
|
||||||
{option.name}
|
{option.name}
|
||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
@ -205,88 +246,101 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{issueView !== "calendar" && issueView !== "spreadsheet" && (
|
{displayFilters.layout !== "calendar" &&
|
||||||
|
displayFilters.layout !== "spreadsheet" && (
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h4 className="text-custom-text-200">Order by</h4>
|
||||||
|
<div className="w-28">
|
||||||
|
<CustomMenu
|
||||||
|
label={
|
||||||
|
ORDER_BY_OPTIONS.find(
|
||||||
|
(option) => option.key === displayFilters.order_by
|
||||||
|
)?.name ?? "Select"
|
||||||
|
}
|
||||||
|
className="!w-full"
|
||||||
|
buttonClassName="w-full"
|
||||||
|
>
|
||||||
|
{ORDER_BY_OPTIONS.map((option) =>
|
||||||
|
displayFilters.group_by === "priority" &&
|
||||||
|
option.key === "priority" ? null : (
|
||||||
|
<CustomMenu.MenuItem
|
||||||
|
key={option.key}
|
||||||
|
onClick={() => {
|
||||||
|
setDisplayFilters({ order_by: option.key });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{option.name}
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</CustomMenu>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!isArchivedIssues && (
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h4 className="text-custom-text-200">Order by</h4>
|
<h4 className="text-custom-text-200">Issue type</h4>
|
||||||
<div className="w-28">
|
<div className="w-28">
|
||||||
<CustomMenu
|
<CustomMenu
|
||||||
label={
|
label={
|
||||||
ORDER_BY_OPTIONS.find((option) => option.key === orderBy)?.name ??
|
FILTER_ISSUE_OPTIONS.find(
|
||||||
"Select"
|
(option) => option.key === displayFilters.type
|
||||||
|
)?.name ?? "Select"
|
||||||
}
|
}
|
||||||
className="!w-full"
|
className="!w-full"
|
||||||
buttonClassName="w-full"
|
buttonClassName="w-full"
|
||||||
>
|
>
|
||||||
{ORDER_BY_OPTIONS.map((option) =>
|
{FILTER_ISSUE_OPTIONS.map((option) => (
|
||||||
groupByProperty === "priority" && option.key === "priority" ? null : (
|
<CustomMenu.MenuItem
|
||||||
<CustomMenu.MenuItem
|
key={option.key}
|
||||||
key={option.key}
|
onClick={() =>
|
||||||
onClick={() => {
|
setDisplayFilters({
|
||||||
setOrderBy(option.key);
|
type: option.key,
|
||||||
}}
|
})
|
||||||
>
|
}
|
||||||
{option.name}
|
>
|
||||||
</CustomMenu.MenuItem>
|
{option.name}
|
||||||
)
|
</CustomMenu.MenuItem>
|
||||||
)}
|
))}
|
||||||
</CustomMenu>
|
</CustomMenu>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<h4 className="text-custom-text-200">Issue type</h4>
|
|
||||||
<div className="w-28">
|
|
||||||
<CustomMenu
|
|
||||||
label={
|
|
||||||
FILTER_ISSUE_OPTIONS.find((option) => option.key === filters.type)
|
|
||||||
?.name ?? "Select"
|
|
||||||
}
|
|
||||||
className="!w-full"
|
|
||||||
buttonClassName="w-full"
|
|
||||||
>
|
|
||||||
{FILTER_ISSUE_OPTIONS.map((option) => (
|
|
||||||
<CustomMenu.MenuItem
|
|
||||||
key={option.key}
|
|
||||||
onClick={() =>
|
|
||||||
setFilters({
|
|
||||||
type: option.key,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{option.name}
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
))}
|
|
||||||
</CustomMenu>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{issueView !== "calendar" && issueView !== "spreadsheet" && (
|
{displayFilters.layout !== "calendar" &&
|
||||||
<div className="flex items-center justify-between">
|
displayFilters.layout !== "spreadsheet" && (
|
||||||
<h4 className="text-custom-text-200">Show sub-issues</h4>
|
|
||||||
<div className="w-28">
|
|
||||||
<ToggleSwitch
|
|
||||||
value={showSubIssues}
|
|
||||||
onChange={() => setShowSubIssues(!showSubIssues)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{issueView !== "calendar" &&
|
|
||||||
issueView !== "spreadsheet" &&
|
|
||||||
issueView !== "gantt_chart" && (
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h4 className="text-custom-text-200">Show empty states</h4>
|
<h4 className="text-custom-text-200">Show sub-issues</h4>
|
||||||
<div className="w-28">
|
<div className="w-28">
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
value={showEmptyGroups}
|
value={displayFilters.sub_issue ?? true}
|
||||||
onChange={() => setShowEmptyGroups(!showEmptyGroups)}
|
onChange={() =>
|
||||||
|
setDisplayFilters({ sub_issue: !displayFilters.sub_issue })
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{issueView !== "calendar" &&
|
{displayFilters.layout !== "calendar" &&
|
||||||
issueView !== "spreadsheet" &&
|
displayFilters.layout !== "spreadsheet" &&
|
||||||
issueView !== "gantt_chart" && (
|
displayFilters.layout !== "gantt_chart" && (
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h4 className="text-custom-text-200">Show empty groups</h4>
|
||||||
|
<div className="w-28">
|
||||||
|
<ToggleSwitch
|
||||||
|
value={displayFilters.show_empty_groups ?? true}
|
||||||
|
onChange={() =>
|
||||||
|
setDisplayFilters({
|
||||||
|
show_empty_groups: !displayFilters.show_empty_groups,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{displayFilters.layout !== "calendar" &&
|
||||||
|
displayFilters.layout !== "spreadsheet" &&
|
||||||
|
displayFilters.layout !== "gantt_chart" && (
|
||||||
<div className="relative flex justify-end gap-x-3">
|
<div className="relative flex justify-end gap-x-3">
|
||||||
<button type="button" onClick={() => resetFilterToDefault()}>
|
<button type="button" onClick={() => resetFilterToDefault()}>
|
||||||
Reset to default
|
Reset to default
|
||||||
@ -302,7 +356,7 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{issueView !== "gantt_chart" && (
|
{displayFilters.layout !== "gantt_chart" && (
|
||||||
<div className="space-y-2 py-3">
|
<div className="space-y-2 py-3">
|
||||||
<h4 className="text-sm text-custom-text-200">Display Properties</h4>
|
<h4 className="text-sm text-custom-text-200">Display Properties</h4>
|
||||||
<div className="flex flex-wrap items-center gap-2 text-custom-text-200">
|
<div className="flex flex-wrap items-center gap-2 text-custom-text-200">
|
||||||
@ -310,7 +364,7 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
if (key === "estimate" && !isEstimateActive) return null;
|
if (key === "estimate" && !isEstimateActive) return null;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
issueView === "spreadsheet" &&
|
displayFilters.layout === "spreadsheet" &&
|
||||||
(key === "attachment_count" ||
|
(key === "attachment_count" ||
|
||||||
key === "link" ||
|
key === "link" ||
|
||||||
key === "sub_issue_count")
|
key === "sub_issue_count")
|
||||||
@ -318,7 +372,7 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
return null;
|
return null;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
issueView !== "spreadsheet" &&
|
displayFilters.layout !== "spreadsheet" &&
|
||||||
(key === "created_on" || key === "updated_on")
|
(key === "created_on" || key === "updated_on")
|
||||||
)
|
)
|
||||||
return null;
|
return null;
|
||||||
|
364
web/components/core/filters/workspace-filters-list.tsx
Normal file
364
web/components/core/filters/workspace-filters-list.tsx
Normal file
@ -0,0 +1,364 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
// icons
|
||||||
|
import { XMarkIcon } from "@heroicons/react/24/outline";
|
||||||
|
import { PriorityIcon, StateGroupIcon } from "components/icons";
|
||||||
|
// ui
|
||||||
|
import { Avatar } from "components/ui";
|
||||||
|
// helpers
|
||||||
|
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
||||||
|
// helpers
|
||||||
|
import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
|
||||||
|
// types
|
||||||
|
import {
|
||||||
|
IIssueLabels,
|
||||||
|
IProject,
|
||||||
|
IUserLite,
|
||||||
|
IWorkspaceIssueFilterOptions,
|
||||||
|
TStateGroups,
|
||||||
|
} from "types";
|
||||||
|
// constants
|
||||||
|
import { STATE_GROUP_COLORS } from "constants/state";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
filters: Partial<IWorkspaceIssueFilterOptions>;
|
||||||
|
setFilters: (updatedFilter: Partial<IWorkspaceIssueFilterOptions>) => void;
|
||||||
|
clearAllFilters: (...args: any) => void;
|
||||||
|
labels: IIssueLabels[] | undefined;
|
||||||
|
members: IUserLite[] | undefined;
|
||||||
|
stateGroup: string[] | undefined;
|
||||||
|
project?: IProject[] | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WorkspaceFiltersList: React.FC<Props> = ({
|
||||||
|
filters,
|
||||||
|
setFilters,
|
||||||
|
clearAllFilters,
|
||||||
|
labels,
|
||||||
|
members,
|
||||||
|
stateGroup,
|
||||||
|
project,
|
||||||
|
}) => {
|
||||||
|
if (!filters) return <></>;
|
||||||
|
|
||||||
|
const nullFilters = Object.keys(filters).filter(
|
||||||
|
(key) => filters[key as keyof IWorkspaceIssueFilterOptions] === null
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-1 flex-wrap items-center gap-2 text-xs">
|
||||||
|
{Object.keys(filters).map((filterKey) => {
|
||||||
|
const key = filterKey as keyof typeof filters;
|
||||||
|
|
||||||
|
if (filters[key] === null || (filters[key]?.length ?? 0) <= 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={key}
|
||||||
|
className="flex items-center gap-x-2 rounded-full border border-custom-border-200 bg-custom-background-80 px-2 py-1"
|
||||||
|
>
|
||||||
|
<span className="capitalize text-custom-text-200">
|
||||||
|
{key === "target_date" ? "Due Date" : replaceUnderscoreIfSnakeCase(key)}:
|
||||||
|
</span>
|
||||||
|
{filters[key] === null || (filters[key]?.length ?? 0) <= 0 ? (
|
||||||
|
<span className="inline-flex items-center px-2 py-0.5 font-medium">None</span>
|
||||||
|
) : Array.isArray(filters[key]) ? (
|
||||||
|
<div className="space-x-2">
|
||||||
|
<div className="flex flex-wrap items-center gap-1">
|
||||||
|
{key === "state_group"
|
||||||
|
? filters.state_group?.map((stateGroup) => {
|
||||||
|
const group = stateGroup as TStateGroups;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<p
|
||||||
|
key={group}
|
||||||
|
className="inline-flex items-center gap-x-1 rounded-full px-2 py-0.5 capitalize"
|
||||||
|
style={{
|
||||||
|
color: STATE_GROUP_COLORS[group],
|
||||||
|
backgroundColor: `${STATE_GROUP_COLORS[group]}20`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<StateGroupIcon stateGroup={group} color={undefined} />
|
||||||
|
</span>
|
||||||
|
<span>{group}</span>
|
||||||
|
<span
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={() =>
|
||||||
|
setFilters({
|
||||||
|
state_group: filters.state_group?.filter((g) => g !== group),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<XMarkIcon className="h-3 w-3" />
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
: key === "priority"
|
||||||
|
? filters.priority?.map((priority: any) => (
|
||||||
|
<p
|
||||||
|
key={priority}
|
||||||
|
className={`inline-flex items-center gap-x-1 rounded-full px-2 py-0.5 capitalize ${
|
||||||
|
priority === "urgent"
|
||||||
|
? "bg-red-500/20 text-red-500"
|
||||||
|
: priority === "high"
|
||||||
|
? "bg-orange-500/20 text-orange-500"
|
||||||
|
: priority === "medium"
|
||||||
|
? "bg-yellow-500/20 text-yellow-500"
|
||||||
|
: priority === "low"
|
||||||
|
? "bg-green-500/20 text-green-500"
|
||||||
|
: "bg-custom-background-90 text-custom-text-200"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<PriorityIcon priority={priority} />
|
||||||
|
</span>
|
||||||
|
<span>{priority === "null" ? "None" : priority}</span>
|
||||||
|
<span
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={() =>
|
||||||
|
setFilters({
|
||||||
|
priority: filters.priority?.filter((p: any) => p !== priority),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<XMarkIcon className="h-3 w-3" />
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
))
|
||||||
|
: key === "assignees"
|
||||||
|
? filters.assignees?.map((memberId: string) => {
|
||||||
|
const member = members?.find((m) => m.id === memberId);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={memberId}
|
||||||
|
className="inline-flex items-center gap-x-1 rounded-full bg-custom-background-90 px-1"
|
||||||
|
>
|
||||||
|
<Avatar user={member} />
|
||||||
|
<span>{member?.display_name}</span>
|
||||||
|
<span
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={() =>
|
||||||
|
setFilters({
|
||||||
|
assignees: filters.assignees?.filter((p: any) => p !== memberId),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<XMarkIcon className="h-3 w-3" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
: key === "subscriber"
|
||||||
|
? filters.subscriber?.map((memberId: string) => {
|
||||||
|
const member = members?.find((m) => m.id === memberId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={memberId}
|
||||||
|
className="inline-flex items-center gap-x-1 rounded-full bg-custom-background-90 px-1"
|
||||||
|
>
|
||||||
|
<Avatar user={member} />
|
||||||
|
<span>{member?.display_name}</span>
|
||||||
|
<span
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={() =>
|
||||||
|
setFilters({
|
||||||
|
assignees: filters.assignees?.filter((p: any) => p !== memberId),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<XMarkIcon className="h-3 w-3" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
: key === "created_by"
|
||||||
|
? filters.created_by?.map((memberId: string) => {
|
||||||
|
const member = members?.find((m) => m.id === memberId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={`${memberId}-${key}`}
|
||||||
|
className="inline-flex items-center gap-x-1 rounded-full bg-custom-background-90 px-1 capitalize"
|
||||||
|
>
|
||||||
|
<Avatar user={member} />
|
||||||
|
<span>{member?.display_name}</span>
|
||||||
|
<span
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={() =>
|
||||||
|
setFilters({
|
||||||
|
created_by: filters.created_by?.filter(
|
||||||
|
(p: any) => p !== memberId
|
||||||
|
),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<XMarkIcon className="h-3 w-3" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
: key === "labels"
|
||||||
|
? filters.labels?.map((labelId: string) => {
|
||||||
|
const label = labels?.find((l) => l.id === labelId);
|
||||||
|
|
||||||
|
if (!label) return null;
|
||||||
|
const color = label.color !== "" ? label.color : "#0f172a";
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="inline-flex items-center gap-x-1 rounded-full px-2 py-0.5"
|
||||||
|
style={{
|
||||||
|
color: color,
|
||||||
|
backgroundColor: `${color}20`, // add 20% opacity
|
||||||
|
}}
|
||||||
|
key={labelId}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="h-1.5 w-1.5 rounded-full"
|
||||||
|
style={{
|
||||||
|
backgroundColor: color,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span>{label.name}</span>
|
||||||
|
<span
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={() =>
|
||||||
|
setFilters({
|
||||||
|
labels: filters.labels?.filter((l: any) => l !== labelId),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<XMarkIcon
|
||||||
|
className="h-3 w-3"
|
||||||
|
style={{
|
||||||
|
color: color,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</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;
|
||||||
|
|
||||||
|
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({
|
||||||
|
target_date: filters.target_date?.filter((d: any) => d !== date),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<XMarkIcon className="h-3 w-3" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
: key === "project"
|
||||||
|
? filters.project?.map((projectId) => {
|
||||||
|
const currentProject = project?.find((p) => p.id === projectId);
|
||||||
|
return (
|
||||||
|
<p
|
||||||
|
key={currentProject?.id}
|
||||||
|
className="inline-flex items-center gap-x-1 rounded-full px-2 py-0.5 capitalize"
|
||||||
|
>
|
||||||
|
<span>{currentProject?.name}</span>
|
||||||
|
<span
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={() =>
|
||||||
|
setFilters({
|
||||||
|
project: filters.project?.filter((p) => p !== projectId),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<XMarkIcon className="h-3 w-3" />
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
: (filters[key] as any)?.join(", ")}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
setFilters({
|
||||||
|
[key]: null,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<XMarkIcon className="h-3 w-3" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center gap-x-1 capitalize">
|
||||||
|
{filters[key as keyof typeof filters]}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
setFilters({
|
||||||
|
[key]: null,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<XMarkIcon className="h-3 w-3" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{Object.keys(filters).length > 0 && nullFilters.length !== Object.keys(filters).length && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={clearAllFilters}
|
||||||
|
className="flex items-center gap-x-1 rounded-full border border-custom-border-200 bg-custom-background-80 px-3 py-1.5 text-xs"
|
||||||
|
>
|
||||||
|
<span>Clear all filters</span>
|
||||||
|
<XMarkIcon className="h-3 w-3" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -20,6 +20,7 @@ import fileService from "services/file.service";
|
|||||||
import { Input, Spinner, PrimaryButton, SecondaryButton } from "components/ui";
|
import { Input, Spinner, PrimaryButton, SecondaryButton } from "components/ui";
|
||||||
// hooks
|
// hooks
|
||||||
import useWorkspaceDetails from "hooks/use-workspace-details";
|
import useWorkspaceDetails from "hooks/use-workspace-details";
|
||||||
|
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||||
|
|
||||||
const unsplashEnabled =
|
const unsplashEnabled =
|
||||||
process.env.NEXT_PUBLIC_UNSPLASH_ENABLED === "true" ||
|
process.env.NEXT_PUBLIC_UNSPLASH_ENABLED === "true" ||
|
||||||
@ -67,6 +68,8 @@ export const ImagePickerPopover: React.FC<Props> = ({
|
|||||||
fileService.getUnsplashImages(1, searchParams)
|
fileService.getUnsplashImages(1, searchParams)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const imagePickerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const { workspaceDetails } = useWorkspaceDetails();
|
const { workspaceDetails } = useWorkspaceDetails();
|
||||||
|
|
||||||
const onDrop = useCallback((acceptedFiles: File[]) => {
|
const onDrop = useCallback((acceptedFiles: File[]) => {
|
||||||
@ -116,12 +119,14 @@ export const ImagePickerPopover: React.FC<Props> = ({
|
|||||||
onChange(images[0].urls.regular);
|
onChange(images[0].urls.regular);
|
||||||
}, [value, onChange, images]);
|
}, [value, onChange, images]);
|
||||||
|
|
||||||
|
useOutsideClickDetector(imagePickerRef, () => setIsOpen(false));
|
||||||
|
|
||||||
if (!unsplashEnabled) return null;
|
if (!unsplashEnabled) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover className="relative z-[2]" ref={ref}>
|
<Popover className="relative z-[2]" ref={ref}>
|
||||||
<Popover.Button
|
<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"
|
className="rounded-sm 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)}
|
onClick={() => setIsOpen((prev) => !prev)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
@ -137,7 +142,10 @@ export const ImagePickerPopover: React.FC<Props> = ({
|
|||||||
leaveTo="transform opacity-0 scale-95"
|
leaveTo="transform opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Popover.Panel className="absolute right-0 z-10 mt-2 rounded-md border border-custom-border-200 bg-custom-background-80 shadow-lg">
|
<Popover.Panel className="absolute right-0 z-10 mt-2 rounded-md border border-custom-border-200 bg-custom-background-80 shadow-lg">
|
||||||
<div className="h-96 md:h-[28rem] w-80 md:w-[36rem] flex flex-col overflow-auto rounded border border-custom-border-300 bg-custom-background-100 p-3 shadow-2xl">
|
<div
|
||||||
|
ref={imagePickerRef}
|
||||||
|
className="h-96 md:h-[28rem] w-80 md:w-[36rem] flex flex-col overflow-auto rounded border border-custom-border-300 bg-custom-background-100 p-3 shadow-2xl"
|
||||||
|
>
|
||||||
<Tab.Group>
|
<Tab.Group>
|
||||||
<div>
|
<div>
|
||||||
<Tab.List as="span" className="inline-block rounded bg-custom-background-80 p-1">
|
<Tab.List as="span" className="inline-block rounded bg-custom-background-80 p-1">
|
||||||
|
@ -58,7 +58,7 @@ export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen, user
|
|||||||
);
|
);
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
const { issueView, params } = useIssuesView();
|
const { displayFilters, params } = useIssuesView();
|
||||||
const { params: calendarParams } = useCalendarIssuesView();
|
const { params: calendarParams } = useCalendarIssuesView();
|
||||||
const { order_by, group_by, ...viewGanttParams } = params;
|
const { order_by, group_by, ...viewGanttParams } = params;
|
||||||
|
|
||||||
@ -126,8 +126,8 @@ export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen, user
|
|||||||
message: "Issues deleted successfully!",
|
message: "Issues deleted successfully!",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (issueView === "calendar") mutate(calendarFetchKey);
|
if (displayFilters.layout === "calendar") mutate(calendarFetchKey);
|
||||||
else if (issueView === "gantt_chart") mutate(ganttFetchKey);
|
else if (displayFilters.layout === "gantt_chart") mutate(ganttFetchKey);
|
||||||
else {
|
else {
|
||||||
if (cycleId) {
|
if (cycleId) {
|
||||||
mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), params));
|
mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), params));
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import React, { useCallback, useState } from "react";
|
import React, { useCallback, useState } from "react";
|
||||||
|
|
||||||
import NextImage from "next/image";
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
// react-dropzone
|
// react-dropzone
|
||||||
@ -12,7 +11,7 @@ import fileServices from "services/file.service";
|
|||||||
// hooks
|
// hooks
|
||||||
import useWorkspaceDetails from "hooks/use-workspace-details";
|
import useWorkspaceDetails from "hooks/use-workspace-details";
|
||||||
// ui
|
// ui
|
||||||
import { PrimaryButton, SecondaryButton } from "components/ui";
|
import { DangerButton, PrimaryButton, SecondaryButton } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
import { UserCircleIcon } from "components/icons";
|
import { UserCircleIcon } from "components/icons";
|
||||||
|
|
||||||
@ -21,6 +20,8 @@ type Props = {
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onSuccess: (url: string) => void;
|
onSuccess: (url: string) => void;
|
||||||
|
isRemoving: boolean;
|
||||||
|
handleDelete: () => void;
|
||||||
userImage?: boolean;
|
userImage?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -29,6 +30,8 @@ export const ImageUploadModal: React.FC<Props> = ({
|
|||||||
onSuccess,
|
onSuccess,
|
||||||
isOpen,
|
isOpen,
|
||||||
onClose,
|
onClose,
|
||||||
|
isRemoving,
|
||||||
|
handleDelete,
|
||||||
userImage,
|
userImage,
|
||||||
}) => {
|
}) => {
|
||||||
const [image, setImage] = useState<File | null>(null);
|
const [image, setImage] = useState<File | null>(null);
|
||||||
@ -148,12 +151,10 @@ export const ImageUploadModal: React.FC<Props> = ({
|
|||||||
>
|
>
|
||||||
Edit
|
Edit
|
||||||
</button>
|
</button>
|
||||||
<NextImage
|
<img
|
||||||
layout="fill"
|
|
||||||
objectFit="cover"
|
|
||||||
src={image ? URL.createObjectURL(image) : value ? value : ""}
|
src={image ? URL.createObjectURL(image) : value ? value : ""}
|
||||||
alt="image"
|
alt="image"
|
||||||
className="rounded-lg"
|
className="absolute top-0 left-0 h-full w-full object-cover rounded-md"
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
@ -182,15 +183,22 @@ export const ImageUploadModal: React.FC<Props> = ({
|
|||||||
<p className="my-4 text-custom-text-200 text-sm">
|
<p className="my-4 text-custom-text-200 text-sm">
|
||||||
File formats supported- .jpeg, .jpg, .png, .webp, .svg
|
File formats supported- .jpeg, .jpg, .png, .webp, .svg
|
||||||
</p>
|
</p>
|
||||||
<div className="flex items-center justify-end gap-2">
|
<div className="flex items-center justify-between">
|
||||||
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
|
<div className="flex items-center">
|
||||||
<PrimaryButton
|
<DangerButton onClick={handleDelete} outline disabled={!value}>
|
||||||
onClick={handleSubmit}
|
{isRemoving ? "Removing..." : "Remove"}
|
||||||
disabled={!image}
|
</DangerButton>
|
||||||
loading={isImageUploading}
|
</div>
|
||||||
>
|
<div className="flex items-center gap-2">
|
||||||
{isImageUploading ? "Uploading..." : "Upload & Save"}
|
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
|
||||||
</PrimaryButton>
|
<PrimaryButton
|
||||||
|
onClick={handleSubmit}
|
||||||
|
disabled={!image}
|
||||||
|
loading={isImageUploading}
|
||||||
|
>
|
||||||
|
{isImageUploading ? "Uploading..." : "Upload & Save"}
|
||||||
|
</PrimaryButton>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Dialog.Panel>
|
</Dialog.Panel>
|
||||||
</Transition.Child>
|
</Transition.Child>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useCallback } from "react";
|
import React, { useCallback, useState } from "react";
|
||||||
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
@ -12,6 +12,7 @@ import stateService from "services/state.service";
|
|||||||
// hooks
|
// hooks
|
||||||
import useUser from "hooks/use-user";
|
import useUser from "hooks/use-user";
|
||||||
import { useProjectMyMembership } from "contexts/project-member.context";
|
import { useProjectMyMembership } from "contexts/project-member.context";
|
||||||
|
import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view";
|
||||||
// components
|
// components
|
||||||
import {
|
import {
|
||||||
AllLists,
|
AllLists,
|
||||||
@ -50,6 +51,7 @@ type Props = {
|
|||||||
secondaryButton?: React.ReactNode;
|
secondaryButton?: React.ReactNode;
|
||||||
};
|
};
|
||||||
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
|
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
|
||||||
|
handleDraftIssueAction?: (issue: IIssue, action: "edit" | "delete") => void;
|
||||||
handleOnDragEnd: (result: DropResult) => Promise<void>;
|
handleOnDragEnd: (result: DropResult) => Promise<void>;
|
||||||
openIssuesListModal: (() => void) | null;
|
openIssuesListModal: (() => void) | null;
|
||||||
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
|
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
|
||||||
@ -66,6 +68,7 @@ export const AllViews: React.FC<Props> = ({
|
|||||||
dragDisabled = false,
|
dragDisabled = false,
|
||||||
emptyState,
|
emptyState,
|
||||||
handleIssueAction,
|
handleIssueAction,
|
||||||
|
handleDraftIssueAction,
|
||||||
handleOnDragEnd,
|
handleOnDragEnd,
|
||||||
openIssuesListModal,
|
openIssuesListModal,
|
||||||
removeIssue,
|
removeIssue,
|
||||||
@ -77,10 +80,14 @@ export const AllViews: React.FC<Props> = ({
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
||||||
|
|
||||||
|
const [myIssueProjectId, setMyIssueProjectId] = useState<string | null>(null);
|
||||||
|
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
const { memberRole } = useProjectMyMembership();
|
const { memberRole } = useProjectMyMembership();
|
||||||
|
|
||||||
const { groupedIssues, isEmpty, issueView } = viewProps;
|
const { groupedIssues, isEmpty, displayFilters } = viewProps;
|
||||||
|
|
||||||
|
const { spreadsheetIssues, mutateIssues } = useSpreadsheetIssuesView();
|
||||||
|
|
||||||
const { data: stateGroups } = useSWR(
|
const { data: stateGroups } = useSWR(
|
||||||
workspaceSlug && projectId ? STATES_LIST(projectId as string) : null,
|
workspaceSlug && projectId ? STATES_LIST(projectId as string) : null,
|
||||||
@ -90,6 +97,10 @@ export const AllViews: React.FC<Props> = ({
|
|||||||
);
|
);
|
||||||
const states = getStatesList(stateGroups);
|
const states = getStatesList(stateGroups);
|
||||||
|
|
||||||
|
const handleMyIssueOpen = (issue: IIssue) => {
|
||||||
|
setMyIssueProjectId(issue.project);
|
||||||
|
};
|
||||||
|
|
||||||
const handleTrashBox = useCallback(
|
const handleTrashBox = useCallback(
|
||||||
(isDragging: boolean) => {
|
(isDragging: boolean) => {
|
||||||
if (isDragging && !trashBox) setTrashBox(true);
|
if (isDragging && !trashBox) setTrashBox(true);
|
||||||
@ -117,39 +128,45 @@ export const AllViews: React.FC<Props> = ({
|
|||||||
</StrictModeDroppable>
|
</StrictModeDroppable>
|
||||||
{groupedIssues ? (
|
{groupedIssues ? (
|
||||||
!isEmpty ||
|
!isEmpty ||
|
||||||
issueView === "kanban" ||
|
displayFilters?.layout === "kanban" ||
|
||||||
issueView === "calendar" ||
|
displayFilters?.layout === "calendar" ||
|
||||||
issueView === "gantt_chart" ? (
|
displayFilters?.layout === "gantt_chart" ? (
|
||||||
<>
|
<>
|
||||||
{issueView === "list" ? (
|
{displayFilters?.layout === "list" ? (
|
||||||
<AllLists
|
<AllLists
|
||||||
states={states}
|
states={states}
|
||||||
addIssueToGroup={addIssueToGroup}
|
addIssueToGroup={addIssueToGroup}
|
||||||
handleIssueAction={handleIssueAction}
|
handleIssueAction={handleIssueAction}
|
||||||
|
handleDraftIssueAction={handleDraftIssueAction}
|
||||||
openIssuesListModal={cycleId || moduleId ? openIssuesListModal : null}
|
openIssuesListModal={cycleId || moduleId ? openIssuesListModal : null}
|
||||||
removeIssue={removeIssue}
|
removeIssue={removeIssue}
|
||||||
|
myIssueProjectId={myIssueProjectId}
|
||||||
|
handleMyIssueOpen={handleMyIssueOpen}
|
||||||
disableUserActions={disableUserActions}
|
disableUserActions={disableUserActions}
|
||||||
disableAddIssueOption={disableAddIssueOption}
|
disableAddIssueOption={disableAddIssueOption}
|
||||||
user={user}
|
user={user}
|
||||||
userAuth={memberRole}
|
userAuth={memberRole}
|
||||||
viewProps={viewProps}
|
viewProps={viewProps}
|
||||||
/>
|
/>
|
||||||
) : issueView === "kanban" ? (
|
) : displayFilters?.layout === "kanban" ? (
|
||||||
<AllBoards
|
<AllBoards
|
||||||
addIssueToGroup={addIssueToGroup}
|
addIssueToGroup={addIssueToGroup}
|
||||||
disableUserActions={disableUserActions}
|
disableUserActions={disableUserActions}
|
||||||
disableAddIssueOption={disableAddIssueOption}
|
disableAddIssueOption={disableAddIssueOption}
|
||||||
dragDisabled={dragDisabled}
|
dragDisabled={dragDisabled}
|
||||||
handleIssueAction={handleIssueAction}
|
handleIssueAction={handleIssueAction}
|
||||||
|
handleDraftIssueAction={handleDraftIssueAction}
|
||||||
handleTrashBox={handleTrashBox}
|
handleTrashBox={handleTrashBox}
|
||||||
openIssuesListModal={cycleId || moduleId ? openIssuesListModal : null}
|
openIssuesListModal={cycleId || moduleId ? openIssuesListModal : null}
|
||||||
|
myIssueProjectId={myIssueProjectId}
|
||||||
|
handleMyIssueOpen={handleMyIssueOpen}
|
||||||
removeIssue={removeIssue}
|
removeIssue={removeIssue}
|
||||||
states={states}
|
states={states}
|
||||||
user={user}
|
user={user}
|
||||||
userAuth={memberRole}
|
userAuth={memberRole}
|
||||||
viewProps={viewProps}
|
viewProps={viewProps}
|
||||||
/>
|
/>
|
||||||
) : issueView === "calendar" ? (
|
) : displayFilters?.layout === "calendar" ? (
|
||||||
<CalendarView
|
<CalendarView
|
||||||
handleIssueAction={handleIssueAction}
|
handleIssueAction={handleIssueAction}
|
||||||
addIssueToDate={addIssueToDate}
|
addIssueToDate={addIssueToDate}
|
||||||
@ -157,16 +174,20 @@ export const AllViews: React.FC<Props> = ({
|
|||||||
user={user}
|
user={user}
|
||||||
userAuth={memberRole}
|
userAuth={memberRole}
|
||||||
/>
|
/>
|
||||||
) : issueView === "spreadsheet" ? (
|
) : displayFilters?.layout === "spreadsheet" ? (
|
||||||
<SpreadsheetView
|
<SpreadsheetView
|
||||||
handleIssueAction={handleIssueAction}
|
handleIssueAction={handleIssueAction}
|
||||||
|
spreadsheetIssues={spreadsheetIssues}
|
||||||
|
mutateIssues={mutateIssues}
|
||||||
openIssuesListModal={cycleId || moduleId ? openIssuesListModal : null}
|
openIssuesListModal={cycleId || moduleId ? openIssuesListModal : null}
|
||||||
disableUserActions={disableUserActions}
|
disableUserActions={disableUserActions}
|
||||||
user={user}
|
user={user}
|
||||||
userAuth={memberRole}
|
userAuth={memberRole}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
issueView === "gantt_chart" && <GanttChartView />
|
displayFilters?.layout === "gantt_chart" && (
|
||||||
|
<GanttChartView disableUserActions={disableUserActions} />
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : router.pathname.includes("archived-issues") ? (
|
) : router.pathname.includes("archived-issues") ? (
|
||||||
|
@ -1,5 +1,12 @@
|
|||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
//hook
|
||||||
|
import useMyIssues from "hooks/my-issues/use-my-issues";
|
||||||
|
import useIssuesView from "hooks/use-issues-view";
|
||||||
|
import useProfileIssues from "hooks/use-profile-issues";
|
||||||
// components
|
// components
|
||||||
import { SingleBoard } from "components/core/views/board-view/single-board";
|
import { SingleBoard } from "components/core/views/board-view/single-board";
|
||||||
|
import { IssuePeekOverview } from "components/issues";
|
||||||
// icons
|
// icons
|
||||||
import { StateGroupIcon } from "components/icons";
|
import { StateGroupIcon } from "components/icons";
|
||||||
// helpers
|
// helpers
|
||||||
@ -13,9 +20,12 @@ type Props = {
|
|||||||
disableAddIssueOption?: boolean;
|
disableAddIssueOption?: boolean;
|
||||||
dragDisabled: boolean;
|
dragDisabled: boolean;
|
||||||
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
|
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
|
||||||
|
handleDraftIssueAction?: (issue: IIssue, action: "edit" | "delete") => void;
|
||||||
handleTrashBox: (isDragging: boolean) => void;
|
handleTrashBox: (isDragging: boolean) => void;
|
||||||
openIssuesListModal?: (() => void) | null;
|
openIssuesListModal?: (() => void) | null;
|
||||||
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
|
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
|
||||||
|
myIssueProjectId?: string | null;
|
||||||
|
handleMyIssueOpen?: (issue: IIssue) => void;
|
||||||
states: IState[] | undefined;
|
states: IState[] | undefined;
|
||||||
user: ICurrentUserResponse | undefined;
|
user: ICurrentUserResponse | undefined;
|
||||||
userAuth: UserAuth;
|
userAuth: UserAuth;
|
||||||
@ -28,25 +38,53 @@ export const AllBoards: React.FC<Props> = ({
|
|||||||
disableAddIssueOption = false,
|
disableAddIssueOption = false,
|
||||||
dragDisabled,
|
dragDisabled,
|
||||||
handleIssueAction,
|
handleIssueAction,
|
||||||
|
handleDraftIssueAction,
|
||||||
handleTrashBox,
|
handleTrashBox,
|
||||||
openIssuesListModal,
|
openIssuesListModal,
|
||||||
|
myIssueProjectId,
|
||||||
|
handleMyIssueOpen,
|
||||||
removeIssue,
|
removeIssue,
|
||||||
states,
|
states,
|
||||||
user,
|
user,
|
||||||
userAuth,
|
userAuth,
|
||||||
viewProps,
|
viewProps,
|
||||||
}) => {
|
}) => {
|
||||||
const { groupByProperty: selectedGroup, groupedIssues, showEmptyGroups } = viewProps;
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, projectId, userId } = router.query;
|
||||||
|
|
||||||
|
const isProfileIssue =
|
||||||
|
router.pathname.includes("assigned") ||
|
||||||
|
router.pathname.includes("created") ||
|
||||||
|
router.pathname.includes("subscribed");
|
||||||
|
|
||||||
|
const isMyIssue = router.pathname.includes("my-issues");
|
||||||
|
|
||||||
|
const { mutateIssues } = useIssuesView();
|
||||||
|
const { mutateMyIssues } = useMyIssues(workspaceSlug?.toString());
|
||||||
|
const { mutateProfileIssues } = useProfileIssues(workspaceSlug?.toString(), userId?.toString());
|
||||||
|
|
||||||
|
const { displayFilters, groupedIssues } = viewProps;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<IssuePeekOverview
|
||||||
|
handleMutation={() =>
|
||||||
|
isMyIssue ? mutateMyIssues() : isProfileIssue ? mutateProfileIssues() : mutateIssues()
|
||||||
|
}
|
||||||
|
projectId={myIssueProjectId ? myIssueProjectId : projectId?.toString() ?? ""}
|
||||||
|
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
||||||
|
readOnly={disableUserActions}
|
||||||
|
/>
|
||||||
{groupedIssues ? (
|
{groupedIssues ? (
|
||||||
<div className="horizontal-scroll-enable flex h-full gap-x-4 p-8">
|
<div className="horizontal-scroll-enable flex h-full gap-x-4 p-8 bg-custom-background-90">
|
||||||
{Object.keys(groupedIssues).map((singleGroup, index) => {
|
{Object.keys(groupedIssues).map((singleGroup, index) => {
|
||||||
const currentState =
|
const currentState =
|
||||||
selectedGroup === "state" ? states?.find((s) => s.id === singleGroup) : null;
|
displayFilters?.group_by === "state"
|
||||||
|
? states?.find((s) => s.id === singleGroup)
|
||||||
|
: null;
|
||||||
|
|
||||||
if (!showEmptyGroups && groupedIssues[singleGroup].length === 0) return null;
|
if (!displayFilters?.show_empty_groups && groupedIssues[singleGroup].length === 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SingleBoard
|
<SingleBoard
|
||||||
@ -58,8 +96,10 @@ export const AllBoards: React.FC<Props> = ({
|
|||||||
dragDisabled={dragDisabled}
|
dragDisabled={dragDisabled}
|
||||||
groupTitle={singleGroup}
|
groupTitle={singleGroup}
|
||||||
handleIssueAction={handleIssueAction}
|
handleIssueAction={handleIssueAction}
|
||||||
|
handleDraftIssueAction={handleDraftIssueAction}
|
||||||
handleTrashBox={handleTrashBox}
|
handleTrashBox={handleTrashBox}
|
||||||
openIssuesListModal={openIssuesListModal ?? null}
|
openIssuesListModal={openIssuesListModal ?? null}
|
||||||
|
handleMyIssueOpen={handleMyIssueOpen}
|
||||||
removeIssue={removeIssue}
|
removeIssue={removeIssue}
|
||||||
user={user}
|
user={user}
|
||||||
userAuth={userAuth}
|
userAuth={userAuth}
|
||||||
@ -67,13 +107,15 @@ export const AllBoards: React.FC<Props> = ({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{!showEmptyGroups && (
|
{!displayFilters?.show_empty_groups && (
|
||||||
<div className="h-full w-96 flex-shrink-0 space-y-2 p-1">
|
<div className="h-full w-96 flex-shrink-0 space-y-2 p-1">
|
||||||
<h2 className="text-lg font-semibold">Hidden groups</h2>
|
<h2 className="text-lg font-semibold">Hidden groups</h2>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{Object.keys(groupedIssues).map((singleGroup, index) => {
|
{Object.keys(groupedIssues).map((singleGroup, index) => {
|
||||||
const currentState =
|
const currentState =
|
||||||
selectedGroup === "state" ? states?.find((s) => s.id === singleGroup) : null;
|
displayFilters?.group_by === "state"
|
||||||
|
? states?.find((s) => s.id === singleGroup)
|
||||||
|
: null;
|
||||||
|
|
||||||
if (groupedIssues[singleGroup].length === 0)
|
if (groupedIssues[singleGroup].length === 0)
|
||||||
return (
|
return (
|
||||||
@ -91,7 +133,7 @@ export const AllBoards: React.FC<Props> = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<h4 className="text-sm capitalize">
|
<h4 className="text-sm capitalize">
|
||||||
{selectedGroup === "state"
|
{displayFilters?.group_by === "state"
|
||||||
? addSpaceIfCamelCase(currentState?.name ?? "")
|
? addSpaceIfCamelCase(currentState?.name ?? "")
|
||||||
: addSpaceIfCamelCase(singleGroup)}
|
: addSpaceIfCamelCase(singleGroup)}
|
||||||
</h4>
|
</h4>
|
||||||
|
@ -20,7 +20,7 @@ import { renderEmoji } from "helpers/emoji.helper";
|
|||||||
// types
|
// types
|
||||||
import { IIssueViewProps, IState, TIssuePriorities, TStateGroups } from "types";
|
import { IIssueViewProps, IState, TIssuePriorities, TStateGroups } from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS } from "constants/fetch-keys";
|
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS, WORKSPACE_LABELS } from "constants/fetch-keys";
|
||||||
// constants
|
// constants
|
||||||
import { STATE_GROUP_COLORS } from "constants/state";
|
import { STATE_GROUP_COLORS } from "constants/state";
|
||||||
|
|
||||||
@ -48,22 +48,35 @@ export const BoardHeader: React.FC<Props> = ({
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
const { groupedIssues, groupByProperty: selectedGroup } = viewProps;
|
const { displayFilters, groupedIssues } = viewProps;
|
||||||
|
|
||||||
const { data: issueLabels } = useSWR(
|
const { data: issueLabels } = useSWR(
|
||||||
workspaceSlug && projectId && selectedGroup === "labels"
|
workspaceSlug && projectId && displayFilters?.group_by === "labels"
|
||||||
? PROJECT_ISSUE_LABELS(projectId.toString())
|
? PROJECT_ISSUE_LABELS(projectId.toString())
|
||||||
: null,
|
: null,
|
||||||
workspaceSlug && projectId && selectedGroup === "labels"
|
workspaceSlug && projectId && displayFilters?.group_by === "labels"
|
||||||
? () => issuesService.getIssueLabels(workspaceSlug.toString(), projectId.toString())
|
? () => issuesService.getIssueLabels(workspaceSlug.toString(), projectId.toString())
|
||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { data: workspaceLabels } = useSWR(
|
||||||
|
workspaceSlug && displayFilters?.group_by === "labels"
|
||||||
|
? WORKSPACE_LABELS(workspaceSlug.toString())
|
||||||
|
: null,
|
||||||
|
workspaceSlug && displayFilters?.group_by === "labels"
|
||||||
|
? () => issuesService.getWorkspaceLabels(workspaceSlug.toString())
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
|
||||||
const { data: members } = useSWR(
|
const { data: members } = useSWR(
|
||||||
workspaceSlug && projectId && (selectedGroup === "created_by" || selectedGroup === "assignees")
|
workspaceSlug &&
|
||||||
|
projectId &&
|
||||||
|
(displayFilters?.group_by === "created_by" || displayFilters?.group_by === "assignees")
|
||||||
? PROJECT_MEMBERS(projectId.toString())
|
? PROJECT_MEMBERS(projectId.toString())
|
||||||
: null,
|
: null,
|
||||||
workspaceSlug && projectId && (selectedGroup === "created_by" || selectedGroup === "assignees")
|
workspaceSlug &&
|
||||||
|
projectId &&
|
||||||
|
(displayFilters?.group_by === "created_by" || displayFilters?.group_by === "assignees")
|
||||||
? () => projectService.projectMembers(workspaceSlug.toString(), projectId.toString())
|
? () => projectService.projectMembers(workspaceSlug.toString(), projectId.toString())
|
||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
@ -73,12 +86,15 @@ export const BoardHeader: React.FC<Props> = ({
|
|||||||
const getGroupTitle = () => {
|
const getGroupTitle = () => {
|
||||||
let title = addSpaceIfCamelCase(groupTitle);
|
let title = addSpaceIfCamelCase(groupTitle);
|
||||||
|
|
||||||
switch (selectedGroup) {
|
switch (displayFilters?.group_by) {
|
||||||
case "state":
|
case "state":
|
||||||
title = addSpaceIfCamelCase(currentState?.name ?? "");
|
title = addSpaceIfCamelCase(currentState?.name ?? "");
|
||||||
break;
|
break;
|
||||||
case "labels":
|
case "labels":
|
||||||
title = issueLabels?.find((label) => label.id === groupTitle)?.name ?? "None";
|
title =
|
||||||
|
[...(issueLabels ?? []), ...(workspaceLabels ?? [])]?.find(
|
||||||
|
(label) => label.id === groupTitle
|
||||||
|
)?.name ?? "None";
|
||||||
break;
|
break;
|
||||||
case "project":
|
case "project":
|
||||||
title = projects?.find((p) => p.id === groupTitle)?.name ?? "None";
|
title = projects?.find((p) => p.id === groupTitle)?.name ?? "None";
|
||||||
@ -97,7 +113,7 @@ export const BoardHeader: React.FC<Props> = ({
|
|||||||
const getGroupIcon = () => {
|
const getGroupIcon = () => {
|
||||||
let icon;
|
let icon;
|
||||||
|
|
||||||
switch (selectedGroup) {
|
switch (displayFilters?.group_by) {
|
||||||
case "state":
|
case "state":
|
||||||
icon = currentState && (
|
icon = currentState && (
|
||||||
<StateGroupIcon
|
<StateGroupIcon
|
||||||
@ -133,7 +149,9 @@ export const BoardHeader: React.FC<Props> = ({
|
|||||||
break;
|
break;
|
||||||
case "labels":
|
case "labels":
|
||||||
const labelColor =
|
const labelColor =
|
||||||
issueLabels?.find((label) => label.id === groupTitle)?.color ?? "#000000";
|
[...(issueLabels ?? []), ...(workspaceLabels ?? [])]?.find(
|
||||||
|
(label) => label.id === groupTitle
|
||||||
|
)?.color ?? "#000000";
|
||||||
icon = (
|
icon = (
|
||||||
<span
|
<span
|
||||||
className="h-3.5 w-3.5 flex-shrink-0 rounded-full"
|
className="h-3.5 w-3.5 flex-shrink-0 rounded-full"
|
||||||
@ -167,7 +185,7 @@ export const BoardHeader: React.FC<Props> = ({
|
|||||||
<span className="flex items-center">{getGroupIcon()}</span>
|
<span className="flex items-center">{getGroupIcon()}</span>
|
||||||
<h2
|
<h2
|
||||||
className={`text-lg font-semibold truncate ${
|
className={`text-lg font-semibold truncate ${
|
||||||
selectedGroup === "created_by" ? "" : "capitalize"
|
displayFilters?.group_by === "created_by" ? "" : "capitalize"
|
||||||
}`}
|
}`}
|
||||||
style={{
|
style={{
|
||||||
writingMode: isCollapsed ? "horizontal-tb" : "vertical-rl",
|
writingMode: isCollapsed ? "horizontal-tb" : "vertical-rl",
|
||||||
@ -198,7 +216,7 @@ export const BoardHeader: React.FC<Props> = ({
|
|||||||
<Icon iconName="open_in_full" className="text-base font-medium text-custom-text-900" />
|
<Icon iconName="open_in_full" className="text-base font-medium text-custom-text-900" />
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
{!disableAddIssue && !disableUserActions && selectedGroup !== "created_by" && (
|
{!disableAddIssue && !disableUserActions && displayFilters?.group_by !== "created_by" && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="grid h-7 w-7 place-items-center rounded p-1 text-custom-text-200 outline-none duration-300 hover:bg-custom-background-80"
|
className="grid h-7 w-7 place-items-center rounded p-1 text-custom-text-200 outline-none duration-300 hover:bg-custom-background-80"
|
||||||
|
@ -2,3 +2,4 @@ export * from "./all-boards";
|
|||||||
export * from "./board-header";
|
export * from "./board-header";
|
||||||
export * from "./single-board";
|
export * from "./single-board";
|
||||||
export * from "./single-issue";
|
export * from "./single-issue";
|
||||||
|
export * from "./inline-create-issue-form";
|
||||||
|
@ -0,0 +1,62 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
// react hook form
|
||||||
|
import { useFormContext } from "react-hook-form";
|
||||||
|
|
||||||
|
// components
|
||||||
|
import { InlineCreateIssueFormWrapper } from "components/core";
|
||||||
|
|
||||||
|
// hooks
|
||||||
|
import useProjectDetails from "hooks/use-project-details";
|
||||||
|
|
||||||
|
// types
|
||||||
|
import { IIssue } from "types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isOpen: boolean;
|
||||||
|
handleClose: () => void;
|
||||||
|
onSuccess?: (data: IIssue) => Promise<void> | void;
|
||||||
|
prePopulatedData?: Partial<IIssue>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const InlineInput = () => {
|
||||||
|
const { projectDetails } = useProjectDetails();
|
||||||
|
|
||||||
|
const { register, setFocus } = useFormContext();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setFocus("name");
|
||||||
|
}, [setFocus]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-medium leading-5 text-custom-text-300">
|
||||||
|
{projectDetails?.identifier ?? "..."}
|
||||||
|
</h4>
|
||||||
|
<input
|
||||||
|
autoComplete="off"
|
||||||
|
placeholder="Issue Title"
|
||||||
|
{...register("name", {
|
||||||
|
required: "Issue title is required.",
|
||||||
|
})}
|
||||||
|
className="w-full px-2 pl-0 py-1.5 rounded-md bg-transparent text-sm font-medium leading-5 text-custom-text-200 outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BoardInlineCreateIssueForm: React.FC<Props> = (props) => (
|
||||||
|
<>
|
||||||
|
<InlineCreateIssueFormWrapper
|
||||||
|
className="flex flex-col border-[0.5px] border-custom-border-100 justify-between gap-1.5 group/card relative select-none px-3.5 py-3 h-[118px] mb-3 rounded bg-custom-background-100 shadow-custom-shadow-sm"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<InlineInput />
|
||||||
|
</InlineCreateIssueFormWrapper>
|
||||||
|
{props.isOpen && (
|
||||||
|
<p className="text-xs ml-3 italic text-custom-text-200">
|
||||||
|
Press {"'"}Enter{"'"} to add another issue
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
@ -6,7 +6,7 @@ import { useRouter } from "next/router";
|
|||||||
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
|
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
|
||||||
import { Draggable } from "react-beautiful-dnd";
|
import { Draggable } from "react-beautiful-dnd";
|
||||||
// components
|
// components
|
||||||
import { BoardHeader, SingleBoardIssue } from "components/core";
|
import { BoardHeader, SingleBoardIssue, BoardInlineCreateIssueForm } from "components/core";
|
||||||
// ui
|
// ui
|
||||||
import { CustomMenu } from "components/ui";
|
import { CustomMenu } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
@ -24,37 +24,49 @@ type Props = {
|
|||||||
dragDisabled: boolean;
|
dragDisabled: boolean;
|
||||||
groupTitle: string;
|
groupTitle: string;
|
||||||
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
|
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
|
||||||
|
handleDraftIssueAction?: (issue: IIssue, action: "edit" | "delete") => void;
|
||||||
handleTrashBox: (isDragging: boolean) => void;
|
handleTrashBox: (isDragging: boolean) => void;
|
||||||
openIssuesListModal?: (() => void) | null;
|
openIssuesListModal?: (() => void) | null;
|
||||||
|
handleMyIssueOpen?: (issue: IIssue) => void;
|
||||||
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
|
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
|
||||||
user: ICurrentUserResponse | undefined;
|
user: ICurrentUserResponse | undefined;
|
||||||
userAuth: UserAuth;
|
userAuth: UserAuth;
|
||||||
viewProps: IIssueViewProps;
|
viewProps: IIssueViewProps;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SingleBoard: React.FC<Props> = ({
|
export const SingleBoard: React.FC<Props> = (props) => {
|
||||||
addIssueToGroup,
|
const {
|
||||||
currentState,
|
addIssueToGroup,
|
||||||
groupTitle,
|
currentState,
|
||||||
disableUserActions,
|
groupTitle,
|
||||||
disableAddIssueOption = false,
|
disableUserActions,
|
||||||
dragDisabled,
|
disableAddIssueOption = false,
|
||||||
handleIssueAction,
|
dragDisabled,
|
||||||
handleTrashBox,
|
handleIssueAction,
|
||||||
openIssuesListModal,
|
handleDraftIssueAction,
|
||||||
removeIssue,
|
handleTrashBox,
|
||||||
user,
|
openIssuesListModal,
|
||||||
userAuth,
|
handleMyIssueOpen,
|
||||||
viewProps,
|
removeIssue,
|
||||||
}) => {
|
user,
|
||||||
|
userAuth,
|
||||||
|
viewProps,
|
||||||
|
} = props;
|
||||||
|
|
||||||
// collapse/expand
|
// collapse/expand
|
||||||
const [isCollapsed, setIsCollapsed] = useState(true);
|
const [isCollapsed, setIsCollapsed] = useState(true);
|
||||||
|
|
||||||
const { groupedIssues, groupByProperty: selectedGroup, orderBy, properties } = viewProps;
|
const [isInlineCreateIssueFormOpen, setIsInlineCreateIssueFormOpen] = useState(false);
|
||||||
|
|
||||||
|
const { displayFilters, groupedIssues } = viewProps;
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { cycleId, moduleId } = router.query;
|
const { cycleId, moduleId } = router.query;
|
||||||
|
|
||||||
|
const isMyIssuesPage = router.pathname.split("/")[3] === "my-issues";
|
||||||
|
const isProfileIssuesPage = router.pathname.split("/")[2] === "profile";
|
||||||
|
const isDraftIssuesPage = router.pathname.split("/")[4] === "draft-issues";
|
||||||
|
|
||||||
const type = cycleId ? "cycle" : moduleId ? "module" : "issue";
|
const type = cycleId ? "cycle" : moduleId ? "module" : "issue";
|
||||||
|
|
||||||
// Check if it has at least 4 tickets since it is enough to accommodate the Calendar height
|
// Check if it has at least 4 tickets since it is enough to accommodate the Calendar height
|
||||||
@ -63,6 +75,27 @@ export const SingleBoard: React.FC<Props> = ({
|
|||||||
|
|
||||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disableUserActions;
|
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disableUserActions;
|
||||||
|
|
||||||
|
const scrollToBottom = () => {
|
||||||
|
const boardListElement = document.getElementById(`board-list-${groupTitle}`);
|
||||||
|
|
||||||
|
// timeout is needed because the animation
|
||||||
|
// takes time to complete & we can scroll only after that
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
if (boardListElement)
|
||||||
|
boardListElement.scrollBy({
|
||||||
|
top: boardListElement.scrollHeight,
|
||||||
|
left: 0,
|
||||||
|
behavior: "smooth",
|
||||||
|
});
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
}, 10);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCreateClick = () => {
|
||||||
|
setIsInlineCreateIssueFormOpen(true);
|
||||||
|
scrollToBottom();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`flex-shrink-0 ${!isCollapsed ? "" : "flex h-full flex-col w-96"}`}>
|
<div className={`flex-shrink-0 ${!isCollapsed ? "" : "flex h-full flex-col w-96"}`}>
|
||||||
<BoardHeader
|
<BoardHeader
|
||||||
@ -80,14 +113,14 @@ export const SingleBoard: React.FC<Props> = ({
|
|||||||
{(provided, snapshot) => (
|
{(provided, snapshot) => (
|
||||||
<div
|
<div
|
||||||
className={`relative h-full ${
|
className={`relative h-full ${
|
||||||
orderBy !== "sort_order" && snapshot.isDraggingOver
|
displayFilters?.order_by !== "sort_order" && snapshot.isDraggingOver
|
||||||
? "bg-custom-background-100/20"
|
? "bg-custom-background-100/20"
|
||||||
: ""
|
: ""
|
||||||
} ${!isCollapsed ? "hidden" : "flex flex-col"}`}
|
} ${!isCollapsed ? "hidden" : "flex flex-col"}`}
|
||||||
ref={provided.innerRef}
|
ref={provided.innerRef}
|
||||||
{...provided.droppableProps}
|
{...provided.droppableProps}
|
||||||
>
|
>
|
||||||
{orderBy !== "sort_order" && (
|
{displayFilters?.order_by !== "sort_order" && (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className={`absolute ${
|
className={`absolute ${
|
||||||
@ -101,12 +134,17 @@ export const SingleBoard: React.FC<Props> = ({
|
|||||||
>
|
>
|
||||||
This board is ordered by{" "}
|
This board is ordered by{" "}
|
||||||
{replaceUnderscoreIfSnakeCase(
|
{replaceUnderscoreIfSnakeCase(
|
||||||
orderBy ? (orderBy[0] === "-" ? orderBy.slice(1) : orderBy) : "created_at"
|
displayFilters?.order_by
|
||||||
|
? displayFilters?.order_by[0] === "-"
|
||||||
|
? displayFilters?.order_by.slice(1)
|
||||||
|
: displayFilters?.order_by
|
||||||
|
: "created_at"
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
|
id={`board-list-${groupTitle}`}
|
||||||
className={`pt-3 ${
|
className={`pt-3 ${
|
||||||
hasMinimumNumberOfCards ? "overflow-hidden overflow-y-scroll" : ""
|
hasMinimumNumberOfCards ? "overflow-hidden overflow-y-scroll" : ""
|
||||||
} `}
|
} `}
|
||||||
@ -126,11 +164,23 @@ export const SingleBoard: React.FC<Props> = ({
|
|||||||
type={type}
|
type={type}
|
||||||
index={index}
|
index={index}
|
||||||
issue={issue}
|
issue={issue}
|
||||||
|
projectId={issue.project_detail.id}
|
||||||
groupTitle={groupTitle}
|
groupTitle={groupTitle}
|
||||||
editIssue={() => handleIssueAction(issue, "edit")}
|
editIssue={() => handleIssueAction(issue, "edit")}
|
||||||
makeIssueCopy={() => handleIssueAction(issue, "copy")}
|
makeIssueCopy={() => handleIssueAction(issue, "copy")}
|
||||||
handleDeleteIssue={() => handleIssueAction(issue, "delete")}
|
handleDeleteIssue={() => handleIssueAction(issue, "delete")}
|
||||||
|
handleDraftIssueEdit={
|
||||||
|
handleDraftIssueAction
|
||||||
|
? () => handleDraftIssueAction(issue, "edit")
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
handleDraftIssueDelete={() =>
|
||||||
|
handleDraftIssueAction
|
||||||
|
? handleDraftIssueAction(issue, "delete")
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
handleTrashBox={handleTrashBox}
|
handleTrashBox={handleTrashBox}
|
||||||
|
handleMyIssueOpen={handleMyIssueOpen}
|
||||||
removeIssue={() => {
|
removeIssue={() => {
|
||||||
if (removeIssue && issue.bridge_id)
|
if (removeIssue && issue.bridge_id)
|
||||||
removeIssue(issue.bridge_id, issue.id);
|
removeIssue(issue.bridge_id, issue.id);
|
||||||
@ -145,20 +195,38 @@ export const SingleBoard: React.FC<Props> = ({
|
|||||||
))}
|
))}
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
display: orderBy === "sort_order" ? "inline" : "none",
|
display: displayFilters?.order_by === "sort_order" ? "inline" : "none",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{provided.placeholder}
|
<>{provided.placeholder}</>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
<BoardInlineCreateIssueForm
|
||||||
|
isOpen={isInlineCreateIssueFormOpen}
|
||||||
|
handleClose={() => setIsInlineCreateIssueFormOpen(false)}
|
||||||
|
onSuccess={() => scrollToBottom()}
|
||||||
|
prePopulatedData={{
|
||||||
|
...(cycleId && { cycle: cycleId.toString() }),
|
||||||
|
...(moduleId && { module: moduleId.toString() }),
|
||||||
|
[displayFilters?.group_by! === "labels"
|
||||||
|
? "labels_list"
|
||||||
|
: displayFilters?.group_by!]:
|
||||||
|
displayFilters?.group_by === "labels" ? [groupTitle] : groupTitle,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{selectedGroup !== "created_by" && (
|
{displayFilters?.group_by !== "created_by" && (
|
||||||
<div>
|
<div>
|
||||||
{type === "issue"
|
{type === "issue"
|
||||||
? !disableAddIssueOption && (
|
? !disableAddIssueOption && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="flex items-center gap-2 font-medium text-custom-primary outline-none p-1"
|
className="flex items-center gap-2 font-medium text-custom-primary outline-none p-1"
|
||||||
onClick={addIssueToGroup}
|
onClick={() => {
|
||||||
|
if (isDraftIssuesPage || isMyIssuesPage || isProfileIssuesPage) {
|
||||||
|
addIssueToGroup();
|
||||||
|
} else onCreateClick();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<PlusIcon className="h-4 w-4" />
|
<PlusIcon className="h-4 w-4" />
|
||||||
Add Issue
|
Add Issue
|
||||||
@ -178,7 +246,7 @@ export const SingleBoard: React.FC<Props> = ({
|
|||||||
position="left"
|
position="left"
|
||||||
noBorder
|
noBorder
|
||||||
>
|
>
|
||||||
<CustomMenu.MenuItem onClick={addIssueToGroup}>
|
<CustomMenu.MenuItem onClick={() => onCreateClick()}>
|
||||||
Create new
|
Create new
|
||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
{openIssuesListModal && (
|
{openIssuesListModal && (
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
import { mutate } from "swr";
|
import { mutate } from "swr";
|
||||||
@ -14,19 +13,14 @@ import {
|
|||||||
} from "react-beautiful-dnd";
|
} from "react-beautiful-dnd";
|
||||||
// services
|
// services
|
||||||
import issuesService from "services/issues.service";
|
import issuesService from "services/issues.service";
|
||||||
|
import trackEventServices from "services/track-event.service";
|
||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||||
// components
|
// components
|
||||||
import {
|
import { ViewDueDateSelect, ViewEstimateSelect, ViewStartDateSelect } from "components/issues";
|
||||||
ViewAssigneeSelect,
|
import { MembersSelect, LabelSelect, PrioritySelect } from "components/project";
|
||||||
ViewDueDateSelect,
|
import { StateSelect } from "components/states";
|
||||||
ViewEstimateSelect,
|
|
||||||
ViewIssueLabel,
|
|
||||||
ViewPrioritySelect,
|
|
||||||
ViewStartDateSelect,
|
|
||||||
ViewStateSelect,
|
|
||||||
} from "components/issues";
|
|
||||||
// ui
|
// ui
|
||||||
import { ContextMenu, CustomMenu, Tooltip } from "components/ui";
|
import { ContextMenu, CustomMenu, Tooltip } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
@ -45,7 +39,15 @@ import { LayerDiagonalIcon } from "components/icons";
|
|||||||
import { handleIssuesMutation } from "constants/issue";
|
import { handleIssuesMutation } from "constants/issue";
|
||||||
import { copyTextToClipboard } from "helpers/string.helper";
|
import { copyTextToClipboard } from "helpers/string.helper";
|
||||||
// types
|
// types
|
||||||
import { ICurrentUserResponse, IIssue, IIssueViewProps, ISubIssueResponse, UserAuth } from "types";
|
import {
|
||||||
|
ICurrentUserResponse,
|
||||||
|
IIssue,
|
||||||
|
IIssueViewProps,
|
||||||
|
IState,
|
||||||
|
ISubIssueResponse,
|
||||||
|
TIssuePriorities,
|
||||||
|
UserAuth,
|
||||||
|
} from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { CYCLE_DETAILS, MODULE_DETAILS, SUB_ISSUES } from "constants/fetch-keys";
|
import { CYCLE_DETAILS, MODULE_DETAILS, SUB_ISSUES } from "constants/fetch-keys";
|
||||||
|
|
||||||
@ -54,12 +56,16 @@ type Props = {
|
|||||||
provided: DraggableProvided;
|
provided: DraggableProvided;
|
||||||
snapshot: DraggableStateSnapshot;
|
snapshot: DraggableStateSnapshot;
|
||||||
issue: IIssue;
|
issue: IIssue;
|
||||||
|
projectId: string;
|
||||||
groupTitle?: string;
|
groupTitle?: string;
|
||||||
index: number;
|
index: number;
|
||||||
editIssue: () => void;
|
editIssue: () => void;
|
||||||
makeIssueCopy: () => void;
|
makeIssueCopy: () => void;
|
||||||
|
handleMyIssueOpen?: (issue: IIssue) => void;
|
||||||
removeIssue?: (() => void) | null;
|
removeIssue?: (() => void) | null;
|
||||||
handleDeleteIssue: (issue: IIssue) => void;
|
handleDeleteIssue: (issue: IIssue) => void;
|
||||||
|
handleDraftIssueEdit?: () => void;
|
||||||
|
handleDraftIssueDelete?: () => void;
|
||||||
handleTrashBox: (isDragging: boolean) => void;
|
handleTrashBox: (isDragging: boolean) => void;
|
||||||
disableUserActions: boolean;
|
disableUserActions: boolean;
|
||||||
user: ICurrentUserResponse | undefined;
|
user: ICurrentUserResponse | undefined;
|
||||||
@ -72,12 +78,16 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
provided,
|
provided,
|
||||||
snapshot,
|
snapshot,
|
||||||
issue,
|
issue,
|
||||||
|
projectId,
|
||||||
index,
|
index,
|
||||||
editIssue,
|
editIssue,
|
||||||
makeIssueCopy,
|
makeIssueCopy,
|
||||||
|
handleMyIssueOpen,
|
||||||
removeIssue,
|
removeIssue,
|
||||||
groupTitle,
|
groupTitle,
|
||||||
handleDeleteIssue,
|
handleDeleteIssue,
|
||||||
|
handleDraftIssueEdit,
|
||||||
|
handleDraftIssueDelete,
|
||||||
handleTrashBox,
|
handleTrashBox,
|
||||||
disableUserActions,
|
disableUserActions,
|
||||||
user,
|
user,
|
||||||
@ -93,10 +103,12 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
|
|
||||||
const actionSectionRef = useRef<HTMLDivElement | null>(null);
|
const actionSectionRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
const { groupByProperty: selectedGroup, orderBy, properties, mutateIssues } = viewProps;
|
const { displayFilters, properties, mutateIssues } = viewProps;
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
const { workspaceSlug, cycleId, moduleId } = router.query;
|
||||||
|
|
||||||
|
const isDraftIssue = router.pathname.includes("draft-issues");
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
@ -131,9 +143,9 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
handleIssuesMutation(
|
handleIssuesMutation(
|
||||||
formData,
|
formData,
|
||||||
groupTitle ?? "",
|
groupTitle ?? "",
|
||||||
selectedGroup,
|
displayFilters?.group_by ?? null,
|
||||||
index,
|
index,
|
||||||
orderBy,
|
displayFilters?.order_by ?? "-created_at",
|
||||||
prevData
|
prevData
|
||||||
),
|
),
|
||||||
false
|
false
|
||||||
@ -149,24 +161,14 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
if (moduleId) mutate(MODULE_DETAILS(moduleId as string));
|
if (moduleId) mutate(MODULE_DETAILS(moduleId as string));
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[
|
[displayFilters, workspaceSlug, cycleId, moduleId, groupTitle, index, mutateIssues, user]
|
||||||
workspaceSlug,
|
|
||||||
cycleId,
|
|
||||||
moduleId,
|
|
||||||
groupTitle,
|
|
||||||
index,
|
|
||||||
selectedGroup,
|
|
||||||
mutateIssues,
|
|
||||||
orderBy,
|
|
||||||
user,
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const getStyle = (
|
const getStyle = (
|
||||||
style: DraggingStyle | NotDraggingStyle | undefined,
|
style: DraggingStyle | NotDraggingStyle | undefined,
|
||||||
snapshot: DraggableStateSnapshot
|
snapshot: DraggableStateSnapshot
|
||||||
) => {
|
) => {
|
||||||
if (orderBy === "sort_order") return style;
|
if (displayFilters?.order_by === "sort_order") return style;
|
||||||
if (!snapshot.isDragging) return {};
|
if (!snapshot.isDragging) return {};
|
||||||
if (!snapshot.isDropAnimating) return style;
|
if (!snapshot.isDropAnimating) return style;
|
||||||
|
|
||||||
@ -191,12 +193,103 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleStateChange = (data: string, states: IState[] | undefined) => {
|
||||||
|
const oldState = states?.find((s) => s.id === issue.state);
|
||||||
|
const newState = states?.find((s) => s.id === data);
|
||||||
|
|
||||||
|
partialUpdateIssue(
|
||||||
|
{
|
||||||
|
state: data,
|
||||||
|
state_detail: newState,
|
||||||
|
},
|
||||||
|
issue
|
||||||
|
);
|
||||||
|
trackEventServices.trackIssuePartialPropertyUpdateEvent(
|
||||||
|
{
|
||||||
|
workspaceSlug,
|
||||||
|
workspaceId: issue.workspace,
|
||||||
|
projectId: issue.project_detail.id,
|
||||||
|
projectIdentifier: issue.project_detail.identifier,
|
||||||
|
projectName: issue.project_detail.name,
|
||||||
|
issueId: issue.id,
|
||||||
|
},
|
||||||
|
"ISSUE_PROPERTY_UPDATE_STATE",
|
||||||
|
user
|
||||||
|
);
|
||||||
|
if (oldState?.group !== "completed" && newState?.group !== "completed") {
|
||||||
|
trackEventServices.trackIssueMarkedAsDoneEvent(
|
||||||
|
{
|
||||||
|
workspaceSlug: issue.workspace_detail.slug,
|
||||||
|
workspaceId: issue.workspace_detail.id,
|
||||||
|
projectId: issue.project_detail.id,
|
||||||
|
projectIdentifier: issue.project_detail.identifier,
|
||||||
|
projectName: issue.project_detail.name,
|
||||||
|
issueId: issue.id,
|
||||||
|
},
|
||||||
|
user
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAssigneeChange = (data: any) => {
|
||||||
|
const newData = issue.assignees ?? [];
|
||||||
|
|
||||||
|
if (newData.includes(data)) newData.splice(newData.indexOf(data), 1);
|
||||||
|
else newData.push(data);
|
||||||
|
|
||||||
|
partialUpdateIssue({ assignees_list: data }, issue);
|
||||||
|
|
||||||
|
trackEventServices.trackIssuePartialPropertyUpdateEvent(
|
||||||
|
{
|
||||||
|
workspaceSlug,
|
||||||
|
workspaceId: issue.workspace,
|
||||||
|
projectId: issue.project_detail.id,
|
||||||
|
projectIdentifier: issue.project_detail.identifier,
|
||||||
|
projectName: issue.project_detail.name,
|
||||||
|
issueId: issue.id,
|
||||||
|
},
|
||||||
|
"ISSUE_PROPERTY_UPDATE_ASSIGNEE",
|
||||||
|
user
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLabelChange = (data: any) => {
|
||||||
|
partialUpdateIssue({ labels_list: data }, issue);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePriorityChange = (data: TIssuePriorities) => {
|
||||||
|
partialUpdateIssue({ priority: data }, issue);
|
||||||
|
trackEventServices.trackIssuePartialPropertyUpdateEvent(
|
||||||
|
{
|
||||||
|
workspaceSlug,
|
||||||
|
workspaceId: issue.workspace,
|
||||||
|
projectId: issue.project_detail.id,
|
||||||
|
projectIdentifier: issue.project_detail.identifier,
|
||||||
|
projectName: issue.project_detail.name,
|
||||||
|
issueId: issue.id,
|
||||||
|
},
|
||||||
|
"ISSUE_PROPERTY_UPDATE_PRIORITY",
|
||||||
|
user
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (snapshot.isDragging) handleTrashBox(snapshot.isDragging);
|
if (snapshot.isDragging) handleTrashBox(snapshot.isDragging);
|
||||||
}, [snapshot, handleTrashBox]);
|
}, [snapshot, handleTrashBox]);
|
||||||
|
|
||||||
useOutsideClickDetector(actionSectionRef, () => setIsMenuActive(false));
|
useOutsideClickDetector(actionSectionRef, () => setIsMenuActive(false));
|
||||||
|
|
||||||
|
const openPeekOverview = () => {
|
||||||
|
const { query } = router;
|
||||||
|
|
||||||
|
if (handleMyIssueOpen) handleMyIssueOpen(issue);
|
||||||
|
|
||||||
|
router.push({
|
||||||
|
pathname: router.pathname,
|
||||||
|
query: { ...query, peekIssue: issue.id },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disableUserActions;
|
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disableUserActions;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -209,29 +302,47 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
>
|
>
|
||||||
{!isNotAllowed && (
|
{!isNotAllowed && (
|
||||||
<>
|
<>
|
||||||
<ContextMenu.Item Icon={PencilIcon} onClick={editIssue}>
|
<ContextMenu.Item
|
||||||
|
Icon={PencilIcon}
|
||||||
|
onClick={() => {
|
||||||
|
if (isDraftIssue && handleDraftIssueEdit) handleDraftIssueEdit();
|
||||||
|
else editIssue();
|
||||||
|
}}
|
||||||
|
>
|
||||||
Edit issue
|
Edit issue
|
||||||
</ContextMenu.Item>
|
</ContextMenu.Item>
|
||||||
<ContextMenu.Item Icon={ClipboardDocumentCheckIcon} onClick={makeIssueCopy}>
|
{!isDraftIssue && (
|
||||||
Make a copy...
|
<ContextMenu.Item Icon={ClipboardDocumentCheckIcon} onClick={makeIssueCopy}>
|
||||||
</ContextMenu.Item>
|
Make a copy...
|
||||||
<ContextMenu.Item Icon={TrashIcon} onClick={() => handleDeleteIssue(issue)}>
|
</ContextMenu.Item>
|
||||||
|
)}
|
||||||
|
<ContextMenu.Item
|
||||||
|
Icon={TrashIcon}
|
||||||
|
onClick={() => {
|
||||||
|
if (isDraftIssue && handleDraftIssueDelete) handleDraftIssueDelete();
|
||||||
|
else handleDeleteIssue(issue);
|
||||||
|
}}
|
||||||
|
>
|
||||||
Delete issue
|
Delete issue
|
||||||
</ContextMenu.Item>
|
</ContextMenu.Item>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<ContextMenu.Item Icon={LinkIcon} onClick={handleCopyText}>
|
{!isDraftIssue && (
|
||||||
Copy issue link
|
<ContextMenu.Item Icon={LinkIcon} onClick={handleCopyText}>
|
||||||
</ContextMenu.Item>
|
Copy issue link
|
||||||
<a
|
|
||||||
href={`/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer noopener"
|
|
||||||
>
|
|
||||||
<ContextMenu.Item Icon={ArrowTopRightOnSquareIcon}>
|
|
||||||
Open issue in new tab
|
|
||||||
</ContextMenu.Item>
|
</ContextMenu.Item>
|
||||||
</a>
|
)}
|
||||||
|
{!isDraftIssue && (
|
||||||
|
<a
|
||||||
|
href={`/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer noopener"
|
||||||
|
>
|
||||||
|
<ContextMenu.Item Icon={ArrowTopRightOnSquareIcon}>
|
||||||
|
Open issue in new tab
|
||||||
|
</ContextMenu.Item>
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
<div
|
<div
|
||||||
className={`mb-3 rounded bg-custom-background-100 shadow ${
|
className={`mb-3 rounded bg-custom-background-100 shadow ${
|
||||||
@ -266,13 +377,18 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<CustomMenu.MenuItem onClick={editIssue}>
|
<CustomMenu.MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
if (isDraftIssue && handleDraftIssueEdit) handleDraftIssueEdit();
|
||||||
|
else editIssue();
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div className="flex items-center justify-start gap-2">
|
<div className="flex items-center justify-start gap-2">
|
||||||
<PencilIcon className="h-4 w-4" />
|
<PencilIcon className="h-4 w-4" />
|
||||||
<span>Edit issue</span>
|
<span>Edit issue</span>
|
||||||
</div>
|
</div>
|
||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
{type !== "issue" && removeIssue && (
|
{type !== "issue" && removeIssue && !isDraftIssue && (
|
||||||
<CustomMenu.MenuItem onClick={removeIssue}>
|
<CustomMenu.MenuItem onClick={removeIssue}>
|
||||||
<div className="flex items-center justify-start gap-2">
|
<div className="flex items-center justify-start gap-2">
|
||||||
<XMarkIcon className="h-4 w-4" />
|
<XMarkIcon className="h-4 w-4" />
|
||||||
@ -280,53 +396,67 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
</div>
|
</div>
|
||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
)}
|
)}
|
||||||
<CustomMenu.MenuItem onClick={() => handleDeleteIssue(issue)}>
|
<CustomMenu.MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
if (isDraftIssue && handleDraftIssueDelete) handleDraftIssueDelete();
|
||||||
|
else handleDeleteIssue(issue);
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div className="flex items-center justify-start gap-2">
|
<div className="flex items-center justify-start gap-2">
|
||||||
<TrashIcon className="h-4 w-4" />
|
<TrashIcon className="h-4 w-4" />
|
||||||
<span>Delete issue</span>
|
<span>Delete issue</span>
|
||||||
</div>
|
</div>
|
||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
<CustomMenu.MenuItem onClick={handleCopyText}>
|
{!isDraftIssue && (
|
||||||
<div className="flex items-center justify-start gap-2">
|
<CustomMenu.MenuItem onClick={handleCopyText}>
|
||||||
<LinkIcon className="h-4 w-4" />
|
<div className="flex items-center justify-start gap-2">
|
||||||
<span>Copy issue Link</span>
|
<LinkIcon className="h-4 w-4" />
|
||||||
</div>
|
<span>Copy issue Link</span>
|
||||||
</CustomMenu.MenuItem>
|
</div>
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
)}
|
||||||
</CustomMenu>
|
</CustomMenu>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Link href={`/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`}>
|
|
||||||
<a className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
{properties.key && (
|
{properties.key && (
|
||||||
<div className="text-xs font-medium text-custom-text-200">
|
<div className="text-xs font-medium text-custom-text-200">
|
||||||
{issue.project_detail.identifier}-{issue.sequence_id}
|
{issue.project_detail.identifier}-{issue.sequence_id}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<h5 className="text-sm break-words line-clamp-2">{issue.name}</h5>
|
<button
|
||||||
</a>
|
type="button"
|
||||||
</Link>
|
onClick={() => {
|
||||||
|
if (isDraftIssue && handleDraftIssueEdit) handleDraftIssueEdit();
|
||||||
|
else openPeekOverview();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="text-sm text-left break-words line-clamp-2">{issue.name}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={`flex items-center gap-2 text-xs ${
|
className={`flex items-center gap-2 text-xs ${
|
||||||
isDropdownActive ? "" : "overflow-x-scroll"
|
isDropdownActive ? "" : "overflow-x-scroll"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{properties.priority && (
|
{properties.priority && (
|
||||||
<ViewPrioritySelect
|
<PrioritySelect
|
||||||
issue={issue}
|
value={issue.priority}
|
||||||
partialUpdateIssue={partialUpdateIssue}
|
onChange={handlePriorityChange}
|
||||||
isNotAllowed={isNotAllowed}
|
hideDropdownArrow
|
||||||
user={user}
|
disabled={isNotAllowed}
|
||||||
selfPositioned
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{properties.state && (
|
{properties.state && (
|
||||||
<ViewStateSelect
|
<StateSelect
|
||||||
issue={issue}
|
value={issue.state_detail}
|
||||||
partialUpdateIssue={partialUpdateIssue}
|
onChange={handleStateChange}
|
||||||
isNotAllowed={isNotAllowed}
|
projectId={projectId}
|
||||||
user={user}
|
hideDropdownArrow
|
||||||
selfPositioned
|
disabled={isNotAllowed}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{properties.start_date && issue.start_date && (
|
{properties.start_date && issue.start_date && (
|
||||||
@ -350,16 +480,24 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{properties.labels && issue.labels.length > 0 && (
|
{properties.labels && issue.labels.length > 0 && (
|
||||||
<ViewIssueLabel labelDetails={issue.label_details} maxRender={2} />
|
<LabelSelect
|
||||||
|
value={issue.labels}
|
||||||
|
projectId={projectId}
|
||||||
|
onChange={handleLabelChange}
|
||||||
|
labelsDetails={issue.label_details}
|
||||||
|
hideDropdownArrow
|
||||||
|
user={user}
|
||||||
|
disabled={isNotAllowed}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{properties.assignee && (
|
{properties.assignee && (
|
||||||
<ViewAssigneeSelect
|
<MembersSelect
|
||||||
issue={issue}
|
value={issue.assignees}
|
||||||
partialUpdateIssue={partialUpdateIssue}
|
projectId={projectId}
|
||||||
isNotAllowed={isNotAllowed}
|
onChange={handleAssigneeChange}
|
||||||
customButton
|
membersDetails={issue.assignee_details}
|
||||||
user={user}
|
hideDropdownArrow
|
||||||
selfPositioned
|
disabled={isNotAllowed}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{properties.estimate && issue.estimate_point !== null && (
|
{properties.estimate && issue.estimate_point !== null && (
|
||||||
|
@ -5,25 +5,12 @@ import { Popover, Transition } from "@headlessui/react";
|
|||||||
// ui
|
// ui
|
||||||
import { CustomMenu, ToggleSwitch } from "components/ui";
|
import { CustomMenu, ToggleSwitch } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
import {
|
import { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/24/outline";
|
||||||
CheckIcon,
|
|
||||||
ChevronDownIcon,
|
|
||||||
ChevronLeftIcon,
|
|
||||||
ChevronRightIcon,
|
|
||||||
} from "@heroicons/react/24/outline";
|
|
||||||
// helpers
|
// helpers
|
||||||
import {
|
import {
|
||||||
addMonths,
|
|
||||||
addSevenDaysToDate,
|
|
||||||
formatDate,
|
formatDate,
|
||||||
getCurrentWeekEndDate,
|
|
||||||
getCurrentWeekStartDate,
|
|
||||||
isSameMonth,
|
isSameMonth,
|
||||||
isSameYear,
|
isSameYear,
|
||||||
lastDayOfWeek,
|
|
||||||
startOfWeek,
|
|
||||||
subtract7DaysToDate,
|
|
||||||
subtractMonths,
|
|
||||||
updateDateWithMonth,
|
updateDateWithMonth,
|
||||||
updateDateWithYear,
|
updateDateWithYear,
|
||||||
} from "helpers/calendar.helper";
|
} from "helpers/calendar.helper";
|
||||||
@ -31,190 +18,136 @@ import {
|
|||||||
import { MONTHS_LIST, YEARS_LIST } from "constants/calendar";
|
import { MONTHS_LIST, YEARS_LIST } from "constants/calendar";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isMonthlyView: boolean;
|
|
||||||
setIsMonthlyView: React.Dispatch<React.SetStateAction<boolean>>;
|
|
||||||
currentDate: Date;
|
currentDate: Date;
|
||||||
setCurrentDate: React.Dispatch<React.SetStateAction<Date>>;
|
setCurrentDate: React.Dispatch<React.SetStateAction<Date>>;
|
||||||
showWeekEnds: boolean;
|
showWeekEnds: boolean;
|
||||||
setShowWeekEnds: React.Dispatch<React.SetStateAction<boolean>>;
|
setShowWeekEnds: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
changeDateRange: (startDate: Date, endDate: Date) => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CalendarHeader: React.FC<Props> = ({
|
export const CalendarHeader: React.FC<Props> = ({
|
||||||
setIsMonthlyView,
|
|
||||||
isMonthlyView,
|
|
||||||
currentDate,
|
currentDate,
|
||||||
setCurrentDate,
|
setCurrentDate,
|
||||||
showWeekEnds,
|
showWeekEnds,
|
||||||
setShowWeekEnds,
|
setShowWeekEnds,
|
||||||
changeDateRange,
|
}) => (
|
||||||
}) => {
|
<div className="mb-4 flex items-center justify-between">
|
||||||
const updateDate = (date: Date) => {
|
<div className="relative flex h-full w-full items-center justify-start gap-2 text-sm ">
|
||||||
setCurrentDate(date);
|
<Popover className="flex h-full items-center justify-start rounded-lg">
|
||||||
|
{({ open }) => (
|
||||||
|
<>
|
||||||
|
<Popover.Button>
|
||||||
|
<div className="flex items-center justify-center gap-2 text-2xl font-semibold text-custom-text-100">
|
||||||
|
<span>{formatDate(currentDate, "Month")}</span>{" "}
|
||||||
|
<span>{formatDate(currentDate, "yyyy")}</span>
|
||||||
|
</div>
|
||||||
|
</Popover.Button>
|
||||||
|
|
||||||
changeDateRange(startOfWeek(date), lastDayOfWeek(date));
|
<Transition
|
||||||
};
|
as={React.Fragment}
|
||||||
|
enter="transition ease-out duration-200"
|
||||||
return (
|
enterFrom="opacity-0 translate-y-1"
|
||||||
<div className="mb-4 flex items-center justify-between">
|
enterTo="opacity-100 translate-y-0"
|
||||||
<div className="relative flex h-full w-full items-center justify-start gap-2 text-sm ">
|
leave="transition ease-in duration-150"
|
||||||
<Popover className="flex h-full items-center justify-start rounded-lg">
|
leaveFrom="opacity-100 translate-y-0"
|
||||||
{({ open }) => (
|
leaveTo="opacity-0 translate-y-1"
|
||||||
<>
|
>
|
||||||
<Popover.Button>
|
<Popover.Panel className="absolute top-10 left-0 z-20 flex w-full max-w-xs transform flex-col overflow-hidden rounded-[10px] bg-custom-background-80 shadow-lg">
|
||||||
<div className="flex items-center justify-center gap-2 text-2xl font-semibold text-custom-text-100">
|
<div className="flex items-center justify-center gap-5 px-2 py-2 text-sm">
|
||||||
<span>{formatDate(currentDate, "Month")}</span>{" "}
|
{YEARS_LIST.map((year) => (
|
||||||
<span>{formatDate(currentDate, "yyyy")}</span>
|
<button
|
||||||
|
onClick={() => setCurrentDate(updateDateWithYear(year.label, currentDate))}
|
||||||
|
className={` ${
|
||||||
|
isSameYear(year.value, currentDate)
|
||||||
|
? "text-sm font-medium text-custom-text-100"
|
||||||
|
: "text-xs text-custom-text-200 "
|
||||||
|
} hover:text-sm hover:font-medium hover:text-custom-text-100`}
|
||||||
|
>
|
||||||
|
{year.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</Popover.Button>
|
<div className="grid grid-cols-4 border-t border-custom-border-200 px-2">
|
||||||
|
{MONTHS_LIST.map((month) => (
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
setCurrentDate(updateDateWithMonth(`${month.value}`, currentDate))
|
||||||
|
}
|
||||||
|
className={`px-2 py-2 text-xs text-custom-text-200 hover:font-medium hover:text-custom-text-100 ${
|
||||||
|
isSameMonth(`${month.value}`, currentDate)
|
||||||
|
? "font-medium text-custom-text-100"
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{month.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Popover.Panel>
|
||||||
|
</Transition>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Popover>
|
||||||
|
|
||||||
<Transition
|
<div className="flex items-center gap-2">
|
||||||
as={React.Fragment}
|
|
||||||
enter="transition ease-out duration-200"
|
|
||||||
enterFrom="opacity-0 translate-y-1"
|
|
||||||
enterTo="opacity-100 translate-y-0"
|
|
||||||
leave="transition ease-in duration-150"
|
|
||||||
leaveFrom="opacity-100 translate-y-0"
|
|
||||||
leaveTo="opacity-0 translate-y-1"
|
|
||||||
>
|
|
||||||
<Popover.Panel className="absolute top-10 left-0 z-20 flex w-full max-w-xs transform flex-col overflow-hidden rounded-[10px] bg-custom-background-80 shadow-lg">
|
|
||||||
<div className="flex items-center justify-center gap-5 px-2 py-2 text-sm">
|
|
||||||
{YEARS_LIST.map((year) => (
|
|
||||||
<button
|
|
||||||
onClick={() => updateDate(updateDateWithYear(year.label, currentDate))}
|
|
||||||
className={` ${
|
|
||||||
isSameYear(year.value, currentDate)
|
|
||||||
? "text-sm font-medium text-custom-text-100"
|
|
||||||
: "text-xs text-custom-text-200 "
|
|
||||||
} hover:text-sm hover:font-medium hover:text-custom-text-100`}
|
|
||||||
>
|
|
||||||
{year.label}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-4 border-t border-custom-border-200 px-2">
|
|
||||||
{MONTHS_LIST.map((month) => (
|
|
||||||
<button
|
|
||||||
onClick={() =>
|
|
||||||
updateDate(updateDateWithMonth(`${month.value}`, currentDate))
|
|
||||||
}
|
|
||||||
className={`px-2 py-2 text-xs text-custom-text-200 hover:font-medium hover:text-custom-text-100 ${
|
|
||||||
isSameMonth(`${month.value}`, currentDate)
|
|
||||||
? "font-medium text-custom-text-100"
|
|
||||||
: ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{month.label}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Popover.Panel>
|
|
||||||
</Transition>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Popover>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<button
|
|
||||||
className="cursor-pointer"
|
|
||||||
onClick={() => {
|
|
||||||
if (isMonthlyView) {
|
|
||||||
updateDate(subtractMonths(currentDate, 1));
|
|
||||||
} else {
|
|
||||||
setCurrentDate(subtract7DaysToDate(currentDate));
|
|
||||||
changeDateRange(
|
|
||||||
getCurrentWeekStartDate(subtract7DaysToDate(currentDate)),
|
|
||||||
getCurrentWeekEndDate(subtract7DaysToDate(currentDate))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ChevronLeftIcon className="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="cursor-pointer"
|
|
||||||
onClick={() => {
|
|
||||||
if (isMonthlyView) {
|
|
||||||
updateDate(addMonths(currentDate, 1));
|
|
||||||
} else {
|
|
||||||
setCurrentDate(addSevenDaysToDate(currentDate));
|
|
||||||
changeDateRange(
|
|
||||||
getCurrentWeekStartDate(addSevenDaysToDate(currentDate)),
|
|
||||||
getCurrentWeekEndDate(addSevenDaysToDate(currentDate))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ChevronRightIcon className="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex w-full items-center justify-end gap-2">
|
|
||||||
<button
|
<button
|
||||||
className="group flex cursor-pointer items-center gap-2 rounded-md border border-custom-border-200 px-3 py-1 text-sm hover:bg-custom-background-80 hover:text-custom-text-100 focus:outline-none"
|
className="cursor-pointer"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (isMonthlyView) {
|
const previousMonthYear =
|
||||||
updateDate(new Date());
|
currentDate.getMonth() === 0
|
||||||
} else {
|
? currentDate.getFullYear() - 1
|
||||||
setCurrentDate(new Date());
|
: currentDate.getFullYear();
|
||||||
changeDateRange(
|
const previousMonthMonth =
|
||||||
getCurrentWeekStartDate(new Date()),
|
currentDate.getMonth() === 0 ? 11 : currentDate.getMonth() - 1;
|
||||||
getCurrentWeekEndDate(new Date())
|
|
||||||
);
|
const previousMonthFirstDate = new Date(previousMonthYear, previousMonthMonth, 1);
|
||||||
}
|
|
||||||
|
setCurrentDate(previousMonthFirstDate);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Today
|
<ChevronLeftIcon className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={() => {
|
||||||
|
const nextMonthYear =
|
||||||
|
currentDate.getMonth() === 11
|
||||||
|
? currentDate.getFullYear() + 1
|
||||||
|
: currentDate.getFullYear();
|
||||||
|
const nextMonthMonth = (currentDate.getMonth() + 1) % 12;
|
||||||
|
|
||||||
<CustomMenu
|
const nextMonthFirstDate = new Date(nextMonthYear, nextMonthMonth, 1);
|
||||||
customButton={
|
|
||||||
<div className="group flex cursor-pointer items-center gap-2 rounded-md border border-custom-border-200 px-3 py-1 text-sm hover:bg-custom-background-80 hover:text-custom-text-100 focus:outline-none ">
|
setCurrentDate(nextMonthFirstDate);
|
||||||
{isMonthlyView ? "Monthly" : "Weekly"}
|
}}
|
||||||
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<CustomMenu.MenuItem
|
<ChevronRightIcon className="h-4 w-4" />
|
||||||
onClick={() => {
|
</button>
|
||||||
setIsMonthlyView(true);
|
|
||||||
changeDateRange(startOfWeek(currentDate), lastDayOfWeek(currentDate));
|
|
||||||
}}
|
|
||||||
className="w-52 text-sm text-custom-text-200"
|
|
||||||
>
|
|
||||||
<div className="flex w-full max-w-[260px] items-center justify-between gap-2">
|
|
||||||
<span className="flex items-center gap-2">Monthly View</span>
|
|
||||||
<CheckIcon
|
|
||||||
className={`h-4 w-4 flex-shrink-0 ${isMonthlyView ? "opacity-100" : "opacity-0"}`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
<CustomMenu.MenuItem
|
|
||||||
onClick={() => {
|
|
||||||
setIsMonthlyView(false);
|
|
||||||
changeDateRange(
|
|
||||||
getCurrentWeekStartDate(currentDate),
|
|
||||||
getCurrentWeekEndDate(currentDate)
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
className="w-52 text-sm text-custom-text-200"
|
|
||||||
>
|
|
||||||
<div className="flex w-full items-center justify-between gap-2">
|
|
||||||
<span className="flex items-center gap-2">Weekly View</span>
|
|
||||||
<CheckIcon
|
|
||||||
className={`h-4 w-4 flex-shrink-0 ${isMonthlyView ? "opacity-0" : "opacity-100"}`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
<div className="mt-1 flex w-52 items-center justify-between border-t border-custom-border-200 py-2 px-1 text-sm text-custom-text-200">
|
|
||||||
<h4>Show weekends</h4>
|
|
||||||
<ToggleSwitch value={showWeekEnds} onChange={() => setShowWeekEnds(!showWeekEnds)} />
|
|
||||||
</div>
|
|
||||||
</CustomMenu>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
|
||||||
};
|
<div className="flex w-full items-center justify-end gap-2">
|
||||||
|
<button
|
||||||
|
className="group flex cursor-pointer items-center gap-2 rounded-md border border-custom-border-200 px-3 py-1 text-sm hover:bg-custom-background-80 hover:text-custom-text-100 focus:outline-none"
|
||||||
|
onClick={() => setCurrentDate(new Date())}
|
||||||
|
>
|
||||||
|
Today
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<CustomMenu
|
||||||
|
customButton={
|
||||||
|
<div className="group flex cursor-pointer items-center gap-2 rounded-md border border-custom-border-200 px-3 py-1 text-sm hover:bg-custom-background-80 hover:text-custom-text-100 focus:outline-none">
|
||||||
|
Options
|
||||||
|
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="flex w-52 items-center justify-between px-1 text-sm text-custom-text-200">
|
||||||
|
<h4>Show weekends</h4>
|
||||||
|
<ToggleSwitch value={showWeekEnds} onChange={() => setShowWeekEnds(!showWeekEnds)} />
|
||||||
|
</div>
|
||||||
|
</CustomMenu>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
export default CalendarHeader;
|
export default CalendarHeader;
|
||||||
|
@ -1,10 +1,6 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
import { mutate } from "swr";
|
import { mutate } from "swr";
|
||||||
|
|
||||||
// react-beautiful-dnd
|
|
||||||
import { DragDropContext, DropResult } from "react-beautiful-dnd";
|
import { DragDropContext, DropResult } from "react-beautiful-dnd";
|
||||||
// services
|
// services
|
||||||
import issuesService from "services/issues.service";
|
import issuesService from "services/issues.service";
|
||||||
@ -12,6 +8,7 @@ import issuesService from "services/issues.service";
|
|||||||
import useCalendarIssuesView from "hooks/use-calendar-issues-view";
|
import useCalendarIssuesView from "hooks/use-calendar-issues-view";
|
||||||
// components
|
// components
|
||||||
import { SingleCalendarDate, CalendarHeader } from "components/core";
|
import { SingleCalendarDate, CalendarHeader } from "components/core";
|
||||||
|
import { IssuePeekOverview } from "components/issues";
|
||||||
// ui
|
// ui
|
||||||
import { Spinner } from "components/ui";
|
import { Spinner } from "components/ui";
|
||||||
// helpers
|
// helpers
|
||||||
@ -49,30 +46,27 @@ export const CalendarView: React.FC<Props> = ({
|
|||||||
userAuth,
|
userAuth,
|
||||||
}) => {
|
}) => {
|
||||||
const [showWeekEnds, setShowWeekEnds] = useState(false);
|
const [showWeekEnds, setShowWeekEnds] = useState(false);
|
||||||
const [currentDate, setCurrentDate] = useState(new Date());
|
|
||||||
const [isMonthlyView, setIsMonthlyView] = useState(true);
|
const { calendarIssues, mutateIssues, params, activeMonthDate, setActiveMonthDate } =
|
||||||
|
useCalendarIssuesView();
|
||||||
|
|
||||||
const [calendarDates, setCalendarDates] = useState<ICalendarRange>({
|
const [calendarDates, setCalendarDates] = useState<ICalendarRange>({
|
||||||
startDate: startOfWeek(currentDate),
|
startDate: startOfWeek(activeMonthDate),
|
||||||
endDate: lastDayOfWeek(currentDate),
|
endDate: lastDayOfWeek(activeMonthDate),
|
||||||
});
|
});
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
|
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
|
||||||
|
|
||||||
const { calendarIssues, params, setCalendarDateRange } = useCalendarIssuesView();
|
const currentViewDays = showWeekEnds
|
||||||
|
? eachDayOfInterval({
|
||||||
const totalDate = eachDayOfInterval({
|
start: calendarDates.startDate,
|
||||||
start: calendarDates.startDate,
|
end: calendarDates.endDate,
|
||||||
end: calendarDates.endDate,
|
})
|
||||||
});
|
: weekDayInterval({
|
||||||
|
start: calendarDates.startDate,
|
||||||
const onlyWeekDays = weekDayInterval({
|
end: calendarDates.endDate,
|
||||||
start: calendarDates.startDate,
|
});
|
||||||
end: calendarDates.endDate,
|
|
||||||
});
|
|
||||||
|
|
||||||
const currentViewDays = showWeekEnds ? totalDate : onlyWeekDays;
|
|
||||||
|
|
||||||
const currentViewDaysData = currentViewDays.map((date: Date) => {
|
const currentViewDaysData = currentViewDays.map((date: Date) => {
|
||||||
const filterIssue =
|
const filterIssue =
|
||||||
@ -146,96 +140,78 @@ export const CalendarView: React.FC<Props> = ({
|
|||||||
.then(() => mutate(fetchKey));
|
.then(() => mutate(fetchKey));
|
||||||
};
|
};
|
||||||
|
|
||||||
const changeDateRange = (startDate: Date, endDate: Date) => {
|
|
||||||
setCalendarDates({
|
|
||||||
startDate,
|
|
||||||
endDate,
|
|
||||||
});
|
|
||||||
|
|
||||||
setCalendarDateRange(
|
|
||||||
`${renderDateFormat(startDate)};after,${renderDateFormat(endDate)};before`
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCalendarDateRange(
|
setCalendarDates({
|
||||||
`${renderDateFormat(startOfWeek(currentDate))};after,${renderDateFormat(
|
startDate: startOfWeek(activeMonthDate),
|
||||||
lastDayOfWeek(currentDate)
|
endDate: lastDayOfWeek(activeMonthDate),
|
||||||
)};before`
|
});
|
||||||
);
|
}, [activeMonthDate]);
|
||||||
}, [currentDate]);
|
|
||||||
|
|
||||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disableUserActions;
|
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disableUserActions;
|
||||||
|
|
||||||
return calendarIssues ? (
|
return (
|
||||||
<div className="h-full overflow-y-auto">
|
<>
|
||||||
<DragDropContext onDragEnd={onDragEnd}>
|
<IssuePeekOverview
|
||||||
<div className="h-full rounded-lg p-8 text-custom-text-200">
|
handleMutation={() => mutateIssues()}
|
||||||
<CalendarHeader
|
projectId={projectId?.toString() ?? ""}
|
||||||
isMonthlyView={isMonthlyView}
|
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
||||||
setIsMonthlyView={setIsMonthlyView}
|
readOnly={disableUserActions}
|
||||||
showWeekEnds={showWeekEnds}
|
/>
|
||||||
setShowWeekEnds={setShowWeekEnds}
|
{calendarIssues ? (
|
||||||
currentDate={currentDate}
|
<div className="h-full overflow-y-auto">
|
||||||
setCurrentDate={setCurrentDate}
|
<DragDropContext onDragEnd={onDragEnd}>
|
||||||
changeDateRange={changeDateRange}
|
<div
|
||||||
/>
|
id={`calendar-view-${cycleId ?? moduleId ?? viewId ?? ""}`}
|
||||||
|
className="h-full rounded-lg p-8 text-custom-text-200"
|
||||||
|
>
|
||||||
|
<CalendarHeader
|
||||||
|
showWeekEnds={showWeekEnds}
|
||||||
|
setShowWeekEnds={setShowWeekEnds}
|
||||||
|
currentDate={activeMonthDate}
|
||||||
|
setCurrentDate={setActiveMonthDate}
|
||||||
|
/>
|
||||||
|
|
||||||
<div
|
|
||||||
className={`grid auto-rows-[minmax(36px,1fr)] rounded-lg ${
|
|
||||||
showWeekEnds ? "grid-cols-7" : "grid-cols-5"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{weeks.map((date, index) => (
|
|
||||||
<div
|
<div
|
||||||
key={index}
|
className={`grid auto-rows-[minmax(36px,1fr)] rounded-lg ${
|
||||||
className={`flex items-center justify-start gap-2 border-custom-border-200 bg-custom-background-90 p-1.5 text-base font-medium text-custom-text-200 ${
|
showWeekEnds ? "grid-cols-7" : "grid-cols-5"
|
||||||
!isMonthlyView
|
|
||||||
? showWeekEnds
|
|
||||||
? (index + 1) % 7 === 0
|
|
||||||
? ""
|
|
||||||
: "border-r"
|
|
||||||
: (index + 1) % 5 === 0
|
|
||||||
? ""
|
|
||||||
: "border-r"
|
|
||||||
: ""
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span>
|
{weeks.map((date, index) => (
|
||||||
{isMonthlyView
|
<div
|
||||||
? formatDate(date, "eee").substring(0, 3)
|
key={index}
|
||||||
: formatDate(date, "eee")}
|
className={`flex items-center justify-start gap-2 border-custom-border-200 bg-custom-background-90 p-1.5 text-base font-medium text-custom-text-200`}
|
||||||
</span>
|
>
|
||||||
{!isMonthlyView && <span>{formatDate(date, "d")}</span>}
|
<span>{formatDate(date, "eee").substring(0, 3)}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={`grid h-full ${isMonthlyView ? "auto-rows-min" : ""} ${
|
className={`grid h-full auto-rows-min ${
|
||||||
showWeekEnds ? "grid-cols-7" : "grid-cols-5"
|
showWeekEnds ? "grid-cols-7" : "grid-cols-5"
|
||||||
} `}
|
} `}
|
||||||
>
|
>
|
||||||
{currentViewDaysData.map((date, index) => (
|
{currentViewDaysData.map((date, index) => (
|
||||||
<SingleCalendarDate
|
<SingleCalendarDate
|
||||||
key={`${date}-${index}`}
|
key={`${date}-${index}`}
|
||||||
index={index}
|
index={index}
|
||||||
date={date}
|
date={date}
|
||||||
handleIssueAction={handleIssueAction}
|
handleIssueAction={handleIssueAction}
|
||||||
addIssueToDate={addIssueToDate}
|
addIssueToDate={addIssueToDate}
|
||||||
isMonthlyView={isMonthlyView}
|
showWeekEnds={showWeekEnds}
|
||||||
showWeekEnds={showWeekEnds}
|
user={user}
|
||||||
user={user}
|
isNotAllowed={isNotAllowed}
|
||||||
isNotAllowed={isNotAllowed}
|
/>
|
||||||
/>
|
))}
|
||||||
))}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</DragDropContext>
|
||||||
</div>
|
</div>
|
||||||
</DragDropContext>
|
) : (
|
||||||
</div>
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
) : (
|
<Spinner />
|
||||||
<div className="flex h-full w-full items-center justify-center">
|
</div>
|
||||||
<Spinner />
|
)}
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user