Compare commits

..
Author SHA1 Message Date
Henit Chobisa 016c8dc9c9 chore: updated nginx config 2023-09-02 08:00:53 +00:00
Henit Chobisa eb5882876b chore: updated docker-compose hub 2023-09-02 08:00:24 +00:00
Henit Chobisa 9379d5e4ec chore: added spaces packages 2023-09-02 07:57:40 +00:00
Henit Chobisa e91406bd3f chore: modified next.js config 2023-09-02 07:57:29 +00:00
Henit Chobisa f9c3b4ca12 chore: Updated dockerfiles for web and space 2023-09-02 07:57:00 +00:00
Henit Chobisa 702c27e015 chore: Updated Docker Compose with Added Spaces 2023-09-02 07:03:44 +00:00
Henit Chobisa 8b704f302c chore: Modifed Migration Dependency from deploy-pages 2023-09-02 07:00:24 +00:00
Henit Chobisa 541c7a00d8 chore: Updated Icons from .png to .svg 2023-09-02 06:59:14 +00:00
367cfc4180 fix: plane deploy peek overview bugs (#2050)
* chore: public board endpoints (#2030)

* feat: editor for issue description (#2038)

* chore: cycle endpoint to return display name as well in the assignee distribution (#2041)

* chore: cycle endpoint to return display name as well in the assignee distribution

* fix: value error

* fix: Gantt chart bugs (#2024)

* fix: only left mouse button should trigger all the events

* fix: extra block shadow

* chore: minor updates on the peek overview

* chore: remove filters button from the navbar

---------

Co-authored-by: Nikhil <118773738+pablohashescobar@users.noreply.github.com>
Co-authored-by: Lakhan Baheti <94619783+1akhanBaheti@users.noreply.github.com>
2023-09-01 15:51:34 +05:30
sriram veeraghanta a09e797de8 Merge branch 'develop' of github.com:makeplane/plane into feat/deploy-pages 2023-09-01 15:45:13 +05:30
Kritika UpadhyayandGitHub 42ece0d784 chore: updates project invite placeholder (#2049) 2023-09-01 15:37:27 +05:30
sriram veeraghanta 6f84bc4f52 fix: delete vote fixes 2023-09-01 15:27:37 +05:30
Bavisetti NarayanandGitHub 1e9f0823f8 fix: imported uuid (#2048) 2023-09-01 14:41:20 +05:30
sriram veeraghanta a1e249a5bb reaction fixes 2023-09-01 14:31:48 +05:30
Aaryan KhandelwalandGitHub 4ba3ef5c24 fix: peek overview bugs (#2043)
* fix: side peek modal shaking

* refactor: peek overview layout

* fix: date selector, activity mutation

* fix: delete issue handler

* fix: assignees mutation
2023-09-01 13:52:55 +05:30
NikhilandGitHub c6d9ace6a2 dev: migrations for v0.12 release (#2044) 2023-09-01 13:20:52 +05:30
sriram veeraghanta 77a043b141 fix: commenting fixes 2023-09-01 11:49:07 +05:30
Aaryan KhandelwalandGitHub 0d4bcd2758 fix: Gantt chart bugs (#2024)
* fix: only left mouse button should trigger all the events

* fix: extra block shadow
2023-09-01 11:23:43 +05:30
NikhilandGitHub 3a0d96a48d chore: cycle endpoint to return display name as well in the assignee distribution (#2041)
* chore: cycle endpoint to return display name as well in the assignee distribution

* fix: value error
2023-09-01 11:21:34 +05:30
Lakhan BahetiandGitHub eab1d9329b feat: editor for issue description (#2038) 2023-09-01 10:59:17 +05:30
sriramveeraghanta 5c8060c777 lock file updated 2023-09-01 00:35:29 +05:30
sriramveeraghanta abe3cf9d68 fix: build issues by commenting few parts 2023-09-01 00:34:57 +05:30
sriramveeraghanta 376da835ba fix: commenting issues fixed 2023-09-01 00:22:50 +05:30
NikhilandGitHub 099bce87b5 chore: public board endpoints (#2030) 2023-09-01 00:08:40 +05:30
sriramveeraghanta 4fd7a78d70 merge conflicts resolved 2023-08-31 23:27:04 +05:30
6b4084287c style: plane deploy (#2039)
* chore: improve access field for comments for public boards (#1956)

Co-authored-by: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com>

* chore: update user activity endpoint to return only workspace activities (#1980)

* fix: n+1 in issue history and issue automation tasks (#1994)

* 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>

* dev: remove gunicorn config (#1999)

* feat: mark all read notifications (#1963)

* feat: mark all read notifications

* fix: changed string to boolean

* fix: changed snoozed condition

* chore: project public board issue retrieve (#2003)

* chore: project public board issue retrieve

* dev: project issues list endpoint

* fix: issue public retrieve endpoint

* fix: only external comments will show in deploy boards (#2010)

* fix: issue votes (#2006)

* fix: issue votes

* fix: added default as 1 in vote

* fix: issue vote migration file

* fix: access creation in comments (#2013)

* dev: user timezone select option (#2002)

* fix: start date filter not working on the platform (#2007)

* feat: access selector for comment (#2012)

* dev: access specifier for comment

* chore: change access order

* style: revamp of the issue details sidebar (#2014)

* 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

* 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

* feat: user timezones (#2009)

* dev: user timezones

* feat: user timezones

* fix: user created by stats (#2016)

* 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>

* 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

* 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

* dev: revamp publish project modal (#2022)

* dev: revamp publish project modal

* chore: sidebar dropdown text

* fix: bugs on the user profile page (#2018)

* chore: return issue votes in public issue list endpoint (#2026)

* style: list view

* [feat]: Tiptap table integration (#2008)

* added basic table support

* fixed table position at bottom

* fixed image node deletion logic's regression issue

* added compatible styles

* enabled slash commands

* disabled slash command and bubble menu's node selector for table cells

* added dropcursor support to type below the table/image

* blocked image uploads for handledrop and paste actions

* style: kanban view

* style: tiptap table (#2033)

* style: theming added

* chore: issue reactions and votes

---------

Co-authored-by: Nikhil <118773738+pablohashescobar@users.noreply.github.com>
Co-authored-by: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com>
Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
Co-authored-by: Bavisetti Narayan <narayan@Bavisettis-MacBook-Pro.local>
Co-authored-by: M. Palanikannan <73993394+Palanikannan1437@users.noreply.github.com>
2023-08-31 23:24:03 +05:30
NikhilandGitHub b496a62540 fix: subscribed issues are filtering (#2037) 2023-08-31 19:07:56 +05:30
sriram veeraghanta 1cd1f6e8c9 fix: revalidating using swr 2023-08-31 18:43:37 +05:30
sriram veeraghanta 66bee2a094 fix:key fixes 2023-08-31 16:37:25 +05:30
sriram veeraghanta 240acc6cd0 fix: mobx changes 2023-08-31 16:35:26 +05:30
Aaryan KhandelwalandGitHub af929ab741 style: tiptap table (#2033) 2023-08-31 16:30:28 +05:30
M. PalanikannanandGitHub 38b7f4382f [feat]: Tiptap table integration (#2008)
* added basic table support

* fixed table position at bottom

* fixed image node deletion logic's regression issue

* added compatible styles

* enabled slash commands

* disabled slash command and bubble menu's node selector for table cells

* added dropcursor support to type below the table/image

* blocked image uploads for handledrop and paste actions
2023-08-31 13:41:41 +05:30
NikhilandGitHub 320608ea73 chore: return issue votes in public issue list endpoint (#2026) 2023-08-31 11:32:58 +05:30
sriramveeraghanta 6246a740d5 fix: peek over data fetching fixes 2023-08-31 00:12:45 +05:30
sriram veeraghanta a27edad810 fix: type fixes 2023-08-30 20:35:10 +05:30
sriram veeraghanta 579347c991 fix:mobx fixes 2023-08-30 20:33:21 +05:30
sriram veeraghanta 0deed77f1d fix: repeatative api calls removed 2023-08-30 18:33:18 +05:30
sriram veeraghanta d7c27b75fa fix: mobx changes 2023-08-30 17:39:11 +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
sriram veeraghanta 91910d106c fix: seo fixes 2023-08-30 16:51:13 +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
sriram veeraghanta 061717f0af yarn lock file updated 2023-08-30 14:02:51 +05:30
sriram veeraghanta 93f01ec6d1 seo fixes 2023-08-30 13:56:41 +05:30
sriram veeraghanta 8cadced1aa fix: moving to pages dir and downgrading next to 12 2023-08-30 13:41:25 +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
Dakshesh JainandGitHub 96399c7112 feat: public board (#2017)
* feat: filters in plane deploy

implemented multi-level dropdown for plane deploy

* style: spacing and fonts

* feat: plane deploy

implemented authentication/theming, created/modified all the required store & services

* devL reactions, voting, comments and theme
2023-08-30 12:49:15 +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
1072 changed files with 19261 additions and 14688 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ module.exports = {
extends: ["custom"],
settings: {
next: {
rootDir: ["web/", "deploy/"],
rootDir: ["apps/*"],
},
},
};
@@ -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 }}
+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
@@ -20,6 +20,7 @@ from .project import (
ProjectMemberLiteSerializer,
ProjectDeployBoardSerializer,
ProjectMemberAdminSerializer,
ProjectPublicMemberSerializer
)
from .state import StateSerializer, StateLiteSerializer
from .view import IssueViewSerializer, IssueViewFavoriteSerializer
@@ -44,6 +45,7 @@ from .issue import (
IssueReactionSerializer,
CommentReactionSerializer,
IssueVoteSerializer,
IssuePublicSerializer,
)
from .module import (
+33 -5
View File
@@ -113,7 +113,11 @@ class IssueCreateSerializer(BaseSerializer):
]
def validate(self, data):
if data.get("start_date", None) is not None and data.get("target_date", None) is not None and data.get("start_date", None) > data.get("target_date", None):
if (
data.get("start_date", None) is not None
and data.get("target_date", None) is not None
and data.get("start_date", None) > data.get("target_date", None)
):
raise serializers.ValidationError("Start date cannot exceed target date")
return data
@@ -554,9 +558,7 @@ class CommentReactionSerializer(BaseSerializer):
read_only_fields = ["workspace", "project", "comment", "actor"]
class IssueVoteSerializer(BaseSerializer):
class Meta:
model = IssueVote
fields = ["issue", "vote", "workspace_id", "project_id", "actor"]
@@ -569,7 +571,7 @@ class IssueCommentSerializer(BaseSerializer):
project_detail = ProjectLiteSerializer(read_only=True, source="project")
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
comment_reactions = CommentReactionLiteSerializer(read_only=True, many=True)
is_member = serializers.BooleanField(read_only=True)
class Meta:
model = IssueComment
@@ -582,7 +584,6 @@ class IssueCommentSerializer(BaseSerializer):
"updated_by",
"created_at",
"updated_at",
"access",
]
@@ -676,6 +677,33 @@ class IssueLiteSerializer(BaseSerializer):
]
class IssuePublicSerializer(BaseSerializer):
project_detail = ProjectLiteSerializer(read_only=True, source="project")
state_detail = StateLiteSerializer(read_only=True, source="state")
reactions = IssueReactionLiteSerializer(read_only=True, many=True, source="issue_reactions")
votes = IssueVoteSerializer(read_only=True, many=True)
class Meta:
model = Issue
fields = [
"id",
"name",
"description_html",
"sequence_id",
"state",
"state_detail",
"project",
"project_detail",
"workspace",
"priority",
"target_date",
"reactions",
"votes",
]
read_only_fields = fields
class IssueSubscriberSerializer(BaseSerializer):
class Meta:
model = IssueSubscriber
+15 -2
View File
@@ -15,6 +15,7 @@ from plane.db.models import (
ProjectIdentifier,
ProjectFavorite,
ProjectDeployBoard,
ProjectPublicMember,
)
@@ -112,7 +113,7 @@ class ProjectDetailSerializer(BaseSerializer):
class ProjectMemberSerializer(BaseSerializer):
workspace = WorkSpaceSerializer(read_only=True)
workspace = WorkspaceLiteSerializer(read_only=True)
project = ProjectLiteSerializer(read_only=True)
member = UserLiteSerializer(read_only=True)
@@ -177,5 +178,17 @@ class ProjectDeployBoardSerializer(BaseSerializer):
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",
]
+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,
+3 -4
View File
@@ -191,11 +191,10 @@ class CycleViewSet(BaseViewSet):
workspace__slug=slug,
project_id=project_id,
)
.annotate(first_name=F("assignees__first_name"))
.annotate(last_name=F("assignees__last_name"))
.annotate(display_name=F("assignees__display_name"))
.annotate(assignee_id=F("assignees__id"))
.annotate(avatar=F("assignees__avatar"))
.values("first_name", "last_name", "assignee_id", "avatar")
.values("display_name", "assignee_id", "avatar")
.annotate(total_issues=Count("assignee_id"))
.annotate(
completed_issues=Count(
@@ -209,7 +208,7 @@ class CycleViewSet(BaseViewSet):
filter=Q(completed_at__isnull=True),
)
)
.order_by("first_name", "last_name")
.order_by("display_name")
)
label_distribution = (
+352 -22
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, IsAuthenticated
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):
@@ -333,7 +337,7 @@ class UserWorkSpaceIssues(BaseAPIView):
issue_queryset = (
Issue.issue_objects.filter(
(Q(assignees__in=[request.user]) | Q(created_by=request.user)),
(Q(assignees__in=[request.user]) | Q(created_by=request.user) | Q(issue_subscribers__subscriber=request.user)),
workspace__slug=slug,
)
.annotate(
@@ -482,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")
@@ -492,6 +496,12 @@ class IssueActivityEndpoint(BaseAPIView):
.filter(project__project_projectmember__member=self.request.user)
.order_by("created_at")
.select_related("actor", "issue", "project", "workspace")
.prefetch_related(
Prefetch(
"comment_reactions",
queryset=CommentReaction.objects.select_related("actor"),
)
)
)
issue_activities = IssueActivitySerializer(issue_activities, many=True).data
issue_comments = IssueCommentSerializer(issue_comments, many=True).data
@@ -588,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()
)
@@ -769,7 +788,9 @@ class SubIssuesEndpoint(BaseAPIView):
.order_by("state_group")
)
result = {item["state_group"]: item["state_count"] for item in state_distribution}
result = {
item["state_group"]: item["state_count"] for item in state_distribution
}
serializer = IssueLiteSerializer(
sub_issues,
@@ -1384,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:
@@ -1394,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:
@@ -1434,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:
@@ -1444,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:
@@ -1468,6 +1532,18 @@ class IssueCommentPublicViewSet(BaseViewSet):
"workspace__id",
]
def get_permissions(self):
if self.action in ["list", "retrieve"]:
self.permission_classes = [
AllowAny,
]
else:
self.permission_classes = [
IsAuthenticated,
]
return super(IssueCommentPublicViewSet, self).get_permissions()
def get_queryset(self):
project_deploy_board = ProjectDeployBoard.objects.get(
workspace__slug=self.kwargs.get("slug"),
@@ -1479,9 +1555,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:
@@ -1499,21 +1585,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)
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",
@@ -1523,6 +1601,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:
@@ -1567,7 +1655,8 @@ class IssueCommentPublicViewSet(BaseViewSet):
except (IssueComment.DoesNotExist, ProjectDeployBoard.DoesNotExist):
return Response(
{"error": "IssueComent Does not exists"},
status=status.HTTP_400_BAD_REQUEST,)
status=status.HTTP_400_BAD_REQUEST,
)
def destroy(self, request, slug, project_id, issue_id, pk):
try:
@@ -1648,6 +1737,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:
@@ -1679,6 +1785,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:
@@ -1733,8 +1852,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"},
@@ -1765,6 +1905,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:
@@ -1799,7 +1953,24 @@ 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)
@@ -1818,6 +1989,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:
@@ -1828,27 +2012,173 @@ 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"),
)
)
.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(
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,
)
+23 -133
View File
@@ -287,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,
@@ -1140,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(
+4 -4
View File
@@ -137,11 +137,11 @@ 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,
+1 -1
View File
@@ -1100,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,
)
@@ -1198,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,
+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=settings.AWS_REGION,
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
config=Config(signature_version="s3v4"),
)
file_name = f"{workspace_id}/export-{slug}-{token_id[:6]}-{timezone.now()}.zip"
s3.upload_fileobj(
zip_file,
settings.AWS_S3_BUCKET_NAME,
file_name,
ExtraArgs={"ACL": "public-read", "ContentType": "application/zip"},
)
expires_in = 7 * 24 * 60 * 60
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)
+166 -1
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)
@@ -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', '0042_alter_analyticview_created_by_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 (
+5 -3
View File
@@ -293,7 +293,7 @@ class IssueComment(ProjectBaseModel):
comment_json = models.JSONField(blank=True, default=dict)
comment_html = models.TextField(blank=True, default="<p></p>")
attachments = ArrayField(models.URLField(), size=10, blank=True, default=list)
issue = models.ForeignKey(Issue, on_delete=models.CASCADE)
issue = models.ForeignKey(Issue, on_delete=models.CASCADE, related_name="issue_comments")
# System can also create comment
actor = models.ForeignKey(
settings.AUTH_USER_MODEL,
@@ -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
+85
View File
@@ -0,0 +1,85 @@
{
"name": "Plane",
"description": "Plane helps you track your issues, epics, and product roadmaps.",
"repository": "http://github.com/makeplane/plane",
"logo": "https://avatars.githubusercontent.com/u/115727700?s=200&v=4",
"website": "https://plane.so/",
"success_url": "/",
"stack": "heroku-22",
"keywords": [
"plane",
"project management",
"django",
"next"
],
"addons": [
"heroku-postgresql:mini",
"heroku-redis:mini"
],
"buildpacks": [
{
"url": "https://github.com/heroku/heroku-buildpack-python.git"
},
{
"url": "https://github.com/heroku/heroku-buildpack-nodejs#v176"
}
],
"env": {
"EMAIL_HOST": {
"description": "Email host to send emails from",
"value": ""
},
"EMAIL_HOST_USER": {
"description": "Email host to send emails from",
"value": ""
},
"EMAIL_HOST_PASSWORD": {
"description": "Email host to send emails from",
"value": ""
},
"EMAIL_FROM": {
"description": "Email Sender",
"value": ""
},
"EMAIL_PORT": {
"description": "The default Email PORT to use",
"value": "587"
},
"AWS_REGION": {
"description": "AWS Region to use for S3",
"value": "false"
},
"AWS_ACCESS_KEY_ID": {
"description": "AWS Access Key ID to use for S3",
"value": ""
},
"AWS_SECRET_ACCESS_KEY": {
"description": "AWS Secret Access Key to use for S3",
"value": ""
},
"AWS_S3_BUCKET_NAME": {
"description": "AWS Bucket Name to use for S3",
"value": ""
},
"SENTRY_DSN": {
"description": "",
"value": ""
},
"WEB_URL": {
"description": "Web URL for Plane this will be used for redirections in the emails",
"value": ""
},
"GITHUB_CLIENT_SECRET": {
"description": "Github Client Secret",
"value": ""
},
"NEXT_PUBLIC_API_BASE_URL": {
"description": "Next Public API Base URL",
"value": ""
},
"SECRET_KEY": {
"description": "Django Secret Key",
"value": ""
}
}
}
+7
View File
@@ -0,0 +1,7 @@
module.exports = {
root: true,
extends: ["custom"],
rules: {
"@next/next/no-img-element": "off",
},
};
+11
View File
@@ -0,0 +1,11 @@
FROM node:18-alpine
RUN apk add --no-cache libc6-compat
# Set working directory
WORKDIR /app
COPY . .
RUN yarn global add turbo
RUN yarn install
EXPOSE 3000
CMD ["yarn","dev"]
@@ -15,6 +15,7 @@ FROM node:18-alpine AS installer
RUN apk add --no-cache libc6-compat
WORKDIR /app
ARG NEXT_PUBLIC_API_BASE_URL=http://localhost:8000
ARG NEXT_PUBLIC_DEPLOY_URL=http://localhost/spaces
# First install the dependencies (as they change less often)
COPY .gitignore .gitignore
@@ -54,8 +55,11 @@ COPY --from=installer --chown=captain:plane /app/apps/app/.next/standalone ./
COPY --from=installer --chown=captain:plane /app/apps/app/.next ./apps/app/.next
ARG NEXT_PUBLIC_API_BASE_URL=http://localhost:8000
ARG NEXT_PUBLIC_DEPLOY_URL=http://localhost/spaces
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \
BUILT_NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
BUILT_NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \
NEXT_PUBLIC_DEPLOY_URL=$NEXT_PUBLIC_DEPLOY_URL
USER root
COPY replace-env-vars.sh /usr/local/bin/
@@ -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>
@@ -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;
@@ -0,0 +1,4 @@
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,
@@ -140,13 +133,14 @@ export const GptAssistantModal: React.FC<Props> = ({
return (
<div
className={`absolute ${inset} z-20 w-full space-y-4 rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-4 shadow ${isOpen ? "block" : "hidden"
}`}
className={`absolute ${inset} z-20 w-full space-y-4 rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-4 shadow ${
isOpen ? "block" : "hidden"
}`}
>
{((content && content !== "") || (htmlContent && htmlContent !== "<p></p>")) && (
<div className="text-sm">
Content:
<TiptapEditor
<TipTapEditor
workspaceSlug={workspaceSlug as string}
value={htmlContent ?? `<p>${content}</p>`}
customClassName="-m-3"
@@ -160,7 +154,7 @@ 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"
@@ -180,10 +174,11 @@ export const GptAssistantModal: React.FC<Props> = ({
type="text"
name="task"
register={register}
placeholder={`${content && content !== ""
placeholder={`${
content && content !== ""
? "Tell AI what action to perform on this content..."
: "Ask AI anything..."
}`}
}`}
autoComplete="off"
/>
<div className={`flex gap-2 ${response === "" ? "justify-end" : "justify-between"}`}>
@@ -219,8 +214,8 @@ export const GptAssistantModal: React.FC<Props> = ({
{isSubmitting
? "Generating response..."
: response === ""
? "Generate response"
: "Generate again"}
? "Generate response"
: "Generate again"}
</PrimaryButton>
</div>
</div>

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