Compare commits

...

213 Commits

Author SHA1 Message Date
gakshita
314f714277 fix: added tooltips 2025-01-06 12:41:54 +05:30
gakshita
5a1c521e62 fix: types 2025-01-03 19:59:13 +05:30
gakshita
fb9c3645bd fix: types 2025-01-03 19:55:13 +05:30
gakshita
ff29170ec7 fix: recents api integrations 2025-01-03 19:38:14 +05:30
sangeethailango
99d9702ef5 Return issue ID 2025-01-03 17:02:30 +05:30
sangeethailango
bba84361b1 Only return user ID of project members 2025-01-03 16:51:27 +05:30
gakshita
0263021f2f Merge branch 'preview' of https://github.com/makeplane/plane into feat-home 2025-01-03 15:12:39 +05:30
gakshita
0d564e6a5d fix 2025-01-03 15:12:15 +05:30
Sangeetha
870ca17e13 [WEB-2937] feat: home recent activies list endpoint (#6295)
* Crud for wuick links

* Validate quick link existence

* Add custom method for destroy and retrieve

* Add List method

* Remove print statements

* List all the workspace quick links

* feat: endpoint to get recently active items

* Resolve conflicts

* Resolve conflicts

* Add filter to only list required entities

* Return required fields

* Add filter

* Add filter
2025-01-03 15:09:01 +05:30
sriram veeraghanta
4652ad0fc3 fix: remove localdb tracing 2025-01-03 15:01:57 +05:30
Aaryan Khandelwal
be9dbc1b18 [PE-97] chore: re-order pages options (#6303)
* chore: re-order pages dropdown options

* chore: re-order pages dropdown options
2025-01-03 14:58:43 +05:30
gakshita
a0317650fb fix: seperate route added 2025-01-03 14:57:52 +05:30
gakshita
2254d50491 fix 2025-01-03 14:45:26 +05:30
gakshita
c2412bb3ea fix: preserved old component 2025-01-03 14:39:07 +05:30
Nikhil
3fd2550d69 feat: add issue attachment external endpoint (#6307) 2025-01-03 14:29:40 +05:30
Prateek Shourya
d6bcd8dd15 feat: introduced stacked bar chart and tree map chart. (#6305) 2025-01-03 14:24:14 +05:30
Vamsi Krishna
873e4330bc [WEB-2870]feat: language support (#6215)
* fix: adding language support package

* fix: language support implementation using mobx

* fix: adding more languages for support

* fix: profile settings translations

* feat: added language support for sidebar and user settings

* feat: added language support for deactivation modal

* fix: added project sync after transfer issues (#6200)

* code refactor and improvement (#6203)

* chore: package code refactoring

* chore: component restructuring and refactor

* chore: comment create improvement

* refactor: enhance workspace and project wrapper modularity (#6207)

* [WEB-2678]feat: added functionality to add labels directly from dropdown (#6211)

* enhancement:added functionality to add features directly from dropdown

* fix: fixed import order

* fix: fixed lint errors

* chore: added common component for project activity (#6212)

* chore: added common component for project activity

* fix: added enum

* fix: added enum for initiatives

* - Do not clear temp files that are locked. (#6214)

- Handle edge cases in sync workspace

* fix: labels empty state for drop down (#6216)

* refactor: remove cn helper function from the editor package (#6217)

* * feat: added language support to issue create modal in sidebar
* fix: project activity type

* * fix: added missing translations
* fix: modified translation for plurals

* fix: fixed spanish translation

* dev: language type error in space user profile types

* fix: type fixes

* chore: added alpha tag

---------

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
Co-authored-by: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com>
Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com>
Co-authored-by: Akshita Goyal <36129505+gakshita@users.noreply.github.com>
Co-authored-by: Satish Gandham <satish.iitg@gmail.com>
Co-authored-by: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com>
Co-authored-by: gurusinath <gurusainath007@gmail.com>
2025-01-03 14:16:26 +05:30
Anmol Singh Bhatia
ade0aa1643 fix: active cycle graph tooltip and endpoint validation (#6306) 2025-01-03 14:08:02 +05:30
Vamsi Krishna
6a13a64996 [WEB-1964]chore: cycles actions restructuring (#6298)
* chore: cycles quick actions restructuring

* chore: added additional actions to cycle list actions

* chore: cycle quick action structure

* chore: added additional actions to cycle list actions

* chore: added end cycle hook

* fix: updated end cycle export

---------

Co-authored-by: gurusinath <gurusainath007@gmail.com>
2025-01-02 18:27:34 +05:30
Bavisetti Narayan
5e6c02358d fix: changed the response structure (#6301) 2025-01-02 15:51:38 +05:30
Bavisetti Narayan
61d6d928ba chore: handled the unique state name validation (#6299) 2025-01-02 14:43:43 +05:30
Vamsi Krishna
a555df27e7 [WEB-2941]chore: added transfer issues button to cycles (#6296)
* chore: removed refacotr changes:w

* chore: removed requirements modifications
2025-01-02 12:33:45 +05:30
Manish Gupta
df671d13c7 improvement: build action improvement (#6248)
* updated action to use makeplane/actions repo

* updated action
2025-01-02 00:00:04 +05:30
Akshita Goyal
c7ebaf6ba0 chore: added react masonry package (#6293) 2024-12-31 16:15:40 +05:30
gakshita
1c020f07fe Merge branch 'preview' of https://github.com/makeplane/plane into feat-home 2024-12-31 15:33:01 +05:30
Bavisetti Narayan
119d46dc2b chore: search issues endpoint (#6291) 2024-12-31 15:29:38 +05:30
Sangeetha
25f7d813b9 [WEB-2928] feat: Home Quick links CRUD (#6290)
* Crud for wuick links

* Validate quick link existence

* Add custom method for destroy and retrieve

* Add List method

* Remove print statements

* List all the workspace quick links

* Filter by user
2024-12-31 15:06:24 +05:30
gakshita
8b43497967 Merge branch 'preview' of https://github.com/makeplane/plane into feat-home 2024-12-31 14:31:24 +05:30
Aaryan Khandelwal
ec2af13258 chore: merge two separate info popovers (#6289) 2024-12-31 14:01:27 +05:30
Akash Verma
8833e4e23b Integrates LiteLLM for Unified Access to Multiple LLM Models (#5925)
* adds litellm gateway

* Fixes repeating code

* Fixes error exposing

* Fixes error for None text

* handles logging exception

* Adds multiple providers support

* handling edge cases

* adds new envs to instance store

* strategy pattern for llm config

---------

Co-authored-by: akash5100 <akashzsh08@gmail.com>
2024-12-31 13:57:26 +05:30
Aaryan Khandelwal
752a27a175 [PE-97] refactor: pages actions (#6234)
* dev: support for edition specific options in pages

* refactor: page quick actions

* chore: add customizable page actions

* fix: type errors

* dev: hook to get page operations

* refactor: remove unnecessary props

* chore: add permisssions to duplicate page endpoint

* chore: memoize arranged options

* chore: use enum for page access

* chore: add type assertion

* fix: auth for access change and delete

* fix: removing readonly editor

* chore: add sync for page access cahnge

* fix: sync state

* fix: indexeddb sync loader added

* fix: remove node error fixed

* style: page title and checkbox

* chore: removing the syncing logic

* revert: is editable check removed in display message

* fix: editable field optional

* fix: editable removed as optional prop

* fix: extra options import fix

* fix: remove readonly stuff

* fix: added toggle access

* chore: add access change sync

* fix: full width toggle

* refactor: types and enums added

* refactore: update store action

* chore: changed the duplicate viewset

* fix: remove the page binary

* fix: duplicate page action

* fix: merge conflicts

---------

Co-authored-by: Palanikannan M <akashmalinimurugu@gmail.com>
Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
2024-12-31 12:54:09 +05:30
Aaryan Khandelwal
94f421f27d chore: add live server prettier config (#6287) 2024-12-27 21:03:20 +05:30
Aaryan Khandelwal
8d7425a3b7 [PE-182] refactor: pages' components and store for scalability (#6283)
* refactor: created a generic base page instance

* refactor: project store hooks

* chore: add missing prop declaration

* refactor: editor page root and body

* refactor: issue embed hook

* chore: update search entity types

* fix: version editor component

* fix: add page to favorites action

---------

Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com>
2024-12-27 20:41:38 +05:30
Anmol Singh Bhatia
211d5e1cd0 chore: code refactor and build fix (#6285)
* chore: code refactor and build fix

* chore: code refactor

* chore: code refactor
2024-12-27 18:18:45 +05:30
Vamsi Krishna
3c6bbaef3c fix: modified link behaviour to improve accessibility (#6284) 2024-12-27 17:46:40 +05:30
Prateek Shourya
4159d12959 [WEB-2889] fix: global views sorting when hyper model is enabled. (#6280) 2024-12-27 13:03:26 +05:30
Anmol Singh Bhatia
2f2f8dc5f4 [WEB-2880] chore: project detail response updated (#6281)
* chore: project detail response updated

* chore: code refactor
2024-12-27 09:17:35 +05:30
Anmol Singh Bhatia
756a71ca78 [WEB-2907] chore: issue store updated and code refactor (#6279)
* chore: issue and epic store updated and code refactor

* chore: layout ux copy updated
2024-12-26 20:01:32 +05:30
Vamsi Krishna
36b3328c5e fix: user role not updating in user profile (#6278) 2024-12-26 17:19:43 +05:30
Prateek Shourya
a5c1282e52 [WEB-2896] fix: mutation problem with issue properties while accepting an intake issue. (#6277) 2024-12-26 16:46:52 +05:30
devin-ai-integration[bot]
ed64168ca7 chore(utils): copy helper functions from web/helpers (#6264)
* chore(utils): copy helper functions from web/helpers

Co-Authored-By: sriram@plane.so <sriram@plane.so>

* chore(utils): bump version to 0.24.2

Co-Authored-By: sriram@plane.so <sriram@plane.so>

* chore: bump root package version to 0.24.2

Co-Authored-By: sriram@plane.so <sriram@plane.so>

* fix: remove duplicate function and simplify auth utils

Co-Authored-By: sriram@plane.so <sriram@plane.so>

* fix: improve HTML entity escaping in sanitizeHTML

Co-Authored-By: sriram@plane.so <sriram@plane.so>

* fix: version changes

---------

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: sriram@plane.so <sriram@plane.so>
2024-12-26 15:27:40 +05:30
Bavisetti Narayan
f54f3a6091 chore: workspace entity search endpoint (#6272)
* chore: workspace entity search endpoint

* fix: editor entity search endpoint

* chore: restrict guest users

---------

Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
2024-12-26 15:00:32 +05:30
Bavisetti Narayan
2d9464e841 chore: create unique constraints for webhook (#6257)
* chore: create unique constraints for webhook

* chore: updated the migration file
2024-12-24 21:00:50 +05:30
Vamsi Krishna
70f72a2b0f [WEB-2699]chore: added issue count for upcoming cycles (#6273)
* chore: added issue count for upcoming cycles

* chore: memoized show issue count
2024-12-24 20:53:06 +05:30
Vamsi Krishna
c0b5e0e766 fix: label creation (#6271) 2024-12-24 20:52:31 +05:30
Anmol Singh Bhatia
fedcdf0c84 [WEB-2879] chore sub issue analytics improvements (#6275)
* chore: epics type added to package

* chore: epic analytics helper function added

* chore: sub issue analytics mutation improvement
2024-12-24 20:52:03 +05:30
Bavisetti Narayan
ff936887d2 chore: quick link migration (#6274)
* chore: added workspace link queryset

* chore: added workspace in sort order
2024-12-24 20:51:15 +05:30
gakshita
ae89bed07b chore: wip 2024-12-24 15:56:24 +05:30
Anmol Singh Bhatia
ea78c2bceb fix: active cycle update payload (#6270) 2024-12-24 14:01:47 +05:30
Vamsi Krishna
ba1a314608 [WEB-1412]fix: split labels in kanban board (#6253)
* fix: split labels in kanban board

* chore: incresaed labels max render and moved labels to end of properties
chore: refactored label render component
2024-12-23 20:28:17 +05:30
Vamsi Krishna
3a6a8e3a97 fix: create view - layout drop down close (#6267) 2024-12-23 20:27:54 +05:30
Bavisetti Narayan
1735561ffd chore: remove the default intake state (#6252)
* chore: remove the default intake state

* chore: changed the payload
2024-12-23 20:26:48 +05:30
Prateek Shourya
b80a904bbf [WEB-2863] chore: minor improvements and bug fixes (#6222)
* fix: remove deprecated icons from logo picker

* improvement: minor empty states updates
2024-12-23 20:26:07 +05:30
M. Palanikannan
20260f0720 [PE-101] feat: smooth scrolling in editor while dragging and dropping nodes (#6233)
* fix: smoother drag scrolling

* fix: refactoring out common fns

* fix: moved to mouse events instead of drag

* fix: improving the drag preview

* fix: added better selection logic

* fix: drag handle new way almost working

* fix: drag-handle old behaviour with better scrolling

* fix: remove experiments

* fix: better scroll thresholds

* fix: transition to drop cursor added

* fix: drag handling speed

* fix: cleaning up listeners

* fix: common out selection and dragging logic

* fix: scroll threshold logic fixed
2024-12-23 20:04:34 +05:30
Prateek Shourya
6070ed4d36 improvement: enhance activity components and types modularity (#6262) 2024-12-23 20:03:42 +05:30
M. Palanikannan
ac47cc62ee [PE-102] fix: zooming for fullscreen images (#6266)
* fix: added magnification properly and also moving around the zoomed image

* fix: zoom via trackpad pinch

* fix: update imports

* fix: initial magnification is reset
2024-12-23 20:03:10 +05:30
sriram veeraghanta
1059fbbebf fix: build errors while upgrading date-fns 2024-12-23 19:05:52 +05:30
sriram veeraghanta
61478d1b6b fix: build errors in utils package 2024-12-23 18:45:22 +05:30
Aaryan Khandelwal
88737b1072 fix: issue mentions (#6265) 2024-12-23 17:42:39 +05:30
Anmol Singh Bhatia
34d114a4c5 fix: sub-issue collapsible visibility (#6259) 2024-12-23 15:42:03 +05:30
Aaryan Khandelwal
d54c1bae03 [PE-93] regression: mention users highlight color, reomve bot users from search list (#6258)
* chore: remove bot users in mention

* fix: user highlight color

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
2024-12-23 15:25:40 +05:30
devin-ai-integration[bot]
9f5def3a6a chore: copy helper functions from admin and space into @plane/utils (#6256)
* chore: copy helper functions from space to @plane/utils

Co-Authored-By: sriram@plane.so <sriram@plane.so>

* refactor: move enums from utils/auth.ts to @plane/constants/auth.ts

Co-Authored-By: sriram@plane.so <sriram@plane.so>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: sriram@plane.so <sriram@plane.so>
2024-12-23 14:30:13 +05:30
sriram veeraghanta
043f4eaa5e chore: common services package (#6255)
* fix: initial services package setup

* fix: services packages updates

* fix: services changes

* fix: merge conflicts

* chore: file structuring

* fix: import fixes
2024-12-23 01:51:30 +05:30
sriram veeraghanta
1ee0661ac1 fix: missing packages in utils and live 2024-12-22 22:08:04 +05:30
sriram veeraghanta
60f7edcef8 fix: moving space constants to package 2024-12-21 17:17:43 +05:30
sriram veeraghanta
23849789f9 chore: admin imports refactor (#6251)
* chore: admin package refactoring

* chore: build errors

* fix: removing duplicates
2024-12-20 20:44:46 +05:30
Aaryan Khandelwal
33acb9e8ed [PE-93] regression: mentions in space app, entity search (#6250)
* fix: mentions in space app

* fix: user entity filter
2024-12-20 16:55:57 +05:30
Aaryan Khandelwal
d2c0940f04 refactor: accept generic function to search mentions (#6249) 2024-12-20 15:51:36 +05:30
Nikhil
00624eafbd fix: issue serializer to remove deleted labels and assignees (#6241) 2024-12-20 14:44:38 +05:30
Prateek Shourya
e6bf57aa18 [WEB-2885] fix: retain issue description when creating an issue copy (#6243) 2024-12-20 14:17:41 +05:30
Aaryan Khandelwal
3c8c657ee0 fix: cn helper function inport error (#6244) 2024-12-20 14:17:22 +05:30
Aaryan Khandelwal
119d343d5f [PE-93] refactor: editor mentions extension (#6178)
* refactor: editor mentions

* fix: build errors

* fix: build errors

* chore: add cycle status to search endpoint response

* fix: build errors

* fix: dynamic mention content in markdown

* chore: update entity search endpoint

* style: user mention popover

* chore: edition specific mention content handler

* chore: show deactivated user for old mentions

* chore: update search entity keys

* refactor: use editor mention hook
2024-12-20 13:41:25 +05:30
Aaryan Khandelwal
c10b875e2a fix: page title fixed height (#6242) 2024-12-20 13:24:19 +05:30
Vamsi Krishna
f10f9cbd41 [WEB-2859]chore: sub issue list optimization (#6232)
* chore: optimized api calls for sub-issue widget

* chore: added api call for on sub issues widget click
2024-12-19 22:45:08 +05:30
guru_sainath
9b71a702c7 [WEB-2884] chore: Update timezone list, add new endpoint, and update timezone dropdowns (#6231)
* dev: updated timezones list

* chore: added rate limiting
2024-12-19 20:15:55 +05:30
gakshita
15ff3eea9b wip 2024-12-19 17:36:14 +05:30
Vamsi Krishna
0a320a8540 * fix: avoided uncessary api call while creating issue draft (#6230)
* fix: fixed import order in module header
2024-12-19 16:26:35 +05:30
Anmol Singh Bhatia
44d8de1169 chore: remove workspace toggle from issue parent modal (#6227) 2024-12-19 13:59:44 +05:30
Prateek Shourya
6214c09170 refactor: move all issue related enums to constants package (#6229) 2024-12-19 13:58:54 +05:30
sriram veeraghanta
51ca353577 Merge branch 'preview' of github.com:makeplane/plane into preview 2024-12-18 14:58:13 +05:30
sriram veeraghanta
881c744eb9 fix: build errors 2024-12-18 14:57:59 +05:30
Bavisetti Narayan
ec41ae61b4 chore: removed the deleted votes and reaction (#6218) 2024-12-18 14:54:03 +05:30
Aaryan Khandelwal
5773c2bde3 chore: gif support for editor (#6219) 2024-12-18 13:17:05 +05:30
M. Palanikannan
e33bae2125 [PE-92] fix: removing readonly collaborative document editor (#6209)
* fix: removing readonly editor

* fix: sync state

* fix: indexeddb sync loader added

* fix: remove node error fixed

* style: page title and checkbox

* chore: removing the syncing logic

* revert: is editable check removed in display message

* fix: editable field optional

* fix: editable removed as optional prop

* fix: extra options import fix

---------

Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
2024-12-18 12:58:18 +05:30
Aaryan Khandelwal
580c4b1930 refactor: remove cn helper function from the editor package (#6217) 2024-12-18 12:22:14 +05:30
Vamsi Krishna
ddd4b51b4e fix: labels empty state for drop down (#6216) 2024-12-17 19:14:10 +05:30
Satish Gandham
ede4aad55b - Do not clear temp files that are locked. (#6214)
- Handle edge cases in sync workspace
2024-12-17 17:46:24 +05:30
Akshita Goyal
1a715c98b2 chore: added common component for project activity (#6212)
* chore: added common component for project activity

* fix: added enum

* fix: added enum for initiatives
2024-12-17 17:02:59 +05:30
Vamsi Krishna
8e6d885731 [WEB-2678]feat: added functionality to add labels directly from dropdown (#6211)
* enhancement:added functionality to add features directly from dropdown

* fix: fixed import order

* fix: fixed lint errors
2024-12-17 14:29:56 +05:30
Prateek Shourya
4507802aba refactor: enhance workspace and project wrapper modularity (#6207) 2024-12-16 19:01:37 +05:30
Anmol Singh Bhatia
438cc33046 code refactor and improvement (#6203)
* chore: package code refactoring

* chore: component restructuring and refactor

* chore: comment create improvement
2024-12-16 17:24:50 +05:30
Vamsi Krishna
442b0fd7e5 fix: added project sync after transfer issues (#6200) 2024-12-16 15:15:48 +05:30
Dancia
1119b9dc36 Updated README.md (#6182)
* Updated README.md

* minor fixes

* minor fixes
2024-12-16 14:33:08 +05:30
Manish Gupta
47a76f48b4 fix: separated docker compose environment variables (#5575)
* Separated environment variables for specific app containers.

* updated env

* cleanup

* Separated environment variables for specific app containers.

* updated env

* cleanup

---------

Co-authored-by: Akshat Jain <akshatjain9782@gmail.com>
2024-12-16 13:23:33 +05:30
Manish Gupta
a0f03d07f3 chore: Check github releases for upgrades (#6162)
* modifed action and install.sh for selfhost

* updated selfhost readme and install.sh

* fixes

* changes suggested by code-rabbit

* chore: updated powered by (#6160)

* improvement: update fetch map during workspace-level module fetch to reduce redundant API calls (#6159)

* fix: remove unwanted states fetching logic to avoid multiple API calls. (#6158)

* chore remove unnecessary CTA (#6161)

* fix: build branch workflow upload artifacts

* fixes

* changes suggested by code-rabbit

* modifed action and install.sh for selfhost

* updated selfhost readme and install.sh

* fix: build branch workflow upload artifacts

* fixes

* changes suggested by code-rabbit

---------

Co-authored-by: guru_sainath <gurusainath007@gmail.com>
Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com>
Co-authored-by: rahulramesha <71900764+rahulramesha@users.noreply.github.com>
Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
2024-12-16 13:22:23 +05:30
Nikhil
74b2ec03ff feat: add language support (#6205) 2024-12-15 11:04:03 +05:30
guru_sainath
5908998127 [WEB-2854] chore: trigger issue_description_version task on issue create and update (#6202)
* chore: issue description version task trigger from issue create and update

* chore: add default value in prop
2024-12-13 22:30:29 +05:30
guru_sainath
df6a80e7ae chore: add sync jobs for issue_version and issue_description_version tables (#6199)
* chore: added fields in issue_version and profile tables and created a new sticky table

* chore: removed point in issue version

* chore: add imports in init

* chore: added sync jobs for issue_version and issue_description_version

* chore: removed logs

* chore: updated logginh

---------

Co-authored-by: sainath <sainath@sainaths-MacBook-Pro.local>
2024-12-13 17:48:55 +05:30
guru_sainath
6ff258ceca chore: Add fields to issue_version and profile tables, and create new sticky table (#6198)
* chore: added fields in issue_version and profile tables and created a new sticky table

* chore: removed point in issue version

* chore: add imports in init

---------

Co-authored-by: sainath <sainath@sainaths-MacBook-Pro.local>
2024-12-13 17:30:25 +05:30
Saurabhkmr98
a8140a5f08 chore: Add logger package for node server side apps (#6188)
* chore: Add logger as a package

* chore: Add logger package for node server side apps

* remove plane logger import in web

* resolve pr reviews and add client logger with readme update

* fix: transformation and added middleware for logging requests

* chore: update readme

* fix: env configurable max file size

---------

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
2024-12-13 14:32:56 +05:30
Prateek Shourya
9234f21f26 [WEB-2848] improvement: enhanced components modularity (#6196)
* improvement: enhanced componenets modularity

* fix: lint errors resolved
2024-12-13 14:26:26 +05:30
Bavisetti Narayan
ab11e83535 [WEB-2843] chore: updated the cycle end date logic (#6194)
* chore: updated the cycle end date logic

* chore: changed the key for timezone
2024-12-13 13:34:07 +05:30
Akshita Goyal
b4112358ac [WEB-2688] chore: added icons and splitted issue header (#6195)
* chore: added icons and splitted issue header

* fix: added ee filler component

* fix: component name fixed

* fix: removed dupes

* fix: casing
2024-12-13 13:31:13 +05:30
Aaryan Khandelwal
77239ebcd4 fix: GitHub casing across the platform (#6193) 2024-12-13 02:22:46 +05:30
Prateek Shourya
54f828cbfa refactor: enhance components modularity and introduce new UI componenets (#6192)
* feat: add navigation dropdown component

* chore: enhance title/ description loader and componenet modularity

* chore: issue store filter update

* chore: added few icons to ui package

* chore: improvements for tabs componenet

* chore: enhance sidebar modularity

* chore: update issue and router store to add support for additional issue layouts

* chore: enhanced cycle componenets modularity

* feat: added project grouping header for cycles list

* chore: enhanced project dropdown componenet by adding multiple selection functionality

* chore: enhanced rich text editor modularity by taking members ids as props for mentions

* chore: added functionality to filter disabled layouts in issue-layout dropdown

* chore: added support to pass project ids as props in project card list

* feat: multi select project modal

* chore: seperate out project componenet for reusability

* chore: command pallete store improvements

* fix: build errors
2024-12-12 21:40:57 +05:30
Bavisetti Narayan
9ad8b43408 chore: handled the cycle date time using project timezone (#6187)
* chore: handled the cycle date time using project timezone

* chore: reverted the frontend commit
2024-12-12 14:11:12 +05:30
Prateek Shourya
38e8a5c807 fix: command palette build (#6186) 2024-12-11 18:19:09 +05:30
Prateek Shourya
a9bd2e243a refactor: enhance command palette modularity (#6139)
* refactor: enhance command palette modularity

* chore: minor updates to command palette store
2024-12-11 18:02:58 +05:30
Vamsi Krishna
ca0d50b229 fix: no activity while moving inbox issues (#6185) 2024-12-11 17:57:27 +05:30
Vamsi Krishna
7fca7fd86c [WEB-2774] fix:favorites reorder (#6179)
* fix:favorites reorder

* chore: added error handling

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2024-12-11 16:29:39 +05:30
Prateek Shourya
0ac68f2731 improvement: refactored issue grouping logic to access MobX store directly (#6134)
* improvement: refactored issue grouping logic to access MobX store directly

* chore: minor updates
2024-12-11 15:14:15 +05:30
rahulramesha
5a9ae66680 chore: Remove shouldIgnoreDependencies flags while dragging in timeline view (#6150)
* remove shouldEnable dependency flags for timeline view

* chore: error handling

---------

Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com>
2024-12-11 13:43:48 +05:30
Vamsi Krishna
134644fdf1 [WEB-2382]chore:notification files restructuring (#6181)
* chore: adjusted  increment/decrement  for unread count

* chore: improved param handling for unread notification count function

* chore:file restructuring

* fix:notification types

* chore:file restructuring

* chore:modified notfication types

* chore: modified types for notification

* chore:removed redundant checks for id
2024-12-11 13:41:19 +05:30
sriram veeraghanta
d0f3987aeb fix: instance changelog url updated 2024-12-10 21:03:44 +05:30
sriram veeraghanta
f06b1b8c4a fix: updated package version 2024-12-10 21:02:29 +05:30
sriram veeraghanta
6e56ea4c60 fix: updated changelog url in apiserver 2024-12-10 20:28:51 +05:30
Anmol Singh Bhatia
216a69f991 chore: workspace draft and inbox issue local db mutation (#6180) 2024-12-10 19:12:24 +05:30
Vihar Kurama
205395e079 fix: changed checkboxes to toggles on notifications settings page (#6175) 2024-12-10 01:02:34 +05:30
Bavisetti Narayan
ff8bbed6f9 chore: changed the soft deletion logic (#6171) 2024-12-09 20:29:30 +05:30
Vamsi Krishna
d04619477b [WEB-2382]chore: notifications code improvement (#6172)
* chore: adjusted  increment/decrement  for unread count

* chore: improved param handling for unread notification count function
2024-12-09 18:06:56 +05:30
sriram veeraghanta
547c138084 fix: ui package module resolution 2024-12-09 15:56:20 +05:30
Anmol Singh Bhatia
5c907db0e2 [WEB-2818] chore: project navigation items code refactor (#6170)
* chore: project navigation items code refactor

* fix: build error

* chore: code refactor

* chore: code refactor
2024-12-09 14:37:04 +05:30
Aaryan Khandelwal
a85e592ada fix: creating a new sub-issue from workspace level (#6169) 2024-12-09 12:15:10 +05:30
sriram veeraghanta
b21d190ce0 fix: added github pull request template 2024-12-09 02:55:09 +05:30
sriram veeraghanta
cba41e0755 fix: upgrading the express version 2024-12-09 02:35:48 +05:30
sriram veeraghanta
02308eeb15 fix: django version upgrade 2024-12-09 02:28:06 +05:30
guru_sainath
9ee41ece98 fix: email check validation to handle case in-sensitive email (#6168) 2024-12-07 17:55:50 +05:30
Vamsi Krishna
666ddf73b6 [WEB-2382]chore:notification snooze modal (#6164)
* modified notification store

* notification snooze types fix

* handled promise

* modified notifications layout

* incresed pagination count for notifications
2024-12-06 16:27:45 +05:30
Satish Gandham
4499a5fa25 Sync issues and workspace data when the issue properties like labels/modules/cycles etc are deleted from the project (#6165) 2024-12-06 16:27:07 +05:30
sriram veeraghanta
727dd4002e fix: updated lint command in packages 2024-12-06 15:00:11 +05:30
sriram veeraghanta
4b5a2bc4e5 chore: lint related changes and packaging fixes (#6163)
* fix: lint related changes and packaging fixes

* adding color validations
2024-12-06 14:56:49 +05:30
sriram veeraghanta
b1c340b199 fix: build branch workflow upload artifacts 2024-12-05 16:51:20 +05:30
rahulramesha
a612a17d28 chore remove unnecessary CTA (#6161) 2024-12-05 16:37:55 +05:30
Prateek Shourya
d55ee6d5b8 fix: remove unwanted states fetching logic to avoid multiple API calls. (#6158) 2024-12-05 15:26:35 +05:30
Prateek Shourya
aa1e192a50 improvement: update fetch map during workspace-level module fetch to reduce redundant API calls (#6159) 2024-12-05 15:26:15 +05:30
guru_sainath
6cd8af1092 chore: updated powered by (#6160) 2024-12-05 15:12:37 +05:30
rahulramesha
66652a5d71 refactor project states to ake way for new features (#6156) 2024-12-05 12:46:51 +05:30
sriram veeraghanta
3bccda0c86 chore: formatting and typo fixes 2024-12-04 19:40:37 +05:30
sriram veeraghanta
fb3295f5f4 fix: sites opengraph title and description added 2024-12-04 17:58:23 +05:30
sriram veeraghanta
fa3aa362a9 fix: lint errors 2024-12-04 17:22:41 +05:30
Bavisetti Narayan
b73ea37798 chore: improve the cascading logic (#6152) 2024-12-04 16:15:57 +05:30
Vamsi Krishna
d537e560e3 [WEB-2802]fix: dorpdown visibility issue in safari (#6151)
* filters drop down fix safari

* added comments for translation

* fixed drop down visibility issue
2024-12-04 15:27:34 +05:30
guru_sainath
1b92a18ef8 chore: updated the ssr rendering on sites (#6145)
* fix: refactoring

* fix: site ssr implementation

* chore: fixed auto reload on file change in sites

* chore: updated constant imports and globalised powerBy component

* chore: resolved lint and updated the env

---------

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
2024-12-04 14:24:53 +05:30
rahulramesha
31b6d52417 fix root issue store to have updated url params at all times (#6147) 2024-12-04 13:57:33 +05:30
Vamsi Krishna
a153de34d6 fixed piority icons shape (#6144) 2024-12-04 13:57:14 +05:30
Aaryan Khandelwal
64a44f4fce style: add custom class to editor paragraph and heading blocks (#6143) 2024-12-04 13:43:52 +05:30
guru_sainath
bb8a156bdd fix: removed changelog endpoint (#6146) 2024-12-04 13:42:15 +05:30
Akshita Goyal
f02a2b04a5 fix: export btn overlap issue (#6149) 2024-12-04 13:41:48 +05:30
Bavisetti Narayan
b6ab853c57 chore: filter out the removed cycle from issue detail (#6138) 2024-12-03 16:48:14 +05:30
Aaryan Khandelwal
fe43300aa7 fix: pages empty state authorization (#6141) 2024-12-03 14:53:02 +05:30
Prateek Shourya
849d9891d2 chore: community edition product updates link (#6132)
* chore: community edition product updates link

* fix: iframe embed for changelog

---------

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
2024-12-03 13:28:28 +05:30
Vamsi Krishna
2768f560ad [WEB-2802]fix:filters drop down fix safari (#6133)
* filters drop down fix safari

* added comments for translation
2024-12-03 12:51:39 +05:30
Anmol Singh Bhatia
fe5999ceff fix: intake issue permission (#6136) 2024-12-02 19:49:09 +05:30
rahulramesha
da0071256f fix half block dragging (#6135) 2024-12-02 19:30:58 +05:30
M. Palanikannan
3c6006d04a [PE-31] feat: Add lock unlock archive restore realtime sync (#5629)
* fix: add lock unlock archive restore realtime sync

* fix: show only after editor loads

* fix: added strong types

* fix: live events fixed

* fix: remove unused vars and logs

* fix: converted objects to enum

* fix: error handling and removing the events in read only mode

* fix: added check to only update if the image aspect ratio is not present already

* fix: imports

* fix: props order

* revert: no need of these changes anymore

* fix: updated type names

* fix: order of things

* fix: fixed types and renamed variables

* fix: better typing for the real time updates

* fix: trying multiplexing our socket connection

* fix: multiplexing socket connection in read only editor as well

* fix: remove single socket logic

* fix: fixing the cleanup deps for the provider and localprovider

* fix: add a better data structure for managing events

* chore: refactored realtime events into hooks

* feat: fetch page meta while focusing tabs

* fix: cycling through items on slash command item in down arrow

* fix: better naming convention for realtime events

* fix: simplified localprovider initialization and cleaning

* fix: types from ui

* fix: abstracted away from exposing the provider directly

* fix: coderabbit suggestions

* regression: pass user in dependency array

* fix: removed page action api calls by the other users the document is synced with

* chore: removed unused imports
2024-12-02 14:26:36 +05:30
Aaryan Khandelwal
8c04aa6f51 dev: revamp pages authorization (#6094) 2024-12-02 13:59:01 +05:30
Aaryan Khandelwal
9f14167ef5 refactor: editor code splitting (#6102)
* fix: merge conflicts resolved from preview

* fix: space app build errors

* fix: product updates modal

* fix: build errors

* fix: lite text read only editor

* refactor: additional options push logic
2024-12-02 13:51:27 +05:30
Aaryan Khandelwal
11bfbe560a fix: checked colored todo list item (#6113) 2024-12-02 13:47:50 +05:30
Aaryan Khandelwal
fc52936024 fix: escape markdown content for images (#6096) 2024-12-02 13:36:12 +05:30
Vamsi Krishna
5150c661ab reduced the components moved (#6110) 2024-12-02 13:35:40 +05:30
Vamsi Krishna
63bc01f385 [WEB-2774]fix:reordering favorites and favorite folders (#6119)
* fixed re order for favorites

* fixed lint errors

* added reorder

* fixed reorder inside folder

* fixed lint issues

* memoized reorder

* removed unnecessary comments

* seprated duplicate logic to a common file

* removed code comments

* fixed favorite remove while reorder inside folder

* fixed folder remove while reorder inside folder

* fixed-reorder issue

* added last child to drop handled

* fixed orderby function

* removed unncessasary comments
2024-12-02 13:35:09 +05:30
Anmol Singh Bhatia
1953d6fe3a [WEB-2762] chore: loader code refactor (#5992)
* chore: loader code refactor

* chore: code refactor

* chore: code refactor

* chore: code refactor
2024-12-02 13:24:01 +05:30
Anmol Singh Bhatia
1b9033993d [WEB-2799] chore: global component and code refactor (#6131)
* chore: local storage helper hook added to package

* chore: tabs global component added

* chore: collapsible button improvement

* chore: linear progress indicator improvement

* chore: fill icon set added to package
2024-12-02 13:22:08 +05:30
sriram veeraghanta
75ada1bfac fix: constants package updates 2024-12-01 21:26:35 +05:30
Prateek Shourya
d0f9a4d245 chore: add redirection to plane logo in invitations page (#6125) 2024-11-29 20:20:49 +05:30
sriram veeraghanta
05894c5b9c Merge pull request #6121 from makeplane/preview
release: v0.24.0
2024-11-29 19:36:12 +05:30
Prateek Shourya
5926c9e8e9 fix: comment images in profile activity page (#6123) 2024-11-29 19:20:31 +05:30
Prateek Shourya
5aeedd1e5a [WEB-2610] fix: workspace redirection from admin app (#6122) 2024-11-29 19:02:13 +05:30
sriram veeraghanta
7725b200f7 fix: changelog redirection 2024-11-29 18:13:29 +05:30
sriram veeraghanta
2c69538617 fix: hypermode text typo changes 2024-11-29 17:47:46 +05:30
pablohashescobar
41bd98dd63 fix: instance collect 2024-11-29 17:41:06 +05:30
sriram veeraghanta
bf1c326b44 Merge branch 'preview' of github.com:makeplane/plane into preview 2024-11-29 17:36:00 +05:30
sriram veeraghanta
3d1485461d fix: lockfile udpated 2024-11-29 17:35:47 +05:30
rahulramesha
4251b114c3 chore: enable no load by default (#5968)
* enable no load by default

* remove help section brackets

* fallback to server with mentions
2024-11-29 14:55:39 +05:30
Prateek Shourya
712339a638 minor improvements for workspace management (#6099)
* minor improvements for workspace management

* typo fix
2024-11-29 14:53:30 +05:30
sriram veeraghanta
1c9162e1f1 chore: turbo version upgrade 2024-11-29 14:40:14 +05:30
sriram veeraghanta
f1e6f59716 chore: package version updated 2024-11-29 14:37:53 +05:30
sriram veeraghanta
69f235ed24 fix: merge conflicts 2024-11-29 14:35:43 +05:30
Vamsi Krishna
4aa01ffebe [WEB-2795]chore:removed header links for project bread crumb inside project detail and list (#6116)
* removed header links for project bread crumb inside project detail

* Add total issue count while syncing project to telemetry

---------

Co-authored-by: Satish Gandham <satish.iitg@gmail.com>
2024-11-29 11:39:44 +05:30
Bavisetti Narayan
41c0ba502c fix: intake toggle (#6111) 2024-11-28 16:58:21 +05:30
Bavisetti Narayan
378e896bf0 fix: notification count (#6109) 2024-11-28 12:58:09 +05:30
Prateek Shourya
e3799c8a40 fix: add back issue identifier for relation activity. (#6106) 2024-11-28 12:50:56 +05:30
sriram veeraghanta
0d70397639 chore: issue version migrations updates 2024-11-28 12:42:30 +05:30
sriram veeraghanta
d2758fe5e6 Revert "fix: refactor editor extensions code spliting"
This reverts commit 234513278f.
2024-11-27 18:20:41 +05:30
Bavisetti Narayan
1420b7e7d3 chore: restrict email notifications for removed users (#6100) 2024-11-27 15:06:55 +05:30
Prateek Shourya
05d3e3ae45 feat: workspace management from admin app (#6093)
* feat: workspace management from admin app

* chore: UI and UX copy improvements

* chore: ux copy improvements
2024-11-26 23:57:41 +05:30
Prateek Shourya
9dbb2b26c3 fix: issue activity sort order componenet import (#6098) 2024-11-26 20:49:39 +05:30
Vamsi Krishna
fa2e60101f [WEB-2774] Chore: re-ordering functionality for entities in favorites. (#6078)
* fixed re order for favorites

* fixed lint errors

* added reorder

* fixed reorder inside folder

* fixed lint issues

* memoized reorder

* removed unnecessary comments

* seprated duplicate logic to a common file

* removed code comments
2024-11-26 19:15:21 +05:30
Satish Gandham
6376a09318 - Change batch size to 50 for inserting issues (#6085)
- Fallback to server when mentions filter is used
- Split load workspace into multiple transactions
2024-11-26 19:12:39 +05:30
Vamsi Krishna
32048be26f [WEB-2432]fix: project not found state and error page alignment (#6095)
* fixed error page alignment and projects empty page

* spelling corrected

* spelling corrected
2024-11-26 19:11:35 +05:30
Vamsi Krishna
f09e37fed8 [WEB - 2779] feat: Added sort order for issue activity (#6087)
* added sort order for issue activity

* fixed invalid date generation issue

* fixed lint errors, optimized code
2024-11-26 18:58:01 +05:30
sriram veeraghanta
31c761db25 fix: nivo charts update fixes (#6080) 2024-11-26 18:52:42 +05:30
Aaryan Khandelwal
f7b2cee418 fix: misalignment of swimlanes group header (#6077) 2024-11-26 18:51:46 +05:30
Vamsi Krishna
1d9b02b085 [WEB-2724] fix: custom properties issue while moving to project (#6090)
* fixed custom properties adding issue

* added error handling to function
2024-11-26 18:50:28 +05:30
sriram veeraghanta
84c5e70181 chore: upgrade turbo repo version 2024-11-26 18:14:28 +05:30
sriram veeraghanta
234513278f fix: refactor editor extensions code spliting 2024-11-26 18:08:32 +05:30
Nikhil
76fe136d85 fix: project join for admin and members (#6097)
* chore: add enum role comparison

* chore: add member also to join a project
2024-11-26 16:58:41 +05:30
sriram veeraghanta
c4a5c5973f fix: tracer error handling 2024-11-26 15:30:53 +05:30
sriram veeraghanta
89819a9473 fix: workflow fixes 2024-11-26 15:13:58 +05:30
sriram veeraghanta
182aa58f6c fix: tracer init fixes 2024-11-26 15:11:54 +05:30
Anmol Singh Bhatia
7469e67b71 fix: project view application error (#6091) 2024-11-25 20:05:03 +05:30
sriram veeraghanta
1cb16bf176 fix: email error handling on magic auth 2024-11-25 15:02:50 +05:30
Bavisetti Narayan
ca88675dbf chore: added dates in issue export (#6088)
* chore: added dates in issue export

* chore: added date converter
2024-11-22 19:59:08 +05:30
Nikhil
86f8743ade chore: remove exists checks (#6086) 2024-11-22 17:00:20 +05:30
Nikhil
1a6ec7034a chore: management command to add user to a project (#6084) 2024-11-22 16:05:58 +05:30
Bavisetti Narayan
42d6078f60 [WEB-2776] fix: restrict notifications (#6081)
* chore: restrict notifications

* chore: handled the issue filter duplicates

---------

Co-authored-by: gurusainath <gurusainath007@gmail.com>
2024-11-22 16:02:11 +05:30
Bavisetti Narayan
6ef62820fa [WEB-2778] chore: private project join restriction (#6082)
* chore: private project join restriction

* chore: update project not found container layout

* chore: restrict other users to join private project

* chore: add check condition using enum

---------

Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
2024-11-22 16:00:19 +05:30
sriram veeraghanta
b72d18079f fix: adding start and target date in issue exporter 2024-11-21 19:24:39 +05:30
sriram veeraghanta
a42c69f619 chore: pyporject toml changes 2024-11-21 18:00:02 +05:30
sriram veeraghanta
0dbd4cfe97 chore: formatting changes 2024-11-21 17:42:44 +05:30
Anmol Singh Bhatia
a446bc043e [WEB-2765] fix: issue detail page unnecessary scroll (#6068)
* fix: issue dertail page unnecessary scroll

* fix: issue detail sidebar ui
2024-11-21 15:16:47 +05:30
sriram veeraghanta
daed58be0f fix: adding new restricted workspace slugs 2024-11-20 20:36:53 +05:30
M. Palanikannan
c68658d877 [PE-56] fix: image aspect ratio (#5794)
* regression: image aspect ratio fix

* fix: name of variables changed for clarity
2024-10-10 20:53:20 +05:30
1376 changed files with 31896 additions and 19905 deletions

View File

@@ -1,126 +0,0 @@
name: "Build and Push Docker Image"
description: "Reusable action for building and pushing Docker images"
inputs:
docker-username:
description: "The Dockerhub username"
required: true
docker-token:
description: "The Dockerhub Token"
required: true
# Docker Image Options
docker-image-owner:
description: "The owner of the Docker image"
required: true
docker-image-name:
description: "The name of the Docker image"
required: true
build-context:
description: "The build context"
required: true
default: "."
dockerfile-path:
description: "The path to the Dockerfile"
required: true
build-args:
description: "The build arguments"
required: false
default: ""
# Buildx Options
buildx-driver:
description: "Buildx driver"
required: true
default: "docker-container"
buildx-version:
description: "Buildx version"
required: true
default: "latest"
buildx-platforms:
description: "Buildx platforms"
required: true
default: "linux/amd64"
buildx-endpoint:
description: "Buildx endpoint"
required: true
default: "default"
# Release Build Options
build-release:
description: "Flag to publish release"
required: false
default: "false"
build-prerelease:
description: "Flag to publish prerelease"
required: false
default: "false"
release-version:
description: "The release version"
required: false
default: "latest"
runs:
using: "composite"
steps:
- name: Set Docker Tag
shell: bash
env:
IMG_OWNER: ${{ inputs.docker-image-owner }}
IMG_NAME: ${{ inputs.docker-image-name }}
BUILD_RELEASE: ${{ inputs.build-release }}
IS_PRERELEASE: ${{ inputs.build-prerelease }}
REL_VERSION: ${{ inputs.release-version }}
run: |
FLAT_BRANCH_VERSION=$(echo "${{ github.ref_name }}" | sed 's/[^a-zA-Z0-9.-]//g')
if [ "${{ env.BUILD_RELEASE }}" == "true" ]; then
semver_regex="^v([0-9]+)\.([0-9]+)\.([0-9]+)(-[a-zA-Z0-9]+(-[a-zA-Z0-9]+)*)?$"
if [[ ! ${{ env.REL_VERSION }} =~ $semver_regex ]]; then
echo "Invalid Release Version Format : ${{ env.REL_VERSION }}"
echo "Please provide a valid SemVer version"
echo "e.g. v1.2.3 or v1.2.3-alpha-1"
echo "Exiting the build process"
exit 1 # Exit with status 1 to fail the step
fi
TAG=${{ env.IMG_OWNER }}/${{ env.IMG_NAME }}:${{ env.REL_VERSION }}
if [ "${{ env.IS_PRERELEASE }}" != "true" ]; then
TAG=${TAG},${{ env.IMG_OWNER }}/${{ env.IMG_NAME }}:stable
fi
elif [ "${{ env.TARGET_BRANCH }}" == "master" ]; then
TAG=${{ env.IMG_OWNER }}/${{ env.IMG_NAME }}:latest
else
TAG=${{ env.IMG_OWNER }}/${{ env.IMG_NAME }}:${FLAT_BRANCH_VERSION}
fi
echo "DOCKER_TAGS=${TAG}" >> $GITHUB_ENV
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ inputs.docker-username }}
password: ${{ inputs.docker-token}}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
driver: ${{ inputs.buildx-driver }}
version: ${{ inputs.buildx-version }}
endpoint: ${{ inputs.buildx-endpoint }}
- name: Check out the repo
uses: actions/checkout@v4
- name: Build and Push Docker Image
uses: docker/build-push-action@v5.1.0
with:
context: ${{ inputs.build-context }}
file: ${{ inputs.dockerfile-path }}
platforms: ${{ inputs.buildx-platforms }}
tags: ${{ env.DOCKER_TAGS }}
push: true
build-args: ${{ inputs.build-args }}
env:
DOCKER_BUILDKIT: 1
DOCKER_USERNAME: ${{ inputs.docker-username }}
DOCKER_PASSWORD: ${{ inputs.docker-token }}

20
.github/pull_request_template.md vendored Normal file
View File

@@ -0,0 +1,20 @@
### Description
<!-- Provide a detailed description of the changes in this PR -->
### Type of Change
<!-- Put an 'x' in the boxes that apply -->
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] Feature (non-breaking change which adds functionality)
- [ ] Improvement (change that would cause existing functionality to not work as expected)
- [ ] Code refactoring
- [ ] Performance improvements
- [ ] Documentation update
### Screenshots and Media (if applicable)
<!-- Add screenshots to help explain your changes, ideally showcasing before and after -->
### Test Scenarios
<!-- Please describe the tests that you ran to verify your changes -->
### References
<!-- Link related issues if there are any -->

View File

@@ -25,9 +25,6 @@ on:
required: false
default: false
type: boolean
# push:
# branches:
# - master
env:
TARGET_BRANCH: ${{ github.ref_name }}
@@ -39,7 +36,7 @@ env:
jobs:
branch_build_setup:
name: Build Setup
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
outputs:
gh_branch_name: ${{ steps.set_env_variables.outputs.TARGET_BRANCH }}
gh_buildx_driver: ${{ steps.set_env_variables.outputs.BUILDX_DRIVER }}
@@ -163,20 +160,17 @@ jobs:
branch_build_push_admin:
if: ${{ needs.branch_build_setup.outputs.build_admin == 'true' || github.event_name == 'workflow_dispatch' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
name: Build-Push Admin Docker Image
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
needs: [branch_build_setup]
steps:
- id: checkout_files
name: Checkout Files
uses: actions/checkout@v4
- name: Admin Build and Push
uses: ./.github/actions/build-push-ce
uses: makeplane/actions/build-push@v1.0.0
with:
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
release-version: ${{ needs.branch_build_setup.outputs.release_version }}
docker-username: ${{ secrets.DOCKERHUB_USERNAME }}
docker-token: ${{ secrets.DOCKERHUB_TOKEN }}
dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }}
dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }}
docker-image-owner: makeplane
docker-image-name: ${{ needs.branch_build_setup.outputs.dh_img_admin }}
build-context: .
@@ -189,20 +183,17 @@ jobs:
branch_build_push_web:
if: ${{ needs.branch_build_setup.outputs.build_web == 'true' || github.event_name == 'workflow_dispatch' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
name: Build-Push Web Docker Image
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
needs: [branch_build_setup]
steps:
- id: checkout_files
name: Checkout Files
uses: actions/checkout@v4
- name: Web Build and Push
uses: ./.github/actions/build-push-ce
uses: makeplane/actions/build-push@v1.0.0
with:
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
release-version: ${{ needs.branch_build_setup.outputs.release_version }}
docker-username: ${{ secrets.DOCKERHUB_USERNAME }}
docker-token: ${{ secrets.DOCKERHUB_TOKEN }}
dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }}
dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }}
docker-image-owner: makeplane
docker-image-name: ${{ needs.branch_build_setup.outputs.dh_img_web }}
build-context: .
@@ -215,20 +206,17 @@ jobs:
branch_build_push_space:
if: ${{ needs.branch_build_setup.outputs.build_space == 'true' || github.event_name == 'workflow_dispatch' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
name: Build-Push Space Docker Image
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
needs: [branch_build_setup]
steps:
- id: checkout_files
name: Checkout Files
uses: actions/checkout@v4
- name: Space Build and Push
uses: ./.github/actions/build-push-ce
uses: makeplane/actions/build-push@v1.0.0
with:
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
release-version: ${{ needs.branch_build_setup.outputs.release_version }}
docker-username: ${{ secrets.DOCKERHUB_USERNAME }}
docker-token: ${{ secrets.DOCKERHUB_TOKEN }}
dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }}
dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }}
docker-image-owner: makeplane
docker-image-name: ${{ needs.branch_build_setup.outputs.dh_img_space }}
build-context: .
@@ -241,20 +229,17 @@ jobs:
branch_build_push_live:
if: ${{ needs.branch_build_setup.outputs.build_live == 'true' || github.event_name == 'workflow_dispatch' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
name: Build-Push Live Collaboration Docker Image
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
needs: [branch_build_setup]
steps:
- id: checkout_files
name: Checkout Files
uses: actions/checkout@v4
- name: Live Build and Push
uses: ./.github/actions/build-push-ce
uses: makeplane/actions/build-push@v1.0.0
with:
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
release-version: ${{ needs.branch_build_setup.outputs.release_version }}
docker-username: ${{ secrets.DOCKERHUB_USERNAME }}
docker-token: ${{ secrets.DOCKERHUB_TOKEN }}
dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }}
dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }}
docker-image-owner: makeplane
docker-image-name: ${{ needs.branch_build_setup.outputs.dh_img_live }}
build-context: .
@@ -267,20 +252,17 @@ jobs:
branch_build_push_apiserver:
if: ${{ needs.branch_build_setup.outputs.build_apiserver == 'true' || github.event_name == 'workflow_dispatch' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
name: Build-Push API Server Docker Image
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
needs: [branch_build_setup]
steps:
- id: checkout_files
name: Checkout Files
uses: actions/checkout@v4
- name: Backend Build and Push
uses: ./.github/actions/build-push-ce
uses: makeplane/actions/build-push@v1.0.0
with:
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
release-version: ${{ needs.branch_build_setup.outputs.release_version }}
docker-username: ${{ secrets.DOCKERHUB_USERNAME }}
docker-token: ${{ secrets.DOCKERHUB_TOKEN }}
dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }}
dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }}
docker-image-owner: makeplane
docker-image-name: ${{ needs.branch_build_setup.outputs.dh_img_backend }}
build-context: ./apiserver
@@ -293,20 +275,17 @@ jobs:
branch_build_push_proxy:
if: ${{ needs.branch_build_setup.outputs.build_proxy == 'true' || github.event_name == 'workflow_dispatch' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
name: Build-Push Proxy Docker Image
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
needs: [branch_build_setup]
steps:
- id: checkout_files
name: Checkout Files
uses: actions/checkout@v4
- name: Proxy Build and Push
uses: ./.github/actions/build-push-ce
uses: makeplane/actions/build-push@v1.0.0
with:
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
release-version: ${{ needs.branch_build_setup.outputs.release_version }}
docker-username: ${{ secrets.DOCKERHUB_USERNAME }}
docker-token: ${{ secrets.DOCKERHUB_TOKEN }}
dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }}
dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }}
docker-image-owner: makeplane
docker-image-name: ${{ needs.branch_build_setup.outputs.dh_img_proxy }}
build-context: ./nginx
@@ -317,9 +296,9 @@ jobs:
buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
attach_assets_to_build:
if: ${{ needs.branch_build_setup.outputs.build_type == 'Build' }}
name: Attach Assets to Build
runs-on: ubuntu-20.04
if: ${{ needs.branch_build_setup.outputs.build_type == 'Release' }}
name: Attach Assets to Release
runs-on: ubuntu-22.04
needs: [branch_build_setup]
steps:
- name: Checkout
@@ -344,7 +323,7 @@ jobs:
publish_release:
if: ${{ needs.branch_build_setup.outputs.build_type == 'Release' }}
name: Build Release
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
needs:
[
branch_build_setup,
@@ -354,6 +333,7 @@ jobs:
branch_build_push_live,
branch_build_push_apiserver,
branch_build_push_proxy,
attach_assets_to_build,
]
env:
REL_VERSION: ${{ needs.branch_build_setup.outputs.release_version }}

123
README.md
View File

@@ -5,9 +5,7 @@
<img src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-readme/plane_logo_.webp" alt="Plane Logo" width="70">
</a>
</p>
<h3 align="center"><b>Plane</b></h3>
<p align="center"><b>Open-source project management that unlocks customer value</b></p>
<h1 align="center"><b>Plane</b></h1>
<p align="center">
<a href="https://discord.com/invite/A92xrEGCge">
@@ -44,79 +42,85 @@ Meet [Plane](https://dub.sh/plane-website-readme), an open-source project manage
> Plane is evolving every day. Your suggestions, ideas, and reported bugs help us immensely. Do not hesitate to join in the conversation on [Discord](https://discord.com/invite/A92xrEGCge) or raise a GitHub issue. We read everything and respond to most.
## Installation
## 🚀 Installation
The easiest way to get started with Plane is by creating a [Plane Cloud](https://app.plane.so) account.
Getting started with Plane is simple. Choose the setup that works best for you:
If you would like to self-host Plane, please see our [deployment guide](https://docs.plane.so/docker-compose).
- **Plane Cloud**
Sign up for a free account on [Plane Cloud](https://app.plane.so)—it's the fastest way to get up and running without worrying about infrastructure.
- **Self-host Plane**
Prefer full control over your data and infrastructure? Install and run Plane on your own servers. Follow our detailed [deployment guides](https://developers.plane.so/self-hosting/overview) to get started.
| Installation methods | Docs link |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Docker | [![Docker](https://img.shields.io/badge/docker-%230db7ed.svg?style=for-the-badge&logo=docker&logoColor=white)](https://docs.plane.so/self-hosting/methods/docker-compose) |
| Kubernetes | [![Kubernetes](https://img.shields.io/badge/kubernetes-%23326ce5.svg?style=for-the-badge&logo=kubernetes&logoColor=white)](https://docs.plane.so/kubernetes) |
| Docker | [![Docker](https://img.shields.io/badge/docker-%230db7ed.svg?style=for-the-badge&logo=docker&logoColor=white)](https://developers.plane.so/self-hosting/methods/docker-compose) |
| Kubernetes | [![Kubernetes](https://img.shields.io/badge/kubernetes-%23326ce5.svg?style=for-the-badge&logo=kubernetes&logoColor=white)](https://developers.plane.so/self-hosting/methods/kubernetes) |
`Instance admins` can configure instance settings with [God-mode](https://docs.plane.so/instance-admin).
`Instance admins` can manage and customize settings using [God mode](https://developers.plane.so/self-hosting/govern/instance-admin).
## 🚀 Features
## 🌟 Features
- **Issues**: Quickly create issues and add details using a powerful rich text editor that supports file uploads. Add sub-properties and references to problems for better organization and tracking.
- **Issues**
Efficiently create and manage tasks with a robust rich text editor that supports file uploads. Enhance organization and tracking by adding sub-properties and referencing related issues.
- **Cycles**:
Keep up your team's momentum with Cycles. Gain insights into your project's progress with burn-down charts and other valuable features.
- **Cycles**
Maintain your teams momentum with Cycles. Track progress effortlessly using burn-down charts and other insightful tools.
- **Modules**: Break down your large projects into smaller, more manageable modules. Assign modules between teams to track and plan your project's progress easily.
- **Modules**
Simplify complex projects by dividing them into smaller, manageable modules.
- **Views**: Create custom filters to display only the issues that matter to you. Save and share your filters in just a few clicks.
- **Views**
Customize your workflow by creating filters to display only the most relevant issues. Save and share these views with ease.
- **Pages**: Plane pages, equipped with AI and a rich text editor, let you jot down your thoughts on the fly. Format your text, upload images, hyperlink, or sync your existing ideas into an actionable item or issue.
- **Pages**
Capture and organize ideas using Plane Pages, complete with AI capabilities and a rich text editor. Format text, insert images, add hyperlinks, or convert your notes into actionable items.
- **Analytics**: Get insights into all your Plane data in real-time. Visualize issue data to spot trends, remove blockers, and progress your work.
- **Analytics**
Access real-time insights across all your Plane data. Visualize trends, remove blockers, and keep your projects moving forward.
- **Drive** (_coming soon_): The drive helps you share documents, images, videos, or any other files that make sense to you or your team and align on the problem/solution.
## 🛠️ Quick start for contributors
> Development system must have docker engine installed and running.
## 🛠️ Local development
Setting up local environment is extremely easy and straight forward. Follow the below step and you will be ready to contribute -
### Pre-requisites
- Ensure Docker Engine is installed and running.
1. Clone the code locally using:
### Development setup
Setting up your local environment is simple and straightforward. Follow these steps to get started:
1. Clone the repository:
```
git clone https://github.com/makeplane/plane.git
```
2. Switch to the code folder:
2. Navigate to the project folder:
```
cd plane
```
3. Create your feature or fix branch you plan to work on using:
3. Create a new branch for your feature or fix:
```
git checkout -b <feature-branch-name>
```
4. Open terminal and run:
4. Run the setup script in the terminal:
```
./setup.sh
```
5. Open the code on VSCode or similar equivalent IDE.
6. Review the `.env` files available in various folders.
Visit [Environment Setup](./ENV_SETUP.md) to know about various environment variables used in system.
7. Run the docker command to initiate services:
5. Open the project in an IDE such as VS Code.
6. Review the `.env` files in the relevant folders. Refer to [Environment Setup](./ENV_SETUP.md) for details on the environment variables used.
7. Start the services using Docker:
```
docker compose -f docker-compose-local.yml up -d
```
You are ready to make changes to the code. Do not forget to refresh the browser (in case it does not auto-reload).
Thats it! Youre all set to begin coding. Remember to refresh your browser if changes dont auto-reload. Happy contributing! 🎉
Thats it!
## ❤️ Community
The Plane community can be found on [GitHub Discussions](https://github.com/orgs/makeplane/discussions), and our [Discord server](https://discord.com/invite/A92xrEGCge). Our [Code of conduct](https://github.com/makeplane/plane/blob/master/CODE_OF_CONDUCT.md) applies to all Plane community chanels.
Ask questions, report bugs, join discussions, voice ideas, make feature requests, or share your projects.
### Repo Activity
![Plane Repo Activity](https://repobeats.axiom.co/api/embed/2523c6ed2f77c082b7908c33e2ab208981d76c39.svg "Repobeats analytics image")
## Built with
[![Next JS](https://img.shields.io/badge/next.js-000000?style=for-the-badge&logo=nextdotjs&logoColor=white)](https://nextjs.org/)<br/>
[![Django](https://img.shields.io/badge/Django-092E20?style=for-the-badge&logo=django&logoColor=green)](https://www.djangoproject.com/)<br/>
[![Node JS](https://img.shields.io/badge/node.js-339933?style=for-the-badge&logo=Node.js&logoColor=white)](https://nodejs.org/en)
## 📸 Screenshots
@@ -165,7 +169,7 @@ Ask questions, report bugs, join discussions, voice ideas, make feature requests
</a>
</p>
</p>
<p>
<p>
<a href="https://plane.so" target="_blank">
<img
src="https://ik.imagekit.io/w2okwbtu2/Drive_LlfeY4xn3.png?updatedAt=1709298837917"
@@ -176,23 +180,42 @@ Ask questions, report bugs, join discussions, voice ideas, make feature requests
</p>
</p>
## ⛓️ Security
## 📝 Documentation
Explore Plane's [product documentation](https://docs.plane.so/) and [developer documentation](https://developers.plane.so/) to learn about features, setup, and usage.
If you believe you have found a security vulnerability in Plane, we encourage you to responsibly disclose this and not open a public issue. We will investigate all legitimate reports.
## ❤️ Community
Email squawk@plane.so to disclose any security vulnerabilities.
Join the Plane community on [GitHub Discussions](https://github.com/orgs/makeplane/discussions) and our [Discord server](https://discord.com/invite/A92xrEGCge). We follow a [Code of conduct](https://github.com/makeplane/plane/blob/master/CODE_OF_CONDUCT.md) in all our community channels.
## ❤️ Contribute
Feel free to ask questions, report bugs, participate in discussions, share ideas, request features, or showcase your projects. Wed love to hear from you!
There are many ways to contribute to Plane, including:
## 🛡️ Security
- Submitting [bugs](https://github.com/makeplane/plane/issues/new?assignees=srinivaspendem%2Cpushya22&labels=%F0%9F%90%9Bbug&projects=&template=--bug-report.yaml&title=%5Bbug%5D%3A+) and [feature requests](https://github.com/makeplane/plane/issues/new?assignees=srinivaspendem%2Cpushya22&labels=%E2%9C%A8feature&projects=&template=--feature-request.yaml&title=%5Bfeature%5D%3A+) for various components.
- Reviewing [the documentation](https://docs.plane.so/) and submitting [pull requests](https://github.com/makeplane/plane), from fixing typos to adding new features.
- Speaking or writing about Plane or any other ecosystem integration and [letting us know](https://discord.com/invite/A92xrEGCge)!
- Upvoting [popular feature requests](https://github.com/makeplane/plane/issues) to show your support.
If you discover a security vulnerability in Plane, please report it responsibly instead of opening a public issue. We take all legitimate reports seriously and will investigate them promptly. See [Security policy](https://github.com/makeplane/plane/blob/master/SECURITY.md) for more info.
To disclose any security issues, please email us at security@plane.so.
## 🤝 Contributing
There are many ways you can contribute to Plane:
- Report [bugs](https://github.com/makeplane/plane/issues/new?assignees=srinivaspendem%2Cpushya22&labels=%F0%9F%90%9Bbug&projects=&template=--bug-report.yaml&title=%5Bbug%5D%3A+) or submit [feature requests](https://github.com/makeplane/plane/issues/new?assignees=srinivaspendem%2Cpushya22&labels=%E2%9C%A8feature&projects=&template=--feature-request.yaml&title=%5Bfeature%5D%3A+).
- Review the [documentation](https://docs.plane.so/) and submit [pull requests](https://github.com/makeplane/docs) to improve it—whether it's fixing typos or adding new content.
- Talk or write about Plane or any other ecosystem integration and [let us know](https://discord.com/invite/A92xrEGCge)!
- Show your support by upvoting [popular feature requests](https://github.com/makeplane/plane/issues).
Please read [CONTRIBUTING.md](https://github.com/makeplane/plane/blob/master/CONTRIBUTING.md) for details on the process for submitting pull requests to us.
### Repo activity
![Plane Repo Activity](https://repobeats.axiom.co/api/embed/2523c6ed2f77c082b7908c33e2ab208981d76c39.svg "Repobeats analytics image")
### We couldn't have done this without you.
<a href="https://github.com/makeplane/plane/graphs/contributors">
<img src="https://contrib.rocks/image?repo=makeplane/plane" />
</a>
## License
This project is licensed under the [GNU Affero General Public License v3.0](https://github.com/makeplane/plane/blob/master/LICENSE.txt).

View File

@@ -121,7 +121,12 @@ export const InstanceAIForm: FC<IInstanceAIForm> = (props) => {
<div className="relative inline-flex items-center gap-2 rounded border border-custom-primary-100/20 bg-custom-primary-100/10 px-4 py-2 text-xs text-custom-primary-200">
<Lightbulb height="14" width="14" />
<div>If you have a preferred AI models vendor, please get in touch with us.</div>
<div>
If you have a preferred AI models vendor, please get in{" "}
<a className="underline font-medium" href="https://plane.so/contact">
touch with us.
</a>
</div>
</div>
</div>
</div>

View File

@@ -4,10 +4,11 @@ import { FC, useState } from "react";
import isEmpty from "lodash/isEmpty";
import Link from "next/link";
import { useForm } from "react-hook-form";
// types
// plane internal packages
import { API_BASE_URL } from "@plane/constants";
import { IFormattedInstanceConfiguration, TInstanceGithubAuthenticationConfigurationKeys } from "@plane/types";
// ui
import { Button, TOAST_TYPE, getButtonStyling, setToast } from "@plane/ui";
import { cn } from "@plane/utils";
// components
import {
CodeBlock,
@@ -17,8 +18,6 @@ import {
TControllerInputFormField,
TCopyField,
} from "@/components/common";
// helpers
import { API_BASE_URL, cn } from "@/helpers/common.helper";
// hooks
import { useInstance } from "@/hooks/store";
@@ -103,8 +102,7 @@ export const InstanceGithubConfigForm: FC<Props> = (props) => {
url: originURL,
description: (
<>
We will auto-generate this. Paste this into the{" "}
<CodeBlock darkerShade>Authorized origin URL</CodeBlock> field{" "}
We will auto-generate this. Paste this into the <CodeBlock darkerShade>Authorized origin URL</CodeBlock> field{" "}
<a
tabIndex={-1}
href="https://github.com/settings/applications/new"
@@ -123,8 +121,8 @@ export const InstanceGithubConfigForm: FC<Props> = (props) => {
url: `${originURL}/auth/github/callback/`,
description: (
<>
We will auto-generate this. Paste this into your{" "}
<CodeBlock darkerShade>Authorized Callback URI</CodeBlock> field{" "}
We will auto-generate this. Paste this into your <CodeBlock darkerShade>Authorized Callback URI</CodeBlock>{" "}
field{" "}
<a
tabIndex={-1}
href="https://github.com/settings/applications/new"
@@ -195,7 +193,7 @@ export const InstanceGithubConfigForm: FC<Props> = (props) => {
</Button>
<Link
href="/authentication"
className={cn(getButtonStyling("link-neutral", "md"), "font-medium")}
className={cn(getButtonStyling("neutral-primary", "md"), "font-medium")}
onClick={handleGoBack}
>
Go back

View File

@@ -5,12 +5,12 @@ import { observer } from "mobx-react";
import Image from "next/image";
import { useTheme } from "next-themes";
import useSWR from "swr";
// plane internal packages
import { Loader, ToggleSwitch, setPromiseToast } from "@plane/ui";
import { resolveGeneralTheme } from "@plane/utils";
// components
import { AuthenticationMethodCard } from "@/components/authentication";
import { PageHeader } from "@/components/common";
// helpers
import { resolveGeneralTheme } from "@/helpers/common.helper";
// hooks
import { useInstance } from "@/hooks/store";
// icons
@@ -44,7 +44,7 @@ const InstanceGithubAuthenticationPage = observer(() => {
loading: "Saving Configuration...",
success: {
title: "Configuration saved",
message: () => `Github authentication is now ${value ? "active" : "disabled"}.`,
message: () => `GitHub authentication is now ${value ? "active" : "disabled"}.`,
},
error: {
title: "Error",
@@ -67,8 +67,8 @@ const InstanceGithubAuthenticationPage = observer(() => {
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col">
<div className="border-b border-custom-border-100 mx-4 py-4 space-y-1 flex-shrink-0">
<AuthenticationMethodCard
name="Github"
description="Allow members to login or sign up to plane with their Github accounts."
name="GitHub"
description="Allow members to login or sign up to plane with their GitHub accounts."
icon={
<Image
src={resolveGeneralTheme(resolvedTheme) === "dark" ? githubDarkModeImage : githubLightModeImage}

View File

@@ -2,10 +2,11 @@ import { FC, useState } from "react";
import isEmpty from "lodash/isEmpty";
import Link from "next/link";
import { useForm } from "react-hook-form";
// types
// plane internal packages
import { API_BASE_URL } from "@plane/constants";
import { IFormattedInstanceConfiguration, TInstanceGitlabAuthenticationConfigurationKeys } from "@plane/types";
// ui
import { Button, TOAST_TYPE, getButtonStyling, setToast } from "@plane/ui";
import { cn } from "@plane/utils";
// components
import {
CodeBlock,
@@ -15,8 +16,6 @@ import {
TControllerInputFormField,
TCopyField,
} from "@/components/common";
// helpers
import { API_BASE_URL, cn } from "@/helpers/common.helper";
// hooks
import { useInstance } from "@/hooks/store";
@@ -117,8 +116,7 @@ export const InstanceGitlabConfigForm: FC<Props> = (props) => {
url: `${originURL}/auth/gitlab/callback/`,
description: (
<>
We will auto-generate this. Paste this into the{" "}
<CodeBlock darkerShade>Redirect URI</CodeBlock> field of your{" "}
We will auto-generate this. Paste this into the <CodeBlock darkerShade>Redirect URI</CodeBlock> field of your{" "}
<a
tabIndex={-1}
href="https://docs.gitlab.com/ee/integration/oauth_provider.html"
@@ -191,7 +189,7 @@ export const InstanceGitlabConfigForm: FC<Props> = (props) => {
</Button>
<Link
href="/authentication"
className={cn(getButtonStyling("link-neutral", "md"), "font-medium")}
className={cn(getButtonStyling("neutral-primary", "md"), "font-medium")}
onClick={handleGoBack}
>
Go back

View File

@@ -3,10 +3,11 @@ import { FC, useState } from "react";
import isEmpty from "lodash/isEmpty";
import Link from "next/link";
import { useForm } from "react-hook-form";
// types
// plane internal packages
import { API_BASE_URL } from "@plane/constants";
import { IFormattedInstanceConfiguration, TInstanceGoogleAuthenticationConfigurationKeys } from "@plane/types";
// ui
import { Button, TOAST_TYPE, getButtonStyling, setToast } from "@plane/ui";
import { cn } from "@plane/utils";
// components
import {
CodeBlock,
@@ -16,8 +17,6 @@ import {
TControllerInputFormField,
TCopyField,
} from "@/components/common";
// helpers
import { API_BASE_URL, cn } from "@/helpers/common.helper";
// hooks
import { useInstance } from "@/hooks/store";
@@ -192,7 +191,7 @@ export const InstanceGoogleConfigForm: FC<Props> = (props) => {
</Button>
<Link
href="/authentication"
className={cn(getButtonStyling("link-neutral", "md"), "font-medium")}
className={cn(getButtonStyling("neutral-primary", "md"), "font-medium")}
onClick={handleGoBack}
>
Go back

View File

@@ -3,10 +3,10 @@
import { useState } from "react";
import { observer } from "mobx-react";
import useSWR from "swr";
// plane internal packages
import { TInstanceConfigurationKeys } from "@plane/types";
import { Loader, ToggleSwitch, setPromiseToast } from "@plane/ui";
// helpers
import { cn } from "@/helpers/common.helper";
import { cn } from "@plane/utils";
// hooks
import { useInstance } from "@/hooks/store";
// plane admin components
@@ -60,7 +60,7 @@ const InstanceAuthenticationPage = observer(() => {
<div className="border-b border-custom-border-100 mx-4 py-4 space-y-1 flex-shrink-0">
<div className="text-xl font-medium text-custom-text-100">Manage authentication modes for your instance</div>
<div className="text-sm font-normal text-custom-text-300">
Configure authentication modes for your team and restrict sign ups to be invite only.
Configure authentication modes for your team and restrict sign-ups to be invite only.
</div>
</div>
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4">
@@ -80,9 +80,11 @@ const InstanceAuthenticationPage = observer(() => {
<ToggleSwitch
value={Boolean(parseInt(enableSignUpConfig))}
onChange={() => {
Boolean(parseInt(enableSignUpConfig)) === true
? updateConfig("ENABLE_SIGNUP", "0")
: updateConfig("ENABLE_SIGNUP", "1");
if (Boolean(parseInt(enableSignUpConfig)) === true) {
updateConfig("ENABLE_SIGNUP", "0");
} else {
updateConfig("ENABLE_SIGNUP", "1");
}
}}
size="sm"
disabled={isSubmitting}
@@ -90,7 +92,7 @@ const InstanceAuthenticationPage = observer(() => {
</div>
</div>
</div>
<div className="text-lg font-medium pt-6">Authentication modes</div>
<div className="text-lg font-medium pt-6">Available authentication modes</div>
<AuthenticationModes disabled={isSubmitting} updateConfig={updateConfig} />
</div>
) : (

View File

@@ -72,7 +72,7 @@ export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
{
key: "EMAIL_FROM",
type: "text",
label: "Sender email address",
label: "Sender's email address",
description:
"This is the email address your users will see when getting emails from this instance. You will need to verify this address.",
placeholder: "no-reply@projectplane.so",
@@ -174,12 +174,12 @@ export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
</div>
</div>
<div className="flex flex-col gap-6 my-6 pt-4 border-t border-custom-border-100">
<div className="flex w-full max-w-md flex-col gap-y-10 px-1">
<div className="flex w-full max-w-xl flex-col gap-y-10 px-1">
<div className="mr-8 flex items-center gap-10 pt-4">
<div className="grow">
<div className="text-sm font-medium text-custom-text-100">Authentication (optional)</div>
<div className="text-sm font-medium text-custom-text-100">Authentication</div>
<div className="text-xs font-normal text-custom-text-300">
We recommend setting up a username password for your SMTP server
This is optional, but we recommend setting up a username and a password for your SMTP server.
</div>
</div>
</div>

View File

@@ -117,17 +117,18 @@ export const GeneralConfigurationForm: FC<IGeneralConfigurationForm> = observer(
</div>
<div className="grow">
<div className="text-sm font-medium text-custom-text-100 leading-5">
Allow Plane to collect anonymous usage events
Let Plane collect anonymous usage data
</div>
<div className="text-xs font-normal text-custom-text-300 leading-5">
We collect usage events without any PII to analyse and improve Plane.{" "}
No PII is collected.This anonymized data is used to understand how you use Plane and build new features
in line with{" "}
<a
href="https://docs.plane.so/self-hosting/telemetry"
target="_blank"
className="text-custom-primary-100 hover:underline"
rel="noreferrer"
>
Know more.
our Telemetry Policy.
</a>
</div>
</div>

View File

@@ -60,9 +60,9 @@ export const IntercomConfig: FC<TIntercomConfig> = observer((props) => {
</div>
<div className="grow">
<div className="text-sm font-medium text-custom-text-100 leading-5">Talk to Plane</div>
<div className="text-sm font-medium text-custom-text-100 leading-5">Chat with us</div>
<div className="text-xs font-normal text-custom-text-300 leading-5">
Let your members chat with us via Intercom or another service. Toggling Telemetry off turns this off
Let your users chat with us via Intercom or another service. Toggling Telemetry off turns this off
automatically.
</div>
</div>

View File

@@ -4,11 +4,11 @@ import { ReactNode } from "react";
import { ThemeProvider, useTheme } from "next-themes";
import { SWRConfig } from "swr";
// ui
import { ADMIN_BASE_PATH, DEFAULT_SWR_CONFIG } from "@plane/constants";
import { Toast } from "@plane/ui";
import { resolveGeneralTheme } from "@plane/utils";
// constants
import { SWR_CONFIG } from "@/constants/swr-config";
// helpers
import { ASSET_PREFIX, resolveGeneralTheme } from "@/helpers/common.helper";
// lib
import { InstanceProvider } from "@/lib/instance-provider";
import { StoreProvider } from "@/lib/store-provider";
@@ -22,6 +22,7 @@ const ToastWithTheme = () => {
};
export default function RootLayout({ children }: { children: ReactNode }) {
const ASSET_PREFIX = ADMIN_BASE_PATH;
return (
<html lang="en">
<head>
@@ -34,7 +35,7 @@ export default function RootLayout({ children }: { children: ReactNode }) {
<body className={`antialiased`}>
<ThemeProvider themes={["light", "dark"]} defaultTheme="system" enableSystem>
<ToastWithTheme />
<SWRConfig value={SWR_CONFIG}>
<SWRConfig value={DEFAULT_SWR_CONFIG}>
<StoreProvider>
<InstanceProvider>
<UserProvider>{children}</UserProvider>

View File

@@ -0,0 +1,212 @@
import { useState, useEffect } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { Controller, useForm } from "react-hook-form";
// constants
import { WEB_BASE_URL, ORGANIZATION_SIZE, RESTRICTED_URLS } from "@plane/constants";
// types
import { IWorkspace } from "@plane/types";
// components
import { Button, CustomSelect, getButtonStyling, Input, setToast, TOAST_TYPE } from "@plane/ui";
// hooks
import { useWorkspace } from "@/hooks/store";
// services
import { WorkspaceService } from "@/services/workspace.service";
const workspaceService = new WorkspaceService();
export const WorkspaceCreateForm = () => {
// router
const router = useRouter();
// states
const [slugError, setSlugError] = useState(false);
const [invalidSlug, setInvalidSlug] = useState(false);
const [defaultValues, setDefaultValues] = useState<Partial<IWorkspace>>({
name: "",
slug: "",
organization_size: "",
});
// store hooks
const { createWorkspace } = useWorkspace();
// form info
const {
handleSubmit,
control,
setValue,
getValues,
formState: { errors, isSubmitting, isValid },
} = useForm<IWorkspace>({ defaultValues, mode: "onChange" });
// derived values
const workspaceBaseURL = encodeURI(WEB_BASE_URL || window.location.origin + "/");
const handleCreateWorkspace = async (formData: IWorkspace) => {
await workspaceService
.workspaceSlugCheck(formData.slug)
.then(async (res) => {
if (res.status === true && !RESTRICTED_URLS.includes(formData.slug)) {
setSlugError(false);
await createWorkspace(formData)
.then(async () => {
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
message: "Workspace created successfully.",
});
router.push(`/workspace`);
})
.catch(() => {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Workspace could not be created. Please try again.",
});
});
} else setSlugError(true);
})
.catch(() => {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Some error occurred while creating workspace. Please try again.",
});
});
};
useEffect(
() => () => {
// when the component unmounts set the default values to whatever user typed in
setDefaultValues(getValues());
},
[getValues, setDefaultValues]
);
return (
<div className="space-y-8">
<div className="grid-col grid w-full max-w-4xl grid-cols-1 items-start justify-between gap-x-10 gap-y-6 lg:grid-cols-2">
<div className="flex flex-col gap-1">
<h4 className="text-sm text-custom-text-300">Name your workspace</h4>
<div className="flex flex-col gap-1">
<Controller
control={control}
name="name"
rules={{
required: "This is a required field.",
validate: (value) =>
/^[\w\s-]*$/.test(value) ||
`Workspaces names can contain only (" "), ( - ), ( _ ) and alphanumeric characters.`,
maxLength: {
value: 80,
message: "Limit your name to 80 characters.",
},
}}
render={({ field: { value, ref, onChange } }) => (
<Input
id="workspaceName"
type="text"
value={value}
onChange={(e) => {
onChange(e.target.value);
setValue("name", e.target.value);
setValue("slug", e.target.value.toLocaleLowerCase().trim().replace(/ /g, "-"), {
shouldValidate: true,
});
}}
ref={ref}
hasError={Boolean(errors.name)}
placeholder="Something familiar and recognizable is always best."
className="w-full"
/>
)}
/>
<span className="text-xs text-red-500">{errors?.name?.message}</span>
</div>
</div>
<div className="flex flex-col gap-1">
<h4 className="text-sm text-custom-text-300">Set your workspace&apos;s URL</h4>
<div className="flex gap-0.5 w-full items-center rounded-md border-[0.5px] border-custom-border-200 px-3">
<span className="whitespace-nowrap text-sm text-custom-text-200">{workspaceBaseURL}</span>
<Controller
control={control}
name="slug"
rules={{
required: "The URL is a required field.",
maxLength: {
value: 48,
message: "Limit your URL to 48 characters.",
},
}}
render={({ field: { onChange, value, ref } }) => (
<Input
id="workspaceUrl"
type="text"
value={value.toLocaleLowerCase().trim().replace(/ /g, "-")}
onChange={(e) => {
if (/^[a-zA-Z0-9_-]+$/.test(e.target.value)) setInvalidSlug(false);
else setInvalidSlug(true);
onChange(e.target.value.toLowerCase());
}}
ref={ref}
hasError={Boolean(errors.slug)}
placeholder="workspace-name"
className="block w-full rounded-md border-none bg-transparent !px-0 py-2 text-sm"
/>
)}
/>
</div>
{slugError && <p className="text-sm text-red-500">This URL is taken. Try something else.</p>}
{invalidSlug && (
<p className="text-sm text-red-500">{`URLs can contain only ( - ), ( _ ) and alphanumeric characters.`}</p>
)}
{errors.slug && <span className="text-xs text-red-500">{errors.slug.message}</span>}
</div>
<div className="flex flex-col gap-1">
<h4 className="text-sm text-custom-text-300">How many people will use this workspace?</h4>
<div className="w-full">
<Controller
name="organization_size"
control={control}
rules={{ required: "This is a required field." }}
render={({ field: { value, onChange } }) => (
<CustomSelect
value={value}
onChange={onChange}
label={
ORGANIZATION_SIZE.find((c) => c === value) ?? (
<span className="text-custom-text-400">Select a range</span>
)
}
buttonClassName="!border-[0.5px] !border-custom-border-200 !shadow-none"
input
optionsClassName="w-full"
>
{ORGANIZATION_SIZE.map((item) => (
<CustomSelect.Option key={item} value={item}>
{item}
</CustomSelect.Option>
))}
</CustomSelect>
)}
/>
{errors.organization_size && (
<span className="text-sm text-red-500">{errors.organization_size.message}</span>
)}
</div>
</div>
</div>
<div className="flex max-w-4xl items-center py-1 gap-4">
<Button
variant="primary"
size="sm"
onClick={handleSubmit(handleCreateWorkspace)}
disabled={!isValid}
loading={isSubmitting}
>
{isSubmitting ? "Creating workspace" : "Create workspace"}
</Button>
<Link className={getButtonStyling("neutral-primary", "sm")} href="/workspace">
Go back
</Link>
</div>
</div>
);
};

View File

@@ -0,0 +1,21 @@
"use client";
import { observer } from "mobx-react";
// components
import { WorkspaceCreateForm } from "./form";
const WorkspaceCreatePage = observer(() => (
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col">
<div className="border-b border-custom-border-100 mx-4 py-4 space-y-1 flex-shrink-0">
<div className="text-xl font-medium text-custom-text-100">Create a new workspace on this instance.</div>
<div className="text-sm font-normal text-custom-text-300">
You will need to invite users from Workspace Settings after you create this workspace.
</div>
</div>
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4">
<WorkspaceCreateForm />
</div>
</div>
));
export default WorkspaceCreatePage;

View File

@@ -0,0 +1,12 @@
import { ReactNode } from "react";
import { Metadata } from "next";
// layouts
import { AdminLayout } from "@/layouts/admin-layout";
export const metadata: Metadata = {
title: "Workspace Management - Plane Web",
};
export default function WorkspaceManagementLayout({ children }: { children: ReactNode }) {
return <AdminLayout>{children}</AdminLayout>;
}

View File

@@ -0,0 +1,167 @@
"use client";
import { useState } from "react";
import { observer } from "mobx-react";
import Link from "next/link";
import useSWR from "swr";
import { Loader as LoaderIcon } from "lucide-react";
// types
import { TInstanceConfigurationKeys } from "@plane/types";
import { Button, getButtonStyling, Loader, setPromiseToast, ToggleSwitch } from "@plane/ui";
import { cn } from "@plane/utils";
// components
import { WorkspaceListItem } from "@/components/workspace";
// hooks
import { useInstance, useWorkspace } from "@/hooks/store";
const WorkspaceManagementPage = observer(() => {
// states
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
// store
const { formattedConfig, fetchInstanceConfigurations, updateInstanceConfigurations } = useInstance();
const {
workspaceIds,
loader: workspaceLoader,
paginationInfo,
fetchWorkspaces,
fetchNextWorkspaces,
} = useWorkspace();
// derived values
const disableWorkspaceCreation = formattedConfig?.DISABLE_WORKSPACE_CREATION ?? "";
const hasNextPage = paginationInfo?.next_page_results && paginationInfo?.next_cursor !== undefined;
// fetch data
useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations());
useSWR("INSTANCE_WORKSPACES", () => fetchWorkspaces());
const updateConfig = async (key: TInstanceConfigurationKeys, value: string) => {
setIsSubmitting(true);
const payload = {
[key]: value,
};
const updateConfigPromise = updateInstanceConfigurations(payload);
setPromiseToast(updateConfigPromise, {
loading: "Saving configuration",
success: {
title: "Success",
message: () => "Configuration saved successfully",
},
error: {
title: "Error",
message: () => "Failed to save configuration",
},
});
await updateConfigPromise
.then(() => {
setIsSubmitting(false);
})
.catch((err) => {
console.error(err);
setIsSubmitting(false);
});
};
return (
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col">
<div className="flex items-center justify-between gap-4 border-b border-custom-border-100 mx-4 py-4 space-y-1 flex-shrink-0">
<div className="flex flex-col gap-1">
<div className="text-xl font-medium text-custom-text-100">Workspaces on this instance</div>
<div className="text-sm font-normal text-custom-text-300">
See all workspaces and control who can create them.
</div>
</div>
</div>
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4">
<div className="space-y-3">
{formattedConfig ? (
<div className={cn("w-full flex items-center gap-14 rounded")}>
<div className="flex grow items-center gap-4">
<div className="grow">
<div className="text-lg font-medium pb-1">Prevent anyone else from creating a workspace.</div>
<div className={cn("font-normal leading-5 text-custom-text-300 text-xs")}>
Toggling this on will let only you create workspaces. You will have to invite users to new
workspaces.
</div>
</div>
</div>
<div className={`shrink-0 pr-4 ${isSubmitting && "opacity-70"}`}>
<div className="flex items-center gap-4">
<ToggleSwitch
value={Boolean(parseInt(disableWorkspaceCreation))}
onChange={() => {
if (Boolean(parseInt(disableWorkspaceCreation)) === true) {
updateConfig("DISABLE_WORKSPACE_CREATION", "0");
} else {
updateConfig("DISABLE_WORKSPACE_CREATION", "1");
}
}}
size="sm"
disabled={isSubmitting}
/>
</div>
</div>
</div>
) : (
<Loader>
<Loader.Item height="50px" width="100%" />
</Loader>
)}
{workspaceLoader !== "init-loader" ? (
<>
<div className="pt-6 flex items-center justify-between gap-2">
<div className="flex flex-col items-start gap-x-2">
<div className="flex items-center gap-2 text-lg font-medium">
All workspaces on this instance{" "}
<span className="text-custom-text-300"> {workspaceIds.length}</span>
{workspaceLoader && ["mutation", "pagination"].includes(workspaceLoader) && (
<LoaderIcon className="w-4 h-4 animate-spin" />
)}
</div>
<div className={cn("font-normal leading-5 text-custom-text-300 text-xs")}>
You can&apos;t yet delete workspaces and you can only go to the workspace if you are an Admin or a
Member.
</div>
</div>
<div className="flex items-center gap-2">
<Link href="/workspace/create" className={getButtonStyling("primary", "sm")}>
Create workspace
</Link>
</div>
</div>
<div className="flex flex-col gap-4 py-2">
{workspaceIds.map((workspaceId) => (
<WorkspaceListItem key={workspaceId} workspaceId={workspaceId} />
))}
</div>
{hasNextPage && (
<div className="flex justify-center">
<Button
variant="link-primary"
onClick={() => fetchNextWorkspaces()}
disabled={workspaceLoader === "pagination"}
>
Load more
{workspaceLoader === "pagination" && <LoaderIcon className="w-3 h-3 animate-spin" />}
</Button>
</div>
)}
</>
) : (
<Loader className="space-y-10 py-8">
<Loader.Item height="24px" width="20%" />
<Loader.Item height="92px" width="100%" />
<Loader.Item height="92px" width="100%" />
<Loader.Item height="92px" width="100%" />
</Loader>
)}
</div>
</div>
</div>
);
});
export default WorkspaceManagementPage;

View File

@@ -10,7 +10,7 @@ import {
// components
import { AuthenticationMethodCard } from "@/components/authentication";
// helpers
import { getBaseAuthenticationModes } from "@/helpers/authentication.helper";
import { getBaseAuthenticationModes } from "@/lib/auth-helpers";
// plane admin components
import { UpgradeButton } from "@/plane-admin/components/common";
// images

View File

@@ -3,14 +3,13 @@
import React from "react";
// icons
import { SquareArrowOutUpRight } from "lucide-react";
// ui
// plane internal packages
import { getButtonStyling } from "@plane/ui";
// helpers
import { cn } from "@/helpers/common.helper";
import { cn } from "@plane/utils";
export const UpgradeButton: React.FC = () => (
<a href="https://plane.so/one" target="_blank" className={cn(getButtonStyling("primary", "sm"))}>
Available on One
<a href="https://plane.so/pricing?mode=self-hosted" target="_blank" className={cn(getButtonStyling("primary", "sm"))}>
Upgrade
<SquareArrowOutUpRight className="h-3.5 w-3.5 p-0.5" />
</a>
);

View File

@@ -5,13 +5,14 @@ import { observer } from "mobx-react";
import Link from "next/link";
import { ExternalLink, FileText, HelpCircle, MoveLeft } from "lucide-react";
import { Transition } from "@headlessui/react";
// ui
// plane internal packages
import { WEB_BASE_URL } from "@plane/constants";
import { DiscordIcon, GithubIcon, Tooltip } from "@plane/ui";
// helpers
import { WEB_BASE_URL, cn } from "@/helpers/common.helper";
import { cn } from "@plane/utils";
// hooks
import { useTheme } from "@/hooks/store";
// assets
// eslint-disable-next-line import/order
import packageJson from "package.json";
const helpOptions = [
@@ -52,13 +53,13 @@ export const HelpSection: FC = observer(() => {
)}
>
<div className={`flex items-center gap-1 ${isSidebarCollapsed ? "flex-col justify-center" : "w-full"}`}>
<Tooltip tooltipContent="Redirect to plane" position="right" className="ml-4" disabled={!isSidebarCollapsed}>
<Tooltip tooltipContent="Redirect to Plane" position="right" className="ml-4" disabled={!isSidebarCollapsed}>
<a
href={redirectionLink}
className={`relative px-2 py-1.5 flex items-center gap-2 font-medium rounded border border-custom-primary-100/20 bg-custom-primary-100/10 text-xs text-custom-primary-200 whitespace-nowrap`}
>
<ExternalLink size={14} />
{!isSidebarCollapsed && "Redirect to plane"}
{!isSidebarCollapsed && "Redirect to Plane"}
</a>
</Tooltip>
<Tooltip tooltipContent="Help" position={isSidebarCollapsed ? "right" : "top"} className="ml-4">

View File

@@ -3,7 +3,7 @@
import { FC, useEffect, useRef } from "react";
import { observer } from "mobx-react";
// plane helpers
import { useOutsideClickDetector } from "@plane/helpers";
import { useOutsideClickDetector } from "@plane/hooks";
// components
import { HelpSection, SidebarMenu, SidebarDropdown } from "@/components/admin-sidebar";
// hooks

View File

@@ -5,11 +5,10 @@ import { observer } from "mobx-react";
import { useTheme as useNextTheme } from "next-themes";
import { LogOut, UserCog2, Palette } from "lucide-react";
import { Menu, Transition } from "@headlessui/react";
// plane ui
// plane internal packages
import { API_BASE_URL } from "@plane/constants";
import { Avatar } from "@plane/ui";
// helpers
import { API_BASE_URL, cn } from "@/helpers/common.helper";
import { getFileURL } from "@/helpers/file.helper";
import { getFileURL, cn } from "@plane/utils";
// hooks
import { useTheme, useUser } from "@/hooks/store";
// services

View File

@@ -4,41 +4,47 @@ import { observer } from "mobx-react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { Image, BrainCog, Cog, Lock, Mail } from "lucide-react";
import { Tooltip } from "@plane/ui";
// plane internal packages
import { Tooltip, WorkspaceIcon } from "@plane/ui";
import { cn } from "@plane/utils";
// hooks
import { cn } from "@/helpers/common.helper";
import { useTheme } from "@/hooks/store";
// helpers
const INSTANCE_ADMIN_LINKS = [
{
Icon: Cog,
name: "General",
description: "Identify your instances and get key details",
description: "Identify your instances and get key details.",
href: `/general/`,
},
{
Icon: WorkspaceIcon,
name: "Workspaces",
description: "Manage all workspaces on this instance.",
href: `/workspace/`,
},
{
Icon: Mail,
name: "Email",
description: "Set up emails to your users",
description: "Configure your SMTP controls.",
href: `/email/`,
},
{
Icon: Lock,
name: "Authentication",
description: "Configure authentication modes",
description: "Configure authentication modes.",
href: `/authentication/`,
},
{
Icon: BrainCog,
name: "Artificial intelligence",
description: "Configure your OpenAI creds",
description: "Configure your OpenAI creds.",
href: `/ai/`,
},
{
Icon: Image,
name: "Images in Plane",
description: "Allow third-party image libraries",
description: "Allow third-party image libraries.",
href: `/image/`,
},
];

View File

@@ -30,9 +30,13 @@ export const InstanceHeader: FC = observer(() => {
case "google":
return "Google";
case "github":
return "Github";
return "GitHub";
case "gitlab":
return "GitLab";
case "workspace":
return "Workspace";
case "create":
return "Create";
default:
return pathName.toUpperCase();
}

View File

@@ -1,7 +1,7 @@
import { FC } from "react";
import { Info, X } from "lucide-react";
// helpers
import { TAuthErrorInfo } from "@/helpers/authentication.helper";
// plane constants
import { TAuthErrorInfo } from "@plane/constants";
type TAuthBanner = {
bannerData: TAuthErrorInfo | undefined;

View File

@@ -2,7 +2,7 @@
import { FC } from "react";
// helpers
import { cn } from "helpers/common.helper";
import { cn } from "@plane/utils";
type Props = {
name: string;

View File

@@ -5,12 +5,10 @@ import { observer } from "mobx-react";
import Link from "next/link";
// icons
import { Settings2 } from "lucide-react";
// types
// plane internal packages
import { TInstanceAuthenticationMethodKeys } from "@plane/types";
// ui
import { ToggleSwitch, getButtonStyling } from "@plane/ui";
// helpers
import { cn } from "@/helpers/common.helper";
import { cn } from "@plane/utils";
// hooks
import { useInstance } from "@/hooks/store";

View File

@@ -5,12 +5,10 @@ import { observer } from "mobx-react";
import Link from "next/link";
// icons
import { Settings2 } from "lucide-react";
// types
// plane internal packages
import { TInstanceAuthenticationMethodKeys } from "@plane/types";
// ui
import { ToggleSwitch, getButtonStyling } from "@plane/ui";
// helpers
import { cn } from "@/helpers/common.helper";
import { cn } from "@plane/utils";
// hooks
import { useInstance } from "@/hooks/store";

View File

@@ -5,12 +5,10 @@ import { observer } from "mobx-react";
import Link from "next/link";
// icons
import { Settings2 } from "lucide-react";
// types
// plane internal packages
import { TInstanceAuthenticationMethodKeys } from "@plane/types";
// ui
import { ToggleSwitch, getButtonStyling } from "@plane/ui";
// helpers
import { cn } from "@/helpers/common.helper";
import { cn } from "@plane/utils";
// hooks
import { useInstance } from "@/hooks/store";

View File

@@ -1,4 +1,4 @@
import { cn } from "@/helpers/common.helper";
import { cn } from "@plane/utils";
type TProps = {
children: React.ReactNode;

View File

@@ -4,10 +4,9 @@ import React, { useState } from "react";
import { Controller, Control } from "react-hook-form";
// icons
import { Eye, EyeOff } from "lucide-react";
// ui
// plane internal packages
import { Input } from "@plane/ui";
// helpers
import { cn } from "@/helpers/common.helper";
import { cn } from "@plane/utils";
type Props = {
control: Control<any>;
@@ -37,9 +36,7 @@ export const ControllerInput: React.FC<Props> = (props) => {
return (
<div className="flex flex-col gap-1">
<h4 className="text-sm text-custom-text-300">
{label}
</h4>
<h4 className="text-sm text-custom-text-300">{label}</h4>
<div className="relative">
<Controller
control={control}

View File

@@ -1,14 +1,9 @@
"use client";
import { FC, useMemo } from "react";
// import { CircleCheck } from "lucide-react";
// helpers
import { cn } from "@/helpers/common.helper";
import {
E_PASSWORD_STRENGTH,
// PASSWORD_CRITERIA,
getPasswordStrength,
} from "@/helpers/password.helper";
// plane internal packages
import { E_PASSWORD_STRENGTH } from "@plane/constants";
import { cn, getPasswordStrength } from "@plane/utils";
type TPasswordStrengthMeter = {
password: string;

View File

@@ -4,13 +4,12 @@ import { FC, useEffect, useMemo, useState } from "react";
import { useSearchParams } from "next/navigation";
// icons
import { Eye, EyeOff } from "lucide-react";
// ui
// plane internal packages
import { API_BASE_URL, E_PASSWORD_STRENGTH } from "@plane/constants";
import { Button, Checkbox, Input, Spinner } from "@plane/ui";
import { getPasswordStrength } from "@plane/utils";
// components
import { Banner, PasswordStrengthMeter } from "@/components/common";
// helpers
import { API_BASE_URL } from "@/helpers/common.helper";
import { E_PASSWORD_STRENGTH, getPasswordStrength } from "@/helpers/password.helper";
// services
import { AuthService } from "@/services/auth.service";

View File

@@ -2,24 +2,18 @@
import { FC, useEffect, useMemo, useState } from "react";
import { useSearchParams } from "next/navigation";
// services
import { Eye, EyeOff } from "lucide-react";
// plane internal packages
import { API_BASE_URL, EAdminAuthErrorCodes, TAuthErrorInfo } from "@plane/constants";
import { Button, Input, Spinner } from "@plane/ui";
// components
import { Banner } from "@/components/common";
// helpers
import {
authErrorHandler,
EAuthenticationErrorCodes,
EErrorAlertType,
TAuthErrorInfo,
} from "@/helpers/authentication.helper";
import { API_BASE_URL } from "@/helpers/common.helper";
import { authErrorHandler } from "@/lib/auth-helpers";
// services
import { AuthService } from "@/services/auth.service";
// local components
import { AuthBanner } from "../authentication";
// ui
// icons
// service initialization
const authService = new AuthService();
@@ -102,7 +96,7 @@ export const InstanceSignInForm: FC = (props) => {
useEffect(() => {
if (errorCode) {
const errorDetail = authErrorHandler(errorCode?.toString() as EAuthenticationErrorCodes);
const errorDetail = authErrorHandler(errorCode?.toString() as EAdminAuthErrorCodes);
if (errorDetail) {
setErrorInfo(errorDetail);
}

View File

@@ -3,11 +3,11 @@
import React from "react";
import { observer } from "mobx-react";
import Image from "next/image";
import Link from "next/link";
import { useTheme as nextUseTheme } from "next-themes";
// ui
import { Button, getButtonStyling } from "@plane/ui";
// helpers
import { WEB_BASE_URL, resolveGeneralTheme } from "helpers/common.helper";
import { resolveGeneralTheme } from "@plane/utils";
// hooks
import { useTheme } from "@/hooks/store";
// icons
@@ -20,8 +20,6 @@ export const NewUserPopup: React.FC = observer(() => {
// theme
const { resolvedTheme } = nextUseTheme();
const redirectionLink = encodeURI(WEB_BASE_URL + "/create-workspace");
if (!isNewUserPopup) return <></>;
return (
<div className="absolute bottom-8 right-8 p-6 w-96 border border-custom-border-100 shadow-md rounded-lg bg-custom-background-100">
@@ -30,12 +28,12 @@ export const NewUserPopup: React.FC = observer(() => {
<div className="text-base font-semibold">Create workspace</div>
<div className="py-2 text-sm font-medium text-custom-text-300">
Instance setup done! Welcome to Plane instance portal. Start your journey with by creating your first
workspace, you will need to login again.
workspace.
</div>
<div className="flex items-center gap-4 pt-2">
<a href={redirectionLink} className={getButtonStyling("primary", "sm")}>
<Link href="/workspace/create" className={getButtonStyling("primary", "sm")}>
Create workspace
</a>
</Link>
<Button variant="neutral-primary" size="sm" onClick={toggleNewUserPopup}>
Close
</Button>

View File

@@ -0,0 +1 @@
export * from "./list-item";

View File

@@ -0,0 +1,81 @@
import { observer } from "mobx-react";
import { ExternalLink } from "lucide-react";
// plane internal packages
import { WEB_BASE_URL } from "@plane/constants";
import { Tooltip } from "@plane/ui";
import { getFileURL } from "@plane/utils";
// hooks
import { useWorkspace } from "@/hooks/store";
type TWorkspaceListItemProps = {
workspaceId: string;
};
export const WorkspaceListItem = observer(({ workspaceId }: TWorkspaceListItemProps) => {
// store hooks
const { getWorkspaceById } = useWorkspace();
// derived values
const workspace = getWorkspaceById(workspaceId);
if (!workspace) return null;
return (
<a
key={workspaceId}
href={`${WEB_BASE_URL}/${encodeURIComponent(workspace.slug)}`}
target="_blank"
className="group flex items-center justify-between p-4 gap-2.5 truncate border border-custom-border-200/70 hover:border-custom-border-200 hover:bg-custom-background-90 rounded-md"
>
<div className="flex items-start gap-4">
<span
className={`relative flex h-8 w-8 flex-shrink-0 items-center justify-center p-2 mt-1 text-xs uppercase ${
!workspace?.logo_url && "rounded bg-custom-primary-500 text-white"
}`}
>
{workspace?.logo_url && workspace.logo_url !== "" ? (
<img
src={getFileURL(workspace.logo_url)}
className="absolute left-0 top-0 h-full w-full rounded object-cover"
alt="Workspace Logo"
/>
) : (
(workspace?.name?.[0] ?? "...")
)}
</span>
<div className="flex flex-col items-start gap-1">
<div className="flex flex-wrap w-full items-center gap-2.5">
<h3 className={`text-base font-medium capitalize`}>{workspace.name}</h3>/
<Tooltip tooltipContent="The unique URL of your workspace">
<h4 className="text-sm text-custom-text-300">[{workspace.slug}]</h4>
</Tooltip>
</div>
{workspace.owner.email && (
<div className="flex items-center gap-1 text-xs">
<h3 className="text-custom-text-200 font-medium">Owned by:</h3>
<h4 className="text-custom-text-300">{workspace.owner.email}</h4>
</div>
)}
<div className="flex items-center gap-2.5 text-xs">
{workspace.total_projects !== null && (
<span className="flex items-center gap-1">
<h3 className="text-custom-text-200 font-medium">Total projects:</h3>
<h4 className="text-custom-text-300">{workspace.total_projects}</h4>
</span>
)}
{workspace.total_members !== null && (
<>
<span className="flex items-center gap-1">
<h3 className="text-custom-text-200 font-medium">Total members:</h3>
<h4 className="text-custom-text-300">{workspace.total_members}</h4>
</span>
</>
)}
</div>
</div>
</div>
<div className="flex-shrink-0">
<ExternalLink size={14} className="text-custom-text-400 group-hover:text-custom-text-200" />
</div>
</a>
);
});

View File

@@ -1,8 +0,0 @@
export const SITE_NAME = "Plane | Simple, extensible, open-source project management tool.";
export const SITE_TITLE = "Plane | Simple, extensible, open-source project management tool.";
export const SITE_DESCRIPTION =
"Open-source project management tool to manage issues, sprints, and product roadmaps with peace of mind.";
export const SITE_KEYWORDS =
"software development, plan, ship, software, accelerate, code management, release management, project management, issue tracking, agile, scrum, kanban, collaboration";
export const SITE_URL = "https://app.plane.so/";
export const TWITTER_USER_NAME = "Plane | Simple, extensible, open-source project management tool.";

View File

@@ -1,3 +1,4 @@
export * from "./use-theme";
export * from "./use-instance";
export * from "./use-user";
export * from "./use-workspace";

View File

@@ -0,0 +1,10 @@
import { useContext } from "react";
// store
import { StoreContext } from "@/lib/store-provider";
import { IWorkspaceStore } from "@/store/workspace.store";
export const useWorkspace = (): IWorkspaceStore => {
const context = useContext(StoreContext);
if (context === undefined) throw new Error("useWorkspace must be used within StoreProvider");
return context.workspace;
};

View File

@@ -0,0 +1,164 @@
import { ReactNode } from "react";
import Image from "next/image";
import Link from "next/link";
import { KeyRound, Mails } from "lucide-react";
// plane packages
import { SUPPORT_EMAIL, EAdminAuthErrorCodes, TAuthErrorInfo } from "@plane/constants";
import { TGetBaseAuthenticationModeProps, TInstanceAuthenticationModes } from "@plane/types";
import { resolveGeneralTheme } from "@plane/utils";
// components
import {
EmailCodesConfiguration,
GithubConfiguration,
GitlabConfiguration,
GoogleConfiguration,
PasswordLoginConfiguration,
} from "@/components/authentication";
// images
import githubLightModeImage from "@/public/logos/github-black.png";
import githubDarkModeImage from "@/public/logos/github-white.png";
import GitlabLogo from "@/public/logos/gitlab-logo.svg";
import GoogleLogo from "@/public/logos/google-logo.svg";
export enum EErrorAlertType {
BANNER_ALERT = "BANNER_ALERT",
INLINE_FIRST_NAME = "INLINE_FIRST_NAME",
INLINE_EMAIL = "INLINE_EMAIL",
INLINE_PASSWORD = "INLINE_PASSWORD",
INLINE_EMAIL_CODE = "INLINE_EMAIL_CODE",
}
const errorCodeMessages: {
[key in EAdminAuthErrorCodes]: { title: string; message: (email?: string | undefined) => ReactNode };
} = {
// admin
[EAdminAuthErrorCodes.ADMIN_ALREADY_EXIST]: {
title: `Admin already exists`,
message: () => `Admin already exists. Please try again.`,
},
[EAdminAuthErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME]: {
title: `Email, password and first name required`,
message: () => `Email, password and first name required. Please try again.`,
},
[EAdminAuthErrorCodes.INVALID_ADMIN_EMAIL]: {
title: `Invalid admin email`,
message: () => `Invalid admin email. Please try again.`,
},
[EAdminAuthErrorCodes.INVALID_ADMIN_PASSWORD]: {
title: `Invalid admin password`,
message: () => `Invalid admin password. Please try again.`,
},
[EAdminAuthErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD]: {
title: `Email and password required`,
message: () => `Email and password required. Please try again.`,
},
[EAdminAuthErrorCodes.ADMIN_AUTHENTICATION_FAILED]: {
title: `Authentication failed`,
message: () => `Authentication failed. Please try again.`,
},
[EAdminAuthErrorCodes.ADMIN_USER_ALREADY_EXIST]: {
title: `Admin user already exists`,
message: () => (
<div>
Admin user already exists.&nbsp;
<Link className="underline underline-offset-4 font-medium hover:font-bold transition-all" href={`/admin`}>
Sign In
</Link>
&nbsp;now.
</div>
),
},
[EAdminAuthErrorCodes.ADMIN_USER_DOES_NOT_EXIST]: {
title: `Admin user does not exist`,
message: () => (
<div>
Admin user does not exist.&nbsp;
<Link className="underline underline-offset-4 font-medium hover:font-bold transition-all" href={`/admin`}>
Sign In
</Link>
&nbsp;now.
</div>
),
},
[EAdminAuthErrorCodes.ADMIN_USER_DEACTIVATED]: {
title: `User account deactivated`,
message: () => `User account deactivated. Please contact ${!!SUPPORT_EMAIL ? SUPPORT_EMAIL : "administrator"}.`,
},
};
export const authErrorHandler = (
errorCode: EAdminAuthErrorCodes,
email?: string | undefined
): TAuthErrorInfo | undefined => {
const bannerAlertErrorCodes = [
EAdminAuthErrorCodes.ADMIN_ALREADY_EXIST,
EAdminAuthErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME,
EAdminAuthErrorCodes.INVALID_ADMIN_EMAIL,
EAdminAuthErrorCodes.INVALID_ADMIN_PASSWORD,
EAdminAuthErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD,
EAdminAuthErrorCodes.ADMIN_AUTHENTICATION_FAILED,
EAdminAuthErrorCodes.ADMIN_USER_ALREADY_EXIST,
EAdminAuthErrorCodes.ADMIN_USER_DOES_NOT_EXIST,
EAdminAuthErrorCodes.ADMIN_USER_DEACTIVATED,
];
if (bannerAlertErrorCodes.includes(errorCode))
return {
type: EErrorAlertType.BANNER_ALERT,
code: errorCode,
title: errorCodeMessages[errorCode]?.title || "Error",
message: errorCodeMessages[errorCode]?.message(email) || "Something went wrong. Please try again.",
};
return undefined;
};
export const getBaseAuthenticationModes: (props: TGetBaseAuthenticationModeProps) => TInstanceAuthenticationModes[] = ({
disabled,
updateConfig,
resolvedTheme,
}) => [
{
key: "unique-codes",
name: "Unique codes",
description:
"Log in or sign up for Plane using codes sent via email. You need to have set up SMTP to use this method.",
icon: <Mails className="h-6 w-6 p-0.5 text-custom-text-300/80" />,
config: <EmailCodesConfiguration disabled={disabled} updateConfig={updateConfig} />,
},
{
key: "passwords-login",
name: "Passwords",
description: "Allow members to create accounts with passwords and use it with their email addresses to sign in.",
icon: <KeyRound className="h-6 w-6 p-0.5 text-custom-text-300/80" />,
config: <PasswordLoginConfiguration disabled={disabled} updateConfig={updateConfig} />,
},
{
key: "google",
name: "Google",
description: "Allow members to log in or sign up for Plane with their Google accounts.",
icon: <Image src={GoogleLogo} height={20} width={20} alt="Google Logo" />,
config: <GoogleConfiguration disabled={disabled} updateConfig={updateConfig} />,
},
{
key: "github",
name: "GitHub",
description: "Allow members to log in or sign up for Plane with their GitHub accounts.",
icon: (
<Image
src={resolveGeneralTheme(resolvedTheme) === "dark" ? githubDarkModeImage : githubLightModeImage}
height={20}
width={20}
alt="GitHub Logo"
/>
),
config: <GithubConfiguration disabled={disabled} updateConfig={updateConfig} />,
},
{
key: "gitlab",
name: "GitLab",
description: "Allow members to log in or sign up to plane with their GitLab accounts.",
icon: <Image src={GitlabLogo} height={20} width={20} alt="GitLab Logo" />,
config: <GitlabConfiguration disabled={disabled} updateConfig={updateConfig} />,
},
];

View File

@@ -1,5 +1,4 @@
// helpers
import { API_BASE_URL } from "@/helpers/common.helper";
import { API_BASE_URL } from "@plane/constants";
// services
import { APIService } from "@/services/api.service";

View File

@@ -1,4 +1,5 @@
// types
// plane internal packages
import { API_BASE_URL } from "@plane/constants";
import type {
IFormattedInstanceConfiguration,
IInstance,
@@ -7,7 +8,6 @@ import type {
IInstanceInfo,
} from "@plane/types";
// helpers
import { API_BASE_URL } from "@/helpers/common.helper";
import { APIService } from "@/services/api.service";
export class InstanceService extends APIService {

View File

@@ -1,7 +1,6 @@
// types
// plane internal packages
import { API_BASE_URL } from "@plane/constants";
import type { IUser } from "@plane/types";
// helpers
import { API_BASE_URL } from "@/helpers/common.helper";
// services
import { APIService } from "@/services/api.service";

View File

@@ -0,0 +1,52 @@
// plane internal packages
import { API_BASE_URL } from "@plane/constants";
import type { IWorkspace, TWorkspacePaginationInfo } from "@plane/types";
// services
import { APIService } from "@/services/api.service";
export class WorkspaceService extends APIService {
constructor() {
super(API_BASE_URL);
}
/**
* @description Fetches all workspaces
* @returns Promise<TWorkspacePaginationInfo>
*/
async getWorkspaces(nextPageCursor?: string): Promise<TWorkspacePaginationInfo> {
return this.get<TWorkspacePaginationInfo>("/api/instances/workspaces/", {
cursor: nextPageCursor,
})
.then((response) => response.data)
.catch((error) => {
throw error?.response?.data;
});
}
/**
* @description Checks if a slug is available
* @param slug - string
* @returns Promise<any>
*/
async workspaceSlugCheck(slug: string): Promise<any> {
const params = new URLSearchParams({ slug });
return this.get(`/api/instances/workspace-slug-check/?${params.toString()}`)
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;
});
}
/**
* @description Creates a new workspace
* @param data - IWorkspace
* @returns Promise<IWorkspace>
*/
async createWorkspace(data: IWorkspace): Promise<IWorkspace> {
return this.post<IWorkspace, IWorkspace>("/api/instances/workspaces/", data)
.then((response) => response.data)
.catch((error) => {
throw error?.response?.data;
});
}
}

View File

@@ -1,5 +1,7 @@
import set from "lodash/set";
import { observable, action, computed, makeObservable, runInAction } from "mobx";
// plane internal packages
import { EInstanceStatus, TInstanceStatus } from "@plane/constants";
import {
IInstance,
IInstanceAdmin,
@@ -8,8 +10,6 @@ import {
IInstanceInfo,
IInstanceConfig,
} from "@plane/types";
// helpers
import { EInstanceStatus, TInstanceStatus } from "@/helpers/instance.helper";
// services
import { InstanceService } from "@/services/instance.service";
// root store

View File

@@ -3,6 +3,7 @@ import { enableStaticRendering } from "mobx-react";
import { IInstanceStore, InstanceStore } from "./instance.store";
import { IThemeStore, ThemeStore } from "./theme.store";
import { IUserStore, UserStore } from "./user.store";
import { IWorkspaceStore, WorkspaceStore } from "./workspace.store";
enableStaticRendering(typeof window === "undefined");
@@ -10,17 +11,20 @@ export abstract class CoreRootStore {
theme: IThemeStore;
instance: IInstanceStore;
user: IUserStore;
workspace: IWorkspaceStore;
constructor() {
this.theme = new ThemeStore(this);
this.instance = new InstanceStore(this);
this.user = new UserStore(this);
this.workspace = new WorkspaceStore(this);
}
hydrate(initialData: any) {
this.theme.hydrate(initialData.theme);
this.instance.hydrate(initialData.instance);
this.user.hydrate(initialData.user);
this.workspace.hydrate(initialData.workspace);
}
resetOnSignOut() {
@@ -28,5 +32,6 @@ export abstract class CoreRootStore {
this.instance = new InstanceStore(this);
this.user = new UserStore(this);
this.theme = new ThemeStore(this);
this.workspace = new WorkspaceStore(this);
}
}

View File

@@ -1,7 +1,7 @@
import { action, observable, runInAction, makeObservable } from "mobx";
// plane internal packages
import { EUserStatus, TUserStatus } from "@plane/constants";
import { IUser } from "@plane/types";
// helpers
import { EUserStatus, TUserStatus } from "@/helpers/user.helper";
// services
import { AuthService } from "@/services/auth.service";
import { UserService } from "@/services/user.service";

View File

@@ -0,0 +1,150 @@
import set from "lodash/set";
import { action, observable, runInAction, makeObservable, computed } from "mobx";
import { IWorkspace, TLoader, TPaginationInfo } from "@plane/types";
// services
import { WorkspaceService } from "@/services/workspace.service";
// root store
import { CoreRootStore } from "@/store/root.store";
export interface IWorkspaceStore {
// observables
loader: TLoader;
workspaces: Record<string, IWorkspace>;
paginationInfo: TPaginationInfo | undefined;
// computed
workspaceIds: string[];
// helper actions
hydrate: (data: Record<string, IWorkspace>) => void;
getWorkspaceById: (workspaceId: string) => IWorkspace | undefined;
// fetch actions
fetchWorkspaces: () => Promise<IWorkspace[]>;
fetchNextWorkspaces: () => Promise<IWorkspace[]>;
// curd actions
createWorkspace: (data: IWorkspace) => Promise<IWorkspace>;
}
export class WorkspaceStore implements IWorkspaceStore {
// observables
loader: TLoader = "init-loader";
workspaces: Record<string, IWorkspace> = {};
paginationInfo: TPaginationInfo | undefined = undefined;
// services
workspaceService;
constructor(private store: CoreRootStore) {
makeObservable(this, {
// observables
loader: observable,
workspaces: observable,
paginationInfo: observable,
// computed
workspaceIds: computed,
// helper actions
hydrate: action,
getWorkspaceById: action,
// fetch actions
fetchWorkspaces: action,
fetchNextWorkspaces: action,
// curd actions
createWorkspace: action,
});
this.workspaceService = new WorkspaceService();
}
// computed
get workspaceIds() {
return Object.keys(this.workspaces);
}
// helper actions
/**
* @description Hydrates the workspaces
* @param data - Record<string, IWorkspace>
*/
hydrate = (data: Record<string, IWorkspace>) => {
if (data) this.workspaces = data;
};
/**
* @description Gets a workspace by id
* @param workspaceId - string
* @returns IWorkspace | undefined
*/
getWorkspaceById = (workspaceId: string) => this.workspaces[workspaceId];
// fetch actions
/**
* @description Fetches all workspaces
* @returns Promise<>
*/
fetchWorkspaces = async (): Promise<IWorkspace[]> => {
try {
if (this.workspaceIds.length > 0) {
this.loader = "mutation";
} else {
this.loader = "init-loader";
}
const paginatedWorkspaceData = await this.workspaceService.getWorkspaces();
runInAction(() => {
const { results, ...paginationInfo } = paginatedWorkspaceData;
results.forEach((workspace: IWorkspace) => {
set(this.workspaces, [workspace.id], workspace);
});
set(this, "paginationInfo", paginationInfo);
});
return paginatedWorkspaceData.results;
} catch (error) {
console.error("Error fetching workspaces", error);
throw error;
} finally {
this.loader = "loaded";
}
};
/**
* @description Fetches the next page of workspaces
* @returns Promise<IWorkspace[]>
*/
fetchNextWorkspaces = async (): Promise<IWorkspace[]> => {
if (!this.paginationInfo || this.paginationInfo.next_page_results === false) return [];
try {
this.loader = "pagination";
const paginatedWorkspaceData = await this.workspaceService.getWorkspaces(this.paginationInfo.next_cursor);
runInAction(() => {
const { results, ...paginationInfo } = paginatedWorkspaceData;
results.forEach((workspace: IWorkspace) => {
set(this.workspaces, [workspace.id], workspace);
});
set(this, "paginationInfo", paginationInfo);
});
return paginatedWorkspaceData.results;
} catch (error) {
console.error("Error fetching next workspaces", error);
throw error;
} finally {
this.loader = "loaded";
}
};
// curd actions
/**
* @description Creates a new workspace
* @param data - IWorkspace
* @returns Promise<IWorkspace>
*/
createWorkspace = async (data: IWorkspace): Promise<IWorkspace> => {
try {
this.loader = "mutation";
const workspace = await this.workspaceService.createWorkspace(data);
runInAction(() => {
set(this.workspaces, [workspace.id], workspace);
});
return workspace;
} catch (error) {
console.error("Error creating workspace", error);
throw error;
} finally {
this.loader = "loaded";
}
};
}

View File

@@ -1,203 +0,0 @@
import { ReactNode } from "react";
import Image from "next/image";
import Link from "next/link";
import { KeyRound, Mails } from "lucide-react";
// types
import { TGetBaseAuthenticationModeProps, TInstanceAuthenticationModes } from "@plane/types";
// components
import {
EmailCodesConfiguration,
GithubConfiguration,
GitlabConfiguration,
GoogleConfiguration,
PasswordLoginConfiguration,
} from "@/components/authentication";
// helpers
import { SUPPORT_EMAIL, resolveGeneralTheme } from "@/helpers/common.helper";
// images
import githubLightModeImage from "@/public/logos/github-black.png";
import githubDarkModeImage from "@/public/logos/github-white.png";
import GitlabLogo from "@/public/logos/gitlab-logo.svg";
import GoogleLogo from "@/public/logos/google-logo.svg";
export enum EPageTypes {
PUBLIC = "PUBLIC",
NON_AUTHENTICATED = "NON_AUTHENTICATED",
SET_PASSWORD = "SET_PASSWORD",
ONBOARDING = "ONBOARDING",
AUTHENTICATED = "AUTHENTICATED",
}
export enum EAuthModes {
SIGN_IN = "SIGN_IN",
SIGN_UP = "SIGN_UP",
}
export enum EAuthSteps {
EMAIL = "EMAIL",
PASSWORD = "PASSWORD",
UNIQUE_CODE = "UNIQUE_CODE",
}
export enum EErrorAlertType {
BANNER_ALERT = "BANNER_ALERT",
INLINE_FIRST_NAME = "INLINE_FIRST_NAME",
INLINE_EMAIL = "INLINE_EMAIL",
INLINE_PASSWORD = "INLINE_PASSWORD",
INLINE_EMAIL_CODE = "INLINE_EMAIL_CODE",
}
export enum EAuthenticationErrorCodes {
// Admin
ADMIN_ALREADY_EXIST = "5150",
REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME = "5155",
INVALID_ADMIN_EMAIL = "5160",
INVALID_ADMIN_PASSWORD = "5165",
REQUIRED_ADMIN_EMAIL_PASSWORD = "5170",
ADMIN_AUTHENTICATION_FAILED = "5175",
ADMIN_USER_ALREADY_EXIST = "5180",
ADMIN_USER_DOES_NOT_EXIST = "5185",
ADMIN_USER_DEACTIVATED = "5190",
}
export type TAuthErrorInfo = {
type: EErrorAlertType;
code: EAuthenticationErrorCodes;
title: string;
message: ReactNode;
};
const errorCodeMessages: {
[key in EAuthenticationErrorCodes]: { title: string; message: (email?: string | undefined) => ReactNode };
} = {
// admin
[EAuthenticationErrorCodes.ADMIN_ALREADY_EXIST]: {
title: `Admin already exists`,
message: () => `Admin already exists. Please try again.`,
},
[EAuthenticationErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME]: {
title: `Email, password and first name required`,
message: () => `Email, password and first name required. Please try again.`,
},
[EAuthenticationErrorCodes.INVALID_ADMIN_EMAIL]: {
title: `Invalid admin email`,
message: () => `Invalid admin email. Please try again.`,
},
[EAuthenticationErrorCodes.INVALID_ADMIN_PASSWORD]: {
title: `Invalid admin password`,
message: () => `Invalid admin password. Please try again.`,
},
[EAuthenticationErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD]: {
title: `Email and password required`,
message: () => `Email and password required. Please try again.`,
},
[EAuthenticationErrorCodes.ADMIN_AUTHENTICATION_FAILED]: {
title: `Authentication failed`,
message: () => `Authentication failed. Please try again.`,
},
[EAuthenticationErrorCodes.ADMIN_USER_ALREADY_EXIST]: {
title: `Admin user already exists`,
message: () => (
<div>
Admin user already exists.&nbsp;
<Link className="underline underline-offset-4 font-medium hover:font-bold transition-all" href={`/admin`}>
Sign In
</Link>
&nbsp;now.
</div>
),
},
[EAuthenticationErrorCodes.ADMIN_USER_DOES_NOT_EXIST]: {
title: `Admin user does not exist`,
message: () => (
<div>
Admin user does not exist.&nbsp;
<Link className="underline underline-offset-4 font-medium hover:font-bold transition-all" href={`/admin`}>
Sign In
</Link>
&nbsp;now.
</div>
),
},
[EAuthenticationErrorCodes.ADMIN_USER_DEACTIVATED]: {
title: `User account deactivated`,
message: () => `User account deactivated. Please contact ${!!SUPPORT_EMAIL ? SUPPORT_EMAIL : "administrator"}.`,
},
};
export const authErrorHandler = (
errorCode: EAuthenticationErrorCodes,
email?: string | undefined
): TAuthErrorInfo | undefined => {
const bannerAlertErrorCodes = [
EAuthenticationErrorCodes.ADMIN_ALREADY_EXIST,
EAuthenticationErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME,
EAuthenticationErrorCodes.INVALID_ADMIN_EMAIL,
EAuthenticationErrorCodes.INVALID_ADMIN_PASSWORD,
EAuthenticationErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD,
EAuthenticationErrorCodes.ADMIN_AUTHENTICATION_FAILED,
EAuthenticationErrorCodes.ADMIN_USER_ALREADY_EXIST,
EAuthenticationErrorCodes.ADMIN_USER_DOES_NOT_EXIST,
EAuthenticationErrorCodes.ADMIN_USER_DEACTIVATED,
];
if (bannerAlertErrorCodes.includes(errorCode))
return {
type: EErrorAlertType.BANNER_ALERT,
code: errorCode,
title: errorCodeMessages[errorCode]?.title || "Error",
message: errorCodeMessages[errorCode]?.message(email) || "Something went wrong. Please try again.",
};
return undefined;
};
export const getBaseAuthenticationModes: (props: TGetBaseAuthenticationModeProps) => TInstanceAuthenticationModes[] = ({
disabled,
updateConfig,
resolvedTheme,
}) => [
{
key: "unique-codes",
name: "Unique codes",
description:
"Log in or sign up for Plane using codes sent via email. You need to have set up SMTP to use this method.",
icon: <Mails className="h-6 w-6 p-0.5 text-custom-text-300/80" />,
config: <EmailCodesConfiguration disabled={disabled} updateConfig={updateConfig} />,
},
{
key: "passwords-login",
name: "Passwords",
description: "Allow members to create accounts with passwords and use it with their email addresses to sign in.",
icon: <KeyRound className="h-6 w-6 p-0.5 text-custom-text-300/80" />,
config: <PasswordLoginConfiguration disabled={disabled} updateConfig={updateConfig} />,
},
{
key: "google",
name: "Google",
description: "Allow members to log in or sign up for Plane with their Google accounts.",
icon: <Image src={GoogleLogo} height={20} width={20} alt="Google Logo" />,
config: <GoogleConfiguration disabled={disabled} updateConfig={updateConfig} />,
},
{
key: "github",
name: "GitHub",
description: "Allow members to log in or sign up for Plane with their GitHub accounts.",
icon: (
<Image
src={resolveGeneralTheme(resolvedTheme) === "dark" ? githubDarkModeImage : githubLightModeImage}
height={20}
width={20}
alt="GitHub Logo"
/>
),
config: <GithubConfiguration disabled={disabled} updateConfig={updateConfig} />,
},
{
key: "gitlab",
name: "GitLab",
description: "Allow members to log in or sign up to plane with their GitLab accounts.",
icon: <Image src={GitlabLogo} height={20} width={20} alt="GitLab Logo" />,
config: <GitlabConfiguration disabled={disabled} updateConfig={updateConfig} />,
},
];

View File

@@ -1,20 +0,0 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || "";
export const ADMIN_BASE_PATH = process.env.NEXT_PUBLIC_ADMIN_BASE_PATH || "";
export const SPACE_BASE_URL = process.env.NEXT_PUBLIC_SPACE_BASE_URL || "";
export const SPACE_BASE_PATH = process.env.NEXT_PUBLIC_SPACE_BASE_PATH || "";
export const WEB_BASE_URL = process.env.NEXT_PUBLIC_WEB_BASE_URL || "";
export const SUPPORT_EMAIL = process.env.NEXT_PUBLIC_SUPPORT_EMAIL || "";
export const ASSET_PREFIX = ADMIN_BASE_PATH;
export const cn = (...inputs: ClassValue[]) => twMerge(clsx(inputs));
export const resolveGeneralTheme = (resolvedTheme: string | undefined) =>
resolvedTheme?.includes("light") ? "light" : resolvedTheme?.includes("dark") ? "dark" : "system";

View File

@@ -1,14 +0,0 @@
// helpers
import { API_BASE_URL } from "@/helpers/common.helper";
/**
* @description combine the file path with the base URL
* @param {string} path
* @returns {string} final URL with the base URL
*/
export const getFileURL = (path: string): string | undefined => {
if (!path) return undefined;
const isValidURL = path.startsWith("http");
if (isValidURL) return path;
return `${API_BASE_URL}${path}`;
};

View File

@@ -1,2 +0,0 @@
export * from "./instance.helper";
export * from "./user.helper";

View File

@@ -1,67 +0,0 @@
import zxcvbn from "zxcvbn";
export enum E_PASSWORD_STRENGTH {
EMPTY = "empty",
LENGTH_NOT_VALID = "length_not_valid",
STRENGTH_NOT_VALID = "strength_not_valid",
STRENGTH_VALID = "strength_valid",
}
const PASSWORD_MIN_LENGTH = 8;
// const PASSWORD_NUMBER_REGEX = /\d/;
// const PASSWORD_CHAR_CAPS_REGEX = /[A-Z]/;
// const PASSWORD_SPECIAL_CHAR_REGEX = /[`!@#$%^&*()_\-+=\[\]{};':"\\|,.<>\/?~ ]/;
export const PASSWORD_CRITERIA = [
{
key: "min_8_char",
label: "Min 8 characters",
isCriteriaValid: (password: string) => password.length >= PASSWORD_MIN_LENGTH,
},
// {
// key: "min_1_upper_case",
// label: "Min 1 upper-case letter",
// isCriteriaValid: (password: string) => PASSWORD_NUMBER_REGEX.test(password),
// },
// {
// key: "min_1_number",
// label: "Min 1 number",
// isCriteriaValid: (password: string) => PASSWORD_CHAR_CAPS_REGEX.test(password),
// },
// {
// key: "min_1_special_char",
// label: "Min 1 special character",
// isCriteriaValid: (password: string) => PASSWORD_SPECIAL_CHAR_REGEX.test(password),
// },
];
export const getPasswordStrength = (password: string): E_PASSWORD_STRENGTH => {
let passwordStrength: E_PASSWORD_STRENGTH = E_PASSWORD_STRENGTH.EMPTY;
if (!password || password === "" || password.length <= 0) {
return passwordStrength;
}
if (password.length >= PASSWORD_MIN_LENGTH) {
passwordStrength = E_PASSWORD_STRENGTH.STRENGTH_NOT_VALID;
} else {
passwordStrength = E_PASSWORD_STRENGTH.LENGTH_NOT_VALID;
return passwordStrength;
}
const passwordCriteriaValidation = PASSWORD_CRITERIA.map((criteria) => criteria.isCriteriaValid(password)).every(
(criterion) => criterion
);
const passwordStrengthScore = zxcvbn(password).score;
if (passwordCriteriaValidation === false || passwordStrengthScore <= 2) {
passwordStrength = E_PASSWORD_STRENGTH.STRENGTH_NOT_VALID;
return passwordStrength;
}
if (passwordCriteriaValidation === true && passwordStrengthScore >= 3) {
passwordStrength = E_PASSWORD_STRENGTH.STRENGTH_VALID;
}
return passwordStrength;
};

View File

@@ -1,21 +0,0 @@
/**
* @description
* This function test whether a URL is valid or not.
*
* It accepts URLs with or without the protocol.
* @param {string} url
* @returns {boolean}
* @example
* checkURLValidity("https://example.com") => true
* checkURLValidity("example.com") => true
* checkURLValidity("example") => false
*/
export const checkURLValidity = (url: string): boolean => {
if (!url) return false;
// regex to support complex query parameters and fragments
const urlPattern =
/^(https?:\/\/)?((([a-z\d-]+\.)*[a-z\d-]+\.[a-z]{2,6})|(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}))(:\d+)?(\/[\w.-]*)*(\?[^#\s]*)?(#[\w-]*)?$/i;
return urlPattern.test(url);
};

View File

@@ -1,6 +1,6 @@
{
"name": "admin",
"version": "0.23.1",
"version": "0.24.1",
"private": true,
"scripts": {
"dev": "turbo run develop",
@@ -14,9 +14,10 @@
"dependencies": {
"@headlessui/react": "^1.7.19",
"@plane/constants": "*",
"@plane/helpers": "*",
"@plane/hooks": "*",
"@plane/types": "*",
"@plane/ui": "*",
"@plane/utils": "*",
"@sentry/nextjs": "^8.32.0",
"@tailwindcss/typography": "^0.5.9",
"@types/lodash": "^4.17.0",
@@ -26,7 +27,7 @@
"lucide-react": "^0.356.0",
"mobx": "^6.12.0",
"mobx-react": "^9.1.1",
"next": "^14.2.12",
"next": "^14.2.20",
"next-themes": "^0.2.1",
"postcss": "^8.4.38",
"react": "^18.3.1",

View File

@@ -5,7 +5,6 @@
"baseUrl": ".",
"paths": {
"@/*": ["core/*"],
"@/helpers/*": ["helpers/*"],
"@/public/*": ["public/*"],
"@/plane-admin/*": ["ce/*"]
}

View File

@@ -4,7 +4,7 @@ FROM python:3.12.5-alpine AS backend
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
ENV INSTANCE_CHANGELOG_URL https://api.plane.so/api/public/anchor/8e1c2e4c7bc5493eb7731be3862f6960/pages/
ENV INSTANCE_CHANGELOG_URL https://sites.plane.so/pages/691ef037bcfe416a902e48cb55f59891/
WORKDIR /code

View File

@@ -4,7 +4,7 @@ FROM python:3.12.5-alpine AS backend
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
ENV INSTANCE_CHANGELOG_URL https://api.plane.so/api/public/anchor/8e1c2e4c7bc5493eb7731be3862f6960/pages/
ENV INSTANCE_CHANGELOG_URL https://sites.plane.so/pages/691ef037bcfe416a902e48cb55f59891/
RUN apk --no-cache add \
"bash~=5.2" \

View File

@@ -26,9 +26,7 @@ def update_description():
updated_issues.append(issue)
Issue.objects.bulk_update(
updated_issues,
["description_html", "description_stripped"],
batch_size=100,
updated_issues, ["description_html", "description_stripped"], batch_size=100
)
print("Success")
except Exception as e:
@@ -42,9 +40,7 @@ def update_comments():
updated_issue_comments = []
for issue_comment in issue_comments:
issue_comment.comment_html = (
f"<p>{issue_comment.comment_stripped}</p>"
)
issue_comment.comment_html = f"<p>{issue_comment.comment_stripped}</p>"
updated_issue_comments.append(issue_comment)
IssueComment.objects.bulk_update(
@@ -103,9 +99,7 @@ def updated_issue_sort_order():
issue.sort_order = issue.sequence_id * random.randint(100, 500)
updated_issues.append(issue)
Issue.objects.bulk_update(
updated_issues, ["sort_order"], batch_size=100
)
Issue.objects.bulk_update(updated_issues, ["sort_order"], batch_size=100)
print("Success")
except Exception as e:
print(e)
@@ -143,9 +137,7 @@ def update_project_cover_images():
project.cover_image = project_cover_images[random.randint(0, 19)]
updated_projects.append(project)
Project.objects.bulk_update(
updated_projects, ["cover_image"], batch_size=100
)
Project.objects.bulk_update(updated_projects, ["cover_image"], batch_size=100)
print("Success")
except Exception as e:
print(e)
@@ -194,9 +186,7 @@ def update_label_color():
def create_slack_integration():
try:
_ = Integration.objects.create(
provider="slack", network=2, title="Slack"
)
_ = Integration.objects.create(provider="slack", network=2, title="Slack")
print("Success")
except Exception as e:
print(e)
@@ -222,16 +212,12 @@ def update_integration_verified():
def update_start_date():
try:
issues = Issue.objects.filter(
state__group__in=["started", "completed"]
)
issues = Issue.objects.filter(state__group__in=["started", "completed"])
updated_issues = []
for issue in issues:
issue.start_date = issue.created_at.date()
updated_issues.append(issue)
Issue.objects.bulk_update(
updated_issues, ["start_date"], batch_size=500
)
Issue.objects.bulk_update(updated_issues, ["start_date"], batch_size=500)
print("Success")
except Exception as e:
print(e)

View File

@@ -3,9 +3,7 @@ import os
import sys
if __name__ == "__main__":
os.environ.setdefault(
"DJANGO_SETTINGS_MODULE", "plane.settings.production"
)
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "plane.settings.production")
try:
from django.core.management import execute_from_command_line
except ImportError as exc:

View File

@@ -1,4 +1,4 @@
{
"name": "plane-api",
"version": "0.23.1"
"version": "0.24.1"
}

View File

@@ -25,10 +25,7 @@ class APIKeyAuthentication(authentication.BaseAuthentication):
def validate_api_token(self, token):
try:
api_token = APIToken.objects.get(
Q(
Q(expired_at__gt=timezone.now())
| Q(expired_at__isnull=True)
),
Q(Q(expired_at__gt=timezone.now()) | Q(expired_at__isnull=True)),
token=token,
is_active=True,
)

View File

@@ -80,4 +80,4 @@ class ServiceTokenRateThrottle(SimpleRateThrottle):
request.META["X-RateLimit-Remaining"] = max(0, available)
request.META["X-RateLimit-Reset"] = reset_time
return allowed
return allowed

View File

@@ -13,9 +13,5 @@ from .issue import (
)
from .state import StateLiteSerializer, StateSerializer
from .cycle import CycleSerializer, CycleIssueSerializer, CycleLiteSerializer
from .module import (
ModuleSerializer,
ModuleIssueSerializer,
ModuleLiteSerializer,
)
from .module import ModuleSerializer, ModuleIssueSerializer, ModuleLiteSerializer
from .intake import IntakeIssueSerializer

View File

@@ -102,8 +102,6 @@ class BaseSerializer(serializers.ModelSerializer):
response[expand] = exp_serializer.data
else:
# You might need to handle this case differently
response[expand] = getattr(
instance, f"{expand}_id", None
)
response[expand] = getattr(instance, f"{expand}_id", None)
return response

View File

@@ -4,7 +4,7 @@ from rest_framework import serializers
# Module imports
from .base import BaseSerializer
from plane.db.models import Cycle, CycleIssue
from plane.utils.timezone_converter import convert_to_utc
class CycleSerializer(BaseSerializer):
total_issues = serializers.IntegerField(read_only=True)
@@ -23,8 +23,18 @@ class CycleSerializer(BaseSerializer):
and data.get("end_date", None) is not None
and data.get("start_date", None) > data.get("end_date", None)
):
raise serializers.ValidationError(
"Start date cannot exceed end date"
raise serializers.ValidationError("Start date cannot exceed end date")
if (
data.get("start_date", None) is not None
and data.get("end_date", None) is not None
):
project_id = self.initial_data.get("project_id") or self.instance.project_id
data["start_date"] = convert_to_utc(
str(data.get("start_date").date()), project_id, is_start_date=True
)
data["end_date"] = convert_to_utc(
str(data.get("end_date", None).date()), project_id
)
return data
@@ -50,11 +60,7 @@ class CycleIssueSerializer(BaseSerializer):
class Meta:
model = CycleIssue
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"cycle",
]
read_only_fields = ["workspace", "project", "cycle"]
class CycleLiteSerializer(BaseSerializer):

View File

@@ -6,7 +6,6 @@ from rest_framework import serializers
class IntakeIssueSerializer(BaseSerializer):
issue_detail = IssueExpandSerializer(read_only=True, source="issue")
inbox = serializers.UUIDField(source="intake.id", read_only=True)

View File

@@ -49,25 +49,13 @@ class IssueSerializer(BaseSerializer):
required=False,
)
type_id = serializers.PrimaryKeyRelatedField(
source="type",
queryset=IssueType.objects.all(),
required=False,
allow_null=True,
source="type", queryset=IssueType.objects.all(), required=False, allow_null=True
)
class Meta:
model = Issue
read_only_fields = [
"id",
"workspace",
"project",
"updated_by",
"updated_at",
]
exclude = [
"description",
"description_stripped",
]
read_only_fields = ["id", "workspace", "project", "updated_by", "updated_at"]
exclude = ["description", "description_stripped"]
def validate(self, data):
if (
@@ -75,9 +63,7 @@ class IssueSerializer(BaseSerializer):
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"
)
raise serializers.ValidationError("Start date cannot exceed target date")
try:
if data.get("description_html", None) is not None:
@@ -99,16 +85,14 @@ class IssueSerializer(BaseSerializer):
# Validate labels are from project
if data.get("labels", []):
data["labels"] = Label.objects.filter(
project_id=self.context.get("project_id"),
id__in=data["labels"],
project_id=self.context.get("project_id"), id__in=data["labels"]
).values_list("id", flat=True)
# Check state is from the project only else raise validation error
if (
data.get("state")
and not State.objects.filter(
project_id=self.context.get("project_id"),
pk=data.get("state").id,
project_id=self.context.get("project_id"), pk=data.get("state").id
).exists()
):
raise serializers.ValidationError(
@@ -119,8 +103,7 @@ class IssueSerializer(BaseSerializer):
if (
data.get("parent")
and not Issue.objects.filter(
workspace_id=self.context.get("workspace_id"),
pk=data.get("parent").id,
workspace_id=self.context.get("workspace_id"), pk=data.get("parent").id
).exists()
):
raise serializers.ValidationError(
@@ -147,9 +130,7 @@ class IssueSerializer(BaseSerializer):
issue_type = issue_type
issue = Issue.objects.create(
**validated_data,
project_id=project_id,
type=issue_type,
**validated_data, project_id=project_id, type=issue_type
)
# Issue Audit Users
@@ -256,20 +237,36 @@ class IssueSerializer(BaseSerializer):
from .user import UserLiteSerializer
data["assignees"] = UserLiteSerializer(
instance.assignees.all(), many=True
User.objects.filter(
pk__in=IssueAssignee.objects.filter(issue=instance).values_list(
"assignee_id", flat=True
)
),
many=True,
).data
else:
data["assignees"] = [
str(assignee.id) for assignee in instance.assignees.all()
str(assignee)
for assignee in IssueAssignee.objects.filter(
issue=instance
).values_list("assignee_id", flat=True)
]
if "labels" in self.fields:
if "labels" in self.expand:
data["labels"] = LabelSerializer(
instance.labels.all(), many=True
Label.objects.filter(
pk__in=IssueLabel.objects.filter(issue=instance).values_list(
"label_id", flat=True
)
),
many=True,
).data
else:
data["labels"] = [
str(label.id) for label in instance.labels.all()
str(label)
for label in IssueLabel.objects.filter(issue=instance).values_list(
"label_id", flat=True
)
]
return data
@@ -278,11 +275,7 @@ class IssueSerializer(BaseSerializer):
class IssueLiteSerializer(BaseSerializer):
class Meta:
model = Issue
fields = [
"id",
"sequence_id",
"project_id",
]
fields = ["id", "sequence_id", "project_id"]
read_only_fields = fields
@@ -334,8 +327,7 @@ class IssueLinkSerializer(BaseSerializer):
# Validation if url already exists
def create(self, validated_data):
if IssueLink.objects.filter(
url=validated_data.get("url"),
issue_id=validated_data.get("issue_id"),
url=validated_data.get("url"), issue_id=validated_data.get("issue_id")
).exists():
raise serializers.ValidationError(
{"error": "URL already exists for this Issue"}
@@ -345,8 +337,7 @@ class IssueLinkSerializer(BaseSerializer):
def update(self, instance, validated_data):
if (
IssueLink.objects.filter(
url=validated_data.get("url"),
issue_id=instance.issue_id,
url=validated_data.get("url"), issue_id=instance.issue_id
)
.exclude(pk=instance.id)
.exists()
@@ -387,10 +378,7 @@ class IssueCommentSerializer(BaseSerializer):
"created_at",
"updated_at",
]
exclude = [
"comment_stripped",
"comment_json",
]
exclude = ["comment_stripped", "comment_json"]
def validate(self, data):
try:
@@ -407,38 +395,27 @@ class IssueCommentSerializer(BaseSerializer):
class IssueActivitySerializer(BaseSerializer):
class Meta:
model = IssueActivity
exclude = [
"created_by",
"updated_by",
]
exclude = ["created_by", "updated_by"]
class CycleIssueSerializer(BaseSerializer):
cycle = CycleSerializer(read_only=True)
class Meta:
fields = [
"cycle",
]
fields = ["cycle"]
class ModuleIssueSerializer(BaseSerializer):
module = ModuleSerializer(read_only=True)
class Meta:
fields = [
"module",
]
fields = ["module"]
class LabelLiteSerializer(BaseSerializer):
class Meta:
model = Label
fields = [
"id",
"name",
"color",
]
fields = ["id", "name", "color"]
class IssueExpandSerializer(BaseSerializer):

View File

@@ -53,14 +53,11 @@ class ModuleSerializer(BaseSerializer):
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"
)
raise serializers.ValidationError("Start date cannot exceed target date")
if data.get("members", []):
data["members"] = ProjectMember.objects.filter(
project_id=self.context.get("project_id"),
member_id__in=data["members"],
project_id=self.context.get("project_id"), member_id__in=data["members"]
).values_list("member_id", flat=True)
return data
@@ -74,9 +71,7 @@ class ModuleSerializer(BaseSerializer):
module_name = validated_data.get("name")
if module_name:
# Lookup for the module name in the module table for that project
if Module.objects.filter(
name=module_name, project_id=project_id
).exists():
if Module.objects.filter(name=module_name, project_id=project_id).exists():
raise serializers.ValidationError(
{"error": "Module with this name already exists"}
)
@@ -107,9 +102,7 @@ class ModuleSerializer(BaseSerializer):
if module_name:
# Lookup for the module name in the module table for that project
if (
Module.objects.filter(
name=module_name, project=instance.project
)
Module.objects.filter(name=module_name, project=instance.project)
.exclude(id=instance.id)
.exists()
):
@@ -172,8 +165,7 @@ class ModuleLinkSerializer(BaseSerializer):
# Validation if url already exists
def create(self, validated_data):
if ModuleLink.objects.filter(
url=validated_data.get("url"),
module_id=validated_data.get("module_id"),
url=validated_data.get("url"), module_id=validated_data.get("module_id")
).exists():
raise serializers.ValidationError(
{"error": "URL already exists for this Issue"}

View File

@@ -2,11 +2,7 @@
from rest_framework import serializers
# Module imports
from plane.db.models import (
Project,
ProjectIdentifier,
WorkspaceMember,
)
from plane.db.models import Project, ProjectIdentifier, WorkspaceMember
from .base import BaseSerializer
@@ -67,16 +63,12 @@ class ProjectSerializer(BaseSerializer):
def create(self, validated_data):
identifier = validated_data.get("identifier", "").strip().upper()
if identifier == "":
raise serializers.ValidationError(
detail="Project Identifier is required"
)
raise serializers.ValidationError(detail="Project Identifier is required")
if ProjectIdentifier.objects.filter(
name=identifier, workspace_id=self.context["workspace_id"]
).exists():
raise serializers.ValidationError(
detail="Project Identifier is taken"
)
raise serializers.ValidationError(detail="Project Identifier is taken")
project = Project.objects.create(
**validated_data, workspace_id=self.context["workspace_id"]

View File

@@ -7,9 +7,9 @@ class StateSerializer(BaseSerializer):
def validate(self, data):
# If the default is being provided then make all other states default False
if data.get("default", False):
State.objects.filter(
project_id=self.context.get("project_id")
).update(default=False)
State.objects.filter(project_id=self.context.get("project_id")).update(
default=False
)
return data
class Meta:
@@ -30,10 +30,5 @@ class StateSerializer(BaseSerializer):
class StateLiteSerializer(BaseSerializer):
class Meta:
model = State
fields = [
"id",
"name",
"color",
"group",
]
fields = ["id", "name", "color", "group"]
read_only_fields = fields

View File

@@ -8,9 +8,5 @@ class WorkspaceLiteSerializer(BaseSerializer):
class Meta:
model = Workspace
fields = [
"name",
"slug",
"id",
]
fields = ["name", "slug", "id"]
read_only_fields = fields

View File

@@ -1,13 +1,11 @@
from django.urls import path
from plane.api.views import (
ProjectMemberAPIEndpoint,
)
from plane.api.views import ProjectMemberAPIEndpoint
urlpatterns = [
path(
"workspaces/<str:slug>/projects/<str:project_id>/members/",
ProjectMemberAPIEndpoint.as_view(),
name="users",
),
)
]

View File

@@ -1,15 +1,10 @@
from django.urls import path
from plane.api.views import (
ProjectAPIEndpoint,
ProjectArchiveUnarchiveAPIEndpoint,
)
from plane.api.views import ProjectAPIEndpoint, ProjectArchiveUnarchiveAPIEndpoint
urlpatterns = [
path(
"workspaces/<str:slug>/projects/",
ProjectAPIEndpoint.as_view(),
name="project",
"workspaces/<str:slug>/projects/", ProjectAPIEndpoint.as_view(), name="project"
),
path(
"workspaces/<str:slug>/projects/<uuid:pk>/",

View File

@@ -37,13 +37,9 @@ class TimezoneMixin:
class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
authentication_classes = [
APIKeyAuthentication,
]
authentication_classes = [APIKeyAuthentication]
permission_classes = [
IsAuthenticated,
]
permission_classes = [IsAuthenticated]
def filter_queryset(self, queryset):
for backend in list(self.filter_backends):
@@ -56,8 +52,7 @@ class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
if api_key:
service_token = APIToken.objects.filter(
token=api_key,
is_service=True,
token=api_key, is_service=True
).first()
if service_token:
@@ -123,9 +118,7 @@ class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
def finalize_response(self, request, response, *args, **kwargs):
# Call super to get the default response
response = super().finalize_response(
request, response, *args, **kwargs
)
response = super().finalize_response(request, response, *args, **kwargs)
# Add custom headers if they exist in the request META
ratelimit_remaining = request.META.get("X-RateLimit-Remaining")
@@ -154,17 +147,13 @@ class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
@property
def fields(self):
fields = [
field
for field in self.request.GET.get("fields", "").split(",")
if field
field for field in self.request.GET.get("fields", "").split(",") if field
]
return fields if fields else None
@property
def expand(self):
expand = [
expand
for expand in self.request.GET.get("expand", "").split(",")
if expand
expand for expand in self.request.GET.get("expand", "").split(",") if expand
]
return expand if expand else None
return expand if expand else None

View File

@@ -25,10 +25,7 @@ from rest_framework import status
from rest_framework.response import Response
# Module imports
from plane.api.serializers import (
CycleIssueSerializer,
CycleSerializer,
)
from plane.api.serializers import CycleIssueSerializer, CycleSerializer
from plane.app.permissions import ProjectEntityPermission
from plane.bgtasks.issue_activities_task import issue_activity
from plane.db.models import (
@@ -57,9 +54,7 @@ class CycleAPIEndpoint(BaseAPIView):
serializer_class = CycleSerializer
model = Cycle
webhook_event = "cycle"
permission_classes = [
ProjectEntityPermission,
]
permission_classes = [ProjectEntityPermission]
def get_queryset(self):
return (
@@ -143,26 +138,18 @@ class CycleAPIEndpoint(BaseAPIView):
def get(self, request, slug, project_id, pk=None):
if pk:
queryset = (
self.get_queryset().filter(archived_at__isnull=True).get(pk=pk)
)
queryset = self.get_queryset().filter(archived_at__isnull=True).get(pk=pk)
data = CycleSerializer(
queryset,
fields=self.fields,
expand=self.expand,
queryset, fields=self.fields, expand=self.expand
).data
return Response(
data,
status=status.HTTP_200_OK,
)
return Response(data, status=status.HTTP_200_OK)
queryset = self.get_queryset().filter(archived_at__isnull=True)
cycle_view = request.GET.get("cycle_view", "all")
# Current Cycle
if cycle_view == "current":
queryset = queryset.filter(
start_date__lte=timezone.now(),
end_date__gte=timezone.now(),
start_date__lte=timezone.now(), end_date__gte=timezone.now()
)
data = CycleSerializer(
queryset, many=True, fields=self.fields, expand=self.expand
@@ -176,10 +163,7 @@ class CycleAPIEndpoint(BaseAPIView):
request=request,
queryset=(queryset),
on_results=lambda cycles: CycleSerializer(
cycles,
many=True,
fields=self.fields,
expand=self.expand,
cycles, many=True, fields=self.fields, expand=self.expand
).data,
)
@@ -190,53 +174,38 @@ class CycleAPIEndpoint(BaseAPIView):
request=request,
queryset=(queryset),
on_results=lambda cycles: CycleSerializer(
cycles,
many=True,
fields=self.fields,
expand=self.expand,
cycles, many=True, fields=self.fields, expand=self.expand
).data,
)
# Draft Cycles
if cycle_view == "draft":
queryset = queryset.filter(
end_date=None,
start_date=None,
)
queryset = queryset.filter(end_date=None, start_date=None)
return self.paginate(
request=request,
queryset=(queryset),
on_results=lambda cycles: CycleSerializer(
cycles,
many=True,
fields=self.fields,
expand=self.expand,
cycles, many=True, fields=self.fields, expand=self.expand
).data,
)
# Incomplete Cycles
if cycle_view == "incomplete":
queryset = queryset.filter(
Q(end_date__gte=timezone.now()) | Q(end_date__isnull=True),
Q(end_date__gte=timezone.now()) | Q(end_date__isnull=True)
)
return self.paginate(
request=request,
queryset=(queryset),
on_results=lambda cycles: CycleSerializer(
cycles,
many=True,
fields=self.fields,
expand=self.expand,
cycles, many=True, fields=self.fields, expand=self.expand
).data,
)
return self.paginate(
request=request,
queryset=(queryset),
on_results=lambda cycles: CycleSerializer(
cycles,
many=True,
fields=self.fields,
expand=self.expand,
cycles, many=True, fields=self.fields, expand=self.expand
).data,
)
@@ -273,10 +242,7 @@ class CycleAPIEndpoint(BaseAPIView):
},
status=status.HTTP_409_CONFLICT,
)
serializer.save(
project_id=project_id,
owned_by=request.user,
)
serializer.save(project_id=project_id, owned_by=request.user)
# Send the model activity
model_activity.delay(
model_name="cycle",
@@ -287,12 +253,8 @@ class CycleAPIEndpoint(BaseAPIView):
slug=slug,
origin=request.META.get("HTTP_ORIGIN"),
)
return Response(
serializer.data, status=status.HTTP_201_CREATED
)
return Response(
serializer.errors, status=status.HTTP_400_BAD_REQUEST
)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
else:
return Response(
{
@@ -302,9 +264,7 @@ class CycleAPIEndpoint(BaseAPIView):
)
def patch(self, request, slug, project_id, pk):
cycle = Cycle.objects.get(
workspace__slug=slug, project_id=project_id, pk=pk
)
cycle = Cycle.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
current_instance = json.dumps(
CycleSerializer(cycle).data, cls=DjangoJSONEncoder
@@ -322,9 +282,7 @@ class CycleAPIEndpoint(BaseAPIView):
if "sort_order" in request_data:
# Can only change sort order
request_data = {
"sort_order": request_data.get(
"sort_order", cycle.sort_order
)
"sort_order": request_data.get("sort_order", cycle.sort_order)
}
else:
return Response(
@@ -371,9 +329,7 @@ class CycleAPIEndpoint(BaseAPIView):
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def delete(self, request, slug, project_id, pk):
cycle = Cycle.objects.get(
workspace__slug=slug, project_id=project_id, pk=pk
)
cycle = Cycle.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
if cycle.owned_by_id != request.user.id and (
not ProjectMember.objects.filter(
workspace__slug=slug,
@@ -389,9 +345,9 @@ class CycleAPIEndpoint(BaseAPIView):
)
cycle_issues = list(
CycleIssue.objects.filter(
cycle_id=self.kwargs.get("pk")
).values_list("issue", flat=True)
CycleIssue.objects.filter(cycle_id=self.kwargs.get("pk")).values_list(
"issue", flat=True
)
)
issue_activity.delay(
@@ -413,17 +369,13 @@ class CycleAPIEndpoint(BaseAPIView):
cycle.delete()
# Delete the user favorite cycle
UserFavorite.objects.filter(
entity_type="cycle",
entity_identifier=pk,
project_id=project_id,
entity_type="cycle", entity_identifier=pk, project_id=project_id
).delete()
return Response(status=status.HTTP_204_NO_CONTENT)
class CycleArchiveUnarchiveAPIEndpoint(BaseAPIView):
permission_classes = [
ProjectEntityPermission,
]
permission_classes = [ProjectEntityPermission]
def get_queryset(self):
return (
@@ -502,9 +454,7 @@ class CycleArchiveUnarchiveAPIEndpoint(BaseAPIView):
),
)
)
.annotate(
total_estimates=Sum("issue_cycle__issue__estimate_point")
)
.annotate(total_estimates=Sum("issue_cycle__issue__estimate_point"))
.annotate(
completed_estimates=Sum(
"issue_cycle__issue__estimate_point",
@@ -536,10 +486,7 @@ class CycleArchiveUnarchiveAPIEndpoint(BaseAPIView):
request=request,
queryset=(self.get_queryset()),
on_results=lambda cycles: CycleSerializer(
cycles,
many=True,
fields=self.fields,
expand=self.expand,
cycles, many=True, fields=self.fields, expand=self.expand
).data,
)
@@ -582,16 +529,12 @@ class CycleIssueAPIEndpoint(BaseAPIView):
model = CycleIssue
webhook_event = "cycle_issue"
bulk = True
permission_classes = [
ProjectEntityPermission,
]
permission_classes = [ProjectEntityPermission]
def get_queryset(self):
return (
CycleIssue.objects.annotate(
sub_issues_count=Issue.issue_objects.filter(
parent=OuterRef("issue_id")
)
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("issue_id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
@@ -630,13 +573,10 @@ class CycleIssueAPIEndpoint(BaseAPIView):
order_by = request.GET.get("order_by", "created_at")
issues = (
Issue.issue_objects.filter(
issue_cycle__cycle_id=cycle_id,
issue_cycle__deleted_at__isnull=True,
issue_cycle__cycle_id=cycle_id, issue_cycle__deleted_at__isnull=True
)
.annotate(
sub_issues_count=Issue.issue_objects.filter(
parent=OuterRef("id")
)
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
@@ -672,10 +612,7 @@ class CycleIssueAPIEndpoint(BaseAPIView):
request=request,
queryset=(issues),
on_results=lambda issues: CycleSerializer(
issues,
many=True,
fields=self.fields,
expand=self.expand,
issues, many=True, fields=self.fields, expand=self.expand
).data,
)
@@ -684,8 +621,7 @@ class CycleIssueAPIEndpoint(BaseAPIView):
if not issues:
return Response(
{"error": "Issues are required"},
status=status.HTTP_400_BAD_REQUEST,
{"error": "Issues are required"}, status=status.HTTP_400_BAD_REQUEST
)
cycle = Cycle.objects.get(
@@ -694,9 +630,7 @@ class CycleIssueAPIEndpoint(BaseAPIView):
# Get all CycleIssues already created
cycle_issues = list(
CycleIssue.objects.filter(
~Q(cycle_id=cycle_id), issue_id__in=issues
)
CycleIssue.objects.filter(~Q(cycle_id=cycle_id), issue_id__in=issues)
)
existing_issues = [
@@ -741,9 +675,7 @@ class CycleIssueAPIEndpoint(BaseAPIView):
)
# Update the cycle issues
CycleIssue.objects.bulk_update(
updated_records, ["cycle_id"], batch_size=100
)
CycleIssue.objects.bulk_update(updated_records, ["cycle_id"], batch_size=100)
# Capture Issue Activity
issue_activity.delay(
@@ -802,9 +734,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
"""
permission_classes = [
ProjectEntityPermission,
]
permission_classes = [ProjectEntityPermission]
def post(self, request, slug, project_id, cycle_id):
new_cycle_id = request.data.get("new_cycle_id", False)
@@ -930,9 +860,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
)
.values("display_name", "assignee_id", "avatar", "avatar_url")
.annotate(
total_estimates=Sum(
Cast("estimate_point__value", FloatField())
)
total_estimates=Sum(Cast("estimate_point__value", FloatField()))
)
.annotate(
completed_estimates=Sum(
@@ -961,9 +889,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
{
"display_name": item["display_name"],
"assignee_id": (
str(item["assignee_id"])
if item["assignee_id"]
else None
str(item["assignee_id"]) if item["assignee_id"] else None
),
"avatar": item.get("avatar", None),
"avatar_url": item.get("avatar_url", None),
@@ -986,9 +912,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
.annotate(label_id=F("labels__id"))
.values("label_name", "color", "label_id")
.annotate(
total_estimates=Sum(
Cast("estimate_point__value", FloatField())
)
total_estimates=Sum(Cast("estimate_point__value", FloatField()))
)
.annotate(
completed_estimates=Sum(
@@ -1025,9 +949,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
{
"label_name": item["label_name"],
"color": item["color"],
"label_id": (
str(item["label_id"]) if item["label_id"] else None
),
"label_id": (str(item["label_id"]) if item["label_id"] else None),
"total_estimates": item["total_estimates"],
"completed_estimates": item["completed_estimates"],
"pending_estimates": item["pending_estimates"],
@@ -1059,8 +981,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
),
# If `avatar_asset` is None, fall back to using `avatar` field directly
When(
assignees__avatar_asset__isnull=True,
then="assignees__avatar",
assignees__avatar_asset__isnull=True, then="assignees__avatar"
),
default=Value(None),
output_field=models.CharField(),
@@ -1069,12 +990,8 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
.values("display_name", "assignee_id", "avatar_url")
.annotate(
total_issues=Count(
"id",
filter=Q(
archived_at__isnull=True,
is_draft=False,
),
),
"id", filter=Q(archived_at__isnull=True, is_draft=False)
)
)
.annotate(
completed_issues=Count(
@@ -1128,12 +1045,8 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
.values("label_name", "color", "label_id")
.annotate(
total_issues=Count(
"id",
filter=Q(
archived_at__isnull=True,
is_draft=False,
),
),
"id", filter=Q(archived_at__isnull=True, is_draft=False)
)
)
.annotate(
completed_issues=Count(
@@ -1163,9 +1076,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
{
"label_name": item["label_name"],
"color": item["color"],
"label_id": (
str(item["label_id"]) if item["label_id"] else None
),
"label_id": (str(item["label_id"]) if item["label_id"] else None),
"total_issues": item["total_issues"],
"completed_issues": item["completed_issues"],
"pending_issues": item["pending_issues"],
@@ -1210,10 +1121,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
}
current_cycle.save(update_fields=["progress_snapshot"])
if (
new_cycle.end_date is not None
and new_cycle.end_date < timezone.now()
):
if new_cycle.end_date is not None and new_cycle.end_date < timezone.now():
return Response(
{
"error": "The cycle where the issues are transferred is already completed"

View File

@@ -17,14 +17,7 @@ from rest_framework.response import Response
from plane.api.serializers import IntakeIssueSerializer, IssueSerializer
from plane.app.permissions import ProjectLitePermission
from plane.bgtasks.issue_activities_task import issue_activity
from plane.db.models import (
Intake,
IntakeIssue,
Issue,
Project,
ProjectMember,
State,
)
from plane.db.models import Intake, IntakeIssue, Issue, Project, ProjectMember, State
from .base import BaseAPIView
@@ -36,16 +29,12 @@ class IntakeIssueAPIEndpoint(BaseAPIView):
"""
permission_classes = [
ProjectLitePermission,
]
permission_classes = [ProjectLitePermission]
serializer_class = IntakeIssueSerializer
model = IntakeIssue
filterset_fields = [
"status",
]
filterset_fields = ["status"]
def get_queryset(self):
intake = Intake.objects.filter(
@@ -54,8 +43,7 @@ class IntakeIssueAPIEndpoint(BaseAPIView):
).first()
project = Project.objects.get(
workspace__slug=self.kwargs.get("slug"),
pk=self.kwargs.get("project_id"),
workspace__slug=self.kwargs.get("slug"), pk=self.kwargs.get("project_id")
)
if intake is None and not project.intake_view:
@@ -63,8 +51,7 @@ class IntakeIssueAPIEndpoint(BaseAPIView):
return (
IntakeIssue.objects.filter(
Q(snoozed_till__gte=timezone.now())
| Q(snoozed_till__isnull=True),
Q(snoozed_till__gte=timezone.now()) | Q(snoozed_till__isnull=True),
workspace__slug=self.kwargs.get("slug"),
project_id=self.kwargs.get("project_id"),
intake_id=intake.id,
@@ -77,41 +64,29 @@ class IntakeIssueAPIEndpoint(BaseAPIView):
if issue_id:
intake_issue_queryset = self.get_queryset().get(issue_id=issue_id)
intake_issue_data = IntakeIssueSerializer(
intake_issue_queryset,
fields=self.fields,
expand=self.expand,
intake_issue_queryset, fields=self.fields, expand=self.expand
).data
return Response(
intake_issue_data,
status=status.HTTP_200_OK,
)
return Response(intake_issue_data, status=status.HTTP_200_OK)
issue_queryset = self.get_queryset()
return self.paginate(
request=request,
queryset=(issue_queryset),
on_results=lambda intake_issues: IntakeIssueSerializer(
intake_issues,
many=True,
fields=self.fields,
expand=self.expand,
intake_issues, many=True, fields=self.fields, expand=self.expand
).data,
)
def post(self, request, slug, project_id):
if not request.data.get("issue", {}).get("name", False):
return Response(
{"error": "Name is required"},
status=status.HTTP_400_BAD_REQUEST,
{"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST
)
intake = Intake.objects.filter(
workspace__slug=slug, project_id=project_id
).first()
project = Project.objects.get(
workspace__slug=slug,
pk=project_id,
)
project = Project.objects.get(workspace__slug=slug, pk=project_id)
# Intake view
if intake is None and not project.intake_view:
@@ -131,20 +106,9 @@ class IntakeIssueAPIEndpoint(BaseAPIView):
"none",
]:
return Response(
{"error": "Invalid priority"},
status=status.HTTP_400_BAD_REQUEST,
{"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST
)
# Create or get state
state, _ = State.objects.get_or_create(
name="Triage",
group="triage",
description="Default state for managing all Intake Issues",
project_id=project_id,
color="#ff7700",
is_triage=True,
)
# create an issue
issue = Issue.objects.create(
name=request.data.get("issue", {}).get("name"),
@@ -154,7 +118,6 @@ class IntakeIssueAPIEndpoint(BaseAPIView):
),
priority=request.data.get("issue", {}).get("priority", "none"),
project_id=project_id,
state=state,
)
# create an intake issue
@@ -184,10 +147,7 @@ class IntakeIssueAPIEndpoint(BaseAPIView):
workspace__slug=slug, project_id=project_id
).first()
project = Project.objects.get(
workspace__slug=slug,
pk=project_id,
)
project = Project.objects.get(workspace__slug=slug, pk=project_id)
# Intake view
if intake is None and not project.intake_view:
@@ -234,7 +194,7 @@ class IntakeIssueAPIEndpoint(BaseAPIView):
distinct=True,
filter=Q(
~Q(labels__id__isnull=True)
& Q(label_issue__deleted_at__isnull=True),
& Q(label_issue__deleted_at__isnull=True)
),
),
Value([], output_field=ArrayField(UUIDField())),
@@ -251,11 +211,7 @@ class IntakeIssueAPIEndpoint(BaseAPIView):
),
Value([], output_field=ArrayField(UUIDField())),
),
).get(
pk=issue_id,
workspace__slug=slug,
project_id=project_id,
)
).get(pk=issue_id, workspace__slug=slug, project_id=project_id)
# Only allow guests to edit name and description
if project_member.role <= 5:
issue_data = {
@@ -263,14 +219,10 @@ class IntakeIssueAPIEndpoint(BaseAPIView):
"description_html": issue_data.get(
"description_html", issue.description_html
),
"description": issue_data.get(
"description", issue.description
),
"description": issue_data.get("description", issue.description),
}
issue_serializer = IssueSerializer(
issue, data=issue_data, partial=True
)
issue_serializer = IssueSerializer(issue, data=issue_data, partial=True)
if issue_serializer.is_valid():
current_instance = issue
@@ -310,14 +262,10 @@ class IntakeIssueAPIEndpoint(BaseAPIView):
# Update the issue state if the issue is rejected or marked as duplicate
if serializer.data["status"] in [-1, 2]:
issue = Issue.objects.get(
pk=issue_id,
workspace__slug=slug,
project_id=project_id,
pk=issue_id, workspace__slug=slug, project_id=project_id
)
state = State.objects.filter(
group="cancelled",
workspace__slug=slug,
project_id=project_id,
group="cancelled", workspace__slug=slug, project_id=project_id
).first()
if state is not None:
issue.state = state
@@ -326,18 +274,14 @@ class IntakeIssueAPIEndpoint(BaseAPIView):
# Update the issue state if it is accepted
if serializer.data["status"] in [1]:
issue = Issue.objects.get(
pk=issue_id,
workspace__slug=slug,
project_id=project_id,
pk=issue_id, workspace__slug=slug, project_id=project_id
)
# Update the issue state only if it is in triage state
if issue.state.is_triage:
# Move to default state
state = State.objects.filter(
workspace__slug=slug,
project_id=project_id,
default=True,
workspace__slug=slug, project_id=project_id, default=True
).first()
if state is not None:
issue.state = state
@@ -346,9 +290,7 @@ class IntakeIssueAPIEndpoint(BaseAPIView):
# create a activity for status change
issue_activity.delay(
type="intake.activity.created",
requested_data=json.dumps(
request.data, cls=DjangoJSONEncoder
),
requested_data=json.dumps(request.data, cls=DjangoJSONEncoder),
actor_id=str(request.user.id),
issue_id=str(issue_id),
project_id=str(project_id),
@@ -360,13 +302,10 @@ class IntakeIssueAPIEndpoint(BaseAPIView):
)
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(
serializer.errors, status=status.HTTP_400_BAD_REQUEST
)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
else:
return Response(
IntakeIssueSerializer(intake_issue).data,
status=status.HTTP_200_OK,
IntakeIssueSerializer(intake_issue).data, status=status.HTTP_200_OK
)
def delete(self, request, slug, project_id, issue_id):
@@ -374,10 +313,7 @@ class IntakeIssueAPIEndpoint(BaseAPIView):
workspace__slug=slug, project_id=project_id
).first()
project = Project.objects.get(
workspace__slug=slug,
pk=project_id,
)
project = Project.objects.get(workspace__slug=slug, pk=project_id)
# Intake view
if intake is None and not project.intake_view:

View File

@@ -1,9 +1,10 @@
# Python imports
import json
from django.core.serializers.json import DjangoJSONEncoder
import uuid
# Django imports
from django.core.serializers.json import DjangoJSONEncoder
from django.http import HttpResponseRedirect
from django.db import IntegrityError
from django.db.models import (
Case,
@@ -19,11 +20,11 @@ from django.db.models import (
Subquery,
)
from django.utils import timezone
from django.conf import settings
# Third party imports
from rest_framework import status
from rest_framework.response import Response
from rest_framework.parsers import MultiPartParser, FormParser
# Module imports
from plane.api.serializers import (
@@ -50,8 +51,10 @@ from plane.db.models import (
Project,
ProjectMember,
CycleIssue,
Workspace,
)
from plane.settings.storage import S3Storage
from plane.bgtasks.storage_metadata_task import get_asset_object_metadata
from .base import BaseAPIView
@@ -73,9 +76,7 @@ class WorkspaceIssueAPIEndpoint(BaseAPIView):
def get_queryset(self):
return (
Issue.issue_objects.annotate(
sub_issues_count=Issue.issue_objects.filter(
parent=OuterRef("id")
)
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
@@ -91,14 +92,10 @@ class WorkspaceIssueAPIEndpoint(BaseAPIView):
.order_by(self.kwargs.get("order_by", "-created_at"))
).distinct()
def get(
self, request, slug, project__identifier=None, issue__identifier=None
):
def get(self, request, slug, project__identifier=None, issue__identifier=None):
if issue__identifier and project__identifier:
issue = Issue.issue_objects.annotate(
sub_issues_count=Issue.issue_objects.filter(
parent=OuterRef("id")
)
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
@@ -108,11 +105,7 @@ class WorkspaceIssueAPIEndpoint(BaseAPIView):
sequence_id=issue__identifier,
)
return Response(
IssueSerializer(
issue,
fields=self.fields,
expand=self.expand,
).data,
IssueSerializer(issue, fields=self.fields, expand=self.expand).data,
status=status.HTTP_200_OK,
)
@@ -126,17 +119,13 @@ class IssueAPIEndpoint(BaseAPIView):
model = Issue
webhook_event = "issue"
permission_classes = [
ProjectEntityPermission,
]
permission_classes = [ProjectEntityPermission]
serializer_class = IssueSerializer
def get_queryset(self):
return (
Issue.issue_objects.annotate(
sub_issues_count=Issue.issue_objects.filter(
parent=OuterRef("id")
)
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
@@ -164,41 +153,25 @@ class IssueAPIEndpoint(BaseAPIView):
project_id=project_id,
)
return Response(
IssueSerializer(
issue,
fields=self.fields,
expand=self.expand,
).data,
IssueSerializer(issue, fields=self.fields, expand=self.expand).data,
status=status.HTTP_200_OK,
)
if pk:
issue = Issue.issue_objects.annotate(
sub_issues_count=Issue.issue_objects.filter(
parent=OuterRef("id")
)
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
).get(workspace__slug=slug, project_id=project_id, pk=pk)
return Response(
IssueSerializer(
issue,
fields=self.fields,
expand=self.expand,
).data,
IssueSerializer(issue, fields=self.fields, expand=self.expand).data,
status=status.HTTP_200_OK,
)
# Custom ordering for priority and state
priority_order = ["urgent", "high", "medium", "low", "none"]
state_order = [
"backlog",
"unstarted",
"started",
"completed",
"cancelled",
]
state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
order_by_param = request.GET.get("order_by", "-created_at")
@@ -231,9 +204,7 @@ class IssueAPIEndpoint(BaseAPIView):
# 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]
priority_order if order_by_param == "priority" else priority_order[::-1]
)
issue_queryset = issue_queryset.annotate(
priority_order=Case(
@@ -281,9 +252,7 @@ class IssueAPIEndpoint(BaseAPIView):
else order_by_param
)
).order_by(
"-max_values"
if order_by_param.startswith("-")
else "max_values"
"-max_values" if order_by_param.startswith("-") else "max_values"
)
else:
issue_queryset = issue_queryset.order_by(order_by_param)
@@ -292,10 +261,7 @@ class IssueAPIEndpoint(BaseAPIView):
request=request,
queryset=(issue_queryset),
on_results=lambda issues: IssueSerializer(
issues,
many=True,
fields=self.fields,
expand=self.expand,
issues, many=True, fields=self.fields, expand=self.expand
).data,
)
@@ -339,22 +305,16 @@ class IssueAPIEndpoint(BaseAPIView):
serializer.save()
# Refetch the issue
issue = Issue.objects.filter(
workspace__slug=slug,
project_id=project_id,
pk=serializer.data["id"],
workspace__slug=slug, project_id=project_id, pk=serializer.data["id"]
).first()
issue.created_at = request.data.get("created_at", timezone.now())
issue.created_by_id = request.data.get(
"created_by", request.user.id
)
issue.created_by_id = request.data.get("created_by", request.user.id)
issue.save(update_fields=["created_at", "created_by"])
# Track the issue
issue_activity.delay(
type="issue.activity.created",
requested_data=json.dumps(
self.request.data, cls=DjangoJSONEncoder
),
requested_data=json.dumps(self.request.data, cls=DjangoJSONEncoder),
actor_id=str(request.user.id),
issue_id=str(serializer.data.get("id", None)),
project_id=str(project_id),
@@ -391,9 +351,7 @@ class IssueAPIEndpoint(BaseAPIView):
# Get the requested data, encode it as django object and pass it
# to serializer to validation
requested_data = json.dumps(
self.request.data, cls=DjangoJSONEncoder
)
requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder)
serializer = IssueSerializer(
issue,
data=request.data,
@@ -451,9 +409,7 @@ class IssueAPIEndpoint(BaseAPIView):
# If any of the created_at or created_by is present, update
# the issue with the provided data, else return with the
# default states given.
issue.created_at = request.data.get(
"created_at", timezone.now()
)
issue.created_at = request.data.get("created_at", timezone.now())
issue.created_by_id = request.data.get(
"created_by", request.user.id
)
@@ -470,12 +426,8 @@ class IssueAPIEndpoint(BaseAPIView):
current_instance=None,
epoch=int(timezone.now().timestamp()),
)
return Response(
serializer.data, status=status.HTTP_201_CREATED
)
return Response(
serializer.errors, status=status.HTTP_400_BAD_REQUEST
)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
else:
return Response(
{"error": "external_id and external_source are required"},
@@ -483,9 +435,7 @@ class IssueAPIEndpoint(BaseAPIView):
)
def patch(self, request, slug, project_id, pk=None):
issue = Issue.objects.get(
workspace__slug=slug, project_id=project_id, pk=pk
)
issue = Issue.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
project = Project.objects.get(pk=project_id)
current_instance = json.dumps(
IssueSerializer(issue).data, cls=DjangoJSONEncoder
@@ -494,10 +444,7 @@ class IssueAPIEndpoint(BaseAPIView):
serializer = IssueSerializer(
issue,
data=request.data,
context={
"project_id": project_id,
"workspace_id": project.workspace_id,
},
context={"project_id": project_id, "workspace_id": project.workspace_id},
partial=True,
)
if serializer.is_valid():
@@ -535,9 +482,7 @@ class IssueAPIEndpoint(BaseAPIView):
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def delete(self, request, slug, project_id, pk=None):
issue = Issue.objects.get(
workspace__slug=slug, project_id=project_id, pk=pk
)
issue = Issue.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
if issue.created_by_id != request.user.id and (
not ProjectMember.objects.filter(
workspace__slug=slug,
@@ -576,9 +521,7 @@ class LabelAPIEndpoint(BaseAPIView):
serializer_class = LabelSerializer
model = Label
permission_classes = [
ProjectMemberPermission,
]
permission_classes = [ProjectMemberPermission]
def get_queryset(self):
return (
@@ -625,12 +568,8 @@ class LabelAPIEndpoint(BaseAPIView):
)
serializer.save(project_id=project_id)
return Response(
serializer.data, status=status.HTTP_201_CREATED
)
return Response(
serializer.errors, status=status.HTTP_400_BAD_REQUEST
)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except IntegrityError:
label = Label.objects.filter(
workspace__slug=slug,
@@ -651,18 +590,11 @@ class LabelAPIEndpoint(BaseAPIView):
request=request,
queryset=(self.get_queryset()),
on_results=lambda labels: LabelSerializer(
labels,
many=True,
fields=self.fields,
expand=self.expand,
labels, many=True, fields=self.fields, expand=self.expand
).data,
)
label = self.get_queryset().get(pk=pk)
serializer = LabelSerializer(
label,
fields=self.fields,
expand=self.expand,
)
serializer = LabelSerializer(label, fields=self.fields, expand=self.expand)
return Response(serializer.data, status=status.HTTP_200_OK)
def patch(self, request, slug, project_id, pk=None):
@@ -705,9 +637,7 @@ class IssueLinkAPIEndpoint(BaseAPIView):
"""
permission_classes = [
ProjectEntityPermission,
]
permission_classes = [ProjectEntityPermission]
model = IssueLink
serializer_class = IssueLinkSerializer
@@ -730,46 +660,32 @@ class IssueLinkAPIEndpoint(BaseAPIView):
if pk is None:
issue_links = self.get_queryset()
serializer = IssueLinkSerializer(
issue_links,
fields=self.fields,
expand=self.expand,
issue_links, fields=self.fields, expand=self.expand
)
return self.paginate(
request=request,
queryset=(self.get_queryset()),
on_results=lambda issue_links: IssueLinkSerializer(
issue_links,
many=True,
fields=self.fields,
expand=self.expand,
issue_links, many=True, fields=self.fields, expand=self.expand
).data,
)
issue_link = self.get_queryset().get(pk=pk)
serializer = IssueLinkSerializer(
issue_link,
fields=self.fields,
expand=self.expand,
issue_link, fields=self.fields, expand=self.expand
)
return Response(serializer.data, status=status.HTTP_200_OK)
def post(self, request, slug, project_id, issue_id):
serializer = IssueLinkSerializer(data=request.data)
if serializer.is_valid():
serializer.save(
project_id=project_id,
issue_id=issue_id,
)
serializer.save(project_id=project_id, issue_id=issue_id)
link = IssueLink.objects.get(pk=serializer.data["id"])
link.created_by_id = request.data.get(
"created_by", request.user.id
)
link.created_by_id = request.data.get("created_by", request.user.id)
link.save(update_fields=["created_by"])
issue_activity.delay(
type="link.activity.created",
requested_data=json.dumps(
serializer.data, cls=DjangoJSONEncoder
),
requested_data=json.dumps(serializer.data, cls=DjangoJSONEncoder),
issue_id=str(self.kwargs.get("issue_id")),
project_id=str(self.kwargs.get("project_id")),
actor_id=str(link.created_by_id),
@@ -781,19 +697,13 @@ class IssueLinkAPIEndpoint(BaseAPIView):
def patch(self, request, slug, project_id, issue_id, pk):
issue_link = IssueLink.objects.get(
workspace__slug=slug,
project_id=project_id,
issue_id=issue_id,
pk=pk,
workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
)
requested_data = json.dumps(request.data, cls=DjangoJSONEncoder)
current_instance = json.dumps(
IssueLinkSerializer(issue_link).data,
cls=DjangoJSONEncoder,
)
serializer = IssueLinkSerializer(
issue_link, data=request.data, partial=True
IssueLinkSerializer(issue_link).data, cls=DjangoJSONEncoder
)
serializer = IssueLinkSerializer(issue_link, data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
issue_activity.delay(
@@ -810,14 +720,10 @@ class IssueLinkAPIEndpoint(BaseAPIView):
def delete(self, request, slug, project_id, issue_id, pk):
issue_link = IssueLink.objects.get(
workspace__slug=slug,
project_id=project_id,
issue_id=issue_id,
pk=pk,
workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
)
current_instance = json.dumps(
IssueLinkSerializer(issue_link).data,
cls=DjangoJSONEncoder,
IssueLinkSerializer(issue_link).data, cls=DjangoJSONEncoder
)
issue_activity.delay(
type="link.activity.deleted",
@@ -842,15 +748,11 @@ class IssueCommentAPIEndpoint(BaseAPIView):
serializer_class = IssueCommentSerializer
model = IssueComment
webhook_event = "issue_comment"
permission_classes = [
ProjectLitePermission,
]
permission_classes = [ProjectLitePermission]
def get_queryset(self):
return (
IssueComment.objects.filter(
workspace__slug=self.kwargs.get("slug")
)
IssueComment.objects.filter(workspace__slug=self.kwargs.get("slug"))
.filter(project_id=self.kwargs.get("project_id"))
.filter(issue_id=self.kwargs.get("issue_id"))
.filter(
@@ -877,19 +779,14 @@ class IssueCommentAPIEndpoint(BaseAPIView):
if pk:
issue_comment = self.get_queryset().get(pk=pk)
serializer = IssueCommentSerializer(
issue_comment,
fields=self.fields,
expand=self.expand,
issue_comment, fields=self.fields, expand=self.expand
)
return Response(serializer.data, status=status.HTTP_200_OK)
return self.paginate(
request=request,
queryset=(self.get_queryset()),
on_results=lambda issue_comment: IssueCommentSerializer(
issue_comment,
many=True,
fields=self.fields,
expand=self.expand,
issue_comment, many=True, fields=self.fields, expand=self.expand
).data,
)
@@ -922,17 +819,11 @@ class IssueCommentAPIEndpoint(BaseAPIView):
serializer = IssueCommentSerializer(data=request.data)
if serializer.is_valid():
serializer.save(
project_id=project_id,
issue_id=issue_id,
actor=request.user,
)
issue_comment = IssueComment.objects.get(
pk=serializer.data.get("id")
project_id=project_id, issue_id=issue_id, actor=request.user
)
issue_comment = IssueComment.objects.get(pk=serializer.data.get("id"))
# Update the created_at and the created_by and save the comment
issue_comment.created_at = request.data.get(
"created_at", timezone.now()
)
issue_comment.created_at = request.data.get("created_at", timezone.now())
issue_comment.created_by_id = request.data.get(
"created_by", request.user.id
)
@@ -940,9 +831,7 @@ class IssueCommentAPIEndpoint(BaseAPIView):
issue_activity.delay(
type="comment.activity.created",
requested_data=json.dumps(
serializer.data, cls=DjangoJSONEncoder
),
requested_data=json.dumps(serializer.data, cls=DjangoJSONEncoder),
actor_id=str(issue_comment.created_by_id),
issue_id=str(self.kwargs.get("issue_id")),
project_id=str(self.kwargs.get("project_id")),
@@ -954,24 +843,17 @@ class IssueCommentAPIEndpoint(BaseAPIView):
def patch(self, request, slug, project_id, issue_id, pk):
issue_comment = IssueComment.objects.get(
workspace__slug=slug,
project_id=project_id,
issue_id=issue_id,
pk=pk,
workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
)
requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder)
current_instance = json.dumps(
IssueCommentSerializer(issue_comment).data,
cls=DjangoJSONEncoder,
IssueCommentSerializer(issue_comment).data, cls=DjangoJSONEncoder
)
# Validation check if the issue already exists
if (
request.data.get("external_id")
and (
issue_comment.external_id
!= str(request.data.get("external_id"))
)
and (issue_comment.external_id != str(request.data.get("external_id")))
and IssueComment.objects.filter(
project_id=project_id,
workspace__slug=slug,
@@ -1008,14 +890,10 @@ class IssueCommentAPIEndpoint(BaseAPIView):
def delete(self, request, slug, project_id, issue_id, pk):
issue_comment = IssueComment.objects.get(
workspace__slug=slug,
project_id=project_id,
issue_id=issue_id,
pk=pk,
workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
)
current_instance = json.dumps(
IssueCommentSerializer(issue_comment).data,
cls=DjangoJSONEncoder,
IssueCommentSerializer(issue_comment).data, cls=DjangoJSONEncoder
)
issue_comment.delete()
issue_activity.delay(
@@ -1031,9 +909,7 @@ class IssueCommentAPIEndpoint(BaseAPIView):
class IssueActivityAPIEndpoint(BaseAPIView):
permission_classes = [
ProjectEntityPermission,
]
permission_classes = [ProjectEntityPermission]
def get(self, request, slug, project_id, issue_id, pk=None):
issue_activities = (
@@ -1058,89 +934,189 @@ class IssueActivityAPIEndpoint(BaseAPIView):
request=request,
queryset=(issue_activities),
on_results=lambda issue_activity: IssueActivitySerializer(
issue_activity,
many=True,
fields=self.fields,
expand=self.expand,
issue_activity, many=True, fields=self.fields, expand=self.expand
).data,
)
class IssueAttachmentEndpoint(BaseAPIView):
serializer_class = IssueAttachmentSerializer
permission_classes = [
ProjectEntityPermission,
]
permission_classes = [ProjectEntityPermission]
model = FileAsset
parser_classes = (MultiPartParser, FormParser)
def post(self, request, slug, project_id, issue_id):
serializer = IssueAttachmentSerializer(data=request.data)
name = request.data.get("name")
type = request.data.get("type", False)
size = request.data.get("size")
external_id = request.data.get("external_id")
external_source = request.data.get("external_source")
# Check if the request is valid
if not name or not size:
return Response(
{"error": "Invalid request.", "status": False},
status=status.HTTP_400_BAD_REQUEST,
)
size_limit = min(size, settings.FILE_SIZE_LIMIT)
if not type or type not in settings.ATTACHMENT_MIME_TYPES:
return Response(
{"error": "Invalid file type.", "status": False},
status=status.HTTP_400_BAD_REQUEST,
)
# Get the workspace
workspace = Workspace.objects.get(slug=slug)
# asset key
asset_key = f"{workspace.id}/{uuid.uuid4().hex}-{name}"
if (
request.data.get("external_id")
and request.data.get("external_source")
and FileAsset.objects.filter(
project_id=project_id,
workspace__slug=slug,
issue_id=issue_id,
external_source=request.data.get("external_source"),
external_id=request.data.get("external_id"),
issue_id=issue_id,
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
).exists()
):
issue_attachment = FileAsset.objects.filter(
workspace__slug=slug,
asset = FileAsset.objects.filter(
project_id=project_id,
external_id=request.data.get("external_id"),
workspace__slug=slug,
external_source=request.data.get("external_source"),
external_id=request.data.get("external_id"),
issue_id=issue_id,
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
).first()
return Response(
{
"error": "Issue attachment with the same external id and external source already exists",
"id": str(issue_attachment.id),
"error": "Issue with the same external id and external source already exists",
"id": str(asset.id),
},
status=status.HTTP_409_CONFLICT,
)
if serializer.is_valid():
serializer.save(project_id=project_id, issue_id=issue_id)
issue_activity.delay(
type="attachment.activity.created",
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(
serializer.data,
cls=DjangoJSONEncoder,
),
epoch=int(timezone.now().timestamp()),
notification=True,
origin=request.META.get("HTTP_ORIGIN"),
)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
# Create a File Asset
asset = FileAsset.objects.create(
attributes={"name": name, "type": type, "size": size_limit},
asset=asset_key,
size=size_limit,
workspace_id=workspace.id,
created_by=request.user,
issue_id=issue_id,
project_id=project_id,
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
external_id=external_id,
external_source=external_source,
)
# Get the presigned URL
storage = S3Storage(request=request)
# Generate a presigned URL to share an S3 object
presigned_url = storage.generate_presigned_post(
object_name=asset_key, file_type=type, file_size=size_limit
)
# Return the presigned URL
return Response(
{
"upload_data": presigned_url,
"asset_id": str(asset.id),
"attachment": IssueAttachmentSerializer(asset).data,
"asset_url": asset.asset_url,
},
status=status.HTTP_200_OK,
)
def delete(self, request, slug, project_id, issue_id, pk):
issue_attachment = FileAsset.objects.get(pk=pk)
issue_attachment.asset.delete(save=False)
issue_attachment.delete()
issue_attachment = FileAsset.objects.get(
pk=pk, workspace__slug=slug, project_id=project_id
)
issue_attachment.is_deleted = True
issue_attachment.deleted_at = timezone.now()
issue_attachment.save()
issue_activity.delay(
type="attachment.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)),
issue_id=str(issue_id),
project_id=str(project_id),
current_instance=None,
epoch=int(timezone.now().timestamp()),
notification=True,
origin=request.META.get("HTTP_ORIGIN"),
)
# Get the storage metadata
if not issue_attachment.storage_metadata:
get_asset_object_metadata.delay(str(issue_attachment.id))
issue_attachment.save()
return Response(status=status.HTTP_204_NO_CONTENT)
def get(self, request, slug, project_id, issue_id):
def get(self, request, slug, project_id, issue_id, pk=None):
if pk:
# Get the asset
asset = FileAsset.objects.get(
id=pk, workspace__slug=slug, project_id=project_id
)
# Check if the asset is uploaded
if not asset.is_uploaded:
return Response(
{"error": "The asset is not uploaded.", "status": False},
status=status.HTTP_400_BAD_REQUEST,
)
storage = S3Storage(request=request)
presigned_url = storage.generate_presigned_url(
object_name=asset.asset.name,
disposition="attachment",
filename=asset.attributes.get("name"),
)
return HttpResponseRedirect(presigned_url)
# Get all the attachments
issue_attachments = FileAsset.objects.filter(
issue_id=issue_id, workspace__slug=slug, project_id=project_id
issue_id=issue_id,
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
workspace__slug=slug,
project_id=project_id,
is_uploaded=True,
)
# Serialize the attachments
serializer = IssueAttachmentSerializer(issue_attachments, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
def patch(self, request, slug, project_id, issue_id, pk):
issue_attachment = FileAsset.objects.get(
pk=pk, workspace__slug=slug, project_id=project_id
)
serializer = IssueAttachmentSerializer(issue_attachment)
# Send this activity only if the attachment is not uploaded before
if not issue_attachment.is_uploaded:
issue_activity.delay(
type="attachment.activity.created",
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(serializer.data, cls=DjangoJSONEncoder),
epoch=int(timezone.now().timestamp()),
notification=True,
origin=request.META.get("HTTP_ORIGIN"),
)
# Update the attachment
issue_attachment.is_uploaded = True
issue_attachment.created_by = request.user
# Get the storage metadata
if not issue_attachment.storage_metadata:
get_asset_object_metadata.delay(str(issue_attachment.id))
issue_attachment.save()
return Response(status=status.HTTP_204_NO_CONTENT)

View File

@@ -13,24 +13,14 @@ from rest_framework import status
# Module imports
from .base import BaseAPIView
from plane.api.serializers import UserLiteSerializer
from plane.db.models import (
User,
Workspace,
Project,
WorkspaceMember,
ProjectMember,
)
from plane.db.models import User, Workspace, Project, WorkspaceMember, ProjectMember
from plane.app.permissions import (
ProjectMemberPermission,
)
from plane.app.permissions import ProjectMemberPermission
# API endpoint to get and insert users inside the workspace
class ProjectMemberAPIEndpoint(BaseAPIView):
permission_classes = [
ProjectMemberPermission,
]
permission_classes = [ProjectMemberPermission]
# Get all the users that are present inside the workspace
def get(self, request, slug, project_id):
@@ -48,10 +38,7 @@ class ProjectMemberAPIEndpoint(BaseAPIView):
# Get all the users that are present inside the workspace
users = UserLiteSerializer(
User.objects.filter(
id__in=project_members,
),
many=True,
User.objects.filter(id__in=project_members), many=True
).data
return Response(users, status=status.HTTP_200_OK)
@@ -78,8 +65,7 @@ class ProjectMemberAPIEndpoint(BaseAPIView):
validate_email(email)
except ValidationError:
return Response(
{"error": "Invalid email provided"},
status=status.HTTP_400_BAD_REQUEST,
{"error": "Invalid email provided"}, status=status.HTTP_400_BAD_REQUEST
)
workspace = Workspace.objects.filter(slug=slug).first()
@@ -108,9 +94,7 @@ class ProjectMemberAPIEndpoint(BaseAPIView):
).first()
if project_member:
return Response(
{
"error": "User is already part of the workspace and project"
},
{"error": "User is already part of the workspace and project"},
status=status.HTTP_400_BAD_REQUEST,
)
@@ -131,18 +115,14 @@ class ProjectMemberAPIEndpoint(BaseAPIView):
# Create a workspace member for the user if not already a member
if not workspace_member:
workspace_member = WorkspaceMember.objects.create(
workspace=workspace,
member=user,
role=request.data.get("role", 5),
workspace=workspace, member=user, role=request.data.get("role", 5)
)
workspace_member.save()
# Create a project member for the user if not already a member
if not project_member:
project_member = ProjectMember.objects.create(
project=project,
member=user,
role=request.data.get("role", 5),
project=project, member=user, role=request.data.get("role", 5)
)
project_member.save()
@@ -150,4 +130,3 @@ class ProjectMemberAPIEndpoint(BaseAPIView):
user_data = UserLiteSerializer(user).data
return Response(user_data, status=status.HTTP_201_CREATED)

View File

@@ -43,9 +43,7 @@ class ModuleAPIEndpoint(BaseAPIView):
"""
model = Module
permission_classes = [
ProjectEntityPermission,
]
permission_classes = [ProjectEntityPermission]
serializer_class = ModuleSerializer
webhook_event = "module"
@@ -60,9 +58,7 @@ class ModuleAPIEndpoint(BaseAPIView):
.prefetch_related(
Prefetch(
"link_module",
queryset=ModuleLink.objects.select_related(
"module", "created_by"
),
queryset=ModuleLink.objects.select_related("module", "created_by"),
)
)
.annotate(
@@ -74,7 +70,7 @@ class ModuleAPIEndpoint(BaseAPIView):
issue_module__deleted_at__isnull=True,
),
distinct=True,
),
)
)
.annotate(
completed_issues=Count(
@@ -143,10 +139,7 @@ class ModuleAPIEndpoint(BaseAPIView):
project = Project.objects.get(pk=project_id, workspace__slug=slug)
serializer = ModuleSerializer(
data=request.data,
context={
"project_id": project_id,
"workspace_id": project.workspace_id,
},
context={"project_id": project_id, "workspace_id": project.workspace_id},
)
if serializer.is_valid():
if (
@@ -189,9 +182,7 @@ class ModuleAPIEndpoint(BaseAPIView):
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def patch(self, request, slug, project_id, pk):
module = Module.objects.get(
pk=pk, project_id=project_id, workspace__slug=slug
)
module = Module.objects.get(pk=pk, project_id=project_id, workspace__slug=slug)
current_instance = json.dumps(
ModuleSerializer(module).data, cls=DjangoJSONEncoder
@@ -203,10 +194,7 @@ class ModuleAPIEndpoint(BaseAPIView):
status=status.HTTP_400_BAD_REQUEST,
)
serializer = ModuleSerializer(
module,
data=request.data,
context={"project_id": project_id},
partial=True,
module, data=request.data, context={"project_id": project_id}, partial=True
)
if serializer.is_valid():
if (
@@ -246,33 +234,21 @@ class ModuleAPIEndpoint(BaseAPIView):
def get(self, request, slug, project_id, pk=None):
if pk:
queryset = (
self.get_queryset().filter(archived_at__isnull=True).get(pk=pk)
)
queryset = self.get_queryset().filter(archived_at__isnull=True).get(pk=pk)
data = ModuleSerializer(
queryset,
fields=self.fields,
expand=self.expand,
queryset, fields=self.fields, expand=self.expand
).data
return Response(
data,
status=status.HTTP_200_OK,
)
return Response(data, status=status.HTTP_200_OK)
return self.paginate(
request=request,
queryset=(self.get_queryset().filter(archived_at__isnull=True)),
on_results=lambda modules: ModuleSerializer(
modules,
many=True,
fields=self.fields,
expand=self.expand,
modules, many=True, fields=self.fields, expand=self.expand
).data,
)
def delete(self, request, slug, project_id, pk):
module = Module.objects.get(
workspace__slug=slug, project_id=project_id, pk=pk
)
module = Module.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
if module.created_by_id != request.user.id and (
not ProjectMember.objects.filter(
workspace__slug=slug,
@@ -288,9 +264,7 @@ class ModuleAPIEndpoint(BaseAPIView):
)
module_issues = list(
ModuleIssue.objects.filter(module_id=pk).values_list(
"issue", flat=True
)
ModuleIssue.objects.filter(module_id=pk).values_list("issue", flat=True)
)
issue_activity.delay(
type="module.activity.deleted",
@@ -304,24 +278,15 @@ class ModuleAPIEndpoint(BaseAPIView):
actor_id=str(request.user.id),
issue_id=None,
project_id=str(project_id),
current_instance=json.dumps(
{
"module_name": str(module.name),
}
),
current_instance=json.dumps({"module_name": str(module.name)}),
epoch=int(timezone.now().timestamp()),
)
module.delete()
# Delete the module issues
ModuleIssue.objects.filter(
module=pk,
project_id=project_id,
).delete()
ModuleIssue.objects.filter(module=pk, project_id=project_id).delete()
# Delete the user favorite module
UserFavorite.objects.filter(
entity_type="module",
entity_identifier=pk,
project_id=project_id,
entity_type="module", entity_identifier=pk, project_id=project_id
).delete()
return Response(status=status.HTTP_204_NO_CONTENT)
@@ -338,16 +303,12 @@ class ModuleIssueAPIEndpoint(BaseAPIView):
webhook_event = "module_issue"
bulk = True
permission_classes = [
ProjectEntityPermission,
]
permission_classes = [ProjectEntityPermission]
def get_queryset(self):
return (
ModuleIssue.objects.annotate(
sub_issues_count=Issue.issue_objects.filter(
parent=OuterRef("issue")
)
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("issue"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
@@ -374,13 +335,10 @@ class ModuleIssueAPIEndpoint(BaseAPIView):
order_by = request.GET.get("order_by", "created_at")
issues = (
Issue.issue_objects.filter(
issue_module__module_id=module_id,
issue_module__deleted_at__isnull=True,
issue_module__module_id=module_id, issue_module__deleted_at__isnull=True
)
.annotate(
sub_issues_count=Issue.issue_objects.filter(
parent=OuterRef("id")
)
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
@@ -415,10 +373,7 @@ class ModuleIssueAPIEndpoint(BaseAPIView):
request=request,
queryset=(issues),
on_results=lambda issues: IssueSerializer(
issues,
many=True,
fields=self.fields,
expand=self.expand,
issues, many=True, fields=self.fields, expand=self.expand
).data,
)
@@ -426,8 +381,7 @@ class ModuleIssueAPIEndpoint(BaseAPIView):
issues = request.data.get("issues", [])
if not len(issues):
return Response(
{"error": "Issues are required"},
status=status.HTTP_400_BAD_REQUEST,
{"error": "Issues are required"}, status=status.HTTP_400_BAD_REQUEST
)
module = Module.objects.get(
workspace__slug=slug, project_id=project_id, pk=module_id
@@ -474,16 +428,10 @@ class ModuleIssueAPIEndpoint(BaseAPIView):
)
ModuleIssue.objects.bulk_create(
record_to_create,
batch_size=10,
ignore_conflicts=True,
record_to_create, batch_size=10, ignore_conflicts=True
)
ModuleIssue.objects.bulk_update(
records_to_update,
["module"],
batch_size=10,
)
ModuleIssue.objects.bulk_update(records_to_update, ["module"], batch_size=10)
# Capture Issue Activity
issue_activity.delay(
@@ -519,10 +467,7 @@ class ModuleIssueAPIEndpoint(BaseAPIView):
issue_activity.delay(
type="module.activity.deleted",
requested_data=json.dumps(
{
"module_id": str(module_id),
"issues": [str(module_issue.issue_id)],
}
{"module_id": str(module_id), "issues": [str(module_issue.issue_id)]}
),
actor_id=str(request.user.id),
issue_id=str(issue_id),
@@ -534,9 +479,7 @@ class ModuleIssueAPIEndpoint(BaseAPIView):
class ModuleArchiveUnarchiveAPIEndpoint(BaseAPIView):
permission_classes = [
ProjectEntityPermission,
]
permission_classes = [ProjectEntityPermission]
def get_queryset(self):
return (
@@ -550,9 +493,7 @@ class ModuleArchiveUnarchiveAPIEndpoint(BaseAPIView):
.prefetch_related(
Prefetch(
"link_module",
queryset=ModuleLink.objects.select_related(
"module", "created_by"
),
queryset=ModuleLink.objects.select_related("module", "created_by"),
)
)
.annotate(
@@ -564,7 +505,7 @@ class ModuleArchiveUnarchiveAPIEndpoint(BaseAPIView):
issue_module__deleted_at__isnull=True,
),
distinct=True,
),
)
)
.annotate(
completed_issues=Count(
@@ -634,22 +575,15 @@ class ModuleArchiveUnarchiveAPIEndpoint(BaseAPIView):
request=request,
queryset=(self.get_queryset()),
on_results=lambda modules: ModuleSerializer(
modules,
many=True,
fields=self.fields,
expand=self.expand,
modules, many=True, fields=self.fields, expand=self.expand
).data,
)
def post(self, request, slug, project_id, pk):
module = Module.objects.get(
pk=pk, project_id=project_id, workspace__slug=slug
)
module = Module.objects.get(pk=pk, project_id=project_id, workspace__slug=slug)
if module.status not in ["completed", "cancelled"]:
return Response(
{
"error": "Only completed or cancelled modules can be archived"
},
{"error": "Only completed or cancelled modules can be archived"},
status=status.HTTP_400_BAD_REQUEST,
)
module.archived_at = timezone.now()
@@ -663,9 +597,7 @@ class ModuleArchiveUnarchiveAPIEndpoint(BaseAPIView):
return Response(status=status.HTTP_204_NO_CONTENT)
def delete(self, request, slug, project_id, pk):
module = Module.objects.get(
pk=pk, project_id=project_id, workspace__slug=slug
)
module = Module.objects.get(pk=pk, project_id=project_id, workspace__slug=slug)
module.archived_at = None
module.save()
return Response(status=status.HTTP_204_NO_CONTENT)

View File

@@ -39,9 +39,7 @@ class ProjectAPIEndpoint(BaseAPIView):
model = Project
webhook_event = "project"
permission_classes = [
ProjectBasePermission,
]
permission_classes = [ProjectBasePermission]
def get_queryset(self):
return (
@@ -54,10 +52,7 @@ class ProjectAPIEndpoint(BaseAPIView):
| Q(network=2)
)
.select_related(
"workspace",
"workspace__owner",
"default_assignee",
"project_lead",
"workspace", "workspace__owner", "default_assignee", "project_lead"
)
.annotate(
is_member=Exists(
@@ -71,9 +66,7 @@ class ProjectAPIEndpoint(BaseAPIView):
)
.annotate(
total_members=ProjectMember.objects.filter(
project_id=OuterRef("id"),
member__is_bot=False,
is_active=True,
project_id=OuterRef("id"), member__is_bot=False, is_active=True
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
@@ -125,8 +118,7 @@ class ProjectAPIEndpoint(BaseAPIView):
Prefetch(
"project_projectmember",
queryset=ProjectMember.objects.filter(
workspace__slug=slug,
is_active=True,
workspace__slug=slug, is_active=True
).select_related("member"),
)
)
@@ -136,18 +128,11 @@ class ProjectAPIEndpoint(BaseAPIView):
request=request,
queryset=(projects),
on_results=lambda projects: ProjectSerializer(
projects,
many=True,
fields=self.fields,
expand=self.expand,
projects, many=True, fields=self.fields, expand=self.expand
).data,
)
project = self.get_queryset().get(workspace__slug=slug, pk=pk)
serializer = ProjectSerializer(
project,
fields=self.fields,
expand=self.expand,
)
serializer = ProjectSerializer(project, fields=self.fields, expand=self.expand)
return Response(serializer.data, status=status.HTTP_200_OK)
def post(self, request, slug):
@@ -161,14 +146,11 @@ class ProjectAPIEndpoint(BaseAPIView):
# Add the user as Administrator to the project
_ = ProjectMember.objects.create(
project_id=serializer.data["id"],
member=request.user,
role=20,
project_id=serializer.data["id"], member=request.user, role=20
)
# Also create the issue property for the user
_ = IssueUserProperty.objects.create(
project_id=serializer.data["id"],
user=request.user,
project_id=serializer.data["id"], user=request.user
)
if serializer.data["project_lead"] is not None and str(
@@ -236,11 +218,7 @@ class ProjectAPIEndpoint(BaseAPIView):
]
)
project = (
self.get_queryset()
.filter(pk=serializer.data["id"])
.first()
)
project = self.get_queryset().filter(pk=serializer.data["id"]).first()
# Model activity
model_activity.delay(
@@ -254,13 +232,8 @@ class ProjectAPIEndpoint(BaseAPIView):
)
serializer = ProjectSerializer(project)
return Response(
serializer.data, status=status.HTTP_201_CREATED
)
return Response(
serializer.errors,
status=status.HTTP_400_BAD_REQUEST,
)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except IntegrityError as e:
if "already exists" in str(e):
return Response(
@@ -269,8 +242,7 @@ class ProjectAPIEndpoint(BaseAPIView):
)
except Workspace.DoesNotExist:
return Response(
{"error": "Workspace does not exist"},
status=status.HTTP_404_NOT_FOUND,
{"error": "Workspace does not exist"}, status=status.HTTP_404_NOT_FOUND
)
except ValidationError:
return Response(
@@ -287,7 +259,7 @@ class ProjectAPIEndpoint(BaseAPIView):
)
intake_view = request.data.get(
"inbox_view", request.data.get("intake_view", False)
"inbox_view", request.data.get("intake_view", project.intake_view)
)
if project.archived_at:
@@ -298,10 +270,7 @@ class ProjectAPIEndpoint(BaseAPIView):
serializer = ProjectSerializer(
project,
data={
**request.data,
"intake_view": intake_view,
},
data={**request.data, "intake_view": intake_view},
context={"workspace_id": workspace.id},
partial=True,
)
@@ -310,8 +279,7 @@ class ProjectAPIEndpoint(BaseAPIView):
serializer.save()
if serializer.data["intake_view"]:
intake = Intake.objects.filter(
project=project,
is_default=True,
project=project, is_default=True
).first()
if not intake:
Intake.objects.create(
@@ -330,11 +298,7 @@ class ProjectAPIEndpoint(BaseAPIView):
is_triage=True,
)
project = (
self.get_queryset()
.filter(pk=serializer.data["id"])
.first()
)
project = self.get_queryset().filter(pk=serializer.data["id"]).first()
model_activity.delay(
model_name="project",
@@ -348,9 +312,7 @@ class ProjectAPIEndpoint(BaseAPIView):
serializer = ProjectSerializer(project)
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(
serializer.errors, status=status.HTTP_400_BAD_REQUEST
)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except IntegrityError as e:
if "already exists" in str(e):
return Response(
@@ -359,8 +321,7 @@ class ProjectAPIEndpoint(BaseAPIView):
)
except (Project.DoesNotExist, Workspace.DoesNotExist):
return Response(
{"error": "Project does not exist"},
status=status.HTTP_404_NOT_FOUND,
{"error": "Project does not exist"}, status=status.HTTP_404_NOT_FOUND
)
except ValidationError:
return Response(
@@ -372,28 +333,20 @@ class ProjectAPIEndpoint(BaseAPIView):
project = Project.objects.get(pk=pk, workspace__slug=slug)
# Delete the user favorite cycle
UserFavorite.objects.filter(
entity_type="project",
entity_identifier=pk,
project_id=pk,
entity_type="project", entity_identifier=pk, project_id=pk
).delete()
project.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
class ProjectArchiveUnarchiveAPIEndpoint(BaseAPIView):
permission_classes = [
ProjectBasePermission,
]
permission_classes = [ProjectBasePermission]
def post(self, request, slug, project_id):
project = Project.objects.get(pk=project_id, workspace__slug=slug)
project.archived_at = timezone.now()
project.save()
UserFavorite.objects.filter(
workspace__slug=slug,
project=project_id,
).delete()
UserFavorite.objects.filter(workspace__slug=slug, project=project_id).delete()
return Response(status=status.HTTP_204_NO_CONTENT)
def delete(self, request, slug, project_id):

View File

@@ -16,9 +16,7 @@ from .base import BaseAPIView
class StateAPIEndpoint(BaseAPIView):
serializer_class = StateSerializer
model = State
permission_classes = [
ProjectEntityPermission,
]
permission_classes = [ProjectEntityPermission]
def get_queryset(self):
return (
@@ -67,9 +65,7 @@ class StateAPIEndpoint(BaseAPIView):
serializer.save(project_id=project_id)
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(
serializer.errors, status=status.HTTP_400_BAD_REQUEST
)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except IntegrityError:
state = State.objects.filter(
workspace__slug=slug,
@@ -96,19 +92,13 @@ class StateAPIEndpoint(BaseAPIView):
request=request,
queryset=(self.get_queryset()),
on_results=lambda states: StateSerializer(
states,
many=True,
fields=self.fields,
expand=self.expand,
states, many=True, fields=self.fields, expand=self.expand
).data,
)
def delete(self, request, slug, project_id, state_id):
state = State.objects.get(
is_triage=False,
pk=state_id,
project_id=project_id,
workspace__slug=slug,
is_triage=False, pk=state_id, project_id=project_id, workspace__slug=slug
)
if state.default:
@@ -122,9 +112,7 @@ class StateAPIEndpoint(BaseAPIView):
if issue_exist:
return Response(
{
"error": "The state is not empty, only empty states can be deleted"
},
{"error": "The state is not empty, only empty states can be deleted"},
status=status.HTTP_400_BAD_REQUEST,
)

View File

@@ -25,10 +25,7 @@ class APIKeyAuthentication(authentication.BaseAuthentication):
def validate_api_token(self, token):
try:
api_token = APIToken.objects.get(
Q(
Q(expired_at__gt=timezone.now())
| Q(expired_at__isnull=True)
),
Q(Q(expired_at__gt=timezone.now()) | Q(expired_at__isnull=True)),
token=token,
is_active=True,
)

View File

@@ -12,4 +12,4 @@ from .project import (
ProjectMemberPermission,
ProjectLitePermission,
)
from .base import allow_permission, ROLE
from .base import allow_permission, ROLE

View File

@@ -5,6 +5,7 @@ from rest_framework import status
from enum import Enum
class ROLE(Enum):
ADMIN = 20
MEMBER = 15
@@ -15,7 +16,6 @@ def allow_permission(allowed_roles, level="PROJECT", creator=False, model=None):
def decorator(view_func):
@wraps(view_func)
def _wrapped_view(instance, request, *args, **kwargs):
# Check for creator if required
if creator and model:
obj = model.objects.filter(
@@ -26,8 +26,7 @@ def allow_permission(allowed_roles, level="PROJECT", creator=False, model=None):
# Convert allowed_roles to their values if they are enum members
allowed_role_values = [
role.value if isinstance(role, ROLE) else role
for role in allowed_roles
role.value if isinstance(role, ROLE) else role for role in allowed_roles
]
# Check role permissions

View File

@@ -18,9 +18,7 @@ class ProjectBasePermission(BasePermission):
## Safe Methods -> Handle the filtering logic in queryset
if request.method in SAFE_METHODS:
return WorkspaceMember.objects.filter(
workspace__slug=view.workspace_slug,
member=request.user,
is_active=True,
workspace__slug=view.workspace_slug, member=request.user, is_active=True
).exists()
## Only workspace owners or admins can create the projects
@@ -50,9 +48,7 @@ class ProjectMemberPermission(BasePermission):
## Safe Methods -> Handle the filtering logic in queryset
if request.method in SAFE_METHODS:
return ProjectMember.objects.filter(
workspace__slug=view.workspace_slug,
member=request.user,
is_active=True,
workspace__slug=view.workspace_slug, member=request.user, is_active=True
).exists()
## Only workspace owners or admins can create the projects
if request.method == "POST":

View File

@@ -50,9 +50,7 @@ class WorkspaceOwnerPermission(BasePermission):
return False
return WorkspaceMember.objects.filter(
workspace__slug=view.workspace_slug,
member=request.user,
role=Admin,
workspace__slug=view.workspace_slug, member=request.user, role=Admin
).exists()
@@ -77,9 +75,7 @@ class WorkspaceEntityPermission(BasePermission):
## Safe Methods -> Handle the filtering logic in queryset
if request.method in SAFE_METHODS:
return WorkspaceMember.objects.filter(
workspace__slug=view.workspace_slug,
member=request.user,
is_active=True,
workspace__slug=view.workspace_slug, member=request.user, is_active=True
).exists()
return WorkspaceMember.objects.filter(
@@ -96,9 +92,7 @@ class WorkspaceViewerPermission(BasePermission):
return False
return WorkspaceMember.objects.filter(
member=request.user,
workspace__slug=view.workspace_slug,
is_active=True,
member=request.user, workspace__slug=view.workspace_slug, is_active=True
).exists()
@@ -108,7 +102,5 @@ class WorkspaceUserPermission(BasePermission):
return False
return WorkspaceMember.objects.filter(
member=request.user,
workspace__slug=view.workspace_slug,
is_active=True,
member=request.user, workspace__slug=view.workspace_slug, is_active=True
).exists()

View File

@@ -13,13 +13,14 @@ from .user import (
from .workspace import (
WorkSpaceSerializer,
WorkSpaceMemberSerializer,
TeamSerializer,
WorkSpaceMemberInviteSerializer,
WorkspaceLiteSerializer,
WorkspaceThemeSerializer,
WorkspaceMemberAdminSerializer,
WorkspaceMemberMeSerializer,
WorkspaceUserPropertiesSerializer,
WorkspaceUserLinkSerializer,
WorkspaceRecentVisitSerializer
)
from .project import (
ProjectSerializer,
@@ -36,9 +37,7 @@ from .project import (
ProjectMemberRoleSerializer,
)
from .state import StateSerializer, StateLiteSerializer
from .view import (
IssueViewSerializer,
)
from .view import IssueViewSerializer
from .cycle import (
CycleSerializer,
CycleIssueSerializer,
@@ -112,10 +111,7 @@ from .intake import (
from .analytic import AnalyticViewSerializer
from .notification import (
NotificationSerializer,
UserNotificationPreferenceSerializer,
)
from .notification import NotificationSerializer, UserNotificationPreferenceSerializer
from .exporter import ExporterHistorySerializer

View File

@@ -7,10 +7,7 @@ class AnalyticViewSerializer(BaseSerializer):
class Meta:
model = AnalyticView
fields = "__all__"
read_only_fields = [
"workspace",
"query",
]
read_only_fields = ["workspace", "query"]
def create(self, validated_data):
query_params = validated_data.get("query_dict", {})

View File

@@ -6,9 +6,4 @@ class FileAssetSerializer(BaseSerializer):
class Meta:
model = FileAsset
fields = "__all__"
read_only_fields = [
"created_by",
"updated_by",
"created_at",
"updated_at",
]
read_only_fields = ["created_by", "updated_by", "created_at", "updated_at"]

View File

@@ -178,15 +178,10 @@ class DynamicBaseSerializer(BaseSerializer):
response[expand] = exp_serializer.data
else:
# You might need to handle this case differently
response[expand] = getattr(
instance, f"{expand}_id", None
)
response[expand] = getattr(instance, f"{expand}_id", None)
# Check if issue_attachments is in fields or expand
if (
"issue_attachments" in self.fields
or "issue_attachments" in self.expand
):
if "issue_attachments" in self.fields or "issue_attachments" in self.expand:
# Import the model here to avoid circular imports
from plane.db.models import FileAsset
@@ -199,11 +194,9 @@ class DynamicBaseSerializer(BaseSerializer):
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
)
# Serialize issue_attachments and add them to the response
response["issue_attachments"] = (
IssueAttachmentLiteSerializer(
issue_attachments, many=True
).data
)
response["issue_attachments"] = IssueAttachmentLiteSerializer(
issue_attachments, many=True
).data
else:
response["issue_attachments"] = []

View File

@@ -4,11 +4,8 @@ from rest_framework import serializers
# Module imports
from .base import BaseSerializer
from .issue import IssueStateSerializer
from plane.db.models import (
Cycle,
CycleIssue,
CycleUserProperties,
)
from plane.db.models import Cycle, CycleIssue, CycleUserProperties
from plane.utils.timezone_converter import convert_to_utc
class CycleWriteSerializer(BaseSerializer):
@@ -18,20 +15,24 @@ class CycleWriteSerializer(BaseSerializer):
and data.get("end_date", None) is not None
and data.get("start_date", None) > data.get("end_date", None)
):
raise serializers.ValidationError(
"Start date cannot exceed end date"
raise serializers.ValidationError("Start date cannot exceed end date")
if (
data.get("start_date", None) is not None
and data.get("end_date", None) is not None
):
project_id = self.initial_data.get("project_id") or self.instance.project_id
data["start_date"] = convert_to_utc(
str(data.get("start_date").date()), project_id, is_start_date=True
)
data["end_date"] = convert_to_utc(
str(data.get("end_date", None).date()), project_id
)
return data
class Meta:
model = Cycle
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"owned_by",
"archived_at",
]
read_only_fields = ["workspace", "project", "owned_by", "archived_at"]
class CycleSerializer(BaseSerializer):
@@ -87,18 +88,11 @@ class CycleIssueSerializer(BaseSerializer):
class Meta:
model = CycleIssue
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"cycle",
]
read_only_fields = ["workspace", "project", "cycle"]
class CycleUserPropertiesSerializer(BaseSerializer):
class Meta:
model = CycleUserProperties
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"cycle" "user",
]
read_only_fields = ["workspace", "project", "cycle" "user"]

View File

@@ -22,16 +22,10 @@ from plane.db.models import (
class DraftIssueCreateSerializer(BaseSerializer):
# ids
state_id = serializers.PrimaryKeyRelatedField(
source="state",
queryset=State.objects.all(),
required=False,
allow_null=True,
source="state", queryset=State.objects.all(), required=False, allow_null=True
)
parent_id = serializers.PrimaryKeyRelatedField(
source="parent",
queryset=Issue.objects.all(),
required=False,
allow_null=True,
source="parent", queryset=Issue.objects.all(), required=False, allow_null=True
)
label_ids = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=Label.objects.all()),
@@ -69,9 +63,7 @@ class DraftIssueCreateSerializer(BaseSerializer):
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"
)
raise serializers.ValidationError("Start date cannot exceed target date")
return data
def create(self, validated_data):
@@ -86,9 +78,7 @@ class DraftIssueCreateSerializer(BaseSerializer):
# Create Issue
issue = DraftIssue.objects.create(
**validated_data,
workspace_id=workspace_id,
project_id=project_id,
**validated_data, workspace_id=workspace_id, project_id=project_id
)
# Issue Audit Users
@@ -239,20 +229,11 @@ class DraftIssueCreateSerializer(BaseSerializer):
class DraftIssueSerializer(BaseSerializer):
# ids
cycle_id = serializers.PrimaryKeyRelatedField(read_only=True)
module_ids = serializers.ListField(
child=serializers.UUIDField(),
required=False,
)
module_ids = serializers.ListField(child=serializers.UUIDField(), required=False)
# Many to many
label_ids = serializers.ListField(
child=serializers.UUIDField(),
required=False,
)
assignee_ids = serializers.ListField(
child=serializers.UUIDField(),
required=False,
)
label_ids = serializers.ListField(child=serializers.UUIDField(), required=False)
assignee_ids = serializers.ListField(child=serializers.UUIDField(), required=False)
class Meta:
model = DraftIssue
@@ -286,7 +267,5 @@ class DraftIssueDetailSerializer(DraftIssueSerializer):
description_html = serializers.CharField()
class Meta(DraftIssueSerializer.Meta):
fields = DraftIssueSerializer.Meta.fields + [
"description_html",
]
fields = DraftIssueSerializer.Meta.fields + ["description_html"]
read_only_fields = fields

View File

@@ -7,14 +7,10 @@ from rest_framework import serializers
class EstimateSerializer(BaseSerializer):
class Meta:
model = Estimate
fields = "__all__"
read_only_fields = [
"workspace",
"project",
]
read_only_fields = ["workspace", "project"]
class EstimatePointSerializer(BaseSerializer):
@@ -23,19 +19,13 @@ class EstimatePointSerializer(BaseSerializer):
raise serializers.ValidationError("Estimate points are required")
value = data.get("value")
if value and len(value) > 20:
raise serializers.ValidationError(
"Value can't be more than 20 characters"
)
raise serializers.ValidationError("Value can't be more than 20 characters")
return data
class Meta:
model = EstimatePoint
fields = "__all__"
read_only_fields = [
"estimate",
"workspace",
"project",
]
read_only_fields = ["estimate", "workspace", "project"]
class EstimateReadSerializer(BaseSerializer):
@@ -44,11 +34,7 @@ class EstimateReadSerializer(BaseSerializer):
class Meta:
model = Estimate
fields = "__all__"
read_only_fields = [
"points",
"name",
"description",
]
read_only_fields = ["points", "name", "description"]
class WorkspaceEstimateSerializer(BaseSerializer):
@@ -57,8 +43,4 @@ class WorkspaceEstimateSerializer(BaseSerializer):
class Meta:
model = Estimate
fields = "__all__"
read_only_fields = [
"points",
"name",
"description",
]
read_only_fields = ["points", "name", "description"]

View File

@@ -5,9 +5,7 @@ from .user import UserLiteSerializer
class ExporterHistorySerializer(BaseSerializer):
initiated_by_detail = UserLiteSerializer(
source="initiated_by", read_only=True
)
initiated_by_detail = UserLiteSerializer(source="initiated_by", read_only=True)
class Meta:
model = ExporterHistory

View File

@@ -1,18 +1,9 @@
from rest_framework import serializers
from plane.db.models import (
UserFavorite,
Cycle,
Module,
Issue,
IssueView,
Page,
Project,
)
from plane.db.models import UserFavorite, Cycle, Module, Issue, IssueView, Page, Project
class ProjectFavoriteLiteSerializer(serializers.ModelSerializer):
class Meta:
model = Project
fields = ["id", "name", "logo_props"]
@@ -33,21 +24,18 @@ class PageFavoriteLiteSerializer(serializers.ModelSerializer):
class CycleFavoriteLiteSerializer(serializers.ModelSerializer):
class Meta:
model = Cycle
fields = ["id", "name", "logo_props", "project_id"]
class ModuleFavoriteLiteSerializer(serializers.ModelSerializer):
class Meta:
model = Module
fields = ["id", "name", "logo_props", "project_id"]
class ViewFavoriteSerializer(serializers.ModelSerializer):
class Meta:
model = IssueView
fields = ["id", "name", "logo_props", "project_id"]
@@ -65,7 +53,6 @@ def get_entity_model_and_serializer(entity_type):
}
return entity_map.get(entity_type, (None, None))
class UserFavoriteSerializer(serializers.ModelSerializer):
entity_data = serializers.SerializerMethodField()
@@ -89,9 +76,7 @@ class UserFavoriteSerializer(serializers.ModelSerializer):
entity_type = obj.entity_type
entity_identifier = obj.entity_identifier
entity_model, entity_serializer = get_entity_model_and_serializer(
entity_type
)
entity_model, entity_serializer = get_entity_model_and_serializer(entity_type)
if entity_model and entity_serializer:
try:
entity = entity_model.objects.get(pk=entity_identifier)

View File

@@ -7,13 +7,9 @@ from plane.db.models import Importer
class ImporterSerializer(BaseSerializer):
initiated_by_detail = UserLiteSerializer(
source="initiated_by", read_only=True
)
initiated_by_detail = UserLiteSerializer(source="initiated_by", read_only=True)
project_detail = ProjectLiteSerializer(source="project", read_only=True)
workspace_detail = WorkspaceLiteSerializer(
source="workspace", read_only=True
)
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
class Meta:
model = Importer

View File

@@ -3,11 +3,7 @@ from rest_framework import serializers
# Module imports
from .base import BaseSerializer
from .issue import (
IssueIntakeSerializer,
LabelLiteSerializer,
IssueDetailSerializer,
)
from .issue import IssueIntakeSerializer, LabelLiteSerializer, IssueDetailSerializer
from .project import ProjectLiteSerializer
from .state import StateLiteSerializer
from .user import UserLiteSerializer
@@ -21,10 +17,7 @@ class IntakeSerializer(BaseSerializer):
class Meta:
model = Intake
fields = "__all__"
read_only_fields = [
"project",
"workspace",
]
read_only_fields = ["project", "workspace"]
class IntakeIssueSerializer(BaseSerializer):
@@ -41,10 +34,7 @@ class IntakeIssueSerializer(BaseSerializer):
"issue",
"created_by",
]
read_only_fields = [
"project",
"workspace",
]
read_only_fields = ["project", "workspace"]
def to_representation(self, instance):
# Pass the annotated fields to the Issue instance if they exist
@@ -70,10 +60,7 @@ class IntakeIssueDetailSerializer(BaseSerializer):
"source",
"issue",
]
read_only_fields = [
"project",
"workspace",
]
read_only_fields = ["project", "workspace"]
def to_representation(self, instance):
# Pass the annotated fields to the Issue instance if they exist
@@ -95,12 +82,8 @@ class IntakeIssueLiteSerializer(BaseSerializer):
class IssueStateIntakeSerializer(BaseSerializer):
state_detail = StateLiteSerializer(read_only=True, source="state")
project_detail = ProjectLiteSerializer(read_only=True, source="project")
label_details = LabelLiteSerializer(
read_only=True, source="labels", many=True
)
assignee_details = UserLiteSerializer(
read_only=True, source="assignees", many=True
)
label_details = LabelLiteSerializer(read_only=True, source="labels", many=True)
assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True)
sub_issues_count = serializers.IntegerField(read_only=True)
issue_intake = IntakeIssueLiteSerializer(read_only=True, many=True)

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