Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cd9cebc7af | |||
| b77ddf9c95 | |||
| 4133798a81 | |||
| 761aefa73b | |||
| fa403ef4ae | |||
| 3ce0aa8ebc |
@@ -1,3 +1,36 @@
|
||||
# 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=""
|
||||
|
||||
# Backend
|
||||
# Debug value for api server use it as 0 for production use
|
||||
DEBUG=0
|
||||
|
||||
# Error logs
|
||||
SENTRY_DSN=""
|
||||
|
||||
# Database Settings
|
||||
PGUSER="plane"
|
||||
PGPASSWORD="plane"
|
||||
@@ -10,6 +43,15 @@ 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"
|
||||
@@ -25,6 +67,9 @@ 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
|
||||
@@ -33,3 +78,10 @@ USE_MINIO=1
|
||||
# Nginx Configuration
|
||||
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
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ module.exports = {
|
||||
extends: ["custom"],
|
||||
settings: {
|
||||
next: {
|
||||
rootDir: ["web/", "space/"],
|
||||
rootDir: ["web/", "deploy/"],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
name: Build Pull Request Contents
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: ["opened", "synchronize"]
|
||||
|
||||
jobs:
|
||||
build-pull-request-contents:
|
||||
name: Build Pull Request Contents
|
||||
runs-on: ubuntu-20.04
|
||||
permissions:
|
||||
pull-requests: read
|
||||
|
||||
steps:
|
||||
- name: Checkout Repository to Actions
|
||||
uses: actions/checkout@v3.3.0
|
||||
|
||||
- name: Setup Node.js 18.x
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 18.x
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Get changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v38
|
||||
with:
|
||||
files_yaml: |
|
||||
apiserver:
|
||||
- apiserver/**
|
||||
web:
|
||||
- web/**
|
||||
deploy:
|
||||
- space/**
|
||||
|
||||
- name: Build Plane's Main App
|
||||
if: steps.changed-files.outputs.web_any_changed == 'true'
|
||||
run: |
|
||||
cd web
|
||||
yarn
|
||||
yarn build
|
||||
|
||||
- name: Build Plane's Deploy App
|
||||
if: steps.changed-files.outputs.deploy_any_changed == 'true'
|
||||
run: |
|
||||
cd space
|
||||
yarn
|
||||
yarn build
|
||||
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
name: Update Docker Images for Plane on Release
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [released, prereleased]
|
||||
|
||||
jobs:
|
||||
build_push_backend:
|
||||
name: Build and Push Api Server Docker Image
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v3.3.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2.5.0
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.1.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker (Docker Hub) from Github Release
|
||||
id: metaFrontend
|
||||
uses: docker/metadata-action@v4.3.0
|
||||
with:
|
||||
images: ${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend
|
||||
tags: |
|
||||
type=ref,event=tag
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker (Docker Hub) from Github Release
|
||||
id: metaBackend
|
||||
uses: docker/metadata-action@v4.3.0
|
||||
with:
|
||||
images: ${{ secrets.DOCKERHUB_USERNAME }}/plane-backend
|
||||
tags: |
|
||||
type=ref,event=tag
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker (Docker Hub) from Github Release
|
||||
id: metaDeploy
|
||||
uses: docker/metadata-action@v4.3.0
|
||||
with:
|
||||
images: ${{ secrets.DOCKERHUB_USERNAME }}/plane-deploy
|
||||
tags: |
|
||||
type=ref,event=tag
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker (Docker Hub) from Github Release
|
||||
id: metaProxy
|
||||
uses: docker/metadata-action@v4.3.0
|
||||
with:
|
||||
images: ${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy
|
||||
tags: |
|
||||
type=ref,event=tag
|
||||
|
||||
- name: Build and Push Frontend to Docker Container Registry
|
||||
uses: docker/build-push-action@v4.0.0
|
||||
with:
|
||||
context: .
|
||||
file: ./web/Dockerfile.web
|
||||
platforms: linux/amd64
|
||||
tags: ${{ steps.metaFrontend.outputs.tags }}
|
||||
push: true
|
||||
env:
|
||||
DOCKER_BUILDKIT: 1
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
DOCKET_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and Push Backend to Docker Hub
|
||||
uses: docker/build-push-action@v4.0.0
|
||||
with:
|
||||
context: ./apiserver
|
||||
file: ./apiserver/Dockerfile.api
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
tags: ${{ steps.metaBackend.outputs.tags }}
|
||||
env:
|
||||
DOCKER_BUILDKIT: 1
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
DOCKET_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and Push Plane-Deploy to Docker Hub
|
||||
uses: docker/build-push-action@v4.0.0
|
||||
with:
|
||||
context: .
|
||||
file: ./space/Dockerfile.space
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
tags: ${{ steps.metaDeploy.outputs.tags }}
|
||||
env:
|
||||
DOCKER_BUILDKIT: 1
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
DOCKET_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and Push Plane-Proxy to Docker Hub
|
||||
uses: docker/build-push-action@v4.0.0
|
||||
with:
|
||||
context: ./nginx
|
||||
file: ./nginx/Dockerfile
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
tags: ${{ steps.metaProxy.outputs.tags }}
|
||||
env:
|
||||
DOCKER_BUILDKIT: 1
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
DOCKET_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
@@ -0,0 +1,77 @@
|
||||
name: Build and Push Backend Docker Image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'develop'
|
||||
- 'master'
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
build_push_backend:
|
||||
name: Build and Push Api Server Docker Image
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v3.3.0
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2.1.0
|
||||
with:
|
||||
platforms: linux/arm64,linux/amd64
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2.5.0
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.1.0
|
||||
with:
|
||||
registry: "ghcr.io"
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.1.0
|
||||
with:
|
||||
registry: "registry.hub.docker.com"
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker (Docker Hub)
|
||||
id: ghmeta
|
||||
uses: docker/metadata-action@v4.3.0
|
||||
with:
|
||||
images: makeplane/plane-backend
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker (Github)
|
||||
id: dkrmeta
|
||||
uses: docker/metadata-action@v4.3.0
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository }}-backend
|
||||
|
||||
- name: Build and Push to GitHub Container Registry
|
||||
uses: docker/build-push-action@v4.0.0
|
||||
with:
|
||||
context: ./apiserver
|
||||
file: ./apiserver/Dockerfile.api
|
||||
platforms: linux/arm64,linux/amd64
|
||||
push: true
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha
|
||||
tags: ${{ steps.ghmeta.outputs.tags }}
|
||||
labels: ${{ steps.ghmeta.outputs.labels }}
|
||||
|
||||
- name: Build and Push to Docker Hub
|
||||
uses: docker/build-push-action@v4.0.0
|
||||
with:
|
||||
context: ./apiserver
|
||||
file: ./apiserver/Dockerfile.api
|
||||
platforms: linux/arm64,linux/amd64
|
||||
push: true
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha
|
||||
tags: ${{ steps.dkrmeta.outputs.tags }}
|
||||
labels: ${{ steps.dkrmeta.outputs.labels }}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
name: Build and Push Frontend Docker Image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'develop'
|
||||
- 'master'
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
build_push_frontend:
|
||||
name: Build Frontend Docker Image
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v3.3.0
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2.1.0
|
||||
with:
|
||||
platforms: linux/arm64,linux/amd64
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2.5.0
|
||||
|
||||
- name: Login to Github Container Registry
|
||||
uses: docker/login-action@v2.1.0
|
||||
with:
|
||||
registry: "ghcr.io"
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.1.0
|
||||
with:
|
||||
registry: "registry.hub.docker.com"
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker (Docker Hub)
|
||||
id: ghmeta
|
||||
uses: docker/metadata-action@v4.3.0
|
||||
with:
|
||||
images: makeplane/plane-frontend
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker (Github)
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4.3.0
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository }}-frontend
|
||||
|
||||
- name: Build and Push to GitHub Container Registry
|
||||
uses: docker/build-push-action@v4.0.0
|
||||
with:
|
||||
context: .
|
||||
file: ./apps/app/Dockerfile.web
|
||||
platforms: linux/arm64,linux/amd64
|
||||
push: true
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha
|
||||
tags: ${{ steps.ghmeta.outputs.tags }}
|
||||
labels: ${{ steps.ghmeta.outputs.labels }}
|
||||
|
||||
- name: Build and Push to Docker Container Registry
|
||||
uses: docker/build-push-action@v4.0.0
|
||||
with:
|
||||
context: .
|
||||
file: ./apps/app/Dockerfile.web
|
||||
platforms: linux/arm64,linux/amd64
|
||||
push: true
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha
|
||||
tags: ${{ steps.dkrmeta.outputs.tags }}
|
||||
labels: ${{ steps.dkrmeta.outputs.labels }}
|
||||
|
||||
+12
-12
@@ -17,23 +17,23 @@ diverse, inclusive, and healthy community.
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
- Demonstrating empathy and kindness toward other people
|
||||
- Being respectful of differing opinions, viewpoints, and experiences
|
||||
- Giving and gracefully accepting constructive feedback
|
||||
- Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
- Focusing on what is best not just for us as individuals, but for the
|
||||
* Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
- The use of sexualized language or imagery, and sexual attention or
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
- Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
- Public or private harassment
|
||||
- Publishing others' private information, such as a physical or email
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
- Other conduct which could reasonably be considered inappropriate in a
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
@@ -106,7 +106,7 @@ Violating these terms may lead to a permanent ban.
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
@@ -125,4 +125,4 @@ enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at
|
||||
https://www.contributor-covenant.org/translations.
|
||||
https://www.contributor-covenant.org/translations.
|
||||
@@ -35,10 +35,12 @@
|
||||
|
||||
Meet [Plane](https://plane.so). An open-source software development tool to manage issues, sprints, and product roadmaps with peace of mind 🧘♀️.
|
||||
|
||||
|
||||
> Plane is still in its early days, not everything will be perfect yet, and hiccups may happen. Please let us know of any suggestions, ideas, or bugs that you encounter on our [Discord](https://discord.com/invite/A92xrEGCge) or GitHub issues, and we will use your feedback to improve on our upcoming releases.
|
||||
|
||||
The easiest way to get started with Plane is by creating a [Plane Cloud](https://app.plane.so) account. Plane Cloud offers a hosted solution for Plane. If you prefer to self-host Plane, please refer to our [deployment documentation](https://docs.plane.so/self-hosting).
|
||||
|
||||
|
||||
## ⚡️ Quick start with Docker Compose
|
||||
|
||||
### Docker Compose Setup
|
||||
@@ -54,30 +56,40 @@ chmod +x setup.sh
|
||||
- Run setup.sh
|
||||
|
||||
```bash
|
||||
./setup.sh http://localhost
|
||||
./setup.sh http://localhost
|
||||
```
|
||||
|
||||
> If running in a cloud env replace localhost with public facing IP address of the VM
|
||||
|
||||
- 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
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
<strong>You can use the default email and password for your first login `captain@plane.so` and `password123`.</strong>
|
||||
<strong>You can use the default email and password for your first login `captain@plane.so` and `password123`.</strong>
|
||||
|
||||
## 🚀 Features
|
||||
|
||||
- **Issue Planning and Tracking**: Quickly create issues and add details using a powerful rich text editor that supports file uploads. Add sub-properties and references to issues for better organization and tracking.
|
||||
- **Issue Attachments**: Collaborate effectively by attaching files to issues, making it easy for your team to find and share important project-related documents.
|
||||
- **Layouts**: Customize your project view with your preferred layout - choose from List, Kanban, or Calendar to visualize your project in a way that makes sense to you.
|
||||
- **Cycles**: Plan sprints with Cycles to keep your team on track and productive. Gain insights into your project's progress with burn-down charts and other useful features.
|
||||
- **Modules**: Break down your large projects into smaller, more manageable modules. Assign modules between teams to easily track and plan your project's progress.
|
||||
- **Views**: Create custom filters to display only the issues that matter to you. Save and share your filters in just a few clicks.
|
||||
- **Pages**: Plane pages function as an AI-powered notepad, allowing you to easily document issues, cycle plans, and module details, and then synchronize them with your issues.
|
||||
- **Command K**: Enjoy a better user experience with the new Command + K menu. Easily manage and navigate through your projects from one convenient location.
|
||||
- **GitHub Sync**: Streamline your planning process by syncing your GitHub issues with Plane. Keep all your issues in one place for better tracking and collaboration.
|
||||
* **Issue Planning and Tracking**: Quickly create issues and add details using a powerful rich text editor that supports file uploads. Add sub-properties and references to issues for better organization and tracking.
|
||||
* **Issue Attachments**: Collaborate effectively by attaching files to issues, making it easy for your team to find and share important project-related documents.
|
||||
* **Layouts**: Customize your project view with your preferred layout - choose from List, Kanban, or Calendar to visualize your project in a way that makes sense to you.
|
||||
* **Cycles**: Plan sprints with Cycles to keep your team on track and productive. Gain insights into your project's progress with burn-down charts and other useful features.
|
||||
* **Modules**: Break down your large projects into smaller, more manageable modules. Assign modules between teams to easily track and plan your project's progress.
|
||||
* **Views**: Create custom filters to display only the issues that matter to you. Save and share your filters in just a few clicks.
|
||||
* **Pages**: Plane pages function as an AI-powered notepad, allowing you to easily document issues, cycle plans, and module details, and then synchronize them with your issues.
|
||||
* **Command K**: Enjoy a better user experience with the new Command + K menu. Easily manage and navigate through your projects from one convenient location.
|
||||
* **GitHub Sync**: Streamline your planning process by syncing your GitHub issues with Plane. Keep all your issues in one place for better tracking and collaboration.
|
||||
|
||||
## 📸 Screenshots
|
||||
|
||||
@@ -138,6 +150,7 @@ docker compose up -d
|
||||
</p>
|
||||
</p>
|
||||
|
||||
|
||||
## 📚Documentation
|
||||
|
||||
For full documentation, visit [docs.plane.so](https://docs.plane.so/)
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
# 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"
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
web: gunicorn -w 4 -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:$PORT --max-requests 10000 --max-requests-jitter 1000 --access-logfile -
|
||||
web: gunicorn -w 4 -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:$PORT --config gunicorn.config.py --max-requests 10000 --max-requests-jitter 1000 --access-logfile -
|
||||
worker: celery -A plane worker -l info
|
||||
beat: celery -A plane beat -l INFO
|
||||
@@ -20,10 +20,9 @@ from .project import (
|
||||
ProjectMemberLiteSerializer,
|
||||
ProjectDeployBoardSerializer,
|
||||
ProjectMemberAdminSerializer,
|
||||
ProjectPublicMemberSerializer
|
||||
)
|
||||
from .state import StateSerializer, StateLiteSerializer
|
||||
from .view import GlobalViewSerializer, IssueViewSerializer, IssueViewFavoriteSerializer
|
||||
from .view import IssueViewSerializer, IssueViewFavoriteSerializer
|
||||
from .cycle import CycleSerializer, CycleIssueSerializer, CycleFavoriteSerializer, CycleWriteSerializer
|
||||
from .asset import FileAssetSerializer
|
||||
from .issue import (
|
||||
@@ -31,6 +30,8 @@ from .issue import (
|
||||
IssueActivitySerializer,
|
||||
IssueCommentSerializer,
|
||||
IssuePropertySerializer,
|
||||
BlockerIssueSerializer,
|
||||
BlockedIssueSerializer,
|
||||
IssueAssigneeSerializer,
|
||||
LabelSerializer,
|
||||
IssueSerializer,
|
||||
@@ -43,9 +44,6 @@ from .issue import (
|
||||
IssueReactionSerializer,
|
||||
CommentReactionSerializer,
|
||||
IssueVoteSerializer,
|
||||
IssueRelationSerializer,
|
||||
RelatedIssueSerializer,
|
||||
IssuePublicSerializer,
|
||||
)
|
||||
|
||||
from .module import (
|
||||
|
||||
@@ -17,10 +17,12 @@ from plane.db.models import (
|
||||
IssueActivity,
|
||||
IssueComment,
|
||||
IssueProperty,
|
||||
IssueBlocker,
|
||||
IssueAssignee,
|
||||
IssueSubscriber,
|
||||
IssueLabel,
|
||||
Label,
|
||||
IssueBlocker,
|
||||
CycleIssue,
|
||||
Cycle,
|
||||
Module,
|
||||
@@ -30,7 +32,6 @@ from plane.db.models import (
|
||||
IssueReaction,
|
||||
CommentReaction,
|
||||
IssueVote,
|
||||
IssueRelation,
|
||||
)
|
||||
|
||||
|
||||
@@ -49,7 +50,6 @@ class IssueFlatSerializer(BaseSerializer):
|
||||
"target_date",
|
||||
"sequence_id",
|
||||
"sort_order",
|
||||
"is_draft",
|
||||
]
|
||||
|
||||
|
||||
@@ -81,12 +81,25 @@ class IssueCreateSerializer(BaseSerializer):
|
||||
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(
|
||||
child=serializers.PrimaryKeyRelatedField(queryset=Label.objects.all()),
|
||||
write_only=True,
|
||||
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:
|
||||
model = Issue
|
||||
fields = "__all__"
|
||||
@@ -100,17 +113,15 @@ class IssueCreateSerializer(BaseSerializer):
|
||||
]
|
||||
|
||||
def validate(self, data):
|
||||
if (
|
||||
data.get("start_date", None) is not None
|
||||
and data.get("target_date", None) is not None
|
||||
and data.get("start_date", None) > data.get("target_date", None)
|
||||
):
|
||||
if data.get("start_date", None) is not None and data.get("target_date", None) is not None and data.get("start_date", None) > data.get("target_date", None):
|
||||
raise serializers.ValidationError("Start date cannot exceed target date")
|
||||
return data
|
||||
|
||||
def create(self, validated_data):
|
||||
blockers = validated_data.pop("blockers_list", None)
|
||||
assignees = validated_data.pop("assignees_list", None)
|
||||
labels = validated_data.pop("labels_list", None)
|
||||
blocks = validated_data.pop("blocks_list", None)
|
||||
|
||||
project_id = self.context["project_id"]
|
||||
workspace_id = self.context["workspace_id"]
|
||||
@@ -122,6 +133,22 @@ class IssueCreateSerializer(BaseSerializer):
|
||||
created_by_id = issue.created_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):
|
||||
IssueAssignee.objects.bulk_create(
|
||||
[
|
||||
@@ -165,11 +192,29 @@ class IssueCreateSerializer(BaseSerializer):
|
||||
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
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
blockers = validated_data.pop("blockers_list", None)
|
||||
assignees = validated_data.pop("assignees_list", None)
|
||||
labels = validated_data.pop("labels_list", None)
|
||||
blocks = validated_data.pop("blocks_list", None)
|
||||
|
||||
# Related models
|
||||
project_id = instance.project_id
|
||||
@@ -177,6 +222,23 @@ class IssueCreateSerializer(BaseSerializer):
|
||||
created_by_id = instance.created_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:
|
||||
IssueAssignee.objects.filter(issue=instance).delete()
|
||||
IssueAssignee.objects.bulk_create(
|
||||
@@ -211,6 +273,23 @@ class IssueCreateSerializer(BaseSerializer):
|
||||
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
|
||||
instance.updated_at = timezone.now()
|
||||
return super().update(instance, validated_data)
|
||||
@@ -292,39 +371,32 @@ class IssueLabelSerializer(BaseSerializer):
|
||||
]
|
||||
|
||||
|
||||
class IssueRelationSerializer(BaseSerializer):
|
||||
issue_detail = IssueProjectLiteSerializer(read_only=True, source="related_issue")
|
||||
class BlockedIssueSerializer(BaseSerializer):
|
||||
blocked_issue_detail = IssueProjectLiteSerializer(source="block", read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = IssueRelation
|
||||
model = IssueBlocker
|
||||
fields = [
|
||||
"issue_detail",
|
||||
"relation_type",
|
||||
"related_issue",
|
||||
"issue",
|
||||
"id"
|
||||
]
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
"project",
|
||||
"blocked_issue_detail",
|
||||
"blocked_by",
|
||||
"block",
|
||||
]
|
||||
read_only_fields = fields
|
||||
|
||||
class RelatedIssueSerializer(BaseSerializer):
|
||||
issue_detail = IssueProjectLiteSerializer(read_only=True, source="issue")
|
||||
|
||||
class BlockerIssueSerializer(BaseSerializer):
|
||||
blocker_issue_detail = IssueProjectLiteSerializer(
|
||||
source="blocked_by", read_only=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = IssueRelation
|
||||
model = IssueBlocker
|
||||
fields = [
|
||||
"issue_detail",
|
||||
"relation_type",
|
||||
"related_issue",
|
||||
"issue",
|
||||
"id"
|
||||
]
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
"project",
|
||||
"blocker_issue_detail",
|
||||
"blocked_by",
|
||||
"block",
|
||||
]
|
||||
read_only_fields = fields
|
||||
|
||||
|
||||
class IssueAssigneeSerializer(BaseSerializer):
|
||||
@@ -438,9 +510,6 @@ class IssueAttachmentSerializer(BaseSerializer):
|
||||
|
||||
|
||||
class IssueReactionSerializer(BaseSerializer):
|
||||
|
||||
actor_detail = UserLiteSerializer(read_only=True, source="actor")
|
||||
|
||||
class Meta:
|
||||
model = IssueReaction
|
||||
fields = "__all__"
|
||||
@@ -452,6 +521,19 @@ class IssueReactionSerializer(BaseSerializer):
|
||||
]
|
||||
|
||||
|
||||
class IssueReactionLiteSerializer(BaseSerializer):
|
||||
actor_detail = UserLiteSerializer(read_only=True, source="actor")
|
||||
|
||||
class Meta:
|
||||
model = IssueReaction
|
||||
fields = [
|
||||
"id",
|
||||
"reaction",
|
||||
"issue",
|
||||
"actor_detail",
|
||||
]
|
||||
|
||||
|
||||
class CommentReactionLiteSerializer(BaseSerializer):
|
||||
actor_detail = UserLiteSerializer(read_only=True, source="actor")
|
||||
|
||||
@@ -472,13 +554,12 @@ class CommentReactionSerializer(BaseSerializer):
|
||||
read_only_fields = ["workspace", "project", "comment", "actor"]
|
||||
|
||||
|
||||
class IssueVoteSerializer(BaseSerializer):
|
||||
|
||||
actor_detail = UserLiteSerializer(read_only=True, source="actor")
|
||||
class IssueVoteSerializer(BaseSerializer):
|
||||
|
||||
class Meta:
|
||||
model = IssueVote
|
||||
fields = ["issue", "vote", "workspace", "project", "actor", "actor_detail"]
|
||||
fields = ["issue", "vote", "workspace_id", "project_id", "actor"]
|
||||
read_only_fields = fields
|
||||
|
||||
|
||||
@@ -488,7 +569,7 @@ class IssueCommentSerializer(BaseSerializer):
|
||||
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
||||
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
|
||||
comment_reactions = CommentReactionLiteSerializer(read_only=True, many=True)
|
||||
is_member = serializers.BooleanField(read_only=True)
|
||||
|
||||
|
||||
class Meta:
|
||||
model = IssueComment
|
||||
@@ -501,6 +582,7 @@ class IssueCommentSerializer(BaseSerializer):
|
||||
"updated_by",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"access",
|
||||
]
|
||||
|
||||
|
||||
@@ -541,14 +623,16 @@ class IssueSerializer(BaseSerializer):
|
||||
parent_detail = IssueStateFlatSerializer(read_only=True, source="parent")
|
||||
label_details = LabelSerializer(read_only=True, source="labels", many=True)
|
||||
assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True)
|
||||
related_issues = IssueRelationSerializer(read_only=True, source="issue_relation", many=True)
|
||||
issue_relations = RelatedIssueSerializer(read_only=True, source="issue_related", many=True)
|
||||
# List of issues blocked by this issue
|
||||
blocked_issues = BlockedIssueSerializer(read_only=True, many=True)
|
||||
# List of issues that block this issue
|
||||
blocker_issues = BlockerIssueSerializer(read_only=True, many=True)
|
||||
issue_cycle = IssueCycleDetailSerializer(read_only=True)
|
||||
issue_module = IssueModuleDetailSerializer(read_only=True)
|
||||
issue_link = IssueLinkSerializer(read_only=True, many=True)
|
||||
issue_attachment = IssueAttachmentSerializer(read_only=True, many=True)
|
||||
sub_issues_count = serializers.IntegerField(read_only=True)
|
||||
issue_reactions = IssueReactionSerializer(read_only=True, many=True)
|
||||
issue_reactions = IssueReactionLiteSerializer(read_only=True, many=True)
|
||||
|
||||
class Meta:
|
||||
model = Issue
|
||||
@@ -574,7 +658,7 @@ class IssueLiteSerializer(BaseSerializer):
|
||||
module_id = serializers.UUIDField(read_only=True)
|
||||
attachment_count = serializers.IntegerField(read_only=True)
|
||||
link_count = serializers.IntegerField(read_only=True)
|
||||
issue_reactions = IssueReactionSerializer(read_only=True, many=True)
|
||||
issue_reactions = IssueReactionLiteSerializer(read_only=True, many=True)
|
||||
|
||||
class Meta:
|
||||
model = Issue
|
||||
@@ -592,33 +676,6 @@ class IssueLiteSerializer(BaseSerializer):
|
||||
]
|
||||
|
||||
|
||||
class IssuePublicSerializer(BaseSerializer):
|
||||
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
||||
state_detail = StateLiteSerializer(read_only=True, source="state")
|
||||
reactions = IssueReactionSerializer(read_only=True, many=True, source="issue_reactions")
|
||||
votes = IssueVoteSerializer(read_only=True, many=True)
|
||||
|
||||
class Meta:
|
||||
model = Issue
|
||||
fields = [
|
||||
"id",
|
||||
"name",
|
||||
"description_html",
|
||||
"sequence_id",
|
||||
"state",
|
||||
"state_detail",
|
||||
"project",
|
||||
"project_detail",
|
||||
"workspace",
|
||||
"priority",
|
||||
"target_date",
|
||||
"reactions",
|
||||
"votes",
|
||||
]
|
||||
read_only_fields = fields
|
||||
|
||||
|
||||
|
||||
class IssueSubscriberSerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = IssueSubscriber
|
||||
|
||||
@@ -15,7 +15,6 @@ from plane.db.models import (
|
||||
ProjectIdentifier,
|
||||
ProjectFavorite,
|
||||
ProjectDeployBoard,
|
||||
ProjectPublicMember,
|
||||
)
|
||||
|
||||
|
||||
@@ -113,7 +112,7 @@ class ProjectDetailSerializer(BaseSerializer):
|
||||
|
||||
|
||||
class ProjectMemberSerializer(BaseSerializer):
|
||||
workspace = WorkspaceLiteSerializer(read_only=True)
|
||||
workspace = WorkSpaceSerializer(read_only=True)
|
||||
project = ProjectLiteSerializer(read_only=True)
|
||||
member = UserLiteSerializer(read_only=True)
|
||||
|
||||
@@ -178,17 +177,5 @@ class ProjectDeployBoardSerializer(BaseSerializer):
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
"project", "anchor",
|
||||
]
|
||||
|
||||
|
||||
class ProjectPublicMemberSerializer(BaseSerializer):
|
||||
|
||||
class Meta:
|
||||
model = ProjectPublicMember
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
"project",
|
||||
"member",
|
||||
"project" "anchor",
|
||||
]
|
||||
|
||||
@@ -5,39 +5,10 @@ from rest_framework import serializers
|
||||
from .base import BaseSerializer
|
||||
from .workspace import WorkspaceLiteSerializer
|
||||
from .project import ProjectLiteSerializer
|
||||
from plane.db.models import GlobalView, IssueView, IssueViewFavorite
|
||||
from plane.db.models import IssueView, IssueViewFavorite
|
||||
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):
|
||||
is_favorite = serializers.BooleanField(read_only=True)
|
||||
project_detail = ProjectLiteSerializer(source="project", read_only=True)
|
||||
|
||||
+9
-122
@@ -51,7 +51,6 @@ from plane.api.views import (
|
||||
WorkspaceUserProfileEndpoint,
|
||||
WorkspaceUserProfileIssuesEndpoint,
|
||||
WorkspaceLabelsEndpoint,
|
||||
LeaveWorkspaceEndpoint,
|
||||
## End Workspaces
|
||||
# File Assets
|
||||
FileAssetEndpoint,
|
||||
@@ -69,7 +68,6 @@ from plane.api.views import (
|
||||
UserProjectInvitationsViewset,
|
||||
ProjectIdentifierEndpoint,
|
||||
ProjectFavoritesViewSet,
|
||||
LeaveProjectEndpoint,
|
||||
## End Projects
|
||||
# Issues
|
||||
IssueViewSet,
|
||||
@@ -90,9 +88,8 @@ from plane.api.views import (
|
||||
IssueSubscriberViewSet,
|
||||
IssueCommentPublicViewSet,
|
||||
IssueReactionViewSet,
|
||||
IssueRelationViewSet,
|
||||
CommentReactionViewSet,
|
||||
IssueDraftViewSet,
|
||||
ExportIssuesEndpoint,
|
||||
## End Issues
|
||||
# States
|
||||
StateViewSet,
|
||||
@@ -102,8 +99,6 @@ from plane.api.views import (
|
||||
BulkEstimatePointEndpoint,
|
||||
## End Estimates
|
||||
# Views
|
||||
GlobalViewViewSet,
|
||||
GlobalViewIssuesViewSet,
|
||||
IssueViewViewSet,
|
||||
ViewIssuesEndpoint,
|
||||
IssueViewFavoriteViewSet,
|
||||
@@ -170,22 +165,16 @@ from plane.api.views import (
|
||||
# Notification
|
||||
NotificationViewSet,
|
||||
UnreadNotificationEndpoint,
|
||||
MarkAllReadNotificationViewSet,
|
||||
## End Notification
|
||||
# Public Boards
|
||||
ProjectDeployBoardViewSet,
|
||||
ProjectIssuesPublicEndpoint,
|
||||
ProjectDeployBoardIssuesPublicEndpoint,
|
||||
ProjectDeployBoardPublicSettingsEndpoint,
|
||||
IssueReactionPublicViewSet,
|
||||
CommentReactionPublicViewSet,
|
||||
InboxIssuePublicViewSet,
|
||||
IssueVotePublicViewSet,
|
||||
WorkspaceProjectDeployBoardEndpoint,
|
||||
IssueRetrievePublicEndpoint,
|
||||
## End Public Boards
|
||||
## Exporter
|
||||
ExportIssuesEndpoint,
|
||||
## End Exporter
|
||||
)
|
||||
|
||||
|
||||
@@ -242,11 +231,7 @@ urlpatterns = [
|
||||
UpdateUserTourCompletedEndpoint.as_view(),
|
||||
name="user-tour",
|
||||
),
|
||||
path(
|
||||
"users/workspaces/<str:slug>/activities/",
|
||||
UserActivityEndpoint.as_view(),
|
||||
name="user-activities",
|
||||
),
|
||||
path("users/activities/", UserActivityEndpoint.as_view(), name="user-activities"),
|
||||
# user workspaces
|
||||
path(
|
||||
"users/me/workspaces/",
|
||||
@@ -450,11 +435,6 @@ urlpatterns = [
|
||||
WorkspaceLabelsEndpoint.as_view(),
|
||||
name="workspace-labels",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/members/leave/",
|
||||
LeaveWorkspaceEndpoint.as_view(),
|
||||
name="workspace-labels",
|
||||
),
|
||||
## End Workspaces ##
|
||||
# Projects
|
||||
path(
|
||||
@@ -568,11 +548,6 @@ urlpatterns = [
|
||||
),
|
||||
name="project",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/members/leave/",
|
||||
LeaveProjectEndpoint.as_view(),
|
||||
name="project",
|
||||
),
|
||||
# End Projects
|
||||
# States
|
||||
path(
|
||||
@@ -654,37 +629,6 @@ urlpatterns = [
|
||||
ViewIssuesEndpoint.as_view(),
|
||||
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(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-views/",
|
||||
IssueViewFavoriteViewSet.as_view(
|
||||
@@ -803,6 +747,11 @@ urlpatterns = [
|
||||
),
|
||||
name="project-issue",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/issues/",
|
||||
WorkSpaceIssuesEndpoint.as_view(),
|
||||
name="workspace-issue",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-labels/",
|
||||
LabelViewSet.as_view(
|
||||
@@ -1043,49 +992,6 @@ urlpatterns = [
|
||||
name="project-issue-archive",
|
||||
),
|
||||
## 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
|
||||
path(
|
||||
"workspaces/<str:slug>/file-assets/",
|
||||
@@ -1584,15 +1490,6 @@ urlpatterns = [
|
||||
UnreadNotificationEndpoint.as_view(),
|
||||
name="unread-notifications",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/users/notifications/mark-all-read/",
|
||||
MarkAllReadNotificationViewSet.as_view(
|
||||
{
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="mark-all-read-notifications",
|
||||
),
|
||||
## End Notification
|
||||
# Public Boards
|
||||
path(
|
||||
@@ -1623,14 +1520,9 @@ urlpatterns = [
|
||||
),
|
||||
path(
|
||||
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/",
|
||||
ProjectIssuesPublicEndpoint.as_view(),
|
||||
ProjectDeployBoardIssuesPublicEndpoint.as_view(),
|
||||
name="project-deploy-board",
|
||||
),
|
||||
path(
|
||||
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/<uuid:issue_id>/",
|
||||
IssueRetrievePublicEndpoint.as_view(),
|
||||
name="workspace-project-boards",
|
||||
),
|
||||
path(
|
||||
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/<uuid:issue_id>/comments/",
|
||||
IssueCommentPublicViewSet.as_view(
|
||||
@@ -1722,10 +1614,5 @@ urlpatterns = [
|
||||
),
|
||||
name="issue-vote-project-board",
|
||||
),
|
||||
path(
|
||||
"public/workspaces/<str:slug>/project-boards/",
|
||||
WorkspaceProjectDeployBoardEndpoint.as_view(),
|
||||
name="workspace-project-boards",
|
||||
),
|
||||
## End Public Boards
|
||||
]
|
||||
|
||||
@@ -12,11 +12,10 @@ from .project import (
|
||||
ProjectUserViewsEndpoint,
|
||||
ProjectMemberUserEndpoint,
|
||||
ProjectFavoritesViewSet,
|
||||
ProjectDeployBoardIssuesPublicEndpoint,
|
||||
ProjectDeployBoardViewSet,
|
||||
ProjectDeployBoardPublicSettingsEndpoint,
|
||||
ProjectMemberEndpoint,
|
||||
WorkspaceProjectDeployBoardEndpoint,
|
||||
LeaveProjectEndpoint,
|
||||
)
|
||||
from .user import (
|
||||
UserEndpoint,
|
||||
@@ -53,10 +52,9 @@ from .workspace import (
|
||||
WorkspaceUserProfileIssuesEndpoint,
|
||||
WorkspaceLabelsEndpoint,
|
||||
WorkspaceMembersEndpoint,
|
||||
LeaveWorkspaceEndpoint,
|
||||
)
|
||||
from .state import StateViewSet
|
||||
from .view import GlobalViewViewSet, GlobalViewIssuesViewSet, IssueViewViewSet, ViewIssuesEndpoint, IssueViewFavoriteViewSet
|
||||
from .view import IssueViewViewSet, ViewIssuesEndpoint, IssueViewFavoriteViewSet
|
||||
from .cycle import (
|
||||
CycleViewSet,
|
||||
CycleIssueViewSet,
|
||||
@@ -86,10 +84,6 @@ from .issue import (
|
||||
IssueReactionPublicViewSet,
|
||||
CommentReactionPublicViewSet,
|
||||
IssueVotePublicViewSet,
|
||||
IssueRelationViewSet,
|
||||
IssueRetrievePublicEndpoint,
|
||||
ProjectIssuesPublicEndpoint,
|
||||
IssueDraftViewSet,
|
||||
)
|
||||
|
||||
from .auth_extended import (
|
||||
@@ -167,6 +161,8 @@ from .analytic import (
|
||||
DefaultAnalyticsEndpoint,
|
||||
)
|
||||
|
||||
from .notification import NotificationViewSet, UnreadNotificationEndpoint, MarkAllReadNotificationViewSet
|
||||
from .notification import NotificationViewSet, UnreadNotificationEndpoint
|
||||
|
||||
from .exporter import ExportIssuesEndpoint
|
||||
from .exporter import (
|
||||
ExportIssuesEndpoint,
|
||||
)
|
||||
@@ -18,21 +18,10 @@ class FileAssetEndpoint(BaseAPIView):
|
||||
"""
|
||||
|
||||
def get(self, request, workspace_id, asset_key):
|
||||
try:
|
||||
asset_key = str(workspace_id) + "/" + asset_key
|
||||
files = FileAsset.objects.filter(asset=asset_key)
|
||||
if files.exists():
|
||||
serializer = FileAssetSerializer(files, context={"request": request}, many=True)
|
||||
return Response({"data": serializer.data, "status": True}, status=status.HTTP_200_OK)
|
||||
else:
|
||||
return Response({"error": "Asset key does not exist", "status": False}, status=status.HTTP_200_OK)
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
asset_key = str(workspace_id) + "/" + asset_key
|
||||
files = FileAsset.objects.filter(asset=asset_key)
|
||||
serializer = FileAssetSerializer(files, context={"request": request}, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
def post(self, request, slug):
|
||||
try:
|
||||
@@ -79,16 +68,11 @@ class UserAssetsEndpoint(BaseAPIView):
|
||||
def get(self, request, asset_key):
|
||||
try:
|
||||
files = FileAsset.objects.filter(asset=asset_key, created_by=request.user)
|
||||
if files.exists():
|
||||
serializer = FileAssetSerializer(files, context={"request": request})
|
||||
return Response({"data": serializer.data, "status": True}, status=status.HTTP_200_OK)
|
||||
else:
|
||||
return Response({"error": "Asset key does not exist", "status": False}, status=status.HTTP_200_OK)
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
serializer = FileAssetSerializer(files, context={"request": request})
|
||||
return Response(serializer.data)
|
||||
except FileAsset.DoesNotExist:
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
{"error": "File Asset does not exist"}, status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
|
||||
def post(self, request):
|
||||
|
||||
@@ -1,41 +1,24 @@
|
||||
# Python imports
|
||||
import zoneinfo
|
||||
|
||||
# Django imports
|
||||
from django.urls import resolve
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
# Third part imports
|
||||
|
||||
# Third part imports
|
||||
from rest_framework import status
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
from rest_framework.exceptions import APIException
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.filters import SearchFilter
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.exceptions import NotFound
|
||||
from sentry_sdk import capture_exception
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
|
||||
# Module imports
|
||||
from plane.db.models import Workspace, Project
|
||||
from plane.utils.paginator import BasePaginator
|
||||
|
||||
|
||||
class TimezoneMixin:
|
||||
"""
|
||||
This enables timezone conversion according
|
||||
to the user set timezone
|
||||
"""
|
||||
def initial(self, request, *args, **kwargs):
|
||||
super().initial(request, *args, **kwargs)
|
||||
if request.user.is_authenticated:
|
||||
timezone.activate(zoneinfo.ZoneInfo(request.user.user_timezone))
|
||||
else:
|
||||
timezone.deactivate()
|
||||
|
||||
|
||||
|
||||
|
||||
class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator):
|
||||
class BaseViewSet(ModelViewSet, BasePaginator):
|
||||
|
||||
model = None
|
||||
|
||||
@@ -84,7 +67,7 @@ class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator):
|
||||
return self.kwargs.get("pk", None)
|
||||
|
||||
|
||||
class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
|
||||
class BaseAPIView(APIView, BasePaginator):
|
||||
|
||||
permission_classes = [
|
||||
IsAuthenticated,
|
||||
|
||||
@@ -80,7 +80,6 @@ class CycleViewSet(BaseViewSet):
|
||||
issue_id=str(self.kwargs.get("pk", None)),
|
||||
project_id=str(self.kwargs.get("project_id", None)),
|
||||
current_instance=None,
|
||||
epoch = int(timezone.now().timestamp())
|
||||
)
|
||||
|
||||
return super().perform_destroy(instance)
|
||||
@@ -192,10 +191,11 @@ class CycleViewSet(BaseViewSet):
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
)
|
||||
.annotate(display_name=F("assignees__display_name"))
|
||||
.annotate(first_name=F("assignees__first_name"))
|
||||
.annotate(last_name=F("assignees__last_name"))
|
||||
.annotate(assignee_id=F("assignees__id"))
|
||||
.annotate(avatar=F("assignees__avatar"))
|
||||
.values("display_name", "assignee_id", "avatar")
|
||||
.values("first_name", "last_name", "assignee_id", "avatar")
|
||||
.annotate(total_issues=Count("assignee_id"))
|
||||
.annotate(
|
||||
completed_issues=Count(
|
||||
@@ -209,7 +209,7 @@ class CycleViewSet(BaseViewSet):
|
||||
filter=Q(completed_at__isnull=True),
|
||||
)
|
||||
)
|
||||
.order_by("display_name")
|
||||
.order_by("first_name", "last_name")
|
||||
)
|
||||
|
||||
label_distribution = (
|
||||
@@ -334,21 +334,13 @@ class CycleViewSet(BaseViewSet):
|
||||
workspace__slug=slug, project_id=project_id, pk=pk
|
||||
)
|
||||
|
||||
request_data = request.data
|
||||
|
||||
if cycle.end_date is not None and cycle.end_date < timezone.now().date():
|
||||
if "sort_order" in request_data:
|
||||
# Can only change sort order
|
||||
request_data = {
|
||||
"sort_order": request_data.get("sort_order", cycle.sort_order)
|
||||
}
|
||||
else:
|
||||
return Response(
|
||||
{
|
||||
"error": "The Cycle has already been completed so it cannot be edited"
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
return Response(
|
||||
{
|
||||
"error": "The Cycle has already been completed so it cannot be edited"
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
serializer = CycleWriteSerializer(cycle, data=request.data, partial=True)
|
||||
if serializer.is_valid():
|
||||
@@ -382,9 +374,7 @@ class CycleViewSet(BaseViewSet):
|
||||
.annotate(assignee_id=F("assignees__id"))
|
||||
.annotate(avatar=F("assignees__avatar"))
|
||||
.annotate(display_name=F("assignees__display_name"))
|
||||
.values(
|
||||
"first_name", "last_name", "assignee_id", "avatar", "display_name"
|
||||
)
|
||||
.values("first_name", "last_name", "assignee_id", "avatar", "display_name")
|
||||
.annotate(total_issues=Count("assignee_id"))
|
||||
.annotate(
|
||||
completed_issues=Count(
|
||||
@@ -488,7 +478,6 @@ class CycleIssueViewSet(BaseViewSet):
|
||||
issue_id=str(self.kwargs.get("pk", None)),
|
||||
project_id=str(self.kwargs.get("project_id", None)),
|
||||
current_instance=None,
|
||||
epoch = int(timezone.now().timestamp())
|
||||
)
|
||||
return super().perform_destroy(instance)
|
||||
|
||||
@@ -519,7 +508,6 @@ class CycleIssueViewSet(BaseViewSet):
|
||||
try:
|
||||
order_by = request.GET.get("order_by", "created_at")
|
||||
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")
|
||||
issues = (
|
||||
Issue.issue_objects.filter(issue_cycle__cycle_id=cycle_id)
|
||||
@@ -558,15 +546,9 @@ class CycleIssueViewSet(BaseViewSet):
|
||||
|
||||
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:
|
||||
return Response(
|
||||
group_results(issues_data, group_by, sub_group_by),
|
||||
group_results(issues_data, group_by),
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
@@ -664,7 +646,6 @@ class CycleIssueViewSet(BaseViewSet):
|
||||
),
|
||||
}
|
||||
),
|
||||
epoch = int(timezone.now().timestamp())
|
||||
)
|
||||
|
||||
# Return all Cycle Issues
|
||||
@@ -729,6 +710,7 @@ class CycleDateCheckEndpoint(BaseAPIView):
|
||||
|
||||
|
||||
class CycleFavoriteViewSet(BaseViewSet):
|
||||
|
||||
serializer_class = CycleFavoriteSerializer
|
||||
model = CycleFavorite
|
||||
|
||||
|
||||
@@ -41,9 +41,9 @@ class GPTIntegrationEndpoint(BaseAPIView):
|
||||
final_text = task + "\n" + prompt
|
||||
|
||||
openai.api_key = settings.OPENAI_API_KEY
|
||||
response = openai.ChatCompletion.create(
|
||||
response = openai.Completion.create(
|
||||
model=settings.GPT_ENGINE,
|
||||
messages=[{"role": "user", "content": final_text}],
|
||||
prompt=final_text,
|
||||
temperature=0.7,
|
||||
max_tokens=1024,
|
||||
)
|
||||
@@ -51,7 +51,7 @@ class GPTIntegrationEndpoint(BaseAPIView):
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
project = Project.objects.get(pk=project_id)
|
||||
|
||||
text = response.choices[0].message.content.strip()
|
||||
text = response.choices[0].text.strip()
|
||||
text_html = text.replace("\n", "<br/>")
|
||||
return Response(
|
||||
{
|
||||
|
||||
@@ -213,7 +213,6 @@ class InboxIssueViewSet(BaseViewSet):
|
||||
issue_id=str(issue.id),
|
||||
project_id=str(project_id),
|
||||
current_instance=None,
|
||||
epoch = int(timezone.now().timestamp())
|
||||
)
|
||||
# create an inbox issue
|
||||
InboxIssue.objects.create(
|
||||
@@ -278,7 +277,6 @@ class InboxIssueViewSet(BaseViewSet):
|
||||
IssueSerializer(current_instance).data,
|
||||
cls=DjangoJSONEncoder,
|
||||
),
|
||||
epoch = int(timezone.now().timestamp())
|
||||
)
|
||||
issue_serializer.save()
|
||||
else:
|
||||
@@ -520,7 +518,6 @@ class InboxIssuePublicViewSet(BaseViewSet):
|
||||
issue_id=str(issue.id),
|
||||
project_id=str(project_id),
|
||||
current_instance=None,
|
||||
epoch = int(timezone.now().timestamp())
|
||||
)
|
||||
# create an inbox issue
|
||||
InboxIssue.objects.create(
|
||||
@@ -585,7 +582,6 @@ class InboxIssuePublicViewSet(BaseViewSet):
|
||||
IssueSerializer(current_instance).data,
|
||||
cls=DjangoJSONEncoder,
|
||||
),
|
||||
epoch = int(timezone.now().timestamp())
|
||||
)
|
||||
issue_serializer.save()
|
||||
return Response(issue_serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,6 @@
|
||||
import json
|
||||
|
||||
# Django Imports
|
||||
from django.utils import timezone
|
||||
from django.db import IntegrityError
|
||||
from django.db.models import Prefetch, F, OuterRef, Func, Exists, Count, Q
|
||||
from django.core import serializers
|
||||
@@ -130,7 +129,6 @@ class ModuleViewSet(BaseViewSet):
|
||||
issue_id=str(self.kwargs.get("pk", None)),
|
||||
project_id=str(self.kwargs.get("project_id", None)),
|
||||
current_instance=None,
|
||||
epoch = int(timezone.now().timestamp())
|
||||
)
|
||||
|
||||
return super().perform_destroy(instance)
|
||||
@@ -279,7 +277,6 @@ class ModuleIssueViewSet(BaseViewSet):
|
||||
issue_id=str(self.kwargs.get("pk", None)),
|
||||
project_id=str(self.kwargs.get("project_id", None)),
|
||||
current_instance=None,
|
||||
epoch = int(timezone.now().timestamp())
|
||||
)
|
||||
return super().perform_destroy(instance)
|
||||
|
||||
@@ -311,7 +308,6 @@ class ModuleIssueViewSet(BaseViewSet):
|
||||
try:
|
||||
order_by = request.GET.get("order_by", "created_at")
|
||||
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")
|
||||
issues = (
|
||||
Issue.issue_objects.filter(issue_module__module_id=module_id)
|
||||
@@ -350,15 +346,9 @@ class ModuleIssueViewSet(BaseViewSet):
|
||||
|
||||
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:
|
||||
return Response(
|
||||
group_results(issues_data, group_by, sub_group_by),
|
||||
group_results(issues_data, group_by),
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
@@ -447,7 +437,6 @@ class ModuleIssueViewSet(BaseViewSet):
|
||||
),
|
||||
}
|
||||
),
|
||||
epoch = int(timezone.now().timestamp())
|
||||
)
|
||||
|
||||
return Response(
|
||||
|
||||
@@ -10,13 +10,7 @@ from plane.utils.paginator import BasePaginator
|
||||
|
||||
# Module imports
|
||||
from .base import BaseViewSet, BaseAPIView
|
||||
from plane.db.models import (
|
||||
Notification,
|
||||
IssueAssignee,
|
||||
IssueSubscriber,
|
||||
Issue,
|
||||
WorkspaceMember,
|
||||
)
|
||||
from plane.db.models import Notification, IssueAssignee, IssueSubscriber, Issue, WorkspaceMember
|
||||
from plane.api.serializers import NotificationSerializer
|
||||
|
||||
|
||||
@@ -89,17 +83,13 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
|
||||
|
||||
# Created issues
|
||||
if type == "created":
|
||||
if WorkspaceMember.objects.filter(
|
||||
workspace__slug=slug, member=request.user, role__lt=15
|
||||
).exists():
|
||||
if WorkspaceMember.objects.filter(workspace__slug=slug, member=request.user, role__lt=15).exists():
|
||||
notifications = Notification.objects.none()
|
||||
else:
|
||||
issue_ids = Issue.objects.filter(
|
||||
workspace__slug=slug, created_by=request.user
|
||||
).values_list("pk", flat=True)
|
||||
notifications = notifications.filter(
|
||||
entity_identifier__in=issue_ids
|
||||
)
|
||||
notifications = notifications.filter(entity_identifier__in=issue_ids)
|
||||
|
||||
# Pagination
|
||||
if request.GET.get("per_page", False) and request.GET.get("cursor", False):
|
||||
@@ -284,80 +274,3 @@ class UnreadNotificationEndpoint(BaseAPIView):
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
class MarkAllReadNotificationViewSet(BaseViewSet):
|
||||
def create(self, request, slug):
|
||||
try:
|
||||
snoozed = request.data.get("snoozed", False)
|
||||
archived = request.data.get("archived", False)
|
||||
type = request.data.get("type", "all")
|
||||
|
||||
notifications = (
|
||||
Notification.objects.filter(
|
||||
workspace__slug=slug,
|
||||
receiver_id=request.user.id,
|
||||
read_at__isnull=True,
|
||||
)
|
||||
.select_related("workspace", "project", "triggered_by", "receiver")
|
||||
.order_by("snoozed_till", "-created_at")
|
||||
)
|
||||
|
||||
# Filter for snoozed notifications
|
||||
if snoozed:
|
||||
notifications = notifications.filter(
|
||||
Q(snoozed_till__lt=timezone.now()) | Q(snoozed_till__isnull=False)
|
||||
)
|
||||
else:
|
||||
notifications = notifications.filter(
|
||||
Q(snoozed_till__gte=timezone.now()) | Q(snoozed_till__isnull=True),
|
||||
)
|
||||
|
||||
# Filter for archived or unarchive
|
||||
if archived:
|
||||
notifications = notifications.filter(archived_at__isnull=False)
|
||||
else:
|
||||
notifications = notifications.filter(archived_at__isnull=True)
|
||||
|
||||
# Subscribed issues
|
||||
if type == "watching":
|
||||
issue_ids = IssueSubscriber.objects.filter(
|
||||
workspace__slug=slug, subscriber_id=request.user.id
|
||||
).values_list("issue_id", flat=True)
|
||||
notifications = notifications.filter(entity_identifier__in=issue_ids)
|
||||
|
||||
# Assigned Issues
|
||||
if type == "assigned":
|
||||
issue_ids = IssueAssignee.objects.filter(
|
||||
workspace__slug=slug, assignee_id=request.user.id
|
||||
).values_list("issue_id", flat=True)
|
||||
notifications = notifications.filter(entity_identifier__in=issue_ids)
|
||||
|
||||
# Created issues
|
||||
if type == "created":
|
||||
if WorkspaceMember.objects.filter(
|
||||
workspace__slug=slug, member=request.user, role__lt=15
|
||||
).exists():
|
||||
notifications = Notification.objects.none()
|
||||
else:
|
||||
issue_ids = Issue.objects.filter(
|
||||
workspace__slug=slug, created_by=request.user
|
||||
).values_list("pk", flat=True)
|
||||
notifications = notifications.filter(
|
||||
entity_identifier__in=issue_ids
|
||||
)
|
||||
|
||||
updated_notifications = []
|
||||
for notification in notifications:
|
||||
notification.read_at = timezone.now()
|
||||
updated_notifications.append(notification)
|
||||
Notification.objects.bulk_update(
|
||||
updated_notifications, ["read_at"], batch_size=100
|
||||
)
|
||||
return Response({"message": "Successful"}, status=status.HTTP_200_OK)
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@@ -11,8 +11,14 @@ from django.db.models import (
|
||||
OuterRef,
|
||||
Func,
|
||||
F,
|
||||
Max,
|
||||
CharField,
|
||||
Func,
|
||||
Subquery,
|
||||
Prefetch,
|
||||
When,
|
||||
Case,
|
||||
Value,
|
||||
)
|
||||
from django.core.validators import validate_email
|
||||
from django.conf import settings
|
||||
@@ -41,7 +47,6 @@ from plane.api.permissions import (
|
||||
ProjectBasePermission,
|
||||
ProjectEntityPermission,
|
||||
ProjectMemberPermission,
|
||||
ProjectLitePermission,
|
||||
)
|
||||
|
||||
from plane.db.models import (
|
||||
@@ -66,9 +71,16 @@ from plane.db.models import (
|
||||
ModuleMember,
|
||||
Inbox,
|
||||
ProjectDeployBoard,
|
||||
Issue,
|
||||
IssueReaction,
|
||||
IssueLink,
|
||||
IssueAttachment,
|
||||
Label,
|
||||
)
|
||||
|
||||
from plane.bgtasks.project_invitation_task import project_invitation
|
||||
from plane.utils.grouper import group_results
|
||||
from plane.utils.issue_filters import issue_filters
|
||||
|
||||
|
||||
class ProjectViewSet(BaseViewSet):
|
||||
@@ -275,10 +287,7 @@ class ProjectViewSet(BaseViewSet):
|
||||
)
|
||||
|
||||
data = serializer.data
|
||||
# Additional fields of the member
|
||||
data["sort_order"] = project_member.sort_order
|
||||
data["member_role"] = project_member.role
|
||||
data["is_member"] = True
|
||||
return Response(data, status=status.HTTP_201_CREATED)
|
||||
return Response(
|
||||
serializer.errors,
|
||||
@@ -482,7 +491,7 @@ class UserProjectInvitationsViewset(BaseViewSet):
|
||||
# Delete joined project invites
|
||||
project_invitations.delete()
|
||||
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
return Response(status=status.HTTP_200_OK)
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
return Response(
|
||||
@@ -617,7 +626,7 @@ class ProjectMemberViewSet(BaseViewSet):
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
except ProjectMember.DoesNotExist:
|
||||
return Response(
|
||||
{"error": "Project Member does not exist"}, status=status.HTTP_400_BAD_REQUEST
|
||||
{"error": "Project Member does not exist"}, status=status.HTTP_400
|
||||
)
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
@@ -924,7 +933,8 @@ class ProjectUserViewsEndpoint(BaseAPIView):
|
||||
|
||||
project_member.save()
|
||||
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
return Response(status=status.HTTP_200_OK)
|
||||
|
||||
except Project.DoesNotExist:
|
||||
return Response(
|
||||
{"error": "The requested resource does not exists"},
|
||||
@@ -1130,78 +1140,145 @@ class ProjectDeployBoardPublicSettingsEndpoint(BaseAPIView):
|
||||
)
|
||||
|
||||
|
||||
class WorkspaceProjectDeployBoardEndpoint(BaseAPIView):
|
||||
class ProjectDeployBoardIssuesPublicEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
AllowAny,
|
||||
]
|
||||
|
||||
def get(self, request, slug):
|
||||
def get(self, request, slug, project_id):
|
||||
try:
|
||||
projects = (
|
||||
Project.objects.filter(workspace__slug=slug)
|
||||
.annotate(
|
||||
is_public=Exists(
|
||||
ProjectDeployBoard.objects.filter(
|
||||
workspace__slug=slug, project_id=OuterRef("pk")
|
||||
)
|
||||
project_deploy_board = ProjectDeployBoard.objects.get(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
|
||||
filters = issue_filters(request.query_params, "GET")
|
||||
|
||||
# Custom ordering for priority and state
|
||||
priority_order = ["urgent", "high", "medium", "low", None]
|
||||
state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
|
||||
|
||||
order_by_param = request.GET.get("order_by", "-created_at")
|
||||
|
||||
issue_queryset = (
|
||||
Issue.issue_objects.annotate(
|
||||
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
.filter(project_id=project_id)
|
||||
.filter(workspace__slug=slug)
|
||||
.select_related("project", "workspace", "state", "parent")
|
||||
.prefetch_related("assignees", "labels")
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"issue_reactions",
|
||||
queryset=IssueReaction.objects.select_related("actor"),
|
||||
)
|
||||
)
|
||||
.filter(is_public=True)
|
||||
).values(
|
||||
"id",
|
||||
"identifier",
|
||||
"name",
|
||||
"description",
|
||||
"emoji",
|
||||
"icon_prop",
|
||||
"cover_image",
|
||||
)
|
||||
|
||||
return Response(projects, status=status.HTTP_200_OK)
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
class LeaveProjectEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
ProjectLitePermission,
|
||||
]
|
||||
|
||||
def delete(self, request, slug, project_id):
|
||||
try:
|
||||
project_member = ProjectMember.objects.get(
|
||||
workspace__slug=slug,
|
||||
member=request.user,
|
||||
project_id=project_id,
|
||||
)
|
||||
|
||||
# Only Admin case
|
||||
if (
|
||||
project_member.role == 20
|
||||
and ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
role=20,
|
||||
project_id=project_id,
|
||||
).count()
|
||||
== 1
|
||||
):
|
||||
return Response(
|
||||
{
|
||||
"error": "You cannot leave the project since you are the only admin of the project you should delete the project"
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
.filter(**filters)
|
||||
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||
.annotate(module_id=F("issue_module__module_id"))
|
||||
.annotate(
|
||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
# Delete the member from workspace
|
||||
project_member.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
except ProjectMember.DoesNotExist:
|
||||
.annotate(
|
||||
attachment_count=IssueAttachment.objects.filter(
|
||||
issue=OuterRef("id")
|
||||
)
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
)
|
||||
|
||||
# Priority Ordering
|
||||
if order_by_param == "priority" or order_by_param == "-priority":
|
||||
priority_order = (
|
||||
priority_order
|
||||
if order_by_param == "priority"
|
||||
else priority_order[::-1]
|
||||
)
|
||||
issue_queryset = issue_queryset.annotate(
|
||||
priority_order=Case(
|
||||
*[
|
||||
When(priority=p, then=Value(i))
|
||||
for i, p in enumerate(priority_order)
|
||||
],
|
||||
output_field=CharField(),
|
||||
)
|
||||
).order_by("priority_order")
|
||||
|
||||
# State Ordering
|
||||
elif order_by_param in [
|
||||
"state__name",
|
||||
"state__group",
|
||||
"-state__name",
|
||||
"-state__group",
|
||||
]:
|
||||
state_order = (
|
||||
state_order
|
||||
if order_by_param in ["state__name", "state__group"]
|
||||
else state_order[::-1]
|
||||
)
|
||||
issue_queryset = issue_queryset.annotate(
|
||||
state_order=Case(
|
||||
*[
|
||||
When(state__group=state_group, then=Value(i))
|
||||
for i, state_group in enumerate(state_order)
|
||||
],
|
||||
default=Value(len(state_order)),
|
||||
output_field=CharField(),
|
||||
)
|
||||
).order_by("state_order")
|
||||
# assignee and label ordering
|
||||
elif order_by_param in [
|
||||
"labels__name",
|
||||
"-labels__name",
|
||||
"assignees__first_name",
|
||||
"-assignees__first_name",
|
||||
]:
|
||||
issue_queryset = issue_queryset.annotate(
|
||||
max_values=Max(
|
||||
order_by_param[1::]
|
||||
if order_by_param.startswith("-")
|
||||
else order_by_param
|
||||
)
|
||||
).order_by(
|
||||
"-max_values" if order_by_param.startswith("-") else "max_values"
|
||||
)
|
||||
else:
|
||||
issue_queryset = issue_queryset.order_by(order_by_param)
|
||||
|
||||
issues = IssueLiteSerializer(issue_queryset, many=True).data
|
||||
|
||||
states = State.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
).values("name", "group", "color", "id")
|
||||
|
||||
labels = Label.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
).values("id", "name", "color", "parent")
|
||||
|
||||
## Grouping the results
|
||||
group_by = request.GET.get("group_by", False)
|
||||
if group_by:
|
||||
issues = group_results(issues, group_by)
|
||||
|
||||
return Response(
|
||||
{"error": "Workspace member does not exists"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
{
|
||||
"issues": issues,
|
||||
"states": states,
|
||||
"labels": labels,
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
except ProjectDeployBoard.DoesNotExist:
|
||||
return Response(
|
||||
{"error": "Board does not exists"}, status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
|
||||
@@ -220,7 +220,7 @@ class IssueSearchEndpoint(BaseAPIView):
|
||||
query = request.query_params.get("search", False)
|
||||
workspace_search = request.query_params.get("workspace_search", "false")
|
||||
parent = request.query_params.get("parent", "false")
|
||||
issue_relation = request.query_params.get("issue_relation", "false")
|
||||
blocker_blocked_by = request.query_params.get("blocker_blocked_by", "false")
|
||||
cycle = request.query_params.get("cycle", "false")
|
||||
module = request.query_params.get("module", "false")
|
||||
sub_issue = request.query_params.get("sub_issue", "false")
|
||||
@@ -247,12 +247,12 @@ class IssueSearchEndpoint(BaseAPIView):
|
||||
"parent_id", flat=True
|
||||
)
|
||||
)
|
||||
if issue_relation == "true" and issue_id:
|
||||
if blocker_blocked_by == "true" and issue_id:
|
||||
issue = Issue.issue_objects.get(pk=issue_id)
|
||||
issues = issues.filter(
|
||||
~Q(pk=issue_id),
|
||||
~Q(issue_related__issue=issue),
|
||||
~Q(issue_relation__related_issue=issue),
|
||||
~Q(blocked_issues__block=issue),
|
||||
~Q(blocker_issues__blocked_by=issue),
|
||||
)
|
||||
if sub_issue == "true" and issue_id:
|
||||
issue = Issue.issue_objects.get(pk=issue_id)
|
||||
|
||||
@@ -137,11 +137,11 @@ class UpdateUserTourCompletedEndpoint(BaseAPIView):
|
||||
|
||||
|
||||
class UserActivityEndpoint(BaseAPIView, BasePaginator):
|
||||
def get(self, request, slug):
|
||||
def get(self, request):
|
||||
try:
|
||||
queryset = IssueActivity.objects.filter(
|
||||
actor=request.user, workspace__slug=slug
|
||||
).select_related("actor", "workspace", "issue", "project")
|
||||
queryset = IssueActivity.objects.filter(actor=request.user).select_related(
|
||||
"actor", "workspace", "issue", "project"
|
||||
)
|
||||
|
||||
return self.paginate(
|
||||
request=request,
|
||||
|
||||
@@ -1,18 +1,4 @@
|
||||
# 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.models import Prefetch, OuterRef, Exists
|
||||
|
||||
@@ -24,192 +10,18 @@ from sentry_sdk import capture_exception
|
||||
# Module imports
|
||||
from . import BaseViewSet, BaseAPIView
|
||||
from plane.api.serializers import (
|
||||
GlobalViewSerializer,
|
||||
IssueViewSerializer,
|
||||
IssueLiteSerializer,
|
||||
IssueViewFavoriteSerializer,
|
||||
)
|
||||
from plane.api.permissions import WorkspaceEntityPermission, ProjectEntityPermission
|
||||
from plane.api.permissions import ProjectEntityPermission
|
||||
from plane.db.models import (
|
||||
Workspace,
|
||||
GlobalView,
|
||||
IssueView,
|
||||
Issue,
|
||||
IssueViewFavorite,
|
||||
IssueReaction,
|
||||
IssueLink,
|
||||
IssueAttachment,
|
||||
)
|
||||
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("-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):
|
||||
|
||||
@@ -116,7 +116,7 @@ class WorkSpaceViewSet(BaseViewSet):
|
||||
)
|
||||
|
||||
issue_count = (
|
||||
Issue.issue_objects.filter(workspace=OuterRef("id"))
|
||||
Issue.objects.filter(workspace=OuterRef("id"))
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
@@ -203,7 +203,7 @@ class UserWorkSpacesEndpoint(BaseAPIView):
|
||||
)
|
||||
|
||||
issue_count = (
|
||||
Issue.issue_objects.filter(workspace=OuterRef("id"))
|
||||
Issue.objects.filter(workspace=OuterRef("id"))
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
@@ -532,7 +532,7 @@ class UserWorkspaceInvitationsEndpoint(BaseViewSet):
|
||||
# Delete joined workspace invites
|
||||
workspace_invitations.delete()
|
||||
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
return Response(status=status.HTTP_200_OK)
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
return Response(
|
||||
@@ -846,7 +846,7 @@ class WorkspaceMemberUserViewsEndpoint(BaseAPIView):
|
||||
workspace_member.view_props = request.data.get("view_props", {})
|
||||
workspace_member.save()
|
||||
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
return Response(status=status.HTTP_200_OK)
|
||||
except WorkspaceMember.DoesNotExist:
|
||||
return Response(
|
||||
{"error": "User not a member of workspace"},
|
||||
@@ -1072,10 +1072,10 @@ class WorkspaceUserProfileStatsEndpoint(BaseAPIView):
|
||||
.order_by("state_group")
|
||||
)
|
||||
|
||||
priority_order = ["urgent", "high", "medium", "low", "none"]
|
||||
priority_order = ["urgent", "high", "medium", "low", None]
|
||||
|
||||
priority_distribution = (
|
||||
Issue.issue_objects.filter(
|
||||
Issue.objects.filter(
|
||||
workspace__slug=slug,
|
||||
assignees__in=[user_id],
|
||||
project__project_projectmember__member=request.user,
|
||||
@@ -1100,6 +1100,7 @@ class WorkspaceUserProfileStatsEndpoint(BaseAPIView):
|
||||
created_issues = (
|
||||
Issue.issue_objects.filter(
|
||||
workspace__slug=slug,
|
||||
assignees__in=[user_id],
|
||||
project__project_projectmember__member=request.user,
|
||||
created_by_id=user_id,
|
||||
)
|
||||
@@ -1197,7 +1198,6 @@ class WorkspaceUserActivityEndpoint(BaseAPIView):
|
||||
projects = request.query_params.getlist("project", [])
|
||||
|
||||
queryset = IssueActivity.objects.filter(
|
||||
~Q(field__in=["comment", "vote", "reaction"]),
|
||||
workspace__slug=slug,
|
||||
project__project_projectmember__member=request.user,
|
||||
actor=user_id,
|
||||
@@ -1473,44 +1473,3 @@ class WorkspaceMembersEndpoint(BaseAPIView):
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
class LeaveWorkspaceEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
WorkspaceEntityPermission,
|
||||
]
|
||||
|
||||
def delete(self, request, slug):
|
||||
try:
|
||||
workspace_member = WorkspaceMember.objects.get(
|
||||
workspace__slug=slug, member=request.user
|
||||
)
|
||||
|
||||
# Only Admin case
|
||||
if (
|
||||
workspace_member.role == 20
|
||||
and WorkspaceMember.objects.filter(
|
||||
workspace__slug=slug, role=20
|
||||
).count()
|
||||
== 1
|
||||
):
|
||||
return Response(
|
||||
{
|
||||
"error": "You cannot leave the workspace since you are the only admin of the workspace you should delete the workspace"
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
# Delete the member from workspace
|
||||
workspace_member.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
except WorkspaceMember.DoesNotExist:
|
||||
return Response(
|
||||
{"error": "Workspace member does not exists"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@@ -4,7 +4,6 @@ import io
|
||||
import json
|
||||
import boto3
|
||||
import zipfile
|
||||
from urllib.parse import urlparse, urlunparse
|
||||
|
||||
# Django imports
|
||||
from django.conf import settings
|
||||
@@ -24,11 +23,9 @@ def dateTimeConverter(time):
|
||||
if time:
|
||||
return time.strftime("%a, %d %b %Y %I:%M:%S %Z%z")
|
||||
|
||||
|
||||
def dateConverter(time):
|
||||
if time:
|
||||
return time.strftime("%a, %d %b %Y")
|
||||
|
||||
return time.strftime("%a, %d %b %Y")
|
||||
|
||||
def create_csv_file(data):
|
||||
csv_buffer = io.StringIO()
|
||||
@@ -69,53 +66,28 @@ def create_zip_file(files):
|
||||
|
||||
|
||||
def upload_to_s3(zip_file, workspace_id, token_id, slug):
|
||||
s3 = boto3.client(
|
||||
"s3",
|
||||
region_name=settings.AWS_REGION,
|
||||
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
|
||||
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
|
||||
config=Config(signature_version="s3v4"),
|
||||
)
|
||||
file_name = f"{workspace_id}/export-{slug}-{token_id[:6]}-{timezone.now()}.zip"
|
||||
|
||||
s3.upload_fileobj(
|
||||
zip_file,
|
||||
settings.AWS_S3_BUCKET_NAME,
|
||||
file_name,
|
||||
ExtraArgs={"ACL": "public-read", "ContentType": "application/zip"},
|
||||
)
|
||||
|
||||
expires_in = 7 * 24 * 60 * 60
|
||||
|
||||
if settings.DOCKERIZED and settings.USE_MINIO:
|
||||
s3 = boto3.client(
|
||||
"s3",
|
||||
endpoint_url=settings.AWS_S3_ENDPOINT_URL,
|
||||
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
|
||||
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
|
||||
config=Config(signature_version="s3v4"),
|
||||
)
|
||||
s3.upload_fileobj(
|
||||
zip_file,
|
||||
settings.AWS_STORAGE_BUCKET_NAME,
|
||||
file_name,
|
||||
ExtraArgs={"ACL": "public-read", "ContentType": "application/zip"},
|
||||
)
|
||||
presigned_url = s3.generate_presigned_url(
|
||||
"get_object",
|
||||
Params={"Bucket": settings.AWS_STORAGE_BUCKET_NAME, "Key": file_name},
|
||||
ExpiresIn=expires_in,
|
||||
)
|
||||
# Create the new url with updated domain and protocol
|
||||
presigned_url = presigned_url.replace(
|
||||
"http://plane-minio:9000/uploads/",
|
||||
f"{settings.AWS_S3_URL_PROTOCOL}//{settings.AWS_S3_CUSTOM_DOMAIN}/",
|
||||
)
|
||||
else:
|
||||
s3 = boto3.client(
|
||||
"s3",
|
||||
region_name=settings.AWS_REGION,
|
||||
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
|
||||
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
|
||||
config=Config(signature_version="s3v4"),
|
||||
)
|
||||
s3.upload_fileobj(
|
||||
zip_file,
|
||||
settings.AWS_S3_BUCKET_NAME,
|
||||
file_name,
|
||||
ExtraArgs={"ACL": "public-read", "ContentType": "application/zip"},
|
||||
)
|
||||
|
||||
presigned_url = s3.generate_presigned_url(
|
||||
"get_object",
|
||||
Params={"Bucket": settings.AWS_S3_BUCKET_NAME, "Key": file_name},
|
||||
ExpiresIn=expires_in,
|
||||
)
|
||||
presigned_url = s3.generate_presigned_url(
|
||||
"get_object",
|
||||
Params={"Bucket": settings.AWS_S3_BUCKET_NAME, "Key": file_name},
|
||||
ExpiresIn=expires_in,
|
||||
)
|
||||
|
||||
exporter_instance = ExporterHistory.objects.get(token=token_id)
|
||||
|
||||
@@ -126,7 +98,7 @@ def upload_to_s3(zip_file, workspace_id, token_id, slug):
|
||||
else:
|
||||
exporter_instance.status = "failed"
|
||||
|
||||
exporter_instance.save(update_fields=["status", "url", "key"])
|
||||
exporter_instance.save(update_fields=["status", "url","key"])
|
||||
|
||||
|
||||
def generate_table_row(issue):
|
||||
@@ -173,7 +145,7 @@ def generate_json_row(issue):
|
||||
else "",
|
||||
"Labels": issue["labels__name"],
|
||||
"Cycle Name": issue["issue_cycle__cycle__name"],
|
||||
"Cycle Start Date": dateConverter(issue["issue_cycle__cycle__start_date"]),
|
||||
"Cycle Start Date": dateConverter(issue["issue_cycle__cycle__start_date"]),
|
||||
"Cycle End Date": dateConverter(issue["issue_cycle__cycle__end_date"]),
|
||||
"Module Name": issue["issue_module__module__name"],
|
||||
"Module Start Date": dateConverter(issue["issue_module__module__start_date"]),
|
||||
@@ -270,9 +242,7 @@ def issue_export_task(provider, workspace_id, project_ids, token_id, multiple, s
|
||||
workspace_issues = (
|
||||
(
|
||||
Issue.objects.filter(
|
||||
workspace__id=workspace_id,
|
||||
project_id__in=project_ids,
|
||||
project__project_projectmember__member=exporter_instance.initiated_by_id,
|
||||
workspace__id=workspace_id, project_id__in=project_ids
|
||||
)
|
||||
.select_related("project", "workspace", "state", "parent", "created_by")
|
||||
.prefetch_related(
|
||||
@@ -305,7 +275,7 @@ def issue_export_task(provider, workspace_id, project_ids, token_id, multiple, s
|
||||
"labels__name",
|
||||
)
|
||||
)
|
||||
.order_by("project__identifier", "sequence_id")
|
||||
.order_by("project__identifier","sequence_id")
|
||||
.distinct()
|
||||
)
|
||||
# CSV header
|
||||
@@ -368,6 +338,7 @@ def issue_export_task(provider, workspace_id, project_ids, token_id, multiple, s
|
||||
exporter_instance.status = "failed"
|
||||
exporter_instance.reason = str(e)
|
||||
exporter_instance.save(update_fields=["status", "reason"])
|
||||
|
||||
# Print logs if in DEBUG mode
|
||||
if settings.DEBUG:
|
||||
print(e)
|
||||
|
||||
@@ -21,29 +21,18 @@ def delete_old_s3_link():
|
||||
expired_exporter_history = ExporterHistory.objects.filter(
|
||||
Q(url__isnull=False) & Q(created_at__lte=timezone.now() - timedelta(days=8))
|
||||
).values_list("key", "id")
|
||||
if settings.DOCKERIZED and settings.USE_MINIO:
|
||||
s3 = boto3.client(
|
||||
"s3",
|
||||
endpoint_url=settings.AWS_S3_ENDPOINT_URL,
|
||||
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
|
||||
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
|
||||
config=Config(signature_version="s3v4"),
|
||||
)
|
||||
else:
|
||||
s3 = boto3.client(
|
||||
"s3",
|
||||
region_name="ap-south-1",
|
||||
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
|
||||
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
|
||||
config=Config(signature_version="s3v4"),
|
||||
)
|
||||
|
||||
s3 = boto3.client(
|
||||
"s3",
|
||||
region_name="ap-south-1",
|
||||
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
|
||||
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
|
||||
config=Config(signature_version="s3v4"),
|
||||
)
|
||||
|
||||
for file_name, exporter_id in expired_exporter_history:
|
||||
# Delete object from S3
|
||||
if file_name:
|
||||
if settings.DOCKERIZED and settings.USE_MINIO:
|
||||
s3.delete_object(Bucket=settings.AWS_STORAGE_BUCKET_NAME, Key=file_name)
|
||||
else:
|
||||
s3.delete_object(Bucket=settings.AWS_S3_BUCKET_NAME, Key=file_name)
|
||||
s3.delete_object(Bucket=settings.AWS_S3_BUCKET_NAME, Key=file_name)
|
||||
|
||||
ExporterHistory.objects.filter(id=exporter_id).update(url=None)
|
||||
|
||||
@@ -24,9 +24,6 @@ from plane.db.models import (
|
||||
IssueSubscriber,
|
||||
Notification,
|
||||
IssueAssignee,
|
||||
IssueReaction,
|
||||
CommentReaction,
|
||||
IssueComment,
|
||||
)
|
||||
from plane.api.serializers import IssueActivitySerializer
|
||||
|
||||
@@ -39,7 +36,6 @@ def track_name(
|
||||
project,
|
||||
actor,
|
||||
issue_activities,
|
||||
epoch
|
||||
):
|
||||
if current_instance.get("name") != requested_data.get("name"):
|
||||
issue_activities.append(
|
||||
@@ -53,7 +49,6 @@ def track_name(
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"updated the name to {requested_data.get('name')}",
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -66,7 +61,6 @@ def track_parent(
|
||||
project,
|
||||
actor,
|
||||
issue_activities,
|
||||
epoch
|
||||
):
|
||||
if current_instance.get("parent") != requested_data.get("parent"):
|
||||
if requested_data.get("parent") == None:
|
||||
@@ -84,7 +78,6 @@ def track_parent(
|
||||
comment=f"updated the parent issue to None",
|
||||
old_identifier=old_parent.id,
|
||||
new_identifier=None,
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
else:
|
||||
@@ -105,7 +98,6 @@ def track_parent(
|
||||
comment=f"updated the parent issue to {new_parent.name}",
|
||||
old_identifier=old_parent.id if old_parent is not None else None,
|
||||
new_identifier=new_parent.id,
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -118,7 +110,6 @@ def track_priority(
|
||||
project,
|
||||
actor,
|
||||
issue_activities,
|
||||
epoch
|
||||
):
|
||||
if current_instance.get("priority") != requested_data.get("priority"):
|
||||
if requested_data.get("priority") == None:
|
||||
@@ -133,7 +124,6 @@ def track_priority(
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"updated the priority to None",
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
else:
|
||||
@@ -148,7 +138,6 @@ def track_priority(
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"updated the priority to {requested_data.get('priority')}",
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -161,7 +150,6 @@ def track_state(
|
||||
project,
|
||||
actor,
|
||||
issue_activities,
|
||||
epoch
|
||||
):
|
||||
if current_instance.get("state") != requested_data.get("state"):
|
||||
new_state = State.objects.get(pk=requested_data.get("state", None))
|
||||
@@ -180,7 +168,6 @@ def track_state(
|
||||
comment=f"updated the state to {new_state.name}",
|
||||
old_identifier=old_state.id,
|
||||
new_identifier=new_state.id,
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -193,7 +180,6 @@ def track_description(
|
||||
project,
|
||||
actor,
|
||||
issue_activities,
|
||||
epoch
|
||||
):
|
||||
if current_instance.get("description_html") != requested_data.get(
|
||||
"description_html"
|
||||
@@ -214,7 +200,6 @@ def track_description(
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"updated the description to {requested_data.get('description_html')}",
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -227,7 +212,6 @@ def track_target_date(
|
||||
project,
|
||||
actor,
|
||||
issue_activities,
|
||||
epoch
|
||||
):
|
||||
if current_instance.get("target_date") != requested_data.get("target_date"):
|
||||
if requested_data.get("target_date") == None:
|
||||
@@ -242,7 +226,6 @@ def track_target_date(
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"updated the target date to None",
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
else:
|
||||
@@ -257,7 +240,6 @@ def track_target_date(
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"updated the target date to {requested_data.get('target_date')}",
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -270,7 +252,6 @@ def track_start_date(
|
||||
project,
|
||||
actor,
|
||||
issue_activities,
|
||||
epoch
|
||||
):
|
||||
if current_instance.get("start_date") != requested_data.get("start_date"):
|
||||
if requested_data.get("start_date") == None:
|
||||
@@ -285,7 +266,6 @@ def track_start_date(
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"updated the start date to None",
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
else:
|
||||
@@ -300,7 +280,6 @@ def track_start_date(
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"updated the start date to {requested_data.get('start_date')}",
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -313,7 +292,6 @@ def track_labels(
|
||||
project,
|
||||
actor,
|
||||
issue_activities,
|
||||
epoch
|
||||
):
|
||||
# Label Addition
|
||||
if len(requested_data.get("labels_list")) > len(current_instance.get("labels")):
|
||||
@@ -333,7 +311,6 @@ def track_labels(
|
||||
comment=f"added label {label.name}",
|
||||
new_identifier=label.id,
|
||||
old_identifier=None,
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -355,7 +332,6 @@ def track_labels(
|
||||
comment=f"removed label {label.name}",
|
||||
old_identifier=label.id,
|
||||
new_identifier=None,
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -368,7 +344,6 @@ def track_assignees(
|
||||
project,
|
||||
actor,
|
||||
issue_activities,
|
||||
epoch
|
||||
):
|
||||
# Assignee Addition
|
||||
if len(requested_data.get("assignees_list")) > len(
|
||||
@@ -389,7 +364,6 @@ def track_assignees(
|
||||
workspace=project.workspace,
|
||||
comment=f"added assignee {assignee.display_name}",
|
||||
new_identifier=assignee.id,
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -412,29 +386,151 @@ def track_assignees(
|
||||
workspace=project.workspace,
|
||||
comment=f"removed assignee {assignee.display_name}",
|
||||
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(
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||
):
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"created the issue",
|
||||
verb="created",
|
||||
actor=actor,
|
||||
epoch=epoch,
|
||||
)
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"created the issue",
|
||||
verb="created",
|
||||
actor=actor,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def track_estimate_points(
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||
):
|
||||
if current_instance.get("estimate_point") != requested_data.get("estimate_point"):
|
||||
if requested_data.get("estimate_point") == None:
|
||||
@@ -449,7 +545,6 @@ def track_estimate_points(
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"updated the estimate point to None",
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
else:
|
||||
@@ -464,13 +559,12 @@ def track_estimate_points(
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"updated the estimate point to {requested_data.get('estimate_point')}",
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def track_archive_at(
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||
):
|
||||
if requested_data.get("archived_at") is None:
|
||||
issue_activities.append(
|
||||
@@ -484,7 +578,6 @@ def track_archive_at(
|
||||
field="archived_at",
|
||||
old_value="archive",
|
||||
new_value="restore",
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
else:
|
||||
@@ -499,13 +592,12 @@ def track_archive_at(
|
||||
field="archived_at",
|
||||
old_value=None,
|
||||
new_value="archive",
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def track_closed_to(
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||
):
|
||||
if requested_data.get("closed_to") is not None:
|
||||
updated_state = State.objects.get(
|
||||
@@ -525,24 +617,25 @@ def track_closed_to(
|
||||
comment=f"Plane updated the state to {updated_state.name}",
|
||||
old_identifier=None,
|
||||
new_identifier=updated_state.id,
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def update_issue_activity(
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||
):
|
||||
ISSUE_ACTIVITY_MAPPER = {
|
||||
"name": track_name,
|
||||
"parent": track_parent,
|
||||
"priority": track_priority,
|
||||
"state": track_state,
|
||||
"description_html": track_description,
|
||||
"description": track_description,
|
||||
"target_date": track_target_date,
|
||||
"start_date": track_start_date,
|
||||
"labels_list": track_labels,
|
||||
"assignees_list": track_assignees,
|
||||
"blocks_list": track_blocks,
|
||||
"blockers_list": track_blockings,
|
||||
"estimate_point": track_estimate_points,
|
||||
"archived_at": track_archive_at,
|
||||
"closed_to": track_closed_to,
|
||||
@@ -563,12 +656,11 @@ def update_issue_activity(
|
||||
project,
|
||||
actor,
|
||||
issue_activities,
|
||||
epoch
|
||||
)
|
||||
|
||||
|
||||
def delete_issue_activity(
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||
):
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
@@ -578,13 +670,12 @@ def delete_issue_activity(
|
||||
verb="deleted",
|
||||
actor=actor,
|
||||
field="issue",
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def create_comment_activity(
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||
):
|
||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||
current_instance = (
|
||||
@@ -603,13 +694,12 @@ def create_comment_activity(
|
||||
new_value=requested_data.get("comment_html", ""),
|
||||
new_identifier=requested_data.get("id", None),
|
||||
issue_comment_id=requested_data.get("id", None),
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def update_comment_activity(
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||
):
|
||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||
current_instance = (
|
||||
@@ -631,13 +721,12 @@ def update_comment_activity(
|
||||
new_value=requested_data.get("comment_html", ""),
|
||||
new_identifier=current_instance.get("id", None),
|
||||
issue_comment_id=current_instance.get("id", None),
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def delete_comment_activity(
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||
):
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
@@ -648,13 +737,12 @@ def delete_comment_activity(
|
||||
verb="deleted",
|
||||
actor=actor,
|
||||
field="comment",
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def create_cycle_issue_activity(
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||
):
|
||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||
current_instance = (
|
||||
@@ -686,7 +774,6 @@ def create_cycle_issue_activity(
|
||||
comment=f"updated cycle from {old_cycle.name} to {new_cycle.name}",
|
||||
old_identifier=old_cycle.id,
|
||||
new_identifier=new_cycle.id,
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -707,13 +794,12 @@ def create_cycle_issue_activity(
|
||||
workspace=project.workspace,
|
||||
comment=f"added cycle {cycle.name}",
|
||||
new_identifier=cycle.id,
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def delete_cycle_issue_activity(
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||
):
|
||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||
current_instance = (
|
||||
@@ -737,13 +823,12 @@ def delete_cycle_issue_activity(
|
||||
workspace=project.workspace,
|
||||
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,
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def create_module_issue_activity(
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||
):
|
||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||
current_instance = (
|
||||
@@ -775,7 +860,6 @@ def create_module_issue_activity(
|
||||
comment=f"updated module from {old_module.name} to {new_module.name}",
|
||||
old_identifier=old_module.id,
|
||||
new_identifier=new_module.id,
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -795,13 +879,12 @@ def create_module_issue_activity(
|
||||
workspace=project.workspace,
|
||||
comment=f"added module {module.name}",
|
||||
new_identifier=module.id,
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def delete_module_issue_activity(
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||
):
|
||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||
current_instance = (
|
||||
@@ -825,13 +908,12 @@ def delete_module_issue_activity(
|
||||
workspace=project.workspace,
|
||||
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,
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def create_link_activity(
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||
):
|
||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||
current_instance = (
|
||||
@@ -849,13 +931,12 @@ def create_link_activity(
|
||||
field="link",
|
||||
new_value=requested_data.get("url", ""),
|
||||
new_identifier=requested_data.get("id", None),
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def update_link_activity(
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||
):
|
||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||
current_instance = (
|
||||
@@ -876,13 +957,12 @@ def update_link_activity(
|
||||
old_identifier=current_instance.get("id"),
|
||||
new_value=requested_data.get("url", ""),
|
||||
new_identifier=current_instance.get("id", None),
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def delete_link_activity(
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||
):
|
||||
|
||||
current_instance = (
|
||||
@@ -899,14 +979,13 @@ def delete_link_activity(
|
||||
actor=actor,
|
||||
field="link",
|
||||
old_value=current_instance.get("url", ""),
|
||||
new_value="",
|
||||
epoch=epoch,
|
||||
new_value=""
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def create_attachment_activity(
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||
):
|
||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||
current_instance = (
|
||||
@@ -924,13 +1003,12 @@ def create_attachment_activity(
|
||||
field="attachment",
|
||||
new_value=current_instance.get("asset", ""),
|
||||
new_identifier=current_instance.get("id", None),
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def delete_attachment_activity(
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||
):
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
@@ -941,319 +1019,9 @@ def delete_attachment_activity(
|
||||
verb="deleted",
|
||||
actor=actor,
|
||||
field="attachment",
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
def create_issue_reaction_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
|
||||
if requested_data and requested_data.get("reaction") is not None:
|
||||
issue_reaction = IssueReaction.objects.filter(reaction=requested_data.get("reaction"), project=project, actor=actor).values_list('id', flat=True).first()
|
||||
if issue_reaction is not None:
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="created",
|
||||
old_value=None,
|
||||
new_value=requested_data.get("reaction"),
|
||||
field="reaction",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment="added the reaction",
|
||||
old_identifier=None,
|
||||
new_identifier=issue_reaction,
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def delete_issue_reaction_activity(
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||
):
|
||||
current_instance = (
|
||||
json.loads(current_instance) if current_instance is not None else None
|
||||
)
|
||||
if current_instance and current_instance.get("reaction") is not None:
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="deleted",
|
||||
old_value=current_instance.get("reaction"),
|
||||
new_value=None,
|
||||
field="reaction",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment="removed the reaction",
|
||||
old_identifier=current_instance.get("identifier"),
|
||||
new_identifier=None,
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def create_comment_reaction_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
|
||||
if requested_data and requested_data.get("reaction") is not None:
|
||||
comment_reaction_id, comment_id = CommentReaction.objects.filter(reaction=requested_data.get("reaction"), project=project, actor=actor).values_list('id', 'comment__id').first()
|
||||
comment = IssueComment.objects.get(pk=comment_id,project=project)
|
||||
if comment is not None and comment_reaction_id is not None and comment_id is not None:
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=comment.issue_id,
|
||||
actor=actor,
|
||||
verb="created",
|
||||
old_value=None,
|
||||
new_value=requested_data.get("reaction"),
|
||||
field="reaction",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment="added the reaction",
|
||||
old_identifier=None,
|
||||
new_identifier=comment_reaction_id,
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def delete_comment_reaction_activity(
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||
):
|
||||
current_instance = (
|
||||
json.loads(current_instance) if current_instance is not None else None
|
||||
)
|
||||
if current_instance and current_instance.get("reaction") is not None:
|
||||
issue_id = IssueComment.objects.filter(pk=current_instance.get("comment_id"), project=project).values_list('issue_id', flat=True).first()
|
||||
if issue_id is not None:
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="deleted",
|
||||
old_value=current_instance.get("reaction"),
|
||||
new_value=None,
|
||||
field="reaction",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment="removed the reaction",
|
||||
old_identifier=current_instance.get("identifier"),
|
||||
new_identifier=None,
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def create_issue_vote_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
|
||||
if requested_data and requested_data.get("vote") is not None:
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="created",
|
||||
old_value=None,
|
||||
new_value=requested_data.get("vote"),
|
||||
field="vote",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment="added the vote",
|
||||
old_identifier=None,
|
||||
new_identifier=None,
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def delete_issue_vote_activity(
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||
):
|
||||
current_instance = (
|
||||
json.loads(current_instance) if current_instance is not None else None
|
||||
)
|
||||
if current_instance and current_instance.get("vote") is not None:
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="deleted",
|
||||
old_value=current_instance.get("vote"),
|
||||
new_value=None,
|
||||
field="vote",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment="removed the vote",
|
||||
old_identifier=current_instance.get("identifier"),
|
||||
new_identifier=None,
|
||||
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
|
||||
@shared_task
|
||||
@@ -1264,7 +1032,6 @@ def issue_activity(
|
||||
issue_id,
|
||||
actor_id,
|
||||
project_id,
|
||||
epoch,
|
||||
subscriber=True,
|
||||
):
|
||||
try:
|
||||
@@ -1278,12 +1045,6 @@ def issue_activity(
|
||||
"cycle.activity.deleted",
|
||||
"module.activity.created",
|
||||
"module.activity.deleted",
|
||||
"issue_reaction.activity.created",
|
||||
"issue_reaction.activity.deleted",
|
||||
"comment_reaction.activity.created",
|
||||
"comment_reaction.activity.deleted",
|
||||
"issue_vote.activity.created",
|
||||
"issue_vote.activity.deleted",
|
||||
]:
|
||||
issue = Issue.objects.filter(pk=issue_id).first()
|
||||
|
||||
@@ -1319,17 +1080,6 @@ def issue_activity(
|
||||
"link.activity.deleted": delete_link_activity,
|
||||
"attachment.activity.created": create_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.deleted": delete_issue_reaction_activity,
|
||||
"comment_reaction.activity.created": create_comment_reaction_activity,
|
||||
"comment_reaction.activity.deleted": delete_comment_reaction_activity,
|
||||
"issue_vote.activity.created": create_issue_vote_activity,
|
||||
"issue_vote.activity.deleted": delete_issue_vote_activity,
|
||||
"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)
|
||||
@@ -1341,7 +1091,6 @@ def issue_activity(
|
||||
project,
|
||||
actor,
|
||||
issue_activities,
|
||||
epoch,
|
||||
)
|
||||
|
||||
# Save all the values to database
|
||||
@@ -1370,12 +1119,6 @@ def issue_activity(
|
||||
"cycle.activity.deleted",
|
||||
"module.activity.created",
|
||||
"module.activity.deleted",
|
||||
"issue_reaction.activity.created",
|
||||
"issue_reaction.activity.deleted",
|
||||
"comment_reaction.activity.created",
|
||||
"comment_reaction.activity.deleted",
|
||||
"issue_vote.activity.created",
|
||||
"issue_vote.activity.deleted",
|
||||
]:
|
||||
# Create Notifications
|
||||
bulk_notifications = []
|
||||
|
||||
@@ -32,7 +32,7 @@ def archive_old_issues():
|
||||
archive_in = project.archive_in
|
||||
|
||||
# Get all the issues whose updated_at in less that the archive_in month
|
||||
issues = Issue.issue_objects.filter(
|
||||
issues = Issue.objects.filter(
|
||||
Q(
|
||||
project=project_id,
|
||||
archived_at__isnull=True,
|
||||
@@ -64,23 +64,21 @@ def archive_old_issues():
|
||||
issues_to_update.append(issue)
|
||||
|
||||
# Bulk Update the issues and log the activity
|
||||
if issues_to_update:
|
||||
updated_issues = Issue.objects.bulk_update(
|
||||
issues_to_update, ["archived_at"], batch_size=100
|
||||
Issue.objects.bulk_update(
|
||||
issues_to_update, ["archived_at"], batch_size=100
|
||||
)
|
||||
[
|
||||
issue_activity.delay(
|
||||
type="issue.activity.updated",
|
||||
requested_data=json.dumps({"archived_at": str(issue.archived_at)}),
|
||||
actor_id=str(project.created_by_id),
|
||||
issue_id=issue.id,
|
||||
project_id=project_id,
|
||||
current_instance=None,
|
||||
subscriber=False,
|
||||
)
|
||||
[
|
||||
issue_activity.delay(
|
||||
type="issue.activity.updated",
|
||||
requested_data=json.dumps({"archived_at": str(issue.archived_at)}),
|
||||
actor_id=str(project.created_by_id),
|
||||
issue_id=issue.id,
|
||||
project_id=project_id,
|
||||
current_instance=None,
|
||||
subscriber=False,
|
||||
epoch = int(timezone.now().timestamp())
|
||||
)
|
||||
for issue in updated_issues
|
||||
]
|
||||
for issue in issues_to_update
|
||||
]
|
||||
return
|
||||
except Exception as e:
|
||||
if settings.DEBUG:
|
||||
@@ -101,7 +99,7 @@ def close_old_issues():
|
||||
close_in = project.close_in
|
||||
|
||||
# Get all the issues whose updated_at in less that the close_in month
|
||||
issues = Issue.issue_objects.filter(
|
||||
issues = Issue.objects.filter(
|
||||
Q(
|
||||
project=project_id,
|
||||
archived_at__isnull=True,
|
||||
@@ -138,21 +136,19 @@ def close_old_issues():
|
||||
issues_to_update.append(issue)
|
||||
|
||||
# Bulk Update the issues and log the activity
|
||||
if issues_to_update:
|
||||
updated_issues = Issue.objects.bulk_update(issues_to_update, ["state"], batch_size=100)
|
||||
[
|
||||
issue_activity.delay(
|
||||
type="issue.activity.updated",
|
||||
requested_data=json.dumps({"closed_to": str(issue.state_id)}),
|
||||
actor_id=str(project.created_by_id),
|
||||
issue_id=issue.id,
|
||||
project_id=project_id,
|
||||
current_instance=None,
|
||||
subscriber=False,
|
||||
epoch = int(timezone.now().timestamp())
|
||||
)
|
||||
for issue in updated_issues
|
||||
]
|
||||
Issue.objects.bulk_update(issues_to_update, ["state"], batch_size=100)
|
||||
[
|
||||
issue_activity.delay(
|
||||
type="issue.activity.updated",
|
||||
requested_data=json.dumps({"closed_to": str(issue.state_id)}),
|
||||
actor_id=str(project.created_by_id),
|
||||
issue_id=issue.id,
|
||||
project_id=project_id,
|
||||
current_instance=None,
|
||||
subscriber=False,
|
||||
)
|
||||
for issue in issues_to_update
|
||||
]
|
||||
return
|
||||
except Exception as e:
|
||||
if settings.DEBUG:
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,84 +0,0 @@
|
||||
# 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.all():
|
||||
if obj.priority is 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),
|
||||
]
|
||||
@@ -1,138 +0,0 @@
|
||||
# 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",None),
|
||||
"attachment_count": old_props.get("properties", {}).get("attachment_count", None),
|
||||
"created_on": old_props.get("properties", {}).get("created_on", None),
|
||||
"due_date": old_props.get("properties", {}).get("due_date", None),
|
||||
"estimate": old_props.get("properties", {}).get("estimate", None),
|
||||
"key": old_props.get("properties", {}).get("key", None),
|
||||
"labels": old_props.get("properties", {}).get("labels", None),
|
||||
"link": old_props.get("properties", {}).get("link", None),
|
||||
"priority": old_props.get("properties", {}).get("priority", None),
|
||||
"start_date": old_props.get("properties", {}).get("start_date", None),
|
||||
"state": old_props.get("properties", {}).get("state", None),
|
||||
"sub_issue_count": old_props.get("properties", {}).get("sub_issue_count", None),
|
||||
"updated_on": old_props.get("properties", {}).get("updated_on", None),
|
||||
},
|
||||
}
|
||||
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),
|
||||
]
|
||||
@@ -1,24 +0,0 @@
|
||||
# Generated by Django 4.2.3 on 2023-09-15 06:55
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def update_issue_activity(apps, schema_editor):
|
||||
IssueActivityModel = apps.get_model("db", "IssueActivity")
|
||||
updated_issue_activity = []
|
||||
for obj in IssueActivityModel.objects.all():
|
||||
if obj.field == "blocks":
|
||||
obj.field = "blocked_by"
|
||||
updated_issue_activity.append(obj)
|
||||
IssueActivityModel.objects.bulk_update(updated_issue_activity, ["field"], batch_size=100)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('db', '0044_auto_20230913_0709'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(update_issue_activity),
|
||||
]
|
||||
@@ -1,53 +0,0 @@
|
||||
# Generated by Django 4.2.3 on 2023-09-19 14:21
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
|
||||
|
||||
def update_epoch(apps, schema_editor):
|
||||
IssueActivity = apps.get_model('db', 'IssueActivity')
|
||||
updated_issue_activity = []
|
||||
for obj in IssueActivity.objects.all():
|
||||
obj.epoch = int(obj.created_at.timestamp())
|
||||
updated_issue_activity.append(obj)
|
||||
IssueActivity.objects.bulk_update(updated_issue_activity, ["epoch"], batch_size=100)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('db', '0045_auto_20230915_0655'),
|
||||
]
|
||||
|
||||
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)),
|
||||
('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='issueactivity',
|
||||
name='epoch',
|
||||
field=models.FloatField(null=True),
|
||||
),
|
||||
migrations.RunPython(update_epoch),
|
||||
]
|
||||
@@ -19,7 +19,6 @@ from .project import (
|
||||
ProjectIdentifier,
|
||||
ProjectFavorite,
|
||||
ProjectDeployBoard,
|
||||
ProjectPublicMember,
|
||||
)
|
||||
|
||||
from .issue import (
|
||||
@@ -32,7 +31,6 @@ from .issue import (
|
||||
IssueAssignee,
|
||||
Label,
|
||||
IssueBlocker,
|
||||
IssueRelation,
|
||||
IssueLink,
|
||||
IssueSequence,
|
||||
IssueAttachment,
|
||||
@@ -50,7 +48,7 @@ from .state import State
|
||||
|
||||
from .cycle import Cycle, CycleIssue, CycleFavorite
|
||||
|
||||
from .view import GlobalView, IssueView, IssueViewFavorite
|
||||
from .view import IssueView, IssueViewFavorite
|
||||
|
||||
from .module import Module, ModuleMember, ModuleIssue, ModuleLink, ModuleFavorite
|
||||
|
||||
|
||||
@@ -29,7 +29,6 @@ class IssueManager(models.Manager):
|
||||
| models.Q(issue_inbox__isnull=True)
|
||||
)
|
||||
.exclude(archived_at__isnull=False)
|
||||
.exclude(is_draft=True)
|
||||
)
|
||||
|
||||
|
||||
@@ -39,7 +38,6 @@ class Issue(ProjectBaseModel):
|
||||
("high", "High"),
|
||||
("medium", "Medium"),
|
||||
("low", "Low"),
|
||||
("none", "None")
|
||||
)
|
||||
parent = models.ForeignKey(
|
||||
"self",
|
||||
@@ -66,7 +64,8 @@ class Issue(ProjectBaseModel):
|
||||
max_length=30,
|
||||
choices=PRIORITY_CHOICES,
|
||||
verbose_name="Issue Priority",
|
||||
default="none",
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
start_date = models.DateField(null=True, blank=True)
|
||||
target_date = models.DateField(null=True, blank=True)
|
||||
@@ -84,7 +83,6 @@ class Issue(ProjectBaseModel):
|
||||
sort_order = models.FloatField(default=65535)
|
||||
completed_at = models.DateTimeField(null=True)
|
||||
archived_at = models.DateField(null=True)
|
||||
is_draft = models.BooleanField(default=False)
|
||||
|
||||
objects = models.Manager()
|
||||
issue_objects = IssueManager()
|
||||
@@ -180,37 +178,6 @@ class IssueBlocker(ProjectBaseModel):
|
||||
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):
|
||||
issue = models.ForeignKey(
|
||||
Issue, on_delete=models.CASCADE, related_name="issue_assignee"
|
||||
@@ -309,7 +276,6 @@ class IssueActivity(ProjectBaseModel):
|
||||
)
|
||||
old_identifier = models.UUIDField(null=True)
|
||||
new_identifier = models.UUIDField(null=True)
|
||||
epoch = models.FloatField(null=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Issue Activity"
|
||||
@@ -327,7 +293,7 @@ class IssueComment(ProjectBaseModel):
|
||||
comment_json = models.JSONField(blank=True, default=dict)
|
||||
comment_html = models.TextField(blank=True, default="<p></p>")
|
||||
attachments = ArrayField(models.URLField(), size=10, blank=True, default=list)
|
||||
issue = models.ForeignKey(Issue, on_delete=models.CASCADE, related_name="issue_comments")
|
||||
issue = models.ForeignKey(Issue, on_delete=models.CASCADE)
|
||||
# System can also create comment
|
||||
actor = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
@@ -510,12 +476,10 @@ class IssueVote(ProjectBaseModel):
|
||||
choices=(
|
||||
(-1, "DOWNVOTE"),
|
||||
(1, "UPVOTE"),
|
||||
),
|
||||
default=1,
|
||||
)
|
||||
)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["issue", "actor",]
|
||||
unique_together = ["issue", "actor"]
|
||||
verbose_name = "Issue Vote"
|
||||
verbose_name_plural = "Issue Votes"
|
||||
db_table = "issue_votes"
|
||||
|
||||
@@ -25,26 +25,13 @@ ROLE_CHOICES = (
|
||||
|
||||
def get_default_props():
|
||||
return {
|
||||
"filters": {
|
||||
"priority": None,
|
||||
"state": None,
|
||||
"state_group": None,
|
||||
"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": "",
|
||||
},
|
||||
"filters": {"type": None},
|
||||
"orderBy": "-created_at",
|
||||
"collapsed": True,
|
||||
"issueView": "list",
|
||||
"filterIssue": None,
|
||||
"groupByProperty": None,
|
||||
"showEmptyGroups": True,
|
||||
}
|
||||
|
||||
|
||||
@@ -267,18 +254,3 @@ class ProjectDeployBoard(ProjectBaseModel):
|
||||
def __str__(self):
|
||||
"""Return project and anchor"""
|
||||
return f"{self.anchor} <{self.project.name}>"
|
||||
|
||||
|
||||
class ProjectPublicMember(ProjectBaseModel):
|
||||
member = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="public_project_members",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["project", "member"]
|
||||
verbose_name = "Project Public Member"
|
||||
verbose_name_plural = "Project Public Members"
|
||||
db_table = "project_public_members"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import uuid
|
||||
import string
|
||||
import random
|
||||
import pytz
|
||||
|
||||
# Django imports
|
||||
from django.db import models
|
||||
@@ -10,6 +9,9 @@ from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
from django.contrib.auth.models import AbstractBaseUser, UserManager, PermissionsMixin
|
||||
from django.utils import timezone
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.html import strip_tags
|
||||
from django.conf import settings
|
||||
|
||||
# Third party imports
|
||||
@@ -64,8 +66,7 @@ class User(AbstractBaseUser, PermissionsMixin):
|
||||
billing_address = models.JSONField(null=True)
|
||||
has_billing_address = models.BooleanField(default=False)
|
||||
|
||||
USER_TIMEZONE_CHOICES = tuple(zip(pytz.all_timezones, pytz.all_timezones))
|
||||
user_timezone = models.CharField(max_length=255, default="UTC", choices=USER_TIMEZONE_CHOICES)
|
||||
user_timezone = models.CharField(max_length=255, default="Asia/Kolkata")
|
||||
|
||||
last_active = models.DateTimeField(default=timezone.now, null=True)
|
||||
last_login_time = models.DateTimeField(null=True)
|
||||
|
||||
@@ -3,30 +3,7 @@ from django.db import models
|
||||
from django.conf import settings
|
||||
|
||||
# Module import
|
||||
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)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Global View"
|
||||
verbose_name_plural = "Global Views"
|
||||
db_table = "global_views"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def __str__(self):
|
||||
"""Return name of the View"""
|
||||
return f"{self.name} <{self.workspace.name}>"
|
||||
from . import ProjectBaseModel
|
||||
|
||||
|
||||
class IssueView(ProjectBaseModel):
|
||||
|
||||
@@ -16,41 +16,26 @@ ROLE_CHOICES = (
|
||||
|
||||
def get_default_props():
|
||||
return {
|
||||
"filters": {
|
||||
"priority": None,
|
||||
"state": None,
|
||||
"state_group": None,
|
||||
"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": {
|
||||
"filters": {"type": None},
|
||||
"groupByProperty": None,
|
||||
"issueView": "list",
|
||||
"orderBy": "-created_at",
|
||||
"properties": {
|
||||
"assignee": True,
|
||||
"attachment_count": True,
|
||||
"created_on": True,
|
||||
"due_date": True,
|
||||
"estimate": True,
|
||||
"key": True,
|
||||
"labels": True,
|
||||
"link": True,
|
||||
"priority": True,
|
||||
"start_date": True,
|
||||
"state": True,
|
||||
"sub_issue_count": True,
|
||||
"attachment_count": True,
|
||||
"link": True,
|
||||
"estimate": True,
|
||||
"created_on": True,
|
||||
"updated_on": True,
|
||||
}
|
||||
"start_date": True,
|
||||
},
|
||||
"showEmptyGroups": True,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ MIDDLEWARE = [
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
"crum.CurrentRequestUserMiddleware",
|
||||
"django.middleware.gzip.GZipMiddleware",
|
||||
]
|
||||
]
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
"DEFAULT_AUTHENTICATION_CLASSES": (
|
||||
@@ -161,7 +161,7 @@ MEDIA_URL = "/media/"
|
||||
|
||||
LANGUAGE_CODE = "en-us"
|
||||
|
||||
TIME_ZONE = "UTC"
|
||||
TIME_ZONE = "Asia/Kolkata"
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
"""Production settings and globals."""
|
||||
from urllib.parse import urlparse
|
||||
import ssl
|
||||
import certifi
|
||||
|
||||
import dj_database_url
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import sentry_sdk
|
||||
from sentry_sdk.integrations.django import DjangoIntegration
|
||||
@@ -89,89 +91,112 @@ if bool(os.environ.get("SENTRY_DSN", False)):
|
||||
profiles_sample_rate=1.0,
|
||||
)
|
||||
|
||||
# The AWS region to connect to.
|
||||
AWS_REGION = os.environ.get("AWS_REGION", "")
|
||||
if DOCKERIZED and USE_MINIO:
|
||||
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
|
||||
|
||||
# The AWS access key to use.
|
||||
AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID", "")
|
||||
# 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}:"
|
||||
else:
|
||||
# The AWS region to connect to.
|
||||
AWS_REGION = os.environ.get("AWS_REGION", "")
|
||||
|
||||
# The AWS secret access key to use.
|
||||
AWS_SECRET_ACCESS_KEY = os.environ.get("AWS_SECRET_ACCESS_KEY", "")
|
||||
# The AWS access key to use.
|
||||
AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID", "")
|
||||
|
||||
# The optional AWS session token to use.
|
||||
# AWS_SESSION_TOKEN = ""
|
||||
# The AWS secret access key to use.
|
||||
AWS_SECRET_ACCESS_KEY = os.environ.get("AWS_SECRET_ACCESS_KEY", "")
|
||||
|
||||
# The name of the bucket to store files in.
|
||||
AWS_S3_BUCKET_NAME = os.environ.get("AWS_S3_BUCKET_NAME")
|
||||
# The optional AWS session token to use.
|
||||
# AWS_SESSION_TOKEN = ""
|
||||
|
||||
# How to construct S3 URLs ("auto", "path", "virtual").
|
||||
AWS_S3_ADDRESSING_STYLE = "auto"
|
||||
# The name of the bucket to store files in.
|
||||
AWS_S3_BUCKET_NAME = os.environ.get("AWS_S3_BUCKET_NAME")
|
||||
|
||||
# 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", "")
|
||||
# How to construct S3 URLs ("auto", "path", "virtual").
|
||||
AWS_S3_ADDRESSING_STYLE = "auto"
|
||||
|
||||
# A prefix to be applied to every stored file. This will be joined to every filename using the "/" separator.
|
||||
AWS_S3_KEY_PREFIX = ""
|
||||
# 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", "")
|
||||
|
||||
# Whether to enable authentication for stored files. If True, then generated URLs will include an authentication
|
||||
# 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
|
||||
# A prefix to be applied to every stored file. This will be joined to every filename using the "/" separator.
|
||||
AWS_S3_KEY_PREFIX = ""
|
||||
|
||||
# How long generated URLs are valid for. This affects the expiry of authentication tokens if `AWS_S3_BUCKET_AUTH`
|
||||
# is True. It also affects the "Cache-Control" header of the files.
|
||||
# Important: Changing this setting will not affect existing files.
|
||||
AWS_S3_MAX_AGE_SECONDS = 60 * 60 # 1 hours.
|
||||
# Whether to enable authentication for stored files. If True, then generated URLs will include an authentication
|
||||
# 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
|
||||
|
||||
# A URL prefix to be used for generated URLs. This is useful if your bucket is served through a CDN. This setting
|
||||
# cannot be used with `AWS_S3_BUCKET_AUTH`.
|
||||
AWS_S3_PUBLIC_URL = ""
|
||||
# How long generated URLs are valid for. This affects the expiry of authentication tokens if `AWS_S3_BUCKET_AUTH`
|
||||
# is True. It also affects the "Cache-Control" header of the files.
|
||||
# Important: Changing this setting will not affect existing files.
|
||||
AWS_S3_MAX_AGE_SECONDS = 60 * 60 # 1 hours.
|
||||
|
||||
# If True, then files will be stored with reduced redundancy. Check the S3 documentation and make sure you
|
||||
# understand the consequences before enabling.
|
||||
# Important: Changing this setting will not affect existing files.
|
||||
AWS_S3_REDUCED_REDUNDANCY = False
|
||||
# A URL prefix to be used for generated URLs. This is useful if your bucket is served through a CDN. This setting
|
||||
# cannot be used with `AWS_S3_BUCKET_AUTH`.
|
||||
AWS_S3_PUBLIC_URL = ""
|
||||
|
||||
# The Content-Disposition header used when the file is downloaded. This can be a string, or a function taking a
|
||||
# single `name` argument.
|
||||
# Important: Changing this setting will not affect existing files.
|
||||
AWS_S3_CONTENT_DISPOSITION = ""
|
||||
# If True, then files will be stored with reduced redundancy. Check the S3 documentation and make sure you
|
||||
# understand the consequences before enabling.
|
||||
# Important: Changing this setting will not affect existing files.
|
||||
AWS_S3_REDUCED_REDUNDANCY = False
|
||||
|
||||
# The Content-Language header used when the file is downloaded. This can be a string, or a function taking a
|
||||
# single `name` argument.
|
||||
# Important: Changing this setting will not affect existing files.
|
||||
AWS_S3_CONTENT_LANGUAGE = ""
|
||||
# The Content-Disposition header used when the file is downloaded. This can be a string, or a function taking a
|
||||
# single `name` argument.
|
||||
# Important: Changing this setting will not affect existing files.
|
||||
AWS_S3_CONTENT_DISPOSITION = ""
|
||||
|
||||
# A mapping of custom metadata for each file. Each value can be a string, or a function taking a
|
||||
# single `name` argument.
|
||||
# Important: Changing this setting will not affect existing files.
|
||||
AWS_S3_METADATA = {}
|
||||
# The Content-Language header used when the file is downloaded. This can be a string, or a function taking a
|
||||
# single `name` argument.
|
||||
# Important: Changing this setting will not affect existing files.
|
||||
AWS_S3_CONTENT_LANGUAGE = ""
|
||||
|
||||
# If True, then files will be stored using AES256 server-side encryption.
|
||||
# If this is a string value (e.g., "aws:kms"), that encryption type will be used.
|
||||
# Otherwise, server-side encryption is not be enabled.
|
||||
# Important: Changing this setting will not affect existing files.
|
||||
AWS_S3_ENCRYPT_KEY = False
|
||||
# A mapping of custom metadata for each file. Each value can be a string, or a function taking a
|
||||
# single `name` argument.
|
||||
# Important: Changing this setting will not affect existing files.
|
||||
AWS_S3_METADATA = {}
|
||||
|
||||
# The AWS S3 KMS encryption key ID (the `SSEKMSKeyId` parameter) is set from this string if present.
|
||||
# This is only relevant if AWS S3 KMS server-side encryption is enabled (above).
|
||||
# AWS_S3_KMS_ENCRYPTION_KEY_ID = ""
|
||||
# If True, then files will be stored using AES256 server-side encryption.
|
||||
# If this is a string value (e.g., "aws:kms"), that encryption type will be used.
|
||||
# Otherwise, server-side encryption is not be enabled.
|
||||
# Important: Changing this setting will not affect existing files.
|
||||
AWS_S3_ENCRYPT_KEY = False
|
||||
|
||||
# If True, then text files will be stored using gzip content encoding. Files will only be gzipped if their
|
||||
# compressed size is smaller than their uncompressed size.
|
||||
# Important: Changing this setting will not affect existing files.
|
||||
AWS_S3_GZIP = True
|
||||
# The AWS S3 KMS encryption key ID (the `SSEKMSKeyId` parameter) is set from this string if present.
|
||||
# This is only relevant if AWS S3 KMS server-side encryption is enabled (above).
|
||||
# AWS_S3_KMS_ENCRYPTION_KEY_ID = ""
|
||||
|
||||
# The signature version to use for S3 requests.
|
||||
AWS_S3_SIGNATURE_VERSION = None
|
||||
# If True, then text files will be stored using gzip content encoding. Files will only be gzipped if their
|
||||
# compressed size is smaller than their uncompressed size.
|
||||
# Important: Changing this setting will not affect existing files.
|
||||
AWS_S3_GZIP = True
|
||||
|
||||
# If True, then files with the same name will overwrite each other. By default it's set to False to have
|
||||
# extra characters appended.
|
||||
AWS_S3_FILE_OVERWRITE = False
|
||||
# The signature version to use for S3 requests.
|
||||
AWS_S3_SIGNATURE_VERSION = None
|
||||
|
||||
STORAGES["default"] = {
|
||||
"BACKEND": "django_s3_storage.storage.S3Storage",
|
||||
}
|
||||
# If True, then files with the same name will overwrite each other. By default it's set to False to have
|
||||
# extra characters appended.
|
||||
AWS_S3_FILE_OVERWRITE = False
|
||||
|
||||
STORAGES["default"] = {
|
||||
"BACKEND": "django_s3_storage.storage.S3Storage",
|
||||
}
|
||||
|
||||
# AWS Settings End
|
||||
|
||||
@@ -193,16 +218,27 @@ CSRF_COOKIE_SECURE = True
|
||||
|
||||
REDIS_URL = os.environ.get("REDIS_URL")
|
||||
|
||||
CACHES = {
|
||||
"default": {
|
||||
"BACKEND": "django_redis.cache.RedisCache",
|
||||
"LOCATION": REDIS_URL,
|
||||
"OPTIONS": {
|
||||
"CLIENT_CLASS": "django_redis.client.DefaultClient",
|
||||
"CONNECTION_POOL_KWARGS": {"ssl_cert_reqs": False},
|
||||
},
|
||||
if DOCKERIZED:
|
||||
CACHES = {
|
||||
"default": {
|
||||
"BACKEND": "django_redis.cache.RedisCache",
|
||||
"LOCATION": REDIS_URL,
|
||||
"OPTIONS": {
|
||||
"CLIENT_CLASS": "django_redis.client.DefaultClient",
|
||||
},
|
||||
}
|
||||
}
|
||||
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")
|
||||
@@ -225,16 +261,19 @@ broker_url = (
|
||||
f"{redis_url}?ssl_cert_reqs={ssl.CERT_NONE.name}&ssl_ca_certs={certifi.where()}"
|
||||
)
|
||||
|
||||
CELERY_RESULT_BACKEND = broker_url
|
||||
CELERY_BROKER_URL = broker_url
|
||||
if DOCKERIZED:
|
||||
CELERY_BROKER_URL = REDIS_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)
|
||||
|
||||
# Enable or Disable signups
|
||||
|
||||
ENABLE_SIGNUP = os.environ.get("ENABLE_SIGNUP", "1") == "1"
|
||||
|
||||
# Scout Settings
|
||||
SCOUT_MONITOR = os.environ.get("SCOUT_MONITOR", False)
|
||||
SCOUT_KEY = os.environ.get("SCOUT_KEY", "")
|
||||
SCOUT_NAME = "Plane"
|
||||
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
"""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")
|
||||
@@ -96,7 +96,7 @@ def burndown_plot(queryset, slug, project_id, cycle_id=None, module_id=None):
|
||||
chart_data = {str(date): 0 for date in date_range}
|
||||
|
||||
completed_issues_distribution = (
|
||||
Issue.issue_objects.filter(
|
||||
Issue.objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
issue_cycle__cycle_id=cycle_id,
|
||||
@@ -118,7 +118,7 @@ def burndown_plot(queryset, slug, project_id, cycle_id=None, module_id=None):
|
||||
chart_data = {str(date): 0 for date in date_range}
|
||||
|
||||
completed_issues_distribution = (
|
||||
Issue.issue_objects.filter(
|
||||
Issue.objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
issue_module__module_id=module_id,
|
||||
|
||||
@@ -15,7 +15,7 @@ def resolve_keys(group_keys, value):
|
||||
return value
|
||||
|
||||
|
||||
def group_results(results_data, group_by, sub_group_by=False):
|
||||
def group_results(results_data, group_by):
|
||||
"""group results data into certain group_by
|
||||
|
||||
Args:
|
||||
@@ -25,140 +25,38 @@ def group_results(results_data, group_by, sub_group_by=False):
|
||||
Returns:
|
||||
obj: grouped results
|
||||
"""
|
||||
if sub_group_by:
|
||||
main_responsive_dict = dict()
|
||||
response_dict = dict()
|
||||
|
||||
if sub_group_by == "priority":
|
||||
main_responsive_dict = {
|
||||
"urgent": {},
|
||||
"high": {},
|
||||
"medium": {},
|
||||
"low": {},
|
||||
"none": {},
|
||||
}
|
||||
if group_by == "priority":
|
||||
response_dict = {
|
||||
"urgent": [],
|
||||
"high": [],
|
||||
"medium": [],
|
||||
"low": [],
|
||||
"None": [],
|
||||
}
|
||||
|
||||
for value in results_data:
|
||||
main_group_attribute = resolve_keys(sub_group_by, value)
|
||||
group_attribute = resolve_keys(group_by, value)
|
||||
if isinstance(main_group_attribute, list) and not isinstance(group_attribute, list):
|
||||
if len(main_group_attribute):
|
||||
for attrib in main_group_attribute:
|
||||
if str(attrib) not in main_responsive_dict:
|
||||
main_responsive_dict[str(attrib)] = {}
|
||||
if str(group_attribute) in main_responsive_dict[str(attrib)]:
|
||||
main_responsive_dict[str(attrib)][str(group_attribute)].append(value)
|
||||
else:
|
||||
main_responsive_dict[str(attrib)][str(group_attribute)] = []
|
||||
main_responsive_dict[str(attrib)][str(group_attribute)].append(value)
|
||||
else:
|
||||
if str(None) not in main_responsive_dict:
|
||||
main_responsive_dict[str(None)] = {}
|
||||
|
||||
if str(group_attribute) in main_responsive_dict[str(None)]:
|
||||
main_responsive_dict[str(None)][str(group_attribute)].append(value)
|
||||
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:
|
||||
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)
|
||||
response_dict[str(attrib)] = []
|
||||
response_dict[str(attrib)].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)
|
||||
if str(None) in response_dict:
|
||||
response_dict[str(None)].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)
|
||||
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:
|
||||
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)
|
||||
response_dict[str(group_attribute)] = []
|
||||
response_dict[str(group_attribute)].append(value)
|
||||
|
||||
return response_dict
|
||||
return response_dict
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from django.utils.timezone import make_aware
|
||||
from django.utils.dateparse import parse_datetime
|
||||
|
||||
|
||||
def filter_state(params, filter, method):
|
||||
if method == "GET":
|
||||
states = params.get("state").split(",")
|
||||
@@ -24,6 +23,7 @@ def filter_state_group(params, filter, method):
|
||||
return filter
|
||||
|
||||
|
||||
|
||||
def filter_estimate_point(params, filter, method):
|
||||
if method == "GET":
|
||||
estimate_points = params.get("estimate_point").split(",")
|
||||
@@ -39,10 +39,25 @@ def filter_priority(params, filter, method):
|
||||
if method == "GET":
|
||||
priorities = params.get("priority").split(",")
|
||||
if len(priorities) and "" not in priorities:
|
||||
filter["priority__in"] = priorities
|
||||
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"]
|
||||
|
||||
else:
|
||||
if params.get("priority", None) and len(params.get("priority")):
|
||||
filter["priority__in"] = 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
|
||||
|
||||
|
||||
@@ -214,6 +229,7 @@ def filter_issue_state_type(params, filter, method):
|
||||
return filter
|
||||
|
||||
|
||||
|
||||
def filter_project(params, filter, method):
|
||||
if method == "GET":
|
||||
projects = params.get("project").split(",")
|
||||
@@ -313,7 +329,7 @@ def issue_filters(query_params, method):
|
||||
"module": filter_module,
|
||||
"inbox_status": filter_inbox_status,
|
||||
"sub_issue": filter_sub_issue_toggle,
|
||||
"subscriber": filter_subscribed_issues,
|
||||
"subscriber": filter_subscribed_issues,
|
||||
"start_target_date": filter_start_target_date_issues,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,36 +1,36 @@
|
||||
# base requirements
|
||||
|
||||
Django==4.2.5
|
||||
Django==4.2.3
|
||||
django-braces==1.15.0
|
||||
django-taggit==4.0.0
|
||||
psycopg==3.1.10
|
||||
psycopg==3.1.9
|
||||
django-oauth-toolkit==2.3.0
|
||||
mistune==3.0.1
|
||||
djangorestframework==3.14.0
|
||||
redis==4.6.0
|
||||
django-nested-admin==4.0.2
|
||||
django-cors-headers==4.2.0
|
||||
django-cors-headers==4.1.0
|
||||
whitenoise==6.5.0
|
||||
django-allauth==0.55.2
|
||||
django-allauth==0.54.0
|
||||
faker==18.11.2
|
||||
django-filter==23.2
|
||||
jsonmodels==2.6.0
|
||||
djangorestframework-simplejwt==5.3.0
|
||||
sentry-sdk==1.30.0
|
||||
djangorestframework-simplejwt==5.2.2
|
||||
sentry-sdk==1.27.0
|
||||
django-s3-storage==0.14.0
|
||||
django-crum==0.7.9
|
||||
django-guardian==2.4.0
|
||||
dj_rest_auth==2.2.5
|
||||
google-auth==2.22.0
|
||||
google-api-python-client==2.97.0
|
||||
google-auth==2.21.0
|
||||
google-api-python-client==2.92.0
|
||||
django-redis==5.3.0
|
||||
uvicorn==0.23.2
|
||||
uvicorn==0.22.0
|
||||
channels==4.0.0
|
||||
openai==0.28.0
|
||||
openai==0.27.8
|
||||
slack-sdk==3.21.3
|
||||
celery==5.3.4
|
||||
celery==5.3.1
|
||||
django_celery_beat==2.5.0
|
||||
psycopg-binary==3.1.10
|
||||
psycopg-c==3.1.10
|
||||
psycopg-binary==3.1.9
|
||||
psycopg-c==3.1.9
|
||||
scout-apm==2.26.1
|
||||
openpyxl==3.1.2
|
||||
@@ -1,11 +1,11 @@
|
||||
-r base.txt
|
||||
|
||||
dj-database-url==2.1.0
|
||||
gunicorn==21.2.0
|
||||
dj-database-url==2.0.0
|
||||
gunicorn==20.1.0
|
||||
whitenoise==6.5.0
|
||||
django-storages==1.14
|
||||
boto3==1.28.40
|
||||
django-anymail==10.1
|
||||
django-storages==1.13.2
|
||||
boto3==1.27.0
|
||||
django-anymail==10.0
|
||||
django-debug-toolbar==4.1.0
|
||||
gevent==23.7.0
|
||||
psycogreen==1.0.2
|
||||
@@ -1 +1 @@
|
||||
python-3.11.5
|
||||
python-3.11.4
|
||||
@@ -1,85 +0,0 @@
|
||||
{
|
||||
"name": "Plane",
|
||||
"description": "Plane helps you track your issues, epics, and product roadmaps.",
|
||||
"repository": "http://github.com/makeplane/plane",
|
||||
"logo": "https://avatars.githubusercontent.com/u/115727700?s=200&v=4",
|
||||
"website": "https://plane.so/",
|
||||
"success_url": "/",
|
||||
"stack": "heroku-22",
|
||||
"keywords": [
|
||||
"plane",
|
||||
"project management",
|
||||
"django",
|
||||
"next"
|
||||
],
|
||||
"addons": [
|
||||
"heroku-postgresql:mini",
|
||||
"heroku-redis:mini"
|
||||
],
|
||||
"buildpacks": [
|
||||
{
|
||||
"url": "https://github.com/heroku/heroku-buildpack-python.git"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/heroku/heroku-buildpack-nodejs#v176"
|
||||
}
|
||||
],
|
||||
"env": {
|
||||
"EMAIL_HOST": {
|
||||
"description": "Email host to send emails from",
|
||||
"value": ""
|
||||
},
|
||||
"EMAIL_HOST_USER": {
|
||||
"description": "Email host to send emails from",
|
||||
"value": ""
|
||||
},
|
||||
"EMAIL_HOST_PASSWORD": {
|
||||
"description": "Email host to send emails from",
|
||||
"value": ""
|
||||
},
|
||||
"EMAIL_FROM": {
|
||||
"description": "Email Sender",
|
||||
"value": ""
|
||||
},
|
||||
"EMAIL_PORT": {
|
||||
"description": "The default Email PORT to use",
|
||||
"value": "587"
|
||||
},
|
||||
"AWS_REGION": {
|
||||
"description": "AWS Region to use for S3",
|
||||
"value": "false"
|
||||
},
|
||||
"AWS_ACCESS_KEY_ID": {
|
||||
"description": "AWS Access Key ID to use for S3",
|
||||
"value": ""
|
||||
},
|
||||
"AWS_SECRET_ACCESS_KEY": {
|
||||
"description": "AWS Secret Access Key to use for S3",
|
||||
"value": ""
|
||||
},
|
||||
"AWS_S3_BUCKET_NAME": {
|
||||
"description": "AWS Bucket Name to use for S3",
|
||||
"value": ""
|
||||
},
|
||||
"SENTRY_DSN": {
|
||||
"description": "",
|
||||
"value": ""
|
||||
},
|
||||
"WEB_URL": {
|
||||
"description": "Web URL for Plane this will be used for redirections in the emails",
|
||||
"value": ""
|
||||
},
|
||||
"GITHUB_CLIENT_SECRET": {
|
||||
"description": "Github Client Secret",
|
||||
"value": ""
|
||||
},
|
||||
"NEXT_PUBLIC_API_BASE_URL": {
|
||||
"description": "Next Public API Base URL",
|
||||
"value": ""
|
||||
},
|
||||
"SECRET_KEY": {
|
||||
"description": "Django Secret Key",
|
||||
"value": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
# Deploy the Plane image
|
||||
FROM makeplane/plane
|
||||
|
||||
LABEL maintainer="engineering@plane.so"
|
||||
+4
-18
@@ -4,7 +4,7 @@ x-api-and-worker-env:
|
||||
&api-and-worker-env
|
||||
DEBUG: ${DEBUG}
|
||||
SENTRY_DSN: ${SENTRY_DSN}
|
||||
DJANGO_SETTINGS_MODULE: plane.settings.selfhosted
|
||||
DJANGO_SETTINGS_MODULE: plane.settings.production
|
||||
DATABASE_URL: postgres://${PGUSER}:${PGPASSWORD}@${PGHOST}:5432/${PGDATABASE}
|
||||
REDIS_URL: redis://plane-redis:6379/
|
||||
EMAIL_HOST: ${EMAIL_HOST}
|
||||
@@ -38,7 +38,7 @@ services:
|
||||
container_name: planefrontend
|
||||
image: makeplane/plane-frontend:latest
|
||||
restart: always
|
||||
command: /usr/local/bin/start.sh web/server.js web
|
||||
command: /usr/local/bin/start.sh apps/app/server.js app
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
@@ -56,20 +56,6 @@ services:
|
||||
- plane-api
|
||||
- plane-worker
|
||||
|
||||
plane-deploy:
|
||||
container_name: planedeploy
|
||||
image: makeplane/plane-deploy:latest
|
||||
restart: always
|
||||
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:
|
||||
- plane-api
|
||||
- plane-worker
|
||||
- plane-web
|
||||
|
||||
plane-api:
|
||||
container_name: planebackend
|
||||
image: makeplane/plane-backend:latest
|
||||
@@ -85,7 +71,7 @@ services:
|
||||
|
||||
plane-worker:
|
||||
container_name: planebgworker
|
||||
image: makeplane/plane-backend:latest
|
||||
image: makeplane/plane-worker:latest
|
||||
restart: always
|
||||
command: ./bin/worker
|
||||
env_file:
|
||||
@@ -99,7 +85,7 @@ services:
|
||||
|
||||
plane-beat-worker:
|
||||
container_name: planebeatworker
|
||||
image: makeplane/plane-backend:latest
|
||||
image: makeplane/plane-worker:latest
|
||||
restart: always
|
||||
command: ./bin/beat
|
||||
env_file:
|
||||
|
||||
+163
-126
@@ -1,139 +1,176 @@
|
||||
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:
|
||||
plane-web:
|
||||
container_name: planefrontend
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./web/Dockerfile.web
|
||||
args:
|
||||
DOCKER_BUILDKIT: 1
|
||||
restart: always
|
||||
command: /usr/local/bin/start.sh web/server.js web
|
||||
depends_on:
|
||||
- plane-api
|
||||
- plane-worker
|
||||
plane-web:
|
||||
container_name: planefrontend
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./apps/app/Dockerfile.web
|
||||
args:
|
||||
NEXT_PUBLIC_API_BASE_URL: http://localhost:8000
|
||||
NEXT_PUBLIC_DEPLOY_URL: http://localhost/spaces
|
||||
restart: always
|
||||
command: /usr/local/bin/start.sh apps/app/server.js app
|
||||
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:
|
||||
- plane-api
|
||||
- plane-worker
|
||||
|
||||
plane-deploy:
|
||||
container_name: planedeploy
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./space/Dockerfile.space
|
||||
args:
|
||||
DOCKER_BUILDKIT: 1
|
||||
restart: always
|
||||
command: /usr/local/bin/start.sh space/server.js space
|
||||
depends_on:
|
||||
- plane-api
|
||||
- plane-worker
|
||||
- plane-web
|
||||
plane-api:
|
||||
container_name: planebackend
|
||||
build:
|
||||
context: ./apiserver
|
||||
dockerfile: Dockerfile.api
|
||||
restart: always
|
||||
command: ./bin/takeoff
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
<<: *api-and-worker-env
|
||||
depends_on:
|
||||
- plane-db
|
||||
- plane-redis
|
||||
|
||||
plane-api:
|
||||
container_name: planebackend
|
||||
build:
|
||||
context: ./apiserver
|
||||
dockerfile: Dockerfile.api
|
||||
args:
|
||||
DOCKER_BUILDKIT: 1
|
||||
restart: always
|
||||
command: ./bin/takeoff
|
||||
env_file:
|
||||
- ./apiserver/.env
|
||||
depends_on:
|
||||
- plane-db
|
||||
- plane-redis
|
||||
plane-worker:
|
||||
container_name: planebgworker
|
||||
build:
|
||||
context: ./apiserver
|
||||
dockerfile: Dockerfile.api
|
||||
restart: always
|
||||
command: ./bin/worker
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
<<: *api-and-worker-env
|
||||
depends_on:
|
||||
- plane-api
|
||||
- plane-db
|
||||
- plane-redis
|
||||
|
||||
plane-worker:
|
||||
container_name: planebgworker
|
||||
build:
|
||||
context: ./apiserver
|
||||
dockerfile: Dockerfile.api
|
||||
args:
|
||||
DOCKER_BUILDKIT: 1
|
||||
restart: always
|
||||
command: ./bin/worker
|
||||
env_file:
|
||||
- ./apiserver/.env
|
||||
depends_on:
|
||||
- plane-api
|
||||
- plane-db
|
||||
- plane-redis
|
||||
plane-beat-worker:
|
||||
container_name: planebeatworker
|
||||
build:
|
||||
context: ./apiserver
|
||||
dockerfile: Dockerfile.api
|
||||
restart: always
|
||||
command: ./bin/beat
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
<<: *api-and-worker-env
|
||||
depends_on:
|
||||
- plane-api
|
||||
- plane-db
|
||||
- plane-redis
|
||||
|
||||
plane-beat-worker:
|
||||
container_name: planebeatworker
|
||||
build:
|
||||
context: ./apiserver
|
||||
dockerfile: Dockerfile.api
|
||||
args:
|
||||
DOCKER_BUILDKIT: 1
|
||||
restart: always
|
||||
command: ./bin/beat
|
||||
env_file:
|
||||
- ./apiserver/.env
|
||||
depends_on:
|
||||
- plane-api
|
||||
- plane-db
|
||||
- plane-redis
|
||||
plane-db:
|
||||
container_name: plane-db
|
||||
image: postgres:15.2-alpine
|
||||
restart: always
|
||||
command: postgres -c 'max_connections=1000'
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
POSTGRES_USER: ${PGUSER}
|
||||
POSTGRES_DB: ${PGDATABASE}
|
||||
POSTGRES_PASSWORD: ${PGPASSWORD}
|
||||
PGDATA: /var/lib/postgresql/data
|
||||
|
||||
plane-db:
|
||||
container_name: plane-db
|
||||
image: postgres:15.2-alpine
|
||||
restart: always
|
||||
command: postgres -c 'max_connections=1000'
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
POSTGRES_USER: ${PGUSER}
|
||||
POSTGRES_DB: ${PGDATABASE}
|
||||
POSTGRES_PASSWORD: ${PGPASSWORD}
|
||||
PGDATA: /var/lib/postgresql/data
|
||||
plane-redis:
|
||||
container_name: plane-redis
|
||||
image: redis:6.2.7-alpine
|
||||
restart: always
|
||||
volumes:
|
||||
- redisdata:/data
|
||||
|
||||
plane-redis:
|
||||
container_name: plane-redis
|
||||
image: redis:6.2.7-alpine
|
||||
restart: always
|
||||
volumes:
|
||||
- redisdata:/data
|
||||
plane-minio:
|
||||
container_name: plane-minio
|
||||
image: minio/minio
|
||||
restart: always
|
||||
command: server /export --console-address ":9090"
|
||||
volumes:
|
||||
- uploads:/export
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
MINIO_ROOT_USER: ${AWS_ACCESS_KEY_ID}
|
||||
MINIO_ROOT_PASSWORD: ${AWS_SECRET_ACCESS_KEY}
|
||||
|
||||
plane-minio:
|
||||
container_name: plane-minio
|
||||
image: minio/minio
|
||||
restart: always
|
||||
command: server /export --console-address ":9090"
|
||||
volumes:
|
||||
- uploads:/export
|
||||
environment:
|
||||
MINIO_ROOT_USER: ${AWS_ACCESS_KEY_ID}
|
||||
MINIO_ROOT_PASSWORD: ${AWS_SECRET_ACCESS_KEY}
|
||||
createbuckets:
|
||||
image: minio/mc
|
||||
entrypoint: >
|
||||
/bin/sh -c " /usr/bin/mc config host add plane-minio http://plane-minio:9000 \$AWS_ACCESS_KEY_ID \$AWS_SECRET_ACCESS_KEY; /usr/bin/mc mb plane-minio/\$AWS_S3_BUCKET_NAME; /usr/bin/mc anonymous set download plane-minio/\$AWS_S3_BUCKET_NAME; exit 0; "
|
||||
env_file:
|
||||
- .env
|
||||
depends_on:
|
||||
- plane-minio
|
||||
|
||||
createbuckets:
|
||||
image: minio/mc
|
||||
entrypoint: >
|
||||
/bin/sh -c " /usr/bin/mc config host add plane-minio http://plane-minio:9000 \$AWS_ACCESS_KEY_ID \$AWS_SECRET_ACCESS_KEY; /usr/bin/mc mb plane-minio/\$AWS_S3_BUCKET_NAME; /usr/bin/mc anonymous set download plane-minio/\$AWS_S3_BUCKET_NAME; exit 0; "
|
||||
env_file:
|
||||
- .env
|
||||
depends_on:
|
||||
- plane-minio
|
||||
|
||||
# Comment this if you already have a reverse proxy running
|
||||
plane-proxy:
|
||||
container_name: planeproxy
|
||||
build:
|
||||
context: ./nginx
|
||||
dockerfile: Dockerfile
|
||||
restart: always
|
||||
ports:
|
||||
- ${NGINX_PORT}:80
|
||||
environment:
|
||||
FILE_SIZE_LIMIT: ${FILE_SIZE_LIMIT:-5242880}
|
||||
BUCKET_NAME: ${AWS_S3_BUCKET_NAME:-uploads}
|
||||
depends_on:
|
||||
- plane-web
|
||||
- plane-api
|
||||
# Comment this if you already have a reverse proxy running
|
||||
plane-proxy:
|
||||
container_name: planeproxy
|
||||
build:
|
||||
context: ./nginx
|
||||
dockerfile: Dockerfile
|
||||
restart: always
|
||||
ports:
|
||||
- ${NGINX_PORT}:80
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
FILE_SIZE_LIMIT: ${FILE_SIZE_LIMIT:-5242880}
|
||||
BUCKET_NAME: ${AWS_S3_BUCKET_NAME:-uploads}
|
||||
depends_on:
|
||||
- plane-web
|
||||
- plane-api
|
||||
|
||||
volumes:
|
||||
pgdata:
|
||||
redisdata:
|
||||
uploads:
|
||||
pgdata:
|
||||
redisdata:
|
||||
uploads:
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
build:
|
||||
docker:
|
||||
web: deploy/heroku/Dockerfile
|
||||
+18
-21
@@ -1,29 +1,26 @@
|
||||
events { }
|
||||
|
||||
|
||||
http {
|
||||
sendfile on;
|
||||
sendfile on;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
root /www/data/;
|
||||
access_log /var/log/nginx/access.log;
|
||||
server {
|
||||
listen 80;
|
||||
root /www/data/;
|
||||
access_log /var/log/nginx/access.log;
|
||||
|
||||
client_max_body_size ${FILE_SIZE_LIMIT};
|
||||
client_max_body_size ${FILE_SIZE_LIMIT};
|
||||
|
||||
location / {
|
||||
proxy_pass http://planefrontend:3000/;
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://planebackend:8000/api/;
|
||||
}
|
||||
|
||||
location /spaces/ {
|
||||
proxy_pass http://planedeploy:3000/spaces/;
|
||||
}
|
||||
|
||||
location /${BUCKET_NAME}/ {
|
||||
proxy_pass http://plane-minio:9000/uploads/;
|
||||
}
|
||||
location / {
|
||||
proxy_pass http://planefrontend:3000/;
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://planebackend:8000/api/;
|
||||
}
|
||||
|
||||
location /${BUCKET_NAME}/ {
|
||||
proxy_pass http://plane-minio:9000/uploads/;
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
-16
@@ -1,28 +1,16 @@
|
||||
{
|
||||
"name": "plane",
|
||||
"repository": "https://github.com/makeplane/plane.git",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
"web",
|
||||
"space",
|
||||
"web/",
|
||||
"space/",
|
||||
"packages/*"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "turbo run build",
|
||||
"dev": "turbo run dev",
|
||||
"start": "turbo run start",
|
||||
"lint": "turbo run lint",
|
||||
"clean": "turbo run clean",
|
||||
"format": "prettier --write \"**/*.{ts,tsx,md}\""
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^10.4.15",
|
||||
"eslint-config-custom": "*",
|
||||
"postcss": "^8.4.29",
|
||||
"prettier": "latest",
|
||||
"prettier-plugin-tailwindcss": "^0.5.4",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"turbo": "latest"
|
||||
"prettier": "latest"
|
||||
},
|
||||
"packageManager": "yarn@1.22.19"
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ module.exports = {
|
||||
plugins: ["react", "@typescript-eslint"],
|
||||
settings: {
|
||||
next: {
|
||||
rootDir: ["web/", "space/", "packages/*/"],
|
||||
rootDir: ["app/", "docs/", "packages/*/"],
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
@@ -16,7 +16,5 @@ module.exports = {
|
||||
"no-duplicate-imports": "error",
|
||||
"arrow-body-style": ["error", "as-needed"],
|
||||
"react/self-closing-comp": ["error", { component: true, html: true }],
|
||||
"@next/next/no-img-element": "off",
|
||||
"@typescript-eslint/no-unused-vars": ["warn"],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
"tailwindcss/nesting": {},
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
@@ -1,212 +0,0 @@
|
||||
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")],
|
||||
};
|
||||
@@ -4,9 +4,8 @@
|
||||
"extends": "./base.json",
|
||||
"compilerOptions": {
|
||||
"jsx": "react",
|
||||
"lib": ["ES2015", "DOM"],
|
||||
"lib": ["ES2015"],
|
||||
"module": "ESNext",
|
||||
"target": "es6",
|
||||
"sourceMap": true
|
||||
"target": "es6"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import * as React from "react";
|
||||
|
||||
export const Button = () => {
|
||||
return <button>button</button>;
|
||||
};
|
||||
Vendored
-41
@@ -1,41 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { FC } from 'react';
|
||||
|
||||
declare const Button: () => JSX.Element;
|
||||
|
||||
interface InputProps {
|
||||
type: string;
|
||||
id: string;
|
||||
value: string;
|
||||
name: string;
|
||||
onChange: () => void;
|
||||
className?: string;
|
||||
mode?: "primary" | "transparent" | "true-transparent";
|
||||
size?: "sm" | "md" | "lg";
|
||||
hasError?: boolean;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
declare const Input: React.ForwardRefExoticComponent<InputProps & React.RefAttributes<HTMLInputElement>>;
|
||||
|
||||
interface TextAreaProps {
|
||||
id: string;
|
||||
name: string;
|
||||
placeholder?: string;
|
||||
value?: string;
|
||||
rows?: number;
|
||||
cols?: number;
|
||||
disabled?: boolean;
|
||||
onChange: () => void;
|
||||
mode?: "primary" | "transparent";
|
||||
hasError?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
declare const TextArea: React.ForwardRefExoticComponent<TextAreaProps & React.RefAttributes<HTMLTextAreaElement>>;
|
||||
|
||||
interface IRadialProgressBar {
|
||||
progress: number;
|
||||
}
|
||||
declare const RadialProgressBar: FC<IRadialProgressBar>;
|
||||
|
||||
export { Button, Input, InputProps, RadialProgressBar, TextArea, TextAreaProps };
|
||||
Vendored
-157
@@ -1,157 +0,0 @@
|
||||
"use strict";
|
||||
var __create = Object.create;
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __getProtoOf = Object.getPrototypeOf;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
||||
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
||||
mod
|
||||
));
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
|
||||
// src/index.tsx
|
||||
var src_exports = {};
|
||||
__export(src_exports, {
|
||||
Button: () => Button,
|
||||
Input: () => Input,
|
||||
RadialProgressBar: () => RadialProgressBar,
|
||||
TextArea: () => TextArea
|
||||
});
|
||||
module.exports = __toCommonJS(src_exports);
|
||||
|
||||
// src/buttons/index.tsx
|
||||
var React = __toESM(require("react"));
|
||||
var Button = () => {
|
||||
return /* @__PURE__ */ React.createElement("button", null, "button");
|
||||
};
|
||||
|
||||
// src/form-fields/input.tsx
|
||||
var React2 = __toESM(require("react"));
|
||||
var Input = React2.forwardRef((props, ref) => {
|
||||
const {
|
||||
id,
|
||||
type,
|
||||
value,
|
||||
name,
|
||||
onChange,
|
||||
className = "",
|
||||
mode = "primary",
|
||||
size = "md",
|
||||
hasError = false,
|
||||
placeholder = "",
|
||||
disabled = false
|
||||
} = props;
|
||||
return /* @__PURE__ */ React2.createElement("input", {
|
||||
id,
|
||||
ref,
|
||||
type,
|
||||
value,
|
||||
name,
|
||||
onChange,
|
||||
placeholder,
|
||||
disabled,
|
||||
className: `block rounded-md bg-transparent text-sm focus:outline-none placeholder-custom-text-400 ${mode === "primary" ? "rounded-md border border-custom-border-200" : mode === "transparent" ? "rounded border-none bg-transparent ring-0 transition-all focus:ring-1 focus:ring-custom-primary" : mode === "true-transparent" ? "rounded border-none bg-transparent ring-0" : ""} ${hasError ? "border-red-500" : ""} ${hasError && mode === "primary" ? "bg-red-500/20" : ""} ${size === "sm" ? "px-3 py-2" : size === "lg" ? "p-3" : ""} ${className}`
|
||||
});
|
||||
});
|
||||
Input.displayName = "form-input-field";
|
||||
|
||||
// src/form-fields/textarea.tsx
|
||||
var React3 = __toESM(require("react"));
|
||||
var useAutoSizeTextArea = (textAreaRef, value) => {
|
||||
React3.useEffect(() => {
|
||||
if (textAreaRef) {
|
||||
textAreaRef.style.height = "0px";
|
||||
const scrollHeight = textAreaRef.scrollHeight;
|
||||
textAreaRef.style.height = scrollHeight + "px";
|
||||
}
|
||||
}, [textAreaRef, value]);
|
||||
};
|
||||
var TextArea = React3.forwardRef(
|
||||
(props, ref) => {
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
placeholder = "",
|
||||
value = "",
|
||||
rows = 1,
|
||||
cols = 1,
|
||||
disabled,
|
||||
onChange,
|
||||
mode = "primary",
|
||||
hasError = false,
|
||||
className = ""
|
||||
} = props;
|
||||
const textAreaRef = React3.useRef(ref);
|
||||
ref && useAutoSizeTextArea(textAreaRef == null ? void 0 : textAreaRef.current, value);
|
||||
return /* @__PURE__ */ React3.createElement("textarea", {
|
||||
id,
|
||||
name,
|
||||
ref: textAreaRef,
|
||||
placeholder,
|
||||
value,
|
||||
rows,
|
||||
cols,
|
||||
disabled,
|
||||
onChange,
|
||||
className: `no-scrollbar w-full bg-transparent placeholder-custom-text-400 px-3 py-2 outline-none ${mode === "primary" ? "rounded-md border border-custom-border-200" : mode === "transparent" ? "rounded border-none bg-transparent ring-0 transition-all focus:ring-1 focus:ring-theme" : ""} ${hasError ? "border-red-500" : ""} ${hasError && mode === "primary" ? "bg-red-100" : ""} ${className}`
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
// src/progress/radial-progress.tsx
|
||||
var import_react = __toESM(require("react"));
|
||||
var RadialProgressBar = (props) => {
|
||||
const { progress } = props;
|
||||
const [circumference, setCircumference] = (0, import_react.useState)(0);
|
||||
(0, import_react.useEffect)(() => {
|
||||
const radius = 40;
|
||||
const circumference2 = 2 * Math.PI * radius;
|
||||
setCircumference(circumference2);
|
||||
}, []);
|
||||
const progressOffset = (100 - progress) / 100 * circumference;
|
||||
return /* @__PURE__ */ import_react.default.createElement("div", {
|
||||
className: "relative h-4 w-4"
|
||||
}, /* @__PURE__ */ import_react.default.createElement("svg", {
|
||||
className: "absolute top-0 left-0",
|
||||
viewBox: "0 0 100 100"
|
||||
}, /* @__PURE__ */ import_react.default.createElement("circle", {
|
||||
className: "stroke-current opacity-10",
|
||||
cx: "50",
|
||||
cy: "50",
|
||||
r: "40",
|
||||
strokeWidth: "12",
|
||||
fill: "none",
|
||||
strokeDasharray: `${circumference} ${circumference}`
|
||||
}), /* @__PURE__ */ import_react.default.createElement("circle", {
|
||||
className: `stroke-current`,
|
||||
cx: "50",
|
||||
cy: "50",
|
||||
r: "40",
|
||||
strokeWidth: "12",
|
||||
fill: "none",
|
||||
strokeDasharray: `${circumference} ${circumference}`,
|
||||
strokeDashoffset: progressOffset,
|
||||
transform: "rotate(-90 50 50)"
|
||||
})));
|
||||
};
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
Button,
|
||||
Input,
|
||||
RadialProgressBar,
|
||||
TextArea
|
||||
});
|
||||
Vendored
-121
@@ -1,121 +0,0 @@
|
||||
// src/buttons/index.tsx
|
||||
import * as React from "react";
|
||||
var Button = () => {
|
||||
return /* @__PURE__ */ React.createElement("button", null, "button");
|
||||
};
|
||||
|
||||
// src/form-fields/input.tsx
|
||||
import * as React2 from "react";
|
||||
var Input = React2.forwardRef((props, ref) => {
|
||||
const {
|
||||
id,
|
||||
type,
|
||||
value,
|
||||
name,
|
||||
onChange,
|
||||
className = "",
|
||||
mode = "primary",
|
||||
size = "md",
|
||||
hasError = false,
|
||||
placeholder = "",
|
||||
disabled = false
|
||||
} = props;
|
||||
return /* @__PURE__ */ React2.createElement("input", {
|
||||
id,
|
||||
ref,
|
||||
type,
|
||||
value,
|
||||
name,
|
||||
onChange,
|
||||
placeholder,
|
||||
disabled,
|
||||
className: `block rounded-md bg-transparent text-sm focus:outline-none placeholder-custom-text-400 ${mode === "primary" ? "rounded-md border border-custom-border-200" : mode === "transparent" ? "rounded border-none bg-transparent ring-0 transition-all focus:ring-1 focus:ring-custom-primary" : mode === "true-transparent" ? "rounded border-none bg-transparent ring-0" : ""} ${hasError ? "border-red-500" : ""} ${hasError && mode === "primary" ? "bg-red-500/20" : ""} ${size === "sm" ? "px-3 py-2" : size === "lg" ? "p-3" : ""} ${className}`
|
||||
});
|
||||
});
|
||||
Input.displayName = "form-input-field";
|
||||
|
||||
// src/form-fields/textarea.tsx
|
||||
import * as React3 from "react";
|
||||
var useAutoSizeTextArea = (textAreaRef, value) => {
|
||||
React3.useEffect(() => {
|
||||
if (textAreaRef) {
|
||||
textAreaRef.style.height = "0px";
|
||||
const scrollHeight = textAreaRef.scrollHeight;
|
||||
textAreaRef.style.height = scrollHeight + "px";
|
||||
}
|
||||
}, [textAreaRef, value]);
|
||||
};
|
||||
var TextArea = React3.forwardRef(
|
||||
(props, ref) => {
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
placeholder = "",
|
||||
value = "",
|
||||
rows = 1,
|
||||
cols = 1,
|
||||
disabled,
|
||||
onChange,
|
||||
mode = "primary",
|
||||
hasError = false,
|
||||
className = ""
|
||||
} = props;
|
||||
const textAreaRef = React3.useRef(ref);
|
||||
ref && useAutoSizeTextArea(textAreaRef == null ? void 0 : textAreaRef.current, value);
|
||||
return /* @__PURE__ */ React3.createElement("textarea", {
|
||||
id,
|
||||
name,
|
||||
ref: textAreaRef,
|
||||
placeholder,
|
||||
value,
|
||||
rows,
|
||||
cols,
|
||||
disabled,
|
||||
onChange,
|
||||
className: `no-scrollbar w-full bg-transparent placeholder-custom-text-400 px-3 py-2 outline-none ${mode === "primary" ? "rounded-md border border-custom-border-200" : mode === "transparent" ? "rounded border-none bg-transparent ring-0 transition-all focus:ring-1 focus:ring-theme" : ""} ${hasError ? "border-red-500" : ""} ${hasError && mode === "primary" ? "bg-red-100" : ""} ${className}`
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
// src/progress/radial-progress.tsx
|
||||
import React4, { useState, useEffect as useEffect2 } from "react";
|
||||
var RadialProgressBar = (props) => {
|
||||
const { progress } = props;
|
||||
const [circumference, setCircumference] = useState(0);
|
||||
useEffect2(() => {
|
||||
const radius = 40;
|
||||
const circumference2 = 2 * Math.PI * radius;
|
||||
setCircumference(circumference2);
|
||||
}, []);
|
||||
const progressOffset = (100 - progress) / 100 * circumference;
|
||||
return /* @__PURE__ */ React4.createElement("div", {
|
||||
className: "relative h-4 w-4"
|
||||
}, /* @__PURE__ */ React4.createElement("svg", {
|
||||
className: "absolute top-0 left-0",
|
||||
viewBox: "0 0 100 100"
|
||||
}, /* @__PURE__ */ React4.createElement("circle", {
|
||||
className: "stroke-current opacity-10",
|
||||
cx: "50",
|
||||
cy: "50",
|
||||
r: "40",
|
||||
strokeWidth: "12",
|
||||
fill: "none",
|
||||
strokeDasharray: `${circumference} ${circumference}`
|
||||
}), /* @__PURE__ */ React4.createElement("circle", {
|
||||
className: `stroke-current`,
|
||||
cx: "50",
|
||||
cy: "50",
|
||||
r: "40",
|
||||
strokeWidth: "12",
|
||||
fill: "none",
|
||||
strokeDasharray: `${circumference} ${circumference}`,
|
||||
strokeDashoffset: progressOffset,
|
||||
transform: "rotate(-90 50 50)"
|
||||
})));
|
||||
};
|
||||
export {
|
||||
Button,
|
||||
Input,
|
||||
RadialProgressBar,
|
||||
TextArea
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
// import * as React from "react";
|
||||
// components
|
||||
// export * from "./breadcrumbs";
|
||||
// export * from "./button";
|
||||
// export * from "./custom-listbox";
|
||||
// export * from "./custom-menu";
|
||||
// export * from "./custom-select";
|
||||
// export * from "./empty-space";
|
||||
// export * from "./header-button";
|
||||
// export * from "./input";
|
||||
// export * from "./loader";
|
||||
// export * from "./outline-button";
|
||||
// export * from "./select";
|
||||
// export * from "./spinner";
|
||||
// export * from "./text-area";
|
||||
// export * from "./tooltip";
|
||||
export * from "./button";
|
||||
+10
-21
@@ -1,33 +1,22 @@
|
||||
{
|
||||
"name": "@plane/ui",
|
||||
"version": "0.0.1",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
"sideEffects": false,
|
||||
"name": "ui",
|
||||
"version": "0.0.0",
|
||||
"main": "./index.tsx",
|
||||
"types": "./index.tsx",
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
"dist/**"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsup src/index.tsx --format esm,cjs --dts --external react",
|
||||
"dev": "tsup src/index.tsx --format esm,cjs --watch --dts --external react",
|
||||
"lint": "eslint src/",
|
||||
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist"
|
||||
"lint": "eslint *.ts*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.5.2",
|
||||
"@types/react": "18.2.0",
|
||||
"@types/react-dom": "18.2.0",
|
||||
"@types/react": "^18.0.17",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@typescript-eslint/eslint-plugin": "^5.51.0",
|
||||
"classnames": "^2.3.2",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-custom": "*",
|
||||
"next": "12.3.2",
|
||||
"react": "^18.2.0",
|
||||
"tsconfig": "*",
|
||||
"tailwind-config-custom": "*",
|
||||
"tsup": "^5.10.1",
|
||||
"typescript": "4.7.4"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
module.exports = require("tailwind-config-custom/postcss.config");
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from "./input";
|
||||
export * from "./textarea";
|
||||
@@ -1,61 +0,0 @@
|
||||
import * as React from "react";
|
||||
|
||||
export interface InputProps {
|
||||
type: string;
|
||||
id: string;
|
||||
value: string;
|
||||
name: string;
|
||||
onChange: () => void;
|
||||
className?: string;
|
||||
mode?: "primary" | "transparent" | "true-transparent";
|
||||
size?: "sm" | "md" | "lg";
|
||||
hasError?: boolean;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>((props, ref) => {
|
||||
const {
|
||||
id,
|
||||
type,
|
||||
value,
|
||||
name,
|
||||
onChange,
|
||||
className = "",
|
||||
mode = "primary",
|
||||
size = "md",
|
||||
hasError = false,
|
||||
placeholder = "",
|
||||
disabled = false,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<input
|
||||
id={id}
|
||||
ref={ref}
|
||||
type={type}
|
||||
value={value}
|
||||
name={name}
|
||||
onChange={onChange}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
className={`block rounded-md bg-transparent text-sm focus:outline-none placeholder-custom-text-400 ${
|
||||
mode === "primary"
|
||||
? "rounded-md border border-custom-border-200"
|
||||
: mode === "transparent"
|
||||
? "rounded border-none bg-transparent ring-0 transition-all focus:ring-1 focus:ring-custom-primary"
|
||||
: mode === "true-transparent"
|
||||
? "rounded border-none bg-transparent ring-0"
|
||||
: ""
|
||||
} ${hasError ? "border-red-500" : ""} ${
|
||||
hasError && mode === "primary" ? "bg-red-500/20" : ""
|
||||
} ${
|
||||
size === "sm" ? "px-3 py-2" : size === "lg" ? "p-3" : ""
|
||||
} ${className}`}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
Input.displayName = "form-input-field";
|
||||
|
||||
export { Input };
|
||||
@@ -1,80 +0,0 @@
|
||||
import * as React from "react";
|
||||
|
||||
export interface TextAreaProps {
|
||||
id: string;
|
||||
name: string;
|
||||
placeholder?: string;
|
||||
value?: string;
|
||||
rows?: number;
|
||||
cols?: number;
|
||||
disabled?: boolean;
|
||||
onChange: () => void;
|
||||
mode?: "primary" | "transparent";
|
||||
hasError?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
// Updates the height of a <textarea> when the value changes.
|
||||
const useAutoSizeTextArea = (
|
||||
textAreaRef: HTMLTextAreaElement | null,
|
||||
value: any
|
||||
) => {
|
||||
React.useEffect(() => {
|
||||
if (textAreaRef) {
|
||||
// We need to reset the height momentarily to get the correct scrollHeight for the textarea
|
||||
textAreaRef.style.height = "0px";
|
||||
const scrollHeight = textAreaRef.scrollHeight;
|
||||
|
||||
// We then set the height directly, outside of the render loop
|
||||
// Trying to set this with state or a ref will product an incorrect value.
|
||||
textAreaRef.style.height = scrollHeight + "px";
|
||||
}
|
||||
}, [textAreaRef, value]);
|
||||
};
|
||||
|
||||
const TextArea = React.forwardRef<HTMLTextAreaElement, TextAreaProps>(
|
||||
(props, ref) => {
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
placeholder = "",
|
||||
value = "",
|
||||
rows = 1,
|
||||
cols = 1,
|
||||
disabled,
|
||||
onChange,
|
||||
mode = "primary",
|
||||
hasError = false,
|
||||
className = "",
|
||||
} = props;
|
||||
|
||||
const textAreaRef = React.useRef<any>(ref);
|
||||
|
||||
ref && useAutoSizeTextArea(textAreaRef?.current, value);
|
||||
|
||||
return (
|
||||
<textarea
|
||||
id={id}
|
||||
name={name}
|
||||
ref={textAreaRef}
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
rows={rows}
|
||||
cols={cols}
|
||||
disabled={disabled}
|
||||
onChange={onChange}
|
||||
className={`no-scrollbar w-full bg-transparent placeholder-custom-text-400 px-3 py-2 outline-none ${
|
||||
mode === "primary"
|
||||
? "rounded-md border border-custom-border-200"
|
||||
: mode === "transparent"
|
||||
? "rounded border-none bg-transparent ring-0 transition-all focus:ring-1 focus:ring-theme"
|
||||
: ""
|
||||
} ${hasError ? "border-red-500" : ""} ${
|
||||
hasError && mode === "primary" ? "bg-red-100" : ""
|
||||
} ${className}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export { TextArea };
|
||||
@@ -1,3 +0,0 @@
|
||||
export * from "./buttons";
|
||||
export * from "./form-fields";
|
||||
export * from "./progress";
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./radial-progress";
|
||||
@@ -1,45 +0,0 @@
|
||||
import React, { useState, useEffect, FC } from "react";
|
||||
|
||||
interface IRadialProgressBar {
|
||||
progress: number;
|
||||
}
|
||||
|
||||
export const RadialProgressBar: FC<IRadialProgressBar> = (props) => {
|
||||
const { progress } = props;
|
||||
const [circumference, setCircumference] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const radius = 40;
|
||||
const circumference = 2 * Math.PI * radius;
|
||||
setCircumference(circumference);
|
||||
}, []);
|
||||
|
||||
const progressOffset = ((100 - progress) / 100) * circumference;
|
||||
|
||||
return (
|
||||
<div className="relative h-4 w-4">
|
||||
<svg className="absolute top-0 left-0" viewBox="0 0 100 100">
|
||||
<circle
|
||||
className={"stroke-current opacity-10"}
|
||||
cx="50"
|
||||
cy="50"
|
||||
r="40"
|
||||
strokeWidth="12"
|
||||
fill="none"
|
||||
strokeDasharray={`${circumference} ${circumference}`}
|
||||
/>
|
||||
<circle
|
||||
className={`stroke-current`}
|
||||
cx="50"
|
||||
cy="50"
|
||||
r="40"
|
||||
strokeWidth="12"
|
||||
fill="none"
|
||||
strokeDasharray={`${circumference} ${circumference}`}
|
||||
strokeDashoffset={progressOffset}
|
||||
transform="rotate(-90 50 50)"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
module.exports = require("tailwind-config-custom/tailwind.config");
|
||||
@@ -1,8 +1,9 @@
|
||||
{
|
||||
"extends": "tsconfig/react-library.json",
|
||||
"compilerOptions": {
|
||||
"jsx": "react"
|
||||
},
|
||||
"extends": "../tsconfig/nextjs.json",
|
||||
"include": ["."],
|
||||
"exclude": ["dist", "build", "node_modules"]
|
||||
"exclude": ["dist", "build", "node_modules"],
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"lib": ["DOM"]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
#!/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}" apps/$DIRECTORY/.next | xargs -I{} sed -i "s|$FROM|$TO|g" "{}"
|
||||
@@ -5,9 +5,25 @@ cp ./.env.example ./.env
|
||||
export LC_ALL=C
|
||||
export LC_CTYPE=C
|
||||
|
||||
cp ./web/.env.example ./web/.env
|
||||
cp ./space/.env.example ./space/.env
|
||||
cp ./apiserver/.env.example ./apiserver/.env
|
||||
|
||||
# Generate the NEXT_PUBLIC_API_BASE_URL with given IP
|
||||
echo -e "\nNEXT_PUBLIC_API_BASE_URL=$1" >> ./.env
|
||||
|
||||
# Generate the SECRET_KEY that will be used by django
|
||||
echo -e "SECRET_KEY=\"$(tr -dc 'a-z0-9' < /dev/urandom | head -c50)\"" >> ./apiserver/.env
|
||||
echo -e "SECRET_KEY=\"$(tr -dc 'a-z0-9' < /dev/urandom | head -c50)\"" >> ./.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
-4
@@ -1,4 +1 @@
|
||||
# Google Client ID for Google OAuth
|
||||
NEXT_PUBLIC_GOOGLE_CLIENTID=""
|
||||
# Flag to toggle OAuth
|
||||
NEXT_PUBLIC_ENABLE_OAUTH=0
|
||||
NEXT_PUBLIC_API_BASE_URL=''
|
||||
@@ -1,4 +0,0 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ["custom"],
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
FROM node:18-alpine AS builder
|
||||
RUN apk add --no-cache libc6-compat
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
ENV NEXT_PUBLIC_API_BASE_URL=http://NEXT_PUBLIC_API_BASE_URL_PLACEHOLDER
|
||||
|
||||
RUN yarn global add turbo
|
||||
COPY . .
|
||||
|
||||
RUN turbo prune --scope=space --docker
|
||||
|
||||
# Add lockfile and package.json's of isolated subworkspace
|
||||
FROM node:18-alpine AS installer
|
||||
|
||||
RUN apk add --no-cache libc6-compat
|
||||
WORKDIR /app
|
||||
ARG NEXT_PUBLIC_API_BASE_URL=http://localhost:8000
|
||||
|
||||
# First install the dependencies (as they change less often)
|
||||
COPY .gitignore .gitignore
|
||||
COPY --from=builder /app/out/json/ .
|
||||
COPY --from=builder /app/out/yarn.lock ./yarn.lock
|
||||
RUN yarn install --network-timeout 500000
|
||||
|
||||
# Build the project
|
||||
COPY --from=builder /app/out/full/ .
|
||||
COPY turbo.json turbo.json
|
||||
COPY replace-env-vars.sh /usr/local/bin/
|
||||
USER root
|
||||
RUN chmod +x /usr/local/bin/replace-env-vars.sh
|
||||
|
||||
RUN yarn turbo run build --filter=space
|
||||
|
||||
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \
|
||||
BUILT_NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
|
||||
|
||||
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
|
||||
WORKDIR /app
|
||||
|
||||
# Don't run production as root
|
||||
RUN addgroup --system --gid 1001 plane
|
||||
RUN adduser --system --uid 1001 captain
|
||||
USER captain
|
||||
|
||||
COPY --from=installer /app/apps/space/next.config.js .
|
||||
COPY --from=installer /app/apps/space/package.json .
|
||||
|
||||
# Automatically leverage output traces to reduce image size
|
||||
# https://nextjs.org/docs/advanced-features/output-file-tracing
|
||||
COPY --from=installer --chown=captain:plane /app/apps/space/.next/standalone ./
|
||||
|
||||
COPY --from=installer --chown=captain:plane /app/apps/space/.next ./apps/space/.next
|
||||
|
||||
ARG NEXT_PUBLIC_API_BASE_URL=http://localhost:8000
|
||||
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \
|
||||
BUILT_NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
|
||||
|
||||
USER root
|
||||
COPY replace-env-vars.sh /usr/local/bin/
|
||||
COPY start.sh /usr/local/bin/
|
||||
RUN chmod +x /usr/local/bin/replace-env-vars.sh
|
||||
RUN chmod +x /usr/local/bin/start.sh
|
||||
|
||||
USER captain
|
||||
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
|
||||
EXPOSE 3000
|
||||
+49
-54
@@ -1,61 +1,56 @@
|
||||
FROM node:18-alpine AS builder
|
||||
RUN apk add --no-cache libc6-compat
|
||||
WORKDIR /app
|
||||
FROM node:18-alpine AS base
|
||||
|
||||
# Install dependencies only when needed
|
||||
FROM base AS deps
|
||||
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
|
||||
RUN apk add --no-cache libc6-compat
|
||||
WORKDIR /space
|
||||
|
||||
# Install dependencies based on the preferred package manager
|
||||
COPY package.json yarn.lock* ./
|
||||
RUN yarn
|
||||
|
||||
|
||||
# Rebuild the source code only when needed
|
||||
FROM base AS builder
|
||||
WORKDIR /space
|
||||
|
||||
RUN yarn global add turbo
|
||||
COPY . .
|
||||
|
||||
RUN turbo prune --scope=space --docker
|
||||
|
||||
FROM node:18-alpine AS installer
|
||||
|
||||
RUN apk add --no-cache libc6-compat
|
||||
WORKDIR /app
|
||||
|
||||
COPY .gitignore .gitignore
|
||||
COPY --from=builder /app/out/json/ .
|
||||
COPY --from=builder /app/out/yarn.lock ./yarn.lock
|
||||
RUN yarn install --network-timeout 500000
|
||||
|
||||
COPY --from=builder /app/out/full/ .
|
||||
COPY turbo.json turbo.json
|
||||
USER root
|
||||
|
||||
ARG NEXT_PUBLIC_API_BASE_URL=""
|
||||
ARG NEXT_PUBLIC_DEPLOY_WITH_NGINX=1
|
||||
|
||||
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
|
||||
|
||||
FROM node:18-alpine AS runner
|
||||
WORKDIR /app
|
||||
|
||||
RUN addgroup --system --gid 1001 plane
|
||||
RUN adduser --system --uid 1001 captain
|
||||
USER captain
|
||||
|
||||
COPY --from=installer /app/space/next.config.js .
|
||||
COPY --from=installer /app/space/package.json .
|
||||
|
||||
COPY --from=installer --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/public ./space/public
|
||||
|
||||
ARG NEXT_PUBLIC_API_BASE_URL=""
|
||||
ARG NEXT_PUBLIC_DEPLOY_WITH_NGINX=1
|
||||
|
||||
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
|
||||
ENV NEXT_PUBLIC_DEPLOY_WITH_NGINX=$NEXT_PUBLIC_DEPLOY_WITH_NGINX
|
||||
|
||||
USER root
|
||||
COPY start.sh /usr/local/bin/
|
||||
RUN chmod +x /usr/local/bin/start.sh
|
||||
|
||||
USER captain
|
||||
COPY --from=deps /space/node_modules ./node_modules
|
||||
COPY --from=deps /space/yarn.lock .
|
||||
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
|
||||
RUN yarn build
|
||||
|
||||
# If using npm comment out above and use below instead
|
||||
# RUN npm run build
|
||||
|
||||
# Production image, copy all the files and run next
|
||||
FROM base AS runner
|
||||
WORKDIR /space
|
||||
|
||||
ENV NODE_ENV production
|
||||
# Uncomment the following line in case you want to disable telemetry during runtime.
|
||||
# ENV NEXT_TELEMETRY_DISABLED 1
|
||||
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 nextjs
|
||||
|
||||
COPY --from=builder /space/public ./public
|
||||
|
||||
# Automatically leverage output traces to reduce image size
|
||||
# https://nextjs.org/docs/advanced-features/output-file-tracing
|
||||
COPY --from=builder --chown=nextjs:nodejs /space/.next/standalone ./
|
||||
COPY --from=builder --chown=nextjs:nodejs /space/.next/static ./.next/static
|
||||
|
||||
USER nextjs
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
ENV PORT 3000
|
||||
# set hostname to localhost
|
||||
ENV HOSTNAME "0.0.0.0"
|
||||
|
||||
CMD ["node", "space/server.js"]
|
||||
Vendored
-2
@@ -1,2 +0,0 @@
|
||||
// additional.d.ts
|
||||
/// <reference types="next-images" />
|
||||
@@ -0,0 +1,66 @@
|
||||
// next imports
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { Metadata, ResolvingMetadata } from "next";
|
||||
// components
|
||||
import IssueNavbar from "components/issues/navbar";
|
||||
import IssueFilter from "components/issues/filters-render";
|
||||
// service
|
||||
import ProjectService from "services/project.service";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
type LayoutProps = {
|
||||
params: { workspace_slug: string; project_slug: string };
|
||||
};
|
||||
|
||||
export async function generateMetadata({ params }: LayoutProps): Promise<Metadata> {
|
||||
// read route params
|
||||
const { workspace_slug, project_slug } = params;
|
||||
const projectServiceInstance = new ProjectService();
|
||||
|
||||
try {
|
||||
const project = await projectServiceInstance?.getProjectSettingsAsync(workspace_slug, project_slug);
|
||||
|
||||
return {
|
||||
title: `${project?.project_details?.name} | ${workspace_slug}`,
|
||||
description: `${
|
||||
project?.project_details?.description || `${project?.project_details?.name} | ${workspace_slug}`
|
||||
}`,
|
||||
icons: `data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>${
|
||||
typeof project?.project_details?.emoji != "object"
|
||||
? String.fromCodePoint(parseInt(project?.project_details?.emoji))
|
||||
: "✈️"
|
||||
}</text></svg>`,
|
||||
};
|
||||
} catch (error: any) {
|
||||
if (error?.data?.error) {
|
||||
redirect(`/project-not-published`);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
const RootLayout = ({ children }: { children: React.ReactNode }) => (
|
||||
<div className="relative w-screen min-h-[500px] h-screen overflow-hidden flex flex-col">
|
||||
<div className="flex-shrink-0 h-[60px] border-b border-gray-300 relative flex items-center bg-white select-none">
|
||||
<IssueNavbar />
|
||||
</div>
|
||||
{/* <div className="flex-shrink-0 min-h-[50px] h-auto py-1.5 border-b border-gray-300 relative flex items-center shadow-md bg-white select-none">
|
||||
<IssueFilter />
|
||||
</div> */}
|
||||
<div className="w-full h-full relative bg-gray-100/50 overflow-hidden">{children}</div>
|
||||
|
||||
<div className="absolute z-[99999] bottom-[10px] right-[10px] bg-white rounded-sm shadow-lg border border-gray-100">
|
||||
<Link href="https://plane.so" className="p-1 px-2 flex items-center gap-1" target="_blank">
|
||||
<div className="w-[24px] h-[24px] relative flex justify-center items-center">
|
||||
<Image src="/plane-logo.webp" alt="plane logo" className="w-[24px] h-[24px]" height="24" width="24" />
|
||||
</div>
|
||||
<div className="text-xs">
|
||||
Powered by <b>Plane Deploy</b>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default RootLayout;
|
||||
@@ -0,0 +1,113 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
// next imports
|
||||
import { useRouter, useParams, useSearchParams } from "next/navigation";
|
||||
// mobx
|
||||
import { observer } from "mobx-react-lite";
|
||||
// components
|
||||
import { IssueListView } from "components/issues/board-views/list";
|
||||
import { IssueKanbanView } from "components/issues/board-views/kanban";
|
||||
import { IssueCalendarView } from "components/issues/board-views/calendar";
|
||||
import { IssueSpreadsheetView } from "components/issues/board-views/spreadsheet";
|
||||
import { IssueGanttView } from "components/issues/board-views/gantt";
|
||||
// mobx store
|
||||
import { RootStore } from "store/root";
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// types
|
||||
import { TIssueBoardKeys } from "store/types";
|
||||
|
||||
const WorkspaceProjectPage = observer(() => {
|
||||
const store: RootStore = useMobxStore();
|
||||
|
||||
const router = useRouter();
|
||||
const routerParams = useParams();
|
||||
const routerSearchparams = useSearchParams();
|
||||
|
||||
const { workspace_slug, project_slug } = routerParams as { workspace_slug: string; project_slug: string };
|
||||
const board =
|
||||
routerSearchparams &&
|
||||
routerSearchparams.get("board") != null &&
|
||||
(routerSearchparams.get("board") as TIssueBoardKeys | "");
|
||||
|
||||
// updating default board view when we are in the issues page
|
||||
useEffect(() => {
|
||||
if (workspace_slug && project_slug && store?.project?.workspaceProjectSettings) {
|
||||
const workspacePRojectSettingViews = store?.project?.workspaceProjectSettings?.views;
|
||||
const userAccessViews: TIssueBoardKeys[] = [];
|
||||
|
||||
Object.keys(workspacePRojectSettingViews).filter((_key) => {
|
||||
if (_key === "list" && workspacePRojectSettingViews.list === true) userAccessViews.push(_key);
|
||||
if (_key === "kanban" && workspacePRojectSettingViews.kanban === true) userAccessViews.push(_key);
|
||||
if (_key === "calendar" && workspacePRojectSettingViews.calendar === true) userAccessViews.push(_key);
|
||||
if (_key === "spreadsheet" && workspacePRojectSettingViews.spreadsheet === true) userAccessViews.push(_key);
|
||||
if (_key === "gantt" && workspacePRojectSettingViews.gantt === true) userAccessViews.push(_key);
|
||||
});
|
||||
|
||||
if (userAccessViews && userAccessViews.length > 0) {
|
||||
if (!board) {
|
||||
store.issue.setCurrentIssueBoardView(userAccessViews[0]);
|
||||
router.replace(`/${workspace_slug}/${project_slug}?board=${userAccessViews[0]}`);
|
||||
} else {
|
||||
if (userAccessViews.includes(board)) {
|
||||
if (store.issue.currentIssueBoardView === null) store.issue.setCurrentIssueBoardView(board);
|
||||
else {
|
||||
if (board === store.issue.currentIssueBoardView)
|
||||
router.replace(`/${workspace_slug}/${project_slug}?board=${board}`);
|
||||
else {
|
||||
store.issue.setCurrentIssueBoardView(board);
|
||||
router.replace(`/${workspace_slug}/${project_slug}?board=${board}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
store.issue.setCurrentIssueBoardView(userAccessViews[0]);
|
||||
router.replace(`/${workspace_slug}/${project_slug}?board=${userAccessViews[0]}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [workspace_slug, project_slug, board, router, store?.issue, store?.project?.workspaceProjectSettings]);
|
||||
|
||||
useEffect(() => {
|
||||
if (workspace_slug && project_slug) {
|
||||
store?.project?.getProjectSettingsAsync(workspace_slug, project_slug);
|
||||
store?.issue?.getIssuesAsync(workspace_slug, project_slug);
|
||||
}
|
||||
}, [workspace_slug, project_slug, store?.project, store?.issue]);
|
||||
|
||||
return (
|
||||
<div className="relative w-full h-full overflow-hidden">
|
||||
{store?.issue?.loader && !store.issue.issues ? (
|
||||
<div className="text-sm text-center py-10 text-gray-500">Loading...</div>
|
||||
) : (
|
||||
<>
|
||||
{store?.issue?.error ? (
|
||||
<div className="text-sm text-center py-10 text-gray-500">Something went wrong.</div>
|
||||
) : (
|
||||
store?.issue?.currentIssueBoardView && (
|
||||
<>
|
||||
{store?.issue?.currentIssueBoardView === "list" && (
|
||||
<div className="relative w-full h-full overflow-y-auto">
|
||||
<div className="container mx-auto px-5 py-3">
|
||||
<IssueListView />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{store?.issue?.currentIssueBoardView === "kanban" && (
|
||||
<div className="relative w-full h-full mx-auto px-5">
|
||||
<IssueKanbanView />
|
||||
</div>
|
||||
)}
|
||||
{store?.issue?.currentIssueBoardView === "calendar" && <IssueCalendarView />}
|
||||
{store?.issue?.currentIssueBoardView === "spreadsheet" && <IssueSpreadsheetView />}
|
||||
{store?.issue?.currentIssueBoardView === "gantt" && <IssueGanttView />}
|
||||
</>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default WorkspaceProjectPage;
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
const WorkspaceProjectPage = () => (
|
||||
<div className="relative w-screen h-screen flex justify-center items-center text-5xl">Plane Workspace Space</div>
|
||||
);
|
||||
@@ -0,0 +1,20 @@
|
||||
"use client";
|
||||
|
||||
// root styles
|
||||
import "styles/globals.css";
|
||||
// mobx store provider
|
||||
import { MobxStoreProvider } from "lib/mobx/store-provider";
|
||||
import MobxStoreInit from "lib/mobx/store-init";
|
||||
|
||||
const RootLayout = ({ children }: { children: React.ReactNode }) => (
|
||||
<html lang="en">
|
||||
<body className="antialiased w-100">
|
||||
<MobxStoreProvider>
|
||||
<MobxStoreInit />
|
||||
<main>{children}</main>
|
||||
</MobxStoreProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
export default RootLayout;
|
||||
@@ -0,0 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
|
||||
const HomePage = () => (
|
||||
<div className="relative w-screen h-screen flex justify-center items-center text-5xl">Plane Deploy</div>
|
||||
);
|
||||
|
||||
export default HomePage;
|
||||
+1
-2
@@ -1,13 +1,12 @@
|
||||
// next imports
|
||||
import Image from "next/image";
|
||||
import projectNotPublishedImage from "public/project-not-published.svg";
|
||||
|
||||
const CustomProjectNotPublishedError = () => (
|
||||
<div className="relative w-screen min-h-screen h-full flex justify-center items-center py-5">
|
||||
<div className="max-w-[700px] space-y-5">
|
||||
<div className="flex items-center flex-col gap-3 text-center">
|
||||
<div className="relative w-[240px] h-[240px]">
|
||||
<Image src={projectNotPublishedImage} layout="fill" alt="404- Page not found" />
|
||||
<Image src={`/project-not-published.svg`} layout="fill" alt="404- Page not found" />
|
||||
</div>
|
||||
<div className="text-xl font-medium">
|
||||
Oops! The page you{`'`}re looking for isn{`'`}t live at the moment.
|
||||
@@ -1,216 +0,0 @@
|
||||
import React, { useEffect, useState, useCallback } from "react";
|
||||
|
||||
// react hook form
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
// services
|
||||
import authenticationService from "services/authentication.service";
|
||||
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
import useTimer from "hooks/use-timer";
|
||||
|
||||
// ui
|
||||
import { Input, PrimaryButton } from "components/ui";
|
||||
|
||||
// types
|
||||
type EmailCodeFormValues = {
|
||||
email: string;
|
||||
key?: string;
|
||||
token?: string;
|
||||
};
|
||||
|
||||
export const EmailCodeForm = ({ handleSignIn }: any) => {
|
||||
const [codeSent, setCodeSent] = useState(false);
|
||||
const [codeResent, setCodeResent] = useState(false);
|
||||
const [isCodeResending, setIsCodeResending] = useState(false);
|
||||
const [errorResendingCode, setErrorResendingCode] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
const { timer: resendCodeTimer, setTimer: setResendCodeTimer } = useTimer();
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setError,
|
||||
setValue,
|
||||
getValues,
|
||||
watch,
|
||||
formState: { errors, isSubmitting, isValid, isDirty },
|
||||
} = useForm<EmailCodeFormValues>({
|
||||
defaultValues: {
|
||||
email: "",
|
||||
key: "",
|
||||
token: "",
|
||||
},
|
||||
mode: "onChange",
|
||||
reValidateMode: "onChange",
|
||||
});
|
||||
|
||||
const isResendDisabled = resendCodeTimer > 0 || isCodeResending || isSubmitting || errorResendingCode;
|
||||
|
||||
const onSubmit = useCallback(
|
||||
async ({ email }: EmailCodeFormValues) => {
|
||||
setErrorResendingCode(false);
|
||||
await authenticationService
|
||||
.emailCode({ email })
|
||||
.then((res) => {
|
||||
setValue("key", res.key);
|
||||
setCodeSent(true);
|
||||
})
|
||||
.catch((err) => {
|
||||
setErrorResendingCode(true);
|
||||
setToastAlert({
|
||||
title: "Oops!",
|
||||
type: "error",
|
||||
message: err?.error,
|
||||
});
|
||||
});
|
||||
},
|
||||
[setToastAlert, setValue]
|
||||
);
|
||||
|
||||
const handleSignin = async (formData: EmailCodeFormValues) => {
|
||||
setIsLoading(true);
|
||||
await authenticationService
|
||||
.magicSignIn(formData)
|
||||
.then((response) => {
|
||||
setIsLoading(false);
|
||||
handleSignIn(response);
|
||||
})
|
||||
.catch((error) => {
|
||||
setIsLoading(false);
|
||||
setToastAlert({
|
||||
title: "Oops!",
|
||||
type: "error",
|
||||
message: error?.response?.data?.error ?? "Enter the correct code to sign in",
|
||||
});
|
||||
setError("token" as keyof EmailCodeFormValues, {
|
||||
type: "manual",
|
||||
message: error?.error,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const emailOld = getValues("email");
|
||||
|
||||
useEffect(() => {
|
||||
setErrorResendingCode(false);
|
||||
}, [emailOld]);
|
||||
|
||||
useEffect(() => {
|
||||
const submitForm = (e: KeyboardEvent) => {
|
||||
if (!codeSent && e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
handleSubmit(onSubmit)().then(() => {
|
||||
setResendCodeTimer(30);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (!codeSent) {
|
||||
window.addEventListener("keydown", submitForm);
|
||||
}
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("keydown", submitForm);
|
||||
};
|
||||
}, [handleSubmit, codeSent, onSubmit, setResendCodeTimer]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{(codeSent || codeResent) && (
|
||||
<p className="text-center mt-4">
|
||||
We have sent the sign in code.
|
||||
<br />
|
||||
Please check your inbox at <span className="font-medium">{watch("email")}</span>
|
||||
</p>
|
||||
)}
|
||||
<form className="space-y-4 mt-10 sm:w-[360px] mx-auto">
|
||||
<div className="space-y-1">
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="Enter your email address..."
|
||||
className="border-custom-border-300 h-[46px]"
|
||||
{...register("email", {
|
||||
required: "Email address is required",
|
||||
validate: (value) =>
|
||||
/^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test(
|
||||
value
|
||||
) || "Email address is not valid",
|
||||
})}
|
||||
/>
|
||||
{errors.email && <div className="text-sm text-red-500">{errors.email.message}</div>}
|
||||
</div>
|
||||
|
||||
{codeSent && (
|
||||
<>
|
||||
<Input
|
||||
id="token"
|
||||
type="token"
|
||||
{...register("token", {
|
||||
required: "Code is required",
|
||||
})}
|
||||
placeholder="Enter code..."
|
||||
className="border-custom-border-300 h-[46px]"
|
||||
/>
|
||||
{errors.token && <div className="text-sm text-red-500">{errors.token.message}</div>}
|
||||
<button
|
||||
type="button"
|
||||
className={`flex w-full justify-end text-xs outline-none ${
|
||||
isResendDisabled ? "cursor-default text-custom-text-200" : "cursor-pointer text-custom-primary-100"
|
||||
} `}
|
||||
onClick={() => {
|
||||
setIsCodeResending(true);
|
||||
onSubmit({ email: getValues("email") }).then(() => {
|
||||
setCodeResent(true);
|
||||
setIsCodeResending(false);
|
||||
setResendCodeTimer(30);
|
||||
});
|
||||
}}
|
||||
disabled={isResendDisabled}
|
||||
>
|
||||
{resendCodeTimer > 0 ? (
|
||||
<span className="text-right">Request new code in {resendCodeTimer} seconds</span>
|
||||
) : isCodeResending ? (
|
||||
"Sending new code..."
|
||||
) : errorResendingCode ? (
|
||||
"Please try again later"
|
||||
) : (
|
||||
<span className="font-medium">Resend code</span>
|
||||
)}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
{codeSent ? (
|
||||
<PrimaryButton
|
||||
type="submit"
|
||||
className="w-full text-center h-[46px]"
|
||||
size="md"
|
||||
onClick={handleSubmit(handleSignin)}
|
||||
disabled={!isValid && isDirty}
|
||||
loading={isLoading}
|
||||
>
|
||||
{isLoading ? "Signing in..." : "Sign in"}
|
||||
</PrimaryButton>
|
||||
) : (
|
||||
<PrimaryButton
|
||||
className="w-full text-center h-[46px]"
|
||||
size="md"
|
||||
onClick={() => {
|
||||
handleSubmit(onSubmit)().then(() => {
|
||||
setResendCodeTimer(30);
|
||||
});
|
||||
}}
|
||||
disabled={!isValid && isDirty}
|
||||
loading={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? "Sending code..." : "Send sign in code"}
|
||||
</PrimaryButton>
|
||||
)}
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,113 +0,0 @@
|
||||
import React, { useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import Link from "next/link";
|
||||
import { useForm } from "react-hook-form";
|
||||
// components
|
||||
import { EmailResetPasswordForm } from "./email-reset-password-form";
|
||||
// ui
|
||||
import { Input, PrimaryButton } from "components/ui";
|
||||
// types
|
||||
type EmailPasswordFormValues = {
|
||||
email: string;
|
||||
password?: string;
|
||||
medium?: string;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
onSubmit: (formData: EmailPasswordFormValues) => Promise<void>;
|
||||
};
|
||||
|
||||
export const EmailPasswordForm: React.FC<Props> = ({ onSubmit }) => {
|
||||
const [isResettingPassword, setIsResettingPassword] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const isSignUpPage = router.pathname === "/sign-up";
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors, isSubmitting, isValid, isDirty },
|
||||
} = useForm<EmailPasswordFormValues>({
|
||||
defaultValues: {
|
||||
email: "",
|
||||
password: "",
|
||||
medium: "email",
|
||||
},
|
||||
mode: "onChange",
|
||||
reValidateMode: "onChange",
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1 className="text-center text-2xl sm:text-2.5xl font-semibold text-custom-text-100">
|
||||
{isResettingPassword ? "Reset your password" : isSignUpPage ? "Sign up on Plane" : "Sign in to Plane"}
|
||||
</h1>
|
||||
{isResettingPassword ? (
|
||||
<EmailResetPasswordForm setIsResettingPassword={setIsResettingPassword} />
|
||||
) : (
|
||||
<form className="space-y-4 mt-10 w-full sm:w-[360px] mx-auto" onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="space-y-1">
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
{...register("email", {
|
||||
required: "Email address is required",
|
||||
validate: (value) =>
|
||||
/^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test(
|
||||
value
|
||||
) || "Email address is not valid",
|
||||
})}
|
||||
placeholder="Enter your email address..."
|
||||
className="border-custom-border-300 h-[46px]"
|
||||
/>
|
||||
{errors.email && <div className="text-sm text-red-500">{errors.email.message}</div>}
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
{...register("password", {
|
||||
required: "Password is required",
|
||||
})}
|
||||
placeholder="Enter your password..."
|
||||
className="border-custom-border-300 h-[46px]"
|
||||
/>
|
||||
{errors.password && <div className="text-sm text-red-500">{errors.password.message}</div>}
|
||||
</div>
|
||||
<div className="text-right text-xs">
|
||||
{isSignUpPage ? (
|
||||
<Link href="/">
|
||||
<a className="text-custom-text-200 hover:text-custom-primary-100">Already have an account? Sign in.</a>
|
||||
</Link>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsResettingPassword(true)}
|
||||
className="text-custom-text-200 hover:text-custom-primary-100"
|
||||
>
|
||||
Forgot your password?
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<PrimaryButton
|
||||
type="submit"
|
||||
className="w-full text-center h-[46px]"
|
||||
disabled={!isValid && isDirty}
|
||||
loading={isSubmitting}
|
||||
>
|
||||
{isSignUpPage ? (isSubmitting ? "Signing up..." : "Sign up") : isSubmitting ? "Signing in..." : "Sign in"}
|
||||
</PrimaryButton>
|
||||
{!isSignUpPage && (
|
||||
<Link href="/sign-up">
|
||||
<a className="block text-custom-text-200 hover:text-custom-primary-100 text-xs mt-4">
|
||||
Don{"'"}t have an account? Sign up.
|
||||
</a>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,89 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
// react hook form
|
||||
import { useForm } from "react-hook-form";
|
||||
// services
|
||||
import userService from "services/user.service";
|
||||
// hooks
|
||||
// import useToast from "hooks/use-toast";
|
||||
// ui
|
||||
import { Input, PrimaryButton, SecondaryButton } from "components/ui";
|
||||
// types
|
||||
type Props = {
|
||||
setIsResettingPassword: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
};
|
||||
|
||||
export const EmailResetPasswordForm: React.FC<Props> = ({ setIsResettingPassword }) => {
|
||||
// const { setToastAlert } = useToast();
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors, isSubmitting },
|
||||
} = useForm({
|
||||
defaultValues: {
|
||||
email: "",
|
||||
},
|
||||
mode: "onChange",
|
||||
reValidateMode: "onChange",
|
||||
});
|
||||
|
||||
const forgotPassword = async (formData: any) => {
|
||||
const payload = {
|
||||
email: formData.email,
|
||||
};
|
||||
|
||||
// await userService
|
||||
// .forgotPassword(payload)
|
||||
// .then(() =>
|
||||
// setToastAlert({
|
||||
// type: "success",
|
||||
// title: "Success!",
|
||||
// message: "Password reset link has been sent to your email address.",
|
||||
// })
|
||||
// )
|
||||
// .catch((err) => {
|
||||
// if (err.status === 400)
|
||||
// setToastAlert({
|
||||
// type: "error",
|
||||
// title: "Error!",
|
||||
// message: "Please check the Email ID entered.",
|
||||
// });
|
||||
// else
|
||||
// setToastAlert({
|
||||
// type: "error",
|
||||
// title: "Error!",
|
||||
// message: "Something went wrong. Please try again.",
|
||||
// });
|
||||
// });
|
||||
};
|
||||
|
||||
return (
|
||||
<form className="space-y-4 mt-10 w-full sm:w-[360px] mx-auto" onSubmit={handleSubmit(forgotPassword)}>
|
||||
<div className="space-y-1">
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
{...register("email", {
|
||||
required: "Email address is required",
|
||||
validate: (value) =>
|
||||
/^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test(
|
||||
value
|
||||
) || "Email address is not valid",
|
||||
})}
|
||||
placeholder="Enter registered email address.."
|
||||
className="border-custom-border-300 h-[46px]"
|
||||
/>
|
||||
{errors.email && <div className="text-sm text-red-500">{errors.email.message}</div>}
|
||||
</div>
|
||||
<div className="mt-5 flex flex-col-reverse sm:flex-row items-center gap-2">
|
||||
<SecondaryButton className="w-full text-center h-[46px]" onClick={() => setIsResettingPassword(false)}>
|
||||
Go Back
|
||||
</SecondaryButton>
|
||||
<PrimaryButton type="submit" className="w-full text-center h-[46px]" loading={isSubmitting}>
|
||||
{isSubmitting ? "Sending link..." : "Send reset link"}
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -1,55 +0,0 @@
|
||||
import { useEffect, useState, FC } from "react";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/router";
|
||||
// next-themes
|
||||
import { useTheme } from "next-themes";
|
||||
// images
|
||||
import githubBlackImage from "public/logos/github-black.svg";
|
||||
import githubWhiteImage from "public/logos/github-white.svg";
|
||||
|
||||
export interface GithubLoginButtonProps {
|
||||
handleSignIn: React.Dispatch<string>;
|
||||
}
|
||||
|
||||
export const GithubLoginButton: FC<GithubLoginButtonProps> = ({ handleSignIn }) => {
|
||||
const [loginCallBackURL, setLoginCallBackURL] = useState(undefined);
|
||||
const [gitCode, setGitCode] = useState<null | string>(null);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const { code } = router.query;
|
||||
|
||||
const { theme } = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
if (code && !gitCode) {
|
||||
setGitCode(code.toString());
|
||||
handleSignIn(code.toString());
|
||||
}
|
||||
}, [code, gitCode, handleSignIn]);
|
||||
|
||||
useEffect(() => {
|
||||
const origin = typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
|
||||
setLoginCallBackURL(`${origin}/` as any);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="w-full flex justify-center items-center">
|
||||
<Link
|
||||
className="w-full"
|
||||
href={`https://github.com/login/oauth/authorize?client_id=${process.env.NEXT_PUBLIC_GITHUB_ID}&redirect_uri=${loginCallBackURL}&scope=read:user,user:email`}
|
||||
>
|
||||
<button className="flex w-full items-center justify-center gap-2 rounded border border-custom-border-300 p-2 text-sm font-medium text-custom-text-100 duration-300 hover:bg-custom-background-80 h-[46px]">
|
||||
<Image
|
||||
src={theme === "dark" ? githubWhiteImage : githubBlackImage}
|
||||
height={20}
|
||||
width={20}
|
||||
alt="GitHub Logo"
|
||||
/>
|
||||
<span>Sign in with GitHub</span>
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,59 +0,0 @@
|
||||
import { FC, CSSProperties, useEffect, useRef, useCallback, useState } from "react";
|
||||
|
||||
import Script from "next/script";
|
||||
|
||||
export interface IGoogleLoginButton {
|
||||
text?: string;
|
||||
handleSignIn: React.Dispatch<any>;
|
||||
styles?: CSSProperties;
|
||||
}
|
||||
|
||||
export const GoogleLoginButton: FC<IGoogleLoginButton> = ({ handleSignIn }) => {
|
||||
const googleSignInButton = useRef<HTMLDivElement>(null);
|
||||
const [gsiScriptLoaded, setGsiScriptLoaded] = useState(false);
|
||||
|
||||
const loadScript = useCallback(() => {
|
||||
if (!googleSignInButton.current || gsiScriptLoaded) return;
|
||||
|
||||
(window as any)?.google?.accounts.id.initialize({
|
||||
client_id: process.env.NEXT_PUBLIC_GOOGLE_CLIENTID || "",
|
||||
callback: handleSignIn,
|
||||
});
|
||||
|
||||
try {
|
||||
(window as any)?.google?.accounts.id.renderButton(
|
||||
googleSignInButton.current,
|
||||
{
|
||||
type: "standard",
|
||||
theme: "outline",
|
||||
size: "large",
|
||||
logo_alignment: "center",
|
||||
width: 360,
|
||||
text: "signin_with",
|
||||
} as any // customization attributes
|
||||
);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
|
||||
(window as any)?.google?.accounts.id.prompt(); // also display the One Tap dialog
|
||||
|
||||
setGsiScriptLoaded(true);
|
||||
}, [handleSignIn, gsiScriptLoaded]);
|
||||
|
||||
useEffect(() => {
|
||||
if ((window as any)?.google?.accounts?.id) {
|
||||
loadScript();
|
||||
}
|
||||
return () => {
|
||||
(window as any)?.google?.accounts.id.cancel();
|
||||
};
|
||||
}, [loadScript]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Script src="https://accounts.google.com/gsi/client" async defer onLoad={loadScript} />
|
||||
<div className="overflow-hidden rounded w-full" id="googleSignInButton" ref={googleSignInButton} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,8 +0,0 @@
|
||||
export * from "./email-code-form";
|
||||
export * from "./email-password-form";
|
||||
export * from "./email-reset-password-form";
|
||||
export * from "./github-login-button";
|
||||
export * from "./google-login";
|
||||
export * from "./onboarding-form";
|
||||
export * from "./sign-in";
|
||||
export * from "./user-logged-in";
|
||||
@@ -1,187 +0,0 @@
|
||||
import { useEffect, Fragment } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
// react-hook-form
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
// mobx
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// headless ui
|
||||
import { Listbox, Transition } from "@headlessui/react";
|
||||
// icons
|
||||
import { ChevronDownIcon, CheckIcon } from "@heroicons/react/20/solid";
|
||||
// constants
|
||||
import { USER_ROLES } from "constants/workspace";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// services
|
||||
import UserService from "services/user.service";
|
||||
// ui
|
||||
import { Input, PrimaryButton } from "components/ui";
|
||||
|
||||
const defaultValues = {
|
||||
first_name: "",
|
||||
last_name: "",
|
||||
role: "",
|
||||
};
|
||||
|
||||
type Props = {
|
||||
user?: any;
|
||||
};
|
||||
|
||||
export const OnBoardingForm: React.FC<Props> = observer(({ user }) => {
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const router = useRouter();
|
||||
const { next_path } = router.query;
|
||||
|
||||
const { user: userStore } = useMobxStore();
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
control,
|
||||
reset,
|
||||
formState: { errors, isSubmitting, isValid },
|
||||
} = useForm({
|
||||
defaultValues,
|
||||
});
|
||||
|
||||
const onSubmit = async (formData: any) => {
|
||||
const payload = {
|
||||
...formData,
|
||||
onboarding_step: {
|
||||
...user.onboarding_step,
|
||||
profile_complete: true,
|
||||
},
|
||||
};
|
||||
|
||||
const userService = new UserService();
|
||||
|
||||
await userService
|
||||
.updateMe(payload)
|
||||
.then((response) => {
|
||||
userStore.setCurrentUser(response);
|
||||
router.push(next_path?.toString() || "/");
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Success!",
|
||||
message: "Details updated successfully.",
|
||||
});
|
||||
})
|
||||
.catch((err) => {});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
reset({
|
||||
first_name: user.first_name,
|
||||
last_name: user.last_name,
|
||||
role: user.role,
|
||||
});
|
||||
}
|
||||
}, [user, reset]);
|
||||
|
||||
return (
|
||||
<form
|
||||
className="h-full w-full space-y-7 sm:space-y-10 overflow-y-auto sm:flex sm:flex-col sm:items-start sm:justify-center"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="relative sm:text-lg">
|
||||
<div className="text-gray-800 absolute -top-1 -left-3">{'"'}</div>
|
||||
<h5>Hey there 👋🏻</h5>
|
||||
<h5 className="mt-5 mb-6">Let{"'"}s get you onboard!</h5>
|
||||
<h4 className="text-xl sm:text-2xl font-semibold">Set up your Plane profile.</h4>
|
||||
</div>
|
||||
|
||||
<div className="space-y-7 sm:w-3/4 md:w-2/5">
|
||||
<div className="space-y-1 text-sm">
|
||||
<label htmlFor="firstName">First Name</label>
|
||||
<Input
|
||||
id="firstName"
|
||||
autoComplete="off"
|
||||
placeholder="Enter your first name..."
|
||||
{...register("first_name", {
|
||||
required: "First name is required",
|
||||
})}
|
||||
/>
|
||||
{errors.first_name && <div className="text-sm text-red-500">{errors.first_name.message}</div>}
|
||||
</div>
|
||||
<div className="space-y-1 text-sm">
|
||||
<label htmlFor="lastName">Last Name</label>
|
||||
<Input
|
||||
id="lastName"
|
||||
autoComplete="off"
|
||||
placeholder="Enter your last name..."
|
||||
{...register("last_name", {
|
||||
required: "Last name is required",
|
||||
})}
|
||||
/>
|
||||
{errors.last_name && <div className="text-sm text-red-500">{errors.last_name.message}</div>}
|
||||
</div>
|
||||
<div className="space-y-1 text-sm">
|
||||
<span>What{"'"}s your role?</span>
|
||||
<div className="w-full">
|
||||
<Controller
|
||||
name="role"
|
||||
control={control}
|
||||
rules={{ required: "This field is required" }}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<Listbox as="div" value={value} onChange={onChange} className="relative flex-shrink-0 text-left">
|
||||
<Listbox.Button
|
||||
type="button"
|
||||
className={`flex items-center justify-between gap-1 w-full rounded-md border border-custom-border-300 shadow-sm duration-300 focus:outline-none px-3 py-2 text-sm`}
|
||||
>
|
||||
<span className={value ? "" : "text-custom-text-400"}>{value || "Select your role..."}</span>
|
||||
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
|
||||
</Listbox.Button>
|
||||
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Listbox.Options
|
||||
className={`absolute z-10 border border-custom-border-300 mt-1 overflow-y-auto rounded-md bg-custom-background-90 text-xs shadow-lg focus:outline-none w-full max-h-36 left-0 origin-top-left`}
|
||||
>
|
||||
<div className="space-y-1 p-2">
|
||||
{USER_ROLES.map((role) => (
|
||||
<Listbox.Option
|
||||
key={role.value}
|
||||
value={role.value}
|
||||
className={({ active, selected }) =>
|
||||
`cursor-pointer select-none truncate rounded px-1 py-1.5 ${
|
||||
active || selected ? "bg-custom-background-80" : ""
|
||||
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
|
||||
}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{role.label}</span>
|
||||
</div>
|
||||
{selected && <CheckIcon className="h-4 w-4 flex-shrink-0" />}
|
||||
</div>
|
||||
)}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</div>
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
</Listbox>
|
||||
)}
|
||||
/>
|
||||
{errors.role && <span className="text-sm text-red-500">{errors.role.message}</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<PrimaryButton type="submit" size="md" disabled={!isValid} loading={isSubmitting}>
|
||||
{isSubmitting ? "Updating..." : "Continue"}
|
||||
</PrimaryButton>
|
||||
</form>
|
||||
);
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user