Compare commits

..
Author SHA1 Message Date
pablohashescobar 7dc6800444 dev: remove triage states from public boards 2023-08-31 13:47:32 +05:30
pablohashescobar cf706b7977 dev: optimize public issues list endpoint 2023-08-30 19:01:09 +05:30
pablohashescobar a50cf266bd chore: return issue votes in public issue list endpoint 2023-08-30 18:43:24 +05:30
Aaryan KhandelwalandGitHub 5e00ffee05 fix: bugs on the user profile page (#2018) 2023-08-30 17:28:17 +05:30
Aaryan KhandelwalandGitHub 54527cc2bb dev: revamp publish project modal (#2022)
* dev: revamp publish project modal

* chore: sidebar dropdown text
2023-08-30 17:27:49 +05:30
Bavisetti NarayanandGitHub 6c6b81bea7 chore: tracking the history of issue reactions and votes. (#2020)
* chore: tracking the issues reaction and vote history

* fix: changed the keywords for vote and reaction

* chore: added validation
2023-08-30 16:38:04 +05:30
Aaryan KhandelwalandGitHub f5a076e9a9 dev: revamp peek overview (#2021)
* dev: mobx for issues store

* refactor: peek overview component

* chore: update open issue button

* fix: issue mutation after any crud action

* chore: remove peek overview from gantt

* chore: refactor code
2023-08-30 13:26:28 +05:30
17aff1f369 fix: asset key validation (#1938)
* fix: asset key validation

* chore: asset key validation in user assets

---------

Co-authored-by: Bavisetti Narayan <narayan@Bavisettis-MacBook-Pro.local>
2023-08-30 12:20:13 +05:30
NikhilandGitHub 761a1eb41a fix: user created by stats (#2016) 2023-08-30 12:18:56 +05:30
NikhilandGitHub 426f65898b feat: user timezones (#2009)
* dev: user timezones

* feat: user timezones
2023-08-30 12:18:18 +05:30
NikhilandGitHub 23f5d5d172 chore: track public board comments and reaction users for public deploy boards (#1972)
* chore: track project deploy board comment and reaction users for public deploy boards

* dev: remove tracking from project viewsets
2023-08-30 12:15:08 +05:30
Aaryan KhandelwalandGitHub 2e5ade05fe chore: update module status icons and colors (#2011)
* chore: update module status icons and colors

* refactor: import statements

* fix: add default alue to module status
2023-08-30 11:43:47 +05:30
Aaryan KhandelwalandGitHub 168e79d6df style: revamp of the issue details sidebar (#2014) 2023-08-29 20:15:12 +05:30
Aaryan KhandelwalandGitHub d8bbdc14ac feat: access selector for comment (#2012)
* dev: access specifier for comment

* chore: change access order
2023-08-29 20:14:13 +05:30
Aaryan KhandelwalandGitHub fd0efb0242 fix: start date filter not working on the platform (#2007) 2023-08-29 20:11:38 +05:30
Aaryan KhandelwalandGitHub 38a5623c43 dev: user timezone select option (#2002) 2023-08-29 20:11:06 +05:30
NikhilandGitHub 90cf39cf59 fix: access creation in comments (#2013) 2023-08-29 16:40:28 +05:30
Bavisetti NarayanandGitHub b2a41d3bf6 fix: issue votes (#2006)
* fix: issue votes

* fix: added default as 1 in vote

* fix: issue vote migration file
2023-08-29 15:02:29 +05:30
Bavisetti NarayanandGitHub 1cf5e8d80a fix: only external comments will show in deploy boards (#2010) 2023-08-29 15:01:18 +05:30
NikhilandGitHub 1d30a9a0a8 chore: project public board issue retrieve (#2003)
* chore: project public board issue retrieve

* dev: project issues list endpoint

* fix: issue public retrieve endpoint
2023-08-29 15:00:26 +05:30
Bavisetti NarayanandGitHub 91c10930a4 feat: mark all read notifications (#1963)
* feat: mark all read notifications

* fix: changed string to boolean

* fix: changed snoozed condition
2023-08-29 14:57:27 +05:30
NikhilandGitHub 5ad5da4fd7 dev: remove gunicorn config (#1999) 2023-08-29 13:45:25 +05:30
e1ad385688 fix: issue exports in self hosted instances (#1996)
* fix: issue exports in self hosted instances

* dev: remove print logs

* dev: update url creation function

* fix: changed the presigned url for self hosted exports

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
2023-08-29 13:45:04 +05:30
NikhilandGitHub abcdebef85 fix: n+1 in issue history and issue automation tasks (#1994) 2023-08-29 13:35:36 +05:30
NikhilandGitHub 3a41ec7442 chore: update user activity endpoint to return only workspace activities (#1980) 2023-08-29 13:35:13 +05:30
8581226e60 chore: improve access field for comments for public boards (#1956)
Co-authored-by: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com>
2023-08-29 13:34:38 +05:30
sriram veeraghantaandGitHub c65bbf865d fix: tiptap editor export fixes (#2001) 2023-08-28 15:54:49 +05:30
srinivas pendemGitHubsrinivaspendem <you@example.comsrinivaspendem2612@gmail.com>
b2e5760391 bugfix: Export download next link changed to anchor (#2000)
* bugfix: Export download next link changd to anchot

* bugfix: user workspace service name update

---------

Co-authored-by: srinivaspendem <you@example.comsrinivaspendem2612@gmail.com>
2023-08-28 15:54:20 +05:30
Henit ChobisaandGitHub 8a3b65a740 [chore] Update development workflows with every PR build and removed image update on every merge (#1985)
* chore: Combined Github Workflows for Release

* chore: Added Workflows for Building and Testing Changes
2023-08-28 13:36:08 +05:30
sriram veeraghantaandGitHub 293d90ddda fix: Cycles and Modules Cards view responsiveness (#1997)
* fix: modules cards fixes

* fix:cycles responsive cards
2023-08-28 13:34:05 +05:30
Aaryan KhandelwalandGitHub 485e56bcdf fix: my profile activity endpoint (#1983)
* fix: my profile activity endpoint

* chore: update service name
2023-08-28 13:29:48 +05:30
Aaryan KhandelwalandGitHub 6e7701d854 chore: don't show completion percentage if user has no assigned issues (#1984) 2023-08-28 13:29:07 +05:30
Aaryan KhandelwalandGitHub a1acd2772e feat: mark all as read (#1982) 2023-08-28 13:26:38 +05:30
47abe9db5e dev: gantt chart revamp (#1900)
* style: gantt chart polishing

* chore: sidebar y-axis drag and drop

* chore: remove y-axis drag and drop from the main content

* refactor: drop end function

* refactor: resizing logic

* chore: x-axis block move

* chore: x-axis block move flag

* chore: update scroll end logic

* style: modules gantt chart

* style: block background tint

* refactor: context dispatcher types

* refactor: draggable component

* chore: filters added to gantt chart

* refactor: folder structure

* style: cycle blocks

* chore: move to block arrow

* chore: move to block on the right side arrow

* chore: added proper comments for functions

* refactor: blocks render logic

* fix: x-axis drag and drop

* chore: minor ui fixes

* chore: remove link tag from blocks

---------

Co-authored-by: Aaryan Khandelwal <aaryan610@Aaryans-MacBook-Pro.local>
2023-08-28 13:25:47 +05:30
sriram veeraghantaandGitHub a61e8370b5 fix: workspace accepted invitation redirects to the workspace (#1971)
* fix: workspace accepted invitation redirects to the workspace

* chore: removing logs

* fix: updating user last workspace id with newly joined one

* adding error toast
2023-08-28 13:25:09 +05:30
sriram veeraghantaandGitHub 9f420a00d7 fix: create new project as fav (#1993) 2023-08-28 13:09:27 +05:30
NikhilandGitHub a9ff4b8c93 fix: project members n+1 (#1975) 2023-08-27 20:31:32 +05:30
2b168edd99 feat: peek overview for spreadsheet issues (#1979)
* feat: peak overview for issues

* fix: peek spelling

* chore: truncate issue property labels

* style: full screen view designed

* chore: add comment section

* chore: copy link and delete options added

* chore: update icons

---------

Co-authored-by: Aaryan Khandelwal <aaryan610@Aaryans-MacBook-Pro.local>
2023-08-25 17:41:23 +05:30
NikhilandGitHub 93fa093a79 dev: update python runtime (#1981) 2023-08-25 17:23:07 +05:30
NikhilandGitHub fd8c368c97 fix: add member role and member status in project create response (#1962) 2023-08-25 15:16:15 +05:30
sriram veeraghantaandGitHub 0525e7d6b3 fix: workspace members reordering (#1978) 2023-08-25 13:38:50 +05:30
1530993b84 fix: tiptap editor max width (#1968)
Co-authored-by: Aaryan Khandelwal <aaryan610@Aaryans-MacBook-Pro.local>
2023-08-25 12:42:12 +05:30
d8b8c903f2 fix: issue activity redirection to cycle and module (#1973)
Co-authored-by: Aaryan Khandelwal <aaryan610@Aaryans-MacBook-Pro.local>
2023-08-25 12:21:11 +05:30
bf0d0503b2 fix: redirection after deleting a project (#1970)
Co-authored-by: Aaryan Khandelwal <aaryan610@Aaryans-MacBook-Pro.local>
2023-08-25 12:17:17 +05:30
Henit ChobisaandGitHub fe1b0c1d73 [chore] Fixed Github Workflows for Building and Pushing, frontend, backend, proxy & plane-deploy (#1959)
* chore: modified frontend github workflow

* chore: modified backend github workflows

* chore: added github workflow for build and push for plane-deploy

* chore: added github workflow for plane-proxy
2023-08-25 12:12:07 +05:30
ab4a17c178 chore: custom CSS shadow variables added (#1969)
* chore: custom shadow variables added

* fix: 2xs shade

---------

Co-authored-by: Aaryan Khandelwal <aaryan610@Aaryans-MacBook-Pro.local>
2023-08-24 23:07:20 +05:30
38934e8b99 chore: group by assignees option for project issues (#1957)
* dev: group by assignees option for project issues

* fix: no assignee title

---------

Co-authored-by: Aaryan Khandelwal <aaryan610@Aaryans-MacBook-Pro.local>
2023-08-24 19:46:12 +05:30
d18ac83909 feat: start date filter added across the platform (#1955)
Co-authored-by: Aaryan Khandelwal <aaryan610@Aaryans-MacBook-Pro.local>
2023-08-24 19:45:23 +05:30
sriram veeraghantaandGitHub 802e6b3e8e fix: project member mutate issue (#1967) 2023-08-24 19:43:50 +05:30
sriram veeraghantaandGitHub 489ef6a3cc Merge pull request #1966 from makeplane/fix/workspace-members-mutate
fix: workspace members mutate issue
2023-08-24 18:26:11 +05:30
sriram veeraghanta bce8cae0da fix: mutate fixes 2023-08-24 18:21:57 +05:30
sriram veeraghanta f97597958a fix: workspace memebers mutate issue 2023-08-24 17:44:20 +05:30
NikhilandGitHub 7fca01d8c9 feat: project deploy board endpoint (#1943) 2023-08-23 22:13:37 +05:30
Bavisetti NarayanandGitHub 529ab19747 chore: removed extra exporter function (#1953) 2023-08-23 22:13:04 +05:30
2d1406953e fix: mutate projects list after joining (#1944)
Co-authored-by: Aaryan Khandelwal <aaryan610@Aaryans-MacBook-Pro.local>
2023-08-23 15:44:27 +05:30
a8fdd42cb9 fix: label color select popover overflow (#1949)
Co-authored-by: Aaryan Khandelwal <aaryan610@Aaryans-MacBook-Pro.local>
2023-08-23 15:43:09 +05:30
561fb9815b chore: update export services icons (#1946)
Co-authored-by: Aaryan Khandelwal <aaryan610@Aaryans-MacBook-Pro.local>
2023-08-23 12:29:59 +05:30
Bavisetti NarayanandGitHub 2cc67f6498 fix: date validation in cycle and module (#1945) 2023-08-23 12:17:20 +05:30
NikhilandGitHub eee6658cc2 dev: deploy docker containers (#1939) 2023-08-22 19:33:29 +05:30
68b438ab1a fix: aws region changed for exporter (#1933)
Co-authored-by: Bavisetti Narayan <narayan@Bavisettis-MacBook-Pro.local>
2023-08-22 13:18:15 +05:30
b406a70e72 fix: access environment variables is changed in services (#1930)
Co-authored-by: Sainath <sainath@Sainaths-MacBook-Pro.local>
2023-08-22 01:13:51 +05:30
Aaryan KhandelwalandGitHub b02417120b chore: hide new issue button from my subscribed issues page (#1927) 2023-08-21 20:50:17 +05:30
Aaryan KhandelwalandGitHub d040394826 fix: create project button not appearing on the sidebar (#1926) 2023-08-21 20:44:51 +05:30
NikhilandGitHub f7682c57ba fix: plane space start up command (#1925) 2023-08-21 20:25:12 +05:30
guru_sainathandGitHub 9bb6254515 chore: updated default api base_url (#1922) 2023-08-21 18:17:32 +05:30
Aaryan KhandelwalandGitHub ae052f1890 chore: update restricted workspace slugs (#1920) 2023-08-21 18:13:08 +05:30
cfc7049343 Dockerrizing space project (#1921)
* chore: Added Dockerfile for Space Project

* fix: next js config to standalone mode

* fix: workedaround build error with rename 404 page

* chore: modified dockerfile with new conventions

* chore: modified dockercompose file for new plane-deploy

* fix: handled ts errors with possibly undefined states

* chore: updated main dockerfile with plane-deploy

* feat: included space project to start.sh

* chore: modified space project port while running in production

* chore: restored changes inside space project

* chore: added ngnix config for space project running :4000

* fix: Updated docker-compose files

* chore: added space url for ngnix config

* chore: Updated ngnix template

* chore: updated space url in compose hub file

* dev: updated dockerfile.space and start and replace script

* dev: equate hub and build docker files

* dev: revert workspace space page

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
2023-08-21 18:12:41 +05:30
guru_sainathandGitHub 41e55dff85 fix: build error for 404 and search params null check (#1919) 2023-08-21 12:43:41 +05:30
Aaryan KhandelwalandGitHub 0bccb63a9f fix: module start and target date validations (#1914) 2023-08-21 11:46:02 +05:30
Aaryan KhandelwalandGitHub 2eb956e97e refactor: project and workspace delete modals (#1915) 2023-08-21 11:44:59 +05:30
M. PalanikannanandGitHub d470adf262 fix: Image resize, Link selector in Modals, Delete/ sync images and so much more (#1896)
* added image-resizing support

* link form removed

* updated image upload logic and 35% default width on upload

* removed shadow, added alert if not saved and ts errors

* prevent enter key on Link selector to trigger modal submit

* added workspace slug to all tiptap instances

* added delete plugin with loading indicator

* added better syncing of "Saved" state of editor and Image uploads

* removed redundant description_html check
2023-08-19 18:58:54 +05:30
Aaryan KhandelwalandGitHub cebc8bdc8d fix: context menu dynamic positioning, multiple context menus opening issue (#1913)
* fix: context menu positioning

* fix: close already opened context menu on outside click
2023-08-19 18:50:12 +05:30
Anmol Singh BhatiaandGitHub 64b5ba196f style: responsive empty state for profile stats (#1911) 2023-08-18 20:18:03 +05:30
Anmol Singh BhatiaandGitHub 8d5018318d chore: add favorite project from sidebar (#1909) 2023-08-18 18:42:50 +05:30
Anmol Singh BhatiaandGitHub 0fbdc0b157 style: consistent ui for create update issue modal (#1907) 2023-08-18 18:42:04 +05:30
Bavisetti NarayanandGitHub 2f39181eb7 fix: priority ordering (#1908) 2023-08-18 18:27:29 +05:30
Dakshesh JainandGitHub d825dc5579 style: showing first name for bot profile (#1894) 2023-08-18 17:15:20 +05:30
Aaryan KhandelwalandGitHub 1f8117c987 fix: dashboard upcoming issues list (#1904) 2023-08-18 17:10:12 +05:30
255 changed files with 9258 additions and 4710 deletions
@@ -0,0 +1,55 @@
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:
- apps/app/**
deploy:
- apps/space/**
- name: Setup .npmrc for repository
run: |
echo -e "@tiptap-pro:registry=https://registry.tiptap.dev/\n//registry.tiptap.dev/:_authToken=${{ secrets.TIPTAP_TOKEN }}" > .npmrc
- name: Build Plane's Main App
if: steps.changed-files.outputs.web_any_changed == 'true'
run: |
mv ./.npmrc ./apps/app
cd apps/app
yarn
yarn build
- name: Build Plane's Deploy App
if: steps.changed-files.outputs.deploy_any_changed == 'true'
run: |
cd apps/space
yarn
yarn build
+111
View File
@@ -0,0 +1,111 @@
name: Update Docker Images for Plane on Release
on:
release:
types: [released]
jobs:
build_push_backend:
name: Build and Push Api Server Docker Image
runs-on: ubuntu-20.04
steps:
- name: Check out the repo
uses: actions/checkout@v3.3.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2.5.0
- name: Login to Docker Hub
uses: docker/login-action@v2.1.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Setup .npmrc for repository
run: |
echo -e "@tiptap-pro:registry=https://registry.tiptap.dev/\n//registry.tiptap.dev/:_authToken=${{ secrets.TIPTAP_TOKEN }}" > .npmrc
- name: Extract metadata (tags, labels) for Docker (Docker Hub) from Github Release
id: metaFrontend
uses: docker/metadata-action@v4.3.0
with:
images: ${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend
tags: |
type=ref,event=tag
- name: Extract metadata (tags, labels) for Docker (Docker Hub) from Github Release
id: metaBackend
uses: docker/metadata-action@v4.3.0
with:
images: ${{ secrets.DOCKERHUB_USERNAME }}/plane-backend
tags: |
type=ref,event=tag
- name: Extract metadata (tags, labels) for Docker (Docker Hub) from Github Release
id: metaDeploy
uses: docker/metadata-action@v4.3.0
with:
images: ${{ secrets.DOCKERHUB_USERNAME }}/plane-deploy
tags: |
type=ref,event=tag
- name: Extract metadata (tags, labels) for Docker (Docker Hub) from Github Release
id: metaProxy
uses: docker/metadata-action@v4.3.0
with:
images: ${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy
tags: |
type=ref,event=tag
- name: Build and Push Frontend to Docker Container Registry
uses: docker/build-push-action@v4.0.0
with:
context: .
file: ./apps/app/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: ./apps/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 }}
-77
View File
@@ -1,77 +0,0 @@
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 }}
-77
View File
@@ -1,77 +0,0 @@
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 }}
+10 -3
View File
@@ -5,9 +5,11 @@ WORKDIR /app
ENV NEXT_PUBLIC_API_BASE_URL=http://NEXT_PUBLIC_API_BASE_URL_PLACEHOLDER
RUN yarn global add turbo
RUN apk add tree
COPY . .
RUN turbo prune --scope=app --docker
RUN turbo prune --scope=app --scope=plane-deploy --docker
CMD tree -I node_modules/
# Add lockfile and package.json's of isolated subworkspace
FROM node:18-alpine AS installer
@@ -21,14 +23,14 @@ COPY --from=builder /app/out/json/ .
COPY --from=builder /app/out/yarn.lock ./yarn.lock
RUN yarn install
# Build the project
# # 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=app
RUN yarn turbo run build
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \
BUILT_NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
@@ -96,11 +98,16 @@ RUN adduser --system --uid 1001 captain
COPY --from=installer /app/apps/app/next.config.js .
COPY --from=installer /app/apps/app/package.json .
COPY --from=installer /app/apps/space/next.config.js .
COPY --from=installer /app/apps/space/package.json .
COPY --from=installer --chown=captain:plane /app/apps/app/.next/standalone ./
COPY --from=installer --chown=captain:plane /app/apps/app/.next/static ./apps/app/.next/static
COPY --from=installer --chown=captain:plane /app/apps/space/.next/standalone ./
COPY --from=installer --chown=captain:plane /app/apps/space/.next ./apps/space/.next
ENV NEXT_TELEMETRY_DISABLED 1
# RUN rm /etc/nginx/conf.d/default.conf
+1 -1
View File
@@ -1,3 +1,3 @@
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 -
web: gunicorn -w 4 -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:$PORT --max-requests 10000 --max-requests-jitter 1000 --access-logfile -
worker: celery -A plane worker -l info
beat: celery -A plane beat -l INFO
+9 -3
View File
@@ -1,11 +1,13 @@
from .base import BaseSerializer
from .user import UserSerializer, ChangePasswordSerializer, ResetPasswordSerializer
from .user import UserSerializer, UserLiteSerializer, ChangePasswordSerializer, ResetPasswordSerializer, UserAdminLiteSerializer
from .workspace import (
WorkSpaceSerializer,
WorkSpaceMemberSerializer,
TeamSerializer,
WorkSpaceMemberInviteSerializer,
WorkspaceLiteSerializer,
WorkspaceThemeSerializer,
WorkspaceMemberAdminSerializer,
)
from .project import (
ProjectSerializer,
@@ -14,11 +16,13 @@ from .project import (
ProjectMemberInviteSerializer,
ProjectIdentifierSerializer,
ProjectFavoriteSerializer,
ProjectLiteSerializer,
ProjectMemberLiteSerializer,
ProjectDeployBoardSerializer,
ProjectMemberAdminSerializer,
ProjectPublicMemberSerializer
)
from .state import StateSerializer
from .state import StateSerializer, StateLiteSerializer
from .view import IssueViewSerializer, IssueViewFavoriteSerializer
from .cycle import CycleSerializer, CycleIssueSerializer, CycleFavoriteSerializer, CycleWriteSerializer
from .asset import FileAssetSerializer
@@ -29,7 +33,7 @@ from .issue import (
IssuePropertySerializer,
BlockerIssueSerializer,
BlockedIssueSerializer,
# IssueAssigneeSerializer,
IssueAssigneeSerializer,
LabelSerializer,
IssueSerializer,
IssueFlatSerializer,
@@ -41,6 +45,7 @@ from .issue import (
IssueReactionSerializer,
CommentReactionSerializer,
IssueVoteSerializer,
IssuePublicSerializer,
)
from .module import (
@@ -70,6 +75,7 @@ from .page import PageSerializer, PageBlockSerializer, PageFavoriteSerializer
from .estimate import (
EstimateSerializer,
EstimatePointSerializer,
EstimateReadSerializer,
)
from .inbox import InboxSerializer, InboxIssueSerializer, IssueStateInboxSerializer
-28
View File
@@ -1,33 +1,5 @@
from rest_framework import serializers
def filterFields(self, fields):
for field_name in fields:
if isinstance(field_name, dict):
for key, value in field_name.items():
if isinstance(value, list):
filterFields(self.fields[key], value)
allowed = []
for item in fields:
if isinstance(item, str):
allowed.append(item)
elif isinstance(item, dict):
allowed.append(list(item.keys())[0])
existing = set(self.fields)
allowed = set(allowed)
for field_name in existing - allowed:
self.fields.pop(field_name)
return self.fields
class BaseSerializer(serializers.ModelSerializer):
id = serializers.PrimaryKeyRelatedField(read_only=True)
def __init__(self, *args, **kwargs):
# Don't pass the 'fields' arg up to the superclass
fields = kwargs.pop("fields", None)
# Instantiate the superclass normally
super().__init__(*args, **kwargs)
if fields is not None:
# Drop any fields that are not specified in the `fields` argument.
self.fields = filterFields(self, fields)
+17 -7
View File
@@ -6,21 +6,26 @@ from rest_framework import serializers
# Module imports
from .base import BaseSerializer
from .user import UserSerializer
from .user import UserLiteSerializer
from .issue import IssueStateSerializer
from .workspace import WorkSpaceSerializer
from .project import ProjectSerializer
from .workspace import WorkspaceLiteSerializer
from .project import ProjectLiteSerializer
from plane.db.models import Cycle, CycleIssue, CycleFavorite
class CycleWriteSerializer(BaseSerializer):
def validate(self, data):
if data.get("start_date", None) is not None and data.get("end_date", None) is not None and data.get("start_date", None) > data.get("end_date", None):
raise serializers.ValidationError("Start date cannot exceed end date")
return data
class Meta:
model = Cycle
fields = "__all__"
class CycleSerializer(BaseSerializer):
owned_by = UserSerializer(read_only=True)
owned_by = UserLiteSerializer(read_only=True)
is_favorite = serializers.BooleanField(read_only=True)
total_issues = serializers.IntegerField(read_only=True)
cancelled_issues = serializers.IntegerField(read_only=True)
@@ -33,9 +38,14 @@ class CycleSerializer(BaseSerializer):
total_estimates = serializers.IntegerField(read_only=True)
completed_estimates = serializers.IntegerField(read_only=True)
started_estimates = serializers.IntegerField(read_only=True)
workspace_detail = WorkSpaceSerializer(source="workspace",read_only=True)
project_detail = ProjectSerializer(read_only=True, source="project")
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
project_detail = ProjectLiteSerializer(read_only=True, source="project")
def validate(self, data):
if data.get("start_date", None) is not None and data.get("end_date", None) is not None and data.get("start_date", None) > data.get("end_date", None):
raise serializers.ValidationError("Start date cannot exceed end date")
return data
def get_assignees(self, obj):
members = [
{
@@ -54,7 +64,7 @@ class CycleSerializer(BaseSerializer):
unique_list = [dict(item) for item in unique_objects]
return unique_list
def get_labels(self, obj):
labels = [
{
+18 -3
View File
@@ -2,12 +2,12 @@
from .base import BaseSerializer
from plane.db.models import Estimate, EstimatePoint
from plane.api.serializers import WorkSpaceSerializer, ProjectSerializer
from plane.api.serializers import WorkspaceLiteSerializer, ProjectLiteSerializer
class EstimateSerializer(BaseSerializer):
workspace_detail = WorkSpaceSerializer(source="workspace", read_only=True)
project_detail = ProjectSerializer(read_only=True, source="project")
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
project_detail = ProjectLiteSerializer(read_only=True, source="project")
class Meta:
model = Estimate
@@ -27,3 +27,18 @@ class EstimatePointSerializer(BaseSerializer):
"workspace",
"project",
]
class EstimateReadSerializer(BaseSerializer):
points = EstimatePointSerializer(read_only=True, many=True)
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
project_detail = ProjectLiteSerializer(read_only=True, source="project")
class Meta:
model = Estimate
fields = "__all__"
read_only_fields = [
"points",
"name",
"description",
]
+2 -2
View File
@@ -1,11 +1,11 @@
# Module imports
from .base import BaseSerializer
from plane.db.models import ExporterHistory
from .user import UserSerializer
from .user import UserLiteSerializer
class ExporterHistorySerializer(BaseSerializer):
initiated_by_detail = UserSerializer(source="initiated_by",read_only=True)
initiated_by_detail = UserLiteSerializer(source="initiated_by", read_only=True)
class Meta:
model = ExporterHistory
+6 -6
View File
@@ -1,15 +1,15 @@
# Module imports
from .base import BaseSerializer
from .user import UserSerializer
from .project import ProjectSerializer
from .workspace import WorkSpaceSerializer
from .user import UserLiteSerializer
from .project import ProjectLiteSerializer
from .workspace import WorkspaceLiteSerializer
from plane.db.models import Importer
class ImporterSerializer(BaseSerializer):
initiated_by_detail = UserSerializer(source="initiated_by", read_only=True)
project_detail = ProjectSerializer(source="project", read_only=True)
workspace_detail = WorkSpaceSerializer(source="workspace", read_only=True)
initiated_by_detail = UserLiteSerializer(source="initiated_by", read_only=True)
project_detail = ProjectLiteSerializer(source="project", read_only=True)
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
class Meta:
model = Importer
+7 -6
View File
@@ -4,14 +4,15 @@ from rest_framework import serializers
# Module imports
from .base import BaseSerializer
from .issue import IssueFlatSerializer, LabelLiteSerializer
from .project import ProjectSerializer
from .project import ProjectLiteSerializer
from .state import StateLiteSerializer
from .user import UserSerializer
from .project import ProjectLiteSerializer
from .user import UserLiteSerializer
from plane.db.models import Inbox, InboxIssue, Issue
class InboxSerializer(BaseSerializer):
project_detail = ProjectSerializer(source="project", fields=("id","name","cover_image","icon_prop","emoji","description"), read_only=True)
project_detail = ProjectLiteSerializer(source="project", read_only=True)
pending_issue_count = serializers.IntegerField(read_only=True)
class Meta:
@@ -25,7 +26,7 @@ class InboxSerializer(BaseSerializer):
class InboxIssueSerializer(BaseSerializer):
issue_detail = IssueFlatSerializer(source="issue", read_only=True)
project_detail = ProjectSerializer(source="project", fields=("id","name","cover_image","icon_prop","emoji","description"), read_only=True)
project_detail = ProjectLiteSerializer(source="project", read_only=True)
class Meta:
model = InboxIssue
@@ -45,9 +46,9 @@ class InboxIssueLiteSerializer(BaseSerializer):
class IssueStateInboxSerializer(BaseSerializer):
state_detail = StateLiteSerializer(read_only=True, source="state")
project_detail = ProjectSerializer(source="project", fields=("id", "name", "cover_image", "icon_prop", "emoji", "description"), read_only=True)
project_detail = ProjectLiteSerializer(read_only=True, source="project")
label_details = LabelLiteSerializer(read_only=True, source="labels", many=True)
assignee_details = UserSerializer(source="assignees", read_only=True, many=True)
assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True)
sub_issues_count = serializers.IntegerField(read_only=True)
bridge_id = serializers.UUIDField(read_only=True)
issue_inbox = InboxIssueLiteSerializer(read_only=True, many=True)
+79 -109
View File
@@ -6,10 +6,11 @@ from rest_framework import serializers
# Module imports
from .base import BaseSerializer
from .user import UserSerializer
from .user import UserLiteSerializer
from .state import StateSerializer, StateLiteSerializer
from .project import ProjectSerializer
from .workspace import WorkSpaceSerializer
from .user import UserLiteSerializer
from .project import ProjectSerializer, ProjectLiteSerializer
from .workspace import WorkspaceLiteSerializer
from plane.db.models import (
User,
Issue,
@@ -53,11 +54,7 @@ class IssueFlatSerializer(BaseSerializer):
class IssueProjectLiteSerializer(BaseSerializer):
project_detail = ProjectSerializer(
source="project",
fields=("id", "name", "cover_image", "icon_prop", "emoji", "description"),
read_only=True,
)
project_detail = ProjectLiteSerializer(source="project", read_only=True)
class Meta:
model = Issue
@@ -74,12 +71,9 @@ class IssueProjectLiteSerializer(BaseSerializer):
## Find a better approach to save manytomany?
class IssueCreateSerializer(BaseSerializer):
state_detail = StateSerializer(read_only=True, source="state")
created_by_detail = UserSerializer(
source="created_by",
read_only=True,
)
project_detail = ProjectSerializer(source="project", read_only=True)
workspace_detail = WorkSpaceSerializer(source="workspace", read_only=True)
created_by_detail = UserLiteSerializer(read_only=True, source="created_by")
project_detail = ProjectLiteSerializer(read_only=True, source="project")
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
assignees_list = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()),
@@ -306,19 +300,35 @@ class IssueCreateSerializer(BaseSerializer):
class IssueActivitySerializer(BaseSerializer):
actor_detail = UserSerializer(source="actor", read_only=True)
actor_detail = UserLiteSerializer(read_only=True, source="actor")
issue_detail = IssueFlatSerializer(read_only=True, source="issue")
project_detail = ProjectSerializer(
source="project",
fields=("id", "name", "cover_image", "icon_prop", "emoji", "description"),
read_only=True,
)
project_detail = ProjectLiteSerializer(read_only=True, source="project")
class Meta:
model = IssueActivity
fields = "__all__"
class IssueCommentSerializer(BaseSerializer):
actor_detail = UserLiteSerializer(read_only=True, source="actor")
issue_detail = IssueFlatSerializer(read_only=True, source="issue")
project_detail = ProjectLiteSerializer(read_only=True, source="project")
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
class Meta:
model = IssueComment
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"issue",
"created_by",
"updated_by",
"created_at",
"updated_at",
]
class IssuePropertySerializer(BaseSerializer):
class Meta:
model = IssueProperty
@@ -331,14 +341,8 @@ class IssuePropertySerializer(BaseSerializer):
class LabelSerializer(BaseSerializer):
workspace_detail = WorkSpaceSerializer(
source="workspace", fields=("id", "name", "slug"), read_only=True
)
project_detail = ProjectSerializer(
source="project",
fields=("id", "name", "cover_image", "icon_prop", "emoji", "description"),
read_only=True,
)
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
project_detail = ProjectLiteSerializer(source="project", read_only=True)
class Meta:
model = Label
@@ -399,16 +403,12 @@ class BlockerIssueSerializer(BaseSerializer):
read_only_fields = fields
# class IssueAssigneeSerializer(BaseSerializer):
# assignee_details = UserSerializer(
# source="assignee",
# fields=("id", "first_name", "last_name", "avatar", "is_bot", "display_name"),
# read_only=True,
# )
class IssueAssigneeSerializer(BaseSerializer):
assignee_details = UserLiteSerializer(read_only=True, source="assignee")
# class Meta:
# model = IssueAssignee
# fields = "__all__"
class Meta:
model = IssueAssignee
fields = "__all__"
class CycleBaseSerializer(BaseSerializer):
@@ -472,11 +472,7 @@ class IssueModuleDetailSerializer(BaseSerializer):
class IssueLinkSerializer(BaseSerializer):
created_by_detail = UserSerializer(
source="created_by",
fields=("id", "first_name", "last_name", "avatar", "is_bot", "display_name"),
read_only=True,
)
created_by_detail = UserLiteSerializer(read_only=True, source="created_by")
class Meta:
model = IssueLink
@@ -530,11 +526,7 @@ class IssueReactionSerializer(BaseSerializer):
class IssueReactionLiteSerializer(BaseSerializer):
actor_detail = UserSerializer(
source="actor",
fields=("id", "first_name", "last_name", "avatar", "is_bot", "display_name"),
read_only=True,
)
actor_detail = UserLiteSerializer(read_only=True, source="actor")
class Meta:
model = IssueReaction
@@ -547,11 +539,7 @@ class IssueReactionLiteSerializer(BaseSerializer):
class CommentReactionLiteSerializer(BaseSerializer):
actor_detail = UserSerializer(
source="actor",
fields=("id", "first_name", "last_name", "avatar", "is_bot", "display_name"),
read_only=True,
)
actor_detail = UserLiteSerializer(read_only=True, source="actor")
class Meta:
model = CommentReaction
@@ -573,25 +561,17 @@ class CommentReactionSerializer(BaseSerializer):
class IssueVoteSerializer(BaseSerializer):
class Meta:
model = IssueVote
fields = ["issue", "vote", "workspace_id", "project_id", "actor"]
fields = ["issue", "vote", "workspace", "project", "actor",]
read_only_fields = fields
class IssueCommentSerializer(BaseSerializer):
actor_detail = UserSerializer(source="actor", read_only=True)
actor_detail = UserLiteSerializer(read_only=True, source="actor")
issue_detail = IssueFlatSerializer(read_only=True, source="issue")
project_detail = ProjectSerializer(
source="project",
fields=("id", "name", "cover_image", "icon_prop", "emoji", "description"),
read_only=True,
)
workspace_details = WorkSpaceSerializer(
source="workspace",
fields=("id", "name", "slug"),
read_only=True,
)
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
@@ -604,17 +584,12 @@ class IssueCommentSerializer(BaseSerializer):
"updated_by",
"created_at",
"updated_at",
"access",
]
class IssueStateFlatSerializer(BaseSerializer):
state_detail = StateLiteSerializer(read_only=True, source="state")
project_detail = ProjectSerializer(
source="project",
fields=("id", "name", "cover_image", "icon_prop", "emoji", "description"),
read_only=True,
)
project_detail = ProjectLiteSerializer(read_only=True, source="project")
class Meta:
model = Issue
@@ -631,17 +606,8 @@ class IssueStateFlatSerializer(BaseSerializer):
class IssueStateSerializer(BaseSerializer):
label_details = LabelLiteSerializer(read_only=True, source="labels", many=True)
state_detail = StateLiteSerializer(read_only=True, source="state")
project_detail = ProjectSerializer(
source="project",
fields=("id", "name", "cover_image", "icon_prop", "emoji", "description"),
read_only=True,
)
assignee_details = UserSerializer(
source="assignees",
fields=("id", "first_name", "last_name", "avatar", "is_bot", "display_name"),
read_only=True,
many=True,
)
project_detail = ProjectLiteSerializer(read_only=True, source="project")
assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True)
sub_issues_count = serializers.IntegerField(read_only=True)
bridge_id = serializers.UUIDField(read_only=True)
attachment_count = serializers.IntegerField(read_only=True)
@@ -653,20 +619,11 @@ class IssueStateSerializer(BaseSerializer):
class IssueSerializer(BaseSerializer):
project_detail = ProjectSerializer(
source="project",
fields=("id", "name", "cover_image", "icon_prop", "emoji", "description"),
read_only=True,
)
project_detail = ProjectLiteSerializer(read_only=True, source="project")
state_detail = StateSerializer(read_only=True, source="state")
parent_detail = IssueStateFlatSerializer(read_only=True, source="parent")
label_details = LabelSerializer(read_only=True, source="labels", many=True)
assignee_details = UserSerializer(
source="assignees",
fields=("id", "first_name", "last_name", "avatar", "is_bot", "display_name"),
read_only=True,
many=True,
)
assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True)
# List of issues blocked by this issue
blocked_issues = BlockedIssueSerializer(read_only=True, many=True)
# List of issues that block this issue
@@ -692,24 +649,11 @@ class IssueSerializer(BaseSerializer):
class IssueLiteSerializer(BaseSerializer):
workspace_detail = WorkSpaceSerializer(
source="workspace",
fields=("id", "name", "slug"),
read_only=True,
)
project_detail = ProjectSerializer(
source="project",
fields=("id", "name", "cover_image", "icon_prop", "emoji", "description"),
read_only=True,
)
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
project_detail = ProjectLiteSerializer(read_only=True, source="project")
state_detail = StateLiteSerializer(read_only=True, source="state")
label_details = LabelLiteSerializer(read_only=True, source="labels", many=True)
assignee_details = UserSerializer(
source="assignees",
fields=("id", "first_name", "last_name", "avatar", "is_bot", "display_name"),
read_only=True,
many=True,
)
assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True)
sub_issues_count = serializers.IntegerField(read_only=True)
cycle_id = serializers.UUIDField(read_only=True)
module_id = serializers.UUIDField(read_only=True)
@@ -733,6 +677,32 @@ class IssueLiteSerializer(BaseSerializer):
]
class IssuePublicSerializer(BaseSerializer):
project_detail = ProjectLiteSerializer(read_only=True, source="project")
state_detail = StateLiteSerializer(read_only=True, source="state")
issue_reactions = IssueReactionLiteSerializer(read_only=True, many=True)
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",
"issue_reactions",
"votes",
]
read_only_fields = fields
class IssueSubscriberSerializer(BaseSerializer):
class Meta:
model = IssueSubscriber
+15 -27
View File
@@ -3,9 +3,9 @@ from rest_framework import serializers
# Module imports
from .base import BaseSerializer
from .user import UserSerializer
from .project import ProjectSerializer
from .workspace import WorkSpaceSerializer
from .user import UserLiteSerializer
from .project import ProjectSerializer, ProjectLiteSerializer
from .workspace import WorkspaceLiteSerializer
from .issue import IssueStateSerializer
from plane.db.models import (
@@ -25,12 +25,8 @@ class ModuleWriteSerializer(BaseSerializer):
required=False,
)
project_detail = ProjectSerializer(source="project", fields=("id", "name", "cover_image", "icon_prop", "emoji", "description"), read_only=True)
workspace_detail = WorkSpaceSerializer(
source="workspace",
fields=("id", "name", "slug"),
read_only=True,
)
project_detail = ProjectLiteSerializer(source="project", read_only=True)
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
class Meta:
model = Module
@@ -44,6 +40,11 @@ class ModuleWriteSerializer(BaseSerializer):
"updated_at",
]
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):
raise serializers.ValidationError("Start date cannot exceed target date")
return data
def create(self, validated_data):
members = validated_data.pop("members_list", None)
@@ -110,7 +111,7 @@ class ModuleFlatSerializer(BaseSerializer):
class ModuleIssueSerializer(BaseSerializer):
module_detail = ModuleFlatSerializer(read_only=True, source="module")
issue_detail = ProjectSerializer(source="issue", fields=("id", "name", "cover_image", "icon_prop", "emoji", "description"), read_only=True)
issue_detail = ProjectLiteSerializer(read_only=True, source="issue")
sub_issues_count = serializers.IntegerField(read_only=True)
class Meta:
@@ -128,11 +129,7 @@ class ModuleIssueSerializer(BaseSerializer):
class ModuleLinkSerializer(BaseSerializer):
created_by_detail = UserSerializer(
source="created_by",
fields=("id", "first_name", "last_name", "avatar", "is_bot", "display_name"),
read_only=True,
)
created_by_detail = UserLiteSerializer(read_only=True, source="created_by")
class Meta:
model = ModuleLink
@@ -159,18 +156,9 @@ class ModuleLinkSerializer(BaseSerializer):
class ModuleSerializer(BaseSerializer):
project_detail = ProjectSerializer(source="project", fields=("id", "name", "cover_image", "icon_prop", "emoji", "description"), read_only=True)
lead_detail = UserSerializer(
source="lead",
fields=("id", "first_name", "last_name", "avatar", "is_bot", "display_name"),
read_only=True,
)
members_detail = UserSerializer(
source="members",
fields=("id", "first_name", "last_name", "avatar", "is_bot", "display_name"),
read_only=True,
many=True,
)
project_detail = ProjectLiteSerializer(read_only=True, source="project")
lead_detail = UserLiteSerializer(read_only=True, source="lead")
members_detail = UserLiteSerializer(read_only=True, many=True, source="members")
link_module = ModuleLinkSerializer(read_only=True, many=True)
is_favorite = serializers.BooleanField(read_only=True)
total_issues = serializers.IntegerField(read_only=True)
@@ -1,16 +1,12 @@
# Module imports
from .base import BaseSerializer
from .user import UserSerializer
from .user import UserLiteSerializer
from plane.db.models import Notification
class NotificationSerializer(BaseSerializer):
triggered_by_details = UserSerializer(
source="triggered_by",
# fields=("id", "first_name", "last_name", "avatar", "is_bot", "display_name"),
read_only=True,
)
triggered_by_details = UserLiteSerializer(read_only=True, source="triggered_by")
class Meta:
model = Notification
fields = "__all__"
+6 -14
View File
@@ -4,19 +4,15 @@ from rest_framework import serializers
# Module imports
from .base import BaseSerializer
from .issue import IssueFlatSerializer, LabelLiteSerializer
from .workspace import WorkSpaceSerializer
from .project import ProjectSerializer
from .workspace import WorkspaceLiteSerializer
from .project import ProjectLiteSerializer
from plane.db.models import Page, PageBlock, PageFavorite, PageLabel, Label
class PageBlockSerializer(BaseSerializer):
issue_detail = IssueFlatSerializer(source="issue", read_only=True)
project_detail = ProjectSerializer(source="project", fields=("id", "name", "cover_image", "icon_prop", "emoji", "description"), read_only=True)
workspace_detail = WorkSpaceSerializer(
source="workspace",
fields=("id", "name", "slug"),
read_only=True,
)
project_detail = ProjectLiteSerializer(source="project", read_only=True)
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
class Meta:
model = PageBlock
@@ -43,12 +39,8 @@ class PageSerializer(BaseSerializer):
required=False,
)
blocks = PageBlockLiteSerializer(read_only=True, many=True)
project_detail = ProjectSerializer(source="project", fields=("id", "name", "cover_image", "icon_prop", "emoji", "description"), read_only=True)
workspace_detail = WorkSpaceSerializer(
source="workspace",
fields=("id", "name", "slug"),
read_only=True,
)
project_detail = ProjectLiteSerializer(source="project", read_only=True)
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
class Meta:
model = Page
+46 -73
View File
@@ -6,8 +6,8 @@ from rest_framework import serializers
# Module imports
from .base import BaseSerializer
from plane.api.serializers.workspace import WorkSpaceSerializer
from plane.api.serializers.user import UserSerializer
from plane.api.serializers.workspace import WorkSpaceSerializer, WorkspaceLiteSerializer
from plane.api.serializers.user import UserLiteSerializer, UserAdminLiteSerializer
from plane.db.models import (
Project,
ProjectMember,
@@ -15,32 +15,12 @@ from plane.db.models import (
ProjectIdentifier,
ProjectFavorite,
ProjectDeployBoard,
ProjectPublicMember,
)
class ProjectSerializer(BaseSerializer):
# workspace = WorkSpaceSerializer(read_only=True)
default_assignee = UserSerializer(
# fields=("id", "first_name", "last_name", "avatar", "is_bot", "display_name"),
read_only=True,
)
project_lead = UserSerializer(
# fields=("id", "first_name", "last_name", "avatar", "is_bot", "display_name"),
read_only=True,
)
is_favorite = serializers.BooleanField(read_only=True)
total_members = serializers.IntegerField(read_only=True)
total_cycles = serializers.IntegerField(read_only=True)
total_modules = serializers.IntegerField(read_only=True)
is_member = serializers.BooleanField(read_only=True)
sort_order = serializers.FloatField(read_only=True)
member_role = serializers.IntegerField(read_only=True)
is_deployed = serializers.BooleanField(read_only=True)
workspace_detail = WorkSpaceSerializer(
source="workspace",
# fields=("id", "name", "slug"),
read_only=True,
)
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
class Meta:
model = Project
@@ -99,16 +79,25 @@ class ProjectSerializer(BaseSerializer):
raise serializers.ValidationError(detail="Project Identifier is already taken")
class ProjectLiteSerializer(BaseSerializer):
class Meta:
model = Project
fields = [
"id",
"identifier",
"name",
"cover_image",
"icon_prop",
"emoji",
"description",
]
read_only_fields = fields
class ProjectDetailSerializer(BaseSerializer):
workspace = WorkSpaceSerializer(read_only=True)
default_assignee = UserSerializer(
# fields=("id", "first_name", "last_name", "avatar", "is_bot", "display_name"),
read_only=True,
)
project_lead = UserSerializer(
# fields=("id", "first_name", "last_name", "avatar", "is_bot", "display_name"),
read_only=True,
)
default_assignee = UserLiteSerializer(read_only=True)
project_lead = UserLiteSerializer(read_only=True)
is_favorite = serializers.BooleanField(read_only=True)
total_members = serializers.IntegerField(read_only=True)
total_cycles = serializers.IntegerField(read_only=True)
@@ -124,12 +113,9 @@ class ProjectDetailSerializer(BaseSerializer):
class ProjectMemberSerializer(BaseSerializer):
workspace = WorkSpaceSerializer(read_only=True)
project = ProjectSerializer(fields=("id", "name", "cover_image", "icon_prop", "emoji", "description"), read_only=True)
member = UserSerializer(
# fields=("id", "first_name", "last_name", "avatar", "is_bot", "display_name"),
read_only=True,
)
workspace = WorkspaceLiteSerializer(read_only=True)
project = ProjectLiteSerializer(read_only=True)
member = UserLiteSerializer(read_only=True)
class Meta:
model = ProjectMember
@@ -137,24 +123,9 @@ class ProjectMemberSerializer(BaseSerializer):
class ProjectMemberAdminSerializer(BaseSerializer):
workspace = WorkSpaceSerializer(
source="workspace",
fields=("id", "name", "slug"),
read_only=True,
)
project = ProjectSerializer(fields=("id", "name", "cover_image", "icon_prop", "emoji", "description"), read_only=True)
member = UserSerializer(
# fields=(
# "id",
# "first_name",
# "last_name",
# "avatar",
# "is_bot",
# "display_name",
# "email",
# ),
read_only=True,
)
workspace = WorkspaceLiteSerializer(read_only=True)
project = ProjectLiteSerializer(read_only=True)
member = UserAdminLiteSerializer(read_only=True)
class Meta:
model = ProjectMember
@@ -162,11 +133,8 @@ class ProjectMemberAdminSerializer(BaseSerializer):
class ProjectMemberInviteSerializer(BaseSerializer):
project = ProjectSerializer(fields=("id", "name", "cover_image", "icon_prop", "emoji", "description"), read_only=True)
workspace = WorkSpaceSerializer(
fields=("id", "name", "slug"),
read_only=True,
)
project = ProjectLiteSerializer(read_only=True)
workspace = WorkspaceLiteSerializer(read_only=True)
class Meta:
model = ProjectMemberInvite
@@ -180,7 +148,7 @@ class ProjectIdentifierSerializer(BaseSerializer):
class ProjectFavoriteSerializer(BaseSerializer):
project_detail = ProjectSerializer(source="project", fields=("id", "name", "cover_image", "icon_prop", "emoji", "description"), read_only=True)
project_detail = ProjectLiteSerializer(source="project", read_only=True)
class Meta:
model = ProjectFavorite
@@ -192,10 +160,7 @@ class ProjectFavoriteSerializer(BaseSerializer):
class ProjectMemberLiteSerializer(BaseSerializer):
member = UserSerializer(
# fields=("id", "first_name", "last_name", "avatar", "is_bot", "display_name"),
read_only=True,
)
member = UserLiteSerializer(read_only=True)
is_subscribed = serializers.BooleanField(read_only=True)
class Meta:
@@ -205,17 +170,25 @@ class ProjectMemberLiteSerializer(BaseSerializer):
class ProjectDeployBoardSerializer(BaseSerializer):
project_details = ProjectSerializer(source="project", fields=("id", "name", "cover_image", "icon_prop", "emoji", "description"), read_only=True)
workspace_detail = WorkSpaceSerializer(
source="workspace",
fields=("id", "name", "slug"),
read_only=True,
)
project_details = ProjectLiteSerializer(read_only=True, source="project")
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
class Meta:
model = ProjectDeployBoard
fields = "__all__"
read_only_fields = [
"workspace",
"project" "anchor",
"project", "anchor",
]
class ProjectPublicMemberSerializer(BaseSerializer):
class Meta:
model = ProjectPublicMember
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"member",
]
+4 -4
View File
@@ -1,14 +1,14 @@
# Module imports
from .base import BaseSerializer
from .workspace import WorkSpaceSerializer
from .project import ProjectSerializer
from .workspace import WorkspaceLiteSerializer
from .project import ProjectLiteSerializer
from plane.db.models import State
class StateSerializer(BaseSerializer):
workspace_detail = WorkSpaceSerializer(source="workspace", read_only=True)
project_detail = ProjectSerializer(source="project", read_only=True)
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
project_detail = ProjectLiteSerializer(read_only=True, source="project")
class Meta:
model = State
+36
View File
@@ -33,6 +33,42 @@ class UserSerializer(BaseSerializer):
return bool(obj.first_name) or bool(obj.last_name)
class UserLiteSerializer(BaseSerializer):
class Meta:
model = User
fields = [
"id",
"first_name",
"last_name",
"avatar",
"is_bot",
"display_name",
]
read_only_fields = [
"id",
"is_bot",
]
class UserAdminLiteSerializer(BaseSerializer):
class Meta:
model = User
fields = [
"id",
"first_name",
"last_name",
"avatar",
"is_bot",
"display_name",
"email",
]
read_only_fields = [
"id",
"is_bot",
]
class ChangePasswordSerializer(serializers.Serializer):
model = User
+4 -8
View File
@@ -3,20 +3,16 @@ from rest_framework import serializers
# Module imports
from .base import BaseSerializer
from .workspace import WorkSpaceSerializer
from .project import ProjectSerializer
from .workspace import WorkspaceLiteSerializer
from .project import ProjectLiteSerializer
from plane.db.models import IssueView, IssueViewFavorite
from plane.utils.issue_filters import issue_filters
class IssueViewSerializer(BaseSerializer):
is_favorite = serializers.BooleanField(read_only=True)
project_detail = ProjectSerializer(source="project", fields=("id", "name", "cover_image", "icon_prop", "emoji", "description"), read_only=True)
workspace_detail = WorkSpaceSerializer(
source="workspace",
fields=("id", "name", "slug"),
read_only=True,
)
project_detail = ProjectLiteSerializer(source="project", read_only=True)
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
class Meta:
model = IssueView
+26 -24
View File
@@ -3,7 +3,7 @@ from rest_framework import serializers
# Module imports
from .base import BaseSerializer
from .user import UserSerializer
from .user import UserLiteSerializer, UserAdminLiteSerializer
from plane.db.models import (
User,
@@ -17,10 +17,7 @@ from plane.db.models import (
class WorkSpaceSerializer(BaseSerializer):
owner = UserSerializer(
# fields=("id", "first_name", "last_name", "avatar", "is_bot", "display_name"),
read_only=True,
)
owner = UserLiteSerializer(read_only=True)
total_members = serializers.IntegerField(read_only=True)
total_issues = serializers.IntegerField(read_only=True)
@@ -36,16 +33,30 @@ class WorkSpaceSerializer(BaseSerializer):
"owner",
]
class WorkspaceLiteSerializer(BaseSerializer):
class Meta:
model = Workspace
fields = [
"name",
"slug",
"id",
]
read_only_fields = fields
class WorkSpaceMemberSerializer(BaseSerializer):
member = UserSerializer(
# fields=("id", "first_name", "last_name", "avatar", "is_bot", "display_name" ,"email"),
read_only=True,
)
workspace = WorkSpaceSerializer(
fields=("id", "name", "slug"),
read_only=True,
)
member = UserLiteSerializer(read_only=True)
workspace = WorkspaceLiteSerializer(read_only=True)
class Meta:
model = WorkspaceMember
fields = "__all__"
class WorkspaceMemberAdminSerializer(BaseSerializer):
member = UserAdminLiteSerializer(read_only=True)
workspace = WorkspaceLiteSerializer(read_only=True)
class Meta:
model = WorkspaceMember
@@ -55,11 +66,7 @@ class WorkSpaceMemberSerializer(BaseSerializer):
class WorkSpaceMemberInviteSerializer(BaseSerializer):
workspace = WorkSpaceSerializer(read_only=True)
total_members = serializers.IntegerField(read_only=True)
created_by_detail = UserSerializer(
source="created_by",
# fields=("id", "first_name", "last_name", "avatar", "is_bot", "display_name"),
read_only=True,
)
created_by_detail = UserLiteSerializer(read_only=True, source="created_by")
class Meta:
model = WorkspaceMemberInvite
@@ -67,12 +74,7 @@ class WorkSpaceMemberInviteSerializer(BaseSerializer):
class TeamSerializer(BaseSerializer):
members_detail = UserSerializer(
source="members",
# fields=("id", "first_name", "last_name", "avatar", "is_bot", "display_name"),
read_only=True,
many=True,
)
members_detail = UserLiteSerializer(read_only=True, source="members", many=True)
members = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()),
write_only=True,
+29 -4
View File
@@ -89,7 +89,6 @@ from plane.api.views import (
IssueCommentPublicViewSet,
IssueReactionViewSet,
CommentReactionViewSet,
ExportIssuesEndpoint,
## End Issues
# States
StateViewSet,
@@ -165,16 +164,23 @@ from plane.api.views import (
# Notification
NotificationViewSet,
UnreadNotificationEndpoint,
MarkAllReadNotificationViewSet,
## End Notification
# Public Boards
ProjectDeployBoardViewSet,
ProjectDeployBoardIssuesPublicEndpoint,
ProjectIssuesPublicEndpoint,
ProjectDeployBoardPublicSettingsEndpoint,
IssueReactionPublicViewSet,
CommentReactionPublicViewSet,
InboxIssuePublicViewSet,
IssueVotePublicViewSet,
WorkspaceProjectDeployBoardEndpoint,
IssueRetrievePublicEndpoint,
## End Public Boards
## Exporter
ExportIssuesEndpoint,
## End Exporter
)
@@ -231,7 +237,7 @@ urlpatterns = [
UpdateUserTourCompletedEndpoint.as_view(),
name="user-tour",
),
path("users/activities/", UserActivityEndpoint.as_view(), name="user-activities"),
path("users/workspaces/<str:slug>/activities/", UserActivityEndpoint.as_view(), name="user-activities"),
# user workspaces
path(
"users/me/workspaces/",
@@ -1490,6 +1496,15 @@ 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(
@@ -1520,9 +1535,14 @@ urlpatterns = [
),
path(
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/",
ProjectDeployBoardIssuesPublicEndpoint.as_view(),
ProjectIssuesPublicEndpoint.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(
@@ -1614,5 +1634,10 @@ urlpatterns = [
),
name="issue-vote-project-board",
),
path(
"public/workspaces/<str:slug>/project-boards/",
WorkspaceProjectDeployBoardEndpoint.as_view(),
name="workspace-project-boards",
),
## End Public Boards
]
+4 -2
View File
@@ -12,10 +12,10 @@ from .project import (
ProjectUserViewsEndpoint,
ProjectMemberUserEndpoint,
ProjectFavoritesViewSet,
ProjectDeployBoardIssuesPublicEndpoint,
ProjectDeployBoardViewSet,
ProjectDeployBoardPublicSettingsEndpoint,
ProjectMemberEndpoint,
WorkspaceProjectDeployBoardEndpoint,
)
from .user import (
UserEndpoint,
@@ -84,6 +84,8 @@ from .issue import (
IssueReactionPublicViewSet,
CommentReactionPublicViewSet,
IssueVotePublicViewSet,
IssueRetrievePublicEndpoint,
ProjectIssuesPublicEndpoint,
)
from .auth_extended import (
@@ -161,7 +163,7 @@ from .analytic import (
DefaultAnalyticsEndpoint,
)
from .notification import NotificationViewSet, UnreadNotificationEndpoint
from .notification import NotificationViewSet, UnreadNotificationEndpoint, MarkAllReadNotificationViewSet
from .exporter import (
ExportIssuesEndpoint,
+24 -8
View File
@@ -18,10 +18,21 @@ class FileAssetEndpoint(BaseAPIView):
"""
def get(self, request, workspace_id, asset_key):
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)
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,
)
def post(self, request, slug):
try:
@@ -68,11 +79,16 @@ class UserAssetsEndpoint(BaseAPIView):
def get(self, request, asset_key):
try:
files = FileAsset.objects.filter(asset=asset_key, created_by=request.user)
serializer = FileAssetSerializer(files, context={"request": request})
return Response(serializer.data)
except FileAsset.DoesNotExist:
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)
return Response(
{"error": "File Asset does not exist"}, status=status.HTTP_404_NOT_FOUND
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
def post(self, request):
+22 -5
View File
@@ -1,24 +1,41 @@
# Python imports
import zoneinfo
# Django imports
from django.urls import resolve
from django.conf import settings
from django.utils import timezone
# 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 BaseViewSet(ModelViewSet, 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):
model = None
@@ -67,7 +84,7 @@ class BaseViewSet(ModelViewSet, BasePaginator):
return self.kwargs.get("pk", None)
class BaseAPIView(APIView, BasePaginator):
class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
permission_classes = [
IsAuthenticated,
+14 -32
View File
@@ -56,10 +56,6 @@ class CycleViewSet(BaseViewSet):
permission_classes = [
ProjectEntityPermission,
]
# def get_serializer_class(self):
# return (
# CycleSerializer(nested_fields={"owned_by":("id", "first_name", "last_name", "avatar", "is_bot", "display_name")})
# )
def perform_create(self, serializer):
serializer.save(
@@ -152,15 +148,13 @@ class CycleViewSet(BaseViewSet):
.prefetch_related(
Prefetch(
"issue_cycle__issue__assignees",
queryset=User.objects.only(
"avatar", "first_name", "id").distinct(),
queryset=User.objects.only("avatar", "first_name", "id").distinct(),
)
)
.prefetch_related(
Prefetch(
"issue_cycle__issue__labels",
queryset=Label.objects.only(
"name", "color", "id").distinct(),
queryset=Label.objects.only("name", "color", "id").distinct(),
)
)
.order_by("-is_favorite", "name")
@@ -178,8 +172,7 @@ class CycleViewSet(BaseViewSet):
# All Cycles
if cycle_view == "all":
return Response(
CycleSerializer(queryset, fields=["id", "created_by", "created_at", "updated_at", "updated_by", "project", "workspace", "name", "description", "start_date", "end_date", "owned_by", "view_props", "sort_order", "is_favorite", "total_issues", "cancelled_issues", "completed_issues", "started_issues", "unstarted_issues"
, "backlog_issues", "assignees", "labels", "total_estimates", "completed_estimates", "started_estimates",{"workspace_detail": ["id", "name","slug"]},{ "project_detail": ["id", "name", "cover_image", "icon_prop", "emoji", "description"]}, {"owned_by": ["id", "first_name", "last_name", "avatar", "is_bot", "display_name"]}], many=True).data, status=status.HTTP_200_OK
CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK
)
# Current Cycle
@@ -189,8 +182,7 @@ class CycleViewSet(BaseViewSet):
end_date__gte=timezone.now(),
)
data = CycleSerializer(queryset, fields=["id", "created_by", "created_at", "updated_at", "updated_by", "project", "workspace", "name", "description", "start_date", "end_date", "owned_by", "view_props", "sort_order", "is_favorite", "total_issues", "cancelled_issues", "completed_issues", "started_issues", "unstarted_issues"
, "backlog_issues", "assignees", "labels", "total_estimates", "completed_estimates", "started_estimates",{"workspace_detail": ["id", "name","slug"]},{ "project_detail": ["id", "name", "cover_image", "icon_prop", "emoji", "description"]}, {"owned_by": ["id", "first_name", "last_name", "avatar", "is_bot", "display_name"]}], many=True).data
data = CycleSerializer(queryset, many=True).data
if len(data):
assignee_distribution = (
@@ -264,16 +256,14 @@ class CycleViewSet(BaseViewSet):
if cycle_view == "upcoming":
queryset = queryset.filter(start_date__gt=timezone.now())
return Response(
CycleSerializer(queryset, fields=["id", "created_by", "created_at", "updated_at", "updated_by", "project", "workspace", "name", "description", "start_date", "end_date", "owned_by", "view_props", "sort_order", "is_favorite", "total_issues", "cancelled_issues", "completed_issues", "started_issues", "unstarted_issues"
, "backlog_issues", "assignees", "labels", "total_estimates", "completed_estimates", "started_estimates",{"workspace_detail": ["id", "name","slug"]},{ "project_detail": ["id", "name", "cover_image", "icon_prop", "emoji", "description"]}, {"owned_by": ["id", "first_name", "last_name", "avatar", "is_bot", "display_name"]}], many=True).data, status=status.HTTP_200_OK
CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK
)
# Completed Cycles
if cycle_view == "completed":
queryset = queryset.filter(end_date__lt=timezone.now())
return Response(
CycleSerializer(queryset, fields=["id", "created_by", "created_at", "updated_at", "updated_by", "project", "workspace", "name", "description", "start_date", "end_date", "owned_by", "view_props", "sort_order", "is_favorite", "total_issues", "cancelled_issues", "completed_issues", "started_issues", "unstarted_issues"
, "backlog_issues", "assignees", "labels", "total_estimates", "completed_estimates", "started_estimates",{"workspace_detail": ["id", "name","slug"]},{ "project_detail": ["id", "name", "cover_image", "icon_prop", "emoji", "description"]}, {"owned_by": ["id", "first_name", "last_name", "avatar", "is_bot", "display_name"]}], many=True).data, status=status.HTTP_200_OK
CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK
)
# Draft Cycles
@@ -284,19 +274,16 @@ class CycleViewSet(BaseViewSet):
)
return Response(
CycleSerializer(queryset, fields=["id", "created_by", "created_at", "updated_at", "updated_by", "project", "workspace", "name", "description", "start_date", "end_date", "owned_by", "view_props", "sort_order", "is_favorite", "total_issues", "cancelled_issues", "completed_issues", "started_issues", "unstarted_issues"
, "backlog_issues", "assignees", "labels", "total_estimates", "completed_estimates", "started_estimates",{"workspace_detail": ["id", "name","slug"]},{ "project_detail": ["id", "name", "cover_image", "icon_prop", "emoji", "description"]}, {"owned_by": ["id", "first_name", "last_name", "avatar", "is_bot", "display_name"]}], many=True).data, status=status.HTTP_200_OK
CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK
)
# Incomplete Cycles
if cycle_view == "incomplete":
queryset = queryset.filter(
Q(end_date__gte=timezone.now().date()) | Q(
end_date__isnull=True),
Q(end_date__gte=timezone.now().date()) | Q(end_date__isnull=True),
)
return Response(
CycleSerializer(queryset, fields=["id", "created_by", "created_at", "updated_at", "updated_by", "project", "workspace", "name", "description", "start_date", "end_date", "owned_by", "view_props", "sort_order", "is_favorite", "total_issues", "cancelled_issues", "completed_issues", "started_issues", "unstarted_issues"
, "backlog_issues", "assignees", "labels", "total_estimates", "completed_estimates", "started_estimates",{"workspace_detail": ["id", "name","slug"]},{ "project_detail": ["id", "name", "cover_image", "icon_prop", "emoji", "description"]}, {"owned_by": ["id", "first_name", "last_name", "avatar", "is_bot", "display_name"]}], many=True).data, status=status.HTTP_200_OK
CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK
)
return Response(
@@ -319,8 +306,7 @@ class CycleViewSet(BaseViewSet):
request.data.get("start_date", None) is not None
and request.data.get("end_date", None) is not None
):
serializer = CycleSerializer(data=request.data, fields=["id", "created_by", "created_at", "updated_at", "updated_by", "project", "workspace", "name", "description", "start_date", "end_date", "owned_by", "view_props", "sort_order", "is_favorite", "total_issues", "cancelled_issues", "completed_issues", "started_issues", "unstarted_issues"
, "backlog_issues", "assignees", "labels", "total_estimates", "completed_estimates", "started_estimates",{"workspace_detail": ["id", "name","slug"]},{ "project_detail": ["id", "name", "cover_image", "icon_prop", "emoji", "description"]}, {"owned_by": ["id", "first_name", "last_name", "avatar", "is_bot", "display_name"]}])
serializer = CycleSerializer(data=request.data)
if serializer.is_valid():
serializer.save(
project_id=project_id,
@@ -356,8 +342,7 @@ class CycleViewSet(BaseViewSet):
status=status.HTTP_400_BAD_REQUEST,
)
serializer = CycleWriteSerializer(
cycle, data=request.data, partial=True)
serializer = CycleWriteSerializer(cycle, data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
@@ -433,8 +418,7 @@ class CycleViewSet(BaseViewSet):
.order_by("label_name")
)
data = CycleSerializer(queryset, fields=["id", "created_by", "created_at", "updated_at", "updated_by", "project", "workspace", "name", "description", "start_date", "end_date", "owned_by", "view_props", "sort_order", "is_favorite", "total_issues", "cancelled_issues", "completed_issues", "started_issues", "unstarted_issues"
, "backlog_issues", "assignees", "labels", "total_estimates", "completed_estimates", "started_estimates",{"workspace_detail": ["id", "name","slug"]},{ "project_detail": ["id", "name", "cover_image", "icon_prop", "emoji", "description"]}, {"owned_by": ["id", "first_name", "last_name", "avatar", "is_bot", "display_name"]}]).data
data = CycleSerializer(queryset).data
data["distribution"] = {
"assignees": assignee_distribution,
"labels": label_distribution,
@@ -502,8 +486,7 @@ class CycleIssueViewSet(BaseViewSet):
super()
.get_queryset()
.annotate(
sub_issues_count=Issue.issue_objects.filter(
parent=OuterRef("issue_id"))
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("issue_id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
@@ -529,8 +512,7 @@ class CycleIssueViewSet(BaseViewSet):
issues = (
Issue.issue_objects.filter(issue_cycle__cycle_id=cycle_id)
.annotate(
sub_issues_count=Issue.issue_objects.filter(
parent=OuterRef("id"))
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
+10 -114
View File
@@ -13,6 +13,7 @@ from plane.db.models import Project, Estimate, EstimatePoint
from plane.api.serializers import (
EstimateSerializer,
EstimatePointSerializer,
EstimateReadSerializer,
)
@@ -50,38 +51,10 @@ class BulkEstimatePointEndpoint(BaseViewSet):
def list(self, request, slug, project_id):
try:
estimates = (
Estimate.objects.filter(workspace__slug=slug, project_id=project_id)
.prefetch_related("points")
.select_related("workspace", "project")
)
serializer = EstimateSerializer(
estimates,
fields=[
"id",
"created_at",
"created_by",
"updated_at",
"updated_by",
"workspace",
"project",
{"workspace_detail": ["id", "name", "slug"]},
{
"project_detail": [
"id",
"name",
"cover_image",
"icon_prop",
"emoji",
"description",
]
},
"name",
"points",
"description",
],
many=True,
)
estimates = Estimate.objects.filter(
workspace__slug=slug, project_id=project_id
).prefetch_related("points").select_related("workspace", "project")
serializer = EstimateReadSerializer(estimates, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
except Exception as e:
capture_exception(e)
@@ -106,31 +79,7 @@ class BulkEstimatePointEndpoint(BaseViewSet):
status=status.HTTP_400_BAD_REQUEST,
)
estimate_serializer = EstimateSerializer(
data=request.data.get("estimate"),
fields=[
"id",
"created_at",
"created_by",
"updated_at",
"updated_by",
"workspace",
"project",
{"workspace_detail": ["id", "name", "slug"]},
{
"project_detail": [
"id",
"name",
"cover_image",
"icon_prop",
"emoji",
"description",
]
},
"name",
"description",
],
)
estimate_serializer = EstimateSerializer(data=request.data.get("estimate"))
if not estimate_serializer.is_valid():
return Response(
estimate_serializer.errors, status=status.HTTP_400_BAD_REQUEST
@@ -188,32 +137,7 @@ class BulkEstimatePointEndpoint(BaseViewSet):
estimate = Estimate.objects.get(
pk=estimate_id, workspace__slug=slug, project_id=project_id
)
serializer = EstimateSerializer(
estimate,
fields=[
"id",
"created_at",
"created_by",
"updated_at",
"updated_by",
"workspace",
"project",
{"workspace_detail": ["id", "name", "slug"]},
{
"project_detail": [
"id",
"name",
"cover_image",
"icon_prop",
"emoji",
"description",
]
},
"name",
"points",
"description",
],
)
serializer = EstimateReadSerializer(estimate)
return Response(
serializer.data,
status=status.HTTP_200_OK,
@@ -246,31 +170,7 @@ class BulkEstimatePointEndpoint(BaseViewSet):
estimate = Estimate.objects.get(pk=estimate_id)
estimate_serializer = EstimateSerializer(
estimate,
data=request.data.get("estimate"),
fields=[
"id",
"created_at",
"created_by",
"updated_at",
"updated_by",
"workspace",
"project",
{"workspace_detail": ["id", "name", "slug"]},
{
"project_detail": [
"id",
"name",
"cover_image",
"icon_prop",
"emoji",
"description",
]
},
"name",
"description",
],
partial=True,
estimate, data=request.data.get("estimate"), partial=True
)
if not estimate_serializer.is_valid():
return Response(
@@ -311,9 +211,7 @@ class BulkEstimatePointEndpoint(BaseViewSet):
try:
EstimatePoint.objects.bulk_update(
updated_estimate_points,
["value"],
batch_size=10,
updated_estimate_points, ["value"], batch_size=10,
)
except IntegrityError as e:
return Response(
@@ -321,9 +219,7 @@ class BulkEstimatePointEndpoint(BaseViewSet):
status=status.HTTP_400_BAD_REQUEST,
)
estimate_point_serializer = EstimatePointSerializer(
estimate_points, many=True
)
estimate_point_serializer = EstimatePointSerializer(estimate_points, many=True)
return Response(
{
"estimate": estimate_serializer.data,
+4 -28
View File
@@ -23,11 +23,11 @@ class ExportIssuesEndpoint(BaseAPIView):
try:
# Get the workspace
workspace = Workspace.objects.get(slug=slug)
provider = request.data.get("provider", False)
multiple = request.data.get("multiple", False)
project_ids = request.data.get("project", [])
if provider in ["csv", "xlsx", "json"]:
if not project_ids:
project_ids = Project.objects.filter(
@@ -77,38 +77,14 @@ class ExportIssuesEndpoint(BaseAPIView):
try:
exporter_history = ExporterHistory.objects.filter(
workspace__slug=slug
).select_related("workspace", "initiated_by")
).select_related("workspace","initiated_by")
if request.GET.get("per_page", False) and request.GET.get("cursor", False):
return self.paginate(
request=request,
queryset=exporter_history,
on_results=lambda exporter_history: ExporterHistorySerializer(
exporter_history,
fields=[
"id",
"created_by",
"created_at",
"updated_at",
"updated_by",
"initiated_by",
"status",
"url",
"token",
"project",
"provider",
{
"initiated_by_detail": [
"id",
"first_name",
"last_name",
"avatar",
"is_bot",
"display_name",
]
},
],
many=True,
exporter_history, many=True
).data,
)
else:
+3 -21
View File
@@ -14,7 +14,7 @@ from django.conf import settings
from .base import BaseAPIView
from plane.api.permissions import ProjectEntityPermission
from plane.db.models import Workspace, Project
from plane.api.serializers import ProjectSerializer, WorkSpaceSerializer
from plane.api.serializers import ProjectLiteSerializer, WorkspaceLiteSerializer
class GPTIntegrationEndpoint(BaseAPIView):
@@ -57,26 +57,8 @@ class GPTIntegrationEndpoint(BaseAPIView):
{
"response": text,
"response_html": text_html,
"project_detail": ProjectSerializer(
project,
fields=[
"id",
"name",
"cover_image",
"icon_prop",
"emoji",
"description",
"created_by",
"updated_by",
"created_at",
"updated_at",
],
read_only=True,
).data,
"workspace_detail": WorkSpaceSerializer(
workspace,
fields=["id", "name", "slug"],
).data,
"project_detail": ProjectLiteSerializer(project).data,
"workspace_detail": WorkspaceLiteSerializer(workspace).data,
},
status=status.HTTP_200_OK,
)
+5 -167
View File
@@ -42,6 +42,7 @@ from plane.utils.html_processor import strip_tags
class ServiceIssueImportSummaryEndpoint(BaseAPIView):
def get(self, request, slug, service):
try:
if service == "github":
@@ -176,47 +177,7 @@ class ImportServiceEndpoint(BaseAPIView):
)
service_importer.delay(service, importer.id)
serializer = ImporterSerializer(
importer,
fields=[
"id",
"created_by",
"created_at",
"updated_at",
"updated_by",
"workspace",
"project",
"service",
"status",
"initiated_by",
"metadata",
"config",
"data",
"token",
"imported_data",
{
"initiated_by_detail": [
"id",
"first_name",
"last_name",
"avatar",
"is_bot",
"display_name",
],
},
{
"project_detail": [
"id",
"name",
"cover_image",
"icon_prop",
"emoji",
"description",
]
},
{"workspace_detail": ["id", "name", "slug"]},
],
)
serializer = ImporterSerializer(importer)
return Response(serializer.data, status=status.HTTP_201_CREATED)
if service == "jira":
@@ -252,47 +213,7 @@ class ImportServiceEndpoint(BaseAPIView):
)
service_importer.delay(service, importer.id)
serializer = ImporterSerializer(
importer,
fields=[
"id",
"created_by",
"created_at",
"updated_at",
"updated_by",
"workspace",
"project",
"service",
"status",
"initiated_by",
"metadata",
"config",
"data",
"token",
"imported_data",
{
"initiated_by_detail": [
"id",
"first_name",
"last_name",
"avatar",
"is_bot",
"display_name",
],
},
{
"project_detail": [
"id",
"name",
"cover_image",
"icon_prop",
"emoji",
"description",
]
},
{"workspace_detail": ["id", "name", "slug"]},
],
)
serializer = ImporterSerializer(importer)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(
@@ -322,48 +243,7 @@ class ImportServiceEndpoint(BaseAPIView):
.order_by("-created_at")
.select_related("initiated_by", "project", "workspace")
)
serializer = ImporterSerializer(
imports,
fields=[
"id",
"created_by",
"created_at",
"updated_at",
"updated_by",
"workspace",
"project",
"service",
"status",
"initiated_by",
"metadata",
"config",
"data",
"token",
"imported_data",
{
"initiated_by_detail": [
"id",
"first_name",
"last_name",
"avatar",
"is_bot",
"display_name",
],
},
{
"project_detail": [
"id",
"name",
"cover_image",
"icon_prop",
"emoji",
"description",
]
},
{"workspace_detail": ["id", "name", "slug"]},
],
many=True,
)
serializer = ImporterSerializer(imports, many=True)
return Response(serializer.data)
except Exception as e:
capture_exception(e)
@@ -404,49 +284,7 @@ class ImportServiceEndpoint(BaseAPIView):
importer = Importer.objects.get(
pk=pk, service=service, workspace__slug=slug
)
serializer = ImporterSerializer(
importer,
fields=[
"id",
"created_by",
"created_at",
"updated_at",
"updated_by",
"workspace",
"project",
"service",
"status",
"initiated_by",
"metadata",
"config",
"data",
"token",
"imported_data",
{
"initiated_by_detail": [
"id",
"first_name",
"last_name",
"avatar",
"is_bot",
"display_name",
],
},
{
"project_detail": [
"id",
"name",
"cover_image",
"icon_prop",
"emoji",
"description",
]
},
{"workspace_detail": ["id", "name", "slug"]},
],
data=request.data,
partial=True,
)
serializer = ImporterSerializer(importer, data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
+43 -371
View File
@@ -152,22 +152,7 @@ class InboxIssueViewSet(BaseViewSet):
)
)
)
issues_data = IssueStateInboxSerializer(
issues,
fields=[
{
"assignee_details": (
"id",
"first_name",
"last_name",
"avatar",
"is_bot",
"display_name",
)
}
],
many=True,
).data
issues_data = IssueStateInboxSerializer(issues, many=True).data
return Response(
issues_data,
status=status.HTTP_200_OK,
@@ -237,19 +222,7 @@ class InboxIssueViewSet(BaseViewSet):
source=request.data.get("source", "in-app"),
)
serializer = IssueStateInboxSerializer(
issue,
nested_fields={
"assignee_details": (
"id",
"first_name",
"last_name",
"avatar",
"is_bot",
"display_name",
)
},
)
serializer = IssueStateInboxSerializer(issue)
return Response(serializer.data, status=status.HTTP_200_OK)
except Exception as e:
capture_exception(e)
@@ -264,17 +237,10 @@ class InboxIssueViewSet(BaseViewSet):
pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
)
# Get the project member
project_member = ProjectMember.objects.get(
workspace__slug=slug, project_id=project_id, member=request.user
)
project_member = ProjectMember.objects.get(workspace__slug=slug, project_id=project_id, member=request.user)
# Only project members admins and created_by users can access this endpoint
if project_member.role <= 10 and str(inbox_issue.created_by_id) != str(
request.user.id
):
return Response(
{"error": "You cannot edit inbox issues"},
status=status.HTTP_400_BAD_REQUEST,
)
if project_member.role <= 10 and str(inbox_issue.created_by_id) != str(request.user.id):
return Response({"error": "You cannot edit inbox issues"}, status=status.HTTP_400_BAD_REQUEST)
# Get issue data
issue_data = request.data.pop("issue", False)
@@ -285,99 +251,15 @@ class InboxIssueViewSet(BaseViewSet):
)
# Only allow guests and viewers to edit name and description
if project_member.role <= 10:
# viewers and guests since only viewers and guests
# viewers and guests since only viewers and guests
issue_data = {
"name": issue_data.get("name", issue.name),
"description_html": issue_data.get(
"description_html", issue.description_html
),
"description": issue_data.get("description", issue.description),
"description_html": issue_data.get("description_html", issue.description_html),
"description": issue_data.get("description", issue.description)
}
issue_serializer = IssueCreateSerializer(
issue,
data=issue_data,
fields=[
{
"created_by_detail": [
"id",
"first_name",
"last_name",
"avatar",
"is_bot",
"display_name",
]
},
{
"state_detail": [
"id",
"created_by",
"created_at",
"updated_at",
"updated_by",
"name",
"description",
"color",
"slug",
"sequence",
"group",
"default",
"project",
"workspace",
{"workspace_detail": ["id", "name", "slug"]},
{
"project_detail": [
"id",
"name",
"cover_image",
"icon_prop",
"emoji",
"description",
]
},
],
},
{
"project_detail": [
"id",
"name",
"cover_image",
"icon_prop",
"emoji",
"description",
]
},
{"workspace_detail": ["id", "name", "slug"]},
"id",
"created_at",
"created_by",
"updated_at",
"updated_by",
"workspace",
"project",
"assignees_list",
"blockers_list",
"labels_list",
"blocks_list",
"PRIORITY_CHOICES",
"parent",
"state",
"estimate_point",
"name",
"description",
"description_html",
"description_stripped",
"priority",
"start_date",
"target_date",
"assignees",
"sequence_id",
"labels",
"sort_order",
"completed_at",
"archived_at",
],
partial=True,
issue, data=issue_data, partial=True
)
if issue_serializer.is_valid():
@@ -405,30 +287,7 @@ class InboxIssueViewSet(BaseViewSet):
# Only project admins and members can edit inbox issue attributes
if project_member.role > 10:
serializer = InboxIssueSerializer(
inbox_issue,
fields=[
{
"project_detail": [
"id",
"name",
"cover_image",
"icon_prop",
"emoji",
"description",
]
},
"id",
"created_at",
"updated_at",
"inbox",
"issue",
"status",
"snoozed_till",
"duplicate_to",
"source",
],
data=request.data,
partial=True,
inbox_issue, data=request.data, partial=True
)
if serializer.is_valid():
@@ -441,9 +300,7 @@ class InboxIssueViewSet(BaseViewSet):
project_id=project_id,
)
state = State.objects.filter(
group="cancelled",
workspace__slug=slug,
project_id=project_id,
group="cancelled", workspace__slug=slug, project_id=project_id
).first()
if state is not None:
issue.state = state
@@ -461,9 +318,7 @@ class InboxIssueViewSet(BaseViewSet):
if issue.state.name == "Triage":
# Move to default state
state = State.objects.filter(
workspace__slug=slug,
project_id=project_id,
default=True,
workspace__slug=slug, project_id=project_id, default=True
).first()
if state is not None:
issue.state = state
@@ -472,9 +327,7 @@ class InboxIssueViewSet(BaseViewSet):
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
else:
return Response(
InboxIssueSerializer(inbox_issue).data, status=status.HTTP_200_OK
)
return Response(InboxIssueSerializer(inbox_issue).data, status=status.HTTP_200_OK)
except InboxIssue.DoesNotExist:
return Response(
{"error": "Inbox Issue does not exist"},
@@ -495,19 +348,7 @@ class InboxIssueViewSet(BaseViewSet):
issue = Issue.objects.get(
pk=inbox_issue.issue_id, workspace__slug=slug, project_id=project_id
)
serializer = IssueStateInboxSerializer(
issue,
nested_fields={
"assignee_details": (
"id",
"first_name",
"last_name",
"avatar",
"is_bot",
"display_name",
)
},
)
serializer = IssueStateInboxSerializer(issue)
return Response(serializer.data, status=status.HTTP_200_OK)
except Exception as e:
capture_exception(e)
@@ -522,25 +363,15 @@ class InboxIssueViewSet(BaseViewSet):
pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
)
# Get the project member
project_member = ProjectMember.objects.get(
workspace__slug=slug, project_id=project_id, member=request.user
)
project_member = ProjectMember.objects.get(workspace__slug=slug, project_id=project_id, member=request.user)
if project_member.role <= 10 and str(inbox_issue.created_by_id) != str(
request.user.id
):
return Response(
{"error": "You cannot delete inbox issue"},
status=status.HTTP_400_BAD_REQUEST,
)
if project_member.role <= 10 and str(inbox_issue.created_by_id) != str(request.user.id):
return Response({"error": "You cannot delete inbox issue"}, status=status.HTTP_400_BAD_REQUEST)
inbox_issue.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
except InboxIssue.DoesNotExist:
return Response(
{"error": "Inbox Issue does not exists"},
status=status.HTTP_400_BAD_REQUEST,
)
return Response({"error": "Inbox Issue does not exists"}, status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
capture_exception(e)
return Response(
@@ -558,10 +389,7 @@ class InboxIssuePublicViewSet(BaseViewSet):
]
def get_queryset(self):
project_deploy_board = ProjectDeployBoard.objects.get(
workspace__slug=self.kwargs.get("slug"),
project_id=self.kwargs.get("project_id"),
)
project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=self.kwargs.get("slug"), project_id=self.kwargs.get("project_id"))
if project_deploy_board is not None:
return self.filter_queryset(
super()
@@ -579,14 +407,9 @@ class InboxIssuePublicViewSet(BaseViewSet):
def list(self, request, slug, project_id, inbox_id):
try:
project_deploy_board = ProjectDeployBoard.objects.get(
workspace__slug=slug, project_id=project_id
)
project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id)
if project_deploy_board.inbox is None:
return Response(
{"error": "Inbox is not enabled for this Project Board"},
status=status.HTTP_400_BAD_REQUEST,
)
return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST)
filters = issue_filters(request.query_params, "GET")
issues = (
@@ -629,29 +452,13 @@ class InboxIssuePublicViewSet(BaseViewSet):
)
)
)
issues_data = IssueStateInboxSerializer(
issues,
nested_fields={
"assignee_details": (
"id",
"first_name",
"last_name",
"avatar",
"is_bot",
"display_name",
)
},
many=True,
).data
issues_data = IssueStateInboxSerializer(issues, many=True).data
return Response(
issues_data,
status=status.HTTP_200_OK,
)
except ProjectDeployBoard.DoesNotExist:
return Response(
{"error": "Project Deploy Board does not exist"},
status=status.HTTP_400_BAD_REQUEST,
)
return Response({"error": "Project Deploy Board does not exist"}, status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
capture_exception(e)
return Response(
@@ -661,14 +468,9 @@ class InboxIssuePublicViewSet(BaseViewSet):
def create(self, request, slug, project_id, inbox_id):
try:
project_deploy_board = ProjectDeployBoard.objects.get(
workspace__slug=slug, project_id=project_id
)
project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id)
if project_deploy_board.inbox is None:
return Response(
{"error": "Inbox is not enabled for this Project Board"},
status=status.HTTP_400_BAD_REQUEST,
)
return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST)
if not request.data.get("issue", {}).get("name", False):
return Response(
@@ -725,19 +527,7 @@ class InboxIssuePublicViewSet(BaseViewSet):
source=request.data.get("source", "in-app"),
)
serializer = IssueStateInboxSerializer(
issue,
nested_fields={
"assignee_details": (
"id",
"first_name",
"last_name",
"avatar",
"is_bot",
"display_name",
)
},
)
serializer = IssueStateInboxSerializer(issue)
return Response(serializer.data, status=status.HTTP_200_OK)
except Exception as e:
capture_exception(e)
@@ -748,124 +538,33 @@ class InboxIssuePublicViewSet(BaseViewSet):
def partial_update(self, request, slug, project_id, inbox_id, pk):
try:
project_deploy_board = ProjectDeployBoard.objects.get(
workspace__slug=slug, project_id=project_id
)
project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id)
if project_deploy_board.inbox is None:
return Response(
{"error": "Inbox is not enabled for this Project Board"},
status=status.HTTP_400_BAD_REQUEST,
)
return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST)
inbox_issue = InboxIssue.objects.get(
pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
)
# Get the project member
if str(inbox_issue.created_by_id) != str(request.user.id):
return Response(
{"error": "You cannot edit inbox issues"},
status=status.HTTP_400_BAD_REQUEST,
)
return Response({"error": "You cannot edit inbox issues"}, status=status.HTTP_400_BAD_REQUEST)
# Get issue data
issue_data = request.data.pop("issue", False)
issue = Issue.objects.get(
pk=inbox_issue.issue_id, workspace__slug=slug, project_id=project_id
)
# viewers and guests since only viewers and guests
# viewers and guests since only viewers and guests
issue_data = {
"name": issue_data.get("name", issue.name),
"description_html": issue_data.get(
"description_html", issue.description_html
),
"description": issue_data.get("description", issue.description),
"description_html": issue_data.get("description_html", issue.description_html),
"description": issue_data.get("description", issue.description)
}
issue_serializer = IssueCreateSerializer(
issue,
data=issue_data,
fields=[
{
"created_by_detail": [
"id",
"first_name",
"last_name",
"avatar",
"is_bot",
"display_name",
]
},
{
"state_detail": [
"id",
"created_by",
"created_at",
"updated_at",
"updated_by",
"name",
"description",
"color",
"slug",
"sequence",
"group",
"default",
"project",
"workspace",
{"workspace_detail": ["id", "name", "slug"]},
{
"project_detail": [
"id",
"name",
"cover_image",
"icon_prop",
"emoji",
"description",
]
},
],
},
{
"project_detail": [
"id",
"name",
"cover_image",
"icon_prop",
"emoji",
"description",
]
},
{"workspace_detail": ["id", "name", "slug"]},
"id",
"created_at",
"created_by",
"updated_at",
"updated_by",
"workspace",
"project",
"assignees_list",
"blockers_list",
"labels_list",
"blocks_list",
"PRIORITY_CHOICES",
"parent",
"state",
"estimate_point",
"name",
"description",
"description_html",
"description_stripped",
"priority",
"start_date",
"target_date",
"assignees",
"sequence_id",
"labels",
"sort_order",
"completed_at",
"archived_at",
],
partial=True,
issue, data=issue_data, partial=True
)
if issue_serializer.is_valid():
@@ -901,34 +600,17 @@ class InboxIssuePublicViewSet(BaseViewSet):
def retrieve(self, request, slug, project_id, inbox_id, pk):
try:
project_deploy_board = ProjectDeployBoard.objects.get(
workspace__slug=slug, project_id=project_id
)
project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id)
if project_deploy_board.inbox is None:
return Response(
{"error": "Inbox is not enabled for this Project Board"},
status=status.HTTP_400_BAD_REQUEST,
)
return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST)
inbox_issue = InboxIssue.objects.get(
pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
)
issue = Issue.objects.get(
pk=inbox_issue.issue_id, workspace__slug=slug, project_id=project_id
)
serializer = IssueStateInboxSerializer(
issue,
nested_fields={
"assignee_details": (
"id",
"first_name",
"last_name",
"avatar",
"is_bot",
"display_name",
)
},
)
serializer = IssueStateInboxSerializer(issue)
return Response(serializer.data, status=status.HTTP_200_OK)
except Exception as e:
capture_exception(e)
@@ -939,35 +621,25 @@ class InboxIssuePublicViewSet(BaseViewSet):
def destroy(self, request, slug, project_id, inbox_id, pk):
try:
project_deploy_board = ProjectDeployBoard.objects.get(
workspace__slug=slug, project_id=project_id
)
project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id)
if project_deploy_board.inbox is None:
return Response(
{"error": "Inbox is not enabled for this Project Board"},
status=status.HTTP_400_BAD_REQUEST,
)
return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST)
inbox_issue = InboxIssue.objects.get(
pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
)
if str(inbox_issue.created_by_id) != str(request.user.id):
return Response(
{"error": "You cannot delete inbox issue"},
status=status.HTTP_400_BAD_REQUEST,
)
return Response({"error": "You cannot delete inbox issue"}, status=status.HTTP_400_BAD_REQUEST)
inbox_issue.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
except InboxIssue.DoesNotExist:
return Response(
{"error": "Inbox Issue does not exists"},
status=status.HTTP_400_BAD_REQUEST,
)
return Response({"error": "Inbox Issue does not exists"}, status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
+349 -209
View File
@@ -28,6 +28,7 @@ from django.conf import settings
from rest_framework.response import Response
from rest_framework import status
from rest_framework.parsers import MultiPartParser, FormParser
from rest_framework.permissions import AllowAny
from sentry_sdk import capture_exception
# Module imports
@@ -49,6 +50,7 @@ from plane.api.serializers import (
IssueReactionSerializer,
CommentReactionSerializer,
IssueVoteSerializer,
IssuePublicSerializer,
)
from plane.api.permissions import (
WorkspaceEntityPermission,
@@ -73,10 +75,12 @@ from plane.db.models import (
CommentReaction,
ProjectDeployBoard,
IssueVote,
ProjectPublicMember,
)
from plane.bgtasks.issue_activites_task import issue_activity
from plane.utils.grouper import group_results
from plane.utils.issue_filters import issue_filters
from plane.bgtasks.export_task import issue_export_task
class IssueViewSet(BaseViewSet):
@@ -281,86 +285,6 @@ class IssueViewSet(BaseViewSet):
serializer = IssueCreateSerializer(
data=request.data,
fields=[
{
"created_by_detail": [
"id",
"first_name",
"last_name",
"avatar",
"is_bot",
"display_name",
]
},
{
"state_detail": [
"id",
"created_by",
"created_at",
"updated_at",
"updated_by",
"name",
"description",
"color",
"slug",
"sequence",
"group",
"default",
"project",
"workspace",
{"workspace_detail": ["id", "name", "slug"]},
{
"project_detail": [
"id",
"name",
"cover_image",
"icon_prop",
"emoji",
"description",
]
},
],
},
{
"project_detail": [
"id",
"name",
"cover_image",
"icon_prop",
"emoji",
"description",
]
},
{"workspace_detail": ["id", "name", "slug"]},
"id",
"created_at",
"created_by",
"updated_at",
"updated_by",
"workspace",
"project",
"assignees_list",
"blockers_list",
"labels_list",
"blocks_list",
"PRIORITY_CHOICES",
"parent",
"state",
"estimate_point",
"name",
"description",
"description_html",
"description_stripped",
"priority",
"start_date",
"target_date",
"assignees",
"sequence_id",
"labels",
"sort_order",
"completed_at",
"archived_at",
],
context={
"project_id": project_id,
"workspace_id": project.workspace_id,
@@ -562,7 +486,7 @@ class IssueActivityEndpoint(BaseAPIView):
issue_activities = (
IssueActivity.objects.filter(issue_id=issue_id)
.filter(
~Q(field="comment"),
~Q(field__in=["comment", "vote", "reaction"]),
project__project_projectmember__member=self.request.user,
)
.select_related("actor", "workspace", "issue", "project")
@@ -572,35 +496,15 @@ class IssueActivityEndpoint(BaseAPIView):
.filter(project__project_projectmember__member=self.request.user)
.order_by("created_at")
.select_related("actor", "issue", "project", "workspace")
.prefetch_related(
Prefetch(
"comment_reactions",
queryset=CommentReaction.objects.select_related("actor"),
)
)
)
issue_activities = IssueActivitySerializer(
issue_activities,
nested_fields={
"actor_detail": (
"id",
"first_name",
"last_name",
"avatar",
"is_bot",
"display_name",
)
},
many=True,
).data
issue_comments = IssueCommentSerializer(
issue_comments,
nested_fields={
"actor_detail": (
"id",
"first_name",
"last_name",
"avatar",
"is_bot",
"display_name",
)
},
many=True,
).data
issue_activities = IssueActivitySerializer(issue_activities, many=True).data
issue_comments = IssueCommentSerializer(issue_comments, many=True).data
result_list = sorted(
chain(issue_activities, issue_comments),
@@ -628,9 +532,6 @@ class IssueCommentViewSet(BaseViewSet):
"workspace__id",
]
# def get_serializer_class(self):
# return IssueCommentSerializer(nested_fields={"actor_detail":("id", "first_name", "last_name", "avatar", "is_bot", "display_name")})
def perform_create(self, serializer):
serializer.save(
project_id=self.kwargs.get("project_id"),
@@ -659,19 +560,7 @@ class IssueCommentViewSet(BaseViewSet):
issue_id=str(self.kwargs.get("issue_id", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=json.dumps(
IssueCommentSerializer(
current_instance,
nested_fields={
"actor_detail": (
"id",
"first_name",
"last_name",
"avatar",
"is_bot",
"display_name",
)
},
).data,
IssueCommentSerializer(current_instance).data,
cls=DjangoJSONEncoder,
),
)
@@ -692,19 +581,7 @@ class IssueCommentViewSet(BaseViewSet):
issue_id=str(self.kwargs.get("issue_id", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=json.dumps(
IssueCommentSerializer(
current_instance,
nested_fields={
"actor_detail": (
"id",
"first_name",
"last_name",
"avatar",
"is_bot",
"display_name",
)
},
).data,
IssueCommentSerializer(current_instance).data,
cls=DjangoJSONEncoder,
),
)
@@ -721,6 +598,15 @@ class IssueCommentViewSet(BaseViewSet):
.select_related("project")
.select_related("workspace")
.select_related("issue")
.annotate(
is_member=Exists(
ProjectMember.objects.filter(
workspace__slug=self.kwargs.get("slug"),
project_id=self.kwargs.get("project_id"),
member_id=self.request.user.id,
)
)
)
.distinct()
)
@@ -1519,6 +1405,14 @@ class IssueReactionViewSet(BaseViewSet):
project_id=self.kwargs.get("project_id"),
actor=self.request.user,
)
issue_activity.delay(
type="issue_reaction.activity.created",
requested_data=json.dumps(self.request.data, cls=DjangoJSONEncoder),
actor_id=str(self.request.user.id),
issue_id=str(self.kwargs.get("issue_id", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=None,
)
def destroy(self, request, slug, project_id, issue_id, reaction_code):
try:
@@ -1529,6 +1423,19 @@ class IssueReactionViewSet(BaseViewSet):
reaction=reaction_code,
actor=request.user,
)
issue_activity.delay(
type="issue_reaction.activity.deleted",
requested_data=None,
actor_id=str(self.request.user.id),
issue_id=str(self.kwargs.get("issue_id", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=json.dumps(
{
"reaction": str(reaction_code),
"identifier": str(issue_reaction.id),
}
),
)
issue_reaction.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
except IssueReaction.DoesNotExist:
@@ -1569,6 +1476,14 @@ class CommentReactionViewSet(BaseViewSet):
comment_id=self.kwargs.get("comment_id"),
project_id=self.kwargs.get("project_id"),
)
issue_activity.delay(
type="comment_reaction.activity.created",
requested_data=json.dumps(self.request.data, cls=DjangoJSONEncoder),
actor_id=str(self.request.user.id),
issue_id=None,
project_id=str(self.kwargs.get("project_id", None)),
current_instance=None,
)
def destroy(self, request, slug, project_id, comment_id, reaction_code):
try:
@@ -1579,6 +1494,20 @@ class CommentReactionViewSet(BaseViewSet):
reaction=reaction_code,
actor=request.user,
)
issue_activity.delay(
type="comment_reaction.activity.deleted",
requested_data=None,
actor_id=str(self.request.user.id),
issue_id=None,
project_id=str(self.kwargs.get("project_id", None)),
current_instance=json.dumps(
{
"reaction": str(reaction_code),
"identifier": str(comment_reaction.id),
"comment_id": str(comment_id)
}
),
)
comment_reaction.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
except CommentReaction.DoesNotExist:
@@ -1614,9 +1543,19 @@ class IssueCommentPublicViewSet(BaseViewSet):
.get_queryset()
.filter(workspace__slug=self.kwargs.get("slug"))
.filter(issue_id=self.kwargs.get("issue_id"))
.filter(access="EXTERNAL")
.select_related("project")
.select_related("workspace")
.select_related("issue")
.annotate(
is_member=Exists(
ProjectMember.objects.filter(
workspace__slug=self.kwargs.get("slug"),
project_id=self.kwargs.get("project_id"),
member_id=self.request.user.id,
)
)
)
.distinct()
)
else:
@@ -1634,33 +1573,13 @@ class IssueCommentPublicViewSet(BaseViewSet):
status=status.HTTP_400_BAD_REQUEST,
)
access = (
"INTERNAL"
if ProjectMember.objects.filter(
project_id=project_id, member=request.user
).exists()
else "EXTERNAL"
)
serializer = IssueCommentSerializer(
data=request.data,
nested_fields={
"actor_detail": (
"id",
"first_name",
"last_name",
"avatar",
"is_bot",
"display_name",
)
},
)
serializer = IssueCommentSerializer(data=request.data)
if serializer.is_valid():
serializer.save(
project_id=project_id,
issue_id=issue_id,
actor=request.user,
access=access,
access="EXTERNAL",
)
issue_activity.delay(
type="comment.activity.created",
@@ -1670,6 +1589,16 @@ class IssueCommentPublicViewSet(BaseViewSet):
project_id=str(project_id),
current_instance=None,
)
if not ProjectMember.objects.filter(
project_id=project_id,
member=request.user,
).exists():
# Add the user for workspace tracking
_ = ProjectPublicMember.objects.get_or_create(
project_id=project_id,
member=request.user,
)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
@@ -1694,19 +1623,7 @@ class IssueCommentPublicViewSet(BaseViewSet):
workspace__slug=slug, pk=pk, actor=request.user
)
serializer = IssueCommentSerializer(
comment,
data=request.data,
nested_fields={
"actor_detail": (
"id",
"first_name",
"last_name",
"avatar",
"is_bot",
"display_name",
)
},
partial=True,
comment, data=request.data, partial=True
)
if serializer.is_valid():
serializer.save()
@@ -1717,19 +1634,7 @@ class IssueCommentPublicViewSet(BaseViewSet):
issue_id=str(issue_id),
project_id=str(project_id),
current_instance=json.dumps(
IssueCommentSerializer(
comment,
nested_fields={
"actor_detail": (
"id",
"first_name",
"last_name",
"avatar",
"is_bot",
"display_name",
)
},
).data,
IssueCommentSerializer(comment).data,
cls=DjangoJSONEncoder,
),
)
@@ -1762,19 +1667,7 @@ class IssueCommentPublicViewSet(BaseViewSet):
issue_id=str(issue_id),
project_id=str(project_id),
current_instance=json.dumps(
IssueCommentSerializer(
comment,
nested_fields={
"actor_detail": (
"id",
"first_name",
"last_name",
"avatar",
"is_bot",
"display_name",
)
},
).data,
IssueCommentSerializer(comment).data,
cls=DjangoJSONEncoder,
),
)
@@ -1832,6 +1725,23 @@ class IssueReactionPublicViewSet(BaseViewSet):
serializer.save(
project_id=project_id, issue_id=issue_id, actor=request.user
)
if not ProjectMember.objects.filter(
project_id=project_id,
member=request.user,
).exists():
# Add the user for workspace tracking
_ = ProjectPublicMember.objects.get_or_create(
project_id=project_id,
member=request.user,
)
issue_activity.delay(
type="issue_reaction.activity.created",
requested_data=json.dumps(self.request.data, cls=DjangoJSONEncoder),
actor_id=str(self.request.user.id),
issue_id=str(self.kwargs.get("issue_id", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=None,
)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except ProjectDeployBoard.DoesNotExist:
@@ -1863,6 +1773,19 @@ class IssueReactionPublicViewSet(BaseViewSet):
reaction=reaction_code,
actor=request.user,
)
issue_activity.delay(
type="issue_reaction.activity.deleted",
requested_data=None,
actor_id=str(self.request.user.id),
issue_id=str(self.kwargs.get("issue_id", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=json.dumps(
{
"reaction": str(reaction_code),
"identifier": str(issue_reaction.id),
}
),
)
issue_reaction.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
except IssueReaction.DoesNotExist:
@@ -1917,8 +1840,29 @@ class CommentReactionPublicViewSet(BaseViewSet):
serializer.save(
project_id=project_id, comment_id=comment_id, actor=request.user
)
if not ProjectMember.objects.filter(
project_id=project_id, member=request.user
).exists():
# Add the user for workspace tracking
_ = ProjectPublicMember.objects.get_or_create(
project_id=project_id,
member=request.user,
)
issue_activity.delay(
type="comment_reaction.activity.created",
requested_data=json.dumps(self.request.data, cls=DjangoJSONEncoder),
actor_id=str(self.request.user.id),
issue_id=None,
project_id=str(self.kwargs.get("project_id", None)),
current_instance=None,
)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except IssueComment.DoesNotExist:
return Response(
{"error": "Comment does not exist"},
status=status.HTTP_400_BAD_REQUEST,
)
except ProjectDeployBoard.DoesNotExist:
return Response(
{"error": "Project board does not exist"},
@@ -1949,6 +1893,20 @@ class CommentReactionPublicViewSet(BaseViewSet):
reaction=reaction_code,
actor=request.user,
)
issue_activity.delay(
type="comment_reaction.activity.deleted",
requested_data=None,
actor_id=str(self.request.user.id),
issue_id=None,
project_id=str(self.kwargs.get("project_id", None)),
current_instance=json.dumps(
{
"reaction": str(reaction_code),
"identifier": str(comment_reaction.id),
"comment_id": str(comment_id)
}
),
)
comment_reaction.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
except CommentReaction.DoesNotExist:
@@ -1983,8 +1941,25 @@ class IssueVotePublicViewSet(BaseViewSet):
actor_id=request.user.id,
project_id=project_id,
issue_id=issue_id,
vote=request.data.get("vote", 1),
)
# Add the user for workspace tracking
if not ProjectMember.objects.filter(
project_id=project_id, member=request.user
).exists():
_ = ProjectPublicMember.objects.get_or_create(
project_id=project_id,
member=request.user,
)
issue_vote.vote = request.data.get("vote", 1)
issue_vote.save()
issue_activity.delay(
type="issue_vote.activity.created",
requested_data=json.dumps(self.request.data, cls=DjangoJSONEncoder),
actor_id=str(self.request.user.id),
issue_id=str(self.kwargs.get("issue_id", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=None,
)
serializer = IssueVoteSerializer(issue_vote)
return Response(serializer.data, status=status.HTTP_201_CREATED)
except Exception as e:
@@ -2002,6 +1977,19 @@ class IssueVotePublicViewSet(BaseViewSet):
issue_id=issue_id,
actor_id=request.user.id,
)
issue_activity.delay(
type="issue_vote.activity.deleted",
requested_data=None,
actor_id=str(self.request.user.id),
issue_id=str(self.kwargs.get("issue_id", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=json.dumps(
{
"vote": str(issue_vote.vote),
"identifier": str(issue_vote.id),
}
),
)
issue_vote.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
except Exception as e:
@@ -2012,29 +2000,181 @@ class IssueVotePublicViewSet(BaseViewSet):
)
class ExportIssuesEndpoint(BaseAPIView):
class IssueRetrievePublicEndpoint(BaseAPIView):
permission_classes = [
WorkSpaceAdminPermission,
AllowAny,
]
def post(self, request, slug):
def get(self, request, slug, project_id, issue_id):
try:
issue_export_task.delay(
email=request.user.email,
data=request.data,
slug=slug,
exporter_name=request.user.first_name,
issue = Issue.objects.get(
workspace__slug=slug, project_id=project_id, pk=issue_id
)
serializer = IssuePublicSerializer(issue)
return Response(serializer.data, status=status.HTTP_200_OK)
except Issue.DoesNotExist:
return Response(
{"error": "Issue Does not exist"}, status=status.HTTP_400_BAD_REQUEST
)
except Exception as e:
print(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class ProjectIssuesPublicEndpoint(BaseAPIView):
permission_classes = [
AllowAny,
]
def get(self, request, slug, project_id):
try:
project_deploy_board = ProjectDeployBoard.objects.get(
workspace__slug=slug, project_id=project_id
)
filters = issue_filters(request.query_params, "GET")
# Custom ordering for priority and state
priority_order = ["urgent", "high", "medium", "low", None]
state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
order_by_param = request.GET.get("order_by", "-created_at")
issue_queryset = (
Issue.issue_objects.annotate(
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.filter(project_id=project_id)
.filter(workspace__slug=slug)
.select_related("project", "workspace", "state", "parent")
.prefetch_related("assignees", "labels")
.prefetch_related(
Prefetch(
"issue_reactions",
queryset=IssueReaction.objects.select_related("actor"),
)
)
.prefetch_related(
Prefetch(
"votes",
queryset=IssueVote.objects.select_related("actor"),
)
)
.filter(**filters)
.annotate(cycle_id=F("issue_cycle__cycle_id"))
.annotate(module_id=F("issue_module__module_id"))
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
attachment_count=IssueAttachment.objects.filter(
issue=OuterRef("id")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
)
# Priority Ordering
if order_by_param == "priority" or order_by_param == "-priority":
priority_order = (
priority_order
if order_by_param == "priority"
else priority_order[::-1]
)
issue_queryset = issue_queryset.annotate(
priority_order=Case(
*[
When(priority=p, then=Value(i))
for i, p in enumerate(priority_order)
],
output_field=CharField(),
)
).order_by("priority_order")
# State Ordering
elif order_by_param in [
"state__name",
"state__group",
"-state__name",
"-state__group",
]:
state_order = (
state_order
if order_by_param in ["state__name", "state__group"]
else state_order[::-1]
)
issue_queryset = issue_queryset.annotate(
state_order=Case(
*[
When(state__group=state_group, then=Value(i))
for i, state_group in enumerate(state_order)
],
default=Value(len(state_order)),
output_field=CharField(),
)
).order_by("state_order")
# assignee and label ordering
elif order_by_param in [
"labels__name",
"-labels__name",
"assignees__first_name",
"-assignees__first_name",
]:
issue_queryset = issue_queryset.annotate(
max_values=Max(
order_by_param[1::]
if order_by_param.startswith("-")
else order_by_param
)
).order_by(
"-max_values" if order_by_param.startswith("-") else "max_values"
)
else:
issue_queryset = issue_queryset.order_by(order_by_param)
issues = IssuePublicSerializer(issue_queryset, many=True).data
states = State.objects.filter(
~Q(name="Triage"),
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(
{
"message": f"Once the export is ready it will be emailed to you at {str(request.user.email)}"
"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)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
+90 -3
View File
@@ -10,7 +10,13 @@ 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
@@ -83,13 +89,17 @@ 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):
@@ -274,3 +284,80 @@ 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,
)
+28 -199
View File
@@ -91,10 +91,10 @@ class ProjectViewSet(BaseViewSet):
ProjectBasePermission,
]
# def get_serializer_class(self, *args, **kwargs):
# if self.action == "update" or self.action == "partial_update":
# return ProjectSerializer(fields=['id', {'workspace_detail': ["id", "name", "slug"]}, 'created_at', 'updated_at', 'name', 'description', 'description_text', 'description_html', 'network', 'identifier', 'emoji', 'icon_prop', 'module_view', 'cycle_view', 'issue_views_view', 'page_view', 'inbox_view', 'cover_image', 'archive_in', 'close_in', 'created_by', 'updated_by', 'workspace', 'default_assignee', 'project_lead', 'estimate', 'default_state', 'sort_order'],)
# return ProjectDetailSerializer
def get_serializer_class(self, *args, **kwargs):
if self.action == "update" or self.action == "partial_update":
return ProjectSerializer
return ProjectDetailSerializer
def get_queryset(self):
subquery = ProjectFavorite.objects.filter(
@@ -216,38 +216,7 @@ class ProjectViewSet(BaseViewSet):
workspace = Workspace.objects.get(slug=slug)
serializer = ProjectSerializer(
data={**request.data},
fields=[
"id",
{"workspace_detail": ["id", "name", "slug"]},
"created_at",
"updated_at",
"name",
"description",
"description_text",
"description_html",
"network",
"identifier",
"emoji",
"icon_prop",
"module_view",
"cycle_view",
"issue_views_view",
"page_view",
"inbox_view",
"cover_image",
"archive_in",
"close_in",
"created_by",
"updated_by",
"workspace",
"default_assignee",
"project_lead",
"estimate",
"default_state",
"sort_order",
],
context={"workspace_id": workspace.id},
data={**request.data}, context={"workspace_id": workspace.id}
)
if serializer.is_valid():
serializer.save()
@@ -318,7 +287,10 @@ 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,
@@ -361,36 +333,6 @@ class ProjectViewSet(BaseViewSet):
serializer = ProjectSerializer(
project,
data={**request.data},
fields=[
"id",
{"workspace_detail": ["id", "name", "slug"]},
"created_at",
"updated_at",
"name",
"description",
"description_text",
"description_html",
"network",
"identifier",
"emoji",
"icon_prop",
"module_view",
"cycle_view",
"issue_views_view",
"page_view",
"inbox_view",
"cover_image",
"archive_in",
"close_in",
"created_by",
"updated_by",
"workspace",
"default_assignee",
"project_lead",
"estimate",
"default_state",
"sort_order",
],
context={"workspace_id": workspace.id},
partial=True,
)
@@ -1201,146 +1143,33 @@ class ProjectDeployBoardPublicSettingsEndpoint(BaseAPIView):
)
class ProjectDeployBoardIssuesPublicEndpoint(BaseAPIView):
permission_classes = [
AllowAny,
]
class WorkspaceProjectDeployBoardEndpoint(BaseAPIView):
def get(self, request, slug, project_id):
permission_classes = [AllowAny,]
def get(self, request, slug):
try:
project_deploy_board = ProjectDeployBoard.objects.get(
workspace__slug=slug, project_id=project_id
)
filters = issue_filters(request.query_params, "GET")
# Custom ordering for priority and state
priority_order = ["urgent", "high", "medium", "low", None]
state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
order_by_param = request.GET.get("order_by", "-created_at")
issue_queryset = (
Issue.issue_objects.annotate(
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.filter(project_id=project_id)
.filter(workspace__slug=slug)
.select_related("project", "workspace", "state", "parent")
.prefetch_related("assignees", "labels")
.prefetch_related(
Prefetch(
"issue_reactions",
queryset=IssueReaction.objects.select_related("actor"),
)
)
.filter(**filters)
.annotate(cycle_id=F("issue_cycle__cycle_id"))
.annotate(module_id=F("issue_module__module_id"))
projects = (
Project.objects.filter(workspace__slug=slug)
.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")
is_public=Exists(
ProjectDeployBoard.objects.filter(
workspace__slug=slug, project_id=OuterRef("pk")
)
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.filter(is_public=True)
).values(
"id",
"identifier",
"name",
"description",
"emoji",
"icon_prop",
"cover_image",
)
# 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(
{
"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
)
return Response(projects, status=status.HTTP_200_OK)
except Exception as e:
capture_exception(e)
return Response(
+3 -64
View File
@@ -42,36 +42,7 @@ class StateViewSet(BaseViewSet):
def create(self, request, slug, project_id):
try:
serializer = StateSerializer(
data=request.data,
fields=[
"id",
"created_by",
"created_at",
"updated_at",
"updated_by",
"name",
"description",
"color",
"slug",
"sequence",
"group",
"default",
"project",
"workspace",
{"workspace_detail": ["id", "name", "slug"]},
{
"project_detail": [
"id",
"name",
"cover_image",
"icon_prop",
"emoji",
"description",
]
},
],
)
serializer = StateSerializer(data=request.data)
if serializer.is_valid():
serializer.save(project_id=project_id)
return Response(serializer.data, status=status.HTTP_200_OK)
@@ -91,37 +62,7 @@ class StateViewSet(BaseViewSet):
def list(self, request, slug, project_id):
try:
state_dict = dict()
states = StateSerializer(
self.get_queryset(),
fields=[
"id",
"created_by",
"created_at",
"updated_at",
"updated_by",
"name",
"description",
"color",
"slug",
"sequence",
"group",
"default",
"project",
"workspace",
{"workspace_detail": ["id", "name", "slug"]},
{
"project_detail": [
"id",
"name",
"cover_image",
"icon_prop",
"emoji",
"description",
]
},
],
many=True,
).data
states = StateSerializer(self.get_queryset(), many=True).data
for key, value in groupby(
sorted(states, key=lambda state: state["group"]),
@@ -141,9 +82,7 @@ class StateViewSet(BaseViewSet):
try:
state = State.objects.get(
~Q(name="Triage"),
pk=pk,
project_id=project_id,
workspace__slug=slug,
pk=pk, project_id=project_id, workspace__slug=slug,
)
if state.default:
+5 -16
View File
@@ -137,28 +137,17 @@ class UpdateUserTourCompletedEndpoint(BaseAPIView):
class UserActivityEndpoint(BaseAPIView, BasePaginator):
def get(self, request):
def get(self, request, slug):
try:
queryset = IssueActivity.objects.filter(actor=request.user).select_related(
"actor", "workspace", "issue", "project"
)
queryset = IssueActivity.objects.filter(
actor=request.user, workspace__slug=slug
).select_related("actor", "workspace", "issue", "project")
return self.paginate(
request=request,
queryset=queryset,
on_results=lambda issue_activities: IssueActivitySerializer(
issue_activities,
nested_fields={
"actor_detail": (
"id",
"first_name",
"last_name",
"avatar",
"is_bot",
"display_name",
)
},
many=True,
issue_activities, many=True
).data,
)
except Exception as e:
+17 -218
View File
@@ -42,11 +42,12 @@ from plane.api.serializers import (
WorkSpaceMemberSerializer,
TeamSerializer,
WorkSpaceMemberInviteSerializer,
UserSerializer,
UserLiteSerializer,
ProjectMemberSerializer,
WorkspaceThemeSerializer,
IssueActivitySerializer,
IssueLiteSerializer,
WorkspaceMemberAdminSerializer,
)
from plane.api.views.base import BaseAPIView
from . import BaseViewSet
@@ -163,8 +164,8 @@ class WorkSpaceViewSet(BaseViewSet):
status=status.HTTP_400_BAD_REQUEST,
)
# Handling unique integrity error for now
# TODO: Extend this to handle other common errors which are not automatically handled by APIException
## Handling unique integrity error for now
## TODO: Extend this to handle other common errors which are not automatically handled by APIException
except IntegrityError as e:
if "already exists" in str(e):
return Response(
@@ -299,31 +300,7 @@ class InviteWorkspaceEndpoint(BaseAPIView):
{
"error": "Some users are already member of workspace",
"workspace_users": WorkSpaceMemberSerializer(
workspace_members,
fields=[
"id",
"created_by",
"created_at",
"updated_at",
"updated_by",
"name",
"logo",
"owner",
"slug",
"organization_size",
{"workspace": ["id", "name", "slug"]},
{
"member": [
"id",
"first_name",
"last_name",
"avatar",
"is_bot",
"display_name",
]
},
],
many=True,
workspace_members, many=True
).data,
},
status=status.HTTP_400_BAD_REQUEST,
@@ -565,7 +542,7 @@ class UserWorkspaceInvitationsEndpoint(BaseViewSet):
class WorkSpaceMemberViewSet(BaseViewSet):
serializer_class = WorkSpaceMemberSerializer
serializer_class = WorkspaceMemberAdminSerializer
model = WorkspaceMember
permission_classes = [
@@ -614,32 +591,7 @@ class WorkSpaceMemberViewSet(BaseViewSet):
)
serializer = WorkSpaceMemberSerializer(
workspace_member,
data=request.data,
partial=True,
fields=[
"id",
"created_by",
"created_at",
"updated_at",
"updated_by",
"name",
"logo",
"owner",
"slug",
"organization_size",
{"workspace": ["id", "name", "slug"]},
{
"member": [
"id",
"first_name",
"last_name",
"avatar",
"is_bot",
"display_name",
]
},
],
workspace_member, data=request.data, partial=True
)
if serializer.is_valid():
@@ -773,18 +725,7 @@ class TeamMemberViewSet(BaseViewSet):
users = list(set(request.data.get("members", [])).difference(members))
users = User.objects.filter(pk__in=users)
serializer = UserSerializer(
users,
fields=[
"id",
"first_name",
"last_name",
"avatar",
"is_bot",
"display_name",
],
many=True,
)
serializer = UserLiteSerializer(users, many=True)
return Response(
{
"error": f"{len(users)} of the member(s) are not a part of the workspace",
@@ -850,95 +791,14 @@ class UserLastProjectWithWorkspaceEndpoint(BaseAPIView):
)
workspace = Workspace.objects.get(pk=last_workspace_id)
workspace_serializer = WorkSpaceSerializer(
workspace,
fields=[
"id",
{
"owner": [
"id",
"first_name",
"last_name",
"avatar",
"is_bot",
"display_name",
]
},
"created_at",
"updated_at",
"name",
"logo",
"slug",
"organization_size",
"created_by",
"updated_by",
],
)
workspace_serializer = WorkSpaceSerializer(workspace)
project_member = ProjectMember.objects.filter(
workspace_id=last_workspace_id, member=request.user
).select_related("workspace", "project", "member", "workspace__owner")
project_member_serializer = ProjectMemberSerializer(
project_member,
fields=[
"id",
{
"workspace": [
"id",
{
"owner": [
"id",
"first_name",
"last_name",
"avatar",
"is_bot",
"display_name",
]
},
"created_at",
"updated_at",
"name",
"logo",
"slug",
"organization_size",
"created_by",
"updated_by",
]
},
{
"project": [
"id",
"identifier",
"name",
"cover_image",
"icon_prop",
"emoji",
"description",
]
},
{
"member": [
"id",
"first_name",
"last_name",
"avatar",
"is_bot",
"display_name",
]
},
"created_at",
"updated_at",
"comment",
"role",
"view_props",
"default_props",
"preferences",
"sort_order",
"created_by",
"updated_by",
],
many=True,
project_member, many=True
)
return Response(
@@ -965,32 +825,7 @@ class WorkspaceMemberUserEndpoint(BaseAPIView):
workspace_member = WorkspaceMember.objects.get(
member=request.user, workspace__slug=slug
)
serializer = WorkSpaceMemberSerializer(
workspace_member,
fields=[
"id",
"created_by",
"created_at",
"updated_at",
"updated_by",
"name",
"logo",
"owner",
"slug",
"organization_size",
{"workspace": ["id", "name", "slug"]},
{
"member": [
"id",
"first_name",
"last_name",
"avatar",
"is_bot",
"display_name",
]
},
],
)
serializer = WorkSpaceMemberSerializer(workspace_member)
return Response(serializer.data, status=status.HTTP_200_OK)
except (Workspace.DoesNotExist, WorkspaceMember.DoesNotExist):
return Response({"error": "Forbidden"}, status=status.HTTP_403_FORBIDDEN)
@@ -1248,6 +1083,7 @@ class WorkspaceUserProfileStatsEndpoint(BaseAPIView):
.filter(**filters)
.values("priority")
.annotate(priority_count=Count("priority"))
.filter(priority_count__gte=1)
.annotate(
priority_order=Case(
*[
@@ -1264,7 +1100,6 @@ 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,
)
@@ -1362,6 +1197,7 @@ 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,
@@ -1374,18 +1210,7 @@ class WorkspaceUserActivityEndpoint(BaseAPIView):
request=request,
queryset=queryset,
on_results=lambda issue_activities: IssueActivitySerializer(
issue_activities,
nested_fields={
"actor_detail": (
"id",
"first_name",
"last_name",
"avatar",
"is_bot",
"display_name",
)
},
many=True,
issue_activities, many=True
).data,
)
except Exception as e:
@@ -1593,7 +1418,7 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
issues = IssueLiteSerializer(issue_queryset, many=True).data
# Grouping the results
## Grouping the results
group_by = request.GET.get("group_by", False)
if group_by:
return Response(
@@ -1640,34 +1465,8 @@ class WorkspaceMembersEndpoint(BaseAPIView):
workspace__slug=slug,
member__is_bot=False,
).select_related("workspace", "member")
serializer = WorkSpaceMemberSerializer(
workspace_members,
fields=[
"id",
"created_by",
"created_at",
"updated_at",
"updated_by",
"name",
"logo",
"owner",
"slug",
"organization_size",
{"workspace": ["id", "name", "slug"]},
{
"member": [
"id",
"first_name",
"last_name",
"avatar",
"is_bot",
"display_name",
]
},
],
many=True,
)
return Response(serializer.data, status=status.HTTP_200_OK)
serialzier = WorkSpaceMemberSerializer(workspace_members, many=True)
return Response(serialzier.data, status=status.HTTP_200_OK)
except Exception as e:
capture_exception(e)
return Response(
+55 -26
View File
@@ -4,6 +4,7 @@ import io
import json
import boto3
import zipfile
from urllib.parse import urlparse, urlunparse
# Django imports
from django.conf import settings
@@ -23,9 +24,11 @@ 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()
@@ -66,28 +69,53 @@ def create_zip_file(files):
def upload_to_s3(zip_file, workspace_id, token_id, slug):
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"),
)
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
presigned_url = s3.generate_presigned_url(
"get_object",
Params={"Bucket": settings.AWS_S3_BUCKET_NAME, "Key": file_name},
ExpiresIn=expires_in,
)
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,
)
exporter_instance = ExporterHistory.objects.get(token=token_id)
@@ -98,7 +126,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):
@@ -145,7 +173,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"]),
@@ -242,7 +270,9 @@ 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
workspace__id=workspace_id,
project_id__in=project_ids,
project__project_projectmember__member=exporter_instance.initiated_by_id,
)
.select_related("project", "workspace", "state", "parent", "created_by")
.prefetch_related(
@@ -275,7 +305,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
@@ -338,7 +368,6 @@ 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,18 +21,29 @@ 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")
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"),
)
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"),
)
for file_name, exporter_id in expired_exporter_history:
# Delete object from S3
if file_name:
s3.delete_object(Bucket=settings.AWS_S3_BUCKET_NAME, Key=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)
ExporterHistory.objects.filter(id=exporter_id).update(url=None)
+1 -41
View File
@@ -161,47 +161,7 @@ def service_importer(service, importer_id):
if settings.PROXY_BASE_URL:
headers = {"Content-Type": "application/json"}
import_data_json = json.dumps(
ImporterSerializer(
importer,
fields=[
"id",
"created_by",
"created_at",
"updated_at",
"updated_by",
"workspace",
"project",
"service",
"status",
"initiated_by",
"metadata",
"config",
"data",
"token",
"imported_data",
{
"initiated_by_detail": [
"id",
"first_name",
"last_name",
"avatar",
"is_bot",
"display_name",
],
},
{
"project_detail": [
"id",
"name",
"cover_image",
"icon_prop",
"emoji",
"description",
]
},
{"workspace_detail": ["id", "name", "slug"]},
],
).data,
ImporterSerializer(importer).data,
cls=DjangoJSONEncoder,
)
res = requests.post(
+167 -2
View File
@@ -24,6 +24,9 @@ from plane.db.models import (
IssueSubscriber,
Notification,
IssueAssignee,
IssueReaction,
CommentReaction,
IssueComment,
)
from plane.api.serializers import IssueActivitySerializer
@@ -629,7 +632,7 @@ def update_issue_activity(
"parent": track_parent,
"priority": track_priority,
"state": track_state,
"description": track_description,
"description_html": track_description,
"target_date": track_target_date,
"start_date": track_start_date,
"labels_list": track_labels,
@@ -1022,6 +1025,150 @@ def delete_attachment_activity(
)
)
def create_issue_reaction_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities
):
requested_data = json.loads(requested_data) if requested_data is not None else None
if requested_data and requested_data.get("reaction") is not None:
issue_reaction = IssueReaction.objects.filter(reaction=requested_data.get("reaction"), project=project, actor=actor).values_list('id', flat=True).first()
if issue_reaction is not None:
issue_activities.append(
IssueActivity(
issue_id=issue_id,
actor=actor,
verb="created",
old_value=None,
new_value=requested_data.get("reaction"),
field="reaction",
project=project,
workspace=project.workspace,
comment="added the reaction",
old_identifier=None,
new_identifier=issue_reaction,
)
)
def delete_issue_reaction_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities
):
current_instance = (
json.loads(current_instance) if current_instance is not None else None
)
if current_instance and current_instance.get("reaction") is not None:
issue_activities.append(
IssueActivity(
issue_id=issue_id,
actor=actor,
verb="deleted",
old_value=current_instance.get("reaction"),
new_value=None,
field="reaction",
project=project,
workspace=project.workspace,
comment="removed the reaction",
old_identifier=current_instance.get("identifier"),
new_identifier=None,
)
)
def create_comment_reaction_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities
):
requested_data = json.loads(requested_data) if requested_data is not None else None
if requested_data and requested_data.get("reaction") is not None:
comment_reaction_id, comment_id = CommentReaction.objects.filter(reaction=requested_data.get("reaction"), project=project, actor=actor).values_list('id', 'comment__id').first()
comment = IssueComment.objects.get(pk=comment_id,project=project)
if comment is not None and comment_reaction_id is not None and comment_id is not None:
issue_activities.append(
IssueActivity(
issue_id=comment.issue_id,
actor=actor,
verb="created",
old_value=None,
new_value=requested_data.get("reaction"),
field="reaction",
project=project,
workspace=project.workspace,
comment="added the reaction",
old_identifier=None,
new_identifier=comment_reaction_id,
)
)
def delete_comment_reaction_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities
):
current_instance = (
json.loads(current_instance) if current_instance is not None else None
)
if current_instance and current_instance.get("reaction") is not None:
issue_id = IssueComment.objects.filter(pk=current_instance.get("comment_id"), project=project).values_list('issue_id', flat=True).first()
if issue_id is not None:
issue_activities.append(
IssueActivity(
issue_id=issue_id,
actor=actor,
verb="deleted",
old_value=current_instance.get("reaction"),
new_value=None,
field="reaction",
project=project,
workspace=project.workspace,
comment="removed the reaction",
old_identifier=current_instance.get("identifier"),
new_identifier=None,
)
)
def create_issue_vote_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities
):
requested_data = json.loads(requested_data) if requested_data is not None else None
if requested_data and requested_data.get("vote") is not None:
issue_activities.append(
IssueActivity(
issue_id=issue_id,
actor=actor,
verb="created",
old_value=None,
new_value=requested_data.get("vote"),
field="vote",
project=project,
workspace=project.workspace,
comment="added the vote",
old_identifier=None,
new_identifier=None,
)
)
def delete_issue_vote_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities
):
current_instance = (
json.loads(current_instance) if current_instance is not None else None
)
if current_instance and current_instance.get("vote") is not None:
issue_activities.append(
IssueActivity(
issue_id=issue_id,
actor=actor,
verb="deleted",
old_value=current_instance.get("vote"),
new_value=None,
field="vote",
project=project,
workspace=project.workspace,
comment="removed the vote",
old_identifier=current_instance.get("identifier"),
new_identifier=None,
)
)
# Receive message from room group
@shared_task
@@ -1045,6 +1192,12 @@ 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()
@@ -1080,6 +1233,12 @@ def issue_activity(
"link.activity.deleted": delete_link_activity,
"attachment.activity.created": create_attachment_activity,
"attachment.activity.deleted": delete_attachment_activity,
"issue_reaction.activity.created": create_issue_reaction_activity,
"issue_reaction.activity.deleted": delete_issue_reaction_activity,
"comment_reaction.activity.created": create_comment_reaction_activity,
"comment_reaction.activity.deleted": delete_comment_reaction_activity,
"issue_vote.activity.created": create_issue_vote_activity,
"issue_vote.activity.deleted": delete_issue_vote_activity,
}
func = ACTIVITY_MAPPER.get(type)
@@ -1103,7 +1262,7 @@ def issue_activity(
for issue_activity in issue_activities_created:
headers = {"Content-Type": "application/json"}
issue_activity_json = json.dumps(
IssueActivitySerializer(issue_activity,fields=[{"actor_detail":["id", "first_name", "last_name", "avatar", "is_bot", "display_name"]}]).data,
IssueActivitySerializer(issue_activity).data,
cls=DjangoJSONEncoder,
)
_ = requests.post(
@@ -1119,6 +1278,12 @@ def issue_activity(
"cycle.activity.deleted",
"module.activity.created",
"module.activity.deleted",
"issue_reaction.activity.created",
"issue_reaction.activity.deleted",
"comment_reaction.activity.created",
"comment_reaction.activity.deleted",
"issue_vote.activity.created",
"issue_vote.activity.deleted",
]:
# Create Notifications
bulk_notifications = []
@@ -64,7 +64,7 @@ def archive_old_issues():
issues_to_update.append(issue)
# Bulk Update the issues and log the activity
Issue.objects.bulk_update(
updated_issues = Issue.objects.bulk_update(
issues_to_update, ["archived_at"], batch_size=100
)
[
@@ -77,7 +77,7 @@ def archive_old_issues():
current_instance=None,
subscriber=False,
)
for issue in issues_to_update
for issue in updated_issues
]
return
except Exception as e:
@@ -136,7 +136,7 @@ def close_old_issues():
issues_to_update.append(issue)
# Bulk Update the issues and log the activity
Issue.objects.bulk_update(issues_to_update, ["state"], batch_size=100)
updated_issues = Issue.objects.bulk_update(issues_to_update, ["state"], batch_size=100)
[
issue_activity.delay(
type="issue.activity.updated",
@@ -147,7 +147,7 @@ def close_old_issues():
current_instance=None,
subscriber=False,
)
for issue in issues_to_update
for issue in updated_issues
]
return
except Exception as e:
File diff suppressed because one or more lines are too long
@@ -0,0 +1,26 @@
# Generated by Django 4.2.3 on 2023-08-29 07:58
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('db', '0041_cycle_sort_order_issuecomment_access_and_more'),
]
operations = [
migrations.AlterUniqueTogether(
name='issuevote',
unique_together=set(),
),
migrations.AlterField(
model_name='issuevote',
name='vote',
field=models.IntegerField(choices=[(-1, 'DOWNVOTE'), (1, 'UPVOTE')], default=1),
),
migrations.AlterUniqueTogether(
name='issuevote',
unique_together={('issue', 'actor', 'vote')},
),
]
+1
View File
@@ -19,6 +19,7 @@ from .project import (
ProjectIdentifier,
ProjectFavorite,
ProjectDeployBoard,
ProjectPublicMember,
)
from .issue import (
+4 -2
View File
@@ -476,10 +476,12 @@ class IssueVote(ProjectBaseModel):
choices=(
(-1, "DOWNVOTE"),
(1, "UPVOTE"),
)
),
default=1,
)
class Meta:
unique_together = ["issue", "actor"]
unique_together = ["issue", "actor", "vote"]
verbose_name = "Issue Vote"
verbose_name_plural = "Issue Votes"
db_table = "issue_votes"
+15
View File
@@ -254,3 +254,18 @@ 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",)
+3 -4
View File
@@ -2,6 +2,7 @@
import uuid
import string
import random
import pytz
# Django imports
from django.db import models
@@ -9,9 +10,6 @@ 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
@@ -66,7 +64,8 @@ class User(AbstractBaseUser, PermissionsMixin):
billing_address = models.JSONField(null=True)
has_billing_address = models.BooleanField(default=False)
user_timezone = models.CharField(max_length=255, default="Asia/Kolkata")
USER_TIMEZONE_CHOICES = tuple(zip(pytz.all_timezones, pytz.all_timezones))
user_timezone = models.CharField(max_length=255, default="UTC", choices=USER_TIMEZONE_CHOICES)
last_active = models.DateTimeField(default=timezone.now, null=True)
last_login_time = models.DateTimeField(null=True)
+2 -2
View File
@@ -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 = "Asia/Kolkata"
TIME_ZONE = "UTC"
USE_I18N = True
+1 -1
View File
@@ -1 +1 @@
python-3.11.4
python-3.11.5
+2 -2
View File
@@ -33,8 +33,8 @@ RUN yarn turbo run build --filter=app
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \
BUILT_NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
RUN /usr/local/bin/replace-env-vars.sh http://NEXT_PUBLIC_WEBAPP_URL_PLACEHOLDER ${NEXT_PUBLIC_API_BASE_URL}
ENV NEXT_PUBLIC_DEPLOY_URL=${NEXT_PUBLIC_DEPLOY_URL}
RUN /usr/local/bin/replace-env-vars.sh http://NEXT_PUBLIC_WEBAPP_URL_PLACEHOLDER ${NEXT_PUBLIC_API_BASE_URL} app
FROM node:18-alpine AS runner
WORKDIR /app
@@ -665,7 +665,7 @@ export const CommandK: React.FC<Props> = ({ deleteIssue, isPaletteOpen, setIsPal
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-custom-text-200">
<DiscordIcon className="h-4 w-4" color="#6b7280" />
<DiscordIcon className="h-4 w-4" color="rgb(var(--color-text-200))" />
Join our Discord
</div>
</Command.Item>
+75 -14
View File
@@ -53,7 +53,11 @@ const UserLink = ({ activity }: { activity: IIssueActivity }) => {
const activityDetails: {
[key: string]: {
message: (activity: IIssueActivity, showIssue: boolean) => React.ReactNode;
message: (
activity: IIssueActivity,
showIssue: boolean,
workspaceSlug: string
) => React.ReactNode;
icon: React.ReactNode;
};
} = {
@@ -173,26 +177,50 @@ const activityDetails: {
icon: <BlockedIcon height="12" width="12" color="#6b7280" />,
},
cycles: {
message: (activity) => {
message: (activity, showIssue, workspaceSlug) => {
if (activity.verb === "created")
return (
<>
added this issue to the cycle{" "}
<span className="font-medium text-custom-text-100">{activity.new_value}</span>.
<a
href={`/${workspaceSlug}/projects/${activity.project}/cycles/${activity.new_identifier}`}
target="_blank"
rel="noopener noreferrer"
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
>
{activity.new_value}
<Icon iconName="launch" className="!text-xs" />
</a>
</>
);
else if (activity.verb === "updated")
return (
<>
set the cycle to{" "}
<span className="font-medium text-custom-text-100">{activity.new_value}</span>.
<a
href={`/${workspaceSlug}/projects/${activity.project}/cycles/${activity.new_identifier}`}
target="_blank"
rel="noopener noreferrer"
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
>
{activity.new_value}
<Icon iconName="launch" className="!text-xs" />
</a>
</>
);
else
return (
<>
removed the issue from the cycle{" "}
<span className="font-medium text-custom-text-100">{activity.old_value}</span>.
<a
href={`/${workspaceSlug}/projects/${activity.project}/cycles/${activity.old_identifier}`}
target="_blank"
rel="noopener noreferrer"
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
>
{activity.old_value}
<Icon iconName="launch" className="!text-xs" />
</a>
</>
);
},
@@ -351,26 +379,50 @@ const activityDetails: {
icon: <Icon iconName="link" className="!text-sm" aria-hidden="true" />,
},
modules: {
message: (activity) => {
message: (activity, showIssue, workspaceSlug) => {
if (activity.verb === "created")
return (
<>
added this issue to the module{" "}
<span className="font-medium text-custom-text-100">{activity.new_value}</span>.
<a
href={`/${workspaceSlug}/projects/${activity.project}/modules/${activity.new_identifier}`}
target="_blank"
rel="noopener noreferrer"
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
>
{activity.new_value}
<Icon iconName="launch" className="!text-xs" />
</a>
</>
);
else if (activity.verb === "updated")
return (
<>
set the module to{" "}
<span className="font-medium text-custom-text-100">{activity.new_value}</span>.
<a
href={`/${workspaceSlug}/projects/${activity.project}/modules/${activity.new_identifier}`}
target="_blank"
rel="noopener noreferrer"
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
>
{activity.new_value}
<Icon iconName="launch" className="!text-xs" />
</a>
</>
);
else
return (
<>
removed the issue from the module{" "}
<span className="font-medium text-custom-text-100">{activity.old_value}</span>.
<a
href={`/${workspaceSlug}/projects/${activity.project}/modules/${activity.old_identifier}`}
target="_blank"
rel="noopener noreferrer"
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
>
{activity.old_value}
<Icon iconName="launch" className="!text-xs" />
</a>
</>
);
},
@@ -538,8 +590,17 @@ export const ActivityMessage = ({
}: {
activity: IIssueActivity;
showIssue?: boolean;
}) => (
<>
{activityDetails[activity.field as keyof typeof activityDetails]?.message(activity, showIssue)}
</>
);
}) => {
const router = useRouter();
const { workspaceSlug } = router.query;
return (
<>
{activityDetails[activity.field as keyof typeof activityDetails]?.message(
activity,
showIssue,
workspaceSlug?.toString() ?? ""
)}
</>
);
};
@@ -11,15 +11,18 @@ import { Dialog, Transition } from "@headlessui/react";
// hooks
import useIssuesView from "hooks/use-issues-view";
// components
import { DueDateFilterSelect } from "./due-date-filter-select";
import { DateFilterSelect } from "./date-filter-select";
// ui
import { PrimaryButton, SecondaryButton } from "components/ui";
// icons
import { XMarkIcon } from "@heroicons/react/20/solid";
// helpers
import { renderDateFormat, renderShortDateWithYearFormat } from "helpers/date-time.helper";
import { IIssueFilterOptions } from "types";
type Props = {
title: string;
field: keyof IIssueFilterOptions;
isOpen: boolean;
handleClose: () => void;
};
@@ -36,7 +39,7 @@ const defaultValues: TFormValues = {
date2: new Date(new Date().getFullYear(), new Date().getMonth() + 1, new Date().getDate()),
};
export const DueDateFilterModal: React.FC<Props> = ({ isOpen, handleClose }) => {
export const DateFilterModal: React.FC<Props> = ({ title, field, isOpen, handleClose }) => {
const { filters, setFilters } = useIssuesView();
const router = useRouter();
@@ -51,11 +54,11 @@ export const DueDateFilterModal: React.FC<Props> = ({ isOpen, handleClose }) =>
if (filterType === "range") {
setFilters(
{ target_date: [`${renderDateFormat(date1)};after`, `${renderDateFormat(date2)};before`] },
{ [field]: [`${renderDateFormat(date1)};after`, `${renderDateFormat(date2)};before`] },
!Boolean(viewId)
);
} else {
const filteredArray = filters?.target_date?.filter((item) => {
const filteredArray = (filters?.[field] as string[])?.filter((item) => {
if (item?.includes(filterType)) return false;
return true;
@@ -64,13 +67,13 @@ export const DueDateFilterModal: React.FC<Props> = ({ isOpen, handleClose }) =>
const filterOne = filteredArray && filteredArray?.length > 0 ? filteredArray[0] : null;
if (filterOne)
setFilters(
{ target_date: [filterOne, `${renderDateFormat(date1)};${filterType}`] },
{ [field]: [filterOne, `${renderDateFormat(date1)};${filterType}`] },
!Boolean(viewId)
);
else
setFilters(
{
target_date: [`${renderDateFormat(date1)};${filterType}`],
[field]: [`${renderDateFormat(date1)};${filterType}`],
},
!Boolean(viewId)
);
@@ -116,7 +119,7 @@ export const DueDateFilterModal: React.FC<Props> = ({ isOpen, handleClose }) =>
control={control}
name="filterType"
render={({ field: { value, onChange } }) => (
<DueDateFilterSelect value={value} onChange={onChange} />
<DateFilterSelect title={title} value={value} onChange={onChange} />
)}
/>
<XMarkIcon
@@ -7,6 +7,7 @@ import { CalendarBeforeIcon, CalendarAfterIcon, CalendarMonthIcon } from "compon
// fetch-keys
type Props = {
title: string;
value: string;
onChange: (value: string) => void;
};
@@ -19,29 +20,31 @@ type DueDate = {
const dueDateRange: DueDate[] = [
{
name: "Due date before",
name: "before",
value: "before",
icon: <CalendarBeforeIcon className="h-4 w-4 " />,
},
{
name: "Due date after",
name: "after",
value: "after",
icon: <CalendarAfterIcon className="h-4 w-4 " />,
},
{
name: "Due date range",
name: "range",
value: "range",
icon: <CalendarMonthIcon className="h-4 w-4 " />,
},
];
export const DueDateFilterSelect: React.FC<Props> = ({ value, onChange }) => (
export const DateFilterSelect: React.FC<Props> = ({ title, value, onChange }) => (
<CustomSelect
value={value}
label={
<div className="flex items-center gap-2 text-xs">
{dueDateRange.find((item) => item.value === value)?.icon}
<span>{dueDateRange.find((item) => item.value === value)?.name}</span>
<span>
{title} {dueDateRange.find((item) => item.value === value)?.name}
</span>
</div>
}
onChange={onChange}
@@ -50,7 +53,7 @@ export const DueDateFilterSelect: React.FC<Props> = ({ value, onChange }) => (
<CustomSelect.Option key={index} value={option.value}>
<>
<span>{option.icon}</span>
{option.name}
{title} {option.name}
</>
</CustomSelect.Option>
))}
@@ -240,6 +240,34 @@ export const FiltersList: React.FC<Props> = ({
</div>
);
})
: key === "start_date"
? filters.start_date?.map((date: string) => {
if (filters.start_date && filters.start_date.length <= 0) return null;
const splitDate = date.split(";");
return (
<div
key={date}
className="inline-flex items-center gap-x-1 rounded-full border border-custom-border-200 bg-custom-background-100 px-1 py-0.5"
>
<div className="h-1.5 w-1.5 rounded-full" />
<span className="capitalize">
{splitDate[1]} {renderShortDateWithYearFormat(splitDate[0])}
</span>
<span
className="cursor-pointer"
onClick={() =>
setFilters({
start_date: filters.start_date?.filter((d: any) => d !== date),
})
}
>
<XMarkIcon className="h-3 w-3" />
</span>
</div>
);
})
: key === "target_date"
? filters.target_date?.map((date: string) => {
if (filters.target_date && filters.target_date.length <= 0) return null;
+2 -2
View File
@@ -1,4 +1,4 @@
export * from "./due-date-filter-modal";
export * from "./due-date-filter-select";
export * from "./date-filter-modal";
export * from "./date-filter-select";
export * from "./filters-list";
export * from "./issues-view-filter";
@@ -113,46 +113,41 @@ export const IssuesFilterView: React.FC = () => {
))}
</div>
)}
{issueView !== "gantt_chart" && (
<SelectFilters
filters={filters}
onSelect={(option) => {
const key = option.key as keyof typeof filters;
<SelectFilters
filters={filters}
onSelect={(option) => {
const key = option.key as keyof typeof filters;
if (key === "target_date") {
const valueExists = checkIfArraysHaveSameElements(
filters.target_date ?? [],
option.value
if (key === "start_date" || key === "target_date") {
const valueExists = checkIfArraysHaveSameElements(filters[key] ?? [], option.value);
setFilters({
[key]: valueExists ? null : option.value,
});
} else {
const valueExists = filters[key]?.includes(option.value);
if (valueExists)
setFilters(
{
[option.key]: ((filters[key] ?? []) as any[])?.filter(
(val) => val !== option.value
),
},
!Boolean(viewId)
);
setFilters({
target_date: valueExists ? null : option.value,
});
} else {
const valueExists = filters[key]?.includes(option.value);
if (valueExists)
setFilters(
{
[option.key]: ((filters[key] ?? []) as any[])?.filter(
(val) => val !== option.value
),
},
!Boolean(viewId)
);
else
setFilters(
{
[option.key]: [...((filters[key] ?? []) as any[]), option.value],
},
!Boolean(viewId)
);
}
}}
direction="left"
height="rg"
/>
)}
else
setFilters(
{
[option.key]: [...((filters[key] ?? []) as any[]), option.value],
},
!Boolean(viewId)
);
}
}}
direction="left"
height="rg"
/>
<Popover className="relative">
{({ open }) => (
<>
@@ -1,7 +1,5 @@
import React, { useEffect, useState, forwardRef, useRef } from "react";
import { useRouter } from "next/router";
// react-hook-form
import { useForm } from "react-hook-form";
// services
@@ -12,9 +10,10 @@ import useToast from "hooks/use-toast";
import useUserAuth from "hooks/use-user-auth";
// ui
import { Input, PrimaryButton, SecondaryButton } from "components/ui";
import { TipTapEditor } from "components/tiptap";
// types
import { IIssue, IPageBlock } from "types";
import Tiptap, { ITiptapRichTextEditor } from "components/tiptap";
type Props = {
isOpen: boolean;
handleClose: () => void;
@@ -32,12 +31,6 @@ type FormData = {
task: string;
};
const TiptapEditor = React.forwardRef<ITiptapRichTextEditor, ITiptapRichTextEditor>(
(props, ref) => <Tiptap {...props} forwardedRef={ref} />
);
TiptapEditor.displayName = "TiptapEditor";
export const GptAssistantModal: React.FC<Props> = ({
isOpen,
handleClose,
@@ -147,7 +140,8 @@ export const GptAssistantModal: React.FC<Props> = ({
{((content && content !== "") || (htmlContent && htmlContent !== "<p></p>")) && (
<div className="text-sm">
Content:
<TiptapEditor
<TipTapEditor
workspaceSlug={workspaceSlug as string}
value={htmlContent ?? `<p>${content}</p>`}
customClassName="-m-3"
noBorder
@@ -160,7 +154,8 @@ export const GptAssistantModal: React.FC<Props> = ({
{response !== "" && (
<div className="page-block-section text-sm">
Response:
<Tiptap
<TipTapEditor
workspaceSlug={workspaceSlug as string}
value={`<p>${response}</p>`}
customClassName="-mx-3 -my-3"
noBorder
+4 -1
View File
@@ -114,7 +114,10 @@ export const AllViews: React.FC<Props> = ({
)}
</StrictModeDroppable>
{groupedIssues ? (
!isEmpty || issueView === "kanban" || issueView === "calendar" ? (
!isEmpty ||
issueView === "kanban" ||
issueView === "calendar" ||
issueView === "gantt_chart" ? (
<>
{issueView === "list" ? (
<AllLists
@@ -12,7 +12,7 @@ import useProjects from "hooks/use-projects";
// component
import { Avatar, Icon } from "components/ui";
// icons
import { ArrowsPointingInIcon, ArrowsPointingOutIcon, PlusIcon } from "@heroicons/react/24/outline";
import { PlusIcon } from "@heroicons/react/24/outline";
import { getPriorityIcon, getStateGroupIcon } from "components/icons";
// helpers
import { addSpaceIfCamelCase } from "helpers/string.helper";
@@ -56,10 +56,10 @@ export const BoardHeader: React.FC<Props> = ({
);
const { data: members } = useSWR(
workspaceSlug && projectId && selectedGroup === "created_by"
workspaceSlug && projectId && (selectedGroup === "created_by" || selectedGroup === "assignees")
? PROJECT_MEMBERS(projectId.toString())
: null,
workspaceSlug && projectId && selectedGroup === "created_by"
workspaceSlug && projectId && (selectedGroup === "created_by" || selectedGroup === "assignees")
? () => projectService.projectMembers(workspaceSlug.toString(), projectId.toString())
: null
);
@@ -79,9 +79,11 @@ export const BoardHeader: React.FC<Props> = ({
case "project":
title = projects?.find((p) => p.id === groupTitle)?.name ?? "None";
break;
case "assignees":
case "created_by":
const member = members?.find((member) => member.member.id === groupTitle)?.member;
title = member?.display_name ?? "";
title = member ? member.display_name : "None";
break;
}
@@ -122,9 +124,10 @@ export const BoardHeader: React.FC<Props> = ({
/>
);
break;
case "assignees":
case "created_by":
const member = members?.find((member) => member.member.id === groupTitle)?.member;
icon = <Avatar user={member} height="24px" width="24px" fontSize="12px" />;
icon = member ? <Avatar user={member} height="24px" width="24px" fontSize="12px" /> : <></>;
break;
}
@@ -86,7 +86,8 @@ export const SingleBoardIssue: React.FC<Props> = ({
}) => {
// context menu
const [contextMenu, setContextMenu] = useState(false);
const [contextMenuPosition, setContextMenuPosition] = useState({ x: 0, y: 0 });
const [contextMenuPosition, setContextMenuPosition] = useState<React.MouseEvent | null>(null);
const [isMenuActive, setIsMenuActive] = useState(false);
const [isDropdownActive, setIsDropdownActive] = useState(false);
@@ -201,7 +202,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
return (
<>
<ContextMenu
position={contextMenuPosition}
clickEvent={contextMenuPosition}
title="Quick actions"
isOpen={contextMenu}
setIsOpen={setContextMenu}
@@ -243,7 +244,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
onContextMenu={(e) => {
e.preventDefault();
setContextMenu(true);
setContextMenuPosition({ x: e.pageX, y: e.pageY });
setContextMenuPosition(e);
}}
>
<div className="flex flex-col justify-between gap-1.5 group/card relative select-none px-3.5 py-3 h-[118px]">
@@ -478,6 +478,7 @@ export const IssuesView: React.FC<Props> = ({
labels: null,
priority: null,
state: null,
start_date: null,
target_date: null,
type: null,
})
@@ -513,7 +514,8 @@ export const IssuesView: React.FC<Props> = ({
dragDisabled={
selectedGroup === "created_by" ||
selectedGroup === "labels" ||
selectedGroup === "state_detail.group"
selectedGroup === "state_detail.group" ||
selectedGroup === "assignees"
}
emptyState={{
title: cycleId
@@ -546,7 +548,7 @@ export const IssuesView: React.FC<Props> = ({
}}
handleOnDragEnd={handleOnDragEnd}
handleIssueAction={handleIssueAction}
openIssuesListModal={openIssuesListModal ? openIssuesListModal : null}
openIssuesListModal={openIssuesListModal ?? null}
removeIssue={cycleId ? removeIssueFromCycle : moduleId ? removeIssueFromModule : null}
trashBox={trashBox}
setTrashBox={setTrashBox}
@@ -33,7 +33,7 @@ import {
} from "@heroicons/react/24/outline";
import { LayerDiagonalIcon } from "components/icons";
// helpers
import { copyTextToClipboard, truncateText } from "helpers/string.helper";
import { copyTextToClipboard } from "helpers/string.helper";
import { handleIssuesMutation } from "constants/issue";
// types
import { ICurrentUserResponse, IIssue, IIssueViewProps, ISubIssueResponse, UserAuth } from "types";
@@ -71,7 +71,7 @@ export const SingleListIssue: React.FC<Props> = ({
}) => {
// context menu
const [contextMenu, setContextMenu] = useState(false);
const [contextMenuPosition, setContextMenuPosition] = useState({ x: 0, y: 0 });
const [contextMenuPosition, setContextMenuPosition] = useState<React.MouseEvent | null>(null);
const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
@@ -167,7 +167,7 @@ export const SingleListIssue: React.FC<Props> = ({
return (
<>
<ContextMenu
position={contextMenuPosition}
clickEvent={contextMenuPosition}
title="Quick actions"
isOpen={contextMenu}
setIsOpen={setContextMenu}
@@ -199,7 +199,7 @@ export const SingleListIssue: React.FC<Props> = ({
onContextMenu={(e) => {
e.preventDefault();
setContextMenu(true);
setContextMenuPosition({ x: e.pageX, y: e.pageY });
setContextMenuPosition(e);
}}
>
<div className="flex-grow cursor-pointer min-w-[200px] whitespace-nowrap overflow-hidden overflow-ellipsis">
@@ -94,9 +94,10 @@ export const SingleList: React.FC<Props> = ({
case "project":
title = projects?.find((p) => p.id === groupTitle)?.name ?? "None";
break;
case "assignees":
case "created_by":
const member = members?.find((member) => member.member.id === groupTitle)?.member;
title = member?.display_name ?? "";
title = member ? member.display_name : "None";
break;
}
@@ -137,9 +138,10 @@ export const SingleList: React.FC<Props> = ({
/>
);
break;
case "assignees":
case "created_by":
const member = members?.find((member) => member.member.id === groupTitle)?.member;
icon = <Avatar user={member} height="24px" width="24px" fontSize="12px" />;
icon = member ? <Avatar user={member} height="24px" width="24px" fontSize="12px" /> : <></>;
break;
}
@@ -1,6 +1,5 @@
import React, { useCallback, useState } from "react";
import Link from "next/link";
import { useRouter } from "next/router";
import { mutate } from "swr";
@@ -75,6 +74,7 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
nestingLevel,
}) => {
const [isOpen, setIsOpen] = useState(false);
const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
@@ -95,7 +95,7 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
? VIEW_ISSUES(viewId.toString(), params)
: PROJECT_ISSUES_LIST_WITH_PARAMS(projectId.toString(), params);
if (issue.parent) {
if (issue.parent)
mutate<ISubIssueResponse>(
SUB_ISSUES(issue.parent.toString()),
(prevData) => {
@@ -116,7 +116,7 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
},
false
);
} else {
else
mutate<IIssue[]>(
fetchKey,
(prevData) =>
@@ -131,7 +131,6 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
}),
false
);
}
issuesService
.patchIssue(
@@ -158,6 +157,15 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
[workspaceSlug, projectId, cycleId, moduleId, viewId, params, user]
);
const openPeekOverview = () => {
const { query } = router;
router.push({
pathname: router.pathname,
query: { ...query, peekIssue: issue.id },
});
};
const handleCopyText = () => {
const originURL =
typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
@@ -179,190 +187,194 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
return (
<div
className="relative group grid auto-rows-[minmax(44px,1fr)] hover:rounded-sm hover:bg-custom-background-80 border-b border-custom-border-200 w-full min-w-max"
style={{ gridTemplateColumns }}
>
<div className="flex gap-1.5 items-center px-4 sticky z-[1] left-0 text-custom-text-200 bg-custom-background-100 group-hover:text-custom-text-100 group-hover:bg-custom-background-80 border-custom-border-200 w-full">
<div className="flex gap-1.5 items-center" style={issue.parent ? { paddingLeft } : {}}>
<div className="relative flex items-center cursor-pointer text-xs text-center hover:text-custom-text-100 w-14">
{properties.key && (
<span className="flex items-center justify-center opacity-100 group-hover:opacity-0">
{issue.project_detail?.identifier}-{issue.sequence_id}
</span>
)}
{!isNotAllowed && !disableUserActions && (
<div className="absolute top-0 left-2.5 opacity-0 group-hover:opacity-100">
<Popover2
isOpen={isOpen}
canEscapeKeyClose
onInteraction={(nextOpenState) => setIsOpen(nextOpenState)}
content={
<div
className={`flex flex-col gap-1.5 overflow-y-scroll whitespace-nowrap rounded-md border p-1 text-xs shadow-lg focus:outline-none max-h-44 min-w-full border-custom-border-200 bg-custom-background-90`}
>
<button
type="button"
className="hover:text-custom-text-200 w-full select-none gap-2 truncate rounded px-1 py-1.5 text-left text-custom-text-200 hover:bg-custom-background-80"
onClick={() => {
handleEditIssue(issue);
setIsOpen(false);
}}
<>
<div
className="relative group grid auto-rows-[minmax(44px,1fr)] hover:rounded-sm hover:bg-custom-background-80 border-b border-custom-border-200 w-full min-w-max"
style={{ gridTemplateColumns }}
>
<div className="flex gap-1.5 items-center px-4 sticky z-[1] left-0 text-custom-text-200 bg-custom-background-100 group-hover:text-custom-text-100 group-hover:bg-custom-background-80 border-custom-border-200 w-full">
<div className="flex gap-1.5 items-center" style={issue.parent ? { paddingLeft } : {}}>
<div className="relative flex items-center cursor-pointer text-xs text-center hover:text-custom-text-100 w-14">
{properties.key && (
<span className="flex items-center justify-center opacity-100 group-hover:opacity-0">
{issue.project_detail?.identifier}-{issue.sequence_id}
</span>
)}
{!isNotAllowed && !disableUserActions && (
<div className="absolute top-0 left-2.5 opacity-0 group-hover:opacity-100">
<Popover2
isOpen={isOpen}
canEscapeKeyClose
onInteraction={(nextOpenState) => setIsOpen(nextOpenState)}
content={
<div
className={`flex flex-col gap-1.5 overflow-y-scroll whitespace-nowrap rounded-md border p-1 text-xs shadow-lg focus:outline-none max-h-44 min-w-full border-custom-border-200 bg-custom-background-90`}
>
<div className="flex items-center justify-start gap-2">
<PencilIcon className="h-4 w-4" />
<span>Edit issue</span>
</div>
</button>
<button
type="button"
className="hover:text-custom-text-200 w-full select-none gap-2 truncate rounded px-1 py-1.5 text-left text-custom-text-200 hover:bg-custom-background-80"
onClick={() => {
handleEditIssue(issue);
setIsOpen(false);
}}
>
<div className="flex items-center justify-start gap-2">
<PencilIcon className="h-4 w-4" />
<span>Edit issue</span>
</div>
</button>
<button
type="button"
className="hover:text-custom-text-200 w-full select-none gap-2 truncate rounded px-1 py-1.5 text-left text-custom-text-200 hover:bg-custom-background-80"
onClick={() => {
handleDeleteIssue(issue);
setIsOpen(false);
}}
>
<div className="flex items-center justify-start gap-2">
<TrashIcon className="h-4 w-4" />
<span>Delete issue</span>
</div>
</button>
<button
type="button"
className="hover:text-custom-text-200 w-full select-none gap-2 truncate rounded px-1 py-1.5 text-left text-custom-text-200 hover:bg-custom-background-80"
onClick={() => {
handleDeleteIssue(issue);
setIsOpen(false);
}}
>
<div className="flex items-center justify-start gap-2">
<TrashIcon className="h-4 w-4" />
<span>Delete issue</span>
</div>
</button>
<button
type="button"
className="hover:text-custom-text-200 w-full select-none gap-2 truncate rounded px-1 py-1.5 text-left text-custom-text-200 hover:bg-custom-background-80"
onClick={() => {
handleCopyText();
setIsOpen(false);
}}
>
<div className="flex items-center justify-start gap-2">
<LinkIcon className="h-4 w-4" />
<span>Copy issue link</span>
</div>
</button>
</div>
}
placement="bottom-start"
<button
type="button"
className="hover:text-custom-text-200 w-full select-none gap-2 truncate rounded px-1 py-1.5 text-left text-custom-text-200 hover:bg-custom-background-80"
onClick={() => {
handleCopyText();
setIsOpen(false);
}}
>
<div className="flex items-center justify-start gap-2">
<LinkIcon className="h-4 w-4" />
<span>Copy issue link</span>
</div>
</button>
</div>
}
placement="bottom-start"
>
<EllipsisHorizontalIcon className="h-5 w-5 text-custom-text-200" />
</Popover2>
</div>
)}
</div>
{issue.sub_issues_count > 0 && (
<div className="h-6 w-6 flex justify-center items-center">
<button
className="h-5 w-5 hover:bg-custom-background-90 hover:text-custom-text-100 rounded-sm cursor-pointer"
onClick={() => handleToggleExpand(issue.id)}
>
<EllipsisHorizontalIcon className="h-5 w-5 text-custom-text-200" />
</Popover2>
<Icon iconName="chevron_right" className={`${expanded ? "rotate-90" : ""}`} />
</button>
</div>
)}
</div>
{issue.sub_issues_count > 0 && (
<div className="h-6 w-6 flex justify-center items-center">
<button
className="h-5 w-5 hover:bg-custom-background-90 hover:text-custom-text-100 rounded-sm cursor-pointer"
onClick={() => handleToggleExpand(issue.id)}
>
<Icon iconName="chevron_right" className={`${expanded ? "rotate-90" : ""}`} />
</button>
</div>
)}
</div>
<Link href={`/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`}>
<a className="truncate text-custom-text-100 cursor-pointer w-full text-[0.825rem]">
<button
type="button"
className="truncate text-custom-text-100 text-left cursor-pointer w-full text-[0.825rem]"
onClick={openPeekOverview}
>
{issue.name}
</a>
</Link>
</button>
</div>
{properties.state && (
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
<ViewStateSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
position="left"
className="max-w-full"
tooltipPosition={tooltipPosition}
customButton
user={user}
isNotAllowed={isNotAllowed}
/>
</div>
)}
{properties.priority && (
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
<ViewPrioritySelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
position="left"
tooltipPosition={tooltipPosition}
noBorder
user={user}
isNotAllowed={isNotAllowed}
/>
</div>
)}
{properties.assignee && (
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
<ViewAssigneeSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
position="left"
tooltipPosition={tooltipPosition}
customButton
user={user}
isNotAllowed={isNotAllowed}
/>
</div>
)}
{properties.labels && (
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
<ViewIssueLabel issue={issue} maxRender={1} />
</div>
)}
{properties.start_date && (
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
<ViewStartDateSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
tooltipPosition={tooltipPosition}
noBorder
user={user}
isNotAllowed={isNotAllowed}
/>
</div>
)}
{properties.due_date && (
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
<ViewDueDateSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
tooltipPosition={tooltipPosition}
noBorder
user={user}
isNotAllowed={isNotAllowed}
/>
</div>
)}
{properties.estimate && (
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
<ViewEstimateSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
position="left"
tooltipPosition={tooltipPosition}
user={user}
isNotAllowed={isNotAllowed}
/>
</div>
)}
{properties.created_on && (
<div className="flex items-center text-xs cursor-default text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
{renderLongDetailDateFormat(issue.created_at)}
</div>
)}
{properties.updated_on && (
<div className="flex items-center text-xs cursor-default text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
{renderLongDetailDateFormat(issue.updated_at)}
</div>
)}
</div>
{properties.state && (
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
<ViewStateSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
position="left"
className="max-w-full"
tooltipPosition={tooltipPosition}
customButton
user={user}
isNotAllowed={isNotAllowed}
/>
</div>
)}
{properties.priority && (
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
<ViewPrioritySelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
position="left"
tooltipPosition={tooltipPosition}
noBorder
user={user}
isNotAllowed={isNotAllowed}
/>
</div>
)}
{properties.assignee && (
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
<ViewAssigneeSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
position="left"
tooltipPosition={tooltipPosition}
customButton
user={user}
isNotAllowed={isNotAllowed}
/>
</div>
)}
{properties.labels && (
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
<ViewIssueLabel issue={issue} maxRender={1} />
</div>
)}
{properties.start_date && (
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
<ViewStartDateSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
tooltipPosition={tooltipPosition}
noBorder
user={user}
isNotAllowed={isNotAllowed}
/>
</div>
)}
{properties.due_date && (
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
<ViewDueDateSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
tooltipPosition={tooltipPosition}
noBorder
user={user}
isNotAllowed={isNotAllowed}
/>
</div>
)}
{properties.estimate && (
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
<ViewEstimateSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
position="left"
tooltipPosition={tooltipPosition}
user={user}
isNotAllowed={isNotAllowed}
/>
</div>
)}
{properties.created_on && (
<div className="flex items-center text-xs cursor-default text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
{renderLongDetailDateFormat(issue.created_at)}
</div>
)}
{properties.updated_on && (
<div className="flex items-center text-xs cursor-default text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
{renderLongDetailDateFormat(issue.updated_at)}
</div>
)}
</div>
</>
);
};
@@ -5,7 +5,8 @@ import { useRouter } from "next/router";
// components
import { SpreadsheetColumns, SpreadsheetIssues } from "components/core";
import { CustomMenu, Icon, Spinner } from "components/ui";
import { CustomMenu, Spinner } from "components/ui";
import { IssuePeekOverview } from "components/issues";
// hooks
import useIssuesProperties from "hooks/use-issue-properties";
import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view";
@@ -38,7 +39,7 @@ export const SpreadsheetView: React.FC<Props> = ({
const type = cycleId ? "cycle" : moduleId ? "module" : "issue";
const { spreadsheetIssues } = useSpreadsheetIssuesView();
const { spreadsheetIssues, mutateIssues } = useSpreadsheetIssuesView();
const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string);
@@ -59,80 +60,88 @@ export const SpreadsheetView: React.FC<Props> = ({
.join(" ");
return (
<div className="h-full rounded-lg text-custom-text-200 overflow-x-auto whitespace-nowrap bg-custom-background-100">
<div className="sticky z-[2] top-0 border-b border-custom-border-200 bg-custom-background-90 w-full min-w-max">
<SpreadsheetColumns columnData={columnData} gridTemplateColumns={gridTemplateColumns} />
</div>
{spreadsheetIssues ? (
<div className="flex flex-col h-full w-full bg-custom-background-100 rounded-sm ">
{spreadsheetIssues.map((issue: IIssue, index) => (
<SpreadsheetIssues
key={`${issue.id}_${index}`}
index={index}
issue={issue}
expandedIssues={expandedIssues}
setExpandedIssues={setExpandedIssues}
gridTemplateColumns={gridTemplateColumns}
properties={properties}
handleIssueAction={handleIssueAction}
disableUserActions={disableUserActions}
user={user}
userAuth={userAuth}
/>
))}
<div
className="relative group grid auto-rows-[minmax(44px,1fr)] hover:rounded-sm hover:bg-custom-background-80 border-b border-custom-border-200 w-full min-w-max"
style={{ gridTemplateColumns }}
>
{type === "issue" ? (
<button
className="flex gap-1.5 items-center pl-7 py-2.5 text-sm sticky left-0 z-[1] text-custom-text-200 bg-custom-background-100 group-hover:text-custom-text-100 group-hover:bg-custom-background-80 border-custom-border-200 w-full"
onClick={() => {
const e = new KeyboardEvent("keydown", { key: "c" });
document.dispatchEvent(e);
}}
>
<PlusIcon className="h-4 w-4" />
Add Issue
</button>
) : (
!disableUserActions && (
<CustomMenu
className="sticky left-0 z-[1]"
customButton={
<button
className="flex gap-1.5 items-center pl-7 py-2.5 text-sm sticky left-0 z-[1] text-custom-text-200 bg-custom-background-100 group-hover:text-custom-text-100 group-hover:bg-custom-background-80 border-custom-border-200 w-full"
type="button"
>
<PlusIcon className="h-4 w-4" />
Add Issue
</button>
}
position="left"
optionsClassName="left-5 !w-36"
noBorder
>
<CustomMenu.MenuItem
onClick={() => {
const e = new KeyboardEvent("keydown", { key: "c" });
document.dispatchEvent(e);
}}
>
Create new
</CustomMenu.MenuItem>
{openIssuesListModal && (
<CustomMenu.MenuItem onClick={openIssuesListModal}>
Add an existing issue
</CustomMenu.MenuItem>
)}
</CustomMenu>
)
)}
</div>
<>
<IssuePeekOverview
handleMutation={() => mutateIssues()}
projectId={projectId?.toString() ?? ""}
workspaceSlug={workspaceSlug?.toString() ?? ""}
readOnly={disableUserActions}
/>
<div className="h-full rounded-lg text-custom-text-200 overflow-x-auto whitespace-nowrap bg-custom-background-100">
<div className="sticky z-[2] top-0 border-b border-custom-border-200 bg-custom-background-90 w-full min-w-max">
<SpreadsheetColumns columnData={columnData} gridTemplateColumns={gridTemplateColumns} />
</div>
) : (
<Spinner />
)}
</div>
{spreadsheetIssues ? (
<div className="flex flex-col h-full w-full bg-custom-background-100 rounded-sm ">
{spreadsheetIssues.map((issue: IIssue, index) => (
<SpreadsheetIssues
key={`${issue.id}_${index}`}
index={index}
issue={issue}
expandedIssues={expandedIssues}
setExpandedIssues={setExpandedIssues}
gridTemplateColumns={gridTemplateColumns}
properties={properties}
handleIssueAction={handleIssueAction}
disableUserActions={disableUserActions}
user={user}
userAuth={userAuth}
/>
))}
<div
className="relative group grid auto-rows-[minmax(44px,1fr)] hover:rounded-sm hover:bg-custom-background-80 border-b border-custom-border-200 w-full min-w-max"
style={{ gridTemplateColumns }}
>
{type === "issue" ? (
<button
className="flex gap-1.5 items-center pl-7 py-2.5 text-sm sticky left-0 z-[1] text-custom-text-200 bg-custom-background-100 group-hover:text-custom-text-100 group-hover:bg-custom-background-80 border-custom-border-200 w-full"
onClick={() => {
const e = new KeyboardEvent("keydown", { key: "c" });
document.dispatchEvent(e);
}}
>
<PlusIcon className="h-4 w-4" />
Add Issue
</button>
) : (
!disableUserActions && (
<CustomMenu
className="sticky left-0 z-[1]"
customButton={
<button
className="flex gap-1.5 items-center pl-7 py-2.5 text-sm sticky left-0 z-[1] text-custom-text-200 bg-custom-background-100 group-hover:text-custom-text-100 group-hover:bg-custom-background-80 border-custom-border-200 w-full"
type="button"
>
<PlusIcon className="h-4 w-4" />
Add Issue
</button>
}
position="left"
optionsClassName="left-5 !w-36"
noBorder
>
<CustomMenu.MenuItem
onClick={() => {
const e = new KeyboardEvent("keydown", { key: "c" });
document.dispatchEvent(e);
}}
>
Create new
</CustomMenu.MenuItem>
{openIssuesListModal && (
<CustomMenu.MenuItem onClick={openIssuesListModal}>
Add an existing issue
</CustomMenu.MenuItem>
)}
</CustomMenu>
)
)}
</div>
</div>
) : (
<Spinner />
)}
</div>
</>
);
};
+1 -1
View File
@@ -190,7 +190,7 @@ export const CyclesView: React.FC<Props> = ({ cycles, mutateCycles, viewType })
))}
</div>
) : viewType === "board" ? (
<div className="grid grid-cols-1 gap-9 md:grid-cols-2 lg:grid-cols-3">
<div className="grid grid-cols-1 gap-9 lg:grid-cols-2 xl:grid-cols-3">
{cycles.map((cycle) => (
<SingleCycleCard
key={cycle.id}
@@ -0,0 +1,83 @@
import { useRouter } from "next/router";
// ui
import { Tooltip } from "components/ui";
// icons
import { ContrastIcon } from "components/icons";
// helpers
import { getDateRangeStatus, renderShortDate } from "helpers/date-time.helper";
// types
import { ICycle } from "types";
export const CycleGanttBlock = ({ data }: { data: ICycle }) => {
const router = useRouter();
const { workspaceSlug } = router.query;
const cycleStatus = getDateRangeStatus(data?.start_date, data?.end_date);
return (
<div
className="flex items-center relative h-full w-full rounded"
style={{
backgroundColor:
cycleStatus === "current"
? "#09a953"
: cycleStatus === "upcoming"
? "#f7ae59"
: cycleStatus === "completed"
? "#3f76ff"
: cycleStatus === "draft"
? "rgb(var(--color-text-200))"
: "",
}}
onClick={() => router.push(`/${workspaceSlug}/projects/${data?.project}/cycles/${data?.id}`)}
>
<div className="absolute top-0 left-0 h-full w-full bg-custom-background-100/50" />
<Tooltip
tooltipContent={
<div className="space-y-1">
<h5>{data?.name}</h5>
<div>
{renderShortDate(data?.start_date ?? "")} to {renderShortDate(data?.end_date ?? "")}
</div>
</div>
}
position="top-left"
>
<div className="relative text-custom-text-100 text-sm truncate py-1 px-2.5 w-full">
{data?.name}
</div>
</Tooltip>
</div>
);
};
export const CycleGanttSidebarBlock = ({ data }: { data: ICycle }) => {
const router = useRouter();
const { workspaceSlug } = router.query;
const cycleStatus = getDateRangeStatus(data?.start_date, data?.end_date);
return (
<div
className="relative w-full flex items-center gap-2 h-full"
onClick={() => router.push(`/${workspaceSlug}/projects/${data?.project}/cycles/${data?.id}`)}
>
<ContrastIcon
className="h-5 w-5 flex-shrink-0"
color={`${
cycleStatus === "current"
? "#09a953"
: cycleStatus === "upcoming"
? "#f7ae59"
: cycleStatus === "completed"
? "#3f76ff"
: cycleStatus === "draft"
? "rgb(var(--color-text-200))"
: ""
}`}
/>
<h6 className="text-sm font-medium flex-grow truncate">{data?.name}</h6>
</div>
);
};
@@ -6,11 +6,8 @@ import useUser from "hooks/use-user";
import useGanttChartCycleIssues from "hooks/gantt-chart/cycle-issues-view";
import { updateGanttIssue } from "components/gantt-chart/hooks/block-update";
// components
import {
GanttChartRoot,
IssueGanttBlock,
renderIssueBlocksStructure,
} from "components/gantt-chart";
import { GanttChartRoot, renderIssueBlocksStructure } from "components/gantt-chart";
import { IssueGanttBlock, IssueGanttSidebarBlock } from "components/issues";
// types
import { IIssue } from "types";
@@ -28,29 +25,20 @@ export const CycleIssuesGanttChartView = () => {
cycleId as string
);
// rendering issues on gantt sidebar
const GanttSidebarBlockView = ({ data }: any) => (
<div className="relative flex w-full h-full items-center p-1 overflow-hidden gap-1">
<div
className="rounded-sm flex-shrink-0 w-[10px] h-[10px] flex justify-center items-center"
style={{ backgroundColor: data?.state_detail?.color || "rgb(var(--color-primary-100))" }}
/>
<div className="text-custom-text-100 text-sm">{data?.name}</div>
</div>
);
return (
<div className="w-full h-full p-3">
<div className="w-full h-full">
<GanttChartRoot
title="Cycles"
loaderTitle="Cycles"
border={false}
title="Issues"
loaderTitle="Issues"
blocks={ganttIssues ? renderIssueBlocksStructure(ganttIssues as IIssue[]) : null}
blockUpdateHandler={(block, payload) =>
updateGanttIssue(block, payload, mutateGanttIssues, user, workspaceSlug?.toString())
}
sidebarBlockRender={(data: any) => <GanttSidebarBlockView data={data} />}
blockRender={(data: any) => <IssueGanttBlock issue={data as IIssue} />}
SidebarBlockRender={IssueGanttSidebarBlock}
BlockRender={IssueGanttBlock}
enableReorder={orderBy === "sort_order"}
bottomSpacing
/>
</div>
);
@@ -9,7 +9,8 @@ import cyclesService from "services/cycles.service";
// hooks
import useUser from "hooks/use-user";
// components
import { CycleGanttBlock, GanttChartRoot, IBlockUpdateData } from "components/gantt-chart";
import { GanttChartRoot, IBlockUpdateData } from "components/gantt-chart";
import { CycleGanttBlock, CycleGanttSidebarBlock } from "components/cycles";
// types
import { ICycle } from "types";
@@ -24,17 +25,6 @@ export const CyclesListGanttChartView: FC<Props> = ({ cycles, mutateCycles }) =>
const { user } = useUser();
// rendering issues on gantt sidebar
const GanttSidebarBlockView = ({ data }: any) => (
<div className="relative flex w-full h-full items-center p-1 overflow-hidden gap-1">
<div
className="rounded-sm flex-shrink-0 w-[10px] h-[10px] flex justify-center items-center"
style={{ backgroundColor: "rgb(var(--color-primary-100))" }}
/>
<div className="text-custom-text-100 text-sm">{data?.name}</div>
</div>
);
const handleCycleUpdate = (cycle: ICycle, payload: IBlockUpdateData) => {
if (!workspaceSlug || !user) return;
@@ -88,10 +78,11 @@ export const CyclesListGanttChartView: FC<Props> = ({ cycles, mutateCycles }) =>
loaderTitle="Cycles"
blocks={cycles ? blockFormat(cycles) : null}
blockUpdateHandler={(block, payload) => handleCycleUpdate(block, payload)}
sidebarBlockRender={(data: any) => <GanttSidebarBlockView data={data} />}
blockRender={(data: any) => <CycleGanttBlock cycle={data as ICycle} />}
enableLeftDrag={false}
enableRightDrag={false}
SidebarBlockRender={CycleGanttSidebarBlock}
BlockRender={CycleGanttBlock}
enableBlockLeftResize={false}
enableBlockRightResize={false}
enableBlockMove={false}
/>
</div>
);
@@ -0,0 +1,3 @@
export * from "./blocks";
export * from "./cycle-issues-layout";
export * from "./cycles-list-layout";
+1 -2
View File
@@ -1,11 +1,10 @@
export * from "./cycles-list";
export * from "./active-cycle-details";
export * from "./active-cycle-stats";
export * from "./cycles-list-gantt-chart";
export * from "./gantt-chart";
export * from "./cycles-view";
export * from "./delete-cycle-modal";
export * from "./form";
export * from "./gantt-chart";
export * from "./modal";
export * from "./select";
export * from "./sidebar";
@@ -106,6 +106,7 @@ function RadialProgressBar({ progress }: progress) {
</div>
);
}
export const SingleCycleList: React.FC<TSingleStatProps> = ({
cycle,
handleEditCycle,
@@ -1,6 +1,4 @@
import React from "react";
// next imports
import Link from "next/link";
// ui
import { PrimaryButton } from "components/ui"; // icons
// helpers
@@ -65,11 +63,11 @@ export const SingleExport: React.FC<Props> = ({ service, refreshing }) => {
<>
{service.status == "completed" && (
<div>
<Link href={service?.url}>
<a target="_blank" href={service?.url} rel="noopener noreferrer">
<PrimaryButton className="w-full text-center">
{isLoading ? "Downloading..." : "Download"}
</PrimaryButton>
</Link>
</a>
</div>
)}
</>
@@ -1,103 +0,0 @@
import Link from "next/link";
import { useRouter } from "next/router";
// ui
import { Tooltip } from "components/ui";
// helpers
import { renderShortDate } from "helpers/date-time.helper";
// types
import { ICycle, IIssue, IModule } from "types";
// constants
import { MODULE_STATUS } from "constants/module";
export const IssueGanttBlock = ({ issue }: { issue: IIssue }) => {
const router = useRouter();
const { workspaceSlug } = router.query;
return (
<Link href={`/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`}>
<a className="relative flex items-center w-full h-full shadow-sm transition-all duration-300">
<div
className="flex-shrink-0 w-0.5 h-full"
style={{ backgroundColor: issue.state_detail?.color }}
/>
<Tooltip
tooltipContent={
<div className="space-y-1">
<h5>{issue.name}</h5>
<div>
{renderShortDate(issue.start_date ?? "")} to{" "}
{renderShortDate(issue.target_date ?? "")}
</div>
</div>
}
position="top-left"
>
<div className="text-custom-text-100 text-sm truncate py-1 px-2.5 w-full">
{issue.name}
</div>
</Tooltip>
</a>
</Link>
);
};
export const CycleGanttBlock = ({ cycle }: { cycle: ICycle }) => {
const router = useRouter();
const { workspaceSlug } = router.query;
return (
<Link href={`/${workspaceSlug}/projects/${cycle.project}/cycles/${cycle.id}`}>
<a className="relative flex items-center w-full h-full shadow-sm transition-all duration-300">
<div className="flex-shrink-0 w-0.5 h-full bg-custom-primary-100" />
<Tooltip
tooltipContent={
<div className="space-y-1">
<h5>{cycle.name}</h5>
<div>
{renderShortDate(cycle.start_date ?? "")} to {renderShortDate(cycle.end_date ?? "")}
</div>
</div>
}
position="top-left"
>
<div className="text-custom-text-100 text-sm truncate py-1 px-2.5 w-full">
{cycle.name}
</div>
</Tooltip>
</a>
</Link>
);
};
export const ModuleGanttBlock = ({ module }: { module: IModule }) => {
const router = useRouter();
const { workspaceSlug } = router.query;
return (
<Link href={`/${workspaceSlug}/projects/${module.project}/modules/${module.id}`}>
<a className="relative flex items-center w-full h-full shadow-sm transition-all duration-300">
<div
className="flex-shrink-0 w-0.5 h-full"
style={{ backgroundColor: MODULE_STATUS.find((s) => s.value === module.status)?.color }}
/>
<Tooltip
tooltipContent={
<div className="space-y-1">
<h5>{module.name}</h5>
<div>
{renderShortDate(module.start_date ?? "")} to{" "}
{renderShortDate(module.target_date ?? "")}
</div>
</div>
}
position="top-left"
>
<div className="text-custom-text-100 text-sm truncate py-1 px-2.5 w-full">
{module.name}
</div>
</Tooltip>
</a>
</Link>
);
};
@@ -1,8 +1,7 @@
import { FC } from "react";
// react-beautiful-dnd
import { DragDropContext, Draggable, DropResult } from "react-beautiful-dnd";
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
// hooks
import { useChart } from "../hooks";
// helpers
import { ChartDraggable } from "../helpers/draggable";
import { renderDateFormat } from "helpers/date-time.helper";
@@ -12,90 +11,59 @@ import { IBlockUpdateData, IGanttBlock } from "../types";
export const GanttChartBlocks: FC<{
itemsContainerWidth: number;
blocks: IGanttBlock[] | null;
sidebarBlockRender: FC;
blockRender: FC;
BlockRender: React.FC<any>;
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
enableLeftDrag: boolean;
enableRightDrag: boolean;
enableReorder: boolean;
enableBlockLeftResize: boolean;
enableBlockRightResize: boolean;
enableBlockMove: boolean;
}> = ({
itemsContainerWidth,
blocks,
sidebarBlockRender,
blockRender,
BlockRender,
blockUpdateHandler,
enableLeftDrag,
enableRightDrag,
enableReorder,
enableBlockLeftResize,
enableBlockRightResize,
enableBlockMove,
}) => {
const handleChartBlockPosition = (
block: IGanttBlock,
totalBlockShifts: number,
dragDirection: "left" | "right"
) => {
let updatedDate = new Date();
const { activeBlock, dispatch } = useChart();
if (dragDirection === "left") {
const originalDate = new Date(block.start_date);
const currentDay = originalDate.getDate();
updatedDate = new Date(originalDate);
updatedDate.setDate(currentDay - totalBlockShifts);
} else {
const originalDate = new Date(block.target_date);
const currentDay = originalDate.getDate();
updatedDate = new Date(originalDate);
updatedDate.setDate(currentDay + totalBlockShifts);
}
blockUpdateHandler(block.data, {
[dragDirection === "left" ? "start_date" : "target_date"]: renderDateFormat(updatedDate),
// update the active block on hover
const updateActiveBlock = (block: IGanttBlock | null) => {
dispatch({
type: "PARTIAL_UPDATE",
payload: {
activeBlock: block,
},
});
};
const handleOrderChange = (result: DropResult) => {
if (!blocks) return;
const handleChartBlockPosition = (
block: IGanttBlock,
totalBlockShifts: number,
dragDirection: "left" | "right" | "move"
) => {
const originalStartDate = new Date(block.start_date);
const updatedStartDate = new Date(originalStartDate);
const { source, destination, draggableId } = result;
const originalTargetDate = new Date(block.target_date);
const updatedTargetDate = new Date(originalTargetDate);
if (!destination) return;
if (source.index === destination.index && document) {
// const draggedBlock = document.querySelector(`#${draggableId}`) as HTMLElement;
// const blockStyles = window.getComputedStyle(draggedBlock);
// console.log(blockStyles.marginLeft);
return;
// update the start date on left resize
if (dragDirection === "left")
updatedStartDate.setDate(originalStartDate.getDate() - totalBlockShifts);
// update the target date on right resize
else if (dragDirection === "right")
updatedTargetDate.setDate(originalTargetDate.getDate() + totalBlockShifts);
// update both the dates on x-axis move
else if (dragDirection === "move") {
updatedStartDate.setDate(originalStartDate.getDate() + totalBlockShifts);
updatedTargetDate.setDate(originalTargetDate.getDate() + totalBlockShifts);
}
let updatedSortOrder = blocks[source.index].sort_order;
if (destination.index === 0) updatedSortOrder = blocks[0].sort_order - 1000;
else if (destination.index === blocks.length - 1)
updatedSortOrder = blocks[blocks.length - 1].sort_order + 1000;
else {
const destinationSortingOrder = blocks[destination.index].sort_order;
const relativeDestinationSortingOrder =
source.index < destination.index
? blocks[destination.index + 1].sort_order
: blocks[destination.index - 1].sort_order;
updatedSortOrder = (destinationSortingOrder + relativeDestinationSortingOrder) / 2;
}
const removedElement = blocks.splice(source.index, 1)[0];
blocks.splice(destination.index, 0, removedElement);
blockUpdateHandler(removedElement.data, {
sort_order: {
destinationIndex: destination.index,
newSortOrder: updatedSortOrder,
sourceIndex: source.index,
},
// call the block update handler with the updated dates
blockUpdateHandler(block.data, {
start_date: renderDateFormat(updatedStartDate),
target_date: renderDateFormat(updatedTargetDate),
});
};
@@ -104,75 +72,29 @@ export const GanttChartBlocks: FC<{
className="relative z-[5] mt-[72px] h-full overflow-hidden overflow-y-auto"
style={{ width: `${itemsContainerWidth}px` }}
>
<DragDropContext onDragEnd={handleOrderChange}>
<StrictModeDroppable droppableId="gantt">
{(droppableProvided, droppableSnapshot) => (
<div
className="w-full space-y-2"
ref={droppableProvided.innerRef}
{...droppableProvided.droppableProps}
>
<>
{blocks &&
blocks.length > 0 &&
blocks.map(
(block, index: number) =>
block.start_date &&
block.target_date && (
<Draggable
key={`block-${block.id}`}
draggableId={`block-${block.id}`}
index={index}
isDragDisabled={!enableReorder}
>
{(provided) => (
<div
className={
droppableSnapshot.isDraggingOver ? "bg-custom-border-100/10" : ""
}
ref={provided.innerRef}
{...provided.draggableProps}
>
<ChartDraggable
block={block}
handleBlock={(...args) => handleChartBlockPosition(block, ...args)}
enableLeftDrag={enableLeftDrag}
enableRightDrag={enableRightDrag}
provided={provided}
>
<div
className="rounded shadow-sm bg-custom-background-80 overflow-hidden h-9 flex items-center transition-all"
style={{
width: `${block.position?.width}px`,
}}
>
{blockRender({
...block.data,
})}
</div>
</ChartDraggable>
</div>
)}
</Draggable>
)
)}
{droppableProvided.placeholder}
</>
</div>
)}
</StrictModeDroppable>
</DragDropContext>
{/* sidebar */}
{/* <div className="fixed top-0 bottom-0 w-[300px] flex-shrink-0 divide-y divide-custom-border-200 border-r border-custom-border-200 overflow-y-auto">
{blocks &&
blocks.length > 0 &&
blocks.map((block: any, _idx: number) => (
<div className="relative h-[40px] bg-custom-background-100" key={`sidebar-blocks-${_idx}`}>
{sidebarBlockRender(block?.data)}
</div>
))}
</div> */}
{blocks &&
blocks.length > 0 &&
blocks.map(
(block) =>
block.start_date &&
block.target_date && (
<div
key={`block-${block.id}`}
className={`h-11 ${activeBlock?.id === block.id ? "bg-custom-background-80" : ""}`}
onMouseEnter={() => updateActiveBlock(block)}
onMouseLeave={() => updateActiveBlock(null)}
>
<ChartDraggable
block={block}
BlockRender={BlockRender}
handleBlock={(...args) => handleChartBlockPosition(block, ...args)}
enableBlockLeftResize={enableBlockLeftResize}
enableBlockRightResize={enableBlockRightResize}
enableBlockMove={enableBlockMove}
/>
</div>
)
)}
</div>
);
};
@@ -1,2 +1 @@
export * from "./block";
export * from "./blocks-display";
+67 -80
View File
@@ -3,6 +3,7 @@ import { FC, useEffect, useState } from "react";
import { ArrowsPointingInIcon, ArrowsPointingOutIcon } from "@heroicons/react/20/solid";
// components
import { GanttChartBlocks } from "components/gantt-chart";
import { GanttSidebar } from "../sidebar";
// import { HourChartView } from "./hours";
// import { DayChartView } from "./day";
// import { WeekChartView } from "./week";
@@ -25,7 +26,7 @@ import {
getMonthChartItemPositionWidthInMonth,
} from "../views";
// types
import { ChartDataType, IBlockUpdateData, IGanttBlock } from "../types";
import { ChartDataType, IBlockUpdateData, IGanttBlock, TGanttViews } from "../types";
// data
import { currentViewDataWithView } from "../data";
// context
@@ -33,15 +34,17 @@ import { useChart } from "../hooks";
type ChartViewRootProps = {
border: boolean;
title: null | string;
title: string;
loaderTitle: string;
blocks: IGanttBlock[] | null;
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
sidebarBlockRender: FC<any>;
blockRender: FC<any>;
enableLeftDrag: boolean;
enableRightDrag: boolean;
SidebarBlockRender: React.FC<any>;
BlockRender: React.FC<any>;
enableBlockLeftResize: boolean;
enableBlockRightResize: boolean;
enableBlockMove: boolean;
enableReorder: boolean;
bottomSpacing: boolean;
};
export const ChartViewRoot: FC<ChartViewRootProps> = ({
@@ -50,22 +53,24 @@ export const ChartViewRoot: FC<ChartViewRootProps> = ({
blocks = null,
loaderTitle,
blockUpdateHandler,
sidebarBlockRender,
blockRender,
enableLeftDrag,
enableRightDrag,
SidebarBlockRender,
BlockRender,
enableBlockLeftResize,
enableBlockRightResize,
enableBlockMove,
enableReorder,
bottomSpacing,
}) => {
const { currentView, currentViewData, renderView, dispatch, allViews } = useChart();
const [itemsContainerWidth, setItemsContainerWidth] = useState<number>(0);
const [fullScreenMode, setFullScreenMode] = useState<boolean>(false);
const [blocksSidebarView, setBlocksSidebarView] = useState<boolean>(false);
// blocks state management starts
const [chartBlocks, setChartBlocks] = useState<IGanttBlock[] | null>(null);
const renderBlockStructure = (view: any, blocks: IGanttBlock[]) =>
const { currentView, currentViewData, renderView, dispatch, allViews, updateScrollLeft } =
useChart();
const renderBlockStructure = (view: any, blocks: IGanttBlock[] | null) =>
blocks && blocks.length > 0
? blocks.map((block: any) => ({
...block,
@@ -74,16 +79,16 @@ export const ChartViewRoot: FC<ChartViewRootProps> = ({
: [];
useEffect(() => {
if (currentViewData && blocks && blocks.length > 0)
if (currentViewData && blocks)
setChartBlocks(() => renderBlockStructure(currentViewData, blocks));
}, [currentViewData, blocks]);
// blocks state management ends
const handleChartView = (key: string) => updateCurrentViewRenderPayload(null, key);
const handleChartView = (key: TGanttViews) => updateCurrentViewRenderPayload(null, key);
const updateCurrentViewRenderPayload = (side: null | "left" | "right", view: string) => {
const selectedCurrentView = view;
const updateCurrentViewRenderPayload = (side: null | "left" | "right", view: TGanttViews) => {
const selectedCurrentView: TGanttViews = view;
const selectedCurrentViewData: ChartDataType | undefined =
selectedCurrentView && selectedCurrentView === currentViewData?.key
? currentViewData
@@ -155,6 +160,9 @@ export const ChartViewRoot: FC<ChartViewRootProps> = ({
const updatingCurrentLeftScrollPosition = (width: number) => {
const scrollContainer = document.getElementById("scroll-container") as HTMLElement;
if (!scrollContainer) return;
scrollContainer.scrollLeft = width + scrollContainer?.scrollLeft;
setItemsContainerWidth(width + scrollContainer?.scrollLeft);
};
@@ -195,6 +203,8 @@ export const ChartViewRoot: FC<ChartViewRootProps> = ({
const clientVisibleWidth: number = scrollContainer?.clientWidth;
const currentScrollPosition: number = scrollContainer?.scrollLeft;
updateScrollLeft(currentScrollPosition);
const approxRangeLeft: number =
scrollWidth >= clientVisibleWidth + 1000 ? 1000 : scrollWidth - clientVisibleWidth;
const approxRangeRight: number = scrollWidth - (approxRangeLeft + clientVisibleWidth);
@@ -205,16 +215,6 @@ export const ChartViewRoot: FC<ChartViewRootProps> = ({
updateCurrentViewRenderPayload("left", currentView);
};
useEffect(() => {
const scrollContainer = document.getElementById("scroll-container") as HTMLElement;
scrollContainer.addEventListener("scroll", onScroll);
return () => {
scrollContainer.removeEventListener("scroll", onScroll);
};
}, [renderView]);
return (
<div
className={`${
@@ -225,44 +225,14 @@ export const ChartViewRoot: FC<ChartViewRootProps> = ({
border ? `border border-custom-border-200` : ``
} flex h-full flex-col rounded-sm select-none bg-custom-background-100 shadow`}
>
{/* chart title */}
{/* <div className="flex w-full flex-shrink-0 flex-wrap items-center gap-5 gap-y-3 whitespace-nowrap p-2 border-b border-custom-border-200">
{title && (
<div className="text-lg font-medium flex gap-2 items-center">
<div>{title}</div>
<div className="text-xs rounded-full px-2 py-1 font-bold border border-custom-primary/75 bg-custom-primary/5 text-custom-text-100">
Gantt View Beta
</div>
</div>
)}
{blocks === null ? (
<div className="text-sm font-medium ml-auto">Loading...</div>
) : (
<div className="text-sm font-medium ml-auto">
{blocks.length} {loaderTitle}
</div>
)}
</div> */}
{/* chart header */}
<div className="flex w-full flex-shrink-0 flex-wrap items-center gap-2 whitespace-nowrap p-2">
{/* <div
className="transition-all border border-custom-border-200 w-[30px] h-[30px] flex justify-center items-center cursor-pointer rounded-sm hover:bg-custom-background-80"
onClick={() => setBlocksSidebarView(() => !blocksSidebarView)}
>
{blocksSidebarView ? (
<XMarkIcon className="h-5 w-5" />
) : (
<Bars4Icon className="h-4 w-4" />
)}
</div> */}
<div className="flex w-full flex-shrink-0 flex-wrap items-center gap-2 whitespace-nowrap px-2.5 py-2">
{title && (
<div className="text-lg font-medium flex gap-2 items-center">
<div>{title}</div>
<div className="text-xs rounded-full px-2 py-1 font-bold border border-custom-primary/75 bg-custom-primary/5 text-custom-text-100">
{/* <div className="text-xs rounded-full px-2 py-1 font-bold border border-custom-primary/75 bg-custom-primary/5 text-custom-text-100">
Gantt View Beta
</div>
</div> */}
</div>
)}
@@ -282,7 +252,7 @@ export const ChartViewRoot: FC<ChartViewRootProps> = ({
allViews.map((_chatView: any, _idx: any) => (
<div
key={_chatView?.key}
className={`cursor-pointer rounded-sm border border-custom-border-200 p-1 px-2 text-xs ${
className={`cursor-pointer rounded-sm p-1 px-2 text-xs ${
currentView === _chatView?.key
? `bg-custom-background-80`
: `hover:bg-custom-background-90`
@@ -296,7 +266,7 @@ export const ChartViewRoot: FC<ChartViewRootProps> = ({
<div className="flex items-center gap-1">
<div
className={`cursor-pointer rounded-sm border border-custom-border-200 p-1 px-2 text-xs hover:bg-custom-background-80`}
className="cursor-pointer rounded-sm p-1 px-2 text-xs hover:bg-custom-background-80"
onClick={handleToday}
>
Today
@@ -316,26 +286,30 @@ export const ChartViewRoot: FC<ChartViewRootProps> = ({
</div>
{/* content */}
<div className="relative flex h-full w-full flex-1 overflow-hidden border-t border-custom-border-200">
<div
id="gantt-container"
className={`relative flex h-full w-full flex-1 overflow-hidden border-t border-custom-border-200 ${
bottomSpacing ? "mb-8" : ""
}`}
>
<div
className="relative flex h-full w-full flex-1 flex-col overflow-hidden overflow-x-auto"
id="gantt-sidebar"
className="h-full w-1/4 flex flex-col border-r border-custom-border-200 space-y-3"
>
<div className="h-[60px] border-b border-custom-border-200 box-border flex-shrink-0" />
<GanttSidebar
title={title}
blockUpdateHandler={blockUpdateHandler}
blocks={chartBlocks}
SidebarBlockRender={SidebarBlockRender}
enableReorder={enableReorder}
/>
</div>
<div
className="relative flex h-full w-full flex-1 flex-col overflow-hidden overflow-x-auto horizontal-scroll-enable"
id="scroll-container"
onScroll={onScroll}
>
{/* blocks components */}
{currentView && currentViewData && (
<GanttChartBlocks
itemsContainerWidth={itemsContainerWidth}
blocks={chartBlocks}
sidebarBlockRender={sidebarBlockRender}
blockRender={blockRender}
blockUpdateHandler={blockUpdateHandler}
enableLeftDrag={enableLeftDrag}
enableRightDrag={enableRightDrag}
enableReorder={enableReorder}
/>
)}
{/* chart */}
{/* {currentView && currentView === "hours" && <HourChartView />} */}
{/* {currentView && currentView === "day" && <DayChartView />} */}
{/* {currentView && currentView === "week" && <WeekChartView />} */}
@@ -343,6 +317,19 @@ export const ChartViewRoot: FC<ChartViewRootProps> = ({
{currentView && currentView === "month" && <MonthChartView />}
{/* {currentView && currentView === "quarter" && <QuarterChartView />} */}
{/* {currentView && currentView === "year" && <YearChartView />} */}
{/* blocks */}
{currentView && currentViewData && (
<GanttChartBlocks
itemsContainerWidth={itemsContainerWidth}
blocks={chartBlocks}
BlockRender={BlockRender}
blockUpdateHandler={blockUpdateHandler}
enableBlockLeftResize={enableBlockLeftResize}
enableBlockRightResize={enableBlockRightResize}
enableBlockMove={enableBlockMove}
/>
)}
</div>
</div>
</div>
+35 -15
View File
@@ -17,9 +17,38 @@ export const MonthChartView: FC<any> = () => {
monthBlocks.length > 0 &&
monthBlocks.map((block, _idxRoot) => (
<div key={`month-${block?.month}-${block?.year}`} className="relative flex flex-col">
<div className="relative border-b border-custom-border-200">
<div className="sticky left-0 inline-flex whitespace-nowrap px-2 py-1 text-sm font-medium capitalize">
{block?.title}
<div className="h-[60px] w-full">
<div className="relative h-[30px]">
<div className="sticky left-0 inline-flex whitespace-nowrap px-3 py-2 text-xs font-medium capitalize">
{block?.title}
</div>
</div>
<div className="flex w-full h-[30px]">
{block?.children &&
block?.children.length > 0 &&
block?.children.map((monthDay, _idx) => (
<div
key={`sub-title-${_idxRoot}-${_idx}`}
className="flex-shrink-0 border-b py-1 text-center capitalize border-custom-border-200"
style={{ width: `${currentViewData?.data.width}px` }}
>
<div className="text-xs space-x-1">
<span className="text-custom-text-200">
{monthDay.dayData.shortTitle[0]}
</span>{" "}
<span
className={
monthDay.today
? "bg-custom-primary-100 text-white px-1 rounded-full"
: ""
}
>
{monthDay.day}
</span>
</div>
</div>
))}
</div>
</div>
@@ -28,19 +57,10 @@ export const MonthChartView: FC<any> = () => {
block?.children.length > 0 &&
block?.children.map((monthDay, _idx) => (
<div
key={`sub-title-${_idxRoot}-${_idx}`}
key={`column-${_idxRoot}-${_idx}`}
className="relative flex h-full flex-col overflow-hidden whitespace-nowrap"
style={{ width: `${currentViewData?.data.width}px` }}
>
<div
className={`flex-shrink-0 border-b py-1 text-center text-sm capitalize font-medium ${
monthDay?.today
? `text-red-500 border-red-500`
: `border-custom-border-200`
}`}
>
<div>{monthDay?.title}</div>
</div>
<div
className={`relative h-full w-full flex-1 flex justify-center ${
["sat", "sun"].includes(monthDay?.dayData?.shortTitle || "")
@@ -48,9 +68,9 @@ export const MonthChartView: FC<any> = () => {
: ``
}`}
>
{monthDay?.today && (
{/* {monthDay?.today && (
<div className="absolute top-0 bottom-0 w-[1px] bg-red-500" />
)}
)} */}
</div>
</div>
))}
@@ -32,16 +32,27 @@ export const ChartContextProvider: React.FC<{ children: React.ReactNode }> = ({
currentViewData: currentViewDataWithView(initialView),
renderView: [],
allViews: allViewsWithData,
activeBlock: null,
});
const [scrollLeft, setScrollLeft] = useState(0);
const handleDispatch = (action: ChartContextActionPayload): ChartContextData => {
const newState = chartReducer(state, action);
dispatch(() => newState);
return newState;
};
const updateScrollLeft = (scrollLeft: number) => {
setScrollLeft(scrollLeft);
};
return (
<ChartContext.Provider value={{ ...state, dispatch: handleDispatch }}>
<ChartContext.Provider
value={{ ...state, scrollLeft, updateScrollLeft, dispatch: handleDispatch }}
>
{children}
</ChartContext.Provider>
);
@@ -108,8 +108,8 @@ export const allViewsWithData: ChartDataType[] = [
startDate: new Date(),
currentDate: new Date(),
endDate: new Date(),
approxFilterRange: 8,
width: 80, // it will preview monthly all dates with weekends highlighted with no limitations ex: title (1, 2, 3)
approxFilterRange: 6,
width: 55, // it will preview monthly all dates with weekends highlighted with no limitations ex: title (1, 2, 3)
},
},
// {
@@ -1,45 +1,57 @@
import React, { useRef, useState } from "react";
import React, { useEffect, useRef, useState } from "react";
// react-beautiful-dnd
import { DraggableProvided } from "react-beautiful-dnd";
// icons
import { Icon } from "components/ui";
// hooks
import { useChart } from "../hooks";
// types
import { IGanttBlock } from "../types";
type Props = {
children: any;
block: IGanttBlock;
handleBlock: (totalBlockShifts: number, dragDirection: "left" | "right") => void;
enableLeftDrag: boolean;
enableRightDrag: boolean;
provided: DraggableProvided;
BlockRender: React.FC<any>;
handleBlock: (totalBlockShifts: number, dragDirection: "left" | "right" | "move") => void;
enableBlockLeftResize: boolean;
enableBlockRightResize: boolean;
enableBlockMove: boolean;
};
export const ChartDraggable: React.FC<Props> = ({
children,
block,
BlockRender,
handleBlock,
enableLeftDrag = true,
enableRightDrag = true,
provided,
enableBlockLeftResize,
enableBlockRightResize,
enableBlockMove,
}) => {
const [isLeftResizing, setIsLeftResizing] = useState(false);
const [isRightResizing, setIsRightResizing] = useState(false);
const [isMoving, setIsMoving] = useState(false);
const [posFromLeft, setPosFromLeft] = useState<number | null>(null);
const parentDivRef = useRef<HTMLDivElement>(null);
const resizableRef = useRef<HTMLDivElement>(null);
const { currentViewData } = useChart();
const { currentViewData, scrollLeft } = useChart();
// check if cursor reaches either end while resizing/dragging
const checkScrollEnd = (e: MouseEvent): number => {
const SCROLL_THRESHOLD = 70;
let delWidth = 0;
const ganttContainer = document.querySelector("#gantt-container") as HTMLElement;
const ganttSidebar = document.querySelector("#gantt-sidebar") as HTMLElement;
const scrollContainer = document.querySelector("#scroll-container") as HTMLElement;
const appSidebar = document.querySelector("#app-sidebar") as HTMLElement;
if (!ganttContainer || !ganttSidebar || !scrollContainer) return 0;
const posFromLeft = e.clientX;
// manually scroll to left if reached the left end while dragging
if (posFromLeft - appSidebar.clientWidth <= 70) {
if (
posFromLeft - (ganttContainer.getBoundingClientRect().left + ganttSidebar.clientWidth) <=
SCROLL_THRESHOLD
) {
if (e.movementX > 0) return 0;
delWidth = -5;
@@ -48,8 +60,8 @@ export const ChartDraggable: React.FC<Props> = ({
} else delWidth = e.movementX;
// manually scroll to right if reached the right end while dragging
const posFromRight = window.innerWidth - e.clientX;
if (posFromRight <= 70) {
const posFromRight = ganttContainer.getBoundingClientRect().right - e.clientX;
if (posFromRight <= SCROLL_THRESHOLD) {
if (e.movementX < 0) return 0;
delWidth = 5;
@@ -60,12 +72,11 @@ export const ChartDraggable: React.FC<Props> = ({
return delWidth;
};
const handleLeftDrag = () => {
if (!currentViewData || !resizableRef.current || !parentDivRef.current || !block.position)
return;
// handle block resize from the left end
const handleBlockLeftResize = () => {
if (!currentViewData || !resizableRef.current || !block.position) return;
const resizableDiv = resizableRef.current;
const parentDiv = parentDivRef.current;
const columnWidth = currentViewData.data.width;
@@ -73,11 +84,9 @@ export const ChartDraggable: React.FC<Props> = ({
resizableDiv.clientWidth ?? parseInt(block.position.width.toString(), 10);
let initialWidth = resizableDiv.clientWidth ?? parseInt(block.position.width.toString(), 10);
let initialMarginLeft = parseInt(parentDiv.style.marginLeft);
let initialMarginLeft = parseInt(resizableDiv.style.marginLeft);
const handleMouseMove = (e: MouseEvent) => {
if (!window) return;
let delWidth = 0;
delWidth = checkScrollEnd(e);
@@ -92,7 +101,7 @@ export const ChartDraggable: React.FC<Props> = ({
if (newWidth < columnWidth) return;
resizableDiv.style.width = `${newWidth}px`;
parentDiv.style.marginLeft = `${newMarginLeft}px`;
resizableDiv.style.marginLeft = `${newMarginLeft}px`;
if (block.position) {
block.position.width = newWidth;
@@ -100,6 +109,7 @@ export const ChartDraggable: React.FC<Props> = ({
}
};
// remove event listeners and call block handler with the updated start date
const handleMouseUp = () => {
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
@@ -115,9 +125,9 @@ export const ChartDraggable: React.FC<Props> = ({
document.addEventListener("mouseup", handleMouseUp);
};
const handleRightDrag = () => {
if (!currentViewData || !resizableRef.current || !parentDivRef.current || !block.position)
return;
// handle block resize from the right end
const handleBlockRightResize = () => {
if (!currentViewData || !resizableRef.current || !block.position) return;
const resizableDiv = resizableRef.current;
@@ -129,8 +139,6 @@ export const ChartDraggable: React.FC<Props> = ({
let initialWidth = resizableDiv.clientWidth ?? parseInt(block.position.width.toString(), 10);
const handleMouseMove = (e: MouseEvent) => {
if (!window) return;
let delWidth = 0;
delWidth = checkScrollEnd(e);
@@ -145,6 +153,7 @@ export const ChartDraggable: React.FC<Props> = ({
if (block.position) block.position.width = Math.max(newWidth, 80);
};
// remove event listeners and call block handler with the updated target date
const handleMouseUp = () => {
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
@@ -160,46 +169,148 @@ export const ChartDraggable: React.FC<Props> = ({
document.addEventListener("mouseup", handleMouseUp);
};
// handle block x-axis move
const handleBlockMove = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
if (!enableBlockMove || !currentViewData || !resizableRef.current || !block.position) return;
e.preventDefault();
e.stopPropagation();
setIsMoving(true);
const resizableDiv = resizableRef.current;
const columnWidth = currentViewData.data.width;
const blockInitialMarginLeft = parseInt(resizableDiv.style.marginLeft);
let initialMarginLeft = parseInt(resizableDiv.style.marginLeft);
const handleMouseMove = (e: MouseEvent) => {
let delWidth = 0;
delWidth = checkScrollEnd(e);
// calculate new marginLeft and update the initial marginLeft using -=
const newMarginLeft = Math.round((initialMarginLeft += delWidth) / columnWidth) * columnWidth;
resizableDiv.style.marginLeft = `${newMarginLeft}px`;
if (block.position) block.position.marginLeft = newMarginLeft;
};
// remove event listeners and call block handler with the updated dates
const handleMouseUp = () => {
setIsMoving(false);
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
const totalBlockShifts = Math.ceil(
(parseInt(resizableDiv.style.marginLeft) - blockInitialMarginLeft) / columnWidth
);
handleBlock(totalBlockShifts, "move");
};
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
};
// scroll to a hidden block
const handleScrollToBlock = () => {
const scrollContainer = document.querySelector("#scroll-container") as HTMLElement;
if (!scrollContainer || !block.position) return;
// update container's scroll position to the block's position
scrollContainer.scrollLeft = block.position.marginLeft - 4;
};
// update block position from viewport's left end on scroll
useEffect(() => {
const block = resizableRef.current;
if (!block) return;
setPosFromLeft(block.getBoundingClientRect().left);
}, [scrollLeft]);
// check if block is hidden on either side
const isBlockHiddenOnLeft =
block.position?.marginLeft &&
block.position?.width &&
scrollLeft > block.position.marginLeft + block.position.width;
const isBlockHiddenOnRight = posFromLeft && window && posFromLeft > window.innerWidth;
return (
<div
id={`block-${block.id}`}
ref={parentDivRef}
className="relative group inline-flex cursor-pointer items-center font-medium transition-all"
style={{
marginLeft: `${block.position?.marginLeft}px`,
}}
>
{enableLeftDrag && (
<>
<div
onMouseDown={handleLeftDrag}
onMouseEnter={() => setIsLeftResizing(true)}
onMouseLeave={() => setIsLeftResizing(false)}
className="absolute top-1/2 -left-2.5 -translate-y-1/2 z-[1] w-6 h-10 bg-brand-backdrop rounded-md cursor-col-resize"
/>
<div
className={`absolute top-1/2 -translate-y-1/2 w-1 h-4/5 rounded-sm bg-custom-background-80 transition-all duration-300 ${
isLeftResizing ? "-left-2.5" : "left-1"
}`}
/>
</>
<>
{/* move to left side hidden block button */}
{isBlockHiddenOnLeft && (
<div
className="fixed ml-1 mt-1.5 z-[1] h-8 w-8 grid place-items-center border border-custom-border-300 rounded cursor-pointer bg-custom-background-80 text-custom-text-200 hover:text-custom-text-100"
onClick={handleScrollToBlock}
>
<Icon iconName="arrow_back" />
</div>
)}
{React.cloneElement(children, { ref: resizableRef, ...provided.dragHandleProps })}
{enableRightDrag && (
<>
<div
onMouseDown={handleRightDrag}
onMouseEnter={() => setIsRightResizing(true)}
onMouseLeave={() => setIsRightResizing(false)}
className="absolute top-1/2 -right-2.5 -translate-y-1/2 z-[1] w-6 h-6 bg-brand-backdrop rounded-md cursor-col-resize"
/>
<div
className={`absolute top-1/2 -translate-y-1/2 w-1 h-4/5 rounded-sm bg-custom-background-80 transition-all duration-300 ${
isRightResizing ? "-right-2.5" : "right-1"
}`}
/>
</>
{/* move to right side hidden block button */}
{isBlockHiddenOnRight && (
<div
className="fixed right-1 mt-1.5 z-[1] h-8 w-8 grid place-items-center border border-custom-border-300 rounded cursor-pointer bg-custom-background-80 text-custom-text-200 hover:text-custom-text-100"
onClick={handleScrollToBlock}
>
<Icon iconName="arrow_forward" />
</div>
)}
</div>
<div
id={`block-${block.id}`}
ref={resizableRef}
className="relative group cursor-pointer font-medium rounded shadow-sm h-full inline-flex items-center transition-all"
style={{
marginLeft: `${block.position?.marginLeft}px`,
width: `${block.position?.width}px`,
}}
>
{/* left resize drag handle */}
{enableBlockLeftResize && (
<>
<div
onMouseDown={handleBlockLeftResize}
onMouseEnter={() => setIsLeftResizing(true)}
onMouseLeave={() => setIsLeftResizing(false)}
className="absolute top-1/2 -left-2.5 -translate-y-1/2 z-[3] w-6 h-full rounded-md cursor-col-resize"
/>
<div
className={`absolute top-1/2 -translate-y-1/2 w-1 h-7 rounded-sm bg-custom-background-100 transition-all duration-300 ${
isLeftResizing ? "-left-2.5" : "left-1"
}`}
/>
</>
)}
<div
className="relative z-[2] rounded h-8 w-full flex items-center"
onMouseDown={handleBlockMove}
>
<BlockRender data={block.data} />
</div>
{/* right resize drag handle */}
{enableBlockRightResize && (
<>
<div
onMouseDown={handleBlockRightResize}
onMouseEnter={() => setIsRightResizing(true)}
onMouseLeave={() => setIsRightResizing(false)}
className="absolute top-1/2 -right-2.5 -translate-y-1/2 z-[2] w-6 h-full rounded-md cursor-col-resize"
/>
<div
className={`absolute top-1/2 -translate-y-1/2 w-1 h-7 rounded-sm bg-custom-background-100 transition-all duration-300 ${
isRightResizing ? "-right-2.5" : "right-1"
}`}
/>
</>
)}
</div>
</>
);
};
@@ -7,9 +7,7 @@ import { ChartContext } from "../contexts";
export const useChart = (): ChartContextReducer => {
const context = useContext(ChartContext);
if (!context) {
throw new Error("useChart must be used within a GanttChart");
}
if (!context) throw new Error("useChart must be used within a GanttChart");
return context;
};
+20 -14
View File
@@ -8,28 +8,32 @@ import { IBlockUpdateData, IGanttBlock } from "./types";
type GanttChartRootProps = {
border?: boolean;
title: null | string;
title: string;
loaderTitle: string;
blocks: IGanttBlock[] | null;
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
sidebarBlockRender: FC<any>;
blockRender: FC<any>;
enableLeftDrag?: boolean;
enableRightDrag?: boolean;
SidebarBlockRender: FC<any>;
BlockRender: FC<any>;
enableBlockLeftResize?: boolean;
enableBlockRightResize?: boolean;
enableBlockMove?: boolean;
enableReorder?: boolean;
bottomSpacing?: boolean;
};
export const GanttChartRoot: FC<GanttChartRootProps> = ({
border = true,
title = null,
title,
blocks,
loaderTitle = "blocks",
blockUpdateHandler,
sidebarBlockRender,
blockRender,
enableLeftDrag = true,
enableRightDrag = true,
SidebarBlockRender,
BlockRender,
enableBlockLeftResize = true,
enableBlockRightResize = true,
enableBlockMove = true,
enableReorder = true,
bottomSpacing = false,
}) => (
<ChartContextProvider>
<ChartViewRoot
@@ -38,11 +42,13 @@ export const GanttChartRoot: FC<GanttChartRootProps> = ({
blocks={blocks}
loaderTitle={loaderTitle}
blockUpdateHandler={blockUpdateHandler}
sidebarBlockRender={sidebarBlockRender}
blockRender={blockRender}
enableLeftDrag={enableLeftDrag}
enableRightDrag={enableRightDrag}
SidebarBlockRender={SidebarBlockRender}
BlockRender={BlockRender}
enableBlockLeftResize={enableBlockLeftResize}
enableBlockRightResize={enableBlockRightResize}
enableBlockMove={enableBlockMove}
enableReorder={enableReorder}
bottomSpacing={bottomSpacing}
/>
</ChartContextProvider>
);
+156
View File
@@ -0,0 +1,156 @@
// react-beautiful-dnd
import { DragDropContext, Draggable, DropResult } from "react-beautiful-dnd";
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
// hooks
import { useChart } from "./hooks";
// ui
import { Loader } from "components/ui";
// icons
import { EllipsisVerticalIcon } from "@heroicons/react/24/outline";
// types
import { IBlockUpdateData, IGanttBlock } from "./types";
type Props = {
title: string;
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
blocks: IGanttBlock[] | null;
SidebarBlockRender: React.FC<any>;
enableReorder: boolean;
};
export const GanttSidebar: React.FC<Props> = ({
title,
blockUpdateHandler,
blocks,
SidebarBlockRender,
enableReorder,
}) => {
const { activeBlock, dispatch } = useChart();
// update the active block on hover
const updateActiveBlock = (block: IGanttBlock | null) => {
dispatch({
type: "PARTIAL_UPDATE",
payload: {
activeBlock: block,
},
});
};
const handleOrderChange = (result: DropResult) => {
if (!blocks) return;
const { source, destination } = result;
// return if dropped outside the list
if (!destination) return;
// return if dropped on the same index
if (source.index === destination.index) return;
let updatedSortOrder = blocks[source.index].sort_order;
// update the sort order to the lowest if dropped at the top
if (destination.index === 0) updatedSortOrder = blocks[0].sort_order - 1000;
// update the sort order to the highest if dropped at the bottom
else if (destination.index === blocks.length - 1)
updatedSortOrder = blocks[blocks.length - 1].sort_order + 1000;
// update the sort order to the average of the two adjacent blocks if dropped in between
else {
const destinationSortingOrder = blocks[destination.index].sort_order;
const relativeDestinationSortingOrder =
source.index < destination.index
? blocks[destination.index + 1].sort_order
: blocks[destination.index - 1].sort_order;
updatedSortOrder = (destinationSortingOrder + relativeDestinationSortingOrder) / 2;
}
// extract the element from the source index and insert it at the destination index without updating the entire array
const removedElement = blocks.splice(source.index, 1)[0];
blocks.splice(destination.index, 0, removedElement);
// call the block update handler with the updated sort order, new and old index
blockUpdateHandler(removedElement.data, {
sort_order: {
destinationIndex: destination.index,
newSortOrder: updatedSortOrder,
sourceIndex: source.index,
},
});
};
return (
<DragDropContext onDragEnd={handleOrderChange}>
<StrictModeDroppable droppableId="gantt-sidebar">
{(droppableProvided) => (
<div
className="h-full overflow-y-auto pl-2.5"
ref={droppableProvided.innerRef}
{...droppableProvided.droppableProps}
>
<>
{blocks ? (
blocks.length > 0 ? (
blocks.map((block, index) => (
<Draggable
key={`sidebar-block-${block.id}`}
draggableId={`sidebar-block-${block.id}`}
index={index}
isDragDisabled={!enableReorder}
>
{(provided, snapshot) => (
<div
className={`h-11 ${
snapshot.isDragging ? "bg-custom-background-80 rounded" : ""
}`}
onMouseEnter={() => updateActiveBlock(block)}
onMouseLeave={() => updateActiveBlock(null)}
ref={provided.innerRef}
{...provided.draggableProps}
>
<div
id={`sidebar-block-${block.id}`}
className={`group h-full w-full flex items-center gap-2 rounded-l px-2 pr-4 ${
activeBlock?.id === block.id ? "bg-custom-background-80" : ""
}`}
>
{enableReorder && (
<button
type="button"
className="rounded p-0.5 text-custom-sidebar-text-200 flex flex-shrink-0 opacity-0 group-hover:opacity-100"
{...provided.dragHandleProps}
>
<EllipsisVerticalIcon className="h-4" />
<EllipsisVerticalIcon className="h-4 -ml-5" />
</button>
)}
<div className="flex-grow truncate w-full h-full">
<SidebarBlockRender data={block.data} />
</div>
</div>
</div>
)}
</Draggable>
))
) : (
<div className="text-custom-text-200 text-sm text-center mt-8">
No <span className="lowercase">{title}</span> found
</div>
)
) : (
<Loader className="pr-2 space-y-3">
<Loader.Item height="34px" />
<Loader.Item height="34px" />
<Loader.Item height="34px" />
<Loader.Item height="34px" />
</Loader>
)}
{droppableProvided.placeholder}
</>
</div>
)}
</StrictModeDroppable>
</DragDropContext>
);
};
+19 -5
View File
@@ -27,19 +27,33 @@ export interface IBlockUpdateData {
target_date?: string;
}
export type TGanttViews = "hours" | "day" | "week" | "bi_week" | "month" | "quarter" | "year";
export interface ChartContextData {
allViews: allViewsType[];
currentView: "hours" | "day" | "week" | "bi_week" | "month" | "quarter" | "year";
currentView: TGanttViews;
currentViewData: ChartDataType | undefined;
renderView: any;
activeBlock: IGanttBlock | null;
}
export type ChartContextActionPayload = {
type: "CURRENT_VIEW" | "CURRENT_VIEW_DATA" | "PARTIAL_UPDATE" | "RENDER_VIEW";
payload: any;
};
export type ChartContextActionPayload =
| {
type: "CURRENT_VIEW";
payload: TGanttViews;
}
| {
type: "CURRENT_VIEW_DATA" | "RENDER_VIEW";
payload: ChartDataType | undefined;
}
| {
type: "PARTIAL_UPDATE";
payload: Partial<ChartContextData>;
};
export interface ChartContextReducer extends ChartContextData {
scrollLeft: number;
updateScrollLeft: (scrollLeft: number) => void;
dispatch: (action: ChartContextActionPayload) => void;
}
+1
View File
@@ -27,6 +27,7 @@ export * from "./started-state-icon";
export * from "./layer-diagonal-icon";
export * from "./lock-icon";
export * from "./menu-icon";
export * from "./module";
export * from "./pencil-scribble-icon";
export * from "./plus-icon";
export * from "./person-running-icon";
@@ -0,0 +1,57 @@
import React from "react";
type Props = {
width?: string;
height?: string;
className?: string;
color?: string;
};
export const ModuleBacklogIcon: React.FC<Props> = ({ width = "20", height = "20", className }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={width}
height={height}
className={className}
viewBox="0 0 247.63 247.6"
>
<g id="Layer_2" data-name="Layer 2">
<g id="Layer_1-2" data-name="Layer 1">
<path fill="#f6aa3e" d="M87.76,165.33a2.1,2.1,0,0,1-2.33,0Z" />
<path fill="#f5a839" d="M94.08,165.33a1.67,1.67,0,0,1-2,0Z" />
<path
fill="#a3a3a2"
d="M.29,115.46A130.18,130.18,0,0,1,2.05,101c.15-1,.53-1.37,1.62-1.15q7.78,1.64,15.6,3.12c1,.2,1.27.56,1.07,1.63a105.92,105.92,0,0,0-1.7,23.11,99.36,99.36,0,0,0,1.7,15.3c.2,1.05,0,1.44-1.06,1.64q-7.82,1.49-15.6,3.12c-1.22.25-1.5-.29-1.66-1.29C1.33,142,.64,137.63.34,133.16c0-.28-.05-.56-.34-.71v-.66c.36-.68.08-1.41.17-2.12A15,15,0,0,0,0,126.13v-4.66a17,17,0,0,0,.17-3.7A9.41,9.41,0,0,1,.29,115.46Z"
/>
<path
fill="#a3a3a2"
d="M132.14.29a130.64,130.64,0,0,1,14.48,1.76c1,.15,1.36.55,1.13,1.63q-1.62,7.79-3.11,15.6c-.2,1-.58,1.26-1.63,1.06a106.48,106.48,0,0,0-23-1.71,101.71,101.71,0,0,0-15.47,1.71c-1.08.21-1.42-.05-1.62-1.08-1-5.2-2-10.41-3.12-15.59-.23-1.1.17-1.47,1.15-1.62A137.72,137.72,0,0,1,115.46.28a5.78,5.78,0,0,1,1.66-.11h1.66A9,9,0,0,0,121.47,0h4.66a17,17,0,0,0,3.7.17A9.41,9.41,0,0,1,132.14.29Z"
/>
<path
fill="#a3a3a2"
d="M229,123.63a92.74,92.74,0,0,0-1.71-18.81c-.25-1.28.09-1.7,1.31-1.93q7.57-1.44,15.12-3c1.13-.24,1.66,0,1.88,1.22a133,133,0,0,1,2,26.28,141.92,141.92,0,0,1-2,19.29c-.19,1.08-.71,1.27-1.69,1.07q-7.71-1.58-15.45-3.07c-1.07-.21-1.36-.6-1.15-1.73A98.45,98.45,0,0,0,229,123.63Z"
/>
<path
fill="#a3a3a2"
d="M123.83,247.6a131.89,131.89,0,0,1-22.79-2c-1.14-.19-1.4-.68-1.18-1.76q1.61-7.71,3.09-15.45c.19-1,.45-1.34,1.6-1.12a105.9,105.9,0,0,0,23,1.72A101.84,101.84,0,0,0,143,227.26c1.05-.19,1.44,0,1.64,1.07q1.49,7.81,3.12,15.61c.22,1.08-.15,1.45-1.15,1.62A129.86,129.86,0,0,1,123.83,247.6Z"
/>
<path
fill="#a3a3a2"
d="M65.13,211.57c-.17.28-.37.62-.58.94-2.93,4.37-5.88,8.72-8.76,13.13-.6.91-1,1-1.92.4a126.44,126.44,0,0,1-32-32c-.8-1.15-.84-1.7.45-2.52,4.35-2.77,8.61-5.66,12.86-8.56.9-.62,1.33-.5,1.94.38a103.22,103.22,0,0,0,27.2,27.21C65.1,211.15,65.14,211.21,65.13,211.57Z"
/>
<path
fill="#a3a3a2"
d="M192.79,226.56c-.51-.06-.61-.4-.79-.67-3.05-4.55-6.08-9.12-9.17-13.65-.6-.89-.16-1.22.5-1.66a103.37,103.37,0,0,0,16.56-14.05,93.49,93.49,0,0,0,10.53-13c.68-1,1.15-1.13,2.18-.42,4.32,3,8.7,5.91,13.09,8.81.83.54,1,.94.38,1.8a125.4,125.4,0,0,1-32,32C193.65,226,193.18,226.31,192.79,226.56Z"
/>
<path
fill="#a3a3a2"
d="M36,65.13c-.28-.18-.62-.37-.94-.59-4.37-2.92-8.72-5.88-13.12-8.75-.95-.62-1-1-.36-1.91A127.22,127.22,0,0,1,53.69,21.72c1.07-.74,1.56-.65,2.27.46,2.79,4.32,5.67,8.6,8.57,12.85.63.91.68,1.38-.32,2.07A105,105,0,0,0,37,64.29C36.43,65.13,36.4,65.14,36,65.13Z"
/>
<path
fill="#a3a3a2"
d="M226.53,54.77c.07.52-.35.64-.66.85-4.56,3.05-9.12,6.08-13.65,9.15-.76.51-1.12.3-1.57-.38a97.64,97.64,0,0,0-11.79-14.34,97,97,0,0,0-15.37-12.87c-1.05-.7-1.09-1.18-.4-2.19,3-4.33,5.91-8.7,8.8-13.09.57-.86,1-.91,1.79-.32A126.12,126.12,0,0,1,225.92,53.8C226.14,54.11,226.33,54.45,226.53,54.77Z"
/>
</g>
</g>
</svg>
);
@@ -0,0 +1,35 @@
import React from "react";
type Props = {
width?: string;
height?: string;
className?: string;
color?: string;
};
export const ModuleCancelledIcon: React.FC<Props> = ({
width = "20",
height = "20",
className,
}) => (
<svg
width={width}
height={height}
className={className}
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clip-path="url(#clip0_4052_100277)">
<path
d="M8 8.84L10.58 11.42C10.7 11.54 10.84 11.6 11 11.6C11.16 11.6 11.3 11.54 11.42 11.42C11.54 11.3 11.6 11.16 11.6 11C11.6 10.84 11.54 10.7 11.42 10.58L8.84 8L11.42 5.42C11.54 5.3 11.6 5.16 11.6 5C11.6 4.84 11.54 4.7 11.42 4.58C11.3 4.46 11.16 4.4 11 4.4C10.84 4.4 10.7 4.46 10.58 4.58L8 7.16L5.42 4.58C5.3 4.46 5.16 4.4 5 4.4C4.84 4.4 4.7 4.46 4.58 4.58C4.46 4.7 4.4 4.84 4.4 5C4.4 5.16 4.46 5.3 4.58 5.42L7.16 8L4.58 10.58C4.46 10.7 4.4 10.84 4.4 11C4.4 11.16 4.46 11.3 4.58 11.42C4.7 11.54 4.84 11.6 5 11.6C5.16 11.6 5.3 11.54 5.42 11.42L8 8.84ZM8 16C6.90667 16 5.87333 15.79 4.9 15.37C3.92667 14.95 3.07667 14.3767 2.35 13.65C1.62333 12.9233 1.05 12.0733 0.63 11.1C0.21 10.1267 0 9.09333 0 8C0 6.89333 0.21 5.85333 0.63 4.88C1.05 3.90667 1.62333 3.06 2.35 2.34C3.07667 1.62 3.92667 1.05 4.9 0.63C5.87333 0.21 6.90667 0 8 0C9.10667 0 10.1467 0.21 11.12 0.63C12.0933 1.05 12.94 1.62 13.66 2.34C14.38 3.06 14.95 3.90667 15.37 4.88C15.79 5.85333 16 6.89333 16 8C16 9.09333 15.79 10.1267 15.37 11.1C14.95 12.0733 14.38 12.9233 13.66 13.65C12.94 14.3767 12.0933 14.95 11.12 15.37C10.1467 15.79 9.10667 16 8 16ZM8 14.8C9.89333 14.8 11.5 14.1367 12.82 12.81C14.14 11.4833 14.8 9.88 14.8 8C14.8 6.10667 14.14 4.5 12.82 3.18C11.5 1.86 9.89333 1.2 8 1.2C6.12 1.2 4.51667 1.86 3.19 3.18C1.86333 4.5 1.2 6.10667 1.2 8C1.2 9.88 1.86333 11.4833 3.19 12.81C4.51667 14.1367 6.12 14.8 8 14.8Z"
fill="#ef4444"
/>
</g>
<defs>
<clipPath id="clip0_4052_100277">
<rect width="16" height="16" fill="white" />
</clipPath>
</defs>
</svg>
);
@@ -0,0 +1,28 @@
import React from "react";
type Props = {
width?: string;
height?: string;
className?: string;
color?: string;
};
export const ModuleCompletedIcon: React.FC<Props> = ({
width = "20",
height = "20",
className,
}) => (
<svg
width={width}
height={height}
className={className}
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6.80486 9.80731L4.84856 7.85103C4.73197 7.73443 4.58542 7.67478 4.4089 7.67208C4.23238 7.66937 4.08312 7.72902 3.96113 7.85103C3.83913 7.97302 3.77814 8.12093 3.77814 8.29474C3.77814 8.46855 3.83913 8.61645 3.96113 8.73844L6.27206 11.0494C6.42428 11.2016 6.60188 11.2777 6.80486 11.2777C7.00782 11.2777 7.18541 11.2016 7.33764 11.0494L12.0227 6.36435C12.1393 6.24776 12.1989 6.10121 12.2016 5.92469C12.2043 5.74817 12.1447 5.59891 12.0227 5.47692C11.9007 5.35493 11.7528 5.29393 11.579 5.29393C11.4051 5.29393 11.2572 5.35493 11.1353 5.47692L6.80486 9.80731ZM8.00141 16C6.89494 16 5.85491 15.79 4.88132 15.3701C3.90772 14.9502 3.06082 14.3803 2.34064 13.6604C1.62044 12.9405 1.05028 12.094 0.63017 11.1208C0.210057 10.1477 0 9.10788 0 8.00141C0 6.89494 0.209966 5.85491 0.629896 4.88132C1.04983 3.90772 1.61972 3.06082 2.33958 2.34064C3.05946 1.62044 3.90598 1.05028 4.87915 0.630171C5.8523 0.210058 6.89212 0 7.99859 0C9.10506 0 10.1451 0.209966 11.1187 0.629897C12.0923 1.04983 12.9392 1.61972 13.6594 2.33959C14.3796 3.05946 14.9497 3.90598 15.3698 4.87915C15.7899 5.8523 16 6.89212 16 7.99859C16 9.10506 15.79 10.1451 15.3701 11.1187C14.9502 12.0923 14.3803 12.9392 13.6604 13.6594C12.9405 14.3796 12.094 14.9497 11.1208 15.3698C10.1477 15.7899 9.10788 16 8.00141 16ZM8 14.7369C9.88071 14.7369 11.4737 14.0842 12.779 12.779C14.0842 11.4737 14.7369 9.88071 14.7369 8C14.7369 6.11929 14.0842 4.52631 12.779 3.22104C11.4737 1.91577 9.88071 1.26314 8 1.26314C6.11929 1.26314 4.52631 1.91577 3.22104 3.22104C1.91577 4.52631 1.26314 6.11929 1.26314 8C1.26314 9.88071 1.91577 11.4737 3.22104 12.779C4.52631 14.0842 6.11929 14.7369 8 14.7369Z"
fill="#16a34a"
/>
</svg>
);
@@ -0,0 +1,71 @@
import React from "react";
type Props = {
width?: string;
height?: string;
className?: string;
color?: string;
};
export const ModuleInProgressIcon: React.FC<Props> = ({
width = "20",
height = "20",
className,
}) => (
<svg
width={width}
height={height}
className={className}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 234.83 234.82"
>
<g id="Layer_2" data-name="Layer 2">
<g id="Layer_1-2" data-name="Layer 1">
<path fill="#f7b964" d="M0,111.14c.63.7.21,1.53.3,2.29-.07.26-.17.28-.3,0Z" />
<path fill="#f6ab3e" d="M0,119.46a3.11,3.11,0,0,1,.3,2q-.19.33-.3,0Z" />
<path
fill="#facf96"
d="M.27,123.16c0,.66.38,1.38-.27,2v-2C.13,122.89.22,122.91.27,123.16Z"
/>
<path fill="#f5a939" d="M0,113.47l.3,0a2.39,2.39,0,0,1-.3,1.71Z" />
<path fill="#f8ba67" d="M.27,123.16a.63.63,0,0,1-.27,0v-1.66l.3,0Z" />
<path
fill="#f39e1f"
d="M234.58,106.92a72,72,0,0,0-.65-8.42,117.08,117.08,0,0,0-13.46-38.74,118.87,118.87,0,0,0-31.73-36.49A115,115,0,0,0,151.17,4.14,83.24,83.24,0,0,0,134.28.58c-2.94-.24-5.89-.22-8.83-.58h-4a2.66,2.66,0,0,1-2,0h-4.32a3.45,3.45,0,0,1-2.33,0h-3.66c-.51.33-1.08.14-1.62.16A87.24,87.24,0,0,0,90,2.35,118.53,118.53,0,0,0,23.16,46,115.24,115.24,0,0,0,4.29,83,85.41,85.41,0,0,0,.6,100.15c-.26,3-.22,6-.6,9v2a6.63,6.63,0,0,1,.17,2.26c-.08.58.17,1.19-.17,1.74v4.32c.35.66.08,1.37.17,2.05v1.57c-.09.68.18,1.39-.17,2v.67c.3.39.14.85.16,1.28.2,3.18.22,6.38.66,9.53a101.21,101.21,0,0,0,4.27,17.76A118.17,118.17,0,0,0,99,234a100.25,100.25,0,0,0,11.37.65,167.86,167.86,0,0,0,23.84-.54,100.39,100.39,0,0,0,23.35-5.72,117.87,117.87,0,0,0,39.67-24.08,117.77,117.77,0,0,0,33.27-53.2,85.63,85.63,0,0,0,3.71-17.37A212.22,212.22,0,0,0,234.58,106.92ZM117.31,217a99.63,99.63,0,0,1-99.7-100.05c0-54.91,44.8-99.35,100.09-99.33,54.89,0,99.32,44.83,99.29,100.14C217,172.43,172.21,217,117.31,217Z"
/>
<path
fill="#f39e1f"
d="M117.33,44a84.49,84.49,0,0,1,12.9,1.15c1.09.19,1.37.56,1.15,1.6q-1.51,7.41-2.94,14.82c-.16.82-.45,1.11-1.33.95a53.31,53.31,0,0,0-19.67,0c-.77.14-1.11-.06-1.26-.83q-1.47-7.59-3-15.16c-.2-1,.21-1.19,1.08-1.35A80.7,80.7,0,0,1,117.33,44Z"
/>
<path
fill="#f39e1f"
d="M44,117.2a80.88,80.88,0,0,1,1.17-12.9c.18-1,.49-1.3,1.49-1.1q7.49,1.53,15,3c.89.18,1,.59.85,1.39a53.54,53.54,0,0,0,0,19.51c.15.83,0,1.2-.88,1.36-5,1-10,2-15,3-.85.17-1.25,0-1.43-1A82.68,82.68,0,0,1,44,117.2Z"
/>
<path
fill="#f39e1f"
d="M190.64,117.39a80.88,80.88,0,0,1-1.17,12.9c-.18,1-.46,1.32-1.48,1.11q-7.49-1.53-15-3c-.88-.17-1-.57-.86-1.38a53.54,53.54,0,0,0,0-19.51c-.18-1,.16-1.23,1-1.39q7.33-1.41,14.66-2.91c1-.21,1.46-.09,1.65,1.08A86.71,86.71,0,0,1,190.64,117.39Z"
/>
<path
fill="#f39e1f"
d="M117.28,190.64a83.24,83.24,0,0,1-12.9-1.15c-1.07-.19-1.38-.53-1.16-1.6q1.52-7.39,2.94-14.82c.16-.8.43-1.12,1.32-.95a53.31,53.31,0,0,0,19.67,0c.92-.17,1.14.2,1.29,1q1.44,7.42,2.95,14.82c.19.95,0,1.35-1,1.54A83,83,0,0,1,117.28,190.64Z"
/>
<path
fill="#f39e1f"
d="M70.7,86.15,70,85.74c-4.23-2.84-8.45-5.69-12.71-8.49-.76-.5-.93-.86-.36-1.67A75.59,75.59,0,0,1,75.41,57.11c.85-.6,1.29-.66,1.93.33,2.71,4.18,5.5,8.3,8.3,12.42.53.78.62,1.18-.28,1.81A54.6,54.6,0,0,0,71.68,85.32C71.07,86.18,71.05,86.17,70.7,86.15Z"
/>
<path
fill="#f39e1f"
d="M178,76.28c.05.58-.38.7-.69.9-4.27,2.87-8.55,5.72-12.82,8.6-.6.41-1,.47-1.44-.23a54.76,54.76,0,0,0-14-14c-.66-.46-.69-.8-.25-1.45q4.33-6.39,8.59-12.83c.47-.72.82-.84,1.56-.33A74.64,74.64,0,0,1,177.53,75.5C177.72,75.77,177.89,76.06,178,76.28Z"
/>
<path
fill="#f39e1f"
d="M70.68,148.46c.48-.11.59.27.77.52A55.65,55.65,0,0,0,85.59,163.1c.58.4.66.72.26,1.32q-4.38,6.47-8.69,13c-.41.63-.74.81-1.43.32a74.65,74.65,0,0,1-18.8-18.8c-.42-.61-.48-1,.23-1.46,4.34-2.87,8.65-5.78,13-8.67C70.32,148.67,70.52,148.56,70.68,148.46Z"
/>
<path
fill="#f39e1f"
d="M158.24,178.06c-.56-.08-.67-.5-.87-.8-2.84-4.23-5.66-8.47-8.52-12.68-.47-.69-.47-1,.29-1.56A54.46,54.46,0,0,0,163,149.11c.53-.77.9-.7,1.57-.24q6.33,4.29,12.7,8.49c.81.53.86.91.32,1.68a74.06,74.06,0,0,1-18.46,18.45C158.84,177.71,158.5,177.9,158.24,178.06Z"
/>
</g>
</g>
</svg>
);
@@ -0,0 +1,7 @@
export * from "./backlog";
export * from "./cancelled";
export * from "./completed";
export * from "./in-progress";
export * from "./module-status-icon";
export * from "./paused";
export * from "./planned";
@@ -0,0 +1,37 @@
// icons
import {
ModuleBacklogIcon,
ModuleCancelledIcon,
ModuleCompletedIcon,
ModuleInProgressIcon,
ModulePausedIcon,
ModulePlannedIcon,
} from "components/icons";
// types
import { TModuleStatus } from "types";
type Props = {
status: TModuleStatus;
className?: string;
height?: string;
width?: string;
};
export const ModuleStatusIcon: React.FC<Props> = ({
status,
className,
height = "12px",
width = "12px",
}) => {
if (status === "backlog")
return <ModuleBacklogIcon className={className} height={height} width={width} />;
else if (status === "cancelled")
return <ModuleCancelledIcon className={className} height={height} width={width} />;
else if (status === "completed")
return <ModuleCompletedIcon className={className} height={height} width={width} />;
else if (status === "in-progress")
return <ModuleInProgressIcon className={className} height={height} width={width} />;
else if (status === "paused")
return <ModulePausedIcon className={className} height={height} width={width} />;
else return <ModulePlannedIcon className={className} height={height} width={width} />;
};
@@ -0,0 +1,31 @@
import React from "react";
type Props = {
width?: string;
height?: string;
className?: string;
color?: string;
};
export const ModulePausedIcon: React.FC<Props> = ({ width = "20", height = "20", className }) => (
<svg
width={width}
height={height}
className={className}
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clip-path="url(#clip0_4052_100275)">
<path
d="M6.4435 10.34C6.6145 10.34 6.75667 10.2825 6.87 10.1675C6.98333 10.0525 7.04 9.91 7.04 9.74V6.24C7.04 6.07 6.98217 5.9275 6.8665 5.8125C6.75082 5.6975 6.60749 5.64 6.4365 5.64C6.2655 5.64 6.12333 5.6975 6.01 5.8125C5.89667 5.9275 5.84 6.07 5.84 6.24V9.74C5.84 9.91 5.89783 10.0525 6.0135 10.1675C6.12918 10.2825 6.27251 10.34 6.4435 10.34ZM9.5635 10.34C9.7345 10.34 9.87667 10.2825 9.99 10.1675C10.1033 10.0525 10.16 9.91 10.16 9.74V6.24C10.16 6.07 10.1022 5.9275 9.9865 5.8125C9.87082 5.6975 9.72749 5.64 9.5565 5.64C9.3855 5.64 9.24333 5.6975 9.13 5.8125C9.01667 5.9275 8.96 6.07 8.96 6.24V9.74C8.96 9.91 9.01783 10.0525 9.1335 10.1675C9.24918 10.2825 9.39251 10.34 9.5635 10.34ZM8 16C6.89333 16 5.85333 15.79 4.88 15.37C3.90667 14.95 3.06 14.38 2.34 13.66C1.62 12.94 1.05 12.0933 0.63 11.12C0.21 10.1467 0 9.10667 0 8C0 7.54667 0.0366667 7.09993 0.11 6.6598C0.183333 6.21965 0.293333 5.78639 0.44 5.36C0.493333 5.21333 0.593333 5.11667 0.74 5.07C0.886667 5.02333 1.02667 5.04199 1.16 5.12596C1.30285 5.20993 1.40523 5.33327 1.46714 5.49596C1.52905 5.65865 1.54 5.82 1.5 5.98C1.42 6.31333 1.35 6.64765 1.29 6.98294C1.23 7.31823 1.2 7.65725 1.2 8C1.2 9.89833 1.85875 11.5063 3.17624 12.8238C4.49375 14.1413 6.10167 14.8 8 14.8C9.89833 14.8 11.5063 14.1413 12.8238 12.8238C14.1413 11.5063 14.8 9.89833 14.8 8C14.8 6.10167 14.1413 4.49375 12.8238 3.17624C11.5063 1.85875 9.89833 1.2 8 1.2C7.63235 1.2 7.26852 1.22667 6.90852 1.28C6.54852 1.33333 6.19235 1.41333 5.84 1.52C5.68 1.57333 5.52 1.56667 5.36 1.5C5.2 1.43333 5.08667 1.32667 5.02 1.18C4.95333 1.03333 4.96 0.886667 5.04 0.74C5.12 0.593333 5.23333 0.493333 5.38 0.44C5.79333 0.306667 6.21333 0.2 6.64 0.12C7.06667 0.04 7.49333 0 7.92 0C9.02667 0 10.07 0.21 11.05 0.63C12.03 1.05 12.8863 1.62 13.6189 2.34C14.3516 3.06 14.9316 3.90667 15.3589 4.88C15.7863 5.85333 16 6.89333 16 8C16 9.10667 15.79 10.1467 15.37 11.12C14.95 12.0933 14.38 12.94 13.66 13.66C12.94 14.38 12.0933 14.95 11.12 15.37C10.1467 15.79 9.10667 16 8 16ZM2.65764 3.62C2.37921 3.62 2.14333 3.52255 1.95 3.32764C1.75667 3.13275 1.66 2.89608 1.66 2.61764C1.66 2.33921 1.75745 2.10333 1.95236 1.91C2.14725 1.71667 2.38392 1.62 2.66236 1.62C2.94079 1.62 3.17667 1.71745 3.37 1.91236C3.56333 2.10725 3.66 2.34392 3.66 2.62236C3.66 2.90079 3.56255 3.13667 3.36764 3.33C3.17275 3.52333 2.93608 3.62 2.65764 3.62Z"
fill="#525252"
/>
</g>
<defs>
<clipPath id="clip0_4052_100275">
<rect width="16" height="16" fill="white" />
</clipPath>
</defs>
</svg>
);
@@ -0,0 +1,24 @@
import React from "react";
type Props = {
width?: string;
height?: string;
className?: string;
color?: string;
};
export const ModulePlannedIcon: React.FC<Props> = ({ width = "20", height = "20", className }) => (
<svg
width={width}
height={height}
className={className}
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8.57177 7.43329L11.3665 10.228C11.4883 10.3498 11.5441 10.4809 11.5339 10.6213C11.5238 10.7617 11.4578 10.8928 11.336 11.0146C11.2142 11.1364 11.0794 11.1973 10.9317 11.1973C10.784 11.1973 10.6492 11.1364 10.5274 11.0146L7.64476 8.12349C7.57709 8.05582 7.52408 7.98139 7.48574 7.90018C7.4474 7.81898 7.42823 7.72538 7.42823 7.61936V3.51362C7.42823 3.35574 7.48405 3.22097 7.5957 3.10932C7.70734 2.99768 7.84211 2.94185 8 2.94185C8.15789 2.94185 8.29266 2.99768 8.4043 3.10932C8.51595 3.22097 8.57177 3.35574 8.57177 3.51362V7.43329ZM0.806954 11.4933C0.573486 11.04 0.390212 10.5655 0.257131 10.0698C0.124064 9.57411 0.0383541 9.07477 0 8.57177H1.15709C1.18077 8.98793 1.24646 9.38746 1.35418 9.77036C1.46188 10.1533 1.60427 10.5297 1.78133 10.8996L0.806954 11.4933ZM0.021992 7.42823C0.0603462 6.92523 0.143806 6.4273 0.272371 5.93444C0.400937 5.4416 0.579131 4.96567 0.806954 4.50665L1.80333 5.07842C1.62062 5.44834 1.47315 5.82983 1.36093 6.22287C1.24872 6.61591 1.18077 7.0177 1.15709 7.42823H0.021992ZM3.52381 14.6128C3.10877 14.3286 2.71855 14.0103 2.35315 13.6578C1.98774 13.3054 1.66294 12.9217 1.37872 12.5067L2.3751 11.9044C2.5939 12.2507 2.84879 12.5656 3.13976 12.8492C3.43073 13.1329 3.74934 13.3914 4.09558 13.6249L3.52381 14.6128ZM2.3751 4.09558L1.37872 3.52381C1.66294 3.09411 1.98408 2.70163 2.34215 2.34637C2.70023 1.99112 3.09411 1.67139 3.52381 1.38719L4.09558 2.3751C3.75837 2.60856 3.44568 2.86571 3.15753 3.14653C2.86937 3.42736 2.60856 3.7437 2.3751 4.09558ZM7.42823 16C6.92523 15.9616 6.4273 15.8745 5.93444 15.7386C5.4416 15.6027 4.96567 15.4209 4.50665 15.193L5.07842 14.2187C5.44834 14.4014 5.82983 14.5452 6.22287 14.6501C6.61591 14.7549 7.0177 14.8192 7.42823 14.8429V16ZM5.10042 1.80333L4.50665 0.806954C4.96567 0.579131 5.4416 0.397272 5.93444 0.261376C6.4273 0.125479 6.92523 0.0383541 7.42823 0V1.15709C7.0177 1.18077 6.61957 1.24872 6.23386 1.36093C5.84815 1.47315 5.47034 1.62062 5.10042 1.80333ZM8.57177 16V14.8429C8.9823 14.8192 9.38409 14.7549 9.77713 14.6501C10.1702 14.5452 10.5517 14.4014 10.9216 14.2187L11.4933 15.193C11.0343 15.4209 10.5584 15.6027 10.0656 15.7386C9.5727 15.8745 9.07477 15.9616 8.57177 16ZM10.9216 1.78133C10.5517 1.59862 10.1702 1.45482 9.77713 1.34994C9.38409 1.24505 8.9823 1.18077 8.57177 1.15709V0C9.07477 0.0383541 9.5727 0.125479 10.0656 0.261376C10.5584 0.397272 11.0343 0.579131 11.4933 0.806954L10.9216 1.78133ZM12.4762 14.6128L11.9044 13.6469C12.2563 13.4134 12.5726 13.149 12.8535 12.8535C13.1343 12.558 13.3914 12.2416 13.6249 11.9044L14.6128 12.4982C14.3286 12.9132 14.0052 13.297 13.6426 13.6494C13.28 14.0018 12.8912 14.323 12.4762 14.6128ZM13.6249 4.08713C13.3914 3.74991 13.1306 3.43862 12.8425 3.15328C12.5543 2.86796 12.2416 2.60856 11.9044 2.3751L12.4762 1.38719C12.8912 1.67703 13.28 1.99817 13.6426 2.3506C14.0052 2.70304 14.3314 3.08678 14.6213 3.50181L13.6249 4.08713ZM14.8429 7.42823C14.8192 7.0177 14.7535 6.61957 14.6458 6.23386C14.5381 5.84815 14.3929 5.46752 14.2102 5.09197L15.193 4.50665C15.4265 4.96003 15.6098 5.43314 15.7429 5.926C15.8759 6.41884 15.9616 6.91958 16 7.42823H14.8429ZM15.193 11.4933L14.2187 10.9216C14.4014 10.5517 14.5452 10.1702 14.6501 9.77713C14.7549 9.38409 14.8192 8.9823 14.8429 8.57177H16C15.9616 9.07477 15.8745 9.5727 15.7386 10.0656C15.6027 10.5584 15.4209 11.0343 15.193 11.4933Z"
fill="#3f76ff"
/>
</svg>
);
@@ -21,6 +21,7 @@ export const getStateGroupIcon = (
width={width}
height={height}
color={color ?? STATE_GROUP_COLORS["backlog"]}
className="flex-shrink-0"
/>
);
case "unstarted":
@@ -29,6 +30,7 @@ export const getStateGroupIcon = (
width={width}
height={height}
color={color ?? STATE_GROUP_COLORS["unstarted"]}
className="flex-shrink-0"
/>
);
case "started":
@@ -37,6 +39,7 @@ export const getStateGroupIcon = (
width={width}
height={height}
color={color ?? STATE_GROUP_COLORS["started"]}
className="flex-shrink-0"
/>
);
case "completed":
@@ -45,6 +48,7 @@ export const getStateGroupIcon = (
width={width}
height={height}
color={color ?? STATE_GROUP_COLORS["completed"]}
className="flex-shrink-0"
/>
);
case "cancelled":
@@ -53,6 +57,7 @@ export const getStateGroupIcon = (
width={width}
height={height}
color={color ?? STATE_GROUP_COLORS["cancelled"]}
className="flex-shrink-0"
/>
);
default:
@@ -0,0 +1,106 @@
import { useRouter } from "next/router";
import useSWR, { mutate } from "swr";
// components
import { AddComment, IssueActivitySection } from "components/issues";
// services
import issuesService from "services/issues.service";
// hooks
import useUser from "hooks/use-user";
import useToast from "hooks/use-toast";
// types
import { IIssue, IIssueComment } from "types";
// fetch-keys
import { PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
type Props = { issueDetails: IIssue };
export const InboxIssueActivity: React.FC<Props> = ({ issueDetails }) => {
const router = useRouter();
const { workspaceSlug, projectId, inboxIssueId } = router.query;
const { setToastAlert } = useToast();
const { user } = useUser();
const { data: issueActivity, mutate: mutateIssueActivity } = useSWR(
workspaceSlug && projectId && inboxIssueId
? PROJECT_ISSUES_ACTIVITY(inboxIssueId.toString())
: null,
workspaceSlug && projectId && inboxIssueId
? () =>
issuesService.getIssueActivities(
workspaceSlug.toString(),
projectId.toString(),
inboxIssueId.toString()
)
: null
);
const handleCommentUpdate = async (comment: IIssueComment) => {
if (!workspaceSlug || !projectId || !inboxIssueId) return;
await issuesService
.patchIssueComment(
workspaceSlug as string,
projectId as string,
inboxIssueId as string,
comment.id,
comment,
user
)
.then(() => mutateIssueActivity());
};
const handleCommentDelete = async (commentId: string) => {
if (!workspaceSlug || !projectId || !inboxIssueId) return;
mutateIssueActivity((prevData: any) => prevData?.filter((p: any) => p.id !== commentId), false);
await issuesService
.deleteIssueComment(
workspaceSlug as string,
projectId as string,
inboxIssueId as string,
commentId,
user
)
.then(() => mutateIssueActivity());
};
const handleAddComment = async (formData: IIssueComment) => {
if (!workspaceSlug || !issueDetails) return;
await issuesService
.createIssueComment(
workspaceSlug.toString(),
issueDetails.project,
issueDetails.id,
formData,
user
)
.then(() => {
mutate(PROJECT_ISSUES_ACTIVITY(issueDetails.id));
})
.catch(() =>
setToastAlert({
type: "error",
title: "Error!",
message: "Comment could not be posted. Please try again.",
})
);
};
return (
<div className="space-y-5">
<h3 className="text-lg text-custom-text-100">Comments/Activity</h3>
<IssueActivitySection
activity={issueActivity}
handleCommentUpdate={handleCommentUpdate}
handleCommentDelete={handleCommentDelete}
/>
<AddComment onSubmit={handleAddComment} />
</div>
);
};
@@ -14,13 +14,8 @@ import inboxServices from "services/inbox.service";
import useInboxView from "hooks/use-inbox-view";
import useUserAuth from "hooks/use-user-auth";
// components
import {
AddComment,
IssueActivitySection,
IssueDescriptionForm,
IssueDetailsSidebar,
IssueReaction,
} from "components/issues";
import { IssueDescriptionForm, IssueDetailsSidebar, IssueReaction } from "components/issues";
import { InboxIssueActivity } from "components/inbox";
// ui
import { Loader } from "components/ui";
// icons
@@ -42,7 +37,6 @@ import { INBOX_ISSUES, INBOX_ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "cons
const defaultValues = {
name: "",
description: "",
description_html: "",
estimate_point: null,
assignees_list: [],
@@ -293,9 +287,9 @@ export const InboxMainContent: React.FC = () => {
</div>
<div>
<IssueDescriptionForm
workspaceSlug={workspaceSlug as string}
issue={{
name: issueDetails.name,
description: issueDetails.description,
description_html: issueDetails.description_html,
}}
handleFormSubmit={submitChanges}
@@ -311,11 +305,7 @@ export const InboxMainContent: React.FC = () => {
issueId={issueDetails.id}
/>
<div className="space-y-5">
<h3 className="text-lg text-custom-text-100">Comments/Activity</h3>
<IssueActivitySection issueId={issueDetails.id} user={user} />
<AddComment issueId={issueDetails.id} user={user} />
</div>
<InboxIssueActivity issueDetails={issueDetails} />
</div>
<div className="basis-1/3 space-y-5 border-custom-border-200 p-5">

Some files were not shown because too many files have changed in this diff Show More